From 838d05ba42a8796d58f099b96bd1b66e60020158 Mon Sep 17 00:00:00 2001 From: Dan Keder Date: Tue, 30 Sep 2014 13:14:21 +0200 Subject: [PATCH 0001/2522] Add module system/seport.py Module for managing SELinux network port type definitions --- system/seport.py | 254 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 system/seport.py diff --git a/system/seport.py b/system/seport.py new file mode 100644 index 00000000000..0770b7e69ee --- /dev/null +++ b/system/seport.py @@ -0,0 +1,254 @@ +#!/usr/bin/python + +# (c) 2014, Dan Keder +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: seport +short_description: Manages SELinux network port type definitions +description: + - Manages SELinux network port type definitions. +version_added: "1.7.1" +options: + port: + description: + - Port number or port range + required: true + default: null + proto: + description: + - Protocol for the specified port. + required: true + default: null + choices: [ 'tcp', 'udp' ] + setype: + description: + - SELinux type for the specified port. + required: true + default: null + state: + description: + - Desired boolean value. + required: true + default: present + choices: [ 'present', 'absent' ] + reload: + description: + - Reload SELinux policy after commit. + required: false + default: yes +notes: + - The changes are persistent across reboots + - Not tested on any debian based system +requirements: [ 'libselinux-python', 'policycoreutils-python' ] +author: Dan Keder +''' + +EXAMPLES = ''' +# Allow Apache to listen on tcp port 8888 +- seport: port=8888 proto=tcp setype=http_port_t state=present +# Allow sshd to listen on tcp port 8991 +- seport: port=8991 proto=tcp setype=ssh_port_t state=present +# Allow memcached to listen on tcp ports 10000-10100 +- seport: port=10000-10100 proto=tcp setype=memcache_port_t state=present +''' + +try: + import selinux + HAVE_SELINUX=True +except ImportError: + HAVE_SELINUX=False + +try: + import seobject + HAVE_SEOBJECT=True +except ImportError: + HAVE_SEOBJECT=False + + +def semanage_port_exists(seport, port, proto): + """ Get the SELinux port type definition from policy. Return None if it does + not exist. + + :param seport: Instance of seobject.portRecords + + :type port: basestring + :param port: Port or port range (example: "8080", "8080-9090") + + :type proto: basestring + :param proto: Protocol ('tcp' or 'udp') + + :rtype: bool + :return: True if the SELinux port type definition exists, False otherwise + """ + ports = port.split('-', 1) + if len(ports) == 1: + ports.extend(ports) + ports = map(int, ports) + record = (ports[0], ports[1], proto) + return record in seport.get_all() + + +def semanage_port_add(module, port, proto, setype, do_reload, serange='s0', sestore=''): + """ Add SELinux port type definition to the policy. + + :type module: AnsibleModule + :param module: Ansible module + + :type port: basestring + :param port: Port or port range to add (example: "8080", "8080-9090") + + :type proto: basestring + :param proto: Protocol ('tcp' or 'udp') + + :type setype: basestring + :param setype: SELinux type + + :type do_reload: bool + :param do_reload: Whether to reload SELinux policy after commit + + :type serange: basestring + :param serange: SELinux MLS/MCS range (defaults to 's0') + + :type sestore: basestring + :param sestore: SELinux store + + :rtype: bool + :return: True if the policy was changed, otherwise False + """ + try: + seport = seobject.portRecords(sestore) + change = not semanage_port_exists(seport, port, proto) + if change and not module.check_mode: + seport.set_reload(do_reload) + seport.add(port, proto, serange, setype) + + except ValueError as e: + module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) + except IOError as e: + module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) + except KeyError as e: + module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) + except OSError as e: + module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) + except RuntimeError as e: + module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) + + return change + + +def semanage_port_del(module, port, proto, do_reload, sestore=''): + """ Delete SELinux port type definition from the policy. + + :type module: AnsibleModule + :param module: Ansible module + + :type port: basestring + :param port: Port or port range to delete (example: "8080", "8080-9090") + + :type proto: basestring + :param proto: Protocol ('tcp' or 'udp') + + :type do_reload: bool + :param do_reload: Whether to reload SELinux policy after commit + + :type sestore: basestring + :param sestore: SELinux store + + :rtype: bool + :return: True if the policy was changed, otherwise False + """ + try: + seport = seobject.portRecords(sestore) + change = not semanage_port_exists(seport, port, proto) + if change and not module.check_mode: + seport.set_reload(do_reload) + seport.delete(port, proto) + + except ValueError as e: + module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) + except IOError as e: + module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) + except KeyError as e: + module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) + except OSError as e: + module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) + except RuntimeError as e: + module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) + + return change + + +def main(): + module = AnsibleModule( + argument_spec={ + 'port': { + 'required': True, + }, + 'proto': { + 'required': True, + 'choices': ['tcp', 'udp'], + }, + 'setype': { + 'required': True, + }, + 'state': { + 'required': True, + 'choices': ['present', 'absent'], + }, + 'reload': { + 'required': False, + 'type': 'bool', + 'default': 'yes', + }, + }, + supports_check_mode=True + ) + if not HAVE_SELINUX: + module.fail_json(msg="This module requires libselinux-python") + + if not HAVE_SEOBJECT: + module.fail_json(msg="This module requires policycoreutils-python") + + if not selinux.is_selinux_enabled(): + module.fail_json(msg="SELinux is disabled on this host.") + + port = module.params['port'] + proto = module.params['proto'] + setype = module.params['setype'] + state = module.params['state'] + do_reload = module.params['reload'] + + result = {} + result['port'] = port + result['proto'] = proto + result['setype'] = setype + result['state'] = state + + if state == 'present': + result['changed'] = semanage_port_add(module, port, proto, setype, do_reload) + elif state == 'absent': + result['changed'] = semanage_port_del(module, port, proto, do_reload) + else: + module.fail_json(msg='Invalid value of argument "state": {0}'.format(state)) + + module.exit_json(**result) + + +from ansible.module_utils.basic import * +main() From a55035c55874ac66351672039e94dd44349d5c7b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 12 Oct 2014 00:20:00 +0200 Subject: [PATCH 0002/2522] Added module for managing Apple Mac OSX user defaults --- system/mac_defaults.py | 351 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 system/mac_defaults.py diff --git a/system/mac_defaults.py b/system/mac_defaults.py new file mode 100644 index 00000000000..861bebb8033 --- /dev/null +++ b/system/mac_defaults.py @@ -0,0 +1,351 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2014, GeekChimp - Franck Nijhof +# +# Originally developed for Macable: https://github.com/GeekChimp/macable +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: mac_defaults +author: Franck Nijhof +short_description: mac_defaults allows users to read, write, and delete Mac OS X user defaults from Ansible +description: + - mac_defaults allows users to read, write, and delete Mac OS X user defaults from Ansible scripts. + Mac OS X applications and other programs use the defaults system to record user preferences and other + information that must be maintained when the applications aren't running (such as default font for new + documents, or the position of an Info panel). +version_added: 1.8 +options: + domain: + description: + - The domain is a domain name of the form com.companyname.appname. + required: false + default: NSGlobalDomain + key: + description: + - The key of the user preference + required: true + type: + description: + - The type of value to write. + required: false + default: string + choices: [ "array", "bool", "boolean", "date", "float", "int", "integer", "string" ] + array_add: + description: + - Add new elements to the array for a key which has an array as its value. + required: false + default: string + choices: [ "true", "false" ] + value: + description: + - The value to write. Only required when state = present. + required: false + default: null + state: + description: + - The state of the user defaults + required: false + default: present + choices: [ "present", "absent" ] +notes: + - Apple Mac caches defaults. You may need to logout and login to apply the changes. +''' + +EXAMPLES = ''' +- mac_defaults: domain=com.apple.Safari key=IncludeInternalDebugMenu type=bool value=true state=present +- mac_defaults: domain=NSGlobalDomain key=AppleMeasurementUnits type=string value=Centimeters state=present +- mac_defaults: key=AppleMeasurementUnits type=string value=Centimeters +- mac_defaults: + key: AppleLanguages + type: array + value: ["en", "nl"] +- mac_defaults: domain=com.geekchimp.macable key=ExampleKeyToRemove state=absent +''' + +from datetime import datetime + +# exceptions --------------------------------------------------------------- {{{ +class MacDefaultsException(Exception): + pass +# /exceptions -------------------------------------------------------------- }}} + +# class MacDefaults -------------------------------------------------------- {{{ +class MacDefaults(object): + + """ Class to manage Mac OS user defaults """ + + # init ---------------------------------------------------------------- {{{ + """ Initialize this module. Finds 'defaults' executable and preps the parameters """ + def __init__(self, **kwargs): + + # Initial var for storing current defaults value + self.current_value = None + + # Just set all given parameters + for key, val in kwargs.iteritems(): + setattr(self, key, val) + + # Try to find the defaults executable + self.executable = self.module.get_bin_path( + 'defaults', + required=False, + opt_dirs=self.path.split(':'), + ) + + if not self.executable: + raise MacDefaultsException("Unable to locate defaults executable.") + + # When state is present, we require a parameter + if self.state == "present" and self.value is None: + raise MacDefaultsException("Missing value parameter") + + # Ensure the value is the correct type + self.value = self._convert_type(self.type, self.value) + + # /init --------------------------------------------------------------- }}} + + # tools --------------------------------------------------------------- {{{ + """ Converts value to given type """ + def _convert_type(self, type, value): + + if type == "string": + return str(value) + elif type in ["bool", "boolean"]: + if value in [True, 1, "true", "1", "yes"]: + return True + elif value in [False, 0, "false", "0", "no"]: + return False + raise MacDefaultsException("Invalid boolean value: {0}".format(repr(value))) + elif type == "date": + try: + return datetime.strptime(value.split("+")[0].strip(), "%Y-%m-%d %H:%M:%S") + except ValueError: + raise MacDefaultsException( + "Invalid date value: {0}. Required format yyy-mm-dd hh:mm:ss.".format(repr(value)) + ) + elif type in ["int", "integer"]: + if not str(value).isdigit(): + raise MacDefaultsException("Invalid integer value: {0}".format(repr(value))) + return int(value) + elif type == "float": + try: + value = float(value) + except ValueError: + raise MacDefaultsException("Invalid float value: {0}".format(repr(value))) + return value + elif type == "array": + if not isinstance(value, list): + raise MacDefaultsException("Invalid value. Expected value to be an array") + return value + + raise MacDefaultsException('Type is not supported: {0}'.format(type)) + + """ Converts array output from defaults to an list """ + @staticmethod + def _convert_defaults_str_to_list(value): + + # Split output of defaults. Every line contains a value + value = value.splitlines() + + # Remove first and last item, those are not actual values + value.pop(0) + value.pop(-1) + + # Remove extra spaces and comma (,) at the end of values + value = [re.sub(',$', '', x.strip(' ')) for x in value] + + return value + # /tools -------------------------------------------------------------- }}} + + # commands ------------------------------------------------------------ {{{ + """ Reads value of this domain & key from defaults """ + def read(self): + # First try to find out the type + rc, out, err = self.module.run_command([self.executable, "read-type", self.domain, self.key]) + + # If RC is 1, the key does not exists + if rc == 1: + return None + + # If the RC is not 0, then terrible happened! Ooooh nooo! + if rc != 0: + raise MacDefaultsException("An error occurred while reading key type from defaults: " + out) + + # Ok, lets parse the type from output + type = out.strip().replace('Type is ', '') + + # Now get the current value + rc, out, err = self.module.run_command([self.executable, "read", self.domain, self.key]) + + # Strip output + # out = out.strip() + + # An non zero RC at this point is kinda strange... + if rc != 0: + raise MacDefaultsException("An error occurred while reading key value from defaults: " + out) + + # Convert string to list when type is array + if type == "array": + out = self._convert_defaults_str_to_list(out) + + # Store the current_value + self.current_value = self._convert_type(type, out) + + """ Writes value to this domain & key to defaults """ + def write(self): + + # We need to convert some values so the defaults commandline understands it + if type(self.value) is bool: + value = "TRUE" if self.value else "FALSE" + elif type(self.value) is int or type(self.value) is float: + value = str(self.value) + elif self.array_add and self.current_value is not None: + value = list(set(self.value) - set(self.current_value)) + elif isinstance(self.value, datetime): + value = self.value.strftime('%Y-%m-%d %H:%M:%S') + else: + value = self.value + + # When the type is array and array_add is enabled, morph the type :) + if self.type == "array" and self.array_add: + self.type = "array-add" + + # All values should be a list, for easy passing it to the command + if not isinstance(value, list): + value = [value] + + rc, out, err = self.module.run_command([self.executable, 'write', self.domain, self.key, '-' + self.type] + value) + + if rc != 0: + raise MacDefaultsException('An error occurred while writing value to defaults: ' + out) + + """ Deletes defaults key from domain """ + def delete(self): + rc, out, err = self.module.run_command([self.executable, 'delete', self.domain, self.key]) + if rc != 0: + raise MacDefaultsException("An error occurred while deleting key from defaults: " + out) + + # /commands ----------------------------------------------------------- }}} + + # run ----------------------------------------------------------------- {{{ + """ Does the magic! :) """ + def run(self): + + # Get the current value from defaults + self.read() + + # Handle absent state + if self.state == "absent": + print "Absent state detected!" + if self.current_value is None: + return False + self.delete() + return True + + # There is a type mismatch! Given type does not match the type in defaults + if self.current_value is not None and type(self.current_value) is not type(self.value): + raise MacDefaultsException("Type mismatch. Type in defaults: " + type(self.current_value).__name__) + + # Current value matches the given value. Nothing need to be done. Arrays need extra care + if self.type == "array" and self.current_value is not None and not self.array_add and \ + set(self.current_value) == set(self.value): + return False + elif self.type == "array" and self.current_value is not None and self.array_add and \ + len(list(set(self.value) - set(self.current_value))) == 0: + return False + elif self.current_value == self.value: + return False + + # Change/Create/Set given key/value for domain in defaults + self.write() + return True + + # /run ---------------------------------------------------------------- }}} + +# /class MacDefaults ------------------------------------------------------ }}} + + +# main -------------------------------------------------------------------- {{{ +def main(): + module = AnsibleModule( + argument_spec=dict( + domain=dict( + default="NSGlobalDomain", + required=False, + ), + key=dict( + default=None, + ), + type=dict( + default="string", + required=False, + choices=[ + "array", + "bool", + "boolean", + "date", + "float", + "int", + "integer", + "string", + ], + ), + array_add=dict( + default=False, + required=False, + choices=BOOLEANS, + ), + value=dict( + default=None, + required=False, + ), + state=dict( + default="present", + required=False, + choices=[ + "absent", "present" + ], + ), + path=dict( + default="/usr/bin:/usr/local/bin", + required=False, + ) + ), + supports_check_mode=True, + ) + + domain = module.params['domain'] + key = module.params['key'] + type = module.params['type'] + array_add = module.params['array_add'] + value = module.params['value'] + state = module.params['state'] + path = module.params['path'] + + try: + defaults = MacDefaults(module=module, domain=domain, key=key, type=type, + array_add=array_add, value=value, state=state, path=path) + changed = defaults.run() + module.exit_json(changed=changed) + except MacDefaultsException as e: + module.fail_json(msg=e.message) + +# /main ------------------------------------------------------------------- }}} + +from ansible.module_utils.basic import * +main() From 2c43cdb12378ba7c01ba2bb785f05f5a1c54d2cc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 12 Oct 2014 14:41:57 +0200 Subject: [PATCH 0003/2522] Renamed module from mac_defaults to osx_defaults so the naming is more up to par with existing modules (e.g. osx_say) --- system/{mac_defaults.py => osx_defaults.py} | 52 ++++++++++----------- 1 file changed, 25 insertions(+), 27 deletions(-) rename system/{mac_defaults.py => osx_defaults.py} (88%) diff --git a/system/mac_defaults.py b/system/osx_defaults.py similarity index 88% rename from system/mac_defaults.py rename to system/osx_defaults.py index 861bebb8033..8baed17f2eb 100644 --- a/system/mac_defaults.py +++ b/system/osx_defaults.py @@ -3,8 +3,6 @@ # (c) 2014, GeekChimp - Franck Nijhof # -# Originally developed for Macable: https://github.com/GeekChimp/macable -# # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or @@ -20,11 +18,11 @@ DOCUMENTATION = ''' --- -module: mac_defaults +module: osx_defaults author: Franck Nijhof -short_description: mac_defaults allows users to read, write, and delete Mac OS X user defaults from Ansible +short_description: osx_defaults allows users to read, write, and delete Mac OS X user defaults from Ansible description: - - mac_defaults allows users to read, write, and delete Mac OS X user defaults from Ansible scripts. + - osx_defaults allows users to read, write, and delete Mac OS X user defaults from Ansible scripts. Mac OS X applications and other programs use the defaults system to record user preferences and other information that must be maintained when the applications aren't running (such as default font for new documents, or the position of an Info panel). @@ -67,25 +65,25 @@ ''' EXAMPLES = ''' -- mac_defaults: domain=com.apple.Safari key=IncludeInternalDebugMenu type=bool value=true state=present -- mac_defaults: domain=NSGlobalDomain key=AppleMeasurementUnits type=string value=Centimeters state=present -- mac_defaults: key=AppleMeasurementUnits type=string value=Centimeters -- mac_defaults: +- osx_defaults: domain=com.apple.Safari key=IncludeInternalDebugMenu type=bool value=true state=present +- osx_defaults: domain=NSGlobalDomain key=AppleMeasurementUnits type=string value=Centimeters state=present +- osx_defaults: key=AppleMeasurementUnits type=string value=Centimeters +- osx_defaults: key: AppleLanguages type: array value: ["en", "nl"] -- mac_defaults: domain=com.geekchimp.macable key=ExampleKeyToRemove state=absent +- osx_defaults: domain=com.geekchimp.macable key=ExampleKeyToRemove state=absent ''' from datetime import datetime # exceptions --------------------------------------------------------------- {{{ -class MacDefaultsException(Exception): +class OSXDefaultsException(Exception): pass # /exceptions -------------------------------------------------------------- }}} # class MacDefaults -------------------------------------------------------- {{{ -class MacDefaults(object): +class OSXDefaults(object): """ Class to manage Mac OS user defaults """ @@ -108,11 +106,11 @@ def __init__(self, **kwargs): ) if not self.executable: - raise MacDefaultsException("Unable to locate defaults executable.") + raise OSXDefaultsException("Unable to locate defaults executable.") # When state is present, we require a parameter if self.state == "present" and self.value is None: - raise MacDefaultsException("Missing value parameter") + raise OSXDefaultsException("Missing value parameter") # Ensure the value is the correct type self.value = self._convert_type(self.type, self.value) @@ -130,30 +128,30 @@ def _convert_type(self, type, value): return True elif value in [False, 0, "false", "0", "no"]: return False - raise MacDefaultsException("Invalid boolean value: {0}".format(repr(value))) + raise OSXDefaultsException("Invalid boolean value: {0}".format(repr(value))) elif type == "date": try: return datetime.strptime(value.split("+")[0].strip(), "%Y-%m-%d %H:%M:%S") except ValueError: - raise MacDefaultsException( + raise OSXDefaultsException( "Invalid date value: {0}. Required format yyy-mm-dd hh:mm:ss.".format(repr(value)) ) elif type in ["int", "integer"]: if not str(value).isdigit(): - raise MacDefaultsException("Invalid integer value: {0}".format(repr(value))) + raise OSXDefaultsException("Invalid integer value: {0}".format(repr(value))) return int(value) elif type == "float": try: value = float(value) except ValueError: - raise MacDefaultsException("Invalid float value: {0}".format(repr(value))) + raise OSXDefaultsException("Invalid float value: {0}".format(repr(value))) return value elif type == "array": if not isinstance(value, list): - raise MacDefaultsException("Invalid value. Expected value to be an array") + raise OSXDefaultsException("Invalid value. Expected value to be an array") return value - raise MacDefaultsException('Type is not supported: {0}'.format(type)) + raise OSXDefaultsException('Type is not supported: {0}'.format(type)) """ Converts array output from defaults to an list """ @staticmethod @@ -184,7 +182,7 @@ def read(self): # If the RC is not 0, then terrible happened! Ooooh nooo! if rc != 0: - raise MacDefaultsException("An error occurred while reading key type from defaults: " + out) + raise OSXDefaultsException("An error occurred while reading key type from defaults: " + out) # Ok, lets parse the type from output type = out.strip().replace('Type is ', '') @@ -197,7 +195,7 @@ def read(self): # An non zero RC at this point is kinda strange... if rc != 0: - raise MacDefaultsException("An error occurred while reading key value from defaults: " + out) + raise OSXDefaultsException("An error occurred while reading key value from defaults: " + out) # Convert string to list when type is array if type == "array": @@ -232,13 +230,13 @@ def write(self): rc, out, err = self.module.run_command([self.executable, 'write', self.domain, self.key, '-' + self.type] + value) if rc != 0: - raise MacDefaultsException('An error occurred while writing value to defaults: ' + out) + raise OSXDefaultsException('An error occurred while writing value to defaults: ' + out) """ Deletes defaults key from domain """ def delete(self): rc, out, err = self.module.run_command([self.executable, 'delete', self.domain, self.key]) if rc != 0: - raise MacDefaultsException("An error occurred while deleting key from defaults: " + out) + raise OSXDefaultsException("An error occurred while deleting key from defaults: " + out) # /commands ----------------------------------------------------------- }}} @@ -259,7 +257,7 @@ def run(self): # There is a type mismatch! Given type does not match the type in defaults if self.current_value is not None and type(self.current_value) is not type(self.value): - raise MacDefaultsException("Type mismatch. Type in defaults: " + type(self.current_value).__name__) + raise OSXDefaultsException("Type mismatch. Type in defaults: " + type(self.current_value).__name__) # Current value matches the given value. Nothing need to be done. Arrays need extra care if self.type == "array" and self.current_value is not None and not self.array_add and \ @@ -338,11 +336,11 @@ def main(): path = module.params['path'] try: - defaults = MacDefaults(module=module, domain=domain, key=key, type=type, + defaults = OSXDefaults(module=module, domain=domain, key=key, type=type, array_add=array_add, value=value, state=state, path=path) changed = defaults.run() module.exit_json(changed=changed) - except MacDefaultsException as e: + except OSXDefaultsException as e: module.fail_json(msg=e.message) # /main ------------------------------------------------------------------- }}} From 130bd670d82cc55fa321021e819838e07ff10c08 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 13 Oct 2014 07:34:24 +0200 Subject: [PATCH 0004/2522] Small fix for boolean when boolean type was set via a variable (somehow changes the behaviour of Ansible because of YAML as it seems. Booleans then become represented as a string). --- system/osx_defaults.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/osx_defaults.py b/system/osx_defaults.py index 8baed17f2eb..0dd7ca8ff6b 100644 --- a/system/osx_defaults.py +++ b/system/osx_defaults.py @@ -124,9 +124,9 @@ def _convert_type(self, type, value): if type == "string": return str(value) elif type in ["bool", "boolean"]: - if value in [True, 1, "true", "1", "yes"]: + if value.lower() in [True, 1, "true", "1", "yes"]: return True - elif value in [False, 0, "false", "0", "no"]: + elif value.lower() in [False, 0, "false", "0", "no"]: return False raise OSXDefaultsException("Invalid boolean value: {0}".format(repr(value))) elif type == "date": @@ -191,7 +191,7 @@ def read(self): rc, out, err = self.module.run_command([self.executable, "read", self.domain, self.key]) # Strip output - # out = out.strip() + out = out.strip() # An non zero RC at this point is kinda strange... if rc != 0: From e5939fffdd579c731f4252a33692b40188d7ed2b Mon Sep 17 00:00:00 2001 From: Sebastien ROHAUT Date: Thu, 30 Oct 2014 11:03:44 +0100 Subject: [PATCH 0005/2522] Create pam_limits.py The pam_limits module modify PAM limits, default in /etc/security/limits.conf. For the full documentation, see man limits.conf(5). --- system/pam_limits.py | 222 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 system/pam_limits.py diff --git a/system/pam_limits.py b/system/pam_limits.py new file mode 100644 index 00000000000..0c492699eba --- /dev/null +++ b/system/pam_limits.py @@ -0,0 +1,222 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2014, Sebastien Rohaut +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import os +import os.path +import shutil +import re + +DOCUMENTATION = ''' +--- +module: pam_limits +version_added: "historical" +short_description: Modify Linux PAM limits +description: + - The M(pam_limits) module modify PAM limits, default in /etc/security/limits.conf. + For the full documentation, see man limits.conf(5). +options: + domain: + description: + - A username, @groupname, wildcard, uid/gid range. + required: true + default: null + limit_type: + description: + - Limit type : hard or soft. + required: true + choices: [ "hard", "soft" ] + default: null + limit_item: + description: + - The limit to be set : core, data, nofile, cpu, etc. + required: true + choices: [ "core", "data", "fsize", "memlock", "nofile", "rss", "stack", "cpu", "nproc", "as", "maxlogins", "maxsyslogins", "priority", "locks", "sigpending", "msgqueue", "nice", "rtprio", "chroot" ] + default: null + value: + description: + - The value of the limit. + required: true + default: null + backup: + description: + - Create a backup file including the timestamp information so you can get + the original file back if you somehow clobbered it incorrectly. + required: false + choices: [ "yes", "no" ] + default: "no" + use_min: + description: + - If set to C(yes), the minimal value will be used or conserved. + If the specified value is inferior to the value in the file, file content is replaced with the new value, + else content is not modified. + required: false + choices: [ "yes", "no" ] + default: "no" + use_max: + description: + - If set to C(yes), the maximal value will be used or conserved. + If the specified value is superior to the value in the file, file content is replaced with the new value, + else content is not modified. + required: false + choices: [ "yes", "no" ] + default: "no" + dest: + description: + - Modify the limits.conf path. + required: false + default: "/etc/security/limits.conf" +''' + +EXAMPLES = ''' +# Add or modify limits for the user joe +- pam_limits: domain=joe limit_type=soft limit_item=nofile value=64000 + +# Add or modify limits for the user joe. Keep or set the maximal value +- pam_limits: domain=joe limit_type=soft limit_item=nofile value=1000000 +''' + +def main(): + + pam_items = [ 'core', 'data', 'fsize', 'memlock', 'nofile', 'rss', 'stack', 'cpu', 'nproc', 'as', 'maxlogins', 'maxsyslogins', 'priority', 'locks', 'sigpending', 'msgqueue', 'nice', 'rtprio', 'chroot' ] + + pam_types = [ 'soft', 'hard' ] + + limits_conf = '/home/slyce/limits.conf' + + module = AnsibleModule( + # not checking because of daisy chain to file module + argument_spec = dict( + domain = dict(required=True, type='str'), + limit_type = dict(required=True, type='str', choices=pam_types), + limit_item = dict(required=True, type='str', choices=pam_items), + value = dict(required=True, type='int'), + use_max = dict(default=False, type='bool'), + use_min = dict(default=False, type='bool'), + backup = dict(default=False, type='bool'), + dest = dict(default=limits_conf, type='str') + ) + ) + + domain = module.params['domain'] + limit_type = module.params['limit_type'] + limit_item = module.params['limit_item'] + value = module.params['value'] + use_max = module.params['use_max'] + use_min = module.params['use_min'] + backup = module.params['backup'] + limits_conf = module.params['dest'] + + changed = False + + if os.path.isfile(limits_conf): + if not os.access(limits_conf, os.W_OK): + module.fail_json(msg="%s is not writable. Use sudo" % (limits_conf) ) + else: + module.fail_json(msg="%s is not visible (check presence, access rights, use sudo)" % (limits_conf) ) + + # Backup + if backup: + backup_file = module.backup_local(limits_conf) + + space_pattern = re.compile(r'\s+') + + message = '' + f = open (limits_conf, 'r') + # Tempfile + nf = tempfile.NamedTemporaryFile(delete = False) + + found = False + new_value = value + + for line in f: + if line.startswith('#'): + nf.write(line) + continue + + newline = re.sub(space_pattern, ' ', line).strip() + if not newline: + nf.write(line) + continue + + line_fields = newline.split(' ') + + if len(line_fields) != 4: + nf.write(line) + continue + + line_domain = line_fields[0] + line_type = line_fields[1] + line_item = line_fields[2] + actual_value = int(line_fields[3]) + + # Found the line + if line_domain == domain and line_type == limit_type and line_item == limit_item: + found = True + if value == actual_value: + message = line + nf.write(line) + continue + + if use_max: + new_value = max(value, actual_value) + + if use_min: + new_value = min(value,actual_value) + + # Change line only if value has changed + if new_value != actual_value: + changed = True + new_limit = domain + "\t" + limit_type + "\t" + limit_item + "\t" + str(new_value) + "\n" + message = new_limit + nf.write(new_limit) + else: + message = line + nf.write(line) + else: + nf.write(line) + + if not found: + changed = True + new_limit = domain + "\t" + limit_type + "\t" + limit_item + "\t" + str(new_value) + "\n" + message = new_limit + nf.write(new_limit) + + f.close() + nf.close() + + # Copy tempfile to newfile + shutil.copy(nf.name, f.name) + + # delete tempfile + os.unlink(nf.name) + + res_args = dict( + changed = changed, msg = message + ) + + if backup: + res_args['backup_file'] = backup_file + + module.exit_json(**res_args) + + +# import module snippets +from ansible.module_utils.basic import * +main() From 8122edd727483aa2a80744dc9be9d7b9cb1b7e05 Mon Sep 17 00:00:00 2001 From: Sebastien ROHAUT Date: Thu, 30 Oct 2014 11:07:42 +0100 Subject: [PATCH 0006/2522] Update pam_limits.py Tabs... --- system/pam_limits.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/pam_limits.py b/system/pam_limits.py index 0c492699eba..ae0e181d6e1 100644 --- a/system/pam_limits.py +++ b/system/pam_limits.py @@ -138,12 +138,12 @@ def main(): space_pattern = re.compile(r'\s+') message = '' - f = open (limits_conf, 'r') + f = open (limits_conf, 'r') # Tempfile nf = tempfile.NamedTemporaryFile(delete = False) found = False - new_value = value + new_value = value for line in f: if line.startswith('#'): From b845c0ef56ac94bfd6d59912ba5a8628880093c4 Mon Sep 17 00:00:00 2001 From: Sebastien ROHAUT Date: Thu, 30 Oct 2014 11:10:02 +0100 Subject: [PATCH 0007/2522] Update pam_limits.py --- system/pam_limits.py | 1 + 1 file changed, 1 insertion(+) diff --git a/system/pam_limits.py b/system/pam_limits.py index ae0e181d6e1..338c00e03d5 100644 --- a/system/pam_limits.py +++ b/system/pam_limits.py @@ -139,6 +139,7 @@ def main(): message = '' f = open (limits_conf, 'r') + # Tempfile nf = tempfile.NamedTemporaryFile(delete = False) From c8fbac0ff21d515b417bbde9572df018fa5be867 Mon Sep 17 00:00:00 2001 From: Sebastien ROHAUT Date: Thu, 30 Oct 2014 11:10:49 +0100 Subject: [PATCH 0008/2522] Update pam_limits.py --- system/pam_limits.py | 1 - 1 file changed, 1 deletion(-) diff --git a/system/pam_limits.py b/system/pam_limits.py index 338c00e03d5..ae0e181d6e1 100644 --- a/system/pam_limits.py +++ b/system/pam_limits.py @@ -139,7 +139,6 @@ def main(): message = '' f = open (limits_conf, 'r') - # Tempfile nf = tempfile.NamedTemporaryFile(delete = False) From 722d810ef22df1bd794e1ef01a53ad7995f28c1d Mon Sep 17 00:00:00 2001 From: Sebastien ROHAUT Date: Thu, 30 Oct 2014 11:13:17 +0100 Subject: [PATCH 0009/2522] Update pam_limits.py Change path --- system/pam_limits.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/pam_limits.py b/system/pam_limits.py index ae0e181d6e1..fde70abd800 100644 --- a/system/pam_limits.py +++ b/system/pam_limits.py @@ -98,7 +98,7 @@ def main(): pam_types = [ 'soft', 'hard' ] - limits_conf = '/home/slyce/limits.conf' + limits_conf = '/etc/security/limits.conf' module = AnsibleModule( # not checking because of daisy chain to file module From 5f870b094b4e682c654ff6c298f4dd3b9e5dd486 Mon Sep 17 00:00:00 2001 From: Timothy Vandenbrande Date: Fri, 21 Nov 2014 14:26:47 +0100 Subject: [PATCH 0010/2522] added a source/network add/remove to/from zone for firewalld --- system/firewalld.py | 55 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/system/firewalld.py b/system/firewalld.py index 22db165aad3..ec4be051c9e 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -41,6 +41,11 @@ - "Rich rule to add/remove to/from firewalld" required: false default: null + source: + description: + - 'The source/network you would like to add/remove to/from firewalld' + required: false + default: null zone: description: - 'The firewalld zone to add/remove to/from (NOTE: default zone can be configured per system but "public" is default from upstream. Available choices can be extended based on per-system configs, listed here are "out of the box" defaults).' @@ -73,6 +78,7 @@ - firewalld: port=8081/tcp permanent=true state=disabled - firewalld: zone=dmz service=http permanent=true state=enabled - firewalld: rich_rule='rule service name="ftp" audit limit value="1/m" accept' permanent=true state=enabled +- firewalld: source='192.168.1.0/24' zone=internal state=enabled ''' import os @@ -128,7 +134,29 @@ def set_port_disabled_permanent(zone, port, protocol): fw_settings = fw_zone.getSettings() fw_settings.removePort(port, protocol) fw_zone.update(fw_settings) - + +#################### +# source handling +# +def get_source(zone, source): + fw_zone = fw.config().getZoneByName(zone) + fw_settings = fw_zone.getSettings() + if source in fw_settings.getSources(): + return True + else: + return False + +def add_source(zone, source): + fw_zone = fw.config().getZoneByName(zone) + fw_settings = fw_zone.getSettings() + fw_settings.addSource(source) + fw_zone.update(fw_settings) + +def remove_source(zone, source): + fw_zone = fw.config().getZoneByName(zone) + fw_settings = fw_zone.getSettings() + fw_settings.removeSource(source) + fw_zone.update(fw_settings) #################### # service handling @@ -210,12 +238,15 @@ def main(): port=dict(required=False,default=None), rich_rule=dict(required=False,default=None), zone=dict(required=False,default=None), - permanent=dict(type='bool',required=True), + source=dict(required=False,default=None), + permanent=dict(type='bool',required=False,default=None), state=dict(choices=['enabled', 'disabled'], required=True), timeout=dict(type='int',required=False,default=0), ), supports_check_mode=True ) + if module.params['source'] == None and module.params['permanent'] == None: + module.fail(msg='permanent is a required parameter') ## Pre-run version checking if FW_VERSION < "0.2.11": @@ -226,6 +257,7 @@ def main(): msgs = [] service = module.params['service'] rich_rule = module.params['rich_rule'] + source = module.params['source'] if module.params['port'] != None: port, protocol = module.params['port'].split('/') @@ -304,6 +336,25 @@ def main(): if changed == True: msgs.append("Changed service %s to %s" % (service, desired_state)) + if source != None: + is_enabled = get_source(zone, source) + if desired_state == "enabled": + if is_enabled == False: + if module.check_mode: + module.exit_json(changed=True) + + add_source(zone, source) + changed=True + msgs.append("Added %s to zone %s" % (source, zone)) + elif desired_state == "disabled": + if is_enabled == True: + msgs.append("source is present") + if module.check_mode: + module.exit_json(changed=True) + + remove_source(zone, source) + changed=True + msgs.append("Removed %s from zone %s" % (source, zone)) if port != None: if permanent: is_enabled = get_port_enabled_permanent(zone, [port, protocol]) From d6fbfdefd5ced3c8db63f0bef14900a816fddb5b Mon Sep 17 00:00:00 2001 From: Timothy Vandenbrande Date: Fri, 21 Nov 2014 15:39:07 +0100 Subject: [PATCH 0011/2522] added a source/network add/remove to/from zone for firewalld - removed useless comment --- system/firewalld.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/system/firewalld.py b/system/firewalld.py index ec4be051c9e..ed49f0860be 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -150,13 +150,11 @@ def add_source(zone, source): fw_zone = fw.config().getZoneByName(zone) fw_settings = fw_zone.getSettings() fw_settings.addSource(source) - fw_zone.update(fw_settings) def remove_source(zone, source): fw_zone = fw.config().getZoneByName(zone) fw_settings = fw_zone.getSettings() fw_settings.removeSource(source) - fw_zone.update(fw_settings) #################### # service handling @@ -348,7 +346,6 @@ def main(): msgs.append("Added %s to zone %s" % (source, zone)) elif desired_state == "disabled": if is_enabled == True: - msgs.append("source is present") if module.check_mode: module.exit_json(changed=True) From 5c03696693cdee250e3bc78166bdb41846c685f3 Mon Sep 17 00:00:00 2001 From: Philip Carinhas Date: Sat, 6 Dec 2014 12:31:08 -0600 Subject: [PATCH 0012/2522] Fix rabbitmq_plugin.py: broken prefix path --- messaging/rabbitmq_plugin.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/messaging/rabbitmq_plugin.py b/messaging/rabbitmq_plugin.py index 53c38f978d5..194af2f6dc9 100644 --- a/messaging/rabbitmq_plugin.py +++ b/messaging/rabbitmq_plugin.py @@ -59,12 +59,23 @@ - rabbitmq_plugin: names=rabbitmq_management state=enabled ''' +import os + class RabbitMqPlugins(object): def __init__(self, module): self.module = module if module.params['prefix']: - self._rabbitmq_plugins = module.params['prefix'] + "/sbin/rabbitmq-plugins" + if os.path.isdir(os.path.join(module.params['prefix'], 'bin')): + bin_path = os.path.join(module.params['prefix'], 'bin') + elif os.path.isdir(os.path.join(module.params['prefix'], 'sbin')): + bin_path = os.path.join(module.params['prefix'], 'sbin') + else: + # No such path exists. + raise Exception("No binary folder in RabbitMQ prefix") + + self._rabbitmq_plugins = bin_path + "/rabbitmq-plugins" + else: self._rabbitmq_plugins = module.get_bin_path('rabbitmq-plugins', True) From 530323bf63ad0331229631f7a254480d1a2abc92 Mon Sep 17 00:00:00 2001 From: Philip Carinhas Date: Sun, 7 Dec 2014 10:01:49 -0600 Subject: [PATCH 0013/2522] Improve error message --- messaging/rabbitmq_plugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/messaging/rabbitmq_plugin.py b/messaging/rabbitmq_plugin.py index 194af2f6dc9..8c6ca1381d8 100644 --- a/messaging/rabbitmq_plugin.py +++ b/messaging/rabbitmq_plugin.py @@ -72,7 +72,8 @@ def __init__(self, module): bin_path = os.path.join(module.params['prefix'], 'sbin') else: # No such path exists. - raise Exception("No binary folder in RabbitMQ prefix") + raise Exception("No binary folder in prefix %s" % + module.params['prefix']) self._rabbitmq_plugins = bin_path + "/rabbitmq-plugins" From 6fab8f49a965c708be9ac2290c074d050d6a6832 Mon Sep 17 00:00:00 2001 From: Timothy Vandenbrande Date: Fri, 21 Nov 2014 14:26:47 +0100 Subject: [PATCH 0014/2522] added a source/network add/remove to/from zone for firewalld --- system/firewalld.py | 55 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/system/firewalld.py b/system/firewalld.py index dedc9260740..ace5e5fd1e4 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -41,6 +41,11 @@ - "Rich rule to add/remove to/from firewalld" required: false default: null + source: + description: + - 'The source/network you would like to add/remove to/from firewalld' + required: false + default: null zone: description: - 'The firewalld zone to add/remove to/from (NOTE: default zone can be configured per system but "public" is default from upstream. Available choices can be extended based on per-system configs, listed here are "out of the box" defaults).' @@ -77,6 +82,7 @@ - firewalld: port=8081/tcp permanent=true state=disabled - firewalld: zone=dmz service=http permanent=true state=enabled - firewalld: rich_rule='rule service name="ftp" audit limit value="1/m" accept' permanent=true state=enabled +- firewalld: source='192.168.1.0/24' zone=internal state=enabled ''' import os @@ -132,7 +138,29 @@ def set_port_disabled_permanent(zone, port, protocol): fw_settings = fw_zone.getSettings() fw_settings.removePort(port, protocol) fw_zone.update(fw_settings) - + +#################### +# source handling +# +def get_source(zone, source): + fw_zone = fw.config().getZoneByName(zone) + fw_settings = fw_zone.getSettings() + if source in fw_settings.getSources(): + return True + else: + return False + +def add_source(zone, source): + fw_zone = fw.config().getZoneByName(zone) + fw_settings = fw_zone.getSettings() + fw_settings.addSource(source) + fw_zone.update(fw_settings) + +def remove_source(zone, source): + fw_zone = fw.config().getZoneByName(zone) + fw_settings = fw_zone.getSettings() + fw_settings.removeSource(source) + fw_zone.update(fw_settings) #################### # service handling @@ -214,13 +242,16 @@ def main(): port=dict(required=False,default=None), rich_rule=dict(required=False,default=None), zone=dict(required=False,default=None), - permanent=dict(type='bool',required=True), immediate=dict(type='bool',default=False), + source=dict(required=False,default=None), + permanent=dict(type='bool',required=False,default=None), state=dict(choices=['enabled', 'disabled'], required=True), timeout=dict(type='int',required=False,default=0), ), supports_check_mode=True ) + if module.params['source'] == None and module.params['permanent'] == None: + module.fail(msg='permanent is a required parameter') ## Pre-run version checking if FW_VERSION < "0.2.11": @@ -231,6 +262,7 @@ def main(): msgs = [] service = module.params['service'] rich_rule = module.params['rich_rule'] + source = module.params['source'] if module.params['port'] != None: port, protocol = module.params['port'].split('/') @@ -310,6 +342,25 @@ def main(): if changed == True: msgs.append("Changed service %s to %s" % (service, desired_state)) + if source != None: + is_enabled = get_source(zone, source) + if desired_state == "enabled": + if is_enabled == False: + if module.check_mode: + module.exit_json(changed=True) + + add_source(zone, source) + changed=True + msgs.append("Added %s to zone %s" % (source, zone)) + elif desired_state == "disabled": + if is_enabled == True: + msgs.append("source is present") + if module.check_mode: + module.exit_json(changed=True) + + remove_source(zone, source) + changed=True + msgs.append("Removed %s from zone %s" % (source, zone)) if port != None: if permanent: is_enabled = get_port_enabled_permanent(zone, [port, protocol]) From b365fc44645a4d81b7e7780708a4b7dd24faf1ce Mon Sep 17 00:00:00 2001 From: Timothy Vandenbrande Date: Fri, 21 Nov 2014 15:39:07 +0100 Subject: [PATCH 0015/2522] added a source/network add/remove to/from zone for firewalld - removed useless comment --- system/firewalld.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/system/firewalld.py b/system/firewalld.py index ace5e5fd1e4..cf90c5ace56 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -154,13 +154,11 @@ def add_source(zone, source): fw_zone = fw.config().getZoneByName(zone) fw_settings = fw_zone.getSettings() fw_settings.addSource(source) - fw_zone.update(fw_settings) def remove_source(zone, source): fw_zone = fw.config().getZoneByName(zone) fw_settings = fw_zone.getSettings() fw_settings.removeSource(source) - fw_zone.update(fw_settings) #################### # service handling @@ -354,7 +352,6 @@ def main(): msgs.append("Added %s to zone %s" % (source, zone)) elif desired_state == "disabled": if is_enabled == True: - msgs.append("source is present") if module.check_mode: module.exit_json(changed=True) From 4c22ee75b15bd412118231d3a89450a44f595af8 Mon Sep 17 00:00:00 2001 From: Sebastien ROHAUT Date: Wed, 24 Dec 2014 15:36:54 +0100 Subject: [PATCH 0016/2522] Update pam_limits.py - Comments are now managed correctly - Cannot use use_min and use_max at the same time --- system/pam_limits.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/system/pam_limits.py b/system/pam_limits.py index fde70abd800..2a6bec383fd 100644 --- a/system/pam_limits.py +++ b/system/pam_limits.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # (c) 2014, Sebastien Rohaut @@ -110,7 +109,8 @@ def main(): use_max = dict(default=False, type='bool'), use_min = dict(default=False, type='bool'), backup = dict(default=False, type='bool'), - dest = dict(default=limits_conf, type='str') + dest = dict(default=limits_conf, type='str'), + comment = dict(required=False, default='', type='str') ) ) @@ -122,6 +122,7 @@ def main(): use_min = module.params['use_min'] backup = module.params['backup'] limits_conf = module.params['dest'] + new_comment = module.params['comment'] changed = False @@ -131,6 +132,9 @@ def main(): else: module.fail_json(msg="%s is not visible (check presence, access rights, use sudo)" % (limits_conf) ) + if use_max and use_min: + module.fail_json(msg="Cannot use use_min and use_max at the same time." ) + # Backup if backup: backup_file = module.backup_local(limits_conf) @@ -146,6 +150,7 @@ def main(): new_value = value for line in f: + if line.startswith('#'): nf.write(line) continue @@ -155,6 +160,21 @@ def main(): nf.write(line) continue + # Remove comment in line + newline = newline.split('#',1)[0] + try: + old_comment = line.split('#',1)[1] + except: + old_comment = '' + + newline = newline.rstrip() + + if not new_comment: + new_comment = old_comment + + if new_comment: + new_comment = "\t#"+new_comment + line_fields = newline.split(' ') if len(line_fields) != 4: @@ -183,7 +203,7 @@ def main(): # Change line only if value has changed if new_value != actual_value: changed = True - new_limit = domain + "\t" + limit_type + "\t" + limit_item + "\t" + str(new_value) + "\n" + new_limit = domain + "\t" + limit_type + "\t" + limit_item + "\t" + str(new_value) + new_comment + "\n" message = new_limit nf.write(new_limit) else: @@ -194,7 +214,7 @@ def main(): if not found: changed = True - new_limit = domain + "\t" + limit_type + "\t" + limit_item + "\t" + str(new_value) + "\n" + new_limit = domain + "\t" + limit_type + "\t" + limit_item + "\t" + str(new_value) + new_comment + "\n" message = new_limit nf.write(new_limit) From 192bf06af9709c7fdaefdbbd8c00329262747a7f Mon Sep 17 00:00:00 2001 From: Boris Ekelchik Date: Wed, 24 Dec 2014 11:52:52 -0800 Subject: [PATCH 0017/2522] New sts_assume_role module --- cloud/amazon/sts_assume_role.py | 166 ++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 cloud/amazon/sts_assume_role.py diff --git a/cloud/amazon/sts_assume_role.py b/cloud/amazon/sts_assume_role.py new file mode 100644 index 00000000000..7e02dbbd84e --- /dev/null +++ b/cloud/amazon/sts_assume_role.py @@ -0,0 +1,166 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: sts_assume_role +short_description: assume a role in AWS account and obtain temporary credentials. +description: + - call AWS STS (Security Token Service) to assume a role in AWS account and obtain temporary credentials. This module has a dependency on python-boto. + For details on base AWS API reference http://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html +version_added: "1.7" +options: + role_arn: + description: + - The Amazon Resource Name (ARN) of the role that the caller is assuming (http://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html#Identifiers_ARNs) + required: true + aliases: [] + role_session_name: + description: + - Name of the role's session - will be used by CloudTrail + required: true + aliases: [] + policy: + description: + - Supplemental policy to use in addition to assumed role's policies. + required: false + default: null + aliases: [] + duration_seconds: + description: + - The duration, in seconds, of the role session. The value can range from 900 seconds (15 minutes) to 3600 seconds (1 hour). By default, the value is set to 3600 seconds. + required: false + default: null + aliases: [] + external_id: + description: + - A unique identifier that is used by third parties to assume a role in their customers' accounts. + required: false + default: null + aliases: [] + mfa_serial_number: + description: + - he identification number of the MFA device that is associated with the user who is making the AssumeRole call. + required: false + default: null + aliases: [] + mfa_token: + description: + - The value provided by the MFA device, if the trust policy of the role being assumed requires MFA. + required: false + default: null + aliases: [] + +author: Boris Ekelchik +extends_documentation_fragment: aws +''' + +EXAMPLES = ''' +# Basic example of assuming a role +tasks: +- name: assume a role in account 123456789012 + sts_assume_role: role_arn="arn:aws:iam::123456789012:role/someRole" session_name="someRoleSession" + +- name: display temporary credentials + debug: "temporary credentials for the assumed role are {{ ansible_temp_credentials }}" + +- name: use temporary credentials for tagging an instance in account 123456789012 + ec2_tag: resource=i-xyzxyz01 region=us-west-1 state=present + args: + aws_access_key: "{{ ansible_temp_credentials.access_key }}" + aws_secret_key: "{{ ansible_temp_credentials.secret_key }}" + security_token: "{{ ansible_temp_credentials.session_token }}" + + tags: + Test: value +''' + +import sys +import time + +try: + import boto.sts + +except ImportError: + print "failed=True msg='boto required for this module'" + sys.exit(1) + +def sts_connect(module): + + """ Return an STS connection""" + + region, ec2_url, boto_params = get_aws_connection_info(module) + + # If we have a region specified, connect to its endpoint. + if region: + try: + sts = connect_to_aws(boto.sts, region, **boto_params) + except boto.exception.NoAuthHandlerFound, e: + module.fail_json(msg=str(e)) + # Otherwise, no region so we fallback to connect_sts method + else: + try: + sts = boto.connect_sts(**boto_params) + except boto.exception.NoAuthHandlerFound, e: + module.fail_json(msg=str(e)) + + + return sts + +def assumeRole(): + data = sts.assume_role() + return data + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + role_arn = dict(required=True), + role_session_name = dict(required=True), + duraction_seconds = dict(), + external_id = dict(), + policy = dict(), + mfa_serial_number = dict(), + mfa_token = dict(), + ) + ) + module = AnsibleModule(argument_spec=argument_spec) + + role_arn = module.params.get('role_arn') + role_session_name = module.params.get('role_session_name') + policy = module.params.get('policy') + duraction_seconds = module.params.get('duraction_seconds') + external_id = module.params.get('external_id') + mfa_serial_number = module.params.get('mfa_serial_number') + mfa_token = module.params.get('mfa_token') + + sts = sts_connect(module) + + temp_credentials = {} + + try: + temp_credentials = sts.assume_role(role_arn, role_session_name, policy, duraction_seconds, + external_id, mfa_serial_number, mfa_token).credentials.__dict__ + except boto.exception.BotoServerError, e: + module.fail_json(msg='Unable to assume role {0}, error: {1}'.format(role_arn, e)) + result = dict(changed=False, ansible_facts=dict(ansible_temp_credentials=temp_credentials)) + + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +main() From a21e23846d20359d7d20236431e0bb662cd2a851 Mon Sep 17 00:00:00 2001 From: Phil Schwartz Date: Tue, 30 Dec 2014 08:42:00 -0600 Subject: [PATCH 0018/2522] init commit --- windows/win_unzip.ps1 | 83 +++++++++++++++++++++++++++++++++++++++ windows/win_unzip.py | 90 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 windows/win_unzip.ps1 create mode 100644 windows/win_unzip.py diff --git a/windows/win_unzip.ps1 b/windows/win_unzip.ps1 new file mode 100644 index 00000000000..de9fb73e7ce --- /dev/null +++ b/windows/win_unzip.ps1 @@ -0,0 +1,83 @@ +#!powershell +# This file is part of Ansible +# +# Copyright 2014, Phil Schwartz +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# WANT_JSON +# POWERSHELL_COMMON + +$params = Parse-Args $args; + +$result = New-Object psobject @{ + win_unzip = New-Object psobject + changed = $false +} + +If ($params.zip) { + $zip = $params.zip.toString() + + If (-Not (Test-Path -path $zip)){ + Fail-Json $result "zip file: $zip does not exist." + } +} +Else { + Fail-Json $result "missing required argument: zip" +} + +If (-Not($params.dest -eq $null)) { + $dest = $params.dest.toString() + + If (-Not (Test-Path $dest -PathType Container)){ + New-Item -itemtype directory -path $dest + } +} +Else { + Fail-Json $result "missing required argument: dest" +} + +Try { + cd C:\ + $shell = New-Object -ComObject Shell.Application + $shell.NameSpace($dest).copyhere(($shell.NameSpace($zip)).items(), 20) + $result.changed = $true +} +Catch { + $sp = $zip.split(".") + $ext = $sp[$sp.length-1] + + # Used to allow reboot after exe hotfix extraction (Windows 2008 R2 SP1) + # This will have no effect in most cases. + If (-Not ($ext -eq "exe")){ + $result.changed = $false + Fail-Json $result "Error unzipping $zip to $dest" + } +} + +If ($params.rm -eq "true"){ + Remove-Item $zip -Recurse -Force + Set-Attr $result.win_unzip "rm" "true" +} + +If ($params.restart -eq "true") { + Restart-Computer -Force + Set-Attr $result.win_unzip "restart" "true" +} + + +Set-Attr $result.win_unzip "zip" $zip.toString() +Set-Attr $result.win_unzip "dest" $dest.toString() + +Exit-Json $result; diff --git a/windows/win_unzip.py b/windows/win_unzip.py new file mode 100644 index 00000000000..ae2bfa94ad8 --- /dev/null +++ b/windows/win_unzip.py @@ -0,0 +1,90 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2014, Phil Schwartz +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# this is a windows documentation stub. actual code lives in the .ps1 +# file of the same name + +DOCUMENTATION = ''' +--- +module: win_unzip +version_added: "" +short_description: Unzips compressed files on the Windows node +description: + - Unzips compressed files, and can force reboot (if needed, i.e. such as hotfixes). +options: + zip: + description: + - Zip file to be unzipped (provide absolute path) + required: true + default: null + aliases: [] + dest: + description: + - Destination of zip file (provide absolute path of directory) + required: true + default: null + aliases: [] + rm: + description: + - Remove the zip file, after unzipping + required: no + default: false + aliases: [] + restart: + description: + - Restarts the computer after unzip, can be useful for hotfixes such as http://support.microsoft.com/kb/2842230 (Restarts will have to be accounted for with wait_for module) + choices: + - true + - false + required: false + default: false + aliases: [] +author: Phil Schwartz +''' + +EXAMPLES = ''' +# This unzips hotfix http://support.microsoft.com/kb/2842230 and forces reboot (for hotfix to take effect) +$ ansible -i hosts -m win_unzip -a "zip=C:\\463984_intl_x64_zip.exe dest=C:\\Hotfix restart=true" all +# This unzips a library that was downloaded with win_get_url, and removes the file after extraction +$ ansible -i hosts -m win_unzip -a "zip=C:\\LibraryToUnzip.zip dest=C:\\Lib rm=true" all +# Playbook example +--- +- name: Install WinRM PowerShell Hotfix for Windows Server 2008 SP1 + hosts: all + gather_facts: false + tasks: + - name: Grab Hotfix from URL + win_get_url: + url: 'http://hotfixv4.microsoft.com/Windows%207/Windows%20Server2008%20R2%20SP1/sp2/Fix467402/7600/free/463984_intl_x64_zip.exe' + dest: 'C:\\463984_intl_x64_zip.exe' + - name: Unzip hotfix + win_unzip: + zip: "C:\\463984_intl_x64_zip.exe" + dest: "C:\\Hotfix" + restart: true + - name: Wait for server reboot... + local_action: + module: wait_for + host={{ inventory_hostname }} + port={{ansible_ssh_port|default(5986)}} + delay=15 + timeout=600 + state=started +''' From fd12a5cc8446eb302e643349f9c82bef275e95f6 Mon Sep 17 00:00:00 2001 From: Phil Schwartz Date: Tue, 30 Dec 2014 09:54:22 -0600 Subject: [PATCH 0019/2522] specifies creation of directory if !exists - added try catch for creation of directory, in case of an invalid path specified - added specification to documentation --- windows/win_unzip.ps1 | 7 ++++++- windows/win_unzip.py | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/windows/win_unzip.ps1 b/windows/win_unzip.ps1 index de9fb73e7ce..e76c51dc6aa 100644 --- a/windows/win_unzip.ps1 +++ b/windows/win_unzip.ps1 @@ -41,7 +41,12 @@ If (-Not($params.dest -eq $null)) { $dest = $params.dest.toString() If (-Not (Test-Path $dest -PathType Container)){ - New-Item -itemtype directory -path $dest + Try{ + New-Item -itemtype directory -path $dest + } + Catch { + Fail-Json $result "Error creating $dest directory" + } } } Else { diff --git a/windows/win_unzip.py b/windows/win_unzip.py index ae2bfa94ad8..f9ba5ded0d0 100644 --- a/windows/win_unzip.py +++ b/windows/win_unzip.py @@ -27,7 +27,7 @@ version_added: "" short_description: Unzips compressed files on the Windows node description: - - Unzips compressed files, and can force reboot (if needed, i.e. such as hotfixes). + - Unzips compressed files, and can force reboot (if needed, i.e. such as hotfixes). If the destination directory does not exist, it will be created before unzipping the file. Specifying rm parameter will allow removal of the zip file after extraction. options: zip: description: @@ -37,7 +37,7 @@ aliases: [] dest: description: - - Destination of zip file (provide absolute path of directory) + - Destination of zip file (provide absolute path of directory). If it does not exist, the directory will be created. required: true default: null aliases: [] From 61d3f23c032457ff1a350b7859b5aca193ad4eb9 Mon Sep 17 00:00:00 2001 From: Phil Schwartz Date: Wed, 31 Dec 2014 10:34:32 -0600 Subject: [PATCH 0020/2522] edit check for extension to use library func --- windows/win_unzip.ps1 | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/windows/win_unzip.ps1 b/windows/win_unzip.ps1 index e76c51dc6aa..6772792be08 100644 --- a/windows/win_unzip.ps1 +++ b/windows/win_unzip.ps1 @@ -41,7 +41,7 @@ If (-Not($params.dest -eq $null)) { $dest = $params.dest.toString() If (-Not (Test-Path $dest -PathType Container)){ - Try{ + Try{ New-Item -itemtype directory -path $dest } Catch { @@ -54,18 +54,14 @@ Else { } Try { - cd C:\ $shell = New-Object -ComObject Shell.Application $shell.NameSpace($dest).copyhere(($shell.NameSpace($zip)).items(), 20) $result.changed = $true } Catch { - $sp = $zip.split(".") - $ext = $sp[$sp.length-1] - # Used to allow reboot after exe hotfix extraction (Windows 2008 R2 SP1) # This will have no effect in most cases. - If (-Not ($ext -eq "exe")){ + If (-Not ([System.IO.Path]::GetExtension($zip) -match ".exe")){ $result.changed = $false Fail-Json $result "Error unzipping $zip to $dest" } From a95fabeeb2fff2d0e16b532d35da4dd60adb0b22 Mon Sep 17 00:00:00 2001 From: Phil Schwartz Date: Wed, 31 Dec 2014 19:17:53 -0600 Subject: [PATCH 0021/2522] fixes rm & restart param checks --- windows/win_unzip.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/windows/win_unzip.ps1 b/windows/win_unzip.ps1 index 6772792be08..e77aa9e1df3 100644 --- a/windows/win_unzip.ps1 +++ b/windows/win_unzip.ps1 @@ -67,12 +67,12 @@ Catch { } } -If ($params.rm -eq "true"){ +If ($params.rm -eq "true" -Or $params.rm -eq "yes"){ Remove-Item $zip -Recurse -Force Set-Attr $result.win_unzip "rm" "true" } -If ($params.restart -eq "true") { +If ($params.restart -eq "true" -Or $params.restart -eq "yes") { Restart-Computer -Force Set-Attr $result.win_unzip "restart" "true" } From 99927a5c54aeca8ff18359f17d776a5f624d32a7 Mon Sep 17 00:00:00 2001 From: schwartzmx Date: Sun, 11 Jan 2015 13:03:26 -0600 Subject: [PATCH 0022/2522] =?UTF-8?q?updates=20docs,=20adds=20unzip=20func?= =?UTF-8?q?=20for=20bz2=20gz=20tar=20msu=C2=AC=20-=20Added=20functionality?= =?UTF-8?q?=20for=20unzipping/decompressing=20bzip=20gzip=20tar=20exe=20(s?= =?UTF-8?q?elf=20extracting)=20and=20msu=20(ms=20update)=20files=20to=20co?= =?UTF-8?q?incide=20with=20added=20functionality=20to=20win=5Fzip=C2=AC=20?= =?UTF-8?q?=20=20-=20Added=20functionality=20requires=20PSCX=20(it=20will?= =?UTF-8?q?=20be=20installed=20if=20it=20can't=20be=20imported)=C2=AC?= =?UTF-8?q?=C2=AC=20=20=20=20=20-=20First=20try=20with=20chocolatey,=20if?= =?UTF-8?q?=20fail,=20direct=20install=20from=20msi=20-=20Added=20recurse?= =?UTF-8?q?=20param=20to=20recursively=20unzip=20files=20from=20a=20compre?= =?UTF-8?q?ssed=20folder=C2=AC=20=20=20-=20useful=20for=20example:=20unzip?= =?UTF-8?q?ping=20a=20Log.zip=20file=20that=20contains=20a=20load=20of=20.?= =?UTF-8?q?gz=20files=C2=AC=20=20=20=20=20-=20setting=20rm=20param=20to=20?= =?UTF-8?q?true=20will=20remove=20all=20compressed=20files=20after=20decom?= =?UTF-8?q?pressing=C2=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- windows/win_unzip.ps1 | 147 ++++++++++++++++++++++++++++++++++++------ windows/win_unzip.py | 53 +++++++++++++-- 2 files changed, 174 insertions(+), 26 deletions(-) diff --git a/windows/win_unzip.ps1 b/windows/win_unzip.ps1 index e77aa9e1df3..f31a6273a39 100644 --- a/windows/win_unzip.ps1 +++ b/windows/win_unzip.ps1 @@ -26,15 +26,17 @@ $result = New-Object psobject @{ changed = $false } -If ($params.zip) { - $zip = $params.zip.toString() +If ($params.src) { + $src = $params.src.toString() - If (-Not (Test-Path -path $zip)){ - Fail-Json $result "zip file: $zip does not exist." + If (-Not (Test-Path -path $src)){ + Fail-Json $result "src file: $src does not exist." } + + $ext = [System.IO.Path]::GetExtension($dest) } Else { - Fail-Json $result "missing required argument: zip" + Fail-Json $result "missing required argument: src" } If (-Not($params.dest -eq $null)) { @@ -53,22 +55,120 @@ Else { Fail-Json $result "missing required argument: dest" } -Try { - $shell = New-Object -ComObject Shell.Application - $shell.NameSpace($dest).copyhere(($shell.NameSpace($zip)).items(), 20) - $result.changed = $true +If ($params.recurse -eq "true" -Or $params.recurse -eq "yes") { + $recurse = $true } -Catch { - # Used to allow reboot after exe hotfix extraction (Windows 2008 R2 SP1) - # This will have no effect in most cases. - If (-Not ([System.IO.Path]::GetExtension($zip) -match ".exe")){ - $result.changed = $false - Fail-Json $result "Error unzipping $zip to $dest" - } +Else { + $recurse = $false } If ($params.rm -eq "true" -Or $params.rm -eq "yes"){ - Remove-Item $zip -Recurse -Force + $rm = $true + Set-Attr $result.win_unzip "rm" "true" +} +Else { + $rm = $false +} + +If ($ext -eq ".zip" -And $recurse -eq $false) { + Try { + $shell = New-Object -ComObject Shell.Application + $shell.NameSpace($dest).copyhere(($shell.NameSpace($src)).items(), 20) + $result.changed = $true + } + Catch { + Fail-Json $result "Error unzipping $src to $dest" + } +} +# Need PSCX +Else { + # Requires PSCX, will be installed if it isn't found + # Pscx-3.2.0.msi + $url = "http://download-codeplex.sec.s-msft.com/Download/Release?ProjectName=pscx&DownloadId=923562&FileTime=130585918034470000&Build=20959" + $msi = "C:\Pscx-3.2.0.msi" + + # Check if PSCX is installed + $list = Get-Module -ListAvailable + # If not download it and install + If (-Not ($list -match "PSCX")) { + # Try install with chocolatey + Try { + cinst -force PSCX + $choco = $true + } + Catch { + $choco = $false + } + # install from downloaded msi if choco failed or is not present + If ($choco -eq $false) { + Try { + $client = New-Object System.Net.WebClient + $client.DownloadFile($url, $msi) + } + Catch { + Fail-Json $result "Error downloading PSCX from $url and saving as $dest" + } + Try { + msiexec.exe /i $msi /qb + # Give it a chance to install, so that it can be imported + sleep 10 + } + Catch { + Fail-Json $result "Error installing $msi" + } + } + Set-Attr $result.win_zip "pscx_status" "pscx was installed" + $installed = $true + } + Else { + Set-Attr $result.win_zip "pscx_status" "present" + } + + # Import + Try { + If ($installed) { + Import-Module 'C:\Program Files (x86)\Powershell Community Extensions\pscx3\pscx\pscx.psd1' + } + Else { + Import-Module PSCX + } + } + Catch { + Fail-Json $result "Error importing module PSCX" + } + + Try { + If ($recurse) { + Expand-Archive -Path $src -OutputPath $dest -Force + + If ($rm) { + Get-ChildItem $dest -recurse | Where {$_.extension -eq ".gz" -Or $_.extension -eq ".zip" -Or $_.extension -eq ".bz2" -Or $_.extension -eq ".tar" -Or $_.extension -eq ".msu"} | % { + Expand-Archive $_.FullName -OutputPath $dest -Force + Remove-Item $_.FullName -Force + } + } + Else { + Get-ChildItem $dest -recurse | Where {$_.extension -eq ".gz" -Or $_.extension -eq ".zip" -Or $_.extension -eq ".bz2" -Or $_.extension -eq ".tar" -Or $_.extension -eq ".msu"} | % { + Expand-Archive $_.FullName -OutputPath $dest -Force + } + } + } + Else { + Expand-Archive -Path $src -OutputPath $dest -Force + } + } + Catch { + If ($recurse) { + Fail-Json "Error recursively expanding $src to $dest" + } + Else { + Fail-Json "Error expanding $src to $dest" + } + } +} + +If ($rm -eq $true){ + Remove-Item $src -Recurse -Force Set-Attr $result.win_unzip "rm" "true" } @@ -77,8 +177,17 @@ If ($params.restart -eq "true" -Or $params.restart -eq "yes") { Set-Attr $result.win_unzip "restart" "true" } - -Set-Attr $result.win_unzip "zip" $zip.toString() +# Fixes a fail error message (when the task actually succeeds) for a "Convert-ToJson: The converted JSON string is in bad format" +# This happens when JSON is parsing a string that ends with a "\", which is possible when specifying a directory to download to. +# This catches that possible error, before assigning the JSON $result +If ($src[$src.length-1] -eq "\") { + $src = $src.Substring(0, $src.length-1) +} +If ($dest[$dest.length-1] -eq "\") { + $dest = $dest.Substring(0, $dest.length-1) +} +Set-Attr $result.win_unzip "src" $src.toString() Set-Attr $result.win_unzip "dest" $dest.toString() +Set-Attr $result.win_unzip "recurse" $recurse.toString() Exit-Json $result; diff --git a/windows/win_unzip.py b/windows/win_unzip.py index f9ba5ded0d0..35093aa8c76 100644 --- a/windows/win_unzip.py +++ b/windows/win_unzip.py @@ -27,11 +27,11 @@ version_added: "" short_description: Unzips compressed files on the Windows node description: - - Unzips compressed files, and can force reboot (if needed, i.e. such as hotfixes). If the destination directory does not exist, it will be created before unzipping the file. Specifying rm parameter will allow removal of the zip file after extraction. + - Unzips compressed files, and can force reboot (if needed, i.e. such as hotfixes). Has ability to recursively unzip files within the src zip file provided using Read-Archive and piping to Expand-Archive (Using PSCX). If the destination directory does not exist, it will be created before unzipping the file. If a .zip file is specified as src and recurse is true then PSCX will be installed. Specifying rm parameter will allow removal of the src file after extraction. options: - zip: + src: description: - - Zip file to be unzipped (provide absolute path) + - File to be unzipped (provide absolute path) required: true default: null aliases: [] @@ -45,14 +45,32 @@ description: - Remove the zip file, after unzipping required: no + choices: + - true + - false + - yes + - no default: false aliases: [] + recurse: + description: + - Recursively expand zipped files within the src file. + required: no + default: false + choices: + - true + - false + - yes + - no + aliases: [] restart: description: - Restarts the computer after unzip, can be useful for hotfixes such as http://support.microsoft.com/kb/2842230 (Restarts will have to be accounted for with wait_for module) choices: - true - false + - yes + - no required: false default: false aliases: [] @@ -60,11 +78,31 @@ ''' EXAMPLES = ''' -# This unzips hotfix http://support.microsoft.com/kb/2842230 and forces reboot (for hotfix to take effect) -$ ansible -i hosts -m win_unzip -a "zip=C:\\463984_intl_x64_zip.exe dest=C:\\Hotfix restart=true" all # This unzips a library that was downloaded with win_get_url, and removes the file after extraction -$ ansible -i hosts -m win_unzip -a "zip=C:\\LibraryToUnzip.zip dest=C:\\Lib rm=true" all +$ ansible -i hosts -m win_unzip -a "src=C:\\LibraryToUnzip.zip dest=C:\\Lib rm=true" all # Playbook example + +# Simple unzip +--- +- name: Unzip a bz2 (BZip) file + win_unzip: + src: "C:\Users\Phil\Logs.bz2" + dest: "C:\Users\Phil\OldLogs" + +# This playbook example unzips a .zip file and recursively decompresses the contained .gz files and removes all unneeded compressed files after completion. +--- +- name: Unzip ApplicationLogs.zip and decompress all GZipped log files + hosts: all + gather_facts: false + tasks: + - name: Recursively decompress GZ files in ApplicationLogs.zip + win_unzip: + src: C:\Downloads\ApplicationLogs.zip + dest: C:\Application\Logs + recurse: yes + rm: true + +# Install hotfix (self-extracting .exe) --- - name: Install WinRM PowerShell Hotfix for Windows Server 2008 SP1 hosts: all @@ -76,8 +114,9 @@ dest: 'C:\\463984_intl_x64_zip.exe' - name: Unzip hotfix win_unzip: - zip: "C:\\463984_intl_x64_zip.exe" + src: "C:\\463984_intl_x64_zip.exe" dest: "C:\\Hotfix" + recurse: true restart: true - name: Wait for server reboot... local_action: From 555ff23434aa5810f035963c6197596edba14836 Mon Sep 17 00:00:00 2001 From: Billy Kimble Date: Mon, 12 Jan 2015 14:13:08 -0800 Subject: [PATCH 0023/2522] added hall.com notification module --- notification/hall.py | 97 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100755 notification/hall.py diff --git a/notification/hall.py b/notification/hall.py new file mode 100755 index 00000000000..7c76e52379f --- /dev/null +++ b/notification/hall.py @@ -0,0 +1,97 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Billy Kimble +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + + +DOCUMENTATION = """ +module: hall +short_description: Send notification to Hall +description: + - The M(hall) module connects to the U(https://hall.com) messaging API and allows you to deliver notication messages to rooms. +version_added: 1.6 +author: Billy Kimble +options: + room_token: + description: + - Room token provided to you by setting up the Ansible room integation on U(https://hall.com) + required: true + msg: + description: + - The message you wish to deliver as a notifcation + required: true + title: + description: + - The title of the message + required: true + picture: + description: + - The full URL to the image you wish to use for the Icon of the message. Defaults to U(http://cdn2.hubspot.net/hub/330046/file-769078210-png/Official_Logos/ansible_logo_black_square_small.png?t=1421076128627) + required: false +""" + +EXAMPLES = """ +- name: Send Hall notifiation + local_action: + module: hall + room_token: + title: Nginx + msg: Created virtual host file on {{ inventory_hostname }} + +- name: Send Hall notification if EC2 servers were created. + when: ec2.instances|length > 0 + local_action: + module: hall + room_token: + title: Server Creation + msg: "Created EC2 instance {{ item.id }} of type {{ item.instance_type }}.\\nInstance can be reached at {{ item.public_ip }} in the {{ item.region }} region." + with_items: ec2.instances +""" + +HALL_API_ENDPOINT = 'https://hall.com/api/1/services/generic/%s' + +def send_request_to_hall(module, room_token, payload): + headers = {'Content-Type': 'application/json'} + payload=module.jsonify(payload) + api_endpoint = HALL_API_ENDPOINT % (room_token) + response, info = fetch_url(module, api_endpoint, data=payload, headers=headers) + if info['status'] != 200: + secure_url = HALL_API_ENDPOINT % ('[redacted]') + module.fail_json(msg=" failed to send %s to %s: %s" % (payload, secure_url, info['msg'])) + +def main(): + module = AnsibleModule( + argument_spec = dict( + room_token = dict(type='str', required=True), + msg = dict(type='str', required=True), + title = dict(type='str', required=True), + picture = dict(type='str', default='http://cdn2.hubspot.net/hub/330046/file-769078210-png/Official_Logos/ansible_logo_black_square_small.png?t=1421076128627'), + ) + ) + + room_token = module.params['room_token'] + message = module.params['msg'] + title = module.params['title'] + picture = module.params['picture'] + payload = {'title': title, 'message': message, 'picture': picture} + send_request_to_hall(module, room_token, payload) + module.exit_json(msg="OK") + +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * +main() From 4ece7362fba8286a032fb4e70e02dff381ca5087 Mon Sep 17 00:00:00 2001 From: schwartzmx Date: Wed, 14 Jan 2015 22:25:22 -0600 Subject: [PATCH 0024/2522] inital commit win_acl --- windows/win_acl.ps1 | 146 +++++++++++++++++++++++++++++++++++++++++++ windows/win_acl.py | 147 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 293 insertions(+) create mode 100644 windows/win_acl.ps1 create mode 100644 windows/win_acl.py diff --git a/windows/win_acl.ps1 b/windows/win_acl.ps1 new file mode 100644 index 00000000000..320627c03f0 --- /dev/null +++ b/windows/win_acl.ps1 @@ -0,0 +1,146 @@ +#!powershell +# This file is part of Ansible +# +# Copyright 2014, Phil Schwartz +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# WANT_JSON +# POWERSHELL_COMMON + +# win_acl module (File/Resources Permission Additions/Removal) +$params = Parse-Args $args; + +$result = New-Object psobject @{ + win_acl = New-Object psobject + changed = $false +} + +If ($params.src) { + $src = $params.src.toString() + + If (-Not (Test-Path -Path $src -PathType Leaf -Or Test-Path -Path $src -PathType Container)) { + Fail-Json $result "$src is not a valid file or directory on the host" + } +} +Else { + Fail-Json $result "missing required argument: src" +} + +If ($params.user) { + $user = $params.user.toString() + + # Test that the user/group exists on the local machine + $localComputer = [ADSI]("WinNT://"+[System.Net.Dns]::GetHostName()) + $list = ($localComputer.psbase.children | Where-Object { (($_.psBase.schemaClassName -eq "User") -Or ($_.psBase.schemaClassName -eq "Group"))} | Select-Object -expand Name) + If (-Not ($list -contains "$user")) { + Fail-Json $result "$user is not a valid user or group on the host machine" + } +} +Else { + Fail-Json $result "missing required argument: user. specify the user or group to apply permission changes." +} + +If ($params.type -eq "allow") { + $type = $true +} +ElseIf ($params.type -eq "deny") { + $type = $false +} +Else { + Fail-Json $result "missing required argument: type. specify whether to allow or deny the specified rights." +} + +If ($params.inherit) { + # If it's a file then no flags can be set or an exception will be thrown + If (Test-Path -Path $src -PathType Leaf) { + $inherit = "None" + } + Else { + $inherit = $params.inherit.toString() + } +} +Else { + # If it's a file then no flags can be set or an exception will be thrown + If (Test-Path -Path $src -PathType Leaf) { + $inherit = "None" + } + Else { + $inherit = "ContainerInherit, ObjectInherit" + } +} + +If ($params.propagation) { + $propagation = $params.propagation.toString() +} +Else { + $propagation = "None" +} + +If ($params.rights) { + $rights = $params.rights.toString() +} +Else { + Fail-Json $result "missing required argument: rights" +} + +If ($params.state -eq "absent") { + $state = "remove" +} +Else { + $state = "add" +} + +Try { + $colRights = [System.Security.AccessControl.FileSystemRights]$rights + $InheritanceFlag = [System.Security.AccessControl.InheritanceFlags]$inherit + $PropagationFlag = [System.Security.AccessControl.PropagationFlags]$propagation + + If ($type) { + $objType =[System.Security.AccessControl.AccessControlType]::Allow + } + Else { + $objType =[System.Security.AccessControl.AccessControlType]::Deny + } + + $objUser = New-Object System.Security.Principal.NTAccount($user) + $objACE = New-Object System.Security.AccessControl.FileSystemAccessRule ($objUser, $colRights, $InheritanceFlag, $PropagationFlag, $objType) + $objACL = Get-ACL $src + + If ($state -eq "add") { + Try { + $objACL.AddAccessRule($objACE) + } + Catch { + Fail-Json $result "an exception occured when adding the specified rule. it may already exist." + } + } + Else { + Try { + $objACL.RemoveAccessRule($objACE) + } + Catch { + Fail-Json $result "an exception occured when removing the specified rule. it may not exist." + } + } + + Set-ACL $src $objACL + + $result.changed = $true +} +Catch { + Fail-Json $result "an error occured when attempting to $state $rights permission(s) on $src for $user" +} + +Exit-Json $result \ No newline at end of file diff --git a/windows/win_acl.py b/windows/win_acl.py new file mode 100644 index 00000000000..56f8c84d0db --- /dev/null +++ b/windows/win_acl.py @@ -0,0 +1,147 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2014, Phil Schwartz +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# this is a windows documentation stub. actual code lives in the .ps1 +# file of the same name + +DOCUMENTATION = ''' +--- +module: win_acl +version_added: "" +short_description: Set file/directory permissions for a system user or group. +description: + - Add or remove rights/permissions for a given user or group for the specified src file or folder. +options: + src: + description: + - File or Directory + required: yes + default: none + aliases: [] + user: + description: + - User or Group to add specified rights to act on src file/folder + required: yes + default: none + aliases: [] + state: + description: + - Specify whether to add (present) or remove (absent) the specified access rule + required: no + choices: + - present + - absent + default: present + aliases: [] + type: + description: + - Specify whether to allow or deny the rights specified + required: yes + choices: + - allow + - deny + default: none + aliases: [] + rights: + description: + - The rights/permissions that are to be allowed/denyed for the specified user or group for the given src file or directory. Can be entered as a comma separated list (Ex. "Modify, Delete, ExecuteFile"). For more information on the choices see MSDN FileSystemRights Enumeration. + required: yes + choices: + - AppendData + - ChangePermissions + - Delete + - DeleteSubdirectoriesAndFiles + - ExecuteFile + - FullControl + - ListDirectory + - Modify + - Read + - ReadAndExecute + - ReadAttributes + - ReadData + - ReadExtendedAttributes + - ReadPermissions + - Synchronize + - TakeOwnership + - Traverse + - Write + - WriteAttributes + - WriteData + - WriteExtendedAttributes + default: none + aliases: [] + inherit: + description: + - Inherit flags on the ACL rules. Can be specified as a comma separated list (Ex. "ContainerInherit, ObjectInherit"). For more information on the choices see MSDN InheritanceFlags Enumeration. + required: no + choices: + - ContainerInherit + - ObjectInherit + - None + default: For Leaf File: None; For Directory: ContainerInherit, ObjectInherit; + aliases: [] + propagation: + description: + - Propagation flag on the ACL rules. For more information on the choices see MSDN PropagationFlags Enumeration. + required: no + choices: + - None + - NoPropagateInherit + - InheritOnly + default: "None" + aliases: [] +author: Phil Schwartz +''' + +EXAMPLES = ''' +# Restrict write,execute access to User Fed-Phil +$ ansible -i hosts -m win_acl -a "user=Fed-Phil src=C:\Important\Executable.exe type=deny rights='ExecuteFile,Write'" all + +# Playbook example +# Add access rule to allow IIS_IUSRS FullControl to MySite +--- +- name: Add IIS_IUSRS allow rights + win_acl: + src: 'C:\inetpub\wwwroot\MySite' + user: 'IIS_IUSRS' + rights: 'FullControl' + type: 'allow' + state: 'present' + inherit: 'ContainerInherit, ObjectInherit' + propagation: 'None' + +# Remove previously added rule for IIS_IUSRS +- name: Remove FullControl AccessRule for IIS_IUSRS + src: 'C:\inetpub\wwwroot\MySite' + user: 'IIS_IUSRS' + rights: 'FullControl' + type: 'allow' + state: 'absent' + inherit: 'ContainerInherit, ObjectInherit' + propagation: 'None' + +# Deny Intern +- name: Deny Deny + src: 'C:\Administrator\Documents' + user: 'Intern' + rights: 'Read,Write,Modify,FullControl,Delete' + type: 'deny' + state: 'present' +''' \ No newline at end of file From 9cb97b28986548e20eb2e1e16c1ed810359b6b66 Mon Sep 17 00:00:00 2001 From: Sebastien ROHAUT Date: Fri, 23 Jan 2015 20:07:27 +0100 Subject: [PATCH 0025/2522] Add "-" to ulimit type Just edited pam_types to add the '-', as explained in man 5 limits.conf --- system/pam_limits.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/pam_limits.py b/system/pam_limits.py index 2a6bec383fd..649b9a175f7 100644 --- a/system/pam_limits.py +++ b/system/pam_limits.py @@ -95,7 +95,7 @@ def main(): pam_items = [ 'core', 'data', 'fsize', 'memlock', 'nofile', 'rss', 'stack', 'cpu', 'nproc', 'as', 'maxlogins', 'maxsyslogins', 'priority', 'locks', 'sigpending', 'msgqueue', 'nice', 'rtprio', 'chroot' ] - pam_types = [ 'soft', 'hard' ] + pam_types = [ 'soft', 'hard', '-' ] limits_conf = '/etc/security/limits.conf' From 0cff70b67805f1025cdd3709303191232170fbd5 Mon Sep 17 00:00:00 2001 From: Kevin Date: Thu, 12 Feb 2015 16:14:13 +0000 Subject: [PATCH 0026/2522] Remove `homebrew-` prefix when checking if repo has already been tapped See: https://github.com/Homebrew/homebrew/blob/master/Library/Homebrew/cmd/tap.rb Example: ``` $ brew tap neovim/homebrew-neovim $ brew tap neovim/neovim ... ``` --- packaging/os/homebrew_tap.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packaging/os/homebrew_tap.py b/packaging/os/homebrew_tap.py index a79ba076a8a..d48a7787b27 100644 --- a/packaging/os/homebrew_tap.py +++ b/packaging/os/homebrew_tap.py @@ -63,8 +63,11 @@ def already_tapped(module, brew_path, tap): brew_path, 'tap', ]) + taps = [tap_.strip().lower() for tap_ in out.split('\n') if tap_] - return tap.lower() in taps + tap_name = re.sub('homebrew-', '', tap.lower()) + + return tap_name in taps def add_tap(module, brew_path, tap): From 46f53724f0005411a6e2526aaef2ada3fc6d6af9 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 13 Feb 2015 13:37:16 -0500 Subject: [PATCH 0027/2522] Restore rax_mon_* modules. --- cloud/rackspace/rax_mon_alarm.py | 240 ++++++++++++++ cloud/rackspace/rax_mon_check.py | 323 +++++++++++++++++++ cloud/rackspace/rax_mon_entity.py | 196 +++++++++++ cloud/rackspace/rax_mon_notification.py | 187 +++++++++++ cloud/rackspace/rax_mon_notification_plan.py | 186 +++++++++++ 5 files changed, 1132 insertions(+) create mode 100644 cloud/rackspace/rax_mon_alarm.py create mode 100644 cloud/rackspace/rax_mon_check.py create mode 100644 cloud/rackspace/rax_mon_entity.py create mode 100644 cloud/rackspace/rax_mon_notification.py create mode 100644 cloud/rackspace/rax_mon_notification_plan.py diff --git a/cloud/rackspace/rax_mon_alarm.py b/cloud/rackspace/rax_mon_alarm.py new file mode 100644 index 00000000000..f5fc9593abd --- /dev/null +++ b/cloud/rackspace/rax_mon_alarm.py @@ -0,0 +1,240 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# This is a DOCUMENTATION stub specific to this module, it extends +# a documentation fragment located in ansible.utils.module_docs_fragments +DOCUMENTATION = ''' +--- +module: rax_mon_alarm +short_description: Create or delete a Rackspace Cloud Monitoring alarm. +description: +- Create or delete a Rackspace Cloud Monitoring alarm that associates an + existing rax_mon_entity, rax_mon_check, and rax_mon_notification_plan with + criteria that specify what conditions will trigger which levels of + notifications. Rackspace monitoring module flow | rax_mon_entity -> + rax_mon_check -> rax_mon_notification -> rax_mon_notification_plan -> + *rax_mon_alarm* +version_added: "1.8.2" +options: + state: + description: + - Ensure that the alarm with this C(label) exists or does not exist. + choices: [ "present", "absent" ] + required: false + default: present + label: + description: + - Friendly name for this alarm, used to achieve idempotence. Must be a String + between 1 and 255 characters long. + required: true + entity_id: + description: + - ID of the entity this alarm is attached to. May be acquired by registering + the value of a rax_mon_entity task. + required: true + check_id: + description: + - ID of the check that should be alerted on. May be acquired by registering + the value of a rax_mon_check task. + required: true + notification_plan_id: + description: + - ID of the notification plan to trigger if this alarm fires. May be acquired + by registering the value of a rax_mon_notification_plan task. + required: true + criteria: + description: + - Alarm DSL that describes alerting conditions and their output states. Must + be between 1 and 16384 characters long. See + http://docs.rackspace.com/cm/api/v1.0/cm-devguide/content/alerts-language.html + for a reference on the alerting language. + disabled: + description: + - If yes, create this alarm, but leave it in an inactive state. Defaults to + no. + choices: [ "yes", "no" ] + metadata: + description: + - Arbitrary key/value pairs to accompany the alarm. Must be a hash of String + keys and values between 1 and 255 characters long. +author: Ash Wilson +extends_documentation_fragment: rackspace.openstack +''' + +EXAMPLES = ''' +- name: Alarm example + gather_facts: False + hosts: local + connection: local + tasks: + - name: Ensure that a specific alarm exists. + rax_mon_alarm: + credentials: ~/.rax_pub + state: present + label: uhoh + entity_id: "{{ the_entity['entity']['id'] }}" + check_id: "{{ the_check['check']['id'] }}" + notification_plan_id: "{{ defcon1['notification_plan']['id'] }}" + criteria: > + if (rate(metric['average']) > 10) { + return new AlarmStatus(WARNING); + } + return new AlarmStatus(OK); + register: the_alarm +''' + +try: + import pyrax + HAS_PYRAX = True +except ImportError: + HAS_PYRAX = False + +def alarm(module, state, label, entity_id, check_id, notification_plan_id, criteria, + disabled, metadata): + + # Verify the presence of required attributes. + + required_attrs = { + "label": label, "entity_id": entity_id, "check_id": check_id, + "notification_plan_id": notification_plan_id + } + + for (key, value) in required_attrs.iteritems(): + if not value: + module.fail_json(msg=('%s is required for rax_mon_alarm' % key)) + + if len(label) < 1 or len(label) > 255: + module.fail_json(msg='label must be between 1 and 255 characters long') + + if criteria and len(criteria) < 1 or len(criteria) > 16384: + module.fail_json(msg='criteria must be between 1 and 16384 characters long') + + # Coerce attributes. + + changed = False + alarm = None + + cm = pyrax.cloud_monitoring + if not cm: + module.fail_json(msg='Failed to instantiate client. This typically ' + 'indicates an invalid region or an incorrectly ' + 'capitalized region name.') + + existing = [a for a in cm.list_alarms(entity_id) if a.label == label] + + if existing: + alarm = existing[0] + + if state == 'present': + should_create = False + should_update = False + should_delete = False + + if len(existing) > 1: + module.fail_json(msg='%s existing alarms have the label %s.' % + (len(existing), label)) + + if alarm: + if check_id != alarm.check_id or notification_plan_id != alarm.notification_plan_id: + should_delete = should_create = True + + should_update = (disabled and disabled != alarm.disabled) or \ + (metadata and metadata != alarm.metadata) or \ + (criteria and criteria != alarm.criteria) + + if should_update and not should_delete: + cm.update_alarm(entity=entity_id, alarm=alarm, + criteria=criteria, disabled=disabled, + label=label, metadata=metadata) + changed = True + + if should_delete: + alarm.delete() + changed = True + else: + should_create = True + + if should_create: + alarm = cm.create_alarm(entity=entity_id, check=check_id, + notification_plan=notification_plan_id, + criteria=criteria, disabled=disabled, label=label, + metadata=metadata) + changed = True + elif state == 'absent': + for a in existing: + a.delete() + changed = True + else: + module.fail_json(msg='state must be either present or absent.') + + if alarm: + alarm_dict = { + "id": alarm.id, + "label": alarm.label, + "check_id": alarm.check_id, + "notification_plan_id": alarm.notification_plan_id, + "criteria": alarm.criteria, + "disabled": alarm.disabled, + "metadata": alarm.metadata + } + module.exit_json(changed=changed, alarm=alarm_dict) + else: + module.exit_json(changed=changed) + +def main(): + argument_spec = rax_argument_spec() + argument_spec.update( + dict( + state=dict(default='present'), + label=dict(), + entity_id=dict(), + check_id=dict(), + notification_plan_id=dict(), + criteria=dict(), + disabled=dict(type='bool', default=False), + metadata=dict(type='dict') + ) + ) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=rax_required_together() + ) + + if not HAS_PYRAX: + module.fail_json(msg='pyrax is required for this module') + + state = module.params.get('state') + label = module.params.get('label') + entity_id = module.params.get('entity_id') + check_id = module.params.get('check_id') + notification_plan_id = module.params.get('notification_plan_id') + criteria = module.params.get('criteria') + disabled = module.boolean(module.params.get('disabled')) + metadata = module.params.get('metadata') + + setup_rax_module(module, pyrax) + + alarm(module, state, label, entity_id, check_id, notification_plan_id, + criteria, disabled, metadata) + + +# Import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.rax import * + +# Invoke the module. +main() diff --git a/cloud/rackspace/rax_mon_check.py b/cloud/rackspace/rax_mon_check.py new file mode 100644 index 00000000000..9da283c3ba0 --- /dev/null +++ b/cloud/rackspace/rax_mon_check.py @@ -0,0 +1,323 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# This is a DOCUMENTATION stub specific to this module, it extends +# a documentation fragment located in ansible.utils.module_docs_fragments +DOCUMENTATION = ''' +--- +module: rax_mon_check +short_description: Create or delete a Rackspace Cloud Monitoring check for an + existing entity. +description: +- Create or delete a Rackspace Cloud Monitoring check associated with an + existing rax_mon_entity. A check is a specific test or measurement that is + performed, possibly from different monitoring zones, on the systems you + monitor. Rackspace monitoring module flow | rax_mon_entity -> + *rax_mon_check* -> rax_mon_notification -> rax_mon_notification_plan -> + rax_mon_alarm +version_added: "1.8.2" +options: + state: + description: + - Ensure that a check with this C(label) exists or does not exist. + choices: ["present", "absent"] + entity_id: + description: + - ID of the rax_mon_entity to target with this check. + required: true + label: + description: + - Defines a label for this check, between 1 and 64 characters long. + required: true + check_type: + description: + - The type of check to create. C(remote.) checks may be created on any + rax_mon_entity. C(agent.) checks may only be created on rax_mon_entities + that have a non-null C(agent_id). + choices: + - remote.dns + - remote.ftp-banner + - remote.http + - remote.imap-banner + - remote.mssql-banner + - remote.mysql-banner + - remote.ping + - remote.pop3-banner + - remote.postgresql-banner + - remote.smtp-banner + - remote.smtp + - remote.ssh + - remote.tcp + - remote.telnet-banner + - agent.filesystem + - agent.memory + - agent.load_average + - agent.cpu + - agent.disk + - agent.network + - agent.plugin + required: true + monitoring_zones_poll: + description: + - Comma-separated list of the names of the monitoring zones the check should + run from. Available monitoring zones include mzdfw, mzhkg, mziad, mzlon, + mzord and mzsyd. Required for remote.* checks; prohibited for agent.* checks. + target_hostname: + description: + - One of `target_hostname` and `target_alias` is required for remote.* checks, + but prohibited for agent.* checks. The hostname this check should target. + Must be a valid IPv4, IPv6, or FQDN. + target_alias: + description: + - One of `target_alias` and `target_hostname` is required for remote.* checks, + but prohibited for agent.* checks. Use the corresponding key in the entity's + `ip_addresses` hash to resolve an IP address to target. + details: + description: + - Additional details specific to the check type. Must be a hash of strings + between 1 and 255 characters long, or an array or object containing 0 to + 256 items. + disabled: + description: + - If "yes", ensure the check is created, but don't actually use it yet. + choices: [ "yes", "no" ] + metadata: + description: + - Hash of arbitrary key-value pairs to accompany this check if it fires. + Keys and values must be strings between 1 and 255 characters long. + period: + description: + - The number of seconds between each time the check is performed. Must be + greater than the minimum period set on your account. + timeout: + description: + - The number of seconds this check will wait when attempting to collect + results. Must be less than the period. +author: Ash Wilson +extends_documentation_fragment: rackspace.openstack +''' + +EXAMPLES = ''' +- name: Create a monitoring check + gather_facts: False + hosts: local + connection: local + tasks: + - name: Associate a check with an existing entity. + rax_mon_check: + credentials: ~/.rax_pub + state: present + entity_id: "{{ the_entity['entity']['id'] }}" + label: the_check + check_type: remote.ping + monitoring_zones_poll: mziad,mzord,mzdfw + details: + count: 10 + meta: + hurf: durf + register: the_check +''' + +try: + import pyrax + HAS_PYRAX = True +except ImportError: + HAS_PYRAX = False + +def cloud_check(module, state, entity_id, label, check_type, + monitoring_zones_poll, target_hostname, target_alias, details, + disabled, metadata, period, timeout): + + # Verify the presence of required attributes. + + required_attrs = { + "entity_id": entity_id, "label": label, "check_type": check_type + } + + for (key, value) in required_attrs.iteritems(): + if not value: + module.fail_json(msg=('%s is required for rax_mon_check' % key)) + + # Coerce attributes. + + if monitoring_zones_poll and not isinstance(monitoring_zones_poll, list): + monitoring_zones_poll = [monitoring_zones_poll] + + if period: + period = int(period) + + if timeout: + timeout = int(timeout) + + changed = False + check = None + + cm = pyrax.cloud_monitoring + if not cm: + module.fail_json(msg='Failed to instantiate client. This typically ' + 'indicates an invalid region or an incorrectly ' + 'capitalized region name.') + + entity = cm.get_entity(entity_id) + if not entity: + module.fail_json(msg='Failed to instantiate entity. "%s" may not be' + ' a valid entity id.' % entity_id) + + existing = [e for e in entity.list_checks() if e.label == label] + + if existing: + check = existing[0] + + if state == 'present': + if len(existing) > 1: + module.fail_json(msg='%s existing checks have a label of %s.' % + (len(existing), label)) + + should_delete = False + should_create = False + should_update = False + + if check: + # Details may include keys set to default values that are not + # included in the initial creation. + # + # Only force a recreation of the check if one of the *specified* + # keys is missing or has a different value. + if details: + for (key, value) in details.iteritems(): + if key not in check.details: + should_delete = should_create = True + elif value != check.details[key]: + should_delete = should_create = True + + should_update = label != check.label or \ + (target_hostname and target_hostname != check.target_hostname) or \ + (target_alias and target_alias != check.target_alias) or \ + (disabled != check.disabled) or \ + (metadata and metadata != check.metadata) or \ + (period and period != check.period) or \ + (timeout and timeout != check.timeout) or \ + (monitoring_zones_poll and monitoring_zones_poll != check.monitoring_zones_poll) + + if should_update and not should_delete: + check.update(label=label, + disabled=disabled, + metadata=metadata, + monitoring_zones_poll=monitoring_zones_poll, + timeout=timeout, + period=period, + target_alias=target_alias, + target_hostname=target_hostname) + changed = True + else: + # The check doesn't exist yet. + should_create = True + + if should_delete: + check.delete() + + if should_create: + check = cm.create_check(entity, + label=label, + check_type=check_type, + target_hostname=target_hostname, + target_alias=target_alias, + monitoring_zones_poll=monitoring_zones_poll, + details=details, + disabled=disabled, + metadata=metadata, + period=period, + timeout=timeout) + changed = True + elif state == 'absent': + if check: + check.delete() + changed = True + else: + module.fail_json(msg='state must be either present or absent.') + + if check: + check_dict = { + "id": check.id, + "label": check.label, + "type": check.type, + "target_hostname": check.target_hostname, + "target_alias": check.target_alias, + "monitoring_zones_poll": check.monitoring_zones_poll, + "details": check.details, + "disabled": check.disabled, + "metadata": check.metadata, + "period": check.period, + "timeout": check.timeout + } + module.exit_json(changed=changed, check=check_dict) + else: + module.exit_json(changed=changed) + +def main(): + argument_spec = rax_argument_spec() + argument_spec.update( + dict( + entity_id=dict(), + label=dict(), + check_type=dict(), + monitoring_zones_poll=dict(), + target_hostname=dict(), + target_alias=dict(), + details=dict(type='dict', default={}), + disabled=dict(type='bool', default=False), + metadata=dict(type='dict', default={}), + period=dict(type='int'), + timeout=dict(type='int'), + state=dict(default='present') + ) + ) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=rax_required_together() + ) + + if not HAS_PYRAX: + module.fail_json(msg='pyrax is required for this module') + + entity_id = module.params.get('entity_id') + label = module.params.get('label') + check_type = module.params.get('check_type') + monitoring_zones_poll = module.params.get('monitoring_zones_poll') + target_hostname = module.params.get('target_hostname') + target_alias = module.params.get('target_alias') + details = module.params.get('details') + disabled = module.boolean(module.params.get('disabled')) + metadata = module.params.get('metadata') + period = module.params.get('period') + timeout = module.params.get('timeout') + + state = module.params.get('state') + + setup_rax_module(module, pyrax) + + cloud_check(module, state, entity_id, label, check_type, + monitoring_zones_poll, target_hostname, target_alias, details, + disabled, metadata, period, timeout) + + +# Import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.rax import * + +# Invoke the module. +main() diff --git a/cloud/rackspace/rax_mon_entity.py b/cloud/rackspace/rax_mon_entity.py new file mode 100644 index 00000000000..8b95c291914 --- /dev/null +++ b/cloud/rackspace/rax_mon_entity.py @@ -0,0 +1,196 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# This is a DOCUMENTATION stub specific to this module, it extends +# a documentation fragment located in ansible.utils.module_docs_fragments +DOCUMENTATION = ''' +--- +module: rax_mon_entity +short_description: Create or delete a Rackspace Cloud Monitoring entity +description: +- Create or delete a Rackspace Cloud Monitoring entity, which represents a device + to monitor. Entities associate checks and alarms with a target system and + provide a convenient, centralized place to store IP addresses. Rackspace + monitoring module flow | *rax_mon_entity* -> rax_mon_check -> + rax_mon_notification -> rax_mon_notification_plan -> rax_mon_alarm +version_added: "1.8.2" +options: + label: + description: + - Defines a name for this entity. Must be a non-empty string between 1 and + 255 characters long. + required: true + state: + description: + - Ensure that an entity with this C(name) exists or does not exist. + choices: ["present", "absent"] + agent_id: + description: + - Rackspace monitoring agent on the target device to which this entity is + bound. Necessary to collect C(agent.) rax_mon_checks against this entity. + named_ip_addresses: + description: + - Hash of IP addresses that may be referenced by name by rax_mon_checks + added to this entity. Must be a dictionary of with keys that are names + between 1 and 64 characters long, and values that are valid IPv4 or IPv6 + addresses. + metadata: + description: + - Hash of arbitrary C(name), C(value) pairs that are passed to associated + rax_mon_alarms. Names and values must all be between 1 and 255 characters + long. +author: Ash Wilson +extends_documentation_fragment: rackspace.openstack +''' + +EXAMPLES = ''' +- name: Entity example + gather_facts: False + hosts: local + connection: local + tasks: + - name: Ensure an entity exists + rax_mon_entity: + credentials: ~/.rax_pub + state: present + label: my_entity + named_ip_addresses: + web_box: 192.168.0.10 + db_box: 192.168.0.11 + meta: + hurf: durf + register: the_entity +''' + +try: + import pyrax + HAS_PYRAX = True +except ImportError: + HAS_PYRAX = False + +def cloud_monitoring(module, state, label, agent_id, named_ip_addresses, + metadata): + if not label: + module.fail_json(msg='label is required for rax_mon_entity') + + if len(label) < 1 or len(label) > 255: + module.fail_json(msg='label must be between 1 and 255 characters long') + + changed = False + + cm = pyrax.cloud_monitoring + if not cm: + module.fail_json(msg='Failed to instantiate client. This typically ' + 'indicates an invalid region or an incorrectly ' + 'capitalized region name.') + + existing = [] + for entity in cm.list_entities(): + if label == entity.label: + existing.append(entity) + + entity = None + + if existing: + entity = existing[0] + + if state == 'present': + should_update = False + should_delete = False + should_create = False + + if len(existing) > 1: + module.fail_json(msg='%s existing entities have the label %s.' % + (len(existing), label)) + + if entity: + if named_ip_addresses and named_ip_addresses != entity.ip_addresses: + should_delete = should_create = True + + # Change an existing Entity, unless there's nothing to do. + should_update = agent_id and agent_id != entity.agent_id or \ + (metadata and metadata != entity.metadata) + + if should_update and not should_delete: + entity.update(agent_id, metadata) + changed = True + + if should_delete: + entity.delete() + else: + should_create = True + + if should_create: + # Create a new Entity. + entity = cm.create_entity(label=label, agent=agent_id, + ip_addresses=named_ip_addresses, + metadata=metadata) + changed = True + elif state == 'absent': + # Delete the existing Entities. + for e in existing: + e.delete() + changed = True + else: + module.fail_json(msg='state must be present or absent') + + if entity: + entity_dict = { + "id": entity.id, + "name": entity.name, + "agent_id": entity.agent_id, + } + module.exit_json(changed=changed, entity=entity_dict) + else: + module.exit_json(changed=changed) + +def main(): + argument_spec = rax_argument_spec() + argument_spec.update( + dict( + state=dict(default='present'), + label=dict(), + agent_id=dict(), + named_ip_addresses=dict(type='dict', default={}), + metadata=dict(type='dict', default={}) + ) + ) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=rax_required_together() + ) + + if not HAS_PYRAX: + module.fail_json(msg='pyrax is required for this module') + + state = module.params.get('state') + + label = module.params.get('label') + agent_id = module.params.get('agent_id') + named_ip_addresses = module.params.get('named_ip_addresses') + metadata = module.params.get('metadata') + + setup_rax_module(module, pyrax) + + cloud_monitoring(module, state, label, agent_id, named_ip_addresses, metadata) + +# Import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.rax import * + +# Invoke the module. +main() diff --git a/cloud/rackspace/rax_mon_notification.py b/cloud/rackspace/rax_mon_notification.py new file mode 100644 index 00000000000..74c4319255b --- /dev/null +++ b/cloud/rackspace/rax_mon_notification.py @@ -0,0 +1,187 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# This is a DOCUMENTATION stub specific to this module, it extends +# a documentation fragment located in ansible.utils.module_docs_fragments +DOCUMENTATION = ''' +--- +module: rax_mon_notification +short_description: Create or delete a Rackspace Cloud Monitoring notification. +description: +- Create or delete a Rackspace Cloud Monitoring notification that specifies a + channel that can be used to communicate alarms, such as email, webhooks, or + PagerDuty. Rackspace monitoring module flow | rax_mon_entity -> rax_mon_check -> + *rax_mon_notification* -> rax_mon_notification_plan -> rax_mon_alarm +version_added: "1.8.2" +options: + state: + description: + - Ensure that the notification with this C(label) exists or does not exist. + choices: ['present', 'absent'] + label: + description: + - Defines a friendly name for this notification. String between 1 and 255 + characters long. + required: true + notification_type: + description: + - A supported notification type. + choices: ["webhook", "email", "pagerduty"] + required: true + details: + description: + - Dictionary of key-value pairs used to initialize the notification. + Required keys and meanings vary with notification type. See + http://docs.rackspace.com/cm/api/v1.0/cm-devguide/content/ + service-notification-types-crud.html for details. + required: true +author: Ash Wilson +extends_documentation_fragment: rackspace.openstack +''' + +EXAMPLES = ''' +- name: Monitoring notification example + gather_facts: False + hosts: local + connection: local + tasks: + - name: Email me when something goes wrong. + rax_mon_entity: + credentials: ~/.rax_pub + label: omg + type: email + details: + address: me@mailhost.com + register: the_notification +''' + +try: + import pyrax + HAS_PYRAX = True +except ImportError: + HAS_PYRAX = False + +def notification(module, state, label, notification_type, details): + + if not label: + module.fail_json(msg='label is required for rax_mon_notification') + + if len(label) < 1 or len(label) > 255: + module.fail_json(msg='label must be between 1 and 255 characters long') + + if not notification_type: + module.fail_json(msg='you must provide a notification_type') + + if not details: + module.fail_json(msg='notification details are required') + + changed = False + notification = None + + cm = pyrax.cloud_monitoring + if not cm: + module.fail_json(msg='Failed to instantiate client. This typically ' + 'indicates an invalid region or an incorrectly ' + 'capitalized region name.') + + existing = [] + for n in cm.list_notifications(): + if n.label == label: + existing.append(n) + + if existing: + notification = existing[0] + + if state == 'present': + should_update = False + should_delete = False + should_create = False + + if len(existing) > 1: + module.fail_json(msg='%s existing notifications are labelled %s.' % + (len(existing), label)) + + if notification: + should_delete = (notification_type != notification.type) + + should_update = (details != notification.details) + + if should_update and not should_delete: + notification.update(details=notification.details) + changed = True + + if should_delete: + notification.delete() + else: + should_create = True + + if should_create: + notification = cm.create_notification(notification_type, + label=label, details=details) + changed = True + elif state == 'absent': + for n in existing: + n.delete() + changed = True + else: + module.fail_json(msg='state must be either "present" or "absent"') + + if notification: + notification_dict = { + "id": notification.id, + "type": notification.type, + "label": notification.label, + "details": notification.details + } + module.exit_json(changed=changed, notification=notification_dict) + else: + module.exit_json(changed=changed) + +def main(): + argument_spec = rax_argument_spec() + argument_spec.update( + dict( + state=dict(default='present'), + label=dict(), + notification_type=dict(), + details=dict(type='dict', default={}) + ) + ) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=rax_required_together() + ) + + if not HAS_PYRAX: + module.fail_json(msg='pyrax is required for this module') + + state = module.params.get('state') + + label = module.params.get('label') + notification_type = module.params.get('notification_type') + details = module.params.get('details') + + setup_rax_module(module, pyrax) + + notification(module, state, label, notification_type, details) + +# Import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.rax import * + +# Invoke the module. +main() diff --git a/cloud/rackspace/rax_mon_notification_plan.py b/cloud/rackspace/rax_mon_notification_plan.py new file mode 100644 index 00000000000..c8d4d215292 --- /dev/null +++ b/cloud/rackspace/rax_mon_notification_plan.py @@ -0,0 +1,186 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# This is a DOCUMENTATION stub specific to this module, it extends +# a documentation fragment located in ansible.utils.module_docs_fragments +DOCUMENTATION = ''' +--- +module: rax_mon_notification_plan +short_description: Create or delete a Rackspace Cloud Monitoring notification + plan. +description: +- Create or delete a Rackspace Cloud Monitoring notification plan by + associating existing rax_mon_notifications with severity levels. Rackspace + monitoring module flow | rax_mon_entity -> rax_mon_check -> + rax_mon_notification -> *rax_mon_notification_plan* -> rax_mon_alarm +version_added: "1.8.2" +options: + state: + description: + - Ensure that the notification plan with this C(label) exists or does not + exist. + choices: ['present', 'absent'] + label: + description: + - Defines a friendly name for this notification plan. String between 1 and + 255 characters long. + required: true + critical_state: + description: + - Notification list to use when the alarm state is CRITICAL. Must be an + array of valid rax_mon_notification ids. + warning_state: + description: + - Notification list to use when the alarm state is WARNING. Must be an array + of valid rax_mon_notification ids. + ok_state: + description: + - Notification list to use when the alarm state is OK. Must be an array of + valid rax_mon_notification ids. +author: Ash Wilson +extends_documentation_fragment: rackspace.openstack +''' + +EXAMPLES = ''' +- name: Example notification plan + gather_facts: False + hosts: local + connection: local + tasks: + - name: Establish who gets called when. + rax_mon_notification_plan: + credentials: ~/.rax_pub + state: present + label: defcon1 + critical_state: + - "{{ everyone['notification']['id'] }}" + warning_state: + - "{{ opsfloor['notification']['id'] }}" + register: defcon1 +''' + +try: + import pyrax + HAS_PYRAX = True +except ImportError: + HAS_PYRAX = False + +def notification_plan(module, state, label, critical_state, warning_state, ok_state): + + if not label: + module.fail_json(msg='label is required for rax_mon_notification_plan') + + if len(label) < 1 or len(label) > 255: + module.fail_json(msg='label must be between 1 and 255 characters long') + + changed = False + notification_plan = None + + cm = pyrax.cloud_monitoring + if not cm: + module.fail_json(msg='Failed to instantiate client. This typically ' + 'indicates an invalid region or an incorrectly ' + 'capitalized region name.') + + existing = [] + for n in cm.list_notification_plans(): + if n.label == label: + existing.append(n) + + if existing: + notification_plan = existing[0] + + if state == 'present': + should_create = False + should_delete = False + + if len(existing) > 1: + module.fail_json(msg='%s notification plans are labelled %s.' % + (len(existing), label)) + + if notification_plan: + should_delete = (critical_state and critical_state != notification_plan.critical_state) or \ + (warning_state and warning_state != notification_plan.warning_state) or \ + (ok_state and ok_state != notification_plan.ok_state) + + if should_delete: + notification_plan.delete() + should_create = True + else: + should_create = True + + if should_create: + notification_plan = cm.create_notification_plan(label=label, + critical_state=critical_state, + warning_state=warning_state, + ok_state=ok_state) + changed = True + elif state == 'absent': + for np in existing: + np.delete() + changed = True + else: + module.fail_json(msg='state must be either "present" or "absent"') + + if notification_plan: + notification_plan_dict = { + "id": notification_plan.id, + "critical_state": notification_plan.critical_state, + "warning_state": notification_plan.warning_state, + "ok_state": notification_plan.ok_state, + "metadata": notification_plan.metadata + } + module.exit_json(changed=changed, notification_plan=notification_plan_dict) + else: + module.exit_json(changed=changed) + +def main(): + argument_spec = rax_argument_spec() + argument_spec.update( + dict( + state=dict(default='present'), + label=dict(), + critical_state=dict(type='list'), + warning_state=dict(type='list'), + ok_state=dict(type='list') + ) + ) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=rax_required_together() + ) + + if not HAS_PYRAX: + module.fail_json(msg='pyrax is required for this module') + + state = module.params.get('state') + + label = module.params.get('label') + critical_state = module.params.get('critical_state') + warning_state = module.params.get('warning_state') + ok_state = module.params.get('ok_state') + + setup_rax_module(module, pyrax) + + notification_plan(module, state, label, critical_state, warning_state, ok_state) + +# Import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.rax import * + +# Invoke the module. +main() From 817f603b6be58d2f44a5c0713d03a5377181915e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 17 Feb 2015 13:33:43 -0500 Subject: [PATCH 0028/2522] Initial implementation of rax_clb_ssl. --- cloud/rackspace/rax_clb_ssl.py | 284 +++++++++++++++++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 cloud/rackspace/rax_clb_ssl.py diff --git a/cloud/rackspace/rax_clb_ssl.py b/cloud/rackspace/rax_clb_ssl.py new file mode 100644 index 00000000000..d93e2f594e7 --- /dev/null +++ b/cloud/rackspace/rax_clb_ssl.py @@ -0,0 +1,284 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# This is a DOCUMENTATION stub specific to this module, it extends +# a documentation fragment located in ansible.utils.module_docs_fragments +DOCUMENTATION=''' +module: rax_clb_ssl +short_description: Manage SSL termination for a Rackspace Cloud Load Balancer. +description: +- Set up, reconfigure, or remove SSL termination for an existing load balancer. +version_added: "1.8.2" +options: + balancer_name: + description: + - Name or ID of the load balancer on which to manage SSL termination. + required: true + state: + description: + - If set to "present", SSL termination will be added to this load balancer. + - If "absent", SSL termination will be removed instead. + choices: + - present + - absent + default: present + enabled: + description: + - If set to "false", temporarily disable SSL termination without discarding + - existing credentials. + default: true + private_key: + description: + - The private SSL key as a string in PEM format. + certificate: + description: + - The public SSL certificates as a string in PEM format. + intermediate_certificate: + description: + - One or more intermediate certificate authorities as a string in PEM + - format, concatenated into a single string. + secure_port: + description: + - The port to listen for secure traffic. + default: 443 + secure_traffic_only: + description: + - If "true", the load balancer will *only* accept secure traffic. + default: false + https_redirect: + description: + - If "true", the load balancer will redirect HTTP traffic to HTTPS. + - Requires "secure_traffic_only" to be true. Incurs an implicit wait if SSL + - termination is also applied or removed. + wait: + description: + - Wait for the balancer to be in state "running" before turning. + default: false + wait_timeout: + description: + - How long before "wait" gives up, in seconds. + default: 300 +author: Ash Wilson +extends_documentation_fragment: rackspace +''' + +EXAMPLES = ''' +- name: Enable SSL termination on a load balancer + rax_clb_ssl: + balancer_name: the_loadbalancer + state: present + private_key: "{{ lookup('file', 'credentials/server.key' ) }}" + certificate: "{{ lookup('file', 'credentials/server.crt' ) }}" + intermediate_certificate: "{{ lookup('file', 'credentials/trust-chain.crt') }}" + secure_traffic_only: true + wait: true + +- name: Disable SSL termination + rax_clb_ssl: + balancer_name: "{{ registered_lb.balancer.id }}" + state: absent + wait: true +''' + +from ansible.module_utils.basic import * +from ansible.module_utils.rax import * + +try: + import pyrax + HAS_PYRAX = True +except ImportError: + HAS_PYRAX = False + +def cloud_load_balancer_ssl(module, balancer_name, state, enabled, private_key, + certificate, intermediate_certificate, secure_port, + secure_traffic_only, https_redirect, + wait, wait_timeout): + # Validate arguments. + + if not balancer_name: + module.fail_json(msg='balancer_name is required.') + + if state == 'present': + if not private_key: + module.fail_json(msg="private_key must be provided.") + else: + private_key = private_key.strip() + + if not certificate: + module.fail_json(msg="certificate must be provided.") + else: + certificate = certificate.strip() + + if state not in ('present', 'absent'): + module.fail_json(msg="State must be either 'present' or 'absent'.") + + attempts = wait_timeout / 5 + + # Locate the load balancer. + + clb = pyrax.cloud_loadbalancers + if not clb: + module.fail_json(msg='Failed to instantiate client. This ' + 'typically indicates an invalid region or an ' + 'incorrectly capitalized region name.') + + balancers = [] + for balancer in clb.list(): + if balancer_name == balancer.name or balancer_name == str(balancer.id): + balancers.append(balancer) + + if not balancers: + module.fail_json(msg='No load balancers matched your criteria. ' + 'Use rax_clb to create the balancer first.') + + if len(balancers) > 1: + module.fail_json(msg="%d load balancers were matched your criteria. Try" + "using the balancer's id instead." % len(balancers)) + + balancer = balancers[0] + existing_ssl = balancer.get_ssl_termination() + + changed = False + + if state == 'present': + # Apply or reconfigure SSL termination on the load balancer. + ssl_attrs = dict( + securePort=secure_port, + privatekey=private_key, + certificate=certificate, + intermediateCertificate=intermediate_certificate, + enabled=enabled, + secureTrafficOnly=secure_traffic_only + ) + + needs_change = False + + if existing_ssl: + for ssl_attr, value in ssl_attrs.iteritems(): + if ssl_attr == 'privatekey': + # The private key is not included in get_ssl_termination's + # output (as it shouldn't be). Also, if you're changing the + # private key, you'll also be changing the certificate, + # so we don't lose anything by not checking it. + continue + + if value is not None and existing_ssl.get(ssl_attr) != value: + # module.fail_json(msg='Unnecessary change', attr=ssl_attr, value=value, existing=existing_ssl.get(ssl_attr)) + needs_change = True + else: + needs_change = True + + if needs_change: + balancer.add_ssl_termination(**ssl_attrs) + changed = True + elif state == 'absent': + # Remove SSL termination if it's already configured. + if existing_ssl: + balancer.delete_ssl_termination() + changed = True + + if https_redirect is not None and balancer.httpsRedirect != https_redirect: + if changed: + # This wait is unavoidable because load balancers are immutable + # while the SSL termination changes above are being applied. + pyrax.utils.wait_for_build(balancer, interval=5, attempts=attempts) + + balancer.update(httpsRedirect=https_redirect) + changed = True + + if changed and wait: + pyrax.utils.wait_for_build(balancer, interval=5, attempts=attempts) + + balancer.get() + new_ssl_termination = balancer.get_ssl_termination() + + # Intentionally omit the private key from the module output, so you don't + # accidentally echo it with `ansible-playbook -v` or `debug`, and the + # certificate, which is just long. Convert other attributes to snake_case + # and include https_redirect at the top-level. + if new_ssl_termination: + new_ssl = dict( + enabled=new_ssl_termination['enabled'], + secure_port=new_ssl_termination['securePort'], + secure_traffic_only=new_ssl_termination['secureTrafficOnly'] + ) + else: + new_ssl = None + + result = dict( + changed=changed, + https_redirect=balancer.httpsRedirect, + ssl_termination=new_ssl + ) + success = True + + if balancer.status == 'ERROR': + result['msg'] = '%s failed to build' % balancer.id + success = False + elif wait and balancer.status not in ('ACTIVE', 'ERROR'): + result['msg'] = 'Timeout waiting on %s' % balancer.id + success = False + + if success: + module.exit_json(**result) + else: + module.fail_json(**result) + +def main(): + argument_spec = rax_argument_spec() + argument_spec.update(dict( + balancer_name=dict(type='str'), + state=dict(default='present', choices=['present', 'absent']), + enabled=dict(type='bool', default=True), + private_key=dict(), + certificate=dict(), + intermediate_certificate=dict(), + secure_port=dict(type='int', default=443), + secure_traffic_only=dict(type='bool', default=False), + https_redirect=dict(type='bool'), + wait=dict(type='bool', default=False), + wait_timeout=dict(type='int', default=300) + )) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=rax_required_together(), + ) + + if not HAS_PYRAX: + module.fail_json(msg='pyrax is required for this module.') + + balancer_name = module.params.get('balancer_name') + state = module.params.get('state') + enabled = module.boolean(module.params.get('enabled')) + private_key = module.params.get('private_key') + certificate = module.params.get('certificate') + intermediate_certificate = module.params.get('intermediate_certificate') + secure_port = module.params.get('secure_port') + secure_traffic_only = module.boolean(module.params.get('secure_traffic_only')) + https_redirect = module.boolean(module.params.get('https_redirect')) + wait = module.boolean(module.params.get('wait')) + wait_timeout = module.params.get('wait_timeout') + + setup_rax_module(module, pyrax) + + cloud_load_balancer_ssl( + module, balancer_name, state, enabled, private_key, certificate, + intermediate_certificate, secure_port, secure_traffic_only, + https_redirect, wait, wait_timeout + ) + +main() From 4c4c0bb11909b4219f8bbe3abc1fce5cfff50e20 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 19 Feb 2015 13:36:33 -0500 Subject: [PATCH 0029/2522] Use the correct version_added. --- cloud/rackspace/rax_clb_ssl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/rackspace/rax_clb_ssl.py b/cloud/rackspace/rax_clb_ssl.py index d93e2f594e7..7a27f93116f 100644 --- a/cloud/rackspace/rax_clb_ssl.py +++ b/cloud/rackspace/rax_clb_ssl.py @@ -21,7 +21,7 @@ short_description: Manage SSL termination for a Rackspace Cloud Load Balancer. description: - Set up, reconfigure, or remove SSL termination for an existing load balancer. -version_added: "1.8.2" +version_added: "1.9" options: balancer_name: description: From 015ffbf9a90c776f8b222e61510b5b45c4fa6e9b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 19 Feb 2015 13:37:28 -0500 Subject: [PATCH 0030/2522] Move ansible imports to the module's bottom. --- cloud/rackspace/rax_clb_ssl.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/rackspace/rax_clb_ssl.py b/cloud/rackspace/rax_clb_ssl.py index 7a27f93116f..d011432e066 100644 --- a/cloud/rackspace/rax_clb_ssl.py +++ b/cloud/rackspace/rax_clb_ssl.py @@ -93,9 +93,6 @@ wait: true ''' -from ansible.module_utils.basic import * -from ansible.module_utils.rax import * - try: import pyrax HAS_PYRAX = True @@ -281,4 +278,7 @@ def main(): https_redirect, wait, wait_timeout ) +from ansible.module_utils.basic import * +from ansible.module_utils.rax import * + main() From 0380490ae9561d414684602f8a0a5323b98949d8 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 19 Feb 2015 13:41:03 -0500 Subject: [PATCH 0031/2522] Rename "balancer_name" to "loadbalancer." --- cloud/rackspace/rax_clb_ssl.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/cloud/rackspace/rax_clb_ssl.py b/cloud/rackspace/rax_clb_ssl.py index d011432e066..2195d08b938 100644 --- a/cloud/rackspace/rax_clb_ssl.py +++ b/cloud/rackspace/rax_clb_ssl.py @@ -23,7 +23,7 @@ - Set up, reconfigure, or remove SSL termination for an existing load balancer. version_added: "1.9" options: - balancer_name: + loadbalancer: description: - Name or ID of the load balancer on which to manage SSL termination. required: true @@ -99,15 +99,12 @@ except ImportError: HAS_PYRAX = False -def cloud_load_balancer_ssl(module, balancer_name, state, enabled, private_key, +def cloud_load_balancer_ssl(module, loadbalancer, state, enabled, private_key, certificate, intermediate_certificate, secure_port, secure_traffic_only, https_redirect, wait, wait_timeout): # Validate arguments. - if not balancer_name: - module.fail_json(msg='balancer_name is required.') - if state == 'present': if not private_key: module.fail_json(msg="private_key must be provided.") @@ -134,7 +131,7 @@ def cloud_load_balancer_ssl(module, balancer_name, state, enabled, private_key, balancers = [] for balancer in clb.list(): - if balancer_name == balancer.name or balancer_name == str(balancer.id): + if loadbalancer == balancer.name or loadbalancer == str(balancer.id): balancers.append(balancer) if not balancers: @@ -237,7 +234,7 @@ def cloud_load_balancer_ssl(module, balancer_name, state, enabled, private_key, def main(): argument_spec = rax_argument_spec() argument_spec.update(dict( - balancer_name=dict(type='str'), + loadbalancer=dict(required=True), state=dict(default='present', choices=['present', 'absent']), enabled=dict(type='bool', default=True), private_key=dict(), @@ -258,7 +255,7 @@ def main(): if not HAS_PYRAX: module.fail_json(msg='pyrax is required for this module.') - balancer_name = module.params.get('balancer_name') + loadbalancer = module.params.get('loadbalancer') state = module.params.get('state') enabled = module.boolean(module.params.get('enabled')) private_key = module.params.get('private_key') @@ -273,7 +270,7 @@ def main(): setup_rax_module(module, pyrax) cloud_load_balancer_ssl( - module, balancer_name, state, enabled, private_key, certificate, + module, loadbalancer, state, enabled, private_key, certificate, intermediate_certificate, secure_port, secure_traffic_only, https_redirect, wait, wait_timeout ) From 1a8ed52819f03a3a3ebde3e1f81e25f35e231fa3 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 19 Feb 2015 13:41:29 -0500 Subject: [PATCH 0032/2522] Remove redundant "state" validity check. --- cloud/rackspace/rax_clb_ssl.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/cloud/rackspace/rax_clb_ssl.py b/cloud/rackspace/rax_clb_ssl.py index 2195d08b938..cff30d67b5e 100644 --- a/cloud/rackspace/rax_clb_ssl.py +++ b/cloud/rackspace/rax_clb_ssl.py @@ -116,9 +116,6 @@ def cloud_load_balancer_ssl(module, loadbalancer, state, enabled, private_key, else: certificate = certificate.strip() - if state not in ('present', 'absent'): - module.fail_json(msg="State must be either 'present' or 'absent'.") - attempts = wait_timeout / 5 # Locate the load balancer. From e1cdda56ff697a891541efd04acf39a9f4dcac64 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 19 Feb 2015 13:48:03 -0500 Subject: [PATCH 0033/2522] Use rax_find_loadbalancer utility method. --- cloud/rackspace/rax_clb_ssl.py | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/cloud/rackspace/rax_clb_ssl.py b/cloud/rackspace/rax_clb_ssl.py index cff30d67b5e..f16118c20f4 100644 --- a/cloud/rackspace/rax_clb_ssl.py +++ b/cloud/rackspace/rax_clb_ssl.py @@ -120,26 +120,7 @@ def cloud_load_balancer_ssl(module, loadbalancer, state, enabled, private_key, # Locate the load balancer. - clb = pyrax.cloud_loadbalancers - if not clb: - module.fail_json(msg='Failed to instantiate client. This ' - 'typically indicates an invalid region or an ' - 'incorrectly capitalized region name.') - - balancers = [] - for balancer in clb.list(): - if loadbalancer == balancer.name or loadbalancer == str(balancer.id): - balancers.append(balancer) - - if not balancers: - module.fail_json(msg='No load balancers matched your criteria. ' - 'Use rax_clb to create the balancer first.') - - if len(balancers) > 1: - module.fail_json(msg="%d load balancers were matched your criteria. Try" - "using the balancer's id instead." % len(balancers)) - - balancer = balancers[0] + balancer = rax_find_loadbalancer(module, pyrax, loadbalancer) existing_ssl = balancer.get_ssl_termination() changed = False From 65a1129ef9800c2f094f07b84677d33b762337cb Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 23 Feb 2015 14:18:35 -0600 Subject: [PATCH 0034/2522] Correct version_added in the documentation. --- cloud/rackspace/rax_mon_alarm.py | 2 +- cloud/rackspace/rax_mon_check.py | 2 +- cloud/rackspace/rax_mon_entity.py | 2 +- cloud/rackspace/rax_mon_notification.py | 2 +- cloud/rackspace/rax_mon_notification_plan.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cloud/rackspace/rax_mon_alarm.py b/cloud/rackspace/rax_mon_alarm.py index f5fc9593abd..aa742d02bd8 100644 --- a/cloud/rackspace/rax_mon_alarm.py +++ b/cloud/rackspace/rax_mon_alarm.py @@ -27,7 +27,7 @@ notifications. Rackspace monitoring module flow | rax_mon_entity -> rax_mon_check -> rax_mon_notification -> rax_mon_notification_plan -> *rax_mon_alarm* -version_added: "1.8.2" +version_added: "1.9" options: state: description: diff --git a/cloud/rackspace/rax_mon_check.py b/cloud/rackspace/rax_mon_check.py index 9da283c3ba0..3f86da93ab6 100644 --- a/cloud/rackspace/rax_mon_check.py +++ b/cloud/rackspace/rax_mon_check.py @@ -28,7 +28,7 @@ monitor. Rackspace monitoring module flow | rax_mon_entity -> *rax_mon_check* -> rax_mon_notification -> rax_mon_notification_plan -> rax_mon_alarm -version_added: "1.8.2" +version_added: "1.9" options: state: description: diff --git a/cloud/rackspace/rax_mon_entity.py b/cloud/rackspace/rax_mon_entity.py index 8b95c291914..9d20252b0e5 100644 --- a/cloud/rackspace/rax_mon_entity.py +++ b/cloud/rackspace/rax_mon_entity.py @@ -26,7 +26,7 @@ provide a convenient, centralized place to store IP addresses. Rackspace monitoring module flow | *rax_mon_entity* -> rax_mon_check -> rax_mon_notification -> rax_mon_notification_plan -> rax_mon_alarm -version_added: "1.8.2" +version_added: "1.9" options: label: description: diff --git a/cloud/rackspace/rax_mon_notification.py b/cloud/rackspace/rax_mon_notification.py index 74c4319255b..475eb345f51 100644 --- a/cloud/rackspace/rax_mon_notification.py +++ b/cloud/rackspace/rax_mon_notification.py @@ -25,7 +25,7 @@ channel that can be used to communicate alarms, such as email, webhooks, or PagerDuty. Rackspace monitoring module flow | rax_mon_entity -> rax_mon_check -> *rax_mon_notification* -> rax_mon_notification_plan -> rax_mon_alarm -version_added: "1.8.2" +version_added: "1.9" options: state: description: diff --git a/cloud/rackspace/rax_mon_notification_plan.py b/cloud/rackspace/rax_mon_notification_plan.py index c8d4d215292..b81b00f7d18 100644 --- a/cloud/rackspace/rax_mon_notification_plan.py +++ b/cloud/rackspace/rax_mon_notification_plan.py @@ -26,7 +26,7 @@ associating existing rax_mon_notifications with severity levels. Rackspace monitoring module flow | rax_mon_entity -> rax_mon_check -> rax_mon_notification -> *rax_mon_notification_plan* -> rax_mon_alarm -version_added: "1.8.2" +version_added: "1.9" options: state: description: From 205e4e5530699809713fdcada0ee477abb68fb50 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 23 Feb 2015 14:25:51 -0600 Subject: [PATCH 0035/2522] Use required=True and choices=[]. --- cloud/rackspace/rax_mon_alarm.py | 10 +++++----- cloud/rackspace/rax_mon_check.py | 8 ++++---- cloud/rackspace/rax_mon_entity.py | 4 ++-- cloud/rackspace/rax_mon_notification.py | 8 ++++---- cloud/rackspace/rax_mon_notification_plan.py | 4 ++-- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/cloud/rackspace/rax_mon_alarm.py b/cloud/rackspace/rax_mon_alarm.py index aa742d02bd8..f4d2a9398a5 100644 --- a/cloud/rackspace/rax_mon_alarm.py +++ b/cloud/rackspace/rax_mon_alarm.py @@ -198,11 +198,11 @@ def main(): argument_spec = rax_argument_spec() argument_spec.update( dict( - state=dict(default='present'), - label=dict(), - entity_id=dict(), - check_id=dict(), - notification_plan_id=dict(), + state=dict(default='present', choices=['present', 'absent']), + label=dict(required=True), + entity_id=dict(required=True), + check_id=dict(required=True), + notification_plan_id=dict(required=True), criteria=dict(), disabled=dict(type='bool', default=False), metadata=dict(type='dict') diff --git a/cloud/rackspace/rax_mon_check.py b/cloud/rackspace/rax_mon_check.py index 3f86da93ab6..27798e6cd5a 100644 --- a/cloud/rackspace/rax_mon_check.py +++ b/cloud/rackspace/rax_mon_check.py @@ -271,9 +271,9 @@ def main(): argument_spec = rax_argument_spec() argument_spec.update( dict( - entity_id=dict(), - label=dict(), - check_type=dict(), + entity_id=dict(required=True), + label=dict(required=True), + check_type=dict(required=True), monitoring_zones_poll=dict(), target_hostname=dict(), target_alias=dict(), @@ -282,7 +282,7 @@ def main(): metadata=dict(type='dict', default={}), period=dict(type='int'), timeout=dict(type='int'), - state=dict(default='present') + state=dict(default='present', choices=['present', 'absent']) ) ) diff --git a/cloud/rackspace/rax_mon_entity.py b/cloud/rackspace/rax_mon_entity.py index 9d20252b0e5..b1bd13c61ad 100644 --- a/cloud/rackspace/rax_mon_entity.py +++ b/cloud/rackspace/rax_mon_entity.py @@ -161,8 +161,8 @@ def main(): argument_spec = rax_argument_spec() argument_spec.update( dict( - state=dict(default='present'), - label=dict(), + state=dict(default='present', choices=['present', 'absent']), + label=dict(required=True), agent_id=dict(), named_ip_addresses=dict(type='dict', default={}), metadata=dict(type='dict', default={}) diff --git a/cloud/rackspace/rax_mon_notification.py b/cloud/rackspace/rax_mon_notification.py index 475eb345f51..6962b14b3e6 100644 --- a/cloud/rackspace/rax_mon_notification.py +++ b/cloud/rackspace/rax_mon_notification.py @@ -154,10 +154,10 @@ def main(): argument_spec = rax_argument_spec() argument_spec.update( dict( - state=dict(default='present'), - label=dict(), - notification_type=dict(), - details=dict(type='dict', default={}) + state=dict(default='present', choices=['present', 'absent']), + label=dict(required=True), + notification_type=dict(required=True, choices=['webhook', 'email', 'pagerduty']), + details=dict(required=True, type='dict') ) ) diff --git a/cloud/rackspace/rax_mon_notification_plan.py b/cloud/rackspace/rax_mon_notification_plan.py index b81b00f7d18..1bb5052c8f2 100644 --- a/cloud/rackspace/rax_mon_notification_plan.py +++ b/cloud/rackspace/rax_mon_notification_plan.py @@ -151,8 +151,8 @@ def main(): argument_spec = rax_argument_spec() argument_spec.update( dict( - state=dict(default='present'), - label=dict(), + state=dict(default='present', choices=['present', 'absent']), + label=dict(required=True), critical_state=dict(type='list'), warning_state=dict(type='list'), ok_state=dict(type='list') From c0549335135e33f1cbd49575e8e7428647a06e28 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 23 Feb 2015 14:33:02 -0600 Subject: [PATCH 0036/2522] Eliminate redundant module argument checks. --- cloud/rackspace/rax_mon_alarm.py | 15 +-------------- cloud/rackspace/rax_mon_check.py | 10 ---------- cloud/rackspace/rax_mon_entity.py | 6 +----- cloud/rackspace/rax_mon_notification.py | 13 +------------ cloud/rackspace/rax_mon_notification_plan.py | 7 +------ 5 files changed, 4 insertions(+), 47 deletions(-) diff --git a/cloud/rackspace/rax_mon_alarm.py b/cloud/rackspace/rax_mon_alarm.py index f4d2a9398a5..f9b97bc8dd1 100644 --- a/cloud/rackspace/rax_mon_alarm.py +++ b/cloud/rackspace/rax_mon_alarm.py @@ -105,17 +105,6 @@ def alarm(module, state, label, entity_id, check_id, notification_plan_id, criteria, disabled, metadata): - # Verify the presence of required attributes. - - required_attrs = { - "label": label, "entity_id": entity_id, "check_id": check_id, - "notification_plan_id": notification_plan_id - } - - for (key, value) in required_attrs.iteritems(): - if not value: - module.fail_json(msg=('%s is required for rax_mon_alarm' % key)) - if len(label) < 1 or len(label) > 255: module.fail_json(msg='label must be between 1 and 255 characters long') @@ -173,12 +162,10 @@ def alarm(module, state, label, entity_id, check_id, notification_plan_id, crite criteria=criteria, disabled=disabled, label=label, metadata=metadata) changed = True - elif state == 'absent': + else: for a in existing: a.delete() changed = True - else: - module.fail_json(msg='state must be either present or absent.') if alarm: alarm_dict = { diff --git a/cloud/rackspace/rax_mon_check.py b/cloud/rackspace/rax_mon_check.py index 27798e6cd5a..101efd3c858 100644 --- a/cloud/rackspace/rax_mon_check.py +++ b/cloud/rackspace/rax_mon_check.py @@ -141,16 +141,6 @@ def cloud_check(module, state, entity_id, label, check_type, monitoring_zones_poll, target_hostname, target_alias, details, disabled, metadata, period, timeout): - # Verify the presence of required attributes. - - required_attrs = { - "entity_id": entity_id, "label": label, "check_type": check_type - } - - for (key, value) in required_attrs.iteritems(): - if not value: - module.fail_json(msg=('%s is required for rax_mon_check' % key)) - # Coerce attributes. if monitoring_zones_poll and not isinstance(monitoring_zones_poll, list): diff --git a/cloud/rackspace/rax_mon_entity.py b/cloud/rackspace/rax_mon_entity.py index b1bd13c61ad..5f82ff9c524 100644 --- a/cloud/rackspace/rax_mon_entity.py +++ b/cloud/rackspace/rax_mon_entity.py @@ -83,8 +83,6 @@ def cloud_monitoring(module, state, label, agent_id, named_ip_addresses, metadata): - if not label: - module.fail_json(msg='label is required for rax_mon_entity') if len(label) < 1 or len(label) > 255: module.fail_json(msg='label must be between 1 and 255 characters long') @@ -139,13 +137,11 @@ def cloud_monitoring(module, state, label, agent_id, named_ip_addresses, ip_addresses=named_ip_addresses, metadata=metadata) changed = True - elif state == 'absent': + else: # Delete the existing Entities. for e in existing: e.delete() changed = True - else: - module.fail_json(msg='state must be present or absent') if entity: entity_dict = { diff --git a/cloud/rackspace/rax_mon_notification.py b/cloud/rackspace/rax_mon_notification.py index 6962b14b3e6..8a21b088c5e 100644 --- a/cloud/rackspace/rax_mon_notification.py +++ b/cloud/rackspace/rax_mon_notification.py @@ -76,18 +76,9 @@ def notification(module, state, label, notification_type, details): - if not label: - module.fail_json(msg='label is required for rax_mon_notification') - if len(label) < 1 or len(label) > 255: module.fail_json(msg='label must be between 1 and 255 characters long') - if not notification_type: - module.fail_json(msg='you must provide a notification_type') - - if not details: - module.fail_json(msg='notification details are required') - changed = False notification = None @@ -132,12 +123,10 @@ def notification(module, state, label, notification_type, details): notification = cm.create_notification(notification_type, label=label, details=details) changed = True - elif state == 'absent': + else: for n in existing: n.delete() changed = True - else: - module.fail_json(msg='state must be either "present" or "absent"') if notification: notification_dict = { diff --git a/cloud/rackspace/rax_mon_notification_plan.py b/cloud/rackspace/rax_mon_notification_plan.py index 1bb5052c8f2..05b89b2cfb3 100644 --- a/cloud/rackspace/rax_mon_notification_plan.py +++ b/cloud/rackspace/rax_mon_notification_plan.py @@ -80,9 +80,6 @@ def notification_plan(module, state, label, critical_state, warning_state, ok_state): - if not label: - module.fail_json(msg='label is required for rax_mon_notification_plan') - if len(label) < 1 or len(label) > 255: module.fail_json(msg='label must be between 1 and 255 characters long') @@ -128,12 +125,10 @@ def notification_plan(module, state, label, critical_state, warning_state, ok_st warning_state=warning_state, ok_state=ok_state) changed = True - elif state == 'absent': + else: for np in existing: np.delete() changed = True - else: - module.fail_json(msg='state must be either "present" or "absent"') if notification_plan: notification_plan_dict = { From 6b43dd630caad7a12440565027f5a53266c23832 Mon Sep 17 00:00:00 2001 From: Timothy Vandenbrande Date: Mon, 16 Mar 2015 11:34:07 +0100 Subject: [PATCH 0037/2522] windows firewall control --- windows/win_fw.ps1 | 346 +++++++++++++++++++++++++++++++++++++++++++++ windows/win_fw.py | 110 ++++++++++++++ 2 files changed, 456 insertions(+) create mode 100644 windows/win_fw.ps1 create mode 100644 windows/win_fw.py diff --git a/windows/win_fw.ps1 b/windows/win_fw.ps1 new file mode 100644 index 00000000000..37e8935a23d --- /dev/null +++ b/windows/win_fw.ps1 @@ -0,0 +1,346 @@ +#!powershell +# +# WANT_JSON +# POWERSHELL_COMMON + +function getFirewallRule ($fwsettings) { + try { + + #$output = Get-NetFirewallRule -name $($fwsettings.name); + $rawoutput=@(netsh advfirewall firewall show rule name=$($fwsettings.Name)) + if (!($rawoutput -eq 'No rules match the specified criteria.')){ + $rawoutput | Where {$_ -match '^([^:]+):\s*(\S.*)$'} | Foreach -Begin { + $FirstRun = $true; + $HashProps = @{}; + } -Process { + if (($Matches[1] -eq 'Rule Name') -and (!($FirstRun))) { + #$output=New-Object -TypeName PSCustomObject -Property $HashProps; + $output=$HashProps; + $HashProps = @{}; + }; + $HashProps.$($Matches[1]) = $Matches[2]; + $FirstRun = $false; + } -End { + #$output=New-Object -TypeName PSCustomObject -Property $HashProps; + $output=$HashProps; + } + } + $exists=$false; + $correct=$true; + $diff=$false; + $multi=$false; + $correct=$false; + $difference=@(); + $msg=@(); + if ($($output|measure).count -gt 0) { + $exists=$true; + $msg += @("The rule '" + $fwsettings.name + "' exists."); + if ($($output|measure).count -gt 1) { + $multi=$true + $msg += @("The rule '" + $fwsettings.name + "' has multiple entries."); + ForEach($rule in $output.GetEnumerator()) { + ForEach($fwsetting in $fwsettings.GetEnumerator()) { + if ( $rule.$fwsetting -ne $fwsettings.$fwsetting) { + $diff=$true; + #$difference+=@($fwsettings.$($fwsetting.Key)); + $difference+=@("output:$rule.$fwsetting,fwsetting:$fwsettings.$fwsetting"); + }; + }; + if ($diff -eq $false) { + $correct=$true + }; + }; + } else { + ForEach($fwsetting in $fwsettings.GetEnumerator()) { + if ( $output.$($fwsetting.Key) -ne $fwsettings.$($fwsetting.Key)) { + + if (($fwsetting.Key -eq 'RemoteIP') -and ($output.$($fwsetting.Key) -eq ($fwsettings.$($fwsetting.Key)+'-'+$fwsettings.$($fwsetting.Key)))) { + $donothing=$false + } elseif ((($fwsetting.Key -eq 'Name') -or ($fwsetting.Key -eq 'DisplayName')) -and ($output."Rule Name" -eq $fwsettings.$($fwsetting.Key))) { + $donothing=$false + } else { + $diff=$true; + $difference+=@($fwsettings.$($fwsetting.Key)); + }; + }; + }; + if ($diff -eq $false) { + $correct=$true + }; + }; + if ($correct) { + $msg += @("An identical rule exists"); + } else { + $msg += @("The rule exists but has different values"); + } + } else { + $msg += @("No rule could be found"); + }; + $result = @{ + exists = $exists + identical = $correct + multiple = $multi + difference = $difference + msg = $msg + } + } catch [Exception]{ + $result = @{ + failed = $true + error = $_.Exception.Message + msg = $msg + } + }; + return $result +}; + +function createFireWallRule ($fwsettings) { + $msg=@() + $execString="netsh advfirewall firewall add rule " + + ForEach ($fwsetting in $fwsettings.GetEnumerator()) { + if ($fwsetting.key -eq 'Direction') { + $key='dir' + } else { + $key=$($fwsetting.key).ToLower() + }; + $execString+=" "; + $execString+=$key; + $execString+="="; + $execString+=$fwsetting.value; + #$execString+="'"; + }; + try { + #$msg+=@($execString); + $output=$(Invoke-Expression $execString| ? {$_}); + $msg+=@("Created firewall rule $name"); + + $result=@{ + output=$output + changed=$true + msg=$msg + }; + + } catch [Exception]{ + $msg=@("Failed to create the rule") + $result=@{ + output=$output + failed=$true + error=$_.Exception.Message + msg=$msg + }; + }; + return $result +}; + +function removeFireWallRule ($fwsettings) { + $msg=@() + try { + $rawoutput=@(netsh advfirewall firewall delete rule name=$($fwsettings.name)) + $rawoutput | Where {$_ -match '^([^:]+):\s*(\S.*)$'} | Foreach -Begin { + $FirstRun = $true; + $HashProps = @{}; + } -Process { + if (($Matches[1] -eq 'Rule Name') -and (!($FirstRun))) { + $output=$HashProps; + $HashProps = @{}; + }; + $HashProps.$($Matches[1]) = $Matches[2]; + $FirstRun = $false; + } -End { + $output=$HashProps; + }; + $msg+=@("Removed the rule") + $result=@{ + failed=$false + changed=$true + msg=$msg + output=$output + }; + } catch [Exception]{ + $msg+=@("Failed to remove the rule") + $result=@{ + failed=$true + error=$_.Exception.Message + msg=$msg + } + }; + return $result +} + +# Mount Drives +$change=$false; +$fail=$false; +$msg=@(); +$fwsettings=@{} + +# Variabelise the arguments +$params=Parse-Args $args; + +$state=Get-Attr $params "state" "present"; +$name=Get-Attr $params "name" ""; +$direction=Get-Attr $params "direction" ""; +$force=Get-Attr $params "force" $false; +$action=Get-Attr $params "action" ""; + +# Check the arguments +if (($state -ne "present") -And ($state -ne "absent")){ + $misArg+="state"; + $msg+=@("for the state parameter only present and absent is allowed"); +}; + +if ($name -eq ""){ + $misArg+="Name"; + $msg+=@("name is a required argument"); +} else { + $fwsettings.Add("Name", $name) + #$fwsettings.Add("displayname", $name) +}; +if ((($direction.ToLower() -ne "In") -And ($direction.ToLower() -ne "Out")) -And ($state -eq "present")){ + $misArg+="Direction"; + $msg+=@("for the Direction parameter only the values 'In' and 'Out' are allowed"); +} else { + $fwsettings.Add("Direction", $direction) +}; +if ((($action.ToLower() -ne "allow") -And ($action.ToLower() -ne "block")) -And ($state -eq "present")){ + $misArg+="Direction"; + $msg+=@("for the Action parameter only the values 'allow' and 'block' are allowed"); +} else { + $fwsettings.Add("Action", $action) +}; +$args=@( + "Description", + "LocalIP", + "RemoteIP", + "LocalPort", + "RemotePort", + "Program", + "Service", + "Protocol" +) + +foreach ($arg in $args){ + New-Variable -Name $arg -Value $(Get-Attr $params $arg ""); + if ((Get-Variable -Name $arg -ValueOnly) -ne ""){ + $fwsettings.Add($arg, $(Get-Variable -Name $arg -ValueOnly)); + }; +}; + + +if ($($($misArg|measure).count) -gt 0){ + $result=New-Object psobject @{ + changed=$false + failed=$true + msg=$msg + }; + Exit-Json($result); +}; + +$output=@() +$capture=getFirewallRule ($fwsettings); +if ($capture.failed -eq $true) { + $msg+=$capture.msg; + $result=New-Object psobject @{ + changed=$false + failed=$true + error=$capture.error + msg=$msg + }; + Exit-Json $result; +} else { + $diff=$capture.difference + $msg+=$capture.msg; + $identical=$capture.identical; + $multiple=$capture.multiple; +} + + +switch ($state.ToLower()){ + "present" { + if ($capture.exists -eq $false) { + $capture=createFireWallRule($fwsettings); + $msg+=$capture.msg; + $change=$true; + if ($capture.failed -eq $true){ + $result=New-Object psobject @{ + failed=$capture.failed + error=$capture.error + output=$capture.output + changed=$change + msg=$msg + difference=$diff + fwsettings=$fwsettings + }; + Exit-Json $result; + } + } elseif ($capture.identical -eq $false) { + if ($force -eq $true) { + $capture=removeFirewallRule($fwsettings); + $msg+=$capture.msg; + $change=$true; + if ($capture.failed -eq $true){ + $result=New-Object psobject @{ + failed=$capture.failed + error=$capture.error + changed=$change + msg=$msg + output=$capture.output + fwsettings=$fwsettings + }; + Exit-Json $result; + } + $capture=createFireWallRule($fwsettings); + $msg+=$capture.msg; + $change=$true; + if ($capture.failed -eq $true){ + $result=New-Object psobject @{ + failed=$capture.failed + error=$capture.error + changed=$change + msg=$msg + difference=$diff + fwsettings=$fwsettings + }; + Exit-Json $result; + } + + } else { + $fail=$true + $msg+=@("There was already a rule $name with different values, use force=True to overwrite it"); + } + } elseif ($capture.identical -eq $true) { + $msg+=@("Firewall rule $name was already created"); + }; + } + "absent" { + if ($capture.exists -eq $true) { + $capture=removeFirewallRule($fwsettings); + $msg+=$capture.msg; + $change=$true; + if ($capture.failed -eq $true){ + $result=New-Object psobject @{ + failed=$capture.failed + error=$capture.error + changed=$change + msg=$msg + output=$capture.output + fwsettings=$fwsettings + }; + Exit-Json $result; + } + } else { + $msg+=@("Firewall rule $name did not exist"); + }; + } +}; + + +$result=New-Object psobject @{ + failed=$fail + changed=$change + msg=$msg + difference=$diff + fwsettings=$fwsettings +}; + + +Exit-Json $result; diff --git a/windows/win_fw.py b/windows/win_fw.py new file mode 100644 index 00000000000..0566ef0608d --- /dev/null +++ b/windows/win_fw.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python + +# (c) 2014, Timothy Vandenbrande +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: win_fw +author: Timothy Vandenbrande +short_description: Windows firewall automation +description: + - allows you to create/remove/update firewall rules +options: + state: + description: + - create/remove/update or powermanage your VM + default: "present" + required: true + choices: ['present', 'absent'] + name: + description: + - the rules name + default: null + required: true + direction: + description: + - is this rule for inbound or outbound trafic + default: null + required: true + choices: [ 'In', 'Out' ] + action: + description: + - what to do with the items this rule is for + default: null + required: true + choices: [ 'allow', 'block' ] + description: + description: + - description for the firewall rule + default: null + required: false + localip: + description: + - the local ip address this rule applies to + default: null + required: false + remoteip: + description: + - the remote ip address/range this rule applies to + default: null + required: false + localport: + description: + - the local port this rule applies to + default: null + required: false + remoteport: + description: + - the remote port this rule applies to + default: null + required: false + program: + description: + - the program this rule applies to + default: null + required: false + service: + description: + - the service this rule applies to + default: null + required: false + protocol: + description: + - the protocol this rule applies to + default: null + required: false + force: + description: + - Enforces the change if a rule with different values exists + default: false + required: false + + +''' + +EXAMPLES = ''' +# create smtp firewall rule + action: win_fw + args: + name: smtp + state: present + localport: 25 + action: allow + protocol: TCP + +''' \ No newline at end of file From f8045f45746e21d0b8cd0ef3a89d670bdbe665fa Mon Sep 17 00:00:00 2001 From: M0ses Date: Thu, 2 Apr 2015 19:04:56 +0200 Subject: [PATCH 0038/2522] fix errorhandling in zypper.py module package_latest was calling package_present but did not care about the return code so errors in package_present were hidden and everthing look ok on the console when zypper update did not fail, but no packages where installed. --- packaging/os/zypper.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packaging/os/zypper.py b/packaging/os/zypper.py index a6fdc5e7189..8fe2fc57b1a 100644 --- a/packaging/os/zypper.py +++ b/packaging/os/zypper.py @@ -183,6 +183,11 @@ def package_latest(m, name, installed_state, disable_gpg_check, disable_recommen # first of all, make sure all the packages are installed (rc, stdout, stderr, changed) = package_present(m, name, installed_state, disable_gpg_check, disable_recommends, old_zypper) + # return if an error occured while installation + # otherwise error messages will be lost and user doesn`t see any error + if rc: + return (rc, stdout, stderr, changed) + # if we've already made a change, we don't have to check whether a version changed if not changed: pre_upgrade_versions = get_current_version(m, name) From c6d56809670b8e486a7f9e420edce178dda8f543 Mon Sep 17 00:00:00 2001 From: schwartzmx Date: Tue, 14 Apr 2015 23:51:02 -0500 Subject: [PATCH 0039/2522] fixes unzip bug for zip files, thanks to @ryanwalls - also fixes possible import errors, and switches to use Start-Process on install to correctly wait --- windows/win_unzip.ps1 | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/windows/win_unzip.ps1 b/windows/win_unzip.ps1 index f31a6273a39..8e6db762fe1 100644 --- a/windows/win_unzip.ps1 +++ b/windows/win_unzip.ps1 @@ -33,7 +33,7 @@ If ($params.src) { Fail-Json $result "src file: $src does not exist." } - $ext = [System.IO.Path]::GetExtension($dest) + $ext = [System.IO.Path]::GetExtension($src) } Else { Fail-Json $result "missing required argument: src" @@ -93,7 +93,7 @@ Else { If (-Not ($list -match "PSCX")) { # Try install with chocolatey Try { - cinst -force PSCX + cinst -force PSCX -y $choco = $true } Catch { @@ -109,9 +109,7 @@ Else { Fail-Json $result "Error downloading PSCX from $url and saving as $dest" } Try { - msiexec.exe /i $msi /qb - # Give it a chance to install, so that it can be imported - sleep 10 + Start-Process -FilePath msiexec.exe -ArgumentList "/i $msi /qb" -Verb Runas -PassThru -Wait | out-null } Catch { Fail-Json $result "Error installing $msi" @@ -127,7 +125,12 @@ Else { # Import Try { If ($installed) { - Import-Module 'C:\Program Files (x86)\Powershell Community Extensions\pscx3\pscx\pscx.psd1' + Try { + Import-Module 'C:\Program Files (x86)\Powershell Community Extensions\pscx3\pscx\pscx.psd1' + } + Catch { + Import-Module PSCX + } } Else { Import-Module PSCX From fbd8c6b398fa3dfe2ffb07a5e38d97f349232470 Mon Sep 17 00:00:00 2001 From: Charlie Root Date: Tue, 21 Apr 2015 15:44:58 +0300 Subject: [PATCH 0040/2522] Add rootdir option to pkgng --- packaging/os/pkgng.py | 74 +++++++++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/packaging/os/pkgng.py b/packaging/os/pkgng.py index 1aa8e0c737f..1bdb55f99c8 100644 --- a/packaging/os/pkgng.py +++ b/packaging/os/pkgng.py @@ -63,6 +63,11 @@ for newer pkgng versions, specify a the name of a repository configured in /usr/local/etc/pkg/repos required: false + rootdir: + description: + - for pkgng versions 1.5 and later, pkg will install all packages + within the specified root directory + required: false author: bleader notes: - When using pkgsite, be careful that already in cache packages won't be downloaded again. @@ -86,9 +91,9 @@ import re import sys -def query_package(module, pkgng_path, name): +def query_package(module, pkgng_path, name, rootdir_arg): - rc, out, err = module.run_command("%s info -g -e %s" % (pkgng_path, name)) + rc, out, err = module.run_command("%s %s info -g -e %s" % (pkgng_path, rootdir_arg, name)) if rc == 0: return True @@ -112,19 +117,19 @@ def pkgng_older_than(module, pkgng_path, compare_version): return not new_pkgng -def remove_packages(module, pkgng_path, packages): +def remove_packages(module, pkgng_path, packages, rootdir_arg): remove_c = 0 # Using a for loop incase of error, we can report the package that failed for package in packages: # Query the package first, to see if we even need to remove - if not query_package(module, pkgng_path, package): + if not query_package(module, pkgng_path, package, rootdir_arg): continue if not module.check_mode: - rc, out, err = module.run_command("%s delete -y %s" % (pkgng_path, package)) + rc, out, err = module.run_command("%s %s delete -y %s" % (pkgng_path, rootdir_arg, package)) - if not module.check_mode and query_package(module, pkgng_path, package): + if not module.check_mode and query_package(module, pkgng_path, package, rootdir_arg): module.fail_json(msg="failed to remove %s: %s" % (package, out)) remove_c += 1 @@ -136,7 +141,7 @@ def remove_packages(module, pkgng_path, packages): return (False, "package(s) already absent") -def install_packages(module, pkgng_path, packages, cached, pkgsite): +def install_packages(module, pkgng_path, packages, cached, pkgsite, rootdir_arg): install_c = 0 @@ -161,16 +166,16 @@ def install_packages(module, pkgng_path, packages, cached, pkgsite): module.fail_json(msg="Could not update catalogue") for package in packages: - if query_package(module, pkgng_path, package): + if query_package(module, pkgng_path, package, rootdir_arg): continue if not module.check_mode: if old_pkgng: rc, out, err = module.run_command("%s %s %s install -g -U -y %s" % (batch_var, pkgsite, pkgng_path, package)) else: - rc, out, err = module.run_command("%s %s install %s -g -U -y %s" % (batch_var, pkgng_path, pkgsite, package)) + rc, out, err = module.run_command("%s %s %s install %s -g -U -y %s" % (batch_var, pkgng_path, rootdir_arg, pkgsite, package)) - if not module.check_mode and not query_package(module, pkgng_path, package): + if not module.check_mode and not query_package(module, pkgng_path, package, rootdir_arg): module.fail_json(msg="failed to install %s: %s" % (package, out), stderr=err) install_c += 1 @@ -180,20 +185,20 @@ def install_packages(module, pkgng_path, packages, cached, pkgsite): return (False, "package(s) already present") -def annotation_query(module, pkgng_path, package, tag): - rc, out, err = module.run_command("%s info -g -A %s" % (pkgng_path, package)) +def annotation_query(module, pkgng_path, package, tag, rootdir_arg): + rc, out, err = module.run_command("%s %s info -g -A %s" % (pkgng_path, rootdir_arg, package)) match = re.search(r'^\s*(?P%s)\s*:\s*(?P\w+)' % tag, out, flags=re.MULTILINE) if match: return match.group('value') return False -def annotation_add(module, pkgng_path, package, tag, value): - _value = annotation_query(module, pkgng_path, package, tag) +def annotation_add(module, pkgng_path, package, tag, value, rootdir_arg): + _value = annotation_query(module, pkgng_path, package, tag, rootdir_arg) if not _value: # Annotation does not exist, add it. - rc, out, err = module.run_command('%s annotate -y -A %s %s "%s"' - % (pkgng_path, package, tag, value)) + rc, out, err = module.run_command('%s %s annotate -y -A %s %s "%s"' + % (pkgng_path, rootdir_arg, package, tag, value)) if rc != 0: module.fail_json("could not annotate %s: %s" % (package, out), stderr=err) @@ -208,19 +213,19 @@ def annotation_add(module, pkgng_path, package, tag, value): # Annotation exists, nothing to do return False -def annotation_delete(module, pkgng_path, package, tag, value): - _value = annotation_query(module, pkgng_path, package, tag) +def annotation_delete(module, pkgng_path, package, tag, value, rootdir_arg): + _value = annotation_query(module, pkgng_path, package, tag, rootdir_arg) if _value: - rc, out, err = module.run_command('%s annotate -y -D %s %s' - % (pkgng_path, package, tag)) + rc, out, err = module.run_command('%s %s annotate -y -D %s %s' + % (pkgng_path, rootdir_arg, package, tag)) if rc != 0: module.fail_json("could not delete annotation to %s: %s" % (package, out), stderr=err) return True return False -def annotation_modify(module, pkgng_path, package, tag, value): - _value = annotation_query(module, pkgng_path, package, tag) +def annotation_modify(module, pkgng_path, package, tag, value, rootdir_arg): + _value = annotation_query(module, pkgng_path, package, tag, rootdir_arg) if not value: # No such tag module.fail_json("could not change annotation to %s: tag %s does not exist" @@ -229,15 +234,15 @@ def annotation_modify(module, pkgng_path, package, tag, value): # No change in value return False else: - rc,out,err = module.run_command('%s annotate -y -M %s %s "%s"' - % (pkgng_path, package, tag, value)) + rc,out,err = module.run_command('%s %s annotate -y -M %s %s "%s"' + % (pkgng_path, rootdir_arg, package, tag, value)) if rc != 0: module.fail_json("could not change annotation annotation to %s: %s" % (package, out), stderr=err) return True -def annotate_packages(module, pkgng_path, packages, annotation): +def annotate_packages(module, pkgng_path, packages, annotation, rootdir_arg): annotate_c = 0 annotations = map(lambda _annotation: re.match(r'(?P[\+-:])(?P\w+)(=(?P\w+))?', @@ -254,7 +259,7 @@ def annotate_packages(module, pkgng_path, packages, annotation): for _annotation in annotations: annotate_c += ( 1 if operation[_annotation['operation']]( module, pkgng_path, package, - _annotation['tag'], _annotation['value']) else 0 ) + _annotation['tag'], _annotation['value'], rootdir_arg) else 0 ) if annotate_c > 0: return (True, "added %s annotations." % annotate_c) @@ -267,7 +272,8 @@ def main(): name = dict(aliases=["pkg"], required=True), cached = dict(default=False, type='bool'), annotation = dict(default="", required=False), - pkgsite = dict(default="", required=False)), + pkgsite = dict(default="", required=False), + rootdir = dict(default="", required=False)), supports_check_mode = True) pkgng_path = module.get_bin_path('pkg', True) @@ -278,19 +284,27 @@ def main(): changed = False msgs = [] + rootdir_arg = "" + + if p["rootdir"] != "": + old_pkgng = pkgng_older_than(module, pkgng_path, [1, 5, 0]) + if old_pkgng: + module.fail_json(msg="To use option 'rootdir' pkg version must be 1.5 or greater") + else: + rootdir_arg = "--rootdir %s" % (p["rootdir"]) if p["state"] == "present": - _changed, _msg = install_packages(module, pkgng_path, pkgs, p["cached"], p["pkgsite"]) + _changed, _msg = install_packages(module, pkgng_path, pkgs, p["cached"], p["pkgsite"], rootdir_arg) changed = changed or _changed msgs.append(_msg) elif p["state"] == "absent": - _changed, _msg = remove_packages(module, pkgng_path, pkgs) + _changed, _msg = remove_packages(module, pkgng_path, pkgs, rootdir_arg) changed = changed or _changed msgs.append(_msg) if p["annotation"]: - _changed, _msg = annotate_packages(module, pkgng_path, pkgs, p["annotation"]) + _changed, _msg = annotate_packages(module, pkgng_path, pkgs, p["annotation"], rootdir_arg) changed = changed or _changed msgs.append(_msg) From 91483bdd6b9a3dd0c0ad047a1209801068afcb27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Wallstro=CC=88m?= Date: Fri, 24 Apr 2015 10:48:02 +0200 Subject: [PATCH 0041/2522] Modules to manage IIS Wraps the Web Server Administration module for PowerShell into Ansible modules. --- windows/win_iis_virtualdirectory.ps1 | 128 +++++++++++++++++++ windows/win_iis_virtualdirectory.py | 67 ++++++++++ windows/win_iis_webapplication.ps1 | 132 ++++++++++++++++++++ windows/win_iis_webapplication.py | 68 ++++++++++ windows/win_iis_webapppool.ps1 | 112 +++++++++++++++++ windows/win_iis_webapppool.py | 112 +++++++++++++++++ windows/win_iis_webbinding.ps1 | 138 +++++++++++++++++++++ windows/win_iis_webbinding.py | 143 +++++++++++++++++++++ windows/win_iis_website.ps1 | 179 +++++++++++++++++++++++++++ windows/win_iis_website.py | 133 ++++++++++++++++++++ 10 files changed, 1212 insertions(+) create mode 100644 windows/win_iis_virtualdirectory.ps1 create mode 100644 windows/win_iis_virtualdirectory.py create mode 100644 windows/win_iis_webapplication.ps1 create mode 100644 windows/win_iis_webapplication.py create mode 100644 windows/win_iis_webapppool.ps1 create mode 100644 windows/win_iis_webapppool.py create mode 100644 windows/win_iis_webbinding.ps1 create mode 100644 windows/win_iis_webbinding.py create mode 100644 windows/win_iis_website.ps1 create mode 100644 windows/win_iis_website.py diff --git a/windows/win_iis_virtualdirectory.ps1 b/windows/win_iis_virtualdirectory.ps1 new file mode 100644 index 00000000000..3f2ab692b42 --- /dev/null +++ b/windows/win_iis_virtualdirectory.ps1 @@ -0,0 +1,128 @@ +#!powershell +# -*- coding: utf-8 -*- + +# (c) 2015, Henrik Wallström +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# WANT_JSON +# POWERSHELL_COMMON + +$params = Parse-Args $args; + +# Name parameter +$name = Get-Attr $params "name" $FALSE; +If ($name -eq $FALSE) { + Fail-Json (New-Object psobject) "missing required argument: name"; +} + +# Site +$site = Get-Attr $params "site" $FALSE; +If ($site -eq $FALSE) { + Fail-Json (New-Object psobject) "missing required argument: site"; +} + +# Application +$application = Get-Attr $params "application" $FALSE; + +# State parameter +$state = Get-Attr $params "state" "present"; +If (($state -ne 'present') -and ($state -ne 'absent')) { + Fail-Json $result "state is '$state'; must be 'present' or 'absent'" +} + +# Path parameter +$physical_path = Get-Attr $params "physical_path" $FALSE; + +# Ensure WebAdministration module is loaded +if ((Get-Module "WebAdministration" -ErrorAction SilentlyContinue) -eq $null) { + Import-Module WebAdministration +} + +# Result +$result = New-Object psobject @{ + directory = New-Object psobject + changed = $false +}; + +# Construct path +$directory_path = if($application) { + "IIS:\Sites\$($site)\$($application)\$($name)" +} else { + "IIS:\Sites\$($site)\$($name)" +} + +# Directory info +$directory = Get-WebVirtualDirectory -Site $site -Name $name + +try { + # Add directory + If(($state -eq 'present') -and (-not $directory)) { + If ($physical_path -eq $FALSE) { + Fail-Json (New-Object psobject) "missing required arguments: physical_path" + } + If (-not (Test-Path $physical_path)) { + Fail-Json (New-Object psobject) "specified folder must already exist: physical_path" + } + + $directory_parameters = New-Object psobject @{ + Site = $site + Name = $name + PhysicalPath = $physical_path + }; + + If ($application) { + $directory_parameters.Application = $application + } + + $directory = New-WebVirtualDirectory @directory_parameters -Force + $result.changed = $true + } + + # Remove directory + If ($state -eq 'absent' -and $directory) { + Remove-Item $directory_path + $result.changed = $true + } + + $directory = Get-WebVirtualDirectory -Site $site -Name $name + If($directory) { + + # Change Physical Path if needed + if($physical_path) { + If (-not (Test-Path $physical_path)) { + Fail-Json (New-Object psobject) "specified folder must already exist: physical_path" + } + + $vdir_folder = Get-Item $directory.PhysicalPath + $folder = Get-Item $physical_path + If($folder.FullName -ne $vdir_folder.FullName) { + Set-ItemProperty $directory_path -name physicalPath -value $physical_path + $result.changed = $true + } + } + } +} catch { + Fail-Json $result $_.Exception.Message +} + +# Result +$directory = Get-WebVirtualDirectory -Site $site -Name $name +$result.directory = New-Object psobject @{ + PhysicalPath = $directory.PhysicalPath +} + +Exit-Json $result diff --git a/windows/win_iis_virtualdirectory.py b/windows/win_iis_virtualdirectory.py new file mode 100644 index 00000000000..bbedfbbb4ab --- /dev/null +++ b/windows/win_iis_virtualdirectory.py @@ -0,0 +1,67 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Henrik Wallström +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: win_iis_virtualdirectory +version_added: "1.9" +short_description: Configures a IIS virtual directories. +description: + - Creates, Removes and configures a IIS Web site +options: + name: + description: + - The name of the virtual directory to create. + required: true + default: null + aliases: [] + state: + description: + - + choices: + - absent + - present + required: false + default: null + aliases: [] + site: + description: + - The site name under which the virtual directory is created or exists. + required: false + default: null + aliases: [] + application: + description: + - The application under which the virtual directory is created or exists. + required: false + default: null + aliases: [] + physical_path: + description: + - The physical path to the folder in which the new virtual directory is created. The specified folder must already exist. + required: false + default: null + aliases: [] +author: Henrik Wallström +''' + +EXAMPLES = ''' + +''' diff --git a/windows/win_iis_webapplication.ps1 b/windows/win_iis_webapplication.ps1 new file mode 100644 index 00000000000..e576dd5081c --- /dev/null +++ b/windows/win_iis_webapplication.ps1 @@ -0,0 +1,132 @@ +#!powershell + +# (c) 2015, Henrik Wallström +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# WANT_JSON +# POWERSHELL_COMMON + +$params = Parse-Args $args; + +# Name parameter +$name = Get-Attr $params "name" $FALSE; +If ($name -eq $FALSE) { + Fail-Json (New-Object psobject) "missing required argument: name"; +} + +# Site +$site = Get-Attr $params "site" $FALSE; +If ($site -eq $FALSE) { + Fail-Json (New-Object psobject) "missing required argument: site"; +} + +# State parameter +$state = Get-Attr $params "state" "present"; +$state.ToString().ToLower(); +If (($state -ne 'present') -and ($state -ne 'absent')) { + Fail-Json $result "state is '$state'; must be 'present' or 'absent'" +} + +# Path parameter +$physical_path = Get-Attr $params "physical_path" $FALSE; + +# Application Pool Parameter +$application_pool = Get-Attr $params "application_pool" $FALSE; + + +# Ensure WebAdministration module is loaded +if ((Get-Module "WebAdministration" -ErrorAction SilentlyContinue) -eq $null) { + Import-Module WebAdministration +} + +# Result +$result = New-Object psobject @{ + application = New-Object psobject + changed = $false +}; + +# Application info +$application = Get-WebApplication -Site $site -Name $name + +try { + # Add application + If(($state -eq 'present') -and (-not $application)) { + If ($physical_path -eq $FALSE) { + Fail-Json (New-Object psobject) "missing required arguments: physical_path" + } + If (-not (Test-Path $physical_path)) { + Fail-Json (New-Object psobject) "specified folder must already exist: physical_path" + } + + $application_parameters = New-Object psobject @{ + Site = $site + Name = $name + PhysicalPath = $physical_path + }; + + If ($application_pool) { + $application_parameters.ApplicationPool = $application_pool + } + + $application = New-WebApplication @application_parameters -Force + $result.changed = $true + + } + + # Remove application + if ($state -eq 'absent' -and $application) { + $application = Remove-WebApplication -Site $site -Name $name + $result.changed = $true + } + + $application = Get-WebApplication -Site $site -Name $name + If($application) { + + # Change Physical Path if needed + if($physical_path) { + If (-not (Test-Path $physical_path)) { + Fail-Json (New-Object psobject) "specified folder must already exist: physical_path" + } + + $app_folder = Get-Item $application.PhysicalPath + $folder = Get-Item $physical_path + If($folder.FullName -ne $app_folder.FullName) { + Set-ItemProperty "IIS:\Sites\$($site)\$($name)" -name physicalPath -value $physical_path + $result.changed = $true + } + } + + # Change Application Pool if needed + if($application_pool) { + If($application_pool -ne $application.applicationPool) { + Set-ItemProperty "IIS:\Sites\$($site)\$($name)" -name applicationPool -value $application_pool + $result.changed = $true + } + } + } +} catch { + Fail-Json $result $_.Exception.Message +} + +# Result +$application = Get-WebApplication -Site $site -Name $name +$result.application = New-Object psobject @{ + PhysicalPath = $application.PhysicalPath + ApplicationPool = $application.applicationPool +} + +Exit-Json $result diff --git a/windows/win_iis_webapplication.py b/windows/win_iis_webapplication.py new file mode 100644 index 00000000000..d8a59b66054 --- /dev/null +++ b/windows/win_iis_webapplication.py @@ -0,0 +1,68 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Henrik Wallström +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: win_iis_website +version_added: "1.9" +short_description: Configures a IIS Web application. +description: + - Creates, Removes and configures a IIS Web applications +options: + name: + description: + - Name of the Web applicatio + required: true + default: null + aliases: [] + site: + description: + - Name of the site on which the application is created. + required: true + default: null + aliases: [] + state: + description: + - State of the web application + choices: + - present + - absent + required: false + default: null + aliases: [] + physical_path: + description: + - The physical path on the remote host to use for the new applicatiojn. The specified folder must already exist. + required: false + default: null + aliases: [] + application_pool: + description: + - The application pool in which the new site executes. + required: false + default: null + aliases: [] +author: Henrik Wallström +''' + +EXAMPLES = ''' +$ ansible -i hosts -m win_iis_webapplication -a "name=api site=acme physical_path=c:\\apps\\acme\\api" host + +''' diff --git a/windows/win_iis_webapppool.ps1 b/windows/win_iis_webapppool.ps1 new file mode 100644 index 00000000000..2ed369e4a3f --- /dev/null +++ b/windows/win_iis_webapppool.ps1 @@ -0,0 +1,112 @@ +#!powershell + +# (c) 2015, Henrik Wallström +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + + +# WANT_JSON +# POWERSHELL_COMMON + +$params = Parse-Args $args; + +# Name parameter +$name = Get-Attr $params "name" $FALSE; +If ($name -eq $FALSE) { + Fail-Json (New-Object psobject) "missing required argument: name"; +} + +# State parameter +$state = Get-Attr $params "state" $FALSE; +$valid_states = ('started', 'restarted', 'stopped', 'absent'); +If (($state -Ne $FALSE) -And ($state -NotIn $valid_states)) { + Fail-Json $result "state is '$state'; must be $($valid_states)" +} + +# Attributes parameter - Pipe separated list of attributes where +# keys and values are separated by comma (paramA:valyeA|paramB:valueB) +$attributes = @{}; +If ($params.attributes) { + $params.attributes -split '\|' | foreach { + $key, $value = $_ -split "\:"; + $attributes.Add($key, $value); + } +} + +# Ensure WebAdministration module is loaded +if ((Get-Module "WebAdministration" -ErrorAction SilentlyContinue) -eq $NULL){ + Import-Module WebAdministration +} + +# Result +$result = New-Object psobject @{ + changed = $FALSE + attributes = $attributes +}; + +# Get pool +$pool = Get-Item IIS:\AppPools\$name + +try { + # Add + if (-not $pool -and $state -in ('started', 'stopped', 'restarted')) { + New-WebAppPool $name + $result.changed = $TRUE + } + + # Remove + if ($pool -and $state -eq 'absent') { + Remove-WebAppPool $name + $result.changed = $TRUE + } + + $pool = Get-Item IIS:\AppPools\$name + if($pool) { + # Set properties + $attributes.GetEnumerator() | foreach { + $newParameter = $_; + $currentParameter = Get-ItemProperty ("IIS:\AppPools\" + $name) $newParameter.Key + if(-not $currentParameter -or ($currentParameter.Value -as [String]) -ne $newParameter.Value) { + Set-ItemProperty ("IIS:\AppPools\" + $name) $newParameter.Key $newParameter.Value + $result.changed = $TRUE + } + } + + # Set run state + if (($state -eq 'stopped') -and ($pool.State -eq 'Started')) { + Stop-WebAppPool -Name $name -ErrorAction Stop + $result.changed = $TRUE + } + if ((($state -eq 'started') -and ($pool.State -eq 'Stopped')) -or ($state -eq 'restarted')) { + Start-WebAppPool -Name $name -ErrorAction Stop + $result.changed = $TRUE + } + } +} catch { + Fail-Json $result $_.Exception.Message +} + +# Result +$pool = Get-Item IIS:\AppPools\$name +$result.info = @{ + name = $pool.Name + state = $pool.State + attributes = New-Object psobject @{} +}; + +$pool.Attributes | ForEach { $result.info.attributes.Add($_.Name, $_.Value)}; + +Exit-Json $result diff --git a/windows/win_iis_webapppool.py b/windows/win_iis_webapppool.py new file mode 100644 index 00000000000..320fe07f637 --- /dev/null +++ b/windows/win_iis_webapppool.py @@ -0,0 +1,112 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Henrik Wallström +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + + +DOCUMENTATION = ''' +--- +module: win_iis_webapppool +version_added: "1.9" +short_description: Configures a IIS Web Application Pool. +description: + - Creates, Removes and configures a IIS Web Application Pool +options: + name: + description: + - Names of application pool + required: true + default: null + aliases: [] + state: + description: + - State of the binding + choices: + - absent + - stopped + - started + - restarted + required: false + default: null + aliases: [] + attributes: + description: + - Application Pool attributes from string where attributes are seperated by a pipe and attribute name/values by colon Ex. "foo:1|bar:2" + required: false + default: null + aliases: [] +author: Henrik Wallström +''' + +EXAMPLES = ''' +# This return information about an existing application pool +$ansible -i inventory -m win_iis_webapppool -a "name='DefaultAppPool'" windows +host | success >> { + "attributes": {}, + "changed": false, + "info": { + "attributes": { + "CLRConfigFile": "", + "applicationPoolSid": "S-1-5-82-3006700770-424185619-1745488364-794895919-4004696415", + "autoStart": true, + "enable32BitAppOnWin64": false, + "enableConfigurationOverride": true, + "managedPipelineMode": 0, + "managedRuntimeLoader": "webengine4.dll", + "managedRuntimeVersion": "v4.0", + "name": "DefaultAppPool", + "passAnonymousToken": true, + "queueLength": 1000, + "startMode": 0, + "state": 1 + }, + "name": "DefaultAppPool", + "state": "Started" + } +} + +# This creates a new application pool in 'Started' state +$ ansible -i inventory -m win_iis_webapppool -a "name='AppPool' state=started" windows + +# This stoppes an application pool +$ ansible -i inventory -m win_iis_webapppool -a "name='AppPool' state=stopped" windows + +# This restarts an application pool +$ ansible -i inventory -m win_iis_webapppool -a "name='AppPool' state=restart" windows + +# This restarts an application pool +$ ansible -i inventory -m win_iis_webapppool -a "name='AppPool' state=restart" windows + +# This change application pool attributes without touching state +$ ansible -i inventory -m win_iis_webapppool -a "name='AppPool' attributes='managedRuntimeVersion:v4.0|autoStart:false'" windows + +# This creates an application pool and sets attributes +$ ansible -i inventory -m win_iis_webapppool -a "name='AnotherAppPool' state=started attributes='managedRuntimeVersion:v4.0|autoStart:false'" windows + + +# Playbook example +--- + +- name: App Pool with .NET 4.0 + win_iis_webapppool: + name: 'AppPool' + state: started + attributes: managedRuntimeVersion:v4.0 + register: webapppool + +''' diff --git a/windows/win_iis_webbinding.ps1 b/windows/win_iis_webbinding.ps1 new file mode 100644 index 00000000000..bdff43fc63c --- /dev/null +++ b/windows/win_iis_webbinding.ps1 @@ -0,0 +1,138 @@ +#!powershell + +# (c) 2015, Henrik Wallström +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + + +# WANT_JSON +# POWERSHELL_COMMON + +$params = Parse-Args $args; + +# Name parameter +$name = Get-Attr $params "name" $FALSE; +If ($name -eq $FALSE) { + Fail-Json (New-Object psobject) "missing required argument: name"; +} + +# State parameter +$state = Get-Attr $params "state" $FALSE; +$valid_states = ($FALSE, 'present', 'absent'); +If ($state -NotIn $valid_states) { + Fail-Json $result "state is '$state'; must be $($valid_states)" +} + +$binding_parameters = New-Object psobject @{ + Name = $name +}; + +If ($params.host_header) { + $binding_parameters.HostHeader = $params.host_header +} + +If ($params.protocol) { + $binding_parameters.Protocol = $params.protocol +} + +If ($params.port) { + $binding_parameters.Port = $params.port +} + +If ($params.ip) { + $binding_parameters.IPAddress = $params.ip +} + +$certificateHash = Get-Attr $params "certificate_hash" $FALSE; +$certificateStoreName = Get-Attr $params "certificate_store_name" "MY"; + +# Ensure WebAdministration module is loaded +if ((Get-Module "WebAdministration" -ErrorAction SilentlyContinue) -eq $null){ + Import-Module WebAdministration +} + +function Create-Binding-Info { + return New-Object psobject @{ + "bindingInformation" = $args[0].bindingInformation + "certificateHash" = $args[0].certificateHash + "certificateStoreName" = $args[0].certificateStoreName + "isDsMapperEnabled" = $args[0].isDsMapperEnabled + "protocol" = $args[0].protocol + "sslFlags" = $args[0].sslFlags + } +} + +# Result +$result = New-Object psobject @{ + changed = $false + parameters = $binding_parameters + matched = @() + removed = @() + added = @() +}; + +# Get bindings matching parameters +$curent_bindings = Get-WebBinding @binding_parameters +$curent_bindings | Foreach { + $result.matched += Create-Binding-Info $_ +} + +try { + # Add + if (-not $curent_bindings -and $state -eq 'present') { + New-WebBinding @binding_parameters -Force + + # Select certificat + if($certificateHash -ne $FALSE) { + + $ip = $binding_parameters.IPAddress + if((!$ip) -or ($ip -eq "*")) { + $ip = "0.0.0.0" + } + + $port = $binding_parameters.Port + if(!$port) { + $port = 443 + } + + $result.port = $port + $result.ip = $ip + + Push-Location IIS:\SslBindings\ + Get-Item Cert:\LocalMachine\$certificateStoreName\$certificateHash | New-Item "$($ip)!$($port)" + Pop-Location + } + + $result.added += Create-Binding-Info (Get-WebBinding @binding_parameters) + $result.changed = $true + } + + # Remove + if ($curent_bindings -and $state -eq 'absent') { + $curent_bindings | foreach { + Remove-WebBinding -InputObject $_ + $result.removed += Create-Binding-Info $_ + } + $result.changed = $true + } + + +} +catch { + Fail-Json $result $_.Exception.Message +} + +Exit-Json $result diff --git a/windows/win_iis_webbinding.py b/windows/win_iis_webbinding.py new file mode 100644 index 00000000000..0cc5da158bf --- /dev/null +++ b/windows/win_iis_webbinding.py @@ -0,0 +1,143 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Henrik Wallström +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + + +DOCUMENTATION = ''' +--- +module: win_iis_webbinding +version_added: "1.9" +short_description: Configures a IIS Web site. +description: + - Creates, Removes and configures a binding to an existing IIS Web site +options: + name: + description: + - Names of web site + required: true + default: null + aliases: [] + state: + description: + - State of the binding + choices: + - present + - absent + required: false + default: null + aliases: [] + port: + description: + - The port to bind to / use for the new site. + required: false + default: null + aliases: [] + ip: + description: + - The IP address to bind to / use for the new site. + required: false + default: null + aliases: [] + host_header: + description: + - The host header to bind to / use for the new site. + required: false + default: null + aliases: [] + protocol: + description: + - The protocol to be used for the Web binding (usually HTTP, HTTPS, or FTP). + required: false + default: null + aliases: [] + protocol: + description: + - The protocol to be used for the Web binding (usually HTTP, HTTPS, or FTP). + required: false + default: null + aliases: [] + certificate_hash: + description: + - Certificate hash for the SSL binding. The certificate hash is the unique identifier for the certificate. + required: false + default: null + aliases: [] + certificate_store_name: + description: + - Name of the certificate store where the certificate for the binding is located. + required: false + default: "My" + aliases: [] +author: Henrik Wallström +''' + +EXAMPLES = ''' +# This will return binding information for an existing host +$ ansible -i vagrant-inventory -m win_iis_webbinding -a "name='Default Web Site'" windows +host | success >> { + "added": [], + "changed": false, + "matched": [ + { + "bindingInformation": "*:80:", + "certificateHash": "", + "certificateStoreName": "", + "isDsMapperEnabled": false, + "protocol": "http", + "sslFlags": 0 + } + ], + "parameters": { + "Name": "Default Web Site" + }, + "removed": [] +} + +# This will return the HTTPS binding information for an existing host +$ ansible -i vagrant-inventory -m win_iis_webbinding -a "name='Default Web Site' protocol=https" windows + +# This will return the HTTPS binding information for an existing host +$ ansible -i vagrant-inventory -m win_iis_webbinding -a "name='Default Web Site' port:9090 state=present" windows + +# This will add a HTTP binding on port 9090 +$ ansible -i vagrant-inventory -m win_iis_webbinding -a "name='Default Web Site' port=9090 state=present" windows + +# This will remove the HTTP binding on port 9090 +$ ansible -i vagrant-inventory -m win_iis_webbinding -a "name='Default Web Site' port=9090 state=present" windows + +# This will add a HTTPS binding +$ ansible -i vagrant-inventory -m win_iis_webbinding -a "name='Default Web Site' protocol=https state=present" windows + +# This will add a HTTPS binding and select certificate to use +# ansible -i vagrant-inventory -m win_iis_webbinding -a "name='Default Web Site' protocol=https certificate_hash= B0D0FA8408FC67B230338FCA584D03792DA73F4C" windows + + +# Playbook example +--- + +- name: Website http/https bidings + win_iis_webbinding: + name: "Default Web Site" + protocol: https + port: 443 + certificate_hash: "D1A3AF8988FD32D1A3AF8988FD323792DA73F4C" + state: present + when: monitor_use_https + +''' diff --git a/windows/win_iis_website.ps1 b/windows/win_iis_website.ps1 new file mode 100644 index 00000000000..bba1e941142 --- /dev/null +++ b/windows/win_iis_website.ps1 @@ -0,0 +1,179 @@ +#!powershell + +# (c) 2015, Henrik Wallström +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# WANT_JSON +# POWERSHELL_COMMON + +$params = Parse-Args $args; + +# Name parameter +$name = Get-Attr $params "name" $FALSE; +If ($name -eq $FALSE) { + Fail-Json (New-Object psobject) "missing required argument: name"; +} + +# State parameter +$state = Get-Attr $params "state" $FALSE; +$state.ToString().ToLower(); +If (($state -ne $FALSE) -and ($state -ne 'started') -and ($state -ne 'stopped') -and ($state -ne 'restarted') -and ($state -ne 'absent')) { + Fail-Json (New-Object psobject) "state is '$state'; must be 'started', 'restarted', 'stopped' or 'absent'" +} + +# Path parameter +$physical_path = Get-Attr $params "physical_path" $FALSE; + +# Application Pool Parameter +$application_pool = Get-Attr $params "application_pool" $FALSE; + +# Binding Parameters +$bind_port = Get-Attr $params "port" $FALSE; +$bind_ip = Get-Attr $params "ip" $FALSE; +$bind_hostname = Get-Attr $params "hostname" $FALSE; +$bind_ssl = Get-Attr $params "ssl" $FALSE; + +# Custom site Parameters from string where properties +# are seperated by a pipe and property name/values by colon. +# Ex. "foo:1|bar:2" +$parameters = Get-Attr $params "parameters" $null; +if($parameters -ne $null) { + $parameters = @($parameters -split '\|' | ForEach { + return ,($_ -split "\:", 2); + }) +} + + +# Ensure WebAdministration module is loaded +if ((Get-Module "WebAdministration" -ErrorAction SilentlyContinue) -eq $null) { + Import-Module WebAdministration +} + +# Result +$result = New-Object psobject @{ + site = New-Object psobject + changed = $false +}; + +# Site info +$site = Get-Website -Name $name + +Try { + # Add site + If(($state -ne 'absent') -and (-not $site)) { + If ($physical_path -eq $FALSE) { + Fail-Json (New-Object psobject) "missing required arguments: physical_path" + } + ElseIf (-not (Test-Path $physical_path)) { + Fail-Json (New-Object psobject) "specified folder must already exist: physical_path" + } + + $site_parameters = New-Object psobject @{ + Name = $name + PhysicalPath = $physical_path + }; + + If ($application_pool) { + $site_parameters.ApplicationPool = $application_pool + } + + If ($bind_port) { + $site_parameters.Port = $bind_port + } + + If ($bind_ip) { + $site_parameters.IPAddress = $bind_ip + } + + If ($bind_hostname) { + $site_parameters.HostHeader = $bind_hostname + } + + $site = New-Website @site_parameters -Force + $result.changed = $true + } + + # Remove site + If ($state -eq 'absent' -and $site) { + $site = Remove-Website -Name $name + $result.changed = $true + } + + $site = Get-Website -Name $name + If($site) { + # Change Physical Path if needed + if($physical_path) { + If (-not (Test-Path $physical_path)) { + Fail-Json (New-Object psobject) "specified folder must already exist: physical_path" + } + + $folder = Get-Item $physical_path + If($folder.FullName -ne $site.PhysicalPath) { + Set-ItemProperty "IIS:\Sites\$($site.Name)" -name physicalPath -value $folder.FullName + $result.changed = $true + } + } + + # Change Application Pool if needed + if($application_pool) { + If($application_pool -ne $site.applicationPool) { + Set-ItemProperty "IIS:\Sites\$($site.Name)" -name applicationPool -value $application_pool + $result.changed = $true + } + } + + # Set properties + if($parameters) { + $parameters | foreach { + $parameter_value = Get-ItemProperty "IIS:\Sites\$($site.Name)" $_[0] + if((-not $parameter_value) -or ($parameter_value.Value -as [String]) -ne $_[1]) { + Set-ItemProperty "IIS:\Sites\$($site.Name)" $_[0] $_[1] + $result.changed = $true + } + } + } + + # Set run state + if (($state -eq 'stopped') -and ($site.State -eq 'Started')) + { + Stop-Website -Name $name -ErrorAction Stop + $result.changed = $true + } + if ((($state -eq 'started') -and ($site.State -eq 'Stopped')) -or ($state -eq 'restarted')) + { + Start-Website -Name $name -ErrorAction Stop + $result.changed = $true + } + } +} +Catch +{ + Fail-Json (New-Object psobject) $_.Exception.Message +} + +$site = Get-Website -Name $name +$result.site = New-Object psobject @{ + Name = $site.Name + ID = $site.ID + State = $site.State + PhysicalPath = $site.PhysicalPath + ApplicationPool = $site.applicationPool + Bindings = @($site.Bindings.Collection | ForEach-Object { $_.BindingInformation }) +} + + +Exit-Json $result diff --git a/windows/win_iis_website.py b/windows/win_iis_website.py new file mode 100644 index 00000000000..0893b11c2bd --- /dev/null +++ b/windows/win_iis_website.py @@ -0,0 +1,133 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Henrik Wallström +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: win_iis_website +version_added: "1.9" +short_description: Configures a IIS Web site. +description: + - Creates, Removes and configures a IIS Web site +options: + name: + description: + - Names of web site + required: true + default: null + aliases: [] + state: + description: + - State of the web site + choices: + - started + - restarted + - stopped + - absent + required: false + default: null + aliases: [] + physical_path: + description: + - The physical path on the remote host to use for the new site. The specified folder must already exist. + required: false + default: null + aliases: [] + application_pool: + description: + - The application pool in which the new site executes. + required: false + default: null + aliases: [] + port: + description: + - The port to bind to / use for the new site. + required: false + default: null + aliases: [] + ip: + description: + - The IP address to bind to / use for the new site. + required: false + default: null + aliases: [] + hostname: + description: + - The host header to bind to / use for the new site. + required: false + default: null + aliases: [] + ssl: + description: + - Enables HTTPS binding on the site.. + required: false + default: null + aliases: [] + parameters: + description: + - Custom site Parameters from string where properties are seperated by a pipe and property name/values by colon Ex. "foo:1|bar:2" + required: false + default: null + aliases: [] +author: Henrik Wallström +''' + +EXAMPLES = ''' +# This return information about an existing host +$ ansible -i vagrant-inventory -m win_iis_website -a "name='Default Web Site'" window +host | success >> { + "changed": false, + "site": { + "ApplicationPool": "DefaultAppPool", + "Bindings": [ + "*:80:" + ], + "ID": 1, + "Name": "Default Web Site", + "PhysicalPath": "%SystemDrive%\\inetpub\\wwwroot", + "State": "Stopped" + } +} + +# This stops an existing site. +$ ansible -i hosts -m win_iis_website -a "name='Default Web Site' state=stopped" host + +# This creates a new site. +$ ansible -i hosts -m win_iis_website -a "name=acme physical_path=c:\\sites\\acme" host + +# Change logfile . +$ ansible -i hosts -m win_iis_website -a "name=acme physical_path=c:\\sites\\acme" host + + +# Playbook example +--- + +- name: Acme IIS site + win_iis_website: + name: "Acme" + state: started + port: 80 + ip: 127.0.0.1 + hostname: acme.local + application_pool: "acme" + physical_path: 'c:\\sites\\acme' + parameters: 'logfile.directory:c:\\sites\\logs' + register: website + +''' From f7961bd227f6edee3d940c55db420f3fa35af26e Mon Sep 17 00:00:00 2001 From: NewGyu Date: Wed, 29 Apr 2015 23:59:16 +0900 Subject: [PATCH 0042/2522] fix cannot download SNAPSHOT version --- packaging/language/maven_artifact.py | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/packaging/language/maven_artifact.py b/packaging/language/maven_artifact.py index 2aeb158625b..e0859dbf938 100644 --- a/packaging/language/maven_artifact.py +++ b/packaging/language/maven_artifact.py @@ -184,29 +184,12 @@ def find_uri_for_artifact(self, artifact): if artifact.is_snapshot(): path = "/%s/maven-metadata.xml" % (artifact.path()) xml = self._request(self.base + path, "Failed to download maven-metadata.xml", lambda r: etree.parse(r)) - basexpath = "/metadata/versioning/" - p = xml.xpath(basexpath + "/snapshotVersions/snapshotVersion") - if p: - return self._find_matching_artifact(p, artifact) + timestamp = xml.xpath("/metadata/versioning/snapshot/timestamp/text()")[0] + buildNumber = xml.xpath("/metadata/versioning/snapshot/buildNumber/text()")[0] + return self._uri_for_artifact(artifact, artifact.version.replace("SNAPSHOT", timestamp + "-" + buildNumber)) else: return self._uri_for_artifact(artifact) - def _find_matching_artifact(self, elems, artifact): - filtered = filter(lambda e: e.xpath("extension/text() = '%s'" % artifact.extension), elems) - if artifact.classifier: - filtered = filter(lambda e: e.xpath("classifier/text() = '%s'" % artifact.classifier), elems) - - if len(filtered) > 1: - print( - "There was more than one match. Selecting the first one. Try adding a classifier to get a better match.") - elif not len(filtered): - print("There were no matches.") - return None - - elem = filtered[0] - value = elem.xpath("value/text()") - return self._uri_for_artifact(artifact, value[0]) - def _uri_for_artifact(self, artifact, version=None): if artifact.is_snapshot() and not version: raise ValueError("Expected uniqueversion for snapshot artifact " + str(artifact)) @@ -309,7 +292,7 @@ def main(): repository_url = dict(default=None), username = dict(default=None), password = dict(default=None), - state = dict(default="present", choices=["present","absent"]), # TODO - Implement a "latest" state + state = dict(default="present", choices=["present","absent"]), # TODO - Implement a "latest" state dest = dict(default=None), ) ) From 772e92eca9ff56f01a390c38fca5d6357e389188 Mon Sep 17 00:00:00 2001 From: Olaf Kilian Date: Sun, 3 May 2015 17:13:34 +0200 Subject: [PATCH 0043/2522] Add docker_login module - Ansible version of "docker login" CLI command - Persists Docker registry authentification in .dockercfg (only login once - no need to specify credentials over and over again anymore) - Works for all other docker-py based modules (docker, docker_images) as well as the Docker CLI client --- cloud/docker/__init__.py | 0 cloud/docker/docker_login.py | 243 +++++++++++++++++++++++++++++++++++ 2 files changed, 243 insertions(+) create mode 100644 cloud/docker/__init__.py create mode 100644 cloud/docker/docker_login.py diff --git a/cloud/docker/__init__.py b/cloud/docker/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cloud/docker/docker_login.py b/cloud/docker/docker_login.py new file mode 100644 index 00000000000..023cbda73a9 --- /dev/null +++ b/cloud/docker/docker_login.py @@ -0,0 +1,243 @@ +#!/usr/bin/python +# + +# (c) 2015, Olaf Kilian +# +# This file is part of Ansible +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + +###################################################################### + +DOCUMENTATION = ''' +--- +module: docker_login +author: Olaf Kilian +version_added: "1.9" +short_description: Manage Docker registry logins +description: + - Ansible version of the "docker login" CLI command. + - This module allows you to login to a Docker registry without directly pulling an image or performing any other actions. + - It will write your login credentials to your local .dockercfg file that is compatible to the Docker CLI client as well as docker-py and all other Docker related modules that are based on docker-py. +options: + registry: + description: + - URL of the registry, for example: https://index.docker.io/v1/ + required: true + default: null + aliases: [] + username: + description: + - The username for the registry account + required: true + default: null + aliases: [] + password: + description: + - The plaintext password for the registry account + required: true + default: null + aliases: [] + email: + description: + - The email address for the registry account + required: false + default: None + aliases: [] + reauth: + description: + - Whether refresh existing authentication on the Docker server (boolean) + required: false + default: false + aliases: [] + dockercfg_path: + description: + - Use a custom path for the .dockercfg file + required: false + default: ~/.dockercfg + aliases: [] + docker_url: + descriptions: + - Refers to the protocol+hostname+port where the Docker server is hosted + required: false + default: unix://var/run/docker.sock + aliases: [] + timeout: + description: + - The HTTP request timeout in seconds + required: false + default: 600 + aliases: [] + +requirements: [ "docker-py" ] +''' + +EXAMPLES = ''' +Login to a Docker registry without performing any other action. Make sure that the user you are using is either in the docker group which owns the Docker socket or use sudo to perform login actions: + +- name: login to DockerHub remote registry using your account + docker_login: + username: docker + password: rekcod + email: docker@docker.io + +- name: login to private Docker remote registry and force reauthentification + docker_login: + registry: https://your.private.registry.io/v1/ + username: yourself + password: secrets3 + reauth: yes + +- name: login to DockerHub remote registry using a custom dockercfg file location + docker_login: + username: docker + password: rekcod + email: docker@docker.io + dockercfg_path: /tmp/.mydockercfg + +''' + +try: + import os.path + import sys + import json + import base64 + import docker.client + from requests.exceptions import * + from urlparse import urlparse +except ImportError, e: + print "failed=True msg='failed to import python module: %s'" % e + sys.exit(1) + +try: + from docker.errors import APIError as DockerAPIError +except ImportError: + from docker.client import APIError as DockerAPIError + +class DockerLoginManager: + + def __init__(self, module): + + self.module = module + self.registry = self.module.params.get('registry') + self.username = self.module.params.get('username') + self.password = self.module.params.get('password') + self.email = self.module.params.get('email') + self.reauth = self.module.params.get('reauth') + self.dockercfg_path = os.path.expanduser(self.module.params.get('dockercfg_path')) + + docker_url = urlparse(module.params.get('docker_url')) + self.client = docker.Client(base_url=docker_url.geturl(), timeout=module.params.get('timeout')) + + self.changed = False + self.response = False + self.log = list() + + def login(self): + + if self.reauth: + self.log.append("Enforcing reauthentification") + + # Connect to registry and login if not already logged in or reauth is enforced. + try: + self.response = self.client.login( + self.username, + password=self.password, + email=self.email, + registry=self.registry, + reauth=self.reauth, + dockercfg_path=self.dockercfg_path + ) + except Exception as e: + self.module.fail_json(msg="failed to login to the remote registry", error=repr(e)) + + # Get status from registry response. + if self.response.has_key("Status"): + self.log.append(self.response["Status"]) + if self.response["Status"] == "Login Succeeded": + self.changed = True + else: + self.log.append("Already Authentificated") + + # Update the dockercfg if changed but not failed. + if self.has_changed(): + self.update_dockercfg() + + # This is what the underlaying docker-py unfortunately doesn't do (yet). + def update_dockercfg(self): + + # Create dockercfg file if it does not exist. + if not os.path.exists(self.dockercfg_path): + open(self.dockercfg_path, "w") + self.log.append("Created new Docker config file at %s" % self.dockercfg_path) + else: + self.log.append("Updated existing Docker config file at %s" % self.dockercfg_path) + + # Get existing dockercfg into a dict. + try: + docker_config = json.load(open(self.dockercfg_path, "r")) + except ValueError: + docker_config = dict() + if not docker_config.has_key(self.registry): + docker_config[self.registry] = dict() + docker_config[self.registry] = dict( + auth = base64.b64encode(self.username + b':' + self.password), + email = self.email + ) + + # Write updated dockercfg to dockercfg file. + try: + json.dump(docker_config, open(self.dockercfg_path, "w"), indent=4, sort_keys=True) + except Exception as e: + self.module.fail_json(msg="failed to write auth details to file", error=repr(e)) + + # Compatible to docker-py auth.decode_docker_auth() + def encode_docker_auth(self, auth): + s = base64.b64decode(auth) + login, pwd = s.split(b':', 1) + return login.decode('ascii'), pwd.decode('ascii') + + def get_msg(self): + return ". ".join(self.log) + + def has_changed(self): + return self.changed + +def main(): + + module = AnsibleModule( + argument_spec = dict( + registry = dict(required=True, default=None), + username = dict(required=True, default=None), + password = dict(required=True, default=None), + email = dict(required=False, default=None), + reauth = dict(required=False, default=False, type='bool'), + dockercfg_path = dict(required=False, default='~/.dockercfg'), + docker_url = dict(default='unix://var/run/docker.sock'), + timeout = dict(default=10, type='int') + ) + ) + + try: + manager = DockerLoginManager(module) + manager.login() + module.exit_json(changed=manager.has_changed(), msg=manager.get_msg(), registry=manager.registry) + + except Exception as e: + module.fail_json(msg="Module execution has failed due to an unexpected error", error=repr(e)) + +# import module snippets +from ansible.module_utils.basic import * + +main() From a79772deb126e262db82a2ee6f172f39d2b71e95 Mon Sep 17 00:00:00 2001 From: Quentin Stafford-Fraser Date: Sun, 3 May 2015 20:58:21 +0100 Subject: [PATCH 0044/2522] Add webfaction modules --- cloud/webfaction/__init__.py | 0 cloud/webfaction/webfaction_app.py | 153 ++++++++++++++++++++ cloud/webfaction/webfaction_db.py | 147 +++++++++++++++++++ cloud/webfaction/webfaction_domain.py | 134 ++++++++++++++++++ cloud/webfaction/webfaction_mailbox.py | 112 +++++++++++++++ cloud/webfaction/webfaction_site.py | 189 +++++++++++++++++++++++++ 6 files changed, 735 insertions(+) create mode 100644 cloud/webfaction/__init__.py create mode 100644 cloud/webfaction/webfaction_app.py create mode 100644 cloud/webfaction/webfaction_db.py create mode 100644 cloud/webfaction/webfaction_domain.py create mode 100644 cloud/webfaction/webfaction_mailbox.py create mode 100644 cloud/webfaction/webfaction_site.py diff --git a/cloud/webfaction/__init__.py b/cloud/webfaction/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cloud/webfaction/webfaction_app.py b/cloud/webfaction/webfaction_app.py new file mode 100644 index 00000000000..b1ddcd5a9c0 --- /dev/null +++ b/cloud/webfaction/webfaction_app.py @@ -0,0 +1,153 @@ +#! /usr/bin/python +# Create a Webfaction application using Ansible and the Webfaction API +# +# Valid application types can be found by looking here: +# http://docs.webfaction.com/xmlrpc-api/apps.html#application-types +# +# Quentin Stafford-Fraser 2015 + +DOCUMENTATION = ''' +--- +module: webfaction_app +short_description: Add or remove applications on a Webfaction host +description: + - Add or remove applications on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. +author: Quentin Stafford-Fraser +version_added: 1.99 +notes: + - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." + - See `the webfaction API `_ for more info. + +options: + name: + description: + - The name of the application + required: true + default: null + + state: + description: + - Whether the application should exist + required: false + choices: ['present', 'absent'] + default: "present" + + type: + description: + - The type of application to create. See the Webfaction docs at http://docs.webfaction.com/xmlrpc-api/apps.html for a list. + required: true + + autostart: + description: + - Whether the app should restart with an autostart.cgi script + required: false + default: "no" + + extra_info: + description: + - Any extra parameters required by the app + required: false + default: null + + open_port: + required: false + default: false + + login_name: + description: + - The webfaction account to use + required: true + + login_password: + description: + - The webfaction password to use + required: true +''' + +import xmlrpclib +from ansible.module_utils.basic import * + +webfaction = xmlrpclib.ServerProxy('https://api.webfaction.com/') + +def main(): + + module = AnsibleModule( + argument_spec = dict( + name = dict(required=True, default=None), + state = dict(required=False, default='present'), + type = dict(required=True), + autostart = dict(required=False, choices=BOOLEANS, default='false'), + extra_info = dict(required=False, default=""), + port_open = dict(required=False, default="false"), + login_name = dict(required=True), + login_password = dict(required=True), + ), + supports_check_mode=True + ) + app_name = module.params['name'] + app_type = module.params['type'] + app_state = module.params['state'] + + session_id, account = webfaction.login( + module.params['login_name'], + module.params['login_password'] + ) + + app_list = webfaction.list_apps(session_id) + app_map = dict([(i['name'], i) for i in app_list]) + existing_app = app_map.get(app_name) + + result = {} + + # Here's where the real stuff happens + + if app_state == 'present': + + # Does an app with this name already exist? + if existing_app: + if existing_app['type'] != app_type: + module.fail_json(msg="App already exists with different type. Please fix by hand.") + + # If it exists with the right type, we don't change it + # Should check other parameters. + module.exit_json( + changed = False, + ) + + if not module.check_mode: + # If this isn't a dry run, create the app + result.update( + webfaction.create_app( + session_id, app_name, app_type, + module.boolean(module.params['autostart']), + module.params['extra_info'], + module.boolean(module.params['port_open']) + ) + ) + + elif app_state == 'absent': + + # If the app's already not there, nothing changed. + if not existing_app: + module.exit_json( + changed = False, + ) + + if not module.check_mode: + # If this isn't a dry run, delete the app + result.update( + webfaction.delete_app(session_id, app_name) + ) + + else: + module.fail_json(msg="Unknown state specified: {}".format(app_state)) + + + module.exit_json( + changed = True, + result = result + ) + +# The conventional ending +main() + diff --git a/cloud/webfaction/webfaction_db.py b/cloud/webfaction/webfaction_db.py new file mode 100644 index 00000000000..7205a084ef2 --- /dev/null +++ b/cloud/webfaction/webfaction_db.py @@ -0,0 +1,147 @@ +#! /usr/bin/python +# Create webfaction database using Ansible and the Webfaction API +# +# Quentin Stafford-Fraser 2015 + +DOCUMENTATION = ''' +--- +module: webfaction_db +short_description: Add or remove a database on Webfaction +description: + - Add or remove a database on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. +author: Quentin Stafford-Fraser +version_added: 1.99 +notes: + - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." + - See `the webfaction API `_ for more info. +options: + + name: + description: + - The name of the database + required: true + default: null + + state: + description: + - Whether the database should exist + required: false + choices: ['present', 'absent'] + default: "present" + + type: + description: + - The type of database to create. + required: true + choices: ['mysql', 'postgresql'] + + login_name: + description: + - The webfaction account to use + required: true + + login_password: + description: + - The webfaction password to use + required: true +''' + +EXAMPLES = ''' + # This will also create a default DB user with the same + # name as the database, and the specified password. + + - name: Create a database + webfaction_db: + name: "{{webfaction_user}}_db1" + password: mytestsql + type: mysql + login_name: "{{webfaction_user}}" + login_password: "{{webfaction_passwd}}" +''' + +import socket +import xmlrpclib +from ansible.module_utils.basic import * + +webfaction = xmlrpclib.ServerProxy('https://api.webfaction.com/') + +def main(): + + module = AnsibleModule( + argument_spec = dict( + name = dict(required=True, default=None), + state = dict(required=False, default='present'), + # You can specify an IP address or hostname. + type = dict(required=True, default=None), + password = dict(required=False, default=None), + login_name = dict(required=True), + login_password = dict(required=True), + ), + supports_check_mode=True + ) + db_name = module.params['name'] + db_state = module.params['state'] + db_type = module.params['type'] + db_passwd = module.params['password'] + + session_id, account = webfaction.login( + module.params['login_name'], + module.params['login_password'] + ) + + db_list = webfaction.list_dbs(session_id) + db_map = dict([(i['name'], i) for i in db_list]) + existing_db = db_map.get(db_name) + + result = {} + + # Here's where the real stuff happens + + if db_state == 'present': + + # Does an app with this name already exist? + if existing_db: + # Yes, but of a different type - fail + if existing_db['db_type'] != db_type: + module.fail_json(msg="Database already exists but is a different type. Please fix by hand.") + + # If it exists with the right type, we don't change anything. + module.exit_json( + changed = False, + ) + + + if not module.check_mode: + # If this isn't a dry run, create the app + # print positional_args + result.update( + webfaction.create_db( + session_id, db_name, db_type, db_passwd + ) + ) + + elif db_state == 'absent': + + # If the app's already not there, nothing changed. + if not existing_db: + module.exit_json( + changed = False, + ) + + if not module.check_mode: + # If this isn't a dry run, delete the app + result.update( + webfaction.delete_db(session_id, db_name, db_type) + ) + + else: + module.fail_json(msg="Unknown state specified: {}".format(db_state)) + + module.exit_json( + changed = True, + result = result + ) + +# The conventional ending +main() + diff --git a/cloud/webfaction/webfaction_domain.py b/cloud/webfaction/webfaction_domain.py new file mode 100644 index 00000000000..2f3c8542754 --- /dev/null +++ b/cloud/webfaction/webfaction_domain.py @@ -0,0 +1,134 @@ +#! /usr/bin/python +# Create Webfaction domains and subdomains using Ansible and the Webfaction API +# +# Quentin Stafford-Fraser 2015 + +DOCUMENTATION = ''' +--- +module: webfaction_domain +short_description: Add or remove domains and subdomains on Webfaction +description: + - Add or remove domains or subdomains on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. +author: Quentin Stafford-Fraser +version_added: 1.99 +notes: + - If you are I(deleting) domains by using C(state=absent), then note that if you specify subdomains, just those particular subdomains will be deleted. If you don't specify subdomains, the domain will be deleted. + - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." + - See `the webfaction API `_ for more info. + +options: + + name: + description: + - The name of the domain + required: true + default: null + + state: + description: + - Whether the domain should exist + required: false + choices: ['present', 'absent'] + default: "present" + + subdomains: + description: + - Any subdomains to create. + required: false + default: null + + login_name: + description: + - The webfaction account to use + required: true + + login_password: + description: + - The webfaction password to use + required: true +''' + +import socket +import xmlrpclib +from ansible.module_utils.basic import * + +webfaction = xmlrpclib.ServerProxy('https://api.webfaction.com/') + +def main(): + + module = AnsibleModule( + argument_spec = dict( + name = dict(required=True, default=None), + state = dict(required=False, default='present'), + subdomains = dict(required=False, default=[]), + login_name = dict(required=True), + login_password = dict(required=True), + ), + supports_check_mode=True + ) + domain_name = module.params['name'] + domain_state = module.params['state'] + domain_subdomains = module.params['subdomains'] + + session_id, account = webfaction.login( + module.params['login_name'], + module.params['login_password'] + ) + + domain_list = webfaction.list_domains(session_id) + domain_map = dict([(i['domain'], i) for i in domain_list]) + existing_domain = domain_map.get(domain_name) + + result = {} + + # Here's where the real stuff happens + + if domain_state == 'present': + + # Does an app with this name already exist? + if existing_domain: + + if set(existing_domain['subdomains']) >= set(domain_subdomains): + # If it exists with the right subdomains, we don't change anything. + module.exit_json( + changed = False, + ) + + positional_args = [session_id, domain_name] + domain_subdomains + + if not module.check_mode: + # If this isn't a dry run, create the app + # print positional_args + result.update( + webfaction.create_domain( + *positional_args + ) + ) + + elif domain_state == 'absent': + + # If the app's already not there, nothing changed. + if not existing_domain: + module.exit_json( + changed = False, + ) + + positional_args = [session_id, domain_name] + domain_subdomains + + if not module.check_mode: + # If this isn't a dry run, delete the app + result.update( + webfaction.delete_domain(*positional_args) + ) + + else: + module.fail_json(msg="Unknown state specified: {}".format(domain_state)) + + module.exit_json( + changed = True, + result = result + ) + +# The conventional ending +main() + diff --git a/cloud/webfaction/webfaction_mailbox.py b/cloud/webfaction/webfaction_mailbox.py new file mode 100644 index 00000000000..3ac848d6a94 --- /dev/null +++ b/cloud/webfaction/webfaction_mailbox.py @@ -0,0 +1,112 @@ +#! /usr/bin/python +# Create webfaction mailbox using Ansible and the Webfaction API +# +# Quentin Stafford-Fraser and Andy Baker 2015 + +DOCUMENTATION = ''' +--- +module: webfaction_mailbox +short_description: Add or remove mailboxes on Webfaction +description: + - Add or remove mailboxes on a Webfaction account. Further documentation at http://github.com/quentinsf/ansible-webfaction. +author: Quentin Stafford-Fraser +version_added: 1.99 +notes: + - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." + - See `the webfaction API `_ for more info. +options: + + mailbox_name: + description: + - The name of the mailbox + required: true + default: null + + mailbox_password: + description: + - The password for the mailbox + required: true + default: null + + state: + description: + - Whether the mailbox should exist + required: false + choices: ['present', 'absent'] + default: "present" + + login_name: + description: + - The webfaction account to use + required: true + + login_password: + description: + - The webfaction password to use + required: true +''' + +import socket +import xmlrpclib +from ansible.module_utils.basic import * + +webfaction = xmlrpclib.ServerProxy('https://api.webfaction.com/') + +def main(): + + module = AnsibleModule( + argument_spec=dict( + mailbox_name=dict(required=True, default=None), + mailbox_password=dict(required=True), + state=dict(required=False, default='present'), + login_name=dict(required=True), + login_password=dict(required=True), + ), + supports_check_mode=True + ) + + mailbox_name = module.params['mailbox_name'] + site_state = module.params['state'] + + session_id, account = webfaction.login( + module.params['login_name'], + module.params['login_password'] + ) + + mailbox_list = webfaction.list_mailboxes(session_id) + existing_mailbox = mailbox_name in mailbox_list + + result = {} + + # Here's where the real stuff happens + + if site_state == 'present': + + # Does a mailbox with this name already exist? + if existing_mailbox: + module.exit_json(changed=False,) + + positional_args = [session_id, mailbox_name] + + if not module.check_mode: + # If this isn't a dry run, create the mailbox + result.update(webfaction.create_mailbox(*positional_args)) + + elif site_state == 'absent': + + # If the mailbox is already not there, nothing changed. + if not existing_mailbox: + module.exit_json(changed=False) + + if not module.check_mode: + # If this isn't a dry run, delete the mailbox + result.update(webfaction.delete_mailbox(session_id, mailbox_name)) + + else: + module.fail_json(msg="Unknown state specified: {}".format(site_state)) + + module.exit_json(changed=True, result=result) + +# The conventional ending +main() + diff --git a/cloud/webfaction/webfaction_site.py b/cloud/webfaction/webfaction_site.py new file mode 100644 index 00000000000..5db89355966 --- /dev/null +++ b/cloud/webfaction/webfaction_site.py @@ -0,0 +1,189 @@ +#! /usr/bin/python +# Create Webfaction website using Ansible and the Webfaction API +# +# Quentin Stafford-Fraser 2015 + +DOCUMENTATION = ''' +--- +module: webfaction_site +short_description: Add or remove a website on a Webfaction host +description: + - Add or remove a website on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. +author: Quentin Stafford-Fraser +version_added: 1.99 +notes: + - Sadly, you I(do) need to know your webfaction hostname for the C(host) parameter. But at least, unlike the API, you don't need to know the IP address - you can use a DNS name. + - If a site of the same name exists in the account but on a different host, the operation will exit. + - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." + - See `the webfaction API `_ for more info. + +options: + + name: + description: + - The name of the website + required: true + default: null + + state: + description: + - Whether the website should exist + required: false + choices: ['present', 'absent'] + default: "present" + + host: + description: + - The webfaction host on which the site should be created. + required: true + + https: + description: + - Whether or not to use HTTPS + required: false + choices: BOOLEANS + default: 'false' + + site_apps: + description: + - A mapping of URLs to apps + required: false + + subdomains: + description: + - A list of subdomains associated with this site. + required: false + default: null + + login_name: + description: + - The webfaction account to use + required: true + + login_password: + description: + - The webfaction password to use + required: true +''' + +EXAMPLES = ''' + - name: create website + webfaction_site: + name: testsite1 + state: present + host: myhost.webfaction.com + subdomains: + - 'testsite1.my_domain.org' + site_apps: + - ['testapp1', '/'] + https: no + login_name: "{{webfaction_user}}" + login_password: "{{webfaction_passwd}}" +''' + +import socket +import xmlrpclib +from ansible.module_utils.basic import * + +webfaction = xmlrpclib.ServerProxy('https://api.webfaction.com/') + +def main(): + + module = AnsibleModule( + argument_spec = dict( + name = dict(required=True, default=None), + state = dict(required=False, default='present'), + # You can specify an IP address or hostname. + host = dict(required=True, default=None), + https = dict(required=False, choices=BOOLEANS, default='false'), + subdomains = dict(required=False, default=[]), + site_apps = dict(required=False, default=[]), + login_name = dict(required=True), + login_password = dict(required=True), + ), + supports_check_mode=True + ) + site_name = module.params['name'] + site_state = module.params['state'] + site_host = module.params['host'] + site_ip = socket.gethostbyname(site_host) + + session_id, account = webfaction.login( + module.params['login_name'], + module.params['login_password'] + ) + + site_list = webfaction.list_websites(session_id) + site_map = dict([(i['name'], i) for i in site_list]) + existing_site = site_map.get(site_name) + + result = {} + + # Here's where the real stuff happens + + if site_state == 'present': + + # Does a site with this name already exist? + if existing_site: + + # If yes, but it's on a different IP address, then fail. + # If we wanted to allow relocation, we could add a 'relocate=true' option + # which would get the existing IP address, delete the site there, and create it + # at the new address. A bit dangerous, perhaps, so for now we'll require manual + # deletion if it's on another host. + + if existing_site['ip'] != site_ip: + module.fail_json(msg="Website already exists with a different IP address. Please fix by hand.") + + # If it's on this host and the key parameters are the same, nothing needs to be done. + + if (existing_site['https'] == module.boolean(module.params['https'])) and \ + (set(existing_site['subdomains']) == set(module.params['subdomains'])) and \ + (dict(existing_site['website_apps']) == dict(module.params['site_apps'])): + module.exit_json( + changed = False + ) + + positional_args = [ + session_id, site_name, site_ip, + module.boolean(module.params['https']), + module.params['subdomains'], + ] + for a in module.params['site_apps']: + positional_args.append( (a[0], a[1]) ) + + if not module.check_mode: + # If this isn't a dry run, create or modify the site + result.update( + webfaction.create_website( + *positional_args + ) if not existing_site else webfaction.update_website ( + *positional_args + ) + ) + + elif site_state == 'absent': + + # If the site's already not there, nothing changed. + if not existing_site: + module.exit_json( + changed = False, + ) + + if not module.check_mode: + # If this isn't a dry run, delete the site + result.update( + webfaction.delete_website(session_id, site_name, site_ip) + ) + + else: + module.fail_json(msg="Unknown state specified: {}".format(site_state)) + + module.exit_json( + changed = True, + result = result + ) + +# The conventional ending +main() + From d1d65fe544c6a26263778643da07d3fd77bb482e Mon Sep 17 00:00:00 2001 From: Quentin Stafford-Fraser Date: Sun, 3 May 2015 23:48:51 +0100 Subject: [PATCH 0045/2522] Tidying of webfaction modules --- cloud/webfaction/webfaction_app.py | 12 +++++------- cloud/webfaction/webfaction_db.py | 10 ++++------ cloud/webfaction/webfaction_domain.py | 8 +++----- cloud/webfaction/webfaction_mailbox.py | 9 ++++----- cloud/webfaction/webfaction_site.py | 14 +++++++------- 5 files changed, 23 insertions(+), 30 deletions(-) diff --git a/cloud/webfaction/webfaction_app.py b/cloud/webfaction/webfaction_app.py index b1ddcd5a9c0..08a0205eb87 100644 --- a/cloud/webfaction/webfaction_app.py +++ b/cloud/webfaction/webfaction_app.py @@ -13,7 +13,7 @@ description: - Add or remove applications on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. author: Quentin Stafford-Fraser -version_added: 1.99 +version_added: 2.0 notes: - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." - See `the webfaction API `_ for more info. @@ -23,7 +23,6 @@ description: - The name of the application required: true - default: null state: description: @@ -65,7 +64,6 @@ ''' import xmlrpclib -from ansible.module_utils.basic import * webfaction = xmlrpclib.ServerProxy('https://api.webfaction.com/') @@ -73,12 +71,12 @@ def main(): module = AnsibleModule( argument_spec = dict( - name = dict(required=True, default=None), + name = dict(required=True), state = dict(required=False, default='present'), type = dict(required=True), - autostart = dict(required=False, choices=BOOLEANS, default='false'), + autostart = dict(required=False, choices=BOOLEANS, default=False), extra_info = dict(required=False, default=""), - port_open = dict(required=False, default="false"), + port_open = dict(required=False, choices=BOOLEANS, default=False), login_name = dict(required=True), login_password = dict(required=True), ), @@ -148,6 +146,6 @@ def main(): result = result ) -# The conventional ending +from ansible.module_utils.basic import * main() diff --git a/cloud/webfaction/webfaction_db.py b/cloud/webfaction/webfaction_db.py index 7205a084ef2..479540abc5c 100644 --- a/cloud/webfaction/webfaction_db.py +++ b/cloud/webfaction/webfaction_db.py @@ -10,7 +10,7 @@ description: - Add or remove a database on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. author: Quentin Stafford-Fraser -version_added: 1.99 +version_added: 2.0 notes: - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." - See `the webfaction API `_ for more info. @@ -20,7 +20,6 @@ description: - The name of the database required: true - default: null state: description: @@ -61,7 +60,6 @@ import socket import xmlrpclib -from ansible.module_utils.basic import * webfaction = xmlrpclib.ServerProxy('https://api.webfaction.com/') @@ -69,10 +67,10 @@ def main(): module = AnsibleModule( argument_spec = dict( - name = dict(required=True, default=None), + name = dict(required=True), state = dict(required=False, default='present'), # You can specify an IP address or hostname. - type = dict(required=True, default=None), + type = dict(required=True), password = dict(required=False, default=None), login_name = dict(required=True), login_password = dict(required=True), @@ -142,6 +140,6 @@ def main(): result = result ) -# The conventional ending +from ansible.module_utils.basic import * main() diff --git a/cloud/webfaction/webfaction_domain.py b/cloud/webfaction/webfaction_domain.py index 2f3c8542754..a9e2b7dd9bb 100644 --- a/cloud/webfaction/webfaction_domain.py +++ b/cloud/webfaction/webfaction_domain.py @@ -10,7 +10,7 @@ description: - Add or remove domains or subdomains on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. author: Quentin Stafford-Fraser -version_added: 1.99 +version_added: 2.0 notes: - If you are I(deleting) domains by using C(state=absent), then note that if you specify subdomains, just those particular subdomains will be deleted. If you don't specify subdomains, the domain will be deleted. - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." @@ -22,7 +22,6 @@ description: - The name of the domain required: true - default: null state: description: @@ -50,7 +49,6 @@ import socket import xmlrpclib -from ansible.module_utils.basic import * webfaction = xmlrpclib.ServerProxy('https://api.webfaction.com/') @@ -58,7 +56,7 @@ def main(): module = AnsibleModule( argument_spec = dict( - name = dict(required=True, default=None), + name = dict(required=True), state = dict(required=False, default='present'), subdomains = dict(required=False, default=[]), login_name = dict(required=True), @@ -129,6 +127,6 @@ def main(): result = result ) -# The conventional ending +from ansible.module_utils.basic import * main() diff --git a/cloud/webfaction/webfaction_mailbox.py b/cloud/webfaction/webfaction_mailbox.py index 3ac848d6a94..1ba571a1dd1 100644 --- a/cloud/webfaction/webfaction_mailbox.py +++ b/cloud/webfaction/webfaction_mailbox.py @@ -10,7 +10,7 @@ description: - Add or remove mailboxes on a Webfaction account. Further documentation at http://github.com/quentinsf/ansible-webfaction. author: Quentin Stafford-Fraser -version_added: 1.99 +version_added: 2.0 notes: - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." - See `the webfaction API `_ for more info. @@ -20,7 +20,6 @@ description: - The name of the mailbox required: true - default: null mailbox_password: description: @@ -48,7 +47,6 @@ import socket import xmlrpclib -from ansible.module_utils.basic import * webfaction = xmlrpclib.ServerProxy('https://api.webfaction.com/') @@ -56,7 +54,7 @@ def main(): module = AnsibleModule( argument_spec=dict( - mailbox_name=dict(required=True, default=None), + mailbox_name=dict(required=True), mailbox_password=dict(required=True), state=dict(required=False, default='present'), login_name=dict(required=True), @@ -107,6 +105,7 @@ def main(): module.exit_json(changed=True, result=result) -# The conventional ending + +from ansible.module_utils.basic import * main() diff --git a/cloud/webfaction/webfaction_site.py b/cloud/webfaction/webfaction_site.py index 5db89355966..575e6eec996 100644 --- a/cloud/webfaction/webfaction_site.py +++ b/cloud/webfaction/webfaction_site.py @@ -10,7 +10,7 @@ description: - Add or remove a website on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. author: Quentin Stafford-Fraser -version_added: 1.99 +version_added: 2.0 notes: - Sadly, you I(do) need to know your webfaction hostname for the C(host) parameter. But at least, unlike the API, you don't need to know the IP address - you can use a DNS name. - If a site of the same name exists in the account but on a different host, the operation will exit. @@ -23,7 +23,6 @@ description: - The name of the website required: true - default: null state: description: @@ -83,7 +82,6 @@ import socket import xmlrpclib -from ansible.module_utils.basic import * webfaction = xmlrpclib.ServerProxy('https://api.webfaction.com/') @@ -91,11 +89,11 @@ def main(): module = AnsibleModule( argument_spec = dict( - name = dict(required=True, default=None), + name = dict(required=True), state = dict(required=False, default='present'), # You can specify an IP address or hostname. - host = dict(required=True, default=None), - https = dict(required=False, choices=BOOLEANS, default='false'), + host = dict(required=True), + https = dict(required=False, choices=BOOLEANS, default=False), subdomains = dict(required=False, default=[]), site_apps = dict(required=False, default=[]), login_name = dict(required=True), @@ -184,6 +182,8 @@ def main(): result = result ) -# The conventional ending + + +from ansible.module_utils.basic import * main() From e362583abdf4f1ebd3bb44683a9aaf678b2456db Mon Sep 17 00:00:00 2001 From: xiaclo Date: Mon, 4 May 2015 14:08:39 +1000 Subject: [PATCH 0046/2522] Allow NPM to update packages --- packaging/language/npm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/language/npm.py b/packaging/language/npm.py index 8407589116a..6f3767988e1 100644 --- a/packaging/language/npm.py +++ b/packaging/language/npm.py @@ -250,7 +250,7 @@ def main(): outdated = npm.list_outdated() if len(missing) or len(outdated): changed = True - npm.install() + npm.update() else: #absent installed, missing = npm.list() if name in installed: From feb20eeadd62ed683870c033394037fa99437848 Mon Sep 17 00:00:00 2001 From: Olaf Kilian Date: Wed, 6 May 2015 22:28:36 +0200 Subject: [PATCH 0047/2522] Update PR based on review from @resmo --- cloud/docker/docker_login.py | 55 +++++++++++++++++------------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/cloud/docker/docker_login.py b/cloud/docker/docker_login.py index 023cbda73a9..09d9d599432 100644 --- a/cloud/docker/docker_login.py +++ b/cloud/docker/docker_login.py @@ -24,7 +24,7 @@ --- module: docker_login author: Olaf Kilian -version_added: "1.9" +version_added: "2.0" short_description: Manage Docker registry logins description: - Ansible version of the "docker login" CLI command. @@ -35,50 +35,38 @@ description: - URL of the registry, for example: https://index.docker.io/v1/ required: true - default: null - aliases: [] username: description: - The username for the registry account required: true - default: null - aliases: [] password: description: - The plaintext password for the registry account required: true - default: null - aliases: [] email: description: - The email address for the registry account required: false - default: None - aliases: [] reauth: description: - Whether refresh existing authentication on the Docker server (boolean) required: false default: false - aliases: [] dockercfg_path: description: - Use a custom path for the .dockercfg file required: false default: ~/.dockercfg - aliases: [] docker_url: descriptions: - Refers to the protocol+hostname+port where the Docker server is hosted required: false default: unix://var/run/docker.sock - aliases: [] timeout: description: - The HTTP request timeout in seconds required: false default: 600 - aliases: [] requirements: [ "docker-py" ] ''' @@ -108,22 +96,24 @@ ''' +import os.path +import sys +import json +import base64 +from urlparse import urlparse + try: - import os.path - import sys - import json - import base64 import docker.client - from requests.exceptions import * - from urlparse import urlparse + from docker.errors import APIError as DockerAPIError + has_lib_docker = True except ImportError, e: - print "failed=True msg='failed to import python module: %s'" % e - sys.exit(1) + has_lib_docker = False try: - from docker.errors import APIError as DockerAPIError -except ImportError: - from docker.client import APIError as DockerAPIError + from requests.exceptions import * + has_lib_requests_execeptions = True +except ImportError, e: + has_lib_requests_execeptions = False class DockerLoginManager: @@ -171,7 +161,7 @@ def login(self): self.log.append("Already Authentificated") # Update the dockercfg if changed but not failed. - if self.has_changed(): + if self.has_changed() and not self.module.check_mode: self.update_dockercfg() # This is what the underlaying docker-py unfortunately doesn't do (yet). @@ -218,17 +208,24 @@ def main(): module = AnsibleModule( argument_spec = dict( - registry = dict(required=True, default=None), - username = dict(required=True, default=None), - password = dict(required=True, default=None), + registry = dict(required=True), + username = dict(required=True), + password = dict(required=True), email = dict(required=False, default=None), reauth = dict(required=False, default=False, type='bool'), dockercfg_path = dict(required=False, default='~/.dockercfg'), docker_url = dict(default='unix://var/run/docker.sock'), timeout = dict(default=10, type='int') - ) + ), + supports_check_mode=True ) + if not has_lib_docker: + module.fail_json(msg="python library docker-py required: pip install docker-py==1.1.0") + + if not has_lib_requests_execeptions: + module.fail_json(msg="python library requests required: pip install requests") + try: manager = DockerLoginManager(module) manager.login() From 7a2a75f6c0474edfdf9a74f5b2f6072d5dee6fd5 Mon Sep 17 00:00:00 2001 From: Olaf Kilian Date: Wed, 6 May 2015 22:33:31 +0200 Subject: [PATCH 0048/2522] Remove registry from exit_json because misleading docker-py is not returning the name of the registry if already logged in. It can differ from the registry specified by the user, which was return as registry. --- cloud/docker/docker_login.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/docker/docker_login.py b/cloud/docker/docker_login.py index 09d9d599432..db8fa906320 100644 --- a/cloud/docker/docker_login.py +++ b/cloud/docker/docker_login.py @@ -229,7 +229,7 @@ def main(): try: manager = DockerLoginManager(module) manager.login() - module.exit_json(changed=manager.has_changed(), msg=manager.get_msg(), registry=manager.registry) + module.exit_json(changed=manager.has_changed(), msg=manager.get_msg()) except Exception as e: module.fail_json(msg="Module execution has failed due to an unexpected error", error=repr(e)) From f5e7ce00e7ab5b5331d43fb3d81649f2124efbf4 Mon Sep 17 00:00:00 2001 From: Olaf Kilian Date: Wed, 6 May 2015 22:43:28 +0200 Subject: [PATCH 0049/2522] Extract only the hostname part from self.registry This is needed for update_dockercfg() to register only the host part of a specified registry URL in the .dockercfg. --- cloud/docker/docker_login.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cloud/docker/docker_login.py b/cloud/docker/docker_login.py index db8fa906320..be5a46977a7 100644 --- a/cloud/docker/docker_login.py +++ b/cloud/docker/docker_login.py @@ -139,6 +139,10 @@ def login(self): if self.reauth: self.log.append("Enforcing reauthentification") + # Extract hostname part from self.registry if url was specified. + registry_url = urlparse(self.registry) + self.registry = registry_url.netloc or registry_url.path + # Connect to registry and login if not already logged in or reauth is enforced. try: self.response = self.client.login( From 3d3efa3614bd69286d538914f72b1f60b066f07a Mon Sep 17 00:00:00 2001 From: Olaf Kilian Date: Thu, 7 May 2015 09:15:04 +0200 Subject: [PATCH 0050/2522] Removed unused import of sys module --- cloud/docker/docker_login.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cloud/docker/docker_login.py b/cloud/docker/docker_login.py index be5a46977a7..a6f119168bc 100644 --- a/cloud/docker/docker_login.py +++ b/cloud/docker/docker_login.py @@ -97,7 +97,6 @@ ''' import os.path -import sys import json import base64 from urlparse import urlparse From 30fa6e3ea4f5177d6d8d3732d26452792264777e Mon Sep 17 00:00:00 2001 From: Olaf Kilian Date: Thu, 7 May 2015 09:35:40 +0200 Subject: [PATCH 0051/2522] Added default email address --- cloud/docker/docker_login.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cloud/docker/docker_login.py b/cloud/docker/docker_login.py index a6f119168bc..b515b414c5a 100644 --- a/cloud/docker/docker_login.py +++ b/cloud/docker/docker_login.py @@ -47,6 +47,7 @@ description: - The email address for the registry account required: false + default: anonymous@localhost.local reauth: description: - Whether refresh existing authentication on the Docker server (boolean) @@ -214,7 +215,7 @@ def main(): registry = dict(required=True), username = dict(required=True), password = dict(required=True), - email = dict(required=False, default=None), + email = dict(required=False, default='anonymous@localhost.local'), reauth = dict(required=False, default=False, type='bool'), dockercfg_path = dict(required=False, default='~/.dockercfg'), docker_url = dict(default='unix://var/run/docker.sock'), From 3bcb24e6569a446c489f6058239db42adbf73a24 Mon Sep 17 00:00:00 2001 From: Olaf Kilian Date: Thu, 7 May 2015 09:36:32 +0200 Subject: [PATCH 0052/2522] Added more meaningful fail messages on Docker API --- cloud/docker/docker_login.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cloud/docker/docker_login.py b/cloud/docker/docker_login.py index b515b414c5a..1292fe38909 100644 --- a/cloud/docker/docker_login.py +++ b/cloud/docker/docker_login.py @@ -153,6 +153,8 @@ def login(self): reauth=self.reauth, dockercfg_path=self.dockercfg_path ) + except DockerAPIError as e: + self.module.fail_json(msg="Docker API Error: %s" % e.explanation) except Exception as e: self.module.fail_json(msg="failed to login to the remote registry", error=repr(e)) From a0ef5e4a5973f850fdcc019896ede93fe8595675 Mon Sep 17 00:00:00 2001 From: Quentin Stafford-Fraser Date: Sun, 10 May 2015 20:40:50 +0100 Subject: [PATCH 0053/2522] Documentation version_added numbers are strings. --- cloud/webfaction/webfaction_app.py | 2 +- cloud/webfaction/webfaction_db.py | 2 +- cloud/webfaction/webfaction_domain.py | 2 +- cloud/webfaction/webfaction_mailbox.py | 2 +- cloud/webfaction/webfaction_site.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cloud/webfaction/webfaction_app.py b/cloud/webfaction/webfaction_app.py index 08a0205eb87..dec5f8e5d5e 100644 --- a/cloud/webfaction/webfaction_app.py +++ b/cloud/webfaction/webfaction_app.py @@ -13,7 +13,7 @@ description: - Add or remove applications on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. author: Quentin Stafford-Fraser -version_added: 2.0 +version_added: "2.0" notes: - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." - See `the webfaction API `_ for more info. diff --git a/cloud/webfaction/webfaction_db.py b/cloud/webfaction/webfaction_db.py index 479540abc5c..fc522439591 100644 --- a/cloud/webfaction/webfaction_db.py +++ b/cloud/webfaction/webfaction_db.py @@ -10,7 +10,7 @@ description: - Add or remove a database on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. author: Quentin Stafford-Fraser -version_added: 2.0 +version_added: "2.0" notes: - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." - See `the webfaction API `_ for more info. diff --git a/cloud/webfaction/webfaction_domain.py b/cloud/webfaction/webfaction_domain.py index a9e2b7dd9bb..31339014e6c 100644 --- a/cloud/webfaction/webfaction_domain.py +++ b/cloud/webfaction/webfaction_domain.py @@ -10,7 +10,7 @@ description: - Add or remove domains or subdomains on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. author: Quentin Stafford-Fraser -version_added: 2.0 +version_added: "2.0" notes: - If you are I(deleting) domains by using C(state=absent), then note that if you specify subdomains, just those particular subdomains will be deleted. If you don't specify subdomains, the domain will be deleted. - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." diff --git a/cloud/webfaction/webfaction_mailbox.py b/cloud/webfaction/webfaction_mailbox.py index 1ba571a1dd1..5eb82df3eaa 100644 --- a/cloud/webfaction/webfaction_mailbox.py +++ b/cloud/webfaction/webfaction_mailbox.py @@ -10,7 +10,7 @@ description: - Add or remove mailboxes on a Webfaction account. Further documentation at http://github.com/quentinsf/ansible-webfaction. author: Quentin Stafford-Fraser -version_added: 2.0 +version_added: "2.0" notes: - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." - See `the webfaction API `_ for more info. diff --git a/cloud/webfaction/webfaction_site.py b/cloud/webfaction/webfaction_site.py index 575e6eec996..c981a21fc2b 100644 --- a/cloud/webfaction/webfaction_site.py +++ b/cloud/webfaction/webfaction_site.py @@ -10,7 +10,7 @@ description: - Add or remove a website on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. author: Quentin Stafford-Fraser -version_added: 2.0 +version_added: "2.0" notes: - Sadly, you I(do) need to know your webfaction hostname for the C(host) parameter. But at least, unlike the API, you don't need to know the IP address - you can use a DNS name. - If a site of the same name exists in the account but on a different host, the operation will exit. From de28b84bf79a3b36e95aa4fabf6b080736bceee7 Mon Sep 17 00:00:00 2001 From: Quentin Stafford-Fraser Date: Sun, 10 May 2015 20:47:31 +0100 Subject: [PATCH 0054/2522] Available choices for 'state' explicitly listed. --- cloud/webfaction/webfaction_app.py | 2 +- cloud/webfaction/webfaction_db.py | 2 +- cloud/webfaction/webfaction_domain.py | 2 +- cloud/webfaction/webfaction_mailbox.py | 2 +- cloud/webfaction/webfaction_site.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cloud/webfaction/webfaction_app.py b/cloud/webfaction/webfaction_app.py index dec5f8e5d5e..05b31f55a4a 100644 --- a/cloud/webfaction/webfaction_app.py +++ b/cloud/webfaction/webfaction_app.py @@ -72,7 +72,7 @@ def main(): module = AnsibleModule( argument_spec = dict( name = dict(required=True), - state = dict(required=False, default='present'), + state = dict(required=False, choices=['present', 'absent'], default='present'), type = dict(required=True), autostart = dict(required=False, choices=BOOLEANS, default=False), extra_info = dict(required=False, default=""), diff --git a/cloud/webfaction/webfaction_db.py b/cloud/webfaction/webfaction_db.py index fc522439591..784477c5409 100644 --- a/cloud/webfaction/webfaction_db.py +++ b/cloud/webfaction/webfaction_db.py @@ -68,7 +68,7 @@ def main(): module = AnsibleModule( argument_spec = dict( name = dict(required=True), - state = dict(required=False, default='present'), + state = dict(required=False, choices=['present', 'absent'], default='present'), # You can specify an IP address or hostname. type = dict(required=True), password = dict(required=False, default=None), diff --git a/cloud/webfaction/webfaction_domain.py b/cloud/webfaction/webfaction_domain.py index 31339014e6c..8548c4fba37 100644 --- a/cloud/webfaction/webfaction_domain.py +++ b/cloud/webfaction/webfaction_domain.py @@ -57,7 +57,7 @@ def main(): module = AnsibleModule( argument_spec = dict( name = dict(required=True), - state = dict(required=False, default='present'), + state = dict(required=False, choices=['present', 'absent'], default='present'), subdomains = dict(required=False, default=[]), login_name = dict(required=True), login_password = dict(required=True), diff --git a/cloud/webfaction/webfaction_mailbox.py b/cloud/webfaction/webfaction_mailbox.py index 5eb82df3eaa..fee5700e50e 100644 --- a/cloud/webfaction/webfaction_mailbox.py +++ b/cloud/webfaction/webfaction_mailbox.py @@ -56,7 +56,7 @@ def main(): argument_spec=dict( mailbox_name=dict(required=True), mailbox_password=dict(required=True), - state=dict(required=False, default='present'), + state=dict(required=False, choices=['present', 'absent'], default='present'), login_name=dict(required=True), login_password=dict(required=True), ), diff --git a/cloud/webfaction/webfaction_site.py b/cloud/webfaction/webfaction_site.py index c981a21fc2b..a5be4f5407b 100644 --- a/cloud/webfaction/webfaction_site.py +++ b/cloud/webfaction/webfaction_site.py @@ -90,7 +90,7 @@ def main(): module = AnsibleModule( argument_spec = dict( name = dict(required=True), - state = dict(required=False, default='present'), + state = dict(required=False, choices=['present', 'absent'], default='present'), # You can specify an IP address or hostname. host = dict(required=True), https = dict(required=False, choices=BOOLEANS, default=False), From 3645b61f46ae2e4a436401735bb4a4227516e3b5 Mon Sep 17 00:00:00 2001 From: Quentin Stafford-Fraser Date: Sun, 10 May 2015 22:07:49 +0100 Subject: [PATCH 0055/2522] Add examples. --- cloud/webfaction/webfaction_app.py | 10 ++++++++++ cloud/webfaction/webfaction_domain.py | 20 ++++++++++++++++++++ cloud/webfaction/webfaction_mailbox.py | 10 ++++++++++ 3 files changed, 40 insertions(+) diff --git a/cloud/webfaction/webfaction_app.py b/cloud/webfaction/webfaction_app.py index 05b31f55a4a..20e94a7b5f6 100644 --- a/cloud/webfaction/webfaction_app.py +++ b/cloud/webfaction/webfaction_app.py @@ -63,6 +63,16 @@ required: true ''' +EXAMPLES = ''' + - name: Create a test app + webfaction_app: + name="my_wsgi_app1" + state=present + type=mod_wsgi35-python27 + login_name={{webfaction_user}} + login_password={{webfaction_passwd}} +''' + import xmlrpclib webfaction = xmlrpclib.ServerProxy('https://api.webfaction.com/') diff --git a/cloud/webfaction/webfaction_domain.py b/cloud/webfaction/webfaction_domain.py index 8548c4fba37..c99a0f23f6d 100644 --- a/cloud/webfaction/webfaction_domain.py +++ b/cloud/webfaction/webfaction_domain.py @@ -47,6 +47,26 @@ required: true ''' +EXAMPLES = ''' + - name: Create a test domain + webfaction_domain: + name: mydomain.com + state: present + subdomains: + - www + - blog + login_name: "{{webfaction_user}}" + login_password: "{{webfaction_passwd}}" + + - name: Delete test domain and any subdomains + webfaction_domain: + name: mydomain.com + state: absent + login_name: "{{webfaction_user}}" + login_password: "{{webfaction_passwd}}" + +''' + import socket import xmlrpclib diff --git a/cloud/webfaction/webfaction_mailbox.py b/cloud/webfaction/webfaction_mailbox.py index fee5700e50e..87ca1fd1a26 100644 --- a/cloud/webfaction/webfaction_mailbox.py +++ b/cloud/webfaction/webfaction_mailbox.py @@ -45,6 +45,16 @@ required: true ''' +EXAMPLES = ''' + - name: Create a mailbox + webfaction_mailbox: + mailbox_name="mybox" + mailbox_password="myboxpw" + state=present + login_name={{webfaction_user}} + login_password={{webfaction_passwd}} +''' + import socket import xmlrpclib From 9b32a5d8bf345b7cee3609573c0ebcdba69f8b2f Mon Sep 17 00:00:00 2001 From: Chris Long Date: Tue, 12 May 2015 22:10:53 +1000 Subject: [PATCH 0056/2522] Initial commit of nmcli: NetworkManager module. Currently supports: Create, modify, remove of - team, team-slave, bond, bond-slave, ethernet TODO: vlan, bridge, wireless related connections. --- network/nmcli.py | 1089 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1089 insertions(+) create mode 100644 network/nmcli.py diff --git a/network/nmcli.py b/network/nmcli.py new file mode 100644 index 00000000000..0532058da3b --- /dev/null +++ b/network/nmcli.py @@ -0,0 +1,1089 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Chris Long +# +# This file is a module for Ansible that interacts with Network Manager +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + + +DOCUMENTATION=''' +--- +module: nmcli +author: Chris Long +short_description: Manage Networking +requirements: [ nmcli, dbus ] +description: + - Manage the network devices. Create, modify, and manage, ethernet, teams, bonds, vlans etc. +options: + state: + required: True + default: "present" + choices: [ present, absent ] + description: + - Whether the device should exist or not, taking action if the state is different from what is stated. + enabled: + required: False + default: "yes" + choices: [ "yes", "no" ] + description: + - Whether the service should start on boot. B(At least one of state and enabled are required.) + - Whether the connection profile can be automatically activated ( default: yes) + action: + required: False + default: None + choices: [ add, modify, show, up, down ] + description: + - Set to 'add' if you want to add a connection. + - Set to 'modify' if you want to modify a connection. Modify one or more properties in the connection profile. + - Set to 'delete' if you want to delete a connection. Delete a configured connection. The connection to be deleted is identified by its name 'cfname'. + - Set to 'show' if you want to show a connection. Will show all devices unless 'cfname' is set. + - Set to 'up' if you want to bring a connection up. Requires 'cfname' to be set. + - Set to 'down' if you want to bring a connection down. Requires 'cfname' to be set. + cname: + required: True + default: None + description: + - Where CNAME will be the name used to call the connection. when not provided a default name is generated: [-][-] + ifname: + required: False + default: cname + description: + - Where INAME will be the what we call the interface name. Required with 'up', 'down' modifiers. + - interface to bind the connection to. The connection will only be applicable to this interface name. + - A special value of "*" can be used for interface-independent connections. + - The ifname argument is mandatory for all connection types except bond, team, bridge and vlan. + type: + required: False + choices: [ ethernet, team, team-slave, bond, bond-slave, bridge, vlan ] + description: + - This is the type of device or network connection that you wish to create. + mode: + required: False + choices: [ "balance-rr", "active-backup", "balance-xor", "broadcast", "802.3ad", "balance-tlb", "balance-alb" ] + default: None + description: + - This is the type of device or network connection that you wish to create for a bond, team or bridge. (NetworkManager default: balance-rr) + master: + required: False + default: None + description: + - master ] STP forwarding delay, in seconds (NetworkManager default: 15) + hellotime: + required: False + default: None + description: + - This is only used with bridge - [hello-time <1-10>] STP hello time, in seconds (NetworkManager default: 2) + maxage: + required: False + default: None + description: + - This is only used with bridge - [max-age <6-42>] STP maximum message age, in seconds (NetworkManager default: 20) + ageingtime: + required: False + default: None + description: + - This is only used with bridge - [ageing-time <0-1000000>] the Ethernet MAC address aging time, in seconds (NetworkManager default: 300) + mac: + required: False + default: None + description: + - This is only used with bridge - MAC address of the bridge (note: this requires a recent kernel feature, originally introduced in 3.15 upstream kernel) + slavepriority: + required: False + default: None + description: + - This is only used with 'bridge-slave' - [<0-63>] - STP priority of this slave (default: 32) + path_cost: + required: False + default: None + description: + - This is only used with 'bridge-slave' - [<1-65535>] - STP port cost for destinations via this slave (NetworkManager default: 100) + hairpin: + required: False + default: None + description: + - This is only used with 'bridge-slave' - 'hairpin mode' for the slave, which allows frames to be sent back out through the slave the frame was received on. (NetworkManager default: yes) + vlanid: + required: False + default: None + description: + - This is only used with VLAN - VLAN ID in range <0-4095> + vlandev: + required: False + default: None + description: + - This is only used with VLAN - parent device this VLAN is on, can use ifname + flags: + required: False + default: None + description: + - This is only used with VLAN - flags + ingress: + required: False + default: None + description: + - This is only used with VLAN - VLAN ingress priority mapping + egress: + required: False + default: None + description: + - This is only used with VLAN - VLAN egress priority mapping + +''' + +EXAMPLES=''' +The following examples are working examples that I have run in the field. I followed follow the structure: +``` +|_/inventory/cloud-hosts +| /group_vars/openstack-stage.yml +| /host_vars/controller-01.openstack.host.com +| /host_vars/controller-02.openstack.host.com +|_/playbook/library/nmcli.py +| /playbook-add.yml +| /playbook-del.yml +``` + +## inventory examples +### groups_vars +```yml +--- +#devops_os_define_network +storage_gw: "192.168.0.254" +external_gw: "10.10.0.254" +tenant_gw: "172.100.0.254" + +#Team vars +nmcli_team: + - {cname: 'tenant', ip4: "{{tenant_ip}}", gw4: "{{tenant_gw}}"} + - {cname: 'external', ip4: "{{external_ip}}", gw4: "{{external_gw}}"} + - {cname: 'storage', ip4: "{{storage_ip}}", gw4: "{{storage_gw}}"} +nmcli_team_slave: + - {cname: 'em1', ifname: 'em1', master: 'tenant'} + - {cname: 'em2', ifname: 'em2', master: 'tenant'} + - {cname: 'p2p1', ifname: 'p2p1', master: 'storage'} + - {cname: 'p2p2', ifname: 'p2p2', master: 'external'} + +#bond vars +nmcli_bond: + - {cname: 'tenant', ip4: "{{tenant_ip}}", gw4: '', mode: 'balance-rr'} + - {cname: 'external', ip4: "{{external_ip}}", gw4: '', mode: 'balance-rr'} + - {cname: 'storage', ip4: "{{storage_ip}}", gw4: "{{storage_gw}}", mode: 'balance-rr'} +nmcli_bond_slave: + - {cname: 'em1', ifname: 'em1', master: 'tenant'} + - {cname: 'em2', ifname: 'em2', master: 'tenant'} + - {cname: 'p2p1', ifname: 'p2p1', master: 'storage'} + - {cname: 'p2p2', ifname: 'p2p2', master: 'external'} + +#ethernet vars +nmcli_ethernet: + - {cname: 'em1', ifname: 'em1', ip4: "{{tenant_ip}}", gw4: "{{tenant_gw}}"} + - {cname: 'em2', ifname: 'em2', ip4: "{{tenant_ip1}}", gw4: "{{tenant_gw}}"} + - {cname: 'p2p1', ifname: 'p2p1', ip4: "{{storage_ip}}", gw4: "{{storage_gw}}"} + - {cname: 'p2p2', ifname: 'p2p2', ip4: "{{external_ip}}", gw4: "{{external_gw}}"} +``` + +### host_vars +```yml +--- +storage_ip: "192.168.160.21/23" +external_ip: "10.10.152.21/21" +tenant_ip: "192.168.200.21/23" +``` + + + +## playbook-add.yml example + +```yml +--- +- hosts: openstack-stage + remote_user: root + tasks: + +- name: install needed network manager libs + yum: name={{ item }} state=installed + with_items: + - libnm-qt-devel.x86_64 + - nm-connection-editor.x86_64 + - libsemanage-python + - policycoreutils-python + +##### Working with all cloud nodes - Teaming + - name: try nmcli add team - cname only & ip4 gw4 + nmcli: type=team cname={{item.cname}} ip4={{item.ip4}} gw4={{item.gw4}} state=present + with_items: + - "{{nmcli_team}}" + + - name: try nmcli add teams-slave + nmcli: type=team-slave cname={{item.cname}} ifname={{item.ifname}} master={{item.master}} state=present + with_items: + - "{{nmcli_team_slave}}" + +###### Working with all cloud nodes - Bonding +# - name: try nmcli add bond - cname only & ip4 gw4 mode +# nmcli: type=bond cname={{item.cname}} ip4={{item.ip4}} gw4={{item.gw4}} mode={{item.mode}} state=present +# with_items: +# - "{{nmcli_bond}}" +# +# - name: try nmcli add bond-slave +# nmcli: type=bond-slave cname={{item.cname}} ifname={{item.ifname}} master={{item.master}} state=present +# with_items: +# - "{{nmcli_bond_slave}}" + +##### Working with all cloud nodes - Ethernet +# - name: nmcli add Ethernet - cname only & ip4 gw4 +# nmcli: type=ethernet cname={{item.cname}} ip4={{item.ip4}} gw4={{item.gw4}} state=present +# with_items: +# - "{{nmcli_ethernet}}" +``` + +## playbook-del.yml example + +```yml +--- +- hosts: openstack-stage + remote_user: root + tasks: + + - name: try nmcli del team - multiple + nmcli: cname={{item.cname}} state=absent + with_items: + - { cname: 'em1'} + - { cname: 'em2'} + - { cname: 'p1p1'} + - { cname: 'p1p2'} + - { cname: 'p2p1'} + - { cname: 'p2p2'} + - { cname: 'tenant'} + - { cname: 'storage'} + - { cname: 'external'} + - { cname: 'team-em1'} + - { cname: 'team-em2'} + - { cname: 'team-p1p1'} + - { cname: 'team-p1p2'} + - { cname: 'team-p2p1'} + - { cname: 'team-p2p2'} +``` +# To add an Ethernet connection with static IP configuration, issue a command as follows +- nmcli: cname=my-eth1 ifname=eth1 type=ethernet ip4=192.168.100.100/24 gw4=192.168.100.1 state=present + +# To add an Team connection with static IP configuration, issue a command as follows +- nmcli: cname=my-team1 ifname=my-team1 type=team ip4=192.168.100.100/24 gw4=192.168.100.1 state=present enabled=yes + +# Optionally, at the same time specify IPv6 addresses for the device as follows: +- nmcli: cname=my-eth1 ifname=eth1 type=ethernet ip4=192.168.100.100/24 gw4=192.168.100.1 ip6=abbe::cafe gw6=2001:db8::1 state=present + +# To add two IPv4 DNS server addresses: +-nmcli: cname=my-eth1 dns4=["8.8.8.8", "8.8.4.4"] state=present + +# To make a profile usable for all compatible Ethernet interfaces, issue a command as follows +- nmcli: ctype=ethernet name=my-eth1 ifname="*" state=present + +# To change the property of a setting e.g. MTU, issue a command as follows: +- nmcli: cname=my-eth1 mtu=9000 state=present + + Exit Status's: + - nmcli exits with status 0 if it succeeds, a value greater than 0 is + returned if an error occurs. + - 0 Success - indicates the operation succeeded + - 1 Unknown or unspecified error + - 2 Invalid user input, wrong nmcli invocation + - 3 Timeout expired (see --wait option) + - 4 Connection activation failed + - 5 Connection deactivation failed + - 6 Disconnecting device failed + - 7 Connection deletion failed + - 8 NetworkManager is not running + - 9 nmcli and NetworkManager versions mismatch + - 10 Connection, device, or access point does not exist. +''' +# import ansible.module_utils.basic +import os +import syslog +import sys +import dbus +from gi.repository import NetworkManager, NMClient + + +class Nmcli(object): + """ + This is the generic nmcli manipulation class that is subclassed based on platform. + A subclass may wish to override the following action methods:- + - create_connection() + - delete_connection() + - modify_connection() + - show_connection() + - up_connection() + - down_connection() + All subclasses MUST define platform and distribution (which may be None). + """ + + platform='Generic' + distribution=None + bus=dbus.SystemBus() + # The following is going to be used in dbus code + DEVTYPES={1: "Ethernet", + 2: "Wi-Fi", + 5: "Bluetooth", + 6: "OLPC", + 7: "WiMAX", + 8: "Modem", + 9: "InfiniBand", + 10: "Bond", + 11: "VLAN", + 12: "ADSL", + 13: "Bridge", + 14: "Generic", + 15: "Team" + } + STATES={0: "Unknown", + 10: "Unmanaged", + 20: "Unavailable", + 30: "Disconnected", + 40: "Prepare", + 50: "Config", + 60: "Need Auth", + 70: "IP Config", + 80: "IP Check", + 90: "Secondaries", + 100: "Activated", + 110: "Deactivating", + 120: "Failed" + } + + def __new__(cls, *args, **kwargs): + return load_platform_subclass(Nmcli, args, kwargs) + + def __init__(self, module): + self.module=module + self.state=module.params['state'] + self.enabled=module.params['enabled'] + self.action=module.params['action'] + self.cname=module.params['cname'] + self.master=module.params['master'] + self.autoconnect=module.params['autoconnect'] + self.ifname=module.params['ifname'] + self.type=module.params['type'] + self.ip4=module.params['ip4'] + self.gw4=module.params['gw4'] + self.dns4=module.params['dns4'] + self.ip6=module.params['ip6'] + self.gw6=module.params['gw6'] + self.dns6=module.params['dns6'] + self.mtu=module.params['mtu'] + self.stp=module.params['stp'] + self.priority=module.params['priority'] + self.mode=module.params['mode'] + self.miimon=module.params['miimon'] + self.downdelay=module.params['downdelay'] + self.updelay=module.params['updelay'] + self.arp_interval=module.params['arp_interval'] + self.arp_ip_target=module.params['arp_ip_target'] + self.slavepriority=module.params['slavepriority'] + self.forwarddelay=module.params['forwarddelay'] + self.hellotime=module.params['hellotime'] + self.maxage=module.params['maxage'] + self.ageingtime=module.params['ageingtime'] + self.mac=module.params['mac'] + self.vlanid=module.params['vlanid'] + self.vlandev=module.params['vlandev'] + self.flags=module.params['flags'] + self.ingress=module.params['ingress'] + self.egress=module.params['egress'] + # select whether we dump additional debug info through syslog + self.syslogging=True + + def execute_command(self, cmd, use_unsafe_shell=False, data=None): + if self.syslogging: + syslog.openlog('ansible-%s' % os.path.basename(__file__)) + syslog.syslog(syslog.LOG_NOTICE, 'Command %s' % '|'.join(cmd)) + + return self.module.run_command(cmd, use_unsafe_shell=use_unsafe_shell, data=data) + + def merge_secrets(self, proxy, config, setting_name): + try: + # returns a dict of dicts mapping name::setting, where setting is a dict + # mapping key::value. Each member of the 'setting' dict is a secret + secrets=proxy.GetSecrets(setting_name) + + # Copy the secrets into our connection config + for setting in secrets: + for key in secrets[setting]: + config[setting_name][key]=secrets[setting][key] + except Exception, e: + pass + + def dict_to_string(self, d): + # Try to trivially translate a dictionary's elements into nice string + # formatting. + dstr="" + for key in d: + val=d[key] + str_val="" + add_string=True + if type(val)==type(dbus.Array([])): + for elt in val: + if type(elt)==type(dbus.Byte(1)): + str_val+="%s " % int(elt) + elif type(elt)==type(dbus.String("")): + str_val+="%s" % elt + elif type(val)==type(dbus.Dictionary({})): + dstr+=self.dict_to_string(val) + add_string=False + else: + str_val=val + if add_string: + dstr+="%s: %s\n" % ( key, str_val) + return dstr + + def connection_to_string(self, config): + # dump a connection configuration to use in list_connection_info + setting_list=[] + for setting_name in config: + setting_list.append(self.dict_to_string(config[setting_name])) + return setting_list + # print "" + + def list_connection_info(self): + # Ask the settings service for the list of connections it provides + bus=dbus.SystemBus() + + service_name="org.freedesktop.NetworkManager" + proxy=bus.get_object(service_name, "/org/freedesktop/NetworkManager/Settings") + settings=dbus.Interface(proxy, "org.freedesktop.NetworkManager.Settings") + connection_paths=settings.ListConnections() + connection_list=[] + # List each connection's name, UUID, and type + for path in connection_paths: + con_proxy=bus.get_object(service_name, path) + settings_connection=dbus.Interface(con_proxy, "org.freedesktop.NetworkManager.Settings.Connection") + config=settings_connection.GetSettings() + + # Now get secrets too; we grab the secrets for each type of connection + # (since there isn't a "get all secrets" call because most of the time + # you only need 'wifi' secrets or '802.1x' secrets, not everything) and + # merge that into the configuration data - To use at a later stage + self.merge_secrets(settings_connection, config, '802-11-wireless') + self.merge_secrets(settings_connection, config, '802-11-wireless-security') + self.merge_secrets(settings_connection, config, '802-1x') + self.merge_secrets(settings_connection, config, 'gsm') + self.merge_secrets(settings_connection, config, 'cdma') + self.merge_secrets(settings_connection, config, 'ppp') + + # Get the details of the 'connection' setting + s_con=config['connection'] + connection_list.append(s_con['id']) + connection_list.append(s_con['uuid']) + connection_list.append(s_con['type']) + connection_list.append(self.connection_to_string(config)) + return connection_list + + def connection_exists(self): + # we are going to use name and type in this instance to find if that connection exists and is of type x + connections=self.list_connection_info() + + for con_item in connections: + if self.cname==con_item: + return True + + def down_connection(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # if self.connection_exists(): + cmd.append('con') + cmd.append('down') + cmd.append(self.cname) + return self.execute_command(cmd) + + def up_connection(self): + cmd=[self.module.get_bin_path('nmcli', True)] + cmd.append('con') + cmd.append('up') + cmd.append(self.cname) + return self.execute_command(cmd) + + def create_connection_team(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for creating team interface + cmd.append('con') + cmd.append('add') + cmd.append('type') + cmd.append('team') + cmd.append('con-name') + if self.cname is not None: + cmd.append(self.cname) + elif self.ifname is not None: + cmd.append(self.ifname) + cmd.append('ifname') + if self.ifname is not None: + cmd.append(self.ifname) + elif self.cname is not None: + cmd.append(self.cname) + if self.ip4 is not None: + cmd.append('ip4') + cmd.append(self.ip4) + if self.gw4 is not None: + cmd.append('gw4') + cmd.append(self.gw4) + if self.ip6 is not None: + cmd.append('ip6') + cmd.append(self.ip6) + if self.gw6 is not None: + cmd.append('gw6') + cmd.append(self.gw6) + if self.enabled is not None: + cmd.append('autoconnect') + cmd.append(self.enabled) + return cmd + + def modify_connection_team(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for modifying team interface + cmd.append('con') + cmd.append('mod') + cmd.append(self.cname) + if self.ip4 is not None: + cmd.append('ipv4.address') + cmd.append(self.ip4) + if self.gw4 is not None: + cmd.append('ipv4.gateway') + cmd.append(self.gw4) + if self.dns4 is not None: + cmd.append('ipv4.dns') + cmd.append(self.dns4) + if self.ip6 is not None: + cmd.append('ipv6.address') + cmd.append(self.ip6) + if self.gw6 is not None: + cmd.append('ipv6.gateway') + cmd.append(self.gw4) + if self.dns6 is not None: + cmd.append('ipv6.dns') + cmd.append(self.dns6) + if self.enabled is not None: + cmd.append('autoconnect') + cmd.append(self.enabled) + # Can't use MTU with team + return cmd + + def create_connection_team_slave(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for creating team-slave interface + cmd.append('connection') + cmd.append('add') + cmd.append('type') + cmd.append(self.type) + cmd.append('con-name') + if self.cname is not None: + cmd.append(self.cname) + elif self.ifname is not None: + cmd.append(self.ifname) + cmd.append('ifname') + if self.ifname is not None: + cmd.append(self.ifname) + elif self.cname is not None: + cmd.append(self.cname) + cmd.append('master') + if self.cname is not None: + cmd.append(self.master) + # if self.mtu is not None: + # cmd.append('802-3-ethernet.mtu') + # cmd.append(self.mtu) + return cmd + + def modify_connection_team_slave(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for modifying team-slave interface + cmd.append('con') + cmd.append('mod') + cmd.append(self.cname) + cmd.append('connection.master') + cmd.append(self.master) + if self.mtu is not None: + cmd.append('802-3-ethernet.mtu') + cmd.append(self.mtu) + return cmd + + def create_connection_bond(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for creating bond interface + cmd.append('con') + cmd.append('add') + cmd.append('type') + cmd.append('bond') + cmd.append('con-name') + if self.cname is not None: + cmd.append(self.cname) + elif self.ifname is not None: + cmd.append(self.ifname) + cmd.append('ifname') + if self.ifname is not None: + cmd.append(self.ifname) + elif self.cname is not None: + cmd.append(self.cname) + if self.ip4 is not None: + cmd.append('ip4') + cmd.append(self.ip4) + if self.gw4 is not None: + cmd.append('gw4') + cmd.append(self.gw4) + if self.ip6 is not None: + cmd.append('ip6') + cmd.append(self.ip6) + if self.gw6 is not None: + cmd.append('gw6') + cmd.append(self.gw6) + if self.enabled is not None: + cmd.append('autoconnect') + cmd.append(self.enabled) + if self.mode is not None: + cmd.append('mode') + cmd.append(self.mode) + if self.miimon is not None: + cmd.append('miimon') + cmd.append(self.miimon) + if self.downdelay is not None: + cmd.append('downdelay') + cmd.append(self.downdelay) + if self.downdelay is not None: + cmd.append('updelay') + cmd.append(self.updelay) + if self.downdelay is not None: + cmd.append('arp-interval') + cmd.append(self.arp_interval) + if self.downdelay is not None: + cmd.append('arp-ip-target') + cmd.append(self.arp_ip_target) + return cmd + + def modify_connection_bond(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for modifying bond interface + cmd.append('con') + cmd.append('mod') + cmd.append(self.cname) + if self.ip4 is not None: + cmd.append('ipv4.address') + cmd.append(self.ip4) + if self.gw4 is not None: + cmd.append('ipv4.gateway') + cmd.append(self.gw4) + if self.dns4 is not None: + cmd.append('ipv4.dns') + cmd.append(self.dns4) + if self.ip6 is not None: + cmd.append('ipv6.address') + cmd.append(self.ip6) + if self.gw6 is not None: + cmd.append('ipv6.gateway') + cmd.append(self.gw4) + if self.dns6 is not None: + cmd.append('ipv6.dns') + cmd.append(self.dns6) + if self.enabled is not None: + cmd.append('autoconnect') + cmd.append(self.enabled) + return cmd + + def create_connection_bond_slave(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for creating bond-slave interface + cmd.append('connection') + cmd.append('add') + cmd.append('type') + cmd.append('bond-slave') + cmd.append('con-name') + if self.cname is not None: + cmd.append(self.cname) + elif self.ifname is not None: + cmd.append(self.ifname) + cmd.append('ifname') + if self.ifname is not None: + cmd.append(self.ifname) + elif self.cname is not None: + cmd.append(self.cname) + cmd.append('master') + if self.cname is not None: + cmd.append(self.master) + return cmd + + def modify_connection_bond_slave(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for modifying bond-slave interface + cmd.append('con') + cmd.append('mod') + cmd.append(self.cname) + cmd.append('connection.master') + cmd.append(self.master) + return cmd + + def create_connection_ethernet(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for creating ethernet interface + # To add an Ethernet connection with static IP configuration, issue a command as follows + # - nmcli: name=add cname=my-eth1 ifname=eth1 type=ethernet ip4=192.168.100.100/24 gw4=192.168.100.1 state=present + # nmcli con add con-name my-eth1 ifname eth1 type ethernet ip4 192.168.100.100/24 gw4 192.168.100.1 + cmd.append('con') + cmd.append('add') + cmd.append('type') + cmd.append('ethernet') + cmd.append('con-name') + if self.cname is not None: + cmd.append(self.cname) + elif self.ifname is not None: + cmd.append(self.ifname) + cmd.append('ifname') + if self.ifname is not None: + cmd.append(self.ifname) + elif self.cname is not None: + cmd.append(self.cname) + if self.ip4 is not None: + cmd.append('ip4') + cmd.append(self.ip4) + if self.gw4 is not None: + cmd.append('gw4') + cmd.append(self.gw4) + if self.ip6 is not None: + cmd.append('ip6') + cmd.append(self.ip6) + if self.gw6 is not None: + cmd.append('gw6') + cmd.append(self.gw6) + if self.enabled is not None: + cmd.append('autoconnect') + cmd.append(self.enabled) + return cmd + + def modify_connection_ethernet(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for modifying ethernet interface + # To add an Ethernet connection with static IP configuration, issue a command as follows + # - nmcli: name=add cname=my-eth1 ifname=eth1 type=ethernet ip4=192.168.100.100/24 gw4=192.168.100.1 state=present + # nmcli con add con-name my-eth1 ifname eth1 type ethernet ip4 192.168.100.100/24 gw4 192.168.100.1 + cmd.append('con') + cmd.append('mod') + cmd.append(self.cname) + if self.ip4 is not None: + cmd.append('ipv4.address') + cmd.append(self.ip4) + if self.gw4 is not None: + cmd.append('ipv4.gateway') + cmd.append(self.gw4) + if self.dns4 is not None: + cmd.append('ipv4.dns') + cmd.append(self.dns4) + if self.ip6 is not None: + cmd.append('ipv6.address') + cmd.append(self.ip6) + if self.gw6 is not None: + cmd.append('ipv6.gateway') + cmd.append(self.gw4) + if self.dns6 is not None: + cmd.append('ipv6.dns') + cmd.append(self.dns6) + if self.mtu is not None: + cmd.append('802-3-ethernet.mtu') + cmd.append(self.mtu) + if self.enabled is not None: + cmd.append('autoconnect') + cmd.append(self.enabled) + return cmd + + def create_connection_bridge(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for creating bridge interface + return cmd + + def modify_connection_bridge(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for modifying bridge interface + return cmd + + def create_connection_vlan(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for creating ethernet interface + return cmd + + def modify_connection_vlan(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for modifying ethernet interface + return cmd + + def create_connection(self): + cmd=[] + if self.type=='team': + # cmd=self.create_connection_team() + if (self.dns4 is not None) or (self.dns6 is not None): + cmd=self.create_connection_team() + self.execute_command(cmd) + cmd=self.modify_connection_team() + self.execute_command(cmd) + cmd=self.up_connection() + return self.execute_command(cmd) + elif (self.dns4 is None) or (self.dns6 is None): + cmd=self.create_connection_team() + return self.execute_command(cmd) + elif self.type=='team-slave': + if self.mtu is not None: + cmd=self.create_connection_team_slave() + self.execute_command(cmd) + cmd=self.modify_connection_team_slave() + self.execute_command(cmd) + # cmd=self.up_connection() + return self.execute_command(cmd) + else: + cmd=self.create_connection_team_slave() + return self.execute_command(cmd) + elif self.type=='bond': + if (self.mtu is not None) or (self.dns4 is not None) or (self.dns6 is not None): + cmd=self.create_connection_bond() + self.execute_command(cmd) + cmd=self.modify_connection_bond() + self.execute_command(cmd) + cmd=self.up_connection() + return self.execute_command(cmd) + else: + cmd=self.create_connection_bond() + return self.execute_command(cmd) + elif self.type=='bond-slave': + cmd=self.create_connection_bond_slave() + elif self.type=='ethernet': + if (self.mtu is not None) or (self.dns4 is not None) or (self.dns6 is not None): + cmd=self.create_connection_ethernet() + self.execute_command(cmd) + cmd=self.modify_connection_ethernet() + self.execute_command(cmd) + cmd=self.up_connection() + return self.execute_command(cmd) + else: + cmd=self.create_connection_ethernet() + return self.execute_command(cmd) + elif self.type=='bridge': + cmd=self.create_connection_bridge() + elif self.type=='vlan': + cmd=self.create_connection_vlan() + return self.execute_command(cmd) + + def remove_connection(self): + # self.down_connection() + cmd=[self.module.get_bin_path('nmcli', True)] + cmd.append('con') + cmd.append('del') + cmd.append(self.cname) + return self.execute_command(cmd) + + def modify_connection(self): + cmd=[] + if self.type=='team': + cmd=self.modify_connection_team() + elif self.type=='team-slave': + cmd=self.modify_connection_team_slave() + elif self.type=='bond': + cmd=self.modify_connection_bond() + elif self.type=='bond-slave': + cmd=self.modify_connection_bond_slave() + elif self.type=='ethernet': + cmd=self.modify_connection_ethernet() + elif self.type=='bridge': + cmd=self.modify_connection_bridge() + elif self.type=='vlan': + cmd=self.modify_connection_vlan() + return self.execute_command(cmd) + + +def main(): + # Parsing argument file + module=AnsibleModule( + argument_spec=dict( + enabled=dict(required=False, default=None, choices=['yes', 'no'], type='str'), + action=dict(required=False, default=None, choices=['add', 'mod', 'show', 'up', 'down', 'del'], type='str'), + state=dict(required=True, default=None, choices=['present', 'absent'], type='str'), + cname=dict(required=False, type='str'), + master=dict(required=False, default=None, type='str'), + autoconnect=dict(required=False, default=None, choices=['yes', 'no'], type='str'), + ifname=dict(required=False, default=None, type='str'), + type=dict(required=False, default=None, choices=['ethernet', 'team', 'team-slave', 'bond', 'bond-slave', 'bridge', 'vlan'], type='str'), + ip4=dict(required=False, default=None, type='str'), + gw4=dict(required=False, default=None, type='str'), + dns4=dict(required=False, default=None, type='str'), + ip6=dict(required=False, default=None, type='str'), + gw6=dict(required=False, default=None, type='str'), + dns6=dict(required=False, default=None, type='str'), + # Bond Specific vars + mode=dict(require=False, default="balance-rr", choices=["balance-rr", "active-backup", "balance-xor", "broadcast", "802.3ad", "balance-tlb", "balance-alb"], type='str'), + miimon=dict(required=False, default=None, type='str'), + downdelay=dict(required=False, default=None, type='str'), + updelay=dict(required=False, default=None, type='str'), + arp_interval=dict(required=False, default=None, type='str'), + arp_ip_target=dict(required=False, default=None, type='str'), + # general usage + mtu=dict(required=False, default=None, type='str'), + mac=dict(required=False, default=None, type='str'), + # bridge specific vars + stp=dict(required=False, default='yes', choices=['yes', 'no'], type='str'), + priority=dict(required=False, default="128", type='str'), + slavepriority=dict(required=False, default="32", type='str'), + forwarddelay=dict(required=False, default="15", type='str'), + hellotime=dict(required=False, default="2", type='str'), + maxage=dict(required=False, default="20", type='str'), + ageingtime=dict(required=False, default="300", type='str'), + # vlan specific vars + vlanid=dict(required=False, default=None, type='str'), + vlandev=dict(required=False, default=None, type='str'), + flags=dict(required=False, default=None, type='str'), + ingress=dict(required=False, default=None, type='str'), + egress=dict(required=False, default=None, type='str'), + ), + supports_check_mode=True + ) + + nmcli=Nmcli(module) + + if nmcli.syslogging: + syslog.openlog('ansible-%s' % os.path.basename(__file__)) + syslog.syslog(syslog.LOG_NOTICE, 'Nmcli instantiated - platform %s' % nmcli.platform) + if nmcli.distribution: + syslog.syslog(syslog.LOG_NOTICE, 'Nuser instantiated - distribution %s' % nmcli.distribution) + + rc=None + out='' + err='' + result={} + result['cname']=nmcli.cname + result['state']=nmcli.state + + # check for issues + if nmcli.cname is None: + nmcli.module.fail_json(msg="You haven't specified a name for the connection") + # team-slave checks + if nmcli.type=='team-slave' and nmcli.master is None: + nmcli.module.fail_json(msg="You haven't specified a name for the master so we're not changing a thing") + if nmcli.type=='team-slave' and nmcli.ifname is None: + nmcli.module.fail_json(msg="You haven't specified a name for the connection") + + if nmcli.state=='absent': + if nmcli.connection_exists(): + if module.check_mode: + module.exit_json(changed=True) + (rc, out, err)=nmcli.down_connection() + (rc, out, err)=nmcli.remove_connection() + if rc!=0: + module.fail_json(name =('No Connection named %s exists' % nmcli.cname), msg=err, rc=rc) + + elif nmcli.state=='present': + if nmcli.connection_exists(): + # modify connection (note: this function is check mode aware) + # result['Connection']=('Connection %s of Type %s is not being added' % (nmcli.cname, nmcli.type)) + result['Exists']='Connections do exist so we are modifying them' + if module.check_mode: + module.exit_json(changed=True) + (rc, out, err)=nmcli.modify_connection() + if not nmcli.connection_exists(): + result['Connection']=('Connection %s of Type %s is being added' % (nmcli.cname, nmcli.type)) + if module.check_mode: + module.exit_json(changed=True) + (rc, out, err)=nmcli.create_connection() + if rc is not None and rc!=0: + module.fail_json(name=nmcli.cname, msg=err, rc=rc) + + if rc is None: + result['changed']=False + else: + result['changed']=True + if out: + result['stdout']=out + if err: + result['stderr']=err + + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * + +main() \ No newline at end of file From 8781bf828104876426f999994db210e3c0eb1c48 Mon Sep 17 00:00:00 2001 From: Chris Long Date: Fri, 15 May 2015 00:45:51 +1000 Subject: [PATCH 0057/2522] Updated as per bcoca's comments: removed 'default' in state: removed defunct action: removed reference to load_platform_subclass changed cname to conn_name --- network/nmcli.py | 202 ++++++++++++++++++++++------------------------- 1 file changed, 93 insertions(+), 109 deletions(-) diff --git a/network/nmcli.py b/network/nmcli.py index 0532058da3b..55edb322ad7 100644 --- a/network/nmcli.py +++ b/network/nmcli.py @@ -30,7 +30,6 @@ options: state: required: True - default: "present" choices: [ present, absent ] description: - Whether the device should exist or not, taking action if the state is different from what is stated. @@ -41,25 +40,14 @@ description: - Whether the service should start on boot. B(At least one of state and enabled are required.) - Whether the connection profile can be automatically activated ( default: yes) - action: - required: False - default: None - choices: [ add, modify, show, up, down ] - description: - - Set to 'add' if you want to add a connection. - - Set to 'modify' if you want to modify a connection. Modify one or more properties in the connection profile. - - Set to 'delete' if you want to delete a connection. Delete a configured connection. The connection to be deleted is identified by its name 'cfname'. - - Set to 'show' if you want to show a connection. Will show all devices unless 'cfname' is set. - - Set to 'up' if you want to bring a connection up. Requires 'cfname' to be set. - - Set to 'down' if you want to bring a connection down. Requires 'cfname' to be set. - cname: + conn_name: required: True default: None description: - - Where CNAME will be the name used to call the connection. when not provided a default name is generated: [-][-] + - Where conn_name will be the name used to call the connection. when not provided a default name is generated: [-][-] ifname: required: False - default: cname + default: conn_name description: - Where INAME will be the what we call the interface name. Required with 'up', 'down' modifiers. - interface to bind the connection to. The connection will only be applicable to this interface name. @@ -80,7 +68,7 @@ required: False default: None description: - - master Date: Fri, 15 May 2015 01:09:49 +1000 Subject: [PATCH 0058/2522] Fixed descriptions to all be lists replaced enabled with autoconnect - refactored code to reflect update. removed ansible syslog entry. --- network/nmcli.py | 66 +++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/network/nmcli.py b/network/nmcli.py index 55edb322ad7..18f0ecbab1f 100644 --- a/network/nmcli.py +++ b/network/nmcli.py @@ -31,25 +31,24 @@ state: required: True choices: [ present, absent ] - description: - - Whether the device should exist or not, taking action if the state is different from what is stated. - enabled: + description: + - Whether the device should exist or not, taking action if the state is different from what is stated. + autoconnect: required: False default: "yes" choices: [ "yes", "no" ] description: - - Whether the service should start on boot. B(At least one of state and enabled are required.) + - Whether the connection should start on boot. - Whether the connection profile can be automatically activated ( default: yes) conn_name: required: True - default: None description: - Where conn_name will be the name used to call the connection. when not provided a default name is generated: [-][-] ifname: required: False default: conn_name description: - - Where INAME will be the what we call the interface name. Required with 'up', 'down' modifiers. + - Where IFNAME will be the what we call the interface name. - interface to bind the connection to. The connection will only be applicable to this interface name. - A special value of "*" can be used for interface-independent connections. - The ifname argument is mandatory for all connection types except bond, team, bridge and vlan. @@ -72,14 +71,17 @@ ip4: required: False default: None - description: The IPv4 address to this interface using this format ie: "192.168.1.24/24" + description: + - The IPv4 address to this interface using this format ie: "192.168.1.24/24" gw4: required: False - description: The IPv4 gateway for this interface using this format ie: "192.168.100.1" + description: + - The IPv4 gateway for this interface using this format ie: "192.168.100.1" dns4: required: False default: None - description: A list of upto 3 dns servers, ipv4 format e.g. To add two IPv4 DNS server addresses: ['"8.8.8.8 8.8.4.4"'] + description: + - A list of upto 3 dns servers, ipv4 format e.g. To add two IPv4 DNS server addresses: ['"8.8.8.8 8.8.4.4"'] ip6: required: False default: None @@ -88,10 +90,12 @@ gw6: required: False default: None - description: The IPv6 gateway for this interface using this format ie: "2001:db8::1" + description: + - The IPv6 gateway for this interface using this format ie: "2001:db8::1" dns6: required: False - description: A list of upto 3 dns servers, ipv6 format e.g. To add two IPv6 DNS server addresses: ['"2001:4860:4860::8888 2001:4860:4860::8844"'] + description: + - A list of upto 3 dns servers, ipv6 format e.g. To add two IPv6 DNS server addresses: ['"2001:4860:4860::8888 2001:4860:4860::8844"'] mtu: required: False default: None @@ -343,7 +347,7 @@ - nmcli: conn_name=my-eth1 ifname=eth1 type=ethernet ip4=192.168.100.100/24 gw4=192.168.100.1 state=present # To add an Team connection with static IP configuration, issue a command as follows -- nmcli: conn_name=my-team1 ifname=my-team1 type=team ip4=192.168.100.100/24 gw4=192.168.100.1 state=present enabled=yes +- nmcli: conn_name=my-team1 ifname=my-team1 type=team ip4=192.168.100.100/24 gw4=192.168.100.1 state=present autoconnect=yes # Optionally, at the same time specify IPv6 addresses for the device as follows: - nmcli: conn_name=my-eth1 ifname=eth1 type=ethernet ip4=192.168.100.100/24 gw4=192.168.100.1 ip6=abbe::cafe gw6=2001:db8::1 state=present @@ -430,10 +434,9 @@ class Nmcli(object): def __init__(self, module): self.module=module self.state=module.params['state'] - self.enabled=module.params['enabled'] + self.autoconnect=module.params['autoconnect'] self.conn_name=module.params['conn_name'] self.master=module.params['master'] - self.autoconnect=module.params['autoconnect'] self.ifname=module.params['ifname'] self.type=module.params['type'] self.ip4=module.params['ip4'] @@ -602,9 +605,9 @@ def create_connection_team(self): if self.gw6 is not None: cmd.append('gw6') cmd.append(self.gw6) - if self.enabled is not None: + if self.autoconnect is not None: cmd.append('autoconnect') - cmd.append(self.enabled) + cmd.append(self.autoconnect) return cmd def modify_connection_team(self): @@ -631,9 +634,9 @@ def modify_connection_team(self): if self.dns6 is not None: cmd.append('ipv6.dns') cmd.append(self.dns6) - if self.enabled is not None: + if self.autoconnect is not None: cmd.append('autoconnect') - cmd.append(self.enabled) + cmd.append(self.autoconnect) # Can't use MTU with team return cmd @@ -704,9 +707,9 @@ def create_connection_bond(self): if self.gw6 is not None: cmd.append('gw6') cmd.append(self.gw6) - if self.enabled is not None: + if self.autoconnect is not None: cmd.append('autoconnect') - cmd.append(self.enabled) + cmd.append(self.autoconnect) if self.mode is not None: cmd.append('mode') cmd.append(self.mode) @@ -751,9 +754,9 @@ def modify_connection_bond(self): if self.dns6 is not None: cmd.append('ipv6.dns') cmd.append(self.dns6) - if self.enabled is not None: + if self.autoconnect is not None: cmd.append('autoconnect') - cmd.append(self.enabled) + cmd.append(self.autoconnect) return cmd def create_connection_bond_slave(self): @@ -820,9 +823,9 @@ def create_connection_ethernet(self): if self.gw6 is not None: cmd.append('gw6') cmd.append(self.gw6) - if self.enabled is not None: + if self.autoconnect is not None: cmd.append('autoconnect') - cmd.append(self.enabled) + cmd.append(self.autoconnect) return cmd def modify_connection_ethernet(self): @@ -855,9 +858,9 @@ def modify_connection_ethernet(self): if self.mtu is not None: cmd.append('802-3-ethernet.mtu') cmd.append(self.mtu) - if self.enabled is not None: + if self.autoconnect is not None: cmd.append('autoconnect') - cmd.append(self.enabled) + cmd.append(self.autoconnect) return cmd def create_connection_bridge(self): @@ -966,11 +969,10 @@ def main(): # Parsing argument file module=AnsibleModule( argument_spec=dict( - enabled=dict(required=False, default=None, choices=['yes', 'no'], type='str'), + autoconnect=dict(required=False, default=None, choices=['yes', 'no'], type='str'), state=dict(required=True, choices=['present', 'absent'], type='str'), - conn_name=dict(required=False, type='str'), + conn_name=dict(required=True, type='str'), master=dict(required=False, default=None, type='str'), - autoconnect=dict(required=False, default=None, choices=['yes', 'no'], type='str'), ifname=dict(required=False, default=None, type='str'), type=dict(required=False, default=None, choices=['ethernet', 'team', 'team-slave', 'bond', 'bond-slave', 'bridge', 'vlan'], type='str'), ip4=dict(required=False, default=None, type='str'), @@ -1009,12 +1011,6 @@ def main(): nmcli=Nmcli(module) - if nmcli.syslogging: - syslog.openlog('ansible-%s' % os.path.basename(__file__)) - syslog.syslog(syslog.LOG_NOTICE, 'Nmcli instantiated - platform %s' % nmcli.platform) - if nmcli.distribution: - syslog.syslog(syslog.LOG_NOTICE, 'Nuser instantiated - distribution %s' % nmcli.distribution) - rc=None out='' err='' From 7c199cad252468c90a512bf735336285e893a200 Mon Sep 17 00:00:00 2001 From: Alan Loi Date: Sat, 16 May 2015 21:53:27 +1000 Subject: [PATCH 0059/2522] Add dynamodb_table module --- cloud/amazon/dynamodb_table | 261 ++++++++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 cloud/amazon/dynamodb_table diff --git a/cloud/amazon/dynamodb_table b/cloud/amazon/dynamodb_table new file mode 100644 index 00000000000..7a200a3b271 --- /dev/null +++ b/cloud/amazon/dynamodb_table @@ -0,0 +1,261 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = """ +--- +module: dynamodb_table +short_description: Create, update or delete AWS Dynamo DB tables. +description: + - Create or delete AWS Dynamo DB tables. + - Can update the provisioned throughput on existing tables. + - Returns the status of the specified table. +author: Alan Loi (@loia) +requirements: + - "boto >= 2.13.2" +options: + state: + description: + - Create or delete the table + required: false + choices: ['present', 'absent'] + default: 'present' + name: + description: + - Name of the table. + required: true + hash_key_name: + description: + - Name of the hash key. + - Required when state=present. + required: false + hash_key_type: + description: + - Type of the hash key. + required: false + choices: ['STRING', 'NUMBER', 'BINARY'] + default: 'STRING' + range_key_name: + description: + - Name of the range key. + required: false + range_key_type: + description: + - Type of the range key. + required: false + choices: ['STRING', 'NUMBER', 'BINARY'] + default: 'STRING' + read_capacity: + description: + - Read throughput capacity (units) to provision. + required: false + default: 1 + write_capacity: + description: + - Write throughput capacity (units) to provision. + required: false + default: 1 + region: + description: + - The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used. + required: false + aliases: ['aws_region', 'ec2_region'] + +extends_documentation_fragment: aws +""" + +EXAMPLES = ''' +# Create dynamo table with hash and range primary key +- dynamodb_table: + name: my-table + region: us-east-1 + hash_key_name: id + hash_key_type: STRING + range_key_name: create_time + range_key_type: NUMBER + read_capacity: 2 + write_capacity: 2 + +# Update capacity on existing dynamo table +- dynamodb_table: + name: my-table + region: us-east-1 + read_capacity: 10 + write_capacity: 10 + +# Delete dynamo table +- dynamodb_table: + name: my-table + region: us-east-1 + state: absent +''' + +try: + import boto + import boto.dynamodb2 + from boto.dynamodb2.table import Table + from boto.dynamodb2.fields import HashKey, RangeKey + from boto.dynamodb2.types import STRING, NUMBER, BINARY + from boto.exception import BotoServerError, JSONResponseError + +except ImportError: + print "failed=True msg='boto required for this module'" + sys.exit(1) + + +DYNAMO_TYPE_MAP = { + 'STRING': STRING, + 'NUMBER': NUMBER, + 'BINARY': BINARY +} + + +def create_or_update_dynamo_table(connection, module): + table_name = module.params.get('name') + hash_key_name = module.params.get('hash_key_name') + hash_key_type = module.params.get('hash_key_type') + range_key_name = module.params.get('range_key_name') + range_key_type = module.params.get('range_key_type') + read_capacity = module.params.get('read_capacity') + write_capacity = module.params.get('write_capacity') + + schema = [ + HashKey(hash_key_name, map_dynamo_type(hash_key_type)), + RangeKey(range_key_name, map_dynamo_type(range_key_type)) + ] + throughput = { + 'read': read_capacity, + 'write': write_capacity + } + + result = dict( + region=module.params.get('region'), + table_name=table_name, + hash_key_name=hash_key_name, + hash_key_type=hash_key_type, + range_key_name=range_key_name, + range_key_type=range_key_type, + read_capacity=read_capacity, + write_capacity=write_capacity, + ) + + try: + table = Table(table_name, connection=connection) + + if dynamo_table_exists(table): + changed = update_dynamo_table(table, throughput=throughput) + else: + Table.create(table_name, connection=connection, schema=schema, throughput=throughput) + changed = True + + result['table_status'] = table.describe()['Table']['TableStatus'] + result['changed'] = changed + + except BotoServerError: + result['msg'] = 'Failed to create/update dynamo table due to error: ' + traceback.format_exc() + module.fail_json(**result) + else: + module.exit_json(**result) + + +def delete_dynamo_table(connection, module): + table_name = module.params.get('table_name') + + result = dict( + region=module.params.get('region'), + table_name=table_name, + ) + + try: + changed = False + table = Table(table_name, connection=connection) + + if dynamo_table_exists(table): + table.delete() + changed = True + + result['changed'] = changed + + except BotoServerError: + result['msg'] = 'Failed to delete dynamo table due to error: ' + traceback.format_exc() + module.fail_json(**result) + else: + module.exit_json(**result) + + +def dynamo_table_exists(table): + try: + table.describe() + return True + + except JSONResponseError, e: + if e.message and e.message.startswith('Requested resource not found'): + return False + else: + raise e + + +def update_dynamo_table(table, throughput=None): + table.describe() # populate table details + + # AWS complains if the throughput hasn't changed + if has_throughput_changed(table, throughput): + return table.update(throughput=throughput) + + return False + + +def has_throughput_changed(table, new_throughput): + if not new_throughput: + return False + + return new_throughput['read'] != table.throughput['read'] or \ + new_throughput['write'] != table.throughput['write'] + + +def map_dynamo_type(dynamo_type): + return DYNAMO_TYPE_MAP.get(dynamo_type) + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + state=dict(default='present', choices=['present', 'absent']), + name=dict(required=True, type='str'), + hash_key_name=dict(required=True, type='str'), + hash_key_type=dict(default='STRING', type='str', choices=['STRING', 'NUMBER', 'BINARY']), + range_key_name=dict(type='str'), + range_key_type=dict(default='STRING', type='str', choices=['STRING', 'NUMBER', 'BINARY']), + read_capacity=dict(default=1, type='int'), + write_capacity=dict(default=1, type='int'), + )) + + module = AnsibleModule(argument_spec=argument_spec) + + region, ec2_url, aws_connect_params = get_aws_connection_info(module) + connection = boto.dynamodb2.connect_to_region(region) + + state = module.params.get('state') + if state == 'present': + create_or_update_dynamo_table(connection, module) + elif state == 'absent': + delete_dynamo_table(connection, module) + + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +main() From d1c896d31edf7bea35c02bd641555626b4caa79b Mon Sep 17 00:00:00 2001 From: Peter Mounce Date: Fri, 1 May 2015 21:17:34 +0100 Subject: [PATCH 0060/2522] win_scheduled_task module for windows Fledgling module to allow scheduled tasks to be managed. At present, I only need enabled/disabled support. There's lots of scope for more features. --- windows/win_scheduled_task.ps1 | 77 ++++++++++++++++++++++++++++++++++ windows/win_scheduled_task.py | 54 ++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 windows/win_scheduled_task.ps1 create mode 100644 windows/win_scheduled_task.py diff --git a/windows/win_scheduled_task.ps1 b/windows/win_scheduled_task.ps1 new file mode 100644 index 00000000000..2716ed32ea9 --- /dev/null +++ b/windows/win_scheduled_task.ps1 @@ -0,0 +1,77 @@ +#!powershell +# This file is part of Ansible +# +# Copyright 2015, Peter Mounce +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +$ErrorActionPreference = "Stop" + +# WANT_JSON +# POWERSHELL_COMMON + +$params = Parse-Args $args; +$result = New-Object PSObject; +Set-Attr $result "changed" $false; + +if ($params.name) +{ + $package = $params.name +} +else +{ + Fail-Json $result "missing required argument: name" +} +if ($params.state) +{ + $state = $params.state.ToString() + if (($state -ne 'Enabled') -and ($state -ne 'Disabled')) + { + Fail-Json $result "state is '$state'; must be 'Enabled' or 'Disabled'" + } +} +else +{ + $state = "Enabled" +} + + +try +{ + $tasks = Get-ScheduledTask -TaskPath $name + $tasks_needing_changing |? { $_.State -ne $state } + if ($tasks_needing_changing -eq $null) + { + if ($state -eq 'Disabled') + { + $tasks_needing_changing | Disable-ScheduledTask + } + elseif ($state -eq 'Enabled') + { + $tasks_needing_changing | Enable-ScheduledTask + } + Set-Attr $result "tasks_changed" ($tasks_needing_changing | foreach { $_.TaskPath + $_.TaskName }) + $result.changed = $true + } + else + { + Set-Attr $result "tasks_changed" @() + $result.changed = $false + } + Exit-Json $result; +} +catch +{ + Fail-Json $result $_.Exception.Message +} diff --git a/windows/win_scheduled_task.py b/windows/win_scheduled_task.py new file mode 100644 index 00000000000..ac353c14c0a --- /dev/null +++ b/windows/win_scheduled_task.py @@ -0,0 +1,54 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Peter Mounce +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# this is a windows documentation stub. actual code lives in the .ps1 +# file of the same name + +DOCUMENTATION = ''' +--- +module: win_scheduled_task +version_added: "1.9" +short_description: Manage scheduled tasks +description: + - Manage scheduled tasks +options: + name: + description: + - Name of the scheduled task + - Supports * as wildcard + required: true + default: null + aliases: [] + state: + description: + - State that the task should become + required: false + choices: + - Disabled + - Enabled + default: Enabled + aliases: [] +author: Peter Mounce +''' + +EXAMPLES = ''' + # Disable the scheduled tasks with "WindowsUpdate" in their name + win_scheduled_task: name="*WindowsUpdate*" state=disabled +''' From 6f1d9fbbccea3c37f3ab672a544903297da311a5 Mon Sep 17 00:00:00 2001 From: Peter Mounce Date: Sat, 2 May 2015 13:56:01 +0100 Subject: [PATCH 0061/2522] correct variable name --- windows/win_scheduled_task.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_scheduled_task.ps1 b/windows/win_scheduled_task.ps1 index 2716ed32ea9..763bfb53862 100644 --- a/windows/win_scheduled_task.ps1 +++ b/windows/win_scheduled_task.ps1 @@ -27,7 +27,7 @@ Set-Attr $result "changed" $false; if ($params.name) { - $package = $params.name + $name = $params.name } else { From 4fef779f09b0d3b8d7fd7fa893d54c4fc09f2475 Mon Sep 17 00:00:00 2001 From: Peter Mounce Date: Sat, 2 May 2015 17:24:30 +0100 Subject: [PATCH 0062/2522] caught out by syntax --- windows/win_scheduled_task.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/windows/win_scheduled_task.ps1 b/windows/win_scheduled_task.ps1 index 763bfb53862..52b68dd5b6a 100644 --- a/windows/win_scheduled_task.ps1 +++ b/windows/win_scheduled_task.ps1 @@ -50,8 +50,8 @@ else try { $tasks = Get-ScheduledTask -TaskPath $name - $tasks_needing_changing |? { $_.State -ne $state } - if ($tasks_needing_changing -eq $null) + $tasks_needing_changing = $tasks |? { $_.State -ne $state } + if (-not($tasks_needing_changing -eq $null)) { if ($state -eq 'Disabled') { @@ -69,6 +69,7 @@ try Set-Attr $result "tasks_changed" @() $result.changed = $false } + Exit-Json $result; } catch From ede4820562423632610359c07623a158acf0282f Mon Sep 17 00:00:00 2001 From: Peter Mounce Date: Wed, 6 May 2015 21:47:39 +0100 Subject: [PATCH 0063/2522] version_added -> 2, remove empty aliases --- windows/win_scheduled_task.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/windows/win_scheduled_task.py b/windows/win_scheduled_task.py index ac353c14c0a..e755890b319 100644 --- a/windows/win_scheduled_task.py +++ b/windows/win_scheduled_task.py @@ -24,7 +24,7 @@ DOCUMENTATION = ''' --- module: win_scheduled_task -version_added: "1.9" +version_added: "2.0" short_description: Manage scheduled tasks description: - Manage scheduled tasks @@ -35,7 +35,6 @@ - Supports * as wildcard required: true default: null - aliases: [] state: description: - State that the task should become @@ -44,7 +43,6 @@ - Disabled - Enabled default: Enabled - aliases: [] author: Peter Mounce ''' From d9211b709b2f6a8bb46118ec3ae95907551c158f Mon Sep 17 00:00:00 2001 From: Peter Mounce Date: Wed, 6 May 2015 21:48:19 +0100 Subject: [PATCH 0064/2522] no default, remove it --- windows/win_scheduled_task.py | 1 - 1 file changed, 1 deletion(-) diff --git a/windows/win_scheduled_task.py b/windows/win_scheduled_task.py index e755890b319..7c604ecec20 100644 --- a/windows/win_scheduled_task.py +++ b/windows/win_scheduled_task.py @@ -34,7 +34,6 @@ - Name of the scheduled task - Supports * as wildcard required: true - default: null state: description: - State that the task should become From a4a3a1343953cf996b57bb6b91a55cdb6678ca12 Mon Sep 17 00:00:00 2001 From: Peter Mounce Date: Tue, 19 May 2015 11:21:23 +0100 Subject: [PATCH 0065/2522] Code-review Swap state enabled/disabled -> enabled yes/no --- windows/win_scheduled_task.ps1 | 22 +++++++++------------- windows/win_scheduled_task.py | 10 +++++----- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/windows/win_scheduled_task.ps1 b/windows/win_scheduled_task.ps1 index 52b68dd5b6a..2f802f59cd0 100644 --- a/windows/win_scheduled_task.ps1 +++ b/windows/win_scheduled_task.ps1 @@ -33,33 +33,29 @@ else { Fail-Json $result "missing required argument: name" } -if ($params.state) +if ($params.enabled) { - $state = $params.state.ToString() - if (($state -ne 'Enabled') -and ($state -ne 'Disabled')) - { - Fail-Json $result "state is '$state'; must be 'Enabled' or 'Disabled'" - } + $enabled = $params.enabled | ConvertTo-Bool } else { - $state = "Enabled" + $enabled = $true } - +$target_state = @{$true = "Enabled"; $false="Disabled"}[$enabled] try { $tasks = Get-ScheduledTask -TaskPath $name - $tasks_needing_changing = $tasks |? { $_.State -ne $state } + $tasks_needing_changing = $tasks |? { $_.State -ne $target_state } if (-not($tasks_needing_changing -eq $null)) { - if ($state -eq 'Disabled') + if ($enabled) { - $tasks_needing_changing | Disable-ScheduledTask + $tasks_needing_changing | Enable-ScheduledTask } - elseif ($state -eq 'Enabled') + else { - $tasks_needing_changing | Enable-ScheduledTask + $tasks_needing_changing | Disable-ScheduledTask } Set-Attr $result "tasks_changed" ($tasks_needing_changing | foreach { $_.TaskPath + $_.TaskName }) $result.changed = $true diff --git a/windows/win_scheduled_task.py b/windows/win_scheduled_task.py index 7c604ecec20..2c5867402c5 100644 --- a/windows/win_scheduled_task.py +++ b/windows/win_scheduled_task.py @@ -34,18 +34,18 @@ - Name of the scheduled task - Supports * as wildcard required: true - state: + enabled: description: - State that the task should become required: false choices: - - Disabled - - Enabled - default: Enabled + - yes + - no + default: yes author: Peter Mounce ''' EXAMPLES = ''' # Disable the scheduled tasks with "WindowsUpdate" in their name - win_scheduled_task: name="*WindowsUpdate*" state=disabled + win_scheduled_task: name="*WindowsUpdate*" enabled=no ''' From 0b0416aca1cd0fb8e37508a4b9c5915b17cf5423 Mon Sep 17 00:00:00 2001 From: Peter Mounce Date: Fri, 1 May 2015 20:28:56 +0100 Subject: [PATCH 0066/2522] Create win_dotnet_ngen module When .NET is installed or updated, ngen is triggered to optimise the installation. This triggers high CPU while it's happening, and usually happens at an inconvenient time. This allows you to trigger it when you like. Full details and background in doc. I don't know a way to figure out whether this is required without actually running it. --- windows/win_dotnet_ngen.ps1 | 69 +++++++++++++++++++++++++++++++++++++ windows/win_dotnet_ngen.py | 43 +++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 windows/win_dotnet_ngen.ps1 create mode 100644 windows/win_dotnet_ngen.py diff --git a/windows/win_dotnet_ngen.ps1 b/windows/win_dotnet_ngen.ps1 new file mode 100644 index 00000000000..52b4ebf82d5 --- /dev/null +++ b/windows/win_dotnet_ngen.ps1 @@ -0,0 +1,69 @@ +#!powershell +# This file is part of Ansible +# +# Copyright 2015, Peter Mounce +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +$ErrorActionPreference = "Stop" + +# WANT_JSON +# POWERSHELL_COMMON + +$params = Parse-Args $args; +$result = New-Object PSObject; +Set-Attr $result "changed" $false; + +function Invoke-NGen +{ + [CmdletBinding()] + + param + ( + [Parameter(Mandatory=$false, Position=0)] [string] $arity = "" + ) + + if ($arity -eq $null) + { + $arity = "" + } + $cmd = "$($env:windir)\microsoft.net\framework$($arity)\v4.0.30319\ngen.exe" + if (test-path $cmd) + { + $update = Invoke-Expression "$cmd update /force"; + Set-Attr $result "dotnet_ngen$($arity)_update_exit_code" $lastexitcode + Set-Attr $result "dotnet_ngen$($arity)_update_output" $update + $eqi = Invoke-Expression "$cmd executequeueditems"; + Set-Attr $result "dotnet_ngen$($arity)_eqi_exit_code" $lastexitcode + Set-Attr $result "dotnet_ngen$($arity)_eqi_output" $eqi + + $result.changed = $true + } + else + { + Write-Host "Not found: $cmd" + } +} + +Try +{ + Invoke-NGen + Invoke-NGen -arity "64" + + Exit-Json $result; +} +Catch +{ + Fail-Json $result $_.Exception.Message +} diff --git a/windows/win_dotnet_ngen.py b/windows/win_dotnet_ngen.py new file mode 100644 index 00000000000..d3dc4622e7c --- /dev/null +++ b/windows/win_dotnet_ngen.py @@ -0,0 +1,43 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Peter Mounce +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# this is a windows documentation stub. actual code lives in the .ps1 +# file of the same name + +DOCUMENTATION = ''' +--- +module: win_dotnet_ngen +version_added: "1.9" +short_description: Runs ngen to recompile DLLs after .NET updates +description: + - After .NET framework is installed/updated, Windows will probably want to recompile things to optimise for the host. + - This happens via scheduled task, usually at some inopportune time. + - This module allows you to run this task on your own schedule, so you incur the CPU hit at some more convenient and controlled time. + - http://blogs.msdn.com/b/dotnet/archive/2013/08/06/wondering-why-mscorsvw-exe-has-high-cpu-usage-you-can-speed-it-up.aspx + - Note: there are in fact two scheduled tasks for ngen but they have no triggers so aren't a problem + - Note: there's no way to test if they've been completed (?) +options: +author: Peter Mounce +''' + +EXAMPLES = ''' + # Run ngen tasks + win_dotnet_ngen: +''' From ddb85479cd79417ff0df345a9fae20ffb84b21b0 Mon Sep 17 00:00:00 2001 From: Peter Mounce Date: Sat, 2 May 2015 13:33:38 +0100 Subject: [PATCH 0067/2522] Add documentation note --- windows/win_dotnet_ngen.py | 1 + 1 file changed, 1 insertion(+) diff --git a/windows/win_dotnet_ngen.py b/windows/win_dotnet_ngen.py index d3dc4622e7c..c6c1c6de069 100644 --- a/windows/win_dotnet_ngen.py +++ b/windows/win_dotnet_ngen.py @@ -33,6 +33,7 @@ - http://blogs.msdn.com/b/dotnet/archive/2013/08/06/wondering-why-mscorsvw-exe-has-high-cpu-usage-you-can-speed-it-up.aspx - Note: there are in fact two scheduled tasks for ngen but they have no triggers so aren't a problem - Note: there's no way to test if they've been completed (?) + - Note: the stdout is quite likely to be several megabytes options: author: Peter Mounce ''' From 52a87e16379edf21c46bf45725694cad4c5a91f5 Mon Sep 17 00:00:00 2001 From: Peter Mounce Date: Wed, 6 May 2015 21:51:05 +0100 Subject: [PATCH 0068/2522] version -> 2, quote doc strings with colons --- windows/win_dotnet_ngen.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/windows/win_dotnet_ngen.py b/windows/win_dotnet_ngen.py index c6c1c6de069..5f6d45062d6 100644 --- a/windows/win_dotnet_ngen.py +++ b/windows/win_dotnet_ngen.py @@ -24,15 +24,15 @@ DOCUMENTATION = ''' --- module: win_dotnet_ngen -version_added: "1.9" +version_added: "2.0" short_description: Runs ngen to recompile DLLs after .NET updates description: - After .NET framework is installed/updated, Windows will probably want to recompile things to optimise for the host. - This happens via scheduled task, usually at some inopportune time. - This module allows you to run this task on your own schedule, so you incur the CPU hit at some more convenient and controlled time. - - http://blogs.msdn.com/b/dotnet/archive/2013/08/06/wondering-why-mscorsvw-exe-has-high-cpu-usage-you-can-speed-it-up.aspx - - Note: there are in fact two scheduled tasks for ngen but they have no triggers so aren't a problem - - Note: there's no way to test if they've been completed (?) + - "http://blogs.msdn.com/b/dotnet/archive/2013/08/06/wondering-why-mscorsvw-exe-has-high-cpu-usage-you-can-speed-it-up.aspx" + - "Note: there are in fact two scheduled tasks for ngen but they have no triggers so aren't a problem" + - "Note: there's no way to test if they've been completed (?)" - Note: the stdout is quite likely to be several megabytes options: author: Peter Mounce From c32262732013e2177dd4779be4984ffb580dea9e Mon Sep 17 00:00:00 2001 From: Peter Mounce Date: Sat, 9 May 2015 11:01:46 +0100 Subject: [PATCH 0069/2522] use the notes property --- windows/win_dotnet_ngen.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/windows/win_dotnet_ngen.py b/windows/win_dotnet_ngen.py index 5f6d45062d6..90f464a1b0b 100644 --- a/windows/win_dotnet_ngen.py +++ b/windows/win_dotnet_ngen.py @@ -31,9 +31,10 @@ - This happens via scheduled task, usually at some inopportune time. - This module allows you to run this task on your own schedule, so you incur the CPU hit at some more convenient and controlled time. - "http://blogs.msdn.com/b/dotnet/archive/2013/08/06/wondering-why-mscorsvw-exe-has-high-cpu-usage-you-can-speed-it-up.aspx" - - "Note: there are in fact two scheduled tasks for ngen but they have no triggers so aren't a problem" - - "Note: there's no way to test if they've been completed (?)" - - Note: the stdout is quite likely to be several megabytes +notes: + - there are in fact two scheduled tasks for ngen but they have no triggers so aren't a problem + - there's no way to test if they've been completed (?) + - the stdout is quite likely to be several megabytes options: author: Peter Mounce ''' From ec172be310168de9f8775fb85c36dc3fb0d92bf6 Mon Sep 17 00:00:00 2001 From: Peter Mounce Date: Thu, 30 Apr 2015 16:53:48 +0100 Subject: [PATCH 0070/2522] win_webpicmd module for IIS module installation Chocolatey 0.9.9+ deprecated support for the `webpi` custom source, so I needed to write this. [Windows Web Platform Installer](http://www.microsoft.com/web/downloads/platform.aspx) is a way of installing products and applications for Microsoft IIS on Windows. It has a [command line](http://www.iis.net/learn/install/web-platform-installer/web-platform-installer-v4-command-line-webpicmdexe-rtw-release); this ansible module allows IIS modules to be installed via this means. To find out names of modules, use `webpicmd /list /listoption:available`. Notes: * `webpicmd` must be installed and on `PATH` first (see `win_chocolatey` module; package is `webpicmd`) * `webpicmd` does not allow modules to be uninstalled * IIS must be installed first (see `win_feature` module; package is `Web-Server`) * Installations will * accept EULA (which otherwise requires user input) * suppress reboots (so you have to manage those; see `win_reboot` module) --- windows/win_webpicmd.ps1 | 135 +++++++++++++++++++++++++++++++++++++++ windows/win_webpicmd.py | 48 ++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 windows/win_webpicmd.ps1 create mode 100644 windows/win_webpicmd.py diff --git a/windows/win_webpicmd.ps1 b/windows/win_webpicmd.ps1 new file mode 100644 index 00000000000..377edcdc3c8 --- /dev/null +++ b/windows/win_webpicmd.ps1 @@ -0,0 +1,135 @@ +#!powershell +# This file is part of Ansible +# +# Copyright 2015, Peter Mounce +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +$ErrorActionPreference = "Stop" + +# WANT_JSON +# POWERSHELL_COMMON + +$params = Parse-Args $args; +$result = New-Object PSObject; +Set-Attr $result "changed" $false; + +If ($params.name) +{ + $package = $params.name +} +Else +{ + Fail-Json $result "missing required argument: name" +} + +Function Find-Command +{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true, Position=0)] [string] $command + ) + $installed = get-command $command -erroraction Ignore + write-verbose "$installed" + if ($installed.length -gt 0) + { + return $installed[0] + } + return $null +} + +Function Find-WebPiCmd +{ + [CmdletBinding()] + param() + $p = Find-Command "webpicmd.exe" + if ($p -ne $null) + { + return $p + } + $a = Find-Command "c:\programdata\chocolatey\bin\webpicmd.exe" + if ($a -ne $null) + { + return $a + } + Throw "webpicmd.exe is not installed. It must be installed (use chocolatey)" +} + +Function Test-IsInstalledFromWebPI +{ + [CmdletBinding()] + + param( + [Parameter(Mandatory=$true, Position=0)] + [string]$package + ) + + $cmd = "$executable /list /listoption:installed" + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "webpicmd_error_cmd" $cmd + Set-Attr $result "webpicmd_error_log" "$results" + + Throw "Error checking installation status for $package" + } + Write-Verbose "$results" + + $matches = $results | select-string -pattern "^$package\s+" + return $matches.length -gt 0 +} + +Function Install-WithWebPICmd +{ + [CmdletBinding()] + + param( + [Parameter(Mandatory=$true, Position=0)] + [string]$package + ) + + $cmd = "$executable /install /products:$package /accepteula /suppressreboot" + + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "webpicmd_error_cmd" $cmd + Set-Attr $result "webpicmd_error_log" "$results" + Throw "Error installing $package" + } + + write-verbose "$results" + $success = $results | select-string -pattern "Install of Products: SUCCESS" + if ($success.length -gt 0) + { + $result.changed = $true + } +} + +Try +{ + $script:executable = Find-WebPiCmd + if ((Test-IsInstalledFromWebPI -package $package) -eq $false) + { + Install-WithWebPICmd -package $package + } + + Exit-Json $result; +} +Catch +{ + Fail-Json $result $_.Exception.Message +} diff --git a/windows/win_webpicmd.py b/windows/win_webpicmd.py new file mode 100644 index 00000000000..b41a47d16c2 --- /dev/null +++ b/windows/win_webpicmd.py @@ -0,0 +1,48 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2014, Trond Hindenes +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# this is a windows documentation stub. actual code lives in the .ps1 +# file of the same name + +DOCUMENTATION = ''' +--- +module: win_webpicmd +version_added: "1.9" +short_description: Installs packages using Web Platform Installer command-line +description: + - Installs packages using Web Platform Installer command-line (http://www.iis.net/learn/install/web-platform-installer/web-platform-installer-v4-command-line-webpicmdexe-rtw-release). + - Must be installed and present in PATH (see win_chocolatey module; 'webpicmd' is the package name, and you must install 'lessmsi' first too) + - Install IIS first (see win_feature module) + - Note: accepts EULAs and suppresses reboot - you will need to check manage reboots yourself (see win_reboot module) +options: + name: + description: + - Name of the package to be installed + required: true + default: null + aliases: [] +author: Peter Mounce +''' + +EXAMPLES = ''' + # Install URLRewrite2. + win_webpicmd: + name: URLRewrite2 +''' From dc347b06836b815ebf78d43f633021c26b62289a Mon Sep 17 00:00:00 2001 From: Peter Mounce Date: Fri, 1 May 2015 20:30:05 +0100 Subject: [PATCH 0071/2522] fix license boilerplate --- windows/win_webpicmd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_webpicmd.py b/windows/win_webpicmd.py index b41a47d16c2..e90db5bc411 100644 --- a/windows/win_webpicmd.py +++ b/windows/win_webpicmd.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# (c) 2014, Trond Hindenes +# (c) 2015, Peter Mounce # # This file is part of Ansible # From 8454997cd9287f0ee96c4852f44416fd181f56cb Mon Sep 17 00:00:00 2001 From: Peter Mounce Date: Wed, 6 May 2015 21:50:11 +0100 Subject: [PATCH 0072/2522] version -> 2, remove unnecessary doc attributes --- windows/win_webpicmd.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/windows/win_webpicmd.py b/windows/win_webpicmd.py index e90db5bc411..dc26e88bfba 100644 --- a/windows/win_webpicmd.py +++ b/windows/win_webpicmd.py @@ -24,7 +24,7 @@ DOCUMENTATION = ''' --- module: win_webpicmd -version_added: "1.9" +version_added: "2.0" short_description: Installs packages using Web Platform Installer command-line description: - Installs packages using Web Platform Installer command-line (http://www.iis.net/learn/install/web-platform-installer/web-platform-installer-v4-command-line-webpicmdexe-rtw-release). @@ -36,8 +36,6 @@ description: - Name of the package to be installed required: true - default: null - aliases: [] author: Peter Mounce ''' From f9be73f4a1333f50e00f231e9a8eb3e402fc0c76 Mon Sep 17 00:00:00 2001 From: Michal Svab Date: Fri, 22 May 2015 16:17:26 +0100 Subject: [PATCH 0073/2522] maven_artifact: check whether snapshot is the latest snapshot --- packaging/language/maven_artifact.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packaging/language/maven_artifact.py b/packaging/language/maven_artifact.py index d6dd33166dc..70e3dad2f75 100644 --- a/packaging/language/maven_artifact.py +++ b/packaging/language/maven_artifact.py @@ -309,7 +309,7 @@ def main(): repository_url = dict(default=None), username = dict(default=None), password = dict(default=None), - state = dict(default="present", choices=["present","absent"]), # TODO - Implement a "latest" state + state = dict(default="present", choices=["present","absent"]), # TODO - Implement a "latest" state dest = dict(default=None), ) ) @@ -339,7 +339,10 @@ def main(): if os.path.isdir(dest): dest = dest + "/" + artifact_id + "-" + version + "." + extension if os.path.lexists(dest): - prev_state = "present" + if not artifact.is_snapshot(): + prev_state = "present" + elif downloader.verify_md5(dest, downloader.find_uri_for_artifact(artifact) + '.md5'): + prev_state = "present" else: path = os.path.dirname(dest) if not os.path.exists(path): From 700b719fffcb5f852d95824930ee0cc9efe4df49 Mon Sep 17 00:00:00 2001 From: Dan Keder Date: Tue, 26 May 2015 16:37:32 +0200 Subject: [PATCH 0074/2522] seport.py: Add ability to specify multiple ports/port ranges It's way faster than running the module repeatedly. --- system/seport.py | 61 +++++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/system/seport.py b/system/seport.py index 0770b7e69ee..c1f81a7777c 100644 --- a/system/seport.py +++ b/system/seport.py @@ -25,9 +25,9 @@ - Manages SELinux network port type definitions. version_added: "1.7.1" options: - port: + ports: description: - - Port number or port range + - Ports or port ranges, separated by a comma required: true default: null proto: @@ -61,11 +61,11 @@ EXAMPLES = ''' # Allow Apache to listen on tcp port 8888 -- seport: port=8888 proto=tcp setype=http_port_t state=present +- seport: ports=8888 proto=tcp setype=http_port_t state=present # Allow sshd to listen on tcp port 8991 -- seport: port=8991 proto=tcp setype=ssh_port_t state=present -# Allow memcached to listen on tcp ports 10000-10100 -- seport: port=10000-10100 proto=tcp setype=memcache_port_t state=present +- seport: ports=8991 proto=tcp setype=ssh_port_t state=present +# Allow memcached to listen on tcp ports 10000-10100 and 10112 +- seport: ports=10000-10100,10112 proto=tcp setype=memcache_port_t state=present ''' try: @@ -104,14 +104,14 @@ def semanage_port_exists(seport, port, proto): return record in seport.get_all() -def semanage_port_add(module, port, proto, setype, do_reload, serange='s0', sestore=''): +def semanage_port_add(module, ports, proto, setype, do_reload, serange='s0', sestore=''): """ Add SELinux port type definition to the policy. :type module: AnsibleModule :param module: Ansible module - :type port: basestring - :param port: Port or port range to add (example: "8080", "8080-9090") + :type ports: list + :param ports: List of ports and port ranges to add (e.g. ["8080", "8080-9090"]) :type proto: basestring :param proto: Protocol ('tcp' or 'udp') @@ -133,10 +133,11 @@ def semanage_port_add(module, port, proto, setype, do_reload, serange='s0', sest """ try: seport = seobject.portRecords(sestore) - change = not semanage_port_exists(seport, port, proto) - if change and not module.check_mode: - seport.set_reload(do_reload) - seport.add(port, proto, serange, setype) + seport.set_reload(do_reload) + for port in ports: + change = not semanage_port_exists(seport, port, proto) + if change and not module.check_mode: + seport.add(port, proto, serange, setype) except ValueError as e: module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) @@ -152,14 +153,14 @@ def semanage_port_add(module, port, proto, setype, do_reload, serange='s0', sest return change -def semanage_port_del(module, port, proto, do_reload, sestore=''): +def semanage_port_del(module, ports, proto, do_reload, sestore=''): """ Delete SELinux port type definition from the policy. :type module: AnsibleModule :param module: Ansible module - :type port: basestring - :param port: Port or port range to delete (example: "8080", "8080-9090") + :type ports: list + :param ports: List of ports and port ranges to delete (e.g. ["8080", "8080-9090"]) :type proto: basestring :param proto: Protocol ('tcp' or 'udp') @@ -175,10 +176,11 @@ def semanage_port_del(module, port, proto, do_reload, sestore=''): """ try: seport = seobject.portRecords(sestore) - change = not semanage_port_exists(seport, port, proto) - if change and not module.check_mode: - seport.set_reload(do_reload) - seport.delete(port, proto) + seport.set_reload(do_reload) + for port in ports: + change = not semanage_port_exists(seport, port, proto) + if change and not module.check_mode: + seport.delete(port, proto) except ValueError as e: module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) @@ -197,7 +199,7 @@ def semanage_port_del(module, port, proto, do_reload, sestore=''): def main(): module = AnsibleModule( argument_spec={ - 'port': { + 'ports': { 'required': True, }, 'proto': { @@ -228,22 +230,23 @@ def main(): if not selinux.is_selinux_enabled(): module.fail_json(msg="SELinux is disabled on this host.") - port = module.params['port'] + ports = [x.strip() for x in module.params['ports'].split(',')] proto = module.params['proto'] setype = module.params['setype'] state = module.params['state'] do_reload = module.params['reload'] - result = {} - result['port'] = port - result['proto'] = proto - result['setype'] = setype - result['state'] = state + result = { + 'ports': ports, + 'proto': proto, + 'setype': setype, + 'state': state, + } if state == 'present': - result['changed'] = semanage_port_add(module, port, proto, setype, do_reload) + result['changed'] = semanage_port_add(module, ports, proto, setype, do_reload) elif state == 'absent': - result['changed'] = semanage_port_del(module, port, proto, do_reload) + result['changed'] = semanage_port_del(module, ports, proto, do_reload) else: module.fail_json(msg='Invalid value of argument "state": {0}'.format(state)) From 504bf832b47c24e05197297753b0c069ccc04478 Mon Sep 17 00:00:00 2001 From: Dan Keder Date: Tue, 26 May 2015 16:37:56 +0200 Subject: [PATCH 0075/2522] seport.py: Minor changes in docstrings --- system/seport.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/system/seport.py b/system/seport.py index c1f81a7777c..3d6cb6170fa 100644 --- a/system/seport.py +++ b/system/seport.py @@ -87,10 +87,10 @@ def semanage_port_exists(seport, port, proto): :param seport: Instance of seobject.portRecords - :type port: basestring + :type port: str :param port: Port or port range (example: "8080", "8080-9090") - :type proto: basestring + :type proto: str :param proto: Protocol ('tcp' or 'udp') :rtype: bool @@ -113,19 +113,19 @@ def semanage_port_add(module, ports, proto, setype, do_reload, serange='s0', ses :type ports: list :param ports: List of ports and port ranges to add (e.g. ["8080", "8080-9090"]) - :type proto: basestring + :type proto: str :param proto: Protocol ('tcp' or 'udp') - :type setype: basestring + :type setype: str :param setype: SELinux type :type do_reload: bool :param do_reload: Whether to reload SELinux policy after commit - :type serange: basestring + :type serange: str :param serange: SELinux MLS/MCS range (defaults to 's0') - :type sestore: basestring + :type sestore: str :param sestore: SELinux store :rtype: bool @@ -162,13 +162,13 @@ def semanage_port_del(module, ports, proto, do_reload, sestore=''): :type ports: list :param ports: List of ports and port ranges to delete (e.g. ["8080", "8080-9090"]) - :type proto: basestring + :type proto: str :param proto: Protocol ('tcp' or 'udp') :type do_reload: bool :param do_reload: Whether to reload SELinux policy after commit - :type sestore: basestring + :type sestore: str :param sestore: SELinux store :rtype: bool From 9fe9f6e7e6fb66bbe70dcd4b1ab516751f7cbddd Mon Sep 17 00:00:00 2001 From: Dan Keder Date: Wed, 27 May 2015 16:30:49 +0200 Subject: [PATCH 0076/2522] seport.py: fix the "change" indication --- system/seport.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/system/seport.py b/system/seport.py index 3d6cb6170fa..c264334ae86 100644 --- a/system/seport.py +++ b/system/seport.py @@ -134,10 +134,12 @@ def semanage_port_add(module, ports, proto, setype, do_reload, serange='s0', ses try: seport = seobject.portRecords(sestore) seport.set_reload(do_reload) + change = False for port in ports: - change = not semanage_port_exists(seport, port, proto) - if change and not module.check_mode: + exists = semanage_port_exists(seport, port, proto) + if not exists and not module.check_mode: seport.add(port, proto, serange, setype) + change = change or not exists except ValueError as e: module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) @@ -177,10 +179,12 @@ def semanage_port_del(module, ports, proto, do_reload, sestore=''): try: seport = seobject.portRecords(sestore) seport.set_reload(do_reload) + change = False for port in ports: - change = not semanage_port_exists(seport, port, proto) - if change and not module.check_mode: + exists = semanage_port_exists(seport, port, proto) + if not exists and not module.check_mode: seport.delete(port, proto) + change = change or not exists except ValueError as e: module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) From 5b401cfcc30cb84dcf19a4c05b5a0791303d8378 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 28 May 2015 16:23:27 -0400 Subject: [PATCH 0077/2522] Add module to run puppet There is a growing pattern for using ansible to orchestrate runs of existing puppet code. For instance, the OpenStack Infrastructure team started using ansible for this very reason. It also turns out that successfully running puppet and interpreting success or failure is harder than you'd expect, thus warranting a module and not just a shell command. This is ported in from http://git.openstack.org/cgit/openstack-infra/ansible-puppet --- system/puppet.py | 186 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 system/puppet.py diff --git a/system/puppet.py b/system/puppet.py new file mode 100644 index 00000000000..c53c88f595d --- /dev/null +++ b/system/puppet.py @@ -0,0 +1,186 @@ +#!/usr/bin/python + +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + +import json +import os +import pipes + +DOCUMENTATION = ''' +--- +module: puppet +short_description: Runs puppet +description: + - Runs I(puppet) agent or apply in a reliable manner +version_added: "2.0" +options: + timeout: + description: + - How long to wait for I(puppet) to finish. + required: false + default: 30m + puppetmaster: + description: + - The hostname of the puppetmaster to contact. Must have this or manifest + required: false + default: None + manifest: + desciption: + - Path to the manifest file to run puppet apply on. Must have this or puppetmaster + required: false + default: None + show_diff: + description: + - Should puppet return diffs of changes applied. Defaults to off to avoid leaking secret changes by default. + required: false + default: no + choices: [ "yes", "no" ] + facts: + description: + - A dict of values to pass in as persistent external facter facts + required: false + default: None + facter_basename: + desciption: + - Basename of the facter output file + required: false + default: ansible +requirements: [ puppet ] +author: Monty Taylor +''' + +EXAMPLES = ''' +# Run puppet and fail if anything goes wrong +- puppet + +# Run puppet and timeout in 5 minutes +- puppet: timeout=5m +''' + + +def _get_facter_dir(): + if os.getuid() == 0: + return '/etc/facter/facts.d' + else: + return os.path.expanduser('~/.facter/facts.d') + + +def _write_structured_data(basedir, basename, data): + if not os.path.exists(basedir): + os.makedirs(basedir) + file_path = os.path.join(basedir, "{0}.json".format(basename)) + with os.fdopen( + os.open(file_path, os.O_CREAT | os.O_WRONLY, 0o600), + 'w') as out_file: + out_file.write(json.dumps(data).encode('utf8')) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + timeout=dict(default="30m"), + puppetmaster=dict(required=False, default=None), + manifest=dict(required=False, default=None), + show_diff=dict( + default=False, aliases=['show-diff'], type='bool'), + facts=dict(default=None), + facter_basename=dict(default='ansible'), + ), + required_one_of=[ + ('puppetmaster', 'manifest'), + ], + ) + p = module.params + + global PUPPET_CMD + PUPPET_CMD = module.get_bin_path("puppet", False) + + if not PUPPET_CMD: + module.fail_json( + msg="Could not find puppet. Please ensure it is installed.") + + if p['manifest']: + if not os.path.exists(p['manifest']): + module.fail_json( + msg="Manifest file %(manifest)s not found." % dict( + manifest=p['manifest']) + + # Check if puppet is disabled here + if p['puppetmaster']: + rc, stdout, stderr = module.run_command( + PUPPET_CMD + "config print agent_disabled_lockfile") + if os.path.exists(stdout.strip()): + module.fail_json( + msg="Puppet agent is administratively disabled.", disabled=True) + elif rc != 0: + module.fail_json( + msg="Puppet agent state could not be determined.") + + if module.params['facts']: + _write_structured_data( + _get_facter_dir(), + module.params['facter_basename'], + module.params['facts']) + + base_cmd = "timeout -s 9 %(timeout)s %(puppet_cmd)s" % dict( + timeout=pipes.quote(p['timeout']), puppet_cmd=PUPPET_CMD) + + if p['puppetmaster']: + cmd = ("%(base_cmd) agent --onetime" + " --server %(puppetmaster)s" + " --ignorecache --no-daemonize --no-usecacheonfailure --no-splay" + " --detailed-exitcodes --verbose") % dict( + base_cmd=base_cmd, + puppetmaster=pipes.quote(p['puppetmaster'])) + if p['show_diff']: + cmd += " --show-diff" + else: + cmd = ("%(base_cmd) apply --detailed-exitcodes %(manifest)s" % dict( + base_cmd=base_cmd, + manifest=pipes.quote(p['manifest'])) + rc, stdout, stderr = module.run_command(cmd) + + if rc == 0: + # success + module.exit_json(rc=rc, changed=False, stdout=stdout) + elif rc == 1: + # rc==1 could be because it's disabled + # rc==1 could also mean there was a compilation failure + disabled = "administratively disabled" in stdout + if disabled: + msg = "puppet is disabled" + else: + msg = "puppet did not run" + module.exit_json( + rc=rc, disabled=disabled, msg=msg, + error=True, stdout=stdout, stderr=stderr) + elif rc == 2: + # success with changes + module.exit_json(rc=0, changed=True) + elif rc == 124: + # timeout + module.exit_json( + rc=rc, msg="%s timed out" % cmd, stdout=stdout, stderr=stderr) + else: + # failure + module.fail_json( + rc=rc, msg="%s failed with return code: %d" % (cmd, rc), + stdout=stdout, stderr=stderr) + +# import module snippets +from ansible.module_utils.basic import * + +main() From 1605b1ec9cb7746dada8006fe317999511ac46cc Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 29 May 2015 07:00:30 -0400 Subject: [PATCH 0078/2522] Fix some errors pointed out by travis --- system/puppet.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/system/puppet.py b/system/puppet.py index c53c88f595d..57c76eeec9f 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -82,10 +82,10 @@ def _write_structured_data(basedir, basename, data): if not os.path.exists(basedir): os.makedirs(basedir) file_path = os.path.join(basedir, "{0}.json".format(basename)) - with os.fdopen( - os.open(file_path, os.O_CREAT | os.O_WRONLY, 0o600), - 'w') as out_file: - out_file.write(json.dumps(data).encode('utf8')) + out_file = os.fdopen( + os.open(file_path, os.O_CREAT | os.O_WRONLY, 0o600), 'w') + out_file.write(json.dumps(data).encode('utf8')) + out_file.close() def main(): @@ -116,7 +116,7 @@ def main(): if not os.path.exists(p['manifest']): module.fail_json( msg="Manifest file %(manifest)s not found." % dict( - manifest=p['manifest']) + manifest=p['manifest'])) # Check if puppet is disabled here if p['puppetmaster']: @@ -149,8 +149,8 @@ def main(): cmd += " --show-diff" else: cmd = ("%(base_cmd) apply --detailed-exitcodes %(manifest)s" % dict( - base_cmd=base_cmd, - manifest=pipes.quote(p['manifest'])) + base_cmd=base_cmd, + manifest=pipes.quote(p['manifest']))) rc, stdout, stderr = module.run_command(cmd) if rc == 0: From 12c945388b0ffa37aecc7b7f33fb11b41b82f309 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 29 May 2015 07:06:15 -0400 Subject: [PATCH 0079/2522] Add support for check mode --- system/puppet.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/system/puppet.py b/system/puppet.py index 57c76eeec9f..d6bc4348375 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -99,6 +99,7 @@ def main(): facts=dict(default=None), facter_basename=dict(default='ansible'), ), + supports_check_mode=True, required_one_of=[ ('puppetmaster', 'manifest'), ], @@ -129,7 +130,7 @@ def main(): module.fail_json( msg="Puppet agent state could not be determined.") - if module.params['facts']: + if module.params['facts'] and not module.check_mode: _write_structured_data( _get_facter_dir(), module.params['facter_basename'], @@ -139,7 +140,7 @@ def main(): timeout=pipes.quote(p['timeout']), puppet_cmd=PUPPET_CMD) if p['puppetmaster']: - cmd = ("%(base_cmd) agent --onetime" + cmd = ("%(base_cmd)s agent --onetime" " --server %(puppetmaster)s" " --ignorecache --no-daemonize --no-usecacheonfailure --no-splay" " --detailed-exitcodes --verbose") % dict( @@ -147,10 +148,13 @@ def main(): puppetmaster=pipes.quote(p['puppetmaster'])) if p['show_diff']: cmd += " --show-diff" + if module.check_mode: + cmd += " --noop" else: - cmd = ("%(base_cmd) apply --detailed-exitcodes %(manifest)s" % dict( - base_cmd=base_cmd, - manifest=pipes.quote(p['manifest']))) + cmd = "%s apply --detailed-exitcodes " % base_cmd + if module.check_mode: + cmd += "--noop " + cmd += pipes.quote(p['manifest']) rc, stdout, stderr = module.run_command(cmd) if rc == 0: From 8b6001c3da53553688f218e6b11c84c0c705c2a2 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 29 May 2015 08:09:31 -0400 Subject: [PATCH 0080/2522] Fix octal values for python 2.4 --- system/puppet.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/system/puppet.py b/system/puppet.py index d6bc4348375..46a5ea58d4f 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -18,6 +18,7 @@ import json import os import pipes +import stat DOCUMENTATION = ''' --- @@ -82,8 +83,13 @@ def _write_structured_data(basedir, basename, data): if not os.path.exists(basedir): os.makedirs(basedir) file_path = os.path.join(basedir, "{0}.json".format(basename)) + # This is more complex than you might normally expect because we want to + # open the file with only u+rw set. Also, we use the stat constants + # because ansible still supports python 2.4 and the octal syntax changed out_file = os.fdopen( - os.open(file_path, os.O_CREAT | os.O_WRONLY, 0o600), 'w') + os.open( + file_path, os.O_CREAT | os.O_WRONLY, + stat.S_IRUSR | stat.S_IWUSR), 'w') out_file.write(json.dumps(data).encode('utf8')) out_file.close() From fb8c2ee8109410a04f65f94452d3134f6ba3fca8 Mon Sep 17 00:00:00 2001 From: Michael Perzel Date: Fri, 29 May 2015 14:19:38 -0500 Subject: [PATCH 0081/2522] Module for managing F5 wide ip --- network/f5/bigip_gtm_wide_ip.py | 178 ++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 network/f5/bigip_gtm_wide_ip.py diff --git a/network/f5/bigip_gtm_wide_ip.py b/network/f5/bigip_gtm_wide_ip.py new file mode 100644 index 00000000000..1b3a3ed48e5 --- /dev/null +++ b/network/f5/bigip_gtm_wide_ip.py @@ -0,0 +1,178 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2015, Michael Perzel +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: bigip_gtm_wide_ip +short_description: "Manages F5 BIG-IP GTM wide ip" +description: + - "Manages F5 BIG-IP GTM wide ip" +version_added: "1.9" +author: 'Michael Perzel' +notes: + - "Requires BIG-IP software version >= 11.4" + - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" + - "Best run as a local_action in your playbook" + - "Tested with manager and above account privilege level" + +requirements: + - bigsuds +options: + server: + description: + - BIG-IP host + required: true + default: null + choices: [] + aliases: [] + user: + description: + - BIG-IP username + required: true + default: null + choices: [] + aliases: [] + password: + description: + - BIG-IP password + required: true + default: null + choices: [] + aliases: [] + lb_method: + description: + - LB method of wide ip + required: true + choices: ['return_to_dns', 'null', 'round_robin', + 'ratio', 'topology', 'static_persist', 'global_availability', + 'vs_capacity', 'least_conn', 'lowest_rtt', 'lowest_hops', + 'packet_rate', 'cpu', 'hit_ratio', 'qos', 'bps', + 'drop_packet', 'explicit_ip', 'connection_rate', 'vs_score'] + aliases: [] + wide_ip: + description: + - Wide IP name + required: true + default: null + aliases: [] +''' + +EXAMPLES = ''' + - name: Set lb method + local_action: > + bigip_gtm_wide_ip + server=192.168.0.1 + user=admin + password=mysecret + lb_method=round_robin + wide_ip=my_wide_ip +''' + +try: + import bigsuds +except ImportError: + bigsuds_found = False +else: + bigsuds_found = True + +def bigip_api(bigip, user, password): + api = bigsuds.BIGIP(hostname=bigip, username=user, password=password) + return api + +def get_wide_ip_lb_method(api, wide_ip): + lb_method = api.GlobalLB.WideIP.get_lb_method(wide_ips=[wide_ip])[0] + lb_method = lb_method.strip().replace('LB_METHOD_', '').lower() + return lb_method + +def get_wide_ip_pools(api, wide_ip): + try: + return api.GlobalLB.WideIP.get_wideip_pool([wide_ip]) + except Exception, e: + print e + +def wide_ip_exists(api, wide_ip): + # hack to determine if wide_ip exists + result = False + try: + api.GlobalLB.WideIP.get_object_status(wide_ips=[wide_ip]) + result = True + except bigsuds.OperationFailed, e: + if "was not found" in str(e): + result = False + else: + # genuine exception + raise + return result + +def set_wide_ip_lb_method(api, wide_ip, lb_method): + lb_method = "LB_METHOD_%s" % lb_method.strip().upper() + api.GlobalLB.WideIP.set_lb_method(wide_ips=[wide_ip], lb_methods=[lb_method]) + +def main(): + + lb_method_choices = ['return_to_dns', 'null', 'round_robin', + 'ratio', 'topology', 'static_persist', 'global_availability', + 'vs_capacity', 'least_conn', 'lowest_rtt', 'lowest_hops', + 'packet_rate', 'cpu', 'hit_ratio', 'qos', 'bps', + 'drop_packet', 'explicit_ip', 'connection_rate', 'vs_score'] + + module = AnsibleModule( + argument_spec = dict( + server = dict(type='str', required=True), + user = dict(type='str', required=True), + password = dict(type='str', required=True), + lb_method = dict(type='str', required=True, choices=lb_method_choices), + wide_ip = dict(type='str', required=True) + ), + supports_check_mode=True + ) + + if not bigsuds_found: + module.fail_json(msg="the python bigsuds module is required") + + server = module.params['server'] + user = module.params['user'] + password = module.params['password'] + wide_ip = module.params['wide_ip'] + lb_method = module.params['lb_method'] + + result = {'changed': False} # default + + try: + api = bigip_api(server, user, password) + + if not wide_ip_exists(api, wide_ip): + module.fail_json(msg="wide ip %s does not exist" % wide_ip) + + if lb_method is not None and lb_method != get_wide_ip_lb_method(api, wide_ip): + if not module.check_mode: + set_wide_ip_lb_method(api, wide_ip, lb_method) + result = {'changed': True} + else: + result = {'changed': True} + + except Exception, e: + module.fail_json(msg="received exception: %s" % e) + + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * +main() From 408214d1a886cd41eb73d3bd35385bed9a44838e Mon Sep 17 00:00:00 2001 From: Michael Perzel Date: Mon, 1 Jun 2015 11:22:40 -0500 Subject: [PATCH 0082/2522] Style updates --- network/f5/bigip_gtm_wide_ip.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/network/f5/bigip_gtm_wide_ip.py b/network/f5/bigip_gtm_wide_ip.py index 1b3a3ed48e5..f201e5d9760 100644 --- a/network/f5/bigip_gtm_wide_ip.py +++ b/network/f5/bigip_gtm_wide_ip.py @@ -24,7 +24,7 @@ short_description: "Manages F5 BIG-IP GTM wide ip" description: - "Manages F5 BIG-IP GTM wide ip" -version_added: "1.9" +version_added: "2.0" author: 'Michael Perzel' notes: - "Requires BIG-IP software version >= 11.4" @@ -39,23 +39,14 @@ description: - BIG-IP host required: true - default: null - choices: [] - aliases: [] user: description: - BIG-IP username required: true - default: null - choices: [] - aliases: [] password: description: - BIG-IP password required: true - default: null - choices: [] - aliases: [] lb_method: description: - LB method of wide ip @@ -65,13 +56,10 @@ 'vs_capacity', 'least_conn', 'lowest_rtt', 'lowest_hops', 'packet_rate', 'cpu', 'hit_ratio', 'qos', 'bps', 'drop_packet', 'explicit_ip', 'connection_rate', 'vs_score'] - aliases: [] wide_ip: description: - Wide IP name required: true - default: null - aliases: [] ''' EXAMPLES = ''' From 858f9e3601f58dc16ede3056dc4aeff4f8da7cb0 Mon Sep 17 00:00:00 2001 From: Kevin Carter Date: Mon, 1 Jun 2015 15:31:56 -0500 Subject: [PATCH 0083/2522] Updates the doc information for the python2-lxc dep The python2-lxc library has been uploaded to pypi as such this commit updates the requirements and doc information for the module such that it instructs the user to install the pip package "lxc-python2" while also noting that the package could be gotten from source as well. In the update comments have been added to the requirements list which notes where the package should come from, Closes-Bug: https://github.com/ansible/ansible-modules-extras/issues/550 --- cloud/lxc/lxc_container.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index 119d45069c3..15d76df79a0 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -173,9 +173,9 @@ - list of 'key=value' options to use when configuring a container. required: false requirements: - - 'lxc >= 1.0' - - 'python >= 2.6' - - 'python2-lxc >= 0.1' + - 'lxc >= 1.0 # OS package' + - 'python >= 2.6 # OS Package' + - 'lxc-python2 >= 0.1 # PIP Package from https://github.com/lxc/python2-lxc' notes: - Containers must have a unique name. If you attempt to create a container with a name that already exists in the users namespace the module will @@ -195,7 +195,8 @@ creating the archive. - If your distro does not have a package for "python2-lxc", which is a requirement for this module, it can be installed from source at - "https://github.com/lxc/python2-lxc" + "https://github.com/lxc/python2-lxc" or installed via pip using the package + name lxc-python2. """ EXAMPLES = """ From 1c3afeadfc3a68173b652a8c0bf08646cf3ac1ab Mon Sep 17 00:00:00 2001 From: Quentin Stafford-Fraser Date: Tue, 2 Jun 2015 09:25:55 +0100 Subject: [PATCH 0084/2522] Add GPL notices --- cloud/webfaction/webfaction_app.py | 21 ++++++++++++++++++++- cloud/webfaction/webfaction_db.py | 23 +++++++++++++++++++++-- cloud/webfaction/webfaction_domain.py | 21 ++++++++++++++++++++- cloud/webfaction/webfaction_mailbox.py | 20 +++++++++++++++++++- cloud/webfaction/webfaction_site.py | 21 ++++++++++++++++++++- 5 files changed, 100 insertions(+), 6 deletions(-) diff --git a/cloud/webfaction/webfaction_app.py b/cloud/webfaction/webfaction_app.py index 20e94a7b5f6..55599bdcca6 100644 --- a/cloud/webfaction/webfaction_app.py +++ b/cloud/webfaction/webfaction_app.py @@ -1,10 +1,29 @@ #! /usr/bin/python +# # Create a Webfaction application using Ansible and the Webfaction API # # Valid application types can be found by looking here: # http://docs.webfaction.com/xmlrpc-api/apps.html#application-types # -# Quentin Stafford-Fraser 2015 +# ------------------------------------------ +# +# (c) Quentin Stafford-Fraser 2015 +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# DOCUMENTATION = ''' --- diff --git a/cloud/webfaction/webfaction_db.py b/cloud/webfaction/webfaction_db.py index 784477c5409..a9ef88b943e 100644 --- a/cloud/webfaction/webfaction_db.py +++ b/cloud/webfaction/webfaction_db.py @@ -1,7 +1,26 @@ #! /usr/bin/python -# Create webfaction database using Ansible and the Webfaction API # -# Quentin Stafford-Fraser 2015 +# Create a webfaction database using Ansible and the Webfaction API +# +# ------------------------------------------ +# +# (c) Quentin Stafford-Fraser 2015 +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# DOCUMENTATION = ''' --- diff --git a/cloud/webfaction/webfaction_domain.py b/cloud/webfaction/webfaction_domain.py index c99a0f23f6d..f2c95897bc5 100644 --- a/cloud/webfaction/webfaction_domain.py +++ b/cloud/webfaction/webfaction_domain.py @@ -1,7 +1,26 @@ #! /usr/bin/python +# # Create Webfaction domains and subdomains using Ansible and the Webfaction API # -# Quentin Stafford-Fraser 2015 +# ------------------------------------------ +# +# (c) Quentin Stafford-Fraser 2015 +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# DOCUMENTATION = ''' --- diff --git a/cloud/webfaction/webfaction_mailbox.py b/cloud/webfaction/webfaction_mailbox.py index 87ca1fd1a26..976a428f3d3 100644 --- a/cloud/webfaction/webfaction_mailbox.py +++ b/cloud/webfaction/webfaction_mailbox.py @@ -1,7 +1,25 @@ #! /usr/bin/python +# # Create webfaction mailbox using Ansible and the Webfaction API # -# Quentin Stafford-Fraser and Andy Baker 2015 +# ------------------------------------------ +# (c) Quentin Stafford-Fraser and Andy Baker 2015 +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# DOCUMENTATION = ''' --- diff --git a/cloud/webfaction/webfaction_site.py b/cloud/webfaction/webfaction_site.py index a5be4f5407b..223458faf46 100644 --- a/cloud/webfaction/webfaction_site.py +++ b/cloud/webfaction/webfaction_site.py @@ -1,7 +1,26 @@ #! /usr/bin/python +# # Create Webfaction website using Ansible and the Webfaction API # -# Quentin Stafford-Fraser 2015 +# ------------------------------------------ +# +# (c) Quentin Stafford-Fraser 2015 +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# DOCUMENTATION = ''' --- From a0905a9d5ecf9101288080324483fb8ca56f87ba Mon Sep 17 00:00:00 2001 From: Etienne CARRIERE Date: Wed, 3 Jun 2015 08:22:18 +0200 Subject: [PATCH 0085/2522] Factor common functions for F5 modules --- network/f5/bigip_monitor_http.py | 61 ++++++------------------------ network/f5/bigip_monitor_tcp.py | 64 +++++++------------------------- network/f5/bigip_node.py | 52 +++++--------------------- network/f5/bigip_pool.py | 56 ++++++---------------------- network/f5/bigip_pool_member.py | 54 ++++++--------------------- 5 files changed, 58 insertions(+), 229 deletions(-) diff --git a/network/f5/bigip_monitor_http.py b/network/f5/bigip_monitor_http.py index 6a31afb2ee7..5299bdb0f44 100644 --- a/network/f5/bigip_monitor_http.py +++ b/network/f5/bigip_monitor_http.py @@ -163,35 +163,10 @@ name: "{{ monitorname }}" ''' -try: - import bigsuds -except ImportError: - bigsuds_found = False -else: - bigsuds_found = True - TEMPLATE_TYPE = 'TTYPE_HTTP' DEFAULT_PARENT_TYPE = 'http' -# =========================================== -# bigip_monitor module generic methods. -# these should be re-useable for other monitor types -# - -def bigip_api(bigip, user, password): - - api = bigsuds.BIGIP(hostname=bigip, username=user, password=password) - return api - - -def disable_ssl_cert_validation(): - - # You probably only want to do this for testing and never in production. - # From https://www.python.org/dev/peps/pep-0476/#id29 - import ssl - ssl._create_default_https_context = ssl._create_unverified_context - def check_monitor_exists(module, api, monitor, parent): @@ -278,7 +253,6 @@ def set_integer_property(api, monitor, int_property): def update_monitor_properties(api, module, monitor, template_string_properties, template_integer_properties): - changed = False for str_property in template_string_properties: if str_property['value'] is not None and not check_string_property(api, monitor, str_property): @@ -321,15 +295,8 @@ def set_ipport(api, monitor, ipport): def main(): # begin monitor specific stuff - - module = AnsibleModule( - argument_spec = dict( - server = dict(required=True), - user = dict(required=True), - password = dict(required=True), - validate_certs = dict(default='yes', type='bool'), - partition = dict(default='Common'), - state = dict(default='present', choices=['present', 'absent']), + argument_spec=f5_argument_spec(); + argument_spec.update( dict( name = dict(required=True), parent = dict(default=DEFAULT_PARENT_TYPE), parent_partition = dict(default='Common'), @@ -341,20 +308,20 @@ def main(): interval = dict(required=False, type='int'), timeout = dict(required=False, type='int'), time_until_up = dict(required=False, type='int', default=0) - ), + ) + ) + + module = AnsibleModule( + argument_spec = argument_spec, supports_check_mode=True ) - server = module.params['server'] - user = module.params['user'] - password = module.params['password'] - validate_certs = module.params['validate_certs'] - partition = module.params['partition'] + (server,user,password,state,partition,validate_certs) = f5_parse_arguments(module) + parent_partition = module.params['parent_partition'] - state = module.params['state'] name = module.params['name'] - parent = "/%s/%s" % (parent_partition, module.params['parent']) - monitor = "/%s/%s" % (partition, name) + parent = fq_name(parent_partition, module.params['parent']) + monitor = fq_name(partition, name) send = module.params['send'] receive = module.params['receive'] receive_disable = module.params['receive_disable'] @@ -366,11 +333,6 @@ def main(): # end monitor specific stuff - if not validate_certs: - disable_ssl_cert_validation() - - if not bigsuds_found: - module.fail_json(msg="the python bigsuds module is required") api = bigip_api(server, user, password) monitor_exists = check_monitor_exists(module, api, monitor, parent) @@ -481,5 +443,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.f5 import * main() diff --git a/network/f5/bigip_monitor_tcp.py b/network/f5/bigip_monitor_tcp.py index d5855e0f15d..b5f58da8397 100644 --- a/network/f5/bigip_monitor_tcp.py +++ b/network/f5/bigip_monitor_tcp.py @@ -181,37 +181,11 @@ ''' -try: - import bigsuds -except ImportError: - bigsuds_found = False -else: - bigsuds_found = True - TEMPLATE_TYPE = DEFAULT_TEMPLATE_TYPE = 'TTYPE_TCP' TEMPLATE_TYPE_CHOICES = ['tcp', 'tcp_echo', 'tcp_half_open'] DEFAULT_PARENT = DEFAULT_TEMPLATE_TYPE_CHOICE = DEFAULT_TEMPLATE_TYPE.replace('TTYPE_', '').lower() -# =========================================== -# bigip_monitor module generic methods. -# these should be re-useable for other monitor types -# - -def bigip_api(bigip, user, password): - - api = bigsuds.BIGIP(hostname=bigip, username=user, password=password) - return api - - -def disable_ssl_cert_validation(): - - # You probably only want to do this for testing and never in production. - # From https://www.python.org/dev/peps/pep-0476/#id29 - import ssl - ssl._create_default_https_context = ssl._create_unverified_context - - def check_monitor_exists(module, api, monitor, parent): # hack to determine if monitor exists @@ -234,7 +208,7 @@ def check_monitor_exists(module, api, monitor, parent): def create_monitor(api, monitor, template_attributes): - try: + try: api.LocalLB.Monitor.create_template(templates=[{'template_name': monitor, 'template_type': TEMPLATE_TYPE}], template_attributes=[template_attributes]) except bigsuds.OperationFailed, e: if "already exists" in str(e): @@ -298,7 +272,6 @@ def set_integer_property(api, monitor, int_property): def update_monitor_properties(api, module, monitor, template_string_properties, template_integer_properties): - changed = False for str_property in template_string_properties: if str_property['value'] is not None and not check_string_property(api, monitor, str_property): @@ -341,15 +314,8 @@ def set_ipport(api, monitor, ipport): def main(): # begin monitor specific stuff - - module = AnsibleModule( - argument_spec = dict( - server = dict(required=True), - user = dict(required=True), - password = dict(required=True), - validate_certs = dict(default='yes', type='bool'), - partition = dict(default='Common'), - state = dict(default='present', choices=['present', 'absent']), + argument_spec=f5_argument_spec(); + argument_spec.update(dict( name = dict(required=True), type = dict(default=DEFAULT_TEMPLATE_TYPE_CHOICE, choices=TEMPLATE_TYPE_CHOICES), parent = dict(default=DEFAULT_PARENT), @@ -361,21 +327,21 @@ def main(): interval = dict(required=False, type='int'), timeout = dict(required=False, type='int'), time_until_up = dict(required=False, type='int', default=0) - ), + ) + ) + + module = AnsibleModule( + argument_spec = argument_spec, supports_check_mode=True ) - server = module.params['server'] - user = module.params['user'] - password = module.params['password'] - validate_certs = module.params['validate_certs'] - partition = module.params['partition'] + (server,user,password,state,partition,validate_certs) = f5_parse_arguments(module) + parent_partition = module.params['parent_partition'] - state = module.params['state'] name = module.params['name'] type = 'TTYPE_' + module.params['type'].upper() - parent = "/%s/%s" % (parent_partition, module.params['parent']) - monitor = "/%s/%s" % (partition, name) + parent = fq_name(parent_partition, module.params['parent']) + monitor = fq_name(partition, name) send = module.params['send'] receive = module.params['receive'] ip = module.params['ip'] @@ -390,11 +356,6 @@ def main(): # end monitor specific stuff - if not validate_certs: - disable_ssl_cert_validation() - - if not bigsuds_found: - module.fail_json(msg="the python bigsuds module is required") api = bigip_api(server, user, password) monitor_exists = check_monitor_exists(module, api, monitor, parent) @@ -506,5 +467,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.f5 import * main() diff --git a/network/f5/bigip_node.py b/network/f5/bigip_node.py index 31e34fdeb47..49f721aa8c5 100644 --- a/network/f5/bigip_node.py +++ b/network/f5/bigip_node.py @@ -188,27 +188,6 @@ ''' -try: - import bigsuds -except ImportError: - bigsuds_found = False -else: - bigsuds_found = True - -# ========================== -# bigip_node module specific -# - -def bigip_api(bigip, user, password): - api = bigsuds.BIGIP(hostname=bigip, username=user, password=password) - return api - -def disable_ssl_cert_validation(): - # You probably only want to do this for testing and never in production. - # From https://www.python.org/dev/peps/pep-0476/#id29 - import ssl - ssl._create_default_https_context = ssl._create_unverified_context - def node_exists(api, address): # hack to determine if node exists result = False @@ -283,42 +262,30 @@ def get_node_monitor_status(api, name): def main(): - module = AnsibleModule( - argument_spec = dict( - server = dict(type='str', required=True), - user = dict(type='str', required=True), - password = dict(type='str', required=True), - validate_certs = dict(default='yes', type='bool'), - state = dict(type='str', default='present', choices=['present', 'absent']), + argument_spec=f5_argument_spec(); + argument_spec.update(dict( session_state = dict(type='str', choices=['enabled', 'disabled']), monitor_state = dict(type='str', choices=['enabled', 'disabled']), - partition = dict(type='str', default='Common'), name = dict(type='str', required=True), host = dict(type='str', aliases=['address', 'ip']), description = dict(type='str') - ), + ) + ) + + module = AnsibleModule( + argument_spec = argument_spec, supports_check_mode=True ) - if not bigsuds_found: - module.fail_json(msg="the python bigsuds module is required") + (server,user,password,state,partition,validate_certs) = f5_parse_arguments(module) - server = module.params['server'] - user = module.params['user'] - password = module.params['password'] - validate_certs = module.params['validate_certs'] - state = module.params['state'] session_state = module.params['session_state'] monitor_state = module.params['monitor_state'] - partition = module.params['partition'] host = module.params['host'] name = module.params['name'] - address = "/%s/%s" % (partition, name) + address = fq_name(partition, name) description = module.params['description'] - if not validate_certs: - disable_ssl_cert_validation() - if state == 'absent' and host is not None: module.fail_json(msg="host parameter invalid when state=absent") @@ -410,5 +377,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.f5 import * main() diff --git a/network/f5/bigip_pool.py b/network/f5/bigip_pool.py index 2eaaf8f3a34..4d8d599134e 100644 --- a/network/f5/bigip_pool.py +++ b/network/f5/bigip_pool.py @@ -228,27 +228,6 @@ ''' -try: - import bigsuds -except ImportError: - bigsuds_found = False -else: - bigsuds_found = True - -# =========================================== -# bigip_pool module specific support methods. -# - -def bigip_api(bigip, user, password): - api = bigsuds.BIGIP(hostname=bigip, username=user, password=password) - return api - -def disable_ssl_cert_validation(): - # You probably only want to do this for testing and never in production. - # From https://www.python.org/dev/peps/pep-0476/#id29 - import ssl - ssl._create_default_https_context = ssl._create_unverified_context - def pool_exists(api, pool): # hack to determine if pool exists result = False @@ -368,15 +347,9 @@ def main(): service_down_choices = ['none', 'reset', 'drop', 'reselect'] - module = AnsibleModule( - argument_spec = dict( - server = dict(type='str', required=True), - user = dict(type='str', required=True), - password = dict(type='str', required=True), - validate_certs = dict(default='yes', type='bool'), - state = dict(type='str', default='present', choices=['present', 'absent']), + argument_spec=f5_argument_spec(); + argument_spec.update(dict( name = dict(type='str', required=True, aliases=['pool']), - partition = dict(type='str', default='Common'), lb_method = dict(type='str', choices=lb_method_choices), monitor_type = dict(type='str', choices=monitor_type_choices), quorum = dict(type='int'), @@ -385,21 +358,18 @@ def main(): service_down_action = dict(type='str', choices=service_down_choices), host = dict(type='str', aliases=['address']), port = dict(type='int') - ), + ) + ) + + module = AnsibleModule( + argument_spec = argument_spec, supports_check_mode=True ) - if not bigsuds_found: - module.fail_json(msg="the python bigsuds module is required") + (server,user,password,state,partition,validate_certs) = f5_parse_arguments(module) - server = module.params['server'] - user = module.params['user'] - password = module.params['password'] - validate_certs = module.params['validate_certs'] - state = module.params['state'] name = module.params['name'] - partition = module.params['partition'] - pool = "/%s/%s" % (partition, name) + pool = fq_name(partition,name) lb_method = module.params['lb_method'] if lb_method: lb_method = lb_method.lower() @@ -411,16 +381,13 @@ def main(): if monitors: monitors = [] for monitor in module.params['monitors']: - if "/" not in monitor: - monitors.append("/%s/%s" % (partition, monitor)) - else: - monitors.append(monitor) + monitors.append(fq_name(partition, monitor)) slow_ramp_time = module.params['slow_ramp_time'] service_down_action = module.params['service_down_action'] if service_down_action: service_down_action = service_down_action.lower() host = module.params['host'] - address = "/%s/%s" % (partition, host) + address = fq_name(partition,host) port = module.params['port'] if not validate_certs: @@ -551,5 +518,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.f5 import * main() diff --git a/network/f5/bigip_pool_member.py b/network/f5/bigip_pool_member.py index bc4b7be2f7b..1d59462023f 100644 --- a/network/f5/bigip_pool_member.py +++ b/network/f5/bigip_pool_member.py @@ -196,27 +196,6 @@ ''' -try: - import bigsuds -except ImportError: - bigsuds_found = False -else: - bigsuds_found = True - -# =========================================== -# bigip_pool_member module specific support methods. -# - -def bigip_api(bigip, user, password): - api = bigsuds.BIGIP(hostname=bigip, username=user, password=password) - return api - -def disable_ssl_cert_validation(): - # You probably only want to do this for testing and never in production. - # From https://www.python.org/dev/peps/pep-0476/#id29 - import ssl - ssl._create_default_https_context = ssl._create_unverified_context - def pool_exists(api, pool): # hack to determine if pool exists result = False @@ -327,49 +306,37 @@ def get_member_monitor_status(api, pool, address, port): return result def main(): - module = AnsibleModule( - argument_spec = dict( - server = dict(type='str', required=True), - user = dict(type='str', required=True), - password = dict(type='str', required=True), - validate_certs = dict(default='yes', type='bool'), - state = dict(type='str', default='present', choices=['present', 'absent']), + argument_spec = f5_argument_spec(); + argument_spec.update(dict( session_state = dict(type='str', choices=['enabled', 'disabled']), monitor_state = dict(type='str', choices=['enabled', 'disabled']), pool = dict(type='str', required=True), - partition = dict(type='str', default='Common'), host = dict(type='str', required=True, aliases=['address', 'name']), port = dict(type='int', required=True), connection_limit = dict(type='int'), description = dict(type='str'), rate_limit = dict(type='int'), ratio = dict(type='int') - ), - supports_check_mode=True + ) ) - if not bigsuds_found: - module.fail_json(msg="the python bigsuds module is required") + module = AnsibleModule( + argument_spec = argument_spec, + supports_check_mode=True + ) - server = module.params['server'] - user = module.params['user'] - password = module.params['password'] - validate_certs = module.params['validate_certs'] - state = module.params['state'] + (server,user,password,state,partition,validate_certs) = f5_parse_arguments(module) session_state = module.params['session_state'] monitor_state = module.params['monitor_state'] - partition = module.params['partition'] - pool = "/%s/%s" % (partition, module.params['pool']) + pool = fq_name(partition, module.params['pool']) connection_limit = module.params['connection_limit'] description = module.params['description'] rate_limit = module.params['rate_limit'] ratio = module.params['ratio'] host = module.params['host'] - address = "/%s/%s" % (partition, host) + address = fq_name(partition, host) port = module.params['port'] - if not validate_certs: - disable_ssl_cert_validation() # sanity check user supplied values @@ -457,5 +424,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.f5 import * main() From 9ee29fa5798ca9149b78a01cfcfa6a0ce61114f4 Mon Sep 17 00:00:00 2001 From: Sebastian Kornehl Date: Wed, 3 Jun 2015 13:15:59 +0200 Subject: [PATCH 0086/2522] Added datadog_monitor module --- monitoring/datadog_monitor.py | 278 ++++++++++++++++++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 monitoring/datadog_monitor.py diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py new file mode 100644 index 00000000000..b5ad2d2d6d6 --- /dev/null +++ b/monitoring/datadog_monitor.py @@ -0,0 +1,278 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Sebastian Kornehl +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# import module snippets + +# Import Datadog +try: + from datadog import initialize, api + HAS_DATADOG = True +except: + HAS_DATADOG = False + +DOCUMENTATION = ''' +--- +module: datadog_monitor +short_description: Manages Datadog monitors +description: +- "Manages monitors within Datadog" +- "Options like described on http://docs.datadoghq.com/api/" +version_added: "2.0" +author: '"Sebastian Kornehl" ' +notes: [] +requirements: [datadog] +options: + api_key: + description: ["Your DataDog API key."] + required: true + default: null + app_key: + description: ["Your DataDog app key."] + required: true + default: null + state: + description: ["The designated state of the monitor."] + required: true + default: null + choices: ['present', 'absent', 'muted', 'unmuted'] + type: + description: ["The type of the monitor."] + required: false + default: null + choices: ['metric alert', 'service check'] + query: + description: ["he monitor query to notify on with syntax varying depending on what type of monitor you are creating."] + required: false + default: null + name: + description: ["The name of the alert."] + required: true + default: null + message: + description: ["A message to include with notifications for this monitor. Email notifications can be sent to specific users by using the same '@username' notation as events."] + required: false + default: null + silenced: + description: ["Dictionary of scopes to timestamps or None. Each scope will be muted until the given POSIX timestamp or forever if the value is None. "] + required: false + default: "" + notify_no_data: + description: ["A boolean indicating whether this monitor will notify when data stops reporting.."] + required: false + default: False + no_data_timeframe: + description: ["The number of minutes before a monitor will notify when data stops reporting. Must be at least 2x the monitor timeframe for metric alerts or 2 minutes for service checks."] + required: false + default: 2x timeframe for metric, 2 minutes for service + timeout_h: + description: ["The number of hours of the monitor not reporting data before it will automatically resolve from a triggered state."] + required: false + default: null + renotify_interval: + description: ["The number of minutes after the last notification before a monitor will re-notify on the current status. It will only re-notify if it's not resolved."] + required: false + default: null + escalation_message: + description: ["A message to include with a re-notification. Supports the '@username' notification we allow elsewhere. Not applicable if renotify_interval is None"] + required: false + default: null + notify_audit: + description: ["A boolean indicating whether tagged users will be notified on changes to this monitor."] + required: false + default: False + thresholds: + description: ["A dictionary of thresholds by status. Because service checks can have multiple thresholds, we don't define them directly in the query."] + required: false + default: {'ok': 1, 'critical': 1, 'warning': 1} +''' + +EXAMPLES = ''' +# Create a metric monitor +datadog_monitor: + type: "metric alert" + name: "Test monitor" + state: "present" + query: "datadog.agent.up".over("host:host1").last(2).count_by_status()" + message: "Some message." + api_key: "9775a026f1ca7d1c6c5af9d94d9595a4" + app_key: "87ce4a24b5553d2e482ea8a8500e71b8ad4554ff" + +# Deletes a monitor +datadog_monitor: + name: "Test monitor" + state: "absent" + api_key: "9775a026f1ca7d1c6c5af9d94d9595a4" + app_key: "87ce4a24b5553d2e482ea8a8500e71b8ad4554ff" + +# Mutes a monitor +datadog_monitor: + name: "Test monitor" + state: "mute" + silenced: '{"*":None}' + api_key: "9775a026f1ca7d1c6c5af9d94d9595a4" + app_key: "87ce4a24b5553d2e482ea8a8500e71b8ad4554ff" + +# Unmutes a monitor +datadog_monitor: + name: "Test monitor" + state: "unmute" + api_key: "9775a026f1ca7d1c6c5af9d94d9595a4" + app_key: "87ce4a24b5553d2e482ea8a8500e71b8ad4554ff" +''' + + +def main(): + module = AnsibleModule( + argument_spec=dict( + api_key=dict(required=True), + app_key=dict(required=True), + state=dict(required=True, choises=['present', 'absent', 'mute', 'unmute']), + type=dict(required=False, choises=['metric alert', 'service check']), + name=dict(required=True), + query=dict(required=False), + message=dict(required=False, default=None), + silenced=dict(required=False, default=None, type='dict'), + notify_no_data=dict(required=False, default=False, choices=BOOLEANS), + no_data_timeframe=dict(required=False, default=None), + timeout_h=dict(required=False, default=None), + renotify_interval=dict(required=False, default=None), + escalation_message=dict(required=False, default=None), + notify_audit=dict(required=False, default=False, choices=BOOLEANS), + thresholds=dict(required=False, type='dict', default={'ok': 1, 'critical': 1, 'warning': 1}), + ) + ) + + # Prepare Datadog + if not HAS_DATADOG: + module.fail_json(msg='datadogpy required for this module') + + options = { + 'api_key': module.params['api_key'], + 'app_key': module.params['app_key'] + } + + initialize(**options) + + if module.params['state'] == 'present': + install_monitor(module) + elif module.params['state'] == 'absent': + delete_monitor(module) + elif module.params['state'] == 'mute': + mute_monitor(module) + elif module.params['state'] == 'unmute': + unmute_monitor(module) + + +def _get_monitor(module): + for monitor in api.Monitor.get_all(): + if monitor['name'] == module.params['name']: + return monitor + return {} + + +def _post_monitor(module, options): + try: + msg = api.Monitor.create(type=module.params['type'], query=module.params['query'], + name=module.params['name'], message=module.params['message'], + options=options) + module.exit_json(changed=True, msg=msg) + except Exception, e: + module.fail_json(msg=str(e)) + + +def _update_monitor(module, monitor, options): + try: + msg = api.Monitor.update(id=monitor['id'], query=module.params['query'], + name=module.params['name'], message=module.params['message'], + options=options) + if len(set(msg) - set(monitor)) == 0: + module.exit_json(changed=False, msg=msg) + else: + module.exit_json(changed=True, msg=msg) + except Exception, e: + module.fail_json(msg=str(e)) + + +def install_monitor(module): + options = { + "silenced": module.params['silenced'], + "notify_no_data": module.boolean(module.params['notify_no_data']), + "no_data_timeframe": module.params['no_data_timeframe'], + "timeout_h": module.params['timeout_h'], + "renotify_interval": module.params['renotify_interval'], + "escalation_message": module.params['escalation_message'], + "notify_audit": module.boolean(module.params['notify_audit']), + } + + if module.params['type'] == "service check": + options["thresholds"] = module.params['thresholds'] + + monitor = _get_monitor(module) + if not monitor: + _post_monitor(module, options) + else: + _update_monitor(module, monitor, options) + + +def delete_monitor(module): + monitor = _get_monitor(module) + if not monitor: + module.exit_json(changed=False) + try: + msg = api.Monitor.delete(monitor['id']) + module.exit_json(changed=True, msg=msg) + except Exception, e: + module.fail_json(msg=str(e)) + + +def mute_monitor(module): + monitor = _get_monitor(module) + if not monitor: + module.fail_json(msg="Monitor %s not found!" % module.params['name']) + elif monitor['options']['silenced']: + module.fail_json(msg="Monitor is already muted. Datadog does not allow to modify muted alerts, consider unmuting it first.") + elif (module.params['silenced'] is not None + and len(set(monitor['options']['silenced']) - set(module.params['silenced'])) == 0): + module.exit_json(changed=False) + try: + if module.params['silenced'] is None or module.params['silenced'] == "": + msg = api.Monitor.mute(id=monitor['id']) + else: + msg = api.Monitor.mute(id=monitor['id'], silenced=module.params['silenced']) + module.exit_json(changed=True, msg=msg) + except Exception, e: + module.fail_json(msg=str(e)) + + +def unmute_monitor(module): + monitor = _get_monitor(module) + if not monitor: + module.fail_json(msg="Monitor %s not found!" % module.params['name']) + elif not monitor['options']['silenced']: + module.exit_json(changed=False) + try: + msg = api.Monitor.unmute(monitor['id']) + module.exit_json(changed=True, msg=msg) + except Exception, e: + module.fail_json(msg=str(e)) + + +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * +main() From 172b012ee2982f15025b1022ffd7b0ef442893d9 Mon Sep 17 00:00:00 2001 From: Rick Mendes Date: Wed, 3 Jun 2015 08:46:29 -0700 Subject: [PATCH 0087/2522] now handles keys protected with a passphrase --- cloud/amazon/ec2_win_password.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_win_password.py b/cloud/amazon/ec2_win_password.py index 33a6ae7f947..c873bb9ecb0 100644 --- a/cloud/amazon/ec2_win_password.py +++ b/cloud/amazon/ec2_win_password.py @@ -17,6 +17,10 @@ description: - path to the file containing the key pair used on the instance required: true + key_passphrase: + description: + - The passphrase for the instance key pair. The key must use DES or 3DES encryption for this module to decrypt it. You can use openssl to convert your password protected keys if they do not use DES or 3DES. ex) openssl rsa -in current_key -out new_key -des3. + required: false region: description: - The AWS region to use. Must be specified if ec2_url is not used. If not specified then the value of the EC2_REGION environment variable, if any, is used. @@ -36,6 +40,16 @@ instance_id: i-XXXXXX region: us-east-1 key_file: "~/aws-creds/my_test_key.pem" + +# Example of getting a password with a password protected key +tasks: +- name: get the Administrator password + ec2_win_password: + profile: my-boto-profile + instance_id: i-XXXXXX + region: us-east-1 + key_file: "~/aws-creds/my_protected_test_key.pem" + key_passphrase: "secret" ''' from base64 import b64decode @@ -54,6 +68,7 @@ def main(): argument_spec.update(dict( instance_id = dict(required=True), key_file = dict(required=True), + key_passphrase = dict(default=None), ) ) module = AnsibleModule(argument_spec=argument_spec) @@ -63,6 +78,7 @@ def main(): instance_id = module.params.get('instance_id') key_file = expanduser(module.params.get('key_file')) + key_passphrase = module.params.get('key_passphrase') ec2 = ec2_connect(module) @@ -70,7 +86,7 @@ def main(): decoded = b64decode(data) f = open(key_file, 'r') - key = RSA.importKey(f.read()) + key = RSA.importKey(f.read(), key_passphrase) cipher = PKCS1_v1_5.new(key) sentinel = 'password decryption failed!!!' From 80153611abda732446f8412ed21fd04a93aa96fb Mon Sep 17 00:00:00 2001 From: Rick Mendes Date: Wed, 3 Jun 2015 09:06:43 -0700 Subject: [PATCH 0088/2522] added version_added to key_passphrase --- cloud/amazon/ec2_win_password.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud/amazon/ec2_win_password.py b/cloud/amazon/ec2_win_password.py index c873bb9ecb0..6a81192016a 100644 --- a/cloud/amazon/ec2_win_password.py +++ b/cloud/amazon/ec2_win_password.py @@ -18,6 +18,7 @@ - path to the file containing the key pair used on the instance required: true key_passphrase: + version_added: "2.0" description: - The passphrase for the instance key pair. The key must use DES or 3DES encryption for this module to decrypt it. You can use openssl to convert your password protected keys if they do not use DES or 3DES. ex) openssl rsa -in current_key -out new_key -des3. required: false From ab8de7a3e7f6b62119b3c65f74f96ea06ab3572f Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Thu, 4 Jun 2015 01:25:08 +0300 Subject: [PATCH 0089/2522] bower module. Non-interactive mode and allow-root moved to _exec, they should affect all commands --- packaging/language/bower.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packaging/language/bower.py b/packaging/language/bower.py index 34284356f6e..8fbe20f7e0c 100644 --- a/packaging/language/bower.py +++ b/packaging/language/bower.py @@ -86,7 +86,7 @@ def __init__(self, module, **kwargs): def _exec(self, args, run_in_check_mode=False, check_rc=True): if not self.module.check_mode or (self.module.check_mode and run_in_check_mode): - cmd = ["bower"] + args + cmd = ["bower"] + args + ['--config.interactive=false', '--allow-root'] if self.name: cmd.append(self.name_version) @@ -108,7 +108,7 @@ def _exec(self, args, run_in_check_mode=False, check_rc=True): return '' def list(self): - cmd = ['list', '--json', '--config.interactive=false', '--allow-root'] + cmd = ['list', '--json'] installed = list() missing = list() From df618c2d48b3348028e98e3e8de706d33d489050 Mon Sep 17 00:00:00 2001 From: Sebastian Kornehl Date: Thu, 4 Jun 2015 06:54:02 +0200 Subject: [PATCH 0090/2522] docs: removed default when required is true --- monitoring/datadog_monitor.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py index b5ad2d2d6d6..24de8af10ba 100644 --- a/monitoring/datadog_monitor.py +++ b/monitoring/datadog_monitor.py @@ -41,15 +41,12 @@ api_key: description: ["Your DataDog API key."] required: true - default: null app_key: description: ["Your DataDog app key."] required: true - default: null state: description: ["The designated state of the monitor."] required: true - default: null choices: ['present', 'absent', 'muted', 'unmuted'] type: description: ["The type of the monitor."] @@ -63,7 +60,6 @@ name: description: ["The name of the alert."] required: true - default: null message: description: ["A message to include with notifications for this monitor. Email notifications can be sent to specific users by using the same '@username' notation as events."] required: false From 9139ac54607be1ba568102854aa241be85cce6c8 Mon Sep 17 00:00:00 2001 From: Etienne CARRIERE Date: Sun, 10 May 2015 01:32:16 +0200 Subject: [PATCH 0091/2522] Creation of bigip_virtual_server module --- network/f5/bigip_virtual_server.py | 453 +++++++++++++++++++++++++++++ 1 file changed, 453 insertions(+) create mode 100644 network/f5/bigip_virtual_server.py diff --git a/network/f5/bigip_virtual_server.py b/network/f5/bigip_virtual_server.py new file mode 100644 index 00000000000..30b2c7008bb --- /dev/null +++ b/network/f5/bigip_virtual_server.py @@ -0,0 +1,453 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Etienne Carriere +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: bigip_virtual_server +short_description: "Manages F5 BIG-IP LTM virtual servers" +description: + - "Manages F5 BIG-IP LTM virtual servers via iControl SOAP API" +version_added: "2.0" +author: Etienne Carriere +notes: + - "Requires BIG-IP software version >= 11" + - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" + - "Best run as a local_action in your playbook" +requirements: + - bigsuds +options: + server: + description: + - BIG-IP host + required: true + default: null + choices: [] + aliases: [] + user: + description: + - BIG-IP username + required: true + default: null + choices: [] + aliases: [] + password: + description: + - BIG-IP password + required: true + default: null + choices: [] + aliases: [] + validate_certs: + description: + - If C(no), SSL certificates will not be validated. This should only be used + on personally controlled sites using self-signed certificates. + required: false + default: 'yes' + choices: ['yes', 'no'] + version_added: 2.0 + state: + description: + - Pool member state + required: true + default: present + choices: ['present', 'absent', 'enabled', 'disabled'] + aliases: [] + partition: + description: + - Partition + required: false + default: 'Common' + choices: [] + aliases: [] + name: + description: + - "Virtual server name." + required: true + default: null + choices: [] + aliases: ['vs'] + destination: + description: + - "Destination IP of the virtual server (only host is currently supported) . Required when state=present and vs does not exist. Error when state=absent." + required: true + default: null + choices: [] + aliases: ['address', 'ip'] + port: + description: + - "Port of the virtual server . Required when state=present and vs does not exist" + required: true + default: null + choices: [] + aliases: [] + all_profiles: + description: + - "List of all Profiles (HTTP,ClientSSL,ServerSSL,etc) that must be used by the virtual server" + required: false + default: null + choices: [] + aliases: [] + pool: + description: + - "Default pool for the virtual server" + required: false + default: null + choices: [] + aliases: [] + snat: + description: + - "Source network address policy" + required: false + default: None + choices: [] + aliases: [] + + description: + description: + - "Virtual server description." + required: false + default: null + choices: [] +''' + +EXAMPLES = ''' + +## playbook task examples: + +--- +# file bigip-test.yml +# ... + - name: Add VS + local_action: + module: bigip_virtual_server + server: lb.mydomain.net + user: admin + password: secret + state: present + partition: MyPartition + name: myvirtualserver + destination: "{{ ansible_default_ipv4["address"] }}" + port: 443 + pool: "{{ mypool }}" + snat: Automap + description: Test Virtual Server + all_profiles: + - http + - clientssl + + - name: Modify Port of the Virtual Server + local_action: + module: bigip_virtual_server + server: lb.mydomain.net + user: admin + password: secret + state: present + partition: MyPartition + name: myvirtualserver + port: 8080 + + - name: Delete pool + local_action: + module: bigip_virtual_server + server: lb.mydomain.net + user: admin + password: secret + state: absent + partition: MyPartition + name: myvirtualserver +''' + +try: + import bigsuds +except ImportError: + bigsuds_found = False +else: + bigsuds_found = True + +# ========================== +# bigip_node module specific +# + +# map of state values +STATES={'enabled': 'STATE_ENABLED', + 'disabled': 'STATE_DISABLED'} +STATUSES={'enabled': 'SESSION_STATUS_ENABLED', + 'disabled': 'SESSION_STATUS_DISABLED', + 'offline': 'SESSION_STATUS_FORCED_DISABLED'} + +def bigip_api(bigip, user, password): + api = bigsuds.BIGIP(hostname=bigip, username=user, password=password) + return api + +def disable_ssl_cert_validation(): + # You probably only want to do this for testing and never in production. + # From https://www.python.org/dev/peps/pep-0476/#id29 + import ssl + ssl._create_default_https_context = ssl._create_unverified_context + +def fq_name(partition,name): + if name is None: + return None + if name[0] is '/': + return name + else: + return '/%s/%s' % (partition,name) + +def fq_list_names(partition,list_names): + if list_names is None: + return None + return map(lambda x: fq_name(partition,x),list_names) + + +def vs_exists(api, vs): + # hack to determine if pool exists + result = False + try: + api.LocalLB.VirtualServer.get_object_status(virtual_servers=[vs]) + result = True + except bigsuds.OperationFailed, e: + if "was not found" in str(e): + result = False + else: + # genuine exception + raise + return result + +def vs_create(api,name,destination,port,pool): + _profiles=[[{'profile_context': 'PROFILE_CONTEXT_TYPE_ALL', 'profile_name': 'tcp'}]] + try: + api.LocalLB.VirtualServer.create( + definitions = [{'name': [name], 'address': [destination], 'port': port, 'protocol': 'PROTOCOL_TCP'}], + wildmasks = ['255.255.255.255'], + resources = [{'type': 'RESOURCE_TYPE_POOL', 'default_pool_name': pool}], + profiles = _profiles) + result = True + desc = 0 + except Exception, e : + print e.args + +def vs_remove(api,name): + api.LocalLB.VirtualServer.delete_virtual_server(virtual_servers = [name ]) + +def get_profiles(api,name): + return api.LocalLB.VirtualServer.get_profile(virtual_servers = [name])[0] + + + +def set_profiles(api,name,profiles_list): + if profiles_list is None: + return False + current_profiles=map(lambda x:x['profile_name'], get_profiles(api,name)) + to_add_profiles=[] + for x in profiles_list: + if x not in current_profiles: + to_add_profiles.append({'profile_context': 'PROFILE_CONTEXT_TYPE_ALL', 'profile_name': x}) + to_del_profiles=[] + for x in current_profiles: + if (x not in profiles_list) and (x!= "/Common/tcp"): + to_del_profiles.append({'profile_context': 'PROFILE_CONTEXT_TYPE_ALL', 'profile_name': x}) + changed=False + if len(to_del_profiles)>0: + api.LocalLB.VirtualServer.remove_profile(virtual_servers = [name],profiles = [to_del_profiles]) + changed=True + if len(to_add_profiles)>0: + api.LocalLB.VirtualServer.add_profile(virtual_servers = [name],profiles= [to_add_profiles]) + changed=True + return changed + + +def set_snat(api,name,snat): + current_state=get_snat_type(api,name) + update = False + if snat is None: + return update + if snat == 'None' and current_state != 'SRC_TRANS_NONE': + api.LocalLB.VirtualServer.set_source_address_translation_none(virtual_servers = [name]) + update = True + if snat == 'Automap' and current_state != 'SRC_TRANS_AUTOMAP': + api.LocalLB.VirtualServer.set_source_address_translation_automap(virtual_servers = [name]) + update = True + return update + +def get_snat_type(api,name): + return api.LocalLB.VirtualServer.get_source_address_translation_type(virtual_servers = [name])[0] + + +def get_pool(api,name): + return api.LocalLB.VirtualServer.get_default_pool_name(virtual_servers = [name])[0] + +def set_pool(api,name,pool): + current_pool = get_pool (api,name) + updated=False + if pool is not None and (pool != current_pool): + api.LocalLB.VirtualServer.set_default_pool_name(virtual_servers = [name],default_pools = [pool]) + updated=True + return updated + + + +def get_destination(api,name): + return api.LocalLB.VirtualServer.get_destination_v2(virtual_servers = [name])[0] + +def set_destination(api,name,destination,port): + current_destination = get_destination(api,name) + updated=False + if (destination is not None and port is not None) and (destination != current_destination['address'] or port != current_destination['port']): + api.LocalLB.VirtualServer.set_destination_v2(virtual_servers = [name],destinations=[{'address': destination, 'port':port}]) + updated=True + return updated + + +def get_description(api,name): + return api.LocalLB.VirtualServer.get_description(virtual_servers = [name])[0] + +def set_description(api,name,description): + current_description = get_description(api,name) + updated=False + if description is not None and current_description != description: + api.LocalLB.VirtualServer.set_description(virtual_servers =[name],descriptions=[description]) + updated=True + return updated + + +def main(): + module = AnsibleModule( + argument_spec = dict( + server = dict(type='str', required=True), + user = dict(type='str', required=True), + password = dict(type='str', required=True), + validate_certs = dict(default='yes', type='bool'), + state = dict(type='str', default='present', + choices=['present', 'absent', 'disabled', 'enabled']), + partition = dict(type='str', default='Common'), + name = dict(type='str', required=True,aliases=['vs']), + destination = dict(type='str', aliases=['address', 'ip']), + port = dict(type='int'), + all_profiles = dict(type='list'), + pool=dict(type='str'), + description = dict(type='str'), + snat=dict(type='str') + ), + supports_check_mode=True + ) + + if not bigsuds_found: + module.fail_json(msg="the python bigsuds module is required") + server = module.params['server'] + user = module.params['user'] + password = module.params['password'] + validate_certs = module.params['validate_certs'] + state = module.params['state'] + partition = module.params['partition'] + name = fq_name(partition,module.params['name']) + destination=module.params['destination'] + port=module.params['port'] + all_profiles=fq_list_names(partition,module.params['all_profiles']) + pool=fq_name(partition,module.params['pool']) + description = module.params['description'] + snat = module.params['snat'] + if not validate_certs: + disable_ssl_cert_validation() + + if 1 > port > 65535: + module.fail_json(msg="valid ports must be in range 1 - 65535") + + try: + api = bigip_api(server, user, password) + result = {'changed': False} # default + + if state == 'absent': + if not module.check_mode: + if vs_exists(api,name): + # hack to handle concurrent runs of module + # pool might be gone before we actually remove + try: + vs_remove(api,name) + result = {'changed' : True, 'deleted' : name } + except bigsuds.OperationFailed, e: + if "was not found" in str(e): + result['changed']= False + else: + raise + else: + # check-mode return value + result = {'changed': True} + + elif state == 'present': + update = False + if not vs_exists(api, name): + if (not destination) or (not port): + module.fail_json(msg="both destination and port must be supplied to create a VS") + if not module.check_mode: + # a bit of a hack to handle concurrent runs of this module. + # even though we've checked the pool doesn't exist, + # it may exist by the time we run create_pool(). + # this catches the exception and does something smart + # about it! + try: + vs_create(api,name,destination,port,pool) + result = {'changed': True} + except bigsuds.OperationFailed, e: + if "already exists" in str(e): + update = True + else: + raise + else: + set_profiles(api,name,all_profiles) + set_snat(api,name,snat) + set_description(api,name,description) + else: + # check-mode return value + result = {'changed': True} + else: + update = True + if update: + # VS exists + if not module.check_mode: + # Have a transaction for all the changes + api.System.Session.start_transaction() + result['changed']|=set_destination(api,name,fq_name(partition,destination),port) + result['changed']|=set_pool(api,name,pool) + result['changed']|=set_description(api,name,description) + result['changed']|=set_snat(api,name,snat) + result['changed']|=set_profiles(api,name,all_profiles) + api.System.Session.submit_transaction() + else: + # check-mode return value + result = {'changed': True} + + elif state in ('disabled', 'enabled'): + if name is None: + module.fail_json(msg="name parameter required when " \ + "state=enabled/disabled") + if not module.check_mode: + pass + else: + # check-mode return value + result = {'changed': True} + + except Exception, e: + module.fail_json(msg="received exception: %s" % e) + + module.exit_json(**result) +# import module snippets +from ansible.module_utils.basic import * +main() + From 24ae114a2aef7b86fe9db5094091429a01831f00 Mon Sep 17 00:00:00 2001 From: Etienne CARRIERE Date: Thu, 4 Jun 2015 08:30:32 +0200 Subject: [PATCH 0092/2522] Factor F5 virtual_server module with the common functions --- network/f5/bigip_virtual_server.py | 59 +++++------------------------- 1 file changed, 10 insertions(+), 49 deletions(-) diff --git a/network/f5/bigip_virtual_server.py b/network/f5/bigip_virtual_server.py index 30b2c7008bb..dda9e51dbad 100644 --- a/network/f5/bigip_virtual_server.py +++ b/network/f5/bigip_virtual_server.py @@ -174,15 +174,9 @@ name: myvirtualserver ''' -try: - import bigsuds -except ImportError: - bigsuds_found = False -else: - bigsuds_found = True # ========================== -# bigip_node module specific +# bigip_virtual_server module specific # # map of state values @@ -192,30 +186,6 @@ 'disabled': 'SESSION_STATUS_DISABLED', 'offline': 'SESSION_STATUS_FORCED_DISABLED'} -def bigip_api(bigip, user, password): - api = bigsuds.BIGIP(hostname=bigip, username=user, password=password) - return api - -def disable_ssl_cert_validation(): - # You probably only want to do this for testing and never in production. - # From https://www.python.org/dev/peps/pep-0476/#id29 - import ssl - ssl._create_default_https_context = ssl._create_unverified_context - -def fq_name(partition,name): - if name is None: - return None - if name[0] is '/': - return name - else: - return '/%s/%s' % (partition,name) - -def fq_list_names(partition,list_names): - if list_names is None: - return None - return map(lambda x: fq_name(partition,x),list_names) - - def vs_exists(api, vs): # hack to determine if pool exists result = False @@ -328,15 +298,10 @@ def set_description(api,name,description): def main(): - module = AnsibleModule( - argument_spec = dict( - server = dict(type='str', required=True), - user = dict(type='str', required=True), - password = dict(type='str', required=True), - validate_certs = dict(default='yes', type='bool'), + argument_spec = f5_argument_spec() + argument_spec.update( dict( state = dict(type='str', default='present', choices=['present', 'absent', 'disabled', 'enabled']), - partition = dict(type='str', default='Common'), name = dict(type='str', required=True,aliases=['vs']), destination = dict(type='str', aliases=['address', 'ip']), port = dict(type='int'), @@ -344,18 +309,15 @@ def main(): pool=dict(type='str'), description = dict(type='str'), snat=dict(type='str') - ), + ) + ) + + module = AnsibleModule( + argument_spec = argument_spec, supports_check_mode=True ) - if not bigsuds_found: - module.fail_json(msg="the python bigsuds module is required") - server = module.params['server'] - user = module.params['user'] - password = module.params['password'] - validate_certs = module.params['validate_certs'] - state = module.params['state'] - partition = module.params['partition'] + (server,user,password,state,partition,validate_certs) = f5_parse_arguments(module) name = fq_name(partition,module.params['name']) destination=module.params['destination'] port=module.params['port'] @@ -363,8 +325,6 @@ def main(): pool=fq_name(partition,module.params['pool']) description = module.params['description'] snat = module.params['snat'] - if not validate_certs: - disable_ssl_cert_validation() if 1 > port > 65535: module.fail_json(msg="valid ports must be in range 1 - 65535") @@ -449,5 +409,6 @@ def main(): module.exit_json(**result) # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.f5 import * main() From 80b1b3add239c58582bc71576a5666d81580bff0 Mon Sep 17 00:00:00 2001 From: Quentin Stafford-Fraser Date: Thu, 4 Jun 2015 22:17:16 +0100 Subject: [PATCH 0093/2522] Webfaction will create a default database user when db is created. For symmetry and repeatability, delete it when db is deleted. Add missing param to documentation. --- cloud/webfaction/webfaction_db.py | 48 ++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/cloud/webfaction/webfaction_db.py b/cloud/webfaction/webfaction_db.py index a9ef88b943e..1a91d649458 100644 --- a/cloud/webfaction/webfaction_db.py +++ b/cloud/webfaction/webfaction_db.py @@ -4,7 +4,7 @@ # # ------------------------------------------ # -# (c) Quentin Stafford-Fraser 2015 +# (c) Quentin Stafford-Fraser and Andy Baker 2015 # # This file is part of Ansible # @@ -53,6 +53,12 @@ required: true choices: ['mysql', 'postgresql'] + password: + description: + - The password for the new database user. + required: false + default: None + login_name: description: - The webfaction account to use @@ -75,6 +81,10 @@ type: mysql login_name: "{{webfaction_user}}" login_password: "{{webfaction_passwd}}" + + # Note that, for symmetry's sake, deleting a database using + # 'state: absent' will also delete the matching user. + ''' import socket @@ -110,13 +120,17 @@ def main(): db_map = dict([(i['name'], i) for i in db_list]) existing_db = db_map.get(db_name) + user_list = webfaction.list_db_users(session_id) + user_map = dict([(i['username'], i) for i in user_list]) + existing_user = user_map.get(db_name) + result = {} # Here's where the real stuff happens if db_state == 'present': - # Does an app with this name already exist? + # Does an database with this name already exist? if existing_db: # Yes, but of a different type - fail if existing_db['db_type'] != db_type: @@ -129,8 +143,8 @@ def main(): if not module.check_mode: - # If this isn't a dry run, create the app - # print positional_args + # If this isn't a dry run, create the db + # and default user. result.update( webfaction.create_db( session_id, db_name, db_type, db_passwd @@ -139,17 +153,23 @@ def main(): elif db_state == 'absent': - # If the app's already not there, nothing changed. - if not existing_db: - module.exit_json( - changed = False, - ) - + # If this isn't a dry run... if not module.check_mode: - # If this isn't a dry run, delete the app - result.update( - webfaction.delete_db(session_id, db_name, db_type) - ) + + if not (existing_db or existing_user): + module.exit_json(changed = False,) + + if existing_db: + # Delete the db if it exists + result.update( + webfaction.delete_db(session_id, db_name, db_type) + ) + + if existing_user: + # Delete the default db user if it exists + result.update( + webfaction.delete_db_user(session_id, db_name, db_type) + ) else: module.fail_json(msg="Unknown state specified: {}".format(db_state)) From 84b9ab435de312ddac377cb2f57f52da0a28f04d Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Fri, 5 Jun 2015 11:25:27 -0400 Subject: [PATCH 0094/2522] minor docs update --- cloud/webfaction/webfaction_app.py | 2 +- cloud/webfaction/webfaction_db.py | 2 +- cloud/webfaction/webfaction_domain.py | 2 +- cloud/webfaction/webfaction_mailbox.py | 2 +- cloud/webfaction/webfaction_site.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cloud/webfaction/webfaction_app.py b/cloud/webfaction/webfaction_app.py index 55599bdcca6..3e42ec1265e 100644 --- a/cloud/webfaction/webfaction_app.py +++ b/cloud/webfaction/webfaction_app.py @@ -31,7 +31,7 @@ short_description: Add or remove applications on a Webfaction host description: - Add or remove applications on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. -author: Quentin Stafford-Fraser +author: Quentin Stafford-Fraser (@quentinsf) version_added: "2.0" notes: - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." diff --git a/cloud/webfaction/webfaction_db.py b/cloud/webfaction/webfaction_db.py index 1a91d649458..f420490711c 100644 --- a/cloud/webfaction/webfaction_db.py +++ b/cloud/webfaction/webfaction_db.py @@ -28,7 +28,7 @@ short_description: Add or remove a database on Webfaction description: - Add or remove a database on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. -author: Quentin Stafford-Fraser +author: Quentin Stafford-Fraser (@quentinsf) version_added: "2.0" notes: - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." diff --git a/cloud/webfaction/webfaction_domain.py b/cloud/webfaction/webfaction_domain.py index f2c95897bc5..0b35faf110f 100644 --- a/cloud/webfaction/webfaction_domain.py +++ b/cloud/webfaction/webfaction_domain.py @@ -28,7 +28,7 @@ short_description: Add or remove domains and subdomains on Webfaction description: - Add or remove domains or subdomains on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. -author: Quentin Stafford-Fraser +author: Quentin Stafford-Fraser (@quentinsf) version_added: "2.0" notes: - If you are I(deleting) domains by using C(state=absent), then note that if you specify subdomains, just those particular subdomains will be deleted. If you don't specify subdomains, the domain will be deleted. diff --git a/cloud/webfaction/webfaction_mailbox.py b/cloud/webfaction/webfaction_mailbox.py index 976a428f3d3..7547b6154e5 100644 --- a/cloud/webfaction/webfaction_mailbox.py +++ b/cloud/webfaction/webfaction_mailbox.py @@ -27,7 +27,7 @@ short_description: Add or remove mailboxes on Webfaction description: - Add or remove mailboxes on a Webfaction account. Further documentation at http://github.com/quentinsf/ansible-webfaction. -author: Quentin Stafford-Fraser +author: Quentin Stafford-Fraser (@quentinsf) version_added: "2.0" notes: - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." diff --git a/cloud/webfaction/webfaction_site.py b/cloud/webfaction/webfaction_site.py index 223458faf46..57eae39c0dc 100644 --- a/cloud/webfaction/webfaction_site.py +++ b/cloud/webfaction/webfaction_site.py @@ -28,7 +28,7 @@ short_description: Add or remove a website on a Webfaction host description: - Add or remove a website on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. -author: Quentin Stafford-Fraser +author: Quentin Stafford-Fraser (@quentinsf) version_added: "2.0" notes: - Sadly, you I(do) need to know your webfaction hostname for the C(host) parameter. But at least, unlike the API, you don't need to know the IP address - you can use a DNS name. From ab8dbd90f9869b343573391c2639e17c15e10071 Mon Sep 17 00:00:00 2001 From: "jonathan.lestrelin" Date: Fri, 5 Jun 2015 18:18:48 +0200 Subject: [PATCH 0095/2522] Add pear packaging module to manage PHP PEAR an PECL packages --- packaging/language/pear.py | 230 +++++++++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 packaging/language/pear.py diff --git a/packaging/language/pear.py b/packaging/language/pear.py new file mode 100644 index 00000000000..c9e3862a31f --- /dev/null +++ b/packaging/language/pear.py @@ -0,0 +1,230 @@ +#!/usr/bin/python -tt +# -*- coding: utf-8 -*- + +# (c) 2012, Afterburn +# (c) 2013, Aaron Bull Schaefer +# (c) 2015, Jonathan Lestrelin +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: pear +short_description: Manage pear/pecl packages +description: + - Manage PHP packages with the pear package manager. +author: + - "'jonathan.lestrelin' " +notes: [] +requirements: [] +options: + name: + description: + - Name of the package to install, upgrade, or remove. + required: true + default: null + + state: + description: + - Desired state of the package. + required: false + default: "present" + choices: ["present", "absent", "latest"] +''' + +EXAMPLES = ''' +# Install pear package +- pear: name=Net_URL2 state=present + +# Install pecl package +- pear: name=pecl/json_post state=present + +# Upgrade package +- pear: name=Net_URL2 state=latest + +# Remove packages +- pear: name=Net_URL2,pecl/json_post state=absent +''' + +import os + +def get_local_version(pear_output): + """Take pear remoteinfo output and get the installed version""" + lines = pear_output.split('\n') + for line in lines: + if 'Installed ' in line: + installed = line.rsplit(None, 1)[-1].strip() + if installed == '-': continue + return installed + return None + +def get_repository_version(pear_output): + """Take pear remote-info output and get the latest version""" + lines = pear_output.split('\n') + for line in lines: + if 'Latest ' in line: + return line.rsplit(None, 1)[-1].strip() + return None + +def query_package(module, name, state="present"): + """Query the package status in both the local system and the repository. + Returns a boolean to indicate if the package is installed, + and a second boolean to indicate if the package is up-to-date.""" + if state == "present": + lcmd = "pear info %s" % (name) + lrc, lstdout, lstderr = module.run_command(lcmd, check_rc=False) + if lrc != 0: + # package is not installed locally + return False, False + + rcmd = "pear remote-info %s" % (name) + rrc, rstdout, rstderr = module.run_command(rcmd, check_rc=False) + + # get the version installed locally (if any) + lversion = get_local_version(rstdout) + + # get the version in the repository + rversion = get_repository_version(rstdout) + + if rrc == 0: + # Return True to indicate that the package is installed locally, + # and the result of the version number comparison + # to determine if the package is up-to-date. + return True, (lversion == rversion) + + return False, False + + +def remove_packages(module, packages): + remove_c = 0 + # Using a for loop incase of error, we can report the package that failed + for package in packages: + # Query the package first, to see if we even need to remove + installed, updated = query_package(module, package) + if not installed: + continue + + cmd = "pear uninstall %s" % (package) + rc, stdout, stderr = module.run_command(cmd, check_rc=False) + + if rc != 0: + module.fail_json(msg="failed to remove %s" % (package)) + + remove_c += 1 + + if remove_c > 0: + + module.exit_json(changed=True, msg="removed %s package(s)" % remove_c) + + module.exit_json(changed=False, msg="package(s) already absent") + + +def install_packages(module, state, packages, package_files): + install_c = 0 + + for i, package in enumerate(packages): + # if the package is installed and state == present + # or state == latest and is up-to-date then skip + installed, updated = query_package(module, package) + if installed and (state == 'present' or (state == 'latest' and updated)): + continue + + if state == 'present': + command = 'install' + + if state == 'latest': + command = 'upgrade' + + cmd = "pear %s %s" % (command, package) + rc, stdout, stderr = module.run_command(cmd, check_rc=False) + + if rc != 0: + module.fail_json(msg="failed to install %s" % (package)) + + install_c += 1 + + if install_c > 0: + module.exit_json(changed=True, msg="installed %s package(s)" % (install_c)) + + module.exit_json(changed=False, msg="package(s) already installed") + + +def check_packages(module, packages, state): + would_be_changed = [] + for package in packages: + installed, updated = query_package(module, package) + if ((state in ["present", "latest"] and not installed) or + (state == "absent" and installed) or + (state == "latest" and not updated)): + would_be_changed.append(package) + if would_be_changed: + if state == "absent": + state = "removed" + module.exit_json(changed=True, msg="%s package(s) would be %s" % ( + len(would_be_changed), state)) + else: + module.exit_json(change=False, msg="package(s) already %s" % state) + +import os + +def exe_exists(program): + for path in os.environ["PATH"].split(os.pathsep): + path = path.strip('"') + exe_file = os.path.join(path, program) + if os.path.isfile(exe_file) and os.access(exe_file, os.X_OK): + return True + + return False + + +def main(): + module = AnsibleModule( + argument_spec = dict( + name = dict(aliases=['pkg']), + state = dict(default='present', choices=['present', 'installed', "latest", 'absent', 'removed'])), + required_one_of = [['name']], + supports_check_mode = True) + + if not exe_exists("pear"): + module.fail_json(msg="cannot find pear executable in PATH") + + p = module.params + + # normalize the state parameter + if p['state'] in ['present', 'installed']: + p['state'] = 'present' + elif p['state'] in ['absent', 'removed']: + p['state'] = 'absent' + + if p['name']: + pkgs = p['name'].split(',') + + pkg_files = [] + for i, pkg in enumerate(pkgs): + pkg_files.append(None) + + if module.check_mode: + check_packages(module, pkgs, p['state']) + + if p['state'] in ['present', 'latest']: + install_packages(module, p['state'], pkgs, pkg_files) + elif p['state'] == 'absent': + remove_packages(module, pkgs) + +# import module snippets +from ansible.module_utils.basic import * + +main() From ca366059d33d3986e34f499db634c1497b7fd5f6 Mon Sep 17 00:00:00 2001 From: Etienne CARRIERE Date: Fri, 5 Jun 2015 20:43:38 +0200 Subject: [PATCH 0096/2522] Documentation fixes --- network/f5/bigip_virtual_server.py | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/network/f5/bigip_virtual_server.py b/network/f5/bigip_virtual_server.py index dda9e51dbad..e6402833cb5 100644 --- a/network/f5/bigip_virtual_server.py +++ b/network/f5/bigip_virtual_server.py @@ -37,23 +37,14 @@ description: - BIG-IP host required: true - default: null - choices: [] - aliases: [] user: description: - BIG-IP username required: true - default: null - choices: [] - aliases: [] password: description: - BIG-IP password required: true - default: null - choices: [] - aliases: [] validate_certs: description: - If C(no), SSL certificates will not be validated. This should only be used @@ -61,11 +52,10 @@ required: false default: 'yes' choices: ['yes', 'no'] - version_added: 2.0 state: description: - Pool member state - required: true + required: false default: present choices: ['present', 'absent', 'enabled', 'disabled'] aliases: [] @@ -74,57 +64,41 @@ - Partition required: false default: 'Common' - choices: [] - aliases: [] name: description: - "Virtual server name." required: true - default: null - choices: [] aliases: ['vs'] destination: description: - "Destination IP of the virtual server (only host is currently supported) . Required when state=present and vs does not exist. Error when state=absent." required: true - default: null - choices: [] aliases: ['address', 'ip'] port: description: - "Port of the virtual server . Required when state=present and vs does not exist" required: true - default: null - choices: [] - aliases: [] all_profiles: description: - "List of all Profiles (HTTP,ClientSSL,ServerSSL,etc) that must be used by the virtual server" required: false default: null - choices: [] - aliases: [] pool: description: - "Default pool for the virtual server" required: false default: null - choices: [] - aliases: [] snat: description: - "Source network address policy" required: false default: None - choices: [] - aliases: [] description: description: - "Virtual server description." required: false default: null - choices: [] ''' EXAMPLES = ''' From 537562217fbc7645a9771efb2f7bd051c948077a Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 6 Jun 2015 09:13:11 +0200 Subject: [PATCH 0097/2522] puppet: ensure puppet is in live mode per default puppet may be configured to operate in `--noop` mode per default. That is why we must pass a `--no-noop` to make sure, changes are going to be applied. --- system/puppet.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/system/puppet.py b/system/puppet.py index 46a5ea58d4f..3d4223bd1e5 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -156,10 +156,14 @@ def main(): cmd += " --show-diff" if module.check_mode: cmd += " --noop" + else: + cmd += " --no-noop" else: cmd = "%s apply --detailed-exitcodes " % base_cmd if module.check_mode: cmd += "--noop " + else: + cmd += "--no-noop " cmd += pipes.quote(p['manifest']) rc, stdout, stderr = module.run_command(cmd) From f33efc929a87fb3b206c106eeda70153e546b740 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 6 Jun 2015 09:42:56 +0200 Subject: [PATCH 0098/2522] puppet: add --environment support --- system/puppet.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/system/puppet.py b/system/puppet.py index 46a5ea58d4f..49ccfaf3cbd 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -59,6 +59,11 @@ - Basename of the facter output file required: false default: ansible + environment: + desciption: + - Puppet environment to be used. + required: false + default: None requirements: [ puppet ] author: Monty Taylor ''' @@ -69,6 +74,9 @@ # Run puppet and timeout in 5 minutes - puppet: timeout=5m + +# Run puppet using a different environment +- puppet: environment=testing ''' @@ -104,6 +112,7 @@ def main(): default=False, aliases=['show-diff'], type='bool'), facts=dict(default=None), facter_basename=dict(default='ansible'), + environment=dict(required=False, default=None), ), supports_check_mode=True, required_one_of=[ @@ -154,10 +163,14 @@ def main(): puppetmaster=pipes.quote(p['puppetmaster'])) if p['show_diff']: cmd += " --show-diff" + if p['environment']: + cmd += " --environment '%s'" % p['environment'] if module.check_mode: cmd += " --noop" else: cmd = "%s apply --detailed-exitcodes " % base_cmd + if p['environment']: + cmd += "--environment '%s' " % p['environment'] if module.check_mode: cmd += "--noop " cmd += pipes.quote(p['manifest']) From d63425388b4e58d37d435afadf40cbde9117d937 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 6 Jun 2015 09:46:16 +0200 Subject: [PATCH 0099/2522] puppet: fix missing space between command and arg Fixes: ~~~ { "cmd": "/usr/bin/puppetconfig print agent_disabled_lockfile", "failed": true, "msg": "[Errno 2] No such file or directory", "rc": 2 } ~~~ --- system/puppet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/puppet.py b/system/puppet.py index 46a5ea58d4f..a7796c1b7ca 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -128,7 +128,7 @@ def main(): # Check if puppet is disabled here if p['puppetmaster']: rc, stdout, stderr = module.run_command( - PUPPET_CMD + "config print agent_disabled_lockfile") + PUPPET_CMD + " config print agent_disabled_lockfile") if os.path.exists(stdout.strip()): module.fail_json( msg="Puppet agent is administratively disabled.", disabled=True) From a7c7e2d6d55a94e85192a95213c4cff28342c28c Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 6 Jun 2015 10:08:16 +0200 Subject: [PATCH 0100/2522] puppet: make arg puppetmaster optional puppetmaster was used to determine if `agent` or `apply` should be used. But puppetmaster is not required by puppet per default. Puppet may have a config or could find out by itself (...) where the puppet master is. It changed the code so we only use `apply` if a manifest was passed, otherwise we use `agent`. This also fixes the example, which did not work the way without this change. ~~~ # Run puppet agent and fail if anything goes wrong - puppet ~~~ --- system/puppet.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/system/puppet.py b/system/puppet.py index 46a5ea58d4f..e0a1cf79853 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -35,12 +35,12 @@ default: 30m puppetmaster: description: - - The hostname of the puppetmaster to contact. Must have this or manifest + - The hostname of the puppetmaster to contact. required: false default: None manifest: desciption: - - Path to the manifest file to run puppet apply on. Must have this or puppetmaster + - Path to the manifest file to run puppet apply on. required: false default: None show_diff: @@ -64,7 +64,7 @@ ''' EXAMPLES = ''' -# Run puppet and fail if anything goes wrong +# Run puppet agent and fail if anything goes wrong - puppet # Run puppet and timeout in 5 minutes @@ -106,7 +106,7 @@ def main(): facter_basename=dict(default='ansible'), ), supports_check_mode=True, - required_one_of=[ + mutually_exclusive=[ ('puppetmaster', 'manifest'), ], ) @@ -126,7 +126,7 @@ def main(): manifest=p['manifest'])) # Check if puppet is disabled here - if p['puppetmaster']: + if not p['manifest']: rc, stdout, stderr = module.run_command( PUPPET_CMD + "config print agent_disabled_lockfile") if os.path.exists(stdout.strip()): @@ -145,13 +145,14 @@ def main(): base_cmd = "timeout -s 9 %(timeout)s %(puppet_cmd)s" % dict( timeout=pipes.quote(p['timeout']), puppet_cmd=PUPPET_CMD) - if p['puppetmaster']: + if not p['manifest']: cmd = ("%(base_cmd)s agent --onetime" - " --server %(puppetmaster)s" " --ignorecache --no-daemonize --no-usecacheonfailure --no-splay" " --detailed-exitcodes --verbose") % dict( base_cmd=base_cmd, - puppetmaster=pipes.quote(p['puppetmaster'])) + ) + if p['puppetmaster']: + cmd += " -- server %s" % pipes.quote(p['puppetmaster']) if p['show_diff']: cmd += " --show-diff" if module.check_mode: From 724501e9afc586f1a207d23fca3a72535ce4c738 Mon Sep 17 00:00:00 2001 From: Pepe Barbe Date: Sun, 7 Jun 2015 13:18:33 -0500 Subject: [PATCH 0101/2522] Refactor win_chocolatey module * Refactor code to be more robust. Run main logic inside a try {} catch {} block. If there is any error, bail out and log all the command output automatically. * Rely on error code generated by chocolatey instead of scraping text output to determine success/failure. * Add support for unattended installs: (`-y` flag is a requirement by chocolatey) * Before (un)installing, check existence of files. * Use functions to abstract logic * The great rewrite of 0.9.9, the `choco` interface has changed, check if chocolatey is installed and an older version. If so upgrade to latest. * Allow upgrading packages that are already installed * Use verbose logging for chocolate actions * Adding functionality to specify a source for a chocolatey repository. (@smadam813) * Removing pre-determined sources and adding specified source url in it's place. (@smadam813) Contains contributions from: * Adam Keech (@smadam813) --- windows/win_chocolatey.ps1 | 339 ++++++++++++++++++++++--------------- windows/win_chocolatey.py | 43 ++--- 2 files changed, 218 insertions(+), 164 deletions(-) diff --git a/windows/win_chocolatey.ps1 b/windows/win_chocolatey.ps1 index de42434da76..4a033d23157 100644 --- a/windows/win_chocolatey.ps1 +++ b/windows/win_chocolatey.ps1 @@ -16,25 +16,11 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +$ErrorActionPreference = "Stop" + # WANT_JSON # POWERSHELL_COMMON -function Write-Log -{ - param - ( - [parameter(mandatory=$false)] - [System.String] - $message - ) - - $date = get-date -format 'yyyy-MM-dd hh:mm:ss.zz' - - Write-Host "$date | $message" - - Out-File -InputObject "$date $message" -FilePath $global:LoggingFile -Append -} - $params = Parse-Args $args; $result = New-Object PSObject; Set-Attr $result "changed" $false; @@ -48,21 +34,22 @@ Else Fail-Json $result "missing required argument: name" } -if(($params.logPath).length -gt 0) +If ($params.force) { - $global:LoggingFile = $params.logPath + $force = $params.force | ConvertTo-Bool } -else +Else { - $global:LoggingFile = "c:\ansible-playbook.log" + $force = $false } -If ($params.force) + +If ($params.upgrade) { - $force = $params.force | ConvertTo-Bool + $upgrade = $params.upgrade | ConvertTo-Bool } Else { - $force = $false + $upgrade = $false } If ($params.version) @@ -74,6 +61,15 @@ Else $version = $null } +If ($params.source) +{ + $source = $params.source.ToString().ToLower() +} +Else +{ + $source = $null +} + If ($params.showlog) { $showlog = $params.showlog | ConvertTo-Bool @@ -96,157 +92,230 @@ Else $state = "present" } -$ChocoAlreadyInstalled = get-command choco -ErrorAction 0 -if ($ChocoAlreadyInstalled -eq $null) +Function Chocolatey-Install-Upgrade { - #We need to install chocolatey - $install_choco_result = iex ((new-object net.webclient).DownloadString("https://chocolatey.org/install.ps1")) - $result.changed = $true - $executable = "C:\ProgramData\chocolatey\bin\choco.exe" -} -Else -{ - $executable = "choco.exe" -} + [CmdletBinding()] -If ($params.source) -{ - $source = $params.source.ToString().ToLower() - If (($source -ne "chocolatey") -and ($source -ne "webpi") -and ($source -ne "windowsfeatures") -and ($source -ne "ruby") -and (!$source.startsWith("http://", "CurrentCultureIgnoreCase")) -and (!$source.startsWith("https://", "CurrentCultureIgnoreCase"))) + param() + + $ChocoAlreadyInstalled = get-command choco -ErrorAction 0 + if ($ChocoAlreadyInstalled -eq $null) + { + #We need to install chocolatey + iex ((new-object net.webclient).DownloadString("https://chocolatey.org/install.ps1")) + $result.changed = $true + $script:executable = "C:\ProgramData\chocolatey\bin\choco.exe" + } + else { - Fail-Json $result "source is $source - must be one of chocolatey, ruby, webpi, windowsfeatures or a custom source url." + $script:executable = "choco.exe" + + if ((choco --version) -lt '0.9.9') + { + Choco-Upgrade chocolatey + } } } -Elseif (!$params.source) + + +Function Choco-IsInstalled { - $source = "chocolatey" + [CmdletBinding()] + + param( + [Parameter(Mandatory=$true, Position=1)] + [string]$package + ) + + $cmd = "$executable list --local-only $package" + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "choco_error_cmd" $cmd + Set-Attr $result "choco_error_log" "$results" + + Throw "Error checking installation status for $package" + } + + If ("$results" -match " $package .* (\d+) packages installed.") + { + return $matches[1] -gt 0 + } + + $false } -if ($source -eq "webpi") +Function Choco-Upgrade { - # check whether 'webpi' installation source is available; if it isn't, install it - $webpi_check_cmd = "$executable list webpicmd -localonly" - $webpi_check_result = invoke-expression $webpi_check_cmd - Set-Attr $result "chocolatey_bootstrap_webpi_check_cmd" $webpi_check_cmd - Set-Attr $result "chocolatey_bootstrap_webpi_check_log" $webpi_check_result - if ( - ( - ($webpi_check_result.GetType().Name -eq "String") -and - ($webpi_check_result -match "No packages found") - ) -or - ($webpi_check_result -contains "No packages found.") + [CmdletBinding()] + + param( + [Parameter(Mandatory=$true, Position=1)] + [string]$package, + [Parameter(Mandatory=$false, Position=2)] + [string]$version, + [Parameter(Mandatory=$false, Position=3)] + [string]$source, + [Parameter(Mandatory=$false, Position=4)] + [bool]$force ) + + if (-not (Choco-IsInstalled $package)) { - #lessmsi is a webpicmd dependency, but dependency resolution fails unless it's installed separately - $lessmsi_install_cmd = "$executable install lessmsi" - $lessmsi_install_result = invoke-expression $lessmsi_install_cmd - Set-Attr $result "chocolatey_bootstrap_lessmsi_install_cmd" $lessmsi_install_cmd - Set-Attr $result "chocolatey_bootstrap_lessmsi_install_log" $lessmsi_install_result + throw "$package is not installed, you cannot upgrade" + } - $webpi_install_cmd = "$executable install webpicmd" - $webpi_install_result = invoke-expression $webpi_install_cmd - Set-Attr $result "chocolatey_bootstrap_webpi_install_cmd" $webpi_install_cmd - Set-Attr $result "chocolatey_bootstrap_webpi_install_log" $webpi_install_result + $cmd = "$executable upgrade -dv -y $package" - if (($webpi_install_result | select-string "already installed").length -gt 0) - { - #no change - } - elseif (($webpi_install_result | select-string "webpicmd has finished successfully").length -gt 0) + if ($version) + { + $cmd += " -version $version" + } + + if ($source) + { + $cmd += " -source $source" + } + + if ($force) + { + $cmd += " -force" + } + + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "choco_error_cmd" $cmd + Set-Attr $result "choco_error_log" "$results" + Throw "Error installing $package" + } + + if ("$results" -match ' upgraded (\d+)/\d+ package\(s\)\. ') + { + if ($matches[1] -gt 0) { $result.changed = $true } - Else - { - Fail-Json $result "WebPI install error: $webpi_install_result" - } } } -$expression = $executable -if ($state -eq "present") -{ - $expression += " install $package" -} -Elseif ($state -eq "absent") -{ - $expression += " uninstall $package" -} -if ($force) + +Function Choco-Install { - if ($state -eq "present") + [CmdletBinding()] + + param( + [Parameter(Mandatory=$true, Position=1)] + [string]$package, + [Parameter(Mandatory=$false, Position=2)] + [string]$version, + [Parameter(Mandatory=$false, Position=3)] + [string]$source, + [Parameter(Mandatory=$false, Position=4)] + [bool]$force, + [Parameter(Mandatory=$false, Position=5)] + [bool]$upgrade + ) + + if (Choco-IsInstalled $package) { - $expression += " -force" + if ($upgrade) + { + Choco-Upgrade -package $package -version $version -source $source -force $force + } + + return } -} -if ($version) -{ - $expression += " -version $version" -} -if ($source -eq "chocolatey") -{ - $expression += " -source https://chocolatey.org/api/v2/" -} -elseif (($source -eq "windowsfeatures") -or ($source -eq "webpi") -or ($source -eq "ruby")) -{ - $expression += " -source $source" -} -elseif(($source -ne $Null) -and ($source -ne "")) -{ - $expression += " -source $source" -} -Set-Attr $result "chocolatey command" $expression -$op_result = invoke-expression $expression -if ($state -eq "present") -{ - if ( - (($op_result | select-string "already installed").length -gt 0) -or - # webpi has different text output, and that doesn't include the package name but instead the human-friendly name - (($op_result | select-string "No products to be installed").length -gt 0) - ) + $cmd = "$executable install -dv -y $package" + + if ($version) { - #no change + $cmd += " -version $version" } - elseif ( - (($op_result | select-string "has finished successfully").length -gt 0) -or - # webpi has different text output, and that doesn't include the package name but instead the human-friendly name - (($op_result | select-string "Install of Products: SUCCESS").length -gt 0) -or - (($op_result | select-string "gem installed").length -gt 0) -or - (($op_result | select-string "gems installed").length -gt 0) - ) + + if ($source) { - $result.changed = $true + $cmd += " -source $source" + } + + if ($force) + { + $cmd += " -force" } - Else + + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) { - Fail-Json $result "Install error: $op_result" + Set-Attr $result "choco_error_cmd" $cmd + Set-Attr $result "choco_error_log" "$results" + Throw "Error installing $package" } + + $result.changed = $true } -Elseif ($state -eq "absent") + +Function Choco-Uninstall { - $op_result = invoke-expression "$executable uninstall $package" - # HACK: Misleading - 'Uninstalling from folder' appears in output even when package is not installed, hence order of checks this way - if ( - (($op_result | select-string "not installed").length -gt 0) -or - (($op_result | select-string "Cannot find path").length -gt 0) + [CmdletBinding()] + + param( + [Parameter(Mandatory=$true, Position=1)] + [string]$package, + [Parameter(Mandatory=$false, Position=2)] + [string]$version, + [Parameter(Mandatory=$false, Position=3)] + [bool]$force ) + + if (-not (Choco-IsInstalled $package)) { - #no change + return } - elseif (($op_result | select-string "Uninstalling from folder").length -gt 0) + + $cmd = "$executable uninstall -dv -y $package" + + if ($version) { - $result.changed = $true + $cmd += " -version $version" } - else + + if ($force) { - Fail-Json $result "Uninstall error: $op_result" + $cmd += " -force" } + + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "choco_error_cmd" $cmd + Set-Attr $result "choco_error_log" "$results" + Throw "Error uninstalling $package" + } + + $result.changed = $true } +Try +{ + Chocolatey-Install-Upgrade + + if ($state -eq "present") + { + Choco-Install -package $package -version $version -source $source ` + -force $force -upgrade $upgrade + } + else + { + Choco-Uninstall -package $package -version $version -force $force + } -if ($showlog) + Exit-Json $result; +} +Catch { - Set-Attr $result "chocolatey_log" $op_result + Fail-Json $result $_.Exception.Message } -Set-Attr $result "chocolatey_success" "true" -Exit-Json $result; diff --git a/windows/win_chocolatey.py b/windows/win_chocolatey.py index 63ec1ecd214..fe00f2e0f6a 100644 --- a/windows/win_chocolatey.py +++ b/windows/win_chocolatey.py @@ -53,42 +53,29 @@ - no default: no aliases: [] - version: + upgrade: description: - - Specific version of the package to be installed - - Ignored when state == 'absent' - required: false - default: null - aliases: [] - showlog: - description: - - Outputs the chocolatey log inside a chocolatey_log property. + - If package is already installed it, try to upgrade to the latest version or to the specified version required: false choices: - yes - no default: no aliases: [] - source: + version: description: - - Which source to install from - require: false - choices: - - chocolatey - - ruby - - webpi - - windowsfeatures - default: chocolatey + - Specific version of the package to be installed + - Ignored when state == 'absent' + required: false + default: null aliases: [] - logPath: + source: description: - - Where to log command output to + - Specify source rather than using default chocolatey repository require: false - default: c:\\ansible-playbook.log + default: null aliases: [] -author: - - '"Trond Hindenes (@trondhindenes)" ' - - '"Peter Mounce (@petemounce)" ' +author: Trond Hindenes, Peter Mounce, Pepe Barbe, Adam Keech ''' # TODO: @@ -111,10 +98,8 @@ name: git state: absent - # Install Application Request Routing v3 from webpi - # Logically, this requires that you install IIS first (see win_feature) - # To find a list of packages available via webpi source, `choco list -source webpi` + # Install git from specified repository win_chocolatey: - name: ARRv3 - source: webpi + name: git + source: https://someserver/api/v2/ ''' From 53bb87d110d2e4b8dd429f66ab93e2d2bf646335 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Sun, 7 Jun 2015 17:45:33 -0400 Subject: [PATCH 0102/2522] added missing options: --- cloud/cloudstack/cs_project.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud/cloudstack/cs_project.py b/cloud/cloudstack/cs_project.py index b604a1b6f32..e604abc13db 100644 --- a/cloud/cloudstack/cs_project.py +++ b/cloud/cloudstack/cs_project.py @@ -26,6 +26,7 @@ - Create, update, suspend, activate and remove projects. version_added: '2.0' author: '"René Moser (@resmo)" ' +options: name: description: - Name of the project. From bcee7c13cfd867c880914d8547e3ddee844acf46 Mon Sep 17 00:00:00 2001 From: "jonathan.lestrelin" Date: Mon, 8 Jun 2015 09:28:01 +0200 Subject: [PATCH 0103/2522] Fix unused import and variable and correct documentation --- packaging/language/pear.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packaging/language/pear.py b/packaging/language/pear.py index c9e3862a31f..5762f9c815c 100644 --- a/packaging/language/pear.py +++ b/packaging/language/pear.py @@ -26,16 +26,14 @@ short_description: Manage pear/pecl packages description: - Manage PHP packages with the pear package manager. +version_added: 2.0 author: - "'jonathan.lestrelin' " -notes: [] -requirements: [] options: name: description: - Name of the package to install, upgrade, or remove. required: true - default: null state: description: @@ -132,7 +130,7 @@ def remove_packages(module, packages): module.exit_json(changed=False, msg="package(s) already absent") -def install_packages(module, state, packages, package_files): +def install_packages(module, state, packages): install_c = 0 for i, package in enumerate(packages): @@ -178,7 +176,6 @@ def check_packages(module, packages, state): else: module.exit_json(change=False, msg="package(s) already %s" % state) -import os def exe_exists(program): for path in os.environ["PATH"].split(os.pathsep): @@ -220,7 +217,7 @@ def main(): check_packages(module, pkgs, p['state']) if p['state'] in ['present', 'latest']: - install_packages(module, p['state'], pkgs, pkg_files) + install_packages(module, p['state'], pkgs) elif p['state'] == 'absent': remove_packages(module, pkgs) From f09389b1792a720cc9eede346eebeb1a6a88510f Mon Sep 17 00:00:00 2001 From: Jhonny Everson Date: Mon, 8 Jun 2015 17:46:53 -0300 Subject: [PATCH 0104/2522] Adds handler for error responses --- monitoring/datadog_monitor.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py index 24de8af10ba..97968ed648d 100644 --- a/monitoring/datadog_monitor.py +++ b/monitoring/datadog_monitor.py @@ -187,7 +187,10 @@ def _post_monitor(module, options): msg = api.Monitor.create(type=module.params['type'], query=module.params['query'], name=module.params['name'], message=module.params['message'], options=options) - module.exit_json(changed=True, msg=msg) + if 'errors' in msg: + module.fail_json(msg=str(msg['errors'])) + else: + module.exit_json(changed=True, msg=msg) except Exception, e: module.fail_json(msg=str(e)) @@ -197,7 +200,9 @@ def _update_monitor(module, monitor, options): msg = api.Monitor.update(id=monitor['id'], query=module.params['query'], name=module.params['name'], message=module.params['message'], options=options) - if len(set(msg) - set(monitor)) == 0: + if 'errors' in msg: + module.fail_json(msg=str(msg['errors'])) + elif len(set(msg) - set(monitor)) == 0: module.exit_json(changed=False, msg=msg) else: module.exit_json(changed=True, msg=msg) @@ -243,7 +248,7 @@ def mute_monitor(module): module.fail_json(msg="Monitor %s not found!" % module.params['name']) elif monitor['options']['silenced']: module.fail_json(msg="Monitor is already muted. Datadog does not allow to modify muted alerts, consider unmuting it first.") - elif (module.params['silenced'] is not None + elif (module.params['silenced'] is not None and len(set(monitor['options']['silenced']) - set(module.params['silenced'])) == 0): module.exit_json(changed=False) try: From 443be858f1b2705617787ab4035ba00e3f840e7d Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Tue, 9 Jun 2015 13:06:24 +0200 Subject: [PATCH 0105/2522] cloudstack: fix project name must not be case sensitiv --- cloud/cloudstack/cs_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/cloudstack/cs_project.py b/cloud/cloudstack/cs_project.py index e604abc13db..13209853527 100644 --- a/cloud/cloudstack/cs_project.py +++ b/cloud/cloudstack/cs_project.py @@ -167,7 +167,7 @@ def get_project(self): projects = self.cs.listProjects(**args) if projects: for p in projects['project']: - if project in [ p['name'], p['id']]: + if project.lower() in [ p['name'].lower(), p['id']]: self.project = p break return self.project From 1b8eb9091b53610e8cf71562509c610e5f0ef23e Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Tue, 9 Jun 2015 13:08:38 +0200 Subject: [PATCH 0106/2522] cloudstack: remove listall in cs_project listall in cs_project can return the wrong project for root admins, because project name are not unique in separate accounts. --- cloud/cloudstack/cs_project.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cloud/cloudstack/cs_project.py b/cloud/cloudstack/cs_project.py index 13209853527..b505433892e 100644 --- a/cloud/cloudstack/cs_project.py +++ b/cloud/cloudstack/cs_project.py @@ -160,7 +160,6 @@ def get_project(self): project = self.module.params.get('name') args = {} - args['listall'] = True args['account'] = self.get_account(key='name') args['domainid'] = self.get_domain(key='id') From d517abf44b515746f44c757e0949977e68e6f723 Mon Sep 17 00:00:00 2001 From: Jhonny Everson Date: Tue, 9 Jun 2015 09:44:34 -0300 Subject: [PATCH 0107/2522] Fixes the bug where it was using only the keys to determine whether a change was made, i.e. values changes for existing keys was reported incorrectly. --- monitoring/datadog_monitor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py index 97968ed648d..cb54cd32b5d 100644 --- a/monitoring/datadog_monitor.py +++ b/monitoring/datadog_monitor.py @@ -194,6 +194,10 @@ def _post_monitor(module, options): except Exception, e: module.fail_json(msg=str(e)) +def _equal_dicts(a, b, ignore_keys): + ka = set(a).difference(ignore_keys) + kb = set(b).difference(ignore_keys) + return ka == kb and all(a[k] == b[k] for k in ka) def _update_monitor(module, monitor, options): try: @@ -202,7 +206,7 @@ def _update_monitor(module, monitor, options): options=options) if 'errors' in msg: module.fail_json(msg=str(msg['errors'])) - elif len(set(msg) - set(monitor)) == 0: + elif _equal_dicts(msg, monitor, ['creator', 'overall_state']): module.exit_json(changed=False, msg=msg) else: module.exit_json(changed=True, msg=msg) From bca0d2d32b105b34d050754a1ba69353805ff60d Mon Sep 17 00:00:00 2001 From: David Siefert Date: Tue, 9 Jun 2015 10:21:33 -0500 Subject: [PATCH 0108/2522] Adding support for setting the topic of a channel --- notification/irc.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/notification/irc.py b/notification/irc.py index 8b87c41f1ba..e6852c8510a 100644 --- a/notification/irc.py +++ b/notification/irc.py @@ -47,6 +47,12 @@ - The message body. required: true default: null + topic: + description: + - Set the channel topic + required: false + default: null + version_added: 2.0 color: description: - Text color for the message. ("none" is a valid option in 1.6 or later, in 1.6 and prior, the default color is black, not "none"). @@ -106,7 +112,7 @@ from time import sleep -def send_msg(channel, msg, server='localhost', port='6667', key=None, +def send_msg(channel, msg, server='localhost', port='6667', key=None, topic=None, nick="ansible", color='none', passwd=False, timeout=30, use_ssl=False): '''send message to IRC''' @@ -163,6 +169,10 @@ def send_msg(channel, msg, server='localhost', port='6667', key=None, raise Exception('Timeout waiting for IRC JOIN response') sleep(0.5) + if topic is not None: + irc.send('TOPIC %s :%s\r\n' % (channel, topic)) + sleep(1) + irc.send('PRIVMSG %s :%s\r\n' % (channel, message)) sleep(1) irc.send('PART %s\r\n' % channel) @@ -186,6 +196,7 @@ def main(): "blue", "black", "none"]), channel=dict(required=True), key=dict(), + topic=dict(), passwd=dict(), timeout=dict(type='int', default=30), use_ssl=dict(type='bool', default=False) @@ -196,6 +207,7 @@ def main(): server = module.params["server"] port = module.params["port"] nick = module.params["nick"] + topic = module.params["topic"] msg = module.params["msg"] color = module.params["color"] channel = module.params["channel"] @@ -205,7 +217,7 @@ def main(): use_ssl = module.params["use_ssl"] try: - send_msg(channel, msg, server, port, key, nick, color, passwd, timeout, use_ssl) + send_msg(channel, msg, server, port, key, topic, nick, color, passwd, timeout, use_ssl) except Exception, e: module.fail_json(msg="unable to send to IRC: %s" % e) From 70a7a46d52751f9bed4cb4fa8de08ae9ac9f57ad Mon Sep 17 00:00:00 2001 From: Rick Mendes Date: Tue, 9 Jun 2015 08:55:58 -0700 Subject: [PATCH 0109/2522] optionally supports waiting for the password to be available --- cloud/amazon/ec2_win_password.py | 59 ++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/cloud/amazon/ec2_win_password.py b/cloud/amazon/ec2_win_password.py index 6a81192016a..05aa67e3d29 100644 --- a/cloud/amazon/ec2_win_password.py +++ b/cloud/amazon/ec2_win_password.py @@ -15,7 +15,7 @@ required: true key_file: description: - - path to the file containing the key pair used on the instance + - Path to the file containing the key pair used on the instance. required: true key_passphrase: version_added: "2.0" @@ -28,6 +28,18 @@ required: false default: null aliases: [ 'aws_region', 'ec2_region' ] + wait: + version_added: "2.0" + description: + - Whether or not to wait for the password to be available before returning. + required: false + default: "no" + choices: [ "yes", "no" ] + wait_timeout: + version_added: "2.0" + description: + - Number of seconds to wait before giving up. + default: 120 extends_documentation_fragment: aws ''' @@ -51,12 +63,24 @@ region: us-east-1 key_file: "~/aws-creds/my_protected_test_key.pem" key_passphrase: "secret" + +# Example of waiting for a password +tasks: +- name: get the Administrator password + ec2_win_password: + profile: my-boto-profile + instance_id: i-XXXXXX + region: us-east-1 + key_file: "~/aws-creds/my_test_key.pem" + wait: yes + wait_timeout: 45 ''' from base64 import b64decode from os.path import expanduser from Crypto.Cipher import PKCS1_v1_5 from Crypto.PublicKey import RSA +import datetime try: import boto.ec2 @@ -70,6 +94,8 @@ def main(): instance_id = dict(required=True), key_file = dict(required=True), key_passphrase = dict(default=None), + wait = dict(type='bool', default=False), + wait_timeout = dict(default=120), ) ) module = AnsibleModule(argument_spec=argument_spec) @@ -80,11 +106,28 @@ def main(): instance_id = module.params.get('instance_id') key_file = expanduser(module.params.get('key_file')) key_passphrase = module.params.get('key_passphrase') + wait = module.params.get('wait') + wait_timeout = int(module.params.get('wait_timeout')) ec2 = ec2_connect(module) - data = ec2.get_password_data(instance_id) - decoded = b64decode(data) + if wait: + start = datetime.datetime.now() + end = start + datetime.timedelta(seconds=wait_timeout) + + while datetime.datetime.now() < end: + data = ec2.get_password_data(instance_id) + decoded = b64decode(data) + if wait and not decoded: + time.sleep(5) + else: + break + else: + data = ec2.get_password_data(instance_id) + decoded = b64decode(data) + + if wait and datetime.datetime.now() >= end: + module.fail_json(msg = "wait for password timeout after %d seconds" % wait_timeout) f = open(key_file, 'r') key = RSA.importKey(f.read(), key_passphrase) @@ -92,14 +135,18 @@ def main(): sentinel = 'password decryption failed!!!' try: - decrypted = cipher.decrypt(decoded, sentinel) + decrypted = cipher.decrypt(decoded, sentinel) except ValueError as e: - decrypted = None + decrypted = None if decrypted == None: module.exit_json(win_password='', changed=False) else: - module.exit_json(win_password=decrypted, changed=True) + if wait: + elapsed = datetime.datetime.now() - start + module.exit_json(win_password=decrypted, changed=True, elapsed=elapsed.seconds) + else: + module.exit_json(win_password=decrypted, changed=True) # import module snippets from ansible.module_utils.basic import * From ef7381f24636a350dd7bd0d061634fd2203d1b61 Mon Sep 17 00:00:00 2001 From: Greg DeKoenigsberg Date: Tue, 9 Jun 2015 12:58:45 -0400 Subject: [PATCH 0110/2522] Adding author's github id --- monitoring/datadog_monitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py index cb54cd32b5d..f1acb169ce0 100644 --- a/monitoring/datadog_monitor.py +++ b/monitoring/datadog_monitor.py @@ -34,7 +34,7 @@ - "Manages monitors within Datadog" - "Options like described on http://docs.datadoghq.com/api/" version_added: "2.0" -author: '"Sebastian Kornehl" ' +author: '"Sebastian Kornehl (@skornehl)" ' notes: [] requirements: [datadog] options: From 2643c3eddad1d313ead9131405f66c927ba999d2 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Wed, 10 Jun 2015 13:00:02 +0200 Subject: [PATCH 0111/2522] puppet: update author to new format --- system/puppet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/puppet.py b/system/puppet.py index 83bbcbe6e18..336b2c81108 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -65,7 +65,7 @@ required: false default: None requirements: [ puppet ] -author: Monty Taylor +author: "Monty Taylor (@emonty)" ''' EXAMPLES = ''' From 2f967a949f9a45657c31ae66c0c7e7c2672a87d8 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Wed, 10 Jun 2015 12:58:44 -0400 Subject: [PATCH 0112/2522] minor docfix --- monitoring/nagios.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/nagios.py b/monitoring/nagios.py index 543f094b70e..0026751ea58 100644 --- a/monitoring/nagios.py +++ b/monitoring/nagios.py @@ -77,7 +77,7 @@ version_added: "2.0" description: - the Servicegroup we want to set downtimes/alerts for. - B(Required) option when using the C(servicegroup_service_downtime) amd C(servicegroup_host_downtime). + B(Required) option when using the C(servicegroup_service_downtime) amd C(servicegroup_host_downtime). command: description: - The raw command to send to nagios, which From bec97ff60e95029efe17e3781ac8de64ce10478e Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Wed, 10 Jun 2015 23:31:48 +0200 Subject: [PATCH 0113/2522] cloudstack: add new module cs_network --- cloud/cloudstack/cs_network.py | 637 +++++++++++++++++++++++++++++++++ 1 file changed, 637 insertions(+) create mode 100644 cloud/cloudstack/cs_network.py diff --git a/cloud/cloudstack/cs_network.py b/cloud/cloudstack/cs_network.py new file mode 100644 index 00000000000..c8b3b32539d --- /dev/null +++ b/cloud/cloudstack/cs_network.py @@ -0,0 +1,637 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2015, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: cs_network +short_description: Manages networks on Apache CloudStack based clouds. +description: + - Create, update, restart and delete networks. +version_added: '2.0' +author: '"René Moser (@resmo)" ' +options: + name: + description: + - Name (case sensitive) of the network. + required: true + displaytext: + description: + - Displaytext of the network. + - If not specified, C(name) will be used as displaytext. + required: false + default: null + network_offering: + description: + - Name of the offering for the network. + - Required if C(state=present). + required: false + default: null + start_ip: + description: + - The beginning IPv4 address of the network belongs to. + - Only considered on create. + required: false + default: null + end_ip: + description: + - The ending IPv4 address of the network belongs to. + - If not specified, value of C(start_ip) is used. + - Only considered on create. + required: false + default: null + gateway: + description: + - The gateway of the network. + - Required for shared networks and isolated networks when it belongs to VPC. + - Only considered on create. + required: false + default: null + netmask: + description: + - The netmask of the network. + - Required for shared networks and isolated networks when it belongs to VPC. + - Only considered on create. + required: false + default: null + start_ipv6: + description: + - The beginning IPv6 address of the network belongs to. + - Only considered on create. + required: false + default: null + end_ipv6: + description: + - The ending IPv6 address of the network belongs to. + - If not specified, value of C(start_ipv6) is used. + - Only considered on create. + required: false + default: null + cidr_ipv6: + description: + - CIDR of IPv6 network, must be at least /64. + - Only considered on create. + required: false + default: null + gateway_ipv6: + description: + - The gateway of the IPv6 network. + - Required for shared networks. + - Only considered on create. + required: false + default: null + vlan: + description: + - The ID or VID of the network. + required: false + default: null + vpc: + description: + - The ID or VID of the network. + required: false + default: null + isolated_pvlan: + description: + - The isolated private vlan for this network. + required: false + default: null + clean_up: + description: + - Cleanup old network elements. + - Only considered on C(state=restarted). + required: false + default: null + acl_type: + description: + - Access control type. + - Only considered on create. + required: false + default: account + choices: [ 'account', 'domain' ] + network_domain: + description: + - The network domain. + required: false + default: null + state: + description: + - State of the network. + required: false + default: present + choices: [ 'present', 'absent', 'restarted' ] + zone: + description: + - Name of the zone in which the network should be deployed. + - If not set, default zone is used. + required: false + default: null + project: + description: + - Name of the project the network to be deployed in. + required: false + default: null + domain: + description: + - Domain the network is related to. + required: false + default: null + account: + description: + - Account the network is related to. + required: false + default: null + poll_async: + description: + - Poll async jobs until job has finished. + required: false + default: true +extends_documentation_fragment: cloudstack +''' + +EXAMPLES = ''' +# create a network +- local_action: + module: cs_network + name: my network + zone: gva-01 + network_offering: DefaultIsolatedNetworkOfferingWithSourceNatService + network_domain: example.com + +# update a network +- local_action: + module: cs_network + name: my network + displaytext: network of domain example.local + network_domain: example.local + +# restart a network with clean up +- local_action: + module: cs_network + name: my network + clean_up: yes + state: restared + +# remove a network +- local_action: + module: cs_network + name: my network + state: absent +''' + +RETURN = ''' +--- +id: + description: ID of the network. + returned: success + type: string + sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6 +name: + description: Name of the network. + returned: success + type: string + sample: web project +displaytext: + description: Display text of the network. + returned: success + type: string + sample: web project +dns1: + description: IP address of the 1st nameserver. + returned: success + type: string + sample: 1.2.3.4 +dns2: + description: IP address of the 2nd nameserver. + returned: success + type: string + sample: 1.2.3.4 +cidr: + description: IPv4 network CIDR. + returned: success + type: string + sample: 10.101.64.0/24 +gateway: + description: IPv4 gateway. + returned: success + type: string + sample: 10.101.64.1 +netmask: + description: IPv4 netmask. + returned: success + type: string + sample: 255.255.255.0 +cidr_ipv6: + description: IPv6 network CIDR. + returned: success + type: string + sample: 2001:db8::/64 +gateway_ipv6: + description: IPv6 gateway. + returned: success + type: string + sample: 2001:db8::1 +state: + description: State of the network. + returned: success + type: string + sample: Implemented +zone: + description: Name of zone. + returned: success + type: string + sample: ch-gva-2 +domain: + description: Domain the network is related to. + returned: success + type: string + sample: ROOT +account: + description: Account the network is related to. + returned: success + type: string + sample: example account +project: + description: Name of project. + returned: success + type: string + sample: Production +tags: + description: List of resource tags associated with the network. + returned: success + type: dict + sample: '[ { "key": "foo", "value": "bar" } ]' +acl_type: + description: Access type of the network (Domain, Account). + returned: success + type: string + sample: Account +broadcast_domaintype: + description: Broadcast domain type of the network. + returned: success + type: string + sample: Vlan +type: + description: Type of the network. + returned: success + type: string + sample: Isolated +traffic_type: + description: Traffic type of the network. + returned: success + type: string + sample: Guest +state: + description: State of the network (Allocated, Implemented, Setup). + returned: success + type: string + sample: Allocated +is_persistent: + description: Whether the network is persistent or not. + returned: success + type: boolean + sample: false +network_domain: + description: The network domain + returned: success + type: string + sample: example.local +network_offering: + description: The network offering name. + returned: success + type: string + sample: DefaultIsolatedNetworkOfferingWithSourceNatService +''' + +try: + from cs import CloudStack, CloudStackException, read_config + has_lib_cs = True +except ImportError: + has_lib_cs = False + +# import cloudstack common +from ansible.module_utils.cloudstack import * + + +class AnsibleCloudStackNetwork(AnsibleCloudStack): + + def __init__(self, module): + AnsibleCloudStack.__init__(self, module) + self.network = None + + + def get_or_fallback(self, key=None, fallback_key=None): + value = self.module.params.get(key) + if not value: + value = self.module.params.get(fallback_key) + return value + + + def get_vpc(self, key=None): + vpc = self.module.params.get('vpc') + if not vpc: + return None + + args = {} + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') + args['projectid'] = self.get_project(key='id') + args['zoneid'] = self.get_zone(key='id') + + vpcs = self.cs.listVPCs(**args) + if vpcs: + for v in vpcs['vpc']: + if vpc in [ v['name'], v['displaytext'], v['id'] ]: + return self._get_by_key(key, v) + self.module.fail_json(msg="VPC '%s' not found" % vpc) + + + def get_network_offering(self, key=None): + network_offering = self.module.params.get('network_offering') + if not network_offering: + self.module.fail_json(msg="missing required arguments: network_offering") + + args = {} + args['zoneid'] = self.get_zone(key='id') + + network_offerings = self.cs.listNetworkOfferings(**args) + if network_offerings: + for no in network_offerings['networkoffering']: + if network_offering in [ no['name'], no['displaytext'], no['id'] ]: + return self._get_by_key(key, no) + self.module.fail_json(msg="Network offering '%s' not found" % network_offering) + + + def _get_args(self): + args = {} + args['name'] = self.module.params.get('name') + args['displaytext'] = self.get_or_fallback('displaytext','name') + args['networkdomain'] = self.module.params.get('network_domain') + args['networkofferingid'] = self.get_network_offering(key='id') + return args + + + def get_network(self): + if not self.network: + network = self.module.params.get('name') + + args = {} + args['zoneid'] = self.get_zone(key='id') + args['projectid'] = self.get_project(key='id') + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') + + networks = self.cs.listNetworks(**args) + if networks: + for n in networks['network']: + if network in [ n['name'], n['displaytext'], n['id']]: + self.network = n + break + return self.network + + + def present_network(self): + network = self.get_network() + if not network: + network = self.create_network(network) + else: + network = self.update_network(network) + return network + + + def update_network(self, network): + args = self._get_args() + args['id'] = network['id'] + + if self._has_changed(args, network): + self.result['changed'] = True + if not self.module.check_mode: + network = self.cs.updateNetwork(**args) + + if 'errortext' in network: + self.module.fail_json(msg="Failed: '%s'" % network['errortext']) + + poll_async = self.module.params.get('poll_async') + if network and poll_async: + network = self._poll_job(network, 'network') + return network + + + def create_network(self, network): + self.result['changed'] = True + + args = self._get_args() + args['acltype'] = self.module.params.get('acl_type') + args['zoneid'] = self.get_zone(key='id') + args['projectid'] = self.get_project(key='id') + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') + args['startip'] = self.module.params.get('start_ip') + args['endip'] = self.get_or_fallback('end_ip', 'start_ip') + args['netmask'] = self.module.params.get('netmask') + args['gateway'] = self.module.params.get('gateway') + args['startipv6'] = self.module.params.get('start_ipv6') + args['endipv6'] = self.get_or_fallback('end_ipv6', 'start_ipv6') + args['ip6cidr'] = self.module.params.get('cidr_ipv6') + args['ip6gateway'] = self.module.params.get('gateway_ipv6') + args['vlan'] = self.module.params.get('vlan') + args['isolatedpvlan'] = self.module.params.get('isolated_pvlan') + args['subdomainaccess'] = self.module.params.get('subdomain_access') + args['vpcid'] = self.get_vpc(key='id') + + if not self.module.check_mode: + res = self.cs.createNetwork(**args) + + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + + network = res['network'] + return network + + + def restart_network(self): + network = self.get_network() + + if not network: + self.module.fail_json(msg="No network named '%s' found." % self.module.params('name')) + + # Restarting only available for these states + if network['state'].lower() in [ 'implemented', 'setup' ]: + self.result['changed'] = True + + args = {} + args['id'] = network['id'] + args['cleanup'] = self.module.params.get('clean_up') + + if not self.module.check_mode: + network = self.cs.restartNetwork(**args) + + if 'errortext' in network: + self.module.fail_json(msg="Failed: '%s'" % network['errortext']) + + poll_async = self.module.params.get('poll_async') + if network and poll_async: + network = self._poll_job(network, 'network') + return network + + + def absent_network(self): + network = self.get_network() + if network: + self.result['changed'] = True + + args = {} + args['id'] = network['id'] + + if not self.module.check_mode: + res = self.cs.deleteNetwork(**args) + + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + + poll_async = self.module.params.get('poll_async') + if res and poll_async: + res = self._poll_job(res, 'network') + return network + + + def get_result(self, network): + if network: + if 'id' in network: + self.result['id'] = network['id'] + if 'name' in network: + self.result['name'] = network['name'] + if 'displaytext' in network: + self.result['displaytext'] = network['displaytext'] + if 'dns1' in network: + self.result['dns1'] = network['dns1'] + if 'dns2' in network: + self.result['dns2'] = network['dns2'] + if 'cidr' in network: + self.result['cidr'] = network['cidr'] + if 'broadcastdomaintype' in network: + self.result['broadcast_domaintype'] = network['broadcastdomaintype'] + if 'netmask' in network: + self.result['netmask'] = network['netmask'] + if 'gateway' in network: + self.result['gateway'] = network['gateway'] + if 'ip6cidr' in network: + self.result['cidr_ipv6'] = network['ip6cidr'] + if 'ip6gateway' in network: + self.result['gateway_ipv6'] = network['ip6gateway'] + if 'state' in network: + self.result['state'] = network['state'] + if 'type' in network: + self.result['type'] = network['type'] + if 'traffictype' in network: + self.result['traffic_type'] = network['traffictype'] + if 'zone' in network: + self.result['zone'] = network['zonename'] + if 'domain' in network: + self.result['domain'] = network['domain'] + if 'account' in network: + self.result['account'] = network['account'] + if 'project' in network: + self.result['project'] = network['project'] + if 'acltype' in network: + self.result['acl_type'] = network['acltype'] + if 'networkdomain' in network: + self.result['network_domain'] = network['networkdomain'] + if 'networkofferingname' in network: + self.result['network_offering'] = network['networkofferingname'] + if 'ispersistent' in network: + self.result['is_persistent'] = network['ispersistent'] + if 'tags' in network: + self.result['tags'] = [] + for tag in network['tags']: + result_tag = {} + result_tag['key'] = tag['key'] + result_tag['value'] = tag['value'] + self.result['tags'].append(result_tag) + return self.result + + +def main(): + module = AnsibleModule( + argument_spec = dict( + name = dict(required=True), + displaytext = dict(default=None), + network_offering = dict(default=None), + zone = dict(default=None), + start_ip = dict(default=None), + end_ip = dict(default=None), + gateway = dict(default=None), + netmask = dict(default=None), + start_ipv6 = dict(default=None), + end_ipv6 = dict(default=None), + cidr_ipv6 = dict(default=None), + gateway_ipv6 = dict(default=None), + vlan = dict(default=None), + vpc = dict(default=None), + isolated_pvlan = dict(default=None), + clean_up = dict(default=None), + network_domain = dict(default=None), + state = dict(choices=['present', 'absent', 'restarted' ], default='present'), + acl_type = dict(choices=['account', 'domain'], default='account'), + project = dict(default=None), + domain = dict(default=None), + account = dict(default=None), + poll_async = dict(type='bool', choices=BOOLEANS, default=True), + api_key = dict(default=None), + api_secret = dict(default=None, no_log=True), + api_url = dict(default=None), + api_http_method = dict(choices=['get', 'post'], default='get'), + api_timeout = dict(type='int', default=10), + ), + required_together = ( + ['api_key', 'api_secret', 'api_url'], + ['start_ip', 'netmask', 'gateway'], + ['start_ipv6', 'cidr_ipv6', 'gateway_ipv6'], + ), + supports_check_mode=True + ) + + if not has_lib_cs: + module.fail_json(msg="python library cs required: pip install cs") + + try: + acs_network = AnsibleCloudStackNetwork(module) + + state = module.params.get('state') + if state in ['absent']: + network = acs_network.absent_network() + + elif state in ['restarted']: + network = acs_network.restart_network() + + else: + network = acs_network.present_network() + + result = acs_network.get_result(network) + + except CloudStackException, e: + module.fail_json(msg='CloudStackException: %s' % str(e)) + + except Exception, e: + module.fail_json(msg='Exception: %s' % str(e)) + + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * +main() From 4f38c4387b7dc079af2fa3f684d68eb7bab2b541 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Thu, 11 Jun 2015 11:36:34 -0500 Subject: [PATCH 0114/2522] Add new module 'expect' --- commands/__init__.py | 0 commands/expect.py | 189 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 commands/__init__.py create mode 100644 commands/expect.py diff --git a/commands/__init__.py b/commands/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/commands/expect.py b/commands/expect.py new file mode 100644 index 00000000000..0922ba4e464 --- /dev/null +++ b/commands/expect.py @@ -0,0 +1,189 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Matt Martz +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import datetime + +try: + import pexpect + HAS_PEXPECT = True +except ImportError: + HAS_PEXPECT = False + + +DOCUMENTATION = ''' +--- +module: expect +version_added: 2.0 +short_description: Executes a command and responds to prompts +description: + - The M(expect) module executes a command and responds to prompts + - The given command will be executed on all selected nodes. It will not be + processed through the shell, so variables like C($HOME) and operations + like C("<"), C(">"), C("|"), and C("&") will not work +options: + command: + description: + - the command module takes command to run. + required: true + creates: + description: + - a filename, when it already exists, this step will B(not) be run. + required: false + removes: + description: + - a filename, when it does not exist, this step will B(not) be run. + required: false + chdir: + description: + - cd into this directory before running the command + required: false + executable: + description: + - change the shell used to execute the command. Should be an absolute + path to the executable. + required: false + responses: + description: + - Mapping of expected string and string to respond with + required: true + timeout: + description: + - Amount of time in seconds to wait for the expected strings + default: 30 + echo: + description: + - Whether or not to echo out your response strings + default: false +requirements: + - python >= 2.6 + - pexpect >= 3.3 +notes: + - If you want to run a command through the shell (say you are using C(<), + C(>), C(|), etc), you must specify a shell in the command such as + C(/bin/bash -c "/path/to/something | grep else") +author: '"Matt Martz (@sivel)" ' +''' + +EXAMPLES = ''' +- expect: + command: passwd username + responses: + (?i)password: "MySekretPa$$word" +''' + + +def main(): + module = AnsibleModule( + argument_spec=dict( + command=dict(required=True), + chdir=dict(), + executable=dict(), + creates=dict(), + removes=dict(), + responses=dict(type='dict', required=True), + timeout=dict(type='int', default=30), + echo=dict(type='bool', default=False), + ) + ) + + if not HAS_PEXPECT: + module.fail_json(msg='The pexpect python module is required') + + chdir = module.params['chdir'] + executable = module.params['executable'] + args = module.params['command'] + creates = module.params['creates'] + removes = module.params['removes'] + responses = module.params['responses'] + timeout = module.params['timeout'] + echo = module.params['echo'] + + events = dict() + for key, value in responses.iteritems(): + events[key.decode()] = u'%s\n' % value.rstrip('\n').decode() + + if args.strip() == '': + module.fail_json(rc=256, msg="no command given") + + if chdir: + chdir = os.path.abspath(os.path.expanduser(chdir)) + os.chdir(chdir) + + if creates: + # do not run the command if the line contains creates=filename + # and the filename already exists. This allows idempotence + # of command executions. + v = os.path.expanduser(creates) + if os.path.exists(v): + module.exit_json( + cmd=args, + stdout="skipped, since %s exists" % v, + changed=False, + stderr=False, + rc=0 + ) + + if removes: + # do not run the command if the line contains removes=filename + # and the filename does not exist. This allows idempotence + # of command executions. + v = os.path.expanduser(removes) + if not os.path.exists(v): + module.exit_json( + cmd=args, + stdout="skipped, since %s does not exist" % v, + changed=False, + stderr=False, + rc=0 + ) + + startd = datetime.datetime.now() + + if executable: + cmd = '%s %s' % (executable, args) + else: + cmd = args + + try: + out, rc = pexpect.runu(cmd, timeout=timeout, withexitstatus=True, + events=events, cwd=chdir, echo=echo) + except pexpect.ExceptionPexpect, e: + module.fail_json(msg='%s' % e) + + endd = datetime.datetime.now() + delta = endd - startd + + if out is None: + out = '' + + module.exit_json( + cmd=args, + stdout=out.rstrip('\r\n'), + rc=rc, + start=str(startd), + end=str(endd), + delta=str(delta), + changed=True, + ) + +# import module snippets +from ansible.module_utils.basic import * + +main() From 76e382abaa3f5906dc79a4d9bfeb66c39892ebc8 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Thu, 11 Jun 2015 12:36:47 -0500 Subject: [PATCH 0115/2522] Remove the executable option as it's redundant --- commands/expect.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/commands/expect.py b/commands/expect.py index 0922ba4e464..124c718b73b 100644 --- a/commands/expect.py +++ b/commands/expect.py @@ -54,11 +54,6 @@ description: - cd into this directory before running the command required: false - executable: - description: - - change the shell used to execute the command. Should be an absolute - path to the executable. - required: false responses: description: - Mapping of expected string and string to respond with @@ -94,7 +89,6 @@ def main(): argument_spec=dict( command=dict(required=True), chdir=dict(), - executable=dict(), creates=dict(), removes=dict(), responses=dict(type='dict', required=True), @@ -107,7 +101,6 @@ def main(): module.fail_json(msg='The pexpect python module is required') chdir = module.params['chdir'] - executable = module.params['executable'] args = module.params['command'] creates = module.params['creates'] removes = module.params['removes'] @@ -156,13 +149,8 @@ def main(): startd = datetime.datetime.now() - if executable: - cmd = '%s %s' % (executable, args) - else: - cmd = args - try: - out, rc = pexpect.runu(cmd, timeout=timeout, withexitstatus=True, + out, rc = pexpect.runu(args, timeout=timeout, withexitstatus=True, events=events, cwd=chdir, echo=echo) except pexpect.ExceptionPexpect, e: module.fail_json(msg='%s' % e) From 4fc275d1c59e91864f1f84af950e79bd28759fd2 Mon Sep 17 00:00:00 2001 From: Alex Lo Date: Fri, 12 Jun 2015 00:49:37 -0400 Subject: [PATCH 0116/2522] remove extraneous imports --- cloud/amazon/cloudtrail.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/cloud/amazon/cloudtrail.py b/cloud/amazon/cloudtrail.py index 6a1885d6ee7..d6ed254df91 100644 --- a/cloud/amazon/cloudtrail.py +++ b/cloud/amazon/cloudtrail.py @@ -90,11 +90,6 @@ local_action: cloudtrail state=absent name=main region=us-east-1 """ -import time -import sys -import os -from collections import Counter - boto_import_failed = False try: import boto From d0ef6db43cb5788bdac4a296537f2e3ce11d3ef6 Mon Sep 17 00:00:00 2001 From: Alex Lo Date: Fri, 12 Jun 2015 00:49:59 -0400 Subject: [PATCH 0117/2522] There is no absent, only disabled --- cloud/amazon/cloudtrail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/cloudtrail.py b/cloud/amazon/cloudtrail.py index d6ed254df91..eb445768ed5 100644 --- a/cloud/amazon/cloudtrail.py +++ b/cloud/amazon/cloudtrail.py @@ -87,7 +87,7 @@ s3_key_prefix='' region=us-east-1 - name: remove cloudtrail - local_action: cloudtrail state=absent name=main region=us-east-1 + local_action: cloudtrail state=disabled name=main region=us-east-1 """ boto_import_failed = False From d1f50493bd062cdd9320916a1c1a891ac8553186 Mon Sep 17 00:00:00 2001 From: Alex Lo Date: Fri, 12 Jun 2015 00:50:27 -0400 Subject: [PATCH 0118/2522] Fix boto library checking --- cloud/amazon/cloudtrail.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/cloud/amazon/cloudtrail.py b/cloud/amazon/cloudtrail.py index eb445768ed5..5a87f35e918 100644 --- a/cloud/amazon/cloudtrail.py +++ b/cloud/amazon/cloudtrail.py @@ -90,13 +90,14 @@ local_action: cloudtrail state=disabled name=main region=us-east-1 """ -boto_import_failed = False +HAS_BOTO = False try: import boto import boto.cloudtrail from boto.regioninfo import RegionInfo + HAS_BOTO = True except ImportError: - boto_import_failed = True + HAS_BOTO = False class CloudTrailManager: """Handles cloudtrail configuration""" @@ -147,9 +148,6 @@ def delete(self, name): def main(): - if not has_libcloud: - module.fail_json(msg='boto is required.') - argument_spec = ec2_argument_spec() argument_spec.update(dict( state={'required': True, 'choices': ['enabled', 'disabled'] }, @@ -161,6 +159,10 @@ def main(): required_together = ( ['state', 's3_bucket_name'] ) module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True, required_together=required_together) + + if not HAS_BOTO: + module.fail_json(msg='Alex sucks boto is required.') + ec2_url, access_key, secret_key, region = get_ec2_creds(module) aws_connect_params = dict(aws_access_key_id=access_key, aws_secret_access_key=secret_key) From 416d96a1e67847609a5642690545f6db17a637c4 Mon Sep 17 00:00:00 2001 From: Alex Lo Date: Fri, 12 Jun 2015 01:31:45 -0400 Subject: [PATCH 0119/2522] Error message typo --- cloud/amazon/cloudtrail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/cloudtrail.py b/cloud/amazon/cloudtrail.py index 5a87f35e918..962473e6a9e 100644 --- a/cloud/amazon/cloudtrail.py +++ b/cloud/amazon/cloudtrail.py @@ -161,7 +161,7 @@ def main(): module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True, required_together=required_together) if not HAS_BOTO: - module.fail_json(msg='Alex sucks boto is required.') + module.fail_json(msg='boto is required.') ec2_url, access_key, secret_key, region = get_ec2_creds(module) aws_connect_params = dict(aws_access_key_id=access_key, From 3f76a37f27dac02bc0423565904bb6cad2957760 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Fri, 12 Jun 2015 14:11:38 -0400 Subject: [PATCH 0120/2522] fixed doc issues --- network/nmcli.py | 71 ++++++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/network/nmcli.py b/network/nmcli.py index 18f0ecbab1f..45043fd2807 100644 --- a/network/nmcli.py +++ b/network/nmcli.py @@ -25,6 +25,7 @@ author: Chris Long short_description: Manage Networking requirements: [ nmcli, dbus ] +version_added: "2.0" description: - Manage the network devices. Create, modify, and manage, ethernet, teams, bonds, vlans etc. options: @@ -39,11 +40,11 @@ choices: [ "yes", "no" ] description: - Whether the connection should start on boot. - - Whether the connection profile can be automatically activated ( default: yes) + - Whether the connection profile can be automatically activated conn_name: required: True description: - - Where conn_name will be the name used to call the connection. when not provided a default name is generated: [-][-] + - 'Where conn_name will be the name used to call the connection. when not provided a default name is generated: [-][-]' ifname: required: False default: conn_name @@ -60,9 +61,9 @@ mode: required: False choices: [ "balance-rr", "active-backup", "balance-xor", "broadcast", "802.3ad", "balance-tlb", "balance-alb" ] - default: None + default: balence-rr description: - - This is the type of device or network connection that you wish to create for a bond, team or bridge. (NetworkManager default: balance-rr) + - This is the type of device or network connection that you wish to create for a bond, team or bridge. master: required: False default: None @@ -72,35 +73,35 @@ required: False default: None description: - - The IPv4 address to this interface using this format ie: "192.168.1.24/24" + - 'The IPv4 address to this interface using this format ie: "192.168.1.24/24"' gw4: required: False description: - - The IPv4 gateway for this interface using this format ie: "192.168.100.1" + - 'The IPv4 gateway for this interface using this format ie: "192.168.100.1"' dns4: required: False default: None description: - - A list of upto 3 dns servers, ipv4 format e.g. To add two IPv4 DNS server addresses: ['"8.8.8.8 8.8.4.4"'] + - 'A list of upto 3 dns servers, ipv4 format e.g. To add two IPv4 DNS server addresses: ["8.8.8.8 8.8.4.4"]' ip6: required: False default: None description: - - The IPv6 address to this interface using this format ie: "abbe::cafe" + - 'The IPv6 address to this interface using this format ie: "abbe::cafe"' gw6: required: False default: None description: - - The IPv6 gateway for this interface using this format ie: "2001:db8::1" + - 'The IPv6 gateway for this interface using this format ie: "2001:db8::1"' dns6: required: False description: - - A list of upto 3 dns servers, ipv6 format e.g. To add two IPv6 DNS server addresses: ['"2001:4860:4860::8888 2001:4860:4860::8844"'] + - 'A list of upto 3 dns servers, ipv6 format e.g. To add two IPv6 DNS server addresses: ["2001:4860:4860::8888 2001:4860:4860::8844"]' mtu: required: False - default: None + default: 1500 description: - - The connection MTU, e.g. 9000. This can't be applied when creating the interface and is done once the interface has been created. (NetworkManager default: 1500) + - The connection MTU, e.g. 9000. This can't be applied when creating the interface and is done once the interface has been created. - Can be used when modifying Team, VLAN, Ethernet (Future plans to implement wifi, pppoe, infiniband) primary: required: False @@ -109,24 +110,24 @@ - This is only used with bond and is the primary interface name (for "active-backup" mode), this is the usually the 'ifname' miimon: required: False - default: None + default: 100 description: - - This is only used with bond - miimon (NetworkManager default: 100) + - This is only used with bond - miimon downdelay: required: False default: None description: - - This is only used with bond - downdelay (NetworkManager default: 0) + - This is only used with bond - downdelay updelay: required: False default: None description: - - This is only used with bond - updelay (NetworkManager default: 0) + - This is only used with bond - updelay arp_interval: required: False default: None description: - - This is only used with bond - ARP interval (NetworkManager default: 0) + - This is only used with bond - ARP interval arp_ip_target: required: False default: None @@ -139,49 +140,49 @@ - This is only used with bridge and controls whether Spanning Tree Protocol (STP) is enabled for this bridge priority: required: False - default: None + default: 128 description: - - This is only used with 'bridge' - sets STP priority (NetworkManager default: 128) + - This is only used with 'bridge' - sets STP priority forwarddelay: required: False - default: None + default: 15 description: - - This is only used with bridge - [forward-delay <2-30>] STP forwarding delay, in seconds (NetworkManager default: 15) + - This is only used with bridge - [forward-delay <2-30>] STP forwarding delay, in seconds hellotime: required: False - default: None + default: 2 description: - - This is only used with bridge - [hello-time <1-10>] STP hello time, in seconds (NetworkManager default: 2) + - This is only used with bridge - [hello-time <1-10>] STP hello time, in seconds maxage: required: False - default: None + default: 20 description: - - This is only used with bridge - [max-age <6-42>] STP maximum message age, in seconds (NetworkManager default: 20) + - This is only used with bridge - [max-age <6-42>] STP maximum message age, in seconds ageingtime: required: False - default: None + default: 300 description: - - This is only used with bridge - [ageing-time <0-1000000>] the Ethernet MAC address aging time, in seconds (NetworkManager default: 300) + - This is only used with bridge - [ageing-time <0-1000000>] the Ethernet MAC address aging time, in seconds mac: required: False default: None description: - - This is only used with bridge - MAC address of the bridge (note: this requires a recent kernel feature, originally introduced in 3.15 upstream kernel) + - 'This is only used with bridge - MAC address of the bridge (note: this requires a recent kernel feature, originally introduced in 3.15 upstream kernel)' slavepriority: required: False - default: None + default: 32 description: - - This is only used with 'bridge-slave' - [<0-63>] - STP priority of this slave (default: 32) + - This is only used with 'bridge-slave' - [<0-63>] - STP priority of this slave path_cost: required: False - default: None + default: 100 description: - - This is only used with 'bridge-slave' - [<1-65535>] - STP port cost for destinations via this slave (NetworkManager default: 100) + - This is only used with 'bridge-slave' - [<1-65535>] - STP port cost for destinations via this slave hairpin: required: False - default: None + default: yes description: - - This is only used with 'bridge-slave' - 'hairpin mode' for the slave, which allows frames to be sent back out through the slave the frame was received on. (NetworkManager default: yes) + - This is only used with 'bridge-slave' - 'hairpin mode' for the slave, which allows frames to be sent back out through the slave the frame was received on. vlanid: required: False default: None @@ -1066,4 +1067,4 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() \ No newline at end of file +main() From 6a89b92cdaba2f98196bcdafefb9dbcee503e650 Mon Sep 17 00:00:00 2001 From: Bruce Pennypacker Date: Fri, 12 Jun 2015 18:36:23 +0000 Subject: [PATCH 0121/2522] Fixed results & 'Changed'. Added 'deleted' action. Added ability to specify multiple services. --- monitoring/pagerduty.py | 72 +++++++++++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 14 deletions(-) diff --git a/monitoring/pagerduty.py b/monitoring/pagerduty.py index 24c622c83a8..2ed7c0cc6bb 100644 --- a/monitoring/pagerduty.py +++ b/monitoring/pagerduty.py @@ -11,6 +11,7 @@ - "Andrew Newdigate (@suprememoocow)" - "Dylan Silva (@thaumos)" - "Justin Johns" + - "Bruce Pennypacker" requirements: - PagerDuty API access options: @@ -19,7 +20,7 @@ - Create a maintenance window or get a list of ongoing windows. required: true default: null - choices: [ "running", "started", "ongoing" ] + choices: [ "running", "started", "ongoing", "deleted" ] aliases: [] name: description: @@ -61,11 +62,11 @@ version_added: '1.8' service: description: - - PagerDuty service ID. + - A comma separated list of PagerDuty service IDs. required: false default: null choices: [] - aliases: [] + aliases: [ services ] hours: description: - Length of maintenance window in hours. @@ -96,9 +97,6 @@ default: 'yes' choices: ['yes', 'no'] version_added: 1.5.1 - -notes: - - This module does not yet have support to end maintenance windows. ''' EXAMPLES=''' @@ -132,6 +130,14 @@ service=FOO123 hours=4 desc=deployment + register: pd_window + +# Delete the previous maintenance window +- pagerduty: name=companyabc + user=example@example.com + passwd=password123 + state=deleted + service={{ pd_window.result.maintenance_window.id }} ''' import datetime @@ -152,7 +158,7 @@ def ongoing(module, name, user, passwd, token): if info['status'] != 200: module.fail_json(msg="failed to lookup the ongoing window: %s" % info['msg']) - return False, response.read() + return False, response.read(), False def create(module, name, user, passwd, token, requester_id, service, hours, minutes, desc): @@ -166,7 +172,8 @@ def create(module, name, user, passwd, token, requester_id, service, hours, minu 'Authorization': auth_header(user, passwd, token), 'Content-Type' : 'application/json', } - request_data = {'maintenance_window': {'start_time': start, 'end_time': end, 'description': desc, 'service_ids': [service]}} + request_data = {'maintenance_window': {'start_time': start, 'end_time': end, 'description': desc, 'service_ids': service}} + if requester_id: request_data['requester_id'] = requester_id else: @@ -178,19 +185,50 @@ def create(module, name, user, passwd, token, requester_id, service, hours, minu if info['status'] != 200: module.fail_json(msg="failed to create the window: %s" % info['msg']) - return False, response.read() + try: + json_out = json.loads(response.read()) + except: + json_out = "" + + return False, json_out, True + +def delete(module, name, user, passwd, token, requester_id, service): + url = "https://" + name + ".pagerduty.com/api/v1/maintenance_windows/" + service[0] + headers = { + 'Authorization': auth_header(user, passwd, token), + 'Content-Type' : 'application/json', + } + request_data = {} + + if requester_id: + request_data['requester_id'] = requester_id + else: + if token: + module.fail_json(msg="requester_id is required when using a token") + + data = json.dumps(request_data) + response, info = fetch_url(module, url, data=data, headers=headers, method='DELETE') + if info['status'] != 200: + module.fail_json(msg="failed to delete the window: %s" % info['msg']) + + try: + json_out = json.loads(response.read()) + except: + json_out = "" + + return False, json_out, True def main(): module = AnsibleModule( argument_spec=dict( - state=dict(required=True, choices=['running', 'started', 'ongoing']), + state=dict(required=True, choices=['running', 'started', 'ongoing', 'deleted']), name=dict(required=True), user=dict(required=False), passwd=dict(required=False), token=dict(required=False), - service=dict(required=False), + service=dict(required=False, type='list', aliases=["services"]), requester_id=dict(required=False), hours=dict(default='1', required=False), minutes=dict(default='0', required=False), @@ -217,15 +255,21 @@ def main(): if state == "running" or state == "started": if not service: module.fail_json(msg="service not specified") - (rc, out) = create(module, name, user, passwd, token, requester_id, service, hours, minutes, desc) + (rc, out, changed) = create(module, name, user, passwd, token, requester_id, service, hours, minutes, desc) + if rc == 0: + changed=True if state == "ongoing": - (rc, out) = ongoing(module, name, user, passwd, token) + (rc, out, changed) = ongoing(module, name, user, passwd, token) + + if state == "deleted": + (rc, out, changed) = delete(module, name, user, passwd, token, requester_id, service) if rc != 0: module.fail_json(msg="failed", result=out) - module.exit_json(msg="success", result=out) + + module.exit_json(msg="success", result=out, changed=changed) # import module snippets from ansible.module_utils.basic import * From 51bba578b6fa6103400c45d9b7015299797aef4d Mon Sep 17 00:00:00 2001 From: Bruce Pennypacker Date: Fri, 12 Jun 2015 19:58:57 +0000 Subject: [PATCH 0122/2522] Updated 'ongoing' to also return properly formatted json --- monitoring/pagerduty.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/monitoring/pagerduty.py b/monitoring/pagerduty.py index 2ed7c0cc6bb..bd35fbb6003 100644 --- a/monitoring/pagerduty.py +++ b/monitoring/pagerduty.py @@ -158,7 +158,12 @@ def ongoing(module, name, user, passwd, token): if info['status'] != 200: module.fail_json(msg="failed to lookup the ongoing window: %s" % info['msg']) - return False, response.read(), False + try: + json_out = json.loads(response.read()) + except: + json_out = "" + + return False, json_out, False def create(module, name, user, passwd, token, requester_id, service, hours, minutes, desc): From d3b3d7ff3c249de062df93a533a568f0681cce3c Mon Sep 17 00:00:00 2001 From: Kevin Carter Date: Sat, 13 Jun 2015 13:56:26 -0500 Subject: [PATCH 0123/2522] Fix the lxc container restart state The lxc container restart state does not ensure that the container is in fact started unless another config or command is passed into the task. to fix this the module simply needs to have the function call added ``self._container_startup()`` after the container is put into a stopped state. Signed-off By: Kevin Carter --- cloud/lxc/lxc_container.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index 18555e2e351..7fc86825c52 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -1065,6 +1065,9 @@ def _restarted(self, count=0): self.container.stop() self.state_change = True + # Run container startup + self._container_startup() + # Check if the container needs to have an archive created. self._check_archive() From 9f0ee40b42f491421e582066c8b82ea95d0cf769 Mon Sep 17 00:00:00 2001 From: Robert Estelle Date: Thu, 13 Nov 2014 18:57:00 -0500 Subject: [PATCH 0124/2522] Add ec2_vpc_igw module. --- cloud/amazon/ec2_vpc_igw.py | 189 ++++++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 cloud/amazon/ec2_vpc_igw.py diff --git a/cloud/amazon/ec2_vpc_igw.py b/cloud/amazon/ec2_vpc_igw.py new file mode 100644 index 00000000000..1c5bf9dea1c --- /dev/null +++ b/cloud/amazon/ec2_vpc_igw.py @@ -0,0 +1,189 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ec2_vpc_igw +short_description: configure AWS virtual private clouds +description: + - Create or terminates AWS internat gateway in a virtual private cloud. ''' +'''This module has a dependency on python-boto. +version_added: "1.8" +options: + vpc_id: + description: + - "The VPC ID for which to create or remove the Internet Gateway." + required: true + state: + description: + - Create or terminate the IGW + required: true + default: present + aliases: [] + region: + description: + - region in which the resource exists. + required: false + default: null + aliases: ['aws_region', 'ec2_region'] + aws_secret_key: + description: + - AWS secret key. If not set then the value of the AWS_SECRET_KEY''' +''' environment variable is used. + required: false + default: None + aliases: ['ec2_secret_key', 'secret_key' ] + aws_access_key: + description: + - AWS access key. If not set then the value of the AWS_ACCESS_KEY''' +''' environment variable is used. + required: false + default: None + aliases: ['ec2_access_key', 'access_key' ] + validate_certs: + description: + - When set to "no", SSL certificates will not be validated for boto''' +''' versions >= 2.6.0. + required: false + default: "yes" + choices: ["yes", "no"] + aliases: [] + version_added: "1.5" + +requirements: [ "boto" ] +author: Robert Estelle +''' + +EXAMPLES = ''' +# Note: None of these examples set aws_access_key, aws_secret_key, or region. +# It is assumed that their matching environment variables are set. + +# Ensure that the VPC has an Internet Gateway. +# The Internet Gateway ID is can be accessed via {{igw.gateway_id}} for use +# in setting up NATs etc. + local_action: + module: ec2_vpc_igw + vpc_id: {{vpc.vpc_id}} + region: {{vpc.vpc.region}} + state: present + register: igw +''' + + +import sys + +try: + import boto.ec2 + import boto.vpc + from boto.exception import EC2ResponseError +except ImportError: + print "failed=True msg='boto required for this module'" + sys.exit(1) + + +class IGWExcepton(Exception): + pass + + +def ensure_igw_absent(vpc_conn, vpc_id, check_mode): + igws = vpc_conn.get_all_internet_gateways( + filters={'attachment.vpc-id': vpc_id}) + + if not igws: + return {'changed': False} + + if check_mode: + return {'changed': True} + + for igw in igws: + try: + vpc_conn.detach_internet_gateway(igw.id, vpc_id) + vpc_conn.delete_internet_gateway(igw.id) + except EC2ResponseError as e: + raise IGWExcepton('Unable to delete Internet Gateway, error: {0}' + .format(e)) + + return {'changed': True} + + +def ensure_igw_present(vpc_conn, vpc_id, check_mode): + igws = vpc_conn.get_all_internet_gateways( + filters={'attachment.vpc-id': vpc_id}) + + if len(igws) > 1: + raise IGWExcepton( + 'EC2 returned more than one Internet Gateway for VPC {0}, aborting' + .format(vpc_id)) + + if igws: + return {'changed': False, 'gateway_id': igws[0].id} + else: + if check_mode: + return {'changed': True, 'gateway_id': None} + + try: + igw = vpc_conn.create_internet_gateway() + vpc_conn.attach_internet_gateway(igw.id, vpc_id) + return {'changed': True, 'gateway_id': igw.id} + except EC2ResponseError as e: + raise IGWExcepton('Unable to create Internet Gateway, error: {0}' + .format(e)) + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update({ + 'vpc_id': {'required': True}, + 'state': {'choices': ['present', 'absent'], 'default': 'present'}, + }) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module) + if not region: + module.fail_json(msg='Region must be specified') + + try: + vpc_conn = boto.vpc.connect_to_region( + region, + aws_access_key_id=aws_access_key, + aws_secret_access_key=aws_secret_key + ) + except boto.exception.NoAuthHandlerFound as e: + module.fail_json(msg=str(e)) + + vpc_id = module.params.get('vpc_id') + state = module.params.get('state', 'present') + + try: + if state == 'present': + result = ensure_igw_present(vpc_conn, vpc_id, + check_mode=module.check_mode) + elif state == 'absent': + result = ensure_igw_absent(vpc_conn, vpc_id, + check_mode=module.check_mode) + except IGWExcepton as e: + module.fail_json(msg=str(e)) + + module.exit_json(**result) + +from ansible.module_utils.basic import * # noqa +from ansible.module_utils.ec2 import * # noqa + +if __name__ == '__main__': + main() From 829759fba7f392e5998e5508faa2c30b85249ea2 Mon Sep 17 00:00:00 2001 From: Robert Estelle Date: Mon, 1 Dec 2014 16:01:46 -0500 Subject: [PATCH 0125/2522] ec2_vpc_igw - Exit with fail_json when boto is unavailable. --- cloud/amazon/ec2_vpc_igw.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cloud/amazon/ec2_vpc_igw.py b/cloud/amazon/ec2_vpc_igw.py index 1c5bf9dea1c..7276157bd56 100644 --- a/cloud/amazon/ec2_vpc_igw.py +++ b/cloud/amazon/ec2_vpc_igw.py @@ -83,15 +83,17 @@ ''' -import sys +import sys # noqa try: import boto.ec2 import boto.vpc from boto.exception import EC2ResponseError + HAS_BOTO = True except ImportError: - print "failed=True msg='boto required for this module'" - sys.exit(1) + HAS_BOTO = False + if __name__ != '__main__': + raise class IGWExcepton(Exception): @@ -153,6 +155,8 @@ def main(): argument_spec=argument_spec, supports_check_mode=True, ) + if not HAS_BOTO: + module.fail_json(msg='boto is required for this module') ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module) if not region: From 6b32b95252c582a1687d98e435f5e33726c8a59d Mon Sep 17 00:00:00 2001 From: Robert Estelle Date: Mon, 1 Dec 2014 16:02:09 -0500 Subject: [PATCH 0126/2522] ec2_vpc_igw - Rename IGWException to AnsibleIGWException. --- cloud/amazon/ec2_vpc_igw.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cloud/amazon/ec2_vpc_igw.py b/cloud/amazon/ec2_vpc_igw.py index 7276157bd56..cbac94528d2 100644 --- a/cloud/amazon/ec2_vpc_igw.py +++ b/cloud/amazon/ec2_vpc_igw.py @@ -96,7 +96,7 @@ raise -class IGWExcepton(Exception): +class AnsibleIGWException(Exception): pass @@ -115,8 +115,8 @@ def ensure_igw_absent(vpc_conn, vpc_id, check_mode): vpc_conn.detach_internet_gateway(igw.id, vpc_id) vpc_conn.delete_internet_gateway(igw.id) except EC2ResponseError as e: - raise IGWExcepton('Unable to delete Internet Gateway, error: {0}' - .format(e)) + raise AnsibleIGWException( + 'Unable to delete Internet Gateway, error: {0}'.format(e)) return {'changed': True} @@ -126,7 +126,7 @@ def ensure_igw_present(vpc_conn, vpc_id, check_mode): filters={'attachment.vpc-id': vpc_id}) if len(igws) > 1: - raise IGWExcepton( + raise AnsibleIGWException( 'EC2 returned more than one Internet Gateway for VPC {0}, aborting' .format(vpc_id)) @@ -141,8 +141,8 @@ def ensure_igw_present(vpc_conn, vpc_id, check_mode): vpc_conn.attach_internet_gateway(igw.id, vpc_id) return {'changed': True, 'gateway_id': igw.id} except EC2ResponseError as e: - raise IGWExcepton('Unable to create Internet Gateway, error: {0}' - .format(e)) + raise AnsibleIGWException( + 'Unable to create Internet Gateway, error: {0}'.format(e)) def main(): @@ -181,7 +181,7 @@ def main(): elif state == 'absent': result = ensure_igw_absent(vpc_conn, vpc_id, check_mode=module.check_mode) - except IGWExcepton as e: + except AnsibleIGWException as e: module.fail_json(msg=str(e)) module.exit_json(**result) From c21eebdd7b40f76a5e9d6d60102773e597f096a6 Mon Sep 17 00:00:00 2001 From: Rob White Date: Sun, 14 Jun 2015 16:31:31 +1000 Subject: [PATCH 0127/2522] Updated documentation and added boto profile support. --- cloud/amazon/ec2_vpc_igw.py | 94 ++++++++++++------------------------- 1 file changed, 30 insertions(+), 64 deletions(-) diff --git a/cloud/amazon/ec2_vpc_igw.py b/cloud/amazon/ec2_vpc_igw.py index cbac94528d2..63be48248ef 100644 --- a/cloud/amazon/ec2_vpc_igw.py +++ b/cloud/amazon/ec2_vpc_igw.py @@ -1,75 +1,42 @@ #!/usr/bin/python -# This file is part of Ansible # -# Ansible is free software: you can redistribute it and/or modify +# This is a free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# Ansible is distributed in the hope that it will be useful, +# This Ansible library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . +# along with this library. If not, see . DOCUMENTATION = ''' --- module: ec2_vpc_igw -short_description: configure AWS virtual private clouds +short_description: Manage an AWS VPC Internet gateway description: - - Create or terminates AWS internat gateway in a virtual private cloud. ''' -'''This module has a dependency on python-boto. -version_added: "1.8" + - Manage an AWS VPC Internet gateway +version_added: "2.0" +author: Robert Estelle, @erydo options: vpc_id: description: - - "The VPC ID for which to create or remove the Internet Gateway." + - The VPC ID for the VPC in which to manage the Internet Gateway. required: true + default: null state: description: - Create or terminate the IGW - required: true - default: present - aliases: [] - region: - description: - - region in which the resource exists. - required: false - default: null - aliases: ['aws_region', 'ec2_region'] - aws_secret_key: - description: - - AWS secret key. If not set then the value of the AWS_SECRET_KEY''' -''' environment variable is used. - required: false - default: None - aliases: ['ec2_secret_key', 'secret_key' ] - aws_access_key: - description: - - AWS access key. If not set then the value of the AWS_ACCESS_KEY''' -''' environment variable is used. - required: false - default: None - aliases: ['ec2_access_key', 'access_key' ] - validate_certs: - description: - - When set to "no", SSL certificates will not be validated for boto''' -''' versions >= 2.6.0. required: false - default: "yes" - choices: ["yes", "no"] - aliases: [] - version_added: "1.5" - -requirements: [ "boto" ] -author: Robert Estelle + default: present +extends_documentation_fragment: aws ''' EXAMPLES = ''' -# Note: None of these examples set aws_access_key, aws_secret_key, or region. -# It is assumed that their matching environment variables are set. +# Note: These examples do not set authentication details, see the AWS Guide for details. # Ensure that the VPC has an Internet Gateway. # The Internet Gateway ID is can be accessed via {{igw.gateway_id}} for use @@ -147,40 +114,39 @@ def ensure_igw_present(vpc_conn, vpc_id, check_mode): def main(): argument_spec = ec2_argument_spec() - argument_spec.update({ - 'vpc_id': {'required': True}, - 'state': {'choices': ['present', 'absent'], 'default': 'present'}, - }) + argument_spec.update( + dict( + vpc_id = dict(required=True), + state = dict(choices=['present', 'absent'], default='present') + ) + ) + module = AnsibleModule( argument_spec=argument_spec, supports_check_mode=True, ) + if not HAS_BOTO: module.fail_json(msg='boto is required for this module') - ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module) - if not region: - module.fail_json(msg='Region must be specified') + region, ec2_url, aws_connect_params = get_aws_connection_info(module) - try: - vpc_conn = boto.vpc.connect_to_region( - region, - aws_access_key_id=aws_access_key, - aws_secret_access_key=aws_secret_key - ) - except boto.exception.NoAuthHandlerFound as e: - module.fail_json(msg=str(e)) + if region: + try: + connection = connect_to_aws(boto.ec2, region, **aws_connect_params) + except (boto.exception.NoAuthHandlerFound, StandardError), e: + module.fail_json(msg=str(e)) + else: + module.fail_json(msg="region must be specified") vpc_id = module.params.get('vpc_id') state = module.params.get('state', 'present') try: if state == 'present': - result = ensure_igw_present(vpc_conn, vpc_id, - check_mode=module.check_mode) + result = ensure_igw_present(connection, vpc_id, check_mode=module.check_mode) elif state == 'absent': - result = ensure_igw_absent(vpc_conn, vpc_id, - check_mode=module.check_mode) + result = ensure_igw_absent(connection, vpc_id, check_mode=module.check_mode) except AnsibleIGWException as e: module.fail_json(msg=str(e)) From 9285d0a1c75b21a412e70a0e432d1a125945e179 Mon Sep 17 00:00:00 2001 From: Bruce Pennypacker Date: Sun, 14 Jun 2015 20:20:36 +0000 Subject: [PATCH 0128/2522] changed 'deleted' to 'absent' --- monitoring/pagerduty.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/monitoring/pagerduty.py b/monitoring/pagerduty.py index bd35fbb6003..b35cfbf4992 100644 --- a/monitoring/pagerduty.py +++ b/monitoring/pagerduty.py @@ -20,7 +20,7 @@ - Create a maintenance window or get a list of ongoing windows. required: true default: null - choices: [ "running", "started", "ongoing", "deleted" ] + choices: [ "running", "started", "ongoing", "absent" ] aliases: [] name: description: @@ -136,7 +136,7 @@ - pagerduty: name=companyabc user=example@example.com passwd=password123 - state=deleted + state=absent service={{ pd_window.result.maintenance_window.id }} ''' @@ -197,7 +197,7 @@ def create(module, name, user, passwd, token, requester_id, service, hours, minu return False, json_out, True -def delete(module, name, user, passwd, token, requester_id, service): +def absent(module, name, user, passwd, token, requester_id, service): url = "https://" + name + ".pagerduty.com/api/v1/maintenance_windows/" + service[0] headers = { 'Authorization': auth_header(user, passwd, token), @@ -228,7 +228,7 @@ def main(): module = AnsibleModule( argument_spec=dict( - state=dict(required=True, choices=['running', 'started', 'ongoing', 'deleted']), + state=dict(required=True, choices=['running', 'started', 'ongoing', 'absent']), name=dict(required=True), user=dict(required=False), passwd=dict(required=False), @@ -267,8 +267,8 @@ def main(): if state == "ongoing": (rc, out, changed) = ongoing(module, name, user, passwd, token) - if state == "deleted": - (rc, out, changed) = delete(module, name, user, passwd, token, requester_id, service) + if state == "absent": + (rc, out, changed) = absent(module, name, user, passwd, token, requester_id, service) if rc != 0: module.fail_json(msg="failed", result=out) From 5f5577e110aec5f44f6544fc3ecbbeaf2230a025 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sun, 12 Apr 2015 23:09:45 +0200 Subject: [PATCH 0129/2522] cloudstack: add new module cs_template --- cloud/cloudstack/cs_template.py | 633 ++++++++++++++++++++++++++++++++ 1 file changed, 633 insertions(+) create mode 100644 cloud/cloudstack/cs_template.py diff --git a/cloud/cloudstack/cs_template.py b/cloud/cloudstack/cs_template.py new file mode 100644 index 00000000000..48f00fad553 --- /dev/null +++ b/cloud/cloudstack/cs_template.py @@ -0,0 +1,633 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2015, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: cs_template +short_description: Manages templates on Apache CloudStack based clouds. +description: + - Register a template from URL, create a template from a ROOT volume of a stopped VM or its snapshot and delete templates. +version_added: '2.0' +author: '"René Moser (@resmo)" ' +options: + name: + description: + - Name of the template. + required: true + url: + description: + - URL of where the template is hosted. + - Mutually exclusive with C(vm). + required: false + default: null + vm: + description: + - VM name the template will be created from its volume or alternatively from a snapshot. + - VM must be in stopped state if created from its volume. + - Mutually exclusive with C(url). + required: false + default: null + snapshot: + description: + - Name of the snapshot, created from the VM ROOT volume, the template will be created from. + - C(vm) is required together with this argument. + required: false + default: null + os_type: + description: + - OS type that best represents the OS of this template. + required: false + default: null + checksum: + description: + - The MD5 checksum value of this template. + - If set, we search by checksum instead of name. + required: false + default: false + is_ready: + description: + - This flag is used for searching existing templates. + - If set to C(true), it will only list template ready for deployment e.g. successfully downloaded and installed. + - Recommended to set it to C(false). + required: false + default: false + is_public: + description: + - Register the template to be publicly available to all users. + - Only used if C(state) is present. + required: false + default: false + is_featured: + description: + - Register the template to be featured. + - Only used if C(state) is present. + required: false + default: false + is_dynamically_scalable: + description: + - Register the template having XS/VMWare tools installed in order to support dynamic scaling of VM CPU/memory. + - Only used if C(state) is present. + required: false + default: false + project: + description: + - Name of the project the template to be registered in. + required: false + default: null + zone: + description: + - Name of the zone you wish the template to be registered or deleted from. + - If not specified, first found zone will be used. + required: false + default: null + template_filter: + description: + - Name of the filter used to search for the template. + required: false + default: 'self' + choices: [ 'featured', 'self', 'selfexecutable', 'sharedexecutable', 'executable', 'community' ] + hypervisor: + description: + - Name the hypervisor to be used for creating the new template. + - Relevant when using C(state=present). + required: false + default: none + choices: [ 'KVM', 'VMware', 'BareMetal', 'XenServer', 'LXC', 'HyperV', 'UCS', 'OVM' ] + requires_hvm: + description: + - true if this template requires HVM. + required: false + default: false + password_enabled: + description: + - True if the template supports the password reset feature. + required: false + default: false + template_tag: + description: + - the tag for this template. + required: false + default: null + sshkey_enabled: + description: + - True if the template supports the sshkey upload feature. + required: false + default: false + is_routing: + description: + - True if the template type is routing i.e., if template is used to deploy router. + - Only considered if C(url) is used. + required: false + default: false + format: + description: + - The format for the template. + - Relevant when using C(state=present). + required: false + default: null + choices: [ 'QCOW2', 'RAW', 'VHD', 'OVA' ] + is_extractable: + description: + - True if the template or its derivatives are extractable. + required: false + default: false + details: + description: + - Template details in key/value pairs. + required: false + default: null + bits: + description: + - 32 or 64 bits support. + required: false + default: '64' + displaytext: + description: + - the display text of the template. + required: true + default: null + state: + description: + - State of the template. + required: false + default: 'present' + choices: [ 'present', 'absent' ] + poll_async: + description: + - Poll async jobs until job has finished. + required: false + default: true +extends_documentation_fragment: cloudstack +''' + +EXAMPLES = ''' +# Register a systemvm template +- local_action: + module: cs_template + name: systemvm-4.5 + url: "http://packages.shapeblue.com/systemvmtemplate/4.5/systemvm64template-4.5-vmware.ova" + hypervisor: VMware + format: OVA + zone: tokio-ix + os_type: Debian GNU/Linux 7(64-bit) + is_routing: yes + +# Create a template from a stopped virtual machine's volume +- local_action: + module: cs_template + name: debian-base-template + vm: debian-base-vm + os_type: Debian GNU/Linux 7(64-bit) + zone: tokio-ix + password_enabled: yes + is_public: yes + +# Create a template from a virtual machine's root volume snapshot +- local_action: + module: cs_template + name: debian-base-template + vm: debian-base-vm + snapshot: ROOT-233_2015061509114 + os_type: Debian GNU/Linux 7(64-bit) + zone: tokio-ix + password_enabled: yes + is_public: yes + +# Remove a template +- local_action: + module: cs_template + name: systemvm-4.2 + state: absent +''' + +RETURN = ''' +--- +name: + description: Name of the template. + returned: success + type: string + sample: Debian 7 64-bit +displaytext: + description: Displaytext of the template. + returned: success + type: string + sample: Debian 7.7 64-bit minimal 2015-03-19 +checksum: + description: MD5 checksum of the template. + returned: success + type: string + sample: 0b31bccccb048d20b551f70830bb7ad0 +status: + description: Status of the template. + returned: success + type: string + sample: Download Complete +is_ready: + description: True if the template is ready to be deployed from. + returned: success + type: boolean + sample: true +is_public: + description: True if the template is public. + returned: success + type: boolean + sample: true +is_featured: + description: True if the template is featured. + returned: success + type: boolean + sample: true +is_extractable: + description: True if the template is extractable. + returned: success + type: boolean + sample: true +format: + description: Format of the template. + returned: success + type: string + sample: OVA +os_type: + description: Typo of the OS. + returned: success + type: string + sample: CentOS 6.5 (64-bit) +password_enabled: + description: True if the reset password feature is enabled, false otherwise. + returned: success + type: boolean + sample: false +sshkey_enabled: + description: true if template is sshkey enabled, false otherwise. + returned: success + type: boolean + sample: false +cross_zones: + description: true if the template is managed across all zones, false otherwise. + returned: success + type: boolean + sample: false +template_type: + description: Type of the template. + returned: success + type: string + sample: USER +created: + description: Date of registering. + returned: success + type: string + sample: 2015-03-29T14:57:06+0200 +template_tag: + description: Template tag related to this template. + returned: success + type: string + sample: special +hypervisor: + description: Hypervisor related to this template. + returned: success + type: string + sample: VMware +tags: + description: List of resource tags associated with the template. + returned: success + type: dict + sample: '[ { "key": "foo", "value": "bar" } ]' +zone: + description: Name of zone the template is registered in. + returned: success + type: string + sample: zuerich +domain: + description: Domain the template is related to. + returned: success + type: string + sample: example domain +account: + description: Account the template is related to. + returned: success + type: string + sample: example account +project: + description: Name of project the template is related to. + returned: success + type: string + sample: Production +''' + +try: + from cs import CloudStack, CloudStackException, read_config + has_lib_cs = True +except ImportError: + has_lib_cs = False + +# import cloudstack common +from ansible.module_utils.cloudstack import * + + +class AnsibleCloudStackTemplate(AnsibleCloudStack): + + def __init__(self, module): + AnsibleCloudStack.__init__(self, module) + + + def _get_args(self): + args = {} + args['name'] = self.module.params.get('name') + args['displaytext'] = self.module.params.get('displaytext') + args['bits'] = self.module.params.get('bits') + args['isdynamicallyscalable'] = self.module.params.get('is_dynamically_scalable') + args['isextractable'] = self.module.params.get('is_extractable') + args['isfeatured'] = self.module.params.get('is_featured') + args['ispublic'] = self.module.params.get('is_public') + args['passwordenabled'] = self.module.params.get('password_enabled') + args['requireshvm'] = self.module.params.get('requires_hvm') + args['templatetag'] = self.module.params.get('template_tag') + args['ostypeid'] = self.get_os_type(key='id') + + if not args['ostypeid']: + self.module.fail_json(msg="Missing required arguments: os_type") + + if not args['displaytext']: + args['displaytext'] = self.module.params.get('name') + return args + + + def get_root_volume(self, key=None): + args = {} + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') + args['projectid'] = self.get_project(key='id') + args['virtualmachineid'] = self.get_vm(key='id') + args['type'] = "ROOT" + + volumes = self.cs.listVolumes(**args) + if volumes: + return self._get_by_key(key, volumes['volume'][0]) + self.module.fail_json(msg="Root volume for '%s' not found" % self.get_vm('name')) + + + def get_snapshot(self, key=None): + snapshot = self.module.params.get('snapshot') + if not snapshot: + return None + + args = {} + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') + args['projectid'] = self.get_project(key='id') + args['volumeid'] = self.get_root_volume('id') + snapshots = self.cs.listSnapshots(**args) + if snapshots: + for s in snapshots['snapshot']: + if snapshot in [ s['name'], s['id'] ]: + return self._get_by_key(key, s) + self.module.fail_json(msg="Snapshot '%s' not found" % snapshot) + + + def create_template(self): + template = self.get_template() + if not template: + self.result['changed'] = True + + args = self._get_args() + snapshot_id = self.get_snapshot(key='id') + if snapshot_id: + args['snapshotid'] = snapshot_id + else: + args['volumeid'] = self.get_root_volume('id') + + if not self.module.check_mode: + template = self.cs.createTemplate(**args) + + if 'errortext' in template: + self.module.fail_json(msg="Failed: '%s'" % template['errortext']) + + poll_async = self.module.params.get('poll_async') + if poll_async: + template = self._poll_job(template, 'template') + return template + + + def register_template(self): + template = self.get_template() + if not template: + self.result['changed'] = True + args = self._get_args() + args['url'] = self.module.params.get('url') + args['format'] = self.module.params.get('format') + args['checksum'] = self.module.params.get('checksum') + args['isextractable'] = self.module.params.get('is_extractable') + args['isrouting'] = self.module.params.get('is_routing') + args['sshkeyenabled'] = self.module.params.get('sshkey_enabled') + args['hypervisor'] = self.get_hypervisor() + args['zoneid'] = self.get_zone(key='id') + args['domainid'] = self.get_domain(key='id') + args['account'] = self.get_account(key='name') + args['projectid'] = self.get_project(key='id') + + if not self.module.check_mode: + res = self.cs.registerTemplate(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + template = res['template'] + return template + + + def get_template(self): + args = {} + args['isready'] = self.module.params.get('is_ready') + args['templatefilter'] = self.module.params.get('template_filter') + args['zoneid'] = self.get_zone(key='id') + args['domainid'] = self.get_domain(key='id') + args['account'] = self.get_account(key='name') + args['projectid'] = self.get_project(key='id') + + # if checksum is set, we only look on that. + checksum = self.module.params.get('checksum') + if not checksum: + args['name'] = self.module.params.get('name') + + templates = self.cs.listTemplates(**args) + if templates: + # if checksum is set, we only look on that. + if not checksum: + return templates['template'][0] + else: + for i in templates['template']: + if i['checksum'] == checksum: + return i + return None + + + def remove_template(self): + template = self.get_template() + if template: + self.result['changed'] = True + + args = {} + args['id'] = template['id'] + args['zoneid'] = self.get_zone(key='id') + + if not self.module.check_mode: + res = self.cs.deleteTemplate(**args) + + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + + poll_async = self.module.params.get('poll_async') + if poll_async: + res = self._poll_job(res, 'template') + return template + + + def get_result(self, template): + if template: + if 'displaytext' in template: + self.result['displaytext'] = template['displaytext'] + if 'name' in template: + self.result['name'] = template['name'] + if 'hypervisor' in template: + self.result['hypervisor'] = template['hypervisor'] + if 'zonename' in template: + self.result['zone'] = template['zonename'] + if 'checksum' in template: + self.result['checksum'] = template['checksum'] + if 'format' in template: + self.result['format'] = template['format'] + if 'isready' in template: + self.result['is_ready'] = template['isready'] + if 'ispublic' in template: + self.result['is_public'] = template['ispublic'] + if 'isfeatured' in template: + self.result['is_featured'] = template['isfeatured'] + if 'isextractable' in template: + self.result['is_extractable'] = template['isextractable'] + # and yes! it is really camelCase! + if 'crossZones' in template: + self.result['cross_zones'] = template['crossZones'] + if 'ostypename' in template: + self.result['os_type'] = template['ostypename'] + if 'templatetype' in template: + self.result['template_type'] = template['templatetype'] + if 'passwordenabled' in template: + self.result['password_enabled'] = template['passwordenabled'] + if 'sshkeyenabled' in template: + self.result['sshkey_enabled'] = template['sshkeyenabled'] + if 'status' in template: + self.result['status'] = template['status'] + if 'created' in template: + self.result['created'] = template['created'] + if 'templatetag' in template: + self.result['template_tag'] = template['templatetag'] + if 'tags' in template: + self.result['tags'] = [] + for tag in template['tags']: + result_tag = {} + result_tag['key'] = tag['key'] + result_tag['value'] = tag['value'] + self.result['tags'].append(result_tag) + if 'domain' in template: + self.result['domain'] = template['domain'] + if 'account' in template: + self.result['account'] = template['account'] + if 'project' in template: + self.result['project'] = template['project'] + return self.result + + +def main(): + module = AnsibleModule( + argument_spec = dict( + name = dict(required=True), + displaytext = dict(default=None), + url = dict(default=None), + vm = dict(default=None), + snapshot = dict(default=None), + os_type = dict(default=None), + is_ready = dict(type='bool', choices=BOOLEANS, default=False), + is_public = dict(type='bool', choices=BOOLEANS, default=True), + is_featured = dict(type='bool', choices=BOOLEANS, default=False), + is_dynamically_scalable = dict(type='bool', choices=BOOLEANS, default=False), + is_extractable = dict(type='bool', choices=BOOLEANS, default=False), + is_routing = dict(type='bool', choices=BOOLEANS, default=False), + checksum = dict(default=None), + template_filter = dict(default='self', choices=['featured', 'self', 'selfexecutable', 'sharedexecutable', 'executable', 'community']), + hypervisor = dict(choices=['KVM', 'VMware', 'BareMetal', 'XenServer', 'LXC', 'HyperV', 'UCS', 'OVM'], default=None), + requires_hvm = dict(type='bool', choices=BOOLEANS, default=False), + password_enabled = dict(type='bool', choices=BOOLEANS, default=False), + template_tag = dict(default=None), + sshkey_enabled = dict(type='bool', choices=BOOLEANS, default=False), + format = dict(choices=['QCOW2', 'RAW', 'VHD', 'OVA'], default=None), + details = dict(default=None), + bits = dict(type='int', choices=[ 32, 64 ], default=64), + state = dict(choices=['present', 'absent'], default='present'), + zone = dict(default=None), + domain = dict(default=None), + account = dict(default=None), + project = dict(default=None), + poll_async = dict(type='bool', choices=BOOLEANS, default=True), + api_key = dict(default=None), + api_secret = dict(default=None), + api_url = dict(default=None), + api_http_method = dict(choices=['get', 'post'], default='get'), + api_timeout = dict(type='int', default=10), + ), + mutually_exclusive = ( + ['url', 'vm'], + ), + required_together = ( + ['api_key', 'api_secret', 'api_url'], + ['format', 'url', 'hypervisor'], + ), + required_one_of = ( + ['url', 'vm'], + ), + supports_check_mode=True + ) + + if not has_lib_cs: + module.fail_json(msg="python library cs required: pip install cs") + + try: + acs_tpl = AnsibleCloudStackTemplate(module) + + state = module.params.get('state') + if state in ['absent']: + tpl = acs_tpl.remove_template() + else: + url = module.params.get('url') + if url: + tpl = acs_tpl.register_template() + else: + tpl = acs_tpl.create_template() + + result = acs_tpl.get_result(tpl) + + except CloudStackException, e: + module.fail_json(msg='CloudStackException: %s' % str(e)) + + except Exception, e: + module.fail_json(msg='Exception: %s' % str(e)) + + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * +main() From 96d82b4f9ef61aab4e5a340eefbac973883adecb Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Mon, 15 Jun 2015 12:12:49 +0200 Subject: [PATCH 0130/2522] cloudstack: fix clean_up arg to be boolean in cs_network --- cloud/cloudstack/cs_network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/cloudstack/cs_network.py b/cloud/cloudstack/cs_network.py index c8b3b32539d..e22eaf0a5c3 100644 --- a/cloud/cloudstack/cs_network.py +++ b/cloud/cloudstack/cs_network.py @@ -116,7 +116,7 @@ - Cleanup old network elements. - Only considered on C(state=restarted). required: false - default: null + default: false acl_type: description: - Access control type. @@ -584,7 +584,7 @@ def main(): vlan = dict(default=None), vpc = dict(default=None), isolated_pvlan = dict(default=None), - clean_up = dict(default=None), + clean_up = dict(type='bool', choices=BOOLEANS, default=False), network_domain = dict(default=None), state = dict(choices=['present', 'absent', 'restarted' ], default='present'), acl_type = dict(choices=['account', 'domain'], default='account'), From 5c39a5cc197f7874595bb19bdd611a759a07518b Mon Sep 17 00:00:00 2001 From: whiter Date: Wed, 15 Apr 2015 17:45:41 +1000 Subject: [PATCH 0131/2522] New module - ec2_eni --- cloud/amazon/ec2_eni.py | 404 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 404 insertions(+) create mode 100644 cloud/amazon/ec2_eni.py diff --git a/cloud/amazon/ec2_eni.py b/cloud/amazon/ec2_eni.py new file mode 100644 index 00000000000..2b34e9b9405 --- /dev/null +++ b/cloud/amazon/ec2_eni.py @@ -0,0 +1,404 @@ +#!/usr/bin/python +# +# This is a free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This Ansible library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this library. If not, see . + +DOCUMENTATION = ''' +--- +module: ec2_eni +short_description: Create and optionally attach an Elastic Network Interface (ENI) to an instance +description: + - Create and optionally attach an Elastic Network Interface (ENI) to an instance. If an ENI ID is provided, an attempt is made to update the existing ENI. By passing 'None' as the instance_id, an ENI can be detached from an instance. +version_added: "2.0" +author: Rob White, wimnat [at] gmail.com, @wimnat +options: + eni_id: + description: + - The ID of the ENI + required = false + default = null + instance_id: + description: + - Instance ID that you wish to attach ENI to. To detach an ENI from an instance, use 'None'. + required: false + default: null + private_ip_address: + description: + - Private IP address. + required: false + default: null + subnet_id: + description: + - ID of subnet in which to create the ENI. Only required when state=present. + required: true + description: + description: + - Optional description of the ENI. + required: false + default: null + security_groups: + description: + - List of security groups associated with the interface. Only used when state=present. + required: false + default: null + state: + description: + - Create or delete ENI. + required: false + default: present + choices: [ 'present', 'absent' ] + device_index: + description: + - The index of the device for the network interface attachment on the instance. + required: false + default: 0 + force_detach: + description: + - Force detachment of the interface. This applies either when explicitly detaching the interface by setting instance_id to None or when deleting an interface with state=absent. + required: false + default: no + delete_on_termination: + description: + - Delete the interface when the instance it is attached to is terminated. You can only specify this flag when the interface is being modified, not on creation. + required: false + source_dest_check: + description: + - By default, interfaces perform source/destination checks. NAT instances however need this check to be disabled. You can only specify this flag when the interface is being modified, not on creation. + required: false +extends_documentation_fragment: aws +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Create an ENI. As no security group is defined, ENI will be created in default security group +- ec2_eni: + private_ip_address: 172.31.0.20 + subnet_id: subnet-xxxxxxxx + state: present + +# Create an ENI and attach it to an instance +- ec2_eni: + instance_id: i-xxxxxxx + device_index: 1 + private_ip_address: 172.31.0.20 + subnet_id: subnet-xxxxxxxx + state: present + +# Destroy an ENI, detaching it from any instance if necessary +- ec2_eni: + eni_id: eni-xxxxxxx + force_detach: yes + state: absent + +# Update an ENI +- ec2_eni: + eni_id: eni-xxxxxxx + description: "My new description" + state: present + +# Detach an ENI from an instance +- ec2_eni: + eni_id: eni-xxxxxxx + instance_id: None + state: present + +### Delete an interface on termination +# First create the interface +- ec2_eni: + instance_id: i-xxxxxxx + device_index: 1 + private_ip_address: 172.31.0.20 + subnet_id: subnet-xxxxxxxx + state: present + register: eni + +# Modify the interface to enable the delete_on_terminaton flag +- ec2_eni: + eni_id: {{ "eni.interface.id" }} + delete_on_termination: true + +''' + +import time +import xml.etree.ElementTree as ET +import re + +try: + import boto.ec2 + from boto.exception import BotoServerError + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + + +def get_error_message(xml_string): + + root = ET.fromstring(xml_string) + for message in root.findall('.//Message'): + return message.text + + +def get_eni_info(interface): + + interface_info = {'id': interface.id, + 'subnet_id': interface.subnet_id, + 'vpc_id': interface.vpc_id, + 'description': interface.description, + 'owner_id': interface.owner_id, + 'status': interface.status, + 'mac_address': interface.mac_address, + 'private_ip_address': interface.private_ip_address, + 'source_dest_check': interface.source_dest_check, + 'groups': dict((group.id, group.name) for group in interface.groups), + } + + if interface.attachment is not None: + interface_info['attachment'] = {'attachment_id': interface.attachment.id, + 'instance_id': interface.attachment.instance_id, + 'device_index': interface.attachment.device_index, + 'status': interface.attachment.status, + 'attach_time': interface.attachment.attach_time, + 'delete_on_termination': interface.attachment.delete_on_termination, + } + + return interface_info + +def wait_for_eni(eni, status): + + while True: + time.sleep(3) + eni.update() + # If the status is detached we just need attachment to disappear + if eni.attachment is None: + if status == "detached": + break + else: + if status == "attached" and eni.attachment.status == "attached": + break + + +def create_eni(connection, module): + + instance_id = module.params.get("instance_id") + if instance_id == 'None': + instance_id = None + do_detach = True + else: + do_detach = False + device_index = module.params.get("device_index") + subnet_id = module.params.get('subnet_id') + private_ip_address = module.params.get('private_ip_address') + description = module.params.get('description') + security_groups = module.params.get('security_groups') + changed = False + + try: + eni = compare_eni(connection, module) + if eni is None: + eni = connection.create_network_interface(subnet_id, private_ip_address, description, security_groups) + if instance_id is not None: + try: + eni.attach(instance_id, device_index) + except BotoServerError as ex: + eni.delete() + raise + changed = True + # Wait to allow creation / attachment to finish + wait_for_eni(eni, "attached") + eni.update() + + except BotoServerError as e: + module.fail_json(msg=get_error_message(e.args[2])) + + module.exit_json(changed=changed, interface=get_eni_info(eni)) + + +def modify_eni(connection, module): + + eni_id = module.params.get("eni_id") + instance_id = module.params.get("instance_id") + if instance_id == 'None': + instance_id = None + do_detach = True + else: + do_detach = False + device_index = module.params.get("device_index") + subnet_id = module.params.get('subnet_id') + private_ip_address = module.params.get('private_ip_address') + description = module.params.get('description') + security_groups = module.params.get('security_groups') + force_detach = module.params.get("force_detach") + source_dest_check = module.params.get("source_dest_check") + delete_on_termination = module.params.get("delete_on_termination") + changed = False + + + try: + # Get the eni with the eni_id specified + eni_result_set = connection.get_all_network_interfaces(eni_id) + eni = eni_result_set[0] + if description is not None: + if eni.description != description: + connection.modify_network_interface_attribute(eni.id, "description", description) + changed = True + if security_groups is not None: + if sorted(get_sec_group_list(eni.groups)) != sorted(security_groups): + connection.modify_network_interface_attribute(eni.id, "groupSet", security_groups) + changed = True + if source_dest_check is not None: + if eni.source_dest_check != source_dest_check: + connection.modify_network_interface_attribute(eni.id, "sourceDestCheck", source_dest_check) + changed = True + if delete_on_termination is not None: + if eni.attachment is not None: + if eni.attachment.delete_on_termination is not delete_on_termination: + connection.modify_network_interface_attribute(eni.id, "deleteOnTermination", delete_on_termination, eni.attachment.id) + changed = True + else: + module.fail_json(msg="Can not modify delete_on_termination as the interface is not attached") + if eni.attachment is not None and instance_id is None and do_detach is True: + eni.detach(force_detach) + wait_for_eni(eni, "detached") + changed = True + else: + if instance_id is not None: + eni.attach(instance_id, device_index) + wait_for_eni(eni, "attached") + changed = True + + except BotoServerError as e: + print e + module.fail_json(msg=get_error_message(e.args[2])) + + eni.update() + module.exit_json(changed=changed, interface=get_eni_info(eni)) + + +def delete_eni(connection, module): + + eni_id = module.params.get("eni_id") + force_detach = module.params.get("force_detach") + + try: + eni_result_set = connection.get_all_network_interfaces(eni_id) + eni = eni_result_set[0] + + if force_detach is True: + if eni.attachment is not None: + eni.detach(force_detach) + # Wait to allow detachment to finish + wait_for_eni(eni, "detached") + eni.update() + eni.delete() + changed = True + else: + eni.delete() + changed = True + + module.exit_json(changed=changed) + except BotoServerError as e: + msg = get_error_message(e.args[2]) + regex = re.compile('The networkInterface ID \'.*\' does not exist') + if regex.search(msg) is not None: + module.exit_json(changed=False) + else: + module.fail_json(msg=get_error_message(e.args[2])) + +def compare_eni(connection, module): + + eni_id = module.params.get("eni_id") + subnet_id = module.params.get('subnet_id') + private_ip_address = module.params.get('private_ip_address') + description = module.params.get('description') + security_groups = module.params.get('security_groups') + + try: + all_eni = connection.get_all_network_interfaces(eni_id) + + for eni in all_eni: + remote_security_groups = get_sec_group_list(eni.groups) + if (eni.subnet_id == subnet_id) and (eni.private_ip_address == private_ip_address) and (eni.description == description) and (remote_security_groups == security_groups): + return eni + + except BotoServerError as e: + module.fail_json(msg=get_error_message(e.args[2])) + + return None + +def get_sec_group_list(groups): + + # Build list of remote security groups + remote_security_groups = [] + for group in groups: + remote_security_groups.append(group.id.encode()) + + return remote_security_groups + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + eni_id = dict(default=None), + instance_id = dict(default=None), + private_ip_address = dict(), + subnet_id = dict(), + description = dict(), + security_groups = dict(type='list'), + device_index = dict(default=0, type='int'), + state = dict(default='present', choices=['present', 'absent']), + force_detach = dict(default='no', type='bool'), + source_dest_check = dict(default=None, type='bool'), + delete_on_termination = dict(default=None, type='bool') + ) + ) + + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO: + module.fail_json(msg='boto required for this module') + + region, ec2_url, aws_connect_params = get_aws_connection_info(module) + + if region: + try: + connection = connect_to_aws(boto.ec2, region, **aws_connect_params) + except (boto.exception.NoAuthHandlerFound, StandardError), e: + module.fail_json(msg=str(e)) + else: + module.fail_json(msg="region must be specified") + + state = module.params.get("state") + eni_id = module.params.get("eni_id") + + if state == 'present': + if eni_id is None: + if module.params.get("subnet_id") is None: + module.fail_json(msg="subnet_id must be specified when state=present") + create_eni(connection, module) + else: + modify_eni(connection, module) + elif state == 'absent': + if eni_id is None: + module.fail_json(msg="eni_id must be specified") + else: + delete_eni(connection, module) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +# this is magic, see lib/ansible/module_common.py +#<> + +main() From 8311854fa6a93b10da38c83ad5d62269337e5feb Mon Sep 17 00:00:00 2001 From: whiter Date: Tue, 16 Jun 2015 12:21:37 +1000 Subject: [PATCH 0132/2522] New module - ec2_eni_facts --- cloud/amazon/ec2_eni_facts.py | 135 ++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 cloud/amazon/ec2_eni_facts.py diff --git a/cloud/amazon/ec2_eni_facts.py b/cloud/amazon/ec2_eni_facts.py new file mode 100644 index 00000000000..94b586fb639 --- /dev/null +++ b/cloud/amazon/ec2_eni_facts.py @@ -0,0 +1,135 @@ +#!/usr/bin/python +# +# This is a free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This Ansible library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this library. If not, see . + +DOCUMENTATION = ''' +--- +module: ec2_eni_facts +short_description: Gather facts about ec2 ENI interfaces in AWS +description: + - Gather facts about ec2 ENI interfaces in AWS +version_added: "2.0" +author: Rob White, wimnat [at] gmail.com, @wimnat +options: + eni_id: + description: + - The ID of the ENI. Pass this option to gather facts about a particular ENI, otherwise, all ENIs are returned. + required = false + default = null +extends_documentation_fragment: aws +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Gather facts about all ENIs +- ec2_eni_facts: + +# Gather facts about a particular ENI +- ec2_eni_facts: + eni_id: eni-xxxxxxx + +''' + +import xml.etree.ElementTree as ET + +try: + import boto.ec2 + from boto.exception import BotoServerError + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + + +def get_error_message(xml_string): + + root = ET.fromstring(xml_string) + for message in root.findall('.//Message'): + return message.text + + +def get_eni_info(interface): + + interface_info = {'id': interface.id, + 'subnet_id': interface.subnet_id, + 'vpc_id': interface.vpc_id, + 'description': interface.description, + 'owner_id': interface.owner_id, + 'status': interface.status, + 'mac_address': interface.mac_address, + 'private_ip_address': interface.private_ip_address, + 'source_dest_check': interface.source_dest_check, + 'groups': dict((group.id, group.name) for group in interface.groups), + } + + if interface.attachment is not None: + interface_info['attachment'] = {'attachment_id': interface.attachment.id, + 'instance_id': interface.attachment.instance_id, + 'device_index': interface.attachment.device_index, + 'status': interface.attachment.status, + 'attach_time': interface.attachment.attach_time, + 'delete_on_termination': interface.attachment.delete_on_termination, + } + + return interface_info + + +def list_eni(connection, module): + + eni_id = module.params.get("eni_id") + interface_dict_array = [] + + try: + all_eni = connection.get_all_network_interfaces(eni_id) + except BotoServerError as e: + module.fail_json(msg=get_error_message(e.args[2])) + + for interface in all_eni: + interface_dict_array.append(get_eni_info(interface)) + + module.exit_json(interfaces=interface_dict_array) + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + eni_id = dict(default=None) + ) + ) + + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO: + module.fail_json(msg='boto required for this module') + + region, ec2_url, aws_connect_params = get_aws_connection_info(module) + + if region: + try: + connection = connect_to_aws(boto.ec2, region, **aws_connect_params) + except (boto.exception.NoAuthHandlerFound, StandardError), e: + module.fail_json(msg=str(e)) + else: + module.fail_json(msg="region must be specified") + + list_eni(connection, module) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +# this is magic, see lib/ansible/module_common.py +#<> + +main() From 8829b818b8a1c603364f5af548705625fc9af718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Gr=C3=B6ning?= Date: Fri, 7 Nov 2014 14:14:12 +0100 Subject: [PATCH 0133/2522] add function for servicegrup downtimes --- monitoring/nagios.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/monitoring/nagios.py b/monitoring/nagios.py index 5744fb28875..5b14b331624 100644 --- a/monitoring/nagios.py +++ b/monitoring/nagios.py @@ -169,6 +169,7 @@ def main(): 'silence_nagios', 'unsilence_nagios', 'command', + 'servicegroup_downtime' ] module = AnsibleModule( @@ -176,6 +177,7 @@ def main(): action=dict(required=True, default=None, choices=ACTION_CHOICES), author=dict(default='Ansible'), host=dict(required=False, default=None), + servicegroup=dict(required=False, default=None), minutes=dict(default=30), cmdfile=dict(default=which_cmdfile()), services=dict(default=None, aliases=['service']), @@ -185,6 +187,7 @@ def main(): action = module.params['action'] host = module.params['host'] + servicegroup = module.params['servicegroup'] minutes = module.params['minutes'] services = module.params['services'] cmdfile = module.params['cmdfile'] @@ -201,7 +204,7 @@ def main(): # 'minutes' and 'service' manually. ################################################################## - if action not in ['command', 'silence_nagios', 'unsilence_nagios']: + if action not in ['command', 'silence_nagios', 'unsilence_nagios', 'servicegroup_downtime']: if not host: module.fail_json(msg='no host specified for action requiring one') ###################################################################### @@ -217,6 +220,20 @@ def main(): except Exception: module.fail_json(msg='invalid entry for minutes') + ###################################################################### + + if action == 'servicegroup_downtime': + # Make sure there's an actual service selected + if not servicegroup: + module.fail_json(msg='no servicegroup selected to set downtime for') + # Make sure minutes is a number + try: + m = int(minutes) + if not isinstance(m, types.IntType): + module.fail_json(msg='minutes must be a number') + except Exception: + module.fail_json(msg='invalid entry for minutes') + ################################################################## if action in ['enable_alerts', 'disable_alerts']: if not services: @@ -259,6 +276,7 @@ def __init__(self, module, **kwargs): self.action = kwargs['action'] self.author = kwargs['author'] self.host = kwargs['host'] + self.service_group = kwargs['servicegroup'] self.minutes = int(kwargs['minutes']) self.cmdfile = kwargs['cmdfile'] self.command = kwargs['command'] @@ -847,6 +865,9 @@ def act(self): self.schedule_svc_downtime(self.host, services=self.services, minutes=self.minutes) + if self.action == "servicegroup_downtime": + if self.services == 'servicegroup': + self.schedule_servicegroup_host_downtime(self, self.servicegroup, minutes=30) # toggle the host AND service alerts elif self.action == 'silence': From 0b9863ed0e903b0da5b8ad9d548d4a96cbcd2ea3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Gr=C3=B6ning?= Date: Fri, 7 Nov 2014 14:36:04 +0100 Subject: [PATCH 0134/2522] divided between host an service downtimes --- monitoring/nagios.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/monitoring/nagios.py b/monitoring/nagios.py index 5b14b331624..510ca720fd7 100644 --- a/monitoring/nagios.py +++ b/monitoring/nagios.py @@ -33,7 +33,8 @@ required: true default: null choices: [ "downtime", "enable_alerts", "disable_alerts", "silence", "unsilence", - "silence_nagios", "unsilence_nagios", "command" ] + "silence_nagios", "unsilence_nagios", "command", "servicegroup_service_downtime", + "servicegroup_host_downtime" ] host: description: - Host to operate on in Nagios. @@ -90,6 +91,12 @@ # schedule downtime for a few services - nagios: action=downtime services=frob,foobar,qeuz host={{ inventory_hostname }} +# set 30 minutes downtime for all services in servicegroup foo +- nagios: action=servicegroup_service_downtime minutes=30 servicegroup=foo host={{ inventory_hostname }} + +# set 30 minutes downtime for all host in servicegroup foo +- nagios: action=servicegroup_host_downtime minutes=30 servicegroup=foo host={{ inventory_hostname }} + # enable SMART disk alerts - nagios: action=enable_alerts service=smart host={{ inventory_hostname }} @@ -169,9 +176,11 @@ def main(): 'silence_nagios', 'unsilence_nagios', 'command', - 'servicegroup_downtime' + 'servicegroup_host_downtime', + 'servicegroup_service_downtime', ] + module = AnsibleModule( argument_spec=dict( action=dict(required=True, default=None, choices=ACTION_CHOICES), @@ -222,8 +231,8 @@ def main(): ###################################################################### - if action == 'servicegroup_downtime': - # Make sure there's an actual service selected + if action in ['servicegroup_service_downtime', 'servicegroup_host_downtime']: + # Make sure there's an actual servicegroup selected if not servicegroup: module.fail_json(msg='no servicegroup selected to set downtime for') # Make sure minutes is a number @@ -865,7 +874,10 @@ def act(self): self.schedule_svc_downtime(self.host, services=self.services, minutes=self.minutes) - if self.action == "servicegroup_downtime": + elif self.action == "servicegroup_host_downtime": + if self.services == 'servicegroup': + self.schedule_servicegroup_host_downtime(self, self.servicegroup, minutes=30) + elif self.action == "servicegroup_service_downtime": if self.services == 'servicegroup': self.schedule_servicegroup_host_downtime(self, self.servicegroup, minutes=30) From 304abbce854b81e071334f973be10e4453a004ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Gr=C3=B6ning?= Date: Fri, 7 Nov 2014 15:00:57 +0100 Subject: [PATCH 0135/2522] improved docs --- monitoring/nagios.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/monitoring/nagios.py b/monitoring/nagios.py index 510ca720fd7..4fb44ea0089 100644 --- a/monitoring/nagios.py +++ b/monitoring/nagios.py @@ -66,6 +66,10 @@ aliases: [ "service" ] required: true default: null + servicegroup: + description: + - the Servicegroup we want to set downtimes/alerts for. + B(Required) option when using the C(servicegroup_service_downtime) amd C(servicegroup_host_downtime). command: description: - The raw command to send to nagios, which From f9041a1b29f115791cc244bda10c5fad7976e0a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Gr=C3=B6ning?= Date: Fri, 7 Nov 2014 17:16:48 +0100 Subject: [PATCH 0136/2522] fix bugs --- monitoring/nagios.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/monitoring/nagios.py b/monitoring/nagios.py index 4fb44ea0089..7177ffd2f43 100644 --- a/monitoring/nagios.py +++ b/monitoring/nagios.py @@ -217,7 +217,7 @@ def main(): # 'minutes' and 'service' manually. ################################################################## - if action not in ['command', 'silence_nagios', 'unsilence_nagios', 'servicegroup_downtime']: + if action not in ['command', 'silence_nagios', 'unsilence_nagios']: if not host: module.fail_json(msg='no host specified for action requiring one') ###################################################################### @@ -289,7 +289,7 @@ def __init__(self, module, **kwargs): self.action = kwargs['action'] self.author = kwargs['author'] self.host = kwargs['host'] - self.service_group = kwargs['servicegroup'] + self.servicegroup = kwargs['servicegroup'] self.minutes = int(kwargs['minutes']) self.cmdfile = kwargs['cmdfile'] self.command = kwargs['command'] @@ -879,11 +879,11 @@ def act(self): services=self.services, minutes=self.minutes) elif self.action == "servicegroup_host_downtime": - if self.services == 'servicegroup': - self.schedule_servicegroup_host_downtime(self, self.servicegroup, minutes=30) + if self.servicegroup: + self.schedule_servicegroup_host_downtime(servicegroup = self.servicegroup, minutes = self.minutes) elif self.action == "servicegroup_service_downtime": - if self.services == 'servicegroup': - self.schedule_servicegroup_host_downtime(self, self.servicegroup, minutes=30) + if self.servicegroup: + self.schedule_servicegroup_svc_downtime(servicegroup = self.servicegroup, minutes = self.minutes) # toggle the host AND service alerts elif self.action == 'silence': From bc440ade79115acd83e58f01e7e7e737d430efd2 Mon Sep 17 00:00:00 2001 From: Nicolas Brisac Date: Fri, 14 Nov 2014 17:09:24 +0100 Subject: [PATCH 0137/2522] Allow filtering of routed/forwarded packets MAN page states the following : Rules for traffic not destined for the host itself but instead for traffic that should be routed/forwarded through the firewall should specify the route keyword before the rule (routing rules differ significantly from PF syntax and instead take into account netfilter FORWARD chain conventions). For example: ufw route allow in on eth1 out on eth2 This commit introduces a new parameter "route=yes/no" to allow just that. --- system/ufw.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/system/ufw.py b/system/ufw.py index 3694f2b937a..91d574f945d 100644 --- a/system/ufw.py +++ b/system/ufw.py @@ -116,6 +116,11 @@ - Specify interface for rule. required: false aliases: ['if'] + route: + description: + - Apply the rule to routed/forwarded packets. + required: false + choices: ['yes', 'no'] ''' EXAMPLES = ''' @@ -165,6 +170,10 @@ # Deny all traffic from the IPv6 2001:db8::/32 to tcp port 25 on this host. # Note that IPv6 must be enabled in /etc/default/ufw for IPv6 firewalling to work. ufw: rule=deny proto=tcp src=2001:db8::/32 port=25 + +# Deny forwarded/routed traffic from subnet 1.2.3.0/24 to subnet 4.5.6.0/24. +# Can be used to further restrict a global FORWARD policy set to allow +ufw: rule=deny route=yes src=1.2.3.0/24 dest=4.5.6.0/24 ''' from operator import itemgetter @@ -178,6 +187,7 @@ def main(): logging = dict(default=None, choices=['on', 'off', 'low', 'medium', 'high', 'full']), direction = dict(default=None, choices=['in', 'incoming', 'out', 'outgoing', 'routed']), delete = dict(default=False, type='bool'), + route = dict(default=False, type='bool'), insert = dict(default=None), rule = dict(default=None, choices=['allow', 'deny', 'reject', 'limit']), interface = dict(default=None, aliases=['if']), @@ -241,10 +251,11 @@ def execute(cmd): elif command == 'rule': # Rules are constructed according to the long format # - # ufw [--dry-run] [delete] [insert NUM] allow|deny|reject|limit [in|out on INTERFACE] [log|log-all] \ + # ufw [--dry-run] [delete] [insert NUM] [route] allow|deny|reject|limit [in|out on INTERFACE] [log|log-all] \ # [from ADDRESS [port PORT]] [to ADDRESS [port PORT]] \ # [proto protocol] [app application] cmd.append([module.boolean(params['delete']), 'delete']) + cmd.append([module.boolean(params['route']), 'route']) cmd.append([params['insert'], "insert %s" % params['insert']]) cmd.append([value]) cmd.append([module.boolean(params['log']), 'log']) From b80d2b3cfaf3e166c0de06802ea328069d365910 Mon Sep 17 00:00:00 2001 From: James Cammarata Date: Tue, 25 Nov 2014 15:50:27 -0600 Subject: [PATCH 0138/2522] Adding VERSION file for 1.8.0 --- VERSION | 1 + 1 file changed, 1 insertion(+) create mode 100644 VERSION diff --git a/VERSION b/VERSION new file mode 100644 index 00000000000..27f9cd322bb --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.8.0 From 9859aa0435faeec49a5b31f0b7275ef868e95597 Mon Sep 17 00:00:00 2001 From: James Cammarata Date: Wed, 26 Nov 2014 21:32:16 -0600 Subject: [PATCH 0139/2522] Version bump for extras release 1.8.1 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 27f9cd322bb..a8fdfda1c78 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.8.0 +1.8.1 From 45423973fc23a2f184d2f871f16119db5c5102ff Mon Sep 17 00:00:00 2001 From: James Cammarata Date: Thu, 4 Dec 2014 15:50:48 -0600 Subject: [PATCH 0140/2522] Version bump for 1.8.2 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index a8fdfda1c78..53adb84c822 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.8.1 +1.8.2 From 8383857b5a89f5745268ef9ad38fe2e833604e63 Mon Sep 17 00:00:00 2001 From: Jason Holland Date: Tue, 25 Nov 2014 14:43:47 -0600 Subject: [PATCH 0141/2522] Fix some logical issues with enabling/disabling a server on the A10. --- network/a10/a10_server.py | 51 +++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/network/a10/a10_server.py b/network/a10/a10_server.py index 2d7b8cc5d9c..6714366f1b1 100644 --- a/network/a10/a10_server.py +++ b/network/a10/a10_server.py @@ -183,28 +183,35 @@ def main(): json_post = { 'server': { - 'name': slb_server, - 'host': slb_server_ip, - 'status': axapi_enabled_disabled(slb_server_status), - 'port_list': slb_server_ports, + 'name': slb_server, } } + # add optional module parameters + if slb_server_ip: + json_post['server']['host'] = slb_server_ip + + if slb_server_ports: + json_post['server']['port_list'] = slb_server_ports + + if slb_server_status: + json_post['server']['status'] = axapi_enabled_disabled(slb_server_status) + slb_server_data = axapi_call(module, session_url + '&method=slb.server.search', json.dumps({'name': slb_server})) slb_server_exists = not axapi_failure(slb_server_data) changed = False if state == 'present': - if not slb_server_ip: - module.fail_json(msg='you must specify an IP address when creating a server') - if not slb_server_exists: + if not slb_server_ip: + module.fail_json(msg='you must specify an IP address when creating a server') + result = axapi_call(module, session_url + '&method=slb.server.create', json.dumps(json_post)) if axapi_failure(result): module.fail_json(msg="failed to create the server: %s" % result['response']['err']['msg']) changed = True else: - def needs_update(src_ports, dst_ports): + def port_needs_update(src_ports, dst_ports): ''' Checks to determine if the port definitions of the src_ports array are in or different from those in dst_ports. If there is @@ -227,12 +234,26 @@ def needs_update(src_ports, dst_ports): # every port from the src exists in the dst, and none of them were different return False + def status_needs_update(current_status, new_status): + ''' + Check to determine if we want to change the status of a server. + If there is a difference between the current status of the server and + the desired status, return true, otherwise false. + ''' + if current_status != new_status: + return True + return False + defined_ports = slb_server_data.get('server', {}).get('port_list', []) + current_status = slb_server_data.get('server', {}).get('status') - # we check for a needed update both ways, in case ports - # are missing from either the ones specified by the user - # or from those on the device - if needs_update(defined_ports, slb_server_ports) or needs_update(slb_server_ports, defined_ports): + # we check for a needed update several ways + # - in case ports are missing from the ones specified by the user + # - in case ports are missing from those on the device + # - in case we are change the status of a server + if port_needs_update(defined_ports, slb_server_ports) + or port_needs_update(slb_server_ports, defined_ports) + or status_needs_update(current_status, axapi_enabled_disabled(slb_server_status)): result = axapi_call(module, session_url + '&method=slb.server.update', json.dumps(json_post)) if axapi_failure(result): module.fail_json(msg="failed to update the server: %s" % result['response']['err']['msg']) @@ -249,10 +270,10 @@ def needs_update(src_ports, dst_ports): result = axapi_call(module, session_url + '&method=slb.server.delete', json.dumps({'name': slb_server})) changed = True else: - result = dict(msg="the server was not present") + result = dict(msg="the server was not present") - # if the config has changed, save the config unless otherwise requested - if changed and write_config: + # if the config has changed, or we want to force a save, save the config unless otherwise requested + if changed or write_config: write_result = axapi_call(module, session_url + '&method=system.action.write_memory') if axapi_failure(write_result): module.fail_json(msg="failed to save the configuration: %s" % write_result['response']['err']['msg']) From 669316195f3dc44e7fcbc24636065a32b889bf04 Mon Sep 17 00:00:00 2001 From: Jason Holland Date: Thu, 4 Dec 2014 16:15:23 -0600 Subject: [PATCH 0142/2522] Fix small issue with wrapping syntax --- network/a10/a10_server.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/network/a10/a10_server.py b/network/a10/a10_server.py index 6714366f1b1..72ed0f648e6 100644 --- a/network/a10/a10_server.py +++ b/network/a10/a10_server.py @@ -251,9 +251,7 @@ def status_needs_update(current_status, new_status): # - in case ports are missing from the ones specified by the user # - in case ports are missing from those on the device # - in case we are change the status of a server - if port_needs_update(defined_ports, slb_server_ports) - or port_needs_update(slb_server_ports, defined_ports) - or status_needs_update(current_status, axapi_enabled_disabled(slb_server_status)): + if port_needs_update(defined_ports, slb_server_ports) or port_needs_update(slb_server_ports, defined_ports) or status_needs_update(current_status, axapi_enabled_disabled(slb_server_status)): result = axapi_call(module, session_url + '&method=slb.server.update', json.dumps(json_post)) if axapi_failure(result): module.fail_json(msg="failed to update the server: %s" % result['response']['err']['msg']) From 2be58620e26f456ea2aa4594b6aafddd299e2390 Mon Sep 17 00:00:00 2001 From: Giovanni Tirloni Date: Thu, 22 Jan 2015 09:13:12 -0500 Subject: [PATCH 0143/2522] add createparent option to zfs create --- system/zfs.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/system/zfs.py b/system/zfs.py index fed17b4a18d..503ca7d09ef 100644 --- a/system/zfs.py +++ b/system/zfs.py @@ -250,7 +250,7 @@ def create(self): if self.module.check_mode: self.changed = True return - properties=self.properties + properties = self.properties volsize = properties.pop('volsize', None) volblocksize = properties.pop('volblocksize', None) if "@" in self.name: @@ -260,6 +260,10 @@ def create(self): cmd = [self.module.get_bin_path('zfs', True)] cmd.append(action) + + if createparent: + cmd.append('-p') + if volblocksize: cmd.append('-b %s' % volblocksize) if properties: @@ -271,7 +275,7 @@ def create(self): cmd.append(self.name) (rc, err, out) = self.module.run_command(' '.join(cmd)) if rc == 0: - self.changed=True + self.changed = True else: self.module.fail_json(msg=out) @@ -345,6 +349,7 @@ def main(): 'checksum': {'required': False, 'choices':['on', 'off', 'fletcher2', 'fletcher4', 'sha256']}, 'compression': {'required': False, 'choices':['on', 'off', 'lzjb', 'gzip', 'gzip-1', 'gzip-2', 'gzip-3', 'gzip-4', 'gzip-5', 'gzip-6', 'gzip-7', 'gzip-8', 'gzip-9', 'lz4', 'zle']}, 'copies': {'required': False, 'choices':['1', '2', '3']}, + 'createparent': {'required': False, 'choices':['on', 'off']}, 'dedup': {'required': False, 'choices':['on', 'off']}, 'devices': {'required': False, 'choices':['on', 'off']}, 'exec': {'required': False, 'choices':['on', 'off']}, @@ -396,7 +401,7 @@ def main(): result['name'] = name result['state'] = state - zfs=Zfs(module, name, properties) + zfs = Zfs(module, name, properties) if state == 'present': if zfs.exists(): From 3d2f19c24d8dff48410964acd0703925f3102bd1 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Tue, 17 Feb 2015 16:56:15 +1100 Subject: [PATCH 0144/2522] Fix display of error message It was crashing due to "domain" variable not being defined --- network/dnsmadeeasy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/dnsmadeeasy.py b/network/dnsmadeeasy.py index dc70d0e5569..9fd840f1992 100644 --- a/network/dnsmadeeasy.py +++ b/network/dnsmadeeasy.py @@ -292,7 +292,7 @@ def main(): if not "value" in new_record: if not current_record: module.fail_json( - msg="A record with name '%s' does not exist for domain '%s.'" % (record_name, domain)) + msg="A record with name '%s' does not exist for domain '%s.'" % (record_name, module.params['domain'])) module.exit_json(changed=False, result=current_record) # create record as it does not exist From bdeb0bc8db6511d6b6ff886819a37e40ab6ef056 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Tue, 17 Feb 2015 17:13:27 +1100 Subject: [PATCH 0145/2522] If record_value="" write empty value to dns made easy This is necessary for instance when setting CNAMEs that point to the root of the domain. This is different than leaving record_value out completely which has the same behaviour as before --- network/dnsmadeeasy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/dnsmadeeasy.py b/network/dnsmadeeasy.py index 9fd840f1992..4cb6d7d96a1 100644 --- a/network/dnsmadeeasy.py +++ b/network/dnsmadeeasy.py @@ -275,7 +275,7 @@ def main(): current_record = DME.getRecordByName(record_name) new_record = {'name': record_name} for i in ["record_value", "record_type", "record_ttl"]: - if module.params[i]: + if not module.params[i] is None: new_record[i[len("record_"):]] = module.params[i] # Compare new record against existing one From 5ef2dd8a7710944abbae38ca799096808dd5fc50 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Wed, 18 Feb 2015 10:42:07 +1100 Subject: [PATCH 0146/2522] If record_name="" write empty value to dns made easy This is necessary for instance when setting MX records on the root of a domain. This is different than leaving record_name out completely which has the same behaviour as before --- network/dnsmadeeasy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/dnsmadeeasy.py b/network/dnsmadeeasy.py index 4cb6d7d96a1..46d6769f951 100644 --- a/network/dnsmadeeasy.py +++ b/network/dnsmadeeasy.py @@ -264,7 +264,7 @@ def main(): record_name = module.params["record_name"] # Follow Keyword Controlled Behavior - if not record_name: + if record_name is None: domain_records = DME.getRecords() if not domain_records: module.fail_json( From b0992a97efe81ff45ad1b789d0951e4a98a95d70 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Wed, 18 Feb 2015 12:14:58 +1100 Subject: [PATCH 0147/2522] Handle MX,NS,TXT records correctly and don't assume one record type per name --- network/dnsmadeeasy.py | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/network/dnsmadeeasy.py b/network/dnsmadeeasy.py index 46d6769f951..fcc7232a0da 100644 --- a/network/dnsmadeeasy.py +++ b/network/dnsmadeeasy.py @@ -134,6 +134,7 @@ def __init__(self, apikey, secret, domain, module): self.domain_map = None # ["domain_name"] => ID self.record_map = None # ["record_name"] => ID self.records = None # ["record_ID"] => + self.all_records = None # Lookup the domain ID if passed as a domain name vs. ID if not self.domain.isdigit(): @@ -191,11 +192,33 @@ def getRecord(self, record_id): return self.records.get(record_id, False) - def getRecordByName(self, record_name): - if not self.record_map: - self._instMap('record') - - return self.getRecord(self.record_map.get(record_name, 0)) + # Try to find a single record matching this one. + # How we do this depends on the type of record. For instance, there + # can be several MX records for a single record_name while there can + # only be a single CNAME for a particular record_name. Note also that + # there can be several records with different types for a single name. + def getMatchingRecord(self, record_name, record_type, record_value): + # Get all the records if not already cached + if not self.all_records: + self.all_records = self.getRecords() + + # TODO SRV type not yet implemented + if record_type in ["A", "AAAA", "CNAME", "HTTPRED", "PTR"]: + for result in self.all_records: + if result['name'] == record_name and result['type'] == record_type: + return result + return False + elif record_type in ["MX", "NS", "TXT"]: + for result in self.all_records: + if record_type == "MX": + value = record_value.split(" ")[1] + else: + value = record_value + if result['name'] == record_name and result['type'] == record_type and result['value'] == value: + return result + return False + else: + raise Exception('record_type not yet supported') def getRecords(self): return self.query(self.record_url, 'GET')['data'] @@ -262,6 +285,8 @@ def main(): "account_secret"], module.params["domain"], module) state = module.params["state"] record_name = module.params["record_name"] + record_type = module.params["record_type"] + record_value = module.params["record_value"] # Follow Keyword Controlled Behavior if record_name is None: @@ -272,11 +297,15 @@ def main(): module.exit_json(changed=False, result=domain_records) # Fetch existing record + Build new one - current_record = DME.getRecordByName(record_name) + current_record = DME.getMatchingRecord(record_name, record_type, record_value) new_record = {'name': record_name} for i in ["record_value", "record_type", "record_ttl"]: if not module.params[i] is None: new_record[i[len("record_"):]] = module.params[i] + # Special handling for mx record + if new_record["type"] == "MX": + new_record["mxLevel"] = new_record["value"].split(" ")[0] + new_record["value"] = new_record["value"].split(" ")[1] # Compare new record against existing one changed = False From 49ab501be482dd1417dac7c58e7af1297975b55b Mon Sep 17 00:00:00 2001 From: Kevin Klinemeier Date: Sun, 15 Mar 2015 21:42:35 -0700 Subject: [PATCH 0148/2522] Updated tags example to an actual datadog tag --- monitoring/datadog_event.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/datadog_event.py b/monitoring/datadog_event.py index 1d6a98dc9c3..5319fcb0f1b 100644 --- a/monitoring/datadog_event.py +++ b/monitoring/datadog_event.py @@ -71,7 +71,7 @@ # Post an event with several tags datadog_event: title="Testing from ansible" text="Test!" api_key="6873258723457823548234234234" - tags=aa,bb,cc + tags=aa,bb,#host:{{ inventory_hostname }} ''' import socket From d604f5616230d88af786a59d63e5a0a5f539b585 Mon Sep 17 00:00:00 2001 From: Todd Zullinger Date: Wed, 18 Mar 2015 15:07:56 -0400 Subject: [PATCH 0149/2522] monitoring/nagios: Allow comment to be specified The default remains 'Scheduling downtime' but can be overridden. --- monitoring/nagios.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/monitoring/nagios.py b/monitoring/nagios.py index 7177ffd2f43..5fd51d17123 100644 --- a/monitoring/nagios.py +++ b/monitoring/nagios.py @@ -52,6 +52,11 @@ Only usable with the C(downtime) action. required: false default: Ansible + comment: + description: + - Comment for C(downtime) action. + required: false + default: Scheduling downtime minutes: description: - Minutes to schedule downtime for. @@ -89,6 +94,10 @@ # schedule an hour of HOST downtime - nagios: action=downtime minutes=60 service=host host={{ inventory_hostname }} +# schedule an hour of HOST downtime, with a comment describing the reason +- nagios: action=downtime minutes=60 service=host host={{ inventory_hostname }} + comment='This host needs disciplined' + # schedule downtime for ALL services on HOST - nagios: action=downtime minutes=45 service=all host={{ inventory_hostname }} @@ -189,6 +198,7 @@ def main(): argument_spec=dict( action=dict(required=True, default=None, choices=ACTION_CHOICES), author=dict(default='Ansible'), + comment=dict(default='Scheduling downtime'), host=dict(required=False, default=None), servicegroup=dict(required=False, default=None), minutes=dict(default=30), @@ -288,6 +298,7 @@ def __init__(self, module, **kwargs): self.module = module self.action = kwargs['action'] self.author = kwargs['author'] + self.comment = kwargs['comment'] self.host = kwargs['host'] self.servicegroup = kwargs['servicegroup'] self.minutes = int(kwargs['minutes']) @@ -324,7 +335,7 @@ def _write_command(self, cmd): cmdfile=self.cmdfile) def _fmt_dt_str(self, cmd, host, duration, author=None, - comment="Scheduling downtime", start=None, + comment=None, start=None, svc=None, fixed=1, trigger=0): """ Format an external-command downtime string. @@ -357,6 +368,9 @@ def _fmt_dt_str(self, cmd, host, duration, author=None, if not author: author = self.author + if not comment: + comment = self.comment + if svc is not None: dt_args = [svc, str(start), str(end), str(fixed), str(trigger), str(duration_s), author, comment] From 3ea8ac0e13f67d1405057635b64798f5e80f5477 Mon Sep 17 00:00:00 2001 From: Solomon Gifford Date: Tue, 31 Mar 2015 16:43:40 -0400 Subject: [PATCH 0150/2522] \login_password with missing login_user not caught #363 --- database/misc/mongodb_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index 83a3395216e..ab690f883a8 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -222,7 +222,7 @@ def main(): if mongocnf_creds is not False: login_user = mongocnf_creds['user'] login_password = mongocnf_creds['password'] - elif login_password is None and login_user is not None: + 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') if login_user is not None and login_password is not None: From 62a7742481ee13a2e7bf421ac7052b6c3bae19c9 Mon Sep 17 00:00:00 2001 From: Solomon Gifford Date: Thu, 9 Apr 2015 14:03:14 -0400 Subject: [PATCH 0151/2522] fixes issue #362 --- database/misc/mongodb_user.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index ab690f883a8..907aeadc802 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -134,7 +134,15 @@ # MongoDB module specific support methods. # +def user_find(client, user): + for mongo_user in client["admin"].system.users.find(): + if mongo_user['user'] == user: + return mongo_user + return False + def user_add(module, client, db_name, user, password, roles): + #pymono's user_add is a _create_or_update_user so we won't know if it was changed or updated + #without reproducing a lot of the logic in database.py of pymongo db = client[db_name] if roles is None: db.add_user(user, password, False) @@ -147,9 +155,13 @@ def user_add(module, client, db_name, user, password, roles): err_msg = err_msg + ' (Note: you must be on mongodb 2.4+ and pymongo 2.5+ to use the roles param)' module.fail_json(msg=err_msg) -def user_remove(client, db_name, user): - db = client[db_name] - db.remove_user(user) +def user_remove(module, client, db_name, user): + exists = user_find(client, user) + if exists: + db = client[db_name] + db.remove_user(user) + else: + module.exit_json(changed=False, user=user) def load_mongocnf(): config = ConfigParser.RawConfigParser() @@ -208,15 +220,6 @@ def main(): else: client = MongoClient(login_host, int(login_port), ssl=ssl) - # try to authenticate as a target user to check if it already exists - try: - client[db_name].authenticate(user, password) - if state == 'present': - module.exit_json(changed=False, user=user) - except OperationFailure: - if state == 'absent': - module.exit_json(changed=False, user=user) - if login_user is None and login_password is None: mongocnf_creds = load_mongocnf() if mongocnf_creds is not False: @@ -227,6 +230,10 @@ def main(): if login_user is not None and login_password is not None: client.admin.authenticate(login_user, login_password) + elif LooseVersion(PyMongoVersion) >= LooseVersion('3.0'): + if db_name != "admin": + module.fail_json(msg='The localhost login exception only allows the first admin account to be created') + #else: this has to be the first admin user added except ConnectionFailure, e: module.fail_json(msg='unable to connect to database: %s' % str(e)) @@ -242,7 +249,7 @@ def main(): elif state == 'absent': try: - user_remove(client, db_name, user) + user_remove(module, client, db_name, user) except OperationFailure, e: module.fail_json(msg='Unable to remove user: %s' % str(e)) From d1c68eea9f7a99a41d5e8c630ceff8e4eecb97f3 Mon Sep 17 00:00:00 2001 From: Solomon Gifford Date: Thu, 9 Apr 2015 14:22:24 -0400 Subject: [PATCH 0152/2522] #364 Added support for update_password=dict(default="always", choices=["always", "on_create"]) --- database/misc/mongodb_user.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index 907aeadc802..9802f890a35 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -87,6 +87,14 @@ required: false default: present choices: [ "present", "absent" ] + update_password: + required: false + default: always + choices: ['always', 'on_create'] + version_added: "2.1" + description: + - C(always) will update passwords if they differ. C(on_create) will only set the password for newly created users. + notes: - Requires the pymongo Python package on the remote host, version 2.4.2+. This can be installed using pip or the OS package manager. @see http://api.mongodb.org/python/current/installation.html @@ -196,6 +204,7 @@ def main(): ssl=dict(default=False), roles=dict(default=None, type='list'), state=dict(default='present', choices=['absent', 'present']), + update_password=dict(default="always", choices=["always", "on_create"]), ) ) @@ -213,6 +222,7 @@ def main(): ssl = module.params['ssl'] roles = module.params['roles'] state = module.params['state'] + update_password = module.params['update_password'] try: if replica_set: @@ -239,8 +249,11 @@ def main(): module.fail_json(msg='unable to connect to database: %s' % str(e)) if state == 'present': - if password is None: - module.fail_json(msg='password parameter required when adding a user') + if password is None and update_password == 'always': + module.fail_json(msg='password parameter required when adding a user unless update_password is set to on_create') + + if update_password != 'always' and user_find(client, user): + password = None try: user_add(module, client, db_name, user, password, roles) From 05e0b35a45f9e66dfd996fe6ca3ec118b9b2f2d6 Mon Sep 17 00:00:00 2001 From: Benjamin Albrecht Date: Tue, 14 Apr 2015 20:56:36 +0200 Subject: [PATCH 0153/2522] Fix possible values for zfs sync property --- system/zfs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/zfs.py b/system/zfs.py index 503ca7d09ef..97a0d6f3dba 100644 --- a/system/zfs.py +++ b/system/zfs.py @@ -177,7 +177,7 @@ description: - The sync property. required: False - choices: ['on','off'] + choices: ['standard','always','disabled'] utf8only: description: - The utf8only property. @@ -373,7 +373,7 @@ def main(): 'sharenfs': {'required': False}, 'sharesmb': {'required': False}, 'snapdir': {'required': False, 'choices':['hidden', 'visible']}, - 'sync': {'required': False, 'choices':['on', 'off']}, + 'sync': {'required': False, 'choices':['standard', 'always', 'disabled']}, # Not supported #'userquota': {'required': False}, 'utf8only': {'required': False, 'choices':['on', 'off']}, From 02258902f9d948a22fa96a4763a3e5531637e4f9 Mon Sep 17 00:00:00 2001 From: NewGyu Date: Wed, 29 Apr 2015 23:59:16 +0900 Subject: [PATCH 0154/2522] fix cannot download SNAPSHOT version --- packaging/language/maven_artifact.py | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/packaging/language/maven_artifact.py b/packaging/language/maven_artifact.py index d6dd33166dc..057cb0a3814 100644 --- a/packaging/language/maven_artifact.py +++ b/packaging/language/maven_artifact.py @@ -184,29 +184,12 @@ def find_uri_for_artifact(self, artifact): if artifact.is_snapshot(): path = "/%s/maven-metadata.xml" % (artifact.path()) xml = self._request(self.base + path, "Failed to download maven-metadata.xml", lambda r: etree.parse(r)) - basexpath = "/metadata/versioning/" - p = xml.xpath(basexpath + "/snapshotVersions/snapshotVersion") - if p: - return self._find_matching_artifact(p, artifact) + timestamp = xml.xpath("/metadata/versioning/snapshot/timestamp/text()")[0] + buildNumber = xml.xpath("/metadata/versioning/snapshot/buildNumber/text()")[0] + return self._uri_for_artifact(artifact, artifact.version.replace("SNAPSHOT", timestamp + "-" + buildNumber)) else: return self._uri_for_artifact(artifact) - def _find_matching_artifact(self, elems, artifact): - filtered = filter(lambda e: e.xpath("extension/text() = '%s'" % artifact.extension), elems) - if artifact.classifier: - filtered = filter(lambda e: e.xpath("classifier/text() = '%s'" % artifact.classifier), elems) - - if len(filtered) > 1: - print( - "There was more than one match. Selecting the first one. Try adding a classifier to get a better match.") - elif not len(filtered): - print("There were no matches.") - return None - - elem = filtered[0] - value = elem.xpath("value/text()") - return self._uri_for_artifact(artifact, value[0]) - def _uri_for_artifact(self, artifact, version=None): if artifact.is_snapshot() and not version: raise ValueError("Expected uniqueversion for snapshot artifact " + str(artifact)) @@ -309,7 +292,7 @@ def main(): repository_url = dict(default=None), username = dict(default=None), password = dict(default=None), - state = dict(default="present", choices=["present","absent"]), # TODO - Implement a "latest" state + state = dict(default="present", choices=["present","absent"]), # TODO - Implement a "latest" state dest = dict(default=None), ) ) From f605e388273af8cd309a9a26eb28e2fe22bc2e37 Mon Sep 17 00:00:00 2001 From: Quentin Stafford-Fraser Date: Sun, 3 May 2015 20:58:21 +0100 Subject: [PATCH 0155/2522] Add webfaction modules --- cloud/webfaction/__init__.py | 0 cloud/webfaction/webfaction_app.py | 153 ++++++++++++++++++++ cloud/webfaction/webfaction_db.py | 147 +++++++++++++++++++ cloud/webfaction/webfaction_domain.py | 134 ++++++++++++++++++ cloud/webfaction/webfaction_mailbox.py | 112 +++++++++++++++ cloud/webfaction/webfaction_site.py | 189 +++++++++++++++++++++++++ 6 files changed, 735 insertions(+) create mode 100644 cloud/webfaction/__init__.py create mode 100644 cloud/webfaction/webfaction_app.py create mode 100644 cloud/webfaction/webfaction_db.py create mode 100644 cloud/webfaction/webfaction_domain.py create mode 100644 cloud/webfaction/webfaction_mailbox.py create mode 100644 cloud/webfaction/webfaction_site.py diff --git a/cloud/webfaction/__init__.py b/cloud/webfaction/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cloud/webfaction/webfaction_app.py b/cloud/webfaction/webfaction_app.py new file mode 100644 index 00000000000..b1ddcd5a9c0 --- /dev/null +++ b/cloud/webfaction/webfaction_app.py @@ -0,0 +1,153 @@ +#! /usr/bin/python +# Create a Webfaction application using Ansible and the Webfaction API +# +# Valid application types can be found by looking here: +# http://docs.webfaction.com/xmlrpc-api/apps.html#application-types +# +# Quentin Stafford-Fraser 2015 + +DOCUMENTATION = ''' +--- +module: webfaction_app +short_description: Add or remove applications on a Webfaction host +description: + - Add or remove applications on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. +author: Quentin Stafford-Fraser +version_added: 1.99 +notes: + - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." + - See `the webfaction API `_ for more info. + +options: + name: + description: + - The name of the application + required: true + default: null + + state: + description: + - Whether the application should exist + required: false + choices: ['present', 'absent'] + default: "present" + + type: + description: + - The type of application to create. See the Webfaction docs at http://docs.webfaction.com/xmlrpc-api/apps.html for a list. + required: true + + autostart: + description: + - Whether the app should restart with an autostart.cgi script + required: false + default: "no" + + extra_info: + description: + - Any extra parameters required by the app + required: false + default: null + + open_port: + required: false + default: false + + login_name: + description: + - The webfaction account to use + required: true + + login_password: + description: + - The webfaction password to use + required: true +''' + +import xmlrpclib +from ansible.module_utils.basic import * + +webfaction = xmlrpclib.ServerProxy('https://api.webfaction.com/') + +def main(): + + module = AnsibleModule( + argument_spec = dict( + name = dict(required=True, default=None), + state = dict(required=False, default='present'), + type = dict(required=True), + autostart = dict(required=False, choices=BOOLEANS, default='false'), + extra_info = dict(required=False, default=""), + port_open = dict(required=False, default="false"), + login_name = dict(required=True), + login_password = dict(required=True), + ), + supports_check_mode=True + ) + app_name = module.params['name'] + app_type = module.params['type'] + app_state = module.params['state'] + + session_id, account = webfaction.login( + module.params['login_name'], + module.params['login_password'] + ) + + app_list = webfaction.list_apps(session_id) + app_map = dict([(i['name'], i) for i in app_list]) + existing_app = app_map.get(app_name) + + result = {} + + # Here's where the real stuff happens + + if app_state == 'present': + + # Does an app with this name already exist? + if existing_app: + if existing_app['type'] != app_type: + module.fail_json(msg="App already exists with different type. Please fix by hand.") + + # If it exists with the right type, we don't change it + # Should check other parameters. + module.exit_json( + changed = False, + ) + + if not module.check_mode: + # If this isn't a dry run, create the app + result.update( + webfaction.create_app( + session_id, app_name, app_type, + module.boolean(module.params['autostart']), + module.params['extra_info'], + module.boolean(module.params['port_open']) + ) + ) + + elif app_state == 'absent': + + # If the app's already not there, nothing changed. + if not existing_app: + module.exit_json( + changed = False, + ) + + if not module.check_mode: + # If this isn't a dry run, delete the app + result.update( + webfaction.delete_app(session_id, app_name) + ) + + else: + module.fail_json(msg="Unknown state specified: {}".format(app_state)) + + + module.exit_json( + changed = True, + result = result + ) + +# The conventional ending +main() + diff --git a/cloud/webfaction/webfaction_db.py b/cloud/webfaction/webfaction_db.py new file mode 100644 index 00000000000..7205a084ef2 --- /dev/null +++ b/cloud/webfaction/webfaction_db.py @@ -0,0 +1,147 @@ +#! /usr/bin/python +# Create webfaction database using Ansible and the Webfaction API +# +# Quentin Stafford-Fraser 2015 + +DOCUMENTATION = ''' +--- +module: webfaction_db +short_description: Add or remove a database on Webfaction +description: + - Add or remove a database on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. +author: Quentin Stafford-Fraser +version_added: 1.99 +notes: + - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." + - See `the webfaction API `_ for more info. +options: + + name: + description: + - The name of the database + required: true + default: null + + state: + description: + - Whether the database should exist + required: false + choices: ['present', 'absent'] + default: "present" + + type: + description: + - The type of database to create. + required: true + choices: ['mysql', 'postgresql'] + + login_name: + description: + - The webfaction account to use + required: true + + login_password: + description: + - The webfaction password to use + required: true +''' + +EXAMPLES = ''' + # This will also create a default DB user with the same + # name as the database, and the specified password. + + - name: Create a database + webfaction_db: + name: "{{webfaction_user}}_db1" + password: mytestsql + type: mysql + login_name: "{{webfaction_user}}" + login_password: "{{webfaction_passwd}}" +''' + +import socket +import xmlrpclib +from ansible.module_utils.basic import * + +webfaction = xmlrpclib.ServerProxy('https://api.webfaction.com/') + +def main(): + + module = AnsibleModule( + argument_spec = dict( + name = dict(required=True, default=None), + state = dict(required=False, default='present'), + # You can specify an IP address or hostname. + type = dict(required=True, default=None), + password = dict(required=False, default=None), + login_name = dict(required=True), + login_password = dict(required=True), + ), + supports_check_mode=True + ) + db_name = module.params['name'] + db_state = module.params['state'] + db_type = module.params['type'] + db_passwd = module.params['password'] + + session_id, account = webfaction.login( + module.params['login_name'], + module.params['login_password'] + ) + + db_list = webfaction.list_dbs(session_id) + db_map = dict([(i['name'], i) for i in db_list]) + existing_db = db_map.get(db_name) + + result = {} + + # Here's where the real stuff happens + + if db_state == 'present': + + # Does an app with this name already exist? + if existing_db: + # Yes, but of a different type - fail + if existing_db['db_type'] != db_type: + module.fail_json(msg="Database already exists but is a different type. Please fix by hand.") + + # If it exists with the right type, we don't change anything. + module.exit_json( + changed = False, + ) + + + if not module.check_mode: + # If this isn't a dry run, create the app + # print positional_args + result.update( + webfaction.create_db( + session_id, db_name, db_type, db_passwd + ) + ) + + elif db_state == 'absent': + + # If the app's already not there, nothing changed. + if not existing_db: + module.exit_json( + changed = False, + ) + + if not module.check_mode: + # If this isn't a dry run, delete the app + result.update( + webfaction.delete_db(session_id, db_name, db_type) + ) + + else: + module.fail_json(msg="Unknown state specified: {}".format(db_state)) + + module.exit_json( + changed = True, + result = result + ) + +# The conventional ending +main() + diff --git a/cloud/webfaction/webfaction_domain.py b/cloud/webfaction/webfaction_domain.py new file mode 100644 index 00000000000..2f3c8542754 --- /dev/null +++ b/cloud/webfaction/webfaction_domain.py @@ -0,0 +1,134 @@ +#! /usr/bin/python +# Create Webfaction domains and subdomains using Ansible and the Webfaction API +# +# Quentin Stafford-Fraser 2015 + +DOCUMENTATION = ''' +--- +module: webfaction_domain +short_description: Add or remove domains and subdomains on Webfaction +description: + - Add or remove domains or subdomains on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. +author: Quentin Stafford-Fraser +version_added: 1.99 +notes: + - If you are I(deleting) domains by using C(state=absent), then note that if you specify subdomains, just those particular subdomains will be deleted. If you don't specify subdomains, the domain will be deleted. + - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." + - See `the webfaction API `_ for more info. + +options: + + name: + description: + - The name of the domain + required: true + default: null + + state: + description: + - Whether the domain should exist + required: false + choices: ['present', 'absent'] + default: "present" + + subdomains: + description: + - Any subdomains to create. + required: false + default: null + + login_name: + description: + - The webfaction account to use + required: true + + login_password: + description: + - The webfaction password to use + required: true +''' + +import socket +import xmlrpclib +from ansible.module_utils.basic import * + +webfaction = xmlrpclib.ServerProxy('https://api.webfaction.com/') + +def main(): + + module = AnsibleModule( + argument_spec = dict( + name = dict(required=True, default=None), + state = dict(required=False, default='present'), + subdomains = dict(required=False, default=[]), + login_name = dict(required=True), + login_password = dict(required=True), + ), + supports_check_mode=True + ) + domain_name = module.params['name'] + domain_state = module.params['state'] + domain_subdomains = module.params['subdomains'] + + session_id, account = webfaction.login( + module.params['login_name'], + module.params['login_password'] + ) + + domain_list = webfaction.list_domains(session_id) + domain_map = dict([(i['domain'], i) for i in domain_list]) + existing_domain = domain_map.get(domain_name) + + result = {} + + # Here's where the real stuff happens + + if domain_state == 'present': + + # Does an app with this name already exist? + if existing_domain: + + if set(existing_domain['subdomains']) >= set(domain_subdomains): + # If it exists with the right subdomains, we don't change anything. + module.exit_json( + changed = False, + ) + + positional_args = [session_id, domain_name] + domain_subdomains + + if not module.check_mode: + # If this isn't a dry run, create the app + # print positional_args + result.update( + webfaction.create_domain( + *positional_args + ) + ) + + elif domain_state == 'absent': + + # If the app's already not there, nothing changed. + if not existing_domain: + module.exit_json( + changed = False, + ) + + positional_args = [session_id, domain_name] + domain_subdomains + + if not module.check_mode: + # If this isn't a dry run, delete the app + result.update( + webfaction.delete_domain(*positional_args) + ) + + else: + module.fail_json(msg="Unknown state specified: {}".format(domain_state)) + + module.exit_json( + changed = True, + result = result + ) + +# The conventional ending +main() + diff --git a/cloud/webfaction/webfaction_mailbox.py b/cloud/webfaction/webfaction_mailbox.py new file mode 100644 index 00000000000..3ac848d6a94 --- /dev/null +++ b/cloud/webfaction/webfaction_mailbox.py @@ -0,0 +1,112 @@ +#! /usr/bin/python +# Create webfaction mailbox using Ansible and the Webfaction API +# +# Quentin Stafford-Fraser and Andy Baker 2015 + +DOCUMENTATION = ''' +--- +module: webfaction_mailbox +short_description: Add or remove mailboxes on Webfaction +description: + - Add or remove mailboxes on a Webfaction account. Further documentation at http://github.com/quentinsf/ansible-webfaction. +author: Quentin Stafford-Fraser +version_added: 1.99 +notes: + - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." + - See `the webfaction API `_ for more info. +options: + + mailbox_name: + description: + - The name of the mailbox + required: true + default: null + + mailbox_password: + description: + - The password for the mailbox + required: true + default: null + + state: + description: + - Whether the mailbox should exist + required: false + choices: ['present', 'absent'] + default: "present" + + login_name: + description: + - The webfaction account to use + required: true + + login_password: + description: + - The webfaction password to use + required: true +''' + +import socket +import xmlrpclib +from ansible.module_utils.basic import * + +webfaction = xmlrpclib.ServerProxy('https://api.webfaction.com/') + +def main(): + + module = AnsibleModule( + argument_spec=dict( + mailbox_name=dict(required=True, default=None), + mailbox_password=dict(required=True), + state=dict(required=False, default='present'), + login_name=dict(required=True), + login_password=dict(required=True), + ), + supports_check_mode=True + ) + + mailbox_name = module.params['mailbox_name'] + site_state = module.params['state'] + + session_id, account = webfaction.login( + module.params['login_name'], + module.params['login_password'] + ) + + mailbox_list = webfaction.list_mailboxes(session_id) + existing_mailbox = mailbox_name in mailbox_list + + result = {} + + # Here's where the real stuff happens + + if site_state == 'present': + + # Does a mailbox with this name already exist? + if existing_mailbox: + module.exit_json(changed=False,) + + positional_args = [session_id, mailbox_name] + + if not module.check_mode: + # If this isn't a dry run, create the mailbox + result.update(webfaction.create_mailbox(*positional_args)) + + elif site_state == 'absent': + + # If the mailbox is already not there, nothing changed. + if not existing_mailbox: + module.exit_json(changed=False) + + if not module.check_mode: + # If this isn't a dry run, delete the mailbox + result.update(webfaction.delete_mailbox(session_id, mailbox_name)) + + else: + module.fail_json(msg="Unknown state specified: {}".format(site_state)) + + module.exit_json(changed=True, result=result) + +# The conventional ending +main() + diff --git a/cloud/webfaction/webfaction_site.py b/cloud/webfaction/webfaction_site.py new file mode 100644 index 00000000000..5db89355966 --- /dev/null +++ b/cloud/webfaction/webfaction_site.py @@ -0,0 +1,189 @@ +#! /usr/bin/python +# Create Webfaction website using Ansible and the Webfaction API +# +# Quentin Stafford-Fraser 2015 + +DOCUMENTATION = ''' +--- +module: webfaction_site +short_description: Add or remove a website on a Webfaction host +description: + - Add or remove a website on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. +author: Quentin Stafford-Fraser +version_added: 1.99 +notes: + - Sadly, you I(do) need to know your webfaction hostname for the C(host) parameter. But at least, unlike the API, you don't need to know the IP address - you can use a DNS name. + - If a site of the same name exists in the account but on a different host, the operation will exit. + - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." + - See `the webfaction API `_ for more info. + +options: + + name: + description: + - The name of the website + required: true + default: null + + state: + description: + - Whether the website should exist + required: false + choices: ['present', 'absent'] + default: "present" + + host: + description: + - The webfaction host on which the site should be created. + required: true + + https: + description: + - Whether or not to use HTTPS + required: false + choices: BOOLEANS + default: 'false' + + site_apps: + description: + - A mapping of URLs to apps + required: false + + subdomains: + description: + - A list of subdomains associated with this site. + required: false + default: null + + login_name: + description: + - The webfaction account to use + required: true + + login_password: + description: + - The webfaction password to use + required: true +''' + +EXAMPLES = ''' + - name: create website + webfaction_site: + name: testsite1 + state: present + host: myhost.webfaction.com + subdomains: + - 'testsite1.my_domain.org' + site_apps: + - ['testapp1', '/'] + https: no + login_name: "{{webfaction_user}}" + login_password: "{{webfaction_passwd}}" +''' + +import socket +import xmlrpclib +from ansible.module_utils.basic import * + +webfaction = xmlrpclib.ServerProxy('https://api.webfaction.com/') + +def main(): + + module = AnsibleModule( + argument_spec = dict( + name = dict(required=True, default=None), + state = dict(required=False, default='present'), + # You can specify an IP address or hostname. + host = dict(required=True, default=None), + https = dict(required=False, choices=BOOLEANS, default='false'), + subdomains = dict(required=False, default=[]), + site_apps = dict(required=False, default=[]), + login_name = dict(required=True), + login_password = dict(required=True), + ), + supports_check_mode=True + ) + site_name = module.params['name'] + site_state = module.params['state'] + site_host = module.params['host'] + site_ip = socket.gethostbyname(site_host) + + session_id, account = webfaction.login( + module.params['login_name'], + module.params['login_password'] + ) + + site_list = webfaction.list_websites(session_id) + site_map = dict([(i['name'], i) for i in site_list]) + existing_site = site_map.get(site_name) + + result = {} + + # Here's where the real stuff happens + + if site_state == 'present': + + # Does a site with this name already exist? + if existing_site: + + # If yes, but it's on a different IP address, then fail. + # If we wanted to allow relocation, we could add a 'relocate=true' option + # which would get the existing IP address, delete the site there, and create it + # at the new address. A bit dangerous, perhaps, so for now we'll require manual + # deletion if it's on another host. + + if existing_site['ip'] != site_ip: + module.fail_json(msg="Website already exists with a different IP address. Please fix by hand.") + + # If it's on this host and the key parameters are the same, nothing needs to be done. + + if (existing_site['https'] == module.boolean(module.params['https'])) and \ + (set(existing_site['subdomains']) == set(module.params['subdomains'])) and \ + (dict(existing_site['website_apps']) == dict(module.params['site_apps'])): + module.exit_json( + changed = False + ) + + positional_args = [ + session_id, site_name, site_ip, + module.boolean(module.params['https']), + module.params['subdomains'], + ] + for a in module.params['site_apps']: + positional_args.append( (a[0], a[1]) ) + + if not module.check_mode: + # If this isn't a dry run, create or modify the site + result.update( + webfaction.create_website( + *positional_args + ) if not existing_site else webfaction.update_website ( + *positional_args + ) + ) + + elif site_state == 'absent': + + # If the site's already not there, nothing changed. + if not existing_site: + module.exit_json( + changed = False, + ) + + if not module.check_mode: + # If this isn't a dry run, delete the site + result.update( + webfaction.delete_website(session_id, site_name, site_ip) + ) + + else: + module.fail_json(msg="Unknown state specified: {}".format(site_state)) + + module.exit_json( + changed = True, + result = result + ) + +# The conventional ending +main() + From d524d450aef07cc3a828e5f1887dda8049855d1a Mon Sep 17 00:00:00 2001 From: Quentin Stafford-Fraser Date: Sun, 3 May 2015 23:48:51 +0100 Subject: [PATCH 0156/2522] Tidying of webfaction modules --- cloud/webfaction/webfaction_app.py | 12 +++++------- cloud/webfaction/webfaction_db.py | 10 ++++------ cloud/webfaction/webfaction_domain.py | 8 +++----- cloud/webfaction/webfaction_mailbox.py | 9 ++++----- cloud/webfaction/webfaction_site.py | 14 +++++++------- 5 files changed, 23 insertions(+), 30 deletions(-) diff --git a/cloud/webfaction/webfaction_app.py b/cloud/webfaction/webfaction_app.py index b1ddcd5a9c0..08a0205eb87 100644 --- a/cloud/webfaction/webfaction_app.py +++ b/cloud/webfaction/webfaction_app.py @@ -13,7 +13,7 @@ description: - Add or remove applications on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. author: Quentin Stafford-Fraser -version_added: 1.99 +version_added: 2.0 notes: - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." - See `the webfaction API `_ for more info. @@ -23,7 +23,6 @@ description: - The name of the application required: true - default: null state: description: @@ -65,7 +64,6 @@ ''' import xmlrpclib -from ansible.module_utils.basic import * webfaction = xmlrpclib.ServerProxy('https://api.webfaction.com/') @@ -73,12 +71,12 @@ def main(): module = AnsibleModule( argument_spec = dict( - name = dict(required=True, default=None), + name = dict(required=True), state = dict(required=False, default='present'), type = dict(required=True), - autostart = dict(required=False, choices=BOOLEANS, default='false'), + autostart = dict(required=False, choices=BOOLEANS, default=False), extra_info = dict(required=False, default=""), - port_open = dict(required=False, default="false"), + port_open = dict(required=False, choices=BOOLEANS, default=False), login_name = dict(required=True), login_password = dict(required=True), ), @@ -148,6 +146,6 @@ def main(): result = result ) -# The conventional ending +from ansible.module_utils.basic import * main() diff --git a/cloud/webfaction/webfaction_db.py b/cloud/webfaction/webfaction_db.py index 7205a084ef2..479540abc5c 100644 --- a/cloud/webfaction/webfaction_db.py +++ b/cloud/webfaction/webfaction_db.py @@ -10,7 +10,7 @@ description: - Add or remove a database on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. author: Quentin Stafford-Fraser -version_added: 1.99 +version_added: 2.0 notes: - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." - See `the webfaction API `_ for more info. @@ -20,7 +20,6 @@ description: - The name of the database required: true - default: null state: description: @@ -61,7 +60,6 @@ import socket import xmlrpclib -from ansible.module_utils.basic import * webfaction = xmlrpclib.ServerProxy('https://api.webfaction.com/') @@ -69,10 +67,10 @@ def main(): module = AnsibleModule( argument_spec = dict( - name = dict(required=True, default=None), + name = dict(required=True), state = dict(required=False, default='present'), # You can specify an IP address or hostname. - type = dict(required=True, default=None), + type = dict(required=True), password = dict(required=False, default=None), login_name = dict(required=True), login_password = dict(required=True), @@ -142,6 +140,6 @@ def main(): result = result ) -# The conventional ending +from ansible.module_utils.basic import * main() diff --git a/cloud/webfaction/webfaction_domain.py b/cloud/webfaction/webfaction_domain.py index 2f3c8542754..a9e2b7dd9bb 100644 --- a/cloud/webfaction/webfaction_domain.py +++ b/cloud/webfaction/webfaction_domain.py @@ -10,7 +10,7 @@ description: - Add or remove domains or subdomains on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. author: Quentin Stafford-Fraser -version_added: 1.99 +version_added: 2.0 notes: - If you are I(deleting) domains by using C(state=absent), then note that if you specify subdomains, just those particular subdomains will be deleted. If you don't specify subdomains, the domain will be deleted. - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." @@ -22,7 +22,6 @@ description: - The name of the domain required: true - default: null state: description: @@ -50,7 +49,6 @@ import socket import xmlrpclib -from ansible.module_utils.basic import * webfaction = xmlrpclib.ServerProxy('https://api.webfaction.com/') @@ -58,7 +56,7 @@ def main(): module = AnsibleModule( argument_spec = dict( - name = dict(required=True, default=None), + name = dict(required=True), state = dict(required=False, default='present'), subdomains = dict(required=False, default=[]), login_name = dict(required=True), @@ -129,6 +127,6 @@ def main(): result = result ) -# The conventional ending +from ansible.module_utils.basic import * main() diff --git a/cloud/webfaction/webfaction_mailbox.py b/cloud/webfaction/webfaction_mailbox.py index 3ac848d6a94..1ba571a1dd1 100644 --- a/cloud/webfaction/webfaction_mailbox.py +++ b/cloud/webfaction/webfaction_mailbox.py @@ -10,7 +10,7 @@ description: - Add or remove mailboxes on a Webfaction account. Further documentation at http://github.com/quentinsf/ansible-webfaction. author: Quentin Stafford-Fraser -version_added: 1.99 +version_added: 2.0 notes: - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." - See `the webfaction API `_ for more info. @@ -20,7 +20,6 @@ description: - The name of the mailbox required: true - default: null mailbox_password: description: @@ -48,7 +47,6 @@ import socket import xmlrpclib -from ansible.module_utils.basic import * webfaction = xmlrpclib.ServerProxy('https://api.webfaction.com/') @@ -56,7 +54,7 @@ def main(): module = AnsibleModule( argument_spec=dict( - mailbox_name=dict(required=True, default=None), + mailbox_name=dict(required=True), mailbox_password=dict(required=True), state=dict(required=False, default='present'), login_name=dict(required=True), @@ -107,6 +105,7 @@ def main(): module.exit_json(changed=True, result=result) -# The conventional ending + +from ansible.module_utils.basic import * main() diff --git a/cloud/webfaction/webfaction_site.py b/cloud/webfaction/webfaction_site.py index 5db89355966..575e6eec996 100644 --- a/cloud/webfaction/webfaction_site.py +++ b/cloud/webfaction/webfaction_site.py @@ -10,7 +10,7 @@ description: - Add or remove a website on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. author: Quentin Stafford-Fraser -version_added: 1.99 +version_added: 2.0 notes: - Sadly, you I(do) need to know your webfaction hostname for the C(host) parameter. But at least, unlike the API, you don't need to know the IP address - you can use a DNS name. - If a site of the same name exists in the account but on a different host, the operation will exit. @@ -23,7 +23,6 @@ description: - The name of the website required: true - default: null state: description: @@ -83,7 +82,6 @@ import socket import xmlrpclib -from ansible.module_utils.basic import * webfaction = xmlrpclib.ServerProxy('https://api.webfaction.com/') @@ -91,11 +89,11 @@ def main(): module = AnsibleModule( argument_spec = dict( - name = dict(required=True, default=None), + name = dict(required=True), state = dict(required=False, default='present'), # You can specify an IP address or hostname. - host = dict(required=True, default=None), - https = dict(required=False, choices=BOOLEANS, default='false'), + host = dict(required=True), + https = dict(required=False, choices=BOOLEANS, default=False), subdomains = dict(required=False, default=[]), site_apps = dict(required=False, default=[]), login_name = dict(required=True), @@ -184,6 +182,8 @@ def main(): result = result ) -# The conventional ending + + +from ansible.module_utils.basic import * main() From 4a2e5e4a653c783a10535b173e5293e840744364 Mon Sep 17 00:00:00 2001 From: fdupoux Date: Sat, 9 May 2015 14:06:58 +0100 Subject: [PATCH 0157/2522] Suppress prompts from lvcreate using --yes when LVM supports this option --- system/lvol.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/system/lvol.py b/system/lvol.py index 7ec5ec5cd64..43511ae7b7a 100644 --- a/system/lvol.py +++ b/system/lvol.py @@ -85,6 +85,8 @@ decimal_point = re.compile(r"(\.|,)") +def mkversion(major, minor, patch): + return (1000 * 1000 * int(major)) + (1000 * int(minor)) + int(patch) def parse_lvs(data): lvs = [] @@ -97,6 +99,17 @@ def parse_lvs(data): return lvs +def get_lvm_version(module): + ver_cmd = module.get_bin_path("lvm", required=True) + rc, out, err = module.run_command("%s version" % (ver_cmd)) + if rc != 0: + return None + m = re.search("LVM version:\s+(\d+)\.(\d+)\.(\d+).*(\d{4}-\d{2}-\d{2})", out) + if not m: + return None + return mkversion(m.group(1), m.group(2), m.group(3)) + + def main(): module = AnsibleModule( argument_spec=dict( @@ -109,6 +122,13 @@ def main(): supports_check_mode=True, ) + # Determine if the "--yes" option should be used + version_found = get_lvm_version(module) + if version_found == None: + module.fail_json(msg="Failed to get LVM version number") + version_yesopt = mkversion(2, 2, 99) # First LVM with the "--yes" option + yesopt = "--yes" if version_found >= version_yesopt else "" + vg = module.params['vg'] lv = module.params['lv'] size = module.params['size'] @@ -189,7 +209,7 @@ def main(): changed = True else: lvcreate_cmd = module.get_bin_path("lvcreate", required=True) - rc, _, err = module.run_command("%s -n %s -%s %s%s %s" % (lvcreate_cmd, lv, size_opt, size, size_unit, vg)) + rc, _, err = module.run_command("%s %s -n %s -%s %s%s %s" % (lvcreate_cmd, yesopt, lv, size_opt, size, size_unit, vg)) if rc == 0: changed = True else: From 70983f397698af1b90164ddd8940edd6c38b79c6 Mon Sep 17 00:00:00 2001 From: Quentin Stafford-Fraser Date: Sun, 10 May 2015 20:40:50 +0100 Subject: [PATCH 0158/2522] Documentation version_added numbers are strings. --- cloud/webfaction/webfaction_app.py | 2 +- cloud/webfaction/webfaction_db.py | 2 +- cloud/webfaction/webfaction_domain.py | 2 +- cloud/webfaction/webfaction_mailbox.py | 2 +- cloud/webfaction/webfaction_site.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cloud/webfaction/webfaction_app.py b/cloud/webfaction/webfaction_app.py index 08a0205eb87..dec5f8e5d5e 100644 --- a/cloud/webfaction/webfaction_app.py +++ b/cloud/webfaction/webfaction_app.py @@ -13,7 +13,7 @@ description: - Add or remove applications on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. author: Quentin Stafford-Fraser -version_added: 2.0 +version_added: "2.0" notes: - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." - See `the webfaction API `_ for more info. diff --git a/cloud/webfaction/webfaction_db.py b/cloud/webfaction/webfaction_db.py index 479540abc5c..fc522439591 100644 --- a/cloud/webfaction/webfaction_db.py +++ b/cloud/webfaction/webfaction_db.py @@ -10,7 +10,7 @@ description: - Add or remove a database on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. author: Quentin Stafford-Fraser -version_added: 2.0 +version_added: "2.0" notes: - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." - See `the webfaction API `_ for more info. diff --git a/cloud/webfaction/webfaction_domain.py b/cloud/webfaction/webfaction_domain.py index a9e2b7dd9bb..31339014e6c 100644 --- a/cloud/webfaction/webfaction_domain.py +++ b/cloud/webfaction/webfaction_domain.py @@ -10,7 +10,7 @@ description: - Add or remove domains or subdomains on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. author: Quentin Stafford-Fraser -version_added: 2.0 +version_added: "2.0" notes: - If you are I(deleting) domains by using C(state=absent), then note that if you specify subdomains, just those particular subdomains will be deleted. If you don't specify subdomains, the domain will be deleted. - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." diff --git a/cloud/webfaction/webfaction_mailbox.py b/cloud/webfaction/webfaction_mailbox.py index 1ba571a1dd1..5eb82df3eaa 100644 --- a/cloud/webfaction/webfaction_mailbox.py +++ b/cloud/webfaction/webfaction_mailbox.py @@ -10,7 +10,7 @@ description: - Add or remove mailboxes on a Webfaction account. Further documentation at http://github.com/quentinsf/ansible-webfaction. author: Quentin Stafford-Fraser -version_added: 2.0 +version_added: "2.0" notes: - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." - See `the webfaction API `_ for more info. diff --git a/cloud/webfaction/webfaction_site.py b/cloud/webfaction/webfaction_site.py index 575e6eec996..c981a21fc2b 100644 --- a/cloud/webfaction/webfaction_site.py +++ b/cloud/webfaction/webfaction_site.py @@ -10,7 +10,7 @@ description: - Add or remove a website on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. author: Quentin Stafford-Fraser -version_added: 2.0 +version_added: "2.0" notes: - Sadly, you I(do) need to know your webfaction hostname for the C(host) parameter. But at least, unlike the API, you don't need to know the IP address - you can use a DNS name. - If a site of the same name exists in the account but on a different host, the operation will exit. From 1a19b96464396ac77444b152c59a8c37372d8a7e Mon Sep 17 00:00:00 2001 From: Quentin Stafford-Fraser Date: Sun, 10 May 2015 20:47:31 +0100 Subject: [PATCH 0159/2522] Available choices for 'state' explicitly listed. --- cloud/webfaction/webfaction_app.py | 2 +- cloud/webfaction/webfaction_db.py | 2 +- cloud/webfaction/webfaction_domain.py | 2 +- cloud/webfaction/webfaction_mailbox.py | 2 +- cloud/webfaction/webfaction_site.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cloud/webfaction/webfaction_app.py b/cloud/webfaction/webfaction_app.py index dec5f8e5d5e..05b31f55a4a 100644 --- a/cloud/webfaction/webfaction_app.py +++ b/cloud/webfaction/webfaction_app.py @@ -72,7 +72,7 @@ def main(): module = AnsibleModule( argument_spec = dict( name = dict(required=True), - state = dict(required=False, default='present'), + state = dict(required=False, choices=['present', 'absent'], default='present'), type = dict(required=True), autostart = dict(required=False, choices=BOOLEANS, default=False), extra_info = dict(required=False, default=""), diff --git a/cloud/webfaction/webfaction_db.py b/cloud/webfaction/webfaction_db.py index fc522439591..784477c5409 100644 --- a/cloud/webfaction/webfaction_db.py +++ b/cloud/webfaction/webfaction_db.py @@ -68,7 +68,7 @@ def main(): module = AnsibleModule( argument_spec = dict( name = dict(required=True), - state = dict(required=False, default='present'), + state = dict(required=False, choices=['present', 'absent'], default='present'), # You can specify an IP address or hostname. type = dict(required=True), password = dict(required=False, default=None), diff --git a/cloud/webfaction/webfaction_domain.py b/cloud/webfaction/webfaction_domain.py index 31339014e6c..8548c4fba37 100644 --- a/cloud/webfaction/webfaction_domain.py +++ b/cloud/webfaction/webfaction_domain.py @@ -57,7 +57,7 @@ def main(): module = AnsibleModule( argument_spec = dict( name = dict(required=True), - state = dict(required=False, default='present'), + state = dict(required=False, choices=['present', 'absent'], default='present'), subdomains = dict(required=False, default=[]), login_name = dict(required=True), login_password = dict(required=True), diff --git a/cloud/webfaction/webfaction_mailbox.py b/cloud/webfaction/webfaction_mailbox.py index 5eb82df3eaa..fee5700e50e 100644 --- a/cloud/webfaction/webfaction_mailbox.py +++ b/cloud/webfaction/webfaction_mailbox.py @@ -56,7 +56,7 @@ def main(): argument_spec=dict( mailbox_name=dict(required=True), mailbox_password=dict(required=True), - state=dict(required=False, default='present'), + state=dict(required=False, choices=['present', 'absent'], default='present'), login_name=dict(required=True), login_password=dict(required=True), ), diff --git a/cloud/webfaction/webfaction_site.py b/cloud/webfaction/webfaction_site.py index c981a21fc2b..a5be4f5407b 100644 --- a/cloud/webfaction/webfaction_site.py +++ b/cloud/webfaction/webfaction_site.py @@ -90,7 +90,7 @@ def main(): module = AnsibleModule( argument_spec = dict( name = dict(required=True), - state = dict(required=False, default='present'), + state = dict(required=False, choices=['present', 'absent'], default='present'), # You can specify an IP address or hostname. host = dict(required=True), https = dict(required=False, choices=BOOLEANS, default=False), From 25acd524e789211ce63308da6c99a32220f21303 Mon Sep 17 00:00:00 2001 From: Quentin Stafford-Fraser Date: Sun, 10 May 2015 22:07:49 +0100 Subject: [PATCH 0160/2522] Add examples. --- cloud/webfaction/webfaction_app.py | 10 ++++++++++ cloud/webfaction/webfaction_domain.py | 20 ++++++++++++++++++++ cloud/webfaction/webfaction_mailbox.py | 10 ++++++++++ 3 files changed, 40 insertions(+) diff --git a/cloud/webfaction/webfaction_app.py b/cloud/webfaction/webfaction_app.py index 05b31f55a4a..20e94a7b5f6 100644 --- a/cloud/webfaction/webfaction_app.py +++ b/cloud/webfaction/webfaction_app.py @@ -63,6 +63,16 @@ required: true ''' +EXAMPLES = ''' + - name: Create a test app + webfaction_app: + name="my_wsgi_app1" + state=present + type=mod_wsgi35-python27 + login_name={{webfaction_user}} + login_password={{webfaction_passwd}} +''' + import xmlrpclib webfaction = xmlrpclib.ServerProxy('https://api.webfaction.com/') diff --git a/cloud/webfaction/webfaction_domain.py b/cloud/webfaction/webfaction_domain.py index 8548c4fba37..c99a0f23f6d 100644 --- a/cloud/webfaction/webfaction_domain.py +++ b/cloud/webfaction/webfaction_domain.py @@ -47,6 +47,26 @@ required: true ''' +EXAMPLES = ''' + - name: Create a test domain + webfaction_domain: + name: mydomain.com + state: present + subdomains: + - www + - blog + login_name: "{{webfaction_user}}" + login_password: "{{webfaction_passwd}}" + + - name: Delete test domain and any subdomains + webfaction_domain: + name: mydomain.com + state: absent + login_name: "{{webfaction_user}}" + login_password: "{{webfaction_passwd}}" + +''' + import socket import xmlrpclib diff --git a/cloud/webfaction/webfaction_mailbox.py b/cloud/webfaction/webfaction_mailbox.py index fee5700e50e..87ca1fd1a26 100644 --- a/cloud/webfaction/webfaction_mailbox.py +++ b/cloud/webfaction/webfaction_mailbox.py @@ -45,6 +45,16 @@ required: true ''' +EXAMPLES = ''' + - name: Create a mailbox + webfaction_mailbox: + mailbox_name="mybox" + mailbox_password="myboxpw" + state=present + login_name={{webfaction_user}} + login_password={{webfaction_passwd}} +''' + import socket import xmlrpclib From 2bcb0d4c080abf94bd7e06b61f5ea5a38a7da58a Mon Sep 17 00:00:00 2001 From: Lorenzo Luconi Trombacchi Date: Tue, 12 May 2015 10:56:22 +0200 Subject: [PATCH 0161/2522] added lower function for statuses --- monitoring/monit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/monit.py b/monitoring/monit.py index e87d8edca5a..4ad202993fe 100644 --- a/monitoring/monit.py +++ b/monitoring/monit.py @@ -77,7 +77,7 @@ def status(): # Process 'name' Running - restart pending parts = line.split() if len(parts) > 2 and parts[0].lower() == 'process' and parts[1] == "'%s'" % name: - return ' '.join(parts[2:]) + return ' '.join(parts[2:]).lower() else: return '' From 16db10958b0a163118da3fea544881846162a6c6 Mon Sep 17 00:00:00 2001 From: Lorenzo Luconi Trombacchi Date: Tue, 12 May 2015 10:58:47 +0200 Subject: [PATCH 0162/2522] fix a problem with status detection after unmonitor command --- monitoring/monit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/monit.py b/monitoring/monit.py index 4ad202993fe..6afb95d093d 100644 --- a/monitoring/monit.py +++ b/monitoring/monit.py @@ -119,7 +119,7 @@ def run_command(command): if module.check_mode: module.exit_json(changed=True) status = run_command('unmonitor') - if status in ['not monitored']: + if status in ['not monitored'] or 'unmonitor pending' in status: module.exit_json(changed=True, name=name, state=state) module.fail_json(msg='%s process not unmonitored' % name, status=status) From 51b11fd1af70f335282bfa30450520a815965981 Mon Sep 17 00:00:00 2001 From: Lorenzo Luconi Trombacchi Date: Tue, 12 May 2015 11:07:52 +0200 Subject: [PATCH 0163/2522] status function was called twice --- monitoring/monit.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/monitoring/monit.py b/monitoring/monit.py index 6afb95d093d..6410ce815e8 100644 --- a/monitoring/monit.py +++ b/monitoring/monit.py @@ -86,7 +86,8 @@ def run_command(command): module.run_command('%s %s %s' % (MONIT, command, name), check_rc=True) return status() - present = status() != '' + process_status = status() + present = process_status != '' if not present and not state == 'present': module.fail_json(msg='%s process not presently configured with monit' % name, name=name, state=state) @@ -102,7 +103,7 @@ def run_command(command): module.exit_json(changed=True, name=name, state=state) module.exit_json(changed=False, name=name, state=state) - running = 'running' in status() + running = 'running' in process_status if running and state in ['started', 'monitored']: module.exit_json(changed=False, name=name, state=state) From 3b44082dd67de15d6c27cfba1836f04e83914797 Mon Sep 17 00:00:00 2001 From: Chris Long Date: Tue, 12 May 2015 22:10:53 +1000 Subject: [PATCH 0164/2522] Initial commit of nmcli: NetworkManager module. Currently supports: Create, modify, remove of - team, team-slave, bond, bond-slave, ethernet TODO: vlan, bridge, wireless related connections. --- network/nmcli.py | 1089 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1089 insertions(+) create mode 100644 network/nmcli.py diff --git a/network/nmcli.py b/network/nmcli.py new file mode 100644 index 00000000000..0532058da3b --- /dev/null +++ b/network/nmcli.py @@ -0,0 +1,1089 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Chris Long +# +# This file is a module for Ansible that interacts with Network Manager +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + + +DOCUMENTATION=''' +--- +module: nmcli +author: Chris Long +short_description: Manage Networking +requirements: [ nmcli, dbus ] +description: + - Manage the network devices. Create, modify, and manage, ethernet, teams, bonds, vlans etc. +options: + state: + required: True + default: "present" + choices: [ present, absent ] + description: + - Whether the device should exist or not, taking action if the state is different from what is stated. + enabled: + required: False + default: "yes" + choices: [ "yes", "no" ] + description: + - Whether the service should start on boot. B(At least one of state and enabled are required.) + - Whether the connection profile can be automatically activated ( default: yes) + action: + required: False + default: None + choices: [ add, modify, show, up, down ] + description: + - Set to 'add' if you want to add a connection. + - Set to 'modify' if you want to modify a connection. Modify one or more properties in the connection profile. + - Set to 'delete' if you want to delete a connection. Delete a configured connection. The connection to be deleted is identified by its name 'cfname'. + - Set to 'show' if you want to show a connection. Will show all devices unless 'cfname' is set. + - Set to 'up' if you want to bring a connection up. Requires 'cfname' to be set. + - Set to 'down' if you want to bring a connection down. Requires 'cfname' to be set. + cname: + required: True + default: None + description: + - Where CNAME will be the name used to call the connection. when not provided a default name is generated: [-][-] + ifname: + required: False + default: cname + description: + - Where INAME will be the what we call the interface name. Required with 'up', 'down' modifiers. + - interface to bind the connection to. The connection will only be applicable to this interface name. + - A special value of "*" can be used for interface-independent connections. + - The ifname argument is mandatory for all connection types except bond, team, bridge and vlan. + type: + required: False + choices: [ ethernet, team, team-slave, bond, bond-slave, bridge, vlan ] + description: + - This is the type of device or network connection that you wish to create. + mode: + required: False + choices: [ "balance-rr", "active-backup", "balance-xor", "broadcast", "802.3ad", "balance-tlb", "balance-alb" ] + default: None + description: + - This is the type of device or network connection that you wish to create for a bond, team or bridge. (NetworkManager default: balance-rr) + master: + required: False + default: None + description: + - master ] STP forwarding delay, in seconds (NetworkManager default: 15) + hellotime: + required: False + default: None + description: + - This is only used with bridge - [hello-time <1-10>] STP hello time, in seconds (NetworkManager default: 2) + maxage: + required: False + default: None + description: + - This is only used with bridge - [max-age <6-42>] STP maximum message age, in seconds (NetworkManager default: 20) + ageingtime: + required: False + default: None + description: + - This is only used with bridge - [ageing-time <0-1000000>] the Ethernet MAC address aging time, in seconds (NetworkManager default: 300) + mac: + required: False + default: None + description: + - This is only used with bridge - MAC address of the bridge (note: this requires a recent kernel feature, originally introduced in 3.15 upstream kernel) + slavepriority: + required: False + default: None + description: + - This is only used with 'bridge-slave' - [<0-63>] - STP priority of this slave (default: 32) + path_cost: + required: False + default: None + description: + - This is only used with 'bridge-slave' - [<1-65535>] - STP port cost for destinations via this slave (NetworkManager default: 100) + hairpin: + required: False + default: None + description: + - This is only used with 'bridge-slave' - 'hairpin mode' for the slave, which allows frames to be sent back out through the slave the frame was received on. (NetworkManager default: yes) + vlanid: + required: False + default: None + description: + - This is only used with VLAN - VLAN ID in range <0-4095> + vlandev: + required: False + default: None + description: + - This is only used with VLAN - parent device this VLAN is on, can use ifname + flags: + required: False + default: None + description: + - This is only used with VLAN - flags + ingress: + required: False + default: None + description: + - This is only used with VLAN - VLAN ingress priority mapping + egress: + required: False + default: None + description: + - This is only used with VLAN - VLAN egress priority mapping + +''' + +EXAMPLES=''' +The following examples are working examples that I have run in the field. I followed follow the structure: +``` +|_/inventory/cloud-hosts +| /group_vars/openstack-stage.yml +| /host_vars/controller-01.openstack.host.com +| /host_vars/controller-02.openstack.host.com +|_/playbook/library/nmcli.py +| /playbook-add.yml +| /playbook-del.yml +``` + +## inventory examples +### groups_vars +```yml +--- +#devops_os_define_network +storage_gw: "192.168.0.254" +external_gw: "10.10.0.254" +tenant_gw: "172.100.0.254" + +#Team vars +nmcli_team: + - {cname: 'tenant', ip4: "{{tenant_ip}}", gw4: "{{tenant_gw}}"} + - {cname: 'external', ip4: "{{external_ip}}", gw4: "{{external_gw}}"} + - {cname: 'storage', ip4: "{{storage_ip}}", gw4: "{{storage_gw}}"} +nmcli_team_slave: + - {cname: 'em1', ifname: 'em1', master: 'tenant'} + - {cname: 'em2', ifname: 'em2', master: 'tenant'} + - {cname: 'p2p1', ifname: 'p2p1', master: 'storage'} + - {cname: 'p2p2', ifname: 'p2p2', master: 'external'} + +#bond vars +nmcli_bond: + - {cname: 'tenant', ip4: "{{tenant_ip}}", gw4: '', mode: 'balance-rr'} + - {cname: 'external', ip4: "{{external_ip}}", gw4: '', mode: 'balance-rr'} + - {cname: 'storage', ip4: "{{storage_ip}}", gw4: "{{storage_gw}}", mode: 'balance-rr'} +nmcli_bond_slave: + - {cname: 'em1', ifname: 'em1', master: 'tenant'} + - {cname: 'em2', ifname: 'em2', master: 'tenant'} + - {cname: 'p2p1', ifname: 'p2p1', master: 'storage'} + - {cname: 'p2p2', ifname: 'p2p2', master: 'external'} + +#ethernet vars +nmcli_ethernet: + - {cname: 'em1', ifname: 'em1', ip4: "{{tenant_ip}}", gw4: "{{tenant_gw}}"} + - {cname: 'em2', ifname: 'em2', ip4: "{{tenant_ip1}}", gw4: "{{tenant_gw}}"} + - {cname: 'p2p1', ifname: 'p2p1', ip4: "{{storage_ip}}", gw4: "{{storage_gw}}"} + - {cname: 'p2p2', ifname: 'p2p2', ip4: "{{external_ip}}", gw4: "{{external_gw}}"} +``` + +### host_vars +```yml +--- +storage_ip: "192.168.160.21/23" +external_ip: "10.10.152.21/21" +tenant_ip: "192.168.200.21/23" +``` + + + +## playbook-add.yml example + +```yml +--- +- hosts: openstack-stage + remote_user: root + tasks: + +- name: install needed network manager libs + yum: name={{ item }} state=installed + with_items: + - libnm-qt-devel.x86_64 + - nm-connection-editor.x86_64 + - libsemanage-python + - policycoreutils-python + +##### Working with all cloud nodes - Teaming + - name: try nmcli add team - cname only & ip4 gw4 + nmcli: type=team cname={{item.cname}} ip4={{item.ip4}} gw4={{item.gw4}} state=present + with_items: + - "{{nmcli_team}}" + + - name: try nmcli add teams-slave + nmcli: type=team-slave cname={{item.cname}} ifname={{item.ifname}} master={{item.master}} state=present + with_items: + - "{{nmcli_team_slave}}" + +###### Working with all cloud nodes - Bonding +# - name: try nmcli add bond - cname only & ip4 gw4 mode +# nmcli: type=bond cname={{item.cname}} ip4={{item.ip4}} gw4={{item.gw4}} mode={{item.mode}} state=present +# with_items: +# - "{{nmcli_bond}}" +# +# - name: try nmcli add bond-slave +# nmcli: type=bond-slave cname={{item.cname}} ifname={{item.ifname}} master={{item.master}} state=present +# with_items: +# - "{{nmcli_bond_slave}}" + +##### Working with all cloud nodes - Ethernet +# - name: nmcli add Ethernet - cname only & ip4 gw4 +# nmcli: type=ethernet cname={{item.cname}} ip4={{item.ip4}} gw4={{item.gw4}} state=present +# with_items: +# - "{{nmcli_ethernet}}" +``` + +## playbook-del.yml example + +```yml +--- +- hosts: openstack-stage + remote_user: root + tasks: + + - name: try nmcli del team - multiple + nmcli: cname={{item.cname}} state=absent + with_items: + - { cname: 'em1'} + - { cname: 'em2'} + - { cname: 'p1p1'} + - { cname: 'p1p2'} + - { cname: 'p2p1'} + - { cname: 'p2p2'} + - { cname: 'tenant'} + - { cname: 'storage'} + - { cname: 'external'} + - { cname: 'team-em1'} + - { cname: 'team-em2'} + - { cname: 'team-p1p1'} + - { cname: 'team-p1p2'} + - { cname: 'team-p2p1'} + - { cname: 'team-p2p2'} +``` +# To add an Ethernet connection with static IP configuration, issue a command as follows +- nmcli: cname=my-eth1 ifname=eth1 type=ethernet ip4=192.168.100.100/24 gw4=192.168.100.1 state=present + +# To add an Team connection with static IP configuration, issue a command as follows +- nmcli: cname=my-team1 ifname=my-team1 type=team ip4=192.168.100.100/24 gw4=192.168.100.1 state=present enabled=yes + +# Optionally, at the same time specify IPv6 addresses for the device as follows: +- nmcli: cname=my-eth1 ifname=eth1 type=ethernet ip4=192.168.100.100/24 gw4=192.168.100.1 ip6=abbe::cafe gw6=2001:db8::1 state=present + +# To add two IPv4 DNS server addresses: +-nmcli: cname=my-eth1 dns4=["8.8.8.8", "8.8.4.4"] state=present + +# To make a profile usable for all compatible Ethernet interfaces, issue a command as follows +- nmcli: ctype=ethernet name=my-eth1 ifname="*" state=present + +# To change the property of a setting e.g. MTU, issue a command as follows: +- nmcli: cname=my-eth1 mtu=9000 state=present + + Exit Status's: + - nmcli exits with status 0 if it succeeds, a value greater than 0 is + returned if an error occurs. + - 0 Success - indicates the operation succeeded + - 1 Unknown or unspecified error + - 2 Invalid user input, wrong nmcli invocation + - 3 Timeout expired (see --wait option) + - 4 Connection activation failed + - 5 Connection deactivation failed + - 6 Disconnecting device failed + - 7 Connection deletion failed + - 8 NetworkManager is not running + - 9 nmcli and NetworkManager versions mismatch + - 10 Connection, device, or access point does not exist. +''' +# import ansible.module_utils.basic +import os +import syslog +import sys +import dbus +from gi.repository import NetworkManager, NMClient + + +class Nmcli(object): + """ + This is the generic nmcli manipulation class that is subclassed based on platform. + A subclass may wish to override the following action methods:- + - create_connection() + - delete_connection() + - modify_connection() + - show_connection() + - up_connection() + - down_connection() + All subclasses MUST define platform and distribution (which may be None). + """ + + platform='Generic' + distribution=None + bus=dbus.SystemBus() + # The following is going to be used in dbus code + DEVTYPES={1: "Ethernet", + 2: "Wi-Fi", + 5: "Bluetooth", + 6: "OLPC", + 7: "WiMAX", + 8: "Modem", + 9: "InfiniBand", + 10: "Bond", + 11: "VLAN", + 12: "ADSL", + 13: "Bridge", + 14: "Generic", + 15: "Team" + } + STATES={0: "Unknown", + 10: "Unmanaged", + 20: "Unavailable", + 30: "Disconnected", + 40: "Prepare", + 50: "Config", + 60: "Need Auth", + 70: "IP Config", + 80: "IP Check", + 90: "Secondaries", + 100: "Activated", + 110: "Deactivating", + 120: "Failed" + } + + def __new__(cls, *args, **kwargs): + return load_platform_subclass(Nmcli, args, kwargs) + + def __init__(self, module): + self.module=module + self.state=module.params['state'] + self.enabled=module.params['enabled'] + self.action=module.params['action'] + self.cname=module.params['cname'] + self.master=module.params['master'] + self.autoconnect=module.params['autoconnect'] + self.ifname=module.params['ifname'] + self.type=module.params['type'] + self.ip4=module.params['ip4'] + self.gw4=module.params['gw4'] + self.dns4=module.params['dns4'] + self.ip6=module.params['ip6'] + self.gw6=module.params['gw6'] + self.dns6=module.params['dns6'] + self.mtu=module.params['mtu'] + self.stp=module.params['stp'] + self.priority=module.params['priority'] + self.mode=module.params['mode'] + self.miimon=module.params['miimon'] + self.downdelay=module.params['downdelay'] + self.updelay=module.params['updelay'] + self.arp_interval=module.params['arp_interval'] + self.arp_ip_target=module.params['arp_ip_target'] + self.slavepriority=module.params['slavepriority'] + self.forwarddelay=module.params['forwarddelay'] + self.hellotime=module.params['hellotime'] + self.maxage=module.params['maxage'] + self.ageingtime=module.params['ageingtime'] + self.mac=module.params['mac'] + self.vlanid=module.params['vlanid'] + self.vlandev=module.params['vlandev'] + self.flags=module.params['flags'] + self.ingress=module.params['ingress'] + self.egress=module.params['egress'] + # select whether we dump additional debug info through syslog + self.syslogging=True + + def execute_command(self, cmd, use_unsafe_shell=False, data=None): + if self.syslogging: + syslog.openlog('ansible-%s' % os.path.basename(__file__)) + syslog.syslog(syslog.LOG_NOTICE, 'Command %s' % '|'.join(cmd)) + + return self.module.run_command(cmd, use_unsafe_shell=use_unsafe_shell, data=data) + + def merge_secrets(self, proxy, config, setting_name): + try: + # returns a dict of dicts mapping name::setting, where setting is a dict + # mapping key::value. Each member of the 'setting' dict is a secret + secrets=proxy.GetSecrets(setting_name) + + # Copy the secrets into our connection config + for setting in secrets: + for key in secrets[setting]: + config[setting_name][key]=secrets[setting][key] + except Exception, e: + pass + + def dict_to_string(self, d): + # Try to trivially translate a dictionary's elements into nice string + # formatting. + dstr="" + for key in d: + val=d[key] + str_val="" + add_string=True + if type(val)==type(dbus.Array([])): + for elt in val: + if type(elt)==type(dbus.Byte(1)): + str_val+="%s " % int(elt) + elif type(elt)==type(dbus.String("")): + str_val+="%s" % elt + elif type(val)==type(dbus.Dictionary({})): + dstr+=self.dict_to_string(val) + add_string=False + else: + str_val=val + if add_string: + dstr+="%s: %s\n" % ( key, str_val) + return dstr + + def connection_to_string(self, config): + # dump a connection configuration to use in list_connection_info + setting_list=[] + for setting_name in config: + setting_list.append(self.dict_to_string(config[setting_name])) + return setting_list + # print "" + + def list_connection_info(self): + # Ask the settings service for the list of connections it provides + bus=dbus.SystemBus() + + service_name="org.freedesktop.NetworkManager" + proxy=bus.get_object(service_name, "/org/freedesktop/NetworkManager/Settings") + settings=dbus.Interface(proxy, "org.freedesktop.NetworkManager.Settings") + connection_paths=settings.ListConnections() + connection_list=[] + # List each connection's name, UUID, and type + for path in connection_paths: + con_proxy=bus.get_object(service_name, path) + settings_connection=dbus.Interface(con_proxy, "org.freedesktop.NetworkManager.Settings.Connection") + config=settings_connection.GetSettings() + + # Now get secrets too; we grab the secrets for each type of connection + # (since there isn't a "get all secrets" call because most of the time + # you only need 'wifi' secrets or '802.1x' secrets, not everything) and + # merge that into the configuration data - To use at a later stage + self.merge_secrets(settings_connection, config, '802-11-wireless') + self.merge_secrets(settings_connection, config, '802-11-wireless-security') + self.merge_secrets(settings_connection, config, '802-1x') + self.merge_secrets(settings_connection, config, 'gsm') + self.merge_secrets(settings_connection, config, 'cdma') + self.merge_secrets(settings_connection, config, 'ppp') + + # Get the details of the 'connection' setting + s_con=config['connection'] + connection_list.append(s_con['id']) + connection_list.append(s_con['uuid']) + connection_list.append(s_con['type']) + connection_list.append(self.connection_to_string(config)) + return connection_list + + def connection_exists(self): + # we are going to use name and type in this instance to find if that connection exists and is of type x + connections=self.list_connection_info() + + for con_item in connections: + if self.cname==con_item: + return True + + def down_connection(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # if self.connection_exists(): + cmd.append('con') + cmd.append('down') + cmd.append(self.cname) + return self.execute_command(cmd) + + def up_connection(self): + cmd=[self.module.get_bin_path('nmcli', True)] + cmd.append('con') + cmd.append('up') + cmd.append(self.cname) + return self.execute_command(cmd) + + def create_connection_team(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for creating team interface + cmd.append('con') + cmd.append('add') + cmd.append('type') + cmd.append('team') + cmd.append('con-name') + if self.cname is not None: + cmd.append(self.cname) + elif self.ifname is not None: + cmd.append(self.ifname) + cmd.append('ifname') + if self.ifname is not None: + cmd.append(self.ifname) + elif self.cname is not None: + cmd.append(self.cname) + if self.ip4 is not None: + cmd.append('ip4') + cmd.append(self.ip4) + if self.gw4 is not None: + cmd.append('gw4') + cmd.append(self.gw4) + if self.ip6 is not None: + cmd.append('ip6') + cmd.append(self.ip6) + if self.gw6 is not None: + cmd.append('gw6') + cmd.append(self.gw6) + if self.enabled is not None: + cmd.append('autoconnect') + cmd.append(self.enabled) + return cmd + + def modify_connection_team(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for modifying team interface + cmd.append('con') + cmd.append('mod') + cmd.append(self.cname) + if self.ip4 is not None: + cmd.append('ipv4.address') + cmd.append(self.ip4) + if self.gw4 is not None: + cmd.append('ipv4.gateway') + cmd.append(self.gw4) + if self.dns4 is not None: + cmd.append('ipv4.dns') + cmd.append(self.dns4) + if self.ip6 is not None: + cmd.append('ipv6.address') + cmd.append(self.ip6) + if self.gw6 is not None: + cmd.append('ipv6.gateway') + cmd.append(self.gw4) + if self.dns6 is not None: + cmd.append('ipv6.dns') + cmd.append(self.dns6) + if self.enabled is not None: + cmd.append('autoconnect') + cmd.append(self.enabled) + # Can't use MTU with team + return cmd + + def create_connection_team_slave(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for creating team-slave interface + cmd.append('connection') + cmd.append('add') + cmd.append('type') + cmd.append(self.type) + cmd.append('con-name') + if self.cname is not None: + cmd.append(self.cname) + elif self.ifname is not None: + cmd.append(self.ifname) + cmd.append('ifname') + if self.ifname is not None: + cmd.append(self.ifname) + elif self.cname is not None: + cmd.append(self.cname) + cmd.append('master') + if self.cname is not None: + cmd.append(self.master) + # if self.mtu is not None: + # cmd.append('802-3-ethernet.mtu') + # cmd.append(self.mtu) + return cmd + + def modify_connection_team_slave(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for modifying team-slave interface + cmd.append('con') + cmd.append('mod') + cmd.append(self.cname) + cmd.append('connection.master') + cmd.append(self.master) + if self.mtu is not None: + cmd.append('802-3-ethernet.mtu') + cmd.append(self.mtu) + return cmd + + def create_connection_bond(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for creating bond interface + cmd.append('con') + cmd.append('add') + cmd.append('type') + cmd.append('bond') + cmd.append('con-name') + if self.cname is not None: + cmd.append(self.cname) + elif self.ifname is not None: + cmd.append(self.ifname) + cmd.append('ifname') + if self.ifname is not None: + cmd.append(self.ifname) + elif self.cname is not None: + cmd.append(self.cname) + if self.ip4 is not None: + cmd.append('ip4') + cmd.append(self.ip4) + if self.gw4 is not None: + cmd.append('gw4') + cmd.append(self.gw4) + if self.ip6 is not None: + cmd.append('ip6') + cmd.append(self.ip6) + if self.gw6 is not None: + cmd.append('gw6') + cmd.append(self.gw6) + if self.enabled is not None: + cmd.append('autoconnect') + cmd.append(self.enabled) + if self.mode is not None: + cmd.append('mode') + cmd.append(self.mode) + if self.miimon is not None: + cmd.append('miimon') + cmd.append(self.miimon) + if self.downdelay is not None: + cmd.append('downdelay') + cmd.append(self.downdelay) + if self.downdelay is not None: + cmd.append('updelay') + cmd.append(self.updelay) + if self.downdelay is not None: + cmd.append('arp-interval') + cmd.append(self.arp_interval) + if self.downdelay is not None: + cmd.append('arp-ip-target') + cmd.append(self.arp_ip_target) + return cmd + + def modify_connection_bond(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for modifying bond interface + cmd.append('con') + cmd.append('mod') + cmd.append(self.cname) + if self.ip4 is not None: + cmd.append('ipv4.address') + cmd.append(self.ip4) + if self.gw4 is not None: + cmd.append('ipv4.gateway') + cmd.append(self.gw4) + if self.dns4 is not None: + cmd.append('ipv4.dns') + cmd.append(self.dns4) + if self.ip6 is not None: + cmd.append('ipv6.address') + cmd.append(self.ip6) + if self.gw6 is not None: + cmd.append('ipv6.gateway') + cmd.append(self.gw4) + if self.dns6 is not None: + cmd.append('ipv6.dns') + cmd.append(self.dns6) + if self.enabled is not None: + cmd.append('autoconnect') + cmd.append(self.enabled) + return cmd + + def create_connection_bond_slave(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for creating bond-slave interface + cmd.append('connection') + cmd.append('add') + cmd.append('type') + cmd.append('bond-slave') + cmd.append('con-name') + if self.cname is not None: + cmd.append(self.cname) + elif self.ifname is not None: + cmd.append(self.ifname) + cmd.append('ifname') + if self.ifname is not None: + cmd.append(self.ifname) + elif self.cname is not None: + cmd.append(self.cname) + cmd.append('master') + if self.cname is not None: + cmd.append(self.master) + return cmd + + def modify_connection_bond_slave(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for modifying bond-slave interface + cmd.append('con') + cmd.append('mod') + cmd.append(self.cname) + cmd.append('connection.master') + cmd.append(self.master) + return cmd + + def create_connection_ethernet(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for creating ethernet interface + # To add an Ethernet connection with static IP configuration, issue a command as follows + # - nmcli: name=add cname=my-eth1 ifname=eth1 type=ethernet ip4=192.168.100.100/24 gw4=192.168.100.1 state=present + # nmcli con add con-name my-eth1 ifname eth1 type ethernet ip4 192.168.100.100/24 gw4 192.168.100.1 + cmd.append('con') + cmd.append('add') + cmd.append('type') + cmd.append('ethernet') + cmd.append('con-name') + if self.cname is not None: + cmd.append(self.cname) + elif self.ifname is not None: + cmd.append(self.ifname) + cmd.append('ifname') + if self.ifname is not None: + cmd.append(self.ifname) + elif self.cname is not None: + cmd.append(self.cname) + if self.ip4 is not None: + cmd.append('ip4') + cmd.append(self.ip4) + if self.gw4 is not None: + cmd.append('gw4') + cmd.append(self.gw4) + if self.ip6 is not None: + cmd.append('ip6') + cmd.append(self.ip6) + if self.gw6 is not None: + cmd.append('gw6') + cmd.append(self.gw6) + if self.enabled is not None: + cmd.append('autoconnect') + cmd.append(self.enabled) + return cmd + + def modify_connection_ethernet(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for modifying ethernet interface + # To add an Ethernet connection with static IP configuration, issue a command as follows + # - nmcli: name=add cname=my-eth1 ifname=eth1 type=ethernet ip4=192.168.100.100/24 gw4=192.168.100.1 state=present + # nmcli con add con-name my-eth1 ifname eth1 type ethernet ip4 192.168.100.100/24 gw4 192.168.100.1 + cmd.append('con') + cmd.append('mod') + cmd.append(self.cname) + if self.ip4 is not None: + cmd.append('ipv4.address') + cmd.append(self.ip4) + if self.gw4 is not None: + cmd.append('ipv4.gateway') + cmd.append(self.gw4) + if self.dns4 is not None: + cmd.append('ipv4.dns') + cmd.append(self.dns4) + if self.ip6 is not None: + cmd.append('ipv6.address') + cmd.append(self.ip6) + if self.gw6 is not None: + cmd.append('ipv6.gateway') + cmd.append(self.gw4) + if self.dns6 is not None: + cmd.append('ipv6.dns') + cmd.append(self.dns6) + if self.mtu is not None: + cmd.append('802-3-ethernet.mtu') + cmd.append(self.mtu) + if self.enabled is not None: + cmd.append('autoconnect') + cmd.append(self.enabled) + return cmd + + def create_connection_bridge(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for creating bridge interface + return cmd + + def modify_connection_bridge(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for modifying bridge interface + return cmd + + def create_connection_vlan(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for creating ethernet interface + return cmd + + def modify_connection_vlan(self): + cmd=[self.module.get_bin_path('nmcli', True)] + # format for modifying ethernet interface + return cmd + + def create_connection(self): + cmd=[] + if self.type=='team': + # cmd=self.create_connection_team() + if (self.dns4 is not None) or (self.dns6 is not None): + cmd=self.create_connection_team() + self.execute_command(cmd) + cmd=self.modify_connection_team() + self.execute_command(cmd) + cmd=self.up_connection() + return self.execute_command(cmd) + elif (self.dns4 is None) or (self.dns6 is None): + cmd=self.create_connection_team() + return self.execute_command(cmd) + elif self.type=='team-slave': + if self.mtu is not None: + cmd=self.create_connection_team_slave() + self.execute_command(cmd) + cmd=self.modify_connection_team_slave() + self.execute_command(cmd) + # cmd=self.up_connection() + return self.execute_command(cmd) + else: + cmd=self.create_connection_team_slave() + return self.execute_command(cmd) + elif self.type=='bond': + if (self.mtu is not None) or (self.dns4 is not None) or (self.dns6 is not None): + cmd=self.create_connection_bond() + self.execute_command(cmd) + cmd=self.modify_connection_bond() + self.execute_command(cmd) + cmd=self.up_connection() + return self.execute_command(cmd) + else: + cmd=self.create_connection_bond() + return self.execute_command(cmd) + elif self.type=='bond-slave': + cmd=self.create_connection_bond_slave() + elif self.type=='ethernet': + if (self.mtu is not None) or (self.dns4 is not None) or (self.dns6 is not None): + cmd=self.create_connection_ethernet() + self.execute_command(cmd) + cmd=self.modify_connection_ethernet() + self.execute_command(cmd) + cmd=self.up_connection() + return self.execute_command(cmd) + else: + cmd=self.create_connection_ethernet() + return self.execute_command(cmd) + elif self.type=='bridge': + cmd=self.create_connection_bridge() + elif self.type=='vlan': + cmd=self.create_connection_vlan() + return self.execute_command(cmd) + + def remove_connection(self): + # self.down_connection() + cmd=[self.module.get_bin_path('nmcli', True)] + cmd.append('con') + cmd.append('del') + cmd.append(self.cname) + return self.execute_command(cmd) + + def modify_connection(self): + cmd=[] + if self.type=='team': + cmd=self.modify_connection_team() + elif self.type=='team-slave': + cmd=self.modify_connection_team_slave() + elif self.type=='bond': + cmd=self.modify_connection_bond() + elif self.type=='bond-slave': + cmd=self.modify_connection_bond_slave() + elif self.type=='ethernet': + cmd=self.modify_connection_ethernet() + elif self.type=='bridge': + cmd=self.modify_connection_bridge() + elif self.type=='vlan': + cmd=self.modify_connection_vlan() + return self.execute_command(cmd) + + +def main(): + # Parsing argument file + module=AnsibleModule( + argument_spec=dict( + enabled=dict(required=False, default=None, choices=['yes', 'no'], type='str'), + action=dict(required=False, default=None, choices=['add', 'mod', 'show', 'up', 'down', 'del'], type='str'), + state=dict(required=True, default=None, choices=['present', 'absent'], type='str'), + cname=dict(required=False, type='str'), + master=dict(required=False, default=None, type='str'), + autoconnect=dict(required=False, default=None, choices=['yes', 'no'], type='str'), + ifname=dict(required=False, default=None, type='str'), + type=dict(required=False, default=None, choices=['ethernet', 'team', 'team-slave', 'bond', 'bond-slave', 'bridge', 'vlan'], type='str'), + ip4=dict(required=False, default=None, type='str'), + gw4=dict(required=False, default=None, type='str'), + dns4=dict(required=False, default=None, type='str'), + ip6=dict(required=False, default=None, type='str'), + gw6=dict(required=False, default=None, type='str'), + dns6=dict(required=False, default=None, type='str'), + # Bond Specific vars + mode=dict(require=False, default="balance-rr", choices=["balance-rr", "active-backup", "balance-xor", "broadcast", "802.3ad", "balance-tlb", "balance-alb"], type='str'), + miimon=dict(required=False, default=None, type='str'), + downdelay=dict(required=False, default=None, type='str'), + updelay=dict(required=False, default=None, type='str'), + arp_interval=dict(required=False, default=None, type='str'), + arp_ip_target=dict(required=False, default=None, type='str'), + # general usage + mtu=dict(required=False, default=None, type='str'), + mac=dict(required=False, default=None, type='str'), + # bridge specific vars + stp=dict(required=False, default='yes', choices=['yes', 'no'], type='str'), + priority=dict(required=False, default="128", type='str'), + slavepriority=dict(required=False, default="32", type='str'), + forwarddelay=dict(required=False, default="15", type='str'), + hellotime=dict(required=False, default="2", type='str'), + maxage=dict(required=False, default="20", type='str'), + ageingtime=dict(required=False, default="300", type='str'), + # vlan specific vars + vlanid=dict(required=False, default=None, type='str'), + vlandev=dict(required=False, default=None, type='str'), + flags=dict(required=False, default=None, type='str'), + ingress=dict(required=False, default=None, type='str'), + egress=dict(required=False, default=None, type='str'), + ), + supports_check_mode=True + ) + + nmcli=Nmcli(module) + + if nmcli.syslogging: + syslog.openlog('ansible-%s' % os.path.basename(__file__)) + syslog.syslog(syslog.LOG_NOTICE, 'Nmcli instantiated - platform %s' % nmcli.platform) + if nmcli.distribution: + syslog.syslog(syslog.LOG_NOTICE, 'Nuser instantiated - distribution %s' % nmcli.distribution) + + rc=None + out='' + err='' + result={} + result['cname']=nmcli.cname + result['state']=nmcli.state + + # check for issues + if nmcli.cname is None: + nmcli.module.fail_json(msg="You haven't specified a name for the connection") + # team-slave checks + if nmcli.type=='team-slave' and nmcli.master is None: + nmcli.module.fail_json(msg="You haven't specified a name for the master so we're not changing a thing") + if nmcli.type=='team-slave' and nmcli.ifname is None: + nmcli.module.fail_json(msg="You haven't specified a name for the connection") + + if nmcli.state=='absent': + if nmcli.connection_exists(): + if module.check_mode: + module.exit_json(changed=True) + (rc, out, err)=nmcli.down_connection() + (rc, out, err)=nmcli.remove_connection() + if rc!=0: + module.fail_json(name =('No Connection named %s exists' % nmcli.cname), msg=err, rc=rc) + + elif nmcli.state=='present': + if nmcli.connection_exists(): + # modify connection (note: this function is check mode aware) + # result['Connection']=('Connection %s of Type %s is not being added' % (nmcli.cname, nmcli.type)) + result['Exists']='Connections do exist so we are modifying them' + if module.check_mode: + module.exit_json(changed=True) + (rc, out, err)=nmcli.modify_connection() + if not nmcli.connection_exists(): + result['Connection']=('Connection %s of Type %s is being added' % (nmcli.cname, nmcli.type)) + if module.check_mode: + module.exit_json(changed=True) + (rc, out, err)=nmcli.create_connection() + if rc is not None and rc!=0: + module.fail_json(name=nmcli.cname, msg=err, rc=rc) + + if rc is None: + result['changed']=False + else: + result['changed']=True + if out: + result['stdout']=out + if err: + result['stderr']=err + + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * + +main() \ No newline at end of file From 2856116162aa7430368c395ec099888cb1ea7b7b Mon Sep 17 00:00:00 2001 From: Chris Long Date: Fri, 15 May 2015 00:45:51 +1000 Subject: [PATCH 0165/2522] Updated as per bcoca's comments: removed 'default' in state: removed defunct action: removed reference to load_platform_subclass changed cname to conn_name --- network/nmcli.py | 202 ++++++++++++++++++++++------------------------- 1 file changed, 93 insertions(+), 109 deletions(-) diff --git a/network/nmcli.py b/network/nmcli.py index 0532058da3b..55edb322ad7 100644 --- a/network/nmcli.py +++ b/network/nmcli.py @@ -30,7 +30,6 @@ options: state: required: True - default: "present" choices: [ present, absent ] description: - Whether the device should exist or not, taking action if the state is different from what is stated. @@ -41,25 +40,14 @@ description: - Whether the service should start on boot. B(At least one of state and enabled are required.) - Whether the connection profile can be automatically activated ( default: yes) - action: - required: False - default: None - choices: [ add, modify, show, up, down ] - description: - - Set to 'add' if you want to add a connection. - - Set to 'modify' if you want to modify a connection. Modify one or more properties in the connection profile. - - Set to 'delete' if you want to delete a connection. Delete a configured connection. The connection to be deleted is identified by its name 'cfname'. - - Set to 'show' if you want to show a connection. Will show all devices unless 'cfname' is set. - - Set to 'up' if you want to bring a connection up. Requires 'cfname' to be set. - - Set to 'down' if you want to bring a connection down. Requires 'cfname' to be set. - cname: + conn_name: required: True default: None description: - - Where CNAME will be the name used to call the connection. when not provided a default name is generated: [-][-] + - Where conn_name will be the name used to call the connection. when not provided a default name is generated: [-][-] ifname: required: False - default: cname + default: conn_name description: - Where INAME will be the what we call the interface name. Required with 'up', 'down' modifiers. - interface to bind the connection to. The connection will only be applicable to this interface name. @@ -80,7 +68,7 @@ required: False default: None description: - - master Date: Fri, 15 May 2015 01:09:49 +1000 Subject: [PATCH 0166/2522] Fixed descriptions to all be lists replaced enabled with autoconnect - refactored code to reflect update. removed ansible syslog entry. --- network/nmcli.py | 66 +++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/network/nmcli.py b/network/nmcli.py index 55edb322ad7..18f0ecbab1f 100644 --- a/network/nmcli.py +++ b/network/nmcli.py @@ -31,25 +31,24 @@ state: required: True choices: [ present, absent ] - description: - - Whether the device should exist or not, taking action if the state is different from what is stated. - enabled: + description: + - Whether the device should exist or not, taking action if the state is different from what is stated. + autoconnect: required: False default: "yes" choices: [ "yes", "no" ] description: - - Whether the service should start on boot. B(At least one of state and enabled are required.) + - Whether the connection should start on boot. - Whether the connection profile can be automatically activated ( default: yes) conn_name: required: True - default: None description: - Where conn_name will be the name used to call the connection. when not provided a default name is generated: [-][-] ifname: required: False default: conn_name description: - - Where INAME will be the what we call the interface name. Required with 'up', 'down' modifiers. + - Where IFNAME will be the what we call the interface name. - interface to bind the connection to. The connection will only be applicable to this interface name. - A special value of "*" can be used for interface-independent connections. - The ifname argument is mandatory for all connection types except bond, team, bridge and vlan. @@ -72,14 +71,17 @@ ip4: required: False default: None - description: The IPv4 address to this interface using this format ie: "192.168.1.24/24" + description: + - The IPv4 address to this interface using this format ie: "192.168.1.24/24" gw4: required: False - description: The IPv4 gateway for this interface using this format ie: "192.168.100.1" + description: + - The IPv4 gateway for this interface using this format ie: "192.168.100.1" dns4: required: False default: None - description: A list of upto 3 dns servers, ipv4 format e.g. To add two IPv4 DNS server addresses: ['"8.8.8.8 8.8.4.4"'] + description: + - A list of upto 3 dns servers, ipv4 format e.g. To add two IPv4 DNS server addresses: ['"8.8.8.8 8.8.4.4"'] ip6: required: False default: None @@ -88,10 +90,12 @@ gw6: required: False default: None - description: The IPv6 gateway for this interface using this format ie: "2001:db8::1" + description: + - The IPv6 gateway for this interface using this format ie: "2001:db8::1" dns6: required: False - description: A list of upto 3 dns servers, ipv6 format e.g. To add two IPv6 DNS server addresses: ['"2001:4860:4860::8888 2001:4860:4860::8844"'] + description: + - A list of upto 3 dns servers, ipv6 format e.g. To add two IPv6 DNS server addresses: ['"2001:4860:4860::8888 2001:4860:4860::8844"'] mtu: required: False default: None @@ -343,7 +347,7 @@ - nmcli: conn_name=my-eth1 ifname=eth1 type=ethernet ip4=192.168.100.100/24 gw4=192.168.100.1 state=present # To add an Team connection with static IP configuration, issue a command as follows -- nmcli: conn_name=my-team1 ifname=my-team1 type=team ip4=192.168.100.100/24 gw4=192.168.100.1 state=present enabled=yes +- nmcli: conn_name=my-team1 ifname=my-team1 type=team ip4=192.168.100.100/24 gw4=192.168.100.1 state=present autoconnect=yes # Optionally, at the same time specify IPv6 addresses for the device as follows: - nmcli: conn_name=my-eth1 ifname=eth1 type=ethernet ip4=192.168.100.100/24 gw4=192.168.100.1 ip6=abbe::cafe gw6=2001:db8::1 state=present @@ -430,10 +434,9 @@ class Nmcli(object): def __init__(self, module): self.module=module self.state=module.params['state'] - self.enabled=module.params['enabled'] + self.autoconnect=module.params['autoconnect'] self.conn_name=module.params['conn_name'] self.master=module.params['master'] - self.autoconnect=module.params['autoconnect'] self.ifname=module.params['ifname'] self.type=module.params['type'] self.ip4=module.params['ip4'] @@ -602,9 +605,9 @@ def create_connection_team(self): if self.gw6 is not None: cmd.append('gw6') cmd.append(self.gw6) - if self.enabled is not None: + if self.autoconnect is not None: cmd.append('autoconnect') - cmd.append(self.enabled) + cmd.append(self.autoconnect) return cmd def modify_connection_team(self): @@ -631,9 +634,9 @@ def modify_connection_team(self): if self.dns6 is not None: cmd.append('ipv6.dns') cmd.append(self.dns6) - if self.enabled is not None: + if self.autoconnect is not None: cmd.append('autoconnect') - cmd.append(self.enabled) + cmd.append(self.autoconnect) # Can't use MTU with team return cmd @@ -704,9 +707,9 @@ def create_connection_bond(self): if self.gw6 is not None: cmd.append('gw6') cmd.append(self.gw6) - if self.enabled is not None: + if self.autoconnect is not None: cmd.append('autoconnect') - cmd.append(self.enabled) + cmd.append(self.autoconnect) if self.mode is not None: cmd.append('mode') cmd.append(self.mode) @@ -751,9 +754,9 @@ def modify_connection_bond(self): if self.dns6 is not None: cmd.append('ipv6.dns') cmd.append(self.dns6) - if self.enabled is not None: + if self.autoconnect is not None: cmd.append('autoconnect') - cmd.append(self.enabled) + cmd.append(self.autoconnect) return cmd def create_connection_bond_slave(self): @@ -820,9 +823,9 @@ def create_connection_ethernet(self): if self.gw6 is not None: cmd.append('gw6') cmd.append(self.gw6) - if self.enabled is not None: + if self.autoconnect is not None: cmd.append('autoconnect') - cmd.append(self.enabled) + cmd.append(self.autoconnect) return cmd def modify_connection_ethernet(self): @@ -855,9 +858,9 @@ def modify_connection_ethernet(self): if self.mtu is not None: cmd.append('802-3-ethernet.mtu') cmd.append(self.mtu) - if self.enabled is not None: + if self.autoconnect is not None: cmd.append('autoconnect') - cmd.append(self.enabled) + cmd.append(self.autoconnect) return cmd def create_connection_bridge(self): @@ -966,11 +969,10 @@ def main(): # Parsing argument file module=AnsibleModule( argument_spec=dict( - enabled=dict(required=False, default=None, choices=['yes', 'no'], type='str'), + autoconnect=dict(required=False, default=None, choices=['yes', 'no'], type='str'), state=dict(required=True, choices=['present', 'absent'], type='str'), - conn_name=dict(required=False, type='str'), + conn_name=dict(required=True, type='str'), master=dict(required=False, default=None, type='str'), - autoconnect=dict(required=False, default=None, choices=['yes', 'no'], type='str'), ifname=dict(required=False, default=None, type='str'), type=dict(required=False, default=None, choices=['ethernet', 'team', 'team-slave', 'bond', 'bond-slave', 'bridge', 'vlan'], type='str'), ip4=dict(required=False, default=None, type='str'), @@ -1009,12 +1011,6 @@ def main(): nmcli=Nmcli(module) - if nmcli.syslogging: - syslog.openlog('ansible-%s' % os.path.basename(__file__)) - syslog.syslog(syslog.LOG_NOTICE, 'Nmcli instantiated - platform %s' % nmcli.platform) - if nmcli.distribution: - syslog.syslog(syslog.LOG_NOTICE, 'Nuser instantiated - distribution %s' % nmcli.distribution) - rc=None out='' err='' From 31c63b6755ba2387a301cc9b83f1d3fe54cd1669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Kek=C3=A4l=C3=A4inen?= Date: Fri, 15 May 2015 16:47:23 +0300 Subject: [PATCH 0167/2522] gluster_volume: Typofix in docs (equals, not colon) --- system/gluster_volume.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/gluster_volume.py b/system/gluster_volume.py index 7b83c62297f..7d080f8bfe6 100644 --- a/system/gluster_volume.py +++ b/system/gluster_volume.py @@ -108,7 +108,7 @@ EXAMPLES = """ - name: create gluster volume - gluster_volume: state=present name=test1 bricks=/bricks/brick1/g1 rebalance=yes cluster:"{{ play_hosts }}" + gluster_volume: state=present name=test1 bricks=/bricks/brick1/g1 rebalance=yes cluster="{{ play_hosts }}" run_once: true - name: tune @@ -127,7 +127,7 @@ gluster_volume: state=absent name=test1 - name: create gluster volume with multiple bricks - gluster_volume: state=present name=test2 bricks="/bricks/brick1/g2,/bricks/brick2/g2" cluster:"{{ play_hosts }}" + gluster_volume: state=present name=test2 bricks="/bricks/brick1/g2,/bricks/brick2/g2" cluster="{{ play_hosts }}" run_once: true """ From 9cfe4697516ede70e7722c75adb676c628f7d102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Kek=C3=A4l=C3=A4inen?= Date: Fri, 15 May 2015 16:49:39 +0300 Subject: [PATCH 0168/2522] gluster_volume: Clarify error message to tell what actualy failed --- system/gluster_volume.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/gluster_volume.py b/system/gluster_volume.py index 7d080f8bfe6..cb7882f6c56 100644 --- a/system/gluster_volume.py +++ b/system/gluster_volume.py @@ -247,11 +247,11 @@ def wait_for_peer(host): time.sleep(1) return False -def probe(host): +def probe(host, myhostname): global module run_gluster([ 'peer', 'probe', host ]) if not wait_for_peer(host): - module.fail_json(msg='failed to probe peer %s' % host) + module.fail_json(msg='failed to probe peer %s on %s' % (host, myhostname)) changed = True def probe_all_peers(hosts, peers, myhostname): @@ -259,7 +259,7 @@ def probe_all_peers(hosts, peers, myhostname): if host not in peers: # dont probe ourselves if myhostname != host: - probe(host) + probe(host, myhostname) def create_volume(name, stripe, replica, transport, hosts, bricks, force): args = [ 'volume', 'create' ] From 02ed758af1de3b4603f179bd0ae4c9940cf4051b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Kek=C3=A4l=C3=A4inen?= Date: Fri, 15 May 2015 17:24:18 +0300 Subject: [PATCH 0169/2522] gluster_volume: Parameter expects comma separated list of hosts, passing {{play_hosts}} will fail as Python does not parse it into a list --- system/gluster_volume.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/gluster_volume.py b/system/gluster_volume.py index cb7882f6c56..2ea6b974adc 100644 --- a/system/gluster_volume.py +++ b/system/gluster_volume.py @@ -108,7 +108,7 @@ EXAMPLES = """ - name: create gluster volume - gluster_volume: state=present name=test1 bricks=/bricks/brick1/g1 rebalance=yes cluster="{{ play_hosts }}" + gluster_volume: state=present name=test1 bricks=/bricks/brick1/g1 rebalance=yes cluster="192.168.1.10,192.168.1.11" run_once: true - name: tune @@ -127,7 +127,7 @@ gluster_volume: state=absent name=test1 - name: create gluster volume with multiple bricks - gluster_volume: state=present name=test2 bricks="/bricks/brick1/g2,/bricks/brick2/g2" cluster="{{ play_hosts }}" + gluster_volume: state=present name=test2 bricks="/bricks/brick1/g2,/bricks/brick2/g2" cluster="192.168.1.10,192.168.1.11" run_once: true """ From 0cec4527f0108e4374f00e75ca831b256f9c4248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Kek=C3=A4l=C3=A4inen?= Date: Fri, 15 May 2015 17:40:30 +0300 Subject: [PATCH 0170/2522] gluster_volume: Improved parsing of cluster parameter list --- system/gluster_volume.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/system/gluster_volume.py b/system/gluster_volume.py index 2ea6b974adc..c5d852731c5 100644 --- a/system/gluster_volume.py +++ b/system/gluster_volume.py @@ -256,6 +256,7 @@ def probe(host, myhostname): def probe_all_peers(hosts, peers, myhostname): for host in hosts: + host = host.strip() # Clean up any extra space for exact comparison if host not in peers: # dont probe ourselves if myhostname != host: @@ -347,6 +348,11 @@ def main(): if not myhostname: myhostname = socket.gethostname() + # Clean up if last element is empty. Consider that yml can look like this: + # cluster="{% for host in groups['glusterfs'] %}{{ hostvars[host]['private_ip'] }},{% endfor %}" + if cluster != None and cluster[-1] == '': + cluster = cluster[0:-1] + if brick_paths != None and "," in brick_paths: brick_paths = brick_paths.split(",") else: From c4afe9c5bbc67bba888e6bd7d7d305fa17181dd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Otto=20Kek=C3=A4l=C3=A4inen?= Date: Fri, 15 May 2015 17:55:16 +0300 Subject: [PATCH 0171/2522] gluster_volume: Finalize brick->bricks transition by previous author --- system/gluster_volume.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/gluster_volume.py b/system/gluster_volume.py index c5d852731c5..32359cd2a82 100644 --- a/system/gluster_volume.py +++ b/system/gluster_volume.py @@ -336,7 +336,7 @@ def main(): action = module.params['state'] volume_name = module.params['name'] cluster= module.params['cluster'] - brick_paths = module.params['brick'] + brick_paths = module.params['bricks'] stripes = module.params['stripes'] replicas = module.params['replicas'] transport = module.params['transport'] From 5b7b74eb66883b23e610fa7f4dcc63f3d2dc2577 Mon Sep 17 00:00:00 2001 From: Sebastian Kornehl Date: Tue, 19 May 2015 15:05:31 +0200 Subject: [PATCH 0172/2522] Added eval for pasting tag lists --- monitoring/datadog_event.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/monitoring/datadog_event.py b/monitoring/datadog_event.py index 5319fcb0f1b..bde5cd80069 100644 --- a/monitoring/datadog_event.py +++ b/monitoring/datadog_event.py @@ -116,7 +116,10 @@ def post_event(module): if module.params['date_happened'] != None: body['date_happened'] = module.params['date_happened'] if module.params['tags'] != None: - body['tags'] = module.params['tags'].split(",") + if module.params['tags'].startswith("[") and module.params['tags'].endswith("]"): + body['tags'] = eval(module.params['tags']) + else: + body['tags'] = module.params['tags'].split(",") if module.params['aggregation_key'] != None: body['aggregation_key'] = module.params['aggregation_key'] if module.params['source_type_name'] != None: From 3761052597d67fe011485d055637505a0391ef51 Mon Sep 17 00:00:00 2001 From: Ernst Kuschke Date: Wed, 20 May 2015 16:34:21 +0200 Subject: [PATCH 0173/2522] Allow any custom chocolatey source This is to allow for a local source (for instance in the form of artifactory) --- windows/win_chocolatey.ps1 | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/windows/win_chocolatey.ps1 b/windows/win_chocolatey.ps1 index 22e0d83e77c..de42434da76 100644 --- a/windows/win_chocolatey.ps1 +++ b/windows/win_chocolatey.ps1 @@ -112,9 +112,9 @@ Else If ($params.source) { $source = $params.source.ToString().ToLower() - If (($source -ne "chocolatey") -and ($source -ne "webpi") -and ($source -ne "windowsfeatures") -and ($source -ne "ruby")) + If (($source -ne "chocolatey") -and ($source -ne "webpi") -and ($source -ne "windowsfeatures") -and ($source -ne "ruby") -and (!$source.startsWith("http://", "CurrentCultureIgnoreCase")) -and (!$source.startsWith("https://", "CurrentCultureIgnoreCase"))) { - Fail-Json $result "source is $source - must be one of chocolatey, ruby, webpi or windowsfeatures." + Fail-Json $result "source is $source - must be one of chocolatey, ruby, webpi, windowsfeatures or a custom source url." } } Elseif (!$params.source) @@ -190,6 +190,10 @@ elseif (($source -eq "windowsfeatures") -or ($source -eq "webpi") -or ($source - { $expression += " -source $source" } +elseif(($source -ne $Null) -and ($source -ne "")) +{ + $expression += " -source $source" +} Set-Attr $result "chocolatey command" $expression $op_result = invoke-expression $expression From b527380c6ac0833bff84625c9d6534ac37b9fdfc Mon Sep 17 00:00:00 2001 From: Christian Thiemann Date: Sun, 24 May 2015 02:05:38 +0200 Subject: [PATCH 0174/2522] Fix alternatives module in non-English locale The alternatives module parses the output of update-alternatives, but the expected English phrases may not show up if the system locale is not English. Setting LC_ALL=C when invoking update-alternatives fixes this problem. --- system/alternatives.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/alternatives.py b/system/alternatives.py index c298afc2949..06d9bea25f0 100644 --- a/system/alternatives.py +++ b/system/alternatives.py @@ -85,7 +85,7 @@ def main(): # Run `update-alternatives --display ` to find existing alternatives (rc, display_output, _) = module.run_command( - [UPDATE_ALTERNATIVES, '--display', name] + ['env', 'LC_ALL=C', UPDATE_ALTERNATIVES, '--display', name] ) if rc == 0: @@ -106,7 +106,7 @@ def main(): # This is only compatible on Debian-based systems, as the other # alternatives don't have --query available rc, query_output, _ = module.run_command( - [UPDATE_ALTERNATIVES, '--query', name] + ['env', 'LC_ALL=C', UPDATE_ALTERNATIVES, '--query', name] ) if rc == 0: for line in query_output.splitlines(): From 6f2b61d2d88294ea7938020183ea613b7e5e878d Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Wed, 27 May 2015 20:54:26 +0200 Subject: [PATCH 0175/2522] firewalld: remove BabyJSON See https://github.com/ansible/ansible-modules-extras/issues/430 --- system/firewalld.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/system/firewalld.py b/system/firewalld.py index 77cfc4b6bb8..e16e4e4a9dd 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -67,8 +67,8 @@ required: false default: 0 notes: - - Not tested on any debian based system. -requirements: [ firewalld >= 0.2.11 ] + - Not tested on any Debian based system. +requirements: [ 'firewalld >= 0.2.11' ] author: '"Adam Miller (@maxamillion)" ' ''' @@ -82,7 +82,6 @@ import os import re -import sys try: import firewall.config @@ -90,14 +89,9 @@ from firewall.client import FirewallClient fw = FirewallClient() - if not fw.connected: - raise Exception('failed to connect to the firewalld daemon') + HAS_FIREWALLD = True except ImportError: - print "failed=True msg='firewalld required for this module'" - sys.exit(1) -except Exception, e: - print "failed=True msg='%s'" % str(e) - sys.exit(1) + HAS_FIREWALLD = False ################ # port handling @@ -223,6 +217,9 @@ def main(): supports_check_mode=True ) + if not HAS_FIREWALLD: + module.fail_json(msg='firewalld required for this module') + ## Pre-run version checking if FW_VERSION < "0.2.11": module.fail_json(msg='unsupported version of firewalld, requires >= 2.0.11') @@ -400,6 +397,4 @@ def main(): ################################################# # import module snippets from ansible.module_utils.basic import * - main() - From 1f322876262baa8c16f8439cd909a73743bba8c4 Mon Sep 17 00:00:00 2001 From: fdupoux Date: Thu, 28 May 2015 19:46:53 +0100 Subject: [PATCH 0176/2522] Removed conditional assignment of yesopt to make it work with python-2.4 (to pass the Travis-CI test) --- system/lvol.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/system/lvol.py b/system/lvol.py index 43511ae7b7a..c49cb369440 100644 --- a/system/lvol.py +++ b/system/lvol.py @@ -127,7 +127,10 @@ def main(): if version_found == None: module.fail_json(msg="Failed to get LVM version number") version_yesopt = mkversion(2, 2, 99) # First LVM with the "--yes" option - yesopt = "--yes" if version_found >= version_yesopt else "" + if version_found >= version_yesopt: + yesopt = "--yes" + else: + yesopt = "" vg = module.params['vg'] lv = module.params['lv'] From 9d2d3f0299ad34ee8332bb179afd64bd9da245ae Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 28 May 2015 16:23:27 -0400 Subject: [PATCH 0177/2522] Add module to run puppet There is a growing pattern for using ansible to orchestrate runs of existing puppet code. For instance, the OpenStack Infrastructure team started using ansible for this very reason. It also turns out that successfully running puppet and interpreting success or failure is harder than you'd expect, thus warranting a module and not just a shell command. This is ported in from http://git.openstack.org/cgit/openstack-infra/ansible-puppet --- system/puppet.py | 186 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 system/puppet.py diff --git a/system/puppet.py b/system/puppet.py new file mode 100644 index 00000000000..c53c88f595d --- /dev/null +++ b/system/puppet.py @@ -0,0 +1,186 @@ +#!/usr/bin/python + +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + +import json +import os +import pipes + +DOCUMENTATION = ''' +--- +module: puppet +short_description: Runs puppet +description: + - Runs I(puppet) agent or apply in a reliable manner +version_added: "2.0" +options: + timeout: + description: + - How long to wait for I(puppet) to finish. + required: false + default: 30m + puppetmaster: + description: + - The hostname of the puppetmaster to contact. Must have this or manifest + required: false + default: None + manifest: + desciption: + - Path to the manifest file to run puppet apply on. Must have this or puppetmaster + required: false + default: None + show_diff: + description: + - Should puppet return diffs of changes applied. Defaults to off to avoid leaking secret changes by default. + required: false + default: no + choices: [ "yes", "no" ] + facts: + description: + - A dict of values to pass in as persistent external facter facts + required: false + default: None + facter_basename: + desciption: + - Basename of the facter output file + required: false + default: ansible +requirements: [ puppet ] +author: Monty Taylor +''' + +EXAMPLES = ''' +# Run puppet and fail if anything goes wrong +- puppet + +# Run puppet and timeout in 5 minutes +- puppet: timeout=5m +''' + + +def _get_facter_dir(): + if os.getuid() == 0: + return '/etc/facter/facts.d' + else: + return os.path.expanduser('~/.facter/facts.d') + + +def _write_structured_data(basedir, basename, data): + if not os.path.exists(basedir): + os.makedirs(basedir) + file_path = os.path.join(basedir, "{0}.json".format(basename)) + with os.fdopen( + os.open(file_path, os.O_CREAT | os.O_WRONLY, 0o600), + 'w') as out_file: + out_file.write(json.dumps(data).encode('utf8')) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + timeout=dict(default="30m"), + puppetmaster=dict(required=False, default=None), + manifest=dict(required=False, default=None), + show_diff=dict( + default=False, aliases=['show-diff'], type='bool'), + facts=dict(default=None), + facter_basename=dict(default='ansible'), + ), + required_one_of=[ + ('puppetmaster', 'manifest'), + ], + ) + p = module.params + + global PUPPET_CMD + PUPPET_CMD = module.get_bin_path("puppet", False) + + if not PUPPET_CMD: + module.fail_json( + msg="Could not find puppet. Please ensure it is installed.") + + if p['manifest']: + if not os.path.exists(p['manifest']): + module.fail_json( + msg="Manifest file %(manifest)s not found." % dict( + manifest=p['manifest']) + + # Check if puppet is disabled here + if p['puppetmaster']: + rc, stdout, stderr = module.run_command( + PUPPET_CMD + "config print agent_disabled_lockfile") + if os.path.exists(stdout.strip()): + module.fail_json( + msg="Puppet agent is administratively disabled.", disabled=True) + elif rc != 0: + module.fail_json( + msg="Puppet agent state could not be determined.") + + if module.params['facts']: + _write_structured_data( + _get_facter_dir(), + module.params['facter_basename'], + module.params['facts']) + + base_cmd = "timeout -s 9 %(timeout)s %(puppet_cmd)s" % dict( + timeout=pipes.quote(p['timeout']), puppet_cmd=PUPPET_CMD) + + if p['puppetmaster']: + cmd = ("%(base_cmd) agent --onetime" + " --server %(puppetmaster)s" + " --ignorecache --no-daemonize --no-usecacheonfailure --no-splay" + " --detailed-exitcodes --verbose") % dict( + base_cmd=base_cmd, + puppetmaster=pipes.quote(p['puppetmaster'])) + if p['show_diff']: + cmd += " --show-diff" + else: + cmd = ("%(base_cmd) apply --detailed-exitcodes %(manifest)s" % dict( + base_cmd=base_cmd, + manifest=pipes.quote(p['manifest'])) + rc, stdout, stderr = module.run_command(cmd) + + if rc == 0: + # success + module.exit_json(rc=rc, changed=False, stdout=stdout) + elif rc == 1: + # rc==1 could be because it's disabled + # rc==1 could also mean there was a compilation failure + disabled = "administratively disabled" in stdout + if disabled: + msg = "puppet is disabled" + else: + msg = "puppet did not run" + module.exit_json( + rc=rc, disabled=disabled, msg=msg, + error=True, stdout=stdout, stderr=stderr) + elif rc == 2: + # success with changes + module.exit_json(rc=0, changed=True) + elif rc == 124: + # timeout + module.exit_json( + rc=rc, msg="%s timed out" % cmd, stdout=stdout, stderr=stderr) + else: + # failure + module.fail_json( + rc=rc, msg="%s failed with return code: %d" % (cmd, rc), + stdout=stdout, stderr=stderr) + +# import module snippets +from ansible.module_utils.basic import * + +main() From a1ecd60285c91466577463069a8cf9c6739813b1 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 29 May 2015 07:00:30 -0400 Subject: [PATCH 0178/2522] Fix some errors pointed out by travis --- system/puppet.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/system/puppet.py b/system/puppet.py index c53c88f595d..57c76eeec9f 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -82,10 +82,10 @@ def _write_structured_data(basedir, basename, data): if not os.path.exists(basedir): os.makedirs(basedir) file_path = os.path.join(basedir, "{0}.json".format(basename)) - with os.fdopen( - os.open(file_path, os.O_CREAT | os.O_WRONLY, 0o600), - 'w') as out_file: - out_file.write(json.dumps(data).encode('utf8')) + out_file = os.fdopen( + os.open(file_path, os.O_CREAT | os.O_WRONLY, 0o600), 'w') + out_file.write(json.dumps(data).encode('utf8')) + out_file.close() def main(): @@ -116,7 +116,7 @@ def main(): if not os.path.exists(p['manifest']): module.fail_json( msg="Manifest file %(manifest)s not found." % dict( - manifest=p['manifest']) + manifest=p['manifest'])) # Check if puppet is disabled here if p['puppetmaster']: @@ -149,8 +149,8 @@ def main(): cmd += " --show-diff" else: cmd = ("%(base_cmd) apply --detailed-exitcodes %(manifest)s" % dict( - base_cmd=base_cmd, - manifest=pipes.quote(p['manifest'])) + base_cmd=base_cmd, + manifest=pipes.quote(p['manifest']))) rc, stdout, stderr = module.run_command(cmd) if rc == 0: From e7ed08f762188244406e5fe1252c34ee64dcbfe7 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 29 May 2015 07:06:15 -0400 Subject: [PATCH 0179/2522] Add support for check mode --- system/puppet.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/system/puppet.py b/system/puppet.py index 57c76eeec9f..d6bc4348375 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -99,6 +99,7 @@ def main(): facts=dict(default=None), facter_basename=dict(default='ansible'), ), + supports_check_mode=True, required_one_of=[ ('puppetmaster', 'manifest'), ], @@ -129,7 +130,7 @@ def main(): module.fail_json( msg="Puppet agent state could not be determined.") - if module.params['facts']: + if module.params['facts'] and not module.check_mode: _write_structured_data( _get_facter_dir(), module.params['facter_basename'], @@ -139,7 +140,7 @@ def main(): timeout=pipes.quote(p['timeout']), puppet_cmd=PUPPET_CMD) if p['puppetmaster']: - cmd = ("%(base_cmd) agent --onetime" + cmd = ("%(base_cmd)s agent --onetime" " --server %(puppetmaster)s" " --ignorecache --no-daemonize --no-usecacheonfailure --no-splay" " --detailed-exitcodes --verbose") % dict( @@ -147,10 +148,13 @@ def main(): puppetmaster=pipes.quote(p['puppetmaster'])) if p['show_diff']: cmd += " --show-diff" + if module.check_mode: + cmd += " --noop" else: - cmd = ("%(base_cmd) apply --detailed-exitcodes %(manifest)s" % dict( - base_cmd=base_cmd, - manifest=pipes.quote(p['manifest']))) + cmd = "%s apply --detailed-exitcodes " % base_cmd + if module.check_mode: + cmd += "--noop " + cmd += pipes.quote(p['manifest']) rc, stdout, stderr = module.run_command(cmd) if rc == 0: From ce93a91a590a9eddeaddcfe9601db4f7f08b1cf6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 29 May 2015 08:09:31 -0400 Subject: [PATCH 0180/2522] Fix octal values for python 2.4 --- system/puppet.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/system/puppet.py b/system/puppet.py index d6bc4348375..46a5ea58d4f 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -18,6 +18,7 @@ import json import os import pipes +import stat DOCUMENTATION = ''' --- @@ -82,8 +83,13 @@ def _write_structured_data(basedir, basename, data): if not os.path.exists(basedir): os.makedirs(basedir) file_path = os.path.join(basedir, "{0}.json".format(basename)) + # This is more complex than you might normally expect because we want to + # open the file with only u+rw set. Also, we use the stat constants + # because ansible still supports python 2.4 and the octal syntax changed out_file = os.fdopen( - os.open(file_path, os.O_CREAT | os.O_WRONLY, 0o600), 'w') + os.open( + file_path, os.O_CREAT | os.O_WRONLY, + stat.S_IRUSR | stat.S_IWUSR), 'w') out_file.write(json.dumps(data).encode('utf8')) out_file.close() From 93a1542cc1b0d954b8877f06ba10bd802447977c Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Fri, 29 May 2015 10:07:00 +0200 Subject: [PATCH 0181/2522] cloudstack: improve required params --- cloud/cloudstack/cs_account.py | 3 +++ cloud/cloudstack/cs_affinitygroup.py | 3 +++ cloud/cloudstack/cs_firewall.py | 7 +++++++ cloud/cloudstack/cs_instance.py | 3 +++ cloud/cloudstack/cs_instancegroup.py | 3 +++ cloud/cloudstack/cs_iso.py | 3 +++ cloud/cloudstack/cs_portforward.py | 3 +++ cloud/cloudstack/cs_securitygroup.py | 3 +++ cloud/cloudstack/cs_securitygroup_rule.py | 4 ++++ cloud/cloudstack/cs_sshkeypair.py | 3 +++ cloud/cloudstack/cs_vmsnapshot.py | 4 ++++ 11 files changed, 39 insertions(+) diff --git a/cloud/cloudstack/cs_account.py b/cloud/cloudstack/cs_account.py index 399dfa090cc..a8510bbc5b3 100644 --- a/cloud/cloudstack/cs_account.py +++ b/cloud/cloudstack/cs_account.py @@ -369,6 +369,9 @@ def main(): api_url = dict(default=None), api_http_method = dict(default='get'), ), + required_together = ( + ['api_key', 'api_secret', 'api_url'], + ), supports_check_mode=True ) diff --git a/cloud/cloudstack/cs_affinitygroup.py b/cloud/cloudstack/cs_affinitygroup.py index 2a8de46fe41..9ff3b123a0c 100644 --- a/cloud/cloudstack/cs_affinitygroup.py +++ b/cloud/cloudstack/cs_affinitygroup.py @@ -223,6 +223,9 @@ def main(): api_url = dict(default=None), api_http_method = dict(default='get'), ), + required_together = ( + ['api_key', 'api_secret', 'api_url'], + ), supports_check_mode=True ) diff --git a/cloud/cloudstack/cs_firewall.py b/cloud/cloudstack/cs_firewall.py index c9e42be4a4f..ef78b6a242d 100644 --- a/cloud/cloudstack/cs_firewall.py +++ b/cloud/cloudstack/cs_firewall.py @@ -422,6 +422,13 @@ def main(): api_url = dict(default=None), api_http_method = dict(default='get'), ), + required_one_of = ( + ['ip_address', 'network'], + ), + required_together = ( + ['icmp_type', 'icmp_code'], + ['api_key', 'api_secret', 'api_url'], + ), mutually_exclusive = ( ['icmp_type', 'start_port'], ['icmp_type', 'end_port'], diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 1f5cc6ca393..c2c219febac 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -788,6 +788,9 @@ def main(): api_url = dict(default=None), api_http_method = dict(default='get'), ), + required_together = ( + ['api_key', 'api_secret', 'api_url'], + ), supports_check_mode=True ) diff --git a/cloud/cloudstack/cs_instancegroup.py b/cloud/cloudstack/cs_instancegroup.py index d62004cc94f..9041e351539 100644 --- a/cloud/cloudstack/cs_instancegroup.py +++ b/cloud/cloudstack/cs_instancegroup.py @@ -200,6 +200,9 @@ def main(): api_url = dict(default=None), api_http_method = dict(default='get'), ), + required_together = ( + ['api_key', 'api_secret', 'api_url'], + ), supports_check_mode=True ) diff --git a/cloud/cloudstack/cs_iso.py b/cloud/cloudstack/cs_iso.py index 749acdf594a..4a97fc3d027 100644 --- a/cloud/cloudstack/cs_iso.py +++ b/cloud/cloudstack/cs_iso.py @@ -333,6 +333,9 @@ def main(): api_url = dict(default=None), api_http_method = dict(default='get'), ), + required_together = ( + ['api_key', 'api_secret', 'api_url'], + ), supports_check_mode=True ) diff --git a/cloud/cloudstack/cs_portforward.py b/cloud/cloudstack/cs_portforward.py index 123da67e2bc..47af7848ee1 100644 --- a/cloud/cloudstack/cs_portforward.py +++ b/cloud/cloudstack/cs_portforward.py @@ -407,6 +407,9 @@ def main(): api_url = dict(default=None), api_http_method = dict(default='get'), ), + required_together = ( + ['api_key', 'api_secret', 'api_url'], + ), supports_check_mode=True ) diff --git a/cloud/cloudstack/cs_securitygroup.py b/cloud/cloudstack/cs_securitygroup.py index 73a54fef795..9ef81095322 100644 --- a/cloud/cloudstack/cs_securitygroup.py +++ b/cloud/cloudstack/cs_securitygroup.py @@ -167,6 +167,9 @@ def main(): api_url = dict(default=None), api_http_method = dict(default='get'), ), + required_together = ( + ['api_key', 'api_secret', 'api_url'], + ), supports_check_mode=True ) diff --git a/cloud/cloudstack/cs_securitygroup_rule.py b/cloud/cloudstack/cs_securitygroup_rule.py index ef48b3896ce..a467d3f5c38 100644 --- a/cloud/cloudstack/cs_securitygroup_rule.py +++ b/cloud/cloudstack/cs_securitygroup_rule.py @@ -402,6 +402,10 @@ def main(): api_url = dict(default=None), api_http_method = dict(default='get'), ), + required_together = ( + ['icmp_type', 'icmp_code'], + ['api_key', 'api_secret', 'api_url'], + ), mutually_exclusive = ( ['icmp_type', 'start_port'], ['icmp_type', 'end_port'], diff --git a/cloud/cloudstack/cs_sshkeypair.py b/cloud/cloudstack/cs_sshkeypair.py index 0d2e2c822f1..e7ee88e3bea 100644 --- a/cloud/cloudstack/cs_sshkeypair.py +++ b/cloud/cloudstack/cs_sshkeypair.py @@ -219,6 +219,9 @@ def main(): api_url = dict(default=None), api_http_method = dict(default='get'), ), + required_together = ( + ['api_key', 'api_secret', 'api_url'], + ), supports_check_mode=True ) diff --git a/cloud/cloudstack/cs_vmsnapshot.py b/cloud/cloudstack/cs_vmsnapshot.py index b71901a317f..cadf229af55 100644 --- a/cloud/cloudstack/cs_vmsnapshot.py +++ b/cloud/cloudstack/cs_vmsnapshot.py @@ -292,6 +292,10 @@ def main(): api_url = dict(default=None), api_http_method = dict(default='get'), ), + required_together = ( + ['icmp_type', 'icmp_code'], + ['api_key', 'api_secret', 'api_url'], + ), supports_check_mode=True ) From 7442db3f4160aebf0ac4241e38c89ba52e6a6f4e Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 30 May 2015 00:24:34 +0200 Subject: [PATCH 0182/2522] cs_instance: improve hypervisor argument and return --- cloud/cloudstack/cs_instance.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index c2c219febac..734ffb62d46 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -326,6 +326,11 @@ returned: success type: dict sample: '[ { "key": "foo", "value": "bar" } ]' +hypervisor: + description: Hypervisor related to this instance. + returned: success + type: string + sample: KVM ''' import base64 @@ -712,6 +717,8 @@ def get_result(self, instance): self.result['account'] = instance['account'] if 'project' in instance: self.result['project'] = instance['project'] + if 'hypervisor' in instance: + self.result['hypervisor'] = instance['hypervisor'] if 'publicip' in instance: self.result['public_ip'] = instance['public_ip'] if 'passwordenabled' in instance: @@ -771,7 +778,7 @@ def main(): disk_offering = dict(default=None), disk_size = dict(type='int', default=None), keyboard = dict(choices=['de', 'de-ch', 'es', 'fi', 'fr', 'fr-be', 'fr-ch', 'is', 'it', 'jp', 'nl-be', 'no', 'pt', 'uk', 'us'], default=None), - hypervisor = dict(default=None), + hypervisor = dict(choices=['KVM', 'VMware', 'BareMetal', 'XenServer', 'LXC', 'HyperV', 'UCS', 'OVM'], default=None), security_groups = dict(type='list', aliases=[ 'security_group' ], default=[]), affinity_groups = dict(type='list', aliases=[ 'affinity_group' ], default=[]), domain = dict(default=None), From a13a26aa2a6f52ae5da592933f2f33f2fe59c6b4 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 30 May 2015 00:26:00 +0200 Subject: [PATCH 0183/2522] cloudstack: add instance_name alias internal name to returns in cs_instance --- cloud/cloudstack/cs_instance.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 734ffb62d46..13fc57991d3 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -331,6 +331,11 @@ returned: success type: string sample: KVM +instance_name: + description: Internal name of the instance (ROOT admin only). + returned: success + type: string + sample: i-44-3992-VM ''' import base64 @@ -719,6 +724,8 @@ def get_result(self, instance): self.result['project'] = instance['project'] if 'hypervisor' in instance: self.result['hypervisor'] = instance['hypervisor'] + if 'instancename' in instance: + self.result['instance_name'] = instance['instancename'] if 'publicip' in instance: self.result['public_ip'] = instance['public_ip'] if 'passwordenabled' in instance: From e143689d9c3c086319c5038de3baf09dbc803c2f Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 30 May 2015 00:28:06 +0200 Subject: [PATCH 0184/2522] cloudstack: update doc in cs_instance --- cloud/cloudstack/cs_instance.py | 36 ++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 13fc57991d3..c2dd45fe2b5 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -23,7 +23,7 @@ module: cs_instance short_description: Manages instances and virtual machines on Apache CloudStack based clouds. description: - - Deploy, start, restart, stop and destroy instances on Apache CloudStack, Citrix CloudPlatform and Exoscale. + - Deploy, start, restart, stop and destroy instances. version_added: '2.0' author: '"René Moser (@resmo)" ' options: @@ -49,22 +49,29 @@ choices: [ 'deployed', 'started', 'stopped', 'restarted', 'destroyed', 'expunged', 'present', 'absent' ] service_offering: description: - - Name or id of the service offering of the new instance. If not set, first found service offering is used. + - Name or id of the service offering of the new instance. + - If not set, first found service offering is used. required: false default: null template: description: - - Name or id of the template to be used for creating the new instance. Required when using C(state=present). Mutually exclusive with C(ISO) option. + - Name or id of the template to be used for creating the new instance. + - Required when using C(state=present). + - Mutually exclusive with C(ISO) option. required: false default: null iso: description: - - Name or id of the ISO to be used for creating the new instance. Required when using C(state=present). Mutually exclusive with C(template) option. + - Name or id of the ISO to be used for creating the new instance. + - Required when using C(state=present). + - Mutually exclusive with C(template) option. required: false default: null hypervisor: description: - - Name the hypervisor to be used for creating the new instance. Relevant when using C(state=present) and option C(ISO) is used. If not set, first found hypervisor will be used. + - Name the hypervisor to be used for creating the new instance. + - Relevant when using C(state=present) and option C(ISO) is used. + - If not set, first found hypervisor will be used. required: false default: null choices: [ 'KVM', 'VMware', 'BareMetal', 'XenServer', 'LXC', 'HyperV', 'UCS', 'OVM' ] @@ -82,7 +89,7 @@ aliases: [ 'network' ] ip_address: description: - - IPv4 address for default instance's network during creation + - IPv4 address for default instance's network during creation. required: false default: null ip6_address: @@ -123,7 +130,8 @@ default: null zone: description: - - Name of the zone in which the instance shoud be deployed. If not set, default zone is used. + - Name of the zone in which the instance shoud be deployed. + - If not set, default zone is used. required: false default: null ssh_key: @@ -164,7 +172,7 @@ ''' EXAMPLES = ''' -# Create a instance on CloudStack from an ISO +# Create a instance from an ISO # NOTE: Names of offerings and ISOs depending on the CloudStack configuration. - local_action: module: cs_instance @@ -181,7 +189,6 @@ - Sync Integration - Storage Integration - # For changing a running instance, use the 'force' parameter - local_action: module: cs_instance @@ -191,7 +198,6 @@ service_offering: 2cpu_2gb force: yes - # Create or update a instance on Exoscale's public cloud - local_action: module: cs_instance @@ -202,19 +208,13 @@ tags: - { key: admin, value: john } - { key: foo, value: bar } - register: vm - -- debug: msg='default ip {{ vm.default_ip }} and is in state {{ vm.state }}' - # Ensure a instance has stopped - local_action: cs_instance name=web-vm-1 state=stopped - # Ensure a instance is running - local_action: cs_instance name=web-vm-1 state=started - # Remove a instance - local_action: cs_instance name=web-vm-1 state=absent ''' @@ -257,7 +257,7 @@ type: string sample: Ge2oe7Do ssh_key: - description: Name of ssh key deployed to instance. + description: Name of SSH key deployed to instance. returned: success type: string sample: key@work @@ -282,7 +282,7 @@ type: string sample: 10.23.37.42 public_ip: - description: Public IP address with instance via static nat rule. + description: Public IP address with instance via static NAT rule. returned: success type: string sample: 1.2.3.4 From 01caf84227afc7a100e64017bce380b86819fd18 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 30 May 2015 00:46:20 +0200 Subject: [PATCH 0185/2522] cloudstack: update doc of cs_portforward, fixes typos. --- cloud/cloudstack/cs_portforward.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/cloud/cloudstack/cs_portforward.py b/cloud/cloudstack/cs_portforward.py index 47af7848ee1..cbd363f69e6 100644 --- a/cloud/cloudstack/cs_portforward.py +++ b/cloud/cloudstack/cs_portforward.py @@ -92,12 +92,13 @@ default: null project: description: - - Name of the project the c(vm) is located in. + - Name of the project the C(vm) is located in. required: false default: null zone: description: - - Name of the zone in which the virtual machine is in. If not set, default zone is used. + - Name of the zone in which the virtual machine is in. + - If not set, default zone is used. required: false default: null poll_async: @@ -117,7 +118,6 @@ public_port: 80 private_port: 8080 - # forward SSH and open firewall - local_action: module: cs_portforward @@ -127,7 +127,6 @@ private_port: 22 open_firewall: true - # forward DNS traffic, but do not open firewall - local_action: module: cs_portforward @@ -138,7 +137,6 @@ protocol: udp open_firewall: true - # remove ssh port forwarding - local_action: module: cs_portforward @@ -161,26 +159,26 @@ type: string sample: tcp private_port: - description: Private start port. + description: Start port on the virtual machine's IP address. returned: success type: int sample: 80 private_end_port: - description: Private end port. + description: End port on the virtual machine's IP address. returned: success type: int public_port: - description: Public start port. + description: Start port on the public IP address. returned: success type: int sample: 80 public_end_port: - description: Public end port. + description: End port on the public IP address. returned: success type: int sample: 80 tags: - description: Tag srelated to the port forwarding. + description: Tags related to the port forwarding. returned: success type: list sample: [] @@ -201,7 +199,6 @@ sample: 10.101.65.152 ''' - try: from cs import CloudStack, CloudStackException, read_config has_lib_cs = True From 80663e0fbeaf081d25b3d5691e2bc8f12b86ac8c Mon Sep 17 00:00:00 2001 From: mlamatr Date: Fri, 29 May 2015 23:18:44 -0400 Subject: [PATCH 0186/2522] corrected typo in URL for consul.io --- clustering/consul.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clustering/consul.py b/clustering/consul.py index 0baaae83b84..8423ffe418f 100644 --- a/clustering/consul.py +++ b/clustering/consul.py @@ -20,7 +20,7 @@ DOCUMENTATION = """ module: consul short_description: "Add, modify & delete services within a consul cluster. - See http://conul.io for more details." + See http://consul.io for more details." description: - registers services and checks for an agent with a consul cluster. A service is some process running on the agent node that should be advertised by From eb66f683f538c1fe73902e246de7e8d3bec29c5d Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 30 May 2015 11:03:32 +0200 Subject: [PATCH 0187/2522] cloudstack: add new param api_timeout --- cloud/cloudstack/cs_account.py | 1 + cloud/cloudstack/cs_affinitygroup.py | 1 + cloud/cloudstack/cs_firewall.py | 1 + cloud/cloudstack/cs_instance.py | 1 + cloud/cloudstack/cs_instancegroup.py | 1 + cloud/cloudstack/cs_iso.py | 1 + cloud/cloudstack/cs_portforward.py | 1 + cloud/cloudstack/cs_securitygroup.py | 1 + cloud/cloudstack/cs_securitygroup_rule.py | 1 + cloud/cloudstack/cs_sshkeypair.py | 1 + cloud/cloudstack/cs_vmsnapshot.py | 1 + 11 files changed, 11 insertions(+) diff --git a/cloud/cloudstack/cs_account.py b/cloud/cloudstack/cs_account.py index a8510bbc5b3..dc845acbae2 100644 --- a/cloud/cloudstack/cs_account.py +++ b/cloud/cloudstack/cs_account.py @@ -368,6 +368,7 @@ def main(): api_secret = dict(default=None, no_log=True), api_url = dict(default=None), api_http_method = dict(default='get'), + api_timeout = dict(type='int', default=10), ), required_together = ( ['api_key', 'api_secret', 'api_url'], diff --git a/cloud/cloudstack/cs_affinitygroup.py b/cloud/cloudstack/cs_affinitygroup.py index 9ff3b123a0c..afb60a83baa 100644 --- a/cloud/cloudstack/cs_affinitygroup.py +++ b/cloud/cloudstack/cs_affinitygroup.py @@ -222,6 +222,7 @@ def main(): api_secret = dict(default=None, no_log=True), api_url = dict(default=None), api_http_method = dict(default='get'), + api_timeout = dict(type='int', default=10), ), required_together = ( ['api_key', 'api_secret', 'api_url'], diff --git a/cloud/cloudstack/cs_firewall.py b/cloud/cloudstack/cs_firewall.py index ef78b6a242d..fca8e88a509 100644 --- a/cloud/cloudstack/cs_firewall.py +++ b/cloud/cloudstack/cs_firewall.py @@ -421,6 +421,7 @@ def main(): api_secret = dict(default=None, no_log=True), api_url = dict(default=None), api_http_method = dict(default='get'), + api_timeout = dict(type='int', default=10), ), required_one_of = ( ['ip_address', 'network'], diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index c2dd45fe2b5..b6f2d098346 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -801,6 +801,7 @@ def main(): api_secret = dict(default=None, no_log=True), api_url = dict(default=None), api_http_method = dict(default='get'), + api_timeout = dict(type='int', default=10), ), required_together = ( ['api_key', 'api_secret', 'api_url'], diff --git a/cloud/cloudstack/cs_instancegroup.py b/cloud/cloudstack/cs_instancegroup.py index 9041e351539..01630bc225f 100644 --- a/cloud/cloudstack/cs_instancegroup.py +++ b/cloud/cloudstack/cs_instancegroup.py @@ -199,6 +199,7 @@ def main(): api_secret = dict(default=None, no_log=True), api_url = dict(default=None), api_http_method = dict(default='get'), + api_timeout = dict(type='int', default=10), ), required_together = ( ['api_key', 'api_secret', 'api_url'], diff --git a/cloud/cloudstack/cs_iso.py b/cloud/cloudstack/cs_iso.py index 4a97fc3d027..f38faeceeb4 100644 --- a/cloud/cloudstack/cs_iso.py +++ b/cloud/cloudstack/cs_iso.py @@ -332,6 +332,7 @@ def main(): api_secret = dict(default=None, no_log=True), api_url = dict(default=None), api_http_method = dict(default='get'), + api_timeout = dict(type='int', default=10), ), required_together = ( ['api_key', 'api_secret', 'api_url'], diff --git a/cloud/cloudstack/cs_portforward.py b/cloud/cloudstack/cs_portforward.py index cbd363f69e6..e3a456e424b 100644 --- a/cloud/cloudstack/cs_portforward.py +++ b/cloud/cloudstack/cs_portforward.py @@ -403,6 +403,7 @@ def main(): api_secret = dict(default=None, no_log=True), api_url = dict(default=None), api_http_method = dict(default='get'), + api_timeout = dict(type='int', default=10), ), required_together = ( ['api_key', 'api_secret', 'api_url'], diff --git a/cloud/cloudstack/cs_securitygroup.py b/cloud/cloudstack/cs_securitygroup.py index 9ef81095322..8f1592ca43a 100644 --- a/cloud/cloudstack/cs_securitygroup.py +++ b/cloud/cloudstack/cs_securitygroup.py @@ -166,6 +166,7 @@ def main(): api_secret = dict(default=None, no_log=True), api_url = dict(default=None), api_http_method = dict(default='get'), + api_timeout = dict(type='int', default=10), ), required_together = ( ['api_key', 'api_secret', 'api_url'], diff --git a/cloud/cloudstack/cs_securitygroup_rule.py b/cloud/cloudstack/cs_securitygroup_rule.py index a467d3f5c38..7afb1463503 100644 --- a/cloud/cloudstack/cs_securitygroup_rule.py +++ b/cloud/cloudstack/cs_securitygroup_rule.py @@ -401,6 +401,7 @@ def main(): api_secret = dict(default=None, no_log=True), api_url = dict(default=None), api_http_method = dict(default='get'), + api_timeout = dict(type='int', default=10), ), required_together = ( ['icmp_type', 'icmp_code'], diff --git a/cloud/cloudstack/cs_sshkeypair.py b/cloud/cloudstack/cs_sshkeypair.py index e7ee88e3bea..b4b764dbe33 100644 --- a/cloud/cloudstack/cs_sshkeypair.py +++ b/cloud/cloudstack/cs_sshkeypair.py @@ -218,6 +218,7 @@ def main(): api_secret = dict(default=None, no_log=True), api_url = dict(default=None), api_http_method = dict(default='get'), + api_timeout = dict(type='int', default=10), ), required_together = ( ['api_key', 'api_secret', 'api_url'], diff --git a/cloud/cloudstack/cs_vmsnapshot.py b/cloud/cloudstack/cs_vmsnapshot.py index cadf229af55..218a947ac5a 100644 --- a/cloud/cloudstack/cs_vmsnapshot.py +++ b/cloud/cloudstack/cs_vmsnapshot.py @@ -291,6 +291,7 @@ def main(): api_secret = dict(default=None, no_log=True), api_url = dict(default=None), api_http_method = dict(default='get'), + api_timeout = dict(type='int', default=10), ), required_together = ( ['icmp_type', 'icmp_code'], From 53130de66267068b6dc607f4a44200eee1581f96 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 30 May 2015 11:05:03 +0200 Subject: [PATCH 0188/2522] cloudstack: add choices for api_http_method --- cloud/cloudstack/cs_account.py | 6 +----- cloud/cloudstack/cs_affinitygroup.py | 3 +-- cloud/cloudstack/cs_firewall.py | 6 +----- cloud/cloudstack/cs_instance.py | 2 +- cloud/cloudstack/cs_instancegroup.py | 3 +-- cloud/cloudstack/cs_iso.py | 5 +---- cloud/cloudstack/cs_portforward.py | 2 +- cloud/cloudstack/cs_securitygroup.py | 3 +-- cloud/cloudstack/cs_securitygroup_rule.py | 6 +----- cloud/cloudstack/cs_sshkeypair.py | 2 +- cloud/cloudstack/cs_vmsnapshot.py | 4 +--- 11 files changed, 11 insertions(+), 31 deletions(-) diff --git a/cloud/cloudstack/cs_account.py b/cloud/cloudstack/cs_account.py index dc845acbae2..597e4c7394e 100644 --- a/cloud/cloudstack/cs_account.py +++ b/cloud/cloudstack/cs_account.py @@ -108,7 +108,6 @@ email: john.doe@example.com domain: CUSTOMERS - # Lock an existing account in domain 'CUSTOMERS' local_action: module: cs_account @@ -116,7 +115,6 @@ domain: CUSTOMERS state: locked - # Disable an existing account in domain 'CUSTOMERS' local_action: module: cs_account @@ -124,7 +122,6 @@ domain: CUSTOMERS state: disabled - # Enable an existing account in domain 'CUSTOMERS' local_action: module: cs_account @@ -132,7 +129,6 @@ domain: CUSTOMERS state: enabled - # Remove an account in domain 'CUSTOMERS' local_action: module: cs_account @@ -367,7 +363,7 @@ def main(): api_key = dict(default=None), api_secret = dict(default=None, no_log=True), api_url = dict(default=None), - api_http_method = dict(default='get'), + api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), ), required_together = ( diff --git a/cloud/cloudstack/cs_affinitygroup.py b/cloud/cloudstack/cs_affinitygroup.py index afb60a83baa..40896942cb1 100644 --- a/cloud/cloudstack/cs_affinitygroup.py +++ b/cloud/cloudstack/cs_affinitygroup.py @@ -72,7 +72,6 @@ name: haproxy affinty_type: host anti-affinity - # Remove a affinity group - local_action: module: cs_affinitygroup @@ -221,7 +220,7 @@ def main(): api_key = dict(default=None), api_secret = dict(default=None, no_log=True), api_url = dict(default=None), - api_http_method = dict(default='get'), + api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), ), required_together = ( diff --git a/cloud/cloudstack/cs_firewall.py b/cloud/cloudstack/cs_firewall.py index fca8e88a509..828aa1faf98 100644 --- a/cloud/cloudstack/cs_firewall.py +++ b/cloud/cloudstack/cs_firewall.py @@ -115,7 +115,6 @@ port: 80 cidr: 1.2.3.4/32 - # Allow inbound tcp/udp port 53 to 4.3.2.1 - local_action: module: cs_firewall @@ -126,7 +125,6 @@ - tcp - udp - # Ensure firewall rule is removed - local_action: module: cs_firewall @@ -136,7 +134,6 @@ cidr: 17.0.0.0/8 state: absent - # Allow all outbound traffic - local_action: module: cs_firewall @@ -144,7 +141,6 @@ type: egress protocol: all - # Allow only HTTP outbound traffic for an IP - local_action: module: cs_firewall @@ -420,7 +416,7 @@ def main(): api_key = dict(default=None), api_secret = dict(default=None, no_log=True), api_url = dict(default=None), - api_http_method = dict(default='get'), + api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), ), required_one_of = ( diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index b6f2d098346..05cdc960e95 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -800,7 +800,7 @@ def main(): api_key = dict(default=None), api_secret = dict(default=None, no_log=True), api_url = dict(default=None), - api_http_method = dict(default='get'), + api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), ), required_together = ( diff --git a/cloud/cloudstack/cs_instancegroup.py b/cloud/cloudstack/cs_instancegroup.py index 01630bc225f..396cafa388d 100644 --- a/cloud/cloudstack/cs_instancegroup.py +++ b/cloud/cloudstack/cs_instancegroup.py @@ -61,7 +61,6 @@ module: cs_instancegroup name: loadbalancers - # Remove an instance group - local_action: module: cs_instancegroup @@ -198,7 +197,7 @@ def main(): api_key = dict(default=None), api_secret = dict(default=None, no_log=True), api_url = dict(default=None), - api_http_method = dict(default='get'), + api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), ), required_together = ( diff --git a/cloud/cloudstack/cs_iso.py b/cloud/cloudstack/cs_iso.py index f38faeceeb4..77ce85b505e 100644 --- a/cloud/cloudstack/cs_iso.py +++ b/cloud/cloudstack/cs_iso.py @@ -116,7 +116,6 @@ url: http://mirror.switch.ch/ftp/mirror/debian-cd/current/amd64/iso-cd/debian-7.7.0-amd64-netinst.iso os_type: Debian GNU/Linux 7(64-bit) - # Register an ISO with given name if ISO md5 checksum does not already exist. - local_action: module: cs_iso @@ -125,14 +124,12 @@ os_type: checksum: 0b31bccccb048d20b551f70830bb7ad0 - # Remove an ISO by name - local_action: module: cs_iso name: Debian 7 64-bit state: absent - # Remove an ISO by checksum - local_action: module: cs_iso @@ -331,7 +328,7 @@ def main(): api_key = dict(default=None), api_secret = dict(default=None, no_log=True), api_url = dict(default=None), - api_http_method = dict(default='get'), + api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), ), required_together = ( diff --git a/cloud/cloudstack/cs_portforward.py b/cloud/cloudstack/cs_portforward.py index e3a456e424b..00b084d9195 100644 --- a/cloud/cloudstack/cs_portforward.py +++ b/cloud/cloudstack/cs_portforward.py @@ -402,7 +402,7 @@ def main(): api_key = dict(default=None), api_secret = dict(default=None, no_log=True), api_url = dict(default=None), - api_http_method = dict(default='get'), + api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), ), required_together = ( diff --git a/cloud/cloudstack/cs_securitygroup.py b/cloud/cloudstack/cs_securitygroup.py index 8f1592ca43a..08fb72c821d 100644 --- a/cloud/cloudstack/cs_securitygroup.py +++ b/cloud/cloudstack/cs_securitygroup.py @@ -57,7 +57,6 @@ name: default description: default security group - # Remove a security group - local_action: module: cs_securitygroup @@ -165,7 +164,7 @@ def main(): api_key = dict(default=None), api_secret = dict(default=None, no_log=True), api_url = dict(default=None), - api_http_method = dict(default='get'), + api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), ), required_together = ( diff --git a/cloud/cloudstack/cs_securitygroup_rule.py b/cloud/cloudstack/cs_securitygroup_rule.py index 7afb1463503..9252e06ce62 100644 --- a/cloud/cloudstack/cs_securitygroup_rule.py +++ b/cloud/cloudstack/cs_securitygroup_rule.py @@ -102,7 +102,6 @@ port: 80 cidr: 1.2.3.4/32 - # Allow tcp/udp outbound added to security group 'default' - local_action: module: cs_securitygroup_rule @@ -115,7 +114,6 @@ - tcp - udp - # Allow inbound icmp from 0.0.0.0/0 added to security group 'default' - local_action: module: cs_securitygroup_rule @@ -124,7 +122,6 @@ icmp_code: -1 icmp_type: -1 - # Remove rule inbound port 80/tcp from 0.0.0.0/0 from security group 'default' - local_action: module: cs_securitygroup_rule @@ -132,7 +129,6 @@ port: 80 state: absent - # Allow inbound port 80/tcp from security group web added to security group 'default' - local_action: module: cs_securitygroup_rule @@ -400,7 +396,7 @@ def main(): api_key = dict(default=None), api_secret = dict(default=None, no_log=True), api_url = dict(default=None), - api_http_method = dict(default='get'), + api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), ), required_together = ( diff --git a/cloud/cloudstack/cs_sshkeypair.py b/cloud/cloudstack/cs_sshkeypair.py index b4b764dbe33..0a54a1971bc 100644 --- a/cloud/cloudstack/cs_sshkeypair.py +++ b/cloud/cloudstack/cs_sshkeypair.py @@ -217,7 +217,7 @@ def main(): api_key = dict(default=None), api_secret = dict(default=None, no_log=True), api_url = dict(default=None), - api_http_method = dict(default='get'), + api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), ), required_together = ( diff --git a/cloud/cloudstack/cs_vmsnapshot.py b/cloud/cloudstack/cs_vmsnapshot.py index 218a947ac5a..fb7668640dc 100644 --- a/cloud/cloudstack/cs_vmsnapshot.py +++ b/cloud/cloudstack/cs_vmsnapshot.py @@ -88,7 +88,6 @@ vm: web-01 snapshot_memory: yes - # Revert a VM to a snapshot after a failed upgrade - local_action: module: cs_vmsnapshot @@ -96,7 +95,6 @@ vm: web-01 state: revert - # Remove a VM snapshot after successful upgrade - local_action: module: cs_vmsnapshot @@ -290,7 +288,7 @@ def main(): api_key = dict(default=None), api_secret = dict(default=None, no_log=True), api_url = dict(default=None), - api_http_method = dict(default='get'), + api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), ), required_together = ( From 71066331f31c8619f0bdcf8029e56a27979644c6 Mon Sep 17 00:00:00 2001 From: Q Date: Sat, 30 May 2015 23:01:52 +1000 Subject: [PATCH 0189/2522] Update patch.py --- files/patch.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/files/patch.py b/files/patch.py index c2982e2380e..0932ed3556a 100644 --- a/files/patch.py +++ b/files/patch.py @@ -65,6 +65,13 @@ required: false type: "int" default: "0" + backup_copy: + description: + - passes --backup --version-control=numbered to patch, + producing numbered backup copies + required: false + type: "bool" + default: "False" note: - This module requires GNU I(patch) utility to be installed on the remote host. ''' @@ -101,7 +108,7 @@ def is_already_applied(patch_func, patch_file, basedir, dest_file=None, strip=0) return rc == 0 -def apply_patch(patch_func, patch_file, basedir, dest_file=None, strip=0, dry_run=False): +def apply_patch(patch_func, patch_file, basedir, dest_file=None, strip=0, dry_run=False, backup=False): opts = ['--quiet', '--forward', '--batch', '--reject-file=-', "--strip=%s" % strip, "--directory='%s'" % basedir, "--input='%s'" % patch_file] @@ -109,6 +116,8 @@ def apply_patch(patch_func, patch_file, basedir, dest_file=None, strip=0, dry_ru opts.append('--dry-run') if dest_file: opts.append("'%s'" % dest_file) + if backup: + opts.append('--backup --version-control=numbered') (rc, out, err) = patch_func(opts) if rc != 0: @@ -124,6 +133,8 @@ def main(): 'basedir': {}, 'strip': {'default': 0, 'type': 'int'}, 'remote_src': {'default': False, 'type': 'bool'}, + # don't call it "backup" since the semantics differs from the default one + 'backup_copy': { 'default': False, 'type': 'bool' } }, required_one_of=[['dest', 'basedir']], supports_check_mode=True @@ -156,8 +167,8 @@ def main(): changed = False if not is_already_applied(patch_func, p.src, p.basedir, dest_file=p.dest, strip=p.strip): try: - apply_patch(patch_func, p.src, p.basedir, dest_file=p.dest, strip=p.strip, - dry_run=module.check_mode) + apply_patch( patch_func, p.src, p.basedir, dest_file=p.dest, strip=p.strip, + dry_run=module.check_mode, backup=p.backup_copy ) changed = True except PatchError, e: module.fail_json(msg=str(e)) From 79a5ea2ca61ffb9ec6dec0113c1cc004f935189a Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 30 May 2015 11:05:36 +0200 Subject: [PATCH 0190/2522] cloudstack: fix examples in cs_iso --- cloud/cloudstack/cs_iso.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/cloudstack/cs_iso.py b/cloud/cloudstack/cs_iso.py index 77ce85b505e..d9ec6880627 100644 --- a/cloud/cloudstack/cs_iso.py +++ b/cloud/cloudstack/cs_iso.py @@ -121,7 +121,7 @@ module: cs_iso name: Debian 7 64-bit url: http://mirror.switch.ch/ftp/mirror/debian-cd/current/amd64/iso-cd/debian-7.7.0-amd64-netinst.iso - os_type: + os_type: Debian GNU/Linux 7(64-bit) checksum: 0b31bccccb048d20b551f70830bb7ad0 # Remove an ISO by name From 421b3ff24ebe240054f172d396c869f51cf08ae1 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 30 May 2015 18:28:41 +0200 Subject: [PATCH 0191/2522] cloudstack: fix doc for cs_instance, force is defaulted to false --- cloud/cloudstack/cs_instance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 05cdc960e95..46fd66f510d 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -156,7 +156,7 @@ description: - Force stop/start the instance if required to apply changes, otherwise a running instance will not be changed. required: false - default: true + default: false tags: description: - List of tags. Tags are a list of dictionaries having keys C(key) and C(value). From e1006eb9077baad7d8c79b32608254269b2fd4ad Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 30 May 2015 22:54:56 +0200 Subject: [PATCH 0192/2522] cloudstack: add new module cs_project --- cloud/cloudstack/cs_project.py | 342 +++++++++++++++++++++++++++++++++ 1 file changed, 342 insertions(+) create mode 100644 cloud/cloudstack/cs_project.py diff --git a/cloud/cloudstack/cs_project.py b/cloud/cloudstack/cs_project.py new file mode 100644 index 00000000000..b604a1b6f32 --- /dev/null +++ b/cloud/cloudstack/cs_project.py @@ -0,0 +1,342 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2015, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: cs_project +short_description: Manages projects on Apache CloudStack based clouds. +description: + - Create, update, suspend, activate and remove projects. +version_added: '2.0' +author: '"René Moser (@resmo)" ' + name: + description: + - Name of the project. + required: true + displaytext: + description: + - Displaytext of the project. + - If not specified, C(name) will be used as displaytext. + required: false + default: null + state: + description: + - State of the project. + required: false + default: 'present' + choices: [ 'present', 'absent', 'active', 'suspended' ] + domain: + description: + - Domain the project is related to. + required: false + default: null + account: + description: + - Account the project is related to. + required: false + default: null + poll_async: + description: + - Poll async jobs until job has finished. + required: false + default: true +extends_documentation_fragment: cloudstack +''' + +EXAMPLES = ''' +# Create a project +- local_action: + module: cs_project + name: web + +# Rename a project +- local_action: + module: cs_project + name: web + displaytext: my web project + +# Suspend an existing project +- local_action: + module: cs_project + name: web + state: suspended + +# Activate an existing project +- local_action: + module: cs_project + name: web + state: active + +# Remove a project +- local_action: + module: cs_project + name: web + state: absent +''' + +RETURN = ''' +--- +id: + description: ID of the project. + returned: success + type: string + sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6 +name: + description: Name of the project. + returned: success + type: string + sample: web project +displaytext: + description: Display text of the project. + returned: success + type: string + sample: web project +state: + description: State of the project. + returned: success + type: string + sample: Active +domain: + description: Domain the project is related to. + returned: success + type: string + sample: example domain +account: + description: Account the project is related to. + returned: success + type: string + sample: example account +tags: + description: List of resource tags associated with the project. + returned: success + type: dict + sample: '[ { "key": "foo", "value": "bar" } ]' +''' + +try: + from cs import CloudStack, CloudStackException, read_config + has_lib_cs = True +except ImportError: + has_lib_cs = False + +# import cloudstack common +from ansible.module_utils.cloudstack import * + + +class AnsibleCloudStackProject(AnsibleCloudStack): + + def __init__(self, module): + AnsibleCloudStack.__init__(self, module) + self.project = None + + + def get_displaytext(self): + displaytext = self.module.params.get('displaytext') + if not displaytext: + displaytext = self.module.params.get('name') + return displaytext + + + def get_project(self): + if not self.project: + project = self.module.params.get('name') + + args = {} + args['listall'] = True + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') + + projects = self.cs.listProjects(**args) + if projects: + for p in projects['project']: + if project in [ p['name'], p['id']]: + self.project = p + break + return self.project + + + def present_project(self): + project = self.get_project() + if not project: + project = self.create_project(project) + else: + project = self.update_project(project) + return project + + + def update_project(self, project): + args = {} + args['id'] = project['id'] + args['displaytext'] = self.get_displaytext() + + if self._has_changed(args, project): + self.result['changed'] = True + if not self.module.check_mode: + project = self.cs.updateProject(**args) + + if 'errortext' in project: + self.module.fail_json(msg="Failed: '%s'" % project['errortext']) + + poll_async = self.module.params.get('poll_async') + if project and poll_async: + project = self._poll_job(project, 'project') + return project + + + def create_project(self, project): + self.result['changed'] = True + + args = {} + args['name'] = self.module.params.get('name') + args['displaytext'] = self.get_displaytext() + args['account'] = self.get_account('name') + args['domainid'] = self.get_domain('id') + + if not self.module.check_mode: + project = self.cs.createProject(**args) + + if 'errortext' in project: + self.module.fail_json(msg="Failed: '%s'" % project['errortext']) + + poll_async = self.module.params.get('poll_async') + if project and poll_async: + project = self._poll_job(project, 'project') + return project + + + def state_project(self, state=None): + project = self.get_project() + + if not project: + self.module.fail_json(msg="No project named '%s' found." % self.module.params('name')) + + if project['state'].lower() != state: + self.result['changed'] = True + + args = {} + args['id'] = project['id'] + + if not self.module.check_mode: + if state == 'suspended': + project = self.cs.suspendProject(**args) + else: + project = self.cs.activateProject(**args) + + if 'errortext' in project: + self.module.fail_json(msg="Failed: '%s'" % project['errortext']) + + poll_async = self.module.params.get('poll_async') + if project and poll_async: + project = self._poll_job(project, 'project') + return project + + + def absent_project(self): + project = self.get_project() + if project: + self.result['changed'] = True + + args = {} + args['id'] = project['id'] + + if not self.module.check_mode: + res = self.cs.deleteProject(**args) + + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + + poll_async = self.module.params.get('poll_async') + if res and poll_async: + res = self._poll_job(res, 'project') + return project + + + def get_result(self, project): + if project: + if 'name' in project: + self.result['name'] = project['name'] + if 'displaytext' in project: + self.result['displaytext'] = project['displaytext'] + if 'account' in project: + self.result['account'] = project['account'] + if 'domain' in project: + self.result['domain'] = project['domain'] + if 'state' in project: + self.result['state'] = project['state'] + if 'tags' in project: + self.result['tags'] = [] + for tag in project['tags']: + result_tag = {} + result_tag['key'] = tag['key'] + result_tag['value'] = tag['value'] + self.result['tags'].append(result_tag) + return self.result + + +def main(): + module = AnsibleModule( + argument_spec = dict( + name = dict(required=True), + displaytext = dict(default=None), + state = dict(choices=['present', 'absent', 'active', 'suspended' ], default='present'), + domain = dict(default=None), + account = dict(default=None), + poll_async = dict(type='bool', choices=BOOLEANS, default=True), + api_key = dict(default=None), + api_secret = dict(default=None, no_log=True), + api_url = dict(default=None), + api_http_method = dict(choices=['get', 'post'], default='get'), + api_timeout = dict(type='int', default=10), + ), + required_together = ( + ['api_key', 'api_secret', 'api_url'], + ), + supports_check_mode=True + ) + + if not has_lib_cs: + module.fail_json(msg="python library cs required: pip install cs") + + try: + acs_project = AnsibleCloudStackProject(module) + + state = module.params.get('state') + if state in ['absent']: + project = acs_project.absent_project() + + elif state in ['active', 'suspended']: + project = acs_project.state_project(state=state) + + else: + project = acs_project.present_project() + + result = acs_project.get_result(project) + + except CloudStackException, e: + module.fail_json(msg='CloudStackException: %s' % str(e)) + + except Exception, e: + module.fail_json(msg='Exception: %s' % str(e)) + + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * +main() From 1bc3e10e77b565d5e231f25d8f0be205436fd6cd Mon Sep 17 00:00:00 2001 From: fdupoux Date: Sun, 31 May 2015 12:38:45 +0100 Subject: [PATCH 0193/2522] Devices in the current_devs list must also be converted to absolute device paths so comparison with dev_list works --- system/lvg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/lvg.py b/system/lvg.py index 955b94668dc..3c6c5ef2930 100644 --- a/system/lvg.py +++ b/system/lvg.py @@ -211,7 +211,7 @@ def main(): module.fail_json(msg="Refuse to remove non-empty volume group %s without force=yes"%(vg)) ### resize VG - current_devs = [ pv['name'] for pv in pvs if pv['vg_name'] == vg ] + current_devs = [ os.path.realpath(pv['name']) for pv in pvs if pv['vg_name'] == vg ] devs_to_remove = list(set(current_devs) - set(dev_list)) devs_to_add = list(set(dev_list) - set(current_devs)) From a77de166c4621a39316ca35fe35c8fc6c2a28e0f Mon Sep 17 00:00:00 2001 From: Greg DeKoenigsberg Date: Mon, 1 Jun 2015 08:59:50 -0400 Subject: [PATCH 0194/2522] Add new policy guidelines for Extras More to do here, but this is a start. --- CONTRIBUTING.md | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e441a4e3527..38b95840a77 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,28 +1,37 @@ -Welcome To Ansible GitHub -========================= +Contributing to ansible-modules-extras +====================================== -Hi! Nice to see you here! +The Ansible Extras Modules are written and maintained by the Ansible community, according to the following contribution guidelines. + +If you'd like to contribute code +================================ + +Please see [this web page](http://docs.ansible.com/community.html) for information about the contribution process. Important license agreement information is also included on that page. + +If you'd like to contribute code to an existing module +====================================================== +Each module in Extras is maintained by the owner of that module; each module's owner is indicated in the documentation section of the module itself. Any pull request for a module that is given a +1 by the owner in the comments will be merged by the Ansible team. + +If you'd like to contribute a new module +======================================== +Ansible welcomes new modules. Please be certain that you've read the [module development guide and standards](http://docs.ansible.com/developing_modules.html) thoroughly before submitting your module. + +Each new module requires two current module owners to approve a new module for inclusion. The Ansible community reviews new modules as often as possible, but please be patient; there are a lot of new module submissions in the pipeline, and it takes time to evaluate a new module for its adherence to module standards. + +Once your module is accepted, you become responsible for maintenance of that module, which means responding to pull requests and issues in a reasonably timely manner. If you'd like to ask a question =============================== Please see [this web page ](http://docs.ansible.com/community.html) for community information, which includes pointers on how to ask questions on the [mailing lists](http://docs.ansible.com/community.html#mailing-list-information) and IRC. -The github issue tracker is not the best place for questions for various reasons, but both IRC and the mailing list are very helpful places for those things, and that page has the pointers to those. - -If you'd like to contribute code -================================ - -Please see [this web page](http://docs.ansible.com/community.html) for information about the contribution process. Important license agreement information is also included on that page. +The Github issue tracker is not the best place for questions for various reasons, but both IRC and the mailing list are very helpful places for those things, and that page has the pointers to those. If you'd like to file a bug =========================== -I'd also read the community page above, but in particular, make sure you copy [this issue template](https://github.com/ansible/ansible/blob/devel/ISSUE_TEMPLATE.md) into your ticket description. We have a friendly neighborhood bot that will remind you if you forget :) This template helps us organize tickets faster and prevents asking some repeated questions, so it's very helpful to us and we appreciate your help with it. +Read the community page above, but in particular, make sure you copy [this issue template](https://github.com/ansible/ansible/blob/devel/ISSUE_TEMPLATE.md) into your ticket description. We have a friendly neighborhood bot that will remind you if you forget :) This template helps us organize tickets faster and prevents asking some repeated questions, so it's very helpful to us and we appreciate your help with it. Also please make sure you are testing on the latest released version of Ansible or the development branch. Thanks! - - - From 432477c14c0a34b01d6e8ee959b6bbf44156cfc2 Mon Sep 17 00:00:00 2001 From: Greg DeKoenigsberg Date: Mon, 1 Jun 2015 12:07:23 -0400 Subject: [PATCH 0195/2522] Revert "Added eval for pasting tag lists" --- monitoring/datadog_event.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/monitoring/datadog_event.py b/monitoring/datadog_event.py index bde5cd80069..5319fcb0f1b 100644 --- a/monitoring/datadog_event.py +++ b/monitoring/datadog_event.py @@ -116,10 +116,7 @@ def post_event(module): if module.params['date_happened'] != None: body['date_happened'] = module.params['date_happened'] if module.params['tags'] != None: - if module.params['tags'].startswith("[") and module.params['tags'].endswith("]"): - body['tags'] = eval(module.params['tags']) - else: - body['tags'] = module.params['tags'].split(",") + body['tags'] = module.params['tags'].split(",") if module.params['aggregation_key'] != None: body['aggregation_key'] = module.params['aggregation_key'] if module.params['source_type_name'] != None: From 04e43a9dcb52d590c3d7e4a18170bea4b315c2a6 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 1 Jun 2015 12:31:20 -0400 Subject: [PATCH 0196/2522] added version added --- monitoring/nagios.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monitoring/nagios.py b/monitoring/nagios.py index 5fd51d17123..38a1f8c161a 100644 --- a/monitoring/nagios.py +++ b/monitoring/nagios.py @@ -53,6 +53,7 @@ required: false default: Ansible comment: + version_added: "2.0" description: - Comment for C(downtime) action. required: false From d5c581e9ebcf1d2c25baddfb9f845c5357677b21 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 1 Jun 2015 12:36:49 -0400 Subject: [PATCH 0197/2522] updated docs for 2.0 --- monitoring/nagios.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monitoring/nagios.py b/monitoring/nagios.py index 38a1f8c161a..543f094b70e 100644 --- a/monitoring/nagios.py +++ b/monitoring/nagios.py @@ -30,6 +30,7 @@ action: description: - Action to take. + - servicegroup options were added in 2.0. required: true default: null choices: [ "downtime", "enable_alerts", "disable_alerts", "silence", "unsilence", @@ -73,6 +74,7 @@ required: true default: null servicegroup: + version_added: "2.0" description: - the Servicegroup we want to set downtimes/alerts for. B(Required) option when using the C(servicegroup_service_downtime) amd C(servicegroup_host_downtime). From 01551a8c15129a48100e1e3707e285ead061a2a9 Mon Sep 17 00:00:00 2001 From: David Wittman Date: Tue, 21 Oct 2014 16:56:13 -0500 Subject: [PATCH 0198/2522] [lvol] Add opts parameter Adds the ability to set options to be passed to the lvcreate command using the `opts` parameter. --- system/lvol.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/system/lvol.py b/system/lvol.py index c49cb369440..d807f9e8336 100644 --- a/system/lvol.py +++ b/system/lvol.py @@ -57,6 +57,10 @@ - Shrink or remove operations of volumes requires this switch. Ensures that that filesystems get never corrupted/destroyed by mistake. required: false + opts: + version_added: "1.9" + description: + - Free-form options to be passed to the lvcreate command notes: - Filesystems on top of the volume are not resized. ''' @@ -71,6 +75,9 @@ # Create a logical volume the size of all remaining space in the volume group - lvol: vg=firefly lv=test size=100%FREE +# Create a logical volume with special options +- lvol: vg=firefly lv=test size=512g opts="-r 16" + # Extend the logical volume to 1024m. - lvol: vg=firefly lv=test size=1024 @@ -116,6 +123,7 @@ def main(): vg=dict(required=True), lv=dict(required=True), size=dict(), + opts=dict(type='str'), state=dict(choices=["absent", "present"], default='present'), force=dict(type='bool', default='no'), ), @@ -135,11 +143,15 @@ def main(): vg = module.params['vg'] lv = module.params['lv'] size = module.params['size'] + opts = module.params['opts'] state = module.params['state'] force = module.boolean(module.params['force']) size_opt = 'L' size_unit = 'm' + if opts is None: + opts = "" + if size: # LVCREATE(8) -l --extents option with percentage if '%' in size: @@ -212,7 +224,8 @@ def main(): changed = True else: lvcreate_cmd = module.get_bin_path("lvcreate", required=True) - rc, _, err = module.run_command("%s %s -n %s -%s %s%s %s" % (lvcreate_cmd, yesopt, lv, size_opt, size, size_unit, vg)) + cmd = "%s %s -n %s -%s %s%s %s %s" % (lvcreate_cmd, yesopt, lv, size_opt, size, size_unit, opts, vg) + rc, _, err = module.run_command(cmd) if rc == 0: changed = True else: From eec8dca006ea0f14449de496821ea320ad74c4bc Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 1 Jun 2015 15:27:55 -0400 Subject: [PATCH 0199/2522] added copyright/license info to modules I had missed --- notification/jabber.py | 18 ++++++++++++++++++ system/svc.py | 17 +++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/notification/jabber.py b/notification/jabber.py index 466c72d1570..1a19140a83d 100644 --- a/notification/jabber.py +++ b/notification/jabber.py @@ -1,5 +1,23 @@ #!/usr/bin/python # -*- coding: utf-8 -*- +# +# (c) 2015, Brian Coca +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see + DOCUMENTATION = ''' --- diff --git a/system/svc.py b/system/svc.py index 0227a69ecd8..9831ce42ea7 100644 --- a/system/svc.py +++ b/system/svc.py @@ -1,5 +1,22 @@ #!/usr/bin/python # -*- coding: utf-8 -*- +# +# (c) 2015, Brian Coca +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see DOCUMENTATION = ''' --- From 37db61923420e9be68e1baba7ebda5a550dcd61e Mon Sep 17 00:00:00 2001 From: Kevin Carter Date: Mon, 1 Jun 2015 15:15:37 -0500 Subject: [PATCH 0200/2522] lxc_container: remove BabyJSON Removed the usage of baby json. This is in response to the fact that the baby json functionality was removed in Ansible 1.8 Ref: https://github.com/ansible/ansible-modules-extras/issues/430 --- cloud/lxc/lxc_container.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index 119d45069c3..b2dba2111e4 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -383,9 +383,7 @@ try: import lxc except ImportError: - msg = 'The lxc module is not importable. Check the requirements.' - print("failed=True msg='%s'" % msg) - raise SystemExit(msg) + HAS_LXC = False # LXC_COMPRESSION_MAP is a map of available compression types when creating @@ -1706,6 +1704,11 @@ def main(): supports_check_mode=False, ) + if not HAS_LXC: + module.fail_json( + msg='The `lxc` module is not importable. Check the requirements.' + ) + lv_name = module.params.get('lv_name') if not lv_name: module.params['lv_name'] = module.params.get('name') From 391df0ffe0f7fbf622b0e9261a8fb32e2849a67a Mon Sep 17 00:00:00 2001 From: Kevin Carter Date: Mon, 1 Jun 2015 15:31:56 -0500 Subject: [PATCH 0201/2522] Updates the doc information for the python2-lxc dep The python2-lxc library has been uploaded to pypi as such this commit updates the requirements and doc information for the module such that it instructs the user to install the pip package "lxc-python2" while also noting that the package could be gotten from source as well. In the update comments have been added to the requirements list which notes where the package should come from, Closes-Bug: https://github.com/ansible/ansible-modules-extras/issues/550 --- cloud/lxc/lxc_container.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index b2dba2111e4..18555e2e351 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -173,9 +173,9 @@ - list of 'key=value' options to use when configuring a container. required: false requirements: - - 'lxc >= 1.0' - - 'python >= 2.6' - - 'python2-lxc >= 0.1' + - 'lxc >= 1.0 # OS package' + - 'python >= 2.6 # OS Package' + - 'lxc-python2 >= 0.1 # PIP Package from https://github.com/lxc/python2-lxc' notes: - Containers must have a unique name. If you attempt to create a container with a name that already exists in the users namespace the module will @@ -195,7 +195,8 @@ creating the archive. - If your distro does not have a package for "python2-lxc", which is a requirement for this module, it can be installed from source at - "https://github.com/lxc/python2-lxc" + "https://github.com/lxc/python2-lxc" or installed via pip using the package + name lxc-python2. """ EXAMPLES = """ From 927d490f7644f0bbda3cd167f66c2d74ccb68c5f Mon Sep 17 00:00:00 2001 From: Q Date: Tue, 2 Jun 2015 13:32:22 +1000 Subject: [PATCH 0202/2522] patch module: 'backup_copy' parameter renamed to 'backup' --- files/patch.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/files/patch.py b/files/patch.py index 0932ed3556a..085784e7de5 100644 --- a/files/patch.py +++ b/files/patch.py @@ -65,7 +65,7 @@ required: false type: "int" default: "0" - backup_copy: + backup: description: - passes --backup --version-control=numbered to patch, producing numbered backup copies @@ -133,8 +133,9 @@ def main(): 'basedir': {}, 'strip': {'default': 0, 'type': 'int'}, 'remote_src': {'default': False, 'type': 'bool'}, - # don't call it "backup" since the semantics differs from the default one - 'backup_copy': { 'default': False, 'type': 'bool' } + # NB: for 'backup' parameter, semantics is slightly different from standard + # since patch will create numbered copies, not strftime("%Y-%m-%d@%H:%M:%S~") + 'backup': { 'default': False, 'type': 'bool' } }, required_one_of=[['dest', 'basedir']], supports_check_mode=True @@ -168,7 +169,7 @@ def main(): if not is_already_applied(patch_func, p.src, p.basedir, dest_file=p.dest, strip=p.strip): try: apply_patch( patch_func, p.src, p.basedir, dest_file=p.dest, strip=p.strip, - dry_run=module.check_mode, backup=p.backup_copy ) + dry_run=module.check_mode, backup=p.backup ) changed = True except PatchError, e: module.fail_json(msg=str(e)) From 759a7d84dccb60be4b9a2134e5779d3f834f65f7 Mon Sep 17 00:00:00 2001 From: Quentin Stafford-Fraser Date: Tue, 2 Jun 2015 09:25:55 +0100 Subject: [PATCH 0203/2522] Add GPL notices --- cloud/webfaction/webfaction_app.py | 21 ++++++++++++++++++++- cloud/webfaction/webfaction_db.py | 23 +++++++++++++++++++++-- cloud/webfaction/webfaction_domain.py | 21 ++++++++++++++++++++- cloud/webfaction/webfaction_mailbox.py | 20 +++++++++++++++++++- cloud/webfaction/webfaction_site.py | 21 ++++++++++++++++++++- 5 files changed, 100 insertions(+), 6 deletions(-) diff --git a/cloud/webfaction/webfaction_app.py b/cloud/webfaction/webfaction_app.py index 20e94a7b5f6..55599bdcca6 100644 --- a/cloud/webfaction/webfaction_app.py +++ b/cloud/webfaction/webfaction_app.py @@ -1,10 +1,29 @@ #! /usr/bin/python +# # Create a Webfaction application using Ansible and the Webfaction API # # Valid application types can be found by looking here: # http://docs.webfaction.com/xmlrpc-api/apps.html#application-types # -# Quentin Stafford-Fraser 2015 +# ------------------------------------------ +# +# (c) Quentin Stafford-Fraser 2015 +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# DOCUMENTATION = ''' --- diff --git a/cloud/webfaction/webfaction_db.py b/cloud/webfaction/webfaction_db.py index 784477c5409..a9ef88b943e 100644 --- a/cloud/webfaction/webfaction_db.py +++ b/cloud/webfaction/webfaction_db.py @@ -1,7 +1,26 @@ #! /usr/bin/python -# Create webfaction database using Ansible and the Webfaction API # -# Quentin Stafford-Fraser 2015 +# Create a webfaction database using Ansible and the Webfaction API +# +# ------------------------------------------ +# +# (c) Quentin Stafford-Fraser 2015 +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# DOCUMENTATION = ''' --- diff --git a/cloud/webfaction/webfaction_domain.py b/cloud/webfaction/webfaction_domain.py index c99a0f23f6d..f2c95897bc5 100644 --- a/cloud/webfaction/webfaction_domain.py +++ b/cloud/webfaction/webfaction_domain.py @@ -1,7 +1,26 @@ #! /usr/bin/python +# # Create Webfaction domains and subdomains using Ansible and the Webfaction API # -# Quentin Stafford-Fraser 2015 +# ------------------------------------------ +# +# (c) Quentin Stafford-Fraser 2015 +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# DOCUMENTATION = ''' --- diff --git a/cloud/webfaction/webfaction_mailbox.py b/cloud/webfaction/webfaction_mailbox.py index 87ca1fd1a26..976a428f3d3 100644 --- a/cloud/webfaction/webfaction_mailbox.py +++ b/cloud/webfaction/webfaction_mailbox.py @@ -1,7 +1,25 @@ #! /usr/bin/python +# # Create webfaction mailbox using Ansible and the Webfaction API # -# Quentin Stafford-Fraser and Andy Baker 2015 +# ------------------------------------------ +# (c) Quentin Stafford-Fraser and Andy Baker 2015 +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# DOCUMENTATION = ''' --- diff --git a/cloud/webfaction/webfaction_site.py b/cloud/webfaction/webfaction_site.py index a5be4f5407b..223458faf46 100644 --- a/cloud/webfaction/webfaction_site.py +++ b/cloud/webfaction/webfaction_site.py @@ -1,7 +1,26 @@ #! /usr/bin/python +# # Create Webfaction website using Ansible and the Webfaction API # -# Quentin Stafford-Fraser 2015 +# ------------------------------------------ +# +# (c) Quentin Stafford-Fraser 2015 +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# DOCUMENTATION = ''' --- From 078dc8b205357115c60910e062f5a82fa5e50cfa Mon Sep 17 00:00:00 2001 From: Sergei Antipov Date: Tue, 2 Jun 2015 16:26:32 +0600 Subject: [PATCH 0204/2522] Added proxmox_template module --- cloud/misc/proxmox_template.py | 245 +++++++++++++++++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 cloud/misc/proxmox_template.py diff --git a/cloud/misc/proxmox_template.py b/cloud/misc/proxmox_template.py new file mode 100644 index 00000000000..d07a406122c --- /dev/null +++ b/cloud/misc/proxmox_template.py @@ -0,0 +1,245 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: proxmox_template +short_description: management of OS templates in Proxmox VE cluster +description: + - allows you to list/upload/delete templates in Proxmox VE cluster +version_added: "2.0" +options: + api_host: + description: + - the host of the Proxmox VE cluster + required: true + api_user: + description: + - the user to authenticate with + required: true + api_password: + description: + - the password to authenticate with + - you can use PROXMOX_PASSWORD environment variable + default: null + required: false + https_verify_ssl: + description: + - enable / disable https certificate verification + default: false + required: false + type: boolean + node: + description: + - Proxmox VE node, when you will operate with template + default: null + required: true + src: + description: + - path to uploaded file + - required only for C(state=present) + default: null + required: false + aliases: ['path'] + template: + description: + - the template name + - required only for states C(absent), C(info) + default: null + required: false + content_type: + description: + - content type + - required only for C(state=present) + default: 'vztmpl' + required: false + choices: ['vztmpl', 'iso'] + storage: + description: + - target storage + default: 'local' + required: false + type: string + timeout: + description: + - timeout for operations + default: 300 + required: false + type: integer + force: + description: + - can be used only with C(state=present), exists template will be overwritten + default: false + required: false + type: boolean + state: + description: + - Indicate desired state of the template + choices: ['present', 'absent', 'list'] + default: present +notes: + - Requires proxmoxer and requests modules on host. This modules can be installed with pip. +requirements: [ "proxmoxer", "requests" ] +author: "Sergei Antipov @UnderGreen" +''' + +EXAMPLES = ''' +# Upload new openvz template with minimal options +- proxmox_template: node='uk-mc02' api_user='root@pam' api_password='1q2w3e' api_host='node1' src='~/ubuntu-14.04-x86_64.tar.gz' + +# Upload new openvz template with minimal options use environment PROXMOX_PASSWORD variable(you should export it before) +- proxmox_template: node='uk-mc02' api_user='root@pam' api_host='node1' src='~/ubuntu-14.04-x86_64.tar.gz' + +# Upload new openvz template with all options and force overwrite +- proxmox_template: node='uk-mc02' api_user='root@pam' api_password='1q2w3e' api_host='node1' storage='local' content_type='vztmpl' src='~/ubuntu-14.04-x86_64.tar.gz' force=yes + +# Delete template with minimal options +- proxmox_template: node='uk-mc02' api_user='root@pam' api_password='1q2w3e' api_host='node1' template='ubuntu-14.04-x86_64.tar.gz' state=absent + +# List content of storage(it returns list of dicts) +- proxmox_template: node='uk-mc02' api_user='root@pam' api_password='1q2w3e' api_host='node1' storage='local' state=list +''' + +import os +import time + +try: + from proxmoxer import ProxmoxAPI + HAS_PROXMOXER = True +except ImportError: + HAS_PROXMOXER = False + +def get_template(proxmox, node, storage, content_type, template): + return [ True for tmpl in proxmox.nodes(node).storage(storage).content.get() + if tmpl['volid'] == '%s:%s/%s' % (storage, content_type, template) ] + +def get_content(proxmox, node, storage): + return proxmox.nodes(node).storage(storage).content.get() + +def upload_template(module, proxmox, node, storage, content_type, realpath, timeout): + taskid = proxmox.nodes(node).storage(storage).upload.post(content=content_type, filename=open(realpath)) + while timeout: + task_status = proxmox.nodes(node).tasks(taskid).status.get() + if task_status['status'] == 'stopped' and task_status['exitstatus'] == 'OK': + return True + timeout = timeout - 1 + if timeout == 0: + module.fail_json(msg='Reached timeout while waiting for uploading template. Last line in task before timeout: %s' + % proxmox.node(node).tasks(taskid).log.get()[:1]) + + time.sleep(1) + return False + +def delete_template(module, proxmox, node, storage, content_type, template, timeout): + volid = '%s:%s/%s' % (storage, content_type, template) + proxmox.nodes(node).storage(storage).content.delete(volid) + while timeout: + if not get_template(proxmox, node, storage, content_type, template): + return True + timeout = timeout - 1 + if timeout == 0: + module.fail_json(msg='Reached timeout while waiting for deleting template.') + + time.sleep(1) + return False + +def main(): + module = AnsibleModule( + argument_spec = dict( + api_host = dict(required=True), + api_user = dict(required=True), + api_password = dict(no_log=True), + https_verify_ssl = dict(type='bool', choices=BOOLEANS, default='no'), + node = dict(), + src = dict(), + template = dict(), + content_type = dict(default='vztmpl', choices=['vztmpl','iso']), + storage = dict(default='local'), + timeout = dict(type='int', default=300), + force = dict(type='bool', choices=BOOLEANS, default='no'), + state = dict(default='present', choices=['present', 'absent', 'list']), + ) + ) + + if not HAS_PROXMOXER: + module.fail_json(msg='proxmoxer required for this module') + + state = module.params['state'] + api_user = module.params['api_user'] + api_host = module.params['api_host'] + api_password = module.params['api_password'] + https_verify_ssl = module.params['https_verify_ssl'] + node = module.params['node'] + storage = module.params['storage'] + timeout = module.params['timeout'] + + # If password not set get it from PROXMOX_PASSWORD env + if not api_password: + try: + api_password = os.environ['PROXMOX_PASSWORD'] + except KeyError, e: + module.fail_json(msg='You should set api_password param or use PROXMOX_PASSWORD environment variable') + + try: + proxmox = ProxmoxAPI(api_host, user=api_user, password=api_password, verify_ssl=https_verify_ssl) + except Exception, e: + module.fail_json(msg='authorization on proxmox cluster failed with exception: %s' % e) + + if state == 'present': + try: + content_type = module.params['content_type'] + src = module.params['src'] + + from ansible import utils + realpath = utils.path_dwim(None, src) + template = os.path.basename(realpath) + if get_template(proxmox, node, storage, content_type, template) and not module.params['force']: + module.exit_json(changed=False, msg='template with volid=%s:%s/%s is already exists' % (storage, content_type, template)) + elif not src: + module.fail_json(msg='src param to uploading template file is mandatory') + elif not (os.path.exists(realpath) and os.path.isfile(realpath)): + module.fail_json(msg='template file on path %s not exists' % realpath) + + if upload_template(module, proxmox, node, storage, content_type, realpath, timeout): + module.exit_json(changed=True, msg='template with volid=%s:%s/%s uploaded' % (storage, content_type, template)) + except Exception, e: + module.fail_json(msg="uploading of template %s failed with exception: %s" % ( template, e )) + + elif state == 'absent': + try: + content_type = module.params['content_type'] + template = module.params['template'] + + if not template: + module.fail_json(msg='template param is mandatory') + elif not get_template(proxmox, node, storage, content_type, template): + module.exit_json(changed=False, msg='template with volid=%s:%s/%s is already deleted' % (storage, content_type, template)) + + if delete_template(module, proxmox, node, storage, content_type, template, timeout): + module.exit_json(changed=True, msg='template with volid=%s:%s/%s deleted' % (storage, content_type, template)) + except Exception, e: + module.fail_json(msg="deleting of template %s failed with exception: %s" % ( template, e )) + + elif state == 'list': + try: + + module.exit_json(changed=False, templates=get_content(proxmox, node, storage)) + except Exception, e: + module.fail_json(msg="listing of templates %s failed with exception: %s" % ( template, e )) + +# import module snippets +from ansible.module_utils.basic import * +main() From 08a9096c7512a4f00db3a3a6d9b18e4de2b35cac Mon Sep 17 00:00:00 2001 From: Sergei Antipov Date: Tue, 2 Jun 2015 18:21:36 +0600 Subject: [PATCH 0205/2522] proxmox_template | fixed problem with uploading --- cloud/misc/proxmox_template.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/misc/proxmox_template.py b/cloud/misc/proxmox_template.py index d07a406122c..b1d94d96234 100644 --- a/cloud/misc/proxmox_template.py +++ b/cloud/misc/proxmox_template.py @@ -129,10 +129,10 @@ def get_template(proxmox, node, storage, content_type, template): def get_content(proxmox, node, storage): return proxmox.nodes(node).storage(storage).content.get() -def upload_template(module, proxmox, node, storage, content_type, realpath, timeout): +def upload_template(module, proxmox, api_host, node, storage, content_type, realpath, timeout): taskid = proxmox.nodes(node).storage(storage).upload.post(content=content_type, filename=open(realpath)) while timeout: - task_status = proxmox.nodes(node).tasks(taskid).status.get() + task_status = proxmox.nodes(api_host.split('.')[0]).tasks(taskid).status.get() if task_status['status'] == 'stopped' and task_status['exitstatus'] == 'OK': return True timeout = timeout - 1 @@ -213,7 +213,7 @@ def main(): elif not (os.path.exists(realpath) and os.path.isfile(realpath)): module.fail_json(msg='template file on path %s not exists' % realpath) - if upload_template(module, proxmox, node, storage, content_type, realpath, timeout): + if upload_template(module, proxmox, api_host, node, storage, content_type, realpath, timeout): module.exit_json(changed=True, msg='template with volid=%s:%s/%s uploaded' % (storage, content_type, template)) except Exception, e: module.fail_json(msg="uploading of template %s failed with exception: %s" % ( template, e )) From 6050cc8e5d41363b62be3f369afe18584ac1aea6 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 2 Jun 2015 08:37:45 -0400 Subject: [PATCH 0206/2522] push list nature of tags into spec to allow both for comma delimited strings and actual lists --- monitoring/datadog_event.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monitoring/datadog_event.py b/monitoring/datadog_event.py index 5319fcb0f1b..d363f8b17dc 100644 --- a/monitoring/datadog_event.py +++ b/monitoring/datadog_event.py @@ -86,7 +86,7 @@ def main(): priority=dict( required=False, default='normal', choices=['normal', 'low'] ), - tags=dict(required=False, default=None), + tags=dict(required=False, default=None, type='list'), alert_type=dict( required=False, default='info', choices=['error', 'warning', 'info', 'success'] @@ -116,7 +116,7 @@ def post_event(module): if module.params['date_happened'] != None: body['date_happened'] = module.params['date_happened'] if module.params['tags'] != None: - body['tags'] = module.params['tags'].split(",") + body['tags'] = module.params['tags'] if module.params['aggregation_key'] != None: body['aggregation_key'] = module.params['aggregation_key'] if module.params['source_type_name'] != None: From a38b8205d207fcc1d69a00f7c77d28239f185cc6 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 2 Jun 2015 08:48:20 -0400 Subject: [PATCH 0207/2522] added version added to patch's bacukp --- files/patch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/files/patch.py b/files/patch.py index 085784e7de5..c1a61ce733f 100644 --- a/files/patch.py +++ b/files/patch.py @@ -66,6 +66,7 @@ type: "int" default: "0" backup: + version_added: "2.0" description: - passes --backup --version-control=numbered to patch, producing numbered backup copies From e1c8cdc39d87549183cf880f4df44d0ee87e11de Mon Sep 17 00:00:00 2001 From: Sergei Antipov Date: Tue, 2 Jun 2015 22:26:32 +0600 Subject: [PATCH 0208/2522] proxmox_template | changed http_verify_ssl to validate_certs --- cloud/misc/proxmox_template.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cloud/misc/proxmox_template.py b/cloud/misc/proxmox_template.py index b1d94d96234..4bf71f62b12 100644 --- a/cloud/misc/proxmox_template.py +++ b/cloud/misc/proxmox_template.py @@ -36,7 +36,7 @@ - you can use PROXMOX_PASSWORD environment variable default: null required: false - https_verify_ssl: + validate_certs: description: - enable / disable https certificate verification default: false @@ -162,7 +162,7 @@ def main(): api_host = dict(required=True), api_user = dict(required=True), api_password = dict(no_log=True), - https_verify_ssl = dict(type='bool', choices=BOOLEANS, default='no'), + validate_certs = dict(type='bool', choices=BOOLEANS, default='no'), node = dict(), src = dict(), template = dict(), @@ -181,7 +181,7 @@ def main(): api_user = module.params['api_user'] api_host = module.params['api_host'] api_password = module.params['api_password'] - https_verify_ssl = module.params['https_verify_ssl'] + validate_certs = module.params['validate_certs'] node = module.params['node'] storage = module.params['storage'] timeout = module.params['timeout'] @@ -194,7 +194,7 @@ def main(): module.fail_json(msg='You should set api_password param or use PROXMOX_PASSWORD environment variable') try: - proxmox = ProxmoxAPI(api_host, user=api_user, password=api_password, verify_ssl=https_verify_ssl) + proxmox = ProxmoxAPI(api_host, user=api_user, password=api_password, verify_ssl=validate_certs) except Exception, e: module.fail_json(msg='authorization on proxmox cluster failed with exception: %s' % e) From 5f916ac4e36320a9dd31dbe7226e34479a0663af Mon Sep 17 00:00:00 2001 From: Sergei Antipov Date: Tue, 2 Jun 2015 22:29:19 +0600 Subject: [PATCH 0209/2522] proxmox_template | deleted state=list and changed default timeout to 30 --- cloud/misc/proxmox_template.py | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/cloud/misc/proxmox_template.py b/cloud/misc/proxmox_template.py index 4bf71f62b12..7fed47f7260 100644 --- a/cloud/misc/proxmox_template.py +++ b/cloud/misc/proxmox_template.py @@ -19,7 +19,7 @@ module: proxmox_template short_description: management of OS templates in Proxmox VE cluster description: - - allows you to list/upload/delete templates in Proxmox VE cluster + - allows you to upload/delete templates in Proxmox VE cluster version_added: "2.0" options: api_host: @@ -76,7 +76,7 @@ timeout: description: - timeout for operations - default: 300 + default: 30 required: false type: integer force: @@ -88,7 +88,7 @@ state: description: - Indicate desired state of the template - choices: ['present', 'absent', 'list'] + choices: ['present', 'absent'] default: present notes: - Requires proxmoxer and requests modules on host. This modules can be installed with pip. @@ -108,9 +108,6 @@ # Delete template with minimal options - proxmox_template: node='uk-mc02' api_user='root@pam' api_password='1q2w3e' api_host='node1' template='ubuntu-14.04-x86_64.tar.gz' state=absent - -# List content of storage(it returns list of dicts) -- proxmox_template: node='uk-mc02' api_user='root@pam' api_password='1q2w3e' api_host='node1' storage='local' state=list ''' import os @@ -126,9 +123,6 @@ def get_template(proxmox, node, storage, content_type, template): return [ True for tmpl in proxmox.nodes(node).storage(storage).content.get() if tmpl['volid'] == '%s:%s/%s' % (storage, content_type, template) ] -def get_content(proxmox, node, storage): - return proxmox.nodes(node).storage(storage).content.get() - def upload_template(module, proxmox, api_host, node, storage, content_type, realpath, timeout): taskid = proxmox.nodes(node).storage(storage).upload.post(content=content_type, filename=open(realpath)) while timeout: @@ -168,9 +162,9 @@ def main(): template = dict(), content_type = dict(default='vztmpl', choices=['vztmpl','iso']), storage = dict(default='local'), - timeout = dict(type='int', default=300), + timeout = dict(type='int', default=30), force = dict(type='bool', choices=BOOLEANS, default='no'), - state = dict(default='present', choices=['present', 'absent', 'list']), + state = dict(default='present', choices=['present', 'absent']), ) ) @@ -233,13 +227,6 @@ def main(): except Exception, e: module.fail_json(msg="deleting of template %s failed with exception: %s" % ( template, e )) - elif state == 'list': - try: - - module.exit_json(changed=False, templates=get_content(proxmox, node, storage)) - except Exception, e: - module.fail_json(msg="listing of templates %s failed with exception: %s" % ( template, e )) - # import module snippets from ansible.module_utils.basic import * main() From 35853a3d70310e30a80fe968f7274dc736a61dc9 Mon Sep 17 00:00:00 2001 From: Sergei Antipov Date: Tue, 2 Jun 2015 22:53:47 +0600 Subject: [PATCH 0210/2522] proxmox | changed https_verify_ssl to to validate_certs and added forgotten return --- cloud/misc/proxmox.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cloud/misc/proxmox.py b/cloud/misc/proxmox.py index f3ee1962891..7be4361edbe 100644 --- a/cloud/misc/proxmox.py +++ b/cloud/misc/proxmox.py @@ -41,7 +41,7 @@ - the instance id default: null required: true - https_verify_ssl: + validate_certs: description: - enable / disable https certificate verification default: false @@ -219,6 +219,7 @@ def create_instance(module, proxmox, vmid, node, disk, storage, cpus, memory, sw % proxmox_node.tasks(taskid).log.get()[:1]) time.sleep(1) + return False def start_instance(module, proxmox, vm, vmid, timeout): taskid = proxmox.nodes(vm[0]['node']).openvz(vmid).status.start.post() @@ -272,7 +273,7 @@ def main(): api_user = dict(required=True), api_password = dict(no_log=True), vmid = dict(required=True), - https_verify_ssl = dict(type='bool', choices=BOOLEANS, default='no'), + validate_certs = dict(type='bool', choices=BOOLEANS, default='no'), node = dict(), password = dict(no_log=True), hostname = dict(), @@ -302,7 +303,7 @@ def main(): api_host = module.params['api_host'] api_password = module.params['api_password'] vmid = module.params['vmid'] - https_verify_ssl = module.params['https_verify_ssl'] + validate_certs = module.params['validate_certs'] node = module.params['node'] disk = module.params['disk'] cpus = module.params['cpus'] @@ -319,7 +320,7 @@ def main(): module.fail_json(msg='You should set api_password param or use PROXMOX_PASSWORD environment variable') try: - proxmox = ProxmoxAPI(api_host, user=api_user, password=api_password, verify_ssl=https_verify_ssl) + proxmox = ProxmoxAPI(api_host, user=api_user, password=api_password, verify_ssl=validate_certs) except Exception, e: module.fail_json(msg='authorization on proxmox cluster failed with exception: %s' % e) From 861b4d0c19809db8c954eaeecfe98609aac9a068 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 2 Jun 2015 14:11:51 -0400 Subject: [PATCH 0211/2522] corrected lvol docs version to 2.0 --- system/lvol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/lvol.py b/system/lvol.py index d807f9e8336..3225408d162 100644 --- a/system/lvol.py +++ b/system/lvol.py @@ -58,7 +58,7 @@ that filesystems get never corrupted/destroyed by mistake. required: false opts: - version_added: "1.9" + version_added: "2.0" description: - Free-form options to be passed to the lvcreate command notes: From 4475676866cdfec3704acf445e46a694d1519433 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Wed, 3 Jun 2015 01:57:15 +0300 Subject: [PATCH 0212/2522] composer module. ignore_platform_reqs option added. --- packaging/language/composer.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/packaging/language/composer.py b/packaging/language/composer.py index 5bbd948595a..cfe3f99b9e7 100644 --- a/packaging/language/composer.py +++ b/packaging/language/composer.py @@ -82,6 +82,14 @@ default: "yes" choices: [ "yes", "no" ] aliases: [ "optimize-autoloader" ] + ignore_platform_reqs: + version_added: "2.0" + description: + - Ignore php, hhvm, lib-* and ext-* requirements and force the installation even if the local machine does not fulfill these. + required: false + default: "no" + choices: [ "yes", "no" ] + aliases: [ "ignore-platform-reqs" ] requirements: - php - composer installed in bin path (recommended /usr/local/bin) @@ -116,14 +124,15 @@ def composer_install(module, command, options): def main(): module = AnsibleModule( argument_spec = dict( - command = dict(default="install", type="str", required=False), - working_dir = dict(aliases=["working-dir"], required=True), - prefer_source = dict(default="no", type="bool", aliases=["prefer-source"]), - prefer_dist = dict(default="no", type="bool", aliases=["prefer-dist"]), - no_dev = dict(default="yes", type="bool", aliases=["no-dev"]), - no_scripts = dict(default="no", type="bool", aliases=["no-scripts"]), - no_plugins = dict(default="no", type="bool", aliases=["no-plugins"]), - optimize_autoloader = dict(default="yes", type="bool", aliases=["optimize-autoloader"]), + command = dict(default="install", type="str", required=False), + working_dir = dict(aliases=["working-dir"], required=True), + prefer_source = dict(default="no", type="bool", aliases=["prefer-source"]), + prefer_dist = dict(default="no", type="bool", aliases=["prefer-dist"]), + no_dev = dict(default="yes", type="bool", aliases=["no-dev"]), + no_scripts = dict(default="no", type="bool", aliases=["no-scripts"]), + no_plugins = dict(default="no", type="bool", aliases=["no-plugins"]), + optimize_autoloader = dict(default="yes", type="bool", aliases=["optimize-autoloader"]), + ignore_platform_reqs = dict(default="no", type="bool", aliases=["ignore-platform-reqs"]), ), supports_check_mode=True ) @@ -153,6 +162,8 @@ def main(): options.append('--no-plugins') if module.params['optimize_autoloader']: options.append('--optimize-autoloader') + if module.params['ignore_platform_reqs']: + options.append('--ignore-platform-reqs') if module.check_mode: options.append('--dry-run') From 1c6ae9333cd9c3b73315407e069bc49ff70e03cd Mon Sep 17 00:00:00 2001 From: Etienne CARRIERE Date: Wed, 3 Jun 2015 08:22:18 +0200 Subject: [PATCH 0213/2522] Factor common functions for F5 modules --- network/f5/bigip_monitor_http.py | 61 ++++++------------------------ network/f5/bigip_monitor_tcp.py | 64 +++++++------------------------- network/f5/bigip_node.py | 52 +++++--------------------- network/f5/bigip_pool.py | 56 ++++++---------------------- network/f5/bigip_pool_member.py | 54 ++++++--------------------- 5 files changed, 58 insertions(+), 229 deletions(-) diff --git a/network/f5/bigip_monitor_http.py b/network/f5/bigip_monitor_http.py index 6a31afb2ee7..5299bdb0f44 100644 --- a/network/f5/bigip_monitor_http.py +++ b/network/f5/bigip_monitor_http.py @@ -163,35 +163,10 @@ name: "{{ monitorname }}" ''' -try: - import bigsuds -except ImportError: - bigsuds_found = False -else: - bigsuds_found = True - TEMPLATE_TYPE = 'TTYPE_HTTP' DEFAULT_PARENT_TYPE = 'http' -# =========================================== -# bigip_monitor module generic methods. -# these should be re-useable for other monitor types -# - -def bigip_api(bigip, user, password): - - api = bigsuds.BIGIP(hostname=bigip, username=user, password=password) - return api - - -def disable_ssl_cert_validation(): - - # You probably only want to do this for testing and never in production. - # From https://www.python.org/dev/peps/pep-0476/#id29 - import ssl - ssl._create_default_https_context = ssl._create_unverified_context - def check_monitor_exists(module, api, monitor, parent): @@ -278,7 +253,6 @@ def set_integer_property(api, monitor, int_property): def update_monitor_properties(api, module, monitor, template_string_properties, template_integer_properties): - changed = False for str_property in template_string_properties: if str_property['value'] is not None and not check_string_property(api, monitor, str_property): @@ -321,15 +295,8 @@ def set_ipport(api, monitor, ipport): def main(): # begin monitor specific stuff - - module = AnsibleModule( - argument_spec = dict( - server = dict(required=True), - user = dict(required=True), - password = dict(required=True), - validate_certs = dict(default='yes', type='bool'), - partition = dict(default='Common'), - state = dict(default='present', choices=['present', 'absent']), + argument_spec=f5_argument_spec(); + argument_spec.update( dict( name = dict(required=True), parent = dict(default=DEFAULT_PARENT_TYPE), parent_partition = dict(default='Common'), @@ -341,20 +308,20 @@ def main(): interval = dict(required=False, type='int'), timeout = dict(required=False, type='int'), time_until_up = dict(required=False, type='int', default=0) - ), + ) + ) + + module = AnsibleModule( + argument_spec = argument_spec, supports_check_mode=True ) - server = module.params['server'] - user = module.params['user'] - password = module.params['password'] - validate_certs = module.params['validate_certs'] - partition = module.params['partition'] + (server,user,password,state,partition,validate_certs) = f5_parse_arguments(module) + parent_partition = module.params['parent_partition'] - state = module.params['state'] name = module.params['name'] - parent = "/%s/%s" % (parent_partition, module.params['parent']) - monitor = "/%s/%s" % (partition, name) + parent = fq_name(parent_partition, module.params['parent']) + monitor = fq_name(partition, name) send = module.params['send'] receive = module.params['receive'] receive_disable = module.params['receive_disable'] @@ -366,11 +333,6 @@ def main(): # end monitor specific stuff - if not validate_certs: - disable_ssl_cert_validation() - - if not bigsuds_found: - module.fail_json(msg="the python bigsuds module is required") api = bigip_api(server, user, password) monitor_exists = check_monitor_exists(module, api, monitor, parent) @@ -481,5 +443,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.f5 import * main() diff --git a/network/f5/bigip_monitor_tcp.py b/network/f5/bigip_monitor_tcp.py index d5855e0f15d..b5f58da8397 100644 --- a/network/f5/bigip_monitor_tcp.py +++ b/network/f5/bigip_monitor_tcp.py @@ -181,37 +181,11 @@ ''' -try: - import bigsuds -except ImportError: - bigsuds_found = False -else: - bigsuds_found = True - TEMPLATE_TYPE = DEFAULT_TEMPLATE_TYPE = 'TTYPE_TCP' TEMPLATE_TYPE_CHOICES = ['tcp', 'tcp_echo', 'tcp_half_open'] DEFAULT_PARENT = DEFAULT_TEMPLATE_TYPE_CHOICE = DEFAULT_TEMPLATE_TYPE.replace('TTYPE_', '').lower() -# =========================================== -# bigip_monitor module generic methods. -# these should be re-useable for other monitor types -# - -def bigip_api(bigip, user, password): - - api = bigsuds.BIGIP(hostname=bigip, username=user, password=password) - return api - - -def disable_ssl_cert_validation(): - - # You probably only want to do this for testing and never in production. - # From https://www.python.org/dev/peps/pep-0476/#id29 - import ssl - ssl._create_default_https_context = ssl._create_unverified_context - - def check_monitor_exists(module, api, monitor, parent): # hack to determine if monitor exists @@ -234,7 +208,7 @@ def check_monitor_exists(module, api, monitor, parent): def create_monitor(api, monitor, template_attributes): - try: + try: api.LocalLB.Monitor.create_template(templates=[{'template_name': monitor, 'template_type': TEMPLATE_TYPE}], template_attributes=[template_attributes]) except bigsuds.OperationFailed, e: if "already exists" in str(e): @@ -298,7 +272,6 @@ def set_integer_property(api, monitor, int_property): def update_monitor_properties(api, module, monitor, template_string_properties, template_integer_properties): - changed = False for str_property in template_string_properties: if str_property['value'] is not None and not check_string_property(api, monitor, str_property): @@ -341,15 +314,8 @@ def set_ipport(api, monitor, ipport): def main(): # begin monitor specific stuff - - module = AnsibleModule( - argument_spec = dict( - server = dict(required=True), - user = dict(required=True), - password = dict(required=True), - validate_certs = dict(default='yes', type='bool'), - partition = dict(default='Common'), - state = dict(default='present', choices=['present', 'absent']), + argument_spec=f5_argument_spec(); + argument_spec.update(dict( name = dict(required=True), type = dict(default=DEFAULT_TEMPLATE_TYPE_CHOICE, choices=TEMPLATE_TYPE_CHOICES), parent = dict(default=DEFAULT_PARENT), @@ -361,21 +327,21 @@ def main(): interval = dict(required=False, type='int'), timeout = dict(required=False, type='int'), time_until_up = dict(required=False, type='int', default=0) - ), + ) + ) + + module = AnsibleModule( + argument_spec = argument_spec, supports_check_mode=True ) - server = module.params['server'] - user = module.params['user'] - password = module.params['password'] - validate_certs = module.params['validate_certs'] - partition = module.params['partition'] + (server,user,password,state,partition,validate_certs) = f5_parse_arguments(module) + parent_partition = module.params['parent_partition'] - state = module.params['state'] name = module.params['name'] type = 'TTYPE_' + module.params['type'].upper() - parent = "/%s/%s" % (parent_partition, module.params['parent']) - monitor = "/%s/%s" % (partition, name) + parent = fq_name(parent_partition, module.params['parent']) + monitor = fq_name(partition, name) send = module.params['send'] receive = module.params['receive'] ip = module.params['ip'] @@ -390,11 +356,6 @@ def main(): # end monitor specific stuff - if not validate_certs: - disable_ssl_cert_validation() - - if not bigsuds_found: - module.fail_json(msg="the python bigsuds module is required") api = bigip_api(server, user, password) monitor_exists = check_monitor_exists(module, api, monitor, parent) @@ -506,5 +467,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.f5 import * main() diff --git a/network/f5/bigip_node.py b/network/f5/bigip_node.py index 31e34fdeb47..49f721aa8c5 100644 --- a/network/f5/bigip_node.py +++ b/network/f5/bigip_node.py @@ -188,27 +188,6 @@ ''' -try: - import bigsuds -except ImportError: - bigsuds_found = False -else: - bigsuds_found = True - -# ========================== -# bigip_node module specific -# - -def bigip_api(bigip, user, password): - api = bigsuds.BIGIP(hostname=bigip, username=user, password=password) - return api - -def disable_ssl_cert_validation(): - # You probably only want to do this for testing and never in production. - # From https://www.python.org/dev/peps/pep-0476/#id29 - import ssl - ssl._create_default_https_context = ssl._create_unverified_context - def node_exists(api, address): # hack to determine if node exists result = False @@ -283,42 +262,30 @@ def get_node_monitor_status(api, name): def main(): - module = AnsibleModule( - argument_spec = dict( - server = dict(type='str', required=True), - user = dict(type='str', required=True), - password = dict(type='str', required=True), - validate_certs = dict(default='yes', type='bool'), - state = dict(type='str', default='present', choices=['present', 'absent']), + argument_spec=f5_argument_spec(); + argument_spec.update(dict( session_state = dict(type='str', choices=['enabled', 'disabled']), monitor_state = dict(type='str', choices=['enabled', 'disabled']), - partition = dict(type='str', default='Common'), name = dict(type='str', required=True), host = dict(type='str', aliases=['address', 'ip']), description = dict(type='str') - ), + ) + ) + + module = AnsibleModule( + argument_spec = argument_spec, supports_check_mode=True ) - if not bigsuds_found: - module.fail_json(msg="the python bigsuds module is required") + (server,user,password,state,partition,validate_certs) = f5_parse_arguments(module) - server = module.params['server'] - user = module.params['user'] - password = module.params['password'] - validate_certs = module.params['validate_certs'] - state = module.params['state'] session_state = module.params['session_state'] monitor_state = module.params['monitor_state'] - partition = module.params['partition'] host = module.params['host'] name = module.params['name'] - address = "/%s/%s" % (partition, name) + address = fq_name(partition, name) description = module.params['description'] - if not validate_certs: - disable_ssl_cert_validation() - if state == 'absent' and host is not None: module.fail_json(msg="host parameter invalid when state=absent") @@ -410,5 +377,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.f5 import * main() diff --git a/network/f5/bigip_pool.py b/network/f5/bigip_pool.py index 2eaaf8f3a34..4d8d599134e 100644 --- a/network/f5/bigip_pool.py +++ b/network/f5/bigip_pool.py @@ -228,27 +228,6 @@ ''' -try: - import bigsuds -except ImportError: - bigsuds_found = False -else: - bigsuds_found = True - -# =========================================== -# bigip_pool module specific support methods. -# - -def bigip_api(bigip, user, password): - api = bigsuds.BIGIP(hostname=bigip, username=user, password=password) - return api - -def disable_ssl_cert_validation(): - # You probably only want to do this for testing and never in production. - # From https://www.python.org/dev/peps/pep-0476/#id29 - import ssl - ssl._create_default_https_context = ssl._create_unverified_context - def pool_exists(api, pool): # hack to determine if pool exists result = False @@ -368,15 +347,9 @@ def main(): service_down_choices = ['none', 'reset', 'drop', 'reselect'] - module = AnsibleModule( - argument_spec = dict( - server = dict(type='str', required=True), - user = dict(type='str', required=True), - password = dict(type='str', required=True), - validate_certs = dict(default='yes', type='bool'), - state = dict(type='str', default='present', choices=['present', 'absent']), + argument_spec=f5_argument_spec(); + argument_spec.update(dict( name = dict(type='str', required=True, aliases=['pool']), - partition = dict(type='str', default='Common'), lb_method = dict(type='str', choices=lb_method_choices), monitor_type = dict(type='str', choices=monitor_type_choices), quorum = dict(type='int'), @@ -385,21 +358,18 @@ def main(): service_down_action = dict(type='str', choices=service_down_choices), host = dict(type='str', aliases=['address']), port = dict(type='int') - ), + ) + ) + + module = AnsibleModule( + argument_spec = argument_spec, supports_check_mode=True ) - if not bigsuds_found: - module.fail_json(msg="the python bigsuds module is required") + (server,user,password,state,partition,validate_certs) = f5_parse_arguments(module) - server = module.params['server'] - user = module.params['user'] - password = module.params['password'] - validate_certs = module.params['validate_certs'] - state = module.params['state'] name = module.params['name'] - partition = module.params['partition'] - pool = "/%s/%s" % (partition, name) + pool = fq_name(partition,name) lb_method = module.params['lb_method'] if lb_method: lb_method = lb_method.lower() @@ -411,16 +381,13 @@ def main(): if monitors: monitors = [] for monitor in module.params['monitors']: - if "/" not in monitor: - monitors.append("/%s/%s" % (partition, monitor)) - else: - monitors.append(monitor) + monitors.append(fq_name(partition, monitor)) slow_ramp_time = module.params['slow_ramp_time'] service_down_action = module.params['service_down_action'] if service_down_action: service_down_action = service_down_action.lower() host = module.params['host'] - address = "/%s/%s" % (partition, host) + address = fq_name(partition,host) port = module.params['port'] if not validate_certs: @@ -551,5 +518,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.f5 import * main() diff --git a/network/f5/bigip_pool_member.py b/network/f5/bigip_pool_member.py index bc4b7be2f7b..1d59462023f 100644 --- a/network/f5/bigip_pool_member.py +++ b/network/f5/bigip_pool_member.py @@ -196,27 +196,6 @@ ''' -try: - import bigsuds -except ImportError: - bigsuds_found = False -else: - bigsuds_found = True - -# =========================================== -# bigip_pool_member module specific support methods. -# - -def bigip_api(bigip, user, password): - api = bigsuds.BIGIP(hostname=bigip, username=user, password=password) - return api - -def disable_ssl_cert_validation(): - # You probably only want to do this for testing and never in production. - # From https://www.python.org/dev/peps/pep-0476/#id29 - import ssl - ssl._create_default_https_context = ssl._create_unverified_context - def pool_exists(api, pool): # hack to determine if pool exists result = False @@ -327,49 +306,37 @@ def get_member_monitor_status(api, pool, address, port): return result def main(): - module = AnsibleModule( - argument_spec = dict( - server = dict(type='str', required=True), - user = dict(type='str', required=True), - password = dict(type='str', required=True), - validate_certs = dict(default='yes', type='bool'), - state = dict(type='str', default='present', choices=['present', 'absent']), + argument_spec = f5_argument_spec(); + argument_spec.update(dict( session_state = dict(type='str', choices=['enabled', 'disabled']), monitor_state = dict(type='str', choices=['enabled', 'disabled']), pool = dict(type='str', required=True), - partition = dict(type='str', default='Common'), host = dict(type='str', required=True, aliases=['address', 'name']), port = dict(type='int', required=True), connection_limit = dict(type='int'), description = dict(type='str'), rate_limit = dict(type='int'), ratio = dict(type='int') - ), - supports_check_mode=True + ) ) - if not bigsuds_found: - module.fail_json(msg="the python bigsuds module is required") + module = AnsibleModule( + argument_spec = argument_spec, + supports_check_mode=True + ) - server = module.params['server'] - user = module.params['user'] - password = module.params['password'] - validate_certs = module.params['validate_certs'] - state = module.params['state'] + (server,user,password,state,partition,validate_certs) = f5_parse_arguments(module) session_state = module.params['session_state'] monitor_state = module.params['monitor_state'] - partition = module.params['partition'] - pool = "/%s/%s" % (partition, module.params['pool']) + pool = fq_name(partition, module.params['pool']) connection_limit = module.params['connection_limit'] description = module.params['description'] rate_limit = module.params['rate_limit'] ratio = module.params['ratio'] host = module.params['host'] - address = "/%s/%s" % (partition, host) + address = fq_name(partition, host) port = module.params['port'] - if not validate_certs: - disable_ssl_cert_validation() # sanity check user supplied values @@ -457,5 +424,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.f5 import * main() From 1291f9a25ae0628ef3664d0d797ca0546f03848f Mon Sep 17 00:00:00 2001 From: Sebastian Kornehl Date: Wed, 3 Jun 2015 13:15:59 +0200 Subject: [PATCH 0214/2522] Added datadog_monitor module --- monitoring/datadog_monitor.py | 278 ++++++++++++++++++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 monitoring/datadog_monitor.py diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py new file mode 100644 index 00000000000..b5ad2d2d6d6 --- /dev/null +++ b/monitoring/datadog_monitor.py @@ -0,0 +1,278 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Sebastian Kornehl +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# import module snippets + +# Import Datadog +try: + from datadog import initialize, api + HAS_DATADOG = True +except: + HAS_DATADOG = False + +DOCUMENTATION = ''' +--- +module: datadog_monitor +short_description: Manages Datadog monitors +description: +- "Manages monitors within Datadog" +- "Options like described on http://docs.datadoghq.com/api/" +version_added: "2.0" +author: '"Sebastian Kornehl" ' +notes: [] +requirements: [datadog] +options: + api_key: + description: ["Your DataDog API key."] + required: true + default: null + app_key: + description: ["Your DataDog app key."] + required: true + default: null + state: + description: ["The designated state of the monitor."] + required: true + default: null + choices: ['present', 'absent', 'muted', 'unmuted'] + type: + description: ["The type of the monitor."] + required: false + default: null + choices: ['metric alert', 'service check'] + query: + description: ["he monitor query to notify on with syntax varying depending on what type of monitor you are creating."] + required: false + default: null + name: + description: ["The name of the alert."] + required: true + default: null + message: + description: ["A message to include with notifications for this monitor. Email notifications can be sent to specific users by using the same '@username' notation as events."] + required: false + default: null + silenced: + description: ["Dictionary of scopes to timestamps or None. Each scope will be muted until the given POSIX timestamp or forever if the value is None. "] + required: false + default: "" + notify_no_data: + description: ["A boolean indicating whether this monitor will notify when data stops reporting.."] + required: false + default: False + no_data_timeframe: + description: ["The number of minutes before a monitor will notify when data stops reporting. Must be at least 2x the monitor timeframe for metric alerts or 2 minutes for service checks."] + required: false + default: 2x timeframe for metric, 2 minutes for service + timeout_h: + description: ["The number of hours of the monitor not reporting data before it will automatically resolve from a triggered state."] + required: false + default: null + renotify_interval: + description: ["The number of minutes after the last notification before a monitor will re-notify on the current status. It will only re-notify if it's not resolved."] + required: false + default: null + escalation_message: + description: ["A message to include with a re-notification. Supports the '@username' notification we allow elsewhere. Not applicable if renotify_interval is None"] + required: false + default: null + notify_audit: + description: ["A boolean indicating whether tagged users will be notified on changes to this monitor."] + required: false + default: False + thresholds: + description: ["A dictionary of thresholds by status. Because service checks can have multiple thresholds, we don't define them directly in the query."] + required: false + default: {'ok': 1, 'critical': 1, 'warning': 1} +''' + +EXAMPLES = ''' +# Create a metric monitor +datadog_monitor: + type: "metric alert" + name: "Test monitor" + state: "present" + query: "datadog.agent.up".over("host:host1").last(2).count_by_status()" + message: "Some message." + api_key: "9775a026f1ca7d1c6c5af9d94d9595a4" + app_key: "87ce4a24b5553d2e482ea8a8500e71b8ad4554ff" + +# Deletes a monitor +datadog_monitor: + name: "Test monitor" + state: "absent" + api_key: "9775a026f1ca7d1c6c5af9d94d9595a4" + app_key: "87ce4a24b5553d2e482ea8a8500e71b8ad4554ff" + +# Mutes a monitor +datadog_monitor: + name: "Test monitor" + state: "mute" + silenced: '{"*":None}' + api_key: "9775a026f1ca7d1c6c5af9d94d9595a4" + app_key: "87ce4a24b5553d2e482ea8a8500e71b8ad4554ff" + +# Unmutes a monitor +datadog_monitor: + name: "Test monitor" + state: "unmute" + api_key: "9775a026f1ca7d1c6c5af9d94d9595a4" + app_key: "87ce4a24b5553d2e482ea8a8500e71b8ad4554ff" +''' + + +def main(): + module = AnsibleModule( + argument_spec=dict( + api_key=dict(required=True), + app_key=dict(required=True), + state=dict(required=True, choises=['present', 'absent', 'mute', 'unmute']), + type=dict(required=False, choises=['metric alert', 'service check']), + name=dict(required=True), + query=dict(required=False), + message=dict(required=False, default=None), + silenced=dict(required=False, default=None, type='dict'), + notify_no_data=dict(required=False, default=False, choices=BOOLEANS), + no_data_timeframe=dict(required=False, default=None), + timeout_h=dict(required=False, default=None), + renotify_interval=dict(required=False, default=None), + escalation_message=dict(required=False, default=None), + notify_audit=dict(required=False, default=False, choices=BOOLEANS), + thresholds=dict(required=False, type='dict', default={'ok': 1, 'critical': 1, 'warning': 1}), + ) + ) + + # Prepare Datadog + if not HAS_DATADOG: + module.fail_json(msg='datadogpy required for this module') + + options = { + 'api_key': module.params['api_key'], + 'app_key': module.params['app_key'] + } + + initialize(**options) + + if module.params['state'] == 'present': + install_monitor(module) + elif module.params['state'] == 'absent': + delete_monitor(module) + elif module.params['state'] == 'mute': + mute_monitor(module) + elif module.params['state'] == 'unmute': + unmute_monitor(module) + + +def _get_monitor(module): + for monitor in api.Monitor.get_all(): + if monitor['name'] == module.params['name']: + return monitor + return {} + + +def _post_monitor(module, options): + try: + msg = api.Monitor.create(type=module.params['type'], query=module.params['query'], + name=module.params['name'], message=module.params['message'], + options=options) + module.exit_json(changed=True, msg=msg) + except Exception, e: + module.fail_json(msg=str(e)) + + +def _update_monitor(module, monitor, options): + try: + msg = api.Monitor.update(id=monitor['id'], query=module.params['query'], + name=module.params['name'], message=module.params['message'], + options=options) + if len(set(msg) - set(monitor)) == 0: + module.exit_json(changed=False, msg=msg) + else: + module.exit_json(changed=True, msg=msg) + except Exception, e: + module.fail_json(msg=str(e)) + + +def install_monitor(module): + options = { + "silenced": module.params['silenced'], + "notify_no_data": module.boolean(module.params['notify_no_data']), + "no_data_timeframe": module.params['no_data_timeframe'], + "timeout_h": module.params['timeout_h'], + "renotify_interval": module.params['renotify_interval'], + "escalation_message": module.params['escalation_message'], + "notify_audit": module.boolean(module.params['notify_audit']), + } + + if module.params['type'] == "service check": + options["thresholds"] = module.params['thresholds'] + + monitor = _get_monitor(module) + if not monitor: + _post_monitor(module, options) + else: + _update_monitor(module, monitor, options) + + +def delete_monitor(module): + monitor = _get_monitor(module) + if not monitor: + module.exit_json(changed=False) + try: + msg = api.Monitor.delete(monitor['id']) + module.exit_json(changed=True, msg=msg) + except Exception, e: + module.fail_json(msg=str(e)) + + +def mute_monitor(module): + monitor = _get_monitor(module) + if not monitor: + module.fail_json(msg="Monitor %s not found!" % module.params['name']) + elif monitor['options']['silenced']: + module.fail_json(msg="Monitor is already muted. Datadog does not allow to modify muted alerts, consider unmuting it first.") + elif (module.params['silenced'] is not None + and len(set(monitor['options']['silenced']) - set(module.params['silenced'])) == 0): + module.exit_json(changed=False) + try: + if module.params['silenced'] is None or module.params['silenced'] == "": + msg = api.Monitor.mute(id=monitor['id']) + else: + msg = api.Monitor.mute(id=monitor['id'], silenced=module.params['silenced']) + module.exit_json(changed=True, msg=msg) + except Exception, e: + module.fail_json(msg=str(e)) + + +def unmute_monitor(module): + monitor = _get_monitor(module) + if not monitor: + module.fail_json(msg="Monitor %s not found!" % module.params['name']) + elif not monitor['options']['silenced']: + module.exit_json(changed=False) + try: + msg = api.Monitor.unmute(monitor['id']) + module.exit_json(changed=True, msg=msg) + except Exception, e: + module.fail_json(msg=str(e)) + + +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * +main() From 5bfe8f2a44e3a7637fee173f92c9775f5fe8a7be Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Thu, 4 Jun 2015 01:25:08 +0300 Subject: [PATCH 0215/2522] bower module. Non-interactive mode and allow-root moved to _exec, they should affect all commands --- packaging/language/bower.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packaging/language/bower.py b/packaging/language/bower.py index 34284356f6e..8fbe20f7e0c 100644 --- a/packaging/language/bower.py +++ b/packaging/language/bower.py @@ -86,7 +86,7 @@ def __init__(self, module, **kwargs): def _exec(self, args, run_in_check_mode=False, check_rc=True): if not self.module.check_mode or (self.module.check_mode and run_in_check_mode): - cmd = ["bower"] + args + cmd = ["bower"] + args + ['--config.interactive=false', '--allow-root'] if self.name: cmd.append(self.name_version) @@ -108,7 +108,7 @@ def _exec(self, args, run_in_check_mode=False, check_rc=True): return '' def list(self): - cmd = ['list', '--json', '--config.interactive=false', '--allow-root'] + cmd = ['list', '--json'] installed = list() missing = list() From fdaa4da4476f60213c81ccd5e98d52d7ece6a415 Mon Sep 17 00:00:00 2001 From: Sebastian Kornehl Date: Thu, 4 Jun 2015 06:54:02 +0200 Subject: [PATCH 0216/2522] docs: removed default when required is true --- monitoring/datadog_monitor.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py index b5ad2d2d6d6..24de8af10ba 100644 --- a/monitoring/datadog_monitor.py +++ b/monitoring/datadog_monitor.py @@ -41,15 +41,12 @@ api_key: description: ["Your DataDog API key."] required: true - default: null app_key: description: ["Your DataDog app key."] required: true - default: null state: description: ["The designated state of the monitor."] required: true - default: null choices: ['present', 'absent', 'muted', 'unmuted'] type: description: ["The type of the monitor."] @@ -63,7 +60,6 @@ name: description: ["The name of the alert."] required: true - default: null message: description: ["A message to include with notifications for this monitor. Email notifications can be sent to specific users by using the same '@username' notation as events."] required: false From e972346faea861c1a1067c142d5bf8c4efe331ce Mon Sep 17 00:00:00 2001 From: Quentin Stafford-Fraser Date: Thu, 4 Jun 2015 22:17:16 +0100 Subject: [PATCH 0217/2522] Webfaction will create a default database user when db is created. For symmetry and repeatability, delete it when db is deleted. Add missing param to documentation. --- cloud/webfaction/webfaction_db.py | 48 ++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/cloud/webfaction/webfaction_db.py b/cloud/webfaction/webfaction_db.py index a9ef88b943e..1a91d649458 100644 --- a/cloud/webfaction/webfaction_db.py +++ b/cloud/webfaction/webfaction_db.py @@ -4,7 +4,7 @@ # # ------------------------------------------ # -# (c) Quentin Stafford-Fraser 2015 +# (c) Quentin Stafford-Fraser and Andy Baker 2015 # # This file is part of Ansible # @@ -53,6 +53,12 @@ required: true choices: ['mysql', 'postgresql'] + password: + description: + - The password for the new database user. + required: false + default: None + login_name: description: - The webfaction account to use @@ -75,6 +81,10 @@ type: mysql login_name: "{{webfaction_user}}" login_password: "{{webfaction_passwd}}" + + # Note that, for symmetry's sake, deleting a database using + # 'state: absent' will also delete the matching user. + ''' import socket @@ -110,13 +120,17 @@ def main(): db_map = dict([(i['name'], i) for i in db_list]) existing_db = db_map.get(db_name) + user_list = webfaction.list_db_users(session_id) + user_map = dict([(i['username'], i) for i in user_list]) + existing_user = user_map.get(db_name) + result = {} # Here's where the real stuff happens if db_state == 'present': - # Does an app with this name already exist? + # Does an database with this name already exist? if existing_db: # Yes, but of a different type - fail if existing_db['db_type'] != db_type: @@ -129,8 +143,8 @@ def main(): if not module.check_mode: - # If this isn't a dry run, create the app - # print positional_args + # If this isn't a dry run, create the db + # and default user. result.update( webfaction.create_db( session_id, db_name, db_type, db_passwd @@ -139,17 +153,23 @@ def main(): elif db_state == 'absent': - # If the app's already not there, nothing changed. - if not existing_db: - module.exit_json( - changed = False, - ) - + # If this isn't a dry run... if not module.check_mode: - # If this isn't a dry run, delete the app - result.update( - webfaction.delete_db(session_id, db_name, db_type) - ) + + if not (existing_db or existing_user): + module.exit_json(changed = False,) + + if existing_db: + # Delete the db if it exists + result.update( + webfaction.delete_db(session_id, db_name, db_type) + ) + + if existing_user: + # Delete the default db user if it exists + result.update( + webfaction.delete_db_user(session_id, db_name, db_type) + ) else: module.fail_json(msg="Unknown state specified: {}".format(db_state)) From cc9d2ad03ff5c7dd5d0202bf1b7e69a56c2943cb Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Fri, 5 Jun 2015 11:25:27 -0400 Subject: [PATCH 0218/2522] minor docs update --- cloud/webfaction/webfaction_app.py | 2 +- cloud/webfaction/webfaction_db.py | 2 +- cloud/webfaction/webfaction_domain.py | 2 +- cloud/webfaction/webfaction_mailbox.py | 2 +- cloud/webfaction/webfaction_site.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cloud/webfaction/webfaction_app.py b/cloud/webfaction/webfaction_app.py index 55599bdcca6..3e42ec1265e 100644 --- a/cloud/webfaction/webfaction_app.py +++ b/cloud/webfaction/webfaction_app.py @@ -31,7 +31,7 @@ short_description: Add or remove applications on a Webfaction host description: - Add or remove applications on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. -author: Quentin Stafford-Fraser +author: Quentin Stafford-Fraser (@quentinsf) version_added: "2.0" notes: - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." diff --git a/cloud/webfaction/webfaction_db.py b/cloud/webfaction/webfaction_db.py index 1a91d649458..f420490711c 100644 --- a/cloud/webfaction/webfaction_db.py +++ b/cloud/webfaction/webfaction_db.py @@ -28,7 +28,7 @@ short_description: Add or remove a database on Webfaction description: - Add or remove a database on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. -author: Quentin Stafford-Fraser +author: Quentin Stafford-Fraser (@quentinsf) version_added: "2.0" notes: - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." diff --git a/cloud/webfaction/webfaction_domain.py b/cloud/webfaction/webfaction_domain.py index f2c95897bc5..0b35faf110f 100644 --- a/cloud/webfaction/webfaction_domain.py +++ b/cloud/webfaction/webfaction_domain.py @@ -28,7 +28,7 @@ short_description: Add or remove domains and subdomains on Webfaction description: - Add or remove domains or subdomains on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. -author: Quentin Stafford-Fraser +author: Quentin Stafford-Fraser (@quentinsf) version_added: "2.0" notes: - If you are I(deleting) domains by using C(state=absent), then note that if you specify subdomains, just those particular subdomains will be deleted. If you don't specify subdomains, the domain will be deleted. diff --git a/cloud/webfaction/webfaction_mailbox.py b/cloud/webfaction/webfaction_mailbox.py index 976a428f3d3..7547b6154e5 100644 --- a/cloud/webfaction/webfaction_mailbox.py +++ b/cloud/webfaction/webfaction_mailbox.py @@ -27,7 +27,7 @@ short_description: Add or remove mailboxes on Webfaction description: - Add or remove mailboxes on a Webfaction account. Further documentation at http://github.com/quentinsf/ansible-webfaction. -author: Quentin Stafford-Fraser +author: Quentin Stafford-Fraser (@quentinsf) version_added: "2.0" notes: - "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays." diff --git a/cloud/webfaction/webfaction_site.py b/cloud/webfaction/webfaction_site.py index 223458faf46..57eae39c0dc 100644 --- a/cloud/webfaction/webfaction_site.py +++ b/cloud/webfaction/webfaction_site.py @@ -28,7 +28,7 @@ short_description: Add or remove a website on a Webfaction host description: - Add or remove a website on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction. -author: Quentin Stafford-Fraser +author: Quentin Stafford-Fraser (@quentinsf) version_added: "2.0" notes: - Sadly, you I(do) need to know your webfaction hostname for the C(host) parameter. But at least, unlike the API, you don't need to know the IP address - you can use a DNS name. From 653ce424e094ee65223bd1efd0dd7f9d6d49fdb7 Mon Sep 17 00:00:00 2001 From: "jonathan.lestrelin" Date: Fri, 5 Jun 2015 18:18:48 +0200 Subject: [PATCH 0219/2522] Add pear packaging module to manage PHP PEAR an PECL packages --- packaging/language/pear.py | 230 +++++++++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 packaging/language/pear.py diff --git a/packaging/language/pear.py b/packaging/language/pear.py new file mode 100644 index 00000000000..c9e3862a31f --- /dev/null +++ b/packaging/language/pear.py @@ -0,0 +1,230 @@ +#!/usr/bin/python -tt +# -*- coding: utf-8 -*- + +# (c) 2012, Afterburn +# (c) 2013, Aaron Bull Schaefer +# (c) 2015, Jonathan Lestrelin +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: pear +short_description: Manage pear/pecl packages +description: + - Manage PHP packages with the pear package manager. +author: + - "'jonathan.lestrelin' " +notes: [] +requirements: [] +options: + name: + description: + - Name of the package to install, upgrade, or remove. + required: true + default: null + + state: + description: + - Desired state of the package. + required: false + default: "present" + choices: ["present", "absent", "latest"] +''' + +EXAMPLES = ''' +# Install pear package +- pear: name=Net_URL2 state=present + +# Install pecl package +- pear: name=pecl/json_post state=present + +# Upgrade package +- pear: name=Net_URL2 state=latest + +# Remove packages +- pear: name=Net_URL2,pecl/json_post state=absent +''' + +import os + +def get_local_version(pear_output): + """Take pear remoteinfo output and get the installed version""" + lines = pear_output.split('\n') + for line in lines: + if 'Installed ' in line: + installed = line.rsplit(None, 1)[-1].strip() + if installed == '-': continue + return installed + return None + +def get_repository_version(pear_output): + """Take pear remote-info output and get the latest version""" + lines = pear_output.split('\n') + for line in lines: + if 'Latest ' in line: + return line.rsplit(None, 1)[-1].strip() + return None + +def query_package(module, name, state="present"): + """Query the package status in both the local system and the repository. + Returns a boolean to indicate if the package is installed, + and a second boolean to indicate if the package is up-to-date.""" + if state == "present": + lcmd = "pear info %s" % (name) + lrc, lstdout, lstderr = module.run_command(lcmd, check_rc=False) + if lrc != 0: + # package is not installed locally + return False, False + + rcmd = "pear remote-info %s" % (name) + rrc, rstdout, rstderr = module.run_command(rcmd, check_rc=False) + + # get the version installed locally (if any) + lversion = get_local_version(rstdout) + + # get the version in the repository + rversion = get_repository_version(rstdout) + + if rrc == 0: + # Return True to indicate that the package is installed locally, + # and the result of the version number comparison + # to determine if the package is up-to-date. + return True, (lversion == rversion) + + return False, False + + +def remove_packages(module, packages): + remove_c = 0 + # Using a for loop incase of error, we can report the package that failed + for package in packages: + # Query the package first, to see if we even need to remove + installed, updated = query_package(module, package) + if not installed: + continue + + cmd = "pear uninstall %s" % (package) + rc, stdout, stderr = module.run_command(cmd, check_rc=False) + + if rc != 0: + module.fail_json(msg="failed to remove %s" % (package)) + + remove_c += 1 + + if remove_c > 0: + + module.exit_json(changed=True, msg="removed %s package(s)" % remove_c) + + module.exit_json(changed=False, msg="package(s) already absent") + + +def install_packages(module, state, packages, package_files): + install_c = 0 + + for i, package in enumerate(packages): + # if the package is installed and state == present + # or state == latest and is up-to-date then skip + installed, updated = query_package(module, package) + if installed and (state == 'present' or (state == 'latest' and updated)): + continue + + if state == 'present': + command = 'install' + + if state == 'latest': + command = 'upgrade' + + cmd = "pear %s %s" % (command, package) + rc, stdout, stderr = module.run_command(cmd, check_rc=False) + + if rc != 0: + module.fail_json(msg="failed to install %s" % (package)) + + install_c += 1 + + if install_c > 0: + module.exit_json(changed=True, msg="installed %s package(s)" % (install_c)) + + module.exit_json(changed=False, msg="package(s) already installed") + + +def check_packages(module, packages, state): + would_be_changed = [] + for package in packages: + installed, updated = query_package(module, package) + if ((state in ["present", "latest"] and not installed) or + (state == "absent" and installed) or + (state == "latest" and not updated)): + would_be_changed.append(package) + if would_be_changed: + if state == "absent": + state = "removed" + module.exit_json(changed=True, msg="%s package(s) would be %s" % ( + len(would_be_changed), state)) + else: + module.exit_json(change=False, msg="package(s) already %s" % state) + +import os + +def exe_exists(program): + for path in os.environ["PATH"].split(os.pathsep): + path = path.strip('"') + exe_file = os.path.join(path, program) + if os.path.isfile(exe_file) and os.access(exe_file, os.X_OK): + return True + + return False + + +def main(): + module = AnsibleModule( + argument_spec = dict( + name = dict(aliases=['pkg']), + state = dict(default='present', choices=['present', 'installed', "latest", 'absent', 'removed'])), + required_one_of = [['name']], + supports_check_mode = True) + + if not exe_exists("pear"): + module.fail_json(msg="cannot find pear executable in PATH") + + p = module.params + + # normalize the state parameter + if p['state'] in ['present', 'installed']: + p['state'] = 'present' + elif p['state'] in ['absent', 'removed']: + p['state'] = 'absent' + + if p['name']: + pkgs = p['name'].split(',') + + pkg_files = [] + for i, pkg in enumerate(pkgs): + pkg_files.append(None) + + if module.check_mode: + check_packages(module, pkgs, p['state']) + + if p['state'] in ['present', 'latest']: + install_packages(module, p['state'], pkgs, pkg_files) + elif p['state'] == 'absent': + remove_packages(module, pkgs) + +# import module snippets +from ansible.module_utils.basic import * + +main() From 9d4046f44bd05589594c6d4b3ef178abd2b4758b Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 6 Jun 2015 09:13:11 +0200 Subject: [PATCH 0220/2522] puppet: ensure puppet is in live mode per default puppet may be configured to operate in `--noop` mode per default. That is why we must pass a `--no-noop` to make sure, changes are going to be applied. --- system/puppet.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/system/puppet.py b/system/puppet.py index 46a5ea58d4f..3d4223bd1e5 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -156,10 +156,14 @@ def main(): cmd += " --show-diff" if module.check_mode: cmd += " --noop" + else: + cmd += " --no-noop" else: cmd = "%s apply --detailed-exitcodes " % base_cmd if module.check_mode: cmd += "--noop " + else: + cmd += "--no-noop " cmd += pipes.quote(p['manifest']) rc, stdout, stderr = module.run_command(cmd) From 616a56f871901635d6ea27525f7d4b005048e8b2 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 6 Jun 2015 09:42:56 +0200 Subject: [PATCH 0221/2522] puppet: add --environment support --- system/puppet.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/system/puppet.py b/system/puppet.py index 3d4223bd1e5..c9e7943ff25 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -59,6 +59,11 @@ - Basename of the facter output file required: false default: ansible + environment: + desciption: + - Puppet environment to be used. + required: false + default: None requirements: [ puppet ] author: Monty Taylor ''' @@ -69,6 +74,9 @@ # Run puppet and timeout in 5 minutes - puppet: timeout=5m + +# Run puppet using a different environment +- puppet: environment=testing ''' @@ -104,6 +112,7 @@ def main(): default=False, aliases=['show-diff'], type='bool'), facts=dict(default=None), facter_basename=dict(default='ansible'), + environment=dict(required=False, default=None), ), supports_check_mode=True, required_one_of=[ @@ -154,12 +163,16 @@ def main(): puppetmaster=pipes.quote(p['puppetmaster'])) if p['show_diff']: cmd += " --show-diff" + if p['environment']: + cmd += " --environment '%s'" % p['environment'] if module.check_mode: cmd += " --noop" else: cmd += " --no-noop" else: cmd = "%s apply --detailed-exitcodes " % base_cmd + if p['environment']: + cmd += "--environment '%s' " % p['environment'] if module.check_mode: cmd += "--noop " else: From c277946fb306c826be54c25e283c683557b7c2c5 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 6 Jun 2015 09:46:16 +0200 Subject: [PATCH 0222/2522] puppet: fix missing space between command and arg Fixes: ~~~ { "cmd": "/usr/bin/puppetconfig print agent_disabled_lockfile", "failed": true, "msg": "[Errno 2] No such file or directory", "rc": 2 } ~~~ --- system/puppet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/puppet.py b/system/puppet.py index c9e7943ff25..9e2994225c4 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -137,7 +137,7 @@ def main(): # Check if puppet is disabled here if p['puppetmaster']: rc, stdout, stderr = module.run_command( - PUPPET_CMD + "config print agent_disabled_lockfile") + PUPPET_CMD + " config print agent_disabled_lockfile") if os.path.exists(stdout.strip()): module.fail_json( msg="Puppet agent is administratively disabled.", disabled=True) From e633d9946fc9048e5ec258552fa018d8da27d18d Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 6 Jun 2015 10:08:16 +0200 Subject: [PATCH 0223/2522] puppet: make arg puppetmaster optional puppetmaster was used to determine if `agent` or `apply` should be used. But puppetmaster is not required by puppet per default. Puppet may have a config or could find out by itself (...) where the puppet master is. It changed the code so we only use `apply` if a manifest was passed, otherwise we use `agent`. This also fixes the example, which did not work the way without this change. ~~~ # Run puppet agent and fail if anything goes wrong - puppet ~~~ --- system/puppet.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/system/puppet.py b/system/puppet.py index 9e2994225c4..83bbcbe6e18 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -35,12 +35,12 @@ default: 30m puppetmaster: description: - - The hostname of the puppetmaster to contact. Must have this or manifest + - The hostname of the puppetmaster to contact. required: false default: None manifest: desciption: - - Path to the manifest file to run puppet apply on. Must have this or puppetmaster + - Path to the manifest file to run puppet apply on. required: false default: None show_diff: @@ -69,7 +69,7 @@ ''' EXAMPLES = ''' -# Run puppet and fail if anything goes wrong +# Run puppet agent and fail if anything goes wrong - puppet # Run puppet and timeout in 5 minutes @@ -115,7 +115,7 @@ def main(): environment=dict(required=False, default=None), ), supports_check_mode=True, - required_one_of=[ + mutually_exclusive=[ ('puppetmaster', 'manifest'), ], ) @@ -135,7 +135,7 @@ def main(): manifest=p['manifest'])) # Check if puppet is disabled here - if p['puppetmaster']: + if not p['manifest']: rc, stdout, stderr = module.run_command( PUPPET_CMD + " config print agent_disabled_lockfile") if os.path.exists(stdout.strip()): @@ -154,13 +154,14 @@ def main(): base_cmd = "timeout -s 9 %(timeout)s %(puppet_cmd)s" % dict( timeout=pipes.quote(p['timeout']), puppet_cmd=PUPPET_CMD) - if p['puppetmaster']: + if not p['manifest']: cmd = ("%(base_cmd)s agent --onetime" - " --server %(puppetmaster)s" " --ignorecache --no-daemonize --no-usecacheonfailure --no-splay" " --detailed-exitcodes --verbose") % dict( base_cmd=base_cmd, - puppetmaster=pipes.quote(p['puppetmaster'])) + ) + if p['puppetmaster']: + cmd += " -- server %s" % pipes.quote(p['puppetmaster']) if p['show_diff']: cmd += " --show-diff" if p['environment']: From b5d22eb1ec6c7cd2cbef14554fc92c86c2e24452 Mon Sep 17 00:00:00 2001 From: Pepe Barbe Date: Sun, 7 Jun 2015 13:18:33 -0500 Subject: [PATCH 0224/2522] Refactor win_chocolatey module * Refactor code to be more robust. Run main logic inside a try {} catch {} block. If there is any error, bail out and log all the command output automatically. * Rely on error code generated by chocolatey instead of scraping text output to determine success/failure. * Add support for unattended installs: (`-y` flag is a requirement by chocolatey) * Before (un)installing, check existence of files. * Use functions to abstract logic * The great rewrite of 0.9.9, the `choco` interface has changed, check if chocolatey is installed and an older version. If so upgrade to latest. * Allow upgrading packages that are already installed * Use verbose logging for chocolate actions * Adding functionality to specify a source for a chocolatey repository. (@smadam813) * Removing pre-determined sources and adding specified source url in it's place. (@smadam813) Contains contributions from: * Adam Keech (@smadam813) --- windows/win_chocolatey.ps1 | 339 ++++++++++++++++++++++--------------- windows/win_chocolatey.py | 43 ++--- 2 files changed, 218 insertions(+), 164 deletions(-) diff --git a/windows/win_chocolatey.ps1 b/windows/win_chocolatey.ps1 index de42434da76..4a033d23157 100644 --- a/windows/win_chocolatey.ps1 +++ b/windows/win_chocolatey.ps1 @@ -16,25 +16,11 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +$ErrorActionPreference = "Stop" + # WANT_JSON # POWERSHELL_COMMON -function Write-Log -{ - param - ( - [parameter(mandatory=$false)] - [System.String] - $message - ) - - $date = get-date -format 'yyyy-MM-dd hh:mm:ss.zz' - - Write-Host "$date | $message" - - Out-File -InputObject "$date $message" -FilePath $global:LoggingFile -Append -} - $params = Parse-Args $args; $result = New-Object PSObject; Set-Attr $result "changed" $false; @@ -48,21 +34,22 @@ Else Fail-Json $result "missing required argument: name" } -if(($params.logPath).length -gt 0) +If ($params.force) { - $global:LoggingFile = $params.logPath + $force = $params.force | ConvertTo-Bool } -else +Else { - $global:LoggingFile = "c:\ansible-playbook.log" + $force = $false } -If ($params.force) + +If ($params.upgrade) { - $force = $params.force | ConvertTo-Bool + $upgrade = $params.upgrade | ConvertTo-Bool } Else { - $force = $false + $upgrade = $false } If ($params.version) @@ -74,6 +61,15 @@ Else $version = $null } +If ($params.source) +{ + $source = $params.source.ToString().ToLower() +} +Else +{ + $source = $null +} + If ($params.showlog) { $showlog = $params.showlog | ConvertTo-Bool @@ -96,157 +92,230 @@ Else $state = "present" } -$ChocoAlreadyInstalled = get-command choco -ErrorAction 0 -if ($ChocoAlreadyInstalled -eq $null) +Function Chocolatey-Install-Upgrade { - #We need to install chocolatey - $install_choco_result = iex ((new-object net.webclient).DownloadString("https://chocolatey.org/install.ps1")) - $result.changed = $true - $executable = "C:\ProgramData\chocolatey\bin\choco.exe" -} -Else -{ - $executable = "choco.exe" -} + [CmdletBinding()] -If ($params.source) -{ - $source = $params.source.ToString().ToLower() - If (($source -ne "chocolatey") -and ($source -ne "webpi") -and ($source -ne "windowsfeatures") -and ($source -ne "ruby") -and (!$source.startsWith("http://", "CurrentCultureIgnoreCase")) -and (!$source.startsWith("https://", "CurrentCultureIgnoreCase"))) + param() + + $ChocoAlreadyInstalled = get-command choco -ErrorAction 0 + if ($ChocoAlreadyInstalled -eq $null) + { + #We need to install chocolatey + iex ((new-object net.webclient).DownloadString("https://chocolatey.org/install.ps1")) + $result.changed = $true + $script:executable = "C:\ProgramData\chocolatey\bin\choco.exe" + } + else { - Fail-Json $result "source is $source - must be one of chocolatey, ruby, webpi, windowsfeatures or a custom source url." + $script:executable = "choco.exe" + + if ((choco --version) -lt '0.9.9') + { + Choco-Upgrade chocolatey + } } } -Elseif (!$params.source) + + +Function Choco-IsInstalled { - $source = "chocolatey" + [CmdletBinding()] + + param( + [Parameter(Mandatory=$true, Position=1)] + [string]$package + ) + + $cmd = "$executable list --local-only $package" + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "choco_error_cmd" $cmd + Set-Attr $result "choco_error_log" "$results" + + Throw "Error checking installation status for $package" + } + + If ("$results" -match " $package .* (\d+) packages installed.") + { + return $matches[1] -gt 0 + } + + $false } -if ($source -eq "webpi") +Function Choco-Upgrade { - # check whether 'webpi' installation source is available; if it isn't, install it - $webpi_check_cmd = "$executable list webpicmd -localonly" - $webpi_check_result = invoke-expression $webpi_check_cmd - Set-Attr $result "chocolatey_bootstrap_webpi_check_cmd" $webpi_check_cmd - Set-Attr $result "chocolatey_bootstrap_webpi_check_log" $webpi_check_result - if ( - ( - ($webpi_check_result.GetType().Name -eq "String") -and - ($webpi_check_result -match "No packages found") - ) -or - ($webpi_check_result -contains "No packages found.") + [CmdletBinding()] + + param( + [Parameter(Mandatory=$true, Position=1)] + [string]$package, + [Parameter(Mandatory=$false, Position=2)] + [string]$version, + [Parameter(Mandatory=$false, Position=3)] + [string]$source, + [Parameter(Mandatory=$false, Position=4)] + [bool]$force ) + + if (-not (Choco-IsInstalled $package)) { - #lessmsi is a webpicmd dependency, but dependency resolution fails unless it's installed separately - $lessmsi_install_cmd = "$executable install lessmsi" - $lessmsi_install_result = invoke-expression $lessmsi_install_cmd - Set-Attr $result "chocolatey_bootstrap_lessmsi_install_cmd" $lessmsi_install_cmd - Set-Attr $result "chocolatey_bootstrap_lessmsi_install_log" $lessmsi_install_result + throw "$package is not installed, you cannot upgrade" + } - $webpi_install_cmd = "$executable install webpicmd" - $webpi_install_result = invoke-expression $webpi_install_cmd - Set-Attr $result "chocolatey_bootstrap_webpi_install_cmd" $webpi_install_cmd - Set-Attr $result "chocolatey_bootstrap_webpi_install_log" $webpi_install_result + $cmd = "$executable upgrade -dv -y $package" - if (($webpi_install_result | select-string "already installed").length -gt 0) - { - #no change - } - elseif (($webpi_install_result | select-string "webpicmd has finished successfully").length -gt 0) + if ($version) + { + $cmd += " -version $version" + } + + if ($source) + { + $cmd += " -source $source" + } + + if ($force) + { + $cmd += " -force" + } + + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "choco_error_cmd" $cmd + Set-Attr $result "choco_error_log" "$results" + Throw "Error installing $package" + } + + if ("$results" -match ' upgraded (\d+)/\d+ package\(s\)\. ') + { + if ($matches[1] -gt 0) { $result.changed = $true } - Else - { - Fail-Json $result "WebPI install error: $webpi_install_result" - } } } -$expression = $executable -if ($state -eq "present") -{ - $expression += " install $package" -} -Elseif ($state -eq "absent") -{ - $expression += " uninstall $package" -} -if ($force) + +Function Choco-Install { - if ($state -eq "present") + [CmdletBinding()] + + param( + [Parameter(Mandatory=$true, Position=1)] + [string]$package, + [Parameter(Mandatory=$false, Position=2)] + [string]$version, + [Parameter(Mandatory=$false, Position=3)] + [string]$source, + [Parameter(Mandatory=$false, Position=4)] + [bool]$force, + [Parameter(Mandatory=$false, Position=5)] + [bool]$upgrade + ) + + if (Choco-IsInstalled $package) { - $expression += " -force" + if ($upgrade) + { + Choco-Upgrade -package $package -version $version -source $source -force $force + } + + return } -} -if ($version) -{ - $expression += " -version $version" -} -if ($source -eq "chocolatey") -{ - $expression += " -source https://chocolatey.org/api/v2/" -} -elseif (($source -eq "windowsfeatures") -or ($source -eq "webpi") -or ($source -eq "ruby")) -{ - $expression += " -source $source" -} -elseif(($source -ne $Null) -and ($source -ne "")) -{ - $expression += " -source $source" -} -Set-Attr $result "chocolatey command" $expression -$op_result = invoke-expression $expression -if ($state -eq "present") -{ - if ( - (($op_result | select-string "already installed").length -gt 0) -or - # webpi has different text output, and that doesn't include the package name but instead the human-friendly name - (($op_result | select-string "No products to be installed").length -gt 0) - ) + $cmd = "$executable install -dv -y $package" + + if ($version) { - #no change + $cmd += " -version $version" } - elseif ( - (($op_result | select-string "has finished successfully").length -gt 0) -or - # webpi has different text output, and that doesn't include the package name but instead the human-friendly name - (($op_result | select-string "Install of Products: SUCCESS").length -gt 0) -or - (($op_result | select-string "gem installed").length -gt 0) -or - (($op_result | select-string "gems installed").length -gt 0) - ) + + if ($source) { - $result.changed = $true + $cmd += " -source $source" + } + + if ($force) + { + $cmd += " -force" } - Else + + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) { - Fail-Json $result "Install error: $op_result" + Set-Attr $result "choco_error_cmd" $cmd + Set-Attr $result "choco_error_log" "$results" + Throw "Error installing $package" } + + $result.changed = $true } -Elseif ($state -eq "absent") + +Function Choco-Uninstall { - $op_result = invoke-expression "$executable uninstall $package" - # HACK: Misleading - 'Uninstalling from folder' appears in output even when package is not installed, hence order of checks this way - if ( - (($op_result | select-string "not installed").length -gt 0) -or - (($op_result | select-string "Cannot find path").length -gt 0) + [CmdletBinding()] + + param( + [Parameter(Mandatory=$true, Position=1)] + [string]$package, + [Parameter(Mandatory=$false, Position=2)] + [string]$version, + [Parameter(Mandatory=$false, Position=3)] + [bool]$force ) + + if (-not (Choco-IsInstalled $package)) { - #no change + return } - elseif (($op_result | select-string "Uninstalling from folder").length -gt 0) + + $cmd = "$executable uninstall -dv -y $package" + + if ($version) { - $result.changed = $true + $cmd += " -version $version" } - else + + if ($force) { - Fail-Json $result "Uninstall error: $op_result" + $cmd += " -force" } + + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "choco_error_cmd" $cmd + Set-Attr $result "choco_error_log" "$results" + Throw "Error uninstalling $package" + } + + $result.changed = $true } +Try +{ + Chocolatey-Install-Upgrade + + if ($state -eq "present") + { + Choco-Install -package $package -version $version -source $source ` + -force $force -upgrade $upgrade + } + else + { + Choco-Uninstall -package $package -version $version -force $force + } -if ($showlog) + Exit-Json $result; +} +Catch { - Set-Attr $result "chocolatey_log" $op_result + Fail-Json $result $_.Exception.Message } -Set-Attr $result "chocolatey_success" "true" -Exit-Json $result; diff --git a/windows/win_chocolatey.py b/windows/win_chocolatey.py index 63ec1ecd214..fe00f2e0f6a 100644 --- a/windows/win_chocolatey.py +++ b/windows/win_chocolatey.py @@ -53,42 +53,29 @@ - no default: no aliases: [] - version: + upgrade: description: - - Specific version of the package to be installed - - Ignored when state == 'absent' - required: false - default: null - aliases: [] - showlog: - description: - - Outputs the chocolatey log inside a chocolatey_log property. + - If package is already installed it, try to upgrade to the latest version or to the specified version required: false choices: - yes - no default: no aliases: [] - source: + version: description: - - Which source to install from - require: false - choices: - - chocolatey - - ruby - - webpi - - windowsfeatures - default: chocolatey + - Specific version of the package to be installed + - Ignored when state == 'absent' + required: false + default: null aliases: [] - logPath: + source: description: - - Where to log command output to + - Specify source rather than using default chocolatey repository require: false - default: c:\\ansible-playbook.log + default: null aliases: [] -author: - - '"Trond Hindenes (@trondhindenes)" ' - - '"Peter Mounce (@petemounce)" ' +author: Trond Hindenes, Peter Mounce, Pepe Barbe, Adam Keech ''' # TODO: @@ -111,10 +98,8 @@ name: git state: absent - # Install Application Request Routing v3 from webpi - # Logically, this requires that you install IIS first (see win_feature) - # To find a list of packages available via webpi source, `choco list -source webpi` + # Install git from specified repository win_chocolatey: - name: ARRv3 - source: webpi + name: git + source: https://someserver/api/v2/ ''' From 16851baaf746ad7f5f29c50623c159329fdc219b Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Sun, 7 Jun 2015 17:45:33 -0400 Subject: [PATCH 0225/2522] added missing options: --- cloud/cloudstack/cs_project.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud/cloudstack/cs_project.py b/cloud/cloudstack/cs_project.py index b604a1b6f32..e604abc13db 100644 --- a/cloud/cloudstack/cs_project.py +++ b/cloud/cloudstack/cs_project.py @@ -26,6 +26,7 @@ - Create, update, suspend, activate and remove projects. version_added: '2.0' author: '"René Moser (@resmo)" ' +options: name: description: - Name of the project. From 2e6a16fbc7f4e8b919adf124a894ce1d8136737c Mon Sep 17 00:00:00 2001 From: "jonathan.lestrelin" Date: Mon, 8 Jun 2015 09:28:01 +0200 Subject: [PATCH 0226/2522] Fix unused import and variable and correct documentation --- packaging/language/pear.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packaging/language/pear.py b/packaging/language/pear.py index c9e3862a31f..5762f9c815c 100644 --- a/packaging/language/pear.py +++ b/packaging/language/pear.py @@ -26,16 +26,14 @@ short_description: Manage pear/pecl packages description: - Manage PHP packages with the pear package manager. +version_added: 2.0 author: - "'jonathan.lestrelin' " -notes: [] -requirements: [] options: name: description: - Name of the package to install, upgrade, or remove. required: true - default: null state: description: @@ -132,7 +130,7 @@ def remove_packages(module, packages): module.exit_json(changed=False, msg="package(s) already absent") -def install_packages(module, state, packages, package_files): +def install_packages(module, state, packages): install_c = 0 for i, package in enumerate(packages): @@ -178,7 +176,6 @@ def check_packages(module, packages, state): else: module.exit_json(change=False, msg="package(s) already %s" % state) -import os def exe_exists(program): for path in os.environ["PATH"].split(os.pathsep): @@ -220,7 +217,7 @@ def main(): check_packages(module, pkgs, p['state']) if p['state'] in ['present', 'latest']: - install_packages(module, p['state'], pkgs, pkg_files) + install_packages(module, p['state'], pkgs) elif p['state'] == 'absent': remove_packages(module, pkgs) From d722d6de976328d3fdbde64ca7ecef5cf5516037 Mon Sep 17 00:00:00 2001 From: Jhonny Everson Date: Mon, 8 Jun 2015 17:46:53 -0300 Subject: [PATCH 0227/2522] Adds handler for error responses --- monitoring/datadog_monitor.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py index 24de8af10ba..97968ed648d 100644 --- a/monitoring/datadog_monitor.py +++ b/monitoring/datadog_monitor.py @@ -187,7 +187,10 @@ def _post_monitor(module, options): msg = api.Monitor.create(type=module.params['type'], query=module.params['query'], name=module.params['name'], message=module.params['message'], options=options) - module.exit_json(changed=True, msg=msg) + if 'errors' in msg: + module.fail_json(msg=str(msg['errors'])) + else: + module.exit_json(changed=True, msg=msg) except Exception, e: module.fail_json(msg=str(e)) @@ -197,7 +200,9 @@ def _update_monitor(module, monitor, options): msg = api.Monitor.update(id=monitor['id'], query=module.params['query'], name=module.params['name'], message=module.params['message'], options=options) - if len(set(msg) - set(monitor)) == 0: + if 'errors' in msg: + module.fail_json(msg=str(msg['errors'])) + elif len(set(msg) - set(monitor)) == 0: module.exit_json(changed=False, msg=msg) else: module.exit_json(changed=True, msg=msg) @@ -243,7 +248,7 @@ def mute_monitor(module): module.fail_json(msg="Monitor %s not found!" % module.params['name']) elif monitor['options']['silenced']: module.fail_json(msg="Monitor is already muted. Datadog does not allow to modify muted alerts, consider unmuting it first.") - elif (module.params['silenced'] is not None + elif (module.params['silenced'] is not None and len(set(monitor['options']['silenced']) - set(module.params['silenced'])) == 0): module.exit_json(changed=False) try: From 1d49d4af092eb12329149f087e7bca2574509599 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Tue, 9 Jun 2015 13:06:24 +0200 Subject: [PATCH 0228/2522] cloudstack: fix project name must not be case sensitiv --- cloud/cloudstack/cs_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/cloudstack/cs_project.py b/cloud/cloudstack/cs_project.py index e604abc13db..13209853527 100644 --- a/cloud/cloudstack/cs_project.py +++ b/cloud/cloudstack/cs_project.py @@ -167,7 +167,7 @@ def get_project(self): projects = self.cs.listProjects(**args) if projects: for p in projects['project']: - if project in [ p['name'], p['id']]: + if project.lower() in [ p['name'].lower(), p['id']]: self.project = p break return self.project From ed0395e2cccab1506ff7f103122fc02e46ca6fb9 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Tue, 9 Jun 2015 13:08:38 +0200 Subject: [PATCH 0229/2522] cloudstack: remove listall in cs_project listall in cs_project can return the wrong project for root admins, because project name are not unique in separate accounts. --- cloud/cloudstack/cs_project.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cloud/cloudstack/cs_project.py b/cloud/cloudstack/cs_project.py index 13209853527..b505433892e 100644 --- a/cloud/cloudstack/cs_project.py +++ b/cloud/cloudstack/cs_project.py @@ -160,7 +160,6 @@ def get_project(self): project = self.module.params.get('name') args = {} - args['listall'] = True args['account'] = self.get_account(key='name') args['domainid'] = self.get_domain(key='id') From 4b625bab34a3bb57dd4d966473c42458c89f01f3 Mon Sep 17 00:00:00 2001 From: Jhonny Everson Date: Tue, 9 Jun 2015 09:44:34 -0300 Subject: [PATCH 0230/2522] Fixes the bug where it was using only the keys to determine whether a change was made, i.e. values changes for existing keys was reported incorrectly. --- monitoring/datadog_monitor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py index 97968ed648d..cb54cd32b5d 100644 --- a/monitoring/datadog_monitor.py +++ b/monitoring/datadog_monitor.py @@ -194,6 +194,10 @@ def _post_monitor(module, options): except Exception, e: module.fail_json(msg=str(e)) +def _equal_dicts(a, b, ignore_keys): + ka = set(a).difference(ignore_keys) + kb = set(b).difference(ignore_keys) + return ka == kb and all(a[k] == b[k] for k in ka) def _update_monitor(module, monitor, options): try: @@ -202,7 +206,7 @@ def _update_monitor(module, monitor, options): options=options) if 'errors' in msg: module.fail_json(msg=str(msg['errors'])) - elif len(set(msg) - set(monitor)) == 0: + elif _equal_dicts(msg, monitor, ['creator', 'overall_state']): module.exit_json(changed=False, msg=msg) else: module.exit_json(changed=True, msg=msg) From 3bd19b8ea0cee5b5274eae0dc15492253fec2346 Mon Sep 17 00:00:00 2001 From: David Siefert Date: Tue, 9 Jun 2015 10:21:33 -0500 Subject: [PATCH 0231/2522] Adding support for setting the topic of a channel --- notification/irc.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/notification/irc.py b/notification/irc.py index 8b87c41f1ba..e6852c8510a 100644 --- a/notification/irc.py +++ b/notification/irc.py @@ -47,6 +47,12 @@ - The message body. required: true default: null + topic: + description: + - Set the channel topic + required: false + default: null + version_added: 2.0 color: description: - Text color for the message. ("none" is a valid option in 1.6 or later, in 1.6 and prior, the default color is black, not "none"). @@ -106,7 +112,7 @@ from time import sleep -def send_msg(channel, msg, server='localhost', port='6667', key=None, +def send_msg(channel, msg, server='localhost', port='6667', key=None, topic=None, nick="ansible", color='none', passwd=False, timeout=30, use_ssl=False): '''send message to IRC''' @@ -163,6 +169,10 @@ def send_msg(channel, msg, server='localhost', port='6667', key=None, raise Exception('Timeout waiting for IRC JOIN response') sleep(0.5) + if topic is not None: + irc.send('TOPIC %s :%s\r\n' % (channel, topic)) + sleep(1) + irc.send('PRIVMSG %s :%s\r\n' % (channel, message)) sleep(1) irc.send('PART %s\r\n' % channel) @@ -186,6 +196,7 @@ def main(): "blue", "black", "none"]), channel=dict(required=True), key=dict(), + topic=dict(), passwd=dict(), timeout=dict(type='int', default=30), use_ssl=dict(type='bool', default=False) @@ -196,6 +207,7 @@ def main(): server = module.params["server"] port = module.params["port"] nick = module.params["nick"] + topic = module.params["topic"] msg = module.params["msg"] color = module.params["color"] channel = module.params["channel"] @@ -205,7 +217,7 @@ def main(): use_ssl = module.params["use_ssl"] try: - send_msg(channel, msg, server, port, key, nick, color, passwd, timeout, use_ssl) + send_msg(channel, msg, server, port, key, topic, nick, color, passwd, timeout, use_ssl) except Exception, e: module.fail_json(msg="unable to send to IRC: %s" % e) From 98abb6d2c9ee1e8ae4d88e3577edcd243622d685 Mon Sep 17 00:00:00 2001 From: Greg DeKoenigsberg Date: Tue, 9 Jun 2015 12:58:45 -0400 Subject: [PATCH 0232/2522] Adding author's github id --- monitoring/datadog_monitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py index cb54cd32b5d..f1acb169ce0 100644 --- a/monitoring/datadog_monitor.py +++ b/monitoring/datadog_monitor.py @@ -34,7 +34,7 @@ - "Manages monitors within Datadog" - "Options like described on http://docs.datadoghq.com/api/" version_added: "2.0" -author: '"Sebastian Kornehl" ' +author: '"Sebastian Kornehl (@skornehl)" ' notes: [] requirements: [datadog] options: From f33bbe6e496e9308d06512dcd1741419077c0252 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Wed, 10 Jun 2015 13:00:02 +0200 Subject: [PATCH 0233/2522] puppet: update author to new format --- system/puppet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/puppet.py b/system/puppet.py index 83bbcbe6e18..336b2c81108 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -65,7 +65,7 @@ required: false default: None requirements: [ puppet ] -author: Monty Taylor +author: "Monty Taylor (@emonty)" ''' EXAMPLES = ''' From 0d7332d550f9896732e586cc17492f99724f748f Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Wed, 10 Jun 2015 12:58:44 -0400 Subject: [PATCH 0234/2522] minor docfix --- monitoring/nagios.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/nagios.py b/monitoring/nagios.py index 543f094b70e..0026751ea58 100644 --- a/monitoring/nagios.py +++ b/monitoring/nagios.py @@ -77,7 +77,7 @@ version_added: "2.0" description: - the Servicegroup we want to set downtimes/alerts for. - B(Required) option when using the C(servicegroup_service_downtime) amd C(servicegroup_host_downtime). + B(Required) option when using the C(servicegroup_service_downtime) amd C(servicegroup_host_downtime). command: description: - The raw command to send to nagios, which From c842c71708b3242ad9c15f4d0251fdbf9d0f2aaf Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Wed, 10 Jun 2015 23:31:48 +0200 Subject: [PATCH 0235/2522] cloudstack: add new module cs_network --- cloud/cloudstack/cs_network.py | 637 +++++++++++++++++++++++++++++++++ 1 file changed, 637 insertions(+) create mode 100644 cloud/cloudstack/cs_network.py diff --git a/cloud/cloudstack/cs_network.py b/cloud/cloudstack/cs_network.py new file mode 100644 index 00000000000..c8b3b32539d --- /dev/null +++ b/cloud/cloudstack/cs_network.py @@ -0,0 +1,637 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2015, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: cs_network +short_description: Manages networks on Apache CloudStack based clouds. +description: + - Create, update, restart and delete networks. +version_added: '2.0' +author: '"René Moser (@resmo)" ' +options: + name: + description: + - Name (case sensitive) of the network. + required: true + displaytext: + description: + - Displaytext of the network. + - If not specified, C(name) will be used as displaytext. + required: false + default: null + network_offering: + description: + - Name of the offering for the network. + - Required if C(state=present). + required: false + default: null + start_ip: + description: + - The beginning IPv4 address of the network belongs to. + - Only considered on create. + required: false + default: null + end_ip: + description: + - The ending IPv4 address of the network belongs to. + - If not specified, value of C(start_ip) is used. + - Only considered on create. + required: false + default: null + gateway: + description: + - The gateway of the network. + - Required for shared networks and isolated networks when it belongs to VPC. + - Only considered on create. + required: false + default: null + netmask: + description: + - The netmask of the network. + - Required for shared networks and isolated networks when it belongs to VPC. + - Only considered on create. + required: false + default: null + start_ipv6: + description: + - The beginning IPv6 address of the network belongs to. + - Only considered on create. + required: false + default: null + end_ipv6: + description: + - The ending IPv6 address of the network belongs to. + - If not specified, value of C(start_ipv6) is used. + - Only considered on create. + required: false + default: null + cidr_ipv6: + description: + - CIDR of IPv6 network, must be at least /64. + - Only considered on create. + required: false + default: null + gateway_ipv6: + description: + - The gateway of the IPv6 network. + - Required for shared networks. + - Only considered on create. + required: false + default: null + vlan: + description: + - The ID or VID of the network. + required: false + default: null + vpc: + description: + - The ID or VID of the network. + required: false + default: null + isolated_pvlan: + description: + - The isolated private vlan for this network. + required: false + default: null + clean_up: + description: + - Cleanup old network elements. + - Only considered on C(state=restarted). + required: false + default: null + acl_type: + description: + - Access control type. + - Only considered on create. + required: false + default: account + choices: [ 'account', 'domain' ] + network_domain: + description: + - The network domain. + required: false + default: null + state: + description: + - State of the network. + required: false + default: present + choices: [ 'present', 'absent', 'restarted' ] + zone: + description: + - Name of the zone in which the network should be deployed. + - If not set, default zone is used. + required: false + default: null + project: + description: + - Name of the project the network to be deployed in. + required: false + default: null + domain: + description: + - Domain the network is related to. + required: false + default: null + account: + description: + - Account the network is related to. + required: false + default: null + poll_async: + description: + - Poll async jobs until job has finished. + required: false + default: true +extends_documentation_fragment: cloudstack +''' + +EXAMPLES = ''' +# create a network +- local_action: + module: cs_network + name: my network + zone: gva-01 + network_offering: DefaultIsolatedNetworkOfferingWithSourceNatService + network_domain: example.com + +# update a network +- local_action: + module: cs_network + name: my network + displaytext: network of domain example.local + network_domain: example.local + +# restart a network with clean up +- local_action: + module: cs_network + name: my network + clean_up: yes + state: restared + +# remove a network +- local_action: + module: cs_network + name: my network + state: absent +''' + +RETURN = ''' +--- +id: + description: ID of the network. + returned: success + type: string + sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6 +name: + description: Name of the network. + returned: success + type: string + sample: web project +displaytext: + description: Display text of the network. + returned: success + type: string + sample: web project +dns1: + description: IP address of the 1st nameserver. + returned: success + type: string + sample: 1.2.3.4 +dns2: + description: IP address of the 2nd nameserver. + returned: success + type: string + sample: 1.2.3.4 +cidr: + description: IPv4 network CIDR. + returned: success + type: string + sample: 10.101.64.0/24 +gateway: + description: IPv4 gateway. + returned: success + type: string + sample: 10.101.64.1 +netmask: + description: IPv4 netmask. + returned: success + type: string + sample: 255.255.255.0 +cidr_ipv6: + description: IPv6 network CIDR. + returned: success + type: string + sample: 2001:db8::/64 +gateway_ipv6: + description: IPv6 gateway. + returned: success + type: string + sample: 2001:db8::1 +state: + description: State of the network. + returned: success + type: string + sample: Implemented +zone: + description: Name of zone. + returned: success + type: string + sample: ch-gva-2 +domain: + description: Domain the network is related to. + returned: success + type: string + sample: ROOT +account: + description: Account the network is related to. + returned: success + type: string + sample: example account +project: + description: Name of project. + returned: success + type: string + sample: Production +tags: + description: List of resource tags associated with the network. + returned: success + type: dict + sample: '[ { "key": "foo", "value": "bar" } ]' +acl_type: + description: Access type of the network (Domain, Account). + returned: success + type: string + sample: Account +broadcast_domaintype: + description: Broadcast domain type of the network. + returned: success + type: string + sample: Vlan +type: + description: Type of the network. + returned: success + type: string + sample: Isolated +traffic_type: + description: Traffic type of the network. + returned: success + type: string + sample: Guest +state: + description: State of the network (Allocated, Implemented, Setup). + returned: success + type: string + sample: Allocated +is_persistent: + description: Whether the network is persistent or not. + returned: success + type: boolean + sample: false +network_domain: + description: The network domain + returned: success + type: string + sample: example.local +network_offering: + description: The network offering name. + returned: success + type: string + sample: DefaultIsolatedNetworkOfferingWithSourceNatService +''' + +try: + from cs import CloudStack, CloudStackException, read_config + has_lib_cs = True +except ImportError: + has_lib_cs = False + +# import cloudstack common +from ansible.module_utils.cloudstack import * + + +class AnsibleCloudStackNetwork(AnsibleCloudStack): + + def __init__(self, module): + AnsibleCloudStack.__init__(self, module) + self.network = None + + + def get_or_fallback(self, key=None, fallback_key=None): + value = self.module.params.get(key) + if not value: + value = self.module.params.get(fallback_key) + return value + + + def get_vpc(self, key=None): + vpc = self.module.params.get('vpc') + if not vpc: + return None + + args = {} + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') + args['projectid'] = self.get_project(key='id') + args['zoneid'] = self.get_zone(key='id') + + vpcs = self.cs.listVPCs(**args) + if vpcs: + for v in vpcs['vpc']: + if vpc in [ v['name'], v['displaytext'], v['id'] ]: + return self._get_by_key(key, v) + self.module.fail_json(msg="VPC '%s' not found" % vpc) + + + def get_network_offering(self, key=None): + network_offering = self.module.params.get('network_offering') + if not network_offering: + self.module.fail_json(msg="missing required arguments: network_offering") + + args = {} + args['zoneid'] = self.get_zone(key='id') + + network_offerings = self.cs.listNetworkOfferings(**args) + if network_offerings: + for no in network_offerings['networkoffering']: + if network_offering in [ no['name'], no['displaytext'], no['id'] ]: + return self._get_by_key(key, no) + self.module.fail_json(msg="Network offering '%s' not found" % network_offering) + + + def _get_args(self): + args = {} + args['name'] = self.module.params.get('name') + args['displaytext'] = self.get_or_fallback('displaytext','name') + args['networkdomain'] = self.module.params.get('network_domain') + args['networkofferingid'] = self.get_network_offering(key='id') + return args + + + def get_network(self): + if not self.network: + network = self.module.params.get('name') + + args = {} + args['zoneid'] = self.get_zone(key='id') + args['projectid'] = self.get_project(key='id') + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') + + networks = self.cs.listNetworks(**args) + if networks: + for n in networks['network']: + if network in [ n['name'], n['displaytext'], n['id']]: + self.network = n + break + return self.network + + + def present_network(self): + network = self.get_network() + if not network: + network = self.create_network(network) + else: + network = self.update_network(network) + return network + + + def update_network(self, network): + args = self._get_args() + args['id'] = network['id'] + + if self._has_changed(args, network): + self.result['changed'] = True + if not self.module.check_mode: + network = self.cs.updateNetwork(**args) + + if 'errortext' in network: + self.module.fail_json(msg="Failed: '%s'" % network['errortext']) + + poll_async = self.module.params.get('poll_async') + if network and poll_async: + network = self._poll_job(network, 'network') + return network + + + def create_network(self, network): + self.result['changed'] = True + + args = self._get_args() + args['acltype'] = self.module.params.get('acl_type') + args['zoneid'] = self.get_zone(key='id') + args['projectid'] = self.get_project(key='id') + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') + args['startip'] = self.module.params.get('start_ip') + args['endip'] = self.get_or_fallback('end_ip', 'start_ip') + args['netmask'] = self.module.params.get('netmask') + args['gateway'] = self.module.params.get('gateway') + args['startipv6'] = self.module.params.get('start_ipv6') + args['endipv6'] = self.get_or_fallback('end_ipv6', 'start_ipv6') + args['ip6cidr'] = self.module.params.get('cidr_ipv6') + args['ip6gateway'] = self.module.params.get('gateway_ipv6') + args['vlan'] = self.module.params.get('vlan') + args['isolatedpvlan'] = self.module.params.get('isolated_pvlan') + args['subdomainaccess'] = self.module.params.get('subdomain_access') + args['vpcid'] = self.get_vpc(key='id') + + if not self.module.check_mode: + res = self.cs.createNetwork(**args) + + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + + network = res['network'] + return network + + + def restart_network(self): + network = self.get_network() + + if not network: + self.module.fail_json(msg="No network named '%s' found." % self.module.params('name')) + + # Restarting only available for these states + if network['state'].lower() in [ 'implemented', 'setup' ]: + self.result['changed'] = True + + args = {} + args['id'] = network['id'] + args['cleanup'] = self.module.params.get('clean_up') + + if not self.module.check_mode: + network = self.cs.restartNetwork(**args) + + if 'errortext' in network: + self.module.fail_json(msg="Failed: '%s'" % network['errortext']) + + poll_async = self.module.params.get('poll_async') + if network and poll_async: + network = self._poll_job(network, 'network') + return network + + + def absent_network(self): + network = self.get_network() + if network: + self.result['changed'] = True + + args = {} + args['id'] = network['id'] + + if not self.module.check_mode: + res = self.cs.deleteNetwork(**args) + + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + + poll_async = self.module.params.get('poll_async') + if res and poll_async: + res = self._poll_job(res, 'network') + return network + + + def get_result(self, network): + if network: + if 'id' in network: + self.result['id'] = network['id'] + if 'name' in network: + self.result['name'] = network['name'] + if 'displaytext' in network: + self.result['displaytext'] = network['displaytext'] + if 'dns1' in network: + self.result['dns1'] = network['dns1'] + if 'dns2' in network: + self.result['dns2'] = network['dns2'] + if 'cidr' in network: + self.result['cidr'] = network['cidr'] + if 'broadcastdomaintype' in network: + self.result['broadcast_domaintype'] = network['broadcastdomaintype'] + if 'netmask' in network: + self.result['netmask'] = network['netmask'] + if 'gateway' in network: + self.result['gateway'] = network['gateway'] + if 'ip6cidr' in network: + self.result['cidr_ipv6'] = network['ip6cidr'] + if 'ip6gateway' in network: + self.result['gateway_ipv6'] = network['ip6gateway'] + if 'state' in network: + self.result['state'] = network['state'] + if 'type' in network: + self.result['type'] = network['type'] + if 'traffictype' in network: + self.result['traffic_type'] = network['traffictype'] + if 'zone' in network: + self.result['zone'] = network['zonename'] + if 'domain' in network: + self.result['domain'] = network['domain'] + if 'account' in network: + self.result['account'] = network['account'] + if 'project' in network: + self.result['project'] = network['project'] + if 'acltype' in network: + self.result['acl_type'] = network['acltype'] + if 'networkdomain' in network: + self.result['network_domain'] = network['networkdomain'] + if 'networkofferingname' in network: + self.result['network_offering'] = network['networkofferingname'] + if 'ispersistent' in network: + self.result['is_persistent'] = network['ispersistent'] + if 'tags' in network: + self.result['tags'] = [] + for tag in network['tags']: + result_tag = {} + result_tag['key'] = tag['key'] + result_tag['value'] = tag['value'] + self.result['tags'].append(result_tag) + return self.result + + +def main(): + module = AnsibleModule( + argument_spec = dict( + name = dict(required=True), + displaytext = dict(default=None), + network_offering = dict(default=None), + zone = dict(default=None), + start_ip = dict(default=None), + end_ip = dict(default=None), + gateway = dict(default=None), + netmask = dict(default=None), + start_ipv6 = dict(default=None), + end_ipv6 = dict(default=None), + cidr_ipv6 = dict(default=None), + gateway_ipv6 = dict(default=None), + vlan = dict(default=None), + vpc = dict(default=None), + isolated_pvlan = dict(default=None), + clean_up = dict(default=None), + network_domain = dict(default=None), + state = dict(choices=['present', 'absent', 'restarted' ], default='present'), + acl_type = dict(choices=['account', 'domain'], default='account'), + project = dict(default=None), + domain = dict(default=None), + account = dict(default=None), + poll_async = dict(type='bool', choices=BOOLEANS, default=True), + api_key = dict(default=None), + api_secret = dict(default=None, no_log=True), + api_url = dict(default=None), + api_http_method = dict(choices=['get', 'post'], default='get'), + api_timeout = dict(type='int', default=10), + ), + required_together = ( + ['api_key', 'api_secret', 'api_url'], + ['start_ip', 'netmask', 'gateway'], + ['start_ipv6', 'cidr_ipv6', 'gateway_ipv6'], + ), + supports_check_mode=True + ) + + if not has_lib_cs: + module.fail_json(msg="python library cs required: pip install cs") + + try: + acs_network = AnsibleCloudStackNetwork(module) + + state = module.params.get('state') + if state in ['absent']: + network = acs_network.absent_network() + + elif state in ['restarted']: + network = acs_network.restart_network() + + else: + network = acs_network.present_network() + + result = acs_network.get_result(network) + + except CloudStackException, e: + module.fail_json(msg='CloudStackException: %s' % str(e)) + + except Exception, e: + module.fail_json(msg='Exception: %s' % str(e)) + + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * +main() From 51cf9a029a3967ba2bc84fb9aa25b2cb3c71c423 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Thu, 11 Jun 2015 11:36:34 -0500 Subject: [PATCH 0236/2522] Add new module 'expect' --- commands/__init__.py | 0 commands/expect.py | 189 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 commands/__init__.py create mode 100644 commands/expect.py diff --git a/commands/__init__.py b/commands/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/commands/expect.py b/commands/expect.py new file mode 100644 index 00000000000..0922ba4e464 --- /dev/null +++ b/commands/expect.py @@ -0,0 +1,189 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Matt Martz +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import datetime + +try: + import pexpect + HAS_PEXPECT = True +except ImportError: + HAS_PEXPECT = False + + +DOCUMENTATION = ''' +--- +module: expect +version_added: 2.0 +short_description: Executes a command and responds to prompts +description: + - The M(expect) module executes a command and responds to prompts + - The given command will be executed on all selected nodes. It will not be + processed through the shell, so variables like C($HOME) and operations + like C("<"), C(">"), C("|"), and C("&") will not work +options: + command: + description: + - the command module takes command to run. + required: true + creates: + description: + - a filename, when it already exists, this step will B(not) be run. + required: false + removes: + description: + - a filename, when it does not exist, this step will B(not) be run. + required: false + chdir: + description: + - cd into this directory before running the command + required: false + executable: + description: + - change the shell used to execute the command. Should be an absolute + path to the executable. + required: false + responses: + description: + - Mapping of expected string and string to respond with + required: true + timeout: + description: + - Amount of time in seconds to wait for the expected strings + default: 30 + echo: + description: + - Whether or not to echo out your response strings + default: false +requirements: + - python >= 2.6 + - pexpect >= 3.3 +notes: + - If you want to run a command through the shell (say you are using C(<), + C(>), C(|), etc), you must specify a shell in the command such as + C(/bin/bash -c "/path/to/something | grep else") +author: '"Matt Martz (@sivel)" ' +''' + +EXAMPLES = ''' +- expect: + command: passwd username + responses: + (?i)password: "MySekretPa$$word" +''' + + +def main(): + module = AnsibleModule( + argument_spec=dict( + command=dict(required=True), + chdir=dict(), + executable=dict(), + creates=dict(), + removes=dict(), + responses=dict(type='dict', required=True), + timeout=dict(type='int', default=30), + echo=dict(type='bool', default=False), + ) + ) + + if not HAS_PEXPECT: + module.fail_json(msg='The pexpect python module is required') + + chdir = module.params['chdir'] + executable = module.params['executable'] + args = module.params['command'] + creates = module.params['creates'] + removes = module.params['removes'] + responses = module.params['responses'] + timeout = module.params['timeout'] + echo = module.params['echo'] + + events = dict() + for key, value in responses.iteritems(): + events[key.decode()] = u'%s\n' % value.rstrip('\n').decode() + + if args.strip() == '': + module.fail_json(rc=256, msg="no command given") + + if chdir: + chdir = os.path.abspath(os.path.expanduser(chdir)) + os.chdir(chdir) + + if creates: + # do not run the command if the line contains creates=filename + # and the filename already exists. This allows idempotence + # of command executions. + v = os.path.expanduser(creates) + if os.path.exists(v): + module.exit_json( + cmd=args, + stdout="skipped, since %s exists" % v, + changed=False, + stderr=False, + rc=0 + ) + + if removes: + # do not run the command if the line contains removes=filename + # and the filename does not exist. This allows idempotence + # of command executions. + v = os.path.expanduser(removes) + if not os.path.exists(v): + module.exit_json( + cmd=args, + stdout="skipped, since %s does not exist" % v, + changed=False, + stderr=False, + rc=0 + ) + + startd = datetime.datetime.now() + + if executable: + cmd = '%s %s' % (executable, args) + else: + cmd = args + + try: + out, rc = pexpect.runu(cmd, timeout=timeout, withexitstatus=True, + events=events, cwd=chdir, echo=echo) + except pexpect.ExceptionPexpect, e: + module.fail_json(msg='%s' % e) + + endd = datetime.datetime.now() + delta = endd - startd + + if out is None: + out = '' + + module.exit_json( + cmd=args, + stdout=out.rstrip('\r\n'), + rc=rc, + start=str(startd), + end=str(endd), + delta=str(delta), + changed=True, + ) + +# import module snippets +from ansible.module_utils.basic import * + +main() From ae75c26f870b92880d801c0968578e18f7eeddbc Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Thu, 11 Jun 2015 12:36:47 -0500 Subject: [PATCH 0237/2522] Remove the executable option as it's redundant --- commands/expect.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/commands/expect.py b/commands/expect.py index 0922ba4e464..124c718b73b 100644 --- a/commands/expect.py +++ b/commands/expect.py @@ -54,11 +54,6 @@ description: - cd into this directory before running the command required: false - executable: - description: - - change the shell used to execute the command. Should be an absolute - path to the executable. - required: false responses: description: - Mapping of expected string and string to respond with @@ -94,7 +89,6 @@ def main(): argument_spec=dict( command=dict(required=True), chdir=dict(), - executable=dict(), creates=dict(), removes=dict(), responses=dict(type='dict', required=True), @@ -107,7 +101,6 @@ def main(): module.fail_json(msg='The pexpect python module is required') chdir = module.params['chdir'] - executable = module.params['executable'] args = module.params['command'] creates = module.params['creates'] removes = module.params['removes'] @@ -156,13 +149,8 @@ def main(): startd = datetime.datetime.now() - if executable: - cmd = '%s %s' % (executable, args) - else: - cmd = args - try: - out, rc = pexpect.runu(cmd, timeout=timeout, withexitstatus=True, + out, rc = pexpect.runu(args, timeout=timeout, withexitstatus=True, events=events, cwd=chdir, echo=echo) except pexpect.ExceptionPexpect, e: module.fail_json(msg='%s' % e) From ea0f0ec7d37493c0f87dc3feffa832a7932cb938 Mon Sep 17 00:00:00 2001 From: Alex Lo Date: Fri, 12 Jun 2015 00:49:37 -0400 Subject: [PATCH 0238/2522] remove extraneous imports --- cloud/amazon/cloudtrail.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/cloud/amazon/cloudtrail.py b/cloud/amazon/cloudtrail.py index 6a1885d6ee7..d6ed254df91 100644 --- a/cloud/amazon/cloudtrail.py +++ b/cloud/amazon/cloudtrail.py @@ -90,11 +90,6 @@ local_action: cloudtrail state=absent name=main region=us-east-1 """ -import time -import sys -import os -from collections import Counter - boto_import_failed = False try: import boto From a86c8ab02553a99d554d6e5630646b0ae5031319 Mon Sep 17 00:00:00 2001 From: Alex Lo Date: Fri, 12 Jun 2015 00:49:59 -0400 Subject: [PATCH 0239/2522] There is no absent, only disabled --- cloud/amazon/cloudtrail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/cloudtrail.py b/cloud/amazon/cloudtrail.py index d6ed254df91..eb445768ed5 100644 --- a/cloud/amazon/cloudtrail.py +++ b/cloud/amazon/cloudtrail.py @@ -87,7 +87,7 @@ s3_key_prefix='' region=us-east-1 - name: remove cloudtrail - local_action: cloudtrail state=absent name=main region=us-east-1 + local_action: cloudtrail state=disabled name=main region=us-east-1 """ boto_import_failed = False From 59c3913e0bb6e3800297d270edc034a7952b26bb Mon Sep 17 00:00:00 2001 From: Alex Lo Date: Fri, 12 Jun 2015 00:50:27 -0400 Subject: [PATCH 0240/2522] Fix boto library checking --- cloud/amazon/cloudtrail.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/cloud/amazon/cloudtrail.py b/cloud/amazon/cloudtrail.py index eb445768ed5..5a87f35e918 100644 --- a/cloud/amazon/cloudtrail.py +++ b/cloud/amazon/cloudtrail.py @@ -90,13 +90,14 @@ local_action: cloudtrail state=disabled name=main region=us-east-1 """ -boto_import_failed = False +HAS_BOTO = False try: import boto import boto.cloudtrail from boto.regioninfo import RegionInfo + HAS_BOTO = True except ImportError: - boto_import_failed = True + HAS_BOTO = False class CloudTrailManager: """Handles cloudtrail configuration""" @@ -147,9 +148,6 @@ def delete(self, name): def main(): - if not has_libcloud: - module.fail_json(msg='boto is required.') - argument_spec = ec2_argument_spec() argument_spec.update(dict( state={'required': True, 'choices': ['enabled', 'disabled'] }, @@ -161,6 +159,10 @@ def main(): required_together = ( ['state', 's3_bucket_name'] ) module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True, required_together=required_together) + + if not HAS_BOTO: + module.fail_json(msg='Alex sucks boto is required.') + ec2_url, access_key, secret_key, region = get_ec2_creds(module) aws_connect_params = dict(aws_access_key_id=access_key, aws_secret_access_key=secret_key) From 90b6f0fe68187f4067d716c3313d1fb837fb906c Mon Sep 17 00:00:00 2001 From: Alex Lo Date: Fri, 12 Jun 2015 01:31:45 -0400 Subject: [PATCH 0241/2522] Error message typo --- cloud/amazon/cloudtrail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/cloudtrail.py b/cloud/amazon/cloudtrail.py index 5a87f35e918..962473e6a9e 100644 --- a/cloud/amazon/cloudtrail.py +++ b/cloud/amazon/cloudtrail.py @@ -161,7 +161,7 @@ def main(): module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True, required_together=required_together) if not HAS_BOTO: - module.fail_json(msg='Alex sucks boto is required.') + module.fail_json(msg='boto is required.') ec2_url, access_key, secret_key, region = get_ec2_creds(module) aws_connect_params = dict(aws_access_key_id=access_key, From 0c6e5b9eb4a443aeda5ac377518ade16172d2d82 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Fri, 12 Jun 2015 14:11:38 -0400 Subject: [PATCH 0242/2522] fixed doc issues --- network/nmcli.py | 71 ++++++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/network/nmcli.py b/network/nmcli.py index 18f0ecbab1f..45043fd2807 100644 --- a/network/nmcli.py +++ b/network/nmcli.py @@ -25,6 +25,7 @@ author: Chris Long short_description: Manage Networking requirements: [ nmcli, dbus ] +version_added: "2.0" description: - Manage the network devices. Create, modify, and manage, ethernet, teams, bonds, vlans etc. options: @@ -39,11 +40,11 @@ choices: [ "yes", "no" ] description: - Whether the connection should start on boot. - - Whether the connection profile can be automatically activated ( default: yes) + - Whether the connection profile can be automatically activated conn_name: required: True description: - - Where conn_name will be the name used to call the connection. when not provided a default name is generated: [-][-] + - 'Where conn_name will be the name used to call the connection. when not provided a default name is generated: [-][-]' ifname: required: False default: conn_name @@ -60,9 +61,9 @@ mode: required: False choices: [ "balance-rr", "active-backup", "balance-xor", "broadcast", "802.3ad", "balance-tlb", "balance-alb" ] - default: None + default: balence-rr description: - - This is the type of device or network connection that you wish to create for a bond, team or bridge. (NetworkManager default: balance-rr) + - This is the type of device or network connection that you wish to create for a bond, team or bridge. master: required: False default: None @@ -72,35 +73,35 @@ required: False default: None description: - - The IPv4 address to this interface using this format ie: "192.168.1.24/24" + - 'The IPv4 address to this interface using this format ie: "192.168.1.24/24"' gw4: required: False description: - - The IPv4 gateway for this interface using this format ie: "192.168.100.1" + - 'The IPv4 gateway for this interface using this format ie: "192.168.100.1"' dns4: required: False default: None description: - - A list of upto 3 dns servers, ipv4 format e.g. To add two IPv4 DNS server addresses: ['"8.8.8.8 8.8.4.4"'] + - 'A list of upto 3 dns servers, ipv4 format e.g. To add two IPv4 DNS server addresses: ["8.8.8.8 8.8.4.4"]' ip6: required: False default: None description: - - The IPv6 address to this interface using this format ie: "abbe::cafe" + - 'The IPv6 address to this interface using this format ie: "abbe::cafe"' gw6: required: False default: None description: - - The IPv6 gateway for this interface using this format ie: "2001:db8::1" + - 'The IPv6 gateway for this interface using this format ie: "2001:db8::1"' dns6: required: False description: - - A list of upto 3 dns servers, ipv6 format e.g. To add two IPv6 DNS server addresses: ['"2001:4860:4860::8888 2001:4860:4860::8844"'] + - 'A list of upto 3 dns servers, ipv6 format e.g. To add two IPv6 DNS server addresses: ["2001:4860:4860::8888 2001:4860:4860::8844"]' mtu: required: False - default: None + default: 1500 description: - - The connection MTU, e.g. 9000. This can't be applied when creating the interface and is done once the interface has been created. (NetworkManager default: 1500) + - The connection MTU, e.g. 9000. This can't be applied when creating the interface and is done once the interface has been created. - Can be used when modifying Team, VLAN, Ethernet (Future plans to implement wifi, pppoe, infiniband) primary: required: False @@ -109,24 +110,24 @@ - This is only used with bond and is the primary interface name (for "active-backup" mode), this is the usually the 'ifname' miimon: required: False - default: None + default: 100 description: - - This is only used with bond - miimon (NetworkManager default: 100) + - This is only used with bond - miimon downdelay: required: False default: None description: - - This is only used with bond - downdelay (NetworkManager default: 0) + - This is only used with bond - downdelay updelay: required: False default: None description: - - This is only used with bond - updelay (NetworkManager default: 0) + - This is only used with bond - updelay arp_interval: required: False default: None description: - - This is only used with bond - ARP interval (NetworkManager default: 0) + - This is only used with bond - ARP interval arp_ip_target: required: False default: None @@ -139,49 +140,49 @@ - This is only used with bridge and controls whether Spanning Tree Protocol (STP) is enabled for this bridge priority: required: False - default: None + default: 128 description: - - This is only used with 'bridge' - sets STP priority (NetworkManager default: 128) + - This is only used with 'bridge' - sets STP priority forwarddelay: required: False - default: None + default: 15 description: - - This is only used with bridge - [forward-delay <2-30>] STP forwarding delay, in seconds (NetworkManager default: 15) + - This is only used with bridge - [forward-delay <2-30>] STP forwarding delay, in seconds hellotime: required: False - default: None + default: 2 description: - - This is only used with bridge - [hello-time <1-10>] STP hello time, in seconds (NetworkManager default: 2) + - This is only used with bridge - [hello-time <1-10>] STP hello time, in seconds maxage: required: False - default: None + default: 20 description: - - This is only used with bridge - [max-age <6-42>] STP maximum message age, in seconds (NetworkManager default: 20) + - This is only used with bridge - [max-age <6-42>] STP maximum message age, in seconds ageingtime: required: False - default: None + default: 300 description: - - This is only used with bridge - [ageing-time <0-1000000>] the Ethernet MAC address aging time, in seconds (NetworkManager default: 300) + - This is only used with bridge - [ageing-time <0-1000000>] the Ethernet MAC address aging time, in seconds mac: required: False default: None description: - - This is only used with bridge - MAC address of the bridge (note: this requires a recent kernel feature, originally introduced in 3.15 upstream kernel) + - 'This is only used with bridge - MAC address of the bridge (note: this requires a recent kernel feature, originally introduced in 3.15 upstream kernel)' slavepriority: required: False - default: None + default: 32 description: - - This is only used with 'bridge-slave' - [<0-63>] - STP priority of this slave (default: 32) + - This is only used with 'bridge-slave' - [<0-63>] - STP priority of this slave path_cost: required: False - default: None + default: 100 description: - - This is only used with 'bridge-slave' - [<1-65535>] - STP port cost for destinations via this slave (NetworkManager default: 100) + - This is only used with 'bridge-slave' - [<1-65535>] - STP port cost for destinations via this slave hairpin: required: False - default: None + default: yes description: - - This is only used with 'bridge-slave' - 'hairpin mode' for the slave, which allows frames to be sent back out through the slave the frame was received on. (NetworkManager default: yes) + - This is only used with 'bridge-slave' - 'hairpin mode' for the slave, which allows frames to be sent back out through the slave the frame was received on. vlanid: required: False default: None @@ -1066,4 +1067,4 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() \ No newline at end of file +main() From 68dc905b5fa57874dc4161c88ea981cb52485a36 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sun, 12 Apr 2015 23:09:45 +0200 Subject: [PATCH 0243/2522] cloudstack: add new module cs_template --- cloud/cloudstack/cs_template.py | 633 ++++++++++++++++++++++++++++++++ 1 file changed, 633 insertions(+) create mode 100644 cloud/cloudstack/cs_template.py diff --git a/cloud/cloudstack/cs_template.py b/cloud/cloudstack/cs_template.py new file mode 100644 index 00000000000..48f00fad553 --- /dev/null +++ b/cloud/cloudstack/cs_template.py @@ -0,0 +1,633 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2015, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: cs_template +short_description: Manages templates on Apache CloudStack based clouds. +description: + - Register a template from URL, create a template from a ROOT volume of a stopped VM or its snapshot and delete templates. +version_added: '2.0' +author: '"René Moser (@resmo)" ' +options: + name: + description: + - Name of the template. + required: true + url: + description: + - URL of where the template is hosted. + - Mutually exclusive with C(vm). + required: false + default: null + vm: + description: + - VM name the template will be created from its volume or alternatively from a snapshot. + - VM must be in stopped state if created from its volume. + - Mutually exclusive with C(url). + required: false + default: null + snapshot: + description: + - Name of the snapshot, created from the VM ROOT volume, the template will be created from. + - C(vm) is required together with this argument. + required: false + default: null + os_type: + description: + - OS type that best represents the OS of this template. + required: false + default: null + checksum: + description: + - The MD5 checksum value of this template. + - If set, we search by checksum instead of name. + required: false + default: false + is_ready: + description: + - This flag is used for searching existing templates. + - If set to C(true), it will only list template ready for deployment e.g. successfully downloaded and installed. + - Recommended to set it to C(false). + required: false + default: false + is_public: + description: + - Register the template to be publicly available to all users. + - Only used if C(state) is present. + required: false + default: false + is_featured: + description: + - Register the template to be featured. + - Only used if C(state) is present. + required: false + default: false + is_dynamically_scalable: + description: + - Register the template having XS/VMWare tools installed in order to support dynamic scaling of VM CPU/memory. + - Only used if C(state) is present. + required: false + default: false + project: + description: + - Name of the project the template to be registered in. + required: false + default: null + zone: + description: + - Name of the zone you wish the template to be registered or deleted from. + - If not specified, first found zone will be used. + required: false + default: null + template_filter: + description: + - Name of the filter used to search for the template. + required: false + default: 'self' + choices: [ 'featured', 'self', 'selfexecutable', 'sharedexecutable', 'executable', 'community' ] + hypervisor: + description: + - Name the hypervisor to be used for creating the new template. + - Relevant when using C(state=present). + required: false + default: none + choices: [ 'KVM', 'VMware', 'BareMetal', 'XenServer', 'LXC', 'HyperV', 'UCS', 'OVM' ] + requires_hvm: + description: + - true if this template requires HVM. + required: false + default: false + password_enabled: + description: + - True if the template supports the password reset feature. + required: false + default: false + template_tag: + description: + - the tag for this template. + required: false + default: null + sshkey_enabled: + description: + - True if the template supports the sshkey upload feature. + required: false + default: false + is_routing: + description: + - True if the template type is routing i.e., if template is used to deploy router. + - Only considered if C(url) is used. + required: false + default: false + format: + description: + - The format for the template. + - Relevant when using C(state=present). + required: false + default: null + choices: [ 'QCOW2', 'RAW', 'VHD', 'OVA' ] + is_extractable: + description: + - True if the template or its derivatives are extractable. + required: false + default: false + details: + description: + - Template details in key/value pairs. + required: false + default: null + bits: + description: + - 32 or 64 bits support. + required: false + default: '64' + displaytext: + description: + - the display text of the template. + required: true + default: null + state: + description: + - State of the template. + required: false + default: 'present' + choices: [ 'present', 'absent' ] + poll_async: + description: + - Poll async jobs until job has finished. + required: false + default: true +extends_documentation_fragment: cloudstack +''' + +EXAMPLES = ''' +# Register a systemvm template +- local_action: + module: cs_template + name: systemvm-4.5 + url: "http://packages.shapeblue.com/systemvmtemplate/4.5/systemvm64template-4.5-vmware.ova" + hypervisor: VMware + format: OVA + zone: tokio-ix + os_type: Debian GNU/Linux 7(64-bit) + is_routing: yes + +# Create a template from a stopped virtual machine's volume +- local_action: + module: cs_template + name: debian-base-template + vm: debian-base-vm + os_type: Debian GNU/Linux 7(64-bit) + zone: tokio-ix + password_enabled: yes + is_public: yes + +# Create a template from a virtual machine's root volume snapshot +- local_action: + module: cs_template + name: debian-base-template + vm: debian-base-vm + snapshot: ROOT-233_2015061509114 + os_type: Debian GNU/Linux 7(64-bit) + zone: tokio-ix + password_enabled: yes + is_public: yes + +# Remove a template +- local_action: + module: cs_template + name: systemvm-4.2 + state: absent +''' + +RETURN = ''' +--- +name: + description: Name of the template. + returned: success + type: string + sample: Debian 7 64-bit +displaytext: + description: Displaytext of the template. + returned: success + type: string + sample: Debian 7.7 64-bit minimal 2015-03-19 +checksum: + description: MD5 checksum of the template. + returned: success + type: string + sample: 0b31bccccb048d20b551f70830bb7ad0 +status: + description: Status of the template. + returned: success + type: string + sample: Download Complete +is_ready: + description: True if the template is ready to be deployed from. + returned: success + type: boolean + sample: true +is_public: + description: True if the template is public. + returned: success + type: boolean + sample: true +is_featured: + description: True if the template is featured. + returned: success + type: boolean + sample: true +is_extractable: + description: True if the template is extractable. + returned: success + type: boolean + sample: true +format: + description: Format of the template. + returned: success + type: string + sample: OVA +os_type: + description: Typo of the OS. + returned: success + type: string + sample: CentOS 6.5 (64-bit) +password_enabled: + description: True if the reset password feature is enabled, false otherwise. + returned: success + type: boolean + sample: false +sshkey_enabled: + description: true if template is sshkey enabled, false otherwise. + returned: success + type: boolean + sample: false +cross_zones: + description: true if the template is managed across all zones, false otherwise. + returned: success + type: boolean + sample: false +template_type: + description: Type of the template. + returned: success + type: string + sample: USER +created: + description: Date of registering. + returned: success + type: string + sample: 2015-03-29T14:57:06+0200 +template_tag: + description: Template tag related to this template. + returned: success + type: string + sample: special +hypervisor: + description: Hypervisor related to this template. + returned: success + type: string + sample: VMware +tags: + description: List of resource tags associated with the template. + returned: success + type: dict + sample: '[ { "key": "foo", "value": "bar" } ]' +zone: + description: Name of zone the template is registered in. + returned: success + type: string + sample: zuerich +domain: + description: Domain the template is related to. + returned: success + type: string + sample: example domain +account: + description: Account the template is related to. + returned: success + type: string + sample: example account +project: + description: Name of project the template is related to. + returned: success + type: string + sample: Production +''' + +try: + from cs import CloudStack, CloudStackException, read_config + has_lib_cs = True +except ImportError: + has_lib_cs = False + +# import cloudstack common +from ansible.module_utils.cloudstack import * + + +class AnsibleCloudStackTemplate(AnsibleCloudStack): + + def __init__(self, module): + AnsibleCloudStack.__init__(self, module) + + + def _get_args(self): + args = {} + args['name'] = self.module.params.get('name') + args['displaytext'] = self.module.params.get('displaytext') + args['bits'] = self.module.params.get('bits') + args['isdynamicallyscalable'] = self.module.params.get('is_dynamically_scalable') + args['isextractable'] = self.module.params.get('is_extractable') + args['isfeatured'] = self.module.params.get('is_featured') + args['ispublic'] = self.module.params.get('is_public') + args['passwordenabled'] = self.module.params.get('password_enabled') + args['requireshvm'] = self.module.params.get('requires_hvm') + args['templatetag'] = self.module.params.get('template_tag') + args['ostypeid'] = self.get_os_type(key='id') + + if not args['ostypeid']: + self.module.fail_json(msg="Missing required arguments: os_type") + + if not args['displaytext']: + args['displaytext'] = self.module.params.get('name') + return args + + + def get_root_volume(self, key=None): + args = {} + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') + args['projectid'] = self.get_project(key='id') + args['virtualmachineid'] = self.get_vm(key='id') + args['type'] = "ROOT" + + volumes = self.cs.listVolumes(**args) + if volumes: + return self._get_by_key(key, volumes['volume'][0]) + self.module.fail_json(msg="Root volume for '%s' not found" % self.get_vm('name')) + + + def get_snapshot(self, key=None): + snapshot = self.module.params.get('snapshot') + if not snapshot: + return None + + args = {} + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') + args['projectid'] = self.get_project(key='id') + args['volumeid'] = self.get_root_volume('id') + snapshots = self.cs.listSnapshots(**args) + if snapshots: + for s in snapshots['snapshot']: + if snapshot in [ s['name'], s['id'] ]: + return self._get_by_key(key, s) + self.module.fail_json(msg="Snapshot '%s' not found" % snapshot) + + + def create_template(self): + template = self.get_template() + if not template: + self.result['changed'] = True + + args = self._get_args() + snapshot_id = self.get_snapshot(key='id') + if snapshot_id: + args['snapshotid'] = snapshot_id + else: + args['volumeid'] = self.get_root_volume('id') + + if not self.module.check_mode: + template = self.cs.createTemplate(**args) + + if 'errortext' in template: + self.module.fail_json(msg="Failed: '%s'" % template['errortext']) + + poll_async = self.module.params.get('poll_async') + if poll_async: + template = self._poll_job(template, 'template') + return template + + + def register_template(self): + template = self.get_template() + if not template: + self.result['changed'] = True + args = self._get_args() + args['url'] = self.module.params.get('url') + args['format'] = self.module.params.get('format') + args['checksum'] = self.module.params.get('checksum') + args['isextractable'] = self.module.params.get('is_extractable') + args['isrouting'] = self.module.params.get('is_routing') + args['sshkeyenabled'] = self.module.params.get('sshkey_enabled') + args['hypervisor'] = self.get_hypervisor() + args['zoneid'] = self.get_zone(key='id') + args['domainid'] = self.get_domain(key='id') + args['account'] = self.get_account(key='name') + args['projectid'] = self.get_project(key='id') + + if not self.module.check_mode: + res = self.cs.registerTemplate(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + template = res['template'] + return template + + + def get_template(self): + args = {} + args['isready'] = self.module.params.get('is_ready') + args['templatefilter'] = self.module.params.get('template_filter') + args['zoneid'] = self.get_zone(key='id') + args['domainid'] = self.get_domain(key='id') + args['account'] = self.get_account(key='name') + args['projectid'] = self.get_project(key='id') + + # if checksum is set, we only look on that. + checksum = self.module.params.get('checksum') + if not checksum: + args['name'] = self.module.params.get('name') + + templates = self.cs.listTemplates(**args) + if templates: + # if checksum is set, we only look on that. + if not checksum: + return templates['template'][0] + else: + for i in templates['template']: + if i['checksum'] == checksum: + return i + return None + + + def remove_template(self): + template = self.get_template() + if template: + self.result['changed'] = True + + args = {} + args['id'] = template['id'] + args['zoneid'] = self.get_zone(key='id') + + if not self.module.check_mode: + res = self.cs.deleteTemplate(**args) + + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + + poll_async = self.module.params.get('poll_async') + if poll_async: + res = self._poll_job(res, 'template') + return template + + + def get_result(self, template): + if template: + if 'displaytext' in template: + self.result['displaytext'] = template['displaytext'] + if 'name' in template: + self.result['name'] = template['name'] + if 'hypervisor' in template: + self.result['hypervisor'] = template['hypervisor'] + if 'zonename' in template: + self.result['zone'] = template['zonename'] + if 'checksum' in template: + self.result['checksum'] = template['checksum'] + if 'format' in template: + self.result['format'] = template['format'] + if 'isready' in template: + self.result['is_ready'] = template['isready'] + if 'ispublic' in template: + self.result['is_public'] = template['ispublic'] + if 'isfeatured' in template: + self.result['is_featured'] = template['isfeatured'] + if 'isextractable' in template: + self.result['is_extractable'] = template['isextractable'] + # and yes! it is really camelCase! + if 'crossZones' in template: + self.result['cross_zones'] = template['crossZones'] + if 'ostypename' in template: + self.result['os_type'] = template['ostypename'] + if 'templatetype' in template: + self.result['template_type'] = template['templatetype'] + if 'passwordenabled' in template: + self.result['password_enabled'] = template['passwordenabled'] + if 'sshkeyenabled' in template: + self.result['sshkey_enabled'] = template['sshkeyenabled'] + if 'status' in template: + self.result['status'] = template['status'] + if 'created' in template: + self.result['created'] = template['created'] + if 'templatetag' in template: + self.result['template_tag'] = template['templatetag'] + if 'tags' in template: + self.result['tags'] = [] + for tag in template['tags']: + result_tag = {} + result_tag['key'] = tag['key'] + result_tag['value'] = tag['value'] + self.result['tags'].append(result_tag) + if 'domain' in template: + self.result['domain'] = template['domain'] + if 'account' in template: + self.result['account'] = template['account'] + if 'project' in template: + self.result['project'] = template['project'] + return self.result + + +def main(): + module = AnsibleModule( + argument_spec = dict( + name = dict(required=True), + displaytext = dict(default=None), + url = dict(default=None), + vm = dict(default=None), + snapshot = dict(default=None), + os_type = dict(default=None), + is_ready = dict(type='bool', choices=BOOLEANS, default=False), + is_public = dict(type='bool', choices=BOOLEANS, default=True), + is_featured = dict(type='bool', choices=BOOLEANS, default=False), + is_dynamically_scalable = dict(type='bool', choices=BOOLEANS, default=False), + is_extractable = dict(type='bool', choices=BOOLEANS, default=False), + is_routing = dict(type='bool', choices=BOOLEANS, default=False), + checksum = dict(default=None), + template_filter = dict(default='self', choices=['featured', 'self', 'selfexecutable', 'sharedexecutable', 'executable', 'community']), + hypervisor = dict(choices=['KVM', 'VMware', 'BareMetal', 'XenServer', 'LXC', 'HyperV', 'UCS', 'OVM'], default=None), + requires_hvm = dict(type='bool', choices=BOOLEANS, default=False), + password_enabled = dict(type='bool', choices=BOOLEANS, default=False), + template_tag = dict(default=None), + sshkey_enabled = dict(type='bool', choices=BOOLEANS, default=False), + format = dict(choices=['QCOW2', 'RAW', 'VHD', 'OVA'], default=None), + details = dict(default=None), + bits = dict(type='int', choices=[ 32, 64 ], default=64), + state = dict(choices=['present', 'absent'], default='present'), + zone = dict(default=None), + domain = dict(default=None), + account = dict(default=None), + project = dict(default=None), + poll_async = dict(type='bool', choices=BOOLEANS, default=True), + api_key = dict(default=None), + api_secret = dict(default=None), + api_url = dict(default=None), + api_http_method = dict(choices=['get', 'post'], default='get'), + api_timeout = dict(type='int', default=10), + ), + mutually_exclusive = ( + ['url', 'vm'], + ), + required_together = ( + ['api_key', 'api_secret', 'api_url'], + ['format', 'url', 'hypervisor'], + ), + required_one_of = ( + ['url', 'vm'], + ), + supports_check_mode=True + ) + + if not has_lib_cs: + module.fail_json(msg="python library cs required: pip install cs") + + try: + acs_tpl = AnsibleCloudStackTemplate(module) + + state = module.params.get('state') + if state in ['absent']: + tpl = acs_tpl.remove_template() + else: + url = module.params.get('url') + if url: + tpl = acs_tpl.register_template() + else: + tpl = acs_tpl.create_template() + + result = acs_tpl.get_result(tpl) + + except CloudStackException, e: + module.fail_json(msg='CloudStackException: %s' % str(e)) + + except Exception, e: + module.fail_json(msg='Exception: %s' % str(e)) + + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * +main() From ad845a59b0df3de044810845d15133d0d1fe214f Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Mon, 15 Jun 2015 12:12:49 +0200 Subject: [PATCH 0244/2522] cloudstack: fix clean_up arg to be boolean in cs_network --- cloud/cloudstack/cs_network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/cloudstack/cs_network.py b/cloud/cloudstack/cs_network.py index c8b3b32539d..e22eaf0a5c3 100644 --- a/cloud/cloudstack/cs_network.py +++ b/cloud/cloudstack/cs_network.py @@ -116,7 +116,7 @@ - Cleanup old network elements. - Only considered on C(state=restarted). required: false - default: null + default: false acl_type: description: - Access control type. @@ -584,7 +584,7 @@ def main(): vlan = dict(default=None), vpc = dict(default=None), isolated_pvlan = dict(default=None), - clean_up = dict(default=None), + clean_up = dict(type='bool', choices=BOOLEANS, default=False), network_domain = dict(default=None), state = dict(choices=['present', 'absent', 'restarted' ], default='present'), acl_type = dict(choices=['account', 'domain'], default='account'), From 59c57ee798d4733186bd50c3179db9d0f744c918 Mon Sep 17 00:00:00 2001 From: Greg DeKoenigsberg Date: Tue, 16 Jun 2015 11:32:48 -0400 Subject: [PATCH 0245/2522] Changing maintainer for this module --- cloud/amazon/cloudtrail.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cloud/amazon/cloudtrail.py b/cloud/amazon/cloudtrail.py index 962473e6a9e..1c9313bbf7b 100644 --- a/cloud/amazon/cloudtrail.py +++ b/cloud/amazon/cloudtrail.py @@ -21,7 +21,9 @@ description: - Creates or deletes CloudTrail configuration. Ensures logging is also enabled. version_added: "2.0" -author: "Ted Timmons (@tedder)" +author: + - "Ansible Core Team" + - "Ted Timmons" requirements: - "boto >= 2.21" options: From d831c6a924dab2eb5eba965d489ea37fda217453 Mon Sep 17 00:00:00 2001 From: Greg DeKoenigsberg Date: Tue, 16 Jun 2015 11:41:31 -0400 Subject: [PATCH 0246/2522] Adding author info --- cloud/amazon/ec2_win_password.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_win_password.py b/cloud/amazon/ec2_win_password.py index 33a6ae7f947..b9cb029499a 100644 --- a/cloud/amazon/ec2_win_password.py +++ b/cloud/amazon/ec2_win_password.py @@ -7,7 +7,7 @@ description: - Gets the default administrator password from any EC2 Windows instance. The instance is referenced by its id (e.g. i-XXXXXXX). This module has a dependency on python-boto. version_added: "2.0" -author: Rick Mendes +author: "Rick Mendes (@rickmendes)" options: instance_id: description: From dc519fb848c13c3347d1c6c441ba843581eb1fdf Mon Sep 17 00:00:00 2001 From: Greg DeKoenigsberg Date: Tue, 16 Jun 2015 12:30:47 -0400 Subject: [PATCH 0247/2522] Add author data --- network/nmcli.py | 2 +- windows/win_chocolatey.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/network/nmcli.py b/network/nmcli.py index 45043fd2807..c674114a32e 100644 --- a/network/nmcli.py +++ b/network/nmcli.py @@ -22,7 +22,7 @@ DOCUMENTATION=''' --- module: nmcli -author: Chris Long +author: "Chris Long (@alcamie101)" short_description: Manage Networking requirements: [ nmcli, dbus ] version_added: "2.0" diff --git a/windows/win_chocolatey.py b/windows/win_chocolatey.py index fe00f2e0f6a..7f399dbd22f 100644 --- a/windows/win_chocolatey.py +++ b/windows/win_chocolatey.py @@ -75,7 +75,7 @@ require: false default: null aliases: [] -author: Trond Hindenes, Peter Mounce, Pepe Barbe, Adam Keech +author: "Trond Hindenes (@trondhindenes), Peter Mounce (@petemounce), Pepe Barbe (@elventear), Adam Keech (@smadam813)" ''' # TODO: From 728f2f1bb84ceb278f32f9131019626f361c4218 Mon Sep 17 00:00:00 2001 From: Greg DeKoenigsberg Date: Tue, 16 Jun 2015 12:55:51 -0400 Subject: [PATCH 0248/2522] Adding the list of valid module reviewers --- REVIEWERS.md | 160 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 REVIEWERS.md diff --git a/REVIEWERS.md b/REVIEWERS.md new file mode 100644 index 00000000000..985afce7bea --- /dev/null +++ b/REVIEWERS.md @@ -0,0 +1,160 @@ +New module reviewers +==================== +The following list represents all current Github module reviewers. It's currently comprised of all Ansible module authors, past and present. + +Two +1 votes by any of these module reviewers on a new module pull request will result in the inclusion of that module into Ansible Extras. + +Active +====== +"Adam Garside (@fabulops)" +"Adam Keech (@smadam813)" +"Adam Miller (@maxamillion)" +"Alex Coomans (@drcapulet)" +"Alexander Bulimov (@abulimov)" +"Alexander Saltanov (@sashka)" +"Alexander Winkler (@dermute)" +"Andrew de Quincey (@adq)" +"André Paramés (@andreparames)" +"Andy Hill (@andyhky)" +"Artūras `arturaz` Šlajus (@arturaz)" +"Augustus Kling (@AugustusKling)" +"BOURDEL Paul (@pb8226)" +"Balazs Pocze (@banyek)" +"Ben Whaley (@bwhaley)" +"Benno Joy (@bennojoy)" +"Bernhard Weitzhofer (@b6d)" +"Boyd Adamson (@brontitall)" +"Brad Olson (@bradobro)" +"Brian Coca (@bcoca)" +"Brice Burgess (@briceburg)" +"Bruce Pennypacker (@bpennypacker)" +"Carson Gee (@carsongee)" +"Chris Church (@cchurch)" +"Chris Hoffman (@chrishoffman)" +"Chris Long (@alcamie101)" +"Chris Schmidt (@chrisisbeef)" +"Christian Berendt (@berendt)" +"Christopher H. Laco (@claco)" +"Cristian van Ee (@DJMuggs)" +"Dag Wieers (@dagwieers)" +"Dane Summers (@dsummersl)" +"Daniel Jaouen (@danieljaouen)" +"Daniel Schep (@dschep)" +"Dariusz Owczarek (@dareko)" +"Darryl Stoflet (@dstoflet)" +"David CHANIAL (@davixx)" +"David Stygstra (@stygstra)" +"Derek Carter (@goozbach)" +"Dimitrios Tydeas Mengidis (@dmtrs)" +"Doug Luce (@dougluce)" +"Dylan Martin (@pileofrogs)" +"Elliott Foster (@elliotttf)" +"Eric Johnson (@erjohnso)" +"Evan Duffield (@scicoin-project)" +"Evan Kaufman (@EvanK)" +"Evgenii Terechkov (@evgkrsk)" +"Franck Cuny (@franckcuny)" +"Gareth Rushgrove (@garethr)" +"Hagai Kariti (@hkariti)" +"Hector Acosta (@hacosta)" +"Hiroaki Nakamura (@hnakamur)" +"Ivan Vanderbyl (@ivanvanderbyl)" +"Jakub Jirutka (@jirutka)" +"James Cammarata (@jimi-c)" +"James Laska (@jlaska)" +"James S. Martin (@jsmartin)" +"Jan-Piet Mens (@jpmens)" +"Jayson Vantuyl (@jvantuyl)" +"Jens Depuydt (@jensdepuydt)" +"Jeroen Hoekx (@jhoekx)" +"Jesse Keating (@j2sol)" +"Jim Dalton (@jsdalton)" +"Jim Richardson (@weaselkeeper)" +"Jimmy Tang (@jcftang)" +"Johan Wiren (@johanwiren)" +"John Dewey (@retr0h)" +"John Jarvis (@jarv)" +"John Whitbeck (@jwhitbeck)" +"Jon Hawkesworth (@jhawkesworth)" +"Jonas Pfenniger (@zimbatm)" +"Jonathan I. Davila (@defionscode)" +"Joseph Callen (@jcpowermac)" +"Kevin Carter (@cloudnull)" +"Lester Wade (@lwade)" +"Lorin Hochstein (@lorin)" +"Manuel Sousa (@manuel-sousa)" +"Mark Theunissen (@marktheunissen)" +"Matt Coddington (@mcodd)" +"Matt Hite (@mhite)" +"Matt Makai (@makaimc)" +"Matt Martz (@sivel)" +"Matt Wright (@mattupstate)" +"Matthew Vernon (@mcv21)" +"Matthew Williams (@mgwilliams)" +"Matthias Vogelgesang (@matze)" +"Max Riveiro (@kavu)" +"Michael Gregson (@mgregson)" +"Michael J. Schultz (@mjschultz)" +"Michael Warkentin (@mwarkentin)" +"Mischa Peters (@mischapeters)" +"Monty Taylor (@emonty)" +"Nandor Sivok (@dominis)" +"Nate Coraor (@natefoo)" +"Nate Kingsley (@nate-kingsley)" +"Nick Harring (@NickatEpic)" +"Patrick Callahan (@dirtyharrycallahan)" +"Patrick Ogenstad (@ogenstad)" +"Patrick Pelletier (@skinp)" +"Patrik Lundin (@eest)" +"Paul Durivage (@angstwad)" +"Pavel Antonov (@softzilla)" +"Pepe Barbe (@elventear)" +"Peter Mounce (@petemounce)" +"Peter Oliver (@mavit)" +"Peter Sprygada (@privateip)" +"Peter Tan (@tanpeter)" +"Philippe Makowski (@pmakowski)" +"Phillip Gentry, CX Inc (@pcgentry)" +"Quentin Stafford-Fraser (@quentinsf)" +"Ramon de la Fuente (@ramondelafuente)" +"Raul Melo (@melodous)" +"Ravi Bhure (@ravibhure)" +"René Moser (@resmo)" +"Richard Hoop (@rhoop) " +"Richard Isaacson (@risaacson)" +"Rick Mendes (@rickmendes)" +"Romeo Theriault (@romeotheriault)" +"Scott Anderson (@tastychutney)" +"Sebastian Kornehl (@skornehl)" +"Serge van Ginderachter (@srvg)" +"Sergei Antipov (@UnderGreen)" +"Seth Edwards (@sedward)" +"Silviu Dicu (@silviud) " +"Simon JAILLET (@jails)" +"Stephen Fromm (@sfromm)" +"Steve (@groks)" +"Steve Gargan (@sgargan)" +"Steve Smith (@tarka)" +"Takashi Someda (@tksmd)" +"Taneli Leppä (@rosmo)" +"Tim Bielawa (@tbielawa)" +"Tim Bielawa (@tbielawa)" +"Tim Mahoney (@timmahoney)" +"Timothy Appnel (@tima)" +"Tom Bamford (@tombamford)" +"Trond Hindenes (@trondhindenes)" +"Vincent Van der Kussen (@vincentvdk)" +"Vincent Viallet (@zbal)" +"WAKAYAMA Shirou (@shirou)" +"Will Thames (@willthames)" +"Willy Barro (@willybarro)" +"Xabier Larrakoetxea (@slok)" +"Yeukhon Wong (@yeukhon)" +"Zacharie Eakin (@zeekin)" +"berenddeboer (@berenddeboer)" +"bleader (@bleader)" +"curtis (@ccollicutt)" + +Retired +======= +None yet :) From 004dedba8a8c4ba3c118744c37e5d5c238315345 Mon Sep 17 00:00:00 2001 From: Greg DeKoenigsberg Date: Tue, 16 Jun 2015 14:32:39 -0400 Subject: [PATCH 0249/2522] Changes to author formatting, remove emails --- REVIEWERS.md | 4 ++-- cloud/amazon/ec2_eni_facts.py | 2 +- cloud/cloudstack/cs_account.py | 2 +- cloud/cloudstack/cs_affinitygroup.py | 2 +- cloud/cloudstack/cs_firewall.py | 2 +- cloud/cloudstack/cs_instance.py | 2 +- cloud/cloudstack/cs_instancegroup.py | 2 +- cloud/cloudstack/cs_iso.py | 2 +- cloud/cloudstack/cs_network.py | 2 +- cloud/cloudstack/cs_portforward.py | 2 +- cloud/cloudstack/cs_project.py | 2 +- cloud/cloudstack/cs_securitygroup.py | 2 +- cloud/cloudstack/cs_securitygroup_rule.py | 2 +- cloud/cloudstack/cs_sshkeypair.py | 2 +- cloud/cloudstack/cs_template.py | 2 +- cloud/cloudstack/cs_vmsnapshot.py | 2 +- cloud/google/gce_img.py | 2 +- cloud/lxc/lxc_container.py | 2 +- cloud/misc/ovirt.py | 2 +- cloud/misc/virt.py | 4 ++-- cloud/vmware/vmware_datacenter.py | 2 +- clustering/consul.py | 2 +- clustering/consul_acl.py | 2 +- clustering/consul_kv.py | 2 +- clustering/consul_session.py | 2 +- commands/expect.py | 2 +- database/misc/mongodb_user.py | 2 +- database/misc/riak.py | 4 ++-- database/mysql/mysql_replication.py | 2 +- files/patch.py | 4 ++-- messaging/rabbitmq_binding.py | 2 +- messaging/rabbitmq_exchange.py | 2 +- messaging/rabbitmq_policy.py | 2 +- messaging/rabbitmq_queue.py | 2 +- monitoring/airbrake_deployment.py | 2 +- monitoring/boundary_meter.py | 2 +- monitoring/datadog_event.py | 2 +- monitoring/datadog_monitor.py | 2 +- monitoring/logentries.py | 2 +- monitoring/monit.py | 2 +- monitoring/nagios.py | 2 +- monitoring/newrelic_deployment.py | 2 +- monitoring/rollbar_deployment.py | 2 +- monitoring/zabbix_maintenance.py | 2 +- network/a10/a10_server.py | 2 +- network/a10/a10_service_group.py | 2 +- network/a10/a10_virtual_server.py | 2 +- network/citrix/netscaler.py | 2 +- network/f5/bigip_facts.py | 2 +- network/f5/bigip_monitor_http.py | 2 +- network/f5/bigip_monitor_tcp.py | 2 +- network/f5/bigip_node.py | 2 +- network/f5/bigip_pool.py | 2 +- network/f5/bigip_pool_member.py | 2 +- network/haproxy.py | 2 +- network/openvswitch_bridge.py | 2 +- network/openvswitch_port.py | 2 +- notification/campfire.py | 2 +- notification/flowdock.py | 2 +- notification/grove.py | 2 +- notification/mail.py | 2 +- notification/nexmo.py | 2 +- notification/pushover.py | 2 +- notification/sendgrid.py | 2 +- notification/slack.py | 2 +- notification/sns.py | 2 +- notification/twilio.py | 2 +- notification/typetalk.py | 2 +- packaging/language/bower.py | 2 +- packaging/language/composer.py | 2 +- packaging/language/cpanm.py | 2 +- packaging/language/maven_artifact.py | 2 +- packaging/language/npm.py | 2 +- packaging/os/dnf.py | 2 +- packaging/os/homebrew.py | 4 ++-- packaging/os/homebrew_cask.py | 2 +- packaging/os/homebrew_tap.py | 2 +- packaging/os/layman.py | 2 +- packaging/os/openbsd_pkg.py | 2 +- packaging/os/opkg.py | 2 +- packaging/os/pkg5.py | 2 +- packaging/os/pkg5_publisher.py | 2 +- packaging/os/pkgin.py | 4 ++-- packaging/os/pkgng.py | 2 +- packaging/os/pkgutil.py | 2 +- packaging/os/portinstall.py | 2 +- packaging/os/swdepot.py | 2 +- packaging/os/urpmi.py | 2 +- packaging/os/zypper.py | 2 +- packaging/os/zypper_repository.py | 2 +- source_control/bzr.py | 2 +- source_control/github_hooks.py | 2 +- system/alternatives.py | 4 ++-- system/at.py | 2 +- system/capabilities.py | 2 +- system/crypttab.py | 2 +- system/filesystem.py | 2 +- system/firewalld.py | 2 +- system/gluster_volume.py | 2 +- system/kernel_blacklist.py | 2 +- system/known_hosts.py | 2 +- system/lvg.py | 2 +- system/lvol.py | 4 ++-- system/modprobe.py | 6 +++--- system/open_iscsi.py | 2 +- system/ufw.py | 6 +++--- system/zfs.py | 2 +- web_infrastructure/ejabberd_user.py | 2 +- web_infrastructure/jboss.py | 2 +- web_infrastructure/jira.py | 2 +- windows/win_updates.py | 2 +- 111 files changed, 123 insertions(+), 123 deletions(-) diff --git a/REVIEWERS.md b/REVIEWERS.md index 985afce7bea..5ae08b59b02 100644 --- a/REVIEWERS.md +++ b/REVIEWERS.md @@ -120,7 +120,7 @@ Active "Raul Melo (@melodous)" "Ravi Bhure (@ravibhure)" "René Moser (@resmo)" -"Richard Hoop (@rhoop) " +"Richard Hoop (@rhoop)" "Richard Isaacson (@risaacson)" "Rick Mendes (@rickmendes)" "Romeo Theriault (@romeotheriault)" @@ -129,7 +129,7 @@ Active "Serge van Ginderachter (@srvg)" "Sergei Antipov (@UnderGreen)" "Seth Edwards (@sedward)" -"Silviu Dicu (@silviud) " +"Silviu Dicu (@silviud)" "Simon JAILLET (@jails)" "Stephen Fromm (@sfromm)" "Steve (@groks)" diff --git a/cloud/amazon/ec2_eni_facts.py b/cloud/amazon/ec2_eni_facts.py index 94b586fb639..76347c84261 100644 --- a/cloud/amazon/ec2_eni_facts.py +++ b/cloud/amazon/ec2_eni_facts.py @@ -20,7 +20,7 @@ description: - Gather facts about ec2 ENI interfaces in AWS version_added: "2.0" -author: Rob White, wimnat [at] gmail.com, @wimnat +author: "Rob White (@wimnat)" options: eni_id: description: diff --git a/cloud/cloudstack/cs_account.py b/cloud/cloudstack/cs_account.py index 597e4c7394e..cc487af5e51 100644 --- a/cloud/cloudstack/cs_account.py +++ b/cloud/cloudstack/cs_account.py @@ -25,7 +25,7 @@ description: - Create, disable, lock, enable and remove accounts. version_added: '2.0' -author: '"René Moser (@resmo)" ' +author: "René Moser (@resmo)" options: name: description: diff --git a/cloud/cloudstack/cs_affinitygroup.py b/cloud/cloudstack/cs_affinitygroup.py index 40896942cb1..580cc5d7e8d 100644 --- a/cloud/cloudstack/cs_affinitygroup.py +++ b/cloud/cloudstack/cs_affinitygroup.py @@ -25,7 +25,7 @@ description: - Create and remove affinity groups. version_added: '2.0' -author: '"René Moser (@resmo)" ' +author: "René Moser (@resmo)" options: name: description: diff --git a/cloud/cloudstack/cs_firewall.py b/cloud/cloudstack/cs_firewall.py index 828aa1faf98..96b3f20f7cf 100644 --- a/cloud/cloudstack/cs_firewall.py +++ b/cloud/cloudstack/cs_firewall.py @@ -25,7 +25,7 @@ description: - Creates and removes firewall rules. version_added: '2.0' -author: '"René Moser (@resmo)" ' +author: "René Moser (@resmo)" options: ip_address: description: diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 46fd66f510d..a93a524383a 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -25,7 +25,7 @@ description: - Deploy, start, restart, stop and destroy instances. version_added: '2.0' -author: '"René Moser (@resmo)" ' +author: "René Moser (@resmo)" options: name: description: diff --git a/cloud/cloudstack/cs_instancegroup.py b/cloud/cloudstack/cs_instancegroup.py index 396cafa388d..478748aeec3 100644 --- a/cloud/cloudstack/cs_instancegroup.py +++ b/cloud/cloudstack/cs_instancegroup.py @@ -25,7 +25,7 @@ description: - Create and remove instance groups. version_added: '2.0' -author: '"René Moser (@resmo)" ' +author: "René Moser (@resmo)" options: name: description: diff --git a/cloud/cloudstack/cs_iso.py b/cloud/cloudstack/cs_iso.py index d9ec6880627..e3ba322f6ba 100644 --- a/cloud/cloudstack/cs_iso.py +++ b/cloud/cloudstack/cs_iso.py @@ -25,7 +25,7 @@ description: - Register and remove ISO images. version_added: '2.0' -author: '"René Moser (@resmo)" ' +author: "René Moser (@resmo)" options: name: description: diff --git a/cloud/cloudstack/cs_network.py b/cloud/cloudstack/cs_network.py index e22eaf0a5c3..b602b345677 100644 --- a/cloud/cloudstack/cs_network.py +++ b/cloud/cloudstack/cs_network.py @@ -25,7 +25,7 @@ description: - Create, update, restart and delete networks. version_added: '2.0' -author: '"René Moser (@resmo)" ' +author: "René Moser (@resmo)" options: name: description: diff --git a/cloud/cloudstack/cs_portforward.py b/cloud/cloudstack/cs_portforward.py index 00b084d9195..3b88ca85723 100644 --- a/cloud/cloudstack/cs_portforward.py +++ b/cloud/cloudstack/cs_portforward.py @@ -25,7 +25,7 @@ description: - Create, update and remove port forwarding rules. version_added: '2.0' -author: '"René Moser (@resmo)" ' +author: "René Moser (@resmo)" options: ip_address: description: diff --git a/cloud/cloudstack/cs_project.py b/cloud/cloudstack/cs_project.py index b505433892e..0f391bc5005 100644 --- a/cloud/cloudstack/cs_project.py +++ b/cloud/cloudstack/cs_project.py @@ -25,7 +25,7 @@ description: - Create, update, suspend, activate and remove projects. version_added: '2.0' -author: '"René Moser (@resmo)" ' +author: "René Moser (@resmo)" options: name: description: diff --git a/cloud/cloudstack/cs_securitygroup.py b/cloud/cloudstack/cs_securitygroup.py index 08fb72c821d..54a71686a6e 100644 --- a/cloud/cloudstack/cs_securitygroup.py +++ b/cloud/cloudstack/cs_securitygroup.py @@ -25,7 +25,7 @@ description: - Create and remove security groups. version_added: '2.0' -author: '"René Moser (@resmo)" ' +author: "René Moser (@resmo)" options: name: description: diff --git a/cloud/cloudstack/cs_securitygroup_rule.py b/cloud/cloudstack/cs_securitygroup_rule.py index 9252e06ce62..e943e7d11c2 100644 --- a/cloud/cloudstack/cs_securitygroup_rule.py +++ b/cloud/cloudstack/cs_securitygroup_rule.py @@ -25,7 +25,7 @@ description: - Add and remove security group rules. version_added: '2.0' -author: '"René Moser (@resmo)" ' +author: "René Moser (@resmo)" options: security_group: description: diff --git a/cloud/cloudstack/cs_sshkeypair.py b/cloud/cloudstack/cs_sshkeypair.py index 0a54a1971bc..180e96ca6ae 100644 --- a/cloud/cloudstack/cs_sshkeypair.py +++ b/cloud/cloudstack/cs_sshkeypair.py @@ -27,7 +27,7 @@ - If no key was found and no public key was provided and a new SSH private/public key pair will be created and the private key will be returned. version_added: '2.0' -author: '"René Moser (@resmo)" ' +author: "René Moser (@resmo)" options: name: description: diff --git a/cloud/cloudstack/cs_template.py b/cloud/cloudstack/cs_template.py index 48f00fad553..1cd245d2b5c 100644 --- a/cloud/cloudstack/cs_template.py +++ b/cloud/cloudstack/cs_template.py @@ -25,7 +25,7 @@ description: - Register a template from URL, create a template from a ROOT volume of a stopped VM or its snapshot and delete templates. version_added: '2.0' -author: '"René Moser (@resmo)" ' +author: "René Moser (@resmo)" options: name: description: diff --git a/cloud/cloudstack/cs_vmsnapshot.py b/cloud/cloudstack/cs_vmsnapshot.py index fb7668640dc..24e8a46fa37 100644 --- a/cloud/cloudstack/cs_vmsnapshot.py +++ b/cloud/cloudstack/cs_vmsnapshot.py @@ -25,7 +25,7 @@ description: - Create, remove and revert VM from snapshots. version_added: '2.0' -author: '"René Moser (@resmo)" ' +author: "René Moser (@resmo)" options: name: description: diff --git a/cloud/google/gce_img.py b/cloud/google/gce_img.py index 9cc37f8eb33..5775a94794d 100644 --- a/cloud/google/gce_img.py +++ b/cloud/google/gce_img.py @@ -81,7 +81,7 @@ requirements: - "python >= 2.6" - "apache-libcloud" -author: '"Peter Tan (@tanpeter)" ' +author: "Peter Tan (@tanpeter)" ''' EXAMPLES = ''' diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index 18555e2e351..711c70bca98 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -26,7 +26,7 @@ version_added: 1.8.0 description: - Management of LXC containers -author: '"Kevin Carter (@cloudnull)" ' +author: "Kevin Carter (@cloudnull)" options: name: description: diff --git a/cloud/misc/ovirt.py b/cloud/misc/ovirt.py index 718f25fec2c..6e8f3281dc5 100644 --- a/cloud/misc/ovirt.py +++ b/cloud/misc/ovirt.py @@ -20,7 +20,7 @@ DOCUMENTATION = ''' --- module: ovirt -author: '"Vincent Van der Kussen (@vincentvdk)" ' +author: "Vincent Van der Kussen (@vincentvdk)" short_description: oVirt/RHEV platform management description: - allows you to create new instances, either from scratch or an image, in addition to deleting or stopping instances on the oVirt/RHEV platform diff --git a/cloud/misc/virt.py b/cloud/misc/virt.py index 343a3eedcf7..80b8e2558eb 100644 --- a/cloud/misc/virt.py +++ b/cloud/misc/virt.py @@ -60,8 +60,8 @@ - "libvirt-python" author: - "Ansible Core Team" - - '"Michael DeHaan (@mpdehaan)" ' - - '"Seth Vidal (@skvidal)" ' + - "Michael DeHaan" + - "Seth Vidal" ''' EXAMPLES = ''' diff --git a/cloud/vmware/vmware_datacenter.py b/cloud/vmware/vmware_datacenter.py index b1e995b965b..b2083222ed5 100644 --- a/cloud/vmware/vmware_datacenter.py +++ b/cloud/vmware/vmware_datacenter.py @@ -25,7 +25,7 @@ description: - Manage VMware vSphere Datacenters version_added: 2.0 -author: '"Joseph Callen (@jcpowermac)" ' +author: "Joseph Callen (@jcpowermac)" notes: - Tested on vSphere 5.5 requirements: diff --git a/clustering/consul.py b/clustering/consul.py index 8423ffe418f..083173230f7 100644 --- a/clustering/consul.py +++ b/clustering/consul.py @@ -42,7 +42,7 @@ - python-consul - requests version_added: "2.0" -author: '"Steve Gargan (@sgargan)" ' +author: "Steve Gargan (@sgargan)" options: state: description: diff --git a/clustering/consul_acl.py b/clustering/consul_acl.py index b832281bb80..250de24e2a3 100644 --- a/clustering/consul_acl.py +++ b/clustering/consul_acl.py @@ -30,7 +30,7 @@ - pyhcl - requests version_added: "2.0" -author: '"Steve Gargan (@sgargan)" ' +author: "Steve Gargan (@sgargan)" options: mgmt_token: description: diff --git a/clustering/consul_kv.py b/clustering/consul_kv.py index 69a66c746ab..2ba3a0315a3 100644 --- a/clustering/consul_kv.py +++ b/clustering/consul_kv.py @@ -32,7 +32,7 @@ - python-consul - requests version_added: "2.0" -author: '"Steve Gargan (@sgargan)" ' +author: "Steve Gargan (@sgargan)" options: state: description: diff --git a/clustering/consul_session.py b/clustering/consul_session.py index d57c2b69db8..ef4646c35e4 100644 --- a/clustering/consul_session.py +++ b/clustering/consul_session.py @@ -30,7 +30,7 @@ - python-consul - requests version_added: "2.0" -author: '"Steve Gargan (@sgargan)" ' +author: "Steve Gargan (@sgargan)" options: state: description: diff --git a/commands/expect.py b/commands/expect.py index 124c718b73b..e8f7a049836 100644 --- a/commands/expect.py +++ b/commands/expect.py @@ -73,7 +73,7 @@ - If you want to run a command through the shell (say you are using C(<), C(>), C(|), etc), you must specify a shell in the command such as C(/bin/bash -c "/path/to/something | grep else") -author: '"Matt Martz (@sivel)" ' +author: "Matt Martz (@sivel)" ''' EXAMPLES = ''' diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index 9802f890a35..ede8004945b 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -99,7 +99,7 @@ - Requires the pymongo Python package on the remote host, version 2.4.2+. This can be installed using pip or the OS package manager. @see http://api.mongodb.org/python/current/installation.html requirements: [ "pymongo" ] -author: '"Elliott Foster (@elliotttf)" ' +author: "Elliott Foster (@elliotttf)" ''' EXAMPLES = ''' diff --git a/database/misc/riak.py b/database/misc/riak.py index 4f10775a5ad..12586651887 100644 --- a/database/misc/riak.py +++ b/database/misc/riak.py @@ -27,8 +27,8 @@ the status of the cluster. version_added: "1.2" author: - - '"James Martin (@jsmartin)" ' - - '"Drew Kerrigan (@drewkerrigan)" ' + - "James Martin (@jsmartin)" + - "Drew Kerrigan (@drewkerrigan)" options: command: description: diff --git a/database/mysql/mysql_replication.py b/database/mysql/mysql_replication.py index 898b1510c1d..f5d2d5cf630 100644 --- a/database/mysql/mysql_replication.py +++ b/database/mysql/mysql_replication.py @@ -30,7 +30,7 @@ description: - Manages MySQL server replication, slave, master status get and change master host. version_added: "1.3" -author: '"Balazs Pocze (@banyek)" ' +author: "Balazs Pocze (@banyek)" options: mode: description: diff --git a/files/patch.py b/files/patch.py index c1a61ce733f..60629c922e9 100644 --- a/files/patch.py +++ b/files/patch.py @@ -23,8 +23,8 @@ --- module: patch author: - - '"Jakub Jirutka (@jirutka)" ' - - '"Luis Alberto Perez Lazaro (@luisperlaz)" ' + - "Jakub Jirutka (@jirutka)" + - "Luis Alberto Perez Lazaro (@luisperlaz)" version_added: 1.9 description: - Apply patch files using the GNU patch tool. diff --git a/messaging/rabbitmq_binding.py b/messaging/rabbitmq_binding.py index b0ae3a38bf7..fc69f490fad 100644 --- a/messaging/rabbitmq_binding.py +++ b/messaging/rabbitmq_binding.py @@ -22,7 +22,7 @@ DOCUMENTATION = ''' --- module: rabbitmq_binding -author: '"Manuel Sousa (@manuel-sousa)" ' +author: "Manuel Sousa (@manuel-sousa)" version_added: "2.0" short_description: This module manages rabbitMQ bindings diff --git a/messaging/rabbitmq_exchange.py b/messaging/rabbitmq_exchange.py index 6f3ce143c4a..fb74298879b 100644 --- a/messaging/rabbitmq_exchange.py +++ b/messaging/rabbitmq_exchange.py @@ -22,7 +22,7 @@ DOCUMENTATION = ''' --- module: rabbitmq_exchange -author: '"Manuel Sousa (@manuel-sousa)" ' +author: "Manuel Sousa (@manuel-sousa)" version_added: "2.0" short_description: This module manages rabbitMQ exchanges diff --git a/messaging/rabbitmq_policy.py b/messaging/rabbitmq_policy.py index a4d94decbd1..81d7068ec46 100644 --- a/messaging/rabbitmq_policy.py +++ b/messaging/rabbitmq_policy.py @@ -26,7 +26,7 @@ description: - Manage the state of a virtual host in RabbitMQ. version_added: "1.5" -author: '"John Dewey (@retr0h)" ' +author: "John Dewey (@retr0h)" options: name: description: diff --git a/messaging/rabbitmq_queue.py b/messaging/rabbitmq_queue.py index 105104b3d77..5a403a6b602 100644 --- a/messaging/rabbitmq_queue.py +++ b/messaging/rabbitmq_queue.py @@ -22,7 +22,7 @@ DOCUMENTATION = ''' --- module: rabbitmq_queue -author: '"Manuel Sousa (@manuel-sousa)" ' +author: "Manuel Sousa (@manuel-sousa)" version_added: "2.0" short_description: This module manages rabbitMQ queues diff --git a/monitoring/airbrake_deployment.py b/monitoring/airbrake_deployment.py index 0036bde7daa..3b54e55e751 100644 --- a/monitoring/airbrake_deployment.py +++ b/monitoring/airbrake_deployment.py @@ -22,7 +22,7 @@ --- module: airbrake_deployment version_added: "1.2" -author: '"Bruce Pennypacker (@bpennypacker)" ' +author: "Bruce Pennypacker (@bpennypacker)" short_description: Notify airbrake about app deployments description: - Notify airbrake about app deployments (see http://help.airbrake.io/kb/api-2/deploy-tracking) diff --git a/monitoring/boundary_meter.py b/monitoring/boundary_meter.py index adc2b2433e1..431a6ace1b9 100644 --- a/monitoring/boundary_meter.py +++ b/monitoring/boundary_meter.py @@ -34,7 +34,7 @@ description: - This module manages boundary meters version_added: "1.3" -author: '"curtis (@ccollicutt)" ' +author: "curtis (@ccollicutt)" requirements: - Boundary API access - bprobe is required to send data, but not to register a meter diff --git a/monitoring/datadog_event.py b/monitoring/datadog_event.py index d363f8b17dc..ebbad039dec 100644 --- a/monitoring/datadog_event.py +++ b/monitoring/datadog_event.py @@ -14,7 +14,7 @@ - "Allows to post events to DataDog (www.datadoghq.com) service." - "Uses http://docs.datadoghq.com/api/#events API." version_added: "1.3" -author: '"Artūras `arturaz` Šlajus (@arturaz)" ' +author: "Artūras `arturaz` Šlajus (@arturaz)" notes: [] requirements: [urllib2] options: diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py index f1acb169ce0..9853d748c2c 100644 --- a/monitoring/datadog_monitor.py +++ b/monitoring/datadog_monitor.py @@ -34,7 +34,7 @@ - "Manages monitors within Datadog" - "Options like described on http://docs.datadoghq.com/api/" version_added: "2.0" -author: '"Sebastian Kornehl (@skornehl)" ' +author: "Sebastian Kornehl (@skornehl)" notes: [] requirements: [datadog] options: diff --git a/monitoring/logentries.py b/monitoring/logentries.py index 75ed2e0e6dd..a347afd84c2 100644 --- a/monitoring/logentries.py +++ b/monitoring/logentries.py @@ -19,7 +19,7 @@ DOCUMENTATION = ''' --- module: logentries -author: '"Ivan Vanderbyl (@ivanvanderbyl)" ' +author: "Ivan Vanderbyl (@ivanvanderbyl)" short_description: Module for tracking logs via logentries.com description: - Sends logs to LogEntries in realtime diff --git a/monitoring/monit.py b/monitoring/monit.py index 6410ce815e8..3d3c7c8c3ca 100644 --- a/monitoring/monit.py +++ b/monitoring/monit.py @@ -39,7 +39,7 @@ default: null choices: [ "present", "started", "stopped", "restarted", "monitored", "unmonitored", "reloaded" ] requirements: [ ] -author: '"Darryl Stoflet (@dstoflet)" ' +author: "Darryl Stoflet (@dstoflet)" ''' EXAMPLES = ''' diff --git a/monitoring/nagios.py b/monitoring/nagios.py index 0026751ea58..16edca2aa6a 100644 --- a/monitoring/nagios.py +++ b/monitoring/nagios.py @@ -86,7 +86,7 @@ required: true default: null -author: '"Tim Bielawa (@tbielawa)" ' +author: "Tim Bielawa (@tbielawa)" requirements: [ "Nagios" ] ''' diff --git a/monitoring/newrelic_deployment.py b/monitoring/newrelic_deployment.py index 166cdcda0be..832e467dea0 100644 --- a/monitoring/newrelic_deployment.py +++ b/monitoring/newrelic_deployment.py @@ -22,7 +22,7 @@ --- module: newrelic_deployment version_added: "1.2" -author: '"Matt Coddington (@mcodd)" ' +author: "Matt Coddington (@mcodd)" short_description: Notify newrelic about app deployments description: - Notify newrelic about app deployments (see https://docs.newrelic.com/docs/apm/new-relic-apm/maintenance/deployment-notifications#api) diff --git a/monitoring/rollbar_deployment.py b/monitoring/rollbar_deployment.py index dc064d6194d..43e2aa00722 100644 --- a/monitoring/rollbar_deployment.py +++ b/monitoring/rollbar_deployment.py @@ -22,7 +22,7 @@ --- module: rollbar_deployment version_added: 1.6 -author: '"Max Riveiro (@kavu)" ' +author: "Max Riveiro (@kavu)" short_description: Notify Rollbar about app deployments description: - Notify Rollbar about app deployments diff --git a/monitoring/zabbix_maintenance.py b/monitoring/zabbix_maintenance.py index 25d7c8df95e..2d611382919 100644 --- a/monitoring/zabbix_maintenance.py +++ b/monitoring/zabbix_maintenance.py @@ -26,7 +26,7 @@ description: - This module will let you create Zabbix maintenance windows. version_added: "1.8" -author: '"Alexander Bulimov (@abulimov)" ' +author: "Alexander Bulimov (@abulimov)" requirements: - "python >= 2.6" - zabbix-api diff --git a/network/a10/a10_server.py b/network/a10/a10_server.py index 72ed0f648e6..2ad66c23588 100644 --- a/network/a10/a10_server.py +++ b/network/a10/a10_server.py @@ -28,7 +28,7 @@ short_description: Manage A10 Networks AX/SoftAX/Thunder/vThunder devices description: - Manage slb server objects on A10 Networks devices via aXAPI -author: '"Mischa Peters (@mischapeters)" ' +author: "Mischa Peters (@mischapeters)" notes: - Requires A10 Networks aXAPI 2.1 options: diff --git a/network/a10/a10_service_group.py b/network/a10/a10_service_group.py index 8e84bf9a07d..db1c21bc78e 100644 --- a/network/a10/a10_service_group.py +++ b/network/a10/a10_service_group.py @@ -28,7 +28,7 @@ short_description: Manage A10 Networks AX/SoftAX/Thunder/vThunder devices description: - Manage slb service-group objects on A10 Networks devices via aXAPI -author: '"Mischa Peters (@mischapeters)" ' +author: "Mischa Peters (@mischapeters)" notes: - Requires A10 Networks aXAPI 2.1 - When a server doesn't exist and is added to the service-group the server will be created diff --git a/network/a10/a10_virtual_server.py b/network/a10/a10_virtual_server.py index 3df93f67dbe..eb308a3032a 100644 --- a/network/a10/a10_virtual_server.py +++ b/network/a10/a10_virtual_server.py @@ -28,7 +28,7 @@ short_description: Manage A10 Networks AX/SoftAX/Thunder/vThunder devices description: - Manage slb virtual server objects on A10 Networks devices via aXAPI -author: '"Mischa Peters (@mischapeters)" ' +author: "Mischa Peters (@mischapeters)" notes: - Requires A10 Networks aXAPI 2.1 requirements: diff --git a/network/citrix/netscaler.py b/network/citrix/netscaler.py index 8f78e23caac..61bc35356e5 100644 --- a/network/citrix/netscaler.py +++ b/network/citrix/netscaler.py @@ -82,7 +82,7 @@ choices: ['yes', 'no'] requirements: [ "urllib", "urllib2" ] -author: '"Nandor Sivok (@dominis)" ' +author: "Nandor Sivok (@dominis)" ''' EXAMPLES = ''' diff --git a/network/f5/bigip_facts.py b/network/f5/bigip_facts.py index 7b78c6d97f7..1b106ba0a3e 100644 --- a/network/f5/bigip_facts.py +++ b/network/f5/bigip_facts.py @@ -25,7 +25,7 @@ description: - "Collect facts from F5 BIG-IP devices via iControl SOAP API" version_added: "1.6" -author: '"Matt Hite (@mhite)" ' +author: "Matt Hite (@mhite)" notes: - "Requires BIG-IP software version >= 11.4" - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" diff --git a/network/f5/bigip_monitor_http.py b/network/f5/bigip_monitor_http.py index 5299bdb0f44..ea24e995e27 100644 --- a/network/f5/bigip_monitor_http.py +++ b/network/f5/bigip_monitor_http.py @@ -27,7 +27,7 @@ description: - "Manages F5 BIG-IP LTM monitors via iControl SOAP API" version_added: "1.4" -author: '"Serge van Ginderachter (@srvg)" ' +author: "Serge van Ginderachter (@srvg)" notes: - "Requires BIG-IP software version >= 11" - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" diff --git a/network/f5/bigip_monitor_tcp.py b/network/f5/bigip_monitor_tcp.py index b5f58da8397..0900e95fd20 100644 --- a/network/f5/bigip_monitor_tcp.py +++ b/network/f5/bigip_monitor_tcp.py @@ -25,7 +25,7 @@ description: - "Manages F5 BIG-IP LTM tcp monitors via iControl SOAP API" version_added: "1.4" -author: '"Serge van Ginderachter (@srvg)" ' +author: "Serge van Ginderachter (@srvg)" notes: - "Requires BIG-IP software version >= 11" - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" diff --git a/network/f5/bigip_node.py b/network/f5/bigip_node.py index 49f721aa8c5..28eacc0d6f5 100644 --- a/network/f5/bigip_node.py +++ b/network/f5/bigip_node.py @@ -25,7 +25,7 @@ description: - "Manages F5 BIG-IP LTM nodes via iControl SOAP API" version_added: "1.4" -author: '"Matt Hite (@mhite)" ' +author: "Matt Hite (@mhite)" notes: - "Requires BIG-IP software version >= 11" - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" diff --git a/network/f5/bigip_pool.py b/network/f5/bigip_pool.py index 4d8d599134e..1628f6c68c9 100644 --- a/network/f5/bigip_pool.py +++ b/network/f5/bigip_pool.py @@ -25,7 +25,7 @@ description: - "Manages F5 BIG-IP LTM pools via iControl SOAP API" version_added: "1.2" -author: '"Matt Hite (@mhite)" ' +author: "Matt Hite (@mhite)" notes: - "Requires BIG-IP software version >= 11" - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" diff --git a/network/f5/bigip_pool_member.py b/network/f5/bigip_pool_member.py index 1d59462023f..ec2b7135372 100644 --- a/network/f5/bigip_pool_member.py +++ b/network/f5/bigip_pool_member.py @@ -25,7 +25,7 @@ description: - "Manages F5 BIG-IP LTM pool members via iControl SOAP API" version_added: "1.4" -author: '"Matt Hite (@mhite)" ' +author: "Matt Hite (@mhite)" notes: - "Requires BIG-IP software version >= 11" - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" diff --git a/network/haproxy.py b/network/haproxy.py index c897349019e..00fc4ff63a1 100644 --- a/network/haproxy.py +++ b/network/haproxy.py @@ -91,7 +91,7 @@ # enable server in 'www' backend pool with change server(s) weight - haproxy: state=enabled host={{ inventory_hostname }} socket=/var/run/haproxy.sock weight=10 backend=www -author: "Ravi Bhure (@ravibhure)" +author: "Ravi Bhure (@ravibhure)" ''' import socket diff --git a/network/openvswitch_bridge.py b/network/openvswitch_bridge.py index 28df3e84426..b9ddff562c6 100644 --- a/network/openvswitch_bridge.py +++ b/network/openvswitch_bridge.py @@ -22,7 +22,7 @@ --- module: openvswitch_bridge version_added: 1.4 -author: '"David Stygstra (@stygstra)" ' +author: "David Stygstra (@stygstra)" short_description: Manage Open vSwitch bridges requirements: [ ovs-vsctl ] description: diff --git a/network/openvswitch_port.py b/network/openvswitch_port.py index ab87ea42b4a..6f59f4b134b 100644 --- a/network/openvswitch_port.py +++ b/network/openvswitch_port.py @@ -22,7 +22,7 @@ --- module: openvswitch_port version_added: 1.4 -author: '"David Stygstra (@stygstra)" ' +author: "David Stygstra (@stygstra)" short_description: Manage Open vSwitch ports requirements: [ ovs-vsctl ] description: diff --git a/notification/campfire.py b/notification/campfire.py index 9218826a7b4..2400ad3ba40 100644 --- a/notification/campfire.py +++ b/notification/campfire.py @@ -43,7 +43,7 @@ # informational: requirements for nodes requirements: [ urllib2, cgi ] -author: '"Adam Garside (@fabulops)" ' +author: "Adam Garside (@fabulops)" ''' EXAMPLES = ''' diff --git a/notification/flowdock.py b/notification/flowdock.py index aea107457fb..7c42e58644d 100644 --- a/notification/flowdock.py +++ b/notification/flowdock.py @@ -22,7 +22,7 @@ --- module: flowdock version_added: "1.2" -author: '"Matt Coddington (@mcodd)" ' +author: "Matt Coddington (@mcodd)" short_description: Send a message to a flowdock description: - Send a message to a flowdock team inbox or chat using the push API (see https://www.flowdock.com/api/team-inbox and https://www.flowdock.com/api/chat) diff --git a/notification/grove.py b/notification/grove.py index 5c27b18c30f..85601d1cc78 100644 --- a/notification/grove.py +++ b/notification/grove.py @@ -39,7 +39,7 @@ default: 'yes' choices: ['yes', 'no'] version_added: 1.5.1 -author: '"Jonas Pfenniger (@zimbatm)" ' +author: "Jonas Pfenniger (@zimbatm)" ''' EXAMPLES = ''' diff --git a/notification/mail.py b/notification/mail.py index 4feaebf5d36..c42e80fdabf 100644 --- a/notification/mail.py +++ b/notification/mail.py @@ -20,7 +20,7 @@ DOCUMENTATION = """ --- -author: '"Dag Wieers (@dagwieers)" ' +author: "Dag Wieers (@dagwieers)" module: mail short_description: Send an email description: diff --git a/notification/nexmo.py b/notification/nexmo.py index a1dd9c2b64d..d0c3d05e65c 100644 --- a/notification/nexmo.py +++ b/notification/nexmo.py @@ -24,7 +24,7 @@ description: - Send a SMS message via nexmo version_added: 1.6 -author: '"Matt Martz (@sivel)" ' +author: "Matt Martz (@sivel)" options: api_key: description: diff --git a/notification/pushover.py b/notification/pushover.py index 951c65f43fe..505917189e4 100644 --- a/notification/pushover.py +++ b/notification/pushover.py @@ -48,7 +48,7 @@ description: Message priority (see u(https://pushover.net) for details.) required: false -author: '"Jim Richardson (@weaselkeeper)" ' +author: "Jim Richardson (@weaselkeeper)" ''' EXAMPLES = ''' diff --git a/notification/sendgrid.py b/notification/sendgrid.py index 6278f613ee4..78806687e0b 100644 --- a/notification/sendgrid.py +++ b/notification/sendgrid.py @@ -53,7 +53,7 @@ the desired subject for the email required: true -author: '"Matt Makai (@makaimc)" ' +author: "Matt Makai (@makaimc)" ''' EXAMPLES = ''' diff --git a/notification/slack.py b/notification/slack.py index 7e5215479ab..baabe4f58d2 100644 --- a/notification/slack.py +++ b/notification/slack.py @@ -24,7 +24,7 @@ description: - The M(slack) module sends notifications to U(http://slack.com) via the Incoming WebHook integration version_added: 1.6 -author: '"Ramon de la Fuente (@ramondelafuente)" ' +author: "Ramon de la Fuente (@ramondelafuente)" options: domain: description: diff --git a/notification/sns.py b/notification/sns.py index 910105f0ebb..70030d66196 100644 --- a/notification/sns.py +++ b/notification/sns.py @@ -24,7 +24,7 @@ description: - The M(sns) module sends notifications to a topic on your Amazon SNS account version_added: 1.6 -author: '"Michael J. Schultz (@mjschultz)" ' +author: "Michael J. Schultz (@mjschultz)" options: msg: description: diff --git a/notification/twilio.py b/notification/twilio.py index 568d0c60a58..e9ec5bcf51e 100644 --- a/notification/twilio.py +++ b/notification/twilio.py @@ -58,7 +58,7 @@ (multimedia message) instead of a plain SMS required: false -author: '"Matt Makai (@makaimc)" ' +author: "Matt Makai (@makaimc)" ''' EXAMPLES = ''' diff --git a/notification/typetalk.py b/notification/typetalk.py index 8e79a7617ed..638f97ae530 100644 --- a/notification/typetalk.py +++ b/notification/typetalk.py @@ -26,7 +26,7 @@ - message body required: true requirements: [ urllib, urllib2, json ] -author: '"Takashi Someda (@tksmd)" ' +author: "Takashi Someda (@tksmd)" ''' EXAMPLES = ''' diff --git a/packaging/language/bower.py b/packaging/language/bower.py index 8fbe20f7e0c..7af8136a445 100644 --- a/packaging/language/bower.py +++ b/packaging/language/bower.py @@ -25,7 +25,7 @@ description: - Manage bower packages with bower version_added: 1.9 -author: '"Michael Warkentin (@mwarkentin)" ' +author: "Michael Warkentin (@mwarkentin)" options: name: description: diff --git a/packaging/language/composer.py b/packaging/language/composer.py index cfe3f99b9e7..8e11d25216b 100644 --- a/packaging/language/composer.py +++ b/packaging/language/composer.py @@ -22,7 +22,7 @@ DOCUMENTATION = ''' --- module: composer -author: '"Dimitrios Tydeas Mengidis (@dmtrs)" ' +author: "Dimitrios Tydeas Mengidis (@dmtrs)" short_description: Dependency Manager for PHP version_added: "1.6" description: diff --git a/packaging/language/cpanm.py b/packaging/language/cpanm.py index 5549dab8895..02b306b669c 100644 --- a/packaging/language/cpanm.py +++ b/packaging/language/cpanm.py @@ -73,7 +73,7 @@ description: Install I(Dancer) perl package from a specific mirror notes: - Please note that U(http://search.cpan.org/dist/App-cpanminus/bin/cpanm, cpanm) must be installed on the remote host. -author: '"Franck Cuny (@franckcuny)" ' +author: "Franck Cuny (@franckcuny)" ''' def _is_package_installed(module, name, locallib, cpanm): diff --git a/packaging/language/maven_artifact.py b/packaging/language/maven_artifact.py index 057cb0a3814..3e196dd93a5 100644 --- a/packaging/language/maven_artifact.py +++ b/packaging/language/maven_artifact.py @@ -37,7 +37,7 @@ - Downloads an artifact from a maven repository given the maven coordinates provided to the module. Can retrieve - snapshots or release versions of the artifact and will resolve the latest available version if one is not - available. -author: '"Chris Schmidt (@chrisisbeef)" ' +author: "Chris Schmidt (@chrisisbeef)" requirements: - "python >= 2.6" - lxml diff --git a/packaging/language/npm.py b/packaging/language/npm.py index 3eafcd6c2a7..d804efff331 100644 --- a/packaging/language/npm.py +++ b/packaging/language/npm.py @@ -25,7 +25,7 @@ description: - Manage node.js packages with Node Package Manager (npm) version_added: 1.2 -author: '"Chris Hoffman (@chrishoffman)" ' +author: "Chris Hoffman (@chrishoffman)" options: name: description: diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index e40c268f742..7afbee44c54 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -95,7 +95,7 @@ requirements: - dnf - yum-utils (for repoquery) -author: '"Cristian van Ee (@DJMuggs)" ' +author: "Cristian van Ee (@DJMuggs)" ''' EXAMPLES = ''' diff --git a/packaging/os/homebrew.py b/packaging/os/homebrew.py index 0b37521820d..91888ba6bca 100644 --- a/packaging/os/homebrew.py +++ b/packaging/os/homebrew.py @@ -23,8 +23,8 @@ --- module: homebrew author: - - '"Daniel Jaouen (@danieljaouen)" ' - - '"Andrew Dunham (@andrew-d)" ' + - "Daniel Jaouen (@danieljaouen)" + - "Andrew Dunham (@andrew-d)" short_description: Package manager for Homebrew description: - Manages Homebrew packages diff --git a/packaging/os/homebrew_cask.py b/packaging/os/homebrew_cask.py index bb5cacabbc7..e1b721a97b4 100644 --- a/packaging/os/homebrew_cask.py +++ b/packaging/os/homebrew_cask.py @@ -19,7 +19,7 @@ DOCUMENTATION = ''' --- module: homebrew_cask -author: '"Daniel Jaouen (@danieljaouen)" ' +author: "Daniel Jaouen (@danieljaouen)" short_description: Install/uninstall homebrew casks. description: - Manages Homebrew casks. diff --git a/packaging/os/homebrew_tap.py b/packaging/os/homebrew_tap.py index 504e77eb062..c6511f0c7b2 100644 --- a/packaging/os/homebrew_tap.py +++ b/packaging/os/homebrew_tap.py @@ -24,7 +24,7 @@ DOCUMENTATION = ''' --- module: homebrew_tap -author: '"Daniel Jaouen (@danieljaouen)" ' +author: "Daniel Jaouen (@danieljaouen)" short_description: Tap a Homebrew repository. description: - Tap external Homebrew repositories. diff --git a/packaging/os/layman.py b/packaging/os/layman.py index 3cad5e35642..c9d6b8ed333 100644 --- a/packaging/os/layman.py +++ b/packaging/os/layman.py @@ -25,7 +25,7 @@ DOCUMENTATION = ''' --- module: layman -author: '"Jakub Jirutka (@jirutka)" ' +author: "Jakub Jirutka (@jirutka)" version_added: "1.6" short_description: Manage Gentoo overlays description: diff --git a/packaging/os/openbsd_pkg.py b/packaging/os/openbsd_pkg.py index 2f81753fb64..1b5d0bb06b2 100644 --- a/packaging/os/openbsd_pkg.py +++ b/packaging/os/openbsd_pkg.py @@ -25,7 +25,7 @@ DOCUMENTATION = ''' --- module: openbsd_pkg -author: '"Patrik Lundin (@eest)" ' +author: "Patrik Lundin (@eest)" version_added: "1.1" short_description: Manage packages on OpenBSD. description: diff --git a/packaging/os/opkg.py b/packaging/os/opkg.py index 8f06a03a1b2..5b75ad1a260 100644 --- a/packaging/os/opkg.py +++ b/packaging/os/opkg.py @@ -20,7 +20,7 @@ DOCUMENTATION = ''' --- module: opkg -author: '"Patrick Pelletier (@skinp)" ' +author: "Patrick Pelletier (@skinp)" short_description: Package manager for OpenWrt description: - Manages OpenWrt packages diff --git a/packaging/os/pkg5.py b/packaging/os/pkg5.py index 632a36796dc..837eefd243e 100644 --- a/packaging/os/pkg5.py +++ b/packaging/os/pkg5.py @@ -19,7 +19,7 @@ DOCUMENTATION = ''' --- module: pkg5 -author: '"Peter Oliver (@mavit)" ' +author: "Peter Oliver (@mavit)" short_description: Manages packages with the Solaris 11 Image Packaging System version_added: 1.9 description: diff --git a/packaging/os/pkg5_publisher.py b/packaging/os/pkg5_publisher.py index 1db07d512b7..3881f5dd0b8 100644 --- a/packaging/os/pkg5_publisher.py +++ b/packaging/os/pkg5_publisher.py @@ -19,7 +19,7 @@ DOCUMENTATION = ''' --- module: pkg5_publisher -author: '"Peter Oliver (@mavit)" ' +author: "Peter Oliver (@mavit)" short_description: Manages Solaris 11 Image Packaging System publishers version_added: 1.9 description: diff --git a/packaging/os/pkgin.py b/packaging/os/pkgin.py index 33bcb5482f0..e600026409b 100644 --- a/packaging/os/pkgin.py +++ b/packaging/os/pkgin.py @@ -31,8 +31,8 @@ or any OS that uses C(pkgsrc). (Home: U(http://pkgin.net/))" version_added: "1.0" author: - - '"Larry Gilbert (L2G)" ' - - '"Shaun Zinck (@szinck)" ' + - "Larry Gilbert (L2G)" + - "Shaun Zinck (@szinck)" notes: - "Known bug with pkgin < 0.8.0: if a package is removed and another package depends on it, the other package will be silently removed as diff --git a/packaging/os/pkgng.py b/packaging/os/pkgng.py index 132cff637e6..c0819dbe5b8 100644 --- a/packaging/os/pkgng.py +++ b/packaging/os/pkgng.py @@ -63,7 +63,7 @@ for newer pkgng versions, specify a the name of a repository configured in /usr/local/etc/pkg/repos required: false -author: '"bleader (@bleader)" ' +author: "bleader (@bleader)" notes: - When using pkgsite, be careful that already in cache packages won't be downloaded again. ''' diff --git a/packaging/os/pkgutil.py b/packaging/os/pkgutil.py index 62107aa0475..3a4720630cf 100644 --- a/packaging/os/pkgutil.py +++ b/packaging/os/pkgutil.py @@ -32,7 +32,7 @@ - Pkgutil is an advanced packaging system, which resolves dependency on installation. It is designed for CSW packages. version_added: "1.3" -author: '"Alexander Winkler (@dermute)" ' +author: "Alexander Winkler (@dermute)" options: name: description: diff --git a/packaging/os/portinstall.py b/packaging/os/portinstall.py index 1673c4dde37..b4e3044167e 100644 --- a/packaging/os/portinstall.py +++ b/packaging/os/portinstall.py @@ -43,7 +43,7 @@ choices: [ 'yes', 'no' ] required: false default: yes -author: '"berenddeboer (@berenddeboer)" ' +author: "berenddeboer (@berenddeboer)" ''' EXAMPLES = ''' diff --git a/packaging/os/swdepot.py b/packaging/os/swdepot.py index 56b33d401bf..157fa212c17 100644 --- a/packaging/os/swdepot.py +++ b/packaging/os/swdepot.py @@ -29,7 +29,7 @@ - Will install, upgrade and remove packages with swdepot package manager (HP-UX) version_added: "1.4" notes: [] -author: '"Raul Melo (@melodous)" ' +author: "Raul Melo (@melodous)" options: name: description: diff --git a/packaging/os/urpmi.py b/packaging/os/urpmi.py index c202ee27ace..7b7aaefbd1d 100644 --- a/packaging/os/urpmi.py +++ b/packaging/os/urpmi.py @@ -57,7 +57,7 @@ required: false default: yes choices: [ "yes", "no" ] -author: '"Philippe Makowski (@pmakowski)" ' +author: "Philippe Makowski (@pmakowski)" notes: [] ''' diff --git a/packaging/os/zypper.py b/packaging/os/zypper.py index c175c152050..f3205051fdf 100644 --- a/packaging/os/zypper.py +++ b/packaging/os/zypper.py @@ -31,7 +31,7 @@ DOCUMENTATION = ''' --- module: zypper -author: '"Patrick Callahan (@dirtyharrycallahan)" ' +author: "Patrick Callahan (@dirtyharrycallahan)" version_added: "1.2" short_description: Manage packages on SUSE and openSUSE description: diff --git a/packaging/os/zypper_repository.py b/packaging/os/zypper_repository.py index 3210e93d391..54e20429638 100644 --- a/packaging/os/zypper_repository.py +++ b/packaging/os/zypper_repository.py @@ -23,7 +23,7 @@ DOCUMENTATION = ''' --- module: zypper_repository -author: '"Matthias Vogelgesang (@matze)" ' +author: "Matthias Vogelgesang (@matze)" version_added: "1.4" short_description: Add and remove Zypper repositories description: diff --git a/source_control/bzr.py b/source_control/bzr.py index 5519a8af123..0fc6ac28584 100644 --- a/source_control/bzr.py +++ b/source_control/bzr.py @@ -22,7 +22,7 @@ DOCUMENTATION = u''' --- module: bzr -author: '"André Paramés (@andreparames)" ' +author: "André Paramés (@andreparames)" version_added: "1.1" short_description: Deploy software (or files) from bzr branches description: diff --git a/source_control/github_hooks.py b/source_control/github_hooks.py index bb60b634cb3..d75fcb1573d 100644 --- a/source_control/github_hooks.py +++ b/source_control/github_hooks.py @@ -64,7 +64,7 @@ default: 'json' choices: ['json', 'form'] -author: '"Phillip Gentry, CX Inc (@pcgentry)" ' +author: "Phillip Gentry, CX Inc (@pcgentry)" ''' EXAMPLES = ''' diff --git a/system/alternatives.py b/system/alternatives.py index 06d9bea25f0..90e2237f86c 100644 --- a/system/alternatives.py +++ b/system/alternatives.py @@ -31,8 +31,8 @@ - Useful when multiple programs are installed but provide similar functionality (e.g. different editors). version_added: "1.6" author: - - '"David Wittman (@DavidWittman)" ' - - '"Gabe Mulley (@mulby)" ' + - "David Wittman (@DavidWittman)" + - "Gabe Mulley (@mulby)" options: name: description: diff --git a/system/at.py b/system/at.py index 03ac14a44aa..0ce9ff2c7d4 100644 --- a/system/at.py +++ b/system/at.py @@ -59,7 +59,7 @@ default: false requirements: - at -author: '"Richard Isaacson (@risaacson)" ' +author: "Richard Isaacson (@risaacson)" ''' EXAMPLES = ''' diff --git a/system/capabilities.py b/system/capabilities.py index 0c7f2e22d0b..ce8ffcfa632 100644 --- a/system/capabilities.py +++ b/system/capabilities.py @@ -50,7 +50,7 @@ and flags to compare, so you will want to ensure that your capabilities argument matches the final capabilities. requirements: [] -author: '"Nate Coraor (@natefoo)" ' +author: "Nate Coraor (@natefoo)" ''' EXAMPLES = ''' diff --git a/system/crypttab.py b/system/crypttab.py index 5b0edc62363..44d9f859791 100644 --- a/system/crypttab.py +++ b/system/crypttab.py @@ -69,7 +69,7 @@ notes: [] requirements: [] -author: '"Steve (@groks)" ' +author: "Steve (@groks)" ''' EXAMPLES = ''' diff --git a/system/filesystem.py b/system/filesystem.py index a2f979ecd0b..1e867f30270 100644 --- a/system/filesystem.py +++ b/system/filesystem.py @@ -20,7 +20,7 @@ DOCUMENTATION = ''' --- -author: '"Alexander Bulimov (@abulimov)" ' +author: "Alexander Bulimov (@abulimov)" module: filesystem short_description: Makes file system on block device description: diff --git a/system/firewalld.py b/system/firewalld.py index e16e4e4a9dd..37ed1801f68 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -69,7 +69,7 @@ notes: - Not tested on any Debian based system. requirements: [ 'firewalld >= 0.2.11' ] -author: '"Adam Miller (@maxamillion)" ' +author: "Adam Miller (@maxamillion)" ''' EXAMPLES = ''' diff --git a/system/gluster_volume.py b/system/gluster_volume.py index 32359cd2a82..7719006502d 100644 --- a/system/gluster_volume.py +++ b/system/gluster_volume.py @@ -103,7 +103,7 @@ notes: - "Requires cli tools for GlusterFS on servers" - "Will add new bricks, but not remove them" -author: '"Taneli Leppä (@rosmo)" ' +author: "Taneli Leppä (@rosmo)" """ EXAMPLES = """ diff --git a/system/kernel_blacklist.py b/system/kernel_blacklist.py index b0901473867..296a082a2ea 100644 --- a/system/kernel_blacklist.py +++ b/system/kernel_blacklist.py @@ -25,7 +25,7 @@ DOCUMENTATION = ''' --- module: kernel_blacklist -author: '"Matthias Vogelgesang (@matze)" ' +author: "Matthias Vogelgesang (@matze)" version_added: 1.4 short_description: Blacklist kernel modules description: diff --git a/system/known_hosts.py b/system/known_hosts.py index 74c6b0e90c7..303d9410d1e 100644 --- a/system/known_hosts.py +++ b/system/known_hosts.py @@ -51,7 +51,7 @@ required: no default: present requirements: [ ] -author: '"Matthew Vernon (@mcv21)" ' +author: "Matthew Vernon (@mcv21)" ''' EXAMPLES = ''' diff --git a/system/lvg.py b/system/lvg.py index 3c6c5ef2930..9e3ba2d2931 100644 --- a/system/lvg.py +++ b/system/lvg.py @@ -21,7 +21,7 @@ DOCUMENTATION = ''' --- -author: '"Alexander Bulimov (@abulimov)" ' +author: "Alexander Bulimov (@abulimov)" module: lvg short_description: Configure LVM volume groups description: diff --git a/system/lvol.py b/system/lvol.py index 3225408d162..7a01d83829c 100644 --- a/system/lvol.py +++ b/system/lvol.py @@ -21,8 +21,8 @@ DOCUMENTATION = ''' --- author: - - '"Jeroen Hoekx (@jhoekx)" ' - - '"Alexander Bulimov (@abulimov)" ' + - "Jeroen Hoekx (@jhoekx)" + - "Alexander Bulimov (@abulimov)" module: lvol short_description: Configure LVM logical volumes description: diff --git a/system/modprobe.py b/system/modprobe.py index bf58e435552..64e36c784a7 100644 --- a/system/modprobe.py +++ b/system/modprobe.py @@ -26,9 +26,9 @@ requirements: [] version_added: 1.4 author: - - '"David Stygstra (@stygstra)" ' - - Julien Dauphant - - Matt Jeffery + - "David Stygstra (@stygstra)" + - "Julien Dauphant" + - "Matt Jeffery" description: - Add or remove kernel modules. options: diff --git a/system/open_iscsi.py b/system/open_iscsi.py index 97652311f8d..e2477538888 100644 --- a/system/open_iscsi.py +++ b/system/open_iscsi.py @@ -21,7 +21,7 @@ DOCUMENTATION = ''' --- module: open_iscsi -author: '"Serge van Ginderachter (@srvg)" ' +author: "Serge van Ginderachter (@srvg)" version_added: "1.4" short_description: Manage iscsi targets with open-iscsi description: diff --git a/system/ufw.py b/system/ufw.py index 91d574f945d..cd148edf2ef 100644 --- a/system/ufw.py +++ b/system/ufw.py @@ -29,9 +29,9 @@ - Manage firewall with UFW. version_added: 1.6 author: - - '"Aleksey Ovcharenko (@ovcharenko)" ' - - '"Jarno Keskikangas (@pyykkis)" ' - - '"Ahti Kitsik (@ahtik)" ' + - "Aleksey Ovcharenko (@ovcharenko)" + - "Jarno Keskikangas (@pyykkis)" + - "Ahti Kitsik (@ahtik)" notes: - See C(man ufw) for more examples. requirements: diff --git a/system/zfs.py b/system/zfs.py index 97a0d6f3dba..c3c87634377 100644 --- a/system/zfs.py +++ b/system/zfs.py @@ -206,7 +206,7 @@ - The zoned property. required: False choices: ['on','off'] -author: '"Johan Wiren (@johanwiren)" ' +author: "Johan Wiren (@johanwiren)" ''' EXAMPLES = ''' diff --git a/web_infrastructure/ejabberd_user.py b/web_infrastructure/ejabberd_user.py index 79fe94fcddc..bf86806ad52 100644 --- a/web_infrastructure/ejabberd_user.py +++ b/web_infrastructure/ejabberd_user.py @@ -20,7 +20,7 @@ --- module: ejabberd_user version_added: "1.5" -author: '"Peter Sprygada (@privateip)" ' +author: "Peter Sprygada (@privateip)" short_description: Manages users for ejabberd servers requirements: - ejabberd with mod_admin_extra diff --git a/web_infrastructure/jboss.py b/web_infrastructure/jboss.py index a0949c47531..9ec67b7c7b1 100644 --- a/web_infrastructure/jboss.py +++ b/web_infrastructure/jboss.py @@ -47,7 +47,7 @@ notes: - "The JBoss standalone deployment-scanner has to be enabled in standalone.xml" - "Ensure no identically named application is deployed through the JBoss CLI" -author: '"Jeroen Hoekx (@jhoekx)" ' +author: "Jeroen Hoekx (@jhoekx)" """ EXAMPLES = """ diff --git a/web_infrastructure/jira.py b/web_infrastructure/jira.py index 3dc963cb6bd..79cfb72d4a7 100644 --- a/web_infrastructure/jira.py +++ b/web_infrastructure/jira.py @@ -99,7 +99,7 @@ notes: - "Currently this only works with basic-auth." -author: '"Steve Smith (@tarka)" ' +author: "Steve Smith (@tarka)" """ EXAMPLES = """ diff --git a/windows/win_updates.py b/windows/win_updates.py index 7c93109efb9..4a9f055d8dc 100644 --- a/windows/win_updates.py +++ b/windows/win_updates.py @@ -41,7 +41,7 @@ - (anything that is a valid update category) default: critical aliases: [] -author: '"Peter Mounce (@petemounce)" ' +author: "Peter Mounce (@petemounce)" ''' EXAMPLES = ''' From caed7573d50fd51a658f23f54c61e42868c9bca2 Mon Sep 17 00:00:00 2001 From: Brian Brazil Date: Tue, 30 Sep 2014 10:59:01 +0100 Subject: [PATCH 0250/2522] Add dpkg_selections module, that works with dpkg --get-selections and --set-selections. This is mainly useful for setting packages to 'hold' to prevent them from being automatically upgraded. --- packaging/dpkg_selections | 60 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 packaging/dpkg_selections diff --git a/packaging/dpkg_selections b/packaging/dpkg_selections new file mode 100644 index 00000000000..f09ff9a9f00 --- /dev/null +++ b/packaging/dpkg_selections @@ -0,0 +1,60 @@ +#!/usr/bin/python + +DOCUMENTATION = ''' +--- +module: dpkg_selections +short_description: Dpkg package selection selections +description: + - Change dpkg package selection state via --get-selections and --set-selections. +version_added: "2.0" +author: Brian Brazil +options: + name: + description: + - Name of the package + required: true + selection: + description: + - The selection state to set the package to. + choices: [ 'install', 'hold', 'deinstall', 'purge' ] + required: true +notes: + - This module won't cause any packages to be installed/removed/purged, use the C(apt) module for that. +''' +EXAMPLES = ''' +# Prevent python from being upgraded. +- dpkg_selections: name=python selection=hold +''' + +def main(): + module = AnsibleModule( + argument_spec = dict( + name = dict(required=True), + selection = dict(choices=['install', 'hold', 'deinstall', 'purge']) + ), + supports_check_mode=True, + ) + + dpkg = module.get_bin_path('dpkg', True) + + name = module.params['name'] + selection = module.params['selection'] + + # Get current settings. + rc, out, err = module.run_command([dpkg, '--get-selections', name], check_rc=True) + if not out: + current = 'not present' + else: + current = out.split()[1] + + changed = current != selection + + if module.check_mode or not changed: + module.exit_json(changed=changed, before=current, after=selection) + + module.run_command([dpkg, '--set-selections'], data="%s %s" % (name, selection), check_rc=True) + module.exit_json(changed=changed, before=current, after=selection) + + +from ansible.module_utils.basic import * +main() From 330e66327ae91c378857d992d6edafc2fc883b8b Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Wed, 17 Jun 2015 14:53:17 +0200 Subject: [PATCH 0251/2522] New module to copy (push) files to a vCenter datastore --- cloud/vmware/vsphere_copy | 152 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 cloud/vmware/vsphere_copy diff --git a/cloud/vmware/vsphere_copy b/cloud/vmware/vsphere_copy new file mode 100644 index 00000000000..f5f12f83555 --- /dev/null +++ b/cloud/vmware/vsphere_copy @@ -0,0 +1,152 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2015 Dag Wieers +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: vsphere_copy +short_description: Copy a file to a vCenter datastore +description: Upload files to a vCenter datastore +version_added: 2.0 +author: Dag Wieers +options: + host: + description: + - The vCenter server on which the datastore is available. + required: true + login: + description: + - The login name to authenticate on the vCenter server. + required: true + password: + description: + - The password to authenticate on the vCenter server. + required: true + src: + description: + - The file to push to vCenter + required: true + datacenter: + description: + - The datacenter on the vCenter server that holds the datastore. + required: true + datastore: + description: + - The datastore on the vCenter server to push files to. + required: true + path: + description: + - The file to push to the datastore on the vCenter server. + required: true +notes: + - This module ought to be run from a system that can access vCenter directly. + Either by using C(transport: local), or using C(delegate_to). + - Tested on vSphere 5.5 +''' + +EXAMPLES = ''' +- vsphere_copy: host=vhost login=vuser password=vpass src=/some/local/file datacenter='DC1 Someplace' datastore=datastore1 path=some/remote/file + transport: local +- vsphere_copy: host=vhost login=vuser password=vpass src=/other/local/file datacenter='DC2 Someplace' datastore=datastore2 path=other/remote/file + delegate_to: other_system +''' + +import atexit +import base64 +import httplib +import urllib +import mmap +import errno +import socket + +def vmware_path(datastore, datacenter, path): + ''' Constructs a URL path that VSphere accepts reliably ''' + path = "/folder/%s" % path.lstrip("/") + if not path.startswith("/"): + path = "/" + path + params = dict( dsName = datastore ) + if datacenter: + params["dcPath"] = datacenter + params = urllib.urlencode(params) + return "%s?%s" % (path, params) + +def main(): + + module = AnsibleModule( + argument_spec = dict( + host = dict(required=True, aliases=[ 'hostname' ]), + login = dict(required=True, aliases=[ 'username' ]), + password = dict(required=True), + src = dict(required=True, aliases=[ 'name' ]), + datacenter = dict(required=True), + datastore = dict(required=True), + dest = dict(required=True, aliases=[ 'path' ]), + ), + # Implementing check-mode using HEAD is impossible, since size/date is not 100% reliable + supports_check_mode = False, + ) + + host = module.params.get('host') + login = module.params.get('login') + password = module.params.get('password') + src = module.params.get('src') + datacenter = module.params.get('datacenter') + datastore = module.params.get('datastore') + dest = module.params.get('dest') + + fd = open(src, "rb") + atexit.register(fd.close) + + data = mmap.mmap(fd.fileno(), 0, access=mmap.ACCESS_READ) + atexit.register(data.close) + + conn = httplib.HTTPSConnection(host) + atexit.register(conn.close) + + remote_path = vmware_path(datastore, datacenter, dest) + auth = base64.encodestring('%s:%s' % (login, password)) + headers = { + "Content-Type": "application/octet-stream", + "Content-Length": str(len(data)), + "Accept": "text/plain", + "Authorization": "Basic %s" % auth, + } + + # URL is only used in JSON output (helps troubleshooting) + url = 'https://%s%s' % (host, remote_path) + + try: + conn.request("PUT", remote_path, body=data, headers=headers) + except socket.error, e: + if isinstance(e.args, tuple) and e[0] == errno.ECONNRESET: + # VSphere resets connection if the file is in use and cannot be replaced + module.fail_json(msg='Failed to upload, image probably in use', status=e[0], reason=str(e), url=url) + else: + module.fail_json(msg=str(e), status=e[0], reason=str(e), url=url) + + resp = conn.getresponse() + + if resp.status in range(200, 300): + module.exit_json(changed=True, status=resp.status, reason=resp.reason, url=url) + else: + module.fail_json(msg='Failed to upload', status=resp.status, reason=resp.reason, length=resp.length, version=resp.version, headers=resp.getheaders(), chunked=resp.chunked, url=url) + +# this is magic, see lib/ansible/module_common.py +#<> +main() From f967aa376d583e745a371987414585a359abe25d Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Wed, 17 Jun 2015 15:07:18 +0200 Subject: [PATCH 0252/2522] Fix TravisCI failure on python 2.4 --- system/osx_defaults.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/system/osx_defaults.py b/system/osx_defaults.py index 0dd7ca8ff6b..7e2fe38ad77 100644 --- a/system/osx_defaults.py +++ b/system/osx_defaults.py @@ -209,7 +209,10 @@ def write(self): # We need to convert some values so the defaults commandline understands it if type(self.value) is bool: - value = "TRUE" if self.value else "FALSE" + if self.value: + value = "TRUE" + else: + value = "FALSE" elif type(self.value) is int or type(self.value) is float: value = str(self.value) elif self.array_add and self.current_value is not None: From 685653b23b9d455507e6fa472de7cd3b8c03ee6c Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Wed, 17 Jun 2015 15:22:10 +0200 Subject: [PATCH 0253/2522] Another incompatibility with python 2.4 --- system/osx_defaults.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/osx_defaults.py b/system/osx_defaults.py index 7e2fe38ad77..e5d2bc51731 100644 --- a/system/osx_defaults.py +++ b/system/osx_defaults.py @@ -343,7 +343,7 @@ def main(): array_add=array_add, value=value, state=state, path=path) changed = defaults.run() module.exit_json(changed=changed) - except OSXDefaultsException as e: + except OSXDefaultsException, e: module.fail_json(msg=e.message) # /main ------------------------------------------------------------------- }}} From 8753b2cd208c94d7e4a003462e6720ac3b0965cd Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 16 Jun 2015 19:19:16 -0400 Subject: [PATCH 0254/2522] minor docfixes --- system/osx_defaults.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/osx_defaults.py b/system/osx_defaults.py index e5d2bc51731..e4dc5f8c750 100644 --- a/system/osx_defaults.py +++ b/system/osx_defaults.py @@ -19,14 +19,14 @@ DOCUMENTATION = ''' --- module: osx_defaults -author: Franck Nijhof +author: Franck Nijhof (@frenck) short_description: osx_defaults allows users to read, write, and delete Mac OS X user defaults from Ansible description: - osx_defaults allows users to read, write, and delete Mac OS X user defaults from Ansible scripts. Mac OS X applications and other programs use the defaults system to record user preferences and other information that must be maintained when the applications aren't running (such as default font for new documents, or the position of an Info panel). -version_added: 1.8 +version_added: "2.0" options: domain: description: @@ -47,7 +47,7 @@ description: - Add new elements to the array for a key which has an array as its value. required: false - default: string + default: false choices: [ "true", "false" ] value: description: From 9db032aa118425cde4db904b0e8efac5ed07735b Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Wed, 17 Jun 2015 09:42:18 -0400 Subject: [PATCH 0255/2522] minor doc update --- cloud/vmware/vsphere_copy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/vmware/vsphere_copy b/cloud/vmware/vsphere_copy index f5f12f83555..f85beab481d 100644 --- a/cloud/vmware/vsphere_copy +++ b/cloud/vmware/vsphere_copy @@ -24,7 +24,7 @@ module: vsphere_copy short_description: Copy a file to a vCenter datastore description: Upload files to a vCenter datastore version_added: 2.0 -author: Dag Wieers +author: Dag Wieers (@dagwieers) options: host: description: @@ -55,8 +55,8 @@ options: - The file to push to the datastore on the vCenter server. required: true notes: - - This module ought to be run from a system that can access vCenter directly. - Either by using C(transport: local), or using C(delegate_to). + - This module ought to be run from a system that can access vCenter directly and has the file to transfer. + It can be the normal remote target or you can change it either by using C(transport: local) or using C(delegate_to). - Tested on vSphere 5.5 ''' From 656e1a6deb965dbc25a8e3a4f7afc4ee7ac22814 Mon Sep 17 00:00:00 2001 From: Gerrit Germis Date: Wed, 17 Jun 2015 17:29:38 +0200 Subject: [PATCH 0256/2522] allow wait, wait_retries and wait_interval parameters for haproxy module. This allows the haproxy to wait for status "UP" when state=enabled and status "MAINT" when state=disabled --- network/haproxy.py | 72 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/network/haproxy.py b/network/haproxy.py index 00fc4ff63a1..64059cbdf1c 100644 --- a/network/haproxy.py +++ b/network/haproxy.py @@ -68,6 +68,21 @@ - When disabling server, immediately terminate all the sessions attached to the specified server. This can be used to terminate long-running sessions after a server is put into maintenance mode, for instance. required: false default: false + wait: + description: + - Wait until the server reports a status of 'UP' when state=enabled, or status of 'MAINT' when state=disabled + required: false + default: false + wait_retries: + description: + - number of times to check for status after changing the state + required: false + default: 20 + wait_interval: + description: + - number of seconds to wait between retries + required: false + default: 1 ''' EXAMPLES = ''' @@ -82,12 +97,21 @@ # disable server, provide socket file - haproxy: state=disabled host={{ inventory_hostname }} socket=/var/run/haproxy.sock backend=www +# disable server, provide socket file, wait until status reports in maintenance +- haproxy: state=disabled host={{ inventory_hostname }} socket=/var/run/haproxy.sock backend=www wait=yes + # disable backend server in 'www' backend pool and drop open sessions to it - haproxy: state=disabled host={{ inventory_hostname }} backend=www socket=/var/run/haproxy.sock shutdown_sessions=true # enable server in 'www' backend pool - haproxy: state=enabled host={{ inventory_hostname }} backend=www +# enable server in 'www' backend pool wait until healthy +- haproxy: state=enabled host={{ inventory_hostname }} backend=www wait=yes + +# enable server in 'www' backend pool wait until healthy. Retry 10 times with intervals of 5 seconds to retrieve the health +- haproxy: state=enabled host={{ inventory_hostname }} backend=www wait=yes wait_retries=10 wait_interval=5 + # enable server in 'www' backend pool with change server(s) weight - haproxy: state=enabled host={{ inventory_hostname }} socket=/var/run/haproxy.sock weight=10 backend=www @@ -95,11 +119,15 @@ ''' import socket +import csv +import time DEFAULT_SOCKET_LOCATION="/var/run/haproxy.sock" RECV_SIZE = 1024 ACTION_CHOICES = ['enabled', 'disabled'] +WAIT_RETRIES=20 +WAIT_INTERVAL=1 ###################################################################### class TimeoutException(Exception): @@ -126,10 +154,12 @@ def __init__(self, module): self.weight = self.module.params['weight'] self.socket = self.module.params['socket'] self.shutdown_sessions = self.module.params['shutdown_sessions'] - + self.wait = self.module.params['wait'] + self.wait_retries = self.module.params['wait_retries'] + self.wait_interval = self.module.params['wait_interval'] self.command_results = [] - def execute(self, cmd, timeout=200): + def execute(self, cmd, timeout=200, capture_output=True): """ Executes a HAProxy command by sending a message to a HAProxy's local UNIX socket and waiting up to 'timeout' milliseconds for the response. @@ -144,10 +174,35 @@ def execute(self, cmd, timeout=200): while buf: result += buf buf = self.client.recv(RECV_SIZE) - self.command_results = result.strip() + if capture_output: + self.command_results = result.strip() self.client.close() return result + def wait_until_status(self, pxname, svname, status): + """ + Wait for a server to become active (status == 'UP'). Try RETRIES times + with INTERVAL seconds of sleep in between. If the service has not reached + the expected status in that time, the module will fail. If the service was + not found, the module will fail. + """ + for i in range(1, self.wait_retries): + data = self.execute('show stat', 200, False).lstrip('# ') + r = csv.DictReader(data.splitlines()) + found = False + for row in r: + if row['pxname'] == pxname and row['svname'] == svname: + found = True + if row['status'] == status: + return True; + else: + time.sleep(self.wait_interval) + + if not found: + self.module.fail_json(msg="unable to find server %s/%s" % (pxname, svname)) + + self.module.fail_json(msg="server %s/%s not status '%s' after %d retries. Aborting." % (pxname, svname, status, self.wait_retries)) + def enabled(self, host, backend, weight): """ Enabled action, marks server to UP and checks are re-enabled, @@ -170,6 +225,8 @@ def enabled(self, host, backend, weight): if weight: cmd += "; set weight %s/%s %s" % (pxname, svname, weight) self.execute(cmd) + if self.wait: + self.wait_until_status(pxname, svname, 'UP') else: pxname = backend @@ -177,6 +234,8 @@ def enabled(self, host, backend, weight): if weight: cmd += "; set weight %s/%s %s" % (pxname, svname, weight) self.execute(cmd) + if self.wait: + self.wait_until_status(pxname, svname, 'UP') def disabled(self, host, backend, shutdown_sessions): """ @@ -200,6 +259,8 @@ def disabled(self, host, backend, shutdown_sessions): if shutdown_sessions: cmd += "; shutdown sessions server %s/%s" % (pxname, svname) self.execute(cmd) + if self.wait: + self.wait_until_status(pxname, svname, 'MAINT') else: pxname = backend @@ -207,6 +268,8 @@ def disabled(self, host, backend, shutdown_sessions): if shutdown_sessions: cmd += "; shutdown sessions server %s/%s" % (pxname, svname) self.execute(cmd) + if self.wait: + self.wait_until_status(pxname, svname, 'MAINT') def act(self): """ @@ -236,6 +299,9 @@ def main(): weight=dict(required=False, default=None), socket = dict(required=False, default=DEFAULT_SOCKET_LOCATION), shutdown_sessions=dict(required=False, default=False), + wait=dict(required=False, default=False), + wait_retries=dict(required=False, default=WAIT_RETRIES), + wait_interval=dict(required=False, default=WAIT_INTERVAL), ), ) From 5a1109229d6cb0c352e75866e1b2ace47ff24d17 Mon Sep 17 00:00:00 2001 From: Gerrit Germis Date: Thu, 18 Jun 2015 09:11:16 +0200 Subject: [PATCH 0257/2522] added version_added: "2.0" to new parameters --- network/haproxy.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/network/haproxy.py b/network/haproxy.py index 64059cbdf1c..690aa60bbba 100644 --- a/network/haproxy.py +++ b/network/haproxy.py @@ -73,16 +73,19 @@ - Wait until the server reports a status of 'UP' when state=enabled, or status of 'MAINT' when state=disabled required: false default: false + version_added: "2.0" wait_retries: description: - number of times to check for status after changing the state required: false default: 20 + version_added: "2.0" wait_interval: description: - number of seconds to wait between retries required: false default: 1 + version_added: "2.0" ''' EXAMPLES = ''' @@ -181,7 +184,7 @@ def execute(self, cmd, timeout=200, capture_output=True): def wait_until_status(self, pxname, svname, status): """ - Wait for a server to become active (status == 'UP'). Try RETRIES times + Wait for a service to reach the specified status. Try RETRIES times with INTERVAL seconds of sleep in between. If the service has not reached the expected status in that time, the module will fail. If the service was not found, the module will fail. From 1b0676b559eb0dafb6dba6fe0502903821e0a701 Mon Sep 17 00:00:00 2001 From: "Dustin C. Hatch" Date: Wed, 17 Jun 2015 16:12:58 -0500 Subject: [PATCH 0258/2522] packaging/os/portage: Improve check mode handling When running in check mode, the *portage* module always reports that no changes were made, even if the requested packages do not exist on the system. This is because it was erroneously expecting `emerge --pretend` to produce the same output as `emerge` by itself would, and attempts to parse it. This is not correct, for several reasons. Most specifically, the string for which it is searching does not exist in the pretend output. Additionally, `emerge --pretend` always prints the requested packages, whether they are already installed or not; in the former case, it shows them as reinstalls. This commit adjusts the behavior to rely on `equery` alone when running in check mode. If `equery` reports at least one package is not installed, then nothing else is done: the system will definitely be changed. Signed-off-by: Dustin C. Hatch --- packaging/os/portage.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packaging/os/portage.py b/packaging/os/portage.py index 2ce0379a8ec..712881a91ea 100644 --- a/packaging/os/portage.py +++ b/packaging/os/portage.py @@ -254,6 +254,8 @@ def emerge_packages(module, packages): break else: module.exit_json(changed=False, msg='Packages already present.') + if module.check_mode: + module.exit_json(changed=True, msg='Packages would be installed.') args = [] emerge_flags = { From e3d608297d95a7c04d54303ee0abd6fda64dcde1 Mon Sep 17 00:00:00 2001 From: "Dustin C. Hatch" Date: Thu, 18 Jun 2015 13:55:03 -0500 Subject: [PATCH 0259/2522] packaging/os/portage: Handle noreplace in check mode The `--noreplace` argument to `emerge` is generally coupled with `--newuse` or `--changed-use`, and can be used instruct Portage to rebuild a package only if necessary. Simply checking to see if the package is already installed using `equery` is not sufficient to determine if any changes would be made, so that step is skipped when the `noreplace` module argument is specified. The module then falls back to parsing the output from `emerge` to determine if anything changed. In check mode, `emerge` is called with `--pretend`, so it produces different output, and the parsing fails to correctly infer that a change would be made. This commit adds another regular expression to check when running in check mode that matches the pretend output from `emerge`. Signed-off-by: Dustin C. Hatch --- packaging/os/portage.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packaging/os/portage.py b/packaging/os/portage.py index 712881a91ea..79db8d74740 100644 --- a/packaging/os/portage.py +++ b/packaging/os/portage.py @@ -300,13 +300,18 @@ def emerge_packages(module, packages): changed = True for line in out.splitlines(): if re.match(r'(?:>+) Emerging (?:binary )?\(1 of', line): + msg = 'Packages installed.' + break + elif module.check_mode and re.match(r'\[(binary|ebuild)', line): + msg = 'Packages would be installed.' break else: changed = False + msg = 'No packages installed.' module.exit_json( changed=changed, cmd=cmd, rc=rc, stdout=out, stderr=err, - msg='Packages installed.', + msg=msg, ) From 623a29cc0ecb00ddea636b89414517380a29d48b Mon Sep 17 00:00:00 2001 From: Phil Date: Thu, 18 Jun 2015 16:15:15 -0500 Subject: [PATCH 0260/2522] update to not auto-install PSCX - will use built-in powershell method instead for .zip files - added example for installing pscx as a pretask --- windows/win_unzip.ps1 | 52 +++++-------------------------------------- windows/win_unzip.py | 17 ++++++++++++-- 2 files changed, 21 insertions(+), 48 deletions(-) diff --git a/windows/win_unzip.ps1 b/windows/win_unzip.ps1 index 8e6db762fe1..35a55c811c4 100644 --- a/windows/win_unzip.ps1 +++ b/windows/win_unzip.ps1 @@ -1,7 +1,7 @@ #!powershell # This file is part of Ansible # -# Copyright 2014, Phil Schwartz +# Copyright 2015, Phil Schwartz # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -80,43 +80,13 @@ If ($ext -eq ".zip" -And $recurse -eq $false) { Fail-Json $result "Error unzipping $src to $dest" } } -# Need PSCX +# Requires PSCX Else { - # Requires PSCX, will be installed if it isn't found - # Pscx-3.2.0.msi - $url = "http://download-codeplex.sec.s-msft.com/Download/Release?ProjectName=pscx&DownloadId=923562&FileTime=130585918034470000&Build=20959" - $msi = "C:\Pscx-3.2.0.msi" - # Check if PSCX is installed $list = Get-Module -ListAvailable - # If not download it and install + If (-Not ($list -match "PSCX")) { - # Try install with chocolatey - Try { - cinst -force PSCX -y - $choco = $true - } - Catch { - $choco = $false - } - # install from downloaded msi if choco failed or is not present - If ($choco -eq $false) { - Try { - $client = New-Object System.Net.WebClient - $client.DownloadFile($url, $msi) - } - Catch { - Fail-Json $result "Error downloading PSCX from $url and saving as $dest" - } - Try { - Start-Process -FilePath msiexec.exe -ArgumentList "/i $msi /qb" -Verb Runas -PassThru -Wait | out-null - } - Catch { - Fail-Json $result "Error installing $msi" - } - } - Set-Attr $result.win_zip "pscx_status" "pscx was installed" - $installed = $true + Fail-Json "PowerShellCommunityExtensions PowerShell Module (PSCX) is required for non-'.zip' compressed archive types." } Else { Set-Attr $result.win_zip "pscx_status" "present" @@ -124,17 +94,7 @@ Else { # Import Try { - If ($installed) { - Try { - Import-Module 'C:\Program Files (x86)\Powershell Community Extensions\pscx3\pscx\pscx.psd1' - } - Catch { - Import-Module PSCX - } - } - Else { - Import-Module PSCX - } + Import-Module PSCX } Catch { Fail-Json $result "Error importing module PSCX" @@ -193,4 +153,4 @@ Set-Attr $result.win_unzip "src" $src.toString() Set-Attr $result.win_unzip "dest" $dest.toString() Set-Attr $result.win_unzip "recurse" $recurse.toString() -Exit-Json $result; +Exit-Json $result; \ No newline at end of file diff --git a/windows/win_unzip.py b/windows/win_unzip.py index 35093aa8c76..2c3c41df0b7 100644 --- a/windows/win_unzip.py +++ b/windows/win_unzip.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# (c) 2014, Phil Schwartz +# (c) 2015, Phil Schwartz # # This file is part of Ansible # @@ -74,7 +74,7 @@ required: false default: false aliases: [] -author: Phil Schwartz +author: Phil Schwartz ''' EXAMPLES = ''' @@ -126,4 +126,17 @@ delay=15 timeout=600 state=started + +# Install PSCX to use for extracting a gz file + - name: Grab PSCX msi + win_get_url: + url: 'http://download-codeplex.sec.s-msft.com/Download/Release?ProjectName=pscx&DownloadId=923562&FileTime=130585918034470000&Build=20959' + dest: 'C:\\pscx.msi' + - name: Install PSCX + win_msi: + path: 'C:\\pscx.msi' + - name: Unzip gz log + win_unzip: + src: "C:\\Logs\\application-error-logs.gz" + dest: "C:\\ExtractedLogs\\application-error-logs" ''' From d72bb17de1205276963b169913206feb9410d9e3 Mon Sep 17 00:00:00 2001 From: Phil Date: Thu, 18 Jun 2015 17:25:05 -0500 Subject: [PATCH 0261/2522] check if the rule exists or not before allow/deny rules are added/removed, and fixes where result changed would be true on all executions. --- windows/win_acl.ps1 | 37 ++++++++++++++++++++++++++++--------- windows/win_acl.py | 2 +- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/windows/win_acl.ps1 b/windows/win_acl.ps1 index 320627c03f0..130b17e8304 100644 --- a/windows/win_acl.ps1 +++ b/windows/win_acl.ps1 @@ -1,7 +1,7 @@ #!powershell # This file is part of Ansible # -# Copyright 2014, Phil Schwartz +# Copyright 2015, Phil Schwartz # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -118,26 +118,45 @@ Try { $objACE = New-Object System.Security.AccessControl.FileSystemAccessRule ($objUser, $colRights, $InheritanceFlag, $PropagationFlag, $objType) $objACL = Get-ACL $src - If ($state -eq "add") { + # Check if the ACE exists already in the objects ACL list + $match = $false + ForEach($rule in $objACL.Access){ + If (($rule.FileSystemRights -eq $objACE.FileSystemRights) -And ($rule.AccessControlType -eq $objACE.AccessControlType) -And ($rule.IdentityReference -eq $objACE.IdentityReference) -And ($rule.IsInherited -eq $objACE.IsInherited) -And ($rule.InheritanceFlags -eq $objACE.InheritanceFlags) -And ($rule.PropagationFlags -eq $objACE.PropagationFlags)) { + $match = $true + Break + } + } + + If ($state -eq "add" -And $match -eq $false) { Try { $objACL.AddAccessRule($objACE) + Set-ACL $src $objACL + $result.changed = $true } Catch { - Fail-Json $result "an exception occured when adding the specified rule. it may already exist." + Fail-Json $result "an exception occured when adding the specified rule" } } - Else { + ElseIf ($state -eq "remove" -And $match -eq $true) { Try { $objACL.RemoveAccessRule($objACE) + Set-ACL $src $objACL + $result.changed = $true } Catch { - Fail-Json $result "an exception occured when removing the specified rule. it may not exist." + Fail-Json $result "an exception occured when removing the specified rule" } } - - Set-ACL $src $objACL - - $result.changed = $true + Else { + # A rule was attempting to be added but already exists + If ($match -eq $true) { + Exit-Json $result "the specified rule already exists" + } + # A rule didn't exist that was trying to be removed + Else { + Exit-Json $result "the specified rule does not exist" + } + } } Catch { Fail-Json $result "an error occured when attempting to $state $rights permission(s) on $src for $user" diff --git a/windows/win_acl.py b/windows/win_acl.py index 56f8c84d0db..96cfc5751b9 100644 --- a/windows/win_acl.py +++ b/windows/win_acl.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# (c) 2014, Phil Schwartz +# (c) 2015, Phil Schwartz # # This file is part of Ansible # From 5e5eec1806e406127484e18492f4c1d6b45a6341 Mon Sep 17 00:00:00 2001 From: Andrew Udvare Date: Thu, 18 Jun 2015 15:59:46 -0700 Subject: [PATCH 0262/2522] --usepkgonly does not imply --getbinpkg Add usepkg option to allow conditional building from source if binary packages are not found https://github.com/ansible/ansible-modules-extras/commit/5a6de937cb053d8366e06c01ec59b37c22d0629c#commitcomment-11755140 https://wiki.gentoo.org/wiki/Binary_package_guide#Using_binary_packages --- packaging/os/portage.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packaging/os/portage.py b/packaging/os/portage.py index ab96cb22e60..e62b0983033 100644 --- a/packaging/os/portage.py +++ b/packaging/os/portage.py @@ -267,14 +267,14 @@ def emerge_packages(module, packages): 'verbose': '--verbose', 'getbinpkg': '--getbinpkg', 'usepkgonly': '--usepkgonly', + 'usepkg': '--usepkg', } for flag, arg in emerge_flags.iteritems(): if p[flag]: args.append(arg) - # usepkgonly implies getbinpkg - if p['usepkgonly'] and not p['getbinpkg']: - args.append('--getbinpkg') + if 'usepkg' in p and 'usepkgonly' in p: + module.fail_json(msg='Use only one of usepkg, usepkgonly') cmd, (rc, out, err) = run_emerge(module, packages, *args) if rc != 0: @@ -406,6 +406,7 @@ def main(): sync=dict(default=None, choices=['yes', 'web']), getbinpkg=dict(default=None, choices=['yes']), usepkgonly=dict(default=None, choices=['yes']), + usepkg=dict(default=None, choices=['yes']), ), required_one_of=[['package', 'sync', 'depclean']], mutually_exclusive=[['nodeps', 'onlydeps'], ['quiet', 'verbose']], From d8b4bd3c34c1e8989394decad04912973a4dcc96 Mon Sep 17 00:00:00 2001 From: Robert Estelle Date: Thu, 13 Nov 2014 19:38:52 -0500 Subject: [PATCH 0263/2522] Split out route table and subnet functionality from VPC module. --- cloud/amazon/ec2_vpc_subnet.py | 307 +++++++++++++++++++++++++++++++++ 1 file changed, 307 insertions(+) create mode 100644 cloud/amazon/ec2_vpc_subnet.py diff --git a/cloud/amazon/ec2_vpc_subnet.py b/cloud/amazon/ec2_vpc_subnet.py new file mode 100644 index 00000000000..396719d4e0a --- /dev/null +++ b/cloud/amazon/ec2_vpc_subnet.py @@ -0,0 +1,307 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ec2_vpc_subnet +short_description: Configure subnets in AWS virtual private clouds. +description: + - Create or removes AWS subnets in a VPC. This module has a''' +''' dependency on python-boto. +version_added: "1.8" +options: + vpc_id: + description: + - A VPC id in which the subnet resides + required: false + default: null + aliases: [] + resource_tags: + description: + - 'A dictionary array of resource tags of the form: { tag1: value1,''' +''' tag2: value2 }. This module identifies a subnet by CIDR and will update''' +''' the subnet's tags to match. Tags not in this list will be ignored. + required: false + default: null + aliases: [] + cidr: + description: + - "The cidr block for the subnet, e.g. 10.0.0.0/16" + required: false, unless state=present + az: + description: + - "The availability zone for the subnet" + required: false, unless state=present + region: + description: + - region in which the resource exists. + required: false + default: null + aliases: ['aws_region', 'ec2_region'] + state: + description: + - Create or remove the subnet + required: true + default: present + aliases: [] + aws_secret_key: + description: + - AWS secret key. If not set then the value of the AWS_SECRET_KEY''' +''' environment variable is used. + required: false + default: None + aliases: ['ec2_secret_key', 'secret_key' ] + aws_access_key: + description: + - AWS access key. If not set then the value of the AWS_ACCESS_KEY''' +''' environment variable is used. + required: false + default: None + aliases: ['ec2_access_key', 'access_key' ] + validate_certs: + description: + - When set to "no", SSL certificates will not be validated for''' +''' boto versions >= 2.6.0. + required: false + default: "yes" + choices: ["yes", "no"] + aliases: [] + +requirements: ["boto"] +author: Robert Estelle +''' + +EXAMPLES = ''' +# Note: None of these examples set aws_access_key, aws_secret_key, or region. +# It is assumed that their matching environment variables are set. + +# Basic creation example: +- name: Set up the subnet for database servers + local_action: + module: ec2_vpc_subnet + state: present + vpc_id: vpc-123456 + region: us-west-1 + cidr: 10.0.1.16/28 + resource_tags: + Name: Database Subnet + register: database_subnet + +# Removal of a VPC by id +- name: Set up the subnet for database servers + local_action: + module: ec2_vpc + state: absent + vpc_id: vpc-123456 + region: us-west-1 + cidr: 10.0.1.16/28 +''' + + +import sys +import time + +try: + import boto.ec2 + import boto.vpc + from boto.exception import EC2ResponseError +except ImportError: + print "failed=True msg='boto required for this module'" + sys.exit(1) + + +class VPCSubnetException(Exception): + pass + + +class VPCSubnetCreationException(VPCSubnetException): + pass + + +class VPCSubnetDeletionException(VPCSubnetException): + pass + + +class TagCreationException(VPCSubnetException): + pass + + +def subnet_exists(vpc_conn, subnet_id): + filters = {'subnet-id': subnet_id} + return len(vpc_conn.get_all_subnets(filters=filters)) > 0 + + +def create_subnet(vpc_conn, vpc_id, cidr, az): + try: + new_subnet = vpc_conn.create_subnet(vpc_id, cidr, az) + # Sometimes AWS takes its time to create a subnet and so using + # new subnets's id to do things like create tags results in + # exception. boto doesn't seem to refresh 'state' of the newly + # created subnet, i.e.: it's always 'pending'. + while not subnet_exists(vpc_conn, new_subnet.id): + time.sleep(0.1) + except EC2ResponseError as e: + raise VPCSubnetCreationException( + 'Unable to create subnet {0}, error: {1}'.format(cidr, e)) + return new_subnet + + +def get_resource_tags(vpc_conn, resource_id): + return {t.name: t.value for t in + vpc_conn.get_all_tags(filters={'resource-id': resource_id})} + + +def dict_diff(old, new): + x = {} + old_keys = set(old.keys()) + new_keys = set(new.keys()) + + for k in old_keys.difference(new_keys): + x[k] = {'old': old[k]} + + for k in new_keys.difference(old_keys): + x[k] = {'new': new[k]} + + for k in new_keys.intersection(old_keys): + if new[k] != old[k]: + x[k] = {'new': new[k], 'old': old[k]} + + return x + + +def ensure_tags(vpc_conn, resource_id, tags, add_only, dry_run): + try: + cur_tags = get_resource_tags(vpc_conn, resource_id) + diff = dict_diff(cur_tags, tags) + if not diff: + return {'changed': False, 'tags': cur_tags} + + to_delete = {k: diff[k]['old'] for k in diff if 'new' not in diff[k]} + if to_delete and not add_only: + vpc_conn.delete_tags(resource_id, to_delete, dry_run=dry_run) + + to_add = {k: diff[k]['new'] for k in diff if 'old' not in diff[k]} + if to_add: + vpc_conn.create_tags(resource_id, to_add, dry_run=dry_run) + + latest_tags = get_resource_tags(vpc_conn, resource_id) + return {'changed': True, 'tags': latest_tags} + except EC2ResponseError as e: + raise TagCreationException('Unable to update tags for {0}, error: {1}' + .format(resource_id, e)) + + +def get_matching_subnet(vpc_conn, vpc_id, cidr): + subnets = vpc_conn.get_all_subnets(filters={'vpc_id': vpc_id}) + return next((s for s in subnets if s.cidr_block == cidr), None) + + +def ensure_subnet_present(vpc_conn, vpc_id, cidr, az, tags, check_mode): + subnet = get_matching_subnet(vpc_conn, vpc_id, cidr) + changed = False + if subnet is None: + if check_mode: + return {'changed': True, 'subnet_id': None, 'subnet': {}} + + subnet = create_subnet(vpc_conn, vpc_id, cidr, az) + changed = True + + if tags is not None: + tag_result = ensure_tags(vpc_conn, subnet.id, tags, add_only=True, + dry_run=check_mode) + tags = tag_result['tags'] + changed = changed or tag_result['changed'] + else: + tags = get_resource_tags(vpc_conn, subnet.id) + + return { + 'changed': changed, + 'subnet_id': subnet.id, + 'subnet': { + 'tags': tags, + 'cidr': subnet.cidr_block, + 'az': subnet.availability_zone, + 'id': subnet.id, + } + } + + +def ensure_subnet_absent(vpc_conn, vpc_id, cidr, check_mode): + subnet = get_matching_subnet(vpc_conn, vpc_id, cidr) + if subnet is None: + return {'changed': False} + elif check_mode: + return {'changed': True} + + try: + vpc_conn.delete_subnet(subnet.id) + return {'changed': True} + except EC2ResponseError as e: + raise VPCSubnetDeletionException( + 'Unable to delete subnet {0}, error: {1}' + .format(subnet.cidr_block, e)) + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update({ + 'vpc_id': {'required': True}, + 'resource_tags': {'type': 'dict', 'required': True}, + 'cidr': {'required': True}, + 'az': {}, + 'state': {'choices': ['present', 'absent'], 'default': 'present'}, + }) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module) + if not region: + module.fail_json(msg='Region must be specified') + + try: + vpc_conn = boto.vpc.connect_to_region( + region, + aws_access_key_id=aws_access_key, + aws_secret_access_key=aws_secret_key + ) + except boto.exception.NoAuthHandlerFound as e: + module.fail_json(msg=str(e)) + + vpc_id = module.params.get('vpc_id') + tags = module.params.get('resource_tags') + cidr = module.params.get('cidr') + az = module.params.get('az') + state = module.params.get('state', 'present') + + try: + if state == 'present': + result = ensure_subnet_present(vpc_conn, vpc_id, cidr, az, tags, + check_mode=module.check_mode) + elif state == 'absent': + result = ensure_subnet_absent(vpc_conn, vpc_id, cidr, + check_mode=module.check_mode) + except VPCSubnetException as e: + module.fail_json(msg=str(e)) + + module.exit_json(**result) + +from ansible.module_utils.basic import * # noqa +from ansible.module_utils.ec2 import * # noqa + +if __name__ == '__main__': + main() From fa7848a6c859b4c8842380f4f25852168ecca79f Mon Sep 17 00:00:00 2001 From: Robert Estelle Date: Thu, 13 Nov 2014 19:57:15 -0500 Subject: [PATCH 0264/2522] EC2 subnet/route-table: Simplify tag updating. --- cloud/amazon/ec2_vpc_subnet.py | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/cloud/amazon/ec2_vpc_subnet.py b/cloud/amazon/ec2_vpc_subnet.py index 396719d4e0a..3d2d52c0a58 100644 --- a/cloud/amazon/ec2_vpc_subnet.py +++ b/cloud/amazon/ec2_vpc_subnet.py @@ -164,36 +164,17 @@ def get_resource_tags(vpc_conn, resource_id): vpc_conn.get_all_tags(filters={'resource-id': resource_id})} -def dict_diff(old, new): - x = {} - old_keys = set(old.keys()) - new_keys = set(new.keys()) - - for k in old_keys.difference(new_keys): - x[k] = {'old': old[k]} - - for k in new_keys.difference(old_keys): - x[k] = {'new': new[k]} - - for k in new_keys.intersection(old_keys): - if new[k] != old[k]: - x[k] = {'new': new[k], 'old': old[k]} - - return x - - def ensure_tags(vpc_conn, resource_id, tags, add_only, dry_run): try: cur_tags = get_resource_tags(vpc_conn, resource_id) - diff = dict_diff(cur_tags, tags) - if not diff: + if cur_tags == tags: return {'changed': False, 'tags': cur_tags} - to_delete = {k: diff[k]['old'] for k in diff if 'new' not in diff[k]} + to_delete = {k: cur_tags[k] for k in cur_tags if k not in tags} if to_delete and not add_only: vpc_conn.delete_tags(resource_id, to_delete, dry_run=dry_run) - to_add = {k: diff[k]['new'] for k in diff if 'old' not in diff[k]} + to_add = {k: tags[k] for k in tags if k not in cur_tags} if to_add: vpc_conn.create_tags(resource_id, to_add, dry_run=dry_run) From 54809003e31d296d544d8b51ee73cf8183dbe463 Mon Sep 17 00:00:00 2001 From: Robert Estelle Date: Mon, 1 Dec 2014 13:41:03 -0500 Subject: [PATCH 0265/2522] ec2_vpc - VPCException -> AnsibleVPCException --- cloud/amazon/ec2_vpc_subnet.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cloud/amazon/ec2_vpc_subnet.py b/cloud/amazon/ec2_vpc_subnet.py index 3d2d52c0a58..f0b8d3d5011 100644 --- a/cloud/amazon/ec2_vpc_subnet.py +++ b/cloud/amazon/ec2_vpc_subnet.py @@ -123,19 +123,19 @@ sys.exit(1) -class VPCSubnetException(Exception): +class AnsibleVPCSubnetException(Exception): pass -class VPCSubnetCreationException(VPCSubnetException): +class AnsibleVPCSubnetCreationException(AnsibleVPCSubnetException): pass -class VPCSubnetDeletionException(VPCSubnetException): +class AnsibleVPCSubnetDeletionException(AnsibleVPCSubnetException): pass -class TagCreationException(VPCSubnetException): +class AnsibleTagCreationException(AnsibleVPCSubnetException): pass @@ -154,7 +154,7 @@ def create_subnet(vpc_conn, vpc_id, cidr, az): while not subnet_exists(vpc_conn, new_subnet.id): time.sleep(0.1) except EC2ResponseError as e: - raise VPCSubnetCreationException( + raise AnsibleVPCSubnetCreationException( 'Unable to create subnet {0}, error: {1}'.format(cidr, e)) return new_subnet @@ -181,8 +181,8 @@ def ensure_tags(vpc_conn, resource_id, tags, add_only, dry_run): latest_tags = get_resource_tags(vpc_conn, resource_id) return {'changed': True, 'tags': latest_tags} except EC2ResponseError as e: - raise TagCreationException('Unable to update tags for {0}, error: {1}' - .format(resource_id, e)) + raise AnsibleTagCreationException( + 'Unable to update tags for {0}, error: {1}'.format(resource_id, e)) def get_matching_subnet(vpc_conn, vpc_id, cidr): @@ -231,7 +231,7 @@ def ensure_subnet_absent(vpc_conn, vpc_id, cidr, check_mode): vpc_conn.delete_subnet(subnet.id) return {'changed': True} except EC2ResponseError as e: - raise VPCSubnetDeletionException( + raise AnsibleVPCSubnetDeletionException( 'Unable to delete subnet {0}, error: {1}' .format(subnet.cidr_block, e)) @@ -276,7 +276,7 @@ def main(): elif state == 'absent': result = ensure_subnet_absent(vpc_conn, vpc_id, cidr, check_mode=module.check_mode) - except VPCSubnetException as e: + except AnsibleVPCSubnetException as e: module.fail_json(msg=str(e)) module.exit_json(**result) From 6f978b9c0018b04d1ec10d6643a85123a9fa5a50 Mon Sep 17 00:00:00 2001 From: Robert Estelle Date: Mon, 1 Dec 2014 13:45:50 -0500 Subject: [PATCH 0266/2522] ec2_vpc - Fail module using fail_json on boto import failure. --- cloud/amazon/ec2_vpc_subnet.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cloud/amazon/ec2_vpc_subnet.py b/cloud/amazon/ec2_vpc_subnet.py index f0b8d3d5011..a22d4b45de1 100644 --- a/cloud/amazon/ec2_vpc_subnet.py +++ b/cloud/amazon/ec2_vpc_subnet.py @@ -111,16 +111,18 @@ ''' -import sys +import sys # noqa import time try: import boto.ec2 import boto.vpc from boto.exception import EC2ResponseError + HAS_BOTO = True except ImportError: - print "failed=True msg='boto required for this module'" - sys.exit(1) + HAS_BOTO = False + if __name__ != '__main__': + raise class AnsibleVPCSubnetException(Exception): @@ -249,6 +251,8 @@ def main(): argument_spec=argument_spec, supports_check_mode=True, ) + if not HAS_BOTO: + module.fail_json(msg='boto is required for this module') ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module) if not region: From 0de9722e0cc1cb910af2845d298c73a1da1d63d1 Mon Sep 17 00:00:00 2001 From: Robert Estelle Date: Mon, 1 Dec 2014 14:43:58 -0500 Subject: [PATCH 0267/2522] ec2_vpc_subnet - resource_tags is not required. --- cloud/amazon/ec2_vpc_subnet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_vpc_subnet.py b/cloud/amazon/ec2_vpc_subnet.py index a22d4b45de1..5c7fc8596a2 100644 --- a/cloud/amazon/ec2_vpc_subnet.py +++ b/cloud/amazon/ec2_vpc_subnet.py @@ -242,7 +242,7 @@ def main(): argument_spec = ec2_argument_spec() argument_spec.update({ 'vpc_id': {'required': True}, - 'resource_tags': {'type': 'dict', 'required': True}, + 'resource_tags': {'type': 'dict', 'required': False}, 'cidr': {'required': True}, 'az': {}, 'state': {'choices': ['present', 'absent'], 'default': 'present'}, From e33de8bd88cdf0c0f8cb4e8489889147bc3767e8 Mon Sep 17 00:00:00 2001 From: Robert Estelle Date: Mon, 1 Dec 2014 15:18:56 -0500 Subject: [PATCH 0268/2522] ec2_vpc - More dry running in check mode. --- cloud/amazon/ec2_vpc_subnet.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/cloud/amazon/ec2_vpc_subnet.py b/cloud/amazon/ec2_vpc_subnet.py index 5c7fc8596a2..b79a48f25fe 100644 --- a/cloud/amazon/ec2_vpc_subnet.py +++ b/cloud/amazon/ec2_vpc_subnet.py @@ -166,7 +166,7 @@ def get_resource_tags(vpc_conn, resource_id): vpc_conn.get_all_tags(filters={'resource-id': resource_id})} -def ensure_tags(vpc_conn, resource_id, tags, add_only, dry_run): +def ensure_tags(vpc_conn, resource_id, tags, add_only, check_mode): try: cur_tags = get_resource_tags(vpc_conn, resource_id) if cur_tags == tags: @@ -174,11 +174,11 @@ def ensure_tags(vpc_conn, resource_id, tags, add_only, dry_run): to_delete = {k: cur_tags[k] for k in cur_tags if k not in tags} if to_delete and not add_only: - vpc_conn.delete_tags(resource_id, to_delete, dry_run=dry_run) + vpc_conn.delete_tags(resource_id, to_delete, dry_run=check_mode) to_add = {k: tags[k] for k in tags if k not in cur_tags} if to_add: - vpc_conn.create_tags(resource_id, to_add, dry_run=dry_run) + vpc_conn.create_tags(resource_id, to_add, dry_run=check_mode) latest_tags = get_resource_tags(vpc_conn, resource_id) return {'changed': True, 'tags': latest_tags} @@ -204,7 +204,7 @@ def ensure_subnet_present(vpc_conn, vpc_id, cidr, az, tags, check_mode): if tags is not None: tag_result = ensure_tags(vpc_conn, subnet.id, tags, add_only=True, - dry_run=check_mode) + check_mode=check_mode) tags = tag_result['tags'] changed = changed or tag_result['changed'] else: @@ -226,11 +226,9 @@ def ensure_subnet_absent(vpc_conn, vpc_id, cidr, check_mode): subnet = get_matching_subnet(vpc_conn, vpc_id, cidr) if subnet is None: return {'changed': False} - elif check_mode: - return {'changed': True} try: - vpc_conn.delete_subnet(subnet.id) + vpc_conn.delete_subnet(subnet.id, dry_run=check_mode) return {'changed': True} except EC2ResponseError as e: raise AnsibleVPCSubnetDeletionException( From 3b38209afd8e9f255e9fdde976c7d338675e1896 Mon Sep 17 00:00:00 2001 From: Robert Estelle Date: Wed, 3 Dec 2014 13:24:54 -0500 Subject: [PATCH 0269/2522] ec2_vpc_subnet - Use dict constructor instead of comprehension. --- cloud/amazon/ec2_vpc_subnet.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cloud/amazon/ec2_vpc_subnet.py b/cloud/amazon/ec2_vpc_subnet.py index b79a48f25fe..775e8b9b12d 100644 --- a/cloud/amazon/ec2_vpc_subnet.py +++ b/cloud/amazon/ec2_vpc_subnet.py @@ -162,8 +162,8 @@ def create_subnet(vpc_conn, vpc_id, cidr, az): def get_resource_tags(vpc_conn, resource_id): - return {t.name: t.value for t in - vpc_conn.get_all_tags(filters={'resource-id': resource_id})} + return dict((t.name, t.value) for t in + vpc_conn.get_all_tags(filters={'resource-id': resource_id})) def ensure_tags(vpc_conn, resource_id, tags, add_only, check_mode): @@ -172,11 +172,11 @@ def ensure_tags(vpc_conn, resource_id, tags, add_only, check_mode): if cur_tags == tags: return {'changed': False, 'tags': cur_tags} - to_delete = {k: cur_tags[k] for k in cur_tags if k not in tags} + to_delete = dict((k, cur_tags[k]) for k in cur_tags if k not in tags) if to_delete and not add_only: vpc_conn.delete_tags(resource_id, to_delete, dry_run=check_mode) - to_add = {k: tags[k] for k in tags if k not in cur_tags} + to_add = dict((k, tags[k]) for k in tags if k not in cur_tags) if to_add: vpc_conn.create_tags(resource_id, to_add, dry_run=check_mode) From 65f98d53af8400259f35f3f6eedd7d262a269053 Mon Sep 17 00:00:00 2001 From: whiter Date: Thu, 11 Jun 2015 15:00:55 +1000 Subject: [PATCH 0270/2522] Updated documentation --- cloud/amazon/ec2_vpc_subnet.py | 83 +++++++++------------------------- 1 file changed, 21 insertions(+), 62 deletions(-) diff --git a/cloud/amazon/ec2_vpc_subnet.py b/cloud/amazon/ec2_vpc_subnet.py index 775e8b9b12d..1bfe8bd4741 100644 --- a/cloud/amazon/ec2_vpc_subnet.py +++ b/cloud/amazon/ec2_vpc_subnet.py @@ -1,116 +1,74 @@ #!/usr/bin/python -# This file is part of Ansible # -# Ansible is free software: you can redistribute it and/or modify +# This is a free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# Ansible is distributed in the hope that it will be useful, +# This Ansible library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . +# along with this library. If not, see . DOCUMENTATION = ''' --- module: ec2_vpc_subnet -short_description: Configure subnets in AWS virtual private clouds. +short_description: Manage subnets in AWS virtual private clouds description: - - Create or removes AWS subnets in a VPC. This module has a''' -''' dependency on python-boto. -version_added: "1.8" + - Manage subnets in AWS virtual private clouds +version_added: "2.0" +author: Robert Estelle, @erydo options: vpc_id: description: - - A VPC id in which the subnet resides + - VPC ID of the VPC in which to create the subnet. required: false default: null - aliases: [] resource_tags: description: - - 'A dictionary array of resource tags of the form: { tag1: value1,''' -''' tag2: value2 }. This module identifies a subnet by CIDR and will update''' -''' the subnet's tags to match. Tags not in this list will be ignored. + - A dictionary array of resource tags of the form: { tag1: value1, tag2: value2 }. This module identifies a subnet by CIDR and will update the subnet's tags to match. Tags not in this list will be ignored. required: false default: null - aliases: [] cidr: description: - - "The cidr block for the subnet, e.g. 10.0.0.0/16" - required: false, unless state=present + - The CIDR block for the subnet. E.g. 10.0.0.0/16. Only required when state=present." + required: false az: description: - - "The availability zone for the subnet" - required: false, unless state=present - region: - description: - - region in which the resource exists. + - "The availability zone for the subnet. Only required when state=present." required: false - default: null - aliases: ['aws_region', 'ec2_region'] state: description: - Create or remove the subnet required: true default: present - aliases: [] - aws_secret_key: - description: - - AWS secret key. If not set then the value of the AWS_SECRET_KEY''' -''' environment variable is used. - required: false - default: None - aliases: ['ec2_secret_key', 'secret_key' ] - aws_access_key: - description: - - AWS access key. If not set then the value of the AWS_ACCESS_KEY''' -''' environment variable is used. - required: false - default: None - aliases: ['ec2_access_key', 'access_key' ] - validate_certs: - description: - - When set to "no", SSL certificates will not be validated for''' -''' boto versions >= 2.6.0. - required: false - default: "yes" - choices: ["yes", "no"] - aliases: [] - -requirements: ["boto"] -author: Robert Estelle + choices: [ 'present', 'absent' ] +extends_documentation_fragment: aws ''' EXAMPLES = ''' -# Note: None of these examples set aws_access_key, aws_secret_key, or region. -# It is assumed that their matching environment variables are set. +# Note: These examples do not set authentication details, see the AWS Guide for details. -# Basic creation example: -- name: Set up the subnet for database servers - local_action: - module: ec2_vpc_subnet +- name: Create subnet for database servers + ec2_vpc_subnet: state: present vpc_id: vpc-123456 - region: us-west-1 cidr: 10.0.1.16/28 resource_tags: Name: Database Subnet register: database_subnet -# Removal of a VPC by id -- name: Set up the subnet for database servers - local_action: - module: ec2_vpc +- name: Remove subnet for database servers + ec2_vpc_subnet: state: absent vpc_id: vpc-123456 - region: us-west-1 cidr: 10.0.1.16/28 + ''' - import sys # noqa import time @@ -288,3 +246,4 @@ def main(): if __name__ == '__main__': main() + \ No newline at end of file From c93cf8c0547eb0631145c60bbd0a19a55893f6dd Mon Sep 17 00:00:00 2001 From: whiter Date: Fri, 19 Jun 2015 12:10:03 +1000 Subject: [PATCH 0271/2522] Changed to use "connect_to_aws" method --- cloud/amazon/ec2_vpc_subnet.py | 54 +++++++++++++++++----------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/cloud/amazon/ec2_vpc_subnet.py b/cloud/amazon/ec2_vpc_subnet.py index 1bfe8bd4741..56efd85841a 100644 --- a/cloud/amazon/ec2_vpc_subnet.py +++ b/cloud/amazon/ec2_vpc_subnet.py @@ -20,7 +20,7 @@ description: - Manage subnets in AWS virtual private clouds version_added: "2.0" -author: Robert Estelle, @erydo +author: Robert Estelle (@erydo) options: vpc_id: description: @@ -36,14 +36,16 @@ description: - The CIDR block for the subnet. E.g. 10.0.0.0/16. Only required when state=present." required: false + default: null az: description: - "The availability zone for the subnet. Only required when state=present." required: false + default: null state: description: - Create or remove the subnet - required: true + required: false default: present choices: [ 'present', 'absent' ] extends_documentation_fragment: aws @@ -196,45 +198,43 @@ def ensure_subnet_absent(vpc_conn, vpc_id, cidr, check_mode): def main(): argument_spec = ec2_argument_spec() - argument_spec.update({ - 'vpc_id': {'required': True}, - 'resource_tags': {'type': 'dict', 'required': False}, - 'cidr': {'required': True}, - 'az': {}, - 'state': {'choices': ['present', 'absent'], 'default': 'present'}, - }) - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, + argument_spec.update( + dict( + vpc_id = dict(default=None, required=True), + resource_tags = dict(default=None, required=False, type='dict'), + cidr = dict(default=None, required=True), + az = dict(default=None, required=False), + state = dict(default='present', choices=['present', 'absent']) + ) ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + if not HAS_BOTO: module.fail_json(msg='boto is required for this module') - ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module) - if not region: - module.fail_json(msg='Region must be specified') - - try: - vpc_conn = boto.vpc.connect_to_region( - region, - aws_access_key_id=aws_access_key, - aws_secret_access_key=aws_secret_key - ) - except boto.exception.NoAuthHandlerFound as e: - module.fail_json(msg=str(e)) + region, ec2_url, aws_connect_params = get_aws_connection_info(module) + + if region: + try: + connection = connect_to_aws(boto.vpc, region, **aws_connect_params) + except (boto.exception.NoAuthHandlerFound, StandardError), e: + module.fail_json(msg=str(e)) + else: + module.fail_json(msg="region must be specified") vpc_id = module.params.get('vpc_id') tags = module.params.get('resource_tags') cidr = module.params.get('cidr') az = module.params.get('az') - state = module.params.get('state', 'present') + state = module.params.get('state') try: if state == 'present': - result = ensure_subnet_present(vpc_conn, vpc_id, cidr, az, tags, + result = ensure_subnet_present(connection, vpc_id, cidr, az, tags, check_mode=module.check_mode) elif state == 'absent': - result = ensure_subnet_absent(vpc_conn, vpc_id, cidr, + result = ensure_subnet_absent(connection, vpc_id, cidr, check_mode=module.check_mode) except AnsibleVPCSubnetException as e: module.fail_json(msg=str(e)) From 6b8c462d6605341318279a9ab11cc6843642e230 Mon Sep 17 00:00:00 2001 From: Will Thames Date: Fri, 19 Jun 2015 12:40:56 +1000 Subject: [PATCH 0272/2522] Add GUIDELINES for AWS module development Starting point for a reference when doing pull request reviews. If something doesn't meet the guidelines we can point people at them. If something is bad but is not mentioned in the guidelines, we should add it here. --- cloud/amazon/GUIDELINES.md | 88 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 cloud/amazon/GUIDELINES.md diff --git a/cloud/amazon/GUIDELINES.md b/cloud/amazon/GUIDELINES.md new file mode 100644 index 00000000000..ee5aea90ef7 --- /dev/null +++ b/cloud/amazon/GUIDELINES.md @@ -0,0 +1,88 @@ +Guidelines for AWS modules +-------------------------- + +Naming your module +================== + +Base the name of the module on the part of AWS that +you actually use. (A good rule of thumb is to take +whatever module you use with boto as a starting point). + +Don't further abbreviate names - if something is a well +known abbreviation due to it being a major component of +AWS, that's fine, but don't create new ones independently +(e.g. VPC, ELB, etc. are fine) + +Using boto +========== + +Wrap the `import` statements in a try block and fail the +module later on if the import fails + +``` +try: + import boto + import boto.module.that.you.use + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + module_specific_parameter=dict(), + ) + ) + + module = AnsibleModule( + argument_spec=argument_spec, + ) + if not HAS_BOTO: + module.fail_json(msg='boto required for this module') +``` + + +Try and keep backward compatibility with relatively recent +versions of boto. That means that if want to implement some +functionality that uses a new feature of boto, it should only +fail if that feature actually needs to be run, with a message +saying which version of boto is needed. + +Use feature testing (e.g. `hasattr('boto.module', 'shiny_new_method')`) +to check whether boto supports a feature rather than version checking + +e.g. from the `ec2` module: +``` +if boto_supports_profile_name_arg(ec2): + params['instance_profile_name'] = instance_profile_name +else: + if instance_profile_name is not None: + module.fail_json( + msg="instance_profile_name parameter requires Boto version 2.5.0 or higher") +``` + + +Connecting to AWS +================= + +For EC2 you can just use + +``` +ec2 = ec2_connect(module) +``` + +For other modules, you should use `get_aws_connection_info` and then +`connect_to_aws`. To connect to an example `xyz` service: + +``` +region, ec2_url, aws_connect_params = get_aws_connection_info(module) +xyz = connect_to_aws(boto.xyz, region, **aws_connect_params) +``` + +The reason for using `get_aws_connection_info` and `connect_to_aws` +(and even `ec2_connect` uses those under the hood) rather than doing it +yourself is that they handle some of the more esoteric connection +options such as security tokens and boto profiles. From 628f2b98b69dba0fa741c87ddcd7c45108311509 Mon Sep 17 00:00:00 2001 From: Amir Moulavi Date: Fri, 19 Jun 2015 09:12:08 +0200 Subject: [PATCH 0273/2522] Implementation of EC2 AMI copy between regions --- cloud/amazon/ec2_ami_copy.py | 211 +++++++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 cloud/amazon/ec2_ami_copy.py diff --git a/cloud/amazon/ec2_ami_copy.py b/cloud/amazon/ec2_ami_copy.py new file mode 100644 index 00000000000..909ec4a9c7a --- /dev/null +++ b/cloud/amazon/ec2_ami_copy.py @@ -0,0 +1,211 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ec2_ami_copy +short_description: copies AMI between AWS regions, return new image id +description: + - Copies AMI from a source region to a destination region. This module has a dependency on python-boto >= 2.5 +version_added: "1.7" +options: + source_region: + description: + - the source region that AMI should be copied from + required: true + default: null + region: + description: + - the destination region that AMI should be copied to + required: true + default: null + aliases: ['aws_region', 'ec2_region', 'dest_region'] + source_image_id: + description: + - the id of the image in source region that should be copied + required: true + default: null + name: + description: + - The name of the new image to copy + required: false + default: null + description: + description: + - An optional human-readable string describing the contents and purpose of the new AMI. + required: false + default: null + wait: + description: + - wait for the copied AMI to be in state 'available' before returning. + required: false + default: "no" + choices: [ "yes", "no" ] + wait_timeout: + description: + - how long before wait gives up, in seconds + required: false + default: 1200 + tags: + description: + - a hash/dictionary of tags to add to the new copied AMI; '{"key":"value"}' and '{"key":"value","key":"value"}' + required: false + default: null + +author: Amir Moulavi +extends_documentation_fragment: aws +''' + +EXAMPLES = ''' +# Basic AMI Copy +- local_action: + module: ec2_ami_copy + source_region: eu-west-1 + dest_region: us-east-1 + source_image_id: ami-xxxxxxx + name: SuperService-new-AMI + description: latest patch + tags: '{"Name":"SuperService-new-AMI", "type":"SuperService"}' + wait: yes + register: image_id +''' + + +import sys +import time + +try: + import boto + import boto.ec2 + from boto.vpc import VPCConnection + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +if not HAS_BOTO: + module.fail_json(msg='boto required for this module') + +def copy_image(module, ec2): + """ + Copies an AMI + + module : AnsibleModule object + ec2: authenticated ec2 connection object + """ + + source_region = module.params.get('source_region') + source_image_id = module.params.get('source_image_id') + name = module.params.get('name') + description = module.params.get('description') + tags = module.params.get('tags') + wait_timeout = int(module.params.get('wait_timeout')) + wait = module.params.get('wait') + + try: + params = {'source_region': source_region, + 'source_image_id': source_image_id, + 'name': name, + 'description': description + } + + image_id = ec2.copy_image(**params).image_id + except boto.exception.BotoServerError, e: + module.fail_json(msg="%s: %s" % (e.error_code, e.error_message)) + + img = wait_until_image_is_recognized(module, ec2, wait_timeout, image_id, wait) + + img = wait_until_image_is_copied(module, ec2, wait_timeout, img, image_id, wait) + + register_tags_if_any(module, ec2, tags, image_id) + + module.exit_json(msg="AMI copy operation complete", image_id=image_id, state=img.state, changed=True) + + +# register tags to the copied AMI in dest_region +def register_tags_if_any(module, ec2, tags, image_id): + if tags: + try: + ec2.create_tags([image_id], tags) + except Exception as e: + module.fail_json(msg=str(e)) + + +# wait here until the image is copied (i.e. the state becomes available +def wait_until_image_is_copied(module, ec2, wait_timeout, img, image_id, wait): + wait_timeout = time.time() + wait_timeout + while wait and wait_timeout > time.time() and (img is None or img.state != 'available'): + img = ec2.get_image(image_id) + time.sleep(3) + if wait and wait_timeout <= time.time(): + # waiting took too long + module.fail_json(msg="timed out waiting for image to be copied") + return img + + +# wait until the image is recognized. +def wait_until_image_is_recognized(module, ec2, wait_timeout, image_id, wait): + for i in range(wait_timeout): + try: + return ec2.get_image(image_id) + except boto.exception.EC2ResponseError, e: + # This exception we expect initially right after registering the copy with EC2 API + if 'InvalidAMIID.NotFound' in e.error_code and wait: + time.sleep(1) + else: + # On any other exception we should fail + module.fail_json( + msg="Error while trying to find the new image. Using wait=yes and/or a longer wait_timeout may help: " + str( + e)) + else: + module.fail_json(msg="timed out waiting for image to be recognized") + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + source_region=dict(required=True), + source_image_id=dict(required=True), + name=dict(), + description=dict(default=""), + wait=dict(type='bool', default=False), + wait_timeout=dict(default=1200), + tags=dict(type='dict'))) + + module = AnsibleModule(argument_spec=argument_spec) + + try: + ec2 = ec2_connect(module) + except boto.exception.NoAuthHandlerFound, e: + module.fail_json(msg=str(e)) + + try: + region, ec2_url, boto_params = get_aws_connection_info(module) + vpc = connect_to_aws(boto.vpc, region, **boto_params) + except boto.exception.NoAuthHandlerFound, e: + module.fail_json(msg = str(e)) + + if not region: + module.fail_json(msg="region must be specified") + + copy_image(module, ec2) + + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +main() + From 3f3a73da37c0c8e8425b2c41e7b9ee18f2851656 Mon Sep 17 00:00:00 2001 From: Anders Ingemann Date: Fri, 16 Jan 2015 15:59:17 +0100 Subject: [PATCH 0274/2522] Add sensu_check module --- monitoring/sensu_check.py | 328 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 328 insertions(+) create mode 100644 monitoring/sensu_check.py diff --git a/monitoring/sensu_check.py b/monitoring/sensu_check.py new file mode 100644 index 00000000000..b968304c34f --- /dev/null +++ b/monitoring/sensu_check.py @@ -0,0 +1,328 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2014, Anders Ingemann +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +DOCUMENTATION = ''' +--- +module: sensu_check +short_description: Manage Sensu checks +version_added: 2.0 +description: + - Manage the checks that should be run on a machine by I(Sensu). + - Most options do not have a default and will not be added to the check definition unless specified. + - All defaults except I(path), I(state), I(backup) and I(metric) are not managed by this module, + - they are simply specified for your convenience. +options: + name: + description: + - The name of the check + - This is the key that is used to determine whether a check exists + required: true + state: + description: Whether the check should be present or not + choices: [ 'present', 'absent' ] + required: false + default: present + path: + description: + - Path to the json file of the check to be added/removed. + - Will be created if it does not exist (unless I(state=absent)). + - The parent folders need to exist when I(state=present), otherwise an error will be thrown + required: false + default: /etc/sensu/conf.d/checks.json + backup: + description: + - Create a backup file (if yes), including the timestamp information so + - you can get the original file back if you somehow clobbered it incorrectly. + choices: [ 'yes', 'no' ] + required: false + default: no + command: + description: + - Path to the sensu check to run (not required when I(state=absent)) + required: true + handlers: + description: + - List of handlers to notify when the check fails + required: false + default: [] + subscribers: + description: + - List of subscribers/channels this check should run for + - See sensu_subscribers to subscribe a machine to a channel + required: false + default: [] + interval: + description: + - Check interval in seconds + required: false + default: null + timeout: + description: + - Timeout for the check + required: false + default: 10 + handle: + description: + - Whether the check should be handled or not + choices: [ 'yes', 'no' ] + required: false + default: yes + subdue_begin: + description: + - When to disable handling of check failures + required: false + default: null + subdue_end: + description: + - When to enable handling of check failures + required: false + default: null + dependencies: + description: + - Other checks this check depends on, if dependencies fail, + - handling of this check will be disabled + required: false + default: [] + metric: + description: Whether the check is a metric + choices: [ 'yes', 'no' ] + required: false + default: no + standalone: + description: + - Whether the check should be scheduled by the sensu client or server + - This option obviates the need for specifying the I(subscribers) option + choices: [ 'yes', 'no' ] + required: false + default: no + publish: + description: + - Whether the check should be scheduled at all. + - You can still issue it via the sensu api + choices: [ 'yes', 'no' ] + required: false + default: yes + occurrences: + description: + - Number of event occurrences before the handler should take action + required: false + default: 1 + refresh: + description: + - Number of seconds handlers should wait before taking second action + required: false + default: null + aggregate: + description: + - Classifies the check as an aggregate check, + - making it available via the aggregate API + choices: [ 'yes', 'no' ] + required: false + default: no + low_flap_threshold: + description: + - The low threshhold for flap detection + required: false + default: null + high_flap_threshold: + description: + - The low threshhold for flap detection + required: false + default: null +requirements: [ ] +author: Anders Ingemann +''' + +EXAMPLES = ''' +# Fetch metrics about the CPU load every 60 seconds, +# the sensu server has a handler called 'relay' which forwards stats to graphite +- name: get cpu metrics + sensu_check: name=cpu_load + command=/etc/sensu/plugins/system/cpu-mpstat-metrics.rb + metric=yes handlers=relay subscribers=common interval=60 + +# Check whether nginx is running +- name: check nginx process + sensu_check: name=nginx_running + command='/etc/sensu/plugins/processes/check-procs.rb -f /var/run/nginx.pid' + handlers=default subscribers=nginx interval=60 + +# Stop monitoring the disk capacity. +# Note that the check will still show up in the sensu dashboard, +# to remove it completely you need to issue a DELETE request to the sensu api. +- name: check disk + sensu_check: name=check_disk_capacity +''' + + +def sensu_check(module, path, name, state='present', backup=False): + changed = False + reasons = [] + + try: + import json + except ImportError: + import simplejson as json + + try: + with open(path) as stream: + config = json.load(stream) + except IOError as e: + if e.errno is 2: # File not found, non-fatal + if state == 'absent': + reasons.append('file did not exist and state is `absent\'') + return changed, reasons + config = {} + else: + module.fail_json(msg=str(e)) + except ValueError: + msg = '{path} contains invalid JSON'.format(path=path) + module.fail_json(msg=msg) + + if 'checks' not in config: + if state == 'absent': + reasons.append('`checks\' section did not exist and state is `absent\'') + return changed, reasons + config['checks'] = {} + changed = True + reasons.append('`checks\' section did not exist') + + if state == 'absent': + if name in config['checks']: + del config['checks'][name] + changed = True + reasons.append('check was present and state is `absent\'') + + if state == 'present': + if name not in config['checks']: + check = {} + config['checks'][name] = check + changed = True + reasons.append('check was absent and state is `present\'') + else: + check = config['checks'][name] + simple_opts = ['command', + 'handlers', + 'subscribers', + 'interval', + 'timeout', + 'handle', + 'dependencies', + 'standalone', + 'publish', + 'occurrences', + 'refresh', + 'aggregate', + 'low_flap_threshold', + 'high_flap_threshold', + ] + for opt in simple_opts: + if module.params[opt] is not None: + if opt not in check or check[opt] != module.params[opt]: + check[opt] = module.params[opt] + changed = True + reasons.append('`{opt}\' did not exist or was different'.format(opt=opt)) + else: + if opt in check: + del check[opt] + changed = True + reasons.append('`{opt}\' was removed'.format(opt=opt)) + + if module.params['metric']: + if 'type' not in check or check['type'] != 'metric': + check['type'] = 'metric' + changed = True + reasons.append('`type\' was not defined or not `metric\'') + if not module.params['metric'] and 'type' in check: + del check['type'] + changed = True + reasons.append('`type\' was defined') + + if module.params['subdue_begin'] is not None and module.params['subdue_end'] is not None: + subdue = {'begin': module.params['subdue_begin'], + 'end': module.params['subdue_end'], + } + if 'subdue' not in check or check['subdue'] != subdue: + check['subdue'] = subdue + changed = True + reasons.append('`subdue\' did not exist or was different') + else: + if 'subdue' in check: + del check['subdue'] + changed = True + reasons.append('`subdue\' was removed') + + if changed and not module.check_mode: + if backup: + module.backup_local(path) + try: + with open(path, 'w') as stream: + stream.write(json.dumps(config, indent=2) + '\n') + except IOError as e: + module.fail_json(msg=str(e)) + + return changed, reasons + + +def main(): + + arg_spec = {'name': {'type': 'str', 'required': True}, + 'path': {'type': 'str', 'default': '/etc/sensu/conf.d/checks.json'}, + 'state': {'type': 'str', 'default': 'present', 'choices': ['present', 'absent']}, + 'backup': {'type': 'bool', 'default': 'no'}, + 'command': {'type': 'str'}, + 'handlers': {'type': 'list'}, + 'subscribers': {'type': 'list'}, + 'interval': {'type': 'int'}, + 'timeout': {'type': 'int'}, + 'handle': {'type': 'bool'}, + 'subdue_begin': {'type': 'str'}, + 'subdue_end': {'type': 'str'}, + 'dependencies': {'type': 'list'}, + 'metric': {'type': 'bool', 'default': 'no'}, + 'standalone': {'type': 'bool'}, + 'publish': {'type': 'bool'}, + 'occurrences': {'type': 'int'}, + 'refresh': {'type': 'int'}, + 'aggregate': {'type': 'bool'}, + 'low_flap_threshold': {'type': 'int'}, + 'high_flap_threshold': {'type': 'int'}, + } + + required_together = [['subdue_begin', 'subdue_end']] + + module = AnsibleModule(argument_spec=arg_spec, + required_together=required_together, + supports_check_mode=True) + if module.params['state'] != 'absent' and module.params['command'] is None: + module.fail_json(msg="missing required arguments: %s" % ",".join(['command'])) + + path = module.params['path'] + name = module.params['name'] + state = module.params['state'] + backup = module.params['backup'] + + changed, reasons = sensu_check(module, path, name, state, backup) + + module.exit_json(path=path, changed=changed, msg='OK', name=name, reasons=reasons) + +from ansible.module_utils.basic import * +main() From 35b6bc417d6b825189486a094b833c226ca30bb9 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Fri, 19 Jun 2015 11:55:05 +0200 Subject: [PATCH 0275/2522] cloudstack: new module cs_facts --- cloud/cloudstack/cs_facts.py | 221 +++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 cloud/cloudstack/cs_facts.py diff --git a/cloud/cloudstack/cs_facts.py b/cloud/cloudstack/cs_facts.py new file mode 100644 index 00000000000..f8749834120 --- /dev/null +++ b/cloud/cloudstack/cs_facts.py @@ -0,0 +1,221 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2015, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: cs_facts +short_description: Gather facts on instances of Apache CloudStack based clouds. +description: + - This module fetches data from the metadata API in CloudStack. The module must be called from within the instance itself. +version_added: '2.0' +author: "René Moser (@resmo)" +options: + filter: + description: + - Filter for a specific fact. + required: false + default: null + choices: + - cloudstack_service_offering + - cloudstack_availability_zone + - cloudstack_public_hostname + - cloudstack_public_ipv4 + - cloudstack_local_hostname + - cloudstack_local_ipv4 + - cloudstack_instance_id + - cloudstack_user_data +requirements: [ 'yaml' ] +''' + +EXAMPLES = ''' +# Gather all facts on instances +- name: Gather cloudstack facts + cs_facts: + +# Gather specific fact on instances +- name: Gather cloudstack facts + cs_facts: filter=cloudstack_instance_id +''' + +RETURN = ''' +--- +cloudstack_availability_zone: + description: zone the instance is deployed in. + returned: success + type: string + sample: ch-gva-2 +cloudstack_instance_id: + description: UUID of the instance. + returned: success + type: string + sample: ab4e80b0-3e7e-4936-bdc5-e334ba5b0139 +cloudstack_local_hostname: + description: local hostname of the instance. + returned: success + type: string + sample: VM-ab4e80b0-3e7e-4936-bdc5-e334ba5b0139 +cloudstack_local_ipv4: + description: local IPv4 of the instance. + returned: success + type: string + sample: 185.19.28.35 +cloudstack_public_hostname: + description: public hostname of the instance. + returned: success + type: string + sample: VM-ab4e80b0-3e7e-4936-bdc5-e334ba5b0139 +cloudstack_public_ipv4: + description: public IPv4 of the instance. + returned: success + type: string + sample: 185.19.28.35 +cloudstack_service_offering: + description: service offering of the instance. + returned: success + type: string + sample: Micro 512mb 1cpu +cloudstack_user_data: + description: data of the instance provided by users. + returned: success + type: dict + sample: { "bla": "foo" } +''' + +import os + +try: + import yaml + has_lib_yaml = True +except ImportError: + has_lib_yaml = False + +CS_METADATA_BASE_URL = "http://%s/latest/meta-data" +CS_USERDATA_BASE_URL = "http://%s/latest/user-data" + +class CloudStackFacts(object): + + def __init__(self): + self.facts = ansible_facts(module) + self.api_ip = None + self.fact_paths = { + 'cloudstack_service_offering': 'service-offering', + 'cloudstack_availability_zone': 'availability-zone', + 'cloudstack_public_hostname': 'public-hostname', + 'cloudstack_public_ipv4': 'public-ipv4', + 'cloudstack_local_hostname': 'local-hostname', + 'cloudstack_local_ipv4': 'local-ipv4', + 'cloudstack_instance_id': 'instance-id' + } + + def run(self): + result = {} + filter = module.params.get('filter') + if not filter: + for key,path in self.fact_paths.iteritems(): + result[key] = self._fetch(CS_METADATA_BASE_URL + "/" + path) + result['cloudstack_user_data'] = self._get_user_data_json() + else: + if filter == 'cloudstack_user_data': + result['cloudstack_user_data'] = self._get_user_data_json() + elif filter in self.fact_paths: + result[filter] = self._fetch(CS_METADATA_BASE_URL + "/" + self.fact_paths[filter]) + return result + + + def _get_user_data_json(self): + try: + # this data come form users, we try what we can to parse it... + return yaml.load(self._fetch(CS_USERDATA_BASE_URL)) + except: + return None + + + def _fetch(self, path): + api_ip = self._get_api_ip() + if not api_ip: + return None + api_url = path % api_ip + (response, info) = fetch_url(module, api_url, force=True) + if response: + data = response.read() + else: + data = None + return data + + + def _get_dhcp_lease_file(self): + """Return the path of the lease file.""" + default_iface = self.facts['default_ipv4']['interface'] + dhcp_lease_file_locations = [ + '/var/lib/dhcp/dhclient.%s.leases' % default_iface, # debian / ubuntu + '/var/lib/dhclient/dhclient-%s.leases' % default_iface, # centos 6 + '/var/lib/dhclient/dhclient--%s.lease' % default_iface, # centos 7 + '/var/db/dhclient.leases.%s' % default_iface, # openbsd + ] + for file_path in dhcp_lease_file_locations: + if os.path.exists(file_path): + return file_path + module.fail_json(msg="Could not find dhclient leases file.") + + + def _get_api_ip(self): + """Return the IP of the DHCP server.""" + if not self.api_ip: + dhcp_lease_file = self._get_dhcp_lease_file() + for line in open(dhcp_lease_file): + if 'dhcp-server-identifier' in line: + # get IP of string "option dhcp-server-identifier 185.19.28.176;" + line = line.translate(None, ';') + self.api_ip = line.split()[2] + break + if not self.api_ip: + module.fail_json(msg="No dhcp-server-identifier found in leases file.") + return self.api_ip + + +def main(): + global module + module = AnsibleModule( + argument_spec = dict( + filter = dict(default=None, choices=[ + 'cloudstack_service_offering', + 'cloudstack_availability_zone', + 'cloudstack_public_hostname', + 'cloudstack_public_ipv4', + 'cloudstack_local_hostname', + 'cloudstack_local_ipv4', + 'cloudstack_instance_id', + 'cloudstack_user_data', + ]), + ), + supports_check_mode=False + ) + + if not has_lib_yaml: + module.fail_json(msg="missing python library: yaml") + + cs_facts = CloudStackFacts().run() + cs_facts_result = dict(changed=False, ansible_facts=cs_facts) + module.exit_json(**cs_facts_result) + +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * +from ansible.module_utils.facts import * +main() From d0cf9617a54a49ecf819076555cce931a0f71683 Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Fri, 19 Jun 2015 13:30:29 +0200 Subject: [PATCH 0276/2522] Spurious newline could corrupt payload Due to a spurious newline we corrupted the payload. It depends on the order of the headers and if there were headers added by vSphere. The Accept header was also not needed. --- cloud/vmware/vsphere_copy | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cloud/vmware/vsphere_copy b/cloud/vmware/vsphere_copy index f85beab481d..0ca9780c008 100644 --- a/cloud/vmware/vsphere_copy +++ b/cloud/vmware/vsphere_copy @@ -120,11 +120,10 @@ def main(): atexit.register(conn.close) remote_path = vmware_path(datastore, datacenter, dest) - auth = base64.encodestring('%s:%s' % (login, password)) + auth = base64.encodestring('%s:%s' % (login, password)).rstrip() headers = { "Content-Type": "application/octet-stream", "Content-Length": str(len(data)), - "Accept": "text/plain", "Authorization": "Basic %s" % auth, } From e203087aaabea0c0cefe6ae3d1b072ecbde84cf8 Mon Sep 17 00:00:00 2001 From: Andrew Udvare Date: Fri, 19 Jun 2015 06:04:56 -0700 Subject: [PATCH 0277/2522] Fix comparison --- packaging/os/portage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/os/portage.py b/packaging/os/portage.py index e62b0983033..1043679585b 100644 --- a/packaging/os/portage.py +++ b/packaging/os/portage.py @@ -273,7 +273,7 @@ def emerge_packages(module, packages): if p[flag]: args.append(arg) - if 'usepkg' in p and 'usepkgonly' in p: + if p['usepkg'] and p['usepkgonly']: module.fail_json(msg='Use only one of usepkg, usepkgonly') cmd, (rc, out, err) = run_emerge(module, packages, *args) From 35a4e70deef1860eb944bdc73d6d8ca19af0444d Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Wed, 17 Jun 2015 12:46:16 -0400 Subject: [PATCH 0278/2522] minor fixes --- notification/hall.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/notification/hall.py b/notification/hall.py index 7c76e52379f..05c1a981b73 100755 --- a/notification/hall.py +++ b/notification/hall.py @@ -18,18 +18,18 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . - + DOCUMENTATION = """ module: hall short_description: Send notification to Hall description: - - The M(hall) module connects to the U(https://hall.com) messaging API and allows you to deliver notication messages to rooms. -version_added: 1.6 -author: Billy Kimble + - "The M(hall) module connects to the U(https://hall.com) messaging API and allows you to deliver notication messages to rooms." +version_added: "2.0" +author: Billy Kimble (@bkimble) options: room_token: description: - - Room token provided to you by setting up the Ansible room integation on U(https://hall.com) + - "Room token provided to you by setting up the Ansible room integation on U(https://hall.com)" required: true msg: description: @@ -41,12 +41,12 @@ required: true picture: description: - - The full URL to the image you wish to use for the Icon of the message. Defaults to U(http://cdn2.hubspot.net/hub/330046/file-769078210-png/Official_Logos/ansible_logo_black_square_small.png?t=1421076128627) + - "The full URL to the image you wish to use for the Icon of the message. Defaults to U(http://cdn2.hubspot.net/hub/330046/file-769078210-png/Official_Logos/ansible_logo_black_square_small.png?t=1421076128627)" required: false -""" +""" EXAMPLES = """ -- name: Send Hall notifiation +- name: Send Hall notifiation local_action: module: hall room_token: @@ -57,7 +57,7 @@ when: ec2.instances|length > 0 local_action: module: hall - room_token: + room_token: title: Server Creation msg: "Created EC2 instance {{ item.id }} of type {{ item.instance_type }}.\\nInstance can be reached at {{ item.public_ip }} in the {{ item.region }} region." with_items: ec2.instances @@ -66,7 +66,7 @@ HALL_API_ENDPOINT = 'https://hall.com/api/1/services/generic/%s' def send_request_to_hall(module, room_token, payload): - headers = {'Content-Type': 'application/json'} + headers = {'Content-Type': 'application/json'} payload=module.jsonify(payload) api_endpoint = HALL_API_ENDPOINT % (room_token) response, info = fetch_url(module, api_endpoint, data=payload, headers=headers) @@ -83,7 +83,7 @@ def main(): picture = dict(type='str', default='http://cdn2.hubspot.net/hub/330046/file-769078210-png/Official_Logos/ansible_logo_black_square_small.png?t=1421076128627'), ) ) - + room_token = module.params['room_token'] message = module.params['msg'] title = module.params['title'] From 1604382538db616867207bd1df1b05d893010213 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Fri, 19 Jun 2015 11:04:25 -0400 Subject: [PATCH 0279/2522] monior docfixes added extensino to vsphere_copy so it actually installs --- cloud/amazon/ec2_ami_copy.py | 5 +---- cloud/amazon/ec2_eni.py | 6 +++--- cloud/amazon/ec2_eni_facts.py | 4 ++-- cloud/vmware/{vsphere_copy => vsphere_copy.py} | 4 ++-- 4 files changed, 8 insertions(+), 11 deletions(-) rename cloud/vmware/{vsphere_copy => vsphere_copy.py} (96%) diff --git a/cloud/amazon/ec2_ami_copy.py b/cloud/amazon/ec2_ami_copy.py index 909ec4a9c7a..ff9bde88022 100644 --- a/cloud/amazon/ec2_ami_copy.py +++ b/cloud/amazon/ec2_ami_copy.py @@ -20,24 +20,21 @@ short_description: copies AMI between AWS regions, return new image id description: - Copies AMI from a source region to a destination region. This module has a dependency on python-boto >= 2.5 -version_added: "1.7" +version_added: "2.0" options: source_region: description: - the source region that AMI should be copied from required: true - default: null region: description: - the destination region that AMI should be copied to required: true - default: null aliases: ['aws_region', 'ec2_region', 'dest_region'] source_image_id: description: - the id of the image in source region that should be copied required: true - default: null name: description: - The name of the new image to copy diff --git a/cloud/amazon/ec2_eni.py b/cloud/amazon/ec2_eni.py index 2b34e9b9405..9e878e7d558 100644 --- a/cloud/amazon/ec2_eni.py +++ b/cloud/amazon/ec2_eni.py @@ -25,13 +25,13 @@ eni_id: description: - The ID of the ENI - required = false - default = null + required: false + default: null instance_id: description: - Instance ID that you wish to attach ENI to. To detach an ENI from an instance, use 'None'. required: false - default: null + default: null private_ip_address: description: - Private IP address. diff --git a/cloud/amazon/ec2_eni_facts.py b/cloud/amazon/ec2_eni_facts.py index 76347c84261..981358c33af 100644 --- a/cloud/amazon/ec2_eni_facts.py +++ b/cloud/amazon/ec2_eni_facts.py @@ -25,8 +25,8 @@ eni_id: description: - The ID of the ENI. Pass this option to gather facts about a particular ENI, otherwise, all ENIs are returned. - required = false - default = null + required: false + default: null extends_documentation_fragment: aws ''' diff --git a/cloud/vmware/vsphere_copy b/cloud/vmware/vsphere_copy.py similarity index 96% rename from cloud/vmware/vsphere_copy rename to cloud/vmware/vsphere_copy.py index 0ca9780c008..7c044a7d51a 100644 --- a/cloud/vmware/vsphere_copy +++ b/cloud/vmware/vsphere_copy.py @@ -55,8 +55,8 @@ - The file to push to the datastore on the vCenter server. required: true notes: - - This module ought to be run from a system that can access vCenter directly and has the file to transfer. - It can be the normal remote target or you can change it either by using C(transport: local) or using C(delegate_to). + - "This module ought to be run from a system that can access vCenter directly and has the file to transfer. + It can be the normal remote target or you can change it either by using C(transport: local) or using C(delegate_to)." - Tested on vSphere 5.5 ''' From 4b29146c4d84a94c35e9f1bd763fcb85820e801c Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Fri, 19 Jun 2015 08:59:19 -0700 Subject: [PATCH 0280/2522] be explicit about urllib import and remove conditional urllib(2) import urllib and urllib2 have been in the python stdlib since at least python-2.3. There's no reason to conditionalize it. Fixes https://github.com/ansible/ansible/issues/11322 --- monitoring/airbrake_deployment.py | 5 +++-- monitoring/newrelic_deployment.py | 5 +++-- monitoring/rollbar_deployment.py | 1 + network/citrix/netscaler.py | 4 ++-- network/dnsmadeeasy.py | 4 +++- notification/flowdock.py | 5 +++-- notification/grove.py | 2 ++ notification/hipchat.py | 5 +++-- notification/nexmo.py | 1 + notification/sendgrid.py | 5 +---- notification/twilio.py | 5 +---- 11 files changed, 23 insertions(+), 19 deletions(-) diff --git a/monitoring/airbrake_deployment.py b/monitoring/airbrake_deployment.py index 3b54e55e751..a58df024182 100644 --- a/monitoring/airbrake_deployment.py +++ b/monitoring/airbrake_deployment.py @@ -61,8 +61,7 @@ default: 'yes' choices: ['yes', 'no'] -# informational: requirements for nodes -requirements: [ urllib, urllib2 ] +requirements: [] ''' EXAMPLES = ''' @@ -72,6 +71,8 @@ revision=4.2 ''' +import urllib + # =========================================== # Module execution. # diff --git a/monitoring/newrelic_deployment.py b/monitoring/newrelic_deployment.py index 832e467dea0..3d9bc6c0ec3 100644 --- a/monitoring/newrelic_deployment.py +++ b/monitoring/newrelic_deployment.py @@ -72,8 +72,7 @@ choices: ['yes', 'no'] version_added: 1.5.1 -# informational: requirements for nodes -requirements: [ urllib, urllib2 ] +requirements: [] ''' EXAMPLES = ''' @@ -83,6 +82,8 @@ revision=1.0 ''' +import urllib + # =========================================== # Module execution. # diff --git a/monitoring/rollbar_deployment.py b/monitoring/rollbar_deployment.py index 43e2aa00722..060193b78a5 100644 --- a/monitoring/rollbar_deployment.py +++ b/monitoring/rollbar_deployment.py @@ -76,6 +76,7 @@ comment='Test Deploy' ''' +import urllib def main(): diff --git a/network/citrix/netscaler.py b/network/citrix/netscaler.py index 61bc35356e5..384a625bdca 100644 --- a/network/citrix/netscaler.py +++ b/network/citrix/netscaler.py @@ -81,7 +81,7 @@ default: 'yes' choices: ['yes', 'no'] -requirements: [ "urllib", "urllib2" ] +requirements: [] author: "Nandor Sivok (@dominis)" ''' @@ -99,7 +99,7 @@ import base64 import socket - +import urllib class netscaler(object): diff --git a/network/dnsmadeeasy.py b/network/dnsmadeeasy.py index fcc7232a0da..cce7bd10082 100644 --- a/network/dnsmadeeasy.py +++ b/network/dnsmadeeasy.py @@ -86,7 +86,7 @@ - The DNS Made Easy service requires that machines interacting with the API have the proper time and timezone set. Be sure you are within a few seconds of actual time by using NTP. - This module returns record(s) in the "result" element when 'state' is set to 'present'. This value can be be registered and used in your playbooks. -requirements: [ urllib, urllib2, hashlib, hmac ] +requirements: [ hashlib, hmac ] author: "Brice Burgess (@briceburg)" ''' @@ -113,6 +113,8 @@ # DNSMadeEasy module specific support methods. # +import urllib + IMPORT_ERROR = None try: import json diff --git a/notification/flowdock.py b/notification/flowdock.py index 7c42e58644d..34dad8db375 100644 --- a/notification/flowdock.py +++ b/notification/flowdock.py @@ -85,8 +85,7 @@ choices: ['yes', 'no'] version_added: 1.5.1 -# informational: requirements for nodes -requirements: [ urllib, urllib2 ] +requirements: [ ] ''' EXAMPLES = ''' @@ -104,6 +103,8 @@ tags=tag1,tag2,tag3 ''' +import urllib + # =========================================== # Module execution. # diff --git a/notification/grove.py b/notification/grove.py index 85601d1cc78..4e4a0b5b684 100644 --- a/notification/grove.py +++ b/notification/grove.py @@ -49,6 +49,8 @@ message=deployed {{ target }} ''' +import urllib + BASE_URL = 'https://grove.io/api/notice/%s/' # ============================================================== diff --git a/notification/hipchat.py b/notification/hipchat.py index 2498c11848c..32689965cf9 100644 --- a/notification/hipchat.py +++ b/notification/hipchat.py @@ -62,8 +62,7 @@ version_added: 1.6.0 -# informational: requirements for nodes -requirements: [ urllib, urllib2 ] +requirements: [ ] author: "WAKAYAMA Shirou (@shirou), BOURDEL Paul (@pb8226)" ''' @@ -75,6 +74,8 @@ # HipChat module specific support methods. # +import urllib + DEFAULT_URI = "https://api.hipchat.com/v1" MSG_URI_V1 = "/rooms/message" diff --git a/notification/nexmo.py b/notification/nexmo.py index d0c3d05e65c..89a246c0d90 100644 --- a/notification/nexmo.py +++ b/notification/nexmo.py @@ -71,6 +71,7 @@ msg: "{{ inventory_hostname }} completed" """ +import urllib NEXMO_API = 'https://rest.nexmo.com/sms/json' diff --git a/notification/sendgrid.py b/notification/sendgrid.py index 78806687e0b..7a2ee3ad657 100644 --- a/notification/sendgrid.py +++ b/notification/sendgrid.py @@ -84,10 +84,7 @@ # ======================================= # sendgrid module support methods # -try: - import urllib, urllib2 -except ImportError: - module.fail_json(msg="urllib and urllib2 are required") +import urllib, urllib2 import base64 diff --git a/notification/twilio.py b/notification/twilio.py index e9ec5bcf51e..a2dd77fb2c0 100644 --- a/notification/twilio.py +++ b/notification/twilio.py @@ -104,10 +104,7 @@ # ======================================= # twilio module support methods # -try: - import urllib, urllib2 -except ImportError: - module.fail_json(msg="urllib and urllib2 are required") +import urllib, urllib2 import base64 From 1659af1541648765d955a48be9802703dacc052b Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Fri, 19 Jun 2015 12:05:50 -0400 Subject: [PATCH 0281/2522] made sensu_check 2.4 friendly --- monitoring/sensu_check.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/monitoring/sensu_check.py b/monitoring/sensu_check.py index b968304c34f..eb9d0b7bf04 100644 --- a/monitoring/sensu_check.py +++ b/monitoring/sensu_check.py @@ -183,8 +183,8 @@ def sensu_check(module, path, name, state='present', backup=False): import simplejson as json try: - with open(path) as stream: - config = json.load(stream) + stream = open(path, 'r') + config = json.load(stream.read()) except IOError as e: if e.errno is 2: # File not found, non-fatal if state == 'absent': @@ -196,6 +196,9 @@ def sensu_check(module, path, name, state='present', backup=False): except ValueError: msg = '{path} contains invalid JSON'.format(path=path) module.fail_json(msg=msg) + finally: + if stream: + stream.close() if 'checks' not in config: if state == 'absent': @@ -274,10 +277,13 @@ def sensu_check(module, path, name, state='present', backup=False): if backup: module.backup_local(path) try: - with open(path, 'w') as stream: - stream.write(json.dumps(config, indent=2) + '\n') + stream = open(path, 'w') + stream.write(json.dumps(config, indent=2) + '\n') except IOError as e: module.fail_json(msg=str(e)) + finally: + if stream: + stream.close() return changed, reasons From dd6e8f354aaeeeaccc1566ab14cfd368d6ec1f72 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Fri, 19 Jun 2015 09:07:04 -0700 Subject: [PATCH 0282/2522] Modify a few more modules to not conditionalize urllib(2) import. --- monitoring/librato_annotation.py | 7 +------ notification/sendgrid.py | 3 ++- notification/twilio.py | 3 ++- notification/typetalk.py | 16 +++++----------- 4 files changed, 10 insertions(+), 19 deletions(-) diff --git a/monitoring/librato_annotation.py b/monitoring/librato_annotation.py index 88d3bb81f7b..c606dfdc9a0 100644 --- a/monitoring/librato_annotation.py +++ b/monitoring/librato_annotation.py @@ -31,7 +31,6 @@ version_added: "1.6" author: "Seth Edwards (@sedward)" requirements: - - urllib2 - base64 options: user: @@ -107,11 +106,7 @@ ''' -try: - import urllib2 - HAS_URLLIB2 = True -except ImportError: - HAS_URLLIB2 = False +import urllib2 def post_annotation(module): user = module.params['user'] diff --git a/notification/sendgrid.py b/notification/sendgrid.py index 7a2ee3ad657..e1ae7b7749f 100644 --- a/notification/sendgrid.py +++ b/notification/sendgrid.py @@ -84,7 +84,8 @@ # ======================================= # sendgrid module support methods # -import urllib, urllib2 +import urllib +import urllib2 import base64 diff --git a/notification/twilio.py b/notification/twilio.py index a2dd77fb2c0..ee12d987e9e 100644 --- a/notification/twilio.py +++ b/notification/twilio.py @@ -104,7 +104,8 @@ # ======================================= # twilio module support methods # -import urllib, urllib2 +import urllib +import urllib2 import base64 diff --git a/notification/typetalk.py b/notification/typetalk.py index 638f97ae530..002c8b5cc85 100644 --- a/notification/typetalk.py +++ b/notification/typetalk.py @@ -25,7 +25,7 @@ description: - message body required: true -requirements: [ urllib, urllib2, json ] +requirements: [ json ] author: "Takashi Someda (@tksmd)" ''' @@ -33,15 +33,9 @@ - typetalk: client_id=12345 client_secret=12345 topic=1 msg="install completed" ''' -try: - import urllib -except ImportError: - urllib = None +import urllib -try: - import urllib2 -except ImportError: - urllib2 = None +import urllib2 try: import json @@ -96,8 +90,8 @@ def main(): supports_check_mode=False ) - if not (urllib and urllib2 and json): - module.fail_json(msg="urllib, urllib2 and json modules are required") + if not json: + module.fail_json(msg="json module is required") client_id = module.params["client_id"] client_secret = module.params["client_secret"] From eeb9d3481256b038e69638618f9d3a566e24b6c6 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Fri, 19 Jun 2015 12:10:14 -0400 Subject: [PATCH 0283/2522] also fixed exceptions --- monitoring/sensu_check.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monitoring/sensu_check.py b/monitoring/sensu_check.py index eb9d0b7bf04..5c932a1d303 100644 --- a/monitoring/sensu_check.py +++ b/monitoring/sensu_check.py @@ -185,7 +185,7 @@ def sensu_check(module, path, name, state='present', backup=False): try: stream = open(path, 'r') config = json.load(stream.read()) - except IOError as e: + except IOError, e: if e.errno is 2: # File not found, non-fatal if state == 'absent': reasons.append('file did not exist and state is `absent\'') @@ -279,7 +279,7 @@ def sensu_check(module, path, name, state='present', backup=False): try: stream = open(path, 'w') stream.write(json.dumps(config, indent=2) + '\n') - except IOError as e: + except IOError, e: module.fail_json(msg=str(e)) finally: if stream: From 286bc3d9dc80e2bb3215de823ab5ed6c2a35342c Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Fri, 19 Jun 2015 12:13:43 -0400 Subject: [PATCH 0284/2522] forgot finally 2.4 syntax --- monitoring/sensu_check.py | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/monitoring/sensu_check.py b/monitoring/sensu_check.py index 5c932a1d303..a1bd36ca665 100644 --- a/monitoring/sensu_check.py +++ b/monitoring/sensu_check.py @@ -183,19 +183,20 @@ def sensu_check(module, path, name, state='present', backup=False): import simplejson as json try: - stream = open(path, 'r') - config = json.load(stream.read()) - except IOError, e: - if e.errno is 2: # File not found, non-fatal - if state == 'absent': - reasons.append('file did not exist and state is `absent\'') - return changed, reasons - config = {} - else: - module.fail_json(msg=str(e)) - except ValueError: - msg = '{path} contains invalid JSON'.format(path=path) - module.fail_json(msg=msg) + try: + stream = open(path, 'r') + config = json.load(stream.read()) + except IOError, e: + if e.errno is 2: # File not found, non-fatal + if state == 'absent': + reasons.append('file did not exist and state is `absent\'') + return changed, reasons + config = {} + else: + module.fail_json(msg=str(e)) + except ValueError: + msg = '{path} contains invalid JSON'.format(path=path) + module.fail_json(msg=msg) finally: if stream: stream.close() @@ -277,10 +278,11 @@ def sensu_check(module, path, name, state='present', backup=False): if backup: module.backup_local(path) try: - stream = open(path, 'w') - stream.write(json.dumps(config, indent=2) + '\n') - except IOError, e: - module.fail_json(msg=str(e)) + try: + stream = open(path, 'w') + stream.write(json.dumps(config, indent=2) + '\n') + except IOError, e: + module.fail_json(msg=str(e)) finally: if stream: stream.close() From edc423a18a800ae4b6b30ff6a7dae444a66f10e5 Mon Sep 17 00:00:00 2001 From: Christopher Troup Date: Thu, 18 Jun 2015 18:08:50 -0400 Subject: [PATCH 0285/2522] Add support for creating and deleting Route53 hosted zones Supports both private (per-VPC) and public hosted zones. --- cloud/amazon/route53_zone.py | 148 +++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 cloud/amazon/route53_zone.py diff --git a/cloud/amazon/route53_zone.py b/cloud/amazon/route53_zone.py new file mode 100644 index 00000000000..01eb11eb672 --- /dev/null +++ b/cloud/amazon/route53_zone.py @@ -0,0 +1,148 @@ +#!/usr/bin/python + +DOCUMENTATION = ''' +module: route53_zone +short_description: add or delete Route53 zones +description: + - Creates and deletes Route53 private and public zones +options: + zone: + description: + - The DNS zone record (eg: foo.com.) + required: true + default: null + command: + description: + - whether or not the zone should exist or not + required: false + default: true + vpc_id: + description: + - The VPC ID the zone should be a part of (if this is going to be a private zone) + required: false + default: null + vpc_region: + description: + - The VPC Region the zone should be a part of (if this is going to be a private zone) + required: false + default: null + comment: + description: + - Comment associated with the zone + required: false + default: '' +extends_documentation_fragment: aws +''' + +import time + +try: + import boto + import boto.ec2 + from boto import route53 + from boto.route53 import Route53Connection + from boto.route53.zone import Zone + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + + +def main(): + module = AnsibleModule( + argument_spec=dict( + zone=dict(required=True), + command=dict(default='create', choices=['create', 'delete']), + vpc_id=dict(default=None), + vpc_region=dict(default=None), + comment=dict(default=''), + ) + ) + + if not HAS_BOTO: + module.fail_json(msg='boto required for this module') + + zone_in = module.params.get('zone').lower() + command = module.params.get('command').lower() + vpc_id = module.params.get('vpc_id') + vpc_region = module.params.get('vpc_region') + comment = module.params.get('comment') + + private_zone = vpc_id is not None and vpc_region is not None + + _, _, aws_connect_kwargs = get_aws_connection_info(module) + + # connect to the route53 endpoint + try: + conn = Route53Connection(**aws_connect_kwargs) + except boto.exception.BotoServerError, e: + module.fail_json(msg=e.error_message) + + results = conn.get_all_hosted_zones() + zones = {} + + for r53zone in results['ListHostedZonesResponse']['HostedZones']: + zone_id = r53zone['Id'].replace('/hostedzone/', '') + zone_details = conn.get_hosted_zone(zone_id)['GetHostedZoneResponse'] + if vpc_id and 'VPCs' in zone_details: + # this is to deal with this boto bug: https://github.com/boto/boto/pull/2882 + if isinstance(zone_details['VPCs'], dict): + if zone_details['VPCs']['VPC']['VPCId'] == vpc_id: + zones[r53zone['Name']] = zone_id + else: # Forward compatibility for when boto fixes that bug + if vpc_id in [v['VPCId'] for v in zone_details['VPCs']]: + zones[r53zone['Name']] = zone_id + else: + zones[r53zone['Name']] = zone_id + + record = { + 'private_zone': private_zone, + 'vpc_id': vpc_id, + 'vpc_region': vpc_region, + 'comment': comment, + } + + if command == 'create' and zone_in in zones: + if private_zone: + details = conn.get_hosted_zone(zones[zone_in]) + + if 'VPCs' not in details['GetHostedZoneResponse']: + module.fail_json( + msg="Can't change VPC from public to private" + ) + + vpc_details = details['GetHostedZoneResponse']['VPCs']['VPC'] + current_vpc_id = vpc_details['VPCId'] + current_vpc_region = vpc_details['VPCRegion'] + + if current_vpc_id != vpc_id: + module.fail_json( + msg="Can't change VPC ID once a zone has been created" + ) + if current_vpc_region != vpc_region: + module.fail_json( + msg="Can't change VPC Region once a zone has been created" + ) + + record['zone_id'] = zones[zone_in] + record['name'] = zone_in + module.exit_json(changed=False, set=record) + + elif command == 'create': + result = conn.create_hosted_zone(zone_in, **record) + hosted_zone = result['CreateHostedZoneResponse']['HostedZone'] + zone_id = hosted_zone['Id'].replace('/hostedzone/', '') + record['zone_id'] = zone_id + record['name'] = zone_in + module.exit_json(changed=True, set=record) + + elif command == 'delete' and zone_in in zones: + conn.delete_hosted_zone(zones[zone_in]) + module.exit_json(changed=True) + + elif command == 'delete': + module.exit_json(changed=False) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +main() From 86ae387fdcb7cedf5f658d55e36ddbbf31c59631 Mon Sep 17 00:00:00 2001 From: Christopher Troup Date: Fri, 19 Jun 2015 11:14:27 -0400 Subject: [PATCH 0286/2522] Update documentation to include usual fields - Adds version_added - Adds author - Removed default: null from a required field --- cloud/amazon/route53_zone.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cloud/amazon/route53_zone.py b/cloud/amazon/route53_zone.py index 01eb11eb672..ca9cca8d9f6 100644 --- a/cloud/amazon/route53_zone.py +++ b/cloud/amazon/route53_zone.py @@ -5,12 +5,12 @@ short_description: add or delete Route53 zones description: - Creates and deletes Route53 private and public zones +version_added: "2.0" options: zone: description: - The DNS zone record (eg: foo.com.) required: true - default: null command: description: - whether or not the zone should exist or not @@ -32,6 +32,7 @@ required: false default: '' extends_documentation_fragment: aws +author: "Christopher Troup (@minichate)" ''' import time From 8c643498d37a7c85358a746d6f5467f6d3c34d60 Mon Sep 17 00:00:00 2001 From: Christopher Troup Date: Fri, 19 Jun 2015 11:22:01 -0400 Subject: [PATCH 0287/2522] Use state: present|absent rather than command: create|delete --- cloud/amazon/route53_zone.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/cloud/amazon/route53_zone.py b/cloud/amazon/route53_zone.py index ca9cca8d9f6..2383563fafe 100644 --- a/cloud/amazon/route53_zone.py +++ b/cloud/amazon/route53_zone.py @@ -11,11 +11,12 @@ description: - The DNS zone record (eg: foo.com.) required: true - command: + state: description: - whether or not the zone should exist or not required: false default: true + choices: [ "present", "absent" ] vpc_id: description: - The VPC ID the zone should be a part of (if this is going to be a private zone) @@ -52,7 +53,7 @@ def main(): module = AnsibleModule( argument_spec=dict( zone=dict(required=True), - command=dict(default='create', choices=['create', 'delete']), + state=dict(default='present', choices=['present', 'absent']), vpc_id=dict(default=None), vpc_region=dict(default=None), comment=dict(default=''), @@ -63,7 +64,7 @@ def main(): module.fail_json(msg='boto required for this module') zone_in = module.params.get('zone').lower() - command = module.params.get('command').lower() + state = module.params.get('state').lower() vpc_id = module.params.get('vpc_id') vpc_region = module.params.get('vpc_region') comment = module.params.get('comment') @@ -102,7 +103,7 @@ def main(): 'comment': comment, } - if command == 'create' and zone_in in zones: + if state == 'present' and zone_in in zones: if private_zone: details = conn.get_hosted_zone(zones[zone_in]) @@ -128,7 +129,7 @@ def main(): record['name'] = zone_in module.exit_json(changed=False, set=record) - elif command == 'create': + elif state == 'present': result = conn.create_hosted_zone(zone_in, **record) hosted_zone = result['CreateHostedZoneResponse']['HostedZone'] zone_id = hosted_zone['Id'].replace('/hostedzone/', '') @@ -136,11 +137,11 @@ def main(): record['name'] = zone_in module.exit_json(changed=True, set=record) - elif command == 'delete' and zone_in in zones: + elif state == 'absent' and zone_in in zones: conn.delete_hosted_zone(zones[zone_in]) module.exit_json(changed=True) - elif command == 'delete': + elif state == 'absent': module.exit_json(changed=False) from ansible.module_utils.basic import * From fc43c3a8fd57849133177fd9782e8abe2be467c4 Mon Sep 17 00:00:00 2001 From: Robert Buchholz Date: Wed, 25 Mar 2015 16:14:13 +0100 Subject: [PATCH 0288/2522] patch: Add binary option that maps to --binary to handle CLRF patches --- files/patch.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/files/patch.py b/files/patch.py index c1a61ce733f..22491ef6ac4 100644 --- a/files/patch.py +++ b/files/patch.py @@ -70,6 +70,12 @@ description: - passes --backup --version-control=numbered to patch, producing numbered backup copies + binary: + version_added: "2.0" + description: + - Setting to true will disable patch's heuristic for transforming CRLF + line endings into LF. Line endings of src and dest must match. If set to + False, patch will replace CRLF in src files on POSIX. required: false type: "bool" default: "False" @@ -98,10 +104,12 @@ class PatchError(Exception): pass -def is_already_applied(patch_func, patch_file, basedir, dest_file=None, strip=0): +def is_already_applied(patch_func, patch_file, basedir, dest_file=None, binary=False, strip=0): opts = ['--quiet', '--reverse', '--forward', '--dry-run', "--strip=%s" % strip, "--directory='%s'" % basedir, "--input='%s'" % patch_file] + if binary: + opts.append('--binary') if dest_file: opts.append("'%s'" % dest_file) @@ -109,12 +117,14 @@ def is_already_applied(patch_func, patch_file, basedir, dest_file=None, strip=0) return rc == 0 -def apply_patch(patch_func, patch_file, basedir, dest_file=None, strip=0, dry_run=False, backup=False): +def apply_patch(patch_func, patch_file, basedir, dest_file=None, binary=False, strip=0, dry_run=False, backup=False): opts = ['--quiet', '--forward', '--batch', '--reject-file=-', "--strip=%s" % strip, "--directory='%s'" % basedir, "--input='%s'" % patch_file] if dry_run: opts.append('--dry-run') + if binary: + opts.append('--binary') if dest_file: opts.append("'%s'" % dest_file) if backup: @@ -136,7 +146,8 @@ def main(): 'remote_src': {'default': False, 'type': 'bool'}, # NB: for 'backup' parameter, semantics is slightly different from standard # since patch will create numbered copies, not strftime("%Y-%m-%d@%H:%M:%S~") - 'backup': { 'default': False, 'type': 'bool' } + 'backup': {'default': False, 'type': 'bool'}, + 'binary': {'default': False, 'type': 'bool'}, }, required_one_of=[['dest', 'basedir']], supports_check_mode=True @@ -167,9 +178,9 @@ def main(): p.src = os.path.abspath(p.src) changed = False - if not is_already_applied(patch_func, p.src, p.basedir, dest_file=p.dest, strip=p.strip): + if not is_already_applied(patch_func, p.src, p.basedir, dest_file=p.dest, binary=p.binary, strip=p.strip): try: - apply_patch( patch_func, p.src, p.basedir, dest_file=p.dest, strip=p.strip, + apply_patch( patch_func, p.src, p.basedir, dest_file=p.dest, binary=p.binary, strip=p.strip, dry_run=module.check_mode, backup=p.backup ) changed = True except PatchError, e: From 268104fca321a777e279ed20d252e43da23a2b9a Mon Sep 17 00:00:00 2001 From: Alan Loi Date: Sat, 20 Jun 2015 21:24:36 +1000 Subject: [PATCH 0289/2522] Added check_mode support to dynamodb_table module. --- cloud/amazon/dynamodb_table | 51 ++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/cloud/amazon/dynamodb_table b/cloud/amazon/dynamodb_table index 7a200a3b271..b59280a2e23 100644 --- a/cloud/amazon/dynamodb_table +++ b/cloud/amazon/dynamodb_table @@ -39,7 +39,7 @@ options: hash_key_name: description: - Name of the hash key. - - Required when state=present. + - Required when C(state=present). required: false hash_key_type: description: @@ -109,10 +109,10 @@ try: from boto.dynamodb2.fields import HashKey, RangeKey from boto.dynamodb2.types import STRING, NUMBER, BINARY from boto.exception import BotoServerError, JSONResponseError + HAS_BOTO = True except ImportError: - print "failed=True msg='boto required for this module'" - sys.exit(1) + HAS_BOTO = False DYNAMO_TYPE_MAP = { @@ -132,8 +132,8 @@ def create_or_update_dynamo_table(connection, module): write_capacity = module.params.get('write_capacity') schema = [ - HashKey(hash_key_name, map_dynamo_type(hash_key_type)), - RangeKey(range_key_name, map_dynamo_type(range_key_type)) + HashKey(hash_key_name, DYNAMO_TYPE_MAP.get(hash_key_type)), + RangeKey(range_key_name, DYNAMO_TYPE_MAP.get(range_key_type)) ] throughput = { 'read': read_capacity, @@ -155,13 +155,14 @@ def create_or_update_dynamo_table(connection, module): table = Table(table_name, connection=connection) if dynamo_table_exists(table): - changed = update_dynamo_table(table, throughput=throughput) + result['changed'] = update_dynamo_table(table, throughput=throughput, check_mode=module.check_mode) else: - Table.create(table_name, connection=connection, schema=schema, throughput=throughput) - changed = True + if not module.check_mode: + Table.create(table_name, connection=connection, schema=schema, throughput=throughput) + result['changed'] = True - result['table_status'] = table.describe()['Table']['TableStatus'] - result['changed'] = changed + if not module.check_mode: + result['table_status'] = table.describe()['Table']['TableStatus'] except BotoServerError: result['msg'] = 'Failed to create/update dynamo table due to error: ' + traceback.format_exc() @@ -171,7 +172,7 @@ def create_or_update_dynamo_table(connection, module): def delete_dynamo_table(connection, module): - table_name = module.params.get('table_name') + table_name = module.params.get('name') result = dict( region=module.params.get('region'), @@ -179,14 +180,15 @@ def delete_dynamo_table(connection, module): ) try: - changed = False table = Table(table_name, connection=connection) if dynamo_table_exists(table): - table.delete() - changed = True + if not module.check_mode: + table.delete() + result['changed'] = True - result['changed'] = changed + else: + result['changed'] = False except BotoServerError: result['msg'] = 'Failed to delete dynamo table due to error: ' + traceback.format_exc() @@ -207,12 +209,14 @@ def dynamo_table_exists(table): raise e -def update_dynamo_table(table, throughput=None): +def update_dynamo_table(table, throughput=None, check_mode=False): table.describe() # populate table details - # AWS complains if the throughput hasn't changed if has_throughput_changed(table, throughput): - return table.update(throughput=throughput) + if not check_mode: + return table.update(throughput=throughput) + else: + return True return False @@ -225,10 +229,6 @@ def has_throughput_changed(table, new_throughput): new_throughput['write'] != table.throughput['write'] -def map_dynamo_type(dynamo_type): - return DYNAMO_TYPE_MAP.get(dynamo_type) - - def main(): argument_spec = ec2_argument_spec() argument_spec.update(dict( @@ -242,7 +242,12 @@ def main(): write_capacity=dict(default=1, type='int'), )) - module = AnsibleModule(argument_spec=argument_spec) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True) + + if not HAS_BOTO: + module.fail_json(msg='boto required for this module') region, ec2_url, aws_connect_params = get_aws_connection_info(module) connection = boto.dynamodb2.connect_to_region(region) From 011fef5f3275b5a1cf55a9c578c61d2dde0d3f99 Mon Sep 17 00:00:00 2001 From: Alan Loi Date: Sat, 20 Jun 2015 21:34:27 +1000 Subject: [PATCH 0290/2522] Added return value documentation to dynamodb_table module. --- cloud/amazon/dynamodb_table | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cloud/amazon/dynamodb_table b/cloud/amazon/dynamodb_table index b59280a2e23..89a7e0fbb2e 100644 --- a/cloud/amazon/dynamodb_table +++ b/cloud/amazon/dynamodb_table @@ -102,6 +102,14 @@ EXAMPLES = ''' state: absent ''' +RETURN = ''' +table_status: + description: The current status of the table. + returned: success + type: string + sample: ACTIVE +''' + try: import boto import boto.dynamodb2 From 2ddb8807bf4f2c4106bd2944391e5cea76d6acd5 Mon Sep 17 00:00:00 2001 From: Alan Loi Date: Sat, 20 Jun 2015 22:22:30 +1000 Subject: [PATCH 0291/2522] Add new amazon sqs module. --- cloud/amazon/sqs | 221 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 cloud/amazon/sqs diff --git a/cloud/amazon/sqs b/cloud/amazon/sqs new file mode 100644 index 00000000000..c30a840a22b --- /dev/null +++ b/cloud/amazon/sqs @@ -0,0 +1,221 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = """ +--- +module: sqs +short_description: Creates or deletes AWS SQS queues. +description: + - Create or delete AWS SQS queues. + - Update attributes on existing queues. +author: Alan Loi (@loia) +requirements: + - "boto >= 2.33.0" +options: + state: + description: + - Create or delete the queue + required: true + choices: ['present', 'absent'] + default: 'present' + name: + description: + - Name of the queue. + required: true + default_visibility_timeout: + description: + - The default visibility timeout in seconds. + required: false + message_retention_period: + description: + - The message retention period in seconds. + required: false + maximum_message_size: + description: + - The maximum message size in bytes. + required: false + delivery_delay: + description: + - The delivery delay in seconds. + required: false + receive_message_wait_time: + description: + - The receive message wait time in seconds. + required: false + region: + description: + - The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used. + required: false + aliases: ['aws_region', 'ec2_region'] + +extends_documentation_fragment: aws +""" + +EXAMPLES = ''' +# Create SQS queue +- sqs: + name: my-queue + region: ap-southeast-2 + default_visibility_timeout: 120 + message_retention_period: 86400 + maximum_message_size: 1024 + delivery_delay: 30 + receive_message_wait_time: 20 + +# Delete SQS queue +- sqs: + name: my-queue + region: ap-southeast-2 + state: absent +''' + +try: + import boto.sqs + from boto.exception import BotoServerError + HAS_BOTO = True + +except ImportError: + HAS_BOTO = False + + +def create_or_update_sqs_queue(connection, module): + queue_name = module.params.get('name') + + queue_attributes = dict( + default_visibility_timeout=module.params.get('default_visibility_timeout'), + message_retention_period=module.params.get('message_retention_period'), + maximum_message_size=module.params.get('maximum_message_size'), + delivery_delay=module.params.get('delivery_delay'), + receive_message_wait_time=module.params.get('receive_message_wait_time'), + ) + + result = dict( + region=module.params.get('region'), + name=queue_name, + ) + result.update(queue_attributes) + + try: + queue = connection.get_queue(queue_name) + if queue: + # Update existing + result['changed'] = update_sqs_queue(queue, check_mode=module.check_mode, **queue_attributes) + + else: + # Create new + if not module.check_mode: + queue = connection.create_queue(queue_name) + update_sqs_queue(queue, **queue_attributes) + result['changed'] = True + + except BotoServerError: + result['msg'] = 'Failed to create/update sqs queue due to error: ' + traceback.format_exc() + module.fail_json(**result) + else: + module.exit_json(**result) + + +def update_sqs_queue(queue, + check_mode=False, + default_visibility_timeout=None, + message_retention_period=None, + maximum_message_size=None, + delivery_delay=None, + receive_message_wait_time=None): + changed = False + + changed = set_queue_attribute(queue, 'VisibilityTimeout', default_visibility_timeout, + check_mode=check_mode) or changed + changed = set_queue_attribute(queue, 'MessageRetentionPeriod', message_retention_period, + check_mode=check_mode) or changed + changed = set_queue_attribute(queue, 'MaximumMessageSize', maximum_message_size, + check_mode=check_mode) or changed + changed = set_queue_attribute(queue, 'DelaySeconds', delivery_delay, + check_mode=check_mode) or changed + changed = set_queue_attribute(queue, 'ReceiveMessageWaitTimeSeconds', receive_message_wait_time, + check_mode=check_mode) or changed + return changed + + +def set_queue_attribute(queue, attribute, value, check_mode=False): + if not value: + return False + + existing_value = queue.get_attributes(attributes=attribute)[attribute] + if str(value) != existing_value: + if not check_mode: + queue.set_attribute(attribute, value) + return True + + return False + + +def delete_sqs_queue(connection, module): + queue_name = module.params.get('name') + + result = dict( + region=module.params.get('region'), + name=queue_name, + ) + + try: + queue = connection.get_queue(queue_name) + if queue: + if not module.check_mode: + connection.delete_queue(queue) + result['changed'] = True + + else: + result['changed'] = False + + except BotoServerError: + result['msg'] = 'Failed to delete sqs queue due to error: ' + traceback.format_exc() + module.fail_json(**result) + else: + module.exit_json(**result) + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + state=dict(default='present', choices=['present', 'absent']), + name=dict(required=True, type='str'), + default_visibility_timeout=dict(type='int'), + message_retention_period=dict(type='int'), + maximum_message_size=dict(type='int'), + delivery_delay=dict(type='int'), + receive_message_wait_time=dict(type='int'), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True) + + region, ec2_url, aws_connect_params = get_aws_connection_info(module) + connection = boto.sqs.connect_to_region(region) + + state = module.params.get('state') + if state == 'present': + create_or_update_sqs_queue(connection, module) + elif state == 'absent': + delete_sqs_queue(connection, module) + + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +main() From 3f74ec35e667b004bb21751b8cb731fd0c7677c6 Mon Sep 17 00:00:00 2001 From: Alan Loi Date: Sun, 21 Jun 2015 08:39:13 +1000 Subject: [PATCH 0292/2522] Add .py file extension to sqs module. --- cloud/amazon/{sqs => sqs.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename cloud/amazon/{sqs => sqs.py} (100%) diff --git a/cloud/amazon/sqs b/cloud/amazon/sqs.py similarity index 100% rename from cloud/amazon/sqs rename to cloud/amazon/sqs.py From ac09e609146c3f8c8ef46dc22ab75834aa5d20dc Mon Sep 17 00:00:00 2001 From: Alan Loi Date: Sun, 21 Jun 2015 08:40:57 +1000 Subject: [PATCH 0293/2522] Add .py file extension to dynamodb_table module. --- cloud/amazon/{dynamodb_table => dynamodb_table.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename cloud/amazon/{dynamodb_table => dynamodb_table.py} (100%) diff --git a/cloud/amazon/dynamodb_table b/cloud/amazon/dynamodb_table.py similarity index 100% rename from cloud/amazon/dynamodb_table rename to cloud/amazon/dynamodb_table.py From 44423e4a650d5b5f65640ecc451ad6a709efcb13 Mon Sep 17 00:00:00 2001 From: Sebastien ROHAUT Date: Sun, 21 Jun 2015 18:52:59 +0200 Subject: [PATCH 0294/2522] Update pam_limits.py Add version 2.0 Remove default: from documentation for required values use atomic_move from ansible module API --- system/pam_limits.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/system/pam_limits.py b/system/pam_limits.py index 649b9a175f7..ab429bb8808 100644 --- a/system/pam_limits.py +++ b/system/pam_limits.py @@ -1,3 +1,4 @@ +#!/usr/bin/python # -*- coding: utf-8 -*- # (c) 2014, Sebastien Rohaut @@ -25,7 +26,7 @@ DOCUMENTATION = ''' --- module: pam_limits -version_added: "historical" +version_added: "2.0" short_description: Modify Linux PAM limits description: - The M(pam_limits) module modify PAM limits, default in /etc/security/limits.conf. @@ -35,24 +36,20 @@ description: - A username, @groupname, wildcard, uid/gid range. required: true - default: null limit_type: description: - Limit type : hard or soft. required: true choices: [ "hard", "soft" ] - default: null limit_item: description: - The limit to be set : core, data, nofile, cpu, etc. required: true choices: [ "core", "data", "fsize", "memlock", "nofile", "rss", "stack", "cpu", "nproc", "as", "maxlogins", "maxsyslogins", "priority", "locks", "sigpending", "msgqueue", "nice", "rtprio", "chroot" ] - default: null value: description: - The value of the limit. required: true - default: null backup: description: - Create a backup file including the timestamp information so you can get @@ -222,10 +219,7 @@ def main(): nf.close() # Copy tempfile to newfile - shutil.copy(nf.name, f.name) - - # delete tempfile - os.unlink(nf.name) + module.atomic_move(nf.name, f.name) res_args = dict( changed = changed, msg = message From 75e1e9fcda109b223487de752356066035059ae7 Mon Sep 17 00:00:00 2001 From: Eike Frost Date: Tue, 28 Apr 2015 19:41:54 +0200 Subject: [PATCH 0295/2522] add zabbix proxy support to zabbix_host --- monitoring/zabbix_host.py | 49 ++++++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/monitoring/zabbix_host.py b/monitoring/zabbix_host.py index 772e92cb32d..6fac82c7177 100644 --- a/monitoring/zabbix_host.py +++ b/monitoring/zabbix_host.py @@ -79,6 +79,10 @@ description: - The timeout of API request (seconds). default: 10 + proxy: + description: + - The name of the Zabbix Proxy to be used + default: None interfaces: description: - List of interfaces to be created for the host (see example below). @@ -118,6 +122,7 @@ ip: 10.xx.xx.xx dns: "" port: 12345 + proxy: a.zabbix.proxy ''' import logging @@ -174,21 +179,25 @@ def get_template_ids(self, template_list): template_ids.append(template_id) return template_ids - def add_host(self, host_name, group_ids, status, interfaces): + def add_host(self, host_name, group_ids, status, interfaces, proxy_id): try: if self._module.check_mode: self._module.exit_json(changed=True) - host_list = self._zapi.host.create({'host': host_name, 'interfaces': interfaces, 'groups': group_ids, 'status': status}) + parameters = {'host': host_name, 'interfaces': interfaces, 'groups': group_ids, 'status': status} + if proxy_id: + parameters['proxy_hostid'] = proxy_id + host_list = self._zapi.host.create(parameters) if len(host_list) >= 1: return host_list['hostids'][0] except Exception, e: self._module.fail_json(msg="Failed to create host %s: %s" % (host_name, e)) - def update_host(self, host_name, group_ids, status, host_id, interfaces, exist_interface_list): + def update_host(self, host_name, group_ids, status, host_id, interfaces, exist_interface_list, proxy_id): try: if self._module.check_mode: self._module.exit_json(changed=True) - self._zapi.host.update({'hostid': host_id, 'groups': group_ids, 'status': status}) + parameters = {'hostid': host_id, 'groups': group_ids, 'status': status, 'proxy_hostid': proxy_id} + self._zapi.host.update(parameters) interface_list_copy = exist_interface_list if interfaces: for interface in interfaces: @@ -234,6 +243,14 @@ def get_host_by_host_name(self, host_name): else: return host_list[0] + # get proxyid by proxy name + def get_proxyid_by_proxy_name(self, proxy_name): + proxy_list = self._zapi.proxy.get({'output': 'extend', 'filter': {'host': [proxy_name]}}) + if len(proxy_list) < 1: + self._module.fail_json(msg="Proxy not found: %s" % proxy_name) + else: + return proxy_list[0]['proxyid'] + # get group ids by group names def get_group_ids_by_group_names(self, group_names): group_ids = [] @@ -294,7 +311,7 @@ def get_host_status_by_host(self, host): # check all the properties before link or clear template def check_all_properties(self, host_id, host_groups, status, interfaces, template_ids, - exist_interfaces, host): + exist_interfaces, host, proxy_id): # get the existing host's groups exist_host_groups = self.get_host_groups_by_host_id(host_id) if set(host_groups) != set(exist_host_groups): @@ -314,6 +331,9 @@ def check_all_properties(self, host_id, host_groups, status, interfaces, templat if set(list(template_ids)) != set(exist_template_ids): return True + if host['proxy_hostid'] != proxy_id: + return True + return False # link or clear template of the host @@ -349,7 +369,8 @@ def main(): status=dict(default="enabled", choices=['enabled', 'disabled']), state=dict(default="present", choices=['present', 'absent']), timeout=dict(type='int', default=10), - interfaces=dict(required=False) + interfaces=dict(required=False), + proxy=dict(required=False) ), supports_check_mode=True ) @@ -367,6 +388,7 @@ def main(): state = module.params['state'] timeout = module.params['timeout'] interfaces = module.params['interfaces'] + proxy = module.params['proxy'] # convert enabled to 0; disabled to 1 status = 1 if status == "disabled" else 0 @@ -396,6 +418,11 @@ def main(): if interface['type'] == 1: ip = interface['ip'] + proxy_id = "0" + + if proxy: + proxy_id = host.get_proxyid_by_proxy_name(proxy) + # check if host exist is_host_exist = host.is_host_exist(host_name) @@ -421,10 +448,10 @@ def main(): if len(exist_interfaces) > interfaces_len: if host.check_all_properties(host_id, host_groups, status, interfaces, template_ids, - exist_interfaces, zabbix_host_obj): + exist_interfaces, zabbix_host_obj, proxy_id): host.link_or_clear_template(host_id, template_ids) host.update_host(host_name, group_ids, status, host_id, - interfaces, exist_interfaces) + interfaces, exist_interfaces, proxy_id) module.exit_json(changed=True, result="Successfully update host %s (%s) and linked with template '%s'" % (host_name, ip, link_templates)) @@ -432,8 +459,8 @@ def main(): module.exit_json(changed=False) else: if host.check_all_properties(host_id, host_groups, status, interfaces, template_ids, - exist_interfaces_copy, zabbix_host_obj): - host.update_host(host_name, group_ids, status, host_id, interfaces, exist_interfaces) + exist_interfaces_copy, zabbix_host_obj, proxy_id): + host.update_host(host_name, group_ids, status, host_id, interfaces, exist_interfaces, proxy_id) host.link_or_clear_template(host_id, template_ids) module.exit_json(changed=True, result="Successfully update host %s (%s) and linked with template '%s'" @@ -448,7 +475,7 @@ def main(): module.fail_json(msg="Specify at least one interface for creating host '%s'." % host_name) # create host - host_id = host.add_host(host_name, group_ids, status, interfaces) + host_id = host.add_host(host_name, group_ids, status, interfaces, proxy_id) host.link_or_clear_template(host_id, template_ids) module.exit_json(changed=True, result="Successfully added host %s (%s) and linked with template '%s'" % ( host_name, ip, link_templates)) From 4bf188258d9fa2f0217afdda5982d17987836a7e Mon Sep 17 00:00:00 2001 From: Alan Loi Date: Mon, 22 Jun 2015 19:56:09 +1000 Subject: [PATCH 0296/2522] Rename module to sqs_queue.py to differentiate from other potential modules e.g. reading/writing messages. --- cloud/amazon/{sqs.py => sqs_queue.py} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename cloud/amazon/{sqs.py => sqs_queue.py} (99%) diff --git a/cloud/amazon/sqs.py b/cloud/amazon/sqs_queue.py similarity index 99% rename from cloud/amazon/sqs.py rename to cloud/amazon/sqs_queue.py index c30a840a22b..6ad8d2d1999 100644 --- a/cloud/amazon/sqs.py +++ b/cloud/amazon/sqs_queue.py @@ -16,7 +16,7 @@ DOCUMENTATION = """ --- -module: sqs +module: sqs_queue short_description: Creates or deletes AWS SQS queues. description: - Create or delete AWS SQS queues. @@ -66,7 +66,7 @@ EXAMPLES = ''' # Create SQS queue -- sqs: +- sqs_queue: name: my-queue region: ap-southeast-2 default_visibility_timeout: 120 @@ -76,7 +76,7 @@ receive_message_wait_time: 20 # Delete SQS queue -- sqs: +- sqs_queue: name: my-queue region: ap-southeast-2 state: absent From 1a914128f6d172da7ea349d6b070758e1ebbff9c Mon Sep 17 00:00:00 2001 From: Alan Loi Date: Mon, 22 Jun 2015 20:23:11 +1000 Subject: [PATCH 0297/2522] Fix aws connection to use params. --- cloud/amazon/dynamodb_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/dynamodb_table.py b/cloud/amazon/dynamodb_table.py index 89a7e0fbb2e..130fae44721 100644 --- a/cloud/amazon/dynamodb_table.py +++ b/cloud/amazon/dynamodb_table.py @@ -258,7 +258,7 @@ def main(): module.fail_json(msg='boto required for this module') region, ec2_url, aws_connect_params = get_aws_connection_info(module) - connection = boto.dynamodb2.connect_to_region(region) + connection = connect_to_aws(boto.dynamodb2, region, **aws_connect_params) state = module.params.get('state') if state == 'present': From 92744ef5581d108eba3e17d539fc810de2a36e5f Mon Sep 17 00:00:00 2001 From: Phil Date: Mon, 22 Jun 2015 08:55:41 -0500 Subject: [PATCH 0298/2522] fixes typo --- windows/win_unzip.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_unzip.ps1 b/windows/win_unzip.ps1 index 35a55c811c4..51b092f4665 100644 --- a/windows/win_unzip.ps1 +++ b/windows/win_unzip.ps1 @@ -89,7 +89,7 @@ Else { Fail-Json "PowerShellCommunityExtensions PowerShell Module (PSCX) is required for non-'.zip' compressed archive types." } Else { - Set-Attr $result.win_zip "pscx_status" "present" + Set-Attr $result.win_unzip "pscx_status" "present" } # Import From 0ad12cdcf4e5d4ed90b506917ee5083b1910b0e2 Mon Sep 17 00:00:00 2001 From: Gerrit Germis Date: Mon, 22 Jun 2015 20:09:54 +0200 Subject: [PATCH 0299/2522] specify int parameter types for wait_interval and wait_retries --- network/haproxy.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/network/haproxy.py b/network/haproxy.py index 690aa60bbba..cd17d057b5f 100644 --- a/network/haproxy.py +++ b/network/haproxy.py @@ -78,13 +78,13 @@ description: - number of times to check for status after changing the state required: false - default: 20 + default: 25 version_added: "2.0" wait_interval: description: - number of seconds to wait between retries required: false - default: 1 + default: 5 version_added: "2.0" ''' @@ -129,7 +129,7 @@ DEFAULT_SOCKET_LOCATION="/var/run/haproxy.sock" RECV_SIZE = 1024 ACTION_CHOICES = ['enabled', 'disabled'] -WAIT_RETRIES=20 +WAIT_RETRIES=25 WAIT_INTERVAL=1 ###################################################################### @@ -302,9 +302,9 @@ def main(): weight=dict(required=False, default=None), socket = dict(required=False, default=DEFAULT_SOCKET_LOCATION), shutdown_sessions=dict(required=False, default=False), - wait=dict(required=False, default=False), - wait_retries=dict(required=False, default=WAIT_RETRIES), - wait_interval=dict(required=False, default=WAIT_INTERVAL), + wait=dict(required=False, default=False, type='bool'), + wait_retries=dict(required=False, default=WAIT_RETRIES, type='int'), + wait_interval=dict(required=False, default=WAIT_INTERVAL, type='int'), ), ) From 2612da50ad637bb469431df699c82b5f68d255e6 Mon Sep 17 00:00:00 2001 From: Gerrit Germis Date: Mon, 22 Jun 2015 20:13:12 +0200 Subject: [PATCH 0300/2522] wait_interval default value did not match the documented value --- network/haproxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/haproxy.py b/network/haproxy.py index cd17d057b5f..6d4f6a4279a 100644 --- a/network/haproxy.py +++ b/network/haproxy.py @@ -130,7 +130,7 @@ RECV_SIZE = 1024 ACTION_CHOICES = ['enabled', 'disabled'] WAIT_RETRIES=25 -WAIT_INTERVAL=1 +WAIT_INTERVAL=5 ###################################################################### class TimeoutException(Exception): From 03ce40a62ebb1d8ceb8d2f6f7bebb1b4b90458c0 Mon Sep 17 00:00:00 2001 From: Phil Date: Mon, 22 Jun 2015 18:51:58 -0500 Subject: [PATCH 0301/2522] removes restart functionality, and added creates param for idempotency --- windows/win_unzip.ps1 | 18 ++++++++++-------- windows/win_unzip.py | 39 +++++---------------------------------- 2 files changed, 15 insertions(+), 42 deletions(-) diff --git a/windows/win_unzip.ps1 b/windows/win_unzip.ps1 index 51b092f4665..e4509a290a2 100644 --- a/windows/win_unzip.ps1 +++ b/windows/win_unzip.ps1 @@ -26,6 +26,13 @@ $result = New-Object psobject @{ changed = $false } +If ($params.creates) { + If (Test-Path $params.creates) { + Exit-Json $result "The 'creates' file or directory already exists." + } + +} + If ($params.src) { $src = $params.src.toString() @@ -86,7 +93,7 @@ Else { $list = Get-Module -ListAvailable If (-Not ($list -match "PSCX")) { - Fail-Json "PowerShellCommunityExtensions PowerShell Module (PSCX) is required for non-'.zip' compressed archive types." + Fail-Json $result "PowerShellCommunityExtensions PowerShell Module (PSCX) is required for non-'.zip' compressed archive types." } Else { Set-Attr $result.win_unzip "pscx_status" "present" @@ -122,10 +129,10 @@ Else { } Catch { If ($recurse) { - Fail-Json "Error recursively expanding $src to $dest" + Fail-Json $result "Error recursively expanding $src to $dest" } Else { - Fail-Json "Error expanding $src to $dest" + Fail-Json $result "Error expanding $src to $dest" } } } @@ -135,11 +142,6 @@ If ($rm -eq $true){ Set-Attr $result.win_unzip "rm" "true" } -If ($params.restart -eq "true" -Or $params.restart -eq "yes") { - Restart-Computer -Force - Set-Attr $result.win_unzip "restart" "true" -} - # Fixes a fail error message (when the task actually succeeds) for a "Convert-ToJson: The converted JSON string is in bad format" # This happens when JSON is parsing a string that ends with a "\", which is possible when specifying a directory to download to. # This catches that possible error, before assigning the JSON $result diff --git a/windows/win_unzip.py b/windows/win_unzip.py index 2c3c41df0b7..7c5ac322b97 100644 --- a/windows/win_unzip.py +++ b/windows/win_unzip.py @@ -63,16 +63,11 @@ - yes - no aliases: [] - restart: + creates: description: - - Restarts the computer after unzip, can be useful for hotfixes such as http://support.microsoft.com/kb/2842230 (Restarts will have to be accounted for with wait_for module) - choices: - - true - - false - - yes - - no - required: false - default: false + - If this file or directory exists the specified src will not be extracted. + required: no + default: null aliases: [] author: Phil Schwartz ''' @@ -88,6 +83,7 @@ win_unzip: src: "C:\Users\Phil\Logs.bz2" dest: "C:\Users\Phil\OldLogs" + creates: "C:\Users\Phil\OldLogs" # This playbook example unzips a .zip file and recursively decompresses the contained .gz files and removes all unneeded compressed files after completion. --- @@ -102,31 +98,6 @@ recurse: yes rm: true -# Install hotfix (self-extracting .exe) ---- -- name: Install WinRM PowerShell Hotfix for Windows Server 2008 SP1 - hosts: all - gather_facts: false - tasks: - - name: Grab Hotfix from URL - win_get_url: - url: 'http://hotfixv4.microsoft.com/Windows%207/Windows%20Server2008%20R2%20SP1/sp2/Fix467402/7600/free/463984_intl_x64_zip.exe' - dest: 'C:\\463984_intl_x64_zip.exe' - - name: Unzip hotfix - win_unzip: - src: "C:\\463984_intl_x64_zip.exe" - dest: "C:\\Hotfix" - recurse: true - restart: true - - name: Wait for server reboot... - local_action: - module: wait_for - host={{ inventory_hostname }} - port={{ansible_ssh_port|default(5986)}} - delay=15 - timeout=600 - state=started - # Install PSCX to use for extracting a gz file - name: Grab PSCX msi win_get_url: From 6b41b80b09946d6a8d3ee401e56da70e408a237b Mon Sep 17 00:00:00 2001 From: Phil Date: Mon, 22 Jun 2015 09:40:49 -0500 Subject: [PATCH 0302/2522] init commit win_timezone --- windows/win_timezone.ps1 | 70 ++++++++++++++++++++++++++++++++++++++++ windows/win_timezone.py | 47 +++++++++++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 windows/win_timezone.ps1 create mode 100644 windows/win_timezone.py diff --git a/windows/win_timezone.ps1 b/windows/win_timezone.ps1 new file mode 100644 index 00000000000..4626dd10b54 --- /dev/null +++ b/windows/win_timezone.ps1 @@ -0,0 +1,70 @@ +#!powershell +# This file is part of Ansible +# +# Copyright 2015, Phil Schwartz +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# WANT_JSON +# POWERSHELL_COMMON + +$params = Parse-Args $args; + +$result = New-Object psobject @{ + win_timezone = New-Object psobject + changed = $false +} + +If ($params.timezone) { + Try { + # Get the current timezone set + $currentTZ = $(C:\Windows\System32\tzutil /g) + + If ( $currentTZ -eq $params.timezone ) { + Exit-Json $result "$params.timezone is already set on this machine" + } + Else { + $tzExists = $false + #Check that timezone can even be set (if it is listed from tzutil as an available timezone to the machine) + $tzList = $(C:\Windows\System32\tzutil /l) + ForEach ($tz in $tzList) { + If ( $tz -eq $params.timezone ) { + $tzExists = $true + break + } + } + + If ( $tzExists ) { + C:\Windows\System32\tzutil /s "$params.timezone" + $newTZ = $(C:\Windows\System32\tzutil /g) + + If ( $params.timezone -eq $newTZ ) { + $result.changed = $true + } + } + Else { + Fail-Json $result "The specified timezone: $params.timezone isn't supported on the machine." + } + } + } + Catch { + Fail-Json $result "Error setting timezone to: $params.timezone." + } +} +Else { + Fail-Json $result "Parameter: timezone is required." +} + + +Exit-Json $result; \ No newline at end of file diff --git a/windows/win_timezone.py b/windows/win_timezone.py new file mode 100644 index 00000000000..abe52be1680 --- /dev/null +++ b/windows/win_timezone.py @@ -0,0 +1,47 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Phil Schwartz +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# this is a windows documentation stub. actual code lives in the .ps1 +# file of the same name + +DOCUMENTATION = ''' +--- +module: win_timezone +version_added: "2.0" +short_description: Sets Windows machine timezone +description: + - Sets machine time to the specified timezone, the module will check if the provided timezone is supported on the machine. +options: + timezone: + description: + - Timezone to set to. Example: Central Standard Time + required: true + default: null + aliases: [] + +author: Phil Schwartz +''' + + +EXAMPLES = ''' + # Set machine's timezone to Central Standard Time + win_timezone: + timezone: "Central Standard Time" +''' From d8063b913ee49f03236c30a3d90b6e106c949f3f Mon Sep 17 00:00:00 2001 From: jpic Date: Tue, 23 Jun 2015 19:36:43 +0200 Subject: [PATCH 0303/2522] Define HAS_LXC even if import lxc doesn't fail. This fixes:: Traceback (most recent call last): File "/home/jpic/.ansible/tmp/ansible-tmp-1435080800.61-38257321141340/lxc_container", line 3353, in main() File "/home/jpic/.ansible/tmp/ansible-tmp-1435080800.61-38257321141340/lxc_container", line 1712, in main if not HAS_LXC: NameError: global name 'HAS_LXC' is not defined --- cloud/lxc/lxc_container.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index e6d70f4e487..2264a86c40c 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -385,6 +385,8 @@ import lxc except ImportError: HAS_LXC = False +else: + HAS_LXC = True # LXC_COMPRESSION_MAP is a map of available compression types when creating From c4d24721483af1e347b7408c8d19cf1617a6a91f Mon Sep 17 00:00:00 2001 From: jpic Date: Tue, 23 Jun 2015 19:38:51 +0200 Subject: [PATCH 0304/2522] Fixed lxc option parsing. This fixes:: Traceback (most recent call last): File "/home/jpic/.ansible/tmp/ansible-tmp-1435080916.98-133068627776311/lxc_container", line 3355, in main() File "/home/jpic/.ansible/tmp/ansible-tmp-1435080916.98-133068627776311/lxc_container", line 1724, in main lxc_manage.run() File "/home/jpic/.ansible/tmp/ansible-tmp-1435080916.98-133068627776311/lxc_container", line 1605, in run action() File "/home/jpic/.ansible/tmp/ansible-tmp-1435080916.98-133068627776311/lxc_container", line 1145, in _started self._config() File "/home/jpic/.ansible/tmp/ansible-tmp-1435080916.98-133068627776311/lxc_container", line 714, in _config _, _value = option_line.split('=') ValueError: too many values to unpack With such a task:: tasks: - lxc_container: name: buildbot-master container_config: - "lxc.mount.entry = {{ cwd }} srv/peopletest none defaults,bind,uid=0,create=dir 0 0" --- cloud/lxc/lxc_container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index e6d70f4e487..090d4f73c97 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -708,7 +708,7 @@ def _config(self): for option_line in container_config: # Look for key in config if option_line.startswith(key): - _, _value = option_line.split('=') + _, _value = option_line.split('=', 1) config_value = ' '.join(_value.split()) line_index = container_config.index(option_line) # If the sanitized values don't match replace them From ebe1904e59aaa9a459c3993bce6a499dc5bd9b73 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 23 Jun 2015 14:12:07 -0500 Subject: [PATCH 0305/2522] Add missing __init__.py --- cloud/rackspace/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 cloud/rackspace/__init__.py diff --git a/cloud/rackspace/__init__.py b/cloud/rackspace/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From d5d84288ae0abba26cb8f66ae0ef9f2db07f306c Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 23 Jun 2015 14:12:17 -0500 Subject: [PATCH 0306/2522] Bump version_added to 2.0 --- cloud/rackspace/rax_mon_alarm.py | 2 +- cloud/rackspace/rax_mon_check.py | 2 +- cloud/rackspace/rax_mon_entity.py | 2 +- cloud/rackspace/rax_mon_notification.py | 2 +- cloud/rackspace/rax_mon_notification_plan.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cloud/rackspace/rax_mon_alarm.py b/cloud/rackspace/rax_mon_alarm.py index f9b97bc8dd1..a3f29e22f50 100644 --- a/cloud/rackspace/rax_mon_alarm.py +++ b/cloud/rackspace/rax_mon_alarm.py @@ -27,7 +27,7 @@ notifications. Rackspace monitoring module flow | rax_mon_entity -> rax_mon_check -> rax_mon_notification -> rax_mon_notification_plan -> *rax_mon_alarm* -version_added: "1.9" +version_added: "2.0" options: state: description: diff --git a/cloud/rackspace/rax_mon_check.py b/cloud/rackspace/rax_mon_check.py index 101efd3c858..14b86864e2f 100644 --- a/cloud/rackspace/rax_mon_check.py +++ b/cloud/rackspace/rax_mon_check.py @@ -28,7 +28,7 @@ monitor. Rackspace monitoring module flow | rax_mon_entity -> *rax_mon_check* -> rax_mon_notification -> rax_mon_notification_plan -> rax_mon_alarm -version_added: "1.9" +version_added: "2.0" options: state: description: diff --git a/cloud/rackspace/rax_mon_entity.py b/cloud/rackspace/rax_mon_entity.py index 5f82ff9c524..f5f142d2165 100644 --- a/cloud/rackspace/rax_mon_entity.py +++ b/cloud/rackspace/rax_mon_entity.py @@ -26,7 +26,7 @@ provide a convenient, centralized place to store IP addresses. Rackspace monitoring module flow | *rax_mon_entity* -> rax_mon_check -> rax_mon_notification -> rax_mon_notification_plan -> rax_mon_alarm -version_added: "1.9" +version_added: "2.0" options: label: description: diff --git a/cloud/rackspace/rax_mon_notification.py b/cloud/rackspace/rax_mon_notification.py index 8a21b088c5e..d7b6692dc2c 100644 --- a/cloud/rackspace/rax_mon_notification.py +++ b/cloud/rackspace/rax_mon_notification.py @@ -25,7 +25,7 @@ channel that can be used to communicate alarms, such as email, webhooks, or PagerDuty. Rackspace monitoring module flow | rax_mon_entity -> rax_mon_check -> *rax_mon_notification* -> rax_mon_notification_plan -> rax_mon_alarm -version_added: "1.9" +version_added: "2.0" options: state: description: diff --git a/cloud/rackspace/rax_mon_notification_plan.py b/cloud/rackspace/rax_mon_notification_plan.py index 05b89b2cfb3..5bb3fa1652a 100644 --- a/cloud/rackspace/rax_mon_notification_plan.py +++ b/cloud/rackspace/rax_mon_notification_plan.py @@ -26,7 +26,7 @@ associating existing rax_mon_notifications with severity levels. Rackspace monitoring module flow | rax_mon_entity -> rax_mon_check -> rax_mon_notification -> *rax_mon_notification_plan* -> rax_mon_alarm -version_added: "1.9" +version_added: "2.0" options: state: description: From e66f4b998544adc0ec76e06a0334d67cb1f5ed90 Mon Sep 17 00:00:00 2001 From: Alan Loi Date: Wed, 24 Jun 2015 07:42:36 +1000 Subject: [PATCH 0307/2522] Updated documentation for sqs_queue - state option is not required. --- cloud/amazon/sqs_queue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/sqs_queue.py b/cloud/amazon/sqs_queue.py index 6ad8d2d1999..6bc41544cf9 100644 --- a/cloud/amazon/sqs_queue.py +++ b/cloud/amazon/sqs_queue.py @@ -28,7 +28,7 @@ state: description: - Create or delete the queue - required: true + required: false choices: ['present', 'absent'] default: 'present' name: From c8b6e1efa5e03e2774ef5cadc6a267744abde017 Mon Sep 17 00:00:00 2001 From: Alan Loi Date: Wed, 24 Jun 2015 07:45:23 +1000 Subject: [PATCH 0308/2522] Fix sqs_queue module to check that boto library is installed and AWS region & credentials are provided. --- cloud/amazon/sqs_queue.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/sqs_queue.py b/cloud/amazon/sqs_queue.py index 6bc41544cf9..a40433441aa 100644 --- a/cloud/amazon/sqs_queue.py +++ b/cloud/amazon/sqs_queue.py @@ -84,7 +84,7 @@ try: import boto.sqs - from boto.exception import BotoServerError + from boto.exception import BotoServerError, NoAuthHandlerFound HAS_BOTO = True except ImportError: @@ -204,8 +204,18 @@ def main(): argument_spec=argument_spec, supports_check_mode=True) + if not HAS_BOTO: + module.fail_json(msg='boto required for this module') + region, ec2_url, aws_connect_params = get_aws_connection_info(module) - connection = boto.sqs.connect_to_region(region) + if not region: + module.fail_json(msg='region must be specified') + + try: + connection = connect_to_aws(boto.sqs, region, **aws_connect_params) + + except (NoAuthHandlerFound, StandardError), e: + module.fail_json(msg=str(e)) state = module.params.get('state') if state == 'present': From f1e3260b3f97e37ae70788b42f089dd53f591b99 Mon Sep 17 00:00:00 2001 From: Arnaud Dematte Date: Tue, 21 Apr 2015 14:48:44 +0200 Subject: [PATCH 0309/2522] Update mail.py to allow html content Adding parameter subtype to allow html based content. The default behavior of text/plain has been preserved. --- notification/mail.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/notification/mail.py b/notification/mail.py index c42e80fdabf..52869460862 100644 --- a/notification/mail.py +++ b/notification/mail.py @@ -110,6 +110,11 @@ - The character set of email being sent default: 'us-ascii' required: false + subtype: + description: + - The minor mime type, can be either text or html. The major type is always text. + default: 'plain' + required: false """ EXAMPLES = ''' @@ -183,7 +188,8 @@ def main(): body = dict(default=None), attach = dict(default=None), headers = dict(default=None), - charset = dict(default='us-ascii') + charset = dict(default='us-ascii'), + subtype = dict(default='plain') ) ) @@ -200,6 +206,7 @@ def main(): attach_files = module.params.get('attach') headers = module.params.get('headers') charset = module.params.get('charset') + subtype = module.params.get('subtype') sender_phrase, sender_addr = parseaddr(sender) if not body: @@ -259,7 +266,7 @@ def main(): if len(cc_list) > 0: msg['Cc'] = ", ".join(cc_list) - part = MIMEText(body + "\n\n", _charset=charset) + part = MIMEText(body + "\n\n", _subtype=subtype, _charset=charset) msg.attach(part) if attach_files is not None: From 5a79b5ab0dfe59763ac131c1a77fd10b1dfe00ac Mon Sep 17 00:00:00 2001 From: Paul Markham Date: Thu, 25 Jun 2015 12:46:35 +1000 Subject: [PATCH 0310/2522] Added zone.py module to manage Solaris zones --- system/zone.py | 353 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 system/zone.py diff --git a/system/zone.py b/system/zone.py new file mode 100644 index 00000000000..d24001c973f --- /dev/null +++ b/system/zone.py @@ -0,0 +1,353 @@ +#!/usr/bin/python + +# (c) 2013, Paul Markham +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import sys +import os +import platform +import tempfile + +DOCUMENTATION = ''' +--- +module: zone +short_description: Manage Solaris zones +description: + - Create, start, stop and delete Solaris zones. This module doesn't currently allow + changing of options for a zone that's already been created. +version_added: "1.5" +author: Paul Markham +requirements: + - Solaris 10 or later +options: + state: + required: true + description: + - C(present), create the zone. + C(running), if the zone already exists, boot it, otherwise, create the zone + first, then boot it. + C(stopped), shutdown a zone. + C(absent), destroy the zone. + choices: ['present', 'running', 'stopped', 'absent'] + name: + description: + - Zone name. + required: true + path: + description: + - The path where the zone will be created. This is required when the zone is created, but not + used otherwise. + required: false + default: null + whole_root: + description: + - Whether to create a whole root (C(true)) or sparse root (C(false)) zone. + required: false + default: true + root_password: + description: + - The password hash for the root account. If not specified, the zone's root account + will not have a password. + required: false + default: null + config: + required: false + description: + - 'The zonecfg configuration commands for this zone, separated by commas, e.g. + "set auto-boot=true,add net,set physical=bge0,set address=10.1.1.1,end" + See the Solaris Systems Administrator guide for a list of all configuration commands + that can be used.' + required: false + default: null + create_options: + required: false + description: + - 'Extra options to the zonecfg create command. For example, this can be used to create a + Solaris 11 kernel zone' + required: false + default: null + timeout: + description: + - Timeout, in seconds, for zone to boot. + required: false + default: 600 +''' + +EXAMPLES = ''' +# Create a zone, but don't boot it +zone: name=zone1 state=present path=/zones/zone1 whole_root=true root_password="Be9oX7OSwWoU." + config='set autoboot=true, add net, set physical=bge0, set address=10.1.1.1, end' + +# Create a zone and boot it +zone: name=zone1 state=running path=/zones/zone1 whole_root=true root_password="Be9oX7OSwWoU." + config='set autoboot=true, add net, set physical=bge0, set address=10.1.1.1, end' + +# Boot an already created zone +zone: name=zone1 state=running + +# Stop a zone +zone: name=zone1 state=stopped + +# Destroy a zone +zone: name=zone1 state=absent +''' + +class Zone(object): + def __init__(self, module): + self.changed = False + self.msg = [] + + self.module = module + self.path = self.module.params['path'] + self.name = self.module.params['name'] + self.whole_root = self.module.params['whole_root'] + self.root_password = self.module.params['root_password'] + self.timeout = self.module.params['timeout'] + self.config = self.module.params['config'] + self.create_options = self.module.params['create_options'] + + self.zoneadm_cmd = self.module.get_bin_path('zoneadm', True) + self.zonecfg_cmd = self.module.get_bin_path('zonecfg', True) + self.ssh_keygen_cmd = self.module.get_bin_path('ssh-keygen', True) + + def create(self): + if not self.path: + self.module.fail_json(msg='Missing required argument: path') + + t = tempfile.NamedTemporaryFile(delete = False) + + if self.whole_root: + t.write('create -b %s\n' % self.create_options) + self.msg.append('creating whole root zone') + else: + t.write('create %s\n' % self.create_options) + self.msg.append('creating sparse root zone') + + t.write('set zonepath=%s\n' % self.path) + + if self.config: + for line in self.config: + t.write('%s\n' % line) + t.close() + + cmd = '%s -z %s -f %s' % (self.zonecfg_cmd, self.name, t.name) + (rc, out, err) = self.module.run_command(cmd) + if rc != 0: + self.module.fail_json(msg='Failed to create zone. %s' % (out + err)) + os.unlink(t.name) + + cmd = '%s -z %s install' % (self.zoneadm_cmd, self.name) + (rc, out, err) = self.module.run_command(cmd) + if rc != 0: + self.module.fail_json(msg='Failed to install zone. %s' % (out + err)) + + self.configure_sysid() + self.configure_password() + self.configure_ssh_keys() + + def configure_sysid(self): + os.unlink('%s/root/etc/.UNCONFIGURED' % self.path) + + open('%s/root/noautoshutdown' % self.path, 'w').close() + + node = open('%s/root/etc/nodename' % self.path, 'w') + node.write(self.name) + node.close + + id = open('%s/root/etc/.sysIDtool.state' % self.path, 'w') + id.write('1 # System previously configured?\n') + id.write('1 # Bootparams succeeded?\n') + id.write('1 # System is on a network?\n') + id.write('1 # Extended network information gathered?\n') + id.write('0 # Autobinder succeeded?\n') + id.write('1 # Network has subnets?\n') + id.write('1 # root password prompted for?\n') + id.write('1 # locale and term prompted for?\n') + id.write('1 # security policy in place\n') + id.write('1 # NFSv4 domain configured\n') + id.write('0 # Auto Registration Configured\n') + id.write('vt100') + id.close() + + def configure_ssh_keys(self): + rsa_key_file = '%s/root/etc/ssh/ssh_host_rsa_key' % self.path + dsa_key_file = '%s/root/etc/ssh/ssh_host_dsa_key' % self.path + + if not os.path.isfile(rsa_key_file): + cmd = '%s -f %s -t rsa -N ""' % (self.ssh_keygen_cmd, rsa_key_file) + (rc, out, err) = self.module.run_command(cmd) + if rc != 0: + self.module.fail_json(msg='Failed to create rsa key. %s' % (out + err)) + + if not os.path.isfile(dsa_key_file): + cmd = '%s -f %s -t dsa -N ""' % (self.ssh_keygen_cmd, dsa_key_file) + (rc, out, err) = self.module.run_command(cmd) + if rc != 0: + self.module.fail_json(msg='Failed to create dsa key. %s' % (out + err)) + + def configure_password(self): + shadow = '%s/root/etc/shadow' % self.path + if self.root_password: + f = open(shadow, 'r') + lines = f.readlines() + f.close() + + for i in range(0, len(lines)): + fields = lines[i].split(':') + if fields[0] == 'root': + fields[1] = self.root_password + lines[i] = ':'.join(fields) + + f = open(shadow, 'w') + for line in lines: + f.write(line) + f.close() + + def boot(self): + cmd = '%s -z %s boot' % (self.zoneadm_cmd, self.name) + (rc, out, err) = self.module.run_command(cmd) + if rc != 0: + self.module.fail_json(msg='Failed to boot zone. %s' % (out + err)) + + """ + The boot command can return before the zone has fully booted. This is especially + true on the first boot when the zone initializes the SMF services. Unless the zone + has fully booted, subsequent tasks in the playbook may fail as services aren't running yet. + Wait until the zone's console login is running; once that's running, consider the zone booted. + """ + + elapsed = 0 + while True: + if elapsed > self.timeout: + self.module.fail_json(msg='timed out waiting for zone to boot') + rc = os.system('ps -z %s -o args|grep "/usr/lib/saf/ttymon.*-d /dev/console" > /dev/null 2>/dev/null' % self.name) + if rc == 0: + break + time.sleep(10) + elapsed += 10 + + def destroy(self): + cmd = '%s -z %s delete -F' % (self.zonecfg_cmd, self.name) + (rc, out, err) = self.module.run_command(cmd) + if rc != 0: + self.module.fail_json(msg='Failed to delete zone. %s' % (out + err)) + + def stop(self): + cmd = '%s -z %s halt' % (self.zoneadm_cmd, self.name) + (rc, out, err) = self.module.run_command(cmd) + if rc != 0: + self.module.fail_json(msg='Failed to stop zone. %s' % (out + err)) + + def exists(self): + cmd = '%s -z %s list' % (self.zoneadm_cmd, self.name) + (rc, out, err) = self.module.run_command(cmd) + if rc == 0: + return True + else: + return False + + def running(self): + cmd = '%s -z %s list -p' % (self.zoneadm_cmd, self.name) + (rc, out, err) = self.module.run_command(cmd) + if rc != 0: + self.module.fail_json(msg='Failed to determine zone state. %s' % (out + err)) + + if out.split(':')[2] == 'running': + return True + else: + return False + + + def state_present(self): + if self.exists(): + self.msg.append('zone already exists') + else: + if not self.module.check_mode: + self.create() + self.changed = True + self.msg.append('zone created') + + def state_running(self): + self.state_present() + if self.running(): + self.msg.append('zone already running') + else: + if not self.module.check_mode: + self.boot() + self.changed = True + self.msg.append('zone booted') + + def state_stopped(self): + if self.exists(): + if self.running(): + if not self.module.check_mode: + self.stop() + self.changed = True + self.msg.append('zone stopped') + else: + self.msg.append('zone not running') + else: + self.module.fail_json(msg='zone does not exist') + + def state_absent(self): + if self.exists(): + self.state_stopped() + if not self.module.check_mode: + self.destroy() + self.changed = True + self.msg.append('zone deleted') + else: + self.msg.append('zone does not exist') + + def exit_with_msg(self): + msg = ', '.join(self.msg) + self.module.exit_json(changed=self.changed, msg=msg) + +def main(): + module = AnsibleModule( + argument_spec = dict( + name = dict(required=True), + state = dict(required=True, choices=['running', 'present', 'stopped', 'absent']), + path = dict(defalt=None), + whole_root = dict(default=True, type='bool'), + root_password = dict(default=None), + timeout = dict(default=600, type='int'), + config = dict(default=None, type='list'), + create_options = dict(default=''), + ), + supports_check_mode=True + ) + + zone = Zone(module) + + state = module.params['state'] + + if state == 'running': + zone.state_running() + elif state == 'present': + zone.state_present() + elif state == 'stopped': + zone.state_stopped() + elif state == 'absent': + zone.state_absent() + else: + module.fail_json(msg='Invalid state: %s' % state) + + zone.exit_with_msg() + +from ansible.module_utils.basic import * +main() From 955bf92ff892a7359a045e1ddb3b29b7809a230b Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Thu, 25 Jun 2015 06:53:28 -0700 Subject: [PATCH 0311/2522] Add version_added to the subtype parameter --- notification/mail.py | 1 + 1 file changed, 1 insertion(+) diff --git a/notification/mail.py b/notification/mail.py index 52869460862..8be9a589cbf 100644 --- a/notification/mail.py +++ b/notification/mail.py @@ -115,6 +115,7 @@ - The minor mime type, can be either text or html. The major type is always text. default: 'plain' required: false + version_added: "2.0" """ EXAMPLES = ''' From 9183170a4a0e8d1ccfdf8c3535ad3b28ca25b22c Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Thu, 25 Jun 2015 07:05:29 -0700 Subject: [PATCH 0312/2522] These modules were added to version 2.0, not 1.9 --- windows/win_iis_virtualdirectory.py | 2 +- windows/win_iis_webapplication.py | 2 +- windows/win_iis_webapppool.py | 2 +- windows/win_iis_webbinding.py | 2 +- windows/win_iis_website.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/windows/win_iis_virtualdirectory.py b/windows/win_iis_virtualdirectory.py index bbedfbbb4ab..c8a5dd1dcc8 100644 --- a/windows/win_iis_virtualdirectory.py +++ b/windows/win_iis_virtualdirectory.py @@ -21,7 +21,7 @@ DOCUMENTATION = ''' --- module: win_iis_virtualdirectory -version_added: "1.9" +version_added: "2.0" short_description: Configures a IIS virtual directories. description: - Creates, Removes and configures a IIS Web site diff --git a/windows/win_iis_webapplication.py b/windows/win_iis_webapplication.py index d8a59b66054..11a338e71e0 100644 --- a/windows/win_iis_webapplication.py +++ b/windows/win_iis_webapplication.py @@ -21,7 +21,7 @@ DOCUMENTATION = ''' --- module: win_iis_website -version_added: "1.9" +version_added: "2.0" short_description: Configures a IIS Web application. description: - Creates, Removes and configures a IIS Web applications diff --git a/windows/win_iis_webapppool.py b/windows/win_iis_webapppool.py index 320fe07f637..c77c3b04cb7 100644 --- a/windows/win_iis_webapppool.py +++ b/windows/win_iis_webapppool.py @@ -22,7 +22,7 @@ DOCUMENTATION = ''' --- module: win_iis_webapppool -version_added: "1.9" +version_added: "2.0" short_description: Configures a IIS Web Application Pool. description: - Creates, Removes and configures a IIS Web Application Pool diff --git a/windows/win_iis_webbinding.py b/windows/win_iis_webbinding.py index 0cc5da158bf..061bed73723 100644 --- a/windows/win_iis_webbinding.py +++ b/windows/win_iis_webbinding.py @@ -22,7 +22,7 @@ DOCUMENTATION = ''' --- module: win_iis_webbinding -version_added: "1.9" +version_added: "2.0" short_description: Configures a IIS Web site. description: - Creates, Removes and configures a binding to an existing IIS Web site diff --git a/windows/win_iis_website.py b/windows/win_iis_website.py index 0893b11c2bd..8921afe5970 100644 --- a/windows/win_iis_website.py +++ b/windows/win_iis_website.py @@ -21,7 +21,7 @@ DOCUMENTATION = ''' --- module: win_iis_website -version_added: "1.9" +version_added: "2.0" short_description: Configures a IIS Web site. description: - Creates, Removes and configures a IIS Web site From dec7d95d514ca89c2784b63d836dd6fb872bdd9c Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Thu, 25 Jun 2015 07:12:10 -0700 Subject: [PATCH 0313/2522] Fix up docs --- cloud/amazon/dynamodb_table.py | 1 + windows/win_iis_virtualdirectory.py | 4 ++-- windows/win_iis_webapplication.py | 14 +++++++------- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/cloud/amazon/dynamodb_table.py b/cloud/amazon/dynamodb_table.py index 130fae44721..94d1f4616bb 100644 --- a/cloud/amazon/dynamodb_table.py +++ b/cloud/amazon/dynamodb_table.py @@ -23,6 +23,7 @@ - Can update the provisioned throughput on existing tables. - Returns the status of the specified table. author: Alan Loi (@loia) +version_added: "2.0" requirements: - "boto >= 2.13.2" options: diff --git a/windows/win_iis_virtualdirectory.py b/windows/win_iis_virtualdirectory.py index c8a5dd1dcc8..e5bbd950007 100644 --- a/windows/win_iis_virtualdirectory.py +++ b/windows/win_iis_virtualdirectory.py @@ -28,13 +28,13 @@ options: name: description: - - The name of the virtual directory to create. + - The name of the virtual directory to create or remove required: true default: null aliases: [] state: description: - - + - Whether to add or remove the specified virtual directory choices: - absent - present diff --git a/windows/win_iis_webapplication.py b/windows/win_iis_webapplication.py index 11a338e71e0..b8ebd085162 100644 --- a/windows/win_iis_webapplication.py +++ b/windows/win_iis_webapplication.py @@ -20,7 +20,7 @@ DOCUMENTATION = ''' --- -module: win_iis_website +module: win_iis_webapplication version_added: "2.0" short_description: Configures a IIS Web application. description: @@ -32,12 +32,12 @@ required: true default: null aliases: [] - site: - description: - - Name of the site on which the application is created. - required: true - default: null - aliases: [] + site: + description: + - Name of the site on which the application is created. + required: true + default: null + aliases: [] state: description: - State of the web application From b5d724c2fbdf0dbea8f614c569e65bfcace48b96 Mon Sep 17 00:00:00 2001 From: Phil Date: Thu, 25 Jun 2015 15:55:23 -0500 Subject: [PATCH 0314/2522] removed hardcoded paths, check for .exe error exit code, use get-attr for required param check. --- windows/win_timezone.ps1 | 61 ++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/windows/win_timezone.ps1 b/windows/win_timezone.ps1 index 4626dd10b54..03a6935052d 100644 --- a/windows/win_timezone.ps1 +++ b/windows/win_timezone.ps1 @@ -26,44 +26,45 @@ $result = New-Object psobject @{ changed = $false } -If ($params.timezone) { - Try { - # Get the current timezone set - $currentTZ = $(C:\Windows\System32\tzutil /g) +$timezone = Get-Attr -obj $params -name timezone -failifempty $true -resultobj $result - If ( $currentTZ -eq $params.timezone ) { - Exit-Json $result "$params.timezone is already set on this machine" - } - Else { - $tzExists = $false - #Check that timezone can even be set (if it is listed from tzutil as an available timezone to the machine) - $tzList = $(C:\Windows\System32\tzutil /l) - ForEach ($tz in $tzList) { - If ( $tz -eq $params.timezone ) { - $tzExists = $true - break - } +Try { + # Get the current timezone set + $currentTZ = $(tzutil.exe /g) + If ($LASTEXITCODE -ne 0) { Throw "An error occured when getting the current machine's timezone setting." } + + If ( $currentTZ -eq $timezone ) { + Exit-Json $result "$timezone is already set on this machine" + } + Else { + $tzExists = $false + #Check that timezone can even be set (if it is listed from tzutil as an available timezone to the machine) + $tzList = $(tzutil.exe /l) + If ($LASTEXITCODE -ne 0) { Throw "An error occured when listing the available timezones." } + ForEach ($tz in $tzList) { + If ( $tz -eq $timezone ) { + $tzExists = $true + break } + } - If ( $tzExists ) { - C:\Windows\System32\tzutil /s "$params.timezone" - $newTZ = $(C:\Windows\System32\tzutil /g) + If ( $tzExists ) { + tzutil.exe /s "$timezone" + If ($LASTEXITCODE -ne 0) { Throw "An error occured when setting the specified timezone with tzutil." } + $newTZ = $(tzutil.exe /g) + If ($LASTEXITCODE -ne 0) { Throw "An error occured when getting the current machine's timezone setting." } - If ( $params.timezone -eq $newTZ ) { - $result.changed = $true - } - } - Else { - Fail-Json $result "The specified timezone: $params.timezone isn't supported on the machine." + If ( $timezone -eq $newTZ ) { + $result.changed = $true } } - } - Catch { - Fail-Json $result "Error setting timezone to: $params.timezone." + Else { + Fail-Json $result "The specified timezone: $timezone isn't supported on the machine." + } } } -Else { - Fail-Json $result "Parameter: timezone is required." +Catch { + Fail-Json $result "Error setting timezone to: $timezone." } From 7c231a340a30dcea2668cf6f2b9c5c15f992ae29 Mon Sep 17 00:00:00 2001 From: Paul Markham Date: Fri, 26 Jun 2015 09:44:33 +1000 Subject: [PATCH 0315/2522] - Renamed module to solaris_zone.py - Updated 'version_added' - Updated description of 'state' to make each line a list item - Check that OS is Solaris --- system/{zone.py => solaris_zone.py} | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) rename system/{zone.py => solaris_zone.py} (93%) diff --git a/system/zone.py b/system/solaris_zone.py similarity index 93% rename from system/zone.py rename to system/solaris_zone.py index d24001c973f..be693bf1425 100644 --- a/system/zone.py +++ b/system/solaris_zone.py @@ -24,12 +24,12 @@ DOCUMENTATION = ''' --- -module: zone +module: solaris_zone short_description: Manage Solaris zones description: - Create, start, stop and delete Solaris zones. This module doesn't currently allow changing of options for a zone that's already been created. -version_added: "1.5" +version_added: "2.0" author: Paul Markham requirements: - Solaris 10 or later @@ -38,10 +38,10 @@ required: true description: - C(present), create the zone. - C(running), if the zone already exists, boot it, otherwise, create the zone + - C(running), if the zone already exists, boot it, otherwise, create the zone first, then boot it. - C(stopped), shutdown a zone. - C(absent), destroy the zone. + - C(stopped), shutdown a zone. + - C(absent), destroy the zone. choices: ['present', 'running', 'stopped', 'absent'] name: description: @@ -89,21 +89,21 @@ EXAMPLES = ''' # Create a zone, but don't boot it -zone: name=zone1 state=present path=/zones/zone1 whole_root=true root_password="Be9oX7OSwWoU." +solaris_zone: name=zone1 state=present path=/zones/zone1 whole_root=true root_password="Be9oX7OSwWoU." config='set autoboot=true, add net, set physical=bge0, set address=10.1.1.1, end' # Create a zone and boot it -zone: name=zone1 state=running path=/zones/zone1 whole_root=true root_password="Be9oX7OSwWoU." +solaris_zone: name=zone1 state=running path=/zones/zone1 whole_root=true root_password="Be9oX7OSwWoU." config='set autoboot=true, add net, set physical=bge0, set address=10.1.1.1, end' # Boot an already created zone -zone: name=zone1 state=running +solaris_zone: name=zone1 state=running # Stop a zone -zone: name=zone1 state=stopped +solaris_zone: name=zone1 state=stopped # Destroy a zone -zone: name=zone1 state=absent +solaris_zone: name=zone1 state=absent ''' class Zone(object): @@ -332,6 +332,13 @@ def main(): supports_check_mode=True ) + if platform.system() == 'SunOS': + (major, minor) = platform.release().split('.') + if minor < 10: + module.fail_json(msg='This module requires Solaris 10 or later') + else: + module.fail_json(msg='This module requires Solaris') + zone = Zone(module) state = module.params['state'] From 60b5ae35b30d4c2a2b2d337ac413864d6df8251a Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Fri, 26 Jun 2015 14:23:35 +0200 Subject: [PATCH 0316/2522] cloudstack: make get_template_or_iso returning a dict for fix GH-646 --- cloud/cloudstack/cs_instance.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index a93a524383a..7cf4426267e 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -355,6 +355,8 @@ class AnsibleCloudStackInstance(AnsibleCloudStack): def __init__(self, module): AnsibleCloudStack.__init__(self, module) self.instance = None + self.template = None + self.iso = None def get_service_offering_id(self): @@ -371,7 +373,7 @@ def get_service_offering_id(self): self.module.fail_json(msg="Service offering '%s' not found" % service_offering) - def get_template_or_iso_id(self): + def get_template_or_iso(self, key=None): template = self.module.params.get('template') iso = self.module.params.get('iso') @@ -388,21 +390,28 @@ def get_template_or_iso_id(self): args['zoneid'] = self.get_zone('id') if template: + if self.template: + return self._get_by_key(key, self.template) + args['templatefilter'] = 'executable' templates = self.cs.listTemplates(**args) if templates: for t in templates['template']: if template in [ t['displaytext'], t['name'], t['id'] ]: - return t['id'] + self.template = t + return self._get_by_key(key, self.template) self.module.fail_json(msg="Template '%s' not found" % template) elif iso: + if self.iso: + return self._get_by_key(key, self.iso) args['isofilter'] = 'executable' isos = self.cs.listIsos(**args) if isos: for i in isos['iso']: if iso in [ i['displaytext'], i['name'], i['id'] ]: - return i['id'] + self.iso = i + return self._get_by_key(key, self.iso) self.module.fail_json(msg="ISO '%s' not found" % iso) @@ -503,7 +512,7 @@ def deploy_instance(self): self.result['changed'] = True args = {} - args['templateid'] = self.get_template_or_iso_id() + args['templateid'] = self.get_template_or_iso(key='id') args['zoneid'] = self.get_zone('id') args['serviceofferingid'] = self.get_service_offering_id() args['account'] = self.get_account('name') From b1e6d6ba52c7aaa5f2ab1c73e642d774ad88986c Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Fri, 26 Jun 2015 14:52:31 +0200 Subject: [PATCH 0317/2522] cloudstack: fix cs_instance hypervisor must be omitted if set on template/iso Fix related to issue reported in PR GH-646 --- cloud/cloudstack/cs_instance.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 7cf4426267e..0d156390e83 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -70,8 +70,8 @@ hypervisor: description: - Name the hypervisor to be used for creating the new instance. - - Relevant when using C(state=present) and option C(ISO) is used. - - If not set, first found hypervisor will be used. + - Relevant when using C(state=present), but only considered if not set on ISO/template. + - If not set or found on ISO/template, first found hypervisor will be used. required: false default: null choices: [ 'KVM', 'VMware', 'BareMetal', 'XenServer', 'LXC', 'HyperV', 'UCS', 'OVM' ] @@ -520,7 +520,6 @@ def deploy_instance(self): args['projectid'] = self.get_project('id') args['diskofferingid'] = self.get_disk_offering_id() args['networkids'] = self.get_network_ids() - args['hypervisor'] = self.get_hypervisor() args['userdata'] = self.get_user_data() args['keyboard'] = self.module.params.get('keyboard') args['ipaddress'] = self.module.params.get('ip_address') @@ -532,6 +531,10 @@ def deploy_instance(self): args['securitygroupnames'] = ','.join(self.module.params.get('security_groups')) args['affinitygroupnames'] = ','.join(self.module.params.get('affinity_groups')) + template_iso = self.get_template_or_iso() + if 'hypervisor' not in template_iso: + args['hypervisor'] = self.get_hypervisor() + instance = None if not self.module.check_mode: instance = self.cs.deployVirtualMachine(**args) From 12d76027df51d96f584972b9cae9699395d3de87 Mon Sep 17 00:00:00 2001 From: Rick Mendes Date: Fri, 26 Jun 2015 17:00:58 -0700 Subject: [PATCH 0318/2522] upgraded docs and argspec to match module guidelines --- cloud/amazon/ec2_win_password.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cloud/amazon/ec2_win_password.py b/cloud/amazon/ec2_win_password.py index 05aa67e3d29..d555ce625d3 100644 --- a/cloud/amazon/ec2_win_password.py +++ b/cloud/amazon/ec2_win_password.py @@ -7,7 +7,7 @@ description: - Gets the default administrator password from any EC2 Windows instance. The instance is referenced by its id (e.g. i-XXXXXXX). This module has a dependency on python-boto. version_added: "2.0" -author: Rick Mendes +author: Rick Mendes(@rickmendes) options: instance_id: description: @@ -22,6 +22,7 @@ description: - The passphrase for the instance key pair. The key must use DES or 3DES encryption for this module to decrypt it. You can use openssl to convert your password protected keys if they do not use DES or 3DES. ex) openssl rsa -in current_key -out new_key -des3. required: false + default: null region: description: - The AWS region to use. Must be specified if ec2_url is not used. If not specified then the value of the EC2_REGION environment variable, if any, is used. @@ -39,6 +40,7 @@ version_added: "2.0" description: - Number of seconds to wait before giving up. + required: false default: 120 extends_documentation_fragment: aws @@ -93,9 +95,9 @@ def main(): argument_spec.update(dict( instance_id = dict(required=True), key_file = dict(required=True), - key_passphrase = dict(default=None), - wait = dict(type='bool', default=False), - wait_timeout = dict(default=120), + key_passphrase = dict(no_log=True, default=None, required=False), + wait = dict(type='bool', default=False, required=False), + wait_timeout = dict(default=120, required=False), ) ) module = AnsibleModule(argument_spec=argument_spec) From 9a1918c62875fde93267213631fc8852a704f31e Mon Sep 17 00:00:00 2001 From: Tim Hoiberg Date: Wed, 13 May 2015 19:40:50 +1000 Subject: [PATCH 0319/2522] Adding module to manage Ruby Gem dependencies via Bundler --- packaging/language/bundler.py | 199 ++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 packaging/language/bundler.py diff --git a/packaging/language/bundler.py b/packaging/language/bundler.py new file mode 100644 index 00000000000..877d09dbea5 --- /dev/null +++ b/packaging/language/bundler.py @@ -0,0 +1,199 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Tim Hoiberg +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +DOCUMENTATION=''' +--- +module: bundler +short_description: Manage Ruby Gem dependencies with Bundler +description: + - Manage installation and Gem version dependencies for Ruby using the Bundler gem +version_added: "2.0.0" +options: + executable: + description: + - The path to the bundler executable + required: false + default: null + state: + description: + - The desired state of the Gem bundle. C(latest) updates gems to the most recent, acceptable version + required: false + choices: [present, latest] + default: present + chdir: + description: + - The directory to execute the bundler commands from. This directoy needs to contain a valid Gemfile or .bundle/ directory + required: false + default: temporary working directory + exclude_groups: + description: + - A list of Gemfile groups to exclude during operations. This only applies when state is C(present). Bundler considers this a 'remembered' + property for the Gemfile and will automatically exclude groups in future operations even if C(exclude_groups) is not set + required: false + default: null + clean: + description: + - Only applies if state is C(present). If set removes any gems on the target host that are not in the gemfile + required: false + choices: [yes, no] + default: "no" + gemfile: + description: + - Only applies if state is C(present). The path to the gemfile to use to install gems. + required: false + default: Gemfile in current directory + local: + description: + - If set only installs gems from the cache on the target host + required: false + choices: [yes, no] + default: "no" + deployment_mode: + description: + - Only applies if state is C(present). If set it will only install gems that are in the default or production groups. Requires a Gemfile.lock + file to have been created prior + required: false + choices: [yes, no] + default: "no" + user_install: + description: + - Only applies if state is C(present). Installs gems in the local user's cache or for all users + required: false + choices: [yes, no] + default: "yes" + gem_path: + description: + - Only applies if state is C(present). Specifies the directory to install the gems into. If C(chdir) is set then this path is relative to C(chdir) + required: false + default: RubyGems gem paths + binstub_directory: + description: + - Only applies if state is C(present). Specifies the directory to install any gem bins files to. When executed the bin files will run within + the context of the Gemfile and fail if any required gem dependencies are not installed. If C(chdir) is set then this path is relative to C(chdir) + required: false + default: null + extra_args: + description: + - A space separated string of additional commands that can be applied to the Bundler command. Refer to the Bundler documentation for more + information + required: false + default: null +author: Tim Hoiberg +''' + +EXAMPLES=''' +# Installs gems from a Gemfile in the current directory +- bundler: state=present executable=~/.rvm/gems/2.1.5/bin/bundle + +# Excludes the production group from installing +- bundler: state=present exclude_groups=production + +# Only install gems from the default and production groups +- bundler: state=present deployment=yes + +# Installs gems using a Gemfile in another directory +- bunlder: state=present gemfile=../rails_project/Gemfile + +# Updates Gemfile in another directory +- bundler: state=latest chdir=~/rails_project +''' + + +def get_bundler_executable(module): + if module.params.get('executable'): + return module.params.get('executable').split(' ') + else: + return [ module.get_bin_path('bundle', True) ] + + +def main(): + module = AnsibleModule( + argument_spec=dict( + executable=dict(default=None, required=False), + state=dict(default='present', required=False, choices=['present', 'latest']), + chdir=dict(default=None, required=False), + exclude_groups=dict(default=None, required=False, type='list'), + clean=dict(default=False, required=False, type='bool'), + gemfile=dict(default=None, required=False), + local=dict(default=False, required=False, type='bool'), + deployment_mode=dict(default=False, required=False, type='bool'), + user_install=dict(default=True, required=False, type='bool'), + gem_path=dict(default=None, required=False), + binstub_directory=dict(default=None, required=False), + extra_args=dict(default=None, required=False), + ), + supports_check_mode=True + ) + + executable = module.params.get('executable') + state = module.params.get('state') + chdir = module.params.get('chdir') + exclude_groups = module.params.get('exclude_groups') + clean = module.params.get('clean') + gemfile = module.params.get('gemfile') + local = module.params.get('local') + deployment_mode = module.params.get('deployment_mode') + user_install = module.params.get('user_install') + gem_path = module.params.get('gem_install_path') + binstub_directory = module.params.get('binstub_directory') + extra_args = module.params.get('extra_args') + + cmd = get_bundler_executable(module) + + if module.check_mode: + cmd.append('check') + rc, out, err = module.run_command(cmd, cwd=chdir, check_rc=False) + + module.exit_json(changed=rc != 0, state=state, stdout=out, stderr=err) + + if state == 'present': + cmd.append('install') + if exclude_groups: + cmd.extend(['--without', ':'.join(exclude_groups)]) + if clean: + cmd.append('--clean') + if gemfile: + cmd.extend(['--gemfile', gemfile]) + if local: + cmd.append('--local') + if deployment_mode: + cmd.append('--deployment') + if not user_install: + cmd.append('--system') + if gem_path: + cmd.extend(['--path', gem_path]) + if binstub_directory: + cmd.extend(['--binstubs', binstub_directory]) + else: + cmd.append('update') + if local: + cmd.append('--local') + + if extra_args: + cmd.extend(extra_args.split(' ')) + + rc, out, err = module.run_command(cmd, cwd=chdir, check_rc=True) + + module.exit_json(changed='Installing' in out, state=state, stdout=out, stderr=err) + + +from ansible.module_utils.basic import * +main() \ No newline at end of file From 1d48c9658a6c539f6a82f6b857342cc20a321597 Mon Sep 17 00:00:00 2001 From: Tim Hoiberg Date: Sat, 27 Jun 2015 15:50:30 +1000 Subject: [PATCH 0320/2522] Fixing typo --- packaging/language/bundler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/language/bundler.py b/packaging/language/bundler.py index 877d09dbea5..82ef2838a9a 100644 --- a/packaging/language/bundler.py +++ b/packaging/language/bundler.py @@ -110,7 +110,7 @@ - bundler: state=present deployment=yes # Installs gems using a Gemfile in another directory -- bunlder: state=present gemfile=../rails_project/Gemfile +- bundler: state=present gemfile=../rails_project/Gemfile # Updates Gemfile in another directory - bundler: state=latest chdir=~/rails_project From b031e818b1e3c26bcd5050d2ccf15e614511cd46 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Fri, 26 Jun 2015 09:39:53 +0200 Subject: [PATCH 0321/2522] cloudstack: fix cs_instance can not find iso and disk offering if domain is set. This does only affect root admins. --- cloud/cloudstack/cs_instance.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 0d156390e83..3023498db39 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -388,6 +388,7 @@ def get_template_or_iso(self, key=None): args['domainid'] = self.get_domain('id') args['projectid'] = self.get_project('id') args['zoneid'] = self.get_zone('id') + args['isrecursive'] = True if template: if self.template: @@ -421,10 +422,7 @@ def get_disk_offering_id(self): if not disk_offering: return None - args = {} - args['domainid'] = self.get_domain('id') - - disk_offerings = self.cs.listDiskOfferings(**args) + disk_offerings = self.cs.listDiskOfferings() if disk_offerings: for d in disk_offerings['diskoffering']: if disk_offering in [ d['displaytext'], d['name'], d['id'] ]: From c8d6d68428949e66f8b726fe6094f7794f2e9ec4 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Fri, 26 Jun 2015 09:42:18 +0200 Subject: [PATCH 0322/2522] cloudstack: cleanup cs_instance use param key exlicitly for utils methods --- cloud/cloudstack/cs_instance.py | 36 ++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 3023498db39..f53c0079821 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -384,10 +384,10 @@ def get_template_or_iso(self, key=None): self.module.fail_json(msg="Template are ISO are mutually exclusive.") args = {} - args['account'] = self.get_account('name') - args['domainid'] = self.get_domain('id') - args['projectid'] = self.get_project('id') - args['zoneid'] = self.get_zone('id') + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') + args['projectid'] = self.get_project(key='id') + args['zoneid'] = self.get_zone(key='id') args['isrecursive'] = True if template: @@ -436,10 +436,10 @@ def get_instance(self): instance_name = self.module.params.get('name') args = {} - args['account'] = self.get_account('name') - args['domainid'] = self.get_domain('id') - args['projectid'] = self.get_project('id') - args['zoneid'] = self.get_zone('id') + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') + args['projectid'] = self.get_project(key='id') + args['zoneid'] = self.get_zone(key='id') instances = self.cs.listVirtualMachines(**args) if instances: @@ -456,10 +456,10 @@ def get_network_ids(self): return None args = {} - args['account'] = self.get_account('name') - args['domainid'] = self.get_domain('id') - args['projectid'] = self.get_project('id') - args['zoneid'] = self.get_zone('id') + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') + args['projectid'] = self.get_project(key='id') + args['zoneid'] = self.get_zone(key='id') networks = self.cs.listNetworks(**args) if not networks: @@ -511,11 +511,11 @@ def deploy_instance(self): args = {} args['templateid'] = self.get_template_or_iso(key='id') - args['zoneid'] = self.get_zone('id') + args['zoneid'] = self.get_zone(key='id') args['serviceofferingid'] = self.get_service_offering_id() - args['account'] = self.get_account('name') - args['domainid'] = self.get_domain('id') - args['projectid'] = self.get_project('id') + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') + args['projectid'] = self.get_project(key='id') args['diskofferingid'] = self.get_disk_offering_id() args['networkids'] = self.get_network_ids() args['userdata'] = self.get_user_data() @@ -556,12 +556,12 @@ def update_instance(self, instance): args_instance_update['group'] = self.module.params.get('group') args_instance_update['displayname'] = self.get_display_name() args_instance_update['userdata'] = self.get_user_data() - args_instance_update['ostypeid'] = self.get_os_type('id') + args_instance_update['ostypeid'] = self.get_os_type(key='id') args_ssh_key = {} args_ssh_key['id'] = instance['id'] args_ssh_key['keypair'] = self.module.params.get('ssh_key') - args_ssh_key['projectid'] = self.get_project('id') + args_ssh_key['projectid'] = self.get_project(key='id') if self._has_changed(args_service_offering, instance) or \ self._has_changed(args_instance_update, instance) or \ From 5b86a15cdb960c42a71253e1036e35e1f2eb9977 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Fri, 26 Jun 2015 10:04:19 +0200 Subject: [PATCH 0323/2522] cloudstack: cs_instance do not pass zoneid to listVirtualMachines This change is related to 2 issues; - The API does not return destroyed VMs if zone ID is passed for CS version < 4.5.2. Also see CLOUDSTACK-8578. This only affects domain and root admins. - The instance name must be unique across all zones. If we pass the zone ID to find a VM, it will not be found if it is in a different zone but a deployment with the name would fail. --- cloud/cloudstack/cs_instance.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index f53c0079821..9aa9bb89651 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -439,8 +439,7 @@ def get_instance(self): args['account'] = self.get_account(key='name') args['domainid'] = self.get_domain(key='id') args['projectid'] = self.get_project(key='id') - args['zoneid'] = self.get_zone(key='id') - + # Do not pass zoneid, as the instance name must be unique across zones. instances = self.cs.listVirtualMachines(**args) if instances: for v in instances['virtualmachine']: From 94060b5adeb01b5b88580f0cec7c4a9ea5bff117 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Fri, 26 Jun 2015 11:56:28 +0200 Subject: [PATCH 0324/2522] cloudstack: fix state=expunged in cs_instance --- cloud/cloudstack/cs_instance.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 9aa9bb89651..f6518b85e52 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -633,7 +633,7 @@ def expunge_instance(self): if instance['state'].lower() in [ 'destroying', 'destroyed' ]: self.result['changed'] = True if not self.module.check_mode: - res = self.cs.expungeVirtualMachine(id=instance['id']) + res = self.cs.destroyVirtualMachine(id=instance['id'], expunge=True) elif instance['state'].lower() not in [ 'expunging' ]: self.result['changed'] = True @@ -645,7 +645,7 @@ def expunge_instance(self): poll_async = self.module.params.get('poll_async') if poll_async: - instance = self._poll_job(res, 'virtualmachine') + res = self._poll_job(res, 'virtualmachine') return instance From db33fcf89a1f2c71c33b745810b0823887780e23 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sun, 28 Jun 2015 12:52:05 +0200 Subject: [PATCH 0325/2522] cloudstack: update code to match best practice * Remove catchall exception * use `if __name__ == '__main__':` --- cloud/cloudstack/cs_account.py | 6 ++---- cloud/cloudstack/cs_affinitygroup.py | 6 ++---- cloud/cloudstack/cs_firewall.py | 6 ++---- cloud/cloudstack/cs_instance.py | 6 ++---- cloud/cloudstack/cs_instancegroup.py | 6 ++---- cloud/cloudstack/cs_iso.py | 6 ++---- cloud/cloudstack/cs_network.py | 6 ++---- cloud/cloudstack/cs_portforward.py | 6 ++---- cloud/cloudstack/cs_project.py | 6 ++---- cloud/cloudstack/cs_securitygroup.py | 6 ++---- cloud/cloudstack/cs_securitygroup_rule.py | 6 ++---- cloud/cloudstack/cs_sshkeypair.py | 6 ++---- cloud/cloudstack/cs_template.py | 6 ++---- cloud/cloudstack/cs_vmsnapshot.py | 6 ++---- 14 files changed, 28 insertions(+), 56 deletions(-) diff --git a/cloud/cloudstack/cs_account.py b/cloud/cloudstack/cs_account.py index cc487af5e51..d1302854454 100644 --- a/cloud/cloudstack/cs_account.py +++ b/cloud/cloudstack/cs_account.py @@ -400,11 +400,9 @@ def main(): except CloudStackException, e: module.fail_json(msg='CloudStackException: %s' % str(e)) - except Exception, e: - module.fail_json(msg='Exception: %s' % str(e)) - module.exit_json(**result) # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/cloudstack/cs_affinitygroup.py b/cloud/cloudstack/cs_affinitygroup.py index 580cc5d7e8d..cfd76816e1b 100644 --- a/cloud/cloudstack/cs_affinitygroup.py +++ b/cloud/cloudstack/cs_affinitygroup.py @@ -246,11 +246,9 @@ def main(): except CloudStackException, e: module.fail_json(msg='CloudStackException: %s' % str(e)) - except Exception, e: - module.fail_json(msg='Exception: %s' % str(e)) - module.exit_json(**result) # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/cloudstack/cs_firewall.py b/cloud/cloudstack/cs_firewall.py index 96b3f20f7cf..97cf97e781e 100644 --- a/cloud/cloudstack/cs_firewall.py +++ b/cloud/cloudstack/cs_firewall.py @@ -451,11 +451,9 @@ def main(): except CloudStackException, e: module.fail_json(msg='CloudStackException: %s' % str(e)) - except Exception, e: - module.fail_json(msg='Exception: %s' % str(e)) - module.exit_json(**result) # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index f6518b85e52..79b1c58a586 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -852,11 +852,9 @@ def main(): except CloudStackException, e: module.fail_json(msg='CloudStackException: %s' % str(e)) - except Exception, e: - module.fail_json(msg='Exception: %s' % str(e)) - module.exit_json(**result) # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/cloudstack/cs_instancegroup.py b/cloud/cloudstack/cs_instancegroup.py index 478748aeec3..7280ceff5ea 100644 --- a/cloud/cloudstack/cs_instancegroup.py +++ b/cloud/cloudstack/cs_instancegroup.py @@ -223,11 +223,9 @@ def main(): except CloudStackException, e: module.fail_json(msg='CloudStackException: %s' % str(e)) - except Exception, e: - module.fail_json(msg='Exception: %s' % str(e)) - module.exit_json(**result) # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/cloudstack/cs_iso.py b/cloud/cloudstack/cs_iso.py index e3ba322f6ba..67e4b283155 100644 --- a/cloud/cloudstack/cs_iso.py +++ b/cloud/cloudstack/cs_iso.py @@ -354,11 +354,9 @@ def main(): except CloudStackException, e: module.fail_json(msg='CloudStackException: %s' % str(e)) - except Exception, e: - module.fail_json(msg='Exception: %s' % str(e)) - module.exit_json(**result) # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/cloudstack/cs_network.py b/cloud/cloudstack/cs_network.py index b602b345677..50dd2981e72 100644 --- a/cloud/cloudstack/cs_network.py +++ b/cloud/cloudstack/cs_network.py @@ -627,11 +627,9 @@ def main(): except CloudStackException, e: module.fail_json(msg='CloudStackException: %s' % str(e)) - except Exception, e: - module.fail_json(msg='Exception: %s' % str(e)) - module.exit_json(**result) # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/cloudstack/cs_portforward.py b/cloud/cloudstack/cs_portforward.py index 3b88ca85723..df95bfd3ea6 100644 --- a/cloud/cloudstack/cs_portforward.py +++ b/cloud/cloudstack/cs_portforward.py @@ -427,11 +427,9 @@ def main(): except CloudStackException, e: module.fail_json(msg='CloudStackException: %s' % str(e)) - except Exception, e: - module.fail_json(msg='Exception: %s' % str(e)) - module.exit_json(**result) # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/cloudstack/cs_project.py b/cloud/cloudstack/cs_project.py index 0f391bc5005..f09c42f5899 100644 --- a/cloud/cloudstack/cs_project.py +++ b/cloud/cloudstack/cs_project.py @@ -332,11 +332,9 @@ def main(): except CloudStackException, e: module.fail_json(msg='CloudStackException: %s' % str(e)) - except Exception, e: - module.fail_json(msg='Exception: %s' % str(e)) - module.exit_json(**result) # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/cloudstack/cs_securitygroup.py b/cloud/cloudstack/cs_securitygroup.py index 54a71686a6e..a6827f6f811 100644 --- a/cloud/cloudstack/cs_securitygroup.py +++ b/cloud/cloudstack/cs_securitygroup.py @@ -190,11 +190,9 @@ def main(): except CloudStackException, e: module.fail_json(msg='CloudStackException: %s' % str(e)) - except Exception, e: - module.fail_json(msg='Exception: %s' % str(e)) - module.exit_json(**result) # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/cloudstack/cs_securitygroup_rule.py b/cloud/cloudstack/cs_securitygroup_rule.py index e943e7d11c2..0780e12d70d 100644 --- a/cloud/cloudstack/cs_securitygroup_rule.py +++ b/cloud/cloudstack/cs_securitygroup_rule.py @@ -429,11 +429,9 @@ def main(): except CloudStackException, e: module.fail_json(msg='CloudStackException: %s' % str(e)) - except Exception, e: - module.fail_json(msg='Exception: %s' % str(e)) - module.exit_json(**result) # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/cloudstack/cs_sshkeypair.py b/cloud/cloudstack/cs_sshkeypair.py index 180e96ca6ae..28c6b3802b4 100644 --- a/cloud/cloudstack/cs_sshkeypair.py +++ b/cloud/cloudstack/cs_sshkeypair.py @@ -249,11 +249,9 @@ def main(): except CloudStackException, e: module.fail_json(msg='CloudStackException: %s' % str(e)) - except Exception, e: - module.fail_json(msg='Exception: %s' % str(e)) - module.exit_json(**result) # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/cloudstack/cs_template.py b/cloud/cloudstack/cs_template.py index 1cd245d2b5c..8e56aafaa7e 100644 --- a/cloud/cloudstack/cs_template.py +++ b/cloud/cloudstack/cs_template.py @@ -623,11 +623,9 @@ def main(): except CloudStackException, e: module.fail_json(msg='CloudStackException: %s' % str(e)) - except Exception, e: - module.fail_json(msg='Exception: %s' % str(e)) - module.exit_json(**result) # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/cloudstack/cs_vmsnapshot.py b/cloud/cloudstack/cs_vmsnapshot.py index 24e8a46fa37..62dec7ca35d 100644 --- a/cloud/cloudstack/cs_vmsnapshot.py +++ b/cloud/cloudstack/cs_vmsnapshot.py @@ -317,11 +317,9 @@ def main(): except CloudStackException, e: module.fail_json(msg='CloudStackException: %s' % str(e)) - except Exception, e: - module.fail_json(msg='Exception: %s' % str(e)) - module.exit_json(**result) # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() From 0dc2fb73d3b2d0715695913ffa28ee81bea6eb3b Mon Sep 17 00:00:00 2001 From: Christopher Troup Date: Sun, 28 Jun 2015 11:09:44 -0400 Subject: [PATCH 0326/2522] Add GPL file header --- cloud/amazon/route53_zone.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/cloud/amazon/route53_zone.py b/cloud/amazon/route53_zone.py index 2383563fafe..07a049b14f7 100644 --- a/cloud/amazon/route53_zone.py +++ b/cloud/amazon/route53_zone.py @@ -1,4 +1,18 @@ #!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . DOCUMENTATION = ''' module: route53_zone From 3f395861a0543cfbe3cb138a7ecdbd24e6dffc5d Mon Sep 17 00:00:00 2001 From: Paul Markham Date: Mon, 29 Jun 2015 11:00:30 +1000 Subject: [PATCH 0327/2522] Changed 'whole_root' option to 'sparse'. Added state='started' as synonym for state='running'. --- system/solaris_zone.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/system/solaris_zone.py b/system/solaris_zone.py index be693bf1425..edd078de812 100644 --- a/system/solaris_zone.py +++ b/system/solaris_zone.py @@ -40,9 +40,10 @@ - C(present), create the zone. - C(running), if the zone already exists, boot it, otherwise, create the zone first, then boot it. + - C(started), synonym for C(running). - C(stopped), shutdown a zone. - C(absent), destroy the zone. - choices: ['present', 'running', 'stopped', 'absent'] + choices: ['present', 'started', 'running', 'stopped', 'absent'] name: description: - Zone name. @@ -53,11 +54,11 @@ used otherwise. required: false default: null - whole_root: + sparse: description: - - Whether to create a whole root (C(true)) or sparse root (C(false)) zone. + - Whether to create a sparse (C(true)) or whole root (C(false)) zone. required: false - default: true + default: false root_password: description: - The password hash for the root account. If not specified, the zone's root account @@ -89,11 +90,11 @@ EXAMPLES = ''' # Create a zone, but don't boot it -solaris_zone: name=zone1 state=present path=/zones/zone1 whole_root=true root_password="Be9oX7OSwWoU." +solaris_zone: name=zone1 state=present path=/zones/zone1 sparse=true root_password="Be9oX7OSwWoU." config='set autoboot=true, add net, set physical=bge0, set address=10.1.1.1, end' # Create a zone and boot it -solaris_zone: name=zone1 state=running path=/zones/zone1 whole_root=true root_password="Be9oX7OSwWoU." +solaris_zone: name=zone1 state=running path=/zones/zone1 root_password="Be9oX7OSwWoU." config='set autoboot=true, add net, set physical=bge0, set address=10.1.1.1, end' # Boot an already created zone @@ -114,7 +115,7 @@ def __init__(self, module): self.module = module self.path = self.module.params['path'] self.name = self.module.params['name'] - self.whole_root = self.module.params['whole_root'] + self.sparse = self.module.params['sparse'] self.root_password = self.module.params['root_password'] self.timeout = self.module.params['timeout'] self.config = self.module.params['config'] @@ -130,12 +131,12 @@ def create(self): t = tempfile.NamedTemporaryFile(delete = False) - if self.whole_root: - t.write('create -b %s\n' % self.create_options) - self.msg.append('creating whole root zone') - else: + if self.sparse: t.write('create %s\n' % self.create_options) self.msg.append('creating sparse root zone') + else: + t.write('create -b %s\n' % self.create_options) + self.msg.append('creating whole root zone') t.write('set zonepath=%s\n' % self.path) @@ -321,9 +322,9 @@ def main(): module = AnsibleModule( argument_spec = dict( name = dict(required=True), - state = dict(required=True, choices=['running', 'present', 'stopped', 'absent']), + state = dict(default=None, choices=['running', 'started', 'present', 'stopped', 'absent']), path = dict(defalt=None), - whole_root = dict(default=True, type='bool'), + sparse = dict(default=False, type='bool'), root_password = dict(default=None), timeout = dict(default=600, type='int'), config = dict(default=None, type='list'), @@ -343,7 +344,7 @@ def main(): state = module.params['state'] - if state == 'running': + if state == 'running' or state == 'started': zone.state_running() elif state == 'present': zone.state_present() From 219ec18b335d5e986a8fa2edf9b29e285083fc45 Mon Sep 17 00:00:00 2001 From: Paul Markham Date: Mon, 29 Jun 2015 11:19:36 +1000 Subject: [PATCH 0328/2522] Change state back to a required parameter --- system/solaris_zone.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/solaris_zone.py b/system/solaris_zone.py index edd078de812..bac448fcb8d 100644 --- a/system/solaris_zone.py +++ b/system/solaris_zone.py @@ -322,7 +322,7 @@ def main(): module = AnsibleModule( argument_spec = dict( name = dict(required=True), - state = dict(default=None, choices=['running', 'started', 'present', 'stopped', 'absent']), + state = dict(required=True, choices=['running', 'started', 'present', 'stopped', 'absent']), path = dict(defalt=None), sparse = dict(default=False, type='bool'), root_password = dict(default=None), From eb44a5b6b88cfeaf1cfe1feb24ba616fff908b79 Mon Sep 17 00:00:00 2001 From: Paul Markham Date: Mon, 29 Jun 2015 12:43:14 +1000 Subject: [PATCH 0329/2522] Handle case where .UNFONFIGURE file isn't there --- system/solaris_zone.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system/solaris_zone.py b/system/solaris_zone.py index bac448fcb8d..54ab86eee20 100644 --- a/system/solaris_zone.py +++ b/system/solaris_zone.py @@ -161,7 +161,8 @@ def create(self): self.configure_ssh_keys() def configure_sysid(self): - os.unlink('%s/root/etc/.UNCONFIGURED' % self.path) + if os.path.isfile('%s/root/etc/.UNCONFIGURED' % self.path): + os.unlink('%s/root/etc/.UNCONFIGURED' % self.path) open('%s/root/noautoshutdown' % self.path, 'w').close() From 155c9ff6c9b5be7f38fbd988b1132b9387baf3c2 Mon Sep 17 00:00:00 2001 From: whiter Date: Mon, 29 Jun 2015 11:18:04 +1000 Subject: [PATCH 0330/2522] Fixed incorrect connect_to_aws call - switched to boto.vpc instead of boto.ec2 --- cloud/amazon/ec2_vpc_igw.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/cloud/amazon/ec2_vpc_igw.py b/cloud/amazon/ec2_vpc_igw.py index 63be48248ef..499171ede54 100644 --- a/cloud/amazon/ec2_vpc_igw.py +++ b/cloud/amazon/ec2_vpc_igw.py @@ -20,18 +20,25 @@ description: - Manage an AWS VPC Internet gateway version_added: "2.0" -author: Robert Estelle, @erydo +author: Robert Estelle (@erydo) options: vpc_id: description: - The VPC ID for the VPC in which to manage the Internet Gateway. required: true default: null + region: + description: + - The AWS region to use. Must be specified if ec2_url is not used. If not specified then the value of the EC2_REGION environment variable, if any, is used. See U(http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region) + required: false + default: null + aliases: [ 'aws_region', 'ec2_region' ] state: description: - Create or terminate the IGW required: false default: present + choices: [ 'present', 'absent' ] extends_documentation_fragment: aws ''' @@ -39,16 +46,13 @@ # Note: These examples do not set authentication details, see the AWS Guide for details. # Ensure that the VPC has an Internet Gateway. -# The Internet Gateway ID is can be accessed via {{igw.gateway_id}} for use -# in setting up NATs etc. - local_action: - module: ec2_vpc_igw - vpc_id: {{vpc.vpc_id}} - region: {{vpc.vpc.region}} - state: present - register: igw -''' +# The Internet Gateway ID is can be accessed via {{igw.gateway_id}} for use in setting up NATs etc. +module: ec2_vpc_igw + vpc_id: vpc-abcdefgh + state: present +register: igw +''' import sys # noqa @@ -116,8 +120,8 @@ def main(): argument_spec = ec2_argument_spec() argument_spec.update( dict( - vpc_id = dict(required=True), - state = dict(choices=['present', 'absent'], default='present') + vpc_id = dict(required=True, default=None), + state = dict(required=False, default='present', choices=['present', 'absent']) ) ) @@ -133,14 +137,14 @@ def main(): if region: try: - connection = connect_to_aws(boto.ec2, region, **aws_connect_params) + connection = connect_to_aws(boto.vpc, region, **aws_connect_params) except (boto.exception.NoAuthHandlerFound, StandardError), e: module.fail_json(msg=str(e)) else: module.fail_json(msg="region must be specified") vpc_id = module.params.get('vpc_id') - state = module.params.get('state', 'present') + state = module.params.get('state') try: if state == 'present': From c7d554677736566d8aace3632e84c04ba744bbd9 Mon Sep 17 00:00:00 2001 From: Patrik Lundin Date: Mon, 29 Jun 2015 09:27:44 +0200 Subject: [PATCH 0331/2522] openbsd_pkg: Update author mail address. --- packaging/os/openbsd_pkg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/os/openbsd_pkg.py b/packaging/os/openbsd_pkg.py index 1b5d0bb06b2..1f331261d98 100644 --- a/packaging/os/openbsd_pkg.py +++ b/packaging/os/openbsd_pkg.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# (c) 2013, Patrik Lundin +# (c) 2013, Patrik Lundin # # This file is part of Ansible # From 90d0828b1a6a40ac60b028a4a0c3cec50014692f Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Mon, 29 Jun 2015 11:58:56 +0200 Subject: [PATCH 0332/2522] cloudstack: fix user_data gathering, must not be in for loop --- cloud/cloudstack/cs_facts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/cloudstack/cs_facts.py b/cloud/cloudstack/cs_facts.py index f8749834120..e2bebf8b116 100644 --- a/cloud/cloudstack/cs_facts.py +++ b/cloud/cloudstack/cs_facts.py @@ -130,7 +130,7 @@ def run(self): if not filter: for key,path in self.fact_paths.iteritems(): result[key] = self._fetch(CS_METADATA_BASE_URL + "/" + path) - result['cloudstack_user_data'] = self._get_user_data_json() + result['cloudstack_user_data'] = self._get_user_data_json() else: if filter == 'cloudstack_user_data': result['cloudstack_user_data'] = self._get_user_data_json() From 1934ea6f35f6f80bbf9491ffcff6c8e473244604 Mon Sep 17 00:00:00 2001 From: Rob White Date: Mon, 29 Jun 2015 20:41:13 +1000 Subject: [PATCH 0333/2522] New module - s3_bucket --- cloud/amazon/s3_bucket.py | 392 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 392 insertions(+) create mode 100644 cloud/amazon/s3_bucket.py diff --git a/cloud/amazon/s3_bucket.py b/cloud/amazon/s3_bucket.py new file mode 100644 index 00000000000..7a4bcd01607 --- /dev/null +++ b/cloud/amazon/s3_bucket.py @@ -0,0 +1,392 @@ +#!/usr/bin/python +# +# This is a free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This Ansible library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this library. If not, see . + +DOCUMENTATION = ''' +--- +module: s3_bucket +short_description: Manage s3 buckets in AWS +description: + - Manage s3 buckets in AWS +version_added: "2.0" +author: Rob White (@wimnat) +options: + force: + description: + - When trying to delete a bucket, delete all keys in the bucket first (an s3 bucket must be empty for a successful deletion) + required: false + default: no + choices: [ 'yes', 'no' ] + name: + description: + - Name of the s3 bucket + required: true + default: null + policy: + description: + - The JSON policy as a string. + required: false + default: null + region: + description: + - AWS region to create the bucket in. If not set then the value of the AWS_REGION and EC2_REGION environment variables are checked, followed by the aws_region and ec2_region settings in the Boto config file. If none of those are set the region defaults to the S3 Location: US Standard. + required: false + default: null + s3_url: + description: S3 URL endpoint for usage with Eucalypus, fakes3, etc. Otherwise assumes AWS + default: null + aliases: [ S3_URL ] + requester_pays: + description: + - With Requester Pays buckets, the requester instead of the bucket owner pays the cost of the request and the data download from the bucket. + required: false + default: no + choices: [ 'yes', 'no' ] + state: + description: + - Create or remove the s3 bucket + required: false + default: present + choices: [ 'present', 'absent' ] + tags: + description: + - tags dict to apply to bucket + required: false + default: null + versioning: + description: + - Whether versioning is enabled or disabled (note that once versioning is enabled, it can only be suspended) + required: false + default: no + choices: [ 'yes', 'no' ] + +extends_documentation_fragment: aws +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Create a simple s3 bucket +- s3_bucket: + name: mys3bucket + +# Remove an s3 bucket and any keys it contains +- s3_bucket: + name: mys3bucket + state: absent + force: yes + +# Create a bucket, add a policy from a file, enable requester pays, enable versioning and tag +- s3_bucket: + name: mys3bucket + policy: "{{ lookup('file','policy.json') }}" + requester_pays: yes + versioning: yes + tags: + example: tag1 + another: tag2 + +''' + +import xml.etree.ElementTree as ET + +try: + import boto.ec2 + from boto.s3.connection import OrdinaryCallingFormat + from boto.s3.tagging import Tags, TagSet + from boto.exception import BotoServerError, S3CreateError, S3ResponseError + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +def get_request_payment_status(bucket): + + response = bucket.get_request_payment() + root = ET.fromstring(response) + for message in root.findall('.//{http://s3.amazonaws.com/doc/2006-03-01/}Payer'): + payer = message.text + + if payer == "BucketOwner": + return False + else: + return True + +def create_tags_container(tags): + + tag_set = TagSet() + tags_obj = Tags() + for key, val in tags.iteritems(): + tag_set.add_tag(key, val) + + tags_obj.add_tag_set(tag_set) + return tags_obj + +def create_bucket(connection, module): + + policy = module.params.get("policy") + name = module.params.get("name") + region = module.params.get("region") + requester_pays = module.params.get("requester_pays") + tags = module.params.get("tags") + versioning = module.params.get("versioning") + changed = False + + try: + bucket = connection.get_bucket(name) + except S3ResponseError, e: + try: + bucket = connection.create_bucket(name, location=region) + changed = True + except S3CreateError, e: + module.fail_json(msg=e.message) + + # Versioning + versioning_status = bucket.get_versioning_status() + if not versioning_status and versioning: + try: + bucket.configure_versioning(versioning) + changed = True + versioning_status = bucket.get_versioning_status() + except S3ResponseError, e: + module.fail_json(msg=e.message) + elif not versioning_status and not versioning: + # do nothing + pass + else: + if versioning_status['Versioning'] == "Enabled" and not versioning: + bucket.configure_versioning(versioning) + changed = True + versioning_status = bucket.get_versioning_status() + elif ( (versioning_status['Versioning'] == "Disabled" and versioning) or (versioning_status['Versioning'] == "Suspended" and versioning) ): + bucket.configure_versioning(versioning) + changed = True + versioning_status = bucket.get_versioning_status() + + # Requester pays + requester_pays_status = get_request_payment_status(bucket) + if requester_pays_status != requester_pays: + if requester_pays: + bucket.set_request_payment(payer='Requester') + changed = True + requester_pays_status = get_request_payment_status(bucket) + else: + bucket.set_request_payment(payer='BucketOwner') + changed = True + requester_pays_status = get_request_payment_status(bucket) + + # Policy + try: + current_policy = bucket.get_policy() + except S3ResponseError, e: + if e.error_code == "NoSuchBucketPolicy": + current_policy = None + else: + module.fail_json(msg=e.message) + + if current_policy is not None and policy is not None: + if policy is not None: + policy = json.dumps(policy) + + if json.loads(current_policy) != json.loads(policy): + try: + bucket.set_policy(policy) + changed = True + current_policy = bucket.get_policy() + except S3ResponseError, e: + module.fail_json(msg=e.message) + + elif current_policy is None and policy is not None: + policy = json.dumps(policy) + + try: + bucket.set_policy(policy) + changed = True + current_policy = bucket.get_policy() + except S3ResponseError, e: + module.fail_json(msg=e.message) + + elif current_policy is not None and policy is None: + try: + bucket.delete_policy() + changed = True + current_policy = bucket.get_policy() + except S3ResponseError, e: + if e.error_code == "NoSuchBucketPolicy": + current_policy = None + else: + module.fail_json(msg=e.message) + + #### + ## Fix up json of policy so it's not escaped + #### + + # Tags + try: + current_tags = bucket.get_tags() + tag_set = TagSet() + except S3ResponseError, e: + if e.error_code == "NoSuchTagSet": + current_tags = None + else: + module.fail_json(msg=e.message) + + if current_tags is not None or tags is not None: + + if current_tags is None: + current_tags_dict = {} + else: + current_tags_dict = dict((t.key, t.value) for t in current_tags[0]) + + if sorted(current_tags_dict) != sorted(tags): + try: + if tags: + bucket.set_tags(create_tags_container(tags)) + else: + bucket.delete_tags() + current_tags_dict = tags + changed = True + except S3ResponseError, e: + module.fail_json(msg=e.message) + + module.exit_json(changed=changed, name=bucket.name, versioning=versioning_status, requester_pays=requester_pays_status, policy=current_policy, tags=current_tags_dict) + +def destroy_bucket(connection, module): + + force = module.params.get("force") + name = module.params.get("name") + changed = False + + try: + bucket = connection.get_bucket(name) + except S3ResponseError, e: + if e.error_code != "NoSuchBucket": + module.fail_json(msg=e.message) + else: + # Bucket already absent + module.exit_json(changed=changed) + + if force: + try: + # Empty the bucket + for key in bucket.list(): + key.delete() + + except BotoServerError, e: + module.fail_json(msg=e.message) + + try: + bucket = connection.delete_bucket(name) + changed = True + except S3ResponseError, e: + module.fail_json(msg=e.message) + + module.exit_json(changed=changed) + +def is_fakes3(s3_url): + """ Return True if s3_url has scheme fakes3:// """ + if s3_url is not None: + return urlparse.urlparse(s3_url).scheme in ('fakes3', 'fakes3s') + else: + return False + +def is_walrus(s3_url): + """ Return True if it's Walrus endpoint, not S3 + + We assume anything other than *.amazonaws.com is Walrus""" + if s3_url is not None: + o = urlparse.urlparse(s3_url) + return not o.hostname.endswith('amazonaws.com') + else: + return False + +def main(): + + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + force = dict(required=False, default='no', type='bool'), + policy = dict(required=False, default=None), + name = dict(required=True), + requester_pays = dict(default='no', type='bool'), + s3_url = dict(aliases=['S3_URL']), + state = dict(default='present', choices=['present', 'absent']), + tags = dict(required=None, default={}, type='dict'), + versioning = dict(default='no', type='bool') + ) + ) + + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO: + module.fail_json(msg='boto required for this module') + + region, ec2_url, aws_connect_params = get_aws_connection_info(module) + + if region in ('us-east-1', '', None): + # S3ism for the US Standard region + location = Location.DEFAULT + else: + # Boto uses symbolic names for locations but region strings will + # actually work fine for everything except us-east-1 (US Standard) + location = region + + s3_url = module.params.get('s3_url') + + # allow eucarc environment variables to be used if ansible vars aren't set + if not s3_url and 'S3_URL' in os.environ: + s3_url = os.environ['S3_URL'] + + # Look at s3_url and tweak connection settings + # if connecting to Walrus or fakes3 + try: + if is_fakes3(s3_url): + fakes3 = urlparse.urlparse(s3_url) + connection = S3Connection( + is_secure=fakes3.scheme == 'fakes3s', + host=fakes3.hostname, + port=fakes3.port, + calling_format=OrdinaryCallingFormat(), + **aws_connect_params + ) + elif is_walrus(s3_url): + walrus = urlparse.urlparse(s3_url).hostname + connection = boto.connect_walrus(walrus, **aws_connect_params) + else: + connection = boto.s3.connect_to_region(location, is_secure=True, calling_format=OrdinaryCallingFormat(), **aws_connect_params) + # use this as fallback because connect_to_region seems to fail in boto + non 'classic' aws accounts in some cases + if connection is None: + connection = boto.connect_s3(**aws_connect_params) + + except boto.exception.NoAuthHandlerFound, e: + module.fail_json(msg='No Authentication Handler found: %s ' % str(e)) + except Exception, e: + module.fail_json(msg='Failed to connect to S3: %s' % str(e)) + + if connection is None: # this should never happen + module.fail_json(msg ='Unknown error, failed to create s3 connection, no information from boto.') + + state = module.params.get("state") + + if state == 'present': + create_bucket(connection, module) + elif state == 'absent': + destroy_bucket(connection, module) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +# this is magic, see lib/ansible/module_common.py +#<> + +main() From 9e8802cacd26617efbab32f26505158a6e2d64fc Mon Sep 17 00:00:00 2001 From: Alan Loi Date: Mon, 29 Jun 2015 20:45:53 +1000 Subject: [PATCH 0334/2522] Docfixes - add version_added and default values. --- cloud/amazon/dynamodb_table.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cloud/amazon/dynamodb_table.py b/cloud/amazon/dynamodb_table.py index 130fae44721..f3ba7d7e77c 100644 --- a/cloud/amazon/dynamodb_table.py +++ b/cloud/amazon/dynamodb_table.py @@ -22,6 +22,7 @@ - Create or delete AWS Dynamo DB tables. - Can update the provisioned throughput on existing tables. - Returns the status of the specified table. +version_added: "2.0" author: Alan Loi (@loia) requirements: - "boto >= 2.13.2" @@ -41,6 +42,7 @@ - Name of the hash key. - Required when C(state=present). required: false + default: null hash_key_type: description: - Type of the hash key. @@ -51,6 +53,7 @@ description: - Name of the range key. required: false + default: null range_key_type: description: - Type of the range key. From 556e95379ed57f383658e106382ca482aab9e587 Mon Sep 17 00:00:00 2001 From: Rob White Date: Mon, 29 Jun 2015 20:53:40 +1000 Subject: [PATCH 0335/2522] Doc fix up --- cloud/amazon/ec2_vpc_igw.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/ec2_vpc_igw.py b/cloud/amazon/ec2_vpc_igw.py index 499171ede54..71c2519dc4e 100644 --- a/cloud/amazon/ec2_vpc_igw.py +++ b/cloud/amazon/ec2_vpc_igw.py @@ -29,7 +29,7 @@ default: null region: description: - - The AWS region to use. Must be specified if ec2_url is not used. If not specified then the value of the EC2_REGION environment variable, if any, is used. See U(http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region) + - The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used. See U(http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region) required: false default: null aliases: [ 'aws_region', 'ec2_region' ] @@ -47,7 +47,7 @@ # Ensure that the VPC has an Internet Gateway. # The Internet Gateway ID is can be accessed via {{igw.gateway_id}} for use in setting up NATs etc. -module: ec2_vpc_igw +ec2_vpc_igw: vpc_id: vpc-abcdefgh state: present register: igw From c7f0fafe62c4cb08762dbffa2dbe01921123549b Mon Sep 17 00:00:00 2001 From: Alan Loi Date: Mon, 29 Jun 2015 20:55:33 +1000 Subject: [PATCH 0336/2522] Check AWS region and credentials are provided. --- cloud/amazon/dynamodb_table.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/cloud/amazon/dynamodb_table.py b/cloud/amazon/dynamodb_table.py index f3ba7d7e77c..4b29cfbfaa9 100644 --- a/cloud/amazon/dynamodb_table.py +++ b/cloud/amazon/dynamodb_table.py @@ -119,7 +119,7 @@ from boto.dynamodb2.table import Table from boto.dynamodb2.fields import HashKey, RangeKey from boto.dynamodb2.types import STRING, NUMBER, BINARY - from boto.exception import BotoServerError, JSONResponseError + from boto.exception import BotoServerError, NoAuthHandlerFound, JSONResponseError HAS_BOTO = True except ImportError: @@ -261,7 +261,14 @@ def main(): module.fail_json(msg='boto required for this module') region, ec2_url, aws_connect_params = get_aws_connection_info(module) - connection = connect_to_aws(boto.dynamodb2, region, **aws_connect_params) + if not region: + module.fail_json(msg='region must be specified') + + try: + connection = connect_to_aws(boto.dynamodb2, region, **aws_connect_params) + + except (NoAuthHandlerFound, StandardError), e: + module.fail_json(msg=str(e)) state = module.params.get('state') if state == 'present': @@ -274,4 +281,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * -main() +if __name__ == '__main__': + main() From 86fda85ba38f45d9f274fb0af73d3f291a6e5be3 Mon Sep 17 00:00:00 2001 From: Timothy Vandenbrande Date: Mon, 29 Jun 2015 14:18:09 +0200 Subject: [PATCH 0337/2522] updated version added for source into the docs --- system/firewalld.py | 1 + 1 file changed, 1 insertion(+) diff --git a/system/firewalld.py b/system/firewalld.py index 0348c6ecb47..677ced8aa78 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -46,6 +46,7 @@ - 'The source/network you would like to add/remove to/from firewalld' required: false default: null + version_added: "2.0" zone: description: - 'The firewalld zone to add/remove to/from (NOTE: default zone can be configured per system but "public" is default from upstream. Available choices can be extended based on per-system configs, listed here are "out of the box" defaults).' From f14317f7f54e7cc873f284c1ea82927b6bd45820 Mon Sep 17 00:00:00 2001 From: tylerturk Date: Mon, 29 Jun 2015 07:51:58 -0500 Subject: [PATCH 0338/2522] Fix documentation bug --- system/gluster_volume.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/gluster_volume.py b/system/gluster_volume.py index 7719006502d..ff1ce9831db 100644 --- a/system/gluster_volume.py +++ b/system/gluster_volume.py @@ -115,7 +115,7 @@ gluster_volume: state=present name=test1 options='{performance.cache-size: 256MB}' - name: start gluster volume - gluster_volume: status=started name=test1 + gluster_volume: state=started name=test1 - name: limit usage gluster_volume: state=present name=test1 directory=/foo quota=20.0MB From 57e7a6662a3d5cca7ebed01539b9730941ef6a4b Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Mon, 29 Jun 2015 17:08:48 +0200 Subject: [PATCH 0339/2522] Work around a software bug in vSphere Due to a software bug in vSphere, it fails to handle ampersand in datacenter names. The solution is to do what vSphere does (when browsing) and double-encode ampersands. It is likely other characters need special treatment like this as well, haven't found any. --- cloud/vmware/vsphere_copy.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cloud/vmware/vsphere_copy.py b/cloud/vmware/vsphere_copy.py index 7c044a7d51a..44e20caebdf 100644 --- a/cloud/vmware/vsphere_copy.py +++ b/cloud/vmware/vsphere_copy.py @@ -78,6 +78,9 @@ def vmware_path(datastore, datacenter, path): ''' Constructs a URL path that VSphere accepts reliably ''' path = "/folder/%s" % path.lstrip("/") + # Due to a software bug in vSphere, it fails to handle ampersand in datacenter names + # The solution is to do what vSphere does (when browsing) and double-encode ampersands, maybe others ? + datacenter = datacenter.replace('&', '%26') if not path.startswith("/"): path = "/" + path params = dict( dsName = datastore ) From 86d5ca411c2e8d770515b544602c378a39ac7471 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 29 Jun 2015 13:09:11 -0700 Subject: [PATCH 0340/2522] Add testing documentation to travis --- .travis.yml | 1 + test-docs.sh | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100755 test-docs.sh diff --git a/.travis.yml b/.travis.yml index 84ec3a0983a..d43c6b3b3fa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,3 +13,4 @@ script: - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/layman\.py|/maven_artifact\.py|clustering/consul.*\.py|notification/pushbullet\.py' . - python2.6 -m compileall -fq . - python2.7 -m compileall -fq . + - ./test-docs.sh extras diff --git a/test-docs.sh b/test-docs.sh new file mode 100755 index 00000000000..76297fbada6 --- /dev/null +++ b/test-docs.sh @@ -0,0 +1,21 @@ +#!/bin/sh +set -x + +CHECKOUT_DIR=".ansible-checkout" +MOD_REPO="$1" + +# Hidden file to avoid the module_formatter recursing into the checkout +git clone https://github.com/ansible/ansible "$CHECKOUT_DIR" +cd "$CHECKOUT_DIR" +git submodule update --init +rm -rf "lib/ansible/modules/$MOD_REPO" +ln -s "$TRAVIS_BUILD_DIR/" "lib/ansible/modules/$MOD_REPO" + +pip install -U Jinja2 PyYAML setuptools six pycrypto sphinx + +. ./hacking/env-setup +PAGER=/bin/cat bin/ansible-doc -l +if [ $? -ne 0 ] ; then + exit $? +fi +make -C docsite From ada6d0a18c5fff6cf454c41810c5e4b2e96f6231 Mon Sep 17 00:00:00 2001 From: Timothy Vandenbrande Date: Tue, 30 Jun 2015 08:39:19 +0200 Subject: [PATCH 0341/2522] added the profile option + updated the docs for it --- windows/win_fw.ps1 | 10 +++++++++- windows/win_fw.py | 7 ++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/windows/win_fw.ps1 b/windows/win_fw.ps1 index 37e8935a23d..fcdf34799fc 100644 --- a/windows/win_fw.ps1 +++ b/windows/win_fw.ps1 @@ -202,7 +202,7 @@ if ((($direction.ToLower() -ne "In") -And ($direction.ToLower() -ne "Out")) -And $fwsettings.Add("Direction", $direction) }; if ((($action.ToLower() -ne "allow") -And ($action.ToLower() -ne "block")) -And ($state -eq "present")){ - $misArg+="Direction"; + $misArg+="Action"; $msg+=@("for the Action parameter only the values 'allow' and 'block' are allowed"); } else { $fwsettings.Add("Action", $action) @@ -225,6 +225,14 @@ foreach ($arg in $args){ }; }; +$profile=Get-Attr $params "profile" "all"; +if (($profile -ne 'current') -or ($profile -ne 'domain') -or ($profile -ne 'standard') -or ($profile -ne 'all') ) { + $misArg+="Profile"; + $msg+=@("for the Profile parameter only the values 'current', 'domain', 'standard' or 'all' are allowed"); +} else { + + $fwsettings.Add("profile", $profile) +} if ($($($misArg|measure).count) -gt 0){ $result=New-Object psobject @{ diff --git a/windows/win_fw.py b/windows/win_fw.py index 0566ef0608d..59e1918ff58 100644 --- a/windows/win_fw.py +++ b/windows/win_fw.py @@ -88,6 +88,11 @@ - the protocol this rule applies to default: null required: false + profile: + describtion: + - the profile this rule applies to + default: all + choices: ['current', 'domain', 'standard', 'all'] force: description: - Enforces the change if a rule with different values exists @@ -107,4 +112,4 @@ action: allow protocol: TCP -''' \ No newline at end of file +''' From 2a0df8ec04622de7e1f3d2e062cd95194540b42d Mon Sep 17 00:00:00 2001 From: Timothy Vandenbrande Date: Tue, 30 Jun 2015 08:42:42 +0200 Subject: [PATCH 0342/2522] renamed the module --- windows/{win_fw.ps1 => win_firewall_rule.ps1} | 0 windows/{win_fw.py => win_firewall_rule.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename windows/{win_fw.ps1 => win_firewall_rule.ps1} (100%) rename windows/{win_fw.py => win_firewall_rule.py} (100%) diff --git a/windows/win_fw.ps1 b/windows/win_firewall_rule.ps1 similarity index 100% rename from windows/win_fw.ps1 rename to windows/win_firewall_rule.ps1 diff --git a/windows/win_fw.py b/windows/win_firewall_rule.py similarity index 100% rename from windows/win_fw.py rename to windows/win_firewall_rule.py From 97d8273558ebb7b738146d3b678ad3b875509b21 Mon Sep 17 00:00:00 2001 From: Timothy Vandenbrande Date: Tue, 30 Jun 2015 08:49:47 +0200 Subject: [PATCH 0343/2522] windows default to current instead of all --- windows/win_firewall_rule.ps1 | 2 +- windows/win_firewall_rule.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/windows/win_firewall_rule.ps1 b/windows/win_firewall_rule.ps1 index fcdf34799fc..832d68d8f9c 100644 --- a/windows/win_firewall_rule.ps1 +++ b/windows/win_firewall_rule.ps1 @@ -225,7 +225,7 @@ foreach ($arg in $args){ }; }; -$profile=Get-Attr $params "profile" "all"; +$profile=Get-Attr $params "profile" "current"; if (($profile -ne 'current') -or ($profile -ne 'domain') -or ($profile -ne 'standard') -or ($profile -ne 'all') ) { $misArg+="Profile"; $msg+=@("for the Profile parameter only the values 'current', 'domain', 'standard' or 'all' are allowed"); diff --git a/windows/win_firewall_rule.py b/windows/win_firewall_rule.py index 59e1918ff58..cb167d0f4fa 100644 --- a/windows/win_firewall_rule.py +++ b/windows/win_firewall_rule.py @@ -91,7 +91,7 @@ profile: describtion: - the profile this rule applies to - default: all + default: current choices: ['current', 'domain', 'standard', 'all'] force: description: From d6339c47e4b52f64e310ca1540432bd7966f20a8 Mon Sep 17 00:00:00 2001 From: Michael Perzel Date: Tue, 30 Jun 2015 12:06:16 -0500 Subject: [PATCH 0344/2522] Ability to add/remove scheduled task --- windows/win_scheduled_task.ps1 | 137 +++++++++++++++++++++++++++------ windows/win_scheduled_task.py | 38 +++++++-- 2 files changed, 145 insertions(+), 30 deletions(-) diff --git a/windows/win_scheduled_task.ps1 b/windows/win_scheduled_task.ps1 index 2f802f59cd0..07b1c3adf60 100644 --- a/windows/win_scheduled_task.ps1 +++ b/windows/win_scheduled_task.ps1 @@ -33,42 +33,135 @@ else { Fail-Json $result "missing required argument: name" } +if ($params.state) +{ + $state = $params.state +} +else +{ + Fail-Json $result "missing required argument: state" +} if ($params.enabled) { $enabled = $params.enabled | ConvertTo-Bool } else { - $enabled = $true + $enabled = $true #default +} +if ($params.description) +{ + $description = $params.description +} +else +{ + $description = " " #default +} +if ($params.execute) +{ + $execute = $params.execute +} +elseif ($state -eq "present") +{ + Fail-Json $result "missing required argument: execute" +} +if ($params.path) +{ + $path = "\{0}\" -f $params.path +} +else +{ + $path = "\" #default +} +if ($params.frequency) +{ + $frequency = $params.frequency +} +elseif($state -eq "present") +{ + Fail-Json $result "missing required argument: frequency" +} +if ($params.time) +{ + $time = $params.time +} +elseif($state -eq "present") +{ + Fail-Json $result "missing required argument: time" +} + +$exists = $true +#hack to determine if task exists +try { + $task = Get-ScheduledTask -TaskName $name -TaskPath $path +} +catch { + $exists = $false | ConvertTo-Bool } -$target_state = @{$true = "Enabled"; $false="Disabled"}[$enabled] +Set-Attr $result "exists" "$exists" + try { - $tasks = Get-ScheduledTask -TaskPath $name - $tasks_needing_changing = $tasks |? { $_.State -ne $target_state } - if (-not($tasks_needing_changing -eq $null)) - { - if ($enabled) - { - $tasks_needing_changing | Enable-ScheduledTask + if ($frequency){ + if ($frequency -eq "daily") { + $trigger = New-ScheduledTaskTrigger -Daily -At $time + } + elseif (frequency -eq "weekly"){ + $trigger = New-ScheduledTaskTrigger -Weekly -At $time + } + else { + Fail-Json $result "frequency must be daily or weekly" + } + } + + if ($state -eq "absent" -and $exists -eq $true) { + Unregister-ScheduledTask -TaskName $name -Confirm:$false + $result.changed = $true + Set-Attr $result "msg" "Deleted task $name" + Exit-Json $result + } + elseif ($state -eq "absent" -and $exists -eq $false) { + Set-Attr $result "msg" "Task $name does not exist" + Exit-Json $result + } + + if ($state -eq "present" -and $exists -eq $false){ + $action = New-ScheduledTaskAction -Execute $execute + Register-ScheduledTask -Action $action -Trigger $trigger -TaskName $name -Description $description -TaskPath $path + $task = Get-ScheduledTask -TaskName $name + Set-Attr $result "msg" "Added new task $name" + $result.changed = $true } - else - { - $tasks_needing_changing | Disable-ScheduledTask + elseif($state -eq "present" -and $exists -eq $true) { + if ($task.Description -eq $description -and $task.TaskName -eq $name -and $task.TaskPath -eq $path -and $task.Actions.Execute -eq $execute) { + #No change in the task yet + Set-Attr $result "msg" "No change in task $name" + } + else { + Unregister-ScheduledTask -TaskName $name -Confirm:$false + $action = New-ScheduledTaskAction -Execute $execute + Register-ScheduledTask -Action $action -Trigger $trigger -TaskName $name -Description $description -TaskPath $path + $task = Get-ScheduledTask -TaskName $name + Set-Attr $result "msg" "Updated task $name" + $result.changed = $true + } } - Set-Attr $result "tasks_changed" ($tasks_needing_changing | foreach { $_.TaskPath + $_.TaskName }) - $result.changed = $true - } - else - { - Set-Attr $result "tasks_changed" @() - $result.changed = $false - } - Exit-Json $result; + if ($state -eq "present" -and $enabled -eq $true -and $task.State -ne "Ready" ){ + $task | Enable-ScheduledTask + Set-Attr $result "msg" "Enabled task $name" + $result.changed = $true + } + elseif ($state -eq "present" -and $enabled -eq $false -and $task.State -ne "Disabled"){ + $task | Disable-ScheduledTask + Set-Attr $result "msg" "Disabled task $name" + $result.changed = $true + } + + Exit-Json $result; } catch { Fail-Json $result $_.Exception.Message -} +} \ No newline at end of file diff --git a/windows/win_scheduled_task.py b/windows/win_scheduled_task.py index 2c5867402c5..4ab830d05e1 100644 --- a/windows/win_scheduled_task.py +++ b/windows/win_scheduled_task.py @@ -1,8 +1,5 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - -# (c) 2015, Peter Mounce -# # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify @@ -32,20 +29,45 @@ name: description: - Name of the scheduled task - - Supports * as wildcard required: true enabled: description: - - State that the task should become + - Enable/disable the task required: false choices: - yes - no default: yes -author: Peter Mounce + state: + description: + - State that the task should become + required: false + choices: + - present + - absent + execute: + description: + - Command the scheduled task should execute + required: false + frequency: + description: + - The frequency of the command + choices: + - daily + - weekly + required: false + time: + description: + - Time to execute scheduled task + required: false + path: + description: + - Folder path of scheduled task + default: '\' + required: false ''' EXAMPLES = ''' - # Disable the scheduled tasks with "WindowsUpdate" in their name - win_scheduled_task: name="*WindowsUpdate*" enabled=no + # Create a scheduled task to open a command prompt + win_scheduled_task: name="TaskName" execute="cmd" frequency="daily" time="9am" description="open command prompt" path="example" enable=yes state=present ''' From 00d3a629076cef00917101d0251662f00cf0842d Mon Sep 17 00:00:00 2001 From: Michael Perzel Date: Tue, 30 Jun 2015 12:24:00 -0500 Subject: [PATCH 0345/2522] Documentation updates --- windows/win_scheduled_task.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/windows/win_scheduled_task.py b/windows/win_scheduled_task.py index 4ab830d05e1..9f73cc30612 100644 --- a/windows/win_scheduled_task.py +++ b/windows/win_scheduled_task.py @@ -33,7 +33,6 @@ enabled: description: - Enable/disable the task - required: false choices: - yes - no @@ -41,7 +40,6 @@ state: description: - State that the task should become - required: false choices: - present - absent @@ -55,7 +53,6 @@ choices: - daily - weekly - required: false time: description: - Time to execute scheduled task @@ -64,7 +61,6 @@ description: - Folder path of scheduled task default: '\' - required: false ''' EXAMPLES = ''' From 0f9ade7fe3e02010dc8652126e889f3cb48a79b1 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Tue, 30 Jun 2015 10:37:09 -0700 Subject: [PATCH 0346/2522] Fix bundler documentation --- packaging/language/bundler.py | 39 ++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/packaging/language/bundler.py b/packaging/language/bundler.py index 82ef2838a9a..5f605f5e947 100644 --- a/packaging/language/bundler.py +++ b/packaging/language/bundler.py @@ -40,18 +40,22 @@ default: present chdir: description: - - The directory to execute the bundler commands from. This directoy needs to contain a valid Gemfile or .bundle/ directory + - The directory to execute the bundler commands from. This directoy + needs to contain a valid Gemfile or .bundle/ directory required: false default: temporary working directory exclude_groups: description: - - A list of Gemfile groups to exclude during operations. This only applies when state is C(present). Bundler considers this a 'remembered' - property for the Gemfile and will automatically exclude groups in future operations even if C(exclude_groups) is not set + - A list of Gemfile groups to exclude during operations. This only + applies when state is C(present). Bundler considers this + a 'remembered' property for the Gemfile and will automatically exclude + groups in future operations even if C(exclude_groups) is not set required: false default: null clean: description: - - Only applies if state is C(present). If set removes any gems on the target host that are not in the gemfile + - Only applies if state is C(present). If set removes any gems on the + target host that are not in the gemfile required: false choices: [yes, no] default: "no" @@ -68,8 +72,9 @@ default: "no" deployment_mode: description: - - Only applies if state is C(present). If set it will only install gems that are in the default or production groups. Requires a Gemfile.lock - file to have been created prior + - Only applies if state is C(present). If set it will only install gems + that are in the default or production groups. Requires a Gemfile.lock + file to have been created prior required: false choices: [yes, no] default: "no" @@ -81,19 +86,25 @@ default: "yes" gem_path: description: - - Only applies if state is C(present). Specifies the directory to install the gems into. If C(chdir) is set then this path is relative to C(chdir) - required: false - default: RubyGems gem paths + - Only applies if state is C(present). Specifies the directory to + install the gems into. If C(chdir) is set then this path is relative to + C(chdir) + required: false + default: RubyGems gem paths binstub_directory: description: - - Only applies if state is C(present). Specifies the directory to install any gem bins files to. When executed the bin files will run within - the context of the Gemfile and fail if any required gem dependencies are not installed. If C(chdir) is set then this path is relative to C(chdir) + - Only applies if state is C(present). Specifies the directory to + install any gem bins files to. When executed the bin files will run + within the context of the Gemfile and fail if any required gem + dependencies are not installed. If C(chdir) is set then this path is + relative to C(chdir) required: false default: null extra_args: description: - - A space separated string of additional commands that can be applied to the Bundler command. Refer to the Bundler documentation for more - information + - A space separated string of additional commands that can be applied to + the Bundler command. Refer to the Bundler documentation for more + information required: false default: null author: Tim Hoiberg @@ -196,4 +207,4 @@ def main(): from ansible.module_utils.basic import * -main() \ No newline at end of file +main() From 3be267b57908d013533e8cfbbe3bc78a67da2b0f Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 30 Jun 2015 13:45:12 -0500 Subject: [PATCH 0347/2522] Give dpkg_selections a .py file extension --- packaging/{dpkg_selections => dpkg_selections.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packaging/{dpkg_selections => dpkg_selections.py} (100%) diff --git a/packaging/dpkg_selections b/packaging/dpkg_selections.py similarity index 100% rename from packaging/dpkg_selections rename to packaging/dpkg_selections.py From 8ba11e97e24396a32e8d8a3276f6c6e7960f2371 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 30 Jun 2015 13:45:24 -0500 Subject: [PATCH 0348/2522] Add missing __init__.py file --- clustering/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 clustering/__init__.py diff --git a/clustering/__init__.py b/clustering/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From 9f9422fcb583a81d560627a90a2a503a84942ba6 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 30 Jun 2015 13:45:53 -0500 Subject: [PATCH 0349/2522] Update vsphere_copy.py to use new style module_utils import --- cloud/vmware/vsphere_copy.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cloud/vmware/vsphere_copy.py b/cloud/vmware/vsphere_copy.py index 44e20caebdf..4364e8b5197 100644 --- a/cloud/vmware/vsphere_copy.py +++ b/cloud/vmware/vsphere_copy.py @@ -149,6 +149,7 @@ def main(): else: module.fail_json(msg='Failed to upload', status=resp.status, reason=resp.reason, length=resp.length, version=resp.version, headers=resp.getheaders(), chunked=resp.chunked, url=url) -# this is magic, see lib/ansible/module_common.py -#<> +# Import module snippets +from ansible.module_utils.basic import * + main() From fda25aa93b9f11a144093cc8ec7c167b0ef302ff Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 30 Jun 2015 13:46:14 -0500 Subject: [PATCH 0350/2522] Fix interpreter line in webfaction modules --- cloud/webfaction/webfaction_app.py | 2 +- cloud/webfaction/webfaction_db.py | 2 +- cloud/webfaction/webfaction_domain.py | 2 +- cloud/webfaction/webfaction_mailbox.py | 2 +- cloud/webfaction/webfaction_site.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cloud/webfaction/webfaction_app.py b/cloud/webfaction/webfaction_app.py index 3e42ec1265e..3d11d17a432 100644 --- a/cloud/webfaction/webfaction_app.py +++ b/cloud/webfaction/webfaction_app.py @@ -1,4 +1,4 @@ -#! /usr/bin/python +#!/usr/bin/python # # Create a Webfaction application using Ansible and the Webfaction API # diff --git a/cloud/webfaction/webfaction_db.py b/cloud/webfaction/webfaction_db.py index f420490711c..82eac1c1f42 100644 --- a/cloud/webfaction/webfaction_db.py +++ b/cloud/webfaction/webfaction_db.py @@ -1,4 +1,4 @@ -#! /usr/bin/python +#!/usr/bin/python # # Create a webfaction database using Ansible and the Webfaction API # diff --git a/cloud/webfaction/webfaction_domain.py b/cloud/webfaction/webfaction_domain.py index 0b35faf110f..c809dd6beb3 100644 --- a/cloud/webfaction/webfaction_domain.py +++ b/cloud/webfaction/webfaction_domain.py @@ -1,4 +1,4 @@ -#! /usr/bin/python +#!/usr/bin/python # # Create Webfaction domains and subdomains using Ansible and the Webfaction API # diff --git a/cloud/webfaction/webfaction_mailbox.py b/cloud/webfaction/webfaction_mailbox.py index 7547b6154e5..c08bd477601 100644 --- a/cloud/webfaction/webfaction_mailbox.py +++ b/cloud/webfaction/webfaction_mailbox.py @@ -1,4 +1,4 @@ -#! /usr/bin/python +#!/usr/bin/python # # Create webfaction mailbox using Ansible and the Webfaction API # diff --git a/cloud/webfaction/webfaction_site.py b/cloud/webfaction/webfaction_site.py index 57eae39c0dc..bb1bfb94457 100644 --- a/cloud/webfaction/webfaction_site.py +++ b/cloud/webfaction/webfaction_site.py @@ -1,4 +1,4 @@ -#! /usr/bin/python +#!/usr/bin/python # # Create Webfaction website using Ansible and the Webfaction API # From 9a36454329da8909a675e3cc555dce2acda230df Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 30 Jun 2015 13:46:45 -0500 Subject: [PATCH 0351/2522] replace tabs with spaces in mongodb_user.py --- database/misc/mongodb_user.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index ede8004945b..0529abdea09 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -225,10 +225,10 @@ def main(): update_password = module.params['update_password'] try: - if replica_set: - client = MongoClient(login_host, int(login_port), replicaset=replica_set, ssl=ssl) - else: - client = MongoClient(login_host, int(login_port), ssl=ssl) + if replica_set: + client = MongoClient(login_host, int(login_port), replicaset=replica_set, ssl=ssl) + else: + client = MongoClient(login_host, int(login_port), ssl=ssl) if login_user is None and login_password is None: mongocnf_creds = load_mongocnf() From 5605c4d7b5ef825744c8b4260f19f1c6172f7625 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Tue, 30 Jun 2015 11:09:55 -0700 Subject: [PATCH 0352/2522] Add author github ID --- packaging/language/bundler.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packaging/language/bundler.py b/packaging/language/bundler.py index 5f605f5e947..e98350a7b70 100644 --- a/packaging/language/bundler.py +++ b/packaging/language/bundler.py @@ -107,7 +107,7 @@ information required: false default: null -author: Tim Hoiberg +author: "Tim Hoiberg (@thoiberg)" ''' EXAMPLES=''' @@ -207,4 +207,5 @@ def main(): from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() From 4e48ef9ecace3a6eb92e3e4d2ef1a3ea2b7e33ab Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 1 Jul 2015 07:25:02 -0700 Subject: [PATCH 0353/2522] Disable travis docs checks for now --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d43c6b3b3fa..057524c4def 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,4 +13,4 @@ script: - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/layman\.py|/maven_artifact\.py|clustering/consul.*\.py|notification/pushbullet\.py' . - python2.6 -m compileall -fq . - python2.7 -m compileall -fq . - - ./test-docs.sh extras + #- ./test-docs.sh extras From e724dc2beda665b9537e8dc65a0b553c3e1295b4 Mon Sep 17 00:00:00 2001 From: Quentin Stafford-Fraser Date: Wed, 1 Jul 2015 17:48:06 +0100 Subject: [PATCH 0354/2522] webfaction: Allow machine to be specified if account has more than one. --- cloud/webfaction/webfaction_app.py | 27 ++++++++++++++++++++++----- cloud/webfaction/webfaction_db.py | 28 ++++++++++++++++++++++------ 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/cloud/webfaction/webfaction_app.py b/cloud/webfaction/webfaction_app.py index 3d11d17a432..1c015a401d1 100644 --- a/cloud/webfaction/webfaction_app.py +++ b/cloud/webfaction/webfaction_app.py @@ -7,7 +7,9 @@ # # ------------------------------------------ # -# (c) Quentin Stafford-Fraser 2015 +# (c) Quentin Stafford-Fraser 2015, with contributions gratefully acknowledged from: +# * Andy Baker +# * Federico Tarantini # # This file is part of Ansible # @@ -80,6 +82,12 @@ description: - The webfaction password to use required: true + + machine: + description: + - The machine name to use (optional for accounts with only one machine) + required: false + ''' EXAMPLES = ''' @@ -90,6 +98,7 @@ type=mod_wsgi35-python27 login_name={{webfaction_user}} login_password={{webfaction_passwd}} + machine={{webfaction_machine}} ''' import xmlrpclib @@ -108,6 +117,7 @@ def main(): port_open = dict(required=False, choices=BOOLEANS, default=False), login_name = dict(required=True), login_password = dict(required=True), + machine = dict(required=False, default=False), ), supports_check_mode=True ) @@ -115,10 +125,17 @@ def main(): app_type = module.params['type'] app_state = module.params['state'] - session_id, account = webfaction.login( - module.params['login_name'], - module.params['login_password'] - ) + if module.params['machine']: + session_id, account = webfaction.login( + module.params['login_name'], + module.params['login_password'], + module.params['machine'] + ) + else: + session_id, account = webfaction.login( + module.params['login_name'], + module.params['login_password'] + ) app_list = webfaction.list_apps(session_id) app_map = dict([(i['name'], i) for i in app_list]) diff --git a/cloud/webfaction/webfaction_db.py b/cloud/webfaction/webfaction_db.py index 82eac1c1f42..6c45e700e9b 100644 --- a/cloud/webfaction/webfaction_db.py +++ b/cloud/webfaction/webfaction_db.py @@ -4,7 +4,9 @@ # # ------------------------------------------ # -# (c) Quentin Stafford-Fraser and Andy Baker 2015 +# (c) Quentin Stafford-Fraser 2015, with contributions gratefully acknowledged from: +# * Andy Baker +# * Federico Tarantini # # This file is part of Ansible # @@ -68,6 +70,11 @@ description: - The webfaction password to use required: true + + machine: + description: + - The machine name to use (optional for accounts with only one machine) + required: false ''' EXAMPLES = ''' @@ -81,6 +88,7 @@ type: mysql login_name: "{{webfaction_user}}" login_password: "{{webfaction_passwd}}" + machine: "{{webfaction_machine}}" # Note that, for symmetry's sake, deleting a database using # 'state: absent' will also delete the matching user. @@ -103,6 +111,7 @@ def main(): password = dict(required=False, default=None), login_name = dict(required=True), login_password = dict(required=True), + machine = dict(required=False, default=False), ), supports_check_mode=True ) @@ -111,10 +120,17 @@ def main(): db_type = module.params['type'] db_passwd = module.params['password'] - session_id, account = webfaction.login( - module.params['login_name'], - module.params['login_password'] - ) + if module.params['machine']: + session_id, account = webfaction.login( + module.params['login_name'], + module.params['login_password'], + module.params['machine'] + ) + else: + session_id, account = webfaction.login( + module.params['login_name'], + module.params['login_password'] + ) db_list = webfaction.list_dbs(session_id) db_map = dict([(i['name'], i) for i in db_list]) @@ -130,7 +146,7 @@ def main(): if db_state == 'present': - # Does an database with this name already exist? + # Does a database with this name already exist? if existing_db: # Yes, but of a different type - fail if existing_db['db_type'] != db_type: From 3d9fd24d62b4513495dc2606022691683473332b Mon Sep 17 00:00:00 2001 From: Etienne CARRIERE Date: Wed, 1 Jul 2015 22:26:32 +0200 Subject: [PATCH 0355/2522] Localize exceptions for F5 LTM virtual server module --- network/f5/bigip_virtual_server.py | 127 ++++++++++++++++------------- 1 file changed, 72 insertions(+), 55 deletions(-) diff --git a/network/f5/bigip_virtual_server.py b/network/f5/bigip_virtual_server.py index e6402833cb5..09e9bf0beb5 100644 --- a/network/f5/bigip_virtual_server.py +++ b/network/f5/bigip_virtual_server.py @@ -196,39 +196,44 @@ def get_profiles(api,name): def set_profiles(api,name,profiles_list): - if profiles_list is None: - return False - current_profiles=map(lambda x:x['profile_name'], get_profiles(api,name)) - to_add_profiles=[] - for x in profiles_list: - if x not in current_profiles: - to_add_profiles.append({'profile_context': 'PROFILE_CONTEXT_TYPE_ALL', 'profile_name': x}) - to_del_profiles=[] - for x in current_profiles: - if (x not in profiles_list) and (x!= "/Common/tcp"): - to_del_profiles.append({'profile_context': 'PROFILE_CONTEXT_TYPE_ALL', 'profile_name': x}) - changed=False - if len(to_del_profiles)>0: - api.LocalLB.VirtualServer.remove_profile(virtual_servers = [name],profiles = [to_del_profiles]) - changed=True - if len(to_add_profiles)>0: - api.LocalLB.VirtualServer.add_profile(virtual_servers = [name],profiles= [to_add_profiles]) - changed=True - return changed - + updated=False + try: + if profiles_list is None: + return False + current_profiles=map(lambda x:x['profile_name'], get_profiles(api,name)) + to_add_profiles=[] + for x in profiles_list: + if x not in current_profiles: + to_add_profiles.append({'profile_context': 'PROFILE_CONTEXT_TYPE_ALL', 'profile_name': x}) + to_del_profiles=[] + for x in current_profiles: + if (x not in profiles_list) and (x!= "/Common/tcp"): + to_del_profiles.append({'profile_context': 'PROFILE_CONTEXT_TYPE_ALL', 'profile_name': x}) + if len(to_del_profiles)>0: + api.LocalLB.VirtualServer.remove_profile(virtual_servers = [name],profiles = [to_del_profiles]) + updated=True + if len(to_add_profiles)>0: + api.LocalLB.VirtualServer.add_profile(virtual_servers = [name],profiles= [to_add_profiles]) + updated=True + return updated + except bigsuds.OperationFailed, e: + raise Exception('Error on setting profiles : %s' % e) def set_snat(api,name,snat): - current_state=get_snat_type(api,name) - update = False - if snat is None: - return update - if snat == 'None' and current_state != 'SRC_TRANS_NONE': - api.LocalLB.VirtualServer.set_source_address_translation_none(virtual_servers = [name]) - update = True - if snat == 'Automap' and current_state != 'SRC_TRANS_AUTOMAP': - api.LocalLB.VirtualServer.set_source_address_translation_automap(virtual_servers = [name]) - update = True - return update + updated = False + try: + current_state=get_snat_type(api,name) + if snat is None: + return update + if snat == 'None' and current_state != 'SRC_TRANS_NONE': + api.LocalLB.VirtualServer.set_source_address_translation_none(virtual_servers = [name]) + updated = True + if snat == 'Automap' and current_state != 'SRC_TRANS_AUTOMAP': + api.LocalLB.VirtualServer.set_source_address_translation_automap(virtual_servers = [name]) + updated = True + return updated + except bigsuds.OperationFailed, e: + raise Exception('Error on setting snat : %s' % e) def get_snat_type(api,name): return api.LocalLB.VirtualServer.get_source_address_translation_type(virtual_servers = [name])[0] @@ -238,37 +243,46 @@ def get_pool(api,name): return api.LocalLB.VirtualServer.get_default_pool_name(virtual_servers = [name])[0] def set_pool(api,name,pool): - current_pool = get_pool (api,name) updated=False - if pool is not None and (pool != current_pool): - api.LocalLB.VirtualServer.set_default_pool_name(virtual_servers = [name],default_pools = [pool]) - updated=True - return updated - + try: + current_pool = get_pool (api,name) + if pool is not None and (pool != current_pool): + api.LocalLB.VirtualServer.set_default_pool_name(virtual_servers = [name],default_pools = [pool]) + updated=True + return updated + except bigsuds.OperationFailed, e: + raise Exception('Error on setting pool : %s' % e) def get_destination(api,name): return api.LocalLB.VirtualServer.get_destination_v2(virtual_servers = [name])[0] def set_destination(api,name,destination,port): - current_destination = get_destination(api,name) updated=False - if (destination is not None and port is not None) and (destination != current_destination['address'] or port != current_destination['port']): - api.LocalLB.VirtualServer.set_destination_v2(virtual_servers = [name],destinations=[{'address': destination, 'port':port}]) - updated=True - return updated + try: + current_destination = get_destination(api,name) + if (destination is not None and port is not None) and (destination != current_destination['address'] or port != current_destination['port']): + api.LocalLB.VirtualServer.set_destination_v2(virtual_servers = [name],destinations=[{'address': destination, 'port':port}]) + updated=True + return updated + except bigsuds.OperationFailed, e: + raise Exception('Error on setting destination : %s'% e ) + def get_description(api,name): return api.LocalLB.VirtualServer.get_description(virtual_servers = [name])[0] def set_description(api,name,description): - current_description = get_description(api,name) updated=False - if description is not None and current_description != description: - api.LocalLB.VirtualServer.set_description(virtual_servers =[name],descriptions=[description]) - updated=True - return updated + try: + current_description = get_description(api,name) + if description is not None and current_description != description: + api.LocalLB.VirtualServer.set_description(virtual_servers =[name],descriptions=[description]) + updated=True + return updated + except bigsuds.OperationFailed, e: + raise Exception('Error on setting description : %s ' % e) def main(): @@ -342,7 +356,7 @@ def main(): if "already exists" in str(e): update = True else: - raise + raise Exception('Error on creating Virtual Server : %s' % e) else: set_profiles(api,name,all_profiles) set_snat(api,name,snat) @@ -356,13 +370,16 @@ def main(): # VS exists if not module.check_mode: # Have a transaction for all the changes - api.System.Session.start_transaction() - result['changed']|=set_destination(api,name,fq_name(partition,destination),port) - result['changed']|=set_pool(api,name,pool) - result['changed']|=set_description(api,name,description) - result['changed']|=set_snat(api,name,snat) - result['changed']|=set_profiles(api,name,all_profiles) - api.System.Session.submit_transaction() + try: + api.System.Session.start_transaction() + result['changed']|=set_destination(api,name,fq_name(partition,destination),port) + result['changed']|=set_pool(api,name,pool) + result['changed']|=set_description(api,name,description) + result['changed']|=set_snat(api,name,snat) + result['changed']|=set_profiles(api,name,all_profiles) + api.System.Session.submit_transaction() + except Exception,e: + raise Exception("Error on updating Virtual Server : %s" % e) else: # check-mode return value result = {'changed': True} From 9398d0509fd30cd775aa32ef5a50fe6bc810170c Mon Sep 17 00:00:00 2001 From: William Brown Date: Thu, 2 Jul 2015 07:56:56 +0930 Subject: [PATCH 0356/2522] Changes to allow FS resize in filesystem --- system/filesystem.py | 85 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 74 insertions(+), 11 deletions(-) diff --git a/system/filesystem.py b/system/filesystem.py index a2f979ecd0b..0a98a8e2fdc 100644 --- a/system/filesystem.py +++ b/system/filesystem.py @@ -41,6 +41,12 @@ description: - If yes, allows to create new filesystem on devices that already has filesystem. required: false + resizefs: + choices: [ "yes", "no" ] + default: "no" + description: + - If yes, if the block device and filessytem size differ, grow the filesystem into the space. Note, XFS Will only grow if mounted. + required: false opts: description: - List of options to be passed to mkfs command. @@ -63,17 +69,68 @@ def main(): dev=dict(required=True, aliases=['device']), opts=dict(), force=dict(type='bool', default='no'), + resizefs=dict(type='bool', default='no'), ), supports_check_mode=True, ) + # There is no "single command" to manipulate filesystems, so we map them all out and their options + fs_cmd_map = { + 'ext2' : { + 'mkfs' : 'mkfs.ext2', + 'grow' : 'resize2fs', + 'grow_flag' : None, + 'force_flag' : '-F', + }, + 'ext3' : { + 'mkfs' : 'mkfs.ext3', + 'grow' : 'resize2fs', + 'grow_flag' : None, + 'force_flag' : '-F', + }, + 'ext4' : { + 'mkfs' : 'mkfs.ext4', + 'grow' : 'resize2fs', + 'grow_flag' : None, + 'force_flag' : '-F', + }, + 'ext4dev' : { + 'mkfs' : 'mkfs.ext4', + 'grow' : 'resize2fs', + 'grow_flag' : None, + 'force_flag' : '-F', + }, + 'xfs' : { + 'mkfs' : 'mkfs.xfs', + 'grow' : 'xfs_growfs', + 'grow_flag' : None, + 'force_flag' : '-f', + }, + 'btrfs' : { + 'mkfs' : 'mkfs.btrfs', + 'grow' : 'btrfs', + 'grow_flag' : 'filesystem resize', + 'force_flag' : '-f', + } + } + dev = module.params['dev'] fstype = module.params['fstype'] opts = module.params['opts'] force = module.boolean(module.params['force']) + resizefs = module.boolean(module.params['resizefs']) changed = False + try: + _ = fs_cmd_map[fstype] + except KeyError: + module.exit_json(changed=False, msg="WARNING: module does not support this filesystem yet. %s" % fstype) + + mkfscmd = fs_cmd_map[fstype]['mkfs'] + force_flag = fs_cmd_map[fstype]['force_flag'] + growcmd = fs_cmd_map[fstype]['grow'] + if not os.path.exists(dev): module.fail_json(msg="Device %s not found."%dev) @@ -82,9 +139,21 @@ def main(): rc,raw_fs,err = module.run_command("%s -c /dev/null -o value -s TYPE %s" % (cmd, dev)) fs = raw_fs.strip() - - if fs == fstype: + if fs == fstype and resizefs == False: module.exit_json(changed=False) + elif fs == fstype and resizefs == True: + cmd = module.get_bin_path(growcmd, required=True) + if module.check_mode: + module.exit_json(changed=True, msg="May resize filesystem") + else: + rc,out,err = module.run_command("%s %s" % (cmd, dev)) + # Sadly there is no easy way to determine if this has changed. For now, just say "true" and move on. + # in the future, you would have to parse the output to determine this. + # thankfully, these are safe operations if no change is made. + if rc == 0: + module.exit_json(changed=True, msg=out) + else: + module.fail_json(msg="Resizing filesystem %s on device '%s' failed"%(fstype,dev), rc=rc, err=err) elif fs and not force: module.fail_json(msg="'%s' is already used as %s, use force=yes to overwrite"%(dev,fs), rc=rc, err=err) @@ -93,19 +162,13 @@ def main(): if module.check_mode: changed = True else: - mkfs = module.get_bin_path('mkfs', required=True) + mkfs = module.get_bin_path(mkfscmd, required=True) cmd = None - if fstype in ['ext2', 'ext3', 'ext4', 'ext4dev']: - force_flag="-F" - elif fstype in ['xfs', 'btrfs']: - force_flag="-f" - else: - force_flag="" if opts is None: - cmd = "%s -t %s %s '%s'" % (mkfs, fstype, force_flag, dev) + cmd = "%s %s '%s'" % (mkfs, force_flag, dev) else: - cmd = "%s -t %s %s %s '%s'" % (mkfs, fstype, force_flag, opts, dev) + cmd = "%s %s %s '%s'" % (mkfs, force_flag, opts, dev) rc,_,err = module.run_command(cmd) if rc == 0: changed = True From 757fc291c1a3a793e9d4acdfe90e27c87ac7c53e Mon Sep 17 00:00:00 2001 From: Etienne CARRIERE Date: Thu, 2 Jul 2015 07:35:19 +0200 Subject: [PATCH 0357/2522] Rework on Exception management --- network/f5/bigip_virtual_server.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/network/f5/bigip_virtual_server.py b/network/f5/bigip_virtual_server.py index 09e9bf0beb5..84cfccf2958 100644 --- a/network/f5/bigip_virtual_server.py +++ b/network/f5/bigip_virtual_server.py @@ -176,16 +176,23 @@ def vs_exists(api, vs): def vs_create(api,name,destination,port,pool): _profiles=[[{'profile_context': 'PROFILE_CONTEXT_TYPE_ALL', 'profile_name': 'tcp'}]] + created = False + # a bit of a hack to handle concurrent runs of this module. + # even though we've checked the vs doesn't exist, + # it may exist by the time we run create_vs(). + # this catches the exception and does something smart + # about it! try: api.LocalLB.VirtualServer.create( definitions = [{'name': [name], 'address': [destination], 'port': port, 'protocol': 'PROTOCOL_TCP'}], wildmasks = ['255.255.255.255'], resources = [{'type': 'RESOURCE_TYPE_POOL', 'default_pool_name': pool}], profiles = _profiles) - result = True - desc = 0 - except Exception, e : - print e.args + created = True + return created + except bigsudsOperationFailed, e : + if "already exists" not in str(e): + raise Exception('Error on creating Virtual Server : %s' % e) def vs_remove(api,name): api.LocalLB.VirtualServer.delete_virtual_server(virtual_servers = [name ]) @@ -351,16 +358,12 @@ def main(): # about it! try: vs_create(api,name,destination,port,pool) - result = {'changed': True} - except bigsuds.OperationFailed, e: - if "already exists" in str(e): - update = True - else: - raise Exception('Error on creating Virtual Server : %s' % e) - else: set_profiles(api,name,all_profiles) set_snat(api,name,snat) set_description(api,name,description) + result = {'changed': True} + except bigsuds.OperationFailed, e: + raise Exception('Error on creating Virtual Server : %s' % e) else: # check-mode return value result = {'changed': True} From fc9078229c27f0bccff32c7cee427a1a2b62ca0f Mon Sep 17 00:00:00 2001 From: Etienne CARRIERE Date: Thu, 2 Jul 2015 08:20:29 +0200 Subject: [PATCH 0358/2522] Add "Default Persistence profile" support --- network/f5/bigip_virtual_server.py | 34 ++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/network/f5/bigip_virtual_server.py b/network/f5/bigip_virtual_server.py index 84cfccf2958..0c47eb20943 100644 --- a/network/f5/bigip_virtual_server.py +++ b/network/f5/bigip_virtual_server.py @@ -93,7 +93,11 @@ - "Source network address policy" required: false default: None - + default_persistence_profile: + description: + - "Default Profile which manages the session persistence" + required: false + default: None description: description: - "Virtual server description." @@ -291,6 +295,28 @@ def set_description(api,name,description): except bigsuds.OperationFailed, e: raise Exception('Error on setting description : %s ' % e) +def get_persistence_profiles(api,name): + return api.LocalLB.VirtualServer.get_persistence_profile(virtual_servers = [name])[0] + +def set_default_persistence_profiles(api,name,persistence_profile): + updated=False + if persistence_profile is None: + return updated + try: + current_persistence_profiles = get_persistence_profiles(api,name) + default=None + for profile in current_persistence_profiles: + if profile['default_profile']: + default=profile['profile_name'] + break + if default is not None and default != persistence_profile: + api.LocalLB.VirtualServer.remove_persistence_profile(virtual_servers=[name],profiles=[[{'profile_name':default,'default_profile' : True}]]) + if default != persistence_profile: + api.LocalLB.VirtualServer.add_persistence_profile(virtual_servers=[name],profiles=[[{'profile_name':persistence_profile,'default_profile' : True}]]) + updated=True + return updated + except bigsuds.OperationFailed, e: + raise Exception('Error on setting default persistence profile : %s' % e) def main(): argument_spec = f5_argument_spec() @@ -303,7 +329,8 @@ def main(): all_profiles = dict(type='list'), pool=dict(type='str'), description = dict(type='str'), - snat=dict(type='str') + snat=dict(type='str'), + default_persistence_profile=dict(type='str') ) ) @@ -320,6 +347,7 @@ def main(): pool=fq_name(partition,module.params['pool']) description = module.params['description'] snat = module.params['snat'] + default_persistence_profile=fq_name(partition,module.params['default_persistence_profile']) if 1 > port > 65535: module.fail_json(msg="valid ports must be in range 1 - 65535") @@ -361,6 +389,7 @@ def main(): set_profiles(api,name,all_profiles) set_snat(api,name,snat) set_description(api,name,description) + set_default_persistence_profiles(api,name,default_persistence_profile) result = {'changed': True} except bigsuds.OperationFailed, e: raise Exception('Error on creating Virtual Server : %s' % e) @@ -380,6 +409,7 @@ def main(): result['changed']|=set_description(api,name,description) result['changed']|=set_snat(api,name,snat) result['changed']|=set_profiles(api,name,all_profiles) + result['changed']|=set_default_persistence_profiles(api,name,default_persistence_profile) api.System.Session.submit_transaction() except Exception,e: raise Exception("Error on updating Virtual Server : %s" % e) From d87da2ba2de178ead96286df10520ce933352a46 Mon Sep 17 00:00:00 2001 From: Timothy Vandenbrande Date: Thu, 2 Jul 2015 09:19:08 +0200 Subject: [PATCH 0359/2522] renamed profile var --- windows/win_firewall_rule.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/windows/win_firewall_rule.ps1 b/windows/win_firewall_rule.ps1 index 832d68d8f9c..d19082e6690 100644 --- a/windows/win_firewall_rule.ps1 +++ b/windows/win_firewall_rule.ps1 @@ -225,13 +225,13 @@ foreach ($arg in $args){ }; }; -$profile=Get-Attr $params "profile" "current"; -if (($profile -ne 'current') -or ($profile -ne 'domain') -or ($profile -ne 'standard') -or ($profile -ne 'all') ) { +$winprofile=Get-Attr $params "profile" "current"; +if (($winprofile -ne 'current') -or ($winprofile -ne 'domain') -or ($winprofile -ne 'standard') -or ($winprofile -ne 'all') ) { $misArg+="Profile"; $msg+=@("for the Profile parameter only the values 'current', 'domain', 'standard' or 'all' are allowed"); } else { - $fwsettings.Add("profile", $profile) + $fwsettings.Add("profile", $winprofile) } if ($($($misArg|measure).count) -gt 0){ From b0a637086fa05781fffd967e5fe99a170ee1e9f0 Mon Sep 17 00:00:00 2001 From: Timothy Vandenbrande Date: Thu, 2 Jul 2015 10:00:33 +0200 Subject: [PATCH 0360/2522] add version_added: "2.0" --- windows/win_firewall_rule.py | 1 + 1 file changed, 1 insertion(+) diff --git a/windows/win_firewall_rule.py b/windows/win_firewall_rule.py index cb167d0f4fa..295979b248f 100644 --- a/windows/win_firewall_rule.py +++ b/windows/win_firewall_rule.py @@ -20,6 +20,7 @@ DOCUMENTATION = ''' --- module: win_fw +version_added: "2.0" author: Timothy Vandenbrande short_description: Windows firewall automation description: From 8ded4ae862c94ebe8d82973aed932b37784a0f64 Mon Sep 17 00:00:00 2001 From: Alan Loi Date: Thu, 2 Jul 2015 20:50:02 +1000 Subject: [PATCH 0361/2522] Docfixes - add version_added and missing default values. --- cloud/amazon/sqs_queue.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cloud/amazon/sqs_queue.py b/cloud/amazon/sqs_queue.py index a40433441aa..3febc8981f2 100644 --- a/cloud/amazon/sqs_queue.py +++ b/cloud/amazon/sqs_queue.py @@ -21,6 +21,7 @@ description: - Create or delete AWS SQS queues. - Update attributes on existing queues. +version_added: "2.0" author: Alan Loi (@loia) requirements: - "boto >= 2.33.0" @@ -39,22 +40,27 @@ description: - The default visibility timeout in seconds. required: false + default: null message_retention_period: description: - The message retention period in seconds. required: false + default: null maximum_message_size: description: - The maximum message size in bytes. required: false + default: null delivery_delay: description: - The delivery delay in seconds. required: false + default: null receive_message_wait_time: description: - The receive message wait time in seconds. required: false + default: null region: description: - The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used. From bbb578ac594e313a45d481b7cacec33cbfec4513 Mon Sep 17 00:00:00 2001 From: Igor Khomyakov Date: Thu, 2 Jul 2015 14:17:56 +0300 Subject: [PATCH 0362/2522] fix user notification for v2 api `notify` parameter is not working as expected for hipchat API v2. --- notification/hipchat.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/notification/hipchat.py b/notification/hipchat.py index 32689965cf9..57e97eaefec 100644 --- a/notification/hipchat.py +++ b/notification/hipchat.py @@ -5,7 +5,7 @@ --- module: hipchat version_added: "1.2" -short_description: Send a message to hipchat +short_description: Send a message to hipchat. description: - Send a message to hipchat options: @@ -56,7 +56,7 @@ version_added: 1.5.1 api: description: - - API url if using a self-hosted hipchat server + - API url if using a self-hosted hipchat server. For hipchat api version 2 use C(/v2) path in URI required: false default: 'https://api.hipchat.com/v1' version_added: 1.6.0 @@ -67,7 +67,15 @@ ''' EXAMPLES = ''' -- hipchat: token=AAAAAA room=notify msg="Ansible task finished" +- hipchat: room=notify msg="Ansible task finished" + +# Use Hipchat API version 2 + +- hipchat: + api: "https://api.hipchat.com/v2/" + token: OAUTH2_TOKEN + room: notify + msg: "Ansible task finished" ''' # =========================================== @@ -80,7 +88,6 @@ MSG_URI_V1 = "/rooms/message" -MSG_URI_V2 = "/room/{id_or_name}/message" NOTIFY_URI_V2 = "/room/{id_or_name}/notification" def send_msg_v1(module, token, room, msg_from, msg, msg_format='text', @@ -95,12 +102,8 @@ def send_msg_v1(module, token, room, msg_from, msg, msg_format='text', params['message_format'] = msg_format params['color'] = color params['api'] = api - - if notify: - params['notify'] = 1 - else: - params['notify'] = 0 - + params['notify'] = int(notify) + url = api + MSG_URI_V1 + "?auth_token=%s" % (token) data = urllib.urlencode(params) @@ -116,7 +119,7 @@ def send_msg_v1(module, token, room, msg_from, msg, msg_format='text', def send_msg_v2(module, token, room, msg_from, msg, msg_format='text', - color='yellow', notify=False, api=MSG_URI_V2): + color='yellow', notify=False, api=NOTIFY_URI_V2): '''sending message to hipchat v2 server''' print "Sending message to v2 server" @@ -126,13 +129,11 @@ def send_msg_v2(module, token, room, msg_from, msg, msg_format='text', body['message'] = msg body['color'] = color body['message_format'] = msg_format + params['notify'] = notify - if notify: - POST_URL = api + NOTIFY_URI_V2 - else: - POST_URL = api + MSG_URI_V2 - - url = POST_URL.replace('{id_or_name}',room) + POST_URL = api + NOTIFY_URI_V2 + + url = POST_URL.replace('{id_or_name}', room) data = json.dumps(body) if module.check_mode: From a706689a353f3c6906483f5fd105b2a93b5e8b4e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 2 Jul 2015 08:51:13 -0400 Subject: [PATCH 0363/2522] Bump version_added. --- cloud/rackspace/rax_clb_ssl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/rackspace/rax_clb_ssl.py b/cloud/rackspace/rax_clb_ssl.py index f16118c20f4..b467880400e 100644 --- a/cloud/rackspace/rax_clb_ssl.py +++ b/cloud/rackspace/rax_clb_ssl.py @@ -21,7 +21,7 @@ short_description: Manage SSL termination for a Rackspace Cloud Load Balancer. description: - Set up, reconfigure, or remove SSL termination for an existing load balancer. -version_added: "1.9" +version_added: "2.0" options: loadbalancer: description: From d1a63d39a27f8b2e3f999b8013cb0d52093a0900 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 2 Jul 2015 08:53:19 -0400 Subject: [PATCH 0364/2522] Include the balancer acted upon in the result. --- cloud/rackspace/rax_clb_ssl.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cloud/rackspace/rax_clb_ssl.py b/cloud/rackspace/rax_clb_ssl.py index b467880400e..bfd5f643020 100644 --- a/cloud/rackspace/rax_clb_ssl.py +++ b/cloud/rackspace/rax_clb_ssl.py @@ -193,7 +193,8 @@ def cloud_load_balancer_ssl(module, loadbalancer, state, enabled, private_key, result = dict( changed=changed, https_redirect=balancer.httpsRedirect, - ssl_termination=new_ssl + ssl_termination=new_ssl, + balancer=balancer ) success = True From 9462ad55e3c99163b673850711981ba1737273ed Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 2 Jul 2015 08:59:54 -0400 Subject: [PATCH 0365/2522] Guard calls that modify the CLB with try/except. --- cloud/rackspace/rax_clb_ssl.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/cloud/rackspace/rax_clb_ssl.py b/cloud/rackspace/rax_clb_ssl.py index bfd5f643020..eafa725d286 100644 --- a/cloud/rackspace/rax_clb_ssl.py +++ b/cloud/rackspace/rax_clb_ssl.py @@ -154,12 +154,18 @@ def cloud_load_balancer_ssl(module, loadbalancer, state, enabled, private_key, needs_change = True if needs_change: - balancer.add_ssl_termination(**ssl_attrs) + try: + balancer.add_ssl_termination(**ssl_attrs) + except pyrax.exceptions.PyraxException, e: + module.fail_json(msg='%s' % e.message) changed = True elif state == 'absent': # Remove SSL termination if it's already configured. if existing_ssl: - balancer.delete_ssl_termination() + try: + balancer.delete_ssl_termination() + except pyrax.exceptions.PyraxException, e: + module.fail_json(msg='%s' % e.message) changed = True if https_redirect is not None and balancer.httpsRedirect != https_redirect: @@ -168,7 +174,10 @@ def cloud_load_balancer_ssl(module, loadbalancer, state, enabled, private_key, # while the SSL termination changes above are being applied. pyrax.utils.wait_for_build(balancer, interval=5, attempts=attempts) - balancer.update(httpsRedirect=https_redirect) + try: + balancer.update(httpsRedirect=https_redirect) + except pyrax.exceptions.PyraxException, e: + module.fail_json(msg='%s' % e.message) changed = True if changed and wait: From bd4023fe8f177c457bf40fa9a3ae27af0d012c12 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 2 Jul 2015 09:09:28 -0400 Subject: [PATCH 0366/2522] Bring the examples up to date. --- cloud/rackspace/rax_clb_ssl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/rackspace/rax_clb_ssl.py b/cloud/rackspace/rax_clb_ssl.py index eafa725d286..20ae9698457 100644 --- a/cloud/rackspace/rax_clb_ssl.py +++ b/cloud/rackspace/rax_clb_ssl.py @@ -78,7 +78,7 @@ EXAMPLES = ''' - name: Enable SSL termination on a load balancer rax_clb_ssl: - balancer_name: the_loadbalancer + loadbalancer: the_loadbalancer state: present private_key: "{{ lookup('file', 'credentials/server.key' ) }}" certificate: "{{ lookup('file', 'credentials/server.crt' ) }}" @@ -88,7 +88,7 @@ - name: Disable SSL termination rax_clb_ssl: - balancer_name: "{{ registered_lb.balancer.id }}" + loadbalancer: "{{ registered_lb.balancer.id }}" state: absent wait: true ''' From 84880c5e35a6dc8e2eeddda3a1377d617ee57368 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 2 Jul 2015 09:24:07 -0400 Subject: [PATCH 0367/2522] Use rax_to_dict(). --- cloud/rackspace/rax_clb_ssl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/rackspace/rax_clb_ssl.py b/cloud/rackspace/rax_clb_ssl.py index 20ae9698457..2013b8c4d81 100644 --- a/cloud/rackspace/rax_clb_ssl.py +++ b/cloud/rackspace/rax_clb_ssl.py @@ -203,7 +203,7 @@ def cloud_load_balancer_ssl(module, loadbalancer, state, enabled, private_key, changed=changed, https_redirect=balancer.httpsRedirect, ssl_termination=new_ssl, - balancer=balancer + balancer=rax_to_dict(balancer, 'clb') ) success = True From d8023c225d83ba33454d9b6958d92151216a293f Mon Sep 17 00:00:00 2001 From: John W Higgins Date: Thu, 2 Jul 2015 17:07:07 -0700 Subject: [PATCH 0368/2522] Add zfs cloning --- system/zfs.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/system/zfs.py b/system/zfs.py index c3c87634377..00a81e32c54 100644 --- a/system/zfs.py +++ b/system/zfs.py @@ -66,6 +66,10 @@ - The checksum property. required: False choices: ['on','off',fletcher2,fletcher4,sha256] + clone: + description: + - Name of the snapshot to clone + required: False compression: description: - The compression property. @@ -253,8 +257,11 @@ def create(self): properties = self.properties volsize = properties.pop('volsize', None) volblocksize = properties.pop('volblocksize', None) + clone = properties.pop('clone', None) if "@" in self.name: action = 'snapshot' + elif clone: + action = 'clone' else: action = 'create' @@ -272,6 +279,8 @@ def create(self): if volsize: cmd.append('-V') cmd.append(volsize) + if clone: + cmd.append(clone) cmd.append(self.name) (rc, err, out) = self.module.run_command(' '.join(cmd)) if rc == 0: @@ -347,6 +356,7 @@ def main(): 'canmount': {'required': False, 'choices':['on', 'off', 'noauto']}, 'casesensitivity': {'required': False, 'choices':['sensitive', 'insensitive', 'mixed']}, 'checksum': {'required': False, 'choices':['on', 'off', 'fletcher2', 'fletcher4', 'sha256']}, + 'clone': {'required': False}, 'compression': {'required': False, 'choices':['on', 'off', 'lzjb', 'gzip', 'gzip-1', 'gzip-2', 'gzip-3', 'gzip-4', 'gzip-5', 'gzip-6', 'gzip-7', 'gzip-8', 'gzip-9', 'lz4', 'zle']}, 'copies': {'required': False, 'choices':['1', '2', '3']}, 'createparent': {'required': False, 'choices':['on', 'off']}, From c5c3b8133f3a6afc2e449c7cbef43aa8a29e3275 Mon Sep 17 00:00:00 2001 From: Paul Markham Date: Fri, 3 Jul 2015 14:28:28 +1000 Subject: [PATCH 0369/2522] Updates based on community review. * Changed 'config' from a list to a string so any valid zonecfg(1M) syntax is accepted. * Made default state 'present' * Added 'attached', 'detached' and 'configured' states to allow zones to be moved between hosts. * Updated documentation and examples. * Code tidy up and refactoring. --- system/solaris_zone.py | 312 ++++++++++++++++++++++++++--------------- 1 file changed, 198 insertions(+), 114 deletions(-) diff --git a/system/solaris_zone.py b/system/solaris_zone.py index 54ab86eee20..fd143e9dc01 100644 --- a/system/solaris_zone.py +++ b/system/solaris_zone.py @@ -1,6 +1,6 @@ #!/usr/bin/python -# (c) 2013, Paul Markham +# (c) 2015, Paul Markham # # This file is part of Ansible # @@ -37,13 +37,18 @@ state: required: true description: - - C(present), create the zone. - - C(running), if the zone already exists, boot it, otherwise, create the zone - first, then boot it. + - C(present), configure and install the zone. + - C(installed), synonym for C(present). + - C(running), if the zone already exists, boot it, otherwise, configure and install + the zone first, then boot it. - C(started), synonym for C(running). - C(stopped), shutdown a zone. - C(absent), destroy the zone. - choices: ['present', 'started', 'running', 'stopped', 'absent'] + - C(configured), configure the ready so that it's to be attached. + - C(attached), attach a zone, but do not boot it. + - C(detach), stop and detach a zone + choices: ['present', 'installed', 'started', 'running', 'stopped', 'absent', 'configured', 'attached', 'detached'] + default: present name: description: - Zone name. @@ -68,19 +73,25 @@ config: required: false description: - - 'The zonecfg configuration commands for this zone, separated by commas, e.g. - "set auto-boot=true,add net,set physical=bge0,set address=10.1.1.1,end" - See the Solaris Systems Administrator guide for a list of all configuration commands - that can be used.' + - 'The zonecfg configuration commands for this zone. See zonecfg(1M) for the valid options + and syntax. Typically this is a list of options separated by semi-colons or new lines, e.g. + "set auto-boot=true;add net;set physical=bge0;set address=10.1.1.1;end"' required: false - default: null + default: empty string create_options: required: false description: - - 'Extra options to the zonecfg create command. For example, this can be used to create a - Solaris 11 kernel zone' + - 'Extra options to the zonecfg(1M) create command.' required: false - default: null + default: empty string + attach_options: + required: false + description: + - 'Extra options to the zoneadm attach command. For example, this can be used to specify + whether a minimum or full update of packages is required and if any packages need to + be deleted. For valid values, see zoneadm(1M)' + required: false + default: empty string timeout: description: - Timeout, in seconds, for zone to boot. @@ -89,15 +100,15 @@ ''' EXAMPLES = ''' -# Create a zone, but don't boot it +# Create and install a zone, but don't boot it solaris_zone: name=zone1 state=present path=/zones/zone1 sparse=true root_password="Be9oX7OSwWoU." - config='set autoboot=true, add net, set physical=bge0, set address=10.1.1.1, end' + config='set autoboot=true; add net; set physical=bge0; set address=10.1.1.1; end' -# Create a zone and boot it +# Create and install a zone and boot it solaris_zone: name=zone1 state=running path=/zones/zone1 root_password="Be9oX7OSwWoU." - config='set autoboot=true, add net, set physical=bge0, set address=10.1.1.1, end' + config='set autoboot=true; add net; set physical=bge0; set address=10.1.1.1; end' -# Boot an already created zone +# Boot an already installed zone solaris_zone: name=zone1 state=running # Stop a zone @@ -105,6 +116,16 @@ # Destroy a zone solaris_zone: name=zone1 state=absent + +# Detach a zone +solaris_zone: name=zone1 state=detached + +# Configure a zone, ready to be attached +solaris_zone: name=zone1 state=configured path=/zones/zone1 root_password="Be9oX7OSwWoU." + config='set autoboot=true; add net; set physical=bge0; set address=10.1.1.1; end' + +# Attach a zone +solaris_zone: name=zone1 state=attached attach_options='-u' ''' class Zone(object): @@ -120,45 +141,60 @@ def __init__(self, module): self.timeout = self.module.params['timeout'] self.config = self.module.params['config'] self.create_options = self.module.params['create_options'] + self.attach_options = self.module.params['attach_options'] self.zoneadm_cmd = self.module.get_bin_path('zoneadm', True) self.zonecfg_cmd = self.module.get_bin_path('zonecfg', True) self.ssh_keygen_cmd = self.module.get_bin_path('ssh-keygen', True) - def create(self): + def configure(self): if not self.path: self.module.fail_json(msg='Missing required argument: path') - t = tempfile.NamedTemporaryFile(delete = False) + if not self.module.check_mode: + t = tempfile.NamedTemporaryFile(delete = False) - if self.sparse: - t.write('create %s\n' % self.create_options) - self.msg.append('creating sparse root zone') - else: - t.write('create -b %s\n' % self.create_options) - self.msg.append('creating whole root zone') - - t.write('set zonepath=%s\n' % self.path) + if self.sparse: + t.write('create %s\n' % self.create_options) + self.msg.append('creating sparse-root zone') + else: + t.write('create -b %s\n' % self.create_options) + self.msg.append('creating whole-root zone') - if self.config: - for line in self.config: - t.write('%s\n' % line) - t.close() + t.write('set zonepath=%s\n' % self.path) + t.write('%s\n' % self.config) + t.close() - cmd = '%s -z %s -f %s' % (self.zonecfg_cmd, self.name, t.name) - (rc, out, err) = self.module.run_command(cmd) - if rc != 0: - self.module.fail_json(msg='Failed to create zone. %s' % (out + err)) - os.unlink(t.name) + cmd = '%s -z %s -f %s' % (self.zonecfg_cmd, self.name, t.name) + (rc, out, err) = self.module.run_command(cmd) + if rc != 0: + self.module.fail_json(msg='Failed to create zone. %s' % (out + err)) + os.unlink(t.name) - cmd = '%s -z %s install' % (self.zoneadm_cmd, self.name) - (rc, out, err) = self.module.run_command(cmd) - if rc != 0: - self.module.fail_json(msg='Failed to install zone. %s' % (out + err)) + self.changed = True + self.msg.append('zone configured') - self.configure_sysid() - self.configure_password() - self.configure_ssh_keys() + def install(self): + if not self.module.check_mode: + cmd = '%s -z %s install' % (self.zoneadm_cmd, self.name) + (rc, out, err) = self.module.run_command(cmd) + if rc != 0: + self.module.fail_json(msg='Failed to install zone. %s' % (out + err)) + self.configure_sysid() + self.configure_password() + self.configure_ssh_keys() + self.changed = True + self.msg.append('zone installed') + + def uninstall(self): + if self.is_installed(): + if not self.module.check_mode: + cmd = '%s -z %s uninstall -F' % (self.zoneadm_cmd, self.name) + (rc, out, err) = self.module.run_command(cmd) + if rc != 0: + self.module.fail_json(msg='Failed to uninstall zone. %s' % (out + err)) + self.changed = True + self.msg.append('zone uninstalled') def configure_sysid(self): if os.path.isfile('%s/root/etc/.UNCONFIGURED' % self.path): @@ -208,51 +244,82 @@ def configure_password(self): lines = f.readlines() f.close() - for i in range(0, len(lines)): - fields = lines[i].split(':') + for i in range(0, len(lines)): + fields = lines[i].split(':') if fields[0] == 'root': - fields[1] = self.root_password + fields[1] = self.root_password lines[i] = ':'.join(fields) f = open(shadow, 'w') for line in lines: - f.write(line) + f.write(line) f.close() - + def boot(self): - cmd = '%s -z %s boot' % (self.zoneadm_cmd, self.name) - (rc, out, err) = self.module.run_command(cmd) - if rc != 0: - self.module.fail_json(msg='Failed to boot zone. %s' % (out + err)) - - """ - The boot command can return before the zone has fully booted. This is especially - true on the first boot when the zone initializes the SMF services. Unless the zone - has fully booted, subsequent tasks in the playbook may fail as services aren't running yet. - Wait until the zone's console login is running; once that's running, consider the zone booted. - """ - - elapsed = 0 - while True: - if elapsed > self.timeout: - self.module.fail_json(msg='timed out waiting for zone to boot') - rc = os.system('ps -z %s -o args|grep "/usr/lib/saf/ttymon.*-d /dev/console" > /dev/null 2>/dev/null' % self.name) - if rc == 0: - break - time.sleep(10) - elapsed += 10 + if not self.module.check_mode: + cmd = '%s -z %s boot' % (self.zoneadm_cmd, self.name) + (rc, out, err) = self.module.run_command(cmd) + if rc != 0: + self.module.fail_json(msg='Failed to boot zone. %s' % (out + err)) + + """ + The boot command can return before the zone has fully booted. This is especially + true on the first boot when the zone initializes the SMF services. Unless the zone + has fully booted, subsequent tasks in the playbook may fail as services aren't running yet. + Wait until the zone's console login is running; once that's running, consider the zone booted. + """ + + elapsed = 0 + while True: + if elapsed > self.timeout: + self.module.fail_json(msg='timed out waiting for zone to boot') + rc = os.system('ps -z %s -o args|grep "/usr/lib/saf/ttymon.*-d /dev/console" > /dev/null 2>/dev/null' % self.name) + if rc == 0: + break + time.sleep(10) + elapsed += 10 + self.changed = True + self.msg.append('zone booted') def destroy(self): - cmd = '%s -z %s delete -F' % (self.zonecfg_cmd, self.name) - (rc, out, err) = self.module.run_command(cmd) - if rc != 0: - self.module.fail_json(msg='Failed to delete zone. %s' % (out + err)) + if self.is_running(): + self.stop() + if self.is_installed(): + self.uninstall() + if not self.module.check_mode: + cmd = '%s -z %s delete -F' % (self.zonecfg_cmd, self.name) + (rc, out, err) = self.module.run_command(cmd) + if rc != 0: + self.module.fail_json(msg='Failed to delete zone. %s' % (out + err)) + self.changed = True + self.msg.append('zone deleted') def stop(self): - cmd = '%s -z %s halt' % (self.zoneadm_cmd, self.name) - (rc, out, err) = self.module.run_command(cmd) - if rc != 0: - self.module.fail_json(msg='Failed to stop zone. %s' % (out + err)) + if not self.module.check_mode: + cmd = '%s -z %s halt' % (self.zoneadm_cmd, self.name) + (rc, out, err) = self.module.run_command(cmd) + if rc != 0: + self.module.fail_json(msg='Failed to stop zone. %s' % (out + err)) + self.changed = True + self.msg.append('zone stopped') + + def detach(self): + if not self.module.check_mode: + cmd = '%s -z %s detach' % (self.zoneadm_cmd, self.name) + (rc, out, err) = self.module.run_command(cmd) + if rc != 0: + self.module.fail_json(msg='Failed to detach zone. %s' % (out + err)) + self.changed = True + self.msg.append('zone detached') + + def attach(self): + if not self.module.check_mode: + cmd = '%s -z %s attach %s' % (self.zoneadm_cmd, self.name, self.attach_options) + (rc, out, err) = self.module.run_command(cmd) + if rc != 0: + self.module.fail_json(msg='Failed to attach zone. %s' % (out + err)) + self.changed = True + self.msg.append('zone attached') def exists(self): cmd = '%s -z %s list' % (self.zoneadm_cmd, self.name) @@ -262,74 +329,85 @@ def exists(self): else: return False - def running(self): + def is_running(self): + return self.status() == 'running' + + def is_installed(self): + return self.status() == 'installed' + + def is_configured(self): + return self.status() == 'configured' + + def status(self): cmd = '%s -z %s list -p' % (self.zoneadm_cmd, self.name) (rc, out, err) = self.module.run_command(cmd) if rc != 0: self.module.fail_json(msg='Failed to determine zone state. %s' % (out + err)) - - if out.split(':')[2] == 'running': - return True - else: - return False - + return out.split(':')[2] def state_present(self): if self.exists(): self.msg.append('zone already exists') else: - if not self.module.check_mode: - self.create() - self.changed = True - self.msg.append('zone created') + self.configure() + self.install() def state_running(self): self.state_present() - if self.running(): + if self.is_running(): self.msg.append('zone already running') else: - if not self.module.check_mode: - self.boot() - self.changed = True - self.msg.append('zone booted') + self.boot() def state_stopped(self): if self.exists(): - if self.running(): - if not self.module.check_mode: - self.stop() - self.changed = True - self.msg.append('zone stopped') - else: - self.msg.append('zone not running') + self.stop() else: self.module.fail_json(msg='zone does not exist') def state_absent(self): if self.exists(): - self.state_stopped() - if not self.module.check_mode: - self.destroy() - self.changed = True - self.msg.append('zone deleted') + if self.is_running(): + self.stop() + self.destroy() else: self.msg.append('zone does not exist') - def exit_with_msg(self): - msg = ', '.join(self.msg) - self.module.exit_json(changed=self.changed, msg=msg) + def state_configured(self): + if self.exists(): + self.msg.append('zone already exists') + else: + self.configure() + + def state_detached(self): + if not self.exists(): + self.module.fail_json(msg='zone does not exist') + if self.is_configured(): + self.msg.append('zone already detached') + else: + self.stop() + self.detach() + + def state_attached(self): + if not self.exists(): + self.msg.append('zone does not exist') + if self.is_configured(): + self.attach() + else: + self.msg.append('zone already attached') def main(): module = AnsibleModule( argument_spec = dict( name = dict(required=True), - state = dict(required=True, choices=['running', 'started', 'present', 'stopped', 'absent']), + state = dict(default='present', choices=['running', 'started', 'present', 'installed', 'stopped', 'absent', 'configured', 'detached', 'attached']), path = dict(defalt=None), sparse = dict(default=False, type='bool'), root_password = dict(default=None), timeout = dict(default=600, type='int'), - config = dict(default=None, type='list'), + config = dict(default=''), create_options = dict(default=''), + attach_options = dict(default=''), ), supports_check_mode=True ) @@ -342,21 +420,27 @@ def main(): module.fail_json(msg='This module requires Solaris') zone = Zone(module) - + state = module.params['state'] if state == 'running' or state == 'started': zone.state_running() - elif state == 'present': + elif state == 'present' or state == 'installed': zone.state_present() elif state == 'stopped': zone.state_stopped() elif state == 'absent': zone.state_absent() + elif state == 'configured': + zone.state_configured() + elif state == 'detached': + zone.state_detached() + elif state == 'attached': + zone.state_attached() else: module.fail_json(msg='Invalid state: %s' % state) - zone.exit_with_msg() + module.exit_json(changed=zone.changed, msg=', '.join(zone.msg)) from ansible.module_utils.basic import * main() From 61769eda3340f7898c4ef62cbd4de5bd865e3b24 Mon Sep 17 00:00:00 2001 From: zimbatm Date: Wed, 24 Jun 2015 15:31:26 +0100 Subject: [PATCH 0370/2522] route53_health_check: new module Allows to define and update Route53 health-checks Create and update actions are defined in the module because boto is broken in the first case and doesn't implement the second-one. --- cloud/amazon/route53_health_check.py | 357 +++++++++++++++++++++++++++ 1 file changed, 357 insertions(+) create mode 100644 cloud/amazon/route53_health_check.py diff --git a/cloud/amazon/route53_health_check.py b/cloud/amazon/route53_health_check.py new file mode 100644 index 00000000000..6b4cd1924a7 --- /dev/null +++ b/cloud/amazon/route53_health_check.py @@ -0,0 +1,357 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: route53_health_check +short_description: add or delete health-checks in Amazons Route53 DNS service +description: + - Creates and deletes DNS Health checks in Amazons Route53 service + - Only the port, resource_path, string_match and request_interval are + considered when updating existing health-checks. +version_added: "2.0" +options: + state: + description: + - Specifies the action to take. + required: true + choices: [ 'present', 'absent' ] + ip_address: + description: + - IP address of the end-point to check. Either this or `fqdn` has to be + provided. + required: false + default: null + port: + description: + - The port on the endpoint on which you want Amazon Route 53 to perform + health checks. Required for TCP checks. + required: false + default: null + type: + description: + - The type of health check that you want to create, which indicates how + Amazon Route 53 determines whether an endpoint is healthy. + required: true + choices: [ 'HTTP', 'HTTPS', 'HTTP_STR_MATCH', 'HTTPS_STR_MATCH', 'TCP' ] + resource_path: + description: + - The path that you want Amazon Route 53 to request when performing + health checks. The path can be any value for which your endpoint will + return an HTTP status code of 2xx or 3xx when the endpoint is healthy, + for example the file /docs/route53-health-check.html. + + * Required for all checks except TCP. + * The path must begin with a / + * Maximum 255 characters. + required: false + default: null + fqdn: + description: + - Domain name of the endpoint to check. Either this or `ip_address` has + to be provided. When both are given the `fqdn` is used in the `Host:` + header of the HTTP request. + required: false + string_match: + description: + - If the check type is HTTP_STR_MATCH or HTTP_STR_MATCH, the string + that you want Amazon Route 53 to search for in the response body from + the specified resource. If the string appears in the first 5120 bytes + of the response body, Amazon Route 53 considers the resource healthy. + required: false + default: null + request_interval: + description: + - The number of seconds between the time that Amazon Route 53 gets a + response from your endpoint and the time that it sends the next + health-check request. + required: true + default: 30 + choices: [ 10, 30 ] + failure_threshold: + description: + - The number of consecutive health checks that an endpoint must pass or + fail for Amazon Route 53 to change the current status of the endpoint + from unhealthy to healthy or vice versa. + required: true + default: 3 + choices: [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ] +author: "zimbatm (@zimbatm)" +extends_documentation_fragment: aws +''' + +EXAMPLES = ''' +# Create a health-check for host1.example.com and use it in record +- route53_health_check: + state: present + fqdn: host1.example.com + type: HTTP_STR_MATCH + resource_path: / + string_match: "Hello" + request_interval: 10 + failure_threshold: 2 + record: my_health_check + +- route53: + action: create + zone: "example.com" + type: CNAME + record: "www.example.com" + value: host1.example.com + ttl: 30 + # Routing policy + identifier: "host1@www" + weight: 100 + health_check: "{{ my_health_check.health_check.id }}" + +# Delete health-check +- route53_health_check: + state: absent + fqdn: host1.example.com + +''' + +import time +import uuid + +try: + import boto + import boto.ec2 + from boto import route53 + from boto.route53 import Route53Connection, exception + from boto.route53.healthcheck import HealthCheck + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +# Things that can't get changed: +# protocol +# ip_address or domain +# request_interval +# string_match if not previously enabled +def find_health_check(conn, wanted): + """Searches for health checks that have the exact same set of immutable values""" + for check in conn.get_list_health_checks().HealthChecks: + config = check.HealthCheckConfig + if config.get('IPAddress') == wanted.ip_addr and config.get('FullyQualifiedDomainName') == wanted.fqdn and config.get('Type') == wanted.hc_type and config.get('RequestInterval') == str(wanted.request_interval): + return check + return None + +def to_health_check(config): + return HealthCheck( + config.get('IPAddress'), + config.get('Port'), + config.get('Type'), + config.get('ResourcePath'), + fqdn=config.get('FullyQualifiedDomainName'), + string_match=config.get('SearchString'), + request_interval=int(config.get('RequestInterval')), + failure_threshold=int(config.get('FailureThreshold')), + ) + +def health_check_diff(a, b): + a = a.__dict__ + b = b.__dict__ + if a == b: + return {} + diff = {} + for key in set(a.keys()) | set(b.keys()): + if a.get(key) != b.get(key): + diff[key] = b.get(key) + return diff + +def to_template_params(health_check): + params = { + 'ip_addr_part': '', + 'port': health_check.port, + 'type': health_check.hc_type, + 'resource_path_part': '', + 'fqdn_part': '', + 'string_match_part': '', + 'request_interval': health_check.request_interval, + 'failure_threshold': health_check.failure_threshold, + } + if health_check.ip_addr: + params['ip_addr_part'] = HealthCheck.XMLIpAddrPart % {'ip_addr': health_check.ip_addr} + if health_check.resource_path: + params['resource_path_part'] = XMLResourcePathPart % {'resource_path': health_check.resource_path} + if health_check.fqdn: + params['fqdn_part'] = HealthCheck.XMLFQDNPart % {'fqdn': health_check.fqdn} + if health_check.string_match: + params['string_match_part'] = HealthCheck.XMLStringMatchPart % {'string_match': health_check.string_match} + return params + +XMLResourcePathPart = """%(resource_path)s""" + +POSTXMLBody = """ + + %(caller_ref)s + + %(ip_addr_part)s + %(port)s + %(type)s + %(resource_path_part)s + %(fqdn_part)s + %(string_match_part)s + %(request_interval)s + %(failure_threshold)s + + + """ + +UPDATEHCXMLBody = """ + + %(health_check_version)s + %(ip_addr_part)s + %(port)s + %(resource_path_part)s + %(fqdn_part)s + %(string_match_part)s + %(failure_threshold)i + + """ + +def create_health_check(conn, health_check, caller_ref = None): + if caller_ref is None: + caller_ref = str(uuid.uuid4()) + uri = '/%s/healthcheck' % conn.Version + params = to_template_params(health_check) + params.update(xmlns=conn.XMLNameSpace, caller_ref=caller_ref) + + xml_body = POSTXMLBody % params + response = conn.make_request('POST', uri, {'Content-Type': 'text/xml'}, xml_body) + body = response.read() + boto.log.debug(body) + if response.status == 201: + e = boto.jsonresponse.Element() + h = boto.jsonresponse.XmlHandler(e, None) + h.parse(body) + return e + else: + raise exception.DNSServerError(response.status, response.reason, body) + +def update_health_check(conn, health_check_id, health_check_version, health_check): + uri = '/%s/healthcheck/%s' % (conn.Version, health_check_id) + params = to_template_params(health_check) + params.update( + xmlns=conn.XMLNameSpace, + health_check_version=health_check_version, + ) + xml_body = UPDATEHCXMLBody % params + response = conn.make_request('POST', uri, {'Content-Type': 'text/xml'}, xml_body) + body = response.read() + boto.log.debug(body) + if response.status not in (200, 204): + raise exception.DNSServerError(response.status, + response.reason, + body) + e = boto.jsonresponse.Element() + h = boto.jsonresponse.XmlHandler(e, None) + h.parse(body) + return e + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + state = dict(choices=['present', 'absent'], default='present'), + ip_address = dict(), + port = dict(type='int'), + type = dict(required=True, choices=['HTTP', 'HTTPS', 'HTTP_STR_MATCH', 'HTTPS_STR_MATCH', 'TCP']), + resource_path = dict(), + fqdn = dict(), + string_match = dict(), + request_interval = dict(type='int', choices=[10, 30], default=30), + failure_threshold = dict(type='int', choices=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], default=3), + ) + ) + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO: + module.fail_json(msg='boto 2.27.0+ required for this module') + + state_in = module.params.get('state') + ip_addr_in = module.params.get('ip_address') + port_in = module.params.get('port') + type_in = module.params.get('type') + resource_path_in = module.params.get('resource_path') + fqdn_in = module.params.get('fqdn') + string_match_in = module.params.get('string_match') + request_interval_in = module.params.get('request_interval') + failure_threshold_in = module.params.get('failure_threshold') + + if ip_addr_in is None and fqdn_in is None: + module.fail_json(msg="parameter 'ip_address' or 'fqdn' is required") + + # Default port + if port_in is None: + if type_in in ['HTTP', 'HTTP_STR_MATCH']: + port_in = 80 + elif type_in in ['HTTPS', 'HTTPS_STR_MATCH']: + port_in = 443 + else: + module.fail_json(msg="parameter 'port' is required for 'type' TCP") + + # string_match in relation with type + if type_in in ['HTTP_STR_MATCH', 'HTTPS_STR_MATCH']: + if string_match_in is None: + module.fail_json(msg="parameter 'string_match' is required for the HTTP(S)_STR_MATCH types") + elif len(string_match_in) > 255: + module.fail_json(msg="parameter 'string_match' is limited to 255 characters max") + elif string_match_in: + module.fail_json(msg="parameter 'string_match' argument is only for the HTTP(S)_STR_MATCH types") + + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module) + # connect to the route53 endpoint + try: + conn = Route53Connection(**aws_connect_kwargs) + except boto.exception.BotoServerError, e: + module.fail_json(msg = e.error_message) + + changed = False + action = None + check_id = None + wanted_config = HealthCheck(ip_addr_in, port_in, type_in, resource_path_in, fqdn_in, string_match_in, request_interval_in, failure_threshold_in) + existing_check = find_health_check(conn, wanted_config) + if existing_check: + check_id = existing_check.Id + existing_config = to_health_check(existing_check.HealthCheckConfig) + + if state_in == 'present': + if existing_check is None: + action = "create" + check_id = create_health_check(conn, wanted_config).HealthCheck.Id + changed = True + else: + diff = health_check_diff(existing_config, wanted_config) + if not diff: + action = "update" + update_health_check(conn, existing_check.Id, int(existing_check.HealthCheckVersion), wanted_config) + changed = True + elif state_in == 'absent': + if check_id: + action = "delete" + conn.delete_health_check(check_id) + changed = True + else: + module.fail_json(msg = "Logic Error: Unknown state") + + module.exit_json(changed=changed, health_check=dict(id=check_id), action=action) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +main() From 094ef92aeee49d97e754109df13affdf4739f71f Mon Sep 17 00:00:00 2001 From: John W Higgins Date: Fri, 3 Jul 2015 16:16:18 -0700 Subject: [PATCH 0371/2522] Switch to origin and cleanup --- system/zfs.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/system/zfs.py b/system/zfs.py index 00a81e32c54..f8a72a44f01 100644 --- a/system/zfs.py +++ b/system/zfs.py @@ -66,10 +66,6 @@ - The checksum property. required: False choices: ['on','off',fletcher2,fletcher4,sha256] - clone: - description: - - Name of the snapshot to clone - required: False compression: description: - The compression property. @@ -119,6 +115,10 @@ - The normalization property. required: False choices: [none,formC,formD,formKC,formKD] + origin: + description: + - Name of the snapshot to clone + required: False primarycache: description: - The primarycache property. @@ -225,6 +225,12 @@ # Create a new file system called myfs2 with snapdir enabled - zfs: name=rpool/myfs2 state=present snapdir=enabled + +# Create a new file system by cloning a snapshot +- zfs: name=rpool/cloned_fs state=present origin=rpool/myfs@mysnapshot + +# Destroy a filesystem +- zfs: name=rpool/myfs state=absent ''' @@ -257,10 +263,10 @@ def create(self): properties = self.properties volsize = properties.pop('volsize', None) volblocksize = properties.pop('volblocksize', None) - clone = properties.pop('clone', None) + origin = properties.pop('origin', None) if "@" in self.name: action = 'snapshot' - elif clone: + elif origin: action = 'clone' else: action = 'create' @@ -279,8 +285,8 @@ def create(self): if volsize: cmd.append('-V') cmd.append(volsize) - if clone: - cmd.append(clone) + if origin: + cmd.append(origin) cmd.append(self.name) (rc, err, out) = self.module.run_command(' '.join(cmd)) if rc == 0: @@ -356,7 +362,6 @@ def main(): 'canmount': {'required': False, 'choices':['on', 'off', 'noauto']}, 'casesensitivity': {'required': False, 'choices':['sensitive', 'insensitive', 'mixed']}, 'checksum': {'required': False, 'choices':['on', 'off', 'fletcher2', 'fletcher4', 'sha256']}, - 'clone': {'required': False}, 'compression': {'required': False, 'choices':['on', 'off', 'lzjb', 'gzip', 'gzip-1', 'gzip-2', 'gzip-3', 'gzip-4', 'gzip-5', 'gzip-6', 'gzip-7', 'gzip-8', 'gzip-9', 'lz4', 'zle']}, 'copies': {'required': False, 'choices':['1', '2', '3']}, 'createparent': {'required': False, 'choices':['on', 'off']}, @@ -370,6 +375,7 @@ def main(): 'mountpoint': {'required': False}, 'nbmand': {'required': False, 'choices':['on', 'off']}, 'normalization': {'required': False, 'choices':['none', 'formC', 'formD', 'formKC', 'formKD']}, + 'origin': {'required': False}, 'primarycache': {'required': False, 'choices':['all', 'none', 'metadata']}, 'quota': {'required': False}, 'readonly': {'required': False, 'choices':['on', 'off']}, From d46810fb5eb407c7050f2837355d8d3b7f3cbd9f Mon Sep 17 00:00:00 2001 From: Paul Markham Date: Sat, 4 Jul 2015 11:10:30 +1000 Subject: [PATCH 0372/2522] Documentation fixes --- system/solaris_zone.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/solaris_zone.py b/system/solaris_zone.py index fd143e9dc01..0f064f9efc0 100644 --- a/system/solaris_zone.py +++ b/system/solaris_zone.py @@ -32,7 +32,7 @@ version_added: "2.0" author: Paul Markham requirements: - - Solaris 10 or later + - Solaris 10 options: state: required: true @@ -46,7 +46,7 @@ - C(absent), destroy the zone. - C(configured), configure the ready so that it's to be attached. - C(attached), attach a zone, but do not boot it. - - C(detach), stop and detach a zone + - C(detached), shutdown and detach a zone choices: ['present', 'installed', 'started', 'running', 'stopped', 'absent', 'configured', 'attached', 'detached'] default: present name: From e2fc7b34a7b1f120d4776a41aa31bd37db1b47f2 Mon Sep 17 00:00:00 2001 From: Michael Perzel Date: Sat, 4 Jul 2015 07:51:19 -0500 Subject: [PATCH 0373/2522] Update bigip_api method to use variable name server --- network/f5/bigip_gtm_wide_ip.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/network/f5/bigip_gtm_wide_ip.py b/network/f5/bigip_gtm_wide_ip.py index f201e5d9760..29298114b83 100644 --- a/network/f5/bigip_gtm_wide_ip.py +++ b/network/f5/bigip_gtm_wide_ip.py @@ -80,8 +80,8 @@ else: bigsuds_found = True -def bigip_api(bigip, user, password): - api = bigsuds.BIGIP(hostname=bigip, username=user, password=password) +def bigip_api(server, user, password): + api = bigsuds.BIGIP(hostname=server, username=user, password=password) return api def get_wide_ip_lb_method(api, wide_ip): From 217221caed15d7a8cf714873253a885d5a64b6c3 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Sat, 4 Jul 2015 12:18:45 -0400 Subject: [PATCH 0374/2522] added version_added to new origin option --- system/zfs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/system/zfs.py b/system/zfs.py index f8a72a44f01..51b9db63692 100644 --- a/system/zfs.py +++ b/system/zfs.py @@ -119,6 +119,7 @@ description: - Name of the snapshot to clone required: False + version_added: "2.0" primarycache: description: - The primarycache property. From 311d73620b5788e43f43b7c0672c7b10254f3e4b Mon Sep 17 00:00:00 2001 From: Phil Date: Mon, 6 Jul 2015 09:59:51 -0500 Subject: [PATCH 0375/2522] use convertto-bool for rm and recurse params --- windows/win_unzip.ps1 | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/windows/win_unzip.ps1 b/windows/win_unzip.ps1 index e4509a290a2..a62f246f5c8 100644 --- a/windows/win_unzip.ps1 +++ b/windows/win_unzip.ps1 @@ -62,19 +62,18 @@ Else { Fail-Json $result "missing required argument: dest" } -If ($params.recurse -eq "true" -Or $params.recurse -eq "yes") { - $recurse = $true +If ($params.recurse) { + $recurse = ConvertTo-Bool ($params.recurse) } Else { $recurse = $false } -If ($params.rm -eq "true" -Or $params.rm -eq "yes"){ - $rm = $true - Set-Attr $result.win_unzip "rm" "true" -} -Else { - $rm = $false +If ($params.rm) { + $rm = ConvertTo-Bool ($params.rm) +} +Else { + $rm = $false } If ($ext -eq ".zip" -And $recurse -eq $false) { @@ -111,7 +110,7 @@ Else { If ($recurse) { Expand-Archive -Path $src -OutputPath $dest -Force - If ($rm) { + If ($rm -eq $true) { Get-ChildItem $dest -recurse | Where {$_.extension -eq ".gz" -Or $_.extension -eq ".zip" -Or $_.extension -eq ".bz2" -Or $_.extension -eq ".tar" -Or $_.extension -eq ".msu"} | % { Expand-Archive $_.FullName -OutputPath $dest -Force Remove-Item $_.FullName -Force From dc84f05feda906773bd116105fe46d4fc1642a70 Mon Sep 17 00:00:00 2001 From: Phil Date: Mon, 6 Jul 2015 13:14:18 -0500 Subject: [PATCH 0376/2522] changes check for src --- windows/win_acl.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/windows/win_acl.ps1 b/windows/win_acl.ps1 index 130b17e8304..39530866c61 100644 --- a/windows/win_acl.ps1 +++ b/windows/win_acl.ps1 @@ -30,8 +30,8 @@ $result = New-Object psobject @{ If ($params.src) { $src = $params.src.toString() - If (-Not (Test-Path -Path $src -PathType Leaf -Or Test-Path -Path $src -PathType Container)) { - Fail-Json $result "$src is not a valid file or directory on the host" + If (-Not (Test-Path -Path $src)) { + Fail-Json $result "$src file or directory does not exist on the host" } } Else { From 5a2a22bf68b6cd305e6dbfe1a0044eaf5d9398ed Mon Sep 17 00:00:00 2001 From: Adam Keech Date: Thu, 25 Jun 2015 14:18:57 -0400 Subject: [PATCH 0377/2522] Adding win_regedit module --- windows/win_regedit.ps1 | 173 ++++++++++++++++++++++++++++++++++++++++ windows/win_regedit.py | 101 +++++++++++++++++++++++ 2 files changed, 274 insertions(+) create mode 100644 windows/win_regedit.ps1 create mode 100644 windows/win_regedit.py diff --git a/windows/win_regedit.ps1 b/windows/win_regedit.ps1 new file mode 100644 index 00000000000..18cdc99c6ae --- /dev/null +++ b/windows/win_regedit.ps1 @@ -0,0 +1,173 @@ +#!powershell +# This file is part of Ansible +# +# (c) 2015, Adam Keech , Josh Ludwig +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +$ErrorActionPreference = "Stop" + +# WANT_JSON +# POWERSHELL_COMMON + +$params = Parse-Args $args; +$result = New-Object PSObject; +Set-Attr $result "changed" $false; + +If ($params.name) +{ + $registryKeyName = $params.name +} +Else +{ + Fail-Json $result "missing required argument: name" +} + +If ($params.state) +{ + $state = $params.state.ToString().ToLower() + If (($state -ne "present") -and ($state -ne "absent")) + { + Fail-Json $result "state is $state; must be present or absent" + } +} +Else +{ + $state = "present" +} + +If ($params.value) +{ + $registryKeyValue = $params.value +} +ElseIf ($state -eq "present") +{ + Fail-Json $result "missing required argument: value" +} + +If ($params.valuetype) +{ + $registryValueType = $params.valuetype.ToString().ToLower() + $validRegistryValueTypes = "binary", "dword", "expandstring", "multistring", "string", "qword" + If ($validRegistryValueTypes -notcontains $registryValueType) + { + Fail-Json $result "valuetype is $registryValueType; must be binary, dword, expandstring, multistring, string, or qword" + } +} +Else +{ + $registryValueType = "string" +} + +If ($params.path) +{ + $registryKeyPath = $params.path +} +Else +{ + Fail-Json $result "missing required argument: path" +} + +Function Test-RegistryValue { + Param ( + [parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()]$Path, + [parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()]$Value + ) + Try { + Get-ItemProperty -Path $Path -Name $Value + Return $true + } + Catch { + Return $false + } +} + +if($state -eq "present") { + if (Test-Path $registryKeyPath) { + if (Test-RegistryValue -Path $registryKeyPath -Value $registryKeyName) + { + # Changes Type and Value + If ((Get-Item $registryKeyPath).GetValueKind($registryKeyName) -ne $registryValueType) + { + Try + { + Remove-ItemProperty -Path $registryKeyPath -Name $registryKeyName + New-ItemProperty -Path $registryKeyPath -Name $registryKeyName -Value $registryKeyValue -PropertyType $registryValueType + $result.changed = $true + } + Catch + { + Fail-Json $result $_.Exception.Message + } + } + # Only Changes Value + ElseIf ((Get-ItemProperty -Path $registryKeyPath | Select-Object -ExpandProperty $registryKeyName) -ne $registryKeyValue) + { + Try { + Set-ItemProperty -Path $registryKeyPath -Name $registryKeyName -Value $registryKeyValue + $result.changed = $true + } + Catch + { + Fail-Json $result $_.Exception.Message + } + } + } + else + { + Try + { + New-ItemProperty -Path $registryKeyPath -Name $registryKeyName -Value $registryKeyValue -PropertyType $registryValueType + $result.changed = $true + } + Catch + { + Fail-Json $result $_.Exception.Message + } + } + } + else + { + Try + { + New-Item $registryKeyPath -Force | New-ItemProperty -Name $registryKeyName -Value $registryKeyValue -Force -PropertyType $registryValueType + $result.changed = $true + } + Catch + { + Fail-Json $result $_.Exception.Message + } + } +} +else +{ + if (Test-Path $registryKeyPath) + { + if (Test-RegistryValue -Path $registryKeyPath -Value $registryKeyName) { + Try + { + Remove-ItemProperty -Path $registryKeyPath -Name $registryKeyName + $result.changed = $true + } + Catch + { + Fail-Json $result $_.Exception.Message + } + } + } +} + +Exit-Json $result diff --git a/windows/win_regedit.py b/windows/win_regedit.py new file mode 100644 index 00000000000..007ddd4e8a9 --- /dev/null +++ b/windows/win_regedit.py @@ -0,0 +1,101 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Adam Keech , Josh Ludwig +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# this is a windows documentation stub. actual code lives in the .ps1 +# file of the same name + +DOCUMENTATION = ''' +--- +module: win_regedit +version_added: "2.0" +short_description: Add, Edit, or Remove Registry Key +description: + - Add, Edit, or Remove Registry Key using ItemProperties Cmdlets +options: + name: + description: + - Name of Registry Key + required: true + default: null + aliases: [] + value: + description: + - Value of Registry Key + required: false + default: null + aliases: [] + valuetype: + description: + - Type of Registry Key + required: false + choices: + - binary + - dword + - expandstring + - multistring + - string + - qword + default: string + aliases: [] + path: + description: + - Path of Registry Key + required: true + default: null + aliases: [] + state: + description: + - State of Registry Key + required: false + choices: + - present + - absent + default: present + aliases: [] +author: "Adam Keech (@smadam813), Josh Ludwig (@joshludwig)" +''' + +EXAMPLES = ''' + # Add Registry Key (Default is String) + win_regedit: + name: testkey + value: 1337 + path: HKCU:\Software\MyCompany + + # Add Registry Key with Type DWord + win_regedit: + name: testkey + value: 1337 + valuetype: dword + path: HKCU:\Software\MyCompany + + # Edit Registry Key called testkey + win_regedit: + name: testkey + value: 8008 + path: HKCU:\Software\MyCompany + + # Remove Registry Key called testkey + win_regedit: + name: testkey + path: HKCU:\Software\MyCompany + state: absent +''' + From e84666fd7428012e29e013e792aa5574e6892c75 Mon Sep 17 00:00:00 2001 From: Adam Keech Date: Wed, 1 Jul 2015 09:53:35 -0400 Subject: [PATCH 0378/2522] Renaming variables in win_regedit module to make more sense with actions that are happening. --- windows/win_regedit.ps1 | 49 +++++++++++++++++++++-------------------- windows/win_regedit.py | 43 ++++++++++++++++++------------------ 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/windows/win_regedit.ps1 b/windows/win_regedit.ps1 index 18cdc99c6ae..3e654202296 100644 --- a/windows/win_regedit.ps1 +++ b/windows/win_regedit.ps1 @@ -27,7 +27,7 @@ Set-Attr $result "changed" $false; If ($params.name) { - $registryKeyName = $params.name + $registryValueName = $params.name } Else { @@ -47,39 +47,39 @@ Else $state = "present" } -If ($params.value) +If ($params.data) { - $registryKeyValue = $params.value + $registryValueData = $params.data } ElseIf ($state -eq "present") { - Fail-Json $result "missing required argument: value" + Fail-Json $result "missing required argument: data" } -If ($params.valuetype) +If ($params.type) { - $registryValueType = $params.valuetype.ToString().ToLower() - $validRegistryValueTypes = "binary", "dword", "expandstring", "multistring", "string", "qword" - If ($validRegistryValueTypes -notcontains $registryValueType) + $registryDataType = $params.type.ToString().ToLower() + $validRegistryDataTypes = "binary", "dword", "expandstring", "multistring", "string", "qword" + If ($validRegistryDataTypes -notcontains $registryDataType) { - Fail-Json $result "valuetype is $registryValueType; must be binary, dword, expandstring, multistring, string, or qword" + Fail-Json $result "type is $registryDataType; must be binary, dword, expandstring, multistring, string, or qword" } } Else { - $registryValueType = "string" + $registryDataType = "string" } If ($params.path) { - $registryKeyPath = $params.path + $registryValuePath = $params.path } Else { Fail-Json $result "missing required argument: path" } -Function Test-RegistryValue { +Function Test-RegistryValueData { Param ( [parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()]$Path, @@ -96,16 +96,16 @@ Function Test-RegistryValue { } if($state -eq "present") { - if (Test-Path $registryKeyPath) { - if (Test-RegistryValue -Path $registryKeyPath -Value $registryKeyName) + if (Test-Path $registryValuePath) { + if (Test-RegistryValueData -Path $registryValuePath -Value $registryValueName) { # Changes Type and Value - If ((Get-Item $registryKeyPath).GetValueKind($registryKeyName) -ne $registryValueType) + If ((Get-Item $registryValuePath).GetValueKind($registryValueName) -ne $registryDataType) { Try { - Remove-ItemProperty -Path $registryKeyPath -Name $registryKeyName - New-ItemProperty -Path $registryKeyPath -Name $registryKeyName -Value $registryKeyValue -PropertyType $registryValueType + Remove-ItemProperty -Path $registryValuePath -Name $registryValueName + New-ItemProperty -Path $registryValuePath -Name $registryValueName -Value $registryValueData -PropertyType $registryDataType $result.changed = $true } Catch @@ -114,10 +114,10 @@ if($state -eq "present") { } } # Only Changes Value - ElseIf ((Get-ItemProperty -Path $registryKeyPath | Select-Object -ExpandProperty $registryKeyName) -ne $registryKeyValue) + ElseIf ((Get-ItemProperty -Path $registryValuePath | Select-Object -ExpandProperty $registryValueName) -ne $registryValueData) { Try { - Set-ItemProperty -Path $registryKeyPath -Name $registryKeyName -Value $registryKeyValue + Set-ItemProperty -Path $registryValuePath -Name $registryValueName -Value $registryValueData $result.changed = $true } Catch @@ -130,7 +130,7 @@ if($state -eq "present") { { Try { - New-ItemProperty -Path $registryKeyPath -Name $registryKeyName -Value $registryKeyValue -PropertyType $registryValueType + New-ItemProperty -Path $registryValuePath -Name $registryValueName -Value $registryValueData -PropertyType $registryDataType $result.changed = $true } Catch @@ -143,7 +143,7 @@ if($state -eq "present") { { Try { - New-Item $registryKeyPath -Force | New-ItemProperty -Name $registryKeyName -Value $registryKeyValue -Force -PropertyType $registryValueType + New-Item $registryValuePath -Force | New-ItemProperty -Name $registryValueName -Value $registryValueData -Force -PropertyType $registryDataType $result.changed = $true } Catch @@ -154,12 +154,12 @@ if($state -eq "present") { } else { - if (Test-Path $registryKeyPath) + if (Test-Path $registryValuePath) { - if (Test-RegistryValue -Path $registryKeyPath -Value $registryKeyName) { + if (Test-RegistryValueData -Path $registryValuePath -Value $registryValueName) { Try { - Remove-ItemProperty -Path $registryKeyPath -Name $registryKeyName + Remove-ItemProperty -Path $registryValuePath -Name $registryValueName $result.changed = $true } Catch @@ -171,3 +171,4 @@ else } Exit-Json $result + diff --git a/windows/win_regedit.py b/windows/win_regedit.py index 007ddd4e8a9..d8fd3a7c25e 100644 --- a/windows/win_regedit.py +++ b/windows/win_regedit.py @@ -25,25 +25,25 @@ --- module: win_regedit version_added: "2.0" -short_description: Add, Edit, or Remove Registry Key +short_description: Add, Edit, or Remove Registry Value description: - - Add, Edit, or Remove Registry Key using ItemProperties Cmdlets + - Add, Edit, or Remove Registry Value using ItemProperties Cmdlets options: name: description: - - Name of Registry Key + - Name of Registry Value required: true default: null aliases: [] - value: + data: description: - - Value of Registry Key + - Registry Value Data required: false default: null aliases: [] - valuetype: + type: description: - - Type of Registry Key + - Registry Value Data Type required: false choices: - binary @@ -56,13 +56,13 @@ aliases: [] path: description: - - Path of Registry Key + - Path of Registry Value required: true default: null aliases: [] state: description: - - State of Registry Key + - State of Registry Value required: false choices: - present @@ -73,29 +73,28 @@ ''' EXAMPLES = ''' - # Add Registry Key (Default is String) + # Add Registry Value (Default is String) win_regedit: - name: testkey - value: 1337 + name: testvalue + data: 1337 path: HKCU:\Software\MyCompany - # Add Registry Key with Type DWord + # Add Registry Value with Type DWord win_regedit: - name: testkey - value: 1337 - valuetype: dword + name: testvalue + data: 1337 + type: dword path: HKCU:\Software\MyCompany - # Edit Registry Key called testkey + # Edit Registry Value called testvalue win_regedit: - name: testkey - value: 8008 + name: testvalue + data: 8008 path: HKCU:\Software\MyCompany - # Remove Registry Key called testkey + # Remove Registry Value called testvalue win_regedit: - name: testkey + name: testvalue path: HKCU:\Software\MyCompany state: absent ''' - From 389e59b9708d005c73ed84b5c1703a4c9a3d931a Mon Sep 17 00:00:00 2001 From: Adam Keech Date: Mon, 6 Jul 2015 15:25:01 -0400 Subject: [PATCH 0379/2522] Adding functionality to not only edit Values, but also Keys. --- windows/win_regedit.ps1 | 80 ++++++++++++++++++++++++----------------- windows/win_regedit.py | 61 +++++++++++++++++-------------- 2 files changed, 83 insertions(+), 58 deletions(-) diff --git a/windows/win_regedit.ps1 b/windows/win_regedit.ps1 index 3e654202296..1a257413466 100644 --- a/windows/win_regedit.ps1 +++ b/windows/win_regedit.ps1 @@ -25,13 +25,22 @@ $params = Parse-Args $args; $result = New-Object PSObject; Set-Attr $result "changed" $false; -If ($params.name) +If ($params.key) { - $registryValueName = $params.name + $registryKey = $params.key } Else { - Fail-Json $result "missing required argument: name" + Fail-Json $result "missing required argument: key" +} + +If ($params.value) +{ + $registryValue = $params.value +} +Else +{ + $registryValue = $null } If ($params.state) @@ -49,16 +58,16 @@ Else If ($params.data) { - $registryValueData = $params.data + $registryData = $params.data } -ElseIf ($state -eq "present") +ElseIf ($state -eq "present" -and $registryValue -ne $null) { Fail-Json $result "missing required argument: data" } -If ($params.type) +If ($params.datatype) { - $registryDataType = $params.type.ToString().ToLower() + $registryDataType = $params.datatype.ToString().ToLower() $validRegistryDataTypes = "binary", "dword", "expandstring", "multistring", "string", "qword" If ($validRegistryDataTypes -notcontains $registryDataType) { @@ -70,15 +79,6 @@ Else $registryDataType = "string" } -If ($params.path) -{ - $registryValuePath = $params.path -} -Else -{ - Fail-Json $result "missing required argument: path" -} - Function Test-RegistryValueData { Param ( [parameter(Mandatory=$true)] @@ -96,16 +96,17 @@ Function Test-RegistryValueData { } if($state -eq "present") { - if (Test-Path $registryValuePath) { - if (Test-RegistryValueData -Path $registryValuePath -Value $registryValueName) + if ((Test-Path $registryKey) -and $registryValue -ne $null) + { + if (Test-RegistryValueData -Path $registryKey -Value $registryValue) { - # Changes Type and Value - If ((Get-Item $registryValuePath).GetValueKind($registryValueName) -ne $registryDataType) + # Changes Data and DataType + if ((Get-Item $registryKey).GetValueKind($registryValue) -ne $registryDataType) { Try { - Remove-ItemProperty -Path $registryValuePath -Name $registryValueName - New-ItemProperty -Path $registryValuePath -Name $registryValueName -Value $registryValueData -PropertyType $registryDataType + Remove-ItemProperty -Path $registryKey -Name $registryValue + New-ItemProperty -Path $registryKey -Name $registryValue -Value $registryData -PropertyType $registryDataType $result.changed = $true } Catch @@ -113,11 +114,11 @@ if($state -eq "present") { Fail-Json $result $_.Exception.Message } } - # Only Changes Value - ElseIf ((Get-ItemProperty -Path $registryValuePath | Select-Object -ExpandProperty $registryValueName) -ne $registryValueData) + # Changes Only Data + elseif ((Get-ItemProperty -Path $registryKey | Select-Object -ExpandProperty $registryValue) -ne $registryData) { Try { - Set-ItemProperty -Path $registryValuePath -Name $registryValueName -Value $registryValueData + Set-ItemProperty -Path $registryKey -Name $registryValue -Value $registryData $result.changed = $true } Catch @@ -130,7 +131,7 @@ if($state -eq "present") { { Try { - New-ItemProperty -Path $registryValuePath -Name $registryValueName -Value $registryValueData -PropertyType $registryDataType + New-ItemProperty -Path $registryKey -Name $registryValue -Value $registryData -PropertyType $registryDataType $result.changed = $true } Catch @@ -139,12 +140,17 @@ if($state -eq "present") { } } } - else + elseif(-not (Test-Path $registryKey)) { Try { - New-Item $registryValuePath -Force | New-ItemProperty -Name $registryValueName -Value $registryValueData -Force -PropertyType $registryDataType + $newRegistryKey = New-Item $registryKey -Force $result.changed = $true + + if($registryValue -ne $null) { + $newRegistryKey | New-ItemProperty -Name $registryValue -Value $registryData -Force -PropertyType $registryDataType + $result.changed = $true + } } Catch { @@ -154,12 +160,23 @@ if($state -eq "present") { } else { - if (Test-Path $registryValuePath) + if (Test-Path $registryKey) { - if (Test-RegistryValueData -Path $registryValuePath -Value $registryValueName) { + if ($registryValue -eq $null) { Try { - Remove-ItemProperty -Path $registryValuePath -Name $registryValueName + Remove-Item -Path $registryKey -Recurse + $result.changed = $true + } + Catch + { + Fail-Json $result $_.Exception.Message + } + } + elseif (Test-RegistryValueData -Path $registryKey -Value $registryValue) { + Try + { + Remove-ItemProperty -Path $registryKey -Name $registryValue $result.changed = $true } Catch @@ -171,4 +188,3 @@ else } Exit-Json $result - diff --git a/windows/win_regedit.py b/windows/win_regedit.py index d8fd3a7c25e..5087a5eaa8f 100644 --- a/windows/win_regedit.py +++ b/windows/win_regedit.py @@ -25,11 +25,17 @@ --- module: win_regedit version_added: "2.0" -short_description: Add, Edit, or Remove Registry Value +short_description: Add, Edit, or Remove Registry Keys and Values description: - - Add, Edit, or Remove Registry Value using ItemProperties Cmdlets + - Add, Edit, or Remove Registry Keys and Values using ItemProperties Cmdlets options: - name: + key: + description: + - Name of Registry Key + required: true + default: null + aliases: [] + value: description: - Name of Registry Value required: true @@ -41,7 +47,7 @@ required: false default: null aliases: [] - type: + datatype: description: - Registry Value Data Type required: false @@ -54,12 +60,6 @@ - qword default: string aliases: [] - path: - description: - - Path of Registry Value - required: true - default: null - aliases: [] state: description: - State of Registry Value @@ -73,28 +73,37 @@ ''' EXAMPLES = ''' - # Add Registry Value (Default is String) + # Creates Registry Key called MyCompany. win_regedit: - name: testvalue - data: 1337 - path: HKCU:\Software\MyCompany + key: HKCU:\Software\MyCompany + + # Creates Registry Key called MyCompany, + # a value within MyCompany Key called "hello", and + # data for the value "hello" containing "world". + win_regedit: + key: HKCU:\Software\MyCompany + value: hello + data: world - # Add Registry Value with Type DWord + # Creates Registry Key called MyCompany, + # a value within MyCompany Key called "hello", and + # data for the value "hello" containing "1337" as type "dword". win_regedit: - name: testvalue + key: HKCU:\Software\MyCompany + value: hello data: 1337 - type: dword - path: HKCU:\Software\MyCompany + datatype: dword - # Edit Registry Value called testvalue + # Delete Registry Key MyCompany + # NOTE: Not specifying a value will delete the root key which means + # all values will be deleted win_regedit: - name: testvalue - data: 8008 - path: HKCU:\Software\MyCompany - - # Remove Registry Value called testvalue + key: HKCU:\Software\MyCompany + state: absent + + # Delete Registry Value "hello" from MyCompany Key win_regedit: - name: testvalue - path: HKCU:\Software\MyCompany + key: HKCU:\Software\MyCompany + value: hello state: absent ''' From 2097dccaa4e1eb8441b24786c766cb68456637a7 Mon Sep 17 00:00:00 2001 From: Matt Baldwin Date: Mon, 6 Jul 2015 13:25:15 -0700 Subject: [PATCH 0380/2522] Ansible ProfitBricks Compute Module. --- cloud/profitbricks/__init__.py | 0 cloud/profitbricks/profitbricks.py | 632 +++++++++++++++++++++++++++++ 2 files changed, 632 insertions(+) create mode 100644 cloud/profitbricks/__init__.py create mode 100644 cloud/profitbricks/profitbricks.py diff --git a/cloud/profitbricks/__init__.py b/cloud/profitbricks/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cloud/profitbricks/profitbricks.py b/cloud/profitbricks/profitbricks.py new file mode 100644 index 00000000000..561dbfef63c --- /dev/null +++ b/cloud/profitbricks/profitbricks.py @@ -0,0 +1,632 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: profitbricks +short_description: Create, destroy, start, stop, and reboot a ProfitBricks virtual machine. +description: + - Create, destroy, update, start, stop, and reboot a ProfitBricks virtual machine. When the virtual machine is created it can optionally wait for it to be 'running' before returning. This module has a dependency on profitbricks >= 1.0.0 +version_added: "1.9" +options: + auto_increment: + description: + - Whether or not to increment a single number in the name for created virtual machines. + default: yes + choices: ["yes", "no"] + name: + description: + - The name of the virtual machine. + required: true + default: null + image: + description: + - The system image ID for creating the virtual machine, e.g. a3eae284-a2fe-11e4-b187-5f1f641608c8. + required: true + default: null + datacenter: + description: + - The Datacenter to provision this virtual machine. + required: false + default: null + cores: + description: + - The number of CPU cores to allocate to the virtual machine. + required: false + default: 2 + ram: + description: + - The amount of memory to allocate to the virtual machine. + required: false + default: 2048 + volume_size: + description: + - The size in GB of the boot volume. + required: false + default: 10 + bus: + description: + - The bus type for the volume. + required: false + default: VIRTIO + choices: [ "IDE", "VIRTIO"] + instance_ids: + description: + - list of instance ids, currently only used when state='absent' to remove instances. + required: false + count: + description: + - The number of virtual machines to create. + required: false + default: 1 + location: + description: + - The datacenter location. Use only if you want to create the Datacenter or else this value is ignored. + required: false + default: us/las + choices: [ "us/las", "us/lasdev", "de/fra", "de/fkb" ] + assign_public_ip: + description: + - This will assign the machine to the public LAN. If no LAN exists with public Internet access it is created. + required: false + default: false + lan: + description: + - The ID of the LAN you wish to add the servers to. + required: false + default: 1 + subscription_user: + description: + - The ProfitBricks username. Overrides the PB_SUBSCRIPTION_ID environement variable. + required: false + default: null + subscription_password: + description: + - THe ProfitBricks password. Overrides the PB_PASSWORD environement variable. + required: false + default: null + wait: + description: + - wait for the instance to be in state 'running' before returning + required: false + default: "yes" + choices: [ "yes", "no" ] + aliases: [] + wait_timeout: + description: + - how long before wait gives up, in seconds + default: 600 + aliases: [] + remove_boot_volume: + description: + - remove the bootVolume of the virtual machine you're destroying. + required: false + default: "yes" + choices: ["yes", "no"] + state: + description: + - create or terminate instances + required: false + default: 'present' + aliases: [] + +requirements: [ "profitbricks" ] +author: Matt Baldwin (baldwin@stackpointcloud.com) +''' + +EXAMPLES = ''' + +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Provisioning example. This will create three servers and enumerate their names. + +- profitbricks: + datacenter: Tardis One + name: web%02d.stackpointcloud.com + cores: 4 + ram: 2048 + volume_size: 50 + image: a3eae284-a2fe-11e4-b187-5f1f641608c8 + location: us/las + count: 3 + assign_public_ip: true + +# Removing Virtual machines + +- profitbricks: + datacenter: Tardis One + instance_ids: + - 'web001.stackpointcloud.com' + - 'web002.stackpointcloud.com' + - 'web003.stackpointcloud.com' + wait_timeout: 500 + state: absent + +# Starting Virtual Machines. + +- profitbricks: + datacenter: Tardis One + instance_ids: + - 'web001.stackpointcloud.com' + - 'web002.stackpointcloud.com' + - 'web003.stackpointcloud.com' + wait_timeout: 500 + state: running + +# Stopping Virtual Machines + +- profitbricks: + datacenter: Tardis One + instance_ids: + - 'web001.stackpointcloud.com' + - 'web002.stackpointcloud.com' + - 'web003.stackpointcloud.com' + wait_timeout: 500 + state: stopped + +''' + +import re +import uuid +import time +import json +import sys + +try: + from profitbricks.client import ProfitBricksService, Volume, Server, Datacenter, NIC, LAN +except ImportError: + print "failed=True msg='profitbricks required for this module'" + sys.exit(1) + +LOCATIONS = ['us/las', + 'de/fra', + 'de/fkb', + 'us/lasdev'] + +uuid_match = re.compile( + '[\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12}', re.I) + + +def _wait_for_completion(profitbricks, promise, wait_timeout, msg): + if not promise: return + wait_timeout = time.time() + wait_timeout + while wait_timeout > time.time(): + time.sleep(5) + operation_result = profitbricks.get_request( + request_id=promise['requestId'], + status=True) + + if operation_result['metadata']['status'] == "DONE": + return + elif operation_result['metadata']['status'] == "FAILED": + raise Exception( + 'Request failed to complete ' + msg + ' "' + str( + promise['requestId']) + '" to complete.') + + raise Exception( + 'Timed out waiting for async operation ' + msg + ' "' + str( + promise['requestId'] + ) + '" to complete.') + +def _create_machine(module, profitbricks, datacenter, name): + image = module.params.get('image') + cores = module.params.get('cores') + ram = module.params.get('ram') + volume_size = module.params.get('volume_size') + bus = module.params.get('bus') + lan = module.params.get('lan') + assign_public_ip = module.params.get('assign_public_ip') + subscription_user = module.params.get('subscription_user') + subscription_password = module.params.get('subscription_password') + location = module.params.get('location') + image = module.params.get('image') + assign_public_ip = module.boolean(module.params.get('assign_public_ip')) + wait = module.params.get('wait') + wait_timeout = int(module.params.get('wait_timeout')) + + try: + # Generate name, but grab first 10 chars so we don't + # screw up the uuid match routine. + v = Volume( + name=str(uuid.uuid4()).replace('-','')[:10], + size=volume_size, + image=image, + bus=bus) + + volume_response = profitbricks.create_volume( + datacenter_id=datacenter, volume=v) + + # We're forced to wait on the volume creation since + # server create relies upon this existing. + + _wait_for_completion(profitbricks, volume_response, + wait_timeout, "create_volume") + except Exception as e: + module.fail_json(msg="failed to create the new volume: %s" % str(e)) + + if assign_public_ip: + public_found = False + + lans = profitbricks.list_lans(datacenter) + for lan in lans['items']: + if lan['properties']['public']: + public_found = True + lan = lan['id'] + + if not public_found: + i = LAN( + name='public', + public=True) + + lan_response = profitbricks.create_lan(datacenter, i) + + lan = lan_response['id'] + + _wait_for_completion(profitbricks, lan_response, + wait_timeout, "_create_machine") + + try: + n = NIC( + lan=int(lan) + ) + + nics = [n] + + s = Server( + name=name, + ram=ram, + cores=cores, + nics=nics, + boot_volume_id=volume_response['id'] + ) + + server_response = profitbricks.create_server( + datacenter_id=datacenter, server=s) + + if wait: + _wait_for_completion(profitbricks, server_response, + wait_timeout, "create_virtual_machine") + + + + # return (json.dumps(server_response)) + return (server_response) + except Exception as e: + module.fail_json(msg="failed to create the new server: %s" % str(e)) + +def _remove_machine(module, profitbricks, datacenter, name): + remove_boot_volume = module.params.get('remove_boot_volume') + wait = module.params.get('wait') + wait_timeout = int(module.params.get('wait_timeout')) + changed = False + + # User provided the actual UUID instead of the name. + try: + if remove_boot_volume: + # Collect information needed for later. + server = profitbricks.get_server(datacenter, name) + volume_id = server['properties']['bootVolume']['href'].split('/')[7] + + server_response = profitbricks.delete_server(datacenter, name) + changed = True + + except Exception as e: + module.fail_json(msg="failed to terminate the virtual server: %s" % str(e)) + + # Remove the bootVolume + if remove_boot_volume: + try: + volume_response = profitbricks.delete_volume(datacenter, volume_id) + + except Exception as e: + module.fail_json(msg="failed to remove the virtual server's bootvolume: %s" % str(e)) + + return changed + +def _startstop_machine(module, profitbricks, datacenter, name): + state = module.params.get('state') + + try: + if state == 'running': + profitbricks.start_server(datacenter, name) + else: + profitbricks.stop_server(datacenter, name) + + return True + except Exception as e: + module.fail_json(msg="failed to start or stop the virtual machine %s: %s" % (name, str(e))) + +def _create_datacenter(module, profitbricks): + datacenter = module.params.get('datacenter') + location = module.params.get('location') + wait_timeout = int(module.params.get('wait_timeout')) + + i = Datacenter( + name=datacenter, + location=location + ) + + try: + datacenter_response = profitbricks.create_datacenter(datacenter=i) + + _wait_for_completion(profitbricks, datacenter_response, + wait_timeout, "_create_datacenter") + + return datacenter_response + except Exception as e: + module.fail_json(msg="failed to create the new server(s): %s" % str(e)) + +def create_virtual_machine(module, profitbricks): + """ + Create new virtual machine + + module : AnsibleModule object + profitbricks: authenticated profitbricks object + + Returns: + True if a new virtual machine was created, false otherwise + """ + datacenter = module.params.get('datacenter') + name = module.params.get('name') + auto_increment = module.params.get('auto_increment') + count = module.params.get('count') + lan = module.params.get('lan') + wait_timeout = int(module.params.get('wait_timeout')) + failed = True + datacenter_found = False + + virtual_machines = [] + virtual_machine_ids = [] + + # Locate UUID for Datacenter + if not (uuid_match.match(datacenter)): + datacenter_list = profitbricks.list_datacenters() + for d in datacenter_list['items']: + dc = profitbricks.get_datacenter(d['id']) + if datacenter == dc['properties']['name']: + datacenter = d['id'] + datacenter_found = True + break + + if not datacenter_found: + datacenter_response = _create_datacenter(module, profitbricks) + datacenter = datacenter_response['id'] + + _wait_for_completion(profitbricks, datacenter_response, + wait_timeout, "create_virtual_machine") + + if auto_increment: + numbers = set() + count_offset = 1 + + try: + name % 0 + except TypeError, e: + if e.message.startswith('not all'): + name = '%s%%d' % name + else: + module.fail_json(msg=e.message) + + number_range = xrange(count_offset,count_offset + count + len(numbers)) + available_numbers = list(set(number_range).difference(numbers)) + names = [] + numbers_to_use = available_numbers[:count] + for number in numbers_to_use: + names.append(name % number) + else: + names = [name] * count + + for name in names: + create_response = _create_machine(module, profitbricks, str(datacenter), name) + nics = profitbricks.list_nics(datacenter,create_response['id']) + for n in nics['items']: + if lan == n['properties']['lan']: + create_response.update({ 'public_ip': n['properties']['ips'][0] }) + + virtual_machines.append(create_response) + failed = False + + results = { + 'failed': failed, + 'machines': virtual_machines, + 'action': 'create', + 'instance_ids': { + 'instances': [i['id'] for i in virtual_machines], + } + } + + return results + +def remove_virtual_machine(module, profitbricks): + """ + Removes a virtual machine. + + This will remove the virtual machine along with the bootVolume. + + module : AnsibleModule object + profitbricks: authenticated profitbricks object. + + Not yet supported: handle deletion of attached data disks. + + Returns: + True if a new virtual server was deleted, false otherwise + """ + if not isinstance(module.params.get('instance_ids'), list) or len(module.params.get('instance_ids')) < 1: + module.fail_json(msg='instance_ids should be a list of virtual machine ids or names, aborting') + + datacenter = module.params.get('datacenter') + instance_ids = module.params.get('instance_ids') + + # Locate UUID for Datacenter + if not (uuid_match.match(datacenter)): + datacenter_list = profitbricks.list_datacenters() + for d in datacenter_list['items']: + dc = profitbricks.get_datacenter(d['id']) + if datacenter == dc['properties']['name']: + datacenter = d['id'] + break + + for n in instance_ids: + if(uuid_match.match(n)): + _remove_machine(module, profitbricks, d['id'], n) + else: + servers = profitbricks.list_servers(d['id']) + + for s in servers['items']: + if n == s['properties']['name']: + server_id = s['id'] + + _remove_machine(module, profitbricks, datacenter, server_id) + +def startstop_machine(module, profitbricks, state): + """ + Starts or Stops a virtual machine. + + module : AnsibleModule object + profitbricks: authenticated profitbricks object. + + Returns: + True when the servers process the action successfully, false otherwise. + """ + if not isinstance(module.params.get('instance_ids'), list) or len(module.params.get('instance_ids')) < 1: + module.fail_json(msg='instance_ids should be a list of virtual machine ids or names, aborting') + + wait = module.params.get('wait') + wait_timeout = int(module.params.get('wait_timeout')) + changed = False + + datacenter = module.params.get('datacenter') + instance_ids = module.params.get('instance_ids') + + # Locate UUID for Datacenter + if not (uuid_match.match(datacenter)): + datacenter_list = profitbricks.list_datacenters() + for d in datacenter_list['items']: + dc = profitbricks.get_datacenter(d['id']) + if datacenter == dc['properties']['name']: + datacenter = d['id'] + break + + for n in instance_ids: + if(uuid_match.match(n)): + _startstop_machine(module, profitbricks, datacenter, n) + + changed = True + else: + servers = profitbricks.list_servers(d['id']) + + for s in servers['items']: + if n == s['properties']['name']: + server_id = s['id'] + _startstop_machine(module, profitbricks, datacenter, server_id) + + changed = True + + if wait: + wait_timeout = time.time() + wait_timeout + while wait_timeout > time.time(): + matched_instances = [] + for res in profitbricks.list_servers(datacenter)['items']: + if state == 'running': + if res['properties']['vmState'].lower() == state: + matched_instances.append(res) + elif state == 'stopped': + if res['properties']['vmState'].lower() == 'shutoff': + matched_instances.append(res) + + if len(matched_instances) < len(instance_ids): + time.sleep(5) + else: + break + + if wait_timeout <= time.time(): + # waiting took too long + module.fail_json(msg = "wait for virtual machine state timeout on %s" % time.asctime()) + + return (changed) + +def main(): + module = AnsibleModule( + argument_spec=dict( + datacenter=dict(), + name=dict(), + image=dict(), + cores=dict(default=2), + ram=dict(default=2048), + volume_size=dict(default=10), + bus=dict(default='VIRTIO'), + lan=dict(default=1), + count=dict(default=1), + auto_increment=dict(type='bool', default=True), + instance_ids=dict(), + subscription_user=dict(), + subscription_password=dict(), + location=dict(choices=LOCATIONS, default='us/las'), + assign_public_ip=dict(type='bool', default=False), + wait=dict(type='bool', default=True), + wait_timeout=dict(default=600), + remove_boot_volume=dict(type='bool', default=True), + state=dict(default='present'), + ) + ) + + subscription_user = module.params.get('subscription_user') + subscription_password = module.params.get('subscription_password') + wait = module.params.get('wait') + wait_timeout = int(module.params.get('wait_timeout')) + + profitbricks = ProfitBricksService( + username=subscription_user, + password=subscription_password) + + state = module.params.get('state') + + if state == 'absent': + if not module.params.get('datacenter'): + module.fail_json(msg='datacenter parameter is required for running or stopping machines.') + + (changed) = remove_virtual_machine(module, profitbricks) + + module.exit_json( + changed=changed) + + elif state in ('running', 'stopped'): + if not module.params.get('datacenter'): + module.fail_json(msg='datacenter parameter is required for running or stopping machines.') + + (changed) = startstop_machine(module, profitbricks, state) + + module.exit_json( + changed=changed) + + elif state == 'present': + if not module.params.get('name'): + module.fail_json(msg='name parameter is required for new instance') + if not module.params.get('image'): + module.fail_json(msg='image parameter is required for new instance') + if not module.params.get('subscription_user'): + module.fail_json(msg='subscription_user parameter is required for new instance') + if not module.params.get('subscription_password'): + module.fail_json(msg='subscription_password parameter is required for new instance') + + (machine_dict_array) = create_virtual_machine(module, profitbricks) + + module.exit_json(**machine_dict_array) + + +from ansible.module_utils.basic import * + +main() From 54a79ff85614d9936b2cfdc215020befa03fbae3 Mon Sep 17 00:00:00 2001 From: dohoangkhiem Date: Tue, 7 Jul 2015 11:36:25 +0700 Subject: [PATCH 0381/2522] New module: gce_tag for add/remove tags to/from GCE instance --- cloud/google/gce_tag.py | 236 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 cloud/google/gce_tag.py diff --git a/cloud/google/gce_tag.py b/cloud/google/gce_tag.py new file mode 100644 index 00000000000..205f52cb393 --- /dev/null +++ b/cloud/google/gce_tag.py @@ -0,0 +1,236 @@ +#!/usr/bin/python +# Copyright 2015 Google Inc. All Rights Reserved. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see + +DOCUMENTATION = ''' +--- +module: gce_tag +version_added: "2.0" +short_description: add or remove tag(s) to/from GCE instance +description: + - This module can add or remove tags U(https://cloud.google.com/compute/docs/instances/#tags) + to/from GCE instance. +options: + instance_name: + description: + - the name of the GCE instance to add/remove tags + required: true + default: null + aliases: [] + tags: + description: + - comma-separated list of tags to add or remove + required: true + default: null + aliases: [] + state: + description: + - desired state of the tags + required: false + default: "present" + choices: ["present", "absent"] + aliases: [] + zone: + description: + - the zone of the disk specified by source + required: false + default: "us-central1-a" + aliases: [] + service_account_email: + description: + - service account email + required: false + default: null + aliases: [] + pem_file: + description: + - path to the pem file associated with the service account email + required: false + default: null + aliases: [] + project_id: + description: + - your GCE project ID + required: false + default: null + aliases: [] + +requirements: + - "python >= 2.6" + - "apache-libcloud" +author: "Do Hoang Khiem (dohoangkhiem@gmail.com)" +''' + +EXAMPLES = ''' +# Add tags 'http-server', 'https-server', 'staging' to instance name 'staging-server' in zone us-central1-a. +- gce_tag: + instance_name: staging-server + tags: http-server,https-server,staging + zone: us-central1-a + state: present + +# Remove tags 'foo', 'bar' from instance 'test-server' in default zone (us-central1-a) +- gce_tag: + instance_name: test-server + tags: foo,bar + state: absent + +''' + +try: + from libcloud.compute.types import Provider + from libcloud.compute.providers import get_driver + from libcloud.common.google import GoogleBaseError, QuotaExceededError, \ + ResourceExistsError, ResourceNotFoundError, InvalidRequestError + + _ = Provider.GCE + HAS_LIBCLOUD = True +except ImportError: + HAS_LIBCLOUD = False + + +def add_tags(gce, module, instance_name, tags): + """Add tags to instance.""" + zone = module.params.get('zone') + + if not instance_name: + module.fail_json(msg='Must supply instance_name', changed=False) + + if not tags: + module.fail_json(msg='Must supply tags', changed=False) + + tags = [x.lower() for x in tags] + + try: + node = gce.ex_get_node(instance_name, zone=zone) + except ResourceNotFoundError: + module.fail_json(msg='Instance %s not found in zone %s' % (instance_name, zone), changed=False) + except GoogleBaseError, e: + module.fail_json(msg=str(e), changed=False) + + node_tags = node.extra['tags'] + changed = False + tags_changed = [] + + for t in tags: + if t not in node_tags: + changed = True + node_tags.append(t) + tags_changed.append(t) + + if not changed: + return False, None + + try: + gce.ex_set_node_tags(node, node_tags) + return True, tags_changed + except (GoogleBaseError, InvalidRequestError) as e: + module.fail_json(msg=str(e), changed=False) + + +def remove_tags(gce, module, instance_name, tags): + """Remove tags from instance.""" + zone = module.params.get('zone') + + if not instance_name: + module.fail_json(msg='Must supply instance_name', changed=False) + + if not tags: + module.fail_json(msg='Must supply tags', changed=False) + + tags = [x.lower() for x in tags] + + try: + node = gce.ex_get_node(instance_name, zone=zone) + except ResourceNotFoundError: + module.fail_json(msg='Instance %s not found in zone %s' % (instance_name, zone), changed=False) + except GoogleBaseError, e: + module.fail_json(msg=str(e), changed=False) + + node_tags = node.extra['tags'] + + changed = False + tags_changed = [] + + for t in tags: + if t in node_tags: + node_tags.remove(t) + changed = True + tags_changed.append(t) + + if not changed: + return False, None + + try: + gce.ex_set_node_tags(node, node_tags) + return True, tags_changed + except (GoogleBaseError, InvalidRequestError) as e: + module.fail_json(msg=str(e), changed=False) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + instance_name=dict(required=True), + tags=dict(type='list'), + state=dict(default='present', choices=['present', 'absent']), + zone=dict(default='us-central1-a'), + service_account_email=dict(), + pem_file=dict(), + project_id=dict(), + ) + ) + + if not HAS_LIBCLOUD: + module.fail_json(msg='libcloud with GCE support is required.') + + instance_name = module.params.get('instance_name') + state = module.params.get('state') + tags = module.params.get('tags') + zone = module.params.get('zone') + changed = False + + if not zone: + module.fail_json(msg='Must specify "zone"', changed=False) + + if not tags: + module.fail_json(msg='Must specify "tags"', changed=False) + + gce = gce_connect(module) + + # add tags to instance. + if state == 'present': + results = add_tags(gce, module, instance_name, tags) + changed = results[0] + tags_changed = results[1] + + # remove tags from instance + if state == 'absent': + results = remove_tags(gce, module, instance_name, tags) + changed = results[0] + tags_changed = results[1] + + module.exit_json(changed=changed, instance_name=instance_name, tags=tags_changed, zone=zone) + sys.exit(0) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.gce import * + +if __name__ == '__main__': + main() + From 9fa5152a94449830ba8ba0dec980fa2d347d4248 Mon Sep 17 00:00:00 2001 From: Adrian Muraru Date: Sun, 14 Jun 2015 13:34:15 +0300 Subject: [PATCH 0382/2522] Add option to send a private message in irc module --- notification/irc.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/notification/irc.py b/notification/irc.py index e6852c8510a..faaa7805629 100644 --- a/notification/irc.py +++ b/notification/irc.py @@ -63,6 +63,10 @@ description: - Channel name required: true + nick_to: + description: + - Nick to send the message to + required: false key: description: - Channel key @@ -113,7 +117,7 @@ def send_msg(channel, msg, server='localhost', port='6667', key=None, topic=None, - nick="ansible", color='none', passwd=False, timeout=30, use_ssl=False): + nick="ansible", nick_to=None, color='none', passwd=False, timeout=30, use_ssl=False): '''send message to IRC''' colornumbers = { @@ -173,7 +177,10 @@ def send_msg(channel, msg, server='localhost', port='6667', key=None, topic=None irc.send('TOPIC %s :%s\r\n' % (channel, topic)) sleep(1) - irc.send('PRIVMSG %s :%s\r\n' % (channel, message)) + if nick_to: + irc.send('PRIVMSG %s :%s\r\n' % (nick_to, message)) + else: + irc.send('PRIVMSG %s :%s\r\n' % (channel, message)) sleep(1) irc.send('PART %s\r\n' % channel) irc.send('QUIT\r\n') @@ -191,6 +198,7 @@ def main(): server=dict(default='localhost'), port=dict(default=6667), nick=dict(default='ansible'), + nick_to=dict(), msg=dict(required=True), color=dict(default="none", choices=["yellow", "red", "green", "blue", "black", "none"]), @@ -208,6 +216,7 @@ def main(): port = module.params["port"] nick = module.params["nick"] topic = module.params["topic"] + nick_to = module.params["nick_to"] msg = module.params["msg"] color = module.params["color"] channel = module.params["channel"] @@ -217,7 +226,7 @@ def main(): use_ssl = module.params["use_ssl"] try: - send_msg(channel, msg, server, port, key, topic, nick, color, passwd, timeout, use_ssl) + send_msg(channel, msg, server, port, key, topic, nick, nick_to, color, passwd, timeout, use_ssl) except Exception, e: module.fail_json(msg="unable to send to IRC: %s" % e) From 2dd32236ebec688a885adaddd2354f127dfd2b4d Mon Sep 17 00:00:00 2001 From: Adrian Muraru Date: Tue, 7 Jul 2015 13:35:11 +0300 Subject: [PATCH 0383/2522] Implemented comments --- notification/irc.py | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/notification/irc.py b/notification/irc.py index faaa7805629..2c3c19be4dd 100644 --- a/notification/irc.py +++ b/notification/irc.py @@ -39,7 +39,7 @@ default: 6667 nick: description: - - Nickname. May be shortened, depending on server's NICKLEN setting. + - Nickname to send the message from. May be shortened, depending on server's NICKLEN setting. required: false default: ansible msg: @@ -65,8 +65,9 @@ required: true nick_to: description: - - Nick to send the message to + - A list of nicknames to send the message to. When both channel and nick_to are defined, the message will be send to both of them. required: false + version_added: 2.0 key: description: - Channel key @@ -99,7 +100,16 @@ - irc: server=irc.example.net channel="#t1" msg="Hello world" - local_action: irc port=6669 + server="irc.example.net" + channel="#t1" + msg="All finished at {{ ansible_date_time.iso8601 }}" + color=red + nick=ansibleIRC + +- local_action: irc port=6669 + server="irc.example.net" channel="#t1" + nick_to=["nick1", "nick2"] msg="All finished at {{ ansible_date_time.iso8601 }}" color=red nick=ansibleIRC @@ -116,8 +126,8 @@ from time import sleep -def send_msg(channel, msg, server='localhost', port='6667', key=None, topic=None, - nick="ansible", nick_to=None, color='none', passwd=False, timeout=30, use_ssl=False): +def send_msg(msg, server='localhost', port='6667', channel=None, nick_to=[], key=None, topic=None, + nick="ansible", color='none', passwd=False, timeout=30, use_ssl=False): '''send message to IRC''' colornumbers = { @@ -178,8 +188,9 @@ def send_msg(channel, msg, server='localhost', port='6667', key=None, topic=None sleep(1) if nick_to: - irc.send('PRIVMSG %s :%s\r\n' % (nick_to, message)) - else: + for nick in nick_to: + irc.send('PRIVMSG %s :%s\r\n' % (nick, message)) + if channel: irc.send('PRIVMSG %s :%s\r\n' % (channel, message)) sleep(1) irc.send('PART %s\r\n' % channel) @@ -198,35 +209,38 @@ def main(): server=dict(default='localhost'), port=dict(default=6667), nick=dict(default='ansible'), - nick_to=dict(), + nick_to=dict(required=False, type='list'), msg=dict(required=True), color=dict(default="none", choices=["yellow", "red", "green", "blue", "black", "none"]), - channel=dict(required=True), + channel=dict(required=False), key=dict(), topic=dict(), passwd=dict(), timeout=dict(type='int', default=30), use_ssl=dict(type='bool', default=False) ), - supports_check_mode=True + supports_check_mode=True, + required_one_of=[['channel', 'nick_to']] ) server = module.params["server"] port = module.params["port"] nick = module.params["nick"] - topic = module.params["topic"] nick_to = module.params["nick_to"] msg = module.params["msg"] color = module.params["color"] channel = module.params["channel"] + topic = module.params["topic"] + if topic and not channel: + module.fail_json(msg="When topic is specified, a channel is required.") key = module.params["key"] passwd = module.params["passwd"] timeout = module.params["timeout"] use_ssl = module.params["use_ssl"] try: - send_msg(channel, msg, server, port, key, topic, nick, nick_to, color, passwd, timeout, use_ssl) + send_msg(msg, server, port, channel, nick_to, key, topic, nick, color, passwd, timeout, use_ssl) except Exception, e: module.fail_json(msg="unable to send to IRC: %s" % e) From 3542e7d42dcb7c598ed5f1a515ac6a1718f27b2f Mon Sep 17 00:00:00 2001 From: Michael Perzel Date: Tue, 7 Jul 2015 11:55:46 -0500 Subject: [PATCH 0384/2522] Update method to determine if task exists, add days of week parameter to weekly triggers --- windows/win_scheduled_task.ps1 | 43 ++++++++++++++++++++++++---------- windows/win_scheduled_task.py | 4 ++++ 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/windows/win_scheduled_task.ps1 b/windows/win_scheduled_task.ps1 index 07b1c3adf60..d5102572e69 100644 --- a/windows/win_scheduled_task.ps1 +++ b/windows/win_scheduled_task.ps1 @@ -65,6 +65,9 @@ elseif ($state -eq "present") { Fail-Json $result "missing required argument: execute" } +if( $state -ne "present" -and $state -ne "absent") { + Fail-Json $result "state must be present or absent" +} if ($params.path) { $path = "\{0}\" -f $params.path @@ -89,26 +92,40 @@ elseif($state -eq "present") { Fail-Json $result "missing required argument: time" } - -$exists = $true -#hack to determine if task exists -try { - $task = Get-ScheduledTask -TaskName $name -TaskPath $path +if ($params.daysOfWeek) +{ + $daysOfWeek = $params.daysOfWeek } -catch { - $exists = $false | ConvertTo-Bool +elseif ($frequency -eq "weekly") +{ + Fail-Json $result "missing required argument: daysOfWeek" } -Set-Attr $result "exists" "$exists" +try { + $task = Get-ScheduledTask -TaskPath "$path" | Where-Object {$_.TaskName -eq "$name"} + $measure = $task | measure + if ($measure.count -eq 1 ) { + $exists = $true + } + elseif ($measure.count -eq 0 -and $state -eq "absent" ){ + Set-Attr $result "msg" "Task does not exist" + Exit-Json $result + } + elseif ($measure.count -eq 0){ + $exists = $false + } + else { + # This should never occur + Fail-Json $result "$measure.count scheduled tasks found" + } + Set-Attr $result "exists" "$exists" -try -{ if ($frequency){ if ($frequency -eq "daily") { $trigger = New-ScheduledTaskTrigger -Daily -At $time } - elseif (frequency -eq "weekly"){ - $trigger = New-ScheduledTaskTrigger -Weekly -At $time + elseif ($frequency -eq "weekly"){ + $trigger = New-ScheduledTaskTrigger -Weekly -At $time -DaysOfWeek $daysOfWeek } else { Fail-Json $result "frequency must be daily or weekly" @@ -164,4 +181,4 @@ try catch { Fail-Json $result $_.Exception.Message -} \ No newline at end of file +} diff --git a/windows/win_scheduled_task.py b/windows/win_scheduled_task.py index 9f73cc30612..8b078ae9ae8 100644 --- a/windows/win_scheduled_task.py +++ b/windows/win_scheduled_task.py @@ -57,6 +57,10 @@ description: - Time to execute scheduled task required: false + daysOfWeek: + description: + - Days of the week to run a weekly task + required: false path: description: - Folder path of scheduled task From 8131bd3030006d52f9e33e98d556f4915d1e5f47 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Tue, 7 Jul 2015 10:49:43 -0700 Subject: [PATCH 0385/2522] Documentation update --- notification/irc.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/notification/irc.py b/notification/irc.py index 2c3c19be4dd..1eae8ed8284 100644 --- a/notification/irc.py +++ b/notification/irc.py @@ -61,12 +61,13 @@ choices: [ "none", "yellow", "red", "green", "blue", "black" ] channel: description: - - Channel name + - Channel name. One of nick_to or channel needs to be set. When both are set, the message will be sent to both of them. required: true nick_to: description: - - A list of nicknames to send the message to. When both channel and nick_to are defined, the message will be send to both of them. + - A list of nicknames to send the message to. One of nick_to or channel needs to be set. When both are defined, the message will be sent to both of them. required: false + default: null version_added: 2.0 key: description: From 639902ff2081aa7f90e051878a3abf3f1a67eac4 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Tue, 7 Jul 2015 10:53:09 -0700 Subject: [PATCH 0386/2522] Fix the documentation of route53_zone --- cloud/amazon/route53_zone.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/route53_zone.py b/cloud/amazon/route53_zone.py index 07a049b14f7..4630e00d4fa 100644 --- a/cloud/amazon/route53_zone.py +++ b/cloud/amazon/route53_zone.py @@ -23,7 +23,7 @@ options: zone: description: - - The DNS zone record (eg: foo.com.) + - "The DNS zone record (eg: foo.com.)" required: true state: description: From 689a7524bff8d9e3f933d1351942ea367aa1e28c Mon Sep 17 00:00:00 2001 From: Matt Baldwin Date: Tue, 7 Jul 2015 14:00:28 -0700 Subject: [PATCH 0387/2522] Resolving issues flagged in pull request #683 --- cloud/profitbricks/profitbricks.py | 57 +++++++++++++++++------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/cloud/profitbricks/profitbricks.py b/cloud/profitbricks/profitbricks.py index 561dbfef63c..b42ca00a12f 100644 --- a/cloud/profitbricks/profitbricks.py +++ b/cloud/profitbricks/profitbricks.py @@ -20,7 +20,7 @@ short_description: Create, destroy, start, stop, and reboot a ProfitBricks virtual machine. description: - Create, destroy, update, start, stop, and reboot a ProfitBricks virtual machine. When the virtual machine is created it can optionally wait for it to be 'running' before returning. This module has a dependency on profitbricks >= 1.0.0 -version_added: "1.9" +version_added: "2.0" options: auto_increment: description: @@ -31,12 +31,10 @@ description: - The name of the virtual machine. required: true - default: null image: description: - The system image ID for creating the virtual machine, e.g. a3eae284-a2fe-11e4-b187-5f1f641608c8. required: true - default: null datacenter: description: - The Datacenter to provision this virtual machine. @@ -104,12 +102,10 @@ required: false default: "yes" choices: [ "yes", "no" ] - aliases: [] wait_timeout: description: - how long before wait gives up, in seconds default: 600 - aliases: [] remove_boot_volume: description: - remove the bootVolume of the virtual machine you're destroying. @@ -121,7 +117,7 @@ - create or terminate instances required: false default: 'present' - aliases: [] + choices: [ "running", "stopped", "absent", "present" ] requirements: [ "profitbricks" ] author: Matt Baldwin (baldwin@stackpointcloud.com) @@ -185,11 +181,12 @@ import json import sys +HAS_PB_SDK = True + try: from profitbricks.client import ProfitBricksService, Volume, Server, Datacenter, NIC, LAN except ImportError: - print "failed=True msg='profitbricks required for this module'" - sys.exit(1) + HAS_PB_SDK = False LOCATIONS = ['us/las', 'de/fra', @@ -583,6 +580,9 @@ def main(): ) ) + if not HAS_PB_SDK: + module.fail_json(msg='profitbricks required for this module') + subscription_user = module.params.get('subscription_user') subscription_password = module.params.get('subscription_password') wait = module.params.get('wait') @@ -596,21 +596,24 @@ def main(): if state == 'absent': if not module.params.get('datacenter'): - module.fail_json(msg='datacenter parameter is required for running or stopping machines.') + module.fail_json(msg='datacenter parameter is required ' + + 'for running or stopping machines.') - (changed) = remove_virtual_machine(module, profitbricks) - - module.exit_json( - changed=changed) + try: + (changed) = remove_virtual_machine(module, profitbricks) + module.exit_json(changed=changed) + except Exception as e: + module.fail_json(msg='failed to set instance state: %s' % str(e)) elif state in ('running', 'stopped'): if not module.params.get('datacenter'): - module.fail_json(msg='datacenter parameter is required for running or stopping machines.') - - (changed) = startstop_machine(module, profitbricks, state) - - module.exit_json( - changed=changed) + module.fail_json(msg='datacenter parameter is required for ' + + 'running or stopping machines.') + try: + (changed) = startstop_machine(module, profitbricks, state) + module.exit_json(changed=changed) + except Exception as e: + module.fail_json(msg='failed to set instance state: %s' % str(e)) elif state == 'present': if not module.params.get('name'): @@ -618,15 +621,19 @@ def main(): if not module.params.get('image'): module.fail_json(msg='image parameter is required for new instance') if not module.params.get('subscription_user'): - module.fail_json(msg='subscription_user parameter is required for new instance') + module.fail_json(msg='subscription_user parameter is ' + + 'required for new instance') if not module.params.get('subscription_password'): - module.fail_json(msg='subscription_password parameter is required for new instance') - - (machine_dict_array) = create_virtual_machine(module, profitbricks) - - module.exit_json(**machine_dict_array) + module.fail_json(msg='subscription_password parameter is ' + + 'required for new instance') + try: + (machine_dict_array) = create_virtual_machine(module, profitbricks) + module.exit_json(**machine_dict_array) + except Exception as e: + module.fail_json(msg='failed to set instance state: %s' % str(e)) from ansible.module_utils.basic import * main() + From 5fec1e3994b31e15daa2b0e7d936c2948da1a780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Wed, 8 Jul 2015 12:56:45 +0200 Subject: [PATCH 0388/2522] irc: add version_added to new option nick --- notification/irc.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/notification/irc.py b/notification/irc.py index 1eae8ed8284..70f198883c7 100644 --- a/notification/irc.py +++ b/notification/irc.py @@ -42,6 +42,7 @@ - Nickname to send the message from. May be shortened, depending on server's NICKLEN setting. required: false default: ansible + version_added: "2.0" msg: description: - The message body. @@ -52,7 +53,7 @@ - Set the channel topic required: false default: null - version_added: 2.0 + version_added: "2.0" color: description: - Text color for the message. ("none" is a valid option in 1.6 or later, in 1.6 and prior, the default color is black, not "none"). @@ -68,12 +69,12 @@ - A list of nicknames to send the message to. One of nick_to or channel needs to be set. When both are defined, the message will be sent to both of them. required: false default: null - version_added: 2.0 + version_added: "2.0" key: description: - Channel key required: false - version_added: 1.7 + version_added: "1.7" passwd: description: - Server password @@ -83,12 +84,12 @@ - Timeout to use while waiting for successful registration and join messages, this is to prevent an endless loop default: 30 - version_added: 1.5 + version_added: "1.5" use_ssl: description: - Designates whether TLS/SSL should be used when connecting to the IRC server default: False - version_added: 1.8 + version_added: "1.8" # informational: requirements for nodes requirements: [ socket ] From 8a72cf6a1c1e142b6383e739771bc708a86086fa Mon Sep 17 00:00:00 2001 From: gfrank Date: Wed, 8 Jul 2015 11:44:46 -0400 Subject: [PATCH 0389/2522] Adding win_nssm module --- windows/win_nssm.ps1 | 547 +++++++++++++++++++++++++++++++++++++++++++ windows/win_nssm.py | 136 +++++++++++ 2 files changed, 683 insertions(+) create mode 100644 windows/win_nssm.ps1 create mode 100644 windows/win_nssm.py diff --git a/windows/win_nssm.ps1 b/windows/win_nssm.ps1 new file mode 100644 index 00000000000..088914f4060 --- /dev/null +++ b/windows/win_nssm.ps1 @@ -0,0 +1,547 @@ +#!powershell +# This file is part of Ansible +# +# Copyright 2015, George Frank +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +$ErrorActionPreference = "Stop" + +# WANT_JSON +# POWERSHELL_COMMON + +$params = Parse-Args $args; +$result = New-Object PSObject; +Set-Attr $result "changed" $false; + + +If ($params.name) +{ + $name = $params.name +} +Else +{ + Fail-Json $result "missing required argument: name" +} + +If ($params.state) +{ + $state = $params.state.ToString().ToLower() + $validStates = "present", "absent", "started", "stopped", "restarted" + + # These don't really fit the declarative style of ansible + # If you need to do these things, you can just write a command for it + # "paused", "continued", "rotated" + + If ($validStates -notcontains $state) + { + Fail-Json $result "state is $state; must be one of: $validStates" + } +} +else +{ + $state = "present" +} + +If ($params.application) +{ + $application = $params.application +} +Else +{ + $application = $null +} + +If ($params.app_parameters) +{ + $appParameters = $params.app_parameters +} +Else +{ + $appParameters = $null +} + +If ($params.stdout_file) +{ + $stdoutFile = $params.stdout_file +} +Else +{ + $stdoutFile = $null +} + +If ($params.stderr_file) +{ + $stderrFile = $params.stderr_file +} +Else +{ + $stderrFile = $null +} + +Function Service-Exists +{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string]$name + ) + + return ,[bool](Get-Service "$name" -ErrorAction SilentlyContinue) +} + +Function Nssm-Remove +{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string]$name + ) + + if (Service-Exists -name $name) + { + $cmd = "nssm stop ""$name""" + $results = invoke-expression $cmd + + $cmd = "nssm remove ""$name"" confirm" + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "nssm_error_cmd" $cmd + Set-Attr $result "nssm_error_log" "$results" + Throw "Error removing service ""$name""" + } + + $result.changed = $true + } +} + +Function Nssm-Install +{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string]$name, + [Parameter(Mandatory=$true)] + [string]$application + ) + + #note: the application name must look like the following, if the directory includes spaces: + # nssm install service "c:\Program Files\app.exe" """C:\Path with spaces""" + #see https://git.nssm.cc/?p=nssm/nssm.git;a=commit;h=0b386fc1984ab74ee59b7bed14b7e8f57212c22b for more info + + + if (!$application) + { + Throw "Error installing service ""$name"". No application was supplied." + } + + if (!(Service-Exists -name $name)) + { + $cmd = "nssm install ""$name"" $application" + + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "nssm_error_cmd" $cmd + Set-Attr $result "nssm_error_log" "$results" + Throw "Error installing service ""$name""" + } + + $result.changed = $true + + } else { + $cmd = "nssm get ""$name"" Application" + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "nssm_error_cmd" $cmd + Set-Attr $result "nssm_error_log" "$results" + Throw "Error installing service ""$name""" + } + + if ($results -ne $application) + { + $cmd = "nssm set ""$name"" Application $application" + + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "nssm_error_cmd" $cmd + Set-Attr $result "nssm_error_log" "$results" + Throw "Error installing service ""$name""" + } + + $result.changed = $true + } + } +} + +Function ParseAppParameters() +{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string]$appParameters + ) + + return ConvertFrom-StringData -StringData $appParameters.TrimStart("@").TrimStart("{").TrimEnd("}").Replace("; ","`n") +} + + +Function Nssm-Update-AppParameters +{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string]$name, + [Parameter(Mandatory=$true)] + [string]$appParameters + ) + + $cmd = "nssm get ""$name"" AppParameters" + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "nssm_error_cmd" $cmd + Set-Attr $result "nssm_error_log" "$results" + Throw "Error updating AppParameters for service ""$name""" + } + + $appParametersHash = ParseAppParameters -appParameters $appParameters + + $appParamKeys = @() + $appParamVals = @() + $singleLineParams = "" + $appParametersHash.GetEnumerator() | + % { + $key = $($_.Name) + $val = $($_.Value) + + $appParamKeys += $key + $appParamVals += $val + + if ($key -eq "_") { + $singleLineParams = "$val " + $singleLineParams + } else { + $singleLineParams = $singleLineParams + "$key ""$val""" + } + } + + Set-Attr $result "nssm_app_parameters" $appParameters + Set-Attr $result "nssm_app_parameters_parsed" $appParametersHash + Set-Attr $result "nssm_app_parameters_keys" $appParamKeys + Set-Attr $result "nssm_app_parameters_vals" $appParamVals + Set-Attr $result "nssm_single_line_app_parameters" $singleLineParams + + if ($results -ne $singleLineParams) + { + $cmd = "nssm set ""$name"" AppParameters $singleLineParams" + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "nssm_error_cmd" $cmd + Set-Attr $result "nssm_error_log" "$results" + Throw "Error updating AppParameters for service ""$name""" + } + + $result.changed = $true + } +} + +Function Nssm-Set-Ouput-Files +{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string]$name, + [string]$stdout, + [string]$stderr + ) + + $cmd = "nssm get ""$name"" AppStdout" + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "nssm_error_cmd" $cmd + Set-Attr $result "nssm_error_log" "$results" + Throw "Error retrieving existing stdout file for service ""$name""" + } + + if ($results -ne $stdout) + { + if (!$stdout) + { + $cmd = "nssm reset ""$name"" AppStdout" + } else { + $cmd = "nssm set ""$name"" AppStdout $stdout" + } + + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "nssm_error_cmd" $cmd + Set-Attr $result "nssm_error_log" "$results" + Throw "Error setting stdout file for service ""$name""" + } + + $result.changed = $true + } + + $cmd = "nssm get ""$name"" AppStderr" + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "nssm_error_cmd" $cmd + Set-Attr $result "nssm_error_log" "$results" + Throw "Error retrieving existing stderr file for service ""$name""" + } + + if ($results -ne $stderr) + { + if (!$stderr) + { + $cmd = "nssm reset ""$name"" AppStderr" + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "nssm_error_cmd" $cmd + Set-Attr $result "nssm_error_log" "$results" + Throw "Error clearing stderr file setting for service ""$name""" + } + } else { + $cmd = "nssm set ""$name"" AppStderr $stderr" + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "nssm_error_cmd" $cmd + Set-Attr $result "nssm_error_log" "$results" + Throw "Error setting stderr file for service ""$name""" + } + } + + $result.changed = $true + } + + ### + # Setup file rotation so we don't accidentally consume too much disk + ### + + #set files to overwrite + $cmd = "nssm set ""$name"" AppStdoutCreationDisposition 2" + $results = invoke-expression $cmd + + $cmd = "nssm set ""$name"" AppStderrCreationDisposition 2" + $results = invoke-expression $cmd + + #enable file rotation + $cmd = "nssm set ""$name"" AppRotateFiles 1" + $results = invoke-expression $cmd + + #don't rotate until the service restarts + $cmd = "nssm set ""$name"" AppRotateOnline 0" + $results = invoke-expression $cmd + + #both of the below conditions must be met before rotation will happen + #minimum age before rotating + $cmd = "nssm set ""$name"" AppRotateSeconds 86400" + $results = invoke-expression $cmd + + #minimum size before rotating + $cmd = "nssm set ""$name"" AppRotateBytes 104858" + $results = invoke-expression $cmd +} + +Function Nssm-Get-Status +{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string]$name + ) + + $cmd = "nssm status ""$name""" + $results = invoke-expression $cmd + + return ,$results +} + +Function Nssm-Start +{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string]$name + ) + + $currentStatus = Nssm-Get-Status -name $name + + if ($LastExitCode -ne 0) + { + Set-Attr $result "nssm_error_cmd" $cmd + Set-Attr $result "nssm_error_log" "$results" + Throw "Error starting service ""$name""" + } + + switch ($currentStatus) + { + "SERVICE_RUNNING" { <# Nothing to do #> } + "SERVICE_STOPPED" { Nssm-Start-Service-Command -name $name } + + "SERVICE_CONTINUE_PENDING" { Nssm-Stop-Service-Command -name $name; Nssm-Start-Service-Command -name $name } + "SERVICE_PAUSE_PENDING" { Nssm-Stop-Service-Command -name $name; Nssm-Start-Service-Command -name $name } + "SERVICE_PAUSED" { Nssm-Stop-Service-Command -name $name; Nssm-Start-Service-Command -name $name } + "SERVICE_START_PENDING" { Nssm-Stop-Service-Command -name $name; Nssm-Start-Service-Command -name $name } + "SERVICE_STOP_PENDING" { Nssm-Stop-Service-Command -name $name; Nssm-Start-Service-Command -name $name } + } +} + +Function Nssm-Start-Service-Command +{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string]$name + ) + + $cmd = "nssm start ""$name""" + + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "nssm_error_cmd" $cmd + Set-Attr $result "nssm_error_log" "$results" + Throw "Error starting service ""$name""" + } + + $result.changed = $true +} + +Function Nssm-Stop-Service-Command +{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string]$name + ) + + $cmd = "nssm stop ""$name""" + + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "nssm_error_cmd" $cmd + Set-Attr $result "nssm_error_log" "$results" + Throw "Error stopping service ""$name""" + } + + $result.changed = $true +} + +Function Nssm-Stop +{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string]$name + ) + + $currentStatus = Nssm-Get-Status -name $name + + if ($LastExitCode -ne 0) + { + Set-Attr $result "nssm_error_cmd" $cmd + Set-Attr $result "nssm_error_log" "$results" + Throw "Error stopping service ""$name""" + } + + if (currentStatus -ne "SERVICE_STOPPED") + { + $cmd = "nssm stop ""$name""" + + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "nssm_error_cmd" $cmd + Set-Attr $result "nssm_error_log" "$results" + Throw "Error stopping service ""$name""" + } + + $result.changed = $true + } +} + +Function Nssm-Restart +{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string]$name + ) + + Nssm-Stop-Service-Command -name $name + Nssm-Start-Service-Command -name $name +} + +Try +{ + switch ($state) + { + "absent" { Nssm-Remove -name $name } + "present" { + Nssm-Install -name $name -application $application + Nssm-Update-AppParameters -name $name -appParameters $appParameters + Nssm-Set-Ouput-Files -name $name -stdout $stdoutFile -stderr $stderrFile + } + "started" { + Nssm-Install -name $name -application $application + Nssm-Update-AppParameters -name $name -appParameters $appParameters + Nssm-Set-Ouput-Files -name $name -stdout $stdoutFile -stderr $stderrFile + Nssm-Start -name $name + } + "stopped" { + Nssm-Install -name $name -application $application + Nssm-Update-AppParameters -name $name -appParameters $appParameters + Nssm-Set-Ouput-Files -name $name -stdout $stdoutFile -stderr $stderrFile + Nssm-Stop -name $name + } + "restarted" { + Nssm-Install -name $name -application $application + Nssm-Update-AppParameters -name $name -appParameters $appParameters + Nssm-Set-Ouput-Files -name $name -stdout $stdoutFile -stderr $stderrFile + Nssm-Restart -name $name + } + } + + Exit-Json $result; +} +Catch +{ + Fail-Json $result $_.Exception.Message +} + diff --git a/windows/win_nssm.py b/windows/win_nssm.py new file mode 100644 index 00000000000..46c940ce151 --- /dev/null +++ b/windows/win_nssm.py @@ -0,0 +1,136 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Heyo +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# this is a windows documentation stub. actual code lives in the .ps1 +# file of the same name + +DOCUMENTATION = ''' +--- +module: win_nssm +version_added: "2.0" +short_description: NSSM - the Non-Sucking Service Manager +description: + - nssm is a service helper which doesn't suck. See https://nssm.cc/ for more information. +options: + name: + description: + - Name of the service to operate on + required: true + default: null + aliases: [] + + state: + description: + - State of the service on the system + required: false + choices: + - started + - stopped + - restarted + - absent + default: started + aliases: [] + + application: + description: + - The application binary to run as a service + required: false + default: null + aliases: [] + + stdout_file: + description: + - Path to receive output + required: false + default: null + aliases: [] + + stderr_file: + description: + - Path to receive error output + required: false + default: null + aliases: [] + + app_parameters: + description: + - Parameters to be passed to the application when it starts + required: false + default: null + aliases: [] +author: "Adam Keech (@smadam813), George Frank (@georgefrank)" +''' + +# TODO: +# * Better parsing when a package has dependencies - currently fails +# * Time each item that is run +# * Support 'changed' with gems - would require shelling out to `gem list` first and parsing, kinda defeating the point of using chocolatey. + +EXAMPLES = ''' + # Install and start the foo service + win_nssm: + name: foo + application: C:\windows\foo.exe + + # Install and start the foo service with a key-value pair argument + # This will yield the following command: C:\windows\foo.exe bar "true" + win_nssm: + name: foo + application: C:\windows\foo.exe + app_parameters: + bar: true + + # Install and start the foo service with a key-value pair argument, where the argument needs to start with a dash + # This will yield the following command: C:\windows\foo.exe -bar "true" + win_nssm: + name: foo + application: C:\windows\foo.exe + app_parameters: + "-bar": true + + # Install and start the foo service with a single parameter + # This will yield the following command: C:\windows\foo.exe bar + win_nssm: + name: foo + application: C:\windows\foo.exe + app_parameters: + _: bar + + # Install and start the foo service with a mix of single params, and key value pairs + # This will yield the following command: C:\windows\foo.exe bar -file output.bat + win_nssm: + name: foo + application: C:\windows\foo.exe + app_parameters: + _: bar + "-file": "output.bat" + + # Install and start the foo service, redirecting stdout and stderr to the same file + win_nssm: + name: foo + application: C:\windows\foo.exe + stdout_file: C:\windows\foo.log + stderr_file: C:\windows\foo.log + + # Remove the foo service + win_nssm: + name: foo + state: absent +''' From 11c953477c012435e078d62204b0cf1db2796d2f Mon Sep 17 00:00:00 2001 From: Siva Popuri Date: Wed, 8 Jul 2015 11:22:38 -0500 Subject: [PATCH 0390/2522] ansible modules for centurylink cloud are added --- cloud/centurylink/__init__.py | 1 + cloud/centurylink/clc_aa_policy.py | 294 +++++ cloud/centurylink/clc_alert_policy.py | 473 +++++++ cloud/centurylink/clc_blueprint_package.py | 263 ++++ cloud/centurylink/clc_firewall_policy.py | 542 ++++++++ cloud/centurylink/clc_group.py | 370 ++++++ cloud/centurylink/clc_loadbalancer.py | 759 +++++++++++ cloud/centurylink/clc_modify_server.py | 710 +++++++++++ cloud/centurylink/clc_publicip.py | 316 +++++ cloud/centurylink/clc_server.py | 1323 ++++++++++++++++++++ cloud/centurylink/clc_server_snapshot.py | 341 +++++ 11 files changed, 5392 insertions(+) create mode 100644 cloud/centurylink/__init__.py create mode 100644 cloud/centurylink/clc_aa_policy.py create mode 100644 cloud/centurylink/clc_alert_policy.py create mode 100644 cloud/centurylink/clc_blueprint_package.py create mode 100644 cloud/centurylink/clc_firewall_policy.py create mode 100644 cloud/centurylink/clc_group.py create mode 100644 cloud/centurylink/clc_loadbalancer.py create mode 100644 cloud/centurylink/clc_modify_server.py create mode 100644 cloud/centurylink/clc_publicip.py create mode 100644 cloud/centurylink/clc_server.py create mode 100644 cloud/centurylink/clc_server_snapshot.py diff --git a/cloud/centurylink/__init__.py b/cloud/centurylink/__init__.py new file mode 100644 index 00000000000..71f0abcff9d --- /dev/null +++ b/cloud/centurylink/__init__.py @@ -0,0 +1 @@ +__version__ = "${version}" diff --git a/cloud/centurylink/clc_aa_policy.py b/cloud/centurylink/clc_aa_policy.py new file mode 100644 index 00000000000..644f3817c4f --- /dev/null +++ b/cloud/centurylink/clc_aa_policy.py @@ -0,0 +1,294 @@ +#!/usr/bin/python + +# CenturyLink Cloud Ansible Modules. +# +# These Ansible modules enable the CenturyLink Cloud v2 API to be called +# from an within Ansible Playbook. +# +# This file is part of CenturyLink Cloud, and is maintained +# by the Workflow as a Service Team +# +# Copyright 2015 CenturyLink Cloud +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# CenturyLink Cloud: http://www.CenturyLinkCloud.com +# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ + +DOCUMENTATION = ''' +module: clc_aa_policy +short_descirption: Create or Delete Anti Affinity Policies at CenturyLink Cloud. +description: + - An Ansible module to Create or Delete Anti Affinity Policies at CenturyLink Cloud. +options: + name: + description: + - The name of the Anti Affinity Policy. + required: True + location: + description: + - Datacenter in which the policy lives/should live. + required: True + state: + description: + - Whether to create or delete the policy. + required: False + default: present + choices: ['present','absent'] + wait: + description: + - Whether to wait for the provisioning tasks to finish before returning. + default: True + required: False + choices: [ True, False] + aliases: [] +''' + +EXAMPLES = ''' +# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples + +--- +- name: Create AA Policy + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Create an Anti Affinity Policy + clc_aa_policy: + name: 'Hammer Time' + location: 'UK3' + state: present + register: policy + + - name: debug + debug: var=policy + +--- +- name: Delete AA Policy + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Delete an Anti Affinity Policy + clc_aa_policy: + name: 'Hammer Time' + location: 'UK3' + state: absent + register: policy + + - name: debug + debug: var=policy +''' + +__version__ = '${version}' + +import requests + +# +# Requires the clc-python-sdk. +# sudo pip install clc-sdk +# +try: + import clc as clc_sdk + from clc import CLCException +except ImportError: + clc_found = False + clc_sdk = None +else: + clc_found = True + + +class ClcAntiAffinityPolicy(): + + clc = clc_sdk + module = None + + def __init__(self, module): + """ + Construct module + """ + self.module = module + self.policy_dict = {} + + if not clc_found: + self.module.fail_json( + msg='clc-python-sdk required for this module') + + self._set_user_agent(self.clc) + + @staticmethod + def _define_module_argument_spec(): + """ + Define the argument spec for the ansible module + :return: argument spec dictionary + """ + argument_spec = dict( + name=dict(required=True), + location=dict(required=True), + alias=dict(default=None), + wait=dict(default=True), + state=dict(default='present', choices=['present', 'absent']), + ) + return argument_spec + + # Module Behavior Goodness + def process_request(self): + """ + Process the request - Main Code Path + :return: Returns with either an exit_json or fail_json + """ + p = self.module.params + + if not clc_found: + self.module.fail_json( + msg='clc-python-sdk required for this module') + + self._set_clc_credentials_from_env() + self.policy_dict = self._get_policies_for_datacenter(p) + + if p['state'] == "absent": + changed, policy = self._ensure_policy_is_absent(p) + else: + changed, policy = self._ensure_policy_is_present(p) + + if hasattr(policy, 'data'): + policy = policy.data + elif hasattr(policy, '__dict__'): + policy = policy.__dict__ + + self.module.exit_json(changed=changed, policy=policy) + + def _set_clc_credentials_from_env(self): + """ + Set the CLC Credentials on the sdk by reading environment variables + :return: none + """ + env = os.environ + v2_api_token = env.get('CLC_V2_API_TOKEN', False) + v2_api_username = env.get('CLC_V2_API_USERNAME', False) + v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) + clc_alias = env.get('CLC_ACCT_ALIAS', False) + api_url = env.get('CLC_V2_API_URL', False) + + if api_url: + self.clc.defaults.ENDPOINT_URL_V2 = api_url + + if v2_api_token and clc_alias: + self.clc._LOGIN_TOKEN_V2 = v2_api_token + self.clc._V2_ENABLED = True + self.clc.ALIAS = clc_alias + elif v2_api_username and v2_api_passwd: + self.clc.v2.SetCredentials( + api_username=v2_api_username, + api_passwd=v2_api_passwd) + else: + return self.module.fail_json( + msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " + "environment variables") + + def _get_policies_for_datacenter(self, p): + """ + Get the Policies for a datacenter by calling the CLC API. + :param p: datacenter to get policies from + :return: policies in the datacenter + """ + response = {} + + policies = self.clc.v2.AntiAffinity.GetAll(location=p['location']) + + for policy in policies: + response[policy.name] = policy + return response + + def _create_policy(self, p): + """ + Create an Anti Affinnity Policy using the CLC API. + :param p: datacenter to create policy in + :return: response dictionary from the CLC API. + """ + return self.clc.v2.AntiAffinity.Create( + name=p['name'], + location=p['location']) + + def _delete_policy(self, p): + """ + Delete an Anti Affinity Policy using the CLC API. + :param p: datacenter to delete a policy from + :return: none + """ + policy = self.policy_dict[p['name']] + policy.Delete() + + def _policy_exists(self, policy_name): + """ + Check to see if an Anti Affinity Policy exists + :param policy_name: name of the policy + :return: boolean of if the policy exists + """ + if policy_name in self.policy_dict: + return self.policy_dict.get(policy_name) + + return False + + def _ensure_policy_is_absent(self, p): + """ + Makes sure that a policy is absent + :param p: dictionary of policy name + :return: tuple of if a deletion occurred and the name of the policy that was deleted + """ + changed = False + if self._policy_exists(policy_name=p['name']): + changed = True + if not self.module.check_mode: + self._delete_policy(p) + return changed, None + + def _ensure_policy_is_present(self, p): + """ + Ensures that a policy is present + :param p: dictonary of a policy name + :return: tuple of if an addition occurred and the name of the policy that was added + """ + changed = False + policy = self._policy_exists(policy_name=p['name']) + if not policy: + changed = True + policy = None + if not self.module.check_mode: + policy = self._create_policy(p) + return changed, policy + + @staticmethod + def _set_user_agent(clc): + if hasattr(clc, 'SetRequestsSession'): + agent_string = "ClcAnsibleModule/" + __version__ + ses = requests.Session() + ses.headers.update({"Api-Client": agent_string}) + ses.headers['User-Agent'] += " " + agent_string + clc.SetRequestsSession(ses) + + +def main(): + """ + The main function. Instantiates the module and calls process_request. + :return: none + """ + module = AnsibleModule( + argument_spec=ClcAntiAffinityPolicy._define_module_argument_spec(), + supports_check_mode=True) + clc_aa_policy = ClcAntiAffinityPolicy(module) + clc_aa_policy.process_request() + +from ansible.module_utils.basic import * # pylint: disable=W0614 +if __name__ == '__main__': + main() diff --git a/cloud/centurylink/clc_alert_policy.py b/cloud/centurylink/clc_alert_policy.py new file mode 100644 index 00000000000..75467967a85 --- /dev/null +++ b/cloud/centurylink/clc_alert_policy.py @@ -0,0 +1,473 @@ +#!/usr/bin/python + +# CenturyLink Cloud Ansible Modules. +# +# These Ansible modules enable the CenturyLink Cloud v2 API to be called +# from an within Ansible Playbook. +# +# This file is part of CenturyLink Cloud, and is maintained +# by the Workflow as a Service Team +# +# Copyright 2015 CenturyLink Cloud +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# CenturyLink Cloud: http://www.CenturyLinkCloud.com +# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ + +DOCUMENTATION = ''' +module: clc_alert_policy +short_descirption: Create or Delete Alert Policies at CenturyLink Cloud. +description: + - An Ansible module to Create or Delete Alert Policies at CenturyLink Cloud. +options: + alias: + description: + - The alias of your CLC Account + required: True + name: + description: + - The name of the alert policy. This is mutually exclusive with id + default: None + aliases: [] + id: + description: + - The alert policy id. This is mutually exclusive with name + default: None + aliases: [] + alert_recipients: + description: + - A list of recipient email ids to notify the alert. + required: True + aliases: [] + metric: + description: + - The metric on which to measure the condition that will trigger the alert. + required: True + default: None + choices: ['cpu','memory','disk'] + aliases: [] + duration: + description: + - The length of time in minutes that the condition must exceed the threshold. + required: True + default: None + aliases: [] + threshold: + description: + - The threshold that will trigger the alert when the metric equals or exceeds it. + This number represents a percentage and must be a value between 5.0 - 95.0 that is a multiple of 5.0 + required: True + default: None + aliases: [] + state: + description: + - Whether to create or delete the policy. + required: False + default: present + choices: ['present','absent'] +''' + +EXAMPLES = ''' +# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples + +--- +- name: Create Alert Policy Example + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Create an Alert Policy for disk above 80% for 5 minutes + clc_alert_policy: + alias: wfad + name: 'alert for disk > 80%' + alert_recipients: + - test1@centurylink.com + - test2@centurylink.com + metric: 'disk' + duration: '00:05:00' + threshold: 80 + state: present + register: policy + + - name: debug + debug: var=policy + +--- +- name: Delete Alert Policy Example + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Delete an Alert Policy + clc_alert_policy: + alias: wfad + name: 'alert for disk > 80%' + state: absent + register: policy + + - name: debug + debug: var=policy +''' + +__version__ = '${version}' + +import requests + +# +# Requires the clc-python-sdk. +# sudo pip install clc-sdk +# +try: + import clc as clc_sdk + from clc import CLCException +except ImportError: + clc_found = False + clc_sdk = None +else: + clc_found = True + + +class ClcAlertPolicy(): + + clc = clc_sdk + module = None + + def __init__(self, module): + """ + Construct module + """ + self.module = module + self.policy_dict = {} + + if not clc_found: + self.module.fail_json( + msg='clc-python-sdk required for this module') + + self._set_user_agent(self.clc) + + @staticmethod + def _define_module_argument_spec(): + """ + Define the argument spec for the ansible module + :return: argument spec dictionary + """ + argument_spec = dict( + name=dict(default=None), + id=dict(default=None), + alias=dict(required=True, default=None), + alert_recipients=dict(type='list', required=False, default=None), + metric=dict(required=False, choices=['cpu', 'memory', 'disk'], default=None), + duration=dict(required=False, type='str', default=None), + threshold=dict(required=False, type='int', default=None), + state=dict(default='present', choices=['present', 'absent']) + ) + mutually_exclusive = [ + ['name', 'id'] + ] + return {'argument_spec': argument_spec, + 'mutually_exclusive': mutually_exclusive} + + # Module Behavior Goodness + def process_request(self): + """ + Process the request - Main Code Path + :return: Returns with either an exit_json or fail_json + """ + p = self.module.params + + if not clc_found: + self.module.fail_json( + msg='clc-python-sdk required for this module') + + self._set_clc_credentials_from_env() + self.policy_dict = self._get_alert_policies(p['alias']) + + if p['state'] == 'present': + changed, policy = self._ensure_alert_policy_is_present() + else: + changed, policy = self._ensure_alert_policy_is_absent() + + self.module.exit_json(changed=changed, policy=policy) + + def _set_clc_credentials_from_env(self): + """ + Set the CLC Credentials on the sdk by reading environment variables + :return: none + """ + env = os.environ + v2_api_token = env.get('CLC_V2_API_TOKEN', False) + v2_api_username = env.get('CLC_V2_API_USERNAME', False) + v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) + clc_alias = env.get('CLC_ACCT_ALIAS', False) + api_url = env.get('CLC_V2_API_URL', False) + + if api_url: + self.clc.defaults.ENDPOINT_URL_V2 = api_url + + if v2_api_token and clc_alias: + self.clc._LOGIN_TOKEN_V2 = v2_api_token + self.clc._V2_ENABLED = True + self.clc.ALIAS = clc_alias + elif v2_api_username and v2_api_passwd: + self.clc.v2.SetCredentials( + api_username=v2_api_username, + api_passwd=v2_api_passwd) + else: + return self.module.fail_json( + msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " + "environment variables") + + def _ensure_alert_policy_is_present(self): + """ + Ensures that the alert policy is present + :return: (changed, policy) + canged: A flag representing if anything is modified + policy: the created/updated alert policy + """ + changed = False + p = self.module.params + policy_name = p.get('name') + alias = p.get('alias') + if not policy_name: + self.module.fail_json(msg='Policy name is a required') + policy = self._alert_policy_exists(alias, policy_name) + if not policy: + changed = True + policy = None + if not self.module.check_mode: + policy = self._create_alert_policy() + else: + changed_u, policy = self._ensure_alert_policy_is_updated(policy) + if changed_u: + changed = True + return changed, policy + + def _ensure_alert_policy_is_absent(self): + """ + Ensures that the alert policy is absent + :return: (changed, None) + canged: A flag representing if anything is modified + """ + changed = False + p = self.module.params + alert_policy_id = p.get('id') + alert_policy_name = p.get('name') + alias = p.get('alias') + if not alert_policy_id and not alert_policy_name: + self.module.fail_json( + msg='Either alert policy id or policy name is required') + if not alert_policy_id and alert_policy_name: + alert_policy_id = self._get_alert_policy_id( + self.module, + alert_policy_name) + if alert_policy_id and alert_policy_id in self.policy_dict: + changed = True + if not self.module.check_mode: + self._delete_alert_policy(alias, alert_policy_id) + return changed, None + + def _ensure_alert_policy_is_updated(self, alert_policy): + """ + Ensures the aliert policy is updated if anything is changed in the alert policy configuration + :param alert_policy: the targetalert policy + :return: (changed, policy) + canged: A flag representing if anything is modified + policy: the updated the alert policy + """ + changed = False + p = self.module.params + alert_policy_id = alert_policy.get('id') + email_list = p.get('alert_recipients') + metric = p.get('metric') + duration = p.get('duration') + threshold = p.get('threshold') + policy = alert_policy + if (metric and metric != str(alert_policy.get('triggers')[0].get('metric'))) or \ + (duration and duration != str(alert_policy.get('triggers')[0].get('duration'))) or \ + (threshold and float(threshold) != float(alert_policy.get('triggers')[0].get('threshold'))): + changed = True + elif email_list: + t_email_list = list( + alert_policy.get('actions')[0].get('settings').get('recipients')) + if set(email_list) != set(t_email_list): + changed = True + if changed and not self.module.check_mode: + policy = self._update_alert_policy(alert_policy_id) + return changed, policy + + def _get_alert_policies(self, alias): + """ + Get the alert policies for account alias by calling the CLC API. + :param alias: the account alias + :return: the alert policies for the account alias + """ + response = {} + + policies = self.clc.v2.API.Call('GET', + '/v2/alertPolicies/%s' + % (alias)) + + for policy in policies.get('items'): + response[policy.get('id')] = policy + return response + + def _create_alert_policy(self): + """ + Create an alert Policy using the CLC API. + :return: response dictionary from the CLC API. + """ + p = self.module.params + alias = p['alias'] + email_list = p['alert_recipients'] + metric = p['metric'] + duration = p['duration'] + threshold = p['threshold'] + name = p['name'] + arguments = json.dumps( + { + 'name': name, + 'actions': [{ + 'action': 'email', + 'settings': { + 'recipients': email_list + } + }], + 'triggers': [{ + 'metric': metric, + 'duration': duration, + 'threshold': threshold + }] + } + ) + try: + result = self.clc.v2.API.Call( + 'POST', + '/v2/alertPolicies/%s' % + (alias), + arguments) + except self.clc.APIFailedResponse as e: + return self.module.fail_json( + msg='Unable to create alert policy. %s' % str( + e.response_text)) + return result + + def _update_alert_policy(self, alert_policy_id): + """ + Update alert policy using the CLC API. + :param alert_policy_id: The clc alert policy id + :return: response dictionary from the CLC API. + """ + p = self.module.params + alias = p['alias'] + email_list = p['alert_recipients'] + metric = p['metric'] + duration = p['duration'] + threshold = p['threshold'] + name = p['name'] + arguments = json.dumps( + { + 'name': name, + 'actions': [{ + 'action': 'email', + 'settings': { + 'recipients': email_list + } + }], + 'triggers': [{ + 'metric': metric, + 'duration': duration, + 'threshold': threshold + }] + } + ) + try: + result = self.clc.v2.API.Call( + 'PUT', '/v2/alertPolicies/%s/%s' % + (alias, alert_policy_id), arguments) + except self.clc.APIFailedResponse as e: + return self.module.fail_json( + msg='Unable to update alert policy. %s' % str( + e.response_text)) + return result + + def _delete_alert_policy(self, alias, policy_id): + """ + Delete an alert policy using the CLC API. + :param alias : the account alias + :param policy_id: the alert policy id + :return: response dictionary from the CLC API. + """ + try: + result = self.clc.v2.API.Call( + 'DELETE', '/v2/alertPolicies/%s/%s' % + (alias, policy_id), None) + except self.clc.APIFailedResponse as e: + return self.module.fail_json( + msg='Unable to delete alert policy. %s' % str( + e.response_text)) + return result + + def _alert_policy_exists(self, alias, policy_name): + """ + Check to see if an alert policy exists + :param policy_name: name of the alert policy + :return: boolean of if the policy exists + """ + result = False + for id in self.policy_dict: + if self.policy_dict.get(id).get('name') == policy_name: + result = self.policy_dict.get(id) + return result + + def _get_alert_policy_id(self, module, alert_policy_name): + """ + retrieves the alert policy id of the account based on the name of the policy + :param module: the AnsibleModule object + :param alert_policy_name: the alert policy name + :return: alert_policy_id: The alert policy id + """ + alert_policy_id = None + for id in self.policy_dict: + if self.policy_dict.get(id).get('name') == alert_policy_name: + if not alert_policy_id: + alert_policy_id = id + else: + return module.fail_json( + msg='mutiple alert policies were found with policy name : %s' % + (alert_policy_name)) + return alert_policy_id + + @staticmethod + def _set_user_agent(clc): + if hasattr(clc, 'SetRequestsSession'): + agent_string = "ClcAnsibleModule/" + __version__ + ses = requests.Session() + ses.headers.update({"Api-Client": agent_string}) + ses.headers['User-Agent'] += " " + agent_string + clc.SetRequestsSession(ses) + + +def main(): + """ + The main function. Instantiates the module and calls process_request. + :return: none + """ + argument_dict = ClcAlertPolicy._define_module_argument_spec() + module = AnsibleModule(supports_check_mode=True, **argument_dict) + clc_alert_policy = ClcAlertPolicy(module) + clc_alert_policy.process_request() + +from ansible.module_utils.basic import * # pylint: disable=W0614 +if __name__ == '__main__': + main() diff --git a/cloud/centurylink/clc_blueprint_package.py b/cloud/centurylink/clc_blueprint_package.py new file mode 100644 index 00000000000..80cc18a24ca --- /dev/null +++ b/cloud/centurylink/clc_blueprint_package.py @@ -0,0 +1,263 @@ +#!/usr/bin/python + +# CenturyLink Cloud Ansible Modules. +# +# These Ansible modules enable the CenturyLink Cloud v2 API to be called +# from an within Ansible Playbook. +# +# This file is part of CenturyLink Cloud, and is maintained +# by the Workflow as a Service Team +# +# Copyright 2015 CenturyLink Cloud +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# CenturyLink Cloud: http://www.CenturyLinkCloud.com +# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ +# + +DOCUMENTATION = ''' +module: clc_blueprint_package +short_desciption: deploys a blue print package on a set of servers in CenturyLink Cloud. +description: + - An Ansible module to deploy blue print package on a set of servers in CenturyLink Cloud. +options: + server_ids: + description: + - A list of server Ids to deploy the blue print package. + default: [] + required: True + aliases: [] + package_id: + description: + - The package id of the blue print. + default: None + required: True + aliases: [] + package_params: + description: + - The dictionary of arguments required to deploy the blue print. + default: {} + required: False + aliases: [] +''' + +EXAMPLES = ''' +# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples + +- name: Deploy package + clc_blueprint_package: + server_ids: + - UC1WFSDANS01 + - UC1WFSDANS02 + package_id: 77abb844-579d-478d-3955-c69ab4a7ba1a + package_params: {} +''' + +__version__ = '${version}' + +import requests + +# +# Requires the clc-python-sdk. +# sudo pip install clc-sdk +# +try: + import clc as clc_sdk + from clc import CLCException +except ImportError: + CLC_FOUND = False + clc_sdk = None +else: + CLC_FOUND = True + + +class ClcBlueprintPackage(): + + clc = clc_sdk + module = None + + def __init__(self, module): + """ + Construct module + """ + self.module = module + if not CLC_FOUND: + self.module.fail_json( + msg='clc-python-sdk required for this module') + + self._set_user_agent(self.clc) + + def process_request(self): + """ + Process the request - Main Code Path + :return: Returns with either an exit_json or fail_json + """ + p = self.module.params + + if not CLC_FOUND: + self.module.fail_json( + msg='clc-python-sdk required for this module') + + self._set_clc_credentials_from_env() + + server_ids = p['server_ids'] + package_id = p['package_id'] + package_params = p['package_params'] + state = p['state'] + if state == 'present': + changed, changed_server_ids, requests = self.ensure_package_installed( + server_ids, package_id, package_params) + if not self.module.check_mode: + self._wait_for_requests_to_complete(requests) + self.module.exit_json(changed=changed, server_ids=changed_server_ids) + + @staticmethod + def define_argument_spec(): + """ + This function defnines the dictionary object required for + package module + :return: the package dictionary object + """ + argument_spec = dict( + server_ids=dict(type='list', required=True), + package_id=dict(required=True), + package_params=dict(type='dict', default={}), + wait=dict(default=True), + state=dict(default='present', choices=['present']) + ) + return argument_spec + + def ensure_package_installed(self, server_ids, package_id, package_params): + """ + Ensure the package is installed in the given list of servers + :param server_ids: the server list where the package needs to be installed + :param package_id: the package id + :param package_params: the package arguments + :return: (changed, server_ids) + changed: A flag indicating if a change was made + server_ids: The list of servers modfied + """ + changed = False + requests = [] + servers = self._get_servers_from_clc( + server_ids, + 'Failed to get servers from CLC') + try: + for server in servers: + request = self.clc_install_package( + server, + package_id, + package_params) + requests.append(request) + changed = True + except CLCException as ex: + self.module.fail_json( + msg='Failed while installing package : %s with Error : %s' % + (package_id, ex)) + return changed, server_ids, requests + + def clc_install_package(self, server, package_id, package_params): + """ + Read all servers from CLC and executes each package from package_list + :param server_list: The target list of servers where the packages needs to be installed + :param package_list: The list of packages to be installed + :return: (changed, server_ids) + changed: A flag indicating if a change was made + server_ids: The list of servers modfied + """ + result = None + if not self.module.check_mode: + result = server.ExecutePackage( + package_id=package_id, + parameters=package_params) + return result + + def _wait_for_requests_to_complete(self, requests_lst): + """ + Waits until the CLC requests are complete if the wait argument is True + :param requests_lst: The list of CLC request objects + :return: none + """ + if not self.module.params['wait']: + return + for request in requests_lst: + request.WaitUntilComplete() + for request_details in request.requests: + if request_details.Status() != 'succeeded': + self.module.fail_json( + msg='Unable to process package install request') + + def _get_servers_from_clc(self, server_list, message): + """ + Internal function to fetch list of CLC server objects from a list of server ids + :param the list server ids + :return the list of CLC server objects + """ + try: + return self.clc.v2.Servers(server_list).servers + except CLCException as ex: + self.module.fail_json(msg=message + ': %s' % ex) + + def _set_clc_credentials_from_env(self): + """ + Set the CLC Credentials on the sdk by reading environment variables + :return: none + """ + env = os.environ + v2_api_token = env.get('CLC_V2_API_TOKEN', False) + v2_api_username = env.get('CLC_V2_API_USERNAME', False) + v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) + clc_alias = env.get('CLC_ACCT_ALIAS', False) + api_url = env.get('CLC_V2_API_URL', False) + + if api_url: + self.clc.defaults.ENDPOINT_URL_V2 = api_url + + if v2_api_token and clc_alias: + self.clc._LOGIN_TOKEN_V2 = v2_api_token + self.clc._V2_ENABLED = True + self.clc.ALIAS = clc_alias + elif v2_api_username and v2_api_passwd: + self.clc.v2.SetCredentials( + api_username=v2_api_username, + api_passwd=v2_api_passwd) + else: + return self.module.fail_json( + msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " + "environment variables") + + @staticmethod + def _set_user_agent(clc): + if hasattr(clc, 'SetRequestsSession'): + agent_string = "ClcAnsibleModule/" + __version__ + ses = requests.Session() + ses.headers.update({"Api-Client": agent_string}) + ses.headers['User-Agent'] += " " + agent_string + clc.SetRequestsSession(ses) + +def main(): + """ + Main function + :return: None + """ + module = AnsibleModule( + argument_spec=ClcBlueprintPackage.define_argument_spec(), + supports_check_mode=True + ) + clc_blueprint_package = ClcBlueprintPackage(module) + clc_blueprint_package.process_request() + +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() diff --git a/cloud/centurylink/clc_firewall_policy.py b/cloud/centurylink/clc_firewall_policy.py new file mode 100644 index 00000000000..260c82bc885 --- /dev/null +++ b/cloud/centurylink/clc_firewall_policy.py @@ -0,0 +1,542 @@ +#!/usr/bin/python + +# CenturyLink Cloud Ansible Modules. +# +# These Ansible modules enable the CenturyLink Cloud v2 API to be called +# from an within Ansible Playbook. +# +# This file is part of CenturyLink Cloud, and is maintained +# by the Workflow as a Service Team +# +# Copyright 2015 CenturyLink Cloud +# +# Licensed under the Apache License, Version 2.0 (the "License"); + +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# CenturyLink Cloud: http://www.CenturyLinkCloud.com +# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ +# + +DOCUMENTATION = ''' +module: clc_firewall_policy +short_desciption: Create/delete/update firewall policies +description: + - Create or delete or updated firewall polices on Centurylink Centurylink Cloud +options: + location: + description: + - Target datacenter for the firewall policy + default: None + required: True + aliases: [] + state: + description: + - Whether to create or delete the firewall policy + default: present + required: True + choices: ['present', 'absent'] + aliases: [] + source: + description: + - Source addresses for traffic on the originating firewall + default: None + required: For Creation + aliases: [] + destination: + description: + - Destination addresses for traffic on the terminating firewall + default: None + required: For Creation + aliases: [] + ports: + description: + - types of ports associated with the policy. TCP & UDP can take in single ports or port ranges. + default: None + required: False + choices: ['any', 'icmp', 'TCP/123', 'UDP/123', 'TCP/123-456', 'UDP/123-456'] + aliases: [] + firewall_policy_id: + description: + - Id of the firewall policy + default: None + required: False + aliases: [] + source_account_alias: + description: + - CLC alias for the source account + default: None + required: True + aliases: [] + destination_account_alias: + description: + - CLC alias for the destination account + default: None + required: False + aliases: [] + wait: + description: + - Whether to wait for the provisioning tasks to finish before returning. + default: True + required: False + choices: [ True, False ] + aliases: [] + enabled: + description: + - If the firewall policy is enabled or disabled + default: true + required: False + choices: [ true, false ] + aliases: [] + +''' + +EXAMPLES = ''' +--- +- name: Create Firewall Policy + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Create / Verify an Firewall Policy at CenturyLink Cloud + clc_firewall: + source_account_alias: WFAD + location: VA1 + state: present + source: 10.128.216.0/24 + destination: 10.128.216.0/24 + ports: Any + destination_account_alias: WFAD + +--- +- name: Delete Firewall Policy + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Delete an Firewall Policy at CenturyLink Cloud + clc_firewall: + source_account_alias: WFAD + location: VA1 + state: present + firewall_policy_id: c62105233d7a4231bd2e91b9c791eaae +''' + +__version__ = '${version}' + +import urlparse +from time import sleep +import requests + +try: + import clc as clc_sdk + from clc import CLCException +except ImportError: + CLC_FOUND = False + clc_sdk = None +else: + CLC_FOUND = True + + +class ClcFirewallPolicy(): + + clc = None + + def __init__(self, module): + """ + Construct module + """ + self.clc = clc_sdk + self.module = module + self.firewall_dict = {} + + if not CLC_FOUND: + self.module.fail_json( + msg='clc-python-sdk required for this module') + + self._set_user_agent(self.clc) + + @staticmethod + def _define_module_argument_spec(): + """ + Define the argument spec for the ansible module + :return: argument spec dictionary + """ + argument_spec = dict( + location=dict(required=True, defualt=None), + source_account_alias=dict(required=True, default=None), + destination_account_alias=dict(default=None), + firewall_policy_id=dict(default=None), + ports=dict(default=None, type='list'), + source=dict(defualt=None, type='list'), + destination=dict(defualt=None, type='list'), + wait=dict(default=True), + state=dict(default='present', choices=['present', 'absent']), + enabled=dict(defualt=None) + ) + return argument_spec + + def process_request(self): + """ + Execute the main code path, and handle the request + :return: none + """ + location = self.module.params.get('location') + source_account_alias = self.module.params.get('source_account_alias') + destination_account_alias = self.module.params.get( + 'destination_account_alias') + firewall_policy_id = self.module.params.get('firewall_policy_id') + ports = self.module.params.get('ports') + source = self.module.params.get('source') + destination = self.module.params.get('destination') + wait = self.module.params.get('wait') + state = self.module.params.get('state') + enabled = self.module.params.get('enabled') + + self.firewall_dict = { + 'location': location, + 'source_account_alias': source_account_alias, + 'destination_account_alias': destination_account_alias, + 'firewall_policy_id': firewall_policy_id, + 'ports': ports, + 'source': source, + 'destination': destination, + 'wait': wait, + 'state': state, + 'enabled': enabled} + + self._set_clc_credentials_from_env() + requests = [] + + if state == 'absent': + changed, firewall_policy_id, response = self._ensure_firewall_policy_is_absent( + source_account_alias, location, self.firewall_dict) + + elif state == 'present': + changed, firewall_policy_id, response = self._ensure_firewall_policy_is_present( + source_account_alias, location, self.firewall_dict) + else: + return self.module.fail_json(msg="Unknown State: " + state) + + return self.module.exit_json( + changed=changed, + firewall_policy_id=firewall_policy_id) + + @staticmethod + def _get_policy_id_from_response(response): + """ + Method to parse out the policy id from creation response + :param response: response from firewall creation control + :return: policy_id: firewall policy id from creation call + """ + url = response.get('links')[0]['href'] + path = urlparse.urlparse(url).path + path_list = os.path.split(path) + policy_id = path_list[-1] + return policy_id + + def _set_clc_credentials_from_env(self): + """ + Set the CLC Credentials on the sdk by reading environment variables + :return: none + """ + env = os.environ + v2_api_token = env.get('CLC_V2_API_TOKEN', False) + v2_api_username = env.get('CLC_V2_API_USERNAME', False) + v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) + clc_alias = env.get('CLC_ACCT_ALIAS', False) + api_url = env.get('CLC_V2_API_URL', False) + + if api_url: + self.clc.defaults.ENDPOINT_URL_V2 = api_url + + if v2_api_token and clc_alias: + self.clc._LOGIN_TOKEN_V2 = v2_api_token + self.clc._V2_ENABLED = True + self.clc.ALIAS = clc_alias + elif v2_api_username and v2_api_passwd: + self.clc.v2.SetCredentials( + api_username=v2_api_username, + api_passwd=v2_api_passwd) + else: + return self.module.fail_json( + msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " + "environment variables") + + def _ensure_firewall_policy_is_present( + self, + source_account_alias, + location, + firewall_dict): + """ + Ensures that a given firewall policy is present + :param source_account_alias: the source account alias for the firewall policy + :param location: datacenter of the firewall policy + :param firewall_dict: dictionary or request parameters for firewall policy creation + :return: (changed, firewall_policy, response) + changed: flag for if a change occurred + firewall_policy: policy that was changed + response: response from CLC API call + """ + changed = False + response = {} + firewall_policy_id = firewall_dict.get('firewall_policy_id') + + if firewall_policy_id is None: + if not self.module.check_mode: + response = self._create_firewall_policy( + source_account_alias, + location, + firewall_dict) + firewall_policy_id = self._get_policy_id_from_response( + response) + self._wait_for_requests_to_complete( + firewall_dict.get('wait'), + source_account_alias, + location, + firewall_policy_id) + changed = True + else: + get_before_response, success = self._get_firewall_policy( + source_account_alias, location, firewall_policy_id) + if not success: + return self.module.fail_json( + msg='Unable to find the firewall policy id : %s' % + firewall_policy_id) + changed = self._compare_get_request_with_dict( + get_before_response, + firewall_dict) + if not self.module.check_mode and changed: + response = self._update_firewall_policy( + source_account_alias, + location, + firewall_policy_id, + firewall_dict) + self._wait_for_requests_to_complete( + firewall_dict.get('wait'), + source_account_alias, + location, + firewall_policy_id) + return changed, firewall_policy_id, response + + def _ensure_firewall_policy_is_absent( + self, + source_account_alias, + location, + firewall_dict): + """ + Ensures that a given firewall policy is removed if present + :param source_account_alias: the source account alias for the firewall policy + :param location: datacenter of the firewall policy + :param firewall_dict: firewall policy to delete + :return: (changed, firewall_policy_id, response) + changed: flag for if a change occurred + firewall_policy_id: policy that was changed + response: response from CLC API call + """ + changed = False + response = [] + firewall_policy_id = firewall_dict.get('firewall_policy_id') + result, success = self._get_firewall_policy( + source_account_alias, location, firewall_policy_id) + if success: + if not self.module.check_mode: + response = self._delete_firewall_policy( + source_account_alias, + location, + firewall_policy_id) + changed = True + return changed, firewall_policy_id, response + + def _create_firewall_policy( + self, + source_account_alias, + location, + firewall_dict): + """ + Ensures that a given firewall policy is present + :param source_account_alias: the source account alias for the firewall policy + :param location: datacenter of the firewall policy + :param firewall_dict: dictionary or request parameters for firewall policy creation + :return: response from CLC API call + """ + payload = { + 'destinationAccount': firewall_dict.get('destination_account_alias'), + 'source': firewall_dict.get('source'), + 'destination': firewall_dict.get('destination'), + 'ports': firewall_dict.get('ports')} + try: + response = self.clc.v2.API.Call( + 'POST', '/v2-experimental/firewallPolicies/%s/%s' % + (source_account_alias, location), payload) + except self.clc.APIFailedResponse as e: + return self.module.fail_json( + msg="Unable to successfully create firewall policy. %s" % + str(e.response_text)) + return response + + def _delete_firewall_policy( + self, + source_account_alias, + location, + firewall_policy_id): + """ + Deletes a given firewall policy for an account alias in a datacenter + :param source_account_alias: the source account alias for the firewall policy + :param location: datacenter of the firewall policy + :param firewall_policy_id: firewall policy to delete + :return: response: response from CLC API call + """ + try: + response = self.clc.v2.API.Call( + 'DELETE', '/v2-experimental/firewallPolicies/%s/%s/%s' % + (source_account_alias, location, firewall_policy_id)) + except self.clc.APIFailedResponse as e: + return self.module.fail_json( + msg="Unable to successfully delete firewall policy. %s" % + str(e.response_text)) + return response + + def _update_firewall_policy( + self, + source_account_alias, + location, + firewall_policy_id, + firewall_dict): + """ + Updates a firewall policy for a given datacenter and account alias + :param source_account_alias: the source account alias for the firewall policy + :param location: datacenter of the firewall policy + :param firewall_policy_id: firewall policy to delete + :param firewall_dict: dictionary or request parameters for firewall policy creation + :return: response: response from CLC API call + """ + try: + response = self.clc.v2.API.Call( + 'PUT', + '/v2-experimental/firewallPolicies/%s/%s/%s' % + (source_account_alias, + location, + firewall_policy_id), + firewall_dict) + except self.clc.APIFailedResponse as e: + return self.module.fail_json( + msg="Unable to successfully update firewall policy. %s" % + str(e.response_text)) + return response + + @staticmethod + def _compare_get_request_with_dict(response, firewall_dict): + """ + Helper method to compare the json response for getting the firewall policy with the request parameters + :param response: response from the get method + :param firewall_dict: dictionary or request parameters for firewall policy creation + :return: changed: Boolean that returns true if there are differences between the response parameters and the playbook parameters + """ + + changed = False + + response_dest_account_alias = response.get('destinationAccount') + response_enabled = response.get('enabled') + response_source = response.get('source') + response_dest = response.get('destination') + response_ports = response.get('ports') + + request_dest_account_alias = firewall_dict.get( + 'destination_account_alias') + request_enabled = firewall_dict.get('enabled') + if request_enabled is None: + request_enabled = True + request_source = firewall_dict.get('source') + request_dest = firewall_dict.get('destination') + request_ports = firewall_dict.get('ports') + + if ( + response_dest_account_alias and str(response_dest_account_alias) != str(request_dest_account_alias)) or ( + response_enabled != request_enabled) or ( + response_source and response_source != request_source) or ( + response_dest and response_dest != request_dest) or ( + response_ports and response_ports != request_ports): + changed = True + return changed + + def _get_firewall_policy( + self, + source_account_alias, + location, + firewall_policy_id): + """ + Get back details for a particular firewall policy + :param source_account_alias: the source account alias for the firewall policy + :param location: datacenter of the firewall policy + :param firewall_policy_id: id of the firewall policy to get + :return: response from CLC API call + """ + response = [] + success = False + try: + response = self.clc.v2.API.Call( + 'GET', '/v2-experimental/firewallPolicies/%s/%s/%s' % + (source_account_alias, location, firewall_policy_id)) + success = True + except: + pass + return response, success + + def _wait_for_requests_to_complete( + self, + wait, + source_account_alias, + location, + firewall_policy_id): + """ + Waits until the CLC requests are complete if the wait argument is True + :param requests_lst: The list of CLC request objects + :return: none + """ + if wait: + response, success = self._get_firewall_policy( + source_account_alias, location, firewall_policy_id) + if response.get('status') == 'pending': + sleep(2) + self._wait_for_requests_to_complete( + wait, + source_account_alias, + location, + firewall_policy_id) + return None + + @staticmethod + def _set_user_agent(clc): + if hasattr(clc, 'SetRequestsSession'): + agent_string = "ClcAnsibleModule/" + __version__ + ses = requests.Session() + ses.headers.update({"Api-Client": agent_string}) + ses.headers['User-Agent'] += " " + agent_string + clc.SetRequestsSession(ses) + + +def main(): + """ + The main function. Instantiates the module and calls process_request. + :return: none + """ + module = AnsibleModule( + argument_spec=ClcFirewallPolicy._define_module_argument_spec(), + supports_check_mode=True) + + clc_firewall = ClcFirewallPolicy(module) + clc_firewall.process_request() + +from ansible.module_utils.basic import * # pylint: disable=W0614 +if __name__ == '__main__': + main() diff --git a/cloud/centurylink/clc_group.py b/cloud/centurylink/clc_group.py new file mode 100644 index 00000000000..a4fd976d429 --- /dev/null +++ b/cloud/centurylink/clc_group.py @@ -0,0 +1,370 @@ +#!/usr/bin/python + +# CenturyLink Cloud Ansible Modules. +# +# These Ansible modules enable the CenturyLink Cloud v2 API to be called +# from an within Ansible Playbook. +# +# This file is part of CenturyLink Cloud, and is maintained +# by the Workflow as a Service Team +# +# Copyright 2015 CenturyLink Cloud +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# CenturyLink Cloud: http://www.CenturyLinkCloud.com +# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ +# + +DOCUMENTATION = ''' +module: clc_group +short_desciption: Create/delete Server Groups at Centurylink Cloud +description: + - Create or delete Server Groups at Centurylink Centurylink Cloud +options: + name: + description: + - The name of the Server Group + description: + description: + - A description of the Server Group + parent: + description: + - The parent group of the server group + location: + description: + - Datacenter to create the group in + state: + description: + - Whether to create or delete the group + default: present + choices: ['present', 'absent'] + +''' + +EXAMPLES = ''' + +# Create a Server Group + +--- +- name: Create Server Group + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Create / Verify a Server Group at CenturyLink Cloud + clc_group: + name: 'My Cool Server Group' + parent: 'Default Group' + state: present + register: clc + + - name: debug + debug: var=clc + +# Delete a Server Group + +--- +- name: Delete Server Group + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Delete / Verify Absent a Server Group at CenturyLink Cloud + clc_group: + name: 'My Cool Server Group' + parent: 'Default Group' + state: absent + register: clc + + - name: debug + debug: var=clc + +''' + +__version__ = '${version}' + +import requests + +# +# Requires the clc-python-sdk. +# sudo pip install clc-sdk +# +try: + import clc as clc_sdk + from clc import CLCException +except ImportError: + CLC_FOUND = False + clc_sdk = None +else: + CLC_FOUND = True + + +class ClcGroup(object): + + clc = None + root_group = None + + def __init__(self, module): + """ + Construct module + """ + self.clc = clc_sdk + self.module = module + self.group_dict = {} + + if not CLC_FOUND: + self.module.fail_json( + msg='clc-python-sdk required for this module') + + self._set_user_agent(self.clc) + + def process_request(self): + """ + Execute the main code path, and handle the request + :return: none + """ + location = self.module.params.get('location') + group_name = self.module.params.get('name') + parent_name = self.module.params.get('parent') + group_description = self.module.params.get('description') + state = self.module.params.get('state') + + self._set_clc_credentials_from_env() + self.group_dict = self._get_group_tree_for_datacenter( + datacenter=location) + + if state == "absent": + changed, group, response = self._ensure_group_is_absent( + group_name=group_name, parent_name=parent_name) + + else: + changed, group, response = self._ensure_group_is_present( + group_name=group_name, parent_name=parent_name, group_description=group_description) + + + self.module.exit_json(changed=changed, group=group_name) + + # + # Functions to define the Ansible module and its arguments + # + + @staticmethod + def _define_module_argument_spec(): + """ + Define the argument spec for the ansible module + :return: argument spec dictionary + """ + argument_spec = dict( + name=dict(required=True), + description=dict(default=None), + parent=dict(default=None), + location=dict(default=None), + alias=dict(default=None), + custom_fields=dict(type='list', default=[]), + server_ids=dict(type='list', default=[]), + state=dict(default='present', choices=['present', 'absent'])) + + return argument_spec + + # + # Module Behavior Functions + # + + def _set_clc_credentials_from_env(self): + """ + Set the CLC Credentials on the sdk by reading environment variables + :return: none + """ + env = os.environ + v2_api_token = env.get('CLC_V2_API_TOKEN', False) + v2_api_username = env.get('CLC_V2_API_USERNAME', False) + v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) + clc_alias = env.get('CLC_ACCT_ALIAS', False) + api_url = env.get('CLC_V2_API_URL', False) + + if api_url: + self.clc.defaults.ENDPOINT_URL_V2 = api_url + + if v2_api_token and clc_alias: + self.clc._LOGIN_TOKEN_V2 = v2_api_token + self.clc._V2_ENABLED = True + self.clc.ALIAS = clc_alias + elif v2_api_username and v2_api_passwd: + self.clc.v2.SetCredentials( + api_username=v2_api_username, + api_passwd=v2_api_passwd) + else: + return self.module.fail_json( + msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " + "environment variables") + + def _ensure_group_is_absent(self, group_name, parent_name): + """ + Ensure that group_name is absent by deleting it if necessary + :param group_name: string - the name of the clc server group to delete + :param parent_name: string - the name of the parent group for group_name + :return: changed, group + """ + changed = False + group = [] + results = [] + + if self._group_exists(group_name=group_name, parent_name=parent_name): + if not self.module.check_mode: + group.append(group_name) + for g in group: + result = self._delete_group(group_name) + results.append(result) + changed = True + return changed, group, results + + def _delete_group(self, group_name): + """ + Delete the provided server group + :param group_name: string - the server group to delete + :return: none + """ + group, parent = self.group_dict.get(group_name) + response = group.Delete() + return response + + def _ensure_group_is_present( + self, + group_name, + parent_name, + group_description): + """ + Checks to see if a server group exists, creates it if it doesn't. + :param group_name: the name of the group to validate/create + :param parent_name: the name of the parent group for group_name + :param group_description: a short description of the server group (used when creating) + :return: (changed, group) - + changed: Boolean- whether a change was made, + group: A clc group object for the group + """ + assert self.root_group, "Implementation Error: Root Group not set" + parent = parent_name if parent_name is not None else self.root_group.name + description = group_description + changed = False + results = [] + groups = [] + group = group_name + + parent_exists = self._group_exists(group_name=parent, parent_name=None) + child_exists = self._group_exists(group_name=group_name, parent_name=parent) + + if parent_exists and child_exists: + group, parent = self.group_dict[group_name] + changed = False + elif parent_exists and not child_exists: + if not self.module.check_mode: + groups.append(group_name) + for g in groups: + group = self._create_group( + group=group, + parent=parent, + description=description) + results.append(group) + changed = True + else: + self.module.fail_json( + msg="parent group: " + + parent + + " does not exist") + + return changed, group, results + + def _create_group(self, group, parent, description): + """ + Create the provided server group + :param group: clc_sdk.Group - the group to create + :param parent: clc_sdk.Parent - the parent group for {group} + :param description: string - a text description of the group + :return: clc_sdk.Group - the created group + """ + + (parent, grandparent) = self.group_dict[parent] + return parent.Create(name=group, description=description) + + # + # Utility Functions + # + + def _group_exists(self, group_name, parent_name): + """ + Check to see if a group exists + :param group_name: string - the group to check + :param parent_name: string - the parent of group_name + :return: boolean - whether the group exists + """ + result = False + if group_name in self.group_dict: + (group, parent) = self.group_dict[group_name] + if parent_name is None or parent_name == parent.name: + result = True + return result + + def _get_group_tree_for_datacenter(self, datacenter=None, alias=None): + """ + Walk the tree of groups for a datacenter + :param datacenter: string - the datacenter to walk (ex: 'UC1') + :param alias: string - the account alias to search. Defaults to the current user's account + :return: a dictionary of groups and parents + """ + self.root_group = self.clc.v2.Datacenter( + location=datacenter).RootGroup() + return self._walk_groups_recursive( + parent_group=None, + child_group=self.root_group) + + def _walk_groups_recursive(self, parent_group, child_group): + """ + Walk a parent-child tree of groups, starting with the provided child group + :param parent_group: clc_sdk.Group - the parent group to start the walk + :param child_group: clc_sdk.Group - the child group to start the walk + :return: a dictionary of groups and parents + """ + result = {str(child_group): (child_group, parent_group)} + groups = child_group.Subgroups().groups + if len(groups) > 0: + for group in groups: + if group.type != 'default': + continue + + result.update(self._walk_groups_recursive(child_group, group)) + return result + + @staticmethod + def _set_user_agent(clc): + if hasattr(clc, 'SetRequestsSession'): + agent_string = "ClcAnsibleModule/" + __version__ + ses = requests.Session() + ses.headers.update({"Api-Client": agent_string}) + ses.headers['User-Agent'] += " " + agent_string + clc.SetRequestsSession(ses) + + +def main(): + """ + The main function. Instantiates the module and calls process_request. + :return: none + """ + module = AnsibleModule(argument_spec=ClcGroup._define_module_argument_spec(), supports_check_mode=True) + + clc_group = ClcGroup(module) + clc_group.process_request() + +from ansible.module_utils.basic import * # pylint: disable=W0614 +if __name__ == '__main__': + main() diff --git a/cloud/centurylink/clc_loadbalancer.py b/cloud/centurylink/clc_loadbalancer.py new file mode 100644 index 00000000000..058954c687b --- /dev/null +++ b/cloud/centurylink/clc_loadbalancer.py @@ -0,0 +1,759 @@ +#!/usr/bin/python + +# CenturyLink Cloud Ansible Modules. +# +# These Ansible modules enable the CenturyLink Cloud v2 API to be called +# from an within Ansible Playbook. +# +# This file is part of CenturyLink Cloud, and is maintained +# by the Workflow as a Service Team +# +# Copyright 2015 CenturyLink Cloud +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# CenturyLink Cloud: http://www.CenturyLinkCloud.com +# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ +# + +DOCUMENTATION = ''' +module: +short_desciption: Create, Delete shared loadbalancers in CenturyLink Cloud. +description: + - An Ansible module to Create, Delete shared loadbalancers in CenturyLink Cloud. +options: +options: + name: + description: + - The name of the loadbalancer + required: True + description: + description: + - A description for your loadbalancer + alias: + description: + - The alias of your CLC Account + required: True + location: + description: + - The location of the datacenter your load balancer resides in + required: True + method: + description: + -The balancing method for this pool + default: roundRobin + choices: ['sticky', 'roundRobin'] + persistence: + description: + - The persistence method for this load balancer + default: standard + choices: ['standard', 'sticky'] + port: + description: + - Port to configure on the public-facing side of the load balancer pool + choices: [80, 443] + nodes: + description: + - A list of nodes that you want added to your load balancer pool + status: + description: + - The status of your loadbalancer + default: enabled + choices: ['enabled', 'disabled'] + state: + description: + - Whether to create or delete the load balancer pool + default: present + choices: ['present', 'absent', 'port_absent', 'nodes_present', 'nodes_absent'] +''' + +EXAMPLES = ''' +# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples +- name: Create Loadbalancer + hosts: localhost + connection: local + tasks: + - name: Actually Create things + clc_loadbalancer: + name: test + description: test + alias: TEST + location: WA1 + port: 443 + nodes: + - { 'ipAddress': '10.11.22.123', 'privatePort': 80 } + state: present + +- name: Add node to an existing loadbalancer pool + hosts: localhost + connection: local + tasks: + - name: Actually Create things + clc_loadbalancer: + name: test + description: test + alias: TEST + location: WA1 + port: 443 + nodes: + - { 'ipAddress': '10.11.22.234', 'privatePort': 80 } + state: nodes_present + +- name: Remove node from an existing loadbalancer pool + hosts: localhost + connection: local + tasks: + - name: Actually Create things + clc_loadbalancer: + name: test + description: test + alias: TEST + location: WA1 + port: 443 + nodes: + - { 'ipAddress': '10.11.22.234', 'privatePort': 80 } + state: nodes_absent + +- name: Delete LoadbalancerPool + hosts: localhost + connection: local + tasks: + - name: Actually Delete things + clc_loadbalancer: + name: test + description: test + alias: TEST + location: WA1 + port: 443 + nodes: + - { 'ipAddress': '10.11.22.123', 'privatePort': 80 } + state: port_absent + +- name: Delete Loadbalancer + hosts: localhost + connection: local + tasks: + - name: Actually Delete things + clc_loadbalancer: + name: test + description: test + alias: TEST + location: WA1 + port: 443 + nodes: + - { 'ipAddress': '10.11.22.123', 'privatePort': 80 } + state: absent + +''' + +__version__ = '${version}' + +import requests +from time import sleep + +# +# Requires the clc-python-sdk. +# sudo pip install clc-sdk +# +try: + import clc as clc_sdk + from clc import CLCException +except ImportError: + CLC_FOUND = False + clc_sdk = None +else: + CLC_FOUND = True + +class ClcLoadBalancer(): + + clc = None + + def __init__(self, module): + """ + Construct module + """ + self.clc = clc_sdk + self.module = module + self.lb_dict = {} + + if not CLC_FOUND: + self.module.fail_json( + msg='clc-python-sdk required for this module') + + self._set_user_agent(self.clc) + + def process_request(self): + """ + Execute the main code path, and handle the request + :return: none + """ + + loadbalancer_name=self.module.params.get('name') + loadbalancer_alias=self.module.params.get('alias') + loadbalancer_location=self.module.params.get('location') + loadbalancer_description=self.module.params.get('description') + loadbalancer_port=self.module.params.get('port') + loadbalancer_method=self.module.params.get('method') + loadbalancer_persistence=self.module.params.get('persistence') + loadbalancer_nodes=self.module.params.get('nodes') + loadbalancer_status=self.module.params.get('status') + state=self.module.params.get('state') + + if loadbalancer_description == None: + loadbalancer_description = loadbalancer_name + + self._set_clc_credentials_from_env() + + self.lb_dict = self._get_loadbalancer_list(alias=loadbalancer_alias, location=loadbalancer_location) + + if state == 'present': + changed, result_lb, lb_id = self.ensure_loadbalancer_present(name=loadbalancer_name, + alias=loadbalancer_alias, + location=loadbalancer_location, + description=loadbalancer_description, + status=loadbalancer_status) + if loadbalancer_port: + changed, result_pool, pool_id = self.ensure_loadbalancerpool_present(lb_id=lb_id, + alias=loadbalancer_alias, + location=loadbalancer_location, + method=loadbalancer_method, + persistence=loadbalancer_persistence, + port=loadbalancer_port) + + if loadbalancer_nodes: + changed, result_nodes = self.ensure_lbpool_nodes_set(alias=loadbalancer_alias, + location=loadbalancer_location, + name=loadbalancer_name, + port=loadbalancer_port, + nodes=loadbalancer_nodes + ) + elif state == 'absent': + changed, result_lb = self.ensure_loadbalancer_absent(name=loadbalancer_name, + alias=loadbalancer_alias, + location=loadbalancer_location) + + elif state == 'port_absent': + changed, result_lb = self.ensure_loadbalancerpool_absent(alias=loadbalancer_alias, + location=loadbalancer_location, + name=loadbalancer_name, + port=loadbalancer_port) + + elif state == 'nodes_present': + changed, result_lb = self.ensure_lbpool_nodes_present(alias=loadbalancer_alias, + location=loadbalancer_location, + name=loadbalancer_name, + port=loadbalancer_port, + nodes=loadbalancer_nodes) + + elif state == 'nodes_absent': + changed, result_lb = self.ensure_lbpool_nodes_absent(alias=loadbalancer_alias, + location=loadbalancer_location, + name=loadbalancer_name, + port=loadbalancer_port, + nodes=loadbalancer_nodes) + + self.module.exit_json(changed=changed, loadbalancer=result_lb) + # + # Functions to define the Ansible module and its arguments + # + def ensure_loadbalancer_present(self,name,alias,location,description,status): + """ + Check for loadbalancer presence (available) + :param name: Name of loadbalancer + :param alias: Alias of account + :param location: Datacenter + :param description: Description of loadbalancer + :param status: Enabled / Disabled + :return: True / False + """ + changed = False + result = None + lb_id = self._loadbalancer_exists(name=name) + if lb_id: + result = name + changed = False + else: + if not self.module.check_mode: + result = self.create_loadbalancer(name=name, + alias=alias, + location=location, + description=description, + status=status) + lb_id = result.get('id') + changed = True + + return changed, result, lb_id + + def ensure_loadbalancerpool_present(self, lb_id, alias, location, method, persistence, port): + """ + Checks to see if a load balancer pool exists and creates one if it does not. + :param name: The loadbalancer name + :param alias: The account alias + :param location: the datacenter the load balancer resides in + :param method: the load balancing method + :param persistence: the load balancing persistence type + :param port: the port that the load balancer will listen on + :return: (changed, group, pool_id) - + changed: Boolean whether a change was made + result: The result from the CLC API call + pool_id: The string id of the pool + """ + changed = False + result = None + if not lb_id: + return False, None, None + pool_id = self._loadbalancerpool_exists(alias=alias, location=location, port=port, lb_id=lb_id) + if not pool_id: + changed = True + if not self.module.check_mode: + result = self.create_loadbalancerpool(alias=alias, location=location, lb_id=lb_id, method=method, persistence=persistence, port=port) + pool_id = result.get('id') + + else: + changed = False + result = port + + return changed, result, pool_id + + def ensure_loadbalancer_absent(self,name,alias,location): + """ + Check for loadbalancer presence (not available) + :param name: Name of loadbalancer + :param alias: Alias of account + :param location: Datacenter + :return: (changed, result) + changed: Boolean whether a change was made + result: The result from the CLC API Call + """ + changed = False + result = None + lb_exists = self._loadbalancer_exists(name=name) + if lb_exists: + if not self.module.check_mode: + result = self.delete_loadbalancer(alias=alias, + location=location, + name=name) + changed = True + else: + result = name + changed = False + return changed, result + + def ensure_loadbalancerpool_absent(self, alias, location, name, port): + """ + Checks to see if a load balancer pool exists and deletes it if it does + :param alias: The account alias + :param location: the datacenter the load balancer resides in + :param loadbalancer: the name of the load balancer + :param port: the port that the load balancer will listen on + :return: (changed, group) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + changed = False + result = None + lb_exists = self._loadbalancer_exists(name=name) + if lb_exists: + lb_id = self._get_loadbalancer_id(name=name) + pool_id = self._loadbalancerpool_exists(alias=alias, location=location, port=port, lb_id=lb_id) + if pool_id: + changed = True + if not self.module.check_mode: + result = self.delete_loadbalancerpool(alias=alias, location=location, lb_id=lb_id, pool_id=pool_id) + else: + changed = False + result = "Pool doesn't exist" + else: + result = "LB Doesn't Exist" + return changed, result + + def ensure_lbpool_nodes_set(self, alias, location, name, port, nodes): + """ + Checks to see if the provided list of nodes exist for the pool and set the nodes if any in the list doesn't exist + :param alias: The account alias + :param location: the datacenter the load balancer resides in + :param name: the name of the load balancer + :param port: the port that the load balancer will listen on + :param nodes: The list of nodes to be updated to the pool + :return: (changed, group) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + result = {} + changed = False + lb_exists = self._loadbalancer_exists(name=name) + if lb_exists: + lb_id = self._get_loadbalancer_id(name=name) + pool_id = self._loadbalancerpool_exists(alias=alias, location=location, port=port, lb_id=lb_id) + if pool_id: + nodes_exist = self._loadbalancerpool_nodes_exists(alias=alias, + location=location, + port=port, + lb_id=lb_id, + pool_id=pool_id, + nodes_to_check=nodes) + if not nodes_exist: + changed = True + result = self.set_loadbalancernodes(alias=alias, + location=location, + lb_id=lb_id, + pool_id=pool_id, + nodes=nodes) + else: + result = "Pool doesn't exist" + else: + result = "Load balancer doesn't Exist" + return changed, result + + def ensure_lbpool_nodes_present(self, alias, location, name, port, nodes): + """ + Checks to see if the provided list of nodes exist for the pool and add the missing nodes to the pool + :param alias: The account alias + :param location: the datacenter the load balancer resides in + :param name: the name of the load balancer + :param port: the port that the load balancer will listen on + :param nodes: the list of nodes to be added + :return: (changed, group) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + changed = False + lb_exists = self._loadbalancer_exists(name=name) + if lb_exists: + lb_id = self._get_loadbalancer_id(name=name) + pool_id = self._loadbalancerpool_exists(alias=alias, location=location, port=port, lb_id=lb_id) + if pool_id: + changed, result = self.add_lbpool_nodes(alias=alias, + location=location, + lb_id=lb_id, + pool_id=pool_id, + nodes_to_add=nodes) + else: + result = "Pool doesn't exist" + else: + result = "Load balancer doesn't Exist" + return changed, result + + def ensure_lbpool_nodes_absent(self, alias, location, name, port, nodes): + """ + Checks to see if the provided list of nodes exist for the pool and add the missing nodes to the pool + :param alias: The account alias + :param location: the datacenter the load balancer resides in + :param name: the name of the load balancer + :param port: the port that the load balancer will listen on + :param nodes: the list of nodes to be removed + :return: (changed, group) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + changed = False + lb_exists = self._loadbalancer_exists(name=name) + if lb_exists: + lb_id = self._get_loadbalancer_id(name=name) + pool_id = self._loadbalancerpool_exists(alias=alias, location=location, port=port, lb_id=lb_id) + if pool_id: + changed, result = self.remove_lbpool_nodes(alias=alias, + location=location, + lb_id=lb_id, + pool_id=pool_id, + nodes_to_remove=nodes) + else: + result = "Pool doesn't exist" + else: + result = "Load balancer doesn't Exist" + return changed, result + + def create_loadbalancer(self,name,alias,location,description,status): + """ + Create a loadbalancer w/ params + :param name: Name of loadbalancer + :param alias: Alias of account + :param location: Datacenter + :param description: Description for loadbalancer to be created + :param status: Enabled / Disabled + :return: Success / Failure + """ + result = self.clc.v2.API.Call('POST', '/v2/sharedLoadBalancers/%s/%s' % (alias, location), json.dumps({"name":name,"description":description,"status":status})) + sleep(1) + return result + + def create_loadbalancerpool(self, alias, location, lb_id, method, persistence, port): + """ + Creates a pool on the provided load balancer + :param alias: the account alias + :param location: the datacenter the load balancer resides in + :param lb_id: the id string of the load balancer + :param method: the load balancing method + :param persistence: the load balancing persistence type + :param port: the port that the load balancer will listen on + :return: result: The result from the create API call + """ + result = self.clc.v2.API.Call('POST', '/v2/sharedLoadBalancers/%s/%s/%s/pools' % (alias, location, lb_id), json.dumps({"port":port, "method":method, "persistence":persistence})) + return result + + def delete_loadbalancer(self,alias,location,name): + """ + Delete CLC loadbalancer + :param alias: Alias for account + :param location: Datacenter + :param name: Name of the loadbalancer to delete + :return: 204 if successful else failure + """ + lb_id = self._get_loadbalancer_id(name=name) + result = self.clc.v2.API.Call('DELETE', '/v2/sharedLoadBalancers/%s/%s/%s' % (alias, location, lb_id)) + return result + + def delete_loadbalancerpool(self, alias, location, lb_id, pool_id): + """ + Delete a pool on the provided load balancer + :param alias: The account alias + :param location: the datacenter the load balancer resides in + :param lb_id: the id string of the load balancer + :param pool_id: the id string of the pool + :return: result: The result from the delete API call + """ + result = self.clc.v2.API.Call('DELETE', '/v2/sharedLoadBalancers/%s/%s/%s/pools/%s' % (alias, location, lb_id, pool_id)) + return result + + def _get_loadbalancer_id(self, name): + """ + Retrieve unique ID of loadbalancer + :param name: Name of loadbalancer + :return: Unique ID of loadbalancer + """ + for lb in self.lb_dict: + if lb.get('name') == name: + id = lb.get('id') + return id + + def _get_loadbalancer_list(self, alias, location): + """ + Retrieve a list of loadbalancers + :param alias: Alias for account + :param location: Datacenter + :return: JSON data for all loadbalancers at datacenter + """ + return self.clc.v2.API.Call('GET', '/v2/sharedLoadBalancers/%s/%s' % (alias, location)) + + def _loadbalancer_exists(self, name): + """ + Verify a loadbalancer exists + :param name: Name of loadbalancer + :return: False or the ID of the existing loadbalancer + """ + result = False + + for lb in self.lb_dict: + if lb.get('name') == name: + result = lb.get('id') + return result + + def _loadbalancerpool_exists(self, alias, location, port, lb_id): + """ + Checks to see if a pool exists on the specified port on the provided load balancer + :param alias: the account alias + :param location: the datacenter the load balancer resides in + :param port: the port to check and see if it exists + :param lb_id: the id string of the provided load balancer + :return: result: The id string of the pool or False + """ + result = False + pool_list = self.clc.v2.API.Call('GET', '/v2/sharedLoadBalancers/%s/%s/%s/pools' % (alias, location, lb_id)) + for pool in pool_list: + if int(pool.get('port')) == int(port): + result = pool.get('id') + + return result + + def _loadbalancerpool_nodes_exists(self, alias, location, port, lb_id, pool_id, nodes_to_check): + """ + Checks to see if a set of nodes exists on the specified port on the provided load balancer + :param alias: the account alias + :param location: the datacenter the load balancer resides in + :param port: the port to check and see if it exists + :param lb_id: the id string of the provided load balancer + :param pool_id: the id string of the load balancer pool + :param nodes_to_check: the list of nodes to check for + :return: result: The id string of the pool or False + """ + result = False + nodes = self._get_lbpool_nodes(alias, location, lb_id, pool_id) + for node in nodes_to_check: + if not node.get('status'): + node['status'] = 'enabled' + if node in nodes: + result = True + else: + result = False + return result + + def set_loadbalancernodes(self, alias, location, lb_id, pool_id, nodes): + """ + Updates nodes to the provided pool + :param alias: the account alias + :param location: the datacenter the load balancer resides in + :param lb_id: the id string of the load balancer + :param pool_id: the id string of the pool + :param nodes: a list of dictionaries containing the nodes to set + :return: result: The result from the API call + """ + result = None + if not lb_id: + return result + if not self.module.check_mode: + result = self.clc.v2.API.Call('PUT', + '/v2/sharedLoadBalancers/%s/%s/%s/pools/%s/nodes' + % (alias, location, lb_id, pool_id), json.dumps(nodes)) + return result + + def add_lbpool_nodes(self, alias, location, lb_id, pool_id, nodes_to_add): + """ + Add nodes to the provided pool + :param alias: the account alias + :param location: the datacenter the load balancer resides in + :param lb_id: the id string of the load balancer + :param pool_id: the id string of the pool + :param nodes: a list of dictionaries containing the nodes to add + :return: (changed, group) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + changed = False + result = {} + nodes = self._get_lbpool_nodes(alias, location, lb_id, pool_id) + for node in nodes_to_add: + if not node.get('status'): + node['status'] = 'enabled' + if not node in nodes: + changed = True + nodes.append(node) + if changed == True and not self.module.check_mode: + result = self.set_loadbalancernodes(alias, location, lb_id, pool_id, nodes) + return changed, result + + def remove_lbpool_nodes(self, alias, location, lb_id, pool_id, nodes_to_remove): + """ + Removes nodes from the provided pool + :param alias: the account alias + :param location: the datacenter the load balancer resides in + :param lb_id: the id string of the load balancer + :param pool_id: the id string of the pool + :param nodes: a list of dictionaries containing the nodes to remove + :return: (changed, group) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + changed = False + result = {} + nodes = self._get_lbpool_nodes(alias, location, lb_id, pool_id) + for node in nodes_to_remove: + if not node.get('status'): + node['status'] = 'enabled' + if node in nodes: + changed = True + nodes.remove(node) + if changed == True and not self.module.check_mode: + result = self.set_loadbalancernodes(alias, location, lb_id, pool_id, nodes) + return changed, result + + def _get_lbpool_nodes(self, alias, location, lb_id, pool_id): + """ + Return the list of nodes available to the provided load balancer pool + :param alias: the account alias + :param location: the datacenter the load balancer resides in + :param lb_id: the id string of the load balancer + :param pool_id: the id string of the pool + :return: result: The list of nodes + """ + result = self.clc.v2.API.Call('GET', + '/v2/sharedLoadBalancers/%s/%s/%s/pools/%s/nodes' + % (alias, location, lb_id, pool_id)) + return result + + @staticmethod + def define_argument_spec(): + """ + Define the argument spec for the ansible module + :return: argument spec dictionary + """ + argument_spec = dict( + name=dict(required=True), + description=dict(default=None), + location=dict(required=True, default=None), + alias=dict(required=True, default=None), + port=dict(choices=[80, 443]), + method=dict(choices=['leastConnection', 'roundRobin']), + persistence=dict(choices=['standard', 'sticky']), + nodes=dict(type='list', default=[]), + status=dict(default='enabled', choices=['enabled', 'disabled']), + state=dict(default='present', choices=['present', 'absent', 'port_absent', 'nodes_present', 'nodes_absent']), + wait=dict(type='bool', default=True) + ) + + return argument_spec + + # + # Module Behavior Functions + # + + def _set_clc_credentials_from_env(self): + """ + Set the CLC Credentials on the sdk by reading environment variables + :return: none + """ + env = os.environ + v2_api_token = env.get('CLC_V2_API_TOKEN', False) + v2_api_username = env.get('CLC_V2_API_USERNAME', False) + v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) + clc_alias = env.get('CLC_ACCT_ALIAS', False) + api_url = env.get('CLC_V2_API_URL', False) + + if api_url: + self.clc.defaults.ENDPOINT_URL_V2 = api_url + + if v2_api_token and clc_alias: + self.clc._LOGIN_TOKEN_V2 = v2_api_token + self.clc._V2_ENABLED = True + self.clc.ALIAS = clc_alias + elif v2_api_username and v2_api_passwd: + self.clc.v2.SetCredentials( + api_username=v2_api_username, + api_passwd=v2_api_passwd) + else: + return self.module.fail_json( + msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " + "environment variables") + + @staticmethod + def _set_user_agent(clc): + if hasattr(clc, 'SetRequestsSession'): + agent_string = "ClcAnsibleModule/" + __version__ + ses = requests.Session() + ses.headers.update({"Api-Client": agent_string}) + ses.headers['User-Agent'] += " " + agent_string + clc.SetRequestsSession(ses) + + +def main(): + """ + The main function. Instantiates the module and calls process_request. + :return: none + """ + module = AnsibleModule(argument_spec=ClcLoadBalancer.define_argument_spec(), + supports_check_mode=True) + clc_loadbalancer = ClcLoadBalancer(module) + clc_loadbalancer.process_request() + +from ansible.module_utils.basic import * # pylint: disable=W0614 +if __name__ == '__main__': + main() diff --git a/cloud/centurylink/clc_modify_server.py b/cloud/centurylink/clc_modify_server.py new file mode 100644 index 00000000000..1a1e4d5b858 --- /dev/null +++ b/cloud/centurylink/clc_modify_server.py @@ -0,0 +1,710 @@ +#!/usr/bin/python + +# CenturyLink Cloud Ansible Modules. +# +# These Ansible modules enable the CenturyLink Cloud v2 API to be called +# from an within Ansible Playbook. +# +# This file is part of CenturyLink Cloud, and is maintained +# by the Workflow as a Service Team +# +# Copyright 2015 CenturyLink Cloud +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# CenturyLink Cloud: http://www.CenturyLinkCloud.com +# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ +# + +DOCUMENTATION = ''' +module: clc_modify_server +short_desciption: modify servers in CenturyLink Cloud. +description: + - An Ansible module to modify servers in CenturyLink Cloud. +options: + server_ids: + description: + - A list of server Ids to modify. + default: [] + required: True + aliases: [] + cpu: + description: + - How many CPUs to update on the server + default: None + required: False + aliases: [] + memory: + description: + - Memory in GB. + default: None + required: False + aliases: [] + anti_affinity_policy_id: + description: + - The anti affinity policy id to be set for a heperscale server. + This is mutually exclusive with 'anti_affinity_policy_name' + default: None + required: False + aliases: [] + anti_affinity_policy_name: + description: + - The anti affinity policy name to be set for a heperscale server. + This is mutually exclusive with 'anti_affinity_policy_id' + default: None + required: False + aliases: [] + alert_policy_id: + description: + - The alert policy id to be associated. + This is mutually exclusive with 'alert_policy_name' + default: None + required: False + aliases: [] + alert_policy_name: + description: + - The alert policy name to be associated. + This is mutually exclusive with 'alert_policy_id' + default: None + required: False + aliases: [] + state: + description: + - The state to insure that the provided resources are in. + default: 'present' + required: False + choices: ['present', 'absent'] + aliases: [] + wait: + description: + - Whether to wait for the provisioning tasks to finish before returning. + default: True + required: False + choices: [ True, False] + aliases: [] +''' + +EXAMPLES = ''' +# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples + +- name: set the cpu count to 4 on a server + clc_server: + server_ids: ['UC1ACCTTEST01'] + cpu: 4 + state: present + +- name: set the memory to 8GB on a server + clc_server: + server_ids: ['UC1ACCTTEST01'] + memory: 8 + state: present + +- name: set the anti affinity policy on a server + clc_server: + server_ids: ['UC1ACCTTEST01'] + anti_affinity_policy_name: 'aa_policy' + state: present + +- name: set the alert policy on a server + clc_server: + server_ids: ['UC1ACCTTEST01'] + alert_policy_name: 'alert_policy' + state: present + +- name: set the memory to 16GB and cpu to 8 core on a lust if servers + clc_server: + server_ids: ['UC1ACCTTEST01','UC1ACCTTEST02'] + cpu: 8 + memory: 16 + state: present +''' + +__version__ = '${version}' + +import requests + +# +# Requires the clc-python-sdk. +# sudo pip install clc-sdk +# +try: + import clc as clc_sdk + from clc import CLCException + from clc import APIFailedResponse +except ImportError: + CLC_FOUND = False + clc_sdk = None +else: + CLC_FOUND = True + + +class ClcModifyServer(): + clc = clc_sdk + + def __init__(self, module): + """ + Construct module + """ + self.clc = clc_sdk + self.module = module + self.group_dict = {} + + if not CLC_FOUND: + self.module.fail_json( + msg='clc-python-sdk required for this module') + + self._set_user_agent(self.clc) + + def process_request(self): + """ + Process the request - Main Code Path + :return: Returns with either an exit_json or fail_json + """ + self._set_clc_credentials_from_env() + + p = self.module.params + + server_ids = p['server_ids'] + if not isinstance(server_ids, list): + return self.module.fail_json( + msg='server_ids needs to be a list of instances to modify: %s' % + server_ids) + + (changed, server_dict_array, new_server_ids) = ClcModifyServer._modify_servers( + module=self.module, clc=self.clc, server_ids=server_ids) + + self.module.exit_json( + changed=changed, + server_ids=new_server_ids, + servers=server_dict_array) + + @staticmethod + def _define_module_argument_spec(): + """ + Define the argument spec for the ansible module + :return: argument spec dictionary + """ + argument_spec = dict( + server_ids=dict(type='list', required=True), + state=dict(default='present', choices=['present', 'absent']), + cpu=dict(), + memory=dict(), + anti_affinity_policy_id=dict(), + anti_affinity_policy_name=dict(), + alert_policy_id=dict(), + alert_policy_name=dict(), + wait=dict(type='bool', default=True) + ) + mutually_exclusive = [ + ['anti_affinity_policy_id', 'anti_affinity_policy_name'], + ['alert_policy_id', 'alert_policy_name'] + ] + return {"argument_spec": argument_spec, + "mutually_exclusive": mutually_exclusive} + + def _set_clc_credentials_from_env(self): + """ + Set the CLC Credentials on the sdk by reading environment variables + :return: none + """ + env = os.environ + v2_api_token = env.get('CLC_V2_API_TOKEN', False) + v2_api_username = env.get('CLC_V2_API_USERNAME', False) + v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) + clc_alias = env.get('CLC_ACCT_ALIAS', False) + api_url = env.get('CLC_V2_API_URL', False) + + if api_url: + self.clc.defaults.ENDPOINT_URL_V2 = api_url + + if v2_api_token and clc_alias: + self.clc._LOGIN_TOKEN_V2 = v2_api_token + self.clc._V2_ENABLED = True + self.clc.ALIAS = clc_alias + elif v2_api_username and v2_api_passwd: + self.clc.v2.SetCredentials( + api_username=v2_api_username, + api_passwd=v2_api_passwd) + else: + return self.module.fail_json( + msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " + "environment variables") + + @staticmethod + def _wait_for_requests(clc, requests, servers, wait): + """ + Block until server provisioning requests are completed. + :param clc: the clc-sdk instance to use + :param requests: a list of clc-sdk.Request instances + :param servers: a list of servers to refresh + :param wait: a boolean on whether to block or not. This function is skipped if True + :return: none + """ + if wait: + # Requests.WaitUntilComplete() returns the count of failed requests + failed_requests_count = sum( + [request.WaitUntilComplete() for request in requests]) + + if failed_requests_count > 0: + raise clc + else: + ClcModifyServer._refresh_servers(servers) + + @staticmethod + def _refresh_servers(servers): + """ + Loop through a list of servers and refresh them + :param servers: list of clc-sdk.Server instances to refresh + :return: none + """ + for server in servers: + server.Refresh() + + @staticmethod + def _modify_servers(module, clc, server_ids): + """ + modify the servers configuration on the provided list + :param module: the AnsibleModule object + :param clc: the clc-sdk instance to use + :param server_ids: list of servers to modify + :return: a list of dictionaries with server information about the servers that were modified + """ + p = module.params + wait = p.get('wait') + state = p.get('state') + server_params = { + 'cpu': p.get('cpu'), + 'memory': p.get('memory'), + 'anti_affinity_policy_id': p.get('anti_affinity_policy_id'), + 'anti_affinity_policy_name': p.get('anti_affinity_policy_name'), + 'alert_policy_id': p.get('alert_policy_id'), + 'alert_policy_name': p.get('alert_policy_name'), + } + changed = False + server_changed = False + aa_changed = False + ap_changed = False + server_dict_array = [] + result_server_ids = [] + requests = [] + + if not isinstance(server_ids, list) or len(server_ids) < 1: + return module.fail_json( + msg='server_ids should be a list of servers, aborting') + + servers = clc.v2.Servers(server_ids).Servers() + if state == 'present': + for server in servers: + server_changed, server_result, changed_servers = ClcModifyServer._ensure_server_config( + clc, module, None, server, server_params) + if server_result: + requests.append(server_result) + aa_changed, changed_servers = ClcModifyServer._ensure_aa_policy( + clc, module, None, server, server_params) + ap_changed, changed_servers = ClcModifyServer._ensure_alert_policy_present( + clc, module, None, server, server_params) + elif state == 'absent': + for server in servers: + ap_changed, changed_servers = ClcModifyServer._ensure_alert_policy_absent( + clc, module, None, server, server_params) + if server_changed or aa_changed or ap_changed: + changed = True + + if wait: + for r in requests: + r.WaitUntilComplete() + for server in changed_servers: + server.Refresh() + + for server in changed_servers: + server_dict_array.append(server.data) + result_server_ids.append(server.id) + + return changed, server_dict_array, result_server_ids + + @staticmethod + def _ensure_server_config( + clc, module, alias, server, server_params): + """ + ensures the server is updated with the provided cpu and memory + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param alias: the CLC account alias + :param server: the CLC server object + :param server_params: the dictionary of server parameters + :return: (changed, group) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + cpu = server_params.get('cpu') + memory = server_params.get('memory') + changed = False + result = None + changed_servers = [] + + if not cpu: + cpu = server.cpu + if not memory: + memory = server.memory + if memory != server.memory or cpu != server.cpu: + changed_servers.append(server) + result = ClcModifyServer._modify_clc_server( + clc, + module, + None, + server.id, + cpu, + memory) + changed = True + return changed, result, changed_servers + + @staticmethod + def _modify_clc_server(clc, module, acct_alias, server_id, cpu, memory): + """ + Modify the memory or CPU on a clc server. This function is not yet implemented. + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param acct_alias: the clc account alias to look up the server + :param server_id: id of the server to modify + :param cpu: the new cpu value + :param memory: the new memory value + :return: the result of CLC API call + """ + if not acct_alias: + acct_alias = clc.v2.Account.GetAlias() + if not server_id: + return module.fail_json( + msg='server_id must be provided to modify the server') + + result = None + + if not module.check_mode: + + # Update the server configuation + job_obj = clc.v2.API.Call('PATCH', + 'servers/%s/%s' % (acct_alias, + server_id), + json.dumps([{"op": "set", + "member": "memory", + "value": memory}, + {"op": "set", + "member": "cpu", + "value": cpu}])) + result = clc.v2.Requests(job_obj) + return result + + @staticmethod + def _ensure_aa_policy( + clc, module, acct_alias, server, server_params): + """ + ensures the server is updated with the provided anti affinity policy + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param acct_alias: the CLC account alias + :param server: the CLC server object + :param server_params: the dictionary of server parameters + :return: (changed, group) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + changed = False + changed_servers = [] + + if not acct_alias: + acct_alias = clc.v2.Account.GetAlias() + + aa_policy_id = server_params.get('anti_affinity_policy_id') + aa_policy_name = server_params.get('anti_affinity_policy_name') + if not aa_policy_id and aa_policy_name: + aa_policy_id = ClcModifyServer._get_aa_policy_id_by_name( + clc, + module, + acct_alias, + aa_policy_name) + current_aa_policy_id = ClcModifyServer._get_aa_policy_id_of_server( + clc, + module, + acct_alias, + server.id) + + if aa_policy_id and aa_policy_id != current_aa_policy_id: + if server not in changed_servers: + changed_servers.append(server) + ClcModifyServer._modify_aa_policy( + clc, + module, + acct_alias, + server.id, + aa_policy_id) + changed = True + return changed, changed_servers + + @staticmethod + def _modify_aa_policy(clc, module, acct_alias, server_id, aa_policy_id): + """ + modifies the anti affinity policy of the CLC server + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param acct_alias: the CLC account alias + :param server_id: the CLC server id + :param aa_policy_id: the anti affinity policy id + :return: result: The result from the CLC API call + """ + result = None + if not module.check_mode: + result = clc.v2.API.Call('PUT', + 'servers/%s/%s/antiAffinityPolicy' % ( + acct_alias, + server_id), + json.dumps({"id": aa_policy_id})) + return result + + @staticmethod + def _get_aa_policy_id_by_name(clc, module, alias, aa_policy_name): + """ + retrieves the anti affinity policy id of the server based on the name of the policy + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param alias: the CLC account alias + :param aa_policy_name: the anti affinity policy name + :return: aa_policy_id: The anti affinity policy id + """ + aa_policy_id = None + aa_policies = clc.v2.API.Call(method='GET', + url='antiAffinityPolicies/%s' % (alias)) + for aa_policy in aa_policies.get('items'): + if aa_policy.get('name') == aa_policy_name: + if not aa_policy_id: + aa_policy_id = aa_policy.get('id') + else: + return module.fail_json( + msg='mutiple anti affinity policies were found with policy name : %s' % + (aa_policy_name)) + if not aa_policy_id: + return module.fail_json( + msg='No anti affinity policy was found with policy name : %s' % + (aa_policy_name)) + return aa_policy_id + + @staticmethod + def _get_aa_policy_id_of_server(clc, module, alias, server_id): + """ + retrieves the anti affinity policy id of the server based on the CLC server id + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param alias: the CLC account alias + :param server_id: the CLC server id + :return: aa_policy_id: The anti affinity policy id + """ + aa_policy_id = None + try: + result = clc.v2.API.Call( + method='GET', url='servers/%s/%s/antiAffinityPolicy' % + (alias, server_id)) + aa_policy_id = result.get('id') + except APIFailedResponse as e: + if e.response_status_code != 404: + raise e + return aa_policy_id + + @staticmethod + def _ensure_alert_policy_present( + clc, module, acct_alias, server, server_params): + """ + ensures the server is updated with the provided alert policy + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param acct_alias: the CLC account alias + :param server: the CLC server object + :param server_params: the dictionary of server parameters + :return: (changed, group) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + changed = False + changed_servers = [] + + if not acct_alias: + acct_alias = clc.v2.Account.GetAlias() + + alert_policy_id = server_params.get('alert_policy_id') + alert_policy_name = server_params.get('alert_policy_name') + if not alert_policy_id and alert_policy_name: + alert_policy_id = ClcModifyServer._get_alert_policy_id_by_name( + clc, + module, + acct_alias, + alert_policy_name) + if alert_policy_id and not ClcModifyServer._alert_policy_exists(server, alert_policy_id): + if server not in changed_servers: + changed_servers.append(server) + ClcModifyServer._add_alert_policy_to_server( + clc, + module, + acct_alias, + server.id, + alert_policy_id) + changed = True + return changed, changed_servers + + @staticmethod + def _ensure_alert_policy_absent( + clc, module, acct_alias, server, server_params): + """ + ensures the alert policy is removed from the server + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param acct_alias: the CLC account alias + :param server: the CLC server object + :param server_params: the dictionary of server parameters + :return: (changed, group) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + changed = False + result = None + changed_servers = [] + + if not acct_alias: + acct_alias = clc.v2.Account.GetAlias() + + alert_policy_id = server_params.get('alert_policy_id') + alert_policy_name = server_params.get('alert_policy_name') + if not alert_policy_id and alert_policy_name: + alert_policy_id = ClcModifyServer._get_alert_policy_id_by_name( + clc, + module, + acct_alias, + alert_policy_name) + + if alert_policy_id and ClcModifyServer._alert_policy_exists(server, alert_policy_id): + if server not in changed_servers: + changed_servers.append(server) + ClcModifyServer._remove_alert_policy_to_server( + clc, + module, + acct_alias, + server.id, + alert_policy_id) + changed = True + return changed, changed_servers + + @staticmethod + def _add_alert_policy_to_server(clc, module, acct_alias, server_id, alert_policy_id): + """ + add the alert policy to CLC server + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param acct_alias: the CLC account alias + :param server_id: the CLC server id + :param alert_policy_id: the alert policy id + :return: result: The result from the CLC API call + """ + result = None + if not module.check_mode: + try: + result = clc.v2.API.Call('POST', + 'servers/%s/%s/alertPolicies' % ( + acct_alias, + server_id), + json.dumps({"id": alert_policy_id})) + except clc.APIFailedResponse as e: + return module.fail_json( + msg='Unable to set alert policy to the server : %s. %s' % (server_id, str(e.response_text))) + return result + + @staticmethod + def _remove_alert_policy_to_server(clc, module, acct_alias, server_id, alert_policy_id): + """ + remove the alert policy to the CLC server + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param acct_alias: the CLC account alias + :param server_id: the CLC server id + :param alert_policy_id: the alert policy id + :return: result: The result from the CLC API call + """ + result = None + if not module.check_mode: + try: + result = clc.v2.API.Call('DELETE', + 'servers/%s/%s/alertPolicies/%s' + % (acct_alias, server_id, alert_policy_id)) + except clc.APIFailedResponse as e: + return module.fail_json( + msg='Unable to remove alert policy to the server : %s. %s' % (server_id, str(e.response_text))) + return result + + @staticmethod + def _get_alert_policy_id_by_name(clc, module, alias, alert_policy_name): + """ + retrieves the alert policy id of the server based on the name of the policy + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param alias: the CLC account alias + :param alert_policy_name: the alert policy name + :return: alert_policy_id: The alert policy id + """ + alert_policy_id = None + alert_policies = clc.v2.API.Call(method='GET', + url='alertPolicies/%s' % (alias)) + for alert_policy in alert_policies.get('items'): + if alert_policy.get('name') == alert_policy_name: + if not alert_policy_id: + alert_policy_id = alert_policy.get('id') + else: + return module.fail_json( + msg='mutiple alert policies were found with policy name : %s' % + (alert_policy_name)) + return alert_policy_id + + @staticmethod + def _alert_policy_exists(server, alert_policy_id): + """ + Checks if the alert policy exists for the server + :param server: the clc server object + :param alert_policy_id: the alert policy + :return: True: if the given alert policy id associated to the server, False otherwise + """ + result = False + alert_policies = server.alertPolicies + if alert_policies: + for alert_policy in alert_policies: + if alert_policy.get('id') == alert_policy_id: + result = True + return result + + @staticmethod + def _set_user_agent(clc): + if hasattr(clc, 'SetRequestsSession'): + agent_string = "ClcAnsibleModule/" + __version__ + ses = requests.Session() + ses.headers.update({"Api-Client": agent_string}) + ses.headers['User-Agent'] += " " + agent_string + clc.SetRequestsSession(ses) + + +def main(): + """ + The main function. Instantiates the module and calls process_request. + :return: none + """ + + argument_dict = ClcModifyServer._define_module_argument_spec() + module = AnsibleModule(supports_check_mode=True, **argument_dict) + clc_modify_server = ClcModifyServer(module) + clc_modify_server.process_request() + +from ansible.module_utils.basic import * # pylint: disable=W0614 +if __name__ == '__main__': + main() diff --git a/cloud/centurylink/clc_publicip.py b/cloud/centurylink/clc_publicip.py new file mode 100644 index 00000000000..2e525a51455 --- /dev/null +++ b/cloud/centurylink/clc_publicip.py @@ -0,0 +1,316 @@ +#!/usr/bin/python + +# CenturyLink Cloud Ansible Modules. +# +# These Ansible modules enable the CenturyLink Cloud v2 API to be called +# from an within Ansible Playbook. +# +# This file is part of CenturyLink Cloud, and is maintained +# by the Workflow as a Service Team +# +# Copyright 2015 CenturyLink Cloud +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# CenturyLink Cloud: http://www.CenturyLinkCloud.com +# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ +# + +DOCUMENTATION = ''' +module: clc_publicip +short_description: Add and Delete public ips on servers in CenturyLink Cloud. +description: + - An Ansible module to add or delete public ip addresses on an existing server or servers in CenturyLink Cloud. +options: + protocol: + descirption: + - The protocol that the public IP will listen for. + default: TCP + required: False + ports: + description: + - A list of ports to expose. + required: True + server_ids: + description: + - A list of servers to create public ips on. + required: True + state: + description: + - Determine wheteher to create or delete public IPs. If present module will not create a second public ip if one + already exists. + default: present + choices: ['present', 'absent'] + required: False + wait: + description: + - Whether to wait for the tasks to finish before returning. + choices: [ True, False ] + default: True + required: False +''' + +EXAMPLES = ''' +# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples + +- name: Add Public IP to Server + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Create Public IP For Servers + clc_publicip: + protocol: 'TCP' + ports: + - 80 + server_ids: + - UC1ACCTSRVR01 + - UC1ACCTSRVR02 + state: present + register: clc + + - name: debug + debug: var=clc + +- name: Delete Public IP from Server + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Create Public IP For Servers + clc_publicip: + server_ids: + - UC1ACCTSRVR01 + - UC1ACCTSRVR02 + state: absent + register: clc + + - name: debug + debug: var=clc +''' + +__version__ = '${version}' + +import requests + +# +# Requires the clc-python-sdk. +# sudo pip install clc-sdk +# +try: + import clc as clc_sdk + from clc import CLCException +except ImportError: + CLC_FOUND = False + clc_sdk = None +else: + CLC_FOUND = True + + +class ClcPublicIp(object): + clc = clc_sdk + module = None + group_dict = {} + + def __init__(self, module): + """ + Construct module + """ + self.module = module + if not CLC_FOUND: + self.module.fail_json( + msg='clc-python-sdk required for this module') + + self._set_user_agent(self.clc) + + def process_request(self): + """ + Process the request - Main Code Path + :param params: dictionary of module parameters + :return: Returns with either an exit_json or fail_json + """ + self._set_clc_credentials_from_env() + params = self.module.params + server_ids = params['server_ids'] + ports = params['ports'] + protocol = params['protocol'] + state = params['state'] + requests = [] + chagned_server_ids = [] + changed = False + + if state == 'present': + changed, chagned_server_ids, requests = self.ensure_public_ip_present( + server_ids=server_ids, protocol=protocol, ports=ports) + elif state == 'absent': + changed, chagned_server_ids, requests = self.ensure_public_ip_absent( + server_ids=server_ids) + else: + return self.module.fail_json(msg="Unknown State: " + state) + self._wait_for_requests_to_complete(requests) + return self.module.exit_json(changed=changed, + server_ids=chagned_server_ids) + + @staticmethod + def _define_module_argument_spec(): + """ + Define the argument spec for the ansible module + :return: argument spec dictionary + """ + argument_spec = dict( + server_ids=dict(type='list', required=True), + protocol=dict(default='TCP'), + ports=dict(type='list'), + wait=dict(type='bool', default=True), + state=dict(default='present', choices=['present', 'absent']), + ) + return argument_spec + + def ensure_public_ip_present(self, server_ids, protocol, ports): + """ + Ensures the given server ids having the public ip available + :param server_ids: the list of server ids + :param protocol: the ip protocol + :param ports: the list of ports to expose + :return: (changed, changed_server_ids, results) + changed: A flag indicating if there is any change + changed_server_ids : the list of server ids that are changed + results: The result list from clc public ip call + """ + changed = False + results = [] + changed_server_ids = [] + servers = self._get_servers_from_clc( + server_ids, + 'Failed to obtain server list from the CLC API') + servers_to_change = [ + server for server in servers if len( + server.PublicIPs().public_ips) == 0] + ports_to_expose = [{'protocol': protocol, 'port': port} + for port in ports] + for server in servers_to_change: + if not self.module.check_mode: + result = server.PublicIPs().Add(ports_to_expose) + results.append(result) + changed_server_ids.append(server.id) + changed = True + return changed, changed_server_ids, results + + def ensure_public_ip_absent(self, server_ids): + """ + Ensures the given server ids having the public ip removed if there is any + :param server_ids: the list of server ids + :return: (changed, changed_server_ids, results) + changed: A flag indicating if there is any change + changed_server_ids : the list of server ids that are changed + results: The result list from clc public ip call + """ + changed = False + results = [] + changed_server_ids = [] + servers = self._get_servers_from_clc( + server_ids, + 'Failed to obtain server list from the CLC API') + servers_to_change = [ + server for server in servers if len( + server.PublicIPs().public_ips) > 0] + ips_to_delete = [] + for server in servers_to_change: + for ip_address in server.PublicIPs().public_ips: + ips_to_delete.append(ip_address) + for server in servers_to_change: + if not self.module.check_mode: + for ip in ips_to_delete: + result = ip.Delete() + results.append(result) + changed_server_ids.append(server.id) + changed = True + return changed, changed_server_ids, results + + def _wait_for_requests_to_complete(self, requests_lst): + """ + Waits until the CLC requests are complete if the wait argument is True + :param requests_lst: The list of CLC request objects + :return: none + """ + if not self.module.params['wait']: + return + for request in requests_lst: + request.WaitUntilComplete() + for request_details in request.requests: + if request_details.Status() != 'succeeded': + self.module.fail_json( + msg='Unable to process public ip request') + + def _set_clc_credentials_from_env(self): + """ + Set the CLC Credentials on the sdk by reading environment variables + :return: none + """ + env = os.environ + v2_api_token = env.get('CLC_V2_API_TOKEN', False) + v2_api_username = env.get('CLC_V2_API_USERNAME', False) + v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) + clc_alias = env.get('CLC_ACCT_ALIAS', False) + api_url = env.get('CLC_V2_API_URL', False) + + if api_url: + self.clc.defaults.ENDPOINT_URL_V2 = api_url + + if v2_api_token and clc_alias: + self.clc._LOGIN_TOKEN_V2 = v2_api_token + self.clc._V2_ENABLED = True + self.clc.ALIAS = clc_alias + elif v2_api_username and v2_api_passwd: + self.clc.v2.SetCredentials( + api_username=v2_api_username, + api_passwd=v2_api_passwd) + else: + return self.module.fail_json( + msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " + "environment variables") + + def _get_servers_from_clc(self, server_ids, message): + """ + Gets list of servers form CLC api + """ + try: + return self.clc.v2.Servers(server_ids).servers + except CLCException as exception: + self.module.fail_json(msg=message + ': %s' % exception) + + @staticmethod + def _set_user_agent(clc): + if hasattr(clc, 'SetRequestsSession'): + agent_string = "ClcAnsibleModule/" + __version__ + ses = requests.Session() + ses.headers.update({"Api-Client": agent_string}) + ses.headers['User-Agent'] += " " + agent_string + clc.SetRequestsSession(ses) + + +def main(): + """ + The main function. Instantiates the module and calls process_request. + :return: none + """ + module = AnsibleModule( + argument_spec=ClcPublicIp._define_module_argument_spec(), + supports_check_mode=True + ) + clc_public_ip = ClcPublicIp(module) + clc_public_ip.process_request() + +from ansible.module_utils.basic import * # pylint: disable=W0614 +if __name__ == '__main__': + main() diff --git a/cloud/centurylink/clc_server.py b/cloud/centurylink/clc_server.py new file mode 100644 index 00000000000..e102cd21f47 --- /dev/null +++ b/cloud/centurylink/clc_server.py @@ -0,0 +1,1323 @@ +#!/usr/bin/python + +# CenturyLink Cloud Ansible Modules. +# +# These Ansible modules enable the CenturyLink Cloud v2 API to be called +# from an within Ansible Playbook. +# +# This file is part of CenturyLink Cloud, and is maintained +# by the Workflow as a Service Team +# +# Copyright 2015 CenturyLink Cloud +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# CenturyLink Cloud: http://www.CenturyLinkCloud.com +# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ +# + +DOCUMENTATION = ''' +module: clc_server +short_desciption: Create, Delete, Start and Stop servers in CenturyLink Cloud. +description: + - An Ansible module to Create, Delete, Start and Stop servers in CenturyLink Cloud. +options: + additional_disks: + description: + - Specify additional disks for the server + required: False + default: None + aliases: [] + add_public_ip: + description: + - Whether to add a public ip to the server + required: False + default: False + choices: [False, True] + aliases: [] + alias: + description: + - The account alias to provision the servers under. + default: + - The default alias for the API credentials + required: False + default: None + aliases: [] + anti_affinity_policy_id: + description: + - The anti-affinity policy to assign to the server. This is mutually exclusive with 'anti_affinity_policy_name'. + required: False + default: None + aliases: [] + anti_affinity_policy_name: + description: + - The anti-affinity policy to assign to the server. This is mutually exclusive with 'anti_affinity_policy_id'. + required: False + default: None + aliases: [] + alert_policy_id: + description: + - The alert policy to assign to the server. This is mutually exclusive with 'alert_policy_name'. + required: False + default: None + aliases: [] + alert_policy_name: + description: + - The alert policy to assign to the server. This is mutually exclusive with 'alert_policy_id'. + required: False + default: None + aliases: [] + + count: + description: + - The number of servers to build (mutually exclusive with exact_count) + default: None + aliases: [] + count_group: + description: + - Required when exact_count is specified. The Server Group use to determine how many severs to deploy. + default: 1 + required: False + aliases: [] + cpu: + description: + - How many CPUs to provision on the server + default: None + required: False + aliases: [] + cpu_autoscale_policy_id: + description: + - The autoscale policy to assign to the server. + default: None + required: False + aliases: [] + custom_fields: + description: + - A dictionary of custom fields to set on the server. + default: [] + required: False + aliases: [] + description: + description: + - The description to set for the server. + default: None + required: False + aliases: [] + exact_count: + description: + - Run in idempotent mode. Will insure that this exact number of servers are running in the provided group, creating and deleting them to reach that count. Requires count_group to be set. + default: None + required: False + aliases: [] + group: + description: + - The Server Group to create servers under. + default: 'Default Group' + required: False + aliases: [] + ip_address: + description: + - The IP Address for the server. One is assigned if not provided. + default: None + required: False + aliases: [] + location: + description: + - The Datacenter to create servers in. + default: None + required: False + aliases: [] + managed_os: + description: + - Whether to create the server as 'Managed' or not. + default: False + required: False + choices: [True, False] + aliases: [] + memory: + description: + - Memory in GB. + default: 1 + required: False + aliases: [] + name: + description: + - A 1 to 6 character identifier to use for the server. + default: None + required: False + aliases: [] + network_id: + description: + - The network UUID on which to create servers. + default: None + required: False + aliases: [] + packages: + description: + - Blueprints to run on the server after its created. + default: [] + required: False + aliases: [] + password: + description: + - Password for the administrator user + default: None + required: False + aliases: [] + primary_dns: + description: + - Primary DNS used by the server. + default: None + required: False + aliases: [] + public_ip_protocol: + description: + - The protocol to use for the public ip if add_public_ip is set to True. + default: 'TCP' + required: False + aliases: [] + public_ip_ports: + description: + - A list of ports to allow on the firewall to thes servers public ip, if add_public_ip is set to True. + default: [] + required: False + aliases: [] + secondary_dns: + description: + - Secondary DNS used by the server. + default: None + required: False + aliases: [] + server_ids: + description: + - Required for started, stopped, and absent states. A list of server Ids to insure are started, stopped, or absent. + default: [] + required: False + aliases: [] + source_server_password: + description: + - The password for the source server if a clone is specified. + default: None + required: False + aliases: [] + state: + description: + - The state to insure that the provided resources are in. + default: 'present' + required: False + choices: ['present', 'absent', 'started', 'stopped'] + aliases: [] + storage_type: + description: + - The type of storage to attach to the server. + default: 'standard' + required: False + choices: ['standard', 'hyperscale'] + aliases: [] + template: + description: + - The template to use for server creation. Will search for a template if a partial string is provided. + default: None + required: false + aliases: [] + ttl: + description: + - The time to live for the server in seconds. The server will be deleted when this time expires. + default: None + required: False + aliases: [] + type: + description: + - The type of server to create. + default: 'standard' + required: False + choices: ['standard', 'hyperscale'] + aliases: [] + wait: + description: + - Whether to wait for the provisioning tasks to finish before returning. + default: True + required: False + choices: [ True, False] + aliases: [] +''' + +EXAMPLES = ''' +# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples + +- name: Provision a single Ubuntu Server + clc_server: + name: test + template: ubuntu-14-64 + count: 1 + group: 'Default Group' + state: present + +- name: Ensure 'Default Group' has exactly 5 servers + clc_server: + name: test + template: ubuntu-14-64 + exact_count: 5 + count_group: 'Default Group' + group: 'Default Group' + +- name: Stop a Server + clc_server: + server_ids: ['UC1ACCTTEST01'] + state: stopped + +- name: Start a Server + clc_server: + server_ids: ['UC1ACCTTEST01'] + state: started + +- name: Delete a Server + clc_server: + server_ids: ['UC1ACCTTEST01'] + state: absent +''' + +__version__ = '${version}' + +import requests +from time import sleep + +# +# Requires the clc-python-sdk. +# sudo pip install clc-sdk +# +try: + import clc as clc_sdk + from clc import CLCException + from clc import APIFailedResponse +except ImportError: + CLC_FOUND = False + clc_sdk = None +else: + CLC_FOUND = True + + +class ClcServer(): + clc = clc_sdk + + def __init__(self, module): + """ + Construct module + """ + self.clc = clc_sdk + self.module = module + self.group_dict = {} + + if not CLC_FOUND: + self.module.fail_json( + msg='clc-python-sdk required for this module') + + self._set_user_agent(self.clc) + + def process_request(self): + """ + Process the request - Main Code Path + :return: Returns with either an exit_json or fail_json + """ + self._set_clc_credentials_from_env() + + self.module.params = ClcServer._validate_module_params(self.clc, + self.module) + p = self.module.params + state = p.get('state') + + # + # Handle each state + # + + if state == 'absent': + server_ids = p['server_ids'] + if not isinstance(server_ids, list): + self.module.fail_json( + msg='server_ids needs to be a list of instances to delete: %s' % + server_ids) + + (changed, + server_dict_array, + new_server_ids) = ClcServer._delete_servers(module=self.module, + clc=self.clc, + server_ids=server_ids) + + elif state in ('started', 'stopped'): + server_ids = p.get('server_ids') + if not isinstance(server_ids, list): + self.module.fail_json( + msg='server_ids needs to be a list of servers to run: %s' % + server_ids) + + (changed, + server_dict_array, + new_server_ids) = ClcServer._startstop_servers(self.module, + self.clc, + server_ids) + + elif state == 'present': + # Changed is always set to true when provisioning new instances + if not p.get('template'): + self.module.fail_json( + msg='template parameter is required for new instance') + + if p.get('exact_count') is None: + (server_dict_array, + new_server_ids, + changed) = ClcServer._create_servers(self.module, + self.clc) + else: + (server_dict_array, + new_server_ids, + changed) = ClcServer._enforce_count(self.module, + self.clc) + + self.module.exit_json( + changed=changed, + server_ids=new_server_ids, + servers=server_dict_array) + + @staticmethod + def _define_module_argument_spec(): + """ + Define the argument spec for the ansible module + :return: argument spec dictionary + """ + argument_spec = dict(name=dict(), + template=dict(), + group=dict(default='Default Group'), + network_id=dict(), + location=dict(default=None), + cpu=dict(default=1), + memory=dict(default='1'), + alias=dict(default=None), + password=dict(default=None), + ip_address=dict(default=None), + storage_type=dict(default='standard'), + type=dict( + default='standard', + choices=[ + 'standard', + 'hyperscale']), + primary_dns=dict(default=None), + secondary_dns=dict(default=None), + additional_disks=dict(type='list', default=[]), + custom_fields=dict(type='list', default=[]), + ttl=dict(default=None), + managed_os=dict(type='bool', default=False), + description=dict(default=None), + source_server_password=dict(default=None), + cpu_autoscale_policy_id=dict(default=None), + anti_affinity_policy_id=dict(default=None), + anti_affinity_policy_name=dict(default=None), + alert_policy_id=dict(default=None), + alert_policy_name=dict(default=None), + packages=dict(type='list', default=[]), + state=dict( + default='present', + choices=[ + 'present', + 'absent', + 'started', + 'stopped']), + count=dict(type='int', default='1'), + exact_count=dict(type='int', default=None), + count_group=dict(), + server_ids=dict(type='list'), + add_public_ip=dict(type='bool', default=False), + public_ip_protocol=dict(default='TCP'), + public_ip_ports=dict(type='list'), + wait=dict(type='bool', default=True)) + + mutually_exclusive = [ + ['exact_count', 'count'], + ['exact_count', 'state'], + ['anti_affinity_policy_id', 'anti_affinity_policy_name'], + ['alert_policy_id', 'alert_policy_name'], + ] + return {"argument_spec": argument_spec, + "mutually_exclusive": mutually_exclusive} + + def _set_clc_credentials_from_env(self): + """ + Set the CLC Credentials on the sdk by reading environment variables + :return: none + """ + env = os.environ + v2_api_token = env.get('CLC_V2_API_TOKEN', False) + v2_api_username = env.get('CLC_V2_API_USERNAME', False) + v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) + clc_alias = env.get('CLC_ACCT_ALIAS', False) + api_url = env.get('CLC_V2_API_URL', False) + + if api_url: + self.clc.defaults.ENDPOINT_URL_V2 = api_url + + if v2_api_token and clc_alias: + self.clc._LOGIN_TOKEN_V2 = v2_api_token + self.clc._V2_ENABLED = True + self.clc.ALIAS = clc_alias + elif v2_api_username and v2_api_passwd: + self.clc.v2.SetCredentials( + api_username=v2_api_username, + api_passwd=v2_api_passwd) + else: + return self.module.fail_json( + msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " + "environment variables") + + @staticmethod + def _validate_module_params(clc, module): + """ + Validate the module params, and lookup default values. + :param clc: clc-sdk instance to use + :param module: module to validate + :return: dictionary of validated params + """ + params = module.params + datacenter = ClcServer._find_datacenter(clc, module) + + ClcServer._validate_types(module) + ClcServer._validate_name(module) + + params['alias'] = ClcServer._find_alias(clc, module) + params['cpu'] = ClcServer._find_cpu(clc, module) + params['memory'] = ClcServer._find_memory(clc, module) + params['description'] = ClcServer._find_description(module) + params['ttl'] = ClcServer._find_ttl(clc, module) + params['template'] = ClcServer._find_template_id(module, datacenter) + params['group'] = ClcServer._find_group(module, datacenter).id + params['network_id'] = ClcServer._find_network_id(module, datacenter) + + return params + + @staticmethod + def _find_datacenter(clc, module): + """ + Find the datacenter by calling the CLC API. + :param clc: clc-sdk instance to use + :param module: module to validate + :return: clc-sdk.Datacenter instance + """ + location = module.params.get('location') + try: + datacenter = clc.v2.Datacenter(location) + return datacenter + except CLCException: + module.fail_json(msg=str("Unable to find location: " + location)) + + @staticmethod + def _find_alias(clc, module): + """ + Find or Validate the Account Alias by calling the CLC API + :param clc: clc-sdk instance to use + :param module: module to validate + :return: clc-sdk.Account instance + """ + alias = module.params.get('alias') + if not alias: + alias = clc.v2.Account.GetAlias() + return alias + + @staticmethod + def _find_cpu(clc, module): + """ + Find or validate the CPU value by calling the CLC API + :param clc: clc-sdk instance to use + :param module: module to validate + :return: Int value for CPU + """ + cpu = module.params.get('cpu') + group_id = module.params.get('group_id') + alias = module.params.get('alias') + state = module.params.get('state') + + if not cpu and state == 'present': + group = clc.v2.Group(id=group_id, + alias=alias) + if group.Defaults("cpu"): + cpu = group.Defaults("cpu") + else: + module.fail_json( + msg=str("Cannot determine a default cpu value. Please provide a value for cpu.")) + return cpu + + @staticmethod + def _find_memory(clc, module): + """ + Find or validate the Memory value by calling the CLC API + :param clc: clc-sdk instance to use + :param module: module to validate + :return: Int value for Memory + """ + memory = module.params.get('memory') + group_id = module.params.get('group_id') + alias = module.params.get('alias') + state = module.params.get('state') + + if not memory and state == 'present': + group = clc.v2.Group(id=group_id, + alias=alias) + if group.Defaults("memory"): + memory = group.Defaults("memory") + else: + module.fail_json(msg=str( + "Cannot determine a default memory value. Please provide a value for memory.")) + return memory + + @staticmethod + def _find_description(module): + """ + Set the description module param to name if description is blank + :param module: the module to validate + :return: string description + """ + description = module.params.get('description') + if not description: + description = module.params.get('name') + return description + + @staticmethod + def _validate_types(module): + """ + Validate that type and storage_type are set appropriately, and fail if not + :param module: the module to validate + :return: none + """ + state = module.params.get('state') + type = module.params.get( + 'type').lower() if module.params.get('type') else None + storage_type = module.params.get( + 'storage_type').lower() if module.params.get('storage_type') else None + + if state == "present": + if type == "standard" and storage_type not in ( + "standard", "premium"): + module.fail_json( + msg=str("Standard VMs must have storage_type = 'standard' or 'premium'")) + + if type == "hyperscale" and storage_type != "hyperscale": + module.fail_json( + msg=str("Hyperscale VMs must have storage_type = 'hyperscale'")) + + @staticmethod + def _find_ttl(clc, module): + """ + Validate that TTL is > 3600 if set, and fail if not + :param clc: clc-sdk instance to use + :param module: module to validate + :return: validated ttl + """ + ttl = module.params.get('ttl') + + if ttl: + if ttl <= 3600: + module.fail_json(msg=str("Ttl cannot be <= 3600")) + else: + ttl = clc.v2.time_utils.SecondsToZuluTS(int(time.time()) + ttl) + return ttl + + @staticmethod + def _find_template_id(module, datacenter): + """ + Find the template id by calling the CLC API. + :param module: the module to validate + :param datacenter: the datacenter to search for the template + :return: a valid clc template id + """ + lookup_template = module.params.get('template') + state = module.params.get('state') + result = None + + if state == 'present': + try: + result = datacenter.Templates().Search(lookup_template)[0].id + except CLCException: + module.fail_json( + msg=str( + "Unable to find a template: " + + lookup_template + + " in location: " + + datacenter.id)) + return result + + @staticmethod + def _find_network_id(module, datacenter): + """ + Validate the provided network id or return a default. + :param module: the module to validate + :param datacenter: the datacenter to search for a network id + :return: a valid network id + """ + network_id = module.params.get('network_id') + + if not network_id: + try: + network_id = datacenter.Networks().networks[0].id + except CLCException: + module.fail_json( + msg=str( + "Unable to find a network in location: " + + datacenter.id)) + + return network_id + + @staticmethod + def _create_servers(module, clc, override_count=None): + """ + Create New Servers + :param module: the AnsibleModule object + :param clc: the clc-sdk instance to use + :return: a list of dictionaries with server information about the servers that were created + """ + p = module.params + requests = [] + servers = [] + server_dict_array = [] + created_server_ids = [] + + add_public_ip = p.get('add_public_ip') + public_ip_protocol = p.get('public_ip_protocol') + public_ip_ports = p.get('public_ip_ports') + wait = p.get('wait') + + params = { + 'name': p.get('name'), + 'template': p.get('template'), + 'group_id': p.get('group'), + 'network_id': p.get('network_id'), + 'cpu': p.get('cpu'), + 'memory': p.get('memory'), + 'alias': p.get('alias'), + 'password': p.get('password'), + 'ip_address': p.get('ip_address'), + 'storage_type': p.get('storage_type'), + 'type': p.get('type'), + 'primary_dns': p.get('primary_dns'), + 'secondary_dns': p.get('secondary_dns'), + 'additional_disks': p.get('additional_disks'), + 'custom_fields': p.get('custom_fields'), + 'ttl': p.get('ttl'), + 'managed_os': p.get('managed_os'), + 'description': p.get('description'), + 'source_server_password': p.get('source_server_password'), + 'cpu_autoscale_policy_id': p.get('cpu_autoscale_policy_id'), + 'anti_affinity_policy_id': p.get('anti_affinity_policy_id'), + 'anti_affinity_policy_name': p.get('anti_affinity_policy_name'), + 'packages': p.get('packages') + } + + count = override_count if override_count else p.get('count') + + changed = False if count == 0 else True + + if changed: + for i in range(0, count): + if not module.check_mode: + req = ClcServer._create_clc_server(clc=clc, + module=module, + server_params=params) + server = req.requests[0].Server() + requests.append(req) + servers.append(server) + + ClcServer._wait_for_requests(clc, requests, servers, wait) + + ClcServer._add_public_ip_to_servers( + should_add_public_ip=add_public_ip, + servers=servers, + public_ip_protocol=public_ip_protocol, + public_ip_ports=public_ip_ports, + wait=wait) + ClcServer._add_alert_policy_to_servers(clc=clc, + module=module, + servers=servers) + + for server in servers: + # reload server details + server = clc.v2.Server(server.id) + + server.data['ipaddress'] = server.details[ + 'ipAddresses'][0]['internal'] + + if add_public_ip and len(server.PublicIPs().public_ips) > 0: + server.data['publicip'] = str( + server.PublicIPs().public_ips[0]) + + server_dict_array.append(server.data) + created_server_ids.append(server.id) + + return server_dict_array, created_server_ids, changed + + @staticmethod + def _validate_name(module): + """ + Validate that name is the correct length if provided, fail if it's not + :param module: the module to validate + :return: none + """ + name = module.params.get('name') + state = module.params.get('state') + + if state == 'present' and (len(name) < 1 or len(name) > 6): + module.fail_json(msg=str( + "When state = 'present', name must be a string with a minimum length of 1 and a maximum length of 6")) + +# +# Functions to execute the module's behaviors +# (called from main()) +# + + @staticmethod + def _enforce_count(module, clc): + """ + Enforce that there is the right number of servers in the provided group. + Starts or stops servers as necessary. + :param module: the AnsibleModule object + :param clc: the clc-sdk instance to use + :return: a list of dictionaries with server information about the servers that were created or deleted + """ + p = module.params + changed_server_ids = None + changed = False + count_group = p.get('count_group') + datacenter = ClcServer._find_datacenter(clc, module) + exact_count = p.get('exact_count') + server_dict_array = [] + + # fail here if the exact count was specified without filtering + # on a group, as this may lead to a undesired removal of instances + if exact_count and count_group is None: + module.fail_json( + msg="you must use the 'count_group' option with exact_count") + + servers, running_servers = ClcServer._find_running_servers_by_group( + module, datacenter, count_group) + + if len(running_servers) == exact_count: + changed = False + + elif len(running_servers) < exact_count: + changed = True + to_create = exact_count - len(running_servers) + server_dict_array, changed_server_ids, changed \ + = ClcServer._create_servers(module, clc, override_count=to_create) + + for server in server_dict_array: + running_servers.append(server) + + elif len(running_servers) > exact_count: + changed = True + to_remove = len(running_servers) - exact_count + all_server_ids = sorted([x.id for x in running_servers]) + remove_ids = all_server_ids[0:to_remove] + + (changed, server_dict_array, changed_server_ids) \ + = ClcServer._delete_servers(module, clc, remove_ids) + + return server_dict_array, changed_server_ids, changed + + @staticmethod + def _wait_for_requests(clc, requests, servers, wait): + """ + Block until server provisioning requests are completed. + :param clc: the clc-sdk instance to use + :param requests: a list of clc-sdk.Request instances + :param servers: a list of servers to refresh + :param wait: a boolean on whether to block or not. This function is skipped if True + :return: none + """ + if wait: + # Requests.WaitUntilComplete() returns the count of failed requests + failed_requests_count = sum( + [request.WaitUntilComplete() for request in requests]) + + if failed_requests_count > 0: + raise clc + else: + ClcServer._refresh_servers(servers) + + @staticmethod + def _refresh_servers(servers): + """ + Loop through a list of servers and refresh them + :param servers: list of clc-sdk.Server instances to refresh + :return: none + """ + for server in servers: + server.Refresh() + + @staticmethod + def _add_public_ip_to_servers( + should_add_public_ip, + servers, + public_ip_protocol, + public_ip_ports, + wait): + """ + Create a public IP for servers + :param should_add_public_ip: boolean - whether or not to provision a public ip for servers. Skipped if False + :param servers: List of servers to add public ips to + :param public_ip_protocol: a protocol to allow for the public ips + :param public_ip_ports: list of ports to allow for the public ips + :param wait: boolean - whether to block until the provisioning requests complete + :return: none + """ + + if should_add_public_ip: + ports_lst = [] + requests = [] + + for port in public_ip_ports: + ports_lst.append( + {'protocol': public_ip_protocol, 'port': port}) + + for server in servers: + requests.append(server.PublicIPs().Add(ports_lst)) + + if wait: + for r in requests: + r.WaitUntilComplete() + + @staticmethod + def _add_alert_policy_to_servers(clc, module, servers): + """ + Associate an alert policy to servers + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param servers: List of servers to add alert policy to + :return: none + """ + p = module.params + alert_policy_id = p.get('alert_policy_id') + alert_policy_name = p.get('alert_policy_name') + alias = p.get('alias') + if not alert_policy_id and alert_policy_name: + alert_policy_id = ClcServer._get_alert_policy_id_by_name( + clc=clc, + module=module, + alias=alias, + alert_policy_name=alert_policy_name + ) + if not alert_policy_id: + module.fail_json( + msg='No alert policy exist with name : %s' + % (alert_policy_name)) + for server in servers: + ClcServer._add_alert_policy_to_server( + clc=clc, + module=module, + alias=alias, + server_id=server.id, + alert_policy_id=alert_policy_id) + + @staticmethod + def _add_alert_policy_to_server(clc, module, alias, server_id, alert_policy_id): + """ + Associate an alert policy to a clc server + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param alias: the clc account alias + :param serverid: The clc server id + :param alert_policy_id: the alert policy id to be associated to the server + :return: none + """ + try: + clc.v2.API.Call( + method='POST', + url='servers/%s/%s/alertPolicies' % (alias, server_id), + payload=json.dumps( + { + 'id': alert_policy_id + })) + except clc.APIFailedResponse as e: + return module.fail_json( + msg='Failed to associate alert policy to the server : %s with Error %s' + % (server_id, str(e.response_text))) + + @staticmethod + def _get_alert_policy_id_by_name(clc, module, alias, alert_policy_name): + """ + Returns the alert policy id for the given alert policy name + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param alias: the clc account alias + :param alert_policy_name: the name of the alert policy + :return: the alert policy id + """ + alert_policy_id = None + policies = clc.v2.API.Call('GET', '/v2/alertPolicies/%s' % (alias)) + if not policies: + return alert_policy_id + for policy in policies.get('items'): + if policy.get('name') == alert_policy_name: + if not alert_policy_id: + alert_policy_id = policy.get('id') + else: + return module.fail_json( + msg='mutiple alert policies were found with policy name : %s' % + (alert_policy_name)) + return alert_policy_id + + + @staticmethod + def _delete_servers(module, clc, server_ids): + """ + Delete the servers on the provided list + :param module: the AnsibleModule object + :param clc: the clc-sdk instance to use + :param server_ids: list of servers to delete + :return: a list of dictionaries with server information about the servers that were deleted + """ + # Whether to wait for termination to complete before returning + p = module.params + wait = p.get('wait') + terminated_server_ids = [] + server_dict_array = [] + requests = [] + + changed = False + if not isinstance(server_ids, list) or len(server_ids) < 1: + module.fail_json( + msg='server_ids should be a list of servers, aborting') + + servers = clc.v2.Servers(server_ids).Servers() + changed = True + + for server in servers: + if not module.check_mode: + requests.append(server.Delete()) + + if wait: + for r in requests: + r.WaitUntilComplete() + + for server in servers: + terminated_server_ids.append(server.id) + + return changed, server_dict_array, terminated_server_ids + + @staticmethod + def _startstop_servers(module, clc, server_ids): + """ + Start or Stop the servers on the provided list + :param module: the AnsibleModule object + :param clc: the clc-sdk instance to use + :param server_ids: list of servers to start or stop + :return: a list of dictionaries with server information about the servers that were started or stopped + """ + p = module.params + wait = p.get('wait') + state = p.get('state') + changed = False + changed_servers = [] + server_dict_array = [] + result_server_ids = [] + requests = [] + + if not isinstance(server_ids, list) or len(server_ids) < 1: + module.fail_json( + msg='server_ids should be a list of servers, aborting') + + servers = clc.v2.Servers(server_ids).Servers() + for server in servers: + if server.powerState != state: + changed_servers.append(server) + if not module.check_mode: + requests.append( + ClcServer._change_server_power_state( + module, + server, + state)) + changed = True + + if wait: + for r in requests: + r.WaitUntilComplete() + for server in changed_servers: + server.Refresh() + + for server in changed_servers: + server_dict_array.append(server.data) + result_server_ids.append(server.id) + + return changed, server_dict_array, result_server_ids + + @staticmethod + def _change_server_power_state(module, server, state): + """ + Change the server powerState + :param module: the module to check for intended state + :param server: the server to start or stop + :param state: the intended powerState for the server + :return: the request object from clc-sdk call + """ + result = None + try: + if state == 'started': + result = server.PowerOn() + else: + result = server.PowerOff() + except: + module.fail_json( + msg='Unable to change state for server {0}'.format( + server.id)) + return result + return result + + @staticmethod + def _find_running_servers_by_group(module, datacenter, count_group): + """ + Find a list of running servers in the provided group + :param module: the AnsibleModule object + :param datacenter: the clc-sdk.Datacenter instance to use to lookup the group + :param count_group: the group to count the servers + :return: list of servers, and list of running servers + """ + group = ClcServer._find_group( + module=module, + datacenter=datacenter, + lookup_group=count_group) + + servers = group.Servers().Servers() + running_servers = [] + + for server in servers: + if server.status == 'active' and server.powerState == 'started': + running_servers.append(server) + + return servers, running_servers + + @staticmethod + def _find_group(module, datacenter, lookup_group=None): + """ + Find a server group in a datacenter by calling the CLC API + :param module: the AnsibleModule instance + :param datacenter: clc-sdk.Datacenter instance to search for the group + :param lookup_group: string name of the group to search for + :return: clc-sdk.Group instance + """ + result = None + if not lookup_group: + lookup_group = module.params.get('group') + try: + return datacenter.Groups().Get(lookup_group) + except: + pass + + # The search above only acts on the main + result = ClcServer._find_group_recursive( + module, + datacenter.Groups(), + lookup_group) + + if result is None: + module.fail_json( + msg=str( + "Unable to find group: " + + lookup_group + + " in location: " + + datacenter.id)) + + return result + + @staticmethod + def _find_group_recursive(module, group_list, lookup_group): + """ + Find a server group by recursively walking the tree + :param module: the AnsibleModule instance to use + :param group_list: a list of groups to search + :param lookup_group: the group to look for + :return: list of groups + """ + result = None + for group in group_list.groups: + subgroups = group.Subgroups() + try: + return subgroups.Get(lookup_group) + except: + result = ClcServer._find_group_recursive( + module, + subgroups, + lookup_group) + + if result is not None: + break + + return result + + @staticmethod + def _create_clc_server( + clc, + module, + server_params): + """ + Call the CLC Rest API to Create a Server + :param clc: the clc-python-sdk instance to use + :param server_params: a dictionary of params to use to create the servers + :return: clc-sdk.Request object linked to the queued server request + """ + + aa_policy_id = server_params.get('anti_affinity_policy_id') + aa_policy_name = server_params.get('anti_affinity_policy_name') + if not aa_policy_id and aa_policy_name: + aa_policy_id = ClcServer._get_anti_affinity_policy_id( + clc, + module, + server_params.get('alias'), + aa_policy_name) + + res = clc.v2.API.Call( + method='POST', + url='servers/%s' % + (server_params.get('alias')), + payload=json.dumps( + { + 'name': server_params.get('name'), + 'description': server_params.get('description'), + 'groupId': server_params.get('group_id'), + 'sourceServerId': server_params.get('template'), + 'isManagedOS': server_params.get('managed_os'), + 'primaryDNS': server_params.get('primary_dns'), + 'secondaryDNS': server_params.get('secondary_dns'), + 'networkId': server_params.get('network_id'), + 'ipAddress': server_params.get('ip_address'), + 'password': server_params.get('password'), + 'sourceServerPassword': server_params.get('source_server_password'), + 'cpu': server_params.get('cpu'), + 'cpuAutoscalePolicyId': server_params.get('cpu_autoscale_policy_id'), + 'memoryGB': server_params.get('memory'), + 'type': server_params.get('type'), + 'storageType': server_params.get('storage_type'), + 'antiAffinityPolicyId': aa_policy_id, + 'customFields': server_params.get('custom_fields'), + 'additionalDisks': server_params.get('additional_disks'), + 'ttl': server_params.get('ttl'), + 'packages': server_params.get('packages')})) + + result = clc.v2.Requests(res) + + # + # Patch the Request object so that it returns a valid server + + # Find the server's UUID from the API response + server_uuid = [obj['id'] + for obj in res['links'] if obj['rel'] == 'self'][0] + + # Change the request server method to a _find_server_by_uuid closure so + # that it will work + result.requests[0].Server = lambda: ClcServer._find_server_by_uuid_w_retry( + clc, + module, + server_uuid, + server_params.get('alias')) + + return result + + @staticmethod + def _get_anti_affinity_policy_id(clc, module, alias, aa_policy_name): + """ + retrieves the anti affinity policy id of the server based on the name of the policy + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param alias: the CLC account alias + :param aa_policy_name: the anti affinity policy name + :return: aa_policy_id: The anti affinity policy id + """ + aa_policy_id = None + aa_policies = clc.v2.API.Call(method='GET', + url='antiAffinityPolicies/%s' % (alias)) + for aa_policy in aa_policies.get('items'): + if aa_policy.get('name') == aa_policy_name: + if not aa_policy_id: + aa_policy_id = aa_policy.get('id') + else: + return module.fail_json( + msg='mutiple anti affinity policies were found with policy name : %s' % + (aa_policy_name)) + if not aa_policy_id: + return module.fail_json( + msg='No anti affinity policy was found with policy name : %s' % + (aa_policy_name)) + return aa_policy_id + + # + # This is the function that gets patched to the Request.server object using a lamda closure + # + + @staticmethod + def _find_server_by_uuid_w_retry( + clc, module, svr_uuid, alias=None, retries=5, backout=2): + """ + Find the clc server by the UUID returned from the provisioning request. Retry the request if a 404 is returned. + :param clc: the clc-sdk instance to use + :param svr_uuid: UUID of the server + :param alias: the Account Alias to search + :return: a clc-sdk.Server instance + """ + if not alias: + alias = clc.v2.Account.GetAlias() + + # Wait and retry if the api returns a 404 + while True: + retries -= 1 + try: + server_obj = clc.v2.API.Call( + method='GET', url='servers/%s/%s?uuid=true' % + (alias, svr_uuid)) + server_id = server_obj['id'] + server = clc.v2.Server( + id=server_id, + alias=alias, + server_obj=server_obj) + return server + + except APIFailedResponse as e: + if e.response_status_code != 404: + module.fail_json( + msg='A failure response was received from CLC API when ' + 'attempting to get details for a server: UUID=%s, Code=%i, Message=%s' % + (svr_uuid, e.response_status_code, e.message)) + return + if retries == 0: + module.fail_json( + msg='Unable to reach the CLC API after 5 attempts') + return + + sleep(backout) + backout = backout * 2 + + @staticmethod + def _set_user_agent(clc): + if hasattr(clc, 'SetRequestsSession'): + agent_string = "ClcAnsibleModule/" + __version__ + ses = requests.Session() + ses.headers.update({"Api-Client": agent_string}) + ses.headers['User-Agent'] += " " + agent_string + clc.SetRequestsSession(ses) + + +def main(): + """ + The main function. Instantiates the module and calls process_request. + :return: none + """ + argument_dict = ClcServer._define_module_argument_spec() + module = AnsibleModule(supports_check_mode=True, **argument_dict) + clc_server = ClcServer(module) + clc_server.process_request() + +from ansible.module_utils.basic import * # pylint: disable=W0614 +if __name__ == '__main__': + main() diff --git a/cloud/centurylink/clc_server_snapshot.py b/cloud/centurylink/clc_server_snapshot.py new file mode 100644 index 00000000000..9ca1474f248 --- /dev/null +++ b/cloud/centurylink/clc_server_snapshot.py @@ -0,0 +1,341 @@ +#!/usr/bin/python + +# CenturyLink Cloud Ansible Modules. +# +# These Ansible modules enable the CenturyLink Cloud v2 API to be called +# from an within Ansible Playbook. +# +# This file is part of CenturyLink Cloud, and is maintained +# by the Workflow as a Service Team +# +# Copyright 2015 CenturyLink Cloud +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# CenturyLink Cloud: http://www.CenturyLinkCloud.com +# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ +# + +DOCUMENTATION = ''' +module: clc_server +short_desciption: Create, Delete and Restore server snapshots in CenturyLink Cloud. +description: + - An Ansible module to Create, Delete and Restore server snapshots in CenturyLink Cloud. +options: + server_ids: + description: + - A list of server Ids to snapshot. + default: [] + required: True + aliases: [] + expiration_days: + description: + - The number of days to keep the server snapshot before it expires. + default: 7 + required: False + aliases: [] + state: + description: + - The state to insure that the provided resources are in. + default: 'present' + required: False + choices: ['present', 'absent', 'restore'] + aliases: [] + wait: + description: + - Whether to wait for the provisioning tasks to finish before returning. + default: True + required: False + choices: [ True, False] + aliases: [] +''' + +EXAMPLES = ''' +# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples + +- name: Create server snapshot + clc_server_snapshot: + server_ids: + - UC1WFSDTEST01 + - UC1WFSDTEST02 + expiration_days: 10 + wait: True + state: present + +- name: Restore server snapshot + clc_server_snapshot: + server_ids: + - UC1WFSDTEST01 + - UC1WFSDTEST02 + wait: True + state: restore + +- name: Delete server snapshot + clc_server_snapshot: + server_ids: + - UC1WFSDTEST01 + - UC1WFSDTEST02 + wait: True + state: absent +''' + +__version__ = '${version}' + +import requests + +# +# Requires the clc-python-sdk. +# sudo pip install clc-sdk +# +try: + import clc as clc_sdk + from clc import CLCException +except ImportError: + clc_found = False + clc_sdk = None +else: + CLC_FOUND = True + + +class ClcSnapshot(): + + clc = clc_sdk + module = None + + def __init__(self, module): + """ + Construct module + """ + self.module = module + if not CLC_FOUND: + self.module.fail_json( + msg='clc-python-sdk required for this module') + + self._set_user_agent(self.clc) + + def process_request(self): + """ + Process the request - Main Code Path + :return: Returns with either an exit_json or fail_json + """ + p = self.module.params + + if not CLC_FOUND: + self.module.fail_json( + msg='clc-python-sdk required for this module') + + server_ids = p['server_ids'] + expiration_days = p['expiration_days'] + state = p['state'] + + if not server_ids: + return self.module.fail_json(msg='List of Server ids are required') + + self._set_clc_credentials_from_env() + if state == 'present': + changed, requests, changed_servers = self.ensure_server_snapshot_present(server_ids=server_ids, + expiration_days=expiration_days) + elif state == 'absent': + changed, requests, changed_servers = self.ensure_server_snapshot_absent( + server_ids=server_ids) + elif state == 'restore': + changed, requests, changed_servers = self.ensure_server_snapshot_restore( + server_ids=server_ids) + else: + return self.module.fail_json(msg="Unknown State: " + state) + + self._wait_for_requests_to_complete(requests) + return self.module.exit_json( + changed=changed, + server_ids=changed_servers) + + def ensure_server_snapshot_present(self, server_ids, expiration_days): + """ + Ensures the given set of server_ids have the snapshots created + :param server_ids: The list of server_ids to create the snapshot + :param expiration_days: The number of days to keep the snapshot + :return: (changed, result, changed_servers) + changed: A flag indicating whether any change was made + result: the list of clc request objects from CLC API call + changed_servers: The list of servers ids that are modified + """ + result = [] + changed = False + servers = self._get_servers_from_clc( + server_ids, + 'Failed to obtain server list from the CLC API') + servers_to_change = [ + server for server in servers if len( + server.GetSnapshots()) == 0] + for server in servers_to_change: + changed = True + if not self.module.check_mode: + res = server.CreateSnapshot( + delete_existing=True, + expiration_days=expiration_days) + result.append(res) + changed_servers = [ + server.id for server in servers_to_change if server.id] + return changed, result, changed_servers + + def ensure_server_snapshot_absent(self, server_ids): + """ + Ensures the given set of server_ids have the snapshots removed + :param server_ids: The list of server_ids to delete the snapshot + :return: (changed, result, changed_servers) + changed: A flag indicating whether any change was made + result: the list of clc request objects from CLC API call + changed_servers: The list of servers ids that are modified + """ + result = [] + changed = False + servers = self._get_servers_from_clc( + server_ids, + 'Failed to obtain server list from the CLC API') + servers_to_change = [ + server for server in servers if len( + server.GetSnapshots()) > 0] + for server in servers_to_change: + changed = True + if not self.module.check_mode: + res = server.DeleteSnapshot() + result.append(res) + changed_servers = [ + server.id for server in servers_to_change if server.id] + return changed, result, changed_servers + + def ensure_server_snapshot_restore(self, server_ids): + """ + Ensures the given set of server_ids have the snapshots restored + :param server_ids: The list of server_ids to delete the snapshot + :return: (changed, result, changed_servers) + changed: A flag indicating whether any change was made + result: the list of clc request objects from CLC API call + changed_servers: The list of servers ids that are modified + """ + result = [] + changed = False + servers = self._get_servers_from_clc( + server_ids, + 'Failed to obtain server list from the CLC API') + servers_to_change = [ + server for server in servers if len( + server.GetSnapshots()) > 0] + for server in servers_to_change: + changed = True + if not self.module.check_mode: + res = server.RestoreSnapshot() + result.append(res) + changed_servers = [ + server.id for server in servers_to_change if server.id] + return changed, result, changed_servers + + def _wait_for_requests_to_complete(self, requests_lst): + """ + Waits until the CLC requests are complete if the wait argument is True + :param requests_lst: The list of CLC request objects + :return: none + """ + if not self.module.params['wait']: + return + for request in requests_lst: + request.WaitUntilComplete() + for request_details in request.requests: + if request_details.Status() != 'succeeded': + self.module.fail_json( + msg='Unable to process server snapshot request') + + @staticmethod + def define_argument_spec(): + """ + This function defnines the dictionary object required for + package module + :return: the package dictionary object + """ + argument_spec = dict( + server_ids=dict(type='list', required=True), + expiration_days=dict(default=7), + wait=dict(default=True), + state=dict( + default='present', + choices=[ + 'present', + 'absent', + 'restore']), + ) + return argument_spec + + def _get_servers_from_clc(self, server_list, message): + """ + Internal function to fetch list of CLC server objects from a list of server ids + :param the list server ids + :return the list of CLC server objects + """ + try: + return self.clc.v2.Servers(server_list).servers + except CLCException as ex: + return self.module.fail_json(msg=message + ': %s' % ex) + + def _set_clc_credentials_from_env(self): + """ + Set the CLC Credentials on the sdk by reading environment variables + :return: none + """ + env = os.environ + v2_api_token = env.get('CLC_V2_API_TOKEN', False) + v2_api_username = env.get('CLC_V2_API_USERNAME', False) + v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) + clc_alias = env.get('CLC_ACCT_ALIAS', False) + api_url = env.get('CLC_V2_API_URL', False) + + if api_url: + self.clc.defaults.ENDPOINT_URL_V2 = api_url + + if v2_api_token and clc_alias: + self.clc._LOGIN_TOKEN_V2 = v2_api_token + self.clc._V2_ENABLED = True + self.clc.ALIAS = clc_alias + elif v2_api_username and v2_api_passwd: + self.clc.v2.SetCredentials( + api_username=v2_api_username, + api_passwd=v2_api_passwd) + else: + return self.module.fail_json( + msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " + "environment variables") + + @staticmethod + def _set_user_agent(clc): + if hasattr(clc, 'SetRequestsSession'): + agent_string = "ClcAnsibleModule/" + __version__ + ses = requests.Session() + ses.headers.update({"Api-Client": agent_string}) + ses.headers['User-Agent'] += " " + agent_string + clc.SetRequestsSession(ses) + + +def main(): + """ + Main function + :return: None + """ + module = AnsibleModule( + argument_spec=ClcSnapshot.define_argument_spec(), + supports_check_mode=True + ) + clc_snapshot = ClcSnapshot(module) + clc_snapshot.process_request() + +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From 13a3e38a1124f1e3e74a5e33706bc7615a44f73b Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Wed, 8 Jul 2015 13:13:12 -0400 Subject: [PATCH 0391/2522] make token no_log in slack plugin --- notification/slack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notification/slack.py b/notification/slack.py index baabe4f58d2..ba4ed2e4c2d 100644 --- a/notification/slack.py +++ b/notification/slack.py @@ -177,7 +177,7 @@ def main(): module = AnsibleModule( argument_spec = dict( domain = dict(type='str', required=False, default=None), - token = dict(type='str', required=True), + token = dict(type='str', required=True, no_log=True), msg = dict(type='str', required=True), channel = dict(type='str', default=None), username = dict(type='str', default='Ansible'), From 72b9ef4830ff82ab0d35858ab33cb547982e94e3 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Wed, 8 Jul 2015 13:14:01 -0400 Subject: [PATCH 0392/2522] added missing version_added to new filesystem option --- system/filesystem.py | 1 + 1 file changed, 1 insertion(+) diff --git a/system/filesystem.py b/system/filesystem.py index b1a75fc065a..b44168a0e06 100644 --- a/system/filesystem.py +++ b/system/filesystem.py @@ -47,6 +47,7 @@ description: - If yes, if the block device and filessytem size differ, grow the filesystem into the space. Note, XFS Will only grow if mounted. required: false + version_added: "2.0" opts: description: - List of options to be passed to mkfs command. From 647cac63f1b4c64ba8042aec32b39e56ec5389c2 Mon Sep 17 00:00:00 2001 From: Mark Hamilton Date: Wed, 8 Jul 2015 20:45:19 -0700 Subject: [PATCH 0393/2522] Add support for external_id and fail_mode. Updated syntax to pass pep8 v1.6.2 and pylint v0.25.0 --- network/openvswitch_bridge.py | 152 +++++++++++++++++++++++++++++----- 1 file changed, 129 insertions(+), 23 deletions(-) diff --git a/network/openvswitch_bridge.py b/network/openvswitch_bridge.py index b9ddff562c6..e26f5fea904 100644 --- a/network/openvswitch_bridge.py +++ b/network/openvswitch_bridge.py @@ -17,6 +17,10 @@ # # You should have received a copy of the GNU General Public License # along with this software. If not, see . +# +# Portions copyright @ 2015 VMware, Inc. All rights reserved. + +# pylint: disable=C0111 DOCUMENTATION = ''' --- @@ -43,62 +47,98 @@ default: 5 description: - How long to wait for ovs-vswitchd to respond + external_id: + required: false + description: + - bridge external-id + fail_mode: + required: false + choices : [secure, standalone] + description: + - bridge fail-mode ''' EXAMPLES = ''' # Create a bridge named br-int - openvswitch_bridge: bridge=br-int state=present + +# Create an integration bridge +- openvswitch_bridge: bridge=br-int state=present external_id=br-int + fail_mode=secure ''' +import syslog +import os + class OVSBridge(object): + """ Interface to ovs-vsctl. """ def __init__(self, module): self.module = module self.bridge = module.params['bridge'] self.state = module.params['state'] self.timeout = module.params['timeout'] + self.external_id = module.params['external_id'] + self.fail_mode = module.params['fail_mode'] def _vsctl(self, command): '''Run ovs-vsctl command''' - return self.module.run_command(['ovs-vsctl', '-t', str(self.timeout)] + command) + return self.module.run_command(['ovs-vsctl', '-t', + str(self.timeout)] + command) def exists(self): '''Check if the bridge already exists''' - rc, _, err = self._vsctl(['br-exists', self.bridge]) - if rc == 0: # See ovs-vsctl(8) for status codes + rtc, _, err = self._vsctl(['br-exists', self.bridge]) + if rtc == 0: # See ovs-vsctl(8) for status codes return True - if rc == 2: + if rtc == 2: return False - raise Exception(err) + self.module.fail_json(msg=err) def add(self): '''Create the bridge''' - rc, _, err = self._vsctl(['add-br', self.bridge]) - if rc != 0: - raise Exception(err) + rtc, _, err = self._vsctl(['add-br', self.bridge]) + if rtc != 0: + self.module.fail_json(msg=err) + if self.external_id: + self.set_external_id() + if self.fail_mode: + self.set_fail_mode() def delete(self): '''Delete the bridge''' - rc, _, err = self._vsctl(['del-br', self.bridge]) - if rc != 0: - raise Exception(err) + rtc, _, err = self._vsctl(['del-br', self.bridge]) + if rtc != 0: + self.module.fail_json(msg=err) def check(self): '''Run check mode''' + changed = False + + # pylint: disable=W0703 try: - if self.state == 'absent' and self.exists(): + if self.state == 'present' and self.exists(): + if (self.external_id and + (self.external_id != self.get_external_id())): + changed = True + if (self.fail_mode and + (self.fail_mode != self.get_fail_mode())): + changed = True + elif self.state == 'absent' and self.exists(): changed = True elif self.state == 'present' and not self.exists(): changed = True - else: - changed = False - except Exception, e: - self.module.fail_json(msg=str(e)) + except Exception, earg: + self.module.fail_json(msg=str(earg)) + + # pylint: enable=W0703 self.module.exit_json(changed=changed) def run(self): '''Make the necessary changes''' changed = False + # pylint: disable=W0703 + try: if self.state == 'absent': if self.exists(): @@ -108,27 +148,93 @@ def run(self): if not self.exists(): self.add() changed = True - except Exception, e: - self.module.fail_json(msg=str(e)) - self.module.exit_json(changed=changed) + if (self.external_id and + (self.external_id != self.get_external_id())): + self.set_external_id() + changed = True + + current_fail_mode = self.get_fail_mode() + if self.fail_mode and (self.fail_mode != current_fail_mode): + syslog.syslog(syslog.LOG_NOTICE, + "changing fail mode %s to %s" % + (current_fail_mode, self.fail_mode)) + self.set_fail_mode() + changed = True + except Exception, earg: + self.module.fail_json(msg=str(earg)) + # pylint: enable=W0703 + self.module.exit_json(changed=changed) + + def get_external_id(self): + """ Return the current external id. """ + value = '' + if self.exists(): + rtc, out, err = self._vsctl(['br-get-external-id', self.bridge]) + if rtc != 0: + self.module.fail_json(msg=err) + try: + (_, value) = out.split('=') + except ValueError: + pass + return value.strip("\n") + + def set_external_id(self): + """ Set external id. """ + if self.exists(): + (rtc, _, err) = self._vsctl(['br-set-external-id', self.bridge, + 'bridge-id', self.external_id]) + if rtc != 0: + self.module.fail_json(msg=err) + + def get_fail_mode(self): + """ Get failure mode. """ + value = '' + if self.exists(): + rtc, out, err = self._vsctl(['get-fail-mode', self.bridge]) + if rtc != 0: + self.module.fail_json(msg=err) + value = out.strip("\n") + return value + + def set_fail_mode(self): + """ Set failure mode. """ + + if self.exists(): + (rtc, _, err) = self._vsctl(['set-fail-mode', self.bridge, + self.fail_mode]) + if rtc != 0: + self.module.fail_json(msg=err) + + +# pylint: disable=E0602 def main(): + """ Entry point. """ module = AnsibleModule( argument_spec={ 'bridge': {'required': True}, 'state': {'default': 'present', 'choices': ['present', 'absent']}, - 'timeout': {'default': 5, 'type': 'int'} + 'timeout': {'default': 5, 'type': 'int'}, + 'external_id': {'default': ''}, + 'fail_mode': {'default': ''}, + 'syslogging': {'required': False, 'type': 'bool', 'default': True} }, supports_check_mode=True, ) - br = OVSBridge(module) + if (module.params["syslogging"]): + syslog.openlog('ansible-%s' % os.path.basename(__file__)) + + bridge = OVSBridge(module) if module.check_mode: - br.check() + bridge.check() else: - br.run() + bridge.run() +# pylint: disable=W0614 +# pylint: disable=W0401 +# pylint: disable=W0622 # import module snippets from ansible.module_utils.basic import * From 325ef12aeedc71d9e9da66aebbb66caaab64f9bf Mon Sep 17 00:00:00 2001 From: Mark Hamilton Date: Thu, 9 Jul 2015 00:36:30 -0700 Subject: [PATCH 0394/2522] Added support to assign attached mac address interface id and port options. Updated code to pass pep8 v1.6.2 pylint v0.25.0. --- network/openvswitch_port.py | 212 +++++++++++++++++++++++++++++++++--- 1 file changed, 196 insertions(+), 16 deletions(-) diff --git a/network/openvswitch_port.py b/network/openvswitch_port.py index 6f59f4b134b..bb3924161b7 100644 --- a/network/openvswitch_port.py +++ b/network/openvswitch_port.py @@ -1,6 +1,8 @@ #!/usr/bin/python #coding: utf-8 -*- +# pylint: disable=C0111 + # (c) 2013, David Stygstra # # This file is part of Ansible @@ -17,6 +19,11 @@ # # You should have received a copy of the GNU General Public License # along with this software. If not, see . +# +# Portions copyright @ 2015 VMware, Inc. All rights reserved. + +import syslog +import os DOCUMENTATION = ''' --- @@ -47,44 +54,188 @@ default: 5 description: - How long to wait for ovs-vswitchd to respond + iface-id: + required: false + description: + - Used when port is created to set the external_ids:iface-id. + attached-mac: + required: false + description: + - MAC address when port is created using external_ids:attached-mac. + set: + required: false + description: + - Additional set options to apply to the port ''' EXAMPLES = ''' # Creates port eth2 on bridge br-ex - openvswitch_port: bridge=br-ex port=eth2 state=present + +# Creates port eth6 and set ofport equal to 6. +- openvswitch_port: bridge=bridge-loop port=eth6 state=present + set Interface eth6 ofport_request=6 + +# Assign interface id server1-vifeth6 and mac address 52:54:00:30:6d:11 +# to port vifeth6 +- openvswitch_port: bridge=br-int port=vifeth6 state=present + args: + external_ids: + iface-id: "server1-vifeth6" + attached-mac: "52:54:00:30:6d:11" + ''' +# pylint: disable=W0703 + + +def truncate_before(value, srch): + """ Return content of str before the srch parameters. """ + + before_index = value.find(srch) + if (before_index >= 0): + return value[:before_index] + else: + return value + +def _set_to_get(set_cmd): + """ Convert set command to get command and set value. + return tuple (get command, set value) + """ + + ## + # If set has option: then we want to truncate just before that. + set_cmd = truncate_before(set_cmd, " option:") + get_cmd = set_cmd.split(" ") + (key, value) = get_cmd[-1].split("=") + syslog.syslog(syslog.LOG_NOTICE, "get commands %s " % key) + return (["--", "get"] + get_cmd[:-1] + [key], value) + + +# pylint: disable=R0902 class OVSPort(object): + """ Interface to OVS port. """ def __init__(self, module): self.module = module self.bridge = module.params['bridge'] self.port = module.params['port'] self.state = module.params['state'] self.timeout = module.params['timeout'] + self.set_opt = module.params.get('set', None) + + # if the port name starts with "vif", let's assume it's a VIF + self.is_vif = self.port.startswith('vif') + + ## + # Several places need host name. + rtc, out, err = self.module.run_command(["hostname", "-s"]) + if (rtc): + self.module.fail_json(msg=err) + self.hostname = out.strip("\n") + + def _attached_mac_get(self): + """ Return the interface. """ + attached_mac = self.module.params['external_ids'].get('attached-mac', + None) + if (not attached_mac): + attached_mac = "00:50:50:F" + attached_mac += self.hostname[-3] + ":" + attached_mac += self.hostname[-2:] + ":00" + return attached_mac + + def _iface_id_get(self): + """ Return the interface id. """ + + iface_id = self.module.params['external_ids'].get('iface-id', None) + if (not iface_id): + iface_id = "%s-%s" % (self.hostname, self.port) + return iface_id def _vsctl(self, command): '''Run ovs-vsctl command''' - return self.module.run_command(['ovs-vsctl', '-t', str(self.timeout)] + command) + + cmd = ['ovs-vsctl', '-t', str(self.timeout)] + command + syslog.syslog(syslog.LOG_NOTICE, " ".join(cmd)) + return self.module.run_command(cmd) def exists(self): '''Check if the port already exists''' - rc, out, err = self._vsctl(['list-ports', self.bridge]) - if rc != 0: - raise Exception(err) + + (rtc, out, err) = self._vsctl(['list-ports', self.bridge]) + + if rtc != 0: + self.module.fail_json(msg=err) + return any(port.rstrip() == self.port for port in out.split('\n')) + def set(self, set_opt): + """ Set attributes on a port. """ + + syslog.syslog(syslog.LOG_NOTICE, "set called %s" % set_opt) + if (not set_opt): + return False + + (get_cmd, set_value) = _set_to_get(set_opt) + (rtc, out, err) = self._vsctl(get_cmd) + if rtc != 0: + self.module.fail_json(msg=err) + + out = out.strip("\n") + out = out.strip('"') + if (out == set_value): + return False + + (rtc, out, err) = self._vsctl(["--", "set"] + set_opt.split(" ")) + if rtc != 0: + self.module.fail_json(msg=err) + self.module.exit_json(changed=True) + syslog.syslog(syslog.LOG_NOTICE, "-- set %s" % set_opt) + + def set_vif_attributes(self): + ''' Set attributes for a vif ''' + + ## + # create a fake MAC address for the VIF + fmac = self._attached_mac_get() + + ## + # If vif_uuid is missing then construct a new one. + iface_id = self._iface_id_get() + syslog.syslog(syslog.LOG_NOTICE, "iface-id %s" % iface_id) + + attached_mac = "external_ids:attached-mac=%s" % fmac + iface_id = "external_ids:iface-id=%s" % iface_id + vm_id = "external_ids:vm-id=%s" % self.hostname + cmd = ["set", "Interface", self.port, + "external_ids:iface-status=active", iface_id, vm_id, + attached_mac] + + (rtc, _, stderr) = self._vsctl(cmd) + if rtc != 0: + self.module.fail_json(msg="%s returned %s %s" % (" ".join(cmd), + rtc, stderr)) + def add(self): '''Add the port''' - rc, _, err = self._vsctl(['add-port', self.bridge, self.port]) - if rc != 0: - raise Exception(err) + cmd = ['add-port', self.bridge, self.port] + if self.set and self.set_opt: + cmd += ["--", "set"] + cmd += self.set_opt.split(" ") + + (rtc, _, err) = self._vsctl(cmd) + if rtc != 0: + self.module.fail_json(msg=err) + syslog.syslog(syslog.LOG_NOTICE, " ".join(cmd)) + + if self.is_vif: + self.set_vif_attributes() def delete(self): '''Remove the port''' - rc, _, err = self._vsctl(['del-port', self.bridge, self.port]) - if rc != 0: - raise Exception(err) + (rtc, _, err) = self._vsctl(['del-port', self.bridge, self.port]) + if rtc != 0: + self.module.fail_json(msg=err) def check(self): '''Run check mode''' @@ -95,8 +246,8 @@ def check(self): changed = True else: changed = False - except Exception, e: - self.module.fail_json(msg=str(e)) + except Exception, earg: + self.module.fail_json(msg=str(earg)) self.module.exit_json(changed=changed) def run(self): @@ -108,25 +259,50 @@ def run(self): self.delete() changed = True elif self.state == 'present': - if not self.exists(): + ## + # Add any missing ports. + if (not self.exists()): self.add() changed = True - except Exception, e: - self.module.fail_json(msg=str(e)) + + ## + # If the -- set changed check here and make changes + # but this only makes sense when state=present. + if (not changed): + changed = self.set(self.set_opt) or changed + items = self.module.params['external_ids'].items() + for (key, value) in items: + value = value.replace('"', '') + fmt_opt = "Interface %s external_ids:%s=%s" + external_id = fmt_opt % (self.port, key, value) + syslog.syslog(syslog.LOG_NOTICE, + "external %s" % external_id) + changed = self.set(external_id) or changed + ## + except Exception, earg: + self.module.fail_json(msg=str(earg)) self.module.exit_json(changed=changed) +# pylint: disable=E0602 def main(): + """ Entry point. """ module = AnsibleModule( argument_spec={ 'bridge': {'required': True}, 'port': {'required': True}, 'state': {'default': 'present', 'choices': ['present', 'absent']}, - 'timeout': {'default': 5, 'type': 'int'} + 'timeout': {'default': 5, 'type': 'int'}, + 'set': {'required': False}, + 'external_ids': {'default': {}, 'required': False}, + 'syslogging': {'required': False, 'type': "bool", 'default': True} }, supports_check_mode=True, ) + if (module.params["syslogging"]): + syslog.openlog('ansible-%s' % os.path.basename(__file__)) + port = OVSPort(module) if module.check_mode: port.check() @@ -134,6 +310,10 @@ def main(): port.run() +# pylint: disable=W0614 +# pylint: disable=W0401 +# pylint: disable=W0622 + # import module snippets from ansible.module_utils.basic import * main() From 795425f32defdd83d9b1017ab4a124eb73451e73 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Thu, 9 Jul 2015 09:43:26 +0200 Subject: [PATCH 0395/2522] irc: remove version_added for nick option, should have been nick_to option --- notification/irc.py | 1 - 1 file changed, 1 deletion(-) diff --git a/notification/irc.py b/notification/irc.py index 70f198883c7..7e34049c639 100644 --- a/notification/irc.py +++ b/notification/irc.py @@ -42,7 +42,6 @@ - Nickname to send the message from. May be shortened, depending on server's NICKLEN setting. required: false default: ansible - version_added: "2.0" msg: description: - The message body. From 05d5d939e704cb258039dc25d8f88634ed5587e6 Mon Sep 17 00:00:00 2001 From: gfrank Date: Thu, 9 Jul 2015 09:37:51 -0400 Subject: [PATCH 0396/2522] Adding nssm requirement note --- windows/win_nssm.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/windows/win_nssm.py b/windows/win_nssm.py index 46c940ce151..cadb90c5d38 100644 --- a/windows/win_nssm.py +++ b/windows/win_nssm.py @@ -28,6 +28,8 @@ short_description: NSSM - the Non-Sucking Service Manager description: - nssm is a service helper which doesn't suck. See https://nssm.cc/ for more information. +requirements: + - "nssm >= 2.24.0 # (install via win_chocolatey) win_chocolatey: name=nssm" options: name: description: From 539fd2357bf15fa4a63cfc8183d972df752119bc Mon Sep 17 00:00:00 2001 From: Siva Popuri Date: Thu, 9 Jul 2015 09:42:28 -0500 Subject: [PATCH 0397/2522] Removed all of the clc-ansbile-modules and kept only clc_publicip as the first module to go --- cloud/centurylink/__init__.py | 2 +- cloud/centurylink/clc_aa_policy.py | 294 ----- cloud/centurylink/clc_alert_policy.py | 473 ------- cloud/centurylink/clc_blueprint_package.py | 263 ---- cloud/centurylink/clc_firewall_policy.py | 542 -------- cloud/centurylink/clc_group.py | 370 ------ cloud/centurylink/clc_loadbalancer.py | 759 ----------- cloud/centurylink/clc_modify_server.py | 710 ----------- cloud/centurylink/clc_server.py | 1323 -------------------- cloud/centurylink/clc_server_snapshot.py | 341 ----- 10 files changed, 1 insertion(+), 5076 deletions(-) delete mode 100644 cloud/centurylink/clc_aa_policy.py delete mode 100644 cloud/centurylink/clc_alert_policy.py delete mode 100644 cloud/centurylink/clc_blueprint_package.py delete mode 100644 cloud/centurylink/clc_firewall_policy.py delete mode 100644 cloud/centurylink/clc_group.py delete mode 100644 cloud/centurylink/clc_loadbalancer.py delete mode 100644 cloud/centurylink/clc_modify_server.py delete mode 100644 cloud/centurylink/clc_server.py delete mode 100644 cloud/centurylink/clc_server_snapshot.py diff --git a/cloud/centurylink/__init__.py b/cloud/centurylink/__init__.py index 71f0abcff9d..8b137891791 100644 --- a/cloud/centurylink/__init__.py +++ b/cloud/centurylink/__init__.py @@ -1 +1 @@ -__version__ = "${version}" + diff --git a/cloud/centurylink/clc_aa_policy.py b/cloud/centurylink/clc_aa_policy.py deleted file mode 100644 index 644f3817c4f..00000000000 --- a/cloud/centurylink/clc_aa_policy.py +++ /dev/null @@ -1,294 +0,0 @@ -#!/usr/bin/python - -# CenturyLink Cloud Ansible Modules. -# -# These Ansible modules enable the CenturyLink Cloud v2 API to be called -# from an within Ansible Playbook. -# -# This file is part of CenturyLink Cloud, and is maintained -# by the Workflow as a Service Team -# -# Copyright 2015 CenturyLink Cloud -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# CenturyLink Cloud: http://www.CenturyLinkCloud.com -# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ - -DOCUMENTATION = ''' -module: clc_aa_policy -short_descirption: Create or Delete Anti Affinity Policies at CenturyLink Cloud. -description: - - An Ansible module to Create or Delete Anti Affinity Policies at CenturyLink Cloud. -options: - name: - description: - - The name of the Anti Affinity Policy. - required: True - location: - description: - - Datacenter in which the policy lives/should live. - required: True - state: - description: - - Whether to create or delete the policy. - required: False - default: present - choices: ['present','absent'] - wait: - description: - - Whether to wait for the provisioning tasks to finish before returning. - default: True - required: False - choices: [ True, False] - aliases: [] -''' - -EXAMPLES = ''' -# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples - ---- -- name: Create AA Policy - hosts: localhost - gather_facts: False - connection: local - tasks: - - name: Create an Anti Affinity Policy - clc_aa_policy: - name: 'Hammer Time' - location: 'UK3' - state: present - register: policy - - - name: debug - debug: var=policy - ---- -- name: Delete AA Policy - hosts: localhost - gather_facts: False - connection: local - tasks: - - name: Delete an Anti Affinity Policy - clc_aa_policy: - name: 'Hammer Time' - location: 'UK3' - state: absent - register: policy - - - name: debug - debug: var=policy -''' - -__version__ = '${version}' - -import requests - -# -# Requires the clc-python-sdk. -# sudo pip install clc-sdk -# -try: - import clc as clc_sdk - from clc import CLCException -except ImportError: - clc_found = False - clc_sdk = None -else: - clc_found = True - - -class ClcAntiAffinityPolicy(): - - clc = clc_sdk - module = None - - def __init__(self, module): - """ - Construct module - """ - self.module = module - self.policy_dict = {} - - if not clc_found: - self.module.fail_json( - msg='clc-python-sdk required for this module') - - self._set_user_agent(self.clc) - - @staticmethod - def _define_module_argument_spec(): - """ - Define the argument spec for the ansible module - :return: argument spec dictionary - """ - argument_spec = dict( - name=dict(required=True), - location=dict(required=True), - alias=dict(default=None), - wait=dict(default=True), - state=dict(default='present', choices=['present', 'absent']), - ) - return argument_spec - - # Module Behavior Goodness - def process_request(self): - """ - Process the request - Main Code Path - :return: Returns with either an exit_json or fail_json - """ - p = self.module.params - - if not clc_found: - self.module.fail_json( - msg='clc-python-sdk required for this module') - - self._set_clc_credentials_from_env() - self.policy_dict = self._get_policies_for_datacenter(p) - - if p['state'] == "absent": - changed, policy = self._ensure_policy_is_absent(p) - else: - changed, policy = self._ensure_policy_is_present(p) - - if hasattr(policy, 'data'): - policy = policy.data - elif hasattr(policy, '__dict__'): - policy = policy.__dict__ - - self.module.exit_json(changed=changed, policy=policy) - - def _set_clc_credentials_from_env(self): - """ - Set the CLC Credentials on the sdk by reading environment variables - :return: none - """ - env = os.environ - v2_api_token = env.get('CLC_V2_API_TOKEN', False) - v2_api_username = env.get('CLC_V2_API_USERNAME', False) - v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) - clc_alias = env.get('CLC_ACCT_ALIAS', False) - api_url = env.get('CLC_V2_API_URL', False) - - if api_url: - self.clc.defaults.ENDPOINT_URL_V2 = api_url - - if v2_api_token and clc_alias: - self.clc._LOGIN_TOKEN_V2 = v2_api_token - self.clc._V2_ENABLED = True - self.clc.ALIAS = clc_alias - elif v2_api_username and v2_api_passwd: - self.clc.v2.SetCredentials( - api_username=v2_api_username, - api_passwd=v2_api_passwd) - else: - return self.module.fail_json( - msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " - "environment variables") - - def _get_policies_for_datacenter(self, p): - """ - Get the Policies for a datacenter by calling the CLC API. - :param p: datacenter to get policies from - :return: policies in the datacenter - """ - response = {} - - policies = self.clc.v2.AntiAffinity.GetAll(location=p['location']) - - for policy in policies: - response[policy.name] = policy - return response - - def _create_policy(self, p): - """ - Create an Anti Affinnity Policy using the CLC API. - :param p: datacenter to create policy in - :return: response dictionary from the CLC API. - """ - return self.clc.v2.AntiAffinity.Create( - name=p['name'], - location=p['location']) - - def _delete_policy(self, p): - """ - Delete an Anti Affinity Policy using the CLC API. - :param p: datacenter to delete a policy from - :return: none - """ - policy = self.policy_dict[p['name']] - policy.Delete() - - def _policy_exists(self, policy_name): - """ - Check to see if an Anti Affinity Policy exists - :param policy_name: name of the policy - :return: boolean of if the policy exists - """ - if policy_name in self.policy_dict: - return self.policy_dict.get(policy_name) - - return False - - def _ensure_policy_is_absent(self, p): - """ - Makes sure that a policy is absent - :param p: dictionary of policy name - :return: tuple of if a deletion occurred and the name of the policy that was deleted - """ - changed = False - if self._policy_exists(policy_name=p['name']): - changed = True - if not self.module.check_mode: - self._delete_policy(p) - return changed, None - - def _ensure_policy_is_present(self, p): - """ - Ensures that a policy is present - :param p: dictonary of a policy name - :return: tuple of if an addition occurred and the name of the policy that was added - """ - changed = False - policy = self._policy_exists(policy_name=p['name']) - if not policy: - changed = True - policy = None - if not self.module.check_mode: - policy = self._create_policy(p) - return changed, policy - - @staticmethod - def _set_user_agent(clc): - if hasattr(clc, 'SetRequestsSession'): - agent_string = "ClcAnsibleModule/" + __version__ - ses = requests.Session() - ses.headers.update({"Api-Client": agent_string}) - ses.headers['User-Agent'] += " " + agent_string - clc.SetRequestsSession(ses) - - -def main(): - """ - The main function. Instantiates the module and calls process_request. - :return: none - """ - module = AnsibleModule( - argument_spec=ClcAntiAffinityPolicy._define_module_argument_spec(), - supports_check_mode=True) - clc_aa_policy = ClcAntiAffinityPolicy(module) - clc_aa_policy.process_request() - -from ansible.module_utils.basic import * # pylint: disable=W0614 -if __name__ == '__main__': - main() diff --git a/cloud/centurylink/clc_alert_policy.py b/cloud/centurylink/clc_alert_policy.py deleted file mode 100644 index 75467967a85..00000000000 --- a/cloud/centurylink/clc_alert_policy.py +++ /dev/null @@ -1,473 +0,0 @@ -#!/usr/bin/python - -# CenturyLink Cloud Ansible Modules. -# -# These Ansible modules enable the CenturyLink Cloud v2 API to be called -# from an within Ansible Playbook. -# -# This file is part of CenturyLink Cloud, and is maintained -# by the Workflow as a Service Team -# -# Copyright 2015 CenturyLink Cloud -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# CenturyLink Cloud: http://www.CenturyLinkCloud.com -# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ - -DOCUMENTATION = ''' -module: clc_alert_policy -short_descirption: Create or Delete Alert Policies at CenturyLink Cloud. -description: - - An Ansible module to Create or Delete Alert Policies at CenturyLink Cloud. -options: - alias: - description: - - The alias of your CLC Account - required: True - name: - description: - - The name of the alert policy. This is mutually exclusive with id - default: None - aliases: [] - id: - description: - - The alert policy id. This is mutually exclusive with name - default: None - aliases: [] - alert_recipients: - description: - - A list of recipient email ids to notify the alert. - required: True - aliases: [] - metric: - description: - - The metric on which to measure the condition that will trigger the alert. - required: True - default: None - choices: ['cpu','memory','disk'] - aliases: [] - duration: - description: - - The length of time in minutes that the condition must exceed the threshold. - required: True - default: None - aliases: [] - threshold: - description: - - The threshold that will trigger the alert when the metric equals or exceeds it. - This number represents a percentage and must be a value between 5.0 - 95.0 that is a multiple of 5.0 - required: True - default: None - aliases: [] - state: - description: - - Whether to create or delete the policy. - required: False - default: present - choices: ['present','absent'] -''' - -EXAMPLES = ''' -# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples - ---- -- name: Create Alert Policy Example - hosts: localhost - gather_facts: False - connection: local - tasks: - - name: Create an Alert Policy for disk above 80% for 5 minutes - clc_alert_policy: - alias: wfad - name: 'alert for disk > 80%' - alert_recipients: - - test1@centurylink.com - - test2@centurylink.com - metric: 'disk' - duration: '00:05:00' - threshold: 80 - state: present - register: policy - - - name: debug - debug: var=policy - ---- -- name: Delete Alert Policy Example - hosts: localhost - gather_facts: False - connection: local - tasks: - - name: Delete an Alert Policy - clc_alert_policy: - alias: wfad - name: 'alert for disk > 80%' - state: absent - register: policy - - - name: debug - debug: var=policy -''' - -__version__ = '${version}' - -import requests - -# -# Requires the clc-python-sdk. -# sudo pip install clc-sdk -# -try: - import clc as clc_sdk - from clc import CLCException -except ImportError: - clc_found = False - clc_sdk = None -else: - clc_found = True - - -class ClcAlertPolicy(): - - clc = clc_sdk - module = None - - def __init__(self, module): - """ - Construct module - """ - self.module = module - self.policy_dict = {} - - if not clc_found: - self.module.fail_json( - msg='clc-python-sdk required for this module') - - self._set_user_agent(self.clc) - - @staticmethod - def _define_module_argument_spec(): - """ - Define the argument spec for the ansible module - :return: argument spec dictionary - """ - argument_spec = dict( - name=dict(default=None), - id=dict(default=None), - alias=dict(required=True, default=None), - alert_recipients=dict(type='list', required=False, default=None), - metric=dict(required=False, choices=['cpu', 'memory', 'disk'], default=None), - duration=dict(required=False, type='str', default=None), - threshold=dict(required=False, type='int', default=None), - state=dict(default='present', choices=['present', 'absent']) - ) - mutually_exclusive = [ - ['name', 'id'] - ] - return {'argument_spec': argument_spec, - 'mutually_exclusive': mutually_exclusive} - - # Module Behavior Goodness - def process_request(self): - """ - Process the request - Main Code Path - :return: Returns with either an exit_json or fail_json - """ - p = self.module.params - - if not clc_found: - self.module.fail_json( - msg='clc-python-sdk required for this module') - - self._set_clc_credentials_from_env() - self.policy_dict = self._get_alert_policies(p['alias']) - - if p['state'] == 'present': - changed, policy = self._ensure_alert_policy_is_present() - else: - changed, policy = self._ensure_alert_policy_is_absent() - - self.module.exit_json(changed=changed, policy=policy) - - def _set_clc_credentials_from_env(self): - """ - Set the CLC Credentials on the sdk by reading environment variables - :return: none - """ - env = os.environ - v2_api_token = env.get('CLC_V2_API_TOKEN', False) - v2_api_username = env.get('CLC_V2_API_USERNAME', False) - v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) - clc_alias = env.get('CLC_ACCT_ALIAS', False) - api_url = env.get('CLC_V2_API_URL', False) - - if api_url: - self.clc.defaults.ENDPOINT_URL_V2 = api_url - - if v2_api_token and clc_alias: - self.clc._LOGIN_TOKEN_V2 = v2_api_token - self.clc._V2_ENABLED = True - self.clc.ALIAS = clc_alias - elif v2_api_username and v2_api_passwd: - self.clc.v2.SetCredentials( - api_username=v2_api_username, - api_passwd=v2_api_passwd) - else: - return self.module.fail_json( - msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " - "environment variables") - - def _ensure_alert_policy_is_present(self): - """ - Ensures that the alert policy is present - :return: (changed, policy) - canged: A flag representing if anything is modified - policy: the created/updated alert policy - """ - changed = False - p = self.module.params - policy_name = p.get('name') - alias = p.get('alias') - if not policy_name: - self.module.fail_json(msg='Policy name is a required') - policy = self._alert_policy_exists(alias, policy_name) - if not policy: - changed = True - policy = None - if not self.module.check_mode: - policy = self._create_alert_policy() - else: - changed_u, policy = self._ensure_alert_policy_is_updated(policy) - if changed_u: - changed = True - return changed, policy - - def _ensure_alert_policy_is_absent(self): - """ - Ensures that the alert policy is absent - :return: (changed, None) - canged: A flag representing if anything is modified - """ - changed = False - p = self.module.params - alert_policy_id = p.get('id') - alert_policy_name = p.get('name') - alias = p.get('alias') - if not alert_policy_id and not alert_policy_name: - self.module.fail_json( - msg='Either alert policy id or policy name is required') - if not alert_policy_id and alert_policy_name: - alert_policy_id = self._get_alert_policy_id( - self.module, - alert_policy_name) - if alert_policy_id and alert_policy_id in self.policy_dict: - changed = True - if not self.module.check_mode: - self._delete_alert_policy(alias, alert_policy_id) - return changed, None - - def _ensure_alert_policy_is_updated(self, alert_policy): - """ - Ensures the aliert policy is updated if anything is changed in the alert policy configuration - :param alert_policy: the targetalert policy - :return: (changed, policy) - canged: A flag representing if anything is modified - policy: the updated the alert policy - """ - changed = False - p = self.module.params - alert_policy_id = alert_policy.get('id') - email_list = p.get('alert_recipients') - metric = p.get('metric') - duration = p.get('duration') - threshold = p.get('threshold') - policy = alert_policy - if (metric and metric != str(alert_policy.get('triggers')[0].get('metric'))) or \ - (duration and duration != str(alert_policy.get('triggers')[0].get('duration'))) or \ - (threshold and float(threshold) != float(alert_policy.get('triggers')[0].get('threshold'))): - changed = True - elif email_list: - t_email_list = list( - alert_policy.get('actions')[0].get('settings').get('recipients')) - if set(email_list) != set(t_email_list): - changed = True - if changed and not self.module.check_mode: - policy = self._update_alert_policy(alert_policy_id) - return changed, policy - - def _get_alert_policies(self, alias): - """ - Get the alert policies for account alias by calling the CLC API. - :param alias: the account alias - :return: the alert policies for the account alias - """ - response = {} - - policies = self.clc.v2.API.Call('GET', - '/v2/alertPolicies/%s' - % (alias)) - - for policy in policies.get('items'): - response[policy.get('id')] = policy - return response - - def _create_alert_policy(self): - """ - Create an alert Policy using the CLC API. - :return: response dictionary from the CLC API. - """ - p = self.module.params - alias = p['alias'] - email_list = p['alert_recipients'] - metric = p['metric'] - duration = p['duration'] - threshold = p['threshold'] - name = p['name'] - arguments = json.dumps( - { - 'name': name, - 'actions': [{ - 'action': 'email', - 'settings': { - 'recipients': email_list - } - }], - 'triggers': [{ - 'metric': metric, - 'duration': duration, - 'threshold': threshold - }] - } - ) - try: - result = self.clc.v2.API.Call( - 'POST', - '/v2/alertPolicies/%s' % - (alias), - arguments) - except self.clc.APIFailedResponse as e: - return self.module.fail_json( - msg='Unable to create alert policy. %s' % str( - e.response_text)) - return result - - def _update_alert_policy(self, alert_policy_id): - """ - Update alert policy using the CLC API. - :param alert_policy_id: The clc alert policy id - :return: response dictionary from the CLC API. - """ - p = self.module.params - alias = p['alias'] - email_list = p['alert_recipients'] - metric = p['metric'] - duration = p['duration'] - threshold = p['threshold'] - name = p['name'] - arguments = json.dumps( - { - 'name': name, - 'actions': [{ - 'action': 'email', - 'settings': { - 'recipients': email_list - } - }], - 'triggers': [{ - 'metric': metric, - 'duration': duration, - 'threshold': threshold - }] - } - ) - try: - result = self.clc.v2.API.Call( - 'PUT', '/v2/alertPolicies/%s/%s' % - (alias, alert_policy_id), arguments) - except self.clc.APIFailedResponse as e: - return self.module.fail_json( - msg='Unable to update alert policy. %s' % str( - e.response_text)) - return result - - def _delete_alert_policy(self, alias, policy_id): - """ - Delete an alert policy using the CLC API. - :param alias : the account alias - :param policy_id: the alert policy id - :return: response dictionary from the CLC API. - """ - try: - result = self.clc.v2.API.Call( - 'DELETE', '/v2/alertPolicies/%s/%s' % - (alias, policy_id), None) - except self.clc.APIFailedResponse as e: - return self.module.fail_json( - msg='Unable to delete alert policy. %s' % str( - e.response_text)) - return result - - def _alert_policy_exists(self, alias, policy_name): - """ - Check to see if an alert policy exists - :param policy_name: name of the alert policy - :return: boolean of if the policy exists - """ - result = False - for id in self.policy_dict: - if self.policy_dict.get(id).get('name') == policy_name: - result = self.policy_dict.get(id) - return result - - def _get_alert_policy_id(self, module, alert_policy_name): - """ - retrieves the alert policy id of the account based on the name of the policy - :param module: the AnsibleModule object - :param alert_policy_name: the alert policy name - :return: alert_policy_id: The alert policy id - """ - alert_policy_id = None - for id in self.policy_dict: - if self.policy_dict.get(id).get('name') == alert_policy_name: - if not alert_policy_id: - alert_policy_id = id - else: - return module.fail_json( - msg='mutiple alert policies were found with policy name : %s' % - (alert_policy_name)) - return alert_policy_id - - @staticmethod - def _set_user_agent(clc): - if hasattr(clc, 'SetRequestsSession'): - agent_string = "ClcAnsibleModule/" + __version__ - ses = requests.Session() - ses.headers.update({"Api-Client": agent_string}) - ses.headers['User-Agent'] += " " + agent_string - clc.SetRequestsSession(ses) - - -def main(): - """ - The main function. Instantiates the module and calls process_request. - :return: none - """ - argument_dict = ClcAlertPolicy._define_module_argument_spec() - module = AnsibleModule(supports_check_mode=True, **argument_dict) - clc_alert_policy = ClcAlertPolicy(module) - clc_alert_policy.process_request() - -from ansible.module_utils.basic import * # pylint: disable=W0614 -if __name__ == '__main__': - main() diff --git a/cloud/centurylink/clc_blueprint_package.py b/cloud/centurylink/clc_blueprint_package.py deleted file mode 100644 index 80cc18a24ca..00000000000 --- a/cloud/centurylink/clc_blueprint_package.py +++ /dev/null @@ -1,263 +0,0 @@ -#!/usr/bin/python - -# CenturyLink Cloud Ansible Modules. -# -# These Ansible modules enable the CenturyLink Cloud v2 API to be called -# from an within Ansible Playbook. -# -# This file is part of CenturyLink Cloud, and is maintained -# by the Workflow as a Service Team -# -# Copyright 2015 CenturyLink Cloud -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# CenturyLink Cloud: http://www.CenturyLinkCloud.com -# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ -# - -DOCUMENTATION = ''' -module: clc_blueprint_package -short_desciption: deploys a blue print package on a set of servers in CenturyLink Cloud. -description: - - An Ansible module to deploy blue print package on a set of servers in CenturyLink Cloud. -options: - server_ids: - description: - - A list of server Ids to deploy the blue print package. - default: [] - required: True - aliases: [] - package_id: - description: - - The package id of the blue print. - default: None - required: True - aliases: [] - package_params: - description: - - The dictionary of arguments required to deploy the blue print. - default: {} - required: False - aliases: [] -''' - -EXAMPLES = ''' -# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples - -- name: Deploy package - clc_blueprint_package: - server_ids: - - UC1WFSDANS01 - - UC1WFSDANS02 - package_id: 77abb844-579d-478d-3955-c69ab4a7ba1a - package_params: {} -''' - -__version__ = '${version}' - -import requests - -# -# Requires the clc-python-sdk. -# sudo pip install clc-sdk -# -try: - import clc as clc_sdk - from clc import CLCException -except ImportError: - CLC_FOUND = False - clc_sdk = None -else: - CLC_FOUND = True - - -class ClcBlueprintPackage(): - - clc = clc_sdk - module = None - - def __init__(self, module): - """ - Construct module - """ - self.module = module - if not CLC_FOUND: - self.module.fail_json( - msg='clc-python-sdk required for this module') - - self._set_user_agent(self.clc) - - def process_request(self): - """ - Process the request - Main Code Path - :return: Returns with either an exit_json or fail_json - """ - p = self.module.params - - if not CLC_FOUND: - self.module.fail_json( - msg='clc-python-sdk required for this module') - - self._set_clc_credentials_from_env() - - server_ids = p['server_ids'] - package_id = p['package_id'] - package_params = p['package_params'] - state = p['state'] - if state == 'present': - changed, changed_server_ids, requests = self.ensure_package_installed( - server_ids, package_id, package_params) - if not self.module.check_mode: - self._wait_for_requests_to_complete(requests) - self.module.exit_json(changed=changed, server_ids=changed_server_ids) - - @staticmethod - def define_argument_spec(): - """ - This function defnines the dictionary object required for - package module - :return: the package dictionary object - """ - argument_spec = dict( - server_ids=dict(type='list', required=True), - package_id=dict(required=True), - package_params=dict(type='dict', default={}), - wait=dict(default=True), - state=dict(default='present', choices=['present']) - ) - return argument_spec - - def ensure_package_installed(self, server_ids, package_id, package_params): - """ - Ensure the package is installed in the given list of servers - :param server_ids: the server list where the package needs to be installed - :param package_id: the package id - :param package_params: the package arguments - :return: (changed, server_ids) - changed: A flag indicating if a change was made - server_ids: The list of servers modfied - """ - changed = False - requests = [] - servers = self._get_servers_from_clc( - server_ids, - 'Failed to get servers from CLC') - try: - for server in servers: - request = self.clc_install_package( - server, - package_id, - package_params) - requests.append(request) - changed = True - except CLCException as ex: - self.module.fail_json( - msg='Failed while installing package : %s with Error : %s' % - (package_id, ex)) - return changed, server_ids, requests - - def clc_install_package(self, server, package_id, package_params): - """ - Read all servers from CLC and executes each package from package_list - :param server_list: The target list of servers where the packages needs to be installed - :param package_list: The list of packages to be installed - :return: (changed, server_ids) - changed: A flag indicating if a change was made - server_ids: The list of servers modfied - """ - result = None - if not self.module.check_mode: - result = server.ExecutePackage( - package_id=package_id, - parameters=package_params) - return result - - def _wait_for_requests_to_complete(self, requests_lst): - """ - Waits until the CLC requests are complete if the wait argument is True - :param requests_lst: The list of CLC request objects - :return: none - """ - if not self.module.params['wait']: - return - for request in requests_lst: - request.WaitUntilComplete() - for request_details in request.requests: - if request_details.Status() != 'succeeded': - self.module.fail_json( - msg='Unable to process package install request') - - def _get_servers_from_clc(self, server_list, message): - """ - Internal function to fetch list of CLC server objects from a list of server ids - :param the list server ids - :return the list of CLC server objects - """ - try: - return self.clc.v2.Servers(server_list).servers - except CLCException as ex: - self.module.fail_json(msg=message + ': %s' % ex) - - def _set_clc_credentials_from_env(self): - """ - Set the CLC Credentials on the sdk by reading environment variables - :return: none - """ - env = os.environ - v2_api_token = env.get('CLC_V2_API_TOKEN', False) - v2_api_username = env.get('CLC_V2_API_USERNAME', False) - v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) - clc_alias = env.get('CLC_ACCT_ALIAS', False) - api_url = env.get('CLC_V2_API_URL', False) - - if api_url: - self.clc.defaults.ENDPOINT_URL_V2 = api_url - - if v2_api_token and clc_alias: - self.clc._LOGIN_TOKEN_V2 = v2_api_token - self.clc._V2_ENABLED = True - self.clc.ALIAS = clc_alias - elif v2_api_username and v2_api_passwd: - self.clc.v2.SetCredentials( - api_username=v2_api_username, - api_passwd=v2_api_passwd) - else: - return self.module.fail_json( - msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " - "environment variables") - - @staticmethod - def _set_user_agent(clc): - if hasattr(clc, 'SetRequestsSession'): - agent_string = "ClcAnsibleModule/" + __version__ - ses = requests.Session() - ses.headers.update({"Api-Client": agent_string}) - ses.headers['User-Agent'] += " " + agent_string - clc.SetRequestsSession(ses) - -def main(): - """ - Main function - :return: None - """ - module = AnsibleModule( - argument_spec=ClcBlueprintPackage.define_argument_spec(), - supports_check_mode=True - ) - clc_blueprint_package = ClcBlueprintPackage(module) - clc_blueprint_package.process_request() - -from ansible.module_utils.basic import * -if __name__ == '__main__': - main() diff --git a/cloud/centurylink/clc_firewall_policy.py b/cloud/centurylink/clc_firewall_policy.py deleted file mode 100644 index 260c82bc885..00000000000 --- a/cloud/centurylink/clc_firewall_policy.py +++ /dev/null @@ -1,542 +0,0 @@ -#!/usr/bin/python - -# CenturyLink Cloud Ansible Modules. -# -# These Ansible modules enable the CenturyLink Cloud v2 API to be called -# from an within Ansible Playbook. -# -# This file is part of CenturyLink Cloud, and is maintained -# by the Workflow as a Service Team -# -# Copyright 2015 CenturyLink Cloud -# -# Licensed under the Apache License, Version 2.0 (the "License"); - -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# CenturyLink Cloud: http://www.CenturyLinkCloud.com -# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ -# - -DOCUMENTATION = ''' -module: clc_firewall_policy -short_desciption: Create/delete/update firewall policies -description: - - Create or delete or updated firewall polices on Centurylink Centurylink Cloud -options: - location: - description: - - Target datacenter for the firewall policy - default: None - required: True - aliases: [] - state: - description: - - Whether to create or delete the firewall policy - default: present - required: True - choices: ['present', 'absent'] - aliases: [] - source: - description: - - Source addresses for traffic on the originating firewall - default: None - required: For Creation - aliases: [] - destination: - description: - - Destination addresses for traffic on the terminating firewall - default: None - required: For Creation - aliases: [] - ports: - description: - - types of ports associated with the policy. TCP & UDP can take in single ports or port ranges. - default: None - required: False - choices: ['any', 'icmp', 'TCP/123', 'UDP/123', 'TCP/123-456', 'UDP/123-456'] - aliases: [] - firewall_policy_id: - description: - - Id of the firewall policy - default: None - required: False - aliases: [] - source_account_alias: - description: - - CLC alias for the source account - default: None - required: True - aliases: [] - destination_account_alias: - description: - - CLC alias for the destination account - default: None - required: False - aliases: [] - wait: - description: - - Whether to wait for the provisioning tasks to finish before returning. - default: True - required: False - choices: [ True, False ] - aliases: [] - enabled: - description: - - If the firewall policy is enabled or disabled - default: true - required: False - choices: [ true, false ] - aliases: [] - -''' - -EXAMPLES = ''' ---- -- name: Create Firewall Policy - hosts: localhost - gather_facts: False - connection: local - tasks: - - name: Create / Verify an Firewall Policy at CenturyLink Cloud - clc_firewall: - source_account_alias: WFAD - location: VA1 - state: present - source: 10.128.216.0/24 - destination: 10.128.216.0/24 - ports: Any - destination_account_alias: WFAD - ---- -- name: Delete Firewall Policy - hosts: localhost - gather_facts: False - connection: local - tasks: - - name: Delete an Firewall Policy at CenturyLink Cloud - clc_firewall: - source_account_alias: WFAD - location: VA1 - state: present - firewall_policy_id: c62105233d7a4231bd2e91b9c791eaae -''' - -__version__ = '${version}' - -import urlparse -from time import sleep -import requests - -try: - import clc as clc_sdk - from clc import CLCException -except ImportError: - CLC_FOUND = False - clc_sdk = None -else: - CLC_FOUND = True - - -class ClcFirewallPolicy(): - - clc = None - - def __init__(self, module): - """ - Construct module - """ - self.clc = clc_sdk - self.module = module - self.firewall_dict = {} - - if not CLC_FOUND: - self.module.fail_json( - msg='clc-python-sdk required for this module') - - self._set_user_agent(self.clc) - - @staticmethod - def _define_module_argument_spec(): - """ - Define the argument spec for the ansible module - :return: argument spec dictionary - """ - argument_spec = dict( - location=dict(required=True, defualt=None), - source_account_alias=dict(required=True, default=None), - destination_account_alias=dict(default=None), - firewall_policy_id=dict(default=None), - ports=dict(default=None, type='list'), - source=dict(defualt=None, type='list'), - destination=dict(defualt=None, type='list'), - wait=dict(default=True), - state=dict(default='present', choices=['present', 'absent']), - enabled=dict(defualt=None) - ) - return argument_spec - - def process_request(self): - """ - Execute the main code path, and handle the request - :return: none - """ - location = self.module.params.get('location') - source_account_alias = self.module.params.get('source_account_alias') - destination_account_alias = self.module.params.get( - 'destination_account_alias') - firewall_policy_id = self.module.params.get('firewall_policy_id') - ports = self.module.params.get('ports') - source = self.module.params.get('source') - destination = self.module.params.get('destination') - wait = self.module.params.get('wait') - state = self.module.params.get('state') - enabled = self.module.params.get('enabled') - - self.firewall_dict = { - 'location': location, - 'source_account_alias': source_account_alias, - 'destination_account_alias': destination_account_alias, - 'firewall_policy_id': firewall_policy_id, - 'ports': ports, - 'source': source, - 'destination': destination, - 'wait': wait, - 'state': state, - 'enabled': enabled} - - self._set_clc_credentials_from_env() - requests = [] - - if state == 'absent': - changed, firewall_policy_id, response = self._ensure_firewall_policy_is_absent( - source_account_alias, location, self.firewall_dict) - - elif state == 'present': - changed, firewall_policy_id, response = self._ensure_firewall_policy_is_present( - source_account_alias, location, self.firewall_dict) - else: - return self.module.fail_json(msg="Unknown State: " + state) - - return self.module.exit_json( - changed=changed, - firewall_policy_id=firewall_policy_id) - - @staticmethod - def _get_policy_id_from_response(response): - """ - Method to parse out the policy id from creation response - :param response: response from firewall creation control - :return: policy_id: firewall policy id from creation call - """ - url = response.get('links')[0]['href'] - path = urlparse.urlparse(url).path - path_list = os.path.split(path) - policy_id = path_list[-1] - return policy_id - - def _set_clc_credentials_from_env(self): - """ - Set the CLC Credentials on the sdk by reading environment variables - :return: none - """ - env = os.environ - v2_api_token = env.get('CLC_V2_API_TOKEN', False) - v2_api_username = env.get('CLC_V2_API_USERNAME', False) - v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) - clc_alias = env.get('CLC_ACCT_ALIAS', False) - api_url = env.get('CLC_V2_API_URL', False) - - if api_url: - self.clc.defaults.ENDPOINT_URL_V2 = api_url - - if v2_api_token and clc_alias: - self.clc._LOGIN_TOKEN_V2 = v2_api_token - self.clc._V2_ENABLED = True - self.clc.ALIAS = clc_alias - elif v2_api_username and v2_api_passwd: - self.clc.v2.SetCredentials( - api_username=v2_api_username, - api_passwd=v2_api_passwd) - else: - return self.module.fail_json( - msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " - "environment variables") - - def _ensure_firewall_policy_is_present( - self, - source_account_alias, - location, - firewall_dict): - """ - Ensures that a given firewall policy is present - :param source_account_alias: the source account alias for the firewall policy - :param location: datacenter of the firewall policy - :param firewall_dict: dictionary or request parameters for firewall policy creation - :return: (changed, firewall_policy, response) - changed: flag for if a change occurred - firewall_policy: policy that was changed - response: response from CLC API call - """ - changed = False - response = {} - firewall_policy_id = firewall_dict.get('firewall_policy_id') - - if firewall_policy_id is None: - if not self.module.check_mode: - response = self._create_firewall_policy( - source_account_alias, - location, - firewall_dict) - firewall_policy_id = self._get_policy_id_from_response( - response) - self._wait_for_requests_to_complete( - firewall_dict.get('wait'), - source_account_alias, - location, - firewall_policy_id) - changed = True - else: - get_before_response, success = self._get_firewall_policy( - source_account_alias, location, firewall_policy_id) - if not success: - return self.module.fail_json( - msg='Unable to find the firewall policy id : %s' % - firewall_policy_id) - changed = self._compare_get_request_with_dict( - get_before_response, - firewall_dict) - if not self.module.check_mode and changed: - response = self._update_firewall_policy( - source_account_alias, - location, - firewall_policy_id, - firewall_dict) - self._wait_for_requests_to_complete( - firewall_dict.get('wait'), - source_account_alias, - location, - firewall_policy_id) - return changed, firewall_policy_id, response - - def _ensure_firewall_policy_is_absent( - self, - source_account_alias, - location, - firewall_dict): - """ - Ensures that a given firewall policy is removed if present - :param source_account_alias: the source account alias for the firewall policy - :param location: datacenter of the firewall policy - :param firewall_dict: firewall policy to delete - :return: (changed, firewall_policy_id, response) - changed: flag for if a change occurred - firewall_policy_id: policy that was changed - response: response from CLC API call - """ - changed = False - response = [] - firewall_policy_id = firewall_dict.get('firewall_policy_id') - result, success = self._get_firewall_policy( - source_account_alias, location, firewall_policy_id) - if success: - if not self.module.check_mode: - response = self._delete_firewall_policy( - source_account_alias, - location, - firewall_policy_id) - changed = True - return changed, firewall_policy_id, response - - def _create_firewall_policy( - self, - source_account_alias, - location, - firewall_dict): - """ - Ensures that a given firewall policy is present - :param source_account_alias: the source account alias for the firewall policy - :param location: datacenter of the firewall policy - :param firewall_dict: dictionary or request parameters for firewall policy creation - :return: response from CLC API call - """ - payload = { - 'destinationAccount': firewall_dict.get('destination_account_alias'), - 'source': firewall_dict.get('source'), - 'destination': firewall_dict.get('destination'), - 'ports': firewall_dict.get('ports')} - try: - response = self.clc.v2.API.Call( - 'POST', '/v2-experimental/firewallPolicies/%s/%s' % - (source_account_alias, location), payload) - except self.clc.APIFailedResponse as e: - return self.module.fail_json( - msg="Unable to successfully create firewall policy. %s" % - str(e.response_text)) - return response - - def _delete_firewall_policy( - self, - source_account_alias, - location, - firewall_policy_id): - """ - Deletes a given firewall policy for an account alias in a datacenter - :param source_account_alias: the source account alias for the firewall policy - :param location: datacenter of the firewall policy - :param firewall_policy_id: firewall policy to delete - :return: response: response from CLC API call - """ - try: - response = self.clc.v2.API.Call( - 'DELETE', '/v2-experimental/firewallPolicies/%s/%s/%s' % - (source_account_alias, location, firewall_policy_id)) - except self.clc.APIFailedResponse as e: - return self.module.fail_json( - msg="Unable to successfully delete firewall policy. %s" % - str(e.response_text)) - return response - - def _update_firewall_policy( - self, - source_account_alias, - location, - firewall_policy_id, - firewall_dict): - """ - Updates a firewall policy for a given datacenter and account alias - :param source_account_alias: the source account alias for the firewall policy - :param location: datacenter of the firewall policy - :param firewall_policy_id: firewall policy to delete - :param firewall_dict: dictionary or request parameters for firewall policy creation - :return: response: response from CLC API call - """ - try: - response = self.clc.v2.API.Call( - 'PUT', - '/v2-experimental/firewallPolicies/%s/%s/%s' % - (source_account_alias, - location, - firewall_policy_id), - firewall_dict) - except self.clc.APIFailedResponse as e: - return self.module.fail_json( - msg="Unable to successfully update firewall policy. %s" % - str(e.response_text)) - return response - - @staticmethod - def _compare_get_request_with_dict(response, firewall_dict): - """ - Helper method to compare the json response for getting the firewall policy with the request parameters - :param response: response from the get method - :param firewall_dict: dictionary or request parameters for firewall policy creation - :return: changed: Boolean that returns true if there are differences between the response parameters and the playbook parameters - """ - - changed = False - - response_dest_account_alias = response.get('destinationAccount') - response_enabled = response.get('enabled') - response_source = response.get('source') - response_dest = response.get('destination') - response_ports = response.get('ports') - - request_dest_account_alias = firewall_dict.get( - 'destination_account_alias') - request_enabled = firewall_dict.get('enabled') - if request_enabled is None: - request_enabled = True - request_source = firewall_dict.get('source') - request_dest = firewall_dict.get('destination') - request_ports = firewall_dict.get('ports') - - if ( - response_dest_account_alias and str(response_dest_account_alias) != str(request_dest_account_alias)) or ( - response_enabled != request_enabled) or ( - response_source and response_source != request_source) or ( - response_dest and response_dest != request_dest) or ( - response_ports and response_ports != request_ports): - changed = True - return changed - - def _get_firewall_policy( - self, - source_account_alias, - location, - firewall_policy_id): - """ - Get back details for a particular firewall policy - :param source_account_alias: the source account alias for the firewall policy - :param location: datacenter of the firewall policy - :param firewall_policy_id: id of the firewall policy to get - :return: response from CLC API call - """ - response = [] - success = False - try: - response = self.clc.v2.API.Call( - 'GET', '/v2-experimental/firewallPolicies/%s/%s/%s' % - (source_account_alias, location, firewall_policy_id)) - success = True - except: - pass - return response, success - - def _wait_for_requests_to_complete( - self, - wait, - source_account_alias, - location, - firewall_policy_id): - """ - Waits until the CLC requests are complete if the wait argument is True - :param requests_lst: The list of CLC request objects - :return: none - """ - if wait: - response, success = self._get_firewall_policy( - source_account_alias, location, firewall_policy_id) - if response.get('status') == 'pending': - sleep(2) - self._wait_for_requests_to_complete( - wait, - source_account_alias, - location, - firewall_policy_id) - return None - - @staticmethod - def _set_user_agent(clc): - if hasattr(clc, 'SetRequestsSession'): - agent_string = "ClcAnsibleModule/" + __version__ - ses = requests.Session() - ses.headers.update({"Api-Client": agent_string}) - ses.headers['User-Agent'] += " " + agent_string - clc.SetRequestsSession(ses) - - -def main(): - """ - The main function. Instantiates the module and calls process_request. - :return: none - """ - module = AnsibleModule( - argument_spec=ClcFirewallPolicy._define_module_argument_spec(), - supports_check_mode=True) - - clc_firewall = ClcFirewallPolicy(module) - clc_firewall.process_request() - -from ansible.module_utils.basic import * # pylint: disable=W0614 -if __name__ == '__main__': - main() diff --git a/cloud/centurylink/clc_group.py b/cloud/centurylink/clc_group.py deleted file mode 100644 index a4fd976d429..00000000000 --- a/cloud/centurylink/clc_group.py +++ /dev/null @@ -1,370 +0,0 @@ -#!/usr/bin/python - -# CenturyLink Cloud Ansible Modules. -# -# These Ansible modules enable the CenturyLink Cloud v2 API to be called -# from an within Ansible Playbook. -# -# This file is part of CenturyLink Cloud, and is maintained -# by the Workflow as a Service Team -# -# Copyright 2015 CenturyLink Cloud -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# CenturyLink Cloud: http://www.CenturyLinkCloud.com -# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ -# - -DOCUMENTATION = ''' -module: clc_group -short_desciption: Create/delete Server Groups at Centurylink Cloud -description: - - Create or delete Server Groups at Centurylink Centurylink Cloud -options: - name: - description: - - The name of the Server Group - description: - description: - - A description of the Server Group - parent: - description: - - The parent group of the server group - location: - description: - - Datacenter to create the group in - state: - description: - - Whether to create or delete the group - default: present - choices: ['present', 'absent'] - -''' - -EXAMPLES = ''' - -# Create a Server Group - ---- -- name: Create Server Group - hosts: localhost - gather_facts: False - connection: local - tasks: - - name: Create / Verify a Server Group at CenturyLink Cloud - clc_group: - name: 'My Cool Server Group' - parent: 'Default Group' - state: present - register: clc - - - name: debug - debug: var=clc - -# Delete a Server Group - ---- -- name: Delete Server Group - hosts: localhost - gather_facts: False - connection: local - tasks: - - name: Delete / Verify Absent a Server Group at CenturyLink Cloud - clc_group: - name: 'My Cool Server Group' - parent: 'Default Group' - state: absent - register: clc - - - name: debug - debug: var=clc - -''' - -__version__ = '${version}' - -import requests - -# -# Requires the clc-python-sdk. -# sudo pip install clc-sdk -# -try: - import clc as clc_sdk - from clc import CLCException -except ImportError: - CLC_FOUND = False - clc_sdk = None -else: - CLC_FOUND = True - - -class ClcGroup(object): - - clc = None - root_group = None - - def __init__(self, module): - """ - Construct module - """ - self.clc = clc_sdk - self.module = module - self.group_dict = {} - - if not CLC_FOUND: - self.module.fail_json( - msg='clc-python-sdk required for this module') - - self._set_user_agent(self.clc) - - def process_request(self): - """ - Execute the main code path, and handle the request - :return: none - """ - location = self.module.params.get('location') - group_name = self.module.params.get('name') - parent_name = self.module.params.get('parent') - group_description = self.module.params.get('description') - state = self.module.params.get('state') - - self._set_clc_credentials_from_env() - self.group_dict = self._get_group_tree_for_datacenter( - datacenter=location) - - if state == "absent": - changed, group, response = self._ensure_group_is_absent( - group_name=group_name, parent_name=parent_name) - - else: - changed, group, response = self._ensure_group_is_present( - group_name=group_name, parent_name=parent_name, group_description=group_description) - - - self.module.exit_json(changed=changed, group=group_name) - - # - # Functions to define the Ansible module and its arguments - # - - @staticmethod - def _define_module_argument_spec(): - """ - Define the argument spec for the ansible module - :return: argument spec dictionary - """ - argument_spec = dict( - name=dict(required=True), - description=dict(default=None), - parent=dict(default=None), - location=dict(default=None), - alias=dict(default=None), - custom_fields=dict(type='list', default=[]), - server_ids=dict(type='list', default=[]), - state=dict(default='present', choices=['present', 'absent'])) - - return argument_spec - - # - # Module Behavior Functions - # - - def _set_clc_credentials_from_env(self): - """ - Set the CLC Credentials on the sdk by reading environment variables - :return: none - """ - env = os.environ - v2_api_token = env.get('CLC_V2_API_TOKEN', False) - v2_api_username = env.get('CLC_V2_API_USERNAME', False) - v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) - clc_alias = env.get('CLC_ACCT_ALIAS', False) - api_url = env.get('CLC_V2_API_URL', False) - - if api_url: - self.clc.defaults.ENDPOINT_URL_V2 = api_url - - if v2_api_token and clc_alias: - self.clc._LOGIN_TOKEN_V2 = v2_api_token - self.clc._V2_ENABLED = True - self.clc.ALIAS = clc_alias - elif v2_api_username and v2_api_passwd: - self.clc.v2.SetCredentials( - api_username=v2_api_username, - api_passwd=v2_api_passwd) - else: - return self.module.fail_json( - msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " - "environment variables") - - def _ensure_group_is_absent(self, group_name, parent_name): - """ - Ensure that group_name is absent by deleting it if necessary - :param group_name: string - the name of the clc server group to delete - :param parent_name: string - the name of the parent group for group_name - :return: changed, group - """ - changed = False - group = [] - results = [] - - if self._group_exists(group_name=group_name, parent_name=parent_name): - if not self.module.check_mode: - group.append(group_name) - for g in group: - result = self._delete_group(group_name) - results.append(result) - changed = True - return changed, group, results - - def _delete_group(self, group_name): - """ - Delete the provided server group - :param group_name: string - the server group to delete - :return: none - """ - group, parent = self.group_dict.get(group_name) - response = group.Delete() - return response - - def _ensure_group_is_present( - self, - group_name, - parent_name, - group_description): - """ - Checks to see if a server group exists, creates it if it doesn't. - :param group_name: the name of the group to validate/create - :param parent_name: the name of the parent group for group_name - :param group_description: a short description of the server group (used when creating) - :return: (changed, group) - - changed: Boolean- whether a change was made, - group: A clc group object for the group - """ - assert self.root_group, "Implementation Error: Root Group not set" - parent = parent_name if parent_name is not None else self.root_group.name - description = group_description - changed = False - results = [] - groups = [] - group = group_name - - parent_exists = self._group_exists(group_name=parent, parent_name=None) - child_exists = self._group_exists(group_name=group_name, parent_name=parent) - - if parent_exists and child_exists: - group, parent = self.group_dict[group_name] - changed = False - elif parent_exists and not child_exists: - if not self.module.check_mode: - groups.append(group_name) - for g in groups: - group = self._create_group( - group=group, - parent=parent, - description=description) - results.append(group) - changed = True - else: - self.module.fail_json( - msg="parent group: " + - parent + - " does not exist") - - return changed, group, results - - def _create_group(self, group, parent, description): - """ - Create the provided server group - :param group: clc_sdk.Group - the group to create - :param parent: clc_sdk.Parent - the parent group for {group} - :param description: string - a text description of the group - :return: clc_sdk.Group - the created group - """ - - (parent, grandparent) = self.group_dict[parent] - return parent.Create(name=group, description=description) - - # - # Utility Functions - # - - def _group_exists(self, group_name, parent_name): - """ - Check to see if a group exists - :param group_name: string - the group to check - :param parent_name: string - the parent of group_name - :return: boolean - whether the group exists - """ - result = False - if group_name in self.group_dict: - (group, parent) = self.group_dict[group_name] - if parent_name is None or parent_name == parent.name: - result = True - return result - - def _get_group_tree_for_datacenter(self, datacenter=None, alias=None): - """ - Walk the tree of groups for a datacenter - :param datacenter: string - the datacenter to walk (ex: 'UC1') - :param alias: string - the account alias to search. Defaults to the current user's account - :return: a dictionary of groups and parents - """ - self.root_group = self.clc.v2.Datacenter( - location=datacenter).RootGroup() - return self._walk_groups_recursive( - parent_group=None, - child_group=self.root_group) - - def _walk_groups_recursive(self, parent_group, child_group): - """ - Walk a parent-child tree of groups, starting with the provided child group - :param parent_group: clc_sdk.Group - the parent group to start the walk - :param child_group: clc_sdk.Group - the child group to start the walk - :return: a dictionary of groups and parents - """ - result = {str(child_group): (child_group, parent_group)} - groups = child_group.Subgroups().groups - if len(groups) > 0: - for group in groups: - if group.type != 'default': - continue - - result.update(self._walk_groups_recursive(child_group, group)) - return result - - @staticmethod - def _set_user_agent(clc): - if hasattr(clc, 'SetRequestsSession'): - agent_string = "ClcAnsibleModule/" + __version__ - ses = requests.Session() - ses.headers.update({"Api-Client": agent_string}) - ses.headers['User-Agent'] += " " + agent_string - clc.SetRequestsSession(ses) - - -def main(): - """ - The main function. Instantiates the module and calls process_request. - :return: none - """ - module = AnsibleModule(argument_spec=ClcGroup._define_module_argument_spec(), supports_check_mode=True) - - clc_group = ClcGroup(module) - clc_group.process_request() - -from ansible.module_utils.basic import * # pylint: disable=W0614 -if __name__ == '__main__': - main() diff --git a/cloud/centurylink/clc_loadbalancer.py b/cloud/centurylink/clc_loadbalancer.py deleted file mode 100644 index 058954c687b..00000000000 --- a/cloud/centurylink/clc_loadbalancer.py +++ /dev/null @@ -1,759 +0,0 @@ -#!/usr/bin/python - -# CenturyLink Cloud Ansible Modules. -# -# These Ansible modules enable the CenturyLink Cloud v2 API to be called -# from an within Ansible Playbook. -# -# This file is part of CenturyLink Cloud, and is maintained -# by the Workflow as a Service Team -# -# Copyright 2015 CenturyLink Cloud -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# CenturyLink Cloud: http://www.CenturyLinkCloud.com -# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ -# - -DOCUMENTATION = ''' -module: -short_desciption: Create, Delete shared loadbalancers in CenturyLink Cloud. -description: - - An Ansible module to Create, Delete shared loadbalancers in CenturyLink Cloud. -options: -options: - name: - description: - - The name of the loadbalancer - required: True - description: - description: - - A description for your loadbalancer - alias: - description: - - The alias of your CLC Account - required: True - location: - description: - - The location of the datacenter your load balancer resides in - required: True - method: - description: - -The balancing method for this pool - default: roundRobin - choices: ['sticky', 'roundRobin'] - persistence: - description: - - The persistence method for this load balancer - default: standard - choices: ['standard', 'sticky'] - port: - description: - - Port to configure on the public-facing side of the load balancer pool - choices: [80, 443] - nodes: - description: - - A list of nodes that you want added to your load balancer pool - status: - description: - - The status of your loadbalancer - default: enabled - choices: ['enabled', 'disabled'] - state: - description: - - Whether to create or delete the load balancer pool - default: present - choices: ['present', 'absent', 'port_absent', 'nodes_present', 'nodes_absent'] -''' - -EXAMPLES = ''' -# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples -- name: Create Loadbalancer - hosts: localhost - connection: local - tasks: - - name: Actually Create things - clc_loadbalancer: - name: test - description: test - alias: TEST - location: WA1 - port: 443 - nodes: - - { 'ipAddress': '10.11.22.123', 'privatePort': 80 } - state: present - -- name: Add node to an existing loadbalancer pool - hosts: localhost - connection: local - tasks: - - name: Actually Create things - clc_loadbalancer: - name: test - description: test - alias: TEST - location: WA1 - port: 443 - nodes: - - { 'ipAddress': '10.11.22.234', 'privatePort': 80 } - state: nodes_present - -- name: Remove node from an existing loadbalancer pool - hosts: localhost - connection: local - tasks: - - name: Actually Create things - clc_loadbalancer: - name: test - description: test - alias: TEST - location: WA1 - port: 443 - nodes: - - { 'ipAddress': '10.11.22.234', 'privatePort': 80 } - state: nodes_absent - -- name: Delete LoadbalancerPool - hosts: localhost - connection: local - tasks: - - name: Actually Delete things - clc_loadbalancer: - name: test - description: test - alias: TEST - location: WA1 - port: 443 - nodes: - - { 'ipAddress': '10.11.22.123', 'privatePort': 80 } - state: port_absent - -- name: Delete Loadbalancer - hosts: localhost - connection: local - tasks: - - name: Actually Delete things - clc_loadbalancer: - name: test - description: test - alias: TEST - location: WA1 - port: 443 - nodes: - - { 'ipAddress': '10.11.22.123', 'privatePort': 80 } - state: absent - -''' - -__version__ = '${version}' - -import requests -from time import sleep - -# -# Requires the clc-python-sdk. -# sudo pip install clc-sdk -# -try: - import clc as clc_sdk - from clc import CLCException -except ImportError: - CLC_FOUND = False - clc_sdk = None -else: - CLC_FOUND = True - -class ClcLoadBalancer(): - - clc = None - - def __init__(self, module): - """ - Construct module - """ - self.clc = clc_sdk - self.module = module - self.lb_dict = {} - - if not CLC_FOUND: - self.module.fail_json( - msg='clc-python-sdk required for this module') - - self._set_user_agent(self.clc) - - def process_request(self): - """ - Execute the main code path, and handle the request - :return: none - """ - - loadbalancer_name=self.module.params.get('name') - loadbalancer_alias=self.module.params.get('alias') - loadbalancer_location=self.module.params.get('location') - loadbalancer_description=self.module.params.get('description') - loadbalancer_port=self.module.params.get('port') - loadbalancer_method=self.module.params.get('method') - loadbalancer_persistence=self.module.params.get('persistence') - loadbalancer_nodes=self.module.params.get('nodes') - loadbalancer_status=self.module.params.get('status') - state=self.module.params.get('state') - - if loadbalancer_description == None: - loadbalancer_description = loadbalancer_name - - self._set_clc_credentials_from_env() - - self.lb_dict = self._get_loadbalancer_list(alias=loadbalancer_alias, location=loadbalancer_location) - - if state == 'present': - changed, result_lb, lb_id = self.ensure_loadbalancer_present(name=loadbalancer_name, - alias=loadbalancer_alias, - location=loadbalancer_location, - description=loadbalancer_description, - status=loadbalancer_status) - if loadbalancer_port: - changed, result_pool, pool_id = self.ensure_loadbalancerpool_present(lb_id=lb_id, - alias=loadbalancer_alias, - location=loadbalancer_location, - method=loadbalancer_method, - persistence=loadbalancer_persistence, - port=loadbalancer_port) - - if loadbalancer_nodes: - changed, result_nodes = self.ensure_lbpool_nodes_set(alias=loadbalancer_alias, - location=loadbalancer_location, - name=loadbalancer_name, - port=loadbalancer_port, - nodes=loadbalancer_nodes - ) - elif state == 'absent': - changed, result_lb = self.ensure_loadbalancer_absent(name=loadbalancer_name, - alias=loadbalancer_alias, - location=loadbalancer_location) - - elif state == 'port_absent': - changed, result_lb = self.ensure_loadbalancerpool_absent(alias=loadbalancer_alias, - location=loadbalancer_location, - name=loadbalancer_name, - port=loadbalancer_port) - - elif state == 'nodes_present': - changed, result_lb = self.ensure_lbpool_nodes_present(alias=loadbalancer_alias, - location=loadbalancer_location, - name=loadbalancer_name, - port=loadbalancer_port, - nodes=loadbalancer_nodes) - - elif state == 'nodes_absent': - changed, result_lb = self.ensure_lbpool_nodes_absent(alias=loadbalancer_alias, - location=loadbalancer_location, - name=loadbalancer_name, - port=loadbalancer_port, - nodes=loadbalancer_nodes) - - self.module.exit_json(changed=changed, loadbalancer=result_lb) - # - # Functions to define the Ansible module and its arguments - # - def ensure_loadbalancer_present(self,name,alias,location,description,status): - """ - Check for loadbalancer presence (available) - :param name: Name of loadbalancer - :param alias: Alias of account - :param location: Datacenter - :param description: Description of loadbalancer - :param status: Enabled / Disabled - :return: True / False - """ - changed = False - result = None - lb_id = self._loadbalancer_exists(name=name) - if lb_id: - result = name - changed = False - else: - if not self.module.check_mode: - result = self.create_loadbalancer(name=name, - alias=alias, - location=location, - description=description, - status=status) - lb_id = result.get('id') - changed = True - - return changed, result, lb_id - - def ensure_loadbalancerpool_present(self, lb_id, alias, location, method, persistence, port): - """ - Checks to see if a load balancer pool exists and creates one if it does not. - :param name: The loadbalancer name - :param alias: The account alias - :param location: the datacenter the load balancer resides in - :param method: the load balancing method - :param persistence: the load balancing persistence type - :param port: the port that the load balancer will listen on - :return: (changed, group, pool_id) - - changed: Boolean whether a change was made - result: The result from the CLC API call - pool_id: The string id of the pool - """ - changed = False - result = None - if not lb_id: - return False, None, None - pool_id = self._loadbalancerpool_exists(alias=alias, location=location, port=port, lb_id=lb_id) - if not pool_id: - changed = True - if not self.module.check_mode: - result = self.create_loadbalancerpool(alias=alias, location=location, lb_id=lb_id, method=method, persistence=persistence, port=port) - pool_id = result.get('id') - - else: - changed = False - result = port - - return changed, result, pool_id - - def ensure_loadbalancer_absent(self,name,alias,location): - """ - Check for loadbalancer presence (not available) - :param name: Name of loadbalancer - :param alias: Alias of account - :param location: Datacenter - :return: (changed, result) - changed: Boolean whether a change was made - result: The result from the CLC API Call - """ - changed = False - result = None - lb_exists = self._loadbalancer_exists(name=name) - if lb_exists: - if not self.module.check_mode: - result = self.delete_loadbalancer(alias=alias, - location=location, - name=name) - changed = True - else: - result = name - changed = False - return changed, result - - def ensure_loadbalancerpool_absent(self, alias, location, name, port): - """ - Checks to see if a load balancer pool exists and deletes it if it does - :param alias: The account alias - :param location: the datacenter the load balancer resides in - :param loadbalancer: the name of the load balancer - :param port: the port that the load balancer will listen on - :return: (changed, group) - - changed: Boolean whether a change was made - result: The result from the CLC API call - """ - changed = False - result = None - lb_exists = self._loadbalancer_exists(name=name) - if lb_exists: - lb_id = self._get_loadbalancer_id(name=name) - pool_id = self._loadbalancerpool_exists(alias=alias, location=location, port=port, lb_id=lb_id) - if pool_id: - changed = True - if not self.module.check_mode: - result = self.delete_loadbalancerpool(alias=alias, location=location, lb_id=lb_id, pool_id=pool_id) - else: - changed = False - result = "Pool doesn't exist" - else: - result = "LB Doesn't Exist" - return changed, result - - def ensure_lbpool_nodes_set(self, alias, location, name, port, nodes): - """ - Checks to see if the provided list of nodes exist for the pool and set the nodes if any in the list doesn't exist - :param alias: The account alias - :param location: the datacenter the load balancer resides in - :param name: the name of the load balancer - :param port: the port that the load balancer will listen on - :param nodes: The list of nodes to be updated to the pool - :return: (changed, group) - - changed: Boolean whether a change was made - result: The result from the CLC API call - """ - result = {} - changed = False - lb_exists = self._loadbalancer_exists(name=name) - if lb_exists: - lb_id = self._get_loadbalancer_id(name=name) - pool_id = self._loadbalancerpool_exists(alias=alias, location=location, port=port, lb_id=lb_id) - if pool_id: - nodes_exist = self._loadbalancerpool_nodes_exists(alias=alias, - location=location, - port=port, - lb_id=lb_id, - pool_id=pool_id, - nodes_to_check=nodes) - if not nodes_exist: - changed = True - result = self.set_loadbalancernodes(alias=alias, - location=location, - lb_id=lb_id, - pool_id=pool_id, - nodes=nodes) - else: - result = "Pool doesn't exist" - else: - result = "Load balancer doesn't Exist" - return changed, result - - def ensure_lbpool_nodes_present(self, alias, location, name, port, nodes): - """ - Checks to see if the provided list of nodes exist for the pool and add the missing nodes to the pool - :param alias: The account alias - :param location: the datacenter the load balancer resides in - :param name: the name of the load balancer - :param port: the port that the load balancer will listen on - :param nodes: the list of nodes to be added - :return: (changed, group) - - changed: Boolean whether a change was made - result: The result from the CLC API call - """ - changed = False - lb_exists = self._loadbalancer_exists(name=name) - if lb_exists: - lb_id = self._get_loadbalancer_id(name=name) - pool_id = self._loadbalancerpool_exists(alias=alias, location=location, port=port, lb_id=lb_id) - if pool_id: - changed, result = self.add_lbpool_nodes(alias=alias, - location=location, - lb_id=lb_id, - pool_id=pool_id, - nodes_to_add=nodes) - else: - result = "Pool doesn't exist" - else: - result = "Load balancer doesn't Exist" - return changed, result - - def ensure_lbpool_nodes_absent(self, alias, location, name, port, nodes): - """ - Checks to see if the provided list of nodes exist for the pool and add the missing nodes to the pool - :param alias: The account alias - :param location: the datacenter the load balancer resides in - :param name: the name of the load balancer - :param port: the port that the load balancer will listen on - :param nodes: the list of nodes to be removed - :return: (changed, group) - - changed: Boolean whether a change was made - result: The result from the CLC API call - """ - changed = False - lb_exists = self._loadbalancer_exists(name=name) - if lb_exists: - lb_id = self._get_loadbalancer_id(name=name) - pool_id = self._loadbalancerpool_exists(alias=alias, location=location, port=port, lb_id=lb_id) - if pool_id: - changed, result = self.remove_lbpool_nodes(alias=alias, - location=location, - lb_id=lb_id, - pool_id=pool_id, - nodes_to_remove=nodes) - else: - result = "Pool doesn't exist" - else: - result = "Load balancer doesn't Exist" - return changed, result - - def create_loadbalancer(self,name,alias,location,description,status): - """ - Create a loadbalancer w/ params - :param name: Name of loadbalancer - :param alias: Alias of account - :param location: Datacenter - :param description: Description for loadbalancer to be created - :param status: Enabled / Disabled - :return: Success / Failure - """ - result = self.clc.v2.API.Call('POST', '/v2/sharedLoadBalancers/%s/%s' % (alias, location), json.dumps({"name":name,"description":description,"status":status})) - sleep(1) - return result - - def create_loadbalancerpool(self, alias, location, lb_id, method, persistence, port): - """ - Creates a pool on the provided load balancer - :param alias: the account alias - :param location: the datacenter the load balancer resides in - :param lb_id: the id string of the load balancer - :param method: the load balancing method - :param persistence: the load balancing persistence type - :param port: the port that the load balancer will listen on - :return: result: The result from the create API call - """ - result = self.clc.v2.API.Call('POST', '/v2/sharedLoadBalancers/%s/%s/%s/pools' % (alias, location, lb_id), json.dumps({"port":port, "method":method, "persistence":persistence})) - return result - - def delete_loadbalancer(self,alias,location,name): - """ - Delete CLC loadbalancer - :param alias: Alias for account - :param location: Datacenter - :param name: Name of the loadbalancer to delete - :return: 204 if successful else failure - """ - lb_id = self._get_loadbalancer_id(name=name) - result = self.clc.v2.API.Call('DELETE', '/v2/sharedLoadBalancers/%s/%s/%s' % (alias, location, lb_id)) - return result - - def delete_loadbalancerpool(self, alias, location, lb_id, pool_id): - """ - Delete a pool on the provided load balancer - :param alias: The account alias - :param location: the datacenter the load balancer resides in - :param lb_id: the id string of the load balancer - :param pool_id: the id string of the pool - :return: result: The result from the delete API call - """ - result = self.clc.v2.API.Call('DELETE', '/v2/sharedLoadBalancers/%s/%s/%s/pools/%s' % (alias, location, lb_id, pool_id)) - return result - - def _get_loadbalancer_id(self, name): - """ - Retrieve unique ID of loadbalancer - :param name: Name of loadbalancer - :return: Unique ID of loadbalancer - """ - for lb in self.lb_dict: - if lb.get('name') == name: - id = lb.get('id') - return id - - def _get_loadbalancer_list(self, alias, location): - """ - Retrieve a list of loadbalancers - :param alias: Alias for account - :param location: Datacenter - :return: JSON data for all loadbalancers at datacenter - """ - return self.clc.v2.API.Call('GET', '/v2/sharedLoadBalancers/%s/%s' % (alias, location)) - - def _loadbalancer_exists(self, name): - """ - Verify a loadbalancer exists - :param name: Name of loadbalancer - :return: False or the ID of the existing loadbalancer - """ - result = False - - for lb in self.lb_dict: - if lb.get('name') == name: - result = lb.get('id') - return result - - def _loadbalancerpool_exists(self, alias, location, port, lb_id): - """ - Checks to see if a pool exists on the specified port on the provided load balancer - :param alias: the account alias - :param location: the datacenter the load balancer resides in - :param port: the port to check and see if it exists - :param lb_id: the id string of the provided load balancer - :return: result: The id string of the pool or False - """ - result = False - pool_list = self.clc.v2.API.Call('GET', '/v2/sharedLoadBalancers/%s/%s/%s/pools' % (alias, location, lb_id)) - for pool in pool_list: - if int(pool.get('port')) == int(port): - result = pool.get('id') - - return result - - def _loadbalancerpool_nodes_exists(self, alias, location, port, lb_id, pool_id, nodes_to_check): - """ - Checks to see if a set of nodes exists on the specified port on the provided load balancer - :param alias: the account alias - :param location: the datacenter the load balancer resides in - :param port: the port to check and see if it exists - :param lb_id: the id string of the provided load balancer - :param pool_id: the id string of the load balancer pool - :param nodes_to_check: the list of nodes to check for - :return: result: The id string of the pool or False - """ - result = False - nodes = self._get_lbpool_nodes(alias, location, lb_id, pool_id) - for node in nodes_to_check: - if not node.get('status'): - node['status'] = 'enabled' - if node in nodes: - result = True - else: - result = False - return result - - def set_loadbalancernodes(self, alias, location, lb_id, pool_id, nodes): - """ - Updates nodes to the provided pool - :param alias: the account alias - :param location: the datacenter the load balancer resides in - :param lb_id: the id string of the load balancer - :param pool_id: the id string of the pool - :param nodes: a list of dictionaries containing the nodes to set - :return: result: The result from the API call - """ - result = None - if not lb_id: - return result - if not self.module.check_mode: - result = self.clc.v2.API.Call('PUT', - '/v2/sharedLoadBalancers/%s/%s/%s/pools/%s/nodes' - % (alias, location, lb_id, pool_id), json.dumps(nodes)) - return result - - def add_lbpool_nodes(self, alias, location, lb_id, pool_id, nodes_to_add): - """ - Add nodes to the provided pool - :param alias: the account alias - :param location: the datacenter the load balancer resides in - :param lb_id: the id string of the load balancer - :param pool_id: the id string of the pool - :param nodes: a list of dictionaries containing the nodes to add - :return: (changed, group) - - changed: Boolean whether a change was made - result: The result from the CLC API call - """ - changed = False - result = {} - nodes = self._get_lbpool_nodes(alias, location, lb_id, pool_id) - for node in nodes_to_add: - if not node.get('status'): - node['status'] = 'enabled' - if not node in nodes: - changed = True - nodes.append(node) - if changed == True and not self.module.check_mode: - result = self.set_loadbalancernodes(alias, location, lb_id, pool_id, nodes) - return changed, result - - def remove_lbpool_nodes(self, alias, location, lb_id, pool_id, nodes_to_remove): - """ - Removes nodes from the provided pool - :param alias: the account alias - :param location: the datacenter the load balancer resides in - :param lb_id: the id string of the load balancer - :param pool_id: the id string of the pool - :param nodes: a list of dictionaries containing the nodes to remove - :return: (changed, group) - - changed: Boolean whether a change was made - result: The result from the CLC API call - """ - changed = False - result = {} - nodes = self._get_lbpool_nodes(alias, location, lb_id, pool_id) - for node in nodes_to_remove: - if not node.get('status'): - node['status'] = 'enabled' - if node in nodes: - changed = True - nodes.remove(node) - if changed == True and not self.module.check_mode: - result = self.set_loadbalancernodes(alias, location, lb_id, pool_id, nodes) - return changed, result - - def _get_lbpool_nodes(self, alias, location, lb_id, pool_id): - """ - Return the list of nodes available to the provided load balancer pool - :param alias: the account alias - :param location: the datacenter the load balancer resides in - :param lb_id: the id string of the load balancer - :param pool_id: the id string of the pool - :return: result: The list of nodes - """ - result = self.clc.v2.API.Call('GET', - '/v2/sharedLoadBalancers/%s/%s/%s/pools/%s/nodes' - % (alias, location, lb_id, pool_id)) - return result - - @staticmethod - def define_argument_spec(): - """ - Define the argument spec for the ansible module - :return: argument spec dictionary - """ - argument_spec = dict( - name=dict(required=True), - description=dict(default=None), - location=dict(required=True, default=None), - alias=dict(required=True, default=None), - port=dict(choices=[80, 443]), - method=dict(choices=['leastConnection', 'roundRobin']), - persistence=dict(choices=['standard', 'sticky']), - nodes=dict(type='list', default=[]), - status=dict(default='enabled', choices=['enabled', 'disabled']), - state=dict(default='present', choices=['present', 'absent', 'port_absent', 'nodes_present', 'nodes_absent']), - wait=dict(type='bool', default=True) - ) - - return argument_spec - - # - # Module Behavior Functions - # - - def _set_clc_credentials_from_env(self): - """ - Set the CLC Credentials on the sdk by reading environment variables - :return: none - """ - env = os.environ - v2_api_token = env.get('CLC_V2_API_TOKEN', False) - v2_api_username = env.get('CLC_V2_API_USERNAME', False) - v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) - clc_alias = env.get('CLC_ACCT_ALIAS', False) - api_url = env.get('CLC_V2_API_URL', False) - - if api_url: - self.clc.defaults.ENDPOINT_URL_V2 = api_url - - if v2_api_token and clc_alias: - self.clc._LOGIN_TOKEN_V2 = v2_api_token - self.clc._V2_ENABLED = True - self.clc.ALIAS = clc_alias - elif v2_api_username and v2_api_passwd: - self.clc.v2.SetCredentials( - api_username=v2_api_username, - api_passwd=v2_api_passwd) - else: - return self.module.fail_json( - msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " - "environment variables") - - @staticmethod - def _set_user_agent(clc): - if hasattr(clc, 'SetRequestsSession'): - agent_string = "ClcAnsibleModule/" + __version__ - ses = requests.Session() - ses.headers.update({"Api-Client": agent_string}) - ses.headers['User-Agent'] += " " + agent_string - clc.SetRequestsSession(ses) - - -def main(): - """ - The main function. Instantiates the module and calls process_request. - :return: none - """ - module = AnsibleModule(argument_spec=ClcLoadBalancer.define_argument_spec(), - supports_check_mode=True) - clc_loadbalancer = ClcLoadBalancer(module) - clc_loadbalancer.process_request() - -from ansible.module_utils.basic import * # pylint: disable=W0614 -if __name__ == '__main__': - main() diff --git a/cloud/centurylink/clc_modify_server.py b/cloud/centurylink/clc_modify_server.py deleted file mode 100644 index 1a1e4d5b858..00000000000 --- a/cloud/centurylink/clc_modify_server.py +++ /dev/null @@ -1,710 +0,0 @@ -#!/usr/bin/python - -# CenturyLink Cloud Ansible Modules. -# -# These Ansible modules enable the CenturyLink Cloud v2 API to be called -# from an within Ansible Playbook. -# -# This file is part of CenturyLink Cloud, and is maintained -# by the Workflow as a Service Team -# -# Copyright 2015 CenturyLink Cloud -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# CenturyLink Cloud: http://www.CenturyLinkCloud.com -# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ -# - -DOCUMENTATION = ''' -module: clc_modify_server -short_desciption: modify servers in CenturyLink Cloud. -description: - - An Ansible module to modify servers in CenturyLink Cloud. -options: - server_ids: - description: - - A list of server Ids to modify. - default: [] - required: True - aliases: [] - cpu: - description: - - How many CPUs to update on the server - default: None - required: False - aliases: [] - memory: - description: - - Memory in GB. - default: None - required: False - aliases: [] - anti_affinity_policy_id: - description: - - The anti affinity policy id to be set for a heperscale server. - This is mutually exclusive with 'anti_affinity_policy_name' - default: None - required: False - aliases: [] - anti_affinity_policy_name: - description: - - The anti affinity policy name to be set for a heperscale server. - This is mutually exclusive with 'anti_affinity_policy_id' - default: None - required: False - aliases: [] - alert_policy_id: - description: - - The alert policy id to be associated. - This is mutually exclusive with 'alert_policy_name' - default: None - required: False - aliases: [] - alert_policy_name: - description: - - The alert policy name to be associated. - This is mutually exclusive with 'alert_policy_id' - default: None - required: False - aliases: [] - state: - description: - - The state to insure that the provided resources are in. - default: 'present' - required: False - choices: ['present', 'absent'] - aliases: [] - wait: - description: - - Whether to wait for the provisioning tasks to finish before returning. - default: True - required: False - choices: [ True, False] - aliases: [] -''' - -EXAMPLES = ''' -# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples - -- name: set the cpu count to 4 on a server - clc_server: - server_ids: ['UC1ACCTTEST01'] - cpu: 4 - state: present - -- name: set the memory to 8GB on a server - clc_server: - server_ids: ['UC1ACCTTEST01'] - memory: 8 - state: present - -- name: set the anti affinity policy on a server - clc_server: - server_ids: ['UC1ACCTTEST01'] - anti_affinity_policy_name: 'aa_policy' - state: present - -- name: set the alert policy on a server - clc_server: - server_ids: ['UC1ACCTTEST01'] - alert_policy_name: 'alert_policy' - state: present - -- name: set the memory to 16GB and cpu to 8 core on a lust if servers - clc_server: - server_ids: ['UC1ACCTTEST01','UC1ACCTTEST02'] - cpu: 8 - memory: 16 - state: present -''' - -__version__ = '${version}' - -import requests - -# -# Requires the clc-python-sdk. -# sudo pip install clc-sdk -# -try: - import clc as clc_sdk - from clc import CLCException - from clc import APIFailedResponse -except ImportError: - CLC_FOUND = False - clc_sdk = None -else: - CLC_FOUND = True - - -class ClcModifyServer(): - clc = clc_sdk - - def __init__(self, module): - """ - Construct module - """ - self.clc = clc_sdk - self.module = module - self.group_dict = {} - - if not CLC_FOUND: - self.module.fail_json( - msg='clc-python-sdk required for this module') - - self._set_user_agent(self.clc) - - def process_request(self): - """ - Process the request - Main Code Path - :return: Returns with either an exit_json or fail_json - """ - self._set_clc_credentials_from_env() - - p = self.module.params - - server_ids = p['server_ids'] - if not isinstance(server_ids, list): - return self.module.fail_json( - msg='server_ids needs to be a list of instances to modify: %s' % - server_ids) - - (changed, server_dict_array, new_server_ids) = ClcModifyServer._modify_servers( - module=self.module, clc=self.clc, server_ids=server_ids) - - self.module.exit_json( - changed=changed, - server_ids=new_server_ids, - servers=server_dict_array) - - @staticmethod - def _define_module_argument_spec(): - """ - Define the argument spec for the ansible module - :return: argument spec dictionary - """ - argument_spec = dict( - server_ids=dict(type='list', required=True), - state=dict(default='present', choices=['present', 'absent']), - cpu=dict(), - memory=dict(), - anti_affinity_policy_id=dict(), - anti_affinity_policy_name=dict(), - alert_policy_id=dict(), - alert_policy_name=dict(), - wait=dict(type='bool', default=True) - ) - mutually_exclusive = [ - ['anti_affinity_policy_id', 'anti_affinity_policy_name'], - ['alert_policy_id', 'alert_policy_name'] - ] - return {"argument_spec": argument_spec, - "mutually_exclusive": mutually_exclusive} - - def _set_clc_credentials_from_env(self): - """ - Set the CLC Credentials on the sdk by reading environment variables - :return: none - """ - env = os.environ - v2_api_token = env.get('CLC_V2_API_TOKEN', False) - v2_api_username = env.get('CLC_V2_API_USERNAME', False) - v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) - clc_alias = env.get('CLC_ACCT_ALIAS', False) - api_url = env.get('CLC_V2_API_URL', False) - - if api_url: - self.clc.defaults.ENDPOINT_URL_V2 = api_url - - if v2_api_token and clc_alias: - self.clc._LOGIN_TOKEN_V2 = v2_api_token - self.clc._V2_ENABLED = True - self.clc.ALIAS = clc_alias - elif v2_api_username and v2_api_passwd: - self.clc.v2.SetCredentials( - api_username=v2_api_username, - api_passwd=v2_api_passwd) - else: - return self.module.fail_json( - msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " - "environment variables") - - @staticmethod - def _wait_for_requests(clc, requests, servers, wait): - """ - Block until server provisioning requests are completed. - :param clc: the clc-sdk instance to use - :param requests: a list of clc-sdk.Request instances - :param servers: a list of servers to refresh - :param wait: a boolean on whether to block or not. This function is skipped if True - :return: none - """ - if wait: - # Requests.WaitUntilComplete() returns the count of failed requests - failed_requests_count = sum( - [request.WaitUntilComplete() for request in requests]) - - if failed_requests_count > 0: - raise clc - else: - ClcModifyServer._refresh_servers(servers) - - @staticmethod - def _refresh_servers(servers): - """ - Loop through a list of servers and refresh them - :param servers: list of clc-sdk.Server instances to refresh - :return: none - """ - for server in servers: - server.Refresh() - - @staticmethod - def _modify_servers(module, clc, server_ids): - """ - modify the servers configuration on the provided list - :param module: the AnsibleModule object - :param clc: the clc-sdk instance to use - :param server_ids: list of servers to modify - :return: a list of dictionaries with server information about the servers that were modified - """ - p = module.params - wait = p.get('wait') - state = p.get('state') - server_params = { - 'cpu': p.get('cpu'), - 'memory': p.get('memory'), - 'anti_affinity_policy_id': p.get('anti_affinity_policy_id'), - 'anti_affinity_policy_name': p.get('anti_affinity_policy_name'), - 'alert_policy_id': p.get('alert_policy_id'), - 'alert_policy_name': p.get('alert_policy_name'), - } - changed = False - server_changed = False - aa_changed = False - ap_changed = False - server_dict_array = [] - result_server_ids = [] - requests = [] - - if not isinstance(server_ids, list) or len(server_ids) < 1: - return module.fail_json( - msg='server_ids should be a list of servers, aborting') - - servers = clc.v2.Servers(server_ids).Servers() - if state == 'present': - for server in servers: - server_changed, server_result, changed_servers = ClcModifyServer._ensure_server_config( - clc, module, None, server, server_params) - if server_result: - requests.append(server_result) - aa_changed, changed_servers = ClcModifyServer._ensure_aa_policy( - clc, module, None, server, server_params) - ap_changed, changed_servers = ClcModifyServer._ensure_alert_policy_present( - clc, module, None, server, server_params) - elif state == 'absent': - for server in servers: - ap_changed, changed_servers = ClcModifyServer._ensure_alert_policy_absent( - clc, module, None, server, server_params) - if server_changed or aa_changed or ap_changed: - changed = True - - if wait: - for r in requests: - r.WaitUntilComplete() - for server in changed_servers: - server.Refresh() - - for server in changed_servers: - server_dict_array.append(server.data) - result_server_ids.append(server.id) - - return changed, server_dict_array, result_server_ids - - @staticmethod - def _ensure_server_config( - clc, module, alias, server, server_params): - """ - ensures the server is updated with the provided cpu and memory - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param alias: the CLC account alias - :param server: the CLC server object - :param server_params: the dictionary of server parameters - :return: (changed, group) - - changed: Boolean whether a change was made - result: The result from the CLC API call - """ - cpu = server_params.get('cpu') - memory = server_params.get('memory') - changed = False - result = None - changed_servers = [] - - if not cpu: - cpu = server.cpu - if not memory: - memory = server.memory - if memory != server.memory or cpu != server.cpu: - changed_servers.append(server) - result = ClcModifyServer._modify_clc_server( - clc, - module, - None, - server.id, - cpu, - memory) - changed = True - return changed, result, changed_servers - - @staticmethod - def _modify_clc_server(clc, module, acct_alias, server_id, cpu, memory): - """ - Modify the memory or CPU on a clc server. This function is not yet implemented. - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param acct_alias: the clc account alias to look up the server - :param server_id: id of the server to modify - :param cpu: the new cpu value - :param memory: the new memory value - :return: the result of CLC API call - """ - if not acct_alias: - acct_alias = clc.v2.Account.GetAlias() - if not server_id: - return module.fail_json( - msg='server_id must be provided to modify the server') - - result = None - - if not module.check_mode: - - # Update the server configuation - job_obj = clc.v2.API.Call('PATCH', - 'servers/%s/%s' % (acct_alias, - server_id), - json.dumps([{"op": "set", - "member": "memory", - "value": memory}, - {"op": "set", - "member": "cpu", - "value": cpu}])) - result = clc.v2.Requests(job_obj) - return result - - @staticmethod - def _ensure_aa_policy( - clc, module, acct_alias, server, server_params): - """ - ensures the server is updated with the provided anti affinity policy - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param acct_alias: the CLC account alias - :param server: the CLC server object - :param server_params: the dictionary of server parameters - :return: (changed, group) - - changed: Boolean whether a change was made - result: The result from the CLC API call - """ - changed = False - changed_servers = [] - - if not acct_alias: - acct_alias = clc.v2.Account.GetAlias() - - aa_policy_id = server_params.get('anti_affinity_policy_id') - aa_policy_name = server_params.get('anti_affinity_policy_name') - if not aa_policy_id and aa_policy_name: - aa_policy_id = ClcModifyServer._get_aa_policy_id_by_name( - clc, - module, - acct_alias, - aa_policy_name) - current_aa_policy_id = ClcModifyServer._get_aa_policy_id_of_server( - clc, - module, - acct_alias, - server.id) - - if aa_policy_id and aa_policy_id != current_aa_policy_id: - if server not in changed_servers: - changed_servers.append(server) - ClcModifyServer._modify_aa_policy( - clc, - module, - acct_alias, - server.id, - aa_policy_id) - changed = True - return changed, changed_servers - - @staticmethod - def _modify_aa_policy(clc, module, acct_alias, server_id, aa_policy_id): - """ - modifies the anti affinity policy of the CLC server - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param acct_alias: the CLC account alias - :param server_id: the CLC server id - :param aa_policy_id: the anti affinity policy id - :return: result: The result from the CLC API call - """ - result = None - if not module.check_mode: - result = clc.v2.API.Call('PUT', - 'servers/%s/%s/antiAffinityPolicy' % ( - acct_alias, - server_id), - json.dumps({"id": aa_policy_id})) - return result - - @staticmethod - def _get_aa_policy_id_by_name(clc, module, alias, aa_policy_name): - """ - retrieves the anti affinity policy id of the server based on the name of the policy - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param alias: the CLC account alias - :param aa_policy_name: the anti affinity policy name - :return: aa_policy_id: The anti affinity policy id - """ - aa_policy_id = None - aa_policies = clc.v2.API.Call(method='GET', - url='antiAffinityPolicies/%s' % (alias)) - for aa_policy in aa_policies.get('items'): - if aa_policy.get('name') == aa_policy_name: - if not aa_policy_id: - aa_policy_id = aa_policy.get('id') - else: - return module.fail_json( - msg='mutiple anti affinity policies were found with policy name : %s' % - (aa_policy_name)) - if not aa_policy_id: - return module.fail_json( - msg='No anti affinity policy was found with policy name : %s' % - (aa_policy_name)) - return aa_policy_id - - @staticmethod - def _get_aa_policy_id_of_server(clc, module, alias, server_id): - """ - retrieves the anti affinity policy id of the server based on the CLC server id - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param alias: the CLC account alias - :param server_id: the CLC server id - :return: aa_policy_id: The anti affinity policy id - """ - aa_policy_id = None - try: - result = clc.v2.API.Call( - method='GET', url='servers/%s/%s/antiAffinityPolicy' % - (alias, server_id)) - aa_policy_id = result.get('id') - except APIFailedResponse as e: - if e.response_status_code != 404: - raise e - return aa_policy_id - - @staticmethod - def _ensure_alert_policy_present( - clc, module, acct_alias, server, server_params): - """ - ensures the server is updated with the provided alert policy - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param acct_alias: the CLC account alias - :param server: the CLC server object - :param server_params: the dictionary of server parameters - :return: (changed, group) - - changed: Boolean whether a change was made - result: The result from the CLC API call - """ - changed = False - changed_servers = [] - - if not acct_alias: - acct_alias = clc.v2.Account.GetAlias() - - alert_policy_id = server_params.get('alert_policy_id') - alert_policy_name = server_params.get('alert_policy_name') - if not alert_policy_id and alert_policy_name: - alert_policy_id = ClcModifyServer._get_alert_policy_id_by_name( - clc, - module, - acct_alias, - alert_policy_name) - if alert_policy_id and not ClcModifyServer._alert_policy_exists(server, alert_policy_id): - if server not in changed_servers: - changed_servers.append(server) - ClcModifyServer._add_alert_policy_to_server( - clc, - module, - acct_alias, - server.id, - alert_policy_id) - changed = True - return changed, changed_servers - - @staticmethod - def _ensure_alert_policy_absent( - clc, module, acct_alias, server, server_params): - """ - ensures the alert policy is removed from the server - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param acct_alias: the CLC account alias - :param server: the CLC server object - :param server_params: the dictionary of server parameters - :return: (changed, group) - - changed: Boolean whether a change was made - result: The result from the CLC API call - """ - changed = False - result = None - changed_servers = [] - - if not acct_alias: - acct_alias = clc.v2.Account.GetAlias() - - alert_policy_id = server_params.get('alert_policy_id') - alert_policy_name = server_params.get('alert_policy_name') - if not alert_policy_id and alert_policy_name: - alert_policy_id = ClcModifyServer._get_alert_policy_id_by_name( - clc, - module, - acct_alias, - alert_policy_name) - - if alert_policy_id and ClcModifyServer._alert_policy_exists(server, alert_policy_id): - if server not in changed_servers: - changed_servers.append(server) - ClcModifyServer._remove_alert_policy_to_server( - clc, - module, - acct_alias, - server.id, - alert_policy_id) - changed = True - return changed, changed_servers - - @staticmethod - def _add_alert_policy_to_server(clc, module, acct_alias, server_id, alert_policy_id): - """ - add the alert policy to CLC server - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param acct_alias: the CLC account alias - :param server_id: the CLC server id - :param alert_policy_id: the alert policy id - :return: result: The result from the CLC API call - """ - result = None - if not module.check_mode: - try: - result = clc.v2.API.Call('POST', - 'servers/%s/%s/alertPolicies' % ( - acct_alias, - server_id), - json.dumps({"id": alert_policy_id})) - except clc.APIFailedResponse as e: - return module.fail_json( - msg='Unable to set alert policy to the server : %s. %s' % (server_id, str(e.response_text))) - return result - - @staticmethod - def _remove_alert_policy_to_server(clc, module, acct_alias, server_id, alert_policy_id): - """ - remove the alert policy to the CLC server - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param acct_alias: the CLC account alias - :param server_id: the CLC server id - :param alert_policy_id: the alert policy id - :return: result: The result from the CLC API call - """ - result = None - if not module.check_mode: - try: - result = clc.v2.API.Call('DELETE', - 'servers/%s/%s/alertPolicies/%s' - % (acct_alias, server_id, alert_policy_id)) - except clc.APIFailedResponse as e: - return module.fail_json( - msg='Unable to remove alert policy to the server : %s. %s' % (server_id, str(e.response_text))) - return result - - @staticmethod - def _get_alert_policy_id_by_name(clc, module, alias, alert_policy_name): - """ - retrieves the alert policy id of the server based on the name of the policy - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param alias: the CLC account alias - :param alert_policy_name: the alert policy name - :return: alert_policy_id: The alert policy id - """ - alert_policy_id = None - alert_policies = clc.v2.API.Call(method='GET', - url='alertPolicies/%s' % (alias)) - for alert_policy in alert_policies.get('items'): - if alert_policy.get('name') == alert_policy_name: - if not alert_policy_id: - alert_policy_id = alert_policy.get('id') - else: - return module.fail_json( - msg='mutiple alert policies were found with policy name : %s' % - (alert_policy_name)) - return alert_policy_id - - @staticmethod - def _alert_policy_exists(server, alert_policy_id): - """ - Checks if the alert policy exists for the server - :param server: the clc server object - :param alert_policy_id: the alert policy - :return: True: if the given alert policy id associated to the server, False otherwise - """ - result = False - alert_policies = server.alertPolicies - if alert_policies: - for alert_policy in alert_policies: - if alert_policy.get('id') == alert_policy_id: - result = True - return result - - @staticmethod - def _set_user_agent(clc): - if hasattr(clc, 'SetRequestsSession'): - agent_string = "ClcAnsibleModule/" + __version__ - ses = requests.Session() - ses.headers.update({"Api-Client": agent_string}) - ses.headers['User-Agent'] += " " + agent_string - clc.SetRequestsSession(ses) - - -def main(): - """ - The main function. Instantiates the module and calls process_request. - :return: none - """ - - argument_dict = ClcModifyServer._define_module_argument_spec() - module = AnsibleModule(supports_check_mode=True, **argument_dict) - clc_modify_server = ClcModifyServer(module) - clc_modify_server.process_request() - -from ansible.module_utils.basic import * # pylint: disable=W0614 -if __name__ == '__main__': - main() diff --git a/cloud/centurylink/clc_server.py b/cloud/centurylink/clc_server.py deleted file mode 100644 index e102cd21f47..00000000000 --- a/cloud/centurylink/clc_server.py +++ /dev/null @@ -1,1323 +0,0 @@ -#!/usr/bin/python - -# CenturyLink Cloud Ansible Modules. -# -# These Ansible modules enable the CenturyLink Cloud v2 API to be called -# from an within Ansible Playbook. -# -# This file is part of CenturyLink Cloud, and is maintained -# by the Workflow as a Service Team -# -# Copyright 2015 CenturyLink Cloud -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# CenturyLink Cloud: http://www.CenturyLinkCloud.com -# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ -# - -DOCUMENTATION = ''' -module: clc_server -short_desciption: Create, Delete, Start and Stop servers in CenturyLink Cloud. -description: - - An Ansible module to Create, Delete, Start and Stop servers in CenturyLink Cloud. -options: - additional_disks: - description: - - Specify additional disks for the server - required: False - default: None - aliases: [] - add_public_ip: - description: - - Whether to add a public ip to the server - required: False - default: False - choices: [False, True] - aliases: [] - alias: - description: - - The account alias to provision the servers under. - default: - - The default alias for the API credentials - required: False - default: None - aliases: [] - anti_affinity_policy_id: - description: - - The anti-affinity policy to assign to the server. This is mutually exclusive with 'anti_affinity_policy_name'. - required: False - default: None - aliases: [] - anti_affinity_policy_name: - description: - - The anti-affinity policy to assign to the server. This is mutually exclusive with 'anti_affinity_policy_id'. - required: False - default: None - aliases: [] - alert_policy_id: - description: - - The alert policy to assign to the server. This is mutually exclusive with 'alert_policy_name'. - required: False - default: None - aliases: [] - alert_policy_name: - description: - - The alert policy to assign to the server. This is mutually exclusive with 'alert_policy_id'. - required: False - default: None - aliases: [] - - count: - description: - - The number of servers to build (mutually exclusive with exact_count) - default: None - aliases: [] - count_group: - description: - - Required when exact_count is specified. The Server Group use to determine how many severs to deploy. - default: 1 - required: False - aliases: [] - cpu: - description: - - How many CPUs to provision on the server - default: None - required: False - aliases: [] - cpu_autoscale_policy_id: - description: - - The autoscale policy to assign to the server. - default: None - required: False - aliases: [] - custom_fields: - description: - - A dictionary of custom fields to set on the server. - default: [] - required: False - aliases: [] - description: - description: - - The description to set for the server. - default: None - required: False - aliases: [] - exact_count: - description: - - Run in idempotent mode. Will insure that this exact number of servers are running in the provided group, creating and deleting them to reach that count. Requires count_group to be set. - default: None - required: False - aliases: [] - group: - description: - - The Server Group to create servers under. - default: 'Default Group' - required: False - aliases: [] - ip_address: - description: - - The IP Address for the server. One is assigned if not provided. - default: None - required: False - aliases: [] - location: - description: - - The Datacenter to create servers in. - default: None - required: False - aliases: [] - managed_os: - description: - - Whether to create the server as 'Managed' or not. - default: False - required: False - choices: [True, False] - aliases: [] - memory: - description: - - Memory in GB. - default: 1 - required: False - aliases: [] - name: - description: - - A 1 to 6 character identifier to use for the server. - default: None - required: False - aliases: [] - network_id: - description: - - The network UUID on which to create servers. - default: None - required: False - aliases: [] - packages: - description: - - Blueprints to run on the server after its created. - default: [] - required: False - aliases: [] - password: - description: - - Password for the administrator user - default: None - required: False - aliases: [] - primary_dns: - description: - - Primary DNS used by the server. - default: None - required: False - aliases: [] - public_ip_protocol: - description: - - The protocol to use for the public ip if add_public_ip is set to True. - default: 'TCP' - required: False - aliases: [] - public_ip_ports: - description: - - A list of ports to allow on the firewall to thes servers public ip, if add_public_ip is set to True. - default: [] - required: False - aliases: [] - secondary_dns: - description: - - Secondary DNS used by the server. - default: None - required: False - aliases: [] - server_ids: - description: - - Required for started, stopped, and absent states. A list of server Ids to insure are started, stopped, or absent. - default: [] - required: False - aliases: [] - source_server_password: - description: - - The password for the source server if a clone is specified. - default: None - required: False - aliases: [] - state: - description: - - The state to insure that the provided resources are in. - default: 'present' - required: False - choices: ['present', 'absent', 'started', 'stopped'] - aliases: [] - storage_type: - description: - - The type of storage to attach to the server. - default: 'standard' - required: False - choices: ['standard', 'hyperscale'] - aliases: [] - template: - description: - - The template to use for server creation. Will search for a template if a partial string is provided. - default: None - required: false - aliases: [] - ttl: - description: - - The time to live for the server in seconds. The server will be deleted when this time expires. - default: None - required: False - aliases: [] - type: - description: - - The type of server to create. - default: 'standard' - required: False - choices: ['standard', 'hyperscale'] - aliases: [] - wait: - description: - - Whether to wait for the provisioning tasks to finish before returning. - default: True - required: False - choices: [ True, False] - aliases: [] -''' - -EXAMPLES = ''' -# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples - -- name: Provision a single Ubuntu Server - clc_server: - name: test - template: ubuntu-14-64 - count: 1 - group: 'Default Group' - state: present - -- name: Ensure 'Default Group' has exactly 5 servers - clc_server: - name: test - template: ubuntu-14-64 - exact_count: 5 - count_group: 'Default Group' - group: 'Default Group' - -- name: Stop a Server - clc_server: - server_ids: ['UC1ACCTTEST01'] - state: stopped - -- name: Start a Server - clc_server: - server_ids: ['UC1ACCTTEST01'] - state: started - -- name: Delete a Server - clc_server: - server_ids: ['UC1ACCTTEST01'] - state: absent -''' - -__version__ = '${version}' - -import requests -from time import sleep - -# -# Requires the clc-python-sdk. -# sudo pip install clc-sdk -# -try: - import clc as clc_sdk - from clc import CLCException - from clc import APIFailedResponse -except ImportError: - CLC_FOUND = False - clc_sdk = None -else: - CLC_FOUND = True - - -class ClcServer(): - clc = clc_sdk - - def __init__(self, module): - """ - Construct module - """ - self.clc = clc_sdk - self.module = module - self.group_dict = {} - - if not CLC_FOUND: - self.module.fail_json( - msg='clc-python-sdk required for this module') - - self._set_user_agent(self.clc) - - def process_request(self): - """ - Process the request - Main Code Path - :return: Returns with either an exit_json or fail_json - """ - self._set_clc_credentials_from_env() - - self.module.params = ClcServer._validate_module_params(self.clc, - self.module) - p = self.module.params - state = p.get('state') - - # - # Handle each state - # - - if state == 'absent': - server_ids = p['server_ids'] - if not isinstance(server_ids, list): - self.module.fail_json( - msg='server_ids needs to be a list of instances to delete: %s' % - server_ids) - - (changed, - server_dict_array, - new_server_ids) = ClcServer._delete_servers(module=self.module, - clc=self.clc, - server_ids=server_ids) - - elif state in ('started', 'stopped'): - server_ids = p.get('server_ids') - if not isinstance(server_ids, list): - self.module.fail_json( - msg='server_ids needs to be a list of servers to run: %s' % - server_ids) - - (changed, - server_dict_array, - new_server_ids) = ClcServer._startstop_servers(self.module, - self.clc, - server_ids) - - elif state == 'present': - # Changed is always set to true when provisioning new instances - if not p.get('template'): - self.module.fail_json( - msg='template parameter is required for new instance') - - if p.get('exact_count') is None: - (server_dict_array, - new_server_ids, - changed) = ClcServer._create_servers(self.module, - self.clc) - else: - (server_dict_array, - new_server_ids, - changed) = ClcServer._enforce_count(self.module, - self.clc) - - self.module.exit_json( - changed=changed, - server_ids=new_server_ids, - servers=server_dict_array) - - @staticmethod - def _define_module_argument_spec(): - """ - Define the argument spec for the ansible module - :return: argument spec dictionary - """ - argument_spec = dict(name=dict(), - template=dict(), - group=dict(default='Default Group'), - network_id=dict(), - location=dict(default=None), - cpu=dict(default=1), - memory=dict(default='1'), - alias=dict(default=None), - password=dict(default=None), - ip_address=dict(default=None), - storage_type=dict(default='standard'), - type=dict( - default='standard', - choices=[ - 'standard', - 'hyperscale']), - primary_dns=dict(default=None), - secondary_dns=dict(default=None), - additional_disks=dict(type='list', default=[]), - custom_fields=dict(type='list', default=[]), - ttl=dict(default=None), - managed_os=dict(type='bool', default=False), - description=dict(default=None), - source_server_password=dict(default=None), - cpu_autoscale_policy_id=dict(default=None), - anti_affinity_policy_id=dict(default=None), - anti_affinity_policy_name=dict(default=None), - alert_policy_id=dict(default=None), - alert_policy_name=dict(default=None), - packages=dict(type='list', default=[]), - state=dict( - default='present', - choices=[ - 'present', - 'absent', - 'started', - 'stopped']), - count=dict(type='int', default='1'), - exact_count=dict(type='int', default=None), - count_group=dict(), - server_ids=dict(type='list'), - add_public_ip=dict(type='bool', default=False), - public_ip_protocol=dict(default='TCP'), - public_ip_ports=dict(type='list'), - wait=dict(type='bool', default=True)) - - mutually_exclusive = [ - ['exact_count', 'count'], - ['exact_count', 'state'], - ['anti_affinity_policy_id', 'anti_affinity_policy_name'], - ['alert_policy_id', 'alert_policy_name'], - ] - return {"argument_spec": argument_spec, - "mutually_exclusive": mutually_exclusive} - - def _set_clc_credentials_from_env(self): - """ - Set the CLC Credentials on the sdk by reading environment variables - :return: none - """ - env = os.environ - v2_api_token = env.get('CLC_V2_API_TOKEN', False) - v2_api_username = env.get('CLC_V2_API_USERNAME', False) - v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) - clc_alias = env.get('CLC_ACCT_ALIAS', False) - api_url = env.get('CLC_V2_API_URL', False) - - if api_url: - self.clc.defaults.ENDPOINT_URL_V2 = api_url - - if v2_api_token and clc_alias: - self.clc._LOGIN_TOKEN_V2 = v2_api_token - self.clc._V2_ENABLED = True - self.clc.ALIAS = clc_alias - elif v2_api_username and v2_api_passwd: - self.clc.v2.SetCredentials( - api_username=v2_api_username, - api_passwd=v2_api_passwd) - else: - return self.module.fail_json( - msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " - "environment variables") - - @staticmethod - def _validate_module_params(clc, module): - """ - Validate the module params, and lookup default values. - :param clc: clc-sdk instance to use - :param module: module to validate - :return: dictionary of validated params - """ - params = module.params - datacenter = ClcServer._find_datacenter(clc, module) - - ClcServer._validate_types(module) - ClcServer._validate_name(module) - - params['alias'] = ClcServer._find_alias(clc, module) - params['cpu'] = ClcServer._find_cpu(clc, module) - params['memory'] = ClcServer._find_memory(clc, module) - params['description'] = ClcServer._find_description(module) - params['ttl'] = ClcServer._find_ttl(clc, module) - params['template'] = ClcServer._find_template_id(module, datacenter) - params['group'] = ClcServer._find_group(module, datacenter).id - params['network_id'] = ClcServer._find_network_id(module, datacenter) - - return params - - @staticmethod - def _find_datacenter(clc, module): - """ - Find the datacenter by calling the CLC API. - :param clc: clc-sdk instance to use - :param module: module to validate - :return: clc-sdk.Datacenter instance - """ - location = module.params.get('location') - try: - datacenter = clc.v2.Datacenter(location) - return datacenter - except CLCException: - module.fail_json(msg=str("Unable to find location: " + location)) - - @staticmethod - def _find_alias(clc, module): - """ - Find or Validate the Account Alias by calling the CLC API - :param clc: clc-sdk instance to use - :param module: module to validate - :return: clc-sdk.Account instance - """ - alias = module.params.get('alias') - if not alias: - alias = clc.v2.Account.GetAlias() - return alias - - @staticmethod - def _find_cpu(clc, module): - """ - Find or validate the CPU value by calling the CLC API - :param clc: clc-sdk instance to use - :param module: module to validate - :return: Int value for CPU - """ - cpu = module.params.get('cpu') - group_id = module.params.get('group_id') - alias = module.params.get('alias') - state = module.params.get('state') - - if not cpu and state == 'present': - group = clc.v2.Group(id=group_id, - alias=alias) - if group.Defaults("cpu"): - cpu = group.Defaults("cpu") - else: - module.fail_json( - msg=str("Cannot determine a default cpu value. Please provide a value for cpu.")) - return cpu - - @staticmethod - def _find_memory(clc, module): - """ - Find or validate the Memory value by calling the CLC API - :param clc: clc-sdk instance to use - :param module: module to validate - :return: Int value for Memory - """ - memory = module.params.get('memory') - group_id = module.params.get('group_id') - alias = module.params.get('alias') - state = module.params.get('state') - - if not memory and state == 'present': - group = clc.v2.Group(id=group_id, - alias=alias) - if group.Defaults("memory"): - memory = group.Defaults("memory") - else: - module.fail_json(msg=str( - "Cannot determine a default memory value. Please provide a value for memory.")) - return memory - - @staticmethod - def _find_description(module): - """ - Set the description module param to name if description is blank - :param module: the module to validate - :return: string description - """ - description = module.params.get('description') - if not description: - description = module.params.get('name') - return description - - @staticmethod - def _validate_types(module): - """ - Validate that type and storage_type are set appropriately, and fail if not - :param module: the module to validate - :return: none - """ - state = module.params.get('state') - type = module.params.get( - 'type').lower() if module.params.get('type') else None - storage_type = module.params.get( - 'storage_type').lower() if module.params.get('storage_type') else None - - if state == "present": - if type == "standard" and storage_type not in ( - "standard", "premium"): - module.fail_json( - msg=str("Standard VMs must have storage_type = 'standard' or 'premium'")) - - if type == "hyperscale" and storage_type != "hyperscale": - module.fail_json( - msg=str("Hyperscale VMs must have storage_type = 'hyperscale'")) - - @staticmethod - def _find_ttl(clc, module): - """ - Validate that TTL is > 3600 if set, and fail if not - :param clc: clc-sdk instance to use - :param module: module to validate - :return: validated ttl - """ - ttl = module.params.get('ttl') - - if ttl: - if ttl <= 3600: - module.fail_json(msg=str("Ttl cannot be <= 3600")) - else: - ttl = clc.v2.time_utils.SecondsToZuluTS(int(time.time()) + ttl) - return ttl - - @staticmethod - def _find_template_id(module, datacenter): - """ - Find the template id by calling the CLC API. - :param module: the module to validate - :param datacenter: the datacenter to search for the template - :return: a valid clc template id - """ - lookup_template = module.params.get('template') - state = module.params.get('state') - result = None - - if state == 'present': - try: - result = datacenter.Templates().Search(lookup_template)[0].id - except CLCException: - module.fail_json( - msg=str( - "Unable to find a template: " + - lookup_template + - " in location: " + - datacenter.id)) - return result - - @staticmethod - def _find_network_id(module, datacenter): - """ - Validate the provided network id or return a default. - :param module: the module to validate - :param datacenter: the datacenter to search for a network id - :return: a valid network id - """ - network_id = module.params.get('network_id') - - if not network_id: - try: - network_id = datacenter.Networks().networks[0].id - except CLCException: - module.fail_json( - msg=str( - "Unable to find a network in location: " + - datacenter.id)) - - return network_id - - @staticmethod - def _create_servers(module, clc, override_count=None): - """ - Create New Servers - :param module: the AnsibleModule object - :param clc: the clc-sdk instance to use - :return: a list of dictionaries with server information about the servers that were created - """ - p = module.params - requests = [] - servers = [] - server_dict_array = [] - created_server_ids = [] - - add_public_ip = p.get('add_public_ip') - public_ip_protocol = p.get('public_ip_protocol') - public_ip_ports = p.get('public_ip_ports') - wait = p.get('wait') - - params = { - 'name': p.get('name'), - 'template': p.get('template'), - 'group_id': p.get('group'), - 'network_id': p.get('network_id'), - 'cpu': p.get('cpu'), - 'memory': p.get('memory'), - 'alias': p.get('alias'), - 'password': p.get('password'), - 'ip_address': p.get('ip_address'), - 'storage_type': p.get('storage_type'), - 'type': p.get('type'), - 'primary_dns': p.get('primary_dns'), - 'secondary_dns': p.get('secondary_dns'), - 'additional_disks': p.get('additional_disks'), - 'custom_fields': p.get('custom_fields'), - 'ttl': p.get('ttl'), - 'managed_os': p.get('managed_os'), - 'description': p.get('description'), - 'source_server_password': p.get('source_server_password'), - 'cpu_autoscale_policy_id': p.get('cpu_autoscale_policy_id'), - 'anti_affinity_policy_id': p.get('anti_affinity_policy_id'), - 'anti_affinity_policy_name': p.get('anti_affinity_policy_name'), - 'packages': p.get('packages') - } - - count = override_count if override_count else p.get('count') - - changed = False if count == 0 else True - - if changed: - for i in range(0, count): - if not module.check_mode: - req = ClcServer._create_clc_server(clc=clc, - module=module, - server_params=params) - server = req.requests[0].Server() - requests.append(req) - servers.append(server) - - ClcServer._wait_for_requests(clc, requests, servers, wait) - - ClcServer._add_public_ip_to_servers( - should_add_public_ip=add_public_ip, - servers=servers, - public_ip_protocol=public_ip_protocol, - public_ip_ports=public_ip_ports, - wait=wait) - ClcServer._add_alert_policy_to_servers(clc=clc, - module=module, - servers=servers) - - for server in servers: - # reload server details - server = clc.v2.Server(server.id) - - server.data['ipaddress'] = server.details[ - 'ipAddresses'][0]['internal'] - - if add_public_ip and len(server.PublicIPs().public_ips) > 0: - server.data['publicip'] = str( - server.PublicIPs().public_ips[0]) - - server_dict_array.append(server.data) - created_server_ids.append(server.id) - - return server_dict_array, created_server_ids, changed - - @staticmethod - def _validate_name(module): - """ - Validate that name is the correct length if provided, fail if it's not - :param module: the module to validate - :return: none - """ - name = module.params.get('name') - state = module.params.get('state') - - if state == 'present' and (len(name) < 1 or len(name) > 6): - module.fail_json(msg=str( - "When state = 'present', name must be a string with a minimum length of 1 and a maximum length of 6")) - -# -# Functions to execute the module's behaviors -# (called from main()) -# - - @staticmethod - def _enforce_count(module, clc): - """ - Enforce that there is the right number of servers in the provided group. - Starts or stops servers as necessary. - :param module: the AnsibleModule object - :param clc: the clc-sdk instance to use - :return: a list of dictionaries with server information about the servers that were created or deleted - """ - p = module.params - changed_server_ids = None - changed = False - count_group = p.get('count_group') - datacenter = ClcServer._find_datacenter(clc, module) - exact_count = p.get('exact_count') - server_dict_array = [] - - # fail here if the exact count was specified without filtering - # on a group, as this may lead to a undesired removal of instances - if exact_count and count_group is None: - module.fail_json( - msg="you must use the 'count_group' option with exact_count") - - servers, running_servers = ClcServer._find_running_servers_by_group( - module, datacenter, count_group) - - if len(running_servers) == exact_count: - changed = False - - elif len(running_servers) < exact_count: - changed = True - to_create = exact_count - len(running_servers) - server_dict_array, changed_server_ids, changed \ - = ClcServer._create_servers(module, clc, override_count=to_create) - - for server in server_dict_array: - running_servers.append(server) - - elif len(running_servers) > exact_count: - changed = True - to_remove = len(running_servers) - exact_count - all_server_ids = sorted([x.id for x in running_servers]) - remove_ids = all_server_ids[0:to_remove] - - (changed, server_dict_array, changed_server_ids) \ - = ClcServer._delete_servers(module, clc, remove_ids) - - return server_dict_array, changed_server_ids, changed - - @staticmethod - def _wait_for_requests(clc, requests, servers, wait): - """ - Block until server provisioning requests are completed. - :param clc: the clc-sdk instance to use - :param requests: a list of clc-sdk.Request instances - :param servers: a list of servers to refresh - :param wait: a boolean on whether to block or not. This function is skipped if True - :return: none - """ - if wait: - # Requests.WaitUntilComplete() returns the count of failed requests - failed_requests_count = sum( - [request.WaitUntilComplete() for request in requests]) - - if failed_requests_count > 0: - raise clc - else: - ClcServer._refresh_servers(servers) - - @staticmethod - def _refresh_servers(servers): - """ - Loop through a list of servers and refresh them - :param servers: list of clc-sdk.Server instances to refresh - :return: none - """ - for server in servers: - server.Refresh() - - @staticmethod - def _add_public_ip_to_servers( - should_add_public_ip, - servers, - public_ip_protocol, - public_ip_ports, - wait): - """ - Create a public IP for servers - :param should_add_public_ip: boolean - whether or not to provision a public ip for servers. Skipped if False - :param servers: List of servers to add public ips to - :param public_ip_protocol: a protocol to allow for the public ips - :param public_ip_ports: list of ports to allow for the public ips - :param wait: boolean - whether to block until the provisioning requests complete - :return: none - """ - - if should_add_public_ip: - ports_lst = [] - requests = [] - - for port in public_ip_ports: - ports_lst.append( - {'protocol': public_ip_protocol, 'port': port}) - - for server in servers: - requests.append(server.PublicIPs().Add(ports_lst)) - - if wait: - for r in requests: - r.WaitUntilComplete() - - @staticmethod - def _add_alert_policy_to_servers(clc, module, servers): - """ - Associate an alert policy to servers - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param servers: List of servers to add alert policy to - :return: none - """ - p = module.params - alert_policy_id = p.get('alert_policy_id') - alert_policy_name = p.get('alert_policy_name') - alias = p.get('alias') - if not alert_policy_id and alert_policy_name: - alert_policy_id = ClcServer._get_alert_policy_id_by_name( - clc=clc, - module=module, - alias=alias, - alert_policy_name=alert_policy_name - ) - if not alert_policy_id: - module.fail_json( - msg='No alert policy exist with name : %s' - % (alert_policy_name)) - for server in servers: - ClcServer._add_alert_policy_to_server( - clc=clc, - module=module, - alias=alias, - server_id=server.id, - alert_policy_id=alert_policy_id) - - @staticmethod - def _add_alert_policy_to_server(clc, module, alias, server_id, alert_policy_id): - """ - Associate an alert policy to a clc server - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param alias: the clc account alias - :param serverid: The clc server id - :param alert_policy_id: the alert policy id to be associated to the server - :return: none - """ - try: - clc.v2.API.Call( - method='POST', - url='servers/%s/%s/alertPolicies' % (alias, server_id), - payload=json.dumps( - { - 'id': alert_policy_id - })) - except clc.APIFailedResponse as e: - return module.fail_json( - msg='Failed to associate alert policy to the server : %s with Error %s' - % (server_id, str(e.response_text))) - - @staticmethod - def _get_alert_policy_id_by_name(clc, module, alias, alert_policy_name): - """ - Returns the alert policy id for the given alert policy name - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param alias: the clc account alias - :param alert_policy_name: the name of the alert policy - :return: the alert policy id - """ - alert_policy_id = None - policies = clc.v2.API.Call('GET', '/v2/alertPolicies/%s' % (alias)) - if not policies: - return alert_policy_id - for policy in policies.get('items'): - if policy.get('name') == alert_policy_name: - if not alert_policy_id: - alert_policy_id = policy.get('id') - else: - return module.fail_json( - msg='mutiple alert policies were found with policy name : %s' % - (alert_policy_name)) - return alert_policy_id - - - @staticmethod - def _delete_servers(module, clc, server_ids): - """ - Delete the servers on the provided list - :param module: the AnsibleModule object - :param clc: the clc-sdk instance to use - :param server_ids: list of servers to delete - :return: a list of dictionaries with server information about the servers that were deleted - """ - # Whether to wait for termination to complete before returning - p = module.params - wait = p.get('wait') - terminated_server_ids = [] - server_dict_array = [] - requests = [] - - changed = False - if not isinstance(server_ids, list) or len(server_ids) < 1: - module.fail_json( - msg='server_ids should be a list of servers, aborting') - - servers = clc.v2.Servers(server_ids).Servers() - changed = True - - for server in servers: - if not module.check_mode: - requests.append(server.Delete()) - - if wait: - for r in requests: - r.WaitUntilComplete() - - for server in servers: - terminated_server_ids.append(server.id) - - return changed, server_dict_array, terminated_server_ids - - @staticmethod - def _startstop_servers(module, clc, server_ids): - """ - Start or Stop the servers on the provided list - :param module: the AnsibleModule object - :param clc: the clc-sdk instance to use - :param server_ids: list of servers to start or stop - :return: a list of dictionaries with server information about the servers that were started or stopped - """ - p = module.params - wait = p.get('wait') - state = p.get('state') - changed = False - changed_servers = [] - server_dict_array = [] - result_server_ids = [] - requests = [] - - if not isinstance(server_ids, list) or len(server_ids) < 1: - module.fail_json( - msg='server_ids should be a list of servers, aborting') - - servers = clc.v2.Servers(server_ids).Servers() - for server in servers: - if server.powerState != state: - changed_servers.append(server) - if not module.check_mode: - requests.append( - ClcServer._change_server_power_state( - module, - server, - state)) - changed = True - - if wait: - for r in requests: - r.WaitUntilComplete() - for server in changed_servers: - server.Refresh() - - for server in changed_servers: - server_dict_array.append(server.data) - result_server_ids.append(server.id) - - return changed, server_dict_array, result_server_ids - - @staticmethod - def _change_server_power_state(module, server, state): - """ - Change the server powerState - :param module: the module to check for intended state - :param server: the server to start or stop - :param state: the intended powerState for the server - :return: the request object from clc-sdk call - """ - result = None - try: - if state == 'started': - result = server.PowerOn() - else: - result = server.PowerOff() - except: - module.fail_json( - msg='Unable to change state for server {0}'.format( - server.id)) - return result - return result - - @staticmethod - def _find_running_servers_by_group(module, datacenter, count_group): - """ - Find a list of running servers in the provided group - :param module: the AnsibleModule object - :param datacenter: the clc-sdk.Datacenter instance to use to lookup the group - :param count_group: the group to count the servers - :return: list of servers, and list of running servers - """ - group = ClcServer._find_group( - module=module, - datacenter=datacenter, - lookup_group=count_group) - - servers = group.Servers().Servers() - running_servers = [] - - for server in servers: - if server.status == 'active' and server.powerState == 'started': - running_servers.append(server) - - return servers, running_servers - - @staticmethod - def _find_group(module, datacenter, lookup_group=None): - """ - Find a server group in a datacenter by calling the CLC API - :param module: the AnsibleModule instance - :param datacenter: clc-sdk.Datacenter instance to search for the group - :param lookup_group: string name of the group to search for - :return: clc-sdk.Group instance - """ - result = None - if not lookup_group: - lookup_group = module.params.get('group') - try: - return datacenter.Groups().Get(lookup_group) - except: - pass - - # The search above only acts on the main - result = ClcServer._find_group_recursive( - module, - datacenter.Groups(), - lookup_group) - - if result is None: - module.fail_json( - msg=str( - "Unable to find group: " + - lookup_group + - " in location: " + - datacenter.id)) - - return result - - @staticmethod - def _find_group_recursive(module, group_list, lookup_group): - """ - Find a server group by recursively walking the tree - :param module: the AnsibleModule instance to use - :param group_list: a list of groups to search - :param lookup_group: the group to look for - :return: list of groups - """ - result = None - for group in group_list.groups: - subgroups = group.Subgroups() - try: - return subgroups.Get(lookup_group) - except: - result = ClcServer._find_group_recursive( - module, - subgroups, - lookup_group) - - if result is not None: - break - - return result - - @staticmethod - def _create_clc_server( - clc, - module, - server_params): - """ - Call the CLC Rest API to Create a Server - :param clc: the clc-python-sdk instance to use - :param server_params: a dictionary of params to use to create the servers - :return: clc-sdk.Request object linked to the queued server request - """ - - aa_policy_id = server_params.get('anti_affinity_policy_id') - aa_policy_name = server_params.get('anti_affinity_policy_name') - if not aa_policy_id and aa_policy_name: - aa_policy_id = ClcServer._get_anti_affinity_policy_id( - clc, - module, - server_params.get('alias'), - aa_policy_name) - - res = clc.v2.API.Call( - method='POST', - url='servers/%s' % - (server_params.get('alias')), - payload=json.dumps( - { - 'name': server_params.get('name'), - 'description': server_params.get('description'), - 'groupId': server_params.get('group_id'), - 'sourceServerId': server_params.get('template'), - 'isManagedOS': server_params.get('managed_os'), - 'primaryDNS': server_params.get('primary_dns'), - 'secondaryDNS': server_params.get('secondary_dns'), - 'networkId': server_params.get('network_id'), - 'ipAddress': server_params.get('ip_address'), - 'password': server_params.get('password'), - 'sourceServerPassword': server_params.get('source_server_password'), - 'cpu': server_params.get('cpu'), - 'cpuAutoscalePolicyId': server_params.get('cpu_autoscale_policy_id'), - 'memoryGB': server_params.get('memory'), - 'type': server_params.get('type'), - 'storageType': server_params.get('storage_type'), - 'antiAffinityPolicyId': aa_policy_id, - 'customFields': server_params.get('custom_fields'), - 'additionalDisks': server_params.get('additional_disks'), - 'ttl': server_params.get('ttl'), - 'packages': server_params.get('packages')})) - - result = clc.v2.Requests(res) - - # - # Patch the Request object so that it returns a valid server - - # Find the server's UUID from the API response - server_uuid = [obj['id'] - for obj in res['links'] if obj['rel'] == 'self'][0] - - # Change the request server method to a _find_server_by_uuid closure so - # that it will work - result.requests[0].Server = lambda: ClcServer._find_server_by_uuid_w_retry( - clc, - module, - server_uuid, - server_params.get('alias')) - - return result - - @staticmethod - def _get_anti_affinity_policy_id(clc, module, alias, aa_policy_name): - """ - retrieves the anti affinity policy id of the server based on the name of the policy - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param alias: the CLC account alias - :param aa_policy_name: the anti affinity policy name - :return: aa_policy_id: The anti affinity policy id - """ - aa_policy_id = None - aa_policies = clc.v2.API.Call(method='GET', - url='antiAffinityPolicies/%s' % (alias)) - for aa_policy in aa_policies.get('items'): - if aa_policy.get('name') == aa_policy_name: - if not aa_policy_id: - aa_policy_id = aa_policy.get('id') - else: - return module.fail_json( - msg='mutiple anti affinity policies were found with policy name : %s' % - (aa_policy_name)) - if not aa_policy_id: - return module.fail_json( - msg='No anti affinity policy was found with policy name : %s' % - (aa_policy_name)) - return aa_policy_id - - # - # This is the function that gets patched to the Request.server object using a lamda closure - # - - @staticmethod - def _find_server_by_uuid_w_retry( - clc, module, svr_uuid, alias=None, retries=5, backout=2): - """ - Find the clc server by the UUID returned from the provisioning request. Retry the request if a 404 is returned. - :param clc: the clc-sdk instance to use - :param svr_uuid: UUID of the server - :param alias: the Account Alias to search - :return: a clc-sdk.Server instance - """ - if not alias: - alias = clc.v2.Account.GetAlias() - - # Wait and retry if the api returns a 404 - while True: - retries -= 1 - try: - server_obj = clc.v2.API.Call( - method='GET', url='servers/%s/%s?uuid=true' % - (alias, svr_uuid)) - server_id = server_obj['id'] - server = clc.v2.Server( - id=server_id, - alias=alias, - server_obj=server_obj) - return server - - except APIFailedResponse as e: - if e.response_status_code != 404: - module.fail_json( - msg='A failure response was received from CLC API when ' - 'attempting to get details for a server: UUID=%s, Code=%i, Message=%s' % - (svr_uuid, e.response_status_code, e.message)) - return - if retries == 0: - module.fail_json( - msg='Unable to reach the CLC API after 5 attempts') - return - - sleep(backout) - backout = backout * 2 - - @staticmethod - def _set_user_agent(clc): - if hasattr(clc, 'SetRequestsSession'): - agent_string = "ClcAnsibleModule/" + __version__ - ses = requests.Session() - ses.headers.update({"Api-Client": agent_string}) - ses.headers['User-Agent'] += " " + agent_string - clc.SetRequestsSession(ses) - - -def main(): - """ - The main function. Instantiates the module and calls process_request. - :return: none - """ - argument_dict = ClcServer._define_module_argument_spec() - module = AnsibleModule(supports_check_mode=True, **argument_dict) - clc_server = ClcServer(module) - clc_server.process_request() - -from ansible.module_utils.basic import * # pylint: disable=W0614 -if __name__ == '__main__': - main() diff --git a/cloud/centurylink/clc_server_snapshot.py b/cloud/centurylink/clc_server_snapshot.py deleted file mode 100644 index 9ca1474f248..00000000000 --- a/cloud/centurylink/clc_server_snapshot.py +++ /dev/null @@ -1,341 +0,0 @@ -#!/usr/bin/python - -# CenturyLink Cloud Ansible Modules. -# -# These Ansible modules enable the CenturyLink Cloud v2 API to be called -# from an within Ansible Playbook. -# -# This file is part of CenturyLink Cloud, and is maintained -# by the Workflow as a Service Team -# -# Copyright 2015 CenturyLink Cloud -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# CenturyLink Cloud: http://www.CenturyLinkCloud.com -# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ -# - -DOCUMENTATION = ''' -module: clc_server -short_desciption: Create, Delete and Restore server snapshots in CenturyLink Cloud. -description: - - An Ansible module to Create, Delete and Restore server snapshots in CenturyLink Cloud. -options: - server_ids: - description: - - A list of server Ids to snapshot. - default: [] - required: True - aliases: [] - expiration_days: - description: - - The number of days to keep the server snapshot before it expires. - default: 7 - required: False - aliases: [] - state: - description: - - The state to insure that the provided resources are in. - default: 'present' - required: False - choices: ['present', 'absent', 'restore'] - aliases: [] - wait: - description: - - Whether to wait for the provisioning tasks to finish before returning. - default: True - required: False - choices: [ True, False] - aliases: [] -''' - -EXAMPLES = ''' -# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples - -- name: Create server snapshot - clc_server_snapshot: - server_ids: - - UC1WFSDTEST01 - - UC1WFSDTEST02 - expiration_days: 10 - wait: True - state: present - -- name: Restore server snapshot - clc_server_snapshot: - server_ids: - - UC1WFSDTEST01 - - UC1WFSDTEST02 - wait: True - state: restore - -- name: Delete server snapshot - clc_server_snapshot: - server_ids: - - UC1WFSDTEST01 - - UC1WFSDTEST02 - wait: True - state: absent -''' - -__version__ = '${version}' - -import requests - -# -# Requires the clc-python-sdk. -# sudo pip install clc-sdk -# -try: - import clc as clc_sdk - from clc import CLCException -except ImportError: - clc_found = False - clc_sdk = None -else: - CLC_FOUND = True - - -class ClcSnapshot(): - - clc = clc_sdk - module = None - - def __init__(self, module): - """ - Construct module - """ - self.module = module - if not CLC_FOUND: - self.module.fail_json( - msg='clc-python-sdk required for this module') - - self._set_user_agent(self.clc) - - def process_request(self): - """ - Process the request - Main Code Path - :return: Returns with either an exit_json or fail_json - """ - p = self.module.params - - if not CLC_FOUND: - self.module.fail_json( - msg='clc-python-sdk required for this module') - - server_ids = p['server_ids'] - expiration_days = p['expiration_days'] - state = p['state'] - - if not server_ids: - return self.module.fail_json(msg='List of Server ids are required') - - self._set_clc_credentials_from_env() - if state == 'present': - changed, requests, changed_servers = self.ensure_server_snapshot_present(server_ids=server_ids, - expiration_days=expiration_days) - elif state == 'absent': - changed, requests, changed_servers = self.ensure_server_snapshot_absent( - server_ids=server_ids) - elif state == 'restore': - changed, requests, changed_servers = self.ensure_server_snapshot_restore( - server_ids=server_ids) - else: - return self.module.fail_json(msg="Unknown State: " + state) - - self._wait_for_requests_to_complete(requests) - return self.module.exit_json( - changed=changed, - server_ids=changed_servers) - - def ensure_server_snapshot_present(self, server_ids, expiration_days): - """ - Ensures the given set of server_ids have the snapshots created - :param server_ids: The list of server_ids to create the snapshot - :param expiration_days: The number of days to keep the snapshot - :return: (changed, result, changed_servers) - changed: A flag indicating whether any change was made - result: the list of clc request objects from CLC API call - changed_servers: The list of servers ids that are modified - """ - result = [] - changed = False - servers = self._get_servers_from_clc( - server_ids, - 'Failed to obtain server list from the CLC API') - servers_to_change = [ - server for server in servers if len( - server.GetSnapshots()) == 0] - for server in servers_to_change: - changed = True - if not self.module.check_mode: - res = server.CreateSnapshot( - delete_existing=True, - expiration_days=expiration_days) - result.append(res) - changed_servers = [ - server.id for server in servers_to_change if server.id] - return changed, result, changed_servers - - def ensure_server_snapshot_absent(self, server_ids): - """ - Ensures the given set of server_ids have the snapshots removed - :param server_ids: The list of server_ids to delete the snapshot - :return: (changed, result, changed_servers) - changed: A flag indicating whether any change was made - result: the list of clc request objects from CLC API call - changed_servers: The list of servers ids that are modified - """ - result = [] - changed = False - servers = self._get_servers_from_clc( - server_ids, - 'Failed to obtain server list from the CLC API') - servers_to_change = [ - server for server in servers if len( - server.GetSnapshots()) > 0] - for server in servers_to_change: - changed = True - if not self.module.check_mode: - res = server.DeleteSnapshot() - result.append(res) - changed_servers = [ - server.id for server in servers_to_change if server.id] - return changed, result, changed_servers - - def ensure_server_snapshot_restore(self, server_ids): - """ - Ensures the given set of server_ids have the snapshots restored - :param server_ids: The list of server_ids to delete the snapshot - :return: (changed, result, changed_servers) - changed: A flag indicating whether any change was made - result: the list of clc request objects from CLC API call - changed_servers: The list of servers ids that are modified - """ - result = [] - changed = False - servers = self._get_servers_from_clc( - server_ids, - 'Failed to obtain server list from the CLC API') - servers_to_change = [ - server for server in servers if len( - server.GetSnapshots()) > 0] - for server in servers_to_change: - changed = True - if not self.module.check_mode: - res = server.RestoreSnapshot() - result.append(res) - changed_servers = [ - server.id for server in servers_to_change if server.id] - return changed, result, changed_servers - - def _wait_for_requests_to_complete(self, requests_lst): - """ - Waits until the CLC requests are complete if the wait argument is True - :param requests_lst: The list of CLC request objects - :return: none - """ - if not self.module.params['wait']: - return - for request in requests_lst: - request.WaitUntilComplete() - for request_details in request.requests: - if request_details.Status() != 'succeeded': - self.module.fail_json( - msg='Unable to process server snapshot request') - - @staticmethod - def define_argument_spec(): - """ - This function defnines the dictionary object required for - package module - :return: the package dictionary object - """ - argument_spec = dict( - server_ids=dict(type='list', required=True), - expiration_days=dict(default=7), - wait=dict(default=True), - state=dict( - default='present', - choices=[ - 'present', - 'absent', - 'restore']), - ) - return argument_spec - - def _get_servers_from_clc(self, server_list, message): - """ - Internal function to fetch list of CLC server objects from a list of server ids - :param the list server ids - :return the list of CLC server objects - """ - try: - return self.clc.v2.Servers(server_list).servers - except CLCException as ex: - return self.module.fail_json(msg=message + ': %s' % ex) - - def _set_clc_credentials_from_env(self): - """ - Set the CLC Credentials on the sdk by reading environment variables - :return: none - """ - env = os.environ - v2_api_token = env.get('CLC_V2_API_TOKEN', False) - v2_api_username = env.get('CLC_V2_API_USERNAME', False) - v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) - clc_alias = env.get('CLC_ACCT_ALIAS', False) - api_url = env.get('CLC_V2_API_URL', False) - - if api_url: - self.clc.defaults.ENDPOINT_URL_V2 = api_url - - if v2_api_token and clc_alias: - self.clc._LOGIN_TOKEN_V2 = v2_api_token - self.clc._V2_ENABLED = True - self.clc.ALIAS = clc_alias - elif v2_api_username and v2_api_passwd: - self.clc.v2.SetCredentials( - api_username=v2_api_username, - api_passwd=v2_api_passwd) - else: - return self.module.fail_json( - msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " - "environment variables") - - @staticmethod - def _set_user_agent(clc): - if hasattr(clc, 'SetRequestsSession'): - agent_string = "ClcAnsibleModule/" + __version__ - ses = requests.Session() - ses.headers.update({"Api-Client": agent_string}) - ses.headers['User-Agent'] += " " + agent_string - clc.SetRequestsSession(ses) - - -def main(): - """ - Main function - :return: None - """ - module = AnsibleModule( - argument_spec=ClcSnapshot.define_argument_spec(), - supports_check_mode=True - ) - clc_snapshot = ClcSnapshot(module) - clc_snapshot.process_request() - -from ansible.module_utils.basic import * -if __name__ == '__main__': - main() From 25d61b7d01d5da493a4cc3cebaec8a4e5148d07e Mon Sep 17 00:00:00 2001 From: Igor Gnatenko Date: Fri, 22 May 2015 21:09:01 +0300 Subject: [PATCH 0398/2522] remove all and start from scratch Signed-off-by: Igor Gnatenko --- packaging/os/dnf.py | 652 ++------------------------------------------ 1 file changed, 25 insertions(+), 627 deletions(-) diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index 7afbee44c54..b3fa60bef42 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -1,7 +1,7 @@ #!/usr/bin/python -tt # -*- coding: utf-8 -*- -# Written by Cristian van Ee +# Written by Igor Gnatenko # # This file is part of Ansible # @@ -92,10 +92,8 @@ notes: [] # informational: requirements for nodes -requirements: - - dnf - - yum-utils (for repoquery) -author: "Cristian van Ee (@DJMuggs)" +requirements: [ dnf ] +author: '"Igor Gnatenko" ' ''' EXAMPLES = ''' @@ -122,24 +120,15 @@ ''' -def_qf = "%{name}-%{version}-%{release}.%{arch}" - -repoquery='/usr/bin/repoquery' -if not os.path.exists(repoquery): - repoquery = None - -dnfbin='/usr/bin/dnf' - import syslog def log(msg): syslog.openlog('ansible-dnf', 0, syslog.LOG_USER) syslog.syslog(syslog.LOG_NOTICE, msg) -def dnf_base(conf_file=None, cachedir=False): - +def dnf_base(conf_file=None): my = dnf.Base() - my.conf.debuglevel=0 + my.conf.debuglevel = 0 if conf_file and os.path.exists(conf_file): my.conf.config_file_path = conf_file my.conf.read() @@ -148,629 +137,42 @@ def dnf_base(conf_file=None, cachedir=False): return my -def install_dnf_utils(module): - - if not module.check_mode: - dnf_path = module.get_bin_path('dnf') - if dnf_path: - rc, so, se = module.run_command('%s -y install yum-utils' % dnf_path) - if rc == 0: - this_path = module.get_bin_path('repoquery') - global repoquery - repoquery = this_path - -def po_to_nevra(po): - - if hasattr(po, 'ui_nevra'): - return po.ui_nevra - else: - return '%s-%s-%s.%s' % (po.name, po.version, po.release, po.arch) - -def is_installed(module, repoq, pkgspec, conf_file, qf=def_qf, en_repos=[], dis_repos=[], is_pkg=False): - - if not repoq: - - pkgs = [] - try: - my = dnf_base(conf_file) - for rid in en_repos: - my.repos.enableRepo(rid) - for rid in dis_repos: - my.repos.disableRepo(rid) - - e,m,u = my.rpmdb.matchPackageNames([pkgspec]) - pkgs = e + m - if not pkgs: - pkgs.extend(my.returnInstalledPackagesByDep(pkgspec)) - except Exception, e: - module.fail_json(msg="Failure talking to dnf: %s" % e) - - return [ po_to_nevra(p) for p in pkgs ] - - else: - - cmd = repoq + ["--disablerepo=*", "--pkgnarrow=installed", "--qf", qf, pkgspec] - rc,out,err = module.run_command(cmd) - if not is_pkg: - cmd = repoq + ["--disablerepo=*", "--pkgnarrow=installed", "--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)) - - return [] - -def is_available(module, repoq, pkgspec, conf_file, qf=def_qf, en_repos=[], dis_repos=[]): - - if not repoq: - - pkgs = [] - try: - my = dnf_base(conf_file) - for rid in en_repos: - my.repos.enableRepo(rid) - for rid in dis_repos: - my.repos.disableRepo(rid) - - e,m,u = my.pkgSack.matchPackageNames([pkgspec]) - pkgs = e + m - if not pkgs: - pkgs.extend(my.returnPackagesByDep(pkgspec)) - except Exception, e: - module.fail_json(msg="Failure talking to dnf: %s" % e) - - return [ po_to_nevra(p) for p in pkgs ] - - else: - myrepoq = list(repoq) - - for repoid in dis_repos: - r_cmd = ['--disablerepo', repoid] - myrepoq.extend(r_cmd) - - for repoid in en_repos: - r_cmd = ['--enablerepo', repoid] - myrepoq.extend(r_cmd) - - cmd = myrepoq + ["--qf", qf, pkgspec] - rc,out,err = module.run_command(cmd) - if rc == 0: - return [ p for p in out.split('\n') if p.strip() ] - else: - module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err)) - - - return [] - -def is_update(module, repoq, pkgspec, conf_file, qf=def_qf, en_repos=[], dis_repos=[]): - - if not repoq: - - retpkgs = [] - pkgs = [] - updates = [] - - try: - my = dnf_base(conf_file) - for rid in en_repos: - my.repos.enableRepo(rid) - for rid in dis_repos: - my.repos.disableRepo(rid) - - pkgs = my.returnPackagesByDep(pkgspec) + my.returnInstalledPackagesByDep(pkgspec) - if not pkgs: - e,m,u = my.pkgSack.matchPackageNames([pkgspec]) - pkgs = e + m - updates = my.doPackageLists(pkgnarrow='updates').updates - except Exception, e: - module.fail_json(msg="Failure talking to dnf: %s" % e) - - for pkg in pkgs: - if pkg in updates: - retpkgs.append(pkg) - - return set([ po_to_nevra(p) for p in retpkgs ]) - - else: - myrepoq = list(repoq) - for repoid in dis_repos: - r_cmd = ['--disablerepo', repoid] - myrepoq.extend(r_cmd) - - for repoid in en_repos: - r_cmd = ['--enablerepo', repoid] - myrepoq.extend(r_cmd) - - cmd = myrepoq + ["--pkgnarrow=updates", "--qf", qf, pkgspec] - rc,out,err = module.run_command(cmd) - - if rc == 0: - return set([ p for p in out.split('\n') if p.strip() ]) - else: - module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err)) - - return [] - -def what_provides(module, repoq, req_spec, conf_file, qf=def_qf, en_repos=[], dis_repos=[]): - - if not repoq: - - pkgs = [] - try: - my = dnf_base(conf_file) - for rid in en_repos: - my.repos.enableRepo(rid) - for rid in dis_repos: - my.repos.disableRepo(rid) - - pkgs = my.returnPackagesByDep(req_spec) + my.returnInstalledPackagesByDep(req_spec) - if not pkgs: - e,m,u = my.pkgSack.matchPackageNames([req_spec]) - pkgs.extend(e) - pkgs.extend(m) - e,m,u = my.rpmdb.matchPackageNames([req_spec]) - pkgs.extend(e) - pkgs.extend(m) - except Exception, e: - module.fail_json(msg="Failure talking to dnf: %s" % e) - - return set([ po_to_nevra(p) for p in pkgs ]) - - else: - myrepoq = list(repoq) - for repoid in dis_repos: - r_cmd = ['--disablerepo', repoid] - myrepoq.extend(r_cmd) - - for repoid in en_repos: - r_cmd = ['--enablerepo', repoid] - myrepoq.extend(r_cmd) - - cmd = myrepoq + ["--qf", qf, "--whatprovides", req_spec] - rc,out,err = module.run_command(cmd) - cmd = myrepoq + ["--qf", qf, req_spec] - rc2,out2,err2 = module.run_command(cmd) - if rc == 0 and rc2 == 0: - out += out2 - pkgs = set([ p for p in out.split('\n') if p.strip() ]) - if not pkgs: - pkgs = is_installed(module, repoq, req_spec, conf_file, qf=qf) - return pkgs - else: - module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err + err2)) - - return [] - -def transaction_exists(pkglist): - """ - checks the package list to see if any packages are - involved in an incomplete transaction +def pkg_to_dict(pkg): + """ + Args: + pkg (hawkey.Package): The package """ - - conflicts = [] - if not transaction_helpers: - return conflicts - - # first, we create a list of the package 'nvreas' - # so we can compare the pieces later more easily - pkglist_nvreas = [] - for pkg in pkglist: - pkglist_nvreas.append(splitFilename(pkg)) - - # next, we build the list of packages that are - # contained within an unfinished transaction - unfinished_transactions = find_unfinished_transactions() - for trans in unfinished_transactions: - steps = find_ts_remaining(trans) - for step in steps: - # the action is install/erase/etc., but we only - # care about the package spec contained in the step - (action, step_spec) = step - (n,v,r,e,a) = splitFilename(step_spec) - # and see if that spec is in the list of packages - # requested for installation/updating - for pkg in pkglist_nvreas: - # if the name and arch match, we're going to assume - # this package is part of a pending transaction - # the label is just for display purposes - label = "%s-%s" % (n,a) - if n == pkg[0] and a == pkg[4]: - if label not in conflicts: - conflicts.append("%s-%s" % (n,a)) - break - return conflicts - -def local_nvra(module, path): - """return nvra of a local rpm passed in""" - - cmd = ['/bin/rpm', '-qp' ,'--qf', - '%{name}-%{version}-%{release}.%{arch}\n', path ] - rc, out, err = module.run_command(cmd) - if rc != 0: - return None - nvra = out.split('\n')[0] - return nvra - -def pkg_to_dict(pkgstr): - - if pkgstr.strip(): - n,e,v,r,a,repo = pkgstr.split('|') - else: - return {'error_parsing': pkgstr} d = { - 'name':n, - 'arch':a, - 'epoch':e, - 'release':r, - 'version':v, - 'repo':repo, - 'nevra': '%s:%s-%s-%s.%s' % (e,n,v,r,a) + 'name': pkg.name, + 'arch': pkg.arch, + 'epoch': str(pkg.epoch), + 'release': pkg.release, + 'version': pkg.version, + 'repo': pkg.repoid, + 'nevra': str(pkg) } - if repo == 'installed': + if pkg.installed: d['dnfstate'] = 'installed' else: d['dnfstate'] = 'available' return d -def repolist(module, repoq, qf="%{repoid}"): - - cmd = repoq + ["--qf", qf, "-a"] - rc,out,err = module.run_command(cmd) - ret = [] - if rc == 0: - ret = set([ p for p in out.split('\n') if p.strip() ]) - return ret - def list_stuff(module, conf_file, stuff): - - qf = "%{name}|%{epoch}|%{version}|%{release}|%{arch}|%{repoid}" - repoq = [repoquery, '--show-duplicates', '--plugins', '--quiet', '-q'] - if conf_file and os.path.exists(conf_file): - repoq += ['-c', conf_file] + my = dnf_base(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 my.sack.query().installed()] 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 my.sack.query().upgrades()] 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 my.sack.query().available()] elif stuff == 'repos': - return [ dict(repoid=name, state='enabled') for name in repolist(module, repoq) if name.strip() ] + return [dict(repoid=repo.id, state='enabled') for repo in my.repos.iter_enabled()] 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() ] - -def install(module, items, repoq, dnf_basecmd, conf_file, en_repos, dis_repos): - - res = {} - res['results'] = [] - res['msg'] = '' - res['rc'] = 0 - res['changed'] = False - - for spec in items: - pkg = None - - # check if pkgspec is installed (if possible for idempotence) - # localpkg - if spec.endswith('.rpm') and '://' not in spec: - # get the pkg name-v-r.arch - if not os.path.exists(spec): - res['msg'] += "No Package file matching '%s' found on system" % spec - module.fail_json(**res) - - nvra = local_nvra(module, spec) - # look for them in the rpmdb - if is_installed(module, repoq, nvra, conf_file, en_repos=en_repos, dis_repos=dis_repos): - # if they are there, skip it - continue - pkg = spec - - # URL - elif '://' in spec: - pkg = spec - - #groups :( - elif spec.startswith('@'): - # complete wild ass guess b/c it's a group - pkg = spec - - # range requires or file-requires or pkgname :( - else: - # most common case is the pkg is already installed and done - # short circuit all the bs - and search for it as a pkg in is_installed - # if you find it then we're done - if not set(['*','?']).intersection(set(spec)): - pkgs = is_installed(module, repoq, spec, conf_file, en_repos=en_repos, dis_repos=dis_repos, is_pkg=True) - if pkgs: - res['results'].append('%s providing %s is already installed' % (pkgs[0], spec)) - continue - - # look up what pkgs provide this - pkglist = what_provides(module, repoq, spec, conf_file, en_repos=en_repos, dis_repos=dis_repos) - if not pkglist: - res['msg'] += "No Package matching '%s' found available, installed or updated" % spec - module.fail_json(**res) - - # if any of the packages are involved in a transaction, fail now - # so that we don't hang on the dnf operation later - conflicts = transaction_exists(pkglist) - if len(conflicts) > 0: - res['msg'] += "The following packages have pending transactions: %s" % ", ".join(conflicts) - module.fail_json(**res) - - # if any of them are installed - # then nothing to do - - found = False - for this in pkglist: - if is_installed(module, repoq, this, conf_file, en_repos=en_repos, dis_repos=dis_repos, is_pkg=True): - found = True - res['results'].append('%s providing %s is already installed' % (this, spec)) - break - - # if the version of the pkg you have installed is not in ANY repo, but there are - # other versions in the repos (both higher and lower) then the previous checks won't work. - # so we check one more time. This really only works for pkgname - not for file provides or virt provides - # but virt provides should be all caught in what_provides on its own. - # highly irritating - if not found: - if is_installed(module, repoq, spec, conf_file, en_repos=en_repos, dis_repos=dis_repos): - found = True - res['results'].append('package providing %s is already installed' % (spec)) - - if found: - continue - - # if not - then pass in the spec as what to install - # we could get here if nothing provides it but that's not - # the error we're catching here - pkg = spec - - cmd = dnf_basecmd + ['install', pkg] - - if module.check_mode: - module.exit_json(changed=True) - - changed = True - - rc, out, err = module.run_command(cmd) - - # Fail on invalid urls: - if (rc == 1 and '://' in spec and ('No package %s available.' % spec in out or 'Cannot open: %s. Skipping.' % spec in err)): - err = 'Package at %s could not be installed' % spec - module.fail_json(changed=False,msg=err,rc=1) - elif (rc != 0 and 'Nothing to do' in err) or 'Nothing to do' in out: - # avoid failing in the 'Nothing To Do' case - # this may happen with an URL spec. - # for an already installed group, - # we get rc = 0 and 'Nothing to do' in out, not in err. - rc = 0 - err = '' - out = '%s: Nothing to do' % spec - changed = False - - res['rc'] += rc - res['results'].append(out) - res['msg'] += err - - # FIXME - if we did an install - go and check the rpmdb to see if it actually installed - # look for the pkg in rpmdb - # look for the pkg via obsoletes - - # accumulate any changes - res['changed'] |= changed - - module.exit_json(**res) - - -def remove(module, items, repoq, dnf_basecmd, conf_file, en_repos, dis_repos): - - res = {} - res['results'] = [] - res['msg'] = '' - res['changed'] = False - res['rc'] = 0 - - for pkg in items: - is_group = False - # group remove - this is doom on a stick - if pkg.startswith('@'): - is_group = True - else: - if not is_installed(module, repoq, pkg, conf_file, en_repos=en_repos, dis_repos=dis_repos): - res['results'].append('%s is not installed' % pkg) - continue - - # run an actual dnf transaction - cmd = dnf_basecmd + ["remove", pkg] - - if module.check_mode: - module.exit_json(changed=True) - - rc, out, err = module.run_command(cmd) - - res['rc'] += rc - res['results'].append(out) - res['msg'] += err - - # compile the results into one batch. If anything is changed - # then mark changed - # at the end - if we've end up failed then fail out of the rest - # of the process - - # at this point we should check to see if the pkg is no longer present - - if not is_group: # we can't sensibly check for a group being uninstalled reliably - # look to see if the pkg shows up from is_installed. If it doesn't - if not is_installed(module, repoq, pkg, conf_file, en_repos=en_repos, dis_repos=dis_repos): - res['changed'] = True - else: - module.fail_json(**res) - - if rc != 0: - module.fail_json(**res) - - module.exit_json(**res) - -def latest(module, items, repoq, dnf_basecmd, conf_file, en_repos, dis_repos): - - res = {} - res['results'] = [] - res['msg'] = '' - res['changed'] = False - res['rc'] = 0 - - for spec in items: - - pkg = None - basecmd = 'update' - cmd = '' - # groups, again - if spec.startswith('@'): - pkg = spec - - elif spec == '*': #update all - # use check-update to see if there is any need - rc,out,err = module.run_command(dnf_basecmd + ['check-update']) - if rc == 100: - cmd = dnf_basecmd + [basecmd] - else: - res['results'].append('All packages up to date') - continue - - # dep/pkgname - find it - else: - if is_installed(module, repoq, spec, conf_file, en_repos=en_repos, dis_repos=dis_repos): - basecmd = 'update' - else: - basecmd = 'install' - - pkglist = what_provides(module, repoq, spec, conf_file, en_repos=en_repos, dis_repos=dis_repos) - if not pkglist: - res['msg'] += "No Package matching '%s' found available, installed or updated" % spec - module.fail_json(**res) - - nothing_to_do = True - for this in pkglist: - if basecmd == 'install' and is_available(module, repoq, this, conf_file, en_repos=en_repos, dis_repos=dis_repos): - nothing_to_do = False - break - - if basecmd == 'update' and is_update(module, repoq, this, conf_file, en_repos=en_repos, dis_repos=en_repos): - nothing_to_do = False - break - - if nothing_to_do: - res['results'].append("All packages providing %s are up to date" % spec) - continue - - # if any of the packages are involved in a transaction, fail now - # so that we don't hang on the dnf operation later - conflicts = transaction_exists(pkglist) - if len(conflicts) > 0: - res['msg'] += "The following packages have pending transactions: %s" % ", ".join(conflicts) - module.fail_json(**res) - - pkg = spec - if not cmd: - cmd = dnf_basecmd + [basecmd, pkg] - - if module.check_mode: - return module.exit_json(changed=True) - - rc, out, err = module.run_command(cmd) - - res['rc'] += rc - res['results'].append(out) - res['msg'] += err - - # FIXME if it is - update it and check to see if it applied - # check to see if there is no longer an update available for the pkgspec - - if rc: - res['failed'] = True - else: - res['changed'] = True - - module.exit_json(**res) - -def ensure(module, state, pkgspec, conf_file, enablerepo, disablerepo, - disable_gpg_check): - - # take multiple args comma separated - items = pkgspec.split(',') - - # need debug level 2 to get 'Nothing to do' for groupinstall. - dnf_basecmd = [dnfbin, '-d', '2', '-y'] - - - if not repoquery: - repoq = None - else: - repoq = [repoquery, '--show-duplicates', '--plugins', '--quiet', '-q'] - - if conf_file and os.path.exists(conf_file): - dnf_basecmd += ['-c', conf_file] - if repoq: - repoq += ['-c', conf_file] - - dis_repos =[] - en_repos = [] - if disablerepo: - dis_repos = disablerepo.split(',') - if enablerepo: - en_repos = enablerepo.split(',') - - for repoid in dis_repos: - r_cmd = ['--disablerepo=%s' % repoid] - dnf_basecmd.extend(r_cmd) - - for repoid in en_repos: - r_cmd = ['--enablerepo=%s' % repoid] - dnf_basecmd.extend(r_cmd) - - if state in ['installed', 'present', 'latest']: - my = dnf_base(conf_file) - try: - for r in dis_repos: - my.repos.disableRepo(r) - - current_repos = dnf.yum.config.RepoConf() - for r in en_repos: - try: - my.repos.enableRepo(r) - new_repos = my.repos.repos.keys() - for i in new_repos: - if not i in current_repos: - rid = my.repos.getRepo(i) - a = rid.repoXML.repoid - current_repos = new_repos - except dnf.exceptions.Error, e: - module.fail_json(msg="Error setting/accessing repo %s: %s" % (r, e)) - except dnf.exceptions.Error, e: - module.fail_json(msg="Error accessing repos: %s" % e) - - if state in ['installed', 'present']: - if disable_gpg_check: - dnf_basecmd.append('--nogpgcheck') - install(module, items, repoq, dnf_basecmd, conf_file, en_repos, dis_repos) - elif state in ['removed', 'absent']: - remove(module, items, repoq, dnf_basecmd, conf_file, en_repos, dis_repos) - elif state == 'latest': - if disable_gpg_check: - dnf_basecmd.append('--nogpgcheck') - latest(module, items, repoq, dnf_basecmd, conf_file, en_repos, dis_repos) - - # should be caught by AnsibleModule argument_spec - return dict(changed=False, failed=True, results='', errors='unexpected state') + return [pkg_to_dict(p) for p in dnf.subject.Subject(stuff).get_best_query(my.sack)] def main(): @@ -789,24 +191,19 @@ def main(): argument_spec = dict( name=dict(aliases=['pkg']), # removed==absent, installed==present, these are accepted as aliases - state=dict(default='installed', choices=['absent','present','installed','removed','latest']), + state=dict(default='installed', choices=['absent', 'present', 'installed', 'removed', 'latest']), enablerepo=dict(), disablerepo=dict(), list=dict(), conf_file=dict(default=None), disable_gpg_check=dict(required=False, default="no", type='bool'), - # this should not be needed, but exists as a failsafe - install_repoquery=dict(required=False, default="yes", type='bool'), ), required_one_of = [['name','list']], mutually_exclusive = [['name','list']], supports_check_mode = True ) - # this should not be needed, but exists as a failsafe params = module.params - if params['install_repoquery'] and not repoquery and not module.check_mode: - install_dnf_utils(module) if not repoquery: module.fail_json(msg="repoquery is required to use this module at this time. Please install the yum-utils package.") @@ -815,6 +212,7 @@ def main(): module.exit_json(**results) else: + return pkg = params['name'] state = params['state'] enablerepo = params.get('enablerepo', '') From bbc8dae00694386240cd5ff95ac02373be9110c6 Mon Sep 17 00:00:00 2001 From: Igor Gnatenko Date: Fri, 22 May 2015 23:04:16 +0300 Subject: [PATCH 0399/2522] add ability to install packages Signed-off-by: Igor Gnatenko --- packaging/os/dnf.py | 60 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index b3fa60bef42..32ae442042c 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -22,14 +22,11 @@ import traceback import os +import operator +import functools import dnf - -try: - from dnf import find_unfinished_transactions, find_ts_remaining - from rpmUtils.miscutils import splitFilename - transaction_helpers = True -except: - transaction_helpers = False +import dnf.cli +import dnf.util DOCUMENTATION = ''' --- @@ -174,6 +171,54 @@ def list_stuff(module, conf_file, stuff): else: return [pkg_to_dict(p) for p in dnf.subject.Subject(stuff).get_best_query(my.sack)] +def ensure(module, state, pkgspec, conf_file, enablerepo, disablerepo, disable_gpg_check): + my = dnf_base(conf_file) + items = pkgspec.split(',') + if disablerepo: + for repo in disablerepo.split(','): + [r.disable() for r in b.repos.get_matching(repo)] + if enablerepo: + for repo in enablerepo.split(','): + [r.enable() for r in b.repos.get_matching(repo)] + my.conf.gpgcheck = disable_gpg_check + + res = {} + res['results'] = [] + res['msg'] = '' + res['rc'] = 0 + res['changed'] = False + + if not dnf.util.am_i_root(): + res['msg'] = 'This command has to be run under the root user.' + res['rc'] = 1 + + pkg_specs, grp_specs, filenames = dnf.cli.commands.parse_spec_group_file(items) + if state in ['installed', 'present']: + # Install files. + local_pkgs = map(my.add_remote_rpm, filenames) + map(my.package_install, local_pkgs) + # Install groups. + if grp_specs: + my.read_comps() + my.env_group_install(grp_specs, dnf.const.GROUP_PACKAGE_TYPES) + # Install packages. + for pkg_spec in pkg_specs: + try: + my.install(pkg_spec) + except dnf.exceptions.MarkingError: + res['results'].append('No package %s available.' % pkg_spec) + res['rc'] = 1 + if not my.resolve() and res['rc'] == 0: + res['msg'] += 'Nothing to do' + res['changed'] = False + else: + my.download_packages(my.transaction.install_set) + my.do_transaction() + [res['results'].append('Installed: %s' % pkg) for pkg in my.transaction.install_set) + [res['results'].append('Removed: %s' % pkg) for pkg in my.transaction.remove_set) + + module.exit_json(**res) + def main(): # state=installed name=pkgspec @@ -212,7 +257,6 @@ def main(): module.exit_json(**results) else: - return pkg = params['name'] state = params['state'] enablerepo = params.get('enablerepo', '') From c798019c739279618ba661bb5552d434559fd123 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Fri, 22 May 2015 12:44:32 -0700 Subject: [PATCH 0400/2522] Fix nevra, fixes to docs and copyright info --- packaging/os/dnf.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index 32ae442042c..8fea0ac89a8 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -1,7 +1,8 @@ #!/usr/bin/python -tt # -*- coding: utf-8 -*- -# Written by Igor Gnatenko +# Copyright 2015 Cristian van Ee +# Copyright 2015 Igor Gnatenko # # This file is part of Ansible # @@ -89,8 +90,12 @@ notes: [] # informational: requirements for nodes -requirements: [ dnf ] -author: '"Igor Gnatenko" ' +requirements: + - "python >= 2.6" + - dnf +author: + - '"Igor Gnatenko (@ignatenkobrain) " ' + - '"Cristian van Ee (@DJMuggs)" ' ''' EXAMPLES = ''' @@ -147,8 +152,8 @@ def pkg_to_dict(pkg): 'release': pkg.release, 'version': pkg.version, 'repo': pkg.repoid, - 'nevra': str(pkg) } + d['nevra'] = '{epoch}:{name}-{version}-{release}.{arch}'.format(**d) if pkg.installed: d['dnfstate'] = 'installed' @@ -268,5 +273,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() From 5d99dcfe4f1e77d59b7c4c7441bdde7240a5c665 Mon Sep 17 00:00:00 2001 From: Igor Gnatenko Date: Fri, 22 May 2015 23:11:13 +0300 Subject: [PATCH 0401/2522] typo fix in oneliners Signed-off-by: Igor Gnatenko --- packaging/os/dnf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index 8fea0ac89a8..60b991f8cd7 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -219,8 +219,8 @@ def ensure(module, state, pkgspec, conf_file, enablerepo, disablerepo, disable_g else: my.download_packages(my.transaction.install_set) my.do_transaction() - [res['results'].append('Installed: %s' % pkg) for pkg in my.transaction.install_set) - [res['results'].append('Removed: %s' % pkg) for pkg in my.transaction.remove_set) + [res['results'].append('Installed: %s' % pkg) for pkg in my.transaction.install_set] + [res['results'].append('Removed: %s' % pkg) for pkg in my.transaction.remove_set] module.exit_json(**res) From 050e619e7b40df3a5a63caa46e1ec4d4d272b2ff Mon Sep 17 00:00:00 2001 From: ToBeReplaced Date: Tue, 7 Jul 2015 11:47:34 -0600 Subject: [PATCH 0402/2522] Add change reporting, enablerepo support, and gpgcheck. disable_gpg_check was configured backwards, so it was toggled. Typos in enablerepo/disablerepo are removed. fill_sack() calls are relocated to occur after repo decisions. The "changed" key is now set for new installations. --- packaging/os/dnf.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index 60b991f8cd7..63e3e4a62d9 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -129,13 +129,13 @@ def log(msg): syslog.syslog(syslog.LOG_NOTICE, msg) def dnf_base(conf_file=None): + """Return a dnf Base object. You must call fill_sack.""" my = dnf.Base() my.conf.debuglevel = 0 if conf_file and os.path.exists(conf_file): my.conf.config_file_path = conf_file my.conf.read() my.read_all_repos() - my.fill_sack() return my @@ -164,6 +164,7 @@ def pkg_to_dict(pkg): def list_stuff(module, conf_file, stuff): my = dnf_base(conf_file) + my.fill_sack() if stuff == 'installed': return [pkg_to_dict(p) for p in my.sack.query().installed()] @@ -181,11 +182,12 @@ def ensure(module, state, pkgspec, conf_file, enablerepo, disablerepo, disable_g items = pkgspec.split(',') if disablerepo: for repo in disablerepo.split(','): - [r.disable() for r in b.repos.get_matching(repo)] + [r.disable() for r in my.repos.get_matching(repo)] if enablerepo: for repo in enablerepo.split(','): - [r.enable() for r in b.repos.get_matching(repo)] - my.conf.gpgcheck = disable_gpg_check + [r.enable() for r in my.repos.get_matching(repo)] + my.fill_sack() + my.conf.gpgcheck = not disable_gpg_check res = {} res['results'] = [] @@ -219,6 +221,7 @@ def ensure(module, state, pkgspec, conf_file, enablerepo, disablerepo, disable_g else: my.download_packages(my.transaction.install_set) my.do_transaction() + res['changed'] = True [res['results'].append('Installed: %s' % pkg) for pkg in my.transaction.install_set] [res['results'].append('Removed: %s' % pkg) for pkg in my.transaction.remove_set] @@ -275,4 +278,3 @@ def main(): from ansible.module_utils.basic import * if __name__ == '__main__': main() - From 495af842fcee94b4e07d8af77cfd8753b3799bd2 Mon Sep 17 00:00:00 2001 From: ToBeReplaced Date: Thu, 9 Jul 2015 15:00:22 -0600 Subject: [PATCH 0403/2522] Add support for state=latest and * --- packaging/os/dnf.py | 54 +++++++++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index 63e3e4a62d9..107895bddb6 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -177,9 +177,18 @@ def list_stuff(module, conf_file, stuff): else: return [pkg_to_dict(p) for p in dnf.subject.Subject(stuff).get_best_query(my.sack)] + +def _mark_package_install(my, res, pkg_spec): + """Mark the package for install.""" + try: + my.install(pkg_spec) + except dnf.exceptions.MarkingError: + res['results'].append('No package %s available.' % pkg_spec) + res['rc'] = 1 + + def ensure(module, state, pkgspec, conf_file, enablerepo, disablerepo, disable_gpg_check): my = dnf_base(conf_file) - items = pkgspec.split(',') if disablerepo: for repo in disablerepo.split(','): [r.disable() for r in my.repos.get_matching(repo)] @@ -199,22 +208,33 @@ def ensure(module, state, pkgspec, conf_file, enablerepo, disablerepo, disable_g res['msg'] = 'This command has to be run under the root user.' res['rc'] = 1 - pkg_specs, grp_specs, filenames = dnf.cli.commands.parse_spec_group_file(items) - if state in ['installed', 'present']: - # Install files. - local_pkgs = map(my.add_remote_rpm, filenames) - map(my.package_install, local_pkgs) - # Install groups. - if grp_specs: - my.read_comps() - my.env_group_install(grp_specs, dnf.const.GROUP_PACKAGE_TYPES) - # Install packages. - for pkg_spec in pkg_specs: - try: - my.install(pkg_spec) - except dnf.exceptions.MarkingError: - res['results'].append('No package %s available.' % pkg_spec) - res['rc'] = 1 + if pkgspec == '*' and state == 'latest': + my.upgrade_all() + else: + items = pkgspec.split(',') + pkg_specs, grp_specs, filenames = dnf.cli.commands.parse_spec_group_file(items) + if state in ['installed', 'present']: + # Install files. + for filename in filenames: + my.package_install(my.add_remote_rpm(filename)) + # Install groups. + if grp_specs: + my.read_comps() + my.env_group_install(grp_specs, dnf.const.GROUP_PACKAGE_TYPES) + # Install packages. + for pkg_spec in pkg_specs: + _mark_package_install(my, res, pkg_spec) + elif state == 'latest': + # These aren't implemented yet, so assert them out. + assert not filenames + assert not grp_specs + for pkg_spec in pkg_specs: + try: + my.upgrade(pkg_spec) + except dnf.exceptions.MarkingError: + # If not already installed, try to install. + _mark_package_install(my, res, pkg_spec) + if not my.resolve() and res['rc'] == 0: res['msg'] += 'Nothing to do' res['changed'] = False From 69f330ff970b7e9972565997b9423661b3a634cd Mon Sep 17 00:00:00 2001 From: Paul Markham Date: Fri, 10 Jul 2015 13:15:47 +1000 Subject: [PATCH 0404/2522] Updates for Solaris 11 --- system/solaris_zone.py | 85 ++++++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 36 deletions(-) diff --git a/system/solaris_zone.py b/system/solaris_zone.py index 0f064f9efc0..d1277366e2a 100644 --- a/system/solaris_zone.py +++ b/system/solaris_zone.py @@ -32,7 +32,7 @@ version_added: "2.0" author: Paul Markham requirements: - - Solaris 10 + - Solaris 10 or 11 options: state: required: true @@ -84,6 +84,12 @@ - 'Extra options to the zonecfg(1M) create command.' required: false default: empty string + install_options: + required: false + description: + - 'Extra options to the zoneadm(1M) install command. To automate Solaris 11 zone creation, + use this to specify the profile XML file, e.g. install_options="-c sc_profile.xml"' + required: false attach_options: required: false description: @@ -130,23 +136,34 @@ class Zone(object): def __init__(self, module): - self.changed = False - self.msg = [] - - self.module = module - self.path = self.module.params['path'] - self.name = self.module.params['name'] - self.sparse = self.module.params['sparse'] - self.root_password = self.module.params['root_password'] - self.timeout = self.module.params['timeout'] - self.config = self.module.params['config'] - self.create_options = self.module.params['create_options'] - self.attach_options = self.module.params['attach_options'] + self.changed = False + self.msg = [] + + self.module = module + self.path = self.module.params['path'] + self.name = self.module.params['name'] + self.sparse = self.module.params['sparse'] + self.root_password = self.module.params['root_password'] + self.timeout = self.module.params['timeout'] + self.config = self.module.params['config'] + self.create_options = self.module.params['create_options'] + self.install_options = self.module.params['install_options'] + self.attach_options = self.module.params['attach_options'] self.zoneadm_cmd = self.module.get_bin_path('zoneadm', True) self.zonecfg_cmd = self.module.get_bin_path('zonecfg', True) self.ssh_keygen_cmd = self.module.get_bin_path('ssh-keygen', True) + if self.module.check_mode: + self.msg.append('Running in check mode') + + if platform.system() != 'SunOS': + self.module.fail_json(msg='This module requires Solaris') + + (self.os_major, self.os_minor) = platform.release().split('.') + if int(self.os_minor) < 10: + self.module.fail_json(msg='This module requires Solaris 10 or later') + def configure(self): if not self.path: self.module.fail_json(msg='Missing required argument: path') @@ -176,11 +193,12 @@ def configure(self): def install(self): if not self.module.check_mode: - cmd = '%s -z %s install' % (self.zoneadm_cmd, self.name) + cmd = '%s -z %s install %s' % (self.zoneadm_cmd, self.name, self.install_options) (rc, out, err) = self.module.run_command(cmd) if rc != 0: self.module.fail_json(msg='Failed to install zone. %s' % (out + err)) - self.configure_sysid() + if int(self.os_minor) == 10: + self.configure_sysid() self.configure_password() self.configure_ssh_keys() self.changed = True @@ -273,7 +291,7 @@ def boot(self): while True: if elapsed > self.timeout: self.module.fail_json(msg='timed out waiting for zone to boot') - rc = os.system('ps -z %s -o args|grep "/usr/lib/saf/ttymon.*-d /dev/console" > /dev/null 2>/dev/null' % self.name) + rc = os.system('ps -z %s -o args|grep "ttymon.*-d /dev/console" > /dev/null 2>/dev/null' % self.name) if rc == 0: break time.sleep(10) @@ -341,9 +359,10 @@ def is_configured(self): def status(self): cmd = '%s -z %s list -p' % (self.zoneadm_cmd, self.name) (rc, out, err) = self.module.run_command(cmd) - if rc != 0: - self.module.fail_json(msg='Failed to determine zone state. %s' % (out + err)) - return out.split(':')[2] + if rc == 0: + return out.split(':')[2] + else: + return 'undefined' def state_present(self): if self.exists(): @@ -398,27 +417,21 @@ def state_attached(self): def main(): module = AnsibleModule( - argument_spec = dict( - name = dict(required=True), - state = dict(default='present', choices=['running', 'started', 'present', 'installed', 'stopped', 'absent', 'configured', 'detached', 'attached']), - path = dict(defalt=None), - sparse = dict(default=False, type='bool'), - root_password = dict(default=None), - timeout = dict(default=600, type='int'), - config = dict(default=''), - create_options = dict(default=''), - attach_options = dict(default=''), + argument_spec = dict( + name = dict(required=True), + state = dict(default='present', choices=['running', 'started', 'present', 'installed', 'stopped', 'absent', 'configured', 'detached', 'attached']), + path = dict(defalt=None), + sparse = dict(default=False, type='bool'), + root_password = dict(default=None), + timeout = dict(default=600, type='int'), + config = dict(default=''), + create_options = dict(default=''), + install_options = dict(default=''), + attach_options = dict(default=''), ), supports_check_mode=True ) - if platform.system() == 'SunOS': - (major, minor) = platform.release().split('.') - if minor < 10: - module.fail_json(msg='This module requires Solaris 10 or later') - else: - module.fail_json(msg='This module requires Solaris') - zone = Zone(module) state = module.params['state'] From 1ccb21bd182954a6802cf97451a5d17c5f57a17e Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Fri, 10 Jul 2015 17:36:20 +0200 Subject: [PATCH 0405/2522] cloudstack: cs_instance: fix missing resource error in check mode if instance is not yet present --- cloud/cloudstack/cs_instance.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 79b1c58a586..7c2c117604d 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -485,8 +485,10 @@ def present_instance(self): instance = self.deploy_instance() else: instance = self.update_instance(instance) - - instance = self.ensure_tags(resource=instance, resource_type='UserVm') + + # In check mode, we do not necessarely have an instance + if instance: + instance = self.ensure_tags(resource=instance, resource_type='UserVm') return instance From 80d959f9373dd1095a43a51f1fb0ac0fa905127b Mon Sep 17 00:00:00 2001 From: Paul Markham Date: Sat, 11 Jul 2015 10:08:51 +1000 Subject: [PATCH 0406/2522] Documentation fixes --- system/solaris_zone.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/system/solaris_zone.py b/system/solaris_zone.py index d1277366e2a..375196cb1e7 100644 --- a/system/solaris_zone.py +++ b/system/solaris_zone.py @@ -71,7 +71,6 @@ required: false default: null config: - required: false description: - 'The zonecfg configuration commands for this zone. See zonecfg(1M) for the valid options and syntax. Typically this is a list of options separated by semi-colons or new lines, e.g. @@ -79,19 +78,17 @@ required: false default: empty string create_options: - required: false description: - 'Extra options to the zonecfg(1M) create command.' required: false default: empty string install_options: - required: false description: - 'Extra options to the zoneadm(1M) install command. To automate Solaris 11 zone creation, use this to specify the profile XML file, e.g. install_options="-c sc_profile.xml"' required: false + default: empty string attach_options: - required: false description: - 'Extra options to the zoneadm attach command. For example, this can be used to specify whether a minimum or full update of packages is required and if any packages need to From e82f6e9463d512cfe698155bfed683f89ba45a4d Mon Sep 17 00:00:00 2001 From: ToBeReplaced Date: Sat, 11 Jul 2015 01:29:18 -0600 Subject: [PATCH 0407/2522] Rewrite dnf module. This fully implements all expected functionality of the dnf module. Group removal may behave oddly due to hiccups in tagging groups as being installed. A pkg_types option could be added to specify the group package types. --- packaging/os/dnf.py | 351 +++++++++++++++++++++++++------------------- 1 file changed, 198 insertions(+), 153 deletions(-) diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index 107895bddb6..bb6a2c9d495 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -1,4 +1,4 @@ -#!/usr/bin/python -tt +#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2015 Cristian van Ee @@ -20,15 +20,6 @@ # along with Ansible. If not, see . # - -import traceback -import os -import operator -import functools -import dnf -import dnf.cli -import dnf.util - DOCUMENTATION = ''' --- module: dnf @@ -43,17 +34,20 @@ required: true default: null aliases: [] + list: description: - Various (non-idempotent) commands for usage with C(/usr/bin/ansible) and I(not) playbooks. See examples. required: false default: null + state: description: - Whether to install (C(present), C(latest)), or remove (C(absent)) a package. required: false choices: [ "present", "latest", "absent" ] default: "present" + enablerepo: description: - I(Repoid) of repositories to enable for the install/update operation. @@ -94,7 +88,7 @@ - "python >= 2.6" - dnf author: - - '"Igor Gnatenko (@ignatenkobrain) " ' + - '"Igor Gnatenko (@ignatenkobrain)" ' - '"Cristian van Ee (@DJMuggs)" ' ''' @@ -121,178 +115,229 @@ dnf: name="@Development tools" state=present ''' +import os -import syslog - -def log(msg): - syslog.openlog('ansible-dnf', 0, syslog.LOG_USER) - syslog.syslog(syslog.LOG_NOTICE, msg) - -def dnf_base(conf_file=None): - """Return a dnf Base object. You must call fill_sack.""" - my = dnf.Base() - my.conf.debuglevel = 0 - if conf_file and os.path.exists(conf_file): - my.conf.config_file_path = conf_file - my.conf.read() - my.read_all_repos() - - return my - -def pkg_to_dict(pkg): - """ - Args: - pkg (hawkey.Package): The package - """ - - d = { - 'name': pkg.name, - 'arch': pkg.arch, - 'epoch': str(pkg.epoch), - 'release': pkg.release, - 'version': pkg.version, - 'repo': pkg.repoid, - } - d['nevra'] = '{epoch}:{name}-{version}-{release}.{arch}'.format(**d) - - if pkg.installed: - d['dnfstate'] = 'installed' +try: + import dnf + from dnf import cli, const, exceptions, subject, util + HAS_DNF = True +except ImportError: + HAS_DNF = False + + +def _fail_if_no_dnf(module): + """Fail if unable to import dnf.""" + if not HAS_DNF: + module.fail_json( + msg="`python-dnf` is not installed, but it is required for the Ansible dnf module.") + + +def _configure_base(module, base, conf_file, disable_gpg_check): + """Configure the dnf Base object.""" + conf = base.conf + + # Turn off debug messages in the output + conf.debuglevel = 0 + + # Set whether to check gpg signatures + conf.gpgcheck = not disable_gpg_check + + # Don't prompt for user confirmations + conf.assumeyes = True + + # Change the configuration file path if provided + if conf_file: + # Fail if we can't read the configuration file. + if not os.access(conf_file, os.R_OK): + module.fail_json( + msg="cannot read configuration file", conf_file=conf_file) + else: + conf.config_file_path = conf_file + + # Read the configuration file + conf.read() + + +def _specify_repositories(base, disablerepo, enablerepo): + """Enable and disable repositories matching the provided patterns.""" + base.read_all_repos() + repos = base.repos + + # Disable repositories + for repo_pattern in disablerepo: + for repo in repos.get_matching(repo_pattern): + repo.disable() + + # Enable repositories + for repo_pattern in enablerepo: + for repo in repos.get_matching(repo_pattern): + repo.enable() + + +def _base(module, conf_file, disable_gpg_check, disablerepo, enablerepo): + """Return a fully configured dnf Base object.""" + _fail_if_no_dnf(module) + base = dnf.Base() + _configure_base(module, base, conf_file, disable_gpg_check) + _specify_repositories(base, disablerepo, enablerepo) + base.fill_sack() + return base + + +def _package_dict(package): + """Return a dictionary of information for the package.""" + # NOTE: This no longer contains the 'dnfstate' field because it is + # already known based on the query type. + result = { + 'name': package.name, + 'arch': package.arch, + 'epoch': str(package.epoch), + 'release': package.release, + 'version': package.version, + 'repo': package.repoid} + result['nevra'] = '{epoch}:{name}-{version}-{release}.{arch}'.format( + **result) + + return result + + +def list_items(module, base, command): + """List package info based on the command.""" + # Rename updates to upgrades + if command == 'updates': + command = 'upgrades' + + # Return the corresponding packages + if command in ['installed', 'upgrades', 'available']: + results = [ + _package_dict(package) + for package in getattr(base.sack.query(), command)()] + # Return the enabled repository ids + elif command in ['repos', 'repositories']: + results = [ + {'repoid': repo.id, 'state': 'enabled'} + for repo in base.repos.iter_enabled()] + # Return any matching packages else: - d['dnfstate'] = 'available' - - return d - -def list_stuff(module, conf_file, stuff): - my = dnf_base(conf_file) - my.fill_sack() - - if stuff == 'installed': - return [pkg_to_dict(p) for p in my.sack.query().installed()] - elif stuff == 'updates': - return [pkg_to_dict(p) for p in my.sack.query().upgrades()] - elif stuff == 'available': - return [pkg_to_dict(p) for p in my.sack.query().available()] - elif stuff == 'repos': - return [dict(repoid=repo.id, state='enabled') for repo in my.repos.iter_enabled()] - else: - return [pkg_to_dict(p) for p in dnf.subject.Subject(stuff).get_best_query(my.sack)] + packages = subject.Subject(command).get_best_query(base.sack) + results = [_package_dict(package) for package in packages] + + module.exit_json(results=results) -def _mark_package_install(my, res, pkg_spec): +def _mark_package_install(module, base, pkg_spec): """Mark the package for install.""" try: - my.install(pkg_spec) - except dnf.exceptions.MarkingError: - res['results'].append('No package %s available.' % pkg_spec) - res['rc'] = 1 - - -def ensure(module, state, pkgspec, conf_file, enablerepo, disablerepo, disable_gpg_check): - my = dnf_base(conf_file) - if disablerepo: - for repo in disablerepo.split(','): - [r.disable() for r in my.repos.get_matching(repo)] - if enablerepo: - for repo in enablerepo.split(','): - [r.enable() for r in my.repos.get_matching(repo)] - my.fill_sack() - my.conf.gpgcheck = not disable_gpg_check - - res = {} - res['results'] = [] - res['msg'] = '' - res['rc'] = 0 - res['changed'] = False - - if not dnf.util.am_i_root(): - res['msg'] = 'This command has to be run under the root user.' - res['rc'] = 1 - - if pkgspec == '*' and state == 'latest': - my.upgrade_all() + base.install(pkg_spec) + except exceptions.MarkingError: + module.fail(msg="No package {} available.".format(pkg_spec)) + + +def ensure(module, base, state, names): + if not util.am_i_root(): + module.fail_json(msg="This command has to be run under the root user.") + + if names == ['*'] and state == 'latest': + base.upgrade_all() else: - items = pkgspec.split(',') - pkg_specs, grp_specs, filenames = dnf.cli.commands.parse_spec_group_file(items) + pkg_specs, group_specs, filenames = cli.commands.parse_spec_group_file( + names) + if group_specs: + base.read_comps() + + groups = [] + for group_spec in group_specs: + group = base.comps.group_by_pattern(group_spec) + if group: + groups.append(group) + else: + module.fail_json( + msg="No group {} available.".format(group_spec)) + if state in ['installed', 'present']: # Install files. for filename in filenames: - my.package_install(my.add_remote_rpm(filename)) + base.package_install(base.add_remote_rpm(filename)) # Install groups. - if grp_specs: - my.read_comps() - my.env_group_install(grp_specs, dnf.const.GROUP_PACKAGE_TYPES) + for group in groups: + base.group_install(group, const.GROUP_PACKAGE_TYPES) # Install packages. for pkg_spec in pkg_specs: - _mark_package_install(my, res, pkg_spec) + _mark_package_install(module, base, pkg_spec) + elif state == 'latest': - # These aren't implemented yet, so assert them out. - assert not filenames - assert not grp_specs + # "latest" is same as "installed" for filenames. + for filename in filenames: + base.package_install(base.add_remote_rpm(filename)) + for group in groups: + try: + base.group_upgrade(group) + except exceptions.CompsError: + # If not already installed, try to install. + base.group_install(group, const.GROUP_PACKAGE_TYPES) for pkg_spec in pkg_specs: try: - my.upgrade(pkg_spec) + base.upgrade(pkg_spec) except dnf.exceptions.MarkingError: # If not already installed, try to install. - _mark_package_install(my, res, pkg_spec) + _mark_package_install(module, base, pkg_spec) - if not my.resolve() and res['rc'] == 0: - res['msg'] += 'Nothing to do' - res['changed'] = False - else: - my.download_packages(my.transaction.install_set) - my.do_transaction() - res['changed'] = True - [res['results'].append('Installed: %s' % pkg) for pkg in my.transaction.install_set] - [res['results'].append('Removed: %s' % pkg) for pkg in my.transaction.remove_set] + else: + if filenames: + module.fail_json( + msg="Cannot remove paths -- please specify package name.") - module.exit_json(**res) + installed = base.sack.query().installed() + for group in groups: + if installed.filter(name=group.name): + base.group_remove(group) + for pkg_spec in pkg_specs: + if installed.filter(name=pkg_spec): + base.remove(pkg_spec) -def main(): + if not base.resolve(): + module.exit_json(msg="Nothing to do") + else: + if module.check_mode: + module.exit_json(changed=True) + base.download_packages(base.transaction.install_set) + base.do_transaction() + response = {'changed': True, 'results': []} + for package in base.transaction.install_set: + response['results'].append("Installed: {}".format(package)) + for package in base.transaction.remove_set: + response['results'].append("Removed: {}".format(package)) - # state=installed name=pkgspec - # state=removed name=pkgspec - # state=latest name=pkgspec - # - # informational commands: - # list=installed - # list=updates - # list=available - # list=repos - # list=pkgspec + module.exit_json(**response) + +def main(): + """The main function.""" module = AnsibleModule( - argument_spec = dict( - name=dict(aliases=['pkg']), - # removed==absent, installed==present, these are accepted as aliases - state=dict(default='installed', choices=['absent', 'present', 'installed', 'removed', 'latest']), - enablerepo=dict(), - disablerepo=dict(), + argument_spec=dict( + name=dict(aliases=['pkg'], type='list'), + state=dict( + default='installed', + choices=[ + 'absent', 'present', 'installed', 'removed', 'latest']), + enablerepo=dict(type='list', default=[]), + disablerepo=dict(type='list', default=[]), list=dict(), conf_file=dict(default=None), - disable_gpg_check=dict(required=False, default="no", type='bool'), + disable_gpg_check=dict(default=False, type='bool'), ), - required_one_of = [['name','list']], - mutually_exclusive = [['name','list']], - supports_check_mode = True - ) - + required_one_of=[['name', 'list']], + mutually_exclusive=[['name', 'list']], + supports_check_mode=True) params = module.params - - if not repoquery: - module.fail_json(msg="repoquery is required to use this module at this time. Please install the yum-utils package.") + base = _base( + module, params['conf_file'], params['disable_gpg_check'], + params['disablerepo'], params['enablerepo']) if params['list']: - results = dict(results=list_stuff(module, params['conf_file'], params['list'])) - module.exit_json(**results) - + list_items(module, base, params['list']) else: - pkg = params['name'] - state = params['state'] - enablerepo = params.get('enablerepo', '') - disablerepo = params.get('disablerepo', '') - disable_gpg_check = params['disable_gpg_check'] - res = ensure(module, state, pkg, params['conf_file'], enablerepo, - disablerepo, disable_gpg_check) - module.fail_json(msg="we should never get here unless this all failed", **res) + ensure(module, base, params['state'], params['name']) + # import module snippets from ansible.module_utils.basic import * From fbac5a140ef1ec23d2eb2bba40f2bfc7fbd63c56 Mon Sep 17 00:00:00 2001 From: Stefan Berggren Date: Thu, 2 Apr 2015 16:30:18 +0200 Subject: [PATCH 0408/2522] Add attachments support to slack module. --- notification/slack.py | 48 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/notification/slack.py b/notification/slack.py index ba4ed2e4c2d..96102b3d5fe 100644 --- a/notification/slack.py +++ b/notification/slack.py @@ -1,6 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- +# (c) 2015, Stefan Berggren # (c) 2014, Ramon de la Fuente # # This file is part of Ansible @@ -46,7 +47,7 @@ msg: description: - Message to send. - required: true + required: false channel: description: - Channel to send the message to. If absent, the message goes to the channel selected for the I(token). @@ -100,6 +101,10 @@ - 'good' - 'warning' - 'danger' + attachments: + description: + - Define a list of attachments. This list mirrors the Slack JSON API. For more information, see https://api.slack.com/docs/attachments + required: false """ EXAMPLES = """ @@ -130,15 +135,32 @@ color: good username: "" icon_url: "" + +- name: Use the attachments API + slack: + domain: future500.slack.com + token: thetokengeneratedbyslack + attachments: + - text: "Display my system load on host A and B" + color: "#ff00dd" + title: "System load" + fields: + - title: "System A" + value: "load average: 0,74, 0,66, 0,63" + short: "true" + - title: "System B" + value: "load average: 5,16, 4,64, 2,43" + short: "true" """ OLD_SLACK_INCOMING_WEBHOOK = 'https://%s/services/hooks/incoming-webhook?token=%s' SLACK_INCOMING_WEBHOOK = 'https://hooks.slack.com/services/%s' -def build_payload_for_slack(module, text, channel, username, icon_url, icon_emoji, link_names, parse, color): - if color == 'normal': +def build_payload_for_slack(module, text, channel, username, icon_url, icon_emoji, link_names, parse, color, attachments): + payload = {} + if color == "normal" and text is not None: payload = dict(text=text) - else: + elif text is not None: payload = dict(attachments=[dict(text=text, color=color)]) if channel is not None: if (channel[0] == '#') or (channel[0] == '@'): @@ -156,6 +178,16 @@ def build_payload_for_slack(module, text, channel, username, icon_url, icon_emoj if parse is not None: payload['parse'] = parse + if attachments is not None: + if 'attachments' not in payload: + payload['attachments'] = [] + + if attachments is not None: + for attachment in attachments: + if 'fallback' not in attachment: + attachment['fallback'] = attachment['text'] + payload['attachments'].append(attachment) + payload="payload=" + module.jsonify(payload) return payload @@ -178,7 +210,7 @@ def main(): argument_spec = dict( domain = dict(type='str', required=False, default=None), token = dict(type='str', required=True, no_log=True), - msg = dict(type='str', required=True), + msg = dict(type='str', required=False, default=None), channel = dict(type='str', default=None), username = dict(type='str', default='Ansible'), icon_url = dict(type='str', default='http://www.ansible.com/favicon.ico'), @@ -186,7 +218,8 @@ def main(): link_names = dict(type='int', default=1, choices=[0,1]), parse = dict(type='str', default=None, choices=['none', 'full']), validate_certs = dict(default='yes', type='bool'), - color = dict(type='str', default='normal', choices=['normal', 'good', 'warning', 'danger']) + color = dict(type='str', default='normal', choices=['normal', 'good', 'warning', 'danger']), + attachments = dict(type='list', required=False, default=None) ) ) @@ -200,8 +233,9 @@ def main(): link_names = module.params['link_names'] parse = module.params['parse'] color = module.params['color'] + attachments = module.params['attachments'] - payload = build_payload_for_slack(module, text, channel, username, icon_url, icon_emoji, link_names, parse, color) + payload = build_payload_for_slack(module, text, channel, username, icon_url, icon_emoji, link_names, parse, color, attachments) do_notify_slack(module, domain, token, payload) module.exit_json(msg="OK") From 998bba4a211dcad5a50cef9f048ccf43aa1c2905 Mon Sep 17 00:00:00 2001 From: Boris Ekelchik Date: Mon, 13 Jul 2015 12:05:47 -0700 Subject: [PATCH 0409/2522] Added changes requested by reviewers Copied @wimnat incorporating changes requested in feedback comments --- cloud/amazon/sts_assume_role.py | 152 +++++++++++++++----------------- 1 file changed, 70 insertions(+), 82 deletions(-) diff --git a/cloud/amazon/sts_assume_role.py b/cloud/amazon/sts_assume_role.py index 7e02dbbd84e..7eec28b843a 100644 --- a/cloud/amazon/sts_assume_role.py +++ b/cloud/amazon/sts_assume_role.py @@ -17,75 +17,69 @@ DOCUMENTATION = ''' --- module: sts_assume_role -short_description: assume a role in AWS account and obtain temporary credentials. +short_description: Assume a role using AWS Security Token Service and obtain temporary credentials description: - - call AWS STS (Security Token Service) to assume a role in AWS account and obtain temporary credentials. This module has a dependency on python-boto. - For details on base AWS API reference http://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html -version_added: "1.7" + - Assume a role using AWS Security Token Service and obtain temporary credentials +version_added: "2.0" +author: Boris Ekelchik (@bekelchik) options: role_arn: description: - The Amazon Resource Name (ARN) of the role that the caller is assuming (http://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html#Identifiers_ARNs) required: true - aliases: [] role_session_name: description: - Name of the role's session - will be used by CloudTrail required: true - aliases: [] policy: description: - Supplemental policy to use in addition to assumed role's policies. required: false default: null - aliases: [] duration_seconds: description: - The duration, in seconds, of the role session. The value can range from 900 seconds (15 minutes) to 3600 seconds (1 hour). By default, the value is set to 3600 seconds. required: false default: null - aliases: [] external_id: description: - A unique identifier that is used by third parties to assume a role in their customers' accounts. required: false default: null - aliases: [] mfa_serial_number: description: - he identification number of the MFA device that is associated with the user who is making the AssumeRole call. required: false default: null - aliases: [] mfa_token: description: - The value provided by the MFA device, if the trust policy of the role being assumed requires MFA. required: false default: null - aliases: [] - -author: Boris Ekelchik +notes: + - In order to use the assumed role in a following playbook task you must pass the access_key, access_secret and access_token extends_documentation_fragment: aws ''' EXAMPLES = ''' -# Basic example of assuming a role -tasks: -- name: assume a role in account 123456789012 - sts_assume_role: role_arn="arn:aws:iam::123456789012:role/someRole" session_name="someRoleSession" +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Assume an existing role (more details: http://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html) +sts_assume_role: + role_arn: "arn:aws:iam::123456789012:role/someRole" + session_name: "someRoleSession" +register: assumed_role + +# Use the assumed role above to tag an instance in account 123456789012 +ec2_tag: + aws_access_key: "{{ assumed_role.sts_creds.access_key }}" + aws_secret_key: "{{ assumed_role.sts_creds.secret_key }}" + security_token: "{{ assumed_role.sts_creds.session_token }}" + resource: i-xyzxyz01 + state: present + tags: + MyNewTag: value -- name: display temporary credentials - debug: "temporary credentials for the assumed role are {{ ansible_temp_credentials }}" - -- name: use temporary credentials for tagging an instance in account 123456789012 - ec2_tag: resource=i-xyzxyz01 region=us-west-1 state=present - args: - aws_access_key: "{{ ansible_temp_credentials.access_key }}" - aws_secret_key: "{{ ansible_temp_credentials.secret_key }}" - security_token: "{{ ansible_temp_credentials.session_token }}" - - tags: - Test: value ''' import sys @@ -93,71 +87,65 @@ try: import boto.sts - + from boto.exception import BotoServerError + HAS_BOTO = True except ImportError: - print "failed=True msg='boto required for this module'" - sys.exit(1) - -def sts_connect(module): - - """ Return an STS connection""" - - region, ec2_url, boto_params = get_aws_connection_info(module) - - # If we have a region specified, connect to its endpoint. - if region: - try: - sts = connect_to_aws(boto.sts, region, **boto_params) - except boto.exception.NoAuthHandlerFound, e: - module.fail_json(msg=str(e)) - # Otherwise, no region so we fallback to connect_sts method - else: - try: - sts = boto.connect_sts(**boto_params) - except boto.exception.NoAuthHandlerFound, e: - module.fail_json(msg=str(e)) - - - return sts + HAS_BOTO = False + -def assumeRole(): - data = sts.assume_role() - return data +def assume_role_policy(connection, module): + + role_arn = module.params.get('role_arn') + role_session_name = module.params.get('role_session_name') + policy = module.params.get('policy') + duration_seconds = module.params.get('duration_seconds') + external_id = module.params.get('external_id') + mfa_serial_number = module.params.get('mfa_serial_number') + mfa_token = module.params.get('mfa_token') + changed = False + + try: + assumed_role = connection.assume_role(role_arn, role_session_name, policy, duration_seconds, external_id, mfa_serial_number, mfa_token) + changed = True + except BotoServerError, e: + module.fail_json(msg=e) + + module.exit_json(changed=changed, sts_creds=assumed_role.credentials.__dict__, sts_user=assumed_role.user.__dict__) def main(): argument_spec = ec2_argument_spec() - argument_spec.update(dict( - role_arn = dict(required=True), - role_session_name = dict(required=True), - duraction_seconds = dict(), - external_id = dict(), - policy = dict(), - mfa_serial_number = dict(), - mfa_token = dict(), + argument_spec.update( + dict( + role_arn = dict(required=True, default=None), + role_session_name = dict(required=True, default=None), + duration_seconds = dict(required=False, default=None, type='int'), + external_id = dict(required=False, default=None), + policy = dict(required=False, default=None), + mfa_serial_number = dict(required=False, default=None), + mfa_token = dict(required=False, default=None) ) ) + module = AnsibleModule(argument_spec=argument_spec) - role_arn = module.params.get('role_arn') - role_session_name = module.params.get('role_session_name') - policy = module.params.get('policy') - duraction_seconds = module.params.get('duraction_seconds') - external_id = module.params.get('external_id') - mfa_serial_number = module.params.get('mfa_serial_number') - mfa_token = module.params.get('mfa_token') - - sts = sts_connect(module) + if not HAS_BOTO: + module.fail_json(msg='boto required for this module') - temp_credentials = {} + region, ec2_url, aws_connect_params = get_aws_connection_info(module) - try: - temp_credentials = sts.assume_role(role_arn, role_session_name, policy, duraction_seconds, - external_id, mfa_serial_number, mfa_token).credentials.__dict__ - except boto.exception.BotoServerError, e: - module.fail_json(msg='Unable to assume role {0}, error: {1}'.format(role_arn, e)) - result = dict(changed=False, ansible_facts=dict(ansible_temp_credentials=temp_credentials)) + if region: + try: + connection = connect_to_aws(boto.sts, region, **aws_connect_params) + except (boto.exception.NoAuthHandlerFound, StandardError), e: + module.fail_json(msg=str(e)) + else: + module.fail_json(msg="region must be specified") - module.exit_json(**result) + try: + assume_role_policy(connection, module) + except BotoServerError, e: + module.fail_json(msg=e) + # import module snippets from ansible.module_utils.basic import * From 3ac3f02c324daf80f4a9a5176e13998517fab9a3 Mon Sep 17 00:00:00 2001 From: Siva Popuri Date: Mon, 13 Jul 2015 15:52:00 -0500 Subject: [PATCH 0410/2522] changes to include PR review comments --- cloud/centurylink/clc_publicip.py | 66 +++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 11 deletions(-) diff --git a/cloud/centurylink/clc_publicip.py b/cloud/centurylink/clc_publicip.py index 2e525a51455..ed3228e1996 100644 --- a/cloud/centurylink/clc_publicip.py +++ b/cloud/centurylink/clc_publicip.py @@ -31,11 +31,13 @@ short_description: Add and Delete public ips on servers in CenturyLink Cloud. description: - An Ansible module to add or delete public ip addresses on an existing server or servers in CenturyLink Cloud. +version_added: 1.0 options: protocol: descirption: - The protocol that the public IP will listen for. default: TCP + choices: ['TCP', 'UDP', 'ICMP'] required: False ports: description: @@ -58,6 +60,20 @@ choices: [ True, False ] default: True required: False +requirements: + - python = 2.7 + - requests >= 2.5.0 + - clc-sdk +notes: + - To use this module, it is required to set the below environment variables which enables access to the + Centurylink Cloud + - CLC_V2_API_USERNAME: the account login id for the centurylink cloud + - CLC_V2_API_PASSWORD: the account passwod for the centurylink cloud + - Alternatively, the module accepts the API token and account alias. The API token can be generated using the + CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login + - CLC_V2_API_TOKEN: the API token generated from https://api.ctl.io/v2/authentication/login + - CLC_ACCT_ALIAS: the account alias associated with the centurylink cloud + - Users can set CLC_V2_API_URL to specify an endpoint for pointing to a different CLC environment. ''' EXAMPLES = ''' @@ -101,7 +117,14 @@ __version__ = '${version}' -import requests +from distutils.version import LooseVersion + +try: + import requests +except ImportError: + REQUESTS_FOUND = False +else: + REQUESTS_FOUND = True # # Requires the clc-python-sdk. @@ -130,6 +153,12 @@ def __init__(self, module): if not CLC_FOUND: self.module.fail_json( msg='clc-python-sdk required for this module') + if not REQUESTS_FOUND: + self.module.fail_json( + msg='requests library is required for this module') + if requests.__version__ and LooseVersion(requests.__version__) < LooseVersion('2.5.0'): + self.module.fail_json( + msg='requests library version should be >= 2.5.0') self._set_user_agent(self.clc) @@ -169,8 +198,8 @@ def _define_module_argument_spec(): """ argument_spec = dict( server_ids=dict(type='list', required=True), - protocol=dict(default='TCP'), - ports=dict(type='list'), + protocol=dict(default='TCP', choices=['TCP', 'UDP', 'ICMP']), + ports=dict(type='list', required=True), wait=dict(type='bool', default=True), state=dict(default='present', choices=['present', 'absent']), ) @@ -200,12 +229,22 @@ def ensure_public_ip_present(self, server_ids, protocol, ports): for port in ports] for server in servers_to_change: if not self.module.check_mode: - result = server.PublicIPs().Add(ports_to_expose) + result = self._add_publicip_to_server(server, ports_to_expose) results.append(result) changed_server_ids.append(server.id) changed = True return changed, changed_server_ids, results + def _add_publicip_to_server(self, server, ports_to_expose): + result = None + try: + result = server.PublicIPs().Add(ports_to_expose) + except CLCException, ex: + self.module.fail_json(msg='Failed to add public ip to the server : {0}. {1}'.format( + server.id, ex.response_text + )) + return result + def ensure_public_ip_absent(self, server_ids): """ Ensures the given server ids having the public ip removed if there is any @@ -224,19 +263,24 @@ def ensure_public_ip_absent(self, server_ids): servers_to_change = [ server for server in servers if len( server.PublicIPs().public_ips) > 0] - ips_to_delete = [] - for server in servers_to_change: - for ip_address in server.PublicIPs().public_ips: - ips_to_delete.append(ip_address) for server in servers_to_change: if not self.module.check_mode: - for ip in ips_to_delete: - result = ip.Delete() - results.append(result) + result = self._remove_publicip_from_server(server) + results.append(result) changed_server_ids.append(server.id) changed = True return changed, changed_server_ids, results + def _remove_publicip_from_server(self, server): + try: + for ip_address in server.PublicIPs().public_ips: + result = ip_address.Delete() + except CLCException, ex: + self.module.fail_json(msg='Failed to remove public ip from the server : {0}. {1}'.format( + server.id, ex.response_text + )) + return result + def _wait_for_requests_to_complete(self, requests_lst): """ Waits until the CLC requests are complete if the wait argument is True From ed5c623e7c1fa69d7c9570194e4d7612d957029c Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 13 Jul 2015 22:25:52 -0400 Subject: [PATCH 0411/2522] added placeholder to be used on build (TODO: update makefile) --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 53adb84c822..ee36851a03e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.8.2 +${version} From 0395de2f098faa1f532c946a014294d7ae9109cc Mon Sep 17 00:00:00 2001 From: Mark Hamilton Date: Tue, 14 Jul 2015 01:49:16 -0700 Subject: [PATCH 0412/2522] Added version_added and default to new parameters external_ids and set. Generalized external_ids to handle any parameter. --- network/openvswitch_port.py | 104 +++++++++--------------------------- 1 file changed, 26 insertions(+), 78 deletions(-) diff --git a/network/openvswitch_port.py b/network/openvswitch_port.py index bb3924161b7..3f9c22fe432 100644 --- a/network/openvswitch_port.py +++ b/network/openvswitch_port.py @@ -5,6 +5,8 @@ # (c) 2013, David Stygstra # +# Portions copyright @ 2015 VMware, Inc. +# # This file is part of Ansible # # This module is free software: you can redistribute it and/or modify @@ -19,8 +21,6 @@ # # You should have received a copy of the GNU General Public License # along with this software. If not, see . -# -# Portions copyright @ 2015 VMware, Inc. All rights reserved. import syslog import os @@ -54,18 +54,18 @@ default: 5 description: - How long to wait for ovs-vswitchd to respond - iface-id: - required: false - description: - - Used when port is created to set the external_ids:iface-id. - attached-mac: + external_ids: + version_added: 2.0 required: false + default: {} description: - - MAC address when port is created using external_ids:attached-mac. + - Dictionary of external_ids applied to a port. set: + version_added: 2.0 required: false + default: None description: - - Additional set options to apply to the port + - Set a single property on a port. ''' EXAMPLES = ''' @@ -77,13 +77,14 @@ set Interface eth6 ofport_request=6 # Assign interface id server1-vifeth6 and mac address 52:54:00:30:6d:11 -# to port vifeth6 +# to port vifeth6 and setup port to be managed by a controller. - openvswitch_port: bridge=br-int port=vifeth6 state=present args: external_ids: - iface-id: "server1-vifeth6" + iface-id: "{{inventory_hostname}}-vifeth6" attached-mac: "52:54:00:30:6d:11" - + vm-id: "{{inventory_hostname}}" + iface-status: "active" ''' # pylint: disable=W0703 @@ -124,40 +125,12 @@ def __init__(self, module): self.timeout = module.params['timeout'] self.set_opt = module.params.get('set', None) - # if the port name starts with "vif", let's assume it's a VIF - self.is_vif = self.port.startswith('vif') - - ## - # Several places need host name. - rtc, out, err = self.module.run_command(["hostname", "-s"]) - if (rtc): - self.module.fail_json(msg=err) - self.hostname = out.strip("\n") - - def _attached_mac_get(self): - """ Return the interface. """ - attached_mac = self.module.params['external_ids'].get('attached-mac', - None) - if (not attached_mac): - attached_mac = "00:50:50:F" - attached_mac += self.hostname[-3] + ":" - attached_mac += self.hostname[-2:] + ":00" - return attached_mac - - def _iface_id_get(self): - """ Return the interface id. """ - - iface_id = self.module.params['external_ids'].get('iface-id', None) - if (not iface_id): - iface_id = "%s-%s" % (self.hostname, self.port) - return iface_id - - def _vsctl(self, command): + def _vsctl(self, command, check_rc=True): '''Run ovs-vsctl command''' cmd = ['ovs-vsctl', '-t', str(self.timeout)] + command syslog.syslog(syslog.LOG_NOTICE, " ".join(cmd)) - return self.module.run_command(cmd) + return self.module.run_command(cmd, check_rc=check_rc) def exists(self): '''Check if the port already exists''' @@ -171,50 +144,29 @@ def exists(self): def set(self, set_opt): """ Set attributes on a port. """ - syslog.syslog(syslog.LOG_NOTICE, "set called %s" % set_opt) if (not set_opt): return False (get_cmd, set_value) = _set_to_get(set_opt) - (rtc, out, err) = self._vsctl(get_cmd) + (rtc, out, err) = self._vsctl(get_cmd, False) if rtc != 0: - self.module.fail_json(msg=err) + ## + # ovs-vsctl -t 5 -- get Interface port external_ids:key + # returns failure if key does not exist. + out = None + else: + out = out.strip("\n") + out = out.strip('"') - out = out.strip("\n") - out = out.strip('"') if (out == set_value): return False (rtc, out, err) = self._vsctl(["--", "set"] + set_opt.split(" ")) if rtc != 0: self.module.fail_json(msg=err) - self.module.exit_json(changed=True) - syslog.syslog(syslog.LOG_NOTICE, "-- set %s" % set_opt) - - def set_vif_attributes(self): - ''' Set attributes for a vif ''' - - ## - # create a fake MAC address for the VIF - fmac = self._attached_mac_get() - ## - # If vif_uuid is missing then construct a new one. - iface_id = self._iface_id_get() - syslog.syslog(syslog.LOG_NOTICE, "iface-id %s" % iface_id) - - attached_mac = "external_ids:attached-mac=%s" % fmac - iface_id = "external_ids:iface-id=%s" % iface_id - vm_id = "external_ids:vm-id=%s" % self.hostname - cmd = ["set", "Interface", self.port, - "external_ids:iface-status=active", iface_id, vm_id, - attached_mac] - - (rtc, _, stderr) = self._vsctl(cmd) - if rtc != 0: - self.module.fail_json(msg="%s returned %s %s" % (" ".join(cmd), - rtc, stderr)) + return True def add(self): '''Add the port''' @@ -226,10 +178,8 @@ def add(self): (rtc, _, err) = self._vsctl(cmd) if rtc != 0: self.module.fail_json(msg=err) - syslog.syslog(syslog.LOG_NOTICE, " ".join(cmd)) - if self.is_vif: - self.set_vif_attributes() + return True def delete(self): '''Remove the port''' @@ -275,8 +225,6 @@ def run(self): value = value.replace('"', '') fmt_opt = "Interface %s external_ids:%s=%s" external_id = fmt_opt % (self.port, key, value) - syslog.syslog(syslog.LOG_NOTICE, - "external %s" % external_id) changed = self.set(external_id) or changed ## except Exception, earg: @@ -293,7 +241,7 @@ def main(): 'port': {'required': True}, 'state': {'default': 'present', 'choices': ['present', 'absent']}, 'timeout': {'default': 5, 'type': 'int'}, - 'set': {'required': False}, + 'set': {'required': False, 'default': None}, 'external_ids': {'default': {}, 'required': False}, 'syslogging': {'required': False, 'type': "bool", 'default': True} }, From 018db7d068a31e8f477e1a90a3454c3b7c0b7d56 Mon Sep 17 00:00:00 2001 From: Mark Hamilton Date: Tue, 14 Jul 2015 02:10:57 -0700 Subject: [PATCH 0413/2522] removed syslog.openlog --- network/openvswitch_port.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/network/openvswitch_port.py b/network/openvswitch_port.py index 3f9c22fe432..469d53730da 100644 --- a/network/openvswitch_port.py +++ b/network/openvswitch_port.py @@ -23,7 +23,6 @@ # along with this software. If not, see . import syslog -import os DOCUMENTATION = ''' --- @@ -243,14 +242,10 @@ def main(): 'timeout': {'default': 5, 'type': 'int'}, 'set': {'required': False, 'default': None}, 'external_ids': {'default': {}, 'required': False}, - 'syslogging': {'required': False, 'type': "bool", 'default': True} }, supports_check_mode=True, ) - if (module.params["syslogging"]): - syslog.openlog('ansible-%s' % os.path.basename(__file__)) - port = OVSPort(module) if module.check_mode: port.check() From a2166b60cba0302eedfff0e0081ceccdbfd8d377 Mon Sep 17 00:00:00 2001 From: Trond Hindenes Date: Tue, 14 Jul 2015 12:03:04 +0200 Subject: [PATCH 0414/2522] Added module win_package --- windows/win_package.ps1 | 1305 +++++++++++++++++++++++++++++++++++++++ windows/win_package.py | 87 +++ 2 files changed, 1392 insertions(+) create mode 100644 windows/win_package.ps1 create mode 100644 windows/win_package.py diff --git a/windows/win_package.ps1 b/windows/win_package.ps1 new file mode 100644 index 00000000000..02bb908a944 --- /dev/null +++ b/windows/win_package.ps1 @@ -0,0 +1,1305 @@ +#!powershell +# (c) 2014, Trond Hindenes , and others +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# WANT_JSON +# POWERSHELL_COMMON + +#region DSC + +data LocalizedData +{ + # culture="en-US" + # TODO: Support WhatIf + ConvertFrom-StringData @' +InvalidIdentifyingNumber=The specified IdentifyingNumber ({0}) is not a valid Guid +InvalidPath=The specified Path ({0}) is not in a valid format. Valid formats are local paths, UNC, and HTTP +InvalidNameOrId=The specified Name ({0}) and IdentifyingNumber ({1}) do not match Name ({2}) and IdentifyingNumber ({3}) in the MSI file +NeedsMoreInfo=Either Name or ProductId is required +InvalidBinaryType=The specified Path ({0}) does not appear to specify an EXE or MSI file and as such is not supported +CouldNotOpenLog=The specified LogPath ({0}) could not be opened +CouldNotStartProcess=The process {0} could not be started +UnexpectedReturnCode=The return code {0} was not expected. Configuration is likely not correct +PathDoesNotExist=The given Path ({0}) could not be found +CouldNotOpenDestFile=Could not open the file {0} for writing +CouldNotGetHttpStream=Could not get the {0} stream for file {1} +ErrorCopyingDataToFile=Encountered error while writing the contents of {0} to {1} +PackageConfigurationComplete=Package configuration finished +PackageConfigurationStarting=Package configuration starting +InstalledPackage=Installed package +UninstalledPackage=Uninstalled package +NoChangeRequired=Package found in desired state, no action required +RemoveExistingLogFile=Remove existing log file +CreateLogFile=Create log file +MountSharePath=Mount share to get media +DownloadHTTPFile=Download the media over HTTP or HTTPS +StartingProcessMessage=Starting process {0} with arguments {1} +RemoveDownloadedFile=Remove the downloaded file +PackageInstalled=Package has been installed +PackageUninstalled=Package has been uninstalled +MachineRequiresReboot=The machine requires a reboot +PackageDoesNotAppearInstalled=The package {0} is not installed +PackageAppearsInstalled=The package {0} is already installed +PostValidationError=Package from {0} was installed, but the specified ProductId and/or Name does not match package details +'@ +} + +$Debug = $true +Function Trace-Message +{ + param([string] $Message) + if($Debug) + { + Write-Verbose $Message + } +} + +$CacheLocation = "$env:ProgramData\Microsoft\Windows\PowerShell\Configuration\BuiltinProvCache\MSFT_PackageResource" + +Function Throw-InvalidArgumentException +{ + param( + [string] $Message, + [string] $ParamName + ) + + $exception = new-object System.ArgumentException $Message,$ParamName + $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,$ParamName,"InvalidArgument",$null + throw $errorRecord +} + +Function Throw-InvalidNameOrIdException +{ + param( + [string] $Message + ) + + $exception = new-object System.ArgumentException $Message + $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,"NameOrIdNotInMSI","InvalidArgument",$null + throw $errorRecord +} + +Function Throw-TerminatingError +{ + param( + [string] $Message, + [System.Management.Automation.ErrorRecord] $ErrorRecord + ) + + $exception = new-object "System.InvalidOperationException" $Message,$ErrorRecord.Exception + $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,"MachineStateIncorrect","InvalidOperation",$null + throw $errorRecord +} + +Function Get-RegistryValueIgnoreError +{ + param + ( + [parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryHive] + $RegistryHive, + + [parameter(Mandatory = $true)] + [System.String] + $Key, + + [parameter(Mandatory = $true)] + [System.String] + $Value, + + [parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryView] + $RegistryView + ) + + try + { + $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, $RegistryView) + $subKey = $baseKey.OpenSubKey($Key) + if($subKey -ne $null) + { + return $subKey.GetValue($Value) + } + } + catch + { + $exceptionText = ($_ | Out-String).Trim() + Write-Verbose "Exception occured in Get-RegistryValueIgnoreError: $exceptionText" + } + return $null +} + +Function Validate-StandardArguments +{ + param( + $Path, + $ProductId, + $Name + ) + + Trace-Message "Validate-StandardArguments, Path was $Path" + $uri = $null + try + { + $uri = [uri] $Path + } + catch + { + Throw-InvalidArgumentException ($LocalizedData.InvalidPath -f $Path) "Path" + } + + if(-not @("file", "http", "https") -contains $uri.Scheme) + { + Trace-Message "The uri scheme was $uri.Scheme" + Throw-InvalidArgumentException ($LocalizedData.InvalidPath -f $Path) "Path" + } + + $pathExt = [System.IO.Path]::GetExtension($Path) + Trace-Message "The path extension was $pathExt" + if(-not @(".msi",".exe") -contains $pathExt.ToLower()) + { + Throw-InvalidArgumentException ($LocalizedData.InvalidBinaryType -f $Path) "Path" + } + + $identifyingNumber = $null + if(-not $Name -and -not $ProductId) + { + #It's a tossup here which argument to blame, so just pick ProductId to encourage customers to use the most efficient version + Throw-InvalidArgumentException ($LocalizedData.NeedsMoreInfo -f $Path) "ProductId" + } + elseif($ProductId) + { + try + { + Trace-Message "Parsing $ProductId as an identifyingNumber" + $identifyingNumber = "{{{0}}}" -f [Guid]::Parse($ProductId).ToString().ToUpper() + Trace-Message "Parsed $ProductId as $identifyingNumber" + } + catch + { + Throw-InvalidArgumentException ($LocalizedData.InvalidIdentifyingNumber -f $ProductId) $ProductId + } + } + + return $uri, $identifyingNumber +} + +Function Get-ProductEntry +{ + param + ( + [string] $Name, + [string] $IdentifyingNumber, + [string] $InstalledCheckRegKey, + [string] $InstalledCheckRegValueName, + [string] $InstalledCheckRegValueData + ) + + $uninstallKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" + $uninstallKeyWow64 = "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" + + if($IdentifyingNumber) + { + $keyLocation = "$uninstallKey\$identifyingNumber" + $item = Get-Item $keyLocation -EA SilentlyContinue + if(-not $item) + { + $keyLocation = "$uninstallKeyWow64\$identifyingNumber" + $item = Get-Item $keyLocation -EA SilentlyContinue + } + + return $item + } + + foreach($item in (Get-ChildItem -EA Ignore $uninstallKey, $uninstallKeyWow64)) + { + if($Name -eq (Get-LocalizableRegKeyValue $item "DisplayName")) + { + return $item + } + } + + if ($InstalledCheckRegKey -and $InstalledCheckRegValueName -and $InstalledCheckRegValueData) + { + $installValue = $null + + #if 64bit OS, check 64bit registry view first + if ((Get-WmiObject -Class Win32_OperatingSystem -ComputerName "localhost" -ea 0).OSArchitecture -eq '64-bit') + { + $installValue = Get-RegistryValueIgnoreError LocalMachine "$InstalledCheckRegKey" "$InstalledCheckRegValueName" Registry64 + } + + if($installValue -eq $null) + { + $installValue = Get-RegistryValueIgnoreError LocalMachine "$InstalledCheckRegKey" "$InstalledCheckRegValueName" Registry32 + } + + if($installValue) + { + if($InstalledCheckRegValueData -and $installValue -eq $InstalledCheckRegValueData) + { + return @{ + Installed = $true + } + } + } + } + + return $null +} + +function Test-TargetResource +{ + param + ( + [ValidateSet("Present", "Absent")] + [string] $Ensure = "Present", + + [parameter(Mandatory = $true)] + [AllowEmptyString()] + [string] $Name, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $Path, + + [parameter(Mandatory = $true)] + [AllowEmptyString()] + [string] $ProductId, + + [string] $Arguments, + + [pscredential] $Credential, + + [int[]] $ReturnCode, + + [string] $LogPath, + + [pscredential] $RunAsCredential, + + [string] $InstalledCheckRegKey, + + [string] $InstalledCheckRegValueName, + + [string] $InstalledCheckRegValueData + ) + + $uri, $identifyingNumber = Validate-StandardArguments $Path $ProductId $Name + $product = Get-ProductEntry $Name $identifyingNumber $InstalledCheckRegKey $InstalledCheckRegValueName $InstalledCheckRegValueData + Trace-Message "Ensure is $Ensure" + if($product) + { + Trace-Message "product found" + } + else + { + Trace-Message "product installation cannot be determined" + } + Trace-Message ("product as boolean is {0}" -f [boolean]$product) + $res = ($product -ne $null -and $Ensure -eq "Present") -or ($product -eq $null -and $Ensure -eq "Absent") + + # install registry test overrides the product id test and there is no true product information + # when doing a lookup via registry key + if ($product -and $InstalledCheckRegKey -and $InstalledCheckRegValueName -and $InstalledCheckRegValueData) + { + Write-Verbose ($LocalizedData.PackageAppearsInstalled -f $Name) + } + else + { + if ($product -ne $null) + { + $name = Get-LocalizableRegKeyValue $product "DisplayName" + Write-Verbose ($LocalizedData.PackageAppearsInstalled -f $name) + } + else + { + $displayName = $null + if($Name) + { + $displayName = $Name + } + else + { + $displayName = $ProductId + } + + Write-Verbose ($LocalizedData.PackageDoesNotAppearInstalled -f $displayName) + } + + } + + return $res +} + +function Get-LocalizableRegKeyValue +{ + param( + [object] $RegKey, + [string] $ValueName + ) + + $res = $RegKey.GetValue("{0}_Localized" -f $ValueName) + if(-not $res) + { + $res = $RegKey.GetValue($ValueName) + } + + return $res +} + +function Get-TargetResource +{ + param + ( + [parameter(Mandatory = $true)] + [AllowEmptyString()] + [string] $Name, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $Path, + + [parameter(Mandatory = $true)] + [AllowEmptyString()] + [string] $ProductId, + + [string] $InstalledCheckRegKey, + + [string] $InstalledCheckRegValueName, + + [string] $InstalledCheckRegValueData + ) + + #If the user gave the ProductId then we derive $identifyingNumber + $uri, $identifyingNumber = Validate-StandardArguments $Path $ProductId $Name + + $localMsi = $uri.IsFile -and -not $uri.IsUnc + + $product = Get-ProductEntry $Name $identifyingNumber $InstalledCheckRegKey $InstalledCheckRegValueName $InstalledCheckRegValueData + + if(-not $product) + { + return @{ + Ensure = "Absent" + Name = $Name + ProductId = $identifyingNumber + Installed = $false + InstalledCheckRegKey = $InstalledCheckRegKey + InstalledCheckRegValueName = $InstalledCheckRegValueName + InstalledCheckRegValueData = $InstalledCheckRegValueData + } + } + + if ($InstalledCheckRegKey -and $InstalledCheckRegValueName -and $InstalledCheckRegValueData) + { + return @{ + Ensure = "Present" + Name = $Name + ProductId = $identifyingNumber + Installed = $true + InstalledCheckRegKey = $InstalledCheckRegKey + InstalledCheckRegValueName = $InstalledCheckRegValueName + InstalledCheckRegValueData = $InstalledCheckRegValueData + } + } + + #$identifyingNumber can still be null here (e.g. remote MSI with Name specified, local EXE) + #If the user gave a ProductId just pass it through, otherwise fill it from the product + if(-not $identifyingNumber) + { + $identifyingNumber = Split-Path -Leaf $product.Name + } + + $date = $product.GetValue("InstallDate") + if($date) + { + try + { + $date = "{0:d}" -f [DateTime]::ParseExact($date, "yyyyMMdd",[System.Globalization.CultureInfo]::CurrentCulture).Date + } + catch + { + $date = $null + } + } + + $publisher = Get-LocalizableRegKeyValue $product "Publisher" + $size = $product.GetValue("EstimatedSize") + if($size) + { + $size = $size/1024 + } + + $version = $product.GetValue("DisplayVersion") + $description = $product.GetValue("Comments") + $name = Get-LocalizableRegKeyValue $product "DisplayName" + return @{ + Ensure = "Present" + Name = $name + Path = $Path + InstalledOn = $date + ProductId = $identifyingNumber + Size = $size + Installed = $true + Version = $version + PackageDescription = $description + Publisher = $publisher + } +} + +Function Get-MsiTools +{ + if($script:MsiTools) + { + return $script:MsiTools + } + + $sig = @' + [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)] + private static extern UInt32 MsiOpenPackageW(string szPackagePath, out IntPtr hProduct); + + [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)] + private static extern uint MsiCloseHandle(IntPtr hAny); + + [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)] + private static extern uint MsiGetPropertyW(IntPtr hAny, string name, StringBuilder buffer, ref int bufferLength); + + private static string GetPackageProperty(string msi, string property) + { + IntPtr MsiHandle = IntPtr.Zero; + try + { + var res = MsiOpenPackageW(msi, out MsiHandle); + if (res != 0) + { + return null; + } + + int length = 256; + var buffer = new StringBuilder(length); + res = MsiGetPropertyW(MsiHandle, property, buffer, ref length); + return buffer.ToString(); + } + finally + { + if (MsiHandle != IntPtr.Zero) + { + MsiCloseHandle(MsiHandle); + } + } + } + public static string GetProductCode(string msi) + { + return GetPackageProperty(msi, "ProductCode"); + } + + public static string GetProductName(string msi) + { + return GetPackageProperty(msi, "ProductName"); + } +'@ + $script:MsiTools = Add-Type -PassThru -Namespace Microsoft.Windows.DesiredStateConfiguration.PackageResource ` + -Name MsiTools -Using System.Text -MemberDefinition $sig + return $script:MsiTools +} + + +Function Get-MsiProductEntry +{ + param + ( + [string] $Path + ) + + if(-not (Test-Path -PathType Leaf $Path) -and ($fileExtension -ne ".msi")) + { + Throw-TerminatingError ($LocalizedData.PathDoesNotExist -f $Path) + } + + $tools = Get-MsiTools + + $pn = $tools::GetProductName($Path) + + $pc = $tools::GetProductCode($Path) + + return $pn,$pc +} + + +function Set-TargetResource +{ + [CmdletBinding(SupportsShouldProcess=$true)] + param + ( + [ValidateSet("Present", "Absent")] + [string] $Ensure = "Present", + + [parameter(Mandatory = $true)] + [AllowEmptyString()] + [string] $Name, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $Path, + + [parameter(Mandatory = $true)] + [AllowEmptyString()] + [string] $ProductId, + + [string] $Arguments, + + [pscredential] $Credential, + + [int[]] $ReturnCode, + + [string] $LogPath, + + [pscredential] $RunAsCredential, + + [string] $InstalledCheckRegKey, + + [string] $InstalledCheckRegValueName, + + [string] $InstalledCheckRegValueData + ) + + $ErrorActionPreference = "Stop" + + if((Test-TargetResource -Ensure $Ensure -Name $Name -Path $Path -ProductId $ProductId ` + -InstalledCheckRegKey $InstalledCheckRegKey -InstalledCheckRegValueName $InstalledCheckRegValueName ` + -InstalledCheckRegValueData $InstalledCheckRegValueData)) + { + return + } + + $uri, $identifyingNumber = Validate-StandardArguments $Path $ProductId $Name + + #Path gets overwritten in the download code path. Retain the user's original Path in case the install succeeded + #but the named package wasn't present on the system afterward so we can give a better message + $OrigPath = $Path + + Write-Verbose $LocalizedData.PackageConfigurationStarting + if(-not $ReturnCode) + { + $ReturnCode = @(0) + } + + $logStream = $null + $psdrive = $null + $downloadedFileName = $null + try + { + $fileExtension = [System.IO.Path]::GetExtension($Path).ToLower() + if($LogPath) + { + try + { + if($fileExtension -eq ".msi") + { + #We want to pre-verify the path exists and is writable ahead of time + #even in the MSI case, as detecting WHY the MSI log doesn't exist would + #be rather problematic for the user + if((Test-Path $LogPath) -and $PSCmdlet.ShouldProcess($LocalizedData.RemoveExistingLogFile,$null,$null)) + { + rm $LogPath + } + + if($PSCmdlet.ShouldProcess($LocalizedData.CreateLogFile, $null, $null)) + { + New-Item -Type File $LogPath | Out-Null + } + } + elseif($PSCmdlet.ShouldProcess($LocalizedData.CreateLogFile, $null, $null)) + { + $logStream = new-object "System.IO.StreamWriter" $LogPath,$false + } + } + catch + { + Throw-TerminatingError ($LocalizedData.CouldNotOpenLog -f $LogPath) $_ + } + } + + #Download or mount file as necessary + if(-not ($fileExtension -eq ".msi" -and $Ensure -eq "Absent")) + { + if($uri.IsUnc -and $PSCmdlet.ShouldProcess($LocalizedData.MountSharePath, $null, $null)) + { + $psdriveArgs = @{Name=([guid]::NewGuid());PSProvider="FileSystem";Root=(Split-Path $uri.LocalPath)} + if($Credential) + { + #We need to optionally include these and then splat the hash otherwise + #we pass a null for Credential which causes the cmdlet to pop a dialog up + $psdriveArgs["Credential"] = $Credential + } + + $psdrive = New-PSDrive @psdriveArgs + $Path = Join-Path $psdrive.Root (Split-Path -Leaf $uri.LocalPath) #Necessary? + } + elseif(@("http", "https") -contains $uri.Scheme -and $Ensure -eq "Present" -and $PSCmdlet.ShouldProcess($LocalizedData.DownloadHTTPFile, $null, $null)) + { + $scheme = $uri.Scheme + $outStream = $null + $responseStream = $null + + try + { + Trace-Message "Creating cache location" + + if(-not (Test-Path -PathType Container $CacheLocation)) + { + mkdir $CacheLocation | Out-Null + } + + $destName = Join-Path $CacheLocation (Split-Path -Leaf $uri.LocalPath) + + Trace-Message "Need to download file from $scheme, destination will be $destName" + + try + { + Trace-Message "Creating the destination cache file" + $outStream = New-Object System.IO.FileStream $destName, "Create" + } + catch + { + #Should never happen since we own the cache directory + Throw-TerminatingError ($LocalizedData.CouldNotOpenDestFile -f $destName) $_ + } + + try + { + Trace-Message "Creating the $scheme stream" + $request = [System.Net.WebRequest]::Create($uri) + Trace-Message "Setting default credential" + $request.Credentials = [System.Net.CredentialCache]::DefaultCredentials + if ($scheme -eq "http") + { + Trace-Message "Setting authentication level" + # default value is MutualAuthRequested, which applies to https scheme + $request.AuthenticationLevel = [System.Net.Security.AuthenticationLevel]::None + } + if ($scheme -eq "https") + { + Trace-Message "Ignoring bad certificates" + $request.ServerCertificateValidationCallBack = {$true} + } + Trace-Message "Getting the $scheme response stream" + $responseStream = (([System.Net.HttpWebRequest]$request).GetResponse()).GetResponseStream() + } + catch + { + Trace-Message ("Error: " + ($_ | Out-String)) + Throw-TerminatingError ($LocalizedData.CouldNotGetHttpStream -f $scheme, $Path) $_ + } + + try + { + Trace-Message "Copying the $scheme stream bytes to the disk cache" + $responseStream.CopyTo($outStream) + $responseStream.Flush() + $outStream.Flush() + } + catch + { + Throw-TerminatingError ($LocalizedData.ErrorCopyingDataToFile -f $Path,$destName) $_ + } + } + finally + { + if($outStream) + { + $outStream.Close() + } + + if($responseStream) + { + $responseStream.Close() + } + } + Trace-Message "Redirecting package path to cache file location" + $Path = $downloadedFileName = $destName + } + } + + #At this point the Path ought to be valid unless it's an MSI uninstall case + if(-not (Test-Path -PathType Leaf $Path) -and -not ($Ensure -eq "Absent" -and $fileExtension -eq ".msi")) + { + Throw-TerminatingError ($LocalizedData.PathDoesNotExist -f $Path) + } + + $startInfo = New-Object System.Diagnostics.ProcessStartInfo + $startInfo.UseShellExecute = $false #Necessary for I/O redirection and just generally a good idea + $process = New-Object System.Diagnostics.Process + $process.StartInfo = $startInfo + $errLogPath = $LogPath + ".err" #Concept only, will never touch disk + if($fileExtension -eq ".msi") + { + $startInfo.FileName = "$env:windir\system32\msiexec.exe" + if($Ensure -eq "Present") + { + # check if Msi package contains the ProductName and Code specified + + $pName,$pCode = Get-MsiProductEntry -Path $Path + + if ( + ( (-not [String]::IsNullOrEmpty($Name)) -and ($pName -ne $Name)) ` + -or ( (-not [String]::IsNullOrEmpty($identifyingNumber)) -and ($identifyingNumber -ne $pCode)) + ) + { + Throw-InvalidNameOrIdException ($LocalizedData.InvalidNameOrId -f $Name,$identifyingNumber,$pName,$pCode) + } + + $startInfo.Arguments = '/i "{0}"' -f $Path + } + else + { + $product = Get-ProductEntry $Name $identifyingNumber + $id = Split-Path -Leaf $product.Name #We may have used the Name earlier, now we need the actual ID + $startInfo.Arguments = ("/x{0}" -f $id) + } + + if($LogPath) + { + $startInfo.Arguments += ' /log "{0}"' -f $LogPath + } + + $startInfo.Arguments += " /quiet" + + if($Arguments) + { + $startInfo.Arguments += " " + $Arguments + } + } + else #EXE + { + Trace-Message "The binary is an EXE" + $startInfo.FileName = $Path + $startInfo.Arguments = $Arguments + if($LogPath) + { + Trace-Message "User has requested logging, need to attach event handlers to the process" + $startInfo.RedirectStandardError = $true + $startInfo.RedirectStandardOutput = $true + Register-ObjectEvent -InputObject $process -EventName "OutputDataReceived" -SourceIdentifier $LogPath + Register-ObjectEvent -InputObject $process -EventName "ErrorDataReceived" -SourceIdentifier $errLogPath + } + } + + Trace-Message ("Starting {0} with {1}" -f $startInfo.FileName, $startInfo.Arguments) + + if($PSCmdlet.ShouldProcess(($LocalizedData.StartingProcessMessage -f $startInfo.FileName, $startInfo.Arguments), $null, $null)) + { + try + { + $exitCode = 0 + + if($PSBoundParameters.ContainsKey("RunAsCredential")) + { + CallPInvoke + [Source.NativeMethods]::CreateProcessAsUser("""" + $startInfo.FileName + """ " + $startInfo.Arguments, ` + $RunAsCredential.GetNetworkCredential().Domain, $RunAsCredential.GetNetworkCredential().UserName, ` + $RunAsCredential.GetNetworkCredential().Password, [ref] $exitCode) + } + else + { + $process.Start() | Out-Null + + if($logStream) #Identical to $fileExtension -eq ".exe" -and $logPath + { + $process.BeginOutputReadLine(); + $process.BeginErrorReadLine(); + } + + $process.WaitForExit() + + if($process) + { + $exitCode = $process.ExitCode + } + } + } + catch + { + Throw-TerminatingError ($LocalizedData.CouldNotStartProcess -f $Path) $_ + } + + + if($logStream) + { + #We have to re-mux these since they appear to us as different streams + #The underlying Win32 APIs prevent this problem, as would constructing a script + #on the fly and executing it, but the former is highly problematic from PowerShell + #and the latter doesn't let us get the return code for UI-based EXEs + $outputEvents = Get-Event -SourceIdentifier $LogPath + $errorEvents = Get-Event -SourceIdentifier $errLogPath + $masterEvents = @() + $outputEvents + $errorEvents + $masterEvents = $masterEvents | Sort-Object -Property TimeGenerated + + foreach($event in $masterEvents) + { + $logStream.Write($event.SourceEventArgs.Data); + } + + Remove-Event -SourceIdentifier $LogPath + Remove-Event -SourceIdentifier $errLogPath + } + + if(-not ($ReturnCode -contains $exitCode)) + { + Throw-TerminatingError ($LocalizedData.UnexpectedReturnCode -f $exitCode.ToString()) + } + } + } + finally + { + if($psdrive) + { + Remove-PSDrive -Force $psdrive + } + + if($logStream) + { + $logStream.Dispose() + } + } + + if($downloadedFileName -and $PSCmdlet.ShouldProcess($LocalizedData.RemoveDownloadedFile, $null, $null)) + { + #This is deliberately not in the Finally block. We want to leave the downloaded file on disk + #in the error case as a debugging aid for the user + rm $downloadedFileName + } + + $operationString = $LocalizedData.PackageUninstalled + if($Ensure -eq "Present") + { + $operationString = $LocalizedData.PackageInstalled + } + + # Check if reboot is required, if so notify CA. The MSFT_ServerManagerTasks provider is missing on client SKUs + $featureData = invoke-wmimethod -EA Ignore -Name GetServerFeature -namespace root\microsoft\windows\servermanager -Class MSFT_ServerManagerTasks + $regData = Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" "PendingFileRenameOperations" -EA Ignore + if(($featureData -and $featureData.RequiresReboot) -or $regData) + { + Write-Verbose $LocalizedData.MachineRequiresReboot + $global:DSCMachineStatus = 1 + } + + if($Ensure -eq "Present") + { + $productEntry = Get-ProductEntry $Name $identifyingNumber $InstalledCheckRegKey $InstalledCheckRegValueName $InstalledCheckRegValueData + if(-not $productEntry) + { + Throw-TerminatingError ($LocalizedData.PostValidationError -f $OrigPath) + } + } + + Write-Verbose $operationString + Write-Verbose $LocalizedData.PackageConfigurationComplete +} + +function CallPInvoke +{ +$script:ProgramSource = @" +using System; +using System.Collections.Generic; +using System.Text; +using System.Security; +using System.Runtime.InteropServices; +using System.Diagnostics; +using System.Security.Principal; +using System.ComponentModel; +using System.IO; + +namespace Source +{ + [SuppressUnmanagedCodeSecurity] + public static class NativeMethods + { + //The following structs and enums are used by the various Win32 API's that are used in the code below + + [StructLayout(LayoutKind.Sequential)] + public struct STARTUPINFO + { + public Int32 cb; + public string lpReserved; + public string lpDesktop; + public string lpTitle; + public Int32 dwX; + public Int32 dwY; + public Int32 dwXSize; + public Int32 dwXCountChars; + public Int32 dwYCountChars; + public Int32 dwFillAttribute; + public Int32 dwFlags; + public Int16 wShowWindow; + public Int16 cbReserved2; + public IntPtr lpReserved2; + public IntPtr hStdInput; + public IntPtr hStdOutput; + public IntPtr hStdError; + } + + [StructLayout(LayoutKind.Sequential)] + public struct PROCESS_INFORMATION + { + public IntPtr hProcess; + public IntPtr hThread; + public Int32 dwProcessID; + public Int32 dwThreadID; + } + + [Flags] + public enum LogonType + { + LOGON32_LOGON_INTERACTIVE = 2, + LOGON32_LOGON_NETWORK = 3, + LOGON32_LOGON_BATCH = 4, + LOGON32_LOGON_SERVICE = 5, + LOGON32_LOGON_UNLOCK = 7, + LOGON32_LOGON_NETWORK_CLEARTEXT = 8, + LOGON32_LOGON_NEW_CREDENTIALS = 9 + } + + [Flags] + public enum LogonProvider + { + LOGON32_PROVIDER_DEFAULT = 0, + LOGON32_PROVIDER_WINNT35, + LOGON32_PROVIDER_WINNT40, + LOGON32_PROVIDER_WINNT50 + } + [StructLayout(LayoutKind.Sequential)] + public struct SECURITY_ATTRIBUTES + { + public Int32 Length; + public IntPtr lpSecurityDescriptor; + public bool bInheritHandle; + } + + public enum SECURITY_IMPERSONATION_LEVEL + { + SecurityAnonymous, + SecurityIdentification, + SecurityImpersonation, + SecurityDelegation + } + + public enum TOKEN_TYPE + { + TokenPrimary = 1, + TokenImpersonation + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal struct TokPriv1Luid + { + public int Count; + public long Luid; + public int Attr; + } + + public const int GENERIC_ALL_ACCESS = 0x10000000; + public const int CREATE_NO_WINDOW = 0x08000000; + internal const int SE_PRIVILEGE_ENABLED = 0x00000002; + internal const int TOKEN_QUERY = 0x00000008; + internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020; + internal const string SE_INCRASE_QUOTA = "SeIncreaseQuotaPrivilege"; + + [DllImport("kernel32.dll", + EntryPoint = "CloseHandle", SetLastError = true, + CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] + public static extern bool CloseHandle(IntPtr handle); + + [DllImport("advapi32.dll", + EntryPoint = "CreateProcessAsUser", SetLastError = true, + CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)] + public static extern bool CreateProcessAsUser( + IntPtr hToken, + string lpApplicationName, + string lpCommandLine, + ref SECURITY_ATTRIBUTES lpProcessAttributes, + ref SECURITY_ATTRIBUTES lpThreadAttributes, + bool bInheritHandle, + Int32 dwCreationFlags, + IntPtr lpEnvrionment, + string lpCurrentDirectory, + ref STARTUPINFO lpStartupInfo, + ref PROCESS_INFORMATION lpProcessInformation + ); + + [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")] + public static extern bool DuplicateTokenEx( + IntPtr hExistingToken, + Int32 dwDesiredAccess, + ref SECURITY_ATTRIBUTES lpThreadAttributes, + Int32 ImpersonationLevel, + Int32 dwTokenType, + ref IntPtr phNewToken + ); + + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern Boolean LogonUser( + String lpszUserName, + String lpszDomain, + String lpszPassword, + LogonType dwLogonType, + LogonProvider dwLogonProvider, + out IntPtr phToken + ); + + [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] + internal static extern bool AdjustTokenPrivileges( + IntPtr htok, + bool disall, + ref TokPriv1Luid newst, + int len, + IntPtr prev, + IntPtr relen + ); + + [DllImport("kernel32.dll", ExactSpelling = true)] + internal static extern IntPtr GetCurrentProcess(); + + [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] + internal static extern bool OpenProcessToken( + IntPtr h, + int acc, + ref IntPtr phtok + ); + + [DllImport("kernel32.dll", ExactSpelling = true)] + internal static extern int WaitForSingleObject( + IntPtr h, + int milliseconds + ); + + [DllImport("kernel32.dll", ExactSpelling = true)] + internal static extern bool GetExitCodeProcess( + IntPtr h, + out int exitcode + ); + + [DllImport("advapi32.dll", SetLastError = true)] + internal static extern bool LookupPrivilegeValue( + string host, + string name, + ref long pluid + ); + + public static void CreateProcessAsUser(string strCommand, string strDomain, string strName, string strPassword, ref int ExitCode ) + { + var hToken = IntPtr.Zero; + var hDupedToken = IntPtr.Zero; + TokPriv1Luid tp; + var pi = new PROCESS_INFORMATION(); + var sa = new SECURITY_ATTRIBUTES(); + sa.Length = Marshal.SizeOf(sa); + Boolean bResult = false; + try + { + bResult = LogonUser( + strName, + strDomain, + strPassword, + LogonType.LOGON32_LOGON_BATCH, + LogonProvider.LOGON32_PROVIDER_DEFAULT, + out hToken + ); + if (!bResult) + { + throw new Win32Exception("Logon error #" + Marshal.GetLastWin32Error().ToString()); + } + IntPtr hproc = GetCurrentProcess(); + IntPtr htok = IntPtr.Zero; + bResult = OpenProcessToken( + hproc, + TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, + ref htok + ); + if(!bResult) + { + throw new Win32Exception("Open process token error #" + Marshal.GetLastWin32Error().ToString()); + } + tp.Count = 1; + tp.Luid = 0; + tp.Attr = SE_PRIVILEGE_ENABLED; + bResult = LookupPrivilegeValue( + null, + SE_INCRASE_QUOTA, + ref tp.Luid + ); + if(!bResult) + { + throw new Win32Exception("Lookup privilege error #" + Marshal.GetLastWin32Error().ToString()); + } + bResult = AdjustTokenPrivileges( + htok, + false, + ref tp, + 0, + IntPtr.Zero, + IntPtr.Zero + ); + if(!bResult) + { + throw new Win32Exception("Token elevation error #" + Marshal.GetLastWin32Error().ToString()); + } + + bResult = DuplicateTokenEx( + hToken, + GENERIC_ALL_ACCESS, + ref sa, + (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, + (int)TOKEN_TYPE.TokenPrimary, + ref hDupedToken + ); + if(!bResult) + { + throw new Win32Exception("Duplicate Token error #" + Marshal.GetLastWin32Error().ToString()); + } + var si = new STARTUPINFO(); + si.cb = Marshal.SizeOf(si); + si.lpDesktop = ""; + bResult = CreateProcessAsUser( + hDupedToken, + null, + strCommand, + ref sa, + ref sa, + false, + 0, + IntPtr.Zero, + null, + ref si, + ref pi + ); + if(!bResult) + { + throw new Win32Exception("Create process as user error #" + Marshal.GetLastWin32Error().ToString()); + } + + int status = WaitForSingleObject(pi.hProcess, -1); + if(status == -1) + { + throw new Win32Exception("Wait during create process failed user error #" + Marshal.GetLastWin32Error().ToString()); + } + + bResult = GetExitCodeProcess(pi.hProcess, out ExitCode); + if(!bResult) + { + throw new Win32Exception("Retrieving status error #" + Marshal.GetLastWin32Error().ToString()); + } + } + finally + { + if (pi.hThread != IntPtr.Zero) + { + CloseHandle(pi.hThread); + } + if (pi.hProcess != IntPtr.Zero) + { + CloseHandle(pi.hProcess); + } + if (hDupedToken != IntPtr.Zero) + { + CloseHandle(hDupedToken); + } + } + } + } +} + +"@ + Add-Type -TypeDefinition $ProgramSource -ReferencedAssemblies "System.ServiceProcess" +} + +#endregion + + +$params = Parse-Args $args; +$result = New-Object psobject; +Set-Attr $result "changed" $false; + +$path = Get-Attr -obj $params -name path -failifempty $true -resultobj $result +$name = Get-Attr -obj $params -name name -default $path +$productid = Get-Attr -obj $params -name productid -failifempty $true -resultobj $result +$arguments = Get-Attr -obj $params -name arguments +$ensure = Get-Attr -obj $params -name state -default "present" +if (!$ensure) +{ + $ensure = Get-Attr -obj $params -name ensure -default "present" +} +$username = Get-Attr -obj $params -name user_name +$password = Get-Attr -obj $params -name user_password +$return_code = Get-Attr -obj $params -name expected_return_code -default 0 + +#Construct the DSC param hashtable +$dscparams = @{ + name=$name + path=$path + productid = $productid + arguments = $arguments + ensure = $ensure + returncode = $return_code +} + +if (($username -ne $null) -and ($password -ne $null)) +{ + #Add network credential to the list + $secpassword = $password | ConvertTo-SecureString -AsPlainText -Force + $credential = New-Object pscredential -ArgumentList $username, $secpassword + $dscparams.add("Credential",$credential) +} + +#Always return the name +set-attr -obj $result -name "name" -value $name + +$testdscresult = Test-TargetResource @dscparams +if ($testdscresult -eq $true) +{ + Exit-Json -obj $result +} +Else +{ + try + { + set-TargetResource @dscparams + } + catch + { + $errormsg = $_[0].exception + } + + if ($errormsg) + { + Fail-Json -obj $result -message $errormsg.ToString() + } + Else + { + #Check if DSC thinks the computer needs a reboot: + if ($global:DSCMachineStatus -eq 1) + { + Set-Attr $result "restart_required" $true + } + + #Set-TargetResource did its job. We can assume a change has happened + Set-Attr $result "changed" $true + Exit-Json -obj $result + } +} + diff --git a/windows/win_package.py b/windows/win_package.py new file mode 100644 index 00000000000..3072dbed3de --- /dev/null +++ b/windows/win_package.py @@ -0,0 +1,87 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2014, Trond Hindenes , and others +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# this is a windows documentation stub. actual code lives in the .ps1 +# file of the same name + +DOCUMENTATION = ''' +--- +module: win_package +version_added: "1.7" +short_description: Installs/Uninstalls a installable package, either from local file system or url +description: + - Installs or uninstalls a package +options: + path: + description: + - Location of the package to be installed (either on file system, network share or url) + required: true + default: null + aliases: [] + name: + description: + - name of the package. Just for logging reasons, will use the value of path if name isn't specified + required: false + default: null + aliases: [] + product_id: + description: + - product id of the installed package (used for checking if already installed) + required: false + default: null + aliases: [] + arguments: + description: + - Any arguments the installer needs + default: null + aliases: [] + state: + description: + - Install or Uninstall + choices: + - present + - absent + default: present + aliases: [ensure] + user_name: + description: + - Username of an account with access to the package if its located on a file share. Only needed if the winrm user doesn't have access to the package. Also specify user_password for this to function properly. + default: null + aliases: [] + user_password: + description: + - Password of an account with access to the package if its located on a file share. Only needed if the winrm user doesn't have access to the package. Also specify user_name for this to function properly. + default: null + aliases: [] +author: Trond Hindenes +''' + +EXAMPLES = ''' +# Playbook example + - name: Install the vc thingy + win_package: + name="Microsoft Visual C thingy" + path="http://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x64.exe" + ProductId="{CF2BEA3C-26EA-32F8-AA9B-331F7E34BA97}" + Arguments="/install /passive /norestart" + + +''' + From 90a831a8285a20c368f39ded0cfc7a73ce51e036 Mon Sep 17 00:00:00 2001 From: Mark Hamilton Date: Tue, 14 Jul 2015 05:17:38 -0700 Subject: [PATCH 0415/2522] removed syslog. Generalized external id concept. Now user can add or remove multiple external ids. Added documenation about fail_main parameter. --- network/openvswitch_bridge.py | 99 ++++++++++++++++++++++------------- 1 file changed, 63 insertions(+), 36 deletions(-) diff --git a/network/openvswitch_bridge.py b/network/openvswitch_bridge.py index e26f5fea904..58ba8d740e6 100644 --- a/network/openvswitch_bridge.py +++ b/network/openvswitch_bridge.py @@ -3,6 +3,8 @@ # (c) 2013, David Stygstra # +# Portions copyright @ 2015 VMware, Inc. +# # This file is part of Ansible # # This module is free software: you can redistribute it and/or modify @@ -17,8 +19,6 @@ # # You should have received a copy of the GNU General Public License # along with this software. If not, see . -# -# Portions copyright @ 2015 VMware, Inc. All rights reserved. # pylint: disable=C0111 @@ -47,15 +47,20 @@ default: 5 description: - How long to wait for ovs-vswitchd to respond - external_id: + external_ids: + version_added: 2.0 required: false + default: None description: - - bridge external-id + - A dictionary of external-ids. Omitting this parameter is a No-op. + To clear all external-ids pass an empty value. fail_mode: + version_added: 2.0 + default: None required: false choices : [secure, standalone] description: - - bridge fail-mode + - Set bridge fail-mode. The default value (None) is a No-op. ''' EXAMPLES = ''' @@ -63,12 +68,13 @@ - openvswitch_bridge: bridge=br-int state=present # Create an integration bridge -- openvswitch_bridge: bridge=br-int state=present external_id=br-int - fail_mode=secure +- openvswitch_bridge: bridge=br-int state=present fail_mode=secure + args: + external_ids: + bridge-id: "br-int" ''' import syslog -import os class OVSBridge(object): @@ -78,7 +84,6 @@ def __init__(self, module): self.bridge = module.params['bridge'] self.state = module.params['state'] self.timeout = module.params['timeout'] - self.external_id = module.params['external_id'] self.fail_mode = module.params['fail_mode'] def _vsctl(self, command): @@ -100,8 +105,6 @@ def add(self): rtc, _, err = self._vsctl(['add-br', self.bridge]) if rtc != 0: self.module.fail_json(msg=err) - if self.external_id: - self.set_external_id() if self.fail_mode: self.set_fail_mode() @@ -118,12 +121,25 @@ def check(self): # pylint: disable=W0703 try: if self.state == 'present' and self.exists(): - if (self.external_id and - (self.external_id != self.get_external_id())): - changed = True if (self.fail_mode and (self.fail_mode != self.get_fail_mode())): changed = True + + ## + # Check if external ids would change. + current_external_ids = self.get_external_ids() + items = self.module.params['external_ids'].items() + for (key, value) in items: + if ((key in current_external_ids) and + (value != current_external_ids[key])): + changed = True + + ## + # Check if external ids would be removed. + for (key, value) in current_external_ids.items(): + if key not in self.module.params['external_ids']: + changed = True + elif self.state == 'absent' and self.exists(): changed = True elif self.state == 'present' and not self.exists(): @@ -145,15 +161,11 @@ def run(self): self.delete() changed = True elif self.state == 'present': + if not self.exists(): self.add() changed = True - if (self.external_id and - (self.external_id != self.get_external_id())): - self.set_external_id() - changed = True - current_fail_mode = self.get_fail_mode() if self.fail_mode and (self.fail_mode != current_fail_mode): syslog.syslog(syslog.LOG_NOTICE, @@ -162,31 +174,50 @@ def run(self): self.set_fail_mode() changed = True + current_external_ids = self.get_external_ids() + + ## + # Change and add existing external ids. + items = self.module.params['external_ids'].items() + for (key, value) in items: + if (value != current_external_ids.get(key, None)): + changed = self.set_external_id(key, value) or changed + + ## + # Remove current external ids that are not passed in. + for (key, value) in current_external_ids.items(): + if key not in self.module.params['external_ids']: + changed = self.set_external_id(key, None) or changed + except Exception, earg: self.module.fail_json(msg=str(earg)) # pylint: enable=W0703 self.module.exit_json(changed=changed) - def get_external_id(self): - """ Return the current external id. """ - value = '' + def get_external_ids(self): + """ Return the bridge's external ids as a dict. """ if self.exists(): rtc, out, err = self._vsctl(['br-get-external-id', self.bridge]) if rtc != 0: self.module.fail_json(msg=err) - try: - (_, value) = out.split('=') - except ValueError: - pass - return value.strip("\n") + lines = out.split("\n") + lines = [item.split("=") for item in lines if (len(item) > 0)] + return {item[0]: item[1] for item in lines} - def set_external_id(self): + return {} + + def set_external_id(self, key, value): """ Set external id. """ if self.exists(): - (rtc, _, err) = self._vsctl(['br-set-external-id', self.bridge, - 'bridge-id', self.external_id]) + cmd = ['br-set-external-id', self.bridge, key] + if (value): + cmd += [value] + + (rtc, _, err) = self._vsctl(cmd) if rtc != 0: self.module.fail_json(msg=err) + return True + return False def get_fail_mode(self): """ Get failure mode. """ @@ -216,16 +247,12 @@ def main(): 'bridge': {'required': True}, 'state': {'default': 'present', 'choices': ['present', 'absent']}, 'timeout': {'default': 5, 'type': 'int'}, - 'external_id': {'default': ''}, - 'fail_mode': {'default': ''}, - 'syslogging': {'required': False, 'type': 'bool', 'default': True} + 'external_ids': {'default': None}, + 'fail_mode': {'default': None}, }, supports_check_mode=True, ) - if (module.params["syslogging"]): - syslog.openlog('ansible-%s' % os.path.basename(__file__)) - bridge = OVSBridge(module) if module.check_mode: bridge.check() From ff2386faf49dd44964fac084ed7199ab4ea5f741 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Tue, 14 Jul 2015 07:30:41 -0700 Subject: [PATCH 0416/2522] Tabs to spaces Fixes #666 --- packaging/language/bundler.py | 144 +++++++++++++++++----------------- 1 file changed, 72 insertions(+), 72 deletions(-) diff --git a/packaging/language/bundler.py b/packaging/language/bundler.py index e98350a7b70..f4aeff4156f 100644 --- a/packaging/language/bundler.py +++ b/packaging/language/bundler.py @@ -129,81 +129,81 @@ def get_bundler_executable(module): - if module.params.get('executable'): - return module.params.get('executable').split(' ') - else: - return [ module.get_bin_path('bundle', True) ] + if module.params.get('executable'): + return module.params.get('executable').split(' ') + else: + return [ module.get_bin_path('bundle', True) ] def main(): - module = AnsibleModule( - argument_spec=dict( - executable=dict(default=None, required=False), - state=dict(default='present', required=False, choices=['present', 'latest']), - chdir=dict(default=None, required=False), - exclude_groups=dict(default=None, required=False, type='list'), - clean=dict(default=False, required=False, type='bool'), - gemfile=dict(default=None, required=False), - local=dict(default=False, required=False, type='bool'), - deployment_mode=dict(default=False, required=False, type='bool'), - user_install=dict(default=True, required=False, type='bool'), - gem_path=dict(default=None, required=False), - binstub_directory=dict(default=None, required=False), - extra_args=dict(default=None, required=False), - ), - supports_check_mode=True - ) - - executable = module.params.get('executable') - state = module.params.get('state') - chdir = module.params.get('chdir') - exclude_groups = module.params.get('exclude_groups') - clean = module.params.get('clean') - gemfile = module.params.get('gemfile') - local = module.params.get('local') - deployment_mode = module.params.get('deployment_mode') - user_install = module.params.get('user_install') - gem_path = module.params.get('gem_install_path') - binstub_directory = module.params.get('binstub_directory') - extra_args = module.params.get('extra_args') - - cmd = get_bundler_executable(module) - - if module.check_mode: - cmd.append('check') - rc, out, err = module.run_command(cmd, cwd=chdir, check_rc=False) - - module.exit_json(changed=rc != 0, state=state, stdout=out, stderr=err) - - if state == 'present': - cmd.append('install') - if exclude_groups: - cmd.extend(['--without', ':'.join(exclude_groups)]) - if clean: - cmd.append('--clean') - if gemfile: - cmd.extend(['--gemfile', gemfile]) - if local: - cmd.append('--local') - if deployment_mode: - cmd.append('--deployment') - if not user_install: - cmd.append('--system') - if gem_path: - cmd.extend(['--path', gem_path]) - if binstub_directory: - cmd.extend(['--binstubs', binstub_directory]) - else: - cmd.append('update') - if local: - cmd.append('--local') - - if extra_args: - cmd.extend(extra_args.split(' ')) - - rc, out, err = module.run_command(cmd, cwd=chdir, check_rc=True) - - module.exit_json(changed='Installing' in out, state=state, stdout=out, stderr=err) + module = AnsibleModule( + argument_spec=dict( + executable=dict(default=None, required=False), + state=dict(default='present', required=False, choices=['present', 'latest']), + chdir=dict(default=None, required=False), + exclude_groups=dict(default=None, required=False, type='list'), + clean=dict(default=False, required=False, type='bool'), + gemfile=dict(default=None, required=False), + local=dict(default=False, required=False, type='bool'), + deployment_mode=dict(default=False, required=False, type='bool'), + user_install=dict(default=True, required=False, type='bool'), + gem_path=dict(default=None, required=False), + binstub_directory=dict(default=None, required=False), + extra_args=dict(default=None, required=False), + ), + supports_check_mode=True + ) + + executable = module.params.get('executable') + state = module.params.get('state') + chdir = module.params.get('chdir') + exclude_groups = module.params.get('exclude_groups') + clean = module.params.get('clean') + gemfile = module.params.get('gemfile') + local = module.params.get('local') + deployment_mode = module.params.get('deployment_mode') + user_install = module.params.get('user_install') + gem_path = module.params.get('gem_install_path') + binstub_directory = module.params.get('binstub_directory') + extra_args = module.params.get('extra_args') + + cmd = get_bundler_executable(module) + + if module.check_mode: + cmd.append('check') + rc, out, err = module.run_command(cmd, cwd=chdir, check_rc=False) + + module.exit_json(changed=rc != 0, state=state, stdout=out, stderr=err) + + if state == 'present': + cmd.append('install') + if exclude_groups: + cmd.extend(['--without', ':'.join(exclude_groups)]) + if clean: + cmd.append('--clean') + if gemfile: + cmd.extend(['--gemfile', gemfile]) + if local: + cmd.append('--local') + if deployment_mode: + cmd.append('--deployment') + if not user_install: + cmd.append('--system') + if gem_path: + cmd.extend(['--path', gem_path]) + if binstub_directory: + cmd.extend(['--binstubs', binstub_directory]) + else: + cmd.append('update') + if local: + cmd.append('--local') + + if extra_args: + cmd.extend(extra_args.split(' ')) + + rc, out, err = module.run_command(cmd, cwd=chdir, check_rc=True) + + module.exit_json(changed='Installing' in out, state=state, stdout=out, stderr=err) from ansible.module_utils.basic import * From f82a363a33aa8c70f0003cb8efd8c95ada2daf4e Mon Sep 17 00:00:00 2001 From: "Mehmet Ali \"Mali\" Akmanalp" Date: Tue, 14 Jul 2015 14:22:15 -0400 Subject: [PATCH 0417/2522] Fixes issues where keys missing from bower output Fixes #495 --- packaging/language/bower.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packaging/language/bower.py b/packaging/language/bower.py index 7af8136a445..bd7d4b26159 100644 --- a/packaging/language/bower.py +++ b/packaging/language/bower.py @@ -116,11 +116,15 @@ def list(self): data = json.loads(self._exec(cmd, True, False)) if 'dependencies' in data: for dep in data['dependencies']: - if 'missing' in data['dependencies'][dep] and data['dependencies'][dep]['missing']: + dep_data = data['dependencies'][dep] + if dep_data.get('missing', False): missing.append(dep) - elif data['dependencies'][dep]['pkgMeta']['version'] != data['dependencies'][dep]['update']['latest']: + elif \ + 'version' in dep_data['pkgMeta'] and \ + 'update' in dep_data and \ + dep_data['pkgMeta']['version'] != dep_data['update']['latest']: outdated.append(dep) - elif 'incompatible' in data['dependencies'][dep] and data['dependencies'][dep]['incompatible']: + elif dep_data.get('incompatible', False): outdated.append(dep) else: installed.append(dep) From 8a41108b2b0f25f709ee7939f918550a44b81fe4 Mon Sep 17 00:00:00 2001 From: Siva Popuri Date: Tue, 14 Jul 2015 16:36:50 -0500 Subject: [PATCH 0418/2522] corrected the license string to make it compatible with GPLV3 --- cloud/centurylink/clc_publicip.py | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/cloud/centurylink/clc_publicip.py b/cloud/centurylink/clc_publicip.py index ed3228e1996..77632c1cbfe 100644 --- a/cloud/centurylink/clc_publicip.py +++ b/cloud/centurylink/clc_publicip.py @@ -1,29 +1,22 @@ #!/usr/bin/python -# CenturyLink Cloud Ansible Modules. # -# These Ansible modules enable the CenturyLink Cloud v2 API to be called -# from an within Ansible Playbook. +# Copyright (c) 2015 CenturyLink # -# This file is part of CenturyLink Cloud, and is maintained -# by the Workflow as a Service Team +# This file is part of Ansible. # -# Copyright 2015 CenturyLink Cloud +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. # -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# CenturyLink Cloud: http://www.CenturyLinkCloud.com -# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see # DOCUMENTATION = ''' From 05e01bd3b50404371a8f9ee1128ff08925e8b906 Mon Sep 17 00:00:00 2001 From: Phil Date: Tue, 14 Jul 2015 18:35:28 -0500 Subject: [PATCH 0419/2522] updates user search verification for local/domain - Thanks to @trondhindenes for implementing this strategy - also updated documentation --- windows/win_acl.ps1 | 110 ++++++++++++++++++++++++++++++++++---------- windows/win_acl.py | 4 +- 2 files changed, 87 insertions(+), 27 deletions(-) diff --git a/windows/win_acl.ps1 b/windows/win_acl.ps1 index 39530866c61..ec72828a531 100644 --- a/windows/win_acl.ps1 +++ b/windows/win_acl.ps1 @@ -15,21 +15,82 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . - + # WANT_JSON # POWERSHELL_COMMON - + # win_acl module (File/Resources Permission Additions/Removal) + + +#Functions +Function UserSearch +{ + Param ([string]$AccountName) + #Check if there's a realm specified + if ($AccountName.Split("\").count -gt 1) + { + if ($AccountName.Split("\")[0] -eq $env:COMPUTERNAME) + { + $IsLocalAccount = $true + } + Else + { + $IsDomainAccount = $true + $IsUpn = $false + } + + } + Elseif ($AccountName -contains "@") + { + $IsDomainAccount = $true + $IsUpn = $true + } + Else + { + #Default to local user account + $accountname = $env:COMPUTERNAME + "\" + $AccountName + $IsLocalAccount = $true + } + + + if ($IsLocalAccount -eq $true) + { + $localaccount = get-wmiobject -class "Win32_UserAccount" -namespace "root\CIMV2" -filter "(LocalAccount = True)" | where {$_.Caption -eq $AccountName} + if ($localaccount) + { + return $localaccount.Caption + } + $LocalGroup = get-wmiobject -class "Win32_Group" -namespace "root\CIMV2" -filter "LocalAccount = True"| where {$_.Caption -eq $AccountName} + if ($LocalGroup) + { + return $LocalGroup.Caption + } + } + ElseIf (($IsDomainAccount -eq $true) -and ($IsUpn -eq $false)) + { + #Search by samaccountname + $Searcher = [adsisearcher]"" + $Searcher.Filter = "sAMAccountName=$($accountname.split("\")[1])" + $result = $Searcher.FindOne() + + if ($result) + { + return $accountname + } + } + +} + $params = Parse-Args $args; - + $result = New-Object psobject @{ win_acl = New-Object psobject changed = $false } - + If ($params.src) { $src = $params.src.toString() - + If (-Not (Test-Path -Path $src)) { Fail-Json $result "$src file or directory does not exist on the host" } @@ -37,21 +98,20 @@ If ($params.src) { Else { Fail-Json $result "missing required argument: src" } - + If ($params.user) { - $user = $params.user.toString() - - # Test that the user/group exists on the local machine - $localComputer = [ADSI]("WinNT://"+[System.Net.Dns]::GetHostName()) - $list = ($localComputer.psbase.children | Where-Object { (($_.psBase.schemaClassName -eq "User") -Or ($_.psBase.schemaClassName -eq "Group"))} | Select-Object -expand Name) - If (-Not ($list -contains "$user")) { - Fail-Json $result "$user is not a valid user or group on the host machine" - } + $user = UserSearch -AccountName ($Params.User) + + # Test that the user/group is resolvable on the local machine + if (!$user) + { + Fail-Json $result "$($Params.User) is not a valid user or group on the host machine or domain" + } } Else { Fail-Json $result "missing required argument: user. specify the user or group to apply permission changes." } - + If ($params.type -eq "allow") { $type = $true } @@ -61,7 +121,7 @@ ElseIf ($params.type -eq "deny") { Else { Fail-Json $result "missing required argument: type. specify whether to allow or deny the specified rights." } - + If ($params.inherit) { # If it's a file then no flags can be set or an exception will be thrown If (Test-Path -Path $src -PathType Leaf) { @@ -80,44 +140,44 @@ Else { $inherit = "ContainerInherit, ObjectInherit" } } - + If ($params.propagation) { $propagation = $params.propagation.toString() } Else { $propagation = "None" } - + If ($params.rights) { $rights = $params.rights.toString() } Else { Fail-Json $result "missing required argument: rights" } - + If ($params.state -eq "absent") { $state = "remove" } Else { $state = "add" } - + Try { $colRights = [System.Security.AccessControl.FileSystemRights]$rights $InheritanceFlag = [System.Security.AccessControl.InheritanceFlags]$inherit $PropagationFlag = [System.Security.AccessControl.PropagationFlags]$propagation - + If ($type) { $objType =[System.Security.AccessControl.AccessControlType]::Allow } Else { $objType =[System.Security.AccessControl.AccessControlType]::Deny } - + $objUser = New-Object System.Security.Principal.NTAccount($user) $objACE = New-Object System.Security.AccessControl.FileSystemAccessRule ($objUser, $colRights, $InheritanceFlag, $PropagationFlag, $objType) $objACL = Get-ACL $src - + # Check if the ACE exists already in the objects ACL list $match = $false ForEach($rule in $objACL.Access){ @@ -126,7 +186,7 @@ Try { Break } } - + If ($state -eq "add" -And $match -eq $false) { Try { $objACL.AddAccessRule($objACE) @@ -161,5 +221,5 @@ Try { Catch { Fail-Json $result "an error occured when attempting to $state $rights permission(s) on $src for $user" } - + Exit-Json $result \ No newline at end of file diff --git a/windows/win_acl.py b/windows/win_acl.py index 96cfc5751b9..b3ddcce7ac8 100644 --- a/windows/win_acl.py +++ b/windows/win_acl.py @@ -24,7 +24,7 @@ DOCUMENTATION = ''' --- module: win_acl -version_added: "" +version_added: "2.0" short_description: Set file/directory permissions for a system user or group. description: - Add or remove rights/permissions for a given user or group for the specified src file or folder. @@ -107,7 +107,7 @@ - InheritOnly default: "None" aliases: [] -author: Phil Schwartz +author: Phil Schwartz, Trond Hindenes ''' EXAMPLES = ''' From 26bdf16e695a4407303d1e856d6aa17c5b5ad552 Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 15 Jul 2015 11:42:27 -0500 Subject: [PATCH 0420/2522] changes param src to path, and updates docs accordingly --- windows/win_acl.ps1 | 22 +++++++++++----------- windows/win_acl.py | 10 +--------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/windows/win_acl.ps1 b/windows/win_acl.ps1 index ec72828a531..b08fb03e7f3 100644 --- a/windows/win_acl.ps1 +++ b/windows/win_acl.ps1 @@ -88,15 +88,15 @@ $result = New-Object psobject @{ changed = $false } -If ($params.src) { - $src = $params.src.toString() +If ($params.path) { + $path = $params.path.toString() - If (-Not (Test-Path -Path $src)) { - Fail-Json $result "$src file or directory does not exist on the host" + If (-Not (Test-Path -Path $path)) { + Fail-Json $result "$path file or directory does not exist on the host" } } Else { - Fail-Json $result "missing required argument: src" + Fail-Json $result "missing required argument: path" } If ($params.user) { @@ -124,7 +124,7 @@ Else { If ($params.inherit) { # If it's a file then no flags can be set or an exception will be thrown - If (Test-Path -Path $src -PathType Leaf) { + If (Test-Path -Path $path -PathType Leaf) { $inherit = "None" } Else { @@ -133,7 +133,7 @@ If ($params.inherit) { } Else { # If it's a file then no flags can be set or an exception will be thrown - If (Test-Path -Path $src -PathType Leaf) { + If (Test-Path -Path $path -PathType Leaf) { $inherit = "None" } Else { @@ -176,7 +176,7 @@ Try { $objUser = New-Object System.Security.Principal.NTAccount($user) $objACE = New-Object System.Security.AccessControl.FileSystemAccessRule ($objUser, $colRights, $InheritanceFlag, $PropagationFlag, $objType) - $objACL = Get-ACL $src + $objACL = Get-ACL $path # Check if the ACE exists already in the objects ACL list $match = $false @@ -190,7 +190,7 @@ Try { If ($state -eq "add" -And $match -eq $false) { Try { $objACL.AddAccessRule($objACE) - Set-ACL $src $objACL + Set-ACL $path $objACL $result.changed = $true } Catch { @@ -200,7 +200,7 @@ Try { ElseIf ($state -eq "remove" -And $match -eq $true) { Try { $objACL.RemoveAccessRule($objACE) - Set-ACL $src $objACL + Set-ACL $path $objACL $result.changed = $true } Catch { @@ -219,7 +219,7 @@ Try { } } Catch { - Fail-Json $result "an error occured when attempting to $state $rights permission(s) on $src for $user" + Fail-Json $result "an error occured when attempting to $state $rights permission(s) on $path for $user" } Exit-Json $result \ No newline at end of file diff --git a/windows/win_acl.py b/windows/win_acl.py index b3ddcce7ac8..7a1b256ef83 100644 --- a/windows/win_acl.py +++ b/windows/win_acl.py @@ -29,18 +29,15 @@ description: - Add or remove rights/permissions for a given user or group for the specified src file or folder. options: - src: + path: description: - File or Directory required: yes - default: none - aliases: [] user: description: - User or Group to add specified rights to act on src file/folder required: yes default: none - aliases: [] state: description: - Specify whether to add (present) or remove (absent) the specified access rule @@ -49,7 +46,6 @@ - present - absent default: present - aliases: [] type: description: - Specify whether to allow or deny the rights specified @@ -58,7 +54,6 @@ - allow - deny default: none - aliases: [] rights: description: - The rights/permissions that are to be allowed/denyed for the specified user or group for the given src file or directory. Can be entered as a comma separated list (Ex. "Modify, Delete, ExecuteFile"). For more information on the choices see MSDN FileSystemRights Enumeration. @@ -86,7 +81,6 @@ - WriteData - WriteExtendedAttributes default: none - aliases: [] inherit: description: - Inherit flags on the ACL rules. Can be specified as a comma separated list (Ex. "ContainerInherit, ObjectInherit"). For more information on the choices see MSDN InheritanceFlags Enumeration. @@ -96,7 +90,6 @@ - ObjectInherit - None default: For Leaf File: None; For Directory: ContainerInherit, ObjectInherit; - aliases: [] propagation: description: - Propagation flag on the ACL rules. For more information on the choices see MSDN PropagationFlags Enumeration. @@ -106,7 +99,6 @@ - NoPropagateInherit - InheritOnly default: "None" - aliases: [] author: Phil Schwartz, Trond Hindenes ''' From a6f3a0e0a92079a42f723b852073d034c8d35028 Mon Sep 17 00:00:00 2001 From: Mark Hamilton Date: Thu, 16 Jul 2015 10:05:59 -0700 Subject: [PATCH 0421/2522] Changed syntax to support python2.4. Allow external_ids to be None. --- network/openvswitch_bridge.py | 56 +++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/network/openvswitch_bridge.py b/network/openvswitch_bridge.py index 58ba8d740e6..2396b1f2b49 100644 --- a/network/openvswitch_bridge.py +++ b/network/openvswitch_bridge.py @@ -128,17 +128,18 @@ def check(self): ## # Check if external ids would change. current_external_ids = self.get_external_ids() - items = self.module.params['external_ids'].items() - for (key, value) in items: - if ((key in current_external_ids) and - (value != current_external_ids[key])): - changed = True - - ## - # Check if external ids would be removed. - for (key, value) in current_external_ids.items(): - if key not in self.module.params['external_ids']: - changed = True + exp_external_ids = self.module.params['external_ids'] + if exp_external_ids is not None: + for (key, value) in exp_external_ids: + if ((key in current_external_ids) and + (value != current_external_ids[key])): + changed = True + + ## + # Check if external ids would be removed. + for (key, value) in current_external_ids.items(): + if key not in exp_external_ids: + changed = True elif self.state == 'absent' and self.exists(): changed = True @@ -178,16 +179,19 @@ def run(self): ## # Change and add existing external ids. - items = self.module.params['external_ids'].items() - for (key, value) in items: - if (value != current_external_ids.get(key, None)): - changed = self.set_external_id(key, value) or changed - - ## - # Remove current external ids that are not passed in. - for (key, value) in current_external_ids.items(): - if key not in self.module.params['external_ids']: - changed = self.set_external_id(key, None) or changed + exp_external_ids = self.module.params['external_ids'] + if exp_external_ids is not None: + for (key, value) in exp_external_ids.items(): + if ((value != current_external_ids.get(key, None)) and + self.set_external_id(key, value)): + changed = True + + ## + # Remove current external ids that are not passed in. + for (key, value) in current_external_ids.items(): + if ((key not in exp_external_ids) and + self.set_external_id(key, None)): + changed = True except Exception, earg: self.module.fail_json(msg=str(earg)) @@ -196,21 +200,23 @@ def run(self): def get_external_ids(self): """ Return the bridge's external ids as a dict. """ + results = {} if self.exists(): rtc, out, err = self._vsctl(['br-get-external-id', self.bridge]) if rtc != 0: self.module.fail_json(msg=err) lines = out.split("\n") - lines = [item.split("=") for item in lines if (len(item) > 0)] - return {item[0]: item[1] for item in lines} + lines = [item.split("=") for item in lines if len(item) > 0] + for item in lines: + results[item[0]] = item[1] - return {} + return results def set_external_id(self, key, value): """ Set external id. """ if self.exists(): cmd = ['br-set-external-id', self.bridge, key] - if (value): + if value: cmd += [value] (rtc, _, err) = self._vsctl(cmd) From 4e140bb80e15b85da3ff724f30c5d4342e0dd544 Mon Sep 17 00:00:00 2001 From: Mathew Davies Date: Sun, 11 Jan 2015 16:42:45 +0000 Subject: [PATCH 0422/2522] Add Elasticsearch plugin module --- packaging/elasticsearch_plugin.py | 160 ++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 packaging/elasticsearch_plugin.py diff --git a/packaging/elasticsearch_plugin.py b/packaging/elasticsearch_plugin.py new file mode 100644 index 00000000000..38303686e8d --- /dev/null +++ b/packaging/elasticsearch_plugin.py @@ -0,0 +1,160 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +import os + +from ansible.module_utils.basic import * + +""" +Ansible module to manage elasticsearch plugins +(c) 2015, Mathew Davies + +This file is part of Ansible + +Ansible is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Ansible is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +You should have received a copy of the GNU General Public License +along with Ansible. If not, see . +""" + +DOCUMENTATION = ''' +--- +module: elasticsearch_plugin +short_description: Manage Elasticsearch plugins +description: + - Manages Elasticsearch plugins. +version_added: "" +author: Mathew Davies (@ThePixelDeveloper) +options: + name: + description: + - Name of the plugin to install + required: True + state: + description: + - Desired state of a plugin. + required: False + choices: [present, absent] + default: present + url: + description: + - Set exact URL to download the plugin from + required: False + timeout: + description: + - Timeout setting: 30s, 1m, 1h... (1m by default) + required: False + plugin_bin: + description: + - Location of the plugin binary + required: False + default: /usr/share/elasticsearch/bin/plugin + plugin_dir: + description: + - Your configured plugin directory specified in Elasticsearch + required: False + default: /usr/share/elasticsearch/plugins/ + version: + description: + - Version of the plugin to be installed. +''' + +EXAMPLES = ''' +# Install Elasticsearch head plugin +- elasticsearch_plugin: state=present name="mobz/elasticsearch-head" +''' + + +def parse_plugin_repo(string): + elements = string.split("/") + + # We first consider the simplest form: pluginname + repo = elements[0] + + # We consider the form: username/pluginname + if len(elements) > 1: + repo = elements[1] + + # remove elasticsearch- prefix + # remove es- prefix + for string in ("elasticsearch-", "es-"): + if repo.startswith(string): + return repo[len(string):] + + return repo + + +def is_plugin_present(plugin_dir, working_dir): + return os.path.isdir(os.path.join(working_dir, plugin_dir)) + + +def parse_error(string): + reason = "reason: " + return string[string.index(reason) + len(reason):].strip() + + +def main(): + + package_state_map = dict( + present="--install", + absent="--remove" + ) + + module = AnsibleModule( + argument_spec=dict( + name=dict(required=True), + state=dict(default="present", choices=package_state_map.keys()), + url=dict(default=None), + timeout=dict(default="1m"), + plugin_bin=dict(default="/usr/share/elasticsearch/bin/plugin"), + plugin_dir=dict(default="/usr/share/elasticsearch/plugins/"), + version=dict(default=None) + ) + ) + + plugin_bin = module.params["plugin_bin"] + plugin_dir = module.params["plugin_dir"] + name = module.params["name"] + state = module.params["state"] + url = module.params["url"] + timeout = module.params["timeout"] + version = module.params["version"] + + present = is_plugin_present(parse_plugin_repo(name), plugin_dir) + + print state + + # skip if the state is correct + if (present and state == "present") or (state == "absent" and not present): + module.exit_json(changed=False, name=name) + + if (version): + name = name + '/' + version + + cmd_args = [plugin_bin, package_state_map[state], name] + + if url: + cmd_args.append("--url %s" % url) + + if timeout: + cmd_args.append("--timeout %s" % timeout) + + cmd = " ".join(cmd_args) + + rc, out, err = module.run_command(cmd) + + if rc != 0: + reason = parse_error(out) + module.fail_json(msg=reason) + + module.exit_json(changed=True, cmd=cmd, name=name, state=state, url=url, timeout=timeout, stdout=out, stderr=err) + +if __name__ == "__main__": + main() From ebbe84b2d6fda94f3d40c7bbbd95ba48bfddb65d Mon Sep 17 00:00:00 2001 From: Mathew Davies Date: Thu, 16 Jul 2015 20:38:58 +0100 Subject: [PATCH 0423/2522] Document defaults --- packaging/elasticsearch_plugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packaging/elasticsearch_plugin.py b/packaging/elasticsearch_plugin.py index 38303686e8d..818ebb00484 100644 --- a/packaging/elasticsearch_plugin.py +++ b/packaging/elasticsearch_plugin.py @@ -47,6 +47,7 @@ description: - Set exact URL to download the plugin from required: False + default: None timeout: description: - Timeout setting: 30s, 1m, 1h... (1m by default) @@ -64,6 +65,7 @@ version: description: - Version of the plugin to be installed. + default: None ''' EXAMPLES = ''' From 93e59297f0f71e85d18482aa2abe1733720c5d66 Mon Sep 17 00:00:00 2001 From: Mathew Davies Date: Thu, 16 Jul 2015 20:55:50 +0100 Subject: [PATCH 0424/2522] Remove debugging line --- packaging/elasticsearch_plugin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/packaging/elasticsearch_plugin.py b/packaging/elasticsearch_plugin.py index 818ebb00484..3c0d6124e82 100644 --- a/packaging/elasticsearch_plugin.py +++ b/packaging/elasticsearch_plugin.py @@ -130,8 +130,6 @@ def main(): version = module.params["version"] present = is_plugin_present(parse_plugin_repo(name), plugin_dir) - - print state # skip if the state is correct if (present and state == "present") or (state == "absent" and not present): From 045f0908e2002a98e8b50200530a32872dcb87d5 Mon Sep 17 00:00:00 2001 From: Mathew Davies Date: Thu, 16 Jul 2015 20:56:05 +0100 Subject: [PATCH 0425/2522] Add required property to version documentation --- packaging/elasticsearch_plugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packaging/elasticsearch_plugin.py b/packaging/elasticsearch_plugin.py index 3c0d6124e82..15821eb6adc 100644 --- a/packaging/elasticsearch_plugin.py +++ b/packaging/elasticsearch_plugin.py @@ -65,6 +65,7 @@ version: description: - Version of the plugin to be installed. + required: False default: None ''' From 394053ff2bab918d905a44ba11704aa0ebf39124 Mon Sep 17 00:00:00 2001 From: Mathew Davies Date: Thu, 16 Jul 2015 20:56:45 +0100 Subject: [PATCH 0426/2522] Add default documentation for timeout --- packaging/elasticsearch_plugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packaging/elasticsearch_plugin.py b/packaging/elasticsearch_plugin.py index 15821eb6adc..4838d478ef4 100644 --- a/packaging/elasticsearch_plugin.py +++ b/packaging/elasticsearch_plugin.py @@ -50,8 +50,9 @@ default: None timeout: description: - - Timeout setting: 30s, 1m, 1h... (1m by default) + - Timeout setting: 30s, 1m, 1h... required: False + default: 1m plugin_bin: description: - Location of the plugin binary From 6fa1809ec4007faf623b955e8a811e87aa87c3b9 Mon Sep 17 00:00:00 2001 From: Mathew Davies Date: Thu, 16 Jul 2015 21:00:15 +0100 Subject: [PATCH 0427/2522] Move ansible util import to the bottom of the module --- packaging/elasticsearch_plugin.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packaging/elasticsearch_plugin.py b/packaging/elasticsearch_plugin.py index 4838d478ef4..34b028accaf 100644 --- a/packaging/elasticsearch_plugin.py +++ b/packaging/elasticsearch_plugin.py @@ -3,8 +3,6 @@ import os -from ansible.module_utils.basic import * - """ Ansible module to manage elasticsearch plugins (c) 2015, Mathew Davies @@ -158,5 +156,6 @@ def main(): module.exit_json(changed=True, cmd=cmd, name=name, state=state, url=url, timeout=timeout, stdout=out, stderr=err) -if __name__ == "__main__": - main() +from ansible.module_utils.basic import * + +main() From fb42f6effcbed7980ff9c1db9a5f85ffa3d1183e Mon Sep 17 00:00:00 2001 From: Mathew Davies Date: Thu, 16 Jul 2015 21:12:42 +0100 Subject: [PATCH 0428/2522] Note that the plugin can't be updated once installed --- packaging/elasticsearch_plugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packaging/elasticsearch_plugin.py b/packaging/elasticsearch_plugin.py index 34b028accaf..c263388b9e6 100644 --- a/packaging/elasticsearch_plugin.py +++ b/packaging/elasticsearch_plugin.py @@ -63,7 +63,8 @@ default: /usr/share/elasticsearch/plugins/ version: description: - - Version of the plugin to be installed. + - Version of the plugin to be installed. + If plugin exists with previous version, it will NOT be updated required: False default: None ''' From 2d2ea412aeef207af26906bd78a30b29486dbfd9 Mon Sep 17 00:00:00 2001 From: Mathew Davies Date: Thu, 16 Jul 2015 21:15:15 +0100 Subject: [PATCH 0429/2522] Add more examples --- packaging/elasticsearch_plugin.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packaging/elasticsearch_plugin.py b/packaging/elasticsearch_plugin.py index c263388b9e6..f1053144e12 100644 --- a/packaging/elasticsearch_plugin.py +++ b/packaging/elasticsearch_plugin.py @@ -72,6 +72,12 @@ EXAMPLES = ''' # Install Elasticsearch head plugin - elasticsearch_plugin: state=present name="mobz/elasticsearch-head" + +# Install specific version of a plugin +- elasticsearch_plugin: state=present name="com.github.kzwang/elasticsearch-image" version="1.2.0" + +# Uninstall Elasticsearch head plugin +- elasticsearch_plugin: state=absent name="mobz/elasticsearch-head" ''' From 7b2f2b766799e179c0220aff92b0464e6782a927 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 16 Jul 2015 17:55:20 -0400 Subject: [PATCH 0430/2522] added version added --- packaging/elasticsearch_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/elasticsearch_plugin.py b/packaging/elasticsearch_plugin.py index f1053144e12..6cddd8643c3 100644 --- a/packaging/elasticsearch_plugin.py +++ b/packaging/elasticsearch_plugin.py @@ -28,7 +28,7 @@ short_description: Manage Elasticsearch plugins description: - Manages Elasticsearch plugins. -version_added: "" +version_added: "2.0" author: Mathew Davies (@ThePixelDeveloper) options: name: From bbc0f853d06fa2c28b097723e7f9bc92a9ba8107 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 16 Jul 2015 18:02:42 -0400 Subject: [PATCH 0431/2522] minor doc fixes --- packaging/elasticsearch_plugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packaging/elasticsearch_plugin.py b/packaging/elasticsearch_plugin.py index 6cddd8643c3..7b092a13667 100644 --- a/packaging/elasticsearch_plugin.py +++ b/packaging/elasticsearch_plugin.py @@ -48,7 +48,7 @@ default: None timeout: description: - - Timeout setting: 30s, 1m, 1h... + - "Timeout setting: 30s, 1m, 1h..." required: False default: 1m plugin_bin: @@ -62,8 +62,8 @@ required: False default: /usr/share/elasticsearch/plugins/ version: - description: - - Version of the plugin to be installed. + description: + - Version of the plugin to be installed. If plugin exists with previous version, it will NOT be updated required: False default: None @@ -141,7 +141,7 @@ def main(): # skip if the state is correct if (present and state == "present") or (state == "absent" and not present): module.exit_json(changed=False, name=name) - + if (version): name = name + '/' + version From 2754157d87d0c3db2c769a68fb0fa63f2dd53611 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Fri, 17 Jul 2015 00:48:33 -0400 Subject: [PATCH 0432/2522] minor doc fixes --- windows/win_unzip.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/windows/win_unzip.py b/windows/win_unzip.py index 7c5ac322b97..799ab1bda31 100644 --- a/windows/win_unzip.py +++ b/windows/win_unzip.py @@ -24,7 +24,7 @@ DOCUMENTATION = ''' --- module: win_unzip -version_added: "" +version_added: "2.0" short_description: Unzips compressed files on the Windows node description: - Unzips compressed files, and can force reboot (if needed, i.e. such as hotfixes). Has ability to recursively unzip files within the src zip file provided using Read-Archive and piping to Expand-Archive (Using PSCX). If the destination directory does not exist, it will be created before unzipping the file. If a .zip file is specified as src and recurse is true then PSCX will be installed. Specifying rm parameter will allow removal of the src file after extraction. @@ -33,14 +33,10 @@ description: - File to be unzipped (provide absolute path) required: true - default: null - aliases: [] dest: description: - Destination of zip file (provide absolute path of directory). If it does not exist, the directory will be created. required: true - default: null - aliases: [] rm: description: - Remove the zip file, after unzipping @@ -51,7 +47,6 @@ - yes - no default: false - aliases: [] recurse: description: - Recursively expand zipped files within the src file. @@ -62,14 +57,12 @@ - false - yes - no - aliases: [] creates: description: - If this file or directory exists the specified src will not be extracted. required: no default: null - aliases: [] -author: Phil Schwartz +author: Phil Schwartz ''' EXAMPLES = ''' From cc305adfb672283d91e5e03775dd4f512350b65e Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Fri, 17 Jul 2015 00:51:08 -0400 Subject: [PATCH 0433/2522] minor doc fixes --- windows/win_iis_virtualdirectory.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/windows/win_iis_virtualdirectory.py b/windows/win_iis_virtualdirectory.py index e5bbd950007..1ccb34a65d3 100644 --- a/windows/win_iis_virtualdirectory.py +++ b/windows/win_iis_virtualdirectory.py @@ -30,8 +30,6 @@ description: - The name of the virtual directory to create or remove required: true - default: null - aliases: [] state: description: - Whether to add or remove the specified virtual directory @@ -40,28 +38,20 @@ - present required: false default: null - aliases: [] site: description: - The site name under which the virtual directory is created or exists. required: false default: null - aliases: [] application: description: - The application under which the virtual directory is created or exists. required: false default: null - aliases: [] physical_path: description: - The physical path to the folder in which the new virtual directory is created. The specified folder must already exist. required: false default: null - aliases: [] author: Henrik Wallström ''' - -EXAMPLES = ''' - -''' From cf764bf0604ead05fda0457b3342d556b4ef4807 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Fri, 17 Jul 2015 01:19:21 -0400 Subject: [PATCH 0434/2522] minor doc fixes --- cloud/amazon/cloudtrail.py | 2 +- clustering/consul.py | 41 +++++++++++++++++++------------------- clustering/consul_kv.py | 10 +++++----- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/cloud/amazon/cloudtrail.py b/cloud/amazon/cloudtrail.py index 1c9313bbf7b..557f2ebaae3 100644 --- a/cloud/amazon/cloudtrail.py +++ b/cloud/amazon/cloudtrail.py @@ -21,7 +21,7 @@ description: - Creates or deletes CloudTrail configuration. Ensures logging is also enabled. version_added: "2.0" -author: +author: - "Ansible Core Team" - "Ted Timmons" requirements: diff --git a/clustering/consul.py b/clustering/consul.py index 083173230f7..116517571a5 100644 --- a/clustering/consul.py +++ b/clustering/consul.py @@ -19,30 +19,30 @@ DOCUMENTATION = """ module: consul -short_description: "Add, modify & delete services within a consul cluster. - See http://consul.io for more details." +short_description: "Add, modify & delete services within a consul cluster." description: - - registers services and checks for an agent with a consul cluster. A service - is some process running on the agent node that should be advertised by + - Registers services and checks for an agent with a consul cluster. + A service is some process running on the agent node that should be advertised by consul's discovery mechanism. It may optionally supply a check definition, a periodic service test to notify the consul cluster of service's health. - Checks may also be registered per node e.g. disk usage, or cpu usage and + - "Checks may also be registered per node e.g. disk usage, or cpu usage and notify the health of the entire node to the cluster. Service level checks do not require a check name or id as these are derived - by Consul from the Service name and id respectively by appending 'service:'. - Node level checks require a check_name and optionally a check_id. - Currently, there is no complete way to retrieve the script, interval or ttl + by Consul from the Service name and id respectively by appending 'service:' + Node level checks require a check_name and optionally a check_id." + - Currently, there is no complete way to retrieve the script, interval or ttl metadata for a registered check. Without this metadata it is not possible to - tell if the data supplied with ansible represents a change to a check. As a - result this does not attempt to determine changes and will always report a + tell if the data supplied with ansible represents a change to a check. As a + result this does not attempt to determine changes and will always report a changed occurred. An api method is planned to supply this metadata so at that stage change management will be added. + - "See http://consul.io for more details." requirements: - "python >= 2.6" - python-consul - requests version_added: "2.0" -author: "Steve Gargan (@sgargan)" +author: "Steve Gargan (@sgargan)" options: state: description: @@ -50,7 +50,7 @@ required: true choices: ['present', 'absent'] service_name: - desciption: + description: - Unique name for the service on a node, must be unique per node, required if registering a service. May be ommitted if registering a node level check @@ -95,11 +95,11 @@ interval: description: - the interval at which the service check will be run. This is a number - with a s or m suffix to signify the units of seconds or minutes e.g - 15s or 1m. If no suffix is supplied, m will be used by default e.g. + with a s or m suffix to signify the units of seconds or minutes e.g + 15s or 1m. If no suffix is supplied, m will be used by default e.g. 1 will be 1m. Required if the script param is specified. required: false - default: None + default: None check_id: description: - an ID for the service check, defaults to the check name, ignored if @@ -113,20 +113,19 @@ required: false default: None ttl: - description: + description: - checks can be registered with a ttl instead of a script and interval this means that the service will check in with the agent before the - ttl expires. If it doesn't the check will be considered failed. + ttl expires. If it doesn't the check will be considered failed. Required if registering a check and the script an interval are missing - Similar to the interval this is a number with a s or m suffix to - signify the units of seconds or minutes e.g 15s or 1m. If no suffix + Similar to the interval this is a number with a s or m suffix to + signify the units of seconds or minutes e.g 15s or 1m. If no suffix is supplied, m will be used by default e.g. 1 will be 1m required: false default: None token: description: - - the token key indentifying an ACL rule set. May be required to - register services. + - the token key indentifying an ACL rule set. May be required to register services. required: false default: None """ diff --git a/clustering/consul_kv.py b/clustering/consul_kv.py index 2ba3a0315a3..b0d07dda83a 100644 --- a/clustering/consul_kv.py +++ b/clustering/consul_kv.py @@ -19,14 +19,14 @@ DOCUMENTATION = """ module: consul_kv -short_description: "manipulate entries in the key/value store of a consul - cluster. See http://www.consul.io/docs/agent/http.html#kv for more details." +short_description: Manipulate entries in the key/value store of a consul cluster. description: - - allows the addition, modification and deletion of key/value entries in a + - Allows the addition, modification and deletion of key/value entries in a consul cluster via the agent. The entire contents of the record, including - the indices, flags and session are returned as 'value'. If the key - represents a prefix then Note that when a value is removed, the existing + the indices, flags and session are returned as 'value'. + - If the key represents a prefix then Note that when a value is removed, the existing value if any is returned as part of the results. + - "See http://www.consul.io/docs/agent/http.html#kv for more details." requirements: - "python >= 2.6" - python-consul From 52895ee924b2898ea10eb193032d368d18b294e1 Mon Sep 17 00:00:00 2001 From: whiter Date: Thu, 16 Jul 2015 11:16:24 +1000 Subject: [PATCH 0435/2522] New module - s3_lifecycle --- cloud/amazon/s3_lifecycle.py | 421 +++++++++++++++++++++++++++++++++++ 1 file changed, 421 insertions(+) create mode 100644 cloud/amazon/s3_lifecycle.py diff --git a/cloud/amazon/s3_lifecycle.py b/cloud/amazon/s3_lifecycle.py new file mode 100644 index 00000000000..3328a33f15f --- /dev/null +++ b/cloud/amazon/s3_lifecycle.py @@ -0,0 +1,421 @@ +#!/usr/bin/python +# +# This is a free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This Ansible library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this library. If not, see . + +DOCUMENTATION = ''' +--- +module: s3_lifecycle +short_description: Manage s3 bucket lifecycle rules in AWS +description: + - Manage s3 bucket lifecycle rules in AWS +version_added: "2.0" +author: Rob White (@wimnat) +notes: + - If specifying expiration time as days then transition time must also be specified in days + - If specifying expiration time as a date then transition time must also be specified as a date +requirements: + - python-dateutil +options: + name: + description: + - "Name of the s3 bucket" + required: true + expiration_date: + description: + - "Indicates the lifetime of the objects that are subject to the rule by the date they will expire. The value must be ISO-8601 format, the time must be midnight and a GMT timezone must be specified." + required: false + default: null + expiration_days: + description: + - "Indicates the lifetime, in days, of the objects that are subject to the rule. The value must be a non-zero positive integer." + required: false + default: null + prefix: + description: + - "Prefix identifying one or more objects to which the rule applies. If no prefix is specified, the rule will apply to the whole bucket." + required: false + default: null + region: + description: + - "AWS region to create the bucket in. If not set then the value of the AWS_REGION and EC2_REGION environment variables are checked, followed by the aws_region and ec2_region settings in the Boto config file. If none of those are set the region defaults to the S3 Location: US Standard." + required: false + default: null + rule_id: + description: + - "Unique identifier for the rule. The value cannot be longer than 255 characters. A unique value for the rule will be generated if no value is provided." + required: false + default: null + state: + description: + - "Create or remove the lifecycle rule" + required: false + default: present + choices: [ 'present', 'absent' ] + status: + description: + - "If 'enabled', the rule is currently being applied. If 'disabled', the rule is not currently being applied." + required: false + default: enabled + choices: [ 'enabled', 'disabled' ] + storage_class: + description: + - "The storage class to transition to. Currently there is only one valid value - 'glacier'." + required: false + default: glacier + choices: [ 'glacier' ] + transition_date: + description: + - "Indicates the lifetime of the objects that are subject to the rule by the date they will transition to a different storage class. The value must be ISO-8601 format, the time must be midnight and a GMT timezone must be specified. If transition_days is not specified, this parameter is required." + required: false + default: null + transition_days: + description: + - "Indicates when, in days, an object transitions to a different storage class. If transition_date is not specified, this parameter is required." + required: false + default: null + +extends_documentation_fragment: aws +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Configure a lifecycle rule on a bucket to expire (delete) items with a prefix of /logs/ after 30 days +- s3_lifecycle: + name: mybucket + expiration_days: 30 + prefix: /logs/ + status: enabled + state: present + +# Configure a lifecycle rule to transition all items with a prefix of /logs/ to glacier after 7 days and then delete after 90 days +- s3_lifecycle: + name: mybucket + transition_days: 7 + expiration_days: 90 + prefix: /logs/ + status: enabled + state: present + +# Configure a lifecycle rule to transition all items with a prefix of /logs/ to glacier on 31 Dec 2020 and then delete on 31 Dec 2030. Note that midnight GMT must be specified. +# Be sure to quote your date strings +- s3_lifecycle: + name: mybucket + transition_date: "2020-12-30T00:00:00.000Z" + expiration_date: "2030-12-30T00:00:00.000Z" + prefix: /logs/ + status: enabled + state: present + +# Disable the rule created above +- s3_lifecycle: + name: mybucket + prefix: /logs/ + status: disabled + state: present + +# Delete the lifecycle rule created above +- s3_lifecycle: + name: mybucket + prefix: /logs/ + state: absent + +''' + +import xml.etree.ElementTree as ET +import copy +import datetime + +try: + import dateutil.parser + HAS_DATEUTIL = True +except ImportError: + HAS_DATEUTIL = False + +try: + import boto.ec2 + from boto.s3.connection import OrdinaryCallingFormat, Location + from boto.s3.lifecycle import Lifecycle, Rule, Expiration, Transition + from boto.exception import BotoServerError, S3CreateError, S3ResponseError + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +def create_lifecycle_rule(connection, module): + + name = module.params.get("name") + expiration_date = module.params.get("expiration_date") + expiration_days = module.params.get("expiration_days") + prefix = module.params.get("prefix") + rule_id = module.params.get("rule_id") + status = module.params.get("status") + storage_class = module.params.get("storage_class") + transition_date = module.params.get("transition_date") + transition_days = module.params.get("transition_days") + changed = False + + try: + bucket = connection.get_bucket(name) + except S3ResponseError, e: + module.fail_json(msg=e.message) + + # Get the bucket's current lifecycle rules + try: + current_lifecycle_obj = bucket.get_lifecycle_config() + except S3ResponseError, e: + if e.error_code == "NoSuchLifecycleConfiguration": + current_lifecycle_obj = Lifecycle() + else: + module.fail_json(msg=e.message) + + # Create expiration + if expiration_days is not None: + expiration_obj = Expiration(days=expiration_days) + elif expiration_date is not None: + expiration_obj = Expiration(date=expiration_date) + else: + expiration_obj = None + + # Create transition + if transition_days is not None: + transition_obj = Transition(days=transition_days, storage_class=storage_class.upper()) + elif transition_date is not None: + transition_obj = Transition(date=transition_date, storage_class=storage_class.upper()) + else: + transition_obj = None + + # Create rule + rule = Rule(rule_id, prefix, status.title(), expiration_obj, transition_obj) + + # Create lifecycle + lifecycle_obj = Lifecycle() + + appended = False + # If current_lifecycle_obj is not None then we have rules to compare, otherwise just add the rule + if current_lifecycle_obj: + # If rule ID exists, use that for comparison otherwise compare based on prefix + for existing_rule in current_lifecycle_obj: + if rule.id == existing_rule.id: + if compare_rule(rule, existing_rule): + lifecycle_obj.append(rule) + appended = True + else: + lifecycle_obj.append(rule) + changed = True + appended = True + elif rule.prefix == existing_rule.prefix: + existing_rule.id = None + if compare_rule(rule, existing_rule): + lifecycle_obj.append(rule) + appended = True + else: + lifecycle_obj.append(rule) + changed = True + appended = True + # If nothing appended then append now as the rule must not exist + if not appended: + lifecycle_obj.append(rule) + changed = True + else: + lifecycle_obj.append(rule) + changed = True + + # Write lifecycle to bucket + try: + bucket.configure_lifecycle(lifecycle_obj) + except S3ResponseError, e: + module.fail_json(msg=e.message) + + module.exit_json(changed=changed) + +def compare_rule(rule_a, rule_b): + + # Copy objects + rule1 = copy.deepcopy(rule_a) + rule2 = copy.deepcopy(rule_b) + + # Delete Rule from Rule + try: + del rule1.Rule + except AttributeError: + pass + + try: + del rule2.Rule + except AttributeError: + pass + + # Extract Expiration and Transition objects + rule1_expiration = rule1.expiration + rule1_transition = rule1.transition + rule2_expiration = rule2.expiration + rule2_transition = rule2.transition + + # Delete the Expiration and Transition objects from the Rule objects + del rule1.expiration + del rule1.transition + del rule2.expiration + del rule2.transition + + # Compare + if rule1_transition is None: + rule1_transition = Transition() + if rule2_transition is None: + rule2_transition = Transition() + if rule1_expiration is None: + rule1_expiration = Expiration() + if rule2_expiration is None: + rule2_expiration = Expiration() + + if (rule1.__dict__ == rule2.__dict__) and (rule1_expiration.__dict__ == rule2_expiration.__dict__) and (rule1_transition.__dict__ == rule2_transition.__dict__): + return True + else: + return False + + +def destroy_lifecycle_rule(connection, module): + + name = module.params.get("name") + prefix = module.params.get("prefix") + rule_id = module.params.get("rule_id") + changed = False + + if prefix is None: + prefix = "" + + try: + bucket = connection.get_bucket(name) + except S3ResponseError, e: + module.fail_json(msg=e.message) + + # Get the bucket's current lifecycle rules + try: + current_lifecycle_obj = bucket.get_lifecycle_config() + except S3ResponseError, e: + if e.error_code == "NoSuchLifecycleConfiguration": + module.exit_json(changed=changed) + else: + module.fail_json(msg=e.message) + + # Create lifecycle + lifecycle_obj = Lifecycle() + + # Check if rule exists + # If an ID exists, use that otherwise compare based on prefix + if rule_id is not None: + for existing_rule in current_lifecycle_obj: + if rule_id == existing_rule.id: + # We're not keeping the rule (i.e. deleting) so mark as changed + changed = True + else: + lifecycle_obj.append(existing_rule) + else: + for existing_rule in current_lifecycle_obj: + if prefix == existing_rule.prefix: + # We're not keeping the rule (i.e. deleting) so mark as changed + changed = True + else: + lifecycle_obj.append(existing_rule) + + + # Write lifecycle to bucket or, if there no rules left, delete lifecycle configuration + try: + if lifecycle_obj: + bucket.configure_lifecycle(lifecycle_obj) + else: + bucket.delete_lifecycle_configuration() + except BotoServerError, e: + module.fail_json(msg=e.message) + + module.exit_json(changed=changed) + + +def main(): + + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + name = dict(required=True), + expiration_days = dict(default=None, required=False, type='int'), + expiration_date = dict(default=None, required=False, type='str'), + prefix = dict(default=None, required=False), + requester_pays = dict(default='no', type='bool'), + rule_id = dict(required=False), + state = dict(default='present', choices=['present', 'absent']), + status = dict(default='enabled', choices=['enabled', 'disabled']), + storage_class = dict(default='glacier', choices=['glacier']), + transition_days = dict(default=None, required=False, type='int'), + transition_date = dict(default=None, required=False, type='str') + ) + ) + + module = AnsibleModule(argument_spec=argument_spec, + mutually_exclusive = [ + [ 'expiration_days', 'expiration_date' ], + [ 'expiration_days', 'transition_date' ], + [ 'transition_days', 'transition_date' ], + [ 'transition_days', 'expiration_date' ] + ] + ) + + if not HAS_BOTO: + module.fail_json(msg='boto required for this module') + + if not HAS_DATEUTIL: + module.fail_json(msg='dateutil required for this module') + + region, ec2_url, aws_connect_params = get_aws_connection_info(module) + + if region in ('us-east-1', '', None): + # S3ism for the US Standard region + location = Location.DEFAULT + else: + # Boto uses symbolic names for locations but region strings will + # actually work fine for everything except us-east-1 (US Standard) + location = region + try: + connection = boto.s3.connect_to_region(location, is_secure=True, calling_format=OrdinaryCallingFormat(), **aws_connect_params) + # use this as fallback because connect_to_region seems to fail in boto + non 'classic' aws accounts in some cases + if connection is None: + connection = boto.connect_s3(**aws_connect_params) + except (boto.exception.NoAuthHandlerFound, StandardError), e: + module.fail_json(msg=str(e)) + + expiration_date = module.params.get("expiration_date") + transition_date = module.params.get("transition_date") + state = module.params.get("state") + + # If expiration_date set, check string is valid + if expiration_date is not None: + try: + datetime.datetime.strptime(expiration_date, "%Y-%m-%dT%H:%M:%S.000Z") + except ValueError, e: + module.fail_json(msg="expiration_date is not a valid ISO-8601 format. The time must be midnight and a timezone of GMT must be included") + + if transition_date is not None: + try: + datetime.datetime.strptime(transition_date, "%Y-%m-%dT%H:%M:%S.000Z") + except ValueError, e: + module.fail_json(msg="expiration_date is not a valid ISO-8601 format. The time must be midnight and a timezone of GMT must be included") + + if state == 'present': + create_lifecycle_rule(connection, module) + elif state == 'absent': + destroy_lifecycle_rule(connection, module) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From dd4d33b1fe959b57f96ed65ea642a258359cb846 Mon Sep 17 00:00:00 2001 From: Rob White Date: Mon, 6 Jul 2015 19:46:33 +1000 Subject: [PATCH 0436/2522] New module - s3_logging --- cloud/amazon/s3_logging.py | 185 +++++++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 cloud/amazon/s3_logging.py diff --git a/cloud/amazon/s3_logging.py b/cloud/amazon/s3_logging.py new file mode 100644 index 00000000000..313518510c9 --- /dev/null +++ b/cloud/amazon/s3_logging.py @@ -0,0 +1,185 @@ +#!/usr/bin/python +# +# This is a free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This Ansible library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this library. If not, see . + +DOCUMENTATION = ''' +--- +module: s3_logging +short_description: Manage logging facility of an s3 bucket in AWS +description: + - Manage logging facility of an s3 bucket in AWS +version_added: "2.0" +author: Rob White (@wimnat) +options: + name: + description: + - "Name of the s3 bucket." + required: true + default: null + region: + description: + - "AWS region to create the bucket in. If not set then the value of the AWS_REGION and EC2_REGION environment variables are checked, followed by the aws_region and ec2_region settings in the Boto config file. If none of those are set the region defaults to the S3 Location: US Standard." + required: false + default: null + state: + description: + - "Enable or disable logging." + required: false + default: present + choices: [ 'present', 'absent' ] + target_bucket: + description: + - "The bucket to log to." + required: true + default: null + target_prefix: + description: + - "The prefix that should be prepended to the generated log files written to the target_bucket." + required: false + default: no + +extends_documentation_fragment: aws +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +- name: Enable logging of s3 bucket mywebsite.com to s3 bucket mylogs + s3_logging: + name: mywebsite.com + target_bucket: mylogs + target_prefix: logs/mywebsite.com + state: present + +- name: Remove logging on an s3 bucket + s3_logging: + name: mywebsite.com + state: absent + +''' + +try: + import boto.ec2 + from boto.s3.connection import OrdinaryCallingFormat, Location + from boto.exception import BotoServerError, S3CreateError, S3ResponseError + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + + +def compare_bucket_logging(bucket, target_bucket, target_prefix): + + bucket_log_obj = bucket.get_logging_status() + if bucket_log_obj.target != target_bucket or bucket_log_obj.prefix != target_prefix: + return False + else: + return True + + +def enable_bucket_logging(connection, module): + + bucket_name = module.params.get("name") + target_bucket = module.params.get("target_bucket") + target_prefix = module.params.get("target_prefix") + changed = False + + try: + bucket = connection.get_bucket(bucket_name) + except S3ResponseError as e: + module.fail_json(msg=e.message) + + try: + if not compare_bucket_logging(bucket, target_bucket, target_prefix): + # Before we can enable logging we must give the log-delivery group WRITE and READ_ACP permissions to the target bucket + try: + target_bucket_obj = connection.get_bucket(target_bucket) + except S3ResponseError as e: + if e.status == 301: + module.fail_json(msg="the logging target bucket must be in the same region as the bucket being logged") + else: + module.fail_json(msg=e.message) + target_bucket_obj.set_as_logging_target() + + bucket.enable_logging(target_bucket, target_prefix) + changed = True + + except S3ResponseError as e: + module.fail_json(msg=e.message) + + module.exit_json(changed=changed) + + +def disable_bucket_logging(connection, module): + + bucket_name = module.params.get("name") + changed = False + + try: + bucket = connection.get_bucket(bucket_name) + if not compare_bucket_logging(bucket, None, None): + bucket.disable_logging() + changed = True + except S3ResponseError as e: + module.fail_json(msg=e.message) + + module.exit_json(changed=changed) + + +def main(): + + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + name = dict(required=True, default=None), + target_bucket = dict(required=True, default=None), + target_prefix = dict(required=False, default=""), + state = dict(required=False, default='present', choices=['present', 'absent']) + ) + ) + + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO: + module.fail_json(msg='boto required for this module') + + region, ec2_url, aws_connect_params = get_aws_connection_info(module) + + if region in ('us-east-1', '', None): + # S3ism for the US Standard region + location = Location.DEFAULT + else: + # Boto uses symbolic names for locations but region strings will + # actually work fine for everything except us-east-1 (US Standard) + location = region + try: + connection = boto.s3.connect_to_region(location, is_secure=True, calling_format=OrdinaryCallingFormat(), **aws_connect_params) + # use this as fallback because connect_to_region seems to fail in boto + non 'classic' aws accounts in some cases + if connection is None: + connection = boto.connect_s3(**aws_connect_params) + except (boto.exception.NoAuthHandlerFound, StandardError), e: + module.fail_json(msg=str(e)) + + + state = module.params.get("state") + + if state == 'present': + enable_bucket_logging(connection, module) + elif state == 'absent': + disable_bucket_logging(connection, module) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() \ No newline at end of file From 511d6a7ff5dd2972246fc8ac91fa672a6c275f52 Mon Sep 17 00:00:00 2001 From: whiter Date: Tue, 7 Jul 2015 09:38:33 +1000 Subject: [PATCH 0437/2522] Fixed tag comparison --- cloud/amazon/s3_bucket.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/cloud/amazon/s3_bucket.py b/cloud/amazon/s3_bucket.py index 7a4bcd01607..25c085f8173 100644 --- a/cloud/amazon/s3_bucket.py +++ b/cloud/amazon/s3_bucket.py @@ -103,7 +103,7 @@ try: import boto.ec2 - from boto.s3.connection import OrdinaryCallingFormat + from boto.s3.connection import OrdinaryCallingFormat, Location from boto.s3.tagging import Tags, TagSet from boto.exception import BotoServerError, S3CreateError, S3ResponseError HAS_BOTO = True @@ -248,7 +248,7 @@ def create_bucket(connection, module): else: current_tags_dict = dict((t.key, t.value) for t in current_tags[0]) - if sorted(current_tags_dict) != sorted(tags): + if current_tags_dict != tags: try: if tags: bucket.set_tags(create_tags_container(tags)) @@ -386,7 +386,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * -# this is magic, see lib/ansible/module_common.py -#<> - -main() +if __name__ == '__main__': + main() \ No newline at end of file From 3e5fa95891b9c170301305423886cbd9a8075fa2 Mon Sep 17 00:00:00 2001 From: Siva Popuri Date: Fri, 17 Jul 2015 13:15:10 -0500 Subject: [PATCH 0438/2522] Revert "ansible modules for centurylink cloud are added" This reverts commit 11c953477c012435e078d62204b0cf1db2796d2f. --- cloud/centurylink/__init__.py | 1 - cloud/centurylink/clc_aa_policy.py | 294 ----- cloud/centurylink/clc_alert_policy.py | 473 ------- cloud/centurylink/clc_blueprint_package.py | 263 ---- cloud/centurylink/clc_firewall_policy.py | 542 -------- cloud/centurylink/clc_group.py | 370 ------ cloud/centurylink/clc_loadbalancer.py | 759 ----------- cloud/centurylink/clc_modify_server.py | 710 ----------- cloud/centurylink/clc_publicip.py | 316 ----- cloud/centurylink/clc_server.py | 1323 -------------------- cloud/centurylink/clc_server_snapshot.py | 341 ----- 11 files changed, 5392 deletions(-) delete mode 100644 cloud/centurylink/__init__.py delete mode 100644 cloud/centurylink/clc_aa_policy.py delete mode 100644 cloud/centurylink/clc_alert_policy.py delete mode 100644 cloud/centurylink/clc_blueprint_package.py delete mode 100644 cloud/centurylink/clc_firewall_policy.py delete mode 100644 cloud/centurylink/clc_group.py delete mode 100644 cloud/centurylink/clc_loadbalancer.py delete mode 100644 cloud/centurylink/clc_modify_server.py delete mode 100644 cloud/centurylink/clc_publicip.py delete mode 100644 cloud/centurylink/clc_server.py delete mode 100644 cloud/centurylink/clc_server_snapshot.py diff --git a/cloud/centurylink/__init__.py b/cloud/centurylink/__init__.py deleted file mode 100644 index 71f0abcff9d..00000000000 --- a/cloud/centurylink/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "${version}" diff --git a/cloud/centurylink/clc_aa_policy.py b/cloud/centurylink/clc_aa_policy.py deleted file mode 100644 index 644f3817c4f..00000000000 --- a/cloud/centurylink/clc_aa_policy.py +++ /dev/null @@ -1,294 +0,0 @@ -#!/usr/bin/python - -# CenturyLink Cloud Ansible Modules. -# -# These Ansible modules enable the CenturyLink Cloud v2 API to be called -# from an within Ansible Playbook. -# -# This file is part of CenturyLink Cloud, and is maintained -# by the Workflow as a Service Team -# -# Copyright 2015 CenturyLink Cloud -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# CenturyLink Cloud: http://www.CenturyLinkCloud.com -# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ - -DOCUMENTATION = ''' -module: clc_aa_policy -short_descirption: Create or Delete Anti Affinity Policies at CenturyLink Cloud. -description: - - An Ansible module to Create or Delete Anti Affinity Policies at CenturyLink Cloud. -options: - name: - description: - - The name of the Anti Affinity Policy. - required: True - location: - description: - - Datacenter in which the policy lives/should live. - required: True - state: - description: - - Whether to create or delete the policy. - required: False - default: present - choices: ['present','absent'] - wait: - description: - - Whether to wait for the provisioning tasks to finish before returning. - default: True - required: False - choices: [ True, False] - aliases: [] -''' - -EXAMPLES = ''' -# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples - ---- -- name: Create AA Policy - hosts: localhost - gather_facts: False - connection: local - tasks: - - name: Create an Anti Affinity Policy - clc_aa_policy: - name: 'Hammer Time' - location: 'UK3' - state: present - register: policy - - - name: debug - debug: var=policy - ---- -- name: Delete AA Policy - hosts: localhost - gather_facts: False - connection: local - tasks: - - name: Delete an Anti Affinity Policy - clc_aa_policy: - name: 'Hammer Time' - location: 'UK3' - state: absent - register: policy - - - name: debug - debug: var=policy -''' - -__version__ = '${version}' - -import requests - -# -# Requires the clc-python-sdk. -# sudo pip install clc-sdk -# -try: - import clc as clc_sdk - from clc import CLCException -except ImportError: - clc_found = False - clc_sdk = None -else: - clc_found = True - - -class ClcAntiAffinityPolicy(): - - clc = clc_sdk - module = None - - def __init__(self, module): - """ - Construct module - """ - self.module = module - self.policy_dict = {} - - if not clc_found: - self.module.fail_json( - msg='clc-python-sdk required for this module') - - self._set_user_agent(self.clc) - - @staticmethod - def _define_module_argument_spec(): - """ - Define the argument spec for the ansible module - :return: argument spec dictionary - """ - argument_spec = dict( - name=dict(required=True), - location=dict(required=True), - alias=dict(default=None), - wait=dict(default=True), - state=dict(default='present', choices=['present', 'absent']), - ) - return argument_spec - - # Module Behavior Goodness - def process_request(self): - """ - Process the request - Main Code Path - :return: Returns with either an exit_json or fail_json - """ - p = self.module.params - - if not clc_found: - self.module.fail_json( - msg='clc-python-sdk required for this module') - - self._set_clc_credentials_from_env() - self.policy_dict = self._get_policies_for_datacenter(p) - - if p['state'] == "absent": - changed, policy = self._ensure_policy_is_absent(p) - else: - changed, policy = self._ensure_policy_is_present(p) - - if hasattr(policy, 'data'): - policy = policy.data - elif hasattr(policy, '__dict__'): - policy = policy.__dict__ - - self.module.exit_json(changed=changed, policy=policy) - - def _set_clc_credentials_from_env(self): - """ - Set the CLC Credentials on the sdk by reading environment variables - :return: none - """ - env = os.environ - v2_api_token = env.get('CLC_V2_API_TOKEN', False) - v2_api_username = env.get('CLC_V2_API_USERNAME', False) - v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) - clc_alias = env.get('CLC_ACCT_ALIAS', False) - api_url = env.get('CLC_V2_API_URL', False) - - if api_url: - self.clc.defaults.ENDPOINT_URL_V2 = api_url - - if v2_api_token and clc_alias: - self.clc._LOGIN_TOKEN_V2 = v2_api_token - self.clc._V2_ENABLED = True - self.clc.ALIAS = clc_alias - elif v2_api_username and v2_api_passwd: - self.clc.v2.SetCredentials( - api_username=v2_api_username, - api_passwd=v2_api_passwd) - else: - return self.module.fail_json( - msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " - "environment variables") - - def _get_policies_for_datacenter(self, p): - """ - Get the Policies for a datacenter by calling the CLC API. - :param p: datacenter to get policies from - :return: policies in the datacenter - """ - response = {} - - policies = self.clc.v2.AntiAffinity.GetAll(location=p['location']) - - for policy in policies: - response[policy.name] = policy - return response - - def _create_policy(self, p): - """ - Create an Anti Affinnity Policy using the CLC API. - :param p: datacenter to create policy in - :return: response dictionary from the CLC API. - """ - return self.clc.v2.AntiAffinity.Create( - name=p['name'], - location=p['location']) - - def _delete_policy(self, p): - """ - Delete an Anti Affinity Policy using the CLC API. - :param p: datacenter to delete a policy from - :return: none - """ - policy = self.policy_dict[p['name']] - policy.Delete() - - def _policy_exists(self, policy_name): - """ - Check to see if an Anti Affinity Policy exists - :param policy_name: name of the policy - :return: boolean of if the policy exists - """ - if policy_name in self.policy_dict: - return self.policy_dict.get(policy_name) - - return False - - def _ensure_policy_is_absent(self, p): - """ - Makes sure that a policy is absent - :param p: dictionary of policy name - :return: tuple of if a deletion occurred and the name of the policy that was deleted - """ - changed = False - if self._policy_exists(policy_name=p['name']): - changed = True - if not self.module.check_mode: - self._delete_policy(p) - return changed, None - - def _ensure_policy_is_present(self, p): - """ - Ensures that a policy is present - :param p: dictonary of a policy name - :return: tuple of if an addition occurred and the name of the policy that was added - """ - changed = False - policy = self._policy_exists(policy_name=p['name']) - if not policy: - changed = True - policy = None - if not self.module.check_mode: - policy = self._create_policy(p) - return changed, policy - - @staticmethod - def _set_user_agent(clc): - if hasattr(clc, 'SetRequestsSession'): - agent_string = "ClcAnsibleModule/" + __version__ - ses = requests.Session() - ses.headers.update({"Api-Client": agent_string}) - ses.headers['User-Agent'] += " " + agent_string - clc.SetRequestsSession(ses) - - -def main(): - """ - The main function. Instantiates the module and calls process_request. - :return: none - """ - module = AnsibleModule( - argument_spec=ClcAntiAffinityPolicy._define_module_argument_spec(), - supports_check_mode=True) - clc_aa_policy = ClcAntiAffinityPolicy(module) - clc_aa_policy.process_request() - -from ansible.module_utils.basic import * # pylint: disable=W0614 -if __name__ == '__main__': - main() diff --git a/cloud/centurylink/clc_alert_policy.py b/cloud/centurylink/clc_alert_policy.py deleted file mode 100644 index 75467967a85..00000000000 --- a/cloud/centurylink/clc_alert_policy.py +++ /dev/null @@ -1,473 +0,0 @@ -#!/usr/bin/python - -# CenturyLink Cloud Ansible Modules. -# -# These Ansible modules enable the CenturyLink Cloud v2 API to be called -# from an within Ansible Playbook. -# -# This file is part of CenturyLink Cloud, and is maintained -# by the Workflow as a Service Team -# -# Copyright 2015 CenturyLink Cloud -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# CenturyLink Cloud: http://www.CenturyLinkCloud.com -# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ - -DOCUMENTATION = ''' -module: clc_alert_policy -short_descirption: Create or Delete Alert Policies at CenturyLink Cloud. -description: - - An Ansible module to Create or Delete Alert Policies at CenturyLink Cloud. -options: - alias: - description: - - The alias of your CLC Account - required: True - name: - description: - - The name of the alert policy. This is mutually exclusive with id - default: None - aliases: [] - id: - description: - - The alert policy id. This is mutually exclusive with name - default: None - aliases: [] - alert_recipients: - description: - - A list of recipient email ids to notify the alert. - required: True - aliases: [] - metric: - description: - - The metric on which to measure the condition that will trigger the alert. - required: True - default: None - choices: ['cpu','memory','disk'] - aliases: [] - duration: - description: - - The length of time in minutes that the condition must exceed the threshold. - required: True - default: None - aliases: [] - threshold: - description: - - The threshold that will trigger the alert when the metric equals or exceeds it. - This number represents a percentage and must be a value between 5.0 - 95.0 that is a multiple of 5.0 - required: True - default: None - aliases: [] - state: - description: - - Whether to create or delete the policy. - required: False - default: present - choices: ['present','absent'] -''' - -EXAMPLES = ''' -# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples - ---- -- name: Create Alert Policy Example - hosts: localhost - gather_facts: False - connection: local - tasks: - - name: Create an Alert Policy for disk above 80% for 5 minutes - clc_alert_policy: - alias: wfad - name: 'alert for disk > 80%' - alert_recipients: - - test1@centurylink.com - - test2@centurylink.com - metric: 'disk' - duration: '00:05:00' - threshold: 80 - state: present - register: policy - - - name: debug - debug: var=policy - ---- -- name: Delete Alert Policy Example - hosts: localhost - gather_facts: False - connection: local - tasks: - - name: Delete an Alert Policy - clc_alert_policy: - alias: wfad - name: 'alert for disk > 80%' - state: absent - register: policy - - - name: debug - debug: var=policy -''' - -__version__ = '${version}' - -import requests - -# -# Requires the clc-python-sdk. -# sudo pip install clc-sdk -# -try: - import clc as clc_sdk - from clc import CLCException -except ImportError: - clc_found = False - clc_sdk = None -else: - clc_found = True - - -class ClcAlertPolicy(): - - clc = clc_sdk - module = None - - def __init__(self, module): - """ - Construct module - """ - self.module = module - self.policy_dict = {} - - if not clc_found: - self.module.fail_json( - msg='clc-python-sdk required for this module') - - self._set_user_agent(self.clc) - - @staticmethod - def _define_module_argument_spec(): - """ - Define the argument spec for the ansible module - :return: argument spec dictionary - """ - argument_spec = dict( - name=dict(default=None), - id=dict(default=None), - alias=dict(required=True, default=None), - alert_recipients=dict(type='list', required=False, default=None), - metric=dict(required=False, choices=['cpu', 'memory', 'disk'], default=None), - duration=dict(required=False, type='str', default=None), - threshold=dict(required=False, type='int', default=None), - state=dict(default='present', choices=['present', 'absent']) - ) - mutually_exclusive = [ - ['name', 'id'] - ] - return {'argument_spec': argument_spec, - 'mutually_exclusive': mutually_exclusive} - - # Module Behavior Goodness - def process_request(self): - """ - Process the request - Main Code Path - :return: Returns with either an exit_json or fail_json - """ - p = self.module.params - - if not clc_found: - self.module.fail_json( - msg='clc-python-sdk required for this module') - - self._set_clc_credentials_from_env() - self.policy_dict = self._get_alert_policies(p['alias']) - - if p['state'] == 'present': - changed, policy = self._ensure_alert_policy_is_present() - else: - changed, policy = self._ensure_alert_policy_is_absent() - - self.module.exit_json(changed=changed, policy=policy) - - def _set_clc_credentials_from_env(self): - """ - Set the CLC Credentials on the sdk by reading environment variables - :return: none - """ - env = os.environ - v2_api_token = env.get('CLC_V2_API_TOKEN', False) - v2_api_username = env.get('CLC_V2_API_USERNAME', False) - v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) - clc_alias = env.get('CLC_ACCT_ALIAS', False) - api_url = env.get('CLC_V2_API_URL', False) - - if api_url: - self.clc.defaults.ENDPOINT_URL_V2 = api_url - - if v2_api_token and clc_alias: - self.clc._LOGIN_TOKEN_V2 = v2_api_token - self.clc._V2_ENABLED = True - self.clc.ALIAS = clc_alias - elif v2_api_username and v2_api_passwd: - self.clc.v2.SetCredentials( - api_username=v2_api_username, - api_passwd=v2_api_passwd) - else: - return self.module.fail_json( - msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " - "environment variables") - - def _ensure_alert_policy_is_present(self): - """ - Ensures that the alert policy is present - :return: (changed, policy) - canged: A flag representing if anything is modified - policy: the created/updated alert policy - """ - changed = False - p = self.module.params - policy_name = p.get('name') - alias = p.get('alias') - if not policy_name: - self.module.fail_json(msg='Policy name is a required') - policy = self._alert_policy_exists(alias, policy_name) - if not policy: - changed = True - policy = None - if not self.module.check_mode: - policy = self._create_alert_policy() - else: - changed_u, policy = self._ensure_alert_policy_is_updated(policy) - if changed_u: - changed = True - return changed, policy - - def _ensure_alert_policy_is_absent(self): - """ - Ensures that the alert policy is absent - :return: (changed, None) - canged: A flag representing if anything is modified - """ - changed = False - p = self.module.params - alert_policy_id = p.get('id') - alert_policy_name = p.get('name') - alias = p.get('alias') - if not alert_policy_id and not alert_policy_name: - self.module.fail_json( - msg='Either alert policy id or policy name is required') - if not alert_policy_id and alert_policy_name: - alert_policy_id = self._get_alert_policy_id( - self.module, - alert_policy_name) - if alert_policy_id and alert_policy_id in self.policy_dict: - changed = True - if not self.module.check_mode: - self._delete_alert_policy(alias, alert_policy_id) - return changed, None - - def _ensure_alert_policy_is_updated(self, alert_policy): - """ - Ensures the aliert policy is updated if anything is changed in the alert policy configuration - :param alert_policy: the targetalert policy - :return: (changed, policy) - canged: A flag representing if anything is modified - policy: the updated the alert policy - """ - changed = False - p = self.module.params - alert_policy_id = alert_policy.get('id') - email_list = p.get('alert_recipients') - metric = p.get('metric') - duration = p.get('duration') - threshold = p.get('threshold') - policy = alert_policy - if (metric and metric != str(alert_policy.get('triggers')[0].get('metric'))) or \ - (duration and duration != str(alert_policy.get('triggers')[0].get('duration'))) or \ - (threshold and float(threshold) != float(alert_policy.get('triggers')[0].get('threshold'))): - changed = True - elif email_list: - t_email_list = list( - alert_policy.get('actions')[0].get('settings').get('recipients')) - if set(email_list) != set(t_email_list): - changed = True - if changed and not self.module.check_mode: - policy = self._update_alert_policy(alert_policy_id) - return changed, policy - - def _get_alert_policies(self, alias): - """ - Get the alert policies for account alias by calling the CLC API. - :param alias: the account alias - :return: the alert policies for the account alias - """ - response = {} - - policies = self.clc.v2.API.Call('GET', - '/v2/alertPolicies/%s' - % (alias)) - - for policy in policies.get('items'): - response[policy.get('id')] = policy - return response - - def _create_alert_policy(self): - """ - Create an alert Policy using the CLC API. - :return: response dictionary from the CLC API. - """ - p = self.module.params - alias = p['alias'] - email_list = p['alert_recipients'] - metric = p['metric'] - duration = p['duration'] - threshold = p['threshold'] - name = p['name'] - arguments = json.dumps( - { - 'name': name, - 'actions': [{ - 'action': 'email', - 'settings': { - 'recipients': email_list - } - }], - 'triggers': [{ - 'metric': metric, - 'duration': duration, - 'threshold': threshold - }] - } - ) - try: - result = self.clc.v2.API.Call( - 'POST', - '/v2/alertPolicies/%s' % - (alias), - arguments) - except self.clc.APIFailedResponse as e: - return self.module.fail_json( - msg='Unable to create alert policy. %s' % str( - e.response_text)) - return result - - def _update_alert_policy(self, alert_policy_id): - """ - Update alert policy using the CLC API. - :param alert_policy_id: The clc alert policy id - :return: response dictionary from the CLC API. - """ - p = self.module.params - alias = p['alias'] - email_list = p['alert_recipients'] - metric = p['metric'] - duration = p['duration'] - threshold = p['threshold'] - name = p['name'] - arguments = json.dumps( - { - 'name': name, - 'actions': [{ - 'action': 'email', - 'settings': { - 'recipients': email_list - } - }], - 'triggers': [{ - 'metric': metric, - 'duration': duration, - 'threshold': threshold - }] - } - ) - try: - result = self.clc.v2.API.Call( - 'PUT', '/v2/alertPolicies/%s/%s' % - (alias, alert_policy_id), arguments) - except self.clc.APIFailedResponse as e: - return self.module.fail_json( - msg='Unable to update alert policy. %s' % str( - e.response_text)) - return result - - def _delete_alert_policy(self, alias, policy_id): - """ - Delete an alert policy using the CLC API. - :param alias : the account alias - :param policy_id: the alert policy id - :return: response dictionary from the CLC API. - """ - try: - result = self.clc.v2.API.Call( - 'DELETE', '/v2/alertPolicies/%s/%s' % - (alias, policy_id), None) - except self.clc.APIFailedResponse as e: - return self.module.fail_json( - msg='Unable to delete alert policy. %s' % str( - e.response_text)) - return result - - def _alert_policy_exists(self, alias, policy_name): - """ - Check to see if an alert policy exists - :param policy_name: name of the alert policy - :return: boolean of if the policy exists - """ - result = False - for id in self.policy_dict: - if self.policy_dict.get(id).get('name') == policy_name: - result = self.policy_dict.get(id) - return result - - def _get_alert_policy_id(self, module, alert_policy_name): - """ - retrieves the alert policy id of the account based on the name of the policy - :param module: the AnsibleModule object - :param alert_policy_name: the alert policy name - :return: alert_policy_id: The alert policy id - """ - alert_policy_id = None - for id in self.policy_dict: - if self.policy_dict.get(id).get('name') == alert_policy_name: - if not alert_policy_id: - alert_policy_id = id - else: - return module.fail_json( - msg='mutiple alert policies were found with policy name : %s' % - (alert_policy_name)) - return alert_policy_id - - @staticmethod - def _set_user_agent(clc): - if hasattr(clc, 'SetRequestsSession'): - agent_string = "ClcAnsibleModule/" + __version__ - ses = requests.Session() - ses.headers.update({"Api-Client": agent_string}) - ses.headers['User-Agent'] += " " + agent_string - clc.SetRequestsSession(ses) - - -def main(): - """ - The main function. Instantiates the module and calls process_request. - :return: none - """ - argument_dict = ClcAlertPolicy._define_module_argument_spec() - module = AnsibleModule(supports_check_mode=True, **argument_dict) - clc_alert_policy = ClcAlertPolicy(module) - clc_alert_policy.process_request() - -from ansible.module_utils.basic import * # pylint: disable=W0614 -if __name__ == '__main__': - main() diff --git a/cloud/centurylink/clc_blueprint_package.py b/cloud/centurylink/clc_blueprint_package.py deleted file mode 100644 index 80cc18a24ca..00000000000 --- a/cloud/centurylink/clc_blueprint_package.py +++ /dev/null @@ -1,263 +0,0 @@ -#!/usr/bin/python - -# CenturyLink Cloud Ansible Modules. -# -# These Ansible modules enable the CenturyLink Cloud v2 API to be called -# from an within Ansible Playbook. -# -# This file is part of CenturyLink Cloud, and is maintained -# by the Workflow as a Service Team -# -# Copyright 2015 CenturyLink Cloud -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# CenturyLink Cloud: http://www.CenturyLinkCloud.com -# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ -# - -DOCUMENTATION = ''' -module: clc_blueprint_package -short_desciption: deploys a blue print package on a set of servers in CenturyLink Cloud. -description: - - An Ansible module to deploy blue print package on a set of servers in CenturyLink Cloud. -options: - server_ids: - description: - - A list of server Ids to deploy the blue print package. - default: [] - required: True - aliases: [] - package_id: - description: - - The package id of the blue print. - default: None - required: True - aliases: [] - package_params: - description: - - The dictionary of arguments required to deploy the blue print. - default: {} - required: False - aliases: [] -''' - -EXAMPLES = ''' -# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples - -- name: Deploy package - clc_blueprint_package: - server_ids: - - UC1WFSDANS01 - - UC1WFSDANS02 - package_id: 77abb844-579d-478d-3955-c69ab4a7ba1a - package_params: {} -''' - -__version__ = '${version}' - -import requests - -# -# Requires the clc-python-sdk. -# sudo pip install clc-sdk -# -try: - import clc as clc_sdk - from clc import CLCException -except ImportError: - CLC_FOUND = False - clc_sdk = None -else: - CLC_FOUND = True - - -class ClcBlueprintPackage(): - - clc = clc_sdk - module = None - - def __init__(self, module): - """ - Construct module - """ - self.module = module - if not CLC_FOUND: - self.module.fail_json( - msg='clc-python-sdk required for this module') - - self._set_user_agent(self.clc) - - def process_request(self): - """ - Process the request - Main Code Path - :return: Returns with either an exit_json or fail_json - """ - p = self.module.params - - if not CLC_FOUND: - self.module.fail_json( - msg='clc-python-sdk required for this module') - - self._set_clc_credentials_from_env() - - server_ids = p['server_ids'] - package_id = p['package_id'] - package_params = p['package_params'] - state = p['state'] - if state == 'present': - changed, changed_server_ids, requests = self.ensure_package_installed( - server_ids, package_id, package_params) - if not self.module.check_mode: - self._wait_for_requests_to_complete(requests) - self.module.exit_json(changed=changed, server_ids=changed_server_ids) - - @staticmethod - def define_argument_spec(): - """ - This function defnines the dictionary object required for - package module - :return: the package dictionary object - """ - argument_spec = dict( - server_ids=dict(type='list', required=True), - package_id=dict(required=True), - package_params=dict(type='dict', default={}), - wait=dict(default=True), - state=dict(default='present', choices=['present']) - ) - return argument_spec - - def ensure_package_installed(self, server_ids, package_id, package_params): - """ - Ensure the package is installed in the given list of servers - :param server_ids: the server list where the package needs to be installed - :param package_id: the package id - :param package_params: the package arguments - :return: (changed, server_ids) - changed: A flag indicating if a change was made - server_ids: The list of servers modfied - """ - changed = False - requests = [] - servers = self._get_servers_from_clc( - server_ids, - 'Failed to get servers from CLC') - try: - for server in servers: - request = self.clc_install_package( - server, - package_id, - package_params) - requests.append(request) - changed = True - except CLCException as ex: - self.module.fail_json( - msg='Failed while installing package : %s with Error : %s' % - (package_id, ex)) - return changed, server_ids, requests - - def clc_install_package(self, server, package_id, package_params): - """ - Read all servers from CLC and executes each package from package_list - :param server_list: The target list of servers where the packages needs to be installed - :param package_list: The list of packages to be installed - :return: (changed, server_ids) - changed: A flag indicating if a change was made - server_ids: The list of servers modfied - """ - result = None - if not self.module.check_mode: - result = server.ExecutePackage( - package_id=package_id, - parameters=package_params) - return result - - def _wait_for_requests_to_complete(self, requests_lst): - """ - Waits until the CLC requests are complete if the wait argument is True - :param requests_lst: The list of CLC request objects - :return: none - """ - if not self.module.params['wait']: - return - for request in requests_lst: - request.WaitUntilComplete() - for request_details in request.requests: - if request_details.Status() != 'succeeded': - self.module.fail_json( - msg='Unable to process package install request') - - def _get_servers_from_clc(self, server_list, message): - """ - Internal function to fetch list of CLC server objects from a list of server ids - :param the list server ids - :return the list of CLC server objects - """ - try: - return self.clc.v2.Servers(server_list).servers - except CLCException as ex: - self.module.fail_json(msg=message + ': %s' % ex) - - def _set_clc_credentials_from_env(self): - """ - Set the CLC Credentials on the sdk by reading environment variables - :return: none - """ - env = os.environ - v2_api_token = env.get('CLC_V2_API_TOKEN', False) - v2_api_username = env.get('CLC_V2_API_USERNAME', False) - v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) - clc_alias = env.get('CLC_ACCT_ALIAS', False) - api_url = env.get('CLC_V2_API_URL', False) - - if api_url: - self.clc.defaults.ENDPOINT_URL_V2 = api_url - - if v2_api_token and clc_alias: - self.clc._LOGIN_TOKEN_V2 = v2_api_token - self.clc._V2_ENABLED = True - self.clc.ALIAS = clc_alias - elif v2_api_username and v2_api_passwd: - self.clc.v2.SetCredentials( - api_username=v2_api_username, - api_passwd=v2_api_passwd) - else: - return self.module.fail_json( - msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " - "environment variables") - - @staticmethod - def _set_user_agent(clc): - if hasattr(clc, 'SetRequestsSession'): - agent_string = "ClcAnsibleModule/" + __version__ - ses = requests.Session() - ses.headers.update({"Api-Client": agent_string}) - ses.headers['User-Agent'] += " " + agent_string - clc.SetRequestsSession(ses) - -def main(): - """ - Main function - :return: None - """ - module = AnsibleModule( - argument_spec=ClcBlueprintPackage.define_argument_spec(), - supports_check_mode=True - ) - clc_blueprint_package = ClcBlueprintPackage(module) - clc_blueprint_package.process_request() - -from ansible.module_utils.basic import * -if __name__ == '__main__': - main() diff --git a/cloud/centurylink/clc_firewall_policy.py b/cloud/centurylink/clc_firewall_policy.py deleted file mode 100644 index 260c82bc885..00000000000 --- a/cloud/centurylink/clc_firewall_policy.py +++ /dev/null @@ -1,542 +0,0 @@ -#!/usr/bin/python - -# CenturyLink Cloud Ansible Modules. -# -# These Ansible modules enable the CenturyLink Cloud v2 API to be called -# from an within Ansible Playbook. -# -# This file is part of CenturyLink Cloud, and is maintained -# by the Workflow as a Service Team -# -# Copyright 2015 CenturyLink Cloud -# -# Licensed under the Apache License, Version 2.0 (the "License"); - -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# CenturyLink Cloud: http://www.CenturyLinkCloud.com -# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ -# - -DOCUMENTATION = ''' -module: clc_firewall_policy -short_desciption: Create/delete/update firewall policies -description: - - Create or delete or updated firewall polices on Centurylink Centurylink Cloud -options: - location: - description: - - Target datacenter for the firewall policy - default: None - required: True - aliases: [] - state: - description: - - Whether to create or delete the firewall policy - default: present - required: True - choices: ['present', 'absent'] - aliases: [] - source: - description: - - Source addresses for traffic on the originating firewall - default: None - required: For Creation - aliases: [] - destination: - description: - - Destination addresses for traffic on the terminating firewall - default: None - required: For Creation - aliases: [] - ports: - description: - - types of ports associated with the policy. TCP & UDP can take in single ports or port ranges. - default: None - required: False - choices: ['any', 'icmp', 'TCP/123', 'UDP/123', 'TCP/123-456', 'UDP/123-456'] - aliases: [] - firewall_policy_id: - description: - - Id of the firewall policy - default: None - required: False - aliases: [] - source_account_alias: - description: - - CLC alias for the source account - default: None - required: True - aliases: [] - destination_account_alias: - description: - - CLC alias for the destination account - default: None - required: False - aliases: [] - wait: - description: - - Whether to wait for the provisioning tasks to finish before returning. - default: True - required: False - choices: [ True, False ] - aliases: [] - enabled: - description: - - If the firewall policy is enabled or disabled - default: true - required: False - choices: [ true, false ] - aliases: [] - -''' - -EXAMPLES = ''' ---- -- name: Create Firewall Policy - hosts: localhost - gather_facts: False - connection: local - tasks: - - name: Create / Verify an Firewall Policy at CenturyLink Cloud - clc_firewall: - source_account_alias: WFAD - location: VA1 - state: present - source: 10.128.216.0/24 - destination: 10.128.216.0/24 - ports: Any - destination_account_alias: WFAD - ---- -- name: Delete Firewall Policy - hosts: localhost - gather_facts: False - connection: local - tasks: - - name: Delete an Firewall Policy at CenturyLink Cloud - clc_firewall: - source_account_alias: WFAD - location: VA1 - state: present - firewall_policy_id: c62105233d7a4231bd2e91b9c791eaae -''' - -__version__ = '${version}' - -import urlparse -from time import sleep -import requests - -try: - import clc as clc_sdk - from clc import CLCException -except ImportError: - CLC_FOUND = False - clc_sdk = None -else: - CLC_FOUND = True - - -class ClcFirewallPolicy(): - - clc = None - - def __init__(self, module): - """ - Construct module - """ - self.clc = clc_sdk - self.module = module - self.firewall_dict = {} - - if not CLC_FOUND: - self.module.fail_json( - msg='clc-python-sdk required for this module') - - self._set_user_agent(self.clc) - - @staticmethod - def _define_module_argument_spec(): - """ - Define the argument spec for the ansible module - :return: argument spec dictionary - """ - argument_spec = dict( - location=dict(required=True, defualt=None), - source_account_alias=dict(required=True, default=None), - destination_account_alias=dict(default=None), - firewall_policy_id=dict(default=None), - ports=dict(default=None, type='list'), - source=dict(defualt=None, type='list'), - destination=dict(defualt=None, type='list'), - wait=dict(default=True), - state=dict(default='present', choices=['present', 'absent']), - enabled=dict(defualt=None) - ) - return argument_spec - - def process_request(self): - """ - Execute the main code path, and handle the request - :return: none - """ - location = self.module.params.get('location') - source_account_alias = self.module.params.get('source_account_alias') - destination_account_alias = self.module.params.get( - 'destination_account_alias') - firewall_policy_id = self.module.params.get('firewall_policy_id') - ports = self.module.params.get('ports') - source = self.module.params.get('source') - destination = self.module.params.get('destination') - wait = self.module.params.get('wait') - state = self.module.params.get('state') - enabled = self.module.params.get('enabled') - - self.firewall_dict = { - 'location': location, - 'source_account_alias': source_account_alias, - 'destination_account_alias': destination_account_alias, - 'firewall_policy_id': firewall_policy_id, - 'ports': ports, - 'source': source, - 'destination': destination, - 'wait': wait, - 'state': state, - 'enabled': enabled} - - self._set_clc_credentials_from_env() - requests = [] - - if state == 'absent': - changed, firewall_policy_id, response = self._ensure_firewall_policy_is_absent( - source_account_alias, location, self.firewall_dict) - - elif state == 'present': - changed, firewall_policy_id, response = self._ensure_firewall_policy_is_present( - source_account_alias, location, self.firewall_dict) - else: - return self.module.fail_json(msg="Unknown State: " + state) - - return self.module.exit_json( - changed=changed, - firewall_policy_id=firewall_policy_id) - - @staticmethod - def _get_policy_id_from_response(response): - """ - Method to parse out the policy id from creation response - :param response: response from firewall creation control - :return: policy_id: firewall policy id from creation call - """ - url = response.get('links')[0]['href'] - path = urlparse.urlparse(url).path - path_list = os.path.split(path) - policy_id = path_list[-1] - return policy_id - - def _set_clc_credentials_from_env(self): - """ - Set the CLC Credentials on the sdk by reading environment variables - :return: none - """ - env = os.environ - v2_api_token = env.get('CLC_V2_API_TOKEN', False) - v2_api_username = env.get('CLC_V2_API_USERNAME', False) - v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) - clc_alias = env.get('CLC_ACCT_ALIAS', False) - api_url = env.get('CLC_V2_API_URL', False) - - if api_url: - self.clc.defaults.ENDPOINT_URL_V2 = api_url - - if v2_api_token and clc_alias: - self.clc._LOGIN_TOKEN_V2 = v2_api_token - self.clc._V2_ENABLED = True - self.clc.ALIAS = clc_alias - elif v2_api_username and v2_api_passwd: - self.clc.v2.SetCredentials( - api_username=v2_api_username, - api_passwd=v2_api_passwd) - else: - return self.module.fail_json( - msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " - "environment variables") - - def _ensure_firewall_policy_is_present( - self, - source_account_alias, - location, - firewall_dict): - """ - Ensures that a given firewall policy is present - :param source_account_alias: the source account alias for the firewall policy - :param location: datacenter of the firewall policy - :param firewall_dict: dictionary or request parameters for firewall policy creation - :return: (changed, firewall_policy, response) - changed: flag for if a change occurred - firewall_policy: policy that was changed - response: response from CLC API call - """ - changed = False - response = {} - firewall_policy_id = firewall_dict.get('firewall_policy_id') - - if firewall_policy_id is None: - if not self.module.check_mode: - response = self._create_firewall_policy( - source_account_alias, - location, - firewall_dict) - firewall_policy_id = self._get_policy_id_from_response( - response) - self._wait_for_requests_to_complete( - firewall_dict.get('wait'), - source_account_alias, - location, - firewall_policy_id) - changed = True - else: - get_before_response, success = self._get_firewall_policy( - source_account_alias, location, firewall_policy_id) - if not success: - return self.module.fail_json( - msg='Unable to find the firewall policy id : %s' % - firewall_policy_id) - changed = self._compare_get_request_with_dict( - get_before_response, - firewall_dict) - if not self.module.check_mode and changed: - response = self._update_firewall_policy( - source_account_alias, - location, - firewall_policy_id, - firewall_dict) - self._wait_for_requests_to_complete( - firewall_dict.get('wait'), - source_account_alias, - location, - firewall_policy_id) - return changed, firewall_policy_id, response - - def _ensure_firewall_policy_is_absent( - self, - source_account_alias, - location, - firewall_dict): - """ - Ensures that a given firewall policy is removed if present - :param source_account_alias: the source account alias for the firewall policy - :param location: datacenter of the firewall policy - :param firewall_dict: firewall policy to delete - :return: (changed, firewall_policy_id, response) - changed: flag for if a change occurred - firewall_policy_id: policy that was changed - response: response from CLC API call - """ - changed = False - response = [] - firewall_policy_id = firewall_dict.get('firewall_policy_id') - result, success = self._get_firewall_policy( - source_account_alias, location, firewall_policy_id) - if success: - if not self.module.check_mode: - response = self._delete_firewall_policy( - source_account_alias, - location, - firewall_policy_id) - changed = True - return changed, firewall_policy_id, response - - def _create_firewall_policy( - self, - source_account_alias, - location, - firewall_dict): - """ - Ensures that a given firewall policy is present - :param source_account_alias: the source account alias for the firewall policy - :param location: datacenter of the firewall policy - :param firewall_dict: dictionary or request parameters for firewall policy creation - :return: response from CLC API call - """ - payload = { - 'destinationAccount': firewall_dict.get('destination_account_alias'), - 'source': firewall_dict.get('source'), - 'destination': firewall_dict.get('destination'), - 'ports': firewall_dict.get('ports')} - try: - response = self.clc.v2.API.Call( - 'POST', '/v2-experimental/firewallPolicies/%s/%s' % - (source_account_alias, location), payload) - except self.clc.APIFailedResponse as e: - return self.module.fail_json( - msg="Unable to successfully create firewall policy. %s" % - str(e.response_text)) - return response - - def _delete_firewall_policy( - self, - source_account_alias, - location, - firewall_policy_id): - """ - Deletes a given firewall policy for an account alias in a datacenter - :param source_account_alias: the source account alias for the firewall policy - :param location: datacenter of the firewall policy - :param firewall_policy_id: firewall policy to delete - :return: response: response from CLC API call - """ - try: - response = self.clc.v2.API.Call( - 'DELETE', '/v2-experimental/firewallPolicies/%s/%s/%s' % - (source_account_alias, location, firewall_policy_id)) - except self.clc.APIFailedResponse as e: - return self.module.fail_json( - msg="Unable to successfully delete firewall policy. %s" % - str(e.response_text)) - return response - - def _update_firewall_policy( - self, - source_account_alias, - location, - firewall_policy_id, - firewall_dict): - """ - Updates a firewall policy for a given datacenter and account alias - :param source_account_alias: the source account alias for the firewall policy - :param location: datacenter of the firewall policy - :param firewall_policy_id: firewall policy to delete - :param firewall_dict: dictionary or request parameters for firewall policy creation - :return: response: response from CLC API call - """ - try: - response = self.clc.v2.API.Call( - 'PUT', - '/v2-experimental/firewallPolicies/%s/%s/%s' % - (source_account_alias, - location, - firewall_policy_id), - firewall_dict) - except self.clc.APIFailedResponse as e: - return self.module.fail_json( - msg="Unable to successfully update firewall policy. %s" % - str(e.response_text)) - return response - - @staticmethod - def _compare_get_request_with_dict(response, firewall_dict): - """ - Helper method to compare the json response for getting the firewall policy with the request parameters - :param response: response from the get method - :param firewall_dict: dictionary or request parameters for firewall policy creation - :return: changed: Boolean that returns true if there are differences between the response parameters and the playbook parameters - """ - - changed = False - - response_dest_account_alias = response.get('destinationAccount') - response_enabled = response.get('enabled') - response_source = response.get('source') - response_dest = response.get('destination') - response_ports = response.get('ports') - - request_dest_account_alias = firewall_dict.get( - 'destination_account_alias') - request_enabled = firewall_dict.get('enabled') - if request_enabled is None: - request_enabled = True - request_source = firewall_dict.get('source') - request_dest = firewall_dict.get('destination') - request_ports = firewall_dict.get('ports') - - if ( - response_dest_account_alias and str(response_dest_account_alias) != str(request_dest_account_alias)) or ( - response_enabled != request_enabled) or ( - response_source and response_source != request_source) or ( - response_dest and response_dest != request_dest) or ( - response_ports and response_ports != request_ports): - changed = True - return changed - - def _get_firewall_policy( - self, - source_account_alias, - location, - firewall_policy_id): - """ - Get back details for a particular firewall policy - :param source_account_alias: the source account alias for the firewall policy - :param location: datacenter of the firewall policy - :param firewall_policy_id: id of the firewall policy to get - :return: response from CLC API call - """ - response = [] - success = False - try: - response = self.clc.v2.API.Call( - 'GET', '/v2-experimental/firewallPolicies/%s/%s/%s' % - (source_account_alias, location, firewall_policy_id)) - success = True - except: - pass - return response, success - - def _wait_for_requests_to_complete( - self, - wait, - source_account_alias, - location, - firewall_policy_id): - """ - Waits until the CLC requests are complete if the wait argument is True - :param requests_lst: The list of CLC request objects - :return: none - """ - if wait: - response, success = self._get_firewall_policy( - source_account_alias, location, firewall_policy_id) - if response.get('status') == 'pending': - sleep(2) - self._wait_for_requests_to_complete( - wait, - source_account_alias, - location, - firewall_policy_id) - return None - - @staticmethod - def _set_user_agent(clc): - if hasattr(clc, 'SetRequestsSession'): - agent_string = "ClcAnsibleModule/" + __version__ - ses = requests.Session() - ses.headers.update({"Api-Client": agent_string}) - ses.headers['User-Agent'] += " " + agent_string - clc.SetRequestsSession(ses) - - -def main(): - """ - The main function. Instantiates the module and calls process_request. - :return: none - """ - module = AnsibleModule( - argument_spec=ClcFirewallPolicy._define_module_argument_spec(), - supports_check_mode=True) - - clc_firewall = ClcFirewallPolicy(module) - clc_firewall.process_request() - -from ansible.module_utils.basic import * # pylint: disable=W0614 -if __name__ == '__main__': - main() diff --git a/cloud/centurylink/clc_group.py b/cloud/centurylink/clc_group.py deleted file mode 100644 index a4fd976d429..00000000000 --- a/cloud/centurylink/clc_group.py +++ /dev/null @@ -1,370 +0,0 @@ -#!/usr/bin/python - -# CenturyLink Cloud Ansible Modules. -# -# These Ansible modules enable the CenturyLink Cloud v2 API to be called -# from an within Ansible Playbook. -# -# This file is part of CenturyLink Cloud, and is maintained -# by the Workflow as a Service Team -# -# Copyright 2015 CenturyLink Cloud -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# CenturyLink Cloud: http://www.CenturyLinkCloud.com -# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ -# - -DOCUMENTATION = ''' -module: clc_group -short_desciption: Create/delete Server Groups at Centurylink Cloud -description: - - Create or delete Server Groups at Centurylink Centurylink Cloud -options: - name: - description: - - The name of the Server Group - description: - description: - - A description of the Server Group - parent: - description: - - The parent group of the server group - location: - description: - - Datacenter to create the group in - state: - description: - - Whether to create or delete the group - default: present - choices: ['present', 'absent'] - -''' - -EXAMPLES = ''' - -# Create a Server Group - ---- -- name: Create Server Group - hosts: localhost - gather_facts: False - connection: local - tasks: - - name: Create / Verify a Server Group at CenturyLink Cloud - clc_group: - name: 'My Cool Server Group' - parent: 'Default Group' - state: present - register: clc - - - name: debug - debug: var=clc - -# Delete a Server Group - ---- -- name: Delete Server Group - hosts: localhost - gather_facts: False - connection: local - tasks: - - name: Delete / Verify Absent a Server Group at CenturyLink Cloud - clc_group: - name: 'My Cool Server Group' - parent: 'Default Group' - state: absent - register: clc - - - name: debug - debug: var=clc - -''' - -__version__ = '${version}' - -import requests - -# -# Requires the clc-python-sdk. -# sudo pip install clc-sdk -# -try: - import clc as clc_sdk - from clc import CLCException -except ImportError: - CLC_FOUND = False - clc_sdk = None -else: - CLC_FOUND = True - - -class ClcGroup(object): - - clc = None - root_group = None - - def __init__(self, module): - """ - Construct module - """ - self.clc = clc_sdk - self.module = module - self.group_dict = {} - - if not CLC_FOUND: - self.module.fail_json( - msg='clc-python-sdk required for this module') - - self._set_user_agent(self.clc) - - def process_request(self): - """ - Execute the main code path, and handle the request - :return: none - """ - location = self.module.params.get('location') - group_name = self.module.params.get('name') - parent_name = self.module.params.get('parent') - group_description = self.module.params.get('description') - state = self.module.params.get('state') - - self._set_clc_credentials_from_env() - self.group_dict = self._get_group_tree_for_datacenter( - datacenter=location) - - if state == "absent": - changed, group, response = self._ensure_group_is_absent( - group_name=group_name, parent_name=parent_name) - - else: - changed, group, response = self._ensure_group_is_present( - group_name=group_name, parent_name=parent_name, group_description=group_description) - - - self.module.exit_json(changed=changed, group=group_name) - - # - # Functions to define the Ansible module and its arguments - # - - @staticmethod - def _define_module_argument_spec(): - """ - Define the argument spec for the ansible module - :return: argument spec dictionary - """ - argument_spec = dict( - name=dict(required=True), - description=dict(default=None), - parent=dict(default=None), - location=dict(default=None), - alias=dict(default=None), - custom_fields=dict(type='list', default=[]), - server_ids=dict(type='list', default=[]), - state=dict(default='present', choices=['present', 'absent'])) - - return argument_spec - - # - # Module Behavior Functions - # - - def _set_clc_credentials_from_env(self): - """ - Set the CLC Credentials on the sdk by reading environment variables - :return: none - """ - env = os.environ - v2_api_token = env.get('CLC_V2_API_TOKEN', False) - v2_api_username = env.get('CLC_V2_API_USERNAME', False) - v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) - clc_alias = env.get('CLC_ACCT_ALIAS', False) - api_url = env.get('CLC_V2_API_URL', False) - - if api_url: - self.clc.defaults.ENDPOINT_URL_V2 = api_url - - if v2_api_token and clc_alias: - self.clc._LOGIN_TOKEN_V2 = v2_api_token - self.clc._V2_ENABLED = True - self.clc.ALIAS = clc_alias - elif v2_api_username and v2_api_passwd: - self.clc.v2.SetCredentials( - api_username=v2_api_username, - api_passwd=v2_api_passwd) - else: - return self.module.fail_json( - msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " - "environment variables") - - def _ensure_group_is_absent(self, group_name, parent_name): - """ - Ensure that group_name is absent by deleting it if necessary - :param group_name: string - the name of the clc server group to delete - :param parent_name: string - the name of the parent group for group_name - :return: changed, group - """ - changed = False - group = [] - results = [] - - if self._group_exists(group_name=group_name, parent_name=parent_name): - if not self.module.check_mode: - group.append(group_name) - for g in group: - result = self._delete_group(group_name) - results.append(result) - changed = True - return changed, group, results - - def _delete_group(self, group_name): - """ - Delete the provided server group - :param group_name: string - the server group to delete - :return: none - """ - group, parent = self.group_dict.get(group_name) - response = group.Delete() - return response - - def _ensure_group_is_present( - self, - group_name, - parent_name, - group_description): - """ - Checks to see if a server group exists, creates it if it doesn't. - :param group_name: the name of the group to validate/create - :param parent_name: the name of the parent group for group_name - :param group_description: a short description of the server group (used when creating) - :return: (changed, group) - - changed: Boolean- whether a change was made, - group: A clc group object for the group - """ - assert self.root_group, "Implementation Error: Root Group not set" - parent = parent_name if parent_name is not None else self.root_group.name - description = group_description - changed = False - results = [] - groups = [] - group = group_name - - parent_exists = self._group_exists(group_name=parent, parent_name=None) - child_exists = self._group_exists(group_name=group_name, parent_name=parent) - - if parent_exists and child_exists: - group, parent = self.group_dict[group_name] - changed = False - elif parent_exists and not child_exists: - if not self.module.check_mode: - groups.append(group_name) - for g in groups: - group = self._create_group( - group=group, - parent=parent, - description=description) - results.append(group) - changed = True - else: - self.module.fail_json( - msg="parent group: " + - parent + - " does not exist") - - return changed, group, results - - def _create_group(self, group, parent, description): - """ - Create the provided server group - :param group: clc_sdk.Group - the group to create - :param parent: clc_sdk.Parent - the parent group for {group} - :param description: string - a text description of the group - :return: clc_sdk.Group - the created group - """ - - (parent, grandparent) = self.group_dict[parent] - return parent.Create(name=group, description=description) - - # - # Utility Functions - # - - def _group_exists(self, group_name, parent_name): - """ - Check to see if a group exists - :param group_name: string - the group to check - :param parent_name: string - the parent of group_name - :return: boolean - whether the group exists - """ - result = False - if group_name in self.group_dict: - (group, parent) = self.group_dict[group_name] - if parent_name is None or parent_name == parent.name: - result = True - return result - - def _get_group_tree_for_datacenter(self, datacenter=None, alias=None): - """ - Walk the tree of groups for a datacenter - :param datacenter: string - the datacenter to walk (ex: 'UC1') - :param alias: string - the account alias to search. Defaults to the current user's account - :return: a dictionary of groups and parents - """ - self.root_group = self.clc.v2.Datacenter( - location=datacenter).RootGroup() - return self._walk_groups_recursive( - parent_group=None, - child_group=self.root_group) - - def _walk_groups_recursive(self, parent_group, child_group): - """ - Walk a parent-child tree of groups, starting with the provided child group - :param parent_group: clc_sdk.Group - the parent group to start the walk - :param child_group: clc_sdk.Group - the child group to start the walk - :return: a dictionary of groups and parents - """ - result = {str(child_group): (child_group, parent_group)} - groups = child_group.Subgroups().groups - if len(groups) > 0: - for group in groups: - if group.type != 'default': - continue - - result.update(self._walk_groups_recursive(child_group, group)) - return result - - @staticmethod - def _set_user_agent(clc): - if hasattr(clc, 'SetRequestsSession'): - agent_string = "ClcAnsibleModule/" + __version__ - ses = requests.Session() - ses.headers.update({"Api-Client": agent_string}) - ses.headers['User-Agent'] += " " + agent_string - clc.SetRequestsSession(ses) - - -def main(): - """ - The main function. Instantiates the module and calls process_request. - :return: none - """ - module = AnsibleModule(argument_spec=ClcGroup._define_module_argument_spec(), supports_check_mode=True) - - clc_group = ClcGroup(module) - clc_group.process_request() - -from ansible.module_utils.basic import * # pylint: disable=W0614 -if __name__ == '__main__': - main() diff --git a/cloud/centurylink/clc_loadbalancer.py b/cloud/centurylink/clc_loadbalancer.py deleted file mode 100644 index 058954c687b..00000000000 --- a/cloud/centurylink/clc_loadbalancer.py +++ /dev/null @@ -1,759 +0,0 @@ -#!/usr/bin/python - -# CenturyLink Cloud Ansible Modules. -# -# These Ansible modules enable the CenturyLink Cloud v2 API to be called -# from an within Ansible Playbook. -# -# This file is part of CenturyLink Cloud, and is maintained -# by the Workflow as a Service Team -# -# Copyright 2015 CenturyLink Cloud -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# CenturyLink Cloud: http://www.CenturyLinkCloud.com -# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ -# - -DOCUMENTATION = ''' -module: -short_desciption: Create, Delete shared loadbalancers in CenturyLink Cloud. -description: - - An Ansible module to Create, Delete shared loadbalancers in CenturyLink Cloud. -options: -options: - name: - description: - - The name of the loadbalancer - required: True - description: - description: - - A description for your loadbalancer - alias: - description: - - The alias of your CLC Account - required: True - location: - description: - - The location of the datacenter your load balancer resides in - required: True - method: - description: - -The balancing method for this pool - default: roundRobin - choices: ['sticky', 'roundRobin'] - persistence: - description: - - The persistence method for this load balancer - default: standard - choices: ['standard', 'sticky'] - port: - description: - - Port to configure on the public-facing side of the load balancer pool - choices: [80, 443] - nodes: - description: - - A list of nodes that you want added to your load balancer pool - status: - description: - - The status of your loadbalancer - default: enabled - choices: ['enabled', 'disabled'] - state: - description: - - Whether to create or delete the load balancer pool - default: present - choices: ['present', 'absent', 'port_absent', 'nodes_present', 'nodes_absent'] -''' - -EXAMPLES = ''' -# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples -- name: Create Loadbalancer - hosts: localhost - connection: local - tasks: - - name: Actually Create things - clc_loadbalancer: - name: test - description: test - alias: TEST - location: WA1 - port: 443 - nodes: - - { 'ipAddress': '10.11.22.123', 'privatePort': 80 } - state: present - -- name: Add node to an existing loadbalancer pool - hosts: localhost - connection: local - tasks: - - name: Actually Create things - clc_loadbalancer: - name: test - description: test - alias: TEST - location: WA1 - port: 443 - nodes: - - { 'ipAddress': '10.11.22.234', 'privatePort': 80 } - state: nodes_present - -- name: Remove node from an existing loadbalancer pool - hosts: localhost - connection: local - tasks: - - name: Actually Create things - clc_loadbalancer: - name: test - description: test - alias: TEST - location: WA1 - port: 443 - nodes: - - { 'ipAddress': '10.11.22.234', 'privatePort': 80 } - state: nodes_absent - -- name: Delete LoadbalancerPool - hosts: localhost - connection: local - tasks: - - name: Actually Delete things - clc_loadbalancer: - name: test - description: test - alias: TEST - location: WA1 - port: 443 - nodes: - - { 'ipAddress': '10.11.22.123', 'privatePort': 80 } - state: port_absent - -- name: Delete Loadbalancer - hosts: localhost - connection: local - tasks: - - name: Actually Delete things - clc_loadbalancer: - name: test - description: test - alias: TEST - location: WA1 - port: 443 - nodes: - - { 'ipAddress': '10.11.22.123', 'privatePort': 80 } - state: absent - -''' - -__version__ = '${version}' - -import requests -from time import sleep - -# -# Requires the clc-python-sdk. -# sudo pip install clc-sdk -# -try: - import clc as clc_sdk - from clc import CLCException -except ImportError: - CLC_FOUND = False - clc_sdk = None -else: - CLC_FOUND = True - -class ClcLoadBalancer(): - - clc = None - - def __init__(self, module): - """ - Construct module - """ - self.clc = clc_sdk - self.module = module - self.lb_dict = {} - - if not CLC_FOUND: - self.module.fail_json( - msg='clc-python-sdk required for this module') - - self._set_user_agent(self.clc) - - def process_request(self): - """ - Execute the main code path, and handle the request - :return: none - """ - - loadbalancer_name=self.module.params.get('name') - loadbalancer_alias=self.module.params.get('alias') - loadbalancer_location=self.module.params.get('location') - loadbalancer_description=self.module.params.get('description') - loadbalancer_port=self.module.params.get('port') - loadbalancer_method=self.module.params.get('method') - loadbalancer_persistence=self.module.params.get('persistence') - loadbalancer_nodes=self.module.params.get('nodes') - loadbalancer_status=self.module.params.get('status') - state=self.module.params.get('state') - - if loadbalancer_description == None: - loadbalancer_description = loadbalancer_name - - self._set_clc_credentials_from_env() - - self.lb_dict = self._get_loadbalancer_list(alias=loadbalancer_alias, location=loadbalancer_location) - - if state == 'present': - changed, result_lb, lb_id = self.ensure_loadbalancer_present(name=loadbalancer_name, - alias=loadbalancer_alias, - location=loadbalancer_location, - description=loadbalancer_description, - status=loadbalancer_status) - if loadbalancer_port: - changed, result_pool, pool_id = self.ensure_loadbalancerpool_present(lb_id=lb_id, - alias=loadbalancer_alias, - location=loadbalancer_location, - method=loadbalancer_method, - persistence=loadbalancer_persistence, - port=loadbalancer_port) - - if loadbalancer_nodes: - changed, result_nodes = self.ensure_lbpool_nodes_set(alias=loadbalancer_alias, - location=loadbalancer_location, - name=loadbalancer_name, - port=loadbalancer_port, - nodes=loadbalancer_nodes - ) - elif state == 'absent': - changed, result_lb = self.ensure_loadbalancer_absent(name=loadbalancer_name, - alias=loadbalancer_alias, - location=loadbalancer_location) - - elif state == 'port_absent': - changed, result_lb = self.ensure_loadbalancerpool_absent(alias=loadbalancer_alias, - location=loadbalancer_location, - name=loadbalancer_name, - port=loadbalancer_port) - - elif state == 'nodes_present': - changed, result_lb = self.ensure_lbpool_nodes_present(alias=loadbalancer_alias, - location=loadbalancer_location, - name=loadbalancer_name, - port=loadbalancer_port, - nodes=loadbalancer_nodes) - - elif state == 'nodes_absent': - changed, result_lb = self.ensure_lbpool_nodes_absent(alias=loadbalancer_alias, - location=loadbalancer_location, - name=loadbalancer_name, - port=loadbalancer_port, - nodes=loadbalancer_nodes) - - self.module.exit_json(changed=changed, loadbalancer=result_lb) - # - # Functions to define the Ansible module and its arguments - # - def ensure_loadbalancer_present(self,name,alias,location,description,status): - """ - Check for loadbalancer presence (available) - :param name: Name of loadbalancer - :param alias: Alias of account - :param location: Datacenter - :param description: Description of loadbalancer - :param status: Enabled / Disabled - :return: True / False - """ - changed = False - result = None - lb_id = self._loadbalancer_exists(name=name) - if lb_id: - result = name - changed = False - else: - if not self.module.check_mode: - result = self.create_loadbalancer(name=name, - alias=alias, - location=location, - description=description, - status=status) - lb_id = result.get('id') - changed = True - - return changed, result, lb_id - - def ensure_loadbalancerpool_present(self, lb_id, alias, location, method, persistence, port): - """ - Checks to see if a load balancer pool exists and creates one if it does not. - :param name: The loadbalancer name - :param alias: The account alias - :param location: the datacenter the load balancer resides in - :param method: the load balancing method - :param persistence: the load balancing persistence type - :param port: the port that the load balancer will listen on - :return: (changed, group, pool_id) - - changed: Boolean whether a change was made - result: The result from the CLC API call - pool_id: The string id of the pool - """ - changed = False - result = None - if not lb_id: - return False, None, None - pool_id = self._loadbalancerpool_exists(alias=alias, location=location, port=port, lb_id=lb_id) - if not pool_id: - changed = True - if not self.module.check_mode: - result = self.create_loadbalancerpool(alias=alias, location=location, lb_id=lb_id, method=method, persistence=persistence, port=port) - pool_id = result.get('id') - - else: - changed = False - result = port - - return changed, result, pool_id - - def ensure_loadbalancer_absent(self,name,alias,location): - """ - Check for loadbalancer presence (not available) - :param name: Name of loadbalancer - :param alias: Alias of account - :param location: Datacenter - :return: (changed, result) - changed: Boolean whether a change was made - result: The result from the CLC API Call - """ - changed = False - result = None - lb_exists = self._loadbalancer_exists(name=name) - if lb_exists: - if not self.module.check_mode: - result = self.delete_loadbalancer(alias=alias, - location=location, - name=name) - changed = True - else: - result = name - changed = False - return changed, result - - def ensure_loadbalancerpool_absent(self, alias, location, name, port): - """ - Checks to see if a load balancer pool exists and deletes it if it does - :param alias: The account alias - :param location: the datacenter the load balancer resides in - :param loadbalancer: the name of the load balancer - :param port: the port that the load balancer will listen on - :return: (changed, group) - - changed: Boolean whether a change was made - result: The result from the CLC API call - """ - changed = False - result = None - lb_exists = self._loadbalancer_exists(name=name) - if lb_exists: - lb_id = self._get_loadbalancer_id(name=name) - pool_id = self._loadbalancerpool_exists(alias=alias, location=location, port=port, lb_id=lb_id) - if pool_id: - changed = True - if not self.module.check_mode: - result = self.delete_loadbalancerpool(alias=alias, location=location, lb_id=lb_id, pool_id=pool_id) - else: - changed = False - result = "Pool doesn't exist" - else: - result = "LB Doesn't Exist" - return changed, result - - def ensure_lbpool_nodes_set(self, alias, location, name, port, nodes): - """ - Checks to see if the provided list of nodes exist for the pool and set the nodes if any in the list doesn't exist - :param alias: The account alias - :param location: the datacenter the load balancer resides in - :param name: the name of the load balancer - :param port: the port that the load balancer will listen on - :param nodes: The list of nodes to be updated to the pool - :return: (changed, group) - - changed: Boolean whether a change was made - result: The result from the CLC API call - """ - result = {} - changed = False - lb_exists = self._loadbalancer_exists(name=name) - if lb_exists: - lb_id = self._get_loadbalancer_id(name=name) - pool_id = self._loadbalancerpool_exists(alias=alias, location=location, port=port, lb_id=lb_id) - if pool_id: - nodes_exist = self._loadbalancerpool_nodes_exists(alias=alias, - location=location, - port=port, - lb_id=lb_id, - pool_id=pool_id, - nodes_to_check=nodes) - if not nodes_exist: - changed = True - result = self.set_loadbalancernodes(alias=alias, - location=location, - lb_id=lb_id, - pool_id=pool_id, - nodes=nodes) - else: - result = "Pool doesn't exist" - else: - result = "Load balancer doesn't Exist" - return changed, result - - def ensure_lbpool_nodes_present(self, alias, location, name, port, nodes): - """ - Checks to see if the provided list of nodes exist for the pool and add the missing nodes to the pool - :param alias: The account alias - :param location: the datacenter the load balancer resides in - :param name: the name of the load balancer - :param port: the port that the load balancer will listen on - :param nodes: the list of nodes to be added - :return: (changed, group) - - changed: Boolean whether a change was made - result: The result from the CLC API call - """ - changed = False - lb_exists = self._loadbalancer_exists(name=name) - if lb_exists: - lb_id = self._get_loadbalancer_id(name=name) - pool_id = self._loadbalancerpool_exists(alias=alias, location=location, port=port, lb_id=lb_id) - if pool_id: - changed, result = self.add_lbpool_nodes(alias=alias, - location=location, - lb_id=lb_id, - pool_id=pool_id, - nodes_to_add=nodes) - else: - result = "Pool doesn't exist" - else: - result = "Load balancer doesn't Exist" - return changed, result - - def ensure_lbpool_nodes_absent(self, alias, location, name, port, nodes): - """ - Checks to see if the provided list of nodes exist for the pool and add the missing nodes to the pool - :param alias: The account alias - :param location: the datacenter the load balancer resides in - :param name: the name of the load balancer - :param port: the port that the load balancer will listen on - :param nodes: the list of nodes to be removed - :return: (changed, group) - - changed: Boolean whether a change was made - result: The result from the CLC API call - """ - changed = False - lb_exists = self._loadbalancer_exists(name=name) - if lb_exists: - lb_id = self._get_loadbalancer_id(name=name) - pool_id = self._loadbalancerpool_exists(alias=alias, location=location, port=port, lb_id=lb_id) - if pool_id: - changed, result = self.remove_lbpool_nodes(alias=alias, - location=location, - lb_id=lb_id, - pool_id=pool_id, - nodes_to_remove=nodes) - else: - result = "Pool doesn't exist" - else: - result = "Load balancer doesn't Exist" - return changed, result - - def create_loadbalancer(self,name,alias,location,description,status): - """ - Create a loadbalancer w/ params - :param name: Name of loadbalancer - :param alias: Alias of account - :param location: Datacenter - :param description: Description for loadbalancer to be created - :param status: Enabled / Disabled - :return: Success / Failure - """ - result = self.clc.v2.API.Call('POST', '/v2/sharedLoadBalancers/%s/%s' % (alias, location), json.dumps({"name":name,"description":description,"status":status})) - sleep(1) - return result - - def create_loadbalancerpool(self, alias, location, lb_id, method, persistence, port): - """ - Creates a pool on the provided load balancer - :param alias: the account alias - :param location: the datacenter the load balancer resides in - :param lb_id: the id string of the load balancer - :param method: the load balancing method - :param persistence: the load balancing persistence type - :param port: the port that the load balancer will listen on - :return: result: The result from the create API call - """ - result = self.clc.v2.API.Call('POST', '/v2/sharedLoadBalancers/%s/%s/%s/pools' % (alias, location, lb_id), json.dumps({"port":port, "method":method, "persistence":persistence})) - return result - - def delete_loadbalancer(self,alias,location,name): - """ - Delete CLC loadbalancer - :param alias: Alias for account - :param location: Datacenter - :param name: Name of the loadbalancer to delete - :return: 204 if successful else failure - """ - lb_id = self._get_loadbalancer_id(name=name) - result = self.clc.v2.API.Call('DELETE', '/v2/sharedLoadBalancers/%s/%s/%s' % (alias, location, lb_id)) - return result - - def delete_loadbalancerpool(self, alias, location, lb_id, pool_id): - """ - Delete a pool on the provided load balancer - :param alias: The account alias - :param location: the datacenter the load balancer resides in - :param lb_id: the id string of the load balancer - :param pool_id: the id string of the pool - :return: result: The result from the delete API call - """ - result = self.clc.v2.API.Call('DELETE', '/v2/sharedLoadBalancers/%s/%s/%s/pools/%s' % (alias, location, lb_id, pool_id)) - return result - - def _get_loadbalancer_id(self, name): - """ - Retrieve unique ID of loadbalancer - :param name: Name of loadbalancer - :return: Unique ID of loadbalancer - """ - for lb in self.lb_dict: - if lb.get('name') == name: - id = lb.get('id') - return id - - def _get_loadbalancer_list(self, alias, location): - """ - Retrieve a list of loadbalancers - :param alias: Alias for account - :param location: Datacenter - :return: JSON data for all loadbalancers at datacenter - """ - return self.clc.v2.API.Call('GET', '/v2/sharedLoadBalancers/%s/%s' % (alias, location)) - - def _loadbalancer_exists(self, name): - """ - Verify a loadbalancer exists - :param name: Name of loadbalancer - :return: False or the ID of the existing loadbalancer - """ - result = False - - for lb in self.lb_dict: - if lb.get('name') == name: - result = lb.get('id') - return result - - def _loadbalancerpool_exists(self, alias, location, port, lb_id): - """ - Checks to see if a pool exists on the specified port on the provided load balancer - :param alias: the account alias - :param location: the datacenter the load balancer resides in - :param port: the port to check and see if it exists - :param lb_id: the id string of the provided load balancer - :return: result: The id string of the pool or False - """ - result = False - pool_list = self.clc.v2.API.Call('GET', '/v2/sharedLoadBalancers/%s/%s/%s/pools' % (alias, location, lb_id)) - for pool in pool_list: - if int(pool.get('port')) == int(port): - result = pool.get('id') - - return result - - def _loadbalancerpool_nodes_exists(self, alias, location, port, lb_id, pool_id, nodes_to_check): - """ - Checks to see if a set of nodes exists on the specified port on the provided load balancer - :param alias: the account alias - :param location: the datacenter the load balancer resides in - :param port: the port to check and see if it exists - :param lb_id: the id string of the provided load balancer - :param pool_id: the id string of the load balancer pool - :param nodes_to_check: the list of nodes to check for - :return: result: The id string of the pool or False - """ - result = False - nodes = self._get_lbpool_nodes(alias, location, lb_id, pool_id) - for node in nodes_to_check: - if not node.get('status'): - node['status'] = 'enabled' - if node in nodes: - result = True - else: - result = False - return result - - def set_loadbalancernodes(self, alias, location, lb_id, pool_id, nodes): - """ - Updates nodes to the provided pool - :param alias: the account alias - :param location: the datacenter the load balancer resides in - :param lb_id: the id string of the load balancer - :param pool_id: the id string of the pool - :param nodes: a list of dictionaries containing the nodes to set - :return: result: The result from the API call - """ - result = None - if not lb_id: - return result - if not self.module.check_mode: - result = self.clc.v2.API.Call('PUT', - '/v2/sharedLoadBalancers/%s/%s/%s/pools/%s/nodes' - % (alias, location, lb_id, pool_id), json.dumps(nodes)) - return result - - def add_lbpool_nodes(self, alias, location, lb_id, pool_id, nodes_to_add): - """ - Add nodes to the provided pool - :param alias: the account alias - :param location: the datacenter the load balancer resides in - :param lb_id: the id string of the load balancer - :param pool_id: the id string of the pool - :param nodes: a list of dictionaries containing the nodes to add - :return: (changed, group) - - changed: Boolean whether a change was made - result: The result from the CLC API call - """ - changed = False - result = {} - nodes = self._get_lbpool_nodes(alias, location, lb_id, pool_id) - for node in nodes_to_add: - if not node.get('status'): - node['status'] = 'enabled' - if not node in nodes: - changed = True - nodes.append(node) - if changed == True and not self.module.check_mode: - result = self.set_loadbalancernodes(alias, location, lb_id, pool_id, nodes) - return changed, result - - def remove_lbpool_nodes(self, alias, location, lb_id, pool_id, nodes_to_remove): - """ - Removes nodes from the provided pool - :param alias: the account alias - :param location: the datacenter the load balancer resides in - :param lb_id: the id string of the load balancer - :param pool_id: the id string of the pool - :param nodes: a list of dictionaries containing the nodes to remove - :return: (changed, group) - - changed: Boolean whether a change was made - result: The result from the CLC API call - """ - changed = False - result = {} - nodes = self._get_lbpool_nodes(alias, location, lb_id, pool_id) - for node in nodes_to_remove: - if not node.get('status'): - node['status'] = 'enabled' - if node in nodes: - changed = True - nodes.remove(node) - if changed == True and not self.module.check_mode: - result = self.set_loadbalancernodes(alias, location, lb_id, pool_id, nodes) - return changed, result - - def _get_lbpool_nodes(self, alias, location, lb_id, pool_id): - """ - Return the list of nodes available to the provided load balancer pool - :param alias: the account alias - :param location: the datacenter the load balancer resides in - :param lb_id: the id string of the load balancer - :param pool_id: the id string of the pool - :return: result: The list of nodes - """ - result = self.clc.v2.API.Call('GET', - '/v2/sharedLoadBalancers/%s/%s/%s/pools/%s/nodes' - % (alias, location, lb_id, pool_id)) - return result - - @staticmethod - def define_argument_spec(): - """ - Define the argument spec for the ansible module - :return: argument spec dictionary - """ - argument_spec = dict( - name=dict(required=True), - description=dict(default=None), - location=dict(required=True, default=None), - alias=dict(required=True, default=None), - port=dict(choices=[80, 443]), - method=dict(choices=['leastConnection', 'roundRobin']), - persistence=dict(choices=['standard', 'sticky']), - nodes=dict(type='list', default=[]), - status=dict(default='enabled', choices=['enabled', 'disabled']), - state=dict(default='present', choices=['present', 'absent', 'port_absent', 'nodes_present', 'nodes_absent']), - wait=dict(type='bool', default=True) - ) - - return argument_spec - - # - # Module Behavior Functions - # - - def _set_clc_credentials_from_env(self): - """ - Set the CLC Credentials on the sdk by reading environment variables - :return: none - """ - env = os.environ - v2_api_token = env.get('CLC_V2_API_TOKEN', False) - v2_api_username = env.get('CLC_V2_API_USERNAME', False) - v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) - clc_alias = env.get('CLC_ACCT_ALIAS', False) - api_url = env.get('CLC_V2_API_URL', False) - - if api_url: - self.clc.defaults.ENDPOINT_URL_V2 = api_url - - if v2_api_token and clc_alias: - self.clc._LOGIN_TOKEN_V2 = v2_api_token - self.clc._V2_ENABLED = True - self.clc.ALIAS = clc_alias - elif v2_api_username and v2_api_passwd: - self.clc.v2.SetCredentials( - api_username=v2_api_username, - api_passwd=v2_api_passwd) - else: - return self.module.fail_json( - msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " - "environment variables") - - @staticmethod - def _set_user_agent(clc): - if hasattr(clc, 'SetRequestsSession'): - agent_string = "ClcAnsibleModule/" + __version__ - ses = requests.Session() - ses.headers.update({"Api-Client": agent_string}) - ses.headers['User-Agent'] += " " + agent_string - clc.SetRequestsSession(ses) - - -def main(): - """ - The main function. Instantiates the module and calls process_request. - :return: none - """ - module = AnsibleModule(argument_spec=ClcLoadBalancer.define_argument_spec(), - supports_check_mode=True) - clc_loadbalancer = ClcLoadBalancer(module) - clc_loadbalancer.process_request() - -from ansible.module_utils.basic import * # pylint: disable=W0614 -if __name__ == '__main__': - main() diff --git a/cloud/centurylink/clc_modify_server.py b/cloud/centurylink/clc_modify_server.py deleted file mode 100644 index 1a1e4d5b858..00000000000 --- a/cloud/centurylink/clc_modify_server.py +++ /dev/null @@ -1,710 +0,0 @@ -#!/usr/bin/python - -# CenturyLink Cloud Ansible Modules. -# -# These Ansible modules enable the CenturyLink Cloud v2 API to be called -# from an within Ansible Playbook. -# -# This file is part of CenturyLink Cloud, and is maintained -# by the Workflow as a Service Team -# -# Copyright 2015 CenturyLink Cloud -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# CenturyLink Cloud: http://www.CenturyLinkCloud.com -# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ -# - -DOCUMENTATION = ''' -module: clc_modify_server -short_desciption: modify servers in CenturyLink Cloud. -description: - - An Ansible module to modify servers in CenturyLink Cloud. -options: - server_ids: - description: - - A list of server Ids to modify. - default: [] - required: True - aliases: [] - cpu: - description: - - How many CPUs to update on the server - default: None - required: False - aliases: [] - memory: - description: - - Memory in GB. - default: None - required: False - aliases: [] - anti_affinity_policy_id: - description: - - The anti affinity policy id to be set for a heperscale server. - This is mutually exclusive with 'anti_affinity_policy_name' - default: None - required: False - aliases: [] - anti_affinity_policy_name: - description: - - The anti affinity policy name to be set for a heperscale server. - This is mutually exclusive with 'anti_affinity_policy_id' - default: None - required: False - aliases: [] - alert_policy_id: - description: - - The alert policy id to be associated. - This is mutually exclusive with 'alert_policy_name' - default: None - required: False - aliases: [] - alert_policy_name: - description: - - The alert policy name to be associated. - This is mutually exclusive with 'alert_policy_id' - default: None - required: False - aliases: [] - state: - description: - - The state to insure that the provided resources are in. - default: 'present' - required: False - choices: ['present', 'absent'] - aliases: [] - wait: - description: - - Whether to wait for the provisioning tasks to finish before returning. - default: True - required: False - choices: [ True, False] - aliases: [] -''' - -EXAMPLES = ''' -# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples - -- name: set the cpu count to 4 on a server - clc_server: - server_ids: ['UC1ACCTTEST01'] - cpu: 4 - state: present - -- name: set the memory to 8GB on a server - clc_server: - server_ids: ['UC1ACCTTEST01'] - memory: 8 - state: present - -- name: set the anti affinity policy on a server - clc_server: - server_ids: ['UC1ACCTTEST01'] - anti_affinity_policy_name: 'aa_policy' - state: present - -- name: set the alert policy on a server - clc_server: - server_ids: ['UC1ACCTTEST01'] - alert_policy_name: 'alert_policy' - state: present - -- name: set the memory to 16GB and cpu to 8 core on a lust if servers - clc_server: - server_ids: ['UC1ACCTTEST01','UC1ACCTTEST02'] - cpu: 8 - memory: 16 - state: present -''' - -__version__ = '${version}' - -import requests - -# -# Requires the clc-python-sdk. -# sudo pip install clc-sdk -# -try: - import clc as clc_sdk - from clc import CLCException - from clc import APIFailedResponse -except ImportError: - CLC_FOUND = False - clc_sdk = None -else: - CLC_FOUND = True - - -class ClcModifyServer(): - clc = clc_sdk - - def __init__(self, module): - """ - Construct module - """ - self.clc = clc_sdk - self.module = module - self.group_dict = {} - - if not CLC_FOUND: - self.module.fail_json( - msg='clc-python-sdk required for this module') - - self._set_user_agent(self.clc) - - def process_request(self): - """ - Process the request - Main Code Path - :return: Returns with either an exit_json or fail_json - """ - self._set_clc_credentials_from_env() - - p = self.module.params - - server_ids = p['server_ids'] - if not isinstance(server_ids, list): - return self.module.fail_json( - msg='server_ids needs to be a list of instances to modify: %s' % - server_ids) - - (changed, server_dict_array, new_server_ids) = ClcModifyServer._modify_servers( - module=self.module, clc=self.clc, server_ids=server_ids) - - self.module.exit_json( - changed=changed, - server_ids=new_server_ids, - servers=server_dict_array) - - @staticmethod - def _define_module_argument_spec(): - """ - Define the argument spec for the ansible module - :return: argument spec dictionary - """ - argument_spec = dict( - server_ids=dict(type='list', required=True), - state=dict(default='present', choices=['present', 'absent']), - cpu=dict(), - memory=dict(), - anti_affinity_policy_id=dict(), - anti_affinity_policy_name=dict(), - alert_policy_id=dict(), - alert_policy_name=dict(), - wait=dict(type='bool', default=True) - ) - mutually_exclusive = [ - ['anti_affinity_policy_id', 'anti_affinity_policy_name'], - ['alert_policy_id', 'alert_policy_name'] - ] - return {"argument_spec": argument_spec, - "mutually_exclusive": mutually_exclusive} - - def _set_clc_credentials_from_env(self): - """ - Set the CLC Credentials on the sdk by reading environment variables - :return: none - """ - env = os.environ - v2_api_token = env.get('CLC_V2_API_TOKEN', False) - v2_api_username = env.get('CLC_V2_API_USERNAME', False) - v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) - clc_alias = env.get('CLC_ACCT_ALIAS', False) - api_url = env.get('CLC_V2_API_URL', False) - - if api_url: - self.clc.defaults.ENDPOINT_URL_V2 = api_url - - if v2_api_token and clc_alias: - self.clc._LOGIN_TOKEN_V2 = v2_api_token - self.clc._V2_ENABLED = True - self.clc.ALIAS = clc_alias - elif v2_api_username and v2_api_passwd: - self.clc.v2.SetCredentials( - api_username=v2_api_username, - api_passwd=v2_api_passwd) - else: - return self.module.fail_json( - msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " - "environment variables") - - @staticmethod - def _wait_for_requests(clc, requests, servers, wait): - """ - Block until server provisioning requests are completed. - :param clc: the clc-sdk instance to use - :param requests: a list of clc-sdk.Request instances - :param servers: a list of servers to refresh - :param wait: a boolean on whether to block or not. This function is skipped if True - :return: none - """ - if wait: - # Requests.WaitUntilComplete() returns the count of failed requests - failed_requests_count = sum( - [request.WaitUntilComplete() for request in requests]) - - if failed_requests_count > 0: - raise clc - else: - ClcModifyServer._refresh_servers(servers) - - @staticmethod - def _refresh_servers(servers): - """ - Loop through a list of servers and refresh them - :param servers: list of clc-sdk.Server instances to refresh - :return: none - """ - for server in servers: - server.Refresh() - - @staticmethod - def _modify_servers(module, clc, server_ids): - """ - modify the servers configuration on the provided list - :param module: the AnsibleModule object - :param clc: the clc-sdk instance to use - :param server_ids: list of servers to modify - :return: a list of dictionaries with server information about the servers that were modified - """ - p = module.params - wait = p.get('wait') - state = p.get('state') - server_params = { - 'cpu': p.get('cpu'), - 'memory': p.get('memory'), - 'anti_affinity_policy_id': p.get('anti_affinity_policy_id'), - 'anti_affinity_policy_name': p.get('anti_affinity_policy_name'), - 'alert_policy_id': p.get('alert_policy_id'), - 'alert_policy_name': p.get('alert_policy_name'), - } - changed = False - server_changed = False - aa_changed = False - ap_changed = False - server_dict_array = [] - result_server_ids = [] - requests = [] - - if not isinstance(server_ids, list) or len(server_ids) < 1: - return module.fail_json( - msg='server_ids should be a list of servers, aborting') - - servers = clc.v2.Servers(server_ids).Servers() - if state == 'present': - for server in servers: - server_changed, server_result, changed_servers = ClcModifyServer._ensure_server_config( - clc, module, None, server, server_params) - if server_result: - requests.append(server_result) - aa_changed, changed_servers = ClcModifyServer._ensure_aa_policy( - clc, module, None, server, server_params) - ap_changed, changed_servers = ClcModifyServer._ensure_alert_policy_present( - clc, module, None, server, server_params) - elif state == 'absent': - for server in servers: - ap_changed, changed_servers = ClcModifyServer._ensure_alert_policy_absent( - clc, module, None, server, server_params) - if server_changed or aa_changed or ap_changed: - changed = True - - if wait: - for r in requests: - r.WaitUntilComplete() - for server in changed_servers: - server.Refresh() - - for server in changed_servers: - server_dict_array.append(server.data) - result_server_ids.append(server.id) - - return changed, server_dict_array, result_server_ids - - @staticmethod - def _ensure_server_config( - clc, module, alias, server, server_params): - """ - ensures the server is updated with the provided cpu and memory - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param alias: the CLC account alias - :param server: the CLC server object - :param server_params: the dictionary of server parameters - :return: (changed, group) - - changed: Boolean whether a change was made - result: The result from the CLC API call - """ - cpu = server_params.get('cpu') - memory = server_params.get('memory') - changed = False - result = None - changed_servers = [] - - if not cpu: - cpu = server.cpu - if not memory: - memory = server.memory - if memory != server.memory or cpu != server.cpu: - changed_servers.append(server) - result = ClcModifyServer._modify_clc_server( - clc, - module, - None, - server.id, - cpu, - memory) - changed = True - return changed, result, changed_servers - - @staticmethod - def _modify_clc_server(clc, module, acct_alias, server_id, cpu, memory): - """ - Modify the memory or CPU on a clc server. This function is not yet implemented. - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param acct_alias: the clc account alias to look up the server - :param server_id: id of the server to modify - :param cpu: the new cpu value - :param memory: the new memory value - :return: the result of CLC API call - """ - if not acct_alias: - acct_alias = clc.v2.Account.GetAlias() - if not server_id: - return module.fail_json( - msg='server_id must be provided to modify the server') - - result = None - - if not module.check_mode: - - # Update the server configuation - job_obj = clc.v2.API.Call('PATCH', - 'servers/%s/%s' % (acct_alias, - server_id), - json.dumps([{"op": "set", - "member": "memory", - "value": memory}, - {"op": "set", - "member": "cpu", - "value": cpu}])) - result = clc.v2.Requests(job_obj) - return result - - @staticmethod - def _ensure_aa_policy( - clc, module, acct_alias, server, server_params): - """ - ensures the server is updated with the provided anti affinity policy - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param acct_alias: the CLC account alias - :param server: the CLC server object - :param server_params: the dictionary of server parameters - :return: (changed, group) - - changed: Boolean whether a change was made - result: The result from the CLC API call - """ - changed = False - changed_servers = [] - - if not acct_alias: - acct_alias = clc.v2.Account.GetAlias() - - aa_policy_id = server_params.get('anti_affinity_policy_id') - aa_policy_name = server_params.get('anti_affinity_policy_name') - if not aa_policy_id and aa_policy_name: - aa_policy_id = ClcModifyServer._get_aa_policy_id_by_name( - clc, - module, - acct_alias, - aa_policy_name) - current_aa_policy_id = ClcModifyServer._get_aa_policy_id_of_server( - clc, - module, - acct_alias, - server.id) - - if aa_policy_id and aa_policy_id != current_aa_policy_id: - if server not in changed_servers: - changed_servers.append(server) - ClcModifyServer._modify_aa_policy( - clc, - module, - acct_alias, - server.id, - aa_policy_id) - changed = True - return changed, changed_servers - - @staticmethod - def _modify_aa_policy(clc, module, acct_alias, server_id, aa_policy_id): - """ - modifies the anti affinity policy of the CLC server - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param acct_alias: the CLC account alias - :param server_id: the CLC server id - :param aa_policy_id: the anti affinity policy id - :return: result: The result from the CLC API call - """ - result = None - if not module.check_mode: - result = clc.v2.API.Call('PUT', - 'servers/%s/%s/antiAffinityPolicy' % ( - acct_alias, - server_id), - json.dumps({"id": aa_policy_id})) - return result - - @staticmethod - def _get_aa_policy_id_by_name(clc, module, alias, aa_policy_name): - """ - retrieves the anti affinity policy id of the server based on the name of the policy - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param alias: the CLC account alias - :param aa_policy_name: the anti affinity policy name - :return: aa_policy_id: The anti affinity policy id - """ - aa_policy_id = None - aa_policies = clc.v2.API.Call(method='GET', - url='antiAffinityPolicies/%s' % (alias)) - for aa_policy in aa_policies.get('items'): - if aa_policy.get('name') == aa_policy_name: - if not aa_policy_id: - aa_policy_id = aa_policy.get('id') - else: - return module.fail_json( - msg='mutiple anti affinity policies were found with policy name : %s' % - (aa_policy_name)) - if not aa_policy_id: - return module.fail_json( - msg='No anti affinity policy was found with policy name : %s' % - (aa_policy_name)) - return aa_policy_id - - @staticmethod - def _get_aa_policy_id_of_server(clc, module, alias, server_id): - """ - retrieves the anti affinity policy id of the server based on the CLC server id - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param alias: the CLC account alias - :param server_id: the CLC server id - :return: aa_policy_id: The anti affinity policy id - """ - aa_policy_id = None - try: - result = clc.v2.API.Call( - method='GET', url='servers/%s/%s/antiAffinityPolicy' % - (alias, server_id)) - aa_policy_id = result.get('id') - except APIFailedResponse as e: - if e.response_status_code != 404: - raise e - return aa_policy_id - - @staticmethod - def _ensure_alert_policy_present( - clc, module, acct_alias, server, server_params): - """ - ensures the server is updated with the provided alert policy - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param acct_alias: the CLC account alias - :param server: the CLC server object - :param server_params: the dictionary of server parameters - :return: (changed, group) - - changed: Boolean whether a change was made - result: The result from the CLC API call - """ - changed = False - changed_servers = [] - - if not acct_alias: - acct_alias = clc.v2.Account.GetAlias() - - alert_policy_id = server_params.get('alert_policy_id') - alert_policy_name = server_params.get('alert_policy_name') - if not alert_policy_id and alert_policy_name: - alert_policy_id = ClcModifyServer._get_alert_policy_id_by_name( - clc, - module, - acct_alias, - alert_policy_name) - if alert_policy_id and not ClcModifyServer._alert_policy_exists(server, alert_policy_id): - if server not in changed_servers: - changed_servers.append(server) - ClcModifyServer._add_alert_policy_to_server( - clc, - module, - acct_alias, - server.id, - alert_policy_id) - changed = True - return changed, changed_servers - - @staticmethod - def _ensure_alert_policy_absent( - clc, module, acct_alias, server, server_params): - """ - ensures the alert policy is removed from the server - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param acct_alias: the CLC account alias - :param server: the CLC server object - :param server_params: the dictionary of server parameters - :return: (changed, group) - - changed: Boolean whether a change was made - result: The result from the CLC API call - """ - changed = False - result = None - changed_servers = [] - - if not acct_alias: - acct_alias = clc.v2.Account.GetAlias() - - alert_policy_id = server_params.get('alert_policy_id') - alert_policy_name = server_params.get('alert_policy_name') - if not alert_policy_id and alert_policy_name: - alert_policy_id = ClcModifyServer._get_alert_policy_id_by_name( - clc, - module, - acct_alias, - alert_policy_name) - - if alert_policy_id and ClcModifyServer._alert_policy_exists(server, alert_policy_id): - if server not in changed_servers: - changed_servers.append(server) - ClcModifyServer._remove_alert_policy_to_server( - clc, - module, - acct_alias, - server.id, - alert_policy_id) - changed = True - return changed, changed_servers - - @staticmethod - def _add_alert_policy_to_server(clc, module, acct_alias, server_id, alert_policy_id): - """ - add the alert policy to CLC server - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param acct_alias: the CLC account alias - :param server_id: the CLC server id - :param alert_policy_id: the alert policy id - :return: result: The result from the CLC API call - """ - result = None - if not module.check_mode: - try: - result = clc.v2.API.Call('POST', - 'servers/%s/%s/alertPolicies' % ( - acct_alias, - server_id), - json.dumps({"id": alert_policy_id})) - except clc.APIFailedResponse as e: - return module.fail_json( - msg='Unable to set alert policy to the server : %s. %s' % (server_id, str(e.response_text))) - return result - - @staticmethod - def _remove_alert_policy_to_server(clc, module, acct_alias, server_id, alert_policy_id): - """ - remove the alert policy to the CLC server - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param acct_alias: the CLC account alias - :param server_id: the CLC server id - :param alert_policy_id: the alert policy id - :return: result: The result from the CLC API call - """ - result = None - if not module.check_mode: - try: - result = clc.v2.API.Call('DELETE', - 'servers/%s/%s/alertPolicies/%s' - % (acct_alias, server_id, alert_policy_id)) - except clc.APIFailedResponse as e: - return module.fail_json( - msg='Unable to remove alert policy to the server : %s. %s' % (server_id, str(e.response_text))) - return result - - @staticmethod - def _get_alert_policy_id_by_name(clc, module, alias, alert_policy_name): - """ - retrieves the alert policy id of the server based on the name of the policy - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param alias: the CLC account alias - :param alert_policy_name: the alert policy name - :return: alert_policy_id: The alert policy id - """ - alert_policy_id = None - alert_policies = clc.v2.API.Call(method='GET', - url='alertPolicies/%s' % (alias)) - for alert_policy in alert_policies.get('items'): - if alert_policy.get('name') == alert_policy_name: - if not alert_policy_id: - alert_policy_id = alert_policy.get('id') - else: - return module.fail_json( - msg='mutiple alert policies were found with policy name : %s' % - (alert_policy_name)) - return alert_policy_id - - @staticmethod - def _alert_policy_exists(server, alert_policy_id): - """ - Checks if the alert policy exists for the server - :param server: the clc server object - :param alert_policy_id: the alert policy - :return: True: if the given alert policy id associated to the server, False otherwise - """ - result = False - alert_policies = server.alertPolicies - if alert_policies: - for alert_policy in alert_policies: - if alert_policy.get('id') == alert_policy_id: - result = True - return result - - @staticmethod - def _set_user_agent(clc): - if hasattr(clc, 'SetRequestsSession'): - agent_string = "ClcAnsibleModule/" + __version__ - ses = requests.Session() - ses.headers.update({"Api-Client": agent_string}) - ses.headers['User-Agent'] += " " + agent_string - clc.SetRequestsSession(ses) - - -def main(): - """ - The main function. Instantiates the module and calls process_request. - :return: none - """ - - argument_dict = ClcModifyServer._define_module_argument_spec() - module = AnsibleModule(supports_check_mode=True, **argument_dict) - clc_modify_server = ClcModifyServer(module) - clc_modify_server.process_request() - -from ansible.module_utils.basic import * # pylint: disable=W0614 -if __name__ == '__main__': - main() diff --git a/cloud/centurylink/clc_publicip.py b/cloud/centurylink/clc_publicip.py deleted file mode 100644 index 2e525a51455..00000000000 --- a/cloud/centurylink/clc_publicip.py +++ /dev/null @@ -1,316 +0,0 @@ -#!/usr/bin/python - -# CenturyLink Cloud Ansible Modules. -# -# These Ansible modules enable the CenturyLink Cloud v2 API to be called -# from an within Ansible Playbook. -# -# This file is part of CenturyLink Cloud, and is maintained -# by the Workflow as a Service Team -# -# Copyright 2015 CenturyLink Cloud -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# CenturyLink Cloud: http://www.CenturyLinkCloud.com -# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ -# - -DOCUMENTATION = ''' -module: clc_publicip -short_description: Add and Delete public ips on servers in CenturyLink Cloud. -description: - - An Ansible module to add or delete public ip addresses on an existing server or servers in CenturyLink Cloud. -options: - protocol: - descirption: - - The protocol that the public IP will listen for. - default: TCP - required: False - ports: - description: - - A list of ports to expose. - required: True - server_ids: - description: - - A list of servers to create public ips on. - required: True - state: - description: - - Determine wheteher to create or delete public IPs. If present module will not create a second public ip if one - already exists. - default: present - choices: ['present', 'absent'] - required: False - wait: - description: - - Whether to wait for the tasks to finish before returning. - choices: [ True, False ] - default: True - required: False -''' - -EXAMPLES = ''' -# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples - -- name: Add Public IP to Server - hosts: localhost - gather_facts: False - connection: local - tasks: - - name: Create Public IP For Servers - clc_publicip: - protocol: 'TCP' - ports: - - 80 - server_ids: - - UC1ACCTSRVR01 - - UC1ACCTSRVR02 - state: present - register: clc - - - name: debug - debug: var=clc - -- name: Delete Public IP from Server - hosts: localhost - gather_facts: False - connection: local - tasks: - - name: Create Public IP For Servers - clc_publicip: - server_ids: - - UC1ACCTSRVR01 - - UC1ACCTSRVR02 - state: absent - register: clc - - - name: debug - debug: var=clc -''' - -__version__ = '${version}' - -import requests - -# -# Requires the clc-python-sdk. -# sudo pip install clc-sdk -# -try: - import clc as clc_sdk - from clc import CLCException -except ImportError: - CLC_FOUND = False - clc_sdk = None -else: - CLC_FOUND = True - - -class ClcPublicIp(object): - clc = clc_sdk - module = None - group_dict = {} - - def __init__(self, module): - """ - Construct module - """ - self.module = module - if not CLC_FOUND: - self.module.fail_json( - msg='clc-python-sdk required for this module') - - self._set_user_agent(self.clc) - - def process_request(self): - """ - Process the request - Main Code Path - :param params: dictionary of module parameters - :return: Returns with either an exit_json or fail_json - """ - self._set_clc_credentials_from_env() - params = self.module.params - server_ids = params['server_ids'] - ports = params['ports'] - protocol = params['protocol'] - state = params['state'] - requests = [] - chagned_server_ids = [] - changed = False - - if state == 'present': - changed, chagned_server_ids, requests = self.ensure_public_ip_present( - server_ids=server_ids, protocol=protocol, ports=ports) - elif state == 'absent': - changed, chagned_server_ids, requests = self.ensure_public_ip_absent( - server_ids=server_ids) - else: - return self.module.fail_json(msg="Unknown State: " + state) - self._wait_for_requests_to_complete(requests) - return self.module.exit_json(changed=changed, - server_ids=chagned_server_ids) - - @staticmethod - def _define_module_argument_spec(): - """ - Define the argument spec for the ansible module - :return: argument spec dictionary - """ - argument_spec = dict( - server_ids=dict(type='list', required=True), - protocol=dict(default='TCP'), - ports=dict(type='list'), - wait=dict(type='bool', default=True), - state=dict(default='present', choices=['present', 'absent']), - ) - return argument_spec - - def ensure_public_ip_present(self, server_ids, protocol, ports): - """ - Ensures the given server ids having the public ip available - :param server_ids: the list of server ids - :param protocol: the ip protocol - :param ports: the list of ports to expose - :return: (changed, changed_server_ids, results) - changed: A flag indicating if there is any change - changed_server_ids : the list of server ids that are changed - results: The result list from clc public ip call - """ - changed = False - results = [] - changed_server_ids = [] - servers = self._get_servers_from_clc( - server_ids, - 'Failed to obtain server list from the CLC API') - servers_to_change = [ - server for server in servers if len( - server.PublicIPs().public_ips) == 0] - ports_to_expose = [{'protocol': protocol, 'port': port} - for port in ports] - for server in servers_to_change: - if not self.module.check_mode: - result = server.PublicIPs().Add(ports_to_expose) - results.append(result) - changed_server_ids.append(server.id) - changed = True - return changed, changed_server_ids, results - - def ensure_public_ip_absent(self, server_ids): - """ - Ensures the given server ids having the public ip removed if there is any - :param server_ids: the list of server ids - :return: (changed, changed_server_ids, results) - changed: A flag indicating if there is any change - changed_server_ids : the list of server ids that are changed - results: The result list from clc public ip call - """ - changed = False - results = [] - changed_server_ids = [] - servers = self._get_servers_from_clc( - server_ids, - 'Failed to obtain server list from the CLC API') - servers_to_change = [ - server for server in servers if len( - server.PublicIPs().public_ips) > 0] - ips_to_delete = [] - for server in servers_to_change: - for ip_address in server.PublicIPs().public_ips: - ips_to_delete.append(ip_address) - for server in servers_to_change: - if not self.module.check_mode: - for ip in ips_to_delete: - result = ip.Delete() - results.append(result) - changed_server_ids.append(server.id) - changed = True - return changed, changed_server_ids, results - - def _wait_for_requests_to_complete(self, requests_lst): - """ - Waits until the CLC requests are complete if the wait argument is True - :param requests_lst: The list of CLC request objects - :return: none - """ - if not self.module.params['wait']: - return - for request in requests_lst: - request.WaitUntilComplete() - for request_details in request.requests: - if request_details.Status() != 'succeeded': - self.module.fail_json( - msg='Unable to process public ip request') - - def _set_clc_credentials_from_env(self): - """ - Set the CLC Credentials on the sdk by reading environment variables - :return: none - """ - env = os.environ - v2_api_token = env.get('CLC_V2_API_TOKEN', False) - v2_api_username = env.get('CLC_V2_API_USERNAME', False) - v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) - clc_alias = env.get('CLC_ACCT_ALIAS', False) - api_url = env.get('CLC_V2_API_URL', False) - - if api_url: - self.clc.defaults.ENDPOINT_URL_V2 = api_url - - if v2_api_token and clc_alias: - self.clc._LOGIN_TOKEN_V2 = v2_api_token - self.clc._V2_ENABLED = True - self.clc.ALIAS = clc_alias - elif v2_api_username and v2_api_passwd: - self.clc.v2.SetCredentials( - api_username=v2_api_username, - api_passwd=v2_api_passwd) - else: - return self.module.fail_json( - msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " - "environment variables") - - def _get_servers_from_clc(self, server_ids, message): - """ - Gets list of servers form CLC api - """ - try: - return self.clc.v2.Servers(server_ids).servers - except CLCException as exception: - self.module.fail_json(msg=message + ': %s' % exception) - - @staticmethod - def _set_user_agent(clc): - if hasattr(clc, 'SetRequestsSession'): - agent_string = "ClcAnsibleModule/" + __version__ - ses = requests.Session() - ses.headers.update({"Api-Client": agent_string}) - ses.headers['User-Agent'] += " " + agent_string - clc.SetRequestsSession(ses) - - -def main(): - """ - The main function. Instantiates the module and calls process_request. - :return: none - """ - module = AnsibleModule( - argument_spec=ClcPublicIp._define_module_argument_spec(), - supports_check_mode=True - ) - clc_public_ip = ClcPublicIp(module) - clc_public_ip.process_request() - -from ansible.module_utils.basic import * # pylint: disable=W0614 -if __name__ == '__main__': - main() diff --git a/cloud/centurylink/clc_server.py b/cloud/centurylink/clc_server.py deleted file mode 100644 index e102cd21f47..00000000000 --- a/cloud/centurylink/clc_server.py +++ /dev/null @@ -1,1323 +0,0 @@ -#!/usr/bin/python - -# CenturyLink Cloud Ansible Modules. -# -# These Ansible modules enable the CenturyLink Cloud v2 API to be called -# from an within Ansible Playbook. -# -# This file is part of CenturyLink Cloud, and is maintained -# by the Workflow as a Service Team -# -# Copyright 2015 CenturyLink Cloud -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# CenturyLink Cloud: http://www.CenturyLinkCloud.com -# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ -# - -DOCUMENTATION = ''' -module: clc_server -short_desciption: Create, Delete, Start and Stop servers in CenturyLink Cloud. -description: - - An Ansible module to Create, Delete, Start and Stop servers in CenturyLink Cloud. -options: - additional_disks: - description: - - Specify additional disks for the server - required: False - default: None - aliases: [] - add_public_ip: - description: - - Whether to add a public ip to the server - required: False - default: False - choices: [False, True] - aliases: [] - alias: - description: - - The account alias to provision the servers under. - default: - - The default alias for the API credentials - required: False - default: None - aliases: [] - anti_affinity_policy_id: - description: - - The anti-affinity policy to assign to the server. This is mutually exclusive with 'anti_affinity_policy_name'. - required: False - default: None - aliases: [] - anti_affinity_policy_name: - description: - - The anti-affinity policy to assign to the server. This is mutually exclusive with 'anti_affinity_policy_id'. - required: False - default: None - aliases: [] - alert_policy_id: - description: - - The alert policy to assign to the server. This is mutually exclusive with 'alert_policy_name'. - required: False - default: None - aliases: [] - alert_policy_name: - description: - - The alert policy to assign to the server. This is mutually exclusive with 'alert_policy_id'. - required: False - default: None - aliases: [] - - count: - description: - - The number of servers to build (mutually exclusive with exact_count) - default: None - aliases: [] - count_group: - description: - - Required when exact_count is specified. The Server Group use to determine how many severs to deploy. - default: 1 - required: False - aliases: [] - cpu: - description: - - How many CPUs to provision on the server - default: None - required: False - aliases: [] - cpu_autoscale_policy_id: - description: - - The autoscale policy to assign to the server. - default: None - required: False - aliases: [] - custom_fields: - description: - - A dictionary of custom fields to set on the server. - default: [] - required: False - aliases: [] - description: - description: - - The description to set for the server. - default: None - required: False - aliases: [] - exact_count: - description: - - Run in idempotent mode. Will insure that this exact number of servers are running in the provided group, creating and deleting them to reach that count. Requires count_group to be set. - default: None - required: False - aliases: [] - group: - description: - - The Server Group to create servers under. - default: 'Default Group' - required: False - aliases: [] - ip_address: - description: - - The IP Address for the server. One is assigned if not provided. - default: None - required: False - aliases: [] - location: - description: - - The Datacenter to create servers in. - default: None - required: False - aliases: [] - managed_os: - description: - - Whether to create the server as 'Managed' or not. - default: False - required: False - choices: [True, False] - aliases: [] - memory: - description: - - Memory in GB. - default: 1 - required: False - aliases: [] - name: - description: - - A 1 to 6 character identifier to use for the server. - default: None - required: False - aliases: [] - network_id: - description: - - The network UUID on which to create servers. - default: None - required: False - aliases: [] - packages: - description: - - Blueprints to run on the server after its created. - default: [] - required: False - aliases: [] - password: - description: - - Password for the administrator user - default: None - required: False - aliases: [] - primary_dns: - description: - - Primary DNS used by the server. - default: None - required: False - aliases: [] - public_ip_protocol: - description: - - The protocol to use for the public ip if add_public_ip is set to True. - default: 'TCP' - required: False - aliases: [] - public_ip_ports: - description: - - A list of ports to allow on the firewall to thes servers public ip, if add_public_ip is set to True. - default: [] - required: False - aliases: [] - secondary_dns: - description: - - Secondary DNS used by the server. - default: None - required: False - aliases: [] - server_ids: - description: - - Required for started, stopped, and absent states. A list of server Ids to insure are started, stopped, or absent. - default: [] - required: False - aliases: [] - source_server_password: - description: - - The password for the source server if a clone is specified. - default: None - required: False - aliases: [] - state: - description: - - The state to insure that the provided resources are in. - default: 'present' - required: False - choices: ['present', 'absent', 'started', 'stopped'] - aliases: [] - storage_type: - description: - - The type of storage to attach to the server. - default: 'standard' - required: False - choices: ['standard', 'hyperscale'] - aliases: [] - template: - description: - - The template to use for server creation. Will search for a template if a partial string is provided. - default: None - required: false - aliases: [] - ttl: - description: - - The time to live for the server in seconds. The server will be deleted when this time expires. - default: None - required: False - aliases: [] - type: - description: - - The type of server to create. - default: 'standard' - required: False - choices: ['standard', 'hyperscale'] - aliases: [] - wait: - description: - - Whether to wait for the provisioning tasks to finish before returning. - default: True - required: False - choices: [ True, False] - aliases: [] -''' - -EXAMPLES = ''' -# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples - -- name: Provision a single Ubuntu Server - clc_server: - name: test - template: ubuntu-14-64 - count: 1 - group: 'Default Group' - state: present - -- name: Ensure 'Default Group' has exactly 5 servers - clc_server: - name: test - template: ubuntu-14-64 - exact_count: 5 - count_group: 'Default Group' - group: 'Default Group' - -- name: Stop a Server - clc_server: - server_ids: ['UC1ACCTTEST01'] - state: stopped - -- name: Start a Server - clc_server: - server_ids: ['UC1ACCTTEST01'] - state: started - -- name: Delete a Server - clc_server: - server_ids: ['UC1ACCTTEST01'] - state: absent -''' - -__version__ = '${version}' - -import requests -from time import sleep - -# -# Requires the clc-python-sdk. -# sudo pip install clc-sdk -# -try: - import clc as clc_sdk - from clc import CLCException - from clc import APIFailedResponse -except ImportError: - CLC_FOUND = False - clc_sdk = None -else: - CLC_FOUND = True - - -class ClcServer(): - clc = clc_sdk - - def __init__(self, module): - """ - Construct module - """ - self.clc = clc_sdk - self.module = module - self.group_dict = {} - - if not CLC_FOUND: - self.module.fail_json( - msg='clc-python-sdk required for this module') - - self._set_user_agent(self.clc) - - def process_request(self): - """ - Process the request - Main Code Path - :return: Returns with either an exit_json or fail_json - """ - self._set_clc_credentials_from_env() - - self.module.params = ClcServer._validate_module_params(self.clc, - self.module) - p = self.module.params - state = p.get('state') - - # - # Handle each state - # - - if state == 'absent': - server_ids = p['server_ids'] - if not isinstance(server_ids, list): - self.module.fail_json( - msg='server_ids needs to be a list of instances to delete: %s' % - server_ids) - - (changed, - server_dict_array, - new_server_ids) = ClcServer._delete_servers(module=self.module, - clc=self.clc, - server_ids=server_ids) - - elif state in ('started', 'stopped'): - server_ids = p.get('server_ids') - if not isinstance(server_ids, list): - self.module.fail_json( - msg='server_ids needs to be a list of servers to run: %s' % - server_ids) - - (changed, - server_dict_array, - new_server_ids) = ClcServer._startstop_servers(self.module, - self.clc, - server_ids) - - elif state == 'present': - # Changed is always set to true when provisioning new instances - if not p.get('template'): - self.module.fail_json( - msg='template parameter is required for new instance') - - if p.get('exact_count') is None: - (server_dict_array, - new_server_ids, - changed) = ClcServer._create_servers(self.module, - self.clc) - else: - (server_dict_array, - new_server_ids, - changed) = ClcServer._enforce_count(self.module, - self.clc) - - self.module.exit_json( - changed=changed, - server_ids=new_server_ids, - servers=server_dict_array) - - @staticmethod - def _define_module_argument_spec(): - """ - Define the argument spec for the ansible module - :return: argument spec dictionary - """ - argument_spec = dict(name=dict(), - template=dict(), - group=dict(default='Default Group'), - network_id=dict(), - location=dict(default=None), - cpu=dict(default=1), - memory=dict(default='1'), - alias=dict(default=None), - password=dict(default=None), - ip_address=dict(default=None), - storage_type=dict(default='standard'), - type=dict( - default='standard', - choices=[ - 'standard', - 'hyperscale']), - primary_dns=dict(default=None), - secondary_dns=dict(default=None), - additional_disks=dict(type='list', default=[]), - custom_fields=dict(type='list', default=[]), - ttl=dict(default=None), - managed_os=dict(type='bool', default=False), - description=dict(default=None), - source_server_password=dict(default=None), - cpu_autoscale_policy_id=dict(default=None), - anti_affinity_policy_id=dict(default=None), - anti_affinity_policy_name=dict(default=None), - alert_policy_id=dict(default=None), - alert_policy_name=dict(default=None), - packages=dict(type='list', default=[]), - state=dict( - default='present', - choices=[ - 'present', - 'absent', - 'started', - 'stopped']), - count=dict(type='int', default='1'), - exact_count=dict(type='int', default=None), - count_group=dict(), - server_ids=dict(type='list'), - add_public_ip=dict(type='bool', default=False), - public_ip_protocol=dict(default='TCP'), - public_ip_ports=dict(type='list'), - wait=dict(type='bool', default=True)) - - mutually_exclusive = [ - ['exact_count', 'count'], - ['exact_count', 'state'], - ['anti_affinity_policy_id', 'anti_affinity_policy_name'], - ['alert_policy_id', 'alert_policy_name'], - ] - return {"argument_spec": argument_spec, - "mutually_exclusive": mutually_exclusive} - - def _set_clc_credentials_from_env(self): - """ - Set the CLC Credentials on the sdk by reading environment variables - :return: none - """ - env = os.environ - v2_api_token = env.get('CLC_V2_API_TOKEN', False) - v2_api_username = env.get('CLC_V2_API_USERNAME', False) - v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) - clc_alias = env.get('CLC_ACCT_ALIAS', False) - api_url = env.get('CLC_V2_API_URL', False) - - if api_url: - self.clc.defaults.ENDPOINT_URL_V2 = api_url - - if v2_api_token and clc_alias: - self.clc._LOGIN_TOKEN_V2 = v2_api_token - self.clc._V2_ENABLED = True - self.clc.ALIAS = clc_alias - elif v2_api_username and v2_api_passwd: - self.clc.v2.SetCredentials( - api_username=v2_api_username, - api_passwd=v2_api_passwd) - else: - return self.module.fail_json( - msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " - "environment variables") - - @staticmethod - def _validate_module_params(clc, module): - """ - Validate the module params, and lookup default values. - :param clc: clc-sdk instance to use - :param module: module to validate - :return: dictionary of validated params - """ - params = module.params - datacenter = ClcServer._find_datacenter(clc, module) - - ClcServer._validate_types(module) - ClcServer._validate_name(module) - - params['alias'] = ClcServer._find_alias(clc, module) - params['cpu'] = ClcServer._find_cpu(clc, module) - params['memory'] = ClcServer._find_memory(clc, module) - params['description'] = ClcServer._find_description(module) - params['ttl'] = ClcServer._find_ttl(clc, module) - params['template'] = ClcServer._find_template_id(module, datacenter) - params['group'] = ClcServer._find_group(module, datacenter).id - params['network_id'] = ClcServer._find_network_id(module, datacenter) - - return params - - @staticmethod - def _find_datacenter(clc, module): - """ - Find the datacenter by calling the CLC API. - :param clc: clc-sdk instance to use - :param module: module to validate - :return: clc-sdk.Datacenter instance - """ - location = module.params.get('location') - try: - datacenter = clc.v2.Datacenter(location) - return datacenter - except CLCException: - module.fail_json(msg=str("Unable to find location: " + location)) - - @staticmethod - def _find_alias(clc, module): - """ - Find or Validate the Account Alias by calling the CLC API - :param clc: clc-sdk instance to use - :param module: module to validate - :return: clc-sdk.Account instance - """ - alias = module.params.get('alias') - if not alias: - alias = clc.v2.Account.GetAlias() - return alias - - @staticmethod - def _find_cpu(clc, module): - """ - Find or validate the CPU value by calling the CLC API - :param clc: clc-sdk instance to use - :param module: module to validate - :return: Int value for CPU - """ - cpu = module.params.get('cpu') - group_id = module.params.get('group_id') - alias = module.params.get('alias') - state = module.params.get('state') - - if not cpu and state == 'present': - group = clc.v2.Group(id=group_id, - alias=alias) - if group.Defaults("cpu"): - cpu = group.Defaults("cpu") - else: - module.fail_json( - msg=str("Cannot determine a default cpu value. Please provide a value for cpu.")) - return cpu - - @staticmethod - def _find_memory(clc, module): - """ - Find or validate the Memory value by calling the CLC API - :param clc: clc-sdk instance to use - :param module: module to validate - :return: Int value for Memory - """ - memory = module.params.get('memory') - group_id = module.params.get('group_id') - alias = module.params.get('alias') - state = module.params.get('state') - - if not memory and state == 'present': - group = clc.v2.Group(id=group_id, - alias=alias) - if group.Defaults("memory"): - memory = group.Defaults("memory") - else: - module.fail_json(msg=str( - "Cannot determine a default memory value. Please provide a value for memory.")) - return memory - - @staticmethod - def _find_description(module): - """ - Set the description module param to name if description is blank - :param module: the module to validate - :return: string description - """ - description = module.params.get('description') - if not description: - description = module.params.get('name') - return description - - @staticmethod - def _validate_types(module): - """ - Validate that type and storage_type are set appropriately, and fail if not - :param module: the module to validate - :return: none - """ - state = module.params.get('state') - type = module.params.get( - 'type').lower() if module.params.get('type') else None - storage_type = module.params.get( - 'storage_type').lower() if module.params.get('storage_type') else None - - if state == "present": - if type == "standard" and storage_type not in ( - "standard", "premium"): - module.fail_json( - msg=str("Standard VMs must have storage_type = 'standard' or 'premium'")) - - if type == "hyperscale" and storage_type != "hyperscale": - module.fail_json( - msg=str("Hyperscale VMs must have storage_type = 'hyperscale'")) - - @staticmethod - def _find_ttl(clc, module): - """ - Validate that TTL is > 3600 if set, and fail if not - :param clc: clc-sdk instance to use - :param module: module to validate - :return: validated ttl - """ - ttl = module.params.get('ttl') - - if ttl: - if ttl <= 3600: - module.fail_json(msg=str("Ttl cannot be <= 3600")) - else: - ttl = clc.v2.time_utils.SecondsToZuluTS(int(time.time()) + ttl) - return ttl - - @staticmethod - def _find_template_id(module, datacenter): - """ - Find the template id by calling the CLC API. - :param module: the module to validate - :param datacenter: the datacenter to search for the template - :return: a valid clc template id - """ - lookup_template = module.params.get('template') - state = module.params.get('state') - result = None - - if state == 'present': - try: - result = datacenter.Templates().Search(lookup_template)[0].id - except CLCException: - module.fail_json( - msg=str( - "Unable to find a template: " + - lookup_template + - " in location: " + - datacenter.id)) - return result - - @staticmethod - def _find_network_id(module, datacenter): - """ - Validate the provided network id or return a default. - :param module: the module to validate - :param datacenter: the datacenter to search for a network id - :return: a valid network id - """ - network_id = module.params.get('network_id') - - if not network_id: - try: - network_id = datacenter.Networks().networks[0].id - except CLCException: - module.fail_json( - msg=str( - "Unable to find a network in location: " + - datacenter.id)) - - return network_id - - @staticmethod - def _create_servers(module, clc, override_count=None): - """ - Create New Servers - :param module: the AnsibleModule object - :param clc: the clc-sdk instance to use - :return: a list of dictionaries with server information about the servers that were created - """ - p = module.params - requests = [] - servers = [] - server_dict_array = [] - created_server_ids = [] - - add_public_ip = p.get('add_public_ip') - public_ip_protocol = p.get('public_ip_protocol') - public_ip_ports = p.get('public_ip_ports') - wait = p.get('wait') - - params = { - 'name': p.get('name'), - 'template': p.get('template'), - 'group_id': p.get('group'), - 'network_id': p.get('network_id'), - 'cpu': p.get('cpu'), - 'memory': p.get('memory'), - 'alias': p.get('alias'), - 'password': p.get('password'), - 'ip_address': p.get('ip_address'), - 'storage_type': p.get('storage_type'), - 'type': p.get('type'), - 'primary_dns': p.get('primary_dns'), - 'secondary_dns': p.get('secondary_dns'), - 'additional_disks': p.get('additional_disks'), - 'custom_fields': p.get('custom_fields'), - 'ttl': p.get('ttl'), - 'managed_os': p.get('managed_os'), - 'description': p.get('description'), - 'source_server_password': p.get('source_server_password'), - 'cpu_autoscale_policy_id': p.get('cpu_autoscale_policy_id'), - 'anti_affinity_policy_id': p.get('anti_affinity_policy_id'), - 'anti_affinity_policy_name': p.get('anti_affinity_policy_name'), - 'packages': p.get('packages') - } - - count = override_count if override_count else p.get('count') - - changed = False if count == 0 else True - - if changed: - for i in range(0, count): - if not module.check_mode: - req = ClcServer._create_clc_server(clc=clc, - module=module, - server_params=params) - server = req.requests[0].Server() - requests.append(req) - servers.append(server) - - ClcServer._wait_for_requests(clc, requests, servers, wait) - - ClcServer._add_public_ip_to_servers( - should_add_public_ip=add_public_ip, - servers=servers, - public_ip_protocol=public_ip_protocol, - public_ip_ports=public_ip_ports, - wait=wait) - ClcServer._add_alert_policy_to_servers(clc=clc, - module=module, - servers=servers) - - for server in servers: - # reload server details - server = clc.v2.Server(server.id) - - server.data['ipaddress'] = server.details[ - 'ipAddresses'][0]['internal'] - - if add_public_ip and len(server.PublicIPs().public_ips) > 0: - server.data['publicip'] = str( - server.PublicIPs().public_ips[0]) - - server_dict_array.append(server.data) - created_server_ids.append(server.id) - - return server_dict_array, created_server_ids, changed - - @staticmethod - def _validate_name(module): - """ - Validate that name is the correct length if provided, fail if it's not - :param module: the module to validate - :return: none - """ - name = module.params.get('name') - state = module.params.get('state') - - if state == 'present' and (len(name) < 1 or len(name) > 6): - module.fail_json(msg=str( - "When state = 'present', name must be a string with a minimum length of 1 and a maximum length of 6")) - -# -# Functions to execute the module's behaviors -# (called from main()) -# - - @staticmethod - def _enforce_count(module, clc): - """ - Enforce that there is the right number of servers in the provided group. - Starts or stops servers as necessary. - :param module: the AnsibleModule object - :param clc: the clc-sdk instance to use - :return: a list of dictionaries with server information about the servers that were created or deleted - """ - p = module.params - changed_server_ids = None - changed = False - count_group = p.get('count_group') - datacenter = ClcServer._find_datacenter(clc, module) - exact_count = p.get('exact_count') - server_dict_array = [] - - # fail here if the exact count was specified without filtering - # on a group, as this may lead to a undesired removal of instances - if exact_count and count_group is None: - module.fail_json( - msg="you must use the 'count_group' option with exact_count") - - servers, running_servers = ClcServer._find_running_servers_by_group( - module, datacenter, count_group) - - if len(running_servers) == exact_count: - changed = False - - elif len(running_servers) < exact_count: - changed = True - to_create = exact_count - len(running_servers) - server_dict_array, changed_server_ids, changed \ - = ClcServer._create_servers(module, clc, override_count=to_create) - - for server in server_dict_array: - running_servers.append(server) - - elif len(running_servers) > exact_count: - changed = True - to_remove = len(running_servers) - exact_count - all_server_ids = sorted([x.id for x in running_servers]) - remove_ids = all_server_ids[0:to_remove] - - (changed, server_dict_array, changed_server_ids) \ - = ClcServer._delete_servers(module, clc, remove_ids) - - return server_dict_array, changed_server_ids, changed - - @staticmethod - def _wait_for_requests(clc, requests, servers, wait): - """ - Block until server provisioning requests are completed. - :param clc: the clc-sdk instance to use - :param requests: a list of clc-sdk.Request instances - :param servers: a list of servers to refresh - :param wait: a boolean on whether to block or not. This function is skipped if True - :return: none - """ - if wait: - # Requests.WaitUntilComplete() returns the count of failed requests - failed_requests_count = sum( - [request.WaitUntilComplete() for request in requests]) - - if failed_requests_count > 0: - raise clc - else: - ClcServer._refresh_servers(servers) - - @staticmethod - def _refresh_servers(servers): - """ - Loop through a list of servers and refresh them - :param servers: list of clc-sdk.Server instances to refresh - :return: none - """ - for server in servers: - server.Refresh() - - @staticmethod - def _add_public_ip_to_servers( - should_add_public_ip, - servers, - public_ip_protocol, - public_ip_ports, - wait): - """ - Create a public IP for servers - :param should_add_public_ip: boolean - whether or not to provision a public ip for servers. Skipped if False - :param servers: List of servers to add public ips to - :param public_ip_protocol: a protocol to allow for the public ips - :param public_ip_ports: list of ports to allow for the public ips - :param wait: boolean - whether to block until the provisioning requests complete - :return: none - """ - - if should_add_public_ip: - ports_lst = [] - requests = [] - - for port in public_ip_ports: - ports_lst.append( - {'protocol': public_ip_protocol, 'port': port}) - - for server in servers: - requests.append(server.PublicIPs().Add(ports_lst)) - - if wait: - for r in requests: - r.WaitUntilComplete() - - @staticmethod - def _add_alert_policy_to_servers(clc, module, servers): - """ - Associate an alert policy to servers - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param servers: List of servers to add alert policy to - :return: none - """ - p = module.params - alert_policy_id = p.get('alert_policy_id') - alert_policy_name = p.get('alert_policy_name') - alias = p.get('alias') - if not alert_policy_id and alert_policy_name: - alert_policy_id = ClcServer._get_alert_policy_id_by_name( - clc=clc, - module=module, - alias=alias, - alert_policy_name=alert_policy_name - ) - if not alert_policy_id: - module.fail_json( - msg='No alert policy exist with name : %s' - % (alert_policy_name)) - for server in servers: - ClcServer._add_alert_policy_to_server( - clc=clc, - module=module, - alias=alias, - server_id=server.id, - alert_policy_id=alert_policy_id) - - @staticmethod - def _add_alert_policy_to_server(clc, module, alias, server_id, alert_policy_id): - """ - Associate an alert policy to a clc server - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param alias: the clc account alias - :param serverid: The clc server id - :param alert_policy_id: the alert policy id to be associated to the server - :return: none - """ - try: - clc.v2.API.Call( - method='POST', - url='servers/%s/%s/alertPolicies' % (alias, server_id), - payload=json.dumps( - { - 'id': alert_policy_id - })) - except clc.APIFailedResponse as e: - return module.fail_json( - msg='Failed to associate alert policy to the server : %s with Error %s' - % (server_id, str(e.response_text))) - - @staticmethod - def _get_alert_policy_id_by_name(clc, module, alias, alert_policy_name): - """ - Returns the alert policy id for the given alert policy name - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param alias: the clc account alias - :param alert_policy_name: the name of the alert policy - :return: the alert policy id - """ - alert_policy_id = None - policies = clc.v2.API.Call('GET', '/v2/alertPolicies/%s' % (alias)) - if not policies: - return alert_policy_id - for policy in policies.get('items'): - if policy.get('name') == alert_policy_name: - if not alert_policy_id: - alert_policy_id = policy.get('id') - else: - return module.fail_json( - msg='mutiple alert policies were found with policy name : %s' % - (alert_policy_name)) - return alert_policy_id - - - @staticmethod - def _delete_servers(module, clc, server_ids): - """ - Delete the servers on the provided list - :param module: the AnsibleModule object - :param clc: the clc-sdk instance to use - :param server_ids: list of servers to delete - :return: a list of dictionaries with server information about the servers that were deleted - """ - # Whether to wait for termination to complete before returning - p = module.params - wait = p.get('wait') - terminated_server_ids = [] - server_dict_array = [] - requests = [] - - changed = False - if not isinstance(server_ids, list) or len(server_ids) < 1: - module.fail_json( - msg='server_ids should be a list of servers, aborting') - - servers = clc.v2.Servers(server_ids).Servers() - changed = True - - for server in servers: - if not module.check_mode: - requests.append(server.Delete()) - - if wait: - for r in requests: - r.WaitUntilComplete() - - for server in servers: - terminated_server_ids.append(server.id) - - return changed, server_dict_array, terminated_server_ids - - @staticmethod - def _startstop_servers(module, clc, server_ids): - """ - Start or Stop the servers on the provided list - :param module: the AnsibleModule object - :param clc: the clc-sdk instance to use - :param server_ids: list of servers to start or stop - :return: a list of dictionaries with server information about the servers that were started or stopped - """ - p = module.params - wait = p.get('wait') - state = p.get('state') - changed = False - changed_servers = [] - server_dict_array = [] - result_server_ids = [] - requests = [] - - if not isinstance(server_ids, list) or len(server_ids) < 1: - module.fail_json( - msg='server_ids should be a list of servers, aborting') - - servers = clc.v2.Servers(server_ids).Servers() - for server in servers: - if server.powerState != state: - changed_servers.append(server) - if not module.check_mode: - requests.append( - ClcServer._change_server_power_state( - module, - server, - state)) - changed = True - - if wait: - for r in requests: - r.WaitUntilComplete() - for server in changed_servers: - server.Refresh() - - for server in changed_servers: - server_dict_array.append(server.data) - result_server_ids.append(server.id) - - return changed, server_dict_array, result_server_ids - - @staticmethod - def _change_server_power_state(module, server, state): - """ - Change the server powerState - :param module: the module to check for intended state - :param server: the server to start or stop - :param state: the intended powerState for the server - :return: the request object from clc-sdk call - """ - result = None - try: - if state == 'started': - result = server.PowerOn() - else: - result = server.PowerOff() - except: - module.fail_json( - msg='Unable to change state for server {0}'.format( - server.id)) - return result - return result - - @staticmethod - def _find_running_servers_by_group(module, datacenter, count_group): - """ - Find a list of running servers in the provided group - :param module: the AnsibleModule object - :param datacenter: the clc-sdk.Datacenter instance to use to lookup the group - :param count_group: the group to count the servers - :return: list of servers, and list of running servers - """ - group = ClcServer._find_group( - module=module, - datacenter=datacenter, - lookup_group=count_group) - - servers = group.Servers().Servers() - running_servers = [] - - for server in servers: - if server.status == 'active' and server.powerState == 'started': - running_servers.append(server) - - return servers, running_servers - - @staticmethod - def _find_group(module, datacenter, lookup_group=None): - """ - Find a server group in a datacenter by calling the CLC API - :param module: the AnsibleModule instance - :param datacenter: clc-sdk.Datacenter instance to search for the group - :param lookup_group: string name of the group to search for - :return: clc-sdk.Group instance - """ - result = None - if not lookup_group: - lookup_group = module.params.get('group') - try: - return datacenter.Groups().Get(lookup_group) - except: - pass - - # The search above only acts on the main - result = ClcServer._find_group_recursive( - module, - datacenter.Groups(), - lookup_group) - - if result is None: - module.fail_json( - msg=str( - "Unable to find group: " + - lookup_group + - " in location: " + - datacenter.id)) - - return result - - @staticmethod - def _find_group_recursive(module, group_list, lookup_group): - """ - Find a server group by recursively walking the tree - :param module: the AnsibleModule instance to use - :param group_list: a list of groups to search - :param lookup_group: the group to look for - :return: list of groups - """ - result = None - for group in group_list.groups: - subgroups = group.Subgroups() - try: - return subgroups.Get(lookup_group) - except: - result = ClcServer._find_group_recursive( - module, - subgroups, - lookup_group) - - if result is not None: - break - - return result - - @staticmethod - def _create_clc_server( - clc, - module, - server_params): - """ - Call the CLC Rest API to Create a Server - :param clc: the clc-python-sdk instance to use - :param server_params: a dictionary of params to use to create the servers - :return: clc-sdk.Request object linked to the queued server request - """ - - aa_policy_id = server_params.get('anti_affinity_policy_id') - aa_policy_name = server_params.get('anti_affinity_policy_name') - if not aa_policy_id and aa_policy_name: - aa_policy_id = ClcServer._get_anti_affinity_policy_id( - clc, - module, - server_params.get('alias'), - aa_policy_name) - - res = clc.v2.API.Call( - method='POST', - url='servers/%s' % - (server_params.get('alias')), - payload=json.dumps( - { - 'name': server_params.get('name'), - 'description': server_params.get('description'), - 'groupId': server_params.get('group_id'), - 'sourceServerId': server_params.get('template'), - 'isManagedOS': server_params.get('managed_os'), - 'primaryDNS': server_params.get('primary_dns'), - 'secondaryDNS': server_params.get('secondary_dns'), - 'networkId': server_params.get('network_id'), - 'ipAddress': server_params.get('ip_address'), - 'password': server_params.get('password'), - 'sourceServerPassword': server_params.get('source_server_password'), - 'cpu': server_params.get('cpu'), - 'cpuAutoscalePolicyId': server_params.get('cpu_autoscale_policy_id'), - 'memoryGB': server_params.get('memory'), - 'type': server_params.get('type'), - 'storageType': server_params.get('storage_type'), - 'antiAffinityPolicyId': aa_policy_id, - 'customFields': server_params.get('custom_fields'), - 'additionalDisks': server_params.get('additional_disks'), - 'ttl': server_params.get('ttl'), - 'packages': server_params.get('packages')})) - - result = clc.v2.Requests(res) - - # - # Patch the Request object so that it returns a valid server - - # Find the server's UUID from the API response - server_uuid = [obj['id'] - for obj in res['links'] if obj['rel'] == 'self'][0] - - # Change the request server method to a _find_server_by_uuid closure so - # that it will work - result.requests[0].Server = lambda: ClcServer._find_server_by_uuid_w_retry( - clc, - module, - server_uuid, - server_params.get('alias')) - - return result - - @staticmethod - def _get_anti_affinity_policy_id(clc, module, alias, aa_policy_name): - """ - retrieves the anti affinity policy id of the server based on the name of the policy - :param clc: the clc-sdk instance to use - :param module: the AnsibleModule object - :param alias: the CLC account alias - :param aa_policy_name: the anti affinity policy name - :return: aa_policy_id: The anti affinity policy id - """ - aa_policy_id = None - aa_policies = clc.v2.API.Call(method='GET', - url='antiAffinityPolicies/%s' % (alias)) - for aa_policy in aa_policies.get('items'): - if aa_policy.get('name') == aa_policy_name: - if not aa_policy_id: - aa_policy_id = aa_policy.get('id') - else: - return module.fail_json( - msg='mutiple anti affinity policies were found with policy name : %s' % - (aa_policy_name)) - if not aa_policy_id: - return module.fail_json( - msg='No anti affinity policy was found with policy name : %s' % - (aa_policy_name)) - return aa_policy_id - - # - # This is the function that gets patched to the Request.server object using a lamda closure - # - - @staticmethod - def _find_server_by_uuid_w_retry( - clc, module, svr_uuid, alias=None, retries=5, backout=2): - """ - Find the clc server by the UUID returned from the provisioning request. Retry the request if a 404 is returned. - :param clc: the clc-sdk instance to use - :param svr_uuid: UUID of the server - :param alias: the Account Alias to search - :return: a clc-sdk.Server instance - """ - if not alias: - alias = clc.v2.Account.GetAlias() - - # Wait and retry if the api returns a 404 - while True: - retries -= 1 - try: - server_obj = clc.v2.API.Call( - method='GET', url='servers/%s/%s?uuid=true' % - (alias, svr_uuid)) - server_id = server_obj['id'] - server = clc.v2.Server( - id=server_id, - alias=alias, - server_obj=server_obj) - return server - - except APIFailedResponse as e: - if e.response_status_code != 404: - module.fail_json( - msg='A failure response was received from CLC API when ' - 'attempting to get details for a server: UUID=%s, Code=%i, Message=%s' % - (svr_uuid, e.response_status_code, e.message)) - return - if retries == 0: - module.fail_json( - msg='Unable to reach the CLC API after 5 attempts') - return - - sleep(backout) - backout = backout * 2 - - @staticmethod - def _set_user_agent(clc): - if hasattr(clc, 'SetRequestsSession'): - agent_string = "ClcAnsibleModule/" + __version__ - ses = requests.Session() - ses.headers.update({"Api-Client": agent_string}) - ses.headers['User-Agent'] += " " + agent_string - clc.SetRequestsSession(ses) - - -def main(): - """ - The main function. Instantiates the module and calls process_request. - :return: none - """ - argument_dict = ClcServer._define_module_argument_spec() - module = AnsibleModule(supports_check_mode=True, **argument_dict) - clc_server = ClcServer(module) - clc_server.process_request() - -from ansible.module_utils.basic import * # pylint: disable=W0614 -if __name__ == '__main__': - main() diff --git a/cloud/centurylink/clc_server_snapshot.py b/cloud/centurylink/clc_server_snapshot.py deleted file mode 100644 index 9ca1474f248..00000000000 --- a/cloud/centurylink/clc_server_snapshot.py +++ /dev/null @@ -1,341 +0,0 @@ -#!/usr/bin/python - -# CenturyLink Cloud Ansible Modules. -# -# These Ansible modules enable the CenturyLink Cloud v2 API to be called -# from an within Ansible Playbook. -# -# This file is part of CenturyLink Cloud, and is maintained -# by the Workflow as a Service Team -# -# Copyright 2015 CenturyLink Cloud -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# CenturyLink Cloud: http://www.CenturyLinkCloud.com -# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ -# - -DOCUMENTATION = ''' -module: clc_server -short_desciption: Create, Delete and Restore server snapshots in CenturyLink Cloud. -description: - - An Ansible module to Create, Delete and Restore server snapshots in CenturyLink Cloud. -options: - server_ids: - description: - - A list of server Ids to snapshot. - default: [] - required: True - aliases: [] - expiration_days: - description: - - The number of days to keep the server snapshot before it expires. - default: 7 - required: False - aliases: [] - state: - description: - - The state to insure that the provided resources are in. - default: 'present' - required: False - choices: ['present', 'absent', 'restore'] - aliases: [] - wait: - description: - - Whether to wait for the provisioning tasks to finish before returning. - default: True - required: False - choices: [ True, False] - aliases: [] -''' - -EXAMPLES = ''' -# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples - -- name: Create server snapshot - clc_server_snapshot: - server_ids: - - UC1WFSDTEST01 - - UC1WFSDTEST02 - expiration_days: 10 - wait: True - state: present - -- name: Restore server snapshot - clc_server_snapshot: - server_ids: - - UC1WFSDTEST01 - - UC1WFSDTEST02 - wait: True - state: restore - -- name: Delete server snapshot - clc_server_snapshot: - server_ids: - - UC1WFSDTEST01 - - UC1WFSDTEST02 - wait: True - state: absent -''' - -__version__ = '${version}' - -import requests - -# -# Requires the clc-python-sdk. -# sudo pip install clc-sdk -# -try: - import clc as clc_sdk - from clc import CLCException -except ImportError: - clc_found = False - clc_sdk = None -else: - CLC_FOUND = True - - -class ClcSnapshot(): - - clc = clc_sdk - module = None - - def __init__(self, module): - """ - Construct module - """ - self.module = module - if not CLC_FOUND: - self.module.fail_json( - msg='clc-python-sdk required for this module') - - self._set_user_agent(self.clc) - - def process_request(self): - """ - Process the request - Main Code Path - :return: Returns with either an exit_json or fail_json - """ - p = self.module.params - - if not CLC_FOUND: - self.module.fail_json( - msg='clc-python-sdk required for this module') - - server_ids = p['server_ids'] - expiration_days = p['expiration_days'] - state = p['state'] - - if not server_ids: - return self.module.fail_json(msg='List of Server ids are required') - - self._set_clc_credentials_from_env() - if state == 'present': - changed, requests, changed_servers = self.ensure_server_snapshot_present(server_ids=server_ids, - expiration_days=expiration_days) - elif state == 'absent': - changed, requests, changed_servers = self.ensure_server_snapshot_absent( - server_ids=server_ids) - elif state == 'restore': - changed, requests, changed_servers = self.ensure_server_snapshot_restore( - server_ids=server_ids) - else: - return self.module.fail_json(msg="Unknown State: " + state) - - self._wait_for_requests_to_complete(requests) - return self.module.exit_json( - changed=changed, - server_ids=changed_servers) - - def ensure_server_snapshot_present(self, server_ids, expiration_days): - """ - Ensures the given set of server_ids have the snapshots created - :param server_ids: The list of server_ids to create the snapshot - :param expiration_days: The number of days to keep the snapshot - :return: (changed, result, changed_servers) - changed: A flag indicating whether any change was made - result: the list of clc request objects from CLC API call - changed_servers: The list of servers ids that are modified - """ - result = [] - changed = False - servers = self._get_servers_from_clc( - server_ids, - 'Failed to obtain server list from the CLC API') - servers_to_change = [ - server for server in servers if len( - server.GetSnapshots()) == 0] - for server in servers_to_change: - changed = True - if not self.module.check_mode: - res = server.CreateSnapshot( - delete_existing=True, - expiration_days=expiration_days) - result.append(res) - changed_servers = [ - server.id for server in servers_to_change if server.id] - return changed, result, changed_servers - - def ensure_server_snapshot_absent(self, server_ids): - """ - Ensures the given set of server_ids have the snapshots removed - :param server_ids: The list of server_ids to delete the snapshot - :return: (changed, result, changed_servers) - changed: A flag indicating whether any change was made - result: the list of clc request objects from CLC API call - changed_servers: The list of servers ids that are modified - """ - result = [] - changed = False - servers = self._get_servers_from_clc( - server_ids, - 'Failed to obtain server list from the CLC API') - servers_to_change = [ - server for server in servers if len( - server.GetSnapshots()) > 0] - for server in servers_to_change: - changed = True - if not self.module.check_mode: - res = server.DeleteSnapshot() - result.append(res) - changed_servers = [ - server.id for server in servers_to_change if server.id] - return changed, result, changed_servers - - def ensure_server_snapshot_restore(self, server_ids): - """ - Ensures the given set of server_ids have the snapshots restored - :param server_ids: The list of server_ids to delete the snapshot - :return: (changed, result, changed_servers) - changed: A flag indicating whether any change was made - result: the list of clc request objects from CLC API call - changed_servers: The list of servers ids that are modified - """ - result = [] - changed = False - servers = self._get_servers_from_clc( - server_ids, - 'Failed to obtain server list from the CLC API') - servers_to_change = [ - server for server in servers if len( - server.GetSnapshots()) > 0] - for server in servers_to_change: - changed = True - if not self.module.check_mode: - res = server.RestoreSnapshot() - result.append(res) - changed_servers = [ - server.id for server in servers_to_change if server.id] - return changed, result, changed_servers - - def _wait_for_requests_to_complete(self, requests_lst): - """ - Waits until the CLC requests are complete if the wait argument is True - :param requests_lst: The list of CLC request objects - :return: none - """ - if not self.module.params['wait']: - return - for request in requests_lst: - request.WaitUntilComplete() - for request_details in request.requests: - if request_details.Status() != 'succeeded': - self.module.fail_json( - msg='Unable to process server snapshot request') - - @staticmethod - def define_argument_spec(): - """ - This function defnines the dictionary object required for - package module - :return: the package dictionary object - """ - argument_spec = dict( - server_ids=dict(type='list', required=True), - expiration_days=dict(default=7), - wait=dict(default=True), - state=dict( - default='present', - choices=[ - 'present', - 'absent', - 'restore']), - ) - return argument_spec - - def _get_servers_from_clc(self, server_list, message): - """ - Internal function to fetch list of CLC server objects from a list of server ids - :param the list server ids - :return the list of CLC server objects - """ - try: - return self.clc.v2.Servers(server_list).servers - except CLCException as ex: - return self.module.fail_json(msg=message + ': %s' % ex) - - def _set_clc_credentials_from_env(self): - """ - Set the CLC Credentials on the sdk by reading environment variables - :return: none - """ - env = os.environ - v2_api_token = env.get('CLC_V2_API_TOKEN', False) - v2_api_username = env.get('CLC_V2_API_USERNAME', False) - v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) - clc_alias = env.get('CLC_ACCT_ALIAS', False) - api_url = env.get('CLC_V2_API_URL', False) - - if api_url: - self.clc.defaults.ENDPOINT_URL_V2 = api_url - - if v2_api_token and clc_alias: - self.clc._LOGIN_TOKEN_V2 = v2_api_token - self.clc._V2_ENABLED = True - self.clc.ALIAS = clc_alias - elif v2_api_username and v2_api_passwd: - self.clc.v2.SetCredentials( - api_username=v2_api_username, - api_passwd=v2_api_passwd) - else: - return self.module.fail_json( - msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " - "environment variables") - - @staticmethod - def _set_user_agent(clc): - if hasattr(clc, 'SetRequestsSession'): - agent_string = "ClcAnsibleModule/" + __version__ - ses = requests.Session() - ses.headers.update({"Api-Client": agent_string}) - ses.headers['User-Agent'] += " " + agent_string - clc.SetRequestsSession(ses) - - -def main(): - """ - Main function - :return: None - """ - module = AnsibleModule( - argument_spec=ClcSnapshot.define_argument_spec(), - supports_check_mode=True - ) - clc_snapshot = ClcSnapshot(module) - clc_snapshot.process_request() - -from ansible.module_utils.basic import * -if __name__ == '__main__': - main() From fa7a5f4233384d844587464571042f778ebc8462 Mon Sep 17 00:00:00 2001 From: Siva Popuri Date: Fri, 17 Jul 2015 13:24:31 -0500 Subject: [PATCH 0439/2522] Added clc ansible module for managing groups in centurylink cloud --- cloud/centurylink/clc_group.py | 415 +++++++++++++++++++++++++++++++++ 1 file changed, 415 insertions(+) create mode 100644 cloud/centurylink/clc_group.py diff --git a/cloud/centurylink/clc_group.py b/cloud/centurylink/clc_group.py new file mode 100644 index 00000000000..2dd9683081e --- /dev/null +++ b/cloud/centurylink/clc_group.py @@ -0,0 +1,415 @@ +#!/usr/bin/python + +# CenturyLink Cloud Ansible Modules. +# +# These Ansible modules enable the CenturyLink Cloud v2 API to be called +# from an within Ansible Playbook. +# +# This file is part of CenturyLink Cloud, and is maintained +# by the Workflow as a Service Team +# +# Copyright 2015 CenturyLink Cloud +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# CenturyLink Cloud: http://www.CenturyLinkCloud.com +# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ +# + +DOCUMENTATION = ''' +module: clc_group +short_desciption: Create/delete Server Groups at Centurylink Cloud +description: + - Create or delete Server Groups at Centurylink Centurylink Cloud +version_added: 1.0 +options: + name: + description: + - The name of the Server Group + required: True + description: + description: + - A description of the Server Group + required: False + parent: + description: + - The parent group of the server group. If parent is not provided, it creates the group at top level. + required: False + location: + description: + - Datacenter to create the group in. If location is not provided, the group gets created in the default datacenter + associated with the account + required: False + state: + description: + - Whether to create or delete the group + default: present + choices: ['present', 'absent'] + wait: + description: + - Whether to wait for the tasks to finish before returning. + choices: [ True, False ] + default: True + required: False +requirements: + - python = 2.7 + - requests >= 2.5.0 + - clc-sdk +notes: + - To use this module, it is required to set the below environment variables which enables access to the + Centurylink Cloud + - CLC_V2_API_USERNAME: the account login id for the centurylink cloud + - CLC_V2_API_PASSWORD: the account passwod for the centurylink cloud + - Alternatively, the module accepts the API token and account alias. The API token can be generated using the + CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login + - CLC_V2_API_TOKEN: the API token generated from https://api.ctl.io/v2/authentication/login + - CLC_ACCT_ALIAS: the account alias associated with the centurylink cloud + - Users can set CLC_V2_API_URL to specify an endpoint for pointing to a different CLC environment. +''' + +EXAMPLES = ''' + +# Create a Server Group + +--- +- name: Create Server Group + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Create / Verify a Server Group at CenturyLink Cloud + clc_group: + name: 'My Cool Server Group' + parent: 'Default Group' + state: present + register: clc + + - name: debug + debug: var=clc + +# Delete a Server Group + +--- +- name: Delete Server Group + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Delete / Verify Absent a Server Group at CenturyLink Cloud + clc_group: + name: 'My Cool Server Group' + parent: 'Default Group' + state: absent + register: clc + + - name: debug + debug: var=clc + +''' + +__version__ = '${version}' + +from distutils.version import LooseVersion + +try: + import requests +except ImportError: + REQUESTS_FOUND = False +else: + REQUESTS_FOUND = True + +# +# Requires the clc-python-sdk. +# sudo pip install clc-sdk +# +try: + import clc as clc_sdk + from clc import CLCException +except ImportError: + CLC_FOUND = False + clc_sdk = None +else: + CLC_FOUND = True + + +class ClcGroup(object): + + clc = None + root_group = None + + def __init__(self, module): + """ + Construct module + """ + self.clc = clc_sdk + self.module = module + self.group_dict = {} + + if not CLC_FOUND: + self.module.fail_json( + msg='clc-python-sdk required for this module') + if not REQUESTS_FOUND: + self.module.fail_json( + msg='requests library is required for this module') + if requests.__version__ and LooseVersion(requests.__version__) < LooseVersion('2.5.0'): + self.module.fail_json( + msg='requests library version should be >= 2.5.0') + + self._set_user_agent(self.clc) + + def process_request(self): + """ + Execute the main code path, and handle the request + :return: none + """ + location = self.module.params.get('location') + group_name = self.module.params.get('name') + parent_name = self.module.params.get('parent') + group_description = self.module.params.get('description') + state = self.module.params.get('state') + + self._set_clc_credentials_from_env() + self.group_dict = self._get_group_tree_for_datacenter( + datacenter=location) + + if state == "absent": + changed, group, requests = self._ensure_group_is_absent( + group_name=group_name, parent_name=parent_name) + + else: + changed, group, requests = self._ensure_group_is_present( + group_name=group_name, parent_name=parent_name, group_description=group_description) + if requests: + self._wait_for_requests_to_complete(requests) + self.module.exit_json(changed=changed, group=group_name) + + @staticmethod + def _define_module_argument_spec(): + """ + Define the argument spec for the ansible module + :return: argument spec dictionary + """ + argument_spec = dict( + name=dict(required=True), + description=dict(default=None), + parent=dict(default=None), + location=dict(default=None), + state=dict(default='present', choices=['present', 'absent']), + wait=dict(type='bool', default=True)) + + return argument_spec + + def _set_clc_credentials_from_env(self): + """ + Set the CLC Credentials on the sdk by reading environment variables + :return: none + """ + env = os.environ + v2_api_token = env.get('CLC_V2_API_TOKEN', False) + v2_api_username = env.get('CLC_V2_API_USERNAME', False) + v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) + clc_alias = env.get('CLC_ACCT_ALIAS', False) + api_url = env.get('CLC_V2_API_URL', False) + + if api_url: + self.clc.defaults.ENDPOINT_URL_V2 = api_url + + if v2_api_token and clc_alias: + self.clc._LOGIN_TOKEN_V2 = v2_api_token + self.clc._V2_ENABLED = True + self.clc.ALIAS = clc_alias + elif v2_api_username and v2_api_passwd: + self.clc.v2.SetCredentials( + api_username=v2_api_username, + api_passwd=v2_api_passwd) + else: + return self.module.fail_json( + msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " + "environment variables") + + def _ensure_group_is_absent(self, group_name, parent_name): + """ + Ensure that group_name is absent by deleting it if necessary + :param group_name: string - the name of the clc server group to delete + :param parent_name: string - the name of the parent group for group_name + :return: changed, group + """ + changed = False + requests = [] + + if self._group_exists(group_name=group_name, parent_name=parent_name): + if not self.module.check_mode: + request = self._delete_group(group_name) + requests.append(request) + changed = True + return changed, group_name, requests + + def _delete_group(self, group_name): + """ + Delete the provided server group + :param group_name: string - the server group to delete + :return: none + """ + response = None + group, parent = self.group_dict.get(group_name) + try: + response = group.Delete() + except CLCException, ex: + self.module.fail_json(msg='Failed to delete group :{0}. {1}'.format( + group_name, ex.response_text + )) + return response + + def _ensure_group_is_present( + self, + group_name, + parent_name, + group_description): + """ + Checks to see if a server group exists, creates it if it doesn't. + :param group_name: the name of the group to validate/create + :param parent_name: the name of the parent group for group_name + :param group_description: a short description of the server group (used when creating) + :return: (changed, group) - + changed: Boolean- whether a change was made, + group: A clc group object for the group + """ + assert self.root_group, "Implementation Error: Root Group not set" + parent = parent_name if parent_name is not None else self.root_group.name + description = group_description + changed = False + + parent_exists = self._group_exists(group_name=parent, parent_name=None) + child_exists = self._group_exists( + group_name=group_name, + parent_name=parent) + + if parent_exists and child_exists: + group, parent = self.group_dict[group_name] + changed = False + elif parent_exists and not child_exists: + if not self.module.check_mode: + self._create_group( + group=group_name, + parent=parent, + description=description) + changed = True + else: + self.module.fail_json( + msg="parent group: " + + parent + + " does not exist") + + return changed, group_name, None + + def _create_group(self, group, parent, description): + """ + Create the provided server group + :param group: clc_sdk.Group - the group to create + :param parent: clc_sdk.Parent - the parent group for {group} + :param description: string - a text description of the group + :return: clc_sdk.Group - the created group + """ + response = None + (parent, grandparent) = self.group_dict[parent] + try: + response = parent.Create(name=group, description=description) + except CLCException, ex: + self.module.fail_json(msg='Failed to create group :{0}. {1}'.format( + group, ex.response_text + )) + return response + + def _group_exists(self, group_name, parent_name): + """ + Check to see if a group exists + :param group_name: string - the group to check + :param parent_name: string - the parent of group_name + :return: boolean - whether the group exists + """ + result = False + if group_name in self.group_dict: + (group, parent) = self.group_dict[group_name] + if parent_name is None or parent_name == parent.name: + result = True + return result + + def _get_group_tree_for_datacenter(self, datacenter=None): + """ + Walk the tree of groups for a datacenter + :param datacenter: string - the datacenter to walk (ex: 'UC1') + :return: a dictionary of groups and parents + """ + self.root_group = self.clc.v2.Datacenter( + location=datacenter).RootGroup() + return self._walk_groups_recursive( + parent_group=None, + child_group=self.root_group) + + def _walk_groups_recursive(self, parent_group, child_group): + """ + Walk a parent-child tree of groups, starting with the provided child group + :param parent_group: clc_sdk.Group - the parent group to start the walk + :param child_group: clc_sdk.Group - the child group to start the walk + :return: a dictionary of groups and parents + """ + result = {str(child_group): (child_group, parent_group)} + groups = child_group.Subgroups().groups + if len(groups) > 0: + for group in groups: + if group.type != 'default': + continue + + result.update(self._walk_groups_recursive(child_group, group)) + return result + + def _wait_for_requests_to_complete(self, requests_lst): + """ + Waits until the CLC requests are complete if the wait argument is True + :param requests_lst: The list of CLC request objects + :return: none + """ + if not self.module.params['wait']: + return + for request in requests_lst: + request.WaitUntilComplete() + for request_details in request.requests: + if request_details.Status() != 'succeeded': + self.module.fail_json( + msg='Unable to process group request') + + @staticmethod + def _set_user_agent(clc): + if hasattr(clc, 'SetRequestsSession'): + agent_string = "ClcAnsibleModule/" + __version__ + ses = requests.Session() + ses.headers.update({"Api-Client": agent_string}) + ses.headers['User-Agent'] += " " + agent_string + clc.SetRequestsSession(ses) + + +def main(): + """ + The main function. Instantiates the module and calls process_request. + :return: none + """ + module = AnsibleModule( + argument_spec=ClcGroup._define_module_argument_spec(), + supports_check_mode=True) + + clc_group = ClcGroup(module) + clc_group.process_request() + +from ansible.module_utils.basic import * # pylint: disable=W0614 +if __name__ == '__main__': + main() From ef1199a03857529c60ccf82e269838f7469e917a Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 17 Jul 2015 15:54:11 -0600 Subject: [PATCH 0440/2522] Add 'production' option to bower module. --- packaging/language/bower.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packaging/language/bower.py b/packaging/language/bower.py index 7af8136a445..e14ee0e0669 100644 --- a/packaging/language/bower.py +++ b/packaging/language/bower.py @@ -37,6 +37,12 @@ required: false default: no choices: [ "yes", "no" ] + production: + description: + - Install with --production flag + required: false + default: no + choices: [ "yes", "no" ] path: description: - The base path where to install the bower packages @@ -76,6 +82,7 @@ def __init__(self, module, **kwargs): self.module = module self.name = kwargs['name'] self.offline = kwargs['offline'] + self.production = kwargs['production'] self.path = kwargs['path'] self.version = kwargs['version'] @@ -94,6 +101,9 @@ def _exec(self, args, run_in_check_mode=False, check_rc=True): if self.offline: cmd.append('--offline') + if self.production: + cmd.append('--production') + # If path is specified, cd into that path and run the command. cwd = None if self.path: @@ -144,6 +154,7 @@ def main(): arg_spec = dict( name=dict(default=None), offline=dict(default='no', type='bool'), + production=dict(default='no', type='bool'), path=dict(required=True), state=dict(default='present', choices=['present', 'absent', 'latest', ]), version=dict(default=None), @@ -154,6 +165,7 @@ def main(): name = module.params['name'] offline = module.params['offline'] + production = module.params['production'] path = os.path.expanduser(module.params['path']) state = module.params['state'] version = module.params['version'] @@ -161,7 +173,7 @@ def main(): if state == 'absent' and not name: module.fail_json(msg='uninstalling a package is only available for named packages') - bower = Bower(module, name=name, offline=offline, path=path, version=version) + bower = Bower(module, name=name, offline=offline, production=production, path=path, version=version) changed = False if state == 'present': From b598161f4646678f419d4afd3a651019d7e4c895 Mon Sep 17 00:00:00 2001 From: Phil Date: Fri, 17 Jul 2015 22:14:18 -0500 Subject: [PATCH 0441/2522] minor doc fixes that had lingering description of deprecated functions --- windows/win_unzip.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/windows/win_unzip.py b/windows/win_unzip.py index 799ab1bda31..aa0180baf74 100644 --- a/windows/win_unzip.py +++ b/windows/win_unzip.py @@ -25,9 +25,9 @@ --- module: win_unzip version_added: "2.0" -short_description: Unzips compressed files on the Windows node +short_description: Unzips compressed files and archives on the Windows node description: - - Unzips compressed files, and can force reboot (if needed, i.e. such as hotfixes). Has ability to recursively unzip files within the src zip file provided using Read-Archive and piping to Expand-Archive (Using PSCX). If the destination directory does not exist, it will be created before unzipping the file. If a .zip file is specified as src and recurse is true then PSCX will be installed. Specifying rm parameter will allow removal of the src file after extraction. + - Unzips compressed files and archives. For extracting any compression types other than .zip, the PowerShellCommunityExtensions (PSCX) Module is required. This module (in conjunction with PSCX) has the ability to recursively unzip files within the src zip file provided and also functionality for many other compression types. If the destination directory does not exist, it will be created before unzipping the file. Specifying rm parameter will force removal of the src file after extraction. options: src: description: From ebb91255670c4e02aadc3defcea6a09fd85b8726 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Sat, 18 Jul 2015 13:54:30 +0200 Subject: [PATCH 0442/2522] virt: remove BabyJSON Removed the usage of baby json. This is in response to the fact that the baby json functionality was removed in Ansible 1.8 Ref: #430 --- cloud/misc/virt.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cloud/misc/virt.py b/cloud/misc/virt.py index 80b8e2558eb..b59c7ed3de3 100644 --- a/cloud/misc/virt.py +++ b/cloud/misc/virt.py @@ -93,8 +93,9 @@ try: import libvirt except ImportError: - print "failed=True msg='libvirt python module unavailable'" - sys.exit(1) + HAS_VIRT = False +else: + HAS_VIRT = True ALL_COMMANDS = [] VM_COMMANDS = ['create','status', 'start', 'stop', 'pause', 'unpause', @@ -481,6 +482,11 @@ def main(): xml = dict(), )) + if not HAS_VIRT: + module.fail_json( + msg='The `libvirt` module is not importable. Check the requirements.' + ) + rc = VIRT_SUCCESS try: rc, result = core(module) From d812db9f7f8e5d0988ca4ab347772a4004242e40 Mon Sep 17 00:00:00 2001 From: Kevin Brebanov Date: Sat, 18 Jul 2015 11:37:29 -0400 Subject: [PATCH 0443/2522] Adding apk module --- packaging/os/apk.py | 196 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 packaging/os/apk.py diff --git a/packaging/os/apk.py b/packaging/os/apk.py new file mode 100644 index 00000000000..53672ca4489 --- /dev/null +++ b/packaging/os/apk.py @@ -0,0 +1,196 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Kevin Brebanov +# Based on pacman (Afterburn , Aaron Bull Schaefer ) +# and apt (Matthew Williams >) modules. +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + +DOCUMENTATION = ''' +--- +module: apk +short_description: Manages apk packages +description: + - Manages I(apk) packages for Alpine Linux. +options: + name: + description: + - A package name, like C(foo). + required: false + default: null + state: + description: + - Indicates the desired package state. + - C(present) ensures the package is present. + - C(absent) ensures the package is absent. + - C(latest) ensures the package is present and the latest version. + required: false + default: present + choices: [ "present", "absent", "latest" ] + update_cache: + description: + - Update repository indexes. Can be run with other steps or on it's own. + required: false + default: no + choices: [ "yes", "no" ] + upgrade: + description: + - Upgrade all installed packages to their latest version. + required: false + default: no + choices: [ "yes", "no" ] +''' + +EXAMPLES = ''' +# Update repositories and install "foo" package +- apk: name=foo update_cache=yes + +# Remove "foo" package +- apk: name=foo state=absent + +# Install the package "foo" +- apk: name=foo state=present + +# Update repositories and update package "foo" to latest version +- apk: name=foo state=latest update_cache=yes + +# Update all installed packages to the latest versions +- apk: upgrade=yes + +# Update repositories as a separate step +- apk: update_cache=yes +''' + +import os +import re + +APK_PATH="/sbin/apk" + +def update_package_db(module): + cmd = "apk update" + rc, stdout, stderr = module.run_command(cmd, check_rc=False) + if rc == 0: + return True + else: + module.fail_json(msg="could not update package db") + +def query_package(module, name): + cmd = "apk -v info --installed %s" % (name) + rc, stdout, stderr = module.run_command(cmd, check_rc=False) + if rc == 0: + return True + else: + return False + +def query_latest(module, name): + cmd = "apk version %s" % (name) + rc, stdout, stderr = module.run_command(cmd, check_rc=False) + search_pattern = "(%s)-[\d\.\w]+-[\d\w]+\s+(.)\s+[\d\.\w]+-[\d\w]+\s+" % (name) + match = re.search(search_pattern, stdout) + if match and match.group(2) == "<": + return False + return True + +def upgrade_packages(module): + if module.check_mode: + cmd = "apk upgrade --simulate" + else: + cmd = "apk upgrade" + rc, stdout, stderr = module.run_command(cmd, check_rc=False) + if rc != 0: + module.fail_json(msg="failed to upgrade packages") + if re.search('^OK', stdout): + module.exit_json(changed=False, msg="packages already upgraded") + module.exit_json(changed=True, msg="upgraded packages") + +def install_package(module, name, state): + upgrade = False + installed = query_package(module, name) + latest = query_latest(module, name) + if state == 'latest' and not latest: + upgrade = True + if installed and not upgrade: + module.exit_json(changed=False, msg="package already installed") + if upgrade: + if module.check_mode: + cmd = "apk add --upgrade --simulate %s" % (name) + else: + cmd = "apk add --upgrade %s" % (name) + else: + if module.check_mode: + cmd = "apk add --simulate %s" % (name) + else: + cmd = "apk add %s" % (name) + rc, stdout, stderr = module.run_command(cmd, check_rc=False) + if rc != 0: + module.fail_json(msg="failed to install %s" % (name)) + module.exit_json(changed=True, msg="installed %s package" % (name)) + +def remove_package(module, name): + installed = query_package(module, name) + if not installed: + module.exit_json(changed=False, msg="package already removed") + if module.check_mode: + cmd = "apk del --purge --simulate %s" % (name) + else: + cmd = "apk del --purge %s" % (name) + rc, stdout, stderr = module.run_command(cmd, check_rc=False) + if rc != 0: + module.fail_json(msg="failed to remove %s" % (name)) + module.exit_json(changed=True, msg="removed %s package" % (name)) + +# ========================================== +# Main control flow. + +def main(): + module = AnsibleModule( + argument_spec = dict( + state = dict(default='present', choices=['present', 'installed', 'absent', 'removed', 'latest']), + name = dict(type='str'), + update_cache = dict(default='no', choices=BOOLEANS, type='bool'), + upgrade = dict(default='no', choices=BOOLEANS, type='bool'), + ), + required_one_of = [['name', 'update_cache', 'upgrade']], + supports_check_mode = True + ) + + if not os.path.exists(APK_PATH): + module.fail_json(msg="cannot find apk, looking for %s" % (APK_PATH)) + + p = module.params + + # normalize the state parameter + if p['state'] in ['present', 'installed']: + p['state'] = 'present' + if p['state'] in ['absent', 'removed']: + p['state'] = 'absent' + + if p['update_cache']: + update_package_db(module) + if not p['name']: + module.exit_json(changed=True, msg='updated repository indexes') + + if p['upgrade']: + upgrade_packages(module) + + if p['state'] in ['present', 'latest']: + install_package(module, p['name'], p['state']) + elif p['state'] == 'absent': + remove_package(module, p['name']) + +# Import module snippets. +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From c791282c95a3c5905456450ddba7f83f427ab0d0 Mon Sep 17 00:00:00 2001 From: Rob White Date: Sun, 19 Jul 2015 11:10:48 +1000 Subject: [PATCH 0444/2522] Removed requirement for target_bucket --- cloud/amazon/s3_logging.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/amazon/s3_logging.py b/cloud/amazon/s3_logging.py index 313518510c9..fc199c500dd 100644 --- a/cloud/amazon/s3_logging.py +++ b/cloud/amazon/s3_logging.py @@ -40,8 +40,8 @@ choices: [ 'present', 'absent' ] target_bucket: description: - - "The bucket to log to." - required: true + - "The bucket to log to. Required when state=present." + required: false default: null target_prefix: description: @@ -142,7 +142,7 @@ def main(): argument_spec.update( dict( name = dict(required=True, default=None), - target_bucket = dict(required=True, default=None), + target_bucket = dict(required=False, default=None), target_prefix = dict(required=False, default=""), state = dict(required=False, default='present', choices=['present', 'absent']) ) From 9fb2eae7ddba6be8aa0d71f4ee3d983ef177c741 Mon Sep 17 00:00:00 2001 From: Rob White Date: Sun, 19 Jul 2015 13:43:04 +1000 Subject: [PATCH 0445/2522] Doc fixup --- cloud/amazon/s3_logging.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cloud/amazon/s3_logging.py b/cloud/amazon/s3_logging.py index fc199c500dd..75b3fe73508 100644 --- a/cloud/amazon/s3_logging.py +++ b/cloud/amazon/s3_logging.py @@ -26,7 +26,6 @@ description: - "Name of the s3 bucket." required: true - default: null region: description: - "AWS region to create the bucket in. If not set then the value of the AWS_REGION and EC2_REGION environment variables are checked, followed by the aws_region and ec2_region settings in the Boto config file. If none of those are set the region defaults to the S3 Location: US Standard." @@ -47,7 +46,7 @@ description: - "The prefix that should be prepended to the generated log files written to the target_bucket." required: false - default: no + default: "" extends_documentation_fragment: aws ''' @@ -141,7 +140,7 @@ def main(): argument_spec = ec2_argument_spec() argument_spec.update( dict( - name = dict(required=True, default=None), + name = dict(required=True), target_bucket = dict(required=False, default=None), target_prefix = dict(required=False, default=""), state = dict(required=False, default='present', choices=['present', 'absent']) From 6c9410dce9fecff12cbb48001f6db166e47d0599 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sun, 19 Jul 2015 14:57:08 +0200 Subject: [PATCH 0446/2522] cloudstack: cs_portforward: fix public_end_port was used for private_end_port --- cloud/cloudstack/cs_portforward.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/cloudstack/cs_portforward.py b/cloud/cloudstack/cs_portforward.py index df95bfd3ea6..960a8607065 100644 --- a/cloud/cloudstack/cs_portforward.py +++ b/cloud/cloudstack/cs_portforward.py @@ -261,7 +261,7 @@ def get_portforwarding_rule(self): public_port = self.module.params.get('public_port') public_end_port = self.get_public_end_port() private_port = self.module.params.get('private_port') - private_end_port = self.get_public_end_port() + private_end_port = self.get_private_end_port() args = {} args['ipaddressid'] = self.get_ip_address(key='id') From 8e6e9c782bc4425404d3f0dcce8ea19219f03ed4 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sun, 19 Jul 2015 15:09:49 +0200 Subject: [PATCH 0447/2522] cloudstack: use get_or_fallback() from cloudstack utils --- cloud/cloudstack/cs_firewall.py | 10 ++-------- cloud/cloudstack/cs_instance.py | 9 +-------- cloud/cloudstack/cs_network.py | 9 +-------- cloud/cloudstack/cs_portforward.py | 24 ++++++----------------- cloud/cloudstack/cs_project.py | 11 ++--------- cloud/cloudstack/cs_securitygroup_rule.py | 10 ++-------- 6 files changed, 14 insertions(+), 59 deletions(-) diff --git a/cloud/cloudstack/cs_firewall.py b/cloud/cloudstack/cs_firewall.py index 97cf97e781e..27350eab91b 100644 --- a/cloud/cloudstack/cs_firewall.py +++ b/cloud/cloudstack/cs_firewall.py @@ -216,18 +216,12 @@ def __init__(self, module): self.firewall_rule = None - def get_end_port(self): - if self.module.params.get('end_port'): - return self.module.params.get('end_port') - return self.module.params.get('start_port') - - def get_firewall_rule(self): if not self.firewall_rule: cidr = self.module.params.get('cidr') protocol = self.module.params.get('protocol') start_port = self.module.params.get('start_port') - end_port = self.get_end_port() + end_port = self.get_or_fallback('end_port', 'start_port') icmp_code = self.module.params.get('icmp_code') icmp_type = self.module.params.get('icmp_type') fw_type = self.module.params.get('type') @@ -328,7 +322,7 @@ def create_firewall_rule(self): args['cidrlist'] = self.module.params.get('cidr') args['protocol'] = self.module.params.get('protocol') args['startport'] = self.module.params.get('start_port') - args['endport'] = self.get_end_port() + args['endport'] = self.get_or_fallback('end_port', 'start_port') args['icmptype'] = self.module.params.get('icmp_type') args['icmpcode'] = self.module.params.get('icmp_code') diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 7c2c117604d..d8412879691 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -500,13 +500,6 @@ def get_user_data(self): return user_data - def get_display_name(self): - display_name = self.module.params.get('display_name') - if not display_name: - display_name = self.module.params.get('name') - return display_name - - def deploy_instance(self): self.result['changed'] = True @@ -555,7 +548,7 @@ def update_instance(self, instance): args_instance_update = {} args_instance_update['id'] = instance['id'] args_instance_update['group'] = self.module.params.get('group') - args_instance_update['displayname'] = self.get_display_name() + args_instance_update['displayname'] = self.get_or_fallback('display_name', 'name') args_instance_update['userdata'] = self.get_user_data() args_instance_update['ostypeid'] = self.get_os_type(key='id') diff --git a/cloud/cloudstack/cs_network.py b/cloud/cloudstack/cs_network.py index 50dd2981e72..c4fd51b7a0b 100644 --- a/cloud/cloudstack/cs_network.py +++ b/cloud/cloudstack/cs_network.py @@ -335,13 +335,6 @@ def __init__(self, module): self.network = None - def get_or_fallback(self, key=None, fallback_key=None): - value = self.module.params.get(key) - if not value: - value = self.module.params.get(fallback_key) - return value - - def get_vpc(self, key=None): vpc = self.module.params.get('vpc') if not vpc: @@ -380,7 +373,7 @@ def get_network_offering(self, key=None): def _get_args(self): args = {} args['name'] = self.module.params.get('name') - args['displaytext'] = self.get_or_fallback('displaytext','name') + args['displaytext'] = self.get_or_fallback('displaytext', 'name') args['networkdomain'] = self.module.params.get('network_domain') args['networkofferingid'] = self.get_network_offering(key='id') return args diff --git a/cloud/cloudstack/cs_portforward.py b/cloud/cloudstack/cs_portforward.py index 960a8607065..d1b8db4d65a 100644 --- a/cloud/cloudstack/cs_portforward.py +++ b/cloud/cloudstack/cs_portforward.py @@ -217,18 +217,6 @@ def __init__(self, module): self.vm_default_nic = None - def get_public_end_port(self): - if not self.module.params.get('public_end_port'): - return self.module.params.get('public_port') - return self.module.params.get('public_end_port') - - - def get_private_end_port(self): - if not self.module.params.get('private_end_port'): - return self.module.params.get('private_port') - return self.module.params.get('private_end_port') - - def get_vm_guest_ip(self): vm_guest_ip = self.module.params.get('vm_guest_ip') default_nic = self.get_vm_default_nic() @@ -259,9 +247,9 @@ def get_portforwarding_rule(self): if not self.portforwarding_rule: protocol = self.module.params.get('protocol') public_port = self.module.params.get('public_port') - public_end_port = self.get_public_end_port() + public_end_port = self.get_or_fallback('public_end_port', 'public_port') private_port = self.module.params.get('private_port') - private_end_port = self.get_private_end_port() + private_end_port = self.get_or_fallback('private_end_port', 'private_port') args = {} args['ipaddressid'] = self.get_ip_address(key='id') @@ -290,9 +278,9 @@ def create_portforwarding_rule(self): args = {} args['protocol'] = self.module.params.get('protocol') args['publicport'] = self.module.params.get('public_port') - args['publicendport'] = self.get_public_end_port() + args['publicendport'] = self.get_or_fallback('public_end_port', 'public_port') args['privateport'] = self.module.params.get('private_port') - args['privateendport'] = self.get_private_end_port() + args['privateendport'] = self.get_or_fallback('private_end_port', 'private_port') args['openfirewall'] = self.module.params.get('open_firewall') args['vmguestip'] = self.get_vm_guest_ip() args['ipaddressid'] = self.get_ip_address(key='id') @@ -312,9 +300,9 @@ def update_portforwarding_rule(self, portforwarding_rule): args = {} args['protocol'] = self.module.params.get('protocol') args['publicport'] = self.module.params.get('public_port') - args['publicendport'] = self.get_public_end_port() + args['publicendport'] = self.get_or_fallback('public_end_port', 'public_port') args['privateport'] = self.module.params.get('private_port') - args['privateendport'] = self.get_private_end_port() + args['privateendport'] = self.get_or_fallback('private_end_port', 'private_port') args['openfirewall'] = self.module.params.get('open_firewall') args['vmguestip'] = self.get_vm_guest_ip() args['ipaddressid'] = self.get_ip_address(key='id') diff --git a/cloud/cloudstack/cs_project.py b/cloud/cloudstack/cs_project.py index f09c42f5899..896232f3053 100644 --- a/cloud/cloudstack/cs_project.py +++ b/cloud/cloudstack/cs_project.py @@ -148,13 +148,6 @@ def __init__(self, module): self.project = None - def get_displaytext(self): - displaytext = self.module.params.get('displaytext') - if not displaytext: - displaytext = self.module.params.get('name') - return displaytext - - def get_project(self): if not self.project: project = self.module.params.get('name') @@ -184,7 +177,7 @@ def present_project(self): def update_project(self, project): args = {} args['id'] = project['id'] - args['displaytext'] = self.get_displaytext() + args['displaytext'] = self.get_or_fallback('displaytext', 'name') if self._has_changed(args, project): self.result['changed'] = True @@ -205,7 +198,7 @@ def create_project(self, project): args = {} args['name'] = self.module.params.get('name') - args['displaytext'] = self.get_displaytext() + args['displaytext'] = self.get_or_fallback('displaytext', 'name') args['account'] = self.get_account('name') args['domainid'] = self.get_domain('id') diff --git a/cloud/cloudstack/cs_securitygroup_rule.py b/cloud/cloudstack/cs_securitygroup_rule.py index 0780e12d70d..65bd7fd5640 100644 --- a/cloud/cloudstack/cs_securitygroup_rule.py +++ b/cloud/cloudstack/cs_securitygroup_rule.py @@ -222,18 +222,12 @@ def _type_cidr_match(self, rule, cidr): and cidr == rule['cidr'] - def get_end_port(self): - if self.module.params.get('end_port'): - return self.module.params.get('end_port') - return self.module.params.get('start_port') - - def _get_rule(self, rules): user_security_group_name = self.module.params.get('user_security_group') cidr = self.module.params.get('cidr') protocol = self.module.params.get('protocol') start_port = self.module.params.get('start_port') - end_port = self.get_end_port() + end_port = self.get_or_fallback('end_port', 'start_port') icmp_code = self.module.params.get('icmp_code') icmp_type = self.module.params.get('icmp_type') @@ -291,7 +285,7 @@ def add_rule(self): args['protocol'] = self.module.params.get('protocol') args['startport'] = self.module.params.get('start_port') - args['endport'] = self.get_end_port() + args['endport'] = self.get_or_fallback('end_port', 'start_port') args['icmptype'] = self.module.params.get('icmp_type') args['icmpcode'] = self.module.params.get('icmp_code') args['projectid'] = self.get_project('id') From 7d6738ab9dcf1d85f5a0ae95d304e431b64c0797 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sun, 19 Jul 2015 15:10:36 +0200 Subject: [PATCH 0448/2522] cloudstack: cs_instance: fix display_name not used in deployment --- cloud/cloudstack/cs_instance.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index d8412879691..f8bef7c89e2 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -517,6 +517,7 @@ def deploy_instance(self): args['ipaddress'] = self.module.params.get('ip_address') args['ip6address'] = self.module.params.get('ip6_address') args['name'] = self.module.params.get('name') + args['displayname'] = self.get_or_fallback('display_name', 'name') args['group'] = self.module.params.get('group') args['keypair'] = self.module.params.get('ssh_key') args['size'] = self.module.params.get('disk_size') From c4c65b6c9164abe73f615e95425e46da20aeb7ab Mon Sep 17 00:00:00 2001 From: Kevin Brebanov Date: Sun, 19 Jul 2015 13:47:17 -0400 Subject: [PATCH 0449/2522] Allow multiple packages to removed at the same time --- packaging/os/apk.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/packaging/os/apk.py b/packaging/os/apk.py index 53672ca4489..858d4ad7450 100644 --- a/packaging/os/apk.py +++ b/packaging/os/apk.py @@ -60,6 +60,9 @@ # Remove "foo" package - apk: name=foo state=absent +# Remove "foo" and "bar" packages +- apk: name=foo,bar state=absent + # Install the package "foo" - apk: name=foo state=present @@ -138,19 +141,23 @@ def install_package(module, name, state): module.fail_json(msg="failed to install %s" % (name)) module.exit_json(changed=True, msg="installed %s package" % (name)) -def remove_package(module, name): - installed = query_package(module, name) +def remove_packages(module, names): + installed = [] + for name in names: + if query_package(module, name): + installed.append(name) if not installed: - module.exit_json(changed=False, msg="package already removed") + module.exit_json(changed=False, msg="package(s) already removed") + names = " ".join(installed) if module.check_mode: - cmd = "apk del --purge --simulate %s" % (name) + cmd = "apk del --purge --simulate %s" % (names) else: - cmd = "apk del --purge %s" % (name) + cmd = "apk del --purge %s" % (names) rc, stdout, stderr = module.run_command(cmd, check_rc=False) if rc != 0: - module.fail_json(msg="failed to remove %s" % (name)) - module.exit_json(changed=True, msg="removed %s package" % (name)) - + module.fail_json(msg="failed to remove %s package(s)" % (names)) + module.exit_json(changed=True, msg="removed %s package(s)" % (names)) + # ========================================== # Main control flow. @@ -185,10 +192,14 @@ def main(): if p['upgrade']: upgrade_packages(module) + # Create a list of package names + # Removing empty strings that may have been created by a trailing ',' + names = filter((lambda x: x != ''), p['name'].split(',')) + if p['state'] in ['present', 'latest']: install_package(module, p['name'], p['state']) elif p['state'] == 'absent': - remove_package(module, p['name']) + remove_packages(module, names) # Import module snippets. from ansible.module_utils.basic import * From dd2d35c888f28f9b9f6d7b5a16b95be6dbef4c7d Mon Sep 17 00:00:00 2001 From: Kevin Brebanov Date: Sun, 19 Jul 2015 14:33:35 -0400 Subject: [PATCH 0450/2522] Allow multiple packages to be installed at the same time --- packaging/os/apk.py | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/packaging/os/apk.py b/packaging/os/apk.py index 858d4ad7450..55d79f3686d 100644 --- a/packaging/os/apk.py +++ b/packaging/os/apk.py @@ -57,6 +57,9 @@ # Update repositories and install "foo" package - apk: name=foo update_cache=yes +# Update repositories and install "foo" and "bar" packages +- apk: name=foo,bar update_cache=yes + # Remove "foo" package - apk: name=foo state=absent @@ -66,9 +69,15 @@ # Install the package "foo" - apk: name=foo state=present +# Install the packages "foo" and "bar" +- apk: name=foo,bar state=present + # Update repositories and update package "foo" to latest version - apk: name=foo state=latest update_cache=yes +# Update repositories and update packages "foo" and "bar" to latest versions +- apk: name=foo,bar state=latest update_cache=yes + # Update all installed packages to the latest versions - apk: upgrade=yes @@ -118,28 +127,31 @@ def upgrade_packages(module): module.exit_json(changed=False, msg="packages already upgraded") module.exit_json(changed=True, msg="upgraded packages") -def install_package(module, name, state): +def install_packages(module, names, state): upgrade = False - installed = query_package(module, name) - latest = query_latest(module, name) - if state == 'latest' and not latest: - upgrade = True - if installed and not upgrade: - module.exit_json(changed=False, msg="package already installed") + uninstalled = [] + for name in names: + if not query_package(module, name): + uninstalled.append(name) + elif state == 'latest' and not query_latest(module, name): + upgrade = True + if not uninstalled and not upgrade: + module.exit_json(changed=False, msg="package(s) already installed") + names = " ".join(uninstalled) if upgrade: if module.check_mode: - cmd = "apk add --upgrade --simulate %s" % (name) + cmd = "apk add --upgrade --simulate %s" % (names) else: - cmd = "apk add --upgrade %s" % (name) + cmd = "apk add --upgrade %s" % (names) else: if module.check_mode: - cmd = "apk add --simulate %s" % (name) + cmd = "apk add --simulate %s" % (names) else: - cmd = "apk add %s" % (name) + cmd = "apk add %s" % (names) rc, stdout, stderr = module.run_command(cmd, check_rc=False) if rc != 0: - module.fail_json(msg="failed to install %s" % (name)) - module.exit_json(changed=True, msg="installed %s package" % (name)) + module.fail_json(msg="failed to install %s" % (names)) + module.exit_json(changed=True, msg="installed %s package(s)" % (names)) def remove_packages(module, names): installed = [] @@ -197,7 +209,7 @@ def main(): names = filter((lambda x: x != ''), p['name'].split(',')) if p['state'] in ['present', 'latest']: - install_package(module, p['name'], p['state']) + install_packages(module, names, p['state']) elif p['state'] == 'absent': remove_packages(module, names) From 91e3d2afd550466b821554f534472d213542aaa4 Mon Sep 17 00:00:00 2001 From: Kevin Brebanov Date: Sun, 19 Jul 2015 14:36:16 -0400 Subject: [PATCH 0451/2522] Update documentation --- packaging/os/apk.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packaging/os/apk.py b/packaging/os/apk.py index 55d79f3686d..4b78a898901 100644 --- a/packaging/os/apk.py +++ b/packaging/os/apk.py @@ -27,15 +27,15 @@ options: name: description: - - A package name, like C(foo). + - A package name, like C(foo), or mutliple packages, like C(foo, bar). required: false default: null state: description: - - Indicates the desired package state. - - C(present) ensures the package is present. - - C(absent) ensures the package is absent. - - C(latest) ensures the package is present and the latest version. + - Indicates the desired package(s) state. + - C(present) ensures the package(s) is/are present. + - C(absent) ensures the package(s) is/are absent. + - C(latest) ensures the package(s) is/are present and the latest version(s). required: false default: present choices: [ "present", "absent", "latest" ] From b781b8bda18e3b73a6c498b4e664265fc3766d9d Mon Sep 17 00:00:00 2001 From: Jeff Geerling Date: Sun, 19 Jul 2015 17:35:32 -0500 Subject: [PATCH 0452/2522] Make HAProxy module documentation a little easier to read. --- network/haproxy.py | 77 +++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/network/haproxy.py b/network/haproxy.py index 6d4f6a4279a..cada704e342 100644 --- a/network/haproxy.py +++ b/network/haproxy.py @@ -22,75 +22,74 @@ --- module: haproxy version_added: "1.9" -short_description: An Ansible module to handle states enable/disable server and set weight to backend host in haproxy using socket commands. +short_description: Enable, disable, and set weights for HAProxy backend servers using socket commands. description: - - The Enable Haproxy Backend Server, with - supports get current weight for server (default) and - set weight for haproxy backend server when provides. - - - The Disable Haproxy Backend Server, with - supports get current weight for server (default) and - shutdown sessions while disabling backend host server. + - Enable, disable, and set weights for HAProxy backend servers using socket + commands. notes: - - "enable or disable commands are restricted and can only be issued on sockets configured for level 'admin', " - - "Check - http://haproxy.1wt.eu/download/1.5/doc/configuration.txt, " - - "Example: 'stats socket /var/run/haproxy.sock level admin'" + - Enable and disable commands are restricted and can only be issued on + sockets configured for level 'admin'. For example, you can add the line + 'stats socket /var/run/haproxy.sock level admin' to the general section of + haproxy.cfg. See http://haproxy.1wt.eu/download/1.5/doc/configuration.txt. options: - state: + backend: description: - - describe the desired state of the given host in lb pool. - required: true - default: null - choices: [ "enabled", "disabled" ] + - Name of the HAProxy backend pool. + required: false + default: auto-detected host: description: - - Host (backend) to operate in Haproxy. + - Name of the backend host to change. required: true default: null - socket: + shutdown_sessions: description: - - Haproxy socket file name with path. + - When disabling a server, immediately terminate all the sessions attached + to the specified server. This can be used to terminate long-running + sessions after a server is put into maintenance mode. required: false - default: /var/run/haproxy.sock - backend: + default: false + socket: description: - - Name of the haproxy backend pool. - Required, else auto-detection applied. + - Path to the HAProxy socket file. required: false - default: auto-detected - weight: + default: /var/run/haproxy.sock + state: description: - - The value passed in argument. If the value ends with the '%' sign, then the new weight will be relative to the initially cnfigured weight. Relative weights are only permitted between 0 and 100% and absolute weights are permitted between 0 and 256. - required: false + - Desired state of the provided backend host. + required: true default: null - shutdown_sessions: + choices: [ "enabled", "disabled" ] + wait: description: - - When disabling server, immediately terminate all the sessions attached to the specified server. This can be used to terminate long-running sessions after a server is put into maintenance mode, for instance. + - Wait until the server reports a status of 'UP' when `state=enabled`, or + status of 'MAINT' when `state=disabled`. required: false default: false - wait: + version_added: "2.0" + wait_interval: description: - - Wait until the server reports a status of 'UP' when state=enabled, or status of 'MAINT' when state=disabled + - Number of seconds to wait between retries. required: false - default: false + default: 5 version_added: "2.0" wait_retries: description: - - number of times to check for status after changing the state + - Number of times to check for status after changing the state. required: false default: 25 version_added: "2.0" - wait_interval: + weight: description: - - number of seconds to wait between retries + - The value passed in argument. If the value ends with the `%` sign, then + the new weight will be relative to the initially configured weight. + Relative weights are only permitted between 0 and 100% and absolute + weights are permitted between 0 and 256. required: false - default: 5 - version_added: "2.0" + default: null ''' EXAMPLES = ''' -examples: - # disable server in 'www' backend pool - haproxy: state=disabled host={{ inventory_hostname }} backend=www From fd55a3075719453509635a2b2245158782f7a661 Mon Sep 17 00:00:00 2001 From: Matt Baldwin Date: Sun, 19 Jul 2015 17:11:23 -0700 Subject: [PATCH 0453/2522] Correcting issues raised by @abadger. --- cloud/profitbricks/profitbricks.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/cloud/profitbricks/profitbricks.py b/cloud/profitbricks/profitbricks.py index b42ca00a12f..556c652828e 100644 --- a/cloud/profitbricks/profitbricks.py +++ b/cloud/profitbricks/profitbricks.py @@ -119,7 +119,9 @@ default: 'present' choices: [ "running", "stopped", "absent", "present" ] -requirements: [ "profitbricks" ] +requirements: + - "profitbricks" + - "python >= 2.6" author: Matt Baldwin (baldwin@stackpointcloud.com) ''' @@ -178,8 +180,6 @@ import re import uuid import time -import json -import sys HAS_PB_SDK = True @@ -232,7 +232,7 @@ def _create_machine(module, profitbricks, datacenter, name): image = module.params.get('image') assign_public_ip = module.boolean(module.params.get('assign_public_ip')) wait = module.params.get('wait') - wait_timeout = int(module.params.get('wait_timeout')) + wait_timeout = module.params.get('wait_timeout') try: # Generate name, but grab first 10 chars so we don't @@ -298,8 +298,6 @@ def _create_machine(module, profitbricks, datacenter, name): wait_timeout, "create_virtual_machine") - - # return (json.dumps(server_response)) return (server_response) except Exception as e: module.fail_json(msg="failed to create the new server: %s" % str(e)) @@ -307,7 +305,7 @@ def _create_machine(module, profitbricks, datacenter, name): def _remove_machine(module, profitbricks, datacenter, name): remove_boot_volume = module.params.get('remove_boot_volume') wait = module.params.get('wait') - wait_timeout = int(module.params.get('wait_timeout')) + wait_timeout = module.params.get('wait_timeout') changed = False # User provided the actual UUID instead of the name. @@ -349,7 +347,7 @@ def _startstop_machine(module, profitbricks, datacenter, name): def _create_datacenter(module, profitbricks): datacenter = module.params.get('datacenter') location = module.params.get('location') - wait_timeout = int(module.params.get('wait_timeout')) + wait_timeout = module.params.get('wait_timeout') i = Datacenter( name=datacenter, @@ -381,7 +379,7 @@ def create_virtual_machine(module, profitbricks): auto_increment = module.params.get('auto_increment') count = module.params.get('count') lan = module.params.get('lan') - wait_timeout = int(module.params.get('wait_timeout')) + wait_timeout = module.params.get('wait_timeout') failed = True datacenter_found = False @@ -502,7 +500,7 @@ def startstop_machine(module, profitbricks, state): module.fail_json(msg='instance_ids should be a list of virtual machine ids or names, aborting') wait = module.params.get('wait') - wait_timeout = int(module.params.get('wait_timeout')) + wait_timeout = module.params.get('wait_timeout') changed = False datacenter = module.params.get('datacenter') @@ -574,7 +572,7 @@ def main(): location=dict(choices=LOCATIONS, default='us/las'), assign_public_ip=dict(type='bool', default=False), wait=dict(type='bool', default=True), - wait_timeout=dict(default=600), + wait_timeout=dict(type='int', default=600), remove_boot_volume=dict(type='bool', default=True), state=dict(default='present'), ) @@ -586,7 +584,7 @@ def main(): subscription_user = module.params.get('subscription_user') subscription_password = module.params.get('subscription_password') wait = module.params.get('wait') - wait_timeout = int(module.params.get('wait_timeout')) + wait_timeout = module.params.get('wait_timeout') profitbricks = ProfitBricksService( username=subscription_user, From 312b34ad81b6fa9325af969f12b1470d8df21e75 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Mon, 20 Jul 2015 15:33:35 +0200 Subject: [PATCH 0454/2522] cloudstack: new module cs_staticnat --- cloud/cloudstack/cs_staticnat.py | 316 +++++++++++++++++++++++++++++++ 1 file changed, 316 insertions(+) create mode 100644 cloud/cloudstack/cs_staticnat.py diff --git a/cloud/cloudstack/cs_staticnat.py b/cloud/cloudstack/cs_staticnat.py new file mode 100644 index 00000000000..5761a3990e9 --- /dev/null +++ b/cloud/cloudstack/cs_staticnat.py @@ -0,0 +1,316 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2015, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: cs_staticnat +short_description: Manages static NATs on Apache CloudStack based clouds. +description: + - Create, update and remove static NATs. +version_added: '2.0' +author: "René Moser (@resmo)" +options: + ip_address: + description: + - Public IP address the static NAT is assigned to. + required: true + vm: + description: + - Name of virtual machine which we make the static NAT for. + - Required if C(state=present). + required: false + default: null + vm_guest_ip: + description: + - VM guest NIC secondary IP address for the static NAT. + required: false + default: false + state: + description: + - State of the static NAT. + required: false + default: 'present' + choices: [ 'present', 'absent' ] + domain: + description: + - Domain the static NAT is related to. + required: false + default: null + account: + description: + - Account the static NAT is related to. + required: false + default: null + project: + description: + - Name of the project the static NAT is related to. + required: false + default: null + zone: + description: + - Name of the zone in which the virtual machine is in. + - If not set, default zone is used. + required: false + default: null + poll_async: + description: + - Poll async jobs until job has finished. + required: false + default: true +extends_documentation_fragment: cloudstack +''' + +EXAMPLES = ''' +# create a static NAT: 1.2.3.4 -> web01 +- local_action: + module: cs_staticnat + ip_address: 1.2.3.4 + vm: web01 + +# remove a static NAT +- local_action: + module: cs_staticnat + ip_address: 1.2.3.4 + state: absent +''' + +RETURN = ''' +--- +ip_address: + description: Public IP address. + returned: success + type: string + sample: 1.2.3.4 +vm_name: + description: Name of the virtual machine. + returned: success + type: string + sample: web-01 +vm_display_name: + description: Display name of the virtual machine. + returned: success + type: string + sample: web-01 +vm_guest_ip: + description: IP of the virtual machine. + returned: success + type: string + sample: 10.101.65.152 +zone: + description: Name of zone the static NAT is related to. + returned: success + type: string + sample: ch-gva-2 +project: + description: Name of project the static NAT is related to. + returned: success + type: string + sample: Production +account: + description: Account the static NAT is related to. + returned: success + type: string + sample: example account +domain: + description: Domain the static NAT is related to. + returned: success + type: string + sample: example domain +''' + + +try: + from cs import CloudStack, CloudStackException, read_config + has_lib_cs = True +except ImportError: + has_lib_cs = False + +# import cloudstack common +from ansible.module_utils.cloudstack import * + + +class AnsibleCloudStackStaticNat(AnsibleCloudStack): + + def __init__(self, module): + AnsibleCloudStack.__init__(self, module) + self.vm_default_nic = None + + +# TODO: move it to cloudstack utils, also used in cs_portforward + def get_vm_guest_ip(self): + vm_guest_ip = self.module.params.get('vm_guest_ip') + default_nic = self.get_vm_default_nic() + + if not vm_guest_ip: + return default_nic['ipaddress'] + + for secondary_ip in default_nic['secondaryip']: + if vm_guest_ip == secondary_ip['ipaddress']: + return vm_guest_ip + self.module.fail_json(msg="Secondary IP '%s' not assigned to VM" % vm_guest_ip) + + +# TODO: move it to cloudstack utils, also used in cs_portforward + def get_vm_default_nic(self): + if self.vm_default_nic: + return self.vm_default_nic + + nics = self.cs.listNics(virtualmachineid=self.get_vm(key='id')) + if nics: + for n in nics['nic']: + if n['isdefault']: + self.vm_default_nic = n + return self.vm_default_nic + self.module.fail_json(msg="No default IP address of VM '%s' found" % self.module.params.get('vm')) + + + def create_static_nat(self, ip_address): + self.result['changed'] = True + args = {} + args['virtualmachineid'] = self.get_vm(key='id') + args['ipaddressid'] = ip_address['id'] + args['vmguestip'] = self.get_vm_guest_ip() + if not self.module.check_mode: + res = self.cs.enableStaticNat(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + + # reset ip address and query new values + self.ip_address = None + ip_address = self.get_ip_address() + return ip_address + + + def update_static_nat(self, ip_address): + args = {} + args['virtualmachineid'] = self.get_vm(key='id') + args['ipaddressid'] = ip_address['id'] + args['vmguestip'] = self.get_vm_guest_ip() + + # make an alias, so we can use _has_changed() + ip_address['vmguestip'] = ip_address['vmipaddress'] + if self._has_changed(args, ip_address): + self.result['changed'] = True + if not self.module.check_mode: + res = self.cs.disableStaticNat(ipaddressid=ip_address['id']) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + res = self._poll_job(res, 'staticnat') + res = self.cs.enableStaticNat(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + + # reset ip address and query new values + self.ip_address = None + ip_address = self.get_ip_address() + return ip_address + + + def present_static_nat(self): + ip_address = self.get_ip_address() + if not ip_address['isstaticnat']: + ip_address = self.create_static_nat(ip_address) + else: + ip_address = self.update_static_nat(ip_address) + return ip_address + + + def absent_static_nat(self): + ip_address = self.get_ip_address() + if ip_address['isstaticnat']: + self.result['changed'] = True + if not self.module.check_mode: + res = self.cs.disableStaticNat(ipaddressid=ip_address['id']) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + poll_async = self.module.params.get('poll_async') + if poll_async: + res = self._poll_job(res, 'staticnat') + return ip_address + + + def get_result(self, ip_address): + if ip_address: + if 'zonename' in ip_address: + self.result['zone'] = ip_address['zonename'] + if 'domain' in ip_address: + self.result['domain'] = ip_address['domain'] + if 'account' in ip_address: + self.result['account'] = ip_address['account'] + if 'project' in ip_address: + self.result['project'] = ip_address['project'] + if 'virtualmachinedisplayname' in ip_address: + self.result['vm_display_name'] = ip_address['virtualmachinedisplayname'] + if 'virtualmachinename' in ip_address: + self.result['vm'] = ip_address['virtualmachinename'] + if 'vmipaddress' in ip_address: + self.result['vm_guest_ip'] = ip_address['vmipaddress'] + if 'ipaddress' in ip_address: + self.result['ip_address'] = ip_address['ipaddress'] + return self.result + + +def main(): + module = AnsibleModule( + argument_spec = dict( + ip_address = dict(required=True), + vm = dict(default=None), + vm_guest_ip = dict(default=None), + state = dict(choices=['present', 'absent'], default='present'), + zone = dict(default=None), + domain = dict(default=None), + account = dict(default=None), + project = dict(default=None), + poll_async = dict(choices=BOOLEANS, default=True), + api_key = dict(default=None), + api_secret = dict(default=None, no_log=True), + api_url = dict(default=None), + api_http_method = dict(choices=['get', 'post'], default='get'), + api_timeout = dict(type='int', default=10), + ), + required_together = ( + ['api_key', 'api_secret', 'api_url'], + ), + supports_check_mode=True + ) + + if not has_lib_cs: + module.fail_json(msg="python library cs required: pip install cs") + + try: + acs_static_nat = AnsibleCloudStackStaticNat(module) + + state = module.params.get('state') + if state in ['absent']: + ip_address = acs_static_nat.absent_static_nat() + else: + ip_address = acs_static_nat.present_static_nat() + + result = acs_static_nat.get_result(ip_address) + + except CloudStackException, e: + module.fail_json(msg='CloudStackException: %s' % str(e)) + + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From 4c0aaf8c90e0143d65e45b90acce25b1de596aad Mon Sep 17 00:00:00 2001 From: Siva Popuri Date: Mon, 20 Jul 2015 10:05:35 -0500 Subject: [PATCH 0455/2522] Corrected the license text to match GPLv3 --- cloud/centurylink/clc_group.py | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/cloud/centurylink/clc_group.py b/cloud/centurylink/clc_group.py index 2dd9683081e..5bbf5166b6f 100644 --- a/cloud/centurylink/clc_group.py +++ b/cloud/centurylink/clc_group.py @@ -1,29 +1,22 @@ #!/usr/bin/python -# CenturyLink Cloud Ansible Modules. # -# These Ansible modules enable the CenturyLink Cloud v2 API to be called -# from an within Ansible Playbook. +# Copyright (c) 2015 CenturyLink # -# This file is part of CenturyLink Cloud, and is maintained -# by the Workflow as a Service Team +# This file is part of Ansible. # -# Copyright 2015 CenturyLink Cloud +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. # -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# CenturyLink Cloud: http://www.CenturyLinkCloud.com -# API Documentation: https://www.centurylinkcloud.com/api-docs/v2/ +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see # DOCUMENTATION = ''' From aea762bfce9d6ebe0b062a1b7e23df4c8df92380 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 20 Jul 2015 11:31:33 -0400 Subject: [PATCH 0456/2522] fixed version added --- cloud/centurylink/clc_group.py | 2 +- cloud/centurylink/clc_publicip.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/centurylink/clc_group.py b/cloud/centurylink/clc_group.py index 5bbf5166b6f..122b71b2399 100644 --- a/cloud/centurylink/clc_group.py +++ b/cloud/centurylink/clc_group.py @@ -24,7 +24,7 @@ short_desciption: Create/delete Server Groups at Centurylink Cloud description: - Create or delete Server Groups at Centurylink Centurylink Cloud -version_added: 1.0 +version_added: "2.0" options: name: description: diff --git a/cloud/centurylink/clc_publicip.py b/cloud/centurylink/clc_publicip.py index 77632c1cbfe..0b001d0c5ce 100644 --- a/cloud/centurylink/clc_publicip.py +++ b/cloud/centurylink/clc_publicip.py @@ -24,7 +24,7 @@ short_description: Add and Delete public ips on servers in CenturyLink Cloud. description: - An Ansible module to add or delete public ip addresses on an existing server or servers in CenturyLink Cloud. -version_added: 1.0 +version_added: "2.0" options: protocol: descirption: From 46f01535ec343e4e136694d6f3ad04b039beb5a4 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 20 Jul 2015 11:34:57 -0400 Subject: [PATCH 0457/2522] minor docfixes --- cloud/centurylink/clc_group.py | 8 ++++---- cloud/centurylink/clc_publicip.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cloud/centurylink/clc_group.py b/cloud/centurylink/clc_group.py index 122b71b2399..3ec92612b17 100644 --- a/cloud/centurylink/clc_group.py +++ b/cloud/centurylink/clc_group.py @@ -61,12 +61,12 @@ notes: - To use this module, it is required to set the below environment variables which enables access to the Centurylink Cloud - - CLC_V2_API_USERNAME: the account login id for the centurylink cloud - - CLC_V2_API_PASSWORD: the account passwod for the centurylink cloud + - CLC_V2_API_USERNAME, the account login id for the centurylink cloud + - CLC_V2_API_PASSWORD, the account passwod for the centurylink cloud - Alternatively, the module accepts the API token and account alias. The API token can be generated using the CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login - - CLC_V2_API_TOKEN: the API token generated from https://api.ctl.io/v2/authentication/login - - CLC_ACCT_ALIAS: the account alias associated with the centurylink cloud + - CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login + - CLC_ACCT_ALIAS, the account alias associated with the centurylink cloud - Users can set CLC_V2_API_URL to specify an endpoint for pointing to a different CLC environment. ''' diff --git a/cloud/centurylink/clc_publicip.py b/cloud/centurylink/clc_publicip.py index 0b001d0c5ce..9879b61fd49 100644 --- a/cloud/centurylink/clc_publicip.py +++ b/cloud/centurylink/clc_publicip.py @@ -60,12 +60,12 @@ notes: - To use this module, it is required to set the below environment variables which enables access to the Centurylink Cloud - - CLC_V2_API_USERNAME: the account login id for the centurylink cloud - - CLC_V2_API_PASSWORD: the account passwod for the centurylink cloud + - CLC_V2_API_USERNAME, the account login id for the centurylink cloud + - CLC_V2_API_PASSWORD, the account passwod for the centurylink cloud - Alternatively, the module accepts the API token and account alias. The API token can be generated using the CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login - - CLC_V2_API_TOKEN: the API token generated from https://api.ctl.io/v2/authentication/login - - CLC_ACCT_ALIAS: the account alias associated with the centurylink cloud + - CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login + - CLC_ACCT_ALIAS, the account alias associated with the centurylink cloud - Users can set CLC_V2_API_URL to specify an endpoint for pointing to a different CLC environment. ''' From 69ce2ecaaa9a614754d5689cc4fd0c6a5349f73f Mon Sep 17 00:00:00 2001 From: Siva Popuri Date: Mon, 20 Jul 2015 11:02:10 -0500 Subject: [PATCH 0458/2522] ansible module for managing anti affinity policy in centurylink cloud has been added --- cloud/centurylink/clc_aa_policy.py | 321 +++++++++++++++++++++++++++++ 1 file changed, 321 insertions(+) create mode 100644 cloud/centurylink/clc_aa_policy.py diff --git a/cloud/centurylink/clc_aa_policy.py b/cloud/centurylink/clc_aa_policy.py new file mode 100644 index 00000000000..30be6f40715 --- /dev/null +++ b/cloud/centurylink/clc_aa_policy.py @@ -0,0 +1,321 @@ +#!/usr/bin/python + +# +# Copyright (c) 2015 CenturyLink +# +# This file is part of Ansible. +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see +# + +DOCUMENTATION = ''' +module: clc_aa_policy +short_descirption: Create or Delete Anti Affinity Policies at CenturyLink Cloud. +description: + - An Ansible module to Create or Delete Anti Affinity Policies at CenturyLink Cloud. +version_added: "2.0" +options: + name: + description: + - The name of the Anti Affinity Policy. + required: True + location: + description: + - Datacenter in which the policy lives/should live. + required: True + state: + description: + - Whether to create or delete the policy. + required: False + default: present + choices: ['present','absent'] + wait: + description: + - Whether to wait for the tasks to finish before returning. + default: True + required: False + choices: [True, False] + aliases: [] +requirements: + - python = 2.7 + - requests >= 2.5.0 + - clc-sdk +notes: + - To use this module, it is required to set the below environment variables which enables access to the + Centurylink Cloud + - CLC_V2_API_USERNAME, the account login id for the centurylink cloud + - CLC_V2_API_PASSWORD, the account passwod for the centurylink cloud + - Alternatively, the module accepts the API token and account alias. The API token can be generated using the + CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login + - CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login + - CLC_ACCT_ALIAS, the account alias associated with the centurylink cloud + - Users can set CLC_V2_API_URL to specify an endpoint for pointing to a different CLC environment. +''' + +EXAMPLES = ''' +# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples + +--- +- name: Create AA Policy + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Create an Anti Affinity Policy + clc_aa_policy: + name: 'Hammer Time' + location: 'UK3' + state: present + register: policy + + - name: debug + debug: var=policy + +--- +- name: Delete AA Policy + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Delete an Anti Affinity Policy + clc_aa_policy: + name: 'Hammer Time' + location: 'UK3' + state: absent + register: policy + + - name: debug + debug: var=policy +''' + +__version__ = '${version}' + +from distutils.version import LooseVersion + +try: + import requests +except ImportError: + REQUESTS_FOUND = False +else: + REQUESTS_FOUND = True + +# +# Requires the clc-python-sdk. +# sudo pip install clc-sdk +# +try: + import clc as clc_sdk + from clc import CLCException +except ImportError: + CLC_FOUND = False + clc_sdk = None +else: + CLC_FOUND = True + + +class ClcAntiAffinityPolicy(): + + clc = clc_sdk + module = None + + def __init__(self, module): + """ + Construct module + """ + self.module = module + self.policy_dict = {} + + if not CLC_FOUND: + self.module.fail_json( + msg='clc-python-sdk required for this module') + if not REQUESTS_FOUND: + self.module.fail_json( + msg='requests library is required for this module') + if requests.__version__ and LooseVersion(requests.__version__) < LooseVersion('2.5.0'): + self.module.fail_json( + msg='requests library version should be >= 2.5.0') + + self._set_user_agent(self.clc) + + @staticmethod + def _define_module_argument_spec(): + """ + Define the argument spec for the ansible module + :return: argument spec dictionary + """ + argument_spec = dict( + name=dict(required=True), + location=dict(required=True), + wait=dict(default=True), + state=dict(default='present', choices=['present', 'absent']), + ) + return argument_spec + + # Module Behavior Goodness + def process_request(self): + """ + Process the request - Main Code Path + :return: Returns with either an exit_json or fail_json + """ + p = self.module.params + + self._set_clc_credentials_from_env() + self.policy_dict = self._get_policies_for_datacenter(p) + + if p['state'] == "absent": + changed, policy = self._ensure_policy_is_absent(p) + else: + changed, policy = self._ensure_policy_is_present(p) + + if hasattr(policy, 'data'): + policy = policy.data + elif hasattr(policy, '__dict__'): + policy = policy.__dict__ + + self.module.exit_json(changed=changed, policy=policy) + + def _set_clc_credentials_from_env(self): + """ + Set the CLC Credentials on the sdk by reading environment variables + :return: none + """ + env = os.environ + v2_api_token = env.get('CLC_V2_API_TOKEN', False) + v2_api_username = env.get('CLC_V2_API_USERNAME', False) + v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) + clc_alias = env.get('CLC_ACCT_ALIAS', False) + api_url = env.get('CLC_V2_API_URL', False) + + if api_url: + self.clc.defaults.ENDPOINT_URL_V2 = api_url + + if v2_api_token and clc_alias: + self.clc._LOGIN_TOKEN_V2 = v2_api_token + self.clc._V2_ENABLED = True + self.clc.ALIAS = clc_alias + elif v2_api_username and v2_api_passwd: + self.clc.v2.SetCredentials( + api_username=v2_api_username, + api_passwd=v2_api_passwd) + else: + return self.module.fail_json( + msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " + "environment variables") + + def _get_policies_for_datacenter(self, p): + """ + Get the Policies for a datacenter by calling the CLC API. + :param p: datacenter to get policies from + :return: policies in the datacenter + """ + response = {} + + policies = self.clc.v2.AntiAffinity.GetAll(location=p['location']) + + for policy in policies: + response[policy.name] = policy + return response + + def _create_policy(self, p): + """ + Create an Anti Affinnity Policy using the CLC API. + :param p: datacenter to create policy in + :return: response dictionary from the CLC API. + """ + try: + return self.clc.v2.AntiAffinity.Create( + name=p['name'], + location=p['location']) + except CLCException, ex: + self.module.fail_json(msg='Failed to create anti affinity policy : {0}. {1}'.format( + p['name'], ex.response_text + )) + + def _delete_policy(self, p): + """ + Delete an Anti Affinity Policy using the CLC API. + :param p: datacenter to delete a policy from + :return: none + """ + try: + policy = self.policy_dict[p['name']] + policy.Delete() + except CLCException, ex: + self.module.fail_json(msg='Failed to delete anti affinity policy : {0}. {1}'.format( + p['name'], ex.response_text + )) + + def _policy_exists(self, policy_name): + """ + Check to see if an Anti Affinity Policy exists + :param policy_name: name of the policy + :return: boolean of if the policy exists + """ + if policy_name in self.policy_dict: + return self.policy_dict.get(policy_name) + + return False + + def _ensure_policy_is_absent(self, p): + """ + Makes sure that a policy is absent + :param p: dictionary of policy name + :return: tuple of if a deletion occurred and the name of the policy that was deleted + """ + changed = False + if self._policy_exists(policy_name=p['name']): + changed = True + if not self.module.check_mode: + self._delete_policy(p) + return changed, None + + def _ensure_policy_is_present(self, p): + """ + Ensures that a policy is present + :param p: dictonary of a policy name + :return: tuple of if an addition occurred and the name of the policy that was added + """ + changed = False + policy = self._policy_exists(policy_name=p['name']) + if not policy: + changed = True + policy = None + if not self.module.check_mode: + policy = self._create_policy(p) + return changed, policy + + @staticmethod + def _set_user_agent(clc): + if hasattr(clc, 'SetRequestsSession'): + agent_string = "ClcAnsibleModule/" + __version__ + ses = requests.Session() + ses.headers.update({"Api-Client": agent_string}) + ses.headers['User-Agent'] += " " + agent_string + clc.SetRequestsSession(ses) + + +def main(): + """ + The main function. Instantiates the module and calls process_request. + :return: none + """ + module = AnsibleModule( + argument_spec=ClcAntiAffinityPolicy._define_module_argument_spec(), + supports_check_mode=True) + clc_aa_policy = ClcAntiAffinityPolicy(module) + clc_aa_policy.process_request() + +from ansible.module_utils.basic import * # pylint: disable=W0614 +if __name__ == '__main__': + main() From ecf30095bca767e77780fb7c14c1c4104e2d6ce1 Mon Sep 17 00:00:00 2001 From: Siva Popuri Date: Mon, 20 Jul 2015 11:52:24 -0500 Subject: [PATCH 0459/2522] ansible module for managing alert policy in centurylink cloud has been added --- cloud/centurylink/clc_alert_policy.py | 501 ++++++++++++++++++++++++++ 1 file changed, 501 insertions(+) create mode 100644 cloud/centurylink/clc_alert_policy.py diff --git a/cloud/centurylink/clc_alert_policy.py b/cloud/centurylink/clc_alert_policy.py new file mode 100644 index 00000000000..71cfe70169f --- /dev/null +++ b/cloud/centurylink/clc_alert_policy.py @@ -0,0 +1,501 @@ +#!/usr/bin/python + +# +# Copyright (c) 2015 CenturyLink +# +# This file is part of Ansible. +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see +# + +DOCUMENTATION = ''' +module: clc_alert_policy +short_descirption: Create or Delete Alert Policies at CenturyLink Cloud. +description: + - An Ansible module to Create or Delete Alert Policies at CenturyLink Cloud. +version_added: "2.0" +options: + alias: + description: + - The alias of your CLC Account + required: True + name: + description: + - The name of the alert policy. This is mutually exclusive with id + required: False + default: None + aliases: [] + id: + description: + - The alert policy id. This is mutually exclusive with name + required: False + default: None + aliases: [] + alert_recipients: + description: + - A list of recipient email ids to notify the alert. + This is required for state 'present' + required: False + aliases: [] + metric: + description: + - The metric on which to measure the condition that will trigger the alert. + This is required for state 'present' + required: False + default: None + choices: ['cpu','memory','disk'] + aliases: [] + duration: + description: + - The length of time in minutes that the condition must exceed the threshold. + This is required for state 'present' + required: False + default: None + aliases: [] + threshold: + description: + - The threshold that will trigger the alert when the metric equals or exceeds it. + This is required for state 'present' + This number represents a percentage and must be a value between 5.0 - 95.0 that is a multiple of 5.0 + required: False + default: None + aliases: [] + state: + description: + - Whether to create or delete the policy. + required: False + default: present + choices: ['present','absent'] +requirements: + - python = 2.7 + - requests >= 2.5.0 + - clc-sdk +notes: + - To use this module, it is required to set the below environment variables which enables access to the + Centurylink Cloud + - CLC_V2_API_USERNAME, the account login id for the centurylink cloud + - CLC_V2_API_PASSWORD, the account passwod for the centurylink cloud + - Alternatively, the module accepts the API token and account alias. The API token can be generated using the + CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login + - CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login + - CLC_ACCT_ALIAS, the account alias associated with the centurylink cloud + - Users can set CLC_V2_API_URL to specify an endpoint for pointing to a different CLC environment. +''' + +EXAMPLES = ''' +# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples + +--- +- name: Create Alert Policy Example + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Create an Alert Policy for disk above 80% for 5 minutes + clc_alert_policy: + alias: wfad + name: 'alert for disk > 80%' + alert_recipients: + - test1@centurylink.com + - test2@centurylink.com + metric: 'disk' + duration: '00:05:00' + threshold: 80 + state: present + register: policy + + - name: debug + debug: var=policy + +--- +- name: Delete Alert Policy Example + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Delete an Alert Policy + clc_alert_policy: + alias: wfad + name: 'alert for disk > 80%' + state: absent + register: policy + + - name: debug + debug: var=policy +''' + +__version__ = '${version}' + +from distutils.version import LooseVersion + +try: + import requests +except ImportError: + REQUESTS_FOUND = False +else: + REQUESTS_FOUND = True + +# +# Requires the clc-python-sdk. +# sudo pip install clc-sdk +# +try: + import clc as clc_sdk + from clc import APIFailedResponse +except ImportError: + CLC_FOUND = False + clc_sdk = None +else: + CLC_FOUND = True + + +class ClcAlertPolicy(): + + clc = clc_sdk + module = None + + def __init__(self, module): + """ + Construct module + """ + self.module = module + self.policy_dict = {} + + if not CLC_FOUND: + self.module.fail_json( + msg='clc-python-sdk required for this module') + if not REQUESTS_FOUND: + self.module.fail_json( + msg='requests library is required for this module') + if requests.__version__ and LooseVersion(requests.__version__) < LooseVersion('2.5.0'): + self.module.fail_json( + msg='requests library version should be >= 2.5.0') + + self._set_user_agent(self.clc) + + @staticmethod + def _define_module_argument_spec(): + """ + Define the argument spec for the ansible module + :return: argument spec dictionary + """ + argument_spec = dict( + name=dict(default=None), + id=dict(default=None), + alias=dict(required=True, default=None), + alert_recipients=dict(type='list', default=None), + metric=dict( + choices=[ + 'cpu', + 'memory', + 'disk'], + default=None), + duration=dict(type='str', default=None), + threshold=dict(type='int', default=None), + state=dict(default='present', choices=['present', 'absent']) + ) + mutually_exclusive = [ + ['name', 'id'] + ] + return {'argument_spec': argument_spec, + 'mutually_exclusive': mutually_exclusive} + + # Module Behavior Goodness + def process_request(self): + """ + Process the request - Main Code Path + :return: Returns with either an exit_json or fail_json + """ + p = self.module.params + + self._set_clc_credentials_from_env() + self.policy_dict = self._get_alert_policies(p['alias']) + + if p['state'] == 'present': + changed, policy = self._ensure_alert_policy_is_present() + else: + changed, policy = self._ensure_alert_policy_is_absent() + + self.module.exit_json(changed=changed, policy=policy) + + def _set_clc_credentials_from_env(self): + """ + Set the CLC Credentials on the sdk by reading environment variables + :return: none + """ + env = os.environ + v2_api_token = env.get('CLC_V2_API_TOKEN', False) + v2_api_username = env.get('CLC_V2_API_USERNAME', False) + v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) + clc_alias = env.get('CLC_ACCT_ALIAS', False) + api_url = env.get('CLC_V2_API_URL', False) + + if api_url: + self.clc.defaults.ENDPOINT_URL_V2 = api_url + + if v2_api_token and clc_alias: + self.clc._LOGIN_TOKEN_V2 = v2_api_token + self.clc._V2_ENABLED = True + self.clc.ALIAS = clc_alias + elif v2_api_username and v2_api_passwd: + self.clc.v2.SetCredentials( + api_username=v2_api_username, + api_passwd=v2_api_passwd) + else: + return self.module.fail_json( + msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " + "environment variables") + + def _ensure_alert_policy_is_present(self): + """ + Ensures that the alert policy is present + :return: (changed, policy) + canged: A flag representing if anything is modified + policy: the created/updated alert policy + """ + changed = False + p = self.module.params + policy_name = p.get('name') + alias = p.get('alias') + if not policy_name: + self.module.fail_json(msg='Policy name is a required') + policy = self._alert_policy_exists(alias, policy_name) + if not policy: + changed = True + policy = None + if not self.module.check_mode: + policy = self._create_alert_policy() + else: + changed_u, policy = self._ensure_alert_policy_is_updated(policy) + if changed_u: + changed = True + return changed, policy + + def _ensure_alert_policy_is_absent(self): + """ + Ensures that the alert policy is absent + :return: (changed, None) + canged: A flag representing if anything is modified + """ + changed = False + p = self.module.params + alert_policy_id = p.get('id') + alert_policy_name = p.get('name') + alias = p.get('alias') + if not alert_policy_id and not alert_policy_name: + self.module.fail_json( + msg='Either alert policy id or policy name is required') + if not alert_policy_id and alert_policy_name: + alert_policy_id = self._get_alert_policy_id( + self.module, + alert_policy_name) + if alert_policy_id and alert_policy_id in self.policy_dict: + changed = True + if not self.module.check_mode: + self._delete_alert_policy(alias, alert_policy_id) + return changed, None + + def _ensure_alert_policy_is_updated(self, alert_policy): + """ + Ensures the aliert policy is updated if anything is changed in the alert policy configuration + :param alert_policy: the targetalert policy + :return: (changed, policy) + canged: A flag representing if anything is modified + policy: the updated the alert policy + """ + changed = False + p = self.module.params + alert_policy_id = alert_policy.get('id') + email_list = p.get('alert_recipients') + metric = p.get('metric') + duration = p.get('duration') + threshold = p.get('threshold') + policy = alert_policy + if (metric and metric != str(alert_policy.get('triggers')[0].get('metric'))) or \ + (duration and duration != str(alert_policy.get('triggers')[0].get('duration'))) or \ + (threshold and float(threshold) != float(alert_policy.get('triggers')[0].get('threshold'))): + changed = True + elif email_list: + t_email_list = list( + alert_policy.get('actions')[0].get('settings').get('recipients')) + if set(email_list) != set(t_email_list): + changed = True + if changed and not self.module.check_mode: + policy = self._update_alert_policy(alert_policy_id) + return changed, policy + + def _get_alert_policies(self, alias): + """ + Get the alert policies for account alias by calling the CLC API. + :param alias: the account alias + :return: the alert policies for the account alias + """ + response = {} + + policies = self.clc.v2.API.Call('GET', + '/v2/alertPolicies/%s' + % (alias)) + + for policy in policies.get('items'): + response[policy.get('id')] = policy + return response + + def _create_alert_policy(self): + """ + Create an alert Policy using the CLC API. + :return: response dictionary from the CLC API. + """ + p = self.module.params + alias = p['alias'] + email_list = p['alert_recipients'] + metric = p['metric'] + duration = p['duration'] + threshold = p['threshold'] + policy_name = p['name'] + arguments = json.dumps( + { + 'name': policy_name, + 'actions': [{ + 'action': 'email', + 'settings': { + 'recipients': email_list + } + }], + 'triggers': [{ + 'metric': metric, + 'duration': duration, + 'threshold': threshold + }] + } + ) + try: + result = self.clc.v2.API.Call( + 'POST', + '/v2/alertPolicies/%s' %alias, + arguments) + except APIFailedResponse as e: + return self.module.fail_json( + msg='Unable to create alert policy "{0}". {1}'.format( + policy_name, str(e.response_text))) + return result + + def _update_alert_policy(self, alert_policy_id): + """ + Update alert policy using the CLC API. + :param alert_policy_id: The clc alert policy id + :return: response dictionary from the CLC API. + """ + p = self.module.params + alias = p['alias'] + email_list = p['alert_recipients'] + metric = p['metric'] + duration = p['duration'] + threshold = p['threshold'] + policy_name = p['name'] + arguments = json.dumps( + { + 'name': policy_name, + 'actions': [{ + 'action': 'email', + 'settings': { + 'recipients': email_list + } + }], + 'triggers': [{ + 'metric': metric, + 'duration': duration, + 'threshold': threshold + }] + } + ) + try: + result = self.clc.v2.API.Call( + 'PUT', '/v2/alertPolicies/%s/%s' % + (alias, alert_policy_id), arguments) + except APIFailedResponse as e: + return self.module.fail_json( + msg='Unable to update alert policy "{0}". {1}'.format( + policy_name, str(e.response_text))) + return result + + def _delete_alert_policy(self, alias, policy_id): + """ + Delete an alert policy using the CLC API. + :param alias : the account alias + :param policy_id: the alert policy id + :return: response dictionary from the CLC API. + """ + try: + result = self.clc.v2.API.Call( + 'DELETE', '/v2/alertPolicies/%s/%s' % + (alias, policy_id), None) + except APIFailedResponse as e: + return self.module.fail_json( + msg='Unable to delete alert policy id "{0}". {1}'.format( + policy_id, str(e.response_text))) + return result + + def _alert_policy_exists(self, alias, policy_name): + """ + Check to see if an alert policy exists + :param policy_name: name of the alert policy + :return: boolean of if the policy exists + """ + result = False + for id in self.policy_dict: + if self.policy_dict.get(id).get('name') == policy_name: + result = self.policy_dict.get(id) + return result + + def _get_alert_policy_id(self, module, alert_policy_name): + """ + retrieves the alert policy id of the account based on the name of the policy + :param module: the AnsibleModule object + :param alert_policy_name: the alert policy name + :return: alert_policy_id: The alert policy id + """ + alert_policy_id = None + for id in self.policy_dict: + if self.policy_dict.get(id).get('name') == alert_policy_name: + if not alert_policy_id: + alert_policy_id = id + else: + return module.fail_json( + msg='mutiple alert policies were found with policy name : %s' % + (alert_policy_name)) + return alert_policy_id + + @staticmethod + def _set_user_agent(clc): + if hasattr(clc, 'SetRequestsSession'): + agent_string = "ClcAnsibleModule/" + __version__ + ses = requests.Session() + ses.headers.update({"Api-Client": agent_string}) + ses.headers['User-Agent'] += " " + agent_string + clc.SetRequestsSession(ses) + + +def main(): + """ + The main function. Instantiates the module and calls process_request. + :return: none + """ + argument_dict = ClcAlertPolicy._define_module_argument_spec() + module = AnsibleModule(supports_check_mode=True, **argument_dict) + clc_alert_policy = ClcAlertPolicy(module) + clc_alert_policy.process_request() + +from ansible.module_utils.basic import * # pylint: disable=W0614 +if __name__ == '__main__': + main() From c9064959ea50dbe0136cd903aad34c48bf7bbad7 Mon Sep 17 00:00:00 2001 From: Siva Popuri Date: Mon, 20 Jul 2015 13:30:31 -0500 Subject: [PATCH 0460/2522] corrected a typo --- cloud/centurylink/clc_aa_policy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/centurylink/clc_aa_policy.py b/cloud/centurylink/clc_aa_policy.py index 30be6f40715..790632ddb72 100644 --- a/cloud/centurylink/clc_aa_policy.py +++ b/cloud/centurylink/clc_aa_policy.py @@ -21,7 +21,7 @@ DOCUMENTATION = ''' module: clc_aa_policy -short_descirption: Create or Delete Anti Affinity Policies at CenturyLink Cloud. +short_description: Create or Delete Anti Affinity Policies at CenturyLink Cloud. description: - An Ansible module to Create or Delete Anti Affinity Policies at CenturyLink Cloud. version_added: "2.0" From 7d26a49dbad502393dd99a62794831b640237a23 Mon Sep 17 00:00:00 2001 From: Siva Popuri Date: Mon, 20 Jul 2015 13:31:22 -0500 Subject: [PATCH 0461/2522] Corrected a typo --- cloud/centurylink/clc_alert_policy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/centurylink/clc_alert_policy.py b/cloud/centurylink/clc_alert_policy.py index 71cfe70169f..b4495481ed3 100644 --- a/cloud/centurylink/clc_alert_policy.py +++ b/cloud/centurylink/clc_alert_policy.py @@ -21,7 +21,7 @@ DOCUMENTATION = ''' module: clc_alert_policy -short_descirption: Create or Delete Alert Policies at CenturyLink Cloud. +short_description: Create or Delete Alert Policies at CenturyLink Cloud. description: - An Ansible module to Create or Delete Alert Policies at CenturyLink Cloud. version_added: "2.0" From 69a920a5f52adeb2b16dcbf3ca92d174531b02e0 Mon Sep 17 00:00:00 2001 From: Toby Fleming Date: Mon, 20 Jul 2015 21:07:43 +0100 Subject: [PATCH 0462/2522] Homebrew: Allow colons, direct check for outdated formula --- packaging/os/homebrew.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packaging/os/homebrew.py b/packaging/os/homebrew.py index 91888ba6bca..6d295375ede 100644 --- a/packaging/os/homebrew.py +++ b/packaging/os/homebrew.py @@ -119,6 +119,7 @@ class Homebrew(object): / # slash (for taps) \+ # plusses - # dashes + : # colons (for URLs) ''' INVALID_PATH_REGEX = _create_regex_group(VALID_PATH_CHARS) @@ -394,18 +395,17 @@ def _current_package_is_installed(self): return False - def _outdated_packages(self): + def _current_package_is_outdated(self): + if not self.valid_package(self.current_package): + return False + rc, out, err = self.module.run_command([ self.brew_path, 'outdated', + self.current_package, ]) - return [line.split(' ')[0].strip() for line in out.split('\n') if line] - - def _current_package_is_outdated(self): - if not self.valid_package(self.current_package): - return False - return self.current_package in self._outdated_packages() + return rc != 0 def _current_package_is_installed_from_head(self): if not Homebrew.valid_package(self.current_package): From b51c252127472719562d44a706a6dd5482a32f8a Mon Sep 17 00:00:00 2001 From: Mark Hamilton Date: Mon, 20 Jul 2015 18:06:17 -0700 Subject: [PATCH 0463/2522] removed syslog import --- network/openvswitch_bridge.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/network/openvswitch_bridge.py b/network/openvswitch_bridge.py index 2396b1f2b49..8f29735862f 100644 --- a/network/openvswitch_bridge.py +++ b/network/openvswitch_bridge.py @@ -74,8 +74,6 @@ bridge-id: "br-int" ''' -import syslog - class OVSBridge(object): """ Interface to ovs-vsctl. """ From 79173ac18de66bcdb6c25729fb4561c772d37cab Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 20 Jul 2015 23:02:44 -0700 Subject: [PATCH 0464/2522] Clean up unneeded urllib2 imports --- clustering/consul.py | 6 +++--- clustering/consul_acl.py | 4 ++-- clustering/consul_kv.py | 4 ++-- clustering/consul_session.py | 4 ++-- database/misc/riak.py | 5 ++--- monitoring/bigpanda.py | 6 +++--- monitoring/boundary_meter.py | 4 ++-- monitoring/datadog_event.py | 6 +++--- network/a10/a10_virtual_server.py | 8 +++----- 9 files changed, 22 insertions(+), 25 deletions(-) diff --git a/clustering/consul.py b/clustering/consul.py index 116517571a5..f72fc6ddcac 100644 --- a/clustering/consul.py +++ b/clustering/consul.py @@ -166,7 +166,6 @@ ''' import sys -import urllib2 try: import json @@ -179,7 +178,7 @@ python_consul_installed = True except ImportError, e: python_consul_installed = False - + def register_with_consul(module): state = module.params.get('state') @@ -503,4 +502,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/clustering/consul_acl.py b/clustering/consul_acl.py index 250de24e2a3..c133704b64d 100644 --- a/clustering/consul_acl.py +++ b/clustering/consul_acl.py @@ -92,7 +92,6 @@ ''' import sys -import urllib2 try: import consul @@ -318,4 +317,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/clustering/consul_kv.py b/clustering/consul_kv.py index b0d07dda83a..06dd55b71fc 100644 --- a/clustering/consul_kv.py +++ b/clustering/consul_kv.py @@ -121,7 +121,6 @@ ''' import sys -import urllib2 try: import json @@ -261,4 +260,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/clustering/consul_session.py b/clustering/consul_session.py index ef4646c35e4..c298ea7fa57 100644 --- a/clustering/consul_session.py +++ b/clustering/consul_session.py @@ -114,7 +114,6 @@ ''' import sys -import urllib2 try: import consul @@ -266,4 +265,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/database/misc/riak.py b/database/misc/riak.py index 12586651887..453e6c15f3e 100644 --- a/database/misc/riak.py +++ b/database/misc/riak.py @@ -97,7 +97,6 @@ - riak: wait_for_service=kv ''' -import urllib2 import time import socket import sys @@ -254,5 +253,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.urls import * - -main() +if __name__ == '__main__': + main() diff --git a/monitoring/bigpanda.py b/monitoring/bigpanda.py index 3bed44893b7..cd08ac9f29e 100644 --- a/monitoring/bigpanda.py +++ b/monitoring/bigpanda.py @@ -59,7 +59,7 @@ choices: ['yes', 'no'] # informational: requirements for nodes -requirements: [ urllib, urllib2 ] +requirements: [ ] ''' EXAMPLES = ''' @@ -168,5 +168,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.urls import * - -main() +if __name__ == '__main__': + main() diff --git a/monitoring/boundary_meter.py b/monitoring/boundary_meter.py index 431a6ace1b9..3e03a55c8aa 100644 --- a/monitoring/boundary_meter.py +++ b/monitoring/boundary_meter.py @@ -38,7 +38,6 @@ requirements: - Boundary API access - bprobe is required to send data, but not to register a meter - - Python urllib2 options: name: description: @@ -252,5 +251,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.urls import * -main() +if __name__ == '__main__': + main() diff --git a/monitoring/datadog_event.py b/monitoring/datadog_event.py index ebbad039dec..406a5ea1865 100644 --- a/monitoring/datadog_event.py +++ b/monitoring/datadog_event.py @@ -16,7 +16,7 @@ version_added: "1.3" author: "Artūras `arturaz` Šlajus (@arturaz)" notes: [] -requirements: [urllib2] +requirements: [] options: api_key: description: ["Your DataDog API key."] @@ -139,5 +139,5 @@ def post_event(module): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.urls import * - -main() +if __name__ == '__main__': + main() diff --git a/network/a10/a10_virtual_server.py b/network/a10/a10_virtual_server.py index eb308a3032a..2dbaa9121eb 100644 --- a/network/a10/a10_virtual_server.py +++ b/network/a10/a10_virtual_server.py @@ -31,9 +31,7 @@ author: "Mischa Peters (@mischapeters)" notes: - Requires A10 Networks aXAPI 2.1 -requirements: - - urllib2 - - re +requirements: [] options: host: description: @@ -294,6 +292,6 @@ def needs_update(src_ports, dst_ports): from ansible.module_utils.basic import * from ansible.module_utils.urls import * from ansible.module_utils.a10 import * - -main() +if __name__ == '__main__': + main() From 626977f90e68282db4449f4cd3d8be7361cef597 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 20 Jul 2015 23:09:59 -0700 Subject: [PATCH 0465/2522] Port librarto_annotation from urllib2 to fetch_url --- monitoring/librato_annotation.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/monitoring/librato_annotation.py b/monitoring/librato_annotation.py index c606dfdc9a0..4927a9cf4c7 100644 --- a/monitoring/librato_annotation.py +++ b/monitoring/librato_annotation.py @@ -105,9 +105,6 @@ end_time: 1395954406 ''' - -import urllib2 - def post_annotation(module): user = module.params['user'] api_key = module.params['api_key'] @@ -134,10 +131,9 @@ def post_annotation(module): headers = {} headers['Content-Type'] = 'application/json' headers['Authorization'] = "Basic " + base64.b64encode(user + ":" + api_key).strip() - req = urllib2.Request(url, json_body, headers) - try: - response = urllib2.urlopen(req) - except urllib2.HTTPError, e: + + response, info = fetch_url(module, url, data=json_body, headers=headers) + if info['status'] != 200: module.fail_json(msg="Request Failed", reason=e.reason) response = response.read() module.exit_json(changed=True, annotation=response) @@ -161,4 +157,6 @@ def main(): post_annotation(module) from ansible.module_utils.basic import * -main() +from ansible.module_utils.urls import * +if __name__ == '__main__': + main() From 8aa490c638b3d8dff3074a77b3a6632a51bf973f Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 20 Jul 2015 23:22:33 -0700 Subject: [PATCH 0466/2522] Port uptimerobot to fetch_url --- monitoring/uptimerobot.py | 153 ++++++++++++++++---------------------- 1 file changed, 65 insertions(+), 88 deletions(-) diff --git a/monitoring/uptimerobot.py b/monitoring/uptimerobot.py index 6d5c9c7bac0..197c8877014 100644 --- a/monitoring/uptimerobot.py +++ b/monitoring/uptimerobot.py @@ -51,118 +51,95 @@ import json import urllib -import urllib2 import time API_BASE = "http://api.uptimerobot.com/" API_ACTIONS = dict( - status='getMonitors?', - editMonitor='editMonitor?' + status='getMonitors?', + editMonitor='editMonitor?' ) API_FORMAT = 'json' - API_NOJSONCALLBACK = 1 - CHANGED_STATE = False - SUPPORTS_CHECK_MODE = False -def checkID(params): - - data = urllib.urlencode(params) - - full_uri = API_BASE + API_ACTIONS['status'] + data - - req = urllib2.urlopen(full_uri) - - result = req.read() - - jsonresult = json.loads(result) - - req.close() - - return jsonresult - - -def startMonitor(params): - - params['monitorStatus'] = 1 - - data = urllib.urlencode(params) - - full_uri = API_BASE + API_ACTIONS['editMonitor'] + data - req = urllib2.urlopen(full_uri) +def checkID(module, params): - result = req.read() + data = urllib.urlencode(params) + full_uri = API_BASE + API_ACTIONS['status'] + data + req, info = fetch_url(module, full_uri) + result = req.read() + jsonresult = json.loads(result) + req.close() + return jsonresult - jsonresult = json.loads(result) - req.close() +def startMonitor(module, params): - return jsonresult['stat'] + params['monitorStatus'] = 1 + data = urllib.urlencode(params) + full_uri = API_BASE + API_ACTIONS['editMonitor'] + data + req, info = fetch_url(module, full_uri) + result = req.read() + jsonresult = json.loads(result) + req.close() + return jsonresult['stat'] -def pauseMonitor(params): +def pauseMonitor(module, params): - params['monitorStatus'] = 0 - - data = urllib.urlencode(params) - - full_uri = API_BASE + API_ACTIONS['editMonitor'] + data - - req = urllib2.urlopen(full_uri) - - result = req.read() - - jsonresult = json.loads(result) - - req.close() - - return jsonresult['stat'] + params['monitorStatus'] = 0 + data = urllib.urlencode(params) + full_uri = API_BASE + API_ACTIONS['editMonitor'] + data + req, info = fetch_url(module, full_uri) + result = req.read() + jsonresult = json.loads(result) + req.close() + return jsonresult['stat'] def main(): - module = AnsibleModule( - argument_spec = dict( - state = dict(required=True, choices=['started', 'paused']), - apikey = dict(required=True), - monitorid = dict(required=True) - ), - supports_check_mode=SUPPORTS_CHECK_MODE - ) - - params = dict( - apiKey=module.params['apikey'], - monitors=module.params['monitorid'], - monitorID=module.params['monitorid'], - format=API_FORMAT, - noJsonCallback=API_NOJSONCALLBACK - ) - - check_result = checkID(params) - - if check_result['stat'] != "ok": - module.fail_json( - msg="failed", - result=check_result['message'] - ) - - if module.params['state'] == 'started': - monitor_result = startMonitor(params) - else: - monitor_result = pauseMonitor(params) - - - - module.exit_json( - msg="success", - result=monitor_result - ) + module = AnsibleModule( + argument_spec = dict( + state = dict(required=True, choices=['started', 'paused']), + apikey = dict(required=True), + monitorid = dict(required=True) + ), + supports_check_mode=SUPPORTS_CHECK_MODE + ) + + params = dict( + apiKey=module.params['apikey'], + monitors=module.params['monitorid'], + monitorID=module.params['monitorid'], + format=API_FORMAT, + noJsonCallback=API_NOJSONCALLBACK + ) + + check_result = checkID(module, params) + + if check_result['stat'] != "ok": + module.fail_json( + msg="failed", + result=check_result['message'] + ) + + if module.params['state'] == 'started': + monitor_result = startMonitor(module, params) + else: + monitor_result = pauseMonitor(module, params) + + module.exit_json( + msg="success", + result=monitor_result + ) from ansible.module_utils.basic import * -main() +from ansible.module_utils.urls import * +if __name__ == '__main__': + main() From d6b0cf7bc4ff320cd2e2ed00739a7c4fa5f7ff3b Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 20 Jul 2015 23:28:29 -0700 Subject: [PATCH 0467/2522] Port campifre to fetch_url --- notification/campfire.py | 72 +++++++++++++++------------------------- 1 file changed, 27 insertions(+), 45 deletions(-) diff --git a/notification/campfire.py b/notification/campfire.py index 2400ad3ba40..62d65015213 100644 --- a/notification/campfire.py +++ b/notification/campfire.py @@ -42,7 +42,7 @@ "vuvuzela", "what", "whoomp", "yeah", "yodel"] # informational: requirements for nodes -requirements: [ urllib2, cgi ] +requirements: [ ] author: "Adam Garside (@fabulops)" ''' @@ -53,19 +53,10 @@ msg="Task completed ... with feeling." ''' +import cgi def main(): - try: - import urllib2 - except ImportError: - module.fail_json(msg="urllib2 is required") - - try: - import cgi - except ImportError: - module.fail_json(msg="cgi is required") - module = AnsibleModule( argument_spec=dict( subscription=dict(required=True), @@ -102,42 +93,33 @@ def main(): MSTR = "%s" AGENT = "Ansible/1.2" - try: - - # Setup basic auth using token as the username - pm = urllib2.HTTPPasswordMgrWithDefaultRealm() - pm.add_password(None, URI, token, 'X') - - # Setup Handler and define the opener for the request - handler = urllib2.HTTPBasicAuthHandler(pm) - opener = urllib2.build_opener(handler) - - target_url = '%s/room/%s/speak.xml' % (URI, room) - - # Send some audible notification if requested - if notify: - req = urllib2.Request(target_url, NSTR % cgi.escape(notify)) - req.add_header('Content-Type', 'application/xml') - req.add_header('User-agent', AGENT) - response = opener.open(req) - - # Send the message - req = urllib2.Request(target_url, MSTR % cgi.escape(msg)) - req.add_header('Content-Type', 'application/xml') - req.add_header('User-agent', AGENT) - response = opener.open(req) - - except urllib2.HTTPError, e: - if not (200 <= e.code < 300): - module.fail_json(msg="unable to send msg: '%s', campfire api" - " returned error code: '%s'" % - (msg, e.code)) - - except Exception, e: - module.fail_json(msg="unable to send msg: %s" % msg) + # Hack to add basic auth username and password the way fetch_url expects + module.params['username'] = token + module.params['password'] = 'X' + + target_url = '%s/room/%s/speak.xml' % (URI, room) + headers = {'Content-Type': 'application/xml', + 'User-agent': AGENT} + + # Send some audible notification if requested + if notify: + response, info = fetch_url(module, target_url, data=NSTR % cgi.escape(notify), headers=headers) + if info['status'] != 200: + module.fail_json(msg="unable to send msg: '%s', campfire api" + " returned error code: '%s'" % + (notify, info['status'])) + + # Send the message + response, info = fetch_url(module, target_url, data=MSTR %cgi.escape(msg), headers=headers) + if info['status'] != 200: + module.fail_json(msg="unable to send msg: '%s', campfire api" + " returned error code: '%s'" % + (msg, info['status'])) module.exit_json(changed=True, room=room, msg=msg, notify=notify) # import module snippets from ansible.module_utils.basic import * -main() +from ansible.module_utils.urls import * +if __name__ == '__main__': + main() From b8df0d32a2e2c8f2b2069c6170cc1a0f9a923d3e Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 20 Jul 2015 23:33:05 -0700 Subject: [PATCH 0468/2522] Port sendgrid to fetch_url --- notification/sendgrid.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/notification/sendgrid.py b/notification/sendgrid.py index e1ae7b7749f..2655b4248bb 100644 --- a/notification/sendgrid.py +++ b/notification/sendgrid.py @@ -85,9 +85,6 @@ # sendgrid module support methods # import urllib -import urllib2 - -import base64 def post_sendgrid_api(module, username, password, from_address, to_addresses, subject, body): @@ -102,11 +99,11 @@ def post_sendgrid_api(module, username, password, from_address, to_addresses, recipient = recipient.encode('utf-8') to_addresses_api += '&to[]=%s' % recipient encoded_data += to_addresses_api - request = urllib2.Request(SENDGRID_URI) - request.add_header('User-Agent', AGENT) - request.add_header('Content-type', 'application/x-www-form-urlencoded') - request.add_header('Accept', 'application/json') - return urllib2.urlopen(request, encoded_data) + + headers = { 'User-Agent': AGENT, + 'Content-type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json'} + return fetch_url(module, SENDGRID_URI, data=encoded_data, headers=headers, method='POST') # ======================================= @@ -133,14 +130,16 @@ def main(): subject = module.params['subject'] body = module.params['body'] - try: - response = post_sendgrid_api(module, username, password, - from_address, to_addresses, subject, body) - except Exception: - module.fail_json(msg="unable to send email through SendGrid API") + response, info = post_sendgrid_api(module, username, password, + from_address, to_addresses, subject, body) + if info['status'] != 200: + module.fail_json(msg="unable to send email through SendGrid API: %s" % info['msg']) + module.exit_json(msg=subject, changed=False) # import module snippets from ansible.module_utils.basic import * -main() +from ansible.module_utils.urls import * +if __name__ == '__main__': + main() From 4ef6f4ea4248d2b16771cfcf72cab0cb4009fbd9 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 20 Jul 2015 23:39:42 -0700 Subject: [PATCH 0469/2522] Port twilio to fetch_url --- notification/twilio.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/notification/twilio.py b/notification/twilio.py index ee12d987e9e..58898b0bc73 100644 --- a/notification/twilio.py +++ b/notification/twilio.py @@ -104,10 +104,8 @@ # ======================================= # twilio module support methods # -import urllib -import urllib2 - import base64 +import urllib def post_twilio_api(module, account_sid, auth_token, msg, from_number, @@ -120,14 +118,16 @@ def post_twilio_api(module, account_sid, auth_token, msg, from_number, if media_url: data['MediaUrl'] = media_url encoded_data = urllib.urlencode(data) - request = urllib2.Request(URI) + base64string = base64.encodestring('%s:%s' % \ (account_sid, auth_token)).replace('\n', '') - request.add_header('User-Agent', AGENT) - request.add_header('Content-type', 'application/x-www-form-urlencoded') - request.add_header('Accept', 'application/json') - request.add_header('Authorization', 'Basic %s' % base64string) - return urllib2.urlopen(request, encoded_data) + + headers = {'User-Agent': AGENT, + 'Content-type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json', + 'Authorization': 'Basic %s' % base64string, + } + return fetch_url(module, URI, data=encoded_data, headers=headers) # ======================================= @@ -159,14 +159,15 @@ def main(): to_number = [to_number] for number in to_number: - try: - post_twilio_api(module, account_sid, auth_token, msg, + r, info = post_twilio_api(module, account_sid, auth_token, msg, from_number, number, media_url) - except Exception: + if info['status'] != 200: module.fail_json(msg="unable to send message to %s" % number) module.exit_json(msg=msg, changed=False) # import module snippets from ansible.module_utils.basic import * -main() +from ansible.module_utils.urls import * +if __name__ == '__main__': + main() From 905737c974c928482cda94397a7939cb45e2ae60 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 20 Jul 2015 23:47:56 -0700 Subject: [PATCH 0470/2522] Port typetalk to fetch_url --- notification/typetalk.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/notification/typetalk.py b/notification/typetalk.py index 002c8b5cc85..f6b84cb00e7 100644 --- a/notification/typetalk.py +++ b/notification/typetalk.py @@ -35,21 +35,28 @@ import urllib -import urllib2 - try: import json except ImportError: - json = None + try: + import simplejson as json + except ImportError: + json = None -def do_request(url, params, headers={}): +def do_request(module, url, params, headers=None): data = urllib.urlencode(params) + if headers is None: + headers = dict() headers = dict(headers, **{ 'User-Agent': 'Ansible/typetalk module', }) - return urllib2.urlopen(urllib2.Request(url, data, headers)) - + r, info = fetch_url(module, url, data=data, headers=headers) + if info['status'] != 200: + exc = ConnectionError(info['msg']) + exc.code = info['status'] + raise exc + return r def get_access_token(client_id, client_secret): params = { @@ -62,7 +69,7 @@ def get_access_token(client_id, client_secret): return json.load(res)['access_token'] -def send_message(client_id, client_secret, topic, msg): +def send_message(module, client_id, client_secret, topic, msg): """ send message to typetalk """ @@ -72,9 +79,9 @@ def send_message(client_id, client_secret, topic, msg): headers = { 'Authorization': 'Bearer %s' % access_token, } - do_request(url, {'message': msg}, headers) + do_request(module, url, {'message': msg}, headers) return True, {'access_token': access_token} - except urllib2.HTTPError, e: + except ConnectionError, e: return False, e @@ -98,7 +105,7 @@ def main(): topic = module.params["topic"] msg = module.params["msg"] - res, error = send_message(client_id, client_secret, topic, msg) + res, error = send_message(module, client_id, client_secret, topic, msg) if not res: module.fail_json(msg='fail to send message with response code %s' % error.code) @@ -107,4 +114,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +from ansible.module_utils.urls import * +if __name__ == '__main__': + main() From 45f1c7903a811945cd5b4018ddd679b48e16a4da Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Tue, 21 Jul 2015 07:02:50 -0700 Subject: [PATCH 0471/2522] dnf rewrite requires 2.6 and above (dnf bindings require 2.6 and above as well) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 057524c4def..409c24454ac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ addons: - python2.4 - python2.6 script: - - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/layman\.py|/maven_artifact\.py|clustering/consul.*\.py|notification/pushbullet\.py' . + - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/consul.*\.py|notification/pushbullet\.py' . - python2.6 -m compileall -fq . - python2.7 -m compileall -fq . #- ./test-docs.sh extras From d63648d3c2b23928d6689eea272b76702d794acd Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 21 Jul 2015 12:58:18 -0400 Subject: [PATCH 0472/2522] fixed decriptions to be lists --- notification/jabber.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/notification/jabber.py b/notification/jabber.py index 1a19140a83d..606287cd8e4 100644 --- a/notification/jabber.py +++ b/notification/jabber.py @@ -29,15 +29,15 @@ options: user: description: - User as which to connect + - User as which to connect required: true password: description: - password for user to connect + - password for user to connect required: true to: description: - user ID or name of the room, when using room use a slash to indicate your nick. + - user ID or name of the room, when using room use a slash to indicate your nick. required: true msg: description: @@ -46,16 +46,16 @@ default: null host: description: - host to connect, overrides user info + - host to connect, overrides user info required: false port: description: - port to connect to, overrides default + - port to connect to, overrides default required: false default: 5222 encoding: description: - message encoding + - message encoding required: false # informational: requirements for nodes From 26c1bd76c39e895020fda93fc7fb51ded56395b3 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Tue, 21 Jul 2015 11:21:19 -0700 Subject: [PATCH 0473/2522] Fix typo in docs --- cloud/centurylink/clc_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/centurylink/clc_group.py b/cloud/centurylink/clc_group.py index 3ec92612b17..e6e7267f05e 100644 --- a/cloud/centurylink/clc_group.py +++ b/cloud/centurylink/clc_group.py @@ -21,7 +21,7 @@ DOCUMENTATION = ''' module: clc_group -short_desciption: Create/delete Server Groups at Centurylink Cloud +short_description: Create/delete Server Groups at Centurylink Cloud description: - Create or delete Server Groups at Centurylink Centurylink Cloud version_added: "2.0" From 44df5a9be56367f1a9f23bdd21486758cca1fdbb Mon Sep 17 00:00:00 2001 From: Mathias Merscher Date: Tue, 21 Jul 2015 21:08:02 +0200 Subject: [PATCH 0474/2522] ISO-8859-15 locale normalization in locale_gen module Signed-off-by: Mathias Merscher --- system/locale_gen.py | 1 + 1 file changed, 1 insertion(+) diff --git a/system/locale_gen.py b/system/locale_gen.py index 9108cfb53cd..d10fc90ad45 100644 --- a/system/locale_gen.py +++ b/system/locale_gen.py @@ -37,6 +37,7 @@ LOCALE_NORMALIZATION = { ".utf8": ".UTF-8", ".eucjp": ".EUC-JP", + ".iso885915": ".ISO-8859-15", } # =========================================== From cdb36ea0fc022e7ee91c36459463337864576dfc Mon Sep 17 00:00:00 2001 From: Michael Perzel Date: Tue, 21 Jul 2015 17:35:33 -0500 Subject: [PATCH 0475/2522] Cleanup enable/disable logic --- windows/win_scheduled_task.ps1 | 43 +++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/windows/win_scheduled_task.ps1 b/windows/win_scheduled_task.ps1 index d5102572e69..e09544226d5 100644 --- a/windows/win_scheduled_task.ps1 +++ b/windows/win_scheduled_task.ps1 @@ -2,6 +2,7 @@ # This file is part of Ansible # # Copyright 2015, Peter Mounce +# Michael Perzel # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -103,6 +104,20 @@ elseif ($frequency -eq "weekly") try { $task = Get-ScheduledTask -TaskPath "$path" | Where-Object {$_.TaskName -eq "$name"} + + # Correlate task state to enable variable, used to calculate if state needs to be changed + $taskState = $task.State + if ($taskState -eq "Ready"){ + $taskState = $true + } + elseif($taskState -eq "Disabled"){ + $taskState = $false + } + else + { + $taskState = $null + } + $measure = $task | measure if ($measure.count -eq 1 ) { $exists = $true @@ -118,6 +133,7 @@ try { # This should never occur Fail-Json $result "$measure.count scheduled tasks found" } + Set-Attr $result "exists" "$exists" if ($frequency){ @@ -143,39 +159,38 @@ try { Exit-Json $result } + if ($enabled -eq $false){ + $settings = New-ScheduledTaskSettingsSet -Disable + } + else { + $settings = New-ScheduledTaskSettingsSet + } + if ($state -eq "present" -and $exists -eq $false){ $action = New-ScheduledTaskAction -Execute $execute - Register-ScheduledTask -Action $action -Trigger $trigger -TaskName $name -Description $description -TaskPath $path + Register-ScheduledTask -Action $action -Trigger $trigger -TaskName $name -Description $ +description -TaskPath $path -Settings $settings $task = Get-ScheduledTask -TaskName $name Set-Attr $result "msg" "Added new task $name" $result.changed = $true } elseif($state -eq "present" -and $exists -eq $true) { - if ($task.Description -eq $description -and $task.TaskName -eq $name -and $task.TaskPath -eq $path -and $task.Actions.Execute -eq $execute) { + if ($task.Description -eq $description -and $task.TaskName -eq $name -and $task.TaskPat +h -eq $path -and $task.Actions.Execute -eq $execute -and $taskState -eq $enabled) { #No change in the task yet Set-Attr $result "msg" "No change in task $name" } else { Unregister-ScheduledTask -TaskName $name -Confirm:$false $action = New-ScheduledTaskAction -Execute $execute - Register-ScheduledTask -Action $action -Trigger $trigger -TaskName $name -Description $description -TaskPath $path + Register-ScheduledTask -Action $action -Trigger $trigger -TaskName $name -Descripti +on $description -TaskPath $path -Settings $settings $task = Get-ScheduledTask -TaskName $name Set-Attr $result "msg" "Updated task $name" $result.changed = $true } } - if ($state -eq "present" -and $enabled -eq $true -and $task.State -ne "Ready" ){ - $task | Enable-ScheduledTask - Set-Attr $result "msg" "Enabled task $name" - $result.changed = $true - } - elseif ($state -eq "present" -and $enabled -eq $false -and $task.State -ne "Disabled"){ - $task | Disable-ScheduledTask - Set-Attr $result "msg" "Disabled task $name" - $result.changed = $true - } - Exit-Json $result; } catch From 681cdd1c12dd434a3bf7cb43e33f47d0cd039726 Mon Sep 17 00:00:00 2001 From: Michael Perzel Date: Tue, 21 Jul 2015 17:38:27 -0500 Subject: [PATCH 0476/2522] Remove accidental newlines --- windows/win_scheduled_task.ps1 | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/windows/win_scheduled_task.ps1 b/windows/win_scheduled_task.ps1 index e09544226d5..c54be17db46 100644 --- a/windows/win_scheduled_task.ps1 +++ b/windows/win_scheduled_task.ps1 @@ -168,23 +168,20 @@ try { if ($state -eq "present" -and $exists -eq $false){ $action = New-ScheduledTaskAction -Execute $execute - Register-ScheduledTask -Action $action -Trigger $trigger -TaskName $name -Description $ -description -TaskPath $path -Settings $settings + Register-ScheduledTask -Action $action -Trigger $trigger -TaskName $name -Description $description -TaskPath $path -Settings $settings $task = Get-ScheduledTask -TaskName $name Set-Attr $result "msg" "Added new task $name" $result.changed = $true } elseif($state -eq "present" -and $exists -eq $true) { - if ($task.Description -eq $description -and $task.TaskName -eq $name -and $task.TaskPat -h -eq $path -and $task.Actions.Execute -eq $execute -and $taskState -eq $enabled) { + if ($task.Description -eq $description -and $task.TaskName -eq $name -and $task.TaskPath -eq $path -and $task.Actions.Execute -eq $execute -and $taskState -eq $enabled) { #No change in the task yet Set-Attr $result "msg" "No change in task $name" } else { Unregister-ScheduledTask -TaskName $name -Confirm:$false $action = New-ScheduledTaskAction -Execute $execute - Register-ScheduledTask -Action $action -Trigger $trigger -TaskName $name -Descripti -on $description -TaskPath $path -Settings $settings + Register-ScheduledTask -Action $action -Trigger $trigger -TaskName $name -Description $description -TaskPath $path -Settings $settings $task = Get-ScheduledTask -TaskName $name Set-Attr $result "msg" "Updated task $name" $result.changed = $true From e08a2e84da362808f3cdd7d228df03388c34c8b2 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 20 Jul 2015 23:59:38 -0700 Subject: [PATCH 0477/2522] Port layman to fetch_url --- packaging/os/layman.py | 57 ++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/packaging/os/layman.py b/packaging/os/layman.py index c9d6b8ed333..62694ee9118 100644 --- a/packaging/os/layman.py +++ b/packaging/os/layman.py @@ -20,7 +20,6 @@ import shutil from os import path -from urllib2 import Request, urlopen, URLError DOCUMENTATION = ''' --- @@ -52,6 +51,15 @@ required: false default: present choices: [present, absent, updated] + validate_certs: + description: + - If C(no), SSL certificates will not be validated. This should only be + set to C(no) when no other option exists. Prior to 1.9.3 the code + defaulted to C(no). + required: false + default: 'yes' + choices: ['yes', 'no'] + version_added: '1.9.3' ''' EXAMPLES = ''' @@ -89,11 +97,12 @@ def init_layman(config=None): :param config: the layman's configuration to use (optional) ''' - if config is None: config = BareConfig(read_configfile=True, quietness=1) + if config is None: + config = BareConfig(read_configfile=True, quietness=1) return LaymanAPI(config) -def download_url(url, dest): +def download_url(module, url, dest): ''' :param url: the URL to download :param dest: the absolute path of where to save the downloaded content to; @@ -101,14 +110,13 @@ def download_url(url, dest): :raises ModuleError ''' - request = Request(url) - request.add_header('User-agent', USERAGENT) - try: - response = urlopen(request) - except URLError, e: - raise ModuleError("Failed to get %s: %s" % (url, str(e))) - + # Hack to add params in the form that fetch_url expects + module.params['http_agent'] = USERAGENT + response, info = fetch_url(module, url) + if info['status'] != 200: + raise ModuleError("Failed to get %s: %s" % (url, info['msg'])) + try: with open(dest, 'w') as f: shutil.copyfileobj(response, f) @@ -116,7 +124,7 @@ def download_url(url, dest): raise ModuleError("Failed to write: %s" % str(e)) -def install_overlay(name, list_url=None): +def install_overlay(module, name, list_url=None): '''Installs the overlay repository. If not on the central overlays list, then :list_url of an alternative list must be provided. The list will be fetched and saved under ``%(overlay_defs)/%(name.xml)`` (location of the @@ -138,18 +146,20 @@ def install_overlay(name, list_url=None): return False if not layman.is_repo(name): - if not list_url: raise ModuleError("Overlay '%s' is not on the list of known " \ + if not list_url: + raise ModuleError("Overlay '%s' is not on the list of known " \ "overlays and URL of the remote list was not provided." % name) overlay_defs = layman_conf.get_option('overlay_defs') dest = path.join(overlay_defs, name + '.xml') - download_url(list_url, dest) + download_url(module, list_url, dest) # reload config layman = init_layman() - if not layman.add_repos(name): raise ModuleError(layman.get_errors()) + if not layman.add_repos(name): + raise ModuleError(layman.get_errors()) return True @@ -201,11 +211,12 @@ def sync_overlays(): def main(): # define module module = AnsibleModule( - argument_spec = { - 'name': { 'required': True }, - 'list_url': { 'aliases': ['url'] }, - 'state': { 'default': "present", 'choices': ['present', 'absent', 'updated'] }, - } + argument_spec = dict( + name = dict(required=True), + list_url = dict(aliases=['url']), + state = dict(default="present", choices=['present', 'absent', 'updated']), + validate_certs = dict(required=False, default=True, type='bool'), + ) ) if not HAS_LAYMAN_API: @@ -216,12 +227,12 @@ def main(): changed = False try: if state == 'present': - changed = install_overlay(name, url) + changed = install_overlay(module, name, url) elif state == 'updated': if name == 'ALL': sync_overlays() - elif install_overlay(name, url): + elif install_overlay(module, name, url): changed = True else: sync_overlay(name) @@ -236,4 +247,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +from ansible.module_utils.urls import * +if __name__ == '__main__': + main() From aaaff1f1206786bd60e93eeabb305e6335f49456 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 22 Jul 2015 12:01:46 -0700 Subject: [PATCH 0478/2522] Use the correct module param names to pass user/pass --- notification/campfire.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notification/campfire.py b/notification/campfire.py index 62d65015213..ea4df7c0ba8 100644 --- a/notification/campfire.py +++ b/notification/campfire.py @@ -94,8 +94,8 @@ def main(): AGENT = "Ansible/1.2" # Hack to add basic auth username and password the way fetch_url expects - module.params['username'] = token - module.params['password'] = 'X' + module.params['url_username'] = token + module.params['url_password'] = 'X' target_url = '%s/room/%s/speak.xml' % (URI, room) headers = {'Content-Type': 'application/xml', From b9b42411f124dd36383e8440eb3720dec2aa8a99 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 20 Jul 2015 23:54:05 -0700 Subject: [PATCH 0479/2522] Port maven_artifact to fetch_url --- packaging/language/maven_artifact.py | 43 ++++++++++++++-------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/packaging/language/maven_artifact.py b/packaging/language/maven_artifact.py index 3e196dd93a5..55dfbd33de5 100644 --- a/packaging/language/maven_artifact.py +++ b/packaging/language/maven_artifact.py @@ -22,11 +22,9 @@ __author__ = 'cschmidt' from lxml import etree -from urllib2 import Request, urlopen, URLError, HTTPError import os import hashlib import sys -import base64 DOCUMENTATION = ''' --- @@ -69,7 +67,7 @@ required: false default: null password: - description: The passwor to authenticate with to the Maven Repository + description: The password to authenticate with to the Maven Repository required: false default: null dest: @@ -81,6 +79,12 @@ required: true default: present choices: [present,absent] + validate_certs: + description: If C(no), SSL certificates will not be validated. This should only be set to C(no) when no other option exists. + required: false + default: 'yes' + choices: ['yes', 'no'] + version_added: "1.9.3" ''' EXAMPLES = ''' @@ -165,13 +169,12 @@ def parse(input): class MavenDownloader: - def __init__(self, base="http://repo1.maven.org/maven2", username=None, password=None): + def __init__(self, module, base="http://repo1.maven.org/maven2"): + self.module = module if base.endswith("/"): base = base.rstrip("/") self.base = base self.user_agent = "Maven Artifact Downloader/1.0" - self.username = username - self.password = password def _find_latest_version_available(self, artifact): path = "/%s/maven-metadata.xml" % (artifact.path(False)) @@ -201,20 +204,14 @@ def _uri_for_artifact(self, artifact, version=None): return self.base + "/" + artifact.path() + "/" + artifact.artifact_id + "-" + version + "." + artifact.extension def _request(self, url, failmsg, f): - if not self.username: - headers = {"User-Agent": self.user_agent} - else: - headers = { - "User-Agent": self.user_agent, - "Authorization": "Basic " + base64.b64encode(self.username + ":" + self.password) - } - req = Request(url, None, headers) - try: - response = urlopen(req) - except HTTPError, e: - raise ValueError(failmsg + " because of " + str(e) + "for URL " + url) - except URLError, e: - raise ValueError(failmsg + " because of " + str(e) + "for URL " + url) + # Hack to add parameters in the way that fetch_url expects + self.module.params['url_username'] = self.module.params.get('username', '') + self.module.params['url_password'] = self.module.params.get('password', '') + self.module.params['http_agent'] = self.module.params.get('user_agent', None) + + response, info = fetch_url(self.module, url) + if info['status'] != 200: + raise ValueError(failmsg + " because of " + info['msg'] + "for URL " + url) else: return f(response) @@ -294,6 +291,7 @@ def main(): password = dict(default=None), state = dict(default="present", choices=["present","absent"]), # TODO - Implement a "latest" state dest = dict(default=None), + validate_certs = dict(required=False, default=True, type='bool'), ) ) @@ -311,7 +309,7 @@ def main(): if not repository_url: repository_url = "http://repo1.maven.org/maven2" - downloader = MavenDownloader(repository_url, repository_username, repository_password) + downloader = MavenDownloader(module, repository_url, repository_username, repository_password) try: artifact = Artifact(group_id, artifact_id, version, classifier, extension) @@ -343,4 +341,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.urls import * -main() +if __name__ == '__main__': + main() From 6432f13049dca44ed04fabe8703704334fc1fc51 Mon Sep 17 00:00:00 2001 From: Siva Popuri Date: Wed, 22 Jul 2015 14:13:38 -0500 Subject: [PATCH 0480/2522] fixed typos --- cloud/centurylink/clc_aa_policy.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/centurylink/clc_aa_policy.py b/cloud/centurylink/clc_aa_policy.py index 790632ddb72..e05ce1a3e78 100644 --- a/cloud/centurylink/clc_aa_policy.py +++ b/cloud/centurylink/clc_aa_policy.py @@ -55,7 +55,7 @@ - To use this module, it is required to set the below environment variables which enables access to the Centurylink Cloud - CLC_V2_API_USERNAME, the account login id for the centurylink cloud - - CLC_V2_API_PASSWORD, the account passwod for the centurylink cloud + - CLC_V2_API_PASSWORD, the account password for the centurylink cloud - Alternatively, the module accepts the API token and account alias. The API token can be generated using the CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login - CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login @@ -229,7 +229,7 @@ def _get_policies_for_datacenter(self, p): def _create_policy(self, p): """ - Create an Anti Affinnity Policy using the CLC API. + Create an Anti Affinity Policy using the CLC API. :param p: datacenter to create policy in :return: response dictionary from the CLC API. """ @@ -283,7 +283,7 @@ def _ensure_policy_is_absent(self, p): def _ensure_policy_is_present(self, p): """ Ensures that a policy is present - :param p: dictonary of a policy name + :param p: dictionary of a policy name :return: tuple of if an addition occurred and the name of the policy that was added """ changed = False From 180baa2ff1243ea104c3cd271bc35ce0c71809bc Mon Sep 17 00:00:00 2001 From: Siva Popuri Date: Wed, 22 Jul 2015 14:16:40 -0500 Subject: [PATCH 0481/2522] fixed typos --- cloud/centurylink/clc_alert_policy.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cloud/centurylink/clc_alert_policy.py b/cloud/centurylink/clc_alert_policy.py index b4495481ed3..90432bf622d 100644 --- a/cloud/centurylink/clc_alert_policy.py +++ b/cloud/centurylink/clc_alert_policy.py @@ -85,7 +85,7 @@ - To use this module, it is required to set the below environment variables which enables access to the Centurylink Cloud - CLC_V2_API_USERNAME, the account login id for the centurylink cloud - - CLC_V2_API_PASSWORD, the account passwod for the centurylink cloud + - CLC_V2_API_PASSWORD, the account password for the centurylink cloud - Alternatively, the module accepts the API token and account alias. The API token can be generated using the CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login - CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login @@ -261,7 +261,7 @@ def _ensure_alert_policy_is_present(self): """ Ensures that the alert policy is present :return: (changed, policy) - canged: A flag representing if anything is modified + changed: A flag representing if anything is modified policy: the created/updated alert policy """ changed = False @@ -286,7 +286,7 @@ def _ensure_alert_policy_is_absent(self): """ Ensures that the alert policy is absent :return: (changed, None) - canged: A flag representing if anything is modified + changed: A flag representing if anything is modified """ changed = False p = self.module.params @@ -308,10 +308,10 @@ def _ensure_alert_policy_is_absent(self): def _ensure_alert_policy_is_updated(self, alert_policy): """ - Ensures the aliert policy is updated if anything is changed in the alert policy configuration - :param alert_policy: the targetalert policy + Ensures the alert policy is updated if anything is changed in the alert policy configuration + :param alert_policy: the target alert policy :return: (changed, policy) - canged: A flag representing if anything is modified + changed: A flag representing if anything is modified policy: the updated the alert policy """ changed = False @@ -472,7 +472,7 @@ def _get_alert_policy_id(self, module, alert_policy_name): alert_policy_id = id else: return module.fail_json( - msg='mutiple alert policies were found with policy name : %s' % + msg='multiple alert policies were found with policy name : %s' % (alert_policy_name)) return alert_policy_id From 8128d42dcd27f45274db10c0ed2e63a804e71745 Mon Sep 17 00:00:00 2001 From: Peter Mounce Date: Wed, 22 Jul 2015 23:29:49 +0100 Subject: [PATCH 0482/2522] re-submit @adematte's PR #278 against HEAD --- windows/win_updates.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/windows/win_updates.py b/windows/win_updates.py index 4a9f055d8dc..13c57f2b6d1 100644 --- a/windows/win_updates.py +++ b/windows/win_updates.py @@ -41,6 +41,12 @@ - (anything that is a valid update category) default: critical aliases: [] + logPath: + description: + - Where to log command output to + required: false + default: c:\\ansible-playbook.log + aliases: [] author: "Peter Mounce (@petemounce)" ''' From f27a817cbf1827b897e89227553f541f4a19841d Mon Sep 17 00:00:00 2001 From: dohoangkhiem Date: Thu, 23 Jul 2015 10:57:44 +0700 Subject: [PATCH 0483/2522] Remove incorrect copyright | minor refinement of code convention --- cloud/google/gce_tag.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/cloud/google/gce_tag.py b/cloud/google/gce_tag.py index 205f52cb393..186f570b3f1 100644 --- a/cloud/google/gce_tag.py +++ b/cloud/google/gce_tag.py @@ -1,6 +1,4 @@ #!/usr/bin/python -# Copyright 2015 Google Inc. All Rights Reserved. -# # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify @@ -214,15 +212,11 @@ def main(): # add tags to instance. if state == 'present': - results = add_tags(gce, module, instance_name, tags) - changed = results[0] - tags_changed = results[1] + changed, tags_changed = add_tags(gce, module, instance_name, tags) # remove tags from instance if state == 'absent': - results = remove_tags(gce, module, instance_name, tags) - changed = results[0] - tags_changed = results[1] + changed, tags_changed = remove_tags(gce, module, instance_name, tags) module.exit_json(changed=changed, instance_name=instance_name, tags=tags_changed, zone=zone) sys.exit(0) From 85ee695cf36fd804c0763a3a2c724e7e3123d8c6 Mon Sep 17 00:00:00 2001 From: Benno Joy Date: Thu, 23 Jul 2015 15:44:47 +0530 Subject: [PATCH 0484/2522] Module for creating vapp/vm in vcloud or vcd --- cloud/vmware/vca_vapp.py | 747 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 747 insertions(+) create mode 100644 cloud/vmware/vca_vapp.py diff --git a/cloud/vmware/vca_vapp.py b/cloud/vmware/vca_vapp.py new file mode 100644 index 00000000000..1b3aeff93c0 --- /dev/null +++ b/cloud/vmware/vca_vapp.py @@ -0,0 +1,747 @@ +#!/usr/bin/python + +# Copyright (c) 2015 VMware, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + + +DOCUMENTATION = ''' +--- +module: vca_vapp +short_description: create, terminate, start or stop a vm in vca +description: + - Creates or terminates vca vms. +version_added: "2.0" +options: + username: + version_added: "2.0" + description: + - The vca username or email address, if not set the environment variable VCA_USER is checked for the username. + required: false + default: None + password: + version_added: "2.0" + description: + - The vca password, if not set the environment variable VCA_PASS is checked for the password + required: false + default: None + org: + version_added: "2.0" + description: + - The org to login to for creating vapp, mostly set when the service_type is vdc. + required: false + default: None + service_id: + version_added: "2.0" + description: + - The service id in a vchs environment to be used for creating the vapp + required: false + default: None + host: + version_added: "2.0" + description: + - The authentication host to be used when service type is vcd. + required: false + default: None + api_version: + version_added: "2.0" + description: + - The api version to be used with the vca + required: false + default: "5.7" + service_type: + version_added: "2.0" + description: + - The type of service we are authenticating against + required: false + default: vca + choices: [ "vca", "vchs", "vcd" ] + state: + version_added: "2.0" + description: + - if the object should be added or removed + required: false + default: present + choices: [ "present", "absent" ] + catalog_name: + version_added: "2.0" + description: + - The catalog from which the vm template is used. + required: false + default: "Public Catalog" + script: + version_added: "2.0" + description: + - The path to script that gets injected to vm during creation. + required: false + default: "Public Catalog" + template_name: + version_added: "2.0" + description: + - The template name from which the vm should be created. + required: True + network_name: + version_added: "2.0" + description: + - The network name to which the vm should be attached. + required: false + default: 'None' + network_ip: + version_added: "2.0" + description: + - The ip address that should be assigned to vm when the ip assignment type is static + required: false + default: None + network_mode: + version_added: "2.0" + description: + - The network mode in which the ip should be allocated. + required: false + default: pool + choices: [ "pool", "dhcp", 'static' ] + instance_id:: + version_added: "2.0" + description: + - The instance id of the region in vca flavour where the vm should be created + required: false + default: None + wait: + version_added: "2.0" + description: + - If the module should wait if the operation is poweroff or poweron, is better to wait to report the right state. + required: false + default: True + wait_timeout: + version_added: "2.0" + description: + - The wait timeout when wait is set to true + required: false + default: 250 + vdc_name: + version_added: "2.0" + description: + - The name of the vdc where the vm should be created. + required: false + default: None + vm_name: + version_added: "2.0" + description: + - The name of the vm to be created, the vapp is named the same as the vapp name + required: false + default: 'default_ansible_vm1' + vm_cpus: + version_added: "2.0" + description: + - The number if cpus to be added to the vm + required: false + default: None + vm_memory: + version_added: "2.0" + description: + - The amount of memory to be added to vm in megabytes + required: false + default: None + verify_certs: + version_added: "2.0" + description: + - If the certificates of the authentication is to be verified + required: false + default: True + admin_password: + version_added: "2.0" + description: + - The password to be set for admin + required: false + default: None + operation: + version_added: "2.0" + description: + - The operation to be done on the vm + required: false + default: poweroff + choices: [ 'shutdown', 'poweroff', 'poweron', 'reboot', 'reset', 'suspend' ] + +''' + +EXAMPLES = ''' + +#Create a vm in an vca environment. The username password is not set as they are set in environment + +- hosts: localhost + connection: local + tasks: + - vca_vapp: + operation: poweroff + instance_id: 'b15ff1e5-1024-4f55-889f-ea0209726282' + vdc_name: 'benz_ansible' + vm_name: benz + vm_cpus: 2 + vm_memory: 1024 + network_mode: pool + template_name: "CentOS63-32BIT" + admin_password: "Password!123" + network_name: "default-routed-network" + +#Create a vm in a vchs environment. + +- hosts: localhost + connection: local + tasks: + - vca_app: + operation: poweron + service_id: '9-69' + vdc_name: 'Marketing' + service_type: 'vchs' + vm_name: benz + vm_cpus: 1 + script: "/tmp/configure_vm.sh" + catalog_name: "Marketing-Catalog" + template_name: "Marketing-Ubuntu-1204x64" + vm_memory: 512 + network_name: "M49-default-isolated" + +#create a vm in a vdc environment + +- hosts: localhost + connection: local + tasks: + - vca_vapp: + operation: poweron + org: IT20 + host: "mycloud.vmware.net" + api_version: "5.5" + service_type: vcd + vdc_name: 'IT20 Data Center (Beta)' + vm_name: benz + vm_cpus: 1 + catalog_name: "OS Templates" + template_name: "CentOS 6.5 64Bit CLI" + network_mode: pool + + +''' + + +import time, json, xmltodict + +HAS_PYVCLOUD = False +try: + from pyvcloud.vcloudair import VCA + HAS_PYVCLOUD = True +except ImportError: + pass + +SERVICE_MAP = {'vca': 'ondemand', 'vchs': 'subscription', 'vcd': 'vcd'} +LOGIN_HOST = {} +LOGIN_HOST['vca'] = 'vca.vmware.com' +LOGIN_HOST['vchs'] = 'vchs.vmware.com' +VM_COMPARE_KEYS = ['admin_password', 'status', 'cpus', 'memory_mb'] + +def vm_state(val=None): + if val == 8: + return "Power_Off" + elif val == 4: + return "Power_On" + else: + return "Unknown Status" + +def serialize_instances(instance_list): + instances = [] + for i in instance_list: + instances.append(dict(apiUrl=i['apiUrl'], instance_id=i['id'])) + return instances + +def get_catalogs(vca): + catalogs = vca.get_catalogs() + results = [] + for catalog in catalogs: + if catalog.CatalogItems and catalog.CatalogItems.CatalogItem: + for item in catalog.CatalogItems.CatalogItem: + results.append([catalog.name, item.name]) + else: + results.append([catalog.name, '']) + return results + +def vca_login(module=None): + service_type = module.params.get('service_type') + username = module.params.get('username') + password = module.params.get('password') + instance = module.params.get('instance_id') + org = module.params.get('org') + service = module.params.get('service_id') + vdc_name = module.params.get('vdc_name') + version = module.params.get('api_version') + verify = module.params.get('verify_certs') + if not vdc_name: + if service_type == 'vchs': + vdc_name = module.params.get('service_id') + if not org: + if service_type == 'vchs': + if vdc_name: + org = vdc_name + else: + org = service + if service_type == 'vcd': + host = module.params.get('host') + else: + host = LOGIN_HOST[service_type] + + if not username: + if 'VCA_USER' in os.environ: + username = os.environ['VCA_USER'] + if not password: + if 'VCA_PASS' in os.environ: + password = os.environ['VCA_PASS'] + if not username or not password: + module.fail_json(msg = "Either the username or password is not set, please check") + + if service_type == 'vchs': + version = '5.6' + if service_type == 'vcd': + if not version: + version == '5.6' + + + vca = VCA(host=host, username=username, service_type=SERVICE_MAP[service_type], version=version, verify=verify) + + if service_type == 'vca': + if not vca.login(password=password): + module.fail_json(msg = "Login Failed: Please check username or password", error=vca.response.content) + if not vca.login_to_instance(password=password, instance=instance, token=None, org_url=None): + s_json = serialize_instances(vca.instances) + module.fail_json(msg = "Login to Instance failed: Seems like instance_id provided is wrong .. Please check",\ + valid_instances=s_json) + if not vca.login_to_instance(instance=instance, password=None, token=vca.vcloud_session.token, + org_url=vca.vcloud_session.org_url): + module.fail_json(msg = "Error logging into org for the instance", error=vca.response.content) + return vca + + if service_type == 'vchs': + if not vca.login(password=password): + module.fail_json(msg = "Login Failed: Please check username or password", error=vca.response.content) + if not vca.login(token=vca.token): + module.fail_json(msg = "Failed to get the token", error=vca.response.content) + if not vca.login_to_org(service, org): + module.fail_json(msg = "Failed to login to org, Please check the orgname", error=vca.response.content) + return vca + + if service_type == 'vcd': + if not vca.login(password=password, org=org): + module.fail_json(msg = "Login Failed: Please check username or password or host parameters") + if not vca.login(password=password, org=org): + module.fail_json(msg = "Failed to get the token", error=vca.response.content) + if not vca.login(token=vca.token, org=org, org_url=vca.vcloud_session.org_url): + module.fail_json(msg = "Failed to login to org", error=vca.response.content) + return vca + +def set_vm_state(module=None, vca=None, state=None): + wait = module.params.get('wait') + wait_tmout = module.params.get('wait_timeout') + vm_name = module.params.get('vm_name') + vdc_name = module.params.get('vdc_name') + vapp_name = module.params.get('vm_name') + service_type = module.params.get('service_type') + service_id = module.params.get('service_id') + if service_type == 'vchs' and not vdc_name: + vdc_name = service_id + vdc = vca.get_vdc(vdc_name) + if wait: + tmout = time.time() + wait_tmout + while tmout > time.time(): + vapp = vca.get_vapp(vdc, vapp_name) + vms = filter(lambda vm: vm['name'] == vm_name, vapp.get_vms_details()) + vm = vms[0] + if vm['status'] == state: + return True + time.sleep(5) + module.fail_json(msg="Timeut waiting for the vms state to change") + return True + +def vm_details(vdc=None, vapp=None, vca=None): + table = [] + networks = [] + vm_name = vapp + vdc1 = vca.get_vdc(vdc) + if not vdc1: + module.fail_json(msg = "Error getting the vdc, Please check the vdc name") + vap = vca.get_vapp(vdc1, vapp) + if vap: + vms = filter(lambda vm: vm['name'] == vm_name, vap.get_vms_details()) + networks = vap.get_vms_network_info() + if len(networks[0]) >= 1: + table.append(dict(vm_info=vms[0], network_info=networks[0][0])) + else: + table.append(dict(vm_info=vms[0], network_info=networks[0])) + return table + + +def vapp_attach_net(module=None, vca=None, vapp=None): + network_name = module.params.get('network_name') + service_type = module.params.get('service_type') + vdc_name = module.params.get('vdc_name') + mode = module.params.get('network_mode') + if mode.upper() == 'STATIC': + network_ip = module.params.get('network_ip') + else: + network_ip = None + if not vdc_name: + if service_type == 'vchs': + vdc_name = module.params.get('service_id') + nets = filter(lambda n: n.name == network_name, vca.get_networks(vdc_name)) + if len(nets) <= 1: + net_task = vapp.disconnect_vms() + if not net_task: + module.fail_json(msg="Failure in detattaching vms from vnetworks", error=vapp.response.content) + if not vca.block_until_completed(net_task): + module.fail_json(msg="Failure in waiting for detaching vms from vnetworks", error=vapp.response.content) + net_task = vapp.disconnect_from_networks() + if not net_task: + module.fail_json(msg="Failure in detattaching network from vapp", error=vapp.response.content) + if not vca.block_until_completed(net_task): + module.fail_json(msg="Failure in waiting for detaching network from vapp", error=vapp.response.content) + if not network_name: + return True + + net_task = vapp.connect_to_network(nets[0].name, nets[0].href) + if not net_task: + module.fail_json(msg="Failure in attaching network to vapp", error=vapp.response.content) + if not vca.block_until_completed(net_task): + module.fail_json(msg="Failure in waiting for attching network to vapp", error=vapp.response.content) + + net_task = vapp.connect_vms(nets[0].name, connection_index=0, ip_allocation_mode=mode.upper(), ip_address=network_ip ) + if not net_task: + module.fail_json(msg="Failure in attaching network to vm", error=vapp.response.content) + if not vca.block_until_completed(net_task): + module.fail_json(msg="Failure in waiting for attaching network to vm", error=vapp.response.content) + return True + nets = [] + for i in vca.get_networks(vdc_name): + nets.append(i.name) + module.fail_json(msg="Seems like network_name is not found in the vdc, please check Available networks as above", Available_networks=nets) + +def create_vm(vca=None, module=None): + vm_name = module.params.get('vm_name') + operation = module.params.get('operation') + vm_cpus = module.params.get('vm_cpus') + vm_memory = module.params.get('vm_memory') + catalog_name = module.params.get('catalog_name') + template_name = module.params.get('template_name') + vdc_name = module.params.get('vdc_name') + network_name = module.params.get('network_name') + service_type = module.params.get('service_type') + admin_pass = module.params.get('admin_password') + script = module.params.get('script') + vapp_name = vm_name + + if not vdc_name: + if service_type == 'vchs': + vdc_name = module.params.get('service_id') + task = vca.create_vapp(vdc_name, vapp_name, template_name, catalog_name, vm_name=None) + if not task: + catalogs = get_catalogs(vca) + module.fail_json(msg="Error in Creating VM, Please check catalog or template, Available catalogs and templates are as above or check the error field", catalogs=catalogs, errors=vca.response.content) + if not vca.block_until_completed(task): + module.fail_json(msg = "Error in waiting for VM Creation, Please check logs", errors=vca.response.content) + vdc = vca.get_vdc(vdc_name) + if not vdc: + module.fail_json(msg = "Error getting the vdc, Please check the vdc name", errors=vca.response.content) + + vapp = vca.get_vapp(vdc, vapp_name) + task = vapp.modify_vm_name(1, vm_name) + if not task: + module.fail_json(msg="Error in setting the vm_name to vapp_name", errors=vca.response.content) + if not vca.block_until_completed(task): + module.fail_json(msg = "Error in waiting for VM Renaming, Please check logs", errors=vca.response.content) + vapp = vca.get_vapp(vdc, vapp_name) + task = vapp.customize_guest_os(vm_name, computer_name=vm_name) + if not task: + module.fail_json(msg="Error in setting the computer_name to vm_name", errors=vca.response.content) + if not vca.block_until_completed(task): + module.fail_json(msg = "Error in waiting for Computer Renaming, Please check logs", errors=vca.response.content) + + + if network_name: + vapp = vca.get_vapp(vdc, vapp_name) + if not vapp_attach_net(module, vca, vapp): + module.fail_json(msg= "Attaching network to VM fails", errors=vca.response.content) + + if vm_cpus: + vapp = vca.get_vapp(vdc, vapp_name) + task = vapp.modify_vm_cpu(vm_name, vm_cpus) + if not task: + module.fail_json(msg="Error adding cpu", error=vapp.resonse.contents) + if not vca.block_until_completed(task): + module.fail_json(msg="Failure in waiting for modifying cpu", error=vapp.response.content) + + if vm_memory: + vapp = vca.get_vapp(vdc, vapp_name) + task = vapp.modify_vm_memory(vm_name, vm_memory) + if not task: + module.fail_json(msg="Error adding memory", error=vapp.resonse.contents) + if not vca.block_until_completed(task): + module.fail_json(msg="Failure in waiting for modifying memory", error=vapp.response.content) + + if admin_pass: + vapp = vca.get_vapp(vdc, vapp_name) + task = vapp.customize_guest_os(vm_name, customization_script=None, + computer_name=None, admin_password=admin_pass, + reset_password_required=False) + if not task: + module.fail_json(msg="Error adding admin password", error=vapp.resonse.contents) + if not vca.block_until_completed(task): + module.fail_json(msg = "Error in waiting for resettng admin pass, Please check logs", errors=vapp.response.content) + + if script: + vapp = vca.get_vapp(vdc, vapp_name) + if os.path.exists(os.path.expanduser(script)): + file_contents = open(script, 'r') + task = vapp.customize_guest_os(vm_name, customization_script=file_contents.read()) + if not task: + module.fail_json(msg="Error adding customization script", error=vapp.resonse.contents) + if not vca.block_until_completed(task): + module.fail_json(msg = "Error in waiting for customization script, please check logs", errors=vapp.response.content) + task = vapp.force_customization(vm_name, power_on=False ) + if not task: + module.fail_json(msg="Error adding customization script", error=vapp.resonse.contents) + if not vca.block_until_completed(task): + module.fail_json(msg = "Error in waiting for customization script, please check logs", errors=vapp.response.content) + else: + module.fail_json(msg = "The file specified in script paramter is not avaialable or accessible") + + vapp = vca.get_vapp(vdc, vapp_name) + if operation == 'poweron': + vapp.poweron() + set_vm_state(module, vca, state='Powered on') + elif operation == 'poweroff': + vapp.poweroff() + elif operation == 'reboot': + vapp.reboot() + elif operation == 'reset': + vapp.reset() + elif operation == 'suspend': + vapp.suspend() + elif operation == 'shutdown': + vapp.shutdown() + details = vm_details(vdc_name, vapp_name, vca) + module.exit_json(changed=True, msg="VM created", vm_details=details[0]) + +def vapp_reconfigure(module=None, diff=None, vm=None, vca=None, vapp=None, vdc_name=None): + flag = False + vapp_name = module.params.get('vm_name') + vm_name = module.params.get('vm_name') + cpus = module.params.get('vm_cpus') + memory = module.params.get('vm_memory') + admin_pass = module.params.get('admin_password') + + if 'status' in diff: + operation = module.params.get('operation') + if operation == 'poweroff': + vapp.poweroff() + set_vm_state(module, vca, state='Powered off') + flag = True + if 'network' in diff: + vapp_attach_net(module, vca, vapp) + flag = True + if 'cpus' in diff: + task = vapp.modify_vm_cpu(vm_name, cpus) + if not vca.block_until_completed(task): + module.fail_json(msg="Failure in waiting for modifying cpu, might be vm is powered on and doesnt support hotplugging", error=vapp.response.content) + flag = True + if 'memory_mb' in diff: + task = vapp.modify_vm_memory(vm_name, memory) + if not vca.block_until_completed(task): + module.fail_json(msg="Failure in waiting for modifying memory, might be vm is powered on and doesnt support hotplugging", error=vapp.response.content) + flag = True + if 'admin_password' in diff: + task = vapp.customize_guest_os(vm_name, customization_script=None, + computer_name=None, admin_password=admin_pass, + reset_password_required=False) + if not task: + module.fail_json(msg="Error adding admin password", error=vapp.resonse.contents) + if not vca.block_until_completed(task): + module.fail_json(msg = "Error in waiting for resettng admin pass, Please check logs", errors=vapp.response.content) + flag = True + if 'status' in diff: + operation = module.params.get('operation') + if operation == 'poweron': + vapp.poweron() + set_vm_state(module, vca, state='Powered on') + elif operation == 'reboot': + vapp.reboot() + elif operation == 'reset': + vapp.reset() + elif operation == 'suspend': + vapp.suspend() + elif operation == 'shutdown': + vapp.shutdown() + flag = True + details = vm_details(vdc_name, vapp_name, vca) + if flag: + module.exit_json(changed=True, msg="VM reconfigured", vm_details=details[0]) + module.exit_json(changed=False, msg="VM exists as per configuration",\ + vm_details=details[0]) + +def vm_exists(module=None, vapp=None, vca=None, vdc_name=None): + vm_name = module.params.get('vm_name') + operation = module.params.get('operation') + vm_cpus = module.params.get('vm_cpus') + vm_memory = module.params.get('vm_memory') + network_name = module.params.get('network_name') + admin_pass = module.params.get('admin_password') + + d_vm = {} + d_vm['name'] = vm_name + d_vm['cpus'] = vm_cpus + d_vm['memory_mb'] = vm_memory + d_vm['admin_password'] = admin_pass + + if operation == 'poweron': + d_vm['status'] = 'Powered on' + elif operation == 'poweroff': + d_vm['status'] = 'Powered off' + else: + d_vm['status'] = 'operate' + + vms = filter(lambda vm: vm['name'] == vm_name, vapp.get_vms_details()) + if len(vms) > 1: + module.fail_json(msg = "The vapp seems to have more than one vm with same name,\ + currently we only support a single vm deployment") + elif len(vms) == 0: + return False + + else: + vm = vms[0] + diff = [] + for i in VM_COMPARE_KEYS: + if not d_vm[i]: + continue + if vm[i] != d_vm[i]: + diff.append(i) + if len(diff) == 1 and 'status' in diff: + vapp_reconfigure(module, diff, vm, vca, vapp, vdc_name) + networks = vapp.get_vms_network_info() + if not network_name and len(networks) >=1: + if len(networks[0]) >= 1: + if networks[0][0]['network_name'] != 'none': + diff.append('network') + if not network_name: + if len(diff) == 0: + return True + if not networks[0] and network_name: + diff.append('network') + if networks[0]: + if len(networks[0]) >= 1: + if networks[0][0]['network_name'] != network_name: + diff.append('network') + if vm['status'] != 'Powered off': + if operation != 'poweroff' and len(diff) > 0: + module.fail_json(msg="To change any properties of a vm, The vm should be in Powered Off state") + if len(diff) == 0: + return True + else: + vapp_reconfigure(module, diff, vm, vca, vapp, vdc_name) + +def main(): + module = AnsibleModule( + argument_spec=dict( + username = dict(default=None), + password = dict(default=None), + org = dict(default=None), + service_id = dict(default=None), + script = dict(default=None), + host = dict(default=None), + api_version = dict(default='5.7'), + service_type = dict(default='vca', choices=['vchs', 'vca', 'vcd']), + state = dict(default='present', choices = ['present', 'absent']), + catalog_name = dict(default="Public Catalog"), + template_name = dict(default=None, required=True), + network_name = dict(default=None), + network_ip = dict(default=None), + network_mode = dict(default='pool', choices=['dhcp', 'static', 'pool']), + instance_id = dict(default=None), + wait = dict(default=True, type='bool'), + wait_timeout = dict(default=250, type='int'), + vdc_name = dict(default=None), + vm_name = dict(default='default_ansible_vm1'), + vm_cpus = dict(default=None, type='int'), + verify_certs = dict(default=True, type='bool'), + vm_memory = dict(default=None, type='int'), + admin_password = dict(default=None), + operation = dict(default='poweroff', choices=['shutdown', 'poweroff', 'poweron', 'reboot', 'reset', 'suspend']) + ) + ) + + + vdc_name = module.params.get('vdc_name') + vm_name = module.params.get('vm_name') + org = module.params.get('org') + service = module.params.get('service_id') + state = module.params.get('state') + service_type = module.params.get('service_type') + host = module.params.get('host') + instance_id = module.params.get('instance_id') + network_mode = module.params.get('network_mode') + network_ip = module.params.get('network_ip') + vapp_name = vm_name + + if not HAS_PYVCLOUD: + module.fail_json(msg="python module pyvcloud is needed for this module") + + if network_mode.upper() == 'STATIC': + if not network_ip: + module.fail_json(msg="if network_mode is STATIC, network_ip is mandatory") + + if service_type == 'vca': + if not instance_id: + module.fail_json(msg="When service type is vca the instance_id parameter is mandatory") + if not vdc_name: + module.fail_json(msg="When service type is vca the vdc_name parameter is mandatory") + + if service_type == 'vchs': + if not service: + module.fail_json(msg="When service type vchs the service_id parameter is mandatory") + if not org: + org = service + if not vdc_name: + vdc_name = service + if service_type == 'vcd': + if not host: + module.fail_json(msg="When service type is vcd host parameter is mandatory") + + vca = vca_login(module) + vdc = vca.get_vdc(vdc_name) + if not vdc: + module.fail_json(msg = "Error getting the vdc, Please check the vdc name") + vapp = vca.get_vapp(vdc, vapp_name) + if vapp: + if state == 'absent': + task = vca.delete_vapp(vdc_name, vapp_name) + if not vca.block_until_completed(task): + module.fail_json(msg="failure in deleting vapp") + module.exit_json(changed=True, msg="Vapp deleted") + if vm_exists(module, vapp, vca, vdc_name ): + details = vm_details(vdc_name, vapp_name, vca) + module.exit_json(changed=False, msg="vapp exists", vm_details=details[0]) + else: + create_vm(vca, module) + if state == 'absent': + module.exit_json(changed=False, msg="Vapp does not exist") + create_vm(vca, module) + + + +# import module snippets +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From 71e08d6c3f484306eb8937f330482245545c3ddc Mon Sep 17 00:00:00 2001 From: Benno Joy Date: Thu, 23 Jul 2015 15:46:27 +0530 Subject: [PATCH 0485/2522] Module for modifying firewall rules in vcloud or vcd --- cloud/vmware/vca_fw.py | 360 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 360 insertions(+) create mode 100644 cloud/vmware/vca_fw.py diff --git a/cloud/vmware/vca_fw.py b/cloud/vmware/vca_fw.py new file mode 100644 index 00000000000..d2ce398cfa8 --- /dev/null +++ b/cloud/vmware/vca_fw.py @@ -0,0 +1,360 @@ +#!/usr/bin/python + +# Copyright (c) 2015 VMware, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +DOCUMENTATION = ''' +--- +module: vca_fw +short_description: add remove firewall rules in a gateway in a vca +description: + - Adds or removes firewall rules from a gateway in a vca environment +version_added: "2.0" +options: + username: + version_added: "2.0" + description: + - The vca username or email address, if not set the environment variable VCA_USER is checked for the username. + required: false + default: None + password: + version_added: "2.0" + description: + - The vca password, if not set the environment variable VCA_PASS is checked for the password + required: false + default: None + org: + version_added: "2.0" + description: + - The org to login to for creating vapp, mostly set when the service_type is vdc. + required: false + default: None + service_id: + version_added: "2.0" + description: + - The service id in a vchs environment to be used for creating the vapp + required: false + default: None + host: + version_added: "2.0" + description: + - The authentication host to be used when service type is vcd. + required: false + default: None + api_version: + version_added: "2.0" + description: + - The api version to be used with the vca + required: false + default: "5.7" + service_type: + version_added: "2.0" + description: + - The type of service we are authenticating against + required: false + default: vca + choices: [ "vca", "vchs", "vcd" ] + state: + version_added: "2.0" + description: + - if the object should be added or removed + required: false + default: present + choices: [ "present", "absent" ] + verify_certs: + version_added: "2.0" + description: + - If the certificates of the authentication is to be verified + required: false + default: True + vdc_name: + version_added: "2.0" + description: + - The name of the vdc where the gateway is located. + required: false + default: None + gateway_name: + version_added: "2.0" + description: + - The name of the gateway of the vdc where the rule should be added + required: false + default: gateway + fw_rules: + version_added: "2.0" + description: + - A list of firewall rules to be added to the gateway, Please see examples on valid entries + required: True + default: false + +''' + +EXAMPLES = ''' + +#Add a set of firewall rules + +- hosts: localhost + connection: local + tasks: + - vca_fw: + instance_id: 'b15ff1e5-1024-4f55-889f-ea0209726282' + vdc_name: 'benz_ansible' + state: 'absent' + fw_rules: + - description: "ben testing" + source_ip: "Any" + dest_ip: 192.168.2.11 + - description: "ben testing 2" + source_ip: 192.168.2.100 + source_port: "Any" + dest_port: "22" + dest_ip: 192.168.2.13 + is_enable: "true" + enable_logging: "false" + protocol: "Tcp" + policy: "allow" + +''' + + + +import time, json, xmltodict +HAS_PYVCLOUD = False +try: + from pyvcloud.vcloudair import VCA + from pyvcloud.schema.vcd.v1_5.schemas.vcloud.networkType import ProtocolsType + HAS_PYVCLOUD = True +except ImportError: + pass + +SERVICE_MAP = {'vca': 'ondemand', 'vchs': 'subscription', 'vcd': 'vcd'} +LOGIN_HOST = {} +LOGIN_HOST['vca'] = 'vca.vmware.com' +LOGIN_HOST['vchs'] = 'vchs.vmware.com' +VALID_RULE_KEYS = ['policy', 'is_enable', 'enable_logging', 'description', 'dest_ip', 'dest_port', 'source_ip', 'source_port', 'protocol'] + +def vca_login(module=None): + service_type = module.params.get('service_type') + username = module.params.get('username') + password = module.params.get('password') + instance = module.params.get('instance_id') + org = module.params.get('org') + service = module.params.get('service_id') + vdc_name = module.params.get('vdc_name') + version = module.params.get('api_version') + verify = module.params.get('verify_certs') + if not vdc_name: + if service_type == 'vchs': + vdc_name = module.params.get('service_id') + if not org: + if service_type == 'vchs': + if vdc_name: + org = vdc_name + else: + org = service + if service_type == 'vcd': + host = module.params.get('host') + else: + host = LOGIN_HOST[service_type] + + if not username: + if 'VCA_USER' in os.environ: + username = os.environ['VCA_USER'] + if not password: + if 'VCA_PASS' in os.environ: + password = os.environ['VCA_PASS'] + if not username or not password: + module.fail_json(msg = "Either the username or password is not set, please check") + + if service_type == 'vchs': + version = '5.6' + if service_type == 'vcd': + if not version: + version == '5.6' + + + vca = VCA(host=host, username=username, service_type=SERVICE_MAP[service_type], version=version, verify=verify) + + if service_type == 'vca': + if not vca.login(password=password): + module.fail_json(msg = "Login Failed: Please check username or password", error=vca.response.content) + if not vca.login_to_instance(password=password, instance=instance, token=None, org_url=None): + s_json = serialize_instances(vca.instances) + module.fail_json(msg = "Login to Instance failed: Seems like instance_id provided is wrong .. Please check",\ + valid_instances=s_json) + if not vca.login_to_instance(instance=instance, password=None, token=vca.vcloud_session.token, + org_url=vca.vcloud_session.org_url): + module.fail_json(msg = "Error logging into org for the instance", error=vca.response.content) + return vca + + if service_type == 'vchs': + if not vca.login(password=password): + module.fail_json(msg = "Login Failed: Please check username or password", error=vca.response.content) + if not vca.login(token=vca.token): + module.fail_json(msg = "Failed to get the token", error=vca.response.content) + if not vca.login_to_org(service, org): + module.fail_json(msg = "Failed to login to org, Please check the orgname", error=vca.response.content) + return vca + + if service_type == 'vcd': + if not vca.login(password=password, org=org): + module.fail_json(msg = "Login Failed: Please check username or password or host parameters") + if not vca.login(password=password, org=org): + module.fail_json(msg = "Failed to get the token", error=vca.response.content) + if not vca.login(token=vca.token, org=org, org_url=vca.vcloud_session.org_url): + module.fail_json(msg = "Failed to login to org", error=vca.response.content) + return vca + +def validate_fw_rules(module=None, fw_rules=None): + VALID_PROTO = ['Tcp', 'Udp', 'Icmp', 'Any'] + for rule in fw_rules: + if not isinstance(rule, dict): + module.fail_json(msg="Firewall rules must be a list of dictionaries, Please check", valid_keys=VALID_RULE_KEYS) + for k in rule.keys(): + if k not in VALID_RULE_KEYS: + module.fail_json(msg="%s is not a valid key in fw rules, Please check above.." %k, valid_keys=VALID_RULE_KEYS) + rule['dest_port'] = rule.get('dest_port', 'Any') + rule['dest_ip'] = rule.get('dest_ip', 'Any') + rule['source_port'] = rule.get('source_port', 'Any') + rule['source_ip'] = rule.get('source_ip', 'Any') + rule['protocol'] = rule.get('protocol', 'Any') + rule['policy'] = rule.get('policy', 'allow') + rule['is_enable'] = rule.get('is_enable', 'true') + rule['enable_logging'] = rule.get('enable_logging', 'false') + rule['description'] = rule.get('description', 'rule added by Ansible') + if not rule['protocol'] in VALID_PROTO: + module.fail_json(msg="the value in protocol is not valid, valid values are as above", valid_proto=VALID_PROTO) + return fw_rules + +def create_protocol_list(protocol): + plist = [] + plist.append(protocol.get_Tcp()) + plist.append(protocol.get_Any()) + plist.append(protocol.get_Tcp()) + plist.append(protocol.get_Udp()) + plist.append(protocol.get_Icmp()) + plist.append(protocol.get_Other()) + return plist + + +def create_protocols_type(protocol): + all_protocols = {"Tcp": None, "Udp": None, "Icmp": None, "Any": None} + all_protocols[protocol] = True + return ProtocolsType(**all_protocols) + +def main(): + module = AnsibleModule( + argument_spec=dict( + username = dict(default=None), + password = dict(default=None), + org = dict(default=None), + service_id = dict(default=None), + instance_id = dict(default=None), + host = dict(default=None), + api_version = dict(default='5.7'), + service_type = dict(default='vca', choices=['vchs', 'vca', 'vcd']), + state = dict(default='present', choices = ['present', 'absent']), + vdc_name = dict(default=None), + gateway_name = dict(default='gateway'), + fw_rules = dict(required=True, default=None, type='list'), + ) + ) + + + vdc_name = module.params.get('vdc_name') + org = module.params.get('org') + service = module.params.get('service_id') + state = module.params.get('state') + service_type = module.params.get('service_type') + host = module.params.get('host') + instance_id = module.params.get('instance_id') + fw_rules = module.params.get('fw_rules') + gateway_name = module.params.get('gateway_name') + verify_certs = dict(default=True, type='bool'), + + if not HAS_PYVCLOUD: + module.fail_json(msg="python module pyvcloud is needed for this module") + if service_type == 'vca': + if not instance_id: + module.fail_json(msg="When service type is vca the instance_id parameter is mandatory") + if not vdc_name: + module.fail_json(msg="When service type is vca the vdc_name parameter is mandatory") + + if service_type == 'vchs': + if not service: + module.fail_json(msg="When service type vchs the service_id parameter is mandatory") + if not org: + org = service + if not vdc_name: + vdc_name = service + if service_type == 'vcd': + if not host: + module.fail_json(msg="When service type is vcd host parameter is mandatory") + + vca = vca_login(module) + vdc = vca.get_vdc(vdc_name) + if not vdc: + module.fail_json(msg = "Error getting the vdc, Please check the vdc name") + + mod_rules = validate_fw_rules(module, fw_rules) + gateway = vca.get_gateway(vdc_name, gateway_name) + if not gateway: + module.fail_json(msg="Not able to find the gateway %s, please check the gateway_name param" %gateway_name) + rules = gateway.get_fw_rules() + existing_rules = [] + del_rules = [] + for rule in rules: + current_trait = (create_protocol_list(rule.get_Protocols()), + rule.get_DestinationPortRange(), + rule.get_DestinationIp(), + rule.get_SourcePortRange(), + rule.get_SourceIp()) + for idx, val in enumerate(mod_rules): + trait = (create_protocol_list(create_protocols_type(val['protocol'])), + val['dest_port'], val['dest_ip'], val['source_port'], val['source_ip']) + if current_trait == trait: + del_rules.append(mod_rules[idx]) + mod_rules.pop(idx) + existing_rules.append(current_trait) + + if state == 'absent': + if len(del_rules) < 1: + module.exit_json(changed=False, msg="Nothing to delete", delete_rules=mod_rules) + else: + for i in del_rules: + gateway.delete_fw_rule(i['protocol'], i['dest_port'], i['dest_ip'], i['source_port'], i['source_ip']) + task = gateway.save_services_configuration() + if not task: + module.fail_json(msg="Unable to Delete Rule, please check above error", error=gateway.response.content) + if not vca.block_until_completed(task): + module.fail_json(msg="Error while waiting to remove Rule, please check above error", error=gateway.response.content) + module.exit_json(changed=True, msg="Rules Deleted", deleted_rules=del_rules) + + if len(mod_rules) < 1: + module.exit_json(changed=False, rules=existing_rules) + if len(mod_rules) >= 1: + for i in mod_rules: + gateway.add_fw_rule(i['is_enable'], i['description'], i['policy'], i['protocol'], i['dest_port'], i['dest_ip'], + i['source_port'], i['source_ip'], i['enable_logging']) + task = gateway.save_services_configuration() + if not task: + module.fail_json(msg="Unable to Add Rule, please check above error", error=gateway.response.content) + if not vca.block_until_completed(task): + module.fail_json(msg="Failure in waiting for adding firewall rule", error=gateway.response.content) + module.exit_json(changed=True, rules=mod_rules) + + +# import module snippets +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From ff2009136f51e177e9593c121fbd6f2cf8945b64 Mon Sep 17 00:00:00 2001 From: Benno Joy Date: Thu, 23 Jul 2015 15:47:32 +0530 Subject: [PATCH 0486/2522] Module for modifying NAT rules in vcloud or vcd --- cloud/vmware/vca_nat.py | 384 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 cloud/vmware/vca_nat.py diff --git a/cloud/vmware/vca_nat.py b/cloud/vmware/vca_nat.py new file mode 100644 index 00000000000..bde72dc07ac --- /dev/null +++ b/cloud/vmware/vca_nat.py @@ -0,0 +1,384 @@ +#!/usr/bin/python + +# Copyright (c) 2015 VMware, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +DOCUMENTATION = ''' +--- +module: vca_nat +short_description: add remove nat rules in a gateway in a vca +description: + - Adds or removes nat rules from a gateway in a vca environment +version_added: "2.0" +options: + username: + version_added: "2.0" + description: + - The vca username or email address, if not set the environment variable VCA_USER is checked for the username. + required: false + default: None + password: + version_added: "2.0" + description: + - The vca password, if not set the environment variable VCA_PASS is checked for the password + required: false + default: None + org: + version_added: "2.0" + description: + - The org to login to for creating vapp, mostly set when the service_type is vdc. + required: false + default: None + service_id: + version_added: "2.0" + description: + - The service id in a vchs environment to be used for creating the vapp + required: false + default: None + host: + version_added: "2.0" + description: + - The authentication host to be used when service type is vcd. + required: false + default: None + api_version: + version_added: "2.0" + description: + - The api version to be used with the vca + required: false + default: "5.7" + service_type: + version_added: "2.0" + description: + - The type of service we are authenticating against + required: false + default: vca + choices: [ "vca", "vchs", "vcd" ] + state: + version_added: "2.0" + description: + - if the object should be added or removed + required: false + default: present + choices: [ "present", "absent" ] + verify_certs: + version_added: "2.0" + description: + - If the certificates of the authentication is to be verified + required: false + default: True + vdc_name: + version_added: "2.0" + description: + - The name of the vdc where the gateway is located. + required: false + default: None + gateway_name: + version_added: "2.0" + description: + - The name of the gateway of the vdc where the rule should be added + required: false + default: gateway + purge_rules: + version_added: "2.0" + description: + - If set to true, it will delete all rules in the gateway that are not given as paramter to this module. + required: false + default: false + nat_rules: + version_added: "2.0" + description: + - A list of rules to be added to the gateway, Please see examples on valid entries + required: True + default: false + + +''' + +EXAMPLES = ''' + +#An example for a source nat + +- hosts: localhost + connection: local + tasks: + - vca_nat: + instance_id: 'b15ff1e5-1024-4f55-889f-ea0209726282' + vdc_name: 'benz_ansible' + state: 'present' + nat_rules: + - rule_type: SNAT + original_ip: 192.168.2.10 + translated_ip: 107.189.95.208 + +#example for a DNAT +- hosts: localhost + connection: local + tasks: + - vca_nat: + instance_id: 'b15ff1e5-1024-4f55-889f-ea0209726282' + vdc_name: 'benz_ansible' + state: 'present' + nat_rules: + - rule_type: DNAT + original_ip: 107.189.95.208 + original_port: 22 + translated_ip: 192.168.2.10 + translated_port: 22 + +''' + +import time, json, xmltodict + +HAS_PYVCLOUD = False +try: + from pyvcloud.vcloudair import VCA + HAS_PYVCLOUD = True +except ImportError: + pass + +SERVICE_MAP = {'vca': 'ondemand', 'vchs': 'subscription', 'vcd': 'vcd'} +LOGIN_HOST = {} +LOGIN_HOST['vca'] = 'vca.vmware.com' +LOGIN_HOST['vchs'] = 'vchs.vmware.com' +VALID_RULE_KEYS = ['rule_type', 'original_ip', 'original_port', 'translated_ip', 'translated_port', 'protocol'] + +def vca_login(module=None): + service_type = module.params.get('service_type') + username = module.params.get('username') + password = module.params.get('password') + instance = module.params.get('instance_id') + org = module.params.get('org') + service = module.params.get('service_id') + vdc_name = module.params.get('vdc_name') + version = module.params.get('api_version') + verify = module.params.get('verify_certs') + if not vdc_name: + if service_type == 'vchs': + vdc_name = module.params.get('service_id') + if not org: + if service_type == 'vchs': + if vdc_name: + org = vdc_name + else: + org = service + if service_type == 'vcd': + host = module.params.get('host') + else: + host = LOGIN_HOST[service_type] + + if not username: + if 'VCA_USER' in os.environ: + username = os.environ['VCA_USER'] + if not password: + if 'VCA_PASS' in os.environ: + password = os.environ['VCA_PASS'] + if not username or not password: + module.fail_json(msg = "Either the username or password is not set, please check") + + if service_type == 'vchs': + version = '5.6' + if service_type == 'vcd': + if not version: + version == '5.6' + + + vca = VCA(host=host, username=username, service_type=SERVICE_MAP[service_type], version=version, verify=verify) + + if service_type == 'vca': + if not vca.login(password=password): + module.fail_json(msg = "Login Failed: Please check username or password", error=vca.response.content) + if not vca.login_to_instance(password=password, instance=instance, token=None, org_url=None): + s_json = serialize_instances(vca.instances) + module.fail_json(msg = "Login to Instance failed: Seems like instance_id provided is wrong .. Please check",\ + valid_instances=s_json) + if not vca.login_to_instance(instance=instance, password=None, token=vca.vcloud_session.token, + org_url=vca.vcloud_session.org_url): + module.fail_json(msg = "Error logging into org for the instance", error=vca.response.content) + return vca + + if service_type == 'vchs': + if not vca.login(password=password): + module.fail_json(msg = "Login Failed: Please check username or password", error=vca.response.content) + if not vca.login(token=vca.token): + module.fail_json(msg = "Failed to get the token", error=vca.response.content) + if not vca.login_to_org(service, org): + module.fail_json(msg = "Failed to login to org, Please check the orgname", error=vca.response.content) + return vca + + if service_type == 'vcd': + if not vca.login(password=password, org=org): + module.fail_json(msg = "Login Failed: Please check username or password or host parameters") + if not vca.login(password=password, org=org): + module.fail_json(msg = "Failed to get the token", error=vca.response.content) + if not vca.login(token=vca.token, org=org, org_url=vca.vcloud_session.org_url): + module.fail_json(msg = "Failed to login to org", error=vca.response.content) + return vca + +def validate_nat_rules(module=None, nat_rules=None): + for rule in nat_rules: + if not isinstance(rule, dict): + module.fail_json(msg="nat rules must be a list of dictionaries, Please check", valid_keys=VALID_RULE_KEYS) + for k in rule.keys(): + if k not in VALID_RULE_KEYS: + module.fail_json(msg="%s is not a valid key in nat rules, Please check above.." %k, valid_keys=VALID_RULE_KEYS) + rule['original_port'] = rule.get('original_port', 'any') + rule['original_ip'] = rule.get('original_ip', 'any') + rule['translated_ip'] = rule.get('translated_ip', 'any') + rule['translated_port'] = rule.get('translated_port', 'any') + rule['protocol'] = rule.get('protocol', 'any') + rule['rule_type'] = rule.get('rule_type', 'DNAT') + return nat_rules + + +def nat_rules_to_dict(natRules): + result = [] + for natRule in natRules: + ruleId = natRule.get_Id() + enable = natRule.get_IsEnabled() + ruleType = natRule.get_RuleType() + gatewayNatRule = natRule.get_GatewayNatRule() + originalIp = gatewayNatRule.get_OriginalIp() + originalPort = gatewayNatRule.get_OriginalPort() + translatedIp = gatewayNatRule.get_TranslatedIp() + translatedPort = gatewayNatRule.get_TranslatedPort() + protocol = gatewayNatRule.get_Protocol() + interface = gatewayNatRule.get_Interface().get_name() + result.append(dict(rule_type=ruleType, original_ip=originalIp, original_port="any" if not originalPort else originalPort, translated_ip=translatedIp, translated_port="any" if not translatedPort else translatedPort, + protocol="any" if not protocol else protocol)) + return result + + +def main(): + module = AnsibleModule( + argument_spec=dict( + username = dict(default=None), + password = dict(default=None), + org = dict(default=None), + service_id = dict(default=None), + instance_id = dict(default=None), + host = dict(default=None), + api_version = dict(default='5.7'), + service_type = dict(default='vca', choices=['vchs', 'vca', 'vcd']), + state = dict(default='present', choices = ['present', 'absent']), + vdc_name = dict(default=None), + gateway_name = dict(default='gateway'), + nat_rules = dict(required=True, default=None, type='list'), + purge_rules = dict(default=False), + ) + ) + + + vdc_name = module.params.get('vdc_name') + org = module.params.get('org') + service = module.params.get('service_id') + state = module.params.get('state') + service_type = module.params.get('service_type') + host = module.params.get('host') + instance_id = module.params.get('instance_id') + nat_rules = module.params.get('nat_rules') + gateway_name = module.params.get('gateway_name') + purge_rules = module.params.get('purge_rules') + verify_certs = dict(default=True, type='bool'), + + if not HAS_PYVCLOUD: + module.fail_json(msg="python module pyvcloud is needed for this module") + if service_type == 'vca': + if not instance_id: + module.fail_json(msg="When service type is vca the instance_id parameter is mandatory") + if not vdc_name: + module.fail_json(msg="When service type is vca the vdc_name parameter is mandatory") + + if service_type == 'vchs': + if not service: + module.fail_json(msg="When service type vchs the service_id parameter is mandatory") + if not org: + org = service + if not vdc_name: + vdc_name = service + if service_type == 'vcd': + if not host: + module.fail_json(msg="When service type is vcd host parameter is mandatory") + + vca = vca_login(module) + vdc = vca.get_vdc(vdc_name) + if not vdc: + module.fail_json(msg = "Error getting the vdc, Please check the vdc name") + + mod_rules = validate_nat_rules(module, nat_rules) + gateway = vca.get_gateway(vdc_name, gateway_name) + if not gateway: + module.fail_json(msg="Not able to find the gateway %s, please check the gateway_name param" %gateway_name) + rules = gateway.get_nat_rules() + cur_rules = nat_rules_to_dict(rules) + delete_cur_rule = [] + delete_rules = [] + for rule in cur_rules: + match = False + for idx, val in enumerate(mod_rules): + match = False + if cmp(rule, val) == 0: + delete_cur_rule.append(val) + mod_rules.pop(idx) + match = True + if not match: + delete_rules.append(rule) + if state == 'absent': + if purge_rules: + if not gateway.del_all_nat_rules(): + module.fail_json(msg="Error deleting all rules") + module.exit_json(changed=True, msg="Removed all rules") + if len(delete_cur_rule) < 1: + module.exit_json(changed=False, msg="No rules to be removed", rules=cur_rules) + else: + for i in delete_cur_rule: + gateway.del_nat_rule(i['rule_type'], i['original_ip'],\ + i['original_port'], i['translated_ip'], i['translated_port'], i['protocol']) + task = gateway.save_services_configuration() + if not task: + module.fail_json(msg="Unable to delete Rule, please check above error", error=gateway.response.content) + if not vca.block_until_completed(task): + module.fail_json(msg="Failure in waiting for removing network rule", error=gateway.response.content) + module.exit_json(changed=True, msg="The rules have been deleted", rules=delete_cur_rule) + changed = False + if len(mod_rules) < 1: + if not purge_rules: + module.exit_json(changed=False, msg="all rules are available", rules=cur_rules) + for i in mod_rules: + gateway.add_nat_rule(i['rule_type'], i['original_ip'], i['original_port'],\ + i['translated_ip'], i['translated_port'], i['protocol']) + task = gateway.save_services_configuration() + if not task: + module.fail_json(msg="Unable to add rule, please check above error", rules=mod_rules, error=gateway.response.content) + if not vca.block_until_completed(task): + module.fail_json(msg="Failure in waiting for adding network rule", error=gateway.response.content) + if purge_rules: + if len(delete_rules) < 1 and len(mod_rules) < 1: + module.exit_json(changed=False, rules=cur_rules) + for i in delete_rules: + gateway.del_nat_rule(i['rule_type'], i['original_ip'],\ + i['original_port'], i['translated_ip'], i['translated_port'], i['protocol']) + task = gateway.save_services_configuration() + if not task: + module.fail_json(msg="Unable to delete Rule, please check above error", error=gateway.response.content) + if not vca.block_until_completed(task): + module.fail_json(msg="Failure in waiting for removing network rule", error=gateway.response.content) + + module.exit_json(changed=True, rules_added=mod_rules) + + +# import module snippets +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From 2d675917a865a9be5bf19699ddebc58c86acb517 Mon Sep 17 00:00:00 2001 From: Siva Popuri Date: Thu, 23 Jul 2015 08:56:51 -0500 Subject: [PATCH 0487/2522] removed empty aliases from doc string --- cloud/centurylink/clc_alert_policy.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/cloud/centurylink/clc_alert_policy.py b/cloud/centurylink/clc_alert_policy.py index 90432bf622d..ae1ed4d7b55 100644 --- a/cloud/centurylink/clc_alert_policy.py +++ b/cloud/centurylink/clc_alert_policy.py @@ -35,19 +35,17 @@ - The name of the alert policy. This is mutually exclusive with id required: False default: None - aliases: [] id: description: - The alert policy id. This is mutually exclusive with name required: False default: None - aliases: [] alert_recipients: description: - A list of recipient email ids to notify the alert. This is required for state 'present' required: False - aliases: [] + default: None metric: description: - The metric on which to measure the condition that will trigger the alert. @@ -55,14 +53,12 @@ required: False default: None choices: ['cpu','memory','disk'] - aliases: [] duration: description: - The length of time in minutes that the condition must exceed the threshold. This is required for state 'present' required: False default: None - aliases: [] threshold: description: - The threshold that will trigger the alert when the metric equals or exceeds it. @@ -70,7 +66,6 @@ This number represents a percentage and must be a value between 5.0 - 95.0 that is a multiple of 5.0 required: False default: None - aliases: [] state: description: - Whether to create or delete the policy. From f7bd44ecd775d62cb65fcea1d4d4b9328d9e077f Mon Sep 17 00:00:00 2001 From: Siva Popuri Date: Thu, 23 Jul 2015 09:27:14 -0500 Subject: [PATCH 0488/2522] clc_blueprint_package: module for installing blueprint packages for Centurylink Cloud servers --- cloud/centurylink/clc_blueprint_package.py | 287 +++++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 cloud/centurylink/clc_blueprint_package.py diff --git a/cloud/centurylink/clc_blueprint_package.py b/cloud/centurylink/clc_blueprint_package.py new file mode 100644 index 00000000000..8feb106dbd1 --- /dev/null +++ b/cloud/centurylink/clc_blueprint_package.py @@ -0,0 +1,287 @@ +#!/usr/bin/python + +# +# Copyright (c) 2015 CenturyLink +# +# This file is part of Ansible. +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see +# + +DOCUMENTATION = ''' +module: clc_blueprint_package +short_description: deploys a blue print package on a set of servers in CenturyLink Cloud. +description: + - An Ansible module to deploy blue print package on a set of servers in CenturyLink Cloud. +version_added: "2.0" +options: + server_ids: + description: + - A list of server Ids to deploy the blue print package. + required: True + package_id: + description: + - The package id of the blue print. + required: True + package_params: + description: + - The dictionary of arguments required to deploy the blue print. + default: {} + required: False + state: + description: + - Whether to install or un-install the package. Currently it supports only "present" for install action. + required: False + default: present + choices: ['present'] + wait: + description: + - Whether to wait for the tasks to finish before returning. + choices: [ True, False ] + default: True + required: False +requirements: + - python = 2.7 + - requests >= 2.5.0 + - clc-sdk +notes: + - To use this module, it is required to set the below environment variables which enables access to the + Centurylink Cloud + - CLC_V2_API_USERNAME, the account login id for the centurylink cloud + - CLC_V2_API_PASSWORD, the account password for the centurylink cloud + - Alternatively, the module accepts the API token and account alias. The API token can be generated using the + CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login + - CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login + - CLC_ACCT_ALIAS, the account alias associated with the centurylink cloud + - Users can set CLC_V2_API_URL to specify an endpoint for pointing to a different CLC environment. +''' + +EXAMPLES = ''' +# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples + +- name: Deploy package + clc_blueprint_package: + server_ids: + - UC1WFSDANS01 + - UC1WFSDANS02 + package_id: 77abb844-579d-478d-3955-c69ab4a7ba1a + package_params: {} +''' + +__version__ = '${version}' + +from distutils.version import LooseVersion + +try: + import requests +except ImportError: + REQUESTS_FOUND = False +else: + REQUESTS_FOUND = True + +# +# Requires the clc-python-sdk. +# sudo pip install clc-sdk +# +try: + import clc as clc_sdk + from clc import CLCException +except ImportError: + CLC_FOUND = False + clc_sdk = None +else: + CLC_FOUND = True + + +class ClcBlueprintPackage(): + + clc = clc_sdk + module = None + + def __init__(self, module): + """ + Construct module + """ + self.module = module + if not CLC_FOUND: + self.module.fail_json( + msg='clc-python-sdk required for this module') + if not REQUESTS_FOUND: + self.module.fail_json( + msg='requests library is required for this module') + if requests.__version__ and LooseVersion(requests.__version__) < LooseVersion('2.5.0'): + self.module.fail_json( + msg='requests library version should be >= 2.5.0') + + self._set_user_agent(self.clc) + + def process_request(self): + """ + Process the request - Main Code Path + :return: Returns with either an exit_json or fail_json + """ + p = self.module.params + + self._set_clc_credentials_from_env() + + server_ids = p['server_ids'] + package_id = p['package_id'] + package_params = p['package_params'] + state = p['state'] + if state == 'present': + changed, changed_server_ids, request_list = self.ensure_package_installed( + server_ids, package_id, package_params) + self._wait_for_requests_to_complete(request_list) + self.module.exit_json(changed=changed, server_ids=changed_server_ids) + + @staticmethod + def define_argument_spec(): + """ + This function defines the dictionary object required for + package module + :return: the package dictionary object + """ + argument_spec = dict( + server_ids=dict(type='list', required=True), + package_id=dict(required=True), + package_params=dict(type='dict', default={}), + wait=dict(default=True), + state=dict(default='present', choices=['present']) + ) + return argument_spec + + def ensure_package_installed(self, server_ids, package_id, package_params): + """ + Ensure the package is installed in the given list of servers + :param server_ids: the server list where the package needs to be installed + :param package_id: the blueprint package id + :param package_params: the package arguments + :return: (changed, server_ids, request_list) + changed: A flag indicating if a change was made + server_ids: The list of servers modified + request_list: The list of request objects from clc-sdk + """ + changed = False + request_list = [] + servers = self._get_servers_from_clc( + server_ids, + 'Failed to get servers from CLC') + for server in servers: + if not self.module.check_mode: + request = self.clc_install_package( + server, + package_id, + package_params) + request_list.append(request) + changed = True + return changed, server_ids, request_list + + def clc_install_package(self, server, package_id, package_params): + """ + Install the package to a given clc server + :param server: The server object where the package needs to be installed + :param package_id: The blue print package id + :param package_params: the required argument dict for the package installation + :return: The result object from the CLC API call + """ + result = None + try: + result = server.ExecutePackage( + package_id=package_id, + parameters=package_params) + except CLCException as ex: + self.module.fail_json(msg='Failed to install package : {0} to server {1}. {2}'.format( + package_id, server.id, ex.response_text + )) + return result + + def _wait_for_requests_to_complete(self, request_lst): + """ + Waits until the CLC requests are complete if the wait argument is True + :param request_lst: The list of CLC request objects + :return: none + """ + if not self.module.params['wait']: + return + for request in request_lst: + request.WaitUntilComplete() + for request_details in request.requests: + if request_details.Status() != 'succeeded': + self.module.fail_json( + msg='Unable to process package install request') + + def _get_servers_from_clc(self, server_list, message): + """ + Internal function to fetch list of CLC server objects from a list of server ids + :param the list server ids + :return the list of CLC server objects + """ + try: + return self.clc.v2.Servers(server_list).servers + except CLCException as ex: + self.module.fail_json(msg=message + ': %s' % ex) + + def _set_clc_credentials_from_env(self): + """ + Set the CLC Credentials on the sdk by reading environment variables + :return: none + """ + env = os.environ + v2_api_token = env.get('CLC_V2_API_TOKEN', False) + v2_api_username = env.get('CLC_V2_API_USERNAME', False) + v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) + clc_alias = env.get('CLC_ACCT_ALIAS', False) + api_url = env.get('CLC_V2_API_URL', False) + + if api_url: + self.clc.defaults.ENDPOINT_URL_V2 = api_url + + if v2_api_token and clc_alias: + self.clc._LOGIN_TOKEN_V2 = v2_api_token + self.clc._V2_ENABLED = True + self.clc.ALIAS = clc_alias + elif v2_api_username and v2_api_passwd: + self.clc.v2.SetCredentials( + api_username=v2_api_username, + api_passwd=v2_api_passwd) + else: + return self.module.fail_json( + msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " + "environment variables") + + @staticmethod + def _set_user_agent(clc): + if hasattr(clc, 'SetRequestsSession'): + agent_string = "ClcAnsibleModule/" + __version__ + ses = requests.Session() + ses.headers.update({"Api-Client": agent_string}) + ses.headers['User-Agent'] += " " + agent_string + clc.SetRequestsSession(ses) + + +def main(): + """ + Main function + :return: None + """ + module = AnsibleModule( + argument_spec=ClcBlueprintPackage.define_argument_spec(), + supports_check_mode=True + ) + clc_blueprint_package = ClcBlueprintPackage(module) + clc_blueprint_package.process_request() + +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From 86409ca2d475ca2b15a547718b90f29741f14b36 Mon Sep 17 00:00:00 2001 From: Siva Popuri Date: Thu, 23 Jul 2015 11:28:25 -0500 Subject: [PATCH 0489/2522] minor documentation change --- cloud/centurylink/clc_blueprint_package.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cloud/centurylink/clc_blueprint_package.py b/cloud/centurylink/clc_blueprint_package.py index 8feb106dbd1..6b06b741287 100644 --- a/cloud/centurylink/clc_blueprint_package.py +++ b/cloud/centurylink/clc_blueprint_package.py @@ -120,7 +120,8 @@ def __init__(self, module): if not REQUESTS_FOUND: self.module.fail_json( msg='requests library is required for this module') - if requests.__version__ and LooseVersion(requests.__version__) < LooseVersion('2.5.0'): + if requests.__version__ and LooseVersion( + requests.__version__) < LooseVersion('2.5.0'): self.module.fail_json( msg='requests library version should be >= 2.5.0') @@ -224,7 +225,8 @@ def _wait_for_requests_to_complete(self, request_lst): def _get_servers_from_clc(self, server_list, message): """ Internal function to fetch list of CLC server objects from a list of server ids - :param the list server ids + :param server_list: the list of server ids + :param message: the error message to raise if there is any error :return the list of CLC server objects """ try: From a3b703a0f6a2fea44d04dc041d57336412655daa Mon Sep 17 00:00:00 2001 From: Siva Popuri Date: Thu, 23 Jul 2015 11:58:01 -0500 Subject: [PATCH 0490/2522] Ansible module for managing load balancers in Centurylink Cloud --- cloud/centurylink/clc_loadbalancer.py | 898 ++++++++++++++++++++++++++ 1 file changed, 898 insertions(+) create mode 100644 cloud/centurylink/clc_loadbalancer.py diff --git a/cloud/centurylink/clc_loadbalancer.py b/cloud/centurylink/clc_loadbalancer.py new file mode 100644 index 00000000000..5847c5b1c00 --- /dev/null +++ b/cloud/centurylink/clc_loadbalancer.py @@ -0,0 +1,898 @@ +#!/usr/bin/python + +# +# Copyright (c) 2015 CenturyLink +# +# This file is part of Ansible. +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see +# + +DOCUMENTATION = ''' +module: clc_loadbalancer +short_description: Create, Delete shared loadbalancers in CenturyLink Cloud. +description: + - An Ansible module to Create, Delete shared loadbalancers in CenturyLink Cloud. +version_added: "2.0" +options: + name: + description: + - The name of the loadbalancer + required: True + description: + description: + - A description for the loadbalancer + required: False + default: None + alias: + description: + - The alias of your CLC Account + required: True + location: + description: + - The location of the datacenter where the load balancer resides in + required: True + method: + description: + -The balancing method for the load balancer pool + required: False + default: None + choices: ['leastConnection', 'roundRobin'] + persistence: + description: + - The persistence method for the load balancer + required: False + default: None + choices: ['standard', 'sticky'] + port: + description: + - Port to configure on the public-facing side of the load balancer pool + required: False + default: None + choices: [80, 443] + nodes: + description: + - A list of nodes that needs to be added to the load balancer pool + required: False + default: [] + status: + description: + - The status of the loadbalancer + required: False + default: enabled + choices: ['enabled', 'disabled'] + state: + description: + - Whether to create or delete the load balancer pool + required: False + default: present + choices: ['present', 'absent', 'port_absent', 'nodes_present', 'nodes_absent'] +''' + +EXAMPLES = ''' +# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples +- name: Create Loadbalancer + hosts: localhost + connection: local + tasks: + - name: Actually Create things + clc_loadbalancer: + name: test + description: test + alias: TEST + location: WA1 + port: 443 + nodes: + - { 'ipAddress': '10.11.22.123', 'privatePort': 80 } + state: present + +- name: Add node to an existing loadbalancer pool + hosts: localhost + connection: local + tasks: + - name: Actually Create things + clc_loadbalancer: + name: test + description: test + alias: TEST + location: WA1 + port: 443 + nodes: + - { 'ipAddress': '10.11.22.234', 'privatePort': 80 } + state: nodes_present + +- name: Remove node from an existing loadbalancer pool + hosts: localhost + connection: local + tasks: + - name: Actually Create things + clc_loadbalancer: + name: test + description: test + alias: TEST + location: WA1 + port: 443 + nodes: + - { 'ipAddress': '10.11.22.234', 'privatePort': 80 } + state: nodes_absent + +- name: Delete LoadbalancerPool + hosts: localhost + connection: local + tasks: + - name: Actually Delete things + clc_loadbalancer: + name: test + description: test + alias: TEST + location: WA1 + port: 443 + nodes: + - { 'ipAddress': '10.11.22.123', 'privatePort': 80 } + state: port_absent + +- name: Delete Loadbalancer + hosts: localhost + connection: local + tasks: + - name: Actually Delete things + clc_loadbalancer: + name: test + description: test + alias: TEST + location: WA1 + port: 443 + nodes: + - { 'ipAddress': '10.11.22.123', 'privatePort': 80 } + state: absent +requirements: + - python = 2.7 + - requests >= 2.5.0 + - clc-sdk +notes: + - To use this module, it is required to set the below environment variables which enables access to the + Centurylink Cloud + - CLC_V2_API_USERNAME, the account login id for the centurylink cloud + - CLC_V2_API_PASSWORD, the account password for the centurylink cloud + - Alternatively, the module accepts the API token and account alias. The API token can be generated using the + CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login + - CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login + - CLC_ACCT_ALIAS, the account alias associated with the centurylink cloud + - Users can set CLC_V2_API_URL to specify an endpoint for pointing to a different CLC environment. +''' + +__version__ = '${version}' + +from time import sleep +from distutils.version import LooseVersion + +try: + import requests +except ImportError: + REQUESTS_FOUND = False +else: + REQUESTS_FOUND = True + +# +# Requires the clc-python-sdk. +# sudo pip install clc-sdk +# +try: + import clc as clc_sdk + from clc import APIFailedResponse +except ImportError: + CLC_FOUND = False + clc_sdk = None +else: + CLC_FOUND = True + + +class ClcLoadBalancer: + + clc = None + + def __init__(self, module): + """ + Construct module + """ + self.clc = clc_sdk + self.module = module + self.lb_dict = {} + + if not CLC_FOUND: + self.module.fail_json( + msg='clc-python-sdk required for this module') + if not REQUESTS_FOUND: + self.module.fail_json( + msg='requests library is required for this module') + if requests.__version__ and LooseVersion( + requests.__version__) < LooseVersion('2.5.0'): + self.module.fail_json( + msg='requests library version should be >= 2.5.0') + + self._set_user_agent(self.clc) + + def process_request(self): + """ + Execute the main code path, and handle the request + :return: none + """ + changed = False + result_lb = None + loadbalancer_name = self.module.params.get('name') + loadbalancer_alias = self.module.params.get('alias') + loadbalancer_location = self.module.params.get('location') + loadbalancer_description = self.module.params.get('description') + loadbalancer_port = self.module.params.get('port') + loadbalancer_method = self.module.params.get('method') + loadbalancer_persistence = self.module.params.get('persistence') + loadbalancer_nodes = self.module.params.get('nodes') + loadbalancer_status = self.module.params.get('status') + state = self.module.params.get('state') + + if loadbalancer_description is None: + loadbalancer_description = loadbalancer_name + + self._set_clc_credentials_from_env() + + self.lb_dict = self._get_loadbalancer_list( + alias=loadbalancer_alias, + location=loadbalancer_location) + + if state == 'present': + changed, result_lb, lb_id = self.ensure_loadbalancer_present( + name=loadbalancer_name, + alias=loadbalancer_alias, + location=loadbalancer_location, + description=loadbalancer_description, + status=loadbalancer_status) + if loadbalancer_port: + changed, result_pool, pool_id = self.ensure_loadbalancerpool_present( + lb_id=lb_id, + alias=loadbalancer_alias, + location=loadbalancer_location, + method=loadbalancer_method, + persistence=loadbalancer_persistence, + port=loadbalancer_port) + + if loadbalancer_nodes: + changed, result_nodes = self.ensure_lbpool_nodes_set( + alias=loadbalancer_alias, + location=loadbalancer_location, + name=loadbalancer_name, + port=loadbalancer_port, + nodes=loadbalancer_nodes) + elif state == 'absent': + changed, result_lb = self.ensure_loadbalancer_absent( + name=loadbalancer_name, + alias=loadbalancer_alias, + location=loadbalancer_location) + + elif state == 'port_absent': + changed, result_lb = self.ensure_loadbalancerpool_absent( + alias=loadbalancer_alias, + location=loadbalancer_location, + name=loadbalancer_name, + port=loadbalancer_port) + + elif state == 'nodes_present': + changed, result_lb = self.ensure_lbpool_nodes_present( + alias=loadbalancer_alias, + location=loadbalancer_location, + name=loadbalancer_name, + port=loadbalancer_port, + nodes=loadbalancer_nodes) + + elif state == 'nodes_absent': + changed, result_lb = self.ensure_lbpool_nodes_absent( + alias=loadbalancer_alias, + location=loadbalancer_location, + name=loadbalancer_name, + port=loadbalancer_port, + nodes=loadbalancer_nodes) + + self.module.exit_json(changed=changed, loadbalancer=result_lb) + + def ensure_loadbalancer_present( + self, name, alias, location, description, status): + """ + Checks to see if a load balancer exists and creates one if it does not. + :param name: Name of loadbalancer + :param alias: Alias of account + :param location: Datacenter + :param description: Description of loadbalancer + :param status: Enabled / Disabled + :return: (changed, result, lb_id) + changed: Boolean whether a change was made + result: The result object from the CLC load balancer request + lb_id: The load balancer id + """ + changed = False + result = name + lb_id = self._loadbalancer_exists(name=name) + if not lb_id: + if not self.module.check_mode: + result = self.create_loadbalancer(name=name, + alias=alias, + location=location, + description=description, + status=status) + lb_id = result.get('id') + changed = True + + return changed, result, lb_id + + def ensure_loadbalancerpool_present( + self, lb_id, alias, location, method, persistence, port): + """ + Checks to see if a load balancer pool exists and creates one if it does not. + :param lb_id: The loadbalancer id + :param alias: The account alias + :param location: the datacenter the load balancer resides in + :param method: the load balancing method + :param persistence: the load balancing persistence type + :param port: the port that the load balancer will listen on + :return: (changed, group, pool_id) - + changed: Boolean whether a change was made + result: The result from the CLC API call + pool_id: The string id of the load balancer pool + """ + changed = False + result = port + if not lb_id: + return changed, None, None + pool_id = self._loadbalancerpool_exists( + alias=alias, + location=location, + port=port, + lb_id=lb_id) + if not pool_id: + if not self.module.check_mode: + result = self.create_loadbalancerpool( + alias=alias, + location=location, + lb_id=lb_id, + method=method, + persistence=persistence, + port=port) + pool_id = result.get('id') + changed = True + + return changed, result, pool_id + + def ensure_loadbalancer_absent(self, name, alias, location): + """ + Checks to see if a load balancer exists and deletes it if it does + :param name: Name of the load balancer + :param alias: Alias of account + :param location: Datacenter + :return: (changed, result) + changed: Boolean whether a change was made + result: The result from the CLC API Call + """ + changed = False + result = name + lb_exists = self._loadbalancer_exists(name=name) + if lb_exists: + if not self.module.check_mode: + result = self.delete_loadbalancer(alias=alias, + location=location, + name=name) + changed = True + return changed, result + + def ensure_loadbalancerpool_absent(self, alias, location, name, port): + """ + Checks to see if a load balancer pool exists and deletes it if it does + :param alias: The account alias + :param location: the datacenter the load balancer resides in + :param name: the name of the load balancer + :param port: the port that the load balancer listens on + :return: (changed, result) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + changed = False + result = None + lb_exists = self._loadbalancer_exists(name=name) + if lb_exists: + lb_id = self._get_loadbalancer_id(name=name) + pool_id = self._loadbalancerpool_exists( + alias=alias, + location=location, + port=port, + lb_id=lb_id) + if pool_id: + changed = True + if not self.module.check_mode: + result = self.delete_loadbalancerpool( + alias=alias, + location=location, + lb_id=lb_id, + pool_id=pool_id) + else: + result = "Pool doesn't exist" + else: + result = "LB Doesn't Exist" + return changed, result + + def ensure_lbpool_nodes_set(self, alias, location, name, port, nodes): + """ + Checks to see if the provided list of nodes exist for the pool + and set the nodes if any in the list those doesn't exist + :param alias: The account alias + :param location: the datacenter the load balancer resides in + :param name: the name of the load balancer + :param port: the port that the load balancer will listen on + :param nodes: The list of nodes to be updated to the pool + :return: (changed, result) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + result = {} + changed = False + lb_exists = self._loadbalancer_exists(name=name) + if lb_exists: + lb_id = self._get_loadbalancer_id(name=name) + pool_id = self._loadbalancerpool_exists( + alias=alias, + location=location, + port=port, + lb_id=lb_id) + if pool_id: + nodes_exist = self._loadbalancerpool_nodes_exists(alias=alias, + location=location, + lb_id=lb_id, + pool_id=pool_id, + nodes_to_check=nodes) + if not nodes_exist: + changed = True + result = self.set_loadbalancernodes(alias=alias, + location=location, + lb_id=lb_id, + pool_id=pool_id, + nodes=nodes) + else: + result = "Pool doesn't exist" + else: + result = "Load balancer doesn't Exist" + return changed, result + + def ensure_lbpool_nodes_present(self, alias, location, name, port, nodes): + """ + Checks to see if the provided list of nodes exist for the pool and add the missing nodes to the pool + :param alias: The account alias + :param location: the datacenter the load balancer resides in + :param name: the name of the load balancer + :param port: the port that the load balancer will listen on + :param nodes: the list of nodes to be added + :return: (changed, result) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + changed = False + lb_exists = self._loadbalancer_exists(name=name) + if lb_exists: + lb_id = self._get_loadbalancer_id(name=name) + pool_id = self._loadbalancerpool_exists( + alias=alias, + location=location, + port=port, + lb_id=lb_id) + if pool_id: + changed, result = self.add_lbpool_nodes(alias=alias, + location=location, + lb_id=lb_id, + pool_id=pool_id, + nodes_to_add=nodes) + else: + result = "Pool doesn't exist" + else: + result = "Load balancer doesn't Exist" + return changed, result + + def ensure_lbpool_nodes_absent(self, alias, location, name, port, nodes): + """ + Checks to see if the provided list of nodes exist for the pool and removes them if found any + :param alias: The account alias + :param location: the datacenter the load balancer resides in + :param name: the name of the load balancer + :param port: the port that the load balancer will listen on + :param nodes: the list of nodes to be removed + :return: (changed, result) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + changed = False + lb_exists = self._loadbalancer_exists(name=name) + if lb_exists: + lb_id = self._get_loadbalancer_id(name=name) + pool_id = self._loadbalancerpool_exists( + alias=alias, + location=location, + port=port, + lb_id=lb_id) + if pool_id: + changed, result = self.remove_lbpool_nodes(alias=alias, + location=location, + lb_id=lb_id, + pool_id=pool_id, + nodes_to_remove=nodes) + else: + result = "Pool doesn't exist" + else: + result = "Load balancer doesn't Exist" + return changed, result + + def create_loadbalancer(self, name, alias, location, description, status): + """ + Create a loadbalancer w/ params + :param name: Name of loadbalancer + :param alias: Alias of account + :param location: Datacenter + :param description: Description for loadbalancer to be created + :param status: Enabled / Disabled + :return: result: The result from the CLC API call + """ + result = None + try: + result = self.clc.v2.API.Call('POST', + '/v2/sharedLoadBalancers/%s/%s' % (alias, + location), + json.dumps({"name": name, + "description": description, + "status": status})) + sleep(1) + except APIFailedResponse as e: + self.module.fail_json( + msg='Unable to create load balancer "{0}". {1}'.format( + name, str(e.response_text))) + return result + + def create_loadbalancerpool( + self, alias, location, lb_id, method, persistence, port): + """ + Creates a pool on the provided load balancer + :param alias: the account alias + :param location: the datacenter the load balancer resides in + :param lb_id: the id string of the load balancer + :param method: the load balancing method + :param persistence: the load balancing persistence type + :param port: the port that the load balancer will listen on + :return: result: The result from the create API call + """ + result = None + try: + result = self.clc.v2.API.Call( + 'POST', '/v2/sharedLoadBalancers/%s/%s/%s/pools' % + (alias, location, lb_id), json.dumps( + { + "port": port, "method": method, "persistence": persistence + })) + except APIFailedResponse as e: + self.module.fail_json( + msg='Unable to create pool for load balancer id "{0}". {1}'.format( + lb_id, str(e.response_text))) + return result + + def delete_loadbalancer(self, alias, location, name): + """ + Delete CLC loadbalancer + :param alias: Alias for account + :param location: Datacenter + :param name: Name of the loadbalancer to delete + :return: result: The result from the CLC API call + """ + result = None + lb_id = self._get_loadbalancer_id(name=name) + try: + result = self.clc.v2.API.Call( + 'DELETE', '/v2/sharedLoadBalancers/%s/%s/%s' % + (alias, location, lb_id)) + except APIFailedResponse as e: + self.module.fail_json( + msg='Unable to delete load balancer "{0}". {1}'.format( + name, str(e.response_text))) + return result + + def delete_loadbalancerpool(self, alias, location, lb_id, pool_id): + """ + Delete the pool on the provided load balancer + :param alias: The account alias + :param location: the datacenter the load balancer resides in + :param lb_id: the id string of the load balancer + :param pool_id: the id string of the load balancer pool + :return: result: The result from the delete API call + """ + result = None + try: + result = self.clc.v2.API.Call( + 'DELETE', '/v2/sharedLoadBalancers/%s/%s/%s/pools/%s' % + (alias, location, lb_id, pool_id)) + except APIFailedResponse as e: + self.module.fail_json( + msg='Unable to delete pool for load balancer id "{0}". {1}'.format( + lb_id, str(e.response_text))) + return result + + def _get_loadbalancer_id(self, name): + """ + Retrieves unique ID of loadbalancer + :param name: Name of loadbalancer + :return: Unique ID of the loadbalancer + """ + id = None + for lb in self.lb_dict: + if lb.get('name') == name: + id = lb.get('id') + return id + + def _get_loadbalancer_list(self, alias, location): + """ + Retrieve a list of loadbalancers + :param alias: Alias for account + :param location: Datacenter + :return: JSON data for all loadbalancers at datacenter + """ + result = None + try: + result = self.clc.v2.API.Call( + 'GET', '/v2/sharedLoadBalancers/%s/%s' % (alias, location)) + except APIFailedResponse as e: + self.module.fail_json( + msg='Unable to fetch load balancers for account: {0}. {1}'.format( + alias, str(e.response_text))) + return result + + def _loadbalancer_exists(self, name): + """ + Verify a loadbalancer exists + :param name: Name of loadbalancer + :return: False or the ID of the existing loadbalancer + """ + result = False + + for lb in self.lb_dict: + if lb.get('name') == name: + result = lb.get('id') + return result + + def _loadbalancerpool_exists(self, alias, location, port, lb_id): + """ + Checks to see if a pool exists on the specified port on the provided load balancer + :param alias: the account alias + :param location: the datacenter the load balancer resides in + :param port: the port to check and see if it exists + :param lb_id: the id string of the provided load balancer + :return: result: The id string of the pool or False + """ + result = False + try: + pool_list = self.clc.v2.API.Call( + 'GET', '/v2/sharedLoadBalancers/%s/%s/%s/pools' % + (alias, location, lb_id)) + except APIFailedResponse as e: + return self.module.fail_json( + msg='Unable to fetch the load balancer pools for for load balancer id: {0}. {1}'.format( + lb_id, str(e.response_text))) + for pool in pool_list: + if int(pool.get('port')) == int(port): + result = pool.get('id') + return result + + def _loadbalancerpool_nodes_exists( + self, alias, location, lb_id, pool_id, nodes_to_check): + """ + Checks to see if a set of nodes exists on the specified port on the provided load balancer + :param alias: the account alias + :param location: the datacenter the load balancer resides in + :param lb_id: the id string of the provided load balancer + :param pool_id: the id string of the load balancer pool + :param nodes_to_check: the list of nodes to check for + :return: result: True / False indicating if the given nodes exist + """ + result = False + nodes = self._get_lbpool_nodes(alias, location, lb_id, pool_id) + for node in nodes_to_check: + if not node.get('status'): + node['status'] = 'enabled' + if node in nodes: + result = True + else: + result = False + return result + + def set_loadbalancernodes(self, alias, location, lb_id, pool_id, nodes): + """ + Updates nodes to the provided pool + :param alias: the account alias + :param location: the datacenter the load balancer resides in + :param lb_id: the id string of the load balancer + :param pool_id: the id string of the pool + :param nodes: a list of dictionaries containing the nodes to set + :return: result: The result from the CLC API call + """ + result = None + if not lb_id: + return result + if not self.module.check_mode: + try: + result = self.clc.v2.API.Call('PUT', + '/v2/sharedLoadBalancers/%s/%s/%s/pools/%s/nodes' + % (alias, location, lb_id, pool_id), json.dumps(nodes)) + except APIFailedResponse as e: + self.module.fail_json( + msg='Unable to set nodes for the load balancer pool id "{0}". {1}'.format( + pool_id, str(e.response_text))) + return result + + def add_lbpool_nodes(self, alias, location, lb_id, pool_id, nodes_to_add): + """ + Add nodes to the provided pool + :param alias: the account alias + :param location: the datacenter the load balancer resides in + :param lb_id: the id string of the load balancer + :param pool_id: the id string of the pool + :param nodes_to_add: a list of dictionaries containing the nodes to add + :return: (changed, result) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + changed = False + result = {} + nodes = self._get_lbpool_nodes(alias, location, lb_id, pool_id) + for node in nodes_to_add: + if not node.get('status'): + node['status'] = 'enabled' + if not node in nodes: + changed = True + nodes.append(node) + if changed == True and not self.module.check_mode: + result = self.set_loadbalancernodes( + alias, + location, + lb_id, + pool_id, + nodes) + return changed, result + + def remove_lbpool_nodes( + self, alias, location, lb_id, pool_id, nodes_to_remove): + """ + Removes nodes from the provided pool + :param alias: the account alias + :param location: the datacenter the load balancer resides in + :param lb_id: the id string of the load balancer + :param pool_id: the id string of the pool + :param nodes_to_remove: a list of dictionaries containing the nodes to remove + :return: (changed, result) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + changed = False + result = {} + nodes = self._get_lbpool_nodes(alias, location, lb_id, pool_id) + for node in nodes_to_remove: + if not node.get('status'): + node['status'] = 'enabled' + if node in nodes: + changed = True + nodes.remove(node) + if changed == True and not self.module.check_mode: + result = self.set_loadbalancernodes( + alias, + location, + lb_id, + pool_id, + nodes) + return changed, result + + def _get_lbpool_nodes(self, alias, location, lb_id, pool_id): + """ + Return the list of nodes available to the provided load balancer pool + :param alias: the account alias + :param location: the datacenter the load balancer resides in + :param lb_id: the id string of the load balancer + :param pool_id: the id string of the pool + :return: result: The list of nodes + """ + result = None + try: + result = self.clc.v2.API.Call('GET', + '/v2/sharedLoadBalancers/%s/%s/%s/pools/%s/nodes' + % (alias, location, lb_id, pool_id)) + except APIFailedResponse as e: + self.module.fail_json( + msg='Unable to fetch list of available nodes for load balancer pool id: {0}. {1}'.format( + pool_id, str(e.response_text))) + return result + + @staticmethod + def define_argument_spec(): + """ + Define the argument spec for the ansible module + :return: argument spec dictionary + """ + argument_spec = dict( + name=dict(required=True), + description=dict(default=None), + location=dict(required=True, default=None), + alias=dict(required=True, default=None), + port=dict(choices=[80, 443]), + method=dict(choices=['leastConnection', 'roundRobin']), + persistence=dict(choices=['standard', 'sticky']), + nodes=dict(type='list', default=[]), + status=dict(default='enabled', choices=['enabled', 'disabled']), + state=dict( + default='present', + choices=[ + 'present', + 'absent', + 'port_absent', + 'nodes_present', + 'nodes_absent']) + ) + return argument_spec + + def _set_clc_credentials_from_env(self): + """ + Set the CLC Credentials on the sdk by reading environment variables + :return: none + """ + env = os.environ + v2_api_token = env.get('CLC_V2_API_TOKEN', False) + v2_api_username = env.get('CLC_V2_API_USERNAME', False) + v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) + clc_alias = env.get('CLC_ACCT_ALIAS', False) + api_url = env.get('CLC_V2_API_URL', False) + + if api_url: + self.clc.defaults.ENDPOINT_URL_V2 = api_url + + if v2_api_token and clc_alias: + self.clc._LOGIN_TOKEN_V2 = v2_api_token + self.clc._V2_ENABLED = True + self.clc.ALIAS = clc_alias + elif v2_api_username and v2_api_passwd: + self.clc.v2.SetCredentials( + api_username=v2_api_username, + api_passwd=v2_api_passwd) + else: + return self.module.fail_json( + msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " + "environment variables") + + @staticmethod + def _set_user_agent(clc): + if hasattr(clc, 'SetRequestsSession'): + agent_string = "ClcAnsibleModule/" + __version__ + ses = requests.Session() + ses.headers.update({"Api-Client": agent_string}) + ses.headers['User-Agent'] += " " + agent_string + clc.SetRequestsSession(ses) + + +def main(): + """ + The main function. Instantiates the module and calls process_request. + :return: none + """ + module = AnsibleModule(argument_spec=ClcLoadBalancer.define_argument_spec(), + supports_check_mode=True) + clc_loadbalancer = ClcLoadBalancer(module) + clc_loadbalancer.process_request() + +from ansible.module_utils.basic import * # pylint: disable=W0614 +if __name__ == '__main__': + main() From 1fd1460d2fc1c069d5caf214a5dda6f7b0584904 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Fri, 24 Jul 2015 08:42:03 +0200 Subject: [PATCH 0491/2522] libvirt: virt_pool module This module manages storage pool configuration in libvirt. --- cloud/misc/virt_pool.py | 705 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 705 insertions(+) create mode 100644 cloud/misc/virt_pool.py diff --git a/cloud/misc/virt_pool.py b/cloud/misc/virt_pool.py new file mode 100644 index 00000000000..12cd85a07e8 --- /dev/null +++ b/cloud/misc/virt_pool.py @@ -0,0 +1,705 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Maciej Delmanowski +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: virt_pool +author: "Maciej Delmanowski" +version_added: "2.0" +short_description: Manage libvirt storage pools +description: + - Manage I(libvirt) storage pools. +options: + name: + required: false + aliases: [ "pool" ] + description: + - name of the storage pool being managed. Note that pool must be previously + defined with xml. + state: + required: false + choices: [ "active", "inactive", "present", "absent", "undefined", "deleted" ] + description: + - specify which state you want a storage pool to be in. + If 'active', pool will be started. + If 'present', ensure that pool is present but do not change its + state; if it's missing, you need to specify xml argument. + If 'inactive', pool will be stopped. + If 'undefined' or 'absent', pool will be removed from I(libvirt) configuration. + If 'deleted', pool contents will be deleted and then pool undefined. + command: + required: false + choices: [ "define", "build", "create", "start", "stop", "destroy", + "delete", "undefine", "get_xml", "list_pools", "facts", + "info", "status" ] + description: + - in addition to state management, various non-idempotent commands are available. + See examples. + autostart: + required: false + choices: ["yes", "no"] + description: + - Specify if a given storage pool should be started automatically on system boot. + uri: + required: false + default: "qemu:///system" + description: + - I(libvirt) connection uri. + xml: + required: false + description: + - XML document used with the define command. + mode: + required: false + choices: [ 'new', 'repair', 'resize', 'no_overwrite', 'overwrite', 'normal', 'zeroed' ] + description: + - Pass additional parameters to 'build' or 'delete' commands. +requirements: + - "python >= 2.6" + - "python-libvirt" + - "python-lxml" +''' + +EXAMPLES = ''' +# Define a new storage pool +- virt_pool: command=define name=vms xml='{{ lookup("template", "pool/dir.xml.j2") }}' + +# Build a storage pool if it does not exist +- virt_pool: command=build name=vms + +# Start a storage pool +- virt_pool: command=create name=vms + +# List available pools +- virt_pool: command=list_pools + +# Get XML data of a specified pool +- virt_pool: command=get_xml name=vms + +# Stop a storage pool +- virt_pool: command=destroy name=vms + +# Delete a storage pool (destroys contents) +- virt_pool: command=delete name=vms + +# Undefine a storage pool +- virt_pool: command=undefine name=vms + +# Gather facts about storage pools +# Facts will be available as 'ansible_libvirt_pools' +- virt_pool: command=facts + +# Gather information about pools managed by 'libvirt' remotely using uri +- virt_pool: command=info uri='{{ item }}' + with_items: libvirt_uris + register: storage_pools + +# Ensure that a pool is active (needs to be defined and built first) +- virt_pool: state=active name=vms + +# Ensure that a pool is inactive +- virt_pool: state=inactive name=vms + +# Ensure that a given pool will be started at boot +- virt_pool: autostart=yes name=vms + +# Disable autostart for a given pool +- virt_pool: autostart=no name=vms +''' + +VIRT_FAILED = 1 +VIRT_SUCCESS = 0 +VIRT_UNAVAILABLE=2 + +import sys + +try: + import libvirt +except ImportError: + HAS_VIRT = False +else: + HAS_VIRT = True + +try: + from lxml import etree +except ImportError: + HAS_XML = False +else: + HAS_XML = True + +ALL_COMMANDS = [] +ENTRY_COMMANDS = ['create', 'status', 'start', 'stop', 'build', 'delete', + 'undefine', 'destroy', 'get_xml', 'define', 'refresh'] +HOST_COMMANDS = [ 'list_pools', 'facts', 'info' ] +ALL_COMMANDS.extend(ENTRY_COMMANDS) +ALL_COMMANDS.extend(HOST_COMMANDS) + +ENTRY_STATE_ACTIVE_MAP = { + 0 : "inactive", + 1 : "active" +} + +ENTRY_STATE_AUTOSTART_MAP = { + 0 : "no", + 1 : "yes" +} + +ENTRY_STATE_PERSISTENT_MAP = { + 0 : "no", + 1 : "yes" +} + +ENTRY_STATE_INFO_MAP = { + 0 : "inactive", + 1 : "building", + 2 : "running", + 3 : "degraded", + 4 : "inaccessible" +} + +ENTRY_BUILD_FLAGS_MAP = { + "new" : 0, + "repair" : 1, + "resize" : 2, + "no_overwrite" : 4, + "overwrite" : 8 +} + +ENTRY_DELETE_FLAGS_MAP = { + "normal" : 0, + "zeroed" : 1 +} + +ALL_MODES = [] +ALL_MODES.extend(ENTRY_BUILD_FLAGS_MAP.keys()) +ALL_MODES.extend(ENTRY_DELETE_FLAGS_MAP.keys()) + + +class EntryNotFound(Exception): + pass + + +class LibvirtConnection(object): + + def __init__(self, uri, module): + + self.module = module + + cmd = "uname -r" + rc, stdout, stderr = self.module.run_command(cmd) + + if "xen" in stdout: + conn = libvirt.open(None) + else: + conn = libvirt.open(uri) + + if not conn: + raise Exception("hypervisor connection failure") + + self.conn = conn + + def find_entry(self, entryid): + # entryid = -1 returns a list of everything + + results = [] + + # Get active entries + entries = self.conn.listStoragePools() + for name in entries: + entry = self.conn.storagePoolLookupByName(name) + results.append(entry) + + # Get inactive entries + entries = self.conn.listDefinedStoragePools() + for name in entries: + entry = self.conn.storagePoolLookupByName(name) + results.append(entry) + + if entryid == -1: + return results + + for entry in results: + if entry.name() == entryid: + return entry + + raise EntryNotFound("storage pool %s not found" % entryid) + + def create(self, entryid): + if not self.module.check_mode: + return self.find_entry(entryid).create() + else: + try: + state = self.find_entry(entryid).isActive() + except: + return self.module.exit_json(changed=True) + if not state: + return self.module.exit_json(changed=True) + + def destroy(self, entryid): + if not self.module.check_mode: + return self.find_entry(entryid).destroy() + else: + if self.find_entry(entryid).isActive(): + return self.module.exit_json(changed=True) + + def undefine(self, entryid): + if not self.module.check_mode: + return self.find_entry(entryid).undefine() + else: + if not self.find_entry(entryid): + return self.module.exit_json(changed=True) + + def get_status2(self, entry): + state = entry.isActive() + return ENTRY_STATE_ACTIVE_MAP.get(state,"unknown") + + def get_status(self, entryid): + if not self.module.check_mode: + state = self.find_entry(entryid).isActive() + return ENTRY_STATE_ACTIVE_MAP.get(state,"unknown") + else: + try: + state = self.find_entry(entryid).isActive() + return ENTRY_STATE_ACTIVE_MAP.get(state,"unknown") + except: + return ENTRY_STATE_ACTIVE_MAP.get("inactive","unknown") + + def get_uuid(self, entryid): + return self.find_entry(entryid).UUIDString() + + def get_xml(self, entryid): + return self.find_entry(entryid).XMLDesc(0) + + def get_info(self, entryid): + return self.find_entry(entryid).info() + + def get_volume_count(self, entryid): + return self.find_entry(entryid).numOfVolumes() + + def get_volume_names(self, entryid): + return self.find_entry(entryid).listVolumes() + + def get_devices(self, entryid): + xml = etree.fromstring(self.find_entry(entryid).XMLDesc(0)) + if xml.xpath('/pool/source/device'): + result = [] + for device in xml.xpath('/pool/source/device'): + result.append(device.get('path')) + try: + return result + except: + raise ValueError('No devices specified') + + def get_format(self, entryid): + xml = etree.fromstring(self.find_entry(entryid).XMLDesc(0)) + try: + result = xml.xpath('/pool/source/format')[0].get('type') + except: + raise ValueError('Format not specified') + return result + + def get_host(self, entryid): + xml = etree.fromstring(self.find_entry(entryid).XMLDesc(0)) + try: + result = xml.xpath('/pool/source/host')[0].get('name') + except: + raise ValueError('Host not specified') + return result + + def get_source_path(self, entryid): + xml = etree.fromstring(self.find_entry(entryid).XMLDesc(0)) + try: + result = xml.xpath('/pool/source/dir')[0].get('path') + except: + raise ValueError('Source path not specified') + return result + + def get_path(self, entryid): + xml = etree.fromstring(self.find_entry(entryid).XMLDesc(0)) + return xml.xpath('/pool/target/path')[0].text + + def get_type(self, entryid): + xml = etree.fromstring(self.find_entry(entryid).XMLDesc(0)) + return xml.get('type') + + def build(self, entryid, flags): + if not self.module.check_mode: + return self.find_entry(entryid).build(flags) + else: + try: + state = self.find_entry(entryid) + except: + return self.module.exit_json(changed=True) + if not state: + return self.module.exit_json(changed=True) + + def delete(self, entryid, flags): + if not self.module.check_mode: + return self.find_entry(entryid).delete(flags) + else: + try: + state = self.find_entry(entryid) + except: + return self.module.exit_json(changed=True) + if state: + return self.module.exit_json(changed=True) + + def get_autostart(self, entryid): + state = self.find_entry(entryid).autostart() + return ENTRY_STATE_AUTOSTART_MAP.get(state,"unknown") + + def get_autostart2(self, entryid): + if not self.module.check_mode: + return self.find_entry(entryid).autostart() + else: + try: + return self.find_entry(entryid).autostart() + except: + return self.module.exit_json(changed=True) + + def set_autostart(self, entryid, val): + if not self.module.check_mode: + return self.find_entry(entryid).setAutostart(val) + else: + try: + state = self.find_entry(entryid).autostart() + except: + return self.module.exit_json(changed=True) + if bool(state) != val: + return self.module.exit_json(changed=True) + + def refresh(self, entryid): + return self.find_entry(entryid).refresh() + + def get_persistent(self, entryid): + state = self.find_entry(entryid).isPersistent() + return ENTRY_STATE_PERSISTENT_MAP.get(state,"unknown") + + def define_from_xml(self, entryid, xml): + if not self.module.check_mode: + return self.conn.storagePoolDefineXML(xml) + else: + try: + state = self.find_entry(entryid) + except: + return self.module.exit_json(changed=True) + + +class VirtStoragePool(object): + + def __init__(self, uri, module): + self.module = module + self.uri = uri + self.conn = LibvirtConnection(self.uri, self.module) + + def get_pool(self, entryid): + return self.conn.find_entry(entryid) + + def list_pools(self, state=None): + entries = self.conn.find_entry(-1) + results = [] + for x in entries: + try: + if state: + entrystate = self.conn.get_status2(x) + if entrystate == state: + results.append(x.name()) + else: + results.append(x.name()) + except: + pass + return results + + def state(self): + entries = self.list_pools() + results = [] + for entry in entries: + state_blurb = self.conn.get_status(entry) + results.append("%s %s" % (entry,state_blurb)) + return results + + def autostart(self, entryid): + return self.conn.set_autostart(entryid, True) + + def get_autostart(self, entryid): + return self.conn.get_autostart2(entryid) + + def set_autostart(self, entryid, state): + return self.conn.set_autostart(entryid, state) + + def create(self, entryid): + return self.conn.create(entryid) + + def start(self, entryid): + return self.conn.create(entryid) + + def stop(self, entryid): + return self.conn.destroy(entryid) + + def destroy(self, entryid): + return self.conn.destroy(entryid) + + def undefine(self, entryid): + return self.conn.undefine(entryid) + + def status(self, entryid): + return self.conn.get_status(entryid) + + def get_xml(self, entryid): + return self.conn.get_xml(entryid) + + def define(self, entryid, xml): + return self.conn.define_from_xml(entryid, xml) + + def build(self, entryid, flags): + return self.conn.build(entryid, ENTRY_BUILD_FLAGS_MAP.get(flags,0)) + + def delete(self, entryid, flags): + return self.conn.delete(entryid, ENTRY_DELETE_FLAGS_MAP.get(flags,0)) + + def refresh(self, entryid): + return self.conn.refresh(entryid) + + def info(self, facts_mode='info'): + return self.facts(facts_mode) + + def facts(self, facts_mode='facts'): + entries = self.list_pools() + results = dict() + for entry in entries: + results[entry] = dict() + if self.conn.find_entry(entry): + data = self.conn.get_info(entry) + # libvirt returns maxMem, memory, and cpuTime as long()'s, which + # xmlrpclib tries to convert to regular int's during serialization. + # This throws exceptions, so convert them to strings here and + # assume the other end of the xmlrpc connection can figure things + # out or doesn't care. + results[entry] = { + "status" : ENTRY_STATE_INFO_MAP.get(data[0],"unknown"), + "size_total" : str(data[1]), + "size_used" : str(data[2]), + "size_available" : str(data[3]), + } + results[entry]["autostart"] = self.conn.get_autostart(entry) + results[entry]["persistent"] = self.conn.get_persistent(entry) + results[entry]["state"] = self.conn.get_status(entry) + results[entry]["path"] = self.conn.get_path(entry) + results[entry]["type"] = self.conn.get_type(entry) + results[entry]["uuid"] = self.conn.get_uuid(entry) + if self.conn.find_entry(entry).isActive(): + results[entry]["volume_count"] = self.conn.get_volume_count(entry) + results[entry]["volumes"] = list() + for volume in self.conn.get_volume_names(entry): + results[entry]["volumes"].append(volume) + else: + results[entry]["volume_count"] = -1 + + try: + results[entry]["host"] = self.conn.get_host(entry) + except ValueError as e: + pass + + try: + results[entry]["source_path"] = self.conn.get_source_path(entry) + except ValueError as e: + pass + + try: + results[entry]["format"] = self.conn.get_format(entry) + except ValueError as e: + pass + + try: + devices = self.conn.get_devices(entry) + results[entry]["devices"] = devices + except ValueError as e: + pass + + else: + results[entry]["state"] = self.conn.get_status(entry) + + facts = dict() + if facts_mode == 'facts': + facts["ansible_facts"] = dict() + facts["ansible_facts"]["ansible_libvirt_pools"] = results + elif facts_mode == 'info': + facts['pools'] = results + return facts + + +def core(module): + + state = module.params.get('state', None) + name = module.params.get('name', None) + command = module.params.get('command', None) + uri = module.params.get('uri', None) + xml = module.params.get('xml', None) + autostart = module.params.get('autostart', None) + mode = module.params.get('mode', None) + + v = VirtStoragePool(uri, module) + res = {} + + if state and command == 'list_pools': + res = v.list_pools(state=state) + if type(res) != dict: + res = { command: res } + return VIRT_SUCCESS, res + + if state: + if not name: + module.fail_json(msg = "state change requires a specified name") + + res['changed'] = False + if state in [ 'active' ]: + if v.status(name) is not 'active': + res['changed'] = True + res['msg'] = v.start(name) + elif state in [ 'present' ]: + try: + v.get_pool(name) + except EntryNotFound: + if not xml: + module.fail_json(msg = "storage pool '" + name + "' not present, but xml not specified") + v.define(name, xml) + res = {'changed': True, 'created': name} + elif state in [ 'inactive' ]: + entries = v.list_pools() + if name in entries: + if v.status(name) is not 'inactive': + res['changed'] = True + res['msg'] = v.destroy(name) + elif state in [ 'undefined', 'absent' ]: + entries = v.list_pools() + if name in entries: + if v.status(name) is not 'inactive': + v.destroy(name) + res['changed'] = True + res['msg'] = v.undefine(name) + elif state in [ 'deleted' ]: + entries = v.list_pools() + if name in entries: + if v.status(name) is not 'inactive': + v.destroy(name) + v.delete(name, mode) + res['changed'] = True + res['msg'] = v.undefine(name) + else: + module.fail_json(msg="unexpected state") + + return VIRT_SUCCESS, res + + if command: + if command in ENTRY_COMMANDS: + if not name: + module.fail_json(msg = "%s requires 1 argument: name" % command) + if command == 'define': + if not xml: + module.fail_json(msg = "define requires xml argument") + try: + v.get_pool(name) + except EntryNotFound: + v.define(name, xml) + res = {'changed': True, 'created': name} + return VIRT_SUCCESS, res + elif command == 'build': + res = v.build(name, mode) + if type(res) != dict: + res = { 'changed': True, command: res } + return VIRT_SUCCESS, res + elif command == 'delete': + res = v.delete(name, mode) + if type(res) != dict: + res = { 'changed': True, command: res } + return VIRT_SUCCESS, res + res = getattr(v, command)(name) + if type(res) != dict: + res = { command: res } + return VIRT_SUCCESS, res + + elif hasattr(v, command): + res = getattr(v, command)() + if type(res) != dict: + res = { command: res } + return VIRT_SUCCESS, res + + else: + module.fail_json(msg="Command %s not recognized" % basecmd) + + if autostart: + if not name: + module.fail_json(msg = "state change requires a specified name") + + res['changed'] = False + if autostart == 'yes': + if not v.get_autostart(name): + res['changed'] = True + res['msg'] = v.set_autostart(name, True) + elif autostart == 'no': + if v.get_autostart(name): + res['changed'] = True + res['msg'] = v.set_autostart(name, False) + + return VIRT_SUCCESS, res + + module.fail_json(msg="expected state or command parameter to be specified") + + +def main(): + + module = AnsibleModule ( + argument_spec = dict( + name = dict(aliases=['pool']), + state = dict(choices=['active', 'inactive', 'present', 'absent', 'undefined', 'deleted']), + command = dict(choices=ALL_COMMANDS), + uri = dict(default='qemu:///system'), + xml = dict(), + autostart = dict(choices=['yes', 'no']), + mode = dict(choices=ALL_MODES), + ), + supports_check_mode = True + ) + + if not HAS_VIRT: + module.fail_json( + msg='The `libvirt` module is not importable. Check the requirements.' + ) + + if not HAS_XML: + module.fail_json( + msg='The `lxml` module is not importable. Check the requirements.' + ) + + rc = VIRT_SUCCESS + try: + rc, result = core(module) + except Exception, e: + module.fail_json(msg=str(e)) + + if rc != 0: # something went wrong emit the msg + module.fail_json(rc=rc, msg=result) + else: + module.exit_json(**result) + + +# import module snippets +from ansible.module_utils.basic import * +main() From 0da9254537f6acc93da1cf09d6b3eee06853dec8 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Fri, 24 Jul 2015 08:43:49 +0200 Subject: [PATCH 0492/2522] libvirt: virt_net module This module manages network configuration in libvirt. --- cloud/misc/virt_net.py | 557 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 557 insertions(+) create mode 100644 cloud/misc/virt_net.py diff --git a/cloud/misc/virt_net.py b/cloud/misc/virt_net.py new file mode 100644 index 00000000000..b84f976c6f3 --- /dev/null +++ b/cloud/misc/virt_net.py @@ -0,0 +1,557 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Maciej Delmanowski +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: virt_net +author: "Maciej Delmanowski" +version_added: "2.0" +short_description: Manage libvirt network configuration +description: + - Manage I(libvirt) networks. +options: + name: + required: true + aliases: ['network'] + description: + - name of the network being managed. Note that network must be previously + defined with xml. + state: + required: false + choices: [ "active", "inactive", "present", "absent" ] + description: + - specify which state you want a network to be in. + If 'active', network will be started. + If 'present', ensure that network is present but do not change its + state; if it's missing, you need to specify xml argument. + If 'inactive', network will be stopped. + If 'undefined' or 'absent', network will be removed from I(libvirt) configuration. + command: + required: false + choices: [ "define", "create", "start", "stop", "destroy", + "undefine", "get_xml", "list_nets", "facts", + "info", "status"] + description: + - in addition to state management, various non-idempotent commands are available. + See examples. + autostart: + required: false + choices: ["yes", "no"] + description: + - Specify if a given storage pool should be started automatically on system boot. + uri: + required: false + default: "qemu:///system" + description: + - libvirt connection uri. + xml: + required: false + description: + - XML document used with the define command. +requirements: + - "python >= 2.6" + - "python-libvirt" + - "python-lxml" +''' + +EXAMPLES = ''' +# Define a new network +- virt_net: command=define name=br_nat xml='{{ lookup("template", "network/bridge.xml.j2") }}' + +# Start a network +- virt_net: command=create name=br_nat + +# List available networks +- virt_net: command=list_nets + +# Get XML data of a specified network +- virt_net: command=get_xml name=br_nat + +# Stop a network +- virt_net: command=destroy name=br_nat + +# Undefine a network +- virt_net: command=undefine name=br_nat + +# Gather facts about networks +# Facts will be available as 'ansible_libvirt_networks' +- virt_net: command=facts + +# Gather information about network managed by 'libvirt' remotely using uri +- virt_net: command=info uri='{{ item }}' + with_items: libvirt_uris + register: networks + +# Ensure that a network is active (needs to be defined and built first) +- virt_net: state=active name=br_nat + +# Ensure that a network is inactive +- virt_net: state=inactive name=br_nat + +# Ensure that a given network will be started at boot +- virt_net: autostart=yes name=br_nat + +# Disable autostart for a given network +- virt_net: autostart=no name=br_nat +''' + +VIRT_FAILED = 1 +VIRT_SUCCESS = 0 +VIRT_UNAVAILABLE=2 + +import sys + +try: + import libvirt +except ImportError: + HAS_VIRT = False +else: + HAS_VIRT = True + +try: + from lxml import etree +except ImportError: + HAS_XML = False +else: + HAS_XML = True + +ALL_COMMANDS = [] +ENTRY_COMMANDS = ['create', 'status', 'start', 'stop', + 'undefine', 'destroy', 'get_xml', 'define'] +HOST_COMMANDS = [ 'list_nets', 'facts', 'info' ] +ALL_COMMANDS.extend(ENTRY_COMMANDS) +ALL_COMMANDS.extend(HOST_COMMANDS) + +ENTRY_STATE_ACTIVE_MAP = { + 0 : "inactive", + 1 : "active" +} + +ENTRY_STATE_AUTOSTART_MAP = { + 0 : "no", + 1 : "yes" +} + +ENTRY_STATE_PERSISTENT_MAP = { + 0 : "no", + 1 : "yes" +} + +class EntryNotFound(Exception): + pass + + +class LibvirtConnection(object): + + def __init__(self, uri, module): + + self.module = module + + cmd = "uname -r" + rc, stdout, stderr = self.module.run_command(cmd) + + if "xen" in stdout: + conn = libvirt.open(None) + else: + conn = libvirt.open(uri) + + if not conn: + raise Exception("hypervisor connection failure") + + self.conn = conn + + def find_entry(self, entryid): + # entryid = -1 returns a list of everything + + results = [] + + # Get active entries + entries = self.conn.listNetworks() + for name in entries: + entry = self.conn.networkLookupByName(name) + results.append(entry) + + # Get inactive entries + entries = self.conn.listDefinedNetworks() + for name in entries: + entry = self.conn.networkLookupByName(name) + results.append(entry) + + if entryid == -1: + return results + + for entry in results: + if entry.name() == entryid: + return entry + + raise EntryNotFound("network %s not found" % entryid) + + def create(self, entryid): + if not self.module.check_mode: + return self.find_entry(entryid).create() + else: + try: + state = self.find_entry(entryid).isActive() + except: + return self.module.exit_json(changed=True) + if not state: + return self.module.exit_json(changed=True) + + def destroy(self, entryid): + if not self.module.check_mode: + return self.find_entry(entryid).destroy() + else: + if self.find_entry(entryid).isActive(): + return self.module.exit_json(changed=True) + + def undefine(self, entryid): + if not self.module.check_mode: + return self.find_entry(entryid).undefine() + else: + if not self.find_entry(entryid): + return self.module.exit_json(changed=True) + + def get_status2(self, entry): + state = entry.isActive() + return ENTRY_STATE_ACTIVE_MAP.get(state,"unknown") + + def get_status(self, entryid): + if not self.module.check_mode: + state = self.find_entry(entryid).isActive() + return ENTRY_STATE_ACTIVE_MAP.get(state,"unknown") + else: + try: + state = self.find_entry(entryid).isActive() + return ENTRY_STATE_ACTIVE_MAP.get(state,"unknown") + except: + return ENTRY_STATE_ACTIVE_MAP.get("inactive","unknown") + + def get_uuid(self, entryid): + return self.find_entry(entryid).UUIDString() + + def get_xml(self, entryid): + return self.find_entry(entryid).XMLDesc(0) + + def get_forward(self, entryid): + xml = etree.fromstring(self.find_entry(entryid).XMLDesc(0)) + try: + result = xml.xpath('/network/forward')[0].get('mode') + except: + raise ValueError('Forward mode not specified') + return result + + def get_domain(self, entryid): + xml = etree.fromstring(self.find_entry(entryid).XMLDesc(0)) + try: + result = xml.xpath('/network/domain')[0].get('name') + except: + raise ValueError('Domain not specified') + return result + + def get_macaddress(self, entryid): + xml = etree.fromstring(self.find_entry(entryid).XMLDesc(0)) + try: + result = xml.xpath('/network/mac')[0].get('address') + except: + raise ValueError('MAC address not specified') + return result + + def get_autostart(self, entryid): + state = self.find_entry(entryid).autostart() + return ENTRY_STATE_AUTOSTART_MAP.get(state,"unknown") + + def get_autostart2(self, entryid): + if not self.module.check_mode: + return self.find_entry(entryid).autostart() + else: + try: + return self.find_entry(entryid).autostart() + except: + return self.module.exit_json(changed=True) + + def set_autostart(self, entryid, val): + if not self.module.check_mode: + return self.find_entry(entryid).setAutostart(val) + else: + try: + state = self.find_entry(entryid).autostart() + except: + return self.module.exit_json(changed=True) + if bool(state) != val: + return self.module.exit_json(changed=True) + + def get_bridge(self, entryid): + return self.find_entry(entryid).bridgeName() + + def get_persistent(self, entryid): + state = self.find_entry(entryid).isPersistent() + return ENTRY_STATE_PERSISTENT_MAP.get(state,"unknown") + + def define_from_xml(self, entryid, xml): + if not self.module.check_mode: + return self.conn.networkDefineXML(xml) + else: + try: + state = self.find_entry(entryid) + except: + return self.module.exit_json(changed=True) + + +class VirtNetwork(object): + + def __init__(self, uri, module): + self.module = module + self.uri = uri + self.conn = LibvirtConnection(self.uri, self.module) + + def get_net(self, entryid): + return self.conn.find_entry(entryid) + + def list_nets(self, state=None): + entries = self.conn.find_entry(-1) + results = [] + for x in entries: + try: + if state: + entrystate = self.conn.get_status2(x) + if entrystate == state: + results.append(x.name()) + else: + results.append(x.name()) + except: + pass + return results + + def state(self): + entries = self.list_nets() + results = [] + for entry in entries: + state_blurb = self.conn.get_status(entry) + results.append("%s %s" % (entry,state_blurb)) + return results + + def autostart(self, entryid): + return self.conn.set_autostart(entryid, True) + + def get_autostart(self, entryid): + return self.conn.get_autostart2(entryid) + + def set_autostart(self, entryid, state): + return self.conn.set_autostart(entryid, state) + + def create(self, entryid): + return self.conn.create(entryid) + + def start(self, entryid): + return self.conn.create(entryid) + + def stop(self, entryid): + return self.conn.destroy(entryid) + + def destroy(self, entryid): + return self.conn.destroy(entryid) + + def undefine(self, entryid): + return self.conn.undefine(entryid) + + def status(self, entryid): + return self.conn.get_status(entryid) + + def get_xml(self, entryid): + return self.conn.get_xml(entryid) + + def define(self, entryid, xml): + return self.conn.define_from_xml(entryid, xml) + + def info(self, facts_mode='info'): + return self.facts(facts_mode) + + def facts(self, facts_mode='facts'): + entries = self.list_nets() + results = dict() + for entry in entries: + results[entry] = dict() + results[entry]["autostart"] = self.conn.get_autostart(entry) + results[entry]["persistent"] = self.conn.get_persistent(entry) + results[entry]["state"] = self.conn.get_status(entry) + results[entry]["bridge"] = self.conn.get_bridge(entry) + results[entry]["uuid"] = self.conn.get_uuid(entry) + + try: + results[entry]["forward_mode"] = self.conn.get_forward(entry) + except ValueError as e: + pass + + try: + results[entry]["domain"] = self.conn.get_domain(entry) + except ValueError as e: + pass + + try: + results[entry]["macaddress"] = self.conn.get_macaddress(entry) + except ValueError as e: + pass + + facts = dict() + if facts_mode == 'facts': + facts["ansible_facts"] = dict() + facts["ansible_facts"]["ansible_libvirt_networks"] = results + elif facts_mode == 'info': + facts['networks'] = results + return facts + + +def core(module): + + state = module.params.get('state', None) + name = module.params.get('name', None) + command = module.params.get('command', None) + uri = module.params.get('uri', None) + xml = module.params.get('xml', None) + autostart = module.params.get('autostart', None) + + v = VirtNetwork(uri, module) + res = {} + + if state and command == 'list_nets': + res = v.list_nets(state=state) + if type(res) != dict: + res = { command: res } + return VIRT_SUCCESS, res + + if state: + if not name: + module.fail_json(msg = "state change requires a specified name") + + res['changed'] = False + if state in [ 'active' ]: + if v.status(name) is not 'active': + res['changed'] = True + res['msg'] = v.start(name) + elif state in [ 'present' ]: + try: + v.get_net(name) + except EntryNotFound: + if not xml: + module.fail_json(msg = "network '" + name + "' not present, but xml not specified") + v.define(name, xml) + res = {'changed': True, 'created': name} + elif state in [ 'inactive' ]: + entries = v.list_nets() + if name in entries: + if v.status(name) is not 'inactive': + res['changed'] = True + res['msg'] = v.destroy(name) + elif state in [ 'undefined', 'absent' ]: + entries = v.list_nets() + if name in entries: + if v.status(name) is not 'inactive': + v.destroy(name) + res['changed'] = True + res['msg'] = v.undefine(name) + else: + module.fail_json(msg="unexpected state") + + return VIRT_SUCCESS, res + + if command: + if command in ENTRY_COMMANDS: + if not name: + module.fail_json(msg = "%s requires 1 argument: name" % command) + if command == 'define': + if not xml: + module.fail_json(msg = "define requires xml argument") + try: + v.get_net(name) + except EntryNotFound: + v.define(name, xml) + res = {'changed': True, 'created': name} + return VIRT_SUCCESS, res + res = getattr(v, command)(name) + if type(res) != dict: + res = { command: res } + return VIRT_SUCCESS, res + + elif hasattr(v, command): + res = getattr(v, command)() + if type(res) != dict: + res = { command: res } + return VIRT_SUCCESS, res + + else: + module.fail_json(msg="Command %s not recognized" % basecmd) + + if autostart: + if not name: + module.fail_json(msg = "state change requires a specified name") + + res['changed'] = False + if autostart == 'yes': + if not v.get_autostart(name): + res['changed'] = True + res['msg'] = v.set_autostart(name, True) + elif autostart == 'no': + if v.get_autostart(name): + res['changed'] = True + res['msg'] = v.set_autostart(name, False) + + return VIRT_SUCCESS, res + + module.fail_json(msg="expected state or command parameter to be specified") + +def main(): + + module = AnsibleModule ( + argument_spec = dict( + name = dict(aliases=['network']), + state = dict(choices=['active', 'inactive', 'present', 'absent']), + command = dict(choices=ALL_COMMANDS), + uri = dict(default='qemu:///system'), + xml = dict(), + autostart = dict(choices=['yes', 'no']) + ), + supports_check_mode = True + ) + + if not HAS_VIRT: + module.fail_json( + msg='The `libvirt` module is not importable. Check the requirements.' + ) + + if not HAS_XML: + module.fail_json( + msg='The `lxml` module is not importable. Check the requirements.' + ) + + rc = VIRT_SUCCESS + try: + rc, result = core(module) + except Exception, e: + module.fail_json(msg=str(e)) + + if rc != 0: # something went wrong emit the msg + module.fail_json(rc=rc, msg=result) + else: + module.exit_json(**result) + + +# import module snippets +from ansible.module_utils.basic import * +main() From a335763188996df85c3e86bb6a3a98ee22314a3d Mon Sep 17 00:00:00 2001 From: Siva Popuri Date: Fri, 24 Jul 2015 14:10:18 -0500 Subject: [PATCH 0493/2522] minor change to remove extra brackets --- cloud/centurylink/clc_aa_policy.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cloud/centurylink/clc_aa_policy.py b/cloud/centurylink/clc_aa_policy.py index e05ce1a3e78..e092af13a07 100644 --- a/cloud/centurylink/clc_aa_policy.py +++ b/cloud/centurylink/clc_aa_policy.py @@ -46,7 +46,6 @@ default: True required: False choices: [True, False] - aliases: [] requirements: - python = 2.7 - requests >= 2.5.0 @@ -124,7 +123,7 @@ CLC_FOUND = True -class ClcAntiAffinityPolicy(): +class ClcAntiAffinityPolicy: clc = clc_sdk module = None From e04dc73ce8d64ba557cfbe77ec156d300487f37b Mon Sep 17 00:00:00 2001 From: Siva Popuri Date: Fri, 24 Jul 2015 14:12:54 -0500 Subject: [PATCH 0494/2522] minor refactor to remove un-used parameter --- cloud/centurylink/clc_alert_policy.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/cloud/centurylink/clc_alert_policy.py b/cloud/centurylink/clc_alert_policy.py index ae1ed4d7b55..ef5498a9ef6 100644 --- a/cloud/centurylink/clc_alert_policy.py +++ b/cloud/centurylink/clc_alert_policy.py @@ -155,7 +155,7 @@ CLC_FOUND = True -class ClcAlertPolicy(): +class ClcAlertPolicy: clc = clc_sdk module = None @@ -262,10 +262,10 @@ def _ensure_alert_policy_is_present(self): changed = False p = self.module.params policy_name = p.get('name') - alias = p.get('alias') + if not policy_name: self.module.fail_json(msg='Policy name is a required') - policy = self._alert_policy_exists(alias, policy_name) + policy = self._alert_policy_exists(policy_name) if not policy: changed = True policy = None @@ -340,7 +340,7 @@ def _get_alert_policies(self, alias): policies = self.clc.v2.API.Call('GET', '/v2/alertPolicies/%s' - % (alias)) + % alias) for policy in policies.get('items'): response[policy.get('id')] = policy @@ -377,7 +377,7 @@ def _create_alert_policy(self): try: result = self.clc.v2.API.Call( 'POST', - '/v2/alertPolicies/%s' %alias, + '/v2/alertPolicies/%s' % alias, arguments) except APIFailedResponse as e: return self.module.fail_json( @@ -441,16 +441,16 @@ def _delete_alert_policy(self, alias, policy_id): policy_id, str(e.response_text))) return result - def _alert_policy_exists(self, alias, policy_name): + def _alert_policy_exists(self, policy_name): """ Check to see if an alert policy exists :param policy_name: name of the alert policy :return: boolean of if the policy exists """ result = False - for id in self.policy_dict: - if self.policy_dict.get(id).get('name') == policy_name: - result = self.policy_dict.get(id) + for policy_id in self.policy_dict: + if self.policy_dict.get(policy_id).get('name') == policy_name: + result = self.policy_dict.get(policy_id) return result def _get_alert_policy_id(self, module, alert_policy_name): @@ -461,14 +461,13 @@ def _get_alert_policy_id(self, module, alert_policy_name): :return: alert_policy_id: The alert policy id """ alert_policy_id = None - for id in self.policy_dict: - if self.policy_dict.get(id).get('name') == alert_policy_name: + for policy_id in self.policy_dict: + if self.policy_dict.get(policy_id).get('name') == alert_policy_name: if not alert_policy_id: - alert_policy_id = id + alert_policy_id = policy_id else: return module.fail_json( - msg='multiple alert policies were found with policy name : %s' % - (alert_policy_name)) + msg='multiple alert policies were found with policy name : %s' % alert_policy_name) return alert_policy_id @staticmethod From c89a4ac460fd6f5201b2827809296a43cdd530a2 Mon Sep 17 00:00:00 2001 From: Siva Popuri Date: Fri, 24 Jul 2015 14:18:01 -0500 Subject: [PATCH 0495/2522] minor change to get rid of code inspection warnings --- cloud/centurylink/clc_blueprint_package.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/centurylink/clc_blueprint_package.py b/cloud/centurylink/clc_blueprint_package.py index 6b06b741287..3548944210d 100644 --- a/cloud/centurylink/clc_blueprint_package.py +++ b/cloud/centurylink/clc_blueprint_package.py @@ -104,7 +104,7 @@ CLC_FOUND = True -class ClcBlueprintPackage(): +class ClcBlueprintPackage: clc = clc_sdk module = None @@ -133,9 +133,9 @@ def process_request(self): :return: Returns with either an exit_json or fail_json """ p = self.module.params - + changed = False + changed_server_ids = [] self._set_clc_credentials_from_env() - server_ids = p['server_ids'] package_id = p['package_id'] package_params = p['package_params'] From 9df04739329db72be59d862fcf57435ad8d37c95 Mon Sep 17 00:00:00 2001 From: Michael Schuett Date: Fri, 24 Jul 2015 17:48:44 -0400 Subject: [PATCH 0496/2522] ec2_search module This module lets you get information about any number of ec2 instances in your environment. It also has the option of creating hostnames based on the ip of your server. --- cloud/amazon/ec2_search.py | 152 +++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 cloud/amazon/ec2_search.py diff --git a/cloud/amazon/ec2_search.py b/cloud/amazon/ec2_search.py new file mode 100644 index 00000000000..b6ce223b158 --- /dev/null +++ b/cloud/amazon/ec2_search.py @@ -0,0 +1,152 @@ +#!/usr/bin/python +# +# This is a free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This Ansible library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this library. If not, see . + +DOCUMENTATION = ''' +--- +module: ec2_search +short_description: ask EC2 for information about other instances. +description: + - Only supports seatch for hostname by tags currently. Looking to add more later. +version_added: "1.9" +options: + key: + description: + - instance tag key in EC2 + required: false + default: Name + aliases: [] + value: + description: + - instance tag value in EC2 + required: false + default: null + aliases: [] + lookup: + description: + - What type of lookup to use when searching EC2 instance info. + required: false + default: tags + aliases: [] + region: + description: + - EC2 region that it should look for tags in + required: false + default: All Regions + aliases: [] +author: + - "Michael Schuett (@michaeljs1990)" +extends_documentation_fragment: aws +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Basic provisioning example +- ec2_search: + key: mykey + value: myvalue + +''' +try: + import boto + import boto.ec2 + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +def todict(obj, classkey=None): + if isinstance(obj, dict): + data = {} + for (k, v) in obj.items(): + data[k] = todict(v, classkey) + return data + elif hasattr(obj, "_ast"): + return todict(obj._ast()) + elif hasattr(obj, "__iter__"): + return [todict(v, classkey) for v in obj] + elif hasattr(obj, "__dict__"): + # This Class causes a recursive loop and at this time is not worth + # debugging. If it's useful later I'll look into it. + if not isinstance(obj, boto.ec2.blockdevicemapping.BlockDeviceType): + data = dict([(key, todict(value, classkey)) + for key, value in obj.__dict__.iteritems() + if not callable(value) and not key.startswith('_')]) + if classkey is not None and hasattr(obj, "__class__"): + data[classkey] = obj.__class__.__name__ + return data + else: + return obj + +def get_all_ec2_regions(module): + try: + regions = boto.ec2.regions() + except Exception, e: + module.fail_json('Boto authentication issue: %s' % e) + + return regions + +# Connect to ec2 region +def connect_to_region(region, module): + try: + conn = boto.ec2.connect_to_region(region.name) + except Exception, e: + print module.jsonify('error connecting to region: ' + region.name) + conn = None + # connect_to_region will fail "silently" by returning + # None if the region name is wrong or not supported + return conn + +def main(): + module = AnsibleModule( + argument_spec = dict( + key = dict(default='Name'), + value = dict(), + lookup = dict(default='tags'), + ignore_state = dict(default='terminated'), + region = dict(), + ) + ) + + if not HAS_BOTO: + module.fail_json(msg='boto required for this module') + + server_info = list() + + for region in get_all_ec2_regions(module): + conn = connect_to_region(region, module) + try: + # Run when looking up by tag names, only returning hostname currently + if module.params.get('lookup') == 'tags': + ec2_key = 'tag:' + module.params.get('key') + ec2_value = module.params.get('value') + reservations = conn.get_all_instances(filters={ec2_key : ec2_value}) + for instance in [i for r in reservations for i in r.instances]: + if instance.private_ip_address != None: + instance.hostname = 'ip-' + instance.private_ip_address.replace('.', '-') + if instance._state.name not in module.params.get('ignore_state') and : + server_info.append(todict(instance)) + except: + print module.jsonify('error getting instances from: ' + region.name) + + ansible_facts = {'info': server_info} + ec2_facts_result = dict(changed=True, ec2=ansible_facts) + + module.exit_json(**ec2_facts_result) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +main() From 1c6597ec733269b2f21d4e8cf84c11409bf80125 Mon Sep 17 00:00:00 2001 From: Michael Schuett Date: Fri, 24 Jul 2015 17:54:47 -0400 Subject: [PATCH 0497/2522] Typo Remove typo from when I recently was testing logic for also having an include state. --- cloud/amazon/ec2_search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_search.py b/cloud/amazon/ec2_search.py index b6ce223b158..cc8929d5f37 100644 --- a/cloud/amazon/ec2_search.py +++ b/cloud/amazon/ec2_search.py @@ -135,7 +135,7 @@ def main(): for instance in [i for r in reservations for i in r.instances]: if instance.private_ip_address != None: instance.hostname = 'ip-' + instance.private_ip_address.replace('.', '-') - if instance._state.name not in module.params.get('ignore_state') and : + if instance._state.name not in module.params.get('ignore_state'): server_info.append(todict(instance)) except: print module.jsonify('error getting instances from: ' + region.name) From c64c2995a4b580934bfed0fe61d2049fe826df23 Mon Sep 17 00:00:00 2001 From: Michael Schuett Date: Fri, 24 Jul 2015 18:09:45 -0400 Subject: [PATCH 0498/2522] Document ignore_state Added documentation for ignore_state and updated the example since you would really only use this module if you are going to register it to a variable. --- cloud/amazon/ec2_search.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_search.py b/cloud/amazon/ec2_search.py index cc8929d5f37..0114bec14b5 100644 --- a/cloud/amazon/ec2_search.py +++ b/cloud/amazon/ec2_search.py @@ -45,6 +45,12 @@ required: false default: All Regions aliases: [] + ignore_state: + description: + - instance state that should be ignored such as terminated. + required: false + default: terminated + aliases: [] author: - "Michael Schuett (@michaeljs1990)" extends_documentation_fragment: aws @@ -57,7 +63,7 @@ - ec2_search: key: mykey value: myvalue - + register: servers ''' try: import boto From e35778feba981bc4bd481f080d6c64ae7ca1c864 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Fri, 24 Jul 2015 19:16:52 -0400 Subject: [PATCH 0499/2522] removed redundant version added --- cloud/vmware/vca_vapp.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/cloud/vmware/vca_vapp.py b/cloud/vmware/vca_vapp.py index 1b3aeff93c0..4c6c90db0ac 100644 --- a/cloud/vmware/vca_vapp.py +++ b/cloud/vmware/vca_vapp.py @@ -25,147 +25,123 @@ version_added: "2.0" options: username: - version_added: "2.0" description: - The vca username or email address, if not set the environment variable VCA_USER is checked for the username. required: false default: None password: - version_added: "2.0" description: - The vca password, if not set the environment variable VCA_PASS is checked for the password required: false default: None org: - version_added: "2.0" description: - The org to login to for creating vapp, mostly set when the service_type is vdc. required: false default: None service_id: - version_added: "2.0" description: - The service id in a vchs environment to be used for creating the vapp required: false default: None host: - version_added: "2.0" description: - The authentication host to be used when service type is vcd. required: false default: None api_version: - version_added: "2.0" description: - The api version to be used with the vca required: false default: "5.7" service_type: - version_added: "2.0" description: - The type of service we are authenticating against required: false default: vca choices: [ "vca", "vchs", "vcd" ] state: - version_added: "2.0" description: - if the object should be added or removed required: false default: present choices: [ "present", "absent" ] catalog_name: - version_added: "2.0" description: - The catalog from which the vm template is used. required: false default: "Public Catalog" script: - version_added: "2.0" description: - The path to script that gets injected to vm during creation. required: false default: "Public Catalog" template_name: - version_added: "2.0" description: - The template name from which the vm should be created. required: True network_name: - version_added: "2.0" description: - The network name to which the vm should be attached. required: false default: 'None' network_ip: - version_added: "2.0" description: - The ip address that should be assigned to vm when the ip assignment type is static required: false default: None network_mode: - version_added: "2.0" description: - The network mode in which the ip should be allocated. required: false default: pool choices: [ "pool", "dhcp", 'static' ] instance_id:: - version_added: "2.0" description: - The instance id of the region in vca flavour where the vm should be created required: false default: None wait: - version_added: "2.0" description: - If the module should wait if the operation is poweroff or poweron, is better to wait to report the right state. required: false default: True wait_timeout: - version_added: "2.0" description: - The wait timeout when wait is set to true required: false default: 250 vdc_name: - version_added: "2.0" description: - The name of the vdc where the vm should be created. required: false default: None vm_name: - version_added: "2.0" description: - The name of the vm to be created, the vapp is named the same as the vapp name required: false default: 'default_ansible_vm1' vm_cpus: - version_added: "2.0" description: - The number if cpus to be added to the vm required: false default: None vm_memory: - version_added: "2.0" description: - The amount of memory to be added to vm in megabytes required: false default: None verify_certs: - version_added: "2.0" description: - If the certificates of the authentication is to be verified required: false default: True admin_password: - version_added: "2.0" description: - The password to be set for admin required: false default: None operation: - version_added: "2.0" description: - The operation to be done on the vm required: false From dfdd2bb5edf6b29a7982687c32d23cb5e8603eab Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Fri, 24 Jul 2015 23:16:35 -0400 Subject: [PATCH 0500/2522] fixed license and copyright --- cloud/vmware/vca_vapp.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/cloud/vmware/vca_vapp.py b/cloud/vmware/vca_vapp.py index 4c6c90db0ac..ef8e52c421b 100644 --- a/cloud/vmware/vca_vapp.py +++ b/cloud/vmware/vca_vapp.py @@ -1,19 +1,21 @@ #!/usr/bin/python -# Copyright (c) 2015 VMware, Inc. All Rights Reserved. +# Copyright (c) 2015 Ansible, Inc. # -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at +# This file is part of Ansible # -# http://www.apache.org/licenses/LICENSE-2.0 +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . DOCUMENTATION = ''' From ca1daba8bbc6502ee415fc3e6da1ae0b85baa7dc Mon Sep 17 00:00:00 2001 From: Konstantin Shalygin Date: Tue, 31 Mar 2015 14:18:22 +0600 Subject: [PATCH 0501/2522] add upgrade future, patch by n0vember- --- packaging/os/pacman.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/packaging/os/pacman.py b/packaging/os/pacman.py index 102865bc443..6859c9d5b86 100644 --- a/packaging/os/pacman.py +++ b/packaging/os/pacman.py @@ -63,6 +63,13 @@ required: false default: "no" choices: ["yes", "no"] + + upgrade + description: + - Whether or not to upgrade whole system + required: false + default: "no" + choices: ["yes", "no"] ''' EXAMPLES = ''' @@ -80,6 +87,9 @@ # Run the equivalent of "pacman -Sy" as a separate step - pacman: update_cache=yes + +# Run the equivalent of "pacman -Su" as a separate step +- pacman: upgrade=yes ''' import json @@ -132,6 +142,27 @@ def update_package_db(module): else: module.fail_json(msg="could not update package db") +def upgrade(module): + cmdupgrade = "pacman -Suq --noconfirm" + cmdneedrefresh = "pacman -Supq" + rc, stdout, stderr = module.run_command(cmdneedrefresh, check_rc=False) + +def upgrade(module): + cmdupgrade = "pacman -Suq --noconfirm" + cmdneedrefresh = "pacman -Supq" + rc, stdout, stderr = module.run_command(cmdneedrefresh, check_rc=False) + + if rc == 0: + if stdout.count('\n') > 1: + rc, stdout, stderr = module.run_command(cmdupgrade, check_rc=False) + if rc == 0: + module.exit_json(changed=True, msg='System upgraded') + else: + module.fail_json(msg="could not upgrade") + else: + module.exit_json(changed=False, msg='Nothing to upgrade') + else: + module.fail_json(msg="could not list upgrades") def remove_packages(module, packages): if module.params["recurse"]: @@ -213,8 +244,9 @@ def main(): name = dict(aliases=['pkg']), state = dict(default='present', choices=['present', 'installed', "latest", 'absent', 'removed']), recurse = dict(default='no', choices=BOOLEANS, type='bool'), + upgrade = dict(default='no', choices=BOOLEANS, type='bool'), update_cache = dict(default='no', aliases=['update-cache'], choices=BOOLEANS, type='bool')), - required_one_of = [['name', 'update_cache']], + required_one_of = [['name', 'update_cache', 'upgrade']], supports_check_mode = True) if not os.path.exists(PACMAN_PATH): @@ -236,6 +268,9 @@ def main(): if p['update_cache'] and module.check_mode and not p['name']: module.exit_json(changed=True, msg='Would have updated the package cache') + if p['upgrade']: + upgrade(module) + if p['name']: pkgs = p['name'].split(',') From d8d90ecb03ccbfc232fa304d7517bf27cb015180 Mon Sep 17 00:00:00 2001 From: Konstantin Shalygin Date: Tue, 31 Mar 2015 14:28:35 +0600 Subject: [PATCH 0502/2522] add force remove feature --- packaging/os/pacman.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packaging/os/pacman.py b/packaging/os/pacman.py index 6859c9d5b86..8abd04bafc6 100644 --- a/packaging/os/pacman.py +++ b/packaging/os/pacman.py @@ -56,6 +56,14 @@ choices: ["yes", "no"] version_added: "1.3" + force: + description: + - Force remove package, without any checks. + required: false + default: "no" + choices: ["yes", "no"] + version_added: "1.3" + update_cache: description: - Whether or not to refresh the master package lists. This can be @@ -90,6 +98,9 @@ # Run the equivalent of "pacman -Su" as a separate step - pacman: upgrade=yes + +# Run the equivalent of "pacman -Rdd", force remove package baz +- pacman: name=baz state=absent force=yes ''' import json @@ -170,6 +181,12 @@ def remove_packages(module, packages): else: args = "R" +def remove_packages(module, packages): + if module.params["force"]: + args = "Rdd" + else: + args = "R" + remove_c = 0 # Using a for loop incase of error, we can report the package that failed for package in packages: @@ -244,6 +261,7 @@ def main(): name = dict(aliases=['pkg']), state = dict(default='present', choices=['present', 'installed', "latest", 'absent', 'removed']), recurse = dict(default='no', choices=BOOLEANS, type='bool'), + force = dict(default='no', choices=BOOLEANS, type='bool'), upgrade = dict(default='no', choices=BOOLEANS, type='bool'), update_cache = dict(default='no', aliases=['update-cache'], choices=BOOLEANS, type='bool')), required_one_of = [['name', 'update_cache', 'upgrade']], From 8f3a2a8e3d2fd843e62801bc390837eefb4be4e4 Mon Sep 17 00:00:00 2001 From: Konstantin Shalygin Date: Wed, 1 Apr 2015 15:27:31 +0600 Subject: [PATCH 0503/2522] add 'version_add' --- packaging/os/pacman.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packaging/os/pacman.py b/packaging/os/pacman.py index 8abd04bafc6..fda700a67b0 100644 --- a/packaging/os/pacman.py +++ b/packaging/os/pacman.py @@ -62,7 +62,7 @@ required: false default: "no" choices: ["yes", "no"] - version_added: "1.3" + version_added: "2.0" update_cache: description: @@ -72,12 +72,13 @@ default: "no" choices: ["yes", "no"] - upgrade + upgrade: description: - Whether or not to upgrade whole system required: false default: "no" choices: ["yes", "no"] + version_added: "2.0" ''' EXAMPLES = ''' @@ -153,11 +154,6 @@ def update_package_db(module): else: module.fail_json(msg="could not update package db") -def upgrade(module): - cmdupgrade = "pacman -Suq --noconfirm" - cmdneedrefresh = "pacman -Supq" - rc, stdout, stderr = module.run_command(cmdneedrefresh, check_rc=False) - def upgrade(module): cmdupgrade = "pacman -Suq --noconfirm" cmdneedrefresh = "pacman -Supq" From e41597a03301122314309c4544afd86ad741c86d Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Fri, 24 Jul 2015 23:07:20 -0500 Subject: [PATCH 0504/2522] Improve pacman module - detect and use pacman_path via get_bin_path helper - simplify pending upgrade detection - apply outstanding changes from #358, #41 --- packaging/os/pacman.py | 74 ++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 38 deletions(-) diff --git a/packaging/os/pacman.py b/packaging/os/pacman.py index fda700a67b0..74e29a1f936 100644 --- a/packaging/os/pacman.py +++ b/packaging/os/pacman.py @@ -110,8 +110,6 @@ import re import sys -PACMAN_PATH = "/usr/bin/pacman" - def get_version(pacman_output): """Take pacman -Qi or pacman -Si output and get the Version""" lines = pacman_output.split('\n') @@ -120,19 +118,19 @@ def get_version(pacman_output): return line.split(':')[1].strip() return None -def query_package(module, name, state="present"): +def query_package(module, pacman_path, name, state="present"): """Query the package status in both the local system and the repository. Returns a boolean to indicate if the package is installed, and a second boolean to indicate if the package is up-to-date.""" if state == "present": - lcmd = "pacman -Qi %s" % (name) + lcmd = "%s -Qi %s" % (pacman_path, name) lrc, lstdout, lstderr = module.run_command(lcmd, check_rc=False) if lrc != 0: # package is not installed locally return False, False - + # get the version installed locally (if any) lversion = get_version(lstdout) - - rcmd = "pacman -Si %s" % (name) + + rcmd = "%s -Si %s" % (pacman_path, name) rrc, rstdout, rstderr = module.run_command(rcmd, check_rc=False) # get the version in the repository rversion = get_version(rstdout) @@ -145,8 +143,8 @@ def query_package(module, name, state="present"): return False, False -def update_package_db(module): - cmd = "pacman -Sy" +def update_package_db(module, pacman_path): + cmd = "%s -Sy" % (pacman_path) rc, stdout, stderr = module.run_command(cmd, check_rc=False) if rc == 0: @@ -154,30 +152,27 @@ def update_package_db(module): else: module.fail_json(msg="could not update package db") -def upgrade(module): - cmdupgrade = "pacman -Suq --noconfirm" - cmdneedrefresh = "pacman -Supq" +def upgrade(module, pacman_path): + cmdupgrade = "%s -Suq --noconfirm" % (pacman_path) + cmdneedrefresh = "%s -Qqu" % (pacman_path) rc, stdout, stderr = module.run_command(cmdneedrefresh, check_rc=False) if rc == 0: - if stdout.count('\n') > 1: - rc, stdout, stderr = module.run_command(cmdupgrade, check_rc=False) - if rc == 0: - module.exit_json(changed=True, msg='System upgraded') - else: - module.fail_json(msg="could not upgrade") + rc, stdout, stderr = module.run_command(cmdupgrade, check_rc=False) + if rc == 0: + module.exit_json(changed=True, msg='System upgraded') else: - module.exit_json(changed=False, msg='Nothing to upgrade') + module.fail_json(msg="could not upgrade") else: - module.fail_json(msg="could not list upgrades") + module.exit_json(changed=False, msg='Nothing to upgrade') -def remove_packages(module, packages): +def remove_packages(module, pacman_path, packages): if module.params["recurse"]: args = "Rs" else: args = "R" -def remove_packages(module, packages): +def remove_packages(module, pacman_path, packages): if module.params["force"]: args = "Rdd" else: @@ -187,11 +182,11 @@ def remove_packages(module, packages): # Using a for loop incase of error, we can report the package that failed for package in packages: # Query the package first, to see if we even need to remove - installed, updated = query_package(module, package) + installed, updated = query_package(module, pacman_path, package) if not installed: continue - cmd = "pacman -%s %s --noconfirm" % (args, package) + cmd = "%s -%s %s --noconfirm" % (pacman_path, args, package) rc, stdout, stderr = module.run_command(cmd, check_rc=False) if rc != 0: @@ -206,12 +201,12 @@ def remove_packages(module, packages): module.exit_json(changed=False, msg="package(s) already absent") -def install_packages(module, state, packages, package_files): +def install_packages(module, pacman_path, state, packages, package_files): install_c = 0 for i, package in enumerate(packages): # if the package is installed and state == present or state == latest and is up-to-date then skip - installed, updated = query_package(module, package) + installed, updated = query_package(module, pacman_path, package) if installed and (state == 'present' or (state == 'latest' and updated)): continue @@ -220,7 +215,7 @@ def install_packages(module, state, packages, package_files): else: params = '-S %s' % package - cmd = "pacman %s --noconfirm" % (params) + cmd = "%s %s --noconfirm" % (pacman_path, params) rc, stdout, stderr = module.run_command(cmd, check_rc=False) if rc != 0: @@ -234,10 +229,10 @@ def install_packages(module, state, packages, package_files): module.exit_json(changed=False, msg="package(s) already installed") -def check_packages(module, packages, state): +def check_packages(module, pacman_path, packages, state): would_be_changed = [] for package in packages: - installed, updated = query_package(module, package) + installed, updated = query_package(module, pacman_path, package) if ((state in ["present", "latest"] and not installed) or (state == "absent" and installed) or (state == "latest" and not updated)): @@ -263,8 +258,10 @@ def main(): required_one_of = [['name', 'update_cache', 'upgrade']], supports_check_mode = True) - if not os.path.exists(PACMAN_PATH): - module.fail_json(msg="cannot find pacman, looking for %s" % (PACMAN_PATH)) + pacman_path = module.get_bin_path('pacman', True) + + if not os.path.exists(pacman_path): + module.fail_json(msg="cannot find pacman, in path %s" % (pacman_path)) p = module.params @@ -275,7 +272,7 @@ def main(): p['state'] = 'absent' if p["update_cache"] and not module.check_mode: - update_package_db(module) + update_package_db(module, pacman_path) if not p['name']: module.exit_json(changed=True, msg='updated the package master lists') @@ -283,7 +280,7 @@ def main(): module.exit_json(changed=True, msg='Would have updated the package cache') if p['upgrade']: - upgrade(module) + upgrade(module, pacman_path) if p['name']: pkgs = p['name'].split(',') @@ -299,14 +296,15 @@ def main(): pkg_files.append(None) if module.check_mode: - check_packages(module, pkgs, p['state']) + check_packages(module, pacman_path, pkgs, p['state']) if p['state'] in ['present', 'latest']: - install_packages(module, p['state'], pkgs, pkg_files) + install_packages(module, pacman_path, p['state'], pkgs, pkg_files) elif p['state'] == 'absent': - remove_packages(module, pkgs) + remove_packages(module, pacman_path, pkgs) # import module snippets from ansible.module_utils.basic import * - -main() + +if __name__ == "__main__": + main() From 1d5afdc139075fd8692a6697914fb491450d8bf2 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sat, 25 Jul 2015 11:57:21 +0200 Subject: [PATCH 0505/2522] Do not assume that /var/lib/locales/supported.d/local exist Since people can generate their own image with debootstrap, and this wouldn't create a file /var/lib/locales/supported.d/local, better check if it exist and work if it doesn't. Fix #656 --- system/locale_gen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/locale_gen.py b/system/locale_gen.py index d10fc90ad45..1988ce4f3b0 100644 --- a/system/locale_gen.py +++ b/system/locale_gen.py @@ -176,7 +176,7 @@ def main(): state = module.params['state'] if not os.path.exists("/etc/locale.gen"): - if os.path.exists("/var/lib/locales/supported.d/local"): + if os.path.exists("/var/lib/locales/supported.d/"): # Ubuntu created its own system to manage locales. ubuntuMode = True else: From c089ab0dd812b89fd2422f8cb8ca5a477ba4e648 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sat, 25 Jul 2015 13:58:45 +0200 Subject: [PATCH 0506/2522] Move examples to EXAMPLES variable Partially fix #507 --- packaging/language/cpanm.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/packaging/language/cpanm.py b/packaging/language/cpanm.py index 02b306b669c..3749fd29db2 100644 --- a/packaging/language/cpanm.py +++ b/packaging/language/cpanm.py @@ -58,24 +58,31 @@ - Use the mirror's index file instead of the CPAN Meta DB required: false default: false -examples: - - code: "cpanm: name=Dancer" - description: Install I(Dancer) perl package. - - code: "cpanm: name=MIYAGAWA/Plack-0.99_05.tar.gz" - description: Install version 0.99_05 of the I(Plack) perl package. - - code: "cpanm: name=Dancer locallib=/srv/webapps/my_app/extlib" - description: "Install I(Dancer) (U(http://perldancer.org/)) into the specified I(locallib)" - - code: "cpanm: from_path=/srv/webapps/my_app/src/" - description: Install perl dependencies from local directory. - - code: "cpanm: name=Dancer notest=True locallib=/srv/webapps/my_app/extlib" - description: Install I(Dancer) perl package without running the unit tests in indicated I(locallib). - - code: "cpanm: name=Dancer mirror=http://cpan.cpantesters.org/" - description: Install I(Dancer) perl package from a specific mirror notes: - Please note that U(http://search.cpan.org/dist/App-cpanminus/bin/cpanm, cpanm) must be installed on the remote host. author: "Franck Cuny (@franckcuny)" ''' +EXAMPLES = ''' +# install Dancer perl package +- cpanm: name=Dancer + +# install version 0.99_05 of the Plack perl package +- cpanm: name=MIYAGAWA/Plack-0.99_05.tar.gz + +# install Dancer into the specified locallib +- cpanm: name=Dancer locallib=/srv/webapps/my_app/extlib + +# install perl dependencies from local directory +- cpanm: from_path=/srv/webapps/my_app/src/ + +# install Dancer perl package without running the unit tests in indicated locallib +- cpanm: name=Dancer notest=True locallib=/srv/webapps/my_app/extlib + +# install Dancer perl package from a specific mirror +- cpanm: name=Dancer mirror=http://cpan.cpantesters.org/ +''' + def _is_package_installed(module, name, locallib, cpanm): cmd = "" if locallib: From 162f257412f0a5c630c85338689ebcb0c0e01bde Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sat, 25 Jul 2015 14:11:03 +0200 Subject: [PATCH 0507/2522] Use the parameter 'name' everywhere for consistency Fix https://github.com/ansible/ansible/issues/11395 --- system/known_hosts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/known_hosts.py b/system/known_hosts.py index 303d9410d1e..7592574d4e7 100644 --- a/system/known_hosts.py +++ b/system/known_hosts.py @@ -58,7 +58,7 @@ # Example using with_file to set the system known_hosts file - name: tell the host about our servers it might want to ssh to known_hosts: path='/etc/ssh/ssh_known_hosts' - host='foo.com.invalid' + name='foo.com.invalid' key="{{ lookup('file', 'pubkeys/foo.com.invalid') }}" ''' From 8b2cc4f7bbee6cf913d055acb1d515e0b73989f6 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Sat, 25 Jul 2015 18:11:52 +0200 Subject: [PATCH 0508/2522] Remove separate check for Xen Module checked for Xen-based system, however since 'xen:///' URI support exists in 'libvirt', we should use it explicitly instead. --- cloud/misc/virt_net.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/cloud/misc/virt_net.py b/cloud/misc/virt_net.py index b84f976c6f3..cd579a3f785 100644 --- a/cloud/misc/virt_net.py +++ b/cloud/misc/virt_net.py @@ -164,13 +164,7 @@ def __init__(self, uri, module): self.module = module - cmd = "uname -r" - rc, stdout, stderr = self.module.run_command(cmd) - - if "xen" in stdout: - conn = libvirt.open(None) - else: - conn = libvirt.open(uri) + conn = libvirt.open(uri) if not conn: raise Exception("hypervisor connection failure") From 00e7e225ce2dec35f1890a691185bc694cf20e2f Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Sat, 25 Jul 2015 18:14:03 +0200 Subject: [PATCH 0509/2522] Rewrite for loops in a more Pythonic style --- cloud/misc/virt_net.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/cloud/misc/virt_net.py b/cloud/misc/virt_net.py index cd579a3f785..3622d455e48 100644 --- a/cloud/misc/virt_net.py +++ b/cloud/misc/virt_net.py @@ -177,14 +177,12 @@ def find_entry(self, entryid): results = [] # Get active entries - entries = self.conn.listNetworks() - for name in entries: + for name in self.conn.listNetworks(): entry = self.conn.networkLookupByName(name) results.append(entry) # Get inactive entries - entries = self.conn.listDefinedNetworks() - for name in entries: + for name in self.conn.listDefinedNetworks(): entry = self.conn.networkLookupByName(name) results.append(entry) @@ -334,9 +332,8 @@ def list_nets(self, state=None): return results def state(self): - entries = self.list_nets() results = [] - for entry in entries: + for entry in self.list_nets(): state_blurb = self.conn.get_status(entry) results.append("%s %s" % (entry,state_blurb)) return results @@ -378,9 +375,8 @@ def info(self, facts_mode='info'): return self.facts(facts_mode) def facts(self, facts_mode='facts'): - entries = self.list_nets() results = dict() - for entry in entries: + for entry in self.list_nets(): results[entry] = dict() results[entry]["autostart"] = self.conn.get_autostart(entry) results[entry]["persistent"] = self.conn.get_persistent(entry) From dc92f0af4ca6c569a3048c25bdb9efef59c1a006 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Sat, 25 Jul 2015 18:15:23 +0200 Subject: [PATCH 0510/2522] Rewrite method to not use try/except Additional checks are not needed, because 'self.conn.get_entry(-1)' returns all existing entries, each one should have state defined. --- cloud/misc/virt_net.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/cloud/misc/virt_net.py b/cloud/misc/virt_net.py index 3622d455e48..30d796b711f 100644 --- a/cloud/misc/virt_net.py +++ b/cloud/misc/virt_net.py @@ -317,18 +317,13 @@ def get_net(self, entryid): return self.conn.find_entry(entryid) def list_nets(self, state=None): - entries = self.conn.find_entry(-1) results = [] - for x in entries: - try: - if state: - entrystate = self.conn.get_status2(x) - if entrystate == state: - results.append(x.name()) - else: - results.append(x.name()) - except: - pass + for entry in self.conn.find_entry(-1): + if state: + if state == self.conn.get_status2(entry): + results.append(entry.name()) + else: + results.append(entry.name()) return results def state(self): From 2b15b0564cb68962100bfd10dab7c44779223096 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Sat, 25 Jul 2015 18:18:00 +0200 Subject: [PATCH 0511/2522] Add whitespace so diff with 'virt_pool' is easier --- cloud/misc/virt_net.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud/misc/virt_net.py b/cloud/misc/virt_net.py index 30d796b711f..2be753c6a89 100644 --- a/cloud/misc/virt_net.py +++ b/cloud/misc/virt_net.py @@ -501,6 +501,7 @@ def core(module): module.fail_json(msg="expected state or command parameter to be specified") + def main(): module = AnsibleModule ( From 13e51060ec2bd9940fae48f4112282733097c380 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Sat, 25 Jul 2015 18:18:39 +0200 Subject: [PATCH 0512/2522] Remove unused parameter from method arguments --- cloud/misc/virt_net.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/misc/virt_net.py b/cloud/misc/virt_net.py index 2be753c6a89..078275cd84a 100644 --- a/cloud/misc/virt_net.py +++ b/cloud/misc/virt_net.py @@ -366,8 +366,8 @@ def get_xml(self, entryid): def define(self, entryid, xml): return self.conn.define_from_xml(entryid, xml) - def info(self, facts_mode='info'): - return self.facts(facts_mode) + def info(self): + return self.facts(facts_mode='info') def facts(self, facts_mode='facts'): results = dict() From 055a31ba0733be97e534786db29f0554d5161d70 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Sat, 25 Jul 2015 18:21:10 +0200 Subject: [PATCH 0513/2522] Remove separate check for Xen Module checked for Xen-based system, however since 'xen:///' URI support exists in 'libvirt', we should use it explicitly instead. --- cloud/misc/virt_pool.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/cloud/misc/virt_pool.py b/cloud/misc/virt_pool.py index 12cd85a07e8..dbe173567c8 100644 --- a/cloud/misc/virt_pool.py +++ b/cloud/misc/virt_pool.py @@ -202,13 +202,7 @@ def __init__(self, uri, module): self.module = module - cmd = "uname -r" - rc, stdout, stderr = self.module.run_command(cmd) - - if "xen" in stdout: - conn = libvirt.open(None) - else: - conn = libvirt.open(uri) + conn = libvirt.open(uri) if not conn: raise Exception("hypervisor connection failure") From d8b6dd5927b44f081fc5e5382013f38a2d0c049c Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Sat, 25 Jul 2015 18:22:31 +0200 Subject: [PATCH 0514/2522] Rewrite for loops in a more Pythonic style --- cloud/misc/virt_pool.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/cloud/misc/virt_pool.py b/cloud/misc/virt_pool.py index dbe173567c8..3c31c82a0f1 100644 --- a/cloud/misc/virt_pool.py +++ b/cloud/misc/virt_pool.py @@ -215,14 +215,12 @@ def find_entry(self, entryid): results = [] # Get active entries - entries = self.conn.listStoragePools() - for name in entries: + for name in self.conn.listStoragePools(): entry = self.conn.storagePoolLookupByName(name) results.append(entry) # Get inactive entries - entries = self.conn.listDefinedStoragePools() - for name in entries: + for name in self.conn.listDefinedStoragePools(): entry = self.conn.storagePoolLookupByName(name) results.append(entry) @@ -422,9 +420,8 @@ def list_pools(self, state=None): return results def state(self): - entries = self.list_pools() results = [] - for entry in entries: + for entry in self.list_pools(): state_blurb = self.conn.get_status(entry) results.append("%s %s" % (entry,state_blurb)) return results @@ -475,9 +472,8 @@ def info(self, facts_mode='info'): return self.facts(facts_mode) def facts(self, facts_mode='facts'): - entries = self.list_pools() results = dict() - for entry in entries: + for entry in self.list_pools(): results[entry] = dict() if self.conn.find_entry(entry): data = self.conn.get_info(entry) From 39b635ae799eb51cf8e2f4618b3a292453668cc5 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Sat, 25 Jul 2015 18:23:01 +0200 Subject: [PATCH 0515/2522] Rewrite method to not use try/except Additional checks are not needed, because 'self.conn.get_entry(-1)' returns all existing entries, each one should have state defined. --- cloud/misc/virt_pool.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/cloud/misc/virt_pool.py b/cloud/misc/virt_pool.py index 3c31c82a0f1..4ca06e2a63a 100644 --- a/cloud/misc/virt_pool.py +++ b/cloud/misc/virt_pool.py @@ -405,18 +405,13 @@ def get_pool(self, entryid): return self.conn.find_entry(entryid) def list_pools(self, state=None): - entries = self.conn.find_entry(-1) results = [] - for x in entries: - try: - if state: - entrystate = self.conn.get_status2(x) - if entrystate == state: - results.append(x.name()) - else: - results.append(x.name()) - except: - pass + for entry in self.conn.find_entry(-1): + if state: + if state == self.conn.get_status2(entry): + results.append(entry.name()) + else: + results.append(entry.name()) return results def state(self): From c011923e3b97a5d0807d6962a2a1f934fcf243f5 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Sat, 25 Jul 2015 18:23:39 +0200 Subject: [PATCH 0516/2522] Remove unused parameter from method arguments --- cloud/misc/virt_pool.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/misc/virt_pool.py b/cloud/misc/virt_pool.py index 4ca06e2a63a..54489495353 100644 --- a/cloud/misc/virt_pool.py +++ b/cloud/misc/virt_pool.py @@ -463,8 +463,8 @@ def delete(self, entryid, flags): def refresh(self, entryid): return self.conn.refresh(entryid) - def info(self, facts_mode='info'): - return self.facts(facts_mode) + def info(self): + return self.facts(facts_mode='info') def facts(self, facts_mode='facts'): results = dict() From c08fa1269d511f8266e4ef61e921bdf2b55f91dc Mon Sep 17 00:00:00 2001 From: Michael Schuett Date: Sat, 25 Jul 2015 18:42:11 -0400 Subject: [PATCH 0517/2522] Remove unneeded nesting The double nesting causes an issue with setting a default empty list if you need to loop over this using with_items. This fixes the issue since it looks like ansible silently fails at setting the default if you use with_items: registered_var['one']['two'] where one is not set. --- cloud/amazon/ec2_search.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cloud/amazon/ec2_search.py b/cloud/amazon/ec2_search.py index 0114bec14b5..5bc1698501c 100644 --- a/cloud/amazon/ec2_search.py +++ b/cloud/amazon/ec2_search.py @@ -146,8 +146,7 @@ def main(): except: print module.jsonify('error getting instances from: ' + region.name) - ansible_facts = {'info': server_info} - ec2_facts_result = dict(changed=True, ec2=ansible_facts) + ec2_facts_result = dict(changed=True, ec2=server_info) module.exit_json(**ec2_facts_result) From 2662ec9669751cdf80d111fa4bf45cd6a6a501f7 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Mon, 29 Sep 2014 21:24:14 -0400 Subject: [PATCH 0518/2522] Add selinux_permissive module, to be able to manage permissive domain --- system/selinux_permissive.py | 130 +++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 system/selinux_permissive.py diff --git a/system/selinux_permissive.py b/system/selinux_permissive.py new file mode 100644 index 00000000000..ec3575d9da4 --- /dev/null +++ b/system/selinux_permissive.py @@ -0,0 +1,130 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Michael Scherer +# inspired by code of github.com/dandiker/ +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: selinux_permissive +short_description: Change permissive domain in SELinux policy +description: + - Add and remove domain from the list of permissive domain. +version_added: "1.9" +options: + domain: + description: + - "the domain that will be added or removed from the list of permissive domains" + required: true + permissive: + description: + - "indicate if the domain should or should not be set as permissive" + required: true + choices: [ 'True', 'False' ] + no_reload: + description: + - "automatically reload the policy after a change" + - "default is set to 'false' as that's what most people would want after changing one domain" + - "Note that this doesn't work on older version of the library (example EL 6), the module will silently ignore it in this case" + required: false + default: False + choices: [ 'True', 'False' ] + store: + description: + - "name of the SELinux policy store to use" + required: false + default: null +notes: + - Requires a version of SELinux recent enough ( ie EL 6 or newer ) +requirements: [ policycoreutils-python ] +author: Michael Scherer +''' + +EXAMPLES = ''' +- selinux_permissive: name=httpd_t permissive=true +''' + +HAVE_SEOBJECT = False +try: + import seobject + HAVE_SEOBJECT = True +except ImportError: + pass + + +def main(): + module = AnsibleModule( + argument_spec=dict( + domain=dict(aliases=['name'], required=True), + store=dict(required=False, default=''), + permissive=dict(type='bool', required=True), + no_reload=dict(type='bool', required=False, default=False), + ), + supports_check_mode=True + ) + + # global vars + changed = False + store = module.params['store'] + permissive = module.params['permissive'] + domain = module.params['domain'] + no_reload = module.params['no_reload'] + + if not HAVE_SEOBJECT: + module.fail_json(changed=False, msg="policycoreutils-python required for this module") + + try: + permissive_domains = seobject.permissiveRecords(store) + except ValueError, e: + module.fail_json(domain=domain, msg=str(e)) + + # not supported on EL 6 + if 'set_reload' in dir(permissive_domains): + permissive_domains.set_reload(not no_reload) + + try: + all_domains = permissive_domains.get_all() + except ValueError, e: + module.fail_json(domain=domain, msg=str(e)) + + if permissive: + if domain not in all_domains: + if not module.check_mode: + try: + permissive_domains.add(domain) + except ValueError, e: + module.fail_json(domain=domain, msg=str(e)) + changed = True + else: + if domain in all_domains: + if not module.check_mode: + try: + permissive_domains.delete(domain) + except ValueError, e: + module.fail_json(domain=domain, msg=str(e)) + changed = True + + module.exit_json(changed=changed, store=store, + permissive=permissive, domain=domain) + + +################################################# +# import module snippets +from ansible.module_utils.basic import * + +main() From 439bccc286ba57b5f7dec5eb5686bda939729401 Mon Sep 17 00:00:00 2001 From: Nando Vieira Date: Mon, 27 Jul 2015 01:45:03 -0300 Subject: [PATCH 0519/2522] Use correct variable name. --- notification/hipchat.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/notification/hipchat.py b/notification/hipchat.py index 57e97eaefec..f38461735c7 100644 --- a/notification/hipchat.py +++ b/notification/hipchat.py @@ -71,10 +71,10 @@ # Use Hipchat API version 2 -- hipchat: +- hipchat: api: "https://api.hipchat.com/v2/" token: OAUTH2_TOKEN - room: notify + room: notify msg: "Ansible task finished" ''' @@ -103,7 +103,7 @@ def send_msg_v1(module, token, room, msg_from, msg, msg_format='text', params['color'] = color params['api'] = api params['notify'] = int(notify) - + url = api + MSG_URI_V1 + "?auth_token=%s" % (token) data = urllib.urlencode(params) @@ -129,10 +129,10 @@ def send_msg_v2(module, token, room, msg_from, msg, msg_format='text', body['message'] = msg body['color'] = color body['message_format'] = msg_format - params['notify'] = notify + body['notify'] = notify POST_URL = api + NOTIFY_URI_V2 - + url = POST_URL.replace('{id_or_name}', room) data = json.dumps(body) From 5eec20df56bc9cd93dc23fdc7656f4f1e12c5b8c Mon Sep 17 00:00:00 2001 From: Alexander Gubin Date: Thu, 23 Jul 2015 13:45:35 +0200 Subject: [PATCH 0520/2522] zypper: Add local/remote rpm installation Add remote rpm example --- packaging/os/zypper.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/packaging/os/zypper.py b/packaging/os/zypper.py index f3205051fdf..5cf2f742f3c 100644 --- a/packaging/os/zypper.py +++ b/packaging/os/zypper.py @@ -31,7 +31,9 @@ DOCUMENTATION = ''' --- module: zypper -author: "Patrick Callahan (@dirtyharrycallahan)" +author: + - "Patrick Callahan (@dirtyharrycallahan)" + - "Alexander Gubin (@alxgu)" version_added: "1.2" short_description: Manage packages on SUSE and openSUSE description: @@ -39,7 +41,7 @@ options: name: description: - - package name or package specifier wth version C(name) or C(name-1.0). + - package name or package specifier with version C(name) or C(name-1.0). You can also pass a url or a local path to a rpm file. required: true aliases: [ 'pkg' ] state: @@ -89,6 +91,12 @@ # Remove the "nmap" package - zypper: name=nmap state=absent + +# Install the nginx rpm from a remote repo +- zypper: name=http://nginx.org/packages/sles/12/x86_64/RPMS/nginx-1.8.0-1.sles12.ngx.x86_64.rpm state=present + +# Install local rpm file +- zypper: name=/tmp/fancy-software.rpm state=present ''' # Function used for getting zypper version @@ -129,6 +137,20 @@ def get_current_version(m, packages): # Function used to find out if a package is currently installed. def get_package_state(m, packages): + for i in range(0, len(packages)): + # Check state of a local rpm-file + if ".rpm" in packages[i]: + # Check if rpm file is available + package = packages[i] + if not os.path.isfile(package) and not '://' in package: + stderr = "No Package file matching '%s' found on system" % package + m.fail_json(msg=stderr) + # Get packagename from rpm file + cmd = ['/bin/rpm', '--query', '--qf', '%{NAME}', '--package'] + cmd.append(package) + rc, stdout, stderr = m.run_command(cmd, check_rc=False) + packages[i] = stdout + cmd = ['/bin/rpm', '--query', '--qf', 'package %{NAME} is installed\n'] cmd.extend(packages) From a778b1eba6f842cf0fb4e5aa100aaa2ac93f1095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kim=20N=C3=B8rgaard?= Date: Mon, 27 Jul 2015 20:21:56 +0200 Subject: [PATCH 0521/2522] Add basic slackpkg support --- packaging/os/slackpkg.py | 183 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 packaging/os/slackpkg.py diff --git a/packaging/os/slackpkg.py b/packaging/os/slackpkg.py new file mode 100644 index 00000000000..943042172ab --- /dev/null +++ b/packaging/os/slackpkg.py @@ -0,0 +1,183 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2014, Kim Nørgaard +# Written by Kim Nørgaard +# Based on pkgng module written by bleader +# that was based on pkgin module written by Shaun Zinck +# that was based on pacman module written by Afterburn +# that was based on apt module written by Matthew Williams +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + + +DOCUMENTATION = ''' +--- +module: slackpkg +short_description: Package manager for Slackware >= 12.2 +description: + - Manage binary packages for Slackware using 'slackpkg' which + is available in versions after 12.2. +version_added: "1.6" +options: + name: + description: + - name of package to install/remove + required: true + + state: + description: + - state of the package + choices: [ 'present', 'absent', 'installed', 'removed', 'latest' ] + required: false + default: present + + update_cache: + description: + - update the package database first + required: false + default: false + choices: [ true, false ] + +author: Kim Nørgaard +notes: [] +''' + +EXAMPLES = ''' +# Install package foo +- slackpkg: name=foo state=present + +# Remove packages foo and bar +- slackpkg: name=foo,bar state=absent +''' + + +import json +import shlex +import os +import sys + +def query_package(module, slackpkg_path, name): + + import glob + import platform + + machine = platform.machine() + packages = glob.glob("/var/log/packages/%s-*-[%s|noarch]*" % (name, machine)) + + if len(packages) > 0: + return True + + return False + + +def remove_packages(module, slackpkg_path, packages): + + remove_c = 0 + # Using a for loop incase of error, we can report the package that failed + for package in packages: + # Query the package first, to see if we even need to remove + if not query_package(module, slackpkg_path, package): + continue + + if not module.check_mode: + rc, out, err = module.run_command("%s -default_answer=y -batch=on remove %s" % (slackpkg_path, package)) + + if not module.check_mode and query_package(module, slackpkg_path, package): + module.fail_json(msg="failed to remove %s: %s" % (package, out)) + + remove_c += 1 + + if remove_c > 0: + + module.exit_json(changed=True, msg="removed %s package(s)" % remove_c) + + module.exit_json(changed=False, msg="package(s) already absent") + + +def install_packages(module, slackpkg_path, packages): + + install_c = 0 + + for package in packages: + if query_package(module, slackpkg_path, package): + continue + + if not module.check_mode: + rc, out, err = module.run_command("%s -default_answer=y -batch=on install %s" % (slackpkg_path, package)) + + if not module.check_mode and not query_package(module, slackpkg_path, package): + module.fail_json(msg="failed to install %s: %s" % (package, out), stderr=err) + + install_c += 1 + + if install_c > 0: + module.exit_json(changed=True, msg="present %s package(s)" % (install_c)) + + module.exit_json(changed=False, msg="package(s) already present") + +def upgrade_packages(module, slackpkg_path, packages): + + install_c = 0 + + for package in packages: + if not module.check_mode: + rc, out, err = module.run_command("%s -default_answer=y -batch=on upgrade %s" % (slackpkg_path, package)) + + if not module.check_mode and not query_package(module, slackpkg_path, package): + module.fail_json(msg="failed to install %s: %s" % (package, out), stderr=err) + + install_c += 1 + + if install_c > 0: + module.exit_json(changed=True, msg="present %s package(s)" % (install_c)) + + module.exit_json(changed=False, msg="package(s) already present") + +def update_cache(module, slackpkg_path): + rc, out, err = module.run_command("%s -batch=on update" % (slackpkg_path)) + if rc != 0: + module.fail_json(msg="Could not update package cache") + +def main(): + module = AnsibleModule( + argument_spec = dict( + state = dict(default="installed", choices=['installed', 'removed', 'absent', 'present', 'latest']), + name = dict(aliases=["pkg"], required=True), + update_cache = dict(default=False, aliases=["update-cache"], type='bool'), + ), + supports_check_mode = True) + + slackpkg_path = module.get_bin_path('slackpkg', True) + + p = module.params + + pkgs = p['name'].split(",") + + if p["update_cache"]: + update_cache(module, slackpkg_path) + + if p['state'] == 'latest': + upgrade_packages(module, slackpkg_path, pkgs) + + elif p['state'] in [ 'present', 'installed' ]: + install_packages(module, slackpkg_path, pkgs) + + elif p["state"] in [ 'removed', 'absent' ]: + remove_packages(module, slackpkg_path, pkgs) + +# import module snippets +from ansible.module_utils.basic import * + +main() From f5a9b458563a20628eef9b5900d11fa8a925e785 Mon Sep 17 00:00:00 2001 From: Siva Popuri Date: Mon, 27 Jul 2015 13:59:16 -0500 Subject: [PATCH 0522/2522] clc_server: Ansible module for creating or deleting servers in Centurylink Cloud --- cloud/centurylink/clc_server.py | 1373 +++++++++++++++++++++++++++++++ 1 file changed, 1373 insertions(+) create mode 100644 cloud/centurylink/clc_server.py diff --git a/cloud/centurylink/clc_server.py b/cloud/centurylink/clc_server.py new file mode 100644 index 00000000000..a53b0ba1e10 --- /dev/null +++ b/cloud/centurylink/clc_server.py @@ -0,0 +1,1373 @@ +#!/usr/bin/python + +# +# Copyright (c) 2015 CenturyLink +# +# This file is part of Ansible. +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see +# + +DOCUMENTATION = ''' +module: clc_server +short_description: Create, Delete, Start and Stop servers in CenturyLink Cloud. +description: + - An Ansible module to Create, Delete, Start and Stop servers in CenturyLink Cloud. +version_added: "2.0" +options: + additional_disks: + description: + - The list of additional disks for the server + required: False + default: [] + add_public_ip: + description: + - Whether to add a public ip to the server + required: False + default: False + choices: [False, True] + alias: + description: + - The account alias to provision the servers under. + required: False + default: None + anti_affinity_policy_id: + description: + - The anti-affinity policy to assign to the server. This is mutually exclusive with 'anti_affinity_policy_name'. + required: False + default: None + anti_affinity_policy_name: + description: + - The anti-affinity policy to assign to the server. This is mutually exclusive with 'anti_affinity_policy_id'. + required: False + default: None + alert_policy_id: + description: + - The alert policy to assign to the server. This is mutually exclusive with 'alert_policy_name'. + required: False + default: None + alert_policy_name: + description: + - The alert policy to assign to the server. This is mutually exclusive with 'alert_policy_id'. + required: False + default: None + count: + description: + - The number of servers to build (mutually exclusive with exact_count) + required: False + default: 1 + count_group: + description: + - Required when exact_count is specified. The Server Group use to determine how many severs to deploy. + required: False + default: None + cpu: + description: + - How many CPUs to provision on the server + default: 1 + required: False + cpu_autoscale_policy_id: + description: + - The autoscale policy to assign to the server. + default: None + required: False + custom_fields: + description: + - The list of custom fields to set on the server. + default: [] + required: False + description: + description: + - The description to set for the server. + default: None + required: False + exact_count: + description: + - Run in idempotent mode. Will insure that this exact number of servers are running in the provided group, + creating and deleting them to reach that count. Requires count_group to be set. + default: None + required: False + group: + description: + - The Server Group to create servers under. + default: 'Default Group' + required: False + ip_address: + description: + - The IP Address for the server. One is assigned if not provided. + default: None + required: False + location: + description: + - The Datacenter to create servers in. + default: None + required: False + managed_os: + description: + - Whether to create the server as 'Managed' or not. + default: False + required: False + choices: [True, False] + memory: + description: + - Memory in GB. + default: 1 + required: False + name: + description: + - A 1 to 6 character identifier to use for the server. This is required when state is 'present' + default: None + required: False + network_id: + description: + - The network UUID on which to create servers. + default: None + required: False + packages: + description: + - The list of blue print packages to run on the server after its created. + default: [] + required: False + password: + description: + - Password for the administrator / root user + default: None + required: False + primary_dns: + description: + - Primary DNS used by the server. + default: None + required: False + public_ip_protocol: + description: + - The protocol to use for the public ip if add_public_ip is set to True. + default: 'TCP' + choices: ['TCP', 'UDP', 'ICMP'] + required: False + public_ip_ports: + description: + - A list of ports to allow on the firewall to the servers public ip, if add_public_ip is set to True. + default: [] + required: False + secondary_dns: + description: + - Secondary DNS used by the server. + default: None + required: False + server_ids: + description: + - Required for started, stopped, and absent states. + A list of server Ids to insure are started, stopped, or absent. + default: [] + required: False + source_server_password: + description: + - The password for the source server if a clone is specified. + default: None + required: False + state: + description: + - The state to insure that the provided resources are in. + default: 'present' + required: False + choices: ['present', 'absent', 'started', 'stopped'] + storage_type: + description: + - The type of storage to attach to the server. + default: 'standard' + required: False + choices: ['standard', 'hyperscale'] + template: + description: + - The template to use for server creation. Will search for a template if a partial string is provided. + This is required when state is 'present' + default: None + required: false + ttl: + description: + - The time to live for the server in seconds. The server will be deleted when this time expires. + default: None + required: False + type: + description: + - The type of server to create. + default: 'standard' + required: False + choices: ['standard', 'hyperscale'] + wait: + description: + - Whether to wait for the provisioning tasks to finish before returning. + default: True + required: False + choices: [True, False] +requirements: + - python = 2.7 + - requests >= 2.5.0 + - clc-sdk +notes: + - To use this module, it is required to set the below environment variables which enables access to the + Centurylink Cloud + - CLC_V2_API_USERNAME, the account login id for the centurylink cloud + - CLC_V2_API_PASSWORD, the account password for the centurylink cloud + - Alternatively, the module accepts the API token and account alias. The API token can be generated using the + CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login + - CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login + - CLC_ACCT_ALIAS, the account alias associated with the centurylink cloud + - Users can set CLC_V2_API_URL to specify an endpoint for pointing to a different CLC environment. +''' + +EXAMPLES = ''' +# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples + +- name: Provision a single Ubuntu Server + clc_server: + name: test + template: ubuntu-14-64 + count: 1 + group: 'Default Group' + state: present + +- name: Ensure 'Default Group' has exactly 5 servers + clc_server: + name: test + template: ubuntu-14-64 + exact_count: 5 + count_group: 'Default Group' + group: 'Default Group' + +- name: Stop a Server + clc_server: + server_ids: ['UC1ACCTTEST01'] + state: stopped + +- name: Start a Server + clc_server: + server_ids: ['UC1ACCTTEST01'] + state: started + +- name: Delete a Server + clc_server: + server_ids: ['UC1ACCTTEST01'] + state: absent +''' + +__version__ = '${version}' + +from time import sleep +from distutils.version import LooseVersion + +try: + import requests +except ImportError: + REQUESTS_FOUND = False +else: + REQUESTS_FOUND = True + +# +# Requires the clc-python-sdk. +# sudo pip install clc-sdk +# +try: + import clc as clc_sdk + from clc import CLCException + from clc import APIFailedResponse +except ImportError: + CLC_FOUND = False + clc_sdk = None +else: + CLC_FOUND = True + + +class ClcServer: + clc = clc_sdk + + def __init__(self, module): + """ + Construct module + """ + self.clc = clc_sdk + self.module = module + self.group_dict = {} + + if not CLC_FOUND: + self.module.fail_json( + msg='clc-python-sdk required for this module') + if not REQUESTS_FOUND: + self.module.fail_json( + msg='requests library is required for this module') + if requests.__version__ and LooseVersion( + requests.__version__) < LooseVersion('2.5.0'): + self.module.fail_json( + msg='requests library version should be >= 2.5.0') + + self._set_user_agent(self.clc) + + def process_request(self): + """ + Process the request - Main Code Path + :return: Returns with either an exit_json or fail_json + """ + changed = False + new_server_ids = [] + server_dict_array = [] + + self._set_clc_credentials_from_env() + self.module.params = self._validate_module_params( + self.clc, + self.module) + p = self.module.params + state = p.get('state') + + # + # Handle each state + # + partial_servers_ids = [] + if state == 'absent': + server_ids = p['server_ids'] + if not isinstance(server_ids, list): + return self.module.fail_json( + msg='server_ids needs to be a list of instances to delete: %s' % + server_ids) + + (changed, + server_dict_array, + new_server_ids) = self._delete_servers(module=self.module, + clc=self.clc, + server_ids=server_ids) + + elif state in ('started', 'stopped'): + server_ids = p.get('server_ids') + if not isinstance(server_ids, list): + return self.module.fail_json( + msg='server_ids needs to be a list of servers to run: %s' % + server_ids) + + (changed, + server_dict_array, + new_server_ids) = self._start_stop_servers(self.module, + self.clc, + server_ids) + + elif state == 'present': + # Changed is always set to true when provisioning new instances + if not p.get('template'): + return self.module.fail_json( + msg='template parameter is required for new instance') + + if p.get('exact_count') is None: + (server_dict_array, + new_server_ids, + partial_servers_ids, + changed) = self._create_servers(self.module, + self.clc) + else: + (server_dict_array, + new_server_ids, + partial_servers_ids, + changed) = self._enforce_count(self.module, + self.clc) + + self.module.exit_json( + changed=changed, + server_ids=new_server_ids, + partially_created_server_ids=partial_servers_ids, + servers=server_dict_array) + + @staticmethod + def _define_module_argument_spec(): + """ + Define the argument spec for the ansible module + :return: argument spec dictionary + """ + argument_spec = dict( + name=dict(), + template=dict(), + group=dict(default='Default Group'), + network_id=dict(), + location=dict(default=None), + cpu=dict(default=1), + memory=dict(default=1), + alias=dict(default=None), + password=dict(default=None), + ip_address=dict(default=None), + storage_type=dict( + default='standard', + choices=[ + 'standard', + 'hyperscale']), + type=dict(default='standard', choices=['standard', 'hyperscale']), + primary_dns=dict(default=None), + secondary_dns=dict(default=None), + additional_disks=dict(type='list', default=[]), + custom_fields=dict(type='list', default=[]), + ttl=dict(default=None), + managed_os=dict(type='bool', default=False), + description=dict(default=None), + source_server_password=dict(default=None), + cpu_autoscale_policy_id=dict(default=None), + anti_affinity_policy_id=dict(default=None), + anti_affinity_policy_name=dict(default=None), + alert_policy_id=dict(default=None), + alert_policy_name=dict(default=None), + packages=dict(type='list', default=[]), + state=dict( + default='present', + choices=[ + 'present', + 'absent', + 'started', + 'stopped']), + count=dict(type='int', default=1), + exact_count=dict(type='int', default=None), + count_group=dict(), + server_ids=dict(type='list', default=[]), + add_public_ip=dict(type='bool', default=False), + public_ip_protocol=dict( + default='TCP', + choices=[ + 'TCP', + 'UDP', + 'ICMP']), + public_ip_ports=dict(type='list', default=[]), + wait=dict(type='bool', default=True)) + + mutually_exclusive = [ + ['exact_count', 'count'], + ['exact_count', 'state'], + ['anti_affinity_policy_id', 'anti_affinity_policy_name'], + ['alert_policy_id', 'alert_policy_name'], + ] + return {"argument_spec": argument_spec, + "mutually_exclusive": mutually_exclusive} + + def _set_clc_credentials_from_env(self): + """ + Set the CLC Credentials on the sdk by reading environment variables + :return: none + """ + env = os.environ + v2_api_token = env.get('CLC_V2_API_TOKEN', False) + v2_api_username = env.get('CLC_V2_API_USERNAME', False) + v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) + clc_alias = env.get('CLC_ACCT_ALIAS', False) + api_url = env.get('CLC_V2_API_URL', False) + + if api_url: + self.clc.defaults.ENDPOINT_URL_V2 = api_url + + if v2_api_token and clc_alias: + self.clc._LOGIN_TOKEN_V2 = v2_api_token + self.clc._V2_ENABLED = True + self.clc.ALIAS = clc_alias + elif v2_api_username and v2_api_passwd: + self.clc.v2.SetCredentials( + api_username=v2_api_username, + api_passwd=v2_api_passwd) + else: + return self.module.fail_json( + msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " + "environment variables") + + @staticmethod + def _validate_module_params(clc, module): + """ + Validate the module params, and lookup default values. + :param clc: clc-sdk instance to use + :param module: module to validate + :return: dictionary of validated params + """ + params = module.params + datacenter = ClcServer._find_datacenter(clc, module) + + ClcServer._validate_types(module) + ClcServer._validate_name(module) + + params['alias'] = ClcServer._find_alias(clc, module) + params['cpu'] = ClcServer._find_cpu(clc, module) + params['memory'] = ClcServer._find_memory(clc, module) + params['description'] = ClcServer._find_description(module) + params['ttl'] = ClcServer._find_ttl(clc, module) + params['template'] = ClcServer._find_template_id(module, datacenter) + params['group'] = ClcServer._find_group(module, datacenter).id + params['network_id'] = ClcServer._find_network_id(module, datacenter) + params['anti_affinity_policy_id'] = ClcServer._find_aa_policy_id( + clc, + module) + params['alert_policy_id'] = ClcServer._find_alert_policy_id( + clc, + module) + + return params + + @staticmethod + def _find_datacenter(clc, module): + """ + Find the datacenter by calling the CLC API. + :param clc: clc-sdk instance to use + :param module: module to validate + :return: clc-sdk.Datacenter instance + """ + location = module.params.get('location') + try: + datacenter = clc.v2.Datacenter(location) + return datacenter + except CLCException: + module.fail_json( + msg=str( + "Unable to find location: {0}".format(location))) + + @staticmethod + def _find_alias(clc, module): + """ + Find or Validate the Account Alias by calling the CLC API + :param clc: clc-sdk instance to use + :param module: module to validate + :return: clc-sdk.Account instance + """ + alias = module.params.get('alias') + if not alias: + try: + alias = clc.v2.Account.GetAlias() + except CLCException as ex: + module.fail_json(msg='Unable to find account alias. {0}'.format( + ex.message + )) + return alias + + @staticmethod + def _find_cpu(clc, module): + """ + Find or validate the CPU value by calling the CLC API + :param clc: clc-sdk instance to use + :param module: module to validate + :return: Int value for CPU + """ + cpu = module.params.get('cpu') + group_id = module.params.get('group_id') + alias = module.params.get('alias') + state = module.params.get('state') + + if not cpu and state == 'present': + group = clc.v2.Group(id=group_id, + alias=alias) + if group.Defaults("cpu"): + cpu = group.Defaults("cpu") + else: + module.fail_json( + msg=str("Can\'t determine a default cpu value. Please provide a value for cpu.")) + return cpu + + @staticmethod + def _find_memory(clc, module): + """ + Find or validate the Memory value by calling the CLC API + :param clc: clc-sdk instance to use + :param module: module to validate + :return: Int value for Memory + """ + memory = module.params.get('memory') + group_id = module.params.get('group_id') + alias = module.params.get('alias') + state = module.params.get('state') + + if not memory and state == 'present': + group = clc.v2.Group(id=group_id, + alias=alias) + if group.Defaults("memory"): + memory = group.Defaults("memory") + else: + module.fail_json(msg=str( + "Can\'t determine a default memory value. Please provide a value for memory.")) + return memory + + @staticmethod + def _find_description(module): + """ + Set the description module param to name if description is blank + :param module: the module to validate + :return: string description + """ + description = module.params.get('description') + if not description: + description = module.params.get('name') + return description + + @staticmethod + def _validate_types(module): + """ + Validate that type and storage_type are set appropriately, and fail if not + :param module: the module to validate + :return: none + """ + state = module.params.get('state') + server_type = module.params.get( + 'type').lower() if module.params.get('type') else None + storage_type = module.params.get( + 'storage_type').lower() if module.params.get('storage_type') else None + + if state == "present": + if server_type == "standard" and storage_type not in ( + "standard", "premium"): + module.fail_json( + msg=str("Standard VMs must have storage_type = 'standard' or 'premium'")) + + if server_type == "hyperscale" and storage_type != "hyperscale": + module.fail_json( + msg=str("Hyperscale VMs must have storage_type = 'hyperscale'")) + + @staticmethod + def _validate_name(module): + """ + Validate that name is the correct length if provided, fail if it's not + :param module: the module to validate + :return: none + """ + server_name = module.params.get('name') + state = module.params.get('state') + + if state == 'present' and ( + len(server_name) < 1 or len(server_name) > 6): + module.fail_json(msg=str( + "When state = 'present', name must be a string with a minimum length of 1 and a maximum length of 6")) + + @staticmethod + def _find_ttl(clc, module): + """ + Validate that TTL is > 3600 if set, and fail if not + :param clc: clc-sdk instance to use + :param module: module to validate + :return: validated ttl + """ + ttl = module.params.get('ttl') + + if ttl: + if ttl <= 3600: + return module.fail_json(msg=str("Ttl cannot be <= 3600")) + else: + ttl = clc.v2.time_utils.SecondsToZuluTS(int(time.time()) + ttl) + return ttl + + @staticmethod + def _find_template_id(module, datacenter): + """ + Find the template id by calling the CLC API. + :param module: the module to validate + :param datacenter: the datacenter to search for the template + :return: a valid clc template id + """ + lookup_template = module.params.get('template') + state = module.params.get('state') + result = None + + if state == 'present': + try: + result = datacenter.Templates().Search(lookup_template)[0].id + except CLCException: + module.fail_json( + msg=str( + "Unable to find a template: " + + lookup_template + + " in location: " + + datacenter.id)) + return result + + @staticmethod + def _find_network_id(module, datacenter): + """ + Validate the provided network id or return a default. + :param module: the module to validate + :param datacenter: the datacenter to search for a network id + :return: a valid network id + """ + network_id = module.params.get('network_id') + + if not network_id: + try: + network_id = datacenter.Networks().networks[0].id + # -- added for clc-sdk 2.23 compatibility + # datacenter_networks = clc_sdk.v2.Networks( + # networks_lst=datacenter._DeploymentCapabilities()['deployableNetworks']) + # network_id = datacenter_networks.networks[0].id + # -- end + except CLCException: + module.fail_json( + msg=str( + "Unable to find a network in location: " + + datacenter.id)) + + return network_id + + @staticmethod + def _find_aa_policy_id(clc, module): + """ + Validate if the anti affinity policy exist for the given name and throw error if not + :param clc: the clc-sdk instance + :param module: the module to validate + :return: aa_policy_id: the anti affinity policy id of the given name. + """ + aa_policy_id = module.params.get('anti_affinity_policy_id') + aa_policy_name = module.params.get('anti_affinity_policy_name') + if not aa_policy_id and aa_policy_name: + alias = module.params.get('alias') + aa_policy_id = ClcServer._get_anti_affinity_policy_id( + clc, + module, + alias, + aa_policy_name) + if not aa_policy_id: + module.fail_json( + msg='No anti affinity policy was found with policy name : %s' % aa_policy_name) + return aa_policy_id + + @staticmethod + def _find_alert_policy_id(clc, module): + """ + Validate if the alert policy exist for the given name and throw error if not + :param clc: the clc-sdk instance + :param module: the module to validate + :return: alert_policy_id: the alert policy id of the given name. + """ + alert_policy_id = module.params.get('alert_policy_id') + alert_policy_name = module.params.get('alert_policy_name') + if not alert_policy_id and alert_policy_name: + alias = module.params.get('alias') + alert_policy_id = ClcServer._get_alert_policy_id_by_name( + clc=clc, + module=module, + alias=alias, + alert_policy_name=alert_policy_name + ) + if not alert_policy_id: + module.fail_json( + msg='No alert policy exist with name : %s' % alert_policy_name) + return alert_policy_id + + def _create_servers(self, module, clc, override_count=None): + """ + Create New Servers in CLC cloud + :param module: the AnsibleModule object + :param clc: the clc-sdk instance to use + :return: a list of dictionaries with server information about the servers that were created + """ + p = module.params + request_list = [] + servers = [] + server_dict_array = [] + created_server_ids = [] + partial_created_servers_ids = [] + + add_public_ip = p.get('add_public_ip') + public_ip_protocol = p.get('public_ip_protocol') + public_ip_ports = p.get('public_ip_ports') + + params = { + 'name': p.get('name'), + 'template': p.get('template'), + 'group_id': p.get('group'), + 'network_id': p.get('network_id'), + 'cpu': p.get('cpu'), + 'memory': p.get('memory'), + 'alias': p.get('alias'), + 'password': p.get('password'), + 'ip_address': p.get('ip_address'), + 'storage_type': p.get('storage_type'), + 'type': p.get('type'), + 'primary_dns': p.get('primary_dns'), + 'secondary_dns': p.get('secondary_dns'), + 'additional_disks': p.get('additional_disks'), + 'custom_fields': p.get('custom_fields'), + 'ttl': p.get('ttl'), + 'managed_os': p.get('managed_os'), + 'description': p.get('description'), + 'source_server_password': p.get('source_server_password'), + 'cpu_autoscale_policy_id': p.get('cpu_autoscale_policy_id'), + 'anti_affinity_policy_id': p.get('anti_affinity_policy_id'), + 'packages': p.get('packages') + } + + count = override_count if override_count else p.get('count') + + changed = False if count == 0 else True + + if not changed: + return server_dict_array, created_server_ids, partial_created_servers_ids, changed + for i in range(0, count): + if not module.check_mode: + req = self._create_clc_server(clc=clc, + module=module, + server_params=params) + server = req.requests[0].Server() + request_list.append(req) + servers.append(server) + + self._wait_for_requests(module, request_list) + self._refresh_servers(module, servers) + + ip_failed_servers = self._add_public_ip_to_servers( + module=module, + should_add_public_ip=add_public_ip, + servers=servers, + public_ip_protocol=public_ip_protocol, + public_ip_ports=public_ip_ports) + ap_failed_servers = self._add_alert_policy_to_servers(clc=clc, + module=module, + servers=servers) + + for server in servers: + if server in ip_failed_servers or server in ap_failed_servers: + partial_created_servers_ids.append(server.id) + else: + # reload server details + server = clc.v2.Server(server.id) + server.data['ipaddress'] = server.details[ + 'ipAddresses'][0]['internal'] + + if add_public_ip and len(server.PublicIPs().public_ips) > 0: + server.data['publicip'] = str( + server.PublicIPs().public_ips[0]) + created_server_ids.append(server.id) + server_dict_array.append(server.data) + + return server_dict_array, created_server_ids, partial_created_servers_ids, changed + + def _enforce_count(self, module, clc): + """ + Enforce that there is the right number of servers in the provided group. + Starts or stops servers as necessary. + :param module: the AnsibleModule object + :param clc: the clc-sdk instance to use + :return: a list of dictionaries with server information about the servers that were created or deleted + """ + p = module.params + changed = False + count_group = p.get('count_group') + datacenter = ClcServer._find_datacenter(clc, module) + exact_count = p.get('exact_count') + server_dict_array = [] + partial_servers_ids = [] + changed_server_ids = [] + + # fail here if the exact count was specified without filtering + # on a group, as this may lead to a undesired removal of instances + if exact_count and count_group is None: + return module.fail_json( + msg="you must use the 'count_group' option with exact_count") + + servers, running_servers = ClcServer._find_running_servers_by_group( + module, datacenter, count_group) + + if len(running_servers) == exact_count: + changed = False + + elif len(running_servers) < exact_count: + to_create = exact_count - len(running_servers) + server_dict_array, changed_server_ids, partial_servers_ids, changed \ + = self._create_servers(module, clc, override_count=to_create) + + for server in server_dict_array: + running_servers.append(server) + + elif len(running_servers) > exact_count: + to_remove = len(running_servers) - exact_count + all_server_ids = sorted([x.id for x in running_servers]) + remove_ids = all_server_ids[0:to_remove] + + (changed, server_dict_array, changed_server_ids) \ + = ClcServer._delete_servers(module, clc, remove_ids) + + return server_dict_array, changed_server_ids, partial_servers_ids, changed + + @staticmethod + def _wait_for_requests(module, request_list): + """ + Block until server provisioning requests are completed. + :param module: the AnsibleModule object + :param request_list: a list of clc-sdk.Request instances + :return: none + """ + wait = module.params.get('wait') + if wait: + # Requests.WaitUntilComplete() returns the count of failed requests + failed_requests_count = sum( + [request.WaitUntilComplete() for request in request_list]) + + if failed_requests_count > 0: + module.fail_json( + msg='Unable to process server request') + + @staticmethod + def _refresh_servers(module, servers): + """ + Loop through a list of servers and refresh them. + :param module: the AnsibleModule object + :param servers: list of clc-sdk.Server instances to refresh + :return: none + """ + for server in servers: + try: + server.Refresh() + except CLCException as ex: + module.fail_json(msg='Unable to refresh the server {0}. {1}'.format( + server.id, ex.message + )) + + @staticmethod + def _add_public_ip_to_servers( + module, + should_add_public_ip, + servers, + public_ip_protocol, + public_ip_ports): + """ + Create a public IP for servers + :param module: the AnsibleModule object + :param should_add_public_ip: boolean - whether or not to provision a public ip for servers. Skipped if False + :param servers: List of servers to add public ips to + :param public_ip_protocol: a protocol to allow for the public ips + :param public_ip_ports: list of ports to allow for the public ips + :return: none + """ + failed_servers = [] + if not should_add_public_ip: + return failed_servers + + ports_lst = [] + request_list = [] + server = None + + for port in public_ip_ports: + ports_lst.append( + {'protocol': public_ip_protocol, 'port': port}) + try: + if not module.check_mode: + for server in servers: + request = server.PublicIPs().Add(ports_lst) + request_list.append(request) + except APIFailedResponse: + failed_servers.append(server) + ClcServer._wait_for_requests(module, request_list) + return failed_servers + + @staticmethod + def _add_alert_policy_to_servers(clc, module, servers): + """ + Associate the alert policy to servers + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param servers: List of servers to add alert policy to + :return: failed_servers: the list of servers which failed while associating alert policy + """ + failed_servers = [] + p = module.params + alert_policy_id = p.get('alert_policy_id') + alias = p.get('alias') + + if alert_policy_id and not module.check_mode: + for server in servers: + try: + ClcServer._add_alert_policy_to_server( + clc=clc, + alias=alias, + server_id=server.id, + alert_policy_id=alert_policy_id) + except CLCException: + failed_servers.append(server) + return failed_servers + + @staticmethod + def _add_alert_policy_to_server( + clc, alias, server_id, alert_policy_id): + """ + Associate an alert policy to a clc server + :param clc: the clc-sdk instance to use + :param alias: the clc account alias + :param server_id: The clc server id + :param alert_policy_id: the alert policy id to be associated to the server + :return: none + """ + try: + clc.v2.API.Call( + method='POST', + url='servers/%s/%s/alertPolicies' % (alias, server_id), + payload=json.dumps( + { + 'id': alert_policy_id + })) + except APIFailedResponse as e: + raise CLCException( + 'Failed to associate alert policy to the server : {0} with Error {1}'.format( + server_id, str(e.response_text))) + + @staticmethod + def _get_alert_policy_id_by_name(clc, module, alias, alert_policy_name): + """ + Returns the alert policy id for the given alert policy name + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param alias: the clc account alias + :param alert_policy_name: the name of the alert policy + :return: alert_policy_id: the alert policy id + """ + alert_policy_id = None + policies = clc.v2.API.Call('GET', '/v2/alertPolicies/%s' % alias) + if not policies: + return alert_policy_id + for policy in policies.get('items'): + if policy.get('name') == alert_policy_name: + if not alert_policy_id: + alert_policy_id = policy.get('id') + else: + return module.fail_json( + msg='multiple alert policies were found with policy name : %s' % alert_policy_name) + return alert_policy_id + + @staticmethod + def _delete_servers(module, clc, server_ids): + """ + Delete the servers on the provided list + :param module: the AnsibleModule object + :param clc: the clc-sdk instance to use + :param server_ids: list of servers to delete + :return: a list of dictionaries with server information about the servers that were deleted + """ + terminated_server_ids = [] + server_dict_array = [] + request_list = [] + + if not isinstance(server_ids, list) or len(server_ids) < 1: + return module.fail_json( + msg='server_ids should be a list of servers, aborting') + + servers = clc.v2.Servers(server_ids).Servers() + for server in servers: + if not module.check_mode: + request_list.append(server.Delete()) + ClcServer._wait_for_requests(module, request_list) + + for server in servers: + terminated_server_ids.append(server.id) + + return True, server_dict_array, terminated_server_ids + + @staticmethod + def _start_stop_servers(module, clc, server_ids): + """ + Start or Stop the servers on the provided list + :param module: the AnsibleModule object + :param clc: the clc-sdk instance to use + :param server_ids: list of servers to start or stop + :return: a list of dictionaries with server information about the servers that were started or stopped + """ + p = module.params + state = p.get('state') + changed = False + changed_servers = [] + server_dict_array = [] + result_server_ids = [] + request_list = [] + + if not isinstance(server_ids, list) or len(server_ids) < 1: + return module.fail_json( + msg='server_ids should be a list of servers, aborting') + + servers = clc.v2.Servers(server_ids).Servers() + for server in servers: + if server.powerState != state: + changed_servers.append(server) + if not module.check_mode: + request_list.append( + ClcServer._change_server_power_state( + module, + server, + state)) + changed = True + + ClcServer._wait_for_requests(module, request_list) + ClcServer._refresh_servers(module, changed_servers) + + for server in set(changed_servers + servers): + try: + server.data['ipaddress'] = server.details[ + 'ipAddresses'][0]['internal'] + server.data['publicip'] = str( + server.PublicIPs().public_ips[0]) + except (KeyError, IndexError): + pass + + server_dict_array.append(server.data) + result_server_ids.append(server.id) + + return changed, server_dict_array, result_server_ids + + @staticmethod + def _change_server_power_state(module, server, state): + """ + Change the server powerState + :param module: the module to check for intended state + :param server: the server to start or stop + :param state: the intended powerState for the server + :return: the request object from clc-sdk call + """ + result = None + try: + if state == 'started': + result = server.PowerOn() + else: + result = server.PowerOff() + except CLCException: + module.fail_json( + msg='Unable to change power state for server {0}'.format( + server.id)) + return result + + @staticmethod + def _find_running_servers_by_group(module, datacenter, count_group): + """ + Find a list of running servers in the provided group + :param module: the AnsibleModule object + :param datacenter: the clc-sdk.Datacenter instance to use to lookup the group + :param count_group: the group to count the servers + :return: list of servers, and list of running servers + """ + group = ClcServer._find_group( + module=module, + datacenter=datacenter, + lookup_group=count_group) + + servers = group.Servers().Servers() + running_servers = [] + + for server in servers: + if server.status == 'active' and server.powerState == 'started': + running_servers.append(server) + + return servers, running_servers + + @staticmethod + def _find_group(module, datacenter, lookup_group=None): + """ + Find a server group in a datacenter by calling the CLC API + :param module: the AnsibleModule instance + :param datacenter: clc-sdk.Datacenter instance to search for the group + :param lookup_group: string name of the group to search for + :return: clc-sdk.Group instance + """ + if not lookup_group: + lookup_group = module.params.get('group') + try: + return datacenter.Groups().Get(lookup_group) + except CLCException: + pass + + # The search above only acts on the main + result = ClcServer._find_group_recursive( + module, + datacenter.Groups(), + lookup_group) + + if result is None: + module.fail_json( + msg=str( + "Unable to find group: " + + lookup_group + + " in location: " + + datacenter.id)) + + return result + + @staticmethod + def _find_group_recursive(module, group_list, lookup_group): + """ + Find a server group by recursively walking the tree + :param module: the AnsibleModule instance to use + :param group_list: a list of groups to search + :param lookup_group: the group to look for + :return: list of groups + """ + result = None + for group in group_list.groups: + subgroups = group.Subgroups() + try: + return subgroups.Get(lookup_group) + except CLCException: + result = ClcServer._find_group_recursive( + module, + subgroups, + lookup_group) + + if result is not None: + break + + return result + + @staticmethod + def _create_clc_server( + clc, + module, + server_params): + """ + Call the CLC Rest API to Create a Server + :param clc: the clc-python-sdk instance to use + :param module: the AnsibleModule instance to use + :param server_params: a dictionary of params to use to create the servers + :return: clc-sdk.Request object linked to the queued server request + """ + + try: + res = clc.v2.API.Call( + method='POST', + url='servers/%s' % + (server_params.get('alias')), + payload=json.dumps( + { + 'name': server_params.get('name'), + 'description': server_params.get('description'), + 'groupId': server_params.get('group_id'), + 'sourceServerId': server_params.get('template'), + 'isManagedOS': server_params.get('managed_os'), + 'primaryDNS': server_params.get('primary_dns'), + 'secondaryDNS': server_params.get('secondary_dns'), + 'networkId': server_params.get('network_id'), + 'ipAddress': server_params.get('ip_address'), + 'password': server_params.get('password'), + 'sourceServerPassword': server_params.get('source_server_password'), + 'cpu': server_params.get('cpu'), + 'cpuAutoscalePolicyId': server_params.get('cpu_autoscale_policy_id'), + 'memoryGB': server_params.get('memory'), + 'type': server_params.get('type'), + 'storageType': server_params.get('storage_type'), + 'antiAffinityPolicyId': server_params.get('anti_affinity_policy_id'), + 'customFields': server_params.get('custom_fields'), + 'additionalDisks': server_params.get('additional_disks'), + 'ttl': server_params.get('ttl'), + 'packages': server_params.get('packages')})) + + result = clc.v2.Requests(res) + except APIFailedResponse as ex: + return module.fail_json(msg='Unable to create the server: {0}. {1}'.format( + server_params.get('name'), + ex.response_text + )) + + # + # Patch the Request object so that it returns a valid server + + # Find the server's UUID from the API response + server_uuid = [obj['id'] + for obj in res['links'] if obj['rel'] == 'self'][0] + + # Change the request server method to a _find_server_by_uuid closure so + # that it will work + result.requests[0].Server = lambda: ClcServer._find_server_by_uuid_w_retry( + clc, + module, + server_uuid, + server_params.get('alias')) + + return result + + @staticmethod + def _get_anti_affinity_policy_id(clc, module, alias, aa_policy_name): + """ + retrieves the anti affinity policy id of the server based on the name of the policy + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param alias: the CLC account alias + :param aa_policy_name: the anti affinity policy name + :return: aa_policy_id: The anti affinity policy id + """ + aa_policy_id = None + try: + aa_policies = clc.v2.API.Call(method='GET', + url='antiAffinityPolicies/%s' % alias) + except APIFailedResponse as ex: + return module.fail_json(msg='Unable to fetch anti affinity policies for account: {0}. {1}'.format( + alias, ex.response_text)) + for aa_policy in aa_policies.get('items'): + if aa_policy.get('name') == aa_policy_name: + if not aa_policy_id: + aa_policy_id = aa_policy.get('id') + else: + return module.fail_json( + msg='multiple anti affinity policies were found with policy name : %s' % aa_policy_name) + return aa_policy_id + + # + # This is the function that gets patched to the Request.server object using a lamda closure + # + + @staticmethod + def _find_server_by_uuid_w_retry( + clc, module, svr_uuid, alias=None, retries=5, back_out=2): + """ + Find the clc server by the UUID returned from the provisioning request. Retry the request if a 404 is returned. + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param svr_uuid: UUID of the server + :param retries: the number of retry attempts to make prior to fail. default is 5 + :param alias: the Account Alias to search + :return: a clc-sdk.Server instance + """ + if not alias: + alias = clc.v2.Account.GetAlias() + + # Wait and retry if the api returns a 404 + while True: + retries -= 1 + try: + server_obj = clc.v2.API.Call( + method='GET', url='servers/%s/%s?uuid=true' % + (alias, svr_uuid)) + server_id = server_obj['id'] + server = clc.v2.Server( + id=server_id, + alias=alias, + server_obj=server_obj) + return server + + except APIFailedResponse as e: + if e.response_status_code != 404: + return module.fail_json( + msg='A failure response was received from CLC API when ' + 'attempting to get details for a server: UUID=%s, Code=%i, Message=%s' % + (svr_uuid, e.response_status_code, e.message)) + if retries == 0: + return module.fail_json( + msg='Unable to reach the CLC API after 5 attempts') + sleep(back_out) + back_out *= 2 + + @staticmethod + def _set_user_agent(clc): + if hasattr(clc, 'SetRequestsSession'): + agent_string = "ClcAnsibleModule/" + __version__ + ses = requests.Session() + ses.headers.update({"Api-Client": agent_string}) + ses.headers['User-Agent'] += " " + agent_string + clc.SetRequestsSession(ses) + + +def main(): + """ + The main function. Instantiates the module and calls process_request. + :return: none + """ + argument_dict = ClcServer._define_module_argument_spec() + module = AnsibleModule(supports_check_mode=True, **argument_dict) + clc_server = ClcServer(module) + clc_server.process_request() + +from ansible.module_utils.basic import * # pylint: disable=W0614 +if __name__ == '__main__': + main() From 5abaab68a46fb845946f8575ff29a03d2574d0e3 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Tue, 28 Jul 2015 00:05:37 +0200 Subject: [PATCH 0523/2522] cloudstack: fix typos in doc --- cloud/cloudstack/cs_account.py | 2 +- cloud/cloudstack/cs_iso.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/cloudstack/cs_account.py b/cloud/cloudstack/cs_account.py index d1302854454..8196196a1a9 100644 --- a/cloud/cloudstack/cs_account.py +++ b/cloud/cloudstack/cs_account.py @@ -21,7 +21,7 @@ DOCUMENTATION = ''' --- module: cs_account -short_description: Manages account on Apache CloudStack based clouds. +short_description: Manages accounts on Apache CloudStack based clouds. description: - Create, disable, lock, enable and remove accounts. version_added: '2.0' diff --git a/cloud/cloudstack/cs_iso.py b/cloud/cloudstack/cs_iso.py index 67e4b283155..7030e4be607 100644 --- a/cloud/cloudstack/cs_iso.py +++ b/cloud/cloudstack/cs_iso.py @@ -21,7 +21,7 @@ DOCUMENTATION = ''' --- module: cs_iso -short_description: Manages ISOs images on Apache CloudStack based clouds. +short_description: Manages ISO images on Apache CloudStack based clouds. description: - Register and remove ISO images. version_added: '2.0' From 2af729944af658d24168be6486d56235f3122030 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Tue, 28 Jul 2015 00:21:27 +0200 Subject: [PATCH 0524/2522] Update author information in virt_net docs --- cloud/misc/virt_net.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 cloud/misc/virt_net.py diff --git a/cloud/misc/virt_net.py b/cloud/misc/virt_net.py old mode 100644 new mode 100755 index 078275cd84a..21cdca5fbd7 --- a/cloud/misc/virt_net.py +++ b/cloud/misc/virt_net.py @@ -21,7 +21,7 @@ DOCUMENTATION = ''' --- module: virt_net -author: "Maciej Delmanowski" +author: "Maciej Delmanowski (@drybjed)" version_added: "2.0" short_description: Manage libvirt network configuration description: From 375b2234b127fdceec6b37babe5e96f0bfe5907e Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Tue, 28 Jul 2015 00:22:51 +0200 Subject: [PATCH 0525/2522] Update author information in virt_pool docs --- cloud/misc/virt_pool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 cloud/misc/virt_pool.py diff --git a/cloud/misc/virt_pool.py b/cloud/misc/virt_pool.py old mode 100644 new mode 100755 index 54489495353..1089269fc84 --- a/cloud/misc/virt_pool.py +++ b/cloud/misc/virt_pool.py @@ -21,7 +21,7 @@ DOCUMENTATION = ''' --- module: virt_pool -author: "Maciej Delmanowski" +author: "Maciej Delmanowski (@drybjed)" version_added: "2.0" short_description: Manage libvirt storage pools description: From 047e37a28c7f1be712331fbc1ac6cf158568be2b Mon Sep 17 00:00:00 2001 From: Benno Joy Date: Tue, 28 Jul 2015 15:49:44 +0530 Subject: [PATCH 0526/2522] update license --- cloud/vmware/vca_fw.py | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/cloud/vmware/vca_fw.py b/cloud/vmware/vca_fw.py index d2ce398cfa8..45ed78ef608 100644 --- a/cloud/vmware/vca_fw.py +++ b/cloud/vmware/vca_fw.py @@ -2,17 +2,21 @@ # Copyright (c) 2015 VMware, Inc. All Rights Reserved. # -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at +# This file is part of Ansible # -# http://www.apache.org/licenses/LICENSE-2.0 +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + DOCUMENTATION = ''' --- @@ -23,75 +27,63 @@ version_added: "2.0" options: username: - version_added: "2.0" description: - The vca username or email address, if not set the environment variable VCA_USER is checked for the username. required: false default: None password: - version_added: "2.0" description: - The vca password, if not set the environment variable VCA_PASS is checked for the password required: false default: None org: - version_added: "2.0" description: - The org to login to for creating vapp, mostly set when the service_type is vdc. required: false default: None service_id: - version_added: "2.0" description: - The service id in a vchs environment to be used for creating the vapp required: false default: None host: - version_added: "2.0" description: - The authentication host to be used when service type is vcd. required: false default: None api_version: - version_added: "2.0" description: - The api version to be used with the vca required: false default: "5.7" service_type: - version_added: "2.0" description: - The type of service we are authenticating against required: false default: vca choices: [ "vca", "vchs", "vcd" ] state: - version_added: "2.0" description: - if the object should be added or removed required: false default: present choices: [ "present", "absent" ] verify_certs: - version_added: "2.0" description: - If the certificates of the authentication is to be verified required: false default: True vdc_name: - version_added: "2.0" description: - The name of the vdc where the gateway is located. required: false default: None gateway_name: - version_added: "2.0" description: - The name of the gateway of the vdc where the rule should be added required: false default: gateway fw_rules: - version_added: "2.0" description: - A list of firewall rules to be added to the gateway, Please see examples on valid entries required: True From abaf4ce59dad68a6981973c7959d0515a56f03c9 Mon Sep 17 00:00:00 2001 From: Benno Joy Date: Tue, 28 Jul 2015 15:51:57 +0530 Subject: [PATCH 0527/2522] update license --- cloud/vmware/vca_nat.py | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/cloud/vmware/vca_nat.py b/cloud/vmware/vca_nat.py index bde72dc07ac..c948605ce48 100644 --- a/cloud/vmware/vca_nat.py +++ b/cloud/vmware/vca_nat.py @@ -2,17 +2,21 @@ # Copyright (c) 2015 VMware, Inc. All Rights Reserved. # -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at +# This file is part of Ansible # -# http://www.apache.org/licenses/LICENSE-2.0 +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. # -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + DOCUMENTATION = ''' --- @@ -23,81 +27,68 @@ version_added: "2.0" options: username: - version_added: "2.0" description: - The vca username or email address, if not set the environment variable VCA_USER is checked for the username. required: false default: None password: - version_added: "2.0" description: - The vca password, if not set the environment variable VCA_PASS is checked for the password required: false default: None org: - version_added: "2.0" description: - The org to login to for creating vapp, mostly set when the service_type is vdc. required: false default: None service_id: - version_added: "2.0" description: - The service id in a vchs environment to be used for creating the vapp required: false default: None host: - version_added: "2.0" description: - The authentication host to be used when service type is vcd. required: false default: None api_version: - version_added: "2.0" description: - The api version to be used with the vca required: false default: "5.7" service_type: - version_added: "2.0" description: - The type of service we are authenticating against required: false default: vca choices: [ "vca", "vchs", "vcd" ] state: - version_added: "2.0" description: - if the object should be added or removed required: false default: present choices: [ "present", "absent" ] verify_certs: - version_added: "2.0" description: - If the certificates of the authentication is to be verified required: false default: True vdc_name: - version_added: "2.0" description: - The name of the vdc where the gateway is located. required: false default: None gateway_name: - version_added: "2.0" description: - The name of the gateway of the vdc where the rule should be added required: false default: gateway purge_rules: - version_added: "2.0" description: - If set to true, it will delete all rules in the gateway that are not given as paramter to this module. required: false default: false nat_rules: - version_added: "2.0" description: - A list of rules to be added to the gateway, Please see examples on valid entries required: True From b37777ed21313d81379d1a592d569ad3d9e800c6 Mon Sep 17 00:00:00 2001 From: Andy Hill Date: Wed, 1 Oct 2014 19:56:08 -0400 Subject: [PATCH 0528/2522] Add xenserver_facts module This module gathers facts about a XenServer host, gathering them in a single connection instead of multiple xe commands. --- cloud/xenserver_facts.py | 196 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 cloud/xenserver_facts.py diff --git a/cloud/xenserver_facts.py b/cloud/xenserver_facts.py new file mode 100644 index 00000000000..1977f9fe2b8 --- /dev/null +++ b/cloud/xenserver_facts.py @@ -0,0 +1,196 @@ +#!/usr/bin/python -tt +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + + +DOCUMENTATION = ''' +--- +module: xenserver_facts +version_added: 1.7 +short_description: get facts reported on xenserver +description: + - Reads data out of XenAPI, can be used instead of multiple xe commands. +author: Andy Hill, Tim Rupp +''' + +import platform +import sys +import XenAPI + +EXAMPLES = ''' +- name: Gather facts from xenserver + xenserver: + +- name: Print running VMs + debug: msg="{{ item }}" + with_items: xs_vms.keys() + when: xs_vms[item]['power_state'] == "Running" + +TASK: [Print running VMs] *********************************************************** +skipping: [10.13.0.22] => (item=CentOS 4.7 (32-bit)) +ok: [10.13.0.22] => (item=Control domain on host: 10.0.13.22) => { + "item": "Control domain on host: 10.0.13.22", + "msg": "Control domain on host: 10.0.13.22" +} +''' + +class XenServerFacts: + def __init__(self): + self.codes = { + '5.5.0': 'george', + '5.6.100': 'oxford', + '6.0.0': 'boston', + '6.1.0': 'tampa', + '6.2.0': 'clearwater' + } + + @property + def version(self): + # Be aware! Deprecated in Python 2.6! + result = platform.dist()[1] + return result + + @property + def codename(self): + if self.version in self.codes: + result = self.codes[self.version] + else: + result = None + + return result + + +def get_xenapi_session(): + try: + session = XenAPI.xapi_local() + session.xenapi.login_with_password('', '') + return session + except XenAPI.Failure: + sys.exit(1) + + +def get_networks(session): + recs = session.xenapi.network.get_all_records() + xs_networks = {} + networks = change_keys(recs, key='uuid') + for network in networks.itervalues(): + xs_networks[network['name_label']] = network + return xs_networks + + +def get_pifs(session): + recs = session.xenapi.PIF.get_all_records() + pifs = change_keys(recs, key='uuid') + xs_pifs = {} + devicenums = range(0, 7) + for pif in pifs.itervalues(): + for eth in devicenums: + interface_name = "eth%s" % (eth) + bond_name = interface_name.replace('eth', 'bond') + if pif['device'] == interface_name: + xs_pifs[interface_name] = pif + elif pif['device'] == bond_name: + xs_pifs[bond_name] = pif + return xs_pifs + + +def get_vlans(session): + recs = session.xenapi.VLAN.get_all_records() + return change_keys(recs, key='tag') + + +def change_keys(recs, key='uuid', filter_func=None): + """ + Take a xapi dict, and make the keys the value of recs[ref][key]. + + Preserves the ref in rec['ref'] + + """ + new_recs = {} + + for ref, rec in recs.iteritems(): + if filter_func is not None and not filter_func(rec): + continue + + new_recs[rec[key]] = rec + new_recs[rec[key]]['ref'] = ref + + return new_recs + +def get_host(session): + """Get the host""" + host_recs = session.xenapi.host.get_all() + # We only have one host, so just return its entry + return session.xenapi.host.get_record(host_recs[0]) + +def get_vms(session): + xs_vms = {} + recs = session.xenapi.VM.get_all() + if not recs: + return None + + vms = change_keys(recs, key='uuid') + for vm in vms.itervalues(): + xs_vms[vm['name_label']] = vm + return xs_vms + + +def get_srs(session): + xs_srs = {} + recs = session.xenapi.SR.get_all() + if not recs: + return None + srs = change_keys(recs, key='uuid') + for sr in srs.itervalues(): + xs_srs[sr['name_label']] = sr + return xs_srs + +def main(): + module = AnsibleModule({}) + + obj = XenServerFacts() + session = get_xenapi_session() + + + data = { + 'xenserver_version': obj.version, + 'xenserver_codename': obj.codename + } + + xs_networks = get_networks(session) + xs_pifs = get_pifs(session) + xs_vlans = get_vlans(session) + xs_vms = get_vms(session) + xs_srs = get_srs(session) + + if xs_vlans: + data['xs_vlans'] = xs_vlans + if xs_pifs: + data['xs_pifs'] = xs_pifs + if xs_networks: + data['xs_networks'] = xs_networks + + if xs_vms: + data['xs_vms'] = xs_vms + + if xs_srs: + data['xs_srs'] = xs_srs + + module.exit_json(ansible=data) + +# this is magic, see lib/ansible/module_common.py +#<> + +main() From 5f5ae26cc1d95ddb10158834cfc44d580d52f878 Mon Sep 17 00:00:00 2001 From: Rob White Date: Tue, 28 Jul 2015 21:40:55 +1000 Subject: [PATCH 0529/2522] New module - ec2_vpc_route_table_facts --- cloud/amazon/ec2_vpc_route_table_facts.py | 130 ++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 cloud/amazon/ec2_vpc_route_table_facts.py diff --git a/cloud/amazon/ec2_vpc_route_table_facts.py b/cloud/amazon/ec2_vpc_route_table_facts.py new file mode 100644 index 00000000000..78ef1be3509 --- /dev/null +++ b/cloud/amazon/ec2_vpc_route_table_facts.py @@ -0,0 +1,130 @@ +#!/usr/bin/python +# +# This is a free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This Ansible library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this library. If not, see . + +DOCUMENTATION = ''' +--- +module: ec2_vpc_route_table_facts +short_description: Gather facts about ec2 VPC route tables in AWS +description: + - Gather facts about ec2 VPC route tables in AWS +version_added: "2.0" +author: "Rob White (@wimnat)" +options: + filters: + description: + - A dict of filters to apply. Each dict item consists of a filter key and a filter value. See U(http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeRouteTables.html) for possible filters. + required: false + default: null + region: + description: + - The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used. See U(http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region) + required: false + default: null + aliases: [ 'aws_region', 'ec2_region' ] + +extends_documentation_fragment: aws +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Gather facts about all VPC route tables +- ec2_vpc_route_table_facts: + +# Gather facts about a particular VPC route table using route table ID +- ec2_vpc_route_table_facts: + filters: + - route-table-id: rtb-00112233 + +# Gather facts about any VPC route table with a tag key Name and value Example +- ec2_vpc_route_table_facts: + filters: + - "tag:Name": Example + +# Gather facts about any VPC route table within VPC with ID vpc-abcdef00 +- ec2_vpc_route_table_facts: + filters: + - vpc-id: vpc-abcdef00 + +''' + +try: + import boto.vpc + from boto.exception import BotoServerError + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +def get_route_table_info(route_table): + + # Add any routes to array + routes = [] + for route in route_table.routes: + routes.append(route.__dict__) + + route_table_info = { 'id': route_table.id, + 'routes': routes, + 'tags': route_table.tags, + 'vpc_id': route_table.vpc_id + } + + return route_table_info + +def list_ec2_vpc_route_tables(connection, module): + + filters = module.params.get("filters") + route_table_dict_array = [] + + try: + all_route_tables = connection.get_all_route_tables(filters=filters) + except BotoServerError as e: + module.fail_json(msg=e.message) + + for route_table in all_route_tables: + route_table_dict_array.append(get_route_table_info(route_table)) + + module.exit_json(route_tables=route_table_dict_array) + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + filters = dict(default=None, type='dict') + ) + ) + + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO: + module.fail_json(msg='boto required for this module') + + region, ec2_url, aws_connect_params = get_aws_connection_info(module) + + if region: + try: + connection = connect_to_aws(boto.vpc, region, **aws_connect_params) + except (boto.exception.NoAuthHandlerFound, StandardError), e: + module.fail_json(msg=str(e)) + else: + module.fail_json(msg="region must be specified") + + list_ec2_vpc_route_tables(connection, module) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From 1d06ee6e2d6da6de158c538cf451aa075384c450 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 28 Jul 2015 08:50:23 -0400 Subject: [PATCH 0530/2522] minor doc fixes to pam_limits --- system/pam_limits.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/pam_limits.py b/system/pam_limits.py index ab429bb8808..080b938dd01 100644 --- a/system/pam_limits.py +++ b/system/pam_limits.py @@ -38,12 +38,12 @@ required: true limit_type: description: - - Limit type : hard or soft. + - Limit type, see C(man limits) for an explanation required: true choices: [ "hard", "soft" ] limit_item: description: - - The limit to be set : core, data, nofile, cpu, etc. + - The limit to be set required: true choices: [ "core", "data", "fsize", "memlock", "nofile", "rss", "stack", "cpu", "nproc", "as", "maxlogins", "maxsyslogins", "priority", "locks", "sigpending", "msgqueue", "nice", "rtprio", "chroot" ] value: From f6e7f33d5e5946edcbf2e826262c5dcb0f2ec7d6 Mon Sep 17 00:00:00 2001 From: Siva Popuri Date: Tue, 28 Jul 2015 08:57:57 -0500 Subject: [PATCH 0531/2522] added no_log attribute to password argument. --- cloud/centurylink/clc_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/centurylink/clc_server.py b/cloud/centurylink/clc_server.py index a53b0ba1e10..d2329465f4a 100644 --- a/cloud/centurylink/clc_server.py +++ b/cloud/centurylink/clc_server.py @@ -399,7 +399,7 @@ def _define_module_argument_spec(): cpu=dict(default=1), memory=dict(default=1), alias=dict(default=None), - password=dict(default=None), + password=dict(default=None, no_log=True), ip_address=dict(default=None), storage_type=dict( default='standard', From 727d2b46fd0c3a5dea0c4d8541c9ca6a6d295e3b Mon Sep 17 00:00:00 2001 From: Siva Popuri Date: Tue, 28 Jul 2015 11:14:11 -0500 Subject: [PATCH 0532/2522] Module for managing firewall policies in centurylink cloud --- cloud/centurylink/clc_firewall_policy.py | 555 +++++++++++++++++++++++ 1 file changed, 555 insertions(+) create mode 100644 cloud/centurylink/clc_firewall_policy.py diff --git a/cloud/centurylink/clc_firewall_policy.py b/cloud/centurylink/clc_firewall_policy.py new file mode 100644 index 00000000000..b851ea48a44 --- /dev/null +++ b/cloud/centurylink/clc_firewall_policy.py @@ -0,0 +1,555 @@ +#!/usr/bin/python + +# +# Copyright (c) 2015 CenturyLink +# +# This file is part of Ansible. +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see +# + +DOCUMENTATION = ''' +module: clc_firewall_policy +short_description: Create/delete/update firewall policies +description: + - Create or delete or update firewall polices on Centurylink Cloud +version_added: "2.0" +options: + location: + description: + - Target datacenter for the firewall policy + required: True + state: + description: + - Whether to create or delete the firewall policy + default: present + required: False + choices: ['present', 'absent'] + source: + description: + - The list of source addresses for traffic on the originating firewall. + This is required when state is 'present" + default: None + required: False + destination: + description: + - The list of destination addresses for traffic on the terminating firewall. + This is required when state is 'present' + default: None + required: False + ports: + description: + - The list of ports associated with the policy. + TCP and UDP can take in single ports or port ranges. + default: None + required: False + choices: ['any', 'icmp', 'TCP/123', 'UDP/123', 'TCP/123-456', 'UDP/123-456'] + firewall_policy_id: + description: + - Id of the firewall policy. This is required to update or delete an existing firewall policy + default: None + required: False + source_account_alias: + description: + - CLC alias for the source account + required: True + destination_account_alias: + description: + - CLC alias for the destination account + default: None + required: False + wait: + description: + - Whether to wait for the provisioning tasks to finish before returning. + default: True + required: False + choices: [True, False] + enabled: + description: + - Whether the firewall policy is enabled or disabled + default: True + required: False + choices: [True, False] +requirements: + - python = 2.7 + - requests >= 2.5.0 + - clc-sdk +notes: + - To use this module, it is required to set the below environment variables which enables access to the + Centurylink Cloud + - CLC_V2_API_USERNAME, the account login id for the centurylink cloud + - CLC_V2_API_PASSWORD, the account password for the centurylink cloud + - Alternatively, the module accepts the API token and account alias. The API token can be generated using the + CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login + - CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login + - CLC_ACCT_ALIAS, the account alias associated with the centurylink cloud + - Users can set CLC_V2_API_URL to specify an endpoint for pointing to a different CLC environment. +''' + +EXAMPLES = ''' +--- +- name: Create Firewall Policy + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Create / Verify an Firewall Policy at CenturyLink Cloud + clc_firewall: + source_account_alias: WFAD + location: VA1 + state: present + source: 10.128.216.0/24 + destination: 10.128.216.0/24 + ports: Any + destination_account_alias: WFAD + +--- +- name: Delete Firewall Policy + hosts: localhost + gather_facts: False + connection: local + tasks: + - name: Delete an Firewall Policy at CenturyLink Cloud + clc_firewall: + source_account_alias: WFAD + location: VA1 + state: absent + firewall_policy_id: 'c62105233d7a4231bd2e91b9c791e43e1' +''' + +__version__ = '${version}' + +import urlparse +from time import sleep +from distutils.version import LooseVersion + +try: + import requests +except ImportError: + REQUESTS_FOUND = False +else: + REQUESTS_FOUND = True + +try: + import clc as clc_sdk + from clc import CLCException + from clc import APIFailedResponse +except ImportError: + CLC_FOUND = False + clc_sdk = None +else: + CLC_FOUND = True + + +class ClcFirewallPolicy: + + clc = None + + def __init__(self, module): + """ + Construct module + """ + self.clc = clc_sdk + self.module = module + self.firewall_dict = {} + + if not CLC_FOUND: + self.module.fail_json( + msg='clc-python-sdk required for this module') + if not REQUESTS_FOUND: + self.module.fail_json( + msg='requests library is required for this module') + if requests.__version__ and LooseVersion( + requests.__version__) < LooseVersion('2.5.0'): + self.module.fail_json( + msg='requests library version should be >= 2.5.0') + + self._set_user_agent(self.clc) + + @staticmethod + def _define_module_argument_spec(): + """ + Define the argument spec for the ansible module + :return: argument spec dictionary + """ + argument_spec = dict( + location=dict(required=True), + source_account_alias=dict(required=True, default=None), + destination_account_alias=dict(default=None), + firewall_policy_id=dict(default=None), + ports=dict(default=None, type='list'), + source=dict(defualt=None, type='list'), + destination=dict(defualt=None, type='list'), + wait=dict(default=True), + state=dict(default='present', choices=['present', 'absent']), + enabled=dict(defualt=True, choices=[True, False]) + ) + return argument_spec + + def process_request(self): + """ + Execute the main code path, and handle the request + :return: none + """ + changed = False + firewall_policy = None + location = self.module.params.get('location') + source_account_alias = self.module.params.get('source_account_alias') + destination_account_alias = self.module.params.get( + 'destination_account_alias') + firewall_policy_id = self.module.params.get('firewall_policy_id') + ports = self.module.params.get('ports') + source = self.module.params.get('source') + destination = self.module.params.get('destination') + wait = self.module.params.get('wait') + state = self.module.params.get('state') + enabled = self.module.params.get('enabled') + + self.firewall_dict = { + 'location': location, + 'source_account_alias': source_account_alias, + 'destination_account_alias': destination_account_alias, + 'firewall_policy_id': firewall_policy_id, + 'ports': ports, + 'source': source, + 'destination': destination, + 'wait': wait, + 'state': state, + 'enabled': enabled} + + self._set_clc_credentials_from_env() + + if state == 'absent': + changed, firewall_policy_id, firewall_policy = self._ensure_firewall_policy_is_absent( + source_account_alias, location, self.firewall_dict) + + elif state == 'present': + changed, firewall_policy_id, firewall_policy = self._ensure_firewall_policy_is_present( + source_account_alias, location, self.firewall_dict) + + return self.module.exit_json( + changed=changed, + firewall_policy_id=firewall_policy_id, + firewall_policy=firewall_policy) + + @staticmethod + def _get_policy_id_from_response(response): + """ + Method to parse out the policy id from creation response + :param response: response from firewall creation API call + :return: policy_id: firewall policy id from creation call + """ + url = response.get('links')[0]['href'] + path = urlparse.urlparse(url).path + path_list = os.path.split(path) + policy_id = path_list[-1] + return policy_id + + def _set_clc_credentials_from_env(self): + """ + Set the CLC Credentials on the sdk by reading environment variables + :return: none + """ + env = os.environ + v2_api_token = env.get('CLC_V2_API_TOKEN', False) + v2_api_username = env.get('CLC_V2_API_USERNAME', False) + v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) + clc_alias = env.get('CLC_ACCT_ALIAS', False) + api_url = env.get('CLC_V2_API_URL', False) + + if api_url: + self.clc.defaults.ENDPOINT_URL_V2 = api_url + + if v2_api_token and clc_alias: + self.clc._LOGIN_TOKEN_V2 = v2_api_token + self.clc._V2_ENABLED = True + self.clc.ALIAS = clc_alias + elif v2_api_username and v2_api_passwd: + self.clc.v2.SetCredentials( + api_username=v2_api_username, + api_passwd=v2_api_passwd) + else: + return self.module.fail_json( + msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " + "environment variables") + + def _ensure_firewall_policy_is_present( + self, + source_account_alias, + location, + firewall_dict): + """ + Ensures that a given firewall policy is present + :param source_account_alias: the source account alias for the firewall policy + :param location: datacenter of the firewall policy + :param firewall_dict: dictionary of request parameters for firewall policy + :return: (changed, firewall_policy_id, firewall_policy) + changed: flag for if a change occurred + firewall_policy_id: the firewall policy id that was created/updated + firewall_policy: The firewall_policy object + """ + firewall_policy = None + firewall_policy_id = firewall_dict.get('firewall_policy_id') + + if firewall_policy_id is None: + if not self.module.check_mode: + response = self._create_firewall_policy( + source_account_alias, + location, + firewall_dict) + firewall_policy_id = self._get_policy_id_from_response( + response) + changed = True + else: + firewall_policy = self._get_firewall_policy( + source_account_alias, location, firewall_policy_id) + if not firewall_policy: + return self.module.fail_json( + msg='Unable to find the firewall policy id : {0}'.format( + firewall_policy_id)) + changed = self._compare_get_request_with_dict( + firewall_policy, + firewall_dict) + if not self.module.check_mode and changed: + self._update_firewall_policy( + source_account_alias, + location, + firewall_policy_id, + firewall_dict) + if changed and firewall_policy_id: + firewall_policy = self._wait_for_requests_to_complete( + source_account_alias, + location, + firewall_policy_id) + return changed, firewall_policy_id, firewall_policy + + def _ensure_firewall_policy_is_absent( + self, + source_account_alias, + location, + firewall_dict): + """ + Ensures that a given firewall policy is removed if present + :param source_account_alias: the source account alias for the firewall policy + :param location: datacenter of the firewall policy + :param firewall_dict: firewall policy to delete + :return: (changed, firewall_policy_id, response) + changed: flag for if a change occurred + firewall_policy_id: the firewall policy id that was deleted + response: response from CLC API call + """ + changed = False + response = [] + firewall_policy_id = firewall_dict.get('firewall_policy_id') + result = self._get_firewall_policy( + source_account_alias, location, firewall_policy_id) + if result: + if not self.module.check_mode: + response = self._delete_firewall_policy( + source_account_alias, + location, + firewall_policy_id) + changed = True + return changed, firewall_policy_id, response + + def _create_firewall_policy( + self, + source_account_alias, + location, + firewall_dict): + """ + Creates the firewall policy for the given account alias + :param source_account_alias: the source account alias for the firewall policy + :param location: datacenter of the firewall policy + :param firewall_dict: dictionary of request parameters for firewall policy + :return: response from CLC API call + """ + payload = { + 'destinationAccount': firewall_dict.get('destination_account_alias'), + 'source': firewall_dict.get('source'), + 'destination': firewall_dict.get('destination'), + 'ports': firewall_dict.get('ports')} + try: + response = self.clc.v2.API.Call( + 'POST', '/v2-experimental/firewallPolicies/%s/%s' % + (source_account_alias, location), payload) + except APIFailedResponse as e: + return self.module.fail_json( + msg="Unable to create firewall policy. %s" % + str(e.response_text)) + return response + + def _delete_firewall_policy( + self, + source_account_alias, + location, + firewall_policy_id): + """ + Deletes a given firewall policy for an account alias in a datacenter + :param source_account_alias: the source account alias for the firewall policy + :param location: datacenter of the firewall policy + :param firewall_policy_id: firewall policy id to delete + :return: response: response from CLC API call + """ + try: + response = self.clc.v2.API.Call( + 'DELETE', '/v2-experimental/firewallPolicies/%s/%s/%s' % + (source_account_alias, location, firewall_policy_id)) + except APIFailedResponse as e: + return self.module.fail_json( + msg="Unable to delete the firewall policy id : {0}. {1}".format( + firewall_policy_id, str(e.response_text))) + return response + + def _update_firewall_policy( + self, + source_account_alias, + location, + firewall_policy_id, + firewall_dict): + """ + Updates a firewall policy for a given datacenter and account alias + :param source_account_alias: the source account alias for the firewall policy + :param location: datacenter of the firewall policy + :param firewall_policy_id: firewall policy id to update + :param firewall_dict: dictionary of request parameters for firewall policy + :return: response: response from CLC API call + """ + try: + response = self.clc.v2.API.Call( + 'PUT', + '/v2-experimental/firewallPolicies/%s/%s/%s' % + (source_account_alias, + location, + firewall_policy_id), + firewall_dict) + except APIFailedResponse as e: + return self.module.fail_json( + msg="Unable to update the firewall policy id : {0}. {1}".format( + firewall_policy_id, str(e.response_text))) + return response + + @staticmethod + def _compare_get_request_with_dict(response, firewall_dict): + """ + Helper method to compare the json response for getting the firewall policy with the request parameters + :param response: response from the get method + :param firewall_dict: dictionary of request parameters for firewall policy + :return: changed: Boolean that returns true if there are differences between + the response parameters and the playbook parameters + """ + + changed = False + + response_dest_account_alias = response.get('destinationAccount') + response_enabled = response.get('enabled') + response_source = response.get('source') + response_dest = response.get('destination') + response_ports = response.get('ports') + request_dest_account_alias = firewall_dict.get( + 'destination_account_alias') + request_enabled = firewall_dict.get('enabled') + if request_enabled is None: + request_enabled = True + request_source = firewall_dict.get('source') + request_dest = firewall_dict.get('destination') + request_ports = firewall_dict.get('ports') + + if ( + response_dest_account_alias and str(response_dest_account_alias) != str(request_dest_account_alias)) or ( + response_enabled != request_enabled) or ( + response_source and response_source != request_source) or ( + response_dest and response_dest != request_dest) or ( + response_ports and response_ports != request_ports): + changed = True + return changed + + def _get_firewall_policy( + self, + source_account_alias, + location, + firewall_policy_id): + """ + Get back details for a particular firewall policy + :param source_account_alias: the source account alias for the firewall policy + :param location: datacenter of the firewall policy + :param firewall_policy_id: id of the firewall policy to get + :return: response - The response from CLC API call + """ + response = None + try: + response = self.clc.v2.API.Call( + 'GET', '/v2-experimental/firewallPolicies/%s/%s/%s' % + (source_account_alias, location, firewall_policy_id)) + except APIFailedResponse as e: + if e.response_status_code != 404: + self.module.fail_json( + msg="Unable to fetch the firewall policy with id : {0}. {1}".format( + firewall_policy_id, str(e.response_text))) + return response + + def _wait_for_requests_to_complete( + self, + source_account_alias, + location, + firewall_policy_id, + wait_limit=50): + """ + Waits until the CLC requests are complete if the wait argument is True + :param source_account_alias: The source account alias for the firewall policy + :param location: datacenter of the firewall policy + :param firewall_policy_id: The firewall policy id + :param wait_limit: The number of times to check the status for completion + :return: the firewall_policy object + """ + wait = self.module.params.get('wait') + count = 0 + firewall_policy = None + while wait: + count += 1 + firewall_policy = self._get_firewall_policy( + source_account_alias, location, firewall_policy_id) + status = firewall_policy.get('status') + if status == 'active' or count > wait_limit: + wait = False + else: + # wait for 2 seconds + sleep(2) + return firewall_policy + + @staticmethod + def _set_user_agent(clc): + if hasattr(clc, 'SetRequestsSession'): + agent_string = "ClcAnsibleModule/" + __version__ + ses = requests.Session() + ses.headers.update({"Api-Client": agent_string}) + ses.headers['User-Agent'] += " " + agent_string + clc.SetRequestsSession(ses) + + +def main(): + """ + The main function. Instantiates the module and calls process_request. + :return: none + """ + module = AnsibleModule( + argument_spec=ClcFirewallPolicy._define_module_argument_spec(), + supports_check_mode=True) + + clc_firewall = ClcFirewallPolicy(module) + clc_firewall.process_request() + +from ansible.module_utils.basic import * # pylint: disable=W0614 +if __name__ == '__main__': + main() From 4a43f52437b5568837925c6cbbc43927365e7d3e Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Tue, 28 Jul 2015 10:49:18 -0700 Subject: [PATCH 0533/2522] Use fetch_url's basic auth instead of code specific to this module --- monitoring/librato_annotation.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/monitoring/librato_annotation.py b/monitoring/librato_annotation.py index 4927a9cf4c7..f174bda0ea4 100644 --- a/monitoring/librato_annotation.py +++ b/monitoring/librato_annotation.py @@ -20,8 +20,6 @@ # -import base64 - DOCUMENTATION = ''' --- module: librato_annotation @@ -29,9 +27,8 @@ description: - Create an annotation event on the given annotation stream :name. If the annotation stream does not exist, it will be created automatically version_added: "1.6" -author: "Seth Edwards (@sedward)" -requirements: - - base64 +author: "Seth Edwards (@sedward)" +requirements: [] options: user: description: @@ -130,8 +127,10 @@ def post_annotation(module): headers = {} headers['Content-Type'] = 'application/json' - headers['Authorization'] = "Basic " + base64.b64encode(user + ":" + api_key).strip() + # Hack send parameters the way fetch_url wants them + module.params['url_username'] = user + module.params['url_password'] = api_key response, info = fetch_url(module, url, data=json_body, headers=headers) if info['status'] != 200: module.fail_json(msg="Request Failed", reason=e.reason) From 650dabff5925684ab570a3702a8a96bdea845bd5 Mon Sep 17 00:00:00 2001 From: Siva Popuri Date: Tue, 28 Jul 2015 12:58:07 -0500 Subject: [PATCH 0534/2522] Ansible module for managing server snapshots in Centurylink Cloud --- cloud/centurylink/clc_server_snapshot.py | 400 +++++++++++++++++++++++ 1 file changed, 400 insertions(+) create mode 100644 cloud/centurylink/clc_server_snapshot.py diff --git a/cloud/centurylink/clc_server_snapshot.py b/cloud/centurylink/clc_server_snapshot.py new file mode 100644 index 00000000000..cb5f66e7a8d --- /dev/null +++ b/cloud/centurylink/clc_server_snapshot.py @@ -0,0 +1,400 @@ +#!/usr/bin/python + +# +# Copyright (c) 2015 CenturyLink +# +# This file is part of Ansible. +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see +# + +DOCUMENTATION = ''' +module: clc_server_snapshot +short_description: Create, Delete and Restore server snapshots in CenturyLink Cloud. +description: + - An Ansible module to Create, Delete and Restore server snapshots in CenturyLink Cloud. +version_added: "2.0" +options: + server_ids: + description: + - The list of CLC server Ids. + required: True + expiration_days: + description: + - The number of days to keep the server snapshot before it expires. + default: 7 + required: False + state: + description: + - The state to insure that the provided resources are in. + default: 'present' + required: False + choices: ['present', 'absent', 'restore'] + wait: + description: + - Whether to wait for the provisioning tasks to finish before returning. + default: True + required: False + choices: [True, False] +requirements: + - python = 2.7 + - requests >= 2.5.0 + - clc-sdk +notes: + - To use this module, it is required to set the below environment variables which enables access to the + Centurylink Cloud + - CLC_V2_API_USERNAME, the account login id for the centurylink cloud + - CLC_V2_API_PASSWORD, the account password for the centurylink cloud + - Alternatively, the module accepts the API token and account alias. The API token can be generated using the + CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login + - CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login + - CLC_ACCT_ALIAS, the account alias associated with the centurylink cloud + - Users can set CLC_V2_API_URL to specify an endpoint for pointing to a different CLC environment. +''' + +EXAMPLES = ''' +# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples + +- name: Create server snapshot + clc_server_snapshot: + server_ids: + - UC1TESTSVR01 + - UC1TESTSVR02 + expiration_days: 10 + wait: True + state: present + +- name: Restore server snapshot + clc_server_snapshot: + server_ids: + - UC1TESTSVR01 + - UC1TESTSVR02 + wait: True + state: restore + +- name: Delete server snapshot + clc_server_snapshot: + server_ids: + - UC1TESTSVR01 + - UC1TESTSVR02 + wait: True + state: absent +''' + +__version__ = '${version}' + +from distutils.version import LooseVersion + +try: + import requests +except ImportError: + REQUESTS_FOUND = False +else: + REQUESTS_FOUND = True + +# +# Requires the clc-python-sdk. +# sudo pip install clc-sdk +# +try: + import clc as clc_sdk + from clc import CLCException +except ImportError: + CLC_FOUND = False + clc_sdk = None +else: + CLC_FOUND = True + + +class ClcSnapshot: + + clc = clc_sdk + module = None + + def __init__(self, module): + """ + Construct module + """ + self.module = module + + if not CLC_FOUND: + self.module.fail_json( + msg='clc-python-sdk required for this module') + if not REQUESTS_FOUND: + self.module.fail_json( + msg='requests library is required for this module') + if requests.__version__ and LooseVersion( + requests.__version__) < LooseVersion('2.5.0'): + self.module.fail_json( + msg='requests library version should be >= 2.5.0') + + self._set_user_agent(self.clc) + + def process_request(self): + """ + Process the request - Main Code Path + :return: Returns with either an exit_json or fail_json + """ + p = self.module.params + server_ids = p['server_ids'] + expiration_days = p['expiration_days'] + state = p['state'] + request_list = [] + changed = False + changed_servers = [] + + self._set_clc_credentials_from_env() + if state == 'present': + changed, request_list, changed_servers = self.ensure_server_snapshot_present( + server_ids=server_ids, + expiration_days=expiration_days) + elif state == 'absent': + changed, request_list, changed_servers = self.ensure_server_snapshot_absent( + server_ids=server_ids) + elif state == 'restore': + changed, request_list, changed_servers = self.ensure_server_snapshot_restore( + server_ids=server_ids) + + self._wait_for_requests_to_complete(request_list) + return self.module.exit_json( + changed=changed, + server_ids=changed_servers) + + def ensure_server_snapshot_present(self, server_ids, expiration_days): + """ + Ensures the given set of server_ids have the snapshots created + :param server_ids: The list of server_ids to create the snapshot + :param expiration_days: The number of days to keep the snapshot + :return: (changed, request_list, changed_servers) + changed: A flag indicating whether any change was made + request_list: the list of clc request objects from CLC API call + changed_servers: The list of servers ids that are modified + """ + request_list = [] + changed = False + servers = self._get_servers_from_clc( + server_ids, + 'Failed to obtain server list from the CLC API') + servers_to_change = [ + server for server in servers if len( + server.GetSnapshots()) == 0] + for server in servers_to_change: + changed = True + if not self.module.check_mode: + request = self._create_server_snapshot(server, expiration_days) + request_list.append(request) + changed_servers = [ + server.id for server in servers_to_change if server.id] + return changed, request_list, changed_servers + + def _create_server_snapshot(self, server, expiration_days): + """ + Create the snapshot for the CLC server + :param server: the CLC server object + :param expiration_days: The number of days to keep the snapshot + :return: the create request object from CLC API Call + """ + result = None + try: + result = server.CreateSnapshot( + delete_existing=True, + expiration_days=expiration_days) + except CLCException as ex: + self.module.fail_json(msg='Failed to create snapshot for server : {0}. {1}'.format( + server.id, ex.response_text + )) + return result + + def ensure_server_snapshot_absent(self, server_ids): + """ + Ensures the given set of server_ids have the snapshots removed + :param server_ids: The list of server_ids to delete the snapshot + :return: (changed, request_list, changed_servers) + changed: A flag indicating whether any change was made + request_list: the list of clc request objects from CLC API call + changed_servers: The list of servers ids that are modified + """ + request_list = [] + changed = False + servers = self._get_servers_from_clc( + server_ids, + 'Failed to obtain server list from the CLC API') + servers_to_change = [ + server for server in servers if len( + server.GetSnapshots()) > 0] + for server in servers_to_change: + changed = True + if not self.module.check_mode: + request = self._delete_server_snapshot(server) + request_list.append(request) + changed_servers = [ + server.id for server in servers_to_change if server.id] + return changed, request_list, changed_servers + + def _delete_server_snapshot(self, server): + """ + Delete snapshot for the CLC server + :param server: the CLC server object + :return: the delete snapshot request object from CLC API + """ + result = None + try: + result = server.DeleteSnapshot() + except CLCException as ex: + self.module.fail_json(msg='Failed to delete snapshot for server : {0}. {1}'.format( + server.id, ex.response_text + )) + return result + + def ensure_server_snapshot_restore(self, server_ids): + """ + Ensures the given set of server_ids have the snapshots restored + :param server_ids: The list of server_ids to delete the snapshot + :return: (changed, request_list, changed_servers) + changed: A flag indicating whether any change was made + request_list: the list of clc request objects from CLC API call + changed_servers: The list of servers ids that are modified + """ + request_list = [] + changed = False + servers = self._get_servers_from_clc( + server_ids, + 'Failed to obtain server list from the CLC API') + servers_to_change = [ + server for server in servers if len( + server.GetSnapshots()) > 0] + for server in servers_to_change: + changed = True + if not self.module.check_mode: + request = self._restore_server_snapshot(server) + request_list.append(request) + changed_servers = [ + server.id for server in servers_to_change if server.id] + return changed, request_list, changed_servers + + def _restore_server_snapshot(self, server): + """ + Restore snapshot for the CLC server + :param server: the CLC server object + :return: the restore snapshot request object from CLC API + """ + result = None + try: + result = server.RestoreSnapshot() + except CLCException as ex: + self.module.fail_json(msg='Failed to restore snapshot for server : {0}. {1}'.format( + server.id, ex.response_text + )) + return result + + def _wait_for_requests_to_complete(self, requests_lst): + """ + Waits until the CLC requests are complete if the wait argument is True + :param requests_lst: The list of CLC request objects + :return: none + """ + if not self.module.params['wait']: + return + for request in requests_lst: + request.WaitUntilComplete() + for request_details in request.requests: + if request_details.Status() != 'succeeded': + self.module.fail_json( + msg='Unable to process server snapshot request') + + @staticmethod + def define_argument_spec(): + """ + This function defines the dictionary object required for + package module + :return: the package dictionary object + """ + argument_spec = dict( + server_ids=dict(type='list', required=True), + expiration_days=dict(default=7), + wait=dict(default=True), + state=dict( + default='present', + choices=[ + 'present', + 'absent', + 'restore']), + ) + return argument_spec + + def _get_servers_from_clc(self, server_list, message): + """ + Internal function to fetch list of CLC server objects from a list of server ids + :param server_list: The list of server ids + :param message: The error message to throw in case of any error + :return the list of CLC server objects + """ + try: + return self.clc.v2.Servers(server_list).servers + except CLCException as ex: + return self.module.fail_json(msg=message + ': %s' % ex) + + def _set_clc_credentials_from_env(self): + """ + Set the CLC Credentials on the sdk by reading environment variables + :return: none + """ + env = os.environ + v2_api_token = env.get('CLC_V2_API_TOKEN', False) + v2_api_username = env.get('CLC_V2_API_USERNAME', False) + v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) + clc_alias = env.get('CLC_ACCT_ALIAS', False) + api_url = env.get('CLC_V2_API_URL', False) + + if api_url: + self.clc.defaults.ENDPOINT_URL_V2 = api_url + + if v2_api_token and clc_alias: + self.clc._LOGIN_TOKEN_V2 = v2_api_token + self.clc._V2_ENABLED = True + self.clc.ALIAS = clc_alias + elif v2_api_username and v2_api_passwd: + self.clc.v2.SetCredentials( + api_username=v2_api_username, + api_passwd=v2_api_passwd) + else: + return self.module.fail_json( + msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " + "environment variables") + + @staticmethod + def _set_user_agent(clc): + if hasattr(clc, 'SetRequestsSession'): + agent_string = "ClcAnsibleModule/" + __version__ + ses = requests.Session() + ses.headers.update({"Api-Client": agent_string}) + ses.headers['User-Agent'] += " " + agent_string + clc.SetRequestsSession(ses) + + +def main(): + """ + Main function + :return: None + """ + module = AnsibleModule( + argument_spec=ClcSnapshot.define_argument_spec(), + supports_check_mode=True + ) + clc_snapshot = ClcSnapshot(module) + clc_snapshot.process_request() + +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From e30d8b84fe7ff3be427a46ff67629cce55252594 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 28 Jul 2015 14:25:59 -0400 Subject: [PATCH 0535/2522] more doc fixes --- windows/win_dotnet_ngen.py | 1 - windows/win_webpicmd.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/windows/win_dotnet_ngen.py b/windows/win_dotnet_ngen.py index 90f464a1b0b..75ce9cc138b 100644 --- a/windows/win_dotnet_ngen.py +++ b/windows/win_dotnet_ngen.py @@ -35,7 +35,6 @@ - there are in fact two scheduled tasks for ngen but they have no triggers so aren't a problem - there's no way to test if they've been completed (?) - the stdout is quite likely to be several megabytes -options: author: Peter Mounce ''' diff --git a/windows/win_webpicmd.py b/windows/win_webpicmd.py index dc26e88bfba..215123cef8c 100644 --- a/windows/win_webpicmd.py +++ b/windows/win_webpicmd.py @@ -30,7 +30,8 @@ - Installs packages using Web Platform Installer command-line (http://www.iis.net/learn/install/web-platform-installer/web-platform-installer-v4-command-line-webpicmdexe-rtw-release). - Must be installed and present in PATH (see win_chocolatey module; 'webpicmd' is the package name, and you must install 'lessmsi' first too) - Install IIS first (see win_feature module) - - Note: accepts EULAs and suppresses reboot - you will need to check manage reboots yourself (see win_reboot module) +notes: + - accepts EULAs and suppresses reboot - you will need to check manage reboots yourself (see win_reboot module) options: name: description: From 0297a7e7e67c43e545b001b1499321d4849a7799 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Tue, 28 Jul 2015 11:35:58 -0700 Subject: [PATCH 0536/2522] Use fetch_urls code to do basic auth instead of our own i nthe twilio module --- notification/twilio.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/notification/twilio.py b/notification/twilio.py index 58898b0bc73..9ed1a09e12e 100644 --- a/notification/twilio.py +++ b/notification/twilio.py @@ -104,7 +104,6 @@ # ======================================= # twilio module support methods # -import base64 import urllib @@ -119,14 +118,15 @@ def post_twilio_api(module, account_sid, auth_token, msg, from_number, data['MediaUrl'] = media_url encoded_data = urllib.urlencode(data) - base64string = base64.encodestring('%s:%s' % \ - (account_sid, auth_token)).replace('\n', '') - headers = {'User-Agent': AGENT, 'Content-type': 'application/x-www-form-urlencoded', 'Accept': 'application/json', - 'Authorization': 'Basic %s' % base64string, } + + # Hack module params to have the Basic auth params that fetch_url expects + module.params['url_username'] = account_sid.replace('\n', '') + module.params['url_password'] = auth_token.replace('\n', '') + return fetch_url(module, URI, data=encoded_data, headers=headers) From fe69c0e67aa872ed948ce8e4474d608f8b8c62e5 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Tue, 28 Jul 2015 12:23:54 -0700 Subject: [PATCH 0537/2522] Switch from httplib to fetch_url --- notification/pushover.py | 47 ++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/notification/pushover.py b/notification/pushover.py index 505917189e4..0c1d6e94ab9 100644 --- a/notification/pushover.py +++ b/notification/pushover.py @@ -57,24 +57,36 @@ ''' import urllib -import httplib -class pushover(object): +class Pushover(object): ''' Instantiates a pushover object, use it to send notifications ''' + base_uri = 'https://api.pushover.net' + port = 443 - def __init__(self): - self.host, self.port = 'api.pushover.net', 443 + def __init__(self, module, user, token): + self.module = module + self.user = user + self.token = token - def run(self): + def run(self, priority, msg): ''' Do, whatever it is, we do. ''' + + url = '%s:%s/1/messages.json' % (self.base_uri, self.port) + # parse config - conn = httplib.HTTPSConnection(self.host, self.port) - conn.request("POST", "/1/messages.json", - urllib.urlencode(self.options), - {"Content-type": "application/x-www-form-urlencoded"}) - conn.getresponse() - return + options = dict(user=self.user, + token=self.token, + priority=priority, + message=msg) + data = urllib.urlencode(options) + + headers = { "Content-type": "application/x-www-form-urlencoded"} + r, info = fetch_url(self.module, url, method='POST', data=data, headers=headers) + if info['status'] != 200: + raise Exception(info) + + return r.read() def main(): @@ -88,14 +100,9 @@ def main(): ), ) - msg_object = pushover() - msg_object.options = {} - msg_object.options['user'] = module.params['user_key'] - msg_object.options['token'] = module.params['app_token'] - msg_object.options['priority'] = module.params['pri'] - msg_object.options['message'] = module.params['msg'] + msg_object = Pushover(module, module.params['user_key'], module.params['app_token']) try: - msg_object.run() + msg_object.run(module.params['pri'], module.params['msg']) except: module.fail_json(msg='Unable to send msg via pushover') @@ -103,4 +110,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +from ansible.module_utils.urls import * +if __name__ == '__main__': + main() From 128e5284fc10648570bd0d2487b638a238582079 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 28 Jul 2015 16:12:50 -0400 Subject: [PATCH 0538/2522] minor doc fixes to xenserver_facts --- cloud/xenserver_facts.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cloud/xenserver_facts.py b/cloud/xenserver_facts.py index 1977f9fe2b8..54ca3389752 100644 --- a/cloud/xenserver_facts.py +++ b/cloud/xenserver_facts.py @@ -18,11 +18,13 @@ DOCUMENTATION = ''' --- module: xenserver_facts -version_added: 1.7 +version_added: "2.0" short_description: get facts reported on xenserver description: - Reads data out of XenAPI, can be used instead of multiple xe commands. -author: Andy Hill, Tim Rupp +author: + - Andy Hill (@andyhky) + - Tim Rupp ''' import platform From 827b2a4196e23c9117b5e975198e5de12e8314b1 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 28 Jul 2015 18:53:13 -0400 Subject: [PATCH 0539/2522] clarified xmpp lib to use --- notification/jabber.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/notification/jabber.py b/notification/jabber.py index 606287cd8e4..6d97e4232df 100644 --- a/notification/jabber.py +++ b/notification/jabber.py @@ -59,7 +59,8 @@ required: false # informational: requirements for nodes -requirements: [ xmpp ] +requirements: + - python xmpp (xmpppy) author: "Brian Coca (@bcoca)" ''' @@ -111,7 +112,7 @@ def main(): ) if not HAS_XMPP: - module.fail_json(msg="xmpp is not installed") + module.fail_json(msg="The required python xmpp library (xmpppy) is not installed") jid = xmpp.JID(module.params['user']) user = jid.getNode() From 23375e52adb3628d5945ef9f8d7d4b3b8a600257 Mon Sep 17 00:00:00 2001 From: Rob White Date: Wed, 29 Jul 2015 19:38:01 +1000 Subject: [PATCH 0540/2522] New module - ec2_vpc_subnet_facts --- cloud/amazon/ec2_vpc_subnet_facts.py | 130 +++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 cloud/amazon/ec2_vpc_subnet_facts.py diff --git a/cloud/amazon/ec2_vpc_subnet_facts.py b/cloud/amazon/ec2_vpc_subnet_facts.py new file mode 100644 index 00000000000..a865d35f6ab --- /dev/null +++ b/cloud/amazon/ec2_vpc_subnet_facts.py @@ -0,0 +1,130 @@ +#!/usr/bin/python +# +# This is a free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This Ansible library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this library. If not, see . + +DOCUMENTATION = ''' +--- +module: ec2_vpc_subnet_facts +short_description: Gather facts about ec2 VPC subnets in AWS +description: + - Gather facts about ec2 VPC subnets in AWS +version_added: "2.0" +author: "Rob White (@wimnat)" +options: + filters: + description: + - A dict of filters to apply. Each dict item consists of a filter key and a filter value. See U(http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeSubnets.html) for possible filters. + required: false + default: null + region: + description: + - The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used. See U(http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region) + required: false + default: null + aliases: [ 'aws_region', 'ec2_region' ] + +extends_documentation_fragment: aws +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Gather facts about all VPC subnets +- ec2_vpc_subnet_facts: + +# Gather facts about a particular VPC subnet using ID +- ec2_vpc_subnet_facts: + filters: + - subnet-id: subnet-00112233 + +# Gather facts about any VPC subnet with a tag key Name and value Example +- ec2_vpc_subnet_facts: + filters: + - "tag:Name": Example + +# Gather facts about any VPC subnet within VPC with ID vpc-abcdef00 +- ec2_vpc_subnet_facts: + filters: + - vpc-id: vpc-abcdef00 + +''' + +try: + import boto.vpc + from boto.exception import BotoServerError + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +def get_subnet_info(subnet): + + subnet_info = { 'id': subnet.id, + 'availability_zone': subnet.availability_zone, + 'available_ip_address_count': subnet.available_ip_address_count, + 'cidr_block': subnet.cidr_block, + 'default_for_az': subnet.defaultForAz, + 'map_public_ip_on_launch': subnet.mapPublicIpOnLaunch, + 'state': subnet.state, + 'tags': subnet.tags, + 'vpc_id': subnet.vpc_id + } + + return subnet_info + +def list_ec2_vpc_subnets(connection, module): + + filters = module.params.get("filters") + subnet_dict_array = [] + + try: + all_subnets = connection.get_all_subnets(filters=filters) + except BotoServerError as e: + module.fail_json(msg=e.message) + + for subnet in all_subnets: + subnet_dict_array.append(get_subnet_info(subnet)) + + module.exit_json(subnets=subnet_dict_array) + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + filters = dict(default=None, type='dict') + ) + ) + + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO: + module.fail_json(msg='boto required for this module') + + region, ec2_url, aws_connect_params = get_aws_connection_info(module) + + if region: + try: + connection = connect_to_aws(boto.vpc, region, **aws_connect_params) + except (boto.exception.NoAuthHandlerFound, StandardError), e: + module.fail_json(msg=str(e)) + else: + module.fail_json(msg="region must be specified") + + list_ec2_vpc_subnets(connection, module) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() \ No newline at end of file From b2f8e5dc590c8a05777c2c48b04209b690fc92b7 Mon Sep 17 00:00:00 2001 From: Serge van Ginderachter Date: Wed, 29 Jul 2015 16:13:02 +0200 Subject: [PATCH 0541/2522] Return devicenodes as empty list if no LUN's got connected. It is possible for an intiator to successfully connect to a target, whilst getting no LUN's back. If no devicenodes get detected, it makes more sense to return an empty list than plainly None. This potentially avoids further tasks to have to check if devicenodes is iterable. --- system/open_iscsi.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/system/open_iscsi.py b/system/open_iscsi.py index c661a723d77..0eea8424e32 100644 --- a/system/open_iscsi.py +++ b/system/open_iscsi.py @@ -206,18 +206,15 @@ def target_device_node(module, target): # a given target... devices = glob.glob('/dev/disk/by-path/*%s*' % target) - if len(devices) == 0: - return None - else: - devdisks = [] - for dev in devices: - # exclude partitions - if "-part" not in dev: - devdisk = os.path.realpath(dev) - # only add once (multi-path?) - if devdisk not in devdisks: - devdisks.append(devdisk) - return devdisks + devdisks = [] + for dev in devices: + # exclude partitions + if "-part" not in dev: + devdisk = os.path.realpath(dev) + # only add once (multi-path?) + if devdisk not in devdisks: + devdisks.append(devdisk) + return devdisks def target_isauto(module, target): From d2b44b1053635bf9c658e945217df17dee189b37 Mon Sep 17 00:00:00 2001 From: Serge van Ginderachter Date: Wed, 29 Jul 2015 16:16:15 +0200 Subject: [PATCH 0542/2522] open_iscsi: minor PEP8 whitespace fixes --- system/open_iscsi.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/system/open_iscsi.py b/system/open_iscsi.py index 0eea8424e32..cbf350a416f 100644 --- a/system/open_iscsi.py +++ b/system/open_iscsi.py @@ -108,6 +108,7 @@ ISCSIADM = 'iscsiadm' + def compare_nodelists(l1, l2): l1.sort() @@ -159,7 +160,7 @@ def target_loggedon(module, target): cmd = '%s --mode session' % iscsiadm_cmd (rc, out, err) = module.run_command(cmd) - + if rc == 0: return target in out elif rc == 21: @@ -186,7 +187,7 @@ def target_login(module, target): cmd = '%s --mode node --targetname %s --login' % (iscsiadm_cmd, target) (rc, out, err) = module.run_command(cmd) - + if rc > 0: module.fail_json(cmd=cmd, rc=rc, msg=err) @@ -195,7 +196,7 @@ def target_logout(module, target): cmd = '%s --mode node --targetname %s --logout' % (iscsiadm_cmd, target) (rc, out, err) = module.run_command(cmd) - + if rc > 0: module.fail_json(cmd=cmd, rc=rc, msg=err) @@ -221,7 +222,7 @@ def target_isauto(module, target): cmd = '%s --mode node --targetname %s' % (iscsiadm_cmd, target) (rc, out, err) = module.run_command(cmd) - + if rc == 0: lines = out.splitlines() for line in lines: @@ -236,7 +237,7 @@ def target_setauto(module, target): cmd = '%s --mode node --targetname %s --op=update --name node.startup --value automatic' % (iscsiadm_cmd, target) (rc, out, err) = module.run_command(cmd) - + if rc > 0: module.fail_json(cmd=cmd, rc=rc, msg=err) @@ -245,7 +246,7 @@ def target_setmanual(module, target): cmd = '%s --mode node --targetname %s --op=update --name node.startup --value manual' % (iscsiadm_cmd, target) (rc, out, err) = module.run_command(cmd) - + if rc > 0: module.fail_json(cmd=cmd, rc=rc, msg=err) @@ -256,7 +257,7 @@ def main(): module = AnsibleModule( argument_spec = dict( - # target + # target portal = dict(required=False, aliases=['ip']), port = dict(required=False, default=3260), target = dict(required=False, aliases=['name', 'targetname']), @@ -269,14 +270,14 @@ def main(): auto_node_startup = dict(type='bool', aliases=['automatic']), discover = dict(type='bool', default=False), show_nodes = dict(type='bool', default=False) - ), + ), required_together=[['discover_user', 'discover_pass'], ['node_user', 'node_pass']], supports_check_mode=True ) - global iscsiadm_cmd + global iscsiadm_cmd iscsiadm_cmd = module.get_bin_path('iscsiadm', required=True) # parameters @@ -292,7 +293,7 @@ def main(): cached = iscsi_get_cached_nodes(module, portal) - # return json dict + # return json dict result = {} result['changed'] = False @@ -330,17 +331,17 @@ def main(): result['nodes'] = nodes if login is not None: - loggedon = target_loggedon(module,target) + loggedon = target_loggedon(module, target) if (login and loggedon) or (not login and not loggedon): result['changed'] |= False if login: - result['devicenodes'] = target_device_node(module,target) + result['devicenodes'] = target_device_node(module, target) elif not check: if login: target_login(module, target) # give udev some time time.sleep(1) - result['devicenodes'] = target_device_node(module,target) + result['devicenodes'] = target_device_node(module, target) else: target_logout(module, target) result['changed'] |= True @@ -368,7 +369,6 @@ def main(): module.exit_json(**result) - # import module snippets from ansible.module_utils.basic import * From 6aaae617cae1067af8fc66bde77f9f195cf83d46 Mon Sep 17 00:00:00 2001 From: Kevin Brebanov Date: Wed, 29 Jul 2015 16:22:32 -0400 Subject: [PATCH 0543/2522] Modify 'name' argument to be of type 'list' in order to support 'with_items' looping --- packaging/os/apk.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packaging/os/apk.py b/packaging/os/apk.py index 4b78a898901..f8ddbc2aa04 100644 --- a/packaging/os/apk.py +++ b/packaging/os/apk.py @@ -177,7 +177,7 @@ def main(): module = AnsibleModule( argument_spec = dict( state = dict(default='present', choices=['present', 'installed', 'absent', 'removed', 'latest']), - name = dict(type='str'), + name = dict(type='list'), update_cache = dict(default='no', choices=BOOLEANS, type='bool'), upgrade = dict(default='no', choices=BOOLEANS, type='bool'), ), @@ -204,14 +204,10 @@ def main(): if p['upgrade']: upgrade_packages(module) - # Create a list of package names - # Removing empty strings that may have been created by a trailing ',' - names = filter((lambda x: x != ''), p['name'].split(',')) - if p['state'] in ['present', 'latest']: - install_packages(module, names, p['state']) + install_packages(module, p['name'], p['state']) elif p['state'] == 'absent': - remove_packages(module, names) + remove_packages(module, p['name']) # Import module snippets. from ansible.module_utils.basic import * From d40bdd464588a1d971393af7af7a033a52797088 Mon Sep 17 00:00:00 2001 From: whiter Date: Thu, 30 Jul 2015 11:37:01 +1000 Subject: [PATCH 0544/2522] Updated doco for vpc_subnet --- cloud/amazon/ec2_vpc_subnet.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/cloud/amazon/ec2_vpc_subnet.py b/cloud/amazon/ec2_vpc_subnet.py index 56efd85841a..f7d96862685 100644 --- a/cloud/amazon/ec2_vpc_subnet.py +++ b/cloud/amazon/ec2_vpc_subnet.py @@ -22,32 +22,34 @@ version_added: "2.0" author: Robert Estelle (@erydo) options: - vpc_id: - description: - - VPC ID of the VPC in which to create the subnet. - required: false - default: null - resource_tags: + az: description: - - A dictionary array of resource tags of the form: { tag1: value1, tag2: value2 }. This module identifies a subnet by CIDR and will update the subnet's tags to match. Tags not in this list will be ignored. + - "The availability zone for the subnet. Only required when state=present." required: false default: null cidr: description: - - The CIDR block for the subnet. E.g. 10.0.0.0/16. Only required when state=present." + - "The CIDR block for the subnet. E.g. 10.0.0.0/16. Only required when state=present." required: false default: null - az: + tags: description: - - "The availability zone for the subnet. Only required when state=present." + - "A dictionary array of resource tags of the form: { tag1: value1, tag2: value2 }. This module identifies a subnet by CIDR and will update the subnet's tags to match. Tags not in this list will be ignored." required: false default: null + aliases: [ 'resource_tags' ] state: description: - - Create or remove the subnet + - "Create or remove the subnet" required: false default: present choices: [ 'present', 'absent' ] + vpc_id: + description: + - "VPC ID of the VPC in which to create the subnet." + required: false + default: null + extends_documentation_fragment: aws ''' From 50f4b2c3de3b2530cceda4e3209c8fbafbedb941 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Wed, 29 Jul 2015 22:11:36 -0400 Subject: [PATCH 0545/2522] minor doc fix --- system/selinux_permissive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/selinux_permissive.py b/system/selinux_permissive.py index ec3575d9da4..1e2a5c6c996 100644 --- a/system/selinux_permissive.py +++ b/system/selinux_permissive.py @@ -25,7 +25,7 @@ short_description: Change permissive domain in SELinux policy description: - Add and remove domain from the list of permissive domain. -version_added: "1.9" +version_added: "2.0" options: domain: description: From e8e0076bf4d61affc1708877daac70d6cff4ff5e Mon Sep 17 00:00:00 2001 From: Michael Schuett Date: Wed, 29 Jul 2015 22:21:14 -0400 Subject: [PATCH 0546/2522] Update and rename ec2_search.py to ec2_remote_facts.py --- cloud/amazon/{ec2_search.py => ec2_remote_facts.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename cloud/amazon/{ec2_search.py => ec2_remote_facts.py} (99%) diff --git a/cloud/amazon/ec2_search.py b/cloud/amazon/ec2_remote_facts.py similarity index 99% rename from cloud/amazon/ec2_search.py rename to cloud/amazon/ec2_remote_facts.py index 5bc1698501c..8980947190c 100644 --- a/cloud/amazon/ec2_search.py +++ b/cloud/amazon/ec2_remote_facts.py @@ -15,7 +15,7 @@ DOCUMENTATION = ''' --- -module: ec2_search +module: ec2_remote_facts short_description: ask EC2 for information about other instances. description: - Only supports seatch for hostname by tags currently. Looking to add more later. From 2583ae6df026af36a7362dae94bf523691ec1c5f Mon Sep 17 00:00:00 2001 From: Michael Schuett Date: Thu, 30 Jul 2015 01:26:36 -0400 Subject: [PATCH 0547/2522] Cleanup docs Change to 2.0 release and remove unneeded empty aliases. --- cloud/amazon/ec2_remote_facts.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/cloud/amazon/ec2_remote_facts.py b/cloud/amazon/ec2_remote_facts.py index 8980947190c..035b7b42394 100644 --- a/cloud/amazon/ec2_remote_facts.py +++ b/cloud/amazon/ec2_remote_facts.py @@ -19,38 +19,33 @@ short_description: ask EC2 for information about other instances. description: - Only supports seatch for hostname by tags currently. Looking to add more later. -version_added: "1.9" +version_added: "2.0" options: key: description: - instance tag key in EC2 required: false default: Name - aliases: [] value: description: - instance tag value in EC2 required: false default: null - aliases: [] lookup: description: - What type of lookup to use when searching EC2 instance info. required: false default: tags - aliases: [] region: description: - EC2 region that it should look for tags in required: false default: All Regions - aliases: [] ignore_state: description: - instance state that should be ignored such as terminated. required: false default: terminated - aliases: [] author: - "Michael Schuett (@michaeljs1990)" extends_documentation_fragment: aws From 1e1855080526fa4c7c930a66990dbb44a732ce7c Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 29 Jul 2015 23:10:46 -0700 Subject: [PATCH 0548/2522] Port vsphere_copy from httplib to open_url for TLS cert validation --- cloud/vmware/vsphere_copy.py | 55 +++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/cloud/vmware/vsphere_copy.py b/cloud/vmware/vsphere_copy.py index 4364e8b5197..2e983589749 100644 --- a/cloud/vmware/vsphere_copy.py +++ b/cloud/vmware/vsphere_copy.py @@ -54,6 +54,14 @@ description: - The file to push to the datastore on the vCenter server. required: true + validate_certs: + description: + - If C(no), SSL certificates will not be validated. This should only be + set to C(no) when no other option exists. + required: false + default: 'yes' + choices: ['yes', 'no'] + notes: - "This module ought to be run from a system that can access vCenter directly and has the file to transfer. It can be the normal remote target or you can change it either by using C(transport: local) or using C(delegate_to)." @@ -68,8 +76,6 @@ ''' import atexit -import base64 -import httplib import urllib import mmap import errno @@ -100,6 +106,7 @@ def main(): datacenter = dict(required=True), datastore = dict(required=True), dest = dict(required=True, aliases=[ 'path' ]), + validate_certs = dict(required=False, default=True, type='bool'), ), # Implementing check-mode using HEAD is impossible, since size/date is not 100% reliable supports_check_mode = False, @@ -112,6 +119,7 @@ def main(): datacenter = module.params.get('datacenter') datastore = module.params.get('datastore') dest = module.params.get('dest') + validate_certs = module.params.get('validate_certs') fd = open(src, "rb") atexit.register(fd.close) @@ -119,37 +127,46 @@ def main(): data = mmap.mmap(fd.fileno(), 0, access=mmap.ACCESS_READ) atexit.register(data.close) - conn = httplib.HTTPSConnection(host) - atexit.register(conn.close) - remote_path = vmware_path(datastore, datacenter, dest) - auth = base64.encodestring('%s:%s' % (login, password)).rstrip() + url = 'https://%s%s' % (host, remote_path) + headers = { "Content-Type": "application/octet-stream", "Content-Length": str(len(data)), - "Authorization": "Basic %s" % auth, } - # URL is only used in JSON output (helps troubleshooting) - url = 'https://%s%s' % (host, remote_path) - try: - conn.request("PUT", remote_path, body=data, headers=headers) + r = open_url(module, url, data=data, headers=headers, method='PUT', + url_username=login, url_password=password, validate_certs=validate_certs) except socket.error, e: if isinstance(e.args, tuple) and e[0] == errno.ECONNRESET: # VSphere resets connection if the file is in use and cannot be replaced module.fail_json(msg='Failed to upload, image probably in use', status=e[0], reason=str(e), url=url) else: module.fail_json(msg=str(e), status=e[0], reason=str(e), url=url) - - resp = conn.getresponse() - - if resp.status in range(200, 300): - module.exit_json(changed=True, status=resp.status, reason=resp.reason, url=url) + except Exception, e: + status = -1 + try: + if isinstance(e[0], int): + status = e[0] + except KeyError: + pass + module.fail_json(msg=str(e), status=status, reason=str(e), url=url) + + status = r.getcode() + if satus >= 200 and status < 300: + module.exit_json(changed=True, status=status, reason=r.msg, url=url) else: - module.fail_json(msg='Failed to upload', status=resp.status, reason=resp.reason, length=resp.length, version=resp.version, headers=resp.getheaders(), chunked=resp.chunked, url=url) + length = r.headers.get('content-length', None) + if r.headers.get('transfer-encoding', '').lower() == 'chunked': + chunked = 1 + else: + chunked = 0 + + module.fail_json(msg='Failed to upload', status=status, reason=r.msg, length=length, headers=dict(r.headers), chunked=chunked, url=url) # Import module snippets from ansible.module_utils.basic import * - -main() +from ansible.module_utils.urls import * +if __name__ == '__main__': + main() From b8d9ab898ddf4991927eaea18f30b67808c46e6e Mon Sep 17 00:00:00 2001 From: Siva Popuri Date: Thu, 30 Jul 2015 13:34:13 -0500 Subject: [PATCH 0549/2522] Ansible module for modifying existing servers in Centurylink cloud --- cloud/centurylink/clc_modify_server.py | 981 +++++++++++++++++++++++++ 1 file changed, 981 insertions(+) create mode 100644 cloud/centurylink/clc_modify_server.py diff --git a/cloud/centurylink/clc_modify_server.py b/cloud/centurylink/clc_modify_server.py new file mode 100644 index 00000000000..9683f6835df --- /dev/null +++ b/cloud/centurylink/clc_modify_server.py @@ -0,0 +1,981 @@ +#!/usr/bin/python + +# +# Copyright (c) 2015 CenturyLink +# +# This file is part of Ansible. +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see +# + +DOCUMENTATION = ''' +module: clc_modify_server +short_description: modify servers in CenturyLink Cloud. +description: + - An Ansible module to modify servers in CenturyLink Cloud. +version_added: "2.0" +options: + server_ids: + description: + - A list of server Ids to modify. + required: True + cpu: + description: + - How many CPUs to update on the server + required: False + default: None + memory: + description: + - Memory (in GB) to set to the server. + required: False + default: None + anti_affinity_policy_id: + description: + - The anti affinity policy id to be set for a hyper scale server. + This is mutually exclusive with 'anti_affinity_policy_name' + required: False + default: None + anti_affinity_policy_name: + description: + - The anti affinity policy name to be set for a hyper scale server. + This is mutually exclusive with 'anti_affinity_policy_id' + required: False + default: None + alert_policy_id: + description: + - The alert policy id to be associated to the server. + This is mutually exclusive with 'alert_policy_name' + required: False + default: None + alert_policy_name: + description: + - The alert policy name to be associated to the server. + This is mutually exclusive with 'alert_policy_id' + required: False + default: None + state: + description: + - The state to insure that the provided resources are in. + default: 'present' + required: False + choices: ['present', 'absent'] + wait: + description: + - Whether to wait for the provisioning tasks to finish before returning. + default: True + required: False + choices: [ True, False] +requirements: + - python = 2.7 + - requests >= 2.5.0 + - clc-sdk +notes: + - To use this module, it is required to set the below environment variables which enables access to the + Centurylink Cloud + - CLC_V2_API_USERNAME, the account login id for the centurylink cloud + - CLC_V2_API_PASSWORD, the account password for the centurylink cloud + - Alternatively, the module accepts the API token and account alias. The API token can be generated using the + CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login + - CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login + - CLC_ACCT_ALIAS, the account alias associated with the centurylink cloud + - Users can set CLC_V2_API_URL to specify an endpoint for pointing to a different CLC environment. +''' + +EXAMPLES = ''' +# Note - You must set the CLC_V2_API_USERNAME And CLC_V2_API_PASSWD Environment variables before running these examples + +- name: set the cpu count to 4 on a server + clc_modify_server: + server_ids: + - UC1TESTSVR01 + - UC1TESTSVR02 + cpu: 4 + state: present + +- name: set the memory to 8GB on a server + clc_modify_server: + server_ids: + - UC1TESTSVR01 + - UC1TESTSVR02 + memory: 8 + state: present + +- name: set the anti affinity policy on a server + clc_modify_server: + server_ids: + - UC1TESTSVR01 + - UC1TESTSVR02 + anti_affinity_policy_name: 'aa_policy' + state: present + +- name: remove the anti affinity policy on a server + clc_modify_server: + server_ids: + - UC1TESTSVR01 + - UC1TESTSVR02 + anti_affinity_policy_name: 'aa_policy' + state: absent + +- name: add the alert policy on a server + clc_modify_server: + server_ids: + - UC1TESTSVR01 + - UC1TESTSVR02 + alert_policy_name: 'alert_policy' + state: present + +- name: remove the alert policy on a server + clc_modify_server: + server_ids: + - UC1TESTSVR01 + - UC1TESTSVR02 + alert_policy_name: 'alert_policy' + state: absent + +- name: set the memory to 16GB and cpu to 8 core on a lust if servers + clc_modify_server: + server_ids: + - UC1TESTSVR01 + - UC1TESTSVR02 + cpu: 8 + memory: 16 + state: present +''' + +RETURN = ''' +changed: + description: A flag indicating if any change was made or not + returned: success + type: boolean + sample: True +server_ids: + description: The list of server ids that are changed + returned: success + type: list + sample: + [ + "UC1TEST-SVR01", + "UC1TEST-SVR02" + ] +servers: + description: The list of server objects that are changed + returned: success + type: list + sample: + [ + { + "changeInfo":{ + "createdBy":"service.wfad", + "createdDate":1438196820, + "modifiedBy":"service.wfad", + "modifiedDate":1438196820 + }, + "description":"test-server", + "details":{ + "alertPolicies":[ + + ], + "cpu":1, + "customFields":[ + + ], + "diskCount":3, + "disks":[ + { + "id":"0:0", + "partitionPaths":[ + + ], + "sizeGB":1 + }, + { + "id":"0:1", + "partitionPaths":[ + + ], + "sizeGB":2 + }, + { + "id":"0:2", + "partitionPaths":[ + + ], + "sizeGB":14 + } + ], + "hostName":"", + "inMaintenanceMode":false, + "ipAddresses":[ + { + "internal":"10.1.1.1" + } + ], + "memoryGB":1, + "memoryMB":1024, + "partitions":[ + + ], + "powerState":"started", + "snapshots":[ + + ], + "storageGB":17 + }, + "groupId":"086ac1dfe0b6411989e8d1b77c4065f0", + "id":"test-server", + "ipaddress":"10.120.45.23", + "isTemplate":false, + "links":[ + { + "href":"/v2/servers/wfad/test-server", + "id":"test-server", + "rel":"self", + "verbs":[ + "GET", + "PATCH", + "DELETE" + ] + }, + { + "href":"/v2/groups/wfad/086ac1dfe0b6411989e8d1b77c4065f0", + "id":"086ac1dfe0b6411989e8d1b77c4065f0", + "rel":"group" + }, + { + "href":"/v2/accounts/wfad", + "id":"wfad", + "rel":"account" + }, + { + "href":"/v2/billing/wfad/serverPricing/test-server", + "rel":"billing" + }, + { + "href":"/v2/servers/wfad/test-server/publicIPAddresses", + "rel":"publicIPAddresses", + "verbs":[ + "POST" + ] + }, + { + "href":"/v2/servers/wfad/test-server/credentials", + "rel":"credentials" + }, + { + "href":"/v2/servers/wfad/test-server/statistics", + "rel":"statistics" + }, + { + "href":"/v2/servers/wfad/510ec21ae82d4dc89d28479753bf736a/upcomingScheduledActivities", + "rel":"upcomingScheduledActivities" + }, + { + "href":"/v2/servers/wfad/510ec21ae82d4dc89d28479753bf736a/scheduledActivities", + "rel":"scheduledActivities", + "verbs":[ + "GET", + "POST" + ] + }, + { + "href":"/v2/servers/wfad/test-server/capabilities", + "rel":"capabilities" + }, + { + "href":"/v2/servers/wfad/test-server/alertPolicies", + "rel":"alertPolicyMappings", + "verbs":[ + "POST" + ] + }, + { + "href":"/v2/servers/wfad/test-server/antiAffinityPolicy", + "rel":"antiAffinityPolicyMapping", + "verbs":[ + "PUT", + "DELETE" + ] + }, + { + "href":"/v2/servers/wfad/test-server/cpuAutoscalePolicy", + "rel":"cpuAutoscalePolicyMapping", + "verbs":[ + "PUT", + "DELETE" + ] + } + ], + "locationId":"UC1", + "name":"test-server", + "os":"ubuntu14_64Bit", + "osType":"Ubuntu 14 64-bit", + "status":"active", + "storageType":"standard", + "type":"standard" + } + ] +''' + +__version__ = '${version}' + +from distutils.version import LooseVersion + +try: + import requests +except ImportError: + REQUESTS_FOUND = False +else: + REQUESTS_FOUND = True + +# +# Requires the clc-python-sdk. +# sudo pip install clc-sdk +# +try: + import clc as clc_sdk + from clc import CLCException + from clc import APIFailedResponse +except ImportError: + CLC_FOUND = False + clc_sdk = None +else: + CLC_FOUND = True + + +class ClcModifyServer: + clc = clc_sdk + + def __init__(self, module): + """ + Construct module + """ + self.clc = clc_sdk + self.module = module + + if not CLC_FOUND: + self.module.fail_json( + msg='clc-python-sdk required for this module') + if not REQUESTS_FOUND: + self.module.fail_json( + msg='requests library is required for this module') + if requests.__version__ and LooseVersion( + requests.__version__) < LooseVersion('2.5.0'): + self.module.fail_json( + msg='requests library version should be >= 2.5.0') + + self._set_user_agent(self.clc) + + def process_request(self): + """ + Process the request - Main Code Path + :return: Returns with either an exit_json or fail_json + """ + self._set_clc_credentials_from_env() + + p = self.module.params + cpu = p.get('cpu') + memory = p.get('memory') + state = p.get('state') + if state == 'absent' and (cpu or memory): + return self.module.fail_json( + msg='\'absent\' state is not supported for \'cpu\' and \'memory\' arguments') + + server_ids = p['server_ids'] + if not isinstance(server_ids, list): + return self.module.fail_json( + msg='server_ids needs to be a list of instances to modify: %s' % + server_ids) + + (changed, server_dict_array, changed_server_ids) = self._modify_servers( + server_ids=server_ids) + + self.module.exit_json( + changed=changed, + server_ids=changed_server_ids, + servers=server_dict_array) + + @staticmethod + def _define_module_argument_spec(): + """ + Define the argument spec for the ansible module + :return: argument spec dictionary + """ + argument_spec = dict( + server_ids=dict(type='list', required=True), + state=dict(default='present', choices=['present', 'absent']), + cpu=dict(), + memory=dict(), + anti_affinity_policy_id=dict(), + anti_affinity_policy_name=dict(), + alert_policy_id=dict(), + alert_policy_name=dict(), + wait=dict(type='bool', default=True) + ) + mutually_exclusive = [ + ['anti_affinity_policy_id', 'anti_affinity_policy_name'], + ['alert_policy_id', 'alert_policy_name'] + ] + return {"argument_spec": argument_spec, + "mutually_exclusive": mutually_exclusive} + + def _set_clc_credentials_from_env(self): + """ + Set the CLC Credentials on the sdk by reading environment variables + :return: none + """ + env = os.environ + v2_api_token = env.get('CLC_V2_API_TOKEN', False) + v2_api_username = env.get('CLC_V2_API_USERNAME', False) + v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) + clc_alias = env.get('CLC_ACCT_ALIAS', False) + api_url = env.get('CLC_V2_API_URL', False) + + if api_url: + self.clc.defaults.ENDPOINT_URL_V2 = api_url + + if v2_api_token and clc_alias: + self.clc._LOGIN_TOKEN_V2 = v2_api_token + self.clc._V2_ENABLED = True + self.clc.ALIAS = clc_alias + elif v2_api_username and v2_api_passwd: + self.clc.v2.SetCredentials( + api_username=v2_api_username, + api_passwd=v2_api_passwd) + else: + return self.module.fail_json( + msg="You must set the CLC_V2_API_USERNAME and CLC_V2_API_PASSWD " + "environment variables") + + def _get_servers_from_clc(self, server_list, message): + """ + Internal function to fetch list of CLC server objects from a list of server ids + :param server_list: The list of server ids + :param message: the error message to throw in case of any error + :return the list of CLC server objects + """ + try: + return self.clc.v2.Servers(server_list).servers + except CLCException as ex: + return self.module.fail_json(msg=message + ': %s' % ex.message) + + def _modify_servers(self, server_ids): + """ + modify the servers configuration on the provided list + :param server_ids: list of servers to modify + :return: a list of dictionaries with server information about the servers that were modified + """ + p = self.module.params + state = p.get('state') + server_params = { + 'cpu': p.get('cpu'), + 'memory': p.get('memory'), + 'anti_affinity_policy_id': p.get('anti_affinity_policy_id'), + 'anti_affinity_policy_name': p.get('anti_affinity_policy_name'), + 'alert_policy_id': p.get('alert_policy_id'), + 'alert_policy_name': p.get('alert_policy_name'), + } + changed = False + server_changed = False + aa_changed = False + ap_changed = False + server_dict_array = [] + result_server_ids = [] + request_list = [] + changed_servers = [] + + if not isinstance(server_ids, list) or len(server_ids) < 1: + return self.module.fail_json( + msg='server_ids should be a list of servers, aborting') + + servers = self._get_servers_from_clc( + server_ids, + 'Failed to obtain server list from the CLC API') + for server in servers: + if state == 'present': + server_changed, server_result = self._ensure_server_config( + server, server_params) + if server_result: + request_list.append(server_result) + aa_changed = self._ensure_aa_policy_present( + server, + server_params) + ap_changed = self._ensure_alert_policy_present( + server, + server_params) + elif state == 'absent': + aa_changed = self._ensure_aa_policy_absent( + server, + server_params) + ap_changed = self._ensure_alert_policy_absent( + server, + server_params) + if server_changed or aa_changed or ap_changed: + changed_servers.append(server) + changed = True + + self._wait_for_requests(self.module, request_list) + self._refresh_servers(self.module, changed_servers) + + for server in changed_servers: + server_dict_array.append(server.data) + result_server_ids.append(server.id) + + return changed, server_dict_array, result_server_ids + + def _ensure_server_config( + self, server, server_params): + """ + ensures the server is updated with the provided cpu and memory + :param server: the CLC server object + :param server_params: the dictionary of server parameters + :return: (changed, group) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + cpu = server_params.get('cpu') + memory = server_params.get('memory') + changed = False + result = None + + if not cpu: + cpu = server.cpu + if not memory: + memory = server.memory + if memory != server.memory or cpu != server.cpu: + if not self.module.check_mode: + result = self._modify_clc_server( + self.clc, + self.module, + server.id, + cpu, + memory) + changed = True + return changed, result + + @staticmethod + def _modify_clc_server(clc, module, server_id, cpu, memory): + """ + Modify the memory or CPU of a clc server. + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param server_id: id of the server to modify + :param cpu: the new cpu value + :param memory: the new memory value + :return: the result of CLC API call + """ + result = None + acct_alias = clc.v2.Account.GetAlias() + try: + # Update the server configuration + job_obj = clc.v2.API.Call('PATCH', + 'servers/%s/%s' % (acct_alias, + server_id), + json.dumps([{"op": "set", + "member": "memory", + "value": memory}, + {"op": "set", + "member": "cpu", + "value": cpu}])) + result = clc.v2.Requests(job_obj) + except APIFailedResponse as ex: + module.fail_json( + msg='Unable to update the server configuration for server : "{0}". {1}'.format( + server_id, str(ex.response_text))) + return result + + @staticmethod + def _wait_for_requests(module, request_list): + """ + Block until server provisioning requests are completed. + :param module: the AnsibleModule object + :param request_list: a list of clc-sdk.Request instances + :return: none + """ + wait = module.params.get('wait') + if wait: + # Requests.WaitUntilComplete() returns the count of failed requests + failed_requests_count = sum( + [request.WaitUntilComplete() for request in request_list]) + + if failed_requests_count > 0: + module.fail_json( + msg='Unable to process modify server request') + + @staticmethod + def _refresh_servers(module, servers): + """ + Loop through a list of servers and refresh them. + :param module: the AnsibleModule object + :param servers: list of clc-sdk.Server instances to refresh + :return: none + """ + for server in servers: + try: + server.Refresh() + except CLCException as ex: + module.fail_json(msg='Unable to refresh the server {0}. {1}'.format( + server.id, ex.message + )) + + def _ensure_aa_policy_present( + self, server, server_params): + """ + ensures the server is updated with the provided anti affinity policy + :param server: the CLC server object + :param server_params: the dictionary of server parameters + :return: (changed, group) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + changed = False + acct_alias = self.clc.v2.Account.GetAlias() + + aa_policy_id = server_params.get('anti_affinity_policy_id') + aa_policy_name = server_params.get('anti_affinity_policy_name') + if not aa_policy_id and aa_policy_name: + aa_policy_id = self._get_aa_policy_id_by_name( + self.clc, + self.module, + acct_alias, + aa_policy_name) + current_aa_policy_id = self._get_aa_policy_id_of_server( + self.clc, + self.module, + acct_alias, + server.id) + + if aa_policy_id and aa_policy_id != current_aa_policy_id: + self._modify_aa_policy( + self.clc, + self.module, + acct_alias, + server.id, + aa_policy_id) + changed = True + return changed + + def _ensure_aa_policy_absent( + self, server, server_params): + """ + ensures the the provided anti affinity policy is removed from the server + :param server: the CLC server object + :param server_params: the dictionary of server parameters + :return: (changed, group) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + changed = False + acct_alias = self.clc.v2.Account.GetAlias() + aa_policy_id = server_params.get('anti_affinity_policy_id') + aa_policy_name = server_params.get('anti_affinity_policy_name') + if not aa_policy_id and aa_policy_name: + aa_policy_id = self._get_aa_policy_id_by_name( + self.clc, + self.module, + acct_alias, + aa_policy_name) + current_aa_policy_id = self._get_aa_policy_id_of_server( + self.clc, + self.module, + acct_alias, + server.id) + + if aa_policy_id and aa_policy_id == current_aa_policy_id: + self._delete_aa_policy( + self.clc, + self.module, + acct_alias, + server.id) + changed = True + return changed + + @staticmethod + def _modify_aa_policy(clc, module, acct_alias, server_id, aa_policy_id): + """ + modifies the anti affinity policy of the CLC server + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param acct_alias: the CLC account alias + :param server_id: the CLC server id + :param aa_policy_id: the anti affinity policy id + :return: result: The result from the CLC API call + """ + result = None + if not module.check_mode: + try: + result = clc.v2.API.Call('PUT', + 'servers/%s/%s/antiAffinityPolicy' % ( + acct_alias, + server_id), + json.dumps({"id": aa_policy_id})) + except APIFailedResponse as ex: + module.fail_json( + msg='Unable to modify anti affinity policy to server : "{0}". {1}'.format( + server_id, str(ex.response_text))) + return result + + @staticmethod + def _delete_aa_policy(clc, module, acct_alias, server_id): + """ + Delete the anti affinity policy of the CLC server + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param acct_alias: the CLC account alias + :param server_id: the CLC server id + :return: result: The result from the CLC API call + """ + result = None + if not module.check_mode: + try: + result = clc.v2.API.Call('DELETE', + 'servers/%s/%s/antiAffinityPolicy' % ( + acct_alias, + server_id), + json.dumps({})) + except APIFailedResponse as ex: + module.fail_json( + msg='Unable to delete anti affinity policy to server : "{0}". {1}'.format( + server_id, str(ex.response_text))) + return result + + @staticmethod + def _get_aa_policy_id_by_name(clc, module, alias, aa_policy_name): + """ + retrieves the anti affinity policy id of the server based on the name of the policy + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param alias: the CLC account alias + :param aa_policy_name: the anti affinity policy name + :return: aa_policy_id: The anti affinity policy id + """ + aa_policy_id = None + try: + aa_policies = clc.v2.API.Call(method='GET', + url='antiAffinityPolicies/%s' % alias) + except APIFailedResponse as ex: + return module.fail_json( + msg='Unable to fetch anti affinity policies from account alias : "{0}". {1}'.format( + alias, str(ex.response_text))) + for aa_policy in aa_policies.get('items'): + if aa_policy.get('name') == aa_policy_name: + if not aa_policy_id: + aa_policy_id = aa_policy.get('id') + else: + return module.fail_json( + msg='multiple anti affinity policies were found with policy name : %s' % aa_policy_name) + if not aa_policy_id: + module.fail_json( + msg='No anti affinity policy was found with policy name : %s' % aa_policy_name) + return aa_policy_id + + @staticmethod + def _get_aa_policy_id_of_server(clc, module, alias, server_id): + """ + retrieves the anti affinity policy id of the server based on the CLC server id + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param alias: the CLC account alias + :param server_id: the CLC server id + :return: aa_policy_id: The anti affinity policy id + """ + aa_policy_id = None + try: + result = clc.v2.API.Call( + method='GET', url='servers/%s/%s/antiAffinityPolicy' % + (alias, server_id)) + aa_policy_id = result.get('id') + except APIFailedResponse as ex: + if ex.response_status_code != 404: + module.fail_json(msg='Unable to fetch anti affinity policy for server "{0}". {1}'.format( + server_id, str(ex.response_text))) + return aa_policy_id + + def _ensure_alert_policy_present( + self, server, server_params): + """ + ensures the server is updated with the provided alert policy + :param server: the CLC server object + :param server_params: the dictionary of server parameters + :return: (changed, group) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + changed = False + acct_alias = self.clc.v2.Account.GetAlias() + alert_policy_id = server_params.get('alert_policy_id') + alert_policy_name = server_params.get('alert_policy_name') + if not alert_policy_id and alert_policy_name: + alert_policy_id = self._get_alert_policy_id_by_name( + self.clc, + self.module, + acct_alias, + alert_policy_name) + if alert_policy_id and not self._alert_policy_exists( + server, alert_policy_id): + self._add_alert_policy_to_server( + self.clc, + self.module, + acct_alias, + server.id, + alert_policy_id) + changed = True + return changed + + def _ensure_alert_policy_absent( + self, server, server_params): + """ + ensures the alert policy is removed from the server + :param server: the CLC server object + :param server_params: the dictionary of server parameters + :return: (changed, group) - + changed: Boolean whether a change was made + result: The result from the CLC API call + """ + changed = False + + acct_alias = self.clc.v2.Account.GetAlias() + alert_policy_id = server_params.get('alert_policy_id') + alert_policy_name = server_params.get('alert_policy_name') + if not alert_policy_id and alert_policy_name: + alert_policy_id = self._get_alert_policy_id_by_name( + self.clc, + self.module, + acct_alias, + alert_policy_name) + + if alert_policy_id and self._alert_policy_exists( + server, alert_policy_id): + self._remove_alert_policy_to_server( + self.clc, + self.module, + acct_alias, + server.id, + alert_policy_id) + changed = True + return changed + + @staticmethod + def _add_alert_policy_to_server( + clc, module, acct_alias, server_id, alert_policy_id): + """ + add the alert policy to CLC server + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param acct_alias: the CLC account alias + :param server_id: the CLC server id + :param alert_policy_id: the alert policy id + :return: result: The result from the CLC API call + """ + result = None + if not module.check_mode: + try: + result = clc.v2.API.Call('POST', + 'servers/%s/%s/alertPolicies' % ( + acct_alias, + server_id), + json.dumps({"id": alert_policy_id})) + except APIFailedResponse as ex: + module.fail_json(msg='Unable to set alert policy to the server : "{0}". {1}'.format( + server_id, str(ex.response_text))) + return result + + @staticmethod + def _remove_alert_policy_to_server( + clc, module, acct_alias, server_id, alert_policy_id): + """ + remove the alert policy to the CLC server + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param acct_alias: the CLC account alias + :param server_id: the CLC server id + :param alert_policy_id: the alert policy id + :return: result: The result from the CLC API call + """ + result = None + if not module.check_mode: + try: + result = clc.v2.API.Call('DELETE', + 'servers/%s/%s/alertPolicies/%s' + % (acct_alias, server_id, alert_policy_id)) + except APIFailedResponse as ex: + module.fail_json(msg='Unable to remove alert policy from the server : "{0}". {1}'.format( + server_id, str(ex.response_text))) + return result + + @staticmethod + def _get_alert_policy_id_by_name(clc, module, alias, alert_policy_name): + """ + retrieves the alert policy id of the server based on the name of the policy + :param clc: the clc-sdk instance to use + :param module: the AnsibleModule object + :param alias: the CLC account alias + :param alert_policy_name: the alert policy name + :return: alert_policy_id: The alert policy id + """ + alert_policy_id = None + try: + alert_policies = clc.v2.API.Call(method='GET', + url='alertPolicies/%s' % alias) + except APIFailedResponse as ex: + return module.fail_json(msg='Unable to fetch alert policies for account : "{0}". {1}'.format( + alias, str(ex.response_text))) + for alert_policy in alert_policies.get('items'): + if alert_policy.get('name') == alert_policy_name: + if not alert_policy_id: + alert_policy_id = alert_policy.get('id') + else: + return module.fail_json( + msg='multiple alert policies were found with policy name : %s' % alert_policy_name) + return alert_policy_id + + @staticmethod + def _alert_policy_exists(server, alert_policy_id): + """ + Checks if the alert policy exists for the server + :param server: the clc server object + :param alert_policy_id: the alert policy + :return: True: if the given alert policy id associated to the server, False otherwise + """ + result = False + alert_policies = server.alertPolicies + if alert_policies: + for alert_policy in alert_policies: + if alert_policy.get('id') == alert_policy_id: + result = True + return result + + @staticmethod + def _set_user_agent(clc): + if hasattr(clc, 'SetRequestsSession'): + agent_string = "ClcAnsibleModule/" + __version__ + ses = requests.Session() + ses.headers.update({"Api-Client": agent_string}) + ses.headers['User-Agent'] += " " + agent_string + clc.SetRequestsSession(ses) + + +def main(): + """ + The main function. Instantiates the module and calls process_request. + :return: none + """ + + argument_dict = ClcModifyServer._define_module_argument_spec() + module = AnsibleModule(supports_check_mode=True, **argument_dict) + clc_modify_server = ClcModifyServer(module) + clc_modify_server.process_request() + +from ansible.module_utils.basic import * # pylint: disable=W0614 +if __name__ == '__main__': + main() From 27bf1934839719ed032fc90d137453d3dc0ee99d Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 30 Jul 2015 14:48:59 -0400 Subject: [PATCH 0550/2522] added missing license headers fixes #508 --- database/postgresql/postgresql_lang.py | 18 ++++++++++++++++-- monitoring/bigpanda.py | 18 +++++++++++++++++- monitoring/circonus_annotation.py | 19 +++++++++++++++++-- monitoring/datadog_event.py | 16 ++++++++++++++++ monitoring/pagerduty.py | 16 ++++++++++++++++ monitoring/pingdom.py | 17 ++++++++++++++++- monitoring/stackdriver.py | 16 +++++++++++++++- monitoring/uptimerobot.py | 17 ++++++++++++++++- notification/campfire.py | 16 +++++++++++++++- notification/hipchat.py | 16 +++++++++++++++- notification/typetalk.py | 16 +++++++++++++++- system/locale_gen.py | 16 +++++++++++++++- 12 files changed, 189 insertions(+), 12 deletions(-) diff --git a/database/postgresql/postgresql_lang.py b/database/postgresql/postgresql_lang.py index f3b1baa4d9a..ccee93194ea 100644 --- a/database/postgresql/postgresql_lang.py +++ b/database/postgresql/postgresql_lang.py @@ -1,8 +1,22 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - +# # (c) 2014, Jens Depuydt - +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . DOCUMENTATION = ''' --- module: postgresql_lang diff --git a/monitoring/bigpanda.py b/monitoring/bigpanda.py index cd08ac9f29e..0139f3a598e 100644 --- a/monitoring/bigpanda.py +++ b/monitoring/bigpanda.py @@ -1,4 +1,20 @@ #!/usr/bin/python +# -*- coding: utf-8 -*- + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . DOCUMENTATION = ''' --- @@ -6,7 +22,7 @@ author: "Hagai Kariti (@hkariti)" short_description: Notify BigPanda about deployments version_added: "1.8" -description: +description: - Notify BigPanda when deployments start and end (successfully or not). Returns a deployment object containing all the parameters for future module calls. options: component: diff --git a/monitoring/circonus_annotation.py b/monitoring/circonus_annotation.py index 1585cd8080a..ae5c98c87a1 100644 --- a/monitoring/circonus_annotation.py +++ b/monitoring/circonus_annotation.py @@ -1,7 +1,22 @@ #!/usr/bin/python - +# -*- coding: utf-8 -*- +# # (c) 2014-2015, Epic Games, Inc. - +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . import requests import time import json diff --git a/monitoring/datadog_event.py b/monitoring/datadog_event.py index 406a5ea1865..25e8ce052b6 100644 --- a/monitoring/datadog_event.py +++ b/monitoring/datadog_event.py @@ -5,6 +5,22 @@ # # This module is proudly sponsored by iGeolise (www.igeolise.com) and # Tiny Lab Productions (www.tinylabproductions.com). +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + DOCUMENTATION = ''' --- diff --git a/monitoring/pagerduty.py b/monitoring/pagerduty.py index b35cfbf4992..99a9be8a044 100644 --- a/monitoring/pagerduty.py +++ b/monitoring/pagerduty.py @@ -1,4 +1,20 @@ #!/usr/bin/python +# -*- coding: utf-8 -*- +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . DOCUMENTATION = ''' diff --git a/monitoring/pingdom.py b/monitoring/pingdom.py index fd06a1217cb..4346e8ca6fe 100644 --- a/monitoring/pingdom.py +++ b/monitoring/pingdom.py @@ -1,5 +1,20 @@ #!/usr/bin/python - +# -*- coding: utf-8 -*- +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . DOCUMENTATION = ''' module: pingdom diff --git a/monitoring/stackdriver.py b/monitoring/stackdriver.py index 570e6659ac0..7b3688cbefc 100644 --- a/monitoring/stackdriver.py +++ b/monitoring/stackdriver.py @@ -1,6 +1,20 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . DOCUMENTATION = ''' module: stackdriver diff --git a/monitoring/uptimerobot.py b/monitoring/uptimerobot.py index 197c8877014..bdff8f1f134 100644 --- a/monitoring/uptimerobot.py +++ b/monitoring/uptimerobot.py @@ -1,5 +1,20 @@ #!/usr/bin/python - +# -*- coding: utf-8 -*- +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . DOCUMENTATION = ''' module: uptimerobot diff --git a/notification/campfire.py b/notification/campfire.py index ea4df7c0ba8..68e64f1bc94 100644 --- a/notification/campfire.py +++ b/notification/campfire.py @@ -1,6 +1,20 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . DOCUMENTATION = ''' --- module: campfire diff --git a/notification/hipchat.py b/notification/hipchat.py index f38461735c7..f565ca9cdfc 100644 --- a/notification/hipchat.py +++ b/notification/hipchat.py @@ -1,6 +1,20 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . DOCUMENTATION = ''' --- module: hipchat diff --git a/notification/typetalk.py b/notification/typetalk.py index f6b84cb00e7..4f6ee28130b 100644 --- a/notification/typetalk.py +++ b/notification/typetalk.py @@ -1,6 +1,20 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . DOCUMENTATION = ''' --- module: typetalk diff --git a/system/locale_gen.py b/system/locale_gen.py index 1988ce4f3b0..410f1dfc23d 100644 --- a/system/locale_gen.py +++ b/system/locale_gen.py @@ -1,6 +1,20 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . import os import os.path from subprocess import Popen, PIPE, call From 3ec3e77d535559d341d19067dd1494fc2cd1aa66 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Thu, 30 Jul 2015 13:44:13 -0700 Subject: [PATCH 0551/2522] Better status conditional from dagwieers --- cloud/vmware/vsphere_copy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/vmware/vsphere_copy.py b/cloud/vmware/vsphere_copy.py index 2e983589749..446f62bd5c5 100644 --- a/cloud/vmware/vsphere_copy.py +++ b/cloud/vmware/vsphere_copy.py @@ -154,7 +154,7 @@ def main(): module.fail_json(msg=str(e), status=status, reason=str(e), url=url) status = r.getcode() - if satus >= 200 and status < 300: + if 200 <= status < 300: module.exit_json(changed=True, status=status, reason=r.msg, url=url) else: length = r.headers.get('content-length', None) From c66a3fd4e11dc901178ae1b117b85415ccc7e1f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kim=20N=C3=B8rgaard?= Date: Fri, 31 Jul 2015 11:21:25 +0200 Subject: [PATCH 0552/2522] fixed a few issues pointed out by @resmo, pyflakes, pep8 --- packaging/os/slackpkg.py | 85 +++++++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 35 deletions(-) diff --git a/packaging/os/slackpkg.py b/packaging/os/slackpkg.py index 943042172ab..577b46d705b 100644 --- a/packaging/os/slackpkg.py +++ b/packaging/os/slackpkg.py @@ -5,8 +5,8 @@ # Written by Kim Nørgaard # Based on pkgng module written by bleader # that was based on pkgin module written by Shaun Zinck -# that was based on pacman module written by Afterburn -# that was based on apt module written by Matthew Williams +# that was based on pacman module written by Afterburn +# that was based on apt module written by Matthew Williams # # This module is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -29,7 +29,7 @@ description: - Manage binary packages for Slackware using 'slackpkg' which is available in versions after 12.2. -version_added: "1.6" +version_added: "2.0" options: name: description: @@ -50,7 +50,8 @@ default: false choices: [ true, false ] -author: Kim Nørgaard +author: Kim Nørgaard (@KimNorgaard) +requirements: [ "Slackware" >= 12.2 ] notes: [] ''' @@ -58,23 +59,19 @@ # Install package foo - slackpkg: name=foo state=present -# Remove packages foo and bar +# Remove packages foo and bar - slackpkg: name=foo,bar state=absent ''' -import json -import shlex -import os -import sys - def query_package(module, slackpkg_path, name): import glob import platform machine = platform.machine() - packages = glob.glob("/var/log/packages/%s-*-[%s|noarch]*" % (name, machine)) + packages = glob.glob("/var/log/packages/%s-*-[%s|noarch]*" % (name, + machine)) if len(packages) > 0: return True @@ -83,7 +80,7 @@ def query_package(module, slackpkg_path, name): def remove_packages(module, slackpkg_path, packages): - + remove_c = 0 # Using a for loop incase of error, we can report the package that failed for package in packages: @@ -92,11 +89,14 @@ def remove_packages(module, slackpkg_path, packages): continue if not module.check_mode: - rc, out, err = module.run_command("%s -default_answer=y -batch=on remove %s" % (slackpkg_path, package)) + rc, out, err = module.run_command("%s -default_answer=y -batch=on \ + remove %s" % (slackpkg_path, + package)) - if not module.check_mode and query_package(module, slackpkg_path, package): + if not module.check_mode and query_package(module, slackpkg_path, + package): module.fail_json(msg="failed to remove %s: %s" % (package, out)) - + remove_c += 1 if remove_c > 0: @@ -115,55 +115,70 @@ def install_packages(module, slackpkg_path, packages): continue if not module.check_mode: - rc, out, err = module.run_command("%s -default_answer=y -batch=on install %s" % (slackpkg_path, package)) + rc, out, err = module.run_command("%s -default_answer=y -batch=on \ + install %s" % (slackpkg_path, + package)) - if not module.check_mode and not query_package(module, slackpkg_path, package): - module.fail_json(msg="failed to install %s: %s" % (package, out), stderr=err) + if not module.check_mode and not query_package(module, slackpkg_path, + package): + module.fail_json(msg="failed to install %s: %s" % (package, out), + stderr=err) install_c += 1 - + if install_c > 0: - module.exit_json(changed=True, msg="present %s package(s)" % (install_c)) + module.exit_json(changed=True, msg="present %s package(s)" + % (install_c)) module.exit_json(changed=False, msg="package(s) already present") -def upgrade_packages(module, slackpkg_path, packages): +def upgrade_packages(module, slackpkg_path, packages): install_c = 0 for package in packages: if not module.check_mode: - rc, out, err = module.run_command("%s -default_answer=y -batch=on upgrade %s" % (slackpkg_path, package)) + rc, out, err = module.run_command("%s -default_answer=y -batch=on \ + upgrade %s" % (slackpkg_path, + package)) - if not module.check_mode and not query_package(module, slackpkg_path, package): - module.fail_json(msg="failed to install %s: %s" % (package, out), stderr=err) + if not module.check_mode and not query_package(module, slackpkg_path, + package): + module.fail_json(msg="failed to install %s: %s" % (package, out), + stderr=err) install_c += 1 - + if install_c > 0: - module.exit_json(changed=True, msg="present %s package(s)" % (install_c)) + module.exit_json(changed=True, msg="present %s package(s)" + % (install_c)) module.exit_json(changed=False, msg="package(s) already present") + def update_cache(module, slackpkg_path): rc, out, err = module.run_command("%s -batch=on update" % (slackpkg_path)) if rc != 0: module.fail_json(msg="Could not update package cache") + def main(): module = AnsibleModule( - argument_spec = dict( - state = dict(default="installed", choices=['installed', 'removed', 'absent', 'present', 'latest']), - name = dict(aliases=["pkg"], required=True), - update_cache = dict(default=False, aliases=["update-cache"], type='bool'), + argument_spec=dict( + state=dict(default="installed", choices=['installed', 'removed', + 'absent', 'present', + 'latest']), + name=dict(aliases=["pkg"], required=True, type='list'), + update_cache=dict(default=False, aliases=["update-cache"], + type='bool'), ), - supports_check_mode = True) + supports_check_mode=True) slackpkg_path = module.get_bin_path('slackpkg', True) p = module.params - pkgs = p['name'].split(",") + pkgs = p['name'] if p["update_cache"]: update_cache(module, slackpkg_path) @@ -171,13 +186,13 @@ def main(): if p['state'] == 'latest': upgrade_packages(module, slackpkg_path, pkgs) - elif p['state'] in [ 'present', 'installed' ]: + elif p['state'] in ['present', 'installed']: install_packages(module, slackpkg_path, pkgs) - elif p["state"] in [ 'removed', 'absent' ]: + elif p["state"] in ['removed', 'absent']: remove_packages(module, slackpkg_path, pkgs) # import module snippets from ansible.module_utils.basic import * - + main() From 04496b70ae6720d536bdcd4e64cf4f1b018d67b3 Mon Sep 17 00:00:00 2001 From: Thomas Bechtold Date: Fri, 31 Jul 2015 16:44:01 +0200 Subject: [PATCH 0553/2522] zypper_repository: Fix repo parsing for empty list When no repositories are defined in zypper, the return code of "zypper repos" is 6. Handle that case and don't fail if zypper_repository has to deal with an empty repo list. Fixes https://github.com/ansible/ansible-modules-extras/issues/795 --- packaging/os/zypper_repository.py | 37 +++++++++++++++++++------------ 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/packaging/os/zypper_repository.py b/packaging/os/zypper_repository.py index 54e20429638..446723ef042 100644 --- a/packaging/os/zypper_repository.py +++ b/packaging/os/zypper_repository.py @@ -95,21 +95,30 @@ def zypper_version(module): def _parse_repos(module): """parses the output of zypper -x lr and returns a parse repo dictionary""" cmd = ['/usr/bin/zypper', '-x', 'lr'] - repos = [] - from xml.dom.minidom import parseString as parseXML - rc, stdout, stderr = module.run_command(cmd, check_rc=True) - dom = parseXML(stdout) - repo_list = dom.getElementsByTagName('repo') - for repo in repo_list: - opts = {} - for o in REPO_OPTS: - opts[o] = repo.getAttribute(o) - opts['url'] = repo.getElementsByTagName('url')[0].firstChild.data - # A repo can be uniquely identified by an alias + url - repos.append(opts) - - return repos + rc, stdout, stderr = module.run_command(cmd, check_rc=False) + if rc == 0: + repos = [] + dom = parseXML(stdout) + repo_list = dom.getElementsByTagName('repo') + for repo in repo_list: + opts = {} + for o in REPO_OPTS: + opts[o] = repo.getAttribute(o) + opts['url'] = repo.getElementsByTagName('url')[0].firstChild.data + # A repo can be uniquely identified by an alias + url + repos.append(opts) + return repos + # exit code 6 is ZYPPER_EXIT_NO_REPOS (no repositories defined) + elif rc == 6: + return [] + else: + d = { 'zypper_exit_code': rc } + if stderr: + d['stderr'] = stderr + if stdout: + d['stdout'] = stdout + module.fail_json(msg='Failed to execute "%s"' % " ".join(cmd), **d) def _parse_repos_old(module): """parses the output of zypper sl and returns a parse repo dictionary""" From e299952bca2e81d83461ad78c98606aeee3f29b3 Mon Sep 17 00:00:00 2001 From: whiter Date: Sun, 2 Aug 2015 21:46:40 +1000 Subject: [PATCH 0554/2522] Changed resource_tags to tags to match other modules (resource_tags still an alias) Added get_subnet_info method to return more subnet info - matches same method in ec2_vpc_subnet_facts module Rework of tags - will now only apply the tags listed in the module. Existing tags not listed will be removed (desired state!) --- cloud/amazon/ec2_vpc_subnet.py | 85 +++++++++++++++++++++------------- 1 file changed, 53 insertions(+), 32 deletions(-) diff --git a/cloud/amazon/ec2_vpc_subnet.py b/cloud/amazon/ec2_vpc_subnet.py index f7d96862685..45e84f66939 100644 --- a/cloud/amazon/ec2_vpc_subnet.py +++ b/cloud/amazon/ec2_vpc_subnet.py @@ -34,7 +34,7 @@ default: null tags: description: - - "A dictionary array of resource tags of the form: { tag1: value1, tag2: value2 }. This module identifies a subnet by CIDR and will update the subnet's tags to match. Tags not in this list will be ignored." + - "A dict of tags to apply to the subnet. Any tags currently applied to the subnet and not present here will be removed." required: false default: null aliases: [ 'resource_tags' ] @@ -103,24 +103,49 @@ class AnsibleTagCreationException(AnsibleVPCSubnetException): pass +def get_subnet_info(subnet): + + subnet_info = { 'id': subnet.id, + 'availability_zone': subnet.availability_zone, + 'available_ip_address_count': subnet.available_ip_address_count, + 'cidr_block': subnet.cidr_block, + 'default_for_az': subnet.defaultForAz, + 'map_public_ip_on_launch': subnet.mapPublicIpOnLaunch, + 'state': subnet.state, + 'tags': subnet.tags, + 'vpc_id': subnet.vpc_id + } + + return subnet_info + def subnet_exists(vpc_conn, subnet_id): filters = {'subnet-id': subnet_id} - return len(vpc_conn.get_all_subnets(filters=filters)) > 0 + subnet = vpc_conn.get_all_subnets(filters=filters) + if subnet[0].state == "available": + return subnet[0] + else: + return False -def create_subnet(vpc_conn, vpc_id, cidr, az): +def create_subnet(vpc_conn, vpc_id, cidr, az, check_mode): try: - new_subnet = vpc_conn.create_subnet(vpc_id, cidr, az) + new_subnet = vpc_conn.create_subnet(vpc_id, cidr, az, dry_run=check_mode) # Sometimes AWS takes its time to create a subnet and so using # new subnets's id to do things like create tags results in # exception. boto doesn't seem to refresh 'state' of the newly # created subnet, i.e.: it's always 'pending'. - while not subnet_exists(vpc_conn, new_subnet.id): + subnet = False + while subnet is False: + subnet = subnet_exists(vpc_conn, new_subnet.id) time.sleep(0.1) except EC2ResponseError as e: - raise AnsibleVPCSubnetCreationException( - 'Unable to create subnet {0}, error: {1}'.format(cidr, e)) - return new_subnet + if e.error_code == "DryRunOperation": + subnet = None + else: + raise AnsibleVPCSubnetCreationException( + 'Unable to create subnet {0}, error: {1}'.format(cidr, e)) + + return subnet def get_resource_tags(vpc_conn, resource_id): @@ -158,29 +183,25 @@ def ensure_subnet_present(vpc_conn, vpc_id, cidr, az, tags, check_mode): subnet = get_matching_subnet(vpc_conn, vpc_id, cidr) changed = False if subnet is None: - if check_mode: - return {'changed': True, 'subnet_id': None, 'subnet': {}} - - subnet = create_subnet(vpc_conn, vpc_id, cidr, az) + subnet = create_subnet(vpc_conn, vpc_id, cidr, az, check_mode) + changed = True + # Subnet will be None when check_mode is true + if subnet is None: + return { + 'changed': changed, + 'subnet': {} + } + + if tags != subnet.tags: + ensure_tags(vpc_conn, subnet.id, tags, False, check_mode) + subnet.tags = tags changed = True - if tags is not None: - tag_result = ensure_tags(vpc_conn, subnet.id, tags, add_only=True, - check_mode=check_mode) - tags = tag_result['tags'] - changed = changed or tag_result['changed'] - else: - tags = get_resource_tags(vpc_conn, subnet.id) + subnet_info = get_subnet_info(subnet) return { 'changed': changed, - 'subnet_id': subnet.id, - 'subnet': { - 'tags': tags, - 'cidr': subnet.cidr_block, - 'az': subnet.availability_zone, - 'id': subnet.id, - } + 'subnet': subnet_info } @@ -202,11 +223,11 @@ def main(): argument_spec = ec2_argument_spec() argument_spec.update( dict( - vpc_id = dict(default=None, required=True), - resource_tags = dict(default=None, required=False, type='dict'), - cidr = dict(default=None, required=True), az = dict(default=None, required=False), - state = dict(default='present', choices=['present', 'absent']) + cidr = dict(default=None, required=True), + state = dict(default='present', choices=['present', 'absent']), + tags = dict(default=None, required=False, type='dict', aliases=['resource_tags']), + vpc_id = dict(default=None, required=True) ) ) @@ -226,7 +247,7 @@ def main(): module.fail_json(msg="region must be specified") vpc_id = module.params.get('vpc_id') - tags = module.params.get('resource_tags') + tags = module.params.get('tags') cidr = module.params.get('cidr') az = module.params.get('az') state = module.params.get('state') @@ -248,4 +269,4 @@ def main(): if __name__ == '__main__': main() - \ No newline at end of file + From 6a7b2f5869664a84a88324fd7636eaa6b7087585 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Sun, 2 Aug 2015 11:31:15 -0400 Subject: [PATCH 0555/2522] Workaround for bug in PowerShell Get-Website cmdlet. --- windows/win_iis_website.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/windows/win_iis_website.ps1 b/windows/win_iis_website.ps1 index bba1e941142..26a8df12730 100644 --- a/windows/win_iis_website.ps1 +++ b/windows/win_iis_website.ps1 @@ -70,7 +70,7 @@ $result = New-Object psobject @{ }; # Site info -$site = Get-Website -Name $name +$site = Get-Website | Where { $_.Name -eq $name } Try { # Add site @@ -113,7 +113,7 @@ Try { $result.changed = $true } - $site = Get-Website -Name $name + $site = Get-Website | Where { $_.Name -eq $name } If($site) { # Change Physical Path if needed if($physical_path) { @@ -165,7 +165,7 @@ Catch Fail-Json (New-Object psobject) $_.Exception.Message } -$site = Get-Website -Name $name +$site = Get-Website | Where { $_.Name -eq $name } $result.site = New-Object psobject @{ Name = $site.Name ID = $site.ID From 150d3ce0eeda61d25d04a97744e3785bf5903268 Mon Sep 17 00:00:00 2001 From: vaupelt Date: Mon, 3 Aug 2015 12:38:16 +0200 Subject: [PATCH 0556/2522] strange results with services=all I issued a command with action=disable_alerts host=webserver services=all set and get this results: "nagios_commands": [ "[1438593631] DISABLE_SVC_NOTIFICATIONS;webserver;a", "[1438593631] DISABLE_SVC_NOTIFICATIONS;webserver;l", "[1438593631] DISABLE_SVC_NOTIFICATIONS;webserver;l" ] This is not a big deal because i have just overlooked the action=silence command. Nevertheless a more predictable result would be a nice thing to have. --- monitoring/nagios.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/monitoring/nagios.py b/monitoring/nagios.py index 16edca2aa6a..6f92dc2e7e3 100644 --- a/monitoring/nagios.py +++ b/monitoring/nagios.py @@ -913,6 +913,8 @@ def act(self): elif self.action == 'enable_alerts': if self.services == 'host': self.enable_host_notifications(self.host) + elif self.services == 'all': + self.enable_host_svc_notifications(self.host) else: self.enable_svc_notifications(self.host, services=self.services) @@ -920,6 +922,8 @@ def act(self): elif self.action == 'disable_alerts': if self.services == 'host': self.disable_host_notifications(self.host) + elif self.services == 'all': + self.disable_host_svc_notifications(self.host) else: self.disable_svc_notifications(self.host, services=self.services) From 5d6f0d153cfb55d8f0f48f1a3e021270e1d711cb Mon Sep 17 00:00:00 2001 From: Kevin Brebanov Date: Mon, 3 Aug 2015 16:14:09 -0400 Subject: [PATCH 0557/2522] Use the module's get_bin_path function to find 'apk' and reuse the return value in all functions --- packaging/os/apk.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/packaging/os/apk.py b/packaging/os/apk.py index f8ddbc2aa04..f14d5593443 100644 --- a/packaging/os/apk.py +++ b/packaging/os/apk.py @@ -88,10 +88,8 @@ import os import re -APK_PATH="/sbin/apk" - def update_package_db(module): - cmd = "apk update" + cmd = "%s update" % (APK_PATH) rc, stdout, stderr = module.run_command(cmd, check_rc=False) if rc == 0: return True @@ -99,7 +97,7 @@ def update_package_db(module): module.fail_json(msg="could not update package db") def query_package(module, name): - cmd = "apk -v info --installed %s" % (name) + cmd = "%s -v info --installed %s" % (APK_PATH, name) rc, stdout, stderr = module.run_command(cmd, check_rc=False) if rc == 0: return True @@ -107,7 +105,7 @@ def query_package(module, name): return False def query_latest(module, name): - cmd = "apk version %s" % (name) + cmd = "%s version %s" % (APK_PATH, name) rc, stdout, stderr = module.run_command(cmd, check_rc=False) search_pattern = "(%s)-[\d\.\w]+-[\d\w]+\s+(.)\s+[\d\.\w]+-[\d\w]+\s+" % (name) match = re.search(search_pattern, stdout) @@ -117,9 +115,9 @@ def query_latest(module, name): def upgrade_packages(module): if module.check_mode: - cmd = "apk upgrade --simulate" + cmd = "%s upgrade --simulate" % (APK_PATH) else: - cmd = "apk upgrade" + cmd = "%s upgrade" % (APK_PATH) rc, stdout, stderr = module.run_command(cmd, check_rc=False) if rc != 0: module.fail_json(msg="failed to upgrade packages") @@ -140,14 +138,14 @@ def install_packages(module, names, state): names = " ".join(uninstalled) if upgrade: if module.check_mode: - cmd = "apk add --upgrade --simulate %s" % (names) + cmd = "%s add --upgrade --simulate %s" % (APK_PATH, names) else: - cmd = "apk add --upgrade %s" % (names) + cmd = "%s add --upgrade %s" % (APK_PATH, names) else: if module.check_mode: - cmd = "apk add --simulate %s" % (names) + cmd = "%s add --simulate %s" % (APK_PATH, names) else: - cmd = "apk add %s" % (names) + cmd = "%s add %s" % (APK_PATH, names) rc, stdout, stderr = module.run_command(cmd, check_rc=False) if rc != 0: module.fail_json(msg="failed to install %s" % (names)) @@ -162,9 +160,9 @@ def remove_packages(module, names): module.exit_json(changed=False, msg="package(s) already removed") names = " ".join(installed) if module.check_mode: - cmd = "apk del --purge --simulate %s" % (names) + cmd = "%s del --purge --simulate %s" % (APK_PATH, names) else: - cmd = "apk del --purge %s" % (names) + cmd = "%s del --purge %s" % (APK_PATH, names) rc, stdout, stderr = module.run_command(cmd, check_rc=False) if rc != 0: module.fail_json(msg="failed to remove %s package(s)" % (names)) @@ -185,8 +183,8 @@ def main(): supports_check_mode = True ) - if not os.path.exists(APK_PATH): - module.fail_json(msg="cannot find apk, looking for %s" % (APK_PATH)) + global APK_PATH + APK_PATH = module.get_bin_path('apk', required=True) p = module.params From 778e51c6b14f700181570034dd64e63713da9055 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 3 Aug 2015 16:28:02 -0400 Subject: [PATCH 0558/2522] updated version_added --- packaging/os/apk.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packaging/os/apk.py b/packaging/os/apk.py index f14d5593443..ec0e3908faf 100644 --- a/packaging/os/apk.py +++ b/packaging/os/apk.py @@ -24,6 +24,7 @@ short_description: Manages apk packages description: - Manages I(apk) packages for Alpine Linux. +version_added: "2.0" options: name: description: From 1b98210f5fae9a116d44eae90e269bd0b9b76165 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 3 Aug 2015 16:34:45 -0400 Subject: [PATCH 0559/2522] minor doc uptates --- packaging/os/slackpkg.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packaging/os/slackpkg.py b/packaging/os/slackpkg.py index 577b46d705b..fe7dacda9d2 100644 --- a/packaging/os/slackpkg.py +++ b/packaging/os/slackpkg.py @@ -38,8 +38,8 @@ state: description: - - state of the package - choices: [ 'present', 'absent', 'installed', 'removed', 'latest' ] + - state of the package, you can use "installed" as an alias for C(present) and removed as one for c(absent). + choices: [ 'present', 'absent', 'latest' ] required: false default: present @@ -52,7 +52,6 @@ author: Kim Nørgaard (@KimNorgaard) requirements: [ "Slackware" >= 12.2 ] -notes: [] ''' EXAMPLES = ''' @@ -61,6 +60,10 @@ # Remove packages foo and bar - slackpkg: name=foo,bar state=absent + +# Make sure that it is the most updated package +- slackpkg: name=foo state=latest + ''' @@ -165,9 +168,7 @@ def update_cache(module, slackpkg_path): def main(): module = AnsibleModule( argument_spec=dict( - state=dict(default="installed", choices=['installed', 'removed', - 'absent', 'present', - 'latest']), + state=dict(default="installed", choices=['installed', 'removed', 'absent', 'present', 'latest']), name=dict(aliases=["pkg"], required=True, type='list'), update_cache=dict(default=False, aliases=["update-cache"], type='bool'), From 2474a1f7cf7d8358ffada745e6a72319b2a8e4a9 Mon Sep 17 00:00:00 2001 From: Matt Baldwin Date: Mon, 3 Aug 2015 13:37:56 -0700 Subject: [PATCH 0560/2522] Rebuilt commit for Ansible PR. --- cloud/profitbricks/profitbricks_datacenter.py | 259 ++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 cloud/profitbricks/profitbricks_datacenter.py diff --git a/cloud/profitbricks/profitbricks_datacenter.py b/cloud/profitbricks/profitbricks_datacenter.py new file mode 100644 index 00000000000..cd0e38ee383 --- /dev/null +++ b/cloud/profitbricks/profitbricks_datacenter.py @@ -0,0 +1,259 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: profitbricks_datacenter +short_description: Create or destroy a ProfitBricks Virtual Datacenter. +description: + - This is a simple module that supports creating or removing vDCs. A vDC is required before you can create servers. This module has a dependency on profitbricks >= 1.0.0 +version_added: "2.0" +options: + name: + description: + - The name of the virtual datacenter. + required: true + description: + description: + - The description of the virtual datacenter. + required: false + location: + description: + - The datacenter location. + required: false + default: us/las + choices: [ "us/las", "us/lasdev", "de/fra", "de/fkb" ] + subscription_user: + description: + - The ProfitBricks username. Overrides the PB_SUBSCRIPTION_ID environement variable. + required: false + subscription_password: + description: + - THe ProfitBricks password. Overrides the PB_PASSWORD environement variable. + required: false + wait: + description: + - wait for the datacenter to be created before returning + required: false + default: "yes" + choices: [ "yes", "no" ] + wait_timeout: + description: + - how long before wait gives up, in seconds + default: 600 + state: + description: + - create or terminate datacenters + required: false + default: 'present' + choices: [ "present", "absent" ] + +requirements: [ "profitbricks" ] +author: Matt Baldwin (baldwin@stackpointcloud.com) +''' + +EXAMPLES = ''' + +# Create a Datacenter +- profitbricks_datacenter: + datacenter: Tardis One + wait_timeout: 500 + +# Destroy a Datacenter. This will remove all servers, volumes, and other objects in the datacenter. +- profitbricks_datacenter: + datacenter: Tardis One + wait_timeout: 500 + state: absent + +''' + +import re +import uuid +import time +import sys + +HAS_PB_SDK = True + +try: + from profitbricks.client import ProfitBricksService, Datacenter +except ImportError: + HAS_PB_SDK = False + +LOCATIONS = ['us/las', + 'de/fra', + 'de/fkb', + 'us/lasdev'] + +uuid_match = re.compile( + '[\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12}', re.I) + + +def _wait_for_completion(profitbricks, promise, wait_timeout, msg): + if not promise: return + wait_timeout = time.time() + wait_timeout + while wait_timeout > time.time(): + time.sleep(5) + operation_result = profitbricks.get_request( + request_id=promise['requestId'], + status=True) + + if operation_result['metadata']['status'] == "DONE": + return + elif operation_result['metadata']['status'] == "FAILED": + raise Exception( + 'Request failed to complete ' + msg + ' "' + str( + promise['requestId']) + '" to complete.') + + raise Exception( + 'Timed out waiting for async operation ' + msg + ' "' + str( + promise['requestId'] + ) + '" to complete.') + +def _remove_datacenter(module, profitbricks, datacenter): + try: + profitbricks.delete_datacenter(datacenter) + except Exception as e: + module.fail_json(msg="failed to remove the datacenter: %s" % str(e)) + +def create_datacenter(module, profitbricks): + """ + Creates a Datacenter + + This will create a new Datacenter in the specified location. + + module : AnsibleModule object + profitbricks: authenticated profitbricks object. + + Returns: + True if a new datacenter was created, false otherwise + """ + name = module.params.get('name') + location = module.params.get('location') + description = module.params.get('description') + wait = module.params.get('wait') + wait_timeout = int(module.params.get('wait_timeout')) + virtual_datacenters = [] + + + i = Datacenter( + name=name, + location=location, + description=description + ) + + try: + datacenter_response = profitbricks.create_datacenter(datacenter=i) + + if wait: + _wait_for_completion(profitbricks, datacenter_response, + wait_timeout, "_create_datacenter") + + results = { + 'datacenter_id': datacenter_response['id'] + } + + return results + + except Exception as e: + module.fail_json(msg="failed to create the new datacenter: %s" % str(e)) + +def remove_datacenter(module, profitbricks): + """ + Removes a Datacenter. + + This will remove a datacenter. + + module : AnsibleModule object + profitbricks: authenticated profitbricks object. + + Returns: + True if the datacenter was deleted, false otherwise + """ + name = module.params.get('name') + changed = False + + if(uuid_match.match(name)): + _remove_datacenter(module, profitbricks, name) + changed = True + else: + datacenters = profitbricks.list_datacenters() + + for d in datacenters['items']: + vdc = profitbricks.get_datacenter(d['id']) + + if name == vdc['properties']['name']: + name = d['id'] + _remove_datacenter(module, profitbricks, name) + changed = True + + return changed + +def main(): + module = AnsibleModule( + argument_spec=dict( + name=dict(), + description=dict(), + location=dict(choices=LOCATIONS, default='us/lasdev'), + subscription_user=dict(), + subscription_password=dict(), + wait=dict(type='bool', default=True), + wait_timeout=dict(default=600), + state=dict(default='present'), + ) + ) + if not HAS_PB_SDK: + module.fail_json(msg='profitbricks required for this module') + + if not module.params.get('subscription_user'): + module.fail_json(msg='subscription_user parameter is required') + if not module.params.get('subscription_password'): + module.fail_json(msg='subscription_password parameter is required') + + subscription_user = module.params.get('subscription_user') + subscription_password = module.params.get('subscription_password') + + profitbricks = ProfitBricksService( + username=subscription_user, + password=subscription_password) + + state = module.params.get('state') + + if state == 'absent': + if not module.params.get('name'): + module.fail_json(msg='name parameter is required deleting a virtual datacenter.') + + try: + (changed) = remove_datacenter(module, profitbricks) + module.exit_json( + changed=changed) + except Exception as e: + module.fail_json(msg='failed to set datacenter state: %s' % str(e)) + + elif state == 'present': + if not module.params.get('name'): + module.fail_json(msg='name parameter is required for a new datacenter') + if not module.params.get('location'): + module.fail_json(msg='location parameter is required for a new datacenter') + + try: + (datacenter_dict_array) = create_datacenter(module, profitbricks) + module.exit_json(**datacenter_dict_array) + except Exception as e: + module.fail_json(msg='failed to set datacenter state: %s' % str(e)) + +from ansible.module_utils.basic import * + +main() \ No newline at end of file From e3fdb834b4a156cf9d5a61868b04e254cc3c060d Mon Sep 17 00:00:00 2001 From: Matt Baldwin Date: Mon, 3 Aug 2015 14:34:08 -0700 Subject: [PATCH 0561/2522] Rebuild of previous PR. --- cloud/profitbricks/profitbricks_nic.py | 290 +++++++++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 cloud/profitbricks/profitbricks_nic.py diff --git a/cloud/profitbricks/profitbricks_nic.py b/cloud/profitbricks/profitbricks_nic.py new file mode 100644 index 00000000000..902d5266843 --- /dev/null +++ b/cloud/profitbricks/profitbricks_nic.py @@ -0,0 +1,290 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: profitbricks_nic +short_description: Create or Remove a NIC. +description: + - This module allows you to create or restore a volume snapshot. This module has a dependency on profitbricks >= 1.0.0 +version_added: "2.0" +options: + datacenter: + description: + - The datacenter in which to operate. + required: true + server: + description: + - The server name or ID. + required: true + name: + description: + - The name or ID of the NIC. This is only required on deletes, but not on create. + required: true + lan: + description: + - The LAN to place the NIC on. You can pass a LAN that doesn't exist and it will be created. Required on create. + required: true + subscription_user: + description: + - The ProfitBricks username. Overrides the PB_SUBSCRIPTION_ID environement variable. + required: false + subscription_password: + description: + - THe ProfitBricks password. Overrides the PB_PASSWORD environement variable. + required: false + wait: + description: + - wait for the operation to complete before returning + required: false + default: "yes" + choices: [ "yes", "no" ] + wait_timeout: + description: + - how long before wait gives up, in seconds + default: 600 + state: + description: + - Indicate desired state of the resource + required: false + default: 'present' + choices: ["present", "absent"] + +requirements: [ "profitbricks" ] +author: Matt Baldwin (baldwin@stackpointcloud.com) +''' + +EXAMPLES = ''' + +# Create a NIC +- profitbricks_nic: + datacenter: Tardis One + server: node002 + lan: 2 + wait_timeout: 500 + state: present + +# Remove a NIC +- profitbricks_nic: + datacenter: Tardis One + server: node002 + name: 7341c2454f + wait_timeout: 500 + state: absent + +''' + +import re +import uuid +import time + +HAS_PB_SDK = True + +try: + from profitbricks.client import ProfitBricksService, NIC +except ImportError: + HAS_PB_SDK = False + +uuid_match = re.compile( + '[\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12}', re.I) + + +def _wait_for_completion(profitbricks, promise, wait_timeout, msg): + if not promise: return + wait_timeout = time.time() + wait_timeout + while wait_timeout > time.time(): + time.sleep(5) + operation_result = profitbricks.get_request( + request_id=promise['requestId'], + status=True) + + if operation_result['metadata']['status'] == "DONE": + return + elif operation_result['metadata']['status'] == "FAILED": + raise Exception( + 'Request failed to complete ' + msg + ' "' + str( + promise['requestId']) + '" to complete.') + + raise Exception( + 'Timed out waiting for async operation ' + msg + ' "' + str( + promise['requestId'] + ) + '" to complete.') + +def create_nic(module, profitbricks): + """ + Creates a NIC. + + module : AnsibleModule object + profitbricks: authenticated profitbricks object. + + Returns: + True if the nic creates, false otherwise + """ + datacenter = module.params.get('datacenter') + server = module.params.get('server') + lan = module.params.get('lan') + name = module.params.get('name') + wait = module.params.get('wait') + wait_timeout = module.params.get('wait_timeout') + + # Locate UUID for Datacenter + if not (uuid_match.match(datacenter)): + datacenter_list = profitbricks.list_datacenters() + for d in datacenter_list['items']: + dc = profitbricks.get_datacenter(d['id']) + if datacenter == dc['properties']['name']: + datacenter = d['id'] + break + + # Locate UUID for Server + if not (uuid_match.match(server)): + server_list = profitbricks.list_servers(datacenter) + for s in server_list['items']: + if server == s['properties']['name']: + server = s['id'] + break + try: + n = NIC( + name=name, + lan=lan + ) + + nic_response = profitbricks.create_nic(datacenter, server, n) + + if wait: + _wait_for_completion(profitbricks, nic_response, + wait_timeout, "create_nic") + + return nic_response + + except Exception as e: + module.fail_json(msg="failed to create the NIC: %s" % str(e)) + +def delete_nic(module, profitbricks): + """ + Removes a NIC + + module : AnsibleModule object + profitbricks: authenticated profitbricks object. + + Returns: + True if the NIC was removed, false otherwise + """ + datacenter = module.params.get('datacenter') + server = module.params.get('server') + name = module.params.get('name') + + # Locate UUID for Datacenter + if not (uuid_match.match(datacenter)): + datacenter_list = profitbricks.list_datacenters() + for d in datacenter_list['items']: + dc = profitbricks.get_datacenter(d['id']) + if datacenter == dc['properties']['name']: + datacenter = d['id'] + break + + # Locate UUID for Server + server_found = False + if not (uuid_match.match(server)): + server_list = profitbricks.list_servers(datacenter) + for s in server_list['items']: + if server == s['properties']['name']: + server_found = True + server = s['id'] + break + + if not server_found: + return False + + # Locate UUID for NIC + nic_found = False + if not (uuid_match.match(name)): + nic_list = profitbricks.list_nics(datacenter, server) + for n in nic_list['items']: + if name == n['properties']['name']: + nic_found = True + name = n['id'] + break + + if not nic_found: + return False + + try: + nic_response = profitbricks.delete_nic(datacenter, server, name) + return nic_response + except Exception as e: + module.fail_json(msg="failed to remove the NIC: %s" % str(e)) + +def main(): + module = AnsibleModule( + argument_spec=dict( + datacenter=dict(), + server=dict(), + name=dict(default=str(uuid.uuid4()).replace('-','')[:10]), + lan=dict(), + subscription_user=dict(), + subscription_password=dict(), + wait=dict(type='bool', default=True), + wait_timeout=dict(type='int', default=600), + state=dict(default='present'), + ) + ) + + if not HAS_PB_SDK: + module.fail_json(msg='profitbricks required for this module') + + if not module.params.get('subscription_user'): + module.fail_json(msg='subscription_user parameter is required') + if not module.params.get('subscription_password'): + module.fail_json(msg='subscription_password parameter is required') + if not module.params.get('datacenter'): + module.fail_json(msg='datacenter parameter is required') + if not module.params.get('server'): + module.fail_json(msg='server parameter is required') + + + subscription_user = module.params.get('subscription_user') + subscription_password = module.params.get('subscription_password') + + profitbricks = ProfitBricksService( + username=subscription_user, + password=subscription_password) + + state = module.params.get('state') + + if state == 'absent': + if not module.params.get('name'): + module.fail_json(msg='name parameter is required') + + try: + (changed) = delete_nic(module, profitbricks) + module.exit_json(changed=changed) + except Exception as e: + module.fail_json(msg='failed to set nic state: %s' % str(e)) + + elif state == 'present': + if not module.params.get('lan'): + module.fail_json(msg='lan parameter is required') + + try: + (nic_dict) = create_nic(module, profitbricks) + module.exit_json(nics=nic_dict) + except Exception as e: + module.fail_json(msg='failed to set nic state: %s' % str(e)) + +from ansible.module_utils.basic import * + +main() \ No newline at end of file From ae116e95b22693acef7f4663e68771e066525fad Mon Sep 17 00:00:00 2001 From: Matt Baldwin Date: Mon, 3 Aug 2015 14:46:53 -0700 Subject: [PATCH 0562/2522] Rebuild of a previous PR. --- cloud/profitbricks/profitbricks_volume.py | 370 ++++++++++++++++++++++ 1 file changed, 370 insertions(+) create mode 100644 cloud/profitbricks/profitbricks_volume.py diff --git a/cloud/profitbricks/profitbricks_volume.py b/cloud/profitbricks/profitbricks_volume.py new file mode 100644 index 00000000000..89a69d5e61a --- /dev/null +++ b/cloud/profitbricks/profitbricks_volume.py @@ -0,0 +1,370 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: profitbricks_volume +short_description: Create or destroy a volume. +description: + - Allows you to create or remove a volume from a ProfitBricks datacenter. This module has a dependency on profitbricks >= 1.0.0 +version_added: "2.0" +options: + datacenter: + description: + - The datacenter in which to create the volumes. + required: true + name: + description: + - The name of the volumes. You can enumerate the names using auto_increment. + required: true + size: + description: + - The size of the volume. + required: false + default: 10 + bus: + description: + - The bus type. + required: false + default: VIRTIO + choices: [ "IDE", "VIRTIO"] + image: + description: + - The system image ID for the volume, e.g. a3eae284-a2fe-11e4-b187-5f1f641608c8. This can also be a snapshot image ID. + required: true + disk_type: + description: + - The disk type. Currently only HDD. + required: false + default: HDD + licence_type: + description: + - The licence type for the volume. This is used when the image is non-standard. + required: false + default: UNKNOWN + choices: ["LINUX", "WINDOWS", "UNKNOWN" , "OTHER"] + count: + description: + - The number of volumes you wish to create. + required: false + default: 1 + auto_increment: + description: + - Whether or not to increment a single number in the name for created virtual machines. + default: yes + choices: ["yes", "no"] + instance_ids: + description: + - list of instance ids, currently only used when state='absent' to remove instances. + required: false + subscription_user: + description: + - The ProfitBricks username. Overrides the PB_SUBSCRIPTION_ID environement variable. + required: false + subscription_password: + description: + - THe ProfitBricks password. Overrides the PB_PASSWORD environement variable. + required: false + wait: + description: + - wait for the datacenter to be created before returning + required: false + default: "yes" + choices: [ "yes", "no" ] + wait_timeout: + description: + - how long before wait gives up, in seconds + default: 600 + state: + description: + - create or terminate datacenters + required: false + default: 'present' + choices: ["present", "absent"] + +requirements: [ "profitbricks" ] +author: Matt Baldwin (baldwin@stackpointcloud.com) +''' + +EXAMPLES = ''' + +# Create Multiple Volumes + +- profitbricks_volume: + datacenter: Tardis One + name: vol%02d + count: 5 + auto_increment: yes + wait_timeout: 500 + state: present + +# Remove Volumes + +- profitbricks_volume: + datacenter: Tardis One + instance_ids: + - 'vol01' + - 'vol02' + wait_timeout: 500 + state: absent + +''' + +import re +import uuid +import time + +HAS_PB_SDK = True + +try: + from profitbricks.client import ProfitBricksService, Volume +except ImportError: + HAS_PB_SDK = False + +uuid_match = re.compile( + '[\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12}', re.I) + + +def _wait_for_completion(profitbricks, promise, wait_timeout, msg): + if not promise: return + wait_timeout = time.time() + wait_timeout + while wait_timeout > time.time(): + time.sleep(5) + operation_result = profitbricks.get_request( + request_id=promise['requestId'], + status=True) + + if operation_result['metadata']['status'] == "DONE": + return + elif operation_result['metadata']['status'] == "FAILED": + raise Exception( + 'Request failed to complete ' + msg + ' "' + str( + promise['requestId']) + '" to complete.') + + raise Exception( + 'Timed out waiting for async operation ' + msg + ' "' + str( + promise['requestId'] + ) + '" to complete.') + +def _create_volume(module, profitbricks, datacenter, name): + size = module.params.get('size') + bus = module.params.get('bus') + image = module.params.get('image') + disk_type = module.params.get('disk_type') + licence_type = module.params.get('licence_type') + wait_timeout = module.params.get('wait_timeout') + wait = module.params.get('wait') + + try: + v = Volume( + name=name, + size=size, + bus=bus, + image=image, + disk_type=disk_type, + licence_type=licence_type + ) + + volume_response = profitbricks.create_volume(datacenter, v) + + if wait: + _wait_for_completion(profitbricks, volume_response, + wait_timeout, "_create_volume") + + except Exception as e: + module.fail_json(msg="failed to create the volume: %s" % str(e)) + + return volume_response + +def _delete_volume(module, profitbricks, datacenter, volume): + try: + profitbricks.delete_volume(datacenter, volume) + except Exception as e: + module.fail_json(msg="failed to remove the volume: %s" % str(e)) + +def create_volume(module, profitbricks): + """ + Creates a volume. + + This will create a volume in a datacenter. + + module : AnsibleModule object + profitbricks: authenticated profitbricks object. + + Returns: + True if the volume was created, false otherwise + """ + datacenter = module.params.get('datacenter') + name = module.params.get('name') + auto_increment = module.params.get('auto_increment') + count = module.params.get('count') + + datacenter_found = False + failed = True + volumes = [] + + # Locate UUID for Datacenter + if not (uuid_match.match(datacenter)): + datacenter_list = profitbricks.list_datacenters() + for d in datacenter_list['items']: + dc = profitbricks.get_datacenter(d['id']) + if datacenter == dc['properties']['name']: + datacenter = d['id'] + datacenter_found = True + break + + if not datacenter_found: + module.fail_json(msg='datacenter could not be found.') + + if auto_increment: + numbers = set() + count_offset = 1 + + try: + name % 0 + except TypeError, e: + if e.message.startswith('not all'): + name = '%s%%d' % name + else: + module.fail_json(msg=e.message) + + number_range = xrange(count_offset,count_offset + count + len(numbers)) + available_numbers = list(set(number_range).difference(numbers)) + names = [] + numbers_to_use = available_numbers[:count] + for number in numbers_to_use: + names.append(name % number) + else: + names = [name] * count + + for name in names: + create_response = _create_volume(module, profitbricks, str(datacenter), name) + volumes.append(create_response) + failed = False + + results = { + 'failed': failed, + 'volumes': volumes, + 'action': 'create', + 'instance_ids': { + 'instances': [i['id'] for i in volumes], + } + } + + return results + +def delete_volume(module, profitbricks): + """ + Removes a volume. + + This will create a volume in a datacenter. + + module : AnsibleModule object + profitbricks: authenticated profitbricks object. + + Returns: + True if the volume was removed, false otherwise + """ + if not isinstance(module.params.get('instance_ids'), list) or len(module.params.get('instance_ids')) < 1: + module.fail_json(msg='instance_ids should be a list of virtual machine ids or names, aborting') + + datacenter = module.params.get('datacenter') + changed = False + instance_ids = module.params.get('instance_ids') + + # Locate UUID for Datacenter + if not (uuid_match.match(datacenter)): + datacenter_list = profitbricks.list_datacenters() + for d in datacenter_list['items']: + dc = profitbricks.get_datacenter(d['id']) + if datacenter == dc['properties']['name']: + datacenter = d['id'] + break + + for n in instance_ids: + if(uuid_match.match(n)): + _delete_volume(module, profitbricks, datacenter, volume) + changed = True + else: + volumes = profitbricks.list_volumes(datacenter) + for v in volumes['items']: + if n == v['properties']['name']: + volume_id = v['id'] + _delete_volume(module, profitbricks, datacenter, volume_id) + changed = True + + return changed + +def main(): + module = AnsibleModule( + argument_spec=dict( + datacenter=dict(), + name=dict(), + size=dict(default=10), + bus=dict(default='VIRTIO'), + image=dict(), + disk_type=dict(default='HDD'), + licence_type=dict(default='UNKNOWN'), + count=dict(default=1), + auto_increment=dict(type='bool', default=True), + instance_ids=dict(), + subscription_user=dict(), + subscription_password=dict(), + wait=dict(type='bool', default=True), + wait_timeout=dict(type='int', default=600), + state=dict(default='present'), + ) + ) + + if not module.params.get('subscription_user'): + module.fail_json(msg='subscription_user parameter is required') + if not module.params.get('subscription_password'): + module.fail_json(msg='subscription_password parameter is required') + + subscription_user = module.params.get('subscription_user') + subscription_password = module.params.get('subscription_password') + + profitbricks = ProfitBricksService( + username=subscription_user, + password=subscription_password) + + state = module.params.get('state') + + if state == 'absent': + if not module.params.get('datacenter'): + module.fail_json(msg='datacenter parameter is required for running or stopping machines.') + + try: + (changed) = delete_volume(module, profitbricks) + module.exit_json(changed=changed) + except Exception as e: + module.fail_json(msg='failed to set volume state: %s' % str(e)) + + elif state == 'present': + if not module.params.get('datacenter'): + module.fail_json(msg='datacenter parameter is required for new instance') + if not module.params.get('name'): + module.fail_json(msg='name parameter is required for new instance') + + try: + (failed, volume_dict_array) = create_volume(module, profitbricks) + module.exit_json(failed=failed, volumes=volume_dict_array) + except Exception as e: + module.fail_json(msg='failed to set volume state: %s' % str(e)) + +from ansible.module_utils.basic import * + +main() \ No newline at end of file From 8fda15fef5afa290ac822c17522fb8862f2ae722 Mon Sep 17 00:00:00 2001 From: Matt Baldwin Date: Mon, 3 Aug 2015 15:00:04 -0700 Subject: [PATCH 0563/2522] Rebuild of ealier PR. --- .../profitbricks_volume_attachments.py | 262 ++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 cloud/profitbricks/profitbricks_volume_attachments.py diff --git a/cloud/profitbricks/profitbricks_volume_attachments.py b/cloud/profitbricks/profitbricks_volume_attachments.py new file mode 100644 index 00000000000..fe87594fddc --- /dev/null +++ b/cloud/profitbricks/profitbricks_volume_attachments.py @@ -0,0 +1,262 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: profitbricks_volume_attachments +short_description: Attach or detach a volume. +description: + - Allows you to attach or detach a volume from a ProfitBricks server. This module has a dependency on profitbricks >= 1.0.0 +version_added: "2.0" +options: + datacenter: + description: + - The datacenter in which to operate. + required: true + server: + description: + - The name of the server you wish to detach or attach the volume. + required: true + volume: + description: + - The volume name or ID. + required: true + subscription_user: + description: + - The ProfitBricks username. Overrides the PB_SUBSCRIPTION_ID environement variable. + required: false + subscription_password: + description: + - THe ProfitBricks password. Overrides the PB_PASSWORD environement variable. + required: false + wait: + description: + - wait for the operation to complete before returning + required: false + default: "yes" + choices: [ "yes", "no" ] + wait_timeout: + description: + - how long before wait gives up, in seconds + default: 600 + state: + description: + - Indicate desired state of the resource + required: false + default: 'present' + choices: ["present", "absent"] + +requirements: [ "profitbricks" ] +author: Matt Baldwin (baldwin@stackpointcloud.com) +''' + +EXAMPLES = ''' + +# Attach a Volume + +- profitbricks_volume_attachments: + datacenter: Tardis One + server: node002 + volume: vol01 + wait_timeout: 500 + state: present + +# Detach a Volume + +- profitbricks_volume_attachments: + datacenter: Tardis One + server: node002 + volume: vol01 + wait_timeout: 500 + state: absent + +''' + +import re +import uuid +import time + +HAS_PB_SDK = True + +try: + from profitbricks.client import ProfitBricksService, Volume +except ImportError: + HAS_PB_SDK = False + +uuid_match = re.compile( + '[\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12}', re.I) + + +def _wait_for_completion(profitbricks, promise, wait_timeout, msg): + if not promise: return + wait_timeout = time.time() + wait_timeout + while wait_timeout > time.time(): + time.sleep(5) + operation_result = profitbricks.get_request( + request_id=promise['requestId'], + status=True) + + if operation_result['metadata']['status'] == "DONE": + return + elif operation_result['metadata']['status'] == "FAILED": + raise Exception( + 'Request failed to complete ' + msg + ' "' + str( + promise['requestId']) + '" to complete.') + + raise Exception( + 'Timed out waiting for async operation ' + msg + ' "' + str( + promise['requestId'] + ) + '" to complete.') + +def attach_volume(module, profitbricks): + """ + Attaches a volume. + + This will attach a volume to the server. + + module : AnsibleModule object + profitbricks: authenticated profitbricks object. + + Returns: + True if the volume was attached, false otherwise + """ + datacenter = module.params.get('datacenter') + server = module.params.get('server') + volume = module.params.get('volume') + + # Locate UUID for Datacenter + if not (uuid_match.match(datacenter)): + datacenter_list = profitbricks.list_datacenters() + for d in datacenter_list['items']: + dc = profitbricks.get_datacenter(d['id']) + if datacenter == dc['properties']['name']: + datacenter = d['id'] + break + + # Locate UUID for Server + if not (uuid_match.match(server)): + server_list = profitbricks.list_servers(datacenter) + for s in server_list['items']: + if server == s['properties']['name']: + server= s['id'] + break + + # Locate UUID for Volume + if not (uuid_match.match(volume)): + volume_list = profitbricks.list_volumes(datacenter) + for v in volume_list['items']: + if volume == v['properties']['name']: + volume = v['id'] + break + + return profitbricks.attach_volume(datacenter, server, volume) + +def detach_volume(module, profitbricks): + """ + Detaches a volume. + + This will remove a volume from the server. + + module : AnsibleModule object + profitbricks: authenticated profitbricks object. + + Returns: + True if the volume was detached, false otherwise + """ + datacenter = module.params.get('datacenter') + server = module.params.get('server') + volume = module.params.get('volume') + + # Locate UUID for Datacenter + if not (uuid_match.match(datacenter)): + datacenter_list = profitbricks.list_datacenters() + for d in datacenter_list['items']: + dc = profitbricks.get_datacenter(d['id']) + if datacenter == dc['properties']['name']: + datacenter = d['id'] + break + + # Locate UUID for Server + if not (uuid_match.match(server)): + server_list = profitbricks.list_servers(datacenter) + for s in server_list['items']: + if server == s['properties']['name']: + server= s['id'] + break + + # Locate UUID for Volume + if not (uuid_match.match(volume)): + volume_list = profitbricks.list_volumes(datacenter) + for v in volume_list['items']: + if volume == v['properties']['name']: + volume = v['id'] + break + + return profitbricks.detach_volume(datacenter, server, volume) + +def main(): + module = AnsibleModule( + argument_spec=dict( + datacenter=dict(), + server=dict(), + volume=dict(), + subscription_user=dict(), + subscription_password=dict(), + wait=dict(type='bool', default=True), + wait_timeout=dict(type='int', default=600), + state=dict(default='present'), + ) + ) + + if not HAS_PB_SDK: + module.fail_json(msg='profitbricks required for this module') + + if not module.params.get('subscription_user'): + module.fail_json(msg='subscription_user parameter is required') + if not module.params.get('subscription_password'): + module.fail_json(msg='subscription_password parameter is required') + if not module.params.get('datacenter'): + module.fail_json(msg='datacenter parameter is required') + if not module.params.get('server'): + module.fail_json(msg='server parameter is required') + if not module.params.get('volume'): + module.fail_json(msg='volume parameter is required') + + subscription_user = module.params.get('subscription_user') + subscription_password = module.params.get('subscription_password') + + profitbricks = ProfitBricksService( + username=subscription_user, + password=subscription_password) + + state = module.params.get('state') + + if state == 'absent': + try: + (changed) = detach_volume(module, profitbricks) + module.exit_json(changed=changed) + except Exception as e: + module.fail_json(msg='failed to set volume_attach state: %s' % str(e)) + elif state == 'present': + try: + attach_volume(module, profitbricks) + module.exit_json() + except Exception as e: + module.fail_json(msg='failed to set volume_attach state: %s' % str(e)) + +from ansible.module_utils.basic import * + +main() \ No newline at end of file From 757b952be45ec9bd40909e381a191bf9011cffc0 Mon Sep 17 00:00:00 2001 From: Jonathan Davila Date: Tue, 4 Aug 2015 07:52:51 -0500 Subject: [PATCH 0564/2522] Doc string fix for vsphere_copy --- cloud/vmware/vsphere_copy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cloud/vmware/vsphere_copy.py b/cloud/vmware/vsphere_copy.py index 4364e8b5197..18799211522 100644 --- a/cloud/vmware/vsphere_copy.py +++ b/cloud/vmware/vsphere_copy.py @@ -22,7 +22,8 @@ --- module: vsphere_copy short_description: Copy a file to a vCenter datastore -description: Upload files to a vCenter datastore +description: + - Upload files to a vCenter datastore version_added: 2.0 author: Dag Wieers (@dagwieers) options: From f74d8cb1e634a1edd1ba9c6ed123b792ce0d2c12 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Wed, 29 Jul 2015 15:27:59 +0200 Subject: [PATCH 0565/2522] cloudstack: new module cs_domain --- cloud/cloudstack/cs_domain.py | 293 ++++++++++++++++++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 cloud/cloudstack/cs_domain.py diff --git a/cloud/cloudstack/cs_domain.py b/cloud/cloudstack/cs_domain.py new file mode 100644 index 00000000000..0860aa3c49e --- /dev/null +++ b/cloud/cloudstack/cs_domain.py @@ -0,0 +1,293 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2015, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: cs_domain +short_description: Manages domains on Apache CloudStack based clouds. +description: + - Create, update and remove domains. +version_added: '2.0' +author: "René Moser (@resmo)" +options: + path: + description: + - Path of the domain. + - Prefix C(ROOT/) or C(/ROOT/) in path is optional. + required: true + network_domain: + description: + - Network domain for networks in the domain. + required: false + default: null + cleanup: + description: + - Clean up all domain resources like child domains and accounts. + - Considered on C(state=absent). + required: false + default: false + state: + description: + - State of the domain. + required: false + default: 'present' + choices: [ 'present', 'absent' ] + poll_async: + description: + - Poll async jobs until job has finished. + required: false + default: true +extends_documentation_fragment: cloudstack +''' + +EXAMPLES = ''' +# Create a domain +local_action: + module: cs_domain + path: ROOT/customers + network_domain: customers.example.com + +# Create another subdomain +local_action: + module: cs_domain + path: ROOT/customers/xy + network_domain: xy.customers.example.com + +# Remove a domain +local_action: + module: cs_domain + path: ROOT/customers/xy + state: absent +''' + +RETURN = ''' +--- +id: + description: ID of the domain. + returned: success + type: string + sample: 87b1e0ce-4e01-11e4-bb66-0050569e64b8 +name: + description: Name of the domain. + returned: success + type: string + sample: customers +path: + description: Domain path. + returned: success + type: string + sample: /ROOT/customers +parent_domain: + description: Parent domain of the domain. + returned: success + type: string + sample: ROOT +network_domain: + description: Network domain of the domain. + returned: success + type: string + sample: example.local + +''' + +try: + from cs import CloudStack, CloudStackException, read_config + has_lib_cs = True +except ImportError: + has_lib_cs = False + +# import cloudstack common +from ansible.module_utils.cloudstack import * + + +class AnsibleCloudStackDomain(AnsibleCloudStack): + + def __init__(self, module): + AnsibleCloudStack.__init__(self, module) + self.domain = None + + + def _get_domain_internal(self, path=None): + if not path: + path = self.module.params.get('path') + + if path.endswith('/'): + self.module.fail_json(msg="Path '%s' must not end with /" % path) + + path = path.lower() + + if path.startswith('/') and not path.startswith('/root/'): + path = "root" + path + elif not path.startswith('root/'): + path = "root/" + path + + args = {} + args['listall'] = True + + domains = self.cs.listDomains(**args) + if domains: + for d in domains['domain']: + if path == d['path'].lower(): + return d + return None + + + def get_name(self): + # last part of the path is the name + name = self.module.params.get('path').split('/')[-1:] + return name + + + def get_domain(self, key=None): + if not self.domain: + self.domain = self._get_domain_internal() + return self._get_by_key(key, self.domain) + + + def get_parent_domain(self, key=None): + path = self.module.params.get('path') + # cut off last /* + path = '/'.join(path.split('/')[:-1]) + if not path: + return None + parent_domain = self._get_domain_internal(path=path) + if not parent_domain: + self.module.fail_json(msg="Parent domain path %s does not exist" % path) + return self._get_by_key(key, parent_domain) + + + def present_domain(self): + domain = self.get_domain() + if not domain: + domain = self.create_domain(domain) + else: + domain = self.update_domain(domain) + return domain + + + def create_domain(self, domain): + self.result['changed'] = True + + args = {} + args['name'] = self.get_name() + args['parentdomainid'] = self.get_parent_domain(key='id') + args['networkdomain'] = self.module.params.get('network_domain') + + if not self.module.check_mode: + res = self.cs.createDomain(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + domain = res['domain'] + return domain + + + def update_domain(self, domain): + args = {} + args['id'] = domain['id'] + args['networkdomain'] = self.module.params.get('network_domain') + + if self._has_changed(args, domain): + self.result['changed'] = True + if not self.module.check_mode: + res = self.cs.updateDomain(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + domain = res['domain'] + return domain + + + def absent_domain(self): + domain = self.get_domain() + if domain: + self.result['changed'] = True + + if not self.module.check_mode: + args = {} + args['id'] = domain['id'] + args['cleanup'] = self.module.params.get('cleanup') + res = self.cs.deleteDomain(**args) + + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + + poll_async = self.module.params.get('poll_async') + if poll_async: + res = self._poll_job(res, 'domain') + return domain + + + def get_result(self, domain): + if domain: + if 'id' in domain: + self.result['id'] = domain['id'] + if 'name' in domain: + self.result['name'] = domain['name'] + if 'path' in domain: + self.result['path'] = domain['path'] + if 'parentdomainname' in domain: + self.result['parent_domain'] = domain['parentdomainname'] + if 'networkdomain' in domain: + self.result['network_domain'] = domain['networkdomain'] + return self.result + + +def main(): + module = AnsibleModule( + argument_spec = dict( + path = dict(required=True), + state = dict(choices=['present', 'absent'], default='present'), + network_domain = dict(default=None), + cleanup = dict(choices=BOOLEANS, default=False), + poll_async = dict(choices=BOOLEANS, default=True), + api_key = dict(default=None), + api_secret = dict(default=None, no_log=True), + api_url = dict(default=None), + api_http_method = dict(choices=['get', 'post'], default='get'), + api_timeout = dict(type='int', default=10), + ), + required_together = ( + ['api_key', 'api_secret', 'api_url'], + ), + supports_check_mode=True + ) + + if not has_lib_cs: + module.fail_json(msg="python library cs required: pip install cs") + + try: + acs_dom = AnsibleCloudStackDomain(module) + + state = module.params.get('state') + if state in ['absent']: + domain = acs_dom.absent_domain() + else: + domain = acs_dom.present_domain() + + result = acs_dom.get_result(domain) + + except CloudStackException, e: + module.fail_json(msg='CloudStackException: %s' % str(e)) + + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From a14f6d4db88f3408a2e976a12bd60b3f971f1001 Mon Sep 17 00:00:00 2001 From: Trey Perry Date: Tue, 4 Aug 2015 12:55:19 -0500 Subject: [PATCH 0566/2522] Contributing a ZooKeeper module (requires Kazoo) --- coordination/znode | 210 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 coordination/znode diff --git a/coordination/znode b/coordination/znode new file mode 100644 index 00000000000..4e9dec4ce49 --- /dev/null +++ b/coordination/znode @@ -0,0 +1,210 @@ +#!/usr/bin/python +DOCUMENTATION = """ +--- +module: znode +short_description: Create, delete, retrieve, and update znodes using ZooKeeper. +options: + hosts: + description: + - A list of ZooKeeper servers (format '[server]:[port]'). + default: localhost:2181 + required: true + path: + description: + - The path of the znode. + required: true + value: + description: + - The value assigned to the znode. + default: None + required: false + op: + description: + - An operation to perform. Mutually exclusive with state. + default: None + required: false + state: + description: + - The state to enforce. Mutually exclusive with op. + default: None + required: false + timeout: + description: + - The amount of time to wait for a node to appear. + default: 300 + required: false +requirements: + - kazoo >= 2.1 +--- +""" + +EXAMPLES = """ +# Creating or updating a znode with a given value +- action: znode hosts=localhost:2181 name=/mypath value=myvalue state=present + +# Getting the value and stat structure for a znode +- action: znode hosts=localhost:2181 name=/mypath op=get + +# Listing a particular znode's children +- action: znode hosts=localhost:2181 name=/zookeeper op=list + +# Waiting 20 seconds for a znode to appear at path /mypath +- action: znode hosts=localhost:2181 name=/mypath op=wait timeout=20 + +# Deleting a znode at path /mypath +- action: znode hosts=localhost:2181 name=/mypath state=absent +""" + +try: + from kazoo.client import KazooClient + from kazoo.exceptions import NoNodeError, ZookeeperError + from kazoo.handlers.threading import KazooTimeoutError + KAZOO_INSTALLED = True +except ImportError: + KAZOO_INSTALLED = False + + +def main(): + module = AnsibleModule( + argument_spec=dict( + hosts=dict(required=True, type='str'), + name=dict(required=True, type='str'), + value=dict(required=False, default=None, type='str'), + op=dict(required=False, default=None, choices=['get', 'wait', 'list']), + state=dict(choices=['present', 'absent']), + timeout=dict(required=False, default=300, type='int') + ), + supports_check_mode=False + ) + + if not KAZOO_INSTALLED: + module.fail_json(msg='kazoo >= 2.1 is required to use this module. Use pip to install it.') + + check = check_params(module.params) + if not check['success']: + module.fail_json(msg=check['msg']) + + zoo = KazooCommandProxy(module) + try: + zoo.start() + except KazooTimeoutError: + module.fail_json(msg='The connection to the ZooKeeper ensemble timed out.') + + command_dict = { + 'op': { + 'get': zoo.get, + 'list': zoo.list, + 'wait': zoo.wait + }, + 'state': { + 'present': zoo.present, + 'absent': zoo.absent + } + } + + command_type = 'op' if 'op' in module.params and module.params['op'] is not None else 'state' + method = module.params[command_type] + result, result_dict = command_dict[command_type][method]() + zoo.shutdown() + + if result: + module.exit_json(**result_dict) + else: + module.fail_json(**result_dict) + + +def check_params(params): + if not params['state'] and not params['op']: + return {'success': False, 'msg': 'Please define an operation (op) or a state.'} + + if params['state'] and params['op']: + return {'success': False, 'msg': 'Please choose an operation (op) or a state, but not both.'} + + return {'success': True} + + +class KazooCommandProxy(): + def __init__(self, module): + self.module = module + self.zk = KazooClient(module.params['hosts']) + + def absent(self): + return self._absent(self.module.params['name']) + + def exists(self, znode): + return self.zk.exists(znode) + + def list(self): + children = self.zk.get_children(self.module.params['name']) + return True, {'count': len(children), 'items': children, 'msg': 'Retrieved znodes in path.', + 'znode': self.module.params['name']} + + def present(self): + return self._present(self.module.params['name'], self.module.params['value']) + + def get(self): + return self._get(self.module.params['name']) + + def shutdown(self): + self.zk.stop() + self.zk.close() + + def start(self): + self.zk.start() + + def wait(self): + return self._wait(self.module.params['name'], self.module.params['timeout']) + + def _absent(self, znode): + if self.exists(znode): + self.zk.delete(znode) + return True, {'changed': True, 'msg': 'The znode was deleted.'} + else: + return True, {'changed': False, 'msg': 'The znode does not exist.'} + + def _get(self, path): + if self.exists(path): + value, zstat = self.zk.get(path) + stat_dict = {} + for i in dir(zstat): + if not i.startswith('_'): + attr = getattr(zstat, i) + if type(attr) in (int, str): + stat_dict[i] = attr + result = True, {'msg': 'The node was retrieved.', 'znode': path, 'value': value, + 'stat': stat_dict} + else: + result = False, {'msg': 'The requested node does not exist.'} + + return result + + def _present(self, path, value): + if self.exists(path): + (current_value, zstat) = self.zk.get(path) + if value != current_value: + self.zk.set(path, value) + return True, {'changed': True, 'msg': 'Updated the znode value.', 'znode': path, + 'value': value} + else: + return True, {'changed': False, 'msg': 'No changes were necessary.', 'znode': path, 'value': value} + else: + self.zk.create(path, value, makepath=True) + return True, {'changed': True, 'msg': 'Created a new znode.', 'znode': path, 'value': value} + + def _wait(self, path, timeout, interval=5): + lim = time.time() + timeout + + while time.time() < lim: + if self.exists(path): + return True, {'msg': 'The node appeared before the configured timeout.', + 'znode': path, 'timeout': timeout} + else: + time.sleep(interval) + + return False, {'msg': 'The node did not appear before the operation timed out.', 'timeout': timeout, + 'znode': path} + +from ansible.module_utils.basic import * + +main() + From 7c1bcbc524c2fbd799f0808787aa4a0bd1aad0f0 Mon Sep 17 00:00:00 2001 From: Trey Perry Date: Tue, 4 Aug 2015 15:42:59 -0500 Subject: [PATCH 0567/2522] Adding license and removing errant default on the hosts option --- coordination/znode | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/coordination/znode b/coordination/znode index 4e9dec4ce49..5d6bff16f06 100644 --- a/coordination/znode +++ b/coordination/znode @@ -1,4 +1,21 @@ #!/usr/bin/python +# Copyright 2015 WP Engine, Inc. All rights reserved. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + DOCUMENTATION = """ --- module: znode @@ -7,7 +24,6 @@ options: hosts: description: - A list of ZooKeeper servers (format '[server]:[port]'). - default: localhost:2181 required: true path: description: @@ -35,6 +51,7 @@ options: required: false requirements: - kazoo >= 2.1 +author: "Trey Perry (@treyperry)" --- """ From dce3d4054e950980207c5705706320f2184eba93 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 5 Aug 2015 09:27:03 -0700 Subject: [PATCH 0568/2522] Return errno and http status as requested by @bcoca --- cloud/vmware/vsphere_copy.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cloud/vmware/vsphere_copy.py b/cloud/vmware/vsphere_copy.py index 446f62bd5c5..100a8c8f3f6 100644 --- a/cloud/vmware/vsphere_copy.py +++ b/cloud/vmware/vsphere_copy.py @@ -141,17 +141,17 @@ def main(): except socket.error, e: if isinstance(e.args, tuple) and e[0] == errno.ECONNRESET: # VSphere resets connection if the file is in use and cannot be replaced - module.fail_json(msg='Failed to upload, image probably in use', status=e[0], reason=str(e), url=url) + module.fail_json(msg='Failed to upload, image probably in use', status=None, errno=e[0], reason=str(e), url=url) else: - module.fail_json(msg=str(e), status=e[0], reason=str(e), url=url) + module.fail_json(msg=str(e), status=None, errno=e[0], reason=str(e), url=url) except Exception, e: - status = -1 + error_code = -1 try: if isinstance(e[0], int): - status = e[0] + error_code = e[0] except KeyError: pass - module.fail_json(msg=str(e), status=status, reason=str(e), url=url) + module.fail_json(msg=str(e), status=None, errno=error_code, reason=str(e), url=url) status = r.getcode() if 200 <= status < 300: @@ -163,7 +163,7 @@ def main(): else: chunked = 0 - module.fail_json(msg='Failed to upload', status=status, reason=r.msg, length=length, headers=dict(r.headers), chunked=chunked, url=url) + module.fail_json(msg='Failed to upload', errno=None, status=status, reason=r.msg, length=length, headers=dict(r.headers), chunked=chunked, url=url) # Import module snippets from ansible.module_utils.basic import * From 210ee3febcf4d65c0c914d63b2005bc0373fb7dd Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Wed, 5 Aug 2015 17:35:36 -0400 Subject: [PATCH 0569/2522] minor doc fix --- packaging/os/slackpkg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/os/slackpkg.py b/packaging/os/slackpkg.py index fe7dacda9d2..674de538efe 100644 --- a/packaging/os/slackpkg.py +++ b/packaging/os/slackpkg.py @@ -51,7 +51,7 @@ choices: [ true, false ] author: Kim Nørgaard (@KimNorgaard) -requirements: [ "Slackware" >= 12.2 ] +requirements: [ "Slackware >= 12.2" ] ''' EXAMPLES = ''' From e75d1fcf095bd2f56069e9a76e881f2e6cbbb3f7 Mon Sep 17 00:00:00 2001 From: Michael Schuett Date: Fri, 7 Aug 2015 12:47:52 -0400 Subject: [PATCH 0570/2522] Update REVIEWERS.md Added myself.... and updated the formatting so you have less eye bleeding when looking at it. --- REVIEWERS.md | 297 ++++++++++++++++++++++++++------------------------- 1 file changed, 149 insertions(+), 148 deletions(-) diff --git a/REVIEWERS.md b/REVIEWERS.md index 5ae08b59b02..8f67835efd4 100644 --- a/REVIEWERS.md +++ b/REVIEWERS.md @@ -6,154 +6,155 @@ Two +1 votes by any of these module reviewers on a new module pull request will Active ====== -"Adam Garside (@fabulops)" -"Adam Keech (@smadam813)" -"Adam Miller (@maxamillion)" -"Alex Coomans (@drcapulet)" -"Alexander Bulimov (@abulimov)" -"Alexander Saltanov (@sashka)" -"Alexander Winkler (@dermute)" -"Andrew de Quincey (@adq)" -"André Paramés (@andreparames)" -"Andy Hill (@andyhky)" -"Artūras `arturaz` Šlajus (@arturaz)" -"Augustus Kling (@AugustusKling)" -"BOURDEL Paul (@pb8226)" -"Balazs Pocze (@banyek)" -"Ben Whaley (@bwhaley)" -"Benno Joy (@bennojoy)" -"Bernhard Weitzhofer (@b6d)" -"Boyd Adamson (@brontitall)" -"Brad Olson (@bradobro)" -"Brian Coca (@bcoca)" -"Brice Burgess (@briceburg)" -"Bruce Pennypacker (@bpennypacker)" -"Carson Gee (@carsongee)" -"Chris Church (@cchurch)" -"Chris Hoffman (@chrishoffman)" -"Chris Long (@alcamie101)" -"Chris Schmidt (@chrisisbeef)" -"Christian Berendt (@berendt)" -"Christopher H. Laco (@claco)" -"Cristian van Ee (@DJMuggs)" -"Dag Wieers (@dagwieers)" -"Dane Summers (@dsummersl)" -"Daniel Jaouen (@danieljaouen)" -"Daniel Schep (@dschep)" -"Dariusz Owczarek (@dareko)" -"Darryl Stoflet (@dstoflet)" -"David CHANIAL (@davixx)" -"David Stygstra (@stygstra)" -"Derek Carter (@goozbach)" -"Dimitrios Tydeas Mengidis (@dmtrs)" -"Doug Luce (@dougluce)" -"Dylan Martin (@pileofrogs)" -"Elliott Foster (@elliotttf)" -"Eric Johnson (@erjohnso)" -"Evan Duffield (@scicoin-project)" -"Evan Kaufman (@EvanK)" -"Evgenii Terechkov (@evgkrsk)" -"Franck Cuny (@franckcuny)" -"Gareth Rushgrove (@garethr)" -"Hagai Kariti (@hkariti)" -"Hector Acosta (@hacosta)" -"Hiroaki Nakamura (@hnakamur)" -"Ivan Vanderbyl (@ivanvanderbyl)" -"Jakub Jirutka (@jirutka)" -"James Cammarata (@jimi-c)" -"James Laska (@jlaska)" -"James S. Martin (@jsmartin)" -"Jan-Piet Mens (@jpmens)" -"Jayson Vantuyl (@jvantuyl)" -"Jens Depuydt (@jensdepuydt)" -"Jeroen Hoekx (@jhoekx)" -"Jesse Keating (@j2sol)" -"Jim Dalton (@jsdalton)" -"Jim Richardson (@weaselkeeper)" -"Jimmy Tang (@jcftang)" -"Johan Wiren (@johanwiren)" -"John Dewey (@retr0h)" -"John Jarvis (@jarv)" -"John Whitbeck (@jwhitbeck)" -"Jon Hawkesworth (@jhawkesworth)" -"Jonas Pfenniger (@zimbatm)" -"Jonathan I. Davila (@defionscode)" -"Joseph Callen (@jcpowermac)" -"Kevin Carter (@cloudnull)" -"Lester Wade (@lwade)" -"Lorin Hochstein (@lorin)" -"Manuel Sousa (@manuel-sousa)" -"Mark Theunissen (@marktheunissen)" -"Matt Coddington (@mcodd)" -"Matt Hite (@mhite)" -"Matt Makai (@makaimc)" -"Matt Martz (@sivel)" -"Matt Wright (@mattupstate)" -"Matthew Vernon (@mcv21)" -"Matthew Williams (@mgwilliams)" -"Matthias Vogelgesang (@matze)" -"Max Riveiro (@kavu)" -"Michael Gregson (@mgregson)" -"Michael J. Schultz (@mjschultz)" -"Michael Warkentin (@mwarkentin)" -"Mischa Peters (@mischapeters)" -"Monty Taylor (@emonty)" -"Nandor Sivok (@dominis)" -"Nate Coraor (@natefoo)" -"Nate Kingsley (@nate-kingsley)" -"Nick Harring (@NickatEpic)" -"Patrick Callahan (@dirtyharrycallahan)" -"Patrick Ogenstad (@ogenstad)" -"Patrick Pelletier (@skinp)" -"Patrik Lundin (@eest)" -"Paul Durivage (@angstwad)" -"Pavel Antonov (@softzilla)" -"Pepe Barbe (@elventear)" -"Peter Mounce (@petemounce)" -"Peter Oliver (@mavit)" -"Peter Sprygada (@privateip)" -"Peter Tan (@tanpeter)" -"Philippe Makowski (@pmakowski)" -"Phillip Gentry, CX Inc (@pcgentry)" -"Quentin Stafford-Fraser (@quentinsf)" -"Ramon de la Fuente (@ramondelafuente)" -"Raul Melo (@melodous)" -"Ravi Bhure (@ravibhure)" -"René Moser (@resmo)" -"Richard Hoop (@rhoop)" -"Richard Isaacson (@risaacson)" -"Rick Mendes (@rickmendes)" -"Romeo Theriault (@romeotheriault)" -"Scott Anderson (@tastychutney)" -"Sebastian Kornehl (@skornehl)" -"Serge van Ginderachter (@srvg)" -"Sergei Antipov (@UnderGreen)" -"Seth Edwards (@sedward)" -"Silviu Dicu (@silviud)" -"Simon JAILLET (@jails)" -"Stephen Fromm (@sfromm)" -"Steve (@groks)" -"Steve Gargan (@sgargan)" -"Steve Smith (@tarka)" -"Takashi Someda (@tksmd)" -"Taneli Leppä (@rosmo)" -"Tim Bielawa (@tbielawa)" -"Tim Bielawa (@tbielawa)" -"Tim Mahoney (@timmahoney)" -"Timothy Appnel (@tima)" -"Tom Bamford (@tombamford)" -"Trond Hindenes (@trondhindenes)" -"Vincent Van der Kussen (@vincentvdk)" -"Vincent Viallet (@zbal)" -"WAKAYAMA Shirou (@shirou)" -"Will Thames (@willthames)" -"Willy Barro (@willybarro)" -"Xabier Larrakoetxea (@slok)" -"Yeukhon Wong (@yeukhon)" -"Zacharie Eakin (@zeekin)" -"berenddeboer (@berenddeboer)" -"bleader (@bleader)" -"curtis (@ccollicutt)" +- "Adam Garside (@fabulops)" +- "Adam Keech (@smadam813)" +- "Adam Miller (@maxamillion)" +- "Alex Coomans (@drcapulet)" +- "Alexander Bulimov (@abulimov)" +- "Alexander Saltanov (@sashka)" +- "Alexander Winkler (@dermute)" +- "Andrew de Quincey (@adq)" +- "André Paramés (@andreparames)" +- "Andy Hill (@andyhky)" +- "Artūras `arturaz` Šlajus (@arturaz)" +- "Augustus Kling (@AugustusKling)" +- "BOURDEL Paul (@pb8226)" +- "Balazs Pocze (@banyek)" +- "Ben Whaley (@bwhaley)" +- "Benno Joy (@bennojoy)" +- "Bernhard Weitzhofer (@b6d)" +- "Boyd Adamson (@brontitall)" +- "Brad Olson (@bradobro)" +- "Brian Coca (@bcoca)" +- "Brice Burgess (@briceburg)" +- "Bruce Pennypacker (@bpennypacker)" +- "Carson Gee (@carsongee)" +- "Chris Church (@cchurch)" +- "Chris Hoffman (@chrishoffman)" +- "Chris Long (@alcamie101)" +- "Chris Schmidt (@chrisisbeef)" +- "Christian Berendt (@berendt)" +- "Christopher H. Laco (@claco)" +- "Cristian van Ee (@DJMuggs)" +- "Dag Wieers (@dagwieers)" +- "Dane Summers (@dsummersl)" +- "Daniel Jaouen (@danieljaouen)" +- "Daniel Schep (@dschep)" +- "Dariusz Owczarek (@dareko)" +- "Darryl Stoflet (@dstoflet)" +- "David CHANIAL (@davixx)" +- "David Stygstra (@stygstra)" +- "Derek Carter (@goozbach)" +- "Dimitrios Tydeas Mengidis (@dmtrs)" +- "Doug Luce (@dougluce)" +- "Dylan Martin (@pileofrogs)" +- "Elliott Foster (@elliotttf)" +- "Eric Johnson (@erjohnso)" +- "Evan Duffield (@scicoin-project)" +- "Evan Kaufman (@EvanK)" +- "Evgenii Terechkov (@evgkrsk)" +- "Franck Cuny (@franckcuny)" +- "Gareth Rushgrove (@garethr)" +- "Hagai Kariti (@hkariti)" +- "Hector Acosta (@hacosta)" +- "Hiroaki Nakamura (@hnakamur)" +- "Ivan Vanderbyl (@ivanvanderbyl)" +- "Jakub Jirutka (@jirutka)" +- "James Cammarata (@jimi-c)" +- "James Laska (@jlaska)" +- "James S. Martin (@jsmartin)" +- "Jan-Piet Mens (@jpmens)" +- "Jayson Vantuyl (@jvantuyl)" +- "Jens Depuydt (@jensdepuydt)" +- "Jeroen Hoekx (@jhoekx)" +- "Jesse Keating (@j2sol)" +- "Jim Dalton (@jsdalton)" +- "Jim Richardson (@weaselkeeper)" +- "Jimmy Tang (@jcftang)" +- "Johan Wiren (@johanwiren)" +- "John Dewey (@retr0h)" +- "John Jarvis (@jarv)" +- "John Whitbeck (@jwhitbeck)" +- "Jon Hawkesworth (@jhawkesworth)" +- "Jonas Pfenniger (@zimbatm)" +- "Jonathan I. Davila (@defionscode)" +- "Joseph Callen (@jcpowermac)" +- "Kevin Carter (@cloudnull)" +- "Lester Wade (@lwade)" +- "Lorin Hochstein (@lorin)" +- "Manuel Sousa (@manuel-sousa)" +- "Mark Theunissen (@marktheunissen)" +- "Matt Coddington (@mcodd)" +- "Matt Hite (@mhite)" +- "Matt Makai (@makaimc)" +- "Matt Martz (@sivel)" +- "Matt Wright (@mattupstate)" +- "Matthew Vernon (@mcv21)" +- "Matthew Williams (@mgwilliams)" +- "Matthias Vogelgesang (@matze)" +- "Max Riveiro (@kavu)" +- "Michael Gregson (@mgregson)" +- "Michael J. Schultz (@mjschultz)" +- "Michael Schuett (@michaeljs1990)" +- "Michael Warkentin (@mwarkentin)" +- "Mischa Peters (@mischapeters)" +- "Monty Taylor (@emonty)" +- "Nandor Sivok (@dominis)" +- "Nate Coraor (@natefoo)" +- "Nate Kingsley (@nate-kingsley)" +- "Nick Harring (@NickatEpic)" +- "Patrick Callahan (@dirtyharrycallahan)" +- "Patrick Ogenstad (@ogenstad)" +- "Patrick Pelletier (@skinp)" +- "Patrik Lundin (@eest)" +- "Paul Durivage (@angstwad)" +- "Pavel Antonov (@softzilla)" +- "Pepe Barbe (@elventear)" +- "Peter Mounce (@petemounce)" +- "Peter Oliver (@mavit)" +- "Peter Sprygada (@privateip)" +- "Peter Tan (@tanpeter)" +- "Philippe Makowski (@pmakowski)" +- "Phillip Gentry, CX Inc (@pcgentry)" +- "Quentin Stafford-Fraser (@quentinsf)" +- "Ramon de la Fuente (@ramondelafuente)" +- "Raul Melo (@melodous)" +- "Ravi Bhure (@ravibhure)" +- "René Moser (@resmo)" +- "Richard Hoop (@rhoop)" +- "Richard Isaacson (@risaacson)" +- "Rick Mendes (@rickmendes)" +- "Romeo Theriault (@romeotheriault)" +- "Scott Anderson (@tastychutney)" +- "Sebastian Kornehl (@skornehl)" +- "Serge van Ginderachter (@srvg)" +- "Sergei Antipov (@UnderGreen)" +- "Seth Edwards (@sedward)" +- "Silviu Dicu (@silviud)" +- "Simon JAILLET (@jails)" +- "Stephen Fromm (@sfromm)" +- "Steve (@groks)" +- "Steve Gargan (@sgargan)" +- "Steve Smith (@tarka)" +- "Takashi Someda (@tksmd)" +- "Taneli Leppä (@rosmo)" +- "Tim Bielawa (@tbielawa)" +- "Tim Bielawa (@tbielawa)" +- "Tim Mahoney (@timmahoney)" +- "Timothy Appnel (@tima)" +- "Tom Bamford (@tombamford)" +- "Trond Hindenes (@trondhindenes)" +- "Vincent Van der Kussen (@vincentvdk)" +- "Vincent Viallet (@zbal)" +- "WAKAYAMA Shirou (@shirou)" +- "Will Thames (@willthames)" +- "Willy Barro (@willybarro)" +- "Xabier Larrakoetxea (@slok)" +- "Yeukhon Wong (@yeukhon)" +- "Zacharie Eakin (@zeekin)" +- "berenddeboer (@berenddeboer)" +- "bleader (@bleader)" +- "curtis (@ccollicutt)" Retired ======= From 4f32de4457ad3dbb1d67ea7e2763b3fc927261f8 Mon Sep 17 00:00:00 2001 From: Michael Schuett Date: Fri, 7 Aug 2015 15:08:51 -0400 Subject: [PATCH 0571/2522] Add link link to github users accounts --- REVIEWERS.md | 297 +++++++++++++++++++++++++-------------------------- 1 file changed, 148 insertions(+), 149 deletions(-) diff --git a/REVIEWERS.md b/REVIEWERS.md index 8f67835efd4..7922fc2c1f7 100644 --- a/REVIEWERS.md +++ b/REVIEWERS.md @@ -6,155 +6,154 @@ Two +1 votes by any of these module reviewers on a new module pull request will Active ====== -- "Adam Garside (@fabulops)" -- "Adam Keech (@smadam813)" -- "Adam Miller (@maxamillion)" -- "Alex Coomans (@drcapulet)" -- "Alexander Bulimov (@abulimov)" -- "Alexander Saltanov (@sashka)" -- "Alexander Winkler (@dermute)" -- "Andrew de Quincey (@adq)" -- "André Paramés (@andreparames)" -- "Andy Hill (@andyhky)" -- "Artūras `arturaz` Šlajus (@arturaz)" -- "Augustus Kling (@AugustusKling)" -- "BOURDEL Paul (@pb8226)" -- "Balazs Pocze (@banyek)" -- "Ben Whaley (@bwhaley)" -- "Benno Joy (@bennojoy)" -- "Bernhard Weitzhofer (@b6d)" -- "Boyd Adamson (@brontitall)" -- "Brad Olson (@bradobro)" -- "Brian Coca (@bcoca)" -- "Brice Burgess (@briceburg)" -- "Bruce Pennypacker (@bpennypacker)" -- "Carson Gee (@carsongee)" -- "Chris Church (@cchurch)" -- "Chris Hoffman (@chrishoffman)" -- "Chris Long (@alcamie101)" -- "Chris Schmidt (@chrisisbeef)" -- "Christian Berendt (@berendt)" -- "Christopher H. Laco (@claco)" -- "Cristian van Ee (@DJMuggs)" -- "Dag Wieers (@dagwieers)" -- "Dane Summers (@dsummersl)" -- "Daniel Jaouen (@danieljaouen)" -- "Daniel Schep (@dschep)" -- "Dariusz Owczarek (@dareko)" -- "Darryl Stoflet (@dstoflet)" -- "David CHANIAL (@davixx)" -- "David Stygstra (@stygstra)" -- "Derek Carter (@goozbach)" -- "Dimitrios Tydeas Mengidis (@dmtrs)" -- "Doug Luce (@dougluce)" -- "Dylan Martin (@pileofrogs)" -- "Elliott Foster (@elliotttf)" -- "Eric Johnson (@erjohnso)" -- "Evan Duffield (@scicoin-project)" -- "Evan Kaufman (@EvanK)" -- "Evgenii Terechkov (@evgkrsk)" -- "Franck Cuny (@franckcuny)" -- "Gareth Rushgrove (@garethr)" -- "Hagai Kariti (@hkariti)" -- "Hector Acosta (@hacosta)" -- "Hiroaki Nakamura (@hnakamur)" -- "Ivan Vanderbyl (@ivanvanderbyl)" -- "Jakub Jirutka (@jirutka)" -- "James Cammarata (@jimi-c)" -- "James Laska (@jlaska)" -- "James S. Martin (@jsmartin)" -- "Jan-Piet Mens (@jpmens)" -- "Jayson Vantuyl (@jvantuyl)" -- "Jens Depuydt (@jensdepuydt)" -- "Jeroen Hoekx (@jhoekx)" -- "Jesse Keating (@j2sol)" -- "Jim Dalton (@jsdalton)" -- "Jim Richardson (@weaselkeeper)" -- "Jimmy Tang (@jcftang)" -- "Johan Wiren (@johanwiren)" -- "John Dewey (@retr0h)" -- "John Jarvis (@jarv)" -- "John Whitbeck (@jwhitbeck)" -- "Jon Hawkesworth (@jhawkesworth)" -- "Jonas Pfenniger (@zimbatm)" -- "Jonathan I. Davila (@defionscode)" -- "Joseph Callen (@jcpowermac)" -- "Kevin Carter (@cloudnull)" -- "Lester Wade (@lwade)" -- "Lorin Hochstein (@lorin)" -- "Manuel Sousa (@manuel-sousa)" -- "Mark Theunissen (@marktheunissen)" -- "Matt Coddington (@mcodd)" -- "Matt Hite (@mhite)" -- "Matt Makai (@makaimc)" -- "Matt Martz (@sivel)" -- "Matt Wright (@mattupstate)" -- "Matthew Vernon (@mcv21)" -- "Matthew Williams (@mgwilliams)" -- "Matthias Vogelgesang (@matze)" -- "Max Riveiro (@kavu)" -- "Michael Gregson (@mgregson)" -- "Michael J. Schultz (@mjschultz)" -- "Michael Schuett (@michaeljs1990)" -- "Michael Warkentin (@mwarkentin)" -- "Mischa Peters (@mischapeters)" -- "Monty Taylor (@emonty)" -- "Nandor Sivok (@dominis)" -- "Nate Coraor (@natefoo)" -- "Nate Kingsley (@nate-kingsley)" -- "Nick Harring (@NickatEpic)" -- "Patrick Callahan (@dirtyharrycallahan)" -- "Patrick Ogenstad (@ogenstad)" -- "Patrick Pelletier (@skinp)" -- "Patrik Lundin (@eest)" -- "Paul Durivage (@angstwad)" -- "Pavel Antonov (@softzilla)" -- "Pepe Barbe (@elventear)" -- "Peter Mounce (@petemounce)" -- "Peter Oliver (@mavit)" -- "Peter Sprygada (@privateip)" -- "Peter Tan (@tanpeter)" -- "Philippe Makowski (@pmakowski)" -- "Phillip Gentry, CX Inc (@pcgentry)" -- "Quentin Stafford-Fraser (@quentinsf)" -- "Ramon de la Fuente (@ramondelafuente)" -- "Raul Melo (@melodous)" -- "Ravi Bhure (@ravibhure)" -- "René Moser (@resmo)" -- "Richard Hoop (@rhoop)" -- "Richard Isaacson (@risaacson)" -- "Rick Mendes (@rickmendes)" -- "Romeo Theriault (@romeotheriault)" -- "Scott Anderson (@tastychutney)" -- "Sebastian Kornehl (@skornehl)" -- "Serge van Ginderachter (@srvg)" -- "Sergei Antipov (@UnderGreen)" -- "Seth Edwards (@sedward)" -- "Silviu Dicu (@silviud)" -- "Simon JAILLET (@jails)" -- "Stephen Fromm (@sfromm)" -- "Steve (@groks)" -- "Steve Gargan (@sgargan)" -- "Steve Smith (@tarka)" -- "Takashi Someda (@tksmd)" -- "Taneli Leppä (@rosmo)" -- "Tim Bielawa (@tbielawa)" -- "Tim Bielawa (@tbielawa)" -- "Tim Mahoney (@timmahoney)" -- "Timothy Appnel (@tima)" -- "Tom Bamford (@tombamford)" -- "Trond Hindenes (@trondhindenes)" -- "Vincent Van der Kussen (@vincentvdk)" -- "Vincent Viallet (@zbal)" -- "WAKAYAMA Shirou (@shirou)" -- "Will Thames (@willthames)" -- "Willy Barro (@willybarro)" -- "Xabier Larrakoetxea (@slok)" -- "Yeukhon Wong (@yeukhon)" -- "Zacharie Eakin (@zeekin)" -- "berenddeboer (@berenddeboer)" -- "bleader (@bleader)" -- "curtis (@ccollicutt)" +- "Adam Garside [@fabulops](https://www.github.com/fabulops)" +- "Adam Keech [@smadam813](https://www.github.com/smadam813)" +- "Adam Miller [@maxamillion](https://www.github.com/maxamillion)" +- "Alex Coomans [@drcapulet](https://www.github.com/drcapulet)" +- "Alexander Bulimov [@abulimov](https://www.github.com/abulimov)" +- "Alexander Saltanov [@sashka](https://www.github.com/sashka)" +- "Alexander Winkler [@dermute](https://www.github.com/dermute)" +- "Andrew de Quincey [@adq](https://www.github.com/adq)" +- "André Paramés [@andreparames](https://www.github.com/andreparames)" +- "Andy Hill [@andyhky](https://www.github.com/andyhky)" +- "Artūras `arturaz` Šlajus [@arturaz](https://www.github.com/arturaz)" +- "Augustus Kling [@AugustusKling](https://www.github.com/AugustusKling)" +- "BOURDEL Paul [@pb8226](https://www.github.com/pb8226)" +- "Balazs Pocze [@banyek](https://www.github.com/banyek)" +- "Ben Whaley [@bwhaley](https://www.github.com/bwhaley)" +- "Benno Joy [@bennojoy](https://www.github.com/bennojoy)" +- "Bernhard Weitzhofer [@b6d](https://www.github.com/b6d)" +- "Boyd Adamson [@brontitall](https://www.github.com/brontitall)" +- "Brad Olson [@bradobro](https://www.github.com/bradobro)" +- "Brian Coca [@bcoca](https://www.github.com/bcoca)" +- "Brice Burgess [@briceburg](https://www.github.com/briceburg)" +- "Bruce Pennypacker [@bpennypacker](https://www.github.com/bpennypacker)" +- "Carson Gee [@carsongee](https://www.github.com/carsongee)" +- "Chris Church [@cchurch](https://www.github.com/cchurch)" +- "Chris Hoffman [@chrishoffman](https://www.github.com/chrishoffman)" +- "Chris Long [@alcamie101](https://www.github.com/alcamie101)" +- "Chris Schmidt [@chrisisbeef](https://www.github.com/chrisisbeef)" +- "Christian Berendt [@berendt](https://www.github.com/berendt)" +- "Christopher H. Laco [@claco](https://www.github.com/claco)" +- "Cristian van Ee [@DJMuggs](https://www.github.com/DJMuggs)" +- "Dag Wieers [@dagwieers](https://www.github.com/dagwieers)" +- "Dane Summers [@dsummersl](https://www.github.com/dsummersl)" +- "Daniel Jaouen [@danieljaouen](https://www.github.com/danieljaouen)" +- "Daniel Schep [@dschep](https://www.github.com/dschep)" +- "Dariusz Owczarek [@dareko](https://www.github.com/dareko)" +- "Darryl Stoflet [@dstoflet](https://www.github.com/dstoflet)" +- "David CHANIAL [@davixx](https://www.github.com/davixx)" +- "David Stygstra [@stygstra](https://www.github.com/)" +- "Derek Carter [@goozbach](https://www.github.com/stygstra)" +- "Dimitrios Tydeas Mengidis [@dmtrs](https://www.github.com/dmtrs)" +- "Doug Luce [@dougluce](https://www.github.com/dougluce)" +- "Dylan Martin [@pileofrogs](https://www.github.com/pileofrogs)" +- "Elliott Foster [@elliotttf](https://www.github.com/elliotttf)" +- "Eric Johnson [@erjohnso](https://www.github.com/erjohnso)" +- "Evan Duffield [@scicoin-project](https://www.github.com/scicoin-project)" +- "Evan Kaufman [@EvanK](https://www.github.com/EvanK)" +- "Evgenii Terechkov [@evgkrsk](https://www.github.com/evgkrsk)" +- "Franck Cuny [@franckcuny](https://www.github.com/franckcuny)" +- "Gareth Rushgrove [@garethr](https://www.github.com/garethr)" +- "Hagai Kariti [@hkariti](https://www.github.com/hkariti)" +- "Hector Acosta [@hacosta](https://www.github.com/hacosta)" +- "Hiroaki Nakamura [@hnakamur](https://www.github.com/hnakamur)" +- "Ivan Vanderbyl [@ivanvanderbyl](https://www.github.com/ivanvanderbyl)" +- "Jakub Jirutka [@jirutka](https://www.github.com/jirutka)" +- "James Cammarata [@jimi-c](https://www.github.com/jimi-c)" +- "James Laska [@jlaska](https://www.github.com/jlaska)" +- "James S. Martin [@jsmartin](https://www.github.com/jsmartin)" +- "Jan-Piet Mens [@jpmens](https://www.github.com/jpmens)" +- "Jayson Vantuyl [@jvantuyl](https://www.github.com/jvantuyl)" +- "Jens Depuydt [@jensdepuydt](https://www.github.com/jensdepuydt)" +- "Jeroen Hoekx [@jhoekx](https://www.github.com/jhoekx)" +- "Jesse Keating [@j2sol](https://www.github.com/j2sol)" +- "Jim Dalton [@jsdalton](https://www.github.com/jsdalton)" +- "Jim Richardson [@weaselkeeper](https://www.github.com/weaselkeeper)" +- "Jimmy Tang [@jcftang](https://www.github.com/jcftang)" +- "Johan Wiren [@johanwiren](https://www.github.com/johanwiren)" +- "John Dewey [@retr0h](https://www.github.com/retr0h)" +- "John Jarvis [@jarv](https://www.github.com/jarv)" +- "John Whitbeck [@jwhitbeck](https://www.github.com/jwhitbeck)" +- "Jon Hawkesworth [@jhawkesworth](https://www.github.com/jhawkesworth)" +- "Jonas Pfenniger [@zimbatm](https://www.github.com/zimbatm)" +- "Jonathan I. Davila [@defionscode](https://www.github.com/defionscode)" +- "Joseph Callen [@jcpowermac](https://www.github.com/jcpowermac)" +- "Kevin Carter [@cloudnull](https://www.github.com/cloudnull)" +- "Lester Wade [@lwade](https://www.github.com/lwade)" +- "Lorin Hochstein [@lorin](https://www.github.com/lorin)" +- "Manuel Sousa [@manuel-sousa](https://www.github.com/manuel-sousa)" +- "Mark Theunissen [@marktheunissen](https://www.github.com/marktheunissen)" +- "Matt Coddington [@mcodd](https://www.github.com/mcodd)" +- "Matt Hite [@mhite](https://www.github.com/mhite)" +- "Matt Makai [@makaimc](https://www.github.com/makaimc)" +- "Matt Martz [@sivel](https://www.github.com/sivel)" +- "Matt Wright [@mattupstate](https://www.github.com/mattupstate)" +- "Matthew Vernon [@mcv21](https://www.github.com/mcv21)" +- "Matthew Williams [@mgwilliams](https://www.github.com/mgwilliams)" +- "Matthias Vogelgesang [@matze](https://www.github.com/matze)" +- "Max Riveiro [@kavu](https://www.github.com/kavu)" +- "Michael Gregson [@mgregson](https://www.github.com/mgregson)" +- "Michael J. Schultz [@mjschultz](https://www.github.com/mjschultz)" +- "Michael Schuett [@michaeljs1990](https://www.github.com/michaeljs1990)" +- "Michael Warkentin [@mwarkentin](https://www.github.com/mwarkentin)" +- "Mischa Peters [@mischapeters](https://www.github.com/mischapeters)" +- "Monty Taylor [@emonty](https://www.github.com/emonty)" +- "Nandor Sivok [@dominis](https://www.github.com/dominis)" +- "Nate Coraor [@natefoo](https://www.github.com/natefoo)" +- "Nate Kingsley [@nate-kingsley](https://www.github.com/nate-kingsley)" +- "Nick Harring [@NickatEpic](https://www.github.com/NickatEpic)" +- "Patrick Callahan [@dirtyharrycallahan](https://www.github.com/dirtyharrycallahan)" +- "Patrick Ogenstad [@ogenstad](https://www.github.com/ogenstad)" +- "Patrick Pelletier [@skinp](https://www.github.com/skinp)" +- "Patrik Lundin [@eest](https://www.github.com/eest)" +- "Paul Durivage [@angstwad](https://www.github.com/angstwad)" +- "Pavel Antonov [@softzilla](https://www.github.com/softzilla)" +- "Pepe Barbe [@elventear](https://www.github.com/elventear)" +- "Peter Mounce [@petemounce](https://www.github.com/petemounce)" +- "Peter Oliver [@mavit](https://www.github.com/mavit)" +- "Peter Sprygada [@privateip](https://www.github.com/privateip)" +- "Peter Tan [@tanpeter](https://www.github.com/tanpeter)" +- "Philippe Makowski [@pmakowski](https://www.github.com/pmakowski)" +- "Phillip Gentry, CX Inc [@pcgentry](https://www.github.com/pcgentry)" +- "Quentin Stafford-Fraser [@quentinsf](https://www.github.com/quentinsf)" +- "Ramon de la Fuente [@ramondelafuente](https://www.github.com/ramondelafuente)" +- "Raul Melo [@melodous](https://www.github.com/melodous)" +- "Ravi Bhure [@ravibhure](https://www.github.com/ravibhure)" +- "René Moser [@resmo](https://www.github.com/resmo)" +- "Richard Hoop [@rhoop](https://www.github.com/rhoop)" +- "Richard Isaacson [@risaacson](https://www.github.com/)" +- "Rick Mendes [@rickmendes](https://www.github.com/risaacson)" +- "Romeo Theriault [@romeotheriault](https://www.github.com/romeotheriault)" +- "Scott Anderson [@tastychutney](https://www.github.com/tastychutney)" +- "Sebastian Kornehl [@skornehl](https://www.github.com/skornehl)" +- "Serge van Ginderachter [@srvg](https://www.github.com/srvg)" +- "Sergei Antipov [@UnderGreen](https://www.github.com/UnderGreen)" +- "Seth Edwards [@sedward](https://www.github.com/sedward)" +- "Silviu Dicu [@silviud](https://www.github.com/silviud)" +- "Simon JAILLET [@jails](https://www.github.com/jails)" +- "Stephen Fromm [@sfromm](https://www.github.com/sfromm)" +- "Steve [@groks](https://www.github.com/groks)" +- "Steve Gargan [@sgargan](https://www.github.com/sgargan)" +- "Steve Smith [@tarka](https://www.github.com/tarka)" +- "Takashi Someda [@tksmd](https://www.github.com/tksmd)" +- "Taneli Leppä [@rosmo](https://www.github.com/rosmo)" +- "Tim Bielawa [@tbielawa](https://www.github.com/tbielawa)" +- "Tim Mahoney [@timmahoney](https://www.github.com/timmahoney)" +- "Timothy Appnel [@tima](https://www.github.com/tima)" +- "Tom Bamford [@tombamford](https://www.github.com/tombamford)" +- "Trond Hindenes [@trondhindenes](https://www.github.com/trondhindenes)" +- "Vincent Van der Kussen [@vincentvdk](https://www.github.com/vincentvdk)" +- "Vincent Viallet [@zbal](https://www.github.com/zbal)" +- "WAKAYAMA Shirou [@shirou](https://www.github.com/shirou)" +- "Will Thames [@willthames](https://www.github.com/willthames)" +- "Willy Barro [@willybarro](https://www.github.com/willybarro)" +- "Xabier Larrakoetxea [@slok](https://www.github.com/slok)" +- "Yeukhon Wong [@yeukhon](https://www.github.com/yeukhon)" +- "Zacharie Eakin [@zeekin](https://www.github.com/zeekin)" +- "berenddeboer [@berenddeboer](https://www.github.com/berenddeboer)" +- "bleader [@bleader](https://www.github.com/bleader)" +- "curtis [@ccollicutt](https://www.github.com/ccollicutt)" Retired ======= From ea48c729cb48c9aa5ea414efe2e7863d0d7f25ae Mon Sep 17 00:00:00 2001 From: Michael Schuett Date: Fri, 7 Aug 2015 15:19:32 -0400 Subject: [PATCH 0572/2522] remove quotes --- REVIEWERS.md | 296 +++++++++++++++++++++++++-------------------------- 1 file changed, 148 insertions(+), 148 deletions(-) diff --git a/REVIEWERS.md b/REVIEWERS.md index 7922fc2c1f7..b3e0c792628 100644 --- a/REVIEWERS.md +++ b/REVIEWERS.md @@ -6,154 +6,154 @@ Two +1 votes by any of these module reviewers on a new module pull request will Active ====== -- "Adam Garside [@fabulops](https://www.github.com/fabulops)" -- "Adam Keech [@smadam813](https://www.github.com/smadam813)" -- "Adam Miller [@maxamillion](https://www.github.com/maxamillion)" -- "Alex Coomans [@drcapulet](https://www.github.com/drcapulet)" -- "Alexander Bulimov [@abulimov](https://www.github.com/abulimov)" -- "Alexander Saltanov [@sashka](https://www.github.com/sashka)" -- "Alexander Winkler [@dermute](https://www.github.com/dermute)" -- "Andrew de Quincey [@adq](https://www.github.com/adq)" -- "André Paramés [@andreparames](https://www.github.com/andreparames)" -- "Andy Hill [@andyhky](https://www.github.com/andyhky)" -- "Artūras `arturaz` Šlajus [@arturaz](https://www.github.com/arturaz)" -- "Augustus Kling [@AugustusKling](https://www.github.com/AugustusKling)" -- "BOURDEL Paul [@pb8226](https://www.github.com/pb8226)" -- "Balazs Pocze [@banyek](https://www.github.com/banyek)" -- "Ben Whaley [@bwhaley](https://www.github.com/bwhaley)" -- "Benno Joy [@bennojoy](https://www.github.com/bennojoy)" -- "Bernhard Weitzhofer [@b6d](https://www.github.com/b6d)" -- "Boyd Adamson [@brontitall](https://www.github.com/brontitall)" -- "Brad Olson [@bradobro](https://www.github.com/bradobro)" -- "Brian Coca [@bcoca](https://www.github.com/bcoca)" -- "Brice Burgess [@briceburg](https://www.github.com/briceburg)" -- "Bruce Pennypacker [@bpennypacker](https://www.github.com/bpennypacker)" -- "Carson Gee [@carsongee](https://www.github.com/carsongee)" -- "Chris Church [@cchurch](https://www.github.com/cchurch)" -- "Chris Hoffman [@chrishoffman](https://www.github.com/chrishoffman)" -- "Chris Long [@alcamie101](https://www.github.com/alcamie101)" -- "Chris Schmidt [@chrisisbeef](https://www.github.com/chrisisbeef)" -- "Christian Berendt [@berendt](https://www.github.com/berendt)" -- "Christopher H. Laco [@claco](https://www.github.com/claco)" -- "Cristian van Ee [@DJMuggs](https://www.github.com/DJMuggs)" -- "Dag Wieers [@dagwieers](https://www.github.com/dagwieers)" -- "Dane Summers [@dsummersl](https://www.github.com/dsummersl)" -- "Daniel Jaouen [@danieljaouen](https://www.github.com/danieljaouen)" -- "Daniel Schep [@dschep](https://www.github.com/dschep)" -- "Dariusz Owczarek [@dareko](https://www.github.com/dareko)" -- "Darryl Stoflet [@dstoflet](https://www.github.com/dstoflet)" -- "David CHANIAL [@davixx](https://www.github.com/davixx)" -- "David Stygstra [@stygstra](https://www.github.com/)" -- "Derek Carter [@goozbach](https://www.github.com/stygstra)" -- "Dimitrios Tydeas Mengidis [@dmtrs](https://www.github.com/dmtrs)" -- "Doug Luce [@dougluce](https://www.github.com/dougluce)" -- "Dylan Martin [@pileofrogs](https://www.github.com/pileofrogs)" -- "Elliott Foster [@elliotttf](https://www.github.com/elliotttf)" -- "Eric Johnson [@erjohnso](https://www.github.com/erjohnso)" -- "Evan Duffield [@scicoin-project](https://www.github.com/scicoin-project)" -- "Evan Kaufman [@EvanK](https://www.github.com/EvanK)" -- "Evgenii Terechkov [@evgkrsk](https://www.github.com/evgkrsk)" -- "Franck Cuny [@franckcuny](https://www.github.com/franckcuny)" -- "Gareth Rushgrove [@garethr](https://www.github.com/garethr)" -- "Hagai Kariti [@hkariti](https://www.github.com/hkariti)" -- "Hector Acosta [@hacosta](https://www.github.com/hacosta)" -- "Hiroaki Nakamura [@hnakamur](https://www.github.com/hnakamur)" -- "Ivan Vanderbyl [@ivanvanderbyl](https://www.github.com/ivanvanderbyl)" -- "Jakub Jirutka [@jirutka](https://www.github.com/jirutka)" -- "James Cammarata [@jimi-c](https://www.github.com/jimi-c)" -- "James Laska [@jlaska](https://www.github.com/jlaska)" -- "James S. Martin [@jsmartin](https://www.github.com/jsmartin)" -- "Jan-Piet Mens [@jpmens](https://www.github.com/jpmens)" -- "Jayson Vantuyl [@jvantuyl](https://www.github.com/jvantuyl)" -- "Jens Depuydt [@jensdepuydt](https://www.github.com/jensdepuydt)" -- "Jeroen Hoekx [@jhoekx](https://www.github.com/jhoekx)" -- "Jesse Keating [@j2sol](https://www.github.com/j2sol)" -- "Jim Dalton [@jsdalton](https://www.github.com/jsdalton)" -- "Jim Richardson [@weaselkeeper](https://www.github.com/weaselkeeper)" -- "Jimmy Tang [@jcftang](https://www.github.com/jcftang)" -- "Johan Wiren [@johanwiren](https://www.github.com/johanwiren)" -- "John Dewey [@retr0h](https://www.github.com/retr0h)" -- "John Jarvis [@jarv](https://www.github.com/jarv)" -- "John Whitbeck [@jwhitbeck](https://www.github.com/jwhitbeck)" -- "Jon Hawkesworth [@jhawkesworth](https://www.github.com/jhawkesworth)" -- "Jonas Pfenniger [@zimbatm](https://www.github.com/zimbatm)" -- "Jonathan I. Davila [@defionscode](https://www.github.com/defionscode)" -- "Joseph Callen [@jcpowermac](https://www.github.com/jcpowermac)" -- "Kevin Carter [@cloudnull](https://www.github.com/cloudnull)" -- "Lester Wade [@lwade](https://www.github.com/lwade)" -- "Lorin Hochstein [@lorin](https://www.github.com/lorin)" -- "Manuel Sousa [@manuel-sousa](https://www.github.com/manuel-sousa)" -- "Mark Theunissen [@marktheunissen](https://www.github.com/marktheunissen)" -- "Matt Coddington [@mcodd](https://www.github.com/mcodd)" -- "Matt Hite [@mhite](https://www.github.com/mhite)" -- "Matt Makai [@makaimc](https://www.github.com/makaimc)" -- "Matt Martz [@sivel](https://www.github.com/sivel)" -- "Matt Wright [@mattupstate](https://www.github.com/mattupstate)" -- "Matthew Vernon [@mcv21](https://www.github.com/mcv21)" -- "Matthew Williams [@mgwilliams](https://www.github.com/mgwilliams)" -- "Matthias Vogelgesang [@matze](https://www.github.com/matze)" -- "Max Riveiro [@kavu](https://www.github.com/kavu)" -- "Michael Gregson [@mgregson](https://www.github.com/mgregson)" -- "Michael J. Schultz [@mjschultz](https://www.github.com/mjschultz)" -- "Michael Schuett [@michaeljs1990](https://www.github.com/michaeljs1990)" -- "Michael Warkentin [@mwarkentin](https://www.github.com/mwarkentin)" -- "Mischa Peters [@mischapeters](https://www.github.com/mischapeters)" -- "Monty Taylor [@emonty](https://www.github.com/emonty)" -- "Nandor Sivok [@dominis](https://www.github.com/dominis)" -- "Nate Coraor [@natefoo](https://www.github.com/natefoo)" -- "Nate Kingsley [@nate-kingsley](https://www.github.com/nate-kingsley)" -- "Nick Harring [@NickatEpic](https://www.github.com/NickatEpic)" -- "Patrick Callahan [@dirtyharrycallahan](https://www.github.com/dirtyharrycallahan)" -- "Patrick Ogenstad [@ogenstad](https://www.github.com/ogenstad)" -- "Patrick Pelletier [@skinp](https://www.github.com/skinp)" -- "Patrik Lundin [@eest](https://www.github.com/eest)" -- "Paul Durivage [@angstwad](https://www.github.com/angstwad)" -- "Pavel Antonov [@softzilla](https://www.github.com/softzilla)" -- "Pepe Barbe [@elventear](https://www.github.com/elventear)" -- "Peter Mounce [@petemounce](https://www.github.com/petemounce)" -- "Peter Oliver [@mavit](https://www.github.com/mavit)" -- "Peter Sprygada [@privateip](https://www.github.com/privateip)" -- "Peter Tan [@tanpeter](https://www.github.com/tanpeter)" -- "Philippe Makowski [@pmakowski](https://www.github.com/pmakowski)" -- "Phillip Gentry, CX Inc [@pcgentry](https://www.github.com/pcgentry)" -- "Quentin Stafford-Fraser [@quentinsf](https://www.github.com/quentinsf)" -- "Ramon de la Fuente [@ramondelafuente](https://www.github.com/ramondelafuente)" -- "Raul Melo [@melodous](https://www.github.com/melodous)" -- "Ravi Bhure [@ravibhure](https://www.github.com/ravibhure)" -- "René Moser [@resmo](https://www.github.com/resmo)" -- "Richard Hoop [@rhoop](https://www.github.com/rhoop)" -- "Richard Isaacson [@risaacson](https://www.github.com/)" -- "Rick Mendes [@rickmendes](https://www.github.com/risaacson)" -- "Romeo Theriault [@romeotheriault](https://www.github.com/romeotheriault)" -- "Scott Anderson [@tastychutney](https://www.github.com/tastychutney)" -- "Sebastian Kornehl [@skornehl](https://www.github.com/skornehl)" -- "Serge van Ginderachter [@srvg](https://www.github.com/srvg)" -- "Sergei Antipov [@UnderGreen](https://www.github.com/UnderGreen)" -- "Seth Edwards [@sedward](https://www.github.com/sedward)" -- "Silviu Dicu [@silviud](https://www.github.com/silviud)" -- "Simon JAILLET [@jails](https://www.github.com/jails)" -- "Stephen Fromm [@sfromm](https://www.github.com/sfromm)" -- "Steve [@groks](https://www.github.com/groks)" -- "Steve Gargan [@sgargan](https://www.github.com/sgargan)" -- "Steve Smith [@tarka](https://www.github.com/tarka)" -- "Takashi Someda [@tksmd](https://www.github.com/tksmd)" -- "Taneli Leppä [@rosmo](https://www.github.com/rosmo)" -- "Tim Bielawa [@tbielawa](https://www.github.com/tbielawa)" -- "Tim Mahoney [@timmahoney](https://www.github.com/timmahoney)" -- "Timothy Appnel [@tima](https://www.github.com/tima)" -- "Tom Bamford [@tombamford](https://www.github.com/tombamford)" -- "Trond Hindenes [@trondhindenes](https://www.github.com/trondhindenes)" -- "Vincent Van der Kussen [@vincentvdk](https://www.github.com/vincentvdk)" -- "Vincent Viallet [@zbal](https://www.github.com/zbal)" -- "WAKAYAMA Shirou [@shirou](https://www.github.com/shirou)" -- "Will Thames [@willthames](https://www.github.com/willthames)" -- "Willy Barro [@willybarro](https://www.github.com/willybarro)" -- "Xabier Larrakoetxea [@slok](https://www.github.com/slok)" -- "Yeukhon Wong [@yeukhon](https://www.github.com/yeukhon)" -- "Zacharie Eakin [@zeekin](https://www.github.com/zeekin)" -- "berenddeboer [@berenddeboer](https://www.github.com/berenddeboer)" -- "bleader [@bleader](https://www.github.com/bleader)" -- "curtis [@ccollicutt](https://www.github.com/ccollicutt)" +- Adam Garside [@fabulops](https://www.github.com/fabulops) +- Adam Keech [@smadam813](https://www.github.com/smadam813) +- Adam Miller [@maxamillion](https://www.github.com/maxamillion) +- Alex Coomans [@drcapulet](https://www.github.com/drcapulet) +- Alexander Bulimov [@abulimov](https://www.github.com/abulimov) +- Alexander Saltanov [@sashka](https://www.github.com/sashka) +- Alexander Winkler [@dermute](https://www.github.com/dermute) +- Andrew de Quincey [@adq](https://www.github.com/adq) +- André Paramés [@andreparames](https://www.github.com/andreparames) +- Andy Hill [@andyhky](https://www.github.com/andyhky) +- Artūras `arturaz` Šlajus [@arturaz](https://www.github.com/arturaz) +- Augustus Kling [@AugustusKling](https://www.github.com/AugustusKling) +- BOURDEL Paul [@pb8226](https://www.github.com/pb8226) +- Balazs Pocze [@banyek](https://www.github.com/banyek) +- Ben Whaley [@bwhaley](https://www.github.com/bwhaley) +- Benno Joy [@bennojoy](https://www.github.com/bennojoy) +- Bernhard Weitzhofer [@b6d](https://www.github.com/b6d) +- Boyd Adamson [@brontitall](https://www.github.com/brontitall) +- Brad Olson [@bradobro](https://www.github.com/bradobro) +- Brian Coca [@bcoca](https://www.github.com/bcoca) +- Brice Burgess [@briceburg](https://www.github.com/briceburg) +- Bruce Pennypacker [@bpennypacker](https://www.github.com/bpennypacker) +- Carson Gee [@carsongee](https://www.github.com/carsongee) +- Chris Church [@cchurch](https://www.github.com/cchurch) +- Chris Hoffman [@chrishoffman](https://www.github.com/chrishoffman) +- Chris Long [@alcamie101](https://www.github.com/alcamie101) +- Chris Schmidt [@chrisisbeef](https://www.github.com/chrisisbeef) +- Christian Berendt [@berendt](https://www.github.com/berendt) +- Christopher H. Laco [@claco](https://www.github.com/claco) +- Cristian van Ee [@DJMuggs](https://www.github.com/DJMuggs) +- Dag Wieers [@dagwieers](https://www.github.com/dagwieers) +- Dane Summers [@dsummersl](https://www.github.com/dsummersl) +- Daniel Jaouen [@danieljaouen](https://www.github.com/danieljaouen) +- Daniel Schep [@dschep](https://www.github.com/dschep) +- Dariusz Owczarek [@dareko](https://www.github.com/dareko) +- Darryl Stoflet [@dstoflet](https://www.github.com/dstoflet) +- David CHANIAL [@davixx](https://www.github.com/davixx) +- David Stygstra [@stygstra](https://www.github.com/) +- Derek Carter [@goozbach](https://www.github.com/stygstra) +- Dimitrios Tydeas Mengidis [@dmtrs](https://www.github.com/dmtrs) +- Doug Luce [@dougluce](https://www.github.com/dougluce) +- Dylan Martin [@pileofrogs](https://www.github.com/pileofrogs) +- Elliott Foster [@elliotttf](https://www.github.com/elliotttf) +- Eric Johnson [@erjohnso](https://www.github.com/erjohnso) +- Evan Duffield [@scicoin-project](https://www.github.com/scicoin-project) +- Evan Kaufman [@EvanK](https://www.github.com/EvanK) +- Evgenii Terechkov [@evgkrsk](https://www.github.com/evgkrsk) +- Franck Cuny [@franckcuny](https://www.github.com/franckcuny) +- Gareth Rushgrove [@garethr](https://www.github.com/garethr) +- Hagai Kariti [@hkariti](https://www.github.com/hkariti) +- Hector Acosta [@hacosta](https://www.github.com/hacosta) +- Hiroaki Nakamura [@hnakamur](https://www.github.com/hnakamur) +- Ivan Vanderbyl [@ivanvanderbyl](https://www.github.com/ivanvanderbyl) +- Jakub Jirutka [@jirutka](https://www.github.com/jirutka) +- James Cammarata [@jimi-c](https://www.github.com/jimi-c) +- James Laska [@jlaska](https://www.github.com/jlaska) +- James S. Martin [@jsmartin](https://www.github.com/jsmartin) +- Jan-Piet Mens [@jpmens](https://www.github.com/jpmens) +- Jayson Vantuyl [@jvantuyl](https://www.github.com/jvantuyl) +- Jens Depuydt [@jensdepuydt](https://www.github.com/jensdepuydt) +- Jeroen Hoekx [@jhoekx](https://www.github.com/jhoekx) +- Jesse Keating [@j2sol](https://www.github.com/j2sol) +- Jim Dalton [@jsdalton](https://www.github.com/jsdalton) +- Jim Richardson [@weaselkeeper](https://www.github.com/weaselkeeper) +- Jimmy Tang [@jcftang](https://www.github.com/jcftang) +- Johan Wiren [@johanwiren](https://www.github.com/johanwiren) +- John Dewey [@retr0h](https://www.github.com/retr0h) +- John Jarvis [@jarv](https://www.github.com/jarv) +- John Whitbeck [@jwhitbeck](https://www.github.com/jwhitbeck) +- Jon Hawkesworth [@jhawkesworth](https://www.github.com/jhawkesworth) +- Jonas Pfenniger [@zimbatm](https://www.github.com/zimbatm) +- Jonathan I. Davila [@defionscode](https://www.github.com/defionscode) +- Joseph Callen [@jcpowermac](https://www.github.com/jcpowermac) +- Kevin Carter [@cloudnull](https://www.github.com/cloudnull) +- Lester Wade [@lwade](https://www.github.com/lwade) +- Lorin Hochstein [@lorin](https://www.github.com/lorin) +- Manuel Sousa [@manuel-sousa](https://www.github.com/manuel-sousa) +- Mark Theunissen [@marktheunissen](https://www.github.com/marktheunissen) +- Matt Coddington [@mcodd](https://www.github.com/mcodd) +- Matt Hite [@mhite](https://www.github.com/mhite) +- Matt Makai [@makaimc](https://www.github.com/makaimc) +- Matt Martz [@sivel](https://www.github.com/sivel) +- Matt Wright [@mattupstate](https://www.github.com/mattupstate) +- Matthew Vernon [@mcv21](https://www.github.com/mcv21) +- Matthew Williams [@mgwilliams](https://www.github.com/mgwilliams) +- Matthias Vogelgesang [@matze](https://www.github.com/matze) +- Max Riveiro [@kavu](https://www.github.com/kavu) +- Michael Gregson [@mgregson](https://www.github.com/mgregson) +- Michael J. Schultz [@mjschultz](https://www.github.com/mjschultz) +- Michael Schuett [@michaeljs1990](https://www.github.com/michaeljs1990) +- Michael Warkentin [@mwarkentin](https://www.github.com/mwarkentin) +- Mischa Peters [@mischapeters](https://www.github.com/mischapeters) +- Monty Taylor [@emonty](https://www.github.com/emonty) +- Nandor Sivok [@dominis](https://www.github.com/dominis) +- Nate Coraor [@natefoo](https://www.github.com/natefoo) +- Nate Kingsley [@nate-kingsley](https://www.github.com/nate-kingsley) +- Nick Harring [@NickatEpic](https://www.github.com/NickatEpic) +- Patrick Callahan [@dirtyharrycallahan](https://www.github.com/dirtyharrycallahan) +- Patrick Ogenstad [@ogenstad](https://www.github.com/ogenstad) +- Patrick Pelletier [@skinp](https://www.github.com/skinp) +- Patrik Lundin [@eest](https://www.github.com/eest) +- Paul Durivage [@angstwad](https://www.github.com/angstwad) +- Pavel Antonov [@softzilla](https://www.github.com/softzilla) +- Pepe Barbe [@elventear](https://www.github.com/elventear) +- Peter Mounce [@petemounce](https://www.github.com/petemounce) +- Peter Oliver [@mavit](https://www.github.com/mavit) +- Peter Sprygada [@privateip](https://www.github.com/privateip) +- Peter Tan [@tanpeter](https://www.github.com/tanpeter) +- Philippe Makowski [@pmakowski](https://www.github.com/pmakowski) +- Phillip Gentry, CX Inc [@pcgentry](https://www.github.com/pcgentry) +- Quentin Stafford-Fraser [@quentinsf](https://www.github.com/quentinsf) +- Ramon de la Fuente [@ramondelafuente](https://www.github.com/ramondelafuente) +- Raul Melo [@melodous](https://www.github.com/melodous) +- Ravi Bhure [@ravibhure](https://www.github.com/ravibhure) +- René Moser [@resmo](https://www.github.com/resmo) +- Richard Hoop [@rhoop](https://www.github.com/rhoop) +- Richard Isaacson [@risaacson](https://www.github.com/) +- Rick Mendes [@rickmendes](https://www.github.com/risaacson) +- Romeo Theriault [@romeotheriault](https://www.github.com/romeotheriault) +- Scott Anderson [@tastychutney](https://www.github.com/tastychutney) +- Sebastian Kornehl [@skornehl](https://www.github.com/skornehl) +- Serge van Ginderachter [@srvg](https://www.github.com/srvg) +- Sergei Antipov [@UnderGreen](https://www.github.com/UnderGreen) +- Seth Edwards [@sedward](https://www.github.com/sedward) +- Silviu Dicu [@silviud](https://www.github.com/silviud) +- Simon JAILLET [@jails](https://www.github.com/jails) +- Stephen Fromm [@sfromm](https://www.github.com/sfromm) +- Steve [@groks](https://www.github.com/groks) +- Steve Gargan [@sgargan](https://www.github.com/sgargan) +- Steve Smith [@tarka](https://www.github.com/tarka) +- Takashi Someda [@tksmd](https://www.github.com/tksmd) +- Taneli Leppä [@rosmo](https://www.github.com/rosmo) +- Tim Bielawa [@tbielawa](https://www.github.com/tbielawa) +- Tim Mahoney [@timmahoney](https://www.github.com/timmahoney) +- Timothy Appnel [@tima](https://www.github.com/tima) +- Tom Bamford [@tombamford](https://www.github.com/tombamford) +- Trond Hindenes [@trondhindenes](https://www.github.com/trondhindenes) +- Vincent Van der Kussen [@vincentvdk](https://www.github.com/vincentvdk) +- Vincent Viallet [@zbal](https://www.github.com/zbal) +- WAKAYAMA Shirou [@shirou](https://www.github.com/shirou) +- Will Thames [@willthames](https://www.github.com/willthames) +- Willy Barro [@willybarro](https://www.github.com/willybarro) +- Xabier Larrakoetxea [@slok](https://www.github.com/slok) +- Yeukhon Wong [@yeukhon](https://www.github.com/yeukhon) +- Zacharie Eakin [@zeekin](https://www.github.com/zeekin) +- berenddeboer [@berenddeboer](https://www.github.com/berenddeboer) +- bleader [@bleader](https://www.github.com/bleader) +- curtis [@ccollicutt](https://www.github.com/ccollicutt) Retired ======= From fdfabb264a7a24c1a9582925dd0c323344e85a5a Mon Sep 17 00:00:00 2001 From: Michael Schuett Date: Fri, 7 Aug 2015 17:47:55 -0400 Subject: [PATCH 0573/2522] Fix username Had missed one username and mixed up one with the other. --- REVIEWERS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/REVIEWERS.md b/REVIEWERS.md index b3e0c792628..b09af51d1c1 100644 --- a/REVIEWERS.md +++ b/REVIEWERS.md @@ -122,8 +122,8 @@ Active - Ravi Bhure [@ravibhure](https://www.github.com/ravibhure) - René Moser [@resmo](https://www.github.com/resmo) - Richard Hoop [@rhoop](https://www.github.com/rhoop) -- Richard Isaacson [@risaacson](https://www.github.com/) -- Rick Mendes [@rickmendes](https://www.github.com/risaacson) +- Richard Isaacson [@risaacson](https://www.github.com/risaacson) +- Rick Mendes [@rickmendes](https://www.github.com/rickmendes) - Romeo Theriault [@romeotheriault](https://www.github.com/romeotheriault) - Scott Anderson [@tastychutney](https://www.github.com/tastychutney) - Sebastian Kornehl [@skornehl](https://www.github.com/skornehl) From e71daafd8ea4ec703296383adc422e5d47982502 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 8 Aug 2015 15:40:05 +0200 Subject: [PATCH 0574/2522] cloudstack: fix KeyError: 'public_ip' in cs_instance --- cloud/cloudstack/cs_instance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index f8bef7c89e2..2204ad8dd16 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -732,7 +732,7 @@ def get_result(self, instance): if 'instancename' in instance: self.result['instance_name'] = instance['instancename'] if 'publicip' in instance: - self.result['public_ip'] = instance['public_ip'] + self.result['public_ip'] = instance['publicip'] if 'passwordenabled' in instance: self.result['password_enabled'] = instance['passwordenabled'] if 'password' in instance: From cbb2e9699328695be21422b0647fc5ffa44872e6 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 8 Aug 2015 15:43:32 +0200 Subject: [PATCH 0575/2522] cloudstack: doc fixes --- cloud/cloudstack/cs_domain.py | 1 - cloud/cloudstack/cs_instance.py | 2 +- cloud/cloudstack/cs_template.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cloud/cloudstack/cs_domain.py b/cloud/cloudstack/cs_domain.py index 0860aa3c49e..3b048eddbb5 100644 --- a/cloud/cloudstack/cs_domain.py +++ b/cloud/cloudstack/cs_domain.py @@ -104,7 +104,6 @@ returned: success type: string sample: example.local - ''' try: diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 2204ad8dd16..f17269848e6 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -23,7 +23,7 @@ module: cs_instance short_description: Manages instances and virtual machines on Apache CloudStack based clouds. description: - - Deploy, start, restart, stop and destroy instances. + - Deploy, start, update, scale, restart, stop and destroy instances. version_added: '2.0' author: "René Moser (@resmo)" options: diff --git a/cloud/cloudstack/cs_template.py b/cloud/cloudstack/cs_template.py index 8e56aafaa7e..11e2cc747cc 100644 --- a/cloud/cloudstack/cs_template.py +++ b/cloud/cloudstack/cs_template.py @@ -181,7 +181,7 @@ # Register a systemvm template - local_action: module: cs_template - name: systemvm-4.5 + name: systemvm-vmware-4.5 url: "http://packages.shapeblue.com/systemvmtemplate/4.5/systemvm64template-4.5-vmware.ova" hypervisor: VMware format: OVA From 27e1ace8a1557d83cc8734f4522b8c1643244948 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 10 Aug 2015 10:05:15 -0400 Subject: [PATCH 0576/2522] moved znode to clustering added version_added --- {coordination => clustering}/znode | 1 + 1 file changed, 1 insertion(+) rename {coordination => clustering}/znode (99%) diff --git a/coordination/znode b/clustering/znode similarity index 99% rename from coordination/znode rename to clustering/znode index 5d6bff16f06..9e34a431e86 100644 --- a/coordination/znode +++ b/clustering/znode @@ -19,6 +19,7 @@ DOCUMENTATION = """ --- module: znode +version_added: "2.0" short_description: Create, delete, retrieve, and update znodes using ZooKeeper. options: hosts: From a53e79d012c04db9f0c73b4673c094dd18895724 Mon Sep 17 00:00:00 2001 From: Darren Worrall Date: Mon, 10 Aug 2015 15:53:46 +0100 Subject: [PATCH 0577/2522] Add cs_ip_address module --- cloud/cloudstack/cs_ip_address.py | 459 ++++++++++++++++++++++++++++++ 1 file changed, 459 insertions(+) create mode 100644 cloud/cloudstack/cs_ip_address.py diff --git a/cloud/cloudstack/cs_ip_address.py b/cloud/cloudstack/cs_ip_address.py new file mode 100644 index 00000000000..e63ae0b7ae0 --- /dev/null +++ b/cloud/cloudstack/cs_ip_address.py @@ -0,0 +1,459 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2015, Darren Worrall +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: cs_ip_address +short_description: Manages Public/Secondary IP address associations +description: + - Acquires and associates a public IP to an account. Due to API limitations, + - this is not an idempotent call, so be sure to only conditionally call this + - when C(state=present) +version_added: '2.0' +author: "Darren Worrall @dazworrall" +options: + ip_address: + description: + - Public IP address. + - Required if C(state=absent) + required: true + domain: + description: + - Domain the IP address is related to. + required: false + default: null + network: + description: + - Network the IP address is related to. + required: false + default: null + account: + description: + - Account the IP address is related to. + required: false + default: null + project: + description: + - Name of the project the IP address is related to. + required: false + default: null + zone: + description: + - Name of the zone in which the virtual machine is in. + - If not set, default zone is used. + required: false + default: null + poll_async: + description: + - Poll async jobs until job has finished. + required: false + default: true +extends_documentation_fragment: cloudstack +''' + +EXAMPLES = ''' +# Associate an IP address +- local_action: + module: cs_ip_address + account: My Account + register: ip_address + when: create_instance|changed + +# Disassociate an IP address +- local_action: + module: cs_ip_address + ip_address: 1.2.3.4 + state: absent +''' + +RETURN = ''' +--- +ip_address: + description: Public IP address. + returned: success + type: string + sample: 1.2.3.4 +zone: + description: Name of zone the IP address is related to. + returned: success + type: string + sample: ch-gva-2 +project: + description: Name of project the IP address is related to. + returned: success + type: string + sample: Production +account: + description: Account the IP address is related to. + returned: success + type: string + sample: example account +domain: + description: Domain the IP address is related to. + returned: success + type: string + sample: example domain +''' + + +try: + from cs import CloudStack, CloudStackException, read_config + has_lib_cs = True +except ImportError: + has_lib_cs = False + +# import cloudstack common +class AnsibleCloudStack: + + def __init__(self, module): + if not has_lib_cs: + module.fail_json(msg="python library cs required: pip install cs") + + self.result = { + 'changed': False, + } + + self.module = module + self._connect() + + self.domain = None + self.account = None + self.project = None + self.ip_address = None + self.zone = None + self.vm = None + self.os_type = None + self.hypervisor = None + self.capabilities = None + + + def _connect(self): + api_key = self.module.params.get('api_key') + api_secret = self.module.params.get('secret_key') + api_url = self.module.params.get('api_url') + api_http_method = self.module.params.get('api_http_method') + api_timeout = self.module.params.get('api_timeout') + + if api_key and api_secret and api_url: + self.cs = CloudStack( + endpoint=api_url, + key=api_key, + secret=api_secret, + timeout=api_timeout, + method=api_http_method + ) + else: + self.cs = CloudStack(**read_config()) + + + def get_or_fallback(self, key=None, fallback_key=None): + value = self.module.params.get(key) + if not value: + value = self.module.params.get(fallback_key) + return value + + + # TODO: for backward compatibility only, remove if not used anymore + def _has_changed(self, want_dict, current_dict, only_keys=None): + return self.has_changed(want_dict=want_dict, current_dict=current_dict, only_keys=only_keys) + + + def has_changed(self, want_dict, current_dict, only_keys=None): + for key, value in want_dict.iteritems(): + + # Optionally limit by a list of keys + if only_keys and key not in only_keys: + continue; + + # Skip None values + if value is None: + continue; + + if key in current_dict: + + # API returns string for int in some cases, just to make sure + if isinstance(value, int): + current_dict[key] = int(current_dict[key]) + elif isinstance(value, str): + current_dict[key] = str(current_dict[key]) + + # Only need to detect a singe change, not every item + if value != current_dict[key]: + return True + return False + + + def _get_by_key(self, key=None, my_dict={}): + if key: + if key in my_dict: + return my_dict[key] + self.module.fail_json(msg="Something went wrong: %s not found" % key) + return my_dict + + + def get_project(self, key=None): + if self.project: + return self._get_by_key(key, self.project) + + project = self.module.params.get('project') + if not project: + return None + args = {} + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') + projects = self.cs.listProjects(**args) + if projects: + for p in projects['project']: + if project.lower() in [ p['name'].lower(), p['id'] ]: + self.project = p + return self._get_by_key(key, self.project) + self.module.fail_json(msg="project '%s' not found" % project) + + + def get_network(self, key=None, network=None): + if not network: + network = self.module.params.get('network') + + if not network: + return None + + args = {} + args['account'] = self.get_account('name') + args['domainid'] = self.get_domain('id') + args['projectid'] = self.get_project('id') + args['zoneid'] = self.get_zone('id') + + networks = self.cs.listNetworks(**args) + if not networks: + self.module.fail_json(msg="No networks available") + + for n in networks['network']: + if network in [ n['displaytext'], n['name'], n['id'] ]: + return self._get_by_key(key, n) + break + self.module.fail_json(msg="Network '%s' not found" % network) + + + def get_ip_address(self, key=None): + if self.ip_address: + return self._get_by_key(key, self.ip_address) + + ip_address = self.module.params.get('ip_address') + if not ip_address: + self.module.fail_json(msg="IP address param 'ip_address' is required") + + args = {} + args['ipaddress'] = ip_address + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') + args['projectid'] = self.get_project(key='id') + args['listall'] = True + ip_addresses = self.cs.listPublicIpAddresses(**args) + + if ip_addresses: + self.ip_address = ip_addresses['publicipaddress'][0] + return self._get_by_key(key, self.ip_address) + + + def get_zone(self, key=None): + if self.zone: + return self._get_by_key(key, self.zone) + + zone = self.module.params.get('zone') + zones = self.cs.listZones() + + # use the first zone if no zone param given + if not zone: + self.zone = zones['zone'][0] + return self._get_by_key(key, self.zone) + + if zones: + for z in zones['zone']: + if zone in [ z['name'], z['id'] ]: + self.zone = z + return self._get_by_key(key, self.zone) + self.module.fail_json(msg="zone '%s' not found" % zone) + + + def get_account(self, key=None): + if self.account: + return self._get_by_key(key, self.account) + + account = self.module.params.get('account') + if not account: + return None + + domain = self.module.params.get('domain') + if not domain: + self.module.fail_json(msg="Account must be specified with Domain") + + args = {} + args['name'] = account + args['domainid'] = self.get_domain(key='id') + args['listall'] = True + accounts = self.cs.listAccounts(**args) + if accounts: + self.account = accounts['account'][0] + return self._get_by_key(key, self.account) + self.module.fail_json(msg="Account '%s' not found" % account) + + + def get_domain(self, key=None): + if self.domain: + return self._get_by_key(key, self.domain) + + domain = self.module.params.get('domain') + if not domain: + return None + + args = {} + args['listall'] = True + domains = self.cs.listDomains(**args) + if domains: + for d in domains['domain']: + if d['path'].lower() in [ domain.lower(), "root/" + domain.lower(), "root" + domain.lower() ] : + self.domain = d + return self._get_by_key(key, self.domain) + self.module.fail_json(msg="Domain '%s' not found" % domain) + + + # TODO: for backward compatibility only, remove if not used anymore + def _poll_job(self, job=None, key=None): + return self.poll_job(job=job, key=key) + + + def poll_job(self, job=None, key=None): + if 'jobid' in job: + while True: + res = self.cs.queryAsyncJobResult(jobid=job['jobid']) + if res['jobstatus'] != 0 and 'jobresult' in res: + if 'errortext' in res['jobresult']: + self.module.fail_json(msg="Failed: '%s'" % res['jobresult']['errortext']) + if key and key in res['jobresult']: + job = res['jobresult'][key] + break + time.sleep(2) + return job + + +class AnsibleCloudStackIPAddress(AnsibleCloudStack): + + def __init__(self, module): + AnsibleCloudStack.__init__(self, module) + self.vm_default_nic = None + + def associate_ip_address(self): + self.result['changed'] = True + args = {} + args['account'] = self.get_account(key='id') + args['networkid'] = self.get_network(key='id') + args['zoneid'] = self.get_zone(key='id') + ip_address = {} + if not self.module.check_mode: + res = self.cs.associateIpAddress(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + + poll_async = self.module.params.get('poll_async') + if poll_async: + res = self._poll_job(res, 'ipaddress') + ip_address = res + return ip_address + + def disassociate_ip_address(self): + ip_address = self.get_ip_address() + if ip_address is None: + return ip_address + if ip_address['isstaticnat']: + self.module.fail_json(msg="IP address is allocated via static nat") + + self.result['changed'] = True + if not self.module.check_mode: + res = self.cs.disassociateIpAddress(id=ip_address['id']) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + poll_async = self.module.params.get('poll_async') + if poll_async: + res = self._poll_job(res, 'ipaddress') + return ip_address + + def get_result(self, ip_address): + if ip_address: + if 'zonename' in ip_address: + self.result['zone'] = ip_address['zonename'] + if 'domain' in ip_address: + self.result['domain'] = ip_address['domain'] + if 'account' in ip_address: + self.result['account'] = ip_address['account'] + if 'project' in ip_address: + self.result['project'] = ip_address['project'] + if 'ipaddress' in ip_address: + self.result['ip_address'] = ip_address['ipaddress'] + if 'id' in ip_address: + self.result['id'] = ip_address['id'] + return self.result + + +def main(): + module = AnsibleModule( + argument_spec = dict( + ip_address = dict(required=False), + state = dict(choices=['present', 'absent'], default='present'), + zone = dict(default=None), + domain = dict(default=None), + account = dict(default=None), + network = dict(default=None), + project = dict(default=None), + poll_async = dict(choices=BOOLEANS, default=True), + api_key = dict(default=None), + api_secret = dict(default=None, no_log=True), + api_url = dict(default=None), + api_http_method = dict(choices=['get', 'post'], default='get'), + api_timeout = dict(type='int', default=10), + ), + required_together = ( + ['api_key', 'api_secret', 'api_url'], + ), + supports_check_mode=True + ) + + if not has_lib_cs: + module.fail_json(msg="python library cs required: pip install cs") + + try: + acs_ip_address = AnsibleCloudStackIPAddress(module) + + state = module.params.get('state') + if state in ['absent']: + ip_address = acs_ip_address.disassociate_ip_address() + else: + ip_address = acs_ip_address.associate_ip_address() + + result = acs_ip_address.get_result(ip_address) + + except CloudStackException, e: + module.fail_json(msg='CloudStackException: %s' % str(e)) + + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From a2c81b198ed50038a4399f279efce2a0e83cd931 Mon Sep 17 00:00:00 2001 From: Darren Worrall Date: Mon, 10 Aug 2015 16:07:04 +0100 Subject: [PATCH 0578/2522] More relevant example --- cloud/cloudstack/cs_ip_address.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/cloudstack/cs_ip_address.py b/cloud/cloudstack/cs_ip_address.py index e63ae0b7ae0..a992b5d95f7 100644 --- a/cloud/cloudstack/cs_ip_address.py +++ b/cloud/cloudstack/cs_ip_address.py @@ -72,7 +72,7 @@ # Associate an IP address - local_action: module: cs_ip_address - account: My Account + network: My Network register: ip_address when: create_instance|changed From f13eb871c71e4559ba558326da5f3581ea45f816 Mon Sep 17 00:00:00 2001 From: Darren Worrall Date: Mon, 10 Aug 2015 16:32:13 +0100 Subject: [PATCH 0579/2522] Fix imports, override get_network and get_ip_address --- cloud/cloudstack/cs_ip_address.py | 197 +----------------------------- 1 file changed, 4 insertions(+), 193 deletions(-) diff --git a/cloud/cloudstack/cs_ip_address.py b/cloud/cloudstack/cs_ip_address.py index a992b5d95f7..c77681697b0 100644 --- a/cloud/cloudstack/cs_ip_address.py +++ b/cloud/cloudstack/cs_ip_address.py @@ -120,113 +120,12 @@ has_lib_cs = False # import cloudstack common -class AnsibleCloudStack: - - def __init__(self, module): - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - - self.result = { - 'changed': False, - } - - self.module = module - self._connect() - - self.domain = None - self.account = None - self.project = None - self.ip_address = None - self.zone = None - self.vm = None - self.os_type = None - self.hypervisor = None - self.capabilities = None - - - def _connect(self): - api_key = self.module.params.get('api_key') - api_secret = self.module.params.get('secret_key') - api_url = self.module.params.get('api_url') - api_http_method = self.module.params.get('api_http_method') - api_timeout = self.module.params.get('api_timeout') - - if api_key and api_secret and api_url: - self.cs = CloudStack( - endpoint=api_url, - key=api_key, - secret=api_secret, - timeout=api_timeout, - method=api_http_method - ) - else: - self.cs = CloudStack(**read_config()) - - - def get_or_fallback(self, key=None, fallback_key=None): - value = self.module.params.get(key) - if not value: - value = self.module.params.get(fallback_key) - return value - - - # TODO: for backward compatibility only, remove if not used anymore - def _has_changed(self, want_dict, current_dict, only_keys=None): - return self.has_changed(want_dict=want_dict, current_dict=current_dict, only_keys=only_keys) - - - def has_changed(self, want_dict, current_dict, only_keys=None): - for key, value in want_dict.iteritems(): - - # Optionally limit by a list of keys - if only_keys and key not in only_keys: - continue; - - # Skip None values - if value is None: - continue; - - if key in current_dict: - - # API returns string for int in some cases, just to make sure - if isinstance(value, int): - current_dict[key] = int(current_dict[key]) - elif isinstance(value, str): - current_dict[key] = str(current_dict[key]) - - # Only need to detect a singe change, not every item - if value != current_dict[key]: - return True - return False +from ansible.module_utils.cloudstack import * - def _get_by_key(self, key=None, my_dict={}): - if key: - if key in my_dict: - return my_dict[key] - self.module.fail_json(msg="Something went wrong: %s not found" % key) - return my_dict - - - def get_project(self, key=None): - if self.project: - return self._get_by_key(key, self.project) - - project = self.module.params.get('project') - if not project: - return None - args = {} - args['account'] = self.get_account(key='name') - args['domainid'] = self.get_domain(key='id') - projects = self.cs.listProjects(**args) - if projects: - for p in projects['project']: - if project.lower() in [ p['name'].lower(), p['id'] ]: - self.project = p - return self._get_by_key(key, self.project) - self.module.fail_json(msg="project '%s' not found" % project) - +class AnsibleCloudStackIPAddress(AnsibleCloudStack): + #TODO: Add to parent class, duplicated in cs_network def get_network(self, key=None, network=None): if not network: network = self.module.params.get('network') @@ -250,7 +149,7 @@ def get_network(self, key=None, network=None): break self.module.fail_json(msg="Network '%s' not found" % network) - + #TODO: Merge changes here with parent class def get_ip_address(self, key=None): if self.ip_address: return self._get_by_key(key, self.ip_address) @@ -271,94 +170,6 @@ def get_ip_address(self, key=None): self.ip_address = ip_addresses['publicipaddress'][0] return self._get_by_key(key, self.ip_address) - - def get_zone(self, key=None): - if self.zone: - return self._get_by_key(key, self.zone) - - zone = self.module.params.get('zone') - zones = self.cs.listZones() - - # use the first zone if no zone param given - if not zone: - self.zone = zones['zone'][0] - return self._get_by_key(key, self.zone) - - if zones: - for z in zones['zone']: - if zone in [ z['name'], z['id'] ]: - self.zone = z - return self._get_by_key(key, self.zone) - self.module.fail_json(msg="zone '%s' not found" % zone) - - - def get_account(self, key=None): - if self.account: - return self._get_by_key(key, self.account) - - account = self.module.params.get('account') - if not account: - return None - - domain = self.module.params.get('domain') - if not domain: - self.module.fail_json(msg="Account must be specified with Domain") - - args = {} - args['name'] = account - args['domainid'] = self.get_domain(key='id') - args['listall'] = True - accounts = self.cs.listAccounts(**args) - if accounts: - self.account = accounts['account'][0] - return self._get_by_key(key, self.account) - self.module.fail_json(msg="Account '%s' not found" % account) - - - def get_domain(self, key=None): - if self.domain: - return self._get_by_key(key, self.domain) - - domain = self.module.params.get('domain') - if not domain: - return None - - args = {} - args['listall'] = True - domains = self.cs.listDomains(**args) - if domains: - for d in domains['domain']: - if d['path'].lower() in [ domain.lower(), "root/" + domain.lower(), "root" + domain.lower() ] : - self.domain = d - return self._get_by_key(key, self.domain) - self.module.fail_json(msg="Domain '%s' not found" % domain) - - - # TODO: for backward compatibility only, remove if not used anymore - def _poll_job(self, job=None, key=None): - return self.poll_job(job=job, key=key) - - - def poll_job(self, job=None, key=None): - if 'jobid' in job: - while True: - res = self.cs.queryAsyncJobResult(jobid=job['jobid']) - if res['jobstatus'] != 0 and 'jobresult' in res: - if 'errortext' in res['jobresult']: - self.module.fail_json(msg="Failed: '%s'" % res['jobresult']['errortext']) - if key and key in res['jobresult']: - job = res['jobresult'][key] - break - time.sleep(2) - return job - - -class AnsibleCloudStackIPAddress(AnsibleCloudStack): - - def __init__(self, module): - AnsibleCloudStack.__init__(self, module) - self.vm_default_nic = None - def associate_ip_address(self): self.result['changed'] = True args = {} From 86bf938575b556e57508246cdd160abe57acc097 Mon Sep 17 00:00:00 2001 From: Darren Worrall Date: Mon, 10 Aug 2015 16:42:44 +0100 Subject: [PATCH 0580/2522] Doc updates --- cloud/cloudstack/cs_ip_address.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cloud/cloudstack/cs_ip_address.py b/cloud/cloudstack/cs_ip_address.py index c77681697b0..b635812694d 100644 --- a/cloud/cloudstack/cs_ip_address.py +++ b/cloud/cloudstack/cs_ip_address.py @@ -23,16 +23,15 @@ module: cs_ip_address short_description: Manages Public/Secondary IP address associations description: - - Acquires and associates a public IP to an account. Due to API limitations, - - this is not an idempotent call, so be sure to only conditionally call this - - when C(state=present) + - Acquires and associates a public IP to an account or project. Due to API + limitations this is not an idempotent call, so be sure to only + conditionally call this when C(state=present) version_added: '2.0' author: "Darren Worrall @dazworrall" options: ip_address: description: - - Public IP address. - - Required if C(state=absent) + - Public IP address. Required if C(state=absent) required: true domain: description: From 1382576100ee3b17f4eb28c7186d92376f370676 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 10 Aug 2015 13:25:13 -0400 Subject: [PATCH 0581/2522] fixed a few typos fixes #821 --- system/puppet.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/system/puppet.py b/system/puppet.py index 336b2c81108..48a497c37ce 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -55,12 +55,12 @@ required: false default: None facter_basename: - desciption: + description: - Basename of the facter output file required: false default: ansible environment: - desciption: + description: - Puppet environment to be used. required: false default: None @@ -161,9 +161,9 @@ def main(): base_cmd=base_cmd, ) if p['puppetmaster']: - cmd += " -- server %s" % pipes.quote(p['puppetmaster']) + cmd += " --server %s" % pipes.quote(p['puppetmaster']) if p['show_diff']: - cmd += " --show-diff" + cmd += " --show_diff" if p['environment']: cmd += " --environment '%s'" % p['environment'] if module.check_mode: From 7d791a8593bad3b30a2ec485886262183aaf21f5 Mon Sep 17 00:00:00 2001 From: Darren Worrall Date: Mon, 10 Aug 2015 19:24:16 +0100 Subject: [PATCH 0582/2522] More doc fixes --- cloud/cloudstack/cs_ip_address.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cloud/cloudstack/cs_ip_address.py b/cloud/cloudstack/cs_ip_address.py index b635812694d..d3513b651ed 100644 --- a/cloud/cloudstack/cs_ip_address.py +++ b/cloud/cloudstack/cs_ip_address.py @@ -32,7 +32,8 @@ ip_address: description: - Public IP address. Required if C(state=absent) - required: true + required: false + default: null domain: description: - Domain the IP address is related to. @@ -73,7 +74,7 @@ module: cs_ip_address network: My Network register: ip_address - when: create_instance|changed + when: instance.public_ip is undefined # Disassociate an IP address - local_action: From abe0bbd5e1958e03375db644dd20ed1da176a91f Mon Sep 17 00:00:00 2001 From: Darren Worrall Date: Mon, 10 Aug 2015 20:59:28 +0100 Subject: [PATCH 0583/2522] Param fixes to associateIpAddress --- cloud/cloudstack/cs_ip_address.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cloud/cloudstack/cs_ip_address.py b/cloud/cloudstack/cs_ip_address.py index d3513b651ed..1228c7aa46a 100644 --- a/cloud/cloudstack/cs_ip_address.py +++ b/cloud/cloudstack/cs_ip_address.py @@ -173,7 +173,9 @@ def get_ip_address(self, key=None): def associate_ip_address(self): self.result['changed'] = True args = {} - args['account'] = self.get_account(key='id') + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') + args['projectid'] = self.get_project(key='id') args['networkid'] = self.get_network(key='id') args['zoneid'] = self.get_zone(key='id') ip_address = {} From 51cd73fd676880d03a2261d174931a235329d795 Mon Sep 17 00:00:00 2001 From: Darren Worrall Date: Mon, 10 Aug 2015 21:02:13 +0100 Subject: [PATCH 0584/2522] Doc fixes --- cloud/cloudstack/cs_ip_address.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/cloudstack/cs_ip_address.py b/cloud/cloudstack/cs_ip_address.py index 1228c7aa46a..21fdcc885f6 100644 --- a/cloud/cloudstack/cs_ip_address.py +++ b/cloud/cloudstack/cs_ip_address.py @@ -56,7 +56,7 @@ default: null zone: description: - - Name of the zone in which the virtual machine is in. + - Name of the zone in which the IP address is in. - If not set, default zone is used. required: false default: null From 0ce060122ae72b1a7bf4a49950e0eb850efcdc76 Mon Sep 17 00:00:00 2001 From: Bill Nottingham Date: Mon, 10 Aug 2015 16:34:41 -0400 Subject: [PATCH 0585/2522] Add lldpctl to requirements. --- network/lldp.py | 1 + 1 file changed, 1 insertion(+) diff --git a/network/lldp.py b/network/lldp.py index 3ed554f79c3..fd1b1092d5e 100644 --- a/network/lldp.py +++ b/network/lldp.py @@ -19,6 +19,7 @@ DOCUMENTATION = ''' --- module: lldp +requirements: [ lldpctl ] version_added: 1.6 short_description: get details reported by lldp description: From e813c54e933e3b372ca2564aae9e88a761d969a8 Mon Sep 17 00:00:00 2001 From: Darren Worrall Date: Mon, 10 Aug 2015 21:59:20 +0100 Subject: [PATCH 0586/2522] Remove listall --- cloud/cloudstack/cs_ip_address.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cloud/cloudstack/cs_ip_address.py b/cloud/cloudstack/cs_ip_address.py index 21fdcc885f6..1dc5a18835e 100644 --- a/cloud/cloudstack/cs_ip_address.py +++ b/cloud/cloudstack/cs_ip_address.py @@ -163,7 +163,6 @@ def get_ip_address(self, key=None): args['account'] = self.get_account(key='name') args['domainid'] = self.get_domain(key='id') args['projectid'] = self.get_project(key='id') - args['listall'] = True ip_addresses = self.cs.listPublicIpAddresses(**args) if ip_addresses: From 51f3b9f6dda474aa7118e4fdc7bacc5fbd537a4f Mon Sep 17 00:00:00 2001 From: Milamber Date: Sun, 9 Aug 2015 12:36:35 +0100 Subject: [PATCH 0587/2522] Add an option to allow the resize of root disk when the instance is created. (need CloudStack 4.4 or +, cloud-initramfs-growroot installed on the template) Signed-off-by: Milamber --- cloud/cloudstack/cs_instance.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index f17269848e6..e71a465c9aa 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -107,6 +107,11 @@ - Disk size in GByte required if deploying instance from ISO. required: false default: null + root_disk_size: + description: + - Root disk size in GByte required if deploying instance with KVM hypervisor and want resize the root disk size at startup (need CloudStack >= 4.4, cloud-initramfs-growroot installed and enabled in the template) + required: false + default: null security_groups: description: - List of security groups the instance to be applied to. @@ -521,6 +526,7 @@ def deploy_instance(self): args['group'] = self.module.params.get('group') args['keypair'] = self.module.params.get('ssh_key') args['size'] = self.module.params.get('disk_size') + args['rootdisksize'] = self.module.params.get('root_disk_size') args['securitygroupnames'] = ','.join(self.module.params.get('security_groups')) args['affinitygroupnames'] = ','.join(self.module.params.get('affinity_groups')) @@ -789,6 +795,7 @@ def main(): ip6_address = dict(defaul=None), disk_offering = dict(default=None), disk_size = dict(type='int', default=None), + root_disk_size = dict(type='int', default=None), keyboard = dict(choices=['de', 'de-ch', 'es', 'fi', 'fr', 'fr-be', 'fr-ch', 'is', 'it', 'jp', 'nl-be', 'no', 'pt', 'uk', 'us'], default=None), hypervisor = dict(choices=['KVM', 'VMware', 'BareMetal', 'XenServer', 'LXC', 'HyperV', 'UCS', 'OVM'], default=None), security_groups = dict(type='list', aliases=[ 'security_group' ], default=[]), From d11182b80b90ae8d01b6b7fdb34a617e91ce9989 Mon Sep 17 00:00:00 2001 From: Darren Worrall Date: Tue, 11 Aug 2015 14:20:22 +0100 Subject: [PATCH 0588/2522] Add iptonetwork parameter --- cloud/cloudstack/cs_instance.py | 38 +++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index f17269848e6..b55fc1b97ee 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -97,6 +97,12 @@ - IPv6 address for default instance's network. required: false default: null + iptonetwork: + description: + - List of mappings in the form {'network': NetworkName, 'ip': 1.2.3.4} + - Mutually exclusive with C(networks) option. + required: false + default: null disk_offering: description: - Name of the disk offering to be used. @@ -209,6 +215,16 @@ - { key: admin, value: john } - { key: foo, value: bar } +# Create an instance with multiple interfaces specifying the IP addresses +- local_action: + module: cs_instance + name: web-vm-1 + template: Linux Debian 7 64-bit + service_offering: Tiny + iptonetwork: + - {'network': NetworkA, 'ip': '10.1.1.1'} + - {'network': NetworkB, 'ip': '192.168.1.1'} + # Ensure a instance has stopped - local_action: cs_instance name=web-vm-1 state=stopped @@ -448,9 +464,25 @@ def get_instance(self): break return self.instance + def get_iptonetwork_mappings(self): + network_mappings = self.module.params.get('iptonetwork') + if network_mappings is None: + return + + if network_mappings and self.module.params.get('networks'): + self.module.fail_json(msg="networks and iptonetwork are mutually exclusive.") + + network_names = [n['network'] for n in network_mappings] + ids = self.get_network_ids(network_names).split(',') + res = [] + for i, data in enumerate(network_mappings): + res.append({'networkid': ids[i], 'ip': data['ip']}) + return res + + def get_network_ids(self, network_names=None): + if network_names is None: + network_names = self.module.params.get('networks') - def get_network_ids(self): - network_names = self.module.params.get('networks') if not network_names: return None @@ -512,6 +544,7 @@ def deploy_instance(self): args['projectid'] = self.get_project(key='id') args['diskofferingid'] = self.get_disk_offering_id() args['networkids'] = self.get_network_ids() + args['iptonetworklist'] = self.get_iptonetwork_mappings() args['userdata'] = self.get_user_data() args['keyboard'] = self.module.params.get('keyboard') args['ipaddress'] = self.module.params.get('ip_address') @@ -785,6 +818,7 @@ def main(): template = dict(default=None), iso = dict(default=None), networks = dict(type='list', aliases=[ 'network' ], default=None), + iptonetwork = dict(type='list', default=None), ip_address = dict(defaul=None), ip6_address = dict(defaul=None), disk_offering = dict(default=None), From 2318009b709467b95c9eca576077cc636321d1e4 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Mon, 10 Aug 2015 15:51:20 +0200 Subject: [PATCH 0589/2522] cloudstack: cs_network fix zone not in result --- cloud/cloudstack/cs_network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/cloudstack/cs_network.py b/cloud/cloudstack/cs_network.py index c4fd51b7a0b..2ae731064a6 100644 --- a/cloud/cloudstack/cs_network.py +++ b/cloud/cloudstack/cs_network.py @@ -533,7 +533,7 @@ def get_result(self, network): self.result['type'] = network['type'] if 'traffictype' in network: self.result['traffic_type'] = network['traffictype'] - if 'zone' in network: + if 'zonename' in network: self.result['zone'] = network['zonename'] if 'domain' in network: self.result['domain'] = network['domain'] From aa14dedb8f60d506e284d30356a1179966a481d5 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Mon, 10 Aug 2015 15:52:03 +0200 Subject: [PATCH 0590/2522] cloudstack: sync cs_facts with best practices --- cloud/cloudstack/cs_facts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cloud/cloudstack/cs_facts.py b/cloud/cloudstack/cs_facts.py index e2bebf8b116..11230b4c229 100644 --- a/cloud/cloudstack/cs_facts.py +++ b/cloud/cloudstack/cs_facts.py @@ -218,4 +218,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.urls import * from ansible.module_utils.facts import * -main() +if __name__ == '__main__': + main() From 713cec442366aca784f94a606a1137776750b617 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Tue, 11 Aug 2015 16:10:49 +0200 Subject: [PATCH 0591/2522] cloudstack: cs_instance: use mutually_exlusive of AnsibleModule --- cloud/cloudstack/cs_instance.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index e71a465c9aa..a5ce8a8e5e7 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -385,9 +385,6 @@ def get_template_or_iso(self, key=None): if not template and not iso: self.module.fail_json(msg="Template or ISO is required.") - if template and iso: - self.module.fail_json(msg="Template are ISO are mutually exclusive.") - args = {} args['account'] = self.get_account(key='name') args['domainid'] = self.get_domain(key='id') @@ -815,6 +812,9 @@ def main(): api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), ), + mutually_exclusive = ( + ['template', 'iso'], + ), required_together = ( ['api_key', 'api_secret', 'api_url'], ), From ecfd18a94185d032b2383f0c638a95a73a58a258 Mon Sep 17 00:00:00 2001 From: Darren Worrall Date: Tue, 11 Aug 2015 15:33:20 +0100 Subject: [PATCH 0592/2522] Rename param to ip_to_networks --- cloud/cloudstack/cs_instance.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index b55fc1b97ee..89882108c47 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -97,7 +97,7 @@ - IPv6 address for default instance's network. required: false default: null - iptonetwork: + ip_to_networks: description: - List of mappings in the form {'network': NetworkName, 'ip': 1.2.3.4} - Mutually exclusive with C(networks) option. @@ -221,7 +221,7 @@ name: web-vm-1 template: Linux Debian 7 64-bit service_offering: Tiny - iptonetwork: + ip_to_networks: - {'network': NetworkA, 'ip': '10.1.1.1'} - {'network': NetworkB, 'ip': '192.168.1.1'} @@ -465,12 +465,12 @@ def get_instance(self): return self.instance def get_iptonetwork_mappings(self): - network_mappings = self.module.params.get('iptonetwork') + network_mappings = self.module.params.get('ip_to_networks') if network_mappings is None: return if network_mappings and self.module.params.get('networks'): - self.module.fail_json(msg="networks and iptonetwork are mutually exclusive.") + self.module.fail_json(msg="networks and ip_to_networks are mutually exclusive.") network_names = [n['network'] for n in network_mappings] ids = self.get_network_ids(network_names).split(',') @@ -818,7 +818,7 @@ def main(): template = dict(default=None), iso = dict(default=None), networks = dict(type='list', aliases=[ 'network' ], default=None), - iptonetwork = dict(type='list', default=None), + ip_to_networks = dict(type='list', default=None), ip_address = dict(defaul=None), ip6_address = dict(defaul=None), disk_offering = dict(default=None), From 53e447e38e0cc8451265b81189d13f084163cb6d Mon Sep 17 00:00:00 2001 From: Darren Worrall Date: Tue, 11 Aug 2015 15:41:07 +0100 Subject: [PATCH 0593/2522] Api tidy up --- cloud/cloudstack/cs_instance.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 89882108c47..6ca3a9f3b38 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -473,7 +473,7 @@ def get_iptonetwork_mappings(self): self.module.fail_json(msg="networks and ip_to_networks are mutually exclusive.") network_names = [n['network'] for n in network_mappings] - ids = self.get_network_ids(network_names).split(',') + ids = self.get_network_ids(network_names) res = [] for i, data in enumerate(network_mappings): res.append({'networkid': ids[i], 'ip': data['ip']}) @@ -508,7 +508,7 @@ def get_network_ids(self, network_names=None): if len(network_ids) != len(network_names): self.module.fail_json(msg="Could not find all networks, networks list found: %s" % network_displaytexts) - return ','.join(network_ids) + return network_ids def present_instance(self): @@ -534,6 +534,9 @@ def get_user_data(self): def deploy_instance(self): self.result['changed'] = True + networkids = self.get_network_ids() + if networkids is not None: + networkids = ','.join(networkids) args = {} args['templateid'] = self.get_template_or_iso(key='id') @@ -543,7 +546,7 @@ def deploy_instance(self): args['domainid'] = self.get_domain(key='id') args['projectid'] = self.get_project(key='id') args['diskofferingid'] = self.get_disk_offering_id() - args['networkids'] = self.get_network_ids() + args['networkids'] = networkids args['iptonetworklist'] = self.get_iptonetwork_mappings() args['userdata'] = self.get_user_data() args['keyboard'] = self.module.params.get('keyboard') From 2be506dbdf64b1d4367d5d3f9aec7fae822963fc Mon Sep 17 00:00:00 2001 From: Darren Worrall Date: Tue, 11 Aug 2015 15:42:55 +0100 Subject: [PATCH 0594/2522] Add alias --- cloud/cloudstack/cs_instance.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 6ca3a9f3b38..7bd06a797d9 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -103,6 +103,7 @@ - Mutually exclusive with C(networks) option. required: false default: null + aliases: [ 'ip_to_network' ] disk_offering: description: - Name of the disk offering to be used. @@ -821,7 +822,7 @@ def main(): template = dict(default=None), iso = dict(default=None), networks = dict(type='list', aliases=[ 'network' ], default=None), - ip_to_networks = dict(type='list', default=None), + ip_to_networks = dict(type='list', aliases=['ip_to_network'], default=None), ip_address = dict(defaul=None), ip6_address = dict(defaul=None), disk_offering = dict(default=None), From 9905034d3b4052619061c9c7e53ca0c1c137d0c6 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Tue, 11 Aug 2015 17:47:00 +0200 Subject: [PATCH 0595/2522] cloudstack: cs_ip_address: doc style fixes --- cloud/cloudstack/cs_ip_address.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/cloud/cloudstack/cs_ip_address.py b/cloud/cloudstack/cs_ip_address.py index 1dc5a18835e..3e9c0f7bf26 100644 --- a/cloud/cloudstack/cs_ip_address.py +++ b/cloud/cloudstack/cs_ip_address.py @@ -21,7 +21,7 @@ DOCUMENTATION = ''' --- module: cs_ip_address -short_description: Manages Public/Secondary IP address associations +short_description: Manages public IP address associations on Apache CloudStack based clouds. description: - Acquires and associates a public IP to an account or project. Due to API limitations this is not an idempotent call, so be sure to only @@ -31,7 +31,8 @@ options: ip_address: description: - - Public IP address. Required if C(state=absent) + - Public IP address. + - Required if C(state=absent) required: false default: null domain: @@ -69,7 +70,7 @@ ''' EXAMPLES = ''' -# Associate an IP address +# Associate an IP address conditonally - local_action: module: cs_ip_address network: My Network @@ -149,6 +150,7 @@ def get_network(self, key=None, network=None): break self.module.fail_json(msg="Network '%s' not found" % network) + #TODO: Merge changes here with parent class def get_ip_address(self, key=None): if self.ip_address: @@ -169,6 +171,7 @@ def get_ip_address(self, key=None): self.ip_address = ip_addresses['publicipaddress'][0] return self._get_by_key(key, self.ip_address) + def associate_ip_address(self): self.result['changed'] = True args = {} @@ -189,6 +192,7 @@ def associate_ip_address(self): ip_address = res return ip_address + def disassociate_ip_address(self): ip_address = self.get_ip_address() if ip_address is None: @@ -206,6 +210,7 @@ def disassociate_ip_address(self): res = self._poll_job(res, 'ipaddress') return ip_address + def get_result(self, ip_address): if ip_address: if 'zonename' in ip_address: From e31a4be192dcb3bb5a769e1f2f5dbda5fface5b6 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 13 Aug 2015 00:00:09 -0400 Subject: [PATCH 0596/2522] fixes to prevent doc brekage --- cloud/cloudstack/cs_instance.py | 2 +- clustering/znode | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 3cda4065f99..9d6dc847b4d 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -99,7 +99,7 @@ default: null ip_to_networks: description: - - List of mappings in the form {'network': NetworkName, 'ip': 1.2.3.4} + - "List of mappings in the form {'network': NetworkName, 'ip': 1.2.3.4}" - Mutually exclusive with C(networks) option. required: false default: null diff --git a/clustering/znode b/clustering/znode index 9e34a431e86..142836281ea 100644 --- a/clustering/znode +++ b/clustering/znode @@ -53,7 +53,6 @@ options: requirements: - kazoo >= 2.1 author: "Trey Perry (@treyperry)" ---- """ EXAMPLES = """ From dd1585cfc82b6ab09acb8e083d964b690a9093bf Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Fri, 14 Aug 2015 22:09:03 -0400 Subject: [PATCH 0597/2522] better short description for a10 modules --- network/a10/a10_service_group.py | 2 +- network/a10/a10_virtual_server.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/network/a10/a10_service_group.py b/network/a10/a10_service_group.py index db1c21bc78e..af664084b6a 100644 --- a/network/a10/a10_service_group.py +++ b/network/a10/a10_service_group.py @@ -25,7 +25,7 @@ --- module: a10_service_group version_added: 1.8 -short_description: Manage A10 Networks AX/SoftAX/Thunder/vThunder devices +short_description: Manage A10 Networks devices' service groups description: - Manage slb service-group objects on A10 Networks devices via aXAPI author: "Mischa Peters (@mischapeters)" diff --git a/network/a10/a10_virtual_server.py b/network/a10/a10_virtual_server.py index 2dbaa9121eb..1a04f1a1754 100644 --- a/network/a10/a10_virtual_server.py +++ b/network/a10/a10_virtual_server.py @@ -25,7 +25,7 @@ --- module: a10_virtual_server version_added: 1.8 -short_description: Manage A10 Networks AX/SoftAX/Thunder/vThunder devices +short_description: Manage A10 Networks devices' virtual servers description: - Manage slb virtual server objects on A10 Networks devices via aXAPI author: "Mischa Peters (@mischapeters)" From fa20898c2d8b8c7cec0412c2748043d21d970ab3 Mon Sep 17 00:00:00 2001 From: Chris Hoffman Date: Sun, 16 Aug 2015 12:13:20 -0400 Subject: [PATCH 0598/2522] Adding support for service ACLs in consul_acl module --- clustering/consul_acl.py | 109 ++++++++++++++++++++++++--------------- 1 file changed, 66 insertions(+), 43 deletions(-) diff --git a/clustering/consul_acl.py b/clustering/consul_acl.py index c133704b64d..e7890336b0f 100644 --- a/clustering/consul_acl.py +++ b/clustering/consul_acl.py @@ -22,7 +22,7 @@ short_description: "manipulate consul acl keys and rules" description: - allows the addition, modification and deletion of ACL keys and associated - rules in a consul cluster via the agent. For more details on using and + rules in a consul cluster via the agent. For more details on using and configuring ACLs, see https://www.consul.io/docs/internals/acl.html. requirements: - "python >= 2.6" @@ -57,7 +57,7 @@ required: false rules: description: - - an list of the rules that should be associated with a given key/token. + - an list of the rules that should be associated with a given token. required: false host: description: @@ -83,6 +83,19 @@ - key: 'private/foo' policy: deny + - name: create an acl with specific token with both key and serivce rules + consul_acl: + mgmt_token: 'some_management_acl' + name: 'Foo access' + token: 'some_client_token' + rules: + - key: 'foo' + policy: read + - service: '' + policy: write + - service: 'secret-' + policy: deny + - name: remove a token consul_acl: mgmt_token: 'some_management_acl' @@ -134,8 +147,6 @@ def update_acl(module): if token: existing_rules = load_rules_for_token(module, consul, token) supplied_rules = yml_to_rules(module, rules) - print existing_rules - print supplied_rules changed = not existing_rules == supplied_rules if changed: y = supplied_rules.to_hcl() @@ -148,7 +159,7 @@ def update_acl(module): try: rules = yml_to_rules(module, rules) if rules.are_rules(): - rules = rules.to_json() + rules = rules.to_hcl() else: rules = None @@ -163,7 +174,7 @@ def update_acl(module): module.fail_json(msg="Could not create/update acl %s" % e) module.exit_json(changed=changed, - token=token, + token=obfuscate_token(token), rules=rules, name=name, type=token_type) @@ -179,18 +190,20 @@ def remove_acl(module): if changed: token = consul.acl.destroy(token) - module.exit_json(changed=changed, token=token) + module.exit_json(changed=changed, token=obfuscate_token(token)) +def obfuscate_token(token): + return token[:4] + "*" * (len(token) - 5) def load_rules_for_token(module, consul_api, token): try: rules = Rules() info = consul_api.acl.info(token) if info and info['Rules']: - rule_set = to_ascii(info['Rules']) - for rule in hcl.loads(rule_set).values(): - for key, policy in rule.iteritems(): - rules.add_rule(Rule(key, policy['policy'])) + rule_set = hcl.loads(to_ascii(info['Rules'])) + for rule_type in rule_set: + for pattern, policy in rule_set[rule_type].iteritems(): + rules.add_rule(rule_type, Rule(pattern, policy['policy'])) return rules except Exception, e: module.fail_json( @@ -208,52 +221,61 @@ def yml_to_rules(module, yml_rules): rules = Rules() if yml_rules: for rule in yml_rules: - if not('key' in rule or 'policy' in rule): - module.fail_json(msg="a rule requires a key and a policy.") - rules.add_rule(Rule(rule['key'], rule['policy'])) + if ('key' in rule and 'policy' in rule): + rules.add_rule('key', Rule(rule['key'], rule['policy'])) + elif ('service' in rule and 'policy' in rule): + rules.add_rule('service', Rule(rule['service'], rule['policy'])) + else: + module.fail_json(msg="a rule requires a key/service and a policy.") return rules -template = '''key "%s" { +template = '''%s "%s" { policy = "%s" -}''' +} +''' + +RULE_TYPES = ['key', 'service'] class Rules: def __init__(self): self.rules = {} + for rule_type in RULE_TYPES: + self.rules[rule_type] = {} - def add_rule(self, rule): - self.rules[rule.key] = rule + def add_rule(self, rule_type, rule): + self.rules[rule_type][rule.pattern] = rule def are_rules(self): - return len(self.rules) > 0 - - def to_json(self): - rules = {} - for key, rule in self.rules.iteritems(): - rules[key] = {'policy': rule.policy} - return json.dumps({'keys': rules}) + return len(self) > 0 def to_hcl(self): rules = "" - for key, rule in self.rules.iteritems(): - rules += template % (key, rule.policy) - + for rule_type in RULE_TYPES: + for pattern, rule in self.rules[rule_type].iteritems(): + rules += template % (rule_type, pattern, rule.policy) return to_ascii(rules) + def __len__(self): + count = 0 + for rule_type in RULE_TYPES: + count += len(self.rules[rule_type]) + return count + def __eq__(self, other): if not (other or isinstance(other, self.__class__) - or len(other.rules) == len(self.rules)): + or len(other) == len(self)): return False - for name, other_rule in other.rules.iteritems(): - if not name in self.rules: - return False - rule = self.rules[name] + for rule_type in RULE_TYPES: + for name, other_rule in other.rules[rule_type].iteritems(): + if not name in self.rules[rule_type]: + return False + rule = self.rules[rule_type][name] - if not (rule and rule == other_rule): - return False + if not (rule and rule == other_rule): + return False return True def __str__(self): @@ -261,23 +283,24 @@ def __str__(self): class Rule: - def __init__(self, key, policy): - self.key = key + def __init__(self, pattern, policy): + self.pattern = pattern self.policy = policy def __eq__(self, other): return (isinstance(other, self.__class__) - and self.key == other.key + and self.pattern == other.pattern and self.policy == other.policy) + def __hash__(self): - return hash(self.key) ^ hash(self.policy) + return hash(self.pattern) ^ hash(self.policy) def __str__(self): - return '%s %s' % (self.key, self.policy) + return '%s %s' % (self.pattern, self.policy) def get_consul_api(module, token=None): if not token: - token = token = module.params.get('token') + token = module.params.get('token') return consul.Consul(host=module.params.get('host'), port=module.params.get('port'), token=token) @@ -286,7 +309,7 @@ def test_dependencies(module): if not python_consul_installed: module.fail_json(msg="python-consul required for this module. "\ "see http://python-consul.readthedocs.org/en/latest/#installation") - + if not pyhcl_installed: module.fail_json( msg="pyhcl required for this module."\ " see https://pypi.python.org/pypi/pyhcl") @@ -306,7 +329,7 @@ def main(): module = AnsibleModule(argument_spec, supports_check_mode=False) test_dependencies(module) - + try: execute(module) except ConnectionError, e: From e5b6d47a545027de87101833d3848097e37ea3fa Mon Sep 17 00:00:00 2001 From: Shayne Clausson Date: Mon, 17 Aug 2015 12:51:10 +0200 Subject: [PATCH 0599/2522] fixes issue where no range_key_name is defined https://github.com/ansible/ansible-modules-extras/issues/841 --- cloud/amazon/dynamodb_table.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/cloud/amazon/dynamodb_table.py b/cloud/amazon/dynamodb_table.py index c97ff6f0be0..29ba230fe48 100644 --- a/cloud/amazon/dynamodb_table.py +++ b/cloud/amazon/dynamodb_table.py @@ -143,10 +143,15 @@ def create_or_update_dynamo_table(connection, module): read_capacity = module.params.get('read_capacity') write_capacity = module.params.get('write_capacity') - schema = [ - HashKey(hash_key_name, DYNAMO_TYPE_MAP.get(hash_key_type)), - RangeKey(range_key_name, DYNAMO_TYPE_MAP.get(range_key_type)) - ] + if range_key_name: + schema = [ + HashKey(hash_key_name, DYNAMO_TYPE_MAP.get(hash_key_type)), + RangeKey(range_key_name, DYNAMO_TYPE_MAP.get(range_key_type)) + ] + else: + schema = [ + HashKey(hash_key_name, DYNAMO_TYPE_MAP.get(hash_key_type)) + ] throughput = { 'read': read_capacity, 'write': write_capacity From d4f22de62d9eb14329a453d3c3a066b665cef40c Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 17 Aug 2015 12:08:46 -0400 Subject: [PATCH 0600/2522] made seport 2.4 compatible --- system/seport.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/system/seport.py b/system/seport.py index c264334ae86..fb1cef661a2 100644 --- a/system/seport.py +++ b/system/seport.py @@ -141,15 +141,15 @@ def semanage_port_add(module, ports, proto, setype, do_reload, serange='s0', ses seport.add(port, proto, serange, setype) change = change or not exists - except ValueError as e: + except ValueError, e: module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) - except IOError as e: + except IOError, e: module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) - except KeyError as e: + except KeyError, e: module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) - except OSError as e: + except OSError, e: module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) - except RuntimeError as e: + except RuntimeError, e: module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) return change @@ -186,15 +186,15 @@ def semanage_port_del(module, ports, proto, do_reload, sestore=''): seport.delete(port, proto) change = change or not exists - except ValueError as e: + except ValueError, e: module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) - except IOError as e: + except IOError,e: module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) - except KeyError as e: + except KeyError, e: module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) - except OSError as e: + except OSError, e: module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) - except RuntimeError as e: + except RuntimeError, e: module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) return change From df235b4d92620a668611a363c7f382c6c4ab00e7 Mon Sep 17 00:00:00 2001 From: sirkubax Date: Mon, 17 Aug 2015 19:08:18 +0200 Subject: [PATCH 0601/2522] QuickFix of issue 813 --- packaging/language/maven_artifact.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packaging/language/maven_artifact.py b/packaging/language/maven_artifact.py index 55dfbd33de5..5f20a9af169 100644 --- a/packaging/language/maven_artifact.py +++ b/packaging/language/maven_artifact.py @@ -285,7 +285,7 @@ def main(): artifact_id = dict(default=None), version = dict(default=None), classifier = dict(default=None), - extension = dict(default=None), + extension = dict(default=None, required=True), repository_url = dict(default=None), username = dict(default=None), password = dict(default=None), @@ -309,7 +309,8 @@ def main(): if not repository_url: repository_url = "http://repo1.maven.org/maven2" - downloader = MavenDownloader(module, repository_url, repository_username, repository_password) + #downloader = MavenDownloader(module, repository_url, repository_username, repository_password) + downloader = MavenDownloader(module, repository_url) try: artifact = Artifact(group_id, artifact_id, version, classifier, extension) From 4be1b3e2ab19ac0eb56e521d44d2b4b4510af87f Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Wed, 12 Aug 2015 21:53:49 +0200 Subject: [PATCH 0602/2522] cloudstack: cs_template: fix state=absent must not need vm, url only for state=present, fixes example. --- cloud/cloudstack/cs_template.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/cloud/cloudstack/cs_template.py b/cloud/cloudstack/cs_template.py index 11e2cc747cc..9539442d06e 100644 --- a/cloud/cloudstack/cs_template.py +++ b/cloud/cloudstack/cs_template.py @@ -596,9 +596,6 @@ def main(): ['api_key', 'api_secret', 'api_url'], ['format', 'url', 'hypervisor'], ), - required_one_of = ( - ['url', 'vm'], - ), supports_check_mode=True ) @@ -612,11 +609,12 @@ def main(): if state in ['absent']: tpl = acs_tpl.remove_template() else: - url = module.params.get('url') - if url: + if module.params.get('url'): tpl = acs_tpl.register_template() - else: + elif module.params.get('vm'): tpl = acs_tpl.create_template() + else: + module.fail_json(msg="one of the following is required on state=present: url,vm") result = acs_tpl.get_result(tpl) From 2e52f11dc376190765ea83e8bd411315fdce2fdb Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Mon, 17 Aug 2015 08:30:11 +0200 Subject: [PATCH 0603/2522] cloudstack: use new get_result() handling --- cloud/cloudstack/cs_account.py | 21 ++++--- cloud/cloudstack/cs_affinitygroup.py | 25 +++----- cloud/cloudstack/cs_domain.py | 23 +++---- cloud/cloudstack/cs_firewall.py | 31 ++++----- cloud/cloudstack/cs_instance.py | 63 +++++-------------- cloud/cloudstack/cs_instancegroup.py | 21 +------ cloud/cloudstack/cs_ip_address.py | 29 ++++----- cloud/cloudstack/cs_iso.py | 36 ++++------- cloud/cloudstack/cs_network.py | 76 ++++++---------------- cloud/cloudstack/cs_portforward.py | 54 ++++++++-------- cloud/cloudstack/cs_project.py | 27 +------- cloud/cloudstack/cs_securitygroup.py | 15 ++--- cloud/cloudstack/cs_securitygroup_rule.py | 36 +++++------ cloud/cloudstack/cs_sshkeypair.py | 21 +++---- cloud/cloudstack/cs_staticnat.py | 33 ++++------ cloud/cloudstack/cs_template.py | 77 +++++++---------------- cloud/cloudstack/cs_vmsnapshot.py | 35 +++-------- 17 files changed, 207 insertions(+), 416 deletions(-) diff --git a/cloud/cloudstack/cs_account.py b/cloud/cloudstack/cs_account.py index 8196196a1a9..1ce6fdde88f 100644 --- a/cloud/cloudstack/cs_account.py +++ b/cloud/cloudstack/cs_account.py @@ -139,6 +139,11 @@ RETURN = ''' --- +id: + description: UUID of the account. + returned: success + type: string + sample: 87b1e0ce-4e01-11e4-bb66-0050569e64b8 name: description: Name of the account. returned: success @@ -149,7 +154,7 @@ returned: success type: string sample: user -account_state: +state: description: State of the account. returned: success type: string @@ -179,7 +184,10 @@ class AnsibleCloudStackAccount(AnsibleCloudStack): def __init__(self, module): - AnsibleCloudStack.__init__(self, module) + super(AnsibleCloudStackAccount, self).__init__(module) + self.returns = { + 'networkdomain': 'network_domain', + } self.account = None self.account_types = { 'user': 0, @@ -328,20 +336,13 @@ def absent_account(self): def get_result(self, account): + super(AnsibleCloudStackAccount, self).get_result(account) if account: - if 'name' in account: - self.result['name'] = account['name'] if 'accounttype' in account: for key,value in self.account_types.items(): if value == account['accounttype']: self.result['account_type'] = key break - if 'state' in account: - self.result['account_state'] = account['state'] - if 'domain' in account: - self.result['domain'] = account['domain'] - if 'networkdomain' in account: - self.result['network_domain'] = account['networkdomain'] return self.result diff --git a/cloud/cloudstack/cs_affinitygroup.py b/cloud/cloudstack/cs_affinitygroup.py index cfd76816e1b..40b764aa8ef 100644 --- a/cloud/cloudstack/cs_affinitygroup.py +++ b/cloud/cloudstack/cs_affinitygroup.py @@ -81,6 +81,11 @@ RETURN = ''' --- +id: + description: UUID of the affinity group. + returned: success + type: string + sample: 87b1e0ce-4e01-11e4-bb66-0050569e64b8 name: description: Name of affinity group. returned: success @@ -111,7 +116,10 @@ class AnsibleCloudStackAffinityGroup(AnsibleCloudStack): def __init__(self, module): - AnsibleCloudStack.__init__(self, module) + super(AnsibleCloudStackAffinityGroup, self).__init__(module) + self.returns = { + 'type': 'affinity_type', + } self.affinity_group = None @@ -192,21 +200,6 @@ def remove_affinity_group(self): return affinity_group - def get_result(self, affinity_group): - if affinity_group: - if 'name' in affinity_group: - self.result['name'] = affinity_group['name'] - if 'description' in affinity_group: - self.result['description'] = affinity_group['description'] - if 'type' in affinity_group: - self.result['affinity_type'] = affinity_group['type'] - if 'domain' in affinity_group: - self.result['domain'] = affinity_group['domain'] - if 'account' in affinity_group: - self.result['account'] = affinity_group['account'] - return self.result - - def main(): module = AnsibleModule( argument_spec = dict( diff --git a/cloud/cloudstack/cs_domain.py b/cloud/cloudstack/cs_domain.py index 3b048eddbb5..c9f345a00c2 100644 --- a/cloud/cloudstack/cs_domain.py +++ b/cloud/cloudstack/cs_domain.py @@ -80,7 +80,7 @@ RETURN = ''' --- id: - description: ID of the domain. + description: UUID of the domain. returned: success type: string sample: 87b1e0ce-4e01-11e4-bb66-0050569e64b8 @@ -119,7 +119,12 @@ class AnsibleCloudStackDomain(AnsibleCloudStack): def __init__(self, module): - AnsibleCloudStack.__init__(self, module) + super(AnsibleCloudStackDomain, self).__init__(module) + self.returns = { + 'path': 'path', + 'networkdomain': 'network_domain', + 'parentdomainname': 'parent_domain', + } self.domain = None @@ -232,20 +237,6 @@ def absent_domain(self): return domain - def get_result(self, domain): - if domain: - if 'id' in domain: - self.result['id'] = domain['id'] - if 'name' in domain: - self.result['name'] = domain['name'] - if 'path' in domain: - self.result['path'] = domain['path'] - if 'parentdomainname' in domain: - self.result['parent_domain'] = domain['parentdomainname'] - if 'networkdomain' in domain: - self.result['network_domain'] = domain['networkdomain'] - return self.result - def main(): module = AnsibleModule( diff --git a/cloud/cloudstack/cs_firewall.py b/cloud/cloudstack/cs_firewall.py index 27350eab91b..e52683d7a67 100644 --- a/cloud/cloudstack/cs_firewall.py +++ b/cloud/cloudstack/cs_firewall.py @@ -152,6 +152,11 @@ RETURN = ''' --- +id: + description: UUID of the rule. + returned: success + type: string + sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6 ip_address: description: IP address of the rule if C(type=ingress) returned: success @@ -212,7 +217,16 @@ class AnsibleCloudStackFirewall(AnsibleCloudStack): def __init__(self, module): - AnsibleCloudStack.__init__(self, module) + super(AnsibleCloudStackFirewall, self).__init__(module) + self.returns = { + 'cidrlist': 'cidr', + 'startport': 'start_port', + 'endpoint': 'end_port', + 'protocol': 'protocol', + 'ipaddress': 'ip_address', + 'icmpcode': 'icmp_code', + 'icmptype': 'icmp_type', + } self.firewall_rule = None @@ -369,22 +383,9 @@ def remove_firewall_rule(self): def get_result(self, firewall_rule): + super(AnsibleCloudStackFirewall, self).get_result(firewall_rule) if firewall_rule: self.result['type'] = self.module.params.get('type') - if 'cidrlist' in firewall_rule: - self.result['cidr'] = firewall_rule['cidrlist'] - if 'startport' in firewall_rule: - self.result['start_port'] = int(firewall_rule['startport']) - if 'endport' in firewall_rule: - self.result['end_port'] = int(firewall_rule['endport']) - if 'protocol' in firewall_rule: - self.result['protocol'] = firewall_rule['protocol'] - if 'ipaddress' in firewall_rule: - self.result['ip_address'] = firewall_rule['ipaddress'] - if 'icmpcode' in firewall_rule: - self.result['icmp_code'] = int(firewall_rule['icmpcode']) - if 'icmptype' in firewall_rule: - self.result['icmp_type'] = int(firewall_rule['icmptype']) if 'networkid' in firewall_rule: self.result['network'] = self.get_network(key='displaytext', network=firewall_rule['networkid']) return self.result diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 9d6dc847b4d..ae9cf3ae70c 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -244,7 +244,7 @@ RETURN = ''' --- id: - description: ID of the instance. + description: UUID of the instance. returned: success type: string sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6 @@ -375,7 +375,20 @@ class AnsibleCloudStackInstance(AnsibleCloudStack): def __init__(self, module): - AnsibleCloudStack.__init__(self, module) + super(AnsibleCloudStackInstance, self).__init__(module) + self.returns = { + 'group': 'group', + 'hypervisor': 'hypervisor', + 'instancename': 'instance_name', + 'publicip': 'public_ip', + 'passwordenabled': 'password_enabled', + 'password': 'password', + 'serviceofferingname': 'service_offering', + 'isoname': 'iso', + 'templatename': 'template', + 'keypair': 'ssh_key', + 'securitygroup': 'security_group', + } self.instance = None self.template = None self.iso = None @@ -752,52 +765,8 @@ def restart_instance(self): def get_result(self, instance): + super(AnsibleCloudStackInstance, self).get_result(instance) if instance: - if 'id' in instance: - self.result['id'] = instance['id'] - if 'name' in instance: - self.result['name'] = instance['name'] - if 'displayname' in instance: - self.result['display_name'] = instance['displayname'] - if 'group' in instance: - self.result['group'] = instance['group'] - if 'domain' in instance: - self.result['domain'] = instance['domain'] - if 'account' in instance: - self.result['account'] = instance['account'] - if 'project' in instance: - self.result['project'] = instance['project'] - if 'hypervisor' in instance: - self.result['hypervisor'] = instance['hypervisor'] - if 'instancename' in instance: - self.result['instance_name'] = instance['instancename'] - if 'publicip' in instance: - self.result['public_ip'] = instance['publicip'] - if 'passwordenabled' in instance: - self.result['password_enabled'] = instance['passwordenabled'] - if 'password' in instance: - self.result['password'] = instance['password'] - if 'serviceofferingname' in instance: - self.result['service_offering'] = instance['serviceofferingname'] - if 'zonename' in instance: - self.result['zone'] = instance['zonename'] - if 'templatename' in instance: - self.result['template'] = instance['templatename'] - if 'isoname' in instance: - self.result['iso'] = instance['isoname'] - if 'keypair' in instance: - self.result['ssh_key'] = instance['keypair'] - if 'created' in instance: - self.result['created'] = instance['created'] - if 'state' in instance: - self.result['state'] = instance['state'] - if 'tags' in instance: - self.result['tags'] = [] - for tag in instance['tags']: - result_tag = {} - result_tag['key'] = tag['key'] - result_tag['value'] = tag['value'] - self.result['tags'].append(result_tag) if 'securitygroup' in instance: security_groups = [] for securitygroup in instance['securitygroup']: diff --git a/cloud/cloudstack/cs_instancegroup.py b/cloud/cloudstack/cs_instancegroup.py index 7280ceff5ea..537d9d90b28 100644 --- a/cloud/cloudstack/cs_instancegroup.py +++ b/cloud/cloudstack/cs_instancegroup.py @@ -71,7 +71,7 @@ RETURN = ''' --- id: - description: ID of the instance group. + description: UUID of the instance group. returned: success type: string sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6 @@ -115,7 +115,7 @@ class AnsibleCloudStackInstanceGroup(AnsibleCloudStack): def __init__(self, module): - AnsibleCloudStack.__init__(self, module) + super(AnsibleCloudStackInstanceGroup, self).__init__(module) self.instance_group = None @@ -169,23 +169,6 @@ def absent_instance_group(self): return instance_group - def get_result(self, instance_group): - if instance_group: - if 'id' in instance_group: - self.result['id'] = instance_group['id'] - if 'created' in instance_group: - self.result['created'] = instance_group['created'] - if 'name' in instance_group: - self.result['name'] = instance_group['name'] - if 'project' in instance_group: - self.result['project'] = instance_group['project'] - if 'domain' in instance_group: - self.result['domain'] = instance_group['domain'] - if 'account' in instance_group: - self.result['account'] = instance_group['account'] - return self.result - - def main(): module = AnsibleModule( argument_spec = dict( diff --git a/cloud/cloudstack/cs_ip_address.py b/cloud/cloudstack/cs_ip_address.py index 3e9c0f7bf26..e9507f855ed 100644 --- a/cloud/cloudstack/cs_ip_address.py +++ b/cloud/cloudstack/cs_ip_address.py @@ -86,6 +86,11 @@ RETURN = ''' --- +id: + description: UUID of the Public IP address. + returned: success + type: string + sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f ip_address: description: Public IP address. returned: success @@ -126,6 +131,13 @@ class AnsibleCloudStackIPAddress(AnsibleCloudStack): + def __init__(self, module): + super(AnsibleCloudStackIPAddress, self).__init__(module) + self.returns = { + 'ipaddress': 'ip_address', + } + + #TODO: Add to parent class, duplicated in cs_network def get_network(self, key=None, network=None): if not network: @@ -211,23 +223,6 @@ def disassociate_ip_address(self): return ip_address - def get_result(self, ip_address): - if ip_address: - if 'zonename' in ip_address: - self.result['zone'] = ip_address['zonename'] - if 'domain' in ip_address: - self.result['domain'] = ip_address['domain'] - if 'account' in ip_address: - self.result['account'] = ip_address['account'] - if 'project' in ip_address: - self.result['project'] = ip_address['project'] - if 'ipaddress' in ip_address: - self.result['ip_address'] = ip_address['ipaddress'] - if 'id' in ip_address: - self.result['id'] = ip_address['id'] - return self.result - - def main(): module = AnsibleModule( argument_spec = dict( diff --git a/cloud/cloudstack/cs_iso.py b/cloud/cloudstack/cs_iso.py index 7030e4be607..62986502a64 100644 --- a/cloud/cloudstack/cs_iso.py +++ b/cloud/cloudstack/cs_iso.py @@ -140,6 +140,11 @@ RETURN = ''' --- +id: + description: UUID of the ISO. + returned: success + type: string + sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f name: description: Name of the ISO. returned: success @@ -205,7 +210,12 @@ class AnsibleCloudStackIso(AnsibleCloudStack): def __init__(self, module): - AnsibleCloudStack.__init__(self, module) + super(AnsibleCloudStackIso, self).__init__(module) + self.returns = { + 'checksum': 'checksum', + 'status': 'status', + 'isready': 'is_ready', + } self.iso = None def register_iso(self): @@ -283,30 +293,6 @@ def remove_iso(self): return iso - def get_result(self, iso): - if iso: - if 'displaytext' in iso: - self.result['displaytext'] = iso['displaytext'] - if 'name' in iso: - self.result['name'] = iso['name'] - if 'zonename' in iso: - self.result['zone'] = iso['zonename'] - if 'checksum' in iso: - self.result['checksum'] = iso['checksum'] - if 'status' in iso: - self.result['status'] = iso['status'] - if 'isready' in iso: - self.result['is_ready'] = iso['isready'] - if 'created' in iso: - self.result['created'] = iso['created'] - if 'project' in iso: - self.result['project'] = iso['project'] - if 'domain' in iso: - self.result['domain'] = iso['domain'] - if 'account' in iso: - self.result['account'] = iso['account'] - return self.result - def main(): module = AnsibleModule( diff --git a/cloud/cloudstack/cs_network.py b/cloud/cloudstack/cs_network.py index 2ae731064a6..538f62896ae 100644 --- a/cloud/cloudstack/cs_network.py +++ b/cloud/cloudstack/cs_network.py @@ -197,7 +197,7 @@ RETURN = ''' --- id: - description: ID of the network. + description: UUID of the network. returned: success type: string sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6 @@ -331,7 +331,24 @@ class AnsibleCloudStackNetwork(AnsibleCloudStack): def __init__(self, module): - AnsibleCloudStack.__init__(self, module) + super(AnsibleCloudStackNetwork, self).__init__(module) + self.returns = { + 'networkdomain': 'network domain', + 'networkofferingname': 'network_offering', + 'ispersistent': 'is_persistent', + 'acltype': 'acl_type', + 'type': 'type', + 'traffictype': 'traffic_type', + 'ip6gateway': 'gateway_ipv6', + 'ip6cidr': 'cidr_ipv6', + 'gateway': 'gateway', + 'cidr': 'cidr', + 'netmask': 'netmask', + 'broadcastdomaintype': 'broadcast_domaintype', + 'dns1': 'dns1', + 'dns2': 'dns2', + } + self.network = None @@ -503,61 +520,6 @@ def absent_network(self): return network - def get_result(self, network): - if network: - if 'id' in network: - self.result['id'] = network['id'] - if 'name' in network: - self.result['name'] = network['name'] - if 'displaytext' in network: - self.result['displaytext'] = network['displaytext'] - if 'dns1' in network: - self.result['dns1'] = network['dns1'] - if 'dns2' in network: - self.result['dns2'] = network['dns2'] - if 'cidr' in network: - self.result['cidr'] = network['cidr'] - if 'broadcastdomaintype' in network: - self.result['broadcast_domaintype'] = network['broadcastdomaintype'] - if 'netmask' in network: - self.result['netmask'] = network['netmask'] - if 'gateway' in network: - self.result['gateway'] = network['gateway'] - if 'ip6cidr' in network: - self.result['cidr_ipv6'] = network['ip6cidr'] - if 'ip6gateway' in network: - self.result['gateway_ipv6'] = network['ip6gateway'] - if 'state' in network: - self.result['state'] = network['state'] - if 'type' in network: - self.result['type'] = network['type'] - if 'traffictype' in network: - self.result['traffic_type'] = network['traffictype'] - if 'zonename' in network: - self.result['zone'] = network['zonename'] - if 'domain' in network: - self.result['domain'] = network['domain'] - if 'account' in network: - self.result['account'] = network['account'] - if 'project' in network: - self.result['project'] = network['project'] - if 'acltype' in network: - self.result['acl_type'] = network['acltype'] - if 'networkdomain' in network: - self.result['network_domain'] = network['networkdomain'] - if 'networkofferingname' in network: - self.result['network_offering'] = network['networkofferingname'] - if 'ispersistent' in network: - self.result['is_persistent'] = network['ispersistent'] - if 'tags' in network: - self.result['tags'] = [] - for tag in network['tags']: - result_tag = {} - result_tag['key'] = tag['key'] - result_tag['value'] = tag['value'] - self.result['tags'].append(result_tag) - return self.result - def main(): module = AnsibleModule( diff --git a/cloud/cloudstack/cs_portforward.py b/cloud/cloudstack/cs_portforward.py index d1b8db4d65a..2fc14aa5ed3 100644 --- a/cloud/cloudstack/cs_portforward.py +++ b/cloud/cloudstack/cs_portforward.py @@ -148,6 +148,11 @@ RETURN = ''' --- +id: + description: UUID of the public IP address. + returned: success + type: string + sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f ip_address: description: Public IP address. returned: success @@ -212,7 +217,22 @@ class AnsibleCloudStackPortforwarding(AnsibleCloudStack): def __init__(self, module): - AnsibleCloudStack.__init__(self, module) + super(AnsibleCloudStackPortforwarding, self).__init__(module) + self.returns = { + 'virtualmachinedisplayname': 'vm_display_name', + 'virtualmachinename': 'vm_name', + 'ipaddress': 'ip_address', + 'vmguestip': 'vm_guest_ip', + 'publicip': 'public_ip', + 'protocol': 'protocol', + } + # these values will be casted to int + self.returns_to_int = { + 'publicport': 'public_port', + 'publicendport': 'public_end_port', + 'privateport': 'private_port', + 'private_end_port': 'private_end_port', + } self.portforwarding_rule = None self.vm_default_nic = None @@ -338,34 +358,12 @@ def absent_portforwarding_rule(self): def get_result(self, portforwarding_rule): + super(AnsibleCloudStackPortforwarding, self).get_result(portforwarding_rule) if portforwarding_rule: - if 'id' in portforwarding_rule: - self.result['id'] = portforwarding_rule['id'] - if 'virtualmachinedisplayname' in portforwarding_rule: - self.result['vm_display_name'] = portforwarding_rule['virtualmachinedisplayname'] - if 'virtualmachinename' in portforwarding_rule: - self.result['vm_name'] = portforwarding_rule['virtualmachinename'] - if 'ipaddress' in portforwarding_rule: - self.result['ip_address'] = portforwarding_rule['ipaddress'] - if 'vmguestip' in portforwarding_rule: - self.result['vm_guest_ip'] = portforwarding_rule['vmguestip'] - if 'publicport' in portforwarding_rule: - self.result['public_port'] = int(portforwarding_rule['publicport']) - if 'publicendport' in portforwarding_rule: - self.result['public_end_port'] = int(portforwarding_rule['publicendport']) - if 'privateport' in portforwarding_rule: - self.result['private_port'] = int(portforwarding_rule['privateport']) - if 'privateendport' in portforwarding_rule: - self.result['private_end_port'] = int(portforwarding_rule['privateendport']) - if 'protocol' in portforwarding_rule: - self.result['protocol'] = portforwarding_rule['protocol'] - if 'tags' in portforwarding_rule: - self.result['tags'] = [] - for tag in portforwarding_rule['tags']: - result_tag = {} - result_tag['key'] = tag['key'] - result_tag['value'] = tag['value'] - self.result['tags'].append(result_tag) + # Bad bad API does not always return int when it should. + for search_key, return_key in returns_to_int.iteritems(): + if search_key in resource: + self.result[return_key] = int(resource[search_key]) return self.result diff --git a/cloud/cloudstack/cs_project.py b/cloud/cloudstack/cs_project.py index 896232f3053..0bd886ad7e3 100644 --- a/cloud/cloudstack/cs_project.py +++ b/cloud/cloudstack/cs_project.py @@ -95,7 +95,7 @@ RETURN = ''' --- id: - description: ID of the project. + description: UUID of the project. returned: success type: string sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6 @@ -143,10 +143,6 @@ class AnsibleCloudStackProject(AnsibleCloudStack): - def __init__(self, module): - AnsibleCloudStack.__init__(self, module) - self.project = None - def get_project(self): if not self.project: @@ -261,27 +257,6 @@ def absent_project(self): return project - def get_result(self, project): - if project: - if 'name' in project: - self.result['name'] = project['name'] - if 'displaytext' in project: - self.result['displaytext'] = project['displaytext'] - if 'account' in project: - self.result['account'] = project['account'] - if 'domain' in project: - self.result['domain'] = project['domain'] - if 'state' in project: - self.result['state'] = project['state'] - if 'tags' in project: - self.result['tags'] = [] - for tag in project['tags']: - result_tag = {} - result_tag['key'] = tag['key'] - result_tag['value'] = tag['value'] - self.result['tags'].append(result_tag) - return self.result - def main(): module = AnsibleModule( diff --git a/cloud/cloudstack/cs_securitygroup.py b/cloud/cloudstack/cs_securitygroup.py index a6827f6f811..f54de925936 100644 --- a/cloud/cloudstack/cs_securitygroup.py +++ b/cloud/cloudstack/cs_securitygroup.py @@ -66,6 +66,11 @@ RETURN = ''' --- +id: + description: UUID of the security group. + returned: success + type: string + sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f name: description: Name of security group. returned: success @@ -91,7 +96,7 @@ class AnsibleCloudStackSecurityGroup(AnsibleCloudStack): def __init__(self, module): - AnsibleCloudStack.__init__(self, module) + super(AnsibleCloudStackSecurityGroup, self).__init__(module) self.security_group = None @@ -145,14 +150,6 @@ def remove_security_group(self): return security_group - def get_result(self, security_group): - if security_group: - if 'name' in security_group: - self.result['name'] = security_group['name'] - if 'description' in security_group: - self.result['description'] = security_group['description'] - return self.result - def main(): module = AnsibleModule( diff --git a/cloud/cloudstack/cs_securitygroup_rule.py b/cloud/cloudstack/cs_securitygroup_rule.py index 65bd7fd5640..877cc5324d5 100644 --- a/cloud/cloudstack/cs_securitygroup_rule.py +++ b/cloud/cloudstack/cs_securitygroup_rule.py @@ -139,6 +139,11 @@ RETURN = ''' --- +id: + description: UUID of the of the rule. + returned: success + type: string + sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f security_group: description: security group of the rule. returned: success @@ -189,7 +194,16 @@ class AnsibleCloudStackSecurityGroupRule(AnsibleCloudStack): def __init__(self, module): - AnsibleCloudStack.__init__(self, module) + super(AnsibleCloudStackSecurityGroupRule, self).__init__(module) + self.returns = { + 'icmptype': 'icmp_type', + 'icmpcode': 'icmp_code', + 'endport': 'end_port', + 'start_port': 'start_port', + 'protocol': 'protocol', + 'cidr': 'cidr', + 'securitygroupname': 'user_security_group', + } def _tcp_udp_match(self, rule, protocol, start_port, end_port): @@ -349,29 +363,13 @@ def remove_rule(self): def get_result(self, security_group_rule): - + super(AnsibleCloudStackSecurityGroupRule, self).get_result(security_group_rule) self.result['type'] = self.module.params.get('type') self.result['security_group'] = self.module.params.get('security_group') - - if security_group_rule: - rule = security_group_rule - if 'securitygroupname' in rule: - self.result['user_security_group'] = rule['securitygroupname'] - if 'cidr' in rule: - self.result['cidr'] = rule['cidr'] - if 'protocol' in rule: - self.result['protocol'] = rule['protocol'] - if 'startport' in rule: - self.result['start_port'] = rule['startport'] - if 'endport' in rule: - self.result['end_port'] = rule['endport'] - if 'icmpcode' in rule: - self.result['icmp_code'] = rule['icmpcode'] - if 'icmptype' in rule: - self.result['icmp_type'] = rule['icmptype'] return self.result + def main(): module = AnsibleModule( argument_spec = dict( diff --git a/cloud/cloudstack/cs_sshkeypair.py b/cloud/cloudstack/cs_sshkeypair.py index 28c6b3802b4..ebd906f7d5c 100644 --- a/cloud/cloudstack/cs_sshkeypair.py +++ b/cloud/cloudstack/cs_sshkeypair.py @@ -77,6 +77,11 @@ RETURN = ''' --- +id: + description: UUID of the SSH public key. + returned: success + type: string + sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f name: description: Name of the SSH public key. returned: success @@ -112,7 +117,11 @@ class AnsibleCloudStackSshKey(AnsibleCloudStack): def __init__(self, module): - AnsibleCloudStack.__init__(self, module) + super(AnsibleCloudStackSshKey, self).__init__(module) + self.returns = { + 'privatekey': 'private_key', + 'fingerprint': 'fingerprint', + } self.ssh_key = None @@ -189,16 +198,6 @@ def get_ssh_key(self): return self.ssh_key - def get_result(self, ssh_key): - if ssh_key: - if 'fingerprint' in ssh_key: - self.result['fingerprint'] = ssh_key['fingerprint'] - if 'name' in ssh_key: - self.result['name'] = ssh_key['name'] - if 'privatekey' in ssh_key: - self.result['private_key'] = ssh_key['privatekey'] - return self.result - def _get_ssh_fingerprint(self, public_key): key = sshpubkeys.SSHKey(public_key) diff --git a/cloud/cloudstack/cs_staticnat.py b/cloud/cloudstack/cs_staticnat.py index 5761a3990e9..500c533915b 100644 --- a/cloud/cloudstack/cs_staticnat.py +++ b/cloud/cloudstack/cs_staticnat.py @@ -93,6 +93,11 @@ RETURN = ''' --- +id: + description: UUID of the ip_address. + returned: success + type: string + sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f ip_address: description: Public IP address. returned: success @@ -149,7 +154,13 @@ class AnsibleCloudStackStaticNat(AnsibleCloudStack): def __init__(self, module): - AnsibleCloudStack.__init__(self, module) + super(AnsibleCloudStackPortforwarding, self).__init__(module) + self.returns = { + 'virtualmachinedisplayname': 'vm_display_name', + 'virtualmachinename': 'vm_name', + 'ipaddress': 'ip_address', + 'vmipaddress': 'vm_guest_ip', + } self.vm_default_nic = None @@ -246,26 +257,6 @@ def absent_static_nat(self): return ip_address - def get_result(self, ip_address): - if ip_address: - if 'zonename' in ip_address: - self.result['zone'] = ip_address['zonename'] - if 'domain' in ip_address: - self.result['domain'] = ip_address['domain'] - if 'account' in ip_address: - self.result['account'] = ip_address['account'] - if 'project' in ip_address: - self.result['project'] = ip_address['project'] - if 'virtualmachinedisplayname' in ip_address: - self.result['vm_display_name'] = ip_address['virtualmachinedisplayname'] - if 'virtualmachinename' in ip_address: - self.result['vm'] = ip_address['virtualmachinename'] - if 'vmipaddress' in ip_address: - self.result['vm_guest_ip'] = ip_address['vmipaddress'] - if 'ipaddress' in ip_address: - self.result['ip_address'] = ip_address['ipaddress'] - return self.result - def main(): module = AnsibleModule( diff --git a/cloud/cloudstack/cs_template.py b/cloud/cloudstack/cs_template.py index 9539442d06e..e53ec8825b8 100644 --- a/cloud/cloudstack/cs_template.py +++ b/cloud/cloudstack/cs_template.py @@ -219,6 +219,11 @@ RETURN = ''' --- +id: + description: UUID of the template. + returned: success + type: string + sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f name: description: Name of the template. returned: success @@ -344,7 +349,23 @@ class AnsibleCloudStackTemplate(AnsibleCloudStack): def __init__(self, module): - AnsibleCloudStack.__init__(self, module) + super(AnsibleCloudStackTemplate, self).__init__(module) + self.returns = { + 'checksum': 'checksum', + 'status': 'status', + 'isready': 'is_ready', + 'templatetag': 'template_tag', + 'sshkeyenabled': 'sshkey_enabled', + 'passwordenabled': 'password_enabled', + 'tempaltetype': 'template_type', + 'ostypename': 'os_type', + 'crossZones': 'cross_zones', + 'isextractable': 'is_extractable', + 'isfeatured': 'is_featured', + 'ispublic': 'is_public', + 'format': 'format', + 'hypervisor': 'hypervisor', + } def _get_args(self): @@ -497,60 +518,6 @@ def remove_template(self): return template - def get_result(self, template): - if template: - if 'displaytext' in template: - self.result['displaytext'] = template['displaytext'] - if 'name' in template: - self.result['name'] = template['name'] - if 'hypervisor' in template: - self.result['hypervisor'] = template['hypervisor'] - if 'zonename' in template: - self.result['zone'] = template['zonename'] - if 'checksum' in template: - self.result['checksum'] = template['checksum'] - if 'format' in template: - self.result['format'] = template['format'] - if 'isready' in template: - self.result['is_ready'] = template['isready'] - if 'ispublic' in template: - self.result['is_public'] = template['ispublic'] - if 'isfeatured' in template: - self.result['is_featured'] = template['isfeatured'] - if 'isextractable' in template: - self.result['is_extractable'] = template['isextractable'] - # and yes! it is really camelCase! - if 'crossZones' in template: - self.result['cross_zones'] = template['crossZones'] - if 'ostypename' in template: - self.result['os_type'] = template['ostypename'] - if 'templatetype' in template: - self.result['template_type'] = template['templatetype'] - if 'passwordenabled' in template: - self.result['password_enabled'] = template['passwordenabled'] - if 'sshkeyenabled' in template: - self.result['sshkey_enabled'] = template['sshkeyenabled'] - if 'status' in template: - self.result['status'] = template['status'] - if 'created' in template: - self.result['created'] = template['created'] - if 'templatetag' in template: - self.result['template_tag'] = template['templatetag'] - if 'tags' in template: - self.result['tags'] = [] - for tag in template['tags']: - result_tag = {} - result_tag['key'] = tag['key'] - result_tag['value'] = tag['value'] - self.result['tags'].append(result_tag) - if 'domain' in template: - self.result['domain'] = template['domain'] - if 'account' in template: - self.result['account'] = template['account'] - if 'project' in template: - self.result['project'] = template['project'] - return self.result - def main(): module = AnsibleModule( diff --git a/cloud/cloudstack/cs_vmsnapshot.py b/cloud/cloudstack/cs_vmsnapshot.py index 62dec7ca35d..4e4ff992784 100644 --- a/cloud/cloudstack/cs_vmsnapshot.py +++ b/cloud/cloudstack/cs_vmsnapshot.py @@ -105,6 +105,11 @@ RETURN = ''' --- +id: + description: UUID of the snapshot. + returned: success + type: string + sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f name: description: Name of the snapshot. returned: success @@ -171,7 +176,11 @@ class AnsibleCloudStackVmSnapshot(AnsibleCloudStack): def __init__(self, module): - AnsibleCloudStack.__init__(self, module) + super(AnsibleCloudStackVmSnapshot, self).__init__(module) + self.returns = { + 'type': 'type', + 'current': 'current', + } def get_snapshot(self): @@ -247,30 +256,6 @@ def revert_vm_to_snapshot(self): self.module.fail_json(msg="snapshot not found, could not revert VM") - def get_result(self, snapshot): - if snapshot: - if 'displayname' in snapshot: - self.result['displayname'] = snapshot['displayname'] - if 'created' in snapshot: - self.result['created'] = snapshot['created'] - if 'current' in snapshot: - self.result['current'] = snapshot['current'] - if 'state' in snapshot: - self.result['state'] = snapshot['state'] - if 'type' in snapshot: - self.result['type'] = snapshot['type'] - if 'name' in snapshot: - self.result['name'] = snapshot['name'] - if 'description' in snapshot: - self.result['description'] = snapshot['description'] - if 'domain' in snapshot: - self.result['domain'] = snapshot['domain'] - if 'account' in snapshot: - self.result['account'] = snapshot['account'] - if 'project' in snapshot: - self.result['project'] = snapshot['project'] - return self.result - def main(): module = AnsibleModule( From 24ae49bbd9c8343eefe6b807063afbd4a10c8abd Mon Sep 17 00:00:00 2001 From: "Christopher M. Fuhrman" Date: Tue, 18 Aug 2015 13:32:17 -0700 Subject: [PATCH 0604/2522] pkgin: Support multiple matching packages pkgin searches for packages such as 'emacs' can return multiple matches, the first of which is not guaranteed to match. So, iterate through found packages until we have an appropriate match. Should we *not* find a match, then return False indicating match failure. --- packaging/os/pkgin.py | 68 ++++++++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/packaging/os/pkgin.py b/packaging/os/pkgin.py index e600026409b..0f2714b6c74 100644 --- a/packaging/os/pkgin.py +++ b/packaging/os/pkgin.py @@ -68,6 +68,7 @@ import os import sys import pipes +import re def query_package(module, pkgin_path, name): """Search for the package by name. @@ -95,33 +96,46 @@ def query_package(module, pkgin_path, name): # rc will not be 0 unless the search was a success if rc == 0: - # Get first line - line = out.split('\n')[0] - - # Break up line at spaces. The first part will be the package with its - # version (e.g. 'gcc47-libs-4.7.2nb4'), and the second will be the state - # of the package: - # '' - not installed - # '<' - installed but out of date - # '=' - installed and up to date - # '>' - installed but newer than the repository version - pkgname_with_version, raw_state = out.split(splitchar)[0:2] - - # Strip version - # (results in sth like 'gcc47-libs') - pkgname_without_version = '-'.join(pkgname_with_version.split('-')[:-1]) - - if name != pkgname_without_version: - return False - # no fall-through - - # The package was found; now return its state - if raw_state == '<': - return 'outdated' - elif raw_state == '=' or raw_state == '>': - return 'present' - else: - return False + # Search results may contain more than one line (e.g., 'emacs'), so iterate + # through each line to see if we have a match. + packages = out.split('\n') + + for package in packages: + + # Break up line at spaces. The first part will be the package with its + # version (e.g. 'gcc47-libs-4.7.2nb4'), and the second will be the state + # of the package: + # '' - not installed + # '<' - installed but out of date + # '=' - installed and up to date + # '>' - installed but newer than the repository version + pkgname_with_version, raw_state = package.split(splitchar)[0:2] + + # Search for package, stripping version + # (results in sth like 'gcc47-libs' or 'emacs24-nox11') + pkg_search_obj = re.search(r'^([a-zA-Z]+[0-9]*[\-]*\w*)-[0-9]', pkgname_with_version, re.M) + + # Do not proceed unless we have a match + if not pkg_search_obj: + continue + + # Grab matched string + pkgname_without_version = pkg_search_obj.group(1) + + if name != pkgname_without_version: + continue + + # The package was found; now return its state + if raw_state == '<': + return 'outdated' + elif raw_state == '=' or raw_state == '>': + return 'present' + else: + return False + # no fall-through + + # No packages were matched, so return False + return False def format_action_message(module, action, count): From e861e587d79f35a244487dab5ff51d85d92c242e Mon Sep 17 00:00:00 2001 From: Chris Hoffman Date: Tue, 18 Aug 2015 19:21:07 -0400 Subject: [PATCH 0605/2522] Removing token obfuscation --- clustering/consul_acl.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/clustering/consul_acl.py b/clustering/consul_acl.py index e7890336b0f..e5d06814ebc 100644 --- a/clustering/consul_acl.py +++ b/clustering/consul_acl.py @@ -174,7 +174,7 @@ def update_acl(module): module.fail_json(msg="Could not create/update acl %s" % e) module.exit_json(changed=changed, - token=obfuscate_token(token), + token=token, rules=rules, name=name, type=token_type) @@ -190,10 +190,7 @@ def remove_acl(module): if changed: token = consul.acl.destroy(token) - module.exit_json(changed=changed, token=obfuscate_token(token)) - -def obfuscate_token(token): - return token[:4] + "*" * (len(token) - 5) + module.exit_json(changed=changed, token=token) def load_rules_for_token(module, consul_api, token): try: From b95abe0ddd609e169f3b428f63550e0ccf01e1d0 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Wed, 19 Aug 2015 21:51:34 +0200 Subject: [PATCH 0606/2522] cloudstack: rename displaytext, displayname to dislplay_... for consistency --- cloud/cloudstack/cs_iso.py | 2 +- cloud/cloudstack/cs_network.py | 14 +++++++------- cloud/cloudstack/cs_project.py | 16 ++++++++-------- cloud/cloudstack/cs_template.py | 14 ++++++-------- cloud/cloudstack/cs_vmsnapshot.py | 11 +++++------ 5 files changed, 27 insertions(+), 30 deletions(-) diff --git a/cloud/cloudstack/cs_iso.py b/cloud/cloudstack/cs_iso.py index 62986502a64..37f110cbe68 100644 --- a/cloud/cloudstack/cs_iso.py +++ b/cloud/cloudstack/cs_iso.py @@ -150,7 +150,7 @@ returned: success type: string sample: Debian 7 64-bit -displaytext: +display_text: description: Text to be displayed of the ISO. returned: success type: string diff --git a/cloud/cloudstack/cs_network.py b/cloud/cloudstack/cs_network.py index 538f62896ae..6076c1a1316 100644 --- a/cloud/cloudstack/cs_network.py +++ b/cloud/cloudstack/cs_network.py @@ -31,10 +31,10 @@ description: - Name (case sensitive) of the network. required: true - displaytext: + display_text: description: - - Displaytext of the network. - - If not specified, C(name) will be used as displaytext. + - Display text of the network. + - If not specified, C(name) will be used as C(display_text). required: false default: null network_offering: @@ -177,7 +177,7 @@ - local_action: module: cs_network name: my network - displaytext: network of domain example.local + display_text: network of domain example.local network_domain: example.local # restart a network with clean up @@ -206,7 +206,7 @@ returned: success type: string sample: web project -displaytext: +display_text: description: Display text of the network. returned: success type: string @@ -390,7 +390,7 @@ def get_network_offering(self, key=None): def _get_args(self): args = {} args['name'] = self.module.params.get('name') - args['displaytext'] = self.get_or_fallback('displaytext', 'name') + args['displaytext'] = self.get_or_fallback('display_text', 'name') args['networkdomain'] = self.module.params.get('network_domain') args['networkofferingid'] = self.get_network_offering(key='id') return args @@ -525,7 +525,7 @@ def main(): module = AnsibleModule( argument_spec = dict( name = dict(required=True), - displaytext = dict(default=None), + display_text = dict(default=None), network_offering = dict(default=None), zone = dict(default=None), start_ip = dict(default=None), diff --git a/cloud/cloudstack/cs_project.py b/cloud/cloudstack/cs_project.py index 0bd886ad7e3..6a48956bb1c 100644 --- a/cloud/cloudstack/cs_project.py +++ b/cloud/cloudstack/cs_project.py @@ -31,10 +31,10 @@ description: - Name of the project. required: true - displaytext: + display_text: description: - - Displaytext of the project. - - If not specified, C(name) will be used as displaytext. + - Display text of the project. + - If not specified, C(name) will be used as C(display_text). required: false default: null state: @@ -71,7 +71,7 @@ - local_action: module: cs_project name: web - displaytext: my web project + display_text: my web project # Suspend an existing project - local_action: @@ -104,7 +104,7 @@ returned: success type: string sample: web project -displaytext: +display_text: description: Display text of the project. returned: success type: string @@ -173,7 +173,7 @@ def present_project(self): def update_project(self, project): args = {} args['id'] = project['id'] - args['displaytext'] = self.get_or_fallback('displaytext', 'name') + args['displaytext'] = self.get_or_fallback('display_text', 'name') if self._has_changed(args, project): self.result['changed'] = True @@ -194,7 +194,7 @@ def create_project(self, project): args = {} args['name'] = self.module.params.get('name') - args['displaytext'] = self.get_or_fallback('displaytext', 'name') + args['displaytext'] = self.get_or_fallback('display_text', 'name') args['account'] = self.get_account('name') args['domainid'] = self.get_domain('id') @@ -262,7 +262,7 @@ def main(): module = AnsibleModule( argument_spec = dict( name = dict(required=True), - displaytext = dict(default=None), + display_text = dict(default=None), state = dict(choices=['present', 'absent', 'active', 'suspended' ], default='present'), domain = dict(default=None), account = dict(default=None), diff --git a/cloud/cloudstack/cs_template.py b/cloud/cloudstack/cs_template.py index e53ec8825b8..8e5b8c24905 100644 --- a/cloud/cloudstack/cs_template.py +++ b/cloud/cloudstack/cs_template.py @@ -158,9 +158,9 @@ - 32 or 64 bits support. required: false default: '64' - displaytext: + display_text: description: - - the display text of the template. + - Display text of the template. required: true default: null state: @@ -229,8 +229,8 @@ returned: success type: string sample: Debian 7 64-bit -displaytext: - description: Displaytext of the template. +display_text: + description: Display text of the template. returned: success type: string sample: Debian 7.7 64-bit minimal 2015-03-19 @@ -371,7 +371,7 @@ def __init__(self, module): def _get_args(self): args = {} args['name'] = self.module.params.get('name') - args['displaytext'] = self.module.params.get('displaytext') + args['displaytext'] = self.get_or_fallback('display_text', 'name') args['bits'] = self.module.params.get('bits') args['isdynamicallyscalable'] = self.module.params.get('is_dynamically_scalable') args['isextractable'] = self.module.params.get('is_extractable') @@ -385,8 +385,6 @@ def _get_args(self): if not args['ostypeid']: self.module.fail_json(msg="Missing required arguments: os_type") - if not args['displaytext']: - args['displaytext'] = self.module.params.get('name') return args @@ -523,7 +521,7 @@ def main(): module = AnsibleModule( argument_spec = dict( name = dict(required=True), - displaytext = dict(default=None), + display_text = dict(default=None), url = dict(default=None), vm = dict(default=None), snapshot = dict(default=None), diff --git a/cloud/cloudstack/cs_vmsnapshot.py b/cloud/cloudstack/cs_vmsnapshot.py index 4e4ff992784..c9e815e4730 100644 --- a/cloud/cloudstack/cs_vmsnapshot.py +++ b/cloud/cloudstack/cs_vmsnapshot.py @@ -29,9 +29,9 @@ options: name: description: - - Unique Name of the snapshot. In CloudStack terms C(displayname). + - Unique Name of the snapshot. In CloudStack terms display name. required: true - aliases: ['displayname'] + aliases: ['display_name'] vm: description: - Name of the virtual machine. @@ -115,8 +115,8 @@ returned: success type: string sample: snapshot before update -displayname: - description: displayname of the snapshot. +display_name: + description: Display name of the snapshot. returned: success type: string sample: snapshot before update @@ -141,7 +141,6 @@ type: string sample: DiskAndMemory description: - description: description: description of vm snapshot returned: success type: string @@ -260,7 +259,7 @@ def revert_vm_to_snapshot(self): def main(): module = AnsibleModule( argument_spec = dict( - name = dict(required=True, aliases=['displayname']), + name = dict(required=True, aliases=['display_name']), vm = dict(required=True), description = dict(default=None), zone = dict(default=None), From 28ad84b87a1978389c21008d49364e4a39f44108 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Wed, 19 Aug 2015 21:53:07 +0200 Subject: [PATCH 0607/2522] cloudstack: add Simulator as hypervisor --- cloud/cloudstack/cs_instance.py | 2 +- cloud/cloudstack/cs_template.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index ae9cf3ae70c..201449b870d 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -801,7 +801,7 @@ def main(): disk_size = dict(type='int', default=None), root_disk_size = dict(type='int', default=None), keyboard = dict(choices=['de', 'de-ch', 'es', 'fi', 'fr', 'fr-be', 'fr-ch', 'is', 'it', 'jp', 'nl-be', 'no', 'pt', 'uk', 'us'], default=None), - hypervisor = dict(choices=['KVM', 'VMware', 'BareMetal', 'XenServer', 'LXC', 'HyperV', 'UCS', 'OVM'], default=None), + hypervisor = dict(choices=['KVM', 'VMware', 'BareMetal', 'XenServer', 'LXC', 'HyperV', 'UCS', 'OVM', 'Simulator'], default=None), security_groups = dict(type='list', aliases=[ 'security_group' ], default=[]), affinity_groups = dict(type='list', aliases=[ 'affinity_group' ], default=[]), domain = dict(default=None), diff --git a/cloud/cloudstack/cs_template.py b/cloud/cloudstack/cs_template.py index 8e5b8c24905..d451ece7138 100644 --- a/cloud/cloudstack/cs_template.py +++ b/cloud/cloudstack/cs_template.py @@ -534,7 +534,7 @@ def main(): is_routing = dict(type='bool', choices=BOOLEANS, default=False), checksum = dict(default=None), template_filter = dict(default='self', choices=['featured', 'self', 'selfexecutable', 'sharedexecutable', 'executable', 'community']), - hypervisor = dict(choices=['KVM', 'VMware', 'BareMetal', 'XenServer', 'LXC', 'HyperV', 'UCS', 'OVM'], default=None), + hypervisor = dict(choices=['KVM', 'VMware', 'BareMetal', 'XenServer', 'LXC', 'HyperV', 'UCS', 'OVM', 'Simulator'], default=None), requires_hvm = dict(type='bool', choices=BOOLEANS, default=False), password_enabled = dict(type='bool', choices=BOOLEANS, default=False), template_tag = dict(default=None), From e8df87375df6118af3401ba870d927e80aaf4c27 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Wed, 19 Aug 2015 21:53:47 +0200 Subject: [PATCH 0608/2522] cloudstack: cs_security_group_rule: fix typo --- cloud/cloudstack/cs_securitygroup_rule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/cloudstack/cs_securitygroup_rule.py b/cloud/cloudstack/cs_securitygroup_rule.py index 877cc5324d5..c17923daca7 100644 --- a/cloud/cloudstack/cs_securitygroup_rule.py +++ b/cloud/cloudstack/cs_securitygroup_rule.py @@ -199,7 +199,7 @@ def __init__(self, module): 'icmptype': 'icmp_type', 'icmpcode': 'icmp_code', 'endport': 'end_port', - 'start_port': 'start_port', + 'startport': 'start_port', 'protocol': 'protocol', 'cidr': 'cidr', 'securitygroupname': 'user_security_group', From 9bdefef05fc41b4e8bb137c9e92e2210a47049e4 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Wed, 19 Aug 2015 21:55:56 +0200 Subject: [PATCH 0609/2522] cloudstack: cs_network: rename broadcast_domaintype to broadcast_domain_type for consistency --- cloud/cloudstack/cs_network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/cloudstack/cs_network.py b/cloud/cloudstack/cs_network.py index 6076c1a1316..cab24bdfefe 100644 --- a/cloud/cloudstack/cs_network.py +++ b/cloud/cloudstack/cs_network.py @@ -281,7 +281,7 @@ returned: success type: string sample: Account -broadcast_domaintype: +broadcast_domain_type: description: Broadcast domain type of the network. returned: success type: string @@ -344,7 +344,7 @@ def __init__(self, module): 'gateway': 'gateway', 'cidr': 'cidr', 'netmask': 'netmask', - 'broadcastdomaintype': 'broadcast_domaintype', + 'broadcastdomaintype': 'broadcast_domain_type', 'dns1': 'dns1', 'dns2': 'dns2', } From 2ca201feaa3b491e53dcc8d420e171eb167a652a Mon Sep 17 00:00:00 2001 From: Andreas Skarmutsos Lindh Date: Thu, 20 Aug 2015 09:57:46 +0200 Subject: [PATCH 0610/2522] cpanm: add installdeps option --- packaging/language/cpanm.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packaging/language/cpanm.py b/packaging/language/cpanm.py index 3749fd29db2..b6bdf0c67a0 100644 --- a/packaging/language/cpanm.py +++ b/packaging/language/cpanm.py @@ -58,6 +58,11 @@ - Use the mirror's index file instead of the CPAN Meta DB required: false default: false + installdeps: + description: + - Only install dependencies + required: false + default: false notes: - Please note that U(http://search.cpan.org/dist/App-cpanminus/bin/cpanm, cpanm) must be installed on the remote host. author: "Franck Cuny (@franckcuny)" @@ -91,10 +96,11 @@ def _is_package_installed(module, name, locallib, cpanm): res, stdout, stderr = module.run_command(cmd, check_rc=False) if res == 0: return True - else: + else: return False -def _build_cmd_line(name, from_path, notest, locallib, mirror, mirror_only, cpanm): +def _build_cmd_line(name, from_path, notest, locallib, mirror, mirror_only, + installdeps, cpanm): # this code should use "%s" like everything else and just return early but not fixing all of it now. # don't copy stuff like this if from_path: @@ -114,6 +120,9 @@ def _build_cmd_line(name, from_path, notest, locallib, mirror, mirror_only, cpan if mirror_only is True: cmd = "{cmd} --mirror-only".format(cmd=cmd) + if installdeps is True: + cmd = "{cmd} --installdeps".format(cmd=cmd) + return cmd @@ -125,6 +134,7 @@ def main(): locallib=dict(default=None, required=False), mirror=dict(default=None, required=False), mirror_only=dict(default=False, type='bool'), + installdeps=dict(default=False, type='bool'), ) module = AnsibleModule( @@ -139,6 +149,7 @@ def main(): locallib = module.params['locallib'] mirror = module.params['mirror'] mirror_only = module.params['mirror_only'] + installdeps = module.params['installdeps'] changed = False @@ -146,7 +157,8 @@ def main(): if not installed: out_cpanm = err_cpanm = '' - cmd = _build_cmd_line(name, from_path, notest, locallib, mirror, mirror_only, cpanm) + cmd = _build_cmd_line(name, from_path, notest, locallib, mirror, + mirror_only, installdeps, cpanm) rc_cpanm, out_cpanm, err_cpanm = module.run_command(cmd, check_rc=False) From ee395ced4deb664fae0413138c2fd697dfce385e Mon Sep 17 00:00:00 2001 From: gfrank Date: Thu, 20 Aug 2015 11:20:40 -0400 Subject: [PATCH 0611/2522] Clean up unneeded comment and comma --- windows/win_nssm.ps1 | 2 +- windows/win_nssm.py | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/windows/win_nssm.ps1 b/windows/win_nssm.ps1 index 088914f4060..f99eec66f0a 100644 --- a/windows/win_nssm.ps1 +++ b/windows/win_nssm.ps1 @@ -98,7 +98,7 @@ Function Service-Exists [string]$name ) - return ,[bool](Get-Service "$name" -ErrorAction SilentlyContinue) + return [bool](Get-Service "$name" -ErrorAction SilentlyContinue) } Function Nssm-Remove diff --git a/windows/win_nssm.py b/windows/win_nssm.py index cadb90c5d38..86d343b1fde 100644 --- a/windows/win_nssm.py +++ b/windows/win_nssm.py @@ -80,11 +80,6 @@ author: "Adam Keech (@smadam813), George Frank (@georgefrank)" ''' -# TODO: -# * Better parsing when a package has dependencies - currently fails -# * Time each item that is run -# * Support 'changed' with gems - would require shelling out to `gem list` first and parsing, kinda defeating the point of using chocolatey. - EXAMPLES = ''' # Install and start the foo service win_nssm: From e44ba01b182345937ce033c65f02229d420eff72 Mon Sep 17 00:00:00 2001 From: "Michael J. Schultz" Date: Thu, 20 Aug 2015 11:11:36 -0500 Subject: [PATCH 0612/2522] Add EC2 ELB Facts module to gather facts about ELBs! --- cloud/amazon/ec2_elb_facts.py | 198 ++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 cloud/amazon/ec2_elb_facts.py diff --git a/cloud/amazon/ec2_elb_facts.py b/cloud/amazon/ec2_elb_facts.py new file mode 100644 index 00000000000..b586842a485 --- /dev/null +++ b/cloud/amazon/ec2_elb_facts.py @@ -0,0 +1,198 @@ +#!/usr/bin/python +# +# This is a free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This Ansible library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this library. If not, see . + +DOCUMENTATION = ''' +--- +module: ec2_elb_facts +short_description: Gather facts about EC2 Elastic Load Balancers in AWS +description: + - Gather facts about EC2 Elastic Load Balancers in AWS +version_added: "2.0" +author: "Michael Schultz (github.com/mjschultz)" +options: + name: + description: + - List of ELB names to gather facts about. Pass this option to gather facts about a set of ELBs, otherwise, all ELBs are returned. + required: false + default: null + aliases: ['elb_id'] +extends_documentation_fragment: aws +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. +# Output format tries to match ec2_elb_lb module input parameters + +# Gather facts about all ELBs +- action: + module: ec2_elb_facts + register: elb_facts + +- action: + module: debug + msg: "{{ item.dns_name }}" + with_items: elb_facts.elbs + +# Gather facts about a particular ELB +- action: + module: ec2_elb_facts + name: frontend-prod-elb + register: elb_facts + +- action: + module: debug + msg: "{{ elb_facts.elbs.0.dns_name }}" + +# Gather facts about a set of ELBs +- action: + module: ec2_elb_facts + name: + - frontend-prod-elb + - backend-prod-elb + register: elb_facts + +- action: + module: debug + msg: "{{ item.dns_name }}" + with_items: elb_facts.elbs + +''' + +import xml.etree.ElementTree as ET + +try: + import boto.ec2.elb + from boto.exception import BotoServerError + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + + +def get_error_message(xml_string): + + root = ET.fromstring(xml_string) + for message in root.findall('.//Message'): + return message.text + + +def get_elb_listeners(listeners): + listener_list = [] + for listener in listeners: + listener_dict = { + 'load_balancer_port': listener[0], + 'instance_port': listener[1], + 'protocol': listener[2], + } + try: + ssl_certificate_id = listener[4] + except IndexError: + pass + else: + if ssl_certificate_id: + listener_dict['ssl_certificate_id'] = ssl_certificate_id + listener_list.append(listener_dict) + + return listener_list + + +def get_health_check(health_check): + protocol, port_path = health_check.target.split(':') + try: + port, path = port_path.split('/') + path = '/{}'.format(path) + except ValueError: + port = port_path + path = None + + health_check_dict = { + 'ping_protocol': protocol.lower(), + 'ping_port': int(port), + 'response_timeout': health_check.timeout, + 'interval': health_check.interval, + 'unhealthy_threshold': health_check.unhealthy_threshold, + 'healthy_threshold': health_check.healthy_threshold, + } + if path: + health_check_dict['ping_path'] = path + return health_check_dict + + +def get_elb_info(elb): + elb_info = { + 'name': elb.name, + 'zones': elb.availability_zones, + 'dns_name': elb.dns_name, + 'instances': [instance.id for instance in elb.instances], + 'listeners': get_elb_listeners(elb.listeners), + 'scheme': elb.scheme, + 'security_groups': elb.security_groups, + 'health_check': get_health_check(elb.health_check), + 'subnets': elb.subnets, + } + if elb.vpc_id: + elb_info['vpc_id'] = elb.vpc_id + + return elb_info + + +def list_elb(connection, module): + elb_names = module.params.get("name") + if not elb_names: + elb_names = None + + try: + all_elbs = connection.get_all_load_balancers(elb_names) + except BotoServerError as e: + module.fail_json(msg=get_error_message(e.args[2])) + + elb_array = [] + for elb in all_elbs: + elb_array.append(get_elb_info(elb)) + + module.exit_json(elbs=elb_array) + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + name={'default': None, 'type': 'list'} + ) + ) + + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO: + module.fail_json(msg='boto required for this module') + + region, ec2_url, aws_connect_params = get_aws_connection_info(module) + + if region: + try: + connection = connect_to_aws(boto.ec2.elb, region, **aws_connect_params) + except (boto.exception.NoAuthHandlerFound, StandardError), e: + module.fail_json(msg=str(e)) + else: + module.fail_json(msg="region must be specified") + + list_elb(connection, module) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +# this is magic, see lib/ansible/module_common.py +#<> + +main() From 9d68a1746f2ef17be5f248613841565f8e91a0a6 Mon Sep 17 00:00:00 2001 From: gfrank Date: Thu, 20 Aug 2015 13:29:10 -0400 Subject: [PATCH 0613/2522] Remove unnecessary aliases arrays --- windows/win_nssm.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/windows/win_nssm.py b/windows/win_nssm.py index 86d343b1fde..5647c60b5a3 100644 --- a/windows/win_nssm.py +++ b/windows/win_nssm.py @@ -35,8 +35,6 @@ description: - Name of the service to operate on required: true - default: null - aliases: [] state: description: @@ -48,35 +46,30 @@ - restarted - absent default: started - aliases: [] application: description: - The application binary to run as a service required: false default: null - aliases: [] stdout_file: description: - Path to receive output required: false default: null - aliases: [] stderr_file: description: - Path to receive error output required: false default: null - aliases: [] app_parameters: description: - Parameters to be passed to the application when it starts required: false default: null - aliases: [] author: "Adam Keech (@smadam813), George Frank (@georgefrank)" ''' From b75f7cc22bdf8366669e253363f099d1378962fa Mon Sep 17 00:00:00 2001 From: gfrank Date: Thu, 20 Aug 2015 13:57:03 -0400 Subject: [PATCH 0614/2522] Move comments to the documentation --- windows/win_nssm.ps1 | 9 --------- windows/win_nssm.py | 6 ++++++ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/windows/win_nssm.ps1 b/windows/win_nssm.ps1 index f99eec66f0a..841bc3aa3fd 100644 --- a/windows/win_nssm.ps1 +++ b/windows/win_nssm.ps1 @@ -39,10 +39,6 @@ If ($params.state) { $state = $params.state.ToString().ToLower() $validStates = "present", "absent", "started", "stopped", "restarted" - - # These don't really fit the declarative style of ansible - # If you need to do these things, you can just write a command for it - # "paused", "continued", "rotated" If ($validStates -notcontains $state) { @@ -138,11 +134,6 @@ Function Nssm-Install [string]$application ) - #note: the application name must look like the following, if the directory includes spaces: - # nssm install service "c:\Program Files\app.exe" """C:\Path with spaces""" - #see https://git.nssm.cc/?p=nssm/nssm.git;a=commit;h=0b386fc1984ab74ee59b7bed14b7e8f57212c22b for more info - - if (!$application) { Throw "Error installing service ""$name"". No application was supplied." diff --git a/windows/win_nssm.py b/windows/win_nssm.py index 5647c60b5a3..86adacf1a7c 100644 --- a/windows/win_nssm.py +++ b/windows/win_nssm.py @@ -39,8 +39,10 @@ state: description: - State of the service on the system + - Note that NSSM actions like "pause", "continue", "rotate" do not fit the declarative style of ansible, so these should be implemented via the ansible command module required: false choices: + - present - started - stopped - restarted @@ -50,6 +52,10 @@ application: description: - The application binary to run as a service + - Specify this whenever the service may need to be installed (state: present, started, stopped, restarted) + - Note that the application name must look like the following, if the directory includes spaces: + - nssm install service "c:\Program Files\app.exe" """C:\Path with spaces""" + - See commit 0b386fc1984ab74ee59b7bed14b7e8f57212c22b in the nssm.git project for more info (https://git.nssm.cc/?p=nssm.git;a=commit;h=0b386fc1984ab74ee59b7bed14b7e8f57212c22b) required: false default: null From 7ee4e68e70e10f1a49135f4a353b2d26bfec73b2 Mon Sep 17 00:00:00 2001 From: Mark Hamilton Date: Thu, 20 Aug 2015 18:35:28 -0700 Subject: [PATCH 0615/2522] Module provides support for manipulating openvswitch tables --- network/openvswitch_db.py | 129 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 network/openvswitch_db.py diff --git a/network/openvswitch_db.py b/network/openvswitch_db.py new file mode 100644 index 00000000000..320fdf87d3d --- /dev/null +++ b/network/openvswitch_db.py @@ -0,0 +1,129 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# pylint: disable=C0111 + +# +# (c) 2015, Mark Hamilton +# +# Portions copyright @ 2015 VMware, Inc. +# +# This file is part of Ansible +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + +DOCUMENTATION = """ +--- +module: openvswitch_db +author: "Mark Hamilton (mhamilton@vmware.com)" +version_added: 2.0 +short_description: Configure open vswitch database. +requirements: [ "ovs-vsctl >= 2.3.3" ] +description: + - Set column values in record in database table. +options: + table: + required: true + description: + - Identifies the table in the database. + record: + required: true + description: + - Identifies the recoard in the table. + column: + required: true + description: + - Identifies the column in the record. + key: + required: true + description: + - Identifies the key in the record column + value: + required: true + description: + - Expected value for the table, record, column and key. + timeout: + required: false + default: 5 + description: + - How long to wait for ovs-vswitchd to respond +""" + +DOCUMENTATION = __doc__ + +EXAMPLES = ''' +# Increase the maximum idle time to 50 seconds before pruning unused kernel +# rules. +- openvswitch_db: table=open_vswitch record=. col=other_config key=max-idle + value=50000 + +# Disable in band +- openvswitch_db: table=Bridge record=br-int col=other_config + key=disable-in-band value=true +''' + + +def cmd_run(module, cmd, check_rc=True): + """ Log and run ovs-vsctl command. """ + return module.run_command(cmd.split(" "), check_rc=check_rc) + + +def params_set(module): + """ Implement the ovs-vsctl set commands. """ + + changed = False + + fmt = "ovs-vsctl -t %(timeout)s get %(table)s %(record)s %(col)s:%(key)s" + + cmd = fmt % module.params + + (_, output, _) = cmd_run(module, cmd, False) + if module.params['value'] not in output: + fmt = "ovs-vsctl -t %(timeout)s set %(table)s %(record)s " \ + "%(col)s:%(key)s=%(value)s" + cmd = fmt % module.params + ## + # Check if flow exists and is the same. + (rtc, _, err) = cmd_run(module, cmd) + if rtc != 0: + module.fail_json(msg=err) + changed = True + module.exit_json(changed=changed) + + +# pylint: disable=E0602 +def main(): + """ Entry point for ansible module. """ + module = AnsibleModule( + argument_spec={ + 'table': {'required': True}, + 'record': {'required': True}, + 'col': {'required': True}, + 'key': {'required': True}, + 'value': {'required': True}, + 'timeout': {'default': 5, 'type': 'int'}, + }, + supports_check_mode=True, + ) + + params_set(module) + + +# pylint: disable=W0614 +# pylint: disable=W0401 +# pylint: disable=W0622 + +# import module snippets +from ansible.module_utils.basic import * +main() From 29a80d35554edb004e24cd629a45baa2efde15f4 Mon Sep 17 00:00:00 2001 From: nitzmahone Date: Fri, 21 Aug 2015 09:49:36 -0700 Subject: [PATCH 0616/2522] win_updates rewrite for 2.0 uses scheduled job to run under a local token (required for WU client) supports check mode no external PS module deps --- windows/win_updates.ps1 | 440 +++++++++++++++++++++++++++++++++++----- windows/win_updates.py | 132 +++++++++--- 2 files changed, 492 insertions(+), 80 deletions(-) diff --git a/windows/win_updates.ps1 b/windows/win_updates.ps1 index 92c1b93e1f8..d790aec6a29 100644 --- a/windows/win_updates.ps1 +++ b/windows/win_updates.ps1 @@ -1,7 +1,7 @@ #!powershell # This file is part of Ansible # -# Copyright 2014, Trond Hindenes +# Copyright 2015, Matt Davis # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -19,68 +19,400 @@ # WANT_JSON # POWERSHELL_COMMON -function Write-Log -{ - param - ( - [parameter(mandatory=$false)] - [System.String] - $message - ) +$ErrorActionPreference = "Stop" +$FormatEnumerationLimit = -1 # prevent out-string et al from truncating collection dumps - $date = get-date -format 'yyyy-MM-dd hh:mm:ss.zz' +<# Most of the Windows Update Agent API will not run under a remote token, +which a remote WinRM session always has. win_updates uses the Task Scheduler +to run the bulk of the update functionality under a local token. Powershell's +Scheduled-Job capability provides a decent abstraction over the Task Scheduler +and handles marshaling Powershell args in and output/errors/etc back. The +module schedules a single job that executes all interactions with the Update +Agent API, then waits for completion. A significant amount of hassle is +involved to ensure that only one of these jobs is running at a time, and to +clean up the various error conditions that can occur. #> - Write-Host "$date $message" +# define the ScriptBlock that will be passed to Register-ScheduledJob +$job_body = { + Param( + [hashtable]$boundparms=@{}, + [Object[]]$unboundargs=$() + ) - Out-File -InputObject "$date $message" -FilePath $global:LoggingFile -Append + Set-StrictMode -Version 2 + + $ErrorActionPreference = "Stop" + $DebugPreference = "Continue" + $FormatEnumerationLimit = -1 # prevent out-string et al from truncating collection dumps + + # set this as a global for the Write-DebugLog function + $log_path = $boundparms['log_path'] + + Write-DebugLog "Scheduled job started with boundparms $($boundparms | out-string) and unboundargs $($unboundargs | out-string)" + + # FUTURE: elevate this to module arg validation once we have it + Function MapCategoryNameToGuid { + Param([string] $category_name) + + $category_guid = switch -exact ($category_name) { + # as documented by TechNet @ https://technet.microsoft.com/en-us/library/ff730937.aspx + "Application" {"5C9376AB-8CE6-464A-B136-22113DD69801"} + "Connectors" {"434DE588-ED14-48F5-8EED-A15E09A991F6"} + "CriticalUpdates" {"E6CF1350-C01B-414D-A61F-263D14D133B4"} + "DefinitionUpdates" {"E0789628-CE08-4437-BE74-2495B842F43B"} + "DeveloperKits" {"E140075D-8433-45C3-AD87-E72345B36078"} + "FeaturePacks" {"B54E7D24-7ADD-428F-8B75-90A396FA584F"} + "Guidance" {"9511D615-35B2-47BB-927F-F73D8E9260BB"} + "SecurityUpdates" {"0FA1201D-4330-4FA8-8AE9-B877473B6441"} + "ServicePacks" {"68C5B0A3-D1A6-4553-AE49-01D3A7827828"} + "Tools" {"B4832BD8-E735-4761-8DAF-37F882276DAB"} + "UpdateRollups" {"28BC880E-0592-4CBF-8F95-C79B17911D5F"} + "Updates" {"CD5FFD1E-E932-4E3A-BF74-18BF0B1BBD83"} + default { throw "Unknown category_name $category_name, must be one of (Application,Connectors,CriticalUpdates,DefinitionUpdates,DeveloperKits,FeaturePacks,Guidance,SecurityUpdates,ServicePacks,Tools,UpdateRollups,Updates)" } + } + + return $category_guid + } + + Function DoWindowsUpdate { + Param( + [string[]]$category_names=@("CriticalUpdates","SecurityUpdates","UpdateRollups"), + [ValidateSet("installed", "searched")] + [string]$state="installed", + [bool]$_ansible_check_mode=$false + ) + + $is_check_mode = $($state -eq "searched") -or $_ansible_check_mode + + $category_guids = $category_names | % { MapCategoryNameToGUID $_ } + + $update_status = @{ changed = $false } + + Write-DebugLog "Creating Windows Update session..." + $session = New-Object -ComObject Microsoft.Update.Session + + Write-DebugLog "Create Windows Update searcher..." + $searcher = $session.CreateUpdateSearcher() + + # OR is only allowed at the top-level, so we have to repeat base criteria inside + # FUTURE: change this to client-side filtered? + $criteriabase = "IsInstalled = 0" + $criteria_list = $category_guids | % { "($criteriabase AND CategoryIDs contains '$_')" } + + $criteria = [string]::Join(" OR ", $criteria_list) + + Write-DebugLog "Search criteria: $criteria" + + Write-DebugLog "Searching for updates to install in category IDs $category_guids..." + $searchresult = $searcher.Search($criteria) + + Write-DebugLog "Creating update collection..." + + $updates_to_install = New-Object -ComObject Microsoft.Update.UpdateColl + + Write-DebugLog "Found $($searchresult.Updates.Count) updates" + + $update_status.updates = @{ } + + # FUTURE: add further filtering options + foreach($update in $searchresult.Updates) { + if(-Not $update.EulaAccepted) { + Write-DebugLog "Accepting EULA for $($update.Identity.UpdateID)" + $update.AcceptEula() + } + + Write-DebugLog "Adding update $($update.Identity.UpdateID) - $($update.Title)" + $res = $updates_to_install.Add($update) + + $update_status.updates[$update.Identity.UpdateID] = @{ + title = $update.Title + # TODO: pluck the first KB out (since most have just one)? + kb = $update.KBArticleIDs + id = $update.Identity.UpdateID + installed = $false + } + } + + Write-DebugLog "Calculating pre-install reboot requirement..." + + # calculate this early for check mode, and to see if we should allow updates to continue + $sysinfo = New-Object -ComObject Microsoft.Update.SystemInfo + $update_status.reboot_required = $sysinfo.RebootRequired + $update_status.found_update_count = $updates_to_install.Count + $update_status.installed_update_count = 0 + + # bail out here for check mode + if($is_check_mode -eq $true) { + Write-DebugLog "Check mode; exiting..." + Write-DebugLog "Return value: $($update_status | out-string)" + + if($updates_to_install.Count -gt 0) { $update_status.changed = $true } + return $update_status + } + + if($updates_to_install.Count -gt 0) { + if($update_status.reboot_required) { + throw "A reboot is required before more updates can be installed." + } + else { + Write-DebugLog "No reboot is pending..." + } + Write-DebugLog "Downloading updates..." + } + + foreach($update in $updates_to_install) { + if($update.IsDownloaded) { + Write-DebugLog "Update $($update.Identity.UpdateID) already downloaded, skipping..." + continue + } + Write-DebugLog "Creating downloader object..." + $dl = $session.CreateUpdateDownloader() + Write-DebugLog "Creating download collection..." + $dl.Updates = New-Object -ComObject Microsoft.Update.UpdateColl + Write-DebugLog "Adding update $($update.Identity.UpdateID)" + $res = $dl.Updates.Add($update) + Write-DebugLog "Downloading update $($update.Identity.UpdateID)..." + $download_result = $dl.Download() + # FUTURE: configurable download retry + if($download_result.ResultCode -ne 2) { # OperationResultCode orcSucceeded + throw "Failed to download update $($update.Identity.UpdateID)" + } + } + + if($updates_to_install.Count -lt 1 ) { return $update_status } + + Write-DebugLog "Installing updates..." + + # install as a batch so the reboot manager will suppress intermediate reboots + Write-DebugLog "Creating installer object..." + $inst = $session.CreateUpdateInstaller() + Write-DebugLog "Creating install collection..." + $inst.Updates = New-Object -ComObject Microsoft.Update.UpdateColl + + foreach($update in $updates_to_install) { + Write-DebugLog "Adding update $($update.Identity.UpdateID)" + $res = $inst.Updates.Add($update) + } + + # FUTURE: use BeginInstall w/ progress reporting so we can at least log intermediate install results + Write-DebugLog "Installing updates..." + $install_result = $inst.Install() + + $update_success_count = 0 + $update_fail_count = 0 + + # WU result API requires us to index in to get the install results + $update_index = 0 + + foreach($update in $updates_to_install) { + $update_result = $install_result.GetUpdateResult($update_index) + $update_resultcode = $update_result.ResultCode + $update_hresult = $update_result.HResult + + $update_index++ + + $update_dict = $update_status.updates[$update.Identity.UpdateID] + + if($update_resultcode -eq 2) { # OperationResultCode orcSucceeded + $update_success_count++ + $update_dict.installed = $true + Write-DebugLog "Update $($update.Identity.UpdateID) succeeded" + } + else { + $update_fail_count++ + $update_dict.installed = $false + $update_dict.failed = $true + $update_dict.failure_hresult_code = $update_hresult + Write-DebugLog "Update $($update.Identity.UpdateID) failed resultcode $update_hresult hresult $update_hresult" + } + + } + + if($update_fail_count -gt 0) { + $update_status.failed = $true + $update_status.msg="Failed to install one or more updates" + } + else { $update_status.changed = $true } + + Write-DebugLog "Performing post-install reboot requirement check..." + + # recalculate reboot status after installs + $sysinfo = New-Object -ComObject Microsoft.Update.SystemInfo + $update_status.reboot_required = $sysinfo.RebootRequired + $update_status.installed_update_count = $update_success_count + $update_status.failed_update_count = $update_fail_count + + Write-DebugLog "Return value: $($update_status | out-string)" + + return $update_status + } + + Try { + # job system adds a bunch of cruft to top-level dict, so we have to send a sub-dict + return @{ job_output = DoWindowsUpdate @boundparms } + } + Catch { + $excep = $_ + Write-DebugLog "Fatal exception: $($excep.Exception.Message) at $($excep.ScriptStackTrace)" + return @{ job_output = @{ failed=$true;error=$excep.Exception.Message;location=$excep.ScriptStackTrace } } + } } -$params = Parse-Args $args; -$result = New-Object PSObject; -Set-Attr $result "changed" $false; +Function DestroyScheduledJob { + Param([string] $job_name) + + # find a scheduled job with the same name (should normally fail) + $schedjob = Get-ScheduledJob -Name $job_name -ErrorAction SilentlyContinue + + # nuke it if it's there + If($schedjob -ne $null) { + Write-DebugLog "ScheduledJob $job_name exists, ensuring it's not running..." + # can't manage jobs across sessions, so we have to resort to the Task Scheduler script object to kill running jobs + $schedserv = New-Object -ComObject Schedule.Service + Write-DebugLog "Connecting to scheduler service..." + $schedserv.Connect() + Write-DebugLog "Getting running tasks named $job_name" + $running_tasks = @($schedserv.GetRunningTasks(0) | Where-Object { $_.Name -eq $job_name }) + + Foreach($task_to_stop in $running_tasks) { + Write-DebugLog "Stopping running task $($task_to_stop.InstanceId)..." + $task_to_stop.Stop() + } + + <# FUTURE: add a global waithandle for this to release any other waiters. Wait-Job + and/or polling will block forever, since the killed job object in the parent + session doesn't know it's been killed :( #> + + Unregister-ScheduledJob -Name $job_name + } -if(($params.logPath).Length -gt 0) { - $global:LoggingFile = $params.logPath -} else { - $global:LoggingFile = "c:\ansible-playbook.log" } -if ($params.category) { - $category = $params.category -} else { - $category = "critical" + +Function RunAsScheduledJob { + Param([scriptblock] $job_body, [string] $job_name, [scriptblock] $job_init, [Object[]] $job_arg_list=@()) + + DestroyScheduledJob -job_name $job_name + + $rsj_args = @{ + ScriptBlock = $job_body + Name = $job_name + ArgumentList = $job_arg_list + ErrorAction = "Stop" + ScheduledJobOption = @{ RunElevated=$True } + } + + if($job_init) { $rsj_args.InitializationScript = $job_init } + + Write-DebugLog "Registering scheduled job with args $($rsj_args | Out-String -Width 300)" + $schedjob = Register-ScheduledJob @rsj_args + + # RunAsTask isn't available in PS3- fall back to a 2s future trigger + if($schedjob.RunAsTask) { + Write-DebugLog "Starting scheduled job (PS4 method)" + $schedjob.RunAsTask() + } + else { + Write-DebugLog "Starting scheduled job (PS3 method)" + Add-JobTrigger -inputobject $schedjob -trigger $(New-JobTrigger -once -at $(Get-Date).AddSeconds(2)) + } + + $sw = [System.Diagnostics.Stopwatch]::StartNew() + + $job = $null + + Write-DebugLog "Waiting for job completion..." + + # Wait-Job can fail for a few seconds until the scheduled task starts- poll for it... + while ($job -eq $null) { + start-sleep -Milliseconds 100 + if($sw.ElapsedMilliseconds -ge 30000) { # tasks scheduled right after boot on 2008R2 can take awhile to start... + Throw "Timed out waiting for scheduled task to start" + } + + # FUTURE: configurable timeout so we don't block forever? + # FUTURE: add a global WaitHandle in case another instance kills our job, so we don't block forever + $job = Wait-Job -Name $schedjob.Name -ErrorAction SilentlyContinue + } + + $sw = [System.Diagnostics.Stopwatch]::StartNew() + + # NB: output from scheduled jobs is delayed after completion (including the sub-objects after the primary Output object is available) + While (($job.Output -eq $null -or $job.Output.job_output -eq $null) -and $sw.ElapsedMilliseconds -lt 15000) { + Write-DebugLog "Waiting for job output to be non-null..." + Start-Sleep -Milliseconds 500 + } + + # NB: fallthru on both timeout and success + + $ret = @{ + ErrorOutput = $job.Error + WarningOutput = $job.Warning + VerboseOutput = $job.Verbose + DebugOutput = $job.Debug + } + + If ($job.Output -eq $null -or $job.Output.job_output -eq $null) { + $ret.Output = @{failed = $true; msg = "job output was lost"} + } + Else { + $ret.Output = $job.Output.job_output # sub-object returned, can only be accessed as a property for some reason + } + + Try { # this shouldn't be fatal, but can fail with both Powershell errors and COM Exceptions, hence the dual error-handling... + Unregister-ScheduledJob -Name $job_name -Force -ErrorAction Continue + } + Catch { + Write-DebugLog "Error unregistering job after execution: $($_.Exception.ToString()) $($_.ScriptStackTrace)" + } + + return $ret } -$installed_prior = get-wulist -isinstalled | foreach { $_.KBArticleIDs } -set-attr $result "updates_already_present" $installed_prior - -write-log "Looking for updates in '$category'" -set-attr $result "updates_category" $category -$to_install = get-wulist -category $category -$installed = @() -foreach ($u in $to_install) { - $kb = $u.KBArticleIDs - write-log "Installing $kb - $($u.Title)" - $install_result = get-wuinstall -KBArticleID $u.KBArticleIDs -acceptall -ignorereboot - Set-Attr $result "updates_installed_KB$kb" $u.Title - $installed += $kb +Function Log-Forensics { + Write-DebugLog "Arguments: $job_args | out-string" + Write-DebugLog "OS Version: $([environment]::OSVersion.Version | out-string)" + Write-DebugLog "Running as user: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)" + # FUTURE: log auth method (kerb, password, etc) } -write-log "Installed: $($installed.count)" -set-attr $result "updates_installed" $installed -set-attr $result "updates_installed_count" $installed.count -$result.changed = $installed.count -gt 0 - -$installed_afterwards = get-wulist -isinstalled | foreach { $_.KBArticleIDs } -set-attr $result "updates_installed_afterwards" $installed_afterwards - -$reboot_needed = Get-WURebootStatus -write-log $reboot_needed -if ($reboot_needed -match "not") { - write-log "Reboot not required" -} else { - write-log "Reboot required" - Set-Attr $result "updates_reboot_needed" $true - $result.changed = $true + +# code shared between the scheduled job and the host script +$common_inject = { + # FUTURE: capture all to a list, dump on error + Function Write-DebugLog { + Param( + [string]$msg + ) + + $DebugPreference = "Continue" + $ErrorActionPreference = "Continue" + $date_str = Get-Date -Format u + $msg = "$date_str $msg" + + Write-Debug $msg + + if($log_path -ne $null) { + Add-Content $log_path $msg + } + } } -Set-Attr $result "updates_success" "true" -Exit-Json $result; +# source the common code into the current scope so we can call it +. $common_inject + +$parsed_args = Parse-Args $args $true +# grr, why use PSCustomObject for args instead of just native hashtable? +$parsed_args.psobject.properties | foreach -begin {$job_args=@{}} -process {$job_args."$($_.Name)" = $_.Value} -end {$job_args} + +# set the log_path for the global log function we injected earlier +$log_path = $job_args.log_path + +Log-Forensics + +Write-DebugLog "Starting scheduled job with args: $($job_args | Out-String -Width 300)" + +# pass the common code as job_init so it'll be injected into the scheduled job script +$sjo = RunAsScheduledJob -job_init $common_inject -job_body $job_body -job_name ansible-win-updates -job_arg_list $job_args + +Write-DebugLog "Scheduled job completed with output: $($sjo.Output | Out-String -Width 300)" + +Exit-Json $sjo.Output \ No newline at end of file diff --git a/windows/win_updates.py b/windows/win_updates.py index 13c57f2b6d1..84d7d54c311 100644 --- a/windows/win_updates.py +++ b/windows/win_updates.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# (c) 2014, Peter Mounce +# (c) 2015, Matt Davis # # This file is part of Ansible # @@ -24,34 +24,114 @@ DOCUMENTATION = ''' --- module: win_updates -version_added: "1.9" -short_description: Lists / Installs windows updates +version_added: "2.0" +short_description: Download and install Windows updates description: - - Installs windows updates using PSWindowsUpdate (http://gallery.technet.microsoft.com/scriptcenter/2d191bcd-3308-4edd-9de2-88dff796b0bc). - - PSWindowsUpdate needs to be installed first - use win_chocolatey. + - Searches, downloads, and installs Windows updates synchronously by automating the Windows Update client options: - category: - description: - - Which category to install updates from - required: false - default: critical - choices: - - critical - - security - - (anything that is a valid update category) - default: critical - aliases: [] - logPath: - description: - - Where to log command output to - required: false - default: c:\\ansible-playbook.log - aliases: [] -author: "Peter Mounce (@petemounce)" + category_names: + description: + - A scalar or list of categories to install updates from + required: false + default: ["CriticalUpdates","SecurityUpdates","UpdateRollups"] + choices: + - Application + - Connectors + - CriticalUpdates + - DefinitionUpdates + - DeveloperKits + - FeaturePacks + - Guidance + - SecurityUpdates + - ServicePacks + - Tools + - UpdateRollups + - Updates + state: + description: + - Controls whether found updates are returned as a list or actually installed. + - This module also supports Ansible check mode, which has the same effect as setting state=searched + required: false + default: installed + choices: + - installed + - searched + log_path: + description: + - If set, win_updates will append update progress to the specified file. The directory must already exist. + required: false +author: "Matt Davis (@mattdavispdx)" +notes: +- win_updates must be run by a user with membership in the local Administrators group +- win_updates will use the default update service configured for the machine (Windows Update, Microsoft Update, WSUS, etc) +- win_updates does not manage reboots, but will signal when a reboot is required with the reboot_required return value. +- win_updates can take a significant amount of time to complete (hours, in some cases). Performance depends on many factors, including OS version, number of updates, system load, and update server load. ''' EXAMPLES = ''' - # Install updates from security category - win_updates: - category: security + # Install all security, critical, and rollup updates + win_updates: + category_names: ['SecurityUpdates','CriticalUpdates','UpdateRollups'] + + # Install only security updates + win_updates: category_names=SecurityUpdates + + # Search-only, return list of found updates (if any), log to c:\ansible_wu.txt + win_updates: category_names=SecurityUpdates status=searched log_path=c:/ansible_wu.txt +''' + +RETURN = ''' +reboot_required: + description: True when the target server requires a reboot to complete updates (no further updates can be installed until after a reboot) + returned: success + type: boolean + sample: True + +updates: + description: List of updates that were found/installed + returned: success + type: dictionary + sample: + contains: + title: + description: Display name + returned: always + type: string + sample: "Security Update for Windows Server 2012 R2 (KB3004365)" + kb: + description: A list of KB article IDs that apply to the update + returned: always + type: list of strings + sample: [ '3004365' ] + id: + description: Internal Windows Update GUID + returned: always + type: string (guid) + sample: "fb95c1c8-de23-4089-ae29-fd3351d55421" + installed: + description: Was the update successfully installed + returned: always + type: boolean + sample: True + failure_hresult_code: + description: The HRESULT code from a failed update + returned: on install failure + type: boolean + sample: 2147942402 + +found_update_count: + description: The number of updates found needing to be applied + returned: success + type: int + sample: 3 +installed_update_count: + description: The number of updates successfully installed + returned: success + type: int + sample: 2 +failed_update_count: + description: The number of updates that failed to install + returned: always + type: int + sample: 0 ''' From b56e5c670b82a29de94a6cf041d1eea9973b3ded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl?= Date: Fri, 21 Aug 2015 19:42:08 +0200 Subject: [PATCH 0617/2522] add zfs backing store support --- cloud/lxc/lxc_container.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index bf5fcf3cecf..6010a4fd33b 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -454,6 +454,9 @@ ], 'overlayfs': [ 'lv_name', 'vg_name', 'fs_type', 'fs_size', 'thinpool', 'zfs_root' + ], + 'zfs': [ + 'lv_name', 'vg_name', 'fs_type', 'fs_size', 'thinpool' ] } From 7a0bfd91bbfd12dcfd0cd5263f193ede30804feb Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Fri, 21 Aug 2015 18:07:20 -0400 Subject: [PATCH 0618/2522] updated docs to new choice option --- cloud/lxc/lxc_container.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index 6010a4fd33b..1f82bcb829e 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -39,6 +39,7 @@ - loop - btrfs - overlayfs + - zfs description: - Backend storage type for the container. required: false From 249b7bf9695c8ef1950fa760ad2a115fdae15871 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sun, 23 Aug 2015 00:01:52 +0200 Subject: [PATCH 0619/2522] cloudstack: cs_instance: fix ip address may not be set on default nic --- cloud/cloudstack/cs_instance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 201449b870d..4ead1317b2f 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -779,7 +779,7 @@ def get_result(self, instance): self.result['affinity_groups'] = affinity_groups if 'nic' in instance: for nic in instance['nic']: - if nic['isdefault']: + if nic['isdefault'] and 'ipaddress' in nic: self.result['default_ip'] = nic['ipaddress'] return self.result From a279207c7bb05c58ae1fcc2d682fad345e656dc0 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sun, 23 Aug 2015 00:04:51 +0200 Subject: [PATCH 0620/2522] cloudstack: cs_portforward: fix returns for int casting * missing self. * variable must be named portforwarding_rule, not resource --- cloud/cloudstack/cs_portforward.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/cloudstack/cs_portforward.py b/cloud/cloudstack/cs_portforward.py index 2fc14aa5ed3..f2f87b660ef 100644 --- a/cloud/cloudstack/cs_portforward.py +++ b/cloud/cloudstack/cs_portforward.py @@ -361,9 +361,9 @@ def get_result(self, portforwarding_rule): super(AnsibleCloudStackPortforwarding, self).get_result(portforwarding_rule) if portforwarding_rule: # Bad bad API does not always return int when it should. - for search_key, return_key in returns_to_int.iteritems(): - if search_key in resource: - self.result[return_key] = int(resource[search_key]) + for search_key, return_key in self.returns_to_int.iteritems(): + if search_key in portforwarding_rule: + self.result[return_key] = int(portforwarding_rule[search_key]) return self.result From 94614d0454e908fe5151451f1c3f23397f3ad747 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sun, 23 Aug 2015 00:06:37 +0200 Subject: [PATCH 0621/2522] cloudstack: cs_staticnat: fix wrong class name used --- cloud/cloudstack/cs_staticnat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/cloudstack/cs_staticnat.py b/cloud/cloudstack/cs_staticnat.py index 500c533915b..4b73d86e32b 100644 --- a/cloud/cloudstack/cs_staticnat.py +++ b/cloud/cloudstack/cs_staticnat.py @@ -154,7 +154,7 @@ class AnsibleCloudStackStaticNat(AnsibleCloudStack): def __init__(self, module): - super(AnsibleCloudStackPortforwarding, self).__init__(module) + super(AnsibleCloudStackStaticNat, self).__init__(module) self.returns = { 'virtualmachinedisplayname': 'vm_display_name', 'virtualmachinename': 'vm_name', From be59c0063b87dcb056fefaf2a1fc1c1f9086d608 Mon Sep 17 00:00:00 2001 From: "Michael J. Schultz" Date: Sun, 23 Aug 2015 21:11:47 -0500 Subject: [PATCH 0622/2522] Names and aliases matching other modules --- cloud/amazon/ec2_elb_facts.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cloud/amazon/ec2_elb_facts.py b/cloud/amazon/ec2_elb_facts.py index b586842a485..fc8fdb878c2 100644 --- a/cloud/amazon/ec2_elb_facts.py +++ b/cloud/amazon/ec2_elb_facts.py @@ -22,12 +22,12 @@ version_added: "2.0" author: "Michael Schultz (github.com/mjschultz)" options: - name: + names: description: - List of ELB names to gather facts about. Pass this option to gather facts about a set of ELBs, otherwise, all ELBs are returned. required: false default: null - aliases: ['elb_id'] + aliases: ['elb_ids', 'ec2_elbs'] extends_documentation_fragment: aws ''' @@ -48,7 +48,7 @@ # Gather facts about a particular ELB - action: module: ec2_elb_facts - name: frontend-prod-elb + names: frontend-prod-elb register: elb_facts - action: @@ -58,7 +58,7 @@ # Gather facts about a set of ELBs - action: module: ec2_elb_facts - name: + names: - frontend-prod-elb - backend-prod-elb register: elb_facts @@ -148,7 +148,7 @@ def get_elb_info(elb): def list_elb(connection, module): - elb_names = module.params.get("name") + elb_names = module.params.get("names") if not elb_names: elb_names = None @@ -168,7 +168,7 @@ def main(): argument_spec = ec2_argument_spec() argument_spec.update( dict( - name={'default': None, 'type': 'list'} + names={'default': None, 'type': 'list'} ) ) From 913266c04f62b775f30d82ffb8c163d0b77c24fa Mon Sep 17 00:00:00 2001 From: "Michael J. Schultz" Date: Mon, 24 Aug 2015 10:14:44 -0500 Subject: [PATCH 0623/2522] Remove old Ansible AWS magic --- cloud/amazon/ec2_elb_facts.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/cloud/amazon/ec2_elb_facts.py b/cloud/amazon/ec2_elb_facts.py index fc8fdb878c2..554b75c951d 100644 --- a/cloud/amazon/ec2_elb_facts.py +++ b/cloud/amazon/ec2_elb_facts.py @@ -192,7 +192,4 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * -# this is magic, see lib/ansible/module_common.py -#<> - main() From 9f47f57700027a0a3cbc87377c127cb5a1990e78 Mon Sep 17 00:00:00 2001 From: Joseph Callen Date: Mon, 24 Aug 2015 13:10:21 -0400 Subject: [PATCH 0624/2522] New VMware Module to support adding a cluster to vCenter --- cloud/vmware/vmware_cluster.py | 286 +++++++++++++++++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 cloud/vmware/vmware_cluster.py diff --git a/cloud/vmware/vmware_cluster.py b/cloud/vmware/vmware_cluster.py new file mode 100644 index 00000000000..d803878b584 --- /dev/null +++ b/cloud/vmware/vmware_cluster.py @@ -0,0 +1,286 @@ +#!/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Joseph Callen +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: vmware_cluster +short_description: Create VMware vSphere Cluster +description: + - Create VMware vSphere Cluster +version_added: 2.0 +author: Joseph Callen +notes: +requirements: + - Tested on ESXi 5.5 + - PyVmomi installed +options: + hostname: + description: + - The hostname or IP address of the vSphere vCenter + required: True + version_added: 2.0 + username: + description: + - The username of the vSphere vCenter + required: True + aliases: ['user', 'admin'] + version_added: 2.0 + password: + description: + - The password of the vSphere vCenter + required: True + aliases: ['pass', 'pwd'] + version_added: 2.0 + datacenter_name: + description: + - The name of the datacenter the cluster will be created in. + required: True + version_added: 2.0 + cluster_name: + description: + - The name of the cluster that will be created + required: True + version_added: 2.0 + enable_ha: + description: + - If set to True will enable HA when the cluster is created. + required: False + default: False + version_added: 2.0 + enable_drs: + description: + - If set to True will enable DRS when the cluster is created. + required: False + default: False + version_added: 2.0 + enable_vsan: + description: + - If set to True will enable vSAN when the cluster is created. + required: False + default: False + version_added: 2.0 +''' + +EXAMPLES = ''' +# Example vmware_cluster command from Ansible Playbooks +- name: Create Cluster + local_action: > + vmware_cluster + hostname="{{ ansible_ssh_host }}" username=root password=vmware + datacenter_name="datacenter" + cluster_name="cluster" + enable_ha=True + enable_drs=True + enable_vsan=True +''' + +try: + from pyVmomi import vim, vmodl + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + + +def configure_ha(enable_ha): + das_config = vim.cluster.DasConfigInfo() + das_config.enabled = enable_ha + das_config.admissionControlPolicy = vim.cluster.FailoverLevelAdmissionControlPolicy() + das_config.admissionControlPolicy.failoverLevel = 2 + return das_config + + +def configure_drs(enable_drs): + drs_config = vim.cluster.DrsConfigInfo() + drs_config.enabled = enable_drs + # Set to partially automated + drs_config.vmotionRate = 3 + return drs_config + + +def configure_vsan(enable_vsan): + vsan_config = vim.vsan.cluster.ConfigInfo() + vsan_config.enabled = enable_vsan + vsan_config.defaultConfig = vim.vsan.cluster.ConfigInfo.HostDefaultInfo() + vsan_config.defaultConfig.autoClaimStorage = False + return vsan_config + + +def state_create_cluster(module): + + enable_ha = module.params['enable_ha'] + enable_drs = module.params['enable_drs'] + enable_vsan = module.params['enable_vsan'] + cluster_name = module.params['cluster_name'] + datacenter = module.params['datacenter'] + + try: + cluster_config_spec = vim.cluster.ConfigSpecEx() + cluster_config_spec.dasConfig = configure_ha(enable_ha) + cluster_config_spec.drsConfig = configure_drs(enable_drs) + if enable_vsan: + cluster_config_spec.vsanConfig = configure_vsan(enable_vsan) + if not module.check_mode: + datacenter.hostFolder.CreateClusterEx(cluster_name, cluster_config_spec) + module.exit_json(changed=True) + except vim.fault.DuplicateName: + module.fail_json(msg="A cluster with the name %s already exists" % cluster_name) + except vmodl.fault.InvalidArgument: + module.fail_json(msg="Cluster configuration specification parameter is invalid") + except vim.fault.InvalidName: + module.fail_json(msg="%s is an invalid name for a cluster" % cluster_name) + except vmodl.fault.NotSupported: + # This should never happen + module.fail_json(msg="Trying to create a cluster on an incorrect folder object") + except vmodl.RuntimeFault as runtime_fault: + module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + # This should never happen either + module.fail_json(msg=method_fault.msg) + + +def state_destroy_cluster(module): + cluster = module.params['cluster'] + changed = True + result = None + + try: + if not module.check_mode: + task = cluster.Destroy_Task() + changed, result = wait_for_task(task) + module.exit_json(changed=changed, result=result) + except vim.fault.VimFault as vim_fault: + module.fail_json(msg=vim_fault.msg) + except vmodl.RuntimeFault as runtime_fault: + module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + module.fail_json(msg=method_fault.msg) + + +def state_exit_unchanged(module): + module.exit_json(changed=False) + + +def state_update_cluster(module): + + cluster_config_spec = vim.cluster.ConfigSpecEx() + cluster = module.params['cluster'] + enable_ha = module.params['enable_ha'] + enable_drs = module.params['enable_drs'] + enable_vsan = module.params['enable_vsan'] + changed = True + result = None + + if cluster.configurationEx.dasConfig.enabled != enable_ha: + cluster_config_spec.dasConfig = configure_ha(enable_ha) + if cluster.configurationEx.drsConfig.enabled != enable_drs: + cluster_config_spec.drsConfig = configure_drs(enable_drs) + if cluster.configurationEx.vsanConfigInfo.enabled != enable_vsan: + cluster_config_spec.vsanConfig = configure_vsan(enable_vsan) + + try: + if not module.check_mode: + task = cluster.ReconfigureComputeResource_Task(cluster_config_spec, True) + changed, result = wait_for_task(task) + module.exit_json(changed=changed, result=result) + except vmodl.RuntimeFault as runtime_fault: + module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + module.fail_json(msg=method_fault.msg) + except TaskError as task_e: + module.fail_json(msg=str(task_e)) + + +def check_cluster_configuration(module): + datacenter_name = module.params['datacenter_name'] + cluster_name = module.params['cluster_name'] + + try: + content = connect_to_api(module) + datacenter = find_datacenter_by_name(content, datacenter_name) + if datacenter is None: + module.fail_json(msg="Datacenter %s does not exist, " + "please create first with Ansible Module vmware_datacenter or manually." + % datacenter_name) + cluster = find_cluster_by_name_datacenter(datacenter, cluster_name) + + module.params['content'] = content + module.params['datacenter'] = datacenter + + if cluster is None: + return 'absent' + else: + module.params['cluster'] = cluster + + desired_state = (module.params['enable_ha'], + module.params['enable_drs'], + module.params['enable_vsan']) + + current_state = (cluster.configurationEx.dasConfig.enabled, + cluster.configurationEx.drsConfig.enabled, + cluster.configurationEx.vsanConfigInfo.enabled) + + if cmp(desired_state, current_state) != 0: + return 'update' + else: + return 'present' + except vmodl.RuntimeFault as runtime_fault: + module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + module.fail_json(msg=method_fault.msg) + + +def main(): + + argument_spec = vmware_argument_spec() + argument_spec.update(dict(datacenter_name=dict(required=True, type='str'), + cluster_name=dict(required=True, type='str'), + enable_ha=dict(default=False, required=False, type='bool'), + enable_drs=dict(default=False, required=False, type='bool'), + enable_vsan=dict(default=False, required=False, type='bool'), + state=dict(default='present', choices=['present', 'absent'], type='str'))) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + if not HAS_PYVMOMI: + module.fail_json(msg='pyvmomi is required for this module') + + cluster_states = { + 'absent': { + 'present': state_destroy_cluster, + 'absent': state_exit_unchanged, + }, + 'present': { + 'update': state_update_cluster, + 'present': state_exit_unchanged, + 'absent': state_create_cluster, + } + } + desired_state = module.params['state'] + current_state = check_cluster_configuration(module) + + # Based on the desired_state and the current_state call + # the appropriate method from the dictionary + cluster_states[desired_state][current_state](module) + +from ansible.module_utils.vmware import * +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From b66c62aab0545df61d4e942f40a880e24d9f63c0 Mon Sep 17 00:00:00 2001 From: Joseph Callen Date: Mon, 24 Aug 2015 13:14:37 -0400 Subject: [PATCH 0625/2522] New VMware Module to support configuring DNS on ESXi hosts --- cloud/vmware/vmware_dns_config.py | 143 ++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 cloud/vmware/vmware_dns_config.py diff --git a/cloud/vmware/vmware_dns_config.py b/cloud/vmware/vmware_dns_config.py new file mode 100644 index 00000000000..b233ed610c8 --- /dev/null +++ b/cloud/vmware/vmware_dns_config.py @@ -0,0 +1,143 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Joseph Callen +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: vmware_dns_config +short_description: Manage VMware ESXi DNS Configuration +description: + - Manage VMware ESXi DNS Configuration +version_added: 2.0 +author: "Joseph Callen (@jcpowermac)" +notes: + - Tested on vSphere 5.5 +requirements: + - "python >= 2.6" + - PyVmomi +options: + hostname: + description: + - The hostname or IP address of the vSphere vCenter API server + required: True + username: + description: + - The username of the vSphere vCenter + required: True + aliases: ['user', 'admin'] + password: + description: + - The password of the vSphere vCenter + required: True + aliases: ['pass', 'pwd'] + change_hostname_to: + description: + - The hostname that an ESXi host should be changed to. + required: True + domainname: + description: + - The domain the ESXi host should be apart of. + required: True + dns_servers: + description: + - The DNS servers that the host should be configured to use. + required: True +''' + +EXAMPLES = ''' +# Example vmware_dns_config command from Ansible Playbooks +- name: Configure ESXi hostname and DNS servers + local_action: + module: vmware_dns_config + hostname: esxi_hostname + username: root + password: your_password + change_hostname_to: esx01 + domainname: foo.org + dns_servers: + - 8.8.8.8 + - 8.8.4.4 +''' +try: + from pyVmomi import vim, vmodl + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + + +def configure_dns(host_system, hostname, domainname, dns_servers): + + changed = False + host_config_manager = host_system.configManager + host_network_system = host_config_manager.networkSystem + config = host_network_system.dnsConfig + + config.dhcp = False + + if config.address != dns_servers: + config.address = dns_servers + changed = True + if config.domainName != domainname: + config.domainName = domainname + changed = True + if config.hostName != hostname: + config.hostName = hostname + changed = True + if changed: + host_network_system.UpdateDnsConfig(config) + + return changed + + +def main(): + + argument_spec = vmware_argument_spec() + argument_spec.update(dict(change_hostname_to=dict(required=True, type='str'), + domainname=dict(required=True, type='str'), + dns_servers=dict(required=True, type='list'))) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False) + + if not HAS_PYVMOMI: + module.fail_json(msg='pyvmomi is required for this module') + + change_hostname_to = module.params['change_hostname_to'] + domainname = module.params['domainname'] + dns_servers = module.params['dns_servers'] + try: + content = connect_to_api(module) + host = get_all_objs(content, [vim.HostSystem]) + if not host: + module.fail_json(msg="Unable to locate Physical Host.") + host_system = host.keys()[0] + changed = configure_dns(host_system, change_hostname_to, domainname, dns_servers) + module.exit_json(changed=changed) + except vmodl.RuntimeFault as runtime_fault: + module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + module.fail_json(msg=method_fault.msg) + except Exception as e: + module.fail_json(msg=str(e)) + + +from ansible.module_utils.vmware import * +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From 8a5b597676f2a5dcf3ae2f1b8dd523c4d0667e0e Mon Sep 17 00:00:00 2001 From: "Luiz Felipe G. Pereira" Date: Mon, 24 Aug 2015 13:54:44 -0300 Subject: [PATCH 0626/2522] Fixing empty tags check Right now even if you pass in an empty tags list to the module (either with an empty string or null) it will erroneously think the tags list have changed and re-apply the tags on every run --- messaging/rabbitmq_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messaging/rabbitmq_user.py b/messaging/rabbitmq_user.py index 6333e42282e..b12178e08ea 100644 --- a/messaging/rabbitmq_user.py +++ b/messaging/rabbitmq_user.py @@ -108,7 +108,7 @@ def __init__(self, module, username, password, tags, vhost, configure_priv, writ self.username = username self.password = password self.node = node - if tags is None: + if not tags: self.tags = list() else: self.tags = tags.split(',') From b82e15a73bb6c00cca67195db091f1f7664700b3 Mon Sep 17 00:00:00 2001 From: Russell Teague Date: Mon, 24 Aug 2015 13:21:41 -0400 Subject: [PATCH 0627/2522] This includes a new module for VMware vSphere Creates a VMware vSwitch We have an end-to-end playbook that performs bare metal provisioning and configuration of vSphere. The playbooks/tasks and results from that testing is what will be listed in this PR. If there are any questions please let either @jcpowermac or @mtnbikenc know. Tested with version ``` $ ansible-playbook --version ansible-playbook 1.9.2 configured module search path = None ``` Associated tasks used for testing below ``` - name: Add a temporary vSwitch local_action: module: vmware_vswitch hostname: "{{ inventory_hostname }}" username: "{{ esxi_username }}" password: "{{ site_passwd }}" switch_name: temp_vswitch nic_name: "{{ vss_vmnic }}" mtu: 9000 ``` Verbose testing output and results ``` TASK: [Configure ESXi hostname and DNS servers] ******************************* <127.0.0.1> REMOTE_MODULE vmware_dns_config password=VALUE_HIDDEN hostname=foundation-esxi-01 change_hostname_to=cscesxtmp001 domainname=lordbusiness.local dns_servers=192.168.70.3,192.168.70.4 username=root <127.0.0.1> REMOTE_MODULE vmware_dns_config password=VALUE_HIDDEN hostname=foundation-esxi-02 change_hostname_to=cscesxtmp002 domainname=lordbusiness.local dns_servers=192.168.70.3,192.168.70.4 username=root <127.0.0.1> REMOTE_MODULE vmware_dns_config password=VALUE_HIDDEN hostname=foundation-esxi-03 change_hostname_to=cscesxtmp003 domainname=lordbusiness.local dns_servers=192.168.70.3,192.168.70.4 username=root changed: [foundation-esxi-01 -> 127.0.0.1] => {"changed": true} changed: [foundation-esxi-03 -> 127.0.0.1] => {"changed": true} changed: [foundation-esxi-02 -> 127.0.0.1] => {"changed": true} ``` --- cloud/vmware/vmware_vswitch.py | 212 +++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 cloud/vmware/vmware_vswitch.py diff --git a/cloud/vmware/vmware_vswitch.py b/cloud/vmware/vmware_vswitch.py new file mode 100644 index 00000000000..d9ac55d2364 --- /dev/null +++ b/cloud/vmware/vmware_vswitch.py @@ -0,0 +1,212 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Joseph Callen +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: vmware_vswitch +short_description: Add a VMware Standard Switch to an ESXi host +description: + - Add a VMware Standard Switch to an ESXi host +version_added: 2.0 +author: "Joseph Callen (@jcpowermac), Russell Teague (@mtnbikenc)" +notes: + - Tested on vSphere 5.5 +requirements: + - "python >= 2.6" + - PyVmomi +options: + hostname: + description: + - The hostname or IP address of the ESXi server + required: True + username: + description: + - The username of the ESXi server + required: True + aliases: ['user', 'admin'] + password: + description: + - The password of the ESXi server + required: True + aliases: ['pass', 'pwd'] + switch_name: + description: + - vSwitch name to add + required: True + nic_name: + description: + - vmnic name to attach to vswitch + required: True + number_of_ports: + description: + - Number of port to configure on vswitch + default: 128 + required: False + mtu: + description: + - MTU to configure on vswitch + required: False + state: + description: + - Add or remove the switch + default: 'present' + choices: + - 'present' + - 'absent' + required: False +''' + +EXAMPLES = ''' +Example from Ansible playbook + + - name: Add a VMware vSwitch + local_action: + module: vmware_vswitch + hostname: esxi_hostname + username: esxi_username + password: esxi_password + switch_name: vswitch_name + nic_name: vmnic_name + mtu: 9000 +''' + +try: + from pyVmomi import vim, vmodl + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + + +def find_vswitch_by_name(host, vswitch_name): + for vss in host.config.network.vswitch: + if vss.name == vswitch_name: + return vss + return None + + +# Source from +# https://github.com/rreubenur/pyvmomi-community-samples/blob/patch-1/samples/create_vswitch.py + +def state_create_vswitch(module): + + switch_name = module.params['switch_name'] + number_of_ports = module.params['number_of_ports'] + nic_name = module.params['nic_name'] + mtu = module.params['mtu'] + host = module.params['host'] + + vss_spec = vim.host.VirtualSwitch.Specification() + vss_spec.numPorts = number_of_ports + vss_spec.mtu = mtu + vss_spec.bridge = vim.host.VirtualSwitch.BondBridge(nicDevice=[nic_name]) + host.configManager.networkSystem.AddVirtualSwitch(vswitchName=switch_name, spec=vss_spec) + module.exit_json(changed=True) + + +def state_exit_unchanged(module): + module.exit_json(changed=False) + + +def state_destroy_vswitch(module): + vss = module.params['vss'] + host = module.params['host'] + config = vim.host.NetworkConfig() + + for portgroup in host.configManager.networkSystem.networkInfo.portgroup: + if portgroup.spec.vswitchName == vss.name: + portgroup_config = vim.host.PortGroup.Config() + portgroup_config.changeOperation = "remove" + portgroup_config.spec = vim.host.PortGroup.Specification() + portgroup_config.spec.name = portgroup.spec.name + portgroup_config.spec.vlanId = portgroup.spec.vlanId + portgroup_config.spec.vswitchName = portgroup.spec.vswitchName + portgroup_config.spec.policy = vim.host.NetworkPolicy() + config.portgroup.append(portgroup_config) + + host.configManager.networkSystem.UpdateNetworkConfig(config, "modify") + host.configManager.networkSystem.RemoveVirtualSwitch(vss.name) + module.exit_json(changed=True) + + +def state_update_vswitch(module): + module.exit_json(changed=False, msg="Currently not implemented.") + + +def check_vswitch_configuration(module): + switch_name = module.params['switch_name'] + content = connect_to_api(module) + module.params['content'] = content + + host = get_all_objs(content, [vim.HostSystem]) + if not host: + module.fail_json(msg="Unble to find host") + + host_system = host.keys()[0] + module.params['host'] = host_system + vss = find_vswitch_by_name(host_system, switch_name) + + if vss is None: + return 'absent' + else: + module.params['vss'] = vss + return 'present' + + +def main(): + + argument_spec = vmware_argument_spec() + argument_spec.update(dict(switch_name=dict(required=True, type='str'), + nic_name=dict(required=True, type='str'), + number_of_ports=dict(required=False, type='int', default=128), + mtu=dict(required=False, type='int', default=1500), + state=dict(default='present', choices=['present', 'absent'], type='str'))) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False) + + if not HAS_PYVMOMI: + module.fail_json(msg='pyvmomi is required for this module') + + try: + vswitch_states = { + 'absent': { + 'present': state_destroy_vswitch, + 'absent': state_exit_unchanged, + }, + 'present': { + 'update': state_update_vswitch, + 'present': state_exit_unchanged, + 'absent': state_create_vswitch, + } + } + + vswitch_states[module.params['state']][check_vswitch_configuration(module)](module) + + except vmodl.RuntimeFault as runtime_fault: + module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + module.fail_json(msg=method_fault.msg) + except Exception as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.vmware import * +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From b0fae45be41a47d1928a1846401b38587f475a66 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 24 Aug 2015 13:23:27 -0400 Subject: [PATCH 0628/2522] minor doc updates to vmware_cluster --- cloud/vmware/vmware_cluster.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/cloud/vmware/vmware_cluster.py b/cloud/vmware/vmware_cluster.py index d803878b584..cc55c6d7b4b 100644 --- a/cloud/vmware/vmware_cluster.py +++ b/cloud/vmware/vmware_cluster.py @@ -25,7 +25,7 @@ description: - Create VMware vSphere Cluster version_added: 2.0 -author: Joseph Callen +author: Joseph Callen (@jcpowermac) notes: requirements: - Tested on ESXi 5.5 @@ -35,47 +35,40 @@ description: - The hostname or IP address of the vSphere vCenter required: True - version_added: 2.0 username: description: - The username of the vSphere vCenter required: True aliases: ['user', 'admin'] - version_added: 2.0 password: description: - The password of the vSphere vCenter required: True aliases: ['pass', 'pwd'] - version_added: 2.0 datacenter_name: description: - The name of the datacenter the cluster will be created in. required: True - version_added: 2.0 cluster_name: description: - The name of the cluster that will be created required: True - version_added: 2.0 enable_ha: description: - If set to True will enable HA when the cluster is created. required: False default: False - version_added: 2.0 enable_drs: description: - If set to True will enable DRS when the cluster is created. required: False default: False - version_added: 2.0 enable_vsan: description: - If set to True will enable vSAN when the cluster is created. required: False default: False - version_added: 2.0 +notes: [] ''' EXAMPLES = ''' From 55f59cec54fac3b00e6b7ad0ed0c0a8086bf82d6 Mon Sep 17 00:00:00 2001 From: Joseph Callen Date: Mon, 24 Aug 2015 13:24:32 -0400 Subject: [PATCH 0629/2522] New VMware Module to support adding an ESXi host to a distributed vswitch --- cloud/vmware/vmware_dvs_host.py | 281 ++++++++++++++++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 cloud/vmware/vmware_dvs_host.py diff --git a/cloud/vmware/vmware_dvs_host.py b/cloud/vmware/vmware_dvs_host.py new file mode 100644 index 00000000000..a9c66e4d1a7 --- /dev/null +++ b/cloud/vmware/vmware_dvs_host.py @@ -0,0 +1,281 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Joseph Callen +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: vmware_dvs_host +short_description: Add or remove a host from distributed virtual switch +description: + - Add or remove a host from distributed virtual switch +version_added: 2.0 +author: "Joseph Callen (@jcpowermac)" +notes: + - Tested on vSphere 5.5 +requirements: + - "python >= 2.6" + - PyVmomi +options: + hostname: + description: + - The hostname or IP address of the vSphere vCenter API server + required: True + username: + description: + - The username of the vSphere vCenter + required: True + aliases: ['user', 'admin'] + password: + description: + - The password of the vSphere vCenter + required: True + aliases: ['pass', 'pwd'] + esxi_hostname: + description: + - The ESXi hostname + required: True + switch_name: + description: + - The name of the Distributed vSwitch + required: True + vmnics: + description: + - The ESXi hosts vmnics to use with the Distributed vSwitch + required: True + state: + description: + - If the host should be present or absent attached to the vSwitch + choices: ['present', 'absent'] + required: True +''' + +EXAMPLES = ''' +# Example vmware_dvs_host command from Ansible Playbooks +- name: Add Host to dVS + local_action: + module: vmware_dvs_host + hostname: vcenter_ip_or_hostname + username: vcenter_username + password: vcenter_password + esxi_hostname: esxi_hostname_as_listed_in_vcenter + switch_name: dvSwitch + vmnics: + - vmnic0 + - vmnic1 + state: present +''' + +try: + import collections + from pyVmomi import vim, vmodl + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + + +def find_dvspg_by_name(dv_switch, portgroup_name): + portgroups = dv_switch.portgroup + + for pg in portgroups: + if pg.name == portgroup_name: + return pg + + return None + + +def find_dvs_uplink_pg(dv_switch): + # There should only always be a single uplink port group on + # a distributed virtual switch + + if len(dv_switch.config.uplinkPortgroup): + return dv_switch.config.uplinkPortgroup[0] + else: + return None + + +# operation should be edit, add and remove +def modify_dvs_host(dv_switch, host, operation, uplink_portgroup=None, vmnics=None): + + spec = vim.DistributedVirtualSwitch.ConfigSpec() + + spec.configVersion = dv_switch.config.configVersion + spec.host = [vim.dvs.HostMember.ConfigSpec()] + spec.host[0].operation = operation + spec.host[0].host = host + + if operation in ("edit", "add"): + spec.host[0].backing = vim.dvs.HostMember.PnicBacking() + count = 0 + + for nic in vmnics: + spec.host[0].backing.pnicSpec.append(vim.dvs.HostMember.PnicSpec()) + spec.host[0].backing.pnicSpec[count].pnicDevice = nic + spec.host[0].backing.pnicSpec[count].uplinkPortgroupKey = uplink_portgroup.key + count += 1 + + task = dv_switch.ReconfigureDvs_Task(spec) + changed, result = wait_for_task(task) + return changed, result + + +def state_destroy_dvs_host(module): + + operation = "remove" + host = module.params['host'] + dv_switch = module.params['dv_switch'] + + changed = True + result = None + + if not module.check_mode: + changed, result = modify_dvs_host(dv_switch, host, operation) + module.exit_json(changed=changed, result=str(result)) + + +def state_exit_unchanged(module): + module.exit_json(changed=False) + + +def state_update_dvs_host(module): + dv_switch = module.params['dv_switch'] + uplink_portgroup = module.params['uplink_portgroup'] + vmnics = module.params['vmnics'] + host = module.params['host'] + operation = "edit" + changed = True + result = None + + if not module.check_mode: + changed, result = modify_dvs_host(dv_switch, host, operation, uplink_portgroup, vmnics) + module.exit_json(changed=changed, result=str(result)) + + +def state_create_dvs_host(module): + dv_switch = module.params['dv_switch'] + uplink_portgroup = module.params['uplink_portgroup'] + vmnics = module.params['vmnics'] + host = module.params['host'] + operation = "add" + changed = True + result = None + + if not module.check_mode: + changed, result = modify_dvs_host(dv_switch, host, operation, uplink_portgroup, vmnics) + module.exit_json(changed=changed, result=str(result)) + + +def find_host_attached_dvs(esxi_hostname, dv_switch): + for dvs_host_member in dv_switch.config.host: + if dvs_host_member.config.host.name == esxi_hostname: + return dvs_host_member.config.host + + return None + + +def check_uplinks(dv_switch, host, vmnics): + pnic_device = [] + + for dvs_host_member in dv_switch.config.host: + if dvs_host_member.config.host == host: + for pnicSpec in dvs_host_member.config.backing.pnicSpec: + pnic_device.append(pnicSpec.pnicDevice) + + return collections.Counter(pnic_device) == collections.Counter(vmnics) + + +def check_dvs_host_state(module): + + switch_name = module.params['switch_name'] + esxi_hostname = module.params['esxi_hostname'] + vmnics = module.params['vmnics'] + + content = connect_to_api(module) + module.params['content'] = content + + dv_switch = find_dvs_by_name(content, switch_name) + + if dv_switch is None: + raise Exception("A distributed virtual switch %s does not exist" % switch_name) + + uplink_portgroup = find_dvs_uplink_pg(dv_switch) + + if uplink_portgroup is None: + raise Exception("An uplink portgroup does not exist on the distributed virtual switch %s" % switch_name) + + module.params['dv_switch'] = dv_switch + module.params['uplink_portgroup'] = uplink_portgroup + + host = find_host_attached_dvs(esxi_hostname, dv_switch) + + if host is None: + # We still need the HostSystem object to add the host + # to the distributed vswitch + host = find_hostsystem_by_name(content, esxi_hostname) + if host is None: + module.fail_json(msg="The esxi_hostname %s does not exist in vCenter" % esxi_hostname) + module.params['host'] = host + return 'absent' + else: + module.params['host'] = host + if check_uplinks(dv_switch, host, vmnics): + return 'present' + else: + return 'update' + + +def main(): + + argument_spec = vmware_argument_spec() + argument_spec.update(dict(esxi_hostname=dict(required=True, type='str'), + switch_name=dict(required=True, type='str'), + vmnics=dict(required=True, type='list'), + state=dict(default='present', choices=['present', 'absent'], type='str'))) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + if not HAS_PYVMOMI: + module.fail_json(msg='pyvmomi is required for this module') + + try: + + dvs_host_states = { + 'absent': { + 'present': state_destroy_dvs_host, + 'absent': state_exit_unchanged, + }, + 'present': { + 'update': state_update_dvs_host, + 'present': state_exit_unchanged, + 'absent': state_create_dvs_host, + } + } + + dvs_host_states[module.params['state']][check_dvs_host_state(module)](module) + except vmodl.RuntimeFault as runtime_fault: + module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + module.fail_json(msg=method_fault.msg) + except Exception as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.vmware import * +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From c4a7221c82e8dc9ac179f26879045442bf927f68 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 24 Aug 2015 13:25:18 -0400 Subject: [PATCH 0630/2522] removed empty notes --- cloud/vmware/vmware_cluster.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cloud/vmware/vmware_cluster.py b/cloud/vmware/vmware_cluster.py index cc55c6d7b4b..ee64b48f08c 100644 --- a/cloud/vmware/vmware_cluster.py +++ b/cloud/vmware/vmware_cluster.py @@ -68,7 +68,6 @@ - If set to True will enable vSAN when the cluster is created. required: False default: False -notes: [] ''' EXAMPLES = ''' From 9cb1e214cbfbadac733f9330cc95be92d4477c4e Mon Sep 17 00:00:00 2001 From: Russell Teague Date: Mon, 24 Aug 2015 13:31:36 -0400 Subject: [PATCH 0631/2522] Adding vmware_vsan_cluster module --- cloud/vmware/vmware_vsan_cluster.py | 143 ++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 cloud/vmware/vmware_vsan_cluster.py diff --git a/cloud/vmware/vmware_vsan_cluster.py b/cloud/vmware/vmware_vsan_cluster.py new file mode 100644 index 00000000000..b7b84d94c43 --- /dev/null +++ b/cloud/vmware/vmware_vsan_cluster.py @@ -0,0 +1,143 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Russell Teague +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: vmware_vsan_cluster +short_description: Configure VSAN clustering on an ESXi host +description: + - This module can be used to configure VSAN clustering on an ESXi host +version_added: 2.0 +author: "Russell Teague (@mtnbikenc)" +notes: + - Tested on vSphere 5.5 +requirements: + - "python >= 2.6" + - PyVmomi +options: + hostname: + description: + - The hostname or IP address of the ESXi Server + required: True + username: + description: + - The username of the ESXi Server + required: True + aliases: ['user', 'admin'] + password: + description: + - The password of ESXi Server + required: True + aliases: ['pass', 'pwd'] + cluster_uuid: + description: + - Desired cluster UUID + required: False +''' + +EXAMPLES = ''' +# Example command from Ansible Playbook + +- name: Configure VMware VSAN Cluster + hosts: deploy_node + gather_facts: False + tags: + - vsan + tasks: + - name: Configure VSAN on first host + vmware_vsan_cluster: + hostname: "{{ groups['esxi'][0] }}" + username: "{{ esxi_username }}" + password: "{{ site_password }}" + register: vsan_cluster + + - name: Configure VSAN on remaining hosts + vmware_vsan_cluster: + hostname: "{{ item }}" + username: "{{ esxi_username }}" + password: "{{ site_password }}" + cluster_uuid: "{{ vsan_cluster.cluster_uuid }}" + with_items: groups['esxi'][1:] + +''' + +try: + from pyVmomi import vim, vmodl + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + + +def create_vsan_cluster(host_system, new_cluster_uuid): + host_config_manager = host_system.configManager + vsan_system = host_config_manager.vsanSystem + + vsan_config = vim.vsan.host.ConfigInfo() + vsan_config.enabled = True + + if new_cluster_uuid is not None: + vsan_config.clusterInfo = vim.vsan.host.ConfigInfo.ClusterInfo() + vsan_config.clusterInfo.uuid = new_cluster_uuid + + vsan_config.storageInfo = vim.vsan.host.ConfigInfo.StorageInfo() + vsan_config.storageInfo.autoClaimStorage = True + + task = vsan_system.UpdateVsan_Task(vsan_config) + changed, result = wait_for_task(task) + + host_status = vsan_system.QueryHostStatus() + cluster_uuid = host_status.uuid + + return changed, result, cluster_uuid + + +def main(): + + argument_spec = vmware_argument_spec() + argument_spec.update(dict(cluster_uuid=dict(required=False, type='str'))) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False) + + if not HAS_PYVMOMI: + module.fail_json(msg='pyvmomi is required for this module') + + new_cluster_uuid = module.params['cluster_uuid'] + + try: + content = connect_to_api(module, False) + host = get_all_objs(content, [vim.HostSystem]) + if not host: + module.fail_json(msg="Unable to locate Physical Host.") + host_system = host.keys()[0] + changed, result, cluster_uuid = create_vsan_cluster(host_system, new_cluster_uuid) + module.exit_json(changed=changed, result=result, cluster_uuid=cluster_uuid) + + except vmodl.RuntimeFault as runtime_fault: + module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + module.fail_json(msg=method_fault.msg) + except Exception as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.vmware import * +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From 6945519411f92820ed14055176b642438774877e Mon Sep 17 00:00:00 2001 From: Joseph Callen Date: Mon, 24 Aug 2015 13:38:13 -0400 Subject: [PATCH 0632/2522] New VMware Module to support adding distributed portgroups --- cloud/vmware/vmware_dvs_portgroup.py | 219 +++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 cloud/vmware/vmware_dvs_portgroup.py diff --git a/cloud/vmware/vmware_dvs_portgroup.py b/cloud/vmware/vmware_dvs_portgroup.py new file mode 100644 index 00000000000..265f9fd71ef --- /dev/null +++ b/cloud/vmware/vmware_dvs_portgroup.py @@ -0,0 +1,219 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Joseph Callen +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: vmware_dvs_portgroup +short_description: Create or remove a Distributed vSwitch portgroup +description: + - Create or remove a Distributed vSwitch portgroup +version_added: 2.0 +author: "Joseph Callen (@jcpowermac)" +notes: + - Tested on vSphere 5.5 +requirements: + - "python >= 2.6" + - PyVmomi +options: + hostname: + description: + - The hostname or IP address of the vSphere vCenter API server + required: True + username: + description: + - The username of the vSphere vCenter + required: True + aliases: ['user', 'admin'] + password: + description: + - The password of the vSphere vCenter + required: True + aliases: ['pass', 'pwd'] + portgroup_name: + description: + - The name of the portgroup that is to be created or deleted + required: True + switch_name: + description: + - The name of the distributed vSwitch the port group should be created on. + required: True + vlan_id: + description: + - The VLAN ID that should be configured with the portgroup + required: True + num_ports: + description: + - The number of ports the portgroup should contain + required: True + portgroup_type: + description: + - See VMware KB 1022312 regarding portgroup types + required: True + choices: + - 'earlyBinding' + - 'lateBinding' + - 'ephemeral' +''' + +EXAMPLES = ''' + - name: Create Management portgroup + local_action: + module: vmware_dvs_portgroup + hostname: vcenter_ip_or_hostname + username: vcenter_username + password: vcenter_password + portgroup_name: Management + switch_name: dvSwitch + vlan_id: 123 + num_ports: 120 + portgroup_type: earlyBinding + state: present +''' + +try: + from pyVmomi import vim, vmodl + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + + +def create_port_group(dv_switch, portgroup_name, vlan_id, num_ports, portgroup_type): + config = vim.dvs.DistributedVirtualPortgroup.ConfigSpec() + + config.name = portgroup_name + config.numPorts = num_ports + + # vim.VMwareDVSPortSetting() does not exist in the pyvmomi documentation + # but this is the correct managed object type. + + config.defaultPortConfig = vim.VMwareDVSPortSetting() + + # vim.VmwareDistributedVirtualSwitchVlanIdSpec() does not exist in the + # pyvmomi documentation but this is the correct managed object type + config.defaultPortConfig.vlan = vim.VmwareDistributedVirtualSwitchVlanIdSpec() + config.defaultPortConfig.vlan.inherited = False + config.defaultPortConfig.vlan.vlanId = vlan_id + config.type = portgroup_type + + spec = [config] + task = dv_switch.AddDVPortgroup_Task(spec) + changed, result = wait_for_task(task) + return changed, result + + +def state_destroy_dvspg(module): + dvs_portgroup = module.params['dvs_portgroup'] + changed = True + result = None + + if not module.check_mode: + task = dvs_portgroup.Destroy_Task() + changed, result = wait_for_task(task) + module.exit_json(changed=changed, result=str(result)) + + +def state_exit_unchanged(module): + module.exit_json(changed=False) + + +def state_update_dvspg(module): + module.exit_json(changed=False, msg="Currently not implemented.") + return + + +def state_create_dvspg(module): + + switch_name = module.params['switch_name'] + portgroup_name = module.params['portgroup_name'] + dv_switch = module.params['dv_switch'] + vlan_id = module.params['vlan_id'] + num_ports = module.params['num_ports'] + portgroup_type = module.params['portgroup_type'] + changed = True + result = None + + if not module.check_mode: + changed, result = create_port_group(dv_switch, portgroup_name, vlan_id, num_ports, portgroup_type) + module.exit_json(changed=changed, result=str(result)) + + +def check_dvspg_state(module): + + switch_name = module.params['switch_name'] + portgroup_name = module.params['portgroup_name'] + + content = connect_to_api(module) + module.params['content'] = content + + dv_switch = find_dvs_by_name(content, switch_name) + + if dv_switch is None: + raise Exception("A distributed virtual switch with name %s does not exist" % switch_name) + + module.params['dv_switch'] = dv_switch + dvs_portgroup = find_dvspg_by_name(dv_switch, portgroup_name) + + if dvs_portgroup is None: + return 'absent' + else: + module.params['dvs_portgroup'] = dvs_portgroup + return 'present' + + +def main(): + + argument_spec = vmware_argument_spec() + argument_spec.update(dict(portgroup_name=dict(required=True, type='str'), + switch_name=dict(required=True, type='str'), + vlan_id=dict(required=True, type='int'), + num_ports=dict(required=True, type='int'), + portgroup_type=dict(required=True, choices=['earlyBinding', 'lateBinding', 'ephemeral'], type='str'), + state=dict(default='present', choices=['present', 'absent'], type='str'))) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + if not HAS_PYVMOMI: + module.fail_json(msg='pyvmomi is required for this module') + + try: + dvspg_states = { + 'absent': { + 'present': state_destroy_dvspg, + 'absent': state_exit_unchanged, + }, + 'present': { + 'update': state_update_dvspg, + 'present': state_exit_unchanged, + 'absent': state_create_dvspg, + } + } + dvspg_states[module.params['state']][check_dvspg_state(module)](module) + except vmodl.RuntimeFault as runtime_fault: + module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + module.fail_json(msg=method_fault.msg) + except Exception as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.vmware import * +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From 6fbadff17c9bc7481e5c67e2f0da690096deed06 Mon Sep 17 00:00:00 2001 From: Russell Teague Date: Mon, 24 Aug 2015 13:39:02 -0400 Subject: [PATCH 0633/2522] Adding vmware_vmkernel_ip_config module --- cloud/vmware/vmware_vmkernel_ip_config.py | 136 ++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 cloud/vmware/vmware_vmkernel_ip_config.py diff --git a/cloud/vmware/vmware_vmkernel_ip_config.py b/cloud/vmware/vmware_vmkernel_ip_config.py new file mode 100644 index 00000000000..c07526f0aeb --- /dev/null +++ b/cloud/vmware/vmware_vmkernel_ip_config.py @@ -0,0 +1,136 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Joseph Callen +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: vmware_vmkernel_ip_config +short_description: Configure the VMkernel IP Address +description: + - Configure the VMkernel IP Address +version_added: 2.0 +author: "Joseph Callen (@jcpowermac), Russell Teague (@mtnbikenc)" +notes: + - Tested on vSphere 5.5 +requirements: + - "python >= 2.6" + - PyVmomi +options: + hostname: + description: + - The hostname or IP address of the ESXi server + required: True + username: + description: + - The username of the ESXi server + required: True + aliases: ['user', 'admin'] + password: + description: + - The password of the ESXi server + required: True + aliases: ['pass', 'pwd'] + vmk_name: + description: + - VMkernel interface name + required: True + ip_address: + description: + - IP address to assign to VMkernel interface + required: True + subnet_mask: + description: + - Subnet Mask to assign to VMkernel interface + required: True +''' + +EXAMPLES = ''' +# Example command from Ansible Playbook + +- name: Configure IP address on ESX host + local_action: + module: vmware_vmkernel_ip_config + hostname: esxi_hostname + username: esxi_username + password: esxi_password + vmk_name: vmk0 + ip_address: 10.0.0.10 + subnet_mask: 255.255.255.0 +''' + +try: + from pyVmomi import vim, vmodl + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + + +def configure_vmkernel_ip_address(host_system, vmk_name, ip_address, subnet_mask): + + host_config_manager = host_system.configManager + host_network_system = host_config_manager.networkSystem + + for vnic in host_network_system.networkConfig.vnic: + if vnic.device == vmk_name: + spec = vnic.spec + if spec.ip.ipAddress != ip_address: + spec.ip.dhcp = False + spec.ip.ipAddress = ip_address + spec.ip.subnetMask = subnet_mask + host_network_system.UpdateVirtualNic(vmk_name, spec) + return True + return False + + +def main(): + + argument_spec = vmware_argument_spec() + argument_spec.update(dict(vmk_name=dict(required=True, type='str'), + ip_address=dict(required=True, type='str'), + subnet_mask=dict(required=True, type='str'))) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False) + + if not HAS_PYVMOMI: + module.fail_json(msg='pyvmomi is required for this module') + + vmk_name = module.params['vmk_name'] + ip_address = module.params['ip_address'] + subnet_mask = module.params['subnet_mask'] + + try: + content = connect_to_api(module, False) + host = get_all_objs(content, [vim.HostSystem]) + if not host: + module.fail_json(msg="Unable to locate Physical Host.") + host_system = host.keys()[0] + changed = configure_vmkernel_ip_address(host_system, vmk_name, ip_address, subnet_mask) + module.exit_json(changed=changed) + except vmodl.RuntimeFault as runtime_fault: + module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + module.fail_json(msg=method_fault.msg) + except Exception as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.vmware import * +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From 7beea8a15234b903b7763c0dc1a82d20ab2b756c Mon Sep 17 00:00:00 2001 From: Joseph Callen Date: Mon, 24 Aug 2015 13:44:27 -0400 Subject: [PATCH 0634/2522] New VMware Module to support adding distribute vswitch --- cloud/vmware/vmware_dvswitch.py | 225 ++++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 cloud/vmware/vmware_dvswitch.py diff --git a/cloud/vmware/vmware_dvswitch.py b/cloud/vmware/vmware_dvswitch.py new file mode 100644 index 00000000000..26212a06c5f --- /dev/null +++ b/cloud/vmware/vmware_dvswitch.py @@ -0,0 +1,225 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Joseph Callen +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: vmware_dvswitch +short_description: Create or remove a distributed vSwitch +description: + - Create or remove a distributed vSwitch +version_added: 2.0 +author: "Joseph Callen (@jcpowermac)" +notes: + - Tested on vSphere 5.5 +requirements: + - "python >= 2.6" + - PyVmomi +options: + hostname: + description: + - The hostname or IP address of the vSphere vCenter API server + required: True + username: + description: + - The username of the vSphere vCenter + required: True + aliases: ['user', 'admin'] + password: + description: + - The password of the vSphere vCenter + required: True + aliases: ['pass', 'pwd'] + datacenter_name: + description: + - The name of the datacenter that will contain the dvSwitch + required: True + switch_name: + description: + - The name of the switch to create or remove + required: True + mtu: + description: + - The switch maximum transmission unit + required: True + uplink_quantity: + description: + - Quantity of uplink per ESXi host added to the switch + required: True + discovery_proto: + description: + - Link discovery protocol between Cisco and Link Layer discovery + choices: + - 'cdp' + - 'lldp' + required: True + discovery_operation: + description: + - Select the discovery operation + choices: + - 'both' + - 'none' + - 'advertise' + - 'listen' + state: + description: + - Create or remove dvSwitch + default: 'present' + choices: + - 'present' + - 'absent' + required: False +''' +EXAMPLES = ''' +- name: Create dvswitch + local_action: + module: vmware_dvswitch + hostname: vcenter_ip_or_hostname + username: vcenter_username + password: vcenter_password + datacenter_name: datacenter + switch_name: dvSwitch + mtu: 9000 + uplink_quantity: 2 + discovery_proto: lldp + discovery_operation: both + state: present +''' + +try: + from pyVmomi import vim, vmodl + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + + +def create_dvswitch(network_folder, switch_name, mtu, uplink_quantity, discovery_proto, discovery_operation): + + result = None + changed = False + + spec = vim.DistributedVirtualSwitch.CreateSpec() + spec.configSpec = vim.dvs.VmwareDistributedVirtualSwitch.ConfigSpec() + spec.configSpec.uplinkPortPolicy = vim.DistributedVirtualSwitch.NameArrayUplinkPortPolicy() + spec.configSpec.linkDiscoveryProtocolConfig = vim.host.LinkDiscoveryProtocolConfig() + + spec.configSpec.name = switch_name + spec.configSpec.maxMtu = mtu + spec.configSpec.linkDiscoveryProtocolConfig.protocol = discovery_proto + spec.configSpec.linkDiscoveryProtocolConfig.operation = discovery_operation + spec.productInfo = vim.dvs.ProductSpec() + spec.productInfo.name = "DVS" + spec.productInfo.vendor = "VMware" + + for count in range(1, uplink_quantity+1): + spec.configSpec.uplinkPortPolicy.uplinkPortName.append("uplink%d" % count) + + task = network_folder.CreateDVS_Task(spec) + changed, result = wait_for_task(task) + return changed, result + + +def state_exit_unchanged(module): + module.exit_json(changed=False) + + +def state_destroy_dvs(module): + dvs = module.params['dvs'] + task = dvs.Destroy_Task() + changed, result = wait_for_task(task) + module.exit_json(changed=changed, result=str(result)) + + +def state_update_dvs(module): + module.exit_json(changed=False, msg="Currently not implemented.") + + +def state_create_dvs(module): + switch_name = module.params['switch_name'] + datacenter_name = module.params['datacenter_name'] + content = module.params['content'] + mtu = module.params['mtu'] + uplink_quantity = module.params['uplink_quantity'] + discovery_proto = module.params['discovery_proto'] + discovery_operation = module.params['discovery_operation'] + + changed = True + result = None + + if not module.check_mode: + dc = find_datacenter_by_name(content, datacenter_name) + changed, result = create_dvswitch(dc.networkFolder, switch_name, + mtu, uplink_quantity, discovery_proto, + discovery_operation) + module.exit_json(changed=changed, result=str(result)) + + +def check_dvs_configuration(module): + switch_name = module.params['switch_name'] + content = connect_to_api(module) + module.params['content'] = content + dvs = find_dvs_by_name(content, switch_name) + if dvs is None: + return 'absent' + else: + module.params['dvs'] = dvs + return 'present' + + +def main(): + argument_spec = vmware_argument_spec() + argument_spec.update(dict(datacenter_name=dict(required=True, type='str'), + switch_name=dict(required=True, type='str'), + mtu=dict(required=True, type='int'), + uplink_quantity=dict(required=True, type='int'), + discovery_proto=dict(required=True, choices=['cdp', 'lldp'], type='str'), + discovery_operation=dict(required=True, choices=['both', 'none', 'advertise', 'listen'], type='str'), + state=dict(default='present', choices=['present', 'absent'], type='str'))) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + if not HAS_PYVMOMI: + module.fail_json(msg='pyvmomi is required for this module') + + try: + # Currently state_update_dvs is not implemented. + dvs_states = { + 'absent': { + 'present': state_destroy_dvs, + 'absent': state_exit_unchanged, + }, + 'present': { + 'update': state_update_dvs, + 'present': state_exit_unchanged, + 'absent': state_create_dvs, + } + } + dvs_states[module.params['state']][check_dvs_configuration(module)](module) + except vmodl.RuntimeFault as runtime_fault: + module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + module.fail_json(msg=method_fault.msg) + except Exception as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.vmware import * +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From 72579ab3e11c02c4d7869fb4634dd08114a7dd3f Mon Sep 17 00:00:00 2001 From: Russell Teague Date: Mon, 24 Aug 2015 13:44:45 -0400 Subject: [PATCH 0635/2522] Adding vmware_vmkernel module --- cloud/vmware/vmware_vmkernel.py | 221 ++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 cloud/vmware/vmware_vmkernel.py diff --git a/cloud/vmware/vmware_vmkernel.py b/cloud/vmware/vmware_vmkernel.py new file mode 100644 index 00000000000..0221f68ad2e --- /dev/null +++ b/cloud/vmware/vmware_vmkernel.py @@ -0,0 +1,221 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Joseph Callen +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: vmware_vmkernel +short_description: Create a VMware VMkernel Interface +description: + - Create a VMware VMkernel Interface +version_added: 2.0 +author: "Joseph Callen (@jcpowermac), Russell Teague (@mtnbikenc)" +notes: + - Tested on vSphere 5.5 +requirements: + - "python >= 2.6" + - PyVmomi +options: + hostname: + description: + - The hostname or IP address of the ESXi Server + required: True + username: + description: + - The username of the ESXi Server + required: True + aliases: ['user', 'admin'] + password: + description: + - The password of ESXi Server + required: True + aliases: ['pass', 'pwd'] + vswitch_name: + description: + - The name of the vswitch where to add the VMK interface + required: True + portgroup_name: + description: + - The name of the portgroup for the VMK interface + required: True + ip_address: + description: + - The IP Address for the VMK interface + required: True + subnet_mask: + description: + - The Subnet Mask for the VMK interface + required: True + vland_id: + description: + - The VLAN ID for the VMK interface + required: True + mtu: + description: + - The MTU for the VMK interface + required: False + enable_vsan: + description: + - Enable the VMK interface for VSAN traffic + required: False + enable_vmotion: + description: + - Enable the VMK interface for vMotion traffic + required: False + enable_mgmt: + description: + - Enable the VMK interface for Management traffic + required: False + enable_ft: + description: + - Enable the VMK interface for Fault Tolerance traffic + required: False +''' + +EXAMPLES = ''' +# Example command from Ansible Playbook + +- name: Add Management vmkernel port (vmk1) + local_action: + module: vmware_vmkernel + hostname: esxi_hostname + username: esxi_username + password: esxi_password + vswitch_name: vswitch_name + portgroup_name: portgroup_name + vlan_id: vlan_id + ip_address: ip_address + subnet_mask: subnet_mask + enable_mgmt: True +''' + +try: + from pyVmomi import vim, vmodl + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + + +def create_vmkernel_adapter(host_system, port_group_name, + vlan_id, vswitch_name, + ip_address, subnet_mask, + mtu, enable_vsan, enable_vmotion, enable_mgmt, enable_ft): + + host_config_manager = host_system.configManager + host_network_system = host_config_manager.networkSystem + host_virtual_vic_manager = host_config_manager.virtualNicManager + config = vim.host.NetworkConfig() + + config.portgroup = [vim.host.PortGroup.Config()] + config.portgroup[0].changeOperation = "add" + config.portgroup[0].spec = vim.host.PortGroup.Specification() + config.portgroup[0].spec.name = port_group_name + config.portgroup[0].spec.vlanId = vlan_id + config.portgroup[0].spec.vswitchName = vswitch_name + config.portgroup[0].spec.policy = vim.host.NetworkPolicy() + + config.vnic = [vim.host.VirtualNic.Config()] + config.vnic[0].changeOperation = "add" + config.vnic[0].portgroup = port_group_name + config.vnic[0].spec = vim.host.VirtualNic.Specification() + config.vnic[0].spec.ip = vim.host.IpConfig() + config.vnic[0].spec.ip.dhcp = False + config.vnic[0].spec.ip.ipAddress = ip_address + config.vnic[0].spec.ip.subnetMask = subnet_mask + if mtu: + config.vnic[0].spec.mtu = mtu + + host_network_config_result = host_network_system.UpdateNetworkConfig(config, "modify") + + for vnic_device in host_network_config_result.vnicDevice: + if enable_vsan: + vsan_system = host_config_manager.vsanSystem + vsan_config = vim.vsan.host.ConfigInfo() + vsan_config.networkInfo = vim.vsan.host.ConfigInfo.NetworkInfo() + + vsan_config.networkInfo.port = [vim.vsan.host.ConfigInfo.NetworkInfo.PortConfig()] + + vsan_config.networkInfo.port[0].device = vnic_device + host_vsan_config_result = vsan_system.UpdateVsan_Task(vsan_config) + + if enable_vmotion: + host_virtual_vic_manager.SelectVnicForNicType("vmotion", vnic_device) + + if enable_mgmt: + host_virtual_vic_manager.SelectVnicForNicType("management", vnic_device) + + if enable_ft: + host_virtual_vic_manager.SelectVnicForNicType("faultToleranceLogging", vnic_device) + return True + + +def main(): + + argument_spec = vmware_argument_spec() + argument_spec.update(dict(portgroup_name=dict(required=True, type='str'), + ip_address=dict(required=True, type='str'), + subnet_mask=dict(required=True, type='str'), + mtu=dict(required=False, type='int'), + enable_vsan=dict(required=False, type='bool'), + enable_vmotion=dict(required=False, type='bool'), + enable_mgmt=dict(required=False, type='bool'), + enable_ft=dict(required=False, type='bool'), + vswitch_name=dict(required=True, type='str'), + vlan_id=dict(required=True, type='int'))) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False) + + if not HAS_PYVMOMI: + module.fail_json(msg='pyvmomi is required for this module') + + port_group_name = module.params['portgroup_name'] + ip_address = module.params['ip_address'] + subnet_mask = module.params['subnet_mask'] + mtu = module.params['mtu'] + enable_vsan = module.params['enable_vsan'] + enable_vmotion = module.params['enable_vmotion'] + enable_mgmt = module.params['enable_mgmt'] + enable_ft = module.params['enable_ft'] + vswitch_name = module.params['vswitch_name'] + vlan_id = module.params['vlan_id'] + + try: + content = connect_to_api(module) + host = get_all_objs(content, [vim.HostSystem]) + if not host: + module.fail_json(msg="Unable to locate Physical Host.") + host_system = host.keys()[0] + changed = create_vmkernel_adapter(host_system, port_group_name, + vlan_id, vswitch_name, + ip_address, subnet_mask, + mtu, enable_vsan, enable_vmotion, enable_mgmt, enable_ft) + module.exit_json(changed=changed) + except vmodl.RuntimeFault as runtime_fault: + module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + module.fail_json(msg=method_fault.msg) + except Exception as e: + module.fail_json(msg=str(e)) + + +from ansible.module_utils.vmware import * +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From d5e3bd770a45d9d333de56571e0208db4c3dedff Mon Sep 17 00:00:00 2001 From: Russell Teague Date: Mon, 24 Aug 2015 13:48:16 -0400 Subject: [PATCH 0636/2522] Adding vmware_vm_vss_dvs_migrate module --- cloud/vmware/vmware_vm_vss_dvs_migrate.py | 176 ++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 cloud/vmware/vmware_vm_vss_dvs_migrate.py diff --git a/cloud/vmware/vmware_vm_vss_dvs_migrate.py b/cloud/vmware/vmware_vm_vss_dvs_migrate.py new file mode 100644 index 00000000000..ff51f86ed09 --- /dev/null +++ b/cloud/vmware/vmware_vm_vss_dvs_migrate.py @@ -0,0 +1,176 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Joseph Callen +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: vmware_vm_vss_dvs_migrate +short_description: Migrates a virtual machine from a standard vswitch to distributed +description: + - Migrates a virtual machine from a standard vswitch to distributed +version_added: 2.0 +author: "Joseph Callen (@jcpowermac)" +notes: + - Tested on vSphere 5.5 +requirements: + - "python >= 2.6" + - PyVmomi +options: + hostname: + description: + - The hostname or IP address of the vSphere vCenter API server + required: True + username: + description: + - The username of the vSphere vCenter + required: True + aliases: ['user', 'admin'] + password: + description: + - The password of the vSphere vCenter + required: True + aliases: ['pass', 'pwd'] + vm_name: + description: + - Name of the virtual machine to migrate to a dvSwitch + required: True + dvportgroup_name: + description: + - Name of the portgroup to migrate to the virtual machine to + required: True +''' + +EXAMPLES = ''' +- name: Migrate VCSA to vDS + local_action: + module: vmware_vm_vss_dvs_migrate + hostname: vcenter_ip_or_hostname + username: vcenter_username + password: vcenter_password + vm_name: virtual_machine_name + dvportgroup_name: distributed_portgroup_name +''' + +try: + from pyVmomi import vim, vmodl + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + + +def _find_dvspg_by_name(content, pg_name): + + vmware_distributed_port_group = get_all_objs(content, [vim.dvs.DistributedVirtualPortgroup]) + for dvspg in vmware_distributed_port_group: + if dvspg.name == pg_name: + return dvspg + return None + + +def find_vm_by_name(content, vm_name): + + virtual_machines = get_all_objs(content, [vim.VirtualMachine]) + for vm in virtual_machines: + if vm.name == vm_name: + return vm + return None + + +def migrate_network_adapter_vds(module): + vm_name = module.params['vm_name'] + dvportgroup_name = module.params['dvportgroup_name'] + content = module.params['content'] + + vm_configspec = vim.vm.ConfigSpec() + nic = vim.vm.device.VirtualEthernetCard.DistributedVirtualPortBackingInfo() + port = vim.dvs.PortConnection() + devicespec = vim.vm.device.VirtualDeviceSpec() + + pg = _find_dvspg_by_name(content, dvportgroup_name) + + if pg is None: + module.fail_json(msg="The standard portgroup was not found") + + vm = find_vm_by_name(content, vm_name) + if vm is None: + module.fail_json(msg="The virtual machine was not found") + + dvswitch = pg.config.distributedVirtualSwitch + port.switchUuid = dvswitch.uuid + port.portgroupKey = pg.key + nic.port = port + + for device in vm.config.hardware.device: + if isinstance(device, vim.vm.device.VirtualEthernetCard): + devicespec.device = device + devicespec.operation = vim.vm.device.VirtualDeviceSpec.Operation.edit + devicespec.device.backing = nic + vm_configspec.deviceChange.append(devicespec) + + task = vm.ReconfigVM_Task(vm_configspec) + changed, result = wait_for_task(task) + module.exit_json(changed=changed, result=result) + + +def state_exit_unchanged(module): + module.exit_json(changed=False) + + +def check_vm_network_state(module): + vm_name = module.params['vm_name'] + try: + content = connect_to_api(module) + module.params['content'] = content + vm = find_vm_by_name(content, vm_name) + module.params['vm'] = vm + if vm is None: + module.fail_json(msg="A virtual machine with name %s does not exist" % vm_name) + for device in vm.config.hardware.device: + if isinstance(device, vim.vm.device.VirtualEthernetCard): + if isinstance(device.backing, vim.vm.device.VirtualEthernetCard.DistributedVirtualPortBackingInfo): + return 'present' + return 'absent' + except vmodl.RuntimeFault as runtime_fault: + module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + module.fail_json(msg=method_fault.msg) + + +def main(): + + argument_spec = vmware_argument_spec() + argument_spec.update(dict(vm_name=dict(required=True, type='str'), + dvportgroup_name=dict(required=True, type='str'))) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False) + if not HAS_PYVMOMI: + module.fail_json(msg='pyvmomi is required for this module') + + vm_nic_states = { + 'absent': migrate_network_adapter_vds, + 'present': state_exit_unchanged, + } + + vm_nic_states[check_vm_network_state(module)](module) + +from ansible.module_utils.vmware import * +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() \ No newline at end of file From a2e15f07f863a33ed49d4709e96a2a9dad7d4d8c Mon Sep 17 00:00:00 2001 From: Joseph Callen Date: Mon, 24 Aug 2015 13:48:17 -0400 Subject: [PATCH 0637/2522] New VMware Module to support adding an ESXi host to vCenter --- cloud/vmware/vmware_host.py | 241 ++++++++++++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 cloud/vmware/vmware_host.py diff --git a/cloud/vmware/vmware_host.py b/cloud/vmware/vmware_host.py new file mode 100644 index 00000000000..162397a2190 --- /dev/null +++ b/cloud/vmware/vmware_host.py @@ -0,0 +1,241 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Joseph Callen +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: vmware_host +short_description: Add/remove ESXi host to/from vCenter +description: + - This module can be used to add/remove an ESXi host to/from vCenter +version_added: 2.0 +author: "Joseph Callen (@jcpowermac), Russell Teague (@mtnbikenc)" +notes: + - Tested on vSphere 5.5 +requirements: + - "python >= 2.6" + - PyVmomi +options: + hostname: + description: + - The hostname or IP address of the vSphere vCenter API server + required: True + username: + description: + - The username of the vSphere vCenter + required: True + aliases: ['user', 'admin'] + password: + description: + - The password of the vSphere vCenter + required: True + aliases: ['pass', 'pwd'] + datacenter_name: + description: + - Name of the datacenter to add the host + required: True + cluster_name: + description: + - Name of the cluster to add the host + required: True + esxi_hostname: + description: + - ESXi hostname to manage + required: True + esxi_username: + description: + - ESXi username + required: True + esxi_password: + description: + - ESXi password + required: True + state: + description: + - Add or remove the host + default: 'present' + choices: + - 'present' + - 'absent' + required: False +''' + +EXAMPLES = ''' +Example from Ansible playbook + + - name: Add ESXi Host to VCSA + local_action: + module: vmware_host + hostname: vcsa_host + username: vcsa_user + password: vcsa_pass + datacenter_name: datacenter_name + cluster_name: cluster_name + esxi_hostname: esxi_hostname + esxi_username: esxi_username + esxi_password: esxi_password + state: present +''' + +try: + from pyVmomi import vim, vmodl + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + + +def find_host_by_cluster_datacenter(module): + datacenter_name = module.params['datacenter_name'] + cluster_name = module.params['cluster_name'] + content = module.params['content'] + esxi_hostname = module.params['esxi_hostname'] + + dc = find_datacenter_by_name(content, datacenter_name) + cluster = find_cluster_by_name_datacenter(dc, cluster_name) + + for host in cluster.host: + if host.name == esxi_hostname: + return host, cluster + + return None, cluster + + +def add_host_to_vcenter(module): + cluster = module.params['cluster'] + + host_connect_spec = vim.host.ConnectSpec() + host_connect_spec.hostName = module.params['esxi_hostname'] + host_connect_spec.userName = module.params['esxi_username'] + host_connect_spec.password = module.params['esxi_password'] + host_connect_spec.force = True + host_connect_spec.sslThumbprint = "" + as_connected = True + esxi_license = None + resource_pool = None + + try: + task = cluster.AddHost_Task(host_connect_spec, as_connected, resource_pool, esxi_license) + success, result = wait_for_task(task) + return success, result + except TaskError as add_task_error: + # This is almost certain to fail the first time. + # In order to get the sslThumbprint we first connect + # get the vim.fault.SSLVerifyFault then grab the sslThumbprint + # from that object. + # + # args is a tuple, selecting the first tuple + ssl_verify_fault = add_task_error.args[0] + host_connect_spec.sslThumbprint = ssl_verify_fault.thumbprint + + task = cluster.AddHost_Task(host_connect_spec, as_connected, resource_pool, esxi_license) + success, result = wait_for_task(task) + return success, result + + +def state_exit_unchanged(module): + module.exit_json(changed=False) + + +def state_remove_host(module): + host = module.params['host'] + changed = True + result = None + if not module.check_mode: + if not host.runtime.inMaintenanceMode: + maintenance_mode_task = host.EnterMaintenanceMode_Task(300, True, None) + changed, result = wait_for_task(maintenance_mode_task) + + if changed: + task = host.Destroy_Task() + changed, result = wait_for_task(task) + else: + raise Exception(result) + module.exit_json(changed=changed, result=str(result)) + + +def state_update_host(module): + module.exit_json(changed=False, msg="Currently not implemented.") + + +def state_add_host(module): + + changed = True + result = None + + if not module.check_mode: + changed, result = add_host_to_vcenter(module) + module.exit_json(changed=changed, result=str(result)) + + +def check_host_state(module): + + content = connect_to_api(module) + module.params['content'] = content + + host, cluster = find_host_by_cluster_datacenter(module) + + module.params['cluster'] = cluster + if host is None: + return 'absent' + else: + module.params['host'] = host + return 'present' + + +def main(): + argument_spec = vmware_argument_spec() + argument_spec.update(dict(datacenter_name=dict(required=True, type='str'), + cluster_name=dict(required=True, type='str'), + esxi_hostname=dict(required=True, type='str'), + esxi_username=dict(required=True, type='str'), + esxi_password=dict(required=True, type='str', no_log=True), + state=dict(default='present', choices=['present', 'absent'], type='str'))) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + if not HAS_PYVMOMI: + module.fail_json(msg='pyvmomi is required for this module') + + try: + # Currently state_update_dvs is not implemented. + host_states = { + 'absent': { + 'present': state_remove_host, + 'absent': state_exit_unchanged, + }, + 'present': { + 'present': state_exit_unchanged, + 'absent': state_add_host, + } + } + + host_states[module.params['state']][check_host_state(module)](module) + + except vmodl.RuntimeFault as runtime_fault: + module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + module.fail_json(msg=method_fault.msg) + except Exception as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.vmware import * +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From c4b0375eb5d365a74ee7514a835de374650229f4 Mon Sep 17 00:00:00 2001 From: Russell Teague Date: Mon, 24 Aug 2015 13:51:57 -0400 Subject: [PATCH 0638/2522] Adding vmware_vm_facts module --- cloud/vmware/vmware_vm_facts.py | 115 ++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 cloud/vmware/vmware_vm_facts.py diff --git a/cloud/vmware/vmware_vm_facts.py b/cloud/vmware/vmware_vm_facts.py new file mode 100644 index 00000000000..3551477f243 --- /dev/null +++ b/cloud/vmware/vmware_vm_facts.py @@ -0,0 +1,115 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Joseph Callen +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: vmware_vm_facts +short_description: Return basic facts pertaining to a vSphere virtual machine guest +description: + - Return basic facts pertaining to a vSphere virtual machine guest +version_added: 2.0 +author: "Joseph Callen (@jcpowermac)" +notes: + - Tested on vSphere 5.5 +requirements: + - "python >= 2.6" + - PyVmomi +options: + hostname: + description: + - The hostname or IP address of the vSphere vCenter API server + required: True + username: + description: + - The username of the vSphere vCenter + required: True + aliases: ['user', 'admin'] + password: + description: + - The password of the vSphere vCenter + required: True + aliases: ['pass', 'pwd'] +''' + +EXAMPLES = ''' +- name: Gather all registered virtual machines + local_action: + module: vmware_vm_facts + hostname: esxi_or_vcenter_ip_or_hostname + username: username + password: password +''' + +try: + from pyVmomi import vim, vmodl + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + + +# https://github.com/vmware/pyvmomi-community-samples/blob/master/samples/getallvms.py +def get_all_virtual_machines(content): + virtual_machines = get_all_objs(content, [vim.VirtualMachine]) + _virtual_machines = {} + + for vm in virtual_machines: + _ip_address = "" + summary = vm.summary + if summary.guest is not None: + _ip_address = summary.guest.ipAddress + if _ip_address is None: + _ip_address = "" + + virtual_machine = { + summary.config.name: { + "guest_fullname": summary.config.guestFullName, + "power_state": summary.runtime.powerState, + "ip_address": _ip_address + } + } + + _virtual_machines.update(virtual_machine) + return _virtual_machines + + +def main(): + + argument_spec = vmware_argument_spec() + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False) + + if not HAS_PYVMOMI: + module.fail_json(msg='pyvmomi is required for this module') + + try: + content = connect_to_api(module) + _virtual_machines = get_all_virtual_machines(content) + module.exit_json(changed=False, virtual_machines=_virtual_machines) + except vmodl.RuntimeFault as runtime_fault: + module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + module.fail_json(msg=method_fault.msg) + except Exception as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.vmware import * +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From 09a6760c51343b2fabe2c689851b16be8667c3cb Mon Sep 17 00:00:00 2001 From: Joseph Callen Date: Mon, 24 Aug 2015 13:54:09 -0400 Subject: [PATCH 0639/2522] New VMware Module to support migrating vmkernel adapter --- cloud/vmware/vmware_migrate_vmk.py | 219 +++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 cloud/vmware/vmware_migrate_vmk.py diff --git a/cloud/vmware/vmware_migrate_vmk.py b/cloud/vmware/vmware_migrate_vmk.py new file mode 100644 index 00000000000..c658c71b682 --- /dev/null +++ b/cloud/vmware/vmware_migrate_vmk.py @@ -0,0 +1,219 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Joseph Callen +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: vmware_migrate_vmk +short_description: Migrate a VMK interface from VSS to VDS +description: + - Migrate a VMK interface from VSS to VDS +version_added: 2.0 +author: "Joseph Callen (@jcpowermac), Russell Teague (@mtnbikenc)" +notes: + - Tested on vSphere 5.5 +requirements: + - "python >= 2.6" + - PyVmomi +options: + hostname: + description: + - The hostname or IP address of the vSphere vCenter API server + required: True + username: + description: + - The username of the vSphere vCenter + required: True + aliases: ['user', 'admin'] + password: + description: + - The password of the vSphere vCenter + required: True + aliases: ['pass', 'pwd'] + esxi_hostname: + description: + - ESXi hostname to be managed + required: True + device: + description: + - VMK interface name + required: True + current_switch_name: + description: + - Switch VMK interface is currently on + required: True + current_portgroup_name: + description: + - Portgroup name VMK interface is currently on + required: True + migrate_switch_name: + description: + - Switch name to migrate VMK interface to + required: True + migrate_portgroup_name: + description: + - Portgroup name to migrate VMK interface to + required: True +''' + +EXAMPLES = ''' +Example from Ansible playbook + + - name: Migrate Management vmk + local_action: + module: vmware_migrate_vmk + hostname: vcsa_host + username: vcsa_user + password: vcsa_pass + esxi_hostname: esxi_hostname + device: vmk1 + current_switch_name: temp_vswitch + current_portgroup_name: esx-mgmt + migrate_switch_name: dvSwitch + migrate_portgroup_name: Management +''' + + +try: + from pyVmomi import vim, vmodl + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + + +def state_exit_unchanged(module): + module.exit_json(changed=False) + + +def state_migrate_vds_vss(module): + module.exit_json(changed=False, msg="Currently Not Implemented") + + +def create_host_vnic_config(dv_switch_uuid, portgroup_key, device): + + host_vnic_config = vim.host.VirtualNic.Config() + host_vnic_config.spec = vim.host.VirtualNic.Specification() + host_vnic_config.changeOperation = "edit" + host_vnic_config.device = device + host_vnic_config.portgroup = "" + host_vnic_config.spec.distributedVirtualPort = vim.dvs.PortConnection() + host_vnic_config.spec.distributedVirtualPort.switchUuid = dv_switch_uuid + host_vnic_config.spec.distributedVirtualPort.portgroupKey = portgroup_key + + return host_vnic_config + + +def create_port_group_config(switch_name, portgroup_name): + port_group_config = vim.host.PortGroup.Config() + port_group_config.spec = vim.host.PortGroup.Specification() + + port_group_config.changeOperation = "remove" + port_group_config.spec.name = portgroup_name + port_group_config.spec.vlanId = -1 + port_group_config.spec.vswitchName = switch_name + port_group_config.spec.policy = vim.host.NetworkPolicy() + + return port_group_config + + +def state_migrate_vss_vds(module): + content = module.params['content'] + host_system = module.params['host_system'] + migrate_switch_name = module.params['migrate_switch_name'] + migrate_portgroup_name = module.params['migrate_portgroup_name'] + current_portgroup_name = module.params['current_portgroup_name'] + current_switch_name = module.params['current_switch_name'] + device = module.params['device'] + + host_network_system = host_system.configManager.networkSystem + + dv_switch = find_dvs_by_name(content, migrate_switch_name) + pg = find_dvspg_by_name(dv_switch, migrate_portgroup_name) + + config = vim.host.NetworkConfig() + config.portgroup = [create_port_group_config(current_switch_name, current_portgroup_name)] + config.vnic = [create_host_vnic_config(dv_switch.uuid, pg.key, device)] + host_network_system.UpdateNetworkConfig(config, "modify") + module.exit_json(changed=True) + + +def check_vmk_current_state(module): + + device = module.params['device'] + esxi_hostname = module.params['esxi_hostname'] + current_portgroup_name = module.params['current_portgroup_name'] + current_switch_name = module.params['current_switch_name'] + + content = connect_to_api(module) + + host_system = find_hostsystem_by_name(content, esxi_hostname) + + module.params['content'] = content + module.params['host_system'] = host_system + + for vnic in host_system.configManager.networkSystem.networkInfo.vnic: + if vnic.device == device: + module.params['vnic'] = vnic + if vnic.spec.distributedVirtualPort is None: + if vnic.portgroup == current_portgroup_name: + return "migrate_vss_vds" + else: + dvs = find_dvs_by_name(content, current_switch_name) + if dvs is None: + return "migrated" + if vnic.spec.distributedVirtualPort.switchUuid == dvs.uuid: + return "migrate_vds_vss" + + +def main(): + + argument_spec = vmware_argument_spec() + argument_spec.update(dict(esxi_hostname=dict(required=True, type='str'), + device=dict(required=True, type='str'), + current_switch_name=dict(required=True, type='str'), + current_portgroup_name=dict(required=True, type='str'), + migrate_switch_name=dict(required=True, type='str'), + migrate_portgroup_name=dict(required=True, type='str'))) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False) + + if not HAS_PYVMOMI: + module.fail_json(msg='pyvmomi required for this module') + + try: + vmk_migration_states = { + 'migrate_vss_vds': state_migrate_vss_vds, + 'migrate_vds_vss': state_migrate_vds_vss, + 'migrated': state_exit_unchanged + } + + vmk_migration_states[check_vmk_current_state(module)](module) + + except vmodl.RuntimeFault as runtime_fault: + module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + module.fail_json(msg=method_fault.msg) + except Exception as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.vmware import * +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From c48945c10e3073fab2f4374e98055f210e8cb13d Mon Sep 17 00:00:00 2001 From: Russell Teague Date: Mon, 24 Aug 2015 13:55:47 -0400 Subject: [PATCH 0640/2522] Adding vmware_target_canonical_facts module --- cloud/vmware/vmware_target_canonical_facts.py | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 cloud/vmware/vmware_target_canonical_facts.py diff --git a/cloud/vmware/vmware_target_canonical_facts.py b/cloud/vmware/vmware_target_canonical_facts.py new file mode 100644 index 00000000000..987b4a98753 --- /dev/null +++ b/cloud/vmware/vmware_target_canonical_facts.py @@ -0,0 +1,108 @@ +#!/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Joseph Callen +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: vmware_target_canonical_facts +short_description: Return canonical (NAA) from an ESXi host +description: + - Return canonical (NAA) from an ESXi host based on SCSI target ID +version_added: 2.0 +author: Joseph Callen +notes: +requirements: + - Tested on vSphere 5.5 + - PyVmomi installed +options: + hostname: + description: + - The hostname or IP address of the vSphere vCenter + required: True + username: + description: + - The username of the vSphere vCenter + required: True + aliases: ['user', 'admin'] + password: + description: + - The password of the vSphere vCenter + required: True + aliases: ['pass', 'pwd'] + target_id: + description: + - The target id based on order of scsi device + required: True +''' + +EXAMPLES = ''' +# Example vmware_target_canonical_facts command from Ansible Playbooks +- name: Get Canonical name + local_action: > + vmware_target_canonical_facts + hostname="{{ ansible_ssh_host }}" username=root password=vmware + target_id=7 +''' + +try: + from pyVmomi import vim, vmodl + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + + +def find_hostsystem(content): + host_system = get_all_objs(content, [vim.HostSystem]) + for host in host_system: + return host + return None + + +def main(): + + argument_spec = vmware_argument_spec() + argument_spec.update(dict(target_id=dict(required=True, type='int'))) + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False) + + if not HAS_PYVMOMI: + module.fail_json(msg='pyvmomi is required for this module') + + content = connect_to_api(module) + host = find_hostsystem(content) + + target_lun_uuid = {} + scsilun_canonical = {} + + # Associate the scsiLun key with the canonicalName (NAA) + for scsilun in host.config.storageDevice.scsiLun: + scsilun_canonical[scsilun.key] = scsilun.canonicalName + + # Associate target number with LUN uuid + for target in host.config.storageDevice.scsiTopology.adapter[0].target: + for lun in target.lun: + target_lun_uuid[target.target] = lun.scsiLun + + module.exit_json(changed=False, canonical=scsilun_canonical[target_lun_uuid[module.params['target_id']]]) + +from ansible.module_utils.basic import * +from ansible.module_utils.vmware import * + +if __name__ == '__main__': + main() + From 6ca9e7c25f5988426f28042b7df6d2573a105e23 Mon Sep 17 00:00:00 2001 From: Joseph Callen Date: Mon, 24 Aug 2015 13:58:42 -0400 Subject: [PATCH 0641/2522] New VMware Module to support adding standard portgroups --- cloud/vmware/vmware_portgroup.py | 136 +++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 cloud/vmware/vmware_portgroup.py diff --git a/cloud/vmware/vmware_portgroup.py b/cloud/vmware/vmware_portgroup.py new file mode 100644 index 00000000000..e354ded510f --- /dev/null +++ b/cloud/vmware/vmware_portgroup.py @@ -0,0 +1,136 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Joseph Callen +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: vmware_portgroup +short_description: Create a VMware portgroup +description: + - Create a VMware portgroup +version_added: 2.0 +author: "Joseph Callen (@jcpowermac), Russell Teague (@mtnbikenc)" +notes: + - Tested on vSphere 5.5 +requirements: + - "python >= 2.6" + - PyVmomi +options: + hostname: + description: + - The hostname or IP address of the ESXi server + required: True + username: + description: + - The username of the ESXi server + required: True + aliases: ['user', 'admin'] + password: + description: + - The password of the ESXi server + required: True + aliases: ['pass', 'pwd'] + switch_name: + description: + - vSwitch to modify + required: True + portgroup_name: + description: + - Portgroup name to add + required: True + vlan_id: + description: + - VLAN ID to assign to portgroup + required: True +''' + +EXAMPLES = ''' +Example from Ansible playbook + + - name: Add Management Network VM Portgroup + local_action: + module: vmware_portgroup + hostname: esxi_hostname + username: esxi_username + password: esxi_password + switch_name: vswitch_name + portgroup_name: portgroup_name + vlan_id: vlan_id +''' + +try: + from pyVmomi import vim, vmodl + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + + +def create_port_group(host_system, portgroup_name, vlan_id, vswitch_name): + + config = vim.host.NetworkConfig() + config.portgroup = [vim.host.PortGroup.Config()] + config.portgroup[0].changeOperation = "add" + config.portgroup[0].spec = vim.host.PortGroup.Specification() + config.portgroup[0].spec.name = portgroup_name + config.portgroup[0].spec.vlanId = vlan_id + config.portgroup[0].spec.vswitchName = vswitch_name + config.portgroup[0].spec.policy = vim.host.NetworkPolicy() + + host_network_config_result = host_system.configManager.networkSystem.UpdateNetworkConfig(config, "modify") + return True + + +def main(): + + argument_spec = vmware_argument_spec() + argument_spec.update(dict(portgroup_name=dict(required=True, type='str'), + switch_name=dict(required=True, type='str'), + vlan_id=dict(required=True, type='int'))) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False) + + if not HAS_PYVMOMI: + module.fail_json(msg='pyvmomi is required for this module') + + portgroup_name = module.params['portgroup_name'] + switch_name = module.params['switch_name'] + vlan_id = module.params['vlan_id'] + + try: + content = connect_to_api(module) + host = get_all_objs(content, [vim.HostSystem]) + if not host: + raise SystemExit("Unable to locate Physical Host.") + host_system = host.keys()[0] + + changed = create_port_group(host_system, portgroup_name, vlan_id, switch_name) + + module.exit_json(changed=changed) + except vmodl.RuntimeFault as runtime_fault: + module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + module.fail_json(msg=method_fault.msg) + except Exception as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.vmware import * +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From 34247d2aab268842e8c0f0e0f6fe7e764b84f0c0 Mon Sep 17 00:00:00 2001 From: Mark Hamilton Date: Mon, 24 Aug 2015 13:45:12 -0700 Subject: [PATCH 0642/2522] using module.get_bin_path to find ovs-vsctl --- network/openvswitch_db.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/network/openvswitch_db.py b/network/openvswitch_db.py index 320fdf87d3d..d5bc5bc7f37 100644 --- a/network/openvswitch_db.py +++ b/network/openvswitch_db.py @@ -68,7 +68,7 @@ - openvswitch_db: table=open_vswitch record=. col=other_config key=max-idle value=50000 -# Disable in band +# Disable in band copy - openvswitch_db: table=Bridge record=br-int col=other_config key=disable-in-band value=true ''' @@ -76,6 +76,7 @@ def cmd_run(module, cmd, check_rc=True): """ Log and run ovs-vsctl command. """ + syslog.syslog(syslog.LOG_NOTICE, cmd) return module.run_command(cmd.split(" "), check_rc=check_rc) @@ -84,13 +85,18 @@ def params_set(module): changed = False - fmt = "ovs-vsctl -t %(timeout)s get %(table)s %(record)s %(col)s:%(key)s" + ## + # Place in params dictionary in order to support the string format below. + module.params["ovs-vsctl"] = module.get_bin_path("ovs-vsctl", True) + + fmt = "%(ovs-vsctl)s -t %(timeout)s get %(table)s %(record)s " \ + "%(col)s:%(key)s" cmd = fmt % module.params (_, output, _) = cmd_run(module, cmd, False) if module.params['value'] not in output: - fmt = "ovs-vsctl -t %(timeout)s set %(table)s %(record)s " \ + fmt = "%(ovs-vsctl)s -t %(timeout)s set %(table)s %(record)s " \ "%(col)s:%(key)s=%(value)s" cmd = fmt % module.params ## From 485670145729be8e2eb5f19bd06c7d4593ba3e84 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sun, 23 Aug 2015 00:09:38 +0200 Subject: [PATCH 0643/2522] cloudstack: cs_domain: rename argument cleanup to clean_up for consistency --- cloud/cloudstack/cs_domain.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/cloudstack/cs_domain.py b/cloud/cloudstack/cs_domain.py index c9f345a00c2..27410040aec 100644 --- a/cloud/cloudstack/cs_domain.py +++ b/cloud/cloudstack/cs_domain.py @@ -37,7 +37,7 @@ - Network domain for networks in the domain. required: false default: null - cleanup: + clean_up: description: - Clean up all domain resources like child domains and accounts. - Considered on C(state=absent). @@ -225,7 +225,7 @@ def absent_domain(self): if not self.module.check_mode: args = {} args['id'] = domain['id'] - args['cleanup'] = self.module.params.get('cleanup') + args['cleanup'] = self.module.params.get('clean_up') res = self.cs.deleteDomain(**args) if 'errortext' in res: @@ -244,7 +244,7 @@ def main(): path = dict(required=True), state = dict(choices=['present', 'absent'], default='present'), network_domain = dict(default=None), - cleanup = dict(choices=BOOLEANS, default=False), + clean_up = dict(choices=BOOLEANS, default=False), poll_async = dict(choices=BOOLEANS, default=True), api_key = dict(default=None), api_secret = dict(default=None, no_log=True), From 6a37c1b72f19daacdabbb2eb3402ec8a02b5e1be Mon Sep 17 00:00:00 2001 From: Andreas Skarmutsos Lindh Date: Tue, 25 Aug 2015 22:00:03 +0200 Subject: [PATCH 0644/2522] add version_added --- packaging/language/cpanm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packaging/language/cpanm.py b/packaging/language/cpanm.py index b6bdf0c67a0..f0954a783c9 100644 --- a/packaging/language/cpanm.py +++ b/packaging/language/cpanm.py @@ -63,6 +63,7 @@ - Only install dependencies required: false default: false + version_added: 2.0 notes: - Please note that U(http://search.cpan.org/dist/App-cpanminus/bin/cpanm, cpanm) must be installed on the remote host. author: "Franck Cuny (@franckcuny)" From 107510385c3b687f5c8b341201587b60bcace7f4 Mon Sep 17 00:00:00 2001 From: Andreas Skarmutsos Lindh Date: Tue, 25 Aug 2015 22:18:52 +0200 Subject: [PATCH 0645/2522] quoted version_added --- packaging/language/cpanm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/language/cpanm.py b/packaging/language/cpanm.py index f0954a783c9..0bee74de4cc 100644 --- a/packaging/language/cpanm.py +++ b/packaging/language/cpanm.py @@ -63,7 +63,7 @@ - Only install dependencies required: false default: false - version_added: 2.0 + version_added: "2.0" notes: - Please note that U(http://search.cpan.org/dist/App-cpanminus/bin/cpanm, cpanm) must be installed on the remote host. author: "Franck Cuny (@franckcuny)" From c39e7a939cf39682ef12e7ccb8ffd1d7fe2a4bdd Mon Sep 17 00:00:00 2001 From: Kristian Koehntopp Date: Wed, 26 Aug 2015 21:58:34 +0200 Subject: [PATCH 0646/2522] add force= option to allow force installation/removal of packages --- packaging/os/opkg.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packaging/os/opkg.py b/packaging/os/opkg.py index 5b75ad1a260..776c9235678 100644 --- a/packaging/os/opkg.py +++ b/packaging/os/opkg.py @@ -77,6 +77,11 @@ def query_package(module, opkg_path, name, state="present"): def remove_packages(module, opkg_path, packages): """ Uninstalls one or more packages if installed. """ + p = module.params + force = p["force"] + if force: + force = "--force-%s" % force + remove_c = 0 # Using a for loop incase of error, we can report the package that failed for package in packages: @@ -84,7 +89,7 @@ def remove_packages(module, opkg_path, packages): if not query_package(module, opkg_path, package): continue - rc, out, err = module.run_command("%s remove %s" % (opkg_path, package)) + rc, out, err = module.run_command("%s remove %s %s" % (opkg_path, force, package)) if query_package(module, opkg_path, package): module.fail_json(msg="failed to remove %s: %s" % (package, out)) @@ -101,13 +106,18 @@ def remove_packages(module, opkg_path, packages): def install_packages(module, opkg_path, packages): """ Installs one or more packages if not already installed. """ + p = module.params + force = p["force"] + if force: + force = "--force-%s" % force + install_c = 0 for package in packages: if query_package(module, opkg_path, package): continue - rc, out, err = module.run_command("%s install %s" % (opkg_path, package)) + rc, out, err = module.run_command("%s install %s %s" % (opkg_path, force, package)) if not query_package(module, opkg_path, package): module.fail_json(msg="failed to install %s: %s" % (package, out)) @@ -125,6 +135,7 @@ def main(): argument_spec = dict( name = dict(aliases=["pkg"], required=True), state = dict(default="present", choices=["present", "installed", "absent", "removed"]), + force = dict(default="", choices=["", "depends", "maintainer", "reinstall", "overwrite", "downgrade", "space", "postinstall", "remove", "checksum", "removal-of-dependent-packages"]), update_cache = dict(default="no", aliases=["update-cache"], type='bool') ) ) From 2da199b51d2ae0ee7b348e1299ecc6ff59895075 Mon Sep 17 00:00:00 2001 From: Kristian Koehntopp Date: Wed, 26 Aug 2015 22:26:02 +0200 Subject: [PATCH 0647/2522] update inline documentation --- packaging/os/opkg.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packaging/os/opkg.py b/packaging/os/opkg.py index 776c9235678..5fb52eae2eb 100644 --- a/packaging/os/opkg.py +++ b/packaging/os/opkg.py @@ -36,6 +36,12 @@ choices: [ 'present', 'absent' ] required: false default: present + force: + description: + - opkg --force parameter used + choices: ["", "depends", "maintainer", "reinstall", "overwrite", "downgrade", "space", "postinstall", "remove", "checksum", "removal-of-dependent-packages"] + required: false + default: absent update_cache: description: - update the package db first @@ -49,6 +55,7 @@ - opkg: name=foo state=present update_cache=yes - opkg: name=foo state=absent - opkg: name=foo,bar state=absent +- opkg: name=foo state=present force=overwrite ''' import pipes From 02e3adf40258bd157563bb3ccd950c673f7a70bd Mon Sep 17 00:00:00 2001 From: Chrrrles Paul Date: Wed, 26 Aug 2015 20:43:43 -0500 Subject: [PATCH 0648/2522] Revert "New VMware Module to support configuring a VMware vmkernel IP Address" --- cloud/vmware/vmware_vmkernel_ip_config.py | 136 ---------------------- 1 file changed, 136 deletions(-) delete mode 100644 cloud/vmware/vmware_vmkernel_ip_config.py diff --git a/cloud/vmware/vmware_vmkernel_ip_config.py b/cloud/vmware/vmware_vmkernel_ip_config.py deleted file mode 100644 index c07526f0aeb..00000000000 --- a/cloud/vmware/vmware_vmkernel_ip_config.py +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2015, Joseph Callen -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -DOCUMENTATION = ''' ---- -module: vmware_vmkernel_ip_config -short_description: Configure the VMkernel IP Address -description: - - Configure the VMkernel IP Address -version_added: 2.0 -author: "Joseph Callen (@jcpowermac), Russell Teague (@mtnbikenc)" -notes: - - Tested on vSphere 5.5 -requirements: - - "python >= 2.6" - - PyVmomi -options: - hostname: - description: - - The hostname or IP address of the ESXi server - required: True - username: - description: - - The username of the ESXi server - required: True - aliases: ['user', 'admin'] - password: - description: - - The password of the ESXi server - required: True - aliases: ['pass', 'pwd'] - vmk_name: - description: - - VMkernel interface name - required: True - ip_address: - description: - - IP address to assign to VMkernel interface - required: True - subnet_mask: - description: - - Subnet Mask to assign to VMkernel interface - required: True -''' - -EXAMPLES = ''' -# Example command from Ansible Playbook - -- name: Configure IP address on ESX host - local_action: - module: vmware_vmkernel_ip_config - hostname: esxi_hostname - username: esxi_username - password: esxi_password - vmk_name: vmk0 - ip_address: 10.0.0.10 - subnet_mask: 255.255.255.0 -''' - -try: - from pyVmomi import vim, vmodl - HAS_PYVMOMI = True -except ImportError: - HAS_PYVMOMI = False - - -def configure_vmkernel_ip_address(host_system, vmk_name, ip_address, subnet_mask): - - host_config_manager = host_system.configManager - host_network_system = host_config_manager.networkSystem - - for vnic in host_network_system.networkConfig.vnic: - if vnic.device == vmk_name: - spec = vnic.spec - if spec.ip.ipAddress != ip_address: - spec.ip.dhcp = False - spec.ip.ipAddress = ip_address - spec.ip.subnetMask = subnet_mask - host_network_system.UpdateVirtualNic(vmk_name, spec) - return True - return False - - -def main(): - - argument_spec = vmware_argument_spec() - argument_spec.update(dict(vmk_name=dict(required=True, type='str'), - ip_address=dict(required=True, type='str'), - subnet_mask=dict(required=True, type='str'))) - - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False) - - if not HAS_PYVMOMI: - module.fail_json(msg='pyvmomi is required for this module') - - vmk_name = module.params['vmk_name'] - ip_address = module.params['ip_address'] - subnet_mask = module.params['subnet_mask'] - - try: - content = connect_to_api(module, False) - host = get_all_objs(content, [vim.HostSystem]) - if not host: - module.fail_json(msg="Unable to locate Physical Host.") - host_system = host.keys()[0] - changed = configure_vmkernel_ip_address(host_system, vmk_name, ip_address, subnet_mask) - module.exit_json(changed=changed) - except vmodl.RuntimeFault as runtime_fault: - module.fail_json(msg=runtime_fault.msg) - except vmodl.MethodFault as method_fault: - module.fail_json(msg=method_fault.msg) - except Exception as e: - module.fail_json(msg=str(e)) - -from ansible.module_utils.vmware import * -from ansible.module_utils.basic import * - -if __name__ == '__main__': - main() From e8137d40658017a21f8305ae13fa4a74800c269f Mon Sep 17 00:00:00 2001 From: Darren Worrall Date: Thu, 27 Aug 2015 09:19:51 +0100 Subject: [PATCH 0649/2522] Add support for custom service offerings This adds 3 new params: cpu, cpu_speed, and memory, which are required together. --- cloud/cloudstack/cs_instance.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 4ead1317b2f..dce853bba47 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -53,6 +53,21 @@ - If not set, first found service offering is used. required: false default: null + cpu_count: + description: + - The number of CPUs to allocate to the instance, used with custom service offerings + required: false + default: null + cpu_speed: + description: + - The clock speed/shares allocated to the instance, used with custom service offerings + required: false + default: null + memory: + description: + - The clock speed/shares allocated to the instance, used with custom service offerings + required: false + default: null template: description: - Name or id of the template to be used for creating the new instance. @@ -547,6 +562,20 @@ def get_user_data(self): user_data = base64.b64encode(user_data) return user_data + def get_details(self): + res = None + cpu = self.module.params.get('cpu') + cpu_speed = self.module.params.get('cpu_speed') + memory = self.module.params.get('memory') + if all([cpu, cpu_speed, memory]): + res = [{ + 'cpuNumber': cpu, + 'cpuSpeed': cpu_speed, + 'memory': memory, + }] + elif any([cpu, cpu_speed, memory]): + self.module.fail_json(msg='cpu, cpu_speed and memory must be used together') + return res def deploy_instance(self): self.result['changed'] = True @@ -576,6 +605,7 @@ def deploy_instance(self): args['rootdisksize'] = self.module.params.get('root_disk_size') args['securitygroupnames'] = ','.join(self.module.params.get('security_groups')) args['affinitygroupnames'] = ','.join(self.module.params.get('affinity_groups')) + args['details'] = self.get_details() template_iso = self.get_template_or_iso() if 'hypervisor' not in template_iso: @@ -791,6 +821,9 @@ def main(): group = dict(default=None), state = dict(choices=['present', 'deployed', 'started', 'stopped', 'restarted', 'absent', 'destroyed', 'expunged'], default='present'), service_offering = dict(default=None), + cpu = dict(default=None, type='int'), + cpu_speed = dict(default=None, type='int'), + memory = dict(default=None, type='int'), template = dict(default=None), iso = dict(default=None), networks = dict(type='list', aliases=[ 'network' ], default=None), From 0847bfecd672f6b2e0e4429e998df7c6e7042b1c Mon Sep 17 00:00:00 2001 From: Amanpreet Singh Date: Thu, 27 Aug 2015 02:26:42 +0530 Subject: [PATCH 0650/2522] Add new module: pagerduty_alert - trigger, acknowledge or resolve pagerduty incidents --- monitoring/pagerduty_alert.py | 157 ++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 monitoring/pagerduty_alert.py diff --git a/monitoring/pagerduty_alert.py b/monitoring/pagerduty_alert.py new file mode 100644 index 00000000000..a2dddb9ea45 --- /dev/null +++ b/monitoring/pagerduty_alert.py @@ -0,0 +1,157 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' + +module: pagerduty_alert +short_description: Trigger, acknowledge or resolve PagerDuty incidents +description: + - This module will let you trigger, acknowledge or resolve a PagerDuty incident by sending events +version_added: "1.9" +author: + - "Amanpreet Singh (@aps-sids)" +requirements: + - PagerDuty API access +options: + service_key: + description: + - The GUID of one of your "Generic API" services. + - This is the "service key" listed on a Generic API's service detail page. + required: true + event_type: + description: + - Type of event to be sent. + required: true + choices: + - 'trigger' + - 'acknowledge' + - 'resolve' + desc: + description: + - For C(trigger) I(event_type) - Required. Short description of the problem that led to this trigger. This field (or a truncated version) will be used when generating phone calls, SMS messages and alert emails. It will also appear on the incidents tables in the PagerDuty UI. The maximum length is 1024 characters. + - For C(acknowledge) or C(resolve) I(event_type) - Text that will appear in the incident's log associated with this event. + required: false + default: Created via Ansible + incident_key: + description: + - Identifies the incident to which this I(event_type) should be applied. + - For C(trigger) I(event_type) - If there's no open (i.e. unresolved) incident with this key, a new one will be created. If there's already an open incident with a matching key, this event will be appended to that incident's log. The event key provides an easy way to "de-dup" problem reports. + - For C(acknowledge) or C(resolve) I(event_type) - This should be the incident_key you received back when the incident was first opened by a trigger event. Acknowledge events referencing resolved or nonexistent incidents will be discarded. + required: false + client: + description: + - The name of the monitoring client that is triggering this event. + required: false + client_url: + description: + - The URL of the monitoring client that is triggering this event. + required: false +''' + +EXAMPLES = ''' +# Trigger an incident with just the basic options +- pagerduty_alert: + service_key=xxx + event_type=trigger + desc="problem that led to this trigger" + +# Trigger an incident with more options +- pagerduty_alert: + service_key=xxx + event_type=trigger + desc="problem that led to this trigger" + incident_key=somekey + client="Sample Monitoring Service" + client_url=http://service.example.com + +# Acknowledge an incident based on incident_key +- pagerduty_alert: + service_key=xxx + event_type=acknowledge + incident_key=somekey + desc="some text for incident's log" + +# Resolve an incident based on incident_key +- pagerduty_alert: + service_key=xxx + event_type=resolve + incident_key=somekey + desc="some text for incident's log" +''' + + +def send_event(module, service_key, event_type, desc, + incident_key=None, client=None, client_url=None): + url = "https://events.pagerduty.com/generic/2010-04-15/create_event.json" + headers = { + "Content-type": "application/json" + } + + data = { + "service_key": service_key, + "event_type": event_type, + "incident_key": incident_key, + "description": desc, + "client": client, + "client_url": client_url + } + + response, info = fetch_url(module, url, method='post', + headers=headers, data=json.dumps(data)) + if info['status'] != 200: + module.fail_json(msg="failed to %s. Reason: %s" % + (event_type, info['msg'])) + json_out = json.loads(response.read()) + return json_out, True + + +def main(): + module = AnsibleModule( + argument_spec=dict( + service_key=dict(required=True), + event_type=dict(required=True, + choices=['trigger', 'acknowledge', 'resolve']), + client=dict(required=False, default=None), + client_url=dict(required=False, default=None), + desc=dict(required=False, default='Created via Ansible'), + incident_key=dict(required=False, default=None) + ) + ) + + service_key = module.params['service_key'] + event_type = module.params['event_type'] + client = module.params['client'] + client_url = module.params['client_url'] + desc = module.params['desc'] + incident_key = module.params['incident_key'] + + if event_type != 'trigger' and incident_key is None: + module.fail_json(msg="incident_key is required for " + "acknowledge or resolve events") + + out, changed = send_event(module, service_key, event_type, desc, + incident_key, client, client_url) + + module.exit_json(msg="success", result=out, changed=changed) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * + +if __name__ == '__main__': + main() From a0af060c258e4fa116533765a0b955ac3fa815c6 Mon Sep 17 00:00:00 2001 From: Amanpreet Singh Date: Thu, 27 Aug 2015 18:02:45 +0530 Subject: [PATCH 0651/2522] Make pagerduty_alert module more inline with ansible modules - use state parameter instead of event_type - add support for check mode --- monitoring/pagerduty_alert.py | 98 +++++++++++++++++++++++++++-------- 1 file changed, 77 insertions(+), 21 deletions(-) diff --git a/monitoring/pagerduty_alert.py b/monitoring/pagerduty_alert.py index a2dddb9ea45..e2d127f0155 100644 --- a/monitoring/pagerduty_alert.py +++ b/monitoring/pagerduty_alert.py @@ -28,30 +28,38 @@ requirements: - PagerDuty API access options: + name: + description: + - PagerDuty unique subdomain. + required: true service_key: description: - The GUID of one of your "Generic API" services. - This is the "service key" listed on a Generic API's service detail page. required: true - event_type: + state: description: - Type of event to be sent. required: true choices: - - 'trigger' - - 'acknowledge' - - 'resolve' + - 'triggered' + - 'acknowledged' + - 'resolved' + api_key: + description: + - The pagerduty API key (readonly access), generated on the pagerduty site. + required: true desc: description: - - For C(trigger) I(event_type) - Required. Short description of the problem that led to this trigger. This field (or a truncated version) will be used when generating phone calls, SMS messages and alert emails. It will also appear on the incidents tables in the PagerDuty UI. The maximum length is 1024 characters. - - For C(acknowledge) or C(resolve) I(event_type) - Text that will appear in the incident's log associated with this event. + - For C(triggered) I(state) - Required. Short description of the problem that led to this trigger. This field (or a truncated version) will be used when generating phone calls, SMS messages and alert emails. It will also appear on the incidents tables in the PagerDuty UI. The maximum length is 1024 characters. + - For C(acknowledged) or C(resolved) I(state) - Text that will appear in the incident's log associated with this event. required: false default: Created via Ansible incident_key: description: - - Identifies the incident to which this I(event_type) should be applied. - - For C(trigger) I(event_type) - If there's no open (i.e. unresolved) incident with this key, a new one will be created. If there's already an open incident with a matching key, this event will be appended to that incident's log. The event key provides an easy way to "de-dup" problem reports. - - For C(acknowledge) or C(resolve) I(event_type) - This should be the incident_key you received back when the incident was first opened by a trigger event. Acknowledge events referencing resolved or nonexistent incidents will be discarded. + - Identifies the incident to which this I(state) should be applied. + - For C(triggered) I(state) - If there's no open (i.e. unresolved) incident with this key, a new one will be created. If there's already an open incident with a matching key, this event will be appended to that incident's log. The event key provides an easy way to "de-dup" problem reports. + - For C(acknowledged) or C(resolved) I(state) - This should be the incident_key you received back when the incident was first opened by a trigger event. Acknowledge events referencing resolved or nonexistent incidents will be discarded. required: false client: description: @@ -66,14 +74,17 @@ EXAMPLES = ''' # Trigger an incident with just the basic options - pagerduty_alert: + name: companyabc service_key=xxx - event_type=trigger + api_key:yourapikey + state=triggered desc="problem that led to this trigger" # Trigger an incident with more options - pagerduty_alert: service_key=xxx - event_type=trigger + api_key=yourapikey + state=triggered desc="problem that led to this trigger" incident_key=somekey client="Sample Monitoring Service" @@ -82,19 +93,47 @@ # Acknowledge an incident based on incident_key - pagerduty_alert: service_key=xxx - event_type=acknowledge + api_key=yourapikey + state=acknowledged incident_key=somekey desc="some text for incident's log" # Resolve an incident based on incident_key - pagerduty_alert: service_key=xxx - event_type=resolve + api_key=yourapikey + state=resolved incident_key=somekey desc="some text for incident's log" ''' +def check(module, name, state, service_key, api_key, incident_key=None): + url = "https://%s.pagerduty.com/api/v1/incidents" % name + headers = { + "Content-type": "application/json", + "Authorization": "Token token=%s" % api_key + } + + data = { + "service_key": service_key, + "incident_key": incident_key, + "sort_by": "incident_number:desc" + } + + response, info = fetch_url(module, url, method='get', + headers=headers, data=json.dumps(data)) + + if info['status'] != 200: + module.fail_json(msg="failed to check current incident status." + "Reason: %s" % info['msg']) + json_out = json.loads(response.read())["incidents"][0] + + if state != json_out["status"]: + return json_out, True + return json_out, False + + def send_event(module, service_key, event_type, desc, incident_key=None, client=None, client_url=None): url = "https://events.pagerduty.com/generic/2010-04-15/create_event.json" @@ -117,37 +156,54 @@ def send_event(module, service_key, event_type, desc, module.fail_json(msg="failed to %s. Reason: %s" % (event_type, info['msg'])) json_out = json.loads(response.read()) - return json_out, True + return json_out def main(): module = AnsibleModule( argument_spec=dict( + name=dict(required=True), service_key=dict(required=True), - event_type=dict(required=True, - choices=['trigger', 'acknowledge', 'resolve']), + api_key=dict(required=True), + state=dict(required=True, + choices=['triggered', 'acknowledged', 'resolved']), client=dict(required=False, default=None), client_url=dict(required=False, default=None), desc=dict(required=False, default='Created via Ansible'), incident_key=dict(required=False, default=None) - ) + ), + supports_check_mode=True ) + name = module.params['name'] service_key = module.params['service_key'] - event_type = module.params['event_type'] + api_key = module.params['api_key'] + state = module.params['state'] client = module.params['client'] client_url = module.params['client_url'] desc = module.params['desc'] incident_key = module.params['incident_key'] + state_event_dict = { + 'triggered': 'trigger', + 'acknowledged': 'acknowledge', + 'resolved': 'resolve' + } + + event_type = state_event_dict[state] + if event_type != 'trigger' and incident_key is None: module.fail_json(msg="incident_key is required for " "acknowledge or resolve events") - out, changed = send_event(module, service_key, event_type, desc, - incident_key, client, client_url) + out, changed = check(module, name, state, + service_key, api_key, incident_key) + + if not module.check_mode and changed is True: + out = send_event(module, service_key, event_type, desc, + incident_key, client, client_url) - module.exit_json(msg="success", result=out, changed=changed) + module.exit_json(result=out, changed=changed) # import module snippets from ansible.module_utils.basic import * From 2647d2b637d8d885485bdd3a7f57cfd4d9da235f Mon Sep 17 00:00:00 2001 From: Chrrrles Paul Date: Thu, 27 Aug 2015 17:44:29 -0500 Subject: [PATCH 0652/2522] =?UTF-8?q?Revert=20"Revert=20"New=20VMware=20Mo?= =?UTF-8?q?dule=20to=20support=20configuring=20a=20VMware=20vmkernel=20IP?= =?UTF-8?q?=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cloud/vmware/vmware_vmkernel_ip_config.py | 136 ++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 cloud/vmware/vmware_vmkernel_ip_config.py diff --git a/cloud/vmware/vmware_vmkernel_ip_config.py b/cloud/vmware/vmware_vmkernel_ip_config.py new file mode 100644 index 00000000000..c07526f0aeb --- /dev/null +++ b/cloud/vmware/vmware_vmkernel_ip_config.py @@ -0,0 +1,136 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Joseph Callen +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: vmware_vmkernel_ip_config +short_description: Configure the VMkernel IP Address +description: + - Configure the VMkernel IP Address +version_added: 2.0 +author: "Joseph Callen (@jcpowermac), Russell Teague (@mtnbikenc)" +notes: + - Tested on vSphere 5.5 +requirements: + - "python >= 2.6" + - PyVmomi +options: + hostname: + description: + - The hostname or IP address of the ESXi server + required: True + username: + description: + - The username of the ESXi server + required: True + aliases: ['user', 'admin'] + password: + description: + - The password of the ESXi server + required: True + aliases: ['pass', 'pwd'] + vmk_name: + description: + - VMkernel interface name + required: True + ip_address: + description: + - IP address to assign to VMkernel interface + required: True + subnet_mask: + description: + - Subnet Mask to assign to VMkernel interface + required: True +''' + +EXAMPLES = ''' +# Example command from Ansible Playbook + +- name: Configure IP address on ESX host + local_action: + module: vmware_vmkernel_ip_config + hostname: esxi_hostname + username: esxi_username + password: esxi_password + vmk_name: vmk0 + ip_address: 10.0.0.10 + subnet_mask: 255.255.255.0 +''' + +try: + from pyVmomi import vim, vmodl + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + + +def configure_vmkernel_ip_address(host_system, vmk_name, ip_address, subnet_mask): + + host_config_manager = host_system.configManager + host_network_system = host_config_manager.networkSystem + + for vnic in host_network_system.networkConfig.vnic: + if vnic.device == vmk_name: + spec = vnic.spec + if spec.ip.ipAddress != ip_address: + spec.ip.dhcp = False + spec.ip.ipAddress = ip_address + spec.ip.subnetMask = subnet_mask + host_network_system.UpdateVirtualNic(vmk_name, spec) + return True + return False + + +def main(): + + argument_spec = vmware_argument_spec() + argument_spec.update(dict(vmk_name=dict(required=True, type='str'), + ip_address=dict(required=True, type='str'), + subnet_mask=dict(required=True, type='str'))) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False) + + if not HAS_PYVMOMI: + module.fail_json(msg='pyvmomi is required for this module') + + vmk_name = module.params['vmk_name'] + ip_address = module.params['ip_address'] + subnet_mask = module.params['subnet_mask'] + + try: + content = connect_to_api(module, False) + host = get_all_objs(content, [vim.HostSystem]) + if not host: + module.fail_json(msg="Unable to locate Physical Host.") + host_system = host.keys()[0] + changed = configure_vmkernel_ip_address(host_system, vmk_name, ip_address, subnet_mask) + module.exit_json(changed=changed) + except vmodl.RuntimeFault as runtime_fault: + module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + module.fail_json(msg=method_fault.msg) + except Exception as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.vmware import * +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From 50d9589bc84af02b27880c3074e535ecb325eb5c Mon Sep 17 00:00:00 2001 From: Darren Worrall Date: Fri, 28 Aug 2015 08:21:20 +0100 Subject: [PATCH 0653/2522] Use module level validation for params --- cloud/cloudstack/cs_instance.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index dce853bba47..7aa1b1afe99 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -573,8 +573,6 @@ def get_details(self): 'cpuSpeed': cpu_speed, 'memory': memory, }] - elif any([cpu, cpu_speed, memory]): - self.module.fail_json(msg='cpu, cpu_speed and memory must be used together') return res def deploy_instance(self): @@ -857,6 +855,7 @@ def main(): ), required_together = ( ['api_key', 'api_secret', 'api_url'], + ['cpu', 'cpu_speed', 'memory'], ), supports_check_mode=True ) From 37b601b5f911892569eaa9f10e703e9410ae312f Mon Sep 17 00:00:00 2001 From: Darren Worrall Date: Fri, 28 Aug 2015 09:00:30 +0100 Subject: [PATCH 0654/2522] Doc fix --- cloud/cloudstack/cs_instance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 7aa1b1afe99..4c412a903b0 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -53,7 +53,7 @@ - If not set, first found service offering is used. required: false default: null - cpu_count: + cpu: description: - The number of CPUs to allocate to the instance, used with custom service offerings required: false From f91e726bf7e91a8e19e9b97982378cb67ad79de2 Mon Sep 17 00:00:00 2001 From: Konstantin Malov Date: Fri, 28 Aug 2015 13:05:25 +0300 Subject: [PATCH 0655/2522] Add some more locales to LOCALE_NORMALIZATION --- system/locale_gen.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/system/locale_gen.py b/system/locale_gen.py index 410f1dfc23d..e17ed5581da 100644 --- a/system/locale_gen.py +++ b/system/locale_gen.py @@ -52,6 +52,13 @@ ".utf8": ".UTF-8", ".eucjp": ".EUC-JP", ".iso885915": ".ISO-8859-15", + ".cp1251": ".CP1251", + ".koi8r": ".KOI8-R", + ".armscii8": ".ARMSCII-8", + ".euckr": ".EUC-KR", + ".gbk": ".GBK", + ".gb18030": ".GB18030", + ".euctw": ".EUC-TW", } # =========================================== From 29c8b50d569b1fbef9710861133975da0edac636 Mon Sep 17 00:00:00 2001 From: Alex Punco Date: Fri, 28 Aug 2015 13:26:21 +0300 Subject: [PATCH 0656/2522] fix creation containers on btrfs subvolumes --- cloud/lxc/lxc_container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index 1f82bcb829e..adb9637acf9 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -448,7 +448,7 @@ 'zfs_root' ], 'btrfs': [ - 'lv_name', 'vg_name', 'thinpool', 'zfs_root' + 'lv_name', 'vg_name', 'thinpool', 'zfs_root', 'fs_type', 'fs_size' ], 'loop': [ 'lv_name', 'vg_name', 'thinpool', 'zfs_root' From a284c4e974a8e7ddd7cca6d6bce92c21845ee9f7 Mon Sep 17 00:00:00 2001 From: Darren Worrall Date: Fri, 28 Aug 2015 13:29:30 +0100 Subject: [PATCH 0657/2522] More doc fixes --- cloud/cloudstack/cs_instance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 4c412a903b0..4f9b45a6184 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -65,7 +65,7 @@ default: null memory: description: - - The clock speed/shares allocated to the instance, used with custom service offerings + - The memory allocated to the instance, used with custom service offerings required: false default: null template: From 009ee165a8970093080391243949ef1b151a6bb2 Mon Sep 17 00:00:00 2001 From: varnav Date: Fri, 28 Aug 2015 18:38:58 +0300 Subject: [PATCH 0658/2522] Small improvement in documentation --- system/firewalld.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system/firewalld.py b/system/firewalld.py index 04dd4981584..9a63da3a544 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -52,7 +52,7 @@ - 'The firewalld zone to add/remove to/from (NOTE: default zone can be configured per system but "public" is default from upstream. Available choices can be extended based on per-system configs, listed here are "out of the box" defaults).' required: false default: system-default(public) - choices: [ "work", "drop", "internal", "external", "trusted", "home", "dmz", "public", "block"] + choices: [ "work", "drop", "internal", "external", "trusted", "home", "dmz", "public", "block" ] permanent: description: - "Should this configuration be in the running firewalld configuration or persist across reboots." @@ -67,6 +67,7 @@ description: - "Should this port accept(enabled) or reject(disabled) connections." required: true + choices: [ "enabled", "disabled" ] timeout: description: - "The amount of time the rule should be in effect for when non-permanent." From 4d35698a304769b5998a2e7d6662d78e083f1c93 Mon Sep 17 00:00:00 2001 From: Robert Estelle Date: Thu, 13 Nov 2014 19:38:52 -0500 Subject: [PATCH 0659/2522] Split out route table and subnet functionality from VPC module. --- cloud/amazon/ec2_vpc_route_table.py | 498 ++++++++++++++++++++++++++++ 1 file changed, 498 insertions(+) create mode 100644 cloud/amazon/ec2_vpc_route_table.py diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py new file mode 100644 index 00000000000..92d938a6ff6 --- /dev/null +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -0,0 +1,498 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ec2_vpc_route_table +short_description: Configure route tables for AWS virtual private clouds +description: + - Create or removes route tables from AWS virtual private clouds.''' +'''This module has a dependency on python-boto. +version_added: "1.8" +options: + vpc_id: + description: + - "The VPC in which to create the route table." + required: true + route_table_id: + description: + - "The ID of the route table to update or delete." + required: false + default: null + resource_tags: + description: + - 'A dictionary array of resource tags of the form: { tag1: value1,''' +''' tag2: value2 }. Tags in this list are used to uniquely identify route''' +''' tables within a VPC when the route_table_id is not supplied. + required: false + default: null + aliases: [] + version_added: "1.6" + routes: + description: + - List of routes in the route table. Routes are specified''' +''' as dicts containing the keys 'dest' and one of 'gateway_id',''' +''' 'instance_id', 'interface_id', or 'vpc_peering_connection'. + required: true + aliases: [] + subnets: + description: + - An array of subnets to add to this route table. Subnets may either be''' +''' specified by subnet ID or by a CIDR such as '10.0.0.0/24'. + required: true + 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: [] + region: + description: + - region in which the resource exists. + required: false + default: null + aliases: ['aws_region', 'ec2_region'] + aws_secret_key: + description: + - AWS secret key. If not set then the value of the AWS_SECRET_KEY''' +''' environment variable is used. + required: false + default: None + aliases: ['ec2_secret_key', 'secret_key' ] + aws_access_key: + description: + - AWS access key. If not set then the value of the AWS_ACCESS_KEY''' +''' environment variable is used. + required: false + default: None + aliases: ['ec2_access_key', 'access_key' ] + validate_certs: + description: + - When set to "no", SSL certificates will not be validated for boto''' +''' versions >= 2.6.0. + required: false + default: "yes" + choices: ["yes", "no"] + aliases: [] + version_added: "1.5" + +requirements: [ "boto" ] +author: Robert Estelle +''' + +EXAMPLES = ''' +# Note: None of these examples set aws_access_key, aws_secret_key, or region. +# It is assumed that their matching environment variables are set. + +# Basic creation example: +- name: Set up public subnet route table + local_action: + module: ec2_vpc_route_table + vpc_id: vpc-1245678 + region: us-west-1 + resource_tags: + Name: Public + subnets: + - '{{jumpbox_subnet.subnet_id}}' + - '{{frontend_subnet.subnet_id}}' + - '{{vpn_subnet.subnet_id}}' + routes: + - dest: 0.0.0.0/0 + gateway_id: '{{igw.gateway_id}}' + register: public_route_table + +- name: Set up NAT-protected route table + local_action: + module: ec2_vpc_route_table + vpc_id: vpc-1245678 + region: us-west-1 + resource_tags: + - Name: Internal + subnets: + - '{{application_subnet.subnet_id}}' + - '{{database_subnet.subnet_id}}' + - '{{splunk_subnet.subnet_id}}' + routes: + - dest: 0.0.0.0/0 + instance_id: '{{nat.instance_id}}' + register: nat_route_table +''' + + +import sys + +try: + import boto.ec2 + import boto.vpc + from boto.exception import EC2ResponseError +except ImportError: + print "failed=True msg='boto required for this module'" + sys.exit(1) + + +class RouteTableException(Exception): + pass + + +class TagCreationException(RouteTableException): + pass + + +def get_resource_tags(vpc_conn, resource_id): + return {t.name: t.value for t in + vpc_conn.get_all_tags(filters={'resource-id': resource_id})} + + +def dict_diff(old, new): + x = {} + old_keys = set(old.keys()) + new_keys = set(new.keys()) + + for k in old_keys.difference(new_keys): + x[k] = {'old': old[k]} + + for k in new_keys.difference(old_keys): + x[k] = {'new': new[k]} + + for k in new_keys.intersection(old_keys): + if new[k] != old[k]: + x[k] = {'new': new[k], 'old': old[k]} + + return x + + +def tags_match(match_tags, candidate_tags): + return all((k in candidate_tags and candidate_tags[k] == v + for k, v in match_tags.iteritems())) + + +def ensure_tags(vpc_conn, resource_id, tags, add_only, dry_run): + try: + cur_tags = get_resource_tags(vpc_conn, resource_id) + diff = dict_diff(cur_tags, tags) + if not diff: + return {'changed': False, 'tags': cur_tags} + + to_delete = {k: diff[k]['old'] for k in diff if 'new' not in diff[k]} + if to_delete and not add_only: + vpc_conn.delete_tags(resource_id, to_delete, dry_run=dry_run) + + to_add = {k: diff[k]['new'] for k in diff if 'old' not in diff[k]} + if to_add: + vpc_conn.create_tags(resource_id, to_add, dry_run=dry_run) + + latest_tags = get_resource_tags(vpc_conn, resource_id) + return {'changed': True, 'tags': latest_tags} + except EC2ResponseError as e: + raise TagCreationException('Unable to update tags for {0}, error: {1}' + .format(resource_id, e)) + + +def get_route_table_by_id(vpc_conn, vpc_id, route_table_id): + route_tables = vpc_conn.get_all_route_tables( + route_table_ids=[route_table_id], filters={'vpc_id': vpc_id}) + return route_tables[0] if route_tables else None + + +def get_route_table_by_tags(vpc_conn, vpc_id, tags): + route_tables = vpc_conn.get_all_route_tables(filters={'vpc_id': vpc_id}) + for route_table in route_tables: + this_tags = get_resource_tags(vpc_conn, route_table.id) + if tags_match(tags, this_tags): + return route_table + + +def route_spec_matches_route(route_spec, route): + key_attr_map = { + 'destination_cidr_block': 'destination_cidr_block', + 'gateway_id': 'gateway_id', + 'instance_id': 'instance_id', + 'interface_id': 'interface_id', + 'vpc_peering_connection_id': 'vpc_peering_connection_id', + } + for k in key_attr_map.iterkeys(): + if k in route_spec: + if route_spec[k] != getattr(route, k): + return False + return True + + +def rename_key(d, old_key, new_key): + d[new_key] = d[old_key] + del d[old_key] + + +def index_of_matching_route(route_spec, routes_to_match): + for i, route in enumerate(routes_to_match): + if route_spec_matches_route(route_spec, route): + return i + + +def ensure_routes(vpc_conn, route_table, route_specs, check_mode): + routes_to_match = list(route_table.routes) + route_specs_to_create = [] + for route_spec in route_specs: + i = index_of_matching_route(route_spec, routes_to_match) + if i is None: + route_specs_to_create.append(route_spec) + else: + del routes_to_match[i] + routes_to_delete = [r for r in routes_to_match + if r.gateway_id != 'local'] + + changed = routes_to_delete or route_specs_to_create + if check_mode and changed: + return {'changed': True} + elif changed: + for route_spec in route_specs_to_create: + vpc_conn.create_route(route_table.id, **route_spec) + + for route in routes_to_delete: + vpc_conn.delete_route(route_table.id, route.destination_cidr_block) + return {'changed': True} + else: + return {'changed': False} + + +def get_subnet_by_cidr(vpc_conn, vpc_id, cidr): + subnets = vpc_conn.get_all_subnets( + filters={'cidr': cidr, 'vpc_id': vpc_id}) + if len(subnets) != 1: + raise RouteTableException( + 'Subnet with CIDR {0} has {1} matches'.format(cidr, len(subnets)) + ) + return subnets[0] + + +def get_subnet_by_id(vpc_conn, vpc_id, subnet_id): + subnets = vpc_conn.get_all_subnets(filters={'subnet-id': subnet_id}) + if len(subnets) != 1: + raise RouteTableException( + 'Subnet with ID {0} has {1} matches'.format(subnet_id, len(subnets)) + ) + return subnets[0] + + +def ensure_subnet_association(vpc_conn, vpc_id, route_table_id, subnet_id, + check_mode): + route_tables = vpc_conn.get_all_route_tables( + filters={'association.subnet_id': subnet_id, 'vpc_id': vpc_id} + ) + for route_table in route_tables: + if route_table.id is None: + continue + for a in route_table.associations: + if a.subnet_id == subnet_id: + if route_table.id == route_table_id: + return {'changed': False, 'association_id': a.id} + else: + if check_mode: + return {'changed': True} + vpc_conn.disassociate_route_table(a.id) + + association_id = vpc_conn.associate_route_table(route_table_id, subnet_id) + return {'changed': True, 'association_id': association_id} + + +def ensure_subnet_associations(vpc_conn, vpc_id, route_table, subnets, + check_mode): + current_association_ids = [a.id for a in route_table.associations] + new_association_ids = [] + changed = False + for subnet in subnets: + result = ensure_subnet_association( + vpc_conn, vpc_id, route_table.id, subnet.id, check_mode) + changed = changed or result['changed'] + if changed and check_mode: + return {'changed': True} + new_association_ids.append(result['association_id']) + + to_delete = [a_id for a_id in current_association_ids + if a_id not in new_association_ids] + + for a_id in to_delete: + if check_mode: + return {'changed': True} + changed = True + vpc_conn.disassociate_route_table(a_id) + + return {'changed': changed} + + +def ensure_route_table_absent(vpc_conn, vpc_id, route_table_id, resource_tags, + check_mode): + if route_table_id: + route_table = get_route_table_by_id(vpc_conn, vpc_id, route_table_id) + elif resource_tags: + route_table = get_route_table_by_tags(vpc_conn, vpc_id, resource_tags) + else: + raise RouteTableException( + 'must provide route_table_id or resource_tags') + + if route_table is None: + return {'changed': False} + + if check_mode: + return {'changed': True} + + vpc_conn.delete_route_table(route_table.id) + return {'changed': True} + + +def ensure_route_table_present(vpc_conn, vpc_id, route_table_id, resource_tags, + routes, subnets, check_mode): + changed = False + tags_valid = False + if route_table_id: + route_table = get_route_table_by_id(vpc_conn, vpc_id, route_table_id) + elif resource_tags: + route_table = get_route_table_by_tags(vpc_conn, vpc_id, resource_tags) + tags_valid = route_table is not None + else: + raise RouteTableException( + 'must provide route_table_id or resource_tags') + + if check_mode and route_table is None: + return {'changed': True} + + if route_table is None: + try: + route_table = vpc_conn.create_route_table(vpc_id) + except EC2ResponseError as e: + raise RouteTableException( + 'Unable to create route table {0}, error: {1}' + .format(route_table_id or resource_tags, e) + ) + + if not tags_valid and resource_tags is not None: + result = ensure_tags(vpc_conn, route_table.id, resource_tags, + add_only=True, dry_run=check_mode) + changed = changed or result['changed'] + + if routes is not None: + try: + result = ensure_routes(vpc_conn, route_table, routes, check_mode) + changed = changed or result['changed'] + except EC2ResponseError as e: + raise RouteTableException( + 'Unable to ensure routes for route table {0}, error: {1}' + .format(route_table, e) + ) + + if subnets: + associated_subnets = [] + try: + for subnet_name in subnets: + if ('.' in subnet_name) and ('/' in subnet_name): + subnet = get_subnet_by_cidr(vpc_conn, vpc_id, subnet_name) + else: + subnet = get_subnet_by_id(vpc_conn, vpc_id, subnet_name) + associated_subnets.append(subnet) + except EC2ResponseError as e: + raise RouteTableException( + 'Unable to find subnets for route table {0}, error: {1}' + .format(route_table, e) + ) + + try: + result = ensure_subnet_associations( + vpc_conn, vpc_id, route_table, associated_subnets, check_mode) + changed = changed or result['changed'] + except EC2ResponseError as e: + raise RouteTableException( + 'Unable to associate subnets for route table {0}, error: {1}' + .format(route_table, e) + ) + + return { + 'changed': changed, + 'route_table_id': route_table.id, + } + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update({ + 'vpc_id': {'required': True}, + 'route_table_id': {'required': False}, + 'resource_tags': {'type': 'dict', 'required': False}, + 'routes': {'type': 'list', 'required': False}, + 'subnets': {'type': 'list', 'required': False}, + 'state': {'choices': ['present', 'absent'], 'default': 'present'}, + }) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module) + if not region: + module.fail_json(msg='Region must be specified') + + try: + vpc_conn = boto.vpc.connect_to_region( + region, + aws_access_key_id=aws_access_key, + aws_secret_access_key=aws_secret_key + ) + except boto.exception.NoAuthHandlerFound as e: + module.fail_json(msg=str(e)) + + vpc_id = module.params.get('vpc_id') + route_table_id = module.params.get('route_table_id') + resource_tags = module.params.get('resource_tags') + + routes = module.params.get('routes') + for route_spec in routes: + rename_key(route_spec, 'dest', 'destination_cidr_block') + + subnets = module.params.get('subnets') + state = module.params.get('state', 'present') + + try: + if state == 'present': + result = ensure_route_table_present( + vpc_conn, vpc_id, route_table_id, resource_tags, + routes, subnets, module.check_mode + ) + elif state == 'absent': + result = ensure_route_table_absent( + vpc_conn, vpc_id, route_table_id, resource_tags, + module.check_mode + ) + except RouteTableException as e: + module.fail_json(msg=str(e)) + + module.exit_json(**result) + +from ansible.module_utils.basic import * # noqa +from ansible.module_utils.ec2 import * # noqa + +if __name__ == '__main__': + main() From e395bb456ec733be7699002082064310eede9224 Mon Sep 17 00:00:00 2001 From: Robert Estelle Date: Thu, 13 Nov 2014 19:57:15 -0500 Subject: [PATCH 0660/2522] EC2 subnet/route-table: Simplify tag updating. --- cloud/amazon/ec2_vpc_route_table.py | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index 92d938a6ff6..6536ff29f94 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -169,24 +169,6 @@ def get_resource_tags(vpc_conn, resource_id): vpc_conn.get_all_tags(filters={'resource-id': resource_id})} -def dict_diff(old, new): - x = {} - old_keys = set(old.keys()) - new_keys = set(new.keys()) - - for k in old_keys.difference(new_keys): - x[k] = {'old': old[k]} - - for k in new_keys.difference(old_keys): - x[k] = {'new': new[k]} - - for k in new_keys.intersection(old_keys): - if new[k] != old[k]: - x[k] = {'new': new[k], 'old': old[k]} - - return x - - def tags_match(match_tags, candidate_tags): return all((k in candidate_tags and candidate_tags[k] == v for k, v in match_tags.iteritems())) @@ -195,15 +177,14 @@ def tags_match(match_tags, candidate_tags): def ensure_tags(vpc_conn, resource_id, tags, add_only, dry_run): try: cur_tags = get_resource_tags(vpc_conn, resource_id) - diff = dict_diff(cur_tags, tags) - if not diff: + if tags == cur_tags: return {'changed': False, 'tags': cur_tags} - to_delete = {k: diff[k]['old'] for k in diff if 'new' not in diff[k]} + to_delete = {k: cur_tags[k] for k in cur_tags if k not in tags} if to_delete and not add_only: vpc_conn.delete_tags(resource_id, to_delete, dry_run=dry_run) - to_add = {k: diff[k]['new'] for k in diff if 'old' not in diff[k]} + to_add = {k: tags[k] for k in tags if k not in cur_tags} if to_add: vpc_conn.create_tags(resource_id, to_add, dry_run=dry_run) From 60efbe8beccf1768e693788d2698c766e0129450 Mon Sep 17 00:00:00 2001 From: Robert Estelle Date: Mon, 1 Dec 2014 13:41:03 -0500 Subject: [PATCH 0661/2522] ec2_vpc - VPCException -> AnsibleVPCException --- cloud/amazon/ec2_vpc_route_table.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index 6536ff29f94..0f6184c40d4 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -156,11 +156,11 @@ sys.exit(1) -class RouteTableException(Exception): +class AnsibleRouteTableException(Exception): pass -class TagCreationException(RouteTableException): +class AnsibleTagCreationException(AnsibleRouteTableException): pass @@ -191,8 +191,8 @@ def ensure_tags(vpc_conn, resource_id, tags, add_only, dry_run): latest_tags = get_resource_tags(vpc_conn, resource_id) return {'changed': True, 'tags': latest_tags} except EC2ResponseError as e: - raise TagCreationException('Unable to update tags for {0}, error: {1}' - .format(resource_id, e)) + raise AnsibleTagCreationException( + 'Unable to update tags for {0}, error: {1}'.format(resource_id, e)) def get_route_table_by_id(vpc_conn, vpc_id, route_table_id): @@ -265,7 +265,7 @@ def get_subnet_by_cidr(vpc_conn, vpc_id, cidr): subnets = vpc_conn.get_all_subnets( filters={'cidr': cidr, 'vpc_id': vpc_id}) if len(subnets) != 1: - raise RouteTableException( + raise AnsibleRouteTableException( 'Subnet with CIDR {0} has {1} matches'.format(cidr, len(subnets)) ) return subnets[0] @@ -274,8 +274,9 @@ def get_subnet_by_cidr(vpc_conn, vpc_id, cidr): def get_subnet_by_id(vpc_conn, vpc_id, subnet_id): subnets = vpc_conn.get_all_subnets(filters={'subnet-id': subnet_id}) if len(subnets) != 1: - raise RouteTableException( - 'Subnet with ID {0} has {1} matches'.format(subnet_id, len(subnets)) + raise AnsibleRouteTableException( + 'Subnet with ID {0} has {1} matches'.format( + subnet_id, len(subnets)) ) return subnets[0] @@ -333,7 +334,7 @@ def ensure_route_table_absent(vpc_conn, vpc_id, route_table_id, resource_tags, elif resource_tags: route_table = get_route_table_by_tags(vpc_conn, vpc_id, resource_tags) else: - raise RouteTableException( + raise AnsibleRouteTableException( 'must provide route_table_id or resource_tags') if route_table is None: @@ -356,7 +357,7 @@ def ensure_route_table_present(vpc_conn, vpc_id, route_table_id, resource_tags, route_table = get_route_table_by_tags(vpc_conn, vpc_id, resource_tags) tags_valid = route_table is not None else: - raise RouteTableException( + raise AnsibleRouteTableException( 'must provide route_table_id or resource_tags') if check_mode and route_table is None: @@ -366,7 +367,7 @@ def ensure_route_table_present(vpc_conn, vpc_id, route_table_id, resource_tags, try: route_table = vpc_conn.create_route_table(vpc_id) except EC2ResponseError as e: - raise RouteTableException( + raise AnsibleRouteTableException( 'Unable to create route table {0}, error: {1}' .format(route_table_id or resource_tags, e) ) @@ -381,7 +382,7 @@ def ensure_route_table_present(vpc_conn, vpc_id, route_table_id, resource_tags, result = ensure_routes(vpc_conn, route_table, routes, check_mode) changed = changed or result['changed'] except EC2ResponseError as e: - raise RouteTableException( + raise AnsibleRouteTableException( 'Unable to ensure routes for route table {0}, error: {1}' .format(route_table, e) ) @@ -396,7 +397,7 @@ def ensure_route_table_present(vpc_conn, vpc_id, route_table_id, resource_tags, subnet = get_subnet_by_id(vpc_conn, vpc_id, subnet_name) associated_subnets.append(subnet) except EC2ResponseError as e: - raise RouteTableException( + raise AnsibleRouteTableException( 'Unable to find subnets for route table {0}, error: {1}' .format(route_table, e) ) @@ -406,7 +407,7 @@ def ensure_route_table_present(vpc_conn, vpc_id, route_table_id, resource_tags, vpc_conn, vpc_id, route_table, associated_subnets, check_mode) changed = changed or result['changed'] except EC2ResponseError as e: - raise RouteTableException( + raise AnsibleRouteTableException( 'Unable to associate subnets for route table {0}, error: {1}' .format(route_table, e) ) @@ -467,7 +468,7 @@ def main(): vpc_conn, vpc_id, route_table_id, resource_tags, module.check_mode ) - except RouteTableException as e: + except AnsibleRouteTableException as e: module.fail_json(msg=str(e)) module.exit_json(**result) From 95006afe8cf244c462c55c649989f85e606bc79b Mon Sep 17 00:00:00 2001 From: Robert Estelle Date: Mon, 1 Dec 2014 13:45:50 -0500 Subject: [PATCH 0662/2522] ec2_vpc - Fail module using fail_json on boto import failure. --- cloud/amazon/ec2_vpc_route_table.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index 0f6184c40d4..56d3c16c9ec 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -145,15 +145,17 @@ ''' -import sys +import sys # noqa try: import boto.ec2 import boto.vpc from boto.exception import EC2ResponseError + HAS_BOTO = True except ImportError: - print "failed=True msg='boto required for this module'" - sys.exit(1) + HAS_BOTO = False + if __name__ != '__main__': + raise class AnsibleRouteTableException(Exception): @@ -432,6 +434,8 @@ def main(): argument_spec=argument_spec, supports_check_mode=True, ) + if not HAS_BOTO: + module.fail_json(msg='boto is required for this module') ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module) if not region: From a50f5cac2cefb0151981f78b7dfac5a2d80ca19a Mon Sep 17 00:00:00 2001 From: Robert Estelle Date: Mon, 1 Dec 2014 14:28:28 -0500 Subject: [PATCH 0663/2522] ec2_vpc - More efficient tag search. --- cloud/amazon/ec2_vpc_route_table.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index 56d3c16c9ec..e79b1b10ee4 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -171,11 +171,6 @@ def get_resource_tags(vpc_conn, resource_id): vpc_conn.get_all_tags(filters={'resource-id': resource_id})} -def tags_match(match_tags, candidate_tags): - return all((k in candidate_tags and candidate_tags[k] == v - for k, v in match_tags.iteritems())) - - def ensure_tags(vpc_conn, resource_id, tags, add_only, dry_run): try: cur_tags = get_resource_tags(vpc_conn, resource_id) @@ -204,11 +199,18 @@ def get_route_table_by_id(vpc_conn, vpc_id, route_table_id): def get_route_table_by_tags(vpc_conn, vpc_id, tags): - route_tables = vpc_conn.get_all_route_tables(filters={'vpc_id': vpc_id}) - for route_table in route_tables: - this_tags = get_resource_tags(vpc_conn, route_table.id) - if tags_match(tags, this_tags): - return route_table + filters = {'vpc_id': vpc_id} + filters.update({'tag:{}'.format(t): v + for t, v in tags.iteritems()}) + route_tables = vpc_conn.get_all_route_tables(filters=filters) + + if not route_tables: + return None + elif len(route_tables) == 1: + return route_tables[0] + + raise RouteTableException( + 'Found more than one route table based on the supplied tags, aborting') def route_spec_matches_route(route_spec, route): From 0e635dd0907731ffcd4a6962e56ea7295e2482df Mon Sep 17 00:00:00 2001 From: Robert Estelle Date: Mon, 1 Dec 2014 14:50:38 -0500 Subject: [PATCH 0664/2522] ec2_vpc - Update some documentation strings. --- cloud/amazon/ec2_vpc_route_table.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index e79b1b10ee4..60a87ae2430 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -50,8 +50,8 @@ aliases: [] subnets: description: - - An array of subnets to add to this route table. Subnets may either be''' -''' specified by subnet ID or by a CIDR such as '10.0.0.0/24'. + - An array of subnets to add to this route table. Subnets may either''' +''' be specified by subnet ID or by a CIDR such as '10.0.0.0/24'. required: true aliases: [] wait: From e3c14c1b021324f84166f8db7286b2447921ddfe Mon Sep 17 00:00:00 2001 From: Robert Estelle Date: Mon, 1 Dec 2014 15:00:14 -0500 Subject: [PATCH 0665/2522] ec2_vpc - Update dict comprehensions and {} formats for python2.6 --- cloud/amazon/ec2_vpc_route_table.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index 60a87ae2430..f12255e7771 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -200,8 +200,8 @@ def get_route_table_by_id(vpc_conn, vpc_id, route_table_id): def get_route_table_by_tags(vpc_conn, vpc_id, tags): filters = {'vpc_id': vpc_id} - filters.update({'tag:{}'.format(t): v - for t, v in tags.iteritems()}) + filters.update(dict((('tag:{0}'.format(t), v) + for t, v in tags.iteritems()))) route_tables = vpc_conn.get_all_route_tables(filters=filters) if not route_tables: From f79aeaee86d9686c09564a47cd4bf8ec9f04087a Mon Sep 17 00:00:00 2001 From: Robert Estelle Date: Mon, 1 Dec 2014 15:18:56 -0500 Subject: [PATCH 0666/2522] ec2_vpc - More dry running in check mode. --- cloud/amazon/ec2_vpc_route_table.py | 33 ++++++++++++----------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index f12255e7771..7f340359077 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -171,7 +171,7 @@ def get_resource_tags(vpc_conn, resource_id): vpc_conn.get_all_tags(filters={'resource-id': resource_id})} -def ensure_tags(vpc_conn, resource_id, tags, add_only, dry_run): +def ensure_tags(vpc_conn, resource_id, tags, add_only, check_mode): try: cur_tags = get_resource_tags(vpc_conn, resource_id) if tags == cur_tags: @@ -179,11 +179,11 @@ def ensure_tags(vpc_conn, resource_id, tags, add_only, dry_run): to_delete = {k: cur_tags[k] for k in cur_tags if k not in tags} if to_delete and not add_only: - vpc_conn.delete_tags(resource_id, to_delete, dry_run=dry_run) + vpc_conn.delete_tags(resource_id, to_delete, dry_run=check_mode) to_add = {k: tags[k] for k in tags if k not in cur_tags} if to_add: - vpc_conn.create_tags(resource_id, to_add, dry_run=dry_run) + vpc_conn.create_tags(resource_id, to_add, dry_run=check_mode) latest_tags = get_resource_tags(vpc_conn, resource_id) return {'changed': True, 'tags': latest_tags} @@ -252,17 +252,17 @@ def ensure_routes(vpc_conn, route_table, route_specs, check_mode): if r.gateway_id != 'local'] changed = routes_to_delete or route_specs_to_create - if check_mode and changed: - return {'changed': True} - elif changed: + if changed: for route_spec in route_specs_to_create: - vpc_conn.create_route(route_table.id, **route_spec) + vpc_conn.create_route(route_table.id, + dry_run=check_mode, + **route_spec) for route in routes_to_delete: - vpc_conn.delete_route(route_table.id, route.destination_cidr_block) - return {'changed': True} - else: - return {'changed': False} + vpc_conn.delete_route(route_table.id, + route.destination_cidr_block, + dry_run=check_mode) + return {'changed': changed} def get_subnet_by_cidr(vpc_conn, vpc_id, cidr): @@ -323,10 +323,8 @@ def ensure_subnet_associations(vpc_conn, vpc_id, route_table, subnets, if a_id not in new_association_ids] for a_id in to_delete: - if check_mode: - return {'changed': True} changed = True - vpc_conn.disassociate_route_table(a_id) + vpc_conn.disassociate_route_table(a_id, dry_run=check_mode) return {'changed': changed} @@ -344,10 +342,7 @@ def ensure_route_table_absent(vpc_conn, vpc_id, route_table_id, resource_tags, if route_table is None: return {'changed': False} - if check_mode: - return {'changed': True} - - vpc_conn.delete_route_table(route_table.id) + vpc_conn.delete_route_table(route_table.id, dry_run=check_mode) return {'changed': True} @@ -378,7 +373,7 @@ def ensure_route_table_present(vpc_conn, vpc_id, route_table_id, resource_tags, if not tags_valid and resource_tags is not None: result = ensure_tags(vpc_conn, route_table.id, resource_tags, - add_only=True, dry_run=check_mode) + add_only=True, check_mode=check_mode) changed = changed or result['changed'] if routes is not None: From f4ce0dbc96b72a24c0527ccf3af37110fbfaf7de Mon Sep 17 00:00:00 2001 From: Robert Estelle Date: Mon, 1 Dec 2014 15:56:04 -0500 Subject: [PATCH 0667/2522] ec2_vpc_route_table - Support route propagation through VGW. Based on work by Bret Martin via pull request #356 --- cloud/amazon/ec2_vpc_route_table.py | 36 +++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index 7f340359077..fc736ba451c 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -54,6 +54,11 @@ ''' be specified by subnet ID or by a CIDR such as '10.0.0.0/24'. required: true aliases: [] + propagating_vgw_ids: + description: + - Enables route propagation from virtual gateways specified by ID. + required: false + aliases: [] wait: description: - wait for the VPC to be in state 'available' before returning @@ -329,6 +334,24 @@ def ensure_subnet_associations(vpc_conn, vpc_id, route_table, subnets, return {'changed': changed} +def ensure_propagation(vpc_conn, route_table_id, propagating_vgw_ids, + check_mode): + + # NOTE: As of boto==2.15.0, it is not yet possible to query the existing + # propagating gateways. However, EC2 does support this as evidenced by + # the describe-route-tables tool. For now, just enable the given VGWs + # and do not disable any others. + changed = False + for vgw_id in propagating_vgw_ids: + if vgw_id not in original_association_ids: + changed = True + vpc_conn.enable_vgw_route_propagation(route_table_id, + vgw_id, + test_run=check_mode) + + return {'changed': changed} + + def ensure_route_table_absent(vpc_conn, vpc_id, route_table_id, resource_tags, check_mode): if route_table_id: @@ -347,7 +370,8 @@ def ensure_route_table_absent(vpc_conn, vpc_id, route_table_id, resource_tags, def ensure_route_table_present(vpc_conn, vpc_id, route_table_id, resource_tags, - routes, subnets, check_mode): + routes, subnets, propagating_vgw_ids, + check_mode): changed = False tags_valid = False if route_table_id: @@ -371,6 +395,12 @@ def ensure_route_table_present(vpc_conn, vpc_id, route_table_id, resource_tags, .format(route_table_id or resource_tags, e) ) + if propagating_vgw_ids is not None: + result = ensure_propagation(vpc_conn, route_table_id, + propagating_vgw_ids, + check_mode=check_mode) + changed = changed or result['changed'] + if not tags_valid and resource_tags is not None: result = ensure_tags(vpc_conn, route_table.id, resource_tags, add_only=True, check_mode=check_mode) @@ -422,6 +452,7 @@ def main(): argument_spec.update({ 'vpc_id': {'required': True}, 'route_table_id': {'required': False}, + 'propagating_vgw_ids': {'type': 'list', 'required': False}, 'resource_tags': {'type': 'dict', 'required': False}, 'routes': {'type': 'list', 'required': False}, 'subnets': {'type': 'list', 'required': False}, @@ -450,6 +481,7 @@ def main(): vpc_id = module.params.get('vpc_id') route_table_id = module.params.get('route_table_id') resource_tags = module.params.get('resource_tags') + propagating_vgw_ids = module.params.get('propagating_vgw_ids', []) routes = module.params.get('routes') for route_spec in routes: @@ -462,7 +494,7 @@ def main(): if state == 'present': result = ensure_route_table_present( vpc_conn, vpc_id, route_table_id, resource_tags, - routes, subnets, module.check_mode + routes, subnets, propagating_vgw_ids, module.check_mode ) elif state == 'absent': result = ensure_route_table_absent( From f0a4be1b4bce41ece9e4ab033fc2cbebc444f2b8 Mon Sep 17 00:00:00 2001 From: Robert Estelle Date: Wed, 3 Dec 2014 13:01:44 -0500 Subject: [PATCH 0668/2522] ec2_vpc_route_table - Fix unintended tag search regression. --- cloud/amazon/ec2_vpc_route_table.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index fc736ba451c..b6fda27b703 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -176,6 +176,11 @@ def get_resource_tags(vpc_conn, resource_id): vpc_conn.get_all_tags(filters={'resource-id': resource_id})} +def tags_match(match_tags, candidate_tags): + return all((k in candidate_tags and candidate_tags[k] == v + for k, v in match_tags.iteritems())) + + def ensure_tags(vpc_conn, resource_id, tags, add_only, check_mode): try: cur_tags = get_resource_tags(vpc_conn, resource_id) @@ -204,18 +209,11 @@ def get_route_table_by_id(vpc_conn, vpc_id, route_table_id): def get_route_table_by_tags(vpc_conn, vpc_id, tags): - filters = {'vpc_id': vpc_id} - filters.update(dict((('tag:{0}'.format(t), v) - for t, v in tags.iteritems()))) - route_tables = vpc_conn.get_all_route_tables(filters=filters) - - if not route_tables: - return None - elif len(route_tables) == 1: - return route_tables[0] - - raise RouteTableException( - 'Found more than one route table based on the supplied tags, aborting') + route_tables = vpc_conn.get_all_route_tables(filters={'vpc_id': vpc_id}) + for route_table in route_tables: + this_tags = get_resource_tags(vpc_conn, route_table.id) + if tags_match(tags, this_tags): + return route_table def route_spec_matches_route(route_spec, route): From 17ed722d556a4a24a3dadfd37c2833f57287d1c5 Mon Sep 17 00:00:00 2001 From: Robert Estelle Date: Thu, 4 Dec 2014 22:10:02 -0500 Subject: [PATCH 0669/2522] ec2_vpc_route_tables - Remove more dict comprehensions. --- cloud/amazon/ec2_vpc_route_table.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index b6fda27b703..af28ef341cc 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -172,8 +172,8 @@ class AnsibleTagCreationException(AnsibleRouteTableException): def get_resource_tags(vpc_conn, resource_id): - return {t.name: t.value for t in - vpc_conn.get_all_tags(filters={'resource-id': resource_id})} + return dict((t.name, t.value) for t in + vpc_conn.get_all_tags(filters={'resource-id': resource_id})) def tags_match(match_tags, candidate_tags): @@ -187,11 +187,11 @@ def ensure_tags(vpc_conn, resource_id, tags, add_only, check_mode): if tags == cur_tags: return {'changed': False, 'tags': cur_tags} - to_delete = {k: cur_tags[k] for k in cur_tags if k not in tags} + to_delete = dict((k, cur_tags[k]) for k in cur_tags if k not in tags) if to_delete and not add_only: vpc_conn.delete_tags(resource_id, to_delete, dry_run=check_mode) - to_add = {k: tags[k] for k in tags if k not in cur_tags} + to_add = dict((k, tags[k]) for k in tags if k not in cur_tags) if to_add: vpc_conn.create_tags(resource_id, to_add, dry_run=check_mode) From 43566b0cafd5f3b6979a4cd182ee3505717a9a71 Mon Sep 17 00:00:00 2001 From: Robert Estelle Date: Thu, 4 Dec 2014 22:10:49 -0500 Subject: [PATCH 0670/2522] ec2_vpc_route_tables - Allow reference to subnets by id, name, or cidr. --- cloud/amazon/ec2_vpc_route_table.py | 98 ++++++++++++++++++++--------- 1 file changed, 69 insertions(+), 29 deletions(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index af28ef341cc..491751e23dd 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -51,7 +51,7 @@ subnets: description: - An array of subnets to add to this route table. Subnets may either''' -''' be specified by subnet ID or by a CIDR such as '10.0.0.0/24'. +''' be specified by subnet ID, Name tag, or by a CIDR such as '10.0.0.0/24'. required: true aliases: [] propagating_vgw_ids: @@ -141,8 +141,8 @@ - Name: Internal subnets: - '{{application_subnet.subnet_id}}' - - '{{database_subnet.subnet_id}}' - - '{{splunk_subnet.subnet_id}}' + - 'Database Subnet' + - '10.0.0.0/8' routes: - dest: 0.0.0.0/0 instance_id: '{{nat.instance_id}}' @@ -151,6 +151,7 @@ import sys # noqa +import re try: import boto.ec2 @@ -171,6 +172,70 @@ class AnsibleTagCreationException(AnsibleRouteTableException): pass +class AnsibleSubnetSearchException(AnsibleRouteTableException): + pass + +CIDR_RE = re.compile('^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$') +SUBNET_RE = re.compile('^subnet-[A-z0-9]+$') +ROUTE_TABLE_RE = re.compile('^rtb-[A-z0-9]+$') + + +def find_subnets(vpc_conn, vpc_id, identified_subnets): + """ + Finds a list of subnets, each identified either by a raw ID, a unique + 'Name' tag, or a CIDR such as 10.0.0.0/8. + + Note that this function is duplicated in other ec2 modules, and should + potentially be moved into potentially be moved into a shared module_utils + """ + subnet_ids = [] + subnet_names = [] + subnet_cidrs = [] + for subnet in (identified_subnets or []): + if re.match(SUBNET_RE, subnet): + subnet_ids.append(subnet) + elif re.match(CIDR_RE, subnet): + subnet_cidrs.append(subnet) + else: + subnet_names.append(subnet) + + subnets_by_id = [] + if subnet_ids: + subnets_by_id = vpc_conn.get_all_subnets( + subnet_ids, filters={'vpc_id': vpc_id}) + + for subnet_id in subnet_ids: + if not any(s.id == subnet_id for s in subnets_by_id): + raise AnsibleSubnetSearchException( + 'Subnet ID "{0}" does not exist'.format(subnet_id)) + + subnets_by_cidr = [] + if subnet_cidrs: + subnets_by_cidr = vpc_conn.get_all_subnets( + filters={'vpc_id': vpc_id, 'cidr': subnet_cidrs}) + + for cidr in subnet_cidrs: + if not any(s.cidr_block == cidr for s in subnets_by_cidr): + raise AnsibleSubnetSearchException( + 'Subnet CIDR "{0}" does not exist'.format(subnet_cidr)) + + subnets_by_name = [] + if subnet_names: + subnets_by_name = vpc_conn.get_all_subnets( + filters={'vpc_id': vpc_id, 'tag:Name': subnet_names}) + + for name in subnet_names: + matching = [s.tags.get('Name') == name for s in subnets_by_name] + if len(matching) == 0: + raise AnsibleSubnetSearchException( + 'Subnet named "{0}" does not exist'.format(name)) + elif len(matching) > 1: + raise AnsibleSubnetSearchException( + 'Multiple subnets named "{0}"'.format(name)) + + return subnets_by_id + subnets_by_cidr + subnets_by_name + + def get_resource_tags(vpc_conn, resource_id): return dict((t.name, t.value) for t in vpc_conn.get_all_tags(filters={'resource-id': resource_id})) @@ -268,26 +333,6 @@ def ensure_routes(vpc_conn, route_table, route_specs, check_mode): return {'changed': changed} -def get_subnet_by_cidr(vpc_conn, vpc_id, cidr): - subnets = vpc_conn.get_all_subnets( - filters={'cidr': cidr, 'vpc_id': vpc_id}) - if len(subnets) != 1: - raise AnsibleRouteTableException( - 'Subnet with CIDR {0} has {1} matches'.format(cidr, len(subnets)) - ) - return subnets[0] - - -def get_subnet_by_id(vpc_conn, vpc_id, subnet_id): - subnets = vpc_conn.get_all_subnets(filters={'subnet-id': subnet_id}) - if len(subnets) != 1: - raise AnsibleRouteTableException( - 'Subnet with ID {0} has {1} matches'.format( - subnet_id, len(subnets)) - ) - return subnets[0] - - def ensure_subnet_association(vpc_conn, vpc_id, route_table_id, subnet_id, check_mode): route_tables = vpc_conn.get_all_route_tables( @@ -417,12 +462,7 @@ def ensure_route_table_present(vpc_conn, vpc_id, route_table_id, resource_tags, if subnets: associated_subnets = [] try: - for subnet_name in subnets: - if ('.' in subnet_name) and ('/' in subnet_name): - subnet = get_subnet_by_cidr(vpc_conn, vpc_id, subnet_name) - else: - subnet = get_subnet_by_id(vpc_conn, vpc_id, subnet_name) - associated_subnets.append(subnet) + associated_subnets = find_subnets(vpc_conn, vpc_id, subnets) except EC2ResponseError as e: raise AnsibleRouteTableException( 'Unable to find subnets for route table {0}, error: {1}' From c9883db03d668c8b5eeff1204c3602c3006fd905 Mon Sep 17 00:00:00 2001 From: Herby Gillot Date: Thu, 11 Jun 2015 14:01:40 +1000 Subject: [PATCH 0671/2522] Allow VPC igw to be specified by gateway_id: "igw" --- cloud/amazon/ec2_vpc_route_table.py | 36 ++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index 491751e23dd..dc21d9607e1 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -45,7 +45,9 @@ description: - List of routes in the route table. Routes are specified''' ''' as dicts containing the keys 'dest' and one of 'gateway_id',''' -''' 'instance_id', 'interface_id', or 'vpc_peering_connection'. +''' 'instance_id', 'interface_id', or 'vpc_peering_connection'. ''' +''' If 'gateway_id' is specified, you can refer to the VPC's IGW ''' +''' by using the value "igw". required: true aliases: [] subnets: @@ -168,6 +170,10 @@ class AnsibleRouteTableException(Exception): pass +class AnsibleIgwSearchException(AnsibleRouteTableException): + pass + + class AnsibleTagCreationException(AnsibleRouteTableException): pass @@ -236,6 +242,29 @@ def find_subnets(vpc_conn, vpc_id, identified_subnets): return subnets_by_id + subnets_by_cidr + subnets_by_name +def find_igw(vpc_conn, vpc_id): + """ + Finds the Internet gateway for the given VPC ID. + + Raises an AnsibleIgwSearchException if either no IGW can be found, or more + than one found for the given VPC. + + Note that this function is duplicated in other ec2 modules, and should + potentially be moved into potentially be moved into a shared module_utils + """ + igw = vpc_conn.get_all_internet_gateways( + filters={'attachment.vpc-id': vpc_id}) + + if not igw: + return AnsibleIgwSearchException('No IGW found for VPC "{0}"'. + format(vpc_id)) + elif len(igw) == 1: + return igw[0].id + else: + raise AnsibleIgwSearchException('Multiple IGWs found for VPC "{0}"'. + format(vpc_id)) + + def get_resource_tags(vpc_conn, resource_id): return dict((t.name, t.value) for t in vpc_conn.get_all_tags(filters={'resource-id': resource_id})) @@ -525,6 +554,11 @@ def main(): for route_spec in routes: rename_key(route_spec, 'dest', 'destination_cidr_block') + if 'gateway_id' in route_spec and route_spec['gateway_id'] and \ + route_spec['gateway_id'].lower() == 'igw': + igw = find_igw(vpc_conn, vpc_id) + route_spec['gateway_id'] = igw + subnets = module.params.get('subnets') state = module.params.get('state', 'present') From 4f2cd7cb6e0ae12a578bc5768846272474c71f46 Mon Sep 17 00:00:00 2001 From: whiter Date: Thu, 11 Jun 2015 14:07:04 +1000 Subject: [PATCH 0672/2522] Documentation update --- cloud/amazon/ec2_vpc_route_table.py | 87 +++++++---------------------- 1 file changed, 21 insertions(+), 66 deletions(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index dc21d9607e1..677d2ea3383 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -1,121 +1,75 @@ #!/usr/bin/python -# This file is part of Ansible # -# Ansible is free software: you can redistribute it and/or modify +# This is a free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # -# Ansible is distributed in the hope that it will be useful, +# This Ansible library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . +# along with this library. If not, see . DOCUMENTATION = ''' --- module: ec2_vpc_route_table -short_description: Configure route tables for AWS virtual private clouds +short_description: Manage route tables for AWS virtual private clouds description: - - Create or removes route tables from AWS virtual private clouds.''' -'''This module has a dependency on python-boto. -version_added: "1.8" + - Manage route tables for AWS virtual private clouds +version_added: "2.0" +author: Robert Estelle, @erydo options: vpc_id: description: - - "The VPC in which to create the route table." + - VPC ID of the VPC in which to create the route table. required: true route_table_id: description: - - "The ID of the route table to update or delete." + - The ID of the route table to update or delete. required: false default: null resource_tags: description: - - 'A dictionary array of resource tags of the form: { tag1: value1,''' -''' tag2: value2 }. Tags in this list are used to uniquely identify route''' -''' tables within a VPC when the route_table_id is not supplied. + - A dictionary array of resource tags of the form: { tag1: value1, tag2: value2 }. Tags in this list are used to uniquely identify route tables within a VPC when the route_table_id is not supplied. required: false default: null - aliases: [] - version_added: "1.6" routes: description: - - List of routes in the route table. Routes are specified''' -''' as dicts containing the keys 'dest' and one of 'gateway_id',''' -''' 'instance_id', 'interface_id', or 'vpc_peering_connection'. ''' -''' If 'gateway_id' is specified, you can refer to the VPC's IGW ''' -''' by using the value "igw". + - List of routes in the route table. Routes are specified as dicts containing the keys 'dest' and one of 'gateway_id', 'instance_id', 'interface_id', or 'vpc_peering_connection'. If 'gateway_id' is specified, you can refer to the VPC's IGW by using the value 'igw'. required: true aliases: [] subnets: description: - - An array of subnets to add to this route table. Subnets may either''' -''' be specified by subnet ID, Name tag, or by a CIDR such as '10.0.0.0/24'. + - An array of subnets to add to this route table. Subnets may be specified by either subnet ID, Name tag, or by a CIDR such as '10.0.0.0/24'. required: true - aliases: [] propagating_vgw_ids: description: - - Enables route propagation from virtual gateways specified by ID. + - Enable route propagation from virtual gateways specified by ID. required: false - aliases: [] wait: description: - - wait for the VPC to be in state 'available' before returning + - 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 + - How long before wait gives up, in seconds. default: 300 - aliases: [] state: description: - - Create or terminate the VPC - required: true - default: present - aliases: [] - region: - description: - - region in which the resource exists. + - Create or destroy the VPC route table required: false - default: null - aliases: ['aws_region', 'ec2_region'] - aws_secret_key: - description: - - AWS secret key. If not set then the value of the AWS_SECRET_KEY''' -''' environment variable is used. - required: false - default: None - aliases: ['ec2_secret_key', 'secret_key' ] - aws_access_key: - description: - - AWS access key. If not set then the value of the AWS_ACCESS_KEY''' -''' environment variable is used. - required: false - default: None - aliases: ['ec2_access_key', 'access_key' ] - validate_certs: - description: - - When set to "no", SSL certificates will not be validated for boto''' -''' versions >= 2.6.0. - required: false - default: "yes" - choices: ["yes", "no"] - aliases: [] - version_added: "1.5" - -requirements: [ "boto" ] -author: Robert Estelle + default: present + choices: [ 'present', 'absent' ] +extends_documentation_fragment: aws ''' EXAMPLES = ''' -# Note: None of these examples set aws_access_key, aws_secret_key, or region. -# It is assumed that their matching environment variables are set. +# Note: These examples do not set authentication details, see the AWS Guide for details. # Basic creation example: - name: Set up public subnet route table @@ -583,3 +537,4 @@ def main(): if __name__ == '__main__': main() + \ No newline at end of file From 3527aec2c5209f10b90827dc7f697cdaffeb2d63 Mon Sep 17 00:00:00 2001 From: whiter Date: Fri, 19 Jun 2015 09:56:50 +1000 Subject: [PATCH 0673/2522] Changed to use "connect_to_aws" method --- cloud/amazon/ec2_vpc_route_table.py | 54 ++++++++++++++--------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index 677d2ea3383..b0fa9cbc426 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -20,7 +20,7 @@ description: - Manage route tables for AWS virtual private clouds version_added: "2.0" -author: Robert Estelle, @erydo +author: Robert Estelle (@erydo) options: vpc_id: description: @@ -470,34 +470,32 @@ def ensure_route_table_present(vpc_conn, vpc_id, route_table_id, resource_tags, def main(): argument_spec = ec2_argument_spec() - argument_spec.update({ - 'vpc_id': {'required': True}, - 'route_table_id': {'required': False}, - 'propagating_vgw_ids': {'type': 'list', 'required': False}, - 'resource_tags': {'type': 'dict', 'required': False}, - 'routes': {'type': 'list', 'required': False}, - 'subnets': {'type': 'list', 'required': False}, - 'state': {'choices': ['present', 'absent'], 'default': 'present'}, - }) - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, + argument_spec.update( + dict( + vpc_id = dict(default=None, required=True), + route_table_id = dict(default=None, required=False), + propagating_vgw_ids = dict(default=None, required=False, type='list'), + resource_tags = dict(default=None, required=False, type='dict'), + routes = dict(default=None, required=False, type='list'), + subnets = dict(default=None, required=False, type='list'), + state = dict(default='present', choices=['present', 'absent']) + ) ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + if not HAS_BOTO: module.fail_json(msg='boto is required for this module') - ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module) - if not region: - module.fail_json(msg='Region must be specified') - - try: - vpc_conn = boto.vpc.connect_to_region( - region, - aws_access_key_id=aws_access_key, - aws_secret_access_key=aws_secret_key - ) - except boto.exception.NoAuthHandlerFound as e: - module.fail_json(msg=str(e)) + region, ec2_url, aws_connect_params = get_aws_connection_info(module) + + if region: + try: + connection = connect_to_aws(boto.vpc, region, **aws_connect_params) + except (boto.exception.NoAuthHandlerFound, StandardError), e: + module.fail_json(msg=str(e)) + else: + module.fail_json(msg="region must be specified") vpc_id = module.params.get('vpc_id') route_table_id = module.params.get('route_table_id') @@ -510,7 +508,7 @@ def main(): if 'gateway_id' in route_spec and route_spec['gateway_id'] and \ route_spec['gateway_id'].lower() == 'igw': - igw = find_igw(vpc_conn, vpc_id) + igw = find_igw(connection, vpc_id) route_spec['gateway_id'] = igw subnets = module.params.get('subnets') @@ -519,12 +517,12 @@ def main(): try: if state == 'present': result = ensure_route_table_present( - vpc_conn, vpc_id, route_table_id, resource_tags, + connection, vpc_id, route_table_id, resource_tags, routes, subnets, propagating_vgw_ids, module.check_mode ) elif state == 'absent': result = ensure_route_table_absent( - vpc_conn, vpc_id, route_table_id, resource_tags, + connection, vpc_id, route_table_id, resource_tags, module.check_mode ) except AnsibleRouteTableException as e: From 3e02c0d3d940c3e7bb0e4c0c3128cf8ffacb6dad Mon Sep 17 00:00:00 2001 From: Rob White Date: Tue, 28 Jul 2015 21:39:09 +1000 Subject: [PATCH 0674/2522] Blank aliases removed --- cloud/amazon/ec2_vpc_route_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index b0fa9cbc426..6b3efa3286a 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -40,7 +40,6 @@ description: - List of routes in the route table. Routes are specified as dicts containing the keys 'dest' and one of 'gateway_id', 'instance_id', 'interface_id', or 'vpc_peering_connection'. If 'gateway_id' is specified, you can refer to the VPC's IGW by using the value 'igw'. required: true - aliases: [] subnets: description: - An array of subnets to add to this route table. Subnets may be specified by either subnet ID, Name tag, or by a CIDR such as '10.0.0.0/24'. @@ -65,6 +64,7 @@ required: false default: present choices: [ 'present', 'absent' ] + extends_documentation_fragment: aws ''' From 546858cec9afbbde1862b9ac4e40da88a1067dc1 Mon Sep 17 00:00:00 2001 From: Bret Martin Date: Mon, 10 Aug 2015 14:30:09 -0400 Subject: [PATCH 0675/2522] Correct enable_vgw_route_propagation test_run parameter to dry_run --- cloud/amazon/ec2_vpc_route_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index 6b3efa3286a..53164e254d9 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -373,7 +373,7 @@ def ensure_propagation(vpc_conn, route_table_id, propagating_vgw_ids, changed = True vpc_conn.enable_vgw_route_propagation(route_table_id, vgw_id, - test_run=check_mode) + dry_run=check_mode) return {'changed': changed} From 954f48f28aed4be47634613ccb0a051ef7ab9874 Mon Sep 17 00:00:00 2001 From: Bret Martin Date: Mon, 10 Aug 2015 14:31:46 -0400 Subject: [PATCH 0676/2522] Don't check original_association_ids since it is not set, per comment above --- cloud/amazon/ec2_vpc_route_table.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index 53164e254d9..bb530cb0e9e 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -369,11 +369,10 @@ def ensure_propagation(vpc_conn, route_table_id, propagating_vgw_ids, # and do not disable any others. changed = False for vgw_id in propagating_vgw_ids: - if vgw_id not in original_association_ids: - changed = True - vpc_conn.enable_vgw_route_propagation(route_table_id, - vgw_id, - dry_run=check_mode) + changed = True + vpc_conn.enable_vgw_route_propagation(route_table_id, + vgw_id, + dry_run=check_mode) return {'changed': changed} From 271cbe833e1ecbc8e43fd295f7d7c140ee41e619 Mon Sep 17 00:00:00 2001 From: Bret Martin Date: Mon, 10 Aug 2015 14:35:25 -0400 Subject: [PATCH 0677/2522] Call ensure_propagation() with the retrieved route table ID --- cloud/amazon/ec2_vpc_route_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index bb530cb0e9e..faafc6955e6 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -421,7 +421,7 @@ def ensure_route_table_present(vpc_conn, vpc_id, route_table_id, resource_tags, ) if propagating_vgw_ids is not None: - result = ensure_propagation(vpc_conn, route_table_id, + result = ensure_propagation(vpc_conn, route_table.id, propagating_vgw_ids, check_mode=check_mode) changed = changed or result['changed'] From 29ce49e84f207892c618d89523ca938e07ec01dd Mon Sep 17 00:00:00 2001 From: Bret Martin Date: Mon, 10 Aug 2015 15:10:19 -0400 Subject: [PATCH 0678/2522] Don't attempt to delete routes using propagating virtual gateways --- cloud/amazon/ec2_vpc_route_table.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index faafc6955e6..d93effcc550 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -290,7 +290,8 @@ def index_of_matching_route(route_spec, routes_to_match): return i -def ensure_routes(vpc_conn, route_table, route_specs, check_mode): +def ensure_routes(vpc_conn, route_table, route_specs, propagating_vgw_ids, + check_mode): routes_to_match = list(route_table.routes) route_specs_to_create = [] for route_spec in route_specs: @@ -299,8 +300,16 @@ def ensure_routes(vpc_conn, route_table, route_specs, check_mode): route_specs_to_create.append(route_spec) else: del routes_to_match[i] + + # NOTE: As of boto==2.38.0, the origin of a route is not available + # (for example, whether it came from a gateway with route propagation + # enabled). Testing for origin == 'EnableVgwRoutePropagation' is more + # correct than checking whether the route uses a propagating VGW. + # The current logic will leave non-propagated routes using propagating + # VGWs in place. routes_to_delete = [r for r in routes_to_match - if r.gateway_id != 'local'] + if r.gateway_id != 'local' + and r.gateway_id not in propagating_vgw_ids] changed = routes_to_delete or route_specs_to_create if changed: @@ -433,7 +442,8 @@ def ensure_route_table_present(vpc_conn, vpc_id, route_table_id, resource_tags, if routes is not None: try: - result = ensure_routes(vpc_conn, route_table, routes, check_mode) + result = ensure_routes(vpc_conn, route_table, routes, + propagating_vgw_ids, check_mode) changed = changed or result['changed'] except EC2ResponseError as e: raise AnsibleRouteTableException( From 96e4194588c088294f1935d7d984065cb3393034 Mon Sep 17 00:00:00 2001 From: Bret Martin Date: Mon, 10 Aug 2015 15:25:13 -0400 Subject: [PATCH 0679/2522] Don't enable route propagation on a virtual gateway with propagated routes --- cloud/amazon/ec2_vpc_route_table.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index d93effcc550..2328006883c 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -369,17 +369,22 @@ def ensure_subnet_associations(vpc_conn, vpc_id, route_table, subnets, return {'changed': changed} -def ensure_propagation(vpc_conn, route_table_id, propagating_vgw_ids, +def ensure_propagation(vpc_conn, route_table, propagating_vgw_ids, check_mode): - # NOTE: As of boto==2.15.0, it is not yet possible to query the existing - # propagating gateways. However, EC2 does support this as evidenced by - # the describe-route-tables tool. For now, just enable the given VGWs - # and do not disable any others. + # NOTE: As of boto==2.38.0, it is not yet possible to query the existing + # propagating gateways. However, EC2 does support this as shown in its API + # documentation. For now, a reasonable proxy for this is the presence of + # propagated routes using the gateway in the route table. If such a route + # is found, propagation is almost certainly enabled. changed = False for vgw_id in propagating_vgw_ids: + for r in list(route_table.routes): + if r.gateway_id == vgw_id: + return {'changed': False} + changed = True - vpc_conn.enable_vgw_route_propagation(route_table_id, + vpc_conn.enable_vgw_route_propagation(route_table.id, vgw_id, dry_run=check_mode) @@ -430,7 +435,7 @@ def ensure_route_table_present(vpc_conn, vpc_id, route_table_id, resource_tags, ) if propagating_vgw_ids is not None: - result = ensure_propagation(vpc_conn, route_table.id, + result = ensure_propagation(vpc_conn, route_table, propagating_vgw_ids, check_mode=check_mode) changed = changed or result['changed'] From a2fb8edb3c67247adad6b7a420929390cb73ece1 Mon Sep 17 00:00:00 2001 From: whiter Date: Sun, 30 Aug 2015 22:25:05 +0200 Subject: [PATCH 0680/2522] Added option to specify tags or route-table-id, quoted doc strings, added more detail to returned route table object, numerous minor fixes --- cloud/amazon/ec2_vpc_route_table.py | 298 ++++++++++++++++------------ 1 file changed, 172 insertions(+), 126 deletions(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index 2328006883c..a65efaa78fc 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -20,50 +20,47 @@ description: - Manage route tables for AWS virtual private clouds version_added: "2.0" -author: Robert Estelle (@erydo) +author: Robert Estelle (@erydo), Rob White (@wimnat) options: - vpc_id: + lookup: description: - - VPC ID of the VPC in which to create the route table. - required: true - route_table_id: + - "Look up route table by either tags or by route table ID. Non-unique tag lookup will fail. If no tags are specifed then no lookup for an existing route table is performed and a new route table will be created. To change tags of a route table, you must look up by id." + required: false + default: tag + choices: [ 'tag', 'id' ] + propagating_vgw_ids: description: - - The ID of the route table to update or delete. + - "Enable route propagation from virtual gateways specified by ID." required: false - default: null - resource_tags: + route_table_id: description: - - A dictionary array of resource tags of the form: { tag1: value1, tag2: value2 }. Tags in this list are used to uniquely identify route tables within a VPC when the route_table_id is not supplied. + - "The ID of the route table to update or delete." required: false default: null routes: description: - - List of routes in the route table. Routes are specified as dicts containing the keys 'dest' and one of 'gateway_id', 'instance_id', 'interface_id', or 'vpc_peering_connection'. If 'gateway_id' is specified, you can refer to the VPC's IGW by using the value 'igw'. + - "List of routes in the route table. Routes are specified as dicts containing the keys 'dest' and one of 'gateway_id', 'instance_id', 'interface_id', or 'vpc_peering_connection'. If 'gateway_id' is specified, you can refer to the VPC's IGW by using the value 'igw'." required: true + state: + description: + - "Create or destroy the VPC route table" + required: false + default: present + choices: [ 'present', 'absent' ] subnets: description: - - An array of subnets to add to this route table. Subnets may be specified by either subnet ID, Name tag, or by a CIDR such as '10.0.0.0/24'. + - "An array of subnets to add to this route table. Subnets may be specified by either subnet ID, Name tag, or by a CIDR such as '10.0.0.0/24'." required: true - propagating_vgw_ids: + tags: description: - - Enable route propagation from virtual gateways specified by ID. + - "A dictionary array of resource tags of the form: { tag1: value1, tag2: value2 }. Tags in this list are used to uniquely identify route tables within a VPC when the route_table_id is not supplied." required: false - wait: - description: - - Wait for the VPC to be in state 'available' before returning. - required: false - default: "no" - choices: [ "yes", "no" ] - wait_timeout: - description: - - How long before wait gives up, in seconds. - default: 300 - state: + default: null + aliases: [ "resource_tags" ] + vpc_id: description: - - Create or destroy the VPC route table - required: false - default: present - choices: [ 'present', 'absent' ] + - "VPC ID of the VPC in which to create the route table." + required: true extends_documentation_fragment: aws ''' @@ -73,36 +70,35 @@ # Basic creation example: - name: Set up public subnet route table - local_action: - module: ec2_vpc_route_table + ec2_vpc_route_table: vpc_id: vpc-1245678 region: us-west-1 - resource_tags: + tags: Name: Public subnets: - - '{{jumpbox_subnet.subnet_id}}' - - '{{frontend_subnet.subnet_id}}' - - '{{vpn_subnet.subnet_id}}' + - "{{ jumpbox_subnet.subnet_id }}" + - "{{ frontend_subnet.subnet_id }}" + - "{{ vpn_subnet.subnet_id }}" routes: - dest: 0.0.0.0/0 - gateway_id: '{{igw.gateway_id}}' + gateway_id: "{{ igw.gateway_id }}" register: public_route_table - name: Set up NAT-protected route table - local_action: - module: ec2_vpc_route_table + ec2_vpc_route_table: vpc_id: vpc-1245678 region: us-west-1 - resource_tags: + tags: - Name: Internal subnets: - - '{{application_subnet.subnet_id}}' + - "{{ application_subnet.subnet_id }}" - 'Database Subnet' - '10.0.0.0/8' routes: - dest: 0.0.0.0/0 - instance_id: '{{nat.instance_id}}' + instance_id: "{{ nat.instance_id }}" register: nat_route_table + ''' @@ -210,12 +206,12 @@ def find_igw(vpc_conn, vpc_id): filters={'attachment.vpc-id': vpc_id}) if not igw: - return AnsibleIgwSearchException('No IGW found for VPC "{0}"'. + raise AnsibleIgwSearchException('No IGW found for VPC {0}'. format(vpc_id)) elif len(igw) == 1: return igw[0].id else: - raise AnsibleIgwSearchException('Multiple IGWs found for VPC "{0}"'. + raise AnsibleIgwSearchException('Multiple IGWs found for VPC {0}'. format(vpc_id)) @@ -251,17 +247,29 @@ def ensure_tags(vpc_conn, resource_id, tags, add_only, check_mode): def get_route_table_by_id(vpc_conn, vpc_id, route_table_id): - route_tables = vpc_conn.get_all_route_tables( - route_table_ids=[route_table_id], filters={'vpc_id': vpc_id}) - return route_tables[0] if route_tables else None - + route_table = None + route_tables = vpc_conn.get_all_route_tables(route_table_ids=[route_table_id], filters={'vpc_id': vpc_id}) + if route_tables: + route_table = route_tables[0] + + return route_table + def get_route_table_by_tags(vpc_conn, vpc_id, tags): + + count = 0 + route_table = None route_tables = vpc_conn.get_all_route_tables(filters={'vpc_id': vpc_id}) - for route_table in route_tables: - this_tags = get_resource_tags(vpc_conn, route_table.id) + for table in route_tables: + this_tags = get_resource_tags(vpc_conn, table.id) if tags_match(tags, this_tags): - return route_table + route_table = table + count +=1 + + if count > 1: + raise RuntimeError("Tags provided do not identify a unique route table") + else: + return route_table def route_spec_matches_route(route_spec, route): @@ -391,75 +399,132 @@ def ensure_propagation(vpc_conn, route_table, propagating_vgw_ids, return {'changed': changed} -def ensure_route_table_absent(vpc_conn, vpc_id, route_table_id, resource_tags, - check_mode): - if route_table_id: - route_table = get_route_table_by_id(vpc_conn, vpc_id, route_table_id) - elif resource_tags: - route_table = get_route_table_by_tags(vpc_conn, vpc_id, resource_tags) - else: - raise AnsibleRouteTableException( - 'must provide route_table_id or resource_tags') +def ensure_route_table_absent(connection, module): + + lookup = module.params.get('lookup') + route_table_id = module.params.get('route_table_id') + tags = module.params.get('tags') + vpc_id = module.params.get('vpc_id') + check_mode = module.params.get('check_mode') + + if lookup == 'tag': + if tags is not None: + try: + route_table = get_route_table_by_tags(connection, vpc_id, tags) + except EC2ResponseError as e: + module.fail_json(msg=e.message) + except RuntimeError as e: + module.fail_json(msg=e.args[0]) + else: + route_table = None + elif lookup == 'id': + try: + route_table = get_route_table_by_id(connection, vpc_id, route_table_id) + except EC2ResponseError as e: + module.fail_json(msg=e.message) if route_table is None: return {'changed': False} - vpc_conn.delete_route_table(route_table.id, dry_run=check_mode) + try: + connection.delete_route_table(route_table.id, dry_run=check_mode) + except EC2ResponseError as e: + module.fail_json(msg=e.message) + return {'changed': True} -def ensure_route_table_present(vpc_conn, vpc_id, route_table_id, resource_tags, - routes, subnets, propagating_vgw_ids, - check_mode): +def get_route_table_info(route_table): + + # Add any routes to array + routes = [] + for route in route_table.routes: + routes.append(route.__dict__) + + route_table_info = { 'id': route_table.id, + 'routes': routes, + 'tags': route_table.tags, + 'vpc_id': route_table.vpc_id + } + + return route_table_info + +def create_route_spec(connection, routes, vpc_id): + + for route_spec in routes: + rename_key(route_spec, 'dest', 'destination_cidr_block') + + if 'gateway_id' in route_spec and route_spec['gateway_id'] and \ + route_spec['gateway_id'].lower() == 'igw': + igw = find_igw(connection, vpc_id) + route_spec['gateway_id'] = igw + + return routes + +def ensure_route_table_present(connection, module): + + lookup = module.params.get('lookup') + propagating_vgw_ids = module.params.get('propagating_vgw_ids', []) + route_table_id = module.params.get('route_table_id') + subnets = module.params.get('subnets') + tags = module.params.get('tags') + vpc_id = module.params.get('vpc_id') + check_mode = module.params.get('check_mode') + try: + routes = create_route_spec(connection, module.params.get('routes'), vpc_id) + except AnsibleIgwSearchException as e: + module.fail_json(msg=e[0]) + changed = False tags_valid = False - if route_table_id: - route_table = get_route_table_by_id(vpc_conn, vpc_id, route_table_id) - elif resource_tags: - route_table = get_route_table_by_tags(vpc_conn, vpc_id, resource_tags) - tags_valid = route_table is not None - else: - raise AnsibleRouteTableException( - 'must provide route_table_id or resource_tags') - - if check_mode and route_table is None: - return {'changed': True} + if lookup == 'tag': + if tags is not None: + try: + route_table = get_route_table_by_tags(connection, vpc_id, tags) + except EC2ResponseError as e: + module.fail_json(msg=e.message) + except RuntimeError as e: + module.fail_json(msg=e.args[0]) + else: + route_table = None + elif lookup == 'id': + try: + route_table = get_route_table_by_id(connection, vpc_id, route_table_id) + except EC2ResponseError as e: + module.fail_json(msg=e.message) + + # If no route table returned then create new route table if route_table is None: + print route_table.keys() + try: + route_table = connection.create_route_table(vpc_id, check_mode) + changed = True + except EC2ResponseError, e: + module.fail_json(msg=e.message) + + if routes is not None: try: - route_table = vpc_conn.create_route_table(vpc_id) + result = ensure_routes(connection, route_table, routes, propagating_vgw_ids, check_mode) + changed = changed or result['changed'] except EC2ResponseError as e: - raise AnsibleRouteTableException( - 'Unable to create route table {0}, error: {1}' - .format(route_table_id or resource_tags, e) - ) + module.fail_json(msg=e.message) if propagating_vgw_ids is not None: - result = ensure_propagation(vpc_conn, route_table, + result = ensure_propagation(vpc_conn, route_table_id, propagating_vgw_ids, check_mode=check_mode) changed = changed or result['changed'] - if not tags_valid and resource_tags is not None: - result = ensure_tags(vpc_conn, route_table.id, resource_tags, + if not tags_valid and tags is not None: + result = ensure_tags(connection, route_table.id, tags, add_only=True, check_mode=check_mode) changed = changed or result['changed'] - if routes is not None: - try: - result = ensure_routes(vpc_conn, route_table, routes, - propagating_vgw_ids, check_mode) - changed = changed or result['changed'] - except EC2ResponseError as e: - raise AnsibleRouteTableException( - 'Unable to ensure routes for route table {0}, error: {1}' - .format(route_table, e) - ) - if subnets: associated_subnets = [] try: - associated_subnets = find_subnets(vpc_conn, vpc_id, subnets) + associated_subnets = find_subnets(connection, vpc_id, subnets) except EC2ResponseError as e: raise AnsibleRouteTableException( 'Unable to find subnets for route table {0}, error: {1}' @@ -467,8 +532,7 @@ def ensure_route_table_present(vpc_conn, vpc_id, route_table_id, resource_tags, ) try: - result = ensure_subnet_associations( - vpc_conn, vpc_id, route_table, associated_subnets, check_mode) + result = ensure_subnet_associations(connection, vpc_id, route_table, associated_subnets, check_mode) changed = changed or result['changed'] except EC2ResponseError as e: raise AnsibleRouteTableException( @@ -476,23 +540,21 @@ def ensure_route_table_present(vpc_conn, vpc_id, route_table_id, resource_tags, .format(route_table, e) ) - return { - 'changed': changed, - 'route_table_id': route_table.id, - } + module.exit_json(changed=changed, route_table=get_route_table_info(route_table)) def main(): argument_spec = ec2_argument_spec() argument_spec.update( dict( - vpc_id = dict(default=None, required=True), - route_table_id = dict(default=None, required=False), + lookup = dict(default='tag', required=False, choices=['tag', 'id']), propagating_vgw_ids = dict(default=None, required=False, type='list'), - resource_tags = dict(default=None, required=False, type='dict'), + route_table_id = dict(default=None, required=False), routes = dict(default=None, required=False, type='list'), + state = dict(default='present', choices=['present', 'absent']), subnets = dict(default=None, required=False, type='list'), - state = dict(default='present', choices=['present', 'absent']) + tags = dict(default=None, required=False, type='dict', aliases=['resource_tags']), + vpc_id = dict(default=None, required=True) ) ) @@ -511,34 +573,18 @@ def main(): else: module.fail_json(msg="region must be specified") - vpc_id = module.params.get('vpc_id') + lookup = module.params.get('lookup') route_table_id = module.params.get('route_table_id') - resource_tags = module.params.get('resource_tags') - propagating_vgw_ids = module.params.get('propagating_vgw_ids', []) - - routes = module.params.get('routes') - for route_spec in routes: - rename_key(route_spec, 'dest', 'destination_cidr_block') - - if 'gateway_id' in route_spec and route_spec['gateway_id'] and \ - route_spec['gateway_id'].lower() == 'igw': - igw = find_igw(connection, vpc_id) - route_spec['gateway_id'] = igw - - subnets = module.params.get('subnets') state = module.params.get('state', 'present') + if lookup == 'id' and route_table_id is None: + module.fail_json("You must specify route_table_id if lookup is set to id") + try: if state == 'present': - result = ensure_route_table_present( - connection, vpc_id, route_table_id, resource_tags, - routes, subnets, propagating_vgw_ids, module.check_mode - ) + result = ensure_route_table_present(connection, module) elif state == 'absent': - result = ensure_route_table_absent( - connection, vpc_id, route_table_id, resource_tags, - module.check_mode - ) + result = ensure_route_table_absent(connection, module) except AnsibleRouteTableException as e: module.fail_json(msg=str(e)) @@ -549,4 +595,4 @@ def main(): if __name__ == '__main__': main() - \ No newline at end of file + From 2dc67f7c6b98d3cde214beba48723a46532404a0 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Mon, 31 Aug 2015 15:53:02 +0200 Subject: [PATCH 0681/2522] cloudstack: cs_template: add new arg cross_zones --- cloud/cloudstack/cs_template.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/cloud/cloudstack/cs_template.py b/cloud/cloudstack/cs_template.py index d451ece7138..c6c482f9c0f 100644 --- a/cloud/cloudstack/cs_template.py +++ b/cloud/cloudstack/cs_template.py @@ -86,6 +86,12 @@ - Only used if C(state) is present. required: false default: false + cross_zones: + description: + - Whether the template should be syned across zones. + - Only used if C(state) is present. + required: false + default: false project: description: - Name of the project the template to be registered in. @@ -185,9 +191,8 @@ url: "http://packages.shapeblue.com/systemvmtemplate/4.5/systemvm64template-4.5-vmware.ova" hypervisor: VMware format: OVA - zone: tokio-ix + cross_zones: yes os_type: Debian GNU/Linux 7(64-bit) - is_routing: yes # Create a template from a stopped virtual machine's volume - local_action: @@ -456,11 +461,15 @@ def register_template(self): args['isrouting'] = self.module.params.get('is_routing') args['sshkeyenabled'] = self.module.params.get('sshkey_enabled') args['hypervisor'] = self.get_hypervisor() - args['zoneid'] = self.get_zone(key='id') args['domainid'] = self.get_domain(key='id') args['account'] = self.get_account(key='name') args['projectid'] = self.get_project(key='id') + if not self.module.params.get('cross_zones'): + args['zoneid'] = self.get_zone(key='id') + else: + args['zoneid'] = -1 + if not self.module.check_mode: res = self.cs.registerTemplate(**args) if 'errortext' in res: @@ -473,11 +482,13 @@ def get_template(self): args = {} args['isready'] = self.module.params.get('is_ready') args['templatefilter'] = self.module.params.get('template_filter') - args['zoneid'] = self.get_zone(key='id') args['domainid'] = self.get_domain(key='id') args['account'] = self.get_account(key='name') args['projectid'] = self.get_project(key='id') + if not self.module.params.get('cross_zones'): + args['zoneid'] = self.get_zone(key='id') + # if checksum is set, we only look on that. checksum = self.module.params.get('checksum') if not checksum: @@ -543,6 +554,7 @@ def main(): details = dict(default=None), bits = dict(type='int', choices=[ 32, 64 ], default=64), state = dict(choices=['present', 'absent'], default='present'), + cross_zones = dict(type='bool', choices=BOOLEANS, default=False), zone = dict(default=None), domain = dict(default=None), account = dict(default=None), From 52a3d99873782383f44195e584823e9c27ef10ae Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Mon, 31 Aug 2015 08:59:39 +0200 Subject: [PATCH 0682/2522] cloudstack: add api_region arg * docs in module_docs_fragments/cloudstack.py * implemented in module_utils/cloudstack.py -> https://github.com/ansible/ansible/pull/12083 --- cloud/cloudstack/cs_account.py | 1 + cloud/cloudstack/cs_affinitygroup.py | 1 + cloud/cloudstack/cs_domain.py | 1 + cloud/cloudstack/cs_firewall.py | 1 + cloud/cloudstack/cs_instance.py | 1 + cloud/cloudstack/cs_instancegroup.py | 1 + cloud/cloudstack/cs_ip_address.py | 1 + cloud/cloudstack/cs_iso.py | 1 + cloud/cloudstack/cs_network.py | 1 + cloud/cloudstack/cs_portforward.py | 1 + cloud/cloudstack/cs_project.py | 1 + cloud/cloudstack/cs_securitygroup.py | 1 + cloud/cloudstack/cs_securitygroup_rule.py | 1 + cloud/cloudstack/cs_sshkeypair.py | 1 + cloud/cloudstack/cs_staticnat.py | 1 + cloud/cloudstack/cs_template.py | 1 + cloud/cloudstack/cs_vmsnapshot.py | 1 + 17 files changed, 17 insertions(+) diff --git a/cloud/cloudstack/cs_account.py b/cloud/cloudstack/cs_account.py index 1ce6fdde88f..052354c581e 100644 --- a/cloud/cloudstack/cs_account.py +++ b/cloud/cloudstack/cs_account.py @@ -366,6 +366,7 @@ def main(): api_url = dict(default=None), api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), + api_region = dict(default='cloudstack'), ), required_together = ( ['api_key', 'api_secret', 'api_url'], diff --git a/cloud/cloudstack/cs_affinitygroup.py b/cloud/cloudstack/cs_affinitygroup.py index 40b764aa8ef..8a2f40fae62 100644 --- a/cloud/cloudstack/cs_affinitygroup.py +++ b/cloud/cloudstack/cs_affinitygroup.py @@ -215,6 +215,7 @@ def main(): api_url = dict(default=None), api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), + api_region = dict(default='cloudstack'), ), required_together = ( ['api_key', 'api_secret', 'api_url'], diff --git a/cloud/cloudstack/cs_domain.py b/cloud/cloudstack/cs_domain.py index 27410040aec..94299d5d6a3 100644 --- a/cloud/cloudstack/cs_domain.py +++ b/cloud/cloudstack/cs_domain.py @@ -251,6 +251,7 @@ def main(): api_url = dict(default=None), api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), + api_region = dict(default='cloudstack'), ), required_together = ( ['api_key', 'api_secret', 'api_url'], diff --git a/cloud/cloudstack/cs_firewall.py b/cloud/cloudstack/cs_firewall.py index e52683d7a67..59a2a57e9f2 100644 --- a/cloud/cloudstack/cs_firewall.py +++ b/cloud/cloudstack/cs_firewall.py @@ -413,6 +413,7 @@ def main(): api_url = dict(default=None), api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), + api_region = dict(default='cloudstack'), ), required_one_of = ( ['ip_address', 'network'], diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 4ead1317b2f..2aaa1c61293 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -818,6 +818,7 @@ def main(): api_url = dict(default=None), api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), + api_region = dict(default='cloudstack'), ), mutually_exclusive = ( ['template', 'iso'], diff --git a/cloud/cloudstack/cs_instancegroup.py b/cloud/cloudstack/cs_instancegroup.py index 537d9d90b28..f3ac8e4caa8 100644 --- a/cloud/cloudstack/cs_instancegroup.py +++ b/cloud/cloudstack/cs_instancegroup.py @@ -182,6 +182,7 @@ def main(): api_url = dict(default=None), api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), + api_region = dict(default='cloudstack'), ), required_together = ( ['api_key', 'api_secret', 'api_url'], diff --git a/cloud/cloudstack/cs_ip_address.py b/cloud/cloudstack/cs_ip_address.py index e9507f855ed..3e55b9f4be1 100644 --- a/cloud/cloudstack/cs_ip_address.py +++ b/cloud/cloudstack/cs_ip_address.py @@ -239,6 +239,7 @@ def main(): api_url = dict(default=None), api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), + api_region = dict(default='cloudstack'), ), required_together = ( ['api_key', 'api_secret', 'api_url'], diff --git a/cloud/cloudstack/cs_iso.py b/cloud/cloudstack/cs_iso.py index 37f110cbe68..4ce1804c762 100644 --- a/cloud/cloudstack/cs_iso.py +++ b/cloud/cloudstack/cs_iso.py @@ -316,6 +316,7 @@ def main(): api_url = dict(default=None), api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), + api_region = dict(default='cloudstack'), ), required_together = ( ['api_key', 'api_secret', 'api_url'], diff --git a/cloud/cloudstack/cs_network.py b/cloud/cloudstack/cs_network.py index cab24bdfefe..1cb97bf86ea 100644 --- a/cloud/cloudstack/cs_network.py +++ b/cloud/cloudstack/cs_network.py @@ -552,6 +552,7 @@ def main(): api_url = dict(default=None), api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), + api_region = dict(default='cloudstack'), ), required_together = ( ['api_key', 'api_secret', 'api_url'], diff --git a/cloud/cloudstack/cs_portforward.py b/cloud/cloudstack/cs_portforward.py index f2f87b660ef..df867915a07 100644 --- a/cloud/cloudstack/cs_portforward.py +++ b/cloud/cloudstack/cs_portforward.py @@ -390,6 +390,7 @@ def main(): api_url = dict(default=None), api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), + api_region = dict(default='cloudstack'), ), required_together = ( ['api_key', 'api_secret', 'api_url'], diff --git a/cloud/cloudstack/cs_project.py b/cloud/cloudstack/cs_project.py index 6a48956bb1c..a7468e63118 100644 --- a/cloud/cloudstack/cs_project.py +++ b/cloud/cloudstack/cs_project.py @@ -272,6 +272,7 @@ def main(): api_url = dict(default=None), api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), + api_region = dict(default='cloudstack'), ), required_together = ( ['api_key', 'api_secret', 'api_url'], diff --git a/cloud/cloudstack/cs_securitygroup.py b/cloud/cloudstack/cs_securitygroup.py index f54de925936..3bae64b4dd9 100644 --- a/cloud/cloudstack/cs_securitygroup.py +++ b/cloud/cloudstack/cs_securitygroup.py @@ -163,6 +163,7 @@ def main(): api_url = dict(default=None), api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), + api_region = dict(default='cloudstack'), ), required_together = ( ['api_key', 'api_secret', 'api_url'], diff --git a/cloud/cloudstack/cs_securitygroup_rule.py b/cloud/cloudstack/cs_securitygroup_rule.py index c17923daca7..2c75a83d4b3 100644 --- a/cloud/cloudstack/cs_securitygroup_rule.py +++ b/cloud/cloudstack/cs_securitygroup_rule.py @@ -390,6 +390,7 @@ def main(): api_url = dict(default=None), api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), + api_region = dict(default='cloudstack'), ), required_together = ( ['icmp_type', 'icmp_code'], diff --git a/cloud/cloudstack/cs_sshkeypair.py b/cloud/cloudstack/cs_sshkeypair.py index ebd906f7d5c..a6576404c3c 100644 --- a/cloud/cloudstack/cs_sshkeypair.py +++ b/cloud/cloudstack/cs_sshkeypair.py @@ -218,6 +218,7 @@ def main(): api_url = dict(default=None), api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), + api_region = dict(default='cloudstack'), ), required_together = ( ['api_key', 'api_secret', 'api_url'], diff --git a/cloud/cloudstack/cs_staticnat.py b/cloud/cloudstack/cs_staticnat.py index 4b73d86e32b..c42b743d51c 100644 --- a/cloud/cloudstack/cs_staticnat.py +++ b/cloud/cloudstack/cs_staticnat.py @@ -275,6 +275,7 @@ def main(): api_url = dict(default=None), api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), + api_region = dict(default='cloudstack'), ), required_together = ( ['api_key', 'api_secret', 'api_url'], diff --git a/cloud/cloudstack/cs_template.py b/cloud/cloudstack/cs_template.py index d451ece7138..89ebd5b799e 100644 --- a/cloud/cloudstack/cs_template.py +++ b/cloud/cloudstack/cs_template.py @@ -553,6 +553,7 @@ def main(): api_url = dict(default=None), api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), + api_region = dict(default='cloudstack'), ), mutually_exclusive = ( ['url', 'vm'], diff --git a/cloud/cloudstack/cs_vmsnapshot.py b/cloud/cloudstack/cs_vmsnapshot.py index c9e815e4730..9b87c1c3567 100644 --- a/cloud/cloudstack/cs_vmsnapshot.py +++ b/cloud/cloudstack/cs_vmsnapshot.py @@ -274,6 +274,7 @@ def main(): api_url = dict(default=None), api_http_method = dict(choices=['get', 'post'], default='get'), api_timeout = dict(type='int', default=10), + api_region = dict(default='cloudstack'), ), required_together = ( ['icmp_type', 'icmp_code'], From 506d1df22c69481d22213d8551f69e7100b9089d Mon Sep 17 00:00:00 2001 From: whiter Date: Mon, 31 Aug 2015 17:39:13 +0200 Subject: [PATCH 0683/2522] Documentation examples fix --- cloud/amazon/ec2_vpc_subnet_facts.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/amazon/ec2_vpc_subnet_facts.py b/cloud/amazon/ec2_vpc_subnet_facts.py index a865d35f6ab..c3c8268579a 100644 --- a/cloud/amazon/ec2_vpc_subnet_facts.py +++ b/cloud/amazon/ec2_vpc_subnet_facts.py @@ -46,17 +46,17 @@ # Gather facts about a particular VPC subnet using ID - ec2_vpc_subnet_facts: filters: - - subnet-id: subnet-00112233 + subnet-id: subnet-00112233 # Gather facts about any VPC subnet with a tag key Name and value Example - ec2_vpc_subnet_facts: filters: - - "tag:Name": Example + "tag:Name": Example # Gather facts about any VPC subnet within VPC with ID vpc-abcdef00 - ec2_vpc_subnet_facts: filters: - - vpc-id: vpc-abcdef00 + vpc-id: vpc-abcdef00 ''' From c9785a69487c10026a46f58e529d94f3c008b9b7 Mon Sep 17 00:00:00 2001 From: Tim Bielawa Date: Mon, 31 Aug 2015 13:14:05 -0400 Subject: [PATCH 0684/2522] Fix capitalization in nagios 'services' parameter comment --- monitoring/nagios.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monitoring/nagios.py b/monitoring/nagios.py index 16edca2aa6a..ed1da7a1e2e 100644 --- a/monitoring/nagios.py +++ b/monitoring/nagios.py @@ -76,7 +76,7 @@ servicegroup: version_added: "2.0" description: - - the Servicegroup we want to set downtimes/alerts for. + - The Servicegroup we want to set downtimes/alerts for. B(Required) option when using the C(servicegroup_service_downtime) amd C(servicegroup_host_downtime). command: description: @@ -86,7 +86,7 @@ required: true default: null -author: "Tim Bielawa (@tbielawa)" +author: "Tim Bielawa (@tbielawa)" requirements: [ "Nagios" ] ''' From 0c1257b0c17da7f8ba629c35cb90fd2456da3828 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Tue, 1 Sep 2015 00:28:27 +0200 Subject: [PATCH 0685/2522] cloudstack: cs_instance: deploy instance in desired state on state=started/stopped Before this change, an instance must be present for make use of state=stopped/started. Now we are deploying an instance in the desire state if it does not exist. In this case all args needed to deploy the instance must be passed. However the short form for stopping/starting an _existing_ instance still works as before. --- cloud/cloudstack/cs_instance.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 4ead1317b2f..6f1339123d8 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -548,7 +548,7 @@ def get_user_data(self): return user_data - def deploy_instance(self): + def deploy_instance(self, start_vm=True): self.result['changed'] = True networkids = self.get_network_ids() if networkids is not None: @@ -573,6 +573,7 @@ def deploy_instance(self): args['group'] = self.module.params.get('group') args['keypair'] = self.module.params.get('ssh_key') args['size'] = self.module.params.get('disk_size') + args['startvm'] = start_vm args['rootdisksize'] = self.module.params.get('root_disk_size') args['securitygroupnames'] = ','.join(self.module.params.get('security_groups')) args['affinitygroupnames'] = ','.join(self.module.params.get('affinity_groups')) @@ -700,10 +701,12 @@ def expunge_instance(self): def stop_instance(self): instance = self.get_instance() + if not instance: - self.module.fail_json(msg="Instance named '%s' not found" % self.module.params.get('name')) + instance = self.deploy_instance(start_vm=False) + return instance - if instance['state'].lower() in ['stopping', 'stopped']: + elif instance['state'].lower() in ['stopping', 'stopped']: return instance if instance['state'].lower() in ['starting', 'running']: @@ -722,10 +725,12 @@ def stop_instance(self): def start_instance(self): instance = self.get_instance() + if not instance: - self.module.fail_json(msg="Instance named '%s' not found" % module.params.get('name')) + instance = self.deploy_instance() + return instance - if instance['state'].lower() in ['starting', 'running']: + elif instance['state'].lower() in ['starting', 'running']: return instance if instance['state'].lower() in ['stopped', 'stopping']: @@ -744,10 +749,12 @@ def start_instance(self): def restart_instance(self): instance = self.get_instance() + if not instance: - module.fail_json(msg="Instance named '%s' not found" % self.module.params.get('name')) + instance = self.deploy_instance() + return instance - if instance['state'].lower() in [ 'running', 'starting' ]: + elif instance['state'].lower() in [ 'running', 'starting' ]: self.result['changed'] = True if not self.module.check_mode: instance = self.cs.rebootVirtualMachine(id=instance['id']) From c02e4195cfcf2ffcf5645c8ae3bcb35384b19899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Andersson?= Date: Thu, 30 Apr 2015 23:26:33 +0800 Subject: [PATCH 0686/2522] Ignore extra lines from Pivotal's RabbitMQ package Pivotal's packaging of RabbitMQ shows a banner at the end of the plugin listing talking about their official plugins. The start of the banner is divided by a blank line so the changed plugin listing will now break after the first empty line. An example listing with the rabbitmq_management plugin enabled: ``` $ rabbitmq-plugins list -E -m rabbitmq_management Pivotal officially maintains and supports the plugins: rabbitmq_auth_backend_ldap, rabbitmq_auth_mechanism_ssl, rabbitmq_consistent_hash_exchange, rabbitmq_federation, rabbitmq_federation_management, rabbitmq_jms_topic_exchange, rabbitmq_management, rabbitmq_management_agent, rabbitmq_mqtt, rabbitmq_shovel, rabbitmq_shovel_management, and rabbitmq_stomp. ``` --- messaging/rabbitmq_plugin.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/messaging/rabbitmq_plugin.py b/messaging/rabbitmq_plugin.py index 8d3a9428016..b52de337e2e 100644 --- a/messaging/rabbitmq_plugin.py +++ b/messaging/rabbitmq_plugin.py @@ -88,7 +88,14 @@ def _exec(self, args, run_in_check_mode=False): return list() def get_all(self): - return self._exec(['list', '-E', '-m'], True) + list_output = self._exec(['list', '-E', '-m'], True) + plugins = [] + for plugin in list_output: + if not plugin: + break + plugins.append(plugin) + + return plugins def enable(self, name): self._exec(['enable', name]) @@ -96,6 +103,7 @@ def enable(self, name): def disable(self, name): self._exec(['disable', name]) + def main(): arg_spec = dict( names=dict(required=True, aliases=['name']), From 48f0e70e6047380e8cb2ccfc0da5ff0fbc688766 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Mon, 3 Aug 2015 19:10:50 -0500 Subject: [PATCH 0687/2522] pacman: Remove choice list for boolean values in arg spec This also makes argument_spec more consistent with core modules. Added self as author. --- packaging/os/pacman.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packaging/os/pacman.py b/packaging/os/pacman.py index 74e29a1f936..6d50fed7912 100644 --- a/packaging/os/pacman.py +++ b/packaging/os/pacman.py @@ -3,6 +3,7 @@ # (c) 2012, Afterburn # (c) 2013, Aaron Bull Schaefer +# (c) 2015, Indrajit Raychaudhuri # # This file is part of Ansible # @@ -28,6 +29,7 @@ Arch Linux and its variants. version_added: "1.0" author: + - "Indrajit Raychaudhuri (@indrajitr)" - "'Aaron Bull Schaefer (@elasticdog)' " - "Afterburn" notes: [] @@ -52,7 +54,7 @@ that they are not required by other packages and were not explicitly installed by a user. required: false - default: "no" + default: no choices: ["yes", "no"] version_added: "1.3" @@ -60,7 +62,7 @@ description: - Force remove package, without any checks. required: false - default: "no" + default: no choices: ["yes", "no"] version_added: "2.0" @@ -69,14 +71,14 @@ - Whether or not to refresh the master package lists. This can be run as part of a package installation or as a separate step. required: false - default: "no" + default: no choices: ["yes", "no"] upgrade: description: - Whether or not to upgrade whole system required: false - default: "no" + default: no choices: ["yes", "no"] version_added: "2.0" ''' @@ -251,10 +253,10 @@ def main(): argument_spec = dict( name = dict(aliases=['pkg']), state = dict(default='present', choices=['present', 'installed', "latest", 'absent', 'removed']), - recurse = dict(default='no', choices=BOOLEANS, type='bool'), - force = dict(default='no', choices=BOOLEANS, type='bool'), - upgrade = dict(default='no', choices=BOOLEANS, type='bool'), - update_cache = dict(default='no', aliases=['update-cache'], choices=BOOLEANS, type='bool')), + recurse = dict(default=False, type='bool'), + force = dict(default=False, type='bool'), + upgrade = dict(default=False, type='bool'), + update_cache = dict(default=False, aliases=['update-cache'], type='bool')), required_one_of = [['name', 'update_cache', 'upgrade']], supports_check_mode = True) From 65d3a3be59f028def687293ddac5aa92eeb705ce Mon Sep 17 00:00:00 2001 From: whiter Date: Wed, 2 Sep 2015 14:34:56 +0100 Subject: [PATCH 0688/2522] Remove debug print statement. Fixed ensure_propagation call to pass 'route_table' and 'connection'. --- cloud/amazon/ec2_vpc_route_table.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index a65efaa78fc..70f53bad26a 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -496,7 +496,6 @@ def ensure_route_table_present(connection, module): # If no route table returned then create new route table if route_table is None: - print route_table.keys() try: route_table = connection.create_route_table(vpc_id, check_mode) changed = True @@ -511,7 +510,7 @@ def ensure_route_table_present(connection, module): module.fail_json(msg=e.message) if propagating_vgw_ids is not None: - result = ensure_propagation(vpc_conn, route_table_id, + result = ensure_propagation(connection, route_table, propagating_vgw_ids, check_mode=check_mode) changed = changed or result['changed'] From 5a4dcc24fc3a64e0c06945484c38bbf12f3415f6 Mon Sep 17 00:00:00 2001 From: Jonathan Mainguy Date: Wed, 2 Sep 2015 14:34:49 -0400 Subject: [PATCH 0689/2522] Adds part=false feature to irc module. This allows people to use a faux bot without part/dconns between messages, tested using a user logged into znc as our faux bot, defaults to old style of part/dconn if part= not specified --- notification/irc.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/notification/irc.py b/notification/irc.py index 7e34049c639..28ad4417ac1 100644 --- a/notification/irc.py +++ b/notification/irc.py @@ -89,6 +89,12 @@ - Designates whether TLS/SSL should be used when connecting to the IRC server default: False version_added: "1.8" + part: + description: + - Designates whether user should part from channel after sending message or not. + Useful for when using a faux bot and not wanting join/parts between messages. + default: True + version_added: "2.0" # informational: requirements for nodes requirements: [ socket ] @@ -128,7 +134,7 @@ def send_msg(msg, server='localhost', port='6667', channel=None, nick_to=[], key=None, topic=None, - nick="ansible", color='none', passwd=False, timeout=30, use_ssl=False): + nick="ansible", color='none', passwd=False, timeout=30, use_ssl=False, part=True): '''send message to IRC''' colornumbers = { @@ -194,9 +200,10 @@ def send_msg(msg, server='localhost', port='6667', channel=None, nick_to=[], key if channel: irc.send('PRIVMSG %s :%s\r\n' % (channel, message)) sleep(1) - irc.send('PART %s\r\n' % channel) - irc.send('QUIT\r\n') - sleep(1) + if part: + irc.send('PART %s\r\n' % channel) + irc.send('QUIT\r\n') + sleep(1) irc.close() # =========================================== @@ -219,6 +226,7 @@ def main(): topic=dict(), passwd=dict(), timeout=dict(type='int', default=30), + part=dict(type='bool', default=True), use_ssl=dict(type='bool', default=False) ), supports_check_mode=True, @@ -239,9 +247,10 @@ def main(): passwd = module.params["passwd"] timeout = module.params["timeout"] use_ssl = module.params["use_ssl"] + part = module.params["part"] try: - send_msg(msg, server, port, channel, nick_to, key, topic, nick, color, passwd, timeout, use_ssl) + send_msg(msg, server, port, channel, nick_to, key, topic, nick, color, passwd, timeout, use_ssl, part) except Exception, e: module.fail_json(msg="unable to send to IRC: %s" % e) From fded415ff2cbe969f6a9427521be9e0b4a785b8c Mon Sep 17 00:00:00 2001 From: Constantine Romanov Date: Thu, 3 Sep 2015 13:20:54 +0300 Subject: [PATCH 0690/2522] Update mongodb_user.py Auth source support --- database/misc/mongodb_user.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index 0529abdea09..6b434f73abe 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -47,6 +47,11 @@ - The port to connect to required: false default: 27017 + login_database: + description: + - The database where login credentials are stored + required: false + default: null replica_set: version_added: "1.6" description: @@ -197,6 +202,7 @@ def main(): login_password=dict(default=None), login_host=dict(default='localhost'), login_port=dict(default='27017'), + login_database=dict(default=None), replica_set=dict(default=None), database=dict(required=True, aliases=['db']), name=dict(required=True, aliases=['user']), @@ -215,6 +221,8 @@ def main(): login_password = module.params['login_password'] login_host = module.params['login_host'] login_port = module.params['login_port'] + login_database = module.params['login_database'] + replica_set = module.params['replica_set'] db_name = module.params['database'] user = module.params['name'] @@ -239,7 +247,7 @@ def main(): module.fail_json(msg='when supplying login arguments, both login_user and login_password must be provided') if login_user is not None and login_password is not None: - client.admin.authenticate(login_user, login_password) + client.admin.authenticate(login_user, login_password, source=login_database) elif LooseVersion(PyMongoVersion) >= LooseVersion('3.0'): if db_name != "admin": module.fail_json(msg='The localhost login exception only allows the first admin account to be created') From 781043f511b83bde28de698ed53a914b40197103 Mon Sep 17 00:00:00 2001 From: Constantine Romanov Date: Thu, 3 Sep 2015 22:02:36 +0300 Subject: [PATCH 0691/2522] Update mongodb_user.py version_added: "2.0" --- database/misc/mongodb_user.py | 1 + 1 file changed, 1 insertion(+) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index 6b434f73abe..c18ad6004f5 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -48,6 +48,7 @@ required: false default: 27017 login_database: + version_added: "2.0" description: - The database where login credentials are stored required: false From 041d6f6077ac61af3808c52d28375af7e8778852 Mon Sep 17 00:00:00 2001 From: dbhirko Date: Thu, 3 Sep 2015 16:00:41 -0400 Subject: [PATCH 0692/2522] Change boto connection object from ec2 to vpc Issue 906 - ec2_vpc_igw has incorrect connection parameters --- cloud/amazon/ec2_vpc_igw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_vpc_igw.py b/cloud/amazon/ec2_vpc_igw.py index 63be48248ef..5218bff5e6e 100644 --- a/cloud/amazon/ec2_vpc_igw.py +++ b/cloud/amazon/ec2_vpc_igw.py @@ -133,7 +133,7 @@ def main(): if region: try: - connection = connect_to_aws(boto.ec2, region, **aws_connect_params) + connection = connect_to_aws(boto.vpc, region, **aws_connect_params) except (boto.exception.NoAuthHandlerFound, StandardError), e: module.fail_json(msg=str(e)) else: From a9d5392b7158bf2a6863d9a1917b2755e1cacf07 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 27 Aug 2015 14:30:29 -0400 Subject: [PATCH 0693/2522] fixed option description --- cloud/amazon/s3_bucket.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/cloud/amazon/s3_bucket.py b/cloud/amazon/s3_bucket.py index 25c085f8173..8e660283472 100644 --- a/cloud/amazon/s3_bucket.py +++ b/cloud/amazon/s3_bucket.py @@ -20,10 +20,10 @@ description: - Manage s3 buckets in AWS version_added: "2.0" -author: Rob White (@wimnat) +author: "Rob White (@wimnat)" options: force: - description: + description: - When trying to delete a bucket, delete all keys in the bucket first (an s3 bucket must be empty for a successful deletion) required: false default: no @@ -40,11 +40,12 @@ default: null region: description: - - AWS region to create the bucket in. If not set then the value of the AWS_REGION and EC2_REGION environment variables are checked, followed by the aws_region and ec2_region settings in the Boto config file. If none of those are set the region defaults to the S3 Location: US Standard. + - AWS region to create the bucket in. If not set then the value of the AWS_REGION and EC2_REGION environment variables are checked, followed by the aws_region and ec2_region settings in the Boto config file. If none of those are set the region defaults to the S3 Location: US Standard. required: false default: null s3_url: - description: S3 URL endpoint for usage with Eucalypus, fakes3, etc. Otherwise assumes AWS + description: + - S3 URL endpoint for usage with Eucalypus, fakes3, etc. Otherwise assumes AWS default: null aliases: [ S3_URL ] requester_pays: @@ -65,12 +66,12 @@ required: false default: null versioning: - description: + description: - Whether versioning is enabled or disabled (note that once versioning is enabled, it can only be suspended) required: false default: no choices: [ 'yes', 'no' ] - + extends_documentation_fragment: aws ''' @@ -387,4 +388,4 @@ def main(): from ansible.module_utils.ec2 import * if __name__ == '__main__': - main() \ No newline at end of file + main() From 5e45d9dffbe941a27afbf828bb31fb1dbc9d725e Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Fri, 4 Sep 2015 14:50:27 -0400 Subject: [PATCH 0694/2522] added version_added to new feature --- packaging/language/bower.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packaging/language/bower.py b/packaging/language/bower.py index d199b756738..c835fbf797d 100644 --- a/packaging/language/bower.py +++ b/packaging/language/bower.py @@ -43,6 +43,7 @@ required: false default: no choices: [ "yes", "no" ] + version_added: "2.0" path: description: - The base path where to install the bower packages From 6a3cf6335172adc94cda8588930e4760b471551d Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Sat, 5 Sep 2015 08:55:34 -0700 Subject: [PATCH 0695/2522] correct documentation formatting --- cloud/amazon/s3_bucket.py | 2 +- cloud/amazon/s3_lifecycle.py | 14 +++++++------- cloud/vmware/vmware_target_canonical_facts.py | 4 ++-- windows/win_package.py | 10 +++++----- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/cloud/amazon/s3_bucket.py b/cloud/amazon/s3_bucket.py index 8e660283472..cc0442eccc1 100644 --- a/cloud/amazon/s3_bucket.py +++ b/cloud/amazon/s3_bucket.py @@ -40,7 +40,7 @@ default: null region: description: - - AWS region to create the bucket in. If not set then the value of the AWS_REGION and EC2_REGION environment variables are checked, followed by the aws_region and ec2_region settings in the Boto config file. If none of those are set the region defaults to the S3 Location: US Standard. + - "AWS region to create the bucket in. If not set then the value of the AWS_REGION and EC2_REGION environment variables are checked, followed by the aws_region and ec2_region settings in the Boto config file. If none of those are set the region defaults to the S3 Location: US Standard." required: false default: null s3_url: diff --git a/cloud/amazon/s3_lifecycle.py b/cloud/amazon/s3_lifecycle.py index 3328a33f15f..7a54365c8bd 100644 --- a/cloud/amazon/s3_lifecycle.py +++ b/cloud/amazon/s3_lifecycle.py @@ -20,7 +20,7 @@ description: - Manage s3 bucket lifecycle rules in AWS version_added: "2.0" -author: Rob White (@wimnat) +author: "Rob White (@wimnat)" notes: - If specifying expiration time as days then transition time must also be specified in days - If specifying expiration time as a date then transition time must also be specified as a date @@ -35,7 +35,7 @@ description: - "Indicates the lifetime of the objects that are subject to the rule by the date they will expire. The value must be ISO-8601 format, the time must be midnight and a GMT timezone must be specified." required: false - default: null + default: null expiration_days: description: - "Indicates the lifetime, in days, of the objects that are subject to the rule. The value must be a non-zero positive integer." @@ -43,9 +43,9 @@ default: null prefix: description: - - "Prefix identifying one or more objects to which the rule applies. If no prefix is specified, the rule will apply to the whole bucket." - required: false - default: null + - "Prefix identifying one or more objects to which the rule applies. If no prefix is specified, the rule will apply to the whole bucket." + required: false + default: null region: description: - "AWS region to create the bucket in. If not set then the value of the AWS_REGION and EC2_REGION environment variables are checked, followed by the aws_region and ec2_region settings in the Boto config file. If none of those are set the region defaults to the S3 Location: US Standard." @@ -54,8 +54,8 @@ rule_id: description: - "Unique identifier for the rule. The value cannot be longer than 255 characters. A unique value for the rule will be generated if no value is provided." - required: false - default: null + required: false + default: null state: description: - "Create or remove the lifecycle rule" diff --git a/cloud/vmware/vmware_target_canonical_facts.py b/cloud/vmware/vmware_target_canonical_facts.py index 987b4a98753..028fc8c83a2 100644 --- a/cloud/vmware/vmware_target_canonical_facts.py +++ b/cloud/vmware/vmware_target_canonical_facts.py @@ -24,14 +24,14 @@ short_description: Return canonical (NAA) from an ESXi host description: - Return canonical (NAA) from an ESXi host based on SCSI target ID -version_added: 2.0 +version_added: "2.0" author: Joseph Callen notes: requirements: - Tested on vSphere 5.5 - PyVmomi installed options: - hostname: + hostname: description: - The hostname or IP address of the vSphere vCenter required: True diff --git a/windows/win_package.py b/windows/win_package.py index 3072dbed3de..d20e5ee8816 100644 --- a/windows/win_package.py +++ b/windows/win_package.py @@ -44,7 +44,7 @@ product_id: description: - product id of the installed package (used for checking if already installed) - required: false + required: false default: null aliases: [] arguments: @@ -54,20 +54,20 @@ aliases: [] state: description: - - Install or Uninstall - choices: + - Install or Uninstall + choices: - present - absent default: present aliases: [ensure] user_name: description: - - Username of an account with access to the package if its located on a file share. Only needed if the winrm user doesn't have access to the package. Also specify user_password for this to function properly. + - Username of an account with access to the package if its located on a file share. Only needed if the winrm user doesn't have access to the package. Also specify user_password for this to function properly. default: null aliases: [] user_password: description: - - Password of an account with access to the package if its located on a file share. Only needed if the winrm user doesn't have access to the package. Also specify user_name for this to function properly. + - Password of an account with access to the package if its located on a file share. Only needed if the winrm user doesn't have access to the package. Also specify user_name for this to function properly. default: null aliases: [] author: Trond Hindenes From ae7f7c2c6423d2cf6afaca1d76eae003673527d6 Mon Sep 17 00:00:00 2001 From: Mike Liu Date: Sat, 5 Sep 2015 12:37:32 -0400 Subject: [PATCH 0696/2522] Check that the current output from 'list_users' command contains a '\t'. The `rabbitmqctl list_users` command will list the user's last login time which does not include `\t` character. This is causing a ValueError exception when attempting to split a user and its tags from the command output. This fix will check for a `\t` in the current line of the output before splitting. --- messaging/rabbitmq_user.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/messaging/rabbitmq_user.py b/messaging/rabbitmq_user.py index b12178e08ea..c6ff6a41928 100644 --- a/messaging/rabbitmq_user.py +++ b/messaging/rabbitmq_user.py @@ -136,6 +136,9 @@ def get(self): users = self._exec(['list_users'], True) for user_tag in users: + if '\t' not in user_tag: + continue + user, tags = user_tag.split('\t') if user == self.username: From 6e2b97427bfa7cfe3d61e88fc71625e86bfd66ab Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sun, 6 Sep 2015 01:42:56 +0200 Subject: [PATCH 0697/2522] cloudstack: cs_account: fix error handing on state=absent --- cloud/cloudstack/cs_account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/cloudstack/cs_account.py b/cloud/cloudstack/cs_account.py index 052354c581e..e96d8e12638 100644 --- a/cloud/cloudstack/cs_account.py +++ b/cloud/cloudstack/cs_account.py @@ -326,7 +326,7 @@ def absent_account(self): if not self.module.check_mode: res = self.cs.deleteAccount(id=account['id']) - if 'errortext' in account: + if 'errortext' in res: self.module.fail_json(msg="Failed: '%s'" % res['errortext']) poll_async = self.module.params.get('poll_async') From a55dbb717cfd1a73062e952623e1009b4ef669a7 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Sun, 6 Sep 2015 07:56:58 -0700 Subject: [PATCH 0698/2522] Try to revert the pieces of #651 that @willthames and @erydo requested --- cloud/amazon/ec2_vpc_igw.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/amazon/ec2_vpc_igw.py b/cloud/amazon/ec2_vpc_igw.py index 71c2519dc4e..e374580433a 100644 --- a/cloud/amazon/ec2_vpc_igw.py +++ b/cloud/amazon/ec2_vpc_igw.py @@ -120,8 +120,8 @@ def main(): argument_spec = ec2_argument_spec() argument_spec.update( dict( - vpc_id = dict(required=True, default=None), - state = dict(required=False, default='present', choices=['present', 'absent']) + vpc_id = dict(required=True), + state = dict(default='present', choices=['present', 'absent']) ) ) @@ -144,7 +144,7 @@ def main(): module.fail_json(msg="region must be specified") vpc_id = module.params.get('vpc_id') - state = module.params.get('state') + state = module.params.get('state', 'present') try: if state == 'present': From e5fdbc25a2b10c7cea8354b9a1977817e1382ef2 Mon Sep 17 00:00:00 2001 From: Bret Martin Date: Sun, 6 Sep 2015 22:00:02 -0400 Subject: [PATCH 0699/2522] Only wait for ENI attachment at creation when instance_id is specified --- cloud/amazon/ec2_eni.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/ec2_eni.py b/cloud/amazon/ec2_eni.py index 9e878e7d558..4d08aeb26d0 100644 --- a/cloud/amazon/ec2_eni.py +++ b/cloud/amazon/ec2_eni.py @@ -213,9 +213,9 @@ def create_eni(connection, module): except BotoServerError as ex: eni.delete() raise + # Wait to allow creation / attachment to finish + wait_for_eni(eni, "attached") changed = True - # Wait to allow creation / attachment to finish - wait_for_eni(eni, "attached") eni.update() except BotoServerError as e: From c7f7698fdc4f7c796300532b2dcd58607f3ed8a6 Mon Sep 17 00:00:00 2001 From: baba Date: Mon, 7 Sep 2015 15:40:52 +0900 Subject: [PATCH 0700/2522] Fix missing parameter in typetalk module --- notification/typetalk.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/notification/typetalk.py b/notification/typetalk.py index 4f6ee28130b..8a2dad3d6a2 100644 --- a/notification/typetalk.py +++ b/notification/typetalk.py @@ -72,14 +72,14 @@ def do_request(module, url, params, headers=None): raise exc return r -def get_access_token(client_id, client_secret): +def get_access_token(module, client_id, client_secret): params = { 'client_id': client_id, 'client_secret': client_secret, 'grant_type': 'client_credentials', 'scope': 'topic.post' } - res = do_request('https://typetalk.in/oauth2/access_token', params) + res = do_request(module, 'https://typetalk.in/oauth2/access_token', params) return json.load(res)['access_token'] @@ -88,7 +88,7 @@ def send_message(module, client_id, client_secret, topic, msg): send message to typetalk """ try: - access_token = get_access_token(client_id, client_secret) + access_token = get_access_token(module, client_id, client_secret) url = 'https://typetalk.in/api/v1/topics/%d' % topic headers = { 'Authorization': 'Bearer %s' % access_token, From 39eb3807f3497fffeea19183691b54489a811239 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20N=C3=A4gele?= Date: Tue, 8 Sep 2015 15:28:05 +0200 Subject: [PATCH 0701/2522] fix #894 by actually updating with the modified settings --- system/firewalld.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/system/firewalld.py b/system/firewalld.py index 9a63da3a544..47d98544000 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -76,7 +76,7 @@ notes: - Not tested on any Debian based system. requirements: [ 'firewalld >= 0.2.11' ] -author: "Adam Miller (@maxamillion)" +author: "Adam Miller (@maxamillion)" ''' EXAMPLES = ''' @@ -138,7 +138,7 @@ def set_port_disabled_permanent(zone, port, protocol): #################### # source handling -# +# def get_source(zone, source): fw_zone = fw.config().getZoneByName(zone) fw_settings = fw_zone.getSettings() @@ -151,11 +151,13 @@ def add_source(zone, source): fw_zone = fw.config().getZoneByName(zone) fw_settings = fw_zone.getSettings() fw_settings.addSource(source) + fw_zone.update(fw_settings) def remove_source(zone, source): fw_zone = fw.config().getZoneByName(zone) fw_settings = fw_zone.getSettings() fw_settings.removeSource(source) + fw_zone.update(fw_settings) #################### # service handling @@ -191,7 +193,7 @@ def set_service_disabled_permanent(zone, service): fw_settings = fw_zone.getSettings() fw_settings.removeService(service) fw_zone.update(fw_settings) - + #################### # rich rule handling From 356e867721568eab2ca4248251daeb8c4820a1c3 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Tue, 8 Sep 2015 10:50:13 -0400 Subject: [PATCH 0702/2522] adds missing serialize_instance function --- cloud/vmware/vca_nat.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/cloud/vmware/vca_nat.py b/cloud/vmware/vca_nat.py index c948605ce48..d34a52b6c75 100644 --- a/cloud/vmware/vca_nat.py +++ b/cloud/vmware/vca_nat.py @@ -61,7 +61,7 @@ - The type of service we are authenticating against required: false default: vca - choices: [ "vca", "vchs", "vcd" ] + choices: [ "vca", "vchs", "vcd" ] state: description: - if the object should be added or removed @@ -108,7 +108,7 @@ instance_id: 'b15ff1e5-1024-4f55-889f-ea0209726282' vdc_name: 'benz_ansible' state: 'present' - nat_rules: + nat_rules: - rule_type: SNAT original_ip: 192.168.2.10 translated_ip: 107.189.95.208 @@ -121,7 +121,7 @@ instance_id: 'b15ff1e5-1024-4f55-889f-ea0209726282' vdc_name: 'benz_ansible' state: 'present' - nat_rules: + nat_rules: - rule_type: DNAT original_ip: 107.189.95.208 original_port: 22 @@ -145,6 +145,12 @@ LOGIN_HOST['vchs'] = 'vchs.vmware.com' VALID_RULE_KEYS = ['rule_type', 'original_ip', 'original_port', 'translated_ip', 'translated_port', 'protocol'] +def serialize_instances(instance_list): + instances = [] + for i in instance_list: + instances.append(dict(apiUrl=i['apiUrl'], instance_id=i['id'])) + return instances + def vca_login(module=None): service_type = module.params.get('service_type') username = module.params.get('username') @@ -216,7 +222,7 @@ def vca_login(module=None): if not vca.login(token=vca.token, org=org, org_url=vca.vcloud_session.org_url): module.fail_json(msg = "Failed to login to org", error=vca.response.content) return vca - + def validate_nat_rules(module=None, nat_rules=None): for rule in nat_rules: if not isinstance(rule, dict): @@ -301,7 +307,7 @@ def main(): if service_type == 'vcd': if not host: module.fail_json(msg="When service type is vcd host parameter is mandatory") - + vca = vca_login(module) vdc = vca.get_vdc(vdc_name) if not vdc: @@ -368,7 +374,7 @@ def main(): module.exit_json(changed=True, rules_added=mod_rules) - + # import module snippets from ansible.module_utils.basic import * if __name__ == '__main__': From a9eb69b0fa54d8299fddae23f6c89d4d0ebf7a6a Mon Sep 17 00:00:00 2001 From: Michael Perzel Date: Tue, 8 Sep 2015 11:18:26 -0500 Subject: [PATCH 0703/2522] Use helper methods to validate vars. Cleanup logic. --- windows/win_scheduled_task.ps1 | 62 ++++++++++------------------------ 1 file changed, 17 insertions(+), 45 deletions(-) diff --git a/windows/win_scheduled_task.ps1 b/windows/win_scheduled_task.ps1 index c54be17db46..85b54c89d05 100644 --- a/windows/win_scheduled_task.ps1 +++ b/windows/win_scheduled_task.ps1 @@ -26,22 +26,29 @@ $params = Parse-Args $args; $result = New-Object PSObject; Set-Attr $result "changed" $false; -if ($params.name) -{ - $name = $params.name +#Required vars +$name = Get-Attr -obj $params -name name -failifempty $true -resultobj $result +$state = Get-Attr -obj $params -name state -failifempty $true -resultobj $result +if( ($state -ne "present") -and ($state -ne "absent") ) { + Fail-Json $result "state must be present or absent" } -else -{ - Fail-Json $result "missing required argument: name" + +#Vars conditionally required +if($state -eq "present") { + $execute = Get-Attr -obj $params -name execute -failifempty $true -resultobj $result + $frequency = Get-Attr -obj $params -name frequency -failifempty $true -resultobj $result + $time = Get-Attr -obj $params -name time -failifempty $true -resultobj $result } -if ($params.state) +if ($params.daysOfWeek) { - $state = $params.state + $daysOfWeek = $params.daysOfWeek } -else +elseif ($frequency -eq "weekly") { - Fail-Json $result "missing required argument: state" + Fail-Json $result "missing required argument: daysOfWeek" } + +# Vars with defaults if ($params.enabled) { $enabled = $params.enabled | ConvertTo-Bool @@ -58,17 +65,6 @@ else { $description = " " #default } -if ($params.execute) -{ - $execute = $params.execute -} -elseif ($state -eq "present") -{ - Fail-Json $result "missing required argument: execute" -} -if( $state -ne "present" -and $state -ne "absent") { - Fail-Json $result "state must be present or absent" -} if ($params.path) { $path = "\{0}\" -f $params.path @@ -77,30 +73,6 @@ else { $path = "\" #default } -if ($params.frequency) -{ - $frequency = $params.frequency -} -elseif($state -eq "present") -{ - Fail-Json $result "missing required argument: frequency" -} -if ($params.time) -{ - $time = $params.time -} -elseif($state -eq "present") -{ - Fail-Json $result "missing required argument: time" -} -if ($params.daysOfWeek) -{ - $daysOfWeek = $params.daysOfWeek -} -elseif ($frequency -eq "weekly") -{ - Fail-Json $result "missing required argument: daysOfWeek" -} try { $task = Get-ScheduledTask -TaskPath "$path" | Where-Object {$_.TaskName -eq "$name"} From d0b4bc0dda729bb87a49ac1a7ccf281aa5a6c6d5 Mon Sep 17 00:00:00 2001 From: Michael Perzel Date: Tue, 8 Sep 2015 11:20:35 -0500 Subject: [PATCH 0704/2522] Show order of operations with parenthesis --- windows/win_scheduled_task.ps1 | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/windows/win_scheduled_task.ps1 b/windows/win_scheduled_task.ps1 index 85b54c89d05..5777d198cc8 100644 --- a/windows/win_scheduled_task.ps1 +++ b/windows/win_scheduled_task.ps1 @@ -94,7 +94,7 @@ try { if ($measure.count -eq 1 ) { $exists = $true } - elseif ($measure.count -eq 0 -and $state -eq "absent" ){ + elseif ( ($measure.count -eq 0) -and ($state -eq "absent") ){ Set-Attr $result "msg" "Task does not exist" Exit-Json $result } @@ -120,13 +120,13 @@ try { } } - if ($state -eq "absent" -and $exists -eq $true) { + if ( ($state -eq "absent") -and ($exists -eq $true) ) { Unregister-ScheduledTask -TaskName $name -Confirm:$false $result.changed = $true Set-Attr $result "msg" "Deleted task $name" Exit-Json $result } - elseif ($state -eq "absent" -and $exists -eq $false) { + elseif ( ($state -eq "absent") -and ($exists -eq $false) ) { Set-Attr $result "msg" "Task $name does not exist" Exit-Json $result } @@ -138,14 +138,14 @@ try { $settings = New-ScheduledTaskSettingsSet } - if ($state -eq "present" -and $exists -eq $false){ + if ( ($state -eq "present") -and ($exists -eq $false) ){ $action = New-ScheduledTaskAction -Execute $execute Register-ScheduledTask -Action $action -Trigger $trigger -TaskName $name -Description $description -TaskPath $path -Settings $settings $task = Get-ScheduledTask -TaskName $name Set-Attr $result "msg" "Added new task $name" $result.changed = $true } - elseif($state -eq "present" -and $exists -eq $true) { + elseif( ($state -eq "present") -and ($exists -eq $true) ) { if ($task.Description -eq $description -and $task.TaskName -eq $name -and $task.TaskPath -eq $path -and $task.Actions.Execute -eq $execute -and $taskState -eq $enabled) { #No change in the task yet Set-Attr $result "msg" "No change in task $name" From 027dff6d3d279ec1eaafbd64ac2cdda0eb1fe75b Mon Sep 17 00:00:00 2001 From: Michael Perzel Date: Tue, 8 Sep 2015 13:37:39 -0500 Subject: [PATCH 0705/2522] Add support for command arguments --- windows/win_scheduled_task.ps1 | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/windows/win_scheduled_task.ps1 b/windows/win_scheduled_task.ps1 index 5777d198cc8..4b64b7999e2 100644 --- a/windows/win_scheduled_task.ps1 +++ b/windows/win_scheduled_task.ps1 @@ -74,6 +74,12 @@ else $path = "\" #default } +# Optional vars +if ($params.argument) +{ + $argument = $params.argument +} + try { $task = Get-ScheduledTask -TaskPath "$path" | Where-Object {$_.TaskName -eq "$name"} @@ -137,6 +143,13 @@ try { else { $settings = New-ScheduledTaskSettingsSet } + + if ($argument) { + $action = New-ScheduledTaskAction -Execute $execute -Argument $argument + } + else { + $action = New-ScheduledTaskAction -Execute $execute + } if ( ($state -eq "present") -and ($exists -eq $false) ){ $action = New-ScheduledTaskAction -Execute $execute @@ -152,9 +165,7 @@ try { } else { Unregister-ScheduledTask -TaskName $name -Confirm:$false - $action = New-ScheduledTaskAction -Execute $execute - Register-ScheduledTask -Action $action -Trigger $trigger -TaskName $name -Description $description -TaskPath $path -Settings $settings - $task = Get-ScheduledTask -TaskName $name + Register-ScheduledTask -Action $action -Trigger $trigger -TaskName $name -Description $description -TaskPath $path -Settings $settings -Principal $principal Set-Attr $result "msg" "Updated task $name" $result.changed = $true } From de1696cb74804c80b095620f9a87c04e3f37b460 Mon Sep 17 00:00:00 2001 From: Michael Perzel Date: Tue, 8 Sep 2015 14:16:30 -0500 Subject: [PATCH 0706/2522] Add support for specifying user for scheduled task to run as --- windows/win_scheduled_task.ps1 | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/windows/win_scheduled_task.ps1 b/windows/win_scheduled_task.ps1 index 4b64b7999e2..e71b9bd7b7e 100644 --- a/windows/win_scheduled_task.ps1 +++ b/windows/win_scheduled_task.ps1 @@ -38,14 +38,15 @@ if($state -eq "present") { $execute = Get-Attr -obj $params -name execute -failifempty $true -resultobj $result $frequency = Get-Attr -obj $params -name frequency -failifempty $true -resultobj $result $time = Get-Attr -obj $params -name time -failifempty $true -resultobj $result + $user = Get-Attr -obj $params -name user -failifempty $true -resultobj $result } -if ($params.daysOfWeek) +if ($params.days_of_week) { - $daysOfWeek = $params.daysOfWeek + $days_of_week = $params.days_of_week } elseif ($frequency -eq "weekly") { - Fail-Json $result "missing required argument: daysOfWeek" + Fail-Json $result "missing required argument: days_of_week" } # Vars with defaults @@ -119,7 +120,7 @@ try { $trigger = New-ScheduledTaskTrigger -Daily -At $time } elseif ($frequency -eq "weekly"){ - $trigger = New-ScheduledTaskTrigger -Weekly -At $time -DaysOfWeek $daysOfWeek + $trigger = New-ScheduledTaskTrigger -Weekly -At $time -DaysOfWeek $days_of_week } else { Fail-Json $result "frequency must be daily or weekly" @@ -137,13 +138,15 @@ try { Exit-Json $result } + $principal = New-ScheduledTaskPrincipal -UserId "$user" -LogonType ServiceAccount + if ($enabled -eq $false){ $settings = New-ScheduledTaskSettingsSet -Disable } else { $settings = New-ScheduledTaskSettingsSet } - + if ($argument) { $action = New-ScheduledTaskAction -Execute $execute -Argument $argument } @@ -153,19 +156,19 @@ try { if ( ($state -eq "present") -and ($exists -eq $false) ){ $action = New-ScheduledTaskAction -Execute $execute - Register-ScheduledTask -Action $action -Trigger $trigger -TaskName $name -Description $description -TaskPath $path -Settings $settings + Register-ScheduledTask -Action $action -Trigger $trigger -TaskName $name -Description $description -TaskPath $path -Settings $settings -Principal $principal $task = Get-ScheduledTask -TaskName $name Set-Attr $result "msg" "Added new task $name" $result.changed = $true } elseif( ($state -eq "present") -and ($exists -eq $true) ) { - if ($task.Description -eq $description -and $task.TaskName -eq $name -and $task.TaskPath -eq $path -and $task.Actions.Execute -eq $execute -and $taskState -eq $enabled) { - #No change in the task yet + if ($task.Description -eq $description -and $task.TaskName -eq $name -and $task.TaskPath -eq $path -and $task.Actions.Execute -eq $execute -and $taskState -eq $enabled -and $task.Principal.UserId -eq $user) { + #No change in the task Set-Attr $result "msg" "No change in task $name" } else { Unregister-ScheduledTask -TaskName $name -Confirm:$false - Register-ScheduledTask -Action $action -Trigger $trigger -TaskName $name -Description $description -TaskPath $path -Settings $settings -Principal $principal + Register-ScheduledTask -Action $action -Trigger $trigger -TaskName $name -Description $description -TaskPath $path -Settings $settings -Principal $principal Set-Attr $result "msg" "Updated task $name" $result.changed = $true } @@ -176,4 +179,4 @@ try { catch { Fail-Json $result $_.Exception.Message -} +} \ No newline at end of file From 64416ff094382b9d72ac01f81664633b287c2622 Mon Sep 17 00:00:00 2001 From: Michael Perzel Date: Tue, 8 Sep 2015 14:35:34 -0500 Subject: [PATCH 0707/2522] Fix logging of error message --- windows/win_scheduled_task.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_scheduled_task.ps1 b/windows/win_scheduled_task.ps1 index e71b9bd7b7e..e6effebb7bd 100644 --- a/windows/win_scheduled_task.ps1 +++ b/windows/win_scheduled_task.ps1 @@ -110,7 +110,7 @@ try { } else { # This should never occur - Fail-Json $result "$measure.count scheduled tasks found" + Fail-Json $result "$($measure.count) scheduled tasks found" } Set-Attr $result "exists" "$exists" From 685c935a371c6ae64c870585f4caaa6aa4f5092d Mon Sep 17 00:00:00 2001 From: Michael Perzel Date: Tue, 8 Sep 2015 14:36:11 -0500 Subject: [PATCH 0708/2522] Documentation updates --- windows/win_scheduled_task.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/windows/win_scheduled_task.py b/windows/win_scheduled_task.py index 8b078ae9ae8..bb2cdbc0b82 100644 --- a/windows/win_scheduled_task.py +++ b/windows/win_scheduled_task.py @@ -30,6 +30,10 @@ description: - Name of the scheduled task required: true + description: + description: + - The description for the scheduled task + required: false enabled: description: - Enable/disable the task @@ -40,16 +44,26 @@ state: description: - State that the task should become + required: true choices: - present - absent + user: + description: + - User to run scheduled task as + required: false execute: description: - Command the scheduled task should execute required: false + argument: + description: + - Arguments to provide scheduled task action + required: false frequency: description: - The frequency of the command + required: false choices: - daily - weekly @@ -57,7 +71,7 @@ description: - Time to execute scheduled task required: false - daysOfWeek: + days_of_week: description: - Days of the week to run a weekly task required: false @@ -69,5 +83,5 @@ EXAMPLES = ''' # Create a scheduled task to open a command prompt - win_scheduled_task: name="TaskName" execute="cmd" frequency="daily" time="9am" description="open command prompt" path="example" enable=yes state=present + win_scheduled_task: name="TaskName" execute="cmd" frequency="daily" time="9am" description="open command prompt" path="example" enable=yes state=present user=SYSTEM ''' From a7675e662154f4475202a37049821777d04c598e Mon Sep 17 00:00:00 2001 From: Michael Perzel Date: Tue, 8 Sep 2015 14:40:21 -0500 Subject: [PATCH 0709/2522] Note parameters that are not idempotent --- windows/win_scheduled_task.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/windows/win_scheduled_task.py b/windows/win_scheduled_task.py index bb2cdbc0b82..e26cbc00cf0 100644 --- a/windows/win_scheduled_task.py +++ b/windows/win_scheduled_task.py @@ -62,18 +62,18 @@ required: false frequency: description: - - The frequency of the command + - The frequency of the command, not idempotent required: false choices: - daily - weekly time: description: - - Time to execute scheduled task + - Time to execute scheduled task, not idempotent required: false days_of_week: description: - - Days of the week to run a weekly task + - Days of the week to run a weekly task, not idempotent required: false path: description: From 123a2b25ed35fec0fd9028f27749df945313d838 Mon Sep 17 00:00:00 2001 From: Michael Perzel Date: Tue, 8 Sep 2015 14:45:21 -0500 Subject: [PATCH 0710/2522] Remove duplicate action declaration. --- windows/win_scheduled_task.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/windows/win_scheduled_task.ps1 b/windows/win_scheduled_task.ps1 index e6effebb7bd..b63bd130134 100644 --- a/windows/win_scheduled_task.ps1 +++ b/windows/win_scheduled_task.ps1 @@ -155,7 +155,6 @@ try { } if ( ($state -eq "present") -and ($exists -eq $false) ){ - $action = New-ScheduledTaskAction -Execute $execute Register-ScheduledTask -Action $action -Trigger $trigger -TaskName $name -Description $description -TaskPath $path -Settings $settings -Principal $principal $task = Get-ScheduledTask -TaskName $name Set-Attr $result "msg" "Added new task $name" From 84b460d96d9cfd9d7445d8149f5df33388b42e14 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Wed, 9 Sep 2015 09:58:24 -0400 Subject: [PATCH 0711/2522] added version added --- packaging/os/opkg.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packaging/os/opkg.py b/packaging/os/opkg.py index 5fb52eae2eb..9ac8f99b8c8 100644 --- a/packaging/os/opkg.py +++ b/packaging/os/opkg.py @@ -42,6 +42,7 @@ choices: ["", "depends", "maintainer", "reinstall", "overwrite", "downgrade", "space", "postinstall", "remove", "checksum", "removal-of-dependent-packages"] required: false default: absent + version_added: "2.0" update_cache: description: - update the package db first From cef26fd695f62237e850503df2a3035612600b84 Mon Sep 17 00:00:00 2001 From: Mark Hamilton Date: Wed, 9 Sep 2015 14:28:50 -0700 Subject: [PATCH 0712/2522] removed extra syslog message and DOCUMENTATION variable --- network/openvswitch_db.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/network/openvswitch_db.py b/network/openvswitch_db.py index d5bc5bc7f37..e6ec2658e0b 100644 --- a/network/openvswitch_db.py +++ b/network/openvswitch_db.py @@ -60,8 +60,6 @@ - How long to wait for ovs-vswitchd to respond """ -DOCUMENTATION = __doc__ - EXAMPLES = ''' # Increase the maximum idle time to 50 seconds before pruning unused kernel # rules. @@ -76,7 +74,6 @@ def cmd_run(module, cmd, check_rc=True): """ Log and run ovs-vsctl command. """ - syslog.syslog(syslog.LOG_NOTICE, cmd) return module.run_command(cmd.split(" "), check_rc=check_rc) From bdfb67ae28943de65b42531f5bc3f03a4b857b12 Mon Sep 17 00:00:00 2001 From: steynovich Date: Wed, 9 Sep 2015 23:43:15 +0200 Subject: [PATCH 0713/2522] Update route53_zone.py Proposed fix for issue #940 Fix: Include common AWS parameters in module arguments --- cloud/amazon/route53_zone.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cloud/amazon/route53_zone.py b/cloud/amazon/route53_zone.py index 4630e00d4fa..b40a033b024 100644 --- a/cloud/amazon/route53_zone.py +++ b/cloud/amazon/route53_zone.py @@ -64,15 +64,14 @@ def main(): - module = AnsibleModule( - argument_spec=dict( + argument_spec = ec2_argument_spec() + argument_spec.update(dict( zone=dict(required=True), state=dict(default='present', choices=['present', 'absent']), vpc_id=dict(default=None), vpc_region=dict(default=None), - comment=dict(default=''), - ) - ) + comment=dict(default=''))) + module = AnsibleModule(argument_spec=argument_spec) if not HAS_BOTO: module.fail_json(msg='boto required for this module') From f478530803d1dad885b5b1b7f80a8b6483a1d57a Mon Sep 17 00:00:00 2001 From: Mark Chance Date: Wed, 9 Sep 2015 17:05:43 -0600 Subject: [PATCH 0714/2522] cloud amazon ECS service modules --- cloud/amazon/ecs_service.py | 282 ++++++++++++++++++++++++++++++ cloud/amazon/ecs_service_facts.py | 177 +++++++++++++++++++ 2 files changed, 459 insertions(+) create mode 100644 cloud/amazon/ecs_service.py create mode 100644 cloud/amazon/ecs_service_facts.py diff --git a/cloud/amazon/ecs_service.py b/cloud/amazon/ecs_service.py new file mode 100644 index 00000000000..a946fbaf287 --- /dev/null +++ b/cloud/amazon/ecs_service.py @@ -0,0 +1,282 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ecs_service +short_description: create, terminate, start or stop a service in ecs +description: + - Creates or terminates ec2 instances. +notes: + - the service role specified must be assumable (i.e. have a trust relationship + for the ecs service, ecs.amazonaws.com) +dependecies: + - An IAM role must have been created +version_added: "0.9" +options: +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Basic provisioning example +- ecs_service: + name: default + state: present + cluster: string +# Simple example to delete +- ecs_cluster: + name: default + state: absent + cluster: string +''' +RETURN = ''' +cache_updated: + description: if the cache was updated or not + returned: success, in some cases + type: boolean + sample: True +cache_update_time: + description: time of the last cache update (0 if unknown) + returned: success, in some cases + type: datetime + sample: 1425828348000 +stdout: + description: output from apt + returned: success, when needed + type: string + sample: "Reading package lists...\nBuilding dependency tree...\nReading state information...\nThe following extra packages will be installed:\n apache2-bin ..." +stderr: + description: error output from apt + returned: success, when needed + type: string + sample: "AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 127.0.1.1. Set the 'ServerName' directive globally to ..." +''' +try: + import json + import boto + import botocore + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +try: + import boto3 + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + +class EcsServiceManager: + """Handles ECS Services""" + + def __init__(self, module): + self.module = module + + try: + # self.ecs = boto3.client('ecs') + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + if not region: + module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") + self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) + except boto.exception.NoAuthHandlerFound, e: + self.module.fail_json(msg=str(e)) + + # def list_clusters(self): + # return self.client.list_clusters() + # {'failures=[], + # 'ResponseMetadata={'HTTPStatusCode=200, 'RequestId='ce7b5880-1c41-11e5-8a31-47a93a8a98eb'}, + # 'clusters=[{'activeServicesCount=0, 'clusterArn='arn:aws:ecs:us-west-2:777110527155:cluster/default', 'status='ACTIVE', 'pendingTasksCount=0, 'runningTasksCount=0, 'registeredContainerInstancesCount=0, 'clusterName='default'}]} + # {'failures=[{'arn='arn:aws:ecs:us-west-2:777110527155:cluster/bogus', 'reason='MISSING'}], + # 'ResponseMetadata={'HTTPStatusCode=200, 'RequestId='0f66c219-1c42-11e5-8a31-47a93a8a98eb'}, + # 'clusters=[]} + + def find_in_array(self, array_of_services, service_name, field_name='serviceArn'): + for c in array_of_services: + if c[field_name].endswith(service_name): + return c + return None + + def describe_service(self, cluster_name, service_name): + response = self.ecs.describe_services( + cluster=cluster_name, + services=[ + service_name + ]) + msg = '' + if len(response['failures'])>0: + c = self.find_in_array(response['failures'], service_name, 'arn') + msg += ", failure reason is "+c['reason'] + if c and c['reason']=='MISSING': + return None + # fall thru and look through found ones + if len(response['services'])>0: + c = self.find_in_array(response['services'], service_name) + if c: + return c + raise StandardError("Unknown problem describing service %s." % service_name) + + def create_service(self, service_name, cluster_name, task_definition, + load_balancers, desired_count, client_token, role): + response = self.ecs.create_service( + cluster=cluster_name, + serviceName=service_name, + taskDefinition=task_definition, + loadBalancers=load_balancers, + desiredCount=desired_count, + clientToken=client_token, + role=role) + # some fields are datetime which is not JSON serializable + # make them strings + service = response['service'] + if 'deployments' in service: + for d in service['deployments']: + if 'createdAt' in d: + d['createdAt'] = str(d['createdAt']) + if 'updatedAt' in d: + d['updatedAt'] = str(d['updatedAt']) + if 'events' in service: + for e in service['events']: + if 'createdAt' in e: + e['createdAt'] = str(e['createdAt']) + return service + + def delete_service(self, service, cluster=None): + return self.ecs.delete_service(cluster=cluster, service=service) + +def main(): + + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + state=dict(required=True, choices=['present', 'absent', 'deleting'] ), + name=dict(required=True, type='str' ), + cluster=dict(required=False, type='str' ), + task_definition=dict(required=False, type='str' ), + load_balancers=dict(required=False, type='list' ), + desired_count=dict(required=False, type='int' ), + client_token=dict(required=False, type='str' ), + role=dict(required=False, type='str' ), + delay=dict(required=False, type='int', default=10), + repeat=dict(required=False, type='int', default=10) + )) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + if not HAS_BOTO: + module.fail_json(msg='boto is required.') + + if not HAS_BOTO3: + module.fail_json(msg='boto3 is required.') + + if module.params['state'] == 'present': + if not 'task_definition' in module.params and module.params['task_definition'] is None: + module.fail_json(msg="To use create a service, a task_definition must be specified") + if not 'desired_count' in module.params and module.params['desired_count'] is None: + module.fail_json(msg="To use create a service, a desired_count must be specified") + + service_mgr = EcsServiceManager(module) + try: + existing = service_mgr.describe_service(module.params['cluster'], module.params['name']) + except Exception, e: + module.fail_json(msg=str(e)) + + results = dict(changed=False ) + if module.params['state'] == 'present': + if existing and 'status' in existing and existing['status']=="ACTIVE": + del existing['deployments'] + del existing['events'] + results['service']=existing + else: + if not module.check_mode: + if module.params['load_balancers'] is None: + loadBalancers = [] + else: + loadBalancers = module.params['load_balancers'] + if module.params['role'] is None: + role = '' + else: + role = module.params['role'] + if module.params['client_token'] is None: + clientToken = '' + else: + clientToken = module.params['client_token'] + # doesn't exist. create it. + response = service_mgr.create_service(module.params['name'], + module.params['cluster'], + module.params['task_definition'], + loadBalancers, + module.params['desired_count'], + clientToken, + role) + # the bad news is the result has datetime fields that aren't JSON serializable + # nuk'em! + + del response['deployments'] + del response['events'] + + results['service'] = response + + results['changed'] = True + + elif module.params['state'] == 'absent': + if not existing: + pass + else: + # it exists, so we should delete it and mark changed. + # return info about the cluster deleted + del existing['deployments'] + del existing['events'] + results['ansible_facts'] = existing + if 'status' in existing and existing['status']=="INACTIVE": + results['changed'] = False + else: + if not module.check_mode: + try: + service_mgr.delete_service( + module.params['name'], + module.params['cluster'] + ) + except botocore.exceptions.ClientError, e: + module.fail_json(msg=e.message) + results['changed'] = True + + elif module.params['state'] == 'deleting': + if not existing: + module.fail_json(msg="Service '"+module.params['name']+" not found.") + return + # it exists, so we should delete it and mark changed. + # return info about the cluster deleted + delay = module.params['delay'] + repeat = module.params['repeat'] + time.sleep(delay) + for i in range(repeat): + existing = service_mgr.describe_service(module.params['cluster'], module.params['name']) + status = existing['status'] + if status == "INACTIVE": + results['changed'] = True + break + time.sleep(delay) + if i is repeat-1: + module.fail_json(msg="Service still not deleted after "+str(repeat)+" tries of "+str(delay)+" seconds each.") + return + + module.exit_json(**results) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() diff --git a/cloud/amazon/ecs_service_facts.py b/cloud/amazon/ecs_service_facts.py new file mode 100644 index 00000000000..08c21ee3213 --- /dev/null +++ b/cloud/amazon/ecs_service_facts.py @@ -0,0 +1,177 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ecs_service_facts +short_description: list or describe services in ecs +description: + - Lists or describes services in ecs. +version_added: "0.9" +options: + details: + description: + - Set this to true if you want detailed information about the services. + required: false + default: 'false' + choices: ['true', 'false'] + version_added: 1.9 + cluster: + description: + - The cluster ARNS in which to list the services. + required: false + default: 'default' + version_added: 1.9 +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Basic listing example +- ecs_task: + cluster=test-cluster + task_list=123456789012345678901234567890123456 + +# Basic example of deregistering task +- ecs_task: + state: absent + family: console-test-tdn + revision: 1 +''' +RETURN = ''' +cache_updated: + description: if the cache was updated or not + returned: success, in some cases + type: boolean + sample: True +cache_update_time: + description: time of the last cache update (0 if unknown) + returned: success, in some cases + type: datetime + sample: 1425828348000 +stdout: + description: output from apt + returned: success, when needed + type: string + sample: "Reading package lists...\nBuilding dependency tree...\nReading state information...\nThe following extra packages will be installed:\n apache2-bin ..." +stderr: + description: error output from apt + returned: success, when needed + type: string + sample: "AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 127.0.1.1. Set the 'ServerName' directive globally to ..." +''' +try: + import json, os + import boto + import botocore + # import module snippets + from ansible.module_utils.basic import * + from ansible.module_utils.ec2 import * + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +try: + import boto3 + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + +class EcsServiceManager: + """Handles ECS Clusters""" + + def __init__(self, module): + self.module = module + + try: + # self.ecs = boto3.client('ecs') + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + if not region: + module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") + self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) + except boto.exception.NoAuthHandlerFound, e: + self.module.fail_json(msg=str(e)) + + # def list_clusters(self): + # return self.client.list_clusters() + # {'failures': [], + # 'ResponseMetadata': {'HTTPStatusCode': 200, 'RequestId': 'ce7b5880-1c41-11e5-8a31-47a93a8a98eb'}, + # 'clusters': [{'activeServicesCount': 0, 'clusterArn': 'arn:aws:ecs:us-west-2:777110527155:cluster/default', 'status': 'ACTIVE', 'pendingTasksCount': 0, 'runningTasksCount': 0, 'registeredContainerInstancesCount': 0, 'clusterName': 'default'}]} + # {'failures': [{'arn': 'arn:aws:ecs:us-west-2:777110527155:cluster/bogus', 'reason': 'MISSING'}], + # 'ResponseMetadata': {'HTTPStatusCode': 200, 'RequestId': '0f66c219-1c42-11e5-8a31-47a93a8a98eb'}, + # 'clusters': []} + + def list_services(self, cluster): + fn_args = dict() + if cluster and cluster is not None: + fn_args['cluster'] = cluster + response = self.ecs.list_services(**fn_args) + relevant_response = dict(services = response['serviceArns']) + return relevant_response + + def describe_services(self, cluster, services): + fn_args = dict() + if cluster and cluster is not None: + fn_args['cluster'] = cluster + fn_args['services']=services.split(",") + response = self.ecs.describe_services(**fn_args) + relevant_response = dict(services = response['services']) + if 'failures' in response and len(response['failures'])>0: + relevant_response['services_not_running'] = response['failures'] + return relevant_response + +def main(): + + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + details=dict(required=False, choices=['true', 'false'] ), + cluster=dict(required=False, type='str' ), + service=dict(required=False, type='str' ) + )) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + if not HAS_BOTO: + module.fail_json(msg='boto is required.') + + if not HAS_BOTO3: + module.fail_json(msg='boto3 is required.') + + show_details = False + if 'details' in module.params and module.params['details'] == 'true': + show_details = True + + task_mgr = EcsServiceManager(module) + if show_details: + if 'service' not in module.params or not module.params['service']: + module.fail_json(msg="service must be specified for ecs_service_facts") + ecs_facts = task_mgr.describe_services(module.params['cluster'], module.params['service']) + # the bad news is the result has datetime fields that aren't JSON serializable + # nuk'em! + for service in ecs_facts['services']: + del service['deployments'] + del service['events'] + else: + ecs_facts = task_mgr.list_services(module.params['cluster']) + ecs_facts_result = dict(changed=False, ansible_facts=ecs_facts) + module.exit_json(**ecs_facts_result) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * + +if __name__ == '__main__': + main() From a5083a4a17599787f897e3b99185dc2453344e87 Mon Sep 17 00:00:00 2001 From: Mark Chance Date: Wed, 9 Sep 2015 17:07:04 -0600 Subject: [PATCH 0715/2522] cloud amazon ECS task modules --- cloud/amazon/ecs_task.py | 268 +++++++++++++++++++++++++++++++++ cloud/amazon/ecs_task_facts.py | 204 +++++++++++++++++++++++++ 2 files changed, 472 insertions(+) create mode 100644 cloud/amazon/ecs_task.py create mode 100644 cloud/amazon/ecs_task_facts.py diff --git a/cloud/amazon/ecs_task.py b/cloud/amazon/ecs_task.py new file mode 100644 index 00000000000..ce9fa2e85a8 --- /dev/null +++ b/cloud/amazon/ecs_task.py @@ -0,0 +1,268 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ecs_task +short_description: run, start or stop a task in ecs +description: + - Creates or deletes instances of task definitions. +version_added: "2.0" +options: + operation: + description: + - Which task operation to execute + required: True + choices: ['run', 'start', 'stop'] + cluster: + description: + - The name of the cluster to run the task on + required: False + task_definition: + description: + - The task definition to start or run + required: False + overrides: + description: + - A dictionary of values to pass to the new instances + required: False + count: + description: + - How many new instances to start + required: False + task: + description: + - The task to stop + required: False + container_instances: + description: + - The list of container instances on which to deploy the task + required: False + started_by: + description: + - A value showing who or what started the task (for informational purposes) + required: False +''' + +EXAMPLES = ''' +# Simple example of run task +- name: Run task + ecs_task: + operation: run + cluster: console-sample-app-static-cluster + task_definition: console-sample-app-static-taskdef + count: 1 + started_by: ansible_user + register: task_output + +# Simple example of start task + +- name: Start a task + ecs_task: + operation: start + cluster: console-sample-app-static-cluster + task_definition: console-sample-app-static-taskdef + task: "arn:aws:ecs:us-west-2:172139249013:task/3f8353d1-29a8-4689-bbf6-ad79937ffe8a" + container_instances: + - arn:aws:ecs:us-west-2:172139249013:container-instance/79c23f22-876c-438a-bddf-55c98a3538a8 + started_by: ansible_user + register: task_output + +- name: Stop a task + ecs_task: + operation: stop + cluster: console-sample-app-static-cluster + task_definition: console-sample-app-static-taskdef + task: "arn:aws:ecs:us-west-2:172139249013:task/3f8353d1-29a8-4689-bbf6-ad79937ffe8a" +''' +RETURN = ''' +task: + description: details about the tast that was started + type: array of dict + sample: [ clusterArn, containerInstanceArn, containers[], desiredStatus, lastStatus + overrides, startedBy, taskArn, taskDefinitionArn ] +''' +try: + import json + import boto + import botocore + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +try: + import boto3 + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + +class EcsExecManager: + """Handles ECS Tasks""" + + def __init__(self, module): + self.module = module + + try: + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + if not region: + module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") + self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) + except boto.exception.NoAuthHandlerFound, e: + self.module.fail_json(msg=str(e)) + + def list_tasks(self, cluster_name, service_name, status): + response = self.ecs.list_tasks( + cluster=cluster_name, + family=service_name, + desiredStatus=status + ) + if len(response['taskArns'])>0: + for c in response['taskArns']: + if c.endswith(service_name): + return c + return None + + def run_task(self, cluster, task_definition, overrides, count, startedBy): + if overrides is None: + overrides = dict() + response = self.ecs.run_task( + cluster=cluster, + taskDefinition=task_definition, + overrides=overrides, + count=count, + startedBy=startedBy) + # include tasks and failures + return response['tasks'] + + def start_task(self, cluster, task_definition, overrides, container_instances, startedBy): + args = dict() + if cluster: + args['cluster'] = cluster + if task_definition: + args['taskDefinition']=task_definition + if overrides: + args['overrides']=overrides + if container_instances: + args['containerInstances']=container_instances + if startedBy: + args['startedBy']=startedBy + response = self.ecs.start_task(**args) + # include tasks and failures + return response['tasks'] + + def stop_task(self, cluster, task): + response = self.ecs.stop_task(cluster=cluster, task=task) + return response['task'] + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + operation=dict(required=True, choices=['run', 'start', 'stop'] ), + cluster=dict(required=False, type='str' ), # R S P + task_definition=dict(required=False, type='str' ), # R* S* + overrides=dict(required=False, type='dict'), # R S + count=dict(required=False, type='int' ), # R + task=dict(required=False, type='str' ), # P* + container_instances=dict(required=False, type='list'), # S* + started_by=dict(required=False, type='str' ) # R S + )) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + # Validate Requirements + if not HAS_BOTO: + module.fail_json(msg='boto is required.') + + if not HAS_BOTO3: + module.fail_json(msg='boto3 is required.') + + # Validate Inputs + if module.params['operation'] == 'run': + if not 'task_definition' in module.params and module.params['task_definition'] is None: + module.fail_json(msg="To run a task, a task_definition must be specified") + task_to_list = module.params['task_definition'] + status_type = "RUNNING" + + if module.params['operation'] == 'start': + if not 'task_definition' in module.params and module.params['task_definition'] is None: + module.fail_json(msg="To start a task, a task_definition must be specified") + if not 'container_instances' in module.params and module.params['container_instances'] is None: + module.fail_json(msg="To start a task, container instances must be specified") + task_to_list = module.params['task'] + status_type = "RUNNING" + + if module.params['operation'] == 'stop': + if not 'task' in module.params and module.params['task'] is None: + module.fail_json(msg="To stop a task, a task must be specified") + if not 'task_definition' in module.params and module.params['task_definition'] is None: + module.fail_json(msg="To stop a task, a task definition must be specified") + task_to_list = module.params['task_definition'] + status_type = "STOPPED" + + service_mgr = EcsExecManager(module) + existing = service_mgr.list_tasks(module.params['cluster'], task_to_list, status_type) + + results = dict(changed=False) + if module.params['operation'] == 'run': + if existing: + # TBD - validate the rest of the details + results['task']=existing + else: + if not module.check_mode: + results['task'] = service_mgr.run_task( + module.params['cluster'], + module.params['task_definition'], + module.params['overrides'], + module.params['count'], + module.params['started_by']) + results['changed'] = True + + elif module.params['operation'] == 'start': + if existing: + # TBD - validate the rest of the details + results['task']=existing + else: + if not module.check_mode: + results['task'] = service_mgr.start_task( + module.params['cluster'], + module.params['task_definition'], + module.params['overrides'], + module.params['container_instances'], + module.params['started_by'] + ) + results['changed'] = True + + elif module.params['operation'] == 'stop': + if existing: + results['task']=existing + else: + if not module.check_mode: + # it exists, so we should delete it and mark changed. + # return info about the cluster deleted + results['task'] = service_mgr.stop_task( + module.params['cluster'], + module.params['task'] + ) + results['changed'] = True + + module.exit_json(**results) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() diff --git a/cloud/amazon/ecs_task_facts.py b/cloud/amazon/ecs_task_facts.py new file mode 100644 index 00000000000..541bfaee3c1 --- /dev/null +++ b/cloud/amazon/ecs_task_facts.py @@ -0,0 +1,204 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ecs_task_facts +short_description: return facts about tasks in ecs +description: + - Describes or lists tasks. +version_added: 1.9 +options: + details: + description: + - Set this to true if you want detailed information about the tasks. + required: false + default: false + type: bool + cluster: + description: + - The cluster in which to list tasks if other than the 'default'. + required: false + default: 'default' + type: str + task_list: + description: + - Set this to a list of task identifiers. If 'details' is false, this is required. + required: false + family: + required: False + type: str + + container_instance: + required: False + type: 'str' + max_results: + required: False + type: 'int' + started_by: + required: False + type: 'str' + service_name: + required: False + type: 'str' + desired_status: + required: False + choices=['RUNNING', 'PENDING', 'STOPPED'] + +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Basic listing example +- ecs_task: + cluster=test-cluster + task_list=123456789012345678901234567890123456 + +# Basic example of deregistering task +- ecs_task: + state: absent + family: console-test-tdn + revision: 1 +''' +RETURN = ''' +cache_updated: + description: if the cache was updated or not + returned: success, in some cases + type: boolean + sample: True +cache_update_time: + description: time of the last cache update (0 if unknown) + returned: success, in some cases + type: datetime + sample: 1425828348000 +stdout: + description: output from apt + returned: success, when needed + type: string + sample: "Reading package lists...\nBuilding dependency tree...\nReading state information...\nThe following extra packages will be installed:\n apache2-bin ..." +stderr: + description: error output from apt + returned: success, when needed + type: string + sample: "AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 127.0.1.1. Set the 'ServerName' directive globally to ..." +''' +try: + import json, os + import boto + import botocore + # import module snippets + from ansible.module_utils.basic import * + from ansible.module_utils.ec2 import * + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +try: + import boto3 + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + +class EcsTaskManager: + """Handles ECS Tasks""" + + def __init__(self, module): + self.module = module + + try: + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + if not region: + module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") + self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) + except boto.exception.NoAuthHandlerFound, e: + self.module.fail_json(msg=str(e)) + + def transmogrify(self, params, field, dictionary, arg_name): + if field in params and params[field] is not None: + dictionary[arg_name] = params[field] + + def list_tasks(self, params): + fn_args = dict() + self.transmogrify(params, 'cluster', fn_args, 'cluster') + self.transmogrify(params, 'container_instance', fn_args, 'containerInstance') + self.transmogrify(params, 'family', fn_args, 'family') + self.transmogrify(params, 'max_results', fn_args, 'maxResults') + self.transmogrify(params, 'started_by', fn_args, 'startedBy') + self.transmogrify(params, 'service_name', fn_args, 'startedBy') + self.transmogrify(params, 'desired_status', fn_args, 'desiredStatus') + relevant_response = dict() + try: + response = self.ecs.list_tasks(**fn_args) + relevant_response['tasks'] = response['taskArns'] + except botocore.exceptions.ClientError: + relevant_response['tasks'] = [] + return relevant_response + + def describe_tasks(self, cluster_name, tasks): + response = self.ecs.describe_tasks( + cluster=cluster_name if cluster_name else '', + tasks=tasks.split(",") if tasks else [] + ) + relevant_response = dict( + tasks = response['tasks'], + tasks_not_running = response['failures']) + return relevant_response + +def main(): + + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + details=dict(required=False, type='bool' ), + cluster=dict(required=False, type='str' ), + task_list = dict(required=False, type='str'), + family=dict(required= False, type='str' ), + container_instance=dict(required=False, type='str' ), + max_results=dict(required=False, type='int' ), + started_by=dict(required=False, type='str' ), + service_name=dict(required=False, type='str' ), + desired_status=dict(required=False, choices=['RUNNING', 'PENDING', 'STOPPED']) + )) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + if not HAS_BOTO: + module.fail_json(msg='boto is required.') + + if not HAS_BOTO3: + module.fail_json(msg='boto3 is required.') + + task_to_describe = module.params['family'] + show_details = False + if 'details' in module.params and module.params['details']: + show_details = True + + task_mgr = EcsTaskManager(module) + if show_details: + if 'task_list' not in module.params or not module.params['task_list']: + module.fail_json(msg="task_list must be specified for ecs_task_facts") + ecs_facts = task_mgr.describe_tasks(module.params['cluster'], module.params['task_list']) + else: + ecs_facts = task_mgr.list_tasks(module.params) + ecs_facts_result = dict(changed=False, ansible_facts=ecs_facts) + module.exit_json(**ecs_facts_result) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * + +if __name__ == '__main__': + main() From f802fc2ce31b66a2c0b59519cbf531343f19fcf3 Mon Sep 17 00:00:00 2001 From: Mark Chance Date: Wed, 9 Sep 2015 17:08:48 -0600 Subject: [PATCH 0716/2522] cloud amazon ECS cluster module --- cloud/amazon/ecs_cluster.py | 240 ++++++++++++++++++++++++++++++ cloud/amazon/ecs_cluster_facts.py | 173 +++++++++++++++++++++ 2 files changed, 413 insertions(+) create mode 100644 cloud/amazon/ecs_cluster.py create mode 100644 cloud/amazon/ecs_cluster_facts.py diff --git a/cloud/amazon/ecs_cluster.py b/cloud/amazon/ecs_cluster.py new file mode 100644 index 00000000000..f5bd1e42bc1 --- /dev/null +++ b/cloud/amazon/ecs_cluster.py @@ -0,0 +1,240 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ecs_cluster +short_description: create or terminate ecs clusters +notes: + - When deleting a cluster, the information returned is the state of the cluster prior to deletion. + - It will also wait for a cluster to have instances registered to it. +description: + - Creates or terminates ecs clusters. +version_added: "1.9" +requirements: [ json, time, boto, boto3 ] +options: + state=dict(required=True, choices=['present', 'absent', 'has_instances'] ), + name=dict(required=True, type='str' ), + delay=dict(required=False, type='int', default=10), + repeat=dict(required=False, type='int', default=10) + + state: + description: + - The desired state of the cluster + required: true + choices: ['present', 'absent', 'has_instances'] + name: + description: + - The cluster name + required: true + delay: + description: + - Number of seconds to wait + required: true + name: + description: + - The cluster name + required: true +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Cluster creation +- ecs_cluster: + name: default + state: present + +# Cluster deletion +- ecs_cluster: + name: default + state: absent + +- name: Wait for register + ecs_cluster: + name: "{{ new_cluster }}" + state: has_instances + delay: 10 + repeat: 10 + register: task_output + +''' +RETURN = ''' +activeServicesCount: + description: how many services are active in this cluster + returned: 0 if a new cluster + type: int +clusterArn: + description: the ARN of the cluster just created + type: string (ARN) + sample: arn:aws:ecs:us-west-2:172139249013:cluster/test-cluster-mfshcdok +clusterName: + description: name of the cluster just created (should match the input argument) + type: string + sample: test-cluster-mfshcdok +pendingTasksCount: + description: how many tasks are waiting to run in this cluster + returned: 0 if a new cluster + type: int +registeredContainerInstancesCount: + description: how many container instances are available in this cluster + returned: 0 if a new cluster + type: int +runningTasksCount: + description: how many tasks are running in this cluster + returned: 0 if a new cluster + type: int +status: + description: the status of the new cluster + returned: ACTIVE + type: string +''' +try: + import json, time + import boto + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +try: + import boto3 + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + +class EcsClusterManager: + """Handles ECS Clusters""" + + def __init__(self, module): + self.module = module + + try: + # self.ecs = boto3.client('ecs') + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + if not region: + module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") + self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) + except boto.exception.NoAuthHandlerFound, e: + self.module.fail_json(msg=str(e)) + + def find_in_array(self, array_of_clusters, cluster_name, field_name='clusterArn'): + for c in array_of_clusters: + if c[field_name].endswith(cluster_name): + return c + return None + + def describe_cluster(self, cluster_name): + response = self.ecs.describe_clusters(clusters=[ + cluster_name + ]) + if len(response['failures'])>0: + c = self.find_in_array(response['failures'], cluster_name, 'arn') + if c and c['reason']=='MISSING': + return None + # fall thru and look through found ones + if len(response['clusters'])>0: + c = self.find_in_array(response['clusters'], cluster_name) + if c: + return c + raise StandardError("Unknown problem describing cluster %s." % cluster_name) + + def create_cluster(self, clusterName = 'default'): + response = self.ecs.create_cluster(clusterName=clusterName) + return response['cluster'] + + def delete_cluster(self, clusterName): + return self.ecs.delete_cluster(cluster=clusterName) + +def main(): + + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + state=dict(required=True, choices=['present', 'absent', 'has_instances'] ), + name=dict(required=True, type='str' ), + delay=dict(required=False, type='int', default=10), + repeat=dict(required=False, type='int', default=10) + )) + required_together = ( ['state', 'name'] ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True, required_together=required_together) + + if not HAS_BOTO: + module.fail_json(msg='boto is required.') + + if not HAS_BOTO3: + module.fail_json(msg='boto3 is required.') + + cluster_name = module.params['name'] + + cluster_mgr = EcsClusterManager(module) + try: + existing = cluster_mgr.describe_cluster(module.params['name']) + except Exception, e: + module.fail_json(msg=str(e)) + + results = dict(changed=False) + if module.params['state'] == 'present': + if existing and 'status' in existing and existing['status']=="ACTIVE": + results['cluster']=existing + else: + if not module.check_mode: + # doesn't exist. create it. + results['cluster'] = cluster_mgr.create_cluster(module.params['name']) + results['changed'] = True + + # delete the cluster + elif module.params['state'] == 'absent': + if not existing: + pass + else: + # it exists, so we should delete it and mark changed. + # return info about the cluster deleted + results['cluster'] = existing + if 'status' in existing and existing['status']=="INACTIVE": + results['changed'] = False + else: + if not module.check_mode: + cluster_mgr.delete_cluster(module.params['name']) + results['changed'] = True + elif module.params['state'] == 'has_instances': + if not existing: + module.fail_json(msg="Cluster '"+module.params['name']+" not found.") + return + # it exists, so we should delete it and mark changed. + # return info about the cluster deleted + delay = module.params['delay'] + repeat = module.params['repeat'] + time.sleep(delay) + count = 0 + for i in range(repeat): + existing = cluster_mgr.describe_cluster(module.params['name']) + count = existing['registeredContainerInstancesCount'] + if count > 0: + results['changed'] = True + break + time.sleep(delay) + if count == 0 and i is repeat-1: + module.fail_json(msg="Cluster instance count still zero after "+str(repeat)+" tries of "+str(delay)+" seconds each.") + return + + module.exit_json(**results) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() diff --git a/cloud/amazon/ecs_cluster_facts.py b/cloud/amazon/ecs_cluster_facts.py new file mode 100644 index 00000000000..4dac2f42daa --- /dev/null +++ b/cloud/amazon/ecs_cluster_facts.py @@ -0,0 +1,173 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ecs_cluster_facts +short_description: list or describe clusters or their instances in ecs +description: + - Lists or describes clusters or cluster instances in ecs. +version_added: 1.9 +options: + details: + description: + - Set this to true if you want detailed information. + required: false + default: false + cluster: + description: + - The cluster ARNS to list. + required: false + default: 'default' + instances: + description: + - The instance ARNS to list. + required: false + default: None (returns all) + option: + description: + - Whether to return information about clusters or their instances + required: false + choices: ['clusters', 'instances'] + default: 'clusters' +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Basic listing example +- ecs_task: + cluster=test-cluster + task_list=123456789012345678901234567890123456 + +# Basic example of deregistering task +- ecs_task: + state: absent + family: console-test-tdn + revision: 1 +''' +RETURN = ''' +clusters: + description: + - array of cluster ARNs when details is false + - array of dicts when details is true + sample: [ "arn:aws:ecs:us-west-2:172139249013:cluster/test-cluster" ] +''' +try: + import json, os + import boto + import botocore + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +try: + import boto3 + # import module snippets + from ansible.module_utils.ec2 import ec2_argument_spec, get_aws_connection_info, boto3_conn + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + +class EcsClusterManager: + """Handles ECS Clusters""" + + def __init__(self, module): + self.module = module + + try: + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + if not region: + module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") + self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) + except boto.exception.NoAuthHandlerFound, e: + self.module.fail_json(msg=str(e)) + + def list_container_instances(self, cluster): + response = self.ecs.list_container_instances(cluster=cluster) + relevant_response = dict(instances = response['containerInstanceArns']) + return relevant_response + + def describe_container_instances(self, cluster, instances): + response = self.ecs.describe_container_instances( + clusters=cluster, + containerInstances=instances.split(",") if instances else [] + ) + relevant_response = dict() + if 'containerInstances' in response and len(response['containerInstances'])>0: + relevant_response['instances'] = response['containerInstances'] + if 'failures' in response and len(response['failures'])>0: + relevant_response['instances_not_running'] = response['failures'] + return relevant_response + + def list_clusters(self): + response = self.ecs.list_clusters() + relevant_response = dict(clusters = response['clusterArns']) + return relevant_response + + def describe_clusters(self, cluster): + response = self.ecs.describe_clusters( + clusters=cluster.split(",") if cluster else [] + ) + relevant_response = dict() + if 'clusters' in response and len(response['clusters'])>0: + relevant_response['clusters'] = response['clusters'] + if 'failures' in response and len(response['failures'])>0: + relevant_response['clusters_not_running'] = response['failures'] + return relevant_response + +def main(): + + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + details=dict(required=False, type='bool' ), + cluster=dict(required=False, type='str' ), + instances=dict(required=False, type='str' ), + option=dict(required=False, choices=['clusters', 'instances'], default='clusters') + )) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + if not HAS_BOTO: + module.fail_json(msg='boto is required.') + + if not HAS_BOTO3: + module.fail_json(msg='boto3 is required.') + + show_details = False + if 'details' in module.params and module.params['details']: + show_details = True + + task_mgr = EcsClusterManager(module) + if module.params['option']=='clusters': + if show_details: + ecs_facts = task_mgr.describe_clusters(module.params['cluster']) + else: + ecs_facts = task_mgr.list_clusters() + if module.params['option']=='instances': + if show_details: + ecs_facts = task_mgr.describe_container_instances(module.params['cluster'], module.params['instances']) + else: + ecs_facts = task_mgr.list_container_instances(module.params['cluster']) + ecs_facts_result = dict(changed=False, ansible_facts=ecs_facts) + module.exit_json(**ecs_facts_result) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * + +if __name__ == '__main__': + main() From 03cd38e7abe057269e7b8c15e0fdedbe18e0b3f4 Mon Sep 17 00:00:00 2001 From: Mark Chance Date: Wed, 9 Sep 2015 17:09:46 -0600 Subject: [PATCH 0717/2522] cloud amazon ECS task definition module --- cloud/amazon/ecs_taskdefinition.py | 215 +++++++++++++++++++++++ cloud/amazon/ecs_taskdefinition_facts.py | 173 ++++++++++++++++++ 2 files changed, 388 insertions(+) create mode 100644 cloud/amazon/ecs_taskdefinition.py create mode 100644 cloud/amazon/ecs_taskdefinition_facts.py diff --git a/cloud/amazon/ecs_taskdefinition.py b/cloud/amazon/ecs_taskdefinition.py new file mode 100644 index 00000000000..9915e9d8070 --- /dev/null +++ b/cloud/amazon/ecs_taskdefinition.py @@ -0,0 +1,215 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ecs_taskdefinition +short_description: register a task definition in ecs +description: + - Creates or terminates task definitions +version_added: "1.9" +requirements: [ json, boto, botocore, boto3 ] +options: + state: + description: + - State whether the task definition should exist or be deleted + required: true + choices=['present', 'absent'] + + arn: + description: + - The arn of the task description to delete + required: false + + family: + =dict(required=False, type='str' ), + + revision: + required: False + type: int + + containers: + required: False + type: list of dicts with container definitions + + volumes: + required: False + type: list of name +''' + +EXAMPLES = ''' +- name: "Create task definition" + ecs_taskdefinition: + containers: + - name: simple-app + cpu: 10 + essential: true + image: "httpd:2.4" + memory: 300 + mountPoints: + - containerPath: /usr/local/apache2/htdocs + sourceVolume: my-vol + portMappings: + - containerPort: 80 + hostPort: 80 + - name: busybox + command: + - "/bin/sh -c \"while true; do echo ' Amazon ECS Sample App

Amazon ECS Sample App

Congratulations!

Your application is now running on a container in Amazon ECS.

' > top; /bin/date > date ; echo '
' > bottom; cat top date bottom > /usr/local/apache2/htdocs/index.html ; sleep 1; done\"" + cpu: 10 + entryPoint: + - sh + - "-c" + essential: false + image: busybox + memory: 200 + volumesFrom: + - sourceContainer: simple-app + volumes: + - name: my-vol + family: test-cluster-taskdef + state: present + register: task_output +''' +RETURN = ''' +taskdefinition: + description: a reflection of the input parameters + type: dict inputs plus revision, status, taskDefinitionArn +''' +try: + import json + import boto + import botocore + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +try: + import boto3 + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + +class EcsTaskManager: + """Handles ECS Tasks""" + + def __init__(self, module): + self.module = module + + try: + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + if not region: + module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") + self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) + except boto.exception.NoAuthHandlerFound, e: + self.module.fail_json(msg=str(e)) + + def describe_task(self, task_name): + try: + response = self.ecs.describe_task_definition(taskDefinition=task_name) + return response['taskDefinition'] + except botocore.exceptions.ClientError: + return None + + def register_task(self, family, container_definitions, volumes): + response = self.ecs.register_task_definition(family=family, + containerDefinitions=container_definitions, volumes=volumes) + return response['taskDefinition'] + + def deregister_task(self, taskArn): + response = self.ecs.deregister_task_definition(taskDefinition=taskArn) + return response['taskDefinition'] + +def main(): + + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + state=dict(required=True, choices=['present', 'absent'] ), + arn=dict(required=False, type='str' ), + family=dict(required=False, type='str' ), + revision=dict(required=False, type='int' ), + containers=dict(required=False, type='list' ), + volumes=dict(required=False, type='list' ) + )) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + if not HAS_BOTO: + module.fail_json(msg='boto is required.') + + if not HAS_BOTO3: + module.fail_json(msg='boto3 is required.') + + task_to_describe = None + # When deregistering a task, we can specify the ARN OR + # the family and revision. + if module.params['state'] == 'absent': + if 'arn' in module.params and module.params['arn'] is not None: + task_to_describe = module.params['arn'] + elif 'family' in module.params and module.params['family'] is not None and 'revision' in module.params and module.params['revision'] is not None: + task_to_describe = module.params['family']+":"+str(module.params['revision']) + else: + module.fail_json(msg="To use task definitions, an arn or family and revision must be specified") + # When registering a task, we can specify the ARN OR + # the family and revision. + if module.params['state'] == 'present': + if not 'family' in module.params: + module.fail_json(msg="To use task definitions, a family must be specified") + if not 'containers' in module.params: + module.fail_json(msg="To use task definitions, a list of containers must be specified") + task_to_describe = module.params['family'] + + task_mgr = EcsTaskManager(module) + existing = task_mgr.describe_task(task_to_describe) + + results = dict(changed=False) + if module.params['state'] == 'present': + if existing and 'status' in existing and existing['status']=="ACTIVE": + results['taskdefinition']=existing + else: + if not module.check_mode: + # doesn't exist. create it. + volumes = [] + if 'volumes' in module.params: + volumes = module.params['volumes'] + if volumes is None: + volumes = [] + results['taskdefinition'] = task_mgr.register_task(module.params['family'], + module.params['containers'], volumes) + results['changed'] = True + + # delete the cloudtrai + elif module.params['state'] == 'absent': + if not existing: + pass + else: + # it exists, so we should delete it and mark changed. + # return info about the cluster deleted + results['taskdefinition'] = existing + if 'status' in existing and existing['status']=="INACTIVE": + results['changed'] = False + else: + if not module.check_mode: + task_mgr.deregister_task(task_to_describe) + results['changed'] = True + + module.exit_json(**results) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() diff --git a/cloud/amazon/ecs_taskdefinition_facts.py b/cloud/amazon/ecs_taskdefinition_facts.py new file mode 100644 index 00000000000..c351639513f --- /dev/null +++ b/cloud/amazon/ecs_taskdefinition_facts.py @@ -0,0 +1,173 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ecs_taskdefinition_facts +short_description: return facts about task definitions in ecs +description: + - Describes or lists task definitions. +version_added: 1.9 +requirements: [ json, os, boto, botocore, boto3 ] +options: + details: + description: + - Set this to true if you want detailed information about the tasks. + required: false + default: false + name: + description: + - When details is true, the name must be provided. + required: false + family: + description: + - the name of the family of task definitions to list. + required: false + max_results: + description: + - The maximum number of results to return. + required: false + status: + description: + - Show only task descriptions of the given status. If omitted, it shows all + required: false + choices: ['ACTIVE', 'INACTIVE'] + sort: + description: + - Sort order of returned list of task definitions + required: false + choices: ['ASC', 'DESC'] + +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Basic listing example +- name: "Get task definitions with details" + ecs_taskdefinition_facts: + name: test-cluster-tasks + details: true + +- name: Get task definitions with details + ecs_taskdefinition_facts: + status: INACTIVE + details: true + family: test-cluster-rbjgjoaj-task + name: "arn:aws:ecs:us-west-2:172139249013:task-definition/test-cluster-rbjgjoaj-task:1" +''' +RETURN = ''' +task_definitions: + description: array of ARN values for the known task definitions + type: array of string or dict if details is true + sample: ["arn:aws:ecs:us-west-2:172139249013:task-definition/console-sample-app-static:1"] +''' +try: + import json, os + import boto + import botocore + # import module snippets + from ansible.module_utils.basic import * + from ansible.module_utils.ec2 import * + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +try: + import boto3 + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + +class EcsTaskManager: + """Handles ECS Tasks""" + + def __init__(self, module): + self.module = module + + try: + # self.ecs = boto3.client('ecs') + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + if not region: + module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") + self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) + except boto.exception.NoAuthHandlerFound, e: + self.module.fail_json(msg=str(e)) + + def transmogrify(self, params, field, dictionary, arg_name): + if field in params and params[field] is not None: + dictionary[arg_name] = params[field] + + def list_taskdefinitions(self, params): + fn_args = dict() + self.transmogrify(params, 'family', fn_args, 'familyPrefix') + self.transmogrify(params, 'max_results', fn_args, 'maxResults') + self.transmogrify(params, 'status', fn_args, 'status') + self.transmogrify(params, 'sort', fn_args, 'sort') + response = self.ecs.list_task_definitions(**fn_args) + return dict(task_definitions=response['taskDefinitionArns']) + + def describe_taskdefinition(self, task_definition): + try: + response = self.ecs.describe_task_definition(taskDefinition=task_definition) + except botocore.exceptions.ClientError: + response = dict(taskDefinition=[ dict( name=task_definition, status="MISSING")]) + relevant_response = dict( + task_definitions = response['taskDefinition'] + ) + return relevant_response + +def main(): + + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + details=dict(required= False, type='bool' ), + name=dict(required=False, type='str' ), + family=dict(required=False, type='str' ), + max_results=dict(required=False, type='int' ), + status=dict(required=False, choices=['ACTIVE', 'INACTIVE']), + sort=dict(required=False, choices=['ASC', 'DESC']) + )) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + if not HAS_BOTO: + module.fail_json(msg='boto is required.') + + if not HAS_BOTO3: + module.fail_json(msg='boto3 is required.') + + show_details = False + if 'details' in module.params and module.params['details']: + if 'name' not in module.params or not module.params['name']: + module.fail_json(msg="task definition name must be specified for ecs_taskdefinition_facts") + show_details = True + + task_mgr = EcsTaskManager(module) + if show_details: + ecs_facts = task_mgr.describe_taskdefinition(module.params['name']) + else: + ecs_facts = task_mgr.list_taskdefinitions(module.params) + ecs_facts_result = dict(changed=False, ansible_facts=ecs_facts) + module.exit_json(**ecs_facts_result) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * +from ansible.module_utils.urls import * + +if __name__ == '__main__': + main() From de95580f665a628ac4fff2af95d043413aac6334 Mon Sep 17 00:00:00 2001 From: Mark Chance Date: Thu, 10 Sep 2015 08:02:24 -0600 Subject: [PATCH 0718/2522] fix docs, enhance fail msgs --- cloud/amazon/ecs_cluster.py | 19 +++++++------------ cloud/amazon/ecs_cluster_facts.py | 4 ++-- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/cloud/amazon/ecs_cluster.py b/cloud/amazon/ecs_cluster.py index f5bd1e42bc1..9dc49860384 100644 --- a/cloud/amazon/ecs_cluster.py +++ b/cloud/amazon/ecs_cluster.py @@ -23,14 +23,9 @@ - It will also wait for a cluster to have instances registered to it. description: - Creates or terminates ecs clusters. -version_added: "1.9" +version_added: "2.0" requirements: [ json, time, boto, boto3 ] options: - state=dict(required=True, choices=['present', 'absent', 'has_instances'] ), - name=dict(required=True, type='str' ), - delay=dict(required=False, type='int', default=10), - repeat=dict(required=False, type='int', default=10) - state: description: - The desired state of the cluster @@ -43,11 +38,11 @@ delay: description: - Number of seconds to wait - required: true - name: + required: false + repeat: description: - - The cluster name - required: true + - The number of times to wait for the cluster to have an instance + required: false ''' EXAMPLES = ''' @@ -128,7 +123,7 @@ def __init__(self, module): module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) except boto.exception.NoAuthHandlerFound, e: - self.module.fail_json(msg=str(e)) + self.module.fail_json(msg="Can't authorize connection - "+str(e)) def find_in_array(self, array_of_clusters, cluster_name, field_name='clusterArn'): for c in array_of_clusters: @@ -183,7 +178,7 @@ def main(): try: existing = cluster_mgr.describe_cluster(module.params['name']) except Exception, e: - module.fail_json(msg=str(e)) + module.fail_json(msg="Exception describing cluster '"+module.params['name']+"': "+str(e)) results = dict(changed=False) if module.params['state'] == 'present': diff --git a/cloud/amazon/ecs_cluster_facts.py b/cloud/amazon/ecs_cluster_facts.py index 4dac2f42daa..ec1a9209ef7 100644 --- a/cloud/amazon/ecs_cluster_facts.py +++ b/cloud/amazon/ecs_cluster_facts.py @@ -20,7 +20,7 @@ short_description: list or describe clusters or their instances in ecs description: - Lists or describes clusters or cluster instances in ecs. -version_added: 1.9 +version_added: "2.0" options: details: description: @@ -94,7 +94,7 @@ def __init__(self, module): module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) except boto.exception.NoAuthHandlerFound, e: - self.module.fail_json(msg=str(e)) + self.module.fail_json(msg="Can't authorize connection - "+str(e)) def list_container_instances(self, cluster): response = self.ecs.list_container_instances(cluster=cluster) From 4524cc1e27b7fffc65e86003ef47320f1cfd2677 Mon Sep 17 00:00:00 2001 From: Mark Chance Date: Thu, 10 Sep 2015 08:22:40 -0600 Subject: [PATCH 0719/2522] update version, fix fail msgs --- cloud/amazon/ecs_service.py | 6 +++--- cloud/amazon/ecs_service_facts.py | 6 ++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/cloud/amazon/ecs_service.py b/cloud/amazon/ecs_service.py index a946fbaf287..cd2b046e1c1 100644 --- a/cloud/amazon/ecs_service.py +++ b/cloud/amazon/ecs_service.py @@ -25,7 +25,7 @@ for the ecs service, ecs.amazonaws.com) dependecies: - An IAM role must have been created -version_added: "0.9" +version_added: "2.0" options: ''' @@ -92,7 +92,7 @@ def __init__(self, module): module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) except boto.exception.NoAuthHandlerFound, e: - self.module.fail_json(msg=str(e)) + self.module.fail_json(msg="Can't authorize connection - "+str(e)) # def list_clusters(self): # return self.client.list_clusters() @@ -190,7 +190,7 @@ def main(): try: existing = service_mgr.describe_service(module.params['cluster'], module.params['name']) except Exception, e: - module.fail_json(msg=str(e)) + module.fail_json(msg="Exception describing service '"+module.params['name']+"' in cluster '"+module.params['cluster']+"': "+str(e)) results = dict(changed=False ) if module.params['state'] == 'present': diff --git a/cloud/amazon/ecs_service_facts.py b/cloud/amazon/ecs_service_facts.py index 08c21ee3213..87d568de800 100644 --- a/cloud/amazon/ecs_service_facts.py +++ b/cloud/amazon/ecs_service_facts.py @@ -20,7 +20,7 @@ short_description: list or describe services in ecs description: - Lists or describes services in ecs. -version_added: "0.9" +version_added: "2.0" options: details: description: @@ -28,13 +28,11 @@ required: false default: 'false' choices: ['true', 'false'] - version_added: 1.9 cluster: description: - The cluster ARNS in which to list the services. required: false default: 'default' - version_added: 1.9 ''' EXAMPLES = ''' @@ -103,7 +101,7 @@ def __init__(self, module): module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) except boto.exception.NoAuthHandlerFound, e: - self.module.fail_json(msg=str(e)) + self.module.fail_json(msg="Can't authorize connection - "+str(e)) # def list_clusters(self): # return self.client.list_clusters() From 158b9b664d9bf648c1cc12bef2cede554ace2570 Mon Sep 17 00:00:00 2001 From: Mark Chance Date: Thu, 10 Sep 2015 08:25:04 -0600 Subject: [PATCH 0720/2522] update version, fix fail msgs --- cloud/amazon/ecs_task.py | 2 +- cloud/amazon/ecs_task_facts.py | 39 ++++++++-------------------------- 2 files changed, 10 insertions(+), 31 deletions(-) diff --git a/cloud/amazon/ecs_task.py b/cloud/amazon/ecs_task.py index ce9fa2e85a8..32bad410606 100644 --- a/cloud/amazon/ecs_task.py +++ b/cloud/amazon/ecs_task.py @@ -121,7 +121,7 @@ def __init__(self, module): module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) except boto.exception.NoAuthHandlerFound, e: - self.module.fail_json(msg=str(e)) + self.module.fail_json(msg="Can't authorize connection - "+str(e)) def list_tasks(self, cluster_name, service_name, status): response = self.ecs.list_tasks( diff --git a/cloud/amazon/ecs_task_facts.py b/cloud/amazon/ecs_task_facts.py index 541bfaee3c1..d5191a271c0 100644 --- a/cloud/amazon/ecs_task_facts.py +++ b/cloud/amazon/ecs_task_facts.py @@ -20,7 +20,7 @@ short_description: return facts about tasks in ecs description: - Describes or lists tasks. -version_added: 1.9 +version_added: 2.0 options: details: description: @@ -64,37 +64,16 @@ # Note: These examples do not set authentication details, see the AWS Guide for details. # Basic listing example -- ecs_task: +- ecs_task_facts: + cluster=test-cluster + task_list=123456789012345678901234567890123456 + +# Listing tasks with details +- ecs_task_facts: + details: true cluster=test-cluster task_list=123456789012345678901234567890123456 -# Basic example of deregistering task -- ecs_task: - state: absent - family: console-test-tdn - revision: 1 -''' -RETURN = ''' -cache_updated: - description: if the cache was updated or not - returned: success, in some cases - type: boolean - sample: True -cache_update_time: - description: time of the last cache update (0 if unknown) - returned: success, in some cases - type: datetime - sample: 1425828348000 -stdout: - description: output from apt - returned: success, when needed - type: string - sample: "Reading package lists...\nBuilding dependency tree...\nReading state information...\nThe following extra packages will be installed:\n apache2-bin ..." -stderr: - description: error output from apt - returned: success, when needed - type: string - sample: "AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 127.0.1.1. Set the 'ServerName' directive globally to ..." ''' try: import json, os @@ -125,7 +104,7 @@ def __init__(self, module): module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) except boto.exception.NoAuthHandlerFound, e: - self.module.fail_json(msg=str(e)) + self.module.fail_json(msg="Can't authorize connection - "+str(e)) def transmogrify(self, params, field, dictionary, arg_name): if field in params and params[field] is not None: From dca0d4a08cda856437f7c2c2768e443fa1791311 Mon Sep 17 00:00:00 2001 From: Mark Chance Date: Thu, 10 Sep 2015 08:25:34 -0600 Subject: [PATCH 0721/2522] update version, fix fail msgs --- cloud/amazon/ecs_taskdefinition.py | 4 ++-- cloud/amazon/ecs_taskdefinition_facts.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cloud/amazon/ecs_taskdefinition.py b/cloud/amazon/ecs_taskdefinition.py index 9915e9d8070..70bf59f7b6d 100644 --- a/cloud/amazon/ecs_taskdefinition.py +++ b/cloud/amazon/ecs_taskdefinition.py @@ -20,7 +20,7 @@ short_description: register a task definition in ecs description: - Creates or terminates task definitions -version_added: "1.9" +version_added: "2.0" requirements: [ json, boto, botocore, boto3 ] options: state: @@ -114,7 +114,7 @@ def __init__(self, module): module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) except boto.exception.NoAuthHandlerFound, e: - self.module.fail_json(msg=str(e)) + self.module.fail_json(msg="Can't authorize connection - "+str(e)) def describe_task(self, task_name): try: diff --git a/cloud/amazon/ecs_taskdefinition_facts.py b/cloud/amazon/ecs_taskdefinition_facts.py index c351639513f..d99f563aeb6 100644 --- a/cloud/amazon/ecs_taskdefinition_facts.py +++ b/cloud/amazon/ecs_taskdefinition_facts.py @@ -20,7 +20,7 @@ short_description: return facts about task definitions in ecs description: - Describes or lists task definitions. -version_added: 1.9 +version_added: 2.0 requirements: [ json, os, boto, botocore, boto3 ] options: details: @@ -105,7 +105,7 @@ def __init__(self, module): module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) except boto.exception.NoAuthHandlerFound, e: - self.module.fail_json(msg=str(e)) + self.module.fail_json(msg="Can't authorize connection - "+str(e)) def transmogrify(self, params, field, dictionary, arg_name): if field in params and params[field] is not None: From 604578cfccdeb467221a8ef1ed9d7f7dc1fd78cb Mon Sep 17 00:00:00 2001 From: Evan Carter Date: Thu, 10 Sep 2015 15:23:57 -0400 Subject: [PATCH 0722/2522] fixing hang after creation --- cloud/amazon/ec2_eni.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_eni.py b/cloud/amazon/ec2_eni.py index 4d08aeb26d0..59a26291388 100644 --- a/cloud/amazon/ec2_eni.py +++ b/cloud/amazon/ec2_eni.py @@ -215,8 +215,8 @@ def create_eni(connection, module): raise # Wait to allow creation / attachment to finish wait_for_eni(eni, "attached") + eni.update() changed = True - eni.update() except BotoServerError as e: module.fail_json(msg=get_error_message(e.args[2])) From 6bd40787ce6f0e7d4825fe49fe5f9c7d07c1e664 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Fri, 11 Sep 2015 08:52:30 +0200 Subject: [PATCH 0723/2522] cloudstack: fix templates not always have checksums It is not documented but it seems only registered templates have checksums. Templates created from VMs and snapshot don't. This change fixes the traceback. But we must re-thinking, if it still makes sense to look for the checksum. --- cloud/cloudstack/cs_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/cloudstack/cs_template.py b/cloud/cloudstack/cs_template.py index ed5472c2c3e..fbaa5665eb2 100644 --- a/cloud/cloudstack/cs_template.py +++ b/cloud/cloudstack/cs_template.py @@ -501,7 +501,7 @@ def get_template(self): return templates['template'][0] else: for i in templates['template']: - if i['checksum'] == checksum: + if 'checksum' in i and i['checksum'] == checksum: return i return None From 578dbe1d379dc2572b6eedbb469c0bf82b563f66 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Wed, 9 Sep 2015 22:25:52 +0200 Subject: [PATCH 0724/2522] cloudstack: new module cs_user --- cloud/cloudstack/cs_user.py | 465 ++++++++++++++++++++++++++++++++++++ 1 file changed, 465 insertions(+) create mode 100644 cloud/cloudstack/cs_user.py diff --git a/cloud/cloudstack/cs_user.py b/cloud/cloudstack/cs_user.py new file mode 100644 index 00000000000..43e83c06784 --- /dev/null +++ b/cloud/cloudstack/cs_user.py @@ -0,0 +1,465 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2015, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: cs_user +short_description: Manages users on Apache CloudStack based clouds. +description: + - Create, update, disable, lock, enable and remove users. +version_added: '2.0' +author: "René Moser (@resmo)" +options: + username: + description: + - Username of the user. + required: true + account: + description: + - Account the user will be created under. + - Required on C(state=present). + required: false + default: null + password: + description: + - Password of the user to be created. + - Required on C(state=present). + - Only considered on creation and will not be updated if user exists. + required: false + default: null + first_name: + description: + - First name of the user. + - Required on C(state=present). + required: false + default: null + last_name: + description: + - Last name of the user. + - Required on C(state=present). + required: false + default: null + email: + description: + - Email of the user. + - Required on C(state=present). + required: false + default: null + timezone: + description: + - Timezone of the user. + required: false + default: null + domain: + description: + - Domain the user is related to. + required: false + default: 'ROOT' + state: + description: + - State of the user. + - C(unlocked) is an alias for C(enabled). + required: false + default: 'present' + choices: [ 'present', 'absent', 'enabled', 'disabled', 'locked', 'unlocked' ] + poll_async: + description: + - Poll async jobs until job has finished. + required: false + default: true +extends_documentation_fragment: cloudstack +''' + +EXAMPLES = ''' +# create an user in domain 'CUSTOMERS' +local_action: + module: cs_user + account: developers + username: johndoe + password: S3Cur3 + last_name: Doe + first_name: John + email: john.doe@example.com + domain: CUSTOMERS + +# Lock an existing user in domain 'CUSTOMERS' +local_action: + module: cs_user + username: johndoe + domain: CUSTOMERS + state: locked + +# Disable an existing user in domain 'CUSTOMERS' +local_action: + module: cs_user + username: johndoe + domain: CUSTOMERS + state: disabled + +# Enable/unlock an existing user in domain 'CUSTOMERS' +local_action: + module: cs_user + username: johndoe + domain: CUSTOMERS + state: enabled + +# Remove an user in domain 'CUSTOMERS' +local_action: + module: cs_user + name: customer_xy + domain: CUSTOMERS + state: absent +''' + +RETURN = ''' +--- +id: + description: UUID of the user. + returned: success + type: string + sample: 87b1e0ce-4e01-11e4-bb66-0050569e64b8 +username: + description: Username of the user. + returned: success + type: string + sample: johndoe +fist_name: + description: First name of the user. + returned: success + type: string + sample: John +last_name: + description: Last name of the user. + returned: success + type: string + sample: Doe +email: + description: Emailof the user. + returned: success + type: string + sample: john.doe@example.com +api_key: + description: API key of the user. + returned: success + type: string + sample: JLhcg8VWi8DoFqL2sSLZMXmGojcLnFrOBTipvBHJjySODcV4mCOo29W2duzPv5cALaZnXj5QxDx3xQfaQt3DKg +api_secret: + description: API secret of the user. + returned: success + type: string + sample: FUELo3LB9fa1UopjTLPdqLv_6OXQMJZv9g9N4B_Ao3HFz8d6IGFCV9MbPFNM8mwz00wbMevja1DoUNDvI8C9-g +account: + description: Account name of the user. + returned: success + type: string + sample: developers +account_type: + description: Type of the account. + returned: success + type: string + sample: user +timezone: + description: Timezone of the user. + returned: success + type: string + sample: enabled +created: + description: Date the user was created. + returned: success + type: string + sample: Doe +state: + description: State of the user. + returned: success + type: string + sample: enabled +domain: + description: Domain the user is related. + returned: success + type: string + sample: ROOT +''' + +try: + from cs import CloudStack, CloudStackException, read_config + has_lib_cs = True +except ImportError: + has_lib_cs = False + +# import cloudstack common +from ansible.module_utils.cloudstack import * + + +class AnsibleCloudStackUser(AnsibleCloudStack): + + def __init__(self, module): + super(AnsibleCloudStackUser, self).__init__(module) + self.returns = { + 'username': 'username', + 'firstname': 'first_name', + 'lastname': 'last_name', + 'email': 'email', + 'secretkey': 'api_secret', + 'apikey': 'api_key', + 'timezone': 'timezone', + } + self.account_types = { + 'user': 0, + 'root_admin': 1, + 'domain_admin': 2, + } + self.user = None + + + def get_account_type(self): + account_type = self.module.params.get('account_type') + return self.account_types[account_type] + + + def get_user(self): + if not self.user: + args = {} + args['domainid'] = self.get_domain('id') + users = self.cs.listUsers(**args) + if users: + user_name = self.module.params.get('username') + for u in users['user']: + if user_name.lower() == u['username'].lower(): + self.user = u + break + return self.user + + + def enable_user(self): + user = self.get_user() + if not user: + user = self.present_user() + + if user['state'].lower() != 'enabled': + self.result['changed'] = True + args = {} + args['id'] = user['id'] + if not self.module.check_mode: + res = self.cs.enableUser(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + user = res['user'] + return user + + + def lock_user(self): + user = self.get_user() + if not user: + user = self.present_user() + + # we need to enable the user to lock it. + if user['state'].lower() == 'disabled': + user = self.enable_user() + + if user['state'].lower() != 'locked': + self.result['changed'] = True + args = {} + args['id'] = user['id'] + if not self.module.check_mode: + res = self.cs.lockUser(**args) + + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + + user = res['user'] + return user + + + def disable_user(self): + user = self.get_user() + if not user: + user = self.present_user() + + if user['state'].lower() != 'disabled': + self.result['changed'] = True + args = {} + args['id'] = user['id'] + if not self.module.check_mode: + user = self.cs.disableUser(**args) + if 'errortext' in user: + self.module.fail_json(msg="Failed: '%s'" % user['errortext']) + + poll_async = self.module.params.get('poll_async') + if poll_async: + user = self._poll_job(user, 'user') + return user + + + def present_user(self): + missing_params = [] + for required_params in [ + 'account', + 'email', + 'password', + 'first_name', + 'last_name', + ]: + if not self.module.params.get(required_params): + missing_params.append(required_params) + if missing_params: + self.module.fail_json(msg="missing required arguments: %s" % ','.join(missing_params)) + + user = self.get_user() + if user: + user = self._update_user(user) + else: + user = self._create_user(user) + return user + + + def _create_user(self, user): + self.result['changed'] = True + + args = {} + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain('id') + args['username'] = self.module.params.get('username') + args['password'] = self.module.params.get('password') + args['firstname'] = self.module.params.get('first_name') + args['lastname'] = self.module.params.get('last_name') + args['email'] = self.module.params.get('email') + args['timezone'] = self.module.params.get('timezone') + if not self.module.check_mode: + res = self.cs.createUser(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + user = res['user'] + # register user api keys + res = self.cs.registerUserKeys(id=user['id']) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + user.update(res['userkeys']) + return user + + + def _update_user(self, user): + args = {} + args['id'] = user['id'] + args['firstname'] = self.module.params.get('first_name') + args['lastname'] = self.module.params.get('last_name') + args['email'] = self.module.params.get('email') + args['timezone'] = self.module.params.get('timezone') + if self.has_changed(args, user): + self.result['changed'] = True + if not self.module.check_mode: + res = self.cs.updateUser(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + user = res['user'] + # register user api keys + if 'apikey' not in user: + self.result['changed'] = True + if not self.module.check_mode: + res = self.cs.registerUserKeys(id=user['id']) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + user.update(res['userkeys']) + return user + + + def absent_user(self): + user = self.get_user() + if user: + self.result['changed'] = True + + if not self.module.check_mode: + res = self.cs.deleteUser(id=user['id']) + + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + return user + + + def get_result(self, user): + super(AnsibleCloudStackUser, self).get_result(user) + if user: + if 'accounttype' in user: + for key,value in self.account_types.items(): + if value == user['accounttype']: + self.result['account_type'] = key + break + return self.result + + +def main(): + module = AnsibleModule( + argument_spec = dict( + username = dict(required=True), + account = dict(default=None), + state = dict(choices=['present', 'absent', 'enabled', 'disabled', 'locked', 'unlocked'], default='present'), + domain = dict(default='ROOT'), + email = dict(default=None), + first_name = dict(default=None), + last_name = dict(default=None), + password = dict(default=None), + timezone = dict(default=None), + poll_async = dict(choices=BOOLEANS, default=True), + api_key = dict(default=None), + api_secret = dict(default=None, no_log=True), + api_url = dict(default=None), + api_http_method = dict(choices=['get', 'post'], default='get'), + api_timeout = dict(type='int', default=10), + api_region = dict(default='cloudstack'), + ), + required_together = ( + ['api_key', 'api_secret', 'api_url'], + ), + supports_check_mode=True + ) + + if not has_lib_cs: + module.fail_json(msg="python library cs required: pip install cs") + + try: + acs_acc = AnsibleCloudStackUser(module) + + state = module.params.get('state') + + if state in ['absent']: + user = acs_acc.absent_user() + + elif state in ['enabled', 'unlocked']: + user = acs_acc.enable_user() + + elif state in ['disabled']: + user = acs_acc.disable_user() + + elif state in ['locked']: + user = acs_acc.lock_user() + + else: + user = acs_acc.present_user() + + result = acs_acc.get_result(user) + + except CloudStackException, e: + module.fail_json(msg='CloudStackException: %s' % str(e)) + + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From 462c90eb546792517349ef74052b07666ed28910 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Fri, 11 Sep 2015 19:02:01 +0200 Subject: [PATCH 0725/2522] cloudstack: cs_account: add state unlocked as alias for enabled --- cloud/cloudstack/cs_account.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cloud/cloudstack/cs_account.py b/cloud/cloudstack/cs_account.py index e96d8e12638..fe2884f721b 100644 --- a/cloud/cloudstack/cs_account.py +++ b/cloud/cloudstack/cs_account.py @@ -85,9 +85,10 @@ state: description: - State of the account. + - C(unlocked) is an alias for C(enabled). required: false default: 'present' - choices: [ 'present', 'absent', 'enabled', 'disabled', 'locked' ] + choices: [ 'present', 'absent', 'enabled', 'disabled', 'locked', 'unlocked' ] poll_async: description: - Poll async jobs until job has finished. @@ -350,7 +351,7 @@ def main(): module = AnsibleModule( argument_spec = dict( name = dict(required=True), - state = dict(choices=['present', 'absent', 'enabled', 'disabled', 'locked' ], default='present'), + state = dict(choices=['present', 'absent', 'enabled', 'disabled', 'locked', 'unlocked'], default='present'), account_type = dict(choices=['user', 'root_admin', 'domain_admin'], default='user'), network_domain = dict(default=None), domain = dict(default='ROOT'), @@ -385,7 +386,7 @@ def main(): if state in ['absent']: account = acs_acc.absent_account() - elif state in ['enabled']: + elif state in ['enabled', 'unlocked']: account = acs_acc.enable_account() elif state in ['disabled']: From 67bc8e9fda560b677e875c27cf104d70ba135c96 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Fri, 11 Sep 2015 19:03:35 +0200 Subject: [PATCH 0726/2522] cloudstack: cs_account: create account for states locked/disabled/enabled if not present --- cloud/cloudstack/cs_account.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/cloudstack/cs_account.py b/cloud/cloudstack/cs_account.py index fe2884f721b..b8c2f0f54ef 100644 --- a/cloud/cloudstack/cs_account.py +++ b/cloud/cloudstack/cs_account.py @@ -221,7 +221,7 @@ def get_account(self): def enable_account(self): account = self.get_account() if not account: - self.module.fail_json(msg="Failed: account not present") + account = self.present_account() if account['state'].lower() != 'enabled': self.result['changed'] = True @@ -248,7 +248,7 @@ def disable_account(self): def lock_or_disable_account(self, lock=False): account = self.get_account() if not account: - self.module.fail_json(msg="Failed: account not present") + account = self.present_account() # we need to enable the account to lock it. if lock and account['state'].lower() == 'disabled': From c419dabe12cbcdcc909f613d8544156b64866dc4 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Fri, 11 Sep 2015 19:04:41 +0200 Subject: [PATCH 0727/2522] cloudstack: cs_account re-factor error handling state=present --- cloud/cloudstack/cs_account.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/cloud/cloudstack/cs_account.py b/cloud/cloudstack/cs_account.py index b8c2f0f54ef..2ffecf06fcf 100644 --- a/cloud/cloudstack/cs_account.py +++ b/cloud/cloudstack/cs_account.py @@ -277,21 +277,16 @@ def lock_or_disable_account(self, lock=False): def present_account(self): missing_params = [] - if not self.module.params.get('email'): - missing_params.append('email') - - if not self.module.params.get('username'): - missing_params.append('username') - - if not self.module.params.get('password'): - missing_params.append('password') - - if not self.module.params.get('first_name'): - missing_params.append('first_name') - - if not self.module.params.get('last_name'): - missing_params.append('last_name') - + missing_params = [] + for required_params in [ + 'email', + 'username', + 'password', + 'first_name', + 'last_name', + ]: + if not self.module.params.get(required_params): + missing_params.append(required_params) if missing_params: self.module.fail_json(msg="missing required arguments: %s" % ','.join(missing_params)) From 57f9b735f3a6c29d2721afbc1248a30721c1bf8b Mon Sep 17 00:00:00 2001 From: Siert Zijl Date: Fri, 11 Sep 2015 20:14:59 +0200 Subject: [PATCH 0728/2522] #931 append dot to zone if not defined --- cloud/amazon/route53_zone.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cloud/amazon/route53_zone.py b/cloud/amazon/route53_zone.py index b40a033b024..d5ba0dcd617 100644 --- a/cloud/amazon/route53_zone.py +++ b/cloud/amazon/route53_zone.py @@ -82,6 +82,9 @@ def main(): vpc_region = module.params.get('vpc_region') comment = module.params.get('comment') + if zone_in[-1:] != '.': + zone_in += "." + private_zone = vpc_id is not None and vpc_region is not None _, _, aws_connect_kwargs = get_aws_connection_info(module) From 0b0cc7b0490c2164f380a1392ec764e1e3926ce7 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Fri, 11 Sep 2015 21:20:20 +0200 Subject: [PATCH 0729/2522] composer: simplify has_changed() --- packaging/language/composer.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packaging/language/composer.py b/packaging/language/composer.py index 8e11d25216b..54cb3f17ba3 100644 --- a/packaging/language/composer.py +++ b/packaging/language/composer.py @@ -109,10 +109,7 @@ def parse_out(string): return re.sub("\s+", " ", string).strip() def has_changed(string): - if "Nothing to install or update" in string: - return False - else: - return True + return "Nothing to install or update" not in string def composer_install(module, command, options): php_path = module.get_bin_path("php", True, ["/usr/local/bin"]) From 7fdfa01615b544e5437219e0eb2e0dd14bcbe1be Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Fri, 11 Sep 2015 21:22:09 +0200 Subject: [PATCH 0730/2522] composer: rename composer_install to composer_command --- packaging/language/composer.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packaging/language/composer.py b/packaging/language/composer.py index 54cb3f17ba3..6d681c5ba2e 100644 --- a/packaging/language/composer.py +++ b/packaging/language/composer.py @@ -111,11 +111,10 @@ def parse_out(string): def has_changed(string): return "Nothing to install or update" not in string -def composer_install(module, command, options): +def composer_command(module, command, options=[]): php_path = module.get_bin_path("php", True, ["/usr/local/bin"]) composer_path = module.get_bin_path("composer", True, ["/usr/local/bin"]) cmd = "%s %s %s %s" % (php_path, composer_path, command, " ".join(options)) - return module.run_command(cmd) def main(): @@ -165,7 +164,7 @@ def main(): if module.check_mode: options.append('--dry-run') - rc, out, err = composer_install(module, command, options) + rc, out, err = composer_command(module, command, options) if rc != 0: output = parse_out(err) From 384eaba7662fbf5c794d7ace26786f41fa1e4de1 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Fri, 11 Sep 2015 21:26:23 +0200 Subject: [PATCH 0731/2522] composer: smarter arguments handling To get all available options in json for each command, `composer help --format=json` can be used. This allows us to simply parse the output and dynamically find out if an option is available. Neat! --- packaging/language/composer.py | 74 ++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 25 deletions(-) diff --git a/packaging/language/composer.py b/packaging/language/composer.py index 6d681c5ba2e..1ef93e736fc 100644 --- a/packaging/language/composer.py +++ b/packaging/language/composer.py @@ -22,7 +22,9 @@ DOCUMENTATION = ''' --- module: composer -author: "Dimitrios Tydeas Mengidis (@dmtrs)" +author: + - "Dimitrios Tydeas Mengidis (@dmtrs)" + - "René Moser (@resmo)" short_description: Dependency Manager for PHP version_added: "1.6" description: @@ -94,7 +96,7 @@ - php - composer installed in bin path (recommended /usr/local/bin) notes: - - Default options that are always appended in each execution are --no-ansi, --no-progress, and --no-interaction + - Default options that are always appended in each execution are --no-ansi, --no-interaction and --no-progress if available. ''' EXAMPLES = ''' @@ -105,12 +107,27 @@ import os import re +try: + import json +except ImportError: + import simplejson as json + def parse_out(string): return re.sub("\s+", " ", string).strip() def has_changed(string): return "Nothing to install or update" not in string +def get_available_options(module, command='install'): + # get all availabe options from a composer command using composer help to json + rc, out, err = composer_command(module, "help %s --format=json" % command) + if rc != 0: + output = parse_out(err) + module.fail_json(msg=output) + + command_help_json = json.loads(out) + return command_help_json['definition']['options'] + def composer_command(module, command, options=[]): php_path = module.get_bin_path("php", True, ["/usr/local/bin"]) composer_path = module.get_bin_path("composer", True, ["/usr/local/bin"]) @@ -133,33 +150,40 @@ def main(): supports_check_mode=True ) + # Get composer command with fallback to default + command = module.params['command'] + available_options = get_available_options(module=module, command=command) + options = [] # Default options - options.append('--no-ansi') - options.append('--no-progress') - options.append('--no-interaction') + default_options = [ + 'no-ansi', + 'no-interaction', + 'no-progress', + ] - options.extend(['--working-dir', os.path.abspath(module.params['working_dir'])]) + for option in default_options: + if option in available_options: + option = "--%s" % option + options.append(option) - # Get composer command with fallback to default - command = module.params['command'] + options.extend(['--working-dir', os.path.abspath(module.params['working_dir'])]) - # Prepare options - if module.params['prefer_source']: - options.append('--prefer-source') - if module.params['prefer_dist']: - options.append('--prefer-dist') - if module.params['no_dev']: - options.append('--no-dev') - if module.params['no_scripts']: - options.append('--no-scripts') - if module.params['no_plugins']: - options.append('--no-plugins') - if module.params['optimize_autoloader']: - options.append('--optimize-autoloader') - if module.params['ignore_platform_reqs']: - options.append('--ignore-platform-reqs') + option_params = { + 'prefer_source': 'prefer-source', + 'prefer_dist': 'prefer-dist', + 'no_dev': 'no-dev', + 'no_scripts': 'no-scripts', + 'no_plugins': 'no_plugins', + 'optimize_autoloader': 'optimize-autoloader', + 'ignore_platform_reqs': 'ignore-platform-reqs', + } + + for param, option in option_params.iteritems(): + if module.params.get(param) and option in available_options: + option = "--%s" % option + options.append(option) if module.check_mode: options.append('--dry-run') @@ -176,5 +200,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * - -main() +if __name__ == '__main__': + main() From 65c41451f08bc12a1e19b9f9f11cb1518ac7b8a1 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Sun, 13 Sep 2015 15:37:23 +0000 Subject: [PATCH 0732/2522] Ensure tag values get updated in ec2_vpc_subnet --- cloud/amazon/ec2_vpc_subnet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_vpc_subnet.py b/cloud/amazon/ec2_vpc_subnet.py index 45e84f66939..ec94459f4b1 100644 --- a/cloud/amazon/ec2_vpc_subnet.py +++ b/cloud/amazon/ec2_vpc_subnet.py @@ -163,7 +163,7 @@ def ensure_tags(vpc_conn, resource_id, tags, add_only, check_mode): if to_delete and not add_only: vpc_conn.delete_tags(resource_id, to_delete, dry_run=check_mode) - to_add = dict((k, tags[k]) for k in tags if k not in cur_tags) + to_add = dict((k, tags[k]) for k in tags if k not in cur_tags or cur_tags[k] != tags[k]) if to_add: vpc_conn.create_tags(resource_id, to_add, dry_run=check_mode) From 767605122a6072339cd730a8f79532267f318e92 Mon Sep 17 00:00:00 2001 From: whiter Date: Mon, 14 Sep 2015 09:43:02 +1000 Subject: [PATCH 0733/2522] New module - ec2_vpc_net_facts --- cloud/amazon/ec2_vpc_net_facts.py | 124 ++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 cloud/amazon/ec2_vpc_net_facts.py diff --git a/cloud/amazon/ec2_vpc_net_facts.py b/cloud/amazon/ec2_vpc_net_facts.py new file mode 100644 index 00000000000..fa45c2635d3 --- /dev/null +++ b/cloud/amazon/ec2_vpc_net_facts.py @@ -0,0 +1,124 @@ +#!/usr/bin/python +# +# This is a free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This Ansible library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this library. If not, see . + +DOCUMENTATION = ''' +--- +module: ec2_vpc_net_facts +short_description: Gather facts about ec2 VPCs in AWS +description: + - Gather facts about ec2 VPCs in AWS +version_added: "2.0" +author: "Rob White (@wimnat)" +options: + filters: + description: + - A dict of filters to apply. Each dict item consists of a filter key and a filter value. See U(http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeVpcs.html) for possible filters. + required: false + default: null + region: + description: + - The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used. See U(http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region) + required: false + default: null + aliases: [ 'aws_region', 'ec2_region' ] + +extends_documentation_fragment: aws +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Gather facts about all VPCs +- ec2_vpc_net_facts: + +# Gather facts about a particular VPC using VPC ID +- ec2_vpc_net_facts: + filters: + - vpc-id: vpc-00112233 + +# Gather facts about any VPC with a tag key Name and value Example +- ec2_vpc_net_facts: + filters: + - "tag:Name": Example + +''' + +try: + import boto.vpc + from boto.exception import BotoServerError + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +def get_vpc_info(vpc): + + vpc_info = { 'id': vpc.id, + 'instance_tenancy': vpc.instance_tenancy, + 'classic_link_enabled': vpc.classic_link_enabled, + 'dhcp_options_id': vpc.dhcp_options_id, + 'state': vpc.state, + 'is_default': vpc.is_default, + 'cidr_block': vpc.cidr_block, + 'tags': vpc.tags + } + + return vpc_info + +def list_ec2_vpcs(connection, module): + + filters = module.params.get("filters") + vpc_dict_array = [] + + try: + all_vpcs = connection.get_all_vpcs(filters=filters) + except BotoServerError as e: + module.fail_json(msg=e.message) + + for vpc in all_vpcs: + vpc_dict_array.append(get_vpc_info(vpc)) + + module.exit_json(vpcs=vpc_dict_array) + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + filters = dict(default=None, type='dict') + ) + ) + + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO: + module.fail_json(msg='boto required for this module') + + region, ec2_url, aws_connect_params = get_aws_connection_info(module) + + if region: + try: + connection = connect_to_aws(boto.vpc, region, **aws_connect_params) + except (boto.exception.NoAuthHandlerFound, StandardError), e: + module.fail_json(msg=str(e)) + else: + module.fail_json(msg="region must be specified") + + list_ec2_vpcs(connection, module) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From d1c15d6c8405a76359b8c3d174baee08a92af7ec Mon Sep 17 00:00:00 2001 From: Rob Date: Mon, 14 Sep 2015 10:38:36 +1000 Subject: [PATCH 0734/2522] Documentation fixup --- cloud/amazon/ec2_vpc_route_table_facts.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/amazon/ec2_vpc_route_table_facts.py b/cloud/amazon/ec2_vpc_route_table_facts.py index 78ef1be3509..7d37b2d79a2 100644 --- a/cloud/amazon/ec2_vpc_route_table_facts.py +++ b/cloud/amazon/ec2_vpc_route_table_facts.py @@ -46,17 +46,17 @@ # Gather facts about a particular VPC route table using route table ID - ec2_vpc_route_table_facts: filters: - - route-table-id: rtb-00112233 + route-table-id: rtb-00112233 # Gather facts about any VPC route table with a tag key Name and value Example - ec2_vpc_route_table_facts: filters: - - "tag:Name": Example + "tag:Name": Example # Gather facts about any VPC route table within VPC with ID vpc-abcdef00 - ec2_vpc_route_table_facts: filters: - - vpc-id: vpc-abcdef00 + vpc-id: vpc-abcdef00 ''' From 22de417dadf346c948b22f5fa1d0d5a9eb02ae0f Mon Sep 17 00:00:00 2001 From: kovacsbalu Date: Tue, 15 Sep 2015 09:35:14 +0200 Subject: [PATCH 0735/2522] Use push_type and send link with push_link. --- notification/pushbullet.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/notification/pushbullet.py b/notification/pushbullet.py index 52d785306ce..dfd89af577d 100644 --- a/notification/pushbullet.py +++ b/notification/pushbullet.py @@ -113,7 +113,8 @@ def main(): device = dict(type='str', default=None), push_type = dict(type='str', default="note", choices=['note', 'link']), title = dict(type='str', required=True), - body = dict(type='str', default=None) + body = dict(type='str', default=None), + url = dict(type='str', default=None), ), mutually_exclusive = ( ['channel', 'device'], @@ -127,6 +128,7 @@ def main(): push_type = module.params['push_type'] title = module.params['title'] body = module.params['body'] + url = module.params['url'] if not pushbullet_found: module.fail_json(msg="Python 'pushbullet.py' module is required. Install via: $ pip install pushbullet.py") @@ -170,7 +172,10 @@ def main(): # Send push notification try: - target.push_note(title, body) + if push_type == "link": + target.push_link(title, url, body) + else: + target.push_note(title, body) module.exit_json(changed=False, msg="OK") except PushError as e: module.fail_json(msg="An error occurred, Pushbullet's response: %s" % str(e)) From 30b676478b78e1d3aa7e7e51613562829caad788 Mon Sep 17 00:00:00 2001 From: Gerard Lynch Date: Tue, 15 Sep 2015 09:42:01 +0100 Subject: [PATCH 0736/2522] fix param description rst syntax --- packaging/language/maven_artifact.py | 33 ++++++++++++++++++---------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/packaging/language/maven_artifact.py b/packaging/language/maven_artifact.py index 5f20a9af169..658ad7f1173 100644 --- a/packaging/language/maven_artifact.py +++ b/packaging/language/maven_artifact.py @@ -41,46 +41,57 @@ - lxml options: group_id: - description: The Maven groupId coordinate + description: + - The Maven groupId coordinate required: true artifact_id: - description: The maven artifactId coordinate + description: + - The maven artifactId coordinate required: true version: - description: The maven version coordinate + description: + - The maven version coordinate required: false default: latest classifier: - description: The maven classifier coordinate + description: + - The maven classifier coordinate required: false default: null extension: - description: The maven type/extension coordinate + description: + - The maven type/extension coordinate required: false default: jar repository_url: - description: The URL of the Maven Repository to download from + description: + - The URL of the Maven Repository to download from required: false default: http://repo1.maven.org/maven2 username: - description: The username to authenticate as to the Maven Repository + description: + - The username to authenticate as to the Maven Repository required: false default: null password: - description: The password to authenticate with to the Maven Repository + description: + - The password to authenticate with to the Maven Repository required: false default: null dest: - description: The path where the artifact should be written to + description: + - The path where the artifact should be written to required: true default: false state: - description: The desired state of the artifact + description: + - The desired state of the artifact required: true default: present choices: [present,absent] validate_certs: - description: If C(no), SSL certificates will not be validated. This should only be set to C(no) when no other option exists. + description: + - If C(no), SSL certificates will not be validated. This should only be set to C(no) when no other option exists. required: false default: 'yes' choices: ['yes', 'no'] From a65965e38ee569f765baa4551dc21b923754f27a Mon Sep 17 00:00:00 2001 From: Mischa ter Smitten Date: Tue, 15 Sep 2015 12:57:37 +0200 Subject: [PATCH 0737/2522] Add more modes to the mysql_replication module Fixes #979 --- database/mysql/mysql_replication.py | 32 +++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/database/mysql/mysql_replication.py b/database/mysql/mysql_replication.py index f5d2d5cf630..a3bd63922ea 100644 --- a/database/mysql/mysql_replication.py +++ b/database/mysql/mysql_replication.py @@ -42,6 +42,8 @@ - changemaster - stopslave - startslave + - resetslave + - resetslaveall default: getslave login_user: description: @@ -165,6 +167,24 @@ def stop_slave(cursor): return stopped +def reset_slave(cursor): + try: + cursor.execute("RESET SLAVE") + reset = True + except: + reset = False + return reset + + +def reset_slave_all(cursor): + try: + cursor.execute("RESET SLAVE ALL") + reset = True + except: + reset = False + return reset + + def start_slave(cursor): try: cursor.execute("START SLAVE") @@ -400,6 +420,18 @@ def main(): module.exit_json(msg="Slave stopped", changed=True) else: module.exit_json(msg="Slave already stopped", changed=False) + elif mode in "resetslave": + reset = reset_slave(cursor) + if reset is True: + module.exit_json(msg="Slave reset", changed=True) + else: + module.exit_json(msg="Slave already reset", changed=False) + elif mode in "resetslaveall": + reset = reset_slave_all(cursor) + if reset is True: + module.exit_json(msg="Slave reset", changed=True) + else: + module.exit_json(msg="Slave already reset", changed=False) # import module snippets from ansible.module_utils.basic import * From 4ead053031ff6ea186aa5c8876e37507aa6a3577 Mon Sep 17 00:00:00 2001 From: Darren Worrall Date: Tue, 15 Sep 2015 16:40:01 +0100 Subject: [PATCH 0738/2522] Initialise `stream` variable Fixes `UnboundLocalError: local variable 'stream' referenced before assignment` when the check path doesnt exist --- monitoring/sensu_check.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monitoring/sensu_check.py b/monitoring/sensu_check.py index a1bd36ca665..e1c51463aeb 100644 --- a/monitoring/sensu_check.py +++ b/monitoring/sensu_check.py @@ -182,6 +182,7 @@ def sensu_check(module, path, name, state='present', backup=False): except ImportError: import simplejson as json + stream = None try: try: stream = open(path, 'r') From b0926125c27aae105be2e4c335c43d05b2f560a6 Mon Sep 17 00:00:00 2001 From: Darren Worrall Date: Tue, 15 Sep 2015 16:43:26 +0100 Subject: [PATCH 0739/2522] Fix json loading in sensu_check Fixes `AttributeError: 'str' object has no attribute 'read'` when the check path exists --- monitoring/sensu_check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/sensu_check.py b/monitoring/sensu_check.py index e1c51463aeb..c73fafdcd62 100644 --- a/monitoring/sensu_check.py +++ b/monitoring/sensu_check.py @@ -186,7 +186,7 @@ def sensu_check(module, path, name, state='present', backup=False): try: try: stream = open(path, 'r') - config = json.load(stream.read()) + config = json.load(stream) except IOError, e: if e.errno is 2: # File not found, non-fatal if state == 'absent': From 90dcc3daf901f0d9286f22fcac1fc05618492fe9 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 12 Sep 2015 13:49:22 +0200 Subject: [PATCH 0740/2522] new module ipify_facts --- network/ipify_facts.py | 95 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 network/ipify_facts.py diff --git a/network/ipify_facts.py b/network/ipify_facts.py new file mode 100644 index 00000000000..adcf5e4702b --- /dev/null +++ b/network/ipify_facts.py @@ -0,0 +1,95 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2015, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ipify_facts +short_description: Retrieve the public IP of your internet gateway. +description: + - If behind NAT and need to know the public IP of your internet gateway. +version_added: '2.0' +author: "René Moser (@resmo)" +options: + api_url: + description: + - URL of the ipify.org API service. + - C(?format=json) will be appended per default. + required: false + default: 'https://api.ipify.org' +notes: + - "Visit https://www.ipify.org to get more information." +''' + +EXAMPLES = ''' +# Gather IP facts from ipify.org +- name: get my public IP + ipify_facts: + +# Gather IP facts from your own ipify service endpoint +- name: get my public IP + ipify_facts: api_url=http://api.example.com/ipify +''' + +RETURN = ''' +--- +ipify_public_ip: + description: Public IP of the internet gateway. + returned: success + type: string + sample: 1.2.3.4 +''' + +try: + import json +except ImportError: + import simplejson as json + +class IpifyFacts(object): + + def __init__(self): + self.api_url = module.params.get('api_url') + + def run(self): + result = { + 'ipify_public_ip': None + } + (response, info) = fetch_url(module, self.api_url + "?format=json" , force=True) + if response: + data = json.loads(response.read()) + result['ipify_public_ip'] = data.get('ip') + return result + +def main(): + global module + module = AnsibleModule( + argument_spec = dict( + api_url = dict(default='https://api.ipify.org'), + ), + supports_check_mode=False + ) + + ipify_facts = IpifyFacts().run() + ipify_facts_result = dict(changed=False, ansible_facts=ipify_facts) + module.exit_json(**ipify_facts_result) + +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * +if __name__ == '__main__': + main() From bd0314c4062b31c88da82c2fe71ef8347ee4bbb2 Mon Sep 17 00:00:00 2001 From: Mischa ter Smitten Date: Thu, 17 Sep 2015 09:52:58 +0200 Subject: [PATCH 0741/2522] Documented new operation modes --- database/mysql/mysql_replication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/mysql/mysql_replication.py b/database/mysql/mysql_replication.py index a3bd63922ea..c833c244f12 100644 --- a/database/mysql/mysql_replication.py +++ b/database/mysql/mysql_replication.py @@ -34,7 +34,7 @@ options: mode: description: - - module operating mode. Could be getslave (SHOW SLAVE STATUS), getmaster (SHOW MASTER STATUS), changemaster (CHANGE MASTER TO), startslave (START SLAVE), stopslave (STOP SLAVE) + - module operating mode. Could be getslave (SHOW SLAVE STATUS), getmaster (SHOW MASTER STATUS), changemaster (CHANGE MASTER TO), startslave (START SLAVE), stopslave (STOP SLAVE), resetslave (RESET SLAVE), resetslaveall (RESET SLAVE ALL) required: False choices: - getslave From 9342c16e788274ad7dc68e50365fb8d1ea611395 Mon Sep 17 00:00:00 2001 From: Anders Ingemann Date: Thu, 17 Sep 2015 12:01:48 +0200 Subject: [PATCH 0742/2522] Add github ID to author field in sensu_check As proposed by @gregdek in #983 --- monitoring/sensu_check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/sensu_check.py b/monitoring/sensu_check.py index c73fafdcd62..72b0b8d8c6a 100644 --- a/monitoring/sensu_check.py +++ b/monitoring/sensu_check.py @@ -148,7 +148,7 @@ required: false default: null requirements: [ ] -author: Anders Ingemann +author: "Anders Ingemann (@andsens)" ''' EXAMPLES = ''' From e0f513a9033c3f7b513e79768f6724c9dad7e2b5 Mon Sep 17 00:00:00 2001 From: Anders Ingemann Date: Thu, 17 Sep 2015 12:12:54 +0200 Subject: [PATCH 0743/2522] Fix state & metric description Also: add state=absent to last example --- monitoring/sensu_check.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/monitoring/sensu_check.py b/monitoring/sensu_check.py index 72b0b8d8c6a..9a004d372e0 100644 --- a/monitoring/sensu_check.py +++ b/monitoring/sensu_check.py @@ -36,7 +36,8 @@ - This is the key that is used to determine whether a check exists required: true state: - description: Whether the check should be present or not + description: + - Whether the check should be present or not choices: [ 'present', 'absent' ] required: false default: present @@ -102,7 +103,8 @@ required: false default: [] metric: - description: Whether the check is a metric + description: + - Whether the check is a metric choices: [ 'yes', 'no' ] required: false default: no @@ -169,7 +171,7 @@ # Note that the check will still show up in the sensu dashboard, # to remove it completely you need to issue a DELETE request to the sensu api. - name: check disk - sensu_check: name=check_disk_capacity + sensu_check: name=check_disk_capacity state=absent ''' From 0c74601ea59981a95a22ada3ea93c5fd48415090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Thu, 17 Sep 2015 14:01:29 +0200 Subject: [PATCH 0744/2522] Fix tpyo --- cloud/amazon/ec2_remote_facts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_remote_facts.py b/cloud/amazon/ec2_remote_facts.py index 035b7b42394..3eadabfc77d 100644 --- a/cloud/amazon/ec2_remote_facts.py +++ b/cloud/amazon/ec2_remote_facts.py @@ -18,7 +18,7 @@ module: ec2_remote_facts short_description: ask EC2 for information about other instances. description: - - Only supports seatch for hostname by tags currently. Looking to add more later. + - Only supports search for hostname by tags currently. Looking to add more later. version_added: "2.0" options: key: From 790c83e78f3d5e8ede2a64bb43cd30c05df93595 Mon Sep 17 00:00:00 2001 From: timeraider4u Date: Tue, 28 Jul 2015 19:08:11 +0200 Subject: [PATCH 0745/2522] added check_mode support added support for the --check option during execution of the playbooks --- packaging/os/layman.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packaging/os/layman.py b/packaging/os/layman.py index 62694ee9118..f9ace121201 100644 --- a/packaging/os/layman.py +++ b/packaging/os/layman.py @@ -143,7 +143,11 @@ def install_overlay(module, name, list_url=None): layman = init_layman(layman_conf) if layman.is_installed(name): - return False + return False + + if module.check_mode: + mymsg = 'Would add layman repo \'' + name + '\'' + module.exit_json(changed=True, msg=mymsg) if not layman.is_repo(name): if not list_url: @@ -164,7 +168,7 @@ def install_overlay(module, name, list_url=None): return True -def uninstall_overlay(name): +def uninstall_overlay(module, name): '''Uninstalls the given overlay repository from the system. :param name: the overlay id to uninstall @@ -177,6 +181,10 @@ def uninstall_overlay(name): if not layman.is_installed(name): return False + + if module.check_mode: + mymsg = 'Would remove layman repo \'' + name + '\'' + module.exit_json(changed=True, msg=mymsg) layman.delete_repos(name) if layman.get_errors(): raise ModuleError(layman.get_errors()) @@ -216,7 +224,8 @@ def main(): list_url = dict(aliases=['url']), state = dict(default="present", choices=['present', 'absent', 'updated']), validate_certs = dict(required=False, default=True, type='bool'), - ) + ), + supports_check_mode=True ) if not HAS_LAYMAN_API: @@ -237,7 +246,7 @@ def main(): else: sync_overlay(name) else: - changed = uninstall_overlay(name) + changed = uninstall_overlay(module, name) except ModuleError, e: module.fail_json(msg=e.message) From a4f1653b95b7ce2c29b44abc0507ad27dbdb62a8 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Thu, 17 Sep 2015 14:52:49 -0400 Subject: [PATCH 0746/2522] updated vca_nat module to use common vca library --- cloud/vmware/vca_nat.py | 335 ++++++++++++++-------------------------- 1 file changed, 113 insertions(+), 222 deletions(-) diff --git a/cloud/vmware/vca_nat.py b/cloud/vmware/vca_nat.py index d34a52b6c75..a1af6883bcc 100644 --- a/cloud/vmware/vca_nat.py +++ b/cloud/vmware/vca_nat.py @@ -25,6 +25,7 @@ description: - Adds or removes nat rules from a gateway in a vca environment version_added: "2.0" +author: Peter Sprygada (@privateip) options: username: description: @@ -93,8 +94,6 @@ - A list of rules to be added to the gateway, Please see examples on valid entries required: True default: false - - ''' EXAMPLES = ''' @@ -130,252 +129,144 @@ ''' -import time, json, xmltodict - -HAS_PYVCLOUD = False -try: - from pyvcloud.vcloudair import VCA - HAS_PYVCLOUD = True -except ImportError: - pass - -SERVICE_MAP = {'vca': 'ondemand', 'vchs': 'subscription', 'vcd': 'vcd'} -LOGIN_HOST = {} -LOGIN_HOST['vca'] = 'vca.vmware.com' -LOGIN_HOST['vchs'] = 'vchs.vmware.com' -VALID_RULE_KEYS = ['rule_type', 'original_ip', 'original_port', 'translated_ip', 'translated_port', 'protocol'] - -def serialize_instances(instance_list): - instances = [] - for i in instance_list: - instances.append(dict(apiUrl=i['apiUrl'], instance_id=i['id'])) - return instances - -def vca_login(module=None): - service_type = module.params.get('service_type') - username = module.params.get('username') - password = module.params.get('password') - instance = module.params.get('instance_id') - org = module.params.get('org') - service = module.params.get('service_id') - vdc_name = module.params.get('vdc_name') - version = module.params.get('api_version') - verify = module.params.get('verify_certs') - if not vdc_name: - if service_type == 'vchs': - vdc_name = module.params.get('service_id') - if not org: - if service_type == 'vchs': - if vdc_name: - org = vdc_name - else: - org = service - if service_type == 'vcd': - host = module.params.get('host') - else: - host = LOGIN_HOST[service_type] - - if not username: - if 'VCA_USER' in os.environ: - username = os.environ['VCA_USER'] - if not password: - if 'VCA_PASS' in os.environ: - password = os.environ['VCA_PASS'] - if not username or not password: - module.fail_json(msg = "Either the username or password is not set, please check") - - if service_type == 'vchs': - version = '5.6' - if service_type == 'vcd': - if not version: - version == '5.6' - - - vca = VCA(host=host, username=username, service_type=SERVICE_MAP[service_type], version=version, verify=verify) - - if service_type == 'vca': - if not vca.login(password=password): - module.fail_json(msg = "Login Failed: Please check username or password", error=vca.response.content) - if not vca.login_to_instance(password=password, instance=instance, token=None, org_url=None): - s_json = serialize_instances(vca.instances) - module.fail_json(msg = "Login to Instance failed: Seems like instance_id provided is wrong .. Please check",\ - valid_instances=s_json) - if not vca.login_to_instance(instance=instance, password=None, token=vca.vcloud_session.token, - org_url=vca.vcloud_session.org_url): - module.fail_json(msg = "Error logging into org for the instance", error=vca.response.content) - return vca - - if service_type == 'vchs': - if not vca.login(password=password): - module.fail_json(msg = "Login Failed: Please check username or password", error=vca.response.content) - if not vca.login(token=vca.token): - module.fail_json(msg = "Failed to get the token", error=vca.response.content) - if not vca.login_to_org(service, org): - module.fail_json(msg = "Failed to login to org, Please check the orgname", error=vca.response.content) - return vca - - if service_type == 'vcd': - if not vca.login(password=password, org=org): - module.fail_json(msg = "Login Failed: Please check username or password or host parameters") - if not vca.login(password=password, org=org): - module.fail_json(msg = "Failed to get the token", error=vca.response.content) - if not vca.login(token=vca.token, org=org, org_url=vca.vcloud_session.org_url): - module.fail_json(msg = "Failed to login to org", error=vca.response.content) - return vca - -def validate_nat_rules(module=None, nat_rules=None): +import time +import json +import xmltodict + +VALID_RULE_KEYS = ['rule_type', 'original_ip', 'original_port', + 'translated_ip', 'translated_port', 'protocol'] + + +def validate_nat_rules(nat_rules): for rule in nat_rules: if not isinstance(rule, dict): - module.fail_json(msg="nat rules must be a list of dictionaries, Please check", valid_keys=VALID_RULE_KEYS) + raise VcaError("nat rules must be a list of dictionaries, " + "Please check", valid_keys=VALID_RULE_KEYS) + for k in rule.keys(): if k not in VALID_RULE_KEYS: - module.fail_json(msg="%s is not a valid key in nat rules, Please check above.." %k, valid_keys=VALID_RULE_KEYS) - rule['original_port'] = rule.get('original_port', 'any') - rule['original_ip'] = rule.get('original_ip', 'any') - rule['translated_ip'] = rule.get('translated_ip', 'any') - rule['translated_port'] = rule.get('translated_port', 'any') - rule['protocol'] = rule.get('protocol', 'any') - rule['rule_type'] = rule.get('rule_type', 'DNAT') + raise VcaError("%s is not a valid key in nat rules, please " + "check above.." % k, valid_keys=VALID_RULE_KEYS) + + rule['original_port'] = str(rule.get('original_port', 'any')).lower() + rule['original_ip'] = rule.get('original_ip', 'any').lower() + rule['translated_ip'] = rule.get('translated_ip', 'any').lower() + rule['translated_port'] = str(rule.get('translated_port', 'any')).lower() + rule['protocol'] = rule.get('protocol', 'any').lower() + rule['rule_type'] = rule.get('rule_type', 'DNAT').lower() + return nat_rules -def nat_rules_to_dict(natRules): +def nat_rules_to_dict(nat_rules): result = [] - for natRule in natRules: - ruleId = natRule.get_Id() - enable = natRule.get_IsEnabled() - ruleType = natRule.get_RuleType() - gatewayNatRule = natRule.get_GatewayNatRule() - originalIp = gatewayNatRule.get_OriginalIp() - originalPort = gatewayNatRule.get_OriginalPort() - translatedIp = gatewayNatRule.get_TranslatedIp() - translatedPort = gatewayNatRule.get_TranslatedPort() - protocol = gatewayNatRule.get_Protocol() - interface = gatewayNatRule.get_Interface().get_name() - result.append(dict(rule_type=ruleType, original_ip=originalIp, original_port="any" if not originalPort else originalPort, translated_ip=translatedIp, translated_port="any" if not translatedPort else translatedPort, - protocol="any" if not protocol else protocol)) + for rule in nat_rules: + gw_rule = rule.get_GatewayNatRule() + result.append( + dict( + rule_type=rule.get_RuleType().lower(), + original_ip=gw_rule.get_OriginalIp().lower(), + original_port=(gw_rule.get_OriginalPort().lower() or 'any'), + translated_ip=gw_rule.get_TranslatedIp().lower(), + translated_port=(gw_rule.get_TranslatedPort().lower() or 'any'), + protocol=(gw_rule.get_Protocol().lower() or 'any') + ) + ) return result +def rule_to_string(rule): + strings = list() + for key, value in rule.items(): + strings.append('%s=%s' % (key, value)) + return ', '.join(string) def main(): - module = AnsibleModule( - argument_spec=dict( - username = dict(default=None), - password = dict(default=None), - org = dict(default=None), - service_id = dict(default=None), - instance_id = dict(default=None), - host = dict(default=None), - api_version = dict(default='5.7'), - service_type = dict(default='vca', choices=['vchs', 'vca', 'vcd']), - state = dict(default='present', choices = ['present', 'absent']), - vdc_name = dict(default=None), - gateway_name = dict(default='gateway'), - nat_rules = dict(required=True, default=None, type='list'), - purge_rules = dict(default=False), + argument_spec = vca_argument_spec() + argument_spec.update( + dict( + nat_rules = dict(type='list', default=[]), + gateway_name = dict(default='gateway'), + purge_rules = dict(default=False, type='bool'), + state = dict(default='present', choices=['present', 'absent']) ) ) + module = AnsibleModule(argument_spec, supports_check_mode=True) - vdc_name = module.params.get('vdc_name') - org = module.params.get('org') - service = module.params.get('service_id') - state = module.params.get('state') - service_type = module.params.get('service_type') - host = module.params.get('host') - instance_id = module.params.get('instance_id') - nat_rules = module.params.get('nat_rules') - gateway_name = module.params.get('gateway_name') - purge_rules = module.params.get('purge_rules') - verify_certs = dict(default=True, type='bool'), - - if not HAS_PYVCLOUD: - module.fail_json(msg="python module pyvcloud is needed for this module") - if service_type == 'vca': - if not instance_id: - module.fail_json(msg="When service type is vca the instance_id parameter is mandatory") - if not vdc_name: - module.fail_json(msg="When service type is vca the vdc_name parameter is mandatory") - - if service_type == 'vchs': - if not service: - module.fail_json(msg="When service type vchs the service_id parameter is mandatory") - if not org: - org = service - if not vdc_name: - vdc_name = service - if service_type == 'vcd': - if not host: - module.fail_json(msg="When service type is vcd host parameter is mandatory") + vdc_name = module.params.get('vdc_name') + state = module.params['state'] + nat_rules = module.params['nat_rules'] + gateway_name = module.params['gateway_name'] + purge_rules = module.params['purge_rules'] + + if not purge_rules and not nat_rules: + module.fail_json('Must define purge_rules or nat_rules') vca = vca_login(module) - vdc = vca.get_vdc(vdc_name) - if not vdc: - module.fail_json(msg = "Error getting the vdc, Please check the vdc name") - mod_rules = validate_nat_rules(module, nat_rules) gateway = vca.get_gateway(vdc_name, gateway_name) if not gateway: - module.fail_json(msg="Not able to find the gateway %s, please check the gateway_name param" %gateway_name) + module.fail_json(msg="Not able to find the gateway %s, please check " + "the gateway_name param" % gateway_name) + + try: + desired_rules = validate_nat_rules(nat_rules) + except VcaError, e: + module.fail_json(msg=e.message) + rules = gateway.get_nat_rules() - cur_rules = nat_rules_to_dict(rules) - delete_cur_rule = [] - delete_rules = [] - for rule in cur_rules: - match = False - for idx, val in enumerate(mod_rules): - match = False - if cmp(rule, val) == 0: - delete_cur_rule.append(val) - mod_rules.pop(idx) - match = True - if not match: - delete_rules.append(rule) - if state == 'absent': - if purge_rules: - if not gateway.del_all_nat_rules(): - module.fail_json(msg="Error deleting all rules") - module.exit_json(changed=True, msg="Removed all rules") - if len(delete_cur_rule) < 1: - module.exit_json(changed=False, msg="No rules to be removed", rules=cur_rules) - else: - for i in delete_cur_rule: - gateway.del_nat_rule(i['rule_type'], i['original_ip'],\ - i['original_port'], i['translated_ip'], i['translated_port'], i['protocol']) - task = gateway.save_services_configuration() - if not task: - module.fail_json(msg="Unable to delete Rule, please check above error", error=gateway.response.content) - if not vca.block_until_completed(task): - module.fail_json(msg="Failure in waiting for removing network rule", error=gateway.response.content) - module.exit_json(changed=True, msg="The rules have been deleted", rules=delete_cur_rule) - changed = False - if len(mod_rules) < 1: - if not purge_rules: - module.exit_json(changed=False, msg="all rules are available", rules=cur_rules) - for i in mod_rules: - gateway.add_nat_rule(i['rule_type'], i['original_ip'], i['original_port'],\ - i['translated_ip'], i['translated_port'], i['protocol']) - task = gateway.save_services_configuration() - if not task: - module.fail_json(msg="Unable to add rule, please check above error", rules=mod_rules, error=gateway.response.content) - if not vca.block_until_completed(task): - module.fail_json(msg="Failure in waiting for adding network rule", error=gateway.response.content) - if purge_rules: - if len(delete_rules) < 1 and len(mod_rules) < 1: - module.exit_json(changed=False, rules=cur_rules) - for i in delete_rules: - gateway.del_nat_rule(i['rule_type'], i['original_ip'],\ - i['original_port'], i['translated_ip'], i['translated_port'], i['protocol']) + + result = dict(changed=False, rules_purged=0) + + deletions = 0 + additions = 0 + + if purge_rules is True and len(rules) > 0: + result['rules_purged'] = len(rules) + deletions = result['rules_purged'] + rules = list() + if not module.check_mode: + gateway.del_all_nat_rules() task = gateway.save_services_configuration() - if not task: - module.fail_json(msg="Unable to delete Rule, please check above error", error=gateway.response.content) - if not vca.block_until_completed(task): - module.fail_json(msg="Failure in waiting for removing network rule", error=gateway.response.content) + vca.block_until_completed(task) + rules = gateway.get_nat_rules() + result['changed'] = True + + current_rules = nat_rules_to_dict(rules) + + result['current_rules'] = current_rules + result['desired_rules'] = desired_rules + + for rule in desired_rules: + if rule not in current_rules: + additions += 1 + if not module.check_mode: + gateway.add_nat_rule(**rule) + result['changed'] = True + result['rules_added'] = additions + + result['delete_rule'] = list() + result['delete_rule_rc'] = list() + for rule in current_rules: + if rule not in desired_rules: + deletions += 1 + if not module.check_mode: + result['delete_rule'].append(rule) + rc = gateway.del_nat_rule(**rule) + result['delete_rule_rc'].append(rc) + result['changed'] = True + result['rules_deleted'] = deletions + + if not module.check_mode and (additions > 0 or deletions > 0): + task = gateway.save_services_configuration() + vca.block_until_completed(task) - module.exit_json(changed=True, rules_added=mod_rules) + module.exit_json(**result) # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.vca import * + if __name__ == '__main__': - main() + main() From 71ecaeb9f0310363e260faa6be058ce7774480ef Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Thu, 17 Sep 2015 14:57:48 -0400 Subject: [PATCH 0747/2522] refactored vca_fw to use vca common module --- cloud/vmware/vca_fw.py | 368 ++++++++++++++++++----------------------- 1 file changed, 159 insertions(+), 209 deletions(-) diff --git a/cloud/vmware/vca_fw.py b/cloud/vmware/vca_fw.py index 45ed78ef608..23356ccebbb 100644 --- a/cloud/vmware/vca_fw.py +++ b/cloud/vmware/vca_fw.py @@ -25,6 +25,7 @@ description: - Adds or removes firewall rules from a gateway in a vca environment version_added: "2.0" +author: Peter Sprygada (@privateip) options: username: description: @@ -61,7 +62,7 @@ - The type of service we are authenticating against required: false default: vca - choices: [ "vca", "vchs", "vcd" ] + choices: [ "vca", "vchs", "vcd" ] state: description: - if the object should be added or removed @@ -102,9 +103,9 @@ instance_id: 'b15ff1e5-1024-4f55-889f-ea0209726282' vdc_name: 'benz_ansible' state: 'absent' - fw_rules: + fw_rules: - description: "ben testing" - source_ip: "Any" + source_ip: "Any" dest_ip: 192.168.2.11 - description: "ben testing 2" source_ip: 192.168.2.100 @@ -118,235 +119,184 @@ ''' - - -import time, json, xmltodict -HAS_PYVCLOUD = False try: - from pyvcloud.vcloudair import VCA - from pyvcloud.schema.vcd.v1_5.schemas.vcloud.networkType import ProtocolsType - HAS_PYVCLOUD = True + from pyvcloud.schema.vcd.v1_5.schemas.vcloud.networkType import FirewallRuleType + from pyvcloud.schema.vcd.v1_5.schemas.vcloud.networkType import ProtocolsType except ImportError: + # normally set a flag here but it will be caught when testing for + # the existence of pyvcloud (see module_utils/vca.py). This just + # protects against generating an exception at runtime pass -SERVICE_MAP = {'vca': 'ondemand', 'vchs': 'subscription', 'vcd': 'vcd'} -LOGIN_HOST = {} -LOGIN_HOST['vca'] = 'vca.vmware.com' -LOGIN_HOST['vchs'] = 'vchs.vmware.com' -VALID_RULE_KEYS = ['policy', 'is_enable', 'enable_logging', 'description', 'dest_ip', 'dest_port', 'source_ip', 'source_port', 'protocol'] - -def vca_login(module=None): - service_type = module.params.get('service_type') - username = module.params.get('username') - password = module.params.get('password') - instance = module.params.get('instance_id') - org = module.params.get('org') - service = module.params.get('service_id') - vdc_name = module.params.get('vdc_name') - version = module.params.get('api_version') - verify = module.params.get('verify_certs') - if not vdc_name: - if service_type == 'vchs': - vdc_name = module.params.get('service_id') - if not org: - if service_type == 'vchs': - if vdc_name: - org = vdc_name - else: - org = service - if service_type == 'vcd': - host = module.params.get('host') - else: - host = LOGIN_HOST[service_type] - - if not username: - if 'VCA_USER' in os.environ: - username = os.environ['VCA_USER'] - if not password: - if 'VCA_PASS' in os.environ: - password = os.environ['VCA_PASS'] - if not username or not password: - module.fail_json(msg = "Either the username or password is not set, please check") - - if service_type == 'vchs': - version = '5.6' - if service_type == 'vcd': - if not version: - version == '5.6' - - - vca = VCA(host=host, username=username, service_type=SERVICE_MAP[service_type], version=version, verify=verify) - - if service_type == 'vca': - if not vca.login(password=password): - module.fail_json(msg = "Login Failed: Please check username or password", error=vca.response.content) - if not vca.login_to_instance(password=password, instance=instance, token=None, org_url=None): - s_json = serialize_instances(vca.instances) - module.fail_json(msg = "Login to Instance failed: Seems like instance_id provided is wrong .. Please check",\ - valid_instances=s_json) - if not vca.login_to_instance(instance=instance, password=None, token=vca.vcloud_session.token, - org_url=vca.vcloud_session.org_url): - module.fail_json(msg = "Error logging into org for the instance", error=vca.response.content) - return vca - - if service_type == 'vchs': - if not vca.login(password=password): - module.fail_json(msg = "Login Failed: Please check username or password", error=vca.response.content) - if not vca.login(token=vca.token): - module.fail_json(msg = "Failed to get the token", error=vca.response.content) - if not vca.login_to_org(service, org): - module.fail_json(msg = "Failed to login to org, Please check the orgname", error=vca.response.content) - return vca - - if service_type == 'vcd': - if not vca.login(password=password, org=org): - module.fail_json(msg = "Login Failed: Please check username or password or host parameters") - if not vca.login(password=password, org=org): - module.fail_json(msg = "Failed to get the token", error=vca.response.content) - if not vca.login(token=vca.token, org=org, org_url=vca.vcloud_session.org_url): - module.fail_json(msg = "Failed to login to org", error=vca.response.content) - return vca - -def validate_fw_rules(module=None, fw_rules=None): - VALID_PROTO = ['Tcp', 'Udp', 'Icmp', 'Any'] +VALID_PROTO = ['Tcp', 'Udp', 'Icmp', 'Other', 'Any'] +VALID_RULE_KEYS = ['policy', 'is_enable', 'enable_logging', 'description', + 'dest_ip', 'dest_port', 'source_ip', 'source_port', + 'protocol'] + +def protocol_to_tuple(protocol): + return (protocol.get_Tcp(), + protocol.get_Udp(), + protocol.get_Icmp(), + protocol.get_Other(), + protocol.get_Any()) + +def protocol_to_string(protocol): + protocol = protocol_to_tuple(protocol) + if protocol[0] is True: + return 'Tcp' + elif protocol[1] is True: + return 'Udp' + elif protocol[2] is True: + return 'Icmp' + elif protocol[3] is True: + return 'Other' + elif protocol[4] is True: + return 'Any' + +def protocol_to_type(protocol): + try: + protocols = ProtocolsType() + setattr(protocols, protocol, True) + return protocols + except AttributeError: + raise VcaError("The value in protocol is not valid") + +def validate_fw_rules(fw_rules): for rule in fw_rules: - if not isinstance(rule, dict): - module.fail_json(msg="Firewall rules must be a list of dictionaries, Please check", valid_keys=VALID_RULE_KEYS) for k in rule.keys(): if k not in VALID_RULE_KEYS: - module.fail_json(msg="%s is not a valid key in fw rules, Please check above.." %k, valid_keys=VALID_RULE_KEYS) - rule['dest_port'] = rule.get('dest_port', 'Any') - rule['dest_ip'] = rule.get('dest_ip', 'Any') - rule['source_port'] = rule.get('source_port', 'Any') - rule['source_ip'] = rule.get('source_ip', 'Any') - rule['protocol'] = rule.get('protocol', 'Any') - rule['policy'] = rule.get('policy', 'allow') - rule['is_enable'] = rule.get('is_enable', 'true') - rule['enable_logging'] = rule.get('enable_logging', 'false') - rule['description'] = rule.get('description', 'rule added by Ansible') - if not rule['protocol'] in VALID_PROTO: - module.fail_json(msg="the value in protocol is not valid, valid values are as above", valid_proto=VALID_PROTO) + raise VcaError("%s is not a valid key in fw rules, please " + "check above.." % k, valid_keys=VALID_RULE_KEYS) + + rule['dest_port'] = str(rule.get('dest_port', 'Any')).lower() + rule['dest_ip'] = rule.get('dest_ip', 'Any').lower() + rule['source_port'] = str(rule.get('source_port', 'Any')).lower() + rule['source_ip'] = rule.get('source_ip', 'Any').lower() + rule['protocol'] = rule.get('protocol', 'Any').lower() + rule['policy'] = rule.get('policy', 'allow').lower() + rule['is_enable'] = rule.get('is_enable', True) + rule['enable_logging'] = rule.get('enable_logging', False) + rule['description'] = rule.get('description', 'rule added by Ansible') + return fw_rules -def create_protocol_list(protocol): - plist = [] - plist.append(protocol.get_Tcp()) - plist.append(protocol.get_Any()) - plist.append(protocol.get_Tcp()) - plist.append(protocol.get_Udp()) - plist.append(protocol.get_Icmp()) - plist.append(protocol.get_Other()) - return plist +def fw_rules_to_dict(rules): + fw_rules = list() + for rule in rules: + fw_rules.append( + dict( + dest_port=rule.get_DestinationPortRange().lower(), + dest_ip=rule.get_DestinationIp().lower().lower(), + source_port=rule.get_SourcePortRange().lower(), + source_ip=rule.get_SourceIp().lower(), + protocol=protocol_to_string(rule.get_Protocols()).lower(), + policy=rule.get_Policy().lower(), + is_enable=rule.get_IsEnabled(), + enable_logging=rule.get_EnableLogging(), + description=rule.get_Description() + ) + ) + return fw_rules +def create_fw_rule(is_enable, description, policy, protocol, dest_port, + dest_ip, source_port, source_ip, enable_logging): -def create_protocols_type(protocol): - all_protocols = {"Tcp": None, "Udp": None, "Icmp": None, "Any": None} - all_protocols[protocol] = True - return ProtocolsType(**all_protocols) + return FirewallRuleType(IsEnabled=is_enable, + Description=description, + Policy=policy, + Protocols=protocol_to_type(protocol), + DestinationPortRange=dest_port, + DestinationIp=dest_ip, + SourcePortRange=source_port, + SourceIp=source_ip, + EnableLogging=enable_logging) def main(): - module = AnsibleModule( - argument_spec=dict( - username = dict(default=None), - password = dict(default=None), - org = dict(default=None), - service_id = dict(default=None), - instance_id = dict(default=None), - host = dict(default=None), - api_version = dict(default='5.7'), - service_type = dict(default='vca', choices=['vchs', 'vca', 'vcd']), - state = dict(default='present', choices = ['present', 'absent']), - vdc_name = dict(default=None), - gateway_name = dict(default='gateway'), - fw_rules = dict(required=True, default=None, type='list'), + argument_spec = vca_argument_spec() + argument_spec.update( + dict( + fw_rules = dict(required=True, type='list'), + gateway_name = dict(default='gateway'), + state = dict(default='present', choices=['present', 'absent']) ) ) + module = AnsibleModule(argument_spec, supports_check_mode=True) + + fw_rules = module.params.get('fw_rules') + gateway_name = module.params.get('gateway_name') + vdc_name = module.params['vdc_name'] - vdc_name = module.params.get('vdc_name') - org = module.params.get('org') - service = module.params.get('service_id') - state = module.params.get('state') - service_type = module.params.get('service_type') - host = module.params.get('host') - instance_id = module.params.get('instance_id') - fw_rules = module.params.get('fw_rules') - gateway_name = module.params.get('gateway_name') - verify_certs = dict(default=True, type='bool'), - - if not HAS_PYVCLOUD: - module.fail_json(msg="python module pyvcloud is needed for this module") - if service_type == 'vca': - if not instance_id: - module.fail_json(msg="When service type is vca the instance_id parameter is mandatory") - if not vdc_name: - module.fail_json(msg="When service type is vca the vdc_name parameter is mandatory") - - if service_type == 'vchs': - if not service: - module.fail_json(msg="When service type vchs the service_id parameter is mandatory") - if not org: - org = service - if not vdc_name: - vdc_name = service - if service_type == 'vcd': - if not host: - module.fail_json(msg="When service type is vcd host parameter is mandatory") - vca = vca_login(module) - vdc = vca.get_vdc(vdc_name) - if not vdc: - module.fail_json(msg = "Error getting the vdc, Please check the vdc name") - mod_rules = validate_fw_rules(module, fw_rules) gateway = vca.get_gateway(vdc_name, gateway_name) if not gateway: - module.fail_json(msg="Not able to find the gateway %s, please check the gateway_name param" %gateway_name) + module.fail_json(msg="Not able to find the gateway %s, please check " + "the gateway_name param" % gateway_name) + + fwservice = gateway._getFirewallService() + rules = gateway.get_fw_rules() - existing_rules = [] - del_rules = [] - for rule in rules: - current_trait = (create_protocol_list(rule.get_Protocols()), - rule.get_DestinationPortRange(), - rule.get_DestinationIp(), - rule.get_SourcePortRange(), - rule.get_SourceIp()) - for idx, val in enumerate(mod_rules): - trait = (create_protocol_list(create_protocols_type(val['protocol'])), - val['dest_port'], val['dest_ip'], val['source_port'], val['source_ip']) - if current_trait == trait: - del_rules.append(mod_rules[idx]) - mod_rules.pop(idx) - existing_rules.append(current_trait) - - if state == 'absent': - if len(del_rules) < 1: - module.exit_json(changed=False, msg="Nothing to delete", delete_rules=mod_rules) - else: - for i in del_rules: - gateway.delete_fw_rule(i['protocol'], i['dest_port'], i['dest_ip'], i['source_port'], i['source_ip']) - task = gateway.save_services_configuration() - if not task: - module.fail_json(msg="Unable to Delete Rule, please check above error", error=gateway.response.content) - if not vca.block_until_completed(task): - module.fail_json(msg="Error while waiting to remove Rule, please check above error", error=gateway.response.content) - module.exit_json(changed=True, msg="Rules Deleted", deleted_rules=del_rules) - - if len(mod_rules) < 1: - module.exit_json(changed=False, rules=existing_rules) - if len(mod_rules) >= 1: - for i in mod_rules: - gateway.add_fw_rule(i['is_enable'], i['description'], i['policy'], i['protocol'], i['dest_port'], i['dest_ip'], - i['source_port'], i['source_ip'], i['enable_logging']) - task = gateway.save_services_configuration() - if not task: - module.fail_json(msg="Unable to Add Rule, please check above error", error=gateway.response.content) - if not vca.block_until_completed(task): - module.fail_json(msg="Failure in waiting for adding firewall rule", error=gateway.response.content) - module.exit_json(changed=True, rules=mod_rules) - - + current_rules = fw_rules_to_dict(rules) + + try: + desired_rules = validate_fw_rules(fw_rules) + except VcaError, e: + module.fail_json(e.message) + + result = dict(changed=False) + result['current_rules'] = current_rules + result['desired_rules'] = desired_rules + + updates = list() + additions = list() + deletions = list() + + for (index, rule) in enumerate(desired_rules): + try: + if rule != current_rules[index]: + updates.append((index, rule)) + except IndexError: + additions.append(rule) + + eol = len(current_rules) > len(desired_rules) + if eol > 0: + for rule in current_rules[eos:]: + deletions.append(rule) + + for rule in additions: + if not module.check_mode: + rule['protocol'] = rule['protocol'].capitalize() + gateway.add_fw_rule(**rule) + result['changed'] = True + + for index, rule in updates: + if not module.check_mode: + rule = create_fw_rule(**rule) + fwservice.replace_FirewallRule_at(index, rule) + result['changed'] = True + + keys = ['protocol', 'dest_port', 'dest_ip', 'source_port', 'source_ip'] + for rule in deletions: + if not module.check_mode: + kwargs = dict([(k, v) for k, v in rule.items() if k in keys]) + kwargs['protocol'] = protocol_to_string(kwargs['protocol']) + gateway.delete_fw_rule(**kwargs) + result['changed'] = True + + if not module.check_mode and result['changed'] == True: + task = gateway.save_services_configuration() + if task: + vca.block_until_completed(task) + + result['rules_updated'] = count=len(updates) + result['rules_added'] = count=len(additions) + result['rules_deleted'] = count=len(deletions) + + return module.exit_json(**result) + # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.vca import * if __name__ == '__main__': main() From fedef0c958ca7927f63119de927f48938b4ddbd6 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Thu, 17 Sep 2015 14:59:30 -0400 Subject: [PATCH 0748/2522] refactor vca_vapp to use vca common module --- cloud/vmware/vca_vapp.py | 651 ++++++++------------------------------- 1 file changed, 126 insertions(+), 525 deletions(-) diff --git a/cloud/vmware/vca_vapp.py b/cloud/vmware/vca_vapp.py index ef8e52c421b..1fb27ef07b7 100644 --- a/cloud/vmware/vca_vapp.py +++ b/cloud/vmware/vca_vapp.py @@ -25,6 +25,7 @@ description: - Creates or terminates vca vms. version_added: "2.0" +author: Peter Sprygada (@privateip) options: username: description: @@ -87,11 +88,6 @@ - The network name to which the vm should be attached. required: false default: 'None' - network_ip: - description: - - The ip address that should be assigned to vm when the ip assignment type is static - required: false - default: None network_mode: description: - The network mode in which the ip should be allocated. @@ -103,16 +99,6 @@ - The instance id of the region in vca flavour where the vm should be created required: false default: None - wait: - description: - - If the module should wait if the operation is poweroff or poweron, is better to wait to report the right state. - required: false - default: True - wait_timeout: - description: - - The wait timeout when wait is set to true - required: false - default: 250 vdc_name: description: - The name of the vdc where the vm should be created. @@ -133,23 +119,6 @@ - The amount of memory to be added to vm in megabytes required: false default: None - verify_certs: - description: - - If the certificates of the authentication is to be verified - required: false - default: True - admin_password: - description: - - The password to be set for admin - required: false - default: None - operation: - description: - - The operation to be done on the vm - required: false - default: poweroff - choices: [ 'shutdown', 'poweroff', 'poweron', 'reboot', 'reset', 'suspend' ] - ''' EXAMPLES = ''' @@ -207,519 +176,151 @@ template_name: "CentOS 6.5 64Bit CLI" network_mode: pool - ''' - -import time, json, xmltodict - -HAS_PYVCLOUD = False try: from pyvcloud.vcloudair import VCA HAS_PYVCLOUD = True except ImportError: - pass - -SERVICE_MAP = {'vca': 'ondemand', 'vchs': 'subscription', 'vcd': 'vcd'} -LOGIN_HOST = {} -LOGIN_HOST['vca'] = 'vca.vmware.com' -LOGIN_HOST['vchs'] = 'vchs.vmware.com' -VM_COMPARE_KEYS = ['admin_password', 'status', 'cpus', 'memory_mb'] - -def vm_state(val=None): - if val == 8: - return "Power_Off" - elif val == 4: - return "Power_On" - else: - return "Unknown Status" - -def serialize_instances(instance_list): - instances = [] - for i in instance_list: - instances.append(dict(apiUrl=i['apiUrl'], instance_id=i['id'])) - return instances - -def get_catalogs(vca): - catalogs = vca.get_catalogs() - results = [] - for catalog in catalogs: - if catalog.CatalogItems and catalog.CatalogItems.CatalogItem: - for item in catalog.CatalogItems.CatalogItem: - results.append([catalog.name, item.name]) - else: - results.append([catalog.name, '']) - return results - -def vca_login(module=None): - service_type = module.params.get('service_type') - username = module.params.get('username') - password = module.params.get('password') - instance = module.params.get('instance_id') - org = module.params.get('org') - service = module.params.get('service_id') - vdc_name = module.params.get('vdc_name') - version = module.params.get('api_version') - verify = module.params.get('verify_certs') - if not vdc_name: - if service_type == 'vchs': - vdc_name = module.params.get('service_id') - if not org: - if service_type == 'vchs': - if vdc_name: - org = vdc_name - else: - org = service - if service_type == 'vcd': - host = module.params.get('host') - else: - host = LOGIN_HOST[service_type] - - if not username: - if 'VCA_USER' in os.environ: - username = os.environ['VCA_USER'] - if not password: - if 'VCA_PASS' in os.environ: - password = os.environ['VCA_PASS'] - if not username or not password: - module.fail_json(msg = "Either the username or password is not set, please check") - - if service_type == 'vchs': - version = '5.6' - if service_type == 'vcd': - if not version: - version == '5.6' - - - vca = VCA(host=host, username=username, service_type=SERVICE_MAP[service_type], version=version, verify=verify) - - if service_type == 'vca': - if not vca.login(password=password): - module.fail_json(msg = "Login Failed: Please check username or password", error=vca.response.content) - if not vca.login_to_instance(password=password, instance=instance, token=None, org_url=None): - s_json = serialize_instances(vca.instances) - module.fail_json(msg = "Login to Instance failed: Seems like instance_id provided is wrong .. Please check",\ - valid_instances=s_json) - if not vca.login_to_instance(instance=instance, password=None, token=vca.vcloud_session.token, - org_url=vca.vcloud_session.org_url): - module.fail_json(msg = "Error logging into org for the instance", error=vca.response.content) - return vca - - if service_type == 'vchs': - if not vca.login(password=password): - module.fail_json(msg = "Login Failed: Please check username or password", error=vca.response.content) - if not vca.login(token=vca.token): - module.fail_json(msg = "Failed to get the token", error=vca.response.content) - if not vca.login_to_org(service, org): - module.fail_json(msg = "Failed to login to org, Please check the orgname", error=vca.response.content) - return vca - - if service_type == 'vcd': - if not vca.login(password=password, org=org): - module.fail_json(msg = "Login Failed: Please check username or password or host parameters") - if not vca.login(password=password, org=org): - module.fail_json(msg = "Failed to get the token", error=vca.response.content) - if not vca.login(token=vca.token, org=org, org_url=vca.vcloud_session.org_url): - module.fail_json(msg = "Failed to login to org", error=vca.response.content) - return vca - -def set_vm_state(module=None, vca=None, state=None): - wait = module.params.get('wait') - wait_tmout = module.params.get('wait_timeout') - vm_name = module.params.get('vm_name') - vdc_name = module.params.get('vdc_name') - vapp_name = module.params.get('vm_name') - service_type = module.params.get('service_type') - service_id = module.params.get('service_id') - if service_type == 'vchs' and not vdc_name: - vdc_name = service_id - vdc = vca.get_vdc(vdc_name) - if wait: - tmout = time.time() + wait_tmout - while tmout > time.time(): - vapp = vca.get_vapp(vdc, vapp_name) - vms = filter(lambda vm: vm['name'] == vm_name, vapp.get_vms_details()) - vm = vms[0] - if vm['status'] == state: - return True - time.sleep(5) - module.fail_json(msg="Timeut waiting for the vms state to change") - return True - -def vm_details(vdc=None, vapp=None, vca=None): - table = [] - networks = [] - vm_name = vapp - vdc1 = vca.get_vdc(vdc) - if not vdc1: - module.fail_json(msg = "Error getting the vdc, Please check the vdc name") - vap = vca.get_vapp(vdc1, vapp) - if vap: - vms = filter(lambda vm: vm['name'] == vm_name, vap.get_vms_details()) - networks = vap.get_vms_network_info() - if len(networks[0]) >= 1: - table.append(dict(vm_info=vms[0], network_info=networks[0][0])) - else: - table.append(dict(vm_info=vms[0], network_info=networks[0])) - return table - - -def vapp_attach_net(module=None, vca=None, vapp=None): - network_name = module.params.get('network_name') - service_type = module.params.get('service_type') - vdc_name = module.params.get('vdc_name') - mode = module.params.get('network_mode') - if mode.upper() == 'STATIC': - network_ip = module.params.get('network_ip') - else: - network_ip = None - if not vdc_name: - if service_type == 'vchs': - vdc_name = module.params.get('service_id') - nets = filter(lambda n: n.name == network_name, vca.get_networks(vdc_name)) - if len(nets) <= 1: - net_task = vapp.disconnect_vms() - if not net_task: - module.fail_json(msg="Failure in detattaching vms from vnetworks", error=vapp.response.content) - if not vca.block_until_completed(net_task): - module.fail_json(msg="Failure in waiting for detaching vms from vnetworks", error=vapp.response.content) - net_task = vapp.disconnect_from_networks() - if not net_task: - module.fail_json(msg="Failure in detattaching network from vapp", error=vapp.response.content) - if not vca.block_until_completed(net_task): - module.fail_json(msg="Failure in waiting for detaching network from vapp", error=vapp.response.content) - if not network_name: - return True - - net_task = vapp.connect_to_network(nets[0].name, nets[0].href) - if not net_task: - module.fail_json(msg="Failure in attaching network to vapp", error=vapp.response.content) - if not vca.block_until_completed(net_task): - module.fail_json(msg="Failure in waiting for attching network to vapp", error=vapp.response.content) - - net_task = vapp.connect_vms(nets[0].name, connection_index=0, ip_allocation_mode=mode.upper(), ip_address=network_ip ) - if not net_task: - module.fail_json(msg="Failure in attaching network to vm", error=vapp.response.content) - if not vca.block_until_completed(net_task): - module.fail_json(msg="Failure in waiting for attaching network to vm", error=vapp.response.content) - return True - nets = [] - for i in vca.get_networks(vdc_name): - nets.append(i.name) - module.fail_json(msg="Seems like network_name is not found in the vdc, please check Available networks as above", Available_networks=nets) - -def create_vm(vca=None, module=None): - vm_name = module.params.get('vm_name') - operation = module.params.get('operation') - vm_cpus = module.params.get('vm_cpus') - vm_memory = module.params.get('vm_memory') - catalog_name = module.params.get('catalog_name') - template_name = module.params.get('template_name') - vdc_name = module.params.get('vdc_name') - network_name = module.params.get('network_name') - service_type = module.params.get('service_type') - admin_pass = module.params.get('admin_password') - script = module.params.get('script') - vapp_name = vm_name - - if not vdc_name: - if service_type == 'vchs': - vdc_name = module.params.get('service_id') - task = vca.create_vapp(vdc_name, vapp_name, template_name, catalog_name, vm_name=None) - if not task: - catalogs = get_catalogs(vca) - module.fail_json(msg="Error in Creating VM, Please check catalog or template, Available catalogs and templates are as above or check the error field", catalogs=catalogs, errors=vca.response.content) - if not vca.block_until_completed(task): - module.fail_json(msg = "Error in waiting for VM Creation, Please check logs", errors=vca.response.content) - vdc = vca.get_vdc(vdc_name) - if not vdc: - module.fail_json(msg = "Error getting the vdc, Please check the vdc name", errors=vca.response.content) - - vapp = vca.get_vapp(vdc, vapp_name) - task = vapp.modify_vm_name(1, vm_name) - if not task: - module.fail_json(msg="Error in setting the vm_name to vapp_name", errors=vca.response.content) - if not vca.block_until_completed(task): - module.fail_json(msg = "Error in waiting for VM Renaming, Please check logs", errors=vca.response.content) - vapp = vca.get_vapp(vdc, vapp_name) - task = vapp.customize_guest_os(vm_name, computer_name=vm_name) - if not task: - module.fail_json(msg="Error in setting the computer_name to vm_name", errors=vca.response.content) - if not vca.block_until_completed(task): - module.fail_json(msg = "Error in waiting for Computer Renaming, Please check logs", errors=vca.response.content) - - - if network_name: - vapp = vca.get_vapp(vdc, vapp_name) - if not vapp_attach_net(module, vca, vapp): - module.fail_json(msg= "Attaching network to VM fails", errors=vca.response.content) - - if vm_cpus: - vapp = vca.get_vapp(vdc, vapp_name) - task = vapp.modify_vm_cpu(vm_name, vm_cpus) - if not task: - module.fail_json(msg="Error adding cpu", error=vapp.resonse.contents) - if not vca.block_until_completed(task): - module.fail_json(msg="Failure in waiting for modifying cpu", error=vapp.response.content) - - if vm_memory: - vapp = vca.get_vapp(vdc, vapp_name) - task = vapp.modify_vm_memory(vm_name, vm_memory) - if not task: - module.fail_json(msg="Error adding memory", error=vapp.resonse.contents) - if not vca.block_until_completed(task): - module.fail_json(msg="Failure in waiting for modifying memory", error=vapp.response.content) - - if admin_pass: - vapp = vca.get_vapp(vdc, vapp_name) - task = vapp.customize_guest_os(vm_name, customization_script=None, - computer_name=None, admin_password=admin_pass, - reset_password_required=False) - if not task: - module.fail_json(msg="Error adding admin password", error=vapp.resonse.contents) - if not vca.block_until_completed(task): - module.fail_json(msg = "Error in waiting for resettng admin pass, Please check logs", errors=vapp.response.content) - - if script: - vapp = vca.get_vapp(vdc, vapp_name) - if os.path.exists(os.path.expanduser(script)): - file_contents = open(script, 'r') - task = vapp.customize_guest_os(vm_name, customization_script=file_contents.read()) - if not task: - module.fail_json(msg="Error adding customization script", error=vapp.resonse.contents) - if not vca.block_until_completed(task): - module.fail_json(msg = "Error in waiting for customization script, please check logs", errors=vapp.response.content) - task = vapp.force_customization(vm_name, power_on=False ) - if not task: - module.fail_json(msg="Error adding customization script", error=vapp.resonse.contents) - if not vca.block_until_completed(task): - module.fail_json(msg = "Error in waiting for customization script, please check logs", errors=vapp.response.content) - else: - module.fail_json(msg = "The file specified in script paramter is not avaialable or accessible") - - vapp = vca.get_vapp(vdc, vapp_name) - if operation == 'poweron': - vapp.poweron() - set_vm_state(module, vca, state='Powered on') - elif operation == 'poweroff': - vapp.poweroff() - elif operation == 'reboot': - vapp.reboot() - elif operation == 'reset': - vapp.reset() - elif operation == 'suspend': - vapp.suspend() - elif operation == 'shutdown': - vapp.shutdown() - details = vm_details(vdc_name, vapp_name, vca) - module.exit_json(changed=True, msg="VM created", vm_details=details[0]) - -def vapp_reconfigure(module=None, diff=None, vm=None, vca=None, vapp=None, vdc_name=None): - flag = False - vapp_name = module.params.get('vm_name') - vm_name = module.params.get('vm_name') - cpus = module.params.get('vm_cpus') - memory = module.params.get('vm_memory') - admin_pass = module.params.get('admin_password') - - if 'status' in diff: - operation = module.params.get('operation') - if operation == 'poweroff': - vapp.poweroff() - set_vm_state(module, vca, state='Powered off') - flag = True - if 'network' in diff: - vapp_attach_net(module, vca, vapp) - flag = True - if 'cpus' in diff: - task = vapp.modify_vm_cpu(vm_name, cpus) - if not vca.block_until_completed(task): - module.fail_json(msg="Failure in waiting for modifying cpu, might be vm is powered on and doesnt support hotplugging", error=vapp.response.content) - flag = True - if 'memory_mb' in diff: - task = vapp.modify_vm_memory(vm_name, memory) - if not vca.block_until_completed(task): - module.fail_json(msg="Failure in waiting for modifying memory, might be vm is powered on and doesnt support hotplugging", error=vapp.response.content) - flag = True - if 'admin_password' in diff: - task = vapp.customize_guest_os(vm_name, customization_script=None, - computer_name=None, admin_password=admin_pass, - reset_password_required=False) - if not task: - module.fail_json(msg="Error adding admin password", error=vapp.resonse.contents) - if not vca.block_until_completed(task): - module.fail_json(msg = "Error in waiting for resettng admin pass, Please check logs", errors=vapp.response.content) - flag = True - if 'status' in diff: - operation = module.params.get('operation') - if operation == 'poweron': - vapp.poweron() - set_vm_state(module, vca, state='Powered on') - elif operation == 'reboot': - vapp.reboot() - elif operation == 'reset': - vapp.reset() - elif operation == 'suspend': - vapp.suspend() - elif operation == 'shutdown': - vapp.shutdown() - flag = True - details = vm_details(vdc_name, vapp_name, vca) - if flag: - module.exit_json(changed=True, msg="VM reconfigured", vm_details=details[0]) - module.exit_json(changed=False, msg="VM exists as per configuration",\ - vm_details=details[0]) - -def vm_exists(module=None, vapp=None, vca=None, vdc_name=None): - vm_name = module.params.get('vm_name') - operation = module.params.get('operation') - vm_cpus = module.params.get('vm_cpus') - vm_memory = module.params.get('vm_memory') - network_name = module.params.get('network_name') - admin_pass = module.params.get('admin_password') - - d_vm = {} - d_vm['name'] = vm_name - d_vm['cpus'] = vm_cpus - d_vm['memory_mb'] = vm_memory - d_vm['admin_password'] = admin_pass - - if operation == 'poweron': - d_vm['status'] = 'Powered on' - elif operation == 'poweroff': - d_vm['status'] = 'Powered off' - else: - d_vm['status'] = 'operate' - - vms = filter(lambda vm: vm['name'] == vm_name, vapp.get_vms_details()) - if len(vms) > 1: - module.fail_json(msg = "The vapp seems to have more than one vm with same name,\ - currently we only support a single vm deployment") - elif len(vms) == 0: - return False - - else: - vm = vms[0] - diff = [] - for i in VM_COMPARE_KEYS: - if not d_vm[i]: - continue - if vm[i] != d_vm[i]: - diff.append(i) - if len(diff) == 1 and 'status' in diff: - vapp_reconfigure(module, diff, vm, vca, vapp, vdc_name) - networks = vapp.get_vms_network_info() - if not network_name and len(networks) >=1: - if len(networks[0]) >= 1: - if networks[0][0]['network_name'] != 'none': - diff.append('network') - if not network_name: - if len(diff) == 0: - return True - if not networks[0] and network_name: - diff.append('network') - if networks[0]: - if len(networks[0]) >= 1: - if networks[0][0]['network_name'] != network_name: - diff.append('network') - if vm['status'] != 'Powered off': - if operation != 'poweroff' and len(diff) > 0: - module.fail_json(msg="To change any properties of a vm, The vm should be in Powered Off state") - if len(diff) == 0: - return True - else: - vapp_reconfigure(module, diff, vm, vca, vapp, vdc_name) + HAS_PYVCLOUD = False + +VAPP_STATE_MAP = { + 'poweron': 'Powered on', + 'poweroff': 'Powered off', + 'reboot': None, + 'reset': None, + 'shutdown': 'Powered off', + 'suspend': 'Suspended', + 'absent': None +} + +def modify_vapp(vapp, module): + vm_name = module.params['vm_name'] + vm_cpus = module.params['vm_cpus'] + vm_memory = module.params['vm_memory'] + + changed = False + + try: + vm = vapp.get_vms_details()[0] + except IndexError: + raise VcaError('No VM provisioned for vapp') + + if vm['status'] != 'Powered off': + raise VcaError('vApp must be powered off to modify') + + if vm_cpus != vm['cpus'] and vm_cpus is not None: + if not module.check_mode: + task = vapp.modify_vm_cpu(vm_name, vm_cpus) + changed = True + + if vm_memory != vm['memory_mb'] and vm_memory is not None: + if not module.check_mode: + task = vca.modify_vm_memory(vm_name, vm_memory) + changed = True + + return changed + + +def set_vapp_state(vapp, state): + vm = vapp.get_vms_details()[0] + try: + if vm['status'] != VAPP_STATE_MAP[state]: + func = getattr(vm, state) + func() + except KeyError: + raise VcaError('unknown vapp state', state=str(state), vm=str(vm)) + + +def create_vapp(vca, module): + vdc_name = module.params['vdc_name'] + vapp_name = module.params['vapp_name'] + template_name = module.params['template_name'] + catalog_name = module.params['catalog_name'] + network_name = module.params['network_name'] + network_mode = module.params['network_mode'] + vm_name = module.params['vm_name'] + vm_cpus = module.params['vm_cpus'] + vm_memory = module.params['vm_memory'] + deploy = module.params['deploy'] + + task = vca.create_vapp(vdc_name, vapp_name, template_name, catalog_name, + network_name, network_mode, vm_name, vm_cpus, + vm_memory, deploy, False) + + vca.block_until_completed(task) + + return vca.get_vapp(vca.get_vdc(vdc_name), vapp_name) + +def remove_vapp(vca, module): + vdc_name = module.params['vdc_name'] + vapp_name = module.params['vapp_name'] + if not vca.delete_vapp(vdc_name, vapp_name): + raise VcaError('unable to delete %s from %s' % (vapp_name, vdc_name)) + def main(): - module = AnsibleModule( - argument_spec=dict( - username = dict(default=None), - password = dict(default=None), - org = dict(default=None), - service_id = dict(default=None), - script = dict(default=None), - host = dict(default=None), - api_version = dict(default='5.7'), - service_type = dict(default='vca', choices=['vchs', 'vca', 'vcd']), - state = dict(default='present', choices = ['present', 'absent']), - catalog_name = dict(default="Public Catalog"), - template_name = dict(default=None, required=True), - network_name = dict(default=None), - network_ip = dict(default=None), - network_mode = dict(default='pool', choices=['dhcp', 'static', 'pool']), - instance_id = dict(default=None), - wait = dict(default=True, type='bool'), - wait_timeout = dict(default=250, type='int'), - vdc_name = dict(default=None), - vm_name = dict(default='default_ansible_vm1'), - vm_cpus = dict(default=None, type='int'), - verify_certs = dict(default=True, type='bool'), - vm_memory = dict(default=None, type='int'), - admin_password = dict(default=None), - operation = dict(default='poweroff', choices=['shutdown', 'poweroff', 'poweron', 'reboot', 'reset', 'suspend']) + argument_spec = vca_argument_spec() + argument_spec.update( + dict( + vdc_name=dict(requred=True), + vapp_name=dict(required=True), + template_name=dict(required=True), + catalog_name=dict(default='Public Catalog'), + network_name=dict(), + network_mode=dict(default='pool', choices=['dhcp', 'static', 'pool']), + vm_name=dict(), + vm_memory=dict(), + vm_cpus=dict(), + deploy=dict(default=False), + state=dict(default='poweron', choices=VAPP_STATE_MAP.keys()) ) ) + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - vdc_name = module.params.get('vdc_name') - vm_name = module.params.get('vm_name') - org = module.params.get('org') - service = module.params.get('service_id') - state = module.params.get('state') - service_type = module.params.get('service_type') - host = module.params.get('host') - instance_id = module.params.get('instance_id') - network_mode = module.params.get('network_mode') - network_ip = module.params.get('network_ip') - vapp_name = vm_name + vdc_name = module.params['vdc_name'] + vapp_name = module.params['vapp_name'] + state = module.params['state'] if not HAS_PYVCLOUD: module.fail_json(msg="python module pyvcloud is needed for this module") - if network_mode.upper() == 'STATIC': - if not network_ip: - module.fail_json(msg="if network_mode is STATIC, network_ip is mandatory") - - if service_type == 'vca': - if not instance_id: - module.fail_json(msg="When service type is vca the instance_id parameter is mandatory") - if not vdc_name: - module.fail_json(msg="When service type is vca the vdc_name parameter is mandatory") - - if service_type == 'vchs': - if not service: - module.fail_json(msg="When service type vchs the service_id parameter is mandatory") - if not org: - org = service - if not vdc_name: - vdc_name = service - if service_type == 'vcd': - if not host: - module.fail_json(msg="When service type is vcd host parameter is mandatory") - vca = vca_login(module) + vdc = vca.get_vdc(vdc_name) if not vdc: - module.fail_json(msg = "Error getting the vdc, Please check the vdc name") + module.fail_json(msg="Error getting the vdc, Please check the vdc name") + + result = dict(changed=False) + vapp = vca.get_vapp(vdc, vapp_name) - if vapp: - if state == 'absent': - task = vca.delete_vapp(vdc_name, vapp_name) - if not vca.block_until_completed(task): - module.fail_json(msg="failure in deleting vapp") - module.exit_json(changed=True, msg="Vapp deleted") - if vm_exists(module, vapp, vca, vdc_name ): - details = vm_details(vdc_name, vapp_name, vca) - module.exit_json(changed=False, msg="vapp exists", vm_details=details[0]) - else: - create_vm(vca, module) - if state == 'absent': - module.exit_json(changed=False, msg="Vapp does not exist") - create_vm(vca, module) - - - + + try: + if not vapp and state != 'absent': + if not module.check_mode: + vapp = create_vapp(vca, module) + set_vapp_state(vapp, state) + result['changed'] = True + elif vapp and state == 'absent': + if not module.check_mode: + remove_vapp(vca, module) + result['changed'] = True + elif vapp: + if not module.check_mode: + changed = modify_vapp(vapp, module) + set_vapp_state(vapp, state) + result['changed'] = True + except VcaError, e: + module.fail_json(msg=e.message, **e.kwargs) + except Exception, e: + module.fail_json(msg=e.message) + + module.exit_json(**result) + # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.vca import * + if __name__ == '__main__': - main() + main() From 5de4a64238d59623382c08c4030ed81a829045df Mon Sep 17 00:00:00 2001 From: Vlad Glagolev Date: Fri, 18 Sep 2015 10:32:10 +0300 Subject: [PATCH 0749/2522] fixed a typo in pacman module --- packaging/os/pacman.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/os/pacman.py b/packaging/os/pacman.py index 6d50fed7912..842676e51ae 100644 --- a/packaging/os/pacman.py +++ b/packaging/os/pacman.py @@ -245,7 +245,7 @@ def check_packages(module, pacman_path, packages, state): module.exit_json(changed=True, msg="%s package(s) would be %s" % ( len(would_be_changed), state)) else: - module.exit_json(change=False, msg="package(s) already %s" % state) + module.exit_json(changed=False, msg="package(s) already %s" % state) def main(): From c77a4a7108ec9674d0f4d5a367abe037346f3421 Mon Sep 17 00:00:00 2001 From: Michael Perzel Date: Fri, 18 Sep 2015 15:31:34 -0500 Subject: [PATCH 0750/2522] Wrap main() in conditional --- network/f5/bigip_gtm_wide_ip.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/network/f5/bigip_gtm_wide_ip.py b/network/f5/bigip_gtm_wide_ip.py index 29298114b83..c6a49f1fa5a 100644 --- a/network/f5/bigip_gtm_wide_ip.py +++ b/network/f5/bigip_gtm_wide_ip.py @@ -163,4 +163,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() From 45f45687536d45168d8ee40ffdf00020bc497dd4 Mon Sep 17 00:00:00 2001 From: knakayama Date: Sun, 20 Sep 2015 18:18:58 +0900 Subject: [PATCH 0751/2522] Fix argument type for zabbix_screen --- monitoring/zabbix_screen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/zabbix_screen.py b/monitoring/zabbix_screen.py index 12ef6c69b6f..2477314f733 100644 --- a/monitoring/zabbix_screen.py +++ b/monitoring/zabbix_screen.py @@ -319,7 +319,7 @@ def main(): login_user=dict(required=True), login_password=dict(required=True, no_log=True), timeout=dict(type='int', default=10), - screens=dict(type='dict', required=True) + screens=dict(type='list', required=True) ), supports_check_mode=True ) From bc560617c1d1383830930c532212875fe4edbc36 Mon Sep 17 00:00:00 2001 From: Rob Date: Mon, 21 Sep 2015 11:13:56 +1000 Subject: [PATCH 0752/2522] Tolerate missing classic_link_enabled attribute --- cloud/amazon/ec2_vpc_net_facts.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_vpc_net_facts.py b/cloud/amazon/ec2_vpc_net_facts.py index fa45c2635d3..538d39f3b41 100644 --- a/cloud/amazon/ec2_vpc_net_facts.py +++ b/cloud/amazon/ec2_vpc_net_facts.py @@ -64,9 +64,14 @@ def get_vpc_info(vpc): + try: + classic_link = vpc.classic_link_enabled + except AttributeError: + classic_link = False + vpc_info = { 'id': vpc.id, 'instance_tenancy': vpc.instance_tenancy, - 'classic_link_enabled': vpc.classic_link_enabled, + 'classic_link_enabled': classic_link, 'dhcp_options_id': vpc.dhcp_options_id, 'state': vpc.state, 'is_default': vpc.is_default, From af699f847510046fd514f606dc155c85405357e8 Mon Sep 17 00:00:00 2001 From: Michael Perzel Date: Mon, 21 Sep 2015 10:18:27 -0500 Subject: [PATCH 0753/2522] Improve example wide_ip variable --- network/f5/bigip_gtm_wide_ip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/f5/bigip_gtm_wide_ip.py b/network/f5/bigip_gtm_wide_ip.py index c6a49f1fa5a..120921b2f7c 100644 --- a/network/f5/bigip_gtm_wide_ip.py +++ b/network/f5/bigip_gtm_wide_ip.py @@ -70,7 +70,7 @@ user=admin password=mysecret lb_method=round_robin - wide_ip=my_wide_ip + wide_ip=my-wide-ip.example.com ''' try: From d3de924981ad489cfffdafb74de93dcc07cd631a Mon Sep 17 00:00:00 2001 From: Mark Chance Date: Mon, 21 Sep 2015 09:52:27 -0600 Subject: [PATCH 0754/2522] add author tag in doc --- cloud/amazon/ecs_service.py | 1 + cloud/amazon/ecs_service_facts.py | 1 + 2 files changed, 2 insertions(+) diff --git a/cloud/amazon/ecs_service.py b/cloud/amazon/ecs_service.py index cd2b046e1c1..f69a57ee5e9 100644 --- a/cloud/amazon/ecs_service.py +++ b/cloud/amazon/ecs_service.py @@ -26,6 +26,7 @@ dependecies: - An IAM role must have been created version_added: "2.0" +author: Mark Chance (@java1guy) options: ''' diff --git a/cloud/amazon/ecs_service_facts.py b/cloud/amazon/ecs_service_facts.py index 87d568de800..644f61a9c5d 100644 --- a/cloud/amazon/ecs_service_facts.py +++ b/cloud/amazon/ecs_service_facts.py @@ -21,6 +21,7 @@ description: - Lists or describes services in ecs. version_added: "2.0" +author: Mark Chance (@java1guy) options: details: description: From 1a653d49213fc1131bb14510623ea9c7aaeb6c49 Mon Sep 17 00:00:00 2001 From: Mark Chance Date: Mon, 21 Sep 2015 09:55:31 -0600 Subject: [PATCH 0755/2522] add author tag in doc --- cloud/amazon/ecs_task.py | 1 + cloud/amazon/ecs_task_facts.py | 1 + 2 files changed, 2 insertions(+) diff --git a/cloud/amazon/ecs_task.py b/cloud/amazon/ecs_task.py index 32bad410606..43b312974eb 100644 --- a/cloud/amazon/ecs_task.py +++ b/cloud/amazon/ecs_task.py @@ -21,6 +21,7 @@ description: - Creates or deletes instances of task definitions. version_added: "2.0" +author: Mark Chance(@Java1Guy) options: operation: description: diff --git a/cloud/amazon/ecs_task_facts.py b/cloud/amazon/ecs_task_facts.py index d5191a271c0..26d9d31ae51 100644 --- a/cloud/amazon/ecs_task_facts.py +++ b/cloud/amazon/ecs_task_facts.py @@ -21,6 +21,7 @@ description: - Describes or lists tasks. version_added: 2.0 +author: Mark Chance(@Java1Guy) options: details: description: From ff4c0004515b2e7fde3a248141c7cd572379f5f7 Mon Sep 17 00:00:00 2001 From: Mark Chance Date: Mon, 21 Sep 2015 09:56:32 -0600 Subject: [PATCH 0756/2522] add author tag in doc --- cloud/amazon/ecs_cluster.py | 1 + cloud/amazon/ecs_cluster_facts.py | 1 + 2 files changed, 2 insertions(+) diff --git a/cloud/amazon/ecs_cluster.py b/cloud/amazon/ecs_cluster.py index 9dc49860384..9dac7cd5bad 100644 --- a/cloud/amazon/ecs_cluster.py +++ b/cloud/amazon/ecs_cluster.py @@ -24,6 +24,7 @@ description: - Creates or terminates ecs clusters. version_added: "2.0" +author: Mark Chance(@Java1Guy) requirements: [ json, time, boto, boto3 ] options: state: diff --git a/cloud/amazon/ecs_cluster_facts.py b/cloud/amazon/ecs_cluster_facts.py index ec1a9209ef7..c4dff2706ad 100644 --- a/cloud/amazon/ecs_cluster_facts.py +++ b/cloud/amazon/ecs_cluster_facts.py @@ -21,6 +21,7 @@ description: - Lists or describes clusters or cluster instances in ecs. version_added: "2.0" +author: Mark Chance(@Java1Guy) options: details: description: From b9aef8beb2c1eb120240b22e458fa93a84f42293 Mon Sep 17 00:00:00 2001 From: Mark Chance Date: Mon, 21 Sep 2015 09:57:37 -0600 Subject: [PATCH 0757/2522] add author tag in doc --- cloud/amazon/ecs_taskdefinition.py | 1 + cloud/amazon/ecs_taskdefinition_facts.py | 1 + 2 files changed, 2 insertions(+) diff --git a/cloud/amazon/ecs_taskdefinition.py b/cloud/amazon/ecs_taskdefinition.py index 70bf59f7b6d..62d6d0cf929 100644 --- a/cloud/amazon/ecs_taskdefinition.py +++ b/cloud/amazon/ecs_taskdefinition.py @@ -21,6 +21,7 @@ description: - Creates or terminates task definitions version_added: "2.0" +author: Mark Chance(@Java1Guy) requirements: [ json, boto, botocore, boto3 ] options: state: diff --git a/cloud/amazon/ecs_taskdefinition_facts.py b/cloud/amazon/ecs_taskdefinition_facts.py index d99f563aeb6..48cdabd259f 100644 --- a/cloud/amazon/ecs_taskdefinition_facts.py +++ b/cloud/amazon/ecs_taskdefinition_facts.py @@ -21,6 +21,7 @@ description: - Describes or lists task definitions. version_added: 2.0 +author: Mark Chance(@Java1Guy) requirements: [ json, os, boto, botocore, boto3 ] options: details: From 401bb3f10da517878f792e9a25929df034c78d27 Mon Sep 17 00:00:00 2001 From: Gerard Lynch Date: Mon, 21 Sep 2015 20:28:42 +0100 Subject: [PATCH 0758/2522] minor doc fix --- cloud/amazon/ec2_vpc_route_table.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index 70f53bad26a..cc2b5ff8ee7 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -53,7 +53,7 @@ required: true tags: description: - - "A dictionary array of resource tags of the form: { tag1: value1, tag2: value2 }. Tags in this list are used to uniquely identify route tables within a VPC when the route_table_id is not supplied." + - "A dictionary of resource tags of the form: { tag1: value1, tag2: value2 }. Tags are used to uniquely identify route tables within a VPC when the route_table_id is not supplied." required: false default: null aliases: [ "resource_tags" ] @@ -89,7 +89,7 @@ vpc_id: vpc-1245678 region: us-west-1 tags: - - Name: Internal + Name: Internal subnets: - "{{ application_subnet.subnet_id }}" - 'Database Subnet' From 0f9066175212e9128ffd2e4a5ac8370177825e65 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Mon, 21 Sep 2015 07:17:16 -0400 Subject: [PATCH 0759/2522] cleaned up vca_vapp module to focus on managing vapps --- cloud/vmware/vca_vapp.py | 383 ++++++++++++++++++--------------------- 1 file changed, 174 insertions(+), 209 deletions(-) diff --git a/cloud/vmware/vca_vapp.py b/cloud/vmware/vca_vapp.py index 1fb27ef07b7..15bf4d31472 100644 --- a/cloud/vmware/vca_vapp.py +++ b/cloud/vmware/vca_vapp.py @@ -21,20 +21,82 @@ DOCUMENTATION = ''' --- module: vca_vapp -short_description: create, terminate, start or stop a vm in vca +short_description: Manages vCloud Air vApp instances. description: - - Creates or terminates vca vms. + - This module will actively managed vCloud Air vApp instances. Instances + can be created and deleted as well as both deployed and undeployed. version_added: "2.0" author: Peter Sprygada (@privateip) options: + vapp_name: + description: + - The name of the vCloud Air vApp instance + required: yes + vdc_name: + description: + - The name of the virtual data center (VDC) that contains the vAPP + required: yes + template_name: + description: + - The name of the vApp template to use to create the vApp instance. If + the I(state) is not `absent` then the I(template_name) value must be + provided. The I(template_name) must be previously uploaded to the + catalog specified by I(catalog_name) + required: no + default: None + network_name: + description: + - The name of the network that should be attached to the virtual machine + in the vApp. The virtual network specified must already be created in + the vCloud Air VDC. If the I(state) is not 'absent' then the + I(network_name) argument must be provided. + required: no + default: None + network_mode: + description: + - Configures the mode of the network connection. + required: no + default: pool + choices: ['pool', 'dhcp', 'static'] + vm_name: + description: + - The name of the virtual machine instance in the vApp to manage. + required: no + default: None + vm_cpus: + description: + - The number of vCPUs to configure for the VM in the vApp. If the + I(vm_name) argument is provided, then this becomes a per VM setting + otherwise it is applied to all VMs in the vApp. + required: no + default: None + vm_memory: + description: + - The amount of memory in MB to allocate to VMs in the vApp. If the + I(vm_name) argument is provided, then this becomes a per VM setting + otherise it is applied to all VMs in the vApp. + required: no + default: None + operation: + description: + - Specifies an operation to be performed on the vApp. + required: no + default: noop + choices: ['noop', 'poweron', 'poweroff', 'suspend', 'shutdown', 'reboot', 'reset'] + state: + description: + - Configures the state of the vApp. + required: no + default: present + choices: ['present', 'absent', 'deployed', 'undeployed'] username: description: - - The vca username or email address, if not set the environment variable VCA_USER is checked for the username. + - The vCloud Air username to use during authentication required: false default: None password: description: - - The vca password, if not set the environment variable VCA_PASS is checked for the password + - The vCloud Air password to use during authentication required: false default: None org: @@ -47,6 +109,11 @@ - The service id in a vchs environment to be used for creating the vapp required: false default: None + instance_id: + description: + - The vCloud Air instance ID + required: no + default: None host: description: - The authentication host to be used when service type is vcd. @@ -63,176 +130,54 @@ required: false default: vca choices: [ "vca", "vchs", "vcd" ] - state: - description: - - if the object should be added or removed - required: false - default: present - choices: [ "present", "absent" ] - catalog_name: - description: - - The catalog from which the vm template is used. - required: false - default: "Public Catalog" - script: - description: - - The path to script that gets injected to vm during creation. - required: false - default: "Public Catalog" - template_name: - description: - - The template name from which the vm should be created. - required: True - network_name: - description: - - The network name to which the vm should be attached. - required: false - default: 'None' - network_mode: - description: - - The network mode in which the ip should be allocated. - required: false - default: pool - choices: [ "pool", "dhcp", 'static' ] - instance_id:: - description: - - The instance id of the region in vca flavour where the vm should be created - required: false - default: None vdc_name: description: - The name of the vdc where the vm should be created. required: false default: None - vm_name: - description: - - The name of the vm to be created, the vapp is named the same as the vapp name - required: false - default: 'default_ansible_vm1' - vm_cpus: - description: - - The number if cpus to be added to the vm - required: false - default: None - vm_memory: - description: - - The amount of memory to be added to vm in megabytes - required: false - default: None ''' EXAMPLES = ''' -#Create a vm in an vca environment. The username password is not set as they are set in environment - -- hosts: localhost - connection: local - tasks: - - vca_vapp: - operation: poweroff - instance_id: 'b15ff1e5-1024-4f55-889f-ea0209726282' - vdc_name: 'benz_ansible' - vm_name: benz - vm_cpus: 2 - vm_memory: 1024 - network_mode: pool - template_name: "CentOS63-32BIT" - admin_password: "Password!123" - network_name: "default-routed-network" - -#Create a vm in a vchs environment. - -- hosts: localhost - connection: local - tasks: - - vca_app: - operation: poweron - service_id: '9-69' - vdc_name: 'Marketing' - service_type: 'vchs' - vm_name: benz - vm_cpus: 1 - script: "/tmp/configure_vm.sh" - catalog_name: "Marketing-Catalog" - template_name: "Marketing-Ubuntu-1204x64" - vm_memory: 512 - network_name: "M49-default-isolated" - -#create a vm in a vdc environment - -- hosts: localhost - connection: local - tasks: - - vca_vapp: - operation: poweron - org: IT20 - host: "mycloud.vmware.net" - api_version: "5.5" - service_type: vcd - vdc_name: 'IT20 Data Center (Beta)' - vm_name: benz - vm_cpus: 1 - catalog_name: "OS Templates" - template_name: "CentOS 6.5 64Bit CLI" - network_mode: pool +- name: Creates a new vApp in a VCA instance + vca_vapp: + vapp_name: tower + state=present + template_name='Ubuntu Server 12.04 LTS (amd64 20150127)' + vdc_name=VDC1 + instance_id= + username= + password= ''' -try: - from pyvcloud.vcloudair import VCA - HAS_PYVCLOUD = True -except ImportError: - HAS_PYVCLOUD = False - -VAPP_STATE_MAP = { - 'poweron': 'Powered on', - 'poweroff': 'Powered off', - 'reboot': None, - 'reset': None, - 'shutdown': 'Powered off', - 'suspend': 'Suspended', - 'absent': None -} - -def modify_vapp(vapp, module): - vm_name = module.params['vm_name'] - vm_cpus = module.params['vm_cpus'] - vm_memory = module.params['vm_memory'] - - changed = False - - try: - vm = vapp.get_vms_details()[0] - except IndexError: - raise VcaError('No VM provisioned for vapp') - - if vm['status'] != 'Powered off': - raise VcaError('vApp must be powered off to modify') +DEFAULT_VAPP_OPERATION = 'noop' - if vm_cpus != vm['cpus'] and vm_cpus is not None: - if not module.check_mode: - task = vapp.modify_vm_cpu(vm_name, vm_cpus) - changed = True - - if vm_memory != vm['memory_mb'] and vm_memory is not None: - if not module.check_mode: - task = vca.modify_vm_memory(vm_name, vm_memory) - changed = True +VAPP_STATUS = { + 'Powered off': 'poweroff', + 'Powered on': 'poweron', + 'Suspended': 'suspend' +} - return changed +VAPP_STATES = ['present', 'absent', 'deployed', 'undeployed'] +VAPP_OPERATIONS = ['poweron', 'poweroff', 'suspend', 'shutdown', + 'reboot', 'reset', 'noop'] -def set_vapp_state(vapp, state): - vm = vapp.get_vms_details()[0] +def get_instance(module): + vapp_name = module.params['vapp_name'] + inst = dict(vapp_name=vapp_name, state='absent') try: - if vm['status'] != VAPP_STATE_MAP[state]: - func = getattr(vm, state) - func() - except KeyError: - raise VcaError('unknown vapp state', state=str(state), vm=str(vm)) - - -def create_vapp(vca, module): + vapp = module.get_vapp(vapp_name) + if vapp: + status = module.vca.get_status(vapp.me.get_status()) + inst['status'] = VAPP_STATUS.get(status, 'unknown') + inst['state'] = 'deployed' if vapp.me.deployed else 'undeployed' + return inst + except VcaError: + return inst + +def create(module): vdc_name = module.params['vdc_name'] vapp_name = module.params['vapp_name'] template_name = module.params['template_name'] @@ -242,85 +187,105 @@ def create_vapp(vca, module): vm_name = module.params['vm_name'] vm_cpus = module.params['vm_cpus'] vm_memory = module.params['vm_memory'] - deploy = module.params['deploy'] - - task = vca.create_vapp(vdc_name, vapp_name, template_name, catalog_name, - network_name, network_mode, vm_name, vm_cpus, - vm_memory, deploy, False) + deploy = module.params['state'] == 'deploy' + poweron = module.params['operation'] == 'poweron' - vca.block_until_completed(task) + task = module.vca.create_vapp(vdc_name, vapp_name, template_name, + catalog_name, network_name, network_mode, + vm_name, vm_cpus, vm_memory, deploy, poweron) - return vca.get_vapp(vca.get_vdc(vdc_name), vapp_name) + module.vca.block_until_completed(task) -def remove_vapp(vca, module): +def delete(module): vdc_name = module.params['vdc_name'] vapp_name = module.params['vapp_name'] - if not vca.delete_vapp(vdc_name, vapp_name): - raise VcaError('unable to delete %s from %s' % (vapp_name, vdc_name)) + module.vca.delete_vapp(vdc_name, vapp_name) +def do_operation(module): + vapp_name = module.params['vapp_name'] + operation = module.params['operation'] -def main(): - argument_spec = vca_argument_spec() - argument_spec.update( - dict( - vdc_name=dict(requred=True), - vapp_name=dict(required=True), - template_name=dict(required=True), - catalog_name=dict(default='Public Catalog'), - network_name=dict(), - network_mode=dict(default='pool', choices=['dhcp', 'static', 'pool']), - vm_name=dict(), - vm_memory=dict(), - vm_cpus=dict(), - deploy=dict(default=False), - state=dict(default='poweron', choices=VAPP_STATE_MAP.keys()) - ) - ) + vm_name = module.params.get('vm_name') + vm = None + if vm_name: + vm = module.get_vm(vapp_name, vm_name) - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + if operation == 'poweron': + operation = 'powerOn' + elif operation == 'poweroff': + operation = 'powerOff' - vdc_name = module.params['vdc_name'] - vapp_name = module.params['vapp_name'] + cmd = 'power:%s' % operation + module.get_vapp(vapp_name).execute(cmd, 'post', targetVM=vm) + +def set_state(module): state = module.params['state'] + vapp = module.get_vapp(module.params['vapp_name']) + if state == 'deployed': + action = module.params['operation'] == 'poweron' + if not vapp.deploy(action): + module.fail('unable to deploy vapp') + elif state == 'undeployed': + action = module.params['operation'] + if action == 'poweroff': + action = 'powerOff' + elif action != 'suspend': + action = None + if not vapp.undeploy(action): + module.fail('unable to undeploy vapp') + + +def main(): + + argument_spec = dict( + vapp_name=dict(required=True), + vdc_name=dict(required=True), + template_name=dict(), + catalog_name=dict(default='Public Catalog'), + network_name=dict(), + network_mode=dict(default='pool', choices=['dhcp', 'static', 'pool']), + vm_name=dict(), + vm_cpus=dict(), + vm_memory=dict(), + operation=dict(default=DEFAULT_VAPP_OPERATION, choices=VAPP_OPERATIONS), + state=dict(default='present', choices=VAPP_STATES) + ) - if not HAS_PYVCLOUD: - module.fail_json(msg="python module pyvcloud is needed for this module") + module = VcaAnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) - vca = vca_login(module) + state = module.params['state'] + operation = module.params['operation'] - vdc = vca.get_vdc(vdc_name) - if not vdc: - module.fail_json(msg="Error getting the vdc, Please check the vdc name") + instance = get_instance(module) result = dict(changed=False) - vapp = vca.get_vapp(vdc, vapp_name) + if instance and state == 'absent': + if not module.check_mode: + delete(module) + result['changed'] = True - try: - if not vapp and state != 'absent': + elif state != 'absent': + if instance['state'] == 'absent': if not module.check_mode: - vapp = create_vapp(vca, module) - set_vapp_state(vapp, state) + create(module) result['changed'] = True - elif vapp and state == 'absent': + + elif instance['state'] != state and state != 'present': if not module.check_mode: - remove_vapp(vca, module) + set_state(module) result['changed'] = True - elif vapp: + + if operation != instance.get('status') and operation != 'noop': if not module.check_mode: - changed = modify_vapp(vapp, module) - set_vapp_state(vapp, state) + do_operation(module) result['changed'] = True - except VcaError, e: - module.fail_json(msg=e.message, **e.kwargs) - except Exception, e: - module.fail_json(msg=e.message) - module.exit_json(**result) + return module.exit(**result) # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.vca import * - if __name__ == '__main__': main() From 0712d2f55139802cb48008ecc2cc116a87c3c15d Mon Sep 17 00:00:00 2001 From: ToBeReplaced Date: Mon, 21 Sep 2015 22:50:52 -0600 Subject: [PATCH 0760/2522] Fix fail_json call in _mark_package_install --- packaging/os/dnf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index bb6a2c9d495..c4522f9105a 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -230,7 +230,7 @@ def _mark_package_install(module, base, pkg_spec): try: base.install(pkg_spec) except exceptions.MarkingError: - module.fail(msg="No package {} available.".format(pkg_spec)) + module.fail_json(msg="No package {} available.".format(pkg_spec)) def ensure(module, base, state, names): From faa16e32c1f361d02466d7f0dc02d57ad6859a9b Mon Sep 17 00:00:00 2001 From: Vlad Glagolev Date: Tue, 22 Sep 2015 17:44:08 +0300 Subject: [PATCH 0761/2522] fixed a typo in swdepot module --- packaging/os/swdepot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/os/swdepot.py b/packaging/os/swdepot.py index 157fa212c17..b14af742057 100644 --- a/packaging/os/swdepot.py +++ b/packaging/os/swdepot.py @@ -147,7 +147,7 @@ def main(): if not rc: changed = True - msg = "Packaged installed" + msg = "Package installed" else: module.fail_json(name=name, msg=output, rc=rc) From faa575afb54b7534d57c06b1ab6e56881eee0a3b Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Tue, 22 Sep 2015 16:28:14 +0200 Subject: [PATCH 0762/2522] cloudstack: implement common argument spec handling --- cloud/cloudstack/cs_account.py | 41 +++++------ cloud/cloudstack/cs_affinitygroup.py | 31 ++++----- cloud/cloudstack/cs_domain.py | 27 +++----- cloud/cloudstack/cs_firewall.py | 51 +++++++------- cloud/cloudstack/cs_instance.py | 83 +++++++++++------------ cloud/cloudstack/cs_instancegroup.py | 27 +++----- cloud/cloudstack/cs_ip_address.py | 33 ++++----- cloud/cloudstack/cs_iso.py | 45 ++++++------ cloud/cloudstack/cs_network.py | 70 ++++++++++--------- cloud/cloudstack/cs_portforward.py | 47 ++++++------- cloud/cloudstack/cs_project.py | 29 ++++---- cloud/cloudstack/cs_securitygroup.py | 25 +++---- cloud/cloudstack/cs_securitygroup_rule.py | 46 ++++++------- cloud/cloudstack/cs_sshkeypair.py | 29 ++++---- cloud/cloudstack/cs_staticnat.py | 35 ++++------ cloud/cloudstack/cs_template.py | 81 +++++++++++----------- cloud/cloudstack/cs_user.py | 37 +++++----- cloud/cloudstack/cs_vmsnapshot.py | 43 ++++++------ 18 files changed, 356 insertions(+), 424 deletions(-) diff --git a/cloud/cloudstack/cs_account.py b/cloud/cloudstack/cs_account.py index 2ffecf06fcf..839f6e53281 100644 --- a/cloud/cloudstack/cs_account.py +++ b/cloud/cloudstack/cs_account.py @@ -343,30 +343,25 @@ def get_result(self, account): def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + name = dict(required=True), + state = dict(choices=['present', 'absent', 'enabled', 'disabled', 'locked', 'unlocked'], default='present'), + account_type = dict(choices=['user', 'root_admin', 'domain_admin'], default='user'), + network_domain = dict(default=None), + domain = dict(default='ROOT'), + email = dict(default=None), + first_name = dict(default=None), + last_name = dict(default=None), + username = dict(default=None), + password = dict(default=None), + timezone = dict(default=None), + poll_async = dict(choices=BOOLEANS, default=True), + )) + module = AnsibleModule( - argument_spec = dict( - name = dict(required=True), - state = dict(choices=['present', 'absent', 'enabled', 'disabled', 'locked', 'unlocked'], default='present'), - account_type = dict(choices=['user', 'root_admin', 'domain_admin'], default='user'), - network_domain = dict(default=None), - domain = dict(default='ROOT'), - email = dict(default=None), - first_name = dict(default=None), - last_name = dict(default=None), - username = dict(default=None), - password = dict(default=None), - timezone = dict(default=None), - poll_async = dict(choices=BOOLEANS, default=True), - api_key = dict(default=None), - api_secret = dict(default=None, no_log=True), - api_url = dict(default=None), - api_http_method = dict(choices=['get', 'post'], default='get'), - api_timeout = dict(type='int', default=10), - api_region = dict(default='cloudstack'), - ), - required_together = ( - ['api_key', 'api_secret', 'api_url'], - ), + argument_spec=argument_spec, + required_together=cs_required_together(), supports_check_mode=True ) diff --git a/cloud/cloudstack/cs_affinitygroup.py b/cloud/cloudstack/cs_affinitygroup.py index 8a2f40fae62..5a7cb5f9714 100644 --- a/cloud/cloudstack/cs_affinitygroup.py +++ b/cloud/cloudstack/cs_affinitygroup.py @@ -201,25 +201,20 @@ def remove_affinity_group(self): def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + name = dict(required=True), + affinty_type = dict(default=None), + description = dict(default=None), + state = dict(choices=['present', 'absent'], default='present'), + domain = dict(default=None), + account = dict(default=None), + poll_async = dict(choices=BOOLEANS, default=True), + )) + module = AnsibleModule( - argument_spec = dict( - name = dict(required=True), - affinty_type = dict(default=None), - description = dict(default=None), - state = dict(choices=['present', 'absent'], default='present'), - domain = dict(default=None), - account = dict(default=None), - poll_async = dict(choices=BOOLEANS, default=True), - api_key = dict(default=None), - api_secret = dict(default=None, no_log=True), - api_url = dict(default=None), - api_http_method = dict(choices=['get', 'post'], default='get'), - api_timeout = dict(type='int', default=10), - api_region = dict(default='cloudstack'), - ), - required_together = ( - ['api_key', 'api_secret', 'api_url'], - ), + argument_spec=argument_spec, + required_together=cs_required_together(), supports_check_mode=True ) diff --git a/cloud/cloudstack/cs_domain.py b/cloud/cloudstack/cs_domain.py index 94299d5d6a3..0d8b7deaab4 100644 --- a/cloud/cloudstack/cs_domain.py +++ b/cloud/cloudstack/cs_domain.py @@ -239,23 +239,18 @@ def absent_domain(self): def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + path = dict(required=True), + state = dict(choices=['present', 'absent'], default='present'), + network_domain = dict(default=None), + clean_up = dict(choices=BOOLEANS, default=False), + poll_async = dict(choices=BOOLEANS, default=True), + )) + module = AnsibleModule( - argument_spec = dict( - path = dict(required=True), - state = dict(choices=['present', 'absent'], default='present'), - network_domain = dict(default=None), - clean_up = dict(choices=BOOLEANS, default=False), - poll_async = dict(choices=BOOLEANS, default=True), - api_key = dict(default=None), - api_secret = dict(default=None, no_log=True), - api_url = dict(default=None), - api_http_method = dict(choices=['get', 'post'], default='get'), - api_timeout = dict(type='int', default=10), - api_region = dict(default='cloudstack'), - ), - required_together = ( - ['api_key', 'api_secret', 'api_url'], - ), + argument_spec=argument_spec, + required_together=cs_required_together(), supports_check_mode=True ) diff --git a/cloud/cloudstack/cs_firewall.py b/cloud/cloudstack/cs_firewall.py index 59a2a57e9f2..4f4c1e7895a 100644 --- a/cloud/cloudstack/cs_firewall.py +++ b/cloud/cloudstack/cs_firewall.py @@ -392,36 +392,35 @@ def get_result(self, firewall_rule): def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + ip_address = dict(default=None), + network = dict(default=None), + cidr = dict(default='0.0.0.0/0'), + protocol = dict(choices=['tcp', 'udp', 'icmp', 'all'], default='tcp'), + type = dict(choices=['ingress', 'egress'], default='ingress'), + icmp_type = dict(type='int', default=None), + icmp_code = dict(type='int', default=None), + start_port = dict(type='int', aliases=['port'], default=None), + end_port = dict(type='int', default=None), + state = dict(choices=['present', 'absent'], default='present'), + domain = dict(default=None), + account = dict(default=None), + project = dict(default=None), + poll_async = dict(choices=BOOLEANS, default=True), + )) + + required_together = cs_required_together() + required_together.extend([ + ['icmp_type', 'icmp_code'], + ]) + module = AnsibleModule( - argument_spec = dict( - ip_address = dict(default=None), - network = dict(default=None), - cidr = dict(default='0.0.0.0/0'), - protocol = dict(choices=['tcp', 'udp', 'icmp', 'all'], default='tcp'), - type = dict(choices=['ingress', 'egress'], default='ingress'), - icmp_type = dict(type='int', default=None), - icmp_code = dict(type='int', default=None), - start_port = dict(type='int', aliases=['port'], default=None), - end_port = dict(type='int', default=None), - state = dict(choices=['present', 'absent'], default='present'), - domain = dict(default=None), - account = dict(default=None), - project = dict(default=None), - poll_async = dict(choices=BOOLEANS, default=True), - api_key = dict(default=None), - api_secret = dict(default=None, no_log=True), - api_url = dict(default=None), - api_http_method = dict(choices=['get', 'post'], default='get'), - api_timeout = dict(type='int', default=10), - api_region = dict(default='cloudstack'), - ), + argument_spec=argument_spec, + required_together=required_together, required_one_of = ( ['ip_address', 'network'], ), - required_together = ( - ['icmp_type', 'icmp_code'], - ['api_key', 'api_secret', 'api_url'], - ), mutually_exclusive = ( ['icmp_type', 'start_port'], ['icmp_type', 'end_port'], diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index f15241f5354..3af06382d6e 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -819,52 +819,51 @@ def get_result(self, instance): return self.result def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + name = dict(required=True), + display_name = dict(default=None), + group = dict(default=None), + state = dict(choices=['present', 'deployed', 'started', 'stopped', 'restarted', 'absent', 'destroyed', 'expunged'], default='present'), + service_offering = dict(default=None), + cpu = dict(default=None, type='int'), + cpu_speed = dict(default=None, type='int'), + memory = dict(default=None, type='int'), + template = dict(default=None), + iso = dict(default=None), + networks = dict(type='list', aliases=[ 'network' ], default=None), + ip_to_networks = dict(type='list', aliases=['ip_to_network'], default=None), + ip_address = dict(defaul=None), + ip6_address = dict(defaul=None), + disk_offering = dict(default=None), + disk_size = dict(type='int', default=None), + root_disk_size = dict(type='int', default=None), + keyboard = dict(choices=['de', 'de-ch', 'es', 'fi', 'fr', 'fr-be', 'fr-ch', 'is', 'it', 'jp', 'nl-be', 'no', 'pt', 'uk', 'us'], default=None), + hypervisor = dict(choices=['KVM', 'VMware', 'BareMetal', 'XenServer', 'LXC', 'HyperV', 'UCS', 'OVM', 'Simulator'], default=None), + security_groups = dict(type='list', aliases=[ 'security_group' ], default=[]), + affinity_groups = dict(type='list', aliases=[ 'affinity_group' ], default=[]), + domain = dict(default=None), + account = dict(default=None), + project = dict(default=None), + user_data = dict(default=None), + zone = dict(default=None), + ssh_key = dict(default=None), + force = dict(choices=BOOLEANS, default=False), + tags = dict(type='list', aliases=[ 'tag' ], default=None), + poll_async = dict(choices=BOOLEANS, default=True), + )) + + required_together = cs_required_together() + required_together.extend([ + ['cpu', 'cpu_speed', 'memory'], + ]) + module = AnsibleModule( - argument_spec = dict( - name = dict(required=True), - display_name = dict(default=None), - group = dict(default=None), - state = dict(choices=['present', 'deployed', 'started', 'stopped', 'restarted', 'absent', 'destroyed', 'expunged'], default='present'), - service_offering = dict(default=None), - cpu = dict(default=None, type='int'), - cpu_speed = dict(default=None, type='int'), - memory = dict(default=None, type='int'), - template = dict(default=None), - iso = dict(default=None), - networks = dict(type='list', aliases=[ 'network' ], default=None), - ip_to_networks = dict(type='list', aliases=['ip_to_network'], default=None), - ip_address = dict(defaul=None), - ip6_address = dict(defaul=None), - disk_offering = dict(default=None), - disk_size = dict(type='int', default=None), - root_disk_size = dict(type='int', default=None), - keyboard = dict(choices=['de', 'de-ch', 'es', 'fi', 'fr', 'fr-be', 'fr-ch', 'is', 'it', 'jp', 'nl-be', 'no', 'pt', 'uk', 'us'], default=None), - hypervisor = dict(choices=['KVM', 'VMware', 'BareMetal', 'XenServer', 'LXC', 'HyperV', 'UCS', 'OVM', 'Simulator'], default=None), - security_groups = dict(type='list', aliases=[ 'security_group' ], default=[]), - affinity_groups = dict(type='list', aliases=[ 'affinity_group' ], default=[]), - domain = dict(default=None), - account = dict(default=None), - project = dict(default=None), - user_data = dict(default=None), - zone = dict(default=None), - ssh_key = dict(default=None), - force = dict(choices=BOOLEANS, default=False), - tags = dict(type='list', aliases=[ 'tag' ], default=None), - poll_async = dict(choices=BOOLEANS, default=True), - api_key = dict(default=None), - api_secret = dict(default=None, no_log=True), - api_url = dict(default=None), - api_http_method = dict(choices=['get', 'post'], default='get'), - api_timeout = dict(type='int', default=10), - api_region = dict(default='cloudstack'), - ), + argument_spec=argument_spec, + required_together=required_together, mutually_exclusive = ( ['template', 'iso'], ), - required_together = ( - ['api_key', 'api_secret', 'api_url'], - ['cpu', 'cpu_speed', 'memory'], - ), supports_check_mode=True ) diff --git a/cloud/cloudstack/cs_instancegroup.py b/cloud/cloudstack/cs_instancegroup.py index f3ac8e4caa8..4ffda0ede1a 100644 --- a/cloud/cloudstack/cs_instancegroup.py +++ b/cloud/cloudstack/cs_instancegroup.py @@ -170,23 +170,18 @@ def absent_instance_group(self): def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + name = dict(required=True), + state = dict(default='present', choices=['present', 'absent']), + domain = dict(default=None), + account = dict(default=None), + project = dict(default=None), + )) + module = AnsibleModule( - argument_spec = dict( - name = dict(required=True), - state = dict(default='present', choices=['present', 'absent']), - domain = dict(default=None), - account = dict(default=None), - project = dict(default=None), - api_key = dict(default=None), - api_secret = dict(default=None, no_log=True), - api_url = dict(default=None), - api_http_method = dict(choices=['get', 'post'], default='get'), - api_timeout = dict(type='int', default=10), - api_region = dict(default='cloudstack'), - ), - required_together = ( - ['api_key', 'api_secret', 'api_url'], - ), + argument_spec=argument_spec, + required_together=cs_required_together(), supports_check_mode=True ) diff --git a/cloud/cloudstack/cs_ip_address.py b/cloud/cloudstack/cs_ip_address.py index 3e55b9f4be1..1be597fd8cb 100644 --- a/cloud/cloudstack/cs_ip_address.py +++ b/cloud/cloudstack/cs_ip_address.py @@ -224,26 +224,21 @@ def disassociate_ip_address(self): def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + ip_address = dict(required=False), + state = dict(choices=['present', 'absent'], default='present'), + zone = dict(default=None), + domain = dict(default=None), + account = dict(default=None), + network = dict(default=None), + project = dict(default=None), + poll_async = dict(choices=BOOLEANS, default=True), + )) + module = AnsibleModule( - argument_spec = dict( - ip_address = dict(required=False), - state = dict(choices=['present', 'absent'], default='present'), - zone = dict(default=None), - domain = dict(default=None), - account = dict(default=None), - network = dict(default=None), - project = dict(default=None), - poll_async = dict(choices=BOOLEANS, default=True), - api_key = dict(default=None), - api_secret = dict(default=None, no_log=True), - api_url = dict(default=None), - api_http_method = dict(choices=['get', 'post'], default='get'), - api_timeout = dict(type='int', default=10), - api_region = dict(default='cloudstack'), - ), - required_together = ( - ['api_key', 'api_secret', 'api_url'], - ), + argument_spec=argument_spec, + required_together=cs_required_together(), supports_check_mode=True ) diff --git a/cloud/cloudstack/cs_iso.py b/cloud/cloudstack/cs_iso.py index 4ce1804c762..98a06f6cd96 100644 --- a/cloud/cloudstack/cs_iso.py +++ b/cloud/cloudstack/cs_iso.py @@ -295,32 +295,27 @@ def remove_iso(self): def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + name = dict(required=True), + url = dict(default=None), + os_type = dict(default=None), + zone = dict(default=None), + iso_filter = dict(default='self', choices=[ 'featured', 'self', 'selfexecutable','sharedexecutable','executable', 'community' ]), + domain = dict(default=None), + account = dict(default=None), + project = dict(default=None), + checksum = dict(default=None), + is_ready = dict(choices=BOOLEANS, default=False), + bootable = dict(choices=BOOLEANS, default=True), + is_featured = dict(choices=BOOLEANS, default=False), + is_dynamically_scalable = dict(choices=BOOLEANS, default=False), + state = dict(choices=['present', 'absent'], default='present'), + )) + module = AnsibleModule( - argument_spec = dict( - name = dict(required=True), - url = dict(default=None), - os_type = dict(default=None), - zone = dict(default=None), - iso_filter = dict(default='self', choices=[ 'featured', 'self', 'selfexecutable','sharedexecutable','executable', 'community' ]), - domain = dict(default=None), - account = dict(default=None), - project = dict(default=None), - checksum = dict(default=None), - is_ready = dict(choices=BOOLEANS, default=False), - bootable = dict(choices=BOOLEANS, default=True), - is_featured = dict(choices=BOOLEANS, default=False), - is_dynamically_scalable = dict(choices=BOOLEANS, default=False), - state = dict(choices=['present', 'absent'], default='present'), - api_key = dict(default=None), - api_secret = dict(default=None, no_log=True), - api_url = dict(default=None), - api_http_method = dict(choices=['get', 'post'], default='get'), - api_timeout = dict(type='int', default=10), - api_region = dict(default='cloudstack'), - ), - required_together = ( - ['api_key', 'api_secret', 'api_url'], - ), + argument_spec=argument_spec, + required_together=cs_required_together(), supports_check_mode=True ) diff --git a/cloud/cloudstack/cs_network.py b/cloud/cloudstack/cs_network.py index 1cb97bf86ea..6dea3dd3ca6 100644 --- a/cloud/cloudstack/cs_network.py +++ b/cloud/cloudstack/cs_network.py @@ -522,43 +522,41 @@ def absent_network(self): def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + name = dict(required=True), + display_text = dict(default=None), + network_offering = dict(default=None), + zone = dict(default=None), + start_ip = dict(default=None), + end_ip = dict(default=None), + gateway = dict(default=None), + netmask = dict(default=None), + start_ipv6 = dict(default=None), + end_ipv6 = dict(default=None), + cidr_ipv6 = dict(default=None), + gateway_ipv6 = dict(default=None), + vlan = dict(default=None), + vpc = dict(default=None), + isolated_pvlan = dict(default=None), + clean_up = dict(type='bool', choices=BOOLEANS, default=False), + network_domain = dict(default=None), + state = dict(choices=['present', 'absent', 'restarted' ], default='present'), + acl_type = dict(choices=['account', 'domain'], default='account'), + project = dict(default=None), + domain = dict(default=None), + account = dict(default=None), + poll_async = dict(type='bool', choices=BOOLEANS, default=True), + )) + required_together = cs_required_together() + required_together.extend([ + ['start_ip', 'netmask', 'gateway'], + ['start_ipv6', 'cidr_ipv6', 'gateway_ipv6'], + ]) + module = AnsibleModule( - argument_spec = dict( - name = dict(required=True), - display_text = dict(default=None), - network_offering = dict(default=None), - zone = dict(default=None), - start_ip = dict(default=None), - end_ip = dict(default=None), - gateway = dict(default=None), - netmask = dict(default=None), - start_ipv6 = dict(default=None), - end_ipv6 = dict(default=None), - cidr_ipv6 = dict(default=None), - gateway_ipv6 = dict(default=None), - vlan = dict(default=None), - vpc = dict(default=None), - isolated_pvlan = dict(default=None), - clean_up = dict(type='bool', choices=BOOLEANS, default=False), - network_domain = dict(default=None), - state = dict(choices=['present', 'absent', 'restarted' ], default='present'), - acl_type = dict(choices=['account', 'domain'], default='account'), - project = dict(default=None), - domain = dict(default=None), - account = dict(default=None), - poll_async = dict(type='bool', choices=BOOLEANS, default=True), - api_key = dict(default=None), - api_secret = dict(default=None, no_log=True), - api_url = dict(default=None), - api_http_method = dict(choices=['get', 'post'], default='get'), - api_timeout = dict(type='int', default=10), - api_region = dict(default='cloudstack'), - ), - required_together = ( - ['api_key', 'api_secret', 'api_url'], - ['start_ip', 'netmask', 'gateway'], - ['start_ipv6', 'cidr_ipv6', 'gateway_ipv6'], - ), + argument_spec=argument_spec, + required_together=required_together, supports_check_mode=True ) diff --git a/cloud/cloudstack/cs_portforward.py b/cloud/cloudstack/cs_portforward.py index df867915a07..555f30e54b3 100644 --- a/cloud/cloudstack/cs_portforward.py +++ b/cloud/cloudstack/cs_portforward.py @@ -368,33 +368,28 @@ def get_result(self, portforwarding_rule): def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + ip_address = dict(required=True), + protocol= dict(choices=['tcp', 'udp'], default='tcp'), + public_port = dict(type='int', required=True), + public_end_port = dict(type='int', default=None), + private_port = dict(type='int', required=True), + private_end_port = dict(type='int', default=None), + state = dict(choices=['present', 'absent'], default='present'), + open_firewall = dict(choices=BOOLEANS, default=False), + vm_guest_ip = dict(default=None), + vm = dict(default=None), + zone = dict(default=None), + domain = dict(default=None), + account = dict(default=None), + project = dict(default=None), + poll_async = dict(choices=BOOLEANS, default=True), + )) + module = AnsibleModule( - argument_spec = dict( - ip_address = dict(required=True), - protocol= dict(choices=['tcp', 'udp'], default='tcp'), - public_port = dict(type='int', required=True), - public_end_port = dict(type='int', default=None), - private_port = dict(type='int', required=True), - private_end_port = dict(type='int', default=None), - state = dict(choices=['present', 'absent'], default='present'), - open_firewall = dict(choices=BOOLEANS, default=False), - vm_guest_ip = dict(default=None), - vm = dict(default=None), - zone = dict(default=None), - domain = dict(default=None), - account = dict(default=None), - project = dict(default=None), - poll_async = dict(choices=BOOLEANS, default=True), - api_key = dict(default=None), - api_secret = dict(default=None, no_log=True), - api_url = dict(default=None), - api_http_method = dict(choices=['get', 'post'], default='get'), - api_timeout = dict(type='int', default=10), - api_region = dict(default='cloudstack'), - ), - required_together = ( - ['api_key', 'api_secret', 'api_url'], - ), + argument_spec=argument_spec, + required_together=cs_required_together(), supports_check_mode=True ) diff --git a/cloud/cloudstack/cs_project.py b/cloud/cloudstack/cs_project.py index a7468e63118..504fefc6f0c 100644 --- a/cloud/cloudstack/cs_project.py +++ b/cloud/cloudstack/cs_project.py @@ -259,24 +259,19 @@ def absent_project(self): def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + name = dict(required=True), + display_text = dict(default=None), + state = dict(choices=['present', 'absent', 'active', 'suspended' ], default='present'), + domain = dict(default=None), + account = dict(default=None), + poll_async = dict(type='bool', choices=BOOLEANS, default=True), + )) + module = AnsibleModule( - argument_spec = dict( - name = dict(required=True), - display_text = dict(default=None), - state = dict(choices=['present', 'absent', 'active', 'suspended' ], default='present'), - domain = dict(default=None), - account = dict(default=None), - poll_async = dict(type='bool', choices=BOOLEANS, default=True), - api_key = dict(default=None), - api_secret = dict(default=None, no_log=True), - api_url = dict(default=None), - api_http_method = dict(choices=['get', 'post'], default='get'), - api_timeout = dict(type='int', default=10), - api_region = dict(default='cloudstack'), - ), - required_together = ( - ['api_key', 'api_secret', 'api_url'], - ), + argument_spec=argument_spec, + required_together=cs_required_together(), supports_check_mode=True ) diff --git a/cloud/cloudstack/cs_securitygroup.py b/cloud/cloudstack/cs_securitygroup.py index 3bae64b4dd9..255d306c789 100644 --- a/cloud/cloudstack/cs_securitygroup.py +++ b/cloud/cloudstack/cs_securitygroup.py @@ -152,22 +152,17 @@ def remove_security_group(self): def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + name = dict(required=True), + description = dict(default=None), + state = dict(choices=['present', 'absent'], default='present'), + project = dict(default=None), + )) + module = AnsibleModule( - argument_spec = dict( - name = dict(required=True), - description = dict(default=None), - state = dict(choices=['present', 'absent'], default='present'), - project = dict(default=None), - api_key = dict(default=None), - api_secret = dict(default=None, no_log=True), - api_url = dict(default=None), - api_http_method = dict(choices=['get', 'post'], default='get'), - api_timeout = dict(type='int', default=10), - api_region = dict(default='cloudstack'), - ), - required_together = ( - ['api_key', 'api_secret', 'api_url'], - ), + argument_spec=argument_spec, + required_together=cs_required_together(), supports_check_mode=True ) diff --git a/cloud/cloudstack/cs_securitygroup_rule.py b/cloud/cloudstack/cs_securitygroup_rule.py index 2c75a83d4b3..69e04ee7f92 100644 --- a/cloud/cloudstack/cs_securitygroup_rule.py +++ b/cloud/cloudstack/cs_securitygroup_rule.py @@ -371,31 +371,29 @@ def get_result(self, security_group_rule): def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + security_group = dict(required=True), + type = dict(choices=['ingress', 'egress'], default='ingress'), + cidr = dict(default='0.0.0.0/0'), + user_security_group = dict(default=None), + protocol = dict(choices=['tcp', 'udp', 'icmp', 'ah', 'esp', 'gre'], default='tcp'), + icmp_type = dict(type='int', default=None), + icmp_code = dict(type='int', default=None), + start_port = dict(type='int', default=None, aliases=['port']), + end_port = dict(type='int', default=None), + state = dict(choices=['present', 'absent'], default='present'), + project = dict(default=None), + poll_async = dict(choices=BOOLEANS, default=True), + )) + required_together = cs_required_together() + required_together.extend([ + ['icmp_type', 'icmp_code'], + ]) + module = AnsibleModule( - argument_spec = dict( - security_group = dict(required=True), - type = dict(choices=['ingress', 'egress'], default='ingress'), - cidr = dict(default='0.0.0.0/0'), - user_security_group = dict(default=None), - protocol = dict(choices=['tcp', 'udp', 'icmp', 'ah', 'esp', 'gre'], default='tcp'), - icmp_type = dict(type='int', default=None), - icmp_code = dict(type='int', default=None), - start_port = dict(type='int', default=None, aliases=['port']), - end_port = dict(type='int', default=None), - state = dict(choices=['present', 'absent'], default='present'), - project = dict(default=None), - poll_async = dict(choices=BOOLEANS, default=True), - api_key = dict(default=None), - api_secret = dict(default=None, no_log=True), - api_url = dict(default=None), - api_http_method = dict(choices=['get', 'post'], default='get'), - api_timeout = dict(type='int', default=10), - api_region = dict(default='cloudstack'), - ), - required_together = ( - ['icmp_type', 'icmp_code'], - ['api_key', 'api_secret', 'api_url'], - ), + argument_spec=argument_spec, + required_together=required_together, mutually_exclusive = ( ['icmp_type', 'start_port'], ['icmp_type', 'end_port'], diff --git a/cloud/cloudstack/cs_sshkeypair.py b/cloud/cloudstack/cs_sshkeypair.py index a6576404c3c..7e665cd62f6 100644 --- a/cloud/cloudstack/cs_sshkeypair.py +++ b/cloud/cloudstack/cs_sshkeypair.py @@ -205,24 +205,19 @@ def _get_ssh_fingerprint(self, public_key): def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + name = dict(required=True), + public_key = dict(default=None), + domain = dict(default=None), + account = dict(default=None), + project = dict(default=None), + state = dict(choices=['present', 'absent'], default='present'), + )) + module = AnsibleModule( - argument_spec = dict( - name = dict(required=True), - public_key = dict(default=None), - domain = dict(default=None), - account = dict(default=None), - project = dict(default=None), - state = dict(choices=['present', 'absent'], default='present'), - api_key = dict(default=None), - api_secret = dict(default=None, no_log=True), - api_url = dict(default=None), - api_http_method = dict(choices=['get', 'post'], default='get'), - api_timeout = dict(type='int', default=10), - api_region = dict(default='cloudstack'), - ), - required_together = ( - ['api_key', 'api_secret', 'api_url'], - ), + argument_spec=argument_spec, + required_together=cs_required_together(), supports_check_mode=True ) diff --git a/cloud/cloudstack/cs_staticnat.py b/cloud/cloudstack/cs_staticnat.py index c42b743d51c..c8fba54885e 100644 --- a/cloud/cloudstack/cs_staticnat.py +++ b/cloud/cloudstack/cs_staticnat.py @@ -259,27 +259,22 @@ def absent_static_nat(self): def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + ip_address = dict(required=True), + vm = dict(default=None), + vm_guest_ip = dict(default=None), + state = dict(choices=['present', 'absent'], default='present'), + zone = dict(default=None), + domain = dict(default=None), + account = dict(default=None), + project = dict(default=None), + poll_async = dict(choices=BOOLEANS, default=True), + )) + module = AnsibleModule( - argument_spec = dict( - ip_address = dict(required=True), - vm = dict(default=None), - vm_guest_ip = dict(default=None), - state = dict(choices=['present', 'absent'], default='present'), - zone = dict(default=None), - domain = dict(default=None), - account = dict(default=None), - project = dict(default=None), - poll_async = dict(choices=BOOLEANS, default=True), - api_key = dict(default=None), - api_secret = dict(default=None, no_log=True), - api_url = dict(default=None), - api_http_method = dict(choices=['get', 'post'], default='get'), - api_timeout = dict(type='int', default=10), - api_region = dict(default='cloudstack'), - ), - required_together = ( - ['api_key', 'api_secret', 'api_url'], - ), + argument_spec=argument_spec, + required_together=cs_required_together(), supports_check_mode=True ) diff --git a/cloud/cloudstack/cs_template.py b/cloud/cloudstack/cs_template.py index fbaa5665eb2..94803aeb9eb 100644 --- a/cloud/cloudstack/cs_template.py +++ b/cloud/cloudstack/cs_template.py @@ -529,51 +529,50 @@ def remove_template(self): def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + name = dict(required=True), + display_text = dict(default=None), + url = dict(default=None), + vm = dict(default=None), + snapshot = dict(default=None), + os_type = dict(default=None), + is_ready = dict(type='bool', choices=BOOLEANS, default=False), + is_public = dict(type='bool', choices=BOOLEANS, default=True), + is_featured = dict(type='bool', choices=BOOLEANS, default=False), + is_dynamically_scalable = dict(type='bool', choices=BOOLEANS, default=False), + is_extractable = dict(type='bool', choices=BOOLEANS, default=False), + is_routing = dict(type='bool', choices=BOOLEANS, default=False), + checksum = dict(default=None), + template_filter = dict(default='self', choices=['featured', 'self', 'selfexecutable', 'sharedexecutable', 'executable', 'community']), + hypervisor = dict(choices=['KVM', 'VMware', 'BareMetal', 'XenServer', 'LXC', 'HyperV', 'UCS', 'OVM', 'Simulator'], default=None), + requires_hvm = dict(type='bool', choices=BOOLEANS, default=False), + password_enabled = dict(type='bool', choices=BOOLEANS, default=False), + template_tag = dict(default=None), + sshkey_enabled = dict(type='bool', choices=BOOLEANS, default=False), + format = dict(choices=['QCOW2', 'RAW', 'VHD', 'OVA'], default=None), + details = dict(default=None), + bits = dict(type='int', choices=[ 32, 64 ], default=64), + state = dict(choices=['present', 'absent'], default='present'), + cross_zones = dict(type='bool', choices=BOOLEANS, default=False), + zone = dict(default=None), + domain = dict(default=None), + account = dict(default=None), + project = dict(default=None), + poll_async = dict(type='bool', choices=BOOLEANS, default=True), + )) + + required_together = cs_required_together() + required_together.extend([ + ['format', 'url', 'hypervisor'], + ]) + module = AnsibleModule( - argument_spec = dict( - name = dict(required=True), - display_text = dict(default=None), - url = dict(default=None), - vm = dict(default=None), - snapshot = dict(default=None), - os_type = dict(default=None), - is_ready = dict(type='bool', choices=BOOLEANS, default=False), - is_public = dict(type='bool', choices=BOOLEANS, default=True), - is_featured = dict(type='bool', choices=BOOLEANS, default=False), - is_dynamically_scalable = dict(type='bool', choices=BOOLEANS, default=False), - is_extractable = dict(type='bool', choices=BOOLEANS, default=False), - is_routing = dict(type='bool', choices=BOOLEANS, default=False), - checksum = dict(default=None), - template_filter = dict(default='self', choices=['featured', 'self', 'selfexecutable', 'sharedexecutable', 'executable', 'community']), - hypervisor = dict(choices=['KVM', 'VMware', 'BareMetal', 'XenServer', 'LXC', 'HyperV', 'UCS', 'OVM', 'Simulator'], default=None), - requires_hvm = dict(type='bool', choices=BOOLEANS, default=False), - password_enabled = dict(type='bool', choices=BOOLEANS, default=False), - template_tag = dict(default=None), - sshkey_enabled = dict(type='bool', choices=BOOLEANS, default=False), - format = dict(choices=['QCOW2', 'RAW', 'VHD', 'OVA'], default=None), - details = dict(default=None), - bits = dict(type='int', choices=[ 32, 64 ], default=64), - state = dict(choices=['present', 'absent'], default='present'), - cross_zones = dict(type='bool', choices=BOOLEANS, default=False), - zone = dict(default=None), - domain = dict(default=None), - account = dict(default=None), - project = dict(default=None), - poll_async = dict(type='bool', choices=BOOLEANS, default=True), - api_key = dict(default=None), - api_secret = dict(default=None), - api_url = dict(default=None), - api_http_method = dict(choices=['get', 'post'], default='get'), - api_timeout = dict(type='int', default=10), - api_region = dict(default='cloudstack'), - ), + argument_spec=argument_spec, + required_together=required_together, mutually_exclusive = ( ['url', 'vm'], ), - required_together = ( - ['api_key', 'api_secret', 'api_url'], - ['format', 'url', 'hypervisor'], - ), supports_check_mode=True ) diff --git a/cloud/cloudstack/cs_user.py b/cloud/cloudstack/cs_user.py index 43e83c06784..e6fe1c1f513 100644 --- a/cloud/cloudstack/cs_user.py +++ b/cloud/cloudstack/cs_user.py @@ -404,28 +404,23 @@ def get_result(self, user): def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + username = dict(required=True), + account = dict(default=None), + state = dict(choices=['present', 'absent', 'enabled', 'disabled', 'locked', 'unlocked'], default='present'), + domain = dict(default='ROOT'), + email = dict(default=None), + first_name = dict(default=None), + last_name = dict(default=None), + password = dict(default=None), + timezone = dict(default=None), + poll_async = dict(choices=BOOLEANS, default=True), + )) + module = AnsibleModule( - argument_spec = dict( - username = dict(required=True), - account = dict(default=None), - state = dict(choices=['present', 'absent', 'enabled', 'disabled', 'locked', 'unlocked'], default='present'), - domain = dict(default='ROOT'), - email = dict(default=None), - first_name = dict(default=None), - last_name = dict(default=None), - password = dict(default=None), - timezone = dict(default=None), - poll_async = dict(choices=BOOLEANS, default=True), - api_key = dict(default=None), - api_secret = dict(default=None, no_log=True), - api_url = dict(default=None), - api_http_method = dict(choices=['get', 'post'], default='get'), - api_timeout = dict(type='int', default=10), - api_region = dict(default='cloudstack'), - ), - required_together = ( - ['api_key', 'api_secret', 'api_url'], - ), + argument_spec=argument_spec, + required_together=cs_required_together(), supports_check_mode=True ) diff --git a/cloud/cloudstack/cs_vmsnapshot.py b/cloud/cloudstack/cs_vmsnapshot.py index 9b87c1c3567..43e561bb93a 100644 --- a/cloud/cloudstack/cs_vmsnapshot.py +++ b/cloud/cloudstack/cs_vmsnapshot.py @@ -257,29 +257,28 @@ def revert_vm_to_snapshot(self): def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + name = dict(required=True, aliases=['display_name']), + vm = dict(required=True), + description = dict(default=None), + zone = dict(default=None), + snapshot_memory = dict(choices=BOOLEANS, default=False), + state = dict(choices=['present', 'absent', 'revert'], default='present'), + domain = dict(default=None), + account = dict(default=None), + project = dict(default=None), + poll_async = dict(type='bool', choices=BOOLEANS, default=True), + )) + + required_together = cs_required_together() + required_together.extend([ + ['icmp_type', 'icmp_code'], + ]) + module = AnsibleModule( - argument_spec = dict( - name = dict(required=True, aliases=['display_name']), - vm = dict(required=True), - description = dict(default=None), - zone = dict(default=None), - snapshot_memory = dict(choices=BOOLEANS, default=False), - state = dict(choices=['present', 'absent', 'revert'], default='present'), - domain = dict(default=None), - account = dict(default=None), - project = dict(default=None), - poll_async = dict(choices=BOOLEANS, default=True), - api_key = dict(default=None), - api_secret = dict(default=None, no_log=True), - api_url = dict(default=None), - api_http_method = dict(choices=['get', 'post'], default='get'), - api_timeout = dict(type='int', default=10), - api_region = dict(default='cloudstack'), - ), - required_together = ( - ['icmp_type', 'icmp_code'], - ['api_key', 'api_secret', 'api_url'], - ), + argument_spec=argument_spec, + required_together=required_together, supports_check_mode=True ) From bcfe75a52d473f461b2320008f8336d1f1b917f4 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Tue, 22 Sep 2015 18:28:18 +0200 Subject: [PATCH 0763/2522] cloudstack: fix redundant security_group return --- cloud/cloudstack/cs_instance.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index f15241f5354..15fd828554f 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -402,7 +402,6 @@ def __init__(self, module): 'isoname': 'iso', 'templatename': 'template', 'keypair': 'ssh_key', - 'securitygroup': 'security_group', } self.instance = None self.template = None From c7c32ef86f2b2e550201e932fe496fe29b505c45 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 22 Sep 2015 13:58:36 -0400 Subject: [PATCH 0764/2522] minor doc fixes nagios --- monitoring/nagios.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/monitoring/nagios.py b/monitoring/nagios.py index dc87c0a018f..ee67a3ae20b 100644 --- a/monitoring/nagios.py +++ b/monitoring/nagios.py @@ -32,7 +32,6 @@ - Action to take. - servicegroup options were added in 2.0. required: true - default: null choices: [ "downtime", "enable_alerts", "disable_alerts", "silence", "unsilence", "silence_nagios", "unsilence_nagios", "command", "servicegroup_service_downtime", "servicegroup_host_downtime" ] @@ -72,7 +71,6 @@ B(Required) option when using the C(downtime), C(enable_alerts), and C(disable_alerts) actions. aliases: [ "service" ] required: true - default: null servicegroup: version_added: "2.0" description: @@ -84,10 +82,8 @@ should not include the submitted time header or the line-feed B(Required) option when using the C(command) action. required: true - default: null author: "Tim Bielawa (@tbielawa)" -requirements: [ "Nagios" ] ''' EXAMPLES = ''' From 8989212fd2917e043579f14aaf6eb780b99f282b Mon Sep 17 00:00:00 2001 From: Milad Soufastai Date: Tue, 22 Sep 2015 18:53:58 -0400 Subject: [PATCH 0765/2522] Adding the --sudo flag for CPANM use since the sudo: yes on the task doesn't work --- packaging/language/cpanm.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packaging/language/cpanm.py b/packaging/language/cpanm.py index 0bee74de4cc..af7224ce5ce 100644 --- a/packaging/language/cpanm.py +++ b/packaging/language/cpanm.py @@ -64,6 +64,12 @@ required: false default: false version_added: "2.0" + use_sudo: + description: + - Use sudo flag for cpanm + required: false + default: false + version_added: "2.0" notes: - Please note that U(http://search.cpan.org/dist/App-cpanminus/bin/cpanm, cpanm) must be installed on the remote host. author: "Franck Cuny (@franckcuny)" @@ -87,6 +93,9 @@ # install Dancer perl package from a specific mirror - cpanm: name=Dancer mirror=http://cpan.cpantesters.org/ + +# install Dancer perl package using --sudo flag +- cpanm: name=Dancer use_sudo=yes ''' def _is_package_installed(module, name, locallib, cpanm): @@ -124,6 +133,9 @@ def _build_cmd_line(name, from_path, notest, locallib, mirror, mirror_only, if installdeps is True: cmd = "{cmd} --installdeps".format(cmd=cmd) + if use_sudo is True: + cmd = "{cmd} --sudo".format(cmd=cmd) + return cmd @@ -136,6 +148,7 @@ def main(): mirror=dict(default=None, required=False), mirror_only=dict(default=False, type='bool'), installdeps=dict(default=False, type='bool'), + use_sudo=dict(default=False, type='bool'), ) module = AnsibleModule( @@ -151,6 +164,7 @@ def main(): mirror = module.params['mirror'] mirror_only = module.params['mirror_only'] installdeps = module.params['installdeps'] + use_sudo = module.params['use_sudo'] changed = False From 6cd7399a71aa9f3699046d5995fb0b36241d1c3d Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 22 Sep 2015 21:02:40 -0400 Subject: [PATCH 0766/2522] minor docfix --- monitoring/zabbix_screen.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monitoring/zabbix_screen.py b/monitoring/zabbix_screen.py index 2477314f733..1896899c3a3 100644 --- a/monitoring/zabbix_screen.py +++ b/monitoring/zabbix_screen.py @@ -27,7 +27,7 @@ description: - This module allows you to create, modify and delete Zabbix screens and associated graph data. version_added: "2.0" -author: +author: - "(@cove)" - "Tony Minfei Ding" - "Harrison Gu (@harrisongu)" @@ -52,7 +52,7 @@ description: - The timeout of API request (seconds). default: 10 - zabbix_screens: + screens: description: - List of screens to be created/updated/deleted(see example). - If the screen(s) already been added, the screen(s) name won't be updated. From 2d6303b3681043578da682f7eb1691f0ae4bee04 Mon Sep 17 00:00:00 2001 From: TimothyVandenbrande Date: Wed, 23 Sep 2015 09:35:17 +0200 Subject: [PATCH 0767/2522] upon request, added the license --- windows/win_firewall_rule.ps1 | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/windows/win_firewall_rule.ps1 b/windows/win_firewall_rule.ps1 index d19082e6690..223d8b17b69 100644 --- a/windows/win_firewall_rule.ps1 +++ b/windows/win_firewall_rule.ps1 @@ -1,5 +1,22 @@ #!powershell # +# (c) 2014, Timothy Vandenbrande +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# # WANT_JSON # POWERSHELL_COMMON From b5207d0fc65eb985f27820bb29200e956c9a6717 Mon Sep 17 00:00:00 2001 From: Milad Soufastai Date: Wed, 23 Sep 2015 09:40:36 -0400 Subject: [PATCH 0768/2522] Updating Description to provide better explanation of what the use_sudo flag does --- packaging/language/cpanm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packaging/language/cpanm.py b/packaging/language/cpanm.py index af7224ce5ce..82a6b5d0a0c 100644 --- a/packaging/language/cpanm.py +++ b/packaging/language/cpanm.py @@ -66,7 +66,7 @@ version_added: "2.0" use_sudo: description: - - Use sudo flag for cpanm + - Use this if you want to install modules to the system perl include path. required: false default: false version_added: "2.0" @@ -94,7 +94,7 @@ # install Dancer perl package from a specific mirror - cpanm: name=Dancer mirror=http://cpan.cpantesters.org/ -# install Dancer perl package using --sudo flag +# install Dancer perl package into the system root path - cpanm: name=Dancer use_sudo=yes ''' From b149233abfdc2bcb7efeb4e36d6c821e3460abda Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Sun, 26 Jul 2015 13:14:14 -0500 Subject: [PATCH 0769/2522] Additional upgrade mode fixes for pacman module with check_mode safety - ensure upgrade option honors and actually supports `check_mode` - enabling just `upgrade` and `update_cache` should perform upgrade - example added for the equivalent for `pacman -Syu` --- packaging/os/pacman.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packaging/os/pacman.py b/packaging/os/pacman.py index 842676e51ae..025268b3627 100644 --- a/packaging/os/pacman.py +++ b/packaging/os/pacman.py @@ -102,6 +102,9 @@ # Run the equivalent of "pacman -Su" as a separate step - pacman: upgrade=yes +# Run the equivalent of "pacman -Syu" as a separate step +- pacman: update_cache=yes upgrade=yes + # Run the equivalent of "pacman -Rdd", force remove package baz - pacman: name=baz state=absent force=yes ''' @@ -160,11 +163,14 @@ def upgrade(module, pacman_path): rc, stdout, stderr = module.run_command(cmdneedrefresh, check_rc=False) if rc == 0: + if module.check_mode: + data = stdout.split('\n') + module.exit_json(changed=True, msg="%s package(s) would be upgraded" % len(data)) rc, stdout, stderr = module.run_command(cmdupgrade, check_rc=False) if rc == 0: module.exit_json(changed=True, msg='System upgraded') else: - module.fail_json(msg="could not upgrade") + module.fail_json(msg="Could not upgrade") else: module.exit_json(changed=False, msg='Nothing to upgrade') @@ -275,10 +281,10 @@ def main(): if p["update_cache"] and not module.check_mode: update_package_db(module, pacman_path) - if not p['name']: - module.exit_json(changed=True, msg='updated the package master lists') + if not (p['name'] or p['upgrade']): + module.exit_json(changed=True, msg='Updated the package master lists') - if p['update_cache'] and module.check_mode and not p['name']: + if p['update_cache'] and module.check_mode and not (p['name'] or p['upgrade']): module.exit_json(changed=True, msg='Would have updated the package cache') if p['upgrade']: From d7cc0f60c9cd001f031c4465e5a94eeae94f2140 Mon Sep 17 00:00:00 2001 From: Trond Hindenes Date: Wed, 23 Sep 2015 20:49:26 +0200 Subject: [PATCH 0770/2522] win_chocolatey bugfixes --- windows/win_chocolatey.ps1 | 72 ++++++-------------------------------- 1 file changed, 10 insertions(+), 62 deletions(-) diff --git a/windows/win_chocolatey.ps1 b/windows/win_chocolatey.ps1 index 4a033d23157..ce006fff152 100644 --- a/windows/win_chocolatey.ps1 +++ b/windows/win_chocolatey.ps1 @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -$ErrorActionPreference = "Stop" # WANT_JSON # POWERSHELL_COMMON @@ -25,72 +24,21 @@ $params = Parse-Args $args; $result = New-Object PSObject; Set-Attr $result "changed" $false; -If ($params.name) -{ - $package = $params.name -} -Else -{ - Fail-Json $result "missing required argument: name" -} - -If ($params.force) -{ - $force = $params.force | ConvertTo-Bool -} -Else -{ - $force = $false -} +$package = Get-Attr -obj $params -name name -failifempty $true -emptyattributefailmessage "missing required argument: name" +$force = Get-Attr -obj $params -name force -default "false" | ConvertTo-Bool +$upgrade = Get-Attr -obj $params -name upgrade -default "false" | ConvertTo-Bool +$version = Get-Attr -obj $params -name version -default $null -If ($params.upgrade) -{ - $upgrade = $params.upgrade | ConvertTo-Bool -} -Else -{ - $upgrade = $false -} +$source = Get-Attr -obj $params -name source -default $null +if ($source) {$source = $source.Tolower()} -If ($params.version) -{ - $version = $params.version -} -Else +$showlog = Get-Attr -obj $params -name showlog -default "false" | ConvertTo-Bool +$state = Get-Attr -obj $params -name state -default "present" +if ("present","absent" -notcontains $state) { - $version = $null + Fail-Json $result "state is $state; must be present or absent" } -If ($params.source) -{ - $source = $params.source.ToString().ToLower() -} -Else -{ - $source = $null -} - -If ($params.showlog) -{ - $showlog = $params.showlog | ConvertTo-Bool -} -Else -{ - $showlog = $null -} - -If ($params.state) -{ - $state = $params.state.ToString().ToLower() - If (($state -ne "present") -and ($state -ne "absent")) - { - Fail-Json $result "state is $state; must be present or absent" - } -} -Else -{ - $state = "present" -} Function Chocolatey-Install-Upgrade { From 0ab8c9383d7d4d27ae91e2db5e9cf8ba201e9698 Mon Sep 17 00:00:00 2001 From: Andy Baker Date: Thu, 24 Sep 2015 09:42:19 +0100 Subject: [PATCH 0771/2522] Fix existing_mailbox check --- cloud/webfaction/webfaction_mailbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/webfaction/webfaction_mailbox.py b/cloud/webfaction/webfaction_mailbox.py index c08bd477601..bcb355c9632 100644 --- a/cloud/webfaction/webfaction_mailbox.py +++ b/cloud/webfaction/webfaction_mailbox.py @@ -99,7 +99,7 @@ def main(): module.params['login_password'] ) - mailbox_list = webfaction.list_mailboxes(session_id) + mailbox_list = [x['mailbox'] for x in webfaction.list_mailboxes(session_id)] existing_mailbox = mailbox_name in mailbox_list result = {} From f3a9a92ffe24b2b1d167f6a141db6c687f66c29e Mon Sep 17 00:00:00 2001 From: Benno Joy Date: Thu, 24 Sep 2015 14:28:10 +0530 Subject: [PATCH 0772/2522] remove facts and fix docs for taskdefinition module --- cloud/amazon/ecs_cluster_facts.py | 174 --------------------- cloud/amazon/ecs_task_facts.py | 184 ----------------------- cloud/amazon/ecs_taskdefinition.py | 18 ++- cloud/amazon/ecs_taskdefinition_facts.py | 174 --------------------- 4 files changed, 11 insertions(+), 539 deletions(-) delete mode 100644 cloud/amazon/ecs_cluster_facts.py delete mode 100644 cloud/amazon/ecs_task_facts.py delete mode 100644 cloud/amazon/ecs_taskdefinition_facts.py diff --git a/cloud/amazon/ecs_cluster_facts.py b/cloud/amazon/ecs_cluster_facts.py deleted file mode 100644 index c4dff2706ad..00000000000 --- a/cloud/amazon/ecs_cluster_facts.py +++ /dev/null @@ -1,174 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -DOCUMENTATION = ''' ---- -module: ecs_cluster_facts -short_description: list or describe clusters or their instances in ecs -description: - - Lists or describes clusters or cluster instances in ecs. -version_added: "2.0" -author: Mark Chance(@Java1Guy) -options: - details: - description: - - Set this to true if you want detailed information. - required: false - default: false - cluster: - description: - - The cluster ARNS to list. - required: false - default: 'default' - instances: - description: - - The instance ARNS to list. - required: false - default: None (returns all) - option: - description: - - Whether to return information about clusters or their instances - required: false - choices: ['clusters', 'instances'] - default: 'clusters' -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Basic listing example -- ecs_task: - cluster=test-cluster - task_list=123456789012345678901234567890123456 - -# Basic example of deregistering task -- ecs_task: - state: absent - family: console-test-tdn - revision: 1 -''' -RETURN = ''' -clusters: - description: - - array of cluster ARNs when details is false - - array of dicts when details is true - sample: [ "arn:aws:ecs:us-west-2:172139249013:cluster/test-cluster" ] -''' -try: - import json, os - import boto - import botocore - HAS_BOTO = True -except ImportError: - HAS_BOTO = False - -try: - import boto3 - # import module snippets - from ansible.module_utils.ec2 import ec2_argument_spec, get_aws_connection_info, boto3_conn - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -class EcsClusterManager: - """Handles ECS Clusters""" - - def __init__(self, module): - self.module = module - - try: - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - if not region: - module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") - self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except boto.exception.NoAuthHandlerFound, e: - self.module.fail_json(msg="Can't authorize connection - "+str(e)) - - def list_container_instances(self, cluster): - response = self.ecs.list_container_instances(cluster=cluster) - relevant_response = dict(instances = response['containerInstanceArns']) - return relevant_response - - def describe_container_instances(self, cluster, instances): - response = self.ecs.describe_container_instances( - clusters=cluster, - containerInstances=instances.split(",") if instances else [] - ) - relevant_response = dict() - if 'containerInstances' in response and len(response['containerInstances'])>0: - relevant_response['instances'] = response['containerInstances'] - if 'failures' in response and len(response['failures'])>0: - relevant_response['instances_not_running'] = response['failures'] - return relevant_response - - def list_clusters(self): - response = self.ecs.list_clusters() - relevant_response = dict(clusters = response['clusterArns']) - return relevant_response - - def describe_clusters(self, cluster): - response = self.ecs.describe_clusters( - clusters=cluster.split(",") if cluster else [] - ) - relevant_response = dict() - if 'clusters' in response and len(response['clusters'])>0: - relevant_response['clusters'] = response['clusters'] - if 'failures' in response and len(response['failures'])>0: - relevant_response['clusters_not_running'] = response['failures'] - return relevant_response - -def main(): - - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - details=dict(required=False, type='bool' ), - cluster=dict(required=False, type='str' ), - instances=dict(required=False, type='str' ), - option=dict(required=False, choices=['clusters', 'instances'], default='clusters') - )) - - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - - if not HAS_BOTO: - module.fail_json(msg='boto is required.') - - if not HAS_BOTO3: - module.fail_json(msg='boto3 is required.') - - show_details = False - if 'details' in module.params and module.params['details']: - show_details = True - - task_mgr = EcsClusterManager(module) - if module.params['option']=='clusters': - if show_details: - ecs_facts = task_mgr.describe_clusters(module.params['cluster']) - else: - ecs_facts = task_mgr.list_clusters() - if module.params['option']=='instances': - if show_details: - ecs_facts = task_mgr.describe_container_instances(module.params['cluster'], module.params['instances']) - else: - ecs_facts = task_mgr.list_container_instances(module.params['cluster']) - ecs_facts_result = dict(changed=False, ansible_facts=ecs_facts) - module.exit_json(**ecs_facts_result) - -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * - -if __name__ == '__main__': - main() diff --git a/cloud/amazon/ecs_task_facts.py b/cloud/amazon/ecs_task_facts.py deleted file mode 100644 index 26d9d31ae51..00000000000 --- a/cloud/amazon/ecs_task_facts.py +++ /dev/null @@ -1,184 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -DOCUMENTATION = ''' ---- -module: ecs_task_facts -short_description: return facts about tasks in ecs -description: - - Describes or lists tasks. -version_added: 2.0 -author: Mark Chance(@Java1Guy) -options: - details: - description: - - Set this to true if you want detailed information about the tasks. - required: false - default: false - type: bool - cluster: - description: - - The cluster in which to list tasks if other than the 'default'. - required: false - default: 'default' - type: str - task_list: - description: - - Set this to a list of task identifiers. If 'details' is false, this is required. - required: false - family: - required: False - type: str - - container_instance: - required: False - type: 'str' - max_results: - required: False - type: 'int' - started_by: - required: False - type: 'str' - service_name: - required: False - type: 'str' - desired_status: - required: False - choices=['RUNNING', 'PENDING', 'STOPPED'] - -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Basic listing example -- ecs_task_facts: - cluster=test-cluster - task_list=123456789012345678901234567890123456 - -# Listing tasks with details -- ecs_task_facts: - details: true - cluster=test-cluster - task_list=123456789012345678901234567890123456 - -''' -try: - import json, os - import boto - import botocore - # import module snippets - from ansible.module_utils.basic import * - from ansible.module_utils.ec2 import * - HAS_BOTO = True -except ImportError: - HAS_BOTO = False - -try: - import boto3 - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -class EcsTaskManager: - """Handles ECS Tasks""" - - def __init__(self, module): - self.module = module - - try: - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - if not region: - module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") - self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except boto.exception.NoAuthHandlerFound, e: - self.module.fail_json(msg="Can't authorize connection - "+str(e)) - - def transmogrify(self, params, field, dictionary, arg_name): - if field in params and params[field] is not None: - dictionary[arg_name] = params[field] - - def list_tasks(self, params): - fn_args = dict() - self.transmogrify(params, 'cluster', fn_args, 'cluster') - self.transmogrify(params, 'container_instance', fn_args, 'containerInstance') - self.transmogrify(params, 'family', fn_args, 'family') - self.transmogrify(params, 'max_results', fn_args, 'maxResults') - self.transmogrify(params, 'started_by', fn_args, 'startedBy') - self.transmogrify(params, 'service_name', fn_args, 'startedBy') - self.transmogrify(params, 'desired_status', fn_args, 'desiredStatus') - relevant_response = dict() - try: - response = self.ecs.list_tasks(**fn_args) - relevant_response['tasks'] = response['taskArns'] - except botocore.exceptions.ClientError: - relevant_response['tasks'] = [] - return relevant_response - - def describe_tasks(self, cluster_name, tasks): - response = self.ecs.describe_tasks( - cluster=cluster_name if cluster_name else '', - tasks=tasks.split(",") if tasks else [] - ) - relevant_response = dict( - tasks = response['tasks'], - tasks_not_running = response['failures']) - return relevant_response - -def main(): - - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - details=dict(required=False, type='bool' ), - cluster=dict(required=False, type='str' ), - task_list = dict(required=False, type='str'), - family=dict(required= False, type='str' ), - container_instance=dict(required=False, type='str' ), - max_results=dict(required=False, type='int' ), - started_by=dict(required=False, type='str' ), - service_name=dict(required=False, type='str' ), - desired_status=dict(required=False, choices=['RUNNING', 'PENDING', 'STOPPED']) - )) - - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - - if not HAS_BOTO: - module.fail_json(msg='boto is required.') - - if not HAS_BOTO3: - module.fail_json(msg='boto3 is required.') - - task_to_describe = module.params['family'] - show_details = False - if 'details' in module.params and module.params['details']: - show_details = True - - task_mgr = EcsTaskManager(module) - if show_details: - if 'task_list' not in module.params or not module.params['task_list']: - module.fail_json(msg="task_list must be specified for ecs_task_facts") - ecs_facts = task_mgr.describe_tasks(module.params['cluster'], module.params['task_list']) - else: - ecs_facts = task_mgr.list_tasks(module.params) - ecs_facts_result = dict(changed=False, ansible_facts=ecs_facts) - module.exit_json(**ecs_facts_result) - -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * - -if __name__ == '__main__': - main() diff --git a/cloud/amazon/ecs_taskdefinition.py b/cloud/amazon/ecs_taskdefinition.py index 62d6d0cf929..8f5cc068393 100644 --- a/cloud/amazon/ecs_taskdefinition.py +++ b/cloud/amazon/ecs_taskdefinition.py @@ -28,27 +28,31 @@ description: - State whether the task definition should exist or be deleted required: true - choices=['present', 'absent'] - + choices: ['present', 'absent'] arn: description: - The arn of the task description to delete required: false - family: - =dict(required=False, type='str' ), - + description: + - A Name that would be given to the task definition + required: false revision: + description: + - A revision number for the task definition required: False type: int - containers: + description: + - A list of containers definitions required: False type: list of dicts with container definitions - volumes: + description: + - A list of names of volumes to be attached required: False type: list of name + ''' EXAMPLES = ''' diff --git a/cloud/amazon/ecs_taskdefinition_facts.py b/cloud/amazon/ecs_taskdefinition_facts.py deleted file mode 100644 index 48cdabd259f..00000000000 --- a/cloud/amazon/ecs_taskdefinition_facts.py +++ /dev/null @@ -1,174 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -DOCUMENTATION = ''' ---- -module: ecs_taskdefinition_facts -short_description: return facts about task definitions in ecs -description: - - Describes or lists task definitions. -version_added: 2.0 -author: Mark Chance(@Java1Guy) -requirements: [ json, os, boto, botocore, boto3 ] -options: - details: - description: - - Set this to true if you want detailed information about the tasks. - required: false - default: false - name: - description: - - When details is true, the name must be provided. - required: false - family: - description: - - the name of the family of task definitions to list. - required: false - max_results: - description: - - The maximum number of results to return. - required: false - status: - description: - - Show only task descriptions of the given status. If omitted, it shows all - required: false - choices: ['ACTIVE', 'INACTIVE'] - sort: - description: - - Sort order of returned list of task definitions - required: false - choices: ['ASC', 'DESC'] - -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Basic listing example -- name: "Get task definitions with details" - ecs_taskdefinition_facts: - name: test-cluster-tasks - details: true - -- name: Get task definitions with details - ecs_taskdefinition_facts: - status: INACTIVE - details: true - family: test-cluster-rbjgjoaj-task - name: "arn:aws:ecs:us-west-2:172139249013:task-definition/test-cluster-rbjgjoaj-task:1" -''' -RETURN = ''' -task_definitions: - description: array of ARN values for the known task definitions - type: array of string or dict if details is true - sample: ["arn:aws:ecs:us-west-2:172139249013:task-definition/console-sample-app-static:1"] -''' -try: - import json, os - import boto - import botocore - # import module snippets - from ansible.module_utils.basic import * - from ansible.module_utils.ec2 import * - HAS_BOTO = True -except ImportError: - HAS_BOTO = False - -try: - import boto3 - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -class EcsTaskManager: - """Handles ECS Tasks""" - - def __init__(self, module): - self.module = module - - try: - # self.ecs = boto3.client('ecs') - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - if not region: - module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") - self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except boto.exception.NoAuthHandlerFound, e: - self.module.fail_json(msg="Can't authorize connection - "+str(e)) - - def transmogrify(self, params, field, dictionary, arg_name): - if field in params and params[field] is not None: - dictionary[arg_name] = params[field] - - def list_taskdefinitions(self, params): - fn_args = dict() - self.transmogrify(params, 'family', fn_args, 'familyPrefix') - self.transmogrify(params, 'max_results', fn_args, 'maxResults') - self.transmogrify(params, 'status', fn_args, 'status') - self.transmogrify(params, 'sort', fn_args, 'sort') - response = self.ecs.list_task_definitions(**fn_args) - return dict(task_definitions=response['taskDefinitionArns']) - - def describe_taskdefinition(self, task_definition): - try: - response = self.ecs.describe_task_definition(taskDefinition=task_definition) - except botocore.exceptions.ClientError: - response = dict(taskDefinition=[ dict( name=task_definition, status="MISSING")]) - relevant_response = dict( - task_definitions = response['taskDefinition'] - ) - return relevant_response - -def main(): - - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - details=dict(required= False, type='bool' ), - name=dict(required=False, type='str' ), - family=dict(required=False, type='str' ), - max_results=dict(required=False, type='int' ), - status=dict(required=False, choices=['ACTIVE', 'INACTIVE']), - sort=dict(required=False, choices=['ASC', 'DESC']) - )) - - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - - if not HAS_BOTO: - module.fail_json(msg='boto is required.') - - if not HAS_BOTO3: - module.fail_json(msg='boto3 is required.') - - show_details = False - if 'details' in module.params and module.params['details']: - if 'name' not in module.params or not module.params['name']: - module.fail_json(msg="task definition name must be specified for ecs_taskdefinition_facts") - show_details = True - - task_mgr = EcsTaskManager(module) - if show_details: - ecs_facts = task_mgr.describe_taskdefinition(module.params['name']) - else: - ecs_facts = task_mgr.list_taskdefinitions(module.params) - ecs_facts_result = dict(changed=False, ansible_facts=ecs_facts) - module.exit_json(**ecs_facts_result) - -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * -from ansible.module_utils.urls import * - -if __name__ == '__main__': - main() From 73382f45743cd085f415b4c81038e350ecbe43ee Mon Sep 17 00:00:00 2001 From: Mark Chance Date: Fri, 25 Sep 2015 11:59:28 -0600 Subject: [PATCH 0773/2522] WIP documentation update --- cloud/amazon/ecs_service.py | 72 ++++++++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 21 deletions(-) diff --git a/cloud/amazon/ecs_service.py b/cloud/amazon/ecs_service.py index f69a57ee5e9..01281638a3d 100644 --- a/cloud/amazon/ecs_service.py +++ b/cloud/amazon/ecs_service.py @@ -19,7 +19,7 @@ module: ecs_service short_description: create, terminate, start or stop a service in ecs description: - - Creates or terminates ec2 instances. + - Creates or terminates ecs services. notes: - the service role specified must be assumable (i.e. have a trust relationship for the ecs service, ecs.amazonaws.com) @@ -28,10 +28,60 @@ version_added: "2.0" author: Mark Chance (@java1guy) options: + state: + description: + - The desired state of the service + required: true + choices: ["present", "absent", "deleting"] + name: + description: + - The name of the service + required: true + cluster: + description: + - The name of the cluster in which the service exists + required: false + task_definition: + description: + - The task definition the service will run + required: false + load_balancers: + description: + - The list of ELBs defined for this service + required: false + + desired_count: + description: + - The count of how many instances of the service + required: false + client_token: + description: + - + required: false + role: + description: + - + required: false + delay: + description: + - The time to wait before checking that the service is available + required: false + default: 10 + repeat: + description: + - The number of times to check that the service is available + required: false + default: 10 ''' EXAMPLES = ''' # Note: These examples do not set authentication details, see the AWS Guide for details. + ecs_service: + state: present + name: console-test-service + cluster: "{{ new_cluster }}" + task_definition: "{{ new_cluster }}-task:{{task_revision}}" + desired_count: 0 # Basic provisioning example - ecs_service: @@ -45,26 +95,6 @@ cluster: string ''' RETURN = ''' -cache_updated: - description: if the cache was updated or not - returned: success, in some cases - type: boolean - sample: True -cache_update_time: - description: time of the last cache update (0 if unknown) - returned: success, in some cases - type: datetime - sample: 1425828348000 -stdout: - description: output from apt - returned: success, when needed - type: string - sample: "Reading package lists...\nBuilding dependency tree...\nReading state information...\nThe following extra packages will be installed:\n apache2-bin ..." -stderr: - description: error output from apt - returned: success, when needed - type: string - sample: "AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 127.0.1.1. Set the 'ServerName' directive globally to ..." ''' try: import json From 5adbe31ac31802983dc7322385892c61354bbb92 Mon Sep 17 00:00:00 2001 From: Robyn Bergeron Date: Fri, 25 Sep 2015 11:49:02 -0700 Subject: [PATCH 0774/2522] Update REVIEWERS.md Updated version of REVIEWERS.md with new review process for ansible-modules-extras, as outlined here: https://groups.google.com/forum/#!topic/ansible-project/bJF3ine7890 --- REVIEWERS.md | 209 +++++++++++++-------------------------------------- 1 file changed, 51 insertions(+), 158 deletions(-) diff --git a/REVIEWERS.md b/REVIEWERS.md index b09af51d1c1..7742e618cf3 100644 --- a/REVIEWERS.md +++ b/REVIEWERS.md @@ -1,160 +1,53 @@ -New module reviewers +Ansible Extras Reviewers ==================== -The following list represents all current Github module reviewers. It's currently comprised of all Ansible module authors, past and present. - -Two +1 votes by any of these module reviewers on a new module pull request will result in the inclusion of that module into Ansible Extras. - -Active -====== -- Adam Garside [@fabulops](https://www.github.com/fabulops) -- Adam Keech [@smadam813](https://www.github.com/smadam813) -- Adam Miller [@maxamillion](https://www.github.com/maxamillion) -- Alex Coomans [@drcapulet](https://www.github.com/drcapulet) -- Alexander Bulimov [@abulimov](https://www.github.com/abulimov) -- Alexander Saltanov [@sashka](https://www.github.com/sashka) -- Alexander Winkler [@dermute](https://www.github.com/dermute) -- Andrew de Quincey [@adq](https://www.github.com/adq) -- André Paramés [@andreparames](https://www.github.com/andreparames) -- Andy Hill [@andyhky](https://www.github.com/andyhky) -- Artūras `arturaz` Šlajus [@arturaz](https://www.github.com/arturaz) -- Augustus Kling [@AugustusKling](https://www.github.com/AugustusKling) -- BOURDEL Paul [@pb8226](https://www.github.com/pb8226) -- Balazs Pocze [@banyek](https://www.github.com/banyek) -- Ben Whaley [@bwhaley](https://www.github.com/bwhaley) -- Benno Joy [@bennojoy](https://www.github.com/bennojoy) -- Bernhard Weitzhofer [@b6d](https://www.github.com/b6d) -- Boyd Adamson [@brontitall](https://www.github.com/brontitall) -- Brad Olson [@bradobro](https://www.github.com/bradobro) -- Brian Coca [@bcoca](https://www.github.com/bcoca) -- Brice Burgess [@briceburg](https://www.github.com/briceburg) -- Bruce Pennypacker [@bpennypacker](https://www.github.com/bpennypacker) -- Carson Gee [@carsongee](https://www.github.com/carsongee) -- Chris Church [@cchurch](https://www.github.com/cchurch) -- Chris Hoffman [@chrishoffman](https://www.github.com/chrishoffman) -- Chris Long [@alcamie101](https://www.github.com/alcamie101) -- Chris Schmidt [@chrisisbeef](https://www.github.com/chrisisbeef) -- Christian Berendt [@berendt](https://www.github.com/berendt) -- Christopher H. Laco [@claco](https://www.github.com/claco) -- Cristian van Ee [@DJMuggs](https://www.github.com/DJMuggs) -- Dag Wieers [@dagwieers](https://www.github.com/dagwieers) -- Dane Summers [@dsummersl](https://www.github.com/dsummersl) -- Daniel Jaouen [@danieljaouen](https://www.github.com/danieljaouen) -- Daniel Schep [@dschep](https://www.github.com/dschep) -- Dariusz Owczarek [@dareko](https://www.github.com/dareko) -- Darryl Stoflet [@dstoflet](https://www.github.com/dstoflet) -- David CHANIAL [@davixx](https://www.github.com/davixx) -- David Stygstra [@stygstra](https://www.github.com/) -- Derek Carter [@goozbach](https://www.github.com/stygstra) -- Dimitrios Tydeas Mengidis [@dmtrs](https://www.github.com/dmtrs) -- Doug Luce [@dougluce](https://www.github.com/dougluce) -- Dylan Martin [@pileofrogs](https://www.github.com/pileofrogs) -- Elliott Foster [@elliotttf](https://www.github.com/elliotttf) -- Eric Johnson [@erjohnso](https://www.github.com/erjohnso) -- Evan Duffield [@scicoin-project](https://www.github.com/scicoin-project) -- Evan Kaufman [@EvanK](https://www.github.com/EvanK) -- Evgenii Terechkov [@evgkrsk](https://www.github.com/evgkrsk) -- Franck Cuny [@franckcuny](https://www.github.com/franckcuny) -- Gareth Rushgrove [@garethr](https://www.github.com/garethr) -- Hagai Kariti [@hkariti](https://www.github.com/hkariti) -- Hector Acosta [@hacosta](https://www.github.com/hacosta) -- Hiroaki Nakamura [@hnakamur](https://www.github.com/hnakamur) -- Ivan Vanderbyl [@ivanvanderbyl](https://www.github.com/ivanvanderbyl) -- Jakub Jirutka [@jirutka](https://www.github.com/jirutka) -- James Cammarata [@jimi-c](https://www.github.com/jimi-c) -- James Laska [@jlaska](https://www.github.com/jlaska) -- James S. Martin [@jsmartin](https://www.github.com/jsmartin) -- Jan-Piet Mens [@jpmens](https://www.github.com/jpmens) -- Jayson Vantuyl [@jvantuyl](https://www.github.com/jvantuyl) -- Jens Depuydt [@jensdepuydt](https://www.github.com/jensdepuydt) -- Jeroen Hoekx [@jhoekx](https://www.github.com/jhoekx) -- Jesse Keating [@j2sol](https://www.github.com/j2sol) -- Jim Dalton [@jsdalton](https://www.github.com/jsdalton) -- Jim Richardson [@weaselkeeper](https://www.github.com/weaselkeeper) -- Jimmy Tang [@jcftang](https://www.github.com/jcftang) -- Johan Wiren [@johanwiren](https://www.github.com/johanwiren) -- John Dewey [@retr0h](https://www.github.com/retr0h) -- John Jarvis [@jarv](https://www.github.com/jarv) -- John Whitbeck [@jwhitbeck](https://www.github.com/jwhitbeck) -- Jon Hawkesworth [@jhawkesworth](https://www.github.com/jhawkesworth) -- Jonas Pfenniger [@zimbatm](https://www.github.com/zimbatm) -- Jonathan I. Davila [@defionscode](https://www.github.com/defionscode) -- Joseph Callen [@jcpowermac](https://www.github.com/jcpowermac) -- Kevin Carter [@cloudnull](https://www.github.com/cloudnull) -- Lester Wade [@lwade](https://www.github.com/lwade) -- Lorin Hochstein [@lorin](https://www.github.com/lorin) -- Manuel Sousa [@manuel-sousa](https://www.github.com/manuel-sousa) -- Mark Theunissen [@marktheunissen](https://www.github.com/marktheunissen) -- Matt Coddington [@mcodd](https://www.github.com/mcodd) -- Matt Hite [@mhite](https://www.github.com/mhite) -- Matt Makai [@makaimc](https://www.github.com/makaimc) -- Matt Martz [@sivel](https://www.github.com/sivel) -- Matt Wright [@mattupstate](https://www.github.com/mattupstate) -- Matthew Vernon [@mcv21](https://www.github.com/mcv21) -- Matthew Williams [@mgwilliams](https://www.github.com/mgwilliams) -- Matthias Vogelgesang [@matze](https://www.github.com/matze) -- Max Riveiro [@kavu](https://www.github.com/kavu) -- Michael Gregson [@mgregson](https://www.github.com/mgregson) -- Michael J. Schultz [@mjschultz](https://www.github.com/mjschultz) -- Michael Schuett [@michaeljs1990](https://www.github.com/michaeljs1990) -- Michael Warkentin [@mwarkentin](https://www.github.com/mwarkentin) -- Mischa Peters [@mischapeters](https://www.github.com/mischapeters) -- Monty Taylor [@emonty](https://www.github.com/emonty) -- Nandor Sivok [@dominis](https://www.github.com/dominis) -- Nate Coraor [@natefoo](https://www.github.com/natefoo) -- Nate Kingsley [@nate-kingsley](https://www.github.com/nate-kingsley) -- Nick Harring [@NickatEpic](https://www.github.com/NickatEpic) -- Patrick Callahan [@dirtyharrycallahan](https://www.github.com/dirtyharrycallahan) -- Patrick Ogenstad [@ogenstad](https://www.github.com/ogenstad) -- Patrick Pelletier [@skinp](https://www.github.com/skinp) -- Patrik Lundin [@eest](https://www.github.com/eest) -- Paul Durivage [@angstwad](https://www.github.com/angstwad) -- Pavel Antonov [@softzilla](https://www.github.com/softzilla) -- Pepe Barbe [@elventear](https://www.github.com/elventear) -- Peter Mounce [@petemounce](https://www.github.com/petemounce) -- Peter Oliver [@mavit](https://www.github.com/mavit) -- Peter Sprygada [@privateip](https://www.github.com/privateip) -- Peter Tan [@tanpeter](https://www.github.com/tanpeter) -- Philippe Makowski [@pmakowski](https://www.github.com/pmakowski) -- Phillip Gentry, CX Inc [@pcgentry](https://www.github.com/pcgentry) -- Quentin Stafford-Fraser [@quentinsf](https://www.github.com/quentinsf) -- Ramon de la Fuente [@ramondelafuente](https://www.github.com/ramondelafuente) -- Raul Melo [@melodous](https://www.github.com/melodous) -- Ravi Bhure [@ravibhure](https://www.github.com/ravibhure) -- René Moser [@resmo](https://www.github.com/resmo) -- Richard Hoop [@rhoop](https://www.github.com/rhoop) -- Richard Isaacson [@risaacson](https://www.github.com/risaacson) -- Rick Mendes [@rickmendes](https://www.github.com/rickmendes) -- Romeo Theriault [@romeotheriault](https://www.github.com/romeotheriault) -- Scott Anderson [@tastychutney](https://www.github.com/tastychutney) -- Sebastian Kornehl [@skornehl](https://www.github.com/skornehl) -- Serge van Ginderachter [@srvg](https://www.github.com/srvg) -- Sergei Antipov [@UnderGreen](https://www.github.com/UnderGreen) -- Seth Edwards [@sedward](https://www.github.com/sedward) -- Silviu Dicu [@silviud](https://www.github.com/silviud) -- Simon JAILLET [@jails](https://www.github.com/jails) -- Stephen Fromm [@sfromm](https://www.github.com/sfromm) -- Steve [@groks](https://www.github.com/groks) -- Steve Gargan [@sgargan](https://www.github.com/sgargan) -- Steve Smith [@tarka](https://www.github.com/tarka) -- Takashi Someda [@tksmd](https://www.github.com/tksmd) -- Taneli Leppä [@rosmo](https://www.github.com/rosmo) -- Tim Bielawa [@tbielawa](https://www.github.com/tbielawa) -- Tim Mahoney [@timmahoney](https://www.github.com/timmahoney) -- Timothy Appnel [@tima](https://www.github.com/tima) -- Tom Bamford [@tombamford](https://www.github.com/tombamford) -- Trond Hindenes [@trondhindenes](https://www.github.com/trondhindenes) -- Vincent Van der Kussen [@vincentvdk](https://www.github.com/vincentvdk) -- Vincent Viallet [@zbal](https://www.github.com/zbal) -- WAKAYAMA Shirou [@shirou](https://www.github.com/shirou) -- Will Thames [@willthames](https://www.github.com/willthames) -- Willy Barro [@willybarro](https://www.github.com/willybarro) -- Xabier Larrakoetxea [@slok](https://www.github.com/slok) -- Yeukhon Wong [@yeukhon](https://www.github.com/yeukhon) -- Zacharie Eakin [@zeekin](https://www.github.com/zeekin) -- berenddeboer [@berenddeboer](https://www.github.com/berenddeboer) -- bleader [@bleader](https://www.github.com/bleader) -- curtis [@ccollicutt](https://www.github.com/ccollicutt) - -Retired +The Ansible Extras Modules are written and maintained by the Ansible community, and are included in Extras through a community-driven approval process. + +A full view of the pull request process for Extras can be seen here: http://gregdek.org/extras_PR_process_2015_09.png + +Expectations +======= + +1. New modules will be tested in good faith by users who care about them. +2. New modules will adhere to the module guidelines, located here: http://docs.ansible.com/ansible/developing_modules.html#module-checklist +3. The submitter of the module is willing and able to maintain the module over time. + +New Modules +======= + +New modules are subject to review by anyone in the Ansible community. For inclusion of a new module into Ansible Extras, a pull request must receive at least one approval from a fellow community member on each of the following criteria: + +* One "worksforme" approval from someone who has thoroughly tested the module, including all parameters and switches. +* One "passes_guidelines" approval from someone who has vetted the code according to the module guidelines. + +Either of these approvals can be given, in a comment, by anybody (except the submitter). + +Any module that has both of these, and no "needs_revision" votes (which can also be given by anybody) will be approved for inclusion. + +The core team will continue to be the point of escalation for any issues that may arise (duplicate modules, disagreements over guidelines, etc.) + +Existing Modules ======= -None yet :) + +PRs made against existing modules in Extras are subject to review by the module author or current maintainer. + +Unmaintained Modules +======= + +If modules in Extras go unmaintained, we will seek new maintainers, and if we don't find new +maintainers, we will ultimately deprecate them. + +Subject Matter Experts +======= + +Subject matter experts are groups of acknowledged community members who have expertise and experience in particular modules. Pull requests for existing or new modules are sometimes referred to these wider groups during triage, for expedience or escalation. + +Openstack: @emonty @shrews @dguerri @juliakreger @j2sol @rcarrillocruz + +Windows: @trondhindenes @petemounce @elventear @smadam813 @jhawkesworth @angstwad @sivel @chrishoffman @cchurch + +AWS: @jsmartin @scicoin-project @tombamford @garethr @lorin @jarv @jsdalton @silviud @adq @zbal @zeekin @willthames @lwade @carsongee @defionscode +@tastychutney @bpennypacker @loia + +Docker: @cove @joshuaconner @softzilla @smashwilson + +Red Hat Network: @barnabycourt @vritant @flossware From 35424842c6d51c0507f95cf01d8f352ff581a837 Mon Sep 17 00:00:00 2001 From: Robyn Bergeron Date: Fri, 25 Sep 2015 11:54:11 -0700 Subject: [PATCH 0775/2522] Update REVIEWERS.md with full picture process Made process picture be inline. Yay, me! --- REVIEWERS.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/REVIEWERS.md b/REVIEWERS.md index 7742e618cf3..73ebdeb58c6 100644 --- a/REVIEWERS.md +++ b/REVIEWERS.md @@ -2,8 +2,6 @@ Ansible Extras Reviewers ==================== The Ansible Extras Modules are written and maintained by the Ansible community, and are included in Extras through a community-driven approval process. -A full view of the pull request process for Extras can be seen here: http://gregdek.org/extras_PR_process_2015_09.png - Expectations ======= @@ -51,3 +49,9 @@ AWS: @jsmartin @scicoin-project @tombamford @garethr @lorin @jarv @jsdalton @sil Docker: @cove @joshuaconner @softzilla @smashwilson Red Hat Network: @barnabycourt @vritant @flossware + +PR Process +======= + +A full view of the pull request process for Extras can be seen here: +![here](http://gregdek.org/extras_PR_process_2015_09.png) From 228abcd3966787efaf29e7f971b0bd610549d667 Mon Sep 17 00:00:00 2001 From: Phil Date: Fri, 25 Sep 2015 15:54:39 -0500 Subject: [PATCH 0776/2522] fixes examples in docs to use updated param name --- windows/win_acl.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/windows/win_acl.py b/windows/win_acl.py index 7a1b256ef83..df815db0a0f 100644 --- a/windows/win_acl.py +++ b/windows/win_acl.py @@ -99,19 +99,19 @@ - NoPropagateInherit - InheritOnly default: "None" -author: Phil Schwartz, Trond Hindenes +author: Phil Schwartz (@schwartzmx), Trond Hindenes (@trondhindenes) ''' EXAMPLES = ''' # Restrict write,execute access to User Fed-Phil -$ ansible -i hosts -m win_acl -a "user=Fed-Phil src=C:\Important\Executable.exe type=deny rights='ExecuteFile,Write'" all +$ ansible -i hosts -m win_acl -a "user=Fed-Phil path=C:\Important\Executable.exe type=deny rights='ExecuteFile,Write'" all # Playbook example # Add access rule to allow IIS_IUSRS FullControl to MySite --- - name: Add IIS_IUSRS allow rights win_acl: - src: 'C:\inetpub\wwwroot\MySite' + path: 'C:\inetpub\wwwroot\MySite' user: 'IIS_IUSRS' rights: 'FullControl' type: 'allow' @@ -121,7 +121,7 @@ # Remove previously added rule for IIS_IUSRS - name: Remove FullControl AccessRule for IIS_IUSRS - src: 'C:\inetpub\wwwroot\MySite' + path: 'C:\inetpub\wwwroot\MySite' user: 'IIS_IUSRS' rights: 'FullControl' type: 'allow' @@ -131,9 +131,9 @@ # Deny Intern - name: Deny Deny - src: 'C:\Administrator\Documents' + path: 'C:\Administrator\Documents' user: 'Intern' rights: 'Read,Write,Modify,FullControl,Delete' type: 'deny' state: 'present' -''' \ No newline at end of file +''' From 953d382aa58d0b03c73b8bb33fec554d614f7dd8 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 28 Sep 2015 08:22:59 -0700 Subject: [PATCH 0777/2522] docs fixes --- cloud/amazon/ecs_task.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cloud/amazon/ecs_task.py b/cloud/amazon/ecs_task.py index 43b312974eb..8a84f74ad43 100644 --- a/cloud/amazon/ecs_task.py +++ b/cloud/amazon/ecs_task.py @@ -92,9 +92,8 @@ RETURN = ''' task: description: details about the tast that was started - type: array of dict - sample: [ clusterArn, containerInstanceArn, containers[], desiredStatus, lastStatus - overrides, startedBy, taskArn, taskDefinitionArn ] + type: complex + sample: "TODO: include sample" ''' try: import json From 4cf2c5555ed7c7c03474d4620c032e56f4a4cae8 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 28 Sep 2015 08:38:04 -0700 Subject: [PATCH 0778/2522] Fix for errors in the generated rst --- cloud/amazon/route53_health_check.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cloud/amazon/route53_health_check.py b/cloud/amazon/route53_health_check.py index 6b4cd1924a7..5ab0cc5d77d 100644 --- a/cloud/amazon/route53_health_check.py +++ b/cloud/amazon/route53_health_check.py @@ -53,10 +53,9 @@ health checks. The path can be any value for which your endpoint will return an HTTP status code of 2xx or 3xx when the endpoint is healthy, for example the file /docs/route53-health-check.html. - - * Required for all checks except TCP. - * The path must begin with a / - * Maximum 255 characters. + - Required for all checks except TCP. + - The path must begin with a / + - Maximum 255 characters. required: false default: null fqdn: From 0147a8995796f21557cc2105db562a19d242d44e Mon Sep 17 00:00:00 2001 From: Mark Chance Date: Mon, 28 Sep 2015 10:53:26 -0600 Subject: [PATCH 0779/2522] fix, update the documentation of the modules --- cloud/amazon/ecs_service.py | 43 +++++++++++++++------- cloud/amazon/ecs_service_facts.py | 60 +++++++++++++++---------------- 2 files changed, 58 insertions(+), 45 deletions(-) diff --git a/cloud/amazon/ecs_service.py b/cloud/amazon/ecs_service.py index 01281638a3d..a683602faf1 100644 --- a/cloud/amazon/ecs_service.py +++ b/cloud/amazon/ecs_service.py @@ -19,12 +19,12 @@ module: ecs_service short_description: create, terminate, start or stop a service in ecs description: - - Creates or terminates ecs services. + - Creates or terminates ecs services. notes: - - the service role specified must be assumable (i.e. have a trust relationship - for the ecs service, ecs.amazonaws.com) -dependecies: - - An IAM role must have been created + - the service role specified must be assumable (i.e. have a trust relationship for the ecs service, ecs.amazonaws.com) + - for details of the parameters and returns see U(http://boto3.readthedocs.org/en/latest/reference/services/ecs.html) +dependencies: + - An IAM role must have been created version_added: "2.0" author: Mark Chance (@java1guy) options: @@ -56,11 +56,11 @@ required: false client_token: description: - - + - Unique, case-sensitive identifier you provide to ensure the idempotency of the request. Up to 32 ASCII characters are allowed. required: false role: description: - - + - The name or full Amazon Resource Name (ARN) of the IAM role that allows your Amazon ECS container agent to make calls to your load balancer on your behalf. This parameter is only required if you are using a load balancer with your service. required: false delay: description: @@ -76,25 +76,42 @@ EXAMPLES = ''' # Note: These examples do not set authentication details, see the AWS Guide for details. - ecs_service: +- ecs_service: state: present name: console-test-service - cluster: "{{ new_cluster }}" - task_definition: "{{ new_cluster }}-task:{{task_revision}}" + cluster: new_cluster + task_definition: new_cluster-task:1" desired_count: 0 # Basic provisioning example - ecs_service: name: default state: present - cluster: string + cluster: new_cluster + # Simple example to delete -- ecs_cluster: +- ecs_service: name: default state: absent - cluster: string + cluster: new_cluster ''' RETURN = ''' +# Create service +service: On create service, it returns the new values; on delete service, it returns the values for the service being deleted. + clusterArn: The Amazon Resource Name (ARN) of the of the cluster that hosts the service. + desiredCount: The desired number of instantiations of the task definition to keep running on the service. + loadBalancers: A list of load balancer objects + loadBalancerName: the name + containerName: The name of the container to associate with the load balancer. + containerPort: The port on the container to associate with the load balancer. + pendingCount: The number of tasks in the cluster that are in the PENDING state. + runningCount: The number of tasks in the cluster that are in the RUNNING state. + serviceArn: The Amazon Resource Name (ARN) that identifies the service. The ARN contains the arn:aws:ecs namespace, followed by the region of the service, the AWS account ID of the service owner, the service namespace, and then the service name. For example, arn:aws:ecs:region :012345678910 :service/my-service . + serviceName: A user-generated string used to identify the service + status: The valid values are ACTIVE, DRAINING, or INACTIVE. + taskDefinition: The ARN of a task definition to use for tasks in the service. +# Delete service +ansible_facts: When deleting a service, the values described above for the service prior to its deletion are returned. ''' try: import json diff --git a/cloud/amazon/ecs_service_facts.py b/cloud/amazon/ecs_service_facts.py index 644f61a9c5d..d8287162c9a 100644 --- a/cloud/amazon/ecs_service_facts.py +++ b/cloud/amazon/ecs_service_facts.py @@ -18,6 +18,8 @@ --- module: ecs_service_facts short_description: list or describe services in ecs +notes: + - for details of the parameters and returns see U(http://boto3.readthedocs.org/en/latest/reference/services/ecs.html) description: - Lists or describes services in ecs. version_added: "2.0" @@ -34,51 +36,44 @@ - The cluster ARNS in which to list the services. required: false default: 'default' + service: + description: + - The service to get details for (required if details is true) + required: false ''' EXAMPLES = ''' # Note: These examples do not set authentication details, see the AWS Guide for details. # Basic listing example -- ecs_task: - cluster=test-cluster - task_list=123456789012345678901234567890123456 - -# Basic example of deregistering task -- ecs_task: - state: absent - family: console-test-tdn - revision: 1 +- ecs_service_facts: + cluster: test-cluster + service: console-test-service + details: "true" + +# Basic listing example +- ecs_service_facts: + cluster: test-cluster ''' RETURN = ''' -cache_updated: - description: if the cache was updated or not - returned: success, in some cases - type: boolean - sample: True -cache_update_time: - description: time of the last cache update (0 if unknown) - returned: success, in some cases - type: datetime - sample: 1425828348000 -stdout: - description: output from apt - returned: success, when needed - type: string - sample: "Reading package lists...\nBuilding dependency tree...\nReading state information...\nThe following extra packages will be installed:\n apache2-bin ..." -stderr: - description: error output from apt - returned: success, when needed - type: string - sample: "AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 127.0.1.1. Set the 'ServerName' directive globally to ..." +services: When details is false, returns an array of service ARNs, else an array of these fields + clusterArn: The Amazon Resource Name (ARN) of the of the cluster that hosts the service. + desiredCount: The desired number of instantiations of the task definition to keep running on the service. + loadBalancers: A list of load balancer objects + loadBalancerName: the name + containerName: The name of the container to associate with the load balancer. + containerPort: The port on the container to associate with the load balancer. + pendingCount: The number of tasks in the cluster that are in the PENDING state. + runningCount: The number of tasks in the cluster that are in the RUNNING state. + serviceArn: The Amazon Resource Name (ARN) that identifies the service. The ARN contains the arn:aws:ecs namespace, followed by the region of the service, the AWS account ID of the service owner, the service namespace, and then the service name. For example, arn:aws:ecs:region :012345678910 :service/my-service . + serviceName: A user-generated string used to identify the service + status: The valid values are ACTIVE, DRAINING, or INACTIVE. + taskDefinition: The ARN of a task definition to use for tasks in the service. ''' try: import json, os import boto import botocore - # import module snippets - from ansible.module_utils.basic import * - from ansible.module_utils.ec2 import * HAS_BOTO = True except ImportError: HAS_BOTO = False @@ -171,6 +166,7 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.urls import * +from ansible.module_utils.ec2 import * if __name__ == '__main__': main() From 5cd6b56453c301c0655e589a89a4fe1b6997b661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Tue, 22 Sep 2015 20:41:53 +0200 Subject: [PATCH 0780/2522] module: system/iptables --- system/iptables.py | 128 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 system/iptables.py diff --git a/system/iptables.py b/system/iptables.py new file mode 100644 index 00000000000..5497d04c6ad --- /dev/null +++ b/system/iptables.py @@ -0,0 +1,128 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Linus Unnebäck +# +# This file is part of Ansible +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + +# import module snippets +from ansible.module_utils.basic import * + +DOCUMENTATION = ''' +--- +module: iptables +short_description: Modify the systems iptables +requirements: [] +version_added: "2.0" +author: Linus Unnebäck (@LinusU) +description: Iptables is used to set up, maintain, and inspect the tables of IPv4 packet filter rules in the Linux kernel. +options: + table: + description: Packet matching table to operate on. + required: false + default: filter + choices: [ "filter", "nat", "mangle", "raw", "security" ] + chain: + description: Chain to operate on. + required: true + choices: [ "INPUT", "FORWARD", "OUTPUT", "PREROUTING", "POSTROUTING", "SECMARK", "CONNSECMARK" ] + rule: + description: The rule that should be absent or present + required: true + state: + description: Wheter the rule should be absent or present + required: false + default: present + choices: [ "present", "absent" ] +''' + +EXAMPLES = ''' +# Block specific IP +- iptables: chain=INPUT rule='-s 8.8.8.8 -j DROP' + become: yes + +# Forward port 80 to 8600 +- iptables: table=nat chain=PREROUTING rule='-i eth0 -p tcp -m tcp --dport 80 -j REDIRECT --to-ports 8600' + become: yes +''' + + +def push_arguments(iptables_path, action, args): + cmd = [iptables_path] + cmd.extend(['-t', args['table']]) + cmd.extend([action, args['chain']]) + cmd.extend(args['rule'].split(' ')) + return cmd + + +def check_present(iptables_path, module, args): + cmd = push_arguments(iptables_path, '-C', args) + rc, _, __ = module.run_command(cmd, check_rc=False) + return (rc == 0) + + +def append_rule(iptables_path, module, args): + cmd = push_arguments(iptables_path, '-A', args) + module.run_command(cmd, check_rc=True) + + +def remove_rule(iptables_path, module, args): + cmd = push_arguments(iptables_path, '-D', args) + module.run_command(cmd, check_rc=True) + + +def main(): + module = AnsibleModule( + supports_check_mode=True, + argument_spec=dict( + table=dict(required=False, default='filter', choices=['filter', 'nat', 'mangle', 'raw', 'security']), + chain=dict(required=True, default=None, choices=['INPUT', 'FORWARD', 'OUTPUT', 'PREROUTING', 'POSTROUTING', 'SECMARK', 'CONNSECMARK']), + rule=dict(required=True, default=None), + state=dict(required=False, default='present', choices=['present', 'absent']), + ), + ) + args = dict( + changed=False, + failed=False, + table=module.params['table'], + chain=module.params['chain'], + rule=module.params['rule'], + state=module.params['state'], + ) + iptables_path = module.get_bin_path('iptables', True) + rule_is_present = check_present(iptables_path, module, args) + should_be_present = (args['state'] == 'present') + + # Check if target is up to date + args['changed'] = (rule_is_present != should_be_present) + + # Check only; don't modify + if module.check_mode: + module.exit_json(changed=args['changed']) + + # Target is already up to date + if args['changed'] == False: + module.exit_json(**args) + + if should_be_present: + append_rule(iptables_path, module, args) + else: + remove_rule(iptables_path, module, args) + + module.exit_json(**args) + +if __name__ == '__main__': + main() From 3c4e6f58b03496918b980b212da203a3a3145195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Mon, 28 Sep 2015 19:02:07 +0200 Subject: [PATCH 0781/2522] iptables: expand rule options --- system/iptables.py | 145 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 116 insertions(+), 29 deletions(-) diff --git a/system/iptables.py b/system/iptables.py index 5497d04c6ad..2b79f1d3ad8 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -21,6 +21,11 @@ # import module snippets from ansible.module_utils.basic import * +BINS = dict( + ipv4='iptables', + ipv6='ip6tables', +) + DOCUMENTATION = ''' --- module: iptables @@ -28,59 +33,126 @@ requirements: [] version_added: "2.0" author: Linus Unnebäck (@LinusU) -description: Iptables is used to set up, maintain, and inspect the tables of IPv4 packet filter rules in the Linux kernel. +description: Iptables is used to set up, maintain, and inspect the tables of IP packet filter rules in the Linux kernel. This module does not handle the saving and/or loading of rules, but rather only manipulates the current rules that are present in memory. This is the same as the behaviour of the "iptables" and "ip6tables" command which this module uses internally. options: table: - description: Packet matching table to operate on. + description: This option specifies the packet matching table which the command should operate on. If the kernel is configured with automatic module loading, an attempt will be made to load the appropriate module for that table if it is not already there. required: false default: filter choices: [ "filter", "nat", "mangle", "raw", "security" ] - chain: - description: Chain to operate on. - required: true - choices: [ "INPUT", "FORWARD", "OUTPUT", "PREROUTING", "POSTROUTING", "SECMARK", "CONNSECMARK" ] - rule: - description: The rule that should be absent or present - required: true state: - description: Wheter the rule should be absent or present + description: Wheter the rule should be absent or present. required: false default: present choices: [ "present", "absent" ] + ip_version: + description: Which version of the IP protocol this rule should apply to. + required: false + default: ipv4 + choices: [ "ipv4", "ipv6" ] + chain: + description: Chain to operate on. This option can either be the name of a user defined chain or any of the builtin chains: "INPUT", "FORWARD", "OUTPUT", "PREROUTING", "POSTROUTING", "SECMARK", "CONNSECMARK" + required: true + protocol: + description: The protocol of the rule or of the packet to check. The specified protocol can be one of tcp, udp, udplite, icmp, esp, ah, sctp or the special keyword "all", or it can be a numeric value, representing one of these protocols or a different one. A protocol name from /etc/protocols is also allowed. A "!" argument before the protocol inverts the test. The number zero is equivalent to all. "all" will match with all protocols and is taken as default when this option is omitted. + required: false + source: + description: Source specification. Address can be either a network name, a hostname, a network IP address (with /mask), or a plain IP address. Hostnames will be resolved once only, before the rule is submitted to the kernel. Please note that specifying any name to be resolved with a remote query such as DNS is a really bad idea. The mask can be either a network mask or a plain number, specifying the number of 1's at the left side of the network mask. Thus, a mask of 24 is equivalent to 255.255.255.0. A "!" argument before the address specification inverts the sense of the address.Source specification. Address can be either a network name, a hostname, a network IP address (with /mask), or a plain IP address. Hostnames will be resolved once only, before the rule is submitted to the kernel. Please note that specifying any name to be resolved with a remote query such as DNS is a really bad idea. The mask can be either a network mask or a plain number, specifying the number of 1's at the left side of the network mask. Thus, a mask of 24 is equivalent to 255.255.255.0. A "!" argument before the address specification inverts the sense of the address. + required: false + destination: + description: Destination specification. Address can be either a network name, a hostname, a network IP address (with /mask), or a plain IP address. Hostnames will be resolved once only, before the rule is submitted to the kernel. Please note that specifying any name to be resolved with a remote query such as DNS is a really bad idea. The mask can be either a network mask or a plain number, specifying the number of 1's at the left side of the network mask. Thus, a mask of 24 is equivalent to 255.255.255.0. A "!" argument before the address specification inverts the sense of the address.Source specification. Address can be either a network name, a hostname, a network IP address (with /mask), or a plain IP address. Hostnames will be resolved once only, before the rule is submitted to the kernel. Please note that specifying any name to be resolved with a remote query such as DNS is a really bad idea. The mask can be either a network mask or a plain number, specifying the number of 1's at the left side of the network mask. Thus, a mask of 24 is equivalent to 255.255.255.0. A "!" argument before the address specification inverts the sense of the address. + required: false + match: + description: Specifies a match to use, that is, an extension module that tests for a specific property. The set of matches make up the condition under which a target is invoked. Matches are evaluated first to last if specified as an array and work in short-circuit fashion, i.e. if one extension yields false, evaluation will stop. + required: false + jump: + description: This specifies the target of the rule; i.e., what to do if the packet matches it. The target can be a user-defined chain (other than the one this rule is in), one of the special builtin targets which decide the fate of the packet immediately, or an extension (see EXTENSIONS below). If this option is omitted in a rule (and the goto paramater is not used), then matching the rule will have no effect on the packet's fate, but the counters on the rule will be incremented. + required: false + goto: + description: This specifies that the processing should continue in a user specified chain. Unlike the jump argument return will not continue processing in this chain but instead in the chain that called us via jump. + required: false + in_interface: + description: Name of an interface via which a packet was received (only for packets entering the INPUT, FORWARD and PREROUTING chains). When the "!" argument is used before the interface name, the sense is inverted. If the interface name ends in a "+", then any interface which begins with this name will match. If this option is omitted, any interface name will match. + required: false + out_interface: + description: Name of an interface via which a packet is going to be sent (for packets entering the FORWARD, OUTPUT and POSTROUTING chains). When the "!" argument is used before the interface name, the sense is inverted. If the interface name ends in a "+", then any interface which begins with this name will match. If this option is omitted, any interface name will match. + required: false + fragment: + description: This means that the rule only refers to second and further fragments of fragmented packets. Since there is no way to tell the source or destination ports of such a packet (or ICMP type), such a packet will not match any rules which specify them. When the "!" argument precedes fragment argument, the rule will only match head fragments, or unfragmented packets. + required: false + set_counters: + description: This enables the administrator to initialize the packet and byte counters of a rule (during INSERT, APPEND, REPLACE operations). + required: false + source_port: + description: Source port or port range specification. This can either be a service name or a port number. An inclusive range can also be specified, using the format first:last. If the first port is omitted, "0" is assumed; if the last is omitted, "65535" is assumed. If the first port is greater than the second one they will be swapped. + required: false + destination_port: + description: Destination port or port range specification. This can either be a service name or a port number. An inclusive range can also be specified, using the format first:last. If the first port is omitted, "0" is assumed; if the last is omitted, "65535" is assumed. If the first port is greater than the second one they will be swapped. + required: false + to_ports: + description: This specifies a destination port or range of ports to use: without this, the destination port is never altered. This is only valid if the rule also specifies one of the following protocols: tcp, udp, dccp or sctp. + required: false ''' EXAMPLES = ''' # Block specific IP -- iptables: chain=INPUT rule='-s 8.8.8.8 -j DROP' +- iptables: chain=INPUT source=8.8.8.8 jump=DROP become: yes # Forward port 80 to 8600 -- iptables: table=nat chain=PREROUTING rule='-i eth0 -p tcp -m tcp --dport 80 -j REDIRECT --to-ports 8600' +- iptables: table=nat chain=PREROUTING in_interface=eth0 protocol=tcp match=tcp destination_port=80 jump=REDIRECT to_ports=8600 become: yes ''' -def push_arguments(iptables_path, action, args): +def append_param(rule, param, flag, is_list): + if is_list: + for item in param: + append_param(rule, item, flag, False) + else: + if param is not None: + rule.extend([flag, param]) + + +def construct_rule(params): + rule = [] + append_param(rule, params['protocol'], '-p', False) + append_param(rule, params['source'], '-s', False) + append_param(rule, params['destination'], '-d', False) + append_param(rule, params['match'], '-m', True) + append_param(rule, params['jump'], '-j', False) + append_param(rule, params['goto'], '-g', False) + append_param(rule, params['in_interface'], '-i', False) + append_param(rule, params['out_interface'], '-o', False) + append_param(rule, params['fragment'], '-f', False) + append_param(rule, params['set_counters'], '-c', False) + append_param(rule, params['source_port'], '--source-port', False) + append_param(rule, params['destination_port'], '--destination-port', False) + append_param(rule, params['to_ports'], '--to-ports', False) + return rule + + +def push_arguments(iptables_path, action, params): cmd = [iptables_path] - cmd.extend(['-t', args['table']]) - cmd.extend([action, args['chain']]) - cmd.extend(args['rule'].split(' ')) + cmd.extend(['-t', params['table']]) + cmd.extend([action, params['chain']]) + cmd.extend(construct_rule(params)) return cmd -def check_present(iptables_path, module, args): - cmd = push_arguments(iptables_path, '-C', args) +def check_present(iptables_path, module, params): + cmd = push_arguments(iptables_path, '-C', params) rc, _, __ = module.run_command(cmd, check_rc=False) return (rc == 0) -def append_rule(iptables_path, module, args): - cmd = push_arguments(iptables_path, '-A', args) +def append_rule(iptables_path, module, params): + cmd = push_arguments(iptables_path, '-A', params) module.run_command(cmd, check_rc=True) -def remove_rule(iptables_path, module, args): - cmd = push_arguments(iptables_path, '-D', args) +def remove_rule(iptables_path, module, params): + cmd = push_arguments(iptables_path, '-D', params) module.run_command(cmd, check_rc=True) @@ -89,21 +161,36 @@ def main(): supports_check_mode=True, argument_spec=dict( table=dict(required=False, default='filter', choices=['filter', 'nat', 'mangle', 'raw', 'security']), - chain=dict(required=True, default=None, choices=['INPUT', 'FORWARD', 'OUTPUT', 'PREROUTING', 'POSTROUTING', 'SECMARK', 'CONNSECMARK']), - rule=dict(required=True, default=None), state=dict(required=False, default='present', choices=['present', 'absent']), + ip_version=dict(required=False, default='ipv4', choices=['ipv4', 'ipv6']), + chain=dict(required=True, default=None, type='str'), + protocol=dict(required=False, default=None, type='str'), + source=dict(required=False, default=None, type='str'), + destination=dict(required=False, default=None, type='str'), + match=dict(required=False, default=[], type='list'), + jump=dict(required=False, default=None, type='str'), + goto=dict(required=False, default=None, type='str'), + in_interface=dict(required=False, default=None, type='str'), + out_interface=dict(required=False, default=None, type='str'), + fragment=dict(required=False, default=None, type='str'), + set_counters=dict(required=False, default=None, type='str'), + source_port=dict(required=False, default=None, type='str'), + destination_port=dict(required=False, default=None, type='str'), + to_ports=dict(required=False, default=None, type='str'), ), ) args = dict( changed=False, failed=False, + ip_version=module.params['ip_version'], table=module.params['table'], chain=module.params['chain'], - rule=module.params['rule'], + rule=' '.join(construct_rule(module.params)), state=module.params['state'], ) - iptables_path = module.get_bin_path('iptables', True) - rule_is_present = check_present(iptables_path, module, args) + ip_version = module.params['ip_version'] + iptables_path = module.get_bin_path(BINS[ip_version], True) + rule_is_present = check_present(iptables_path, module, module.params) should_be_present = (args['state'] == 'present') # Check if target is up to date @@ -118,9 +205,9 @@ def main(): module.exit_json(**args) if should_be_present: - append_rule(iptables_path, module, args) + append_rule(iptables_path, module, module.params) else: - remove_rule(iptables_path, module, args) + remove_rule(iptables_path, module, module.params) module.exit_json(**args) From 25b85dafbb031bdd779fad617b779ac29f54529d Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Mon, 28 Sep 2015 17:11:10 -0500 Subject: [PATCH 0782/2522] Add 'package' alias and support for list type for pacman 'name' parameter --- packaging/os/pacman.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packaging/os/pacman.py b/packaging/os/pacman.py index 842676e51ae..248f4ee8f84 100644 --- a/packaging/os/pacman.py +++ b/packaging/os/pacman.py @@ -251,12 +251,13 @@ def check_packages(module, pacman_path, packages, state): def main(): module = AnsibleModule( argument_spec = dict( - name = dict(aliases=['pkg']), + name = dict(aliases=['pkg', 'package'], type='list'), state = dict(default='present', choices=['present', 'installed', "latest", 'absent', 'removed']), recurse = dict(default=False, type='bool'), force = dict(default=False, type='bool'), upgrade = dict(default=False, type='bool'), - update_cache = dict(default=False, aliases=['update-cache'], type='bool')), + update_cache = dict(default=False, aliases=['update-cache'], type='bool') + ), required_one_of = [['name', 'update_cache', 'upgrade']], supports_check_mode = True) @@ -285,7 +286,7 @@ def main(): upgrade(module, pacman_path) if p['name']: - pkgs = p['name'].split(',') + pkgs = p['name'] pkg_files = [] for i, pkg in enumerate(pkgs): From 97b7a7a7f899bfdbe5cd4c01f65fb43a713c71cf Mon Sep 17 00:00:00 2001 From: Konstantin Manna Date: Sat, 26 Sep 2015 23:51:27 +0200 Subject: [PATCH 0783/2522] bugfix: use correct function close calls --- monitoring/boundary_meter.py | 2 +- system/solaris_zone.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monitoring/boundary_meter.py b/monitoring/boundary_meter.py index 3e03a55c8aa..99cb74f870d 100644 --- a/monitoring/boundary_meter.py +++ b/monitoring/boundary_meter.py @@ -211,7 +211,7 @@ def download_request(module, name, apiid, apikey, cert_type): body = response.read() cert_file = open(cert_file_path, 'w') cert_file.write(body) - cert_file.close + cert_file.close() os.chmod(cert_file_path, 0600) except: module.fail_json("Could not write to certificate file") diff --git a/system/solaris_zone.py b/system/solaris_zone.py index 375196cb1e7..8c8d22305bc 100644 --- a/system/solaris_zone.py +++ b/system/solaris_zone.py @@ -219,7 +219,7 @@ def configure_sysid(self): node = open('%s/root/etc/nodename' % self.path, 'w') node.write(self.name) - node.close + node.close() id = open('%s/root/etc/.sysIDtool.state' % self.path, 'w') id.write('1 # System previously configured?\n') From 11041dd00e7a8583175d59f068641937f9d91a59 Mon Sep 17 00:00:00 2001 From: Konstantin Manna Date: Sun, 27 Sep 2015 00:12:52 +0200 Subject: [PATCH 0784/2522] bugfix: uncall an uncallable dict --- clustering/znode | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clustering/znode b/clustering/znode index 142836281ea..3b39865b46e 100644 --- a/clustering/znode +++ b/clustering/znode @@ -121,7 +121,7 @@ def main(): command_type = 'op' if 'op' in module.params and module.params['op'] is not None else 'state' method = module.params[command_type] - result, result_dict = command_dict[command_type][method]() + result, result_dict = command_dict[command_type][method] zoo.shutdown() if result: From 6186da08ef8336ec776bb496c62a84c7d2900ba3 Mon Sep 17 00:00:00 2001 From: Shawn Siefkas Date: Wed, 30 Sep 2015 11:00:36 -0500 Subject: [PATCH 0785/2522] Check mode support for VPC route table creation --- cloud/amazon/ec2_vpc_route_table.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index cc2b5ff8ee7..9de7a85a14e 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -496,8 +496,11 @@ def ensure_route_table_present(connection, module): # If no route table returned then create new route table if route_table is None: + if module.check_mode: + module.exit_json(changed=True) + try: - route_table = connection.create_route_table(vpc_id, check_mode) + route_table = connection.create_route_table(vpc_id) changed = True except EC2ResponseError, e: module.fail_json(msg=e.message) From c61d430191ec4c2b758274727c8bdcf007b3bd74 Mon Sep 17 00:00:00 2001 From: "Michael J. Schultz" Date: Wed, 30 Sep 2015 15:50:44 -0500 Subject: [PATCH 0786/2522] sns: remove BabyJSON --- notification/sns.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/notification/sns.py b/notification/sns.py index 70030d66196..5fd81e2047f 100644 --- a/notification/sns.py +++ b/notification/sns.py @@ -107,9 +107,9 @@ import boto import boto.ec2 import boto.sns + HAS_BOTO = True except ImportError: - print "failed=True msg='boto required for this module'" - sys.exit(1) + HAS_BOTO = False def arn_topic_lookup(connection, short_topic): @@ -140,6 +140,9 @@ def main(): module = AnsibleModule(argument_spec=argument_spec) + if not HAS_BOTO: + module.fail_json(msg='boto required for this module') + msg = module.params['msg'] subject = module.params['subject'] topic = module.params['topic'] From 255666a09d0c62393400074171aae7c69257d124 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 30 Sep 2015 19:57:20 -0700 Subject: [PATCH 0787/2522] Fix doc build --- windows/win_nssm.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/windows/win_nssm.py b/windows/win_nssm.py index 86adacf1a7c..d2a82c12617 100644 --- a/windows/win_nssm.py +++ b/windows/win_nssm.py @@ -35,7 +35,6 @@ description: - Name of the service to operate on required: true - state: description: - State of the service on the system @@ -48,35 +47,33 @@ - restarted - absent default: started - application: description: - The application binary to run as a service - - Specify this whenever the service may need to be installed (state: present, started, stopped, restarted) - - Note that the application name must look like the following, if the directory includes spaces: - - nssm install service "c:\Program Files\app.exe" """C:\Path with spaces""" - - See commit 0b386fc1984ab74ee59b7bed14b7e8f57212c22b in the nssm.git project for more info (https://git.nssm.cc/?p=nssm.git;a=commit;h=0b386fc1984ab74ee59b7bed14b7e8f57212c22b) + - "Specify this whenever the service may need to be installed (state: present, started, stopped, restarted)" + - "Note that the application name must look like the following, if the directory includes spaces:" + - "nssm install service \\"c:\\Program Files\\app.exe\\" \\"\\"\\"C:\\Path with spaces\\"\\"\\"" + - "See commit 0b386fc1984ab74ee59b7bed14b7e8f57212c22b in the nssm.git project for more info (https://git.nssm.cc/?p=nssm.git;a=commit;h=0b386fc1984ab74ee59b7bed14b7e8f57212c22b)" required: false default: null - stdout_file: description: - Path to receive output required: false default: null - stderr_file: description: - Path to receive error output required: false default: null - app_parameters: description: - Parameters to be passed to the application when it starts required: false default: null -author: "Adam Keech (@smadam813), George Frank (@georgefrank)" +author: + - "Adam Keech (@smadam813)" + - "George Frank (@georgefrank)" ''' EXAMPLES = ''' From 962afeed5434e944e39e37b835b514f9bb9ce9ab Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 30 Sep 2015 19:57:29 -0700 Subject: [PATCH 0788/2522] Make the znode module a valid ansible module --- clustering/{znode => znode.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename clustering/{znode => znode.py} (100%) diff --git a/clustering/znode b/clustering/znode.py similarity index 100% rename from clustering/znode rename to clustering/znode.py From 3564c48f3c154dd571fe797f2d6e1f16cbb8d0b1 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 1 Oct 2015 00:29:57 -0400 Subject: [PATCH 0789/2522] switched to use module loggigng --- network/nmcli.py | 7 ------- network/openvswitch_bridge.py | 4 +--- network/openvswitch_port.py | 11 ++++------- packaging/os/openbsd_pkg.py | 33 +++++++++------------------------ system/cronvar.py | 9 ++------- 5 files changed, 16 insertions(+), 48 deletions(-) diff --git a/network/nmcli.py b/network/nmcli.py index c674114a32e..ccefef18ccf 100644 --- a/network/nmcli.py +++ b/network/nmcli.py @@ -379,7 +379,6 @@ ''' # import ansible.module_utils.basic import os -import syslog import sys import dbus from gi.repository import NetworkManager, NMClient @@ -466,14 +465,8 @@ def __init__(self, module): self.flags=module.params['flags'] self.ingress=module.params['ingress'] self.egress=module.params['egress'] - # select whether we dump additional debug info through syslog - self.syslogging=True def execute_command(self, cmd, use_unsafe_shell=False, data=None): - if self.syslogging: - syslog.openlog('ansible-%s' % os.path.basename(__file__)) - syslog.syslog(syslog.LOG_NOTICE, 'Command %s' % '|'.join(cmd)) - return self.module.run_command(cmd, use_unsafe_shell=use_unsafe_shell, data=data) def merge_secrets(self, proxy, config, setting_name): diff --git a/network/openvswitch_bridge.py b/network/openvswitch_bridge.py index 8f29735862f..411b95b9dc1 100644 --- a/network/openvswitch_bridge.py +++ b/network/openvswitch_bridge.py @@ -167,9 +167,7 @@ def run(self): current_fail_mode = self.get_fail_mode() if self.fail_mode and (self.fail_mode != current_fail_mode): - syslog.syslog(syslog.LOG_NOTICE, - "changing fail mode %s to %s" % - (current_fail_mode, self.fail_mode)) + self.module.log( "changing fail mode %s to %s" % (current_fail_mode, self.fail_mode)) self.set_fail_mode() changed = True diff --git a/network/openvswitch_port.py b/network/openvswitch_port.py index 469d53730da..e98453fc95f 100644 --- a/network/openvswitch_port.py +++ b/network/openvswitch_port.py @@ -22,8 +22,6 @@ # You should have received a copy of the GNU General Public License # along with this software. If not, see . -import syslog - DOCUMENTATION = ''' --- module: openvswitch_port @@ -99,7 +97,7 @@ def truncate_before(value, srch): return value -def _set_to_get(set_cmd): +def _set_to_get(set_cmd, module): """ Convert set command to get command and set value. return tuple (get command, set value) """ @@ -109,7 +107,7 @@ def _set_to_get(set_cmd): set_cmd = truncate_before(set_cmd, " option:") get_cmd = set_cmd.split(" ") (key, value) = get_cmd[-1].split("=") - syslog.syslog(syslog.LOG_NOTICE, "get commands %s " % key) + module.log("get commands %s " % key) return (["--", "get"] + get_cmd[:-1] + [key], value) @@ -128,7 +126,6 @@ def _vsctl(self, command, check_rc=True): '''Run ovs-vsctl command''' cmd = ['ovs-vsctl', '-t', str(self.timeout)] + command - syslog.syslog(syslog.LOG_NOTICE, " ".join(cmd)) return self.module.run_command(cmd, check_rc=check_rc) def exists(self): @@ -143,11 +140,11 @@ def exists(self): def set(self, set_opt): """ Set attributes on a port. """ - syslog.syslog(syslog.LOG_NOTICE, "set called %s" % set_opt) + self.module("set called %s" % set_opt) if (not set_opt): return False - (get_cmd, set_value) = _set_to_get(set_opt) + (get_cmd, set_value) = _set_to_get(set_opt, self.module) (rtc, out, err) = self._vsctl(get_cmd, False) if rtc != 0: ## diff --git a/packaging/os/openbsd_pkg.py b/packaging/os/openbsd_pkg.py index 1f331261d98..13cafa26bc5 100644 --- a/packaging/os/openbsd_pkg.py +++ b/packaging/os/openbsd_pkg.py @@ -20,7 +20,6 @@ import re import shlex -import syslog DOCUMENTATION = ''' --- @@ -64,13 +63,8 @@ - openbsd_pkg: name=* state=latest ''' -# Control if we write debug information to syslog. -debug = False - # Function used for executing commands. def execute_command(cmd, module): - if debug: - syslog.syslog("execute_command(): cmd = %s" % cmd) # Break command line into arguments. # This makes run_command() use shell=False which we need to not cause shell # expansion of special characters like '*'. @@ -91,12 +85,10 @@ def get_current_name(name, pkg_spec, module): else: pattern = "^%s-" % pkg_spec['stem'] - if debug: - syslog.syslog("get_current_name(): pattern = %s" % pattern) + module.debug("get_current_name(): pattern = %s" % pattern) for line in stdout.splitlines(): - if debug: - syslog.syslog("get_current_name: line = %s" % line) + module.debug("get_current_name: line = %s" % line) match = re.search(pattern, line) if match: current_name = line.split()[0] @@ -144,14 +136,12 @@ def package_present(name, installed_state, pkg_spec, module): # supplied the tool will exit 0 in both cases: if pkg_spec['version']: # Depend on the return code. - if debug: - syslog.syslog("package_present(): depending on return code") + module.debug("package_present(): depending on return code") if rc: changed=False else: # Depend on stderr instead. - if debug: - syslog.syslog("package_present(): depending on stderr") + module.debug("package_present(): depending on stderr") if stderr: # There is a corner case where having an empty directory in # installpath prior to the right location will result in a @@ -161,18 +151,15 @@ def package_present(name, installed_state, pkg_spec, module): match = re.search("\W%s-[^:]+: ok\W" % name, stdout) if match: # It turns out we were able to install the package. - if debug: - syslog.syslog("package_present(): we were able to install package") + module.debug("package_present(): we were able to install package") pass else: # We really did fail, fake the return code. - if debug: - syslog.syslog("package_present(): we really did fail") + module.debug("package_present(): we really did fail") rc = 1 changed=False else: - if debug: - syslog.syslog("package_present(): stderr was not set") + module.debug("package_present(): stderr was not set") if rc == 0: if module.check_mode: @@ -202,8 +189,7 @@ def package_latest(name, installed_state, pkg_spec, module): # Fetch name of currently installed package. pre_upgrade_name = get_current_name(name, pkg_spec, module) - if debug: - syslog.syslog("package_latest(): pre_upgrade_name = %s" % pre_upgrade_name) + module.debug("package_latest(): pre_upgrade_name = %s" % pre_upgrade_name) # Attempt to upgrade the package. (rc, stdout, stderr) = execute_command("%s %s" % (upgrade_cmd, name), module) @@ -237,8 +223,7 @@ def package_latest(name, installed_state, pkg_spec, module): else: # If package was not installed at all just make it present. - if debug: - syslog.syslog("package_latest(): package is not installed, calling package_present()") + module.debug("package_latest(): package is not installed, calling package_present()") return package_present(name, installed_state, pkg_spec, module) # Function used to make sure a package is not installed. diff --git a/system/cronvar.py b/system/cronvar.py index fe337752d59..b3b373e9dc3 100644 --- a/system/cronvar.py +++ b/system/cronvar.py @@ -124,8 +124,6 @@ def __init__(self, module, user=None, cron_file=None): self.user = 'root' self.lines = None self.wordchars = ''.join(chr(x) for x in range(128) if chr(x) not in ('=', "'", '"', )) - # select whether we dump additional debug info through syslog - self.syslogging = False if cron_file: self.cron_file = '/etc/cron.d/%s' % cron_file @@ -165,8 +163,7 @@ def read(self): count += 1 def log_message(self, message): - if self.syslogging: - syslog.syslog(syslog.LOG_NOTICE, 'ansible: "%s"' % message) + self.module.debug('ansible: "%s"' % message) def write(self, backup_file=None): """ @@ -363,9 +360,7 @@ def main(): os.umask(022) cronvar = CronVar(module, user, cron_file) - if cronvar.syslogging: - syslog.openlog('ansible-%s' % os.path.basename(__file__)) - syslog.syslog(syslog.LOG_NOTICE, 'cronvar instantiated - name: "%s"' % name) + module.debug('cronvar instantiated - name: "%s"' % name) # --- user input validation --- From e6b6bac820558a1f73528fab67ed5f6be5eb3d41 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Thu, 1 Oct 2015 12:07:43 -0500 Subject: [PATCH 0790/2522] znode requires a minimum of python2.6 --- .travis.yml | 2 +- clustering/znode.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 409c24454ac..c2583d592fc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ addons: - python2.4 - python2.6 script: - - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/consul.*\.py|notification/pushbullet\.py' . + - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py' . - python2.6 -m compileall -fq . - python2.7 -m compileall -fq . #- ./test-docs.sh extras diff --git a/clustering/znode.py b/clustering/znode.py index 3b39865b46e..8effcd9189e 100644 --- a/clustering/znode.py +++ b/clustering/znode.py @@ -52,6 +52,7 @@ required: false requirements: - kazoo >= 2.1 + - python >= 2.6 author: "Trey Perry (@treyperry)" """ From 161b3cf1e1245aa3769692338952f609cab658a8 Mon Sep 17 00:00:00 2001 From: Trond Hindenes Date: Thu, 1 Oct 2015 19:08:42 +0200 Subject: [PATCH 0791/2522] fixes #984 --- windows/win_package.ps1 | 9 +++++++-- windows/win_package.py | 6 +++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/windows/win_package.ps1 b/windows/win_package.ps1 index 02bb908a944..6cdc6bf6e5c 100644 --- a/windows/win_package.ps1 +++ b/windows/win_package.ps1 @@ -1237,10 +1237,15 @@ Set-Attr $result "changed" $false; $path = Get-Attr -obj $params -name path -failifempty $true -resultobj $result $name = Get-Attr -obj $params -name name -default $path -$productid = Get-Attr -obj $params -name productid -failifempty $true -resultobj $result +$productid = Get-Attr -obj $params -name productid +if ($productid -eq $null) +{ + #Alias added for backwards compat. + $productid = Get-Attr -obj $params -name product_id -failifempty $true -resultobj $result +} $arguments = Get-Attr -obj $params -name arguments $ensure = Get-Attr -obj $params -name state -default "present" -if (!$ensure) +if ($ensure -eq $null) { $ensure = Get-Attr -obj $params -name ensure -default "present" } diff --git a/windows/win_package.py b/windows/win_package.py index d20e5ee8816..842aa0693d4 100644 --- a/windows/win_package.py +++ b/windows/win_package.py @@ -44,9 +44,9 @@ product_id: description: - product id of the installed package (used for checking if already installed) - required: false + required: true default: null - aliases: [] + aliases: [productid] arguments: description: - Any arguments the installer needs @@ -79,7 +79,7 @@ win_package: name="Microsoft Visual C thingy" path="http://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x64.exe" - ProductId="{CF2BEA3C-26EA-32F8-AA9B-331F7E34BA97}" + Product_Id="{CF2BEA3C-26EA-32F8-AA9B-331F7E34BA97}" Arguments="/install /passive /norestart" From 7c48fe617df1ea9ba324bec8d7a724055fa04a34 Mon Sep 17 00:00:00 2001 From: Trond Hindenes Date: Thu, 1 Oct 2015 19:12:21 +0200 Subject: [PATCH 0792/2522] added module author --- windows/win_package.py | 1 + 1 file changed, 1 insertion(+) diff --git a/windows/win_package.py b/windows/win_package.py index 842aa0693d4..68497d5ba4f 100644 --- a/windows/win_package.py +++ b/windows/win_package.py @@ -25,6 +25,7 @@ --- module: win_package version_added: "1.7" +author: Trond Hindenes short_description: Installs/Uninstalls a installable package, either from local file system or url description: - Installs or uninstalls a package From 6cdfe07518af82d75fdbbbb36bade26349dd7b6e Mon Sep 17 00:00:00 2001 From: Etienne CARRIERE Date: Thu, 1 Oct 2015 20:59:28 +0200 Subject: [PATCH 0793/2522] Take review comments in accounts (pull request :582) --- network/f5/bigip_virtual_server.py | 44 ++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/network/f5/bigip_virtual_server.py b/network/f5/bigip_virtual_server.py index 0c47eb20943..2387a40a79b 100644 --- a/network/f5/bigip_virtual_server.py +++ b/network/f5/bigip_virtual_server.py @@ -25,7 +25,7 @@ description: - "Manages F5 BIG-IP LTM virtual servers via iControl SOAP API" version_added: "2.0" -author: Etienne Carriere +author: Etienne Carriere (@Etienne-Carriere) notes: - "Requires BIG-IP software version >= 11" - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" @@ -71,23 +71,24 @@ aliases: ['vs'] destination: description: - - "Destination IP of the virtual server (only host is currently supported) . Required when state=present and vs does not exist. Error when state=absent." + - "Destination IP of the virtual server (only host is currently supported) . Required when state=present and vs does not exist." required: true aliases: ['address', 'ip'] port: description: - "Port of the virtual server . Required when state=present and vs does not exist" - required: true + required: false + default: None all_profiles: description: - "List of all Profiles (HTTP,ClientSSL,ServerSSL,etc) that must be used by the virtual server" required: false - default: null + default: None pool: description: - "Default pool for the virtual server" required: false - default: null + default: None snat: description: - "Source network address policy" @@ -102,7 +103,7 @@ description: - "Virtual server description." required: false - default: null + default: None ''' EXAMPLES = ''' @@ -121,7 +122,7 @@ state: present partition: MyPartition name: myvirtualserver - destination: "{{ ansible_default_ipv4["address"] }}" + destination: "{{ ansible_default_ipv4['address'] }}" port: 443 pool: "{{ mypool }}" snat: Automap @@ -194,7 +195,7 @@ def vs_create(api,name,destination,port,pool): profiles = _profiles) created = True return created - except bigsudsOperationFailed, e : + except bigsuds.OperationFailed, e: if "already exists" not in str(e): raise Exception('Error on creating Virtual Server : %s' % e) @@ -235,7 +236,7 @@ def set_snat(api,name,snat): try: current_state=get_snat_type(api,name) if snat is None: - return update + return updated if snat == 'None' and current_state != 'SRC_TRANS_NONE': api.LocalLB.VirtualServer.set_source_address_translation_none(virtual_servers = [name]) updated = True @@ -268,18 +269,30 @@ def set_pool(api,name,pool): def get_destination(api,name): return api.LocalLB.VirtualServer.get_destination_v2(virtual_servers = [name])[0] -def set_destination(api,name,destination,port): +def set_destination(api,name,destination): updated=False try: current_destination = get_destination(api,name) - if (destination is not None and port is not None) and (destination != current_destination['address'] or port != current_destination['port']): - api.LocalLB.VirtualServer.set_destination_v2(virtual_servers = [name],destinations=[{'address': destination, 'port':port}]) + if destination is not None and destination != current_destination['address']: + api.LocalLB.VirtualServer.set_destination_v2(virtual_servers = [name],destinations=[{'address': destination, 'port': current_destination['port']}]) updated=True return updated except bigsuds.OperationFailed, e: raise Exception('Error on setting destination : %s'% e ) +def set_port(api,name,port): + updated=False + try: + current_destination = get_destination(api,name) + if port is not None and port != current_destination['port']: + api.LocalLB.VirtualServer.set_destination_v2(virtual_servers = [name],destinations=[{'address': current_destination['address'], 'port': port}]) + updated=True + return updated + except bigsuds.OperationFailed, e: + raise Exception('Error on setting port : %s'% e ) + + def get_description(api,name): return api.LocalLB.VirtualServer.get_description(virtual_servers = [name])[0] @@ -404,7 +417,8 @@ def main(): # Have a transaction for all the changes try: api.System.Session.start_transaction() - result['changed']|=set_destination(api,name,fq_name(partition,destination),port) + result['changed']|=set_destination(api,name,fq_name(partition,destination)) + result['changed']|=set_port(api,name,port) result['changed']|=set_pool(api,name,pool) result['changed']|=set_description(api,name,description) result['changed']|=set_snat(api,name,snat) @@ -434,5 +448,7 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.f5 import * -main() + +if __name__ == '__main__': + main() From 5eff47c6f15af447cc15b102c79ad3593c6c7c5b Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 1 Oct 2015 19:59:51 -0400 Subject: [PATCH 0794/2522] changed use_sudo to system_lib (kept alias) and expanded description to explain how it works --- packaging/language/cpanm.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packaging/language/cpanm.py b/packaging/language/cpanm.py index 82a6b5d0a0c..10bb66522ae 100644 --- a/packaging/language/cpanm.py +++ b/packaging/language/cpanm.py @@ -64,12 +64,14 @@ required: false default: false version_added: "2.0" - use_sudo: + system_lib: description: - - Use this if you want to install modules to the system perl include path. + - Use this if you want to install modules to the system perl include path. You must be root or have "passwordless" sudo for this to work. + - This uses the cpanm commandline option '--sudo', which has nothing to do with ansible privilege escalation. required: false default: false version_added: "2.0" + aliases: ['use_sudo'] notes: - Please note that U(http://search.cpan.org/dist/App-cpanminus/bin/cpanm, cpanm) must be installed on the remote host. author: "Franck Cuny (@franckcuny)" @@ -95,7 +97,7 @@ - cpanm: name=Dancer mirror=http://cpan.cpantesters.org/ # install Dancer perl package into the system root path -- cpanm: name=Dancer use_sudo=yes +- cpanm: name=Dancer system_lib=yes ''' def _is_package_installed(module, name, locallib, cpanm): @@ -148,7 +150,7 @@ def main(): mirror=dict(default=None, required=False), mirror_only=dict(default=False, type='bool'), installdeps=dict(default=False, type='bool'), - use_sudo=dict(default=False, type='bool'), + system_lib=dict(default=False, type='bool', aliases=['use_sudo']), ) module = AnsibleModule( @@ -164,7 +166,7 @@ def main(): mirror = module.params['mirror'] mirror_only = module.params['mirror_only'] installdeps = module.params['installdeps'] - use_sudo = module.params['use_sudo'] + use_sudo = module.params['system_lib'] changed = False From 791967485bf9414b3d10d1341285a76854dfe094 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Tue, 29 Sep 2015 02:40:55 -0500 Subject: [PATCH 0795/2522] homebrew: Improve 'argument_spec' handling - Remove choice list for boolean values in argument_spec and make it more consistent with core modules - Add 'package' alias and support for list type for 'name' parameter - Added self as maintainer --- packaging/os/homebrew.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packaging/os/homebrew.py b/packaging/os/homebrew.py index 91888ba6bca..6b3aea550a8 100644 --- a/packaging/os/homebrew.py +++ b/packaging/os/homebrew.py @@ -3,6 +3,7 @@ # (c) 2013, Andrew Dunham # (c) 2013, Daniel Jaouen +# (c) 2015, Indrajit Raychaudhuri # # Based on macports (Jimmy Tang ) # @@ -23,6 +24,7 @@ --- module: homebrew author: + - "Indrajit Raychaudhuri (@indrajitr)" - "Daniel Jaouen (@danieljaouen)" - "Andrew Dunham (@andrew-d)" short_description: Package manager for Homebrew @@ -45,13 +47,13 @@ description: - update homebrew itself first required: false - default: "no" + default: no choices: [ "yes", "no" ] upgrade_all: description: - upgrade all homebrew packages required: false - default: "no" + default: no choices: [ "yes", "no" ] install_options: description: @@ -763,7 +765,7 @@ def _unlink_packages(self): def main(): module = AnsibleModule( argument_spec=dict( - name=dict(aliases=["pkg"], required=False), + name=dict(aliases=["pkg", "package"], required=False, type='list'), path=dict(required=False), state=dict( default="present", @@ -775,12 +777,12 @@ def main(): ], ), update_homebrew=dict( - default="no", + default=False, aliases=["update-brew"], type='bool', ), upgrade_all=dict( - default="no", + default=False, aliases=["upgrade"], type='bool', ), @@ -795,7 +797,7 @@ def main(): p = module.params if p['name']: - packages = p['name'].split(',') + packages = p['name'] else: packages = None @@ -839,4 +841,3 @@ def main(): if __name__ == '__main__': main() - From 02c17c023a2c98333fb997429f2fbf6c308e5695 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Sun, 4 Oct 2015 22:13:35 -0500 Subject: [PATCH 0796/2522] homebrew: Also add 'formula' as alias for 'name' and reformat --- packaging/os/homebrew.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packaging/os/homebrew.py b/packaging/os/homebrew.py index 6b3aea550a8..9d41dcd69a2 100644 --- a/packaging/os/homebrew.py +++ b/packaging/os/homebrew.py @@ -765,7 +765,11 @@ def _unlink_packages(self): def main(): module = AnsibleModule( argument_spec=dict( - name=dict(aliases=["pkg", "package"], required=False, type='list'), + name=dict( + aliases=["pkg", "package", "formula"], + required=False, + type='list', + ), path=dict(required=False), state=dict( default="present", From da426da30827472aa39af406d166a8599896113e Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Sun, 4 Oct 2015 21:32:10 -0700 Subject: [PATCH 0797/2522] Change documented requirement from dnf to python-dnf as dnf could be taken to mean the command line tool. Fixes #1032 --- packaging/os/dnf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index c4522f9105a..58cf4bcac95 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -86,7 +86,7 @@ # informational: requirements for nodes requirements: - "python >= 2.6" - - dnf + - python-dnf author: - '"Igor Gnatenko (@ignatenkobrain)" ' - '"Cristian van Ee (@DJMuggs)" ' From ebbd7748b12dabc91b9e98858a9fc26f7d551369 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 5 Oct 2015 11:57:18 -0400 Subject: [PATCH 0798/2522] doc formatting fixes --- windows/win_nssm.py | 56 ++++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/windows/win_nssm.py b/windows/win_nssm.py index d2a82c12617..b3925f0fd50 100644 --- a/windows/win_nssm.py +++ b/windows/win_nssm.py @@ -29,7 +29,7 @@ description: - nssm is a service helper which doesn't suck. See https://nssm.cc/ for more information. requirements: - - "nssm >= 2.24.0 # (install via win_chocolatey) win_chocolatey: name=nssm" + - "nssm >= 2.24.0 # (install via win_chocolatey) win_chocolatey: name=nssm" options: name: description: @@ -52,7 +52,7 @@ - The application binary to run as a service - "Specify this whenever the service may need to be installed (state: present, started, stopped, restarted)" - "Note that the application name must look like the following, if the directory includes spaces:" - - "nssm install service \\"c:\\Program Files\\app.exe\\" \\"\\"\\"C:\\Path with spaces\\"\\"\\"" + - 'nssm install service "c:\\Program Files\\app.exe\\" "C:\\Path with spaces\\"' - "See commit 0b386fc1984ab74ee59b7bed14b7e8f57212c22b in the nssm.git project for more info (https://git.nssm.cc/?p=nssm.git;a=commit;h=0b386fc1984ab74ee59b7bed14b7e8f57212c22b)" required: false default: null @@ -77,53 +77,53 @@ ''' EXAMPLES = ''' - # Install and start the foo service - win_nssm: +# Install and start the foo service +- win_nssm: name: foo - application: C:\windows\foo.exe + application: C:\windowsk\\foo.exe - # Install and start the foo service with a key-value pair argument - # This will yield the following command: C:\windows\foo.exe bar "true" - win_nssm: +# Install and start the foo service with a key-value pair argument +# This will yield the following command: C:\windows\\foo.exe bar "true" +- win_nssm: name: foo - application: C:\windows\foo.exe + application: C:\windows\\foo.exe app_parameters: bar: true - # Install and start the foo service with a key-value pair argument, where the argument needs to start with a dash - # This will yield the following command: C:\windows\foo.exe -bar "true" - win_nssm: +# Install and start the foo service with a key-value pair argument, where the argument needs to start with a dash +# This will yield the following command: C:\windows\\foo.exe -bar "true" +- win_nssm: name: foo - application: C:\windows\foo.exe + application: C:\windows\\foo.exe app_parameters: "-bar": true - # Install and start the foo service with a single parameter - # This will yield the following command: C:\windows\foo.exe bar - win_nssm: +# Install and start the foo service with a single parameter +# This will yield the following command: C:\windows\\foo.exe bar +- win_nssm: name: foo - application: C:\windows\foo.exe + application: C:\windows\\foo.exe app_parameters: _: bar - # Install and start the foo service with a mix of single params, and key value pairs - # This will yield the following command: C:\windows\foo.exe bar -file output.bat - win_nssm: +# Install and start the foo service with a mix of single params, and key value pairs +# This will yield the following command: C:\windows\\foo.exe bar -file output.bat +- win_nssm: name: foo - application: C:\windows\foo.exe + application: C:\windows\\foo.exe app_parameters: _: bar "-file": "output.bat" - # Install and start the foo service, redirecting stdout and stderr to the same file - win_nssm: +# Install and start the foo service, redirecting stdout and stderr to the same file +- win_nssm: name: foo - application: C:\windows\foo.exe - stdout_file: C:\windows\foo.log - stderr_file: C:\windows\foo.log + application: C:\windows\\foo.exe + stdout_file: C:\windows\\foo.log + stderr_file: C:\windows\\foo.log - # Remove the foo service - win_nssm: +# Remove the foo service +- win_nssm: name: foo state: absent ''' From f4be5c63821283fdd946ae208a54e015e0960edc Mon Sep 17 00:00:00 2001 From: Charles Paul Date: Mon, 5 Oct 2015 12:16:29 -0500 Subject: [PATCH 0799/2522] allow floating point input for size vale --- system/lvol.py | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/system/lvol.py b/system/lvol.py index 7a01d83829c..2d588171a91 100644 --- a/system/lvol.py +++ b/system/lvol.py @@ -21,8 +21,8 @@ DOCUMENTATION = ''' --- author: - - "Jeroen Hoekx (@jhoekx)" - - "Alexander Bulimov (@abulimov)" + - "Jeroen Hoekx (@jhoekx)" + - "Alexander Bulimov (@abulimov)" module: lvol short_description: Configure LVM logical volumes description: @@ -42,7 +42,8 @@ - The size of the logical volume, according to lvcreate(8) --size, by default in megabytes or optionally with one of [bBsSkKmMgGtTpPeE] units; or according to lvcreate(8) --extents as a percentage of [VG|PVS|FREE]; - resizing is not supported with percentages. + resizing is not supported with percentages. Float values must begin + with a digit. state: choices: [ "present", "absent" ] default: present @@ -95,6 +96,7 @@ def mkversion(major, minor, patch): return (1000 * 1000 * int(major)) + (1000 * int(minor)) + int(patch) + def parse_lvs(data): lvs = [] for line in data.splitlines(): @@ -122,7 +124,7 @@ def main(): argument_spec=dict( vg=dict(required=True), lv=dict(required=True), - size=dict(), + size=dict(type='str'), opts=dict(type='str'), state=dict(choices=["absent", "present"], default='present'), force=dict(type='bool', default='no'), @@ -167,23 +169,19 @@ def main(): size_opt = 'l' size_unit = '' + if not '%' in size: # LVCREATE(8) -L --size option unit - elif size[-1].isalpha(): if size[-1].lower() in 'bskmgtpe': - size_unit = size[-1].lower() - if size[0:-1].isdigit(): - size = int(size[0:-1]) - else: - module.fail_json(msg="Bad size specification for unit %s" % size_unit) - size_opt = 'L' - else: - module.fail_json(msg="Size unit should be one of [bBsSkKmMgGtTpPeE]") - # when no unit, megabytes by default - elif size.isdigit(): - size = int(size) - else: - module.fail_json(msg="Bad size specification") + size_unit = size[-1].lower() + size = size[0:-1] + + try: + float(size) + if not size[0].isdigit(): raise ValueError() + except ValueError: + module.fail_json(msg="Bad size specification of '%s'" % size) + # when no unit, megabytes by default if size_opt == 'l': unit = 'm' else: From 2cc18b878098210582c1423b24195ed0f916b92e Mon Sep 17 00:00:00 2001 From: Rick Mendes Date: Mon, 5 Oct 2015 10:29:25 -0700 Subject: [PATCH 0800/2522] added missing license --- cloud/amazon/ec2_win_password.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/cloud/amazon/ec2_win_password.py b/cloud/amazon/ec2_win_password.py index 6086c42ffbb..f3a687c2371 100644 --- a/cloud/amazon/ec2_win_password.py +++ b/cloud/amazon/ec2_win_password.py @@ -1,4 +1,17 @@ #!/usr/bin/python +# +# This is a free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This Ansible library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this library. If not, see . DOCUMENTATION = ''' --- From 1a76f4cc22eec6112276d9caf05e68e16f52413a Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 5 Oct 2015 12:55:39 -0500 Subject: [PATCH 0801/2522] Fix interpreter line in a few vmware modules --- cloud/vmware/vmware_cluster.py | 2 +- cloud/vmware/vmware_target_canonical_facts.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/vmware/vmware_cluster.py b/cloud/vmware/vmware_cluster.py index ee64b48f08c..72f29e7dfad 100644 --- a/cloud/vmware/vmware_cluster.py +++ b/cloud/vmware/vmware_cluster.py @@ -1,4 +1,4 @@ -#!/bin/python +#!/usr/bin/python # -*- coding: utf-8 -*- # (c) 2015, Joseph Callen diff --git a/cloud/vmware/vmware_target_canonical_facts.py b/cloud/vmware/vmware_target_canonical_facts.py index 028fc8c83a2..45c183822be 100644 --- a/cloud/vmware/vmware_target_canonical_facts.py +++ b/cloud/vmware/vmware_target_canonical_facts.py @@ -1,4 +1,4 @@ -#!/bin/python +#!/usr/bin/python # -*- coding: utf-8 -*- # (c) 2015, Joseph Callen From 6732181a39a15fb25b60fd480833f1b4f1a4bcba Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 5 Oct 2015 12:58:47 -0500 Subject: [PATCH 0802/2522] Switch from old style replacer code to new style for ansible.module_utils.basic --- cloud/xenserver_facts.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cloud/xenserver_facts.py b/cloud/xenserver_facts.py index 54ca3389752..149030925f9 100644 --- a/cloud/xenserver_facts.py +++ b/cloud/xenserver_facts.py @@ -192,7 +192,6 @@ def main(): module.exit_json(ansible=data) -# this is magic, see lib/ansible/module_common.py -#<> +from ansible.module_utils.basic import * main() From b82ebdde19ae5b721778456cadc5594d699ceb3a Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 5 Oct 2015 13:22:32 -0500 Subject: [PATCH 0803/2522] sys.exit removal for cloud/xenserver_facts.py --- cloud/xenserver_facts.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/cloud/xenserver_facts.py b/cloud/xenserver_facts.py index 149030925f9..d679afce853 100644 --- a/cloud/xenserver_facts.py +++ b/cloud/xenserver_facts.py @@ -28,7 +28,6 @@ ''' import platform -import sys import XenAPI EXAMPLES = ''' @@ -75,12 +74,9 @@ def codename(self): def get_xenapi_session(): - try: - session = XenAPI.xapi_local() - session.xenapi.login_with_password('', '') - return session - except XenAPI.Failure: - sys.exit(1) + session = XenAPI.xapi_local() + session.xenapi.login_with_password('', '') + return session def get_networks(session): @@ -163,8 +159,10 @@ def main(): module = AnsibleModule({}) obj = XenServerFacts() - session = get_xenapi_session() - + try: + session = get_xenapi_session() + except XenAPI.Failure, e: + module.fail_json(msg='%s' % e) data = { 'xenserver_version': obj.version, From 55dab7cd1b52ebed035ed79eae5bdd954eebb2d7 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 5 Oct 2015 13:22:43 -0500 Subject: [PATCH 0804/2522] sys.exit removal for cloud/misc/ovirt.py --- cloud/misc/ovirt.py | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/cloud/misc/ovirt.py b/cloud/misc/ovirt.py index 6e8f3281dc5..10f8e5c0a44 100644 --- a/cloud/misc/ovirt.py +++ b/cloud/misc/ovirt.py @@ -207,14 +207,13 @@ ''' -import sys try: from ovirtsdk.api import API from ovirtsdk.xml import params + HAS_OVIRTSDK = True except ImportError: - print "failed=True msg='ovirtsdk required for this module'" - sys.exit(1) + HAS_OVIRTSDK = False # ------------------------------------------------------------------- # # create connection with API @@ -224,8 +223,7 @@ def conn(url, user, password): try: value = api.test() except: - print "error connecting to the oVirt API" - sys.exit(1) + raise Exception("error connecting to the oVirt API") return api # ------------------------------------------------------------------- # @@ -253,17 +251,16 @@ def create_vm(conn, vmtype, vmname, zone, vmdisk_size, vmcpus, vmnic, vmnetwork, try: conn.vms.add(vmparams) except: - print "Error creating VM with specified parameters" - sys.exit(1) + raise Exception("Error creating VM with specified parameters") vm = conn.vms.get(name=vmname) try: vm.disks.add(vmdisk) except: - print "Error attaching disk" + raise Exception("Error attaching disk") try: vm.nics.add(nic_net1) except: - print "Error adding nic" + raise Exception("Error adding nic") # create an instance from a template @@ -272,8 +269,7 @@ def create_vm_template(conn, vmname, image, zone): try: conn.vms.add(vmparams) except: - print 'error adding template %s' % image - sys.exit(1) + raise Exception('error adding template %s' % image) # start instance @@ -356,6 +352,9 @@ def main(): ) ) + if not HAS_OVIRTSDK: + module.fail_json(msg='ovirtsdk required for this module') + state = module.params['state'] user = module.params['user'] url = module.params['url'] @@ -377,16 +376,25 @@ def main(): sdomain = module.params['sdomain'] # storage domain to store disk on region = module.params['region'] # oVirt Datacenter #initialize connection - c = conn(url+"/api", user, password) + try: + c = conn(url+"/api", user, password) + except Exception, e: + module.fail_json(msg='%s' % e) if state == 'present': if get_vm(c, vmname) == "empty": if resource_type == 'template': - create_vm_template(c, vmname, image, zone) + try: + create_vm_template(c, vmname, image, zone) + except Exception, e: + module.fail_json(msg='%s' % e) module.exit_json(changed=True, msg="deployed VM %s from template %s" % (vmname,image)) elif resource_type == 'new': # FIXME: refactor, use keyword args. - create_vm(c, vmtype, vmname, zone, vmdisk_size, vmcpus, vmnic, vmnetwork, vmmem, vmdisk_alloc, sdomain, vmcores, vmos, vmdisk_int) + try: + create_vm(c, vmtype, vmname, zone, vmdisk_size, vmcpus, vmnic, vmnetwork, vmmem, vmdisk_alloc, sdomain, vmcores, vmos, vmdisk_int) + except Exception, e: + module.fail_json(msg='%s' % e) module.exit_json(changed=True, msg="deployed VM %s from scratch" % vmname) else: module.exit_json(changed=False, msg="You did not specify a resource type") From abf7ee579e50154c67d39a76931d59ce6d7a9a1e Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 5 Oct 2015 13:23:00 -0500 Subject: [PATCH 0805/2522] sys.exit removal for cloud/google/gce_img.py and cloud/google/gce_tag.py --- cloud/google/gce_img.py | 1 - cloud/google/gce_tag.py | 1 - 2 files changed, 2 deletions(-) diff --git a/cloud/google/gce_img.py b/cloud/google/gce_img.py index 5775a94794d..a4a55c16dec 100644 --- a/cloud/google/gce_img.py +++ b/cloud/google/gce_img.py @@ -201,7 +201,6 @@ def main(): changed = delete_image(gce, name, module) module.exit_json(changed=changed, name=name) - sys.exit(0) # import module snippets from ansible.module_utils.basic import * diff --git a/cloud/google/gce_tag.py b/cloud/google/gce_tag.py index 186f570b3f1..4f60f58f760 100644 --- a/cloud/google/gce_tag.py +++ b/cloud/google/gce_tag.py @@ -219,7 +219,6 @@ def main(): changed, tags_changed = remove_tags(gce, module, instance_name, tags) module.exit_json(changed=changed, instance_name=instance_name, tags=tags_changed, zone=zone) - sys.exit(0) # import module snippets from ansible.module_utils.basic import * From eac449342178dafee57a24b0792d3ad2fc96a2cc Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 5 Oct 2015 13:23:34 -0500 Subject: [PATCH 0806/2522] sys.exit removal for notification/mail.py --- notification/mail.py | 1 - 1 file changed, 1 deletion(-) diff --git a/notification/mail.py b/notification/mail.py index 8be9a589cbf..e63a9536996 100644 --- a/notification/mail.py +++ b/notification/mail.py @@ -285,7 +285,6 @@ def main(): msg.attach(part) except Exception, e: module.fail_json(rc=1, msg="Failed to send mail: can't attach file %s: %s" % (file, e)) - sys.exit() composed = msg.as_string() From e68d9315c17fbf6ce37f323d0ee57b1eba1fa0a4 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 5 Oct 2015 13:24:25 -0500 Subject: [PATCH 0807/2522] sys.exit removal for system/capabilities.py --- system/capabilities.py | 1 - 1 file changed, 1 deletion(-) diff --git a/system/capabilities.py b/system/capabilities.py index ce8ffcfa632..aa0785f6f62 100644 --- a/system/capabilities.py +++ b/system/capabilities.py @@ -180,7 +180,6 @@ def main(): CapabilitiesModule(module) - sys.exit(0) # import module snippets from ansible.module_utils.basic import * From 9d015665b89e5e0043ae787f4f73692a3ec545f6 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 5 Oct 2015 13:34:42 -0500 Subject: [PATCH 0808/2522] Fix open_iscsi EXAMPLES section --- system/open_iscsi.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/system/open_iscsi.py b/system/open_iscsi.py index 084303d7b52..74349ce8680 100644 --- a/system/open_iscsi.py +++ b/system/open_iscsi.py @@ -84,23 +84,22 @@ description: - whether the list of nodes in the persistent iscsi database should be returned by the module +''' + +EXAMPLES = ''' +# perform a discovery on 10.1.2.3 and show available target nodes +- open_iscsi: show_nodes=yes discover=yes portal=10.1.2.3 + +# discover targets on portal and login to the one available +# (only works if exactly one target is exported to the initiator) +- open_iscsi: portal={{iscsi_target}} login=yes discover=yes + +# description: connect to the named target, after updating the local +# persistent database (cache) +- open_iscsi: login=yes target=iqn.1986-03.com.sun:02:f8c1f9e0-c3ec-ec84-c9c9-8bfb0cd5de3d -examples: - - description: perform a discovery on 10.1.2.3 and show available target - nodes - code: > - open_iscsi: show_nodes=yes discover=yes portal=10.1.2.3 - - description: discover targets on portal and login to the one available - (only works if exactly one target is exported to the initiator) - code: > - open_iscsi: portal={{iscsi_target}} login=yes discover=yes - - description: connect to the named target, after updating the local - persistent database (cache) - code: > - open_iscsi: login=yes target=iqn.1986-03.com.sun:02:f8c1f9e0-c3ec-ec84-c9c9-8bfb0cd5de3d - - description: discconnect from the cached named target - code: > - open_iscsi: login=no target=iqn.1986-03.com.sun:02:f8c1f9e0-c3ec-ec84-c9c9-8bfb0cd5de3d" +# description: discconnect from the cached named target +- open_iscsi: login=no target=iqn.1986-03.com.sun:02:f8c1f9e0-c3ec-ec84-c9c9-8bfb0cd5de3d" ''' import glob From 2654789af7c56bd114312e76a212018de586d1b4 Mon Sep 17 00:00:00 2001 From: Dreamcat4 Date: Mon, 5 Oct 2015 21:10:59 +0100 Subject: [PATCH 0809/2522] fix: fw rule names must always be quoted, to permit spaces ' ' and brackets '()' Without this fix, the 'netsh' command gets name=Firewall Rule Name instead of name="Firewall Rule Name". Thus causing all sorts of havoc. Basic shell quoting rules seems to apply to Windows Powershell too. This is very much needed as many of windows 10's default firewall rules contain spaces and brackets () characters. --- windows/win_firewall_rule.ps1 | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/windows/win_firewall_rule.ps1 b/windows/win_firewall_rule.ps1 index 223d8b17b69..9c73507509b 100644 --- a/windows/win_firewall_rule.ps1 +++ b/windows/win_firewall_rule.ps1 @@ -24,7 +24,7 @@ function getFirewallRule ($fwsettings) { try { #$output = Get-NetFirewallRule -name $($fwsettings.name); - $rawoutput=@(netsh advfirewall firewall show rule name=$($fwsettings.Name)) + $rawoutput=@(netsh advfirewall firewall show rule name="$($fwsettings.Name)") if (!($rawoutput -eq 'No rules match the specified criteria.')){ $rawoutput | Where {$_ -match '^([^:]+):\s*(\S.*)$'} | Foreach -Begin { $FirstRun = $true; @@ -123,8 +123,9 @@ function createFireWallRule ($fwsettings) { $execString+=" "; $execString+=$key; $execString+="="; + $execString+='"'; $execString+=$fwsetting.value; - #$execString+="'"; + $execString+='"'; }; try { #$msg+=@($execString); @@ -152,7 +153,7 @@ function createFireWallRule ($fwsettings) { function removeFireWallRule ($fwsettings) { $msg=@() try { - $rawoutput=@(netsh advfirewall firewall delete rule name=$($fwsettings.name)) + $rawoutput=@(netsh advfirewall firewall delete rule name="$($fwsettings.name)") $rawoutput | Where {$_ -match '^([^:]+):\s*(\S.*)$'} | Foreach -Begin { $FirstRun = $true; $HashProps = @{}; From 6c5a4a14ef415e4635f7e0dc7fcb345f0c617a98 Mon Sep 17 00:00:00 2001 From: Dreamcat4 Date: Mon, 5 Oct 2015 21:36:24 +0100 Subject: [PATCH 0810/2522] fix: win10 - Add exception handling for 'Profiles:' textual output key name mismatch. In win10 (and pribably win8x also): The output of 'show rule' key includes the line "Profiles:Public,Private". Yet your script expects the key name printed out to be "Profile:value". This commit added the necessary exception handling to avoid flagging 'different=true' under the false circumstance. The key name to SET a firewall rule is still "profile=" and not "profiles=". There is coming up another commit to fix the value handling for win10/win8. Which is another (different) error with the profile: key. --- windows/win_firewall_rule.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/windows/win_firewall_rule.ps1 b/windows/win_firewall_rule.ps1 index 9c73507509b..0b0a2cd54f9 100644 --- a/windows/win_firewall_rule.ps1 +++ b/windows/win_firewall_rule.ps1 @@ -75,6 +75,8 @@ function getFirewallRule ($fwsettings) { $donothing=$false } elseif ((($fwsetting.Key -eq 'Name') -or ($fwsetting.Key -eq 'DisplayName')) -and ($output."Rule Name" -eq $fwsettings.$($fwsetting.Key))) { $donothing=$false + } elseif (($fwsetting.Key -eq 'Profile') -and ($output."Profiles" -eq $fwsettings.$($fwsetting.Key))) { + $donothing=$false } else { $diff=$true; $difference+=@($fwsettings.$($fwsetting.Key)); From 469d22df973508b163a4aae028b8dff6faafbb3f Mon Sep 17 00:00:00 2001 From: Dreamcat4 Date: Mon, 5 Oct 2015 21:53:11 +0100 Subject: [PATCH 0811/2522] fix: The names of firewall profiles are different on win10 & win2008r2 Hi again. This commit removes a small portion of your script's own internal error checking. In specific: for the value of the profile: key. This is essential to avoid errors on other verisons of the windows operating system which are not win2008r2 (your version). For example: on win10 (and most likely win8x too), the names of the profiles don't include the values 'current' and 'all'. But instead the values are 'Public' 'Private' 'Domain' and 'Any. But in addition, there are also certain combinatorial values, such as profile=Public,Private etc. Which is too many to error check yourself. Yet removing the error checking here should not cause any ill effects however: since the netsh advfirewall ... cmds themselves to add / remove / modify actually to their own error checking of the profile=value. So when the cmd is run, it will error out itself with an appropriate / informative error msg. No harm done. Therefore please remove the highlighed portions from your own script. It is essential for interoperability with win10 and win8x. Many thanks. --- windows/win_firewall_rule.ps1 | 8 +------- windows/win_firewall_rule.py | 6 +++--- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/windows/win_firewall_rule.ps1 b/windows/win_firewall_rule.ps1 index 0b0a2cd54f9..8ef2d83aff6 100644 --- a/windows/win_firewall_rule.ps1 +++ b/windows/win_firewall_rule.ps1 @@ -246,13 +246,7 @@ foreach ($arg in $args){ }; $winprofile=Get-Attr $params "profile" "current"; -if (($winprofile -ne 'current') -or ($winprofile -ne 'domain') -or ($winprofile -ne 'standard') -or ($winprofile -ne 'all') ) { - $misArg+="Profile"; - $msg+=@("for the Profile parameter only the values 'current', 'domain', 'standard' or 'all' are allowed"); -} else { - - $fwsettings.Add("profile", $winprofile) -} +$fwsettings.Add("profile", $winprofile) if ($($($misArg|measure).count) -gt 0){ $result=New-Object psobject @{ diff --git a/windows/win_firewall_rule.py b/windows/win_firewall_rule.py index 295979b248f..ecdec5882cd 100644 --- a/windows/win_firewall_rule.py +++ b/windows/win_firewall_rule.py @@ -90,10 +90,10 @@ default: null required: false profile: - describtion: + description: - the profile this rule applies to - default: current - choices: ['current', 'domain', 'standard', 'all'] + default: null + required: false force: description: - Enforces the change if a rule with different values exists From dcaa79494995e50930a0b0b66995bc18cb6f5668 Mon Sep 17 00:00:00 2001 From: Dreamcat4 Date: Tue, 6 Oct 2015 10:47:27 +0100 Subject: [PATCH 0812/2522] fix: update documentation with new module name "win_firewall_rule" --- windows/win_firewall_rule.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/windows/win_firewall_rule.py b/windows/win_firewall_rule.py index ecdec5882cd..1463719356d 100644 --- a/windows/win_firewall_rule.py +++ b/windows/win_firewall_rule.py @@ -19,7 +19,7 @@ DOCUMENTATION = ''' --- -module: win_fw +module: win_firewall_rule version_added: "2.0" author: Timothy Vandenbrande short_description: Windows firewall automation @@ -99,13 +99,13 @@ - Enforces the change if a rule with different values exists default: false required: false - + ''' EXAMPLES = ''' -# create smtp firewall rule - action: win_fw +- name: Firewall rule to allow smtp on TCP port 25 + action: win_firewall_rule args: name: smtp state: present From 0ff1776a8424b166f632ee9199a04d85454dd76d Mon Sep 17 00:00:00 2001 From: James Cammarata Date: Tue, 6 Oct 2015 08:17:58 -0400 Subject: [PATCH 0813/2522] Updating version for 2.0.0-0.3.beta1 release --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index ee36851a03e..8b31b2b4fdb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -${version} +2.0.0-0.3.beta1 From ece9c2b43aba70228efdcf4efe36eb578b81abd3 Mon Sep 17 00:00:00 2001 From: Dreamcat4 Date: Tue, 6 Oct 2015 14:03:27 +0100 Subject: [PATCH 0814/2522] fix: Add 'enable:' flag for enabling existing rules which are disabled by default. This is a very much needed flag. To turn on/off existing firewall rules. And like the recent fix of the 'Profile' key, the netsh cmd prints 'Enabled' in the textual output. (at least on win10 it does). So again a similar small code added for the necessary exception handling when the difference check happens. Please merge / push upstream like the other fixes. Many thanks. This is the last fix I have put together for this patch set. So I will raise my PR now. But if you want to fix more bugs, it seems there may be others. In terms of the control code. Sometimes it will delete a rule under 'force' condition (when found difference) - but instead it is supposed to just modify the existing rule. Some weird behaviour regarding that. The other problem is that ansible does not return the error text printed by 'netsh' cmd verbatim... but it should as that makes debugging these errors a *lot* easier. --- windows/win_firewall_rule.ps1 | 18 ++++++++++++++++-- windows/win_firewall_rule.py | 9 ++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/windows/win_firewall_rule.ps1 b/windows/win_firewall_rule.ps1 index 8ef2d83aff6..63ada997456 100644 --- a/windows/win_firewall_rule.ps1 +++ b/windows/win_firewall_rule.ps1 @@ -22,7 +22,7 @@ function getFirewallRule ($fwsettings) { try { - + #$output = Get-NetFirewallRule -name $($fwsettings.name); $rawoutput=@(netsh advfirewall firewall show rule name="$($fwsettings.Name)") if (!($rawoutput -eq 'No rules match the specified criteria.')){ @@ -77,6 +77,8 @@ function getFirewallRule ($fwsettings) { $donothing=$false } elseif (($fwsetting.Key -eq 'Profile') -and ($output."Profiles" -eq $fwsettings.$($fwsetting.Key))) { $donothing=$false + } elseif (($fwsetting.Key -eq 'Enable') -and ($output."Enabled" -eq $fwsettings.$($fwsetting.Key))) { + $donothing=$false } else { $diff=$true; $difference+=@($fwsettings.$($fwsetting.Key)); @@ -196,6 +198,7 @@ $fwsettings=@{} # Variabelise the arguments $params=Parse-Args $args; +$enable=Get-Attr $params "enable" $null; $state=Get-Attr $params "state" "present"; $name=Get-Attr $params "name" ""; $direction=Get-Attr $params "direction" ""; @@ -203,6 +206,17 @@ $force=Get-Attr $params "force" $false; $action=Get-Attr $params "action" ""; # Check the arguments +if ($enable -ne $null) { + if ($enable -eq $true) { + $fwsettings.Add("Enable", "yes"); + } elseif ($enable -eq $false) { + $fwsettings.Add("Enable", "no"); + } else { + $misArg+="enable"; + $msg+=@("for the enable parameter only yes and no is allowed"); + }; +}; + if (($state -ne "present") -And ($state -ne "absent")){ $misArg+="state"; $msg+=@("for the state parameter only present and absent is allowed"); @@ -294,7 +308,7 @@ switch ($state.ToLower()){ }; Exit-Json $result; } - } elseif ($capture.identical -eq $false) { + } elseif ($capture.identical -eq $false) { if ($force -eq $true) { $capture=removeFirewallRule($fwsettings); $msg+=$capture.msg; diff --git a/windows/win_firewall_rule.py b/windows/win_firewall_rule.py index 1463719356d..64ec3050474 100644 --- a/windows/win_firewall_rule.py +++ b/windows/win_firewall_rule.py @@ -25,7 +25,13 @@ short_description: Windows firewall automation description: - allows you to create/remove/update firewall rules -options: +options: + enable: + description: + - is this firewall rule enabled or disabled + default: null + required: false + choices: ['yes', 'no'] state: description: - create/remove/update or powermanage your VM @@ -108,6 +114,7 @@ action: win_firewall_rule args: name: smtp + enabled: yes state: present localport: 25 action: allow From 576b5e2fc3f6367a9284884485bb92501dac32b5 Mon Sep 17 00:00:00 2001 From: = Date: Wed, 7 Oct 2015 05:25:28 +0100 Subject: [PATCH 0815/2522] Document module limitations to resolve https://github.com/ansible/ansible-modules-extras/issues/908 --- windows/win_environment.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/windows/win_environment.py b/windows/win_environment.py index 8d4a1701695..522eff6a8d3 100644 --- a/windows/win_environment.py +++ b/windows/win_environment.py @@ -25,11 +25,10 @@ --- module: win_environment version_added: "2.0" -short_description: Modifies environment variables on windows guests +short_description: Modifies environment variables on windows hosts. description: - - Uses .net Environment to set or remove environment variables. - - Can set at User, Machine or Process level. - - Note that usual rules apply, so existing environments will not change until new processes are started. + - Uses .net Environment to set or remove environment variables and can set at User, Machine or Process level. + - User level environment variables will be set, but not available until the user has logged off and on again. options: state: description: @@ -62,6 +61,13 @@ - process - user author: "Jon Hawkesworth (@jhawkesworth)" +notes: + - This module does not broadcast change events. + This means that the minority of windows applications which can have + their environment changed without restarting will not be notified and + therefore will need restarting to pick up new environment settings. + User level environment variables will require the user to log out + and in again before they become available. ''' EXAMPLES = ''' From 6e80ed57d2c2dce61919e8f326eb33ed874d341f Mon Sep 17 00:00:00 2001 From: Luis Rodriguez Date: Wed, 7 Oct 2015 19:04:37 +0200 Subject: [PATCH 0816/2522] Fix bug #1067 --- database/mysql/mysql_replication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/mysql/mysql_replication.py b/database/mysql/mysql_replication.py index c833c244f12..348f49df6c2 100644 --- a/database/mysql/mysql_replication.py +++ b/database/mysql/mysql_replication.py @@ -267,7 +267,7 @@ def main(): login_host=dict(default="localhost"), login_port=dict(default=3306, type='int'), login_unix_socket=dict(default=None), - mode=dict(default="getslave", choices=["getmaster", "getslave", "changemaster", "stopslave", "startslave"]), + mode=dict(default="getslave", choices=["getmaster", "getslave", "changemaster", "stopslave", "startslave", "resetslave", "resetslaveall"]), master_auto_position=dict(default=False, type='bool'), master_host=dict(default=None), master_user=dict(default=None), From 8d3122169d8873eb262bf51cbd6c691b311d3611 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Wed, 7 Oct 2015 11:52:52 -0400 Subject: [PATCH 0817/2522] added missing GPL licences fixes #508 --- notification/grove.py | 15 +++++++++++++++ packaging/dpkg_selections.py | 16 ++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/notification/grove.py b/notification/grove.py index 4e4a0b5b684..5e6db30d9a2 100644 --- a/notification/grove.py +++ b/notification/grove.py @@ -1,5 +1,20 @@ #!/usr/bin/python # -*- coding: utf-8 -*- +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . DOCUMENTATION = ''' --- diff --git a/packaging/dpkg_selections.py b/packaging/dpkg_selections.py index f09ff9a9f00..fa0f73a713b 100644 --- a/packaging/dpkg_selections.py +++ b/packaging/dpkg_selections.py @@ -1,4 +1,20 @@ #!/usr/bin/python +# -*- coding: utf-8 -*- +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . DOCUMENTATION = ''' --- From 205115ea1fc85b99fd7e505b58e84db3a4377f5f Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Wed, 7 Oct 2015 23:17:50 -0400 Subject: [PATCH 0818/2522] updated to use shared region docs from ec2 --- cloud/amazon/dynamodb_table.py | 10 ++---- cloud/amazon/ec2_ami_copy.py | 9 ++---- cloud/amazon/ec2_elb_facts.py | 4 ++- cloud/amazon/ec2_eni.py | 6 ++-- cloud/amazon/ec2_eni_facts.py | 39 +++++++++++------------ cloud/amazon/ec2_vpc_igw.py | 10 ++---- cloud/amazon/ec2_vpc_route_table.py | 5 +-- cloud/amazon/ec2_vpc_route_table_facts.py | 11 ++----- cloud/amazon/ec2_vpc_subnet.py | 13 ++++---- cloud/amazon/ec2_vpc_subnet_facts.py | 13 +++----- cloud/amazon/ec2_win_password.py | 10 ++---- cloud/amazon/ecs_cluster.py | 2 ++ cloud/amazon/ecs_task.py | 2 ++ cloud/amazon/ecs_taskdefinition.py | 3 +- cloud/amazon/route53_health_check.py | 4 ++- cloud/amazon/route53_zone.py | 4 ++- cloud/amazon/s3_bucket.py | 10 ++---- cloud/amazon/s3_lifecycle.py | 10 ++---- cloud/amazon/s3_logging.py | 12 +++---- cloud/amazon/sqs_queue.py | 10 ++---- cloud/amazon/sts_assume_role.py | 4 ++- 21 files changed, 83 insertions(+), 108 deletions(-) diff --git a/cloud/amazon/dynamodb_table.py b/cloud/amazon/dynamodb_table.py index 29ba230fe48..1daf55e9d18 100644 --- a/cloud/amazon/dynamodb_table.py +++ b/cloud/amazon/dynamodb_table.py @@ -71,13 +71,9 @@ - Write throughput capacity (units) to provision. required: false default: 1 - region: - description: - - The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used. - required: false - aliases: ['aws_region', 'ec2_region'] - -extends_documentation_fragment: aws +extends_documentation_fragment: + - aws + - ec2 """ EXAMPLES = ''' diff --git a/cloud/amazon/ec2_ami_copy.py b/cloud/amazon/ec2_ami_copy.py index ff9bde88022..49afed7bf98 100644 --- a/cloud/amazon/ec2_ami_copy.py +++ b/cloud/amazon/ec2_ami_copy.py @@ -26,11 +26,6 @@ description: - the source region that AMI should be copied from required: true - region: - description: - - the destination region that AMI should be copied to - required: true - aliases: ['aws_region', 'ec2_region', 'dest_region'] source_image_id: description: - the id of the image in source region that should be copied @@ -63,7 +58,9 @@ default: null author: Amir Moulavi -extends_documentation_fragment: aws +extends_documentation_fragment: + - aws + - ec2 ''' EXAMPLES = ''' diff --git a/cloud/amazon/ec2_elb_facts.py b/cloud/amazon/ec2_elb_facts.py index 554b75c951d..aaf3049bfd2 100644 --- a/cloud/amazon/ec2_elb_facts.py +++ b/cloud/amazon/ec2_elb_facts.py @@ -28,7 +28,9 @@ required: false default: null aliases: ['elb_ids', 'ec2_elbs'] -extends_documentation_fragment: aws +extends_documentation_fragment: + - aws + - ec2 ''' EXAMPLES = ''' diff --git a/cloud/amazon/ec2_eni.py b/cloud/amazon/ec2_eni.py index 59a26291388..72e5483e36b 100644 --- a/cloud/amazon/ec2_eni.py +++ b/cloud/amazon/ec2_eni.py @@ -74,8 +74,10 @@ source_dest_check: description: - By default, interfaces perform source/destination checks. NAT instances however need this check to be disabled. You can only specify this flag when the interface is being modified, not on creation. - required: false -extends_documentation_fragment: aws + required: false +extends_documentation_fragment: + - aws + - ec2 ''' EXAMPLES = ''' diff --git a/cloud/amazon/ec2_eni_facts.py b/cloud/amazon/ec2_eni_facts.py index 981358c33af..2014c3e8eb5 100644 --- a/cloud/amazon/ec2_eni_facts.py +++ b/cloud/amazon/ec2_eni_facts.py @@ -27,7 +27,9 @@ - The ID of the ENI. Pass this option to gather facts about a particular ENI, otherwise, all ENIs are returned. required: false default: null -extends_documentation_fragment: aws +extends_documentation_fragment: + - aws + - ec2 ''' EXAMPLES = ''' @@ -53,14 +55,14 @@ def get_error_message(xml_string): - + root = ET.fromstring(xml_string) - for message in root.findall('.//Message'): + for message in root.findall('.//Message'): return message.text - - + + def get_eni_info(interface): - + interface_info = {'id': interface.id, 'subnet_id': interface.subnet_id, 'vpc_id': interface.vpc_id, @@ -72,7 +74,7 @@ def get_eni_info(interface): 'source_dest_check': interface.source_dest_check, 'groups': dict((group.id, group.name) for group in interface.groups), } - + if interface.attachment is not None: interface_info['attachment'] = {'attachment_id': interface.attachment.id, 'instance_id': interface.attachment.instance_id, @@ -81,23 +83,23 @@ def get_eni_info(interface): 'attach_time': interface.attachment.attach_time, 'delete_on_termination': interface.attachment.delete_on_termination, } - + return interface_info - + def list_eni(connection, module): - + eni_id = module.params.get("eni_id") interface_dict_array = [] - + try: all_eni = connection.get_all_network_interfaces(eni_id) except BotoServerError as e: module.fail_json(msg=get_error_message(e.args[2])) - + for interface in all_eni: interface_dict_array.append(get_eni_info(interface)) - + module.exit_json(interfaces=interface_dict_array) @@ -108,14 +110,14 @@ def main(): eni_id = dict(default=None) ) ) - + module = AnsibleModule(argument_spec=argument_spec) if not HAS_BOTO: module.fail_json(msg='boto required for this module') - + region, ec2_url, aws_connect_params = get_aws_connection_info(module) - + if region: try: connection = connect_to_aws(boto.ec2, region, **aws_connect_params) @@ -125,11 +127,8 @@ def main(): module.fail_json(msg="region must be specified") list_eni(connection, module) - + from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * -# this is magic, see lib/ansible/module_common.py -#<> - main() diff --git a/cloud/amazon/ec2_vpc_igw.py b/cloud/amazon/ec2_vpc_igw.py index e374580433a..16437abf073 100644 --- a/cloud/amazon/ec2_vpc_igw.py +++ b/cloud/amazon/ec2_vpc_igw.py @@ -27,19 +27,15 @@ - The VPC ID for the VPC in which to manage the Internet Gateway. required: true default: null - region: - description: - - The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used. See U(http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region) - required: false - default: null - aliases: [ 'aws_region', 'ec2_region' ] state: description: - Create or terminate the IGW required: false default: present choices: [ 'present', 'absent' ] -extends_documentation_fragment: aws +extends_documentation_fragment: + - aws + - ec2 ''' EXAMPLES = ''' diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index cc2b5ff8ee7..0976c77e282 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -61,8 +61,9 @@ description: - "VPC ID of the VPC in which to create the route table." required: true - -extends_documentation_fragment: aws +extends_documentation_fragment: + - aws + - ec2 ''' EXAMPLES = ''' diff --git a/cloud/amazon/ec2_vpc_route_table_facts.py b/cloud/amazon/ec2_vpc_route_table_facts.py index 7d37b2d79a2..f93ab060fd6 100644 --- a/cloud/amazon/ec2_vpc_route_table_facts.py +++ b/cloud/amazon/ec2_vpc_route_table_facts.py @@ -27,14 +27,9 @@ - A dict of filters to apply. Each dict item consists of a filter key and a filter value. See U(http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeRouteTables.html) for possible filters. required: false default: null - region: - description: - - The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used. See U(http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region) - required: false - default: null - aliases: [ 'aws_region', 'ec2_region' ] - -extends_documentation_fragment: aws +extends_documentation_fragment: + - aws + - ec2 ''' EXAMPLES = ''' diff --git a/cloud/amazon/ec2_vpc_subnet.py b/cloud/amazon/ec2_vpc_subnet.py index 45e84f66939..64b1df387d3 100644 --- a/cloud/amazon/ec2_vpc_subnet.py +++ b/cloud/amazon/ec2_vpc_subnet.py @@ -49,8 +49,9 @@ - "VPC ID of the VPC in which to create the subnet." required: false default: null - -extends_documentation_fragment: aws +extends_documentation_fragment: + - aws + - ec2 ''' EXAMPLES = ''' @@ -230,14 +231,14 @@ def main(): vpc_id = dict(default=None, required=True) ) ) - + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - + if not HAS_BOTO: module.fail_json(msg='boto is required for this module') region, ec2_url, aws_connect_params = get_aws_connection_info(module) - + if region: try: connection = connect_to_aws(boto.vpc, region, **aws_connect_params) @@ -269,4 +270,4 @@ def main(): if __name__ == '__main__': main() - + diff --git a/cloud/amazon/ec2_vpc_subnet_facts.py b/cloud/amazon/ec2_vpc_subnet_facts.py index c3c8268579a..48f514ba49f 100644 --- a/cloud/amazon/ec2_vpc_subnet_facts.py +++ b/cloud/amazon/ec2_vpc_subnet_facts.py @@ -27,14 +27,9 @@ - A dict of filters to apply. Each dict item consists of a filter key and a filter value. See U(http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeSubnets.html) for possible filters. required: false default: null - region: - description: - - The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used. See U(http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region) - required: false - default: null - aliases: [ 'aws_region', 'ec2_region' ] - -extends_documentation_fragment: aws +extends_documentation_fragment: + - aws + - ec2 ''' EXAMPLES = ''' @@ -127,4 +122,4 @@ def main(): from ansible.module_utils.ec2 import * if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/cloud/amazon/ec2_win_password.py b/cloud/amazon/ec2_win_password.py index f3a687c2371..e3a012291e3 100644 --- a/cloud/amazon/ec2_win_password.py +++ b/cloud/amazon/ec2_win_password.py @@ -36,12 +36,6 @@ - The passphrase for the instance key pair. The key must use DES or 3DES encryption for this module to decrypt it. You can use openssl to convert your password protected keys if they do not use DES or 3DES. ex) openssl rsa -in current_key -out new_key -des3. required: false default: null - region: - description: - - The AWS region to use. Must be specified if ec2_url is not used. If not specified then the value of the EC2_REGION environment variable, if any, is used. - required: false - default: null - aliases: [ 'aws_region', 'ec2_region' ] wait: version_added: "2.0" description: @@ -56,7 +50,9 @@ required: false default: 120 -extends_documentation_fragment: aws +extends_documentation_fragment: + - aws + - ec2 ''' EXAMPLES = ''' diff --git a/cloud/amazon/ecs_cluster.py b/cloud/amazon/ecs_cluster.py index 9dac7cd5bad..6b37762521f 100644 --- a/cloud/amazon/ecs_cluster.py +++ b/cloud/amazon/ecs_cluster.py @@ -44,6 +44,8 @@ description: - The number of times to wait for the cluster to have an instance required: false +extends_documentation_fragment: + - ec2 ''' EXAMPLES = ''' diff --git a/cloud/amazon/ecs_task.py b/cloud/amazon/ecs_task.py index 8a84f74ad43..000ce68b56c 100644 --- a/cloud/amazon/ecs_task.py +++ b/cloud/amazon/ecs_task.py @@ -56,6 +56,8 @@ description: - A value showing who or what started the task (for informational purposes) required: False +extends_documentation_fragment: + - ec2 ''' EXAMPLES = ''' diff --git a/cloud/amazon/ecs_taskdefinition.py b/cloud/amazon/ecs_taskdefinition.py index 8f5cc068393..50205d6691c 100644 --- a/cloud/amazon/ecs_taskdefinition.py +++ b/cloud/amazon/ecs_taskdefinition.py @@ -52,7 +52,8 @@ - A list of names of volumes to be attached required: False type: list of name - +extends_documentation_fragment: + - ec2 ''' EXAMPLES = ''' diff --git a/cloud/amazon/route53_health_check.py b/cloud/amazon/route53_health_check.py index 5ab0cc5d77d..a4dcfea6b30 100644 --- a/cloud/amazon/route53_health_check.py +++ b/cloud/amazon/route53_health_check.py @@ -89,7 +89,9 @@ default: 3 choices: [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ] author: "zimbatm (@zimbatm)" -extends_documentation_fragment: aws +extends_documentation_fragment: + - aws + - ec2 ''' EXAMPLES = ''' diff --git a/cloud/amazon/route53_zone.py b/cloud/amazon/route53_zone.py index d5ba0dcd617..33a76ea0c4a 100644 --- a/cloud/amazon/route53_zone.py +++ b/cloud/amazon/route53_zone.py @@ -46,7 +46,9 @@ - Comment associated with the zone required: false default: '' -extends_documentation_fragment: aws +extends_documentation_fragment: + - aws + - ec2 author: "Christopher Troup (@minichate)" ''' diff --git a/cloud/amazon/s3_bucket.py b/cloud/amazon/s3_bucket.py index cc0442eccc1..aa6cc9d1e41 100644 --- a/cloud/amazon/s3_bucket.py +++ b/cloud/amazon/s3_bucket.py @@ -38,11 +38,6 @@ - The JSON policy as a string. required: false default: null - region: - description: - - "AWS region to create the bucket in. If not set then the value of the AWS_REGION and EC2_REGION environment variables are checked, followed by the aws_region and ec2_region settings in the Boto config file. If none of those are set the region defaults to the S3 Location: US Standard." - required: false - default: null s3_url: description: - S3 URL endpoint for usage with Eucalypus, fakes3, etc. Otherwise assumes AWS @@ -71,8 +66,9 @@ required: false default: no choices: [ 'yes', 'no' ] - -extends_documentation_fragment: aws +extends_documentation_fragment: + - aws + - ec2 ''' EXAMPLES = ''' diff --git a/cloud/amazon/s3_lifecycle.py b/cloud/amazon/s3_lifecycle.py index 7a54365c8bd..da8e8a8402f 100644 --- a/cloud/amazon/s3_lifecycle.py +++ b/cloud/amazon/s3_lifecycle.py @@ -46,11 +46,6 @@ - "Prefix identifying one or more objects to which the rule applies. If no prefix is specified, the rule will apply to the whole bucket." required: false default: null - region: - description: - - "AWS region to create the bucket in. If not set then the value of the AWS_REGION and EC2_REGION environment variables are checked, followed by the aws_region and ec2_region settings in the Boto config file. If none of those are set the region defaults to the S3 Location: US Standard." - required: false - default: null rule_id: description: - "Unique identifier for the rule. The value cannot be longer than 255 characters. A unique value for the rule will be generated if no value is provided." @@ -84,8 +79,9 @@ - "Indicates when, in days, an object transitions to a different storage class. If transition_date is not specified, this parameter is required." required: false default: null - -extends_documentation_fragment: aws +extends_documentation_fragment: + - aws + - ec2 ''' EXAMPLES = ''' diff --git a/cloud/amazon/s3_logging.py b/cloud/amazon/s3_logging.py index 75b3fe73508..8047a5083bc 100644 --- a/cloud/amazon/s3_logging.py +++ b/cloud/amazon/s3_logging.py @@ -26,11 +26,6 @@ description: - "Name of the s3 bucket." required: true - region: - description: - - "AWS region to create the bucket in. If not set then the value of the AWS_REGION and EC2_REGION environment variables are checked, followed by the aws_region and ec2_region settings in the Boto config file. If none of those are set the region defaults to the S3 Location: US Standard." - required: false - default: null state: description: - "Enable or disable logging." @@ -47,8 +42,9 @@ - "The prefix that should be prepended to the generated log files written to the target_bucket." required: false default: "" - -extends_documentation_fragment: aws +extends_documentation_fragment: + - aws + - ec2 ''' EXAMPLES = ''' @@ -181,4 +177,4 @@ def main(): from ansible.module_utils.ec2 import * if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/cloud/amazon/sqs_queue.py b/cloud/amazon/sqs_queue.py index 3febc8981f2..0d098c6df52 100644 --- a/cloud/amazon/sqs_queue.py +++ b/cloud/amazon/sqs_queue.py @@ -61,13 +61,9 @@ - The receive message wait time in seconds. required: false default: null - region: - description: - - The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used. - required: false - aliases: ['aws_region', 'ec2_region'] - -extends_documentation_fragment: aws +extends_documentation_fragment: + - aws + - ec2 """ EXAMPLES = ''' diff --git a/cloud/amazon/sts_assume_role.py b/cloud/amazon/sts_assume_role.py index 7eec28b843a..aa48bf8daa5 100644 --- a/cloud/amazon/sts_assume_role.py +++ b/cloud/amazon/sts_assume_role.py @@ -58,7 +58,9 @@ default: null notes: - In order to use the assumed role in a following playbook task you must pass the access_key, access_secret and access_token -extends_documentation_fragment: aws +extends_documentation_fragment: + - aws + - ec2 ''' EXAMPLES = ''' From 8ffc11713f362b4905ae91dc218723183ad21ae0 Mon Sep 17 00:00:00 2001 From: Ramon de la Fuente Date: Thu, 8 Oct 2015 10:18:23 +0200 Subject: [PATCH 0819/2522] module guidelines compliency --- notification/slack.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/notification/slack.py b/notification/slack.py index 96102b3d5fe..de75584fc48 100644 --- a/notification/slack.py +++ b/notification/slack.py @@ -24,7 +24,7 @@ short_description: Send Slack notifications description: - The M(slack) module sends notifications to U(http://slack.com) via the Incoming WebHook integration -version_added: 1.6 +version_added: "1.6" author: "Ramon de la Fuente (@ramondelafuente)" options: domain: @@ -33,6 +33,7 @@ C(future500.slack.com)) In 1.8 and beyond, this is deprecated and may be ignored. See token documentation for information. required: false + default: None token: description: - Slack integration token. This authenticates you to the slack service. @@ -48,15 +49,17 @@ description: - Message to send. required: false + default: None channel: description: - Channel to send the message to. If absent, the message goes to the channel selected for the I(token). required: false + default: None username: description: - This is the sender of the message. required: false - default: ansible + default: "Ansible" icon_url: description: - Url for the message sender's icon (default C(http://www.ansible.com/favicon.ico)) @@ -66,6 +69,7 @@ - Emoji for the message sender. See Slack documentation for options. (if I(icon_emoji) is set, I(icon_url) will not be used) required: false + default: None link_names: description: - Automatically create links for channels and usernames in I(msg). @@ -78,6 +82,7 @@ description: - Setting for the message parser at Slack required: false + default: None choices: - 'full' - 'none' @@ -91,7 +96,7 @@ - 'yes' - 'no' color: - version_added: 2.0 + version_added: "2.0" description: - Allow text to use default colors - use the default of 'normal' to not send a custom color bar at the start of the message required: false @@ -105,6 +110,7 @@ description: - Define a list of attachments. This list mirrors the Slack JSON API. For more information, see https://api.slack.com/docs/attachments required: false + default: None """ EXAMPLES = """ @@ -243,4 +249,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.urls import * -main() + +if __name__ == '__main__': + main() From 559a7e7a32f533d70331b238e4a9cd70d23534de Mon Sep 17 00:00:00 2001 From: Ramon de la Fuente Date: Sun, 16 Nov 2014 22:40:37 +0100 Subject: [PATCH 0820/2522] adding the deploy_helper module --- web_infrastructure/deploy_helper.py | 341 ++++++++++++++++++++++++++++ 1 file changed, 341 insertions(+) create mode 100644 web_infrastructure/deploy_helper.py diff --git a/web_infrastructure/deploy_helper.py b/web_infrastructure/deploy_helper.py new file mode 100644 index 00000000000..b7bf9a3eba9 --- /dev/null +++ b/web_infrastructure/deploy_helper.py @@ -0,0 +1,341 @@ +#!/usr/bin/python + +DOCUMENTATION = ''' +--- +module: deploy_helper +version_added: "1.8" +author: Ramon de la Fuente, Jasper N. Brouwer +short_description: Manages the folders for deploy of a project +description: + - Manages some of the steps common in deploying projects. + It creates a folder structure, cleans up old releases and manages a symlink for the current release. + + For more information, see the :doc:`guide_deploy_helper` + +options: + path: + required: true + aliases: ['dest'] + description: + - the root path of the project. Alias I(dest). + + state: + required: false + choices: [ present, finalize, absent, clean, query ] + default: present + description: + - the state of the project. + C(query) will only gather facts, + C(present) will create the project, + C(finalize) will create a symlink to the newly deployed release, + C(clean) will remove failed & old releases, + C(absent) will remove the project folder (synonymous to M(file) with state=absent) + + release: + required: false + description: + - the release version that is being deployed (defaults to a timestamp %Y%m%d%H%M%S). This parameter is + optional during C(state=present), but needs to be set explicitly for C(state=finalize). You can use the + generated fact C(release={{ deploy_helper.new_release }}) + + releases_path: + required: false + default: releases + description: + - the name of the folder that will hold the releases. This can be relative to C(path) or absolute. + + shared_path: + required: false + default: shared + description: + - the name of the folder that will hold the shared resources. This can be relative to C(path) or absolute. + If this is set to an empty string, no shared folder will be created. + + current_path: + required: false + default: current + description: + - the name of the symlink that is created when the deploy is finalized. Used in C(finalize) and C(clean). + + unfinished_filename: + required: false + default: DEPLOY_UNFINISHED + description: + - the name of the file that indicates a deploy has not finished. All folders in the releases_path that + contain this file will be deleted on C(state=finalize) with clean=True, or C(state=clean). This file is + automatically deleted from the I(new_release_path) during C(state=finalize). + + clean: + required: false + default: True + description: + - Whether to run the clean procedure in case of C(state=finalize). + + keep_releases: + required: false + default: 5 + description: + - the number of old releases to keep when cleaning. Used in C(finalize) and C(clean). Any unfinished builds + will be deleted first, so only correct releases will count. + +notes: + - Facts are only returned for C(state=query) and C(state=present). If you use both, you should pass any overridden + parameters to both calls, otherwise the second call will overwrite the facts of the first one. + - When using C(state=clean), the releases are ordered by creation date. You should be able to switch to a + new naming strategy without problems. + - Because of the default behaviour of generating the I(new_release) fact, this module will not be idempotent + unless you pass your own release name with C(release). Due to the nature of deploying software, this should not + be much of a problem. +''' + +EXAMPLES = ''' +Example usage for the deploy_helper module. + + tasks: + # Typical usage: + - deploy_helper: path=/path/to/root state=present + ...some_build_steps_here, like a git clone to {{ deploy_helper.new_release_path }} for example... + - deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=finalize + + # Gather information only + - deploy_helper: path=/path/to/root state=query + # Remember to set the 'release=' when you actually call state=present later + - deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=present + + # all paths can be absolute or relative (to 'path') + - deploy_helper: path=/path/to/root + releases_path=/var/www/project/releases + shared_path=/var/www/shared + current_path=/var/www/active + + # Using your own naming strategy: + - deploy_helper: path=/path/to/root release=v1.1.1 state=present + - deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=finalize + + # Postponing the cleanup of older builds: + - deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=finalize clean=False + ...anything you do before actually deleting older releases... + - deploy_helper: path=/path/to/root state=clean + + # Keeping more old releases: + - deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=finalize keep_releases=10 + # Or: + - deploy_helper: path=/path/to/root state=clean keep_releases=10 + + # Using a different unfinished_filename: + - deploy_helper: path=/path/to/root unfinished_filename=README.md release={{ deploy_helper.new_release }} state=finalize + +''' + +class DeployHelper(object): + + def __init__(self, module): + module.params['path'] = os.path.expanduser(module.params['path']) + + self.module = module + self.file_args = module.load_file_common_arguments(module.params) + + self.clean = module.params['clean'] + self.current_path = module.params['current_path'] + self.keep_releases = module.params['keep_releases'] + self.path = module.params['path'] + self.release = module.params['release'] + self.releases_path = module.params['releases_path'] + self.shared_path = module.params['shared_path'] + self.state = module.params['state'] + self.unfinished_filename = module.params['unfinished_filename'] + + def gather_facts(self): + current_path = os.path.join(self.path, self.current_path) + releases_path = os.path.join(self.path, self.releases_path) + if self.shared_path: + shared_path = os.path.join(self.path, self.shared_path) + else: + shared_path = None + + previous_release, previous_release_path = self._get_last_release(current_path) + + if not self.release and (self.state == 'query' or self.state == 'present'): + self.release = time.strftime("%Y%m%d%H%M%S") + + new_release_path = os.path.join(releases_path, self.release) + + return { + 'project_path': self.path, + 'current_path': current_path, + 'releases_path': releases_path, + 'shared_path': shared_path, + 'previous_release': previous_release, + 'previous_release_path': previous_release_path, + 'new_release': self.release, + 'new_release_path': new_release_path, + 'unfinished_filename': self.unfinished_filename + } + + def delete_path(self, path): + if not os.path.lexists(path): + return False + + if not os.path.isdir(path): + self.module.fail_json(msg="%s exists but is not a directory" % path) + + if not self.module.check_mode: + try: + shutil.rmtree(path, ignore_errors=False) + except Exception, e: + self.module.fail_json(msg="rmtree failed: %s" % str(e)) + + return True + + def create_path(self, path): + changed = False + + if not os.path.lexists(path): + changed = True + if not self.module.check_mode: + os.makedirs(path) + + elif not os.path.isdir(path): + self.module.fail_json(msg="%s exists but is not a directory" % path) + + changed += self.module.set_directory_attributes_if_different(self._get_file_args(path), changed) + + return changed + + def check_link(self, path): + if os.path.lexists(path): + if not os.path.islink(path): + self.module.fail_json(msg="%s exists but is not a symbolic link" % path) + + def create_link(self, source, link_name): + if not self.module.check_mode: + if os.path.islink(link_name): + os.unlink(link_name) + os.symlink(source, link_name) + + return True + + def remove_unfinished_file(self, new_release_path): + changed = False + unfinished_file_path = os.path.join(new_release_path, self.unfinished_filename) + if os.path.lexists(unfinished_file_path): + changed = True + if not self.module.check_mode: + os.remove(unfinished_file_path) + + return changed + + def remove_unfinished_builds(self, releases_path): + changes = 0 + + for release in os.listdir(releases_path): + if (os.path.isfile(os.path.join(releases_path, release, self.unfinished_filename))): + if self.module.check_mode: + changes += 1 + else: + changes += self.delete_path(os.path.join(releases_path, release)) + + return changes + + def cleanup(self, releases_path): + changes = 0 + + if os.path.lexists(releases_path): + releases = [ f for f in os.listdir(releases_path) if os.path.isdir(os.path.join(releases_path,f)) ] + + if not self.module.check_mode: + releases.sort( key=lambda x: os.path.getctime(os.path.join(releases_path,x)), reverse=True) + for release in releases[self.keep_releases:]: + changes += self.delete_path(os.path.join(releases_path, release)) + elif len(releases) > self.keep_releases: + changes += (len(releases) - self.keep_releases) + + return changes + + def _get_file_args(self, path): + file_args = self.file_args.copy() + file_args['path'] = path + return file_args + + def _get_last_release(self, current_path): + previous_release = None + previous_release_path = None + + if os.path.lexists(current_path): + previous_release_path = os.path.realpath(current_path) + previous_release = os.path.basename(previous_release_path) + + return previous_release, previous_release_path + +def main(): + + module = AnsibleModule( + argument_spec = dict( + path = dict(aliases=['dest'], required=True, type='str'), + release = dict(required=False, type='str', default=''), + releases_path = dict(required=False, type='str', default='releases'), + shared_path = dict(required=False, type='str', default='shared'), + current_path = dict(required=False, type='str', default='current'), + keep_releases = dict(required=False, type='int', default=5), + clean = dict(required=False, type='bool', default=True), + unfinished_filename = dict(required=False, type='str', default='DEPLOY_UNFINISHED'), + state = dict(required=False, choices=['present', 'absent', 'clean', 'finalize', 'query'], default='present') + ), + add_file_common_args = True, + supports_check_mode = True + ) + + deploy_helper = DeployHelper(module) + facts = deploy_helper.gather_facts() + + result = { + 'state': deploy_helper.state + } + + changes = 0 + + if deploy_helper.state == 'query': + result['ansible_facts'] = { 'deploy_helper': facts } + + elif deploy_helper.state == 'present': + deploy_helper.check_link(facts['current_path']) + changes += deploy_helper.create_path(facts['project_path']) + changes += deploy_helper.create_path(facts['releases_path']) + if deploy_helper.shared_path: + changes += deploy_helper.create_path(facts['shared_path']) + + result['ansible_facts'] = { 'deploy_helper': facts } + + elif deploy_helper.state == 'finalize': + if not deploy_helper.release: + module.fail_json(msg="'release' is a required parameter for state=finalize (try the 'deploy_helper.new_release' fact)") + if deploy_helper.keep_releases <= 0: + module.fail_json(msg="'keep_releases' should be at least 1") + + changes += deploy_helper.remove_unfinished_file(facts['new_release_path']) + changes += deploy_helper.create_link(facts['new_release_path'], facts['current_path']) + if deploy_helper.clean: + changes += deploy_helper.remove_unfinished_builds(facts['releases_path']) + changes += deploy_helper.cleanup(facts['releases_path']) + + elif deploy_helper.state == 'clean': + changes += deploy_helper.remove_unfinished_builds(facts['releases_path']) + changes += deploy_helper.cleanup(facts['releases_path']) + + elif deploy_helper.state == 'absent': + # destroy the facts + result['ansible_facts'] = { 'deploy_helper': [] } + changes += deploy_helper.delete_path(facts['project_path']) + + if changes > 0: + result['changed'] = True + else: + result['changed'] = False + + module.exit_json(**result) + + +# import module snippets +from ansible.module_utils.basic import * + +main() From 35ec03ec00f3d6055a91cf60677a468fb5f06f6d Mon Sep 17 00:00:00 2001 From: Ramon de la Fuente Date: Wed, 19 Nov 2014 23:00:08 +0100 Subject: [PATCH 0821/2522] removed link to guide, and added more documentation and examples --- web_infrastructure/deploy_helper.py | 117 +++++++++++++++++----------- 1 file changed, 70 insertions(+), 47 deletions(-) diff --git a/web_infrastructure/deploy_helper.py b/web_infrastructure/deploy_helper.py index b7bf9a3eba9..21c238d255b 100644 --- a/web_infrastructure/deploy_helper.py +++ b/web_infrastructure/deploy_helper.py @@ -5,12 +5,21 @@ module: deploy_helper version_added: "1.8" author: Ramon de la Fuente, Jasper N. Brouwer -short_description: Manages the folders for deploy of a project +short_description: Manages some of the steps common in deploying projects. description: - - Manages some of the steps common in deploying projects. - It creates a folder structure, cleans up old releases and manages a symlink for the current release. - - For more information, see the :doc:`guide_deploy_helper` + - The Deploy Helper manages some of the steps common in deploying software. + It creates a folder structure, manages a symlink for the current release + and cleans up old releases. + - "Running it with the C(state=query) or C(state=present) will return the C(deploy_helper) fact. + C(project_path), whatever you set in the path parameter, + C(current_path), the path to the symlink that points to the active release, + C(releases_path), the path to the folder to keep releases in, + C(shared_path), the path to the folder to keep shared resources in, + C(unfinished_filename), the file to check for to recognize unfinished builds, + C(previous_release), the release the 'current' symlink is pointing to, + C(previous_release_path), the full path to the 'current' symlink target, + C(new_release), either the 'release' parameter or a generated timestamp, + C(new_release_path), the path to the new release folder (not created by the module)." options: path: @@ -18,6 +27,7 @@ aliases: ['dest'] description: - the root path of the project. Alias I(dest). + Returned in the C(deploy_helper.project_path) fact. state: required: false @@ -26,23 +36,25 @@ description: - the state of the project. C(query) will only gather facts, - C(present) will create the project, - C(finalize) will create a symlink to the newly deployed release, + C(present) will create the project I(root) folder, and in it the I(releases) and I(shared) folders, + C(finalize) will remove the unfinished_filename file, create a symlink to the newly + deployed release and optionally clean old releases, C(clean) will remove failed & old releases, - C(absent) will remove the project folder (synonymous to M(file) with state=absent) + C(absent) will remove the project folder (synonymous to the M(file) module with C(state=absent)) release: required: false description: - - the release version that is being deployed (defaults to a timestamp %Y%m%d%H%M%S). This parameter is - optional during C(state=present), but needs to be set explicitly for C(state=finalize). You can use the - generated fact C(release={{ deploy_helper.new_release }}) + - the release version that is being deployed. Defaults to a timestamp format %Y%m%d%H%M%S (i.e. '20141119223359'). + This parameter is optional during C(state=present), but needs to be set explicitly for C(state=finalize). + You can use the generated fact C(release={{ deploy_helper.new_release }}). releases_path: required: false default: releases description: - the name of the folder that will hold the releases. This can be relative to C(path) or absolute. + Returned in the C(deploy_helper.releases_path) fact. shared_path: required: false @@ -50,12 +62,14 @@ description: - the name of the folder that will hold the shared resources. This can be relative to C(path) or absolute. If this is set to an empty string, no shared folder will be created. + Returned in the C(deploy_helper.shared_path) fact. current_path: required: false default: current description: - the name of the symlink that is created when the deploy is finalized. Used in C(finalize) and C(clean). + Returned in the C(deploy_helper.current_path) fact. unfinished_filename: required: false @@ -81,7 +95,7 @@ notes: - Facts are only returned for C(state=query) and C(state=present). If you use both, you should pass any overridden parameters to both calls, otherwise the second call will overwrite the facts of the first one. - - When using C(state=clean), the releases are ordered by creation date. You should be able to switch to a + - When using C(state=clean), the releases are ordered by I(creation date). You should be able to switch to a new naming strategy without problems. - Because of the default behaviour of generating the I(new_release) fact, this module will not be idempotent unless you pass your own release name with C(release). Due to the nature of deploying software, this should not @@ -89,41 +103,50 @@ ''' EXAMPLES = ''' -Example usage for the deploy_helper module. - - tasks: - # Typical usage: - - deploy_helper: path=/path/to/root state=present - ...some_build_steps_here, like a git clone to {{ deploy_helper.new_release_path }} for example... - - deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=finalize - - # Gather information only - - deploy_helper: path=/path/to/root state=query - # Remember to set the 'release=' when you actually call state=present later - - deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=present - - # all paths can be absolute or relative (to 'path') - - deploy_helper: path=/path/to/root - releases_path=/var/www/project/releases - shared_path=/var/www/shared - current_path=/var/www/active - - # Using your own naming strategy: - - deploy_helper: path=/path/to/root release=v1.1.1 state=present - - deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=finalize - - # Postponing the cleanup of older builds: - - deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=finalize clean=False - ...anything you do before actually deleting older releases... - - deploy_helper: path=/path/to/root state=clean - - # Keeping more old releases: - - deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=finalize keep_releases=10 - # Or: - - deploy_helper: path=/path/to/root state=clean keep_releases=10 - - # Using a different unfinished_filename: - - deploy_helper: path=/path/to/root unfinished_filename=README.md release={{ deploy_helper.new_release }} state=finalize + +# Typical usage: +- name: Initialize the deploy root and gather facts + deploy_helper: path=/path/to/root +- name: Clone the project to the new release folder + git: repo=git://foosball.example.org/path/to/repo.git dest={{ deploy.new_release_path }} version=v1.1.1" +- name: Add an unfinished file, to allow cleanup on successful finalize + file: path={{ deploy.new_release_path }}/{{ deploy.unfinished_filename }} state=touch +- name: Perform some build steps, like running your dependency manager for example + composer: command=install working_dir={{ deploy.new_release_path }} +- name: Finalize the deploy, removing the unfinished file and switching the symlink + deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=finalize + +# Retrieving facts before running a deploy +- name: Run query to gather facts without changing anything + deploy_helper: path=/path/to/root state=query + # Remember to set the 'release' parameter when you actually call state=present +- name: Initialize the deploy root + deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=present + +# all paths can be absolute or relative (to the 'path' parameter) +- deploy_helper: path=/path/to/root + releases_path=/var/www/project/releases + shared_path=/var/www/shared + current_path=/var/www/active + +# Using your own naming strategy: +- deploy_helper: path=/path/to/root release=v1.1.1 state=present +- deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=finalize + +# Using a different unfinished_filename: +- deploy_helper: path=/path/to/root + unfinished_filename=README.md + release={{ deploy_helper.new_release }} + state=finalize + +# Postponing the cleanup of older builds: +- deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=finalize clean=False +- deploy_helper: path=/path/to/root state=clean + +# Keeping more old releases: +- deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=finalize keep_releases=10 +# Or: +- deploy_helper: path=/path/to/root state=clean keep_releases=10 ''' From ce76d813c30dfae0f08b806cd346ae9fb03e6857 Mon Sep 17 00:00:00 2001 From: Ramon de la Fuente Date: Wed, 19 Nov 2014 23:41:34 +0100 Subject: [PATCH 0822/2522] added the folder structure and general explanation to examples, removed module name typo --- web_infrastructure/deploy_helper.py | 71 ++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 7 deletions(-) diff --git a/web_infrastructure/deploy_helper.py b/web_infrastructure/deploy_helper.py index 21c238d255b..ce36478020d 100644 --- a/web_infrastructure/deploy_helper.py +++ b/web_infrastructure/deploy_helper.py @@ -104,22 +104,69 @@ EXAMPLES = ''' +# General explanation, starting with an example folder structure for a project: + +root: + releases: + - 20140415234508 + - 20140415235146 + - 20140416082818 + + shared: + - sessions + - uploads + + current: -> releases/20140416082818 + + +The 'releases' folder holds all the available releases. A release is a complete build of the application being +deployed. This can be a clone of a repository for example, or a sync of a local folder on your filesystem. +Having timestamped folders is one way of having distinct releases, but you could choose your own strategy like +git tags or commit hashes. + +During a deploy, a new folder should be created in the releases folder and any build steps required should be +performed. Once the new build is ready, the deploy procedure is 'finalized' by replacing the 'current' symlink +with a link to this build. + +The 'shared' folder holds any resource that is shared between releases. Examples of this are web-server +session files, or files uploaded by users of your application. It's quite common to have symlinks from a release +folder pointing to a shared/subfolder, and creating these links would be automated as part of the build steps. + +The 'current' symlink points to one of the releases. Probably the latest one, unless a deploy is in progress. +The web-server's root for the project will go through this symlink, so the 'downtime' when switching to a new +release is reduced to the time it takes to switch the link. + +To distinguish between successful builds and unfinished ones, a file can be placed in the folder of the release +that is currently in progress. The existence of this file will mark it as unfinished, and allow an automated +procedure to remove it during cleanup. + + # Typical usage: - name: Initialize the deploy root and gather facts deploy_helper: path=/path/to/root - name: Clone the project to the new release folder - git: repo=git://foosball.example.org/path/to/repo.git dest={{ deploy.new_release_path }} version=v1.1.1" + git: repo=git://foosball.example.org/path/to/repo.git dest={{ deploy_helper.new_release_path }} version=v1.1.1 - name: Add an unfinished file, to allow cleanup on successful finalize - file: path={{ deploy.new_release_path }}/{{ deploy.unfinished_filename }} state=touch + file: path={{ deploy_helper.new_release_path }}/{{ deploy_helper.unfinished_filename }} state=touch - name: Perform some build steps, like running your dependency manager for example - composer: command=install working_dir={{ deploy.new_release_path }} + composer: command=install working_dir={{ deploy_helper.new_release_path }} +- name: Create some folders in the shared folder + file: path='{{ deploy_helper.shared_path }}/{{ item }}' state=directory + with_items: ['sessions', 'uploads'] +- name: Add symlinks from the new release to the shared folder + file: path='{{ deploy_helper.new_release_path }}/{{ item.path }}' + src='{{ deploy_helper.shared_path }}/{{ item.src }}' + state=link + with_items: + - { path: "app/sessions", src: "sessions" } + - { path: "web/uploads", src: "uploads" } - name: Finalize the deploy, removing the unfinished file and switching the symlink deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=finalize # Retrieving facts before running a deploy -- name: Run query to gather facts without changing anything +- name: Run 'state=query' to gather facts without changing anything deploy_helper: path=/path/to/root state=query - # Remember to set the 'release' parameter when you actually call state=present +# Remember to set the 'release' parameter when you actually call 'state=present' later - name: Initialize the deploy root deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=present @@ -129,7 +176,7 @@ shared_path=/var/www/shared current_path=/var/www/active -# Using your own naming strategy: +# Using your own naming strategy for releases (a version tag in this case): - deploy_helper: path=/path/to/root release=v1.1.1 state=present - deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=finalize @@ -142,12 +189,22 @@ # Postponing the cleanup of older builds: - deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=finalize clean=False - deploy_helper: path=/path/to/root state=clean +# Or running the cleanup ahead of the new deploy +- deploy_helper: path=/path/to/root state=clean +- deploy_helper: path=/path/to/root state=present # Keeping more old releases: - deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=finalize keep_releases=10 -# Or: +# Or, if you use 'clean=false' on finalize: - deploy_helper: path=/path/to/root state=clean keep_releases=10 +# Removing the entire project root folder +- deploy_helper: path=/path/to/root state=absent + +# Debugging the facts returned by the module +- deploy_helper: path=/path/to/root +- debug: var=deploy_helper + ''' class DeployHelper(object): From d14bb33a03b49e6f51205b88ae01d70875869b51 Mon Sep 17 00:00:00 2001 From: Ramon de la Fuente Date: Thu, 8 Oct 2015 12:59:21 +0200 Subject: [PATCH 0823/2522] updated the examples to the new Slack API --- notification/slack.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/notification/slack.py b/notification/slack.py index de75584fc48..e01272c0703 100644 --- a/notification/slack.py +++ b/notification/slack.py @@ -117,15 +117,13 @@ - name: Send notification message via Slack local_action: module: slack - domain: future500.slack.com - token: thetokengeneratedbyslack + token: thetoken/generatedby/slack msg: "{{ inventory_hostname }} completed" - name: Send notification message via Slack all options local_action: module: slack - domain: future500.slack.com - token: thetokengeneratedbyslack + token: thetoken/generatedby/slack msg: "{{ inventory_hostname }} completed" channel: "#ansible" username: "Ansible on {{ inventory_hostname }}" @@ -135,8 +133,7 @@ - name: insert a color bar in front of the message for visibility purposes and use the default webhook icon and name configured in Slack slack: - domain: future500.slack.com - token: thetokengeneratedbyslack + token: thetoken/generatedby/slack msg: "{{ inventory_hostname }} is alive!" color: good username: "" @@ -144,8 +141,7 @@ - name: Use the attachments API slack: - domain: future500.slack.com - token: thetokengeneratedbyslack + token: thetoken/generatedby/slack attachments: - text: "Display my system load on host A and B" color: "#ff00dd" @@ -157,6 +153,14 @@ - title: "System B" value: "load average: 5,16, 4,64, 2,43" short: "true" + +- name: Send notification message via Slack (deprecated API using domian) + local_action: + module: slack + domain: future500.slack.com + token: thetokengeneratedbyslack + msg: "{{ inventory_hostname }} completed" + """ OLD_SLACK_INCOMING_WEBHOOK = 'https://%s/services/hooks/incoming-webhook?token=%s' From a8ac83e39760f9632d6808250cfed406d79d6965 Mon Sep 17 00:00:00 2001 From: Ramon de la Fuente Date: Thu, 8 Oct 2015 14:25:07 +0200 Subject: [PATCH 0824/2522] module guideline changes --- web_infrastructure/deploy_helper.py | 47 +++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/web_infrastructure/deploy_helper.py b/web_infrastructure/deploy_helper.py index ce36478020d..beec56419e9 100644 --- a/web_infrastructure/deploy_helper.py +++ b/web_infrastructure/deploy_helper.py @@ -1,10 +1,29 @@ #!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2014, Jasper N. Brouwer +# (c) 2014, Ramon de la Fuente +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . DOCUMENTATION = ''' --- module: deploy_helper -version_added: "1.8" -author: Ramon de la Fuente, Jasper N. Brouwer +version_added: "2.0" +author: "Ramon de la Fuente (@ramondelafuente)" short_description: Manages some of the steps common in deploying projects. description: - The Deploy Helper manages some of the steps common in deploying software. @@ -23,14 +42,14 @@ options: path: - required: true + required: True aliases: ['dest'] description: - the root path of the project. Alias I(dest). Returned in the C(deploy_helper.project_path) fact. state: - required: false + required: False choices: [ present, finalize, absent, clean, query ] default: present description: @@ -43,21 +62,22 @@ C(absent) will remove the project folder (synonymous to the M(file) module with C(state=absent)) release: - required: false + required: False + default: None description: - the release version that is being deployed. Defaults to a timestamp format %Y%m%d%H%M%S (i.e. '20141119223359'). This parameter is optional during C(state=present), but needs to be set explicitly for C(state=finalize). You can use the generated fact C(release={{ deploy_helper.new_release }}). releases_path: - required: false + required: False default: releases description: - the name of the folder that will hold the releases. This can be relative to C(path) or absolute. Returned in the C(deploy_helper.releases_path) fact. shared_path: - required: false + required: False default: shared description: - the name of the folder that will hold the shared resources. This can be relative to C(path) or absolute. @@ -65,14 +85,14 @@ Returned in the C(deploy_helper.shared_path) fact. current_path: - required: false + required: False default: current description: - the name of the symlink that is created when the deploy is finalized. Used in C(finalize) and C(clean). Returned in the C(deploy_helper.current_path) fact. unfinished_filename: - required: false + required: False default: DEPLOY_UNFINISHED description: - the name of the file that indicates a deploy has not finished. All folders in the releases_path that @@ -80,13 +100,13 @@ automatically deleted from the I(new_release_path) during C(state=finalize). clean: - required: false + required: False default: True description: - Whether to run the clean procedure in case of C(state=finalize). keep_releases: - required: false + required: False default: 5 description: - the number of old releases to keep when cleaning. Used in C(finalize) and C(clean). Any unfinished builds @@ -352,7 +372,7 @@ def main(): module = AnsibleModule( argument_spec = dict( path = dict(aliases=['dest'], required=True, type='str'), - release = dict(required=False, type='str', default=''), + release = dict(required=False, type='str', default=None), releases_path = dict(required=False, type='str', default='releases'), shared_path = dict(required=False, type='str', default='shared'), current_path = dict(required=False, type='str', default='current'), @@ -418,4 +438,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() From d1dcf8e06b2484ce43682a831f44e363fa8c03f2 Mon Sep 17 00:00:00 2001 From: Nicolas Landais Date: Thu, 8 Oct 2015 12:00:12 -0400 Subject: [PATCH 0825/2522] Adding fix to get around bug found with the New-Webstire command when running playbook on a vanilla machine. --- windows/win_iis_website.ps1 | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/windows/win_iis_website.ps1 b/windows/win_iis_website.ps1 index 26a8df12730..4a9ccbba21f 100644 --- a/windows/win_iis_website.ps1 +++ b/windows/win_iis_website.ps1 @@ -102,6 +102,12 @@ Try { If ($bind_hostname) { $site_parameters.HostHeader = $bind_hostname } + + # Fix for error "New-Item : Index was outside the bounds of the array." + # This is a bug in the New-WebSite commandlet. Apparently there must be at least one site configured in IIS otherwise New-WebSite crashes. + # For more details, see http://stackoverflow.com/questions/3573889/ps-c-new-website-blah-throws-index-was-outside-the-bounds-of-the-array + $sites_list = get-childitem -Path IIS:\sites + if ($sites_list -eq $null) { $site_parameters.ID = 1 } $site = New-Website @site_parameters -Force $result.changed = $true From 3b5c7f293635c6b436586aa9670059092118a852 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Sun, 4 Oct 2015 21:03:11 -0500 Subject: [PATCH 0826/2522] homebrew: Add explicit documentation for 'path' argument (with expected default) In Homebew, a formula is installed in a location relative to the actual `brew` command. The documentation clarifies that. Additionally, removed redundant 'path' reconstruction in multiple places. --- packaging/os/homebrew.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packaging/os/homebrew.py b/packaging/os/homebrew.py index d79a118b932..3607080d0e2 100644 --- a/packaging/os/homebrew.py +++ b/packaging/os/homebrew.py @@ -37,6 +37,11 @@ - name of package to install/remove required: false default: None + path: + description: + - ':' separated list of paths to search for 'brew' executable. Since A package (I(formula) in homebrew parlance) location is prefixed relative to the actual path of I(brew) command, providing an alternative I(brew) path enables managing different set of packages in an alternative location in the system. + required: false + default: '/usr/local/bin' state: description: - state of the package @@ -303,7 +308,7 @@ def current_package(self, package): return package # /class properties -------------------------------------------- }}} - def __init__(self, module, path=None, packages=None, state=None, + def __init__(self, module, path, packages=None, state=None, update_homebrew=False, upgrade_all=False, install_options=None): if not install_options: @@ -329,13 +334,8 @@ def _setup_instance_vars(self, **kwargs): setattr(self, key, val) def _prep(self): - self._prep_path() self._prep_brew_path() - def _prep_path(self): - if not self.path: - self.path = ['/usr/local/bin'] - def _prep_brew_path(self): if not self.module: self.brew_path = None @@ -770,7 +770,10 @@ def main(): required=False, type='list', ), - path=dict(required=False), + path=dict( + default="/usr/local/bin", + required=False, + ), state=dict( default="present", choices=[ @@ -808,8 +811,6 @@ def main(): path = p['path'] if path: path = path.split(':') - else: - path = ['/usr/local/bin'] state = p['state'] if state in ('present', 'installed'): From c6aeaf00b1bf5a42b88415ae17a88c6c6095bb21 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Sun, 4 Oct 2015 23:04:46 -0500 Subject: [PATCH 0827/2522] homebrew: Aditional examples for documentation --- packaging/os/homebrew.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packaging/os/homebrew.py b/packaging/os/homebrew.py index 3607080d0e2..5225e8091c5 100644 --- a/packaging/os/homebrew.py +++ b/packaging/os/homebrew.py @@ -69,10 +69,22 @@ notes: [] ''' EXAMPLES = ''' +# Install formula foo with 'brew' in default path (C(/usr/local/bin)) - homebrew: name=foo state=present + +# Install formula foo with 'brew' in alternate path C(/my/other/location/bin) +- homebrew: name=foo path=/my/other/location/bin state=present + +# Update homebrew first and install formula foo with 'brew' in default path - homebrew: name=foo state=present update_homebrew=yes + +# Update homebrew first and upgrade formula foo to latest available with 'brew' in default path - homebrew: name=foo state=latest update_homebrew=yes + +# Update homebrew and upgrade all packages - homebrew: update_homebrew=yes upgrade_all=yes + +# Miscellaneous other examples - homebrew: name=foo state=head - homebrew: name=foo state=linked - homebrew: name=foo state=absent From b9d842ecd62ba72f3fa7e89f06576e8a108a4f26 Mon Sep 17 00:00:00 2001 From: Trond Hindenes Date: Fri, 9 Oct 2015 20:28:54 +0200 Subject: [PATCH 0828/2522] turned out strict msi code checking --- windows/win_package.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/windows/win_package.ps1 b/windows/win_package.ps1 index 6cdc6bf6e5c..993659f07b0 100644 --- a/windows/win_package.ps1 +++ b/windows/win_package.ps1 @@ -752,7 +752,7 @@ function Set-TargetResource if($Ensure -eq "Present") { # check if Msi package contains the ProductName and Code specified - + <# $pName,$pCode = Get-MsiProductEntry -Path $Path if ( @@ -762,6 +762,7 @@ function Set-TargetResource { Throw-InvalidNameOrIdException ($LocalizedData.InvalidNameOrId -f $Name,$identifyingNumber,$pName,$pCode) } + #> $startInfo.Arguments = '/i "{0}"' -f $Path } From f8e1600444b8952ac7a78e79524c027e952341c3 Mon Sep 17 00:00:00 2001 From: Corwin Brown Date: Fri, 9 Oct 2015 17:49:34 -0500 Subject: [PATCH 0829/2522] Made Powershell Strict Complient --- windows/win_unzip.ps1 | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/windows/win_unzip.ps1 b/windows/win_unzip.ps1 index a62f246f5c8..1214bbaa89e 100644 --- a/windows/win_unzip.ps1 +++ b/windows/win_unzip.ps1 @@ -26,14 +26,14 @@ $result = New-Object psobject @{ changed = $false } -If ($params.creates) { +If (Get-Member -InputObject $params -Name creates) { If (Test-Path $params.creates) { Exit-Json $result "The 'creates' file or directory already exists." } } -If ($params.src) { +If (Get-Member -InputObject $params -Name src) { $src = $params.src.toString() If (-Not (Test-Path -path $src)){ @@ -62,24 +62,26 @@ Else { Fail-Json $result "missing required argument: dest" } -If ($params.recurse) { +If (Get-Member -InputObject $params -Name recurse) { $recurse = ConvertTo-Bool ($params.recurse) } Else { $recurse = $false } -If ($params.rm) { - $rm = ConvertTo-Bool ($params.rm) -} -Else { - $rm = $false +If (Get-Member -InputObject $params -Name rm) { + $rm = ConvertTo-Bool ($params.rm) +} +Else { + $rm = $false } If ($ext -eq ".zip" -And $recurse -eq $false) { Try { $shell = New-Object -ComObject Shell.Application - $shell.NameSpace($dest).copyhere(($shell.NameSpace($src)).items(), 20) + $zipPkg = $shell.NameSpace($src) + $destPath = $shell.NameSpace($dest) + $destPath.CopyHere($zipPkg.Items()) $result.changed = $true } Catch { @@ -154,4 +156,4 @@ Set-Attr $result.win_unzip "src" $src.toString() Set-Attr $result.win_unzip "dest" $dest.toString() Set-Attr $result.win_unzip "recurse" $recurse.toString() -Exit-Json $result; \ No newline at end of file +Exit-Json $result; From 4e6b683aa0da8c6a773dc039f1b651338d4059f9 Mon Sep 17 00:00:00 2001 From: Will Keeling Date: Sat, 10 Oct 2015 21:06:10 +0100 Subject: [PATCH 0830/2522] Fixes #634 - multiple param handling by modprobe.py --- system/modprobe.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/system/modprobe.py b/system/modprobe.py index 64e36c784a7..405d5ea22c3 100644 --- a/system/modprobe.py +++ b/system/modprobe.py @@ -57,6 +57,9 @@ - modprobe: name=dummy state=present params="numdummies=2" ''' +import shlex + + def main(): module = AnsibleModule( argument_spec={ @@ -100,7 +103,9 @@ def main(): # Add/remove module as needed if args['state'] == 'present': if not present: - rc, _, err = module.run_command([module.get_bin_path('modprobe', True), args['name'], args['params']]) + command = [module.get_bin_path('modprobe', True), args['name']] + command.extend(shlex.split(args['params'])) + rc, _, err = module.run_command(command) if rc != 0: module.fail_json(msg=err, **args) args['changed'] = True From 8a07d070e1aa7a0f50e7726776720b417ce6b9fd Mon Sep 17 00:00:00 2001 From: ag-wood Date: Sun, 11 Oct 2015 23:17:23 +1000 Subject: [PATCH 0831/2522] Fix for ansible-modules-extras issue #1080 --- system/firewalld.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/system/firewalld.py b/system/firewalld.py index 47d98544000..33b70b42186 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -95,6 +95,7 @@ import firewall.config FW_VERSION = firewall.config.VERSION + from firewall.client import Rich_Rule from firewall.client import FirewallClient fw = FirewallClient() HAS_FIREWALLD = True @@ -199,6 +200,9 @@ def set_service_disabled_permanent(zone, service): # rich rule handling # def get_rich_rule_enabled(zone, rule): + # Convert the rule string to standard format + # before checking whether it is present + rule = str(Rich_Rule(rule_str=rule)) if rule in fw.getRichRules(zone): return True else: @@ -213,6 +217,9 @@ def set_rich_rule_disabled(zone, rule): def get_rich_rule_enabled_permanent(zone, rule): fw_zone = fw.config().getZoneByName(zone) fw_settings = fw_zone.getSettings() + # Convert the rule string to standard format + # before checking whether it is present + rule = str(Rich_Rule(rule_str=rule)) if rule in fw_settings.getRichRules(): return True else: From dbb0bcd9e436b5228b7d45665a16f498bf6ca53a Mon Sep 17 00:00:00 2001 From: daniel-sc Date: Sun, 11 Oct 2015 17:18:38 +0200 Subject: [PATCH 0832/2522] configurable timeout for creating gce image --- cloud/google/gce_img.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/cloud/google/gce_img.py b/cloud/google/gce_img.py index a4a55c16dec..8e03ee75a90 100644 --- a/cloud/google/gce_img.py +++ b/cloud/google/gce_img.py @@ -59,6 +59,12 @@ required: false default: "us-central1-a" aliases: [] + timeout: + description: + - timeout for the operation + required: false + default: 180 + aliases: [] service_account_email: description: - service account email @@ -130,6 +136,7 @@ def create_image(gce, name, module): source = module.params.get('source') zone = module.params.get('zone') desc = module.params.get('description') + timeout = module.params.get('timeout') if not source: module.fail_json(msg='Must supply a source', changed=False) @@ -149,13 +156,17 @@ def create_image(gce, name, module): except GoogleBaseError, e: module.fail_json(msg=str(e), changed=False) + old_timeout = gce.connection.timeout try: + gce.connection.timeout = timeout gce.ex_create_image(name, volume, desc, False) return True except ResourceExistsError: return False except GoogleBaseError, e: module.fail_json(msg=str(e), changed=False) + finally: + gce.connection.timeout = old_timeout def delete_image(gce, name, module): @@ -180,6 +191,7 @@ def main(): service_account_email=dict(), pem_file=dict(), project_id=dict(), + timeout=dict(type='int', default=180) ) ) From cf4b4be9015a73a5d136aa8f3ecc0969f342a002 Mon Sep 17 00:00:00 2001 From: Jeremy Wells Date: Sun, 16 Aug 2015 17:55:10 +0200 Subject: [PATCH 0833/2522] Add consul http checks to consul.py Consul module already supports ttl and script checks. This commit adds http checks. --- clustering/consul.py | 75 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 59 insertions(+), 16 deletions(-) diff --git a/clustering/consul.py b/clustering/consul.py index f72fc6ddcac..c045db08d5e 100644 --- a/clustering/consul.py +++ b/clustering/consul.py @@ -123,6 +123,20 @@ is supplied, m will be used by default e.g. 1 will be 1m required: false default: None + http: + description: + - checks can be registered with an http endpoint. This means that consul + will check that the http endpoint returns a successful http status. + Interval must also be provided with this option. + required: false + default: None + timeout: + description: + - A custom HTTP check timeout. The consul default is 10 seconds. + Similar to the interval this is a number with a s or m suffix to + signify the units of seconds or minutes, e.g. 15s or 1m. + required: false + default: None token: description: - the token key indentifying an ACL rule set. May be required to register services. @@ -143,6 +157,13 @@ script: "curl http://localhost" interval: 60s + - name: register nginx with an http check + consul: + name: nginx + service_port: 80 + interval: 60s + http: /status + - name: register nginx with some service tags consul: name: nginx @@ -235,7 +256,9 @@ def add_check(module, check): check_name=check.name, script=check.script, interval=check.interval, - ttl=check.ttl) + ttl=check.ttl, + http=check.http, + timeout=check.timeout) def remove_check(module, check_id): @@ -302,12 +325,12 @@ def get_service_by_id(consul_api, service_id): def parse_check(module): - if module.params.get('script') and module.params.get('ttl'): + if len(filter(None, [module.params.get('script'), module.params.get('ttl'), module.params.get('http')])) > 1: module.fail_json( - msg='check are either script or ttl driven, supplying both does'\ - ' not make sense') + msg='check are either script, http or ttl driven, supplying more'\ + ' than one does not make sense') - if module.params.get('check_id') or module.params.get('script') or module.params.get('ttl'): + if module.params.get('check_id') or module.params.get('script') or module.params.get('ttl') or module.params.get('http'): return ConsulCheck( module.params.get('check_id'), @@ -317,7 +340,9 @@ def parse_check(module): module.params.get('script'), module.params.get('interval'), module.params.get('ttl'), - module.params.get('notes') + module.params.get('notes'), + module.params.get('http'), + module.params.get('timeout') ) @@ -357,14 +382,13 @@ def __init__(self, service_id=None, name=None, port=-1, def register(self, consul_api): if len(self.checks) > 0: check = self.checks[0] + consul_api.agent.service.register( self.name, service_id=self.id, port=self.port, tags=self.tags, - script=check.script, - interval=check.interval, - ttl=check.ttl) + check=check.check) else: consul_api.agent.service.register( self.name, @@ -405,17 +429,33 @@ def to_dict(self): class ConsulCheck(): def __init__(self, check_id, name, node=None, host='localhost', - script=None, interval=None, ttl=None, notes=None): + script=None, interval=None, ttl=None, notes=None, http=None, timeout=None): self.check_id = self.name = name if check_id: self.check_id = check_id - self.script = script - self.interval = self.validate_duration('interval', interval) - self.ttl = self.validate_duration('ttl', ttl) self.notes = notes self.node = node self.host = host + self.interval = self.validate_duration('interval', interval) + self.ttl = self.validate_duration('ttl', ttl) + self.script = script + self.http = http + self.timeout = self.validate_duration('timeout', timeout) + + self.check = None + + if script: + self.check = consul.Check.script(script, self.interval) + + if ttl: + self.check = consul.Check.ttl(self.ttl) + + if http: + if interval == None: + raise Exception('http check must specify interval') + + self.check = consul.Check.http(http, self.interval, self.timeout) def validate_duration(self, name, duration): @@ -428,9 +468,8 @@ def validate_duration(self, name, duration): def register(self, consul_api): consul_api.agent.check.register(self.name, check_id=self.check_id, - script=self.script, - interval=self.interval, - ttl=self.ttl, notes=self.notes) + notes=self.notes, + check=self.check) def __eq__(self, other): return (isinstance(other, self.__class__) @@ -452,6 +491,8 @@ def to_dict(self): self._add(data, 'host') self._add(data, 'interval') self._add(data, 'ttl') + self._add(data, 'http') + self._add(data, 'timeout') return data def _add(self, data, key, attr=None): @@ -484,6 +525,8 @@ def main(): state=dict(default='present', choices=['present', 'absent']), interval=dict(required=False, type='str'), ttl=dict(required=False, type='str'), + http=dict(required=False, type='str'), + timeout=dict(required=False, type='str'), tags=dict(required=False, type='list'), token=dict(required=False) ), From 51bb65c9a344b1d9ab2067f0bb5dcb552763879e Mon Sep 17 00:00:00 2001 From: Charles Paul Date: Mon, 12 Oct 2015 08:35:21 -0500 Subject: [PATCH 0834/2522] reference instance_id in docs, fixes for calls to fail_json --- cloud/vmware/vca_fw.py | 6 +++--- cloud/vmware/vca_nat.py | 6 +++--- cloud/vmware/vca_vapp.py | 9 ++------- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/cloud/vmware/vca_fw.py b/cloud/vmware/vca_fw.py index 23356ccebbb..5649d7e5c7b 100644 --- a/cloud/vmware/vca_fw.py +++ b/cloud/vmware/vca_fw.py @@ -42,9 +42,9 @@ - The org to login to for creating vapp, mostly set when the service_type is vdc. required: false default: None - service_id: + instance_id: description: - - The service id in a vchs environment to be used for creating the vapp + - The instance id in a vchs environment to be used for creating the vapp required: false default: None host: @@ -242,7 +242,7 @@ def main(): try: desired_rules = validate_fw_rules(fw_rules) except VcaError, e: - module.fail_json(e.message) + module.fail_json(msg=e.message) result = dict(changed=False) result['current_rules'] = current_rules diff --git a/cloud/vmware/vca_nat.py b/cloud/vmware/vca_nat.py index a1af6883bcc..88fc24a20fc 100644 --- a/cloud/vmware/vca_nat.py +++ b/cloud/vmware/vca_nat.py @@ -42,9 +42,9 @@ - The org to login to for creating vapp, mostly set when the service_type is vdc. required: false default: None - service_id: + instance_id: description: - - The service id in a vchs environment to be used for creating the vapp + - The instance id in a vchs environment to be used for creating the vapp required: false default: None host: @@ -200,7 +200,7 @@ def main(): purge_rules = module.params['purge_rules'] if not purge_rules and not nat_rules: - module.fail_json('Must define purge_rules or nat_rules') + module.fail_json(msg='Must define purge_rules or nat_rules') vca = vca_login(module) diff --git a/cloud/vmware/vca_vapp.py b/cloud/vmware/vca_vapp.py index 15bf4d31472..87810c5fae0 100644 --- a/cloud/vmware/vca_vapp.py +++ b/cloud/vmware/vca_vapp.py @@ -104,15 +104,10 @@ - The org to login to for creating vapp, mostly set when the service_type is vdc. required: false default: None - service_id: - description: - - The service id in a vchs environment to be used for creating the vapp - required: false - default: None instance_id: description: - - The vCloud Air instance ID - required: no + - The instance id in a vchs environment to be used for creating the vapp + required: false default: None host: description: From dbdd2475e6f26531b2be55aa7ab5715fb9320289 Mon Sep 17 00:00:00 2001 From: Shawn Siefkas Date: Thu, 8 Oct 2015 20:53:26 -0500 Subject: [PATCH 0835/2522] Handling dry run exception --- cloud/amazon/ec2_vpc_route_table.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index 9de7a85a14e..12b5b22fa59 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -496,13 +496,13 @@ def ensure_route_table_present(connection, module): # If no route table returned then create new route table if route_table is None: - if module.check_mode: - module.exit_json(changed=True) - try: - route_table = connection.create_route_table(vpc_id) + route_table = connection.create_route_table(vpc_id, module.check_mode) changed = True - except EC2ResponseError, e: + except EC2ResponseError as e: + if e.error_code == 'DryRunOperation': + module.exit_json(changed=True) + module.fail_json(msg=e.message) if routes is not None: From 15480e3d04f586645516b42863e2c05f55239488 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 12 Oct 2015 18:20:01 -0400 Subject: [PATCH 0836/2522] fixed option in examples --- clustering/consul.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/clustering/consul.py b/clustering/consul.py index f72fc6ddcac..af8aacf23f8 100644 --- a/clustering/consul.py +++ b/clustering/consul.py @@ -133,19 +133,19 @@ EXAMPLES = ''' - name: register nginx service with the local consul agent consul: - name: nginx + service_name: nginx service_port: 80 - name: register nginx service with curl check consul: - name: nginx + service_name: nginx service_port: 80 script: "curl http://localhost" interval: 60s - name: register nginx with some service tags consul: - name: nginx + service_name: nginx service_port: 80 tags: - prod @@ -153,7 +153,7 @@ - name: remove nginx service consul: - name: nginx + service_name: nginx state: absent - name: create a node level check to test disk usage From 592e30085144241b34781b3acedc861069f7764d Mon Sep 17 00:00:00 2001 From: YAEGASHI Takeshi Date: Thu, 13 Aug 2015 20:09:55 +0900 Subject: [PATCH 0837/2522] New module: blockinfile --- files/blockinfile.py | 292 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 files/blockinfile.py diff --git a/files/blockinfile.py b/files/blockinfile.py new file mode 100644 index 00000000000..a8499547639 --- /dev/null +++ b/files/blockinfile.py @@ -0,0 +1,292 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2014, 2015 YAEGASHI Takeshi +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import re +import os +import tempfile + +DOCUMENTATION = """ +--- +module: blockinfile +author: + - 'YAEGASHI Takeshi (@yaegashi)' +extends_documentation_fragment: + - files + - validate +short_description: Insert/update/remove a text block + surrounded by marker lines. +version_added: '2.0' +description: + - This module will insert/update/remove a block of multi-line text + surrounded by customizable marker lines. +notes: + - This module supports check mode. +options: + dest: + aliases: [ name, destfile ] + required: true + description: + - The file to modify. + state: + required: false + choices: [ present, absent ] + default: present + description: + - Whether the block should be there or not. + marker: + required: false + default: '# {mark} ANSIBLE MANAGED BLOCK' + description: + - The marker line template. + "{mark}" will be replaced with "BEGIN" or "END". + block: + aliases: [ content ] + required: false + default: '' + description: + - The text to insert inside the marker lines. + If it's missing or an empty string, + the block will be removed as if C(state) were specified to C(absent). + insertafter: + required: false + default: EOF + description: + - If specified, the block will be inserted after the last match of + specified regular expression. A special value is available; C(EOF) for + inserting the block at the end of the file. If specified regular + expresion has no matches, C(EOF) will be used instead. + choices: [ 'EOF', '*regex*' ] + insertbefore: + required: false + default: None + description: + - If specified, the block will be inserted before the last match of + specified regular expression. A special value is available; C(BOF) for + inserting the block at the beginning of the file. If specified regular + expresion has no matches, the block will be inserted at the end of the + file. + choices: [ 'BOF', '*regex*' ] + create: + required: false + default: 'no' + choices: [ 'yes', 'no' ] + description: + - Create a new file if it doesn't exist. + backup: + required: false + default: 'no' + choices: [ 'yes', 'no' ] + description: + - Create a backup file including the timestamp information so you can + get the original file back if you somehow clobbered it incorrectly. +""" + +EXAMPLES = r""" +- name: insert/update "Match User" configuation block in /etc/ssh/sshd_config + blockinfile: + dest: /etc/ssh/sshd_config + block: | + Match User ansible-agent + PasswordAuthentication no + +- name: insert/update eth0 configuration stanza in /etc/network/interfaces + (it might be better to copy files into /etc/network/interfaces.d/) + blockinfile: + dest: /etc/network/interfaces + block: | + iface eth0 inet static + address 192.168.0.1 + netmask 255.255.255.0 + +- name: insert/update HTML surrounded by custom markers after line + blockinfile: + dest: /var/www/html/index.html + marker: "" + insertafter: "" + content: | +

Welcome to {{ansible_hostname}}

+

Last updated on {{ansible_date_time.iso8601}}

+ +- name: remove HTML as well as surrounding markers + blockinfile: + dest: /var/www/html/index.html + marker: "" + content: "" +""" + + +def write_changes(module, contents, dest): + + tmpfd, tmpfile = tempfile.mkstemp() + f = os.fdopen(tmpfd, 'wb') + f.write(contents) + f.close() + + validate = module.params.get('validate', None) + valid = not validate + if validate: + if "%s" not in validate: + module.fail_json(msg="validate must contain %%s: %s" % (validate)) + (rc, out, err) = module.run_command(validate % tmpfile) + valid = rc == 0 + if rc != 0: + module.fail_json(msg='failed to validate: ' + 'rc:%s error:%s' % (rc, err)) + if valid: + module.atomic_move(tmpfile, dest) + + +def check_file_attrs(module, changed, message): + + file_args = module.load_file_common_arguments(module.params) + if module.set_file_attributes_if_different(file_args, False): + + if changed: + message += " and " + changed = True + message += "ownership, perms or SE linux context changed" + + return message, changed + + +def main(): + module = AnsibleModule( + argument_spec=dict( + dest=dict(required=True, aliases=['name', 'destfile']), + state=dict(default='present', choices=['absent', 'present']), + marker=dict(default='# {mark} ANSIBLE MANAGED BLOCK', type='str'), + block=dict(default='', type='str', aliases=['content']), + insertafter=dict(default=None), + insertbefore=dict(default=None), + create=dict(default=False, type='bool'), + backup=dict(default=False, type='bool'), + validate=dict(default=None, type='str'), + ), + mutually_exclusive=[['insertbefore', 'insertafter']], + add_file_common_args=True, + supports_check_mode=True + ) + + params = module.params + dest = os.path.expanduser(params['dest']) + if module.boolean(params.get('follow', None)): + dest = os.path.realpath(dest) + + if os.path.isdir(dest): + module.fail_json(rc=256, + msg='Destination %s is a directory !' % dest) + + if not os.path.exists(dest): + if not module.boolean(params['create']): + module.fail_json(rc=257, + msg='Destination %s does not exist !' % dest) + original = None + lines = [] + else: + f = open(dest, 'rb') + original = f.read() + f.close() + lines = original.splitlines() + + insertbefore = params['insertbefore'] + insertafter = params['insertafter'] + block = params['block'] + marker = params['marker'] + present = params['state'] == 'present' + + if insertbefore is None and insertafter is None: + insertafter = 'EOF' + + if insertafter not in (None, 'EOF'): + insertre = re.compile(insertafter) + elif insertbefore not in (None, 'BOF'): + insertre = re.compile(insertbefore) + else: + insertre = None + + marker0 = re.sub(r'{mark}', 'BEGIN', marker) + marker1 = re.sub(r'{mark}', 'END', marker) + if present and block: + # Escape seqeuences like '\n' need to be handled in Ansible 1.x + if ANSIBLE_VERSION.startswith('1.'): + block = re.sub('', block, '') + blocklines = [marker0] + block.splitlines() + [marker1] + else: + blocklines = [] + + n0 = n1 = None + for i, line in enumerate(lines): + if line.startswith(marker0): + n0 = i + if line.startswith(marker1): + n1 = i + + if None in (n0, n1): + n0 = None + if insertre is not None: + for i, line in enumerate(lines): + if insertre.search(line): + n0 = i + if n0 is None: + n0 = len(lines) + elif insertafter is not None: + n0 += 1 + elif insertbefore is not None: + n0 = 0 # insertbefore=BOF + else: + n0 = len(lines) # insertafter=EOF + elif n0 < n1: + lines[n0:n1+1] = [] + else: + lines[n1:n0+1] = [] + n0 = n1 + + lines[n0:n0] = blocklines + + if lines: + result = '\n'.join(lines)+'\n' + else: + result = '' + if original == result: + msg = '' + changed = False + elif original is None: + msg = 'File created' + changed = True + elif not blocklines: + msg = 'Block removed' + changed = True + else: + msg = 'Block inserted' + changed = True + + if changed and not module.check_mode: + if module.boolean(params['backup']) and os.path.exists(dest): + module.backup_local(dest) + write_changes(module, result, dest) + + msg, changed = check_file_attrs(module, changed, msg) + module.exit_json(changed=changed, msg=msg) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.splitter import * +if __name__ == '__main__': + main() From 5f3f15fbc0987bb737b755cbbe79f91f65386c8f Mon Sep 17 00:00:00 2001 From: Rob Date: Tue, 13 Oct 2015 13:26:19 +1100 Subject: [PATCH 0838/2522] Use doc fragment and fix examples --- cloud/amazon/ec2_vpc_net_facts.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/cloud/amazon/ec2_vpc_net_facts.py b/cloud/amazon/ec2_vpc_net_facts.py index 538d39f3b41..98c7f742a73 100644 --- a/cloud/amazon/ec2_vpc_net_facts.py +++ b/cloud/amazon/ec2_vpc_net_facts.py @@ -27,14 +27,10 @@ - A dict of filters to apply. Each dict item consists of a filter key and a filter value. See U(http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeVpcs.html) for possible filters. required: false default: null - region: - description: - - The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used. See U(http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region) - required: false - default: null - aliases: [ 'aws_region', 'ec2_region' ] -extends_documentation_fragment: aws +extends_documentation_fragment: + - aws + - ec2 ''' EXAMPLES = ''' @@ -46,12 +42,12 @@ # Gather facts about a particular VPC using VPC ID - ec2_vpc_net_facts: filters: - - vpc-id: vpc-00112233 + vpc-id: vpc-00112233 # Gather facts about any VPC with a tag key Name and value Example - ec2_vpc_net_facts: filters: - - "tag:Name": Example + "tag:Name": Example ''' From 62cd2d9c18a008bb7540e32bda4417dd9af06530 Mon Sep 17 00:00:00 2001 From: whiter Date: Tue, 13 Oct 2015 17:54:16 +1100 Subject: [PATCH 0839/2522] Use filters attribute rather than eni id so facts can be filtered on much more. Matches the new ec2_vpc_route_table_facts and ec2_vpc_subnet_facts modules --- cloud/amazon/ec2_eni_facts.py | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/cloud/amazon/ec2_eni_facts.py b/cloud/amazon/ec2_eni_facts.py index 2014c3e8eb5..c25535f51eb 100644 --- a/cloud/amazon/ec2_eni_facts.py +++ b/cloud/amazon/ec2_eni_facts.py @@ -22,11 +22,12 @@ version_added: "2.0" author: "Rob White (@wimnat)" options: - eni_id: + filters: description: - - The ID of the ENI. Pass this option to gather facts about a particular ENI, otherwise, all ENIs are returned. + - A dict of filters to apply. Each dict item consists of a filter key and a filter value. See U(http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeNetworkInterfaces.html) for possible filters. required: false default: null + extends_documentation_fragment: - aws - ec2 @@ -40,12 +41,11 @@ # Gather facts about a particular ENI - ec2_eni_facts: - eni_id: eni-xxxxxxx + filters: + network-interface-id: eni-xxxxxxx ''' -import xml.etree.ElementTree as ET - try: import boto.ec2 from boto.exception import BotoServerError @@ -53,14 +53,6 @@ except ImportError: HAS_BOTO = False - -def get_error_message(xml_string): - - root = ET.fromstring(xml_string) - for message in root.findall('.//Message'): - return message.text - - def get_eni_info(interface): interface_info = {'id': interface.id, @@ -89,13 +81,13 @@ def get_eni_info(interface): def list_eni(connection, module): - eni_id = module.params.get("eni_id") + filters = module.params.get("filters") interface_dict_array = [] try: - all_eni = connection.get_all_network_interfaces(eni_id) + all_eni = connection.get_all_network_interfaces(filters=filters) except BotoServerError as e: - module.fail_json(msg=get_error_message(e.args[2])) + module.fail_json(msg=e.message) for interface in all_eni: interface_dict_array.append(get_eni_info(interface)) @@ -107,7 +99,7 @@ def main(): argument_spec = ec2_argument_spec() argument_spec.update( dict( - eni_id = dict(default=None) + filters = dict(default=None, type='dict') ) ) @@ -131,4 +123,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * -main() +if __name__ == '__main__': + main() From c35c37258f0b46ef8cbecd8c38ffc733160fa019 Mon Sep 17 00:00:00 2001 From: Corwin Brown Date: Tue, 13 Oct 2015 09:47:56 -0500 Subject: [PATCH 0840/2522] Adding win_uri module --- windows/win_uri.ps1 | 76 ++++++++++++++++++++++++++++++++++++ windows/win_uri.py | 93 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 169 insertions(+) create mode 100644 windows/win_uri.ps1 create mode 100644 windows/win_uri.py diff --git a/windows/win_uri.ps1 b/windows/win_uri.ps1 new file mode 100644 index 00000000000..9c2ddededae --- /dev/null +++ b/windows/win_uri.ps1 @@ -0,0 +1,76 @@ +#!powershell +# This file is part of Ansible +# +# Copyright 2015, Corwin Brown +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# WANT_JSON +# POWERSHELL_COMMON + +$params = Parse-Args $args; + +$result = New-Object psobject @{ + win_uri = New-Object psobject +} + +# Build Arguments +$webrequest_opts = @{} +if (Get-Member -InputObject $params -Name url) { + $url = $params.url.ToString() + $webrequest_opts.Uri = $url +} else { + Fail-Json $result "Missing required argument: url" +} + +if (Get-Member -InputObject $params -Name method) { + $method = $params.method.ToString() + $webrequest_opts.Method = $method +} + +if (Get-Member -InputObject $params -Name content_type) { + $content_type = $params.method.content_type.ToString() + $webrequest_opts.ContentType = $content_type +} + +if (Get-Member -InputObject $params -Name body) { + $body = $params.method.body.ToString() + $webrequest_opts.Body = $body +} + +if (Get-Member -InputObject $params -Name headers) { + $headers = $params.headers + Set-Attr $result.win_uri "headers" $headers + + $req_headers = @{} + ForEach ($header in $headers.psobject.properties) { + $req_headers.Add($header.Name, $header.Value) + } + + $webrequest_opts.Headers = $req_headers +} + +try { + $response = Invoke-WebRequest @webrequest_opts +} catch { + $ErrorMessage = $_.Exception.Message + Fail-Json $result $ErrorMessage +} + +ForEach ($prop in $response.psobject.properties) { + Set-Attr $result $prop.Name $prop.Value +} + +Exit-Json $result + diff --git a/windows/win_uri.py b/windows/win_uri.py new file mode 100644 index 00000000000..451c965836d --- /dev/null +++ b/windows/win_uri.py @@ -0,0 +1,93 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Corwin Brown +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# this is a windows documentation stub. actual code lives in the .ps1 +# file of the same name + +DOCUMENTATION = """ +--- +module: win_uri +version_added: "" +short_description: Interacts with webservices. +description: + - Interacts with HTTP and HTTPS services. +options: + url: + description: + - HTTP or HTTPS URL in the form of (http|https)://host.domain:port/path + method: + description: + - The HTTP Method of the request or response. + default: GET + choices: + - GET + - POST + - PUT + - HEAD + - DELETE + - OPTIONS + - PATCH + - TRACE + - CONNECT + - REFRESH + content_type: + description: + - Sets the "Content-Type" header. + body: + description: + - The body of the HTTP request/response to the web service. + headers: + description: + - Key Value pairs for headers. Example "Host: www.somesite.com" +author: Corwin Brown +""" + +Examples= """ +# Send a GET request and store the output: +--- +- name: Perform a GET and Store Output + win_uri: + url: http://www.somesite.com/myendpoint + register: http_output + +# Set a HOST header to hit an internal webserver: +--- +- name: Hit a Specific Host on the Server + win_uri: + url: http://my.internal.server.com + method: GET + headers: + host: "www.somesite.com + +# Do a HEAD request on an endpoint +--- +- name: Perform a HEAD on an Endpoint + win_uri: + url: http://www.somesite.com + method: HEAD + +# Post a body to an endpoint +--- +- name: POST a Body to an Endpoint + win_uri: + url: http://www.somesite.com + method: POST + body: "{ 'some': 'json' }" +""" From 2548112b46990249072954ee429ba15352763d64 Mon Sep 17 00:00:00 2001 From: ogenstad Date: Tue, 13 Oct 2015 20:53:23 +0200 Subject: [PATCH 0841/2522] lookupMib=False for pysnmp 4.3.0 --- network/snmp_facts.py | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/network/snmp_facts.py b/network/snmp_facts.py index 81a91ee6eb2..f89b98b1db3 100644 --- a/network/snmp_facts.py +++ b/network/snmp_facts.py @@ -112,7 +112,7 @@ def __init__(self,dotprefix=False): self.sysContact = dp + "1.3.6.1.2.1.1.4.0" self.sysName = dp + "1.3.6.1.2.1.1.5.0" self.sysLocation = dp + "1.3.6.1.2.1.1.6.0" - + # From IF-MIB self.ifIndex = dp + "1.3.6.1.2.1.2.2.1.1" self.ifDescr = dp + "1.3.6.1.2.1.2.2.1.2" @@ -127,10 +127,10 @@ def __init__(self,dotprefix=False): self.ipAdEntAddr = dp + "1.3.6.1.2.1.4.20.1.1" self.ipAdEntIfIndex = dp + "1.3.6.1.2.1.4.20.1.2" self.ipAdEntNetMask = dp + "1.3.6.1.2.1.4.20.1.3" - + def decode_hex(hexstring): - + if len(hexstring) < 3: return hexstring if hexstring[:2] == "0x": @@ -200,7 +200,7 @@ def main(): if m_args['version'] == "v2" or m_args['version'] == "v2c": if m_args['community'] == False: module.fail_json(msg='Community not set when using snmp version 2') - + if m_args['version'] == "v3": if m_args['username'] == None: module.fail_json(msg='Username not set when using snmp version 3') @@ -208,7 +208,7 @@ def main(): if m_args['level'] == "authPriv" and m_args['privacy'] == None: module.fail_json(msg='Privacy algorithm not set when using authPriv') - + if m_args['integrity'] == "sha": integrity_proto = cmdgen.usmHMACSHAAuthProtocol elif m_args['integrity'] == "md5": @@ -218,7 +218,7 @@ def main(): privacy_proto = cmdgen.usmAesCfb128Protocol elif m_args['privacy'] == "des": privacy_proto = cmdgen.usmDESPrivProtocol - + # Use SNMP Version 2 if m_args['version'] == "v2" or m_args['version'] == "v2c": snmp_auth = cmdgen.CommunityData(m_args['community']) @@ -237,18 +237,19 @@ def main(): v = DefineOid(dotprefix=False) Tree = lambda: defaultdict(Tree) - + results = Tree() - + errorIndication, errorStatus, errorIndex, varBinds = cmdGen.getCmd( snmp_auth, cmdgen.UdpTransportTarget((m_args['host'], 161)), cmdgen.MibVariable(p.sysDescr,), - cmdgen.MibVariable(p.sysObjectId,), + cmdgen.MibVariable(p.sysObjectId,), cmdgen.MibVariable(p.sysUpTime,), - cmdgen.MibVariable(p.sysContact,), + cmdgen.MibVariable(p.sysContact,), cmdgen.MibVariable(p.sysName,), cmdgen.MibVariable(p.sysLocation,), + lookupMib=False ) @@ -273,7 +274,7 @@ def main(): errorIndication, errorStatus, errorIndex, varTable = cmdGen.nextCmd( snmp_auth, - cmdgen.UdpTransportTarget((m_args['host'], 161)), + cmdgen.UdpTransportTarget((m_args['host'], 161)), cmdgen.MibVariable(p.ifIndex,), cmdgen.MibVariable(p.ifDescr,), cmdgen.MibVariable(p.ifMtu,), @@ -281,20 +282,21 @@ def main(): cmdgen.MibVariable(p.ifPhysAddress,), cmdgen.MibVariable(p.ifAdminStatus,), cmdgen.MibVariable(p.ifOperStatus,), - cmdgen.MibVariable(p.ipAdEntAddr,), - cmdgen.MibVariable(p.ipAdEntIfIndex,), - cmdgen.MibVariable(p.ipAdEntNetMask,), + cmdgen.MibVariable(p.ipAdEntAddr,), + cmdgen.MibVariable(p.ipAdEntIfIndex,), + cmdgen.MibVariable(p.ipAdEntNetMask,), cmdgen.MibVariable(p.ifAlias,), + lookupMib=False ) - + if errorIndication: module.fail_json(msg=str(errorIndication)) interface_indexes = [] - - all_ipv4_addresses = [] + + all_ipv4_addresses = [] ipv4_networks = Tree() for varBinds in varTable: @@ -358,9 +360,8 @@ def main(): results['ansible_interfaces'][int(interface)]['ipv4'] = interface_to_ipv4[interface] results['ansible_all_ipv4_addresses'] = all_ipv4_addresses - + module.exit_json(ansible_facts=results) - -main() +main() From 0e64863429575781b437e19f4065d729d1684373 Mon Sep 17 00:00:00 2001 From: Brian Geihsler Date: Wed, 14 Oct 2015 00:33:28 -0700 Subject: [PATCH 0842/2522] Fix #1099: Only populate site result when it is there --- windows/win_iis_website.ps1 | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/windows/win_iis_website.ps1 b/windows/win_iis_website.ps1 index 4a9ccbba21f..c434daba929 100644 --- a/windows/win_iis_website.ps1 +++ b/windows/win_iis_website.ps1 @@ -102,8 +102,8 @@ Try { If ($bind_hostname) { $site_parameters.HostHeader = $bind_hostname } - - # Fix for error "New-Item : Index was outside the bounds of the array." + + # Fix for error "New-Item : Index was outside the bounds of the array." # This is a bug in the New-WebSite commandlet. Apparently there must be at least one site configured in IIS otherwise New-WebSite crashes. # For more details, see http://stackoverflow.com/questions/3573889/ps-c-new-website-blah-throws-index-was-outside-the-bounds-of-the-array $sites_list = get-childitem -Path IIS:\sites @@ -171,15 +171,21 @@ Catch Fail-Json (New-Object psobject) $_.Exception.Message } -$site = Get-Website | Where { $_.Name -eq $name } -$result.site = New-Object psobject @{ - Name = $site.Name - ID = $site.ID - State = $site.State - PhysicalPath = $site.PhysicalPath - ApplicationPool = $site.applicationPool - Bindings = @($site.Bindings.Collection | ForEach-Object { $_.BindingInformation }) +if ($state -ne 'absent') +{ + $site = Get-Website | Where { $_.Name -eq $name } } +if ($site) +{ + $result.site = New-Object psobject @{ + Name = $site.Name + ID = $site.ID + State = $site.State + PhysicalPath = $site.PhysicalPath + ApplicationPool = $site.applicationPool + Bindings = @($site.Bindings.Collection | ForEach-Object { $_.BindingInformation }) + } +} Exit-Json $result From 1e37276c97a08a35b58bf882044a703210763516 Mon Sep 17 00:00:00 2001 From: Brian Geihsler Date: Wed, 14 Oct 2015 00:39:59 -0700 Subject: [PATCH 0843/2522] Fix #1101: win_webpicmd strict mode fixes --- windows/win_webpicmd.ps1 | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/windows/win_webpicmd.ps1 b/windows/win_webpicmd.ps1 index 377edcdc3c8..3bef13f6574 100644 --- a/windows/win_webpicmd.ps1 +++ b/windows/win_webpicmd.ps1 @@ -42,9 +42,9 @@ Function Find-Command ) $installed = get-command $command -erroraction Ignore write-verbose "$installed" - if ($installed.length -gt 0) + if ($installed) { - return $installed[0] + return $installed } return $null } @@ -87,8 +87,12 @@ Function Test-IsInstalledFromWebPI } Write-Verbose "$results" - $matches = $results | select-string -pattern "^$package\s+" - return $matches.length -gt 0 + if ($results -match "^$package\s+") + { + return $true + } + + return $false } Function Install-WithWebPICmd @@ -112,8 +116,8 @@ Function Install-WithWebPICmd } write-verbose "$results" - $success = $results | select-string -pattern "Install of Products: SUCCESS" - if ($success.length -gt 0) + + if ($results -match "Install of Products: SUCCESS") { $result.changed = $true } From c5924aff74481a35f5109b8df689a22946ab8d5f Mon Sep 17 00:00:00 2001 From: Steve Date: Wed, 14 Oct 2015 12:13:57 +0100 Subject: [PATCH 0844/2522] Fix bug #1105 - incorrect use of lstrip() Remove only the literal prefix '/dev/mapper/' - not any of the characters in '/dev/mapper/' - from the name param of the crypttab module. --- system/crypttab.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) mode change 100644 => 100755 system/crypttab.py diff --git a/system/crypttab.py b/system/crypttab.py old mode 100644 new mode 100755 index 44d9f859791..2a8cdc9d36c --- a/system/crypttab.py +++ b/system/crypttab.py @@ -29,7 +29,7 @@ name: description: - Name of the encrypted block device as it appears in the C(/etc/crypttab) file, or - optionaly prefixed with C(/dev/mapper), as it appears in the filesystem. I(/dev/mapper) + optionaly prefixed with C(/dev/mapper/), as it appears in the filesystem. I(/dev/mapper/) will be stripped from I(name). required: true default: null @@ -96,12 +96,15 @@ def main(): supports_check_mode = True ) - name = module.params['name'].lstrip('/dev/mapper') backing_device = module.params['backing_device'] password = module.params['password'] opts = module.params['opts'] state = module.params['state'] path = module.params['path'] + name = module.params['name'] + if name.startswith('/dev/mapper/'): + name = name[len('/dev/mapper/'):] + if state != 'absent' and backing_device is None and password is None and opts is None: module.fail_json(msg="expected one or more of 'backing_device', 'password' or 'opts'", From fdb4a58f975fc4df4b22157b97428b0b274a0d3d Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Wed, 14 Oct 2015 11:06:29 -0400 Subject: [PATCH 0845/2522] added version_added and fixed some doc parsing issues --- clustering/consul.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/clustering/consul.py b/clustering/consul.py index 94423e8c858..41c98d00228 100644 --- a/clustering/consul.py +++ b/clustering/consul.py @@ -130,6 +130,7 @@ Interval must also be provided with this option. required: false default: None + version_added: "2.0" timeout: description: - A custom HTTP check timeout. The consul default is 10 seconds. @@ -137,6 +138,7 @@ signify the units of seconds or minutes, e.g. 15s or 1m. required: false default: None + version_added: "2.0" token: description: - the token key indentifying an ACL rule set. May be required to register services. @@ -231,8 +233,7 @@ def remove(module): service_id = module.params.get('service_id') or module.params.get('service_name') check_id = module.params.get('check_id') or module.params.get('check_name') if not (service_id or check_id): - module.fail_json(msg='services and checks are removed by id or name.'\ - ' please supply a service id/name or a check id/name') + module.fail_json(msg='services and checks are removed by id or name. please supply a service id/name or a check id/name') if service_id: remove_service(module, service_id) else: @@ -245,8 +246,7 @@ def add_check(module, check): Without this we can't compare to the supplied check and so we must assume a change. ''' if not check.name: - module.fail_json(msg='a check name is required for a node level check,'\ - ' one not attached to a service') + module.fail_json(msg='a check name is required for a node level check, one not attached to a service') consul_api = get_consul_api(module) check.register(consul_api) @@ -327,8 +327,7 @@ def parse_check(module): if len(filter(None, [module.params.get('script'), module.params.get('ttl'), module.params.get('http')])) > 1: module.fail_json( - msg='check are either script, http or ttl driven, supplying more'\ - ' than one does not make sense') + msg='check are either script, http or ttl driven, supplying more than one does not make sense') if module.params.get('check_id') or module.params.get('script') or module.params.get('ttl') or module.params.get('http'): @@ -357,10 +356,7 @@ def parse_service(module): ) elif module.params.get('service_name') and not module.params.get('service_port'): - module.fail_json( - msg="service_name supplied but no service_port, a port is required"\ - " to configure a service. Did you configure the 'port' "\ - "argument meaning 'service_port'?") + module.fail_json( msg="service_name supplied but no service_port, a port is required to configure a service. Did you configure the 'port' argument meaning 'service_port'?") class ConsulService(): @@ -456,7 +452,7 @@ def __init__(self, check_id, name, node=None, host='localhost', raise Exception('http check must specify interval') self.check = consul.Check.http(http, self.interval, self.timeout) - + def validate_duration(self, name, duration): if duration: @@ -505,8 +501,7 @@ def _add(self, data, key, attr=None): def test_dependencies(module): if not python_consul_installed: - module.fail_json(msg="python-consul required for this module. "\ - "see http://python-consul.readthedocs.org/en/latest/#installation") + module.fail_json(msg="python-consul required for this module. see http://python-consul.readthedocs.org/en/latest/#installation") def main(): module = AnsibleModule( @@ -532,9 +527,9 @@ def main(): ), supports_check_mode=False, ) - + test_dependencies(module) - + try: register_with_consul(module) except ConnectionError, e: From 8ce3104bc5ec025ad347e8861505b5f5d8b937c2 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 14 Oct 2015 08:42:25 -0700 Subject: [PATCH 0846/2522] Convert from dos line endings to unix because dos line endings break our documentation build. --- cloud/amazon/sts_assume_role.py | 312 ++++++++++++++++---------------- 1 file changed, 156 insertions(+), 156 deletions(-) diff --git a/cloud/amazon/sts_assume_role.py b/cloud/amazon/sts_assume_role.py index aa48bf8daa5..b089550adab 100644 --- a/cloud/amazon/sts_assume_role.py +++ b/cloud/amazon/sts_assume_role.py @@ -1,156 +1,156 @@ -#!/usr/bin/python -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -DOCUMENTATION = ''' ---- -module: sts_assume_role -short_description: Assume a role using AWS Security Token Service and obtain temporary credentials -description: - - Assume a role using AWS Security Token Service and obtain temporary credentials -version_added: "2.0" -author: Boris Ekelchik (@bekelchik) -options: - role_arn: - description: - - The Amazon Resource Name (ARN) of the role that the caller is assuming (http://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html#Identifiers_ARNs) - required: true - role_session_name: - description: - - Name of the role's session - will be used by CloudTrail - required: true - policy: - description: - - Supplemental policy to use in addition to assumed role's policies. - required: false - default: null - duration_seconds: - description: - - The duration, in seconds, of the role session. The value can range from 900 seconds (15 minutes) to 3600 seconds (1 hour). By default, the value is set to 3600 seconds. - required: false - default: null - external_id: - description: - - A unique identifier that is used by third parties to assume a role in their customers' accounts. - required: false - default: null - mfa_serial_number: - description: - - he identification number of the MFA device that is associated with the user who is making the AssumeRole call. - required: false - default: null - mfa_token: - description: - - The value provided by the MFA device, if the trust policy of the role being assumed requires MFA. - required: false - default: null -notes: - - In order to use the assumed role in a following playbook task you must pass the access_key, access_secret and access_token -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Assume an existing role (more details: http://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html) -sts_assume_role: - role_arn: "arn:aws:iam::123456789012:role/someRole" - session_name: "someRoleSession" -register: assumed_role - -# Use the assumed role above to tag an instance in account 123456789012 -ec2_tag: - aws_access_key: "{{ assumed_role.sts_creds.access_key }}" - aws_secret_key: "{{ assumed_role.sts_creds.secret_key }}" - security_token: "{{ assumed_role.sts_creds.session_token }}" - resource: i-xyzxyz01 - state: present - tags: - MyNewTag: value - -''' - -import sys -import time - -try: - import boto.sts - from boto.exception import BotoServerError - HAS_BOTO = True -except ImportError: - HAS_BOTO = False - - -def assume_role_policy(connection, module): - - role_arn = module.params.get('role_arn') - role_session_name = module.params.get('role_session_name') - policy = module.params.get('policy') - duration_seconds = module.params.get('duration_seconds') - external_id = module.params.get('external_id') - mfa_serial_number = module.params.get('mfa_serial_number') - mfa_token = module.params.get('mfa_token') - changed = False - - try: - assumed_role = connection.assume_role(role_arn, role_session_name, policy, duration_seconds, external_id, mfa_serial_number, mfa_token) - changed = True - except BotoServerError, e: - module.fail_json(msg=e) - - module.exit_json(changed=changed, sts_creds=assumed_role.credentials.__dict__, sts_user=assumed_role.user.__dict__) - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - role_arn = dict(required=True, default=None), - role_session_name = dict(required=True, default=None), - duration_seconds = dict(required=False, default=None, type='int'), - external_id = dict(required=False, default=None), - policy = dict(required=False, default=None), - mfa_serial_number = dict(required=False, default=None), - mfa_token = dict(required=False, default=None) - ) - ) - - module = AnsibleModule(argument_spec=argument_spec) - - if not HAS_BOTO: - module.fail_json(msg='boto required for this module') - - region, ec2_url, aws_connect_params = get_aws_connection_info(module) - - if region: - try: - connection = connect_to_aws(boto.sts, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, StandardError), e: - module.fail_json(msg=str(e)) - else: - module.fail_json(msg="region must be specified") - - try: - assume_role_policy(connection, module) - except BotoServerError, e: - module.fail_json(msg=e) - - -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * - -main() +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: sts_assume_role +short_description: Assume a role using AWS Security Token Service and obtain temporary credentials +description: + - Assume a role using AWS Security Token Service and obtain temporary credentials +version_added: "2.0" +author: Boris Ekelchik (@bekelchik) +options: + role_arn: + description: + - The Amazon Resource Name (ARN) of the role that the caller is assuming (http://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html#Identifiers_ARNs) + required: true + role_session_name: + description: + - Name of the role's session - will be used by CloudTrail + required: true + policy: + description: + - Supplemental policy to use in addition to assumed role's policies. + required: false + default: null + duration_seconds: + description: + - The duration, in seconds, of the role session. The value can range from 900 seconds (15 minutes) to 3600 seconds (1 hour). By default, the value is set to 3600 seconds. + required: false + default: null + external_id: + description: + - A unique identifier that is used by third parties to assume a role in their customers' accounts. + required: false + default: null + mfa_serial_number: + description: + - he identification number of the MFA device that is associated with the user who is making the AssumeRole call. + required: false + default: null + mfa_token: + description: + - The value provided by the MFA device, if the trust policy of the role being assumed requires MFA. + required: false + default: null +notes: + - In order to use the assumed role in a following playbook task you must pass the access_key, access_secret and access_token +extends_documentation_fragment: + - aws + - ec2 +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Assume an existing role (more details: http://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html) +sts_assume_role: + role_arn: "arn:aws:iam::123456789012:role/someRole" + session_name: "someRoleSession" +register: assumed_role + +# Use the assumed role above to tag an instance in account 123456789012 +ec2_tag: + aws_access_key: "{{ assumed_role.sts_creds.access_key }}" + aws_secret_key: "{{ assumed_role.sts_creds.secret_key }}" + security_token: "{{ assumed_role.sts_creds.session_token }}" + resource: i-xyzxyz01 + state: present + tags: + MyNewTag: value + +''' + +import sys +import time + +try: + import boto.sts + from boto.exception import BotoServerError + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + + +def assume_role_policy(connection, module): + + role_arn = module.params.get('role_arn') + role_session_name = module.params.get('role_session_name') + policy = module.params.get('policy') + duration_seconds = module.params.get('duration_seconds') + external_id = module.params.get('external_id') + mfa_serial_number = module.params.get('mfa_serial_number') + mfa_token = module.params.get('mfa_token') + changed = False + + try: + assumed_role = connection.assume_role(role_arn, role_session_name, policy, duration_seconds, external_id, mfa_serial_number, mfa_token) + changed = True + except BotoServerError, e: + module.fail_json(msg=e) + + module.exit_json(changed=changed, sts_creds=assumed_role.credentials.__dict__, sts_user=assumed_role.user.__dict__) + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + role_arn = dict(required=True, default=None), + role_session_name = dict(required=True, default=None), + duration_seconds = dict(required=False, default=None, type='int'), + external_id = dict(required=False, default=None), + policy = dict(required=False, default=None), + mfa_serial_number = dict(required=False, default=None), + mfa_token = dict(required=False, default=None) + ) + ) + + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO: + module.fail_json(msg='boto required for this module') + + region, ec2_url, aws_connect_params = get_aws_connection_info(module) + + if region: + try: + connection = connect_to_aws(boto.sts, region, **aws_connect_params) + except (boto.exception.NoAuthHandlerFound, StandardError), e: + module.fail_json(msg=str(e)) + else: + module.fail_json(msg="region must be specified") + + try: + assume_role_policy(connection, module) + except BotoServerError, e: + module.fail_json(msg=e) + + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +main() From d0e3a315acf97623f59efbf6102f164ca01502e5 Mon Sep 17 00:00:00 2001 From: Brian Geihsler Date: Wed, 14 Oct 2015 11:04:07 -0700 Subject: [PATCH 0847/2522] Fix #1107: Use Get-Attr in win_regedit --- windows/win_regedit.ps1 | 59 ++++++----------------------------------- 1 file changed, 8 insertions(+), 51 deletions(-) diff --git a/windows/win_regedit.ps1 b/windows/win_regedit.ps1 index 1a257413466..f9491e39c57 100644 --- a/windows/win_regedit.ps1 +++ b/windows/win_regedit.ps1 @@ -25,60 +25,17 @@ $params = Parse-Args $args; $result = New-Object PSObject; Set-Attr $result "changed" $false; -If ($params.key) -{ - $registryKey = $params.key -} -Else -{ - Fail-Json $result "missing required argument: key" -} +$registryKey = Get-Attr -obj $params -name "key" -failifempty $true +$registryValue = Get-Attr -obj $params -name "value" -default $null +$state = Get-Attr -obj $params -name "state" -validateSet "present","absent" -default "present" +$registryData = Get-Attr -obj $params -name "data" -default $null +$registryDataType = Get-Attr -obj $params -name "datatype" -validateSet "binary","dword","expandstring","multistring","string","qword" -default "string" -If ($params.value) -{ - $registryValue = $params.value -} -Else -{ - $registryValue = $null -} - -If ($params.state) -{ - $state = $params.state.ToString().ToLower() - If (($state -ne "present") -and ($state -ne "absent")) - { - Fail-Json $result "state is $state; must be present or absent" - } -} -Else -{ - $state = "present" -} - -If ($params.data) -{ - $registryData = $params.data -} -ElseIf ($state -eq "present" -and $registryValue -ne $null) +If ($state -eq "present" -and $registryData -eq $null -and $registryValue -ne $null) { Fail-Json $result "missing required argument: data" } -If ($params.datatype) -{ - $registryDataType = $params.datatype.ToString().ToLower() - $validRegistryDataTypes = "binary", "dword", "expandstring", "multistring", "string", "qword" - If ($validRegistryDataTypes -notcontains $registryDataType) - { - Fail-Json $result "type is $registryDataType; must be binary, dword, expandstring, multistring, string, or qword" - } -} -Else -{ - $registryDataType = "string" -} - Function Test-RegistryValueData { Param ( [parameter(Mandatory=$true)] @@ -115,7 +72,7 @@ if($state -eq "present") { } } # Changes Only Data - elseif ((Get-ItemProperty -Path $registryKey | Select-Object -ExpandProperty $registryValue) -ne $registryData) + elseif ((Get-ItemProperty -Path $registryKey | Select-Object -ExpandProperty $registryValue) -ne $registryData) { Try { Set-ItemProperty -Path $registryKey -Name $registryValue -Value $registryData @@ -142,7 +99,7 @@ if($state -eq "present") { } elseif(-not (Test-Path $registryKey)) { - Try + Try { $newRegistryKey = New-Item $registryKey -Force $result.changed = $true From c6a934d57b9d0b171ad6a4c45c14ca5087a941c0 Mon Sep 17 00:00:00 2001 From: Brian Geihsler Date: Wed, 14 Oct 2015 11:04:38 -0700 Subject: [PATCH 0848/2522] Fix #1107: Prepend registry type on key path --- windows/win_regedit.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/windows/win_regedit.ps1 b/windows/win_regedit.ps1 index f9491e39c57..ee92e781d0c 100644 --- a/windows/win_regedit.ps1 +++ b/windows/win_regedit.ps1 @@ -31,6 +31,8 @@ $state = Get-Attr -obj $params -name "state" -validateSet "present","absent" -de $registryData = Get-Attr -obj $params -name "data" -default $null $registryDataType = Get-Attr -obj $params -name "datatype" -validateSet "binary","dword","expandstring","multistring","string","qword" -default "string" +$registryKey = "Registry::" + $registryKey + If ($state -eq "present" -and $registryData -eq $null -and $registryValue -ne $null) { Fail-Json $result "missing required argument: data" From aef2b929c9631d3b276b4fc1a94fbe0ffa95d010 Mon Sep 17 00:00:00 2001 From: wimnat Date: Thu, 15 Oct 2015 01:09:12 +0000 Subject: [PATCH 0849/2522] New module - ec2_vol_facts --- cloud/amazon/ec2_vol_facts.py | 135 ++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 cloud/amazon/ec2_vol_facts.py diff --git a/cloud/amazon/ec2_vol_facts.py b/cloud/amazon/ec2_vol_facts.py new file mode 100644 index 00000000000..a3ab94682e8 --- /dev/null +++ b/cloud/amazon/ec2_vol_facts.py @@ -0,0 +1,135 @@ +#!/usr/bin/python +# +# This is a free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This Ansible library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this library. If not, see . + +DOCUMENTATION = ''' +--- +module: ec2_vol_facts +short_description: Gather facts about ec2 volumes in AWS +description: + - Gather facts about ec2 volumes in AWS +version_added: "2.0" +author: "Rob White (@wimnat)" +options: + filters: + description: + - A dict of filters to apply. Each dict item consists of a filter key and a filter value. See U(http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeVolumes.html) for possible filters. + required: false + default: null +extends_documentation_fragment: + - aws + - ec2 +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Gather facts about all volumes +- ec2_vol_facts: + +# Gather facts about a particular volume using volume ID +- ec2_vol_facts: + filters: + volume-id: vol-00112233 + +# Gather facts about any volume with a tag key Name and value Example +- ec2_vol_facts: + filters: + "tag:Name": Example + +# Gather facts about any volume that is attached +- ec2_vol_facts: + filters: + attachment.status: attached + +''' + +try: + import boto.ec2 + from boto.exception import BotoServerError + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +def get_volume_info(volume): + + attachment = volume.attach_data + + volume_info = { + 'create_time': volume.create_time, + 'id': volume.id, + 'iops': volume.iops, + 'size': volume.size, + 'snapshot_id': volume.snapshot_id, + 'status': volume.status, + 'type': volume.type, + 'zone': volume.zone, + 'region': volume.region.name, + 'attachment_set': { + 'attach_time': attachment.attach_time, + 'device': attachment.device, + 'instance_id': attachment.instance_id, + 'status': attachment.status + }, + 'tags': volume.tags + } + + return volume_info + +def list_ec2_volumes(connection, module): + + filters = module.params.get("filters") + volume_dict_array = [] + + try: + all_volumes = connection.get_all_volumes(filters=filters) + except BotoServerError as e: + module.fail_json(msg=e.message) + + for volume in all_volumes: + volume_dict_array.append(get_volume_info(volume)) + + module.exit_json(volumes=volume_dict_array) + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + filters = dict(default=None, type='dict') + ) + ) + + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO: + module.fail_json(msg='boto required for this module') + + region, ec2_url, aws_connect_params = get_aws_connection_info(module) + + if region: + try: + connection = connect_to_aws(boto.ec2, region, **aws_connect_params) + except (boto.exception.NoAuthHandlerFound, StandardError), e: + module.fail_json(msg=str(e)) + else: + module.fail_json(msg="region must be specified") + + list_ec2_volumes(connection, module) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From e5f2931707d39b0805b9d951720ec5449a24f75e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Thu, 15 Oct 2015 11:28:19 +0200 Subject: [PATCH 0850/2522] iptables: add note about rules --- system/iptables.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/system/iptables.py b/system/iptables.py index 2b79f1d3ad8..f490534f062 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -34,6 +34,8 @@ version_added: "2.0" author: Linus Unnebäck (@LinusU) description: Iptables is used to set up, maintain, and inspect the tables of IP packet filter rules in the Linux kernel. This module does not handle the saving and/or loading of rules, but rather only manipulates the current rules that are present in memory. This is the same as the behaviour of the "iptables" and "ip6tables" command which this module uses internally. +notes: + - This module just deals with individual rules. If you need advanced chaining of rules the recommended way is to template the iptables restore file. options: table: description: This option specifies the packet matching table which the command should operate on. If the kernel is configured with automatic module loading, an attempt will be made to load the appropriate module for that table if it is not already there. From 71dc56956665b380e1f402cdeb516d1ce534dfe9 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Thu, 15 Oct 2015 12:01:11 +0200 Subject: [PATCH 0851/2522] fixed problem with sid/windows 2008 by using SID. fixed problems related to default accounts like BUILTIN\SYSTEM --- windows/win_acl.ps1 | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/windows/win_acl.ps1 b/windows/win_acl.ps1 index b08fb03e7f3..994ff255fa8 100644 --- a/windows/win_acl.ps1 +++ b/windows/win_acl.ps1 @@ -55,15 +55,11 @@ Function UserSearch if ($IsLocalAccount -eq $true) { - $localaccount = get-wmiobject -class "Win32_UserAccount" -namespace "root\CIMV2" -filter "(LocalAccount = True)" | where {$_.Caption -eq $AccountName} + # do not use Win32_UserAccount, because e.g. SYSTEM (BUILTIN\SYSTEM or COMPUUTERNAME\SYSTEM) will not be listed. on Win32_Account groups will be listed too + $localaccount = get-wmiobject -class "Win32_Account" -namespace "root\CIMV2" -filter "(LocalAccount = True)" | where {$_.Caption -eq $AccountName} if ($localaccount) { - return $localaccount.Caption - } - $LocalGroup = get-wmiobject -class "Win32_Group" -namespace "root\CIMV2" -filter "LocalAccount = True"| where {$_.Caption -eq $AccountName} - if ($LocalGroup) - { - return $LocalGroup.Caption + return $localaccount.SID } } ElseIf (($IsDomainAccount -eq $true) -and ($IsUpn -eq $false)) @@ -75,7 +71,13 @@ Function UserSearch if ($result) { - return $accountname + $user = $result.GetDirectoryEntry() + + # get binary SID from AD account + $binarySID = $user.ObjectSid.Value + + # convert to string SID + return (New-Object System.Security.Principal.SecurityIdentifier($binarySID,0)).Value } } @@ -100,10 +102,10 @@ Else { } If ($params.user) { - $user = UserSearch -AccountName ($Params.User) + $sid = UserSearch -AccountName ($Params.User) # Test that the user/group is resolvable on the local machine - if (!$user) + if (!$sid) { Fail-Json $result "$($Params.User) is not a valid user or group on the host machine or domain" } @@ -174,14 +176,15 @@ Try { $objType =[System.Security.AccessControl.AccessControlType]::Deny } - $objUser = New-Object System.Security.Principal.NTAccount($user) + $objUser = New-Object System.Security.Principal.SecurityIdentifier($sid) $objACE = New-Object System.Security.AccessControl.FileSystemAccessRule ($objUser, $colRights, $InheritanceFlag, $PropagationFlag, $objType) $objACL = Get-ACL $path # Check if the ACE exists already in the objects ACL list $match = $false ForEach($rule in $objACL.Access){ - If (($rule.FileSystemRights -eq $objACE.FileSystemRights) -And ($rule.AccessControlType -eq $objACE.AccessControlType) -And ($rule.IdentityReference -eq $objACE.IdentityReference) -And ($rule.IsInherited -eq $objACE.IsInherited) -And ($rule.InheritanceFlags -eq $objACE.InheritanceFlags) -And ($rule.PropagationFlags -eq $objACE.PropagationFlags)) { + $ruleIdentity = $rule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) + If (($rule.FileSystemRights -eq $objACE.FileSystemRights) -And ($rule.AccessControlType -eq $objACE.AccessControlType) -And ($ruleIdentity -eq $objACE.IdentityReference) -And ($rule.IsInherited -eq $objACE.IsInherited) -And ($rule.InheritanceFlags -eq $objACE.InheritanceFlags) -And ($rule.PropagationFlags -eq $objACE.PropagationFlags)) { $match = $true Break } @@ -219,7 +222,7 @@ Try { } } Catch { - Fail-Json $result "an error occured when attempting to $state $rights permission(s) on $path for $user" + Fail-Json $result "an error occured when attempting to $state $rights permission(s) on $path for $($Params.User)" } Exit-Json $result \ No newline at end of file From ea430466a8982a59c8d0b01549135810c87f51b2 Mon Sep 17 00:00:00 2001 From: Corwin Brown Date: Thu, 15 Oct 2015 18:02:21 -0500 Subject: [PATCH 0852/2522] Made win_iis_webapppool Strict Compliant --- windows/win_iis_webapppool.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_iis_webapppool.ps1 b/windows/win_iis_webapppool.ps1 index 2ed369e4a3f..6dcf7ec192c 100644 --- a/windows/win_iis_webapppool.ps1 +++ b/windows/win_iis_webapppool.ps1 @@ -39,7 +39,7 @@ If (($state -Ne $FALSE) -And ($state -NotIn $valid_states)) { # Attributes parameter - Pipe separated list of attributes where # keys and values are separated by comma (paramA:valyeA|paramB:valueB) $attributes = @{}; -If ($params.attributes) { +If (Get-Member -InputObject $params -Name attributes) { $params.attributes -split '\|' | foreach { $key, $value = $_ -split "\:"; $attributes.Add($key, $value); From 5adc4a201470a41a2157231bd8e6bef621a2d8df Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Fri, 16 Oct 2015 10:49:23 -0700 Subject: [PATCH 0853/2522] Fix docs to build --- system/iptables.py | 139 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 122 insertions(+), 17 deletions(-) diff --git a/system/iptables.py b/system/iptables.py index f490534f062..d487476fb74 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -33,12 +33,23 @@ requirements: [] version_added: "2.0" author: Linus Unnebäck (@LinusU) -description: Iptables is used to set up, maintain, and inspect the tables of IP packet filter rules in the Linux kernel. This module does not handle the saving and/or loading of rules, but rather only manipulates the current rules that are present in memory. This is the same as the behaviour of the "iptables" and "ip6tables" command which this module uses internally. +description: + - Iptables is used to set up, maintain, and inspect the tables of IP packet + filter rules in the Linux kernel. This module does not handle the saving + and/or loading of rules, but rather only manipulates the current rules + that are present in memory. This is the same as the behaviour of the + "iptables" and "ip6tables" command which this module uses internally. notes: - - This module just deals with individual rules. If you need advanced chaining of rules the recommended way is to template the iptables restore file. + - This module just deals with individual rules. If you need advanced + chaining of rules the recommended way is to template the iptables restore + file. options: table: - description: This option specifies the packet matching table which the command should operate on. If the kernel is configured with automatic module loading, an attempt will be made to load the appropriate module for that table if it is not already there. + description: + - This option specifies the packet matching table which the command + should operate on. If the kernel is configured with automatic module + loading, an attempt will be made to load the appropriate module for + that table if it is not already there. required: false default: filter choices: [ "filter", "nat", "mangle", "raw", "security" ] @@ -53,46 +64,140 @@ default: ipv4 choices: [ "ipv4", "ipv6" ] chain: - description: Chain to operate on. This option can either be the name of a user defined chain or any of the builtin chains: "INPUT", "FORWARD", "OUTPUT", "PREROUTING", "POSTROUTING", "SECMARK", "CONNSECMARK" + description: + - "Chain to operate on. This option can either be the name of a user + defined chain or any of the builtin chains: 'INPUT', 'FORWARD', + 'OUTPUT', 'PREROUTING', 'POSTROUTING', 'SECMARK', 'CONNSECMARK'" required: true protocol: - description: The protocol of the rule or of the packet to check. The specified protocol can be one of tcp, udp, udplite, icmp, esp, ah, sctp or the special keyword "all", or it can be a numeric value, representing one of these protocols or a different one. A protocol name from /etc/protocols is also allowed. A "!" argument before the protocol inverts the test. The number zero is equivalent to all. "all" will match with all protocols and is taken as default when this option is omitted. + description: + - The protocol of the rule or of the packet to check. The specified + protocol can be one of tcp, udp, udplite, icmp, esp, ah, sctp or the + special keyword "all", or it can be a numeric value, representing one + of these protocols or a different one. A protocol name from + /etc/protocols is also allowed. A "!" argument before the protocol + inverts the test. The number zero is equivalent to all. "all" will + match with all protocols and is taken as default when this option is + omitted. required: false source: - description: Source specification. Address can be either a network name, a hostname, a network IP address (with /mask), or a plain IP address. Hostnames will be resolved once only, before the rule is submitted to the kernel. Please note that specifying any name to be resolved with a remote query such as DNS is a really bad idea. The mask can be either a network mask or a plain number, specifying the number of 1's at the left side of the network mask. Thus, a mask of 24 is equivalent to 255.255.255.0. A "!" argument before the address specification inverts the sense of the address.Source specification. Address can be either a network name, a hostname, a network IP address (with /mask), or a plain IP address. Hostnames will be resolved once only, before the rule is submitted to the kernel. Please note that specifying any name to be resolved with a remote query such as DNS is a really bad idea. The mask can be either a network mask or a plain number, specifying the number of 1's at the left side of the network mask. Thus, a mask of 24 is equivalent to 255.255.255.0. A "!" argument before the address specification inverts the sense of the address. + description: + - Source specification. Address can be either a network name, + a hostname, a network IP address (with /mask), or a plain IP address. + Hostnames will be resolved once only, before the rule is submitted to + the kernel. Please note that specifying any name to be resolved with + a remote query such as DNS is a really bad idea. The mask can be + either a network mask or a plain number, specifying the number of 1's + at the left side of the network mask. Thus, a mask of 24 is equivalent + to 255.255.255.0. A "!" argument before the address specification + inverts the sense of the address.Source specification. Address can be + either a network name, a hostname, a network IP address (with /mask), + or a plain IP address. Hostnames will be resolved once only, before + the rule is submitted to the kernel. Please note that specifying any + name to be resolved with a remote query such as DNS is a really bad + idea. The mask can be either a network mask or a plain number, + specifying the number of 1's at the left side of the network mask. + Thus, a mask of 24 is equivalent to 255.255.255.0. A "!" argument + before the address specification inverts the sense of the address. required: false destination: - description: Destination specification. Address can be either a network name, a hostname, a network IP address (with /mask), or a plain IP address. Hostnames will be resolved once only, before the rule is submitted to the kernel. Please note that specifying any name to be resolved with a remote query such as DNS is a really bad idea. The mask can be either a network mask or a plain number, specifying the number of 1's at the left side of the network mask. Thus, a mask of 24 is equivalent to 255.255.255.0. A "!" argument before the address specification inverts the sense of the address.Source specification. Address can be either a network name, a hostname, a network IP address (with /mask), or a plain IP address. Hostnames will be resolved once only, before the rule is submitted to the kernel. Please note that specifying any name to be resolved with a remote query such as DNS is a really bad idea. The mask can be either a network mask or a plain number, specifying the number of 1's at the left side of the network mask. Thus, a mask of 24 is equivalent to 255.255.255.0. A "!" argument before the address specification inverts the sense of the address. + description: + - Destination specification. Address can be either a network name, + a hostname, a network IP address (with /mask), or a plain IP address. + Hostnames will be resolved once only, before the rule is submitted to + the kernel. Please note that specifying any name to be resolved with + a remote query such as DNS is a really bad idea. The mask can be + either a network mask or a plain number, specifying the number of 1's + at the left side of the network mask. Thus, a mask of 24 is equivalent + to 255.255.255.0. A "!" argument before the address specification + inverts the sense of the address.Source specification. Address can be + either a network name, a hostname, a network IP address (with /mask), + or a plain IP address. Hostnames will be resolved once only, before + the rule is submitted to the kernel. Please note that specifying any + name to be resolved with a remote query such as DNS is a really bad + idea. The mask can be either a network mask or a plain number, + specifying the number of 1's at the left side of the network mask. + Thus, a mask of 24 is equivalent to 255.255.255.0. A "!" argument + before the address specification inverts the sense of the address. required: false match: - description: Specifies a match to use, that is, an extension module that tests for a specific property. The set of matches make up the condition under which a target is invoked. Matches are evaluated first to last if specified as an array and work in short-circuit fashion, i.e. if one extension yields false, evaluation will stop. + description: + - Specifies a match to use, that is, an extension module that tests for + a specific property. The set of matches make up the condition under + which a target is invoked. Matches are evaluated first to last if + specified as an array and work in short-circuit fashion, i.e. if one + extension yields false, evaluation will stop. required: false jump: - description: This specifies the target of the rule; i.e., what to do if the packet matches it. The target can be a user-defined chain (other than the one this rule is in), one of the special builtin targets which decide the fate of the packet immediately, or an extension (see EXTENSIONS below). If this option is omitted in a rule (and the goto paramater is not used), then matching the rule will have no effect on the packet's fate, but the counters on the rule will be incremented. + description: + - This specifies the target of the rule; i.e., what to do if the packet + matches it. The target can be a user-defined chain (other than the one + this rule is in), one of the special builtin targets which decide the + fate of the packet immediately, or an extension (see EXTENSIONS + below). If this option is omitted in a rule (and the goto paramater + is not used), then matching the rule will have no effect on the + packet's fate, but the counters on the rule will be incremented. required: false goto: - description: This specifies that the processing should continue in a user specified chain. Unlike the jump argument return will not continue processing in this chain but instead in the chain that called us via jump. + description: + - This specifies that the processing should continue in a user specified + chain. Unlike the jump argument return will not continue processing in + this chain but instead in the chain that called us via jump. required: false in_interface: - description: Name of an interface via which a packet was received (only for packets entering the INPUT, FORWARD and PREROUTING chains). When the "!" argument is used before the interface name, the sense is inverted. If the interface name ends in a "+", then any interface which begins with this name will match. If this option is omitted, any interface name will match. + description: + - Name of an interface via which a packet was received (only for packets + entering the INPUT, FORWARD and PREROUTING chains). When the "!" + argument is used before the interface name, the sense is inverted. If + the interface name ends in a "+", then any interface which begins with + this name will match. If this option is omitted, any interface name + will match. required: false out_interface: - description: Name of an interface via which a packet is going to be sent (for packets entering the FORWARD, OUTPUT and POSTROUTING chains). When the "!" argument is used before the interface name, the sense is inverted. If the interface name ends in a "+", then any interface which begins with this name will match. If this option is omitted, any interface name will match. + description: + - Name of an interface via which a packet is going to be sent (for + packets entering the FORWARD, OUTPUT and POSTROUTING chains). When the + "!" argument is used before the interface name, the sense is inverted. + If the interface name ends in a "+", then any interface which begins + with this name will match. If this option is omitted, any interface + name will match. required: false fragment: - description: This means that the rule only refers to second and further fragments of fragmented packets. Since there is no way to tell the source or destination ports of such a packet (or ICMP type), such a packet will not match any rules which specify them. When the "!" argument precedes fragment argument, the rule will only match head fragments, or unfragmented packets. + description: + - This means that the rule only refers to second and further fragments + of fragmented packets. Since there is no way to tell the source or + destination ports of such a packet (or ICMP type), such a packet will + not match any rules which specify them. When the "!" argument precedes + fragment argument, the rule will only match head fragments, or + unfragmented packets. required: false set_counters: - description: This enables the administrator to initialize the packet and byte counters of a rule (during INSERT, APPEND, REPLACE operations). + description: + - This enables the administrator to initialize the packet and byte + counters of a rule (during INSERT, APPEND, REPLACE operations). required: false source_port: - description: Source port or port range specification. This can either be a service name or a port number. An inclusive range can also be specified, using the format first:last. If the first port is omitted, "0" is assumed; if the last is omitted, "65535" is assumed. If the first port is greater than the second one they will be swapped. + description: + - "Source port or port range specification. This can either be a service + name or a port number. An inclusive range can also be specified, using + the format first:last. If the first port is omitted, '0' is assumed; + if the last is omitted, '65535' is assumed. If the first port is + greater than the second one they will be swapped." required: false destination_port: - description: Destination port or port range specification. This can either be a service name or a port number. An inclusive range can also be specified, using the format first:last. If the first port is omitted, "0" is assumed; if the last is omitted, "65535" is assumed. If the first port is greater than the second one they will be swapped. + description: + - "Destination port or port range specification. This can either be + a service name or a port number. An inclusive range can also be + specified, using the format first:last. If the first port is omitted, + '0' is assumed; if the last is omitted, '65535' is assumed. If the + first port is greater than the second one they will be swapped." required: false to_ports: - description: This specifies a destination port or range of ports to use: without this, the destination port is never altered. This is only valid if the rule also specifies one of the following protocols: tcp, udp, dccp or sctp. + description: + - "This specifies a destination port or range of ports to use: without + this, the destination port is never altered. This is only valid if the + rule also specifies one of the following protocols: tcp, udp, dccp or + sctp." required: false ''' From 632de528a0e7eb63960b8bf10a1b33e509735408 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Fri, 16 Oct 2015 11:11:42 -0700 Subject: [PATCH 0854/2522] Fix win_acl docs build --- windows/win_acl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_acl.py b/windows/win_acl.py index df815db0a0f..cc748a4412f 100644 --- a/windows/win_acl.py +++ b/windows/win_acl.py @@ -89,7 +89,7 @@ - ContainerInherit - ObjectInherit - None - default: For Leaf File: None; For Directory: ContainerInherit, ObjectInherit; + default: For Leaf File, None; For Directory, ContainerInherit, ObjectInherit; propagation: description: - Propagation flag on the ACL rules. For more information on the choices see MSDN PropagationFlags Enumeration. From 7c8e3f4da1acb4f1c5b341e476faa7db288c1531 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Sat, 17 Oct 2015 17:07:39 +0200 Subject: [PATCH 0855/2522] added separate module to change owner, since win_acl is ACL only and should not be more complex --- windows/win_owner.ps1 | 134 ++++++++++++++++++++++++++++++++++++++++++ windows/win_owner.py | 67 +++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 windows/win_owner.ps1 create mode 100644 windows/win_owner.py diff --git a/windows/win_owner.ps1 b/windows/win_owner.ps1 new file mode 100644 index 00000000000..5af5072b372 --- /dev/null +++ b/windows/win_owner.ps1 @@ -0,0 +1,134 @@ +#!powershell +# This file is part of Ansible +# +# Copyright 2015, Hans-Joachim Kliemeck +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# WANT_JSON +# POWERSHELL_COMMON + +#Functions +Function UserSearch +{ + Param ([string]$AccountName) + #Check if there's a realm specified + if ($AccountName.Split("\").count -gt 1) + { + if ($AccountName.Split("\")[0] -eq $env:COMPUTERNAME) + { + $IsLocalAccount = $true + } + Else + { + $IsDomainAccount = $true + $IsUpn = $false + } + + } + Elseif ($AccountName -contains "@") + { + $IsDomainAccount = $true + $IsUpn = $true + } + Else + { + #Default to local user account + $accountname = $env:COMPUTERNAME + "\" + $AccountName + $IsLocalAccount = $true + } + + + if ($IsLocalAccount -eq $true) + { + # do not use Win32_UserAccount, because e.g. SYSTEM (BUILTIN\SYSTEM or COMPUUTERNAME\SYSTEM) will not be listed. on Win32_Account groups will be listed too + $localaccount = get-wmiobject -class "Win32_Account" -namespace "root\CIMV2" -filter "(LocalAccount = True)" | where {$_.Caption -eq $AccountName} + if ($localaccount) + { + return $localaccount.SID + } + } + ElseIf (($IsDomainAccount -eq $true) -and ($IsUpn -eq $false)) + { + #Search by samaccountname + $Searcher = [adsisearcher]"" + $Searcher.Filter = "sAMAccountName=$($accountname.split("\")[1])" + $result = $Searcher.FindOne() + + if ($result) + { + $user = $result.GetDirectoryEntry() + + # get binary SID from AD account + $binarySID = $user.ObjectSid.Value + + # convert to string SID + return (New-Object System.Security.Principal.SecurityIdentifier($binarySID,0)).Value + } + } + +} + +$params = Parse-Args $args; + +$result = New-Object PSObject; +Set-Attr $result "changed" $false; + +$path = Get-Attr $params "path" -failifempty $true +$user = Get-Attr $params "user" -failifempty $true +$recurse = Get-Attr $params "recurse" "no" -validateSet "no","yes" -resultobj $result + +If (-Not (Test-Path -Path $path)) { + Fail-Json $result "$path file or directory does not exist on the host" +} + +# Test that the user/group is resolvable on the local machine +$sid = UserSearch -AccountName ($user) +if (!$sid) +{ + Fail-Json $result "$user is not a valid user or group on the host machine or domain" +} + +Try { + $objUser = New-Object System.Security.Principal.SecurityIdentifier($sid) + + $file = Get-Item -Path $path + $acl = Get-Acl $file.FullName + + If ($acl.getOwner([System.Security.Principal.SecurityIdentifier]) -ne $objUser) { + Set-Attr $result "changed" $true; + } + + $acl.setOwner($objUser) + Set-Acl $file.FullName $acl + + If ($recurse -eq "yes") { + $files = Get-ChildItem -Path $path -Force -Recurse + ForEach($file in $files){ + $acl = Get-Acl $file.FullName + + If ($acl.getOwner([System.Security.Principal.SecurityIdentifier]) -ne $objUser) { + Set-Attr $result "changed" $true; + } + + $acl.setOwner($objUser) + Set-Acl $file.FullName $acl + } + } +} +Catch { + Fail-Json $result "an error occured when attempting to change owner on $path for $user" +} + +Exit-Json $result diff --git a/windows/win_owner.py b/windows/win_owner.py new file mode 100644 index 00000000000..9af816abfe9 --- /dev/null +++ b/windows/win_owner.py @@ -0,0 +1,67 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2015, Hans-Joachim Kliemeck +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# this is a windows documentation stub. actual code lives in the .ps1 +# file of the same name + +DOCUMENTATION = ''' +--- +module: win_service_configure +version_added: "2.0" +short_description: Set owner +description: + - Set owner of files or directories +options: + path: + description: + - Path to be used for changing owner + required: true + default: null + user: + description: + - Name to be used for changing owner + required: true + default: null + recurse: + description: + - Indicates if the owner should be changed recursively + required: false + choices: + - no + - yes + default: no +author: Hans-Joachim Kliemeck +''' + +EXAMPLES = ''' +# Playbook example +--- +- name: Change owner of Path + win_owner: + path: 'C:\\apache\\' + user: apache + recurse: yes + +- name: Set the owner of root directory + win_owner: + path: 'C:\\apache\\' + user: SYSTEM + recurse: no +''' From cda7e96fcc277ecf285bb61264882a60f91ab9a9 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Sat, 17 Oct 2015 23:10:56 +0200 Subject: [PATCH 0856/2522] added userprincipal support --- windows/win_owner.ps1 | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/windows/win_owner.ps1 b/windows/win_owner.ps1 index 5af5072b372..519d6fe5802 100644 --- a/windows/win_owner.ps1 +++ b/windows/win_owner.ps1 @@ -48,8 +48,7 @@ Function UserSearch $accountname = $env:COMPUTERNAME + "\" + $AccountName $IsLocalAccount = $true } - - + if ($IsLocalAccount -eq $true) { # do not use Win32_UserAccount, because e.g. SYSTEM (BUILTIN\SYSTEM or COMPUUTERNAME\SYSTEM) will not be listed. on Win32_Account groups will be listed too @@ -59,13 +58,19 @@ Function UserSearch return $localaccount.SID } } - ElseIf (($IsDomainAccount -eq $true) -and ($IsUpn -eq $false)) + ElseIf ($IsDomainAccount -eq $true) { #Search by samaccountname $Searcher = [adsisearcher]"" - $Searcher.Filter = "sAMAccountName=$($accountname.split("\")[1])" - $result = $Searcher.FindOne() - + + If ($IsUpn -eq $false) { + $Searcher.Filter = "sAMAccountName=$($accountname.split("\")[1])" + } + Else { + $Searcher.Filter = "userPrincipalName=$($accountname)" + } + + $result = $Searcher.FindOne() if ($result) { $user = $result.GetDirectoryEntry() @@ -77,7 +82,6 @@ Function UserSearch return (New-Object System.Security.Principal.SecurityIdentifier($binarySID,0)).Value } } - } $params = Parse-Args $args; From 21c564848dcc92a6575364531ab76b4e4c6678d5 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Sat, 17 Oct 2015 23:05:51 +0200 Subject: [PATCH 0857/2522] added userprincipal support --- windows/win_acl.ps1 | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/windows/win_acl.ps1 b/windows/win_acl.ps1 index 994ff255fa8..041e66b9c11 100644 --- a/windows/win_acl.ps1 +++ b/windows/win_acl.ps1 @@ -51,8 +51,7 @@ Function UserSearch $accountname = $env:COMPUTERNAME + "\" + $AccountName $IsLocalAccount = $true } - - + if ($IsLocalAccount -eq $true) { # do not use Win32_UserAccount, because e.g. SYSTEM (BUILTIN\SYSTEM or COMPUUTERNAME\SYSTEM) will not be listed. on Win32_Account groups will be listed too @@ -62,13 +61,19 @@ Function UserSearch return $localaccount.SID } } - ElseIf (($IsDomainAccount -eq $true) -and ($IsUpn -eq $false)) + ElseIf ($IsDomainAccount -eq $true) { #Search by samaccountname $Searcher = [adsisearcher]"" - $Searcher.Filter = "sAMAccountName=$($accountname.split("\")[1])" - $result = $Searcher.FindOne() - + + If ($IsUpn -eq $false) { + $Searcher.Filter = "sAMAccountName=$($accountname.split("\")[1])" + } + Else { + $Searcher.Filter = "userPrincipalName=$($accountname)" + } + + $result = $Searcher.FindOne() if ($result) { $user = $result.GetDirectoryEntry() @@ -80,7 +85,6 @@ Function UserSearch return (New-Object System.Security.Principal.SecurityIdentifier($binarySID,0)).Value } } - } $params = Parse-Args $args; @@ -225,4 +229,4 @@ Catch { Fail-Json $result "an error occured when attempting to $state $rights permission(s) on $path for $($Params.User)" } -Exit-Json $result \ No newline at end of file +Exit-Json $result From 75163ac5fea4443f23d853b7967a2ab5ccd98521 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Sun, 18 Oct 2015 00:00:47 +0200 Subject: [PATCH 0858/2522] made win_acl strict compliant --- windows/win_acl.ps1 | 116 +++++++++++++------------------------------- 1 file changed, 34 insertions(+), 82 deletions(-) diff --git a/windows/win_acl.ps1 b/windows/win_acl.ps1 index 041e66b9c11..fa45f023be8 100644 --- a/windows/win_acl.ps1 +++ b/windows/win_acl.ps1 @@ -88,84 +88,36 @@ Function UserSearch } $params = Parse-Args $args; - -$result = New-Object psobject @{ - win_acl = New-Object psobject - changed = $false -} - -If ($params.path) { - $path = $params.path.toString() - - If (-Not (Test-Path -Path $path)) { - Fail-Json $result "$path file or directory does not exist on the host" - } -} -Else { - Fail-Json $result "missing required argument: path" -} - -If ($params.user) { - $sid = UserSearch -AccountName ($Params.User) - - # Test that the user/group is resolvable on the local machine - if (!$sid) - { - Fail-Json $result "$($Params.User) is not a valid user or group on the host machine or domain" - } -} -Else { - Fail-Json $result "missing required argument: user. specify the user or group to apply permission changes." -} - -If ($params.type -eq "allow") { - $type = $true -} -ElseIf ($params.type -eq "deny") { - $type = $false -} -Else { - Fail-Json $result "missing required argument: type. specify whether to allow or deny the specified rights." -} - -If ($params.inherit) { - # If it's a file then no flags can be set or an exception will be thrown - If (Test-Path -Path $path -PathType Leaf) { - $inherit = "None" - } - Else { - $inherit = $params.inherit.toString() - } -} -Else { - # If it's a file then no flags can be set or an exception will be thrown - If (Test-Path -Path $path -PathType Leaf) { - $inherit = "None" - } - Else { - $inherit = "ContainerInherit, ObjectInherit" - } -} - -If ($params.propagation) { - $propagation = $params.propagation.toString() -} -Else { - $propagation = "None" -} - -If ($params.rights) { - $rights = $params.rights.toString() + +$result = New-Object PSObject; +Set-Attr $result "changed" $false; + +$path = Get-Attr $params "path" -failifempty $true +$user = Get-Attr $params "user" -failifempty $true +$rights = Get-Attr $params "rights" -failifempty $true + +$type = Get-Attr $params "type" -validateSet "allow","deny" -resultobj $result +$state = Get-Attr $params "state" "present" -validateSet "present","absent" -resultobj $result + +$inherit = Get-Attr $params "inherit" "" +$propagation = Get-Attr $params "propagation" "None" -validateSet "None","NoPropagateInherit","InheritOnly" -resultobj $result + +If (-Not (Test-Path -Path $path)) { + Fail-Json $result "$path file or directory does not exist on the host" } -Else { - Fail-Json $result "missing required argument: rights" + +# Test that the user/group is resolvable on the local machine +$sid = UserSearch -AccountName ($user) +if (!$sid) +{ + Fail-Json $result "$user is not a valid user or group on the host machine or domain" } - -If ($params.state -eq "absent") { - $state = "remove" + +If (Test-Path -Path $path -PathType Leaf) { + $inherit = "None" } -Else { - $state = "add" +ElseIf ($inherit -eq "") { + $inherit = "ContainerInherit, ObjectInherit" } Try { @@ -173,7 +125,7 @@ Try { $InheritanceFlag = [System.Security.AccessControl.InheritanceFlags]$inherit $PropagationFlag = [System.Security.AccessControl.PropagationFlags]$propagation - If ($type) { + If ($type -eq "allow") { $objType =[System.Security.AccessControl.AccessControlType]::Allow } Else { @@ -193,22 +145,22 @@ Try { Break } } - - If ($state -eq "add" -And $match -eq $false) { + + If ($state -eq "present" -And $match -eq $false) { Try { $objACL.AddAccessRule($objACE) Set-ACL $path $objACL - $result.changed = $true + Set-Attr $result "changed" $true; } Catch { Fail-Json $result "an exception occured when adding the specified rule" } } - ElseIf ($state -eq "remove" -And $match -eq $true) { + ElseIf ($state -eq "absent" -And $match -eq $true) { Try { $objACL.RemoveAccessRule($objACE) Set-ACL $path $objACL - $result.changed = $true + Set-Attr $result "changed" $true; } Catch { Fail-Json $result "an exception occured when removing the specified rule" @@ -226,7 +178,7 @@ Try { } } Catch { - Fail-Json $result "an error occured when attempting to $state $rights permission(s) on $path for $($Params.User)" + Fail-Json $result "an error occured when attempting to $state $rights permission(s) on $path for $user" } Exit-Json $result From a0a51ffd6611e90ef17bf4f0f8b253d5dba380cc Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Sun, 18 Oct 2015 17:06:28 +0200 Subject: [PATCH 0859/2522] added credits, fixed documentation --- windows/win_acl.ps1 | 2 ++ windows/win_acl.py | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/windows/win_acl.ps1 b/windows/win_acl.ps1 index fa45f023be8..fb12ae6cee3 100644 --- a/windows/win_acl.ps1 +++ b/windows/win_acl.ps1 @@ -2,6 +2,8 @@ # This file is part of Ansible # # Copyright 2015, Phil Schwartz +# Copyright 2015, Trond Hindenes +# Copyright 2015, Hans-Joachim Kliemeck # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/windows/win_acl.py b/windows/win_acl.py index df815db0a0f..76c6ffb7293 100644 --- a/windows/win_acl.py +++ b/windows/win_acl.py @@ -1,7 +1,9 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# (c) 2015, Phil Schwartz +# Copyright 2015, Phil Schwartz +# Copyright 2015, Trond Hindenes +# Copyright 2015, Hans-Joachim Kliemeck # # This file is part of Ansible # @@ -40,7 +42,7 @@ default: none state: description: - - Specify whether to add (present) or remove (absent) the specified access rule + - Specify whether to add C(present) or remove C(absent) the specified access rule required: no choices: - present @@ -99,7 +101,7 @@ - NoPropagateInherit - InheritOnly default: "None" -author: Phil Schwartz (@schwartzmx), Trond Hindenes (@trondhindenes) +author: Phil Schwartz (@schwartzmx), Trond Hindenes (@trondhindenes), Hans-Joachim Kliemeck (@h0nIg) ''' EXAMPLES = ''' From 95862793d0c00ee946ed891a021b8569cae9feab Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Sun, 18 Oct 2015 17:24:27 +0200 Subject: [PATCH 0860/2522] added module to disable acl inheritance --- windows/win_acl_inheritance.ps1 | 55 ++++++++++++++++++++++++++++++ windows/win_acl_inheritance.py | 59 +++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 windows/win_acl_inheritance.ps1 create mode 100644 windows/win_acl_inheritance.py diff --git a/windows/win_acl_inheritance.ps1 b/windows/win_acl_inheritance.ps1 new file mode 100644 index 00000000000..e72570ba3a6 --- /dev/null +++ b/windows/win_acl_inheritance.ps1 @@ -0,0 +1,55 @@ +#!powershell +# This file is part of Ansible +# +# Copyright 2015, Hans-Joachim Kliemeck +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# WANT_JSON +# POWERSHELL_COMMON + + +$params = Parse-Args $args; + +$result = New-Object PSObject; +Set-Attr $result "changed" $false; + +$path = Get-Attr $params "path" -failifempty $true +$copy = Get-Attr $params "copy" "no" -validateSet "no","yes" -resultobj $result + +If (-Not (Test-Path -Path $path)) { + Fail-Json $result "$path file or directory does not exist on the host" +} + +Try { + $objACL = Get-ACL $path + $alreadyDisabled = !$objACL.AreAccessRulesProtected + + If ($copy -eq "yes") { + $objACL.SetAccessRuleProtection($True, $True) + } Else { + $objACL.SetAccessRuleProtection($True, $False) + } + + If ($alreadyDisabled) { + Set-Attr $result "changed" $true; + } + + Set-ACL $path $objACL +} +Catch { + Fail-Json $result "an error occured when attempting to disable inheritance" +} + +Exit-Json $result diff --git a/windows/win_acl_inheritance.py b/windows/win_acl_inheritance.py new file mode 100644 index 00000000000..784aa5f9877 --- /dev/null +++ b/windows/win_acl_inheritance.py @@ -0,0 +1,59 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2015, Hans-Joachim Kliemeck +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# this is a windows documentation stub. actual code lives in the .ps1 +# file of the same name + +DOCUMENTATION = ''' +--- +module: win_acl_inheritance +version_added: "2.0" +short_description: Disable ACL inheritance +description: + - Disable ACL inheritance and optionally converts ACE to dedicated ACE +options: + path: + description: + - Path to be used for disabling + required: true + copy: + description: + - Indicates if the inherited ACE should be copied to dedicated ACE + required: false + choices: + - no + - yes + default: no +author: Hans-Joachim Kliemeck (@h0nIg) +''' + +EXAMPLES = ''' +# Playbook example +--- +- name: Disable and copy + win_owner: + path: 'C:\\apache\\' + copy: yes + +- name: Disable + win_owner: + path: 'C:\\apache\\' + copy: no +''' From 7978afe1fde538380ed4c613cbca8b895c22efb0 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Sun, 18 Oct 2015 17:26:12 +0200 Subject: [PATCH 0861/2522] fixed documentation --- windows/win_owner.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/windows/win_owner.py b/windows/win_owner.py index 9af816abfe9..f96b10766b3 100644 --- a/windows/win_owner.py +++ b/windows/win_owner.py @@ -23,7 +23,7 @@ DOCUMENTATION = ''' --- -module: win_service_configure +module: win_owner version_added: "2.0" short_description: Set owner description: @@ -33,12 +33,10 @@ description: - Path to be used for changing owner required: true - default: null user: description: - Name to be used for changing owner required: true - default: null recurse: description: - Indicates if the owner should be changed recursively @@ -47,7 +45,7 @@ - no - yes default: no -author: Hans-Joachim Kliemeck +author: Hans-Joachim Kliemeck (@h0nIg) ''' EXAMPLES = ''' From 918f3fa3290f2917187929ffb1b0c9dd35055e0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Wallstro=CC=88m?= Date: Mon, 19 Oct 2015 16:04:17 +0200 Subject: [PATCH 0862/2522] Adds examples for win_iis_virtualdirectory --- windows/win_iis_virtualdirectory.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/windows/win_iis_virtualdirectory.py b/windows/win_iis_virtualdirectory.py index 1ccb34a65d3..66810b84071 100644 --- a/windows/win_iis_virtualdirectory.py +++ b/windows/win_iis_virtualdirectory.py @@ -22,9 +22,9 @@ --- module: win_iis_virtualdirectory version_added: "2.0" -short_description: Configures a IIS virtual directories. +short_description: Configures a virtual directory in IIS. description: - - Creates, Removes and configures a IIS Web site + - Creates, Removes and configures a virtual directory in IIS. options: name: description: @@ -37,12 +37,11 @@ - absent - present required: false - default: null + default: present site: description: - The site name under which the virtual directory is created or exists. - required: false - default: null + required: true application: description: - The application under which the virtual directory is created or exists. @@ -55,3 +54,14 @@ default: null author: Henrik Wallström ''' + +EXAMPLES = ''' +# This creates a virtual directory if it doesn't exist. +$ ansible -i hosts -m win_iis_virtualdirectory -a "name='somedirectory' site=somesite state=present physical_path=c:\\virtualdirectory\\some" host + +# This removes a virtual directory if it exists. +$ ansible -i hosts -m win_iis_virtualdirectory -a "name='somedirectory' site=somesite state=absent" host + +# This creates a virtual directory on an application if it doesn't exist. +$ ansible -i hosts -m win_iis_virtualdirectory -a "name='somedirectory' site=somesite application=someapp state=present physical_path=c:\\virtualdirectory\\some" host +''' From c893f30b2a48299da1c2b057b8f70695b6cecba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Wallstro=CC=88m?= Date: Mon, 19 Oct 2015 16:29:43 +0200 Subject: [PATCH 0863/2522] Fix: support for virtual directories in applications --- windows/win_iis_virtualdirectory.ps1 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/windows/win_iis_virtualdirectory.ps1 b/windows/win_iis_virtualdirectory.ps1 index 3f2ab692b42..44854ff09b4 100644 --- a/windows/win_iis_virtualdirectory.ps1 +++ b/windows/win_iis_virtualdirectory.ps1 @@ -66,7 +66,11 @@ $directory_path = if($application) { } # Directory info -$directory = Get-WebVirtualDirectory -Site $site -Name $name +$directory = if($application) { + Get-WebVirtualDirectory -Site $site -Name $name -Application $application +} else { + Get-WebVirtualDirectory -Site $site -Name $name +} try { # Add directory From c142bf0d447f077c6cef42bb83143b317a976620 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Mon, 19 Oct 2015 17:24:08 +0200 Subject: [PATCH 0864/2522] cloudstack: add new loadbalancer rule modules --- cloud/cloudstack/cs_loadbalancer_rule.py | 394 ++++++++++++++++++ .../cloudstack/cs_loadbalancer_rule_member.py | 369 ++++++++++++++++ 2 files changed, 763 insertions(+) create mode 100644 cloud/cloudstack/cs_loadbalancer_rule.py create mode 100644 cloud/cloudstack/cs_loadbalancer_rule_member.py diff --git a/cloud/cloudstack/cs_loadbalancer_rule.py b/cloud/cloudstack/cs_loadbalancer_rule.py new file mode 100644 index 00000000000..c662678f2bf --- /dev/null +++ b/cloud/cloudstack/cs_loadbalancer_rule.py @@ -0,0 +1,394 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2015, Darren Worrall +# (c) 2015, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: cs_loadbalancer_rule +short_description: Manages load balancer rules on Apache CloudStack based clouds. +description: + - Add, update and remove load balancer rules. +version_added: '2.0' +author: + - "Darren Worrall (@dazworrall)" + - "René Moser (@resmo)" +options: + name: + description: + - The name of the load balancer rule. + required: true + description: + description: + - The description of the load balancer rule. + required: false + default: null + algorithm: + description: + - Load balancer algorithm + - Required when using C(state=present). + required: false + choices: [ 'source', 'roundrobin', 'leastconn' ] + default: 'source' + protocol: + description: + - Protocol for the load balancer rule. + required: false + default: null + private_port: + description: + - The private port of the private ip address/virtual machine where the network traffic will be load balanced to. + - Required when using C(state=present). + - Can not be changed once the rule exists due API limitation. + required: false + default: null + public_port: + description: + - The public port from where the network traffic will be load balanced from. + - Required when using C(state=present). + - Can not be changed once the rule exists due API limitation. + required: true + default: null + ip_address: + description: + - Public IP address from where the network traffic will be load balanced from. + required: true + aliases: [ 'public_ip' ] + open_firewall: + description: + - Whether the firewall rule for public port should be created, while creating the new rule. + - Use M(cs_firewall) for managing firewall rules. + required: false + default: false + cidr: + description: + required: false + default: null + protocol: + description: + - The protocol to be used on the load balancer + - CIDR (full notation) to be used for firewall rule if required. + required: false + default: null + project: + description: + - Name of the project the load balancer IP address is related to. + required: false + default: null + state: + description: + - State of the rule. + required: true + default: 'present' + choices: [ 'present', 'absent' ] + domain: + description: + - Domain the rule is related to. + required: false + default: null + account: + description: + - Account the rule is related to. + required: false + default: null + zone: + description: + - Name of the zone in which the rule shoud be created. + - If not set, default zone is used. + required: false + default: null +extends_documentation_fragment: cloudstack +''' + +EXAMPLES = ''' +# Create a load balancer rule +- local_action: + module: cs_loadbalancer_rule + name: balance_http + public_ip: 1.2.3.4 + algorithm: leastconn + public_port: 80 + private_port: 8080 + +# update algorithm of an existing load balancer rule +- local_action: + module: cs_loadbalancer_rule + name: balance_http + public_ip: 1.2.3.4 + algorithm: roundrobin + public_port: 80 + private_port: 8080 + +# Delete a load balancer rule +- local_action: + module: cs_loadbalancer_rule + name: balance_http + public_ip: 1.2.3.4 + state: absent +''' + +RETURN = ''' +--- +id: + description: UUID of the rule. + returned: success + type: string + sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f +zone: + description: Name of zone the rule is related to. + returned: success + type: string + sample: ch-gva-2 +project: + description: Name of project the rule is related to. + returned: success + type: string + sample: Production +account: + description: Account the rule is related to. + returned: success + type: string + sample: example account +domain: + description: Domain the rule is related to. + returned: success + type: string + sample: example domain +algorithm: + description: Load balancer algorithm used. + returned: success + type: string + sample: "source" +cidr: + description: CIDR to forward traffic from. + returned: success + type: string + sample: "" +name: + description: Name of the rule. + returned: success + type: string + sample: "http-lb" +description: + description: Description of the rule. + returned: success + type: string + sample: "http load balancer rule" +protocol: + description: Protocol of the rule. + returned: success + type: string + sample: "tcp" +public_port: + description: Public port. + returned: success + type: string + sample: 80 +private_port: + description: Private IP address. + returned: success + type: string + sample: 80 +public_ip: + description: Public IP address. + returned: success + type: string + sample: "1.2.3.4" +tags: + description: List of resource tags associated with the rule. + returned: success + type: dict + sample: '[ { "key": "foo", "value": "bar" } ]' +state: + description: State of the rule. + returned: success + type: string + sample: "Add" +''' + +try: + from cs import CloudStack, CloudStackException, read_config + has_lib_cs = True +except ImportError: + has_lib_cs = False + +# import cloudstack common +from ansible.module_utils.cloudstack import * + +class AnsibleCloudStackLBRule(AnsibleCloudStack): + + def __init__(self, module): + super(AnsibleCloudStackLBRule, self).__init__(module) + self.returns = { + 'publicip': 'public_ip', + 'algorithm': 'algorithm', + 'cidrlist': 'cidr', + 'protocol': 'protocol', + } + # these values will be casted to int + self.returns_to_int = { + 'publicport': 'public_port', + 'privateport': 'private_port', + } + + + def get_rule(self, **kwargs): + rules = self.cs.listLoadBalancerRules(**kwargs) + if rules: + return rules['loadbalancerrule'][0] + + + def _get_common_args(self): + return { + 'account': self.get_account(key='name'), + 'domainid': self.get_domain(key='id'), + 'projectid': self.get_project(key='id'), + 'zoneid': self.get_zone(key='id'), + 'publicipid': self.get_ip_address(key='id'), + 'name': self.module.params.get('name'), + } + + + def present_lb_rule(self): + missing_params = [] + for required_params in [ + 'algorithm', + 'private_port', + 'public_port', + ]: + if not self.module.params.get(required_params): + missing_params.append(required_params) + if missing_params: + self.module.fail_json(msg="missing required arguments: %s" % ','.join(missing_params)) + + args = self._get_common_args() + rule = self.get_rule(**args) + if rule: + rule = self._update_lb_rule(rule) + else: + rule = self._create_lb_rule(rule) + + if rule: + rule = self.ensure_tags(resource=rule, resource_type='LoadBalancer') + return rule + + + def _create_lb_rule(self, rule): + self.result['changed'] = True + if not self.module.check_mode: + args = self._get_common_args() + args['algorithm'] = self.module.params.get('algorithm') + args['privateport'] = self.module.params.get('private_port') + args['publicport'] = self.module.params.get('public_port') + args['cidrlist'] = self.module.params.get('cidr') + args['description'] = self.module.params.get('description') + args['protocol'] = self.module.params.get('protocol') + res = self.cs.createLoadBalancerRule(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + + poll_async = self.module.params.get('poll_async') + if poll_async: + rule = self.poll_job(res, 'loadbalancer') + return rule + + + def _update_lb_rule(self, rule): + args = {} + args['id'] = rule['id'] + args['algorithm'] = self.module.params.get('algorithm') + args['description'] = self.module.params.get('description') + if self.has_changed(args, rule): + self.result['changed'] = True + if not self.module.check_mode: + res = self.cs.updateLoadBalancerRule(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + + poll_async = self.module.params.get('poll_async') + if poll_async: + rule = self.poll_job(res, 'loadbalancer') + return rule + + + def absent_lb_rule(self): + args = self._get_common_args() + rule = self.get_rule(**args) + if rule: + self.result['changed'] = True + if rule and not self.module.check_mode: + res = self.cs.deleteLoadBalancerRule(id=rule['id']) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + poll_async = self.module.params.get('poll_async') + if poll_async: + res = self._poll_job(res, 'loadbalancer') + return rule + + +def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + name = dict(required=True), + description = dict(default=None), + algorithm = dict(choices=['source', 'roundrobin', 'leastconn'], default='source'), + private_port = dict(type='int', default=None), + public_port = dict(type='int', default=None), + protocol = dict(default=None), + state = dict(choices=['present', 'absent'], default='present'), + ip_address = dict(required=True, aliases=['public_ip']), + cidr = dict(default=None), + project = dict(default=None), + open_firewall = dict(choices=BOOLEANS, default=False), + tags = dict(type='list', aliases=['tag'], default=None), + zone = dict(default=None), + domain = dict(default=None), + account = dict(default=None), + poll_async = dict(choices=BOOLEANS, default=True), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=cs_required_together(), + supports_check_mode=True + ) + + if not has_lib_cs: + module.fail_json(msg="python library cs required: pip install cs") + + try: + acs_lb_rule = AnsibleCloudStackLBRule(module) + + state = module.params.get('state') + if state in ['absent']: + rule = acs_lb_rule.absent_lb_rule() + else: + rule = acs_lb_rule.present_lb_rule() + + result = acs_lb_rule.get_result(rule) + + except CloudStackException, e: + module.fail_json(msg='CloudStackException: %s' % str(e)) + + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() diff --git a/cloud/cloudstack/cs_loadbalancer_rule_member.py b/cloud/cloudstack/cs_loadbalancer_rule_member.py new file mode 100644 index 00000000000..f0738830855 --- /dev/null +++ b/cloud/cloudstack/cs_loadbalancer_rule_member.py @@ -0,0 +1,369 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2015, Darren Worrall +# (c) 2015, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: cs_loadbalancer_rule_member +short_description: Manages load balancer rule members on Apache CloudStack based clouds. +description: + - Add and remove load balancer rule members. +version_added: '2.0' +author: + - "Darren Worrall (@dazworrall)" + - "René Moser (@resmo)" +options: + name: + description: + - The name of the load balancer rule. + required: true + ip_address: + description: + - Public IP address from where the network traffic will be load balanced from. + - Only needed to find the rule if C(name) is not unique. + required: false + default: null + aliases: [ 'public_ip' ] + vms: + description: + - List of VMs to assign to or remove from the rule. + required: true + type: list + aliases: [ 'vm' ] + state: + description: + - Should the VMs be present or absent from the rule. + required: true + default: 'present' + choices: [ 'present', 'absent' ] + project: + description: + - Name of the project the firewall rule is related to. + required: false + default: null + domain: + description: + - Domain the rule is related to. + required: false + default: null + account: + description: + - Account the rule is related to. + required: false + default: null + zone: + description: + - Name of the zone in which the rule should be located. + - If not set, default zone is used. + required: false + default: null +extends_documentation_fragment: cloudstack +''' + +EXAMPLES = ''' +# Add VMs to an exising load balancer +- local_action: + module: cs_loadbalancer_rule_member + name: balance_http + vms: + - web01 + - web02 + +# Remove a VM from an existing load balancer +- local_action: + module: cs_loadbalancer_rule_member + name: balance_http + vms: + - web01 + - web02 + state: absent + +# Rolling upgrade of hosts +- hosts: webservers + serial: 1 + pre_tasks: + - name: Remove from load balancer + local_action: + module: cs_loadbalancer_rule_member + name: balance_http + vm: "{{ ansible_hostname }}" + state: absent + tasks: + # Perform update + post_tasks: + - name: Add to load balancer + local_action: + module: cs_loadbalancer_rule_member + name: balance_http + vm: "{{ ansible_hostname }}" + state: present +''' + +RETURN = ''' +--- +id: + description: UUID of the rule. + returned: success + type: string + sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f +zone: + description: Name of zone the rule is related to. + returned: success + type: string + sample: ch-gva-2 +project: + description: Name of project the rule is related to. + returned: success + type: string + sample: Production +account: + description: Account the rule is related to. + returned: success + type: string + sample: example account +domain: + description: Domain the rule is related to. + returned: success + type: string + sample: example domain +algorithm: + description: Load balancer algorithm used. + returned: success + type: string + sample: "source" +cidr: + description: CIDR to forward traffic from. + returned: success + type: string + sample: "" +name: + description: Name of the rule. + returned: success + type: string + sample: "http-lb" +description: + description: Description of the rule. + returned: success + type: string + sample: "http load balancer rule" +protocol: + description: Protocol of the rule. + returned: success + type: string + sample: "tcp" +public_port: + description: Public port. + returned: success + type: string + sample: 80 +private_port: + description: Private IP address. + returned: success + type: string + sample: 80 +public_ip: + description: Public IP address. + returned: success + type: string + sample: "1.2.3.4" +vms: + description: Rule members. + returned: success + type: list + sample: '[ "web01", "web02" ]' +tags: + description: List of resource tags associated with the rule. + returned: success + type: dict + sample: '[ { "key": "foo", "value": "bar" } ]' +state: + description: State of the rule. + returned: success + type: string + sample: "Add" +''' + +try: + from cs import CloudStack, CloudStackException, read_config + has_lib_cs = True +except ImportError: + has_lib_cs = False + +# import cloudstack common +from ansible.module_utils.cloudstack import * + +class AnsibleCloudStackLBRuleMember(AnsibleCloudStack): + + def __init__(self, module): + super(AnsibleCloudStackLBRuleMember, self).__init__(module) + self.returns = { + 'publicip': 'public_ip', + 'algorithm': 'algorithm', + 'cidrlist': 'cidr', + 'protocol': 'protocol', + } + # these values will be casted to int + self.returns_to_int = { + 'publicport': 'public_port', + 'privateport': 'private_port', + } + + + def get_rule(self): + args = self._get_common_args() + args['name'] = self.module.params.get('name') + args['zoneid'] = self.get_zone(key='id') + if self.module.params.get('ip_address'): + args['publicipid'] = self.get_ip_address(key='id') + rules = self.cs.listLoadBalancerRules(**args) + if rules: + if len(rules['loadbalancerrule']) > 1: + self.module.fail_json(msg="More than one rule having name %s. Please pass 'ip_address' as well." % args['name']) + return rules['loadbalancerrule'][0] + return None + + + def _get_common_args(self): + return { + 'account': self.get_account(key='name'), + 'domainid': self.get_domain(key='id'), + 'projectid': self.get_project(key='id'), + } + + + def _get_members_of_rule(self, rule): + res = self.cs.listLoadBalancerRuleInstances(id=rule['id']) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + return res.get('loadbalancerruleinstance', []) + + + def _ensure_members(self, operation): + if operation not in ['add', 'remove']: + self.module.fail_json(msg="Bad operation: %s" % operation) + + rule = self.get_rule() + if not rule: + self.module.fail_json(msg="Unknown rule: %s" % self.module.params.get('name')) + + existing = {} + for vm in self._get_members_of_rule(rule=rule): + existing[vm['name']] = vm['id'] + + wanted_names = self.module.params.get('vms') + + if operation =='add': + cs_func = self.cs.assignToLoadBalancerRule + to_change = set(wanted_names) - set(existing.keys()) + else: + cs_func = self.cs.removeFromLoadBalancerRule + to_change = set(wanted_names) & set(existing.keys()) + + if not to_change: + return rule + + args = self._get_common_args() + vms = self.cs.listVirtualMachines(**args) + to_change_ids = [] + for name in to_change: + for vm in vms.get('virtualmachine', []): + if vm['name'] == name: + to_change_ids.append(vm['id']) + break + else: + self.module.fail_json(msg="Unknown VM: %s" % name) + + if to_change_ids: + self.result['changed'] = True + + if to_change_ids and not self.module.check_mode: + res = cs_func( + id = rule['id'], + virtualmachineids = to_change_ids, + ) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + poll_async = self.module.params.get('poll_async') + if poll_async: + self.poll_job(res) + rule = self.get_rule() + return rule + + + def add_members(self): + return self._ensure_members('add') + + + def remove_members(self): + return self._ensure_members('remove') + + + def get_result(self, rule): + super(AnsibleCloudStackLBRuleMember, self).get_result(rule) + if rule: + self.result['vms'] = [] + for vm in self._get_members_of_rule(rule=rule): + self.result['vms'].append(vm['name']) + return self.result + + +def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + name = dict(required=True), + ip_address = dict(default=None, aliases=['public_ip']), + vms = dict(required=True, aliases=['vm'], type='list'), + state = dict(choices=['present', 'absent'], default='present'), + zone = dict(default=None), + domain = dict(default=None), + project = dict(default=None), + account = dict(default=None), + poll_async = dict(choices=BOOLEANS, default=True), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=cs_required_together(), + supports_check_mode=True + ) + + if not has_lib_cs: + module.fail_json(msg="python library cs required: pip install cs") + + try: + acs_lb_rule_member = AnsibleCloudStackLBRuleMember(module) + + state = module.params.get('state') + if state in ['absent']: + rule = acs_lb_rule_member.remove_members() + else: + rule = acs_lb_rule_member.add_members() + + result = acs_lb_rule_member.get_result(rule) + + except CloudStackException, e: + module.fail_json(msg='CloudStackException: %s' % str(e)) + + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From 22ebf0309320b0029ae1978c4a788f65dbfb636d Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 19 Oct 2015 12:14:51 -0400 Subject: [PATCH 0865/2522] fixed undefined msg and now actually return something meaningful fixes #1127 --- notification/pushover.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notification/pushover.py b/notification/pushover.py index 0c1d6e94ab9..24d343a6809 100644 --- a/notification/pushover.py +++ b/notification/pushover.py @@ -102,11 +102,11 @@ def main(): msg_object = Pushover(module, module.params['user_key'], module.params['app_token']) try: - msg_object.run(module.params['pri'], module.params['msg']) + response = msg_object.run(module.params['pri'], module.params['msg']) except: module.fail_json(msg='Unable to send msg via pushover') - module.exit_json(msg=msg, changed=False) + module.exit_json(msg='message sent successfully: %s' % response, changed=False) # import module snippets from ansible.module_utils.basic import * From b60d6e754b5fada2e00ccbe58aec75e6abe5de9f Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 19 Oct 2015 09:13:36 -0700 Subject: [PATCH 0866/2522] Fix doc build by moving misplaced CIDR documentation --- cloud/cloudstack/cs_loadbalancer_rule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/cloudstack/cs_loadbalancer_rule.py b/cloud/cloudstack/cs_loadbalancer_rule.py index c662678f2bf..713aaad0d45 100644 --- a/cloud/cloudstack/cs_loadbalancer_rule.py +++ b/cloud/cloudstack/cs_loadbalancer_rule.py @@ -78,12 +78,12 @@ default: false cidr: description: + - CIDR (full notation) to be used for firewall rule if required. required: false default: null protocol: description: - The protocol to be used on the load balancer - - CIDR (full notation) to be used for firewall rule if required. required: false default: null project: From d9f5c275ca644ebfcdf5f6e8cc2d8d705a8f04fc Mon Sep 17 00:00:00 2001 From: pmakowski Date: Tue, 20 Oct 2015 14:05:49 +0200 Subject: [PATCH 0867/2522] no-suggests is obsolete, switch to no-recommends --- packaging/os/urpmi.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packaging/os/urpmi.py b/packaging/os/urpmi.py index 7b7aaefbd1d..d344f2e7c5c 100644 --- a/packaging/os/urpmi.py +++ b/packaging/os/urpmi.py @@ -44,9 +44,9 @@ required: false default: no choices: [ "yes", "no" ] - no-suggests: + no-recommends: description: - - Corresponds to the C(--no-suggests) option for I(urpmi). + - Corresponds to the C(--no-recommends) option for I(urpmi). required: false default: yes choices: [ "yes", "no" ] @@ -130,7 +130,7 @@ def remove_packages(module, packages): module.exit_json(changed=False, msg="package(s) already absent") -def install_packages(module, pkgspec, force=True, no_suggests=True): +def install_packages(module, pkgspec, force=True, no_recommends=True): packages = "" for package in pkgspec: @@ -138,17 +138,17 @@ def install_packages(module, pkgspec, force=True, no_suggests=True): packages += "'%s' " % package if len(packages) != 0: - if no_suggests: - no_suggests_yes = '--no-suggests' + if no_recommends: + no_recommends_yes = '--no-recommends' else: - no_suggests_yes = '' + no_recommends_yes = '' if force: force_yes = '--force' else: force_yes = '' - cmd = ("%s --auto %s --quiet %s %s" % (URPMI_PATH, force_yes, no_suggests_yes, packages)) + cmd = ("%s --auto %s --quiet %s %s" % (URPMI_PATH, force_yes, no_recommends_yes, packages)) rc, out, err = module.run_command(cmd) @@ -168,12 +168,12 @@ def install_packages(module, pkgspec, force=True, no_suggests=True): def main(): module = AnsibleModule( - argument_spec = dict( - state = dict(default='installed', choices=['installed', 'removed', 'absent', 'present']), - update_cache = dict(default=False, aliases=['update-cache'], type='bool'), - force = dict(default=True, type='bool'), - no_suggests = dict(default=True, aliases=['no-suggests'], type='bool'), - package = dict(aliases=['pkg', 'name'], required=True))) + argument_spec = dict( + state = dict(default='installed', choices=['installed', 'removed', 'absent', 'present']), + update_cache = dict(default=False, aliases=['update-cache'], type='bool'), + force = dict(default=True, type='bool'), + no_recommends = dict(default=True, aliases=['no-recommends'], type='bool'), + package = dict(aliases=['pkg', 'name'], required=True))) if not os.path.exists(URPMI_PATH): @@ -182,7 +182,7 @@ def main(): p = module.params force_yes = p['force'] - no_suggest_yes = p['no_suggests'] + no_recommends_yes = p['no_recommends'] if p['update_cache']: update_package_db(module) @@ -190,7 +190,7 @@ def main(): packages = p['package'].split(',') if p['state'] in [ 'installed', 'present' ]: - install_packages(module, packages, force_yes, no_suggest_yes) + install_packages(module, packages, force_yes, no_recommends_yes) elif p['state'] in [ 'removed', 'absent' ]: remove_packages(module, packages) From 405c3cb2c72411be395e50106b1bddd2f4fb4fda Mon Sep 17 00:00:00 2001 From: Chrrrles Paul Date: Wed, 21 Oct 2015 18:54:53 +0900 Subject: [PATCH 0868/2522] tpyo in doc string --- cloud/amazon/ec2_remote_facts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_remote_facts.py b/cloud/amazon/ec2_remote_facts.py index 3eadabfc77d..f273f17a8ce 100644 --- a/cloud/amazon/ec2_remote_facts.py +++ b/cloud/amazon/ec2_remote_facts.py @@ -55,7 +55,7 @@ # Note: These examples do not set authentication details, see the AWS Guide for details. # Basic provisioning example -- ec2_search: +- ec2_remote_facts: key: mykey value: myvalue register: servers From d96ca9c8ec51f83c5c51fcb08f0f748eec3ff11e Mon Sep 17 00:00:00 2001 From: Caduri Date: Wed, 21 Oct 2015 14:34:11 +0300 Subject: [PATCH 0869/2522] [Bug] exchange name contains chars that needs to be quoted --- messaging/rabbitmq_exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messaging/rabbitmq_exchange.py b/messaging/rabbitmq_exchange.py index fb74298879b..728186385cb 100644 --- a/messaging/rabbitmq_exchange.py +++ b/messaging/rabbitmq_exchange.py @@ -133,7 +133,7 @@ def main(): module.params['login_host'], module.params['login_port'], urllib.quote(module.params['vhost'],''), - module.params['name'] + urllib.quote(module.params['name'],'') ) # Check if exchange already exists From 2a7b835f7752d21f3f8448dec71a2dff684bf43e Mon Sep 17 00:00:00 2001 From: Caduri Date: Wed, 21 Oct 2015 14:36:23 +0300 Subject: [PATCH 0870/2522] [Bug] exchange name contains chars that needs to be quoted --- messaging/rabbitmq_binding.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/messaging/rabbitmq_binding.py b/messaging/rabbitmq_binding.py index fc69f490fad..ad7fa151461 100644 --- a/messaging/rabbitmq_binding.py +++ b/messaging/rabbitmq_binding.py @@ -131,9 +131,9 @@ def main(): module.params['login_host'], module.params['login_port'], urllib.quote(module.params['vhost'],''), - module.params['name'], + urllib.quote(module.params['name'],''), dest_type, - module.params['destination'], + urllib.quote(module.params['destination'],''), urllib.quote(module.params['routing_key'],'') ) @@ -173,9 +173,9 @@ def main(): module.params['login_host'], module.params['login_port'], urllib.quote(module.params['vhost'],''), - module.params['name'], + urllib.quote(module.params['name'],''), dest_type, - module.params['destination'] + urllib.quote(module.params['destination'],'') ) r = requests.post( From 92dc7cd65c2b5ad9da2a437f8e14415a95b27314 Mon Sep 17 00:00:00 2001 From: Ramunas Dronga Date: Wed, 21 Oct 2015 16:20:53 +0300 Subject: [PATCH 0871/2522] allow specify arguments for composer --- packaging/language/composer.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/packaging/language/composer.py b/packaging/language/composer.py index 1ef93e736fc..fdb84896092 100644 --- a/packaging/language/composer.py +++ b/packaging/language/composer.py @@ -36,6 +36,11 @@ - Composer command like "install", "update" and so on required: false default: install + arguments: + description: + - Composer arguments like required package, version and so on + required: false + default: null working_dir: description: - Directory of your project ( see --working-dir ) @@ -102,6 +107,17 @@ EXAMPLES = ''' # Downloads and installs all the libs and dependencies outlined in the /path/to/project/composer.lock - composer: command=install working_dir=/path/to/project + +- composer: + command: "require my/package" + working_dir: "/path/to/project" + +# Clone project and install with all dependencies +- composer: + command: "create-project" + arguments: "package/package /path/to/project ~1.0" + working_dir: "/path/to/project" + prefer_dist: "yes" ''' import os @@ -116,6 +132,8 @@ def parse_out(string): return re.sub("\s+", " ", string).strip() def has_changed(string): + if string == "": + return False return "Nothing to install or update" not in string def get_available_options(module, command='install'): @@ -128,16 +146,17 @@ def get_available_options(module, command='install'): command_help_json = json.loads(out) return command_help_json['definition']['options'] -def composer_command(module, command, options=[]): +def composer_command(module, command, arguments = "", options=[]): php_path = module.get_bin_path("php", True, ["/usr/local/bin"]) composer_path = module.get_bin_path("composer", True, ["/usr/local/bin"]) - cmd = "%s %s %s %s" % (php_path, composer_path, command, " ".join(options)) + cmd = "%s %s %s %s %s" % (php_path, composer_path, command, " ".join(options), arguments) return module.run_command(cmd) def main(): module = AnsibleModule( argument_spec = dict( command = dict(default="install", type="str", required=False), + arguments = dict(default="", type="str", required=False), working_dir = dict(aliases=["working-dir"], required=True), prefer_source = dict(default="no", type="bool", aliases=["prefer-source"]), prefer_dist = dict(default="no", type="bool", aliases=["prefer-dist"]), @@ -152,6 +171,7 @@ def main(): # Get composer command with fallback to default command = module.params['command'] + arguments = module.params['arguments'] available_options = get_available_options(module=module, command=command) options = [] @@ -188,7 +208,7 @@ def main(): if module.check_mode: options.append('--dry-run') - rc, out, err = composer_command(module, command, options) + rc, out, err = composer_command(module, command, arguments, options) if rc != 0: output = parse_out(err) From 461a479cbd10fd4208976a21fef20fac83590343 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Wed, 21 Oct 2015 17:52:48 +0200 Subject: [PATCH 0872/2522] added dependency and user settings --- windows/win_nssm.ps1 | 120 +++++++++++++++++++++++++++++++++++++++++++ windows/win_nssm.py | 31 ++++++++++- 2 files changed, 150 insertions(+), 1 deletion(-) diff --git a/windows/win_nssm.ps1 b/windows/win_nssm.ps1 index 841bc3aa3fd..89fcfa5e407 100644 --- a/windows/win_nssm.ps1 +++ b/windows/win_nssm.ps1 @@ -2,6 +2,8 @@ # This file is part of Ansible # # Copyright 2015, George Frank +# Copyright 2015, Adam Keech +# Copyright 2015, Hans-Joachim Kliemeck # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -86,6 +88,33 @@ Else $stderrFile = $null } +If ($params.dependencies) +{ + $dependencies = $params.dependencies +} +Else +{ + $dependencies = $null +} + +If ($params.user) +{ + $user = $params.user +} +Else +{ + $user = $null +} + +If ($params.password) +{ + $password = $params.password +} +Else +{ + $password = $null +} + Function Service-Exists { [CmdletBinding()] @@ -365,6 +394,89 @@ Function Nssm-Set-Ouput-Files $results = invoke-expression $cmd } +Function Nssm-Update-Credentials +{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string]$name, + [Parameter(Mandatory=$false)] + [string]$user, + [Parameter(Mandatory=$false)] + [string]$password + ) + + $cmd = "nssm get ""$name"" ObjectName" + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "nssm_error_cmd" $cmd + Set-Attr $result "nssm_error_log" "$results" + Throw "Error updating credentials for service ""$name""" + } + + if ($user -ne $null) { + If ($password -eq $null) { + Throw "User without password is informed for service ""$name""" + } + + $fullUser = $user + If (-Not($user -contains "@") -And ($user.Split("\").count -eq 1)) { + $fullUser = ".\" + $user + } + + If ($results -ne $fullUser) { + $cmd = "nssm set ""$name"" ObjectName $fullUser $password" + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "nssm_error_cmd" $cmd + Set-Attr $result "nssm_error_log" "$results" + Throw "Error updating credentials for service ""$name""" + } + + $result.changed = $true + } + } +} + +Function Nssm-Update-Dependencies +{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string]$name, + [Parameter(Mandatory=$false)] + [string]$dependencies + ) + + $cmd = "nssm get ""$name"" DependOnService" + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "nssm_error_cmd" $cmd + Set-Attr $result "nssm_error_log" "$results" + Throw "Error updating dependencies for service ""$name""" + } + + If (($dependencies -ne $null) -And ($results.Tolower() -ne $dependencies.Tolower())) { + $cmd = "nssm set ""$name"" DependOnService $dependencies" + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "nssm_error_cmd" $cmd + Set-Attr $result "nssm_error_log" "$results" + Throw "Error updating dependencies for service ""$name""" + } + + $result.changed = $true + } +} + Function Nssm-Get-Status { [CmdletBinding()] @@ -508,23 +620,31 @@ Try Nssm-Install -name $name -application $application Nssm-Update-AppParameters -name $name -appParameters $appParameters Nssm-Set-Ouput-Files -name $name -stdout $stdoutFile -stderr $stderrFile + Nssm-Update-Dependencies -name $name -dependencies $dependencies + Nssm-Update-Credentials -name $name -user $user -password $password } "started" { Nssm-Install -name $name -application $application Nssm-Update-AppParameters -name $name -appParameters $appParameters Nssm-Set-Ouput-Files -name $name -stdout $stdoutFile -stderr $stderrFile + Nssm-Update-Dependencies -name $name -dependencies $dependencies + Nssm-Update-Credentials -name $name -user $user -password $password Nssm-Start -name $name } "stopped" { Nssm-Install -name $name -application $application Nssm-Update-AppParameters -name $name -appParameters $appParameters Nssm-Set-Ouput-Files -name $name -stdout $stdoutFile -stderr $stderrFile + Nssm-Update-Dependencies -name $name -dependencies $dependencies + Nssm-Update-Credentials -name $name -user $user -password $password Nssm-Stop -name $name } "restarted" { Nssm-Install -name $name -application $application Nssm-Update-AppParameters -name $name -appParameters $appParameters Nssm-Set-Ouput-Files -name $name -stdout $stdoutFile -stderr $stderrFile + Nssm-Update-Dependencies -name $name -dependencies $dependencies + Nssm-Update-Credentials -name $name -user $user -password $password Nssm-Restart -name $name } } diff --git a/windows/win_nssm.py b/windows/win_nssm.py index b3925f0fd50..94d4079e786 100644 --- a/windows/win_nssm.py +++ b/windows/win_nssm.py @@ -71,16 +71,32 @@ - Parameters to be passed to the application when it starts required: false default: null + dependencies: + description: + - Service dependencies that has to be started to trigger startup, separated by comma. + required: false + default: null + user: + description: + - User to be used for service startup + required: false + default: null + password: + description: + - Password to be used for service startup + required: false + default: null author: - "Adam Keech (@smadam813)" - "George Frank (@georgefrank)" + - "Hans-Joachim Kliemeck (@h0nIg)" ''' EXAMPLES = ''' # Install and start the foo service - win_nssm: name: foo - application: C:\windowsk\\foo.exe + application: C:\windows\\foo.exe # Install and start the foo service with a key-value pair argument # This will yield the following command: C:\windows\\foo.exe bar "true" @@ -122,6 +138,19 @@ stdout_file: C:\windows\\foo.log stderr_file: C:\windows\\foo.log +# Install and start the foo service, but wait for dependencies tcpip and adf +- win_nssm: + name: foo + application: C:\windows\\foo.exe + dependencies: 'adf,tcpip' + +# Install and start the foo service with dedicated user +- win_nssm: + name: foo + application: C:\windows\\foo.exe + user: foouser + password: secret + # Remove the foo service - win_nssm: name: foo From e7fd5dcde06d04fc387abb0b2f7a5801f04543d2 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Wed, 21 Oct 2015 17:52:59 +0200 Subject: [PATCH 0873/2522] strict variables fix --- windows/win_nssm.ps1 | 94 +++++--------------------------------------- 1 file changed, 10 insertions(+), 84 deletions(-) diff --git a/windows/win_nssm.ps1 b/windows/win_nssm.ps1 index 89fcfa5e407..915a3889e17 100644 --- a/windows/win_nssm.ps1 +++ b/windows/win_nssm.ps1 @@ -24,96 +24,22 @@ $ErrorActionPreference = "Stop" # POWERSHELL_COMMON $params = Parse-Args $args; + $result = New-Object PSObject; Set-Attr $result "changed" $false; +$name = Get-Attr $params "name" -failifempty $true +$state = Get-Attr $params "state" "present" -validateSet "present", "absent", "started", "stopped", "restarted" -resultobj $result -If ($params.name) -{ - $name = $params.name -} -Else -{ - Fail-Json $result "missing required argument: name" -} - -If ($params.state) -{ - $state = $params.state.ToString().ToLower() - $validStates = "present", "absent", "started", "stopped", "restarted" - - If ($validStates -notcontains $state) - { - Fail-Json $result "state is $state; must be one of: $validStates" - } -} -else -{ - $state = "present" -} - -If ($params.application) -{ - $application = $params.application -} -Else -{ - $application = $null -} - -If ($params.app_parameters) -{ - $appParameters = $params.app_parameters -} -Else -{ - $appParameters = $null -} - -If ($params.stdout_file) -{ - $stdoutFile = $params.stdout_file -} -Else -{ - $stdoutFile = $null -} - -If ($params.stderr_file) -{ - $stderrFile = $params.stderr_file -} -Else -{ - $stderrFile = $null -} +$application = Get-Attr $params "application" $null +$appParameters = Get-Attr $params "app_parameters" $null -If ($params.dependencies) -{ - $dependencies = $params.dependencies -} -Else -{ - $dependencies = $null -} +$stdoutFile = Get-Attr $params "stdout_file" $null +$stderrFile = Get-Attr $params "stderr_file" $null +$dependencies = Get-Attr $params "dependencies" $null -If ($params.user) -{ - $user = $params.user -} -Else -{ - $user = $null -} - -If ($params.password) -{ - $password = $params.password -} -Else -{ - $password = $null -} +$user = Get-Attr $params "user" $null +$password = Get-Attr $params "password" $null Function Service-Exists { From d3f83ee9a7d40eea3e01adea9b7d16bc0d70c124 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Wed, 21 Oct 2015 18:16:49 +0200 Subject: [PATCH 0874/2522] added start mode feature --- windows/win_nssm.ps1 | 42 ++++++++++++++++++++++++++++++++++++++++++ windows/win_nssm.py | 20 ++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/windows/win_nssm.ps1 b/windows/win_nssm.ps1 index 915a3889e17..99a61d12a90 100644 --- a/windows/win_nssm.ps1 +++ b/windows/win_nssm.ps1 @@ -33,6 +33,7 @@ $state = Get-Attr $params "state" "present" -validateSet "present", "absent", "s $application = Get-Attr $params "application" $null $appParameters = Get-Attr $params "app_parameters" $null +$startMode = Get-Attr $params "start_mode" "auto" -validateSet "auto", "manual", "disabled" -resultobj $result $stdoutFile = Get-Attr $params "stdout_file" $null $stderrFile = Get-Attr $params "stderr_file" $null @@ -403,6 +404,43 @@ Function Nssm-Update-Dependencies } } +Function Nssm-Update-StartMode +{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string]$name, + [Parameter(Mandatory=$true)] + [string]$mode + ) + + $cmd = "nssm get ""$name"" Start" + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "nssm_error_cmd" $cmd + Set-Attr $result "nssm_error_log" "$results" + Throw "Error updating start mode for service ""$name""" + } + + $modes=@{"auto" = "SERVICE_AUTO_START"; "manual" = "SERVICE_DEMAND_START"; "disabled" = "SERVICE_DISABLED"} + $mappedMode = $modes.$mode + if ($mappedMode -ne $results) { + $cmd = "nssm set ""$name"" Start $mappedMode" + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "nssm_error_cmd" $cmd + Set-Attr $result "nssm_error_log" "$results" + Throw "Error updating start mode for service ""$name""" + } + + $result.changed = $true + } +} + Function Nssm-Get-Status { [CmdletBinding()] @@ -548,6 +586,7 @@ Try Nssm-Set-Ouput-Files -name $name -stdout $stdoutFile -stderr $stderrFile Nssm-Update-Dependencies -name $name -dependencies $dependencies Nssm-Update-Credentials -name $name -user $user -password $password + Nssm-Update-StartMode -name $name -mode $startMode } "started" { Nssm-Install -name $name -application $application @@ -555,6 +594,7 @@ Try Nssm-Set-Ouput-Files -name $name -stdout $stdoutFile -stderr $stderrFile Nssm-Update-Dependencies -name $name -dependencies $dependencies Nssm-Update-Credentials -name $name -user $user -password $password + Nssm-Update-StartMode -name $name -mode $startMode Nssm-Start -name $name } "stopped" { @@ -563,6 +603,7 @@ Try Nssm-Set-Ouput-Files -name $name -stdout $stdoutFile -stderr $stderrFile Nssm-Update-Dependencies -name $name -dependencies $dependencies Nssm-Update-Credentials -name $name -user $user -password $password + Nssm-Update-StartMode -name $name -mode $startMode Nssm-Stop -name $name } "restarted" { @@ -571,6 +612,7 @@ Try Nssm-Set-Ouput-Files -name $name -stdout $stdoutFile -stderr $stderrFile Nssm-Update-Dependencies -name $name -dependencies $dependencies Nssm-Update-Credentials -name $name -user $user -password $password + Nssm-Update-StartMode -name $name -mode $startMode Nssm-Restart -name $name } } diff --git a/windows/win_nssm.py b/windows/win_nssm.py index 94d4079e786..1f42f21f45a 100644 --- a/windows/win_nssm.py +++ b/windows/win_nssm.py @@ -86,6 +86,19 @@ - Password to be used for service startup required: false default: null + password: + description: + - Password to be used for service startup + required: false + default: null + start_mode: + description: + - If C(auto) is selected, the service will start at bootup. C(manual) means that the service will start only when another service needs it. C(disabled) means that the service will stay off, regardless if it is needed or not. + required: false + choices: + - auto + - manual + - disabled author: - "Adam Keech (@smadam813)" - "George Frank (@georgefrank)" @@ -151,6 +164,13 @@ user: foouser password: secret +# Install the foo service but do not start it automatically +- win_nssm: + name: foo + application: C:\windows\\foo.exe + state: present + start_mode: manual + # Remove the foo service - win_nssm: name: foo From e8fe9167d7b7d1d7a9c05a0efdf76ca023ca0815 Mon Sep 17 00:00:00 2001 From: Siva Popuri Date: Wed, 21 Oct 2015 12:36:51 -0500 Subject: [PATCH 0875/2522] Added RETURN doc string for all modules and few minor enhancements. --- cloud/centurylink/clc_aa_policy.py | 35 +++ cloud/centurylink/clc_alert_policy.py | 46 ++++ cloud/centurylink/clc_blueprint_package.py | 23 +- cloud/centurylink/clc_firewall_policy.py | 46 ++++ cloud/centurylink/clc_group.py | 139 ++++++++++-- cloud/centurylink/clc_loadbalancer.py | 46 +++- cloud/centurylink/clc_publicip.py | 50 +++-- cloud/centurylink/clc_server.py | 246 +++++++++++++++++++-- cloud/centurylink/clc_server_snapshot.py | 29 ++- 9 files changed, 601 insertions(+), 59 deletions(-) diff --git a/cloud/centurylink/clc_aa_policy.py b/cloud/centurylink/clc_aa_policy.py index e092af13a07..8f4567ea1ac 100644 --- a/cloud/centurylink/clc_aa_policy.py +++ b/cloud/centurylink/clc_aa_policy.py @@ -98,6 +98,41 @@ debug: var=policy ''' +RETURN = ''' +changed: + description: A flag indicating if any change was made or not + returned: success + type: boolean + sample: True +policy: + description: The anti affinity policy information + returned: success + type: dict + sample: + { + "id":"1a28dd0988984d87b9cd61fa8da15424", + "name":"test_aa_policy", + "location":"UC1", + "links":[ + { + "rel":"self", + "href":"/v2/antiAffinityPolicies/wfad/1a28dd0988984d87b9cd61fa8da15424", + "verbs":[ + "GET", + "DELETE", + "PUT" + ] + }, + { + "rel":"location", + "href":"/v2/datacenters/wfad/UC1", + "id":"uc1", + "name":"UC1 - US West (Santa Clara)" + } + ] + } +''' + __version__ = '${version}' from distutils.version import LooseVersion diff --git a/cloud/centurylink/clc_alert_policy.py b/cloud/centurylink/clc_alert_policy.py index ef5498a9ef6..dcb9d47a718 100644 --- a/cloud/centurylink/clc_alert_policy.py +++ b/cloud/centurylink/clc_alert_policy.py @@ -130,6 +130,52 @@ debug: var=policy ''' +RETURN = ''' +changed: + description: A flag indicating if any change was made or not + returned: success + type: boolean + sample: True +policy: + description: The alert policy information + returned: success + type: dict + sample: + { + "actions": [ + { + "action": "email", + "settings": { + "recipients": [ + "user1@domain.com", + "user1@domain.com" + ] + } + } + ], + "id": "ba54ac54a60d4a4f1ed6d48c1ce240a7", + "links": [ + { + "href": "/v2/alertPolicies/alias/ba54ac54a60d4a4fb1d6d48c1ce240a7", + "rel": "self", + "verbs": [ + "GET", + "DELETE", + "PUT" + ] + } + ], + "name": "test_alert", + "triggers": [ + { + "duration": "00:05:00", + "metric": "disk", + "threshold": 80.0 + } + ] + } +''' + __version__ = '${version}' from distutils.version import LooseVersion diff --git a/cloud/centurylink/clc_blueprint_package.py b/cloud/centurylink/clc_blueprint_package.py index 3548944210d..f39d8c03c7f 100644 --- a/cloud/centurylink/clc_blueprint_package.py +++ b/cloud/centurylink/clc_blueprint_package.py @@ -73,12 +73,29 @@ - name: Deploy package clc_blueprint_package: server_ids: - - UC1WFSDANS01 - - UC1WFSDANS02 + - UC1TEST-SERVER1 + - UC1TEST-SERVER2 package_id: 77abb844-579d-478d-3955-c69ab4a7ba1a package_params: {} ''' +RETURN = ''' +changed: + description: A flag indicating if any change was made or not + returned: success + type: boolean + sample: True +server_ids: + description: The list of server ids that are changed + returned: success + type: list + sample: + [ + "UC1TEST-SERVER1", + "UC1TEST-SERVER2" + ] +''' + __version__ = '${version}' from distutils.version import LooseVersion @@ -203,7 +220,7 @@ def clc_install_package(self, server, package_id, package_params): parameters=package_params) except CLCException as ex: self.module.fail_json(msg='Failed to install package : {0} to server {1}. {2}'.format( - package_id, server.id, ex.response_text + package_id, server.id, ex.message )) return result diff --git a/cloud/centurylink/clc_firewall_policy.py b/cloud/centurylink/clc_firewall_policy.py index b851ea48a44..83d75cbdfc0 100644 --- a/cloud/centurylink/clc_firewall_policy.py +++ b/cloud/centurylink/clc_firewall_policy.py @@ -128,6 +128,52 @@ firewall_policy_id: 'c62105233d7a4231bd2e91b9c791e43e1' ''' +RETURN = ''' +changed: + description: A flag indicating if any change was made or not + returned: success + type: boolean + sample: True +firewall_policy_id + description: The fire wall policy id + returned: success + type: string + sample: fc36f1bfd47242e488a9c44346438c05 +firewall_policy: + description: The fire wall policy information + returned: success + type: dict + sample: + { + "destination":[ + "10.1.1.0/24", + "10.2.2.0/24" + ], + "destinationAccount":"wfad", + "enabled":true, + "id":"fc36f1bfd47242e488a9c44346438c05", + "links":[ + { + "href":"http://api.ctl.io/v2-experimental/firewallPolicies/wfad/uc1/fc36f1bfd47242e488a9c44346438c05", + "rel":"self", + "verbs":[ + "GET", + "PUT", + "DELETE" + ] + } + ], + "ports":[ + "any" + ], + "source":[ + "10.1.1.0/24", + "10.2.2.0/24" + ], + "status":"active" + } +''' + __version__ = '${version}' import urlparse diff --git a/cloud/centurylink/clc_group.py b/cloud/centurylink/clc_group.py index e6e7267f05e..7564168978f 100644 --- a/cloud/centurylink/clc_group.py +++ b/cloud/centurylink/clc_group.py @@ -62,7 +62,7 @@ - To use this module, it is required to set the below environment variables which enables access to the Centurylink Cloud - CLC_V2_API_USERNAME, the account login id for the centurylink cloud - - CLC_V2_API_PASSWORD, the account passwod for the centurylink cloud + - CLC_V2_API_PASSWORD, the account password for the centurylink cloud - Alternatively, the module accepts the API token and account alias. The API token can be generated using the CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login - CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login @@ -110,6 +110,110 @@ ''' +RETURN = ''' +changed: + description: A flag indicating if any change was made or not + returned: success + type: boolean + sample: True +group: + description: The group information + returned: success + type: dict + sample: + { + "changeInfo":{ + "createdBy":"service.wfad", + "createdDate":"2015-07-29T18:52:47Z", + "modifiedBy":"service.wfad", + "modifiedDate":"2015-07-29T18:52:47Z" + }, + "customFields":[ + + ], + "description":"test group", + "groups":[ + + ], + "id":"bb5f12a3c6044ae4ad0a03e73ae12cd1", + "links":[ + { + "href":"/v2/groups/wfad", + "rel":"createGroup", + "verbs":[ + "POST" + ] + }, + { + "href":"/v2/servers/wfad", + "rel":"createServer", + "verbs":[ + "POST" + ] + }, + { + "href":"/v2/groups/wfad/bb5f12a3c6044ae4ad0a03e73ae12cd1", + "rel":"self", + "verbs":[ + "GET", + "PATCH", + "DELETE" + ] + }, + { + "href":"/v2/groups/wfad/086ac1dfe0b6411989e8d1b77c4065f0", + "id":"086ac1dfe0b6411989e8d1b77c4065f0", + "rel":"parentGroup" + }, + { + "href":"/v2/groups/wfad/bb5f12a3c6044ae4ad0a03e73ae12cd1/defaults", + "rel":"defaults", + "verbs":[ + "GET", + "POST" + ] + }, + { + "href":"/v2/groups/wfad/bb5f12a3c6044ae4ad0a03e73ae12cd1/billing", + "rel":"billing" + }, + { + "href":"/v2/groups/wfad/bb5f12a3c6044ae4ad0a03e73ae12cd1/archive", + "rel":"archiveGroupAction" + }, + { + "href":"/v2/groups/wfad/bb5f12a3c6044ae4ad0a03e73ae12cd1/statistics", + "rel":"statistics" + }, + { + "href":"/v2/groups/wfad/bb5f12a3c6044ae4ad0a03e73ae12cd1/upcomingScheduledActivities", + "rel":"upcomingScheduledActivities" + }, + { + "href":"/v2/groups/wfad/bb5f12a3c6044ae4ad0a03e73ae12cd1/horizontalAutoscalePolicy", + "rel":"horizontalAutoscalePolicyMapping", + "verbs":[ + "GET", + "PUT", + "DELETE" + ] + }, + { + "href":"/v2/groups/wfad/bb5f12a3c6044ae4ad0a03e73ae12cd1/scheduledActivities", + "rel":"scheduledActivities", + "verbs":[ + "GET", + "POST" + ] + } + ], + "locationId":"UC1", + "name":"test group", + "status":"active", + "type":"default" + } +''' + __version__ = '${version}' from distutils.version import LooseVersion @@ -178,13 +282,16 @@ def process_request(self): if state == "absent": changed, group, requests = self._ensure_group_is_absent( group_name=group_name, parent_name=parent_name) - + if requests: + self._wait_for_requests_to_complete(requests) else: - changed, group, requests = self._ensure_group_is_present( + changed, group = self._ensure_group_is_present( group_name=group_name, parent_name=parent_name, group_description=group_description) - if requests: - self._wait_for_requests_to_complete(requests) - self.module.exit_json(changed=changed, group=group_name) + try: + group = group.data + except AttributeError: + group = group_name + self.module.exit_json(changed=changed, group=group) @staticmethod def _define_module_argument_spec(): @@ -238,14 +345,16 @@ def _ensure_group_is_absent(self, group_name, parent_name): :return: changed, group """ changed = False - requests = [] + group = [] + results = [] if self._group_exists(group_name=group_name, parent_name=parent_name): if not self.module.check_mode: - request = self._delete_group(group_name) - requests.append(request) + group.append(group_name) + result = self._delete_group(group_name) + results.append(result) changed = True - return changed, group_name, requests + return changed, group, results def _delete_group(self, group_name): """ @@ -281,6 +390,7 @@ def _ensure_group_is_present( parent = parent_name if parent_name is not None else self.root_group.name description = group_description changed = False + group = group_name parent_exists = self._group_exists(group_name=parent, parent_name=None) child_exists = self._group_exists( @@ -292,8 +402,8 @@ def _ensure_group_is_present( changed = False elif parent_exists and not child_exists: if not self.module.check_mode: - self._create_group( - group=group_name, + group = self._create_group( + group=group, parent=parent, description=description) changed = True @@ -303,7 +413,7 @@ def _ensure_group_is_present( parent + " does not exist") - return changed, group_name, None + return changed, group def _create_group(self, group, parent, description): """ @@ -319,8 +429,7 @@ def _create_group(self, group, parent, description): response = parent.Create(name=group, description=description) except CLCException, ex: self.module.fail_json(msg='Failed to create group :{0}. {1}'.format( - group, ex.response_text - )) + group, ex.response_text)) return response def _group_exists(self, group_name, parent_name): diff --git a/cloud/centurylink/clc_loadbalancer.py b/cloud/centurylink/clc_loadbalancer.py index 5847c5b1c00..076146c5c8e 100644 --- a/cloud/centurylink/clc_loadbalancer.py +++ b/cloud/centurylink/clc_loadbalancer.py @@ -172,6 +172,48 @@ - Users can set CLC_V2_API_URL to specify an endpoint for pointing to a different CLC environment. ''' +RETURN = ''' +changed: + description: A flag indicating if any change was made or not + returned: success + type: boolean + sample: True +loadbalancer: + description: The load balancer result object from CLC + returned: success + type: dict + sample: + { + "description":"test-lb", + "id":"ab5b18cb81e94ab9925b61d1ca043fb5", + "ipAddress":"66.150.174.197", + "links":[ + { + "href":"/v2/sharedLoadBalancers/wfad/wa1/ab5b18cb81e94ab9925b61d1ca043fb5", + "rel":"self", + "verbs":[ + "GET", + "PUT", + "DELETE" + ] + }, + { + "href":"/v2/sharedLoadBalancers/wfad/wa1/ab5b18cb81e94ab9925b61d1ca043fb5/pools", + "rel":"pools", + "verbs":[ + "GET", + "POST" + ] + } + ], + "name":"test-lb", + "pools":[ + + ], + "status":"enabled" + } +''' + __version__ = '${version}' from time import sleep @@ -827,8 +869,8 @@ def define_argument_spec(): argument_spec = dict( name=dict(required=True), description=dict(default=None), - location=dict(required=True, default=None), - alias=dict(required=True, default=None), + location=dict(required=True), + alias=dict(required=True), port=dict(choices=[80, 443]), method=dict(choices=['leastConnection', 'roundRobin']), persistence=dict(choices=['standard', 'sticky']), diff --git a/cloud/centurylink/clc_publicip.py b/cloud/centurylink/clc_publicip.py index 9879b61fd49..ab1af0e00ca 100644 --- a/cloud/centurylink/clc_publicip.py +++ b/cloud/centurylink/clc_publicip.py @@ -27,22 +27,23 @@ version_added: "2.0" options: protocol: - descirption: + description: - The protocol that the public IP will listen for. default: TCP choices: ['TCP', 'UDP', 'ICMP'] required: False ports: description: - - A list of ports to expose. - required: True + - A list of ports to expose. This is required when state is 'present' + required: False + default: None server_ids: description: - A list of servers to create public ips on. required: True state: description: - - Determine wheteher to create or delete public IPs. If present module will not create a second public ip if one + - Determine whether to create or delete public IPs. If present module will not create a second public ip if one already exists. default: present choices: ['present', 'absent'] @@ -61,7 +62,7 @@ - To use this module, it is required to set the below environment variables which enables access to the Centurylink Cloud - CLC_V2_API_USERNAME, the account login id for the centurylink cloud - - CLC_V2_API_PASSWORD, the account passwod for the centurylink cloud + - CLC_V2_API_PASSWORD, the account password for the centurylink cloud - Alternatively, the module accepts the API token and account alias. The API token can be generated using the CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login - CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login @@ -83,8 +84,8 @@ ports: - 80 server_ids: - - UC1ACCTSRVR01 - - UC1ACCTSRVR02 + - UC1TEST-SVR01 + - UC1TEST-SVR02 state: present register: clc @@ -99,8 +100,8 @@ - name: Create Public IP For Servers clc_publicip: server_ids: - - UC1ACCTSRVR01 - - UC1ACCTSRVR02 + - UC1TEST-SVR01 + - UC1TEST-SVR02 state: absent register: clc @@ -108,6 +109,23 @@ debug: var=clc ''' +RETURN = ''' +changed: + description: A flag indicating if any change was made or not + returned: success + type: boolean + sample: True +server_ids: + description: The list of server ids that are changed + returned: success + type: list + sample: + [ + "UC1TEST-SVR01", + "UC1TEST-SVR02" + ] +''' + __version__ = '${version}' from distutils.version import LooseVersion @@ -136,7 +154,6 @@ class ClcPublicIp(object): clc = clc_sdk module = None - group_dict = {} def __init__(self, module): """ @@ -158,7 +175,6 @@ def __init__(self, module): def process_request(self): """ Process the request - Main Code Path - :param params: dictionary of module parameters :return: Returns with either an exit_json or fail_json """ self._set_clc_credentials_from_env() @@ -167,21 +183,18 @@ def process_request(self): ports = params['ports'] protocol = params['protocol'] state = params['state'] - requests = [] - chagned_server_ids = [] - changed = False if state == 'present': - changed, chagned_server_ids, requests = self.ensure_public_ip_present( + changed, changed_server_ids, requests = self.ensure_public_ip_present( server_ids=server_ids, protocol=protocol, ports=ports) elif state == 'absent': - changed, chagned_server_ids, requests = self.ensure_public_ip_absent( + changed, changed_server_ids, requests = self.ensure_public_ip_absent( server_ids=server_ids) else: return self.module.fail_json(msg="Unknown State: " + state) self._wait_for_requests_to_complete(requests) return self.module.exit_json(changed=changed, - server_ids=chagned_server_ids) + server_ids=changed_server_ids) @staticmethod def _define_module_argument_spec(): @@ -192,7 +205,7 @@ def _define_module_argument_spec(): argument_spec = dict( server_ids=dict(type='list', required=True), protocol=dict(default='TCP', choices=['TCP', 'UDP', 'ICMP']), - ports=dict(type='list', required=True), + ports=dict(type='list'), wait=dict(type='bool', default=True), state=dict(default='present', choices=['present', 'absent']), ) @@ -265,6 +278,7 @@ def ensure_public_ip_absent(self, server_ids): return changed, changed_server_ids, results def _remove_publicip_from_server(self, server): + result = None try: for ip_address in server.PublicIPs().public_ips: result = ip_address.Delete() diff --git a/cloud/centurylink/clc_server.py b/cloud/centurylink/clc_server.py index d2329465f4a..3202f1065fd 100644 --- a/cloud/centurylink/clc_server.py +++ b/cloud/centurylink/clc_server.py @@ -193,7 +193,7 @@ - The template to use for server creation. Will search for a template if a partial string is provided. This is required when state is 'present' default: None - required: false + required: False ttl: description: - The time to live for the server in seconds. The server will be deleted when this time expires. @@ -204,7 +204,20 @@ - The type of server to create. default: 'standard' required: False - choices: ['standard', 'hyperscale'] + choices: ['standard', 'hyperscale', 'bareMetal'] + configuration_id: + description: + - Only required for bare metal servers. + Specifies the identifier for the specific configuration type of bare metal server to deploy. + default: None + required: False + os_type: + description: + - Only required for bare metal servers. + Specifies the OS to provision with the bare metal server. + default: None + required: False + choices: ['redHat6_64Bit', 'centOS6_64Bit', 'windows2012R2Standard_64Bit', 'ubuntu14_64Bit'] wait: description: - Whether to wait for the provisioning tasks to finish before returning. @@ -248,20 +261,203 @@ - name: Stop a Server clc_server: - server_ids: ['UC1ACCTTEST01'] + server_ids: ['UC1ACCT-TEST01'] state: stopped - name: Start a Server clc_server: - server_ids: ['UC1ACCTTEST01'] + server_ids: ['UC1ACCT-TEST01'] state: started - name: Delete a Server clc_server: - server_ids: ['UC1ACCTTEST01'] + server_ids: ['UC1ACCT-TEST01'] state: absent ''' +RETURN = ''' +changed: + description: A flag indicating if any change was made or not + returned: success + type: boolean + sample: True +server_ids: + description: The list of server ids that are created + returned: success + type: list + sample: + [ + "UC1TEST-SVR01", + "UC1TEST-SVR02" + ] +partially_created_server_ids: + description: The list of server ids that are partially created + returned: success + type: list + sample: + [ + "UC1TEST-SVR01", + "UC1TEST-SVR02" + ] +servers: + description: The list of server objects returned from CLC + returned: success + type: list + sample: + [ + { + "changeInfo":{ + "createdBy":"service.wfad", + "createdDate":1438196820, + "modifiedBy":"service.wfad", + "modifiedDate":1438196820 + }, + "description":"test-server", + "details":{ + "alertPolicies":[ + + ], + "cpu":1, + "customFields":[ + + ], + "diskCount":3, + "disks":[ + { + "id":"0:0", + "partitionPaths":[ + + ], + "sizeGB":1 + }, + { + "id":"0:1", + "partitionPaths":[ + + ], + "sizeGB":2 + }, + { + "id":"0:2", + "partitionPaths":[ + + ], + "sizeGB":14 + } + ], + "hostName":"", + "inMaintenanceMode":false, + "ipAddresses":[ + { + "internal":"10.1.1.1" + } + ], + "memoryGB":1, + "memoryMB":1024, + "partitions":[ + + ], + "powerState":"started", + "snapshots":[ + + ], + "storageGB":17 + }, + "groupId":"086ac1dfe0b6411989e8d1b77c4065f0", + "id":"test-server", + "ipaddress":"10.120.45.23", + "isTemplate":false, + "links":[ + { + "href":"/v2/servers/wfad/test-server", + "id":"test-server", + "rel":"self", + "verbs":[ + "GET", + "PATCH", + "DELETE" + ] + }, + { + "href":"/v2/groups/wfad/086ac1dfe0b6411989e8d1b77c4065f0", + "id":"086ac1dfe0b6411989e8d1b77c4065f0", + "rel":"group" + }, + { + "href":"/v2/accounts/wfad", + "id":"wfad", + "rel":"account" + }, + { + "href":"/v2/billing/wfad/serverPricing/test-server", + "rel":"billing" + }, + { + "href":"/v2/servers/wfad/test-server/publicIPAddresses", + "rel":"publicIPAddresses", + "verbs":[ + "POST" + ] + }, + { + "href":"/v2/servers/wfad/test-server/credentials", + "rel":"credentials" + }, + { + "href":"/v2/servers/wfad/test-server/statistics", + "rel":"statistics" + }, + { + "href":"/v2/servers/wfad/510ec21ae82d4dc89d28479753bf736a/upcomingScheduledActivities", + "rel":"upcomingScheduledActivities" + }, + { + "href":"/v2/servers/wfad/510ec21ae82d4dc89d28479753bf736a/scheduledActivities", + "rel":"scheduledActivities", + "verbs":[ + "GET", + "POST" + ] + }, + { + "href":"/v2/servers/wfad/test-server/capabilities", + "rel":"capabilities" + }, + { + "href":"/v2/servers/wfad/test-server/alertPolicies", + "rel":"alertPolicyMappings", + "verbs":[ + "POST" + ] + }, + { + "href":"/v2/servers/wfad/test-server/antiAffinityPolicy", + "rel":"antiAffinityPolicyMapping", + "verbs":[ + "PUT", + "DELETE" + ] + }, + { + "href":"/v2/servers/wfad/test-server/cpuAutoscalePolicy", + "rel":"cpuAutoscalePolicyMapping", + "verbs":[ + "PUT", + "DELETE" + ] + } + ], + "locationId":"UC1", + "name":"test-server", + "os":"ubuntu14_64Bit", + "osType":"Ubuntu 14 64-bit", + "status":"active", + "storageType":"standard", + "type":"standard" + } + ] +''' + __version__ = '${version}' from time import sleep @@ -361,7 +557,7 @@ def process_request(self): elif state == 'present': # Changed is always set to true when provisioning new instances - if not p.get('template'): + if not p.get('template') and p.get('type') != 'bareMetal': return self.module.fail_json( msg='template parameter is required for new instance') @@ -406,7 +602,7 @@ def _define_module_argument_spec(): choices=[ 'standard', 'hyperscale']), - type=dict(default='standard', choices=['standard', 'hyperscale']), + type=dict(default='standard', choices=['standard', 'hyperscale', 'bareMetal']), primary_dns=dict(default=None), secondary_dns=dict(default=None), additional_disks=dict(type='list', default=[]), @@ -440,6 +636,14 @@ def _define_module_argument_spec(): 'UDP', 'ICMP']), public_ip_ports=dict(type='list', default=[]), + configuration_id=dict(default=None), + os_type=dict(default=None, + choices=[ + 'redHat6_64Bit', + 'centOS6_64Bit', + 'windows2012R2Standard_64Bit', + 'ubuntu14_64Bit' + ]), wait=dict(type='bool', default=True)) mutually_exclusive = [ @@ -462,7 +666,6 @@ def _set_clc_credentials_from_env(self): v2_api_passwd = env.get('CLC_V2_API_PASSWD', False) clc_alias = env.get('CLC_ACCT_ALIAS', False) api_url = env.get('CLC_V2_API_URL', False) - if api_url: self.clc.defaults.ENDPOINT_URL_V2 = api_url @@ -520,9 +723,12 @@ def _find_datacenter(clc, module): """ location = module.params.get('location') try: - datacenter = clc.v2.Datacenter(location) - return datacenter - except CLCException: + if not location: + account = clc.v2.Account() + location = account.data.get('primaryDataCenter') + data_center = clc.v2.Datacenter(location) + return data_center + except CLCException as ex: module.fail_json( msg=str( "Unable to find location: {0}".format(location))) @@ -668,9 +874,10 @@ def _find_template_id(module, datacenter): """ lookup_template = module.params.get('template') state = module.params.get('state') + type = module.params.get('type') result = None - if state == 'present': + if state == 'present' and type != 'bareMetal': try: result = datacenter.Templates().Search(lookup_template)[0].id except CLCException: @@ -793,7 +1000,9 @@ def _create_servers(self, module, clc, override_count=None): 'source_server_password': p.get('source_server_password'), 'cpu_autoscale_policy_id': p.get('cpu_autoscale_policy_id'), 'anti_affinity_policy_id': p.get('anti_affinity_policy_id'), - 'packages': p.get('packages') + 'packages': p.get('packages'), + 'configuration_id': p.get('configuration_id'), + 'os_type': p.get('os_type') } count = override_count if override_count else p.get('count') @@ -1124,7 +1333,12 @@ def _change_server_power_state(module, server, state): if state == 'started': result = server.PowerOn() else: - result = server.PowerOff() + # Try to shut down the server and fall back to power off when unable to shut down. + result = server.ShutDown() + if result and hasattr(result, 'requests') and result.requests[0]: + return result + else: + result = server.PowerOff() except CLCException: module.fail_json( msg='Unable to change power state for server {0}'.format( @@ -1251,7 +1465,9 @@ def _create_clc_server( 'customFields': server_params.get('custom_fields'), 'additionalDisks': server_params.get('additional_disks'), 'ttl': server_params.get('ttl'), - 'packages': server_params.get('packages')})) + 'packages': server_params.get('packages'), + 'configurationId': server_params.get('configuration_id'), + 'osType': server_params.get('os_type')})) result = clc.v2.Requests(res) except APIFailedResponse as ex: diff --git a/cloud/centurylink/clc_server_snapshot.py b/cloud/centurylink/clc_server_snapshot.py index cb5f66e7a8d..603d03eac1b 100644 --- a/cloud/centurylink/clc_server_snapshot.py +++ b/cloud/centurylink/clc_server_snapshot.py @@ -69,8 +69,8 @@ - name: Create server snapshot clc_server_snapshot: server_ids: - - UC1TESTSVR01 - - UC1TESTSVR02 + - UC1TEST-SVR01 + - UC1TEST-SVR02 expiration_days: 10 wait: True state: present @@ -78,20 +78,37 @@ - name: Restore server snapshot clc_server_snapshot: server_ids: - - UC1TESTSVR01 - - UC1TESTSVR02 + - UC1TEST-SVR01 + - UC1TEST-SVR02 wait: True state: restore - name: Delete server snapshot clc_server_snapshot: server_ids: - - UC1TESTSVR01 - - UC1TESTSVR02 + - UC1TEST-SVR01 + - UC1TEST-SVR02 wait: True state: absent ''' +RETURN = ''' +changed: + description: A flag indicating if any change was made or not + returned: success + type: boolean + sample: True +server_ids: + description: The list of server ids that are changed + returned: success + type: list + sample: + [ + "UC1TEST-SVR01", + "UC1TEST-SVR02" + ] +''' + __version__ = '${version}' from distutils.version import LooseVersion From adc78eaf873d827c45aa8d610de9e757d1fb59e3 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Wed, 21 Oct 2015 20:06:26 +0200 Subject: [PATCH 0876/2522] fixed documentation --- windows/win_nssm.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/windows/win_nssm.py b/windows/win_nssm.py index 1f42f21f45a..98be076a48b 100644 --- a/windows/win_nssm.py +++ b/windows/win_nssm.py @@ -94,7 +94,8 @@ start_mode: description: - If C(auto) is selected, the service will start at bootup. C(manual) means that the service will start only when another service needs it. C(disabled) means that the service will stay off, regardless if it is needed or not. - required: false + required: true + default: auto choices: - auto - manual From 3a5d4576c65140e09ca021cf505ba2c70fa16c84 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Wed, 21 Oct 2015 21:11:51 +0200 Subject: [PATCH 0877/2522] as suggested by @marcind, convert to boolean --- windows/win_owner.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/windows/win_owner.ps1 b/windows/win_owner.ps1 index 519d6fe5802..eb69e744e63 100644 --- a/windows/win_owner.ps1 +++ b/windows/win_owner.ps1 @@ -91,7 +91,7 @@ Set-Attr $result "changed" $false; $path = Get-Attr $params "path" -failifempty $true $user = Get-Attr $params "user" -failifempty $true -$recurse = Get-Attr $params "recurse" "no" -validateSet "no","yes" -resultobj $result +$recurse = Get-Attr $params "recurse" "no" -validateSet "no","yes" -resultobj $result | ConvertTo-Bool If (-Not (Test-Path -Path $path)) { Fail-Json $result "$path file or directory does not exist on the host" @@ -117,7 +117,7 @@ Try { $acl.setOwner($objUser) Set-Acl $file.FullName $acl - If ($recurse -eq "yes") { + If ($recurse) { $files = Get-ChildItem -Path $path -Force -Recurse ForEach($file in $files){ $acl = Get-Acl $file.FullName From 8de49a5deaa2827407183e8680007e922c19b5d6 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Wed, 21 Oct 2015 21:20:44 +0200 Subject: [PATCH 0878/2522] suggestions by @marcind --- windows/win_acl_inheritance.ps1 | 4 ++-- windows/win_acl_inheritance.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/windows/win_acl_inheritance.ps1 b/windows/win_acl_inheritance.ps1 index e72570ba3a6..674180e33b9 100644 --- a/windows/win_acl_inheritance.ps1 +++ b/windows/win_acl_inheritance.ps1 @@ -26,7 +26,7 @@ $result = New-Object PSObject; Set-Attr $result "changed" $false; $path = Get-Attr $params "path" -failifempty $true -$copy = Get-Attr $params "copy" "no" -validateSet "no","yes" -resultobj $result +$copy = Get-Attr $params "copy" "no" -validateSet "no","yes" -resultobj $result | ConvertTo-Bool If (-Not (Test-Path -Path $path)) { Fail-Json $result "$path file or directory does not exist on the host" @@ -36,7 +36,7 @@ Try { $objACL = Get-ACL $path $alreadyDisabled = !$objACL.AreAccessRulesProtected - If ($copy -eq "yes") { + If ($copy) { $objACL.SetAccessRuleProtection($True, $True) } Else { $objACL.SetAccessRuleProtection($True, $False) diff --git a/windows/win_acl_inheritance.py b/windows/win_acl_inheritance.py index 784aa5f9877..d55473491b8 100644 --- a/windows/win_acl_inheritance.py +++ b/windows/win_acl_inheritance.py @@ -27,7 +27,7 @@ version_added: "2.0" short_description: Disable ACL inheritance description: - - Disable ACL inheritance and optionally converts ACE to dedicated ACE + - Disable ACL (Access Control List) inheritance and optionally converts ACE (Access Control Entry) to dedicated ACE options: path: description: @@ -48,12 +48,12 @@ # Playbook example --- - name: Disable and copy - win_owner: + win_acl_inheritance: path: 'C:\\apache\\' copy: yes - name: Disable - win_owner: + win_acl_inheritance: path: 'C:\\apache\\' copy: no ''' From b03c7ebfa12c8b2b4877745e20aa286c9e4aa126 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Wed, 21 Oct 2015 22:43:42 +0200 Subject: [PATCH 0879/2522] introduced state to differentiate between enabled/disabled inheritance. renamed copy to reorganize, since the meaning for inheritance=enabled is different --- windows/win_acl_inheritance.ps1 | 44 ++++++++++++++++++++++++++------- windows/win_acl_inheritance.py | 33 ++++++++++++++++++------- 2 files changed, 59 insertions(+), 18 deletions(-) diff --git a/windows/win_acl_inheritance.ps1 b/windows/win_acl_inheritance.ps1 index 674180e33b9..35b6809d0ee 100644 --- a/windows/win_acl_inheritance.ps1 +++ b/windows/win_acl_inheritance.ps1 @@ -26,7 +26,8 @@ $result = New-Object PSObject; Set-Attr $result "changed" $false; $path = Get-Attr $params "path" -failifempty $true -$copy = Get-Attr $params "copy" "no" -validateSet "no","yes" -resultobj $result | ConvertTo-Bool +$state = Get-Attr $params "state" "absent" -validateSet "present","absent" -resultobj $result +$reorganize = Get-Attr $params "reorganize" "no" -validateSet "no","yes" -resultobj $result | ConvertTo-Bool If (-Not (Test-Path -Path $path)) { Fail-Json $result "$path file or directory does not exist on the host" @@ -34,19 +35,44 @@ If (-Not (Test-Path -Path $path)) { Try { $objACL = Get-ACL $path - $alreadyDisabled = !$objACL.AreAccessRulesProtected + $inheritanceEnabled = !$objACL.AreAccessRulesProtected - If ($copy) { - $objACL.SetAccessRuleProtection($True, $True) - } Else { - $objACL.SetAccessRuleProtection($True, $False) - } + If (($state -eq "present") -And !$inheritanceEnabled) { + If ($reorganize) { + $objACL.SetAccessRuleProtection($True, $True) + } Else { + $objACL.SetAccessRuleProtection($True, $False) + } - If ($alreadyDisabled) { + Set-ACL $path $objACL Set-Attr $result "changed" $true; } + Elseif (($state -eq "absent") -And $inheritanceEnabled) { + # second parameter is ignored if first=$False + $objACL.SetAccessRuleProtection($False, $False) + + If ($reorganize) { + # convert explicit ACE to inherited ACE + ForEach($inheritedRule in $objACL.Access) { + If (!$inheritedRule.IsInherited) { + Continue + } + + ForEach($explicitRrule in $objACL.Access) { + If ($inheritedRule.IsInherited) { + Continue + } - Set-ACL $path $objACL + If (($inheritedRule.FileSystemRights -eq $explicitRrule.FileSystemRights) -And ($inheritedRule.AccessControlType -eq $explicitRrule.AccessControlType) -And ($inheritedRule.IdentityReference -eq $explicitRrule.IdentityReference) -And ($inheritedRule.InheritanceFlags -eq $explicitRrule.InheritanceFlags) -And ($inheritedRule.PropagationFlags -eq $explicitRrule.PropagationFlags)) { + $objACL.RemoveAccessRule($explicitRrule) + } + } + } + } + + Set-ACL $path $objACL + Set-Attr $result "changed" $true; + } } Catch { Fail-Json $result "an error occured when attempting to disable inheritance" diff --git a/windows/win_acl_inheritance.py b/windows/win_acl_inheritance.py index d55473491b8..6c03b9c75fd 100644 --- a/windows/win_acl_inheritance.py +++ b/windows/win_acl_inheritance.py @@ -25,17 +25,25 @@ --- module: win_acl_inheritance version_added: "2.0" -short_description: Disable ACL inheritance +short_description: Change ACL inheritance description: - - Disable ACL (Access Control List) inheritance and optionally converts ACE (Access Control Entry) to dedicated ACE + - Change ACL (Access Control List) inheritance and optionally copy inherited ACE's (Access Control Entry) to dedicated ACE's or vice versa. options: path: description: - - Path to be used for disabling + - Path to be used for changing inheritance required: true - copy: + state: description: - - Indicates if the inherited ACE should be copied to dedicated ACE + - Specify whether to enable I(present) or disable I(absent) ACL inheritance + required: false + choices: + - present + - absent + default: absent + reorganize: + description: + - For P(state) = I(absent), indicates if the inherited ACE's should be copied. For P(state) = I(present), indicates if the inherited ACE's should be simplified. required: false choices: - no @@ -47,13 +55,20 @@ EXAMPLES = ''' # Playbook example --- -- name: Disable and copy +- name: Disable inherited ACE's + win_acl_inheritance: + path: 'C:\\apache\\' + state: absent + +- name: Disable and copy inherited ACE's win_acl_inheritance: path: 'C:\\apache\\' - copy: yes + state: absent + reorganize: yes -- name: Disable +- name: Enable and remove dedicated ACE's win_acl_inheritance: path: 'C:\\apache\\' - copy: no + state: present + reorganize: yes ''' From 89f8e249fa7a2975d49ae5cdd86c853d734f6a2e Mon Sep 17 00:00:00 2001 From: whiter Date: Tue, 13 Oct 2015 17:32:27 +1100 Subject: [PATCH 0880/2522] Refactor ec2_remote_facts to use filters --- cloud/amazon/ec2_remote_facts.py | 200 ++++++++++++++++--------------- 1 file changed, 106 insertions(+), 94 deletions(-) diff --git a/cloud/amazon/ec2_remote_facts.py b/cloud/amazon/ec2_remote_facts.py index f273f17a8ce..cb92ccba74d 100644 --- a/cloud/amazon/ec2_remote_facts.py +++ b/cloud/amazon/ec2_remote_facts.py @@ -16,137 +16,149 @@ DOCUMENTATION = ''' --- module: ec2_remote_facts -short_description: ask EC2 for information about other instances. +short_description: Gather facts about ec2 instances in AWS description: - - Only supports search for hostname by tags currently. Looking to add more later. + - Gather facts about ec2 instances in AWS version_added: "2.0" options: - key: + filters: description: - - instance tag key in EC2 - required: false - default: Name - value: - description: - - instance tag value in EC2 + - A dict of filters to apply. Each dict item consists of a filter key and a filter value. See U(http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html) for possible filters. required: false default: null - lookup: - description: - - What type of lookup to use when searching EC2 instance info. - required: false - default: tags - region: - description: - - EC2 region that it should look for tags in - required: false - default: All Regions - ignore_state: - description: - - instance state that should be ignored such as terminated. - required: false - default: terminated author: - "Michael Schuett (@michaeljs1990)" -extends_documentation_fragment: aws +extends_documentation_fragment: + - aws + - ec2 ''' EXAMPLES = ''' # Note: These examples do not set authentication details, see the AWS Guide for details. -# Basic provisioning example +# Gather facts about all ec2 instances +- ec2_remote_facts: + +# Gather facts about all running ec2 instances with a tag of Name:Example - ec2_remote_facts: - key: mykey - value: myvalue - register: servers + filters: + instance-state-name: running + "tag:Name": Example + +# Gather facts about instance i-123456 +- ec2_remote_facts: + filters: + instance-id: i-123456 + +# Gather facts about all instances in vpc-123456 that are t2.small type +- ec2_remote_facts: + filters: + vpc-id: vpc-123456 + instance-type: t2.small + ''' + try: - import boto import boto.ec2 + from boto.exception import BotoServerError HAS_BOTO = True except ImportError: HAS_BOTO = False -def todict(obj, classkey=None): - if isinstance(obj, dict): - data = {} - for (k, v) in obj.items(): - data[k] = todict(v, classkey) - return data - elif hasattr(obj, "_ast"): - return todict(obj._ast()) - elif hasattr(obj, "__iter__"): - return [todict(v, classkey) for v in obj] - elif hasattr(obj, "__dict__"): - # This Class causes a recursive loop and at this time is not worth - # debugging. If it's useful later I'll look into it. - if not isinstance(obj, boto.ec2.blockdevicemapping.BlockDeviceType): - data = dict([(key, todict(value, classkey)) - for key, value in obj.__dict__.iteritems() - if not callable(value) and not key.startswith('_')]) - if classkey is not None and hasattr(obj, "__class__"): - data[classkey] = obj.__class__.__name__ - return data - else: - return obj +def get_instance_info(instance): + + # Get groups + groups = [] + for group in instance.groups: + groups.append({ 'id': group.id, 'name': group.name }.copy()) -def get_all_ec2_regions(module): - try: - regions = boto.ec2.regions() - except Exception, e: - module.fail_json('Boto authentication issue: %s' % e) + # Get interfaces + interfaces = [] + for interface in instance.interfaces: + interfaces.append({ 'id': interface.id, 'mac_address': interface.mac_address }.copy()) + + instance_info = { 'id': instance.id, + 'kernel': instance.kernel, + 'instance_profile': instance.instance_profile, + 'root_device_type': instance.root_device_type, + 'private_dns_name': instance.private_dns_name, + 'public_dns_name': instance.public_dns_name, + 'ebs_optimized': instance.ebs_optimized, + 'client_token': instance.client_token, + 'virtualization_type': instance.virtualization_type, + 'architecture': instance.architecture, + 'ramdisk': instance.ramdisk, + 'tags': instance.tags, + 'key_name': instance.key_name, + 'source_destination_check': instance.sourceDestCheck, + 'image_id': instance.image_id, + 'groups': groups, + 'interfaces': interfaces, + 'spot_instance_request_id': instance.spot_instance_request_id, + 'requester_id': instance.requester_id, + 'monitoring_state': instance.monitoring_state, + 'placement': { + 'tenancy': instance._placement.tenancy, + 'zone': instance._placement.zone + }, + 'ami_launch_index': instance.ami_launch_index, + 'launch_time': instance.launch_time, + 'hypervisor': instance.hypervisor, + 'region': instance.region.name, + 'persistent': instance.persistent, + 'private_ip_address': instance.private_ip_address, + 'state': instance._state.name, + 'vpc_id': instance.vpc_id, + } - return regions + return instance_info + -# Connect to ec2 region -def connect_to_region(region, module): +def list_ec2_instances(connection, module): + + filters = module.params.get("filters") + instance_dict_array = [] + try: - conn = boto.ec2.connect_to_region(region.name) - except Exception, e: - print module.jsonify('error connecting to region: ' + region.name) - conn = None - # connect_to_region will fail "silently" by returning - # None if the region name is wrong or not supported - return conn + all_instances = connection.get_only_instances(filters=filters) + except BotoServerError as e: + module.fail_json(msg=e.message) + + for instance in all_instances: + instance_dict_array.append(get_instance_info(instance)) + + module.exit_json(instances=instance_dict_array) + def main(): - module = AnsibleModule( - argument_spec = dict( - key = dict(default='Name'), - value = dict(), - lookup = dict(default='tags'), - ignore_state = dict(default='terminated'), - region = dict(), + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + filters = dict(default=None, type='dict') ) ) + module = AnsibleModule(argument_spec=argument_spec) + if not HAS_BOTO: module.fail_json(msg='boto required for this module') - server_info = list() + region, ec2_url, aws_connect_params = get_aws_connection_info(module) - for region in get_all_ec2_regions(module): - conn = connect_to_region(region, module) + if region: try: - # Run when looking up by tag names, only returning hostname currently - if module.params.get('lookup') == 'tags': - ec2_key = 'tag:' + module.params.get('key') - ec2_value = module.params.get('value') - reservations = conn.get_all_instances(filters={ec2_key : ec2_value}) - for instance in [i for r in reservations for i in r.instances]: - if instance.private_ip_address != None: - instance.hostname = 'ip-' + instance.private_ip_address.replace('.', '-') - if instance._state.name not in module.params.get('ignore_state'): - server_info.append(todict(instance)) - except: - print module.jsonify('error getting instances from: ' + region.name) - - ec2_facts_result = dict(changed=True, ec2=server_info) - - module.exit_json(**ec2_facts_result) + connection = connect_to_aws(boto.ec2, region, **aws_connect_params) + except (boto.exception.NoAuthHandlerFound, StandardError), e: + module.fail_json(msg=str(e)) + else: + module.fail_json(msg="region must be specified") + + list_ec2_instances(connection, module) # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * -main() +if __name__ == '__main__': + main() + From b4f80a777fb066b604e9a8eb8085c32044576316 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Thu, 22 Oct 2015 14:22:50 +0200 Subject: [PATCH 0881/2522] fixed bugs with flipped protection attribute --- windows/win_acl_inheritance.ps1 | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/windows/win_acl_inheritance.ps1 b/windows/win_acl_inheritance.ps1 index 35b6809d0ee..0d808bb8c49 100644 --- a/windows/win_acl_inheritance.ps1 +++ b/windows/win_acl_inheritance.ps1 @@ -38,20 +38,14 @@ Try { $inheritanceEnabled = !$objACL.AreAccessRulesProtected If (($state -eq "present") -And !$inheritanceEnabled) { - If ($reorganize) { - $objACL.SetAccessRuleProtection($True, $True) - } Else { - $objACL.SetAccessRuleProtection($True, $False) - } - - Set-ACL $path $objACL - Set-Attr $result "changed" $true; - } - Elseif (($state -eq "absent") -And $inheritanceEnabled) { # second parameter is ignored if first=$False $objACL.SetAccessRuleProtection($False, $False) If ($reorganize) { + # it wont work without intermediate save, state would be the same + Set-ACL $path $objACL + $objACL = Get-ACL $path + # convert explicit ACE to inherited ACE ForEach($inheritedRule in $objACL.Access) { If (!$inheritedRule.IsInherited) { @@ -59,7 +53,7 @@ Try { } ForEach($explicitRrule in $objACL.Access) { - If ($inheritedRule.IsInherited) { + If ($explicitRrule.IsInherited) { Continue } @@ -70,6 +64,16 @@ Try { } } + Set-ACL $path $objACL + Set-Attr $result "changed" $true; + } + Elseif (($state -eq "absent") -And $inheritanceEnabled) { + If ($reorganize) { + $objACL.SetAccessRuleProtection($True, $True) + } Else { + $objACL.SetAccessRuleProtection($True, $False) + } + Set-ACL $path $objACL Set-Attr $result "changed" $true; } From b65a583902f4486023d1e89864eb36c48acf9d00 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 22 Oct 2015 08:31:49 -0400 Subject: [PATCH 0882/2522] added version_added to new feature, removed unused aliases --- cloud/google/gce_img.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/cloud/google/gce_img.py b/cloud/google/gce_img.py index 8e03ee75a90..b64f12febd0 100644 --- a/cloud/google/gce_img.py +++ b/cloud/google/gce_img.py @@ -33,57 +33,48 @@ - the name of the image to create or delete required: true default: null - aliases: [] description: description: - an optional description required: false default: null - aliases: [] source: description: - the source disk or the Google Cloud Storage URI to create the image from required: false default: null - aliases: [] state: description: - desired state of the image required: false default: "present" choices: ["present", "absent"] - aliases: [] zone: description: - the zone of the disk specified by source required: false default: "us-central1-a" - aliases: [] timeout: description: - timeout for the operation required: false default: 180 - aliases: [] + version_added: "2.0" service_account_email: description: - service account email required: false default: null - aliases: [] pem_file: description: - path to the pem file associated with the service account email required: false default: null - aliases: [] project_id: description: - your GCE project ID required: false default: null - aliases: [] - requirements: - "python >= 2.6" - "apache-libcloud" From c4c7e43020d6386991bd79b14e09385af73740b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Lichtblau?= Date: Wed, 21 Oct 2015 13:36:28 +0200 Subject: [PATCH 0883/2522] Check to make sure the firewalld client is connected before proceeding. Fixes #1138 Original patch referenced in https://github.com/ansible/ansible/issues/6911 ( https://github.com/ansible/ansible/commit/f547733b1f2136a531432ba652edebaec6873baf ) was undone by https://github.com/ansible/ansible-modules-extras/commit/6f2b61d2d88294ea7938020183ea613b7e5e878d --- system/firewalld.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/system/firewalld.py b/system/firewalld.py index 47d98544000..61e4a546132 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -97,7 +97,10 @@ from firewall.client import FirewallClient fw = FirewallClient() - HAS_FIREWALLD = True + if not fw.connected: + HAS_FIREWALLD = False + else: + HAS_FIREWALLD = True except ImportError: HAS_FIREWALLD = False From cfec045697d80e5eb073a6d3b5e06025356f20fe Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Sun, 4 Oct 2015 09:54:25 +0200 Subject: [PATCH 0884/2522] system: pam_limits: add support for unlimited/infinity/-1 Early pam_limits module didn't support special values for items. This patch is adding support to special values unlimited, infinity and -1. Issue: https://github.com/ansible/ansible-modules-extras/issues/1033 Signed-off-by: Ondra Machacek --- system/pam_limits.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/system/pam_limits.py b/system/pam_limits.py index 080b938dd01..eb04021c3e0 100644 --- a/system/pam_limits.py +++ b/system/pam_limits.py @@ -102,7 +102,7 @@ def main(): domain = dict(required=True, type='str'), limit_type = dict(required=True, type='str', choices=pam_types), limit_item = dict(required=True, type='str', choices=pam_items), - value = dict(required=True, type='int'), + value = dict(required=True, type='str'), use_max = dict(default=False, type='bool'), use_min = dict(default=False, type='bool'), backup = dict(default=False, type='bool'), @@ -132,6 +132,9 @@ def main(): if use_max and use_min: module.fail_json(msg="Cannot use use_min and use_max at the same time." ) + if not (value in ['unlimited', 'infinity', '-1'] or value.isdigit()): + module.fail_json(msg="Argument 'value' can be one of 'unlimited', 'infinity', '-1' or positive number. Refer to manual pages for more details.") + # Backup if backup: backup_file = module.backup_local(limits_conf) @@ -181,7 +184,10 @@ def main(): line_domain = line_fields[0] line_type = line_fields[1] line_item = line_fields[2] - actual_value = int(line_fields[3]) + actual_value = line_fields[3] + + if not (actual_value in ['unlimited', 'infinity', '-1'] or actual_value.isdigit()): + module.fail_json(msg="Invalid configuration of '%s'. Current value of %s is unsupported." % (limits_conf, line_item)) # Found the line if line_domain == domain and line_type == limit_type and line_item == limit_item: @@ -191,11 +197,24 @@ def main(): nf.write(line) continue + actual_value_unlimited = actual_value in ['unlimited', 'infinity', '-1'] + value_unlimited = value in ['unlimited', 'infinity', '-1'] + if use_max: - new_value = max(value, actual_value) + if value.isdigit() and actual_value.isdigit(): + new_value = max(int(value), int(actual_value)) + elif actual_value_unlimited: + new_value = actual_value + else: + new_value = value if use_min: - new_value = min(value,actual_value) + if value.isdigit() and actual_value.isdigit(): + new_value = min(int(value), int(actual_value)) + elif value_unlimited: + new_value = actual_value + else: + new_value = value # Change line only if value has changed if new_value != actual_value: From 56e7d80479b9ac11eb75f6885b417f225bbb2caa Mon Sep 17 00:00:00 2001 From: Adam Keech Date: Thu, 22 Oct 2015 17:41:18 -0400 Subject: [PATCH 0885/2522] Fixing Some Issues with out Parameters are passed around. Also some refactoring. --- windows/win_nssm.ps1 | 99 ++++++++++++++++++++------------------------ 1 file changed, 46 insertions(+), 53 deletions(-) diff --git a/windows/win_nssm.ps1 b/windows/win_nssm.ps1 index 99a61d12a90..bf4e798fca5 100644 --- a/windows/win_nssm.ps1 +++ b/windows/win_nssm.ps1 @@ -2,7 +2,7 @@ # This file is part of Ansible # # Copyright 2015, George Frank -# Copyright 2015, Adam Keech +# Copyright 2015, Adam Keech # Copyright 2015, Hans-Joachim Kliemeck # # Ansible is free software: you can redistribute it and/or modify @@ -29,18 +29,18 @@ $result = New-Object PSObject; Set-Attr $result "changed" $false; $name = Get-Attr $params "name" -failifempty $true -$state = Get-Attr $params "state" "present" -validateSet "present", "absent", "started", "stopped", "restarted" -resultobj $result +$state = Get-Attr $params "state" -default "present" -validateSet "present", "absent", "started", "stopped", "restarted" -resultobj $result -$application = Get-Attr $params "application" $null -$appParameters = Get-Attr $params "app_parameters" $null -$startMode = Get-Attr $params "start_mode" "auto" -validateSet "auto", "manual", "disabled" -resultobj $result +$application = Get-Attr $params "application" -default $null +$appParameters = Get-Attr $params "app_parameters" -default $null +$startMode = Get-Attr $params "start_mode" -default "auto" -validateSet "auto", "manual", "disabled" -resultobj $result -$stdoutFile = Get-Attr $params "stdout_file" $null -$stderrFile = Get-Attr $params "stderr_file" $null -$dependencies = Get-Attr $params "dependencies" $null +$stdoutFile = Get-Attr $params "stdout_file" -default $null +$stderrFile = Get-Attr $params "stderr_file" -default $null +$dependencies = Get-Attr $params "dependencies" -default $null -$user = Get-Attr $params "user" $null -$password = Get-Attr $params "password" $null +$user = Get-Attr $params "user" -default $null +$password = Get-Attr $params "password" -default $null Function Service-Exists { @@ -87,6 +87,7 @@ Function Nssm-Install [Parameter(Mandatory=$true)] [string]$name, [Parameter(Mandatory=$true)] + [AllowEmptyString()] [string]$application ) @@ -144,6 +145,7 @@ Function ParseAppParameters() [CmdletBinding()] param( [Parameter(Mandatory=$true)] + [AllowEmptyString()] [string]$appParameters ) @@ -158,6 +160,7 @@ Function Nssm-Update-AppParameters [Parameter(Mandatory=$true)] [string]$name, [Parameter(Mandatory=$true)] + [AllowEmptyString()] [string]$appParameters ) @@ -343,28 +346,29 @@ Function Nssm-Update-Credentials Throw "Error updating credentials for service ""$name""" } - if ($user -ne $null) { - If ($password -eq $null) { + if ($user) { + if (!$password) { Throw "User without password is informed for service ""$name""" } + else { + $fullUser = $user + If (-not($user -contains "@") -and ($user.Split("\").count -eq 1)) { + $fullUser = ".\" + $user + } - $fullUser = $user - If (-Not($user -contains "@") -And ($user.Split("\").count -eq 1)) { - $fullUser = ".\" + $user - } + If ($results -ne $fullUser) { + $cmd = "nssm set ""$name"" ObjectName $fullUser $password" + $results = invoke-expression $cmd - If ($results -ne $fullUser) { - $cmd = "nssm set ""$name"" ObjectName $fullUser $password" - $results = invoke-expression $cmd + if ($LastExitCode -ne 0) + { + Set-Attr $result "nssm_error_cmd" $cmd + Set-Attr $result "nssm_error_log" "$results" + Throw "Error updating credentials for service ""$name""" + } - if ($LastExitCode -ne 0) - { - Set-Attr $result "nssm_error_cmd" $cmd - Set-Attr $result "nssm_error_log" "$results" - Throw "Error updating credentials for service ""$name""" + $result.changed = $true } - - $result.changed = $true } } } @@ -389,7 +393,7 @@ Function Nssm-Update-Dependencies Throw "Error updating dependencies for service ""$name""" } - If (($dependencies -ne $null) -And ($results.Tolower() -ne $dependencies.Tolower())) { + If (($dependencies) -and ($results.Tolower() -ne $dependencies.Tolower())) { $cmd = "nssm set ""$name"" DependOnService $dependencies" $results = invoke-expression $cmd @@ -546,7 +550,7 @@ Function Nssm-Stop Throw "Error stopping service ""$name""" } - if (currentStatus -ne "SERVICE_STOPPED") + if ($currentStatus -ne "SERVICE_STOPPED") { $cmd = "nssm stop ""$name""" @@ -575,44 +579,34 @@ Function Nssm-Restart Nssm-Start-Service-Command -name $name } +Function NssmProcedure +{ + Nssm-Install -name $name -application $application + Nssm-Update-AppParameters -name $name -appParameters $appParameters + Nssm-Set-Ouput-Files -name $name -stdout $stdoutFile -stderr $stderrFile + Nssm-Update-Dependencies -name $name -dependencies $dependencies + Nssm-Update-Credentials -name $name -user $user -password $password + Nssm-Update-StartMode -name $name -mode $startMode +} + Try { switch ($state) { "absent" { Nssm-Remove -name $name } "present" { - Nssm-Install -name $name -application $application - Nssm-Update-AppParameters -name $name -appParameters $appParameters - Nssm-Set-Ouput-Files -name $name -stdout $stdoutFile -stderr $stderrFile - Nssm-Update-Dependencies -name $name -dependencies $dependencies - Nssm-Update-Credentials -name $name -user $user -password $password - Nssm-Update-StartMode -name $name -mode $startMode + NssmProcedure } "started" { - Nssm-Install -name $name -application $application - Nssm-Update-AppParameters -name $name -appParameters $appParameters - Nssm-Set-Ouput-Files -name $name -stdout $stdoutFile -stderr $stderrFile - Nssm-Update-Dependencies -name $name -dependencies $dependencies - Nssm-Update-Credentials -name $name -user $user -password $password - Nssm-Update-StartMode -name $name -mode $startMode + NssmProcedure Nssm-Start -name $name } "stopped" { - Nssm-Install -name $name -application $application - Nssm-Update-AppParameters -name $name -appParameters $appParameters - Nssm-Set-Ouput-Files -name $name -stdout $stdoutFile -stderr $stderrFile - Nssm-Update-Dependencies -name $name -dependencies $dependencies - Nssm-Update-Credentials -name $name -user $user -password $password - Nssm-Update-StartMode -name $name -mode $startMode + NssmProcedure Nssm-Stop -name $name } "restarted" { - Nssm-Install -name $name -application $application - Nssm-Update-AppParameters -name $name -appParameters $appParameters - Nssm-Set-Ouput-Files -name $name -stdout $stdoutFile -stderr $stderrFile - Nssm-Update-Dependencies -name $name -dependencies $dependencies - Nssm-Update-Credentials -name $name -user $user -password $password - Nssm-Update-StartMode -name $name -mode $startMode + NssmProcedure Nssm-Restart -name $name } } @@ -623,4 +617,3 @@ Catch { Fail-Json $result $_.Exception.Message } - From a30a1eef86f53810c301859b9e09baaee8941841 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Proszek?= Date: Fri, 23 Oct 2015 00:35:41 +0200 Subject: [PATCH 0886/2522] add lxc support, without dropping openvz --- cloud/misc/proxmox.py | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/cloud/misc/proxmox.py b/cloud/misc/proxmox.py index 7be4361edbe..b31f96c6829 100644 --- a/cloud/misc/proxmox.py +++ b/cloud/misc/proxmox.py @@ -20,6 +20,7 @@ short_description: management of instances in Proxmox VE cluster description: - allows you to create/delete/stop instances in Proxmox VE cluster + - automatically detects conainerization type (lxc for PVE 4, openvz for older) version_added: "2.0" options: api_host: @@ -195,6 +196,8 @@ except ImportError: HAS_PROXMOXER = False +VZ_TYPE=None + def get_instance(proxmox, vmid): return [ vm for vm in proxmox.cluster.resources.get(type='vm') if vm['vmid'] == int(vmid) ] @@ -206,8 +209,14 @@ def node_check(proxmox, node): def create_instance(module, proxmox, vmid, node, disk, storage, cpus, memory, swap, timeout, **kwargs): proxmox_node = proxmox.nodes(node) - taskid = proxmox_node.openvz.create(vmid=vmid, storage=storage, memory=memory, swap=swap, - cpus=cpus, disk=disk, **kwargs) + kwargs = dict((k,v) for k, v in kwargs.iteritems() if v is not None) + if VZ_TYPE =='lxc': + kwargs['cpulimit']=cpus + kwargs['rootfs']=disk + else: + kwargs['cpus']=cpus + kwargs['disk']=disk + taskid = getattr(proxmox_node, VZ_TYPE).create(vmid=vmid, storage=storage, memory=memory, swap=swap, **kwargs) while timeout: if ( proxmox_node.tasks(taskid).status.get()['status'] == 'stopped' @@ -222,7 +231,7 @@ def create_instance(module, proxmox, vmid, node, disk, storage, cpus, memory, sw return False def start_instance(module, proxmox, vm, vmid, timeout): - taskid = proxmox.nodes(vm[0]['node']).openvz(vmid).status.start.post() + taskid = getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.start.post() while timeout: if ( proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['status'] == 'stopped' and proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['exitstatus'] == 'OK' ): @@ -237,9 +246,9 @@ def start_instance(module, proxmox, vm, vmid, timeout): def stop_instance(module, proxmox, vm, vmid, timeout, force): if force: - taskid = proxmox.nodes(vm[0]['node']).openvz(vmid).status.shutdown.post(forceStop=1) + taskid = getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.shutdown.post(forceStop=1) else: - taskid = proxmox.nodes(vm[0]['node']).openvz(vmid).status.shutdown.post() + taskid = getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.shutdown.post() while timeout: if ( proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['status'] == 'stopped' and proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['exitstatus'] == 'OK' ): @@ -253,7 +262,7 @@ def stop_instance(module, proxmox, vm, vmid, timeout, force): return False def umount_instance(module, proxmox, vm, vmid, timeout): - taskid = proxmox.nodes(vm[0]['node']).openvz(vmid).status.umount.post() + taskid = getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.umount.post() while timeout: if ( proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['status'] == 'stopped' and proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['exitstatus'] == 'OK' ): @@ -321,6 +330,9 @@ def main(): try: proxmox = ProxmoxAPI(api_host, user=api_user, password=api_password, verify_ssl=validate_certs) + global VZ_TYPE + VZ_TYPE = 'openvz' if float(proxmox.version.get()['version']) < 4.0 else 'lxc' + except Exception, e: module.fail_json(msg='authorization on proxmox cluster failed with exception: %s' % e) @@ -350,14 +362,14 @@ def main(): module.exit_json(changed=True, msg="deployed VM %s from template %s" % (vmid, module.params['ostemplate'])) except Exception, e: - module.fail_json(msg="creation of VM %s failed with exception: %s" % ( vmid, e )) + module.fail_json(msg="creation of %s VM %s failed with exception: %s" % ( VZ_TYPE, vmid, e )) elif state == 'started': try: vm = get_instance(proxmox, vmid) if not vm: module.fail_json(msg='VM with vmid = %s not exists in cluster' % vmid) - if proxmox.nodes(vm[0]['node']).openvz(vmid).status.current.get()['status'] == 'running': + if getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'running': module.exit_json(changed=False, msg="VM %s is already running" % vmid) if start_instance(module, proxmox, vm, vmid, timeout): @@ -371,7 +383,7 @@ def main(): if not vm: module.fail_json(msg='VM with vmid = %s not exists in cluster' % vmid) - if proxmox.nodes(vm[0]['node']).openvz(vmid).status.current.get()['status'] == 'mounted': + if getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'mounted': if module.params['force']: if umount_instance(module, proxmox, vm, vmid, timeout): module.exit_json(changed=True, msg="VM %s is shutting down" % vmid) @@ -379,7 +391,7 @@ def main(): module.exit_json(changed=False, msg=("VM %s is already shutdown, but mounted. " "You can use force option to umount it.") % vmid) - if proxmox.nodes(vm[0]['node']).openvz(vmid).status.current.get()['status'] == 'stopped': + if getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'stopped': module.exit_json(changed=False, msg="VM %s is already shutdown" % vmid) if stop_instance(module, proxmox, vm, vmid, timeout, force = module.params['force']): @@ -392,8 +404,8 @@ def main(): vm = get_instance(proxmox, vmid) if not vm: module.fail_json(msg='VM with vmid = %s not exists in cluster' % vmid) - if ( proxmox.nodes(vm[0]['node']).openvz(vmid).status.current.get()['status'] == 'stopped' - or proxmox.nodes(vm[0]['node']).openvz(vmid).status.current.get()['status'] == 'mounted' ): + if ( getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'stopped' + or getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'mounted' ): module.exit_json(changed=False, msg="VM %s is not running" % vmid) if ( stop_instance(module, proxmox, vm, vmid, timeout, force = module.params['force']) and @@ -408,13 +420,13 @@ def main(): if not vm: module.exit_json(changed=False, msg="VM %s does not exist" % vmid) - if proxmox.nodes(vm[0]['node']).openvz(vmid).status.current.get()['status'] == 'running': + if getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'running': module.exit_json(changed=False, msg="VM %s is running. Stop it before deletion." % vmid) - if proxmox.nodes(vm[0]['node']).openvz(vmid).status.current.get()['status'] == 'mounted': + if getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'mounted': module.exit_json(changed=False, msg="VM %s is mounted. Stop it with force option before deletion." % vmid) - taskid = proxmox.nodes(vm[0]['node']).openvz.delete(vmid) + taskid = getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE).delete(vmid) while timeout: if ( proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['status'] == 'stopped' and proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['exitstatus'] == 'OK' ): From a110019a0615fb727aa19f372a3724556cbecdc0 Mon Sep 17 00:00:00 2001 From: james Date: Thu, 22 Oct 2015 18:47:35 -0500 Subject: [PATCH 0887/2522] More consistent use of run_command() wrapper function, which now adds the default binary location to the search path --- packaging/os/pkgutil.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packaging/os/pkgutil.py b/packaging/os/pkgutil.py index 3a4720630cf..cb674b1453c 100644 --- a/packaging/os/pkgutil.py +++ b/packaging/os/pkgutil.py @@ -63,10 +63,10 @@ import pipes def package_installed(module, name): - cmd = [module.get_bin_path('pkginfo', True)] + cmd = ['pkginfo'] cmd.append('-q') cmd.append(name) - rc, out, err = module.run_command(' '.join(cmd)) + rc, out, err = run_command(module, cmd) if rc == 0: return True else: @@ -79,16 +79,16 @@ def package_latest(module, name, site): cmd += [ '-t', pipes.quote(site) ] cmd.append(pipes.quote(name)) cmd += [ '| tail -1 | grep -v SAME' ] - rc, out, err = module.run_command(' '.join(cmd), use_unsafe_shell=True) + rc, out, err = run_command(module, cmd, use_unsafe_shell=True) if rc == 1: return True else: return False -def run_command(module, cmd): +def run_command(module, cmd, **kwargs): progname = cmd[0] - cmd[0] = module.get_bin_path(progname, True) - return module.run_command(cmd) + cmd[0] = module.get_bin_path(progname, True, ['/opt/csw/bin']) + return module.run_command(cmd, **kwargs) def package_install(module, state, name, site): cmd = [ 'pkgutil', '-iy' ] From 85cc47c9e1dc21b2eb61a2b09c390fb941da89c6 Mon Sep 17 00:00:00 2001 From: Olaf Kilian Date: Fri, 23 Oct 2015 06:28:28 +0200 Subject: [PATCH 0888/2522] Default registry to docker hub --- cloud/docker/docker_login.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cloud/docker/docker_login.py b/cloud/docker/docker_login.py index 1292fe38909..cd670d345db 100644 --- a/cloud/docker/docker_login.py +++ b/cloud/docker/docker_login.py @@ -33,8 +33,9 @@ options: registry: description: - - URL of the registry, for example: https://index.docker.io/v1/ - required: true + - URL of the registry, defaults to: https://index.docker.io/v1/ + required: false + default: https://index.docker.io/v1/ username: description: - The username for the registry account @@ -214,7 +215,7 @@ def main(): module = AnsibleModule( argument_spec = dict( - registry = dict(required=True), + registry = dict(required=False, default='https://index.docker.io/v1/'), username = dict(required=True), password = dict(required=True), email = dict(required=False, default='anonymous@localhost.local'), From e86131c62f557aa67e7302a36b1f69746f7c802c Mon Sep 17 00:00:00 2001 From: Olaf Kilian Date: Fri, 23 Oct 2015 06:29:39 +0200 Subject: [PATCH 0889/2522] Add requirement for python >= 2.6 --- cloud/docker/docker_login.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/docker/docker_login.py b/cloud/docker/docker_login.py index cd670d345db..92ab168288f 100644 --- a/cloud/docker/docker_login.py +++ b/cloud/docker/docker_login.py @@ -70,7 +70,7 @@ required: false default: 600 -requirements: [ "docker-py" ] +requirements: [ "python >= 2.6", "docker-py" ] ''' EXAMPLES = ''' From c3d15a56cf8f291e7c76587689feb250facddbcb Mon Sep 17 00:00:00 2001 From: Olaf Kilian Date: Fri, 23 Oct 2015 06:33:10 +0200 Subject: [PATCH 0890/2522] Set default for email parameter to None --- cloud/docker/docker_login.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/docker/docker_login.py b/cloud/docker/docker_login.py index 92ab168288f..a1b469aead1 100644 --- a/cloud/docker/docker_login.py +++ b/cloud/docker/docker_login.py @@ -46,9 +46,9 @@ required: true email: description: - - The email address for the registry account + - The email address for the registry account. Note that private registries usually don't need this, but if you want to log into your Docker Hub account (default behaviour) you need to specify this in order to be able to log in. required: false - default: anonymous@localhost.local + default: None reauth: description: - Whether refresh existing authentication on the Docker server (boolean) @@ -218,7 +218,7 @@ def main(): registry = dict(required=False, default='https://index.docker.io/v1/'), username = dict(required=True), password = dict(required=True), - email = dict(required=False, default='anonymous@localhost.local'), + email = dict(required=False, default=None), reauth = dict(required=False, default=False, type='bool'), dockercfg_path = dict(required=False, default='~/.dockercfg'), docker_url = dict(default='unix://var/run/docker.sock'), From ef64423683093ed3624c05639f8e7a11e70359d1 Mon Sep 17 00:00:00 2001 From: Olaf Kilian Date: Fri, 23 Oct 2015 06:34:22 +0200 Subject: [PATCH 0891/2522] Make module importable for unit tests --- cloud/docker/docker_login.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cloud/docker/docker_login.py b/cloud/docker/docker_login.py index a1b469aead1..cf8147c692b 100644 --- a/cloud/docker/docker_login.py +++ b/cloud/docker/docker_login.py @@ -244,4 +244,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() From 1b7496f8dc623ddfa4e2f8223057dd988cade9b4 Mon Sep 17 00:00:00 2001 From: Ramunas Dronga Date: Fri, 23 Oct 2015 11:00:52 +0300 Subject: [PATCH 0892/2522] added version info for params 'arguments' --- packaging/language/composer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packaging/language/composer.py b/packaging/language/composer.py index fdb84896092..dfb840f66b0 100644 --- a/packaging/language/composer.py +++ b/packaging/language/composer.py @@ -37,6 +37,7 @@ required: false default: install arguments: + version_added: "2.0" description: - Composer arguments like required package, version and so on required: false From 8b7c3677fdcc4f490aef48591484acce7d46c124 Mon Sep 17 00:00:00 2001 From: Siva Popuri Date: Fri, 23 Oct 2015 09:34:58 -0500 Subject: [PATCH 0893/2522] added author to module doc string. --- cloud/centurylink/clc_aa_policy.py | 1 + cloud/centurylink/clc_alert_policy.py | 1 + cloud/centurylink/clc_blueprint_package.py | 1 + cloud/centurylink/clc_firewall_policy.py | 1 + cloud/centurylink/clc_group.py | 1 + cloud/centurylink/clc_loadbalancer.py | 29 +++++++++++----------- cloud/centurylink/clc_modify_server.py | 1 + cloud/centurylink/clc_publicip.py | 1 + cloud/centurylink/clc_server.py | 1 + cloud/centurylink/clc_server_snapshot.py | 1 + 10 files changed, 24 insertions(+), 14 deletions(-) diff --git a/cloud/centurylink/clc_aa_policy.py b/cloud/centurylink/clc_aa_policy.py index 8f4567ea1ac..7596651181a 100644 --- a/cloud/centurylink/clc_aa_policy.py +++ b/cloud/centurylink/clc_aa_policy.py @@ -50,6 +50,7 @@ - python = 2.7 - requests >= 2.5.0 - clc-sdk +author: "CLC Runner (@clc-runner)" notes: - To use this module, it is required to set the below environment variables which enables access to the Centurylink Cloud diff --git a/cloud/centurylink/clc_alert_policy.py b/cloud/centurylink/clc_alert_policy.py index dcb9d47a718..a4f07371dac 100644 --- a/cloud/centurylink/clc_alert_policy.py +++ b/cloud/centurylink/clc_alert_policy.py @@ -76,6 +76,7 @@ - python = 2.7 - requests >= 2.5.0 - clc-sdk +author: "CLC Runner (@clc-runner)" notes: - To use this module, it is required to set the below environment variables which enables access to the Centurylink Cloud diff --git a/cloud/centurylink/clc_blueprint_package.py b/cloud/centurylink/clc_blueprint_package.py index f39d8c03c7f..2b0a9774b62 100644 --- a/cloud/centurylink/clc_blueprint_package.py +++ b/cloud/centurylink/clc_blueprint_package.py @@ -55,6 +55,7 @@ - python = 2.7 - requests >= 2.5.0 - clc-sdk +author: "CLC Runner (@clc-runner)" notes: - To use this module, it is required to set the below environment variables which enables access to the Centurylink Cloud diff --git a/cloud/centurylink/clc_firewall_policy.py b/cloud/centurylink/clc_firewall_policy.py index 83d75cbdfc0..5f208bfa66a 100644 --- a/cloud/centurylink/clc_firewall_policy.py +++ b/cloud/centurylink/clc_firewall_policy.py @@ -85,6 +85,7 @@ - python = 2.7 - requests >= 2.5.0 - clc-sdk +author: "CLC Runner (@clc-runner)" notes: - To use this module, it is required to set the below environment variables which enables access to the Centurylink Cloud diff --git a/cloud/centurylink/clc_group.py b/cloud/centurylink/clc_group.py index 7564168978f..01fbbe2e67e 100644 --- a/cloud/centurylink/clc_group.py +++ b/cloud/centurylink/clc_group.py @@ -58,6 +58,7 @@ - python = 2.7 - requests >= 2.5.0 - clc-sdk +author: "CLC Runner (@clc-runner)" notes: - To use this module, it is required to set the below environment variables which enables access to the Centurylink Cloud diff --git a/cloud/centurylink/clc_loadbalancer.py b/cloud/centurylink/clc_loadbalancer.py index 076146c5c8e..7771a7ea362 100644 --- a/cloud/centurylink/clc_loadbalancer.py +++ b/cloud/centurylink/clc_loadbalancer.py @@ -78,6 +78,21 @@ required: False default: present choices: ['present', 'absent', 'port_absent', 'nodes_present', 'nodes_absent'] +requirements: + - python = 2.7 + - requests >= 2.5.0 + - clc-sdk +author: "CLC Runner (@clc-runner)" +notes: + - To use this module, it is required to set the below environment variables which enables access to the + Centurylink Cloud + - CLC_V2_API_USERNAME, the account login id for the centurylink cloud + - CLC_V2_API_PASSWORD, the account password for the centurylink cloud + - Alternatively, the module accepts the API token and account alias. The API token can be generated using the + CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login + - CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login + - CLC_ACCT_ALIAS, the account alias associated with the centurylink cloud + - Users can set CLC_V2_API_URL to specify an endpoint for pointing to a different CLC environment. ''' EXAMPLES = ''' @@ -156,20 +171,6 @@ nodes: - { 'ipAddress': '10.11.22.123', 'privatePort': 80 } state: absent -requirements: - - python = 2.7 - - requests >= 2.5.0 - - clc-sdk -notes: - - To use this module, it is required to set the below environment variables which enables access to the - Centurylink Cloud - - CLC_V2_API_USERNAME, the account login id for the centurylink cloud - - CLC_V2_API_PASSWORD, the account password for the centurylink cloud - - Alternatively, the module accepts the API token and account alias. The API token can be generated using the - CLC account login and password via the HTTP api call @ https://api.ctl.io/v2/authentication/login - - CLC_V2_API_TOKEN, the API token generated from https://api.ctl.io/v2/authentication/login - - CLC_ACCT_ALIAS, the account alias associated with the centurylink cloud - - Users can set CLC_V2_API_URL to specify an endpoint for pointing to a different CLC environment. ''' RETURN = ''' diff --git a/cloud/centurylink/clc_modify_server.py b/cloud/centurylink/clc_modify_server.py index 9683f6835df..a7ccbaefc47 100644 --- a/cloud/centurylink/clc_modify_server.py +++ b/cloud/centurylink/clc_modify_server.py @@ -80,6 +80,7 @@ - python = 2.7 - requests >= 2.5.0 - clc-sdk +author: "CLC Runner (@clc-runner)" notes: - To use this module, it is required to set the below environment variables which enables access to the Centurylink Cloud diff --git a/cloud/centurylink/clc_publicip.py b/cloud/centurylink/clc_publicip.py index ab1af0e00ca..12cfda3c5d1 100644 --- a/cloud/centurylink/clc_publicip.py +++ b/cloud/centurylink/clc_publicip.py @@ -58,6 +58,7 @@ - python = 2.7 - requests >= 2.5.0 - clc-sdk +author: "CLC Runner (@clc-runner)" notes: - To use this module, it is required to set the below environment variables which enables access to the Centurylink Cloud diff --git a/cloud/centurylink/clc_server.py b/cloud/centurylink/clc_server.py index 3202f1065fd..42cd70ae8e8 100644 --- a/cloud/centurylink/clc_server.py +++ b/cloud/centurylink/clc_server.py @@ -228,6 +228,7 @@ - python = 2.7 - requests >= 2.5.0 - clc-sdk +author: "CLC Runner (@clc-runner)" notes: - To use this module, it is required to set the below environment variables which enables access to the Centurylink Cloud diff --git a/cloud/centurylink/clc_server_snapshot.py b/cloud/centurylink/clc_server_snapshot.py index 603d03eac1b..6d6963e2097 100644 --- a/cloud/centurylink/clc_server_snapshot.py +++ b/cloud/centurylink/clc_server_snapshot.py @@ -51,6 +51,7 @@ - python = 2.7 - requests >= 2.5.0 - clc-sdk +author: "CLC Runner (@clc-runner)" notes: - To use this module, it is required to set the below environment variables which enables access to the Centurylink Cloud From b86762c1806aa7f021a4780d06db2d3937910a62 Mon Sep 17 00:00:00 2001 From: Jonathan Mainguy Date: Fri, 23 Oct 2015 13:24:39 -0400 Subject: [PATCH 0894/2522] Change show_diff to default to yes, to match what puppet agent --test is, since the rest of the options defaulted to on are grabbed from --test --- system/puppet.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/puppet.py b/system/puppet.py index 48a497c37ce..ff5333d3ee1 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -45,9 +45,9 @@ default: None show_diff: description: - - Should puppet return diffs of changes applied. Defaults to off to avoid leaking secret changes by default. + - Should puppet return diffs of changes applied. Defaults to yes, to match puppet agent --test. Change to no to avoid leaking secret changes. required: false - default: no + default: yes choices: [ "yes", "no" ] facts: description: @@ -109,7 +109,7 @@ def main(): puppetmaster=dict(required=False, default=None), manifest=dict(required=False, default=None), show_diff=dict( - default=False, aliases=['show-diff'], type='bool'), + default=True, aliases=['show-diff'], type='bool'), facts=dict(default=None), facter_basename=dict(default='ansible'), environment=dict(required=False, default=None), From f3000ebef84ebacf8d37c9bf2ae418c618f01291 Mon Sep 17 00:00:00 2001 From: James Cammarata Date: Fri, 23 Oct 2015 13:33:21 -0400 Subject: [PATCH 0895/2522] Version bump for new beta 2.0.0-0.4.beta2 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 8b31b2b4fdb..f802f1a2cdb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.0-0.3.beta1 +2.0.0-0.4.beta2 From 5160c4567c6c7cb521cc998c62e18b62ce5ef524 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Sun, 25 Oct 2015 01:06:58 +0100 Subject: [PATCH 0896/2522] Fix ZFS create This was failing due to the createparent variable being referenced but never actually loaded from properties --- system/zfs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/system/zfs.py b/system/zfs.py index 51b9db63692..343f6ea3202 100644 --- a/system/zfs.py +++ b/system/zfs.py @@ -265,6 +265,7 @@ def create(self): volsize = properties.pop('volsize', None) volblocksize = properties.pop('volblocksize', None) origin = properties.pop('origin', None) + createparent = properties.pop('createparent', None) if "@" in self.name: action = 'snapshot' elif origin: From 08d89bca9ca539612943305d94946bf511daac3e Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Sun, 25 Oct 2015 17:02:25 -0400 Subject: [PATCH 0897/2522] fixed missed passing use-sudo --- packaging/language/cpanm.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packaging/language/cpanm.py b/packaging/language/cpanm.py index 10bb66522ae..e4e8624118c 100644 --- a/packaging/language/cpanm.py +++ b/packaging/language/cpanm.py @@ -111,8 +111,7 @@ def _is_package_installed(module, name, locallib, cpanm): else: return False -def _build_cmd_line(name, from_path, notest, locallib, mirror, mirror_only, - installdeps, cpanm): +def _build_cmd_line(name, from_path, notest, locallib, mirror, mirror_only, installdeps, cpanm, use_sudo): # this code should use "%s" like everything else and just return early but not fixing all of it now. # don't copy stuff like this if from_path: @@ -174,8 +173,7 @@ def main(): if not installed: out_cpanm = err_cpanm = '' - cmd = _build_cmd_line(name, from_path, notest, locallib, mirror, - mirror_only, installdeps, cpanm) + cmd = _build_cmd_line(name, from_path, notest, locallib, mirror, mirror_only, installdeps, cpanm, use_sudo) rc_cpanm, out_cpanm, err_cpanm = module.run_command(cmd, check_rc=False) From 45249fb0420a7bfcac7fc59a92124ca2457646c3 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Mon, 26 Oct 2015 10:20:14 +0100 Subject: [PATCH 0898/2522] only call set-acl if necessary --- windows/win_owner.ps1 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/windows/win_owner.ps1 b/windows/win_owner.ps1 index eb69e744e63..0623f2b17a1 100644 --- a/windows/win_owner.ps1 +++ b/windows/win_owner.ps1 @@ -111,23 +111,23 @@ Try { $acl = Get-Acl $file.FullName If ($acl.getOwner([System.Security.Principal.SecurityIdentifier]) -ne $objUser) { + $acl.setOwner($objUser) + Set-Acl $file.FullName $acl + Set-Attr $result "changed" $true; } - $acl.setOwner($objUser) - Set-Acl $file.FullName $acl - If ($recurse) { $files = Get-ChildItem -Path $path -Force -Recurse ForEach($file in $files){ $acl = Get-Acl $file.FullName If ($acl.getOwner([System.Security.Principal.SecurityIdentifier]) -ne $objUser) { + $acl.setOwner($objUser) + Set-Acl $file.FullName $acl + Set-Attr $result "changed" $true; } - - $acl.setOwner($objUser) - Set-Acl $file.FullName $acl } } } From 64d5502fbe43c845e8133b8ab601e3e8da029919 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 26 Oct 2015 23:03:51 +0800 Subject: [PATCH 0899/2522] fix typo error fix spell error for whether --- system/iptables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/iptables.py b/system/iptables.py index d487476fb74..402146f7fc1 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -54,7 +54,7 @@ default: filter choices: [ "filter", "nat", "mangle", "raw", "security" ] state: - description: Wheter the rule should be absent or present. + description: Whether the rule should be absent or present. required: false default: present choices: [ "present", "absent" ] From 4f15e1e7e79907c8dbb1199fbbbdc77cfa4ac3ee Mon Sep 17 00:00:00 2001 From: = Date: Tue, 27 Oct 2015 06:13:42 +0000 Subject: [PATCH 0900/2522] First pass at adding win_regmerge module for handling bulk registry changes --- windows/win_regmerge.ps1 | 90 ++++++++++++++++++++++++++++++++++++++++ windows/win_regmerge.py | 70 +++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 windows/win_regmerge.ps1 create mode 100644 windows/win_regmerge.py diff --git a/windows/win_regmerge.ps1 b/windows/win_regmerge.ps1 new file mode 100644 index 00000000000..3bd1547968b --- /dev/null +++ b/windows/win_regmerge.ps1 @@ -0,0 +1,90 @@ +#!powershell +# This file is part of Ansible +# +# Copyright 2015, Jon Hawkesworth (@jhawkesworth) +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# WANT_JSON +# POWERSHELL_COMMON + +Function Convert-RegistryPath { + Param ( + [parameter(Mandatory=$True)] + [ValidateNotNullOrEmpty()]$Path + ) + + $output = $Path -replace "HKLM:", "HKLM" + $output = $output -replace "HKCU:", "HKCU" + + Return $output +} + +$params = Parse-Args $args +$result = New-Object PSObject +Set-Attr $result "changed" $False + +$path = Get-Attr -obj $params -name path -failifempty $True -resultobj $result +$compare_to = Get-Attr -obj $params -name compare_to -failifempty $False -resultobj $result + +# check it looks like a reg key, warn if key not present - will happen first time +# only accepting PS-Drive style key names (starting with HKLM etc, not HKEY_LOCAL_MACHINE etc) + +$do_comparison = $False + +If ($compare_to) { + $compare_to_key = $params.compare_to.ToString() + If (Test-Path $compare_to_key -pathType container ) { + $do_comparison = $True + } Else { + Set-Attr $result "compare_to_key_found" $False + } +} + +If ( $do_comparison -eq $True ) { + $guid = [guid]::NewGuid() + $exported_path = $env:TEMP + "\" + $guid.ToString() + 'ansible_win_regmerge.reg' + + $expanded_compare_key = Convert-RegistryPath ($compare_to_key) + + # export from the reg key location to a file + $reg_args = @("EXPORT", "$expanded_compare_key", $exported_path) + & reg.exe $reg_args + + # compare the two files + $comparison_result = Compare-Object -ReferenceObject $(Get-Content $path) -DifferenceObject $(Get-Content $exported_path) + + If (Get-Member -InputObject $comparison_result -Name "count" -MemberType Properties ) + { + # Something is different, actually do reg merge + $reg_import_args = @("IMPORT", "$path") + & reg.exe $reg_import_args + Set-Attr $result "changed" $True + Set-Attr $result "difference_count" $comparison_result.count + } Else { + Set-Attr $result "difference_count" 0 + } + + Remove-Item $exported_path + Set-Attr $result "compared" $True + +} Else { + # not comparing, merge and report changed + $reg_import_args = @("IMPORT", "$path") + & reg.exe $reg_import_args + Set-Attr $result "changed" $True + Set-Attr $result "compared" $False +} + +Exit-Json $result diff --git a/windows/win_regmerge.py b/windows/win_regmerge.py new file mode 100644 index 00000000000..53952e71d12 --- /dev/null +++ b/windows/win_regmerge.py @@ -0,0 +1,70 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Jon Hawkesworth (@jhawkesworth) +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# this is a windows documentation stub. actual code lives in the .ps1 +# file of the same name + +DOCUMENTATION = ''' +--- +module: win_regmerge +version_added: "2.0" +short_description: Merges the contents of a registry file into the windows registry +description: + - Wraps the reg.exe command to import the contents of a registry file. + - Suitable for use with registry files created using M(win_template). + - Windows registry files have a specific format and must be constructed correctly with carriage return and line feed line endings otherwise they will not be merged. + - Exported registry files often start with a Byte Order Mark which must be removed if the file is to templated using M(win_template). + - Registry file format is described here: https://support.microsoft.com/en-us/kb/310516 + - See also M(win_template), M(win_regedit) +options: + path: + description: + - The full path including file name to the registry file on the remote machine to be merged + required: true + default: no default + compare_key: + description: + - The parent key to use when comparing the contents of the registry to the contents of the file. Needs to be in HKLM or HKCU part of registry. Use a PS-Drive style path for example HKLM:\SOFTWARE not HKEY_LOCAL_MACHINE\SOFTWARE + If not supplied, or the registry key is not found, no comparison will be made, and the module will report changed. + required: false + default: no default +author: "Jon Hawkesworth (@jhawkesworth)" +notes: + - Organise your registry files so that they contain a single root registry + key if you want to use the compare_to functionality. + This module does not force registry settings to be in the state + described in the file. If registry settings have been modified externally + the module will merge the contents of the file but continue to report + differences on subsequent runs. + To force registry change, use M(win_regedit) with state=absent before + using M(win_regmerge). +''' + +EXAMPLES = ''' + # Merge in a registry file without comparing to current registry + # Note that paths using / to separate are preferred as they require less special handling than \ + win_regmerge: + path: C:/autodeploy/myCompany-settings.reg + # Compare and merge registry file + win_regmerge: + path: C:/autodeploy/myCompany-settings.reg + compare_to: HKLM:\SOFTWARE\myCompany +''' + From 7c12ed8af0d78349e76e098cef5e3d77df2ef0b6 Mon Sep 17 00:00:00 2001 From: Corwin Brown Date: Tue, 27 Oct 2015 15:02:02 -0500 Subject: [PATCH 0901/2522] Using Get-AnsibleParam conflict typo --- windows/win_uri.ps1 | 34 ++++++++++++---------------------- windows/win_uri.py | 27 ++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/windows/win_uri.ps1 b/windows/win_uri.ps1 index 9c2ddededae..cb4fda226ef 100644 --- a/windows/win_uri.ps1 +++ b/windows/win_uri.ps1 @@ -1,7 +1,7 @@ #!powershell # This file is part of Ansible # -# Copyright 2015, Corwin Brown +# Copyright 2015, Corwin Brown # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -27,32 +27,22 @@ $result = New-Object psobject @{ # Build Arguments $webrequest_opts = @{} -if (Get-Member -InputObject $params -Name url) { - $url = $params.url.ToString() - $webrequest_opts.Uri = $url -} else { - Fail-Json $result "Missing required argument: url" -} -if (Get-Member -InputObject $params -Name method) { - $method = $params.method.ToString() - $webrequest_opts.Method = $method -} +$url = Get-AnsibleParam -obj $params -name "url" -failifempty $true +$method = Get-AnsibleParam -obj $params "method" -default "GET" +$content_type = Get-AnsibleParam -obj $params -name "content_type" +$body = Get-AnsibleParam -obj $params -name "body" -if (Get-Member -InputObject $params -Name content_type) { - $content_type = $params.method.content_type.ToString() - $webrequest_opts.ContentType = $content_type -} +$webrequest_opts.Uri = $url +Set-Attr $result.win_uri "url" $url -if (Get-Member -InputObject $params -Name body) { - $body = $params.method.body.ToString() - $webrequest_opts.Body = $body -} +@webrequest_opts.Method = $method +Set-Attr $result.win_uri "method" $method -if (Get-Member -InputObject $params -Name headers) { - $headers = $params.headers - Set-Attr $result.win_uri "headers" $headers +@webrequest_opts.content_type = $content_type +Set-Attr $result.content_type "content_type" $content_type +if ($headers -ne $null) { $req_headers = @{} ForEach ($header in $headers.psobject.properties) { $req_headers.Add($header.Name, $header.Value) diff --git a/windows/win_uri.py b/windows/win_uri.py index 451c965836d..161533631f9 100644 --- a/windows/win_uri.py +++ b/windows/win_uri.py @@ -24,7 +24,7 @@ DOCUMENTATION = """ --- module: win_uri -version_added: "" +version_added: "2.0" short_description: Interacts with webservices. description: - Interacts with HTTP and HTTPS services. @@ -32,10 +32,12 @@ url: description: - HTTP or HTTPS URL in the form of (http|https)://host.domain:port/path + required: true method: description: - The HTTP Method of the request or response. default: GET + required: false choices: - GET - POST @@ -53,13 +55,17 @@ body: description: - The body of the HTTP request/response to the web service. + required: false + default: None headers: description: - Key Value pairs for headers. Example "Host: www.somesite.com" -author: Corwin Brown + required: false + default: None +author: Corwin Brown (@blakfeld) """ -Examples= """ +Examples = """ # Send a GET request and store the output: --- - name: Perform a GET and Store Output @@ -90,4 +96,19 @@ url: http://www.somesite.com method: POST body: "{ 'some': 'json' }" + +# Check if a file is available on a webserver +--- +- name: Ensure Build is Available on Fileserver + when: ensure_build + win_uri: + url: "http://www.somesite.com" + method: HEAD + headers: + test: one + another: two + register: build_check_output + until: build_check_output.StatusCode == 200 + retries: 30 + delay: 10 """ From d5673f6eb4e35ea71cde7f4de0ff40b9c36b0276 Mon Sep 17 00:00:00 2001 From: Ramunas Date: Tue, 27 Oct 2015 22:14:12 +0200 Subject: [PATCH 0902/2522] removed check for empty composer response --- packaging/language/composer.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/packaging/language/composer.py b/packaging/language/composer.py index dfb840f66b0..60b6bec5c71 100644 --- a/packaging/language/composer.py +++ b/packaging/language/composer.py @@ -133,8 +133,6 @@ def parse_out(string): return re.sub("\s+", " ", string).strip() def has_changed(string): - if string == "": - return False return "Nothing to install or update" not in string def get_available_options(module, command='install'): From 8fa10acddf20ea8d8afcc5d761a150edd038ddb3 Mon Sep 17 00:00:00 2001 From: nitzmahone Date: Tue, 27 Oct 2015 16:08:00 -0700 Subject: [PATCH 0903/2522] Strict-Mode fixes Changes to missing member handling to support running top-level script under Strict-Mode v2 (as Ansible 2.0.0 does now) --- windows/win_updates.ps1 | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/windows/win_updates.ps1 b/windows/win_updates.ps1 index d790aec6a29..3d5bc4c57c4 100644 --- a/windows/win_updates.ps1 +++ b/windows/win_updates.ps1 @@ -307,7 +307,7 @@ Function RunAsScheduledJob { $schedjob = Register-ScheduledJob @rsj_args # RunAsTask isn't available in PS3- fall back to a 2s future trigger - if($schedjob.RunAsTask) { + if($schedjob | Get-Member -Name RunAsTask) { Write-DebugLog "Starting scheduled job (PS4 method)" $schedjob.RunAsTask() } @@ -337,8 +337,8 @@ Function RunAsScheduledJob { $sw = [System.Diagnostics.Stopwatch]::StartNew() # NB: output from scheduled jobs is delayed after completion (including the sub-objects after the primary Output object is available) - While (($job.Output -eq $null -or $job.Output.job_output -eq $null) -and $sw.ElapsedMilliseconds -lt 15000) { - Write-DebugLog "Waiting for job output to be non-null..." + While (($job.Output -eq $null -or -not $job.Output.Keys.Contains('job_output')) -and $sw.ElapsedMilliseconds -lt 15000) { + Write-DebugLog "Waiting for job output to populate..." Start-Sleep -Milliseconds 500 } @@ -351,7 +351,7 @@ Function RunAsScheduledJob { DebugOutput = $job.Debug } - If ($job.Output -eq $null -or $job.Output.job_output -eq $null) { + If ($job.Output -eq $null -or -not $job.Output.Keys.Contains('job_output')) { $ret.Output = @{failed = $true; msg = "job output was lost"} } Else { @@ -404,7 +404,7 @@ $parsed_args = Parse-Args $args $true $parsed_args.psobject.properties | foreach -begin {$job_args=@{}} -process {$job_args."$($_.Name)" = $_.Value} -end {$job_args} # set the log_path for the global log function we injected earlier -$log_path = $job_args.log_path +$log_path = $job_args['log_path'] Log-Forensics From c685b3d387d6d6990356e1c4d393604a4b9178e6 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 27 Oct 2015 19:35:33 -0400 Subject: [PATCH 0904/2522] corrected version added fixes #1171 --- system/seport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/seport.py b/system/seport.py index fb1cef661a2..8a512f04d61 100644 --- a/system/seport.py +++ b/system/seport.py @@ -23,7 +23,7 @@ short_description: Manages SELinux network port type definitions description: - Manages SELinux network port type definitions. -version_added: "1.7.1" +version_added: "2.0" options: ports: description: From 9d39885d18b77d63b15753f8a7399812388b869c Mon Sep 17 00:00:00 2001 From: Olaf Kilian Date: Wed, 28 Oct 2015 10:04:55 +0100 Subject: [PATCH 0905/2522] Adapt to new dockercfg file location and structure --- cloud/docker/docker_login.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/cloud/docker/docker_login.py b/cloud/docker/docker_login.py index cf8147c692b..d84abe6fe98 100644 --- a/cloud/docker/docker_login.py +++ b/cloud/docker/docker_login.py @@ -58,7 +58,7 @@ description: - Use a custom path for the .dockercfg file required: false - default: ~/.dockercfg + default: ~/.docker/config.json docker_url: descriptions: - Refers to the protocol+hostname+port where the Docker server is hosted @@ -176,6 +176,9 @@ def update_dockercfg(self): # Create dockercfg file if it does not exist. if not os.path.exists(self.dockercfg_path): + dockercfg_path_dir = os.path.dirname(self.dockercfg_path) + if not os.path.exists(dockercfg_path_dir): + os.makedirs(dockercfg_path_dir) open(self.dockercfg_path, "w") self.log.append("Created new Docker config file at %s" % self.dockercfg_path) else: @@ -186,9 +189,11 @@ def update_dockercfg(self): docker_config = json.load(open(self.dockercfg_path, "r")) except ValueError: docker_config = dict() - if not docker_config.has_key(self.registry): - docker_config[self.registry] = dict() - docker_config[self.registry] = dict( + if not docker_config.has_key("auths"): + docker_config["auths"] = dict() + if not docker_config["auths"].has_key(self.registry): + docker_config["auths"][self.registry] = dict() + docker_config["auths"][self.registry] = dict( auth = base64.b64encode(self.username + b':' + self.password), email = self.email ) @@ -220,7 +225,7 @@ def main(): password = dict(required=True), email = dict(required=False, default=None), reauth = dict(required=False, default=False, type='bool'), - dockercfg_path = dict(required=False, default='~/.dockercfg'), + dockercfg_path = dict(required=False, default='~/.docker/config.json'), docker_url = dict(default='unix://var/run/docker.sock'), timeout = dict(default=10, type='int') ), From 988be3458d9b699f7942e64211a364800cc446b1 Mon Sep 17 00:00:00 2001 From: Olaf Kilian Date: Wed, 28 Oct 2015 10:13:35 +0100 Subject: [PATCH 0906/2522] Rework change detection --- cloud/docker/docker_login.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/cloud/docker/docker_login.py b/cloud/docker/docker_login.py index d84abe6fe98..15216595fad 100644 --- a/cloud/docker/docker_login.py +++ b/cloud/docker/docker_login.py @@ -162,13 +162,9 @@ def login(self): # Get status from registry response. if self.response.has_key("Status"): self.log.append(self.response["Status"]) - if self.response["Status"] == "Login Succeeded": - self.changed = True - else: - self.log.append("Already Authentificated") - # Update the dockercfg if changed but not failed. - if self.has_changed() and not self.module.check_mode: + # Update the dockercfg if not in check mode. + if not self.module.check_mode: self.update_dockercfg() # This is what the underlaying docker-py unfortunately doesn't do (yet). @@ -182,9 +178,9 @@ def update_dockercfg(self): open(self.dockercfg_path, "w") self.log.append("Created new Docker config file at %s" % self.dockercfg_path) else: - self.log.append("Updated existing Docker config file at %s" % self.dockercfg_path) + self.log.append("Found existing Docker config file at %s" % self.dockercfg_path) - # Get existing dockercfg into a dict. + # Build a dict for the existing dockercfg. try: docker_config = json.load(open(self.dockercfg_path, "r")) except ValueError: @@ -193,16 +189,22 @@ def update_dockercfg(self): docker_config["auths"] = dict() if not docker_config["auths"].has_key(self.registry): docker_config["auths"][self.registry] = dict() - docker_config["auths"][self.registry] = dict( + + # Calculate docker credentials based on current parameters. + new_docker_config = dict( auth = base64.b64encode(self.username + b':' + self.password), email = self.email ) - # Write updated dockercfg to dockercfg file. - try: - json.dump(docker_config, open(self.dockercfg_path, "w"), indent=4, sort_keys=True) - except Exception as e: - self.module.fail_json(msg="failed to write auth details to file", error=repr(e)) + # Update config if persisted credentials differ from current credentials. + if new_docker_config != docker_config["auths"][self.registry]: + docker_config["auths"][self.registry] = new_docker_config + try: + json.dump(docker_config, open(self.dockercfg_path, "w"), indent=4, sort_keys=True) + except Exception as e: + self.module.fail_json(msg="failed to write auth details to file", error=repr(e)) + self.log.append("Updated Docker config with new credentials.") + self.changed = True # Compatible to docker-py auth.decode_docker_auth() def encode_docker_auth(self, auth): From 98b21ee7f3d6edf082eeb329f53f1b7dafd23312 Mon Sep 17 00:00:00 2001 From: Olaf Kilian Date: Wed, 28 Oct 2015 10:14:54 +0100 Subject: [PATCH 0907/2522] Improve registry key parity between clients * Don't extract hostname part from docker_url since this leads to docker CLI client not recognizing Docker Hub credentials set by docker_login module anymore (looks for the full URL as a key). --- cloud/docker/docker_login.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/cloud/docker/docker_login.py b/cloud/docker/docker_login.py index 15216595fad..c00dc3f900d 100644 --- a/cloud/docker/docker_login.py +++ b/cloud/docker/docker_login.py @@ -84,7 +84,7 @@ - name: login to private Docker remote registry and force reauthentification docker_login: - registry: https://your.private.registry.io/v1/ + registry: your.private.registry.io username: yourself password: secrets3 reauth: yes @@ -140,10 +140,6 @@ def login(self): if self.reauth: self.log.append("Enforcing reauthentification") - # Extract hostname part from self.registry if url was specified. - registry_url = urlparse(self.registry) - self.registry = registry_url.netloc or registry_url.path - # Connect to registry and login if not already logged in or reauth is enforced. try: self.response = self.client.login( From fdffa554969b236ae0dbe83b30e26a938f8bc007 Mon Sep 17 00:00:00 2001 From: Matteo Acerbi Date: Wed, 28 Oct 2015 12:15:24 +0100 Subject: [PATCH 0908/2522] Fix docs for ec2_vpc_route_table The documentation pointed to vpc_peering_connection, not vpc_peering_connection_id. --- cloud/amazon/ec2_vpc_route_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index 4cbbef5dfb1..829dda62d3e 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -39,7 +39,7 @@ default: null routes: description: - - "List of routes in the route table. Routes are specified as dicts containing the keys 'dest' and one of 'gateway_id', 'instance_id', 'interface_id', or 'vpc_peering_connection'. If 'gateway_id' is specified, you can refer to the VPC's IGW by using the value 'igw'." + - "List of routes in the route table. Routes are specified as dicts containing the keys 'dest' and one of 'gateway_id', 'instance_id', 'interface_id', or 'vpc_peering_connection_id'. If 'gateway_id' is specified, you can refer to the VPC's IGW by using the value 'igw'." required: true state: description: From 4bc04cd9975e76302f2bcc72c569ae18dd5536fa Mon Sep 17 00:00:00 2001 From: Corwin Brown Date: Wed, 28 Oct 2015 11:55:19 -0500 Subject: [PATCH 0909/2522] bug fixes --- windows/win_uri.ps1 | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/windows/win_uri.ps1 b/windows/win_uri.ps1 index cb4fda226ef..471bceab723 100644 --- a/windows/win_uri.ps1 +++ b/windows/win_uri.ps1 @@ -31,16 +31,17 @@ $webrequest_opts = @{} $url = Get-AnsibleParam -obj $params -name "url" -failifempty $true $method = Get-AnsibleParam -obj $params "method" -default "GET" $content_type = Get-AnsibleParam -obj $params -name "content_type" +$headers = Get-AnsibleParam -obj $params -name "headers" $body = Get-AnsibleParam -obj $params -name "body" $webrequest_opts.Uri = $url Set-Attr $result.win_uri "url" $url -@webrequest_opts.Method = $method +$webrequest_opts.Method = $method Set-Attr $result.win_uri "method" $method -@webrequest_opts.content_type = $content_type -Set-Attr $result.content_type "content_type" $content_type +$webrequest_opts.ContentType = $content_type +Set-Attr $result.win_uri "content_type" $content_type if ($headers -ne $null) { $req_headers = @{} From 50320bfa8fdf6dd459665357b493628939915051 Mon Sep 17 00:00:00 2001 From: Greg DeKoenigsberg Date: Wed, 28 Oct 2015 14:27:58 -0400 Subject: [PATCH 0910/2522] Remove @lorin from list of reviewers --- REVIEWERS.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/REVIEWERS.md b/REVIEWERS.md index 73ebdeb58c6..06263169ec7 100644 --- a/REVIEWERS.md +++ b/REVIEWERS.md @@ -43,8 +43,7 @@ Openstack: @emonty @shrews @dguerri @juliakreger @j2sol @rcarrillocruz Windows: @trondhindenes @petemounce @elventear @smadam813 @jhawkesworth @angstwad @sivel @chrishoffman @cchurch -AWS: @jsmartin @scicoin-project @tombamford @garethr @lorin @jarv @jsdalton @silviud @adq @zbal @zeekin @willthames @lwade @carsongee @defionscode -@tastychutney @bpennypacker @loia +AWS: @jsmartin @scicoin-project @tombamford @garethr @jarv @jsdalton @silviud @adq @zbal @zeekin @willthames @lwade @carsongee @defionscode @tastychutney @bpennypacker @loia Docker: @cove @joshuaconner @softzilla @smashwilson From f6ea32b9ec26e10f8bc52ffa7a7ea03f84178bbf Mon Sep 17 00:00:00 2001 From: Trond Hindenes Date: Wed, 28 Oct 2015 21:52:32 +0100 Subject: [PATCH 0911/2522] Various improvements to win_package --- windows/win_package.ps1 | 47 +++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/windows/win_package.ps1 b/windows/win_package.ps1 index 6cdc6bf6e5c..53b2ab602a3 100644 --- a/windows/win_package.ps1 +++ b/windows/win_package.ps1 @@ -100,7 +100,15 @@ Function Throw-TerminatingError [System.Management.Automation.ErrorRecord] $ErrorRecord ) - $exception = new-object "System.InvalidOperationException" $Message,$ErrorRecord.Exception + if ($errorRecord) + { + $exception = new-object "System.InvalidOperationException" $Message,$ErrorRecord.Exception + } + Else + { + $exception = new-object "System.InvalidOperationException" $Message + } + $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,"MachineStateIncorrect","InvalidOperation",$null throw $errorRecord } @@ -186,7 +194,19 @@ Function Validate-StandardArguments try { Trace-Message "Parsing $ProductId as an identifyingNumber" - $identifyingNumber = "{{{0}}}" -f [Guid]::Parse($ProductId).ToString().ToUpper() + $TestGuid = [system.guid]::NewGuid() + #Check to see if the productid is a guid + if ([guid]::TryParse($ProductId, [ref]$TestGuid)) + { + $identifyingNumber = "{{{0}}}" -f [Guid]::Parse($ProductId).ToString().ToUpper() + Trace-Message "Parsed $ProductId as $identifyingNumber (is guid)" + } + Else + { + $identifyingNumber = $ProductId + Trace-Message "Parsed $ProductId as $identifyingNumber (is not guid)" + } + Trace-Message "Parsed $ProductId as $identifyingNumber" } catch @@ -1287,24 +1307,19 @@ Else } catch { - $errormsg = $_[0].exception + $errormsg = $_ + Fail-Json -obj $result -message $errormsg.ToString() } - if ($errormsg) + #Check if DSC thinks the computer needs a reboot: + if ((get-variable DSCMachinestatus -Scope Global -ea 0) -and ($global:DSCMachineStatus -eq 1)) { - Fail-Json -obj $result -message $errormsg.ToString() + Set-Attr $result "restart_required" $true } - Else - { - #Check if DSC thinks the computer needs a reboot: - if ($global:DSCMachineStatus -eq 1) - { - Set-Attr $result "restart_required" $true - } - #Set-TargetResource did its job. We can assume a change has happened - Set-Attr $result "changed" $true - Exit-Json -obj $result - } + #Set-TargetResource did its job. We can assume a change has happened + Set-Attr $result "changed" $true + Exit-Json -obj $result + } From 83c0bfbe3f2e5bdf817001155fb936dfb4d22fad Mon Sep 17 00:00:00 2001 From: Ramunas Dronga Date: Thu, 29 Oct 2015 23:21:46 +0200 Subject: [PATCH 0912/2522] added constraint for space in composer command --- packaging/language/composer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packaging/language/composer.py b/packaging/language/composer.py index 60b6bec5c71..af46a1849cc 100644 --- a/packaging/language/composer.py +++ b/packaging/language/composer.py @@ -170,6 +170,9 @@ def main(): # Get composer command with fallback to default command = module.params['command'] + if re.search(r"\s", command): + module.fail_json(msg="Use the 'arguments' param for passing arguments with the 'command'") + arguments = module.params['arguments'] available_options = get_available_options(module=module, command=command) From 3099469b7f067f7b9884ea663e66970cfdf24319 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 29 Oct 2015 21:04:28 -0400 Subject: [PATCH 0913/2522] fixed typos in cryptab, thanks @timw fixes #1176 --- system/crypttab.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/crypttab.py b/system/crypttab.py index 2a8cdc9d36c..d483e339631 100755 --- a/system/crypttab.py +++ b/system/crypttab.py @@ -252,18 +252,18 @@ def _line_valid(self, line): def _split_line(self, line): fields = line.split() try: - field2 = field[2] + field2 = fields[2] except IndexError: field2 = None try: - field3 = field[3] + field3 = fields[3] except IndexError: field3 = None return (fields[0], fields[1], field2, - fields3) + field3) def remove(self): self.line, self.name, self.backing_device = '', None, None From a0965ff93420d2a2e5119581a87ed5f966562197 Mon Sep 17 00:00:00 2001 From: Ramunas Dronga Date: Fri, 30 Oct 2015 12:24:42 +0200 Subject: [PATCH 0914/2522] fixed composer usage example --- packaging/language/composer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packaging/language/composer.py b/packaging/language/composer.py index af46a1849cc..95b0eb3a940 100644 --- a/packaging/language/composer.py +++ b/packaging/language/composer.py @@ -110,7 +110,8 @@ - composer: command=install working_dir=/path/to/project - composer: - command: "require my/package" + command: "require" + arguments: "my/package" working_dir: "/path/to/project" # Clone project and install with all dependencies @@ -172,7 +173,7 @@ def main(): command = module.params['command'] if re.search(r"\s", command): module.fail_json(msg="Use the 'arguments' param for passing arguments with the 'command'") - + arguments = module.params['arguments'] available_options = get_available_options(module=module, command=command) From c4b4c412a2ff3caafc9ac368351c4e8dc209975f Mon Sep 17 00:00:00 2001 From: Corwin Brown Date: Fri, 30 Oct 2015 09:47:30 -0500 Subject: [PATCH 0915/2522] Switching to Get-AnsibleParam Switching to Win_Unzip --- windows/win_unzip.ps1 | 54 +++++++++++++------------------------------ 1 file changed, 16 insertions(+), 38 deletions(-) diff --git a/windows/win_unzip.ps1 b/windows/win_unzip.ps1 index 1214bbaa89e..ed90dce3659 100644 --- a/windows/win_unzip.ps1 +++ b/windows/win_unzip.ps1 @@ -26,55 +26,33 @@ $result = New-Object psobject @{ changed = $false } -If (Get-Member -InputObject $params -Name creates) { +$creates = Get-AnsibleParam -obj $params -name "creates" +If ($creates -ne $null) { If (Test-Path $params.creates) { Exit-Json $result "The 'creates' file or directory already exists." } - } -If (Get-Member -InputObject $params -Name src) { - $src = $params.src.toString() - - If (-Not (Test-Path -path $src)){ - Fail-Json $result "src file: $src does not exist." - } - - $ext = [System.IO.Path]::GetExtension($src) -} -Else { - Fail-Json $result "missing required argument: src" +$src = Get-AnsibleParam -obj $params -name "src" -failifempty $true +If (-Not (Test-Path -path $src)){ + Fail-Json $result "src file: $src does not exist." } -If (-Not($params.dest -eq $null)) { - $dest = $params.dest.toString() +$ext = [System.IO.Path]::GetExtension($src) - If (-Not (Test-Path $dest -PathType Container)){ - Try{ - New-Item -itemtype directory -path $dest - } - Catch { - Fail-Json $result "Error creating $dest directory" - } - } -} -Else { - Fail-Json $result "missing required argument: dest" -} -If (Get-Member -InputObject $params -Name recurse) { - $recurse = ConvertTo-Bool ($params.recurse) -} -Else { - $recurse = $false +$dest = Get-AnsibleParam -obj $params -name "dest" -failifempty $true +If (-Not (Test-Path $dest -PathType Container)){ + Try{ + New-Item -itemtype directory -path $dest + } + Catch { + Fail-Json $result "Error creating $dest directory" + } } -If (Get-Member -InputObject $params -Name rm) { - $rm = ConvertTo-Bool ($params.rm) -} -Else { - $rm = $false -} +$recurse = ConvertTo-Bool (Get-AnsibleParam -obj $params -name "recurse" -default "false") +$rm = ConvertTo-Bool (Get-AnsibleParam -obj $params -name "rm" -default "false") If ($ext -eq ".zip" -And $recurse -eq $false) { Try { From c648edfbae11aeb129084b3cd93dd8f439a3b027 Mon Sep 17 00:00:00 2001 From: Romain Brucker Date: Fri, 30 Oct 2015 11:29:05 -0500 Subject: [PATCH 0916/2522] Adding comment support for iptables module --- system/iptables.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/system/iptables.py b/system/iptables.py index 402146f7fc1..7a2b7f9c15d 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -199,6 +199,10 @@ rule also specifies one of the following protocols: tcp, udp, dccp or sctp." required: false + comment: + description: + - "This specifies a comment that will be added to the rule" + required: false ''' EXAMPLES = ''' @@ -207,7 +211,7 @@ become: yes # Forward port 80 to 8600 -- iptables: table=nat chain=PREROUTING in_interface=eth0 protocol=tcp match=tcp destination_port=80 jump=REDIRECT to_ports=8600 +- iptables: table=nat chain=PREROUTING in_interface=eth0 protocol=tcp match=tcp destination_port=80 jump=REDIRECT to_ports=8600 comment="Redirect web traffic to port 8600" become: yes ''' @@ -220,6 +224,11 @@ def append_param(rule, param, flag, is_list): if param is not None: rule.extend([flag, param]) +def append_comm(rule, param): + if param: + rule.extend(['-m']) + rule.extend(['comment']) + def construct_rule(params): rule = [] @@ -236,6 +245,8 @@ def construct_rule(params): append_param(rule, params['source_port'], '--source-port', False) append_param(rule, params['destination_port'], '--destination-port', False) append_param(rule, params['to_ports'], '--to-ports', False) + append_comm(rule, params['comment']) + append_param(rule, params['comment'], '--comment', False) return rule @@ -284,6 +295,7 @@ def main(): source_port=dict(required=False, default=None, type='str'), destination_port=dict(required=False, default=None, type='str'), to_ports=dict(required=False, default=None, type='str'), + comment=dict(required=False, default=None, type='str'), ), ) args = dict( From 54b6528327fecaf131fe950dfcfe59c6e590f9b0 Mon Sep 17 00:00:00 2001 From: Corwin Brown Date: Fri, 30 Oct 2015 13:23:14 -0500 Subject: [PATCH 0917/2522] Adding Site_ID param --- windows/win_iis_website.ps1 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/windows/win_iis_website.ps1 b/windows/win_iis_website.ps1 index 26a8df12730..e2c0d4ed325 100644 --- a/windows/win_iis_website.ps1 +++ b/windows/win_iis_website.ps1 @@ -37,6 +37,7 @@ If (($state -ne $FALSE) -and ($state -ne 'started') -and ($state -ne 'stopped') # Path parameter $physical_path = Get-Attr $params "physical_path" $FALSE; +$site_id = Get-Attr $params "site_id" $FALSE; # Application Pool Parameter $application_pool = Get-Attr $params "application_pool" $FALSE; @@ -91,6 +92,10 @@ Try { $site_parameters.ApplicationPool = $application_pool } + If ($site_id) { + $site_parameters.ID = $site_id + } + If ($bind_port) { $site_parameters.Port = $bind_port } From 360734ec09a2d4028f960509263f40bddb49b403 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Sat, 31 Oct 2015 14:31:08 -0400 Subject: [PATCH 0918/2522] whitespace fixes --- system/iptables.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/system/iptables.py b/system/iptables.py index 7a2b7f9c15d..726a5d7e9ac 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - +# # (c) 2015, Linus Unnebäck # # This file is part of Ansible @@ -226,8 +226,8 @@ def append_param(rule, param, flag, is_list): def append_comm(rule, param): if param: - rule.extend(['-m']) - rule.extend(['comment']) + rule.extend(['-m']) + rule.extend(['comment']) def construct_rule(params): @@ -295,7 +295,7 @@ def main(): source_port=dict(required=False, default=None, type='str'), destination_port=dict(required=False, default=None, type='str'), to_ports=dict(required=False, default=None, type='str'), - comment=dict(required=False, default=None, type='str'), + comment=dict(required=False, default=None, type='str'), ), ) args = dict( From ea5af4c27adede4925b477d7aa4bebacd72f1b55 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 1 Nov 2015 10:50:58 +0900 Subject: [PATCH 0919/2522] Revert "Change show_diff to default to yes" This was originally to match what puppet agent --test is, since the rest of the options defaulted to on are grabbed from --test. However, some security concerns have since been raised - namely that since this is not the same invocation as --test but instead a remote orchestration of puppet, the fact that passwords leak into the diff is a dangerous default. This reverts commit b86762c1806aa7f021a4780d06db2d3937910a62. --- system/puppet.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/puppet.py b/system/puppet.py index ff5333d3ee1..48a497c37ce 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -45,9 +45,9 @@ default: None show_diff: description: - - Should puppet return diffs of changes applied. Defaults to yes, to match puppet agent --test. Change to no to avoid leaking secret changes. + - Should puppet return diffs of changes applied. Defaults to off to avoid leaking secret changes by default. required: false - default: yes + default: no choices: [ "yes", "no" ] facts: description: @@ -109,7 +109,7 @@ def main(): puppetmaster=dict(required=False, default=None), manifest=dict(required=False, default=None), show_diff=dict( - default=True, aliases=['show-diff'], type='bool'), + default=False, aliases=['show-diff'], type='bool'), facts=dict(default=None), facter_basename=dict(default='ansible'), environment=dict(required=False, default=None), From d589a2ea1284461bb12b86d67ec31eb3d26d827c Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sun, 1 Nov 2015 17:18:23 +0100 Subject: [PATCH 0920/2522] cloudstack: cs_portforward: fix example does not match description --- cloud/cloudstack/cs_portforward.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cloud/cloudstack/cs_portforward.py b/cloud/cloudstack/cs_portforward.py index 555f30e54b3..4d091f687d9 100644 --- a/cloud/cloudstack/cs_portforward.py +++ b/cloud/cloudstack/cs_portforward.py @@ -135,7 +135,6 @@ public_port: 53 private_port: 53 protocol: udp - open_firewall: true # remove ssh port forwarding - local_action: From 8e0a2ed729901f0bd7b950883d805861883e2fbb Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Sun, 1 Nov 2015 12:13:32 -0800 Subject: [PATCH 0921/2522] Make the pkg/name param into a list so that changes to make changes to _squash_items easier --- packaging/os/pkgng.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packaging/os/pkgng.py b/packaging/os/pkgng.py index fe0f2687b31..0eafcb6d00b 100644 --- a/packaging/os/pkgng.py +++ b/packaging/os/pkgng.py @@ -268,7 +268,7 @@ def main(): module = AnsibleModule( argument_spec = dict( state = dict(default="present", choices=["present","absent"], required=False), - name = dict(aliases=["pkg"], required=True), + name = dict(aliases=["pkg"], required=True, type='list'), cached = dict(default=False, type='bool'), annotation = dict(default="", required=False), pkgsite = dict(default="", required=False), @@ -279,7 +279,7 @@ def main(): p = module.params - pkgs = p["name"].split(",") + pkgs = p["name"] changed = False msgs = [] From ed1cf0ecc218d67c44c64f91762e464e998c35da Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Sun, 1 Nov 2015 19:42:04 -0500 Subject: [PATCH 0922/2522] corrected extension in module spec fixes #1190 --- packaging/language/maven_artifact.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/language/maven_artifact.py b/packaging/language/maven_artifact.py index 658ad7f1173..256cdb39e6b 100644 --- a/packaging/language/maven_artifact.py +++ b/packaging/language/maven_artifact.py @@ -296,7 +296,7 @@ def main(): artifact_id = dict(default=None), version = dict(default=None), classifier = dict(default=None), - extension = dict(default=None, required=True), + extension = dict(default='jar'), repository_url = dict(default=None), username = dict(default=None), password = dict(default=None), From b0278c1f6a0b84c0a91b020c5a5405473924fe1d Mon Sep 17 00:00:00 2001 From: Daniel Vigueras Date: Mon, 2 Nov 2015 10:36:58 +0100 Subject: [PATCH 0923/2522] Add conntrack module ctstate support to iptables --- system/iptables.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/system/iptables.py b/system/iptables.py index 726a5d7e9ac..59dc187c543 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -203,6 +203,12 @@ description: - "This specifies a comment that will be added to the rule" required: false + ctstate: + description: + - "ctstate is a comma separated list of the connection states to match in + the conntrack module. Possible states are: 'INVALID', 'NEW', + 'ESTABLISHED', 'RELATED', 'UNTRACKED', 'SNAT', 'DNAT'" + required: false ''' EXAMPLES = ''' @@ -213,6 +219,10 @@ # Forward port 80 to 8600 - iptables: table=nat chain=PREROUTING in_interface=eth0 protocol=tcp match=tcp destination_port=80 jump=REDIRECT to_ports=8600 comment="Redirect web traffic to port 8600" become: yes + +# Allow related and established connections +- iptables: chain=INPUT ctstate=ESTABLISHED,RELATED jump=ACCEPT + become: yes ''' @@ -230,6 +240,12 @@ def append_comm(rule, param): rule.extend(['comment']) +def append_conntrack(rule, param): + if param: + rule.extend(['-m']) + rule.extend(['conntrack']) + + def construct_rule(params): rule = [] append_param(rule, params['protocol'], '-p', False) @@ -247,6 +263,8 @@ def construct_rule(params): append_param(rule, params['to_ports'], '--to-ports', False) append_comm(rule, params['comment']) append_param(rule, params['comment'], '--comment', False) + append_conntrack(rule, params['ctstate']) + append_param(rule, params['ctstate'], '--ctstate', False) return rule @@ -296,6 +314,7 @@ def main(): destination_port=dict(required=False, default=None, type='str'), to_ports=dict(required=False, default=None, type='str'), comment=dict(required=False, default=None, type='str'), + ctstate=dict(required=False, default=None, type='str'), ), ) args = dict( From 257696c0ed4e9f0c890ee83b0bbf4a9562d230ed Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 2 Nov 2015 09:31:40 -0500 Subject: [PATCH 0924/2522] switched to 'support check mode' as it does not make changes and gathers information that might be needed for other tasks to check --- network/ipify_facts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/ipify_facts.py b/network/ipify_facts.py index adcf5e4702b..8f509dd278a 100644 --- a/network/ipify_facts.py +++ b/network/ipify_facts.py @@ -82,7 +82,7 @@ def main(): argument_spec = dict( api_url = dict(default='https://api.ipify.org'), ), - supports_check_mode=False + supports_check_mode=True, ) ipify_facts = IpifyFacts().run() From 0b9e9fd852db09091793adf9167a29adb2addaf0 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Sun, 1 Nov 2015 12:58:20 -0800 Subject: [PATCH 0925/2522] Move existing check for root before we make expensive network calls --- packaging/os/dnf.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index 58cf4bcac95..50c86fc4f62 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -234,9 +234,6 @@ def _mark_package_install(module, base, pkg_spec): def ensure(module, base, state, names): - if not util.am_i_root(): - module.fail_json(msg="This command has to be run under the root user.") - if names == ['*'] and state == 'latest': base.upgrade_all() else: @@ -330,12 +327,20 @@ def main(): mutually_exclusive=[['name', 'list']], supports_check_mode=True) params = module.params - base = _base( - module, params['conf_file'], params['disable_gpg_check'], - params['disablerepo'], params['enablerepo']) if params['list']: + base = _base( + module, params['conf_file'], params['disable_gpg_check'], + params['disablerepo'], params['enablerepo']) list_items(module, base, params['list']) else: + # Note: base takes a long time to run so we want to check for failure + # before running it. + if not util.am_i_root(): + module.fail_json(msg="This command has to be run under the root user.") + base = _base( + module, params['conf_file'], params['disable_gpg_check'], + params['disablerepo'], params['enablerepo']) + ensure(module, base, params['state'], params['name']) From e033e6e6020ef0c033df279488d4160f0aa6f4df Mon Sep 17 00:00:00 2001 From: Javier Palacios Date: Mon, 2 Nov 2015 16:43:46 +0000 Subject: [PATCH 0926/2522] BUGFIX: misnamed function name breaks check mode --- database/postgresql/postgresql_ext.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/database/postgresql/postgresql_ext.py b/database/postgresql/postgresql_ext.py index 07ed48e9d03..3f4a745846c 100644 --- a/database/postgresql/postgresql_ext.py +++ b/database/postgresql/postgresql_ext.py @@ -165,9 +165,9 @@ def main(): try: if module.check_mode: if state == "absent": - changed = not db_exists(cursor, ext) + changed = not ext_exists(cursor, ext) elif state == "present": - changed = db_exists(cursor, ext) + changed = ext_exists(cursor, ext) module.exit_json(changed=changed,ext=ext) if state == "absent": From f5ed8d0c6e7e85976ae77caff660bfabde199bba Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 2 Nov 2015 12:11:38 -0500 Subject: [PATCH 0927/2522] made ctstate accept lists --- system/iptables.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/system/iptables.py b/system/iptables.py index 59dc187c543..29010b730e5 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -205,9 +205,8 @@ required: false ctstate: description: - - "ctstate is a comma separated list of the connection states to match in - the conntrack module. Possible states are: 'INVALID', 'NEW', - 'ESTABLISHED', 'RELATED', 'UNTRACKED', 'SNAT', 'DNAT'" + - "ctstate is a list of the connection states to match in the conntrack module. + Possible states are: 'INVALID', 'NEW', 'ESTABLISHED', 'RELATED', 'UNTRACKED', 'SNAT', 'DNAT'" required: false ''' @@ -264,7 +263,7 @@ def construct_rule(params): append_comm(rule, params['comment']) append_param(rule, params['comment'], '--comment', False) append_conntrack(rule, params['ctstate']) - append_param(rule, params['ctstate'], '--ctstate', False) + append_param(rule, ','.join(params['ctstate']), '--ctstate', False) return rule @@ -314,7 +313,7 @@ def main(): destination_port=dict(required=False, default=None, type='str'), to_ports=dict(required=False, default=None, type='str'), comment=dict(required=False, default=None, type='str'), - ctstate=dict(required=False, default=None, type='str'), + ctstate=dict(required=False, default=None, type='list'), ), ) args = dict( From 80f198a82f90c171d9d0db1ccaa3a3738f4ea400 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 2 Nov 2015 14:19:11 -0500 Subject: [PATCH 0928/2522] added missing : to docs --- cloud/centurylink/clc_firewall_policy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/centurylink/clc_firewall_policy.py b/cloud/centurylink/clc_firewall_policy.py index 5f208bfa66a..b37123b4ec0 100644 --- a/cloud/centurylink/clc_firewall_policy.py +++ b/cloud/centurylink/clc_firewall_policy.py @@ -135,7 +135,7 @@ returned: success type: boolean sample: True -firewall_policy_id +firewall_policy_id: description: The fire wall policy id returned: success type: string From 228b8707b7da40f7fe6c75c07f783960198f6932 Mon Sep 17 00:00:00 2001 From: Corwin Brown Date: Mon, 2 Nov 2015 14:08:00 -0600 Subject: [PATCH 0929/2522] Added Error messages to the Try/Catch blocks --- windows/win_unzip.ps1 | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/windows/win_unzip.ps1 b/windows/win_unzip.ps1 index ed90dce3659..0e5485dddb9 100644 --- a/windows/win_unzip.ps1 +++ b/windows/win_unzip.ps1 @@ -19,6 +19,7 @@ # WANT_JSON # POWERSHELL_COMMON + $params = Parse-Args $args; $result = New-Object psobject @{ @@ -47,7 +48,8 @@ If (-Not (Test-Path $dest -PathType Container)){ New-Item -itemtype directory -path $dest } Catch { - Fail-Json $result "Error creating $dest directory" + $err_msg = $_.Exception.Message + Fail-Json $result "Error creating $dest directory! Msg: $err_msg" } } @@ -63,7 +65,8 @@ If ($ext -eq ".zip" -And $recurse -eq $false) { $result.changed = $true } Catch { - Fail-Json $result "Error unzipping $src to $dest" + $err_msg = $_.Exception.Message + Fail-Json $result "Error unzipping $src to $dest! Msg: $err_msg" } } # Requires PSCX @@ -107,11 +110,12 @@ Else { } } Catch { + $err_msg = $_.Exception.Message If ($recurse) { - Fail-Json $result "Error recursively expanding $src to $dest" + Fail-Json $result "Error recursively expanding $src to $dest! Msg: $err_msg" } Else { - Fail-Json $result "Error expanding $src to $dest" + Fail-Json $result "Error expanding $src to $dest! Msg: $err_msg" } } } From b1e0e7eae0026e6b2b21cc7779fd3da1d0db9686 Mon Sep 17 00:00:00 2001 From: Kirill Kozlov Date: Mon, 2 Nov 2015 23:15:44 +0300 Subject: [PATCH 0930/2522] Fix broken examples in docs --- packaging/language/maven_artifact.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packaging/language/maven_artifact.py b/packaging/language/maven_artifact.py index 256cdb39e6b..dd19b67a5bf 100644 --- a/packaging/language/maven_artifact.py +++ b/packaging/language/maven_artifact.py @@ -99,11 +99,11 @@ ''' EXAMPLES = ''' -# Download the latest version of the commons-collections artifact from Maven Central -- maven_artifact: group_id=org.apache.commons artifact_id=commons-collections dest=/tmp/commons-collections-latest.jar +# Download the latest version of the JUnit framework artifact from Maven Central +- maven_artifact: group_id=junit artifact_id=junit dest=/tmp/junit-latest.jar -# Download Apache Commons-Collections 3.2 from Maven Central -- maven_artifact: group_id=org.apache.commons artifact_id=commons-collections version=3.2 dest=/tmp/commons-collections-3.2.jar +# Download JUnit 4.11 from Maven Central +- maven_artifact: group_id=junit artifact_id=junit version=4.11 dest=/tmp/junit-4.11.jar # Download an artifact from a private repository requiring authentication - maven_artifact: group_id=com.company artifact_id=library-name repository_url=https://repo.company.com/maven username=user password=pass dest=/tmp/library-name-latest.jar From a21d935e66da40d6184b0dc12d3e894917279476 Mon Sep 17 00:00:00 2001 From: wimnat Date: Tue, 3 Nov 2015 01:03:31 +0000 Subject: [PATCH 0931/2522] Prevent ec2_remote_facts from failing when listing a terminated instance --- cloud/amazon/ec2_remote_facts.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_remote_facts.py b/cloud/amazon/ec2_remote_facts.py index cb92ccba74d..cf54fa0274d 100644 --- a/cloud/amazon/ec2_remote_facts.py +++ b/cloud/amazon/ec2_remote_facts.py @@ -76,6 +76,12 @@ def get_instance_info(instance): interfaces = [] for interface in instance.interfaces: interfaces.append({ 'id': interface.id, 'mac_address': interface.mac_address }.copy()) + + # If an instance is terminated, sourceDestCheck is no longer returned + try: + source_dest_check = instance.sourceDestCheck + except AttributeError: + source_dest_check = None instance_info = { 'id': instance.id, 'kernel': instance.kernel, @@ -90,7 +96,7 @@ def get_instance_info(instance): 'ramdisk': instance.ramdisk, 'tags': instance.tags, 'key_name': instance.key_name, - 'source_destination_check': instance.sourceDestCheck, + 'source_destination_check': source_dest_check, 'image_id': instance.image_id, 'groups': groups, 'interfaces': interfaces, From 9fb6054500a9841ed4aa73733581fe248aaf7bf1 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 2 Nov 2015 21:49:46 -0500 Subject: [PATCH 0932/2522] avoid index error on empty list, key being true means its not None nor [] fixes #13009 --- system/known_hosts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/known_hosts.py b/system/known_hosts.py index 7592574d4e7..b68e85e0e77 100644 --- a/system/known_hosts.py +++ b/system/known_hosts.py @@ -92,8 +92,8 @@ def enforce_state(module, params): #Find the ssh-keygen binary sshkeygen = module.get_bin_path("ssh-keygen",True) - #trailing newline in files gets lost, so re-add if necessary - if key is not None and key[-1]!='\n': + # Trailing newline in files gets lost, so re-add if necessary + if key and key[-1] != '\n': key+='\n' if key is None and state != "absent": From 975d7952b956ff004b5e97ecf2a52a86a034f567 Mon Sep 17 00:00:00 2001 From: Kenny Gryp Date: Tue, 3 Nov 2015 16:44:00 +0100 Subject: [PATCH 0933/2522] including error code and error number when database connection creation fails --- database/mysql/mysql_replication.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/database/mysql/mysql_replication.py b/database/mysql/mysql_replication.py index 348f49df6c2..c8e342a1d23 100644 --- a/database/mysql/mysql_replication.py +++ b/database/mysql/mysql_replication.py @@ -337,7 +337,8 @@ def main(): else: db_connection = MySQLdb.connect(host=module.params["login_host"], port=module.params["login_port"], user=login_user, passwd=login_password) except Exception, e: - module.fail_json(msg="unable to connect to database, check login_user and login_password are correct or ~/.my.cnf has the credentials") + 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 (%s: %s)" % (errno, errstr) ) try: cursor = db_connection.cursor(cursorclass=MySQLdb.cursors.DictCursor) except Exception, e: From ea2fd78e6a1b1bf74d589241fc553a841adc660f Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 3 Nov 2015 12:03:00 -0500 Subject: [PATCH 0934/2522] fixed default from None to [] for ctstate --- system/iptables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/iptables.py b/system/iptables.py index 29010b730e5..e78295cc291 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -313,7 +313,7 @@ def main(): destination_port=dict(required=False, default=None, type='str'), to_ports=dict(required=False, default=None, type='str'), comment=dict(required=False, default=None, type='str'), - ctstate=dict(required=False, default=None, type='list'), + ctstate=dict(required=False, default=[], type='list'), ), ) args = dict( From 6a87eed58690ec8ccb0e6a37da3bdbb45f38e7ff Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 3 Nov 2015 12:12:39 -0500 Subject: [PATCH 0935/2522] made ctstate default to [] and evaluation conditional on the list being popoulated --- system/iptables.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/system/iptables.py b/system/iptables.py index e78295cc291..8c2a67eb636 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -262,8 +262,9 @@ def construct_rule(params): append_param(rule, params['to_ports'], '--to-ports', False) append_comm(rule, params['comment']) append_param(rule, params['comment'], '--comment', False) - append_conntrack(rule, params['ctstate']) - append_param(rule, ','.join(params['ctstate']), '--ctstate', False) + if params['ctstate']: + append_conntrack(rule, params['ctstate']) + append_param(rule, ','.join(params['ctstate']), '--ctstate', False) return rule From dbee2266e198f6d83837421b38612683b814166a Mon Sep 17 00:00:00 2001 From: Romain Brucker Date: Tue, 3 Nov 2015 11:41:30 -0600 Subject: [PATCH 0936/2522] Adding limit feature to iptables module --- system/iptables.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/system/iptables.py b/system/iptables.py index 8c2a67eb636..83eb1b714f8 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -208,6 +208,10 @@ - "ctstate is a list of the connection states to match in the conntrack module. Possible states are: 'INVALID', 'NEW', 'ESTABLISHED', 'RELATED', 'UNTRACKED', 'SNAT', 'DNAT'" required: false + limit: + description: + - "Specifies the maximum average number of matches to allow per second. The number can specify units explicitly, using `/second', `/minute', `/hour' or `/day', or parts of them (so `5/second' is the same as `5/s')." + required: false ''' EXAMPLES = ''' @@ -244,6 +248,11 @@ def append_conntrack(rule, param): rule.extend(['-m']) rule.extend(['conntrack']) +def append_limit(rule, param): + if param: + rule.extend(['-m']) + rule.extend(['limit']) + def construct_rule(params): rule = [] @@ -265,6 +274,8 @@ def construct_rule(params): if params['ctstate']: append_conntrack(rule, params['ctstate']) append_param(rule, ','.join(params['ctstate']), '--ctstate', False) + append_limit(rule, params['limit']) + append_param(rule, params['limit'], '--limit', False) return rule @@ -315,6 +326,7 @@ def main(): to_ports=dict(required=False, default=None, type='str'), comment=dict(required=False, default=None, type='str'), ctstate=dict(required=False, default=[], type='list'), + limit=dict(required=False, default=[], type='list'), ), ) args = dict( From 2b04f0c5cf5180269d12060b86d9cbded37b58e8 Mon Sep 17 00:00:00 2001 From: Romain Brucker Date: Tue, 3 Nov 2015 11:47:28 -0600 Subject: [PATCH 0937/2522] Fixing limit type from list to string --- system/iptables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/iptables.py b/system/iptables.py index 83eb1b714f8..3e42a711db4 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -326,7 +326,7 @@ def main(): to_ports=dict(required=False, default=None, type='str'), comment=dict(required=False, default=None, type='str'), ctstate=dict(required=False, default=[], type='list'), - limit=dict(required=False, default=[], type='list'), + limit=dict(required=False, default=None, type='str'), ), ) args = dict( From f281eb2b30ca5a88f28c8a2c3ee5983b4f42bf54 Mon Sep 17 00:00:00 2001 From: Greg DeKoenigsberg Date: Tue, 3 Nov 2015 15:48:58 -0500 Subject: [PATCH 0938/2522] Add new SMEs for Zabbix --- REVIEWERS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/REVIEWERS.md b/REVIEWERS.md index 06263169ec7..fe7392d7f04 100644 --- a/REVIEWERS.md +++ b/REVIEWERS.md @@ -49,6 +49,8 @@ Docker: @cove @joshuaconner @softzilla @smashwilson Red Hat Network: @barnabycourt @vritant @flossware +Zabbix: @cove @harrisongu @abulimov + PR Process ======= From 437a62836f71b725900bee28d845c1a0aca15129 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Tue, 3 Nov 2015 23:01:50 -0500 Subject: [PATCH 0939/2522] Add sns_topic module to manage AWS SNS topics This adds an sns_topic module which allows you to create and delete AWS SNS topics as well as subscriptions to those topics. --- cloud/amazon/sns_topic.py | 261 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100755 cloud/amazon/sns_topic.py diff --git a/cloud/amazon/sns_topic.py b/cloud/amazon/sns_topic.py new file mode 100755 index 00000000000..a9de7b88f10 --- /dev/null +++ b/cloud/amazon/sns_topic.py @@ -0,0 +1,261 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +DOCUMENTATION = """ +module: sns_topic +short_description: Manages AWS SNS topics and subscriptions +description: + - The M(sns_topic) module allows you to create, delete, and manage subscriptions for AWS SNS topics. +version_added: 2.0 +author: "Joel Thompson (@joelthompson)" +options: + name: + description: + - The name or ARN of the SNS topic to converge + required: true + state: + description: + - Whether to create or destroy an SNS topic + required: false + default: present + choices: ["absent", "present"] + display_name: + description: + - Display name of the topic + required: False + policy: + description: + - Policy to apply to the SNS topic + required: False + delivery_policy: + description: + - Delivery policy to apply to the SNS topic + required: False + subscriptions: + description: + - List of subscriptions to apply to the topic. Note that AWS requires + subscriptions to be confirmed, so you will need to confirm any new + subscriptions. + purge_subscriptions: + description: + - Whether to purge any subscriptions not listed here. NOTE: AWS does not + allow you to purge any PendingConfirmation subscriptions, so if any + exist and would be purged, they are silently skipped. This means that + somebody could come back later and confirm the subscription. Sorry. + Blame Amazon. + default: True +extends_documentation_fragment: aws +requirements: [ "boto" ] +""" + +EXAMPLES = """ + +- name: Create alarm SNS topic + sns_topic: + name: "alarms" + state: present + display_name: "alarm SNS topic" + delivery_policy: + http: + defaultHealthyRetryPolicy: + minDelayTarget: 2 + maxDelayTarget: 4 + numRetries: 3 + numMaxDelayRetries: 5 + backoffFunction: "" + disableSubscriptionOverrides: True + defaultThrottlePolicy: + maxReceivesPerSecond: 10 + subscriptions: + - endpoint: "my_email_address@example.com" + protocol: "email" + - endpoint: "my_mobile_number" + protocol: "sms" + +""" + +import sys +import time +import json +import re + +try: + import boto + import boto.sns +except ImportError: + print "failed=True msg='boto required for this module'" + sys.exit(1) + + +def canonicalize_endpoint(protocol, endpoint): + if protocol == 'sms': + import re + return re.sub('[^0-9]*', '', endpoint) + return endpoint + + + +def get_all_topics(connection): + next_token = None + topics = [] + while True: + response = connection.get_all_topics(next_token) + topics.extend(response['ListTopicsResponse']['ListTopicsResult']['Topics']) + next_token = \ + response['ListTopicsResponse']['ListTopicsResult']['NextToken'] + if not next_token: + break + return [t['TopicArn'] for t in topics] + + +def arn_topic_lookup(connection, short_topic): + # topic names cannot have colons, so this captures the full topic name + all_topics = get_all_topics(connection) + lookup_topic = ':%s' % short_topic + for topic in all_topics: + if topic.endswith(lookup_topic): + return topic + return None + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + name=dict(type='str', required=True), + state=dict(type='str', default='present', choices=['present', + 'absent']), + display_name=dict(type='str', required=False), + policy=dict(type='dict', required=False), + delivery_policy=dict(type='dict', required=False), + subscriptions=dict(type='list', required=False), + purge_subscriptions=dict(type='bool', default=True), + ) + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + name = module.params.get('name') + state = module.params.get('state') + display_name = module.params.get('display_name') + policy = module.params.get('policy') + delivery_policy = module.params.get('delivery_policy') + subscriptions = module.params.get('subscriptions') + purge_subscriptions = module.params.get('purge_subscriptions') + check_mode = module.check_mode + changed = False + + topic_created = False + attributes_set = [] + subscriptions_added = [] + subscriptions_deleted = [] + + region, ec2_url, aws_connect_params = get_aws_connection_info(module) + if not region: + module.fail_json(msg="region must be specified") + try: + connection = connect_to_aws(boto.sns, region, **aws_connect_params) + except boto.exception.NoAuthHandlerFound, e: + module.fail_json(msg=str(e)) + + # topics cannot contain ':', so thats the decider + if ':' in name: + all_topics = get_all_topics(connection) + if name in all_topics: + arn_topic = name + elif state == 'absent': + module.exit_json(changed=False) + else: + module.fail_json(msg="specified an ARN for a topic but it doesn't" + " exist") + else: + arn_topic = arn_topic_lookup(connection, name) + if not arn_topic: + if state == 'absent': + module.exit_json(changed=False) + elif check_mode: + module.exit_json(changed=True, topic_created=True, + subscriptions_added=subscriptions, + subscriptions_deleted=[]) + + changed=True + topic_created = True + connection.create_topic(name) + arn_topic = arn_topic_lookup(connection, name) + while not arn_topic: + time.sleep(3) + arn_topic = arn_topic_lookup(connection, name) + + if arn_topic and state == "absent": + if not check_mode: + connection.delete_topic(arn_topic) + module.exit_json(changed=True) + + topic_attributes = connection.get_topic_attributes(arn_topic) \ + ['GetTopicAttributesResponse'] ['GetTopicAttributesResult'] \ + ['Attributes'] + if display_name and display_name != topic_attributes['DisplayName']: + changed = True + attributes_set.append('display_name') + if not check_mode: + connection.set_topic_attributes(arn_topic, 'DisplayName', + display_name) + + if policy and policy != json.loads(topic_attributes['policy']): + changed = True + attributes_set.append('policy') + if not check_mode: + connection.set_topic_attributes(arn_topic, 'Policy', + json.dumps(policy)) + + if delivery_policy and ('DeliveryPolicy' not in topic_attributes or \ + delivery_policy != json.loads(topic_attributes['DeliveryPolicy'])): + changed = True + attributes_set.append('delivery_policy') + if not check_mode: + connection.set_topic_attributes(arn_topic, 'DeliveryPolicy', + json.dumps(delivery_policy)) + + + next_token = None + aws_subscriptions = [] + while True: + response = connection.get_all_subscriptions_by_topic(arn_topic, + next_token) + aws_subscriptions.extend(response['ListSubscriptionsByTopicResponse'] \ + ['ListSubscriptionsByTopicResult']['Subscriptions']) + next_token = response['ListSubscriptionsByTopicResponse'] \ + ['ListSubscriptionsByTopicResult']['NextToken'] + if not next_token: + break + + desired_subscriptions = [(sub['protocol'], + canonicalize_endpoint(sub['protocol'], sub['endpoint'])) for sub in + subscriptions] + aws_subscriptions_list = [] + + for sub in aws_subscriptions: + sub_key = (sub['Protocol'], sub['Endpoint']) + aws_subscriptions_list.append(sub_key) + if purge_subscriptions and sub_key not in desired_subscriptions and \ + sub['SubscriptionArn'] != 'PendingConfirmation': + changed = True + subscriptions_deleted.append(sub_key) + if not check_mode: + connection.unsubscribe(sub['SubscriptionArn']) + + for (protocol, endpoint) in desired_subscriptions: + if (protocol, endpoint) not in aws_subscriptions_list: + changed = True + subscriptions_added.append(sub) + if not check_mode: + connection.subscribe(arn_topic, protocol, endpoint) + + module.exit_json(changed=changed, topic_created=topic_created, + attributes_set=attributes_set, + subscriptions_added=subscriptions_added, + subscriptions_deleted=subscriptions_deleted, sns_arn=arn_topic) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +main() From 84a8902b4831e77ef2d892414ad672fe2af4e13d Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Thu, 5 Nov 2015 17:44:29 +0100 Subject: [PATCH 0940/2522] fixxed problem with match @ --- windows/win_acl.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_acl.ps1 b/windows/win_acl.ps1 index fb12ae6cee3..6e497c417be 100644 --- a/windows/win_acl.ps1 +++ b/windows/win_acl.ps1 @@ -42,7 +42,7 @@ Function UserSearch } } - Elseif ($AccountName -contains "@") + Elseif ($AccountName.contains("@")) { $IsDomainAccount = $true $IsUpn = $true From 10ce1f92aa61f2d140a2b7ea731bb3a76289e593 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Thu, 5 Nov 2015 17:46:25 +0100 Subject: [PATCH 0941/2522] fixxed problem with match @ --- windows/win_owner.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_owner.ps1 b/windows/win_owner.ps1 index 0623f2b17a1..9bd3c335e9c 100644 --- a/windows/win_owner.ps1 +++ b/windows/win_owner.ps1 @@ -37,7 +37,7 @@ Function UserSearch } } - Elseif ($AccountName -contains "@") + Elseif ($AccountName.contains("@")) { $IsDomainAccount = $true $IsUpn = $true From 1ee6962c938272985e870299f8a04d58fee2e76f Mon Sep 17 00:00:00 2001 From: Etherdaemon Date: Wed, 4 Nov 2015 16:29:47 +1000 Subject: [PATCH 0942/2522] Add new module to allow for getting and listing of Route53 relevant details --- cloud/amazon/route53_facts.py | 434 ++++++++++++++++++++++++++++++++++ 1 file changed, 434 insertions(+) create mode 100644 cloud/amazon/route53_facts.py diff --git a/cloud/amazon/route53_facts.py b/cloud/amazon/route53_facts.py new file mode 100644 index 00000000000..16034acb51a --- /dev/null +++ b/cloud/amazon/route53_facts.py @@ -0,0 +1,434 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +module: route53_facts +short_description: Retrieves route53 details using AWS methods +description: + - Gets various details related to Route53 zone, record set or health check details +version_added: "2.0" +options: + query: + description: + - specifies the query action to take + required: True + choices: [ + 'change', + 'checker_ip_range', + 'health_check', + 'hosted_zone', + 'record_sets', + 'reusable_delegation_set', + ] + change_id: + description: + - The ID of the change batch request. + The value that you specify here is the value that + ChangeResourceRecordSets returned in the Id element + when you submitted the request. + required: false + hosted_zone_id: + description: + - The Hosted Zone ID of the DNS zone + required: false + max_items: + description: + - Maximum number of items to return for various get/list requests + required: false + next_marker: + description: + - Some requests such as list_command: hosted_zones will return a maximum + number of entries - EG 100. If the number of entries exceeds this maximum + another request can be sent using the NextMarker entry from the first response + to get the next page of results + required: false + delegation_set_id: + description: + - The DNS Zone delegation set ID + required: false + start_record_name: + description: + - The first name in the lexicographic ordering of domain names that you want + the list_command: record_sets to start listing from + required: false + type: + description: + - The type of DNS record + required: false + choices: [ 'A', 'CNAME', 'MX', 'AAAA', 'TXT', 'PTR', 'SRV', 'SPF', 'NS' ] + dns_name: + description: + - The first name in the lexicographic ordering of domain names that you want + the list_command to start listing from + required: false + resource_id: + description: + - The ID/s of the specified resource/s + required: false + aliases: ['resource_ids'] + health_check_id: + description: + - The ID of the health check + required: false + hosted_zone_method: + description: + - This is used in conjunction with query: hosted_zone. + It allows for listing details, counts or tags of various + hosted zone details. + required: false + choices: [ + 'details', + 'list', + 'list_by_name', + 'count', + 'tags', + ] + default: 'list' + health_check_method: + description: + - This is used in conjunction with query: health_check. + It allows for listing details, counts or tags of various + health check details. + required: false + choices: [ + 'list', + 'details', + 'status', + 'failure_reason', + 'count', + 'tags', + ] + default: 'list' +author: Karen Cheng(@Etherdaemon) +extends_documentation_fragment: aws +''' + +EXAMPLES = ''' +# Simple example of listing all hosted zones +- name: List all hosted zones + route53_facts: + query: hosted_zone + register: hosted_zones + +# Getting a count of hosted zones +- name: Return a count of all hosted zones + route53_facts: + query: hosted_zone + hosted_zone_method: count + register: hosted_zone_count + +- name: List the first 20 resource record sets in a given hosted zone + route53_facts: + profile: account_name + query: record_sets + hosted_zone_id: 'ZZZ1111112222' + max_items: 20 + register: record_sets + +- name: List first 20 health checks + route53_facts: + query: health_check + health_check_method: list + max_items: 20 + register: health_checks + +- name: Get health check last failure_reason + route53_facts: + query: health_check + health_check_method: failure_reason + health_check_id: '00000000-1111-2222-3333-12345678abcd' + register: health_check_failure_reason + +- name: Retrieve reusable delegation set details + route53_facts: + query: reusable_delegation_set + delegation_set_id: 'delegation id' + register: delegation_sets + +''' +try: + import json + import boto + import botocore + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +try: + import boto3 + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + + +def get_hosted_zone(client, module): + params = dict() + + if module.params.get('hosted_zone_id'): + params['HostedZoneId'] = module.params.get('hosted_zone_id') + else: + module.fail_json(msg="Hosted Zone Id is required") + + results = client.get_hosted_zone(**params) + return results + + +def reusable_delegation_set_details(client, module): + params = dict() + if not module.params.get('delegation_set_id'): + if module.params.get('max_items'): + params['MaxItems'] = module.params.get('max_items') + + if module.params.get('next_marker'): + params['Marker'] = module.params.get('next_marker') + + results = client.list_reusable_delegation_sets(**params) + else: + params['DelegationSetId'] = module.params.get('delegation_set_id') + results = client.get_reusable_delegation_set(**params) + + return results + + +def list_hosted_zones(client, module): + params = dict() + + if module.params.get('max_items'): + params['MaxItems'] = module.params.get('max_items') + + if module.params.get('next_marker'): + params['Marker'] = module.params.get('next_marker') + + if module.params.get('delegation_set_id'): + params['DelegationSetId'] = module.params.get('delegation_set_id') + + results = client.list_hosted_zones(**params) + return results + + +def list_hosted_zones_by_name(client, module): + params = dict() + + if module.params.get('hosted_zone_id'): + params['HostedZoneId'] = module.params.get('hosted_zone_id') + + if module.params.get('dns_name'): + params['DNSName'] = module.params.get('dns_name') + + if module.params.get('max_items'): + params['MaxItems'] = module.params.get('max_items') + + results = client.list_hosted_zones_by_name(**params) + return results + + +def change_details(client, module): + params = dict() + + if module.params.get('change_id'): + params['Id'] = module.params.get('change_id') + else: + module.fail_json(msg="change_id is required") + + results = client.get_change(**params) + return results + + +def checker_ip_range_details(client, module): + results = client.get_checker_ip_ranges() + return results + + +def get_count(client, module): + if module.params.get('query') == 'health_check': + results = client.get_health_check_count() + else: + results = client.get_hosted_zone_count() + + return results + + +def get_health_check(client, module): + params = dict() + + if not module.params.get('health_check_id'): + module.fail_json(msg="health_check_id is required") + else: + params['HealthCheckId'] = module.params.get('health_check_id') + + if module.params.get('health_check_method') == 'details': + results = client.get_health_check(**params) + elif module.params.get('health_check_method') == 'failure_reason': + results = client.get_health_check_last_failure_reason(**params) + elif module.params.get('health_check_method') == 'status': + results = client.get_health_check_status(**params) + + return results + + +def get_resource_tags(client, module): + params = dict() + + if module.params.get('resource_id'): + params['ResourceIds'] = module.params.get('resource_id') + else: + module.fail_json(msg="resource_id or resource_ids is required") + + if module.params.get('query') == 'health_check': + params['ResourceType'] = 'healthcheck' + else: + params['ResourceType'] = 'hostedzone' + + results = client.list_tags_for_resources(**params) + return results + + +def list_health_checks(client, module): + params = dict() + + if module.params.get('max_items'): + params['MaxItems'] = module.params.get('max_items') + + if module.params.get('next_marker'): + params['Marker'] = module.params.get('next_marker') + + results = client.list_health_checks(**params) + return results + + +def record_sets_details(client, module): + params = dict() + + if module.params.get('hosted_zone_id'): + params['HostedZoneId'] = module.params.get('hosted_zone_id') + else: + module.fail_json(msg="Hosted Zone Id is required") + + if module.params.get('start_record_name'): + params['StartRecordName'] = module.params.get('start_record_name') + + if module.params.get('type') and not module.params.get('start_record_name'): + module.fail_json(msg="start_record_name must be specified if type is set") + elif module.params.get('type'): + params['StartRecordType'] = module.params.get('type') + + results = client.list_resource_record_sets(**params) + return results + + +def health_check_details(client, module): + health_check_invocations = { + 'list': list_health_checks, + 'details': get_health_check, + 'status': get_health_check, + 'failure_reason': get_health_check, + 'count': get_count, + 'tags': get_resource_tags, + } + + results = health_check_invocations[module.params.get('health_check_method')](client, module) + return results + + +def hosted_zone_details(client, module): + hosted_zone_invocations = { + 'details': get_hosted_zone, + 'list': list_hosted_zones, + 'list_by_name': list_hosted_zones_by_name, + 'count': get_count, + 'tags': get_resource_tags, + } + + results = hosted_zone_invocations[module.params.get('hosted_zone_method')](client, module) + return results + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + query=dict(choices=[ + 'change', + 'checker_ip_range', + 'health_check', + 'hosted_zone', + 'record_sets', + 'reusable_delegation_set', + ], required=True), + change_id=dict(), + hosted_zone_id=dict(), + max_items=dict(type='str'), + next_marker=dict(), + delegation_set_id=dict(), + start_record_name=dict(), + type=dict(choices=[ + 'A', 'CNAME', 'MX', 'AAAA', 'TXT', 'PTR', 'SRV', 'SPF', 'NS' + ]), + dns_name=dict(), + resource_id=dict(type='list', aliases=['resource_ids']), + health_check_id=dict(), + hosted_zone_method=dict(choices=[ + 'details', + 'list', + 'list_by_name', + 'count', + 'tags' + ], default='list'), + health_check_method=dict(choices=[ + 'list', + 'details', + 'status', + 'failure_reason', + 'count', + 'tags', + ], default='list'), + ) + ) + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=[ + ['hosted_zone_method', 'health_check_method'], + ], + ) + + # Validate Requirements + if not (HAS_BOTO or HAS_BOTO3): + module.fail_json(msg='json and boto/boto3 is required.') + + try: + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + route53 = boto3_conn(module, conn_type='client', resource='route53', region=region, endpoint=ec2_url, **aws_connect_kwargs) + except boto.exception.NoAuthHandlerFound, e: + module.fail_json(msg="Can't authorize connection - "+str(e)) + + invocations = { + 'change': change_details, + 'checker_ip_range': checker_ip_range_details, + 'health_check': health_check_details, + 'hosted_zone': hosted_zone_details, + 'record_sets': record_sets_details, + 'reusable_delegation_set': reusable_delegation_set_details, + } + results = invocations[module.params.get('query')](route53, module) + + module.exit_json(**results) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From 63003372b4f87f6f775b23cc9af491630647b48b Mon Sep 17 00:00:00 2001 From: root Date: Thu, 5 Nov 2015 21:16:41 -0600 Subject: [PATCH 0943/2522] Fixed call to module.log --- network/openvswitch_port.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/openvswitch_port.py b/network/openvswitch_port.py index e98453fc95f..5fbbe8480dd 100644 --- a/network/openvswitch_port.py +++ b/network/openvswitch_port.py @@ -140,7 +140,7 @@ def exists(self): def set(self, set_opt): """ Set attributes on a port. """ - self.module("set called %s" % set_opt) + self.module.log("set called %s" % set_opt) if (not set_opt): return False From 9778015039b7a8a0cfc84525566fb2b34e783e72 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Fri, 6 Nov 2015 09:26:49 +0100 Subject: [PATCH 0944/2522] first implementation of win_share module --- windows/win_share.ps1 | 251 ++++++++++++++++++++++++++++++++++++++++++ windows/win_share.py | 113 +++++++++++++++++++ 2 files changed, 364 insertions(+) create mode 100644 windows/win_share.ps1 create mode 100644 windows/win_share.py diff --git a/windows/win_share.ps1 b/windows/win_share.ps1 new file mode 100644 index 00000000000..3d816ac1657 --- /dev/null +++ b/windows/win_share.ps1 @@ -0,0 +1,251 @@ +#!powershell +# This file is part of Ansible + +# Copyright 2015, Hans-Joachim Kliemeck +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# WANT_JSON +# POWERSHELL_COMMON + +#Functions +Function UserSearch +{ + Param ([string]$AccountName) + #Check if there's a realm specified + if ($AccountName.Split("\").count -gt 1) + { + if ($AccountName.Split("\")[0] -eq $env:COMPUTERNAME) + { + $IsLocalAccount = $true + } + Else + { + $IsDomainAccount = $true + $IsUpn = $false + } + + } + Elseif ($AccountName.contains("@")) + { + $IsDomainAccount = $true + $IsUpn = $true + } + Else + { + #Default to local user account + $accountname = $env:COMPUTERNAME + "\" + $AccountName + $IsLocalAccount = $true + } + + if ($IsLocalAccount -eq $true) + { + # do not use Win32_UserAccount, because e.g. SYSTEM (BUILTIN\SYSTEM or COMPUUTERNAME\SYSTEM) will not be listed. on Win32_Account groups will be listed too + $localaccount = get-wmiobject -class "Win32_Account" -namespace "root\CIMV2" -filter "(LocalAccount = True)" | where {$_.Caption -eq $AccountName} + if ($localaccount) + { + return $localaccount.SID + } + } + ElseIf ($IsDomainAccount -eq $true) + { + #Search by samaccountname + $Searcher = [adsisearcher]"" + + If ($IsUpn -eq $false) { + $Searcher.Filter = "sAMAccountName=$($accountname.split("\")[1])" + } + Else { + $Searcher.Filter = "userPrincipalName=$($accountname)" + } + + $result = $Searcher.FindOne() + if ($result) + { + $user = $result.GetDirectoryEntry() + + # get binary SID from AD account + $binarySID = $user.ObjectSid.Value + + # convert to string SID + return (New-Object System.Security.Principal.SecurityIdentifier($binarySID,0)).Value + } + } +} +Function NormalizeAccounts +{ + param( + [parameter(valuefrompipeline=$true)] + $users + ) + + $users = $users.Trim() + If ($users -eq "") { + $splittedUsers = [Collections.Generic.List[String]] @() + } + Else { + $splittedUsers = [Collections.Generic.List[String]] $users.Split(",") + } + + $normalizedUsers = [Collections.Generic.List[String]] @() + ForEach($splittedUser in $splittedUsers) { + $sid = UserSearch $splittedUser + If (!$sid) { + Fail-Json $result "$splittedUser is not a valid user or group on the host machine or domain" + } + + $normalizedUser = (New-Object System.Security.Principal.SecurityIdentifier($sid)).Translate([System.Security.Principal.NTAccount]) + $normalizedUsers.Add($normalizedUser) + } + + return ,$normalizedUsers +} + +$params = Parse-Args $args; + +$result = New-Object PSObject; +Set-Attr $result "changed" $false; + +$name = Get-Attr $params "name" -failifempty $true +$state = Get-Attr $params "state" "present" -validateSet "present","absent" -resultobj $result + +Try { + $share = Get-SmbShare $name -ErrorAction SilentlyContinue + If ($state -eq "absent") { + If ($share) { + Remove-SmbShare -Force -Name $name + Set-Attr $result "changed" $true; + } + } + Else { + $path = Get-Attr $params "path" -failifempty $true + $description = Get-Attr $params "description" "" + + $permissionList = Get-Attr $params "list" "no" -validateSet "no","yes" -resultobj $result | ConvertTo-Bool + $folderEnum = if ($permissionList) { "Unrestricted" } else { "AccessBased" } + + $permissionRead = Get-Attr $params "read" "" | NormalizeAccounts + $permissionChange = Get-Attr $params "change" "" | NormalizeAccounts + $permissionFull = Get-Attr $params "full" "" | NormalizeAccounts + $permissionDeny = Get-Attr $params "deny" "" | NormalizeAccounts + + If (-Not (Test-Path -Path $path)) { + Fail-Json $result "$path directory does not exist on the host" + } + + # need to (re-)create share + If (!$share) { + New-SmbShare -Name $name -Path $path + $share = Get-SmbShare $name -ErrorAction SilentlyContinue + + Set-Attr $result "changed" $true; + } + If ($share.Path -ne $path) { + Remove-SmbShare -Force -Name $name + + New-SmbShare -Name $name -Path $path + $share = Get-SmbShare $name -ErrorAction SilentlyContinue + + Set-Attr $result "changed" $true; + } + + # updates + If ($share.Description -ne $description) { + Set-SmbShare -Force -Name $name -Description $description + Set-Attr $result "changed" $true; + } + If ($share.FolderEnumerationMode -ne $folderEnum) { + Set-SmbShare -Force -Name $name -FolderEnumerationMode $folderEnum + Set-Attr $result "changed" $true; + } + + # clean permissions that imply others + ForEach ($user in $permissionFull) { + $permissionChange.remove($user) + $permissionRead.remove($user) + } + ForEach ($user in $permissionChange) { + $permissionRead.remove($user) + } + + # remove permissions + $permissions = Get-SmbShareAccess -Name $name + ForEach ($permission in $permissions) { + If ($permission.AccessControlType -eq "Deny") { + If (!$permissionDeny.Contains($permission.AccountName)) { + Unblock-SmbShareAccess -Force -Name $name -AccountName $permission.AccountName + Set-Attr $result "changed" $true; + } + } + ElseIf ($permission.AccessControlType -eq "Allow") { + If ($permission.AccessRight -eq "Full") { + If (!$permissionFull.Contains($permission.AccountName)) { + Revoke-SmbShareAccess -Force -Name $name -AccountName $permission.AccountName + Set-Attr $result "changed" $true; + + Continue + } + + # user got requested permissions + $permissionFull.remove($permission.AccountName) + } + ElseIf ($permission.AccessRight -eq "Change") { + If (!$permissionChange.Contains($permission.AccountName)) { + Revoke-SmbShareAccess -Force -Name $name -AccountName $permission.AccountName + Set-Attr $result "changed" $true; + + Continue + } + + # user got requested permissions + $permissionChange.remove($permission.AccountName) + } + ElseIf ($permission.AccessRight -eq "Read") { + If (!$permissionRead.Contains($permission.AccountName)) { + Revoke-SmbShareAccess -Force -Name $name -AccountName $permission.AccountName + Set-Attr $result "changed" $true; + + Continue + } + + # user got requested permissions + $permissionRead.Remove($permission.AccountName) + } + } + } + + # add missing permissions + ForEach ($user in $permissionRead) { + Grant-SmbShareAccess -Force -Name $name -AccountName $user -AccessRight "Read" + Set-Attr $result "changed" $true; + } + ForEach ($user in $permissionChange) { + Grant-SmbShareAccess -Force -Name $name -AccountName $user -AccessRight "Change" + Set-Attr $result "changed" $true; + } + ForEach ($user in $permissionFull) { + Grant-SmbShareAccess -Force -Name $name -AccountName $user -AccessRight "Full" + Set-Attr $result "changed" $true; + } + ForEach ($user in $permissionDeny) { + Block-SmbShareAccess -Force -Name $name -AccountName $user + Set-Attr $result "changed" $true; + } + } +} +Catch { + Fail-Json $result "an error occured when attempting to create share $name" +} + +Exit-Json $result \ No newline at end of file diff --git a/windows/win_share.py b/windows/win_share.py new file mode 100644 index 00000000000..6a6039bad30 --- /dev/null +++ b/windows/win_share.py @@ -0,0 +1,113 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2015, Hans-Joachim Kliemeck +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# this is a windows documentation stub. actual code lives in the .ps1 +# file of the same name + +DOCUMENTATION = ''' +--- +module: win_share +version_added: "2.0" +short_description: Manage Windows shares +description: + - Add, modify or remove Windows share and set share permissions. +requirements: + - Windows 8, Windows 2012 or newer +options: + name: + description: + - Share name + required: yes + path: + description: + - Share directory + required: yes + state: + description: + - Specify whether to add C(present) or remove C(absent) the specified share + required: no + choices: + - present + - absent + default: present + description: + description: + - Share description + required: no + default: none + list: + description: + - Specify whether to allow or deny file listing, in case user got no permission on share + required: no + choices: + - yes + - no + default: none + read: + description: + - Specify user list that should get read access on share, separated by comma. + required: no + default: none + change: + description: + - Specify user list that should get read and write access on share, separated by comma. + required: no + default: none + full: + description: + - Specify user list that should get full access on share, separated by comma. + required: no + default: none + deny: + description: + - Specify user list that should get no access, regardless of implied access on share, separated by comma. + required: no + default: none +Hans-Joachim Kliemeck (@h0nIg) +''' + +EXAMPLES = ''' +# Playbook example +# Add share and set permissions +--- +- name: Add secret share + win_share: + name: internal + description: top secret share + path: C:\\shares\\internal\\ + list: 'no' + full: Administrators,CEO + read: HR-Global + deny: HR-External + +- name: Add public company share + win_share: + name: company + description: top secret share + path: C:\\shares\\company\\ + list: 'yes' + full: Administrators,CEO + read: Global + +# Remove previously added share + win_share: + name: internal + state: absent +''' \ No newline at end of file From 5753a05625ec5a8da08039a5277b87ee6501b696 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Thu, 5 Nov 2015 17:50:47 +0100 Subject: [PATCH 0945/2522] fixxed problem with match @ --- windows/win_nssm.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_nssm.ps1 b/windows/win_nssm.ps1 index bf4e798fca5..fa61afdaafc 100644 --- a/windows/win_nssm.ps1 +++ b/windows/win_nssm.ps1 @@ -352,7 +352,7 @@ Function Nssm-Update-Credentials } else { $fullUser = $user - If (-not($user -contains "@") -and ($user.Split("\").count -eq 1)) { + If (-Not($user.contains("@")) -And ($user.Split("\").count -eq 1)) { $fullUser = ".\" + $user } From 2ac53bf559232f1ba83548c5f7d2ca208afb25ee Mon Sep 17 00:00:00 2001 From: Kerim Satirli Date: Fri, 6 Nov 2015 11:02:51 +0100 Subject: [PATCH 0946/2522] fixes a typo in Datadog Monitor docs --- monitoring/datadog_monitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py index 9853d748c2c..9318326620e 100644 --- a/monitoring/datadog_monitor.py +++ b/monitoring/datadog_monitor.py @@ -54,7 +54,7 @@ default: null choices: ['metric alert', 'service check'] query: - description: ["he monitor query to notify on with syntax varying depending on what type of monitor you are creating."] + description: ["The monitor query to notify on with syntax varying depending on what type of monitor you are creating."] required: false default: null name: From b51d096c317623251b251c013936ee0a90636b23 Mon Sep 17 00:00:00 2001 From: Jimmy Tang Date: Fri, 6 Nov 2015 13:14:55 +0000 Subject: [PATCH 0947/2522] Fix documentation, the correct parameter is "name" --- clustering/znode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clustering/znode.py b/clustering/znode.py index 8effcd9189e..51ab51d0ea4 100644 --- a/clustering/znode.py +++ b/clustering/znode.py @@ -26,7 +26,7 @@ description: - A list of ZooKeeper servers (format '[server]:[port]'). required: true - path: + name: description: - The path of the znode. required: true From 9d8b6d470dc419f3d153821a196e25e1f25e96a5 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Fri, 6 Nov 2015 14:29:11 +0100 Subject: [PATCH 0948/2522] fixxed problems related to path input --- windows/win_share.ps1 | 3 +++ windows/win_share.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/windows/win_share.ps1 b/windows/win_share.ps1 index 3d816ac1657..86970f88d39 100644 --- a/windows/win_share.ps1 +++ b/windows/win_share.ps1 @@ -144,6 +144,9 @@ Try { Fail-Json $result "$path directory does not exist on the host" } + # normalize path and remove slash at the end + $path = (Get-Item $path).FullName -replace ".$" + # need to (re-)create share If (!$share) { New-SmbShare -Name $name -Path $path diff --git a/windows/win_share.py b/windows/win_share.py index 6a6039bad30..9e54185b64b 100644 --- a/windows/win_share.py +++ b/windows/win_share.py @@ -91,7 +91,7 @@ win_share: name: internal description: top secret share - path: C:\\shares\\internal\\ + path: C:/shares/internal list: 'no' full: Administrators,CEO read: HR-Global @@ -101,7 +101,7 @@ win_share: name: company description: top secret share - path: C:\\shares\\company\\ + path: C:/shares/company list: 'yes' full: Administrators,CEO read: Global From 55f64daee3f1bb1a6d1a24ea106542356315c821 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Fri, 6 Nov 2015 14:36:00 +0100 Subject: [PATCH 0949/2522] corrected requirements --- windows/win_share.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_share.py b/windows/win_share.py index 9e54185b64b..e7c87ccf8a4 100644 --- a/windows/win_share.py +++ b/windows/win_share.py @@ -29,7 +29,7 @@ description: - Add, modify or remove Windows share and set share permissions. requirements: - - Windows 8, Windows 2012 or newer + - Windows 8.1 / Windows 2012 or newer options: name: description: From e1c1ea9013dcde6e66b8e21e35bbce9638fa4e7e Mon Sep 17 00:00:00 2001 From: Kenny Gryp Date: Mon, 9 Nov 2015 10:05:53 +0100 Subject: [PATCH 0950/2522] in order for replication setup to work, some errors should be ignored --- database/mysql/mysql_replication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/mysql/mysql_replication.py b/database/mysql/mysql_replication.py index c8e342a1d23..f01ffa76dc3 100644 --- a/database/mysql/mysql_replication.py +++ b/database/mysql/mysql_replication.py @@ -311,7 +311,7 @@ def main(): if not mysqldb_found: module.fail_json(msg="the python mysqldb module is required") else: - warnings.filterwarnings('error', category=MySQLdb.Warning) + warnings.filterwarnings('ignore', 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 From 53d42cd8d870db500ff623c158365762c5b31a0b Mon Sep 17 00:00:00 2001 From: Kenny Gryp Date: Mon, 9 Nov 2015 10:07:15 +0100 Subject: [PATCH 0951/2522] revert to unbreak pull request --- database/mysql/mysql_replication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/mysql/mysql_replication.py b/database/mysql/mysql_replication.py index f01ffa76dc3..c8e342a1d23 100644 --- a/database/mysql/mysql_replication.py +++ b/database/mysql/mysql_replication.py @@ -311,7 +311,7 @@ def main(): if not mysqldb_found: module.fail_json(msg="the python mysqldb module is required") else: - warnings.filterwarnings('ignore', category=MySQLdb.Warning) + 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 From 5e103d604a707c039a0930c330e7569f867b05a7 Mon Sep 17 00:00:00 2001 From: Ritesh Khadgaray Date: Mon, 9 Nov 2015 20:21:28 +0530 Subject: [PATCH 0952/2522] allows user to not update zabbix host config if host is present. --- monitoring/zabbix_host.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/monitoring/zabbix_host.py b/monitoring/zabbix_host.py index 6fac82c7177..3cb27c5fbb9 100644 --- a/monitoring/zabbix_host.py +++ b/monitoring/zabbix_host.py @@ -91,6 +91,13 @@ - 'https://www.zabbix.com/documentation/2.0/manual/appendix/api/hostinterface/definitions#host_interface' required: false default: [] + force: + description: + - Overwrite the host configuration, even if already present + required: false + default: "yes" + choices: [ "yes", "no" ] + version_added: "2.0" ''' EXAMPLES = ''' @@ -370,6 +377,7 @@ def main(): state=dict(default="present", choices=['present', 'absent']), timeout=dict(type='int', default=10), interfaces=dict(required=False), + force=dict(default='yes', choices='bool'), proxy=dict(required=False) ), supports_check_mode=True @@ -388,6 +396,7 @@ def main(): state = module.params['state'] timeout = module.params['timeout'] interfaces = module.params['interfaces'] + force = module.params['force'] proxy = module.params['proxy'] # convert enabled to 0; disabled to 1 @@ -439,6 +448,9 @@ def main(): if not group_ids: module.fail_json(msg="Specify at least one group for updating host '%s'." % host_name) + if not force: + module.fail_json(changed=False, result="Host present, Can't update configuration without force") + # get exist host's interfaces exist_interfaces = host._zapi.hostinterface.get({'output': 'extend', 'hostids': host_id}) exist_interfaces_copy = copy.deepcopy(exist_interfaces) From b01f083ec333bf04b45d149f7d3e654967d47552 Mon Sep 17 00:00:00 2001 From: Alberto Gireud Date: Sat, 7 Nov 2015 11:48:46 -0600 Subject: [PATCH 0953/2522] Add openstack project module --- cloud/openstack/os_project.py | 201 ++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 cloud/openstack/os_project.py diff --git a/cloud/openstack/os_project.py b/cloud/openstack/os_project.py new file mode 100644 index 00000000000..c1958774976 --- /dev/null +++ b/cloud/openstack/os_project.py @@ -0,0 +1,201 @@ +#!/usr/bin/python +# Copyright (c) 2015 IBM Corporation +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + + +try: + import shade + HAS_SHADE = True +except ImportError: + HAS_SHADE = False + +DOCUMENTATION = ''' +--- +module: os_project +short_description: Manage OpenStack Projects +extends_documentation_fragment: openstack +version_added: "2.0" +author: "Alberto Gireud (@agireud)" +description: + - Manage OpenStack Projects. Projects can be created, + updated or deleted using this module. A project will be updated + if I(name) matches an existing project and I(state) is present. + The value for I(name) cannot be updated without deleting and + re-creating the project. +options: + name: + description: + - Name for the project + required: true + description: + description: + - Description for the project + required: false + default: None + domain_id: + description: + - Domain id to create the project in if the cloud supports domains + required: false + default: None + enabled: + description: + - Is the project enabled + required: false + default: True + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present +requirements: + - "python >= 2.6" + - "shade" +''' + +EXAMPLES = ''' +# Create a project +- os_project: + cloud: mycloud + state: present + name: demoproject + description: demodescription + domain_id: demoid + enabled: True + +# Delete a project +- os_project: + cloud: mycloud + state: absent + name: demoproject +''' + + +RETURN = ''' +project: + description: Dictionary describing the project. + returned: On success when I(state) is 'present' + type: dictionary + contains: + description: + description: Project description + type: string + sample: "demodescription" + domain_id: + description: Project domain ID. Only present with Keystone >= v3. + type: string + sample: "default" + id: + description: Project ID + type: string + sample: "f59382db809c43139982ca4189404650" + name: + description: Project name + type: string + sample: "demoproject" +''' + +def _needs_update(module, project): + keys = ('description', 'enabled') + for key in keys: + if module.params[key] is not None and module.params[key] != project.get(key): + return True + + return False + +def _system_state_change(module, project): + state = module.params['state'] + if state == 'present': + if project is None: + changed = True + else: + if _needs_update(module, project): + changed = True + else: + changed = False + + elif state == 'absent': + if project is None: + changed=False + else: + changed=True + + return changed; + +def main(): + + argument_spec = openstack_full_argument_spec( + name=dict(required=True), + description=dict(required=False, default=None), + domain=dict(required=False, default=None), + enabled=dict(default=True, type='bool'), + state=dict(default='present', choices=['absent', 'present']) + ) + + module_kwargs = openstack_module_kwargs() + module = AnsibleModule( + argument_spec, + supports_check_mode=True, + **module_kwargs + ) + + if not HAS_SHADE: + module.fail_json(msg='shade is required for this module') + + name = module.params['name'] + description = module.params['description'] + domain = module.params['domain'] + enabled = module.params['enabled'] + state = module.params['state'] + + try: + cloud = shade.openstack_cloud(**module.params) + project = cloud.get_project(name) + + if module.check_mode: + module.exit_json(changed=_system_state_change(module, project)) + + if state == 'present': + if project is None: + project = cloud.create_project( + name=name, description=description, + domain_id=domain, + enabled=enabled) + changed = True + else: + if _needs_update(module, project): + project = cloud.update_project( + project['id'], description=description, + enabled=enabled) + changed = True + else: + changed = False + module.exit_json(changed=changed, project=project) + + elif state == 'absent': + if project is None: + changed=False + else: + cloud.delete_project(project['id']) + changed=True + module.exit_json(changed=changed) + + except shade.OpenStackCloudException as e: + module.fail_json(msg=e.message, extra_data=e.extra_data) + +from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * + +if __name__ == '__main__': + main() From e0bdd2e7f686ca0c7f382c1c2766765de216a019 Mon Sep 17 00:00:00 2001 From: Romain Brucker Date: Tue, 10 Nov 2015 09:21:32 -0600 Subject: [PATCH 0954/2522] Editing iptable module to use -m state --state instead of -m conntrack --ctstate --- system/iptables.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/iptables.py b/system/iptables.py index 3e42a711db4..b9368e0688f 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -246,7 +246,7 @@ def append_comm(rule, param): def append_conntrack(rule, param): if param: rule.extend(['-m']) - rule.extend(['conntrack']) + rule.extend(['state']) def append_limit(rule, param): if param: @@ -273,7 +273,7 @@ def construct_rule(params): append_param(rule, params['comment'], '--comment', False) if params['ctstate']: append_conntrack(rule, params['ctstate']) - append_param(rule, ','.join(params['ctstate']), '--ctstate', False) + append_param(rule, ','.join(params['ctstate']), '--state', False) append_limit(rule, params['limit']) append_param(rule, params['limit'], '--limit', False) return rule From 96d044192c82ecf271c7bdca218ad16b34830228 Mon Sep 17 00:00:00 2001 From: Sam Liu Date: Wed, 11 Nov 2015 13:30:09 +0800 Subject: [PATCH 0955/2522] new module win_file_version --- windows/win_file_version.ps1 | 81 ++++++++++++++++++++++++++++++++++++ windows/win_file_version.py | 46 ++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 windows/win_file_version.ps1 create mode 100644 windows/win_file_version.py diff --git a/windows/win_file_version.ps1 b/windows/win_file_version.ps1 new file mode 100644 index 00000000000..4ee8c6e3f6c --- /dev/null +++ b/windows/win_file_version.ps1 @@ -0,0 +1,81 @@ +#!powershell + +#this file is part of Ansible +#Copyright © 2015 Sam Liu + +#This program is free software: you can redistribute it and/or modify +#it under the terms of the GNU General Public License as published by +#the Free Software Foundation, either version 3 of the License, or +#(at your option) any later version. + +#This program is distributed in the hope that it will be useful, +#but WITHOUT ANY WARRANTY; without even the implied warranty of +#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +#GNU General Public License for more details. + +#You should have received a copy of the GNU General Public License +#along with this program. If not, see . + +# WAIT_JSON +# POWERSHELL_COMMON + +$params = Parse-Args $args; + +$result = New-Object psobject @{ + win_file_version = New-Object psobject + changed = $false +} + + +If ($params.path) { + $path = $params.path.ToString() + If (-Not (Test-Path -Path $path -PathType Leaf)){ + Fail-Json $result "Specfied path: $path not exists or not a file" + } + $ext = [System.IO.Path]::GetExtension($path) + If ( $ext -notin '.exe', '.dll'){ + Fail-Json $result "Specfied path: $path is not a vaild file type, Must be DLL or EXE." + } +} +Else{ + Fail-Json $result "Specfied path: $path not define." +} + +Try { + $file_version = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($path).FileVersion + If ($file_version -eq $null){ + $file_version = '' + } + $product_version = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($path).ProductVersion + If ($product_version -eq $null){ + $product_version= '' + } + $file_major_part = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($path).FileMajorPart + If ($file_major_part -eq $null){ + $file_major_part= '' + } + $file_minor_part = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($path).FileMinorPart + If ($file_minor_part -eq $null){ + $file_minor_part= '' + } + $file_build_part = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($path).FileBuildPart + If ($file_build_part -eq $null){ + $file_build_part = '' + } + $file_private_part = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($path).FilePrivatePart + If ($file_private_part -eq $null){ + $file_private_part = '' + } +} +Catch{ +} + +Set-Attr $result.win_file_version "path" $path.toString() +Set-Attr $result.win_file_version "file_version" $file_version.toString() +Set-Attr $result.win_file_version "product_version" $product_version.toString() +Set-Attr $result.win_file_version "file_major_part" $file_major_part.toString() +Set-Attr $result.win_file_version "file_minor_part" $file_minor_part.toString() +Set-Attr $result.win_file_version "file_build_part" $file_build_part.toString() +Set-Attr $result.win_file_version "file_private_part" $file_private_part.toString() +Exit-Json $result; + diff --git a/windows/win_file_version.py b/windows/win_file_version.py new file mode 100644 index 00000000000..7688773e6a7 --- /dev/null +++ b/windows/win_file_version.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Get DLL or EXE build version +# Copyright © 2015 Sam Liu + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +DOCUMENTATION = ''' +--- +module: win_file_version +version_added: "2.0" +short_descriptions: Get DLL or EXE file build version +description: + - Get DLL or EXE file build version + - change state alway be false +options: + path: + description: + - File to get version(provide absolute path) + +author: Sam Liu +''' + +EXAMPLES = ''' +# get C:\Windows\System32\cmd.exe version in playbook +--- +- name: Get acm instance version + win_file_version: + path: 'C:\Windows\System32\cmd.exe' + register: exe_file_version + +- debug: msg="{{exe_file_version}}" + +''' From 9f02fbe07244f37422b128caa59f82ae559edb64 Mon Sep 17 00:00:00 2001 From: Robin Roth Date: Wed, 11 Nov 2015 11:57:11 +0100 Subject: [PATCH 0956/2522] better cope with rpm not returning package name if the rpm query is missing a package name (or giving some error): fail soft before the patch: the module fails because the installed_state dict is missing the package name after the patch: the missing package is assumed to not be in the correct state and is installed/removed with zypper --- packaging/os/zypper.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packaging/os/zypper.py b/packaging/os/zypper.py index b1155c6014d..0a693543d45 100644 --- a/packaging/os/zypper.py +++ b/packaging/os/zypper.py @@ -161,7 +161,7 @@ def get_package_state(m, packages): for stdoutline in stdout.splitlines(): match = rpmoutput_re.match(stdoutline) if match == None: - return None + continue package = match.group(1) result = match.group(2) if result == 'is installed': @@ -169,18 +169,13 @@ def get_package_state(m, packages): else: installed_state[package] = False - for package in packages: - if package not in installed_state: - print package + ' was not returned by rpm \n' - return None - return installed_state # Function used to make sure a package is present. def package_present(m, name, installed_state, package_type, disable_gpg_check, disable_recommends, old_zypper): packages = [] for package in name: - if installed_state[package] is False: + if package not in installed_state or installed_state[package] is False: packages.append(package) if len(packages) != 0: cmd = ['/usr/bin/zypper', '--non-interactive'] @@ -246,7 +241,7 @@ def package_latest(m, name, installed_state, package_type, disable_gpg_check, di def package_absent(m, name, installed_state, package_type, old_zypper): packages = [] for package in name: - if installed_state[package] is True: + if package not in installed_state or installed_state[package] is True: packages.append(package) if len(packages) != 0: cmd = ['/usr/bin/zypper', '--non-interactive', 'remove', '-t', package_type] From e4d9034fbc2ab7f3a0132dccdcf44a751fb8a048 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Wed, 11 Nov 2015 12:51:24 +0100 Subject: [PATCH 0957/2522] corrected replacement of last backslash --- windows/win_share.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_share.ps1 b/windows/win_share.ps1 index 86970f88d39..f409281711e 100644 --- a/windows/win_share.ps1 +++ b/windows/win_share.ps1 @@ -145,7 +145,7 @@ Try { } # normalize path and remove slash at the end - $path = (Get-Item $path).FullName -replace ".$" + $path = (Get-Item $path).FullName -replace "\\$" # need to (re-)create share If (!$share) { From e52e015791a7c1cec3d41c25b18a4243920579b6 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 11 Nov 2015 12:38:51 -0800 Subject: [PATCH 0958/2522] Documentation fixes --- cloud/amazon/route53_facts.py | 16 ++++++++-------- cloud/amazon/sns_topic.py | 8 ++++---- packaging/os/homebrew.py | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/cloud/amazon/route53_facts.py b/cloud/amazon/route53_facts.py index 16034acb51a..d6081dba4da 100644 --- a/cloud/amazon/route53_facts.py +++ b/cloud/amazon/route53_facts.py @@ -50,10 +50,10 @@ required: false next_marker: description: - - Some requests such as list_command: hosted_zones will return a maximum + - "Some requests such as list_command: hosted_zones will return a maximum number of entries - EG 100. If the number of entries exceeds this maximum another request can be sent using the NextMarker entry from the first response - to get the next page of results + to get the next page of results" required: false delegation_set_id: description: @@ -61,8 +61,8 @@ required: false start_record_name: description: - - The first name in the lexicographic ordering of domain names that you want - the list_command: record_sets to start listing from + - "The first name in the lexicographic ordering of domain names that you want + the list_command: record_sets to start listing from" required: false type: description: @@ -85,9 +85,9 @@ required: false hosted_zone_method: description: - - This is used in conjunction with query: hosted_zone. + - "This is used in conjunction with query: hosted_zone. It allows for listing details, counts or tags of various - hosted zone details. + hosted zone details." required: false choices: [ 'details', @@ -99,9 +99,9 @@ default: 'list' health_check_method: description: - - This is used in conjunction with query: health_check. + - "This is used in conjunction with query: health_check. It allows for listing details, counts or tags of various - health check details. + health check details." required: false choices: [ 'list', diff --git a/cloud/amazon/sns_topic.py b/cloud/amazon/sns_topic.py index a9de7b88f10..92d63d02c18 100755 --- a/cloud/amazon/sns_topic.py +++ b/cloud/amazon/sns_topic.py @@ -34,15 +34,15 @@ subscriptions: description: - List of subscriptions to apply to the topic. Note that AWS requires - subscriptions to be confirmed, so you will need to confirm any new - subscriptions. + subscriptions to be confirmed, so you will need to confirm any new + subscriptions. purge_subscriptions: description: - - Whether to purge any subscriptions not listed here. NOTE: AWS does not + - "Whether to purge any subscriptions not listed here. NOTE: AWS does not allow you to purge any PendingConfirmation subscriptions, so if any exist and would be purged, they are silently skipped. This means that somebody could come back later and confirm the subscription. Sorry. - Blame Amazon. + Blame Amazon." default: True extends_documentation_fragment: aws requirements: [ "boto" ] diff --git a/packaging/os/homebrew.py b/packaging/os/homebrew.py index 5225e8091c5..94d0ef865c4 100644 --- a/packaging/os/homebrew.py +++ b/packaging/os/homebrew.py @@ -39,7 +39,7 @@ default: None path: description: - - ':' separated list of paths to search for 'brew' executable. Since A package (I(formula) in homebrew parlance) location is prefixed relative to the actual path of I(brew) command, providing an alternative I(brew) path enables managing different set of packages in an alternative location in the system. + - "':' separated list of paths to search for 'brew' executable. Since A package (I(formula) in homebrew parlance) location is prefixed relative to the actual path of I(brew) command, providing an alternative I(brew) path enables managing different set of packages in an alternative location in the system." required: false default: '/usr/local/bin' state: From 3177e9eaf0144a97f5beb61a4edd4cb63f2f62eb Mon Sep 17 00:00:00 2001 From: Brad Wilson Date: Wed, 11 Nov 2015 14:42:11 -0800 Subject: [PATCH 0959/2522] Add apply_to option to rabbitmq_policy --- messaging/rabbitmq_policy.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/messaging/rabbitmq_policy.py b/messaging/rabbitmq_policy.py index 81d7068ec46..0a7d023dfb6 100644 --- a/messaging/rabbitmq_policy.py +++ b/messaging/rabbitmq_policy.py @@ -38,6 +38,12 @@ - The name of the vhost to apply to. required: false default: / + apply_to: + description: + - What the policy applies to. Requires RabbitMQ 3.2.0 or later. + required: false + default: all + choices: [all, exchanges, queues] pattern: description: - A regex of queues to apply the policy to. @@ -81,6 +87,7 @@ def __init__(self, module, name): self._name = name self._vhost = module.params['vhost'] self._pattern = module.params['pattern'] + self._apply_to = module.params['apply_to'] self._tags = module.params['tags'] self._priority = module.params['priority'] self._node = module.params['node'] @@ -112,6 +119,9 @@ def set(self): args.append(json.dumps(self._tags)) args.append('--priority') args.append(self._priority) + if (self._apply_to != 'all'): + args.append('--apply-to') + args.append(self._apply_to) return self._exec(args) def clear(self): @@ -123,6 +133,7 @@ def main(): name=dict(required=True), vhost=dict(default='/'), pattern=dict(required=True), + apply_to=dict(default='all', choices=['all', 'exchanges', 'queues']), tags=dict(type='dict', required=True), priority=dict(default='0'), node=dict(default='rabbit'), From b0298ba4f15c9869646f95aa015e323075c25162 Mon Sep 17 00:00:00 2001 From: Dylan Martin Date: Thu, 12 Nov 2015 14:38:15 -0600 Subject: [PATCH 0960/2522] added version option --- packaging/language/cpanm.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/packaging/language/cpanm.py b/packaging/language/cpanm.py index e4e8624118c..e79f155bca9 100644 --- a/packaging/language/cpanm.py +++ b/packaging/language/cpanm.py @@ -64,6 +64,12 @@ required: false default: false version_added: "2.0" + version: + description: + - minimum version of perl module to consider acceptable + required: false + default: false + version_added: "2.0" system_lib: description: - Use this if you want to install modules to the system perl include path. You must be root or have "passwordless" sudo for this to work. @@ -98,13 +104,21 @@ # install Dancer perl package into the system root path - cpanm: name=Dancer system_lib=yes + +# install Dancer if it's not already installed +# OR the installed version is older than version 1.0 +- cpanm: name=Dancer version=1.0 ''' -def _is_package_installed(module, name, locallib, cpanm): +def _is_package_installed(module, name, locallib, cpanm, version): cmd = "" if locallib: os.environ["PERL5LIB"] = "%s/lib/perl5" % locallib - cmd = "%s perl -M%s -e '1'" % (cmd, name) + cmd = "%s perl -e ' use %s" % (cmd, name) + if version: + cmd = "%s %s;'" % (cmd, version) + else: + cmd = "%s;'" % cmd res, stdout, stderr = module.run_command(cmd, check_rc=False) if res == 0: return True @@ -150,6 +164,7 @@ def main(): mirror_only=dict(default=False, type='bool'), installdeps=dict(default=False, type='bool'), system_lib=dict(default=False, type='bool', aliases=['use_sudo']), + version=dict(default=None, required=False), ) module = AnsibleModule( @@ -166,10 +181,11 @@ def main(): mirror_only = module.params['mirror_only'] installdeps = module.params['installdeps'] use_sudo = module.params['system_lib'] + version = module.params['version'] changed = False - installed = _is_package_installed(module, name, locallib, cpanm) + installed = _is_package_installed(module, name, locallib, cpanm, version) if not installed: out_cpanm = err_cpanm = '' From 19b506f64f94b8236fe53c6cba8d0691d17dfeb4 Mon Sep 17 00:00:00 2001 From: Jonathan Mainguy Date: Thu, 12 Nov 2015 17:49:44 -0500 Subject: [PATCH 0961/2522] Added style= and more colors. --- notification/irc.py | 54 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/notification/irc.py b/notification/irc.py index 28ad4417ac1..d87d26d367e 100644 --- a/notification/irc.py +++ b/notification/irc.py @@ -56,9 +56,11 @@ color: description: - Text color for the message. ("none" is a valid option in 1.6 or later, in 1.6 and prior, the default color is black, not "none"). + Added 11 more colors in version 2.0. required: false default: "none" - choices: [ "none", "yellow", "red", "green", "blue", "black" ] + choices: [ "none", "white", "black", "blue", "green", "red", "brown", "purple", "orange", "yellow", "light_green", "teal", "light_cyan", + "light_blue", "pink", "gray", "light_gray"] channel: description: - Channel name. One of nick_to or channel needs to be set. When both are set, the message will be sent to both of them. @@ -95,6 +97,13 @@ Useful for when using a faux bot and not wanting join/parts between messages. default: True version_added: "2.0" + style: + description: + - Text style for the message. Note italic does not work on some clients + default: None + required: False + choices: [ "bold", "underline", "reverse", "italic" ] + version_added: "2.0" # informational: requirements for nodes requirements: [ socket ] @@ -134,24 +143,47 @@ def send_msg(msg, server='localhost', port='6667', channel=None, nick_to=[], key=None, topic=None, - nick="ansible", color='none', passwd=False, timeout=30, use_ssl=False, part=True): + nick="ansible", color='none', passwd=False, timeout=30, use_ssl=False, part=True, style=None): '''send message to IRC''' colornumbers = { + 'white': "00", 'black': "01", + 'blue': "02", + 'green': "03", 'red': "04", - 'green': "09", + 'brown': "05", + 'purple': "06", + 'orange': "07", 'yellow': "08", - 'blue': "12", + 'light_green': "09", + 'teal': "10", + 'light_cyan': "11", + 'light_blue': "12", + 'pink': "13", + 'gray': "14", + 'light_gray': "15", + } + + stylechoices = { + 'bold': "\x02", + 'underline': "\x1F", + 'reverse': "\x16", + 'italic': "\x1D", } + try: + styletext = stylechoices[style] + except: + styletext = "" + try: colornumber = colornumbers[color] colortext = "\x03" + colornumber except: colortext = "" - message = colortext + msg + message = styletext + colortext + msg irc = socket.socket(socket.AF_INET, socket.SOCK_STREAM) if use_ssl: @@ -219,8 +251,13 @@ def main(): nick=dict(default='ansible'), nick_to=dict(required=False, type='list'), msg=dict(required=True), - color=dict(default="none", choices=["yellow", "red", "green", - "blue", "black", "none"]), + color=dict(default="none", aliases=['colour'], choices=["white", "black", "blue", + "green", "red", "brown", + "purple", "orange", "yellow", + "light_green", "teal", "light_cyan", + "light_blue", "pink", "gray", + "light_gray", "none"]), + style=dict(default="none", choices=["underline", "reverse", "bold", "italic", "none"]), channel=dict(required=False), key=dict(), topic=dict(), @@ -248,9 +285,10 @@ def main(): timeout = module.params["timeout"] use_ssl = module.params["use_ssl"] part = module.params["part"] + style = module.params["style"] try: - send_msg(msg, server, port, channel, nick_to, key, topic, nick, color, passwd, timeout, use_ssl, part) + send_msg(msg, server, port, channel, nick_to, key, topic, nick, color, passwd, timeout, use_ssl, part, style) except Exception, e: module.fail_json(msg="unable to send to IRC: %s" % e) From e5362cc76a25a734ddacf4d8ac496d9127c4a46d Mon Sep 17 00:00:00 2001 From: James Cammarata Date: Fri, 13 Nov 2015 16:47:22 -0500 Subject: [PATCH 0962/2522] Version bump for new beta 2.0.0-0.5.beta3 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index f802f1a2cdb..47c909bbc53 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.0-0.4.beta2 +2.0.0-0.5.beta3 From 875a0551032e721f06625c237bd7c2a6fecaa7fd Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Sun, 15 Nov 2015 14:31:34 -0800 Subject: [PATCH 0963/2522] corrected choices which was meant to be type --- monitoring/zabbix_host.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/zabbix_host.py b/monitoring/zabbix_host.py index 3cb27c5fbb9..5b6748a3e94 100644 --- a/monitoring/zabbix_host.py +++ b/monitoring/zabbix_host.py @@ -377,7 +377,7 @@ def main(): state=dict(default="present", choices=['present', 'absent']), timeout=dict(type='int', default=10), interfaces=dict(required=False), - force=dict(default='yes', choices='bool'), + force=dict(default=True, type='bool'), proxy=dict(required=False) ), supports_check_mode=True From 20d6e3daafa4deb87f685c690219b8c199925c01 Mon Sep 17 00:00:00 2001 From: Jordan Cohen Date: Mon, 16 Nov 2015 06:31:14 -0500 Subject: [PATCH 0964/2522] support for event alert monitors --- monitoring/datadog_monitor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py index 9318326620e..3d104049e47 100644 --- a/monitoring/datadog_monitor.py +++ b/monitoring/datadog_monitor.py @@ -52,7 +52,7 @@ description: ["The type of the monitor."] required: false default: null - choices: ['metric alert', 'service check'] + choices: ['metric alert', 'service check', 'event alert'] query: description: ["The monitor query to notify on with syntax varying depending on what type of monitor you are creating."] required: false @@ -139,7 +139,7 @@ def main(): api_key=dict(required=True), app_key=dict(required=True), state=dict(required=True, choises=['present', 'absent', 'mute', 'unmute']), - type=dict(required=False, choises=['metric alert', 'service check']), + type=dict(required=False, choises=['metric alert', 'service check', 'event alert']), name=dict(required=True), query=dict(required=False), message=dict(required=False, default=None), From 39a3255ef3d873e1b9974160ce211683f552173b Mon Sep 17 00:00:00 2001 From: Jonathan Davila Date: Mon, 16 Nov 2015 16:54:35 -0500 Subject: [PATCH 0965/2522] Sendgrid docs fix --- notification/sendgrid.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/notification/sendgrid.py b/notification/sendgrid.py index 2655b4248bb..1bac1e5f724 100644 --- a/notification/sendgrid.py +++ b/notification/sendgrid.py @@ -24,33 +24,34 @@ module: sendgrid short_description: Sends an email with the SendGrid API description: - - Sends an email with a SendGrid account through their API, not through - the SMTP service. + - "Sends an email with a SendGrid account through their API, not through + the SMTP service." notes: - - This module is non-idempotent because it sends an email through the - external API. It is idempotent only in the case that the module fails. - - Like the other notification modules, this one requires an external + - "This module is non-idempotent because it sends an email through the + external API. It is idempotent only in the case that the module fails." + - "Like the other notification modules, this one requires an external dependency to work. In this case, you'll need an active SendGrid - account. + account." options: username: description: - username for logging into the SendGrid account + - username for logging into the SendGrid account required: true password: - description: password that corresponds to the username + description: + - password that corresponds to the username required: true from_address: description: - the address in the "from" field for the email + - the address in the "from" field for the email required: true to_addresses: description: - a list with one or more recipient email addresses + - a list with one or more recipient email addresses required: true subject: description: - the desired subject for the email + - the desired subject for the email required: true author: "Matt Makai (@makaimc)" From c9e4c32f41ee6eb5edc0a513139dcaf24353ae00 Mon Sep 17 00:00:00 2001 From: Alberto Gireud Date: Mon, 16 Nov 2015 17:31:53 -0600 Subject: [PATCH 0966/2522] Fix return documentation --- cloud/openstack/os_project.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cloud/openstack/os_project.py b/cloud/openstack/os_project.py index c1958774976..37901f8412e 100644 --- a/cloud/openstack/os_project.py +++ b/cloud/openstack/os_project.py @@ -88,14 +88,6 @@ returned: On success when I(state) is 'present' type: dictionary contains: - description: - description: Project description - type: string - sample: "demodescription" - domain_id: - description: Project domain ID. Only present with Keystone >= v3. - type: string - sample: "default" id: description: Project ID type: string @@ -104,6 +96,14 @@ description: Project name type: string sample: "demoproject" + description: + description: Project description + type: string + sample: "demodescription" + enabled: + description: Boolean to indicate if project is enabled + type: bool + sample: True ''' def _needs_update(module, project): From 0688522eb75cd0e2dda1be992c08a7d71ea084f9 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Tue, 17 Nov 2015 15:36:52 +0100 Subject: [PATCH 0967/2522] fail if type parameter is empty --- windows/win_acl.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_acl.ps1 b/windows/win_acl.ps1 index 6e497c417be..4ea4a2e7c6b 100644 --- a/windows/win_acl.ps1 +++ b/windows/win_acl.ps1 @@ -98,7 +98,7 @@ $path = Get-Attr $params "path" -failifempty $true $user = Get-Attr $params "user" -failifempty $true $rights = Get-Attr $params "rights" -failifempty $true -$type = Get-Attr $params "type" -validateSet "allow","deny" -resultobj $result +$type = Get-Attr $params "type" -failifempty $true -validateSet "allow","deny" -resultobj $result $state = Get-Attr $params "state" "present" -validateSet "present","absent" -resultobj $result $inherit = Get-Attr $params "inherit" "" From c1cf8e671a5b263daa3f998360333758ece5c4e9 Mon Sep 17 00:00:00 2001 From: Xav Paice Date: Wed, 18 Nov 2015 14:33:25 +1300 Subject: [PATCH 0968/2522] Added stdout and stderr to puppet output for rc=2 --- system/puppet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/puppet.py b/system/puppet.py index 48a497c37ce..ab1339ec5ba 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -197,7 +197,7 @@ def main(): error=True, stdout=stdout, stderr=stderr) elif rc == 2: # success with changes - module.exit_json(rc=0, changed=True) + module.exit_json(rc=0, changed=True, stdout=stdout, stderr=stderr) elif rc == 124: # timeout module.exit_json( From be9058201fce7b0a2ed0f6bfbcad48e4fe81bdcb Mon Sep 17 00:00:00 2001 From: Etienne CARRIERE Date: Wed, 18 Nov 2015 06:09:28 +0100 Subject: [PATCH 0969/2522] Apply changes according to the review --- network/f5/bigip_virtual_server.py | 40 +++++++++++++++--------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/network/f5/bigip_virtual_server.py b/network/f5/bigip_virtual_server.py index 2387a40a79b..043a44c6ca8 100644 --- a/network/f5/bigip_virtual_server.py +++ b/network/f5/bigip_virtual_server.py @@ -237,10 +237,10 @@ def set_snat(api,name,snat): current_state=get_snat_type(api,name) if snat is None: return updated - if snat == 'None' and current_state != 'SRC_TRANS_NONE': + elif snat == 'None' and current_state != 'SRC_TRANS_NONE': api.LocalLB.VirtualServer.set_source_address_translation_none(virtual_servers = [name]) updated = True - if snat == 'Automap' and current_state != 'SRC_TRANS_AUTOMAP': + elif snat == 'Automap' and current_state != 'SRC_TRANS_AUTOMAP': api.LocalLB.VirtualServer.set_source_address_translation_automap(virtual_servers = [name]) updated = True return updated @@ -370,21 +370,21 @@ def main(): result = {'changed': False} # default if state == 'absent': - if not module.check_mode: - if vs_exists(api,name): - # hack to handle concurrent runs of module - # pool might be gone before we actually remove - try: - vs_remove(api,name) - result = {'changed' : True, 'deleted' : name } - except bigsuds.OperationFailed, e: - if "was not found" in str(e): - result['changed']= False - else: - raise - else: - # check-mode return value - result = {'changed': True} + if not module.check_mode: + if vs_exists(api,name): + # hack to handle concurrent runs of module + # pool might be gone before we actually remove + try: + vs_remove(api,name) + result = {'changed' : True, 'deleted' : name } + except bigsuds.OperationFailed, e: + if "was not found" in str(e): + result['changed']= False + else: + raise + else: + # check-mode return value + result = {'changed': True} elif state == 'present': update = False @@ -393,8 +393,8 @@ def main(): module.fail_json(msg="both destination and port must be supplied to create a VS") if not module.check_mode: # a bit of a hack to handle concurrent runs of this module. - # even though we've checked the pool doesn't exist, - # it may exist by the time we run create_pool(). + # even though we've checked the virtual_server doesn't exist, + # it may exist by the time we run virtual_server(). # this catches the exception and does something smart # about it! try: @@ -405,7 +405,7 @@ def main(): set_default_persistence_profiles(api,name,default_persistence_profile) result = {'changed': True} except bigsuds.OperationFailed, e: - raise Exception('Error on creating Virtual Server : %s' % e) + raise Exception('Error on creating Virtual Server : %s' % e) else: # check-mode return value result = {'changed': True} From aaf16fe3376773ab50e9c7b98363e294d38eedc4 Mon Sep 17 00:00:00 2001 From: Etienne CARRIERE Date: Wed, 18 Nov 2015 06:52:03 +0100 Subject: [PATCH 0970/2522] Developpement of enabled/disabled state --- network/f5/bigip_virtual_server.py | 35 ++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/network/f5/bigip_virtual_server.py b/network/f5/bigip_virtual_server.py index 043a44c6ca8..f7e243c5d6e 100644 --- a/network/f5/bigip_virtual_server.py +++ b/network/f5/bigip_virtual_server.py @@ -54,9 +54,13 @@ choices: ['yes', 'no'] state: description: - - Pool member state + - Virtual Server state required: false default: present + description: + - Absent : delete the VS if present + - present (and its synonym enabled) : create if needed the VS and set state to enabled + - absent : create if needed the VS and set state to disabled choices: ['present', 'absent', 'enabled', 'disabled'] aliases: [] partition: @@ -292,7 +296,22 @@ def set_port(api,name,port): except bigsuds.OperationFailed, e: raise Exception('Error on setting port : %s'% e ) +def get_state(api,name): + return api.LocalLB.VirtualServer.get_enabled_state(virtual_servers = [name])[0] +def set_state(api,name,state): + updated=False + try: + current_state=get_state(api,name) + # We consider that being present is equivalent to enabled + if state == 'present': + state='enabled' + if STATES[state] != current_state: + api.LocalLB.VirtualServer.set_enabled_state(virtual_servers=[name],states=[STATES[state]]) + updated=True + return updated + except bigsuds.OperationFailed, e: + raise Exception('Error on setting state : %s'% e ) def get_description(api,name): return api.LocalLB.VirtualServer.get_description(virtual_servers = [name])[0] @@ -386,7 +405,7 @@ def main(): # check-mode return value result = {'changed': True} - elif state == 'present': + else: update = False if not vs_exists(api, name): if (not destination) or (not port): @@ -403,6 +422,7 @@ def main(): set_snat(api,name,snat) set_description(api,name,description) set_default_persistence_profiles(api,name,default_persistence_profile) + set_state(api,name,state) result = {'changed': True} except bigsuds.OperationFailed, e: raise Exception('Error on creating Virtual Server : %s' % e) @@ -424,6 +444,7 @@ def main(): result['changed']|=set_snat(api,name,snat) result['changed']|=set_profiles(api,name,all_profiles) result['changed']|=set_default_persistence_profiles(api,name,default_persistence_profile) + result['changed']|=set_state(api,name,state) api.System.Session.submit_transaction() except Exception,e: raise Exception("Error on updating Virtual Server : %s" % e) @@ -431,16 +452,6 @@ def main(): # check-mode return value result = {'changed': True} - elif state in ('disabled', 'enabled'): - if name is None: - module.fail_json(msg="name parameter required when " \ - "state=enabled/disabled") - if not module.check_mode: - pass - else: - # check-mode return value - result = {'changed': True} - except Exception, e: module.fail_json(msg="received exception: %s" % e) From 11e8a176842622a3fad43813636115547830d963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guido=20G=C3=BCnther?= Date: Wed, 18 Nov 2015 17:47:53 +0100 Subject: [PATCH 0971/2522] zypper: Add returncode to result This will be used in integration tssts and makes the whole thing more similar to the yum module. --- packaging/os/zypper.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packaging/os/zypper.py b/packaging/os/zypper.py index 0a693543d45..78c2d489eaa 100644 --- a/packaging/os/zypper.py +++ b/packaging/os/zypper.py @@ -144,7 +144,7 @@ def get_package_state(m, packages): package = packages[i] if not os.path.isfile(package) and not '://' in package: stderr = "No Package file matching '%s' found on system" % package - m.fail_json(msg=stderr) + m.fail_json(msg=stderr, rc=1) # Get packagename from rpm file cmd = ['/bin/rpm', '--query', '--qf', '%{NAME}', '--package'] cmd.append(package) @@ -311,11 +311,12 @@ def main(): if rc != 0: if stderr: - module.fail_json(msg=stderr) + module.fail_json(msg=stderr, rc=rc) else: - module.fail_json(msg=stdout) + module.fail_json(msg=stdout, rc=rc) result['changed'] = changed + result['rc'] = rc module.exit_json(**result) From 7023d87e05b076953efc2d0b970ca24c0f0bb11e Mon Sep 17 00:00:00 2001 From: Sebastien Couture Date: Wed, 18 Nov 2015 18:15:59 -0500 Subject: [PATCH 0972/2522] Added support for DNS SRV records --- network/dnsmadeeasy.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/network/dnsmadeeasy.py b/network/dnsmadeeasy.py index cce7bd10082..090e1f6f342 100644 --- a/network/dnsmadeeasy.py +++ b/network/dnsmadeeasy.py @@ -210,10 +210,12 @@ def getMatchingRecord(self, record_name, record_type, record_value): if result['name'] == record_name and result['type'] == record_type: return result return False - elif record_type in ["MX", "NS", "TXT"]: + elif record_type in ["MX", "NS", "TXT", "SRV"]: for result in self.all_records: if record_type == "MX": value = record_value.split(" ")[1] + elif record_type == "SRV": + value = record_value.split(" ")[3] else: value = record_value if result['name'] == record_name and result['type'] == record_type and result['value'] == value: @@ -309,6 +311,13 @@ def main(): new_record["mxLevel"] = new_record["value"].split(" ")[0] new_record["value"] = new_record["value"].split(" ")[1] + # Special handling for SRV records + if new_record["type"] == "SRV": + new_record["priority"] = new_record["value"].split(" ")[0] + new_record["weight"] = new_record["value"].split(" ")[1] + new_record["port"] = new_record["value"].split(" ")[2] + new_record["value"] = new_record["value"].split(" ")[3] + # Compare new record against existing one changed = False if current_record: From a59f1f528e67daf580c9e1859a5fb719914e7834 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Fri, 20 Nov 2015 09:08:42 +0100 Subject: [PATCH 0973/2522] fix race condition and missing property --- windows/win_updates.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_updates.ps1 b/windows/win_updates.ps1 index 3d5bc4c57c4..890e3670d86 100644 --- a/windows/win_updates.ps1 +++ b/windows/win_updates.ps1 @@ -337,7 +337,7 @@ Function RunAsScheduledJob { $sw = [System.Diagnostics.Stopwatch]::StartNew() # NB: output from scheduled jobs is delayed after completion (including the sub-objects after the primary Output object is available) - While (($job.Output -eq $null -or -not $job.Output.Keys.Contains('job_output')) -and $sw.ElapsedMilliseconds -lt 15000) { + While (($job.Output -eq $null -or -not ($job.Output | Get-Member -Name Keys) -or -not $job.Output.Keys.Contains('job_output')) -and $sw.ElapsedMilliseconds -lt 15000) { Write-DebugLog "Waiting for job output to populate..." Start-Sleep -Milliseconds 500 } From a56fe04683031e1eec0414c1726df4704c7ff79f Mon Sep 17 00:00:00 2001 From: Ryan Sydnor Date: Fri, 20 Nov 2015 13:55:44 -0500 Subject: [PATCH 0974/2522] Use boto normalized location for bucket creation If a bucket is being created in us-east-1, the module passed 'us-east-1' to boto's s3.create_bucket method rather than Location.DEFAULT (an empty string). This caused boto to generate invalid XML which AWS was unable to interpret. --- cloud/amazon/s3_bucket.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cloud/amazon/s3_bucket.py b/cloud/amazon/s3_bucket.py index aa6cc9d1e41..22e68927016 100644 --- a/cloud/amazon/s3_bucket.py +++ b/cloud/amazon/s3_bucket.py @@ -129,11 +129,10 @@ def create_tags_container(tags): tags_obj.add_tag_set(tag_set) return tags_obj -def create_bucket(connection, module): +def create_bucket(connection, module, location): policy = module.params.get("policy") name = module.params.get("name") - region = module.params.get("region") requester_pays = module.params.get("requester_pays") tags = module.params.get("tags") versioning = module.params.get("versioning") @@ -143,7 +142,7 @@ def create_bucket(connection, module): bucket = connection.get_bucket(name) except S3ResponseError, e: try: - bucket = connection.create_bucket(name, location=region) + bucket = connection.create_bucket(name, location=location) changed = True except S3CreateError, e: module.fail_json(msg=e.message) @@ -376,7 +375,7 @@ def main(): state = module.params.get("state") if state == 'present': - create_bucket(connection, module) + create_bucket(connection, module, location) elif state == 'absent': destroy_bucket(connection, module) From 66964f660aa62bff95fc0a2ab2444a2110eabd4b Mon Sep 17 00:00:00 2001 From: Olaf Kilian Date: Fri, 20 Nov 2015 20:25:50 +0100 Subject: [PATCH 0975/2522] Set no_log for password argument --- cloud/docker/docker_login.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/docker/docker_login.py b/cloud/docker/docker_login.py index c00dc3f900d..4fc4decbe62 100644 --- a/cloud/docker/docker_login.py +++ b/cloud/docker/docker_login.py @@ -220,7 +220,7 @@ def main(): argument_spec = dict( registry = dict(required=False, default='https://index.docker.io/v1/'), username = dict(required=True), - password = dict(required=True), + password = dict(required=True, no_log=True), email = dict(required=False, default=None), reauth = dict(required=False, default=False, type='bool'), dockercfg_path = dict(required=False, default='~/.docker/config.json'), From c629d5b0139c0dcd680af273745ab59ff30b19c0 Mon Sep 17 00:00:00 2001 From: Olaf Kilian Date: Fri, 20 Nov 2015 21:05:19 +0100 Subject: [PATCH 0976/2522] Add requirement and check for compatible version of docker-py --- cloud/docker/docker_login.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/cloud/docker/docker_login.py b/cloud/docker/docker_login.py index 4fc4decbe62..cdc1f95d042 100644 --- a/cloud/docker/docker_login.py +++ b/cloud/docker/docker_login.py @@ -70,7 +70,7 @@ required: false default: 600 -requirements: [ "python >= 2.6", "docker-py" ] +requirements: [ "python >= 2.6", "docker-py >= 1.1.0" ] ''' EXAMPLES = ''' @@ -102,11 +102,16 @@ import json import base64 from urlparse import urlparse +from distutils.version import StrictVersion try: import docker.client from docker.errors import APIError as DockerAPIError has_lib_docker = True + if StrictVersion(docker.__version__) >= StrictVersion("1.1.0"): + has_correct_lib_docker_version = True + else: + has_correct_lib_docker_version = False except ImportError, e: has_lib_docker = False @@ -231,7 +236,10 @@ def main(): ) if not has_lib_docker: - module.fail_json(msg="python library docker-py required: pip install docker-py==1.1.0") + module.fail_json(msg="python library docker-py required: pip install docker-py>=1.1.0") + + if not has_correct_lib_docker_version: + module.fail_json(msg="your version of docker-py is outdated: pip install docker-py>=1.1.0") if not has_lib_requests_execeptions: module.fail_json(msg="python library requests required: pip install requests") From 19374903ac679ee100a146f0615b92517020193b Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Fri, 20 Nov 2015 12:36:36 -0800 Subject: [PATCH 0977/2522] Switch StrictVersion for LooseVersion since some distros ship beta versions and StrictVersion would fail on that. Also clean up some minor style things --- cloud/docker/docker_login.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/cloud/docker/docker_login.py b/cloud/docker/docker_login.py index cdc1f95d042..b2117464fd6 100644 --- a/cloud/docker/docker_login.py +++ b/cloud/docker/docker_login.py @@ -102,24 +102,25 @@ import json import base64 from urlparse import urlparse -from distutils.version import StrictVersion +from distutils.version import LooseVersion try: import docker.client from docker.errors import APIError as DockerAPIError has_lib_docker = True - if StrictVersion(docker.__version__) >= StrictVersion("1.1.0"): + if LooseVersion(docker.__version__) >= LooseVersion("1.1.0"): has_correct_lib_docker_version = True else: has_correct_lib_docker_version = False -except ImportError, e: +except ImportError: has_lib_docker = False try: - from requests.exceptions import * - has_lib_requests_execeptions = True -except ImportError, e: - has_lib_requests_execeptions = False + import requests + has_lib_requests = True +except ImportError: + has_lib_requests = False + class DockerLoginManager: @@ -161,7 +162,7 @@ def login(self): self.module.fail_json(msg="failed to login to the remote registry", error=repr(e)) # Get status from registry response. - if self.response.has_key("Status"): + if "Status" in self.response: self.log.append(self.response["Status"]) # Update the dockercfg if not in check mode. @@ -186,9 +187,9 @@ def update_dockercfg(self): docker_config = json.load(open(self.dockercfg_path, "r")) except ValueError: docker_config = dict() - if not docker_config.has_key("auths"): + if "auths" not in docker_config: docker_config["auths"] = dict() - if not docker_config["auths"].has_key(self.registry): + if self.registry not in docker_config["auths"]: docker_config["auths"][self.registry] = dict() # Calculate docker credentials based on current parameters. @@ -219,6 +220,7 @@ def get_msg(self): def has_changed(self): return self.changed + def main(): module = AnsibleModule( @@ -241,7 +243,7 @@ def main(): if not has_correct_lib_docker_version: module.fail_json(msg="your version of docker-py is outdated: pip install docker-py>=1.1.0") - if not has_lib_requests_execeptions: + if not has_lib_requests: module.fail_json(msg="python library requests required: pip install requests") try: From b87e3ce36258e9c7d5184be2af9cf2e4aa73574f Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Fri, 20 Nov 2015 13:57:58 -0800 Subject: [PATCH 0978/2522] Quote strings that make the module docs fail to build --- cloud/docker/docker_login.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/docker/docker_login.py b/cloud/docker/docker_login.py index b2117464fd6..05fac1dd5d0 100644 --- a/cloud/docker/docker_login.py +++ b/cloud/docker/docker_login.py @@ -33,9 +33,9 @@ options: registry: description: - - URL of the registry, defaults to: https://index.docker.io/v1/ + - "URL of the registry, defaults to: https://index.docker.io/v1/" required: false - default: https://index.docker.io/v1/ + default: "https://index.docker.io/v1/" username: description: - The username for the registry account From b9bb6d8d29fbd2b6ffc0548ec93f13411d273bcb Mon Sep 17 00:00:00 2001 From: Jiri tyr Date: Tue, 22 Sep 2015 22:27:27 +0100 Subject: [PATCH 0979/2522] Adding yumrepo module This patch is adding a new module which allows to add and remove YUM repository definitions. The module implements all repository options as described in the `yum.conf` manual page. --- packaging/os/yumrepo.py | 560 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 560 insertions(+) create mode 100644 packaging/os/yumrepo.py diff --git a/packaging/os/yumrepo.py b/packaging/os/yumrepo.py new file mode 100644 index 00000000000..e2052cea191 --- /dev/null +++ b/packaging/os/yumrepo.py @@ -0,0 +1,560 @@ +#!/usr/bin/python +# encoding: utf-8 + +# (c) 2015, Jiri Tyr +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + + +import ConfigParser +import os + + +DOCUMENTATION = ''' +--- +module: yumrepo +author: Jiri Tyr (@jtyr) +version_added: '2.0' +short_description: Add and remove YUM repositories +description: + - Add or remove YUM repositories in RPM-based Linux distributions. + +options: + bandwidth: + required: false + default: 0 + description: + - Maximum available network bandwidth in bytes/second. Used with the + I(throttle) option. + - If I(throttle) is a percentage and bandwidth is C(0) then bandwidth + throttling will be disabled. If I(throttle) is expressed as a data rate + (bytes/sec) then this option is ignored. Default is C(0) (no bandwidth + throttling). + baseurl: + required: false + default: None + description: + - URL to the directory where the yum repository's 'repodata' directory + lives. + - This or the I(mirrorlist) parameter is required. + cost: + required: false + default: 1000 + description: + - Relative cost of accessing this repository. Useful for weighing one + repo's packages as greater/less than any other. + description: + required: false + default: None + description: + - A human readable string describing the repository. + enabled: + required: false + choices: ['yes', 'no'] + default: 'yes' + description: + - This tells yum whether or not use this repository. + enablegroups: + required: false + choices: ['yes', 'no'] + default: 'yes' + description: + - Determines whether yum will allow the use of package groups for this + repository. + exclude: + required: false + default: None + description: + - List of packages to exclude from updates or installs. This should be a + space separated list. Shell globs using wildcards (eg. C(*) and C(?)) + are allowed. + - The list can also be a regular YAML array. + failovermethod: + required: false + choices: [roundrobin, priority] + default: roundrobin + description: + - C(roundrobin) randomly selects a URL out of the list of URLs to start + with and proceeds through each of them as it encounters a failure + contacting the host. + - C(priority) starts from the first baseurl listed and reads through them + sequentially. + file: + required: false + default: None + description: + - File to use to save the repo in. Defaults to the value of I(name). + gpgcakey: + required: false + default: None + description: + - A URL pointing to the ASCII-armored CA key file for the repository. + gpgcheck: + required: false + choices: ['yes', 'no'] + default: 'no' + description: + - Tells yum whether or not it should perform a GPG signature check on + packages. + gpgkey: + required: false + default: None + description: + - A URL pointing to the ASCII-armored GPG key file for the repository. + http_caching: + required: false + choices: [all, packages, none] + default: all + description: + - Determines how upstream HTTP caches are instructed to handle any HTTP + downloads that Yum does. + - C(all) means that all HTTP downloads should be cached. + - C(packages) means that only RPM package downloads should be cached (but + not repository metadata downloads). + - C(none) means that no HTTP downloads should be cached. + includepkgs: + required: false + default: None + description: + - List of packages you want to only use from a repository. This should be + a space separated list. Shell globs using wildcards (eg. C(*) and C(?)) + are allowed. Substitution variables (e.g. C($releasever)) are honored + here. + - The list can also be a regular YAML array. + keepalive: + required: false + choices: ['yes', 'no'] + default: 'no' + description: + - This tells yum whether or not HTTP/1.1 keepalive should be used with + this repository. This can improve transfer speeds by using one + connection when downloading multiple files from a repository. + metadata_expire: + required: false + default: 21600 + description: + - Time (in seconds) after which the metadata will expire. + - Default value is 6 hours. + metalink: + required: false + default: None + description: + - Specifies a URL to a metalink file for the repomd.xml, a list of + mirrors for the entire repository are generated by converting the + mirrors for the repomd.xml file to a baseurl. + mirrorlist: + required: false + default: None + description: + - Specifies a URL to a file containing a list of baseurls. + - This or the I(baseurl) parameter is required. + mirrorlist_expire: + required: false + default: 21600 + description: + - Time (in seconds) after which the mirrorlist locally cached will + expire. + - Default value is 6 hours. + name: + required: true + description: + - Unique repository ID. + password: + required: false + default: None + description: + - Password to use with the username for basic authentication. + protect: + required: false + choices: ['yes', 'no'] + default: 'no' + description: + - Protect packages from updates from other repositories. + proxy: + required: false + default: None + description: + - URL to the proxy server that yum should use. + proxy_password: + required: false + default: None + description: + - Username to use for proxy. + proxy_username: + required: false + default: None + description: + - Password for this proxy. + repo_gpgcheck: + required: false + choices: ['yes', 'no'] + default: 'no' + description: + - This tells yum whether or not it should perform a GPG signature check + on the repodata from this repository. + reposdir: + required: false + default: /etc/yum.repos.d + description: + - Directory where the C(.repo) files will be stored. + retries: + required: false + default: 10 + description: + - Set the number of times any attempt to retrieve a file should retry + before returning an error. Setting this to C(0) makes yum try forever. + skip_if_unavailable: + required: false + choices: ['yes', 'no'] + default: 'no' + description: + - If set to C(yes) yum will continue running if this repository cannot be + contacted for any reason. This should be set carefully as all repos are + consulted for any given command. + sslcacert: + required: false + default: None + description: + - Path to the directory containing the databases of the certificate + authorities yum should use to verify SSL certificates. + ssl_check_cert_permissions: + required: false + choices: ['yes', 'no'] + default: 'no' + description: + - Whether yum should check the permissions on the paths for the + certificates on the repository (both remote and local). + - If we can't read any of the files then yum will force + I(skip_if_unavailable) to be true. This is most useful for non-root + processes which use yum on repos that have client cert files which are + readable only by root. + sslclientcert: + required: false + default: None + description: + - Path to the SSL client certificate yum should use to connect to + repos/remote sites. + sslclientkey: + required: false + default: None + description: + - Path to the SSL client key yum should use to connect to repos/remote + sites. + sslverify: + required: false + choices: ['yes', 'no'] + default: 'yes' + description: + - Defines whether yum should verify SSL certificates/hosts at all. + state: + required: false + choices: [absent, present] + default: present + description: + - A source string state. + throttle: + required: false + default: None + description: + - Enable bandwidth throttling for downloads. + - This option can be expressed as a absolute data rate in bytes/sec. An + SI prefix (k, M or G) may be appended to the bandwidth value. + timeout: + required: false + default: 30 + description: + - Number of seconds to wait for a connection before timing out. + username: + required: false + default: None + description: + - Username to use for basic authentication to a repo or really any url. + +extends_documentation_fragment: files + +notes: + - All comments will be removed if modifying an existing repo file. + - Section order is preserved in an existing repo file. + - Parameters in a section are ordered alphabetically in an existing repo + file. + - The repo file will be automatically deleted if it contains no repository. +''' + +EXAMPLES = ''' +- name: Add repository + yumrepo: + name: epel + description: EPEL YUM repo + baseurl: http://download.fedoraproject.org/pub/epel/$releasever/$basearch/ + +- name: Add multiple repositories into the same file (1/2) + yumrepo: + name: epel + description: EPEL YUM repo + file: external_repos + baseurl: http://download.fedoraproject.org/pub/epel/$releasever/$basearch/ + gpgcheck: no +- name: Add multiple repositories into the same file (2/2) + yumrepo: + name: rpmforge + description: RPMforge YUM repo + file: external_repos + baseurl: http://apt.sw.be/redhat/el7/en/$basearch/rpmforge + mirrorlist: http://mirrorlist.repoforge.org/el7/mirrors-rpmforge + enabled: no + +- name: Remove repository + yumrepo: + name: epel + state: absent + +- name: Remove repository from a specific repo file + yumrepo: + name: epel + file: external_repos + state: absent +''' + +RETURN = ''' +repo: + description: repository name + returned: success + type: string + sample: "epel" +state: + description: state of the target, after execution + returned: success + type: string + sample: "present" +''' + + +class YumRepo(object): + # Class global variables + module = None + params = None + section = None + repofile = ConfigParser.RawConfigParser() + + # List of parameters which will be allowed in the repo file output + allowed_params = [ + 'bandwidth', 'baseurl', 'cost', 'enabled', 'enablegroups', 'exclude', + 'failovermethod', 'gpgcakey', 'gpgcheck', 'gpgkey', 'http_caching', + 'includepkgs', 'keepalive', 'metadata_expire', 'metalink', + 'mirrorlist', 'mirrorlist_expire', 'name', 'password', 'protect', + 'proxy', 'proxy_password', 'proxy_username', 'repo_gpgcheck', + 'retries', 'skip_if_unavailable', 'sslcacert', + 'ssl_check_cert_permissions', 'sslclientcert', 'sslclientkey', + 'sslverify', 'throttle', 'timeout', 'username'] + + # List of parameters which can be a list + list_params = ['exclude', 'includepkgs'] + + def __init__(self, module): + # To be able to use fail_json + self.module = module + # Shortcut for the params + self.params = self.module.params + # Section is always the repoid + self.section = self.params['repoid'] + + # Check if repo directory exists + repos_dir = self.params['reposdir'] + if not os.path.isdir(repos_dir): + self.module.fail_json( + msg='Repo directory "%s" does not exist.' % repos_dir) + + # Get the given or the default repo file name + repo_file = self.params['repoid'] + if self.params['file'] is not None: + repo_file = self.params['file'] + + # Set dest; also used to set dest parameter for the FS attributes + self.params['dest'] = os.path.join(repos_dir, "%s.repo" % repo_file) + + # Read the repo file if it exists + if os.path.isfile(self.params['dest']): + self.repofile.read(self.params['dest']) + + def add(self): + # Remove already existing repo and create a new one + if self.repofile.has_section(self.section): + self.repofile.remove_section(self.section) + + # Add section + self.repofile.add_section(self.section) + + # Baseurl/mirrorlist is not required because for removal we need only + # the repo name. This is why we check if the baseurl/mirrorlist is + # defined. + if (self.params['baseurl'], self.params['mirrorlist']) == (None, None): + self.module.fail_json( + msg='Paramater "baseurl" or "mirrorlist" is required for ' + 'adding a new repo.') + + # Set options + for key, value in sorted(self.params.items()): + if key in self.list_params and isinstance(value, list): + # Join items into one string for specific parameters + value = ' '.join(value) + elif isinstance(value, bool): + # Convert boolean value to integer + value = int(value) + + # Set the value only if it was defined (default is None) + if value is not None and key in self.allowed_params: + self.repofile.set(self.section, key, value) + + def save(self): + if len(self.repofile.sections()): + # Write data into the file + try: + fd = open(self.params['dest'], 'wb') + except IOError: + self.module.fail_json( + msg='Cannot open repo file %s.' % + self.params['dest']) + + try: + try: + self.repofile.write(fd) + except Error: + self.module.fail_json( + msg='Cannot write repo file %s.' % + self.params['dest']) + finally: + fd.close() + else: + # Remove the file if there are not repos + try: + os.remove(self.params['dest']) + except OSError: + self.module.fail_json( + msg='Cannot remove empty repo file %s.' % + self.params['dest']) + + def remove(self): + # Remove section if exists + if self.repofile.has_section(self.section): + self.repofile.remove_section(self.section) + + def dump(self): + repo_string = "" + + # Compose the repo file + for section in sorted(self.repofile.sections()): + repo_string += "[%s]\n" % section + + for key, value in sorted(self.repofile.items(section)): + repo_string += "%s = %s\n" % (key, value) + + repo_string += "\n" + + return repo_string + + +def main(): + # Module settings + module = AnsibleModule( + argument_spec=dict( + bandwidth=dict(), + baseurl=dict(), + cost=dict(), + description=dict(), + enabled=dict(type='bool'), + enablegroups=dict(type='bool'), + exclude=dict(), + failovermethod=dict(choices=['roundrobin', 'priority']), + file=dict(), + gpgcakey=dict(), + gpgcheck=dict(type='bool'), + gpgkey=dict(), + http_caching=dict(choices=['all', 'packages', 'none']), + includepkgs=dict(), + keepalive=dict(type='bool'), + metadata_expire=dict(), + metalink=dict(), + mirrorlist=dict(), + mirrorlist_expire=dict(), + name=dict(required=True), + password=dict(no_log=True), + protect=dict(type='bool'), + proxy=dict(), + proxy_password=dict(no_log=True), + proxy_username=dict(), + repo_gpgcheck=dict(type='bool'), + reposdir=dict(default='/etc/yum.repos.d'), + retries=dict(), + skip_if_unavailable=dict(type='bool'), + sslcacert=dict(), + ssl_check_cert_permissions=dict(type='bool'), + sslclientcert=dict(), + sslclientkey=dict(), + sslverify=dict(type='bool'), + state=dict(choices=['present', 'absent'], default='present'), + throttle=dict(), + timeout=dict(), + username=dict(), + ), + add_file_common_args=True, + supports_check_mode=True, + ) + + name = module.params['name'] + state = module.params['state'] + + # Rename "name" and "description" to ensure correct key sorting + module.params['repoid'] = module.params['name'] + module.params['name'] = module.params['description'] + del module.params['description'] + + # Instantiate the YumRepo object + yumrepo = YumRepo(module) + + # Get repo status before change + yumrepo_before = yumrepo.dump() + + # Perform action depending on the state + if state == 'present': + yumrepo.add() + elif state == 'absent': + yumrepo.remove() + + # Get repo status after change + yumrepo_after = yumrepo.dump() + + # Compare repo states + changed = yumrepo_before != yumrepo_after + + # Save the file only if not in check mode and if there was a change + if not module.check_mode and changed: + yumrepo.save() + + # Change file attributes if needed + if os.path.isfile(module.params['dest']): + file_args = module.load_file_common_arguments(module.params) + changed = module.set_fs_attributes_if_different(file_args, changed) + + # Print status of the change + module.exit_json(changed=changed, repo=name, state=state) + + +# Import module snippets +from ansible.module_utils.basic import * + + +if __name__ == '__main__': + main() From 367b88a2ab19d28e42ffffeb4702badebc09dbcb Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Sat, 21 Nov 2015 16:07:12 -0800 Subject: [PATCH 0980/2522] removed json import --- system/puppet.py | 1 - 1 file changed, 1 deletion(-) diff --git a/system/puppet.py b/system/puppet.py index 48a497c37ce..2cb82f8f85a 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -15,7 +15,6 @@ # You should have received a copy of the GNU General Public License # along with this software. If not, see . -import json import os import pipes import stat From 0d0c887f029eddf51a5d76c83b8cd3a33d033da2 Mon Sep 17 00:00:00 2001 From: Jonathan Mainguy Date: Mon, 16 Nov 2015 23:54:57 -0500 Subject: [PATCH 0981/2522] Unify mysql modules. Added config_file, ssl_ca, ssl_cert, ssl_key, changed connect method, added error checking where a backtrace previoussly was possible --- database/mysql/mysql_replication.py | 132 +++++----------------------- 1 file changed, 23 insertions(+), 109 deletions(-) diff --git a/database/mysql/mysql_replication.py b/database/mysql/mysql_replication.py index c8e342a1d23..8729514760e 100644 --- a/database/mysql/mysql_replication.py +++ b/database/mysql/mysql_replication.py @@ -45,27 +45,6 @@ - resetslave - resetslaveall default: getslave - 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: - description: - - Port of the MySQL server. Requires login_host be defined as other then localhost if login_port is used - required: False - default: 3306 - version_added: "1.9" - login_unix_socket: - description: - - unix socket to connect mysql server master_host: description: - same as mysql variable @@ -118,6 +97,8 @@ required: false default: null version_added: "2.0" + +extends_documentation_fragment: mysql ''' EXAMPLES = ''' @@ -134,7 +115,6 @@ - mysql_replication: mode=getslave login_host=ansible.example.com login_port=3308 ''' -import ConfigParser import os import warnings @@ -200,65 +180,6 @@ def changemaster(cursor, chm, chm_params): cursor.execute(query, chm_params) -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( @@ -284,6 +205,10 @@ def main(): master_ssl_cert=dict(default=None), master_ssl_key=dict(default=None), master_ssl_cipher=dict(default=None), + config_file=dict(default="~/.my.cnf"), + ssl_cert=dict(default=None), + ssl_key=dict(default=None), + ssl_ca=dict(default=None), ) ) user = module.params["login_user"] @@ -307,42 +232,27 @@ def main(): master_ssl_key = module.params["master_ssl_key"] master_ssl_cipher = module.params["master_ssl_cipher"] master_auto_position = module.params["master_auto_position"] + 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)) if not mysqldb_found: module.fail_json(msg="the python mysqldb module is required") 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"], unix_socket=module.params["login_unix_socket"], user=login_user, passwd=login_password) - elif module.params["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=module.params["login_port"], user=login_user, passwd=login_password) - 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 (%s: %s)" % (errno, errstr) ) - try: - cursor = db_connection.cursor(cursorclass=MySQLdb.cursors.DictCursor) + cursor = mysql_connect(module, login_user, login_password, config_file, ssl_cert, ssl_key, ssl_ca, None, 'MySQLdb.cursors.DictCursor') except Exception, e: - module.fail_json(msg="Trouble getting DictCursor from db_connection: %s" % e) + 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)) if mode in "getmaster": masterstatus = get_master_status(cursor) @@ -355,8 +265,8 @@ def main(): slavestatus = get_slave_status(cursor) try: module.exit_json( **slavestatus ) - except TypeError: - module.fail_json(msg="Server is not configured as mysql slave") + except TypeError, e: + module.fail_json(msg="Server is not configured as mysql slave. ERROR: %s" % e) elif mode in "changemaster": chm=[] @@ -407,7 +317,10 @@ def main(): chm_params['master_ssl_cipher'] = master_ssl_cipher if master_auto_position: chm.append("MASTER_AUTO_POSITION = 1") - changemaster(cursor, chm, chm_params) + try: + changemaster(cursor, chm, chm_params) + except Exception, e: + module.fail_json(msg='%s. Query == CHANGE MASTER TO %s' % (e, chm)) module.exit_json(changed=True) elif mode in "startslave": started = start_slave(cursor) @@ -436,5 +349,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.mysql import * main() warnings.simplefilter("ignore") From c978f4b33236f7b95da80fdac084ff12f6d8879e Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 24 Nov 2015 09:55:13 -0800 Subject: [PATCH 0982/2522] updated version added --- packaging/os/yumrepo.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packaging/os/yumrepo.py b/packaging/os/yumrepo.py index e2052cea191..ba4aaa2ae8f 100644 --- a/packaging/os/yumrepo.py +++ b/packaging/os/yumrepo.py @@ -27,7 +27,7 @@ --- module: yumrepo author: Jiri Tyr (@jtyr) -version_added: '2.0' +version_added: '2.1' short_description: Add and remove YUM repositories description: - Add or remove YUM repositories in RPM-based Linux distributions. @@ -283,7 +283,8 @@ description: - Username to use for basic authentication to a repo or really any url. -extends_documentation_fragment: files +extends_documentation_fragment: + - files notes: - All comments will be removed if modifying an existing repo file. From 87065005aa915e1a5d8ee286f431be120f48726e Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Mon, 16 Nov 2015 18:48:21 +0100 Subject: [PATCH 0983/2522] cloudstack: new module cs_volume --- cloud/cloudstack/cs_volume.py | 467 ++++++++++++++++++++++++++++++++++ 1 file changed, 467 insertions(+) create mode 100644 cloud/cloudstack/cs_volume.py diff --git a/cloud/cloudstack/cs_volume.py b/cloud/cloudstack/cs_volume.py new file mode 100644 index 00000000000..30548555587 --- /dev/null +++ b/cloud/cloudstack/cs_volume.py @@ -0,0 +1,467 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2015, Jefferson Girão +# (c) 2015, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: cs_volume +short_description: Manages volumes on Apache CloudStack based clouds. +description: + - Create, destroy, attach, detach volumes. +version_added: "2.1" +author: + - "Jefferson Girão (@jeffersongirao)" + - "René Moser (@resmo)" +options: + name: + description: + - Name of the volume. + - C(name) can only contain ASCII letters. + required: true + account: + description: + - Account the volume is related to. + required: false + default: null + custom_id: + description: + - Custom id to the resource. + - Allowed to Root Admins only. + required: false + default: null + disk_offering: + description: + - Name of the disk offering to be used. + - Required one of C(disk_offering), C(snapshot) if volume is not already C(state=present). + required: false + default: null + display_volume: + description: + - Whether to display the volume to the end user or not. + - Allowed to Root Admins only. + required: false + default: true + domain: + description: + - Name of the domain the volume to be deployed in. + required: false + default: null + max_iops: + description: + - Max iops + required: false + default: null + min_iops: + description: + - Min iops + required: false + default: null + project: + description: + - Name of the project the volume to be deployed in. + required: false + default: null + size: + description: + - Size of disk in GB + required: false + default: null + snapshot: + description: + - The snapshot name for the disk volume. + - Required one of C(disk_offering), C(snapshot) if volume is not already C(state=present). + required: false + default: null + force: + description: + - Force removal of volume even it is attached to a VM. + - Considered on C(state=absnet) only. + required: false + default: false + vm: + description: + - Name of the virtual machine to attach the volume to. + required: false + default: null + zone: + description: + - Name of the zone in which the volume should be deployed. + - If not set, default zone is used. + required: false + default: null + state: + description: + - State of the volume. + required: false + default: 'present' + choices: [ 'present', 'absent', 'attached', 'detached' ] + poll_async: + description: + - Poll async jobs until job has finished. + required: false + default: true +extends_documentation_fragment: cloudstack +''' + +EXAMPLES = ''' +# Create volume within project, zone with specified storage options +- local_action: + module: cs_volume + name: web-vm-1-volume + project: Integration + zone: ch-zrh-ix-01 + disk_offering: PerfPlus Storage + size: 20 + +# Create/attach volume to instance +- local_action: + module: cs_volume + name: web-vm-1-volume + disk_offering: PerfPlus Storage + size: 20 + vm: web-vm-1 + state: attached + +# Detach volume +- local_action: + module: cs_volume + name: web-vm-1-volume + state: detached + +# Remove volume +- local_action: + module: cs_volume + name: web-vm-1-volume + state: absent +''' + +RETURN = ''' +id: + description: ID of the volume. + returned: success + type: string + sample: +name: + description: Name of the volume. + returned: success + type: string + sample: web-volume-01 +display_name: + description: Display name of the volume. + returned: success + type: string + sample: web-volume-01 +group: + description: Group the volume belongs to + returned: success + type: string + sample: web +domain: + description: Domain the volume belongs to + returned: success + type: string + sample: example domain +project: + description: Project the volume belongs to + returned: success + type: string + sample: Production +zone: + description: Name of zone the volume is in. + returned: success + type: string + sample: ch-gva-2 +created: + description: Date of the volume was created. + returned: success + type: string + sample: 2014-12-01T14:57:57+0100 +attached: + description: Date of the volume was attached. + returned: success + type: string + sample: 2014-12-01T14:57:57+0100 +type: + description: Disk volume type. + returned: success + type: string + sample: DATADISK +size: + description: Size of disk volume. + returned: success + type: string + sample: 20 +vm: + description: Name of the vm the volume is attached to (not returned when detached) + returned: success + type: string + sample: web-01 +state: + description: State of the volume + returned: success + type: string + sample: Attached +device_id: + description: Id of the device on user vm the volume is attached to (not returned when detached) + returned: success + type: string + sample: 1 +''' + +try: + from cs import CloudStack, CloudStackException, read_config + has_lib_cs = True +except ImportError: + has_lib_cs = False + +# import cloudstack common +from ansible.module_utils.cloudstack import * + + +class AnsibleCloudStackVolume(AnsibleCloudStack): + + def __init__(self, module): + super(AnsibleCloudStackVolume, self).__init__(module) + self.returns = { + 'group': 'group', + 'attached': 'attached', + 'vmname': 'vm', + 'deviceid': 'device_id', + 'type': 'type', + 'size': 'size', + } + self.volume = None + + #TODO implement in cloudstack utils + def get_disk_offering(self, key=None): + disk_offering = self.module.params.get('disk_offering') + if not disk_offering: + return None + + args = {} + args['domainid'] = self.get_domain(key='id') + + disk_offerings = self.cs.listDiskOfferings(**args) + if disk_offerings: + for d in disk_offerings['diskoffering']: + if disk_offering in [d['displaytext'], d['name'], d['id']]: + return self._get_by_key(key, d) + self.module.fail_json(msg="Disk offering '%s' not found" % disk_offering) + + + def get_volume(self): + if not self.volume: + args = {} + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') + args['projectid'] = self.get_project(key='id') + args['type'] = 'DATADISK' + + volumes = self.cs.listVolumes(**args) + if volumes: + volume_name = self.module.params.get('name') + for v in volumes['volume']: + if volume_name.lower() == v['name'].lower(): + self.volume = v + break + return self.volume + + + def get_snapshot(self, key=None): + snapshot = self.module.params.get('snapshot') + if not snapshot: + return None + + args = {} + args['name'] = snapshot + args['account'] = self.get_account('name') + args['domainid'] = self.get_domain('id') + args['projectid'] = self.get_project('id') + + snapshots = self.cs.listSnapshots(**args) + if snapshots: + return self._get_by_key(key, snapshots['snapshot'][0]) + self.module.fail_json(msg="Snapshot with name %s not found" % snapshot) + + + def present_volume(self): + volume = self.get_volume() + if not volume: + disk_offering_id = self.get_disk_offering(key='id') + snapshot_id = self.get_snapshot(key='id') + + if not disk_offering_id and not snapshot_id: + self.module.fail_json(msg="Required one of: disk_offering,snapshot") + + self.result['changed'] = True + + args = {} + args['name'] = self.module.params.get('name') + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') + args['diskofferingid'] = disk_offering_id + args['displayvolume'] = self.module.params.get('display_volume') + args['maxiops'] = self.module.params.get('max_iops') + args['miniops'] = self.module.params.get('min_iops') + args['projectid'] = self.get_project(key='id') + args['size'] = self.module.params.get('size') + args['snapshotid'] = snapshot_id + args['zoneid'] = self.get_zone(key='id') + + if not self.module.check_mode: + res = self.cs.createVolume(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + poll_async = self.module.params.get('poll_async') + if poll_async: + volume = self.poll_job(res, 'volume') + return volume + + + def attached_volume(self): + volume = self.present_volume() + + if volume.get('virtualmachineid') != self.get_vm(key='id'): + self.result['changed'] = True + + if not self.module.check_mode: + volume = self.detached_volume() + + if 'attached' not in volume: + self.result['changed'] = True + + args = {} + args['id'] = volume['id'] + args['virtualmachineid'] = self.get_vm(key='id') + args['deviceid'] = self.module.params.get('device_id') + + if not self.module.check_mode: + res = self.cs.attachVolume(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + poll_async = self.module.params.get('poll_async') + if poll_async: + volume = self.poll_job(res, 'volume') + return volume + + + def detached_volume(self): + volume = self.present_volume() + + if volume: + if 'attached' not in volume: + return volume + + self.result['changed'] = True + + if not self.module.check_mode: + res = self.cs.detachVolume(id=volume['id']) + if 'errortext' in volume: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + poll_async = self.module.params.get('poll_async') + if poll_async: + volume = self.poll_job(res, 'volume') + return volume + + + def absent_volume(self): + volume = self.get_volume() + + if volume: + if 'attached' in volume: + if self.module.param.get('force'): + self.detached_volume() + else: + self.module.fail_json(msg="Volume '%s' is attached, use force=true for detaching and removing the volume." % volume.get('name')) + + self.result['changed'] = True + if not self.module.check_mode: + volume = self.detached_volume() + + res = self.cs.deleteVolume(id=volume['id']) + if 'errortext' in volume: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + poll_async = self.module.params.get('poll_async') + if poll_async: + res = self.poll_job(res, 'volume') + + return volume + + +def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + name = dict(required=True), + disk_offering = dict(default=None), + display_volume = dict(choices=BOOLEANS, default=True), + max_iops = dict(type='int', default=None), + min_iops = dict(type='int', default=None), + size = dict(type='int', default=None), + snapshot = dict(default=None), + vm = dict(default=None), + device_id = dict(type='int', default=None), + custom_id = dict(default=None), + force = dict(choices=BOOLEANS, default=False), + state = dict(choices=['present', 'absent', 'attached', 'detached'], default='present'), + zone = dict(default=None), + domain = dict(default=None), + account = dict(default=None), + project = dict(default=None), + poll_async = dict(choices=BOOLEANS, default=True), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=cs_required_together(), + mutually_exclusive = ( + ['snapshot', 'disk_offering'], + ), + supports_check_mode=True + ) + + if not has_lib_cs: + module.fail_json(msg="python library cs required: pip install cs") + + try: + acs_vol = AnsibleCloudStackVolume(module) + + state = module.params.get('state') + + if state in ['absent']: + volume = acs_vol.absent_volume() + elif state in ['attached']: + volume = acs_vol.attached_volume() + elif state in ['detached']: + volume = acs_vol.detached_volume() + else: + volume = acs_vol.present_volume() + + result = acs_vol.get_result(volume) + + except CloudStackException, e: + module.fail_json(msg='CloudStackException: %s' % str(e)) + + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From 42efef5682143e654748b6d8bec0589ba8b9a3de Mon Sep 17 00:00:00 2001 From: Adam Keech Date: Wed, 25 Nov 2015 11:41:02 -0500 Subject: [PATCH 0984/2522] Appending "Registry::" is not needed and no longer works. --- windows/win_regedit.ps1 | 2 -- 1 file changed, 2 deletions(-) diff --git a/windows/win_regedit.ps1 b/windows/win_regedit.ps1 index ee92e781d0c..f9491e39c57 100644 --- a/windows/win_regedit.ps1 +++ b/windows/win_regedit.ps1 @@ -31,8 +31,6 @@ $state = Get-Attr -obj $params -name "state" -validateSet "present","absent" -de $registryData = Get-Attr -obj $params -name "data" -default $null $registryDataType = Get-Attr -obj $params -name "datatype" -validateSet "binary","dword","expandstring","multistring","string","qword" -default "string" -$registryKey = "Registry::" + $registryKey - If ($state -eq "present" -and $registryData -eq $null -and $registryValue -ne $null) { Fail-Json $result "missing required argument: data" From b7ca7d15ac36f43bf32715949b152ce7fffe8e76 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Wed, 25 Nov 2015 23:04:51 +0100 Subject: [PATCH 0985/2522] cloudstack: cs_facts: fix wrong description of returns cloudstack_public_ipv4, cloudstack_public_hostname Also see http://docs.cloudstack.apache.org/projects/cloudstack-administration/en/4.6/api.html#user-data-and-meta-data --- cloud/cloudstack/cs_facts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/cloudstack/cs_facts.py b/cloud/cloudstack/cs_facts.py index 11230b4c229..4a774479537 100644 --- a/cloud/cloudstack/cs_facts.py +++ b/cloud/cloudstack/cs_facts.py @@ -77,12 +77,12 @@ type: string sample: 185.19.28.35 cloudstack_public_hostname: - description: public hostname of the instance. + description: public IPv4 of the router. Same as C(cloudstack_public_ipv4). returned: success type: string sample: VM-ab4e80b0-3e7e-4936-bdc5-e334ba5b0139 cloudstack_public_ipv4: - description: public IPv4 of the instance. + description: public IPv4 of the router. returned: success type: string sample: 185.19.28.35 From c28a0031bbaf20b6fae0e2ccc9820698861ff045 Mon Sep 17 00:00:00 2001 From: GUILLAUME GROSSETIE Date: Thu, 26 Nov 2015 11:37:59 +0100 Subject: [PATCH 0986/2522] Resolves #1290, Adds limit_type choice "-" --- system/pam_limits.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/pam_limits.py b/system/pam_limits.py index eb04021c3e0..4003f76d3f8 100644 --- a/system/pam_limits.py +++ b/system/pam_limits.py @@ -40,7 +40,7 @@ description: - Limit type, see C(man limits) for an explanation required: true - choices: [ "hard", "soft" ] + choices: [ "hard", "soft", "-" ] limit_item: description: - The limit to be set From 68b7142d629d548d9be53857d7607aa6156a1a78 Mon Sep 17 00:00:00 2001 From: Charles Ferguson Date: Fri, 27 Nov 2015 00:24:08 +0000 Subject: [PATCH 0987/2522] Update 'patch' documentation for src, remote_src, backup and binary. The patch module has a few missing items, and inconsistencies, in its documentation. A few of which are addressed here. Within Ansible documentation, the choices for boolean values are commonly 'yes', and 'no'. We standardise the options on that. 'remote_src' documentation uses 'False' and 'True' for its documentation, so these have been updated in both the choices and default. 'src' documentation refers to 'remote_src', so is updated to use the 'no' choice. 'backup' did not describe its options and default at all, so we add them. 'binary' default used 'False', but specified the type as 'bool' which is implicitly documented as 'yes'/'no', so we make that 'no' as well. --- files/patch.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/files/patch.py b/files/patch.py index 576333c38f8..af3178ba3a3 100644 --- a/files/patch.py +++ b/files/patch.py @@ -46,17 +46,17 @@ src: description: - Path of the patch file as accepted by the GNU patch tool. If - C(remote_src) is False, the patch source file is looked up from the + C(remote_src) is 'no', the patch source file is looked up from the module's "files" directory. required: true aliases: [ "patchfile" ] remote_src: description: - - If False, it will search for src at originating/master machine, if True it will - go to the remote/target machine for the src. Default is False. - choices: [ "True", "False" ] + - If C(no), it will search for src at originating/master machine, if C(yes) it will + go to the remote/target machine for the src. Default is C(no). + choices: [ "yes", "no" ] required: false - default: "False" + default: "no" strip: description: - Number that indicates the smallest prefix containing leading slashes @@ -70,15 +70,17 @@ description: - passes --backup --version-control=numbered to patch, producing numbered backup copies + choices: [ 'yes', 'no' ] + default: 'no' binary: version_added: "2.0" description: - - Setting to true will disable patch's heuristic for transforming CRLF + - Setting to C(yes) will disable patch's heuristic for transforming CRLF line endings into LF. Line endings of src and dest must match. If set to - False, patch will replace CRLF in src files on POSIX. + C(no), patch will replace CRLF in src files on POSIX. required: false type: "bool" - default: "False" + default: "no" note: - This module requires GNU I(patch) utility to be installed on the remote host. ''' From 5948389d2f1a3b37cc6ed3ffba94f5d53ee24ac4 Mon Sep 17 00:00:00 2001 From: Guido Lorenz Date: Fri, 27 Nov 2015 15:49:57 +0100 Subject: [PATCH 0988/2522] win_chocolatey: Add install_args, params and ignoredependencies --- windows/win_chocolatey.ps1 | 58 +++++++++++++++++++++++++++++++++++--- windows/win_chocolatey.py | 18 ++++++++++++ 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/windows/win_chocolatey.ps1 b/windows/win_chocolatey.ps1 index ce006fff152..99ae8f84c0a 100644 --- a/windows/win_chocolatey.ps1 +++ b/windows/win_chocolatey.ps1 @@ -34,6 +34,11 @@ if ($source) {$source = $source.Tolower()} $showlog = Get-Attr -obj $params -name showlog -default "false" | ConvertTo-Bool $state = Get-Attr -obj $params -name state -default "present" + +$installargs = Get-Attr -obj $params -name install_args -default $null +$packageparams = Get-Attr -obj $params -name params -default $null +$ignoredependencies = Get-Attr -obj $params -name ignore_dependencies -default "false" | ConvertTo-Bool + if ("present","absent" -notcontains $state) { Fail-Json $result "state is $state; must be present or absent" @@ -106,7 +111,13 @@ Function Choco-Upgrade [Parameter(Mandatory=$false, Position=3)] [string]$source, [Parameter(Mandatory=$false, Position=4)] - [bool]$force + [bool]$force, + [Parameter(Mandatory=$false, Position=5)] + [string]$installargs, + [Parameter(Mandatory=$false, Position=6)] + [string]$packageparams, + [Parameter(Mandatory=$false, Position=7)] + [bool]$ignoredependencies ) if (-not (Choco-IsInstalled $package)) @@ -131,6 +142,21 @@ Function Choco-Upgrade $cmd += " -force" } + if ($installargs) + { + $cmd += " -installargs '$installargs'" + } + + if ($packageparams) + { + $cmd += " -params '$packageparams'" + } + + if ($ignoredependencies) + { + $cmd += " -ignoredependencies" + } + $results = invoke-expression $cmd if ($LastExitCode -ne 0) @@ -163,14 +189,22 @@ Function Choco-Install [Parameter(Mandatory=$false, Position=4)] [bool]$force, [Parameter(Mandatory=$false, Position=5)] - [bool]$upgrade + [bool]$upgrade, + [Parameter(Mandatory=$false, Position=6)] + [string]$installargs, + [Parameter(Mandatory=$false, Position=7)] + [string]$packageparams, + [Parameter(Mandatory=$false, Position=8)] + [bool]$ignoredependencies ) if (Choco-IsInstalled $package) { if ($upgrade) { - Choco-Upgrade -package $package -version $version -source $source -force $force + Choco-Upgrade -package $package -version $version -source $source -force $force ` + -installargs $installargs -packageparams $packageparams ` + -ignoredependencies $ignoredependencies } return @@ -193,6 +227,21 @@ Function Choco-Install $cmd += " -force" } + if ($installargs) + { + $cmd += " -installargs '$installargs'" + } + + if ($packageparams) + { + $cmd += " -params '$packageparams'" + } + + if ($ignoredependencies) + { + $cmd += " -ignoredependencies" + } + $results = invoke-expression $cmd if ($LastExitCode -ne 0) @@ -253,7 +302,8 @@ Try if ($state -eq "present") { Choco-Install -package $package -version $version -source $source ` - -force $force -upgrade $upgrade + -force $force -upgrade $upgrade -installargs $installargs ` + -packageparams $packageparams -ignoredependencies $ignoredependencies } else { diff --git a/windows/win_chocolatey.py b/windows/win_chocolatey.py index 7f399dbd22f..f729aee0d6d 100644 --- a/windows/win_chocolatey.py +++ b/windows/win_chocolatey.py @@ -75,6 +75,24 @@ require: false default: null aliases: [] + install_args: + description: + - Arguments to pass to the native installer + require: false + default: null + aliases: [] + params: + description: + - Parameters to pass to the package + require: false + default: null + aliases: [] + ignore_dependencies: + description: + - Ignore dependencies, only install/upgrade the package itself + require: false + default: false + aliases: [] author: "Trond Hindenes (@trondhindenes), Peter Mounce (@petemounce), Pepe Barbe (@elventear), Adam Keech (@smadam813)" ''' From d51950c37e76d9e98fa5d0fc4d53458d167ba40a Mon Sep 17 00:00:00 2001 From: Guido Lorenz Date: Fri, 27 Nov 2015 15:59:24 +0100 Subject: [PATCH 0989/2522] win_chocolatey: Clean up documentation --- windows/win_chocolatey.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/windows/win_chocolatey.py b/windows/win_chocolatey.py index f729aee0d6d..b9a54fddcbf 100644 --- a/windows/win_chocolatey.py +++ b/windows/win_chocolatey.py @@ -33,8 +33,6 @@ description: - Name of the package to be installed required: true - default: null - aliases: [] state: description: - State of the package on the system @@ -43,7 +41,6 @@ - present - absent default: present - aliases: [] force: description: - Forces install of the package (even if it already exists). Using Force will cause ansible to always report that a change was made @@ -52,7 +49,6 @@ - yes - no default: no - aliases: [] upgrade: description: - If package is already installed it, try to upgrade to the latest version or to the specified version @@ -61,38 +57,32 @@ - yes - no default: no - aliases: [] version: description: - Specific version of the package to be installed - Ignored when state == 'absent' required: false default: null - aliases: [] source: description: - Specify source rather than using default chocolatey repository require: false default: null - aliases: [] install_args: description: - Arguments to pass to the native installer require: false default: null - aliases: [] params: description: - Parameters to pass to the package require: false default: null - aliases: [] ignore_dependencies: description: - Ignore dependencies, only install/upgrade the package itself require: false default: false - aliases: [] author: "Trond Hindenes (@trondhindenes), Peter Mounce (@petemounce), Pepe Barbe (@elventear), Adam Keech (@smadam813)" ''' From f8fe5a2fcd1f9d5e01dea75f04598ceb1283ee5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Andersson=20+=20SU=20Sheng=20Loong?= Date: Wed, 3 Jun 2015 02:53:16 +0800 Subject: [PATCH 0990/2522] monit: Add retry for pending/initializing services If there are already ongoing actions for a process managed by monit, the module would exit unsuccessfully. It could also give off false positives because it did not determine whether the service was started/stopped when it was in a pending state. Which might be turning the service off, but the action was to start it. For example "Running - pending stop" would be regarded as the service running and "state=enabled" would do nothing. This will make Ansible wait for the state to finalize, or a timeout decided by the new `max_retries` option, before it decides what to do. This fixes issue #244. --- monitoring/monit.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/monitoring/monit.py b/monitoring/monit.py index 3d3c7c8c3ca..84a897e458d 100644 --- a/monitoring/monit.py +++ b/monitoring/monit.py @@ -18,6 +18,7 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # +from time import sleep DOCUMENTATION = ''' --- @@ -38,6 +39,12 @@ required: true default: null choices: [ "present", "started", "stopped", "restarted", "monitored", "unmonitored", "reloaded" ] + max_retries: + description: + - If there are pending actions for the service monitoried by monit Ansible will retry this + many times to perform the requested action. Between each retry Ansible will sleep for 1 second. + required: false + default: 10 requirements: [ ] author: "Darryl Stoflet (@dstoflet)" ''' @@ -50,6 +57,7 @@ def main(): arg_spec = dict( name=dict(required=True), + max_retries=dict(default=10), state=dict(required=True, choices=['present', 'started', 'restarted', 'stopped', 'monitored', 'unmonitored', 'reloaded']) ) @@ -57,6 +65,7 @@ def main(): name = module.params['name'] state = module.params['state'] + max_retries = module.params['max_retries'] MONIT = module.get_bin_path('monit', True) @@ -103,7 +112,21 @@ def run_command(command): module.exit_json(changed=True, name=name, state=state) module.exit_json(changed=False, name=name, state=state) - running = 'running' in process_status + running_status = status() + retries = 0 + while 'pending' in running_status or 'initializing' in running_status: + if retries >= max_retries: + module.fail_json( + msg='too many retries waiting for "pending" or "initiating" to go away (%s)' % running_status, + retries=retries, + state=state + ) + + sleep(1) + retries += 1 + running_status = status() + + running = 'running' in status() if running and state in ['started', 'monitored']: module.exit_json(changed=False, name=name, state=state) From 5835d06a4eca4438ff6f4e344cd67eb7f3d9ed88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Andersson?= Date: Fri, 5 Jun 2015 05:39:53 +0800 Subject: [PATCH 0991/2522] monit: Wait for pending state changes for reloads @mpeters reported that we're not checking that the named service is actually there after a reload. And that sometimes monit doesn't actually return anything at all after a reload. --- monitoring/monit.py | 54 +++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/monitoring/monit.py b/monitoring/monit.py index 84a897e458d..32fa526e0bb 100644 --- a/monitoring/monit.py +++ b/monitoring/monit.py @@ -69,14 +69,6 @@ def main(): MONIT = module.get_bin_path('monit', True) - if state == 'reloaded': - if module.check_mode: - module.exit_json(changed=True) - rc, out, err = module.run_command('%s reload' % MONIT) - if rc != 0: - module.fail_json(msg='monit reload failed', stdout=out, stderr=err) - module.exit_json(changed=True, name=name, state=state) - def status(): """Return the status of the process in monit, or the empty string if not present.""" rc, out, err = module.run_command('%s summary' % MONIT, check_rc=True) @@ -95,8 +87,35 @@ def run_command(command): module.run_command('%s %s %s' % (MONIT, command, name), check_rc=True) return status() - process_status = status() - present = process_status != '' + def wait_for_monit_to_stop_pending(sleep_time=1): + """Fails this run if there is no status or it's pending/initalizing for max_retries""" + running_status = status() + retries = 0 + + while running_status == '' or 'pending' in running_status or 'initializing' in running_status: + if retries >= max_retries: + module.fail_json( + msg='too many retries waiting for empty, "pending", or "initiating" status to go away ({0})'.format( + running_status + ), + retries=retries, + state=state + ) + + sleep(sleep_time) + retries += 1 + running_status = status() + + if state == 'reloaded': + if module.check_mode: + module.exit_json(changed=True) + rc, out, err = module.run_command('%s reload' % MONIT) + if rc != 0: + module.fail_json(msg='monit reload failed', stdout=out, stderr=err) + wait_for_monit_to_stop_pending() + module.exit_json(changed=True, name=name, state=state) + + present = status() != '' if not present and not state == 'present': module.fail_json(msg='%s process not presently configured with monit' % name, name=name, state=state) @@ -112,20 +131,7 @@ def run_command(command): module.exit_json(changed=True, name=name, state=state) module.exit_json(changed=False, name=name, state=state) - running_status = status() - retries = 0 - while 'pending' in running_status or 'initializing' in running_status: - if retries >= max_retries: - module.fail_json( - msg='too many retries waiting for "pending" or "initiating" to go away (%s)' % running_status, - retries=retries, - state=state - ) - - sleep(1) - retries += 1 - running_status = status() - + wait_for_monit_to_stop_pending() running = 'running' in status() if running and state in ['started', 'monitored']: From 262f2e9048cac9a3867a1f8722a0ebdf2d1fb974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Andersson?= Date: Tue, 4 Aug 2015 16:59:42 +0800 Subject: [PATCH 0992/2522] monit: Add version_added and type for new argument --- monitoring/monit.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monitoring/monit.py b/monitoring/monit.py index 32fa526e0bb..f8151a71f2a 100644 --- a/monitoring/monit.py +++ b/monitoring/monit.py @@ -45,6 +45,7 @@ many times to perform the requested action. Between each retry Ansible will sleep for 1 second. required: false default: 10 + version_added: "2.0" requirements: [ ] author: "Darryl Stoflet (@dstoflet)" ''' @@ -57,7 +58,7 @@ def main(): arg_spec = dict( name=dict(required=True), - max_retries=dict(default=10), + max_retries=dict(default=10, type='int'), state=dict(required=True, choices=['present', 'started', 'restarted', 'stopped', 'monitored', 'unmonitored', 'reloaded']) ) From 72155d40a33fee454ad2a4088d98842d1ab68e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Andersson?= Date: Sun, 29 Nov 2015 22:45:39 +0800 Subject: [PATCH 0993/2522] monit: Set a high timeout waiting for status changes Instead of waiting for up to a certain number of retries we set a high timeout and only re-check every five seconds. Certain services can take a minute or more to start and we want to avoid waisting resources by polling too often. --- monitoring/monit.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/monitoring/monit.py b/monitoring/monit.py index f8151a71f2a..35a386b6c6e 100644 --- a/monitoring/monit.py +++ b/monitoring/monit.py @@ -18,7 +18,7 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # -from time import sleep +import time DOCUMENTATION = ''' --- @@ -39,12 +39,13 @@ required: true default: null choices: [ "present", "started", "stopped", "restarted", "monitored", "unmonitored", "reloaded" ] - max_retries: + timeout: description: - - If there are pending actions for the service monitoried by monit Ansible will retry this - many times to perform the requested action. Between each retry Ansible will sleep for 1 second. + - If there are pending actions for the service monitored by monit, then Ansible will check + for up to this many seconds to verify the the requested action has been performed. + Ansible will sleep for five seconds between each check. required: false - default: 10 + default: 300 version_added: "2.0" requirements: [ ] author: "Darryl Stoflet (@dstoflet)" @@ -58,7 +59,7 @@ def main(): arg_spec = dict( name=dict(required=True), - max_retries=dict(default=10, type='int'), + timeout=dict(default=300, type='int'), state=dict(required=True, choices=['present', 'started', 'restarted', 'stopped', 'monitored', 'unmonitored', 'reloaded']) ) @@ -66,7 +67,7 @@ def main(): name = module.params['name'] state = module.params['state'] - max_retries = module.params['max_retries'] + timeout = module.params['timeout'] MONIT = module.get_bin_path('monit', True) @@ -88,23 +89,22 @@ def run_command(command): module.run_command('%s %s %s' % (MONIT, command, name), check_rc=True) return status() - def wait_for_monit_to_stop_pending(sleep_time=1): - """Fails this run if there is no status or it's pending/initalizing for max_retries""" - running_status = status() - retries = 0 + def wait_for_monit_to_stop_pending(): + """Fails this run if there is no status or it's pending/initalizing for timeout""" + timeout_time = time.time() + timeout + sleep_time = 5 + running_status = status() while running_status == '' or 'pending' in running_status or 'initializing' in running_status: - if retries >= max_retries: + if time.time() >= timeout_time: module.fail_json( - msg='too many retries waiting for empty, "pending", or "initiating" status to go away ({0})'.format( + msg='waited too long for "pending", or "initiating" status to go away ({0})'.format( running_status ), - retries=retries, state=state ) - sleep(sleep_time) - retries += 1 + time.sleep(sleep_time) running_status = status() if state == 'reloaded': From f2eb00cc71661f994c187547aa1048e40d97973d Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Mon, 30 Nov 2015 17:08:07 +0100 Subject: [PATCH 0994/2522] cloudstack: cs_instance: implement state=restored --- cloud/cloudstack/cs_instance.py | 42 +++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 8cf0672e85a..7126f7ff8dd 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -23,7 +23,7 @@ module: cs_instance short_description: Manages instances and virtual machines on Apache CloudStack based clouds. description: - - Deploy, start, update, scale, restart, stop and destroy instances. + - Deploy, start, update, scale, restart, restore, stop and destroy instances. version_added: '2.0' author: "René Moser (@resmo)" options: @@ -44,9 +44,10 @@ state: description: - State of the instance. + - C(restored) added in version 2.1. required: false default: 'present' - choices: [ 'deployed', 'started', 'stopped', 'restarted', 'destroyed', 'expunged', 'present', 'absent' ] + choices: [ 'deployed', 'started', 'stopped', 'restarted', 'restored', 'destroyed', 'expunged', 'present', 'absent' ] service_offering: description: - Name or id of the service offering of the new instance. @@ -427,7 +428,7 @@ def get_template_or_iso(self, key=None): iso = self.module.params.get('iso') if not template and not iso: - self.module.fail_json(msg="Template or ISO is required.") + return None args = {} args['account'] = self.get_account(key='name') @@ -494,6 +495,7 @@ def get_instance(self): break return self.instance + def get_iptonetwork_mappings(self): network_mappings = self.module.params.get('ip_to_networks') if network_mappings is None: @@ -509,6 +511,7 @@ def get_iptonetwork_mappings(self): res.append({'networkid': ids[i], 'ip': data['ip']}) return res + def get_network_ids(self, network_names=None): if network_names is None: network_names = self.module.params.get('networks') @@ -561,6 +564,7 @@ def get_user_data(self): user_data = base64.b64encode(user_data) return user_data + def get_details(self): res = None cpu = self.module.params.get('cpu') @@ -574,6 +578,7 @@ def get_details(self): }] return res + def deploy_instance(self, start_vm=True): self.result['changed'] = True networkids = self.get_network_ids() @@ -582,6 +587,9 @@ def deploy_instance(self, start_vm=True): args = {} args['templateid'] = self.get_template_or_iso(key='id') + if not args['templateid']: + self.module.fail_json(msg="Template or ISO is required.") + args['zoneid'] = self.get_zone(key='id') args['serviceofferingid'] = self.get_service_offering_id() args['account'] = self.get_account(key='name') @@ -798,6 +806,28 @@ def restart_instance(self): return instance + def restore_instance(self): + instance = self.get_instance() + + if not instance: + instance = self.deploy_instance() + return instance + + self.result['changed'] = True + + args = {} + args['templateid'] = self.get_template_or_iso(key='id') + args['virtualmachineid'] = instance['id'] + res = self.cs.restoreVirtualMachine(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + + poll_async = self.module.params.get('poll_async') + if poll_async: + instance = self._poll_job(res, 'virtualmachine') + return instance + + def get_result(self, instance): super(AnsibleCloudStackInstance, self).get_result(instance) if instance: @@ -817,13 +847,14 @@ def get_result(self, instance): self.result['default_ip'] = nic['ipaddress'] return self.result + def main(): argument_spec = cs_argument_spec() argument_spec.update(dict( name = dict(required=True), display_name = dict(default=None), group = dict(default=None), - state = dict(choices=['present', 'deployed', 'started', 'stopped', 'restarted', 'absent', 'destroyed', 'expunged'], default='present'), + state = dict(choices=['present', 'deployed', 'started', 'stopped', 'restarted', 'restored', 'absent', 'destroyed', 'expunged'], default='present'), service_offering = dict(default=None), cpu = dict(default=None, type='int'), cpu_speed = dict(default=None, type='int'), @@ -880,6 +911,9 @@ def main(): elif state in ['expunged']: instance = acs_instance.expunge_instance() + elif state in ['restored']: + instance = acs_instance.restore_instance() + elif state in ['present', 'deployed']: instance = acs_instance.present_instance() From 3c4f954f0fece5dcb3241d6d5391273334206241 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 30 Nov 2015 19:01:57 -0800 Subject: [PATCH 0995/2522] Don't raise or catch StandardError in amazon modules --- cloud/amazon/dynamodb_table.py | 3 +- cloud/amazon/ec2_elb_facts.py | 5 +- cloud/amazon/ec2_eni.py | 99 +++++++++++------------ cloud/amazon/ec2_eni_facts.py | 2 +- cloud/amazon/ec2_remote_facts.py | 33 ++++---- cloud/amazon/ec2_vpc_igw.py | 2 +- cloud/amazon/ec2_vpc_route_table.py | 31 ++++--- cloud/amazon/ec2_vpc_route_table_facts.py | 2 +- cloud/amazon/ec2_vpc_subnet.py | 7 +- cloud/amazon/ec2_vpc_subnet_facts.py | 2 +- cloud/amazon/ecs_cluster.py | 13 ++- cloud/amazon/s3_lifecycle.py | 37 +++++---- cloud/amazon/s3_logging.py | 37 +++++---- cloud/amazon/sqs_queue.py | 7 +- cloud/amazon/sts_assume_role.py | 46 +++++------ 15 files changed, 157 insertions(+), 169 deletions(-) diff --git a/cloud/amazon/dynamodb_table.py b/cloud/amazon/dynamodb_table.py index 1daf55e9d18..a39ecdd3f48 100644 --- a/cloud/amazon/dynamodb_table.py +++ b/cloud/amazon/dynamodb_table.py @@ -268,8 +268,7 @@ def main(): try: connection = connect_to_aws(boto.dynamodb2, region, **aws_connect_params) - - except (NoAuthHandlerFound, StandardError), e: + except (NoAuthHandlerFound, AnsibleAWSError), e: module.fail_json(msg=str(e)) state = module.params.get('state') diff --git a/cloud/amazon/ec2_elb_facts.py b/cloud/amazon/ec2_elb_facts.py index aaf3049bfd2..4289ef7a232 100644 --- a/cloud/amazon/ec2_elb_facts.py +++ b/cloud/amazon/ec2_elb_facts.py @@ -184,7 +184,7 @@ def main(): if region: try: connection = connect_to_aws(boto.ec2.elb, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, StandardError), e: + except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: module.fail_json(msg=str(e)) else: module.fail_json(msg="region must be specified") @@ -194,4 +194,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/amazon/ec2_eni.py b/cloud/amazon/ec2_eni.py index 72e5483e36b..5a6bd1f1b4d 100644 --- a/cloud/amazon/ec2_eni.py +++ b/cloud/amazon/ec2_eni.py @@ -96,25 +96,25 @@ private_ip_address: 172.31.0.20 subnet_id: subnet-xxxxxxxx state: present - + # Destroy an ENI, detaching it from any instance if necessary - ec2_eni: eni_id: eni-xxxxxxx force_detach: yes state: absent - + # Update an ENI - ec2_eni: eni_id: eni-xxxxxxx description: "My new description" state: present - + # Detach an ENI from an instance - ec2_eni: eni_id: eni-xxxxxxx instance_id: None state: present - + ### Delete an interface on termination # First create the interface - ec2_eni: @@ -124,7 +124,7 @@ subnet_id: subnet-xxxxxxxx state: present register: eni - + # Modify the interface to enable the delete_on_terminaton flag - ec2_eni: eni_id: {{ "eni.interface.id" }} @@ -145,14 +145,14 @@ def get_error_message(xml_string): - + root = ET.fromstring(xml_string) - for message in root.findall('.//Message'): + for message in root.findall('.//Message'): return message.text - - + + def get_eni_info(interface): - + interface_info = {'id': interface.id, 'subnet_id': interface.subnet_id, 'vpc_id': interface.vpc_id, @@ -164,7 +164,7 @@ def get_eni_info(interface): 'source_dest_check': interface.source_dest_check, 'groups': dict((group.id, group.name) for group in interface.groups), } - + if interface.attachment is not None: interface_info['attachment'] = {'attachment_id': interface.attachment.id, 'instance_id': interface.attachment.instance_id, @@ -173,11 +173,11 @@ def get_eni_info(interface): 'attach_time': interface.attachment.attach_time, 'delete_on_termination': interface.attachment.delete_on_termination, } - + return interface_info - + def wait_for_eni(eni, status): - + while True: time.sleep(3) eni.update() @@ -188,23 +188,20 @@ def wait_for_eni(eni, status): else: if status == "attached" and eni.attachment.status == "attached": break - - + + def create_eni(connection, module): - + instance_id = module.params.get("instance_id") if instance_id == 'None': instance_id = None - do_detach = True - else: - do_detach = False device_index = module.params.get("device_index") subnet_id = module.params.get('subnet_id') private_ip_address = module.params.get('private_ip_address') description = module.params.get('description') security_groups = module.params.get('security_groups') changed = False - + try: eni = compare_eni(connection, module) if eni is None: @@ -212,22 +209,22 @@ def create_eni(connection, module): if instance_id is not None: try: eni.attach(instance_id, device_index) - except BotoServerError as ex: + except BotoServerError: eni.delete() raise # Wait to allow creation / attachment to finish wait_for_eni(eni, "attached") eni.update() changed = True - + except BotoServerError as e: module.fail_json(msg=get_error_message(e.args[2])) - + module.exit_json(changed=changed, interface=get_eni_info(eni)) - + def modify_eni(connection, module): - + eni_id = module.params.get("eni_id") instance_id = module.params.get("instance_id") if instance_id == 'None': @@ -236,8 +233,6 @@ def modify_eni(connection, module): else: do_detach = False device_index = module.params.get("device_index") - subnet_id = module.params.get('subnet_id') - private_ip_address = module.params.get('private_ip_address') description = module.params.get('description') security_groups = module.params.get('security_groups') force_detach = module.params.get("force_detach") @@ -245,7 +240,6 @@ def modify_eni(connection, module): delete_on_termination = module.params.get("delete_on_termination") changed = False - try: # Get the eni with the eni_id specified eni_result_set = connection.get_all_network_interfaces(eni_id) @@ -282,20 +276,20 @@ def modify_eni(connection, module): except BotoServerError as e: print e module.fail_json(msg=get_error_message(e.args[2])) - + eni.update() module.exit_json(changed=changed, interface=get_eni_info(eni)) - - + + def delete_eni(connection, module): - + eni_id = module.params.get("eni_id") force_detach = module.params.get("force_detach") - + try: eni_result_set = connection.get_all_network_interfaces(eni_id) eni = eni_result_set[0] - + if force_detach is True: if eni.attachment is not None: eni.detach(force_detach) @@ -307,7 +301,7 @@ def delete_eni(connection, module): else: eni.delete() changed = True - + module.exit_json(changed=changed) except BotoServerError as e: msg = get_error_message(e.args[2]) @@ -316,35 +310,35 @@ def delete_eni(connection, module): module.exit_json(changed=False) else: module.fail_json(msg=get_error_message(e.args[2])) - + def compare_eni(connection, module): - + eni_id = module.params.get("eni_id") subnet_id = module.params.get('subnet_id') private_ip_address = module.params.get('private_ip_address') description = module.params.get('description') security_groups = module.params.get('security_groups') - + try: all_eni = connection.get_all_network_interfaces(eni_id) for eni in all_eni: remote_security_groups = get_sec_group_list(eni.groups) - if (eni.subnet_id == subnet_id) and (eni.private_ip_address == private_ip_address) and (eni.description == description) and (remote_security_groups == security_groups): + if (eni.subnet_id == subnet_id) and (eni.private_ip_address == private_ip_address) and (eni.description == description) and (remote_security_groups == security_groups): return eni - + except BotoServerError as e: module.fail_json(msg=get_error_message(e.args[2])) - + return None def get_sec_group_list(groups): - + # Build list of remote security groups remote_security_groups = [] for group in groups: remote_security_groups.append(group.id.encode()) - + return remote_security_groups @@ -357,7 +351,7 @@ def main(): private_ip_address = dict(), subnet_id = dict(), description = dict(), - security_groups = dict(type='list'), + security_groups = dict(type='list'), device_index = dict(default=0, type='int'), state = dict(default='present', choices=['present', 'absent']), force_detach = dict(default='no', type='bool'), @@ -365,18 +359,18 @@ def main(): delete_on_termination = dict(default=None, type='bool') ) ) - + module = AnsibleModule(argument_spec=argument_spec) if not HAS_BOTO: module.fail_json(msg='boto required for this module') - + region, ec2_url, aws_connect_params = get_aws_connection_info(module) - + if region: try: connection = connect_to_aws(boto.ec2, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, StandardError), e: + except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: module.fail_json(msg=str(e)) else: module.fail_json(msg="region must be specified") @@ -395,12 +389,13 @@ def main(): if eni_id is None: module.fail_json(msg="eni_id must be specified") else: - delete_eni(connection, module) - + delete_eni(connection, module) + from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * # this is magic, see lib/ansible/module_common.py #<> -main() +if __name__ == '__main__': + main() diff --git a/cloud/amazon/ec2_eni_facts.py b/cloud/amazon/ec2_eni_facts.py index c25535f51eb..e95a6ea1029 100644 --- a/cloud/amazon/ec2_eni_facts.py +++ b/cloud/amazon/ec2_eni_facts.py @@ -113,7 +113,7 @@ def main(): if region: try: connection = connect_to_aws(boto.ec2, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, StandardError), e: + except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: module.fail_json(msg=str(e)) else: module.fail_json(msg="region must be specified") diff --git a/cloud/amazon/ec2_remote_facts.py b/cloud/amazon/ec2_remote_facts.py index cf54fa0274d..28fc2c97d63 100644 --- a/cloud/amazon/ec2_remote_facts.py +++ b/cloud/amazon/ec2_remote_facts.py @@ -44,12 +44,12 @@ filters: instance-state-name: running "tag:Name": Example - + # Gather facts about instance i-123456 - ec2_remote_facts: filters: instance-id: i-123456 - + # Gather facts about all instances in vpc-123456 that are t2.small type - ec2_remote_facts: filters: @@ -66,23 +66,23 @@ HAS_BOTO = False def get_instance_info(instance): - + # Get groups groups = [] for group in instance.groups: - groups.append({ 'id': group.id, 'name': group.name }.copy()) + groups.append({ 'id': group.id, 'name': group.name }.copy()) # Get interfaces interfaces = [] for interface in instance.interfaces: - interfaces.append({ 'id': interface.id, 'mac_address': interface.mac_address }.copy()) + interfaces.append({ 'id': interface.id, 'mac_address': interface.mac_address }.copy()) # If an instance is terminated, sourceDestCheck is no longer returned try: - source_dest_check = instance.sourceDestCheck + source_dest_check = instance.sourceDestCheck except AttributeError: - source_dest_check = None - + source_dest_check = None + instance_info = { 'id': instance.id, 'kernel': instance.kernel, 'instance_profile': instance.instance_profile, @@ -118,23 +118,23 @@ def get_instance_info(instance): } return instance_info - + def list_ec2_instances(connection, module): - + filters = module.params.get("filters") instance_dict_array = [] - + try: all_instances = connection.get_only_instances(filters=filters) except BotoServerError as e: module.fail_json(msg=e.message) - + for instance in all_instances: instance_dict_array.append(get_instance_info(instance)) - + module.exit_json(instances=instance_dict_array) - + def main(): argument_spec = ec2_argument_spec() @@ -154,11 +154,11 @@ def main(): if region: try: connection = connect_to_aws(boto.ec2, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, StandardError), e: + except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: module.fail_json(msg=str(e)) else: module.fail_json(msg="region must be specified") - + list_ec2_instances(connection, module) # import module snippets @@ -167,4 +167,3 @@ def main(): if __name__ == '__main__': main() - diff --git a/cloud/amazon/ec2_vpc_igw.py b/cloud/amazon/ec2_vpc_igw.py index 16437abf073..a4e58faac8b 100644 --- a/cloud/amazon/ec2_vpc_igw.py +++ b/cloud/amazon/ec2_vpc_igw.py @@ -134,7 +134,7 @@ def main(): if region: try: connection = connect_to_aws(boto.vpc, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, StandardError), e: + except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: module.fail_json(msg=str(e)) else: module.fail_json(msg="region must be specified") diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index 829dda62d3e..eef58c23ced 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -99,7 +99,7 @@ - dest: 0.0.0.0/0 instance_id: "{{ nat.instance_id }}" register: nat_route_table - + ''' @@ -253,23 +253,23 @@ def get_route_table_by_id(vpc_conn, vpc_id, route_table_id): route_tables = vpc_conn.get_all_route_tables(route_table_ids=[route_table_id], filters={'vpc_id': vpc_id}) if route_tables: route_table = route_tables[0] - + return route_table - + def get_route_table_by_tags(vpc_conn, vpc_id, tags): - + count = 0 - route_table = None + route_table = None route_tables = vpc_conn.get_all_route_tables(filters={'vpc_id': vpc_id}) for table in route_tables: this_tags = get_resource_tags(vpc_conn, table.id) if tags_match(tags, this_tags): route_table = table count +=1 - + if count > 1: raise RuntimeError("Tags provided do not identify a unique route table") - else: + else: return route_table @@ -463,7 +463,7 @@ def create_route_spec(connection, routes, vpc_id): return routes def ensure_route_table_present(connection, module): - + lookup = module.params.get('lookup') propagating_vgw_ids = module.params.get('propagating_vgw_ids', []) route_table_id = module.params.get('route_table_id') @@ -475,7 +475,7 @@ def ensure_route_table_present(connection, module): routes = create_route_spec(connection, module.params.get('routes'), vpc_id) except AnsibleIgwSearchException as e: module.fail_json(msg=e[0]) - + changed = False tags_valid = False @@ -494,7 +494,7 @@ def ensure_route_table_present(connection, module): route_table = get_route_table_by_id(connection, vpc_id, route_table_id) except EC2ResponseError as e: module.fail_json(msg=e.message) - + # If no route table returned then create new route table if route_table is None: try: @@ -505,7 +505,7 @@ def ensure_route_table_present(connection, module): module.exit_json(changed=True) module.fail_json(msg=e.message) - + if routes is not None: try: result = ensure_routes(connection, route_table, routes, propagating_vgw_ids, check_mode) @@ -560,18 +560,18 @@ def main(): vpc_id = dict(default=None, required=True) ) ) - + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - + if not HAS_BOTO: module.fail_json(msg='boto is required for this module') region, ec2_url, aws_connect_params = get_aws_connection_info(module) - + if region: try: connection = connect_to_aws(boto.vpc, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, StandardError), e: + except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: module.fail_json(msg=str(e)) else: module.fail_json(msg="region must be specified") @@ -598,4 +598,3 @@ def main(): if __name__ == '__main__': main() - diff --git a/cloud/amazon/ec2_vpc_route_table_facts.py b/cloud/amazon/ec2_vpc_route_table_facts.py index f93ab060fd6..8b5e60ab2c9 100644 --- a/cloud/amazon/ec2_vpc_route_table_facts.py +++ b/cloud/amazon/ec2_vpc_route_table_facts.py @@ -111,7 +111,7 @@ def main(): if region: try: connection = connect_to_aws(boto.vpc, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, StandardError), e: + except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: module.fail_json(msg=str(e)) else: module.fail_json(msg="region must be specified") diff --git a/cloud/amazon/ec2_vpc_subnet.py b/cloud/amazon/ec2_vpc_subnet.py index 46f78362a28..d0cc68e07fa 100644 --- a/cloud/amazon/ec2_vpc_subnet.py +++ b/cloud/amazon/ec2_vpc_subnet.py @@ -71,7 +71,7 @@ state: absent vpc_id: vpc-123456 cidr: 10.0.1.16/28 - + ''' import sys # noqa @@ -143,7 +143,7 @@ def create_subnet(vpc_conn, vpc_id, cidr, az, check_mode): if e.error_code == "DryRunOperation": subnet = None else: - raise AnsibleVPCSubnetCreationException( + raise AnsibleVPCSubnetCreationException( 'Unable to create subnet {0}, error: {1}'.format(cidr, e)) return subnet @@ -242,7 +242,7 @@ def main(): if region: try: connection = connect_to_aws(boto.vpc, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, StandardError), e: + except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: module.fail_json(msg=str(e)) else: module.fail_json(msg="region must be specified") @@ -270,4 +270,3 @@ def main(): if __name__ == '__main__': main() - diff --git a/cloud/amazon/ec2_vpc_subnet_facts.py b/cloud/amazon/ec2_vpc_subnet_facts.py index 48f514ba49f..bfad2fb72a7 100644 --- a/cloud/amazon/ec2_vpc_subnet_facts.py +++ b/cloud/amazon/ec2_vpc_subnet_facts.py @@ -111,7 +111,7 @@ def main(): if region: try: connection = connect_to_aws(boto.vpc, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, StandardError), e: + except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: module.fail_json(msg=str(e)) else: module.fail_json(msg="region must be specified") diff --git a/cloud/amazon/ecs_cluster.py b/cloud/amazon/ecs_cluster.py index 6b37762521f..3a0d7c10636 100644 --- a/cloud/amazon/ecs_cluster.py +++ b/cloud/amazon/ecs_cluster.py @@ -25,7 +25,7 @@ - Creates or terminates ecs clusters. version_added: "2.0" author: Mark Chance(@Java1Guy) -requirements: [ json, time, boto, boto3 ] +requirements: [ boto, boto3 ] options: state: description: @@ -100,8 +100,9 @@ returned: ACTIVE type: string ''' +import time + try: - import json, time import boto HAS_BOTO = True except ImportError: @@ -147,7 +148,7 @@ def describe_cluster(self, cluster_name): c = self.find_in_array(response['clusters'], cluster_name) if c: return c - raise StandardError("Unknown problem describing cluster %s." % cluster_name) + raise Exception("Unknown problem describing cluster %s." % cluster_name) def create_cluster(self, clusterName = 'default'): response = self.ecs.create_cluster(clusterName=clusterName) @@ -170,12 +171,10 @@ def main(): module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True, required_together=required_together) if not HAS_BOTO: - module.fail_json(msg='boto is required.') + module.fail_json(msg='boto is required.') if not HAS_BOTO3: - module.fail_json(msg='boto3 is required.') - - cluster_name = module.params['name'] + module.fail_json(msg='boto3 is required.') cluster_mgr = EcsClusterManager(module) try: diff --git a/cloud/amazon/s3_lifecycle.py b/cloud/amazon/s3_lifecycle.py index da8e8a8402f..891beac01f1 100644 --- a/cloud/amazon/s3_lifecycle.py +++ b/cloud/amazon/s3_lifecycle.py @@ -94,7 +94,7 @@ prefix: /logs/ status: enabled state: present - + # Configure a lifecycle rule to transition all items with a prefix of /logs/ to glacier after 7 days and then delete after 90 days - s3_lifecycle: name: mybucket @@ -103,7 +103,7 @@ prefix: /logs/ status: enabled state: present - + # Configure a lifecycle rule to transition all items with a prefix of /logs/ to glacier on 31 Dec 2020 and then delete on 31 Dec 2030. Note that midnight GMT must be specified. # Be sure to quote your date strings - s3_lifecycle: @@ -113,20 +113,20 @@ prefix: /logs/ status: enabled state: present - + # Disable the rule created above - s3_lifecycle: name: mybucket prefix: /logs/ status: disabled state: present - + # Delete the lifecycle rule created above - s3_lifecycle: name: mybucket prefix: /logs/ state: absent - + ''' import xml.etree.ElementTree as ET @@ -182,7 +182,7 @@ def create_lifecycle_rule(connection, module): expiration_obj = Expiration(date=expiration_date) else: expiration_obj = None - + # Create transition if transition_days is not None: transition_obj = Transition(days=transition_days, storage_class=storage_class.upper()) @@ -232,7 +232,7 @@ def create_lifecycle_rule(connection, module): bucket.configure_lifecycle(lifecycle_obj) except S3ResponseError, e: module.fail_json(msg=e.message) - + module.exit_json(changed=changed) def compare_rule(rule_a, rule_b): @@ -306,7 +306,7 @@ def destroy_lifecycle_rule(connection, module): # Create lifecycle lifecycle_obj = Lifecycle() - + # Check if rule exists # If an ID exists, use that otherwise compare based on prefix if rule_id is not None: @@ -323,8 +323,7 @@ def destroy_lifecycle_rule(connection, module): changed = True else: lifecycle_obj.append(existing_rule) - - + # Write lifecycle to bucket or, if there no rules left, delete lifecycle configuration try: if lifecycle_obj: @@ -333,9 +332,9 @@ def destroy_lifecycle_rule(connection, module): bucket.delete_lifecycle_configuration() except BotoServerError, e: module.fail_json(msg=e.message) - + module.exit_json(changed=changed) - + def main(): @@ -361,18 +360,18 @@ def main(): [ 'expiration_days', 'expiration_date' ], [ 'expiration_days', 'transition_date' ], [ 'transition_days', 'transition_date' ], - [ 'transition_days', 'expiration_date' ] + [ 'transition_days', 'expiration_date' ] ] ) if not HAS_BOTO: module.fail_json(msg='boto required for this module') - + if not HAS_DATEUTIL: - module.fail_json(msg='dateutil required for this module') + module.fail_json(msg='dateutil required for this module') region, ec2_url, aws_connect_params = get_aws_connection_info(module) - + if region in ('us-east-1', '', None): # S3ism for the US Standard region location = Location.DEFAULT @@ -385,7 +384,7 @@ def main(): # use this as fallback because connect_to_region seems to fail in boto + non 'classic' aws accounts in some cases if connection is None: connection = boto.connect_s3(**aws_connect_params) - except (boto.exception.NoAuthHandlerFound, StandardError), e: + except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: module.fail_json(msg=str(e)) expiration_date = module.params.get("expiration_date") @@ -398,13 +397,13 @@ def main(): datetime.datetime.strptime(expiration_date, "%Y-%m-%dT%H:%M:%S.000Z") except ValueError, e: module.fail_json(msg="expiration_date is not a valid ISO-8601 format. The time must be midnight and a timezone of GMT must be included") - + if transition_date is not None: try: datetime.datetime.strptime(transition_date, "%Y-%m-%dT%H:%M:%S.000Z") except ValueError, e: module.fail_json(msg="expiration_date is not a valid ISO-8601 format. The time must be midnight and a timezone of GMT must be included") - + if state == 'present': create_lifecycle_rule(connection, module) elif state == 'absent': diff --git a/cloud/amazon/s3_logging.py b/cloud/amazon/s3_logging.py index 8047a5083bc..dca2a28aca0 100644 --- a/cloud/amazon/s3_logging.py +++ b/cloud/amazon/s3_logging.py @@ -61,7 +61,7 @@ s3_logging: name: mywebsite.com state: absent - + ''' try: @@ -74,21 +74,21 @@ def compare_bucket_logging(bucket, target_bucket, target_prefix): - + bucket_log_obj = bucket.get_logging_status() if bucket_log_obj.target != target_bucket or bucket_log_obj.prefix != target_prefix: return False else: return True - + def enable_bucket_logging(connection, module): - + bucket_name = module.params.get("name") target_bucket = module.params.get("target_bucket") target_prefix = module.params.get("target_prefix") changed = False - + try: bucket = connection.get_bucket(bucket_name) except S3ResponseError as e: @@ -111,15 +111,15 @@ def enable_bucket_logging(connection, module): except S3ResponseError as e: module.fail_json(msg=e.message) - + module.exit_json(changed=changed) - - + + def disable_bucket_logging(connection, module): - + bucket_name = module.params.get("name") changed = False - + try: bucket = connection.get_bucket(bucket_name) if not compare_bucket_logging(bucket, None, None): @@ -127,12 +127,12 @@ def disable_bucket_logging(connection, module): changed = True except S3ResponseError as e: module.fail_json(msg=e.message) - + module.exit_json(changed=changed) - - + + def main(): - + argument_spec = ec2_argument_spec() argument_spec.update( dict( @@ -142,16 +142,16 @@ def main(): state = dict(required=False, default='present', choices=['present', 'absent']) ) ) - + module = AnsibleModule(argument_spec=argument_spec) if not HAS_BOTO: module.fail_json(msg='boto required for this module') - + region, ec2_url, aws_connect_params = get_aws_connection_info(module) if region in ('us-east-1', '', None): - # S3ism for the US Standard region + # S3ism for the US Standard region location = Location.DEFAULT else: # Boto uses symbolic names for locations but region strings will @@ -162,10 +162,9 @@ def main(): # use this as fallback because connect_to_region seems to fail in boto + non 'classic' aws accounts in some cases if connection is None: connection = boto.connect_s3(**aws_connect_params) - except (boto.exception.NoAuthHandlerFound, StandardError), e: + except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: module.fail_json(msg=str(e)) - state = module.params.get("state") if state == 'present': diff --git a/cloud/amazon/sqs_queue.py b/cloud/amazon/sqs_queue.py index 0d098c6df52..de0ca7ebff1 100644 --- a/cloud/amazon/sqs_queue.py +++ b/cloud/amazon/sqs_queue.py @@ -215,8 +215,8 @@ def main(): try: connection = connect_to_aws(boto.sqs, region, **aws_connect_params) - - except (NoAuthHandlerFound, StandardError), e: + + except (NoAuthHandlerFound, AnsibleAWSError), e: module.fail_json(msg=str(e)) state = module.params.get('state') @@ -230,4 +230,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/amazon/sts_assume_role.py b/cloud/amazon/sts_assume_role.py index b089550adab..b5f2c810351 100644 --- a/cloud/amazon/sts_assume_role.py +++ b/cloud/amazon/sts_assume_role.py @@ -16,7 +16,7 @@ DOCUMENTATION = ''' --- -module: sts_assume_role +module: sts_assume_role short_description: Assume a role using AWS Security Token Service and obtain temporary credentials description: - Assume a role using AWS Security Token Service and obtain temporary credentials @@ -25,7 +25,7 @@ options: role_arn: description: - - The Amazon Resource Name (ARN) of the role that the caller is assuming (http://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html#Identifiers_ARNs) + - The Amazon Resource Name (ARN) of the role that the caller is assuming (http://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html#Identifiers_ARNs) required: true role_session_name: description: @@ -33,27 +33,27 @@ required: true policy: description: - - Supplemental policy to use in addition to assumed role's policies. + - Supplemental policy to use in addition to assumed role's policies. required: false default: null duration_seconds: description: - - The duration, in seconds, of the role session. The value can range from 900 seconds (15 minutes) to 3600 seconds (1 hour). By default, the value is set to 3600 seconds. + - The duration, in seconds, of the role session. The value can range from 900 seconds (15 minutes) to 3600 seconds (1 hour). By default, the value is set to 3600 seconds. required: false default: null external_id: description: - - A unique identifier that is used by third parties to assume a role in their customers' accounts. + - A unique identifier that is used by third parties to assume a role in their customers' accounts. required: false default: null mfa_serial_number: description: - - he identification number of the MFA device that is associated with the user who is making the AssumeRole call. + - he identification number of the MFA device that is associated with the user who is making the AssumeRole call. required: false default: null mfa_token: description: - - The value provided by the MFA device, if the trust policy of the role being assumed requires MFA. + - The value provided by the MFA device, if the trust policy of the role being assumed requires MFA. required: false default: null notes: @@ -67,12 +67,12 @@ # Note: These examples do not set authentication details, see the AWS Guide for details. # Assume an existing role (more details: http://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html) -sts_assume_role: +sts_assume_role: role_arn: "arn:aws:iam::123456789012:role/someRole" session_name: "someRoleSession" register: assumed_role -# Use the assumed role above to tag an instance in account 123456789012 +# Use the assumed role above to tag an instance in account 123456789012 ec2_tag: aws_access_key: "{{ assumed_role.sts_creds.access_key }}" aws_secret_key: "{{ assumed_role.sts_creds.secret_key }}" @@ -84,19 +84,16 @@ ''' -import sys -import time - try: import boto.sts from boto.exception import BotoServerError HAS_BOTO = True except ImportError: HAS_BOTO = False - + def assume_role_policy(connection, module): - + role_arn = module.params.get('role_arn') role_session_name = module.params.get('role_session_name') policy = module.params.get('policy') @@ -105,13 +102,13 @@ def assume_role_policy(connection, module): mfa_serial_number = module.params.get('mfa_serial_number') mfa_token = module.params.get('mfa_token') changed = False - + try: assumed_role = connection.assume_role(role_arn, role_session_name, policy, duration_seconds, external_id, mfa_serial_number, mfa_token) - changed = True + changed = True except BotoServerError, e: module.fail_json(msg=e) - + module.exit_json(changed=changed, sts_creds=assumed_role.credentials.__dict__, sts_user=assumed_role.user.__dict__) def main(): @@ -127,18 +124,18 @@ def main(): mfa_token = dict(required=False, default=None) ) ) - + module = AnsibleModule(argument_spec=argument_spec) if not HAS_BOTO: module.fail_json(msg='boto required for this module') - + region, ec2_url, aws_connect_params = get_aws_connection_info(module) - + if region: try: connection = connect_to_aws(boto.sts, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, StandardError), e: + except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: module.fail_json(msg=str(e)) else: module.fail_json(msg="region must be specified") @@ -147,10 +144,11 @@ def main(): assume_role_policy(connection, module) except BotoServerError, e: module.fail_json(msg=e) - - + + # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * -main() +if __name__ == '__main__': + main() From fba8c9f8a7704874f33b260f9d92c942fadb99c4 Mon Sep 17 00:00:00 2001 From: Michael Weber Date: Tue, 1 Dec 2015 01:56:23 -0800 Subject: [PATCH 0996/2522] Fix error 'fail_json() takes exactly 1 argument' Fixes bug #1257 --- monitoring/nagios.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/nagios.py b/monitoring/nagios.py index ee67a3ae20b..b55a374b34f 100644 --- a/monitoring/nagios.py +++ b/monitoring/nagios.py @@ -266,7 +266,7 @@ def main(): module.fail_json(msg='no command passed for command action') ################################################################## if not cmdfile: - module.fail_json('unable to locate nagios.cfg') + module.fail_json(msg='unable to locate nagios.cfg') ################################################################## ansible_nagios = Nagios(module, **module.params) From 32658b9d3b4d0a58459c2fa83a7594bf98676acd Mon Sep 17 00:00:00 2001 From: Guillaume Grossetie Date: Tue, 1 Dec 2015 14:22:25 +0100 Subject: [PATCH 0997/2522] Resolves #1312, Improve pam_limits documentation Adds comment parameter and improve examples. --- system/pam_limits.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/system/pam_limits.py b/system/pam_limits.py index 4003f76d3f8..e14408fb4e2 100644 --- a/system/pam_limits.py +++ b/system/pam_limits.py @@ -78,14 +78,22 @@ - Modify the limits.conf path. required: false default: "/etc/security/limits.conf" + comment: + description: + - Comment associated with the limit. + required: false + default: '' ''' EXAMPLES = ''' -# Add or modify limits for the user joe +# Add or modify nofile soft limit for the user joe - pam_limits: domain=joe limit_type=soft limit_item=nofile value=64000 -# Add or modify limits for the user joe. Keep or set the maximal value -- pam_limits: domain=joe limit_type=soft limit_item=nofile value=1000000 +# Add or modify fsize hard limit for the user smith. Keep or set the maximal value. +- pam_limits: domain=smith limit_type=hard limit_item=fsize value=1000000 use_max=yes + +# Add or modify memlock, both soft and hard, limit for the user james with a comment. +- pam_limits: domain=james limit_type=- limit_item=memlock value=unlimited comment="unlimited memory lock for james" ''' def main(): From 8d866669bbac18a350a2c8616ac1efa94358d68e Mon Sep 17 00:00:00 2001 From: Josh Gachnang Date: Tue, 1 Dec 2015 11:16:29 -0600 Subject: [PATCH 0998/2522] Fix mongodb_user docs typo Bob's last name is Belcher: http://bobs-burgers.wikia.com/wiki/Bob_Belcher. These docs made me chuckle, so thanks :) --- database/misc/mongodb_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index c18ad6004f5..12d348e9a92 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -124,7 +124,7 @@ - mongodb_user: database=burgers name=joe password=12345 roles='readWriteAnyDatabase' state=present # add a user to database in a replica set, the primary server is automatically discovered and written to -- mongodb_user: database=burgers name=bob replica_set=blecher password=12345 roles='readWriteAnyDatabase' state=present +- mongodb_user: database=burgers name=bob replica_set=belcher password=12345 roles='readWriteAnyDatabase' state=present ''' import ConfigParser From 201be81aad5bb0915d4a40e248a27df57a6089dd Mon Sep 17 00:00:00 2001 From: Kirill Kozlov Date: Tue, 1 Dec 2015 23:55:26 +0300 Subject: [PATCH 0999/2522] Set latest as version argument default value --- packaging/language/maven_artifact.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/language/maven_artifact.py b/packaging/language/maven_artifact.py index dd19b67a5bf..4d5a02ca236 100644 --- a/packaging/language/maven_artifact.py +++ b/packaging/language/maven_artifact.py @@ -294,7 +294,7 @@ def main(): argument_spec = dict( group_id = dict(default=None), artifact_id = dict(default=None), - version = dict(default=None), + version = dict(default="latest"), classifier = dict(default=None), extension = dict(default='jar'), repository_url = dict(default=None), From c047814512222b19a082932540cbdb4901b3b96d Mon Sep 17 00:00:00 2001 From: autotune Date: Tue, 1 Dec 2015 17:30:26 -0800 Subject: [PATCH 1000/2522] Fixed bug to recognize haproxy changes --- network/haproxy.py | 50 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/network/haproxy.py b/network/haproxy.py index cada704e342..0fb4beb3004 100644 --- a/network/haproxy.py +++ b/network/haproxy.py @@ -160,6 +160,12 @@ def __init__(self, module): self.wait_retries = self.module.params['wait_retries'] self.wait_interval = self.module.params['wait_interval'] self.command_results = [] + self.status_servers = [] + self.status_weights = [] + self.previous_weights = [] + self.previous_states = [] + self.current_states = [] + self.current_weights = [] def execute(self, cmd, timeout=200, capture_output=True): """ @@ -205,6 +211,34 @@ def wait_until_status(self, pxname, svname, status): self.module.fail_json(msg="server %s/%s not status '%s' after %d retries. Aborting." % (pxname, svname, status, self.wait_retries)) + def get_current_state(self, host, backend): + """ + Gets the each original state value from show stat. + Runs before and after to determine if values are changed. + This relies on weight always being the next element after + status in "show stat" as well as status states remaining + as indicated in status_states and haproxy documentation. + """ + + output = self.execute('show stat') + output = output.lstrip('# ').strip() + output = output.split(',') + result = output + status_states = [ 'UP','DOWN','DRAIN','NOLB','MAINT' ] + self.status_server = [] + status_weight_pos = [] + self.status_weight = [] + + for check, status in enumerate(result): + if status in status_states: + self.status_server.append(status) + status_weight_pos.append(check + 1) + + for weight in status_weight_pos: + self.status_weight.append(result[weight]) + + return{'self.status_server':self.status_server, 'self.status_weight':self.status_weight} + def enabled(self, host, backend, weight): """ Enabled action, marks server to UP and checks are re-enabled, @@ -278,6 +312,10 @@ def act(self): Figure out what you want to do from ansible, and then do it. """ + self.get_current_state(self.host, self.backend) + self.previous_states = ','.join(self.status_server) + self.previous_weights = ','.join(self.status_weight) + # toggle enable/disbale server if self.state == 'enabled': self.enabled(self.host, self.backend, self.weight) @@ -288,7 +326,17 @@ def act(self): else: self.module.fail_json(msg="unknown state specified: '%s'" % self.state) - self.module.exit_json(stdout=self.command_results, changed=True) + self.get_current_state(self.host, self.backend) + self.current_states = ','.join(self.status_server) + self.current_weights = ','.join(self.status_weight) + + + if self.current_weights != self.previous_weights: + self.module.exit_json(stdout=self.command_results, changed=True) + elif self.current_states != self.previous_states: + self.module.exit_json(stdout=self.command_results, changed=True) + else: + self.module.exit_json(stdout=self.command_results, changed=False) def main(): From 426e76dddb1e8ac08d9c16923fa3baa4d218358f Mon Sep 17 00:00:00 2001 From: Ritesh Khadgaray Date: Wed, 2 Dec 2015 19:33:34 +0530 Subject: [PATCH 1001/2522] vmware_vm_shell: add the ability to start program without network connection --- cloud/vmware/vmware_vm_shell.py | 191 ++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 cloud/vmware/vmware_vm_shell.py diff --git a/cloud/vmware/vmware_vm_shell.py b/cloud/vmware/vmware_vm_shell.py new file mode 100644 index 00000000000..c8f42202f42 --- /dev/null +++ b/cloud/vmware/vmware_vm_shell.py @@ -0,0 +1,191 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Ritesh Khadgaray +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: vmware_vm_shell +short_description: Execute a process in VM +description: + - Start a program in a VM without the need for network connection +version_added: 2.0 +author: "Ritesh Khadgaray (@ritzk)" +notes: + - Tested on vSphere 5.5 + - Only the first match against vm_id is used, even if there are multiple matches +requirements: + - "python >= 2.6" + - PyVmomi +options: + hostname: + description: + - The hostname or IP address of the vSphere vCenter API server + required: True + username: + description: + - The username of the vSphere vCenter + required: True + aliases: ['user', 'admin'] + password: + description: + - The password of the vSphere vCenter + required: True + aliases: ['pass', 'pwd'] + datacenter: + description: + - The datacenter hosting the VM + required: False + vm_id: + description: + - The identification for the VM + required: True + vm_id_type: + description: + - The identification tag for the VM + default: dns_name + choices: + - 'uuid' + - 'dns_name' + - 'inventory_path' + required: False + vm_username: + description: + - The user to connect to the VM. + required: False + vm_password: + description: + - The password used to login to the VM. + required: False + vm_shell: + description: + - The absolute path to the program to start. On Linux this is executed via bash. + required: True + vm_shell_args: + description: + - The argument to the program. + required: False + default: None + vm_shell_env: + description: + - Comma seperated list of envirnoment variable, specified in the guest OS notation + required: False + default: None + vm_shell_cwd: + description: + - The current working directory of the application from which it will be run + required: False + default: None +''' + +EXAMPLES = ''' + - name: shell execution + local_action: + module: vmware_vm_shell + hostname: myVSphere + username: myUsername + password: mySecret + datacenter: myDatacenter + vm_id: DNSnameOfVM + vm_username: root + vm_password: superSecret + vm_shell: /bin/echo + vm_shell_args: " $var >> myFile " + vm_shell_env: + - "PATH=/bin" + - "var=test" + vm_shell_cwd: "/tmp" + +''' + +try: + from pyVmomi import vim, vmodl + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + +def find_vm(content, vm_id, vm_id_type="dns_name", datacenter=None): + si = content.searchIndex + vm = None + + if datacenter: + datacenter = find_datacenter_by_name(content, datacenter) + + if vm_id_type == 'dns_name': + vm = si.FindByDnsName(datacenter=datacenter, dnsName=vm_id, vmSearch=True) + elif vm_id_type == 'inventory_path': + vm = si.FindByInventoryPath(inventoryPath=vm_id) + if type(vm) != type(vim.VirtualMachine): + vm = None + elif vm_id_type == 'uuid': + vm = si.FindByUuid(datacenter=datacenter, uuid=vm_id, vmSearch=True) + + return vm + +# https://github.com/vmware/pyvmomi-community-samples/blob/master/samples/execute_program_in_vm.py +def execute_command(content, vm, vm_username, vm_password, program_path, args="", env=None, cwd=None): + + creds = vim.vm.guest.NamePasswordAuthentication(username=vm_username, password=vm_password) + cmdspec = vim.vm.guest.ProcessManager.ProgramSpec(arguments=args, envVariables=env, programPath=program_path, workingDirectory=cwd) + cmdpid = content.guestOperationsManager.processManager.StartProgramInGuest(vm=vm, auth=creds, spec=cmdspec) + + return cmdpid + +def main(): + + argument_spec = vmware_argument_spec() + argument_spec.update(dict(datacenter=dict(default=None, type='str'), + vm_id=dict(required=True, type='str'), + vm_id_type=dict(default='dns_name', type='str', choices=['inventory_path', 'uuid', 'dns_name']), + vm_username=dict(required=False, type='str'), + vm_password=dict(required=False, type='str', no_log=True), + vm_shell=dict(required=True, type='str'), + vm_shell_args=dict(default=" ", type='str'), + vm_shell_env=dict(default=None, type='list'), + vm_shell_cwd=dict(default=None, type='str'))) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False) + + if not HAS_PYVMOMI: + module.fail_json(msg='pyvmomi is required for this module') + + try: + p = module.params + content = connect_to_api(module) + + vm = find_vm(content, p['vm_id'], p['vm_id_type'], p['datacenter']) + if not vm: + module.fail_json(msg='failed to find VM') + + msg = execute_command(content, vm, p['vm_username'], p['vm_password'], + p['vm_shell'], p['vm_shell_args'], p['vm_shell_env'], p['vm_shell_cwd']) + + module.exit_json(changed=False, virtual_machines=vm.name, msg=msg) + except vmodl.RuntimeFault as runtime_fault: + module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + module.fail_json(msg=method_fault.msg) + except Exception as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.vmware import * +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() + From 186fe2babcf41092975f50c1c61cd2642cbba935 Mon Sep 17 00:00:00 2001 From: Rob Date: Fri, 4 Dec 2015 01:50:38 +0000 Subject: [PATCH 1002/2522] Added missing else statement that caused existing rules to be discarded --- cloud/amazon/s3_lifecycle.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cloud/amazon/s3_lifecycle.py b/cloud/amazon/s3_lifecycle.py index 891beac01f1..bdfdc49da91 100644 --- a/cloud/amazon/s3_lifecycle.py +++ b/cloud/amazon/s3_lifecycle.py @@ -219,6 +219,8 @@ def create_lifecycle_rule(connection, module): lifecycle_obj.append(rule) changed = True appended = True + else: + lifecycle_obj.append(existing_rule) # If nothing appended then append now as the rule must not exist if not appended: lifecycle_obj.append(rule) From 8ec66713d9056f4fd0a8d584bc7ddb418b01efcc Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Thu, 3 Dec 2015 01:12:34 +0100 Subject: [PATCH 1003/2522] cloudstack: fixes and improvements - cs_affinitygroup: add project support Project support in CloudStack for affinity groups is going to be fixed/implemented in the near future, this module should already support. - cs_affinitygroup: fix missing returns in doc - cs_volume: fix disk offering not found - cs_volume: fix volume not found if created with display_volume=no - cs_firewall: argument zone is missing, default zone is always used. credits for reporting and fixing to @atsaki closes #1320 - cs_instance: fix user_data base64 encoding fails if not a string --- cloud/cloudstack/cs_affinitygroup.py | 24 ++++++++++++++++++++++++ cloud/cloudstack/cs_firewall.py | 7 +++++++ cloud/cloudstack/cs_instance.py | 2 +- cloud/cloudstack/cs_volume.py | 7 +++---- 4 files changed, 35 insertions(+), 5 deletions(-) diff --git a/cloud/cloudstack/cs_affinitygroup.py b/cloud/cloudstack/cs_affinitygroup.py index 5a7cb5f9714..b1dc075c434 100644 --- a/cloud/cloudstack/cs_affinitygroup.py +++ b/cloud/cloudstack/cs_affinitygroup.py @@ -57,6 +57,11 @@ - Account the affinity group is related to. required: false default: null + project: + description: + - Name of the project the affinity group is related to. + required: false + default: null poll_async: description: - Poll async jobs until job has finished. @@ -101,6 +106,21 @@ returned: success type: string sample: host anti-affinity +project: + description: Name of project the affinity group is related to. + returned: success + type: string + sample: Production +domain: + description: Domain the affinity group is related to. + returned: success + type: string + sample: example domain +account: + description: Account the affinity group is related to. + returned: success + type: string + sample: example account ''' try: @@ -128,6 +148,7 @@ def get_affinity_group(self): affinity_group = self.module.params.get('name') args = {} + args['projectid'] = self.get_project(key='id') args['account'] = self.get_account('name') args['domainid'] = self.get_domain('id') @@ -163,6 +184,7 @@ def create_affinity_group(self): args['name'] = self.module.params.get('name') args['type'] = self.get_affinity_type() args['description'] = self.module.params.get('description') + args['projectid'] = self.get_project(key='id') args['account'] = self.get_account('name') args['domainid'] = self.get_domain('id') @@ -185,6 +207,7 @@ def remove_affinity_group(self): args = {} args['name'] = self.module.params.get('name') + args['projectid'] = self.get_project(key='id') args['account'] = self.get_account('name') args['domainid'] = self.get_domain('id') @@ -209,6 +232,7 @@ def main(): state = dict(choices=['present', 'absent'], default='present'), domain = dict(default=None), account = dict(default=None), + project = dict(default=None), poll_async = dict(choices=BOOLEANS, default=True), )) diff --git a/cloud/cloudstack/cs_firewall.py b/cloud/cloudstack/cs_firewall.py index 4f4c1e7895a..9834dd6713a 100644 --- a/cloud/cloudstack/cs_firewall.py +++ b/cloud/cloudstack/cs_firewall.py @@ -99,6 +99,12 @@ - Name of the project the firewall rule is related to. required: false default: null + zone: + description: + - Name of the zone in which the virtual machine is in. + - If not set, default zone is used. + required: false + default: null poll_async: description: - Poll async jobs until job has finished. @@ -404,6 +410,7 @@ def main(): start_port = dict(type='int', aliases=['port'], default=None), end_port = dict(type='int', default=None), state = dict(choices=['present', 'absent'], default='present'), + zone = dict(default=None), domain = dict(default=None), account = dict(default=None), project = dict(default=None), diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 8cf0672e85a..2b662a2084e 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -558,7 +558,7 @@ def present_instance(self): def get_user_data(self): user_data = self.module.params.get('user_data') if user_data: - user_data = base64.b64encode(user_data) + user_data = base64.b64encode(str(user_data)) return user_data def get_details(self): diff --git a/cloud/cloudstack/cs_volume.py b/cloud/cloudstack/cs_volume.py index 30548555587..ebc5bd28891 100644 --- a/cloud/cloudstack/cs_volume.py +++ b/cloud/cloudstack/cs_volume.py @@ -255,10 +255,8 @@ def get_disk_offering(self, key=None): if not disk_offering: return None - args = {} - args['domainid'] = self.get_domain(key='id') - - disk_offerings = self.cs.listDiskOfferings(**args) + # Do not add domain filter for disk offering listing. + disk_offerings = self.cs.listDiskOfferings() if disk_offerings: for d in disk_offerings['diskoffering']: if disk_offering in [d['displaytext'], d['name'], d['id']]: @@ -272,6 +270,7 @@ def get_volume(self): args['account'] = self.get_account(key='name') args['domainid'] = self.get_domain(key='id') args['projectid'] = self.get_project(key='id') + args['displayvolume'] = self.module.params.get('display_volume') args['type'] = 'DATADISK' volumes = self.cs.listVolumes(**args) From 037ff890639ff5387a170398a4b2f0842b8d55d3 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sun, 29 Nov 2015 23:48:50 +0100 Subject: [PATCH 1004/2522] Add a more explicit error message, fix #1282 --- system/firewalld.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system/firewalld.py b/system/firewalld.py index 47d98544000..4a2e7644bf1 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -75,6 +75,7 @@ default: 0 notes: - Not tested on any Debian based system. + - Requires the python2 bindings of firewalld, who may not be installed by default if the distribution switched to python 3 requirements: [ 'firewalld >= 0.2.11' ] author: "Adam Miller (@maxamillion)" ''' @@ -251,7 +252,7 @@ def main(): module.fail(msg='permanent is a required parameter') if not HAS_FIREWALLD: - module.fail_json(msg='firewalld required for this module') + module.fail_json(msg='firewalld and its python 2 module are required for this module') ## Pre-run version checking if FW_VERSION < "0.2.11": From bc42aea3a30e8e46bd7286df57b58ba6c713bf05 Mon Sep 17 00:00:00 2001 From: gfrank Date: Tue, 3 Nov 2015 09:48:57 -0500 Subject: [PATCH 1005/2522] Replace slashes in the parameter string Also remove duplicate documentation --- windows/win_nssm.ps1 | 4 +++- windows/win_nssm.py | 5 ----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/windows/win_nssm.ps1 b/windows/win_nssm.ps1 index fa61afdaafc..588a2f22672 100644 --- a/windows/win_nssm.ps1 +++ b/windows/win_nssm.ps1 @@ -149,7 +149,9 @@ Function ParseAppParameters() [string]$appParameters ) - return ConvertFrom-StringData -StringData $appParameters.TrimStart("@").TrimStart("{").TrimEnd("}").Replace("; ","`n") + $escapedAppParameters = $appParameters.TrimStart("@").TrimStart("{").TrimEnd("}").Replace("; ","`n").Replace("\","\\") + + return ConvertFrom-StringData -StringData $escapedAppParameters } diff --git a/windows/win_nssm.py b/windows/win_nssm.py index 98be076a48b..c0a4332cc3b 100644 --- a/windows/win_nssm.py +++ b/windows/win_nssm.py @@ -86,11 +86,6 @@ - Password to be used for service startup required: false default: null - password: - description: - - Password to be used for service startup - required: false - default: null start_mode: description: - If C(auto) is selected, the service will start at bootup. C(manual) means that the service will start only when another service needs it. C(disabled) means that the service will stay off, regardless if it is needed or not. From cfe4f59b5be73cf28c615c376a2f537c5e7c881f Mon Sep 17 00:00:00 2001 From: gfrank Date: Fri, 4 Dec 2015 15:46:06 -0500 Subject: [PATCH 1006/2522] Use "" for AppParameters if it's null --- windows/win_nssm.ps1 | 47 ++++++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/windows/win_nssm.ps1 b/windows/win_nssm.ps1 index fa61afdaafc..9f9902d8247 100644 --- a/windows/win_nssm.ps1 +++ b/windows/win_nssm.ps1 @@ -174,35 +174,44 @@ Function Nssm-Update-AppParameters Throw "Error updating AppParameters for service ""$name""" } - $appParametersHash = ParseAppParameters -appParameters $appParameters - $appParamKeys = @() $appParamVals = @() $singleLineParams = "" - $appParametersHash.GetEnumerator() | - % { - $key = $($_.Name) - $val = $($_.Value) - - $appParamKeys += $key - $appParamVals += $val - - if ($key -eq "_") { - $singleLineParams = "$val " + $singleLineParams - } else { - $singleLineParams = $singleLineParams + "$key ""$val""" + + if ($appParameters) + { + $appParametersHash = ParseAppParameters -appParameters $appParameters + $appParametersHash.GetEnumerator() | + % { + $key = $($_.Name) + $val = $($_.Value) + + $appParamKeys += $key + $appParamVals += $val + + if ($key -eq "_") { + $singleLineParams = "$val " + $singleLineParams + } else { + $singleLineParams = $singleLineParams + "$key ""$val""" + } } - } + + Set-Attr $result "nssm_app_parameters_parsed" $appParametersHash + Set-Attr $result "nssm_app_parameters_keys" $appParamKeys + Set-Attr $result "nssm_app_parameters_vals" $appParamVals + } Set-Attr $result "nssm_app_parameters" $appParameters - Set-Attr $result "nssm_app_parameters_parsed" $appParametersHash - Set-Attr $result "nssm_app_parameters_keys" $appParamKeys - Set-Attr $result "nssm_app_parameters_vals" $appParamVals Set-Attr $result "nssm_single_line_app_parameters" $singleLineParams if ($results -ne $singleLineParams) { - $cmd = "nssm set ""$name"" AppParameters $singleLineParams" + if ($appParameters) + { + $cmd = "nssm set ""$name"" AppParameters $singleLineParams" + } else { + $cmd = "nssm set ""$name"" AppParameters '""""'" + } $results = invoke-expression $cmd if ($LastExitCode -ne 0) From 91d3c02571ea0b8d56f641b2e9083065e6522b6a Mon Sep 17 00:00:00 2001 From: Atsushi Sasaki Date: Sat, 5 Dec 2015 19:21:28 +0900 Subject: [PATCH 1007/2522] Enable to resize a volume with cs_volume --- cloud/cloudstack/cs_volume.py | 38 ++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/cloud/cloudstack/cs_volume.py b/cloud/cloudstack/cs_volume.py index ebc5bd28891..5d01046d0a9 100644 --- a/cloud/cloudstack/cs_volume.py +++ b/cloud/cloudstack/cs_volume.py @@ -95,6 +95,11 @@ - Considered on C(state=absnet) only. required: false default: false + shrink_ok: + description: + - Whether to allow to shrink the volume. + required: false + default: false vm: description: - Name of the virtual machine to attach the volume to. @@ -302,7 +307,9 @@ def get_snapshot(self, key=None): def present_volume(self): volume = self.get_volume() - if not volume: + if volume: + volume = self.update_volume(volume) + else: disk_offering_id = self.get_disk_offering(key='id') snapshot_id = self.get_snapshot(key='id') @@ -404,6 +411,34 @@ def absent_volume(self): return volume + def update_volume(self, volume): + args_resize = {} + args_resize['id'] = volume['id'] + args_resize['diskofferingid'] = self.get_disk_offering(key='id') + args_resize['maxiops'] = self.module.params.get('max_iops') + args_resize['miniops'] = self.module.params.get('min_iops') + args_resize['size'] = self.module.params.get('size') + + # change unit from bytes to giga bytes to compare with args + volume_copy = volume.copy() + volume_copy['size'] = volume_copy['size'] / (2**30) + + if self.has_changed(args_resize, volume_copy): + + self.result['changed'] = True + if not self.module.check_mode: + args_resize['shrinkok'] = self.module.params.get('shrink_ok') + res = self.cs.resizeVolume(**args_resize) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + poll_async = self.module.params.get('poll_async') + if poll_async: + volume = self.poll_job(res, 'volume') + self.volume = volume + + return volume + + def main(): argument_spec = cs_argument_spec() argument_spec.update(dict( @@ -418,6 +453,7 @@ def main(): device_id = dict(type='int', default=None), custom_id = dict(default=None), force = dict(choices=BOOLEANS, default=False), + shrink_ok = dict(choices=BOOLEANS, default=False), state = dict(choices=['present', 'absent', 'attached', 'detached'], default='present'), zone = dict(default=None), domain = dict(default=None), From 4c0b91fd9b9202906e0d59ca04ef45075464dec7 Mon Sep 17 00:00:00 2001 From: quoing Date: Sat, 5 Dec 2015 17:05:02 +0100 Subject: [PATCH 1008/2522] Fix 'An error occurred while looking up _xmpp-client._tcp.10.100.1.108' when IP host is configured ... jabber: user=ansible@mydomain.tld host=10.100.1.108 ... fatal: [bruce.mess.cz] => failed to parse: Invalid debugflag given: always Invalid debugflag given: nodebuilder --- notification/jabber.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notification/jabber.py b/notification/jabber.py index 6d97e4232df..6d9c19b789c 100644 --- a/notification/jabber.py +++ b/notification/jabber.py @@ -134,7 +134,7 @@ def main(): msg = xmpp.protocol.Message(body=module.params['msg']) try: - conn=xmpp.Client(server) + conn=xmpp.Client(server, debug=[]) if not conn.connect(server=(host,port)): module.fail_json(rc=1, msg='Failed to connect to server: %s' % (server)) if not conn.auth(user,password,'Ansible'): From 9ba686f8e6018e036e3693b7729ab6b54e9fd332 Mon Sep 17 00:00:00 2001 From: Vladimir Dimov Date: Mon, 7 Dec 2015 18:55:45 +0200 Subject: [PATCH 1009/2522] Doc fix on route53_health_check.py. Fixed first example. Should be register instead of record. --- cloud/amazon/route53_health_check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/route53_health_check.py b/cloud/amazon/route53_health_check.py index a4dcfea6b30..9ad7f63d450 100644 --- a/cloud/amazon/route53_health_check.py +++ b/cloud/amazon/route53_health_check.py @@ -104,7 +104,7 @@ string_match: "Hello" request_interval: 10 failure_threshold: 2 - record: my_health_check + register: my_health_check - route53: action: create From de672f1ab22392af350bdf98f04f075b7e387648 Mon Sep 17 00:00:00 2001 From: Vladimir Dimov Date: Mon, 7 Dec 2015 19:15:24 +0200 Subject: [PATCH 1010/2522] Doc fix ec2_vpc_route_table.py --- cloud/amazon/ec2_vpc_route_table.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index eef58c23ced..f4d27f9580f 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -77,8 +77,8 @@ tags: Name: Public subnets: - - "{{ jumpbox_subnet.subnet_id }}" - - "{{ frontend_subnet.subnet_id }}" + - "{{ jumpbox_subnet.subnet.id }}" + - "{{ frontend_subnet.subnet.id }}" - "{{ vpn_subnet.subnet_id }}" routes: - dest: 0.0.0.0/0 @@ -92,7 +92,7 @@ tags: Name: Internal subnets: - - "{{ application_subnet.subnet_id }}" + - "{{ application_subnet.subnet.id }}" - 'Database Subnet' - '10.0.0.0/8' routes: From 2ce866f759bae9e599a2af919c9bc8761d883ebf Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 7 Dec 2015 10:03:13 -0800 Subject: [PATCH 1011/2522] corrected version added to 2.1 --- cloud/amazon/ec2_vpc_subnet_facts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_vpc_subnet_facts.py b/cloud/amazon/ec2_vpc_subnet_facts.py index bfad2fb72a7..804c48ef50f 100644 --- a/cloud/amazon/ec2_vpc_subnet_facts.py +++ b/cloud/amazon/ec2_vpc_subnet_facts.py @@ -19,7 +19,7 @@ short_description: Gather facts about ec2 VPC subnets in AWS description: - Gather facts about ec2 VPC subnets in AWS -version_added: "2.0" +version_added: "2.1" author: "Rob White (@wimnat)" options: filters: From b0196edb3464a947f52adf4dabd5b4f38ea140be Mon Sep 17 00:00:00 2001 From: Gordon Fierce Date: Mon, 7 Dec 2015 18:47:26 -0500 Subject: [PATCH 1012/2522] Fix documentation to prevent formatting error where each letter has its own line. --- system/iptables.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/system/iptables.py b/system/iptables.py index b9368e0688f..f3ae8da9383 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -54,12 +54,14 @@ default: filter choices: [ "filter", "nat", "mangle", "raw", "security" ] state: - description: Whether the rule should be absent or present. + description: + - Whether the rule should be absent or present. required: false default: present choices: [ "present", "absent" ] ip_version: - description: Which version of the IP protocol this rule should apply to. + description: + - Which version of the IP protocol this rule should apply to. required: false default: ipv4 choices: [ "ipv4", "ipv6" ] From 5d1e9c6acb19815b45483322321e0172ed2bd18a Mon Sep 17 00:00:00 2001 From: daveres Date: Tue, 8 Dec 2015 19:00:31 +0100 Subject: [PATCH 1013/2522] Correct problem with changed:true I have just changed a small piece of this module to avoid to return always that the task is changed --- windows/win_chocolatey.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_chocolatey.ps1 b/windows/win_chocolatey.ps1 index ce006fff152..2c9a945840c 100644 --- a/windows/win_chocolatey.ps1 +++ b/windows/win_chocolatey.ps1 @@ -86,7 +86,7 @@ Function Choco-IsInstalled Throw "Error checking installation status for $package" } - If ("$results" -match " $package .* (\d+) packages installed.") + If ("$results" -match "$package .* (\d+) packages installed.") { return $matches[1] -gt 0 } From e549d967233879981d3639c83d757bafd8274265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Wir=C3=A9n?= Date: Sun, 6 Dec 2015 21:34:55 +0100 Subject: [PATCH 1014/2522] Changes how zfs properties are handled This moves the validation of properties to the zfs command itself. The properties and their choices were not really correct anyway due to differences between OpenZFS and Solaris/ZFS. --- system/zfs.py | 353 ++++++++++++-------------------------------------- 1 file changed, 80 insertions(+), 273 deletions(-) diff --git a/system/zfs.py b/system/zfs.py index 343f6ea3202..f9c7a5bad30 100644 --- a/system/zfs.py +++ b/system/zfs.py @@ -24,7 +24,7 @@ module: zfs short_description: Manage zfs description: - - Manages ZFS file systems on Solaris and FreeBSD. Can manage file systems, volumes and snapshots. See zfs(1M) for more information about the properties. + - Manages ZFS file systems, volumes, clones and snapshots. version_added: "1.1" options: name: @@ -34,183 +34,21 @@ state: description: - Whether to create (C(present)), or remove (C(absent)) a file system, snapshot or volume. + choices: ['present', 'absent'] required: true - choices: [present, absent] - aclinherit: - description: - - The aclinherit property. - required: False - choices: [discard,noallow,restricted,passthrough,passthrough-x] - aclmode: - description: - - The aclmode property. - required: False - choices: [discard,groupmask,passthrough] - atime: - description: - - The atime property. - required: False - choices: ['on','off'] - canmount: - description: - - The canmount property. - required: False - choices: ['on','off','noauto'] - casesensitivity: - description: - - The casesensitivity property. - required: False - choices: [sensitive,insensitive,mixed] - checksum: - description: - - The checksum property. - required: False - choices: ['on','off',fletcher2,fletcher4,sha256] - compression: - description: - - The compression property. - required: False - choices: ['on','off',lzjb,gzip,gzip-1,gzip-2,gzip-3,gzip-4,gzip-5,gzip-6,gzip-7,gzip-8,gzip-9,lz4,zle] - copies: - description: - - The copies property. - required: False - choices: [1,2,3] - dedup: - description: - - The dedup property. - required: False - choices: ['on','off'] - devices: - description: - - The devices property. - required: False - choices: ['on','off'] - exec: - description: - - The exec property. - required: False - choices: ['on','off'] - jailed: - description: - - The jailed property. - required: False - choices: ['on','off'] - logbias: - description: - - The logbias property. - required: False - choices: [latency,throughput] - mountpoint: - description: - - The mountpoint property. - required: False - nbmand: - description: - - The nbmand property. - required: False - choices: ['on','off'] - normalization: - description: - - The normalization property. - required: False - choices: [none,formC,formD,formKC,formKD] origin: description: - - Name of the snapshot to clone - required: False - version_added: "2.0" - primarycache: - description: - - The primarycache property. - required: False - choices: [all,none,metadata] - quota: - description: - - The quota property. - required: False - readonly: - description: - - The readonly property. - required: False - choices: ['on','off'] - recordsize: - description: - - The recordsize property. - required: False - refquota: - description: - - The refquota property. - required: False - refreservation: - description: - - The refreservation property. - required: False - reservation: - description: - - The reservation property. - required: False - secondarycache: - description: - - The secondarycache property. - required: False - choices: [all,none,metadata] - setuid: + - Snapshot from which to create a clone + required: false + createparent: description: - - The setuid property. - required: False - choices: ['on','off'] - shareiscsi: + - Creates all non-existing parent file systems. + required: false + default: "on" + key_value: description: - - The shareiscsi property. - required: False - choices: ['on','off'] - sharenfs: - description: - - The sharenfs property. - required: False - sharesmb: - description: - - The sharesmb property. - required: False - snapdir: - description: - - The snapdir property. - required: False - choices: [hidden,visible] - sync: - description: - - The sync property. - required: False - choices: ['standard','always','disabled'] - utf8only: - description: - - The utf8only property. - required: False - choices: ['on','off'] - volsize: - description: - - The volsize property. - required: False - volblocksize: - description: - - The volblocksize property. - required: False - vscan: - description: - - The vscan property. - required: False - choices: ['on','off'] - xattr: - description: - - The xattr property. - required: False - choices: ['on','off'] - zoned: - description: - - The zoned property. - required: False - choices: ['on','off'] + - The C(zfs) module takes key=value pairs for zfs properties to be set. See the zfs(8) man page for more information. + author: "Johan Wiren (@johanwiren)" ''' @@ -218,7 +56,7 @@ # Create a new file system called myfs in pool rpool - zfs: name=rpool/myfs state=present -# Create a new volume called myvol in pool rpool. +# Create a new volume called myvol in pool rpool. - zfs: name=rpool/myvol state=present volsize=10M # Create a snapshot of rpool/myfs file system. @@ -237,20 +75,33 @@ import os + class Zfs(object): + def __init__(self, module, name, properties): self.module = module self.name = name self.properties = properties self.changed = False - - self.immutable_properties = [ 'casesensitivity', 'normalization', 'utf8only' ] + self.is_solaris = os.uname()[0] == 'SunOS' + self.pool = name.split('/')[0] + self.zfs_cmd = module.get_bin_path('zfs', True) + self.zpool_cmd = module.get_bin_path('zpool', True) + self.enhanced_sharing = self.check_enhanced_sharing() + + def check_enhanced_sharing(self): + if os.uname()[0] == 'SunOS': + cmd = [self.zpool_cmd] + cmd.extend(['get', 'version']) + cmd.append(self.pool) + (rc, out, err) = self.module.run_command(cmd, check_rc=True) + version = out.splitlines()[-1].split()[2] + if int(version) >= 34: + return True + return False def exists(self): - cmd = [self.module.get_bin_path('zfs', True)] - cmd.append('list') - cmd.append('-t all') - cmd.append(self.name) + cmd = [self.zfs_cmd, 'list', '-t', 'all', self.name] (rc, out, err) = self.module.run_command(' '.join(cmd)) if rc == 0: return True @@ -265,7 +116,9 @@ def create(self): volsize = properties.pop('volsize', None) volblocksize = properties.pop('volblocksize', None) origin = properties.pop('origin', None) - createparent = properties.pop('createparent', None) + createparent = self.module.params.get('createparent') + cmd = [self.zfs_cmd] + if "@" in self.name: action = 'snapshot' elif origin: @@ -273,135 +126,83 @@ def create(self): else: action = 'create' - cmd = [self.module.get_bin_path('zfs', True)] cmd.append(action) - if createparent: - cmd.append('-p') + if action in ['create', 'clone']: + if createparent: + cmd += ['-p'] + if volsize: + cmd += ['-V', volsize] if volblocksize: - cmd.append('-b %s' % volblocksize) + cmd += ['-b', 'volblocksize'] if properties: for prop, value in properties.iteritems(): - cmd.append('-o %s="%s"' % (prop, value)) - if volsize: - cmd.append('-V') - cmd.append(volsize) + cmd += ['-o', '%s="%s"' % (prop, value)] if origin: cmd.append(origin) cmd.append(self.name) - (rc, err, out) = self.module.run_command(' '.join(cmd)) + (rc, out, err) = self.module.run_command(' '.join(cmd)) if rc == 0: self.changed = True else: - self.module.fail_json(msg=out) + self.module.fail_json(msg=err) def destroy(self): if self.module.check_mode: self.changed = True return - cmd = [self.module.get_bin_path('zfs', True)] - cmd.append('destroy') - cmd.append(self.name) - (rc, err, out) = self.module.run_command(' '.join(cmd)) + cmd = [self.zfs_cmd, 'destroy', '-R', self.name] + (rc, out, err) = self.module.run_command(' '.join(cmd)) if rc == 0: self.changed = True else: - self.module.fail_json(msg=out) + self.module.fail_json(msg=err) def set_property(self, prop, value): if self.module.check_mode: self.changed = True return - cmd = self.module.get_bin_path('zfs', True) - args = [cmd, 'set', prop + '=' + value, self.name] - (rc, err, out) = self.module.run_command(args) + cmd = [self.zfs_cmd, 'set', prop + '=' + str(value), self.name] + (rc, out, err) = self.module.run_command(cmd) if rc == 0: self.changed = True else: - self.module.fail_json(msg=out) + self.module.fail_json(msg=err) def set_properties_if_changed(self): current_properties = self.get_current_properties() for prop, value in self.properties.iteritems(): + if prop not in current_properties: + self.module.fail_json(msg="invalid property '%s'" % prop) if current_properties[prop] != value: - if prop in self.immutable_properties: - self.module.fail_json(msg='Cannot change property %s after creation.' % prop) - else: - self.set_property(prop, value) + self.set_property(prop, value) def get_current_properties(self): - def get_properties_by_name(propname): - cmd = [self.module.get_bin_path('zfs', True)] - cmd += ['get', '-H', propname, self.name] - rc, out, err = self.module.run_command(cmd) - return [l.split('\t')[1:3] for l in out.splitlines()] - properties = dict(get_properties_by_name('all')) - if 'share.*' in properties: - # Some ZFS pools list the sharenfs and sharesmb properties - # hierarchically as share.nfs and share.smb respectively. - del properties['share.*'] - for p, v in get_properties_by_name('share.all'): - alias = p.replace('.', '') # share.nfs -> sharenfs (etc) - properties[alias] = v + cmd = [self.zfs_cmd, 'get', '-H'] + if self.enhanced_sharing: + cmd += ['-e'] + cmd += ['all', self.name] + rc, out, err = self.module.run_command(" ".join(cmd)) + properties = dict() + for p, v in [l.split('\t')[1:3] for l in out.splitlines()]: + properties[p] = v + # Add alias for enhanced sharing properties + properties['sharenfs'] = properties.get('share.nfs', None) + properties['sharesmb'] = properties.get('share.smb', None) return properties - def run_command(self, cmd): - progname = cmd[0] - cmd[0] = module.get_bin_path(progname, True) - return module.run_command(cmd) def main(): - # FIXME: should use dict() constructor like other modules, required=False is default module = AnsibleModule( - argument_spec = { - 'name': {'required': True}, - 'state': {'required': True, 'choices':['present', 'absent']}, - 'aclinherit': {'required': False, 'choices':['discard', 'noallow', 'restricted', 'passthrough', 'passthrough-x']}, - 'aclmode': {'required': False, 'choices':['discard', 'groupmask', 'passthrough']}, - 'atime': {'required': False, 'choices':['on', 'off']}, - 'canmount': {'required': False, 'choices':['on', 'off', 'noauto']}, - 'casesensitivity': {'required': False, 'choices':['sensitive', 'insensitive', 'mixed']}, - 'checksum': {'required': False, 'choices':['on', 'off', 'fletcher2', 'fletcher4', 'sha256']}, - 'compression': {'required': False, 'choices':['on', 'off', 'lzjb', 'gzip', 'gzip-1', 'gzip-2', 'gzip-3', 'gzip-4', 'gzip-5', 'gzip-6', 'gzip-7', 'gzip-8', 'gzip-9', 'lz4', 'zle']}, - 'copies': {'required': False, 'choices':['1', '2', '3']}, - 'createparent': {'required': False, 'choices':['on', 'off']}, - 'dedup': {'required': False, 'choices':['on', 'off']}, - 'devices': {'required': False, 'choices':['on', 'off']}, - 'exec': {'required': False, 'choices':['on', 'off']}, - # Not supported - #'groupquota': {'required': False}, - 'jailed': {'required': False, 'choices':['on', 'off']}, - 'logbias': {'required': False, 'choices':['latency', 'throughput']}, - 'mountpoint': {'required': False}, - 'nbmand': {'required': False, 'choices':['on', 'off']}, - 'normalization': {'required': False, 'choices':['none', 'formC', 'formD', 'formKC', 'formKD']}, - 'origin': {'required': False}, - 'primarycache': {'required': False, 'choices':['all', 'none', 'metadata']}, - 'quota': {'required': False}, - 'readonly': {'required': False, 'choices':['on', 'off']}, - 'recordsize': {'required': False}, - 'refquota': {'required': False}, - 'refreservation': {'required': False}, - 'reservation': {'required': False}, - 'secondarycache': {'required': False, 'choices':['all', 'none', 'metadata']}, - 'setuid': {'required': False, 'choices':['on', 'off']}, - 'shareiscsi': {'required': False, 'choices':['on', 'off']}, - 'sharenfs': {'required': False}, - 'sharesmb': {'required': False}, - 'snapdir': {'required': False, 'choices':['hidden', 'visible']}, - 'sync': {'required': False, 'choices':['standard', 'always', 'disabled']}, - # Not supported - #'userquota': {'required': False}, - 'utf8only': {'required': False, 'choices':['on', 'off']}, - 'volsize': {'required': False}, - 'volblocksize': {'required': False}, - 'vscan': {'required': False, 'choices':['on', 'off']}, - 'xattr': {'required': False, 'choices':['on', 'off']}, - 'zoned': {'required': False, 'choices':['on', 'off']}, - }, - supports_check_mode=True + argument_spec = dict( + name = dict(type='str', required=True), + state = dict(type='str', required=True, choices=['present', 'absent']), + createparent = dict(type='bool', required=False, default=True), + ), + supports_check_mode=True, + check_invalid_arguments=False ) state = module.params.pop('state') @@ -410,10 +211,16 @@ def main(): # Get all valid zfs-properties properties = dict() for prop, value in module.params.iteritems(): - if prop in ['CHECKMODE']: - continue - if value: - properties[prop] = value + # All freestyle params are zfs properties + if prop not in module.argument_spec: + # Reverse the boolification of freestyle zfs properties + if type(value) == bool: + if value is True: + properties[prop] = 'on' + else: + properties[prop] = 'off' + else: + properties[prop] = value result = {} result['name'] = name From 58d23cba68ee666bd75d66420f4eeb21d23fa90a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Wir=C3=A9n?= Date: Wed, 9 Dec 2015 00:02:34 +0100 Subject: [PATCH 1015/2522] Update example --- system/zfs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/zfs.py b/system/zfs.py index f9c7a5bad30..ff3e89a3092 100644 --- a/system/zfs.py +++ b/system/zfs.py @@ -53,8 +53,8 @@ ''' EXAMPLES = ''' -# Create a new file system called myfs in pool rpool -- zfs: name=rpool/myfs state=present +# Create a new file system called myfs in pool rpool with the setuid property turned off +- zfs: name=rpool/myfs state=present setuid=off # Create a new volume called myvol in pool rpool. - zfs: name=rpool/myvol state=present volsize=10M From fed4b2802063867861419ee1088ce8dc5021938a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Wir=C3=A9n?= Date: Wed, 9 Dec 2015 00:30:39 +0100 Subject: [PATCH 1016/2522] Documentation fixes --- system/zfs.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/system/zfs.py b/system/zfs.py index ff3e89a3092..459d9d5582e 100644 --- a/system/zfs.py +++ b/system/zfs.py @@ -39,6 +39,7 @@ origin: description: - Snapshot from which to create a clone + default: null required: false createparent: description: @@ -48,6 +49,8 @@ key_value: description: - The C(zfs) module takes key=value pairs for zfs properties to be set. See the zfs(8) man page for more information. + default: null + required: false author: "Johan Wiren (@johanwiren)" ''' From cbed642009497ddaf19b5f578ab6c78da1356eda Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 9 Dec 2015 12:07:36 -0800 Subject: [PATCH 1017/2522] Simplify code --- cloud/lxc/lxc_container.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index adb9637acf9..fd5daf1c47c 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -637,9 +637,10 @@ def _get_vars(self, variables): variables.pop(v, None) return_dict = dict() + false_values = [None, ''] + BOOLEANS_FALSE for k, v in variables.items(): _var = self.module.params.get(k) - if not [i for i in [None, ''] + BOOLEANS_FALSE if i == _var]: + if _var not in false_values: return_dict[v] = _var else: return return_dict From 51813e003331c3341b07c5cda33346cada537a3b Mon Sep 17 00:00:00 2001 From: Charles Paul Date: Wed, 9 Dec 2015 16:30:31 -0500 Subject: [PATCH 1018/2522] upped version added, search by vm_name by default --- cloud/vmware/vmware_vm_shell.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cloud/vmware/vmware_vm_shell.py b/cloud/vmware/vmware_vm_shell.py index c8f42202f42..bee7c09d4d3 100644 --- a/cloud/vmware/vmware_vm_shell.py +++ b/cloud/vmware/vmware_vm_shell.py @@ -24,7 +24,7 @@ short_description: Execute a process in VM description: - Start a program in a VM without the need for network connection -version_added: 2.0 +version_added: 2.1 author: "Ritesh Khadgaray (@ritzk)" notes: - Tested on vSphere 5.5 @@ -134,6 +134,10 @@ def find_vm(content, vm_id, vm_id_type="dns_name", datacenter=None): vm = None elif vm_id_type == 'uuid': vm = si.FindByUuid(datacenter=datacenter, uuid=vm_id, vmSearch=True) + elif vm_id_type == 'vm_name': + for machine in get_all_objs(content, [vim.VirtualMachine]): + if machine.name == vm_id: + vm = machine return vm @@ -151,7 +155,7 @@ def main(): argument_spec = vmware_argument_spec() argument_spec.update(dict(datacenter=dict(default=None, type='str'), vm_id=dict(required=True, type='str'), - vm_id_type=dict(default='dns_name', type='str', choices=['inventory_path', 'uuid', 'dns_name']), + vm_id_type=dict(default='vm_name', type='str', choices=['inventory_path', 'uuid', 'dns_name', 'vm_name']), vm_username=dict(required=False, type='str'), vm_password=dict(required=False, type='str', no_log=True), vm_shell=dict(required=True, type='str'), From fed6090e738aec4549668ce8bbb448d2dae95d94 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Thu, 10 Dec 2015 00:51:45 +0100 Subject: [PATCH 1019/2522] Simplify the code and remove use_unsafe_shell=True While there is no security issue with this shell snippet, it is better to not rely on shell and avoid use_unsafe_shell. --- packaging/os/pkgutil.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packaging/os/pkgutil.py b/packaging/os/pkgutil.py index cb674b1453c..e80cdccbae3 100644 --- a/packaging/os/pkgutil.py +++ b/packaging/os/pkgutil.py @@ -76,14 +76,13 @@ def package_latest(module, name, site): # Only supports one package cmd = [ 'pkgutil', '--single', '-c' ] if site is not None: - cmd += [ '-t', pipes.quote(site) ] - cmd.append(pipes.quote(name)) - cmd += [ '| tail -1 | grep -v SAME' ] - rc, out, err = run_command(module, cmd, use_unsafe_shell=True) - if rc == 1: - return True - else: - return False + cmd += [ '-t', site] + cmd.append(name) + rc, out, err = run_command(module, cmd) + # replace | tail -1 |grep -v SAME + # use -2, because splitting on \n create a empty line + # at the end of the list + return 'SAME' in out.split('\n')[-2] def run_command(module, cmd, **kwargs): progname = cmd[0] From 90327ad76bbcc54b7e0b04870fb369be5e7e3fc3 Mon Sep 17 00:00:00 2001 From: twmartin Date: Thu, 10 Dec 2015 09:49:58 -0600 Subject: [PATCH 1020/2522] Correct 'object not iterable' TypeError --- clustering/znode.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/clustering/znode.py b/clustering/znode.py index 51ab51d0ea4..d5913c772b3 100644 --- a/clustering/znode.py +++ b/clustering/znode.py @@ -122,7 +122,7 @@ def main(): command_type = 'op' if 'op' in module.params and module.params['op'] is not None else 'state' method = module.params[command_type] - result, result_dict = command_dict[command_type][method] + result, result_dict = command_dict[command_type][method]() zoo.shutdown() if result: @@ -225,4 +225,3 @@ def _wait(self, path, timeout, interval=5): from ansible.module_utils.basic import * main() - From 7ded482e6c56ef776ad280dcdd274e1d13b71bd9 Mon Sep 17 00:00:00 2001 From: Charles Paul Date: Thu, 10 Dec 2015 15:42:49 -0500 Subject: [PATCH 1021/2522] pep8 whitespace --- cloud/vmware/vmware_vm_shell.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/vmware/vmware_vm_shell.py b/cloud/vmware/vmware_vm_shell.py index bee7c09d4d3..6c81d612f77 100644 --- a/cloud/vmware/vmware_vm_shell.py +++ b/cloud/vmware/vmware_vm_shell.py @@ -135,9 +135,9 @@ def find_vm(content, vm_id, vm_id_type="dns_name", datacenter=None): elif vm_id_type == 'uuid': vm = si.FindByUuid(datacenter=datacenter, uuid=vm_id, vmSearch=True) elif vm_id_type == 'vm_name': - for machine in get_all_objs(content, [vim.VirtualMachine]): - if machine.name == vm_id: - vm = machine + for machine in get_all_objs(content, [vim.VirtualMachine]): + if machine.name == vm_id: + vm = machine return vm From 5437b4a15b4301163062cdd5dffe59b4c6154faa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Wir=C3=A9n?= Date: Thu, 10 Dec 2015 22:53:06 +0100 Subject: [PATCH 1022/2522] Only considers local attributes when comparing state This should fix #1092 --- system/zfs.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/system/zfs.py b/system/zfs.py index 459d9d5582e..fbea120eb04 100644 --- a/system/zfs.py +++ b/system/zfs.py @@ -176,9 +176,7 @@ def set_property(self, prop, value): def set_properties_if_changed(self): current_properties = self.get_current_properties() for prop, value in self.properties.iteritems(): - if prop not in current_properties: - self.module.fail_json(msg="invalid property '%s'" % prop) - if current_properties[prop] != value: + if current_properties.get(prop, None) != value: self.set_property(prop, value) def get_current_properties(self): @@ -188,8 +186,9 @@ def get_current_properties(self): cmd += ['all', self.name] rc, out, err = self.module.run_command(" ".join(cmd)) properties = dict() - for p, v in [l.split('\t')[1:3] for l in out.splitlines()]: - properties[p] = v + for prop, value, source in [l.split('\t')[1:4] for l in out.splitlines()]: + if source == 'local': + properties[prop] = value # Add alias for enhanced sharing properties properties['sharenfs'] = properties.get('share.nfs', None) properties['sharesmb'] = properties.get('share.smb', None) From cd691fad0dc67c010c8a062ab79b4b01dfdc401b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Wir=C3=A9n?= Date: Thu, 10 Dec 2015 23:32:13 +0100 Subject: [PATCH 1023/2522] Removes the createparent property. The least surprising thing is to always create parents --- system/zfs.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/system/zfs.py b/system/zfs.py index fbea120eb04..1d1c48e8ccb 100644 --- a/system/zfs.py +++ b/system/zfs.py @@ -33,7 +33,9 @@ required: true state: description: - - Whether to create (C(present)), or remove (C(absent)) a file system, snapshot or volume. + - Whether to create (C(present)), or remove (C(absent)) a + file system, snapshot or volume. All parents/children + will be created/destroyed as needed to reach the desired state. choices: ['present', 'absent'] required: true origin: @@ -41,11 +43,6 @@ - Snapshot from which to create a clone default: null required: false - createparent: - description: - - Creates all non-existing parent file systems. - required: false - default: "on" key_value: description: - The C(zfs) module takes key=value pairs for zfs properties to be set. See the zfs(8) man page for more information. @@ -119,7 +116,6 @@ def create(self): volsize = properties.pop('volsize', None) volblocksize = properties.pop('volblocksize', None) origin = properties.pop('origin', None) - createparent = self.module.params.get('createparent') cmd = [self.zfs_cmd] if "@" in self.name: @@ -132,8 +128,7 @@ def create(self): cmd.append(action) if action in ['create', 'clone']: - if createparent: - cmd += ['-p'] + cmd += ['-p'] if volsize: cmd += ['-V', volsize] @@ -201,7 +196,6 @@ def main(): argument_spec = dict( name = dict(type='str', required=True), state = dict(type='str', required=True, choices=['present', 'absent']), - createparent = dict(type='bool', required=False, default=True), ), supports_check_mode=True, check_invalid_arguments=False From 98c84c560d6c4979d4a58c457f03f7154598c7bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Wir=C3=A9n?= Date: Fri, 11 Dec 2015 00:24:29 +0100 Subject: [PATCH 1024/2522] Keep, but ignore createparent option --- system/zfs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/system/zfs.py b/system/zfs.py index 1d1c48e8ccb..cf74c5b0b83 100644 --- a/system/zfs.py +++ b/system/zfs.py @@ -196,6 +196,8 @@ def main(): argument_spec = dict( name = dict(type='str', required=True), state = dict(type='str', required=True, choices=['present', 'absent']), + # No longer used. Kept here to not interfere with zfs properties + createparent = dict(type='bool', required=False) ), supports_check_mode=True, check_invalid_arguments=False From 0ee578e938b66e9108a2ba2ce72727716104b21c Mon Sep 17 00:00:00 2001 From: Chrrrles Paul Date: Fri, 11 Dec 2015 09:57:10 -0500 Subject: [PATCH 1025/2522] Changing docs to reflect vm_name as the default vm_id_type --- cloud/vmware/vmware_vm_shell.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cloud/vmware/vmware_vm_shell.py b/cloud/vmware/vmware_vm_shell.py index 6c81d612f77..c33e3e5c71c 100644 --- a/cloud/vmware/vmware_vm_shell.py +++ b/cloud/vmware/vmware_vm_shell.py @@ -58,11 +58,12 @@ vm_id_type: description: - The identification tag for the VM - default: dns_name + default: vm_name choices: - 'uuid' - 'dns_name' - 'inventory_path' + - 'vm_name' required: False vm_username: description: From 5abd7deab02e88e125e5f75abb47a2190f150b0c Mon Sep 17 00:00:00 2001 From: Bruce Pennypacker Date: Fri, 11 Dec 2015 16:06:33 +0000 Subject: [PATCH 1026/2522] Remove extraneous space from end of command string in nagios_cmd --- monitoring/nagios.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/nagios.py b/monitoring/nagios.py index b55a374b34f..5b6e9796b1d 100644 --- a/monitoring/nagios.py +++ b/monitoring/nagios.py @@ -873,7 +873,7 @@ def nagios_cmd(self, cmd): pre = '[%s]' % int(time.time()) post = '\n' - cmdstr = '%s %s %s' % (pre, cmd, post) + cmdstr = '%s %s%s' % (pre, cmd, post) self._write_command(cmdstr) def act(self): From 1b0250125aabac80b84c6200d272ba6796f329bb Mon Sep 17 00:00:00 2001 From: Rob Date: Sat, 12 Dec 2015 11:33:10 +0000 Subject: [PATCH 1027/2522] Add secondary addresses to facts --- cloud/amazon/ec2_eni_facts.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cloud/amazon/ec2_eni_facts.py b/cloud/amazon/ec2_eni_facts.py index e95a6ea1029..da4ebef3d4d 100644 --- a/cloud/amazon/ec2_eni_facts.py +++ b/cloud/amazon/ec2_eni_facts.py @@ -55,6 +55,11 @@ def get_eni_info(interface): + # Private addresses + private_addresses = [] + for ip in interface.private_ip_addresses: + private_addresses.append({ 'private_ip_address': ip.private_ip_address, 'primary_address': ip.primary }) + interface_info = {'id': interface.id, 'subnet_id': interface.subnet_id, 'vpc_id': interface.vpc_id, @@ -65,6 +70,7 @@ def get_eni_info(interface): 'private_ip_address': interface.private_ip_address, 'source_dest_check': interface.source_dest_check, 'groups': dict((group.id, group.name) for group in interface.groups), + 'private_ip_addresses': private_addresses } if interface.attachment is not None: From cc7c929725a816f9e8d73d0bf4850d45d1667676 Mon Sep 17 00:00:00 2001 From: Greg Hurrell Date: Sat, 12 Dec 2015 09:19:32 -0800 Subject: [PATCH 1028/2522] osx_defaults: add "host" attribute This allows us to configure defaults using the `-currentHost` or `-host` arguments to the `defaults` executable. --- system/osx_defaults.py | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/system/osx_defaults.py b/system/osx_defaults.py index e4dc5f8c750..a7a46b749d9 100644 --- a/system/osx_defaults.py +++ b/system/osx_defaults.py @@ -33,6 +33,12 @@ - The domain is a domain name of the form com.companyname.appname. required: false default: NSGlobalDomain + host: + description: + - The host on which the preference should apply. The special value "currentHost" corresponds to the + "-currentHost" switch of the defaults commandline tool. + required: false + default: null key: description: - The key of the user preference @@ -67,6 +73,7 @@ EXAMPLES = ''' - osx_defaults: domain=com.apple.Safari key=IncludeInternalDebugMenu type=bool value=true state=present - osx_defaults: domain=NSGlobalDomain key=AppleMeasurementUnits type=string value=Centimeters state=present +- osx_defaults: domain=com.apple.screensaver host=currentHost key=showClock type=int value=1 - osx_defaults: key=AppleMeasurementUnits type=string value=Centimeters - osx_defaults: key: AppleLanguages @@ -153,6 +160,19 @@ def _convert_type(self, type, value): raise OSXDefaultsException('Type is not supported: {0}'.format(type)) + """ Returns a normalized list of commandline arguments based on the "host" attribute """ + def _host_args(self): + if self.host is None: + return [] + elif self.host == 'currentHost': + return ['-currentHost'] + else: + return ['-host', self.host] + + """ Returns a list containing the "defaults" executable and any common base arguments """ + def _base_command(self): + return [self.executable] + self._host_args() + """ Converts array output from defaults to an list """ @staticmethod def _convert_defaults_str_to_list(value): @@ -174,7 +194,7 @@ def _convert_defaults_str_to_list(value): """ Reads value of this domain & key from defaults """ def read(self): # First try to find out the type - rc, out, err = self.module.run_command([self.executable, "read-type", self.domain, self.key]) + rc, out, err = self.module.run_command(self._base_command() + ["read-type", self.domain, self.key]) # If RC is 1, the key does not exists if rc == 1: @@ -188,7 +208,7 @@ def read(self): type = out.strip().replace('Type is ', '') # Now get the current value - rc, out, err = self.module.run_command([self.executable, "read", self.domain, self.key]) + rc, out, err = self.module.run_command(self._base_command() + ["read", self.domain, self.key]) # Strip output out = out.strip() @@ -230,14 +250,14 @@ def write(self): if not isinstance(value, list): value = [value] - rc, out, err = self.module.run_command([self.executable, 'write', self.domain, self.key, '-' + self.type] + value) + rc, out, err = self.module.run_command(self._base_command() + ['write', self.domain, self.key, '-' + self.type] + value) if rc != 0: raise OSXDefaultsException('An error occurred while writing value to defaults: ' + out) """ Deletes defaults key from domain """ def delete(self): - rc, out, err = self.module.run_command([self.executable, 'delete', self.domain, self.key]) + rc, out, err = self.module.run_command(self._base_command() + ['delete', self.domain, self.key]) if rc != 0: raise OSXDefaultsException("An error occurred while deleting key from defaults: " + out) @@ -289,6 +309,10 @@ def main(): default="NSGlobalDomain", required=False, ), + host=dict( + default=None, + required=False, + ), key=dict( default=None, ), @@ -331,6 +355,7 @@ def main(): ) domain = module.params['domain'] + host = module.params['host'] key = module.params['key'] type = module.params['type'] array_add = module.params['array_add'] @@ -339,7 +364,7 @@ def main(): path = module.params['path'] try: - defaults = OSXDefaults(module=module, domain=domain, key=key, type=type, + defaults = OSXDefaults(module=module, domain=domain, host=host, key=key, type=type, array_add=array_add, value=value, state=state, path=path) changed = defaults.run() module.exit_json(changed=changed) From c157dbe55ab127de3ef307324157d6c840abff4a Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Sun, 13 Dec 2015 01:13:47 -0500 Subject: [PATCH 1029/2522] Fix up vsphere_copy after open_url change * Remove leading module parameter on open_url call as it's no longer used by module_utils.urls.open_url * Force basic auth otherwise vsphere will just return a 401 --- cloud/vmware/vsphere_copy.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cloud/vmware/vsphere_copy.py b/cloud/vmware/vsphere_copy.py index 2f3928575f2..77f147f17a8 100644 --- a/cloud/vmware/vsphere_copy.py +++ b/cloud/vmware/vsphere_copy.py @@ -137,8 +137,9 @@ def main(): } try: - r = open_url(module, url, data=data, headers=headers, method='PUT', - url_username=login, url_password=password, validate_certs=validate_certs) + r = open_url(url, data=data, headers=headers, method='PUT', + url_username=login, url_password=password, validate_certs=validate_certs, + force_basic_auth=True) except socket.error, e: if isinstance(e.args, tuple) and e[0] == errno.ECONNRESET: # VSphere resets connection if the file is in use and cannot be replaced From a6ceec998dab1c7b4cf754219c372139c1292e2c Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 14 Dec 2015 21:47:26 -0500 Subject: [PATCH 1030/2522] corrected version_added for new monit timeout opt --- monitoring/monit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/monit.py b/monitoring/monit.py index 35a386b6c6e..dda3f82d486 100644 --- a/monitoring/monit.py +++ b/monitoring/monit.py @@ -46,7 +46,7 @@ Ansible will sleep for five seconds between each check. required: false default: 300 - version_added: "2.0" + version_added: "2.1" requirements: [ ] author: "Darryl Stoflet (@dstoflet)" ''' From 0f5faf976a7b717188835511d853d96352d57c62 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 14 Dec 2015 21:56:50 -0500 Subject: [PATCH 1031/2522] updated version version_added to 2.1 --- packaging/language/cpanm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/language/cpanm.py b/packaging/language/cpanm.py index e79f155bca9..1e779387635 100644 --- a/packaging/language/cpanm.py +++ b/packaging/language/cpanm.py @@ -69,7 +69,7 @@ - minimum version of perl module to consider acceptable required: false default: false - version_added: "2.0" + version_added: "2.1" system_lib: description: - Use this if you want to install modules to the system perl include path. You must be root or have "passwordless" sudo for this to work. From 892cf445dd14fbc74e8f7199fa49c2bbc9400e81 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 14 Dec 2015 23:09:06 -0500 Subject: [PATCH 1032/2522] added missing version_added to new options --- windows/win_chocolatey.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/windows/win_chocolatey.py b/windows/win_chocolatey.py index b9a54fddcbf..4fd512d1fbe 100644 --- a/windows/win_chocolatey.py +++ b/windows/win_chocolatey.py @@ -73,16 +73,19 @@ - Arguments to pass to the native installer require: false default: null + version_added: '2.1' params: description: - Parameters to pass to the package require: false default: null + version_added: '2.1' ignore_dependencies: description: - Ignore dependencies, only install/upgrade the package itself require: false default: false + version_added: '2.1' author: "Trond Hindenes (@trondhindenes), Peter Mounce (@petemounce), Pepe Barbe (@elventear), Adam Keech (@smadam813)" ''' From 6912ca0acaed0d738d8dd9867721d2ff0094084a Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 25 Nov 2015 07:44:32 -0500 Subject: [PATCH 1033/2522] Sync minor fixes from OpenStack Infra Infra has been keeping a local copy of this waiting for ansible 2 to release. In getting ready for ansible 2 (and our ability to delete our local copy of the file, I noticed we had a couple of minor cleanups. Also, the timeout command is there to improve life and workaround puppet deficiencies. However, it's not working around deficiencies on systems that do not have the timeout command if we blindly use it. The puppet specific timeout options are more complex and out of scope of this. Issue: #1273 --- system/puppet.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/system/puppet.py b/system/puppet.py index 3a3fb6e3544..98b09bb3f90 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -19,6 +19,11 @@ import pipes import stat +try: + import json +except ImportError: + import simplejson as json + DOCUMENTATION = ''' --- module: puppet @@ -38,13 +43,15 @@ required: false default: None manifest: - desciption: + description: - Path to the manifest file to run puppet apply on. required: false default: None show_diff: description: - - Should puppet return diffs of changes applied. Defaults to off to avoid leaking secret changes by default. + - > + Should puppet return diffs of changes applied. Defaults to off to + avoid leaking secret changes by default. required: false default: no choices: [ "yes", "no" ] @@ -127,6 +134,9 @@ def main(): module.fail_json( msg="Could not find puppet. Please ensure it is installed.") + global TIMEOUT_CMD + TIMEOUT_CMD = module.get_bin_path("timeout", False) + if p['manifest']: if not os.path.exists(p['manifest']): module.fail_json( @@ -139,7 +149,8 @@ def main(): PUPPET_CMD + " config print agent_disabled_lockfile") if os.path.exists(stdout.strip()): module.fail_json( - msg="Puppet agent is administratively disabled.", disabled=True) + msg="Puppet agent is administratively disabled.", + disabled=True) elif rc != 0: module.fail_json( msg="Puppet agent state could not be determined.") @@ -150,19 +161,24 @@ def main(): module.params['facter_basename'], module.params['facts']) - base_cmd = "timeout -s 9 %(timeout)s %(puppet_cmd)s" % dict( - timeout=pipes.quote(p['timeout']), puppet_cmd=PUPPET_CMD) + if TIMEOUT_CMD: + base_cmd = "%(timeout_cmd)s -s 9 %(timeout)s %(puppet_cmd)s" % dict( + timeout_cmd=TIMEOUT_CMD, + timeout=pipes.quote(p['timeout']), + puppet_cmd=PUPPET_CMD) + else: + base_cmd = PUPPET_CMD if not p['manifest']: cmd = ("%(base_cmd)s agent --onetime" - " --ignorecache --no-daemonize --no-usecacheonfailure --no-splay" - " --detailed-exitcodes --verbose") % dict( + " --ignorecache --no-daemonize --no-usecacheonfailure" + " --no-splay --detailed-exitcodes --verbose") % dict( base_cmd=base_cmd, ) if p['puppetmaster']: cmd += " --server %s" % pipes.quote(p['puppetmaster']) if p['show_diff']: - cmd += " --show_diff" + cmd += " --show-diff" if p['environment']: cmd += " --environment '%s'" % p['environment'] if module.check_mode: From a0a19e16ff1e5fbc45c9c85e9ab8791aecbaa37c Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Mon, 14 Dec 2015 11:13:10 +0100 Subject: [PATCH 1034/2522] cloudstack: cs_instance: fixes and improvements - cs_instance: fix VM not updated with states given stopped, started, restarted A missing VM will be created though but an existing not updated. This fixes the lack of consistency. - cs_instance: fix user data can not be cleared - cs_instance: fix deleted VM not recovered on state=present --- cloud/cloudstack/cs_instance.py | 172 ++++++++++++++++++-------------- 1 file changed, 97 insertions(+), 75 deletions(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 22d3d815982..852be68a347 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -221,7 +221,7 @@ - local_action: module: cs_instance name: web-vm-1 - display_name: web-vm-01.example.com + display_name: web-vm-01.example.com iso: Linux Debian 7 64-bit service_offering: 2cpu_2gb force: yes @@ -247,13 +247,13 @@ - {'network': NetworkA, 'ip': '10.1.1.1'} - {'network': NetworkB, 'ip': '192.168.1.1'} -# Ensure a instance has stopped +# Ensure an instance is stopped - local_action: cs_instance name=web-vm-1 state=stopped -# Ensure a instance is running +# Ensure an instance is running - local_action: cs_instance name=web-vm-1 state=started -# Remove a instance +# Remove an instance - local_action: cs_instance name=web-vm-1 state=absent ''' @@ -544,23 +544,27 @@ def get_network_ids(self, network_names=None): return network_ids - def present_instance(self): + def present_instance(self, start_vm=True): instance = self.get_instance() + if not instance: - instance = self.deploy_instance() + instance = self.deploy_instance(start_vm=start_vm) else: - instance = self.update_instance(instance) + instance = self.recover_instance(instance=instance) + instance = self.update_instance(instance=instance, start_vm=start_vm) # In check mode, we do not necessarely have an instance if instance: instance = self.ensure_tags(resource=instance, resource_type='UserVm') + # refresh instance data + self.instance = instance return instance def get_user_data(self): user_data = self.module.params.get('user_data') - if user_data: + if user_data is not None: user_data = base64.b64encode(str(user_data)) return user_data @@ -630,30 +634,43 @@ def deploy_instance(self, start_vm=True): return instance - def update_instance(self, instance): - args_service_offering = {} - args_service_offering['id'] = instance['id'] - args_service_offering['serviceofferingid'] = self.get_service_offering_id() + def update_instance(self, instance, start_vm=True): + # Service offering data + args_service_offering = {} + args_service_offering['id'] = instance['id'] + if self.module.params.get('service_offering'): + args_service_offering['serviceofferingid'] = self.get_service_offering_id() + + # Instance data + args_instance_update = {} + args_instance_update['id'] = instance['id'] + args_instance_update['userdata'] = self.get_user_data() + args_instance_update['ostypeid'] = self.get_os_type(key='id') + if self.module.params.get('group'): + args_instance_update['group'] = self.module.params.get('group') + if self.module.params.get('display_name'): + args_instance_update['displayname'] = self.module.params.get('display_name') + + # SSH key data + args_ssh_key = {} + args_ssh_key['id'] = instance['id'] + args_ssh_key['projectid'] = self.get_project(key='id') + if self.module.params.get('ssh_key'): + args_ssh_key['keypair'] = self.module.params.get('ssh_key') + + # SSH key data + args_ssh_key = {} + args_ssh_key['id'] = instance['id'] + args_ssh_key['projectid'] = self.get_project(key='id') + if self.module.params.get('ssh_key'): + args_ssh_key['keypair'] = self.module.params.get('ssh_key') - args_instance_update = {} - args_instance_update['id'] = instance['id'] - args_instance_update['group'] = self.module.params.get('group') - args_instance_update['displayname'] = self.get_or_fallback('display_name', 'name') - args_instance_update['userdata'] = self.get_user_data() - args_instance_update['ostypeid'] = self.get_os_type(key='id') - - args_ssh_key = {} - args_ssh_key['id'] = instance['id'] - args_ssh_key['keypair'] = self.module.params.get('ssh_key') - args_ssh_key['projectid'] = self.get_project(key='id') - if self._has_changed(args_service_offering, instance) or \ self._has_changed(args_instance_update, instance) or \ self._has_changed(args_ssh_key, instance): - + force = self.module.params.get('force') instance_state = instance['state'].lower() - if instance_state == 'stopped' or force: self.result['changed'] = True if not self.module.check_mode: @@ -689,11 +706,22 @@ def update_instance(self, instance): self.instance = instance # Start VM again if it was running before - if instance_state == 'running': + if instance_state == 'running' and start_vm: instance = self.start_instance() return instance + def recover_instance(self, instance): + if instance['state'].lower() in [ 'destroying', 'destroyed' ]: + self.result['changed'] = True + if not self.module.check_mode: + res = self.cs.recoverVirtualMachine(id=instance['id']) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + instance = res['virtualmachine'] + return instance + + def absent_instance(self): instance = self.get_instance() if instance: @@ -736,73 +764,64 @@ def expunge_instance(self): def stop_instance(self): instance = self.get_instance() + # in check mode intance may not be instanciated + if instance: + if instance['state'].lower() in ['stopping', 'stopped']: + return instance - if not instance: - instance = self.deploy_instance(start_vm=False) - return instance - - elif instance['state'].lower() in ['stopping', 'stopped']: - return instance - - if instance['state'].lower() in ['starting', 'running']: - self.result['changed'] = True - if not self.module.check_mode: - instance = self.cs.stopVirtualMachine(id=instance['id']) + if instance['state'].lower() in ['starting', 'running']: + self.result['changed'] = True + if not self.module.check_mode: + instance = self.cs.stopVirtualMachine(id=instance['id']) - if 'errortext' in instance: - self.module.fail_json(msg="Failed: '%s'" % instance['errortext']) + if 'errortext' in instance: + self.module.fail_json(msg="Failed: '%s'" % instance['errortext']) - poll_async = self.module.params.get('poll_async') - if poll_async: - instance = self._poll_job(instance, 'virtualmachine') + poll_async = self.module.params.get('poll_async') + if poll_async: + instance = self._poll_job(instance, 'virtualmachine') return instance def start_instance(self): instance = self.get_instance() + # in check mode intance may not be instanciated + if instance: + if instance['state'].lower() in ['starting', 'running']: + return instance - if not instance: - instance = self.deploy_instance() - return instance - - elif instance['state'].lower() in ['starting', 'running']: - return instance - - if instance['state'].lower() in ['stopped', 'stopping']: - self.result['changed'] = True - if not self.module.check_mode: - instance = self.cs.startVirtualMachine(id=instance['id']) + if instance['state'].lower() in ['stopped', 'stopping']: + self.result['changed'] = True + if not self.module.check_mode: + instance = self.cs.startVirtualMachine(id=instance['id']) - if 'errortext' in instance: - self.module.fail_json(msg="Failed: '%s'" % instance['errortext']) + if 'errortext' in instance: + self.module.fail_json(msg="Failed: '%s'" % instance['errortext']) - poll_async = self.module.params.get('poll_async') - if poll_async: - instance = self._poll_job(instance, 'virtualmachine') + poll_async = self.module.params.get('poll_async') + if poll_async: + instance = self._poll_job(instance, 'virtualmachine') return instance def restart_instance(self): instance = self.get_instance() + # in check mode intance may not be instanciated + if instance: + if instance['state'].lower() in [ 'running', 'starting' ]: + self.result['changed'] = True + if not self.module.check_mode: + instance = self.cs.rebootVirtualMachine(id=instance['id']) - if not instance: - instance = self.deploy_instance() - return instance - - elif instance['state'].lower() in [ 'running', 'starting' ]: - self.result['changed'] = True - if not self.module.check_mode: - instance = self.cs.rebootVirtualMachine(id=instance['id']) - - if 'errortext' in instance: - self.module.fail_json(msg="Failed: '%s'" % instance['errortext']) + if 'errortext' in instance: + self.module.fail_json(msg="Failed: '%s'" % instance['errortext']) - poll_async = self.module.params.get('poll_async') - if poll_async: - instance = self._poll_job(instance, 'virtualmachine') + poll_async = self.module.params.get('poll_async') + if poll_async: + instance = self._poll_job(instance, 'virtualmachine') - elif instance['state'].lower() in [ 'stopping', 'stopped' ]: - instance = self.start_instance() + elif instance['state'].lower() in [ 'stopping', 'stopped' ]: + instance = self.start_instance() return instance @@ -918,12 +937,15 @@ def main(): instance = acs_instance.present_instance() elif state in ['stopped']: + acs_instance.present_instance(start_vm=False) instance = acs_instance.stop_instance() elif state in ['started']: + acs_instance.present_instance() instance = acs_instance.start_instance() elif state in ['restarted']: + acs_instance.present_instance() instance = acs_instance.restart_instance() if instance and 'state' in instance and instance['state'].lower() == 'error': From 641a347d96fe05a2d03e486c5c1313d741153854 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 15 Dec 2015 09:17:48 -0500 Subject: [PATCH 1035/2522] fix error with misArg not being declared also fixed test to work on empty string or not for error reporting --- windows/win_firewall_rule.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/windows/win_firewall_rule.ps1 b/windows/win_firewall_rule.ps1 index 63ada997456..33ac7f3fbbd 100644 --- a/windows/win_firewall_rule.ps1 +++ b/windows/win_firewall_rule.ps1 @@ -205,6 +205,7 @@ $direction=Get-Attr $params "direction" ""; $force=Get-Attr $params "force" $false; $action=Get-Attr $params "action" ""; +$misArg = '' # Check the arguments if ($enable -ne $null) { if ($enable -eq $true) { @@ -262,7 +263,7 @@ foreach ($arg in $args){ $winprofile=Get-Attr $params "profile" "current"; $fwsettings.Add("profile", $winprofile) -if ($($($misArg|measure).count) -gt 0){ +if ($misArg){ $result=New-Object psobject @{ changed=$false failed=$true From 83f4f162357729ddbee81557fb6cc938092d8495 Mon Sep 17 00:00:00 2001 From: Jonathan Mainguy Date: Fri, 23 Oct 2015 13:19:10 -0400 Subject: [PATCH 1036/2522] Enable stdout and stderr on sucessful runs, making show_diff useable omit color symbols as ansible makes them illegible --- system/puppet.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/puppet.py b/system/puppet.py index 98b09bb3f90..5bddfd49283 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -171,8 +171,8 @@ def main(): if not p['manifest']: cmd = ("%(base_cmd)s agent --onetime" - " --ignorecache --no-daemonize --no-usecacheonfailure" - " --no-splay --detailed-exitcodes --verbose") % dict( + " --ignorecache --no-daemonize --no-usecacheonfailure --no-splay" + " --detailed-exitcodes --verbose --color 0") % dict( base_cmd=base_cmd, ) if p['puppetmaster']: @@ -198,7 +198,7 @@ def main(): if rc == 0: # success - module.exit_json(rc=rc, changed=False, stdout=stdout) + module.exit_json(rc=rc, changed=False, stdout=stdout, stderr=stderr) elif rc == 1: # rc==1 could be because it's disabled # rc==1 could also mean there was a compilation failure From d738eb2fed659af8d886ae975d6828d609b0c16a Mon Sep 17 00:00:00 2001 From: Zach Abrahamson Date: Tue, 15 Dec 2015 21:11:41 -0500 Subject: [PATCH 1037/2522] Wrapping room parameter in a string in case of using room IDs --- notification/hipchat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notification/hipchat.py b/notification/hipchat.py index f565ca9cdfc..2ff40be3f24 100644 --- a/notification/hipchat.py +++ b/notification/hipchat.py @@ -184,7 +184,7 @@ def main(): ) token = module.params["token"] - room = module.params["room"] + room = str(module.params["room"]) msg = module.params["msg"] msg_from = module.params["msg_from"] color = module.params["color"] From 822eb97ec8baa7e98cba2dd9b6b0b8e38a6d687c Mon Sep 17 00:00:00 2001 From: Robert Lu Date: Sat, 5 Dec 2015 19:03:51 +0800 Subject: [PATCH 1038/2522] update current_link without downtime fix support of check mode fix check module when needn't update current link --- web_infrastructure/deploy_helper.py | 39 ++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/web_infrastructure/deploy_helper.py b/web_infrastructure/deploy_helper.py index beec56419e9..6eefbd1f891 100644 --- a/web_infrastructure/deploy_helper.py +++ b/web_infrastructure/deploy_helper.py @@ -308,12 +308,27 @@ def check_link(self, path): self.module.fail_json(msg="%s exists but is not a symbolic link" % path) def create_link(self, source, link_name): - if not self.module.check_mode: - if os.path.islink(link_name): - os.unlink(link_name) - os.symlink(source, link_name) + changed = False - return True + if os.path.islink(link_name): + norm_link = os.path.normpath(os.path.realpath(link_name)) + norm_source = os.path.normpath(os.path.realpath(source)) + if norm_link == norm_source: + changed = False + else: + changed = True + if not self.module.check_mode: + tmp_link_name = link_name + '.' + self.unfinished_filename + if os.path.islink(tmp_link_name): + os.unlink(tmp_link_name) + os.symlink(source, tmp_link_name) + os.rename(tmp_link_name, link_name) + else: + changed = True + if not self.module.check_mode: + os.symlink(source, link_name) + + return changed def remove_unfinished_file(self, new_release_path): changed = False @@ -329,7 +344,7 @@ def remove_unfinished_builds(self, releases_path): changes = 0 for release in os.listdir(releases_path): - if (os.path.isfile(os.path.join(releases_path, release, self.unfinished_filename))): + if os.path.isfile(os.path.join(releases_path, release, self.unfinished_filename)): if self.module.check_mode: changes += 1 else: @@ -337,6 +352,16 @@ def remove_unfinished_builds(self, releases_path): return changes + def remove_unfinished_link(self, path): + changed = False + + tmp_link_name = os.path.join(path, self.release + '.' + self.unfinished_filename) + if not self.module.check_mode and os.path.exists(tmp_link_name): + changed = True + os.remove(tmp_link_name) + + return changed + def cleanup(self, releases_path): changes = 0 @@ -415,10 +440,12 @@ def main(): changes += deploy_helper.remove_unfinished_file(facts['new_release_path']) changes += deploy_helper.create_link(facts['new_release_path'], facts['current_path']) if deploy_helper.clean: + changes += deploy_helper.remove_unfinished_link(facts['path']) changes += deploy_helper.remove_unfinished_builds(facts['releases_path']) changes += deploy_helper.cleanup(facts['releases_path']) elif deploy_helper.state == 'clean': + changes += deploy_helper.remove_unfinished_link(facts['path']) changes += deploy_helper.remove_unfinished_builds(facts['releases_path']) changes += deploy_helper.cleanup(facts['releases_path']) From 7b857b073ee56023361efe4a57257e5f8547bf3a Mon Sep 17 00:00:00 2001 From: Jonathan Mainguy Date: Tue, 15 Dec 2015 21:45:11 -0500 Subject: [PATCH 1039/2522] add snapshot feature to lvol --- system/lvol.py | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/system/lvol.py b/system/lvol.py index 2d588171a91..284c37ace5e 100644 --- a/system/lvol.py +++ b/system/lvol.py @@ -62,6 +62,11 @@ version_added: "2.0" description: - Free-form options to be passed to the lvcreate command + snapshot: + version_added: "2.1" + description: + - The name of the snapshot volume + required: false notes: - Filesystems on top of the volume are not resized. ''' @@ -87,6 +92,9 @@ # Remove the logical volume. - lvol: vg=firefly lv=test state=absent force=yes + +# Create a snapshot volume of the test logical volume. +- lvol: vg=firefly lv=test snapshot=snap1 size=100m ''' import re @@ -128,6 +136,7 @@ def main(): opts=dict(type='str'), state=dict(choices=["absent", "present"], default='present'), force=dict(type='bool', default='no'), + snapshot=dict(type='str', default=None), ), supports_check_mode=True, ) @@ -150,6 +159,7 @@ def main(): force = module.boolean(module.params['force']) size_opt = 'L' size_unit = 'm' + snapshot = module.params['snapshot'] if opts is None: opts = "" @@ -201,8 +211,12 @@ def main(): lvs = parse_lvs(current_lvs) + if snapshot is None: + check_lv = lv + else: + check_lv = snapshot for test_lv in lvs: - if test_lv['name'] == lv: + if test_lv['name'] == check_lv: this_lv = test_lv break else: @@ -222,7 +236,10 @@ def main(): changed = True else: lvcreate_cmd = module.get_bin_path("lvcreate", required=True) - cmd = "%s %s -n %s -%s %s%s %s %s" % (lvcreate_cmd, yesopt, lv, size_opt, size, size_unit, opts, vg) + if snapshot is not None: + cmd = "%s %s -%s %s%s -s -n %s %s %s/%s" % (lvcreate_cmd, yesopt, size_opt, size, size_unit, snapshot, opts, vg, lv) + else: + cmd = "%s %s -n %s -%s %s%s %s %s" % (lvcreate_cmd, yesopt, lv, size_opt, size, size_unit, opts, vg) rc, _, err = module.run_command(cmd) if rc == 0: changed = True @@ -247,9 +264,9 @@ def main(): else: ### resize LV tool = None - if size > this_lv['size']: + if int(size) > this_lv['size']: tool = module.get_bin_path("lvextend", required=True) - elif size < this_lv['size']: + elif int(size) < this_lv['size']: if not force: module.fail_json(msg="Sorry, no shrinking of %s without force=yes." % (this_lv['name'])) tool = module.get_bin_path("lvreduce", required=True) @@ -259,8 +276,11 @@ def main(): if module.check_mode: changed = True else: - rc, _, err = module.run_command("%s -%s %s%s %s/%s" % (tool, size_opt, size, size_unit, vg, this_lv['name'])) - if rc == 0: + cmd = "%s -%s %s%s %s/%s" % (tool, size_opt, size, size_unit, vg, this_lv['name']) + rc, out, err = module.run_command(cmd) + if "Reached maximum COW size" in out: + module.fail_json(msg="Unable to resize %s to %s%s" % (lv, size, size_unit), rc=rc, err=err, out=out) + elif rc == 0: changed = True elif "matches existing size" in err: module.exit_json(changed=False, vg=vg, lv=this_lv['name'], size=this_lv['size']) From f6296fec6fe88abcd525d387f8039d843c41bd08 Mon Sep 17 00:00:00 2001 From: Yoshinori Teraoka Date: Wed, 16 Dec 2015 12:18:18 +0900 Subject: [PATCH 1040/2522] add executable parameter to cpanm module like other packaging modules (pip, gem) --- packaging/language/cpanm.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packaging/language/cpanm.py b/packaging/language/cpanm.py index 1e779387635..a017aaf5791 100644 --- a/packaging/language/cpanm.py +++ b/packaging/language/cpanm.py @@ -78,6 +78,12 @@ default: false version_added: "2.0" aliases: ['use_sudo'] + executable: + description: + - Override the path to the cpanm executable + required: false + default: null + version_added: "2.1" notes: - Please note that U(http://search.cpan.org/dist/App-cpanminus/bin/cpanm, cpanm) must be installed on the remote host. author: "Franck Cuny (@franckcuny)" @@ -154,6 +160,13 @@ def _build_cmd_line(name, from_path, notest, locallib, mirror, mirror_only, inst return cmd +def _get_cpanm_path(module): + if module.params['executable']: + return module.params['executable'] + else: + return module.get_bin_path('cpanm', True) + + def main(): arg_spec = dict( name=dict(default=None, required=False, aliases=['pkg']), @@ -165,6 +178,7 @@ def main(): installdeps=dict(default=False, type='bool'), system_lib=dict(default=False, type='bool', aliases=['use_sudo']), version=dict(default=None, required=False), + executable=dict(required=False, type='str'), ) module = AnsibleModule( @@ -172,7 +186,7 @@ def main(): required_one_of=[['name', 'from_path']], ) - cpanm = module.get_bin_path('cpanm', True) + cpanm = _get_cpanm_path(module) name = module.params['name'] from_path = module.params['from_path'] notest = module.boolean(module.params.get('notest', False)) From f0e0be0b21fdfa4a7492b234aa6459272a0ded48 Mon Sep 17 00:00:00 2001 From: Marc Mettke Date: Wed, 16 Dec 2015 07:03:18 +0100 Subject: [PATCH 1041/2522] Pacman Module Fixes Update: query_package documentation Fix: Number of Packages to Updated was one to high, 'cause of counting the '\n' Fix: Pacman was reinstalling state=latest packages, even when it was unable to load the remote version --- packaging/os/pacman.py | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/packaging/os/pacman.py b/packaging/os/pacman.py index da85a6c0a1f..1f955fa269e 100644 --- a/packaging/os/pacman.py +++ b/packaging/os/pacman.py @@ -124,13 +124,13 @@ def get_version(pacman_output): return None def query_package(module, pacman_path, name, state="present"): - """Query the package status in both the local system and the repository. Returns a boolean to indicate if the package is installed, and a second boolean to indicate if the package is up-to-date.""" + """Query the package status in both the local system and the repository. Returns a boolean to indicate if the package is installed, a second boolean to indicate if the package is up-to-date and a third boolean to indicate whether online information were available""" if state == "present": lcmd = "%s -Qi %s" % (pacman_path, name) lrc, lstdout, lstderr = module.run_command(lcmd, check_rc=False) if lrc != 0: # package is not installed locally - return False, False + return False, False, False # get the version installed locally (if any) lversion = get_version(lstdout) @@ -143,9 +143,10 @@ def query_package(module, pacman_path, name, state="present"): if rrc == 0: # Return True to indicate that the package is installed locally, and the result of the version number comparison # to determine if the package is up-to-date. - return True, (lversion == rversion) - - return False, False + return True, (lversion == rversion), False + + # package is installed but cannot fetch remote Version. Last True stands for the error + return True, True, True def update_package_db(module, pacman_path): @@ -165,7 +166,7 @@ def upgrade(module, pacman_path): if rc == 0: if module.check_mode: data = stdout.split('\n') - module.exit_json(changed=True, msg="%s package(s) would be upgraded" % len(data)) + module.exit_json(changed=True, msg="%s package(s) would be upgraded" % (len(data) - 1)) rc, stdout, stderr = module.run_command(cmdupgrade, check_rc=False) if rc == 0: module.exit_json(changed=True, msg='System upgraded') @@ -190,7 +191,7 @@ def remove_packages(module, pacman_path, packages): # Using a for loop incase of error, we can report the package that failed for package in packages: # Query the package first, to see if we even need to remove - installed, updated = query_package(module, pacman_path, package) + installed, updated, unknown = query_package(module, pacman_path, package) if not installed: continue @@ -211,10 +212,15 @@ def remove_packages(module, pacman_path, packages): def install_packages(module, pacman_path, state, packages, package_files): install_c = 0 + package_err = [] + message = "" for i, package in enumerate(packages): # if the package is installed and state == present or state == latest and is up-to-date then skip - installed, updated = query_package(module, pacman_path, package) + installed, updated, latestError = query_package(module, pacman_path, package) + if latestError and state == 'latest': + package_err.append(package) + if installed and (state == 'present' or (state == 'latest' and updated)): continue @@ -230,17 +236,19 @@ def install_packages(module, pacman_path, state, packages, package_files): module.fail_json(msg="failed to install %s" % (package)) install_c += 1 - + + if state == 'latest' and len(package_err) > 0: + message = "But could not ensure 'latest' state for %s package(s) as remote version could not be fetched." % (package_err) + if install_c > 0: - module.exit_json(changed=True, msg="installed %s package(s)" % (install_c)) - - module.exit_json(changed=False, msg="package(s) already installed") - - + module.exit_json(changed=True, msg="installed %s package(s). %s" % (install_c, message)) + + module.exit_json(changed=False, msg="package(s) already installed. %s" % (message)) + def check_packages(module, pacman_path, packages, state): would_be_changed = [] for package in packages: - installed, updated = query_package(module, pacman_path, package) + installed, updated, unknown = query_package(module, pacman_path, package) if ((state in ["present", "latest"] and not installed) or (state == "absent" and installed) or (state == "latest" and not updated)): From 8d05d4e9095cc3e6389fdfefb0cf6d438513b0d4 Mon Sep 17 00:00:00 2001 From: Robert Lu Date: Wed, 16 Dec 2015 14:49:23 +0800 Subject: [PATCH 1042/2522] check current version's existence (fire or cleanup) * reserve current version when cleanup * verify existence before fire a new version * update doc of deploy_helper --- web_infrastructure/deploy_helper.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/web_infrastructure/deploy_helper.py b/web_infrastructure/deploy_helper.py index 6eefbd1f891..21a98765c9e 100644 --- a/web_infrastructure/deploy_helper.py +++ b/web_infrastructure/deploy_helper.py @@ -110,7 +110,7 @@ default: 5 description: - the number of old releases to keep when cleaning. Used in C(finalize) and C(clean). Any unfinished builds - will be deleted first, so only correct releases will count. + will be deleted first, so only correct releases will count. The current version will not count. notes: - Facts are only returned for C(state=query) and C(state=present). If you use both, you should pass any overridden @@ -318,6 +318,8 @@ def create_link(self, source, link_name): else: changed = True if not self.module.check_mode: + if not os.path.lexists(source): + self.module.fail_json(msg="the symlink target %s doesn't exists" % source) tmp_link_name = link_name + '.' + self.unfinished_filename if os.path.islink(tmp_link_name): os.unlink(tmp_link_name) @@ -362,11 +364,15 @@ def remove_unfinished_link(self, path): return changed - def cleanup(self, releases_path): + def cleanup(self, releases_path, reserve_version): changes = 0 if os.path.lexists(releases_path): releases = [ f for f in os.listdir(releases_path) if os.path.isdir(os.path.join(releases_path,f)) ] + try: + releases.remove(reserve_version) + except ValueError: + pass if not self.module.check_mode: releases.sort( key=lambda x: os.path.getctime(os.path.join(releases_path,x)), reverse=True) @@ -442,12 +448,12 @@ def main(): if deploy_helper.clean: changes += deploy_helper.remove_unfinished_link(facts['path']) changes += deploy_helper.remove_unfinished_builds(facts['releases_path']) - changes += deploy_helper.cleanup(facts['releases_path']) + changes += deploy_helper.cleanup(facts['releases_path'], facts['new_release']) elif deploy_helper.state == 'clean': changes += deploy_helper.remove_unfinished_link(facts['path']) changes += deploy_helper.remove_unfinished_builds(facts['releases_path']) - changes += deploy_helper.cleanup(facts['releases_path']) + changes += deploy_helper.cleanup(facts['releases_path'], facts['new_release']) elif deploy_helper.state == 'absent': # destroy the facts From 480db37583e8aed5b7137c4dcac66f3035461289 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 5 Dec 2015 15:48:09 +0100 Subject: [PATCH 1043/2522] cloudstack: cs_volume fixes and improvments cloudstack: cs_volume: fix not usable in older cloudstack versions affects CCP 4.3.0.2 , but not ACS / CCP 4.5.1 closes #1321 cloudstack: cs_volume: fix uable to create volumes with the same name on multiple zones cloudstack: cs_volume: use type bool and fix python3 support --- cloud/cloudstack/cs_volume.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cloud/cloudstack/cs_volume.py b/cloud/cloudstack/cs_volume.py index 5d01046d0a9..e31711f22c7 100644 --- a/cloud/cloudstack/cs_volume.py +++ b/cloud/cloudstack/cs_volume.py @@ -275,6 +275,7 @@ def get_volume(self): args['account'] = self.get_account(key='name') args['domainid'] = self.get_domain(key='id') args['projectid'] = self.get_project(key='id') + args['zoneid'] = self.get_zone(key='id') args['displayvolume'] = self.module.params.get('display_volume') args['type'] = 'DATADISK' @@ -444,7 +445,7 @@ def main(): argument_spec.update(dict( name = dict(required=True), disk_offering = dict(default=None), - display_volume = dict(choices=BOOLEANS, default=True), + display_volume = dict(type='bool', default=None), max_iops = dict(type='int', default=None), min_iops = dict(type='int', default=None), size = dict(type='int', default=None), @@ -452,14 +453,14 @@ def main(): vm = dict(default=None), device_id = dict(type='int', default=None), custom_id = dict(default=None), - force = dict(choices=BOOLEANS, default=False), - shrink_ok = dict(choices=BOOLEANS, default=False), + force = dict(type='bool', default=False), + shrink_ok = dict(type='bool', default=False), state = dict(choices=['present', 'absent', 'attached', 'detached'], default='present'), zone = dict(default=None), domain = dict(default=None), account = dict(default=None), project = dict(default=None), - poll_async = dict(choices=BOOLEANS, default=True), + poll_async = dict(type='bool', default=True), )) module = AnsibleModule( @@ -490,7 +491,7 @@ def main(): result = acs_vol.get_result(volume) - except CloudStackException, e: + except CloudStackException as e: module.fail_json(msg='CloudStackException: %s' % str(e)) module.exit_json(**result) From 99385de340043ab89a0db41a2864312424e696f8 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Wed, 16 Dec 2015 11:00:17 +0100 Subject: [PATCH 1044/2522] cloudstack: fixes and improvements cloudstack: fix python3 support and use type='bool' cloudstack: cs_instance: update vm on state=restored --- cloud/cloudstack/cs_account.py | 4 +-- cloud/cloudstack/cs_affinitygroup.py | 4 +-- cloud/cloudstack/cs_domain.py | 6 ++-- cloud/cloudstack/cs_firewall.py | 4 +-- cloud/cloudstack/cs_instance.py | 34 ++++++++----------- cloud/cloudstack/cs_instancegroup.py | 2 +- cloud/cloudstack/cs_ip_address.py | 4 +-- cloud/cloudstack/cs_iso.py | 10 +++--- cloud/cloudstack/cs_loadbalancer_rule.py | 6 ++-- .../cloudstack/cs_loadbalancer_rule_member.py | 4 +-- cloud/cloudstack/cs_network.py | 6 ++-- cloud/cloudstack/cs_portforward.py | 6 ++-- cloud/cloudstack/cs_project.py | 4 +-- cloud/cloudstack/cs_securitygroup.py | 2 +- cloud/cloudstack/cs_securitygroup_rule.py | 4 +-- cloud/cloudstack/cs_sshkeypair.py | 2 +- cloud/cloudstack/cs_staticnat.py | 4 +-- cloud/cloudstack/cs_template.py | 24 ++++++------- cloud/cloudstack/cs_user.py | 4 +-- cloud/cloudstack/cs_vmsnapshot.py | 6 ++-- 20 files changed, 68 insertions(+), 72 deletions(-) diff --git a/cloud/cloudstack/cs_account.py b/cloud/cloudstack/cs_account.py index 839f6e53281..80ab6748bb0 100644 --- a/cloud/cloudstack/cs_account.py +++ b/cloud/cloudstack/cs_account.py @@ -356,7 +356,7 @@ def main(): username = dict(default=None), password = dict(default=None), timezone = dict(default=None), - poll_async = dict(choices=BOOLEANS, default=True), + poll_async = dict(type='bool', default=True), )) module = AnsibleModule( @@ -390,7 +390,7 @@ def main(): result = acs_acc.get_result(account) - except CloudStackException, e: + except CloudStackException as e: module.fail_json(msg='CloudStackException: %s' % str(e)) module.exit_json(**result) diff --git a/cloud/cloudstack/cs_affinitygroup.py b/cloud/cloudstack/cs_affinitygroup.py index b1dc075c434..77323315d59 100644 --- a/cloud/cloudstack/cs_affinitygroup.py +++ b/cloud/cloudstack/cs_affinitygroup.py @@ -233,7 +233,7 @@ def main(): domain = dict(default=None), account = dict(default=None), project = dict(default=None), - poll_async = dict(choices=BOOLEANS, default=True), + poll_async = dict(type='bool', default=True), )) module = AnsibleModule( @@ -256,7 +256,7 @@ def main(): result = acs_ag.get_result(affinity_group) - except CloudStackException, e: + except CloudStackException as e: module.fail_json(msg='CloudStackException: %s' % str(e)) module.exit_json(**result) diff --git a/cloud/cloudstack/cs_domain.py b/cloud/cloudstack/cs_domain.py index 0d8b7deaab4..0d041d73dd0 100644 --- a/cloud/cloudstack/cs_domain.py +++ b/cloud/cloudstack/cs_domain.py @@ -244,8 +244,8 @@ def main(): path = dict(required=True), state = dict(choices=['present', 'absent'], default='present'), network_domain = dict(default=None), - clean_up = dict(choices=BOOLEANS, default=False), - poll_async = dict(choices=BOOLEANS, default=True), + clean_up = dict(type='bool', default=False), + poll_async = dict(type='bool', default=True), )) module = AnsibleModule( @@ -268,7 +268,7 @@ def main(): result = acs_dom.get_result(domain) - except CloudStackException, e: + except CloudStackException as e: module.fail_json(msg='CloudStackException: %s' % str(e)) module.exit_json(**result) diff --git a/cloud/cloudstack/cs_firewall.py b/cloud/cloudstack/cs_firewall.py index 9834dd6713a..7a6bfb6c093 100644 --- a/cloud/cloudstack/cs_firewall.py +++ b/cloud/cloudstack/cs_firewall.py @@ -414,7 +414,7 @@ def main(): domain = dict(default=None), account = dict(default=None), project = dict(default=None), - poll_async = dict(choices=BOOLEANS, default=True), + poll_async = dict(type='bool', default=True), )) required_together = cs_required_together() @@ -450,7 +450,7 @@ def main(): result = acs_fw.get_result(fw_rule) - except CloudStackException, e: + except CloudStackException as e: module.fail_json(msg='CloudStackException: %s' % str(e)) module.exit_json(**result) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 852be68a347..88076fa1c51 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -44,7 +44,6 @@ state: description: - State of the instance. - - C(restored) added in version 2.1. required: false default: 'present' choices: [ 'deployed', 'started', 'stopped', 'restarted', 'restored', 'destroyed', 'expunged', 'present', 'absent' ] @@ -827,23 +826,19 @@ def restart_instance(self): def restore_instance(self): instance = self.get_instance() - - if not instance: - instance = self.deploy_instance() - return instance - self.result['changed'] = True + # in check mode intance may not be instanciated + if instance: + args = {} + args['templateid'] = self.get_template_or_iso(key='id') + args['virtualmachineid'] = instance['id'] + res = self.cs.restoreVirtualMachine(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) - args = {} - args['templateid'] = self.get_template_or_iso(key='id') - args['virtualmachineid'] = instance['id'] - res = self.cs.restoreVirtualMachine(**args) - if 'errortext' in res: - self.module.fail_json(msg="Failed: '%s'" % res['errortext']) - - poll_async = self.module.params.get('poll_async') - if poll_async: - instance = self._poll_job(res, 'virtualmachine') + poll_async = self.module.params.get('poll_async') + if poll_async: + instance = self._poll_job(res, 'virtualmachine') return instance @@ -897,9 +892,9 @@ def main(): user_data = dict(default=None), zone = dict(default=None), ssh_key = dict(default=None), - force = dict(choices=BOOLEANS, default=False), + force = dict(type='bool', default=False), tags = dict(type='list', aliases=[ 'tag' ], default=None), - poll_async = dict(choices=BOOLEANS, default=True), + poll_async = dict(type='bool', default=True), )) required_together = cs_required_together() @@ -931,6 +926,7 @@ def main(): instance = acs_instance.expunge_instance() elif state in ['restored']: + acs_instance.present_instance() instance = acs_instance.restore_instance() elif state in ['present', 'deployed']: @@ -953,7 +949,7 @@ def main(): result = acs_instance.get_result(instance) - except CloudStackException, e: + except CloudStackException as e: module.fail_json(msg='CloudStackException: %s' % str(e)) module.exit_json(**result) diff --git a/cloud/cloudstack/cs_instancegroup.py b/cloud/cloudstack/cs_instancegroup.py index 4ffda0ede1a..bece79013ee 100644 --- a/cloud/cloudstack/cs_instancegroup.py +++ b/cloud/cloudstack/cs_instancegroup.py @@ -199,7 +199,7 @@ def main(): result = acs_ig.get_result(instance_group) - except CloudStackException, e: + except CloudStackException as e: module.fail_json(msg='CloudStackException: %s' % str(e)) module.exit_json(**result) diff --git a/cloud/cloudstack/cs_ip_address.py b/cloud/cloudstack/cs_ip_address.py index 1be597fd8cb..c5a65d6272c 100644 --- a/cloud/cloudstack/cs_ip_address.py +++ b/cloud/cloudstack/cs_ip_address.py @@ -233,7 +233,7 @@ def main(): account = dict(default=None), network = dict(default=None), project = dict(default=None), - poll_async = dict(choices=BOOLEANS, default=True), + poll_async = dict(type='bool', default=True), )) module = AnsibleModule( @@ -256,7 +256,7 @@ def main(): result = acs_ip_address.get_result(ip_address) - except CloudStackException, e: + except CloudStackException as e: module.fail_json(msg='CloudStackException: %s' % str(e)) module.exit_json(**result) diff --git a/cloud/cloudstack/cs_iso.py b/cloud/cloudstack/cs_iso.py index 98a06f6cd96..5508fdd21fa 100644 --- a/cloud/cloudstack/cs_iso.py +++ b/cloud/cloudstack/cs_iso.py @@ -306,10 +306,10 @@ def main(): account = dict(default=None), project = dict(default=None), checksum = dict(default=None), - is_ready = dict(choices=BOOLEANS, default=False), - bootable = dict(choices=BOOLEANS, default=True), - is_featured = dict(choices=BOOLEANS, default=False), - is_dynamically_scalable = dict(choices=BOOLEANS, default=False), + is_ready = dict(type='bool', default=False), + bootable = dict(type='bool', default=True), + is_featured = dict(type='bool', default=False), + is_dynamically_scalable = dict(type='bool', default=False), state = dict(choices=['present', 'absent'], default='present'), )) @@ -333,7 +333,7 @@ def main(): result = acs_iso.get_result(iso) - except CloudStackException, e: + except CloudStackException as e: module.fail_json(msg='CloudStackException: %s' % str(e)) module.exit_json(**result) diff --git a/cloud/cloudstack/cs_loadbalancer_rule.py b/cloud/cloudstack/cs_loadbalancer_rule.py index 713aaad0d45..aec04cf7eb2 100644 --- a/cloud/cloudstack/cs_loadbalancer_rule.py +++ b/cloud/cloudstack/cs_loadbalancer_rule.py @@ -355,12 +355,12 @@ def main(): ip_address = dict(required=True, aliases=['public_ip']), cidr = dict(default=None), project = dict(default=None), - open_firewall = dict(choices=BOOLEANS, default=False), + open_firewall = dict(type='bool', default=False), tags = dict(type='list', aliases=['tag'], default=None), zone = dict(default=None), domain = dict(default=None), account = dict(default=None), - poll_async = dict(choices=BOOLEANS, default=True), + poll_async = dict(type='bool', default=True), )) module = AnsibleModule( @@ -383,7 +383,7 @@ def main(): result = acs_lb_rule.get_result(rule) - except CloudStackException, e: + except CloudStackException as e: module.fail_json(msg='CloudStackException: %s' % str(e)) module.exit_json(**result) diff --git a/cloud/cloudstack/cs_loadbalancer_rule_member.py b/cloud/cloudstack/cs_loadbalancer_rule_member.py index f0738830855..757c00674ef 100644 --- a/cloud/cloudstack/cs_loadbalancer_rule_member.py +++ b/cloud/cloudstack/cs_loadbalancer_rule_member.py @@ -335,7 +335,7 @@ def main(): domain = dict(default=None), project = dict(default=None), account = dict(default=None), - poll_async = dict(choices=BOOLEANS, default=True), + poll_async = dict(type='bool', default=True), )) module = AnsibleModule( @@ -358,7 +358,7 @@ def main(): result = acs_lb_rule_member.get_result(rule) - except CloudStackException, e: + except CloudStackException as e: module.fail_json(msg='CloudStackException: %s' % str(e)) module.exit_json(**result) diff --git a/cloud/cloudstack/cs_network.py b/cloud/cloudstack/cs_network.py index 6dea3dd3ca6..fa1c7a68870 100644 --- a/cloud/cloudstack/cs_network.py +++ b/cloud/cloudstack/cs_network.py @@ -539,14 +539,14 @@ def main(): vlan = dict(default=None), vpc = dict(default=None), isolated_pvlan = dict(default=None), - clean_up = dict(type='bool', choices=BOOLEANS, default=False), + clean_up = dict(type='bool', default=False), network_domain = dict(default=None), state = dict(choices=['present', 'absent', 'restarted' ], default='present'), acl_type = dict(choices=['account', 'domain'], default='account'), project = dict(default=None), domain = dict(default=None), account = dict(default=None), - poll_async = dict(type='bool', choices=BOOLEANS, default=True), + poll_async = dict(type='bool', default=True), )) required_together = cs_required_together() required_together.extend([ @@ -578,7 +578,7 @@ def main(): result = acs_network.get_result(network) - except CloudStackException, e: + except CloudStackException as e: module.fail_json(msg='CloudStackException: %s' % str(e)) module.exit_json(**result) diff --git a/cloud/cloudstack/cs_portforward.py b/cloud/cloudstack/cs_portforward.py index 4d091f687d9..9932dcb0d6e 100644 --- a/cloud/cloudstack/cs_portforward.py +++ b/cloud/cloudstack/cs_portforward.py @@ -376,14 +376,14 @@ def main(): private_port = dict(type='int', required=True), private_end_port = dict(type='int', default=None), state = dict(choices=['present', 'absent'], default='present'), - open_firewall = dict(choices=BOOLEANS, default=False), + open_firewall = dict(type='bool', default=False), vm_guest_ip = dict(default=None), vm = dict(default=None), zone = dict(default=None), domain = dict(default=None), account = dict(default=None), project = dict(default=None), - poll_async = dict(choices=BOOLEANS, default=True), + poll_async = dict(type='bool', default=True), )) module = AnsibleModule( @@ -405,7 +405,7 @@ def main(): result = acs_pf.get_result(pf_rule) - except CloudStackException, e: + except CloudStackException as e: module.fail_json(msg='CloudStackException: %s' % str(e)) module.exit_json(**result) diff --git a/cloud/cloudstack/cs_project.py b/cloud/cloudstack/cs_project.py index 504fefc6f0c..6b37923d90d 100644 --- a/cloud/cloudstack/cs_project.py +++ b/cloud/cloudstack/cs_project.py @@ -266,7 +266,7 @@ def main(): state = dict(choices=['present', 'absent', 'active', 'suspended' ], default='present'), domain = dict(default=None), account = dict(default=None), - poll_async = dict(type='bool', choices=BOOLEANS, default=True), + poll_async = dict(type='bool', default=True), )) module = AnsibleModule( @@ -293,7 +293,7 @@ def main(): result = acs_project.get_result(project) - except CloudStackException, e: + except CloudStackException as e: module.fail_json(msg='CloudStackException: %s' % str(e)) module.exit_json(**result) diff --git a/cloud/cloudstack/cs_securitygroup.py b/cloud/cloudstack/cs_securitygroup.py index 255d306c789..d0319059793 100644 --- a/cloud/cloudstack/cs_securitygroup.py +++ b/cloud/cloudstack/cs_securitygroup.py @@ -180,7 +180,7 @@ def main(): result = acs_sg.get_result(sg) - except CloudStackException, e: + except CloudStackException as e: module.fail_json(msg='CloudStackException: %s' % str(e)) module.exit_json(**result) diff --git a/cloud/cloudstack/cs_securitygroup_rule.py b/cloud/cloudstack/cs_securitygroup_rule.py index 69e04ee7f92..a088c6c2c1e 100644 --- a/cloud/cloudstack/cs_securitygroup_rule.py +++ b/cloud/cloudstack/cs_securitygroup_rule.py @@ -384,7 +384,7 @@ def main(): end_port = dict(type='int', default=None), state = dict(choices=['present', 'absent'], default='present'), project = dict(default=None), - poll_async = dict(choices=BOOLEANS, default=True), + poll_async = dict(type='bool', default=True), )) required_together = cs_required_together() required_together.extend([ @@ -417,7 +417,7 @@ def main(): result = acs_sg_rule.get_result(sg_rule) - except CloudStackException, e: + except CloudStackException as e: module.fail_json(msg='CloudStackException: %s' % str(e)) module.exit_json(**result) diff --git a/cloud/cloudstack/cs_sshkeypair.py b/cloud/cloudstack/cs_sshkeypair.py index 7e665cd62f6..7794303f019 100644 --- a/cloud/cloudstack/cs_sshkeypair.py +++ b/cloud/cloudstack/cs_sshkeypair.py @@ -241,7 +241,7 @@ def main(): result = acs_sshkey.get_result(ssh_key) - except CloudStackException, e: + except CloudStackException as e: module.fail_json(msg='CloudStackException: %s' % str(e)) module.exit_json(**result) diff --git a/cloud/cloudstack/cs_staticnat.py b/cloud/cloudstack/cs_staticnat.py index c8fba54885e..f6b5d3f9bae 100644 --- a/cloud/cloudstack/cs_staticnat.py +++ b/cloud/cloudstack/cs_staticnat.py @@ -269,7 +269,7 @@ def main(): domain = dict(default=None), account = dict(default=None), project = dict(default=None), - poll_async = dict(choices=BOOLEANS, default=True), + poll_async = dict(type='bool', default=True), )) module = AnsibleModule( @@ -292,7 +292,7 @@ def main(): result = acs_static_nat.get_result(ip_address) - except CloudStackException, e: + except CloudStackException as e: module.fail_json(msg='CloudStackException: %s' % str(e)) module.exit_json(**result) diff --git a/cloud/cloudstack/cs_template.py b/cloud/cloudstack/cs_template.py index 94803aeb9eb..dc043425ded 100644 --- a/cloud/cloudstack/cs_template.py +++ b/cloud/cloudstack/cs_template.py @@ -537,29 +537,29 @@ def main(): vm = dict(default=None), snapshot = dict(default=None), os_type = dict(default=None), - is_ready = dict(type='bool', choices=BOOLEANS, default=False), - is_public = dict(type='bool', choices=BOOLEANS, default=True), - is_featured = dict(type='bool', choices=BOOLEANS, default=False), - is_dynamically_scalable = dict(type='bool', choices=BOOLEANS, default=False), - is_extractable = dict(type='bool', choices=BOOLEANS, default=False), - is_routing = dict(type='bool', choices=BOOLEANS, default=False), + is_ready = dict(type='bool', default=False), + is_public = dict(type='bool', default=True), + is_featured = dict(type='bool', default=False), + is_dynamically_scalable = dict(type='bool', default=False), + is_extractable = dict(type='bool', default=False), + is_routing = dict(type='bool', default=False), checksum = dict(default=None), template_filter = dict(default='self', choices=['featured', 'self', 'selfexecutable', 'sharedexecutable', 'executable', 'community']), hypervisor = dict(choices=['KVM', 'VMware', 'BareMetal', 'XenServer', 'LXC', 'HyperV', 'UCS', 'OVM', 'Simulator'], default=None), - requires_hvm = dict(type='bool', choices=BOOLEANS, default=False), - password_enabled = dict(type='bool', choices=BOOLEANS, default=False), + requires_hvm = dict(type='bool', default=False), + password_enabled = dict(type='bool', default=False), template_tag = dict(default=None), - sshkey_enabled = dict(type='bool', choices=BOOLEANS, default=False), + sshkey_enabled = dict(type='bool', default=False), format = dict(choices=['QCOW2', 'RAW', 'VHD', 'OVA'], default=None), details = dict(default=None), bits = dict(type='int', choices=[ 32, 64 ], default=64), state = dict(choices=['present', 'absent'], default='present'), - cross_zones = dict(type='bool', choices=BOOLEANS, default=False), + cross_zones = dict(type='bool', default=False), zone = dict(default=None), domain = dict(default=None), account = dict(default=None), project = dict(default=None), - poll_async = dict(type='bool', choices=BOOLEANS, default=True), + poll_async = dict(type='bool', default=True), )) required_together = cs_required_together() @@ -595,7 +595,7 @@ def main(): result = acs_tpl.get_result(tpl) - except CloudStackException, e: + except CloudStackException as e: module.fail_json(msg='CloudStackException: %s' % str(e)) module.exit_json(**result) diff --git a/cloud/cloudstack/cs_user.py b/cloud/cloudstack/cs_user.py index e6fe1c1f513..a0be4634904 100644 --- a/cloud/cloudstack/cs_user.py +++ b/cloud/cloudstack/cs_user.py @@ -415,7 +415,7 @@ def main(): last_name = dict(default=None), password = dict(default=None), timezone = dict(default=None), - poll_async = dict(choices=BOOLEANS, default=True), + poll_async = dict(type='bool', default=True), )) module = AnsibleModule( @@ -449,7 +449,7 @@ def main(): result = acs_acc.get_result(user) - except CloudStackException, e: + except CloudStackException as e: module.fail_json(msg='CloudStackException: %s' % str(e)) module.exit_json(**result) diff --git a/cloud/cloudstack/cs_vmsnapshot.py b/cloud/cloudstack/cs_vmsnapshot.py index 43e561bb93a..bec9e5132e3 100644 --- a/cloud/cloudstack/cs_vmsnapshot.py +++ b/cloud/cloudstack/cs_vmsnapshot.py @@ -263,12 +263,12 @@ def main(): vm = dict(required=True), description = dict(default=None), zone = dict(default=None), - snapshot_memory = dict(choices=BOOLEANS, default=False), + snapshot_memory = dict(type='bool', default=False), state = dict(choices=['present', 'absent', 'revert'], default='present'), domain = dict(default=None), account = dict(default=None), project = dict(default=None), - poll_async = dict(type='bool', choices=BOOLEANS, default=True), + poll_async = dict(type='bool', default=True), )) required_together = cs_required_together() @@ -298,7 +298,7 @@ def main(): result = acs_vmsnapshot.get_result(snapshot) - except CloudStackException, e: + except CloudStackException as e: module.fail_json(msg='CloudStackException: %s' % str(e)) module.exit_json(**result) From 1fe0330abe5f9d14e9b8998564efa4e6a6be4349 Mon Sep 17 00:00:00 2001 From: Benjamin Wilson Date: Wed, 16 Dec 2015 16:39:42 -0500 Subject: [PATCH 1045/2522] Properly handle adding multiple bricks to volume --- system/gluster_volume.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/system/gluster_volume.py b/system/gluster_volume.py index ff1ce9831db..24a4c3a2fc0 100644 --- a/system/gluster_volume.py +++ b/system/gluster_volume.py @@ -289,8 +289,9 @@ def stop_volume(name): def set_volume_option(name, option, parameter): run_gluster([ 'volume', 'set', name, option, parameter ]) -def add_brick(name, brick, force): - args = [ 'volume', 'add-brick', name, brick ] +def add_bricks(name, new_bricks, force): + args = [ 'volume', 'add-brick', name ] + args.extend(new_bricks) if force: args.append('force') run_gluster(args) @@ -408,8 +409,8 @@ def main(): if brick not in all_bricks: removed_bricks.append(brick) - for brick in new_bricks: - add_brick(volume_name, brick, force) + if new_bricks: + add_bricks(volume_name, new_bricks, force) changed = True # handle quotas From 652676cf9586bf72c306aba568d066d51f556c31 Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek Date: Thu, 17 Dec 2015 12:26:33 +0100 Subject: [PATCH 1046/2522] Add an __init__.py file in openstack/ The os_project module doesn't get installed if the __init__ file doesn't exist in the repository. --- cloud/openstack/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 cloud/openstack/__init__.py diff --git a/cloud/openstack/__init__.py b/cloud/openstack/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From ae8a4581f4735364094a1198ab7aec14aeae291a Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Thu, 17 Dec 2015 23:32:05 -0500 Subject: [PATCH 1047/2522] Add no_log=True to consul modules' API tokens API tokens should be considered sensitive and not logged. --- clustering/consul.py | 2 +- clustering/consul_acl.py | 4 ++-- clustering/consul_kv.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/clustering/consul.py b/clustering/consul.py index 41c98d00228..609dce89227 100644 --- a/clustering/consul.py +++ b/clustering/consul.py @@ -523,7 +523,7 @@ def main(): http=dict(required=False, type='str'), timeout=dict(required=False, type='str'), tags=dict(required=False, type='list'), - token=dict(required=False) + token=dict(required=False, no_log=True) ), supports_check_mode=False, ) diff --git a/clustering/consul_acl.py b/clustering/consul_acl.py index e5d06814ebc..17d59ea62a8 100644 --- a/clustering/consul_acl.py +++ b/clustering/consul_acl.py @@ -313,13 +313,13 @@ def test_dependencies(module): def main(): argument_spec = dict( - mgmt_token=dict(required=True), + mgmt_token=dict(required=True, no_log=True), host=dict(default='localhost'), name=dict(required=False), port=dict(default=8500, type='int'), rules=dict(default=None, required=False, type='list'), state=dict(default='present', choices=['present', 'absent']), - token=dict(required=False), + token=dict(required=False, no_log=True), token_type=dict( required=False, choices=['client', 'management'], default='client') ) diff --git a/clustering/consul_kv.py b/clustering/consul_kv.py index 06dd55b71fc..bb7dea3ad39 100644 --- a/clustering/consul_kv.py +++ b/clustering/consul_kv.py @@ -241,7 +241,7 @@ def main(): recurse=dict(required=False, type='bool'), retrieve=dict(required=False, default=True), state=dict(default='present', choices=['present', 'absent']), - token=dict(required=False, default='anonymous'), + token=dict(required=False, default='anonymous', no_log=True), value=dict(required=False) ) From 182cbbd4f7f84dcc059deb4f48db9181baaf2917 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Thu, 17 Dec 2015 18:51:53 +0100 Subject: [PATCH 1048/2522] cloudstack: cs_template: implement state=extracted --- cloud/cloudstack/cs_template.py | 60 ++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/cloud/cloudstack/cs_template.py b/cloud/cloudstack/cs_template.py index dc043425ded..753a22abc82 100644 --- a/cloud/cloudstack/cs_template.py +++ b/cloud/cloudstack/cs_template.py @@ -23,7 +23,7 @@ module: cs_template short_description: Manages templates on Apache CloudStack based clouds. description: - - Register a template from URL, create a template from a ROOT volume of a stopped VM or its snapshot and delete templates. + - Register a template from URL, create a template from a ROOT volume of a stopped VM or its snapshot, extract and delete templates. version_added: '2.0' author: "René Moser (@resmo)" options: @@ -33,7 +33,8 @@ required: true url: description: - - URL of where the template is hosted. + - URL of where the template is hosted on C(state=present). + - URL to which the template would be extracted on C(state=extracted). - Mutually exclusive with C(vm). required: false default: null @@ -174,7 +175,7 @@ - State of the template. required: false default: 'present' - choices: [ 'present', 'absent' ] + choices: [ 'present', 'absent', 'extacted' ] poll_async: description: - Poll async jobs until job has finished. @@ -314,6 +315,21 @@ returned: success type: string sample: VMware +mode: + description: Mode of extraction + returned: success + type: string + sample: http_download +state: + description: State of the extracted template + returned: success + type: string + sample: DOWNLOAD_URL_CREATED +url: + description: Url to which the template is extracted to + returned: success + type: string + sample: "http://1.2.3.4/userdata/eb307f13-4aca-45e8-b157-a414a14e6b04.ova" tags: description: List of resource tags associated with the template. returned: success @@ -370,6 +386,9 @@ def __init__(self, module): 'ispublic': 'is_public', 'format': 'format', 'hypervisor': 'hypervisor', + 'url': 'url', + 'extractMode': 'mode', + 'state': 'state', } @@ -506,6 +525,34 @@ def get_template(self): return None + def extract_template(self): + template = self.get_template() + if not template: + self.module.fail_json(msg="Failed: template not found") + + args = {} + args['id'] = template['id'] + args['url'] = self.module.params.get('url') + args['mode'] = self.module.params.get('mode') + args['zoneid'] = self.get_zone(key='id') + + if not args['url']: + self.module.fail_json(msg="Missing required arguments: url") + + self.result['changed'] = True + + if not self.module.check_mode: + template = self.cs.extractTemplate(**args) + + if 'errortext' in template: + self.module.fail_json(msg="Failed: '%s'" % template['errortext']) + + poll_async = self.module.params.get('poll_async') + if poll_async: + template = self._poll_job(template, 'template') + return template + + def remove_template(self): template = self.get_template() if template: @@ -553,8 +600,9 @@ def main(): format = dict(choices=['QCOW2', 'RAW', 'VHD', 'OVA'], default=None), details = dict(default=None), bits = dict(type='int', choices=[ 32, 64 ], default=64), - state = dict(choices=['present', 'absent'], default='present'), + state = dict(choices=['present', 'absent', 'extracted'], default='present'), cross_zones = dict(type='bool', default=False), + mode = dict(choices=['http_download', 'ftp_upload'], default='http_download'), zone = dict(default=None), domain = dict(default=None), account = dict(default=None), @@ -585,6 +633,10 @@ def main(): state = module.params.get('state') if state in ['absent']: tpl = acs_tpl.remove_template() + + elif state in ['extracted']: + tpl = acs_tpl.extract_template() + else: if module.params.get('url'): tpl = acs_tpl.register_template() From 2586541ba13867c88d227ee00e2a3f8c611f911c Mon Sep 17 00:00:00 2001 From: Alexander Winkler Date: Fri, 18 Dec 2015 12:56:45 +0100 Subject: [PATCH 1049/2522] Added update_catalog and some error handling --- packaging/os/pkgutil.py | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/packaging/os/pkgutil.py b/packaging/os/pkgutil.py index cb674b1453c..05d0b173e2c 100644 --- a/packaging/os/pkgutil.py +++ b/packaging/os/pkgutil.py @@ -42,6 +42,7 @@ description: - Specifies the repository path to install the package from. - Its global definition is done in C(/etc/opt/csw/pkgutil.conf). + required: false state: description: - Whether to install (C(present)), or remove (C(absent)) a package. @@ -49,6 +50,12 @@ - "Note: The module has a limitation that (C(latest)) only works for one package, not lists of them." required: true choices: ["present", "absent", "latest"] + update_catalog: + description: + - If you want to refresh your catalog from the mirror, set this to (C(yes)). + required: false + choices: ["yes", "no"] + default: no ''' EXAMPLES = ''' @@ -74,7 +81,7 @@ def package_installed(module, name): def package_latest(module, name, site): # Only supports one package - cmd = [ 'pkgutil', '--single', '-c' ] + cmd = [ 'pkgutil', '-U', '--single', '-c' ] if site is not None: cmd += [ '-t', pipes.quote(site) ] cmd.append(pipes.quote(name)) @@ -90,8 +97,10 @@ def run_command(module, cmd, **kwargs): cmd[0] = module.get_bin_path(progname, True, ['/opt/csw/bin']) return module.run_command(cmd, **kwargs) -def package_install(module, state, name, site): +def package_install(module, state, name, site, update_catalog): cmd = [ 'pkgutil', '-iy' ] + if update_catalog: + cmd += [ '-U' ] if site is not None: cmd += [ '-t', site ] if state == 'latest': @@ -100,8 +109,10 @@ def package_install(module, state, name, site): (rc, out, err) = run_command(module, cmd) return (rc, out, err) -def package_upgrade(module, name, site): +def package_upgrade(module, name, site, update_catalog): cmd = [ 'pkgutil', '-ufy' ] + if update_catalog: + cmd += [ '-U' ] if site is not None: cmd += [ '-t', site ] cmd.append(name) @@ -119,12 +130,14 @@ def main(): name = dict(required = True), state = dict(required = True, choices=['present', 'absent','latest']), site = dict(default = None), + update_catalog = dict(required = False, default = "no", type='bool', choices=["yes","no"]), ), supports_check_mode=True ) name = module.params['name'] state = module.params['state'] site = module.params['site'] + update_catalog = module.params['update_catalog'] rc = None out = '' err = '' @@ -136,31 +149,42 @@ def main(): if not package_installed(module, name): if module.check_mode: module.exit_json(changed=True) - (rc, out, err) = package_install(module, state, name, site) + (rc, out, err) = package_install(module, state, name, site, update_catalog) # Stdout is normally empty but for some packages can be # very long and is not often useful if len(out) > 75: out = out[:75] + '...' + if rc != 0: + module.fail_json(msg=err if err else out) elif state == 'latest': if not package_installed(module, name): if module.check_mode: module.exit_json(changed=True) - (rc, out, err) = package_install(module, state, name, site) + (rc, out, err) = package_install(module, state, name, site, update_catalog) + if len(out) > 75: + out = out[:75] + '...' + if rc != 0: + module.fail_json(msg=err if err else out) else: if not package_latest(module, name, site): if module.check_mode: module.exit_json(changed=True) - (rc, out, err) = package_upgrade(module, name, site) + (rc, out, err) = package_upgrade(module, name, site, update_catalog) if len(out) > 75: out = out[:75] + '...' + if rc != 0: + module.fail_json(msg=err if err else out) elif state == 'absent': if package_installed(module, name): if module.check_mode: module.exit_json(changed=True) (rc, out, err) = package_uninstall(module, name) - out = out[:75] + if len(out) > 75: + out = out[:75] + '...' + if rc != 0: + module.fail_json(msg=err if err else out) if rc is None: # pkgutil was not executed because the package was already present/absent From cdf7fa22501bf1d86107c67c4244ce471347fc9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Fri, 18 Dec 2015 19:40:43 +0100 Subject: [PATCH 1050/2522] iptables: cleanup --- system/iptables.py | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/system/iptables.py b/system/iptables.py index f3ae8da9383..26dfddf015c 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -54,13 +54,13 @@ default: filter choices: [ "filter", "nat", "mangle", "raw", "security" ] state: - description: + description: - Whether the rule should be absent or present. required: false default: present choices: [ "present", "absent" ] ip_version: - description: + description: - Which version of the IP protocol this rule should apply to. required: false default: ipv4 @@ -239,21 +239,15 @@ def append_param(rule, param, flag, is_list): if param is not None: rule.extend([flag, param]) -def append_comm(rule, param): - if param: - rule.extend(['-m']) - rule.extend(['comment']) +def append_csv(rule, param, flag): + if param is not None: + rule.extend([flag, ','.join(param)]) -def append_conntrack(rule, param): - if param: - rule.extend(['-m']) - rule.extend(['state']) -def append_limit(rule, param): +def append_match(rule, param, match): if param: - rule.extend(['-m']) - rule.extend(['limit']) + rule.extend(['-m', match]) def construct_rule(params): @@ -271,12 +265,11 @@ def construct_rule(params): append_param(rule, params['source_port'], '--source-port', False) append_param(rule, params['destination_port'], '--destination-port', False) append_param(rule, params['to_ports'], '--to-ports', False) - append_comm(rule, params['comment']) + append_match(rule, params['comment'], 'comment') append_param(rule, params['comment'], '--comment', False) - if params['ctstate']: - append_conntrack(rule, params['ctstate']) - append_param(rule, ','.join(params['ctstate']), '--state', False) - append_limit(rule, params['limit']) + append_match(rule, params['ctstate'], 'state') + append_csv(rule, params['ctstate'], '--state') + append_match(rule, params['limit'], 'limit') append_param(rule, params['limit'], '--limit', False) return rule From 64be5b395a03f1b0811e75690948f82d30d9f16c Mon Sep 17 00:00:00 2001 From: Dreamcat4 Date: Sat, 19 Dec 2015 21:57:13 +0000 Subject: [PATCH 1051/2522] win_regedit: fixes #1404 When 'value:' is set to be the key's "(default)" property value --- windows/win_regedit.ps1 | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/windows/win_regedit.ps1 b/windows/win_regedit.ps1 index f9491e39c57..e3b8c9d3b10 100644 --- a/windows/win_regedit.ps1 +++ b/windows/win_regedit.ps1 @@ -57,8 +57,16 @@ if($state -eq "present") { { if (Test-RegistryValueData -Path $registryKey -Value $registryValue) { + if ($registryValue.ToLower() -eq "(default)") { + # Special case handling for the key's default property. Because .GetValueKind() doesn't work for the (default) key property + $oldRegistryDataType = "String" + } + else { + $oldRegistryDataType = (Get-Item $registryKey).GetValueKind($registryValue) + } + # Changes Data and DataType - if ((Get-Item $registryKey).GetValueKind($registryValue) -ne $registryDataType) + if ($registryDataType -ne $oldRegistryDataType) { Try { From 56fb7abc65f0e1cc192b519158c87941b2ceb8d8 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sun, 20 Dec 2015 00:29:20 +0100 Subject: [PATCH 1052/2522] cloudstack: cs_volume: simplify detach on state=absent --- cloud/cloudstack/cs_volume.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/cloud/cloudstack/cs_volume.py b/cloud/cloudstack/cs_volume.py index e31711f22c7..b10d34a24ee 100644 --- a/cloud/cloudstack/cs_volume.py +++ b/cloud/cloudstack/cs_volume.py @@ -392,18 +392,15 @@ def absent_volume(self): volume = self.get_volume() if volume: - if 'attached' in volume: - if self.module.param.get('force'): - self.detached_volume() - else: - self.module.fail_json(msg="Volume '%s' is attached, use force=true for detaching and removing the volume." % volume.get('name')) + if 'attached' in volume and not self.module.param.get('force'): + self.module.fail_json(msg="Volume '%s' is attached, use force=true for detaching and removing the volume." % volume.get('name')) self.result['changed'] = True if not self.module.check_mode: volume = self.detached_volume() res = self.cs.deleteVolume(id=volume['id']) - if 'errortext' in volume: + if 'errortext' in res: self.module.fail_json(msg="Failed: '%s'" % res['errortext']) poll_async = self.module.params.get('poll_async') if poll_async: From 15b14f810926fe4dc4ca5a4b40ce073effb2dd7a Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sun, 20 Dec 2015 22:18:30 +0100 Subject: [PATCH 1053/2522] cloudstack: cs_instance: fix do not require name to be set to avoid clashes Require one of display_name or name. If both is given, name is used as identifier. --- cloud/cloudstack/cs_instance.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 88076fa1c51..7b1eeafd4b0 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -30,10 +30,15 @@ name: description: - Host name of the instance. C(name) can only contain ASCII letters. - required: true + - Name will be generated (UUID) by CloudStack if not specified and can not be changed afterwards. + - Either C(name) or C(display_name) is required. + required: false + default: null display_name: description: - Custom display name of the instances. + - Display name will be set to C(name) if not specified. + - Either C(name) or C(display_name) is required. required: false default: null group: @@ -225,16 +230,21 @@ service_offering: 2cpu_2gb force: yes -# Create or update a instance on Exoscale's public cloud +# Create or update a instance on Exoscale's public cloud using display_name. +# Note: user_data can be used to kickstart the instance using cloud-init yaml config. - local_action: module: cs_instance - name: web-vm-1 + display_name: web-vm-1 template: Linux Debian 7 64-bit service_offering: Tiny ssh_key: john@example.com tags: - { key: admin, value: john } - { key: foo, value: bar } + user_data: | + #cloud-config + packages: + - nginx # Create an instance with multiple interfaces specifying the IP addresses - local_action: @@ -479,7 +489,7 @@ def get_disk_offering_id(self): def get_instance(self): instance = self.instance if not instance: - instance_name = self.module.params.get('name') + instance_name = self.get_or_fallback('name', 'display_name') args = {} args['account'] = self.get_account(key='name') @@ -865,7 +875,7 @@ def get_result(self, instance): def main(): argument_spec = cs_argument_spec() argument_spec.update(dict( - name = dict(required=True), + name = dict(default=None), display_name = dict(default=None), group = dict(default=None), state = dict(choices=['present', 'deployed', 'started', 'stopped', 'restarted', 'restored', 'absent', 'destroyed', 'expunged'], default='present'), @@ -905,6 +915,9 @@ def main(): module = AnsibleModule( argument_spec=argument_spec, required_together=required_together, + required_one_of = ( + ['display_name', 'name'], + ), mutually_exclusive = ( ['template', 'iso'], ), From 06f6a5375e484b27cbd8719b80bd25c75d5af0b2 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sun, 20 Dec 2015 22:24:58 +0100 Subject: [PATCH 1054/2522] cloudstack: fix name is not case insensitive --- cloud/cloudstack/cs_affinitygroup.py | 7 ++-- cloud/cloudstack/cs_securitygroup.py | 53 +++++++++++++++++++++++----- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/cloud/cloudstack/cs_affinitygroup.py b/cloud/cloudstack/cs_affinitygroup.py index 77323315d59..9ca801a8f4c 100644 --- a/cloud/cloudstack/cs_affinitygroup.py +++ b/cloud/cloudstack/cs_affinitygroup.py @@ -145,19 +145,16 @@ def __init__(self, module): def get_affinity_group(self): if not self.affinity_group: - affinity_group = self.module.params.get('name') args = {} args['projectid'] = self.get_project(key='id') args['account'] = self.get_account('name') args['domainid'] = self.get_domain('id') + args['name'] = self.module.params.get('name') affinity_groups = self.cs.listAffinityGroups(**args) if affinity_groups: - for a in affinity_groups['affinitygroup']: - if affinity_group in [ a['name'], a['id'] ]: - self.affinity_group = a - break + self.affinity_group = affinity_groups['affinitygroup'][0] return self.affinity_group diff --git a/cloud/cloudstack/cs_securitygroup.py b/cloud/cloudstack/cs_securitygroup.py index d0319059793..2b0f901429d 100644 --- a/cloud/cloudstack/cs_securitygroup.py +++ b/cloud/cloudstack/cs_securitygroup.py @@ -42,6 +42,16 @@ required: false default: 'present' choices: [ 'present', 'absent' ] + domain: + description: + - Domain the security group is related to. + required: false + default: null + account: + description: + - Account the security group is related to. + required: false + default: null project: description: - Name of the project the security group to be created in. @@ -81,6 +91,26 @@ returned: success type: string sample: application security group +tags: + description: List of resource tags associated with the security group. + returned: success + type: dict + sample: '[ { "key": "foo", "value": "bar" } ]' +project: + description: Name of project the security group is related to. + returned: success + type: string + sample: Production +domain: + description: Domain the security group is related to. + returned: success + type: string + sample: example domain +account: + description: Account the security group is related to. + returned: success + type: string + sample: example account ''' try: @@ -102,15 +132,16 @@ def __init__(self, module): def get_security_group(self): if not self.security_group: - sg_name = self.module.params.get('name') + args = {} - args['projectid'] = self.get_project('id') + args['projectid'] = self.get_project(key='id') + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') + args['securitygroupname'] = self.module.params.get('name') + sgs = self.cs.listSecurityGroups(**args) if sgs: - for s in sgs['securitygroup']: - if s['name'] == sg_name: - self.security_group = s - break + self.security_group = sgs['securitygroup'][0] return self.security_group @@ -121,7 +152,9 @@ def create_security_group(self): args = {} args['name'] = self.module.params.get('name') - args['projectid'] = self.get_project('id') + args['projectid'] = self.get_project(key='id') + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') args['description'] = self.module.params.get('description') if not self.module.check_mode: @@ -140,7 +173,9 @@ def remove_security_group(self): args = {} args['name'] = self.module.params.get('name') - args['projectid'] = self.get_project('id') + args['projectid'] = self.get_project(key='id') + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') if not self.module.check_mode: res = self.cs.deleteSecurityGroup(**args) @@ -158,6 +193,8 @@ def main(): description = dict(default=None), state = dict(choices=['present', 'absent'], default='present'), project = dict(default=None), + account = dict(default=None), + domain = dict(default=None), )) module = AnsibleModule( From 2f3dc1352fa4de14c3f42d06428c3a7d41869211 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Mon, 21 Dec 2015 23:39:20 +0100 Subject: [PATCH 1055/2522] Replace choices=BOOLEANS by type='bool', fix #1326 --- cloud/lxc/lxc_container.py | 6 +++--- cloud/misc/proxmox.py | 6 +++--- cloud/misc/proxmox_template.py | 4 ++-- cloud/webfaction/webfaction_app.py | 4 ++-- cloud/webfaction/webfaction_site.py | 2 +- messaging/rabbitmq_exchange.py | 6 +++--- messaging/rabbitmq_queue.py | 4 ++-- monitoring/datadog_monitor.py | 4 ++-- packaging/os/apk.py | 4 ++-- packaging/os/pkg5.py | 2 +- packaging/os/pkg5_publisher.py | 4 ++-- system/osx_defaults.py | 2 +- system/svc.py | 4 ++-- 13 files changed, 26 insertions(+), 26 deletions(-) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index fd5daf1c47c..6c177552d6b 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -1684,7 +1684,7 @@ def main(): type='str' ), container_log=dict( - choices=BOOLEANS, + type='bool', default='false' ), container_log_level=dict( @@ -1696,11 +1696,11 @@ def main(): required=False ), clone_snapshot=dict( - choices=BOOLEANS, + type='bool', default='false' ), archive=dict( - choices=BOOLEANS, + type='bool', default='false' ), archive_path=dict( diff --git a/cloud/misc/proxmox.py b/cloud/misc/proxmox.py index 7be4361edbe..e5a7e0b15d5 100644 --- a/cloud/misc/proxmox.py +++ b/cloud/misc/proxmox.py @@ -273,7 +273,7 @@ def main(): api_user = dict(required=True), api_password = dict(no_log=True), vmid = dict(required=True), - validate_certs = dict(type='bool', choices=BOOLEANS, default='no'), + validate_certs = dict(type='bool', default='no'), node = dict(), password = dict(no_log=True), hostname = dict(), @@ -284,13 +284,13 @@ def main(): swap = dict(type='int', default=0), netif = dict(), ip_address = dict(), - onboot = dict(type='bool', choices=BOOLEANS, default='no'), + onboot = dict(type='bool', default='no'), storage = dict(default='local'), cpuunits = dict(type='int', default=1000), nameserver = dict(), searchdomain = dict(), timeout = dict(type='int', default=30), - force = dict(type='bool', choices=BOOLEANS, default='no'), + force = dict(type='bool', default='no'), state = dict(default='present', choices=['present', 'absent', 'stopped', 'started', 'restarted']), ) ) diff --git a/cloud/misc/proxmox_template.py b/cloud/misc/proxmox_template.py index 7fed47f7260..6434e59be23 100644 --- a/cloud/misc/proxmox_template.py +++ b/cloud/misc/proxmox_template.py @@ -156,14 +156,14 @@ def main(): api_host = dict(required=True), api_user = dict(required=True), api_password = dict(no_log=True), - validate_certs = dict(type='bool', choices=BOOLEANS, default='no'), + validate_certs = dict(type='bool', default='no'), node = dict(), src = dict(), template = dict(), content_type = dict(default='vztmpl', choices=['vztmpl','iso']), storage = dict(default='local'), timeout = dict(type='int', default=30), - force = dict(type='bool', choices=BOOLEANS, default='no'), + force = dict(type='bool', default='no'), state = dict(default='present', choices=['present', 'absent']), ) ) diff --git a/cloud/webfaction/webfaction_app.py b/cloud/webfaction/webfaction_app.py index 1c015a401d1..52ecedf438d 100644 --- a/cloud/webfaction/webfaction_app.py +++ b/cloud/webfaction/webfaction_app.py @@ -112,9 +112,9 @@ def main(): name = dict(required=True), state = dict(required=False, choices=['present', 'absent'], default='present'), type = dict(required=True), - autostart = dict(required=False, choices=BOOLEANS, default=False), + autostart = dict(required=False, type='bool', default=False), extra_info = dict(required=False, default=""), - port_open = dict(required=False, choices=BOOLEANS, default=False), + port_open = dict(required=False, type='bool', default=False), login_name = dict(required=True), login_password = dict(required=True), machine = dict(required=False, default=False), diff --git a/cloud/webfaction/webfaction_site.py b/cloud/webfaction/webfaction_site.py index bb1bfb94457..0226a2a2f92 100644 --- a/cloud/webfaction/webfaction_site.py +++ b/cloud/webfaction/webfaction_site.py @@ -112,7 +112,7 @@ def main(): state = dict(required=False, choices=['present', 'absent'], default='present'), # You can specify an IP address or hostname. host = dict(required=True), - https = dict(required=False, choices=BOOLEANS, default=False), + https = dict(required=False, type='bool', default=False), subdomains = dict(required=False, default=[]), site_apps = dict(required=False, default=[]), login_name = dict(required=True), diff --git a/messaging/rabbitmq_exchange.py b/messaging/rabbitmq_exchange.py index 728186385cb..1e69d434502 100644 --- a/messaging/rabbitmq_exchange.py +++ b/messaging/rabbitmq_exchange.py @@ -120,9 +120,9 @@ def main(): login_host = dict(default='localhost', type='str'), login_port = dict(default='15672', type='str'), vhost = dict(default='/', type='str'), - durable = dict(default=True, choices=BOOLEANS, type='bool'), - auto_delete = dict(default=False, choices=BOOLEANS, type='bool'), - internal = dict(default=False, choices=BOOLEANS, type='bool'), + durable = dict(default=True, type='bool'), + auto_delete = dict(default=False, type='bool'), + internal = dict(default=False, type='bool'), exchange_type = dict(default='direct', aliases=['type'], type='str'), arguments = dict(default=dict(), type='dict') ), diff --git a/messaging/rabbitmq_queue.py b/messaging/rabbitmq_queue.py index 5a403a6b602..be37ce7eaa3 100644 --- a/messaging/rabbitmq_queue.py +++ b/messaging/rabbitmq_queue.py @@ -134,8 +134,8 @@ def main(): login_host = dict(default='localhost', type='str'), login_port = dict(default='15672', type='str'), vhost = dict(default='/', type='str'), - durable = dict(default=True, choices=BOOLEANS, type='bool'), - auto_delete = dict(default=False, choices=BOOLEANS, type='bool'), + durable = dict(default=True, type='bool'), + auto_delete = dict(default=False, type='bool'), message_ttl = dict(default=None, type='int'), auto_expires = dict(default=None, type='int'), max_length = dict(default=None, type='int'), diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py index 9318326620e..474d2e42090 100644 --- a/monitoring/datadog_monitor.py +++ b/monitoring/datadog_monitor.py @@ -144,12 +144,12 @@ def main(): query=dict(required=False), message=dict(required=False, default=None), silenced=dict(required=False, default=None, type='dict'), - notify_no_data=dict(required=False, default=False, choices=BOOLEANS), + notify_no_data=dict(required=False, default=False, type='bool'), no_data_timeframe=dict(required=False, default=None), timeout_h=dict(required=False, default=None), renotify_interval=dict(required=False, default=None), escalation_message=dict(required=False, default=None), - notify_audit=dict(required=False, default=False, choices=BOOLEANS), + notify_audit=dict(required=False, default=False, type='bool'), thresholds=dict(required=False, type='dict', default={'ok': 1, 'critical': 1, 'warning': 1}), ) ) diff --git a/packaging/os/apk.py b/packaging/os/apk.py index ec0e3908faf..191f3b39b09 100644 --- a/packaging/os/apk.py +++ b/packaging/os/apk.py @@ -177,8 +177,8 @@ def main(): argument_spec = dict( state = dict(default='present', choices=['present', 'installed', 'absent', 'removed', 'latest']), name = dict(type='list'), - update_cache = dict(default='no', choices=BOOLEANS, type='bool'), - upgrade = dict(default='no', choices=BOOLEANS, type='bool'), + update_cache = dict(default='no', type='bool'), + upgrade = dict(default='no', type='bool'), ), required_one_of = [['name', 'update_cache', 'upgrade']], supports_check_mode = True diff --git a/packaging/os/pkg5.py b/packaging/os/pkg5.py index 837eefd243e..4fb34d7a51c 100644 --- a/packaging/os/pkg5.py +++ b/packaging/os/pkg5.py @@ -78,7 +78,7 @@ def main(): ] ), accept_licenses=dict( - choices=BOOLEANS, + type='bool', default=False, aliases=['accept_licences', 'accept'], ), diff --git a/packaging/os/pkg5_publisher.py b/packaging/os/pkg5_publisher.py index 3881f5dd0b8..08c33464e7f 100644 --- a/packaging/os/pkg5_publisher.py +++ b/packaging/os/pkg5_publisher.py @@ -77,8 +77,8 @@ def main(): argument_spec=dict( name=dict(required=True, aliases=['publisher']), state=dict(default='present', choices=['present', 'absent']), - sticky=dict(choices=BOOLEANS), - enabled=dict(choices=BOOLEANS), + sticky=dict(type='bool'), + enabled=dict(type='bool'), # search_after=dict(), # search_before=dict(), origin=dict(type='list'), diff --git a/system/osx_defaults.py b/system/osx_defaults.py index e4dc5f8c750..614adc4852b 100644 --- a/system/osx_defaults.py +++ b/system/osx_defaults.py @@ -309,7 +309,7 @@ def main(): array_add=dict( default=False, required=False, - choices=BOOLEANS, + type='bool', ), value=dict( default=None, diff --git a/system/svc.py b/system/svc.py index 9831ce42ea7..36b4df5f9f7 100644 --- a/system/svc.py +++ b/system/svc.py @@ -240,8 +240,8 @@ def main(): argument_spec = dict( name = dict(required=True), state = dict(choices=['started', 'stopped', 'restarted', 'killed', 'reloaded', 'once']), - enabled = dict(required=False, type='bool', choices=BOOLEANS), - downed = dict(required=False, type='bool', choices=BOOLEANS), + enabled = dict(required=False, type='bool'), + downed = dict(required=False, type='bool'), dist = dict(required=False, default='daemontools'), service_dir = dict(required=False, default='/service'), service_src = dict(required=False, default='/etc/service'), From f7f760b8d80f7b5585255c7ce44a8ebc6deafa8f Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 5 Oct 2015 12:47:45 -0500 Subject: [PATCH 1056/2522] Use ansible-testing project to validate basic module requirements --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index c2583d592fc..466ebc1472d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,8 +9,12 @@ addons: packages: - python2.4 - python2.6 +install: + - pip install git+https://github.com/ansible/ansible.git@devel#egg=ansible + - pip install git+https://github.com/sivel/ansible-testing.git#egg=ansible_testing script: - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py' . - python2.6 -m compileall -fq . - python2.7 -m compileall -fq . + - ansible-validate-modules --exclude 'cloud/amazon/route53_zone\.py|network/snmp_facts\.py|notification/sns\.py|windows/win_iis_virtualdirectory\.py' . #- ./test-docs.sh extras From 0826256e0fa11da839acf9cd1b00c9303fd10140 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Thu, 8 Oct 2015 10:11:19 -0500 Subject: [PATCH 1057/2522] Remove unneeded spaces at the end of ohai --- system/ohai.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/system/ohai.py b/system/ohai.py index 6f066ec5ad8..d71d581b628 100644 --- a/system/ohai.py +++ b/system/ohai.py @@ -54,5 +54,3 @@ def main(): from ansible.module_utils.basic import * main() - - From 87b0a45a85797dbd88f10f70698683c41b2dde73 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 22 Dec 2015 15:37:41 -0600 Subject: [PATCH 1058/2522] Fix tabbed indentation in network/haproxy.py --- network/haproxy.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/network/haproxy.py b/network/haproxy.py index 0fb4beb3004..4cc1c1c618b 100644 --- a/network/haproxy.py +++ b/network/haproxy.py @@ -312,9 +312,9 @@ def act(self): Figure out what you want to do from ansible, and then do it. """ - self.get_current_state(self.host, self.backend) - self.previous_states = ','.join(self.status_server) - self.previous_weights = ','.join(self.status_weight) + self.get_current_state(self.host, self.backend) + self.previous_states = ','.join(self.status_server) + self.previous_weights = ','.join(self.status_weight) # toggle enable/disbale server if self.state == 'enabled': From 4b9388ce9912aedaccce099de5f22c6da176356a Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 22 Dec 2015 15:38:03 -0600 Subject: [PATCH 1059/2522] Fix tabbed indentation in packaging/os/pkgutil.py --- packaging/os/pkgutil.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packaging/os/pkgutil.py b/packaging/os/pkgutil.py index cb674b1453c..4307fa6fdac 100644 --- a/packaging/os/pkgutil.py +++ b/packaging/os/pkgutil.py @@ -2,8 +2,8 @@ # -*- coding: utf-8 -*- # (c) 2013, Alexander Winkler -# based on svr4pkg by -# Boyd Adamson (2012) +# based on svr4pkg by +# Boyd Adamson (2012) # # This file is part of Ansible # From 9342bd7c38627a12b17013dcdf445f68172b0d1e Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 22 Dec 2015 15:38:29 -0600 Subject: [PATCH 1060/2522] Import module snippets in the correct location --- system/iptables.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/iptables.py b/system/iptables.py index f3ae8da9383..fc6cdb3be5f 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -18,9 +18,6 @@ # You should have received a copy of the GNU General Public License # along with this software. If not, see . -# import module snippets -from ansible.module_utils.basic import * - BINS = dict( ipv4='iptables', ipv6='ip6tables', @@ -363,5 +360,8 @@ def main(): module.exit_json(**args) +# import module snippets +from ansible.module_utils.basic import * + if __name__ == '__main__': main() From 9645d67fd4b925b83bb95549e53ab5a76ab794a8 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 22 Dec 2015 15:38:44 -0600 Subject: [PATCH 1061/2522] Update excludes with currently failing modules --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 466ebc1472d..d61c5743272 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,5 +16,5 @@ script: - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py' . - python2.6 -m compileall -fq . - python2.7 -m compileall -fq . - - ansible-validate-modules --exclude 'cloud/amazon/route53_zone\.py|network/snmp_facts\.py|notification/sns\.py|windows/win_iis_virtualdirectory\.py' . + - ansible-validate-modules --exclude 'cloud/amazon/route53_zone\.py|cloud/amazon/sns_topic\.py|messaging/rabbitmq_binding\.py|messaging/rabbitmq_exchange\.py|messaging/rabbitmq_queue\.py|monitoring/circonus_annotation\.py|network/snmp_facts\.py|notification/sns\.py' . #- ./test-docs.sh extras From aee09745d36643549317401baee835f3a025dea2 Mon Sep 17 00:00:00 2001 From: Marcin Dobosz Date: Fri, 16 Oct 2015 13:48:32 -0700 Subject: [PATCH 1062/2522] Fix win_iis_webapppool module to not null ref when removing an apppool using PS4 --- windows/win_iis_webapppool.ps1 | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/windows/win_iis_webapppool.ps1 b/windows/win_iis_webapppool.ps1 index 6dcf7ec192c..858a151f2a3 100644 --- a/windows/win_iis_webapppool.ps1 +++ b/windows/win_iis_webapppool.ps1 @@ -101,12 +101,15 @@ try { # Result $pool = Get-Item IIS:\AppPools\$name -$result.info = @{ - name = $pool.Name - state = $pool.State - attributes = New-Object psobject @{} -}; - -$pool.Attributes | ForEach { $result.info.attributes.Add($_.Name, $_.Value)}; +if ($pool) +{ + $result.info = @{ + name = $pool.Name + state = $pool.State + attributes = New-Object psobject @{} + }; + + $pool.Attributes | ForEach { $result.info.attributes.Add($_.Name, $_.Value)}; +} Exit-Json $result From 736321001f3674f6b2dee0082286a601da4ac67d Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Fri, 4 Dec 2015 21:40:49 -0500 Subject: [PATCH 1063/2522] Add new ec2_vpc_dhcp_options module This module manages EC2 DHCP options for a given VPC. It allows you to specify all the options which AWS allows you to set in a DHCP option set. --- cloud/amazon/ec2_vpc_dhcp_options.py | 236 +++++++++++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 cloud/amazon/ec2_vpc_dhcp_options.py diff --git a/cloud/amazon/ec2_vpc_dhcp_options.py b/cloud/amazon/ec2_vpc_dhcp_options.py new file mode 100644 index 00000000000..f68a689a887 --- /dev/null +++ b/cloud/amazon/ec2_vpc_dhcp_options.py @@ -0,0 +1,236 @@ +#!/usr/bin/python + +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = """ +--- +module: ec2_vpc_dhcp_options +short_description: Ensures the DHCP options for the given VPC match what's + requested +description: + - Converges the DHCP option set for the given VPC to the variables requested. + If any of the optional values are missing, they will either be treated + as a no-op (i.e., inherit what already exists for the VPC) or a purge of + existing options. Most of the options should be self-explanatory. +author: "Joel Thompson (@joelthompson)" +version_added: 2.1 +options: + - domain_name: + description: + - The domain name to set in the DHCP option sets + required: + - false + default: "" + - dns_servers: + description: + - A list of hosts to set the DNS servers for the VPC to. (Should be a + list of IP addresses rather than host names.) + required: false + default: [] + - ntp_servers: + description: + - List of hosts to advertise as NTP servers for the VPC. + required: false + default: [] + - netbios_name_servers: + description: + - List of hosts to advertise as NetBIOS servers. + required: false + default: [] + - netbios_node_type: + description: + - NetBIOS node type to advertise in the DHCP options. The + default is 2, per AWS recommendation + http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_DHCP_Options.html + required: false + default: 2 + - vpc_id: + description: + - VPC ID to associate with the requested DHCP option set + required: true + - delete_old: + description: + - Whether to delete the old VPC DHCP option set when creating a new one. + This is primarily useful for debugging/development purposes when you + want to quickly roll back to the old option set. Note that this setting + will be ignored, and the old DHCP option set will be preserved, if it + is in use by any other VPC. (Otherwise, AWS will return an error.) + required: false + default: true + - inherit_existing: + description: + - For any DHCP options not specified in these parameters, whether to + inherit them from the options set already applied to vpc_id, or to + reset them to be empty. + required: false + default: false +extends_documentation_fragment: aws +requirements: + - boto +""" + +RETURN = """ +new_options: + description: The new DHCP options associated with your VPC + returned: changed + type: dict + sample: + domain-name-servers: + - 10.0.0.1 + - 10.0.1.1 + netbois-name-servers: + - 10.0.0.1 + - 10.0.1.1 + ntp-servers: None + netbios-node-type: 2 + domain-name: "my.example.com" +""" + +EXAMPLES = """ +# Completely overrides the VPC DHCP options associated with VPC vpc-123456 and deletes any existing +# DHCP option set that may have been attached to that VPC. +- ec2_vpc_dhcp_options: + domain_name: "foo.example.com" + region: us-east-1 + dns_servers: + - 10.0.0.1 + - 10.0.1.1 + ntp_servers: + - 10.0.0.2 + - 10.0.1.2 + netbios_name_servers: + - 10.0.0.1 + - 10.0.1.1 + netbios_node_type: 2 + vpc_id: vpc-123456 + delete_old: True + inherit_existing: False + + +# Ensure the DHCP option set for the VPC has 10.0.0.4 and 10.0.1.4 as the specified DNS servers, but +# keep any other existing settings. Also, keep the old DHCP option set around. +- ec2_vpc_dhcp_options: + region: us-east-1 + dns_servers: + - "{{groups['dns-primary']}}" + - "{{groups['dns-secondary']}}" + vpc_id: vpc-123456 + inherit_existing: True + delete_old: False +""" + +import boto.vpc +import socket +import collections + +def _get_associated_dhcp_options(vpc_id, vpc_connection): + """ + Returns the DHCP options object currently associated with the requested VPC ID using the VPC + connection variable. + """ + vpcs = vpc_connection.get_all_vpcs(vpc_ids=[vpc_id]) + if len(vpcs) != 1: + return None + dhcp_options = vpc_connection.get_all_dhcp_options(dhcp_options_ids=[vpcs[0].dhcp_options_id]) + if len(dhcp_options) != 1: + return None + return dhcp_options[0] + + +def _get_vpcs_by_dhcp_options(dhcp_options_id, vpc_connection): + return vpc_connection.get_all_vpcs(filters={'dhcpOptionsId': dhcp_options_id}) + + +def _get_updated_option(requested, existing, inherit): + if inherit and (not requested or requested == ['']): + return existing + else: + return requested + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + domain_name=dict(type='str', default=''), + dns_servers=dict(type='list', default=[]), + ntp_servers=dict(type='list', default=[]), + netbios_name_servers=dict(type='list', default=[]), + netbios_node_type=dict(type='int', default=2), + vpc_id=dict(type='str', required=True), + delete_old=dict(type='bool', default=True), + inherit_existing=dict(type='bool', default=False) + ) + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + params = module.params + + + region, ec2_url, boto_params = get_aws_connection_info(module) + connection = connect_to_aws(boto.vpc, region, **boto_params) + + inherit_existing = params['inherit_existing'] + + existing_options = _get_associated_dhcp_options(params['vpc_id'], connection) + new_options = collections.defaultdict(lambda: None) + + new_options['domain-name-servers'] = _get_updated_option( params['dns_servers'], + existing_options.options.get('domain-name-servers'), inherit_existing) + + new_options['netbios-name-servers'] = _get_updated_option(params['netbios_name_servers'], + existing_options.options.get('netbios-name-servers'), inherit_existing) + + + new_options['ntp-servers'] = _get_updated_option(params['ntp_servers'], + existing_options.options.get('ntp-servers'), inherit_existing) + + # HACK: Why do I make the next two lists? The boto api returns a list if present, so + # I need this to properly compare so == works. + + # HACK: netbios-node-type is an int, but boto returns a string. So, asking for an int from Ansible + # for data validation, but still need to cast it to a string + new_options['netbios-node-type'] = _get_updated_option( + [str(params['netbios_node_type'])], existing_options.options.get('netbios-node-type'), + inherit_existing) + + new_options['domain-name'] = _get_updated_option( + [params['domain_name']], existing_options.options.get('domain-name'), inherit_existing) + + if existing_options and new_options == existing_options.options: + module.exit_json(changed=False) + + if new_options['netbios-node-type']: + new_options['netbios-node-type'] = new_options['netbios-node-type'][0] + + if new_options['domain-name']: + new_options['domain-name'] = new_options['domain-name'][0] + + if not module.check_mode: + dhcp_option = connection.create_dhcp_options(new_options['domain-name'], + new_options['domain-name-servers'], new_options['ntp-servers'], + new_options['netbios-name-servers'], new_options['netbios-node-type']) + connection.associate_dhcp_options(dhcp_option.id, params['vpc_id']) + if params['delete_old'] and existing_options: + other_vpcs = _get_vpcs_by_dhcp_options(existing_options.id, connection) + if len(other_vpcs) == 0 or (len(other_vpcs) == 1 and other_vpcs[0].id == params['vpc_id']): + connection.delete_dhcp_options(existing_options.id) + + module.exit_json(changed=True, new_options=new_options) + + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == "__main__": + main() From 6efa406b6f1733dfca7d96b4d5f1d228e6ad609a Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Wed, 23 Dec 2015 12:00:36 -0500 Subject: [PATCH 1064/2522] added examples to route53_zone fixes #1061 --- cloud/amazon/route53_zone.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/cloud/amazon/route53_zone.py b/cloud/amazon/route53_zone.py index 33a76ea0c4a..ca8034fac07 100644 --- a/cloud/amazon/route53_zone.py +++ b/cloud/amazon/route53_zone.py @@ -52,6 +52,29 @@ author: "Christopher Troup (@minichate)" ''' +EXAMPLES = ''' +# create a public zone +- route53_zone: zone=example.com state=present comment="this is an example" + +# delete a public zone +- route53_zone: zone=example.com state=absent + +- name: private zone for devel + route53_zome: zone=devel.example.com state=present vpc_id={{myvpc_id}} comment='developer domain' + +# more complex example +- name: register output after creating zone in parameterized region + route53_zone: + vpc_id: "{{ vpc.vpc_id }}" + vpc_region: "{{ ec2_region }}" + zone: "{{ vpc_dns_zone }}" + state: present + register: zone_out + +- debug: var=zone_out + +''' + import time try: From 01969e7b6342583cd8ea7875406fdfe6fa3a065d Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Wed, 23 Dec 2015 11:10:48 -0600 Subject: [PATCH 1065/2522] Update .travis.yml to remove the validation exclusion for route53_zone --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d61c5743272..b788d1b820e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,5 +16,5 @@ script: - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py' . - python2.6 -m compileall -fq . - python2.7 -m compileall -fq . - - ansible-validate-modules --exclude 'cloud/amazon/route53_zone\.py|cloud/amazon/sns_topic\.py|messaging/rabbitmq_binding\.py|messaging/rabbitmq_exchange\.py|messaging/rabbitmq_queue\.py|monitoring/circonus_annotation\.py|network/snmp_facts\.py|notification/sns\.py' . + - ansible-validate-modules --exclude 'cloud/amazon/sns_topic\.py|messaging/rabbitmq_binding\.py|messaging/rabbitmq_exchange\.py|messaging/rabbitmq_queue\.py|monitoring/circonus_annotation\.py|network/snmp_facts\.py|notification/sns\.py' . #- ./test-docs.sh extras From c67316cbaf9f0a1915f9df48004ee6f08c2216f2 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Thu, 24 Dec 2015 11:57:15 -0800 Subject: [PATCH 1066/2522] Update f5 validate_certs functionality to do the right thing on multiple python versions This requires the implementation in the module_utils code here https://github.com/ansible/ansible/pull/13667 to funciton --- network/f5/bigip_facts.py | 27 +++++++++++++-------------- network/f5/bigip_monitor_http.py | 5 +++-- network/f5/bigip_monitor_tcp.py | 5 +++-- network/f5/bigip_node.py | 5 +++-- network/f5/bigip_pool.py | 8 +++----- network/f5/bigip_pool_member.py | 5 +++-- 6 files changed, 28 insertions(+), 27 deletions(-) diff --git a/network/f5/bigip_facts.py b/network/f5/bigip_facts.py index 1b106ba0a3e..9a2f5abdbf4 100644 --- a/network/f5/bigip_facts.py +++ b/network/f5/bigip_facts.py @@ -59,7 +59,8 @@ validate_certs: description: - If C(no), SSL certificates will not be validated. This should only be used - on personally controlled sites using self-signed certificates. + on personally controlled sites. Prior to 2.0, this module would always + validate on python >= 2.7.9 and never validate on python <= 2.7.8 required: false default: 'yes' choices: ['yes', 'no'] @@ -136,8 +137,8 @@ class F5(object): api: iControl API instance. """ - def __init__(self, host, user, password, session=False): - self.api = bigsuds.BIGIP(hostname=host, username=user, password=password) + def __init__(self, host, user, password, session=False, validate_certs=True): + self.api = bigip_api(host, user, password, validate_certs) if session: self.start_session() @@ -1574,12 +1575,6 @@ def generate_software_list(f5): software_list = software.get_all_software_status() return software_list -def disable_ssl_cert_validation(): - # You probably only want to do this for testing and never in production. - # From https://www.python.org/dev/peps/pep-0476/#id29 - import ssl - ssl._create_default_https_context = ssl._create_unverified_context - def main(): module = AnsibleModule( @@ -1595,7 +1590,7 @@ def main(): ) if not bigsuds_found: - module.fail_json(msg="the python suds and bigsuds modules is required") + module.fail_json(msg="the python suds and bigsuds modules are required") server = module.params['server'] user = module.params['user'] @@ -1603,6 +1598,12 @@ def main(): validate_certs = module.params['validate_certs'] session = module.params['session'] fact_filter = module.params['filter'] + + if validate_certs: + import ssl + if not hasattr(ssl, 'SSLContext'): + module.fail_json(msg='bigsuds does not support verifying certificates with python < 2.7.9. Either update python or set validate_certs=False on the task') + if fact_filter: regex = fnmatch.translate(fact_filter) else: @@ -1617,14 +1618,11 @@ def main(): if not all(include_test): module.fail_json(msg="value of include must be one or more of: %s, got: %s" % (",".join(valid_includes), ",".join(include))) - if not validate_certs: - disable_ssl_cert_validation() - try: facts = {} if len(include) > 0: - f5 = F5(server, user, password, session) + f5 = F5(server, user, password, session, validate_certs) saved_active_folder = f5.get_active_folder() saved_recursive_query_state = f5.get_recursive_query_state() if saved_active_folder != "/": @@ -1685,6 +1683,7 @@ def main(): # include magic from lib/ansible/module_common.py from ansible.module_utils.basic import * +from ansible.module_utils.f5 import * if __name__ == '__main__': main() diff --git a/network/f5/bigip_monitor_http.py b/network/f5/bigip_monitor_http.py index ea24e995e27..9e39aadd109 100644 --- a/network/f5/bigip_monitor_http.py +++ b/network/f5/bigip_monitor_http.py @@ -54,7 +54,8 @@ validate_certs: description: - If C(no), SSL certificates will not be validated. This should only be used - on personally controlled sites using self-signed certificates. + on personally controlled sites. Prior to 2.0, this module would always + validate on python >= 2.7.9 and never validate on python <= 2.7.8 required: false default: 'yes' choices: ['yes', 'no'] @@ -333,7 +334,7 @@ def main(): # end monitor specific stuff - api = bigip_api(server, user, password) + api = bigip_api(server, user, password, validate_certs) monitor_exists = check_monitor_exists(module, api, monitor, parent) diff --git a/network/f5/bigip_monitor_tcp.py b/network/f5/bigip_monitor_tcp.py index 0900e95fd20..2dffa4509d8 100644 --- a/network/f5/bigip_monitor_tcp.py +++ b/network/f5/bigip_monitor_tcp.py @@ -52,7 +52,8 @@ validate_certs: description: - If C(no), SSL certificates will not be validated. This should only be used - on personally controlled sites using self-signed certificates. + on personally controlled sites. Prior to 2.0, this module would always + validate on python >= 2.7.9 and never validate on python <= 2.7.8 required: false default: 'yes' choices: ['yes', 'no'] @@ -356,7 +357,7 @@ def main(): # end monitor specific stuff - api = bigip_api(server, user, password) + api = bigip_api(server, user, password, validate_certs) monitor_exists = check_monitor_exists(module, api, monitor, parent) diff --git a/network/f5/bigip_node.py b/network/f5/bigip_node.py index 28eacc0d6f5..89c17e57735 100644 --- a/network/f5/bigip_node.py +++ b/network/f5/bigip_node.py @@ -57,7 +57,8 @@ validate_certs: description: - If C(no), SSL certificates will not be validated. This should only be used - on personally controlled sites using self-signed certificates. + on personally controlled sites. Prior to 2.0, this module would always + validate on python >= 2.7.9 and never validate on python <= 2.7.8 required: false default: 'yes' choices: ['yes', 'no'] @@ -290,7 +291,7 @@ def main(): module.fail_json(msg="host parameter invalid when state=absent") try: - api = bigip_api(server, user, password) + api = bigip_api(server, user, password, validate_certs) result = {'changed': False} # default if state == 'absent': diff --git a/network/f5/bigip_pool.py b/network/f5/bigip_pool.py index 1628f6c68c9..f65a13797fb 100644 --- a/network/f5/bigip_pool.py +++ b/network/f5/bigip_pool.py @@ -57,7 +57,8 @@ validate_certs: description: - If C(no), SSL certificates will not be validated. This should only be used - on personally controlled sites using self-signed certificates. + on personally controlled sites. Prior to 2.0, this module would always + validate on python >= 2.7.9 and never validate on python <= 2.7.8 required: false default: 'yes' choices: ['yes', 'no'] @@ -390,9 +391,6 @@ def main(): address = fq_name(partition,host) port = module.params['port'] - if not validate_certs: - disable_ssl_cert_validation() - # sanity check user supplied values if (host and not port) or (port and not host): @@ -421,7 +419,7 @@ def main(): module.fail_json(msg="quorum requires monitors parameter") try: - api = bigip_api(server, user, password) + api = bigip_api(server, user, password, validate_certs) result = {'changed': False} # default if state == 'absent': diff --git a/network/f5/bigip_pool_member.py b/network/f5/bigip_pool_member.py index ec2b7135372..c0337180e5b 100644 --- a/network/f5/bigip_pool_member.py +++ b/network/f5/bigip_pool_member.py @@ -50,7 +50,8 @@ validate_certs: description: - If C(no), SSL certificates will not be validated. This should only be used - on personally controlled sites using self-signed certificates. + on personally controlled sites. Prior to 2.0, this module would always + validate on python >= 2.7.9 and never validate on python <= 2.7.8 required: false default: 'yes' choices: ['yes', 'no'] @@ -347,7 +348,7 @@ def main(): module.fail_json(msg="valid ports must be in range 1 - 65535") try: - api = bigip_api(server, user, password) + api = bigip_api(server, user, password, validate_certs) if not pool_exists(api, pool): module.fail_json(msg="pool %s does not exist" % pool) result = {'changed': False} # default From 389a1265dee4c804740cf1928c24cb0661d9f670 Mon Sep 17 00:00:00 2001 From: Alexander Winkler Date: Tue, 29 Dec 2015 08:02:13 +0100 Subject: [PATCH 1067/2522] fixed inline for more compatibility --- packaging/os/pkgutil.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/packaging/os/pkgutil.py b/packaging/os/pkgutil.py index 05d0b173e2c..275fa35fafe 100644 --- a/packaging/os/pkgutil.py +++ b/packaging/os/pkgutil.py @@ -155,7 +155,11 @@ def main(): if len(out) > 75: out = out[:75] + '...' if rc != 0: - module.fail_json(msg=err if err else out) + if err: + msg = err + else: + msg = out + module.fail_json(msg=msg) elif state == 'latest': if not package_installed(module, name): @@ -165,7 +169,12 @@ def main(): if len(out) > 75: out = out[:75] + '...' if rc != 0: - module.fail_json(msg=err if err else out) + if err: + msg = err + else: + msg = out + module.fail_json(msg=msg) + else: if not package_latest(module, name, site): if module.check_mode: @@ -174,7 +183,11 @@ def main(): if len(out) > 75: out = out[:75] + '...' if rc != 0: - module.fail_json(msg=err if err else out) + if err: + msg = err + else: + msg = out + module.fail_json(msg=msg) elif state == 'absent': if package_installed(module, name): @@ -184,7 +197,11 @@ def main(): if len(out) > 75: out = out[:75] + '...' if rc != 0: - module.fail_json(msg=err if err else out) + if err: + msg = err + else: + msg = out + module.fail_json(msg=msg) if rc is None: # pkgutil was not executed because the package was already present/absent From 1dd62b13fa532b9335782f26c253495df6df1c1d Mon Sep 17 00:00:00 2001 From: Stanislav Ivchin Date: Wed, 30 Dec 2015 15:53:36 +0300 Subject: [PATCH 1068/2522] little fix: facts['path'] -> facts['project_path'] --- web_infrastructure/deploy_helper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web_infrastructure/deploy_helper.py b/web_infrastructure/deploy_helper.py index 21a98765c9e..ebf5b54a8bd 100644 --- a/web_infrastructure/deploy_helper.py +++ b/web_infrastructure/deploy_helper.py @@ -446,12 +446,12 @@ def main(): changes += deploy_helper.remove_unfinished_file(facts['new_release_path']) changes += deploy_helper.create_link(facts['new_release_path'], facts['current_path']) if deploy_helper.clean: - changes += deploy_helper.remove_unfinished_link(facts['path']) + changes += deploy_helper.remove_unfinished_link(facts['project_path']) changes += deploy_helper.remove_unfinished_builds(facts['releases_path']) changes += deploy_helper.cleanup(facts['releases_path'], facts['new_release']) elif deploy_helper.state == 'clean': - changes += deploy_helper.remove_unfinished_link(facts['path']) + changes += deploy_helper.remove_unfinished_link(facts['project_path']) changes += deploy_helper.remove_unfinished_builds(facts['releases_path']) changes += deploy_helper.cleanup(facts['releases_path'], facts['new_release']) From fa11718c0f6632dc9b1117c6887f43df6dd60186 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Fri, 1 Jan 2016 01:52:52 -0500 Subject: [PATCH 1069/2522] fixed domain_id to actually be supported also added domain as an alias alt fixes #1437 --- cloud/openstack/os_project.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cloud/openstack/os_project.py b/cloud/openstack/os_project.py index 37901f8412e..b159264ebd4 100644 --- a/cloud/openstack/os_project.py +++ b/cloud/openstack/os_project.py @@ -49,6 +49,7 @@ - Domain id to create the project in if the cloud supports domains required: false default: None + aliases: ['domain'] enabled: description: - Is the project enabled @@ -138,7 +139,7 @@ def main(): argument_spec = openstack_full_argument_spec( name=dict(required=True), description=dict(required=False, default=None), - domain=dict(required=False, default=None), + domain_id=dict(required=False, default=None, aliases=['domain']), enabled=dict(default=True, type='bool'), state=dict(default='present', choices=['absent', 'present']) ) @@ -155,7 +156,7 @@ def main(): name = module.params['name'] description = module.params['description'] - domain = module.params['domain'] + domain = module.params['domain_id'] enabled = module.params['enabled'] state = module.params['state'] From 30c5b303ced99c80bdaa5e8bc4771c5762c83a55 Mon Sep 17 00:00:00 2001 From: Jeroen Geusebroek Date: Sun, 3 Jan 2016 08:49:20 +0100 Subject: [PATCH 1070/2522] Fix for issue #1074. Now able to create volume without replica's. --- system/gluster_volume.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/system/gluster_volume.py b/system/gluster_volume.py index ff1ce9831db..ea74c25f166 100644 --- a/system/gluster_volume.py +++ b/system/gluster_volume.py @@ -353,6 +353,9 @@ def main(): if cluster != None and cluster[-1] == '': cluster = cluster[0:-1] + if cluster == None: + cluster = [myhostname] + if brick_paths != None and "," in brick_paths: brick_paths = brick_paths.split(",") else: From 3647b29bee6a6216ea1a01c433bb55da66888be6 Mon Sep 17 00:00:00 2001 From: Jeroen Geusebroek Date: Sun, 3 Jan 2016 21:09:37 +0100 Subject: [PATCH 1071/2522] Improved fix for #1074. Both None and '' transform to fqdn. --- system/gluster_volume.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/gluster_volume.py b/system/gluster_volume.py index ea74c25f166..10d50c34370 100644 --- a/system/gluster_volume.py +++ b/system/gluster_volume.py @@ -350,10 +350,10 @@ def main(): # Clean up if last element is empty. Consider that yml can look like this: # cluster="{% for host in groups['glusterfs'] %}{{ hostvars[host]['private_ip'] }},{% endfor %}" - if cluster != None and cluster[-1] == '': + if cluster != None and len(cluster) > 1 and cluster[-1] == '': cluster = cluster[0:-1] - if cluster == None: + if cluster == None or cluster[0] == '': cluster = [myhostname] if brick_paths != None and "," in brick_paths: From ffd1cac13ca873973b2a9fb857e2e94a5145368e Mon Sep 17 00:00:00 2001 From: Eric Johnson Date: Tue, 3 Nov 2015 20:49:59 +0000 Subject: [PATCH 1072/2522] Adding kubernetes module --- clustering/kubernetes.py | 449 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 449 insertions(+) create mode 100644 clustering/kubernetes.py diff --git a/clustering/kubernetes.py b/clustering/kubernetes.py new file mode 100644 index 00000000000..2b4921a0136 --- /dev/null +++ b/clustering/kubernetes.py @@ -0,0 +1,449 @@ +#!/usr/bin/python +# Copyright 2015 Google Inc. All Rights Reserved. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see + +DOCUMENTATION = ''' +--- +module: kubernetes +version_added: "2.1" +short_description: Manage Kubernetes resources. +description: + - This module can manage Kubernetes resources on an existing cluster using + the Kubernetes server API. Users can specify in-line API data, or + specify an existing Kubernetes YAML file. Currently, this module, + Only supports HTTP Basic Auth + Only supports 'strategic merge' for update, http://goo.gl/fCPYxT + SSL certs are not working, use 'validate_certs=off' to disable + This module can mimic the 'kubectl' Kubernetes client for commands + such as 'get', 'cluster-info', and 'version'. This is useful if you + want to fetch full object details for existing Kubernetes resources. +options: + api_endpoint: + description: + - The IPv4 API endpoint of the Kubernetes cluster. + required: true + default: null + aliases: ["endpoint"] + inline_data: + description: + - The Kubernetes YAML data to send to the API I(endpoint). + required: true + default: null + file_reference: + description: + - Specify full path to a Kubernets YAML file to send to API I(endpoint). + required: false + default: null + certificate_authority_data: + description: + - Certificate Authority data for Kubernetes server. Should be in either + standard PEM format or base64 encoded PEM data. Note that certificate + verification is broken until ansible supports a version of + 'match_hostname' that can match the IP address against the CA data. + required: false + default: null + kubectl_api_versions: + description: + - Mimic the 'kubectl api-versions' command, values are ignored. + required: false + default: null + kubectl_cluster_info: + description: + - Mimic the 'kubectl cluster-info' command, values are ignored. + required: false + default: null + kubectl_get: + description: + - Mimic the 'kubectl get' command. Specify the object(s) to fetch such + as 'pods' or 'replicationcontrollers/mycontroller'. It does not + support shortcuts (e.g. 'po', 'rc', 'svc'). + required: false + default: null + kubectl_namespace: + description: + - Specify the namespace to use for 'kubectl' commands. + required: false + default: "default" + kubectl_version: + description: + - Mimic the 'kubectl version' command, values are ignored. + required: false + default: null + state: + description: + - The desired action to take on the Kubernetes data, or 'kubectl' to + mimic some kubectl commands. + required: true + default: "present" + choices: ["present", "post", "absent", "delete", "update", "patch", + "replace", "put", "kubectl"] + url_password: + description: + - The HTTP Basic Auth password for the API I(endpoint). + required: true + default: null + aliases: ["password", "api_password"] + url_username: + description: + - The HTTP Basic Auth username for the API I(endpoint). + required: true + default: "admin" + aliases: ["username", "api_username"] + validate_certs: + description: + - Enable/disable certificate validation. Note that this is set to + C(false) until Ansible can support IP address based certificate + hostname matching (exists in >= python3.5.0). + required: false + default: false + choices: BOOLEANS + +author: "Eric Johnson (@erjohnso) " +''' + +EXAMPLES = ''' +# Create a new namespace with in-line YAML. +- name: Create a kubernetes namespace + kubernetes: + api_endpoint: 123.45.67.89 + url_username: admin + url_password: redacted + inline_data: + kind: Namespace + apiVersion: v1 + metadata: + name: ansible-test + labels: + label_env: production + label_ver: latest + annotations: + a1: value1 + a2: value2 + state: present + +# Create a new namespace from a YAML file. +- name: Create a kubernetes namespace + kubernetes: + api_endpoint: 123.45.67.89 + url_username: admin + url_password: redacted + file_reference: /path/to/create_namespace.yaml + state: present + +# Fetch info about the Kubernets cluster with a fake 'kubectl' command. +- name: Look up cluster info + kubernetes: + api_endpoint: 123.45.67.89 + url_username: admin + url_password: redacted + kubectl_cluster_info: 1 + state: kubectl + +# Fetch info about the Kubernets pods with a fake 'kubectl' command. +- name: Look up pods + kubernetes: + api_endpoint: 123.45.67.89 + url_username: admin + url_password: redacted + kubectl_get: pods + state: kubectl +''' + +RETURN = ''' +# Example response from creating a Kubernetes Namespace. +api_response: + description: Raw response from Kubernetes API, content varies with API. + returned: success + type: dictionary + contains: + apiVersion: "v1" + kind: "Namespace" + metadata: + creationTimestamp: "2016-01-04T21:16:32Z" + name: "test-namespace" + resourceVersion: "509635" + selfLink: "/api/v1/namespaces/test-namespace" + uid: "6dbd394e-b328-11e5-9a02-42010af0013a" + spec: + finalizers: + - kubernetes + status: + phase: "Active" +''' + +import yaml +import base64 + +############################################################################ +############################################################################ +# For API coverage, this Anislbe module provides capability to operate on +# all Kubernetes objects that support a "create" call (except for 'Events'). +# In order to obtain a valid list of Kubernetes objects, the v1 spec file +# was referenced and the below python script was used to parse the JSON +# spec file, extract only the objects with a description starting with +# 'create a'. The script then iterates over all of these base objects +# to get the endpoint URL and was used to generate the KIND_URL map. +# +# import json +# from urllib2 import urlopen +# +# r = urlopen("https://raw.githubusercontent.com/kubernetes" +# "/kubernetes/master/api/swagger-spec/v1.json") +# v1 = json.load(r) +# +# apis = {} +# for a in v1['apis']: +# p = a['path'] +# for o in a['operations']: +# if o["summary"].startswith("create a") and o["type"] != "v1.Event": +# apis[o["type"]] = p +# +# def print_kind_url_map(): +# results = [] +# for a in apis.keys(): +# results.append('"%s": "%s"' % (a[3:].lower(), apis[a])) +# results.sort() +# print "KIND_URL = {" +# print ",\n".join(results) +# print "}" +# +# if __name__ == '__main__': +# print_kind_url_map() +############################################################################ +############################################################################ + +KIND_URL = { + "binding": "/api/v1/namespaces/{namespace}/bindings", + "endpoints": "/api/v1/namespaces/{namespace}/endpoints", + "limitrange": "/api/v1/namespaces/{namespace}/limitranges", + "namespace": "/api/v1/namespaces", + "node": "/api/v1/nodes", + "persistentvolume": "/api/v1/persistentvolumes", + "persistentvolumeclaim": "/api/v1/namespaces/{namespace}/persistentvolumeclaims", # NOQA + "pod": "/api/v1/namespaces/{namespace}/pods", + "podtemplate": "/api/v1/namespaces/{namespace}/podtemplates", + "replicationcontroller": "/api/v1/namespaces/{namespace}/replicationcontrollers", # NOQA + "resourcequota": "/api/v1/namespaces/{namespace}/resourcequotas", + "secret": "/api/v1/namespaces/{namespace}/secrets", + "service": "/api/v1/namespaces/{namespace}/services", + "serviceaccount": "/api/v1/namespaces/{namespace}/serviceaccounts" +} +USER_AGENT = "ansible-k8s-module/0.0.1" + + +# TODO(erjohnso): SSL Certificate validation is currently unsupported. +# It can be made to work when the following are true: +# - Ansible consistently uses a "match_hostname" that supports IP Address +# matching. This is now true in >= python3.5.0. Currently, this feature +# is not yet available in backports.ssl_match_hostname (still 3.4). +# - Ansible allows passing in the self-signed CA cert that is created with +# a kubernetes master. The lib/ansible/module_utils/urls.py method, +# SSLValidationHandler.get_ca_certs() needs a way for the Kubernetes +# CA cert to be passed in and included in the generated bundle file. +# When this is fixed, the following changes can be made to this module, +# - Remove the 'return' statement in line 254 below +# - Set 'required=true' for certificate_authority_data and ensure that +# ansible's SSLValidationHandler.get_ca_certs() can pick up this CA cert +# - Set 'required=true' for the validate_certs param. +def decode_cert_data(module): + return + d = module.params.get("certificate_authority_data") + if d and not d.startswith("-----BEGIN"): + module.params["certificate_authority_data"] = base64.b64decode(d) + + +def api_request(module, url, method="GET", headers=None, data=None): + body = None + if data: + data = json.dumps(data) + response, info = fetch_url(module, url, method=method, headers=headers, + data=data) + if response is not None: + body = json.loads(response.read()) + return info, body + + +def k8s_kubectl_get(module, url): + req = module.params.get("kubectl_get") + info, body = api_request(module, url + "/" + req) + return False, body + + +def k8s_delete_resource(module, url, data): + name = None + if 'metadata' in data: + name = data['metadata'].get('name') + if name is None: + module.fail_json(msg="Missing a named resource in object metadata") + url = url + '/' + name + + info, body = api_request(module, url, method="DELETE") + if info['status'] == 404: + return False, {} + if info['status'] == 200: + return True, body + module.fail_json(msg="%s: fetching URL '%s'" % (info['msg'], url)) + + +def k8s_create_resource(module, url, data): + info, body = api_request(module, url, method="POST", data=data) + if info['status'] == 409: + name = data["metadata"].get("name", None) + info, body = api_request(module, url + "/" + name) + return False, body + return True, body + + +def k8s_replace_resource(module, url, data): + name = None + if 'metadata' in data: + name = data['metadata'].get('name') + if name is None: + module.fail_json(msg="Missing a named resource in object metadata") + url = url + '/' + name + + info, body = api_request(module, url, method="PUT", data=data) + if info['status'] == 409: + name = data["metadata"].get("name", None) + info, body = api_request(module, url + "/" + name) + return False, body + return True, body + + +def k8s_update_resource(module, url, data): + name = None + if 'metadata' in data: + name = data['metadata'].get('name') + if name is None: + module.fail_json(msg="Missing a named resource in object metadata") + url = url + '/' + name + + headers = {"Content-Type": "application/strategic-merge-patch+json"} + info, body = api_request(module, url, method="PATCH", data=data, + headers=headers) + if info['status'] == 409: + name = data["metadata"].get("name", None) + info, body = api_request(module, url + "/" + name) + return False, body + return True, body + + +def main(): + module = AnsibleModule( + argument_spec=dict( + http_agent=dict(default=USER_AGENT), + + url_username=dict(default="admin"), + url_password=dict(required=True, no_log=True), + force_basic_auth=dict(default="yes"), + validate_certs=dict(default=False, choices=BOOLEANS), + certificate_authority_data=dict(required=False), + + # fake 'kubectl' commands + kubectl_api_versions=dict(required=False), + kubectl_cluster_info=dict(required=False), + kubectl_get=dict(required=False), + kubectl_namespace=dict(required=False, default="default"), + kubectl_version=dict(required=False), + + # k8s API module variables + api_endpoint=dict(required=True), + file_reference=dict(required=False), + inline_data=dict(required=False), + state=dict(default="present", + choices=["present", "post", + "absent", "delete", + "update", "put", + "replace", "patch", + "kubectl"]) + ) + ) + + decode_cert_data(module) + + changed = False + data = module.params.get('inline_data', {}) + if not data: + dfile = module.params.get('file_reference') + if dfile: + f = open(dfile, "r") + data = yaml.load(f) + + endpoint = "https://" + module.params.get('api_endpoint') + url = endpoint + + namespace = "default" + if data and 'metadata' in data: + namespace = data['metadata'].get('namespace', "default") + kind = data['kind'].lower() + url = endpoint + KIND_URL[kind] + url = url.replace("{namespace}", namespace) + + # check for 'kubectl' commands + kubectl_api_versions = module.params.get('kubectl_api_versions') + kubectl_cluster_info = module.params.get('kubectl_cluster_info') + kubectl_get = module.params.get('kubectl_get') + kubectl_namespace = module.params.get('kubectl_namespace') + kubectl_version = module.params.get('kubectl_version') + + state = module.params.get('state') + if state in ['present', 'post']: + changed, body = k8s_create_resource(module, url, data) + module.exit_json(changed=changed, api_response=body) + + if state in ['absent', 'delete']: + changed, body = k8s_delete_resource(module, url, data) + module.exit_json(changed=changed, api_response=body) + + if state in ['replace', 'put']: + changed, body = k8s_replace_resource(module, url, data) + module.exit_json(changed=changed, api_response=body) + + if state in ['update', 'patch']: + changed, body = k8s_update_resource(module, url, data) + module.exit_json(changed=changed, api_response=body) + + if state == 'kubectl': + kurl = url + "/api/v1/namespaces/" + kubectl_namespace + if kubectl_get: + if kubectl_get.startswith("namespaces"): + kurl = url + "/api/v1" + changed, body = k8s_kubectl_get(module, kurl) + module.exit_json(changed=changed, api_response=body) + if kubectl_version: + info, body = api_request(module, url + "/version") + module.exit_json(changed=False, api_response=body) + if kubectl_api_versions: + info, body = api_request(module, url + "/api") + module.exit_json(changed=False, api_response=body) + if kubectl_cluster_info: + info, body = api_request(module, url + + "/api/v1/namespaces/kube-system" + "/services?labelSelector=kubernetes" + ".io/cluster-service=true") + module.exit_json(changed=False, api_response=body) + + module.fail_json(msg="Invalid state: '%s'" % state) + + +# import module snippets +from ansible.module_utils.basic import * # NOQA +from ansible.module_utils.urls import * # NOQA + + +if __name__ == '__main__': + main() From 6f4aaecb6ac8369c1c7da265e71abe27b2c303ba Mon Sep 17 00:00:00 2001 From: Charles Paul Date: Tue, 22 Dec 2015 09:59:12 -0600 Subject: [PATCH 1073/2522] adding no_log to password --- cloud/vmware/vsphere_copy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/vmware/vsphere_copy.py b/cloud/vmware/vsphere_copy.py index 77f147f17a8..b51efa2d777 100644 --- a/cloud/vmware/vsphere_copy.py +++ b/cloud/vmware/vsphere_copy.py @@ -102,7 +102,7 @@ def main(): argument_spec = dict( host = dict(required=True, aliases=[ 'hostname' ]), login = dict(required=True, aliases=[ 'username' ]), - password = dict(required=True), + password = dict(required=True, no_log=True), src = dict(required=True, aliases=[ 'name' ]), datacenter = dict(required=True), datastore = dict(required=True), From 2a937d20cf3ab97e0d4feca2c0725937d5d62cde Mon Sep 17 00:00:00 2001 From: Charles Paul Date: Tue, 5 Jan 2016 14:58:06 -0600 Subject: [PATCH 1074/2522] make dest use path type path type --- packaging/language/maven_artifact.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/language/maven_artifact.py b/packaging/language/maven_artifact.py index c66a2dcfeee..fe311ea53de 100644 --- a/packaging/language/maven_artifact.py +++ b/packaging/language/maven_artifact.py @@ -301,7 +301,7 @@ def main(): username = dict(default=None), password = dict(default=None), state = dict(default="present", choices=["present","absent"]), # TODO - Implement a "latest" state - dest = dict(default=None), + dest = dict(type="path", default=None), validate_certs = dict(required=False, default=True, type='bool'), ) ) From 3de4f10bb5ebb7dc71d85ffa653e7d0ba894764a Mon Sep 17 00:00:00 2001 From: Casey Lang Date: Tue, 5 Jan 2016 18:22:38 -0600 Subject: [PATCH 1075/2522] Fix puppet module formatting issue The `->` in the `show_diff` option doc seemed to be causing the docs page to break. Not sure why, since it was still valid YAML. --- system/puppet.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/system/puppet.py b/system/puppet.py index 98b09bb3f90..090d6f1d2aa 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -49,9 +49,7 @@ default: None show_diff: description: - - > - Should puppet return diffs of changes applied. Defaults to off to - avoid leaking secret changes by default. + - Should puppet return diffs of changes applied. Defaults to off to avoid leaking secret changes by default. required: false default: no choices: [ "yes", "no" ] From a58b847d5c43d6df36c8b8e4382dcc14ac266ac0 Mon Sep 17 00:00:00 2001 From: Jonathan Mainguy Date: Wed, 6 Jan 2016 09:49:02 -0500 Subject: [PATCH 1076/2522] Remove advertising show_diff feature --diff will be added to more modules soon, and we want puppet module to utilize this instead of show_diff --- system/puppet.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/system/puppet.py b/system/puppet.py index 090d6f1d2aa..2a70da3cea1 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -47,12 +47,6 @@ - Path to the manifest file to run puppet apply on. required: false default: None - show_diff: - description: - - Should puppet return diffs of changes applied. Defaults to off to avoid leaking secret changes by default. - required: false - default: no - choices: [ "yes", "no" ] facts: description: - A dict of values to pass in as persistent external facter facts @@ -113,7 +107,7 @@ def main(): puppetmaster=dict(required=False, default=None), manifest=dict(required=False, default=None), show_diff=dict( - default=False, aliases=['show-diff'], type='bool'), + default=False, aliases=['show-diff'], type='bool'), # internal code to work with --diff, do not use facts=dict(default=None), facter_basename=dict(default='ansible'), environment=dict(required=False, default=None), From 6bad06e8272a1e2cf624b651cf0523a7e714bece Mon Sep 17 00:00:00 2001 From: Stepan Stipl Date: Thu, 7 Jan 2016 18:06:35 +0000 Subject: [PATCH 1077/2522] Fix: route53_facts hosted_zone_id boto error Boto is expecting parameter called "Id", not "HostedZoneId". See http://boto3.readthedocs.org/en/latest/reference/services/route53.html#Route53.Client.get_hosted_zone Fixes ansible/ansible-modules-extras/#1465 --- cloud/amazon/route53_facts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/route53_facts.py b/cloud/amazon/route53_facts.py index d6081dba4da..70d7fd3037a 100644 --- a/cloud/amazon/route53_facts.py +++ b/cloud/amazon/route53_facts.py @@ -178,7 +178,7 @@ def get_hosted_zone(client, module): params = dict() if module.params.get('hosted_zone_id'): - params['HostedZoneId'] = module.params.get('hosted_zone_id') + params['Id'] = module.params.get('hosted_zone_id') else: module.fail_json(msg="Hosted Zone Id is required") From 53e6e8c93600a77d8d7c537969e4e15d61cacb91 Mon Sep 17 00:00:00 2001 From: Constantin07 Date: Fri, 8 Jan 2016 11:55:40 +0000 Subject: [PATCH 1078/2522] Print explicit error cause when no ELBs are found in AWS --- cloud/amazon/ec2_elb_facts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_elb_facts.py b/cloud/amazon/ec2_elb_facts.py index 4289ef7a232..172efe7326e 100644 --- a/cloud/amazon/ec2_elb_facts.py +++ b/cloud/amazon/ec2_elb_facts.py @@ -157,7 +157,7 @@ def list_elb(connection, module): try: all_elbs = connection.get_all_load_balancers(elb_names) except BotoServerError as e: - module.fail_json(msg=get_error_message(e.args[2])) + module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message)) elb_array = [] for elb in all_elbs: From 12bf504173f667b816284b69cc502d9a9736c496 Mon Sep 17 00:00:00 2001 From: "Jose A. Rivera" Date: Sun, 10 Jan 2016 12:45:32 -0600 Subject: [PATCH 1079/2522] gluster_volume: allow probing ourselves We should allow "gluster peer probe" to determine if a given "host" maps to the localhost, and detect that case accordingly. --- system/gluster_volume.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/system/gluster_volume.py b/system/gluster_volume.py index 10d50c34370..68c23d438ce 100644 --- a/system/gluster_volume.py +++ b/system/gluster_volume.py @@ -249,8 +249,8 @@ def wait_for_peer(host): def probe(host, myhostname): global module - run_gluster([ 'peer', 'probe', host ]) - if not wait_for_peer(host): + out = run_gluster([ 'peer', 'probe', host ]) + if not out.find('localhost') and not wait_for_peer(host): module.fail_json(msg='failed to probe peer %s on %s' % (host, myhostname)) changed = True @@ -258,9 +258,7 @@ def probe_all_peers(hosts, peers, myhostname): for host in hosts: host = host.strip() # Clean up any extra space for exact comparison if host not in peers: - # dont probe ourselves - if myhostname != host: - probe(host, myhostname) + probe(host, myhostname) def create_volume(name, stripe, replica, transport, hosts, bricks, force): args = [ 'volume', 'create' ] From c8e35023a47e8cc23e2c00ed5f482941ab0563e7 Mon Sep 17 00:00:00 2001 From: Mo Date: Sun, 10 Jan 2016 23:18:09 +0100 Subject: [PATCH 1080/2522] firewalld: add/remove interfaces to/from zones --- system/firewalld.py | 58 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/system/firewalld.py b/system/firewalld.py index 171cfa5b77e..4f610be3c20 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -47,6 +47,11 @@ required: false default: null version_added: "2.0" + interface: + description: + - 'The interface you would like to add/remove to/from a zone in firewalld - zone must be specified' + required: false + default: null zone: description: - 'The firewalld zone to add/remove to/from (NOTE: default zone can be configured per system but "public" is default from upstream. Available choices can be extended based on per-system configs, listed here are "out of the box" defaults).' @@ -87,6 +92,7 @@ - firewalld: zone=dmz service=http permanent=true state=enabled - firewalld: rich_rule='rule service name="ftp" audit limit value="1/m" accept' permanent=true state=enabled - firewalld: source='192.168.1.0/24' zone=internal state=enabled +- firewalld: zone=trusted interface=eth2 permanent=true state=enabled ''' import os @@ -161,6 +167,29 @@ def remove_source(zone, source): fw_settings.removeSource(source) fw_zone.update(fw_settings) +#################### +# interface handling +# +def get_interface(zone, interface): + fw_zone = fw.config().getZoneByName(zone) + fw_settings = fw_zone.getSettings() + if interface in fw_settings.getInterfaces(): + return True + else: + return False + +def add_interface(zone, interface): + fw_zone = fw.config().getZoneByName(zone) + fw_settings = fw_zone.getSettings() + fw_settings.addInterface(interface) + fw_zone.update(fw_settings) + +def remove_interface(zone, interface): + fw_zone = fw.config().getZoneByName(zone) + fw_settings = fw_zone.getSettings() + fw_settings.removeInterface(interface) + fw_zone.update(fw_settings) + #################### # service handling # @@ -252,12 +281,16 @@ def main(): permanent=dict(type='bool',required=False,default=None), state=dict(choices=['enabled', 'disabled'], required=True), timeout=dict(type='int',required=False,default=0), + interface=dict(required=False,default=None), ), supports_check_mode=True ) if module.params['source'] == None and module.params['permanent'] == None: module.fail(msg='permanent is a required parameter') + if module.params['interface'] != None and module.params['zone'] == None: + module.fail(msg='zone is a required parameter') + if not HAS_FIREWALLD: module.fail_json(msg='firewalld and its python 2 module are required for this module') @@ -288,6 +321,7 @@ def main(): desired_state = module.params['state'] immediate = module.params['immediate'] timeout = module.params['timeout'] + interface = module.params['interface'] ## Check for firewalld running try: @@ -304,9 +338,11 @@ def main(): modification_count += 1 if rich_rule != None: modification_count += 1 + if interface != None: + modification_count += 1 if modification_count > 1: - module.fail_json(msg='can only operate on port, service or rich_rule at once') + module.fail_json(msg='can only operate on port, service, rich_rule or interface at once') if service != None: if permanent: @@ -368,6 +404,7 @@ def main(): remove_source(zone, source) changed=True msgs.append("Removed %s from zone %s" % (source, zone)) + if port != None: if permanent: is_enabled = get_port_enabled_permanent(zone, [port, protocol]) @@ -451,6 +488,25 @@ def main(): if changed == True: msgs.append("Changed rich_rule %s to %s" % (rich_rule, desired_state)) + if interface != None: + is_enabled = get_interface(zone, interface) + if desired_state == "enabled": + if is_enabled == False: + if module.check_mode: + module.exit_json(changed=True) + + add_interface(zone, interface) + changed=True + msgs.append("Added %s to zone %s" % (interface, zone)) + elif desired_state == "disabled": + if is_enabled == True: + if module.check_mode: + module.exit_json(changed=True) + + remove_interface(zone, interface) + changed=True + msgs.append("Removed %s from zone %s" % (interface, zone)) + module.exit_json(changed=changed, msg=', '.join(msgs)) From 11fdb822a4814ecc082d5bf359a63e0c2fcb80e6 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Wed, 6 Jan 2016 23:49:45 +0100 Subject: [PATCH 1081/2522] cloudstack: fixes and improvements cs_instance: fix case insensitivity cs_instance: remove duplicate code block cs_securitygroup_rule: fix KeyError on older ACS --- cloud/cloudstack/cs_instance.py | 8 +------- cloud/cloudstack/cs_securitygroup_rule.py | 6 ++++-- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 7b1eeafd4b0..73deb028be2 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -499,7 +499,7 @@ def get_instance(self): instances = self.cs.listVirtualMachines(**args) if instances: for v in instances['virtualmachine']: - if instance_name in [ v['name'], v['displayname'], v['id'] ]: + if instance_name.lower() in [ v['name'].lower(), v['displayname'].lower(), v['id'] ]: self.instance = v break return self.instance @@ -667,12 +667,6 @@ def update_instance(self, instance, start_vm=True): if self.module.params.get('ssh_key'): args_ssh_key['keypair'] = self.module.params.get('ssh_key') - # SSH key data - args_ssh_key = {} - args_ssh_key['id'] = instance['id'] - args_ssh_key['projectid'] = self.get_project(key='id') - if self.module.params.get('ssh_key'): - args_ssh_key['keypair'] = self.module.params.get('ssh_key') if self._has_changed(args_service_offering, instance) or \ self._has_changed(args_instance_update, instance) or \ diff --git a/cloud/cloudstack/cs_securitygroup_rule.py b/cloud/cloudstack/cs_securitygroup_rule.py index a088c6c2c1e..2a451933a01 100644 --- a/cloud/cloudstack/cs_securitygroup_rule.py +++ b/cloud/cloudstack/cs_securitygroup_rule.py @@ -309,14 +309,16 @@ def add_rule(self): res = None sg_type = self.module.params.get('type') if sg_type == 'ingress': - rule = self._get_rule(security_group['ingressrule']) + if 'ingressrule' in security_group: + rule = self._get_rule(security_group['ingressrule']) if not rule: self.result['changed'] = True if not self.module.check_mode: res = self.cs.authorizeSecurityGroupIngress(**args) elif sg_type == 'egress': - rule = self._get_rule(security_group['egressrule']) + if 'egressrule' in security_group: + rule = self._get_rule(security_group['egressrule']) if not rule: self.result['changed'] = True if not self.module.check_mode: From 6797c42821f57513f63dbfcd26bc9408c93929fe Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 11 Jan 2016 11:34:44 -0800 Subject: [PATCH 1082/2522] Update the version_added to 2.1 --- network/f5/bigip_virtual_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/f5/bigip_virtual_server.py b/network/f5/bigip_virtual_server.py index f7e243c5d6e..2ead3992bf6 100644 --- a/network/f5/bigip_virtual_server.py +++ b/network/f5/bigip_virtual_server.py @@ -24,7 +24,7 @@ short_description: "Manages F5 BIG-IP LTM virtual servers" description: - "Manages F5 BIG-IP LTM virtual servers via iControl SOAP API" -version_added: "2.0" +version_added: "2.1" author: Etienne Carriere (@Etienne-Carriere) notes: - "Requires BIG-IP software version >= 11" From 4ef40ab421e941be9bbb71f909c8b654a9357a6c Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 11 Jan 2016 12:56:43 -0800 Subject: [PATCH 1083/2522] Fix documentation build --- network/f5/bigip_virtual_server.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/network/f5/bigip_virtual_server.py b/network/f5/bigip_virtual_server.py index 2ead3992bf6..5746e2c4fb4 100644 --- a/network/f5/bigip_virtual_server.py +++ b/network/f5/bigip_virtual_server.py @@ -58,9 +58,9 @@ required: false default: present description: - - Absent : delete the VS if present - - present (and its synonym enabled) : create if needed the VS and set state to enabled - - absent : create if needed the VS and set state to disabled + - Absent, delete the VS if present + - present (and its synonym enabled), create if needed the VS and set state to enabled + - disabled, create if needed the VS and set state to disabled choices: ['present', 'absent', 'enabled', 'disabled'] aliases: [] partition: @@ -72,7 +72,7 @@ description: - "Virtual server name." required: true - aliases: ['vs'] + aliases: ['vs'] destination: description: - "Destination IP of the virtual server (only host is currently supported) . Required when state=present and vs does not exist." From 39c3004337b688cd44e711f6eeeb9bc161f0d318 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 11 Jan 2016 13:05:51 -0800 Subject: [PATCH 1084/2522] Add a returns section for bigip_virtual-server --- network/f5/bigip_virtual_server.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/network/f5/bigip_virtual_server.py b/network/f5/bigip_virtual_server.py index 5746e2c4fb4..7ee865fe9a2 100644 --- a/network/f5/bigip_virtual_server.py +++ b/network/f5/bigip_virtual_server.py @@ -157,6 +157,14 @@ name: myvirtualserver ''' +RETURN = ''' +--- +deleted: + description: Name of a virtual server that was deleted + returned: virtual server was successfully deleted on state=absent + type: string +''' + # ========================== # bigip_virtual_server module specific From d6af6f8477d3d1600f3907d4ec1b216c94e67d52 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 11 Jan 2016 12:47:21 -0800 Subject: [PATCH 1085/2522] Update for modules which import json. Some do not use the json module directly so don't need import json. Some needed to fallback to simplejson with no traceback if neither was installed Fixes #1298 --- cloud/amazon/ecs_task.py | 3 +-- cloud/amazon/ecs_taskdefinition.py | 3 +-- cloud/amazon/route53_facts.py | 1 - cloud/vmware/vca_nat.py | 1 - clustering/consul.py | 5 ----- clustering/consul_kv.py | 5 ----- database/misc/riak.py | 7 ++++++- monitoring/boundary_meter.py | 10 +++++++++- monitoring/sensu_check.py | 14 +++++++++----- monitoring/stackdriver.py | 10 ++++++++-- monitoring/uptimerobot.py | 10 +++++++++- network/ipify_facts.py | 7 ++++++- packaging/language/composer.py | 7 ++++++- packaging/language/npm.py | 7 ++++++- packaging/os/pacman.py | 1 - packaging/os/pkgin.py | 1 - packaging/os/pkgng.py | 1 - packaging/os/portinstall.py | 1 - packaging/os/urpmi.py | 1 - source_control/github_hooks.py | 10 +++++++++- system/puppet.py | 7 ++++++- web_infrastructure/jira.py | 10 +++++++++- 22 files changed, 85 insertions(+), 37 deletions(-) diff --git a/cloud/amazon/ecs_task.py b/cloud/amazon/ecs_task.py index 000ce68b56c..c2bd73751ad 100644 --- a/cloud/amazon/ecs_task.py +++ b/cloud/amazon/ecs_task.py @@ -98,7 +98,6 @@ sample: "TODO: include sample" ''' try: - import json import boto import botocore HAS_BOTO = True @@ -123,7 +122,7 @@ def __init__(self, module): module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) except boto.exception.NoAuthHandlerFound, e: - self.module.fail_json(msg="Can't authorize connection - "+str(e)) + module.fail_json(msg="Can't authorize connection - "+str(e)) def list_tasks(self, cluster_name, service_name, status): response = self.ecs.list_tasks( diff --git a/cloud/amazon/ecs_taskdefinition.py b/cloud/amazon/ecs_taskdefinition.py index 50205d6691c..6ad23a88f86 100644 --- a/cloud/amazon/ecs_taskdefinition.py +++ b/cloud/amazon/ecs_taskdefinition.py @@ -95,7 +95,6 @@ type: dict inputs plus revision, status, taskDefinitionArn ''' try: - import json import boto import botocore HAS_BOTO = True @@ -120,7 +119,7 @@ def __init__(self, module): module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) except boto.exception.NoAuthHandlerFound, e: - self.module.fail_json(msg="Can't authorize connection - "+str(e)) + module.fail_json(msg="Can't authorize connection - "+str(e)) def describe_task(self, task_name): try: diff --git a/cloud/amazon/route53_facts.py b/cloud/amazon/route53_facts.py index d6081dba4da..40bcea73e55 100644 --- a/cloud/amazon/route53_facts.py +++ b/cloud/amazon/route53_facts.py @@ -160,7 +160,6 @@ ''' try: - import json import boto import botocore HAS_BOTO = True diff --git a/cloud/vmware/vca_nat.py b/cloud/vmware/vca_nat.py index 88fc24a20fc..2a464673e56 100644 --- a/cloud/vmware/vca_nat.py +++ b/cloud/vmware/vca_nat.py @@ -130,7 +130,6 @@ ''' import time -import json import xmltodict VALID_RULE_KEYS = ['rule_type', 'original_ip', 'original_port', diff --git a/clustering/consul.py b/clustering/consul.py index 609dce89227..627f7fb66af 100644 --- a/clustering/consul.py +++ b/clustering/consul.py @@ -190,11 +190,6 @@ import sys -try: - import json -except ImportError: - import simplejson as json - try: import consul from requests.exceptions import ConnectionError diff --git a/clustering/consul_kv.py b/clustering/consul_kv.py index bb7dea3ad39..b61c0ee1841 100644 --- a/clustering/consul_kv.py +++ b/clustering/consul_kv.py @@ -122,11 +122,6 @@ import sys -try: - import json -except ImportError: - import simplejson as json - try: import consul from requests.exceptions import ConnectionError diff --git a/database/misc/riak.py b/database/misc/riak.py index 453e6c15f3e..1f1cd11e922 100644 --- a/database/misc/riak.py +++ b/database/misc/riak.py @@ -100,10 +100,15 @@ import time import socket import sys + try: import json except ImportError: - import simplejson as json + try: + import simplejson as json + except ImportError: + # Let snippet from module_utils/basic.py return a proper error in this case + pass def ring_check(module, riak_admin_bin): diff --git a/monitoring/boundary_meter.py b/monitoring/boundary_meter.py index 99cb74f870d..ef681704f04 100644 --- a/monitoring/boundary_meter.py +++ b/monitoring/boundary_meter.py @@ -22,7 +22,15 @@ along with Ansible. If not, see . """ -import json +try: + import json +except ImportError: + try: + import simplejson as json + except ImportError: + # Let snippet from module_utils/basic.py return a proper error in this case + pass + import datetime import base64 import os diff --git a/monitoring/sensu_check.py b/monitoring/sensu_check.py index 9a004d372e0..09edae63813 100644 --- a/monitoring/sensu_check.py +++ b/monitoring/sensu_check.py @@ -174,16 +174,20 @@ sensu_check: name=check_disk_capacity state=absent ''' +try: + import json +except ImportError: + try: + import simplejson as json + except ImportError: + # Let snippet from module_utils/basic.py return a proper error in this case + pass + def sensu_check(module, path, name, state='present', backup=False): changed = False reasons = [] - try: - import json - except ImportError: - import simplejson as json - stream = None try: try: diff --git a/monitoring/stackdriver.py b/monitoring/stackdriver.py index 7b3688cbefc..25af77ec26e 100644 --- a/monitoring/stackdriver.py +++ b/monitoring/stackdriver.py @@ -92,10 +92,16 @@ # =========================================== # Stackdriver module specific support methods. # + try: - import json + import json except ImportError: - import simplejson as json + try: + import simplejson as json + except ImportError: + # Let snippet from module_utils/basic.py return a proper error in this case + pass + def send_deploy_event(module, key, revision_id, deployed_by='Ansible', deployed_to=None, repository=None): """Send a deploy event to Stackdriver""" diff --git a/monitoring/uptimerobot.py b/monitoring/uptimerobot.py index bdff8f1f134..65d963cda6d 100644 --- a/monitoring/uptimerobot.py +++ b/monitoring/uptimerobot.py @@ -64,7 +64,15 @@ ''' -import json +try: + import json +except ImportError: + try: + import simplejson as json + except ImportError: + # Let snippet from module_utils/basic.py return a proper error in this case + pass + import urllib import time diff --git a/network/ipify_facts.py b/network/ipify_facts.py index 8f509dd278a..95bf549be92 100644 --- a/network/ipify_facts.py +++ b/network/ipify_facts.py @@ -59,7 +59,12 @@ try: import json except ImportError: - import simplejson as json + try: + import simplejson as json + except ImportError: + # Let snippet from module_utils/basic.py return a proper error in this case + pass + class IpifyFacts(object): diff --git a/packaging/language/composer.py b/packaging/language/composer.py index 95b0eb3a940..5d1ec7b1014 100644 --- a/packaging/language/composer.py +++ b/packaging/language/composer.py @@ -128,7 +128,12 @@ try: import json except ImportError: - import simplejson as json + try: + import simplejson as json + except ImportError: + # Let snippet from module_utils/basic.py return a proper error in this case + pass + def parse_out(string): return re.sub("\s+", " ", string).strip() diff --git a/packaging/language/npm.py b/packaging/language/npm.py index a52b7599d80..43fa1f325ff 100644 --- a/packaging/language/npm.py +++ b/packaging/language/npm.py @@ -107,7 +107,12 @@ try: import json except ImportError: - import simplejson as json + try: + import simplejson as json + except ImportError: + # Let snippet from module_utils/basic.py return a proper error in this case + pass + class Npm(object): def __init__(self, module, **kwargs): diff --git a/packaging/os/pacman.py b/packaging/os/pacman.py index 1f955fa269e..7aa5bf45eaf 100644 --- a/packaging/os/pacman.py +++ b/packaging/os/pacman.py @@ -109,7 +109,6 @@ - pacman: name=baz state=absent force=yes ''' -import json import shlex import os import re diff --git a/packaging/os/pkgin.py b/packaging/os/pkgin.py index 0f2714b6c74..cdba6a9218b 100644 --- a/packaging/os/pkgin.py +++ b/packaging/os/pkgin.py @@ -63,7 +63,6 @@ ''' -import json import shlex import os import sys diff --git a/packaging/os/pkgng.py b/packaging/os/pkgng.py index 0eafcb6d00b..ad097aae0df 100644 --- a/packaging/os/pkgng.py +++ b/packaging/os/pkgng.py @@ -85,7 +85,6 @@ ''' -import json import shlex import os import re diff --git a/packaging/os/portinstall.py b/packaging/os/portinstall.py index b4e3044167e..a5d0e510978 100644 --- a/packaging/os/portinstall.py +++ b/packaging/os/portinstall.py @@ -58,7 +58,6 @@ ''' -import json import shlex import os import sys diff --git a/packaging/os/urpmi.py b/packaging/os/urpmi.py index d344f2e7c5c..0b9ec929316 100644 --- a/packaging/os/urpmi.py +++ b/packaging/os/urpmi.py @@ -73,7 +73,6 @@ ''' -import json import shlex import os import sys diff --git a/source_control/github_hooks.py b/source_control/github_hooks.py index d75fcb1573d..9f664875587 100644 --- a/source_control/github_hooks.py +++ b/source_control/github_hooks.py @@ -18,7 +18,15 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -import json +try: + import json +except ImportError: + try: + import simplejson as json + except ImportError: + # Let snippet from module_utils/basic.py return a proper error in this case + pass + import base64 DOCUMENTATION = ''' diff --git a/system/puppet.py b/system/puppet.py index 2a70da3cea1..d4f69b1d515 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -22,7 +22,12 @@ try: import json except ImportError: - import simplejson as json + try: + import simplejson as json + except ImportError: + # Let snippet from module_utils/basic.py return a proper error in this case + pass + DOCUMENTATION = ''' --- diff --git a/web_infrastructure/jira.py b/web_infrastructure/jira.py index 79cfb72d4a7..dded069f743 100644 --- a/web_infrastructure/jira.py +++ b/web_infrastructure/jira.py @@ -160,7 +160,15 @@ issue={{issue.meta.key}} operation=transition status="Done" """ -import json +try: + import json +except ImportError: + try: + import simplejson as json + except ImportError: + # Let snippet from module_utils/basic.py return a proper error in this case + pass + import base64 def request(url, user, passwd, data=None, method=None): From 816bfd6990ece21590eecabc7179d31dc1f16402 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 11 Jan 2016 13:23:04 -0800 Subject: [PATCH 1086/2522] Disable RETURNS because the approved module doesn't have a RETURNS that is buildable --- cloud/amazon/ecs_service.py | 9 ++++++--- cloud/amazon/ecs_service_facts.py | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/cloud/amazon/ecs_service.py b/cloud/amazon/ecs_service.py index a683602faf1..24904e6e672 100644 --- a/cloud/amazon/ecs_service.py +++ b/cloud/amazon/ecs_service.py @@ -25,7 +25,7 @@ - for details of the parameters and returns see U(http://boto3.readthedocs.org/en/latest/reference/services/ecs.html) dependencies: - An IAM role must have been created -version_added: "2.0" +version_added: "2.1" author: Mark Chance (@java1guy) options: state: @@ -95,7 +95,11 @@ state: absent cluster: new_cluster ''' -RETURN = ''' + +# Disabled the RETURN as it was breaking docs building. Someone needs to fix +# this +RETURN = ''' ''' +''' # Create service service: On create service, it returns the new values; on delete service, it returns the values for the service being deleted. clusterArn: The Amazon Resource Name (ARN) of the of the cluster that hosts the service. @@ -114,7 +118,6 @@ ansible_facts: When deleting a service, the values described above for the service prior to its deletion are returned. ''' try: - import json import boto import botocore HAS_BOTO = True diff --git a/cloud/amazon/ecs_service_facts.py b/cloud/amazon/ecs_service_facts.py index d8287162c9a..126ef9c69cf 100644 --- a/cloud/amazon/ecs_service_facts.py +++ b/cloud/amazon/ecs_service_facts.py @@ -22,7 +22,7 @@ - for details of the parameters and returns see U(http://boto3.readthedocs.org/en/latest/reference/services/ecs.html) description: - Lists or describes services in ecs. -version_added: "2.0" +version_added: "2.1" author: Mark Chance (@java1guy) options: details: @@ -55,7 +55,11 @@ - ecs_service_facts: cluster: test-cluster ''' -RETURN = ''' + +# Disabled the RETURN as it was breaking docs building. Someone needs to fix +# this +RETURN = ''' ''' +''' services: When details is false, returns an array of service ARNs, else an array of these fields clusterArn: The Amazon Resource Name (ARN) of the of the cluster that hosts the service. desiredCount: The desired number of instantiations of the task definition to keep running on the service. @@ -71,7 +75,6 @@ taskDefinition: The ARN of a task definition to use for tasks in the service. ''' try: - import json, os import boto import botocore HAS_BOTO = True From 7e56a66ef69a888c33a5ba010b49c06c6074175a Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 11 Jan 2016 14:59:23 -0800 Subject: [PATCH 1087/2522] Really disable RETURN --- cloud/amazon/ecs_service.py | 4 ++-- cloud/amazon/ecs_service_facts.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/amazon/ecs_service.py b/cloud/amazon/ecs_service.py index 24904e6e672..9056ec73660 100644 --- a/cloud/amazon/ecs_service.py +++ b/cloud/amazon/ecs_service.py @@ -98,7 +98,7 @@ # Disabled the RETURN as it was breaking docs building. Someone needs to fix # this -RETURN = ''' ''' +RETURN = '''# ''' ''' # Create service service: On create service, it returns the new values; on delete service, it returns the values for the service being deleted. @@ -146,7 +146,7 @@ def __init__(self, module): self.module.fail_json(msg="Can't authorize connection - "+str(e)) # def list_clusters(self): - # return self.client.list_clusters() + # return self.client.list_clusters() # {'failures=[], # 'ResponseMetadata={'HTTPStatusCode=200, 'RequestId='ce7b5880-1c41-11e5-8a31-47a93a8a98eb'}, # 'clusters=[{'activeServicesCount=0, 'clusterArn='arn:aws:ecs:us-west-2:777110527155:cluster/default', 'status='ACTIVE', 'pendingTasksCount=0, 'runningTasksCount=0, 'registeredContainerInstancesCount=0, 'clusterName='default'}]} diff --git a/cloud/amazon/ecs_service_facts.py b/cloud/amazon/ecs_service_facts.py index 126ef9c69cf..d86bc93d687 100644 --- a/cloud/amazon/ecs_service_facts.py +++ b/cloud/amazon/ecs_service_facts.py @@ -58,7 +58,7 @@ # Disabled the RETURN as it was breaking docs building. Someone needs to fix # this -RETURN = ''' ''' +RETURN = '''# ''' ''' services: When details is false, returns an array of service ARNs, else an array of these fields clusterArn: The Amazon Resource Name (ARN) of the of the cluster that hosts the service. From e9898190cd73a9945e52e65e094f27c711023ee1 Mon Sep 17 00:00:00 2001 From: Javier Palacios Date: Tue, 29 Sep 2015 15:00:55 +0000 Subject: [PATCH 1088/2522] Allow relative path for bower executable Add default value Rename argument Explicit verification of relative bower path Add example Old keyword name used in example BUGFIX: tilde expansion actually useless on relative paths Modify relative_execpath default value as suggested Added version_added for relative_execpath Update for last few comments on the bug report * version to 2.1 since this feature enhancement will now go into 2.1 * set path and relative_execpath type to path * Set default value of path to None --- packaging/language/bower.py | 39 +++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/packaging/language/bower.py b/packaging/language/bower.py index c835fbf797d..2b58b1ce1f5 100644 --- a/packaging/language/bower.py +++ b/packaging/language/bower.py @@ -48,6 +48,12 @@ description: - The base path where to install the bower packages required: true + relative_execpath: + description: + - Relative path to bower executable from install path + default: null + required: false + version_added: "2.1" state: description: - The state of the bower package @@ -75,6 +81,10 @@ description: Update packages based on bower.json to their latest version. - bower: path=/app/location state=latest + +description: install bower locally and run from there +- npm: path=/app/location name=bower global=no +- bower: path=/app/location relative_execpath=node_modules/.bin ''' @@ -85,6 +95,7 @@ def __init__(self, module, **kwargs): self.offline = kwargs['offline'] self.production = kwargs['production'] self.path = kwargs['path'] + self.relative_execpath = kwargs['relative_execpath'] self.version = kwargs['version'] if kwargs['version']: @@ -94,7 +105,17 @@ def __init__(self, module, **kwargs): def _exec(self, args, run_in_check_mode=False, check_rc=True): if not self.module.check_mode or (self.module.check_mode and run_in_check_mode): - cmd = ["bower"] + args + ['--config.interactive=false', '--allow-root'] + cmd = [] + + if self.relative_execpath: + cmd.append(os.path.join(self.path, self.relative_execpath, "bower")) + if not os.path.isfile(cmd[-1]): + self.module.fail_json(msg="bower not found at relative path %s" % self.relative_execpath) + else: + cmd.append("bower") + + cmd.extend(args) + cmd.extend(['--config.interactive=false', '--allow-root']) if self.name: cmd.append(self.name_version) @@ -130,10 +151,9 @@ def list(self): dep_data = data['dependencies'][dep] if dep_data.get('missing', False): missing.append(dep) - elif \ - 'version' in dep_data['pkgMeta'] and \ - 'update' in dep_data and \ - dep_data['pkgMeta']['version'] != dep_data['update']['latest']: + elif ('version' in dep_data['pkgMeta'] and + 'update' in dep_data and + dep_data['pkgMeta']['version'] != dep_data['update']['latest']): outdated.append(dep) elif dep_data.get('incompatible', False): outdated.append(dep) @@ -160,7 +180,8 @@ def main(): name=dict(default=None), offline=dict(default='no', type='bool'), production=dict(default='no', type='bool'), - path=dict(required=True), + path=dict(required=True, type='path'), + relative_execpath=dict(default=None, required=False, type='path'), state=dict(default='present', choices=['present', 'absent', 'latest', ]), version=dict(default=None), ) @@ -172,13 +193,14 @@ def main(): offline = module.params['offline'] production = module.params['production'] path = os.path.expanduser(module.params['path']) + relative_execpath = module.params['relative_execpath'] state = module.params['state'] version = module.params['version'] if state == 'absent' and not name: module.fail_json(msg='uninstalling a package is only available for named packages') - bower = Bower(module, name=name, offline=offline, production=production, path=path, version=version) + bower = Bower(module, name=name, offline=offline, production=production, path=path, relative_execpath=relative_execpath, version=version) changed = False if state == 'present': @@ -201,4 +223,5 @@ def main(): # Import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() From aa95a81005c02718fca122a400df9367ae3c350d Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 11 Jan 2016 18:58:49 -0500 Subject: [PATCH 1089/2522] added version info on autodetection feature --- cloud/misc/proxmox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/misc/proxmox.py b/cloud/misc/proxmox.py index 7054360fc01..f0d7198111e 100644 --- a/cloud/misc/proxmox.py +++ b/cloud/misc/proxmox.py @@ -20,7 +20,7 @@ short_description: management of instances in Proxmox VE cluster description: - allows you to create/delete/stop instances in Proxmox VE cluster - - automatically detects conainerization type (lxc for PVE 4, openvz for older) + - Starting in Ansible 2.1, it automatically detects conainerization type (lxc for PVE 4, openvz for older) version_added: "2.0" options: api_host: From 7f40f465731e905b54fdcdb5a4e5ab5f52d9c7e8 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 11 Jan 2016 19:04:02 -0500 Subject: [PATCH 1090/2522] added version_added --- messaging/rabbitmq_policy.py | 1 + 1 file changed, 1 insertion(+) diff --git a/messaging/rabbitmq_policy.py b/messaging/rabbitmq_policy.py index 0a7d023dfb6..a9207b3cbcd 100644 --- a/messaging/rabbitmq_policy.py +++ b/messaging/rabbitmq_policy.py @@ -44,6 +44,7 @@ required: false default: all choices: [all, exchanges, queues] + version_added: "2.1" pattern: description: - A regex of queues to apply the policy to. From 520d245ae7c692d53a8fed84744c34b25b54309e Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 11 Jan 2016 19:06:44 -0500 Subject: [PATCH 1091/2522] added note on choice version availability --- monitoring/datadog_monitor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py index d61d1b025e9..71a17e18223 100644 --- a/monitoring/datadog_monitor.py +++ b/monitoring/datadog_monitor.py @@ -49,7 +49,9 @@ required: true choices: ['present', 'absent', 'muted', 'unmuted'] type: - description: ["The type of the monitor."] + description: + - "The type of the monitor." + - The 'event alert'is available starting at Ansible 2.1 required: false default: null choices: ['metric alert', 'service check', 'event alert'] From 264a7c88f657f812f9d3851ad3352b0421e7c2e7 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 11 Jan 2016 19:34:02 -0500 Subject: [PATCH 1092/2522] added missing version_added --- packaging/os/pkgutil.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packaging/os/pkgutil.py b/packaging/os/pkgutil.py index 72d2bf64288..0cdc3dee5e8 100644 --- a/packaging/os/pkgutil.py +++ b/packaging/os/pkgutil.py @@ -56,6 +56,7 @@ required: false choices: ["yes", "no"] default: no + version_added: "2.1" ''' EXAMPLES = ''' From fe4429ba3ed7469feece8b79bf742f82eb906e63 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Tue, 12 Jan 2016 09:52:08 +0100 Subject: [PATCH 1093/2522] fixed problems related to userpricincipalname (user@domain) and undefined variables fixed variable capitalization --- windows/win_acl.ps1 | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/windows/win_acl.ps1 b/windows/win_acl.ps1 index 4ea4a2e7c6b..2e20793e1fe 100644 --- a/windows/win_acl.ps1 +++ b/windows/win_acl.ps1 @@ -27,52 +27,49 @@ #Functions Function UserSearch { - Param ([string]$AccountName) + Param ([string]$accountName) #Check if there's a realm specified - if ($AccountName.Split("\").count -gt 1) + + $searchDomain = $false + $searchDomainUPN = $false + if ($accountName.Split("\").count -gt 1) { - if ($AccountName.Split("\")[0] -eq $env:COMPUTERNAME) - { - $IsLocalAccount = $true - } - Else + if ($accountName.Split("\")[0] -ne $env:COMPUTERNAME) { - $IsDomainAccount = $true - $IsUpn = $false + $searchDomain = $true + $accountName = $accountName.split("\")[1] } - } - Elseif ($AccountName.contains("@")) + Elseif ($accountName.contains("@")) { - $IsDomainAccount = $true - $IsUpn = $true + $searchDomain = $true + $searchDomainUPN = $true } Else { #Default to local user account - $accountname = $env:COMPUTERNAME + "\" + $AccountName - $IsLocalAccount = $true + $accountName = $env:COMPUTERNAME + "\" + $accountName } - if ($IsLocalAccount -eq $true) + if ($searchDomain -eq $false) { # do not use Win32_UserAccount, because e.g. SYSTEM (BUILTIN\SYSTEM or COMPUUTERNAME\SYSTEM) will not be listed. on Win32_Account groups will be listed too - $localaccount = get-wmiobject -class "Win32_Account" -namespace "root\CIMV2" -filter "(LocalAccount = True)" | where {$_.Caption -eq $AccountName} + $localaccount = get-wmiobject -class "Win32_Account" -namespace "root\CIMV2" -filter "(LocalAccount = True)" | where {$_.Caption -eq $accountName} if ($localaccount) { return $localaccount.SID } } - ElseIf ($IsDomainAccount -eq $true) + Else { #Search by samaccountname $Searcher = [adsisearcher]"" - If ($IsUpn -eq $false) { - $Searcher.Filter = "sAMAccountName=$($accountname.split("\")[1])" + If ($searchDomainUPN -eq $false) { + $Searcher.Filter = "sAMAccountName=$($accountName)" } Else { - $Searcher.Filter = "userPrincipalName=$($accountname)" + $Searcher.Filter = "userPrincipalName=$($accountName)" } $result = $Searcher.FindOne() From c0b2080ae18962de859929ac241992ed857e9bca Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Tue, 12 Jan 2016 09:57:56 +0100 Subject: [PATCH 1094/2522] fixed problems related to userpricincipalname (user@domain) and undefined variables fixed variable capitalization --- windows/win_owner.ps1 | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/windows/win_owner.ps1 b/windows/win_owner.ps1 index 9bd3c335e9c..d781dd011d8 100644 --- a/windows/win_owner.ps1 +++ b/windows/win_owner.ps1 @@ -22,52 +22,49 @@ #Functions Function UserSearch { - Param ([string]$AccountName) + Param ([string]$accountName) #Check if there's a realm specified - if ($AccountName.Split("\").count -gt 1) + + $searchDomain = $false + $searchDomainUPN = $false + if ($accountName.Split("\").count -gt 1) { - if ($AccountName.Split("\")[0] -eq $env:COMPUTERNAME) - { - $IsLocalAccount = $true - } - Else + if ($accountName.Split("\")[0] -ne $env:COMPUTERNAME) { - $IsDomainAccount = $true - $IsUpn = $false + $searchDomain = $true + $accountName = $accountName.split("\")[1] } - } - Elseif ($AccountName.contains("@")) + Elseif ($accountName.contains("@")) { - $IsDomainAccount = $true - $IsUpn = $true + $searchDomain = $true + $searchDomainUPN = $true } Else { #Default to local user account - $accountname = $env:COMPUTERNAME + "\" + $AccountName - $IsLocalAccount = $true + $accountName = $env:COMPUTERNAME + "\" + $accountName } - if ($IsLocalAccount -eq $true) + if ($searchDomain -eq $false) { # do not use Win32_UserAccount, because e.g. SYSTEM (BUILTIN\SYSTEM or COMPUUTERNAME\SYSTEM) will not be listed. on Win32_Account groups will be listed too - $localaccount = get-wmiobject -class "Win32_Account" -namespace "root\CIMV2" -filter "(LocalAccount = True)" | where {$_.Caption -eq $AccountName} + $localaccount = get-wmiobject -class "Win32_Account" -namespace "root\CIMV2" -filter "(LocalAccount = True)" | where {$_.Caption -eq $accountName} if ($localaccount) { return $localaccount.SID } } - ElseIf ($IsDomainAccount -eq $true) + Else { #Search by samaccountname $Searcher = [adsisearcher]"" - If ($IsUpn -eq $false) { - $Searcher.Filter = "sAMAccountName=$($accountname.split("\")[1])" + If ($searchDomainUPN -eq $false) { + $Searcher.Filter = "sAMAccountName=$($accountName)" } Else { - $Searcher.Filter = "userPrincipalName=$($accountname)" + $Searcher.Filter = "userPrincipalName=$($accountName)" } $result = $Searcher.FindOne() From c239ee31ac0f4230fbb7533b8a15ab10bbfc99b3 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Tue, 12 Jan 2016 11:39:19 +0100 Subject: [PATCH 1095/2522] fixed problems related to userpricincipalname (user@domain) and undefined variables fixed variable capitalization --- windows/win_share.ps1 | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/windows/win_share.ps1 b/windows/win_share.ps1 index f409281711e..59e4e8ab810 100644 --- a/windows/win_share.ps1 +++ b/windows/win_share.ps1 @@ -22,52 +22,49 @@ #Functions Function UserSearch { - Param ([string]$AccountName) + Param ([string]$accountName) #Check if there's a realm specified - if ($AccountName.Split("\").count -gt 1) + + $searchDomain = $false + $searchDomainUPN = $false + if ($accountName.Split("\").count -gt 1) { - if ($AccountName.Split("\")[0] -eq $env:COMPUTERNAME) - { - $IsLocalAccount = $true - } - Else + if ($accountName.Split("\")[0] -ne $env:COMPUTERNAME) { - $IsDomainAccount = $true - $IsUpn = $false + $searchDomain = $true + $accountName = $accountName.split("\")[1] } - } - Elseif ($AccountName.contains("@")) + Elseif ($accountName.contains("@")) { - $IsDomainAccount = $true - $IsUpn = $true + $searchDomain = $true + $searchDomainUPN = $true } Else { #Default to local user account - $accountname = $env:COMPUTERNAME + "\" + $AccountName - $IsLocalAccount = $true + $accountName = $env:COMPUTERNAME + "\" + $accountName } - if ($IsLocalAccount -eq $true) + if ($searchDomain -eq $false) { # do not use Win32_UserAccount, because e.g. SYSTEM (BUILTIN\SYSTEM or COMPUUTERNAME\SYSTEM) will not be listed. on Win32_Account groups will be listed too - $localaccount = get-wmiobject -class "Win32_Account" -namespace "root\CIMV2" -filter "(LocalAccount = True)" | where {$_.Caption -eq $AccountName} + $localaccount = get-wmiobject -class "Win32_Account" -namespace "root\CIMV2" -filter "(LocalAccount = True)" | where {$_.Caption -eq $accountName} if ($localaccount) { return $localaccount.SID } } - ElseIf ($IsDomainAccount -eq $true) + Else { #Search by samaccountname $Searcher = [adsisearcher]"" - If ($IsUpn -eq $false) { - $Searcher.Filter = "sAMAccountName=$($accountname.split("\")[1])" + If ($searchDomainUPN -eq $false) { + $Searcher.Filter = "sAMAccountName=$($accountName)" } Else { - $Searcher.Filter = "userPrincipalName=$($accountname)" + $Searcher.Filter = "userPrincipalName=$($accountName)" } $result = $Searcher.FindOne() From d5ab52eb58f48c1ba7d2cefeb610c415e440cb1c Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Tue, 12 Jan 2016 12:36:45 +0100 Subject: [PATCH 1096/2522] fixxed tests --- windows/win_owner.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/windows/win_owner.py b/windows/win_owner.py index f96b10766b3..1b16c1b727f 100644 --- a/windows/win_owner.py +++ b/windows/win_owner.py @@ -24,7 +24,7 @@ DOCUMENTATION = ''' --- module: win_owner -version_added: "2.0" +version_added: "2.1" short_description: Set owner description: - Set owner of files or directories @@ -63,3 +63,7 @@ user: SYSTEM recurse: no ''' + +RETURN = ''' + +''' \ No newline at end of file From 39aab8fe06cb6643dcb082c5a192c9974d9219bb Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Tue, 12 Jan 2016 12:41:41 +0100 Subject: [PATCH 1097/2522] fixxed tests --- windows/win_share.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/windows/win_share.py b/windows/win_share.py index e7c87ccf8a4..14608e6e17f 100644 --- a/windows/win_share.py +++ b/windows/win_share.py @@ -24,7 +24,7 @@ DOCUMENTATION = ''' --- module: win_share -version_added: "2.0" +version_added: "2.1" short_description: Manage Windows shares description: - Add, modify or remove Windows share and set share permissions. @@ -80,7 +80,7 @@ - Specify user list that should get no access, regardless of implied access on share, separated by comma. required: no default: none -Hans-Joachim Kliemeck (@h0nIg) +author: Hans-Joachim Kliemeck (@h0nIg) ''' EXAMPLES = ''' @@ -110,4 +110,8 @@ win_share: name: internal state: absent +''' + +RETURN = ''' + ''' \ No newline at end of file From 2984c1303537eb378df0748606071e80ce6be354 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Tue, 12 Jan 2016 13:32:39 +0100 Subject: [PATCH 1098/2522] cloudstack: cs_portforward: fix missing return and remove unused arg --- cloud/cloudstack/cs_portforward.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cloud/cloudstack/cs_portforward.py b/cloud/cloudstack/cs_portforward.py index 9932dcb0d6e..526d4616417 100644 --- a/cloud/cloudstack/cs_portforward.py +++ b/cloud/cloudstack/cs_portforward.py @@ -230,7 +230,7 @@ def __init__(self, module): 'publicport': 'public_port', 'publicendport': 'public_end_port', 'privateport': 'private_port', - 'private_end_port': 'private_end_port', + 'privateendport': 'private_end_port', } self.portforwarding_rule = None self.vm_default_nic = None @@ -322,7 +322,6 @@ def update_portforwarding_rule(self, portforwarding_rule): args['publicendport'] = self.get_or_fallback('public_end_port', 'public_port') args['privateport'] = self.module.params.get('private_port') args['privateendport'] = self.get_or_fallback('private_end_port', 'private_port') - args['openfirewall'] = self.module.params.get('open_firewall') args['vmguestip'] = self.get_vm_guest_ip() args['ipaddressid'] = self.get_ip_address(key='id') args['virtualmachineid'] = self.get_vm(key='id') From 7546cab1e54407a7027adb576a15970bcdd3c390 Mon Sep 17 00:00:00 2001 From: Sebastien Couture Date: Tue, 12 Jan 2016 09:43:10 -0500 Subject: [PATCH 1099/2522] removed comment --- network/dnsmadeeasy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/network/dnsmadeeasy.py b/network/dnsmadeeasy.py index 090e1f6f342..db819b67662 100644 --- a/network/dnsmadeeasy.py +++ b/network/dnsmadeeasy.py @@ -204,7 +204,6 @@ def getMatchingRecord(self, record_name, record_type, record_value): if not self.all_records: self.all_records = self.getRecords() - # TODO SRV type not yet implemented if record_type in ["A", "AAAA", "CNAME", "HTTPRED", "PTR"]: for result in self.all_records: if result['name'] == record_name and result['type'] == record_type: From 0ba3d85522c02e98e48fc5aa339e038291186264 Mon Sep 17 00:00:00 2001 From: Charles Paul Date: Tue, 12 Jan 2016 11:00:22 -0600 Subject: [PATCH 1100/2522] use doc fragments --- cloud/vmware/vca_fw.py | 59 +------------------ cloud/vmware/vca_nat.py | 58 +----------------- cloud/vmware/vmware_cluster.py | 15 +---- cloud/vmware/vmware_datacenter.py | 1 + cloud/vmware/vmware_dns_config.py | 15 +---- cloud/vmware/vmware_dvs_host.py | 15 +---- cloud/vmware/vmware_dvs_portgroup.py | 15 +---- cloud/vmware/vmware_dvswitch.py | 15 +---- cloud/vmware/vmware_host.py | 15 +---- cloud/vmware/vmware_migrate_vmk.py | 15 +---- cloud/vmware/vmware_portgroup.py | 15 +---- cloud/vmware/vmware_target_canonical_facts.py | 15 +---- cloud/vmware/vmware_vm_facts.py | 16 +---- cloud/vmware/vmware_vm_shell.py | 15 +---- cloud/vmware/vmware_vm_vss_dvs_migrate.py | 17 +----- cloud/vmware/vmware_vmkernel.py | 15 +---- cloud/vmware/vmware_vmkernel_ip_config.py | 15 +---- cloud/vmware/vmware_vsan_cluster.py | 15 +---- cloud/vmware/vmware_vswitch.py | 15 +---- 19 files changed, 20 insertions(+), 341 deletions(-) diff --git a/cloud/vmware/vca_fw.py b/cloud/vmware/vca_fw.py index 5649d7e5c7b..30bb16b6a27 100644 --- a/cloud/vmware/vca_fw.py +++ b/cloud/vmware/vca_fw.py @@ -27,69 +27,12 @@ version_added: "2.0" author: Peter Sprygada (@privateip) options: - username: - description: - - The vca username or email address, if not set the environment variable VCA_USER is checked for the username. - required: false - default: None - password: - description: - - The vca password, if not set the environment variable VCA_PASS is checked for the password - required: false - default: None - org: - description: - - The org to login to for creating vapp, mostly set when the service_type is vdc. - required: false - default: None - instance_id: - description: - - The instance id in a vchs environment to be used for creating the vapp - required: false - default: None - host: - description: - - The authentication host to be used when service type is vcd. - required: false - default: None - api_version: - description: - - The api version to be used with the vca - required: false - default: "5.7" - service_type: - description: - - The type of service we are authenticating against - required: false - default: vca - choices: [ "vca", "vchs", "vcd" ] - state: - description: - - if the object should be added or removed - required: false - default: present - choices: [ "present", "absent" ] - verify_certs: - description: - - If the certificates of the authentication is to be verified - required: false - default: True - vdc_name: - description: - - The name of the vdc where the gateway is located. - required: false - default: None - gateway_name: - description: - - The name of the gateway of the vdc where the rule should be added - required: false - default: gateway fw_rules: description: - A list of firewall rules to be added to the gateway, Please see examples on valid entries required: True default: false - +extends_documentation_fragment: vca.documentation ''' EXAMPLES = ''' diff --git a/cloud/vmware/vca_nat.py b/cloud/vmware/vca_nat.py index 88fc24a20fc..5fd0918a19d 100644 --- a/cloud/vmware/vca_nat.py +++ b/cloud/vmware/vca_nat.py @@ -27,63 +27,6 @@ version_added: "2.0" author: Peter Sprygada (@privateip) options: - username: - description: - - The vca username or email address, if not set the environment variable VCA_USER is checked for the username. - required: false - default: None - password: - description: - - The vca password, if not set the environment variable VCA_PASS is checked for the password - required: false - default: None - org: - description: - - The org to login to for creating vapp, mostly set when the service_type is vdc. - required: false - default: None - instance_id: - description: - - The instance id in a vchs environment to be used for creating the vapp - required: false - default: None - host: - description: - - The authentication host to be used when service type is vcd. - required: false - default: None - api_version: - description: - - The api version to be used with the vca - required: false - default: "5.7" - service_type: - description: - - The type of service we are authenticating against - required: false - default: vca - choices: [ "vca", "vchs", "vcd" ] - state: - description: - - if the object should be added or removed - required: false - default: present - choices: [ "present", "absent" ] - verify_certs: - description: - - If the certificates of the authentication is to be verified - required: false - default: True - vdc_name: - description: - - The name of the vdc where the gateway is located. - required: false - default: None - gateway_name: - description: - - The name of the gateway of the vdc where the rule should be added - required: false - default: gateway purge_rules: description: - If set to true, it will delete all rules in the gateway that are not given as paramter to this module. @@ -94,6 +37,7 @@ - A list of rules to be added to the gateway, Please see examples on valid entries required: True default: false +extends_documentation_fragment: vca.documentation ''' EXAMPLES = ''' diff --git a/cloud/vmware/vmware_cluster.py b/cloud/vmware/vmware_cluster.py index 72f29e7dfad..2b939adc8ea 100644 --- a/cloud/vmware/vmware_cluster.py +++ b/cloud/vmware/vmware_cluster.py @@ -31,20 +31,6 @@ - Tested on ESXi 5.5 - PyVmomi installed options: - hostname: - description: - - The hostname or IP address of the vSphere vCenter - required: True - username: - description: - - The username of the vSphere vCenter - required: True - aliases: ['user', 'admin'] - password: - description: - - The password of the vSphere vCenter - required: True - aliases: ['pass', 'pwd'] datacenter_name: description: - The name of the datacenter the cluster will be created in. @@ -68,6 +54,7 @@ - If set to True will enable vSAN when the cluster is created. required: False default: False +extends_documentation_fragment: vmware.documentation ''' EXAMPLES = ''' diff --git a/cloud/vmware/vmware_datacenter.py b/cloud/vmware/vmware_datacenter.py index b2083222ed5..aa85782bbbe 100644 --- a/cloud/vmware/vmware_datacenter.py +++ b/cloud/vmware/vmware_datacenter.py @@ -55,6 +55,7 @@ - If the datacenter should be present or absent choices: ['present', 'absent'] required: True +extends_documentation_fragment: vmware.documentation ''' EXAMPLES = ''' diff --git a/cloud/vmware/vmware_dns_config.py b/cloud/vmware/vmware_dns_config.py index b233ed610c8..57eda23b7d4 100644 --- a/cloud/vmware/vmware_dns_config.py +++ b/cloud/vmware/vmware_dns_config.py @@ -32,20 +32,6 @@ - "python >= 2.6" - PyVmomi options: - hostname: - description: - - The hostname or IP address of the vSphere vCenter API server - required: True - username: - description: - - The username of the vSphere vCenter - required: True - aliases: ['user', 'admin'] - password: - description: - - The password of the vSphere vCenter - required: True - aliases: ['pass', 'pwd'] change_hostname_to: description: - The hostname that an ESXi host should be changed to. @@ -58,6 +44,7 @@ description: - The DNS servers that the host should be configured to use. required: True +extends_documentation_fragment: vmware.documentation ''' EXAMPLES = ''' diff --git a/cloud/vmware/vmware_dvs_host.py b/cloud/vmware/vmware_dvs_host.py index a9c66e4d1a7..9edab7916bc 100644 --- a/cloud/vmware/vmware_dvs_host.py +++ b/cloud/vmware/vmware_dvs_host.py @@ -32,20 +32,6 @@ - "python >= 2.6" - PyVmomi options: - hostname: - description: - - The hostname or IP address of the vSphere vCenter API server - required: True - username: - description: - - The username of the vSphere vCenter - required: True - aliases: ['user', 'admin'] - password: - description: - - The password of the vSphere vCenter - required: True - aliases: ['pass', 'pwd'] esxi_hostname: description: - The ESXi hostname @@ -63,6 +49,7 @@ - If the host should be present or absent attached to the vSwitch choices: ['present', 'absent'] required: True +extends_documentation_fragment: vmware.documentation ''' EXAMPLES = ''' diff --git a/cloud/vmware/vmware_dvs_portgroup.py b/cloud/vmware/vmware_dvs_portgroup.py index 265f9fd71ef..8df629e2400 100644 --- a/cloud/vmware/vmware_dvs_portgroup.py +++ b/cloud/vmware/vmware_dvs_portgroup.py @@ -32,20 +32,6 @@ - "python >= 2.6" - PyVmomi options: - hostname: - description: - - The hostname or IP address of the vSphere vCenter API server - required: True - username: - description: - - The username of the vSphere vCenter - required: True - aliases: ['user', 'admin'] - password: - description: - - The password of the vSphere vCenter - required: True - aliases: ['pass', 'pwd'] portgroup_name: description: - The name of the portgroup that is to be created or deleted @@ -70,6 +56,7 @@ - 'earlyBinding' - 'lateBinding' - 'ephemeral' +extends_documentation_fragment: vmware.documentation ''' EXAMPLES = ''' diff --git a/cloud/vmware/vmware_dvswitch.py b/cloud/vmware/vmware_dvswitch.py index 26212a06c5f..4ebadd0c606 100644 --- a/cloud/vmware/vmware_dvswitch.py +++ b/cloud/vmware/vmware_dvswitch.py @@ -32,20 +32,6 @@ - "python >= 2.6" - PyVmomi options: - hostname: - description: - - The hostname or IP address of the vSphere vCenter API server - required: True - username: - description: - - The username of the vSphere vCenter - required: True - aliases: ['user', 'admin'] - password: - description: - - The password of the vSphere vCenter - required: True - aliases: ['pass', 'pwd'] datacenter_name: description: - The name of the datacenter that will contain the dvSwitch @@ -85,6 +71,7 @@ - 'present' - 'absent' required: False +extends_documentation_fragment: vmware.documentation ''' EXAMPLES = ''' - name: Create dvswitch diff --git a/cloud/vmware/vmware_host.py b/cloud/vmware/vmware_host.py index 162397a2190..dba7ce9a11e 100644 --- a/cloud/vmware/vmware_host.py +++ b/cloud/vmware/vmware_host.py @@ -32,20 +32,6 @@ - "python >= 2.6" - PyVmomi options: - hostname: - description: - - The hostname or IP address of the vSphere vCenter API server - required: True - username: - description: - - The username of the vSphere vCenter - required: True - aliases: ['user', 'admin'] - password: - description: - - The password of the vSphere vCenter - required: True - aliases: ['pass', 'pwd'] datacenter_name: description: - Name of the datacenter to add the host @@ -74,6 +60,7 @@ - 'present' - 'absent' required: False +extends_documentation_fragment: vmware.documentation ''' EXAMPLES = ''' diff --git a/cloud/vmware/vmware_migrate_vmk.py b/cloud/vmware/vmware_migrate_vmk.py index c658c71b682..a3f3db764ca 100644 --- a/cloud/vmware/vmware_migrate_vmk.py +++ b/cloud/vmware/vmware_migrate_vmk.py @@ -32,20 +32,6 @@ - "python >= 2.6" - PyVmomi options: - hostname: - description: - - The hostname or IP address of the vSphere vCenter API server - required: True - username: - description: - - The username of the vSphere vCenter - required: True - aliases: ['user', 'admin'] - password: - description: - - The password of the vSphere vCenter - required: True - aliases: ['pass', 'pwd'] esxi_hostname: description: - ESXi hostname to be managed @@ -70,6 +56,7 @@ description: - Portgroup name to migrate VMK interface to required: True +extends_documentation_fragment: vmware.documentation ''' EXAMPLES = ''' diff --git a/cloud/vmware/vmware_portgroup.py b/cloud/vmware/vmware_portgroup.py index e354ded510f..30e1e212617 100644 --- a/cloud/vmware/vmware_portgroup.py +++ b/cloud/vmware/vmware_portgroup.py @@ -32,20 +32,6 @@ - "python >= 2.6" - PyVmomi options: - hostname: - description: - - The hostname or IP address of the ESXi server - required: True - username: - description: - - The username of the ESXi server - required: True - aliases: ['user', 'admin'] - password: - description: - - The password of the ESXi server - required: True - aliases: ['pass', 'pwd'] switch_name: description: - vSwitch to modify @@ -58,6 +44,7 @@ description: - VLAN ID to assign to portgroup required: True +extends_documentation_fragment: vmware.documentation ''' EXAMPLES = ''' diff --git a/cloud/vmware/vmware_target_canonical_facts.py b/cloud/vmware/vmware_target_canonical_facts.py index 45c183822be..cbf9d3edaa9 100644 --- a/cloud/vmware/vmware_target_canonical_facts.py +++ b/cloud/vmware/vmware_target_canonical_facts.py @@ -31,24 +31,11 @@ - Tested on vSphere 5.5 - PyVmomi installed options: - hostname: - description: - - The hostname or IP address of the vSphere vCenter - required: True - username: - description: - - The username of the vSphere vCenter - required: True - aliases: ['user', 'admin'] - password: - description: - - The password of the vSphere vCenter - required: True - aliases: ['pass', 'pwd'] target_id: description: - The target id based on order of scsi device required: True +extends_documentation_fragment: vmware.documentation ''' EXAMPLES = ''' diff --git a/cloud/vmware/vmware_vm_facts.py b/cloud/vmware/vmware_vm_facts.py index 3551477f243..62381849144 100644 --- a/cloud/vmware/vmware_vm_facts.py +++ b/cloud/vmware/vmware_vm_facts.py @@ -31,21 +31,7 @@ requirements: - "python >= 2.6" - PyVmomi -options: - hostname: - description: - - The hostname or IP address of the vSphere vCenter API server - required: True - username: - description: - - The username of the vSphere vCenter - required: True - aliases: ['user', 'admin'] - password: - description: - - The password of the vSphere vCenter - required: True - aliases: ['pass', 'pwd'] +extends_documentation_fragment: vmware.documentation ''' EXAMPLES = ''' diff --git a/cloud/vmware/vmware_vm_shell.py b/cloud/vmware/vmware_vm_shell.py index c33e3e5c71c..8c5752b3e31 100644 --- a/cloud/vmware/vmware_vm_shell.py +++ b/cloud/vmware/vmware_vm_shell.py @@ -33,20 +33,6 @@ - "python >= 2.6" - PyVmomi options: - hostname: - description: - - The hostname or IP address of the vSphere vCenter API server - required: True - username: - description: - - The username of the vSphere vCenter - required: True - aliases: ['user', 'admin'] - password: - description: - - The password of the vSphere vCenter - required: True - aliases: ['pass', 'pwd'] datacenter: description: - The datacenter hosting the VM @@ -92,6 +78,7 @@ - The current working directory of the application from which it will be run required: False default: None +extends_documentation_fragment: vmware.documentation ''' EXAMPLES = ''' diff --git a/cloud/vmware/vmware_vm_vss_dvs_migrate.py b/cloud/vmware/vmware_vm_vss_dvs_migrate.py index ff51f86ed09..8dbf059965c 100644 --- a/cloud/vmware/vmware_vm_vss_dvs_migrate.py +++ b/cloud/vmware/vmware_vm_vss_dvs_migrate.py @@ -32,20 +32,6 @@ - "python >= 2.6" - PyVmomi options: - hostname: - description: - - The hostname or IP address of the vSphere vCenter API server - required: True - username: - description: - - The username of the vSphere vCenter - required: True - aliases: ['user', 'admin'] - password: - description: - - The password of the vSphere vCenter - required: True - aliases: ['pass', 'pwd'] vm_name: description: - Name of the virtual machine to migrate to a dvSwitch @@ -54,6 +40,7 @@ description: - Name of the portgroup to migrate to the virtual machine to required: True +extends_documentation_fragment: vmware.documentation ''' EXAMPLES = ''' @@ -173,4 +160,4 @@ def main(): from ansible.module_utils.basic import * if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/cloud/vmware/vmware_vmkernel.py b/cloud/vmware/vmware_vmkernel.py index 0221f68ad2e..863a41226af 100644 --- a/cloud/vmware/vmware_vmkernel.py +++ b/cloud/vmware/vmware_vmkernel.py @@ -32,20 +32,6 @@ - "python >= 2.6" - PyVmomi options: - hostname: - description: - - The hostname or IP address of the ESXi Server - required: True - username: - description: - - The username of the ESXi Server - required: True - aliases: ['user', 'admin'] - password: - description: - - The password of ESXi Server - required: True - aliases: ['pass', 'pwd'] vswitch_name: description: - The name of the vswitch where to add the VMK interface @@ -86,6 +72,7 @@ description: - Enable the VMK interface for Fault Tolerance traffic required: False +extends_documentation_fragment: vmware.documentation ''' EXAMPLES = ''' diff --git a/cloud/vmware/vmware_vmkernel_ip_config.py b/cloud/vmware/vmware_vmkernel_ip_config.py index c07526f0aeb..31c50e6c68c 100644 --- a/cloud/vmware/vmware_vmkernel_ip_config.py +++ b/cloud/vmware/vmware_vmkernel_ip_config.py @@ -32,20 +32,6 @@ - "python >= 2.6" - PyVmomi options: - hostname: - description: - - The hostname or IP address of the ESXi server - required: True - username: - description: - - The username of the ESXi server - required: True - aliases: ['user', 'admin'] - password: - description: - - The password of the ESXi server - required: True - aliases: ['pass', 'pwd'] vmk_name: description: - VMkernel interface name @@ -58,6 +44,7 @@ description: - Subnet Mask to assign to VMkernel interface required: True +extends_documentation_fragment: vmware.documentation ''' EXAMPLES = ''' diff --git a/cloud/vmware/vmware_vsan_cluster.py b/cloud/vmware/vmware_vsan_cluster.py index b7b84d94c43..015386d9064 100644 --- a/cloud/vmware/vmware_vsan_cluster.py +++ b/cloud/vmware/vmware_vsan_cluster.py @@ -32,24 +32,11 @@ - "python >= 2.6" - PyVmomi options: - hostname: - description: - - The hostname or IP address of the ESXi Server - required: True - username: - description: - - The username of the ESXi Server - required: True - aliases: ['user', 'admin'] - password: - description: - - The password of ESXi Server - required: True - aliases: ['pass', 'pwd'] cluster_uuid: description: - Desired cluster UUID required: False +extends_documentation_fragment: vmware.documentation ''' EXAMPLES = ''' diff --git a/cloud/vmware/vmware_vswitch.py b/cloud/vmware/vmware_vswitch.py index d9ac55d2364..d4ad8de7a3d 100644 --- a/cloud/vmware/vmware_vswitch.py +++ b/cloud/vmware/vmware_vswitch.py @@ -32,20 +32,6 @@ - "python >= 2.6" - PyVmomi options: - hostname: - description: - - The hostname or IP address of the ESXi server - required: True - username: - description: - - The username of the ESXi server - required: True - aliases: ['user', 'admin'] - password: - description: - - The password of the ESXi server - required: True - aliases: ['pass', 'pwd'] switch_name: description: - vSwitch name to add @@ -71,6 +57,7 @@ - 'present' - 'absent' required: False +extends_documentation_fragment: vmware.documentation ''' EXAMPLES = ''' From 3226ad42842f5e1fdf0cec35e6c3c1808dc13d22 Mon Sep 17 00:00:00 2001 From: Robb Wagoner Date: Tue, 12 Jan 2016 10:07:09 -0700 Subject: [PATCH 1101/2522] Include instance states (InService or OutOfService) as ELB facts --- cloud/amazon/ec2_elb_facts.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/ec2_elb_facts.py b/cloud/amazon/ec2_elb_facts.py index 172efe7326e..0c832382564 100644 --- a/cloud/amazon/ec2_elb_facts.py +++ b/cloud/amazon/ec2_elb_facts.py @@ -131,7 +131,7 @@ def get_health_check(health_check): return health_check_dict -def get_elb_info(elb): +def get_elb_info(connection,elb): elb_info = { 'name': elb.name, 'zones': elb.availability_zones, @@ -142,9 +142,23 @@ def get_elb_info(elb): 'security_groups': elb.security_groups, 'health_check': get_health_check(elb.health_check), 'subnets': elb.subnets, + 'instances_inservice': [], + 'instances_inservice_count': 0, + 'instances_outofservice': [], + 'instances_outofservice_count': 0, + 'instances_inservice_percent': 0.0, } if elb.vpc_id: elb_info['vpc_id'] = elb.vpc_id + if elb.instances: + instance_health = connection.describe_instance_health(elb.name) + elb_info['instances_inservice'] = [inst.instance_id for inst in instance_health if inst.state == 'InService'] + elb_info['instances_inservice_count'] = len(elb_info['instances_inservice']) + elb_info['instances_outofservice'] = [inst.instance_id for inst in instance_health if inst.state == 'OutOfService'] + elb_info['instances_outofservice_count'] = len(elb_info['instances_outofservice']) + elb_info['instances_inservice_percent'] = float(elb_info['instances_inservice_count'])/( + float(elb_info['instances_inservice_count']) + + float(elb_info['instances_outofservice_count'])) return elb_info @@ -161,7 +175,7 @@ def list_elb(connection, module): elb_array = [] for elb in all_elbs: - elb_array.append(get_elb_info(elb)) + elb_array.append(get_elb_info(connection,elb)) module.exit_json(elbs=elb_array) From 4be856a40cc3492df5a95b92b6e81dc007427784 Mon Sep 17 00:00:00 2001 From: Robb Wagoner Date: Tue, 12 Jan 2016 10:46:21 -0700 Subject: [PATCH 1102/2522] InService percent key as literal percent (i.e 50, not .5) --- cloud/amazon/ec2_elb_facts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_elb_facts.py b/cloud/amazon/ec2_elb_facts.py index 0c832382564..8b7853ac6f6 100644 --- a/cloud/amazon/ec2_elb_facts.py +++ b/cloud/amazon/ec2_elb_facts.py @@ -158,7 +158,7 @@ def get_elb_info(connection,elb): elb_info['instances_outofservice_count'] = len(elb_info['instances_outofservice']) elb_info['instances_inservice_percent'] = float(elb_info['instances_inservice_count'])/( float(elb_info['instances_inservice_count']) + - float(elb_info['instances_outofservice_count'])) + float(elb_info['instances_outofservice_count']))*100 return elb_info From 58a61459b3ec43df9eb36f2d2c5a9efa4fb9301f Mon Sep 17 00:00:00 2001 From: Alejandro Guirao Date: Sat, 4 Jul 2015 18:50:29 +0200 Subject: [PATCH 1103/2522] Add taiga_issue module --- web_infrastructure/taiga_issue.py | 311 ++++++++++++++++++++++++++++++ 1 file changed, 311 insertions(+) create mode 100644 web_infrastructure/taiga_issue.py diff --git a/web_infrastructure/taiga_issue.py b/web_infrastructure/taiga_issue.py new file mode 100644 index 00000000000..e90f83c1a69 --- /dev/null +++ b/web_infrastructure/taiga_issue.py @@ -0,0 +1,311 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Alejandro Guirao +# +# This file is part of Ansible. +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: taiga_issue +short_description: Creates/deletes an issue in a Taiga Project Management Platform +description: + - Creates/deletes an issue in a Taiga Project Management Platform (U(https://taiga.io)). + - An issue is identified by the combination of project, issue subject and issue type. + - This module implements the creation or deletion of issues (not the update). +version_added: "1.9" +options: + taiga_host: + description: + - The hostname of the Taiga instance. + required: False + default: https://api.taiga.io + project: + description: + - Name of the project containing the issue. Must exist previously. + required: True + subject: + description: + - The issue subject. + required: True + issue_type: + description: + - The issue type. Must exist previously. + required: True + priority: + description: + - The issue priority. Must exist previously. + required: False + default: Normal + status: + description: + - The issue status. Must exist previously. + required: False + default: New + severity: + description: + - The issue severity. Must exist previously. + required: False + default: New + description: + description: + - The issue description. + required: False + default: "" + attachment: + description: + - Path to a file to be attached to the issue. + required: False + default: None + attachment_description: + description: + - A string describing the file to be attached to the issue. + required: False + default: "" + tags: + description: + - A lists of tags to be assigned to the issue. + required: False + default: [] + state: + description: + - Whether the issue should be present or not. + required: False + choices: ["present", "absent"] + default: present +author: Alejandro Guirao (@lekum) +requirements: [python-taiga] +notes: +- The authentication is achieved either by the environment variable TAIGA_TOKEN or by the pair of environment variables TAIGA_USERNAME and TAIGA_PASSWORD +''' + +EXAMPLES = ''' +# Create an issue in the my hosted Taiga environment and attach an error log +- taiga_issue: + taiga_host: https://mytaigahost.example.com + project: myproject + subject: An error has been found + issue_type: Bug + priority: High + status: New + severity: High + description: An error has been found. Please check the attached error log for details. + attachment: /path/to/error.log + attachment_description: Error log + tags: + - Error + - Needs manual check + state: present + +# Deletes the previously created issue +- taiga_issue: + taiga_host: https://mytaigahost.example.com + project: myproject + subject: An error has been found + issue_type: Bug + state: absent +''' + +from os import getenv +from os.path import isfile + +try: + from taiga import TaigaAPI + from taiga.exceptions import TaigaException + TAIGA_MODULE_IMPORTED=True +except ImportError: + TAIGA_MODULE_IMPORTED=False + +def manage_issue(module, taiga_host, project_name, issue_subject, issue_priority, + issue_status, issue_type, issue_severity, issue_description, + issue_attachment, issue_attachment_description, + issue_tags, state, check_mode=False): + """ + Method that creates/deletes issues depending whether they exist and the state desired + + The credentials should be passed via environment variables: + - TAIGA_TOKEN + - TAIGA_USERNAME and TAIGA_PASSWORD + + Returns a tuple with these elements: + - A boolean representing the success of the operation + - A descriptive message + - A dict with the issue attributes, in case of issue creation, otherwise empty dict + """ + + changed = False + + try: + token = getenv('TAIGA_TOKEN') + if token: + api = TaigaAPI(host=taiga_host, token=token) + else: + api = TaigaAPI(host=taiga_host) + username = getenv('TAIGA_USERNAME') + password = getenv('TAIGA_PASSWORD') + if not any([username, password]): + return (False, changed, "Missing credentials", {}) + api.auth(username=username, password=password) + + user_id = api.me().id + project_list = filter(lambda x: x.name == project_name, api.projects.list(member=user_id)) + if len(project_list) != 1: + return (False, changed, "Unable to find project %s" % project_name, {}) + project = project_list[0] + project_id = project.id + + priority_list = filter(lambda x: x.name == issue_priority, api.priorities.list(project=project_id)) + if len(priority_list) != 1: + return (False, changed, "Unable to find issue priority %s for project %s" % (issue_priority, project_name), {}) + priority_id = priority_list[0].id + + status_list = filter(lambda x: x.name == issue_status, api.issue_statuses.list(project=project_id)) + if len(status_list) != 1: + return (False, changed, "Unable to find issue status %s for project %s" % (issue_status, project_name), {}) + status_id = status_list[0].id + + type_list = filter(lambda x: x.name == issue_type, project.list_issue_types()) + if len(type_list) != 1: + return (False, changed, "Unable to find issue type %s for project %s" % (issue_type, project_name), {}) + type_id = type_list[0].id + + severity_list = filter(lambda x: x.name == issue_severity, project.list_severities()) + if len(severity_list) != 1: + return (False, changed, "Unable to find severity %s for project %s" % (issue_severity, project_name), {}) + severity_id = severity_list[0].id + + issue = { + "project": project_name, + "subject": issue_subject, + "priority": issue_priority, + "status": issue_status, + "type": issue_type, + "severity": issue_severity, + "description": issue_description, + "tags": issue_tags, + } + + # An issue is identified by the project_name, the issue_subject and the issue_type + matching_issue_list = filter(lambda x: x.subject == issue_subject and x.type == type_id, project.list_issues()) + matching_issue_list_len = len(matching_issue_list) + + if matching_issue_list_len == 0: + # The issue does not exist in the project + if state == "present": + # This implies a change + changed = True + if not check_mode: + # Create the issue + new_issue = project.add_issue(issue_subject, priority_id, status_id, type_id, severity_id, tags=issue_tags, description=issue_description) + if issue_attachment: + new_issue.attach(issue_attachment, description=issue_attachment_description) + issue["attachment"] = issue_attachment + issue["attachment_description"] = issue_attachment_description + return (True, changed, "Issue created", issue) + + else: + # If does not exist, do nothing + return (True, changed, "Issue does not exist", {}) + + elif matching_issue_list_len == 1: + # The issue exists in the project + if state == "absent": + # This implies a change + changed = True + if not check_mode: + # Delete the issue + matching_issue_list[0].delete() + return (True, changed, "Issue deleted", {}) + + else: + # Do nothing + return (True, changed, "Issue already exists", {}) + + else: + # More than 1 matching issue + return (False, changed, "More than one issue with subject %s in project %s" % (issue_subject, project_name), {}) + + except TaigaException: + msg = "An exception happened: %s" % sys.exc_info()[1] + return (False, changed, msg, {}) + +def main(): + module = AnsibleModule( + argument_spec=dict( + taiga_host=dict(required=False, default="https://api.taiga.io"), + project=dict(required=True), + subject=dict(required=True), + issue_type=dict(required=True), + priority=dict(required=False, default="Normal"), + status=dict(required=False, default="New"), + severity=dict(required=False, default="Normal"), + description=dict(required=False, default=""), + attachment=dict(required=False, default=None), + attachment_description=dict(required=False, default=""), + tags=dict(required=False, default=[], type='list'), + state=dict(required=False, choices=['present','absent'], default='present'), + ), + supports_check_mode=True + ) + + if not TAIGA_MODULE_IMPORTED: + msg = "This module needs python-taiga module" + module.fail_json(msg=msg) + + taiga_host = module.params['taiga_host'] + project_name = module.params['project'] + issue_subject = module.params['subject'] + issue_priority = module.params['priority'] + issue_status = module.params['status'] + issue_type = module.params['issue_type'] + issue_severity = module.params['severity'] + issue_description = module.params['description'] + issue_attachment = module.params['attachment'] + issue_attachment_description = module.params['attachment_description'] + if issue_attachment: + if not isfile(issue_attachment): + msg = "%s is not a file" % issue_attachment + module.fail_json(msg=msg) + issue_tags = module.params['tags'] + state = module.params['state'] + + return_status, changed, msg, issue_attr_dict = manage_issue( + module, + taiga_host, + project_name, + issue_subject, + issue_priority, + issue_status, + issue_type, + issue_severity, + issue_description, + issue_attachment, + issue_attachment_description, + issue_tags, + state, + check_mode=module.check_mode + ) + if return_status: + if len(issue_attr_dict) > 0: + module.exit_json(changed=changed, msg=msg, issue=issue_attr_dict) + else: + module.exit_json(changed=changed, msg=msg) + else: + module.fail_json(msg=msg) + + +from ansible.module_utils.basic import * +main() From 9d40ed90bb3e58e00e7c47f1a4f4fa5fe5816f1d Mon Sep 17 00:00:00 2001 From: Alejandro Guirao Date: Sat, 4 Jul 2015 19:34:09 +0200 Subject: [PATCH 1104/2522] Fix DOCUMENTATION typo --- web_infrastructure/taiga_issue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_infrastructure/taiga_issue.py b/web_infrastructure/taiga_issue.py index e90f83c1a69..a6d512c4e42 100644 --- a/web_infrastructure/taiga_issue.py +++ b/web_infrastructure/taiga_issue.py @@ -59,7 +59,7 @@ description: - The issue severity. Must exist previously. required: False - default: New + default: Normal description: description: - The issue description. From 8d5ecca97f60a5f0bebdd6acd6cfbf95d5b1ce6f Mon Sep 17 00:00:00 2001 From: Alejandro Guirao Date: Sat, 4 Jul 2015 19:34:47 +0200 Subject: [PATCH 1105/2522] Fix version_added in DOCUMENTATION --- web_infrastructure/taiga_issue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_infrastructure/taiga_issue.py b/web_infrastructure/taiga_issue.py index a6d512c4e42..3664aa6a897 100644 --- a/web_infrastructure/taiga_issue.py +++ b/web_infrastructure/taiga_issue.py @@ -26,7 +26,7 @@ - Creates/deletes an issue in a Taiga Project Management Platform (U(https://taiga.io)). - An issue is identified by the combination of project, issue subject and issue type. - This module implements the creation or deletion of issues (not the update). -version_added: "1.9" +version_added: "2.0" options: taiga_host: description: From 92e2ea2cefb65b622619f89ca462e94614b4cc0c Mon Sep 17 00:00:00 2001 From: Alejandro Guirao Date: Thu, 17 Sep 2015 15:22:14 +0200 Subject: [PATCH 1106/2522] Minor example change --- web_infrastructure/taiga_issue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_infrastructure/taiga_issue.py b/web_infrastructure/taiga_issue.py index 3664aa6a897..2b33a843466 100644 --- a/web_infrastructure/taiga_issue.py +++ b/web_infrastructure/taiga_issue.py @@ -104,7 +104,7 @@ severity: High description: An error has been found. Please check the attached error log for details. attachment: /path/to/error.log - attachment_description: Error log + attachment_description: Error log file tags: - Error - Needs manual check From 0e492bb3785176cbbe623ef8f06771f22ab8bb4f Mon Sep 17 00:00:00 2001 From: Alejandro Guirao Date: Sat, 3 Oct 2015 16:45:23 +0200 Subject: [PATCH 1107/2522] Change example to match a severity providen by Taiga out-of-the-box --- web_infrastructure/taiga_issue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_infrastructure/taiga_issue.py b/web_infrastructure/taiga_issue.py index 2b33a843466..942f8290ffd 100644 --- a/web_infrastructure/taiga_issue.py +++ b/web_infrastructure/taiga_issue.py @@ -101,7 +101,7 @@ issue_type: Bug priority: High status: New - severity: High + severity: Important description: An error has been found. Please check the attached error log for details. attachment: /path/to/error.log attachment_description: Error log file From 5bd31ada0e86b3abf80c0dfd81386ac8eb8677b8 Mon Sep 17 00:00:00 2001 From: Alejandro Guirao Date: Sat, 3 Oct 2015 16:58:16 +0200 Subject: [PATCH 1108/2522] Change for enabling future tests --- web_infrastructure/taiga_issue.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web_infrastructure/taiga_issue.py b/web_infrastructure/taiga_issue.py index 942f8290ffd..40b74b9cb88 100644 --- a/web_infrastructure/taiga_issue.py +++ b/web_infrastructure/taiga_issue.py @@ -308,4 +308,5 @@ def main(): from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() From 39d6066f512b3ceaa4f16592c452cb26c17675f6 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Tue, 12 Jan 2016 11:25:56 -0800 Subject: [PATCH 1109/2522] Trick ansible-modules-validate to pass this with no RETURN. This is a module present in 1.9 so we have to have it in even though it doesn't have RETURN --- web_infrastructure/taiga_issue.py | 1 + 1 file changed, 1 insertion(+) diff --git a/web_infrastructure/taiga_issue.py b/web_infrastructure/taiga_issue.py index 40b74b9cb88..e58c6c0270b 100644 --- a/web_infrastructure/taiga_issue.py +++ b/web_infrastructure/taiga_issue.py @@ -119,6 +119,7 @@ state: absent ''' +RETURN = '''# ''' from os import getenv from os.path import isfile From d2959fb7ed7c565c036e2a25503a8ba952d763f1 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Wed, 13 Jan 2016 10:39:33 +0100 Subject: [PATCH 1110/2522] fixed version added and tests --- windows/win_acl_inheritance.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/windows/win_acl_inheritance.py b/windows/win_acl_inheritance.py index 6c03b9c75fd..0837bab3205 100644 --- a/windows/win_acl_inheritance.py +++ b/windows/win_acl_inheritance.py @@ -24,7 +24,7 @@ DOCUMENTATION = ''' --- module: win_acl_inheritance -version_added: "2.0" +version_added: "2.1" short_description: Change ACL inheritance description: - Change ACL (Access Control List) inheritance and optionally copy inherited ACE's (Access Control Entry) to dedicated ACE's or vice versa. @@ -72,3 +72,7 @@ state: present reorganize: yes ''' + +RETURN = ''' + +''' \ No newline at end of file From ab9b0558d3911b3b738d3615ad70bcc9d437443b Mon Sep 17 00:00:00 2001 From: Alexander Bulimov Date: Wed, 13 Jan 2016 17:39:42 +0300 Subject: [PATCH 1111/2522] Allow recreation of same FS with force=yes --- system/filesystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/filesystem.py b/system/filesystem.py index b44168a0e06..8b19c5bc1dd 100644 --- a/system/filesystem.py +++ b/system/filesystem.py @@ -140,7 +140,7 @@ def main(): rc,raw_fs,err = module.run_command("%s -c /dev/null -o value -s TYPE %s" % (cmd, dev)) fs = raw_fs.strip() - if fs == fstype and resizefs == False: + if fs == fstype and resizefs == False and not force: module.exit_json(changed=False) elif fs == fstype and resizefs == True: cmd = module.get_bin_path(growcmd, required=True) From b52af2dcb1b9ec6cdc645517b826a61f26b6688d Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 13 Jan 2016 13:31:03 -0500 Subject: [PATCH 1112/2522] Allow numeric npm package versions When passing a package version that parses as a number (e.g. `1.9`), the version should be converted to a string before being concatenated to the package name. --- packaging/language/npm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/language/npm.py b/packaging/language/npm.py index 43fa1f325ff..b0882ddec08 100644 --- a/packaging/language/npm.py +++ b/packaging/language/npm.py @@ -131,7 +131,7 @@ def __init__(self, module, **kwargs): self.executable = [module.get_bin_path('npm', True)] if kwargs['version']: - self.name_version = self.name + '@' + self.version + self.name_version = self.name + '@' + str(self.version) else: self.name_version = self.name From e088c9f819665545a58628559a763bec0defd3c2 Mon Sep 17 00:00:00 2001 From: Jeroen Geusebroek Date: Wed, 13 Jan 2016 21:55:56 +0100 Subject: [PATCH 1113/2522] Fix documentation default value for gluster_volume start_on_create --- system/gluster_volume.py | 1 + 1 file changed, 1 insertion(+) diff --git a/system/gluster_volume.py b/system/gluster_volume.py index df185591299..d24930d7290 100644 --- a/system/gluster_volume.py +++ b/system/gluster_volume.py @@ -71,6 +71,7 @@ start_on_create: choices: [ 'yes', 'no'] required: false + default: 'yes' description: - Controls whether the volume is started after creation or not, defaults to yes rebalance: From ee2c730b7e55cfe26781895ee0520d351060c67a Mon Sep 17 00:00:00 2001 From: Michal Mach Date: Thu, 14 Jan 2016 11:47:19 +0100 Subject: [PATCH 1114/2522] Fix seport module issue when ports argument is interpolated from a variable and is a int --- system/seport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/seport.py b/system/seport.py index 8a512f04d61..7192f2726a2 100644 --- a/system/seport.py +++ b/system/seport.py @@ -234,7 +234,7 @@ def main(): if not selinux.is_selinux_enabled(): module.fail_json(msg="SELinux is disabled on this host.") - ports = [x.strip() for x in module.params['ports'].split(',')] + ports = [x.strip() for x in str(module.params['ports']).split(',')] proto = module.params['proto'] setype = module.params['setype'] state = module.params['state'] From bc66ba103fe0a7f44247d8c94bc743c65247e31d Mon Sep 17 00:00:00 2001 From: lipanski Date: Thu, 14 Jan 2016 18:26:39 +0100 Subject: [PATCH 1115/2522] Fix: Rename gem_install_path to gem_path - as specified in the docs --- packaging/language/bundler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/language/bundler.py b/packaging/language/bundler.py index f4aeff4156f..2408741f880 100644 --- a/packaging/language/bundler.py +++ b/packaging/language/bundler.py @@ -163,7 +163,7 @@ def main(): local = module.params.get('local') deployment_mode = module.params.get('deployment_mode') user_install = module.params.get('user_install') - gem_path = module.params.get('gem_install_path') + gem_path = module.params.get('gem_path') binstub_directory = module.params.get('binstub_directory') extra_args = module.params.get('extra_args') From 4c4a58e77b710509677a10a4eba6e3c212aaa3b0 Mon Sep 17 00:00:00 2001 From: "Tom X. Tobin" Date: Thu, 14 Jan 2016 12:44:05 -0500 Subject: [PATCH 1116/2522] osx_defaults: Fix boolean value parsing Values for boolean types were being unconditionally treated as strings (by calling `.lower()`), thus breaking value parsing for actual boolean and integer objects. It looks like the bug was introduced in: - 130bd670d82cc55fa321021e819838e07ff10c08 Fixes #709. --- system/osx_defaults.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/system/osx_defaults.py b/system/osx_defaults.py index 614adc4852b..98aedcbd709 100644 --- a/system/osx_defaults.py +++ b/system/osx_defaults.py @@ -124,9 +124,11 @@ def _convert_type(self, type, value): if type == "string": return str(value) elif type in ["bool", "boolean"]: - if value.lower() in [True, 1, "true", "1", "yes"]: + if isinstance(value, basestring): + value = value.lower() + if value in [True, 1, "true", "1", "yes"]: return True - elif value.lower() in [False, 0, "false", "0", "no"]: + elif value in [False, 0, "false", "0", "no"]: return False raise OSXDefaultsException("Invalid boolean value: {0}".format(repr(value))) elif type == "date": From c276d9f621c4e52290f8315b07436bfd8f313642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yannig=20Perr=C3=A9?= Date: Fri, 15 Jan 2016 16:33:42 +0100 Subject: [PATCH 1117/2522] Use -f when pvcreate to avoid interactive input. Fix https://github.com/ansible/ansible-modules-extras/issues/1504 --- system/lvg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/lvg.py b/system/lvg.py index 9e3ba2d2931..68c9d7575ac 100644 --- a/system/lvg.py +++ b/system/lvg.py @@ -183,7 +183,7 @@ def main(): ### create PV pvcreate_cmd = module.get_bin_path('pvcreate', True) for current_dev in dev_list: - rc,_,err = module.run_command("%s %s" % (pvcreate_cmd,current_dev)) + rc,_,err = module.run_command("%s -f %s" % (pvcreate_cmd,current_dev)) if rc == 0: changed = True else: @@ -224,7 +224,7 @@ def main(): ### create PV pvcreate_cmd = module.get_bin_path('pvcreate', True) for current_dev in devs_to_add: - rc,_,err = module.run_command("%s %s" % (pvcreate_cmd, current_dev)) + rc,_,err = module.run_command("%s -f %s" % (pvcreate_cmd, current_dev)) if rc == 0: changed = True else: From 8d7b1b0bd6be864ef56da80b3c4bd6174c52bb54 Mon Sep 17 00:00:00 2001 From: Alexander Gubin Date: Fri, 15 Jan 2016 18:35:45 +0100 Subject: [PATCH 1118/2522] Make fileystem module idemponent. Compare devicesize and filesystemsize. --- system/filesystem.py | 73 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/system/filesystem.py b/system/filesystem.py index b44168a0e06..d31d6ebec5c 100644 --- a/system/filesystem.py +++ b/system/filesystem.py @@ -63,6 +63,50 @@ - filesystem: fstype=ext4 dev=/dev/sdb1 opts="-cc" ''' +def _get_dev_size(dev, module): + """ Return size in bytes of device. Returns int """ + blockdev_cmd = module.get_bin_path("blockdev", required=True) + rc, devsize_in_bytes, err = module.run_command("%s %s %s" % (blockdev_cmd, "--getsize64", dev)) + return int(devsize_in_bytes) + + +def _get_fs_size(fssize_cmd, dev, module): + """ Return size in bytes of filesystem on device. Returns int """ + cmd = module.get_bin_path(fssize_cmd, required=True) + if 'tune2fs' == fssize_cmd: + # Get Block count and Block size + rc, size, err = module.run_command("%s %s %s" % (cmd, '-l', dev)) + if rc == 0: + for line in size.splitlines(): + if 'Block count:' in line: + block_count = int(line.split(':')[1].strip()) + elif 'Block size:' in line: + block_size = int(line.split(':')[1].strip()) + break + else: + module.fail_json(msg="Failed to get block count and block size of %s with %s" % (dev, cmd), rc=rc, err=err ) + elif 'xfs_info' == fssize_cmd: + # Get Block count and Block size + rc, size, err = module.run_command("%s %s" % (cmd, dev)) + if rc == 0: + for line in size.splitlines(): + #if 'data' in line: + if 'data ' in line: + block_size = int(line.split('=')[2].split()[0]) + block_count = int(line.split('=')[3].split(',')[0]) + break + else: + module.fail_json(msg="Failed to get block count and block size of %s with %s" % (dev, cmd), rc=rc, err=err ) + elif 'btrfs' == fssize_cmd: + #ToDo + # There is no way to get the blocksize and blockcount for btrfs filesystems + block_size = 1 + block_count = 1 + + + return block_size*block_count + + def main(): module = AnsibleModule( argument_spec = dict( @@ -82,36 +126,42 @@ def main(): 'grow' : 'resize2fs', 'grow_flag' : None, 'force_flag' : '-F', + 'fsinfo': 'tune2fs', }, 'ext3' : { 'mkfs' : 'mkfs.ext3', 'grow' : 'resize2fs', 'grow_flag' : None, 'force_flag' : '-F', + 'fsinfo': 'tune2fs', }, 'ext4' : { 'mkfs' : 'mkfs.ext4', 'grow' : 'resize2fs', 'grow_flag' : None, 'force_flag' : '-F', + 'fsinfo': 'tune2fs', }, 'ext4dev' : { 'mkfs' : 'mkfs.ext4', 'grow' : 'resize2fs', 'grow_flag' : None, 'force_flag' : '-F', + 'fsinfo': 'tune2fs', }, 'xfs' : { 'mkfs' : 'mkfs.xfs', 'grow' : 'xfs_growfs', 'grow_flag' : None, 'force_flag' : '-f', + 'fsinfo': 'xfs_info', }, 'btrfs' : { 'mkfs' : 'mkfs.btrfs', 'grow' : 'btrfs', 'grow_flag' : 'filesystem resize', 'force_flag' : '-f', + 'fsinfo': 'btrfs', } } @@ -131,6 +181,7 @@ def main(): mkfscmd = fs_cmd_map[fstype]['mkfs'] force_flag = fs_cmd_map[fstype]['force_flag'] growcmd = fs_cmd_map[fstype]['grow'] + fssize_cmd = fs_cmd_map[fstype]['fsinfo'] if not os.path.exists(dev): module.fail_json(msg="Device %s not found."%dev) @@ -143,10 +194,21 @@ def main(): if fs == fstype and resizefs == False: module.exit_json(changed=False) elif fs == fstype and resizefs == True: - cmd = module.get_bin_path(growcmd, required=True) - if module.check_mode: - module.exit_json(changed=True, msg="May resize filesystem") + # Get dev and fs size and compare + devsize_in_bytes = _get_dev_size(dev, module) + fssize_in_bytes = _get_fs_size(fssize_cmd, dev, module) + if fssize_in_bytes < devsize_in_bytes: + fs_smaller = True else: + fs_smaller = False + + + if module.check_mode and fs_smaller: + module.exit_json(changed=True, msg="Resizing filesystem %s on device %s" % (fstype,dev)) + elif module.check_mode and not fs_smaller: + module.exit_json(changed=False, msg="%s filesystem is using the whole device %s" % (fstype, dev)) + elif fs_smaller: + cmd = module.get_bin_path(growcmd, required=True) rc,out,err = module.run_command("%s %s" % (cmd, dev)) # Sadly there is no easy way to determine if this has changed. For now, just say "true" and move on. # in the future, you would have to parse the output to determine this. @@ -155,6 +217,8 @@ def main(): module.exit_json(changed=True, msg=out) else: module.fail_json(msg="Resizing filesystem %s on device '%s' failed"%(fstype,dev), rc=rc, err=err) + else: + module.exit_json(changed=False, msg="%s filesystem is using the whole device %s" % (fstype, dev)) elif fs and not force: module.fail_json(msg="'%s' is already used as %s, use force=yes to overwrite"%(dev,fs), rc=rc, err=err) @@ -180,4 +244,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() From 5b555b9347e9dbcbc30a85ee9aeb50bcbb86474e Mon Sep 17 00:00:00 2001 From: Scott Bonds Date: Tue, 3 Nov 2015 13:16:33 -0800 Subject: [PATCH 1119/2522] add ports support in openbsd_pkg --- packaging/os/openbsd_pkg.py | 109 +++++++++++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 3 deletions(-) diff --git a/packaging/os/openbsd_pkg.py b/packaging/os/openbsd_pkg.py index 13cafa26bc5..b2299e301e0 100644 --- a/packaging/os/openbsd_pkg.py +++ b/packaging/os/openbsd_pkg.py @@ -18,8 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +import os import re import shlex +import sqlite3 DOCUMENTATION = ''' --- @@ -41,6 +43,21 @@ - C(present) will make sure the package is installed. C(latest) will make sure the latest version of the package is installed. C(absent) will make sure the specified package is not installed. + build: + required: false + choices: [ yes, no ] + default: no + description: + - Build the package from source instead of downloading and installing + a binary. Requires that the port source tree is already installed. + Automatically builds and installs the 'sqlports' package, if it is + not already installed. + ports_dir: + required: false + default: /usr/ports + description: + - When used in combination with the 'build' option, allows overriding + the default ports source directory. ''' EXAMPLES = ''' @@ -53,6 +70,9 @@ # Make sure nmap is not installed - openbsd_pkg: name=nmap state=absent +# Make sure nmap is installed, build it from source if it is not +- openbsd_pkg: name=nmap state=present build=yes + # Specify a pkg flavour with '--' - openbsd_pkg: name=vim--nox11 state=present @@ -118,15 +138,33 @@ def get_package_state(name, pkg_spec, module): # Function used to make sure a package is present. def package_present(name, installed_state, pkg_spec, module): + build = module.params['build'] + if module.check_mode: install_cmd = 'pkg_add -Imn' else: - install_cmd = 'pkg_add -Im' + if build is True: + port_dir = "%s/%s" % (module.params['ports_dir'], get_package_source_path(name, pkg_spec, module)) + if os.path.isdir(port_dir): + if pkg_spec['flavor']: + flavors = pkg_spec['flavor'].replace('-', ' ') + install_cmd = "cd %s && make clean=depends && FLAVOR=\"%s\" make install && make clean=depends" % (port_dir, flavors) + elif pkg_spec['subpackage']: + install_cmd = "cd %s && make clean=depends && SUBPACKAGE=\"%s\" make install && make clean=depends" % (port_dir, pkg_spec['subpackage']) + else: + install_cmd = "cd %s && make install && make clean=depends" % (port_dir) + else: + module.fail_json(msg="the port source directory %s does not exist" % (port_dir)) + else: + install_cmd = 'pkg_add -Im' if installed_state is False: # Attempt to install the package - (rc, stdout, stderr) = execute_command("%s %s" % (install_cmd, name), module) + if build is True and not module.check_mode: + (rc, stdout, stderr) = module.run_command(install_cmd, module, use_unsafe_shell=True) + else: + (rc, stdout, stderr) = execute_command("%s %s" % (install_cmd, name), module) # The behaviour of pkg_add is a bit different depending on if a # specific version is supplied or not. @@ -134,7 +172,7 @@ def package_present(name, installed_state, pkg_spec, module): # When a specific version is supplied the return code will be 0 when # a package is found and 1 when it is not, if a version is not # supplied the tool will exit 0 in both cases: - if pkg_spec['version']: + if pkg_spec['version'] or build is True: # Depend on the return code. module.debug("package_present(): depending on return code") if rc: @@ -177,6 +215,10 @@ def package_present(name, installed_state, pkg_spec, module): # Function used to make sure a package is the latest available version. def package_latest(name, installed_state, pkg_spec, module): + + if module.params['build'] is True: + module.fail_json(msg="the combination of build=%s and state=latest is not supported" % module.params['build']) + if module.check_mode: upgrade_cmd = 'pkg_add -umn' else: @@ -275,6 +317,7 @@ def parse_package_name(name, pkg_spec, module): pkg_spec['version'] = match.group('version') pkg_spec['flavor_separator'] = match.group('flavor_separator') pkg_spec['flavor'] = match.group('flavor') + pkg_spec['style'] = 'version' else: module.fail_json(msg="Unable to parse package name at version_match: " + name) @@ -287,6 +330,7 @@ def parse_package_name(name, pkg_spec, module): pkg_spec['version'] = None pkg_spec['flavor_separator'] = '-' pkg_spec['flavor'] = match.group('flavor') + pkg_spec['style'] = 'versionless' else: module.fail_json(msg="Unable to parse package name at versionless_match: " + name) @@ -299,6 +343,7 @@ def parse_package_name(name, pkg_spec, module): pkg_spec['version'] = None pkg_spec['flavor_separator'] = None pkg_spec['flavor'] = None + pkg_spec['style'] = 'stem' else: module.fail_json(msg="Unable to parse package name at else: " + name) @@ -309,6 +354,48 @@ def parse_package_name(name, pkg_spec, module): if match: module.fail_json(msg="Trailing dash in flavor: " + pkg_spec['flavor']) +# Function used for figuring out the port path. +def get_package_source_path(name, pkg_spec, module): + pkg_spec['subpackage'] = None + if pkg_spec['stem'] == 'sqlports': + return 'databases/sqlports' + else: + # try for an exact match first + conn = sqlite3.connect('/usr/local/share/sqlports') + first_part_of_query = 'SELECT fullpkgpath, fullpkgname FROM ports WHERE fullpkgname' + query = first_part_of_query + ' = ?' + cursor = conn.execute(query, (name,)) + results = cursor.fetchall() + + # next, try for a fuzzier match + if len(results) < 1: + looking_for = pkg_spec['stem'] + (pkg_spec['version_separator'] or '-') + (pkg_spec['version'] or '%') + query = first_part_of_query + ' LIKE ?' + if pkg_spec['flavor']: + looking_for += pkg_spec['flavor_separator'] + pkg_spec['flavor'] + cursor = conn.execute(query, (looking_for,)) + elif pkg_spec['style'] == 'versionless': + query += ' AND fullpkgname NOT LIKE ?' + cursor = conn.execute(query, (looking_for, "%s-%%" % looking_for,)) + else: + cursor = conn.execute(query, (looking_for,)) + results = cursor.fetchall() + + # error if we don't find exactly 1 match + conn.close() + if len(results) < 1: + module.fail_json(msg="could not find a port by the name '%s'" % name) + if len(results) > 1: + matches = map(lambda x:x[1], results) + module.fail_json(msg="too many matches, unsure which to build: %s" % ' OR '.join(matches)) + + # there's exactly 1 match, so figure out the subpackage, if any, then return + fullpkgpath = results[0][0] + parts = fullpkgpath.split(',') + if len(parts) > 1 and parts[1][0] == '-': + pkg_spec['subpackage'] = parts[1] + return parts[0] + # Function used for upgrading all installed packages. def upgrade_packages(module): if module.check_mode: @@ -348,12 +435,16 @@ def main(): argument_spec = dict( name = dict(required=True), state = dict(required=True, choices=['absent', 'installed', 'latest', 'present', 'removed']), + build = dict(default='no', type='bool'), + ports_dir = dict(default='/usr/ports'), ), supports_check_mode = True ) name = module.params['name'] state = module.params['state'] + build = module.params['build'] + ports_dir = module.params['ports_dir'] rc = 0 stdout = '' @@ -361,6 +452,18 @@ def main(): result = {} result['name'] = name result['state'] = state + result['build'] = build + + if build is True: + if not os.path.isdir(ports_dir): + module.fail_json(msg="the ports source directory %s does not exist" % (ports_dir)) + + # build sqlports if its not installed yet + pkg_spec = {} + parse_package_name('sqlports', pkg_spec, module) + installed_state = get_package_state(name, pkg_spec, module) + if not installed_state: + package_present('sqlports', installed_state, pkg_spec, module) if name == '*': if state != 'latest': From 57a6a98cd92b9f7df5668f7275d513159d1b9f00 Mon Sep 17 00:00:00 2001 From: RajeevNambiar Date: Sat, 16 Jan 2016 09:13:18 -0500 Subject: [PATCH 1120/2522] Update sts_assume_role.py for showing the correct example syntax role_session_name instead of session_name Update sts_assume_role.py for showing the correct example syntax role_session_name instead of session_name. session_name is not a valid property. --- cloud/amazon/sts_assume_role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/sts_assume_role.py b/cloud/amazon/sts_assume_role.py index b5f2c810351..a3fab12137e 100644 --- a/cloud/amazon/sts_assume_role.py +++ b/cloud/amazon/sts_assume_role.py @@ -69,7 +69,7 @@ # Assume an existing role (more details: http://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html) sts_assume_role: role_arn: "arn:aws:iam::123456789012:role/someRole" - session_name: "someRoleSession" + role_session_name: "someRoleSession" register: assumed_role # Use the assumed role above to tag an instance in account 123456789012 From 23aa66a48e6d17b6e592582d679b5f739237da2e Mon Sep 17 00:00:00 2001 From: Mo Date: Sat, 16 Jan 2016 23:02:58 +0100 Subject: [PATCH 1121/2522] firewalld: fixes documentation - removes warning, aligning to existing documentation - adds version --- system/firewalld.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system/firewalld.py b/system/firewalld.py index 4f610be3c20..ee3b71bc3b0 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -49,9 +49,10 @@ version_added: "2.0" interface: description: - - 'The interface you would like to add/remove to/from a zone in firewalld - zone must be specified' + - 'The interface you would like to add/remove to/from a zone in firewalld' required: false default: null + version_added: "2.1" zone: description: - 'The firewalld zone to add/remove to/from (NOTE: default zone can be configured per system but "public" is default from upstream. Available choices can be extended based on per-system configs, listed here are "out of the box" defaults).' From 263774ea7da9a9b5ef7dafc84f9e313167dbf549 Mon Sep 17 00:00:00 2001 From: tcr Date: Mon, 18 Jan 2016 15:27:36 +0100 Subject: [PATCH 1122/2522] Fix #1512 add missing property in win_firewall_rule --- windows/win_firewall_rule.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/windows/win_firewall_rule.ps1 b/windows/win_firewall_rule.ps1 index 33ac7f3fbbd..5012cb041da 100644 --- a/windows/win_firewall_rule.ps1 +++ b/windows/win_firewall_rule.ps1 @@ -98,6 +98,7 @@ function getFirewallRule ($fwsettings) { $msg += @("No rule could be found"); }; $result = @{ + failed = $false exists = $exists identical = $correct multiple = $multi @@ -137,6 +138,7 @@ function createFireWallRule ($fwsettings) { $msg+=@("Created firewall rule $name"); $result=@{ + failed = $false output=$output changed=$true msg=$msg From ece6872b811d54d4c696208dc418ea41ed07c855 Mon Sep 17 00:00:00 2001 From: Daniel Vigueras Date: Mon, 18 Jan 2016 16:00:09 +0100 Subject: [PATCH 1123/2522] iptables: add --limit-burst option --- system/iptables.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/system/iptables.py b/system/iptables.py index 2b71e1f9380..725259f14a1 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -211,6 +211,10 @@ description: - "Specifies the maximum average number of matches to allow per second. The number can specify units explicitly, using `/second', `/minute', `/hour' or `/day', or parts of them (so `5/second' is the same as `5/s')." required: false + limit_burst: + description: + - "Specifies the maximum burst before the above limit kicks in." + required: false ''' EXAMPLES = ''' @@ -266,8 +270,9 @@ def construct_rule(params): append_param(rule, params['comment'], '--comment', False) append_match(rule, params['ctstate'], 'state') append_csv(rule, params['ctstate'], '--state') - append_match(rule, params['limit'], 'limit') + append_match(rule, params['limit'] or params['limit_burst'], 'limit') append_param(rule, params['limit'], '--limit', False) + append_param(rule, params['limit_burst'], '--limit-burst', False) return rule @@ -319,6 +324,7 @@ def main(): comment=dict(required=False, default=None, type='str'), ctstate=dict(required=False, default=[], type='list'), limit=dict(required=False, default=None, type='str'), + limit_burst=dict(required=False, default=None, type='str'), ), ) args = dict( From 16d17d287f9b2e29ed7869dfc9f8674410c0a421 Mon Sep 17 00:00:00 2001 From: Daniel Vigueras Date: Mon, 18 Jan 2016 16:11:13 +0100 Subject: [PATCH 1124/2522] iptables: fix param check in append_csv function --- system/iptables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/iptables.py b/system/iptables.py index 2b71e1f9380..4ed53b6e6c9 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -238,7 +238,7 @@ def append_param(rule, param, flag, is_list): def append_csv(rule, param, flag): - if param is not None: + if param: rule.extend([flag, ','.join(param)]) From 2b69a2b14c6d4c88281eaaf885b6fb4becbf8f5f Mon Sep 17 00:00:00 2001 From: Scott Bonds Date: Mon, 18 Jan 2016 12:20:14 -0800 Subject: [PATCH 1125/2522] add python 2.5 requirement and ansible 2.1 version_added --- packaging/os/openbsd_pkg.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packaging/os/openbsd_pkg.py b/packaging/os/openbsd_pkg.py index b2299e301e0..a72140b3bb7 100644 --- a/packaging/os/openbsd_pkg.py +++ b/packaging/os/openbsd_pkg.py @@ -31,6 +31,7 @@ short_description: Manage packages on OpenBSD. description: - Manage packages on OpenBSD using the pkg tools. +requirements: [ "python >= 2.5" ] options: name: required: true @@ -52,12 +53,14 @@ a binary. Requires that the port source tree is already installed. Automatically builds and installs the 'sqlports' package, if it is not already installed. + version_added: "2.1" ports_dir: required: false default: /usr/ports description: - When used in combination with the 'build' option, allows overriding the default ports source directory. + version_added: "2.1" ''' EXAMPLES = ''' From b65bd39615fa3204d4d09bb000c14602fe4fe9cb Mon Sep 17 00:00:00 2001 From: Haneef Ali Date: Mon, 1 Jun 2015 14:06:37 -0700 Subject: [PATCH 1126/2522] Ansible module for Keystone V3 API Change-Id: I9db323cc9e5a42353cab5cf4be6e22449cef8542 --- cloud/openstack/os_keystone.py | 577 +++++++++++++++++++++++++++++++++ 1 file changed, 577 insertions(+) create mode 100644 cloud/openstack/os_keystone.py diff --git a/cloud/openstack/os_keystone.py b/cloud/openstack/os_keystone.py new file mode 100644 index 00000000000..f2455bb0e10 --- /dev/null +++ b/cloud/openstack/os_keystone.py @@ -0,0 +1,577 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +from __builtin__ import False + +DOCUMENTATION = ''' +--- +module: keystone_v3 +version_added: "1.0" +short_description: Manage OpenStack Identity (keystone v3) users, tenants and roles +description: + - Manage users,tenants, roles from OpenStack. +requirements: [ python-keystoneclient ] +author: Haneef Ali +''' + +EXAMPLES = ''' +# Create a project +- keystone_v3: action="create_project" project_name=demo + description="Default Tenant" project_domain_name="Default" + +# Create a user +- keystone_v3: action="create_user" user_name=demo + description="Default User" user_domain_name="Default" + +# Create a domain +- keystone_v3: action="create_domain" domain_name=demo + description="Default User" + +# Grant admin role to the john user in the demo tenant +- keystone_v3: action="grant_project_role" project__name=demo + role_name=admin user_name=john user_domain_name=Default + project_domain_name=Default +''' + + +from keystoneclient.v3 import client as v3client +from keystoneclient.auth.identity import v3 +from keystoneclient.auth import token_endpoint +from keystoneclient import session + + +def _get_client(auth_url=None, token=None, login_username=None, login_password=None, login_project_name=None, + login_project_domain_name=None, login_user_domain_name=None, login_domain_name=None, + insecure=True, ca_cert=None): + """Return a ks_client client object""" + + auth_plugin = None + if token: + auth_plugin = token_endpoint.Token(endpoint=auth_url, token=token) + else: + auth_plugin = v3.Password(auth_url=auth_url, username=login_username, password=login_password, + project_name=login_project_name, project_domain_name=login_project_domain_name, + user_domain_name=login_user_domain_name, domain_name=login_domain_name) + + # Force validation if ca_cert is provided + if ca_cert: + insecure = False + auth_session = session.Session( + auth=auth_plugin, verify=insecure, cert=ca_cert) + return v3client.Client(auth_url=auth_url, session=auth_session) + + +def _find_domain(ks_client, domain_name=None): + domains = ks_client.domains.list(name=domain_name) + return domains[0] if len(domains) else None + + +def _delete_domain(ks_client, domain=None): + + ks_client.domains.update(domain, enabled=False) + return ks_client.domains.delete(domain) + + +def _create_domain(ks_client, domain_name=None, description=None): + return ks_client.domains.create(name=domain_name, description=description) + + +def _find_user(ks_client, domain_name=None, user_name=None): + domain = _find_domain(ks_client, domain_name=domain_name) + + if domain: + users = ks_client.users.list(domain=domain, name=user_name) + if len(users): + return users[0] + + +def _create_user(ks_client, user_name=None, user_password=None, domain_name=None, + email=None, description=None): + + domain = _find_domain(ks_client, domain_name) + return ks_client.users.create(name=user_name, password=user_password, + description=description, + email=email, domain=domain) + + +def _delete_user(ks_client, user=None): + ks_client.users.update(user=user, enabled=False) + ks_client.users.delete(user=user) + + +def _find_project(ks_client, domain_name=None, project_name=None): + domain = _find_domain(ks_client, domain_name=domain_name) + + if domain: + projects = ks_client.projects.list(domain=domain, name=project_name) + if len(projects): + return projects[0] + + +def _create_project(ks_client, project_name=None, domain_name=None, + description=None): + + domain = _find_domain(ks_client, domain_name) + return ks_client.projects.create(name=project_name, + description=description, + domain=domain) + + +def _delete_project(ks_client, project=None): + ks_client.projects.update(project=project, enabled=False) + ks_client.projects.delete(project=project) + + +def _find_role(ks_client, role_name=None): + roles = ks_client.roles.list(name=role_name) + return roles[0] if len(roles) else None + + +def _delete_role(ks_client, role=None): + return ks_client.roles.delete(role) + + +def _create_role(ks_client, role_name=None, description=None): + return ks_client.roles.create(name=role_name, description=description) + + +def _grant_roles(ks_client, role=None, project=None, user=None, domain=None): + return ks_client.roles.grant(role=role, project=project, user=user, domain=domain) + + +def _revoke_roles(ks_client, role=None, project=None, user=None, domain=None): + return ks_client.roles.revoke(role=role, project=project, user=user, domain=domain) + +def _find_service(ks_client, service_name=None, service_type=None): + services = ks_client.services.list(name=service_name, + type=service_type) + return services[0] if len(services) else None + +def _create_service(ks_client, service_name=None, service_type=None, + description=None): + return ks_client.services.create(name=service_name, type=service_type, + description=description) + +def _delete_service(ks_client, service_name=None): + return ks_client.services.delete(name=service_name) + +def _find_endpoint(ks_client, service=None, interface=None): + + endpoints = ks_client.endpoints.list(service=service, interface=interface) + return endpoints[0] if len(endpoints) else None + +def _create_endpoint(ks_client, service=None, url=None, + interface=None, region=None): + return ks_client.endpoints.create(service=service, url=url, + interface=interface, region=region) + +def find_domain(ks_client, domain_name=None): + domain = _find_domain(ks_client, domain_name=domain_name) + result, domain = (True, domain) if (domain) else (False, None) + return result, domain + + +def create_domain(ks_client, domain_name=None, description=None): + + domain = _find_domain(ks_client, domain_name=domain_name) + + if domain: + return (False, domain) + + # Domain with that name doesn't exist + domain = _create_domain( + ks_client, domain_name=domain_name, description=description) + return (True, domain) + + +def delete_domain(ks_client, domain_name=None): + + domain = _find_domain(ks_client, domain_name=domain_name) + + if domain: + _delete_domain(ks_client, domain=domain) + return (True, domain) + + # Domain with that name doesn't exist + return (False, None) + + +def find_user(ks_client, user_name=None, domain_name=None): + user = _find_user(ks_client, domain_name=domain_name, user_name=user_name) + result, user = (True, user) if user else (False, None) + return result, user + + +def create_user(ks_client, user_name=None, user_password=None, domain_name=None, + email=None, description=None): + + user = _find_user(ks_client, user_name=user_name, domain_name=domain_name) + + if user: + return (False, user) + + # User with that name doesn't exist + user = _create_user(ks_client, user_name=user_name, + user_password=user_password, domain_name=domain_name, + email=email, description=description) + return (True, user) + + +def delete_user(ks_client, user_name=None, domain_name=None): + + user = _find_user(ks_client, domain_name=domain_name, user_name=user_name) + + if user: + _delete_user(ks_client, user=user) + return (True, user) + + # User with that name doesn't exist + return (False, None) + + +def find_project(ks_client, project_name=None, domain_name=None): + project = _find_project( + ks_client, domain_name=domain_name, project_name=project_name) + result, project = (True, project) if project else (False, None) + return result, project + + +def create_project(ks_client, project_name=None, domain_name=None, + description=None): + + project = _find_project( + ks_client, project_name=project_name, domain_name=domain_name) + + if project: + return (False, project) + + # Project with that name doesn't exist + project = _create_project(ks_client, project_name=project_name, + domain_name=domain_name, description=description) + return (True, project) + + +def delete_project(ks_client, project_name=None, domain_name=None): + + project = _find_project( + ks_client, domain_name=domain_name, project_name=project_name) + + if project: + _delete_project(ks_client, project=project) + return (True, project) + + # Project with that name doesn't exist + return (False, None) + + +def find_role(ks_client, role_name=None): + role = _find_role(ks_client, role_name=role_name) + result, role = (True, role) if (role) else (False, None) + return result, role + + +def create_role(ks_client, role_name=None, description=None): + + role = _find_role(ks_client, role_name=role_name) + + if role: + return (False, role) + + # role with that name doesn't exist + role = _create_role( + ks_client, role_name=role_name, description=description) + return (True, role) + + +def delete_role(ks_client, role_name=None): + + role = _find_role(ks_client, role_name=role_name) + + if role: + _delete_role(ks_client, role=role) + return (True, role) + + # role with that name doesn't exist + return (False, None) + + +def _grant_or_revoke_project_role(ks_client, role_name=None, user_name=None, project_name=None, + user_domain_name=None, project_domain_name=None, grant=True): + + role = _find_role(ks_client, role_name=role_name) + user = _find_user( + ks_client, user_name=user_name, domain_name=user_domain_name) + project = _find_project( + ks_client, project_name=project_name, domain_name=project_domain_name) + + if (user and role and project): + if (grant): + _grant_roles(ks_client, role=role, project=project, user=user) + else: + _revoke_roles(ks_client, role=role, project=project, user=user) + return (True, "OK") + + return (False, "Not able to find user/role/project with the given inputs") + + +def _grant_or_revoke_domain_role(ks_client, role_name=None, user_name=None, + user_domain_name=None, domain_name=None, grant=True): + + role = _find_role(ks_client, role_name=role_name) + user = _find_user( + ks_client, user_name=user_name, domain_name=user_domain_name) + domain = _find_domain(ks_client, domain_name=domain_name) + + if (user and role and domain): + if (grant): + _grant_roles(ks_client, role=role, domain=domain, user=user) + else: + _revoke_roles(ks_client, role=role, domain=domain, user=user) + return (True, "OK") + + return (False, "Not able to find user/role/domain with the given inputs") + + +def grant_project_role(ks_client, role_name=None, user_name=None, project_name=None, + user_domain_name=None, project_domain_name=None): + + return _grant_or_revoke_project_role(ks_client, role_name=role_name, user_name=user_name, + project_name=project_name, user_domain_name=user_domain_name, + project_domain_name=project_domain_name, grant=True) + + +def revoke_project_role(ks_client, role_name=None, user_name=None, project_name=None, + user_domain_name=None, project_domain_name=None): + + return _grant_or_revoke_project_role(ks_client, role_name=role_name, user_name=user_name, + project_name=project_name, user_domain_name=user_domain_name, + project_domain_name=project_domain_name, grant=False) + + +def grant_domain_role(ks_client, role_name=None, user_name=None, domain_name=None, + user_domain_name=None): + + return _grant_or_revoke_domain_role(ks_client, role_name=role_name, user_name=user_name, + user_domain_name=user_domain_name, + domain_name=domain_name, grant=True) + + +def revoke_domain_role(ks_client, role_name=None, user_name=None, domain_name=None, + user_domain_name=None): + return _grant_or_revoke_domain_role(ks_client, role_name=role_name, user_name=user_name, + user_domain_name=user_domain_name, + domain_name=domain_name, grant=False) + +def create_service(ks_client, service_name=None, service_type=None, + description=None): + + service = _find_service(ks_client, service_name=service_name, + service_type=service_type) + + if service: + return (False, service) + + # Service with that name doesn't exist + service = _create_service( + ks_client, service_name=service_name, service_type=service_type, + description=description) + return (True, service) + +def create_endpoint(ks_client, service_name=None, region=None, + admin_url=None, internal_url=None, + public_url=None): + + service = _find_service(ks_client, service_name=service_name) + + if not service: + raise Exception("Service with the name=%s doesn't exist" %(service_name)) + + # Here we are checking only public endpoint, that should be fine + endpoint = _find_endpoint(ks_client, service=service, interface="public") + if endpoint: + return (False, endpoint) + + public_endpoint = _create_endpoint( + ks_client, service=service, interface="public", region=region, + url=public_url) + internal_endpoint = _create_endpoint( + ks_client, service=service, interface="internal", region=region, + url=internal_url) + public_endpint = _create_endpoint( + ks_client, service=service, interface="admin", region=region, + url=admin_url) + + return (True, public_endpint) + +def process_params(module): + + user_name = module.params.get("user_name", None) + user_password = module.params.get("user_password", None) + email = module.params.get("email", None) + description = module.params.get("description", None) + user_domain_name = module.params.get("user_domain_name", None) + + domain_name = module.params.get("domain_name", None) + + project_name = module.params.get("project_name", None) + project_domain_name = module.params.get("project_domain_name", None) + + role_name = module.params.get("role_name", None) + + service_name = module.params.get("service_name", None) + service_type = module.params.get("service_type", None) + region = module.params.get("region", None) + admin_url = module.params.get("admin_url", None) + internal_url = module.params.get("internal_url", None) + public_url = module.params.get("public_url", None) + + action = module.params["action"] + + if (action == "find_domain" or action == "delete_domain"): + kwargs = dict(domain_name=domain_name) + elif (action == "create_domain"): + kwargs = dict(domain_name=domain_name, description=description) + elif (action == "find_user" or action == "delete_user"): + kwargs = dict(domain_name=user_domain_name, user_name=user_name) + elif (action == "create_user"): + kwargs = dict(domain_name=user_domain_name, description=description, user_name=user_name, + email=email, user_password=user_password) + elif (action == "find_project" or action == "delete_project"): + kwargs = dict( + domain_name=project_domain_name, project_name=project_name) + elif (action == "create_project"): + kwargs = dict(domain_name=project_domain_name, description=description, + project_name=project_name) + elif (action == "find_role" or action == "delete_role"): + kwargs = dict(role_name=role_name) + elif (action == "create_role"): + kwargs = dict(role_name=role_name, description=description) + elif (action == "grant_project_role" or action == "revoke_project_role"): + kwargs = dict(role_name=role_name, user_name=user_name, + user_domain_name=user_domain_name, project_name=project_name, + project_domain_name=project_domain_name) + elif (action == "grant_domain_role" or action == "revoke_domain_role"): + kwargs = dict(role_name=role_name, user_name=user_name, + user_domain_name=user_domain_name, + domain_name=domain_name) + elif (action == "create_service"): + kwargs = dict(service_name=service_name, service_type=service_type, + description=description) + elif (action == "create_endpoint"): + kwargs = dict(service_name=service_name, region=region, + admin_url=admin_url, internal_url=internal_url, + public_url=public_url) + + return kwargs + +dispatch_map = { + "find_domain": find_domain, + "delete_domain": delete_domain, + "create_domain": create_domain, + + "find_user": find_user, + "delete_user": delete_user, + "create_user": create_user, + + "find_project": find_project, + "delete_project": delete_project, + "create_project": create_project, + + "find_role": find_role, + "delete_role": delete_role, + "create_role": create_role, + + "grant_project_role": grant_project_role, + "revoke_project_role": revoke_project_role, + "grant_domain_role": grant_domain_role, + "revoke_domain_role": revoke_domain_role, + + "create_service": create_service, + "create_endpoint": create_endpoint, + +} + + +def get_client(module): + + login_username = module.params.get("login_username") + login_project_name = module.params.get("login_project_name") + login_user_domain_name = module.params.get("login_user_domain_name") + login_project_domain_name = module.params.get("login_project_domain_name") + login_password = module.params.get("login_password") + auth_url = module.params.get("endpoint") + token = module.params.get("login_token") + + ks_client = _get_client(login_username=login_username, + login_project_name=login_project_name, + login_user_domain_name=login_user_domain_name, + login_project_domain_name=login_project_domain_name, + login_password=login_password, + auth_url=auth_url, + token=token) + + return ks_client + + +def process_module_action(module): + + ks_client = get_client(module) + + action = module.params["action"] + kwargs = process_params(module) + + try: + result = dispatch_map[action](ks_client, **kwargs) + except Exception as e: + module.fail_json(msg="%s, failed" % e) + else: + status, resource_data = result + data = dict(changed=status, result=str(resource_data)) + module.exit_json(**data) + + +def main(): + + supported_actions = dispatch_map.keys() + + argument_spec = dict( + login_username=dict(default=None), + login_password=dict(default=None), + login_project_name=dict(default=None), + login_project_domain_name=dict(default=None), + login_user_domain_name=dict(default=None), + login_domain_name=dict(default=None), + login_token=dict(default=None), + + endpoint=dict(default=None), + description=dict(default="Created by Ansible keystone_v3"), + email=dict(default=None), + user_name=dict(default=None), + user_password=dict(default=None), + user_domain_name=dict(default=None), + project_name=dict(default=None), + project_domain_name=dict(default=None), + domain_name=dict(default=None), + role_name=dict(default=None), + + service_name=dict(default=None), + service_type=dict(default=None), + + region=dict(default=None), + admin_url=dict(default=None), + public_url=dict(default=None), + internal_url=dict(default=None), + + action=dict(default=None, required=True, choices=supported_actions) + + ) + + module = AnsibleModule( + argument_spec=argument_spec, supports_check_mode=False) + process_module_action(module) + +from ansible.module_utils.basic import * +from ansible.module_utils.database import * + + +if __name__ == '__main__': + main() From 02e91f88970684f126f8030c35691c18c249ee08 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 12 Jun 2015 14:10:11 +0200 Subject: [PATCH 1127/2522] Extract module for keystone domain management --- cloud/openstack/os_keystone.py | 577 -------------------------- cloud/openstack/os_keystone_domain.py | 123 ++++++ 2 files changed, 123 insertions(+), 577 deletions(-) delete mode 100644 cloud/openstack/os_keystone.py create mode 100644 cloud/openstack/os_keystone_domain.py diff --git a/cloud/openstack/os_keystone.py b/cloud/openstack/os_keystone.py deleted file mode 100644 index f2455bb0e10..00000000000 --- a/cloud/openstack/os_keystone.py +++ /dev/null @@ -1,577 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -from __builtin__ import False - -DOCUMENTATION = ''' ---- -module: keystone_v3 -version_added: "1.0" -short_description: Manage OpenStack Identity (keystone v3) users, tenants and roles -description: - - Manage users,tenants, roles from OpenStack. -requirements: [ python-keystoneclient ] -author: Haneef Ali -''' - -EXAMPLES = ''' -# Create a project -- keystone_v3: action="create_project" project_name=demo - description="Default Tenant" project_domain_name="Default" - -# Create a user -- keystone_v3: action="create_user" user_name=demo - description="Default User" user_domain_name="Default" - -# Create a domain -- keystone_v3: action="create_domain" domain_name=demo - description="Default User" - -# Grant admin role to the john user in the demo tenant -- keystone_v3: action="grant_project_role" project__name=demo - role_name=admin user_name=john user_domain_name=Default - project_domain_name=Default -''' - - -from keystoneclient.v3 import client as v3client -from keystoneclient.auth.identity import v3 -from keystoneclient.auth import token_endpoint -from keystoneclient import session - - -def _get_client(auth_url=None, token=None, login_username=None, login_password=None, login_project_name=None, - login_project_domain_name=None, login_user_domain_name=None, login_domain_name=None, - insecure=True, ca_cert=None): - """Return a ks_client client object""" - - auth_plugin = None - if token: - auth_plugin = token_endpoint.Token(endpoint=auth_url, token=token) - else: - auth_plugin = v3.Password(auth_url=auth_url, username=login_username, password=login_password, - project_name=login_project_name, project_domain_name=login_project_domain_name, - user_domain_name=login_user_domain_name, domain_name=login_domain_name) - - # Force validation if ca_cert is provided - if ca_cert: - insecure = False - auth_session = session.Session( - auth=auth_plugin, verify=insecure, cert=ca_cert) - return v3client.Client(auth_url=auth_url, session=auth_session) - - -def _find_domain(ks_client, domain_name=None): - domains = ks_client.domains.list(name=domain_name) - return domains[0] if len(domains) else None - - -def _delete_domain(ks_client, domain=None): - - ks_client.domains.update(domain, enabled=False) - return ks_client.domains.delete(domain) - - -def _create_domain(ks_client, domain_name=None, description=None): - return ks_client.domains.create(name=domain_name, description=description) - - -def _find_user(ks_client, domain_name=None, user_name=None): - domain = _find_domain(ks_client, domain_name=domain_name) - - if domain: - users = ks_client.users.list(domain=domain, name=user_name) - if len(users): - return users[0] - - -def _create_user(ks_client, user_name=None, user_password=None, domain_name=None, - email=None, description=None): - - domain = _find_domain(ks_client, domain_name) - return ks_client.users.create(name=user_name, password=user_password, - description=description, - email=email, domain=domain) - - -def _delete_user(ks_client, user=None): - ks_client.users.update(user=user, enabled=False) - ks_client.users.delete(user=user) - - -def _find_project(ks_client, domain_name=None, project_name=None): - domain = _find_domain(ks_client, domain_name=domain_name) - - if domain: - projects = ks_client.projects.list(domain=domain, name=project_name) - if len(projects): - return projects[0] - - -def _create_project(ks_client, project_name=None, domain_name=None, - description=None): - - domain = _find_domain(ks_client, domain_name) - return ks_client.projects.create(name=project_name, - description=description, - domain=domain) - - -def _delete_project(ks_client, project=None): - ks_client.projects.update(project=project, enabled=False) - ks_client.projects.delete(project=project) - - -def _find_role(ks_client, role_name=None): - roles = ks_client.roles.list(name=role_name) - return roles[0] if len(roles) else None - - -def _delete_role(ks_client, role=None): - return ks_client.roles.delete(role) - - -def _create_role(ks_client, role_name=None, description=None): - return ks_client.roles.create(name=role_name, description=description) - - -def _grant_roles(ks_client, role=None, project=None, user=None, domain=None): - return ks_client.roles.grant(role=role, project=project, user=user, domain=domain) - - -def _revoke_roles(ks_client, role=None, project=None, user=None, domain=None): - return ks_client.roles.revoke(role=role, project=project, user=user, domain=domain) - -def _find_service(ks_client, service_name=None, service_type=None): - services = ks_client.services.list(name=service_name, - type=service_type) - return services[0] if len(services) else None - -def _create_service(ks_client, service_name=None, service_type=None, - description=None): - return ks_client.services.create(name=service_name, type=service_type, - description=description) - -def _delete_service(ks_client, service_name=None): - return ks_client.services.delete(name=service_name) - -def _find_endpoint(ks_client, service=None, interface=None): - - endpoints = ks_client.endpoints.list(service=service, interface=interface) - return endpoints[0] if len(endpoints) else None - -def _create_endpoint(ks_client, service=None, url=None, - interface=None, region=None): - return ks_client.endpoints.create(service=service, url=url, - interface=interface, region=region) - -def find_domain(ks_client, domain_name=None): - domain = _find_domain(ks_client, domain_name=domain_name) - result, domain = (True, domain) if (domain) else (False, None) - return result, domain - - -def create_domain(ks_client, domain_name=None, description=None): - - domain = _find_domain(ks_client, domain_name=domain_name) - - if domain: - return (False, domain) - - # Domain with that name doesn't exist - domain = _create_domain( - ks_client, domain_name=domain_name, description=description) - return (True, domain) - - -def delete_domain(ks_client, domain_name=None): - - domain = _find_domain(ks_client, domain_name=domain_name) - - if domain: - _delete_domain(ks_client, domain=domain) - return (True, domain) - - # Domain with that name doesn't exist - return (False, None) - - -def find_user(ks_client, user_name=None, domain_name=None): - user = _find_user(ks_client, domain_name=domain_name, user_name=user_name) - result, user = (True, user) if user else (False, None) - return result, user - - -def create_user(ks_client, user_name=None, user_password=None, domain_name=None, - email=None, description=None): - - user = _find_user(ks_client, user_name=user_name, domain_name=domain_name) - - if user: - return (False, user) - - # User with that name doesn't exist - user = _create_user(ks_client, user_name=user_name, - user_password=user_password, domain_name=domain_name, - email=email, description=description) - return (True, user) - - -def delete_user(ks_client, user_name=None, domain_name=None): - - user = _find_user(ks_client, domain_name=domain_name, user_name=user_name) - - if user: - _delete_user(ks_client, user=user) - return (True, user) - - # User with that name doesn't exist - return (False, None) - - -def find_project(ks_client, project_name=None, domain_name=None): - project = _find_project( - ks_client, domain_name=domain_name, project_name=project_name) - result, project = (True, project) if project else (False, None) - return result, project - - -def create_project(ks_client, project_name=None, domain_name=None, - description=None): - - project = _find_project( - ks_client, project_name=project_name, domain_name=domain_name) - - if project: - return (False, project) - - # Project with that name doesn't exist - project = _create_project(ks_client, project_name=project_name, - domain_name=domain_name, description=description) - return (True, project) - - -def delete_project(ks_client, project_name=None, domain_name=None): - - project = _find_project( - ks_client, domain_name=domain_name, project_name=project_name) - - if project: - _delete_project(ks_client, project=project) - return (True, project) - - # Project with that name doesn't exist - return (False, None) - - -def find_role(ks_client, role_name=None): - role = _find_role(ks_client, role_name=role_name) - result, role = (True, role) if (role) else (False, None) - return result, role - - -def create_role(ks_client, role_name=None, description=None): - - role = _find_role(ks_client, role_name=role_name) - - if role: - return (False, role) - - # role with that name doesn't exist - role = _create_role( - ks_client, role_name=role_name, description=description) - return (True, role) - - -def delete_role(ks_client, role_name=None): - - role = _find_role(ks_client, role_name=role_name) - - if role: - _delete_role(ks_client, role=role) - return (True, role) - - # role with that name doesn't exist - return (False, None) - - -def _grant_or_revoke_project_role(ks_client, role_name=None, user_name=None, project_name=None, - user_domain_name=None, project_domain_name=None, grant=True): - - role = _find_role(ks_client, role_name=role_name) - user = _find_user( - ks_client, user_name=user_name, domain_name=user_domain_name) - project = _find_project( - ks_client, project_name=project_name, domain_name=project_domain_name) - - if (user and role and project): - if (grant): - _grant_roles(ks_client, role=role, project=project, user=user) - else: - _revoke_roles(ks_client, role=role, project=project, user=user) - return (True, "OK") - - return (False, "Not able to find user/role/project with the given inputs") - - -def _grant_or_revoke_domain_role(ks_client, role_name=None, user_name=None, - user_domain_name=None, domain_name=None, grant=True): - - role = _find_role(ks_client, role_name=role_name) - user = _find_user( - ks_client, user_name=user_name, domain_name=user_domain_name) - domain = _find_domain(ks_client, domain_name=domain_name) - - if (user and role and domain): - if (grant): - _grant_roles(ks_client, role=role, domain=domain, user=user) - else: - _revoke_roles(ks_client, role=role, domain=domain, user=user) - return (True, "OK") - - return (False, "Not able to find user/role/domain with the given inputs") - - -def grant_project_role(ks_client, role_name=None, user_name=None, project_name=None, - user_domain_name=None, project_domain_name=None): - - return _grant_or_revoke_project_role(ks_client, role_name=role_name, user_name=user_name, - project_name=project_name, user_domain_name=user_domain_name, - project_domain_name=project_domain_name, grant=True) - - -def revoke_project_role(ks_client, role_name=None, user_name=None, project_name=None, - user_domain_name=None, project_domain_name=None): - - return _grant_or_revoke_project_role(ks_client, role_name=role_name, user_name=user_name, - project_name=project_name, user_domain_name=user_domain_name, - project_domain_name=project_domain_name, grant=False) - - -def grant_domain_role(ks_client, role_name=None, user_name=None, domain_name=None, - user_domain_name=None): - - return _grant_or_revoke_domain_role(ks_client, role_name=role_name, user_name=user_name, - user_domain_name=user_domain_name, - domain_name=domain_name, grant=True) - - -def revoke_domain_role(ks_client, role_name=None, user_name=None, domain_name=None, - user_domain_name=None): - return _grant_or_revoke_domain_role(ks_client, role_name=role_name, user_name=user_name, - user_domain_name=user_domain_name, - domain_name=domain_name, grant=False) - -def create_service(ks_client, service_name=None, service_type=None, - description=None): - - service = _find_service(ks_client, service_name=service_name, - service_type=service_type) - - if service: - return (False, service) - - # Service with that name doesn't exist - service = _create_service( - ks_client, service_name=service_name, service_type=service_type, - description=description) - return (True, service) - -def create_endpoint(ks_client, service_name=None, region=None, - admin_url=None, internal_url=None, - public_url=None): - - service = _find_service(ks_client, service_name=service_name) - - if not service: - raise Exception("Service with the name=%s doesn't exist" %(service_name)) - - # Here we are checking only public endpoint, that should be fine - endpoint = _find_endpoint(ks_client, service=service, interface="public") - if endpoint: - return (False, endpoint) - - public_endpoint = _create_endpoint( - ks_client, service=service, interface="public", region=region, - url=public_url) - internal_endpoint = _create_endpoint( - ks_client, service=service, interface="internal", region=region, - url=internal_url) - public_endpint = _create_endpoint( - ks_client, service=service, interface="admin", region=region, - url=admin_url) - - return (True, public_endpint) - -def process_params(module): - - user_name = module.params.get("user_name", None) - user_password = module.params.get("user_password", None) - email = module.params.get("email", None) - description = module.params.get("description", None) - user_domain_name = module.params.get("user_domain_name", None) - - domain_name = module.params.get("domain_name", None) - - project_name = module.params.get("project_name", None) - project_domain_name = module.params.get("project_domain_name", None) - - role_name = module.params.get("role_name", None) - - service_name = module.params.get("service_name", None) - service_type = module.params.get("service_type", None) - region = module.params.get("region", None) - admin_url = module.params.get("admin_url", None) - internal_url = module.params.get("internal_url", None) - public_url = module.params.get("public_url", None) - - action = module.params["action"] - - if (action == "find_domain" or action == "delete_domain"): - kwargs = dict(domain_name=domain_name) - elif (action == "create_domain"): - kwargs = dict(domain_name=domain_name, description=description) - elif (action == "find_user" or action == "delete_user"): - kwargs = dict(domain_name=user_domain_name, user_name=user_name) - elif (action == "create_user"): - kwargs = dict(domain_name=user_domain_name, description=description, user_name=user_name, - email=email, user_password=user_password) - elif (action == "find_project" or action == "delete_project"): - kwargs = dict( - domain_name=project_domain_name, project_name=project_name) - elif (action == "create_project"): - kwargs = dict(domain_name=project_domain_name, description=description, - project_name=project_name) - elif (action == "find_role" or action == "delete_role"): - kwargs = dict(role_name=role_name) - elif (action == "create_role"): - kwargs = dict(role_name=role_name, description=description) - elif (action == "grant_project_role" or action == "revoke_project_role"): - kwargs = dict(role_name=role_name, user_name=user_name, - user_domain_name=user_domain_name, project_name=project_name, - project_domain_name=project_domain_name) - elif (action == "grant_domain_role" or action == "revoke_domain_role"): - kwargs = dict(role_name=role_name, user_name=user_name, - user_domain_name=user_domain_name, - domain_name=domain_name) - elif (action == "create_service"): - kwargs = dict(service_name=service_name, service_type=service_type, - description=description) - elif (action == "create_endpoint"): - kwargs = dict(service_name=service_name, region=region, - admin_url=admin_url, internal_url=internal_url, - public_url=public_url) - - return kwargs - -dispatch_map = { - "find_domain": find_domain, - "delete_domain": delete_domain, - "create_domain": create_domain, - - "find_user": find_user, - "delete_user": delete_user, - "create_user": create_user, - - "find_project": find_project, - "delete_project": delete_project, - "create_project": create_project, - - "find_role": find_role, - "delete_role": delete_role, - "create_role": create_role, - - "grant_project_role": grant_project_role, - "revoke_project_role": revoke_project_role, - "grant_domain_role": grant_domain_role, - "revoke_domain_role": revoke_domain_role, - - "create_service": create_service, - "create_endpoint": create_endpoint, - -} - - -def get_client(module): - - login_username = module.params.get("login_username") - login_project_name = module.params.get("login_project_name") - login_user_domain_name = module.params.get("login_user_domain_name") - login_project_domain_name = module.params.get("login_project_domain_name") - login_password = module.params.get("login_password") - auth_url = module.params.get("endpoint") - token = module.params.get("login_token") - - ks_client = _get_client(login_username=login_username, - login_project_name=login_project_name, - login_user_domain_name=login_user_domain_name, - login_project_domain_name=login_project_domain_name, - login_password=login_password, - auth_url=auth_url, - token=token) - - return ks_client - - -def process_module_action(module): - - ks_client = get_client(module) - - action = module.params["action"] - kwargs = process_params(module) - - try: - result = dispatch_map[action](ks_client, **kwargs) - except Exception as e: - module.fail_json(msg="%s, failed" % e) - else: - status, resource_data = result - data = dict(changed=status, result=str(resource_data)) - module.exit_json(**data) - - -def main(): - - supported_actions = dispatch_map.keys() - - argument_spec = dict( - login_username=dict(default=None), - login_password=dict(default=None), - login_project_name=dict(default=None), - login_project_domain_name=dict(default=None), - login_user_domain_name=dict(default=None), - login_domain_name=dict(default=None), - login_token=dict(default=None), - - endpoint=dict(default=None), - description=dict(default="Created by Ansible keystone_v3"), - email=dict(default=None), - user_name=dict(default=None), - user_password=dict(default=None), - user_domain_name=dict(default=None), - project_name=dict(default=None), - project_domain_name=dict(default=None), - domain_name=dict(default=None), - role_name=dict(default=None), - - service_name=dict(default=None), - service_type=dict(default=None), - - region=dict(default=None), - admin_url=dict(default=None), - public_url=dict(default=None), - internal_url=dict(default=None), - - action=dict(default=None, required=True, choices=supported_actions) - - ) - - module = AnsibleModule( - argument_spec=argument_spec, supports_check_mode=False) - process_module_action(module) - -from ansible.module_utils.basic import * -from ansible.module_utils.database import * - - -if __name__ == '__main__': - main() diff --git a/cloud/openstack/os_keystone_domain.py b/cloud/openstack/os_keystone_domain.py new file mode 100644 index 00000000000..cbb992eff57 --- /dev/null +++ b/cloud/openstack/os_keystone_domain.py @@ -0,0 +1,123 @@ +#!/usr/bin/python +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + + +try: + import shade + from shade import meta + HAS_SHADE = True +except ImportError: + HAS_SHADE = False + +DOCUMENTATION = ''' +--- +module: os_keystone_domain +short_description: Manage OpenStack Identity Domains +extends_documentation_fragment: openstack +version_added: "2.1" +description: + - Manage OpenStack Identity Domains +options: + name: + description: + - Name that has to be given to the instance + required: true + description: + description: + - Description of the domain + required: false + default: None + enabled: + description: + - Is the domain enabled + required: false + default: True + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present +requirements: + - "python >= 2.6" + - "shade" +''' + +EXAMPLES = ''' +# Create a domain +- os_keystone_domain: name=demo description="Demo Domain" +''' + + +def main(): + + argument_spec = openstack_full_argument_spec( + name=dict(required=True), + description=dict(default=None), + enabled=dict(default=True, type='bool'), + state=dict(default='present', choices=['absent', 'present']), + # Override endpoint_type default since this is an admin function + endpoint_type=dict( + default='admin', choices=['public', 'internal', 'admin']), + )) + + module_kwargs = openstack_module_kwargs() + module = AnsibleModule(argument_spec, **module_kwargs) + + if not HAS_SHADE: + module.fail_json(msg='shade is required for this module') + + name = module.params.pop('name') + description = module.params.pop('description') + enabled = module.params.pop('enabled') + state = module.params.pop('state') + + try: + cloud = shade.operator_cloud(**module.params) + + domain = cloud.get_identity_domain(name=name) + + if state == 'present': + if domain is None: + domain = cloud.create_domain( + name=name, description=description, enabled=enabled) + changed = True + else: + if (domain.name != name or domain.description != description + or domain.enabled != enabled): + cloud.update_domain( + domain.id, name=name, description=description, + enabled=enabled) + changed = True + else: + changed = False + module.exit_json(changed=changed, domain=domain) + elif state == 'absent': + if domain is None: + changed=False + else: + cloud.delete_domain(domain.id) + changed=True + module.exit_json(changed=changed) + + except shade.OpenStackCloudException as e: + module.fail_json(msg=e.message, extra_data=e.extra_data) + +from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * + + +if __name__ == '__main__': + main() From f798240f436a16a828f48759bbd176b6bccdfe75 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 4 Nov 2015 15:23:08 -0500 Subject: [PATCH 1128/2522] Update Keystone Domain module for latest shade --- cloud/openstack/os_keystone_domain.py | 106 +++++++++++++++++++++----- 1 file changed, 87 insertions(+), 19 deletions(-) diff --git a/cloud/openstack/os_keystone_domain.py b/cloud/openstack/os_keystone_domain.py index cbb992eff57..ef9e5eb9eba 100644 --- a/cloud/openstack/os_keystone_domain.py +++ b/cloud/openstack/os_keystone_domain.py @@ -17,7 +17,6 @@ try: import shade - from shade import meta HAS_SHADE = True except ImportError: HAS_SHADE = False @@ -29,7 +28,9 @@ extends_documentation_fragment: openstack version_added: "2.1" description: - - Manage OpenStack Identity Domains + - Create, update, or delete OpenStack Identity domains. If a domain + with the supplied name already exists, it will be updated with the + new description and enabled attributes. options: name: description: @@ -57,9 +58,67 @@ EXAMPLES = ''' # Create a domain -- os_keystone_domain: name=demo description="Demo Domain" +- os_keystone_domain: + cloud: mycloud + state: present + name: demo + description: Demo Domain + +# Delete a domain +- os_keystone_domain: + cloud: mycloud + state: absent + name: demo ''' +RETURN = ''' +domain: + description: Dictionary describing the domain. + returned: On success when I(state) is 'present' + type: dictionary + contains: + id: + description: Domain ID. + type: string + sample: "474acfe5-be34-494c-b339-50f06aa143e4" + name: + description: Domain name. + type: string + sample: "demo" + description: + description: Domain description. + type: string + sample: "Demo Domain" + enabled: + description: Domain description. + type: boolean + sample: True + +id: + description: The domain ID. + returned: On success when I(state) is 'present' + type: string + sample: "474acfe5-be34-494c-b339-50f06aa143e4" +''' + +def _needs_update(module, domain): + if domain.description != module.params['description']: + return True + if domain.enabled != module.params['enabled']: + return True + return False + +def _system_state_change(module, domain): + state = module.params['state'] + if state == 'absent' and domain: + return True + + if state == 'present': + if domain is None: + return True + return _needs_update(module, domain) + + return False def main(): @@ -68,26 +127,35 @@ def main(): description=dict(default=None), enabled=dict(default=True, type='bool'), state=dict(default='present', choices=['absent', 'present']), - # Override endpoint_type default since this is an admin function - endpoint_type=dict( - default='admin', choices=['public', 'internal', 'admin']), - )) + ) module_kwargs = openstack_module_kwargs() - module = AnsibleModule(argument_spec, **module_kwargs) + module = AnsibleModule(argument_spec, + supports_check_mode=True, + **module_kwargs) if not HAS_SHADE: module.fail_json(msg='shade is required for this module') - name = module.params.pop('name') - description = module.params.pop('description') - enabled = module.params.pop('enabled') - state = module.params.pop('state') + name = module.params['name'] + description = module.params['description'] + enabled = module.params['enabled'] + state = module.params['state'] try: cloud = shade.operator_cloud(**module.params) - domain = cloud.get_identity_domain(name=name) + domains = cloud.search_domains(filters=dict(name=name)) + + if len(domains) > 1: + module.fail_json(msg='Domain name %s is not unique' % name) + elif len(domains) == 1: + domain = domains[0] + else: + domain = None + + if module.check_mode: + module.exit_json(changed=_system_state_change(module, domain)) if state == 'present': if domain is None: @@ -95,15 +163,15 @@ def main(): name=name, description=description, enabled=enabled) changed = True else: - if (domain.name != name or domain.description != description - or domain.enabled != enabled): - cloud.update_domain( + if _needs_update(module, domain): + domain = cloud.update_domain( domain.id, name=name, description=description, enabled=enabled) changed = True else: changed = False - module.exit_json(changed=changed, domain=domain) + module.exit_json(changed=changed, domain=domain, id=domain.id) + elif state == 'absent': if domain is None: changed=False @@ -113,11 +181,11 @@ def main(): module.exit_json(changed=changed) except shade.OpenStackCloudException as e: - module.fail_json(msg=e.message, extra_data=e.extra_data) + module.fail_json(msg=e.message) + from ansible.module_utils.basic import * from ansible.module_utils.openstack import * - if __name__ == '__main__': main() From b1d6c337128e9e63b60709aee33d58556703ebee Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 19 Jan 2016 11:58:19 -0500 Subject: [PATCH 1129/2522] Add new os_keystone_role module. This new module allows for creating and deleting Keystone roles. --- cloud/openstack/os_keystone_role.py | 136 ++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 cloud/openstack/os_keystone_role.py diff --git a/cloud/openstack/os_keystone_role.py b/cloud/openstack/os_keystone_role.py new file mode 100644 index 00000000000..def91a8b326 --- /dev/null +++ b/cloud/openstack/os_keystone_role.py @@ -0,0 +1,136 @@ +#!/usr/bin/python +# Copyright (c) 2016 IBM +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + + +try: + import shade + HAS_SHADE = True +except ImportError: + HAS_SHADE = False + +DOCUMENTATION = ''' +--- +module: os_keystone_role +short_description: Manage OpenStack Identity Roles +extends_documentation_fragment: openstack +version_added: "2.1" +author: "Monty Taylor (@emonty), David Shrewsbury (@Shrews)" +description: + - Manage OpenStack Identity Roles. +options: + name: + description: + - Role Name + required: true + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present +requirements: + - "python >= 2.6" + - "shade" +''' + +EXAMPLES = ''' +# Create a role named "demo" +- os_keystone_role: + cloud: mycloud + state: present + name: demo + +# Delete the role named "demo" +- os_keystone_role: + cloud: mycloud + state: absent + name: demo +''' + +RETURN = ''' +role: + description: Dictionary describing the role. + returned: On success when I(state) is 'present'. + type: dictionary + contains: + id: + description: Unique role ID. + type: string + sample: "677bfab34c844a01b88a217aa12ec4c2" + name: + description: Role name. + type: string + sample: "demo" +''' + + +def _system_state_change(state, role): + if state == 'present' and not role: + return True + if state == 'absent' and role: + return True + return False + + +def main(): + argument_spec = openstack_full_argument_spec( + name=dict(required=True), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = openstack_module_kwargs() + module = AnsibleModule(argument_spec, + supports_check_mode=True, + **module_kwargs) + + if not HAS_SHADE: + module.fail_json(msg='shade is required for this module') + + name = module.params.pop('name') + state = module.params.pop('state') + + try: + cloud = shade.operator_cloud(**module.params) + + role = cloud.get_role(name) + + if module.check_mode: + module.exit_json(changed=_system_state_change(state, role)) + + if state == 'present': + if role is None: + role = cloud.create_role(name) + changed = True + else: + changed = False + module.exit_json(changed=changed, role=role) + elif state == 'absent': + if role is None: + changed=False + else: + cloud.delete_role(name) + changed=True + module.exit_json(changed=changed) + + except shade.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + + +from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * + + +if __name__ == '__main__': + main() From 46b4622eba9c65bf96dcaed0d23ecbc560288c76 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 19 Jan 2016 12:17:36 -0500 Subject: [PATCH 1130/2522] Fix exception output for os_keystone_domain. The message attribute of a shade exception is not very helpful. Converting to a full string will contain many more details. --- cloud/openstack/os_keystone_domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/openstack/os_keystone_domain.py b/cloud/openstack/os_keystone_domain.py index ef9e5eb9eba..bed2f0410ce 100644 --- a/cloud/openstack/os_keystone_domain.py +++ b/cloud/openstack/os_keystone_domain.py @@ -181,7 +181,7 @@ def main(): module.exit_json(changed=changed) except shade.OpenStackCloudException as e: - module.fail_json(msg=e.message) + module.fail_json(msg=str(e)) from ansible.module_utils.basic import * From 9476366d4e724a80abc90cafc1288c5352212dcb Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 19 Jan 2016 11:37:32 -0600 Subject: [PATCH 1131/2522] Add functionality to give multiple iterative responses for a question in expect --- commands/expect.py | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/commands/expect.py b/commands/expect.py index e8f7a049836..5a7d7dba83b 100644 --- a/commands/expect.py +++ b/commands/expect.py @@ -56,7 +56,9 @@ required: false responses: description: - - Mapping of expected string and string to respond with + - Mapping of expected string/regex and string to respond with. If the + response is a list, successive matches return successive + responses. List functionality is new in 2.1. required: true timeout: description: @@ -73,17 +75,48 @@ - If you want to run a command through the shell (say you are using C(<), C(>), C(|), etc), you must specify a shell in the command such as C(/bin/bash -c "/path/to/something | grep else") + - The question, or key, under I(responses) is a python regex match. Case + insensitive searches are indicated with a prefix of C(?i) + - By default, if a question is encountered multiple times, it's string + response will be repeated. If you need different responses for successive + question matches, instead of a string response, use a list of strings as + the response. The list functionality is new in 2.1 author: "Matt Martz (@sivel)" ''' EXAMPLES = ''' +# Case insensitve password string match - expect: command: passwd username responses: (?i)password: "MySekretPa$$word" + +# Generic question with multiple different responses +- expect: + command: /path/to/custom/command + responses: + Question: + - response1 + - response2 + - response3 ''' +def response_closure(module, question, responses): + resp_gen = (u'%s\n' % r.rstrip('\n').decode() for r in responses) + + def wrapped(info): + try: + return resp_gen.next() + except StopIteration: + module.fail_json(msg="No remaining responses for '%s', " + "output was '%s'" % + (question, + info['child_result_list'][-1])) + + return wrapped + + def main(): module = AnsibleModule( argument_spec=dict( @@ -110,7 +143,12 @@ def main(): events = dict() for key, value in responses.iteritems(): - events[key.decode()] = u'%s\n' % value.rstrip('\n').decode() + if isinstance(value, list): + response = response_closure(module, key, value) + else: + response = u'%s\n' % value.rstrip('\n').decode() + + events[key.decode()] = response if args.strip() == '': module.fail_json(rc=256, msg="no command given") From 4094154afa2b93c130c341e786d2b254fbd17f8a Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 20 Jan 2016 12:25:42 -0500 Subject: [PATCH 1132/2522] Add os_group.py OpenStack module Allows an admin (or privileged user) to manage Keystone v3 groups. --- cloud/openstack/os_group.py | 167 ++++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 cloud/openstack/os_group.py diff --git a/cloud/openstack/os_group.py b/cloud/openstack/os_group.py new file mode 100644 index 00000000000..4f317abccd3 --- /dev/null +++ b/cloud/openstack/os_group.py @@ -0,0 +1,167 @@ +#!/usr/bin/python +# Copyright (c) 2016 IBM +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + + +try: + import shade + HAS_SHADE = True +except ImportError: + HAS_SHADE = False + +DOCUMENTATION = ''' +--- +module: os_group +short_description: Manage OpenStack Identity Groups +extends_documentation_fragment: openstack +version_added: "2.1" +author: "Monty Taylor (@emonty), David Shrewsbury (@Shrews)" +description: + - Manage OpenStack Identity Groups. Groups can be created, deleted or + updated. Only the I(description) value can be updated. +options: + name: + description: + - Group name + required: true + description: + description: + - Group description + required: false + default: None + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present +requirements: + - "python >= 2.6" + - "shade" +''' + +EXAMPLES = ''' +# Create a group named "demo" +- os_group: + cloud: mycloud + state: present + name: demo + description: "Demo Group" + +# Update the description on existing "demo" group +- os_group: + cloud: mycloud + state: present + name: demo + description: "Something else" + +# Delete group named "demo" +- os_group: + cloud: mycloud + state: absent + name: demo +''' + +RETURN = ''' +group: + description: Dictionary describing the group. + returned: On success when I(state) is 'present'. + type: dictionary + contains: + id: + description: Unique group ID + type: string + sample: "ee6156ff04c645f481a6738311aea0b0" + name: + description: Group name + type: string + sample: "demo" + description: + description: Group description + type: string + sample: "Demo Group" + domain_id: + description: Domain for the group + type: string + sample: "default" +''' + + +def _system_state_change(state, description, group): + if state == 'present' and not group: + return True + if state == 'present' and description is not None and group.description != description: + return True + if state == 'absent' and group: + return True + return False + + +def main(): + argument_spec = openstack_full_argument_spec( + name=dict(required=True), + description=dict(required=False, default=None), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = openstack_module_kwargs() + module = AnsibleModule(argument_spec, + supports_check_mode=True, + **module_kwargs) + + if not HAS_SHADE: + module.fail_json(msg='shade is required for this module') + + name = module.params.pop('name') + description = module.params.pop('description') + state = module.params.pop('state') + + try: + cloud = shade.operator_cloud(**module.params) + group = cloud.get_group(name) + + if module.check_mode: + module.exit_json(changed=_system_state_change(state, description, group)) + + if state == 'present': + if group is None: + group = cloud.create_group( + name=name, description=description) + changed = True + else: + if description is not None and group.description != description: + group = cloud.update_group( + group.id, description=description) + changed = True + else: + changed = False + module.exit_json(changed=changed, group=group) + + elif state == 'absent': + if group is None: + changed=False + else: + cloud.delete_group(group.id) + changed=True + module.exit_json(changed=changed) + + except shade.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + + +from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * + +if __name__ == '__main__': + main() From 8e35db0e385c404fd21eed25ae0cbbffa7b1db2d Mon Sep 17 00:00:00 2001 From: Mstislav Bobakov Date: Thu, 21 Jan 2016 13:08:25 +0300 Subject: [PATCH 1133/2522] Add custom parameter for a sensu_check --- monitoring/sensu_check.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/monitoring/sensu_check.py b/monitoring/sensu_check.py index 09edae63813..6a4ef90b8e4 100644 --- a/monitoring/sensu_check.py +++ b/monitoring/sensu_check.py @@ -149,6 +149,11 @@ - The low threshhold for flap detection required: false default: null + custom: + description: + - JSON mixin to add to the configuration + required: false + default: "{}" requirements: [ ] author: "Anders Ingemann (@andsens)" ''' @@ -183,6 +188,7 @@ # Let snippet from module_utils/basic.py return a proper error in this case pass +import ast def sensu_check(module, path, name, state='present', backup=False): changed = False @@ -257,6 +263,36 @@ def sensu_check(module, path, name, state='present', backup=False): changed = True reasons.append('`{opt}\' was removed'.format(opt=opt)) + if module.params['custom']: + # Convert to json + try: + custom_params = ast.literal_eval(module.params['custom']) + except: + msg = 'Module parameter "custom" contains invalid JSON. Example: custom=\'{"JSON": "here"}\'' + module.fail_json(msg=msg) + + overwrited_fields = set(custom_params.keys()) & set(simple_opts + ['type','subdue']) + if overwrited_fields: + msg = 'You can\'t overwriting standard module parameters via "custom". You are trying overwrite: {of}'.format(of=list(overwrited_fields)) + module.fail_json(msg=msg) + + for k,v in custom_params.items(): + if k in config['checks'][name].keys(): + if not config['checks'][name][k] == v: + changed = True + reasons.append('`custom param {k}\' was changed'.format(k=k)) + else: + changed = True + reasons.append('`custom param {k}\' was added'.format(k=k)) + check[k] = v + simple_opts += custom_params.keys() + + # Remove obsolete custom params + for opt in set(config['checks'][name].keys()) - set(simple_opts + ['type','subdue']): + changed = True + reasons.append('`custom param {opt}\' was deleted'.format(opt=opt)) + del check[opt] + if module.params['metric']: if 'type' not in check or check['type'] != 'metric': check['type'] = 'metric' @@ -320,6 +356,7 @@ def main(): 'aggregate': {'type': 'bool'}, 'low_flap_threshold': {'type': 'int'}, 'high_flap_threshold': {'type': 'int'}, + 'custom': {'type': 'str'}, } required_together = [['subdue_begin', 'subdue_end']] From 0c2f602788e0db53e084e280ebf53c27d90d2c12 Mon Sep 17 00:00:00 2001 From: Chad Nelson Date: Thu, 21 Jan 2016 08:23:36 -0500 Subject: [PATCH 1134/2522] Doc wrongly indicates permanent is required But it isn't. :) --- system/firewalld.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system/firewalld.py b/system/firewalld.py index a3a1e5f410e..771d178d9d5 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -56,7 +56,8 @@ permanent: description: - "Should this configuration be in the running firewalld configuration or persist across reboots." - required: true + required: false + default: null immediate: description: - "Should this configuration be applied immediately, if set as permanent" From a59aa2cb341df237c5a237ac1d47c0ac48430ba0 Mon Sep 17 00:00:00 2001 From: "Ryan G. Hunter" Date: Thu, 21 Jan 2016 10:56:58 -0500 Subject: [PATCH 1135/2522] monit startup fix --- monitoring/monit.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/monitoring/monit.py b/monitoring/monit.py index dda3f82d486..2983d5e49af 100644 --- a/monitoring/monit.py +++ b/monitoring/monit.py @@ -127,9 +127,8 @@ def wait_for_monit_to_stop_pending(): module.exit_json(changed=True) status = run_command('reload') if status == '': - module.fail_json(msg='%s process not configured with monit' % name, name=name, state=state) - else: - module.exit_json(changed=True, name=name, state=state) + wait_for_monit_to_stop_pending() + module.exit_json(changed=True, name=name, state=state) module.exit_json(changed=False, name=name, state=state) wait_for_monit_to_stop_pending() From 9b981cecc6ca1791970bedece88165108c14879d Mon Sep 17 00:00:00 2001 From: "colynn.liu" Date: Fri, 22 Jan 2016 18:05:48 +0800 Subject: [PATCH 1136/2522] fixed python 2.4 compatibility nf=tempfile.NamedTemporaryFile(delete=False) TypeError: NamedTemporaryFile() got an unexpected keyword argument 'delete' --- system/pam_limits.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/system/pam_limits.py b/system/pam_limits.py index e14408fb4e2..1c56d852bbb 100644 --- a/system/pam_limits.py +++ b/system/pam_limits.py @@ -243,11 +243,16 @@ def main(): nf.write(new_limit) f.close() - nf.close() + nf.flush() # Copy tempfile to newfile module.atomic_move(nf.name, f.name) + try: + nf.close() + except: + pass + res_args = dict( changed = changed, msg = message ) From ddcc15a60f6a540e0ee5257990fa626a6acf6075 Mon Sep 17 00:00:00 2001 From: Shawn Siefkas Date: Wed, 14 Oct 2015 13:14:33 -0500 Subject: [PATCH 1137/2522] Fixing check mode support for vpc route tables Loop compatibility for dry run exception handling Route table deletion dry run handler Fixing regression in propagating_vgw_ids default value Adjusting truthiness of changed attribute for route manipulation Updating propagating_vgw_ids default in docstring --- cloud/amazon/ec2_vpc_route_table.py | 43 ++++++++++++++++++----------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index f4d27f9580f..1e9de6ec177 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -31,6 +31,7 @@ propagating_vgw_ids: description: - "Enable route propagation from virtual gateways specified by ID." + default: None required: false route_table_id: description: @@ -323,15 +324,24 @@ def ensure_routes(vpc_conn, route_table, route_specs, propagating_vgw_ids, changed = routes_to_delete or route_specs_to_create if changed: for route_spec in route_specs_to_create: - vpc_conn.create_route(route_table.id, - dry_run=check_mode, - **route_spec) + try: + vpc_conn.create_route(route_table.id, + dry_run=check_mode, + **route_spec) + except EC2ResponseError as e: + if e.error_code == 'DryRunOperation': + pass for route in routes_to_delete: - vpc_conn.delete_route(route_table.id, - route.destination_cidr_block, - dry_run=check_mode) - return {'changed': changed} + try: + vpc_conn.delete_route(route_table.id, + route.destination_cidr_block, + dry_run=check_mode) + except EC2ResponseError as e: + if e.error_code == 'DryRunOperation': + pass + + return {'changed': bool(changed)} def ensure_subnet_association(vpc_conn, vpc_id, route_table_id, subnet_id, @@ -406,7 +416,6 @@ def ensure_route_table_absent(connection, module): route_table_id = module.params.get('route_table_id') tags = module.params.get('tags') vpc_id = module.params.get('vpc_id') - check_mode = module.params.get('check_mode') if lookup == 'tag': if tags is not None: @@ -428,9 +437,12 @@ def ensure_route_table_absent(connection, module): return {'changed': False} try: - connection.delete_route_table(route_table.id, dry_run=check_mode) + connection.delete_route_table(route_table.id, dry_run=module.check_mode) except EC2ResponseError as e: - module.fail_json(msg=e.message) + if e.error_code == 'DryRunOperation': + pass + else: + module.fail_json(msg=e.message) return {'changed': True} @@ -465,12 +477,11 @@ def create_route_spec(connection, routes, vpc_id): def ensure_route_table_present(connection, module): lookup = module.params.get('lookup') - propagating_vgw_ids = module.params.get('propagating_vgw_ids', []) + propagating_vgw_ids = module.params.get('propagating_vgw_ids') route_table_id = module.params.get('route_table_id') subnets = module.params.get('subnets') tags = module.params.get('tags') vpc_id = module.params.get('vpc_id') - check_mode = module.params.get('check_mode') try: routes = create_route_spec(connection, module.params.get('routes'), vpc_id) except AnsibleIgwSearchException as e: @@ -508,7 +519,7 @@ def ensure_route_table_present(connection, module): if routes is not None: try: - result = ensure_routes(connection, route_table, routes, propagating_vgw_ids, check_mode) + result = ensure_routes(connection, route_table, routes, propagating_vgw_ids, module.check_mode) changed = changed or result['changed'] except EC2ResponseError as e: module.fail_json(msg=e.message) @@ -516,12 +527,12 @@ def ensure_route_table_present(connection, module): if propagating_vgw_ids is not None: result = ensure_propagation(connection, route_table, propagating_vgw_ids, - check_mode=check_mode) + check_mode=module.check_mode) changed = changed or result['changed'] if not tags_valid and tags is not None: result = ensure_tags(connection, route_table.id, tags, - add_only=True, check_mode=check_mode) + add_only=True, check_mode=module.check_mode) changed = changed or result['changed'] if subnets: @@ -535,7 +546,7 @@ def ensure_route_table_present(connection, module): ) try: - result = ensure_subnet_associations(connection, vpc_id, route_table, associated_subnets, check_mode) + result = ensure_subnet_associations(connection, vpc_id, route_table, associated_subnets, module.check_mode) changed = changed or result['changed'] except EC2ResponseError as e: raise AnsibleRouteTableException( From 09268b1a481040bf50c2250a18b8108a35e6faeb Mon Sep 17 00:00:00 2001 From: Marian Rusu Date: Thu, 13 Aug 2015 18:48:19 +0100 Subject: [PATCH 1138/2522] Enable managing multiple user permissions rules from one shot One of inconvinence this address is the the fact that you have to pass user's tags even if you just want to add a permission rule Signed-off-by: Marian Rusu --- messaging/rabbitmq_user.py | 86 ++++++++++++++++++++++++++++---------- 1 file changed, 65 insertions(+), 21 deletions(-) diff --git a/messaging/rabbitmq_user.py b/messaging/rabbitmq_user.py index b12178e08ea..50f1572d53f 100644 --- a/messaging/rabbitmq_user.py +++ b/messaging/rabbitmq_user.py @@ -45,9 +45,19 @@ - User tags specified as comma delimited required: false default: null + permissions: + description: + - a list of dicts, each dict contains vhost, configure_priv, write_priv, and read_priv, + and represents a permission rule for that vhost. + - This option should be preferable when you care about all permissions of the user. + - You should use vhost, configure_priv, write_priv, and read_priv options instead + if you care about permissions for just some vhosts. + required: false + default: [] vhost: description: - vhost to apply access privileges. + - This option will be ignored when permissions option is used. required: false default: / node: @@ -61,6 +71,7 @@ - Regular expression to restrict configure actions on a resource for the specified vhost. - By default all actions are restricted. + - This option will be ignored when permissions option is used. required: false default: ^$ write_priv: @@ -68,6 +79,7 @@ - Regular expression to restrict configure actions on a resource for the specified vhost. - By default all actions are restricted. + - This option will be ignored when permissions option is used. required: false default: ^$ read_priv: @@ -75,6 +87,7 @@ - Regular expression to restrict configure actions on a resource for the specified vhost. - By default all actions are restricted. + - This option will be ignored when permissions option is used. required: false default: ^$ force: @@ -92,7 +105,8 @@ ''' EXAMPLES = ''' -# Add user to server and assign full access control +# Add user to server and assign full access control on / vhost. +# The user might have permission rules for other vhost but you don't care. - rabbitmq_user: user=joe password=changeme vhost=/ @@ -100,10 +114,18 @@ read_priv=.* write_priv=.* state=present + +# Add user to server and assign full access control on / vhost. +# The user doesn't have permission rules for other vhosts +- rabbitmq_user: user=joe + password=changeme + permissions=[{vhost='/', configure_priv='.*', read_priv='.*', write_priv='.*'}] + state=present ''' class RabbitMqUser(object): - def __init__(self, module, username, password, tags, vhost, configure_priv, write_priv, read_priv, node): + def __init__(self, module, username, password, tags, permissions, + node, bulk_permissions=False): self.module = module self.username = username self.password = password @@ -113,13 +135,8 @@ def __init__(self, module, username, password, tags, vhost, configure_priv, writ else: self.tags = tags.split(',') - permissions = dict( - vhost=vhost, - configure_priv=configure_priv, - write_priv=write_priv, - read_priv=read_priv - ) self.permissions = permissions + self.bulk_permissions = bulk_permissions self._tags = None self._permissions = None @@ -154,12 +171,18 @@ def get(self): def _get_permissions(self): perms_out = self._exec(['list_user_permissions', self.username], True) + perms_list = list() for perm in perms_out: vhost, configure_priv, write_priv, read_priv = perm.split('\t') - if vhost == self.permissions['vhost']: - return dict(vhost=vhost, configure_priv=configure_priv, write_priv=write_priv, read_priv=read_priv) - - return dict() + if not self.bulk_permissions: + if vhost == self.permissions[0]['vhost']: + perms_list.append(dict(vhost=vhost, configure_priv=configure_priv, + write_priv=write_priv, read_priv=read_priv)) + break + else: + perms_list.append(dict(vhost=vhost, configure_priv=configure_priv, + write_priv=write_priv, read_priv=read_priv)) + return perms_list def add(self): if self.password is not None: @@ -175,14 +198,21 @@ def set_tags(self): self._exec(['set_user_tags', self.username] + self.tags) def set_permissions(self): - cmd = ['set_permissions'] - cmd.append('-p') - cmd.append(self.permissions['vhost']) - cmd.append(self.username) - cmd.append(self.permissions['configure_priv']) - cmd.append(self.permissions['write_priv']) - cmd.append(self.permissions['read_priv']) - self._exec(cmd) + for permission in self._permissions: + if permission not in self.permissions: + cmd = ['clear_permissions', '-p'] + cmd.append(permission['vhost']) + cmd.append(self.username) + self._exec(cmd) + for permission in self.permissions: + if permission not in self._permissions: + cmd = ['set_permissions', '-p'] + cmd.append(permission['vhost']) + cmd.append(self.username) + cmd.append(permission['configure_priv']) + cmd.append(permission['write_priv']) + cmd.append(permission['read_priv']) + self._exec(cmd) def has_tags_modifications(self): return set(self.tags) != set(self._tags) @@ -195,6 +225,7 @@ def main(): user=dict(required=True, aliases=['username', 'name']), password=dict(default=None), tags=dict(default=None), + permissions=dict(default=list()), vhost=dict(default='/'), configure_priv=dict(default='^$'), write_priv=dict(default='^$'), @@ -211,6 +242,7 @@ def main(): username = module.params['user'] password = module.params['password'] tags = module.params['tags'] + permissions = module.params['permissions'] vhost = module.params['vhost'] configure_priv = module.params['configure_priv'] write_priv = module.params['write_priv'] @@ -219,7 +251,19 @@ def main(): state = module.params['state'] node = module.params['node'] - rabbitmq_user = RabbitMqUser(module, username, password, tags, vhost, configure_priv, write_priv, read_priv, node) + bulk_permissions = True + if permissions == []: + perm = { + 'vhost': vhost, + 'configure_priv': configure_priv, + 'write_priv': write_priv, + 'read_priv': read_priv + } + permissions.append(perm) + bulk_permissions = False + + rabbitmq_user = RabbitMqUser(module, username, password, tags, permissions, + node, bulk_permissions=bulk_permissions) changed = False if rabbitmq_user.get(): From 0d86bc8d6bf31069be5dbc5c37870f8fe0023c05 Mon Sep 17 00:00:00 2001 From: Rob White Date: Sun, 24 Jan 2016 11:11:49 +1100 Subject: [PATCH 1139/2522] Add missing doc fragments --- cloud/amazon/ecs_cluster.py | 1 + cloud/amazon/ecs_service.py | 3 +++ cloud/amazon/ecs_service_facts.py | 3 +++ cloud/amazon/ecs_task.py | 1 + cloud/amazon/ecs_taskdefinition.py | 1 + 5 files changed, 9 insertions(+) diff --git a/cloud/amazon/ecs_cluster.py b/cloud/amazon/ecs_cluster.py index 3a0d7c10636..22049a9f3c4 100644 --- a/cloud/amazon/ecs_cluster.py +++ b/cloud/amazon/ecs_cluster.py @@ -45,6 +45,7 @@ - The number of times to wait for the cluster to have an instance required: false extends_documentation_fragment: + - aws - ec2 ''' diff --git a/cloud/amazon/ecs_service.py b/cloud/amazon/ecs_service.py index 9056ec73660..d77c4f060ba 100644 --- a/cloud/amazon/ecs_service.py +++ b/cloud/amazon/ecs_service.py @@ -72,6 +72,9 @@ - The number of times to check that the service is available required: false default: 10 +extends_documentation_fragment: + - aws + - ec2 ''' EXAMPLES = ''' diff --git a/cloud/amazon/ecs_service_facts.py b/cloud/amazon/ecs_service_facts.py index d86bc93d687..5e702f27e11 100644 --- a/cloud/amazon/ecs_service_facts.py +++ b/cloud/amazon/ecs_service_facts.py @@ -40,6 +40,9 @@ description: - The service to get details for (required if details is true) required: false +extends_documentation_fragment: + - aws + - ec2 ''' EXAMPLES = ''' diff --git a/cloud/amazon/ecs_task.py b/cloud/amazon/ecs_task.py index c2bd73751ad..16cf4fb2d2b 100644 --- a/cloud/amazon/ecs_task.py +++ b/cloud/amazon/ecs_task.py @@ -57,6 +57,7 @@ - A value showing who or what started the task (for informational purposes) required: False extends_documentation_fragment: + - aws - ec2 ''' diff --git a/cloud/amazon/ecs_taskdefinition.py b/cloud/amazon/ecs_taskdefinition.py index 6ad23a88f86..e924417f9d1 100644 --- a/cloud/amazon/ecs_taskdefinition.py +++ b/cloud/amazon/ecs_taskdefinition.py @@ -53,6 +53,7 @@ required: False type: list of name extends_documentation_fragment: + - aws - ec2 ''' From ce3e972ce8cf074b605271e1c80684304443ea48 Mon Sep 17 00:00:00 2001 From: Vincent VAN HOLLEBEKE Date: Sun, 24 Jan 2016 20:28:01 +0100 Subject: [PATCH 1140/2522] Do not fail when action is delete and volume is not present This prevents failing when a playbook describes a volume deletion and is launched more that once. Without this fix, if you run the playbook a second time, it will fail. --- system/gluster_volume.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/gluster_volume.py b/system/gluster_volume.py index d24930d7290..e0b1ef01708 100644 --- a/system/gluster_volume.py +++ b/system/gluster_volume.py @@ -433,7 +433,7 @@ def main(): else: module.fail_json(msg='failed to create volume %s' % volume_name) - if volume_name not in volumes: + if action != 'delete' and volume_name not in volumes: module.fail_json(msg='volume not found %s' % volume_name) if action == 'started': From 2d7ebf0b1c969b96ca48e58f61fc3ab9013862f5 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Mon, 18 Jan 2016 11:51:44 -0500 Subject: [PATCH 1141/2522] Add new os_flavor_facts.py module New module to retrieve facts about existing instance flavors. By default, facts on all available flavors will be returned. This can be narrowed by naming a flavor or specifying criteria about flavor RAM or VCPUs. --- cloud/openstack/os_flavor_facts.py | 219 +++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 cloud/openstack/os_flavor_facts.py diff --git a/cloud/openstack/os_flavor_facts.py b/cloud/openstack/os_flavor_facts.py new file mode 100644 index 00000000000..e4c65f8ff23 --- /dev/null +++ b/cloud/openstack/os_flavor_facts.py @@ -0,0 +1,219 @@ +#!/usr/bin/python + +# Copyright (c) 2015 IBM +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + +import re + +try: + import shade + HAS_SHADE = True +except ImportError: + HAS_SHADE = False + +DOCUMENTATION = ''' +--- +module: os_flavor_facts +short_description: Retrieve facts about one or more flavors +author: "David Shrewsbury (@Shrews)" +version_added: "2.1" +description: + - Retrieve facts about available OpenStack instance flavors. By default, + facts about ALL flavors are retrieved. Filters can be applied to get + facts for only matching flavors. For example, you can filter on the + amount of RAM available to the flavor, or the number of virtual CPUs + available to the flavor, or both. When specifying multiple filters, + *ALL* filters must match on a flavor before that flavor is returned as + a fact. +notes: + - This module creates a new top-level C(openstack_flavors) fact, which + contains a list of unsorted flavors. +requirements: + - "python >= 2.6" + - "shade" +options: + name: + description: + - A flavor name. Cannot be used with I(ram) or I(vcpus). + required: false + default: None + ram: + description: + - "A string used for filtering flavors based on the amount of RAM + (in MB) desired. This string accepts the following special values: + 'MIN' (return flavors with the minimum amount of RAM), and 'MAX' + (return flavors with the maximum amount of RAM). + + A specific amount of RAM may also be specified. Any flavors with this + exact amount of RAM will be returned. + + A range of acceptable RAM may be given using a special syntax. Simply + prefix the amount of RAM with one of these acceptable range values: + '<', '>', '<=', '>='. These values represent less than, greater than, + less than or equal to, and greater than or equal to, respectively." + required: false + default: false + vcpus: + description: + - A string used for filtering flavors based on the number of virtual + CPUs desired. Format is the same as the I(ram) parameter. + required: false + default: false + limit: + description: + - Limits the number of flavors returned. All matching flavors are + returned by default. + required: false + default: None +extends_documentation_fragment: openstack +''' + +EXAMPLES = ''' +# Gather facts about all available flavors +- os_flavor_facts: + cloud: mycloud + +# Gather facts for the flavor named "xlarge-flavor" +- os_flavor_facts: + cloud: mycloud + name: "xlarge-flavor" + +# Get all flavors that have exactly 512 MB of RAM. +- os_flavor_facts: + cloud: mycloud + ram: "512" + +# Get all flavors that have 1024 MB or more of RAM. +- os_flavor_facts: + cloud: mycloud + ram: ">=1024" + +# Get a single flavor that has the minimum amount of RAM. Using the 'limit' +# option will guarantee only a single flavor is returned. +- os_flavor_facts: + cloud: mycloud + ram: "MIN" + limit: 1 + +# Get all flavors with 1024 MB of RAM or more, AND exactly 2 virtual CPUs. +- os_flavor_facts: + cloud: mycloud + ram: ">=1024" + vcpus: "2" +''' + + +RETURN = ''' +openstack_flavors: + description: Dictionary describing the flavors. + returned: On success. + type: dictionary + contains: + id: + description: Flavor ID. + returned: success + type: string + sample: "515256b8-7027-4d73-aa54-4e30a4a4a339" + name: + description: Flavor name. + returned: success + type: string + sample: "tiny" + disk: + description: Size of local disk, in GB. + returned: success + type: int + sample: 10 + ephemeral: + description: Ephemeral space size, in GB. + returned: success + type: int + sample: 10 + ram: + description: Amount of memory, in MB. + returned: success + type: int + sample: 1024 + swap: + description: Swap space size, in MB. + returned: success + type: int + sample: 100 + vcpus: + description: Number of virtual CPUs. + returned: success + type: int + sample: 2 + is_public: + description: Make flavor accessible to the public. + returned: success + type: bool + sample: true +''' + + +def main(): + argument_spec = openstack_full_argument_spec( + name=dict(required=False, default=None), + ram=dict(required=False, default=None), + vcpus=dict(required=False, default=None), + limit=dict(required=False, default=None, type='int'), + ) + module_kwargs = openstack_module_kwargs( + mutually_exclusive=[ + ['name', 'ram'], + ['name', 'vcpus'], + ] + ) + module = AnsibleModule(argument_spec, **module_kwargs) + + if not HAS_SHADE: + module.fail_json(msg='shade is required for this module') + + name = module.params['name'] + vcpus = module.params['vcpus'] + ram = module.params['ram'] + limit = module.params['limit'] + + try: + cloud = shade.openstack_cloud(**module.params) + if name: + flavors = cloud.search_flavors(filters={'name': name}) + + else: + flavors = cloud.list_flavors() + filters = {} + if vcpus: + filters['vcpus'] = vcpus + if ram: + filters['ram'] = ram + if filters: + flavors = cloud.range_search(flavors, filters) + + if limit is not None: + flavors = flavors[:limit] + + module.exit_json(changed=False, + ansible_facts=dict(openstack_flavors=flavors)) + + except shade.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + + +from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * + +if __name__ == '__main__': + main() From f5bfc4f9e98850114a33ff89eabd63eae978c332 Mon Sep 17 00:00:00 2001 From: Ashley Penney Date: Tue, 26 Jan 2016 11:18:43 -0500 Subject: [PATCH 1142/2522] Change example to include ethernet as the type. --- network/nmcli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/nmcli.py b/network/nmcli.py index ccefef18ccf..115c5939a8f 100644 --- a/network/nmcli.py +++ b/network/nmcli.py @@ -360,7 +360,7 @@ - nmcli: ctype=ethernet name=my-eth1 ifname="*" state=present # To change the property of a setting e.g. MTU, issue a command as follows: -- nmcli: conn_name=my-eth1 mtu=9000 state=present +- nmcli: conn_name=my-eth1 mtu=9000 type=ethernet state=present Exit Status's: - nmcli exits with status 0 if it succeeds, a value greater than 0 is From 25262c2238d2722c1455a63bd9ce4774da7af10d Mon Sep 17 00:00:00 2001 From: Ashley Penney Date: Tue, 26 Jan 2016 13:38:44 -0500 Subject: [PATCH 1143/2522] Fix package list for nmcli module. There was a missing package that causes this to fail. --- network/nmcli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/network/nmcli.py b/network/nmcli.py index 115c5939a8f..bfdd1caf76b 100644 --- a/network/nmcli.py +++ b/network/nmcli.py @@ -283,6 +283,7 @@ - name: install needed network manager libs yum: name={{ item }} state=installed with_items: + - NetworkManager-glib - libnm-qt-devel.x86_64 - nm-connection-editor.x86_64 - libsemanage-python From 6ef3697c52f51b76bbcd6753f9f568e7fde90fe8 Mon Sep 17 00:00:00 2001 From: Mstislav Bobakov Date: Thu, 28 Jan 2016 15:42:09 +0300 Subject: [PATCH 1144/2522] Add custom parameter for a sensu_check. Fixes. JSON replaced within dict. Added more docs. --- monitoring/sensu_check.py | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/monitoring/sensu_check.py b/monitoring/sensu_check.py index 6a4ef90b8e4..f2e336dc114 100644 --- a/monitoring/sensu_check.py +++ b/monitoring/sensu_check.py @@ -151,9 +151,10 @@ default: null custom: description: - - JSON mixin to add to the configuration + - A hash/dictionary of custom parameters for mixing to the configuration. + - You can't rewrite others module parameters using this required: false - default: "{}" + default: {} requirements: [ ] author: "Anders Ingemann (@andsens)" ''' @@ -188,8 +189,6 @@ # Let snippet from module_utils/basic.py return a proper error in this case pass -import ast - def sensu_check(module, path, name, state='present', backup=False): changed = False reasons = [] @@ -265,30 +264,25 @@ def sensu_check(module, path, name, state='present', backup=False): if module.params['custom']: # Convert to json - try: - custom_params = ast.literal_eval(module.params['custom']) - except: - msg = 'Module parameter "custom" contains invalid JSON. Example: custom=\'{"JSON": "here"}\'' - module.fail_json(msg=msg) - - overwrited_fields = set(custom_params.keys()) & set(simple_opts + ['type','subdue']) + custom_params = module.params['custom'] + overwrited_fields = set(custom_params.keys()) & set(simple_opts + ['type','subdue','subdue_begin','subdue_end']) if overwrited_fields: - msg = 'You can\'t overwriting standard module parameters via "custom". You are trying overwrite: {of}'.format(of=list(overwrited_fields)) + msg = 'You can\'t overwriting standard module parameters via "custom". You are trying overwrite: {opt}'.format(opt=list(overwrited_fields)) module.fail_json(msg=msg) for k,v in custom_params.items(): - if k in config['checks'][name].keys(): + if k in config['checks'][name]: if not config['checks'][name][k] == v: changed = True - reasons.append('`custom param {k}\' was changed'.format(k=k)) + reasons.append('`custom param {opt}\' was changed'.format(opt=k)) else: changed = True - reasons.append('`custom param {k}\' was added'.format(k=k)) + reasons.append('`custom param {opt}\' was added'.format(opt=k)) check[k] = v simple_opts += custom_params.keys() # Remove obsolete custom params - for opt in set(config['checks'][name].keys()) - set(simple_opts + ['type','subdue']): + for opt in set(config['checks'][name].keys()) - set(simple_opts + ['type','subdue','subdue_begin','subdue_end']): changed = True reasons.append('`custom param {opt}\' was deleted'.format(opt=opt)) del check[opt] @@ -356,7 +350,7 @@ def main(): 'aggregate': {'type': 'bool'}, 'low_flap_threshold': {'type': 'int'}, 'high_flap_threshold': {'type': 'int'}, - 'custom': {'type': 'str'}, + 'custom': {'type': 'dict'}, } required_together = [['subdue_begin', 'subdue_end']] From c4aa5ee024f9eac010a195955dca80a972c977b2 Mon Sep 17 00:00:00 2001 From: Mstislav Bobakov Date: Thu, 28 Jan 2016 15:44:57 +0300 Subject: [PATCH 1145/2522] Add custom parameter for a sensu_check. revert newline --- monitoring/sensu_check.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monitoring/sensu_check.py b/monitoring/sensu_check.py index f2e336dc114..3fd13ff0aa6 100644 --- a/monitoring/sensu_check.py +++ b/monitoring/sensu_check.py @@ -189,6 +189,7 @@ # Let snippet from module_utils/basic.py return a proper error in this case pass + def sensu_check(module, path, name, state='present', backup=False): changed = False reasons = [] From 27cd172900e29e55275dcd1a02ac05f342b340fa Mon Sep 17 00:00:00 2001 From: Dale Smith Date: Thu, 28 Jan 2016 15:48:18 +0000 Subject: [PATCH 1146/2522] Fix regression of #821 in puppet of show_diff Issue #821 fix: 1382576100ee3b17f4eb28c7186d92376f370676 Regressed since: 6912ca0acaed0d738d8dd9867721d2ff0094084a Issue: #821 --- system/puppet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/puppet.py b/system/puppet.py index d4f69b1d515..e1d21624c47 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -175,7 +175,7 @@ def main(): if p['puppetmaster']: cmd += " --server %s" % pipes.quote(p['puppetmaster']) if p['show_diff']: - cmd += " --show-diff" + cmd += " --show_diff" if p['environment']: cmd += " --environment '%s'" % p['environment'] if module.check_mode: From acc51c29447b27cf2266437ce019c83f06ddee7b Mon Sep 17 00:00:00 2001 From: Sumit Roy Date: Wed, 23 Dec 2015 11:21:28 -0800 Subject: [PATCH 1147/2522] Ensure that port_path is split into at most 2 components. cloudstack: cs_instance: fix do not require name to be set to avoid clashes Require one of display_name or name. If both is given, name is used as identifier. cloudstack: fix name is not case insensitive cloudstack: cs_template: implement state=extracted Update f5 validate_certs functionality to do the right thing on multiple python versions This requires the implementation in the module_utils code here https://github.com/ansible/ansible/pull/13667 to funciton fixed domain_id to actually be supported also added domain as an alias alt fixes #1437 Simplify the code and remove use_unsafe_shell=True While there is no security issue with this shell snippet, it is better to not rely on shell and avoid use_unsafe_shell. Fix for issue #1074. Now able to create volume without replica's. Improved fix for #1074. Both None and '' transform to fqdn. Fix for ansible-modules-extras issue #1080 --- cloud/amazon/ec2_elb_facts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_elb_facts.py b/cloud/amazon/ec2_elb_facts.py index 8b7853ac6f6..549b87bac07 100644 --- a/cloud/amazon/ec2_elb_facts.py +++ b/cloud/amazon/ec2_elb_facts.py @@ -112,7 +112,7 @@ def get_elb_listeners(listeners): def get_health_check(health_check): protocol, port_path = health_check.target.split(':') try: - port, path = port_path.split('/') + port, path = port_path.split('/', 1) path = '/{}'.format(path) except ValueError: port = port_path From 36be779888cb69f6a2849c72731a9d03e5565d1e Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Thu, 28 Jan 2016 09:20:37 -0800 Subject: [PATCH 1148/2522] Remove duplicate documentation fields --- cloud/amazon/dynamodb_table.py | 1 - cloud/cloudstack/cs_loadbalancer_rule.py | 5 ----- network/f5/bigip_virtual_server.py | 5 ++--- notification/campfire.py | 1 - notification/sns.py | 3 +-- packaging/os/zypper.py | 1 - windows/win_iis_webbinding.py | 6 ------ windows/win_package.py | 1 - 8 files changed, 3 insertions(+), 20 deletions(-) diff --git a/cloud/amazon/dynamodb_table.py b/cloud/amazon/dynamodb_table.py index a39ecdd3f48..ed3adea154f 100644 --- a/cloud/amazon/dynamodb_table.py +++ b/cloud/amazon/dynamodb_table.py @@ -22,7 +22,6 @@ - Create or delete AWS Dynamo DB tables. - Can update the provisioned throughput on existing tables. - Returns the status of the specified table. -version_added: "2.0" author: Alan Loi (@loia) version_added: "2.0" requirements: diff --git a/cloud/cloudstack/cs_loadbalancer_rule.py b/cloud/cloudstack/cs_loadbalancer_rule.py index aec04cf7eb2..8d16c058855 100644 --- a/cloud/cloudstack/cs_loadbalancer_rule.py +++ b/cloud/cloudstack/cs_loadbalancer_rule.py @@ -46,11 +46,6 @@ required: false choices: [ 'source', 'roundrobin', 'leastconn' ] default: 'source' - protocol: - description: - - Protocol for the load balancer rule. - required: false - default: null private_port: description: - The private port of the private ip address/virtual machine where the network traffic will be load balanced to. diff --git a/network/f5/bigip_virtual_server.py b/network/f5/bigip_virtual_server.py index 7ee865fe9a2..aebe2673283 100644 --- a/network/f5/bigip_virtual_server.py +++ b/network/f5/bigip_virtual_server.py @@ -55,12 +55,11 @@ state: description: - Virtual Server state - required: false - default: present - description: - Absent, delete the VS if present - present (and its synonym enabled), create if needed the VS and set state to enabled - disabled, create if needed the VS and set state to disabled + required: false + default: present choices: ['present', 'absent', 'enabled', 'disabled'] aliases: [] partition: diff --git a/notification/campfire.py b/notification/campfire.py index 68e64f1bc94..7615f551f7c 100644 --- a/notification/campfire.py +++ b/notification/campfire.py @@ -23,7 +23,6 @@ description: - Send a message to Campfire. - Messages with newlines will result in a "Paste" message being sent. -version_added: "1.2" options: subscription: description: diff --git a/notification/sns.py b/notification/sns.py index 5fd81e2047f..6288ee86a30 100644 --- a/notification/sns.py +++ b/notification/sns.py @@ -24,7 +24,7 @@ description: - The M(sns) module sends notifications to a topic on your Amazon SNS account version_added: 1.6 -author: "Michael J. Schultz (@mjschultz)" +author: "Michael J. Schultz (@mjschultz)" options: msg: description: @@ -78,7 +78,6 @@ aliases: ['aws_region', 'ec2_region'] requirements: [ "boto" ] -author: Michael J. Schultz """ EXAMPLES = """ diff --git a/packaging/os/zypper.py b/packaging/os/zypper.py index 78c2d489eaa..43e8919d5a2 100644 --- a/packaging/os/zypper.py +++ b/packaging/os/zypper.py @@ -79,7 +79,6 @@ notes: [] # informational: requirements for nodes requirements: [ zypper, rpm ] -author: Patrick Callahan ''' EXAMPLES = ''' diff --git a/windows/win_iis_webbinding.py b/windows/win_iis_webbinding.py index 061bed73723..0aa1ee12594 100644 --- a/windows/win_iis_webbinding.py +++ b/windows/win_iis_webbinding.py @@ -66,12 +66,6 @@ required: false default: null aliases: [] - protocol: - description: - - The protocol to be used for the Web binding (usually HTTP, HTTPS, or FTP). - required: false - default: null - aliases: [] certificate_hash: description: - Certificate hash for the SSL binding. The certificate hash is the unique identifier for the certificate. diff --git a/windows/win_package.py b/windows/win_package.py index 68497d5ba4f..946b730b97b 100644 --- a/windows/win_package.py +++ b/windows/win_package.py @@ -71,7 +71,6 @@ - Password of an account with access to the package if its located on a file share. Only needed if the winrm user doesn't have access to the package. Also specify user_name for this to function properly. default: null aliases: [] -author: Trond Hindenes ''' EXAMPLES = ''' From d24721f6518b9149c2dc08c9db6cf6c33feb5bcc Mon Sep 17 00:00:00 2001 From: Daniel Jakots Date: Thu, 28 Jan 2016 21:54:25 +0100 Subject: [PATCH 1149/2522] fix the spelling of vim's flavor in the example --- packaging/os/openbsd_pkg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/os/openbsd_pkg.py b/packaging/os/openbsd_pkg.py index a72140b3bb7..9700e831892 100644 --- a/packaging/os/openbsd_pkg.py +++ b/packaging/os/openbsd_pkg.py @@ -77,7 +77,7 @@ - openbsd_pkg: name=nmap state=present build=yes # Specify a pkg flavour with '--' -- openbsd_pkg: name=vim--nox11 state=present +- openbsd_pkg: name=vim--no_x11 state=present # Specify the default flavour to avoid ambiguity errors - openbsd_pkg: name=vim-- state=present From aaa34b03c11d2b53e6a7dfe209423b9da312d250 Mon Sep 17 00:00:00 2001 From: "Thierno IB. BARRY" Date: Fri, 29 Jan 2016 11:50:55 +0100 Subject: [PATCH 1150/2522] Add ES 2.x support Add compatibility between ES 1.x and ES 2.x. bin/plugin install | remove [plugin_name] works on ES 1.x and ES 2.x --- packaging/elasticsearch_plugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packaging/elasticsearch_plugin.py b/packaging/elasticsearch_plugin.py index 7b092a13667..d2f5e0aa9a2 100644 --- a/packaging/elasticsearch_plugin.py +++ b/packaging/elasticsearch_plugin.py @@ -33,7 +33,7 @@ options: name: description: - - Name of the plugin to install + - Name of the plugin to install. In ES 2.x, the name can be an url or file location required: True state: description: @@ -43,7 +43,7 @@ default: present url: description: - - Set exact URL to download the plugin from + - Set exact URL to download the plugin from (Only works for ES 1.x) required: False default: None timeout: @@ -112,8 +112,8 @@ def parse_error(string): def main(): package_state_map = dict( - present="--install", - absent="--remove" + present="install", + absent="remove" ) module = AnsibleModule( From 1066a7b4e3d537c7157c7e1b86fe742ad1170db4 Mon Sep 17 00:00:00 2001 From: "Thierno IB. BARRY" Date: Fri, 29 Jan 2016 14:07:13 +0100 Subject: [PATCH 1151/2522] Elasticsearch: Add proxy support Add proxy support for plugin installation. --- packaging/elasticsearch_plugin.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packaging/elasticsearch_plugin.py b/packaging/elasticsearch_plugin.py index 7b092a13667..9af64f90096 100644 --- a/packaging/elasticsearch_plugin.py +++ b/packaging/elasticsearch_plugin.py @@ -124,16 +124,20 @@ def main(): timeout=dict(default="1m"), plugin_bin=dict(default="/usr/share/elasticsearch/bin/plugin"), plugin_dir=dict(default="/usr/share/elasticsearch/plugins/"), + proxy_host=dict(default=None), + proxy_port=dict(default=None), version=dict(default=None) ) ) - plugin_bin = module.params["plugin_bin"] - plugin_dir = module.params["plugin_dir"] name = module.params["name"] state = module.params["state"] url = module.params["url"] timeout = module.params["timeout"] + plugin_bin = module.params["plugin_bin"] + plugin_dir = module.params["plugin_dir"] + proxy_host = module.params["proxy_host"] + proxy_port = module.params["proxy_port"] version = module.params["version"] present = is_plugin_present(parse_plugin_repo(name), plugin_dir) @@ -147,6 +151,9 @@ def main(): cmd_args = [plugin_bin, package_state_map[state], name] + if proxy_host and proxy_port: + cmd_args.append("-DproxyHost=%s -DproxyPort=%s" % proxy_host, proxy_port) + if url: cmd_args.append("--url %s" % url) From 18729791ce1f545d1d1a719d7c217f13a5f1a85b Mon Sep 17 00:00:00 2001 From: "Thierno IB. BARRY" Date: Fri, 29 Jan 2016 14:12:43 +0100 Subject: [PATCH 1152/2522] update doc for proxy support Update documentation to add proxy section --- packaging/elasticsearch_plugin.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packaging/elasticsearch_plugin.py b/packaging/elasticsearch_plugin.py index 9af64f90096..5b7d8f89780 100644 --- a/packaging/elasticsearch_plugin.py +++ b/packaging/elasticsearch_plugin.py @@ -61,6 +61,16 @@ - Your configured plugin directory specified in Elasticsearch required: False default: /usr/share/elasticsearch/plugins/ + proxy_host: + description: + - Proxy host to use during plugin installation + required: False + default: None + proxy_port: + description: + - Proxy port to use during plugin installation + required: False + default: None version: description: - Version of the plugin to be installed. From a11220228c428257066d7ef5219eeb509a631f49 Mon Sep 17 00:00:00 2001 From: Baptiste Mille-Mathias Date: Sun, 31 Jan 2016 20:05:45 +0100 Subject: [PATCH 1153/2522] fix typo conainerization => containerization --- cloud/misc/proxmox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/misc/proxmox.py b/cloud/misc/proxmox.py index f0d7198111e..1b908d99c15 100644 --- a/cloud/misc/proxmox.py +++ b/cloud/misc/proxmox.py @@ -20,7 +20,7 @@ short_description: management of instances in Proxmox VE cluster description: - allows you to create/delete/stop instances in Proxmox VE cluster - - Starting in Ansible 2.1, it automatically detects conainerization type (lxc for PVE 4, openvz for older) + - Starting in Ansible 2.1, it automatically detects containerization type (lxc for PVE 4, openvz for older) version_added: "2.0" options: api_host: From 7f918b375fc33be3dd39094690f8fdd6ad74f3d7 Mon Sep 17 00:00:00 2001 From: Jiri Tyr Date: Mon, 21 Dec 2015 15:56:54 +0000 Subject: [PATCH 1154/2522] Adding params option into the yumrepo module --- packaging/os/yumrepo.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/packaging/os/yumrepo.py b/packaging/os/yumrepo.py index ba4aaa2ae8f..0c6ec1c5c05 100644 --- a/packaging/os/yumrepo.py +++ b/packaging/os/yumrepo.py @@ -172,6 +172,12 @@ required: true description: - Unique repository ID. + params: + required: false + default: None + description: + - Option used to allow the user to overwrite any of the other options. To + remove an option, set the value of the option to C(null). password: required: false default: None @@ -284,7 +290,7 @@ - Username to use for basic authentication to a repo or really any url. extends_documentation_fragment: - - files + - files notes: - All comments will be removed if modifying an existing repo file. @@ -327,6 +333,25 @@ name: epel file: external_repos state: absent + +# +# Allow to overwrite the yumrepo parameters by defining the parameters +# as a variable in the defaults or vars file: +# +# my_role_somerepo_params: +# # Disable GPG checking +# gpgcheck: no +# # Remove the gpgkey option +# gpgkey: null +# +- name: Add Some repo + yumrepo: + name: somerepo + description: Some YUM repo + baseurl: http://server.com/path/to/the/repo + gpgkey: http://server.com/keys/somerepo.pub + gpgcheck: yes + params: "{{ my_role_somerepo_params }}" ''' RETURN = ''' @@ -491,6 +516,7 @@ def main(): mirrorlist=dict(), mirrorlist_expire=dict(), name=dict(required=True), + params=dict(), password=dict(no_log=True), protect=dict(type='bool'), proxy=dict(), @@ -514,6 +540,12 @@ def main(): supports_check_mode=True, ) + # Update module parameters by user's parameters if defined + if 'params' in module.params and isinstance(module.params['params'], dict): + module.params.update(module.params['params']) + # Remove the params + module.params.pop('params', None) + name = module.params['name'] state = module.params['state'] From 2fc15c5d2b35721ca8ca7ae4dfc03f59826eb51e Mon Sep 17 00:00:00 2001 From: Joseph Callen Date: Mon, 1 Feb 2016 11:21:38 -0500 Subject: [PATCH 1155/2522] Resolves issue with vmware_vswitch module for v2.0 When this module was written back in May 2015 we were using 1.9.x. Being lazy I added to param the objects that the other functions would need. What I have noticed is in 2.0 exit_json is trying to jsonify those complex objects and failing. Playbook ```yaml - name: Add a temporary vSwitch local_action: module: vmware_vswitch hostname: "{{ inventory_hostname }}" username: "{{ esxi_username }}" password: "{{ site_passwd }}" switch_name: temp_vswitch nic_name: "{{ vss_vmnic }}" mtu: 1500 ``` Module Testing ```bash TASK [Add a temporary vSwitch] ************************************************* task path: /opt/autodeploy/projects/emmet/tasks/deploy/esxi_network.yml:13 ESTABLISH LOCAL CONNECTION FOR USER: root localhost EXEC ( umask 22 && mkdir -p "$( echo $HOME/.ansible/tmp/ansible-tmp-1454342817.37-180776062017566 )" && echo "$( echo $HOME/.ansible/tmp/ansible-tmp-1454342817.37-180776062017566 )" ) ESTABLISH LOCAL CONNECTION FOR USER: root localhost EXEC ( umask 22 && mkdir -p "$( echo $HOME/.ansible/tmp/ansible-tmp-1454342817.41-201974997737598 )" && echo "$( echo $HOME/.ansible/tmp/ansible-tmp-1454342817.41-201974997737598 )" ) ESTABLISH LOCAL CONNECTION FOR USER: root localhost EXEC ( umask 22 && mkdir -p "$( echo $HOME/.ansible/tmp/ansible-tmp-1454342817.44-148446986849801 )" && echo "$( echo $HOME/.ansible/tmp/ansible-tmp-1454342817.44-148446986849801 )" ) localhost PUT /tmp/tmpLLExSG TO /root/.ansible/tmp/ansible-tmp-1454342817.37-180776062017566/vmware_vswitch localhost EXEC LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /root/.ansible/tmp/ansible-tmp-1454342817.37-180776062017566/vmware_vswitch; rm -rf "/root/.ansible/tmp/ansible-tmp-1454342817.37-180776062017566/" > /dev/null 2>&1 localhost PUT /tmp/tmpyoAaHt TO /root/.ansible/tmp/ansible-tmp-1454342817.41-201974997737598/vmware_vswitch localhost EXEC LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /root/.ansible/tmp/ansible-tmp-1454342817.41-201974997737598/vmware_vswitch; rm -rf "/root/.ansible/tmp/ansible-tmp-1454342817.41-201974997737598/" > /dev/null 2>&1 localhost PUT /tmp/tmpPcmaMZ TO /root/.ansible/tmp/ansible-tmp-1454342817.44-148446986849801/vmware_vswitch localhost EXEC LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /root/.ansible/tmp/ansible-tmp-1454342817.44-148446986849801/vmware_vswitch; rm -rf "/root/.ansible/tmp/ansible-tmp-1454342817.44-148446986849801/" > /dev/null 2>&1 changed: [foundation-esxi-01 -> localhost] => {"changed": true, "invocation": {"module_args": {"hostname": "foundation-esxi-01", "mtu": 1500, "nic_name": "vmnic1", "number_of_ports": 128, "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", "state": "present", "switch_name": "temp_vswitch", "username": "root"}, "module_name": "vmware_vswitch"}} changed: [foundation-esxi-02 -> localhost] => {"changed": true, "invocation": {"module_args": {"hostname": "foundation-esxi-02", "mtu": 1500, "nic_name": "vmnic1", "number_of_ports": 128, "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", "state": "present", "switch_name": "temp_vswitch", "username": "root"}, "module_name": "vmware_vswitch"}} changed: [foundation-esxi-03 -> localhost] => {"changed": true, "invocation": {"module_args": {"hostname": "foundation-esxi-03", "mtu": 1500, "nic_name": "vmnic1", "number_of_ports": 128, "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", "state": "present", "switch_name": "temp_vswitch", "username": "root"}, "module_name": "vmware_vswitch"}} ``` Documentation fix --- cloud/vmware/vmware_vswitch.py | 202 ++++++++++++++++++--------------- 1 file changed, 108 insertions(+), 94 deletions(-) diff --git a/cloud/vmware/vmware_vswitch.py b/cloud/vmware/vmware_vswitch.py index d4ad8de7a3d..d3b67000d5a 100644 --- a/cloud/vmware/vmware_vswitch.py +++ b/cloud/vmware/vmware_vswitch.py @@ -32,6 +32,20 @@ - "python >= 2.6" - PyVmomi options: + hostname: + description: + - The hostname or IP address of the ESXi server + required: True + username: + description: + - The username of the ESXi server + required: True + aliases: ['user', 'admin'] + password: + description: + - The password of the ESXi server + required: True + aliases: ['pass', 'pwd'] switch_name: description: - vSwitch name to add @@ -82,82 +96,101 @@ def find_vswitch_by_name(host, vswitch_name): - for vss in host.config.network.vswitch: - if vss.name == vswitch_name: - return vss - return None - - -# Source from -# https://github.com/rreubenur/pyvmomi-community-samples/blob/patch-1/samples/create_vswitch.py - -def state_create_vswitch(module): - - switch_name = module.params['switch_name'] - number_of_ports = module.params['number_of_ports'] - nic_name = module.params['nic_name'] - mtu = module.params['mtu'] - host = module.params['host'] - - vss_spec = vim.host.VirtualSwitch.Specification() - vss_spec.numPorts = number_of_ports - vss_spec.mtu = mtu - vss_spec.bridge = vim.host.VirtualSwitch.BondBridge(nicDevice=[nic_name]) - host.configManager.networkSystem.AddVirtualSwitch(vswitchName=switch_name, spec=vss_spec) - module.exit_json(changed=True) - - -def state_exit_unchanged(module): - module.exit_json(changed=False) - - -def state_destroy_vswitch(module): - vss = module.params['vss'] - host = module.params['host'] - config = vim.host.NetworkConfig() - - for portgroup in host.configManager.networkSystem.networkInfo.portgroup: - if portgroup.spec.vswitchName == vss.name: - portgroup_config = vim.host.PortGroup.Config() - portgroup_config.changeOperation = "remove" - portgroup_config.spec = vim.host.PortGroup.Specification() - portgroup_config.spec.name = portgroup.spec.name - portgroup_config.spec.vlanId = portgroup.spec.vlanId - portgroup_config.spec.vswitchName = portgroup.spec.vswitchName - portgroup_config.spec.policy = vim.host.NetworkPolicy() - config.portgroup.append(portgroup_config) - - host.configManager.networkSystem.UpdateNetworkConfig(config, "modify") - host.configManager.networkSystem.RemoveVirtualSwitch(vss.name) - module.exit_json(changed=True) - - -def state_update_vswitch(module): - module.exit_json(changed=False, msg="Currently not implemented.") - - -def check_vswitch_configuration(module): - switch_name = module.params['switch_name'] - content = connect_to_api(module) - module.params['content'] = content - - host = get_all_objs(content, [vim.HostSystem]) - if not host: - module.fail_json(msg="Unble to find host") - - host_system = host.keys()[0] - module.params['host'] = host_system - vss = find_vswitch_by_name(host_system, switch_name) - - if vss is None: - return 'absent' - else: - module.params['vss'] = vss - return 'present' + for vss in host.config.network.vswitch: + if vss.name == vswitch_name: + return vss + return None + + +class VMwareHostVirtualSwitch(object): + + def __init__(self, module): + self.host_system = None + self.content = None + self.vss = None + self.module = module + self.switch_name = module.params['switch_name'] + self.number_of_ports = module.params['number_of_ports'] + self.nic_name = module.params['nic_name'] + self.mtu = module.params['mtu'] + self.state = module.params['state'] + self.content = connect_to_api(self.module) + + def process_state(self): + try: + vswitch_states = { + 'absent': { + 'present': self.state_destroy_vswitch, + 'absent': self.state_exit_unchanged, + }, + 'present': { + 'update': self.state_update_vswitch, + 'present': self.state_exit_unchanged, + 'absent': self.state_create_vswitch, + } + } + vswitch_states[self.state][self.check_vswitch_configuration()]() + + except vmodl.RuntimeFault as runtime_fault: + self.module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + self.module.fail_json(msg=method_fault.msg) + except Exception as e: + self.module.fail_json(msg=str(e)) + + + # Source from + # https://github.com/rreubenur/pyvmomi-community-samples/blob/patch-1/samples/create_vswitch.py + + def state_create_vswitch(self): + vss_spec = vim.host.VirtualSwitch.Specification() + vss_spec.numPorts = self.number_of_ports + vss_spec.mtu = self.mtu + vss_spec.bridge = vim.host.VirtualSwitch.BondBridge(nicDevice=[self.nic_name]) + self.host_system.configManager.networkSystem.AddVirtualSwitch(vswitchName=self.switch_name, spec=vss_spec) + self.module.exit_json(changed=True) + + def state_exit_unchanged(self): + self.module.exit_json(changed=False) + + def state_destroy_vswitch(self): + config = vim.host.NetworkConfig() + + for portgroup in self.host_system.configManager.networkSystem.networkInfo.portgroup: + if portgroup.spec.vswitchName == self.vss.name: + portgroup_config = vim.host.PortGroup.Config() + portgroup_config.changeOperation = "remove" + portgroup_config.spec = vim.host.PortGroup.Specification() + portgroup_config.spec.name = portgroup.spec.name + portgroup_config.spec.name = portgroup.spec.name + portgroup_config.spec.vlanId = portgroup.spec.vlanId + portgroup_config.spec.vswitchName = portgroup.spec.vswitchName + portgroup_config.spec.policy = vim.host.NetworkPolicy() + config.portgroup.append(portgroup_config) + + self.host_system.configManager.networkSystem.UpdateNetworkConfig(config, "modify") + self.host_system.configManager.networkSystem.RemoveVirtualSwitch(self.vss.name) + self.module.exit_json(changed=True) + + def state_update_vswitch(self): + self.module.exit_json(changed=False, msg="Currently not implemented.") + + def check_vswitch_configuration(self): + host = get_all_objs(self.content, [vim.HostSystem]) + if not host: + self.module.fail_json(msg="Unable to find host") + + self.host_system = host.keys()[0] + self.vss = find_vswitch_by_name(self.host_system, self.switch_name) + + if self.vss is None: + return 'absent' + else: + return 'present' + def main(): - argument_spec = vmware_argument_spec() argument_spec.update(dict(switch_name=dict(required=True, type='str'), nic_name=dict(required=True, type='str'), @@ -170,27 +203,8 @@ def main(): if not HAS_PYVMOMI: module.fail_json(msg='pyvmomi is required for this module') - try: - vswitch_states = { - 'absent': { - 'present': state_destroy_vswitch, - 'absent': state_exit_unchanged, - }, - 'present': { - 'update': state_update_vswitch, - 'present': state_exit_unchanged, - 'absent': state_create_vswitch, - } - } - - vswitch_states[module.params['state']][check_vswitch_configuration(module)](module) - - except vmodl.RuntimeFault as runtime_fault: - module.fail_json(msg=runtime_fault.msg) - except vmodl.MethodFault as method_fault: - module.fail_json(msg=method_fault.msg) - except Exception as e: - module.fail_json(msg=str(e)) + host_virtual_switch = VMwareHostVirtualSwitch(module) + host_virtual_switch.process_state() from ansible.module_utils.vmware import * from ansible.module_utils.basic import * From 37d36a553366334e68b3e84e35ba83dda5974144 Mon Sep 17 00:00:00 2001 From: Joseph Callen Date: Mon, 1 Feb 2016 11:35:02 -0500 Subject: [PATCH 1156/2522] Fixed documentation --- cloud/vmware/vmware_vswitch.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/cloud/vmware/vmware_vswitch.py b/cloud/vmware/vmware_vswitch.py index d3b67000d5a..7b115056ef5 100644 --- a/cloud/vmware/vmware_vswitch.py +++ b/cloud/vmware/vmware_vswitch.py @@ -32,20 +32,6 @@ - "python >= 2.6" - PyVmomi options: - hostname: - description: - - The hostname or IP address of the ESXi server - required: True - username: - description: - - The username of the ESXi server - required: True - aliases: ['user', 'admin'] - password: - description: - - The password of the ESXi server - required: True - aliases: ['pass', 'pwd'] switch_name: description: - vSwitch name to add From 56a517b054e8b24e23c6781c081db31af3fb2ae7 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Mon, 1 Feb 2016 16:27:09 -0800 Subject: [PATCH 1157/2522] Fixes missing validate_certs parameter The bigip_api method was changed in the module_utils function definition to include the validate_certs option but the bigip_virtual_server module was not updated accordingly. This patch updates the method so that the error message below is not returned to the user received exception: bigip_api() takes exactly 4 arguments (3 given) --- network/f5/bigip_virtual_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/f5/bigip_virtual_server.py b/network/f5/bigip_virtual_server.py index aebe2673283..26772f4bca7 100644 --- a/network/f5/bigip_virtual_server.py +++ b/network/f5/bigip_virtual_server.py @@ -392,7 +392,7 @@ def main(): module.fail_json(msg="valid ports must be in range 1 - 65535") try: - api = bigip_api(server, user, password) + api = bigip_api(server, user, password, validate_certs) result = {'changed': False} # default if state == 'absent': From 7e81c60c96beba271129b1f23a0bd1d62632c88a Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 1 Feb 2016 23:49:05 -0500 Subject: [PATCH 1158/2522] corrected version added --- cloud/amazon/ec2_vol_facts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_vol_facts.py b/cloud/amazon/ec2_vol_facts.py index a3ab94682e8..921fbe510d4 100644 --- a/cloud/amazon/ec2_vol_facts.py +++ b/cloud/amazon/ec2_vol_facts.py @@ -19,7 +19,7 @@ short_description: Gather facts about ec2 volumes in AWS description: - Gather facts about ec2 volumes in AWS -version_added: "2.0" +version_added: "2.1" author: "Rob White (@wimnat)" options: filters: From cd64f20034b61055cc2f3473a20de2ad13bf2e6a Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 1 Feb 2016 23:55:41 -0500 Subject: [PATCH 1159/2522] added missing version_added --- system/iptables.py | 1 + 1 file changed, 1 insertion(+) diff --git a/system/iptables.py b/system/iptables.py index 9e94a17a47b..0e8260d226f 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -212,6 +212,7 @@ - "Specifies the maximum average number of matches to allow per second. The number can specify units explicitly, using `/second', `/minute', `/hour' or `/day', or parts of them (so `5/second' is the same as `5/s')." required: false limit_burst: + version_added: "2.1" description: - "Specifies the maximum burst before the above limit kicks in." required: false From 5b61c75fa5dadb687aaa7e499c090bd179aa1de3 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 1 Feb 2016 23:56:58 -0500 Subject: [PATCH 1160/2522] added missing verison_added --- monitoring/sensu_check.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monitoring/sensu_check.py b/monitoring/sensu_check.py index 3fd13ff0aa6..f7f0562f48c 100644 --- a/monitoring/sensu_check.py +++ b/monitoring/sensu_check.py @@ -150,6 +150,7 @@ required: false default: null custom: + version_added: "2.1" description: - A hash/dictionary of custom parameters for mixing to the configuration. - You can't rewrite others module parameters using this From d2e390944356d22e795bd26b6291f4636fa5f576 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 2 Feb 2016 09:53:34 -0600 Subject: [PATCH 1161/2522] Fix DOCUMENTATION for ec2_vpc_dhcp_options.py and add missing RETURN to ec2_vol_facts.py --- cloud/amazon/ec2_vol_facts.py | 4 ++ cloud/amazon/ec2_vpc_dhcp_options.py | 104 +++++++++++++-------------- 2 files changed, 56 insertions(+), 52 deletions(-) diff --git a/cloud/amazon/ec2_vol_facts.py b/cloud/amazon/ec2_vol_facts.py index 921fbe510d4..e053a772d73 100644 --- a/cloud/amazon/ec2_vol_facts.py +++ b/cloud/amazon/ec2_vol_facts.py @@ -55,6 +55,10 @@ ''' +# TODO: Disabled the RETURN as it was breaking docs building. Someone needs to +# fix this +RETURN = '''# ''' + try: import boto.ec2 from boto.exception import BotoServerError diff --git a/cloud/amazon/ec2_vpc_dhcp_options.py b/cloud/amazon/ec2_vpc_dhcp_options.py index f68a689a887..813316cf974 100644 --- a/cloud/amazon/ec2_vpc_dhcp_options.py +++ b/cloud/amazon/ec2_vpc_dhcp_options.py @@ -20,61 +20,61 @@ requested description: - Converges the DHCP option set for the given VPC to the variables requested. - If any of the optional values are missing, they will either be treated - as a no-op (i.e., inherit what already exists for the VPC) or a purge of - existing options. Most of the options should be self-explanatory. + If any of the optional values are missing, they will either be treated + as a no-op (i.e., inherit what already exists for the VPC) or a purge of + existing options. Most of the options should be self-explanatory. author: "Joel Thompson (@joelthompson)" version_added: 2.1 options: - - domain_name: - description: - - The domain name to set in the DHCP option sets - required: - - false - default: "" - - dns_servers: - description: - - A list of hosts to set the DNS servers for the VPC to. (Should be a - list of IP addresses rather than host names.) - required: false - default: [] - - ntp_servers: - description: - - List of hosts to advertise as NTP servers for the VPC. - required: false - default: [] - - netbios_name_servers: - description: - - List of hosts to advertise as NetBIOS servers. - required: false - default: [] - - netbios_node_type: - description: - - NetBIOS node type to advertise in the DHCP options. The - default is 2, per AWS recommendation - http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_DHCP_Options.html - required: false - default: 2 - - vpc_id: - description: - - VPC ID to associate with the requested DHCP option set - required: true - - delete_old: - description: - - Whether to delete the old VPC DHCP option set when creating a new one. - This is primarily useful for debugging/development purposes when you - want to quickly roll back to the old option set. Note that this setting - will be ignored, and the old DHCP option set will be preserved, if it - is in use by any other VPC. (Otherwise, AWS will return an error.) - required: false - default: true - - inherit_existing: - description: - - For any DHCP options not specified in these parameters, whether to - inherit them from the options set already applied to vpc_id, or to - reset them to be empty. - required: false - default: false + domain_name: + description: + - The domain name to set in the DHCP option sets + required: + - false + default: "" + dns_servers: + description: + - A list of hosts to set the DNS servers for the VPC to. (Should be a + list of IP addresses rather than host names.) + required: false + default: [] + ntp_servers: + description: + - List of hosts to advertise as NTP servers for the VPC. + required: false + default: [] + netbios_name_servers: + description: + - List of hosts to advertise as NetBIOS servers. + required: false + default: [] + netbios_node_type: + description: + - NetBIOS node type to advertise in the DHCP options. The + default is 2, per AWS recommendation + http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_DHCP_Options.html + required: false + default: 2 + vpc_id: + description: + - VPC ID to associate with the requested DHCP option set + required: true + delete_old: + description: + - Whether to delete the old VPC DHCP option set when creating a new one. + This is primarily useful for debugging/development purposes when you + want to quickly roll back to the old option set. Note that this setting + will be ignored, and the old DHCP option set will be preserved, if it + is in use by any other VPC. (Otherwise, AWS will return an error.) + required: false + default: true + inherit_existing: + description: + - For any DHCP options not specified in these parameters, whether to + inherit them from the options set already applied to vpc_id, or to + reset them to be empty. + required: false + default: false extends_documentation_fragment: aws requirements: - boto From db31914f58a5d0e87370fb5c67a41d9868fd16ef Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Tue, 2 Feb 2016 19:10:22 +0100 Subject: [PATCH 1162/2522] cloudstack: use CS_HYPERVISORS from cloudstack utils --- cloud/cloudstack/cs_instance.py | 2 +- cloud/cloudstack/cs_template.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 73deb028be2..734faaf8235 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -887,7 +887,7 @@ def main(): disk_size = dict(type='int', default=None), root_disk_size = dict(type='int', default=None), keyboard = dict(choices=['de', 'de-ch', 'es', 'fi', 'fr', 'fr-be', 'fr-ch', 'is', 'it', 'jp', 'nl-be', 'no', 'pt', 'uk', 'us'], default=None), - hypervisor = dict(choices=['KVM', 'VMware', 'BareMetal', 'XenServer', 'LXC', 'HyperV', 'UCS', 'OVM', 'Simulator'], default=None), + hypervisor = dict(choices=CS_HYPERVISORS, default=None), security_groups = dict(type='list', aliases=[ 'security_group' ], default=[]), affinity_groups = dict(type='list', aliases=[ 'affinity_group' ], default=[]), domain = dict(default=None), diff --git a/cloud/cloudstack/cs_template.py b/cloud/cloudstack/cs_template.py index 753a22abc82..8690a6e1756 100644 --- a/cloud/cloudstack/cs_template.py +++ b/cloud/cloudstack/cs_template.py @@ -592,7 +592,7 @@ def main(): is_routing = dict(type='bool', default=False), checksum = dict(default=None), template_filter = dict(default='self', choices=['featured', 'self', 'selfexecutable', 'sharedexecutable', 'executable', 'community']), - hypervisor = dict(choices=['KVM', 'VMware', 'BareMetal', 'XenServer', 'LXC', 'HyperV', 'UCS', 'OVM', 'Simulator'], default=None), + hypervisor = dict(choices=CS_HYPERVISORS, default=None), requires_hvm = dict(type='bool', default=False), password_enabled = dict(type='bool', default=False), template_tag = dict(default=None), From 9f97615060219340c0da8492192cc162fb687428 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Tue, 2 Feb 2016 19:32:49 +0100 Subject: [PATCH 1163/2522] cloudstack: new module cs_configuration --- cloud/cloudstack/cs_configuration.py | 297 +++++++++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 cloud/cloudstack/cs_configuration.py diff --git a/cloud/cloudstack/cs_configuration.py b/cloud/cloudstack/cs_configuration.py new file mode 100644 index 00000000000..b3e68c6a788 --- /dev/null +++ b/cloud/cloudstack/cs_configuration.py @@ -0,0 +1,297 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2016, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: cs_configuration +short_description: Manages configuration on Apache CloudStack based clouds. +description: + - Manages global, zone, account, storage and cluster configurations. +version_added: "2.1" +author: "René Moser (@resmo)" +options: + name: + description: + - Name of the configuration. + required: true + value: + description: + - Value of the configuration. + required: true + account: + description: + - Ensure the value for corresponding account. + required: false + default: null + domain: + description: + - Domain the account is related to. + - Only considered if C(account) is used. + required: false + default: ROOT + zone: + description: + - Ensure the value for corresponding zone. + required: false + default: null + storage: + description: + - Ensure the value for corresponding storage pool. + required: false + default: null + cluster: + description: + - Ensure the value for corresponding cluster. + required: false + default: null +extends_documentation_fragment: cloudstack +''' + +EXAMPLES = ''' +# Ensure global configuration +- local_action: + module: cs_configuration + name: router.reboot.when.outofband.migrated + value: false + +# Ensure zone configuration +- local_action: + module: cs_configuration + name: router.reboot.when.outofband.migrated + zone: ch-gva-01 + value: true + +# Ensure storage configuration +- local_action: + module: cs_configuration + name: storage.overprovisioning.factor + storage: storage01 + value: 2.0 + +# Ensure account configuration +- local_action: + module: cs_configuration: + name: allow.public.user.templates + value: false + account: acme inc + domain: customers +''' + +RETURN = ''' +--- +category: + description: Category of the configuration. + returned: success + type: string + sample: Advanced +scope: + description: Scope (zone/cluster/storagepool/account) of the parameter that needs to be updated. + returned: success + type: string + sample: storagepool +description: + description: Description of the configuration. + returned: success + type: string + sample: Setup the host to do multipath +name: + description: Name of the configuration. + returned: success + type: string + sample: zone.vlan.capacity.notificationthreshold +value: + description: Value of the configuration. + returned: success + type: string + sample: "0.75" +account: + description: Account of the configuration. + returned: success + type: string + sample: admin +Domain: + description: Domain of account of the configuration. + returned: success + type: string + sample: ROOT +zone: + description: Zone of the configuration. + returned: success + type: string + sample: ch-gva-01 +cluster: + description: Cluster of the configuration. + returned: success + type: string + sample: cluster01 +storage: + description: Storage of the configuration. + returned: success + type: string + sample: storage01 +''' + +try: + from cs import CloudStack, CloudStackException, read_config + has_lib_cs = True +except ImportError: + has_lib_cs = False + +# import cloudstack common +from ansible.module_utils.cloudstack import * + +class AnsibleCloudStackConfiguration(AnsibleCloudStack): + + def __init__(self, module): + super(AnsibleCloudStackConfiguration, self).__init__(module) + self.returns = { + 'category': 'category', + 'scope': 'scope', + 'value': 'value', + } + self.storage = None + self.account = None + self.cluster = None + + + def _get_common_configuration_args(self): + args = {} + args['name'] = self.module.params.get('name') + args['accountid'] = self.get_account(key='id') + args['storageid'] = self.get_storage(key='id') + args['zoneid'] = self.get_zone(key='id') + args['clusterid'] = self.get_cluster(key='id') + return args + + + def get_zone(self, key=None): + # make sure we do net use the default zone + zone = self.module.params.get('zone') + if zone: + return super(AnsibleCloudStackConfiguration, self).get_zone(key=key) + + + def get_cluster(self, key=None): + if not self.cluster: + cluster_name = self.module.params.get('cluster') + if not cluster_name: + return None + args = {} + args['name'] = cluster_name + clusters = self.cs.listClusters(**args) + if clusters: + self.cluster = clusters['cluster'][0] + self.result['cluster'] = self.cluster['name'] + else: + self.module.fail_json(msg="Cluster %s not found." % cluster_name) + return self._get_by_key(key=key, my_dict=self.cluster) + + + def get_storage(self, key=None): + if not self.storage: + storage_pool_name = self.module.params.get('storage') + if not storage_pool_name: + return None + args = {} + args['name'] = storage_pool_name + storage_pools = self.cs.listStoragePools(**args) + if storage_pools: + self.storage = storage_pools['storagepool'][0] + self.result['storage'] = self.storage['name'] + else: + self.module.fail_json(msg="Storage pool %s not found." % storage_pool_name) + return self._get_by_key(key=key, my_dict=self.storage) + + + def get_configuration(self): + configuration = None + args = self._get_common_configuration_args() + configurations = self.cs.listConfigurations(**args) + if not configurations: + self.module.fail_json(msg="Configuration %s not found." % args['name']) + configuration = configurations['configuration'][0] + return configuration + + + def get_value(self): + value = str(self.module.params.get('value')) + if value in ('True', 'False'): + value = value.lower() + return value + + + def present_configuration(self): + configuration = self.get_configuration() + args = self._get_common_configuration_args() + args['value'] = self.get_value() + if self.has_changed(args, configuration, ['value']): + self.result['changed'] = True + if not self.module.check_mode: + res = self.cs.updateConfiguration(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + configuration = res['configuration'] + return configuration + + + def get_result(self, configuration): + self.result = super(AnsibleCloudStackConfiguration, self).get_result(configuration) + if self.account: + self.result['account'] = self.account['name'] + self.result['domain'] = self.domain['path'] + elif self.zone: + self.result['zone'] = self.zone['name'] + return self.result + +def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + name = dict(required=True), + value = dict(type='str', required=True), + zone = dict(default=None), + storage = dict(default=None), + cluster = dict(default=None), + account = dict(default=None), + domain = dict(default='ROOT') + )) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=cs_required_together(), + supports_check_mode=True + ) + + if not has_lib_cs: + module.fail_json(msg="python library cs required: pip install cs") + + try: + acs_configuration = AnsibleCloudStackConfiguration(module) + configuration = acs_configuration.present_configuration() + result = acs_configuration.get_result(configuration) + + except CloudStackException as e: + module.fail_json(msg='CloudStackException: %s' % str(e)) + + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From c73ed3b47be9e2659735a5ee906a23c23279ee9c Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Tue, 2 Feb 2016 19:35:58 +0100 Subject: [PATCH 1164/2522] cloudstack: add new module cs_resourcelimit --- cloud/cloudstack/cs_resourcelimit.py | 225 +++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 cloud/cloudstack/cs_resourcelimit.py diff --git a/cloud/cloudstack/cs_resourcelimit.py b/cloud/cloudstack/cs_resourcelimit.py new file mode 100644 index 00000000000..b53b3fb233d --- /dev/null +++ b/cloud/cloudstack/cs_resourcelimit.py @@ -0,0 +1,225 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2016, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: cs_resourcelimit +short_description: Manages resource limits on Apache CloudStack based clouds. +description: + - Manage limits of resources for domains, accounts and projects. +version_added: "2.1" +author: "René Moser (@resmo)" +options: + resource_type: + description: + - Type of the resource. + required: true + choices: + - instance + - ip_address + - volume + - snapshot + - template + - network + - vpc + - cpu + - memory + - primary_storage + - secondary_storage + aliases: [ 'type' ] + limit: + description: + - Maximum number of the resource. + - Default is unlimited C(-1). + required: false + default: -1 + aliases: [ 'max' ] + domain: + description: + - Domain the resource is related to. + required: false + default: null + account: + description: + - Account the resource is related to. + required: false + default: null + project: + description: + - Name of the project the resource is related to. + required: false + default: null +extends_documentation_fragment: cloudstack +''' + +EXAMPLES = ''' +# Update a resource limit for instances of a domain +local_action: + module: cs_resourcelimit + type: instance + limit: 10 + domain: customers + +# Update a resource limit for instances of an account +local_action: + module: cs_resourcelimit + type: instance + limit: 12 + account: moserre + domain: customers +''' + +RETURN = ''' +--- +recource_type: + description: Type of the resource + returned: success + type: string + sample: instance +limit: + description: Maximum number of the resource. + returned: success + type: int + sample: -1 +domain: + description: Domain the resource is related to. + returned: success + type: string + sample: example domain +account: + description: Account the resource is related to. + returned: success + type: string + sample: example account +project: + description: Project the resource is related to. + returned: success + type: string + sample: example project +''' + +try: + from cs import CloudStack, CloudStackException, read_config + has_lib_cs = True +except ImportError: + has_lib_cs = False + +# import cloudstack common +from ansible.module_utils.cloudstack import * + +RESOURCE_TYPES = { + 'instance': 0, + 'ip_address': 1, + 'volume': 2, + 'snapshot': 3, + 'template': 4, + 'network': 6, + 'vpc': 7, + 'cpu': 8, + 'memory': 9, + 'primary_storage': 10, + 'secondary_storage': 11, +} + +class AnsibleCloudStackResourceLimit(AnsibleCloudStack): + + def __init__(self, module): + super(AnsibleCloudStackResourceLimit, self).__init__(module) + self.returns = { + 'max': 'limit', + } + + + def get_resource_type(self): + resource_type = self.module.params.get('resource_type') + return RESOURCE_TYPES.get(resource_type) + + + def get_resource_limit(self): + args = {} + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') + args['projectid'] = self.get_project(key='id') + args['resourcetype'] = self.get_resource_type() + resource_limit = self.cs.listResourceLimits(**args) + if resource_limit: + return resource_limit['resourcelimit'][0] + self.module.fail_json(msg="Resource limit type '%s' not found." % self.module.params.get('resource_type')) + + + def update_resource_limit(self): + resource_limit = self.get_resource_limit() + + args = {} + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') + args['projectid'] = self.get_project(key='id') + args['resourcetype'] = self.get_resource_type() + args['max'] = self.module.params.get('limit', -1) + + if self.has_changed(args, resource_limit): + self.result['changed'] = True + if not self.module.check_mode: + res = self.cs.updateResourceLimit(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + resource_limit = res['resourcelimit'] + return resource_limit + + + def get_result(self, resource_limit): + self.result = super(AnsibleCloudStackResourceLimit, self).get_result(resource_limit) + self.result['resource_type'] = self.module.params.get('resource_type') + return self.result + + +def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + resource_type = dict(required=True, choices=RESOURCE_TYPES.keys(), aliases=['type']), + limit = dict(default=-1, aliases=['max']), + domain = dict(default=None), + account = dict(default=None), + project = dict(default=None), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=cs_required_together(), + supports_check_mode=True + ) + + if not has_lib_cs: + module.fail_json(msg="python library cs required: pip install cs") + + try: + acs_resource_limit = AnsibleCloudStackResourceLimit(module) + resource_limit = acs_resource_limit.update_resource_limit() + result = acs_resource_limit.get_result(resource_limit) + + except CloudStackException as e: + module.fail_json(msg='CloudStackException: %s' % str(e)) + + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From b1e9dc0b52b3bb0d44bf78b6e3797dfc82e21fda Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Tue, 2 Feb 2016 10:39:19 -0800 Subject: [PATCH 1165/2522] Allow dnf to remove dependent packages of a package that is being removed Fixes #1517 --- packaging/os/dnf.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index 50c86fc4f62..56039258ed5 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -234,6 +234,7 @@ def _mark_package_install(module, base, pkg_spec): def ensure(module, base, state, names): + allow_erasing = False if names == ['*'] and state == 'latest': base.upgrade_all() else: @@ -280,6 +281,7 @@ def ensure(module, base, state, names): _mark_package_install(module, base, pkg_spec) else: + # state == absent if filenames: module.fail_json( msg="Cannot remove paths -- please specify package name.") @@ -291,8 +293,11 @@ def ensure(module, base, state, names): for pkg_spec in pkg_specs: if installed.filter(name=pkg_spec): base.remove(pkg_spec) + # Like the dnf CLI we want to allow recursive removal of dependent + # packages + allow_erasing = True - if not base.resolve(): + if not base.resolve(allow_erasing=allow_erasing): module.exit_json(msg="Nothing to do") else: if module.check_mode: From c32569d7f85df19a084fc2e4e8a0b41474ca9719 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Tue, 2 Feb 2016 19:19:07 +0100 Subject: [PATCH 1166/2522] cloudstack: cs_instance: implement updating security groups ACS API implemented in 4.8, has no effect < 4.8. --- cloud/cloudstack/cs_instance.py | 52 +++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 734faaf8235..9674b589da4 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -143,7 +143,7 @@ description: - List of security groups the instance to be applied to. required: false - default: [] + default: null aliases: [ 'security_group' ] domain: description: @@ -521,6 +521,27 @@ def get_iptonetwork_mappings(self): return res + def security_groups_has_changed(self): + security_groups = self.module.params.get('security_groups') + if security_groups is None: + return False + + security_groups = [s.lower() for s in security_groups] + instance_security_groups = self.instance.get('securitygroup',[]) + + instance_security_group_names = [] + for instance_security_group in instance_security_groups: + if instance_security_group['name'].lower() not in security_groups: + return True + else: + instance_security_group_names.append(instance_security_group['name'].lower()) + + for security_group in security_groups: + if security_group not in instance_security_group_names: + return True + return False + + def get_network_ids(self, network_names=None): if network_names is None: network_names = self.module.params.get('networks') @@ -622,10 +643,13 @@ def deploy_instance(self, start_vm=True): args['size'] = self.module.params.get('disk_size') args['startvm'] = start_vm args['rootdisksize'] = self.module.params.get('root_disk_size') - args['securitygroupnames'] = ','.join(self.module.params.get('security_groups')) args['affinitygroupnames'] = ','.join(self.module.params.get('affinity_groups')) args['details'] = self.get_details() + security_groups = self.module.params.get('security_groups') + if security_groups is not None: + args['securitygroupnames'] = ','.join(security_groups) + template_iso = self.get_template_or_iso() if 'hypervisor' not in template_iso: args['hypervisor'] = self.get_hypervisor() @@ -649,6 +673,7 @@ def update_instance(self, instance, start_vm=True): args_service_offering['id'] = instance['id'] if self.module.params.get('service_offering'): args_service_offering['serviceofferingid'] = self.get_service_offering_id() + service_offering_changed = self._has_changed(args_service_offering, instance) # Instance data args_instance_update = {} @@ -659,6 +684,7 @@ def update_instance(self, instance, start_vm=True): args_instance_update['group'] = self.module.params.get('group') if self.module.params.get('display_name'): args_instance_update['displayname'] = self.module.params.get('display_name') + instance_changed = self._has_changed(args_instance_update, instance) # SSH key data args_ssh_key = {} @@ -666,12 +692,18 @@ def update_instance(self, instance, start_vm=True): args_ssh_key['projectid'] = self.get_project(key='id') if self.module.params.get('ssh_key'): args_ssh_key['keypair'] = self.module.params.get('ssh_key') + ssh_key_changed = self._has_changed(args_ssh_key, instance) + security_groups_changed = self.security_groups_has_changed() - if self._has_changed(args_service_offering, instance) or \ - self._has_changed(args_instance_update, instance) or \ - self._has_changed(args_ssh_key, instance): + changed = [ + service_offering_changed, + instance_changed, + security_groups_changed, + ssh_key_changed, + ] + if True in changed: force = self.module.params.get('force') instance_state = instance['state'].lower() if instance_state == 'stopped' or force: @@ -684,7 +716,7 @@ def update_instance(self, instance, start_vm=True): self.instance = instance # Change service offering - if self._has_changed(args_service_offering, instance): + if service_offering_changed: res = self.cs.changeServiceForVirtualMachine(**args_service_offering) if 'errortext' in res: self.module.fail_json(msg="Failed: '%s'" % res['errortext']) @@ -692,7 +724,9 @@ def update_instance(self, instance, start_vm=True): self.instance = instance # Update VM - if self._has_changed(args_instance_update, instance): + if instance_changed or security_groups_changed: + if security_groups_changed: + args_instance_update['securitygroupnames'] = ','.join(self.module.params.get('security_groups')) res = self.cs.updateVirtualMachine(**args_instance_update) if 'errortext' in res: self.module.fail_json(msg="Failed: '%s'" % res['errortext']) @@ -700,7 +734,7 @@ def update_instance(self, instance, start_vm=True): self.instance = instance # Reset SSH key - if self._has_changed(args_ssh_key, instance): + if ssh_key_changed: instance = self.cs.resetSSHKeyForVirtualMachine(**args_ssh_key) if 'errortext' in instance: self.module.fail_json(msg="Failed: '%s'" % instance['errortext']) @@ -888,7 +922,7 @@ def main(): root_disk_size = dict(type='int', default=None), keyboard = dict(choices=['de', 'de-ch', 'es', 'fi', 'fr', 'fr-be', 'fr-ch', 'is', 'it', 'jp', 'nl-be', 'no', 'pt', 'uk', 'us'], default=None), hypervisor = dict(choices=CS_HYPERVISORS, default=None), - security_groups = dict(type='list', aliases=[ 'security_group' ], default=[]), + security_groups = dict(type='list', aliases=[ 'security_group' ], default=None), affinity_groups = dict(type='list', aliases=[ 'affinity_group' ], default=[]), domain = dict(default=None), account = dict(default=None), From 2e9797ca8cc4705bfe1eff2566571e36a969e81f Mon Sep 17 00:00:00 2001 From: yta Date: Tue, 2 Feb 2016 23:19:33 +0900 Subject: [PATCH 1167/2522] osx_defaults: Do not make any changes in check mode --- system/osx_defaults.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/system/osx_defaults.py b/system/osx_defaults.py index 98aedcbd709..c59ba355eb2 100644 --- a/system/osx_defaults.py +++ b/system/osx_defaults.py @@ -257,6 +257,8 @@ def run(self): print "Absent state detected!" if self.current_value is None: return False + if self.module.check_mode: + return True self.delete() return True @@ -274,6 +276,9 @@ def run(self): elif self.current_value == self.value: return False + if self.module.check_mode: + return True + # Change/Create/Set given key/value for domain in defaults self.write() return True From a1c2cb0a2e49ee282110e9b743f05cd6f8421452 Mon Sep 17 00:00:00 2001 From: Daniel Petty Date: Wed, 3 Feb 2016 14:36:04 -0700 Subject: [PATCH 1168/2522] Fix indent --- cloud/vmware/vca_vapp.py | 82 ++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/cloud/vmware/vca_vapp.py b/cloud/vmware/vca_vapp.py index 87810c5fae0..1c37c350abe 100644 --- a/cloud/vmware/vca_vapp.py +++ b/cloud/vmware/vca_vapp.py @@ -89,47 +89,47 @@ required: no default: present choices: ['present', 'absent', 'deployed', 'undeployed'] - username: - description: - - The vCloud Air username to use during authentication - required: false - default: None - password: - description: - - The vCloud Air password to use during authentication - required: false - default: None - org: - description: - - The org to login to for creating vapp, mostly set when the service_type is vdc. - required: false - default: None - instance_id: - description: - - The instance id in a vchs environment to be used for creating the vapp - required: false - default: None - host: - description: - - The authentication host to be used when service type is vcd. - required: false - default: None - api_version: - description: - - The api version to be used with the vca - required: false - default: "5.7" - service_type: - description: - - The type of service we are authenticating against - required: false - default: vca - choices: [ "vca", "vchs", "vcd" ] - vdc_name: - description: - - The name of the vdc where the vm should be created. - required: false - default: None + username: + description: + - The vCloud Air username to use during authentication + required: false + default: None + password: + description: + - The vCloud Air password to use during authentication + required: false + default: None + org: + description: + - The org to login to for creating vapp, mostly set when the service_type is vdc. + required: false + default: None + instance_id: + description: + - The instance id in a vchs environment to be used for creating the vapp + required: false + default: None + host: + description: + - The authentication host to be used when service type is vcd. + required: false + default: None + api_version: + description: + - The api version to be used with the vca + required: false + default: "5.7" + service_type: + description: + - The type of service we are authenticating against + required: false + default: vca + choices: [ "vca", "vchs", "vcd" ] + vdc_name: + description: + - The name of the vdc where the vm should be created. + required: false + default: None ''' EXAMPLES = ''' From 7d79dff1d2242490ae73343ed0833d9c955ddcbf Mon Sep 17 00:00:00 2001 From: Ronny Date: Fri, 5 Feb 2016 13:33:50 +0100 Subject: [PATCH 1169/2522] Update zabbix_host.py Use existing proxy when updating a host unless proxy is specified. Before change proxy was always set to none(0) when updating. --- monitoring/zabbix_host.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/monitoring/zabbix_host.py b/monitoring/zabbix_host.py index 5b6748a3e94..7513d6bde09 100644 --- a/monitoring/zabbix_host.py +++ b/monitoring/zabbix_host.py @@ -203,7 +203,9 @@ def update_host(self, host_name, group_ids, status, host_id, interfaces, exist_i try: if self._module.check_mode: self._module.exit_json(changed=True) - parameters = {'hostid': host_id, 'groups': group_ids, 'status': status, 'proxy_hostid': proxy_id} + parameters = {'hostid': host_id, 'groups': group_ids, 'status': status} + if proxy_id: + parameters['proxy_hostid'] = proxy_id self._zapi.host.update(parameters) interface_list_copy = exist_interface_list if interfaces: @@ -427,15 +429,16 @@ def main(): if interface['type'] == 1: ip = interface['ip'] - proxy_id = "0" - - if proxy: - proxy_id = host.get_proxyid_by_proxy_name(proxy) - # check if host exist is_host_exist = host.is_host_exist(host_name) if is_host_exist: + # Use proxy specified, or set to None when updating host + if proxy: + proxy_id = host.get_proxyid_by_proxy_name(proxy) + else: + proxy_id = None + # get host id by host name zabbix_host_obj = host.get_host_by_host_name(host_name) host_id = zabbix_host_obj['hostid'] @@ -480,6 +483,12 @@ def main(): else: module.exit_json(changed=False) else: + # Use proxy specified, or set to 0 when adding new host + if proxy: + proxy_id = host.get_proxyid_by_proxy_name(proxy) + else: + proxy_id = 0 + if not group_ids: module.fail_json(msg="Specify at least one group for creating host '%s'." % host_name) From df7e3b081e88664dafee51fc5f0d80a7150ad874 Mon Sep 17 00:00:00 2001 From: Joseph Callen Date: Fri, 5 Feb 2016 14:14:25 -0500 Subject: [PATCH 1170/2522] Resolves issue with vmware_cluster module for v2.0 When this module was written back in May 2015 we were using 1.9.x. Being lazy I added to param the objects that the other functions would need. What I have noticed is in 2.0 exit_json is trying to jsonify those complex objects and failing. This PR resolves that issue with the vmware_cluster module. @kamsz reported this issue in https://github.com/ansible/ansible-modules-extras/pull/1568 Playbook ``` - name: Create Cluster local_action: module: vmware_cluster hostname: "{{ mgmt_ip_address }}" username: "{{ vcsa_user }}" password: "{{ vcsa_pass }}" datacenter_name: "{{ mgmt_vdc }}" cluster_name: "{{ mgmt_cluster }}" enable_ha: True enable_drs: True enable_vsan: True ``` Module testing ``` TASK [Create Cluster] ********************************************************** task path: /opt/autodeploy/projects/emmet/site_deploy.yml:188 ESTABLISH LOCAL CONNECTION FOR USER: root localhost EXEC ( umask 22 && mkdir -p "$( echo $HOME/.ansible/tmp/ansible-tmp-1454693788.92-14097560271233 )" && echo "$( echo $HOME/.ansible/tmp/ansible-tmp-1454693788.92-14097560271233 )" ) localhost PUT /tmp/tmpAJfdPb TO /root/.ansible/tmp/ansible-tmp-1454693788.92-14097560271233/vmware_cluster localhost EXEC LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /root/.ansible/tmp/ansible-tmp-1454693788.92-14097560271233/vmware_cluster; rm -rf "/root/.ansible/tmp/ansible-tmp-1454693788.92-14097560271233/" > /dev/null 2>&1 changed: [foundation-vcsa -> localhost] => {"changed": true, "invocation": {"module_args": {"cluster_name": "Foundation", "datacenter_name": "Test-Lab", "enable_drs": true, "enable_ha": true, "enable_vsan": true, "hostname": "172.27.0.100", "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", "state": "present", "username": "root"}, "module_name": "vmware_cluster"}} ``` --- cloud/vmware/vmware_cluster.py | 306 ++++++++++++++++----------------- 1 file changed, 146 insertions(+), 160 deletions(-) diff --git a/cloud/vmware/vmware_cluster.py b/cloud/vmware/vmware_cluster.py index 2b939adc8ea..8067d36de2c 100644 --- a/cloud/vmware/vmware_cluster.py +++ b/cloud/vmware/vmware_cluster.py @@ -77,152 +77,153 @@ HAS_PYVMOMI = False -def configure_ha(enable_ha): - das_config = vim.cluster.DasConfigInfo() - das_config.enabled = enable_ha - das_config.admissionControlPolicy = vim.cluster.FailoverLevelAdmissionControlPolicy() - das_config.admissionControlPolicy.failoverLevel = 2 - return das_config - - -def configure_drs(enable_drs): - drs_config = vim.cluster.DrsConfigInfo() - drs_config.enabled = enable_drs - # Set to partially automated - drs_config.vmotionRate = 3 - return drs_config - - -def configure_vsan(enable_vsan): - vsan_config = vim.vsan.cluster.ConfigInfo() - vsan_config.enabled = enable_vsan - vsan_config.defaultConfig = vim.vsan.cluster.ConfigInfo.HostDefaultInfo() - vsan_config.defaultConfig.autoClaimStorage = False - return vsan_config - - -def state_create_cluster(module): - - enable_ha = module.params['enable_ha'] - enable_drs = module.params['enable_drs'] - enable_vsan = module.params['enable_vsan'] - cluster_name = module.params['cluster_name'] - datacenter = module.params['datacenter'] - - try: +class VMwareCluster(object): + def __init__(self, module): + self.module = module + self.enable_ha = module.params['enable_ha'] + self.enable_drs = module.params['enable_drs'] + self.enable_vsan = module.params['enable_vsan'] + self.cluster_name = module.params['cluster_name'] + self.desired_state = module.params['state'] + self.datacenter = None + self.cluster = None + self.content = connect_to_api(module) + self.datacenter_name = module.params['datacenter_name'] + + def process_state(self): + cluster_states = { + 'absent': { + 'present': self.state_destroy_cluster, + 'absent': self.state_exit_unchanged, + }, + 'present': { + 'update': self.state_update_cluster, + 'present': self.state_exit_unchanged, + 'absent': self.state_create_cluster, + } + } + current_state = self.check_cluster_configuration() + # Based on the desired_state and the current_state call + # the appropriate method from the dictionary + cluster_states[self.desired_state][current_state]() + + def configure_ha(self): + das_config = vim.cluster.DasConfigInfo() + das_config.enabled = self.enable_ha + das_config.admissionControlPolicy = vim.cluster.FailoverLevelAdmissionControlPolicy() + das_config.admissionControlPolicy.failoverLevel = 2 + return das_config + + def configure_drs(self): + drs_config = vim.cluster.DrsConfigInfo() + drs_config.enabled = self.enable_drs + # Set to partially automated + drs_config.vmotionRate = 3 + return drs_config + + def configure_vsan(self): + vsan_config = vim.vsan.cluster.ConfigInfo() + vsan_config.enabled = self.enable_vsan + vsan_config.defaultConfig = vim.vsan.cluster.ConfigInfo.HostDefaultInfo() + vsan_config.defaultConfig.autoClaimStorage = False + return vsan_config + + def state_create_cluster(self): + try: + cluster_config_spec = vim.cluster.ConfigSpecEx() + cluster_config_spec.dasConfig = self.configure_ha() + cluster_config_spec.drsConfig = self.configure_drs() + if self.enable_vsan: + cluster_config_spec.vsanConfig = self.configure_vsan() + if not self.module.check_mode: + self.datacenter.hostFolder.CreateClusterEx(self.cluster_name, cluster_config_spec) + self.module.exit_json(changed=True) + except vim.fault.DuplicateName: + self.module.fail_json(msg="A cluster with the name %s already exists" % self.cluster_name) + except vmodl.fault.InvalidArgument: + self.module.fail_json(msg="Cluster configuration specification parameter is invalid") + except vim.fault.InvalidName: + self.module.fail_json(msg="%s is an invalid name for a cluster" % self.cluster_name) + except vmodl.fault.NotSupported: + # This should never happen + self.module.fail_json(msg="Trying to create a cluster on an incorrect folder object") + except vmodl.RuntimeFault as runtime_fault: + self.module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + # This should never happen either + self.module.fail_json(msg=method_fault.msg) + + def state_destroy_cluster(self): + changed = True + result = None + + try: + if not self.module.check_mode: + task = self.cluster.Destroy_Task() + changed, result = wait_for_task(task) + self.module.exit_json(changed=changed, result=result) + except vim.fault.VimFault as vim_fault: + self.module.fail_json(msg=vim_fault.msg) + except vmodl.RuntimeFault as runtime_fault: + self.module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + self.module.fail_json(msg=method_fault.msg) + + def state_exit_unchanged(self): + self.module.exit_json(changed=False) + + def state_update_cluster(self): cluster_config_spec = vim.cluster.ConfigSpecEx() - cluster_config_spec.dasConfig = configure_ha(enable_ha) - cluster_config_spec.drsConfig = configure_drs(enable_drs) - if enable_vsan: - cluster_config_spec.vsanConfig = configure_vsan(enable_vsan) - if not module.check_mode: - datacenter.hostFolder.CreateClusterEx(cluster_name, cluster_config_spec) - module.exit_json(changed=True) - except vim.fault.DuplicateName: - module.fail_json(msg="A cluster with the name %s already exists" % cluster_name) - except vmodl.fault.InvalidArgument: - module.fail_json(msg="Cluster configuration specification parameter is invalid") - except vim.fault.InvalidName: - module.fail_json(msg="%s is an invalid name for a cluster" % cluster_name) - except vmodl.fault.NotSupported: - # This should never happen - module.fail_json(msg="Trying to create a cluster on an incorrect folder object") - except vmodl.RuntimeFault as runtime_fault: - module.fail_json(msg=runtime_fault.msg) - except vmodl.MethodFault as method_fault: - # This should never happen either - module.fail_json(msg=method_fault.msg) - - -def state_destroy_cluster(module): - cluster = module.params['cluster'] - changed = True - result = None - - try: - if not module.check_mode: - task = cluster.Destroy_Task() - changed, result = wait_for_task(task) - module.exit_json(changed=changed, result=result) - except vim.fault.VimFault as vim_fault: - module.fail_json(msg=vim_fault.msg) - except vmodl.RuntimeFault as runtime_fault: - module.fail_json(msg=runtime_fault.msg) - except vmodl.MethodFault as method_fault: - module.fail_json(msg=method_fault.msg) - - -def state_exit_unchanged(module): - module.exit_json(changed=False) - - -def state_update_cluster(module): - - cluster_config_spec = vim.cluster.ConfigSpecEx() - cluster = module.params['cluster'] - enable_ha = module.params['enable_ha'] - enable_drs = module.params['enable_drs'] - enable_vsan = module.params['enable_vsan'] - changed = True - result = None - - if cluster.configurationEx.dasConfig.enabled != enable_ha: - cluster_config_spec.dasConfig = configure_ha(enable_ha) - if cluster.configurationEx.drsConfig.enabled != enable_drs: - cluster_config_spec.drsConfig = configure_drs(enable_drs) - if cluster.configurationEx.vsanConfigInfo.enabled != enable_vsan: - cluster_config_spec.vsanConfig = configure_vsan(enable_vsan) - - try: - if not module.check_mode: - task = cluster.ReconfigureComputeResource_Task(cluster_config_spec, True) - changed, result = wait_for_task(task) - module.exit_json(changed=changed, result=result) - except vmodl.RuntimeFault as runtime_fault: - module.fail_json(msg=runtime_fault.msg) - except vmodl.MethodFault as method_fault: - module.fail_json(msg=method_fault.msg) - except TaskError as task_e: - module.fail_json(msg=str(task_e)) - - -def check_cluster_configuration(module): - datacenter_name = module.params['datacenter_name'] - cluster_name = module.params['cluster_name'] - - try: - content = connect_to_api(module) - datacenter = find_datacenter_by_name(content, datacenter_name) - if datacenter is None: - module.fail_json(msg="Datacenter %s does not exist, " - "please create first with Ansible Module vmware_datacenter or manually." - % datacenter_name) - cluster = find_cluster_by_name_datacenter(datacenter, cluster_name) - - module.params['content'] = content - module.params['datacenter'] = datacenter - - if cluster is None: - return 'absent' - else: - module.params['cluster'] = cluster + changed = True + result = None + + if self.cluster.configurationEx.dasConfig.enabled != self.enable_ha: + cluster_config_spec.dasConfig = self.configure_ha() + if self.cluster.configurationEx.drsConfig.enabled != self.enable_drs: + cluster_config_spec.drsConfig = self.configure_drs() + if self.cluster.configurationEx.vsanConfigInfo.enabled != self.enable_vsan: + cluster_config_spec.vsanConfig = self.configure_vsan() + + try: + if not self.module.check_mode: + task = self.cluster.ReconfigureComputeResource_Task(cluster_config_spec, True) + changed, result = wait_for_task(task) + self.module.exit_json(changed=changed, result=result) + except vmodl.RuntimeFault as runtime_fault: + self.module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + self.module.fail_json(msg=method_fault.msg) + except TaskError as task_e: + self.module.fail_json(msg=str(task_e)) + + def check_cluster_configuration(self): + try: + self.datacenter = find_datacenter_by_name(self.content, self.datacenter_name) + if self.datacenter is None: + self.module.fail_json(msg="Datacenter %s does not exist, " + "please create first with Ansible Module vmware_datacenter or manually." + % self.datacenter_name) + self.cluster = find_cluster_by_name_datacenter(self.datacenter, self.cluster_name) + + if self.cluster is None: + return 'absent' + else: + desired_state = (self.enable_ha, + self.enable_drs, + self.enable_vsan) - desired_state = (module.params['enable_ha'], - module.params['enable_drs'], - module.params['enable_vsan']) + current_state = (self.cluster.configurationEx.dasConfig.enabled, + self.cluster.configurationEx.drsConfig.enabled, + self.cluster.configurationEx.vsanConfigInfo.enabled) - current_state = (cluster.configurationEx.dasConfig.enabled, - cluster.configurationEx.drsConfig.enabled, - cluster.configurationEx.vsanConfigInfo.enabled) - - if cmp(desired_state, current_state) != 0: - return 'update' - else: - return 'present' - except vmodl.RuntimeFault as runtime_fault: - module.fail_json(msg=runtime_fault.msg) - except vmodl.MethodFault as method_fault: - module.fail_json(msg=method_fault.msg) + if cmp(desired_state, current_state) != 0: + return 'update' + else: + return 'present' + except vmodl.RuntimeFault as runtime_fault: + self.module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + self.module.fail_json(msg=method_fault.msg) def main(): @@ -240,23 +241,8 @@ def main(): if not HAS_PYVMOMI: module.fail_json(msg='pyvmomi is required for this module') - cluster_states = { - 'absent': { - 'present': state_destroy_cluster, - 'absent': state_exit_unchanged, - }, - 'present': { - 'update': state_update_cluster, - 'present': state_exit_unchanged, - 'absent': state_create_cluster, - } - } - desired_state = module.params['state'] - current_state = check_cluster_configuration(module) - - # Based on the desired_state and the current_state call - # the appropriate method from the dictionary - cluster_states[desired_state][current_state](module) + vmware_cluster = VMwareCluster(module) + vmware_cluster.process_state() from ansible.module_utils.vmware import * from ansible.module_utils.basic import * From 9093c02446543b9d6ab4eac44b9f16dcff067519 Mon Sep 17 00:00:00 2001 From: Joseph Callen Date: Fri, 5 Feb 2016 14:25:47 -0500 Subject: [PATCH 1171/2522] Resolves issue with vmware_dvswitch module for v2.0 When this module was written back in May 2015 we were using 1.9.x. Being lazy I added to param the objects that the other functions would need. What I have noticed is in 2.0 exit_json is trying to jsonify those complex objects and failing. This PR resolves that issue with the vmware_dvswitch module. @kamsz reported this issue in https://github.com/ansible/ansible-modules-extras/pull/1568 Playbook ``` - name: Create dvswitch local_action: module: vmware_dvswitch hostname: "{{ mgmt_ip_address }}" username: "{{ vcsa_user }}" password: "{{ vcsa_pass }}" datacenter_name: "{{ mgmt_vdc }}" switch_name: dvSwitch mtu: 1500 uplink_quantity: 2 discovery_proto: lldp discovery_operation: both state: present ``` Module Testing ``` TASK [Create dvswitch] ********************************************************* task path: /opt/autodeploy/projects/emmet/tasks/deploy/dvs_network.yml:3 ESTABLISH LOCAL CONNECTION FOR USER: root localhost EXEC ( umask 22 && mkdir -p "$( echo $HOME/.ansible/tmp/ansible-tmp-1454693792.01-113207408596014 )" && echo "$( echo $HOME/.ansible/tmp/ansible-tmp-1454693792.01-113207408596014 )" ) localhost PUT /tmp/tmptb3e2c TO /root/.ansible/tmp/ansible-tmp-1454693792.01-113207408596014/vmware_dvswitch localhost EXEC LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /root/.ansible/tmp/ansible-tmp-1454693792.01-113207408596014/vmware_dvswitch; rm -rf "/root/.ansible/tmp/ansible-tmp-1454693792.01-113207408596014/" > /dev/null 2>&1 changed: [foundation-vcsa -> localhost] => {"changed": true, "invocation": {"module_args": {"datacenter_name": "Test-Lab", "discovery_operation": "both", "discovery_proto": "lldp", "hostname": "172.27.0.100", "mtu": 1500, "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", "state": "present", "switch_name": "dvSwitch", "uplink_quantity": 2, "username": "root"}, "module_name": "vmware_dvswitch"}, "result": "'vim.dvs.VmwareDistributedVirtualSwitch:dvs-9'"} ``` --- cloud/vmware/vmware_dvswitch.py | 155 ++++++++++++++++---------------- 1 file changed, 76 insertions(+), 79 deletions(-) diff --git a/cloud/vmware/vmware_dvswitch.py b/cloud/vmware/vmware_dvswitch.py index 4ebadd0c606..fb9d530605f 100644 --- a/cloud/vmware/vmware_dvswitch.py +++ b/cloud/vmware/vmware_dvswitch.py @@ -95,78 +95,93 @@ except ImportError: HAS_PYVMOMI = False +class VMwareDVSwitch(object): + def __init__(self, module): + self.module = module + self.dvs = None + self.switch_name = self.module.params['switch_name'] + self.datacenter_name = self.module.params['datacenter_name'] + self.mtu = self.module.params['mtu'] + self.uplink_quantity = self.module.params['uplink_quantity'] + self.discovery_proto = self.module.params['discovery_proto'] + self.discovery_operation = self.module.params['discovery_operation'] + self.switch_name = self.module.params['switch_name'] + self.state = self.module.params['state'] + self.content = connect_to_api(module) + + def process_state(self): + try: + dvs_states = { + 'absent': { + 'present': self.state_destroy_dvs, + 'absent': self.state_exit_unchanged, + }, + 'present': { + 'update': self.state_update_dvs, + 'present': self.state_exit_unchanged, + 'absent': self.state_create_dvs, + } + } + dvs_states[self.state][self.check_dvs_configuration()]() + except vmodl.RuntimeFault as runtime_fault: + self.module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + self.module.fail_json(msg=method_fault.msg) + except Exception as e: + self.module.fail_json(msg=str(e)) -def create_dvswitch(network_folder, switch_name, mtu, uplink_quantity, discovery_proto, discovery_operation): - - result = None - changed = False - - spec = vim.DistributedVirtualSwitch.CreateSpec() - spec.configSpec = vim.dvs.VmwareDistributedVirtualSwitch.ConfigSpec() - spec.configSpec.uplinkPortPolicy = vim.DistributedVirtualSwitch.NameArrayUplinkPortPolicy() - spec.configSpec.linkDiscoveryProtocolConfig = vim.host.LinkDiscoveryProtocolConfig() - - spec.configSpec.name = switch_name - spec.configSpec.maxMtu = mtu - spec.configSpec.linkDiscoveryProtocolConfig.protocol = discovery_proto - spec.configSpec.linkDiscoveryProtocolConfig.operation = discovery_operation - spec.productInfo = vim.dvs.ProductSpec() - spec.productInfo.name = "DVS" - spec.productInfo.vendor = "VMware" - - for count in range(1, uplink_quantity+1): - spec.configSpec.uplinkPortPolicy.uplinkPortName.append("uplink%d" % count) - - task = network_folder.CreateDVS_Task(spec) - changed, result = wait_for_task(task) - return changed, result + def create_dvswitch(self, network_folder): + result = None + changed = False -def state_exit_unchanged(module): - module.exit_json(changed=False) + spec = vim.DistributedVirtualSwitch.CreateSpec() + spec.configSpec = vim.dvs.VmwareDistributedVirtualSwitch.ConfigSpec() + spec.configSpec.uplinkPortPolicy = vim.DistributedVirtualSwitch.NameArrayUplinkPortPolicy() + spec.configSpec.linkDiscoveryProtocolConfig = vim.host.LinkDiscoveryProtocolConfig() + spec.configSpec.name = self.switch_name + spec.configSpec.maxMtu = self.mtu + spec.configSpec.linkDiscoveryProtocolConfig.protocol = self.discovery_proto + spec.configSpec.linkDiscoveryProtocolConfig.operation = self.discovery_operation + spec.productInfo = vim.dvs.ProductSpec() + spec.productInfo.name = "DVS" + spec.productInfo.vendor = "VMware" -def state_destroy_dvs(module): - dvs = module.params['dvs'] - task = dvs.Destroy_Task() - changed, result = wait_for_task(task) - module.exit_json(changed=changed, result=str(result)) + for count in range(1, self.uplink_quantity+1): + spec.configSpec.uplinkPortPolicy.uplinkPortName.append("uplink%d" % count) + task = network_folder.CreateDVS_Task(spec) + changed, result = wait_for_task(task) + return changed, result -def state_update_dvs(module): - module.exit_json(changed=False, msg="Currently not implemented.") + def state_exit_unchanged(self): + self.module.exit_json(changed=False) + def state_destroy_dvs(self): + task = self.dvs.Destroy_Task() + changed, result = wait_for_task(task) + self.module.exit_json(changed=changed, result=str(result)) -def state_create_dvs(module): - switch_name = module.params['switch_name'] - datacenter_name = module.params['datacenter_name'] - content = module.params['content'] - mtu = module.params['mtu'] - uplink_quantity = module.params['uplink_quantity'] - discovery_proto = module.params['discovery_proto'] - discovery_operation = module.params['discovery_operation'] + def state_update_dvs(self): + self.module.exit_json(changed=False, msg="Currently not implemented.") - changed = True - result = None + def state_create_dvs(self): + changed = True + result = None - if not module.check_mode: - dc = find_datacenter_by_name(content, datacenter_name) - changed, result = create_dvswitch(dc.networkFolder, switch_name, - mtu, uplink_quantity, discovery_proto, - discovery_operation) - module.exit_json(changed=changed, result=str(result)) + if not self.module.check_mode: + dc = find_datacenter_by_name(self.content, self.datacenter_name) + changed, result = self.create_dvswitch(dc.networkFolder) + self.module.exit_json(changed=changed, result=str(result)) -def check_dvs_configuration(module): - switch_name = module.params['switch_name'] - content = connect_to_api(module) - module.params['content'] = content - dvs = find_dvs_by_name(content, switch_name) - if dvs is None: - return 'absent' - else: - module.params['dvs'] = dvs - return 'present' + def check_dvs_configuration(self): + self.dvs = find_dvs_by_name(self.content, self.switch_name) + if self.dvs is None: + return 'absent' + else: + return 'present' def main(): @@ -184,26 +199,8 @@ def main(): if not HAS_PYVMOMI: module.fail_json(msg='pyvmomi is required for this module') - try: - # Currently state_update_dvs is not implemented. - dvs_states = { - 'absent': { - 'present': state_destroy_dvs, - 'absent': state_exit_unchanged, - }, - 'present': { - 'update': state_update_dvs, - 'present': state_exit_unchanged, - 'absent': state_create_dvs, - } - } - dvs_states[module.params['state']][check_dvs_configuration(module)](module) - except vmodl.RuntimeFault as runtime_fault: - module.fail_json(msg=runtime_fault.msg) - except vmodl.MethodFault as method_fault: - module.fail_json(msg=method_fault.msg) - except Exception as e: - module.fail_json(msg=str(e)) + vmware_dvswitch = VMwareDVSwitch(module) + vmware_dvswitch.process_state() from ansible.module_utils.vmware import * from ansible.module_utils.basic import * From 56c1ce3df19dbb82cec3f6635dcc2ada05afd8ff Mon Sep 17 00:00:00 2001 From: Joseph Callen Date: Fri, 5 Feb 2016 14:31:20 -0500 Subject: [PATCH 1172/2522] Resolves issue with vmware_dvs_portgroup module for v2.0 When this module was written back in May 2015 we were using 1.9.x. Being lazy I added to param the objects that the other functions would need. What I have noticed is in 2.0 exit_json is trying to jsonify those complex objects and failing. This PR resolves that issue with the vmware_dvs_portgroup module. @kamsz reported this issue in https://github.com/ansible/ansible-modules-extras/pull/1568 Playbook ``` - name: Create Management portgroup local_action: module: vmware_dvs_portgroup hostname: "{{ mgmt_ip_address }}" username: "{{ vcsa_user }}" password: "{{ vcsa_pass }}" portgroup_name: Management switch_name: dvSwitch vlan_id: "{{ hostvars[groups['foundation_esxi'][0]].mgmt_vlan_id }}" num_ports: 120 portgroup_type: earlyBinding state: present ``` Module Testing ``` TASK [Create Management portgroup] ********************************************* task path: /opt/autodeploy/projects/emmet/tasks/deploy/dvs_network.yml:17 ESTABLISH LOCAL CONNECTION FOR USER: root localhost EXEC ( umask 22 && mkdir -p "$( echo $HOME/.ansible/tmp/ansible-tmp-1454693809.13-142252676354410 )" && echo "$( echo $HOME/.ansible/tmp/ansible-tmp-1454693809.13-142252676354410 )" ) localhost PUT /tmp/tmpeQ8M1U TO /root/.ansible/tmp/ansible-tmp-1454693809.13-142252676354410/vmware_dvs_portgroup localhost EXEC LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /root/.ansible/tmp/ansible-tmp-1454693809.13-142252676354410/vmware_dvs_portgroup; rm -rf "/root/.ansible/tmp/ansible-tmp-1454693809.13-142252676354410/" > /dev/null 2>&1 changed: [foundation-vcsa -> localhost] => {"changed": true, "invocation": {"module_args": {"hostname": "172.27.0.100", "num_ports": 120, "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", "portgroup_name": "Management", "portgroup_type": "earlyBinding", "state": "present", "switch_name": "dvSwitch", "username": "root", "vlan_id": 2700}, "module_name": "vmware_dvs_portgroup"}, "result": "None"} ``` --- cloud/vmware/vmware_dvs_portgroup.py | 174 +++++++++++++-------------- 1 file changed, 83 insertions(+), 91 deletions(-) diff --git a/cloud/vmware/vmware_dvs_portgroup.py b/cloud/vmware/vmware_dvs_portgroup.py index 8df629e2400..7c686ba3bb0 100644 --- a/cloud/vmware/vmware_dvs_portgroup.py +++ b/cloud/vmware/vmware_dvs_portgroup.py @@ -81,91 +81,100 @@ HAS_PYVMOMI = False -def create_port_group(dv_switch, portgroup_name, vlan_id, num_ports, portgroup_type): - config = vim.dvs.DistributedVirtualPortgroup.ConfigSpec() - - config.name = portgroup_name - config.numPorts = num_ports - - # vim.VMwareDVSPortSetting() does not exist in the pyvmomi documentation - # but this is the correct managed object type. - - config.defaultPortConfig = vim.VMwareDVSPortSetting() - - # vim.VmwareDistributedVirtualSwitchVlanIdSpec() does not exist in the - # pyvmomi documentation but this is the correct managed object type - config.defaultPortConfig.vlan = vim.VmwareDistributedVirtualSwitchVlanIdSpec() - config.defaultPortConfig.vlan.inherited = False - config.defaultPortConfig.vlan.vlanId = vlan_id - config.type = portgroup_type - - spec = [config] - task = dv_switch.AddDVPortgroup_Task(spec) - changed, result = wait_for_task(task) - return changed, result - - -def state_destroy_dvspg(module): - dvs_portgroup = module.params['dvs_portgroup'] - changed = True - result = None - - if not module.check_mode: - task = dvs_portgroup.Destroy_Task() +class VMwareDvsPortgroup(object): + def __init__(self, module): + self.module = module + self.dvs_portgroup = None + self.switch_name = self.module.params['switch_name'] + self.portgroup_name = self.module.params['portgroup_name'] + self.vlan_id = self.module.params['vlan_id'] + self.num_ports = self.module.params['num_ports'] + self.portgroup_type = self.module.params['portgroup_type'] + self.dv_switch = None + self.state = self.module.params['state'] + self.content = connect_to_api(module) + + def process_state(self): + try: + dvspg_states = { + 'absent': { + 'present': self.state_destroy_dvspg, + 'absent': self.state_exit_unchanged, + }, + 'present': { + 'update': self.state_update_dvspg, + 'present': self.state_exit_unchanged, + 'absent': self.state_create_dvspg, + } + } + dvspg_states[self.state][self.check_dvspg_state()]() + except vmodl.RuntimeFault as runtime_fault: + self.module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + self.module.fail_json(msg=method_fault.msg) + except Exception as e: + self.module.fail_json(msg=str(e)) + + def create_port_group(self): + config = vim.dvs.DistributedVirtualPortgroup.ConfigSpec() + + config.name = self.portgroup_name + config.numPorts = self.num_ports + + # vim.VMwareDVSPortSetting() does not exist in the pyvmomi documentation + # but this is the correct managed object type. + + config.defaultPortConfig = vim.VMwareDVSPortSetting() + + # vim.VmwareDistributedVirtualSwitchVlanIdSpec() does not exist in the + # pyvmomi documentation but this is the correct managed object type + config.defaultPortConfig.vlan = vim.VmwareDistributedVirtualSwitchVlanIdSpec() + config.defaultPortConfig.vlan.inherited = False + config.defaultPortConfig.vlan.vlanId = self.vlan_id + config.type = self.portgroup_type + + spec = [config] + task = self.dv_switch.AddDVPortgroup_Task(spec) changed, result = wait_for_task(task) - module.exit_json(changed=changed, result=str(result)) + return changed, result + def state_destroy_dvspg(self): + changed = True + result = None -def state_exit_unchanged(module): - module.exit_json(changed=False) + if not self.module.check_mode: + task = dvs_portgroup.Destroy_Task() + changed, result = wait_for_task(task) + self.module.exit_json(changed=changed, result=str(result)) + def state_exit_unchanged(self): + self.module.exit_json(changed=False) -def state_update_dvspg(module): - module.exit_json(changed=False, msg="Currently not implemented.") - return + def state_update_dvspg(self): + self.module.exit_json(changed=False, msg="Currently not implemented.") + def state_create_dvspg(self): + changed = True + result = None -def state_create_dvspg(module): + if not self.module.check_mode: + changed, result = self.create_port_group() + self.module.exit_json(changed=changed, result=str(result)) - switch_name = module.params['switch_name'] - portgroup_name = module.params['portgroup_name'] - dv_switch = module.params['dv_switch'] - vlan_id = module.params['vlan_id'] - num_ports = module.params['num_ports'] - portgroup_type = module.params['portgroup_type'] - changed = True - result = None + def check_dvspg_state(self): + self.dv_switch = find_dvs_by_name(self.content, self.switch_name) - if not module.check_mode: - changed, result = create_port_group(dv_switch, portgroup_name, vlan_id, num_ports, portgroup_type) - module.exit_json(changed=changed, result=str(result)) + if self.dv_switch is None: + raise Exception("A distributed virtual switch with name %s does not exist" % self.switch_name) + self.dvs_portgroup = find_dvspg_by_name(self.dv_switch, self.portgroup_name) - -def check_dvspg_state(module): - - switch_name = module.params['switch_name'] - portgroup_name = module.params['portgroup_name'] - - content = connect_to_api(module) - module.params['content'] = content - - dv_switch = find_dvs_by_name(content, switch_name) - - if dv_switch is None: - raise Exception("A distributed virtual switch with name %s does not exist" % switch_name) - - module.params['dv_switch'] = dv_switch - dvs_portgroup = find_dvspg_by_name(dv_switch, portgroup_name) - - if dvs_portgroup is None: - return 'absent' - else: - module.params['dvs_portgroup'] = dvs_portgroup - return 'present' + if self.dvs_portgroup is None: + return 'absent' + else: + return 'present' def main(): - argument_spec = vmware_argument_spec() argument_spec.update(dict(portgroup_name=dict(required=True, type='str'), switch_name=dict(required=True, type='str'), @@ -179,25 +188,8 @@ def main(): if not HAS_PYVMOMI: module.fail_json(msg='pyvmomi is required for this module') - try: - dvspg_states = { - 'absent': { - 'present': state_destroy_dvspg, - 'absent': state_exit_unchanged, - }, - 'present': { - 'update': state_update_dvspg, - 'present': state_exit_unchanged, - 'absent': state_create_dvspg, - } - } - dvspg_states[module.params['state']][check_dvspg_state(module)](module) - except vmodl.RuntimeFault as runtime_fault: - module.fail_json(msg=runtime_fault.msg) - except vmodl.MethodFault as method_fault: - module.fail_json(msg=method_fault.msg) - except Exception as e: - module.fail_json(msg=str(e)) + vmware_dvs_portgroup = VMwareDvsPortgroup(module) + vmware_dvs_portgroup.process_state() from ansible.module_utils.vmware import * from ansible.module_utils.basic import * From 56559ebc3569c52dbf029688fbb1d1f46314298a Mon Sep 17 00:00:00 2001 From: Joseph Callen Date: Fri, 5 Feb 2016 14:39:55 -0500 Subject: [PATCH 1173/2522] Resolves issue with vmware_dvs_host module for v2.0 When this module was written back in May 2015 we were using 1.9.x. Being lazy I added to param the objects that the other functions would need. What I have noticed is in 2.0 exit_json is trying to jsonify those complex objects and failing. This PR resolves that issue with the vmware_dvs_host module. @kamsz reported this issue in https://github.com/ansible/ansible-modules-extras/pull/1568 Playbook ``` - name: Add Host to dVS local_action: module: vmware_dvs_host hostname: "{{ mgmt_ip_address }}" username: "{{ vcsa_user }}" password: "{{ vcsa_pass }}" esxi_hostname: "{{ hostvars[item].hostname }}" switch_name: dvSwitch vmnics: "{{ dvs_vmnic }}" state: present with_items: groups['foundation_esxi'] ``` Module Testing ``` TASK [Add Host to dVS] ********************************************************* task path: /opt/autodeploy/projects/emmet/site_deploy.yml:234 ESTABLISH LOCAL CONNECTION FOR USER: root localhost EXEC ( umask 22 && mkdir -p "$( echo $HOME/.ansible/tmp/ansible-tmp-1454694039.6-259977654985844 )" && echo "$( echo $HOME/.ansible/tmp/ansible-tmp-1454694039.6-259977654985844 )" ) localhost PUT /tmp/tmpGrHqbd TO /root/.ansible/tmp/ansible-tmp-1454694039.6-259977654985844/vmware_dvs_host localhost EXEC LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /root/.ansible/tmp/ansible-tmp-1454694039.6-259977654985844/vmware_dvs_host; rm -rf "/root/.ansible/tmp/ansible-tmp-1454694039.6-259977654985844/" > /dev/null 2>&1 localhost EXEC ( umask 22 && mkdir -p "$( echo $HOME/.ansible/tmp/ansible-tmp-1454694058.76-121920794239796 )" && echo "$( echo $HOME/.ansible/tmp/ansible-tmp-1454694058.76-121920794239796 )" ) localhost PUT /tmp/tmpkP7DPu TO /root/.ansible/tmp/ansible-tmp-1454694058.76-121920794239796/vmware_dvs_host localhost EXEC LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /root/.ansible/tmp/ansible-tmp-1454694058.76-121920794239796/vmware_dvs_host; rm -rf "/root/.ansible/tmp/ansible-tmp-1454694058.76-121920794239796/" > /dev/null 2>&1 localhost EXEC ( umask 22 && mkdir -p "$( echo $HOME/.ansible/tmp/ansible-tmp-1454694090.2-33641188152663 )" && echo "$( echo $HOME/.ansible/tmp/ansible-tmp-1454694090.2-33641188152663 )" ) localhost PUT /tmp/tmp216NwV TO /root/.ansible/tmp/ansible-tmp-1454694090.2-33641188152663/vmware_dvs_host localhost EXEC LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /root/.ansible/tmp/ansible-tmp-1454694090.2-33641188152663/vmware_dvs_host; rm -rf "/root/.ansible/tmp/ansible-tmp-1454694090.2-33641188152663/" > /dev/null 2>&1 changed: [foundation-vcsa -> localhost] => (item=foundation-esxi-01) => {"changed": true, "invocation": {"module_args": {"esxi_hostname": "cscesxtmp001", "hostname": "172.27.0.100", "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", "state": "present", "switch_name": "dvSwitch", "username": "root", "vmnics": ["vmnic2"]}, "module_name": "vmware_dvs_host"}, "item": "foundation-esxi-01", "result": "None"} changed: [foundation-vcsa -> localhost] => (item=foundation-esxi-02) => {"changed": true, "invocation": {"module_args": {"esxi_hostname": "cscesxtmp002", "hostname": "172.27.0.100", "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", "state": "present", "switch_name": "dvSwitch", "username": "root", "vmnics": ["vmnic2"]}, "module_name": "vmware_dvs_host"}, "item": "foundation-esxi-02", "result": "None"} changed: [foundation-vcsa -> localhost] => (item=foundation-esxi-03) => {"changed": true, "invocation": {"module_args": {"esxi_hostname": "cscesxtmp003", "hostname": "172.27.0.100", "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", "state": "present", "switch_name": "dvSwitch", "username": "root", "vmnics": ["vmnic2"]}, "module_name": "vmware_dvs_host"}, "item": "foundation-esxi-03", "result": "None"} ``` --- cloud/vmware/vmware_dvs_host.py | 285 +++++++++++++++----------------- 1 file changed, 133 insertions(+), 152 deletions(-) diff --git a/cloud/vmware/vmware_dvs_host.py b/cloud/vmware/vmware_dvs_host.py index 9edab7916bc..dcfb4ba7f58 100644 --- a/cloud/vmware/vmware_dvs_host.py +++ b/cloud/vmware/vmware_dvs_host.py @@ -76,154 +76,154 @@ HAS_PYVMOMI = False -def find_dvspg_by_name(dv_switch, portgroup_name): - portgroups = dv_switch.portgroup - - for pg in portgroups: - if pg.name == portgroup_name: - return pg - - return None +class VMwareDvsHost(object): + def __init__(self, module): + self.module = module + self.dv_switch = None + self.uplink_portgroup = None + self.host = None + self.dv_switch = None + self.nic = None + self.content = connect_to_api(self.module) + self.state = self.module.params['state'] + self.switch_name = self.module.params['switch_name'] + self.esxi_hostname = self.module.params['esxi_hostname'] + self.vmnics = self.module.params['vmnics'] + + def process_state(self): + try: + dvs_host_states = { + 'absent': { + 'present': self.state_destroy_dvs_host, + 'absent': self.state_exit_unchanged, + }, + 'present': { + 'update': self.state_update_dvs_host, + 'present': self.state_exit_unchanged, + 'absent': self.state_create_dvs_host, + } + } + dvs_host_states[self.state][self.check_dvs_host_state()]() + except vmodl.RuntimeFault as runtime_fault: + self.module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + self.module.fail_json(msg=method_fault.msg) + except Exception as e: + self.module.fail_json(msg=str(e)) -def find_dvs_uplink_pg(dv_switch): - # There should only always be a single uplink port group on - # a distributed virtual switch + def find_dvspg_by_name(self): + portgroups = self.dv_switch.portgroup - if len(dv_switch.config.uplinkPortgroup): - return dv_switch.config.uplinkPortgroup[0] - else: + for pg in portgroups: + if pg.name == self.portgroup_name: + return pg return None + def find_dvs_uplink_pg(self): + # There should only always be a single uplink port group on + # a distributed virtual switch -# operation should be edit, add and remove -def modify_dvs_host(dv_switch, host, operation, uplink_portgroup=None, vmnics=None): - - spec = vim.DistributedVirtualSwitch.ConfigSpec() - - spec.configVersion = dv_switch.config.configVersion - spec.host = [vim.dvs.HostMember.ConfigSpec()] - spec.host[0].operation = operation - spec.host[0].host = host - - if operation in ("edit", "add"): - spec.host[0].backing = vim.dvs.HostMember.PnicBacking() - count = 0 - - for nic in vmnics: - spec.host[0].backing.pnicSpec.append(vim.dvs.HostMember.PnicSpec()) - spec.host[0].backing.pnicSpec[count].pnicDevice = nic - spec.host[0].backing.pnicSpec[count].uplinkPortgroupKey = uplink_portgroup.key - count += 1 - - task = dv_switch.ReconfigureDvs_Task(spec) - changed, result = wait_for_task(task) - return changed, result - - -def state_destroy_dvs_host(module): - - operation = "remove" - host = module.params['host'] - dv_switch = module.params['dv_switch'] - - changed = True - result = None - - if not module.check_mode: - changed, result = modify_dvs_host(dv_switch, host, operation) - module.exit_json(changed=changed, result=str(result)) - - -def state_exit_unchanged(module): - module.exit_json(changed=False) - - -def state_update_dvs_host(module): - dv_switch = module.params['dv_switch'] - uplink_portgroup = module.params['uplink_portgroup'] - vmnics = module.params['vmnics'] - host = module.params['host'] - operation = "edit" - changed = True - result = None - - if not module.check_mode: - changed, result = modify_dvs_host(dv_switch, host, operation, uplink_portgroup, vmnics) - module.exit_json(changed=changed, result=str(result)) - - -def state_create_dvs_host(module): - dv_switch = module.params['dv_switch'] - uplink_portgroup = module.params['uplink_portgroup'] - vmnics = module.params['vmnics'] - host = module.params['host'] - operation = "add" - changed = True - result = None - - if not module.check_mode: - changed, result = modify_dvs_host(dv_switch, host, operation, uplink_portgroup, vmnics) - module.exit_json(changed=changed, result=str(result)) - - -def find_host_attached_dvs(esxi_hostname, dv_switch): - for dvs_host_member in dv_switch.config.host: - if dvs_host_member.config.host.name == esxi_hostname: - return dvs_host_member.config.host - - return None - - -def check_uplinks(dv_switch, host, vmnics): - pnic_device = [] - - for dvs_host_member in dv_switch.config.host: - if dvs_host_member.config.host == host: - for pnicSpec in dvs_host_member.config.backing.pnicSpec: - pnic_device.append(pnicSpec.pnicDevice) - - return collections.Counter(pnic_device) == collections.Counter(vmnics) - + if len(self.dv_switch.config.uplinkPortgroup): + return self.dv_switch.config.uplinkPortgroup[0] + else: + return None + + # operation should be edit, add and remove + def modify_dvs_host(self, operation): + spec = vim.DistributedVirtualSwitch.ConfigSpec() + spec.configVersion = self.dv_switch.config.configVersion + spec.host = [vim.dvs.HostMember.ConfigSpec()] + spec.host[0].operation = operation + spec.host[0].host = self.host + + if operation in ("edit", "add"): + spec.host[0].backing = vim.dvs.HostMember.PnicBacking() + count = 0 + + for nic in self.vmnics: + spec.host[0].backing.pnicSpec.append(vim.dvs.HostMember.PnicSpec()) + spec.host[0].backing.pnicSpec[count].pnicDevice = nic + spec.host[0].backing.pnicSpec[count].uplinkPortgroupKey = self.uplink_portgroup.key + count += 1 + + task = self.dv_switch.ReconfigureDvs_Task(spec) + changed, result = wait_for_task(task) + return changed, result + + def state_destroy_dvs_host(self): + operation = "remove" + changed = True + result = None + + if not self.module.check_mode: + changed, result = self.modify_dvs_host(operation) + self.module.exit_json(changed=changed, result=str(result)) + + def state_exit_unchanged(self): + self.module.exit_json(changed=False) + + def state_update_dvs_host(self): + operation = "edit" + changed = True + result = None + + if not self.module.check_mode: + changed, result = self.modify_dvs_host(operation) + self.module.exit_json(changed=changed, result=str(result)) + + def state_create_dvs_host(self): + operation = "add" + changed = True + result = None + + if not self.module.check_mode: + changed, result = self.modify_dvs_host(operation) + self.module.exit_json(changed=changed, result=str(result)) + + def find_host_attached_dvs(self): + for dvs_host_member in self.dv_switch.config.host: + if dvs_host_member.config.host.name == self.esxi_hostname: + return dvs_host_member.config.host -def check_dvs_host_state(module): + return None - switch_name = module.params['switch_name'] - esxi_hostname = module.params['esxi_hostname'] - vmnics = module.params['vmnics'] + def check_uplinks(self): + pnic_device = [] - content = connect_to_api(module) - module.params['content'] = content + for dvs_host_member in self.dv_switch.config.host: + if dvs_host_member.config.host == self.host: + for pnicSpec in dvs_host_member.config.backing.pnicSpec: + pnic_device.append(pnicSpec.pnicDevice) - dv_switch = find_dvs_by_name(content, switch_name) + return collections.Counter(pnic_device) == collections.Counter(self.vmnics) - if dv_switch is None: - raise Exception("A distributed virtual switch %s does not exist" % switch_name) + def check_dvs_host_state(self): + self.dv_switch = find_dvs_by_name(self.content, self.switch_name) - uplink_portgroup = find_dvs_uplink_pg(dv_switch) + if self.dv_switch is None: + raise Exception("A distributed virtual switch %s does not exist" % self.switch_name) - if uplink_portgroup is None: - raise Exception("An uplink portgroup does not exist on the distributed virtual switch %s" % switch_name) + self.uplink_portgroup = self.find_dvs_uplink_pg() - module.params['dv_switch'] = dv_switch - module.params['uplink_portgroup'] = uplink_portgroup + if self.uplink_portgroup is None: + raise Exception("An uplink portgroup does not exist on the distributed virtual switch %s" + % self.switch_name) - host = find_host_attached_dvs(esxi_hostname, dv_switch) + self.host = self.find_host_attached_dvs() - if host is None: - # We still need the HostSystem object to add the host - # to the distributed vswitch - host = find_hostsystem_by_name(content, esxi_hostname) - if host is None: - module.fail_json(msg="The esxi_hostname %s does not exist in vCenter" % esxi_hostname) - module.params['host'] = host - return 'absent' - else: - module.params['host'] = host - if check_uplinks(dv_switch, host, vmnics): - return 'present' + if self.host is None: + # We still need the HostSystem object to add the host + # to the distributed vswitch + self.host = find_hostsystem_by_name(self.content, self.esxi_hostname) + if self.host is None: + self.module.fail_json(msg="The esxi_hostname %s does not exist in vCenter" % self.esxi_hostname) + return 'absent' else: - return 'update' + if self.check_uplinks(): + return 'present' + else: + return 'update' def main(): @@ -239,27 +239,8 @@ def main(): if not HAS_PYVMOMI: module.fail_json(msg='pyvmomi is required for this module') - try: - - dvs_host_states = { - 'absent': { - 'present': state_destroy_dvs_host, - 'absent': state_exit_unchanged, - }, - 'present': { - 'update': state_update_dvs_host, - 'present': state_exit_unchanged, - 'absent': state_create_dvs_host, - } - } - - dvs_host_states[module.params['state']][check_dvs_host_state(module)](module) - except vmodl.RuntimeFault as runtime_fault: - module.fail_json(msg=runtime_fault.msg) - except vmodl.MethodFault as method_fault: - module.fail_json(msg=method_fault.msg) - except Exception as e: - module.fail_json(msg=str(e)) + vmware_dvs_host = VMwareDvsHost(module) + vmware_dvs_host.process_state() from ansible.module_utils.vmware import * from ansible.module_utils.basic import * From 49cfd24ad81ce3e67ed751511791c597af8ea495 Mon Sep 17 00:00:00 2001 From: Joseph Callen Date: Fri, 5 Feb 2016 14:48:12 -0500 Subject: [PATCH 1174/2522] Resolves issue with vmware_host module for v2.0 When this module was written back in May 2015 we were using 1.9.x. Being lazy I added to param the objects that the other functions would need. What I have noticed is in 2.0 exit_json is trying to jsonify those complex objects and failing. This PR resolves that issue with the vmware_host module. @kamsz reported this issue in https://github.com/ansible/ansible-modules-extras/pull/1568 Playbook ``` - name: Add Host local_action: module: vmware_host hostname: "{{ mgmt_ip_address }}" username: "{{ vcsa_user }}" password: "{{ vcsa_pass }}" datacenter_name: "{{ mgmt_vdc }}" cluster_name: "{{ mgmt_cluster }}" esxi_hostname: "{{ hostvars[item].hostname }}" esxi_username: "{{ esxi_username }}" esxi_password: "{{ site_passwd }}" state: present with_items: groups['foundation_esxi'] ``` Module Testing ``` TASK [Add Host] **************************************************************** task path: /opt/autodeploy/projects/emmet/site_deploy.yml:214 ESTABLISH LOCAL CONNECTION FOR USER: root localhost EXEC ( umask 22 && mkdir -p "$( echo $HOME/.ansible/tmp/ansible-tmp-1454693866.1-87710459703937 )" && echo "$( echo $HOME/.ansible/tmp/ansible-tmp-1454693866.1-87710459703937 )" ) localhost PUT /tmp/tmppmr9i9 TO /root/.ansible/tmp/ansible-tmp-1454693866.1-87710459703937/vmware_host localhost EXEC LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /root/.ansible/tmp/ansible-tmp-1454693866.1-87710459703937/vmware_host; rm -rf "/root/.ansible/tmp/ansible-tmp-1454693866.1-87710459703937/" > /dev/null 2>&1 localhost EXEC ( umask 22 && mkdir -p "$( echo $HOME/.ansible/tmp/ansible-tmp-1454693943.8-75870536677834 )" && echo "$( echo $HOME/.ansible/tmp/ansible-tmp-1454693943.8-75870536677834 )" ) localhost PUT /tmp/tmpVB81f2 TO /root/.ansible/tmp/ansible-tmp-1454693943.8-75870536677834/vmware_host localhost EXEC LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /root/.ansible/tmp/ansible-tmp-1454693943.8-75870536677834/vmware_host; rm -rf "/root/.ansible/tmp/ansible-tmp-1454693943.8-75870536677834/" > /dev/null 2>&1 localhost EXEC ( umask 22 && mkdir -p "$( echo $HOME/.ansible/tmp/ansible-tmp-1454693991.56-163414752982563 )" && echo "$( echo $HOME/.ansible/tmp/ansible-tmp-1454693991.56-163414752982563 )" ) localhost PUT /tmp/tmpFB7VQB TO /root/.ansible/tmp/ansible-tmp-1454693991.56-163414752982563/vmware_host localhost EXEC LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /root/.ansible/tmp/ansible-tmp-1454693991.56-163414752982563/vmware_host; rm -rf "/root/.ansible/tmp/ansible-tmp-1454693991.56-163414752982563/" > /dev/null 2>&1 changed: [foundation-vcsa -> localhost] => (item=foundation-esxi-01) => {"changed": true, "invocation": {"module_args": {"cluster_name": "Foundation", "datacenter_name": "Test-Lab", "esxi_hostname": "cscesxtmp001", "esxi_password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", "esxi_username": "root", "hostname": "172.27.0.100", "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", "state": "present", "username": "root"}, "module_name": "vmware_host"}, "item": "foundation-esxi-01", "result": "'vim.HostSystem:host-15'"} changed: [foundation-vcsa -> localhost] => (item=foundation-esxi-02) => {"changed": true, "invocation": {"module_args": {"cluster_name": "Foundation", "datacenter_name": "Test-Lab", "esxi_hostname": "cscesxtmp002", "esxi_password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", "esxi_username": "root", "hostname": "172.27.0.100", "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", "state": "present", "username": "root"}, "module_name": "vmware_host"}, "item": "foundation-esxi-02", "result": "'vim.HostSystem:host-20'"} changed: [foundation-vcsa -> localhost] => (item=foundation-esxi-03) => {"changed": true, "invocation": {"module_args": {"cluster_name": "Foundation", "datacenter_name": "Test-Lab", "esxi_hostname": "cscesxtmp003", "esxi_password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", "esxi_username": "root", "hostname": "172.27.0.100", "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", "state": "present", "username": "root"}, "module_name": "vmware_host"}, "item": "foundation-esxi-03", "result": "'vim.HostSystem:host-21'"} ``` --- cloud/vmware/vmware_host.py | 209 ++++++++++++++++++------------------ 1 file changed, 103 insertions(+), 106 deletions(-) diff --git a/cloud/vmware/vmware_host.py b/cloud/vmware/vmware_host.py index dba7ce9a11e..dd8e2f9eed4 100644 --- a/cloud/vmware/vmware_host.py +++ b/cloud/vmware/vmware_host.py @@ -87,102 +87,118 @@ HAS_PYVMOMI = False -def find_host_by_cluster_datacenter(module): - datacenter_name = module.params['datacenter_name'] - cluster_name = module.params['cluster_name'] - content = module.params['content'] - esxi_hostname = module.params['esxi_hostname'] - - dc = find_datacenter_by_name(content, datacenter_name) - cluster = find_cluster_by_name_datacenter(dc, cluster_name) - - for host in cluster.host: - if host.name == esxi_hostname: - return host, cluster - - return None, cluster - - -def add_host_to_vcenter(module): - cluster = module.params['cluster'] - - host_connect_spec = vim.host.ConnectSpec() - host_connect_spec.hostName = module.params['esxi_hostname'] - host_connect_spec.userName = module.params['esxi_username'] - host_connect_spec.password = module.params['esxi_password'] - host_connect_spec.force = True - host_connect_spec.sslThumbprint = "" - as_connected = True - esxi_license = None - resource_pool = None +class VMwareHost(object): + def __init__(self, module): + self.module = module + self.datacenter_name = module.params['datacenter_name'] + self.cluster_name = module.params['cluster_name'] + self.esxi_hostname = module.params['esxi_hostname'] + self.esxi_username = module.params['esxi_username'] + self.esxi_password = module.params['esxi_password'] + self.state = module.params['state'] + self.dc = None + self.cluster = None + self.host = None + self.content = connect_to_api(module) + + def process_state(self): + try: + # Currently state_update_dvs is not implemented. + host_states = { + 'absent': { + 'present': self.state_remove_host, + 'absent': self.state_exit_unchanged, + }, + 'present': { + 'present': self.state_exit_unchanged, + 'absent': self.state_add_host, + } + } - try: - task = cluster.AddHost_Task(host_connect_spec, as_connected, resource_pool, esxi_license) + host_states[self.state][self.check_host_state()]() + + except vmodl.RuntimeFault as runtime_fault: + self.module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + self.module.fail_json(msg=method_fault.msg) + except Exception as e: + self.module.fail_json(msg=str(e)) + + def find_host_by_cluster_datacenter(self): + self.dc = find_datacenter_by_name(self.content, self.datacenter_name) + self.cluster = find_cluster_by_name_datacenter(self.dc, self.cluster_name) + + for host in self.cluster.host: + if host.name == self.esxi_hostname: + return host, self.cluster + + return None, self.cluster + + def add_host_to_vcenter(self): + host_connect_spec = vim.host.ConnectSpec() + host_connect_spec.hostName = self.esxi_hostname + host_connect_spec.userName = self.esxi_username + host_connect_spec.password = self.esxi_password + host_connect_spec.force = True + host_connect_spec.sslThumbprint = "" + as_connected = True + esxi_license = None + resource_pool = None + + try: + task = self.cluster.AddHost_Task(host_connect_spec, as_connected, resource_pool, esxi_license) + success, result = wait_for_task(task) + return success, result + except TaskError as add_task_error: + # This is almost certain to fail the first time. + # In order to get the sslThumbprint we first connect + # get the vim.fault.SSLVerifyFault then grab the sslThumbprint + # from that object. + # + # args is a tuple, selecting the first tuple + ssl_verify_fault = add_task_error.args[0] + host_connect_spec.sslThumbprint = ssl_verify_fault.thumbprint + + task = self.cluster.AddHost_Task(host_connect_spec, as_connected, resource_pool, esxi_license) success, result = wait_for_task(task) return success, result - except TaskError as add_task_error: - # This is almost certain to fail the first time. - # In order to get the sslThumbprint we first connect - # get the vim.fault.SSLVerifyFault then grab the sslThumbprint - # from that object. - # - # args is a tuple, selecting the first tuple - ssl_verify_fault = add_task_error.args[0] - host_connect_spec.sslThumbprint = ssl_verify_fault.thumbprint - - task = cluster.AddHost_Task(host_connect_spec, as_connected, resource_pool, esxi_license) - success, result = wait_for_task(task) - return success, result - - -def state_exit_unchanged(module): - module.exit_json(changed=False) - - -def state_remove_host(module): - host = module.params['host'] - changed = True - result = None - if not module.check_mode: - if not host.runtime.inMaintenanceMode: - maintenance_mode_task = host.EnterMaintenanceMode_Task(300, True, None) - changed, result = wait_for_task(maintenance_mode_task) - - if changed: - task = host.Destroy_Task() - changed, result = wait_for_task(task) - else: - raise Exception(result) - module.exit_json(changed=changed, result=str(result)) + def state_exit_unchanged(self): + self.module.exit_json(changed=False) -def state_update_host(module): - module.exit_json(changed=False, msg="Currently not implemented.") + def state_remove_host(self): + changed = True + result = None + if not self.module.check_mode: + if not self.host.runtime.inMaintenanceMode: + maintenance_mode_task = self.host.EnterMaintenanceMode_Task(300, True, None) + changed, result = wait_for_task(maintenance_mode_task) + if changed: + task = self.host.Destroy_Task() + changed, result = wait_for_task(task) + else: + raise Exception(result) + self.module.exit_json(changed=changed, result=str(result)) -def state_add_host(module): + def state_update_host(self): + self.module.exit_json(changed=False, msg="Currently not implemented.") - changed = True - result = None + def state_add_host(self): + changed = True + result = None - if not module.check_mode: - changed, result = add_host_to_vcenter(module) - module.exit_json(changed=changed, result=str(result)) + if not self.module.check_mode: + changed, result = self.add_host_to_vcenter() + self.module.exit_json(changed=changed, result=str(result)) + def check_host_state(self): + self.host, self.cluster = self.find_host_by_cluster_datacenter() -def check_host_state(module): - - content = connect_to_api(module) - module.params['content'] = content - - host, cluster = find_host_by_cluster_datacenter(module) - - module.params['cluster'] = cluster - if host is None: - return 'absent' - else: - module.params['host'] = host - return 'present' + if self.host is None: + return 'absent' + else: + return 'present' def main(): @@ -199,27 +215,8 @@ def main(): if not HAS_PYVMOMI: module.fail_json(msg='pyvmomi is required for this module') - try: - # Currently state_update_dvs is not implemented. - host_states = { - 'absent': { - 'present': state_remove_host, - 'absent': state_exit_unchanged, - }, - 'present': { - 'present': state_exit_unchanged, - 'absent': state_add_host, - } - } - - host_states[module.params['state']][check_host_state(module)](module) - - except vmodl.RuntimeFault as runtime_fault: - module.fail_json(msg=runtime_fault.msg) - except vmodl.MethodFault as method_fault: - module.fail_json(msg=method_fault.msg) - except Exception as e: - module.fail_json(msg=str(e)) + vmware_host = VMwareHost(module) + vmware_host.process_state() from ansible.module_utils.vmware import * from ansible.module_utils.basic import * From 07407532554d78a18ce13ec28a8f73bf10d2b09f Mon Sep 17 00:00:00 2001 From: Joseph Callen Date: Fri, 5 Feb 2016 14:53:36 -0500 Subject: [PATCH 1175/2522] Resolves issue with vmware_vm_vss_dvs_migrate module for v2.0 When this module was written back in May 2015 we were using 1.9.x. Being lazy I added to param the objects that the other functions would need. What I have noticed is in 2.0 exit_json is trying to jsonify those complex objects and failing. This PR resolves that issue with the vmware_vm_vss_dvs_migrate module. @kamsz reported this issue in https://github.com/ansible/ansible-modules-extras/pull/1568 Playbook ``` - name: Migrate VCSA to vDS local_action: module: vmware_vm_vss_dvs_migrate hostname: "{{ mgmt_ip_address }}" username: "{{ vcsa_user }}" password: "{{ vcsa_pass }}" vm_name: "{{ hostname }}" dvportgroup_name: Management ``` Module Testing ``` ASK [Migrate VCSA to vDS] ***************************************************** task path: /opt/autodeploy/projects/emmet/site_deploy.yml:260 ESTABLISH LOCAL CONNECTION FOR USER: root localhost EXEC ( umask 22 && mkdir -p "$( echo $HOME/.ansible/tmp/ansible-tmp-1454695546.3-207189190861859 )" && echo "$( echo $HOME/.ansible/tmp/ansible-tmp-1454695546.3-207189190861859 )" ) localhost PUT /tmp/tmpkzD4pF TO /root/.ansible/tmp/ansible-tmp-1454695546.3-207189190861859/vmware_vm_vss_dvs_migrate localhost EXEC LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /root/.ansible/tmp/ansible-tmp-1454695546.3-207189190861859/vmware_vm_vss_dvs_migrate; rm -rf "/root/.ansible/tmp/ansible-tmp-1454695546.3-207189190861859/" > /dev/null 2>&1 changed: [foundation-vcsa -> localhost] => {"changed": true, "invocation": {"module_args": {"dvportgroup_name": "Management", "hostname": "172.27.0.100", "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", "username": "root", "vm_name": "cscvcatmp001"}, "module_name": "vmware_vm_vss_dvs_migrate"}, "result": null} ``` --- cloud/vmware/vmware_vm_vss_dvs_migrate.py | 160 +++++++++++----------- 1 file changed, 77 insertions(+), 83 deletions(-) diff --git a/cloud/vmware/vmware_vm_vss_dvs_migrate.py b/cloud/vmware/vmware_vm_vss_dvs_migrate.py index 8dbf059965c..7799fa8108a 100644 --- a/cloud/vmware/vmware_vm_vss_dvs_migrate.py +++ b/cloud/vmware/vmware_vm_vss_dvs_migrate.py @@ -40,7 +40,6 @@ description: - Name of the portgroup to migrate to the virtual machine to required: True -extends_documentation_fragment: vmware.documentation ''' EXAMPLES = ''' @@ -61,82 +60,81 @@ HAS_PYVMOMI = False -def _find_dvspg_by_name(content, pg_name): - - vmware_distributed_port_group = get_all_objs(content, [vim.dvs.DistributedVirtualPortgroup]) - for dvspg in vmware_distributed_port_group: - if dvspg.name == pg_name: - return dvspg - return None - - -def find_vm_by_name(content, vm_name): - - virtual_machines = get_all_objs(content, [vim.VirtualMachine]) - for vm in virtual_machines: - if vm.name == vm_name: - return vm - return None - - -def migrate_network_adapter_vds(module): - vm_name = module.params['vm_name'] - dvportgroup_name = module.params['dvportgroup_name'] - content = module.params['content'] - - vm_configspec = vim.vm.ConfigSpec() - nic = vim.vm.device.VirtualEthernetCard.DistributedVirtualPortBackingInfo() - port = vim.dvs.PortConnection() - devicespec = vim.vm.device.VirtualDeviceSpec() - - pg = _find_dvspg_by_name(content, dvportgroup_name) - - if pg is None: - module.fail_json(msg="The standard portgroup was not found") - - vm = find_vm_by_name(content, vm_name) - if vm is None: - module.fail_json(msg="The virtual machine was not found") - - dvswitch = pg.config.distributedVirtualSwitch - port.switchUuid = dvswitch.uuid - port.portgroupKey = pg.key - nic.port = port - - for device in vm.config.hardware.device: - if isinstance(device, vim.vm.device.VirtualEthernetCard): - devicespec.device = device - devicespec.operation = vim.vm.device.VirtualDeviceSpec.Operation.edit - devicespec.device.backing = nic - vm_configspec.deviceChange.append(devicespec) - - task = vm.ReconfigVM_Task(vm_configspec) - changed, result = wait_for_task(task) - module.exit_json(changed=changed, result=result) - - -def state_exit_unchanged(module): - module.exit_json(changed=False) - - -def check_vm_network_state(module): - vm_name = module.params['vm_name'] - try: - content = connect_to_api(module) - module.params['content'] = content - vm = find_vm_by_name(content, vm_name) - module.params['vm'] = vm - if vm is None: - module.fail_json(msg="A virtual machine with name %s does not exist" % vm_name) - for device in vm.config.hardware.device: +class VMwareVmVssDvsMigrate(object): + def __init__(self, module): + self.module = module + self.content = connect_to_api(module) + self.vm = None + self.vm_name = module.params['vm_name'] + self.dvportgroup_name = module.params['dvportgroup_name'] + + def process_state(self): + vm_nic_states = { + 'absent': self.migrate_network_adapter_vds, + 'present': self.state_exit_unchanged, + } + + vm_nic_states[self.check_vm_network_state()]() + + def find_dvspg_by_name(self): + vmware_distributed_port_group = get_all_objs(self.content, [vim.dvs.DistributedVirtualPortgroup]) + for dvspg in vmware_distributed_port_group: + if dvspg.name == self.dvportgroup_name: + return dvspg + return None + + def find_vm_by_name(self): + virtual_machines = get_all_objs(self.content, [vim.VirtualMachine]) + for vm in virtual_machines: + if vm.name == self.vm_name: + return vm + return None + + def migrate_network_adapter_vds(self): + vm_configspec = vim.vm.ConfigSpec() + nic = vim.vm.device.VirtualEthernetCard.DistributedVirtualPortBackingInfo() + port = vim.dvs.PortConnection() + devicespec = vim.vm.device.VirtualDeviceSpec() + + pg = self.find_dvspg_by_name() + + if pg is None: + self.module.fail_json(msg="The standard portgroup was not found") + + dvswitch = pg.config.distributedVirtualSwitch + port.switchUuid = dvswitch.uuid + port.portgroupKey = pg.key + nic.port = port + + for device in self.vm.config.hardware.device: if isinstance(device, vim.vm.device.VirtualEthernetCard): - if isinstance(device.backing, vim.vm.device.VirtualEthernetCard.DistributedVirtualPortBackingInfo): - return 'present' - return 'absent' - except vmodl.RuntimeFault as runtime_fault: - module.fail_json(msg=runtime_fault.msg) - except vmodl.MethodFault as method_fault: - module.fail_json(msg=method_fault.msg) + devicespec.device = device + devicespec.operation = vim.vm.device.VirtualDeviceSpec.Operation.edit + devicespec.device.backing = nic + vm_configspec.deviceChange.append(devicespec) + + task = self.vm.ReconfigVM_Task(vm_configspec) + changed, result = wait_for_task(task) + self.module.exit_json(changed=changed, result=result) + + def state_exit_unchanged(self): + self.module.exit_json(changed=False) + + def check_vm_network_state(self): + try: + self.vm = self.find_vm_by_name() + + if self.vm is None: + self.module.fail_json(msg="A virtual machine with name %s does not exist" % self.vm_name) + for device in self.vm.config.hardware.device: + if isinstance(device, vim.vm.device.VirtualEthernetCard): + if isinstance(device.backing, vim.vm.device.VirtualEthernetCard.DistributedVirtualPortBackingInfo): + return 'present' + return 'absent' + except vmodl.RuntimeFault as runtime_fault: + self.module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + self.module.fail_json(msg=method_fault.msg) def main(): @@ -149,15 +147,11 @@ def main(): if not HAS_PYVMOMI: module.fail_json(msg='pyvmomi is required for this module') - vm_nic_states = { - 'absent': migrate_network_adapter_vds, - 'present': state_exit_unchanged, - } - - vm_nic_states[check_vm_network_state(module)](module) + vmware_vmnic_migrate = VMwareVmVssDvsMigrate(module) + vmware_vmnic_migrate.process_state() from ansible.module_utils.vmware import * from ansible.module_utils.basic import * if __name__ == '__main__': - main() + main() \ No newline at end of file From 3ce496c646e5e3c34e802e7fefecad7bc653aba5 Mon Sep 17 00:00:00 2001 From: Joseph Callen Date: Fri, 5 Feb 2016 14:56:39 -0500 Subject: [PATCH 1176/2522] missing doc fragment --- cloud/vmware/vmware_vm_vss_dvs_migrate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud/vmware/vmware_vm_vss_dvs_migrate.py b/cloud/vmware/vmware_vm_vss_dvs_migrate.py index 7799fa8108a..00d98a3200d 100644 --- a/cloud/vmware/vmware_vm_vss_dvs_migrate.py +++ b/cloud/vmware/vmware_vm_vss_dvs_migrate.py @@ -40,6 +40,7 @@ description: - Name of the portgroup to migrate to the virtual machine to required: True +extends_documentation_fragment: vmware.documentation ''' EXAMPLES = ''' From 35a04ff134645ca91caaaee49b4fb591979f3478 Mon Sep 17 00:00:00 2001 From: Joseph Callen Date: Fri, 5 Feb 2016 15:04:04 -0500 Subject: [PATCH 1177/2522] Resolves issue with vmware_migrate_vmk module for v2.0 When this module was written back in May 2015 we were using 1.9.x. Being lazy I added to param the objects that the other functions would need. What I have noticed is in 2.0 exit_json is trying to jsonify those complex objects and failing. This PR resolves that issue with the vmware_migrate_vmk module. @kamsz reported this issue in https://github.com/ansible/ansible-modules-extras/pull/1568 Playbook ``` - name: Migrate Management vmk local_action: module: vmware_migrate_vmk hostname: "{{ mgmt_ip_address }}" username: "{{ vcsa_user }}" password: "{{ vcsa_pass }}" esxi_hostname: "{{ hostvars[item].hostname }}" device: vmk1 current_switch_name: temp_vswitch current_portgroup_name: esx-mgmt migrate_switch_name: dvSwitch migrate_portgroup_name: Management with_items: groups['foundation_esxi'] ``` Module Testing ``` TASK [Migrate Management vmk] ************************************************** task path: /opt/autodeploy/projects/emmet/tasks/deploy/migrate_vmk.yml:3 ESTABLISH LOCAL CONNECTION FOR USER: root localhost EXEC ( umask 22 && mkdir -p "$( echo $HOME/.ansible/tmp/ansible-tmp-1454695485.85-245405603184252 )" && echo "$( echo $HOME/.ansible/tmp/ansible-tmp-1454695485.85-245405603184252 )" ) localhost PUT /tmp/tmpdlhr6t TO /root/.ansible/tmp/ansible-tmp-1454695485.85-245405603184252/vmware_migrate_vmk localhost EXEC LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /root/.ansible/tmp/ansible-tmp-1454695485.85-245405603184252/vmware_migrate_vmk; rm -rf "/root/.ansible/tmp/ansible-tmp-1454695485.85-245405603184252/" > /dev/null 2>&1 localhost EXEC ( umask 22 && mkdir -p "$( echo $HOME/.ansible/tmp/ansible-tmp-1454695490.35-143738865490168 )" && echo "$( echo $HOME/.ansible/tmp/ansible-tmp-1454695490.35-143738865490168 )" ) localhost PUT /tmp/tmpqfZqh1 TO /root/.ansible/tmp/ansible-tmp-1454695490.35-143738865490168/vmware_migrate_vmk localhost EXEC LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /root/.ansible/tmp/ansible-tmp-1454695490.35-143738865490168/vmware_migrate_vmk; rm -rf "/root/.ansible/tmp/ansible-tmp-1454695490.35-143738865490168/" > /dev/null 2>&1 localhost EXEC ( umask 22 && mkdir -p "$( echo $HOME/.ansible/tmp/ansible-tmp-1454695491.96-124154332968882 )" && echo "$( echo $HOME/.ansible/tmp/ansible-tmp-1454695491.96-124154332968882 )" ) localhost PUT /tmp/tmpf3rKZq TO /root/.ansible/tmp/ansible-tmp-1454695491.96-124154332968882/vmware_migrate_vmk localhost EXEC LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /root/.ansible/tmp/ansible-tmp-1454695491.96-124154332968882/vmware_migrate_vmk; rm -rf "/root/.ansible/tmp/ansible-tmp-1454695491.96-124154332968882/" > /dev/null 2>&1 ok: [foundation-vcsa -> localhost] => (item=foundation-esxi-01) => {"changed": false, "invocation": {"module_args": {"current_portgroup_name": "esx-mgmt", "current_switch_name": "temp_vswitch", "device": "vmk1", "esxi_hostname": "cscesxtmp001", "hostname": "172.27.0.100", "migrate_portgroup_name": "Management", "migrate_switch_name": "dvSwitch", "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", "username": "root"}, "module_name": "vmware_migrate_vmk"}, "item": "foundation-esxi-01"} ok: [foundation-vcsa -> localhost] => (item=foundation-esxi-02) => {"changed": false, "invocation": {"module_args": {"current_portgroup_name": "esx-mgmt", "current_switch_name": "temp_vswitch", "device": "vmk1", "esxi_hostname": "cscesxtmp002", "hostname": "172.27.0.100", "migrate_portgroup_name": "Management", "migrate_switch_name": "dvSwitch", "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", "username": "root"}, "module_name": "vmware_migrate_vmk"}, "item": "foundation-esxi-02"} ok: [foundation-vcsa -> localhost] => (item=foundation-esxi-03) => {"changed": false, "invocation": {"module_args": {"current_portgroup_name": "esx-mgmt", "current_switch_name": "temp_vswitch", "device": "vmk1", "esxi_hostname": "cscesxtmp003", "hostname": "172.27.0.100", "migrate_portgroup_name": "Management", "migrate_switch_name": "dvSwitch", "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", "username": "root"}, "module_name": "vmware_migrate_vmk"}, "item": "foundation-esxi-03"} ``` --- cloud/vmware/vmware_migrate_vmk.py | 192 ++++++++++++++--------------- 1 file changed, 91 insertions(+), 101 deletions(-) diff --git a/cloud/vmware/vmware_migrate_vmk.py b/cloud/vmware/vmware_migrate_vmk.py index a3f3db764ca..a18dcc4a883 100644 --- a/cloud/vmware/vmware_migrate_vmk.py +++ b/cloud/vmware/vmware_migrate_vmk.py @@ -75,8 +75,6 @@ migrate_switch_name: dvSwitch migrate_portgroup_name: Management ''' - - try: from pyVmomi import vim, vmodl HAS_PYVMOMI = True @@ -84,88 +82,93 @@ HAS_PYVMOMI = False -def state_exit_unchanged(module): - module.exit_json(changed=False) - - -def state_migrate_vds_vss(module): - module.exit_json(changed=False, msg="Currently Not Implemented") - - -def create_host_vnic_config(dv_switch_uuid, portgroup_key, device): - - host_vnic_config = vim.host.VirtualNic.Config() - host_vnic_config.spec = vim.host.VirtualNic.Specification() - host_vnic_config.changeOperation = "edit" - host_vnic_config.device = device - host_vnic_config.portgroup = "" - host_vnic_config.spec.distributedVirtualPort = vim.dvs.PortConnection() - host_vnic_config.spec.distributedVirtualPort.switchUuid = dv_switch_uuid - host_vnic_config.spec.distributedVirtualPort.portgroupKey = portgroup_key - - return host_vnic_config - - -def create_port_group_config(switch_name, portgroup_name): - port_group_config = vim.host.PortGroup.Config() - port_group_config.spec = vim.host.PortGroup.Specification() - - port_group_config.changeOperation = "remove" - port_group_config.spec.name = portgroup_name - port_group_config.spec.vlanId = -1 - port_group_config.spec.vswitchName = switch_name - port_group_config.spec.policy = vim.host.NetworkPolicy() - - return port_group_config - - -def state_migrate_vss_vds(module): - content = module.params['content'] - host_system = module.params['host_system'] - migrate_switch_name = module.params['migrate_switch_name'] - migrate_portgroup_name = module.params['migrate_portgroup_name'] - current_portgroup_name = module.params['current_portgroup_name'] - current_switch_name = module.params['current_switch_name'] - device = module.params['device'] - - host_network_system = host_system.configManager.networkSystem - - dv_switch = find_dvs_by_name(content, migrate_switch_name) - pg = find_dvspg_by_name(dv_switch, migrate_portgroup_name) - - config = vim.host.NetworkConfig() - config.portgroup = [create_port_group_config(current_switch_name, current_portgroup_name)] - config.vnic = [create_host_vnic_config(dv_switch.uuid, pg.key, device)] - host_network_system.UpdateNetworkConfig(config, "modify") - module.exit_json(changed=True) - - -def check_vmk_current_state(module): - - device = module.params['device'] - esxi_hostname = module.params['esxi_hostname'] - current_portgroup_name = module.params['current_portgroup_name'] - current_switch_name = module.params['current_switch_name'] - - content = connect_to_api(module) - - host_system = find_hostsystem_by_name(content, esxi_hostname) - - module.params['content'] = content - module.params['host_system'] = host_system - - for vnic in host_system.configManager.networkSystem.networkInfo.vnic: - if vnic.device == device: - module.params['vnic'] = vnic - if vnic.spec.distributedVirtualPort is None: - if vnic.portgroup == current_portgroup_name: - return "migrate_vss_vds" - else: - dvs = find_dvs_by_name(content, current_switch_name) - if dvs is None: - return "migrated" - if vnic.spec.distributedVirtualPort.switchUuid == dvs.uuid: - return "migrate_vds_vss" +class VMwareMigrateVmk(object): + def __init__(self, module): + self.module = module + self.host_system = None + self.migrate_switch_name = self.module.params['migrate_switch_name'] + self.migrate_portgroup_name = self.module.params['migrate_portgroup_name'] + self.device = self.module.params['device'] + self.esxi_hostname = self.module.params['esxi_hostname'] + self.current_portgroup_name = self.module.params['current_portgroup_name'] + self.current_switch_name = self.module.params['current_switch_name'] + self.content = connect_to_api(module) + + def process_state(self): + try: + vmk_migration_states = { + 'migrate_vss_vds': self.state_migrate_vss_vds, + 'migrate_vds_vss': self.state_migrate_vds_vss, + 'migrated': self.state_exit_unchanged + } + + vmk_migration_states[self.check_vmk_current_state()]() + + except vmodl.RuntimeFault as runtime_fault: + self.module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + self.module.fail_json(msg=method_fault.msg) + except Exception as e: + self.module.fail_json(msg=str(e)) + + def state_exit_unchanged(self): + self.module.exit_json(changed=False) + + def state_migrate_vds_vss(self): + self.module.exit_json(changed=False, msg="Currently Not Implemented") + + def create_host_vnic_config(self, dv_switch_uuid, portgroup_key): + host_vnic_config = vim.host.VirtualNic.Config() + host_vnic_config.spec = vim.host.VirtualNic.Specification() + + host_vnic_config.changeOperation = "edit" + host_vnic_config.device = self.device + host_vnic_config.portgroup = "" + host_vnic_config.spec.distributedVirtualPort = vim.dvs.PortConnection() + host_vnic_config.spec.distributedVirtualPort.switchUuid = dv_switch_uuid + host_vnic_config.spec.distributedVirtualPort.portgroupKey = portgroup_key + + return host_vnic_config + + def create_port_group_config(self): + port_group_config = vim.host.PortGroup.Config() + port_group_config.spec = vim.host.PortGroup.Specification() + + port_group_config.changeOperation = "remove" + port_group_config.spec.name = self.current_portgroup_name + port_group_config.spec.vlanId = -1 + port_group_config.spec.vswitchName = self.current_switch_name + port_group_config.spec.policy = vim.host.NetworkPolicy() + + return port_group_config + + def state_migrate_vss_vds(self): + host_network_system = self.host_system.configManager.networkSystem + + dv_switch = find_dvs_by_name(self.content, self.migrate_switch_name) + pg = find_dvspg_by_name(dv_switch, self.migrate_portgroup_name) + + config = vim.host.NetworkConfig() + config.portgroup = [self.create_port_group_config()] + config.vnic = [self.create_host_vnic_config(dv_switch.uuid, pg.key)] + host_network_system.UpdateNetworkConfig(config, "modify") + self.module.exit_json(changed=True) + + def check_vmk_current_state(self): + self.host_system = find_hostsystem_by_name(self.content, self.esxi_hostname) + + for vnic in self.host_system.configManager.networkSystem.networkInfo.vnic: + if vnic.device == self.device: + #self.vnic = vnic + if vnic.spec.distributedVirtualPort is None: + if vnic.portgroup == self.current_portgroup_name: + return "migrate_vss_vds" + else: + dvs = find_dvs_by_name(self.content, self.current_switch_name) + if dvs is None: + return "migrated" + if vnic.spec.distributedVirtualPort.switchUuid == dvs.uuid: + return "migrate_vds_vss" def main(): @@ -181,23 +184,10 @@ def main(): module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False) if not HAS_PYVMOMI: - module.fail_json(msg='pyvmomi required for this module') - - try: - vmk_migration_states = { - 'migrate_vss_vds': state_migrate_vss_vds, - 'migrate_vds_vss': state_migrate_vds_vss, - 'migrated': state_exit_unchanged - } - - vmk_migration_states[check_vmk_current_state(module)](module) - - except vmodl.RuntimeFault as runtime_fault: - module.fail_json(msg=runtime_fault.msg) - except vmodl.MethodFault as method_fault: - module.fail_json(msg=method_fault.msg) - except Exception as e: - module.fail_json(msg=str(e)) + self.module.fail_json(msg='pyvmomi required for this module') + + vmware_migrate_vmk = VMwareMigrateVmk(module) + vmware_migrate_vmk.process_state() from ansible.module_utils.vmware import * from ansible.module_utils.basic import * From 14c0e345d6975bf360e7f4ebbc5c6d83386a23e7 Mon Sep 17 00:00:00 2001 From: Casey Lucas Date: Fri, 5 Feb 2016 19:09:54 -0600 Subject: [PATCH 1178/2522] fix edge case where boto returns empty list after subnet creation --- cloud/amazon/ec2_vpc_subnet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_vpc_subnet.py b/cloud/amazon/ec2_vpc_subnet.py index d0cc68e07fa..729357d5020 100644 --- a/cloud/amazon/ec2_vpc_subnet.py +++ b/cloud/amazon/ec2_vpc_subnet.py @@ -122,7 +122,7 @@ def get_subnet_info(subnet): def subnet_exists(vpc_conn, subnet_id): filters = {'subnet-id': subnet_id} subnet = vpc_conn.get_all_subnets(filters=filters) - if subnet[0].state == "available": + if subnet and subnet[0].state == "available": return subnet[0] else: return False From 1898ffc3cc73544e0f66bf021794740e1623ae1c Mon Sep 17 00:00:00 2001 From: lsb Date: Fri, 5 Feb 2016 17:10:44 -0800 Subject: [PATCH 1179/2522] Update mongodb_user.py Typo --- database/misc/mongodb_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index 12d348e9a92..f66f64ef9c7 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -155,7 +155,7 @@ def user_find(client, user): return False def user_add(module, client, db_name, user, password, roles): - #pymono's user_add is a _create_or_update_user so we won't know if it was changed or updated + #pymongo's user_add is a _create_or_update_user so we won't know if it was changed or updated #without reproducing a lot of the logic in database.py of pymongo db = client[db_name] if roles is None: From 0608318f90250d688fa0dce509c4ed356b525161 Mon Sep 17 00:00:00 2001 From: Baptiste Mille-Mathias Date: Mon, 2 Nov 2015 20:57:02 +0100 Subject: [PATCH 1180/2522] remove unicode prefix and correct text in table Remove the unicode prefix displayed before the url pushover.net Attempt to correct the text in the table which appear vertically. --- notification/pushover.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/notification/pushover.py b/notification/pushover.py index 24d343a6809..794fd4139c4 100644 --- a/notification/pushover.py +++ b/notification/pushover.py @@ -24,7 +24,7 @@ --- module: pushover version_added: "2.0" -short_description: Send notifications via u(https://pushover.net) +short_description: Send notifications via https://pushover.net description: - Send notifications via pushover, to subscriber list of devices, and email addresses. Requires pushover app on devices. @@ -34,18 +34,18 @@ options: msg: description: - What message you wish to send. + - What message you wish to send. required: true app_token: description: - Pushover issued token identifying your pushover app. + - Pushover issued token identifying your pushover app. required: true user_key: description: - Pushover issued authentication key for your user. + - Pushover issued authentication key for your user. required: true pri: - description: Message priority (see u(https://pushover.net) for details.) + description: Message priority (see https://pushover.net for details.) required: false author: "Jim Richardson (@weaselkeeper)" From d56ac42c8b234bf1a8892f2ab770fdc7f5e2edbc Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Sun, 7 Feb 2016 20:20:32 -0800 Subject: [PATCH 1181/2522] U() markings in pushover docs should remain but need to be uppercased. pri description needs to be a list. --- notification/pushover.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/notification/pushover.py b/notification/pushover.py index 794fd4139c4..29afcaa6356 100644 --- a/notification/pushover.py +++ b/notification/pushover.py @@ -24,7 +24,7 @@ --- module: pushover version_added: "2.0" -short_description: Send notifications via https://pushover.net +short_description: Send notifications via U(https://pushover.net) description: - Send notifications via pushover, to subscriber list of devices, and email addresses. Requires pushover app on devices. @@ -45,7 +45,8 @@ - Pushover issued authentication key for your user. required: true pri: - description: Message priority (see https://pushover.net for details.) + description: + - Message priority (see U(https://pushover.net) for details.) required: false author: "Jim Richardson (@weaselkeeper)" From 198c816b7078e4dbdd9312e603c99f080b63d6ba Mon Sep 17 00:00:00 2001 From: Michael Baydoun Date: Mon, 8 Feb 2016 10:09:23 -0500 Subject: [PATCH 1182/2522] Update ec2_vpc_net_facts.py Corrected version_added --- cloud/amazon/ec2_vpc_net_facts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_vpc_net_facts.py b/cloud/amazon/ec2_vpc_net_facts.py index 98c7f742a73..8de47ed9758 100644 --- a/cloud/amazon/ec2_vpc_net_facts.py +++ b/cloud/amazon/ec2_vpc_net_facts.py @@ -19,7 +19,7 @@ short_description: Gather facts about ec2 VPCs in AWS description: - Gather facts about ec2 VPCs in AWS -version_added: "2.0" +version_added: "2.1" author: "Rob White (@wimnat)" options: filters: From c55e4e6787e4e75dc99e42939d2713b33a01d9ed Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 8 Feb 2016 12:07:23 -0500 Subject: [PATCH 1183/2522] fixed misppelled description --- cloud/docker/docker_login.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/docker/docker_login.py b/cloud/docker/docker_login.py index 05fac1dd5d0..964d507a16e 100644 --- a/cloud/docker/docker_login.py +++ b/cloud/docker/docker_login.py @@ -60,7 +60,7 @@ required: false default: ~/.docker/config.json docker_url: - descriptions: + description: - Refers to the protocol+hostname+port where the Docker server is hosted required: false default: unix://var/run/docker.sock From 6aeb2ab6cf54e0e3d309abedb2a9f164fb550914 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 8 Feb 2016 12:13:41 -0500 Subject: [PATCH 1184/2522] corrected parameter name and added missing description --- cloud/webfaction/webfaction_app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cloud/webfaction/webfaction_app.py b/cloud/webfaction/webfaction_app.py index 52ecedf438d..8f40a9ab85f 100644 --- a/cloud/webfaction/webfaction_app.py +++ b/cloud/webfaction/webfaction_app.py @@ -69,7 +69,9 @@ required: false default: null - open_port: + port_open: + description: + - IF the port should be opened required: false default: false From 9ae9d04eab0b7893b75c94fb74169d162304d5c6 Mon Sep 17 00:00:00 2001 From: Corwin Brown Date: Mon, 8 Feb 2016 15:15:05 -0600 Subject: [PATCH 1185/2522] Added UseBasicParsing flag win_uri uses "Invoke-WebRequest" under the covers, which apparently uses Internet Explorer to parse a webpage. The problem is if a user has never run Internet Explorer, it will be unable to do that. The work around for this is to set the "-UseBasicParsing" flag. The only advantage to having the Internet Explorer parsed page is that you can then access the DOM as if it was a powershell argument. That doesn't seem super useful for Ansible to be able to do, so I set the default to be "-UseBasicParsing" --- windows/win_uri.ps1 | 5 ++- windows/win_uri.py | 80 ++++++++++++++++++++++++++++++++------------- 2 files changed, 61 insertions(+), 24 deletions(-) diff --git a/windows/win_uri.ps1 b/windows/win_uri.ps1 index 471bceab723..3dd1d491bf1 100644 --- a/windows/win_uri.ps1 +++ b/windows/win_uri.ps1 @@ -33,6 +33,7 @@ $method = Get-AnsibleParam -obj $params "method" -default "GET" $content_type = Get-AnsibleParam -obj $params -name "content_type" $headers = Get-AnsibleParam -obj $params -name "headers" $body = Get-AnsibleParam -obj $params -name "body" +$use_basic_parsing = ConvertTo-Bool (Get-AnsibleParam -obj $params -name "use_basic_parsing" -default $true) $webrequest_opts.Uri = $url Set-Attr $result.win_uri "url" $url @@ -43,6 +44,9 @@ Set-Attr $result.win_uri "method" $method $webrequest_opts.ContentType = $content_type Set-Attr $result.win_uri "content_type" $content_type +$webrequest_opts.UseBasicParsing = $use_basic_parsing +Set-Attr $result.win_uri "use_basic_parsing" $use_basic_parsing + if ($headers -ne $null) { $req_headers = @{} ForEach ($header in $headers.psobject.properties) { @@ -64,4 +68,3 @@ ForEach ($prop in $response.psobject.properties) { } Exit-Json $result - diff --git a/windows/win_uri.py b/windows/win_uri.py index 161533631f9..fa61d8aa2e5 100644 --- a/windows/win_uri.py +++ b/windows/win_uri.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# (c) 2015, Corwin Brown +# (c) 2015, Corwin Brown # # This file is part of Ansible # @@ -24,7 +24,7 @@ DOCUMENTATION = """ --- module: win_uri -version_added: "2.0" +version_added: "2.1" short_description: Interacts with webservices. description: - Interacts with HTTP and HTTPS services. @@ -32,12 +32,10 @@ url: description: - HTTP or HTTPS URL in the form of (http|https)://host.domain:port/path - required: true method: description: - The HTTP Method of the request or response. default: GET - required: false choices: - GET - POST @@ -55,17 +53,20 @@ body: description: - The body of the HTTP request/response to the web service. - required: false - default: None headers: description: - Key Value pairs for headers. Example "Host: www.somesite.com" - required: false - default: None + use_basic_parsing: + description: + - This module relies upon 'Invoke-WebRequest', which by default uses the Internet Explorer Engine to parse a webpage. There's an edge-case where if a user hasn't run IE before, this will fail. The only advantage to using the Internet Explorer praser is that you can traverse the DOM in a powershell script. That isn't useful for Ansible, so by default we toggle 'UseBasicParsing'. However, you can toggle that off here. + choices: + - True + - False + default: True author: Corwin Brown (@blakfeld) """ -Examples = """ +EXAMPLES = """ # Send a GET request and store the output: --- - name: Perform a GET and Store Output @@ -96,19 +97,52 @@ url: http://www.somesite.com method: POST body: "{ 'some': 'json' }" +""" -# Check if a file is available on a webserver ---- -- name: Ensure Build is Available on Fileserver - when: ensure_build - win_uri: - url: "http://www.somesite.com" - method: HEAD - headers: - test: one - another: two - register: build_check_output - until: build_check_output.StatusCode == 200 - retries: 30 - delay: 10 +RETURN = """ +url: + description: The Target URL + returned: always + type: string + sample: "http://www.ansible.com" +method: + description: The HTTP method used. + returned: always + type: string + sample: "GET" +content_type: + description: The "content-type" header used. + returned: always + type: string + sample: "application/json" +use_basic_parsing: + description: The state of the "use_basic_parsing" flag. + returned: always + type: bool + sample: True +StatusCode: + description: The HTTP Status Code of the response. + returned: success + type: int + sample: 200 +StatusDescription: + description: A summery of the status. + returned: success + type: string + stample: "OK" +RawContent: + description: The raw content of the HTTP response. + returned: success + type: string + sample: 'HTTP/1.1 200 OK\nX-XSS-Protection: 1; mode=block\nX-Frame-Options: SAMEORIGIN\nAlternate-Protocol: 443:quic,p=1\nAlt-Svc: quic="www.google.com:443"; ma=2592000; v="30,29,28,27,26,25",quic=":443"; ma=2...' +Headers: + description: The Headers of the response. + returned: success + type: dict + sample: {"Content-Type": "application/json"} +RawContentLength: + description: The byte size of the response. + returned: success + type: int + sample: 54447 """ From 9695db53e08d5ff79d874593497bdc5a3e0c8fb4 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 9 Feb 2016 06:42:02 +0000 Subject: [PATCH 1186/2522] Add extra PS Drives so you can access other parts of the registry --- windows/win_regedit.ps1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/windows/win_regedit.ps1 b/windows/win_regedit.ps1 index e3b8c9d3b10..fe060e101c6 100644 --- a/windows/win_regedit.ps1 +++ b/windows/win_regedit.ps1 @@ -21,6 +21,10 @@ $ErrorActionPreference = "Stop" # WANT_JSON # POWERSHELL_COMMON +New-PSDrive -PSProvider registry -Root HKEY_CLASSES_ROOT -Name HKCR -ErrorAction SilentlyContinue +New-PSDrive -PSProvider registry -Root HKEY_USERS -Name HKU -ErrorAction SilentlyContinue +New-PSDrive -PSProvider registry -Root HKEY_CURRENT_CONFIG -Name HCCC -ErrorAction SilentlyContinue + $params = Parse-Args $args; $result = New-Object PSObject; Set-Attr $result "changed" $false; From e704525d469075023d6bee94f97d1420fd1dd91e Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 9 Feb 2016 10:38:56 -0600 Subject: [PATCH 1187/2522] Add option to send puppet apply logs to syslog While returning puppet logs as ansible stdout is useful in some cases, there are also cases where it's more destructive than helpful. For those, local logging to syslog so that the ansible logging makes sense is very useful. This defaults to stdout so that behavior does not change for people. --- system/puppet.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/system/puppet.py b/system/puppet.py index e1d21624c47..ebad74fd298 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -67,6 +67,12 @@ - Puppet environment to be used. required: false default: None + logdest: + description: + - Where the puppet logs should go, if puppet apply is being used + required: false + default: stdout + choices: [ 'stdout', 'syslog' ] requirements: [ puppet ] author: "Monty Taylor (@emonty)" ''' @@ -111,8 +117,12 @@ def main(): timeout=dict(default="30m"), puppetmaster=dict(required=False, default=None), manifest=dict(required=False, default=None), + logdest=dict( + required=False, default=['stdout'], + choices=['stdout', 'syslog']), show_diff=dict( - default=False, aliases=['show-diff'], type='bool'), # internal code to work with --diff, do not use + # internal code to work with --diff, do not use + default=False, aliases=['show-diff'], type='bool'), facts=dict(default=None), facter_basename=dict(default='ansible'), environment=dict(required=False, default=None), @@ -184,6 +194,8 @@ def main(): cmd += " --no-noop" else: cmd = "%s apply --detailed-exitcodes " % base_cmd + if p['logdest'] == 'syslog': + cmd += "--logdest syslog " if p['environment']: cmd += "--environment '%s' " % p['environment'] if module.check_mode: From 816d3bff6f5e46f642e22a3eb4a3978a97376089 Mon Sep 17 00:00:00 2001 From: James Cammarata Date: Tue, 9 Feb 2016 14:37:15 -0500 Subject: [PATCH 1188/2522] Adding version_added field to logdest field for puppet module --- system/puppet.py | 1 + 1 file changed, 1 insertion(+) diff --git a/system/puppet.py b/system/puppet.py index ebad74fd298..c26b96db6df 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -73,6 +73,7 @@ required: false default: stdout choices: [ 'stdout', 'syslog' ] + version_added: "2.1" requirements: [ puppet ] author: "Monty Taylor (@emonty)" ''' From d17fdc184617bd4c5783d772763db03e9fcf2882 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Tue, 2 Feb 2016 19:45:55 +0100 Subject: [PATCH 1189/2522] cloudstack: new module cs_cluster --- cloud/cloudstack/cs_cluster.py | 431 +++++++++++++++++++++++++++++++++ 1 file changed, 431 insertions(+) create mode 100644 cloud/cloudstack/cs_cluster.py diff --git a/cloud/cloudstack/cs_cluster.py b/cloud/cloudstack/cs_cluster.py new file mode 100644 index 00000000000..6041d65081f --- /dev/null +++ b/cloud/cloudstack/cs_cluster.py @@ -0,0 +1,431 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2016, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: cs_cluster +short_description: Manages host clusters on Apache CloudStack based clouds. +description: + - Create, update and remove clusters. +version_added: "2.1" +author: "René Moser (@resmo)" +options: + name: + description: + - name of the cluster. + required: true + zone: + description: + - Name of the zone in which the cluster belongs to. + - If not set, default zone is used. + required: false + default: null + pod: + description: + - Name of the pod in which the cluster belongs to. + required: false + default: null + cluster_type: + description: + - Type of the cluster. + - Required if C(state=present) + required: false + default: null + choices: [ 'CloudManaged', 'ExternalManaged' ] + hypervisor: + description: + - Name the hypervisor to be used. + - Required if C(state=present). + required: false + default: none + choices: [ 'KVM', 'VMware', 'BareMetal', 'XenServer', 'LXC', 'HyperV', 'UCS', 'OVM' ] + url: + description: + - URL for the cluster + required: false + default: null + username: + description: + - Username for the cluster. + required: false + default: null + password: + description: + - Password for the cluster. + required: false + default: null + guest_vswitch_name: + description: + - Name of virtual switch used for guest traffic in the cluster. + - This would override zone wide traffic label setting. + required: false + default: null + guest_vswitch_type: + description: + - Type of virtual switch used for guest traffic in the cluster. + - Allowed values are, vmwaresvs (for VMware standard vSwitch) and vmwaredvs (for VMware distributed vSwitch) + required: false + default: null + choices: [ 'vmwaresvs', 'vmwaredvs' ] + public_vswitch_name: + description: + - Name of virtual switch used for public traffic in the cluster. + - This would override zone wide traffic label setting. + required: false + default: null + public_vswitch_type: + description: + - Type of virtual switch used for public traffic in the cluster. + - Allowed values are, vmwaresvs (for VMware standard vSwitch) and vmwaredvs (for VMware distributed vSwitch) + required: false + default: null + choices: [ 'vmwaresvs', 'vmwaredvs' ] + vms_ip_address: + description: + - IP address of the VSM associated with this cluster. + required: false + default: null + vms_username: + description: + - Username for the VSM associated with this cluster. + required: false + default: null + vms_password: + description: + - Password for the VSM associated with this cluster. + required: false + default: null + ovm3_cluster: + description: + - Ovm3 native OCFS2 clustering enabled for cluster. + required: false + default: null + ovm3_pool: + description: + - Ovm3 native pooling enabled for cluster. + required: false + default: null + ovm3_vip: + description: + - Ovm3 vip to use for pool (and cluster). + required: false + default: null + state: + description: + - State of the cluster. + required: false + default: 'present' + choices: [ 'present', 'absent', 'disabled', 'enabled' ] +extends_documentation_fragment: cloudstack +''' + +EXAMPLES = ''' +# Ensure a cluster is present +- local_action: + module: cs_cluster + name: kvm-cluster-01 + zone: ch-zrh-ix-01 + hypervisor: KVM + cluster_type: CloudManaged + +# Ensure a cluster is disabled +- local_action: + module: cs_cluster + name: kvm-cluster-01 + zone: ch-zrh-ix-01 + state: disabled + +# Ensure a cluster is enabled +- local_action: + module: cs_cluster + name: kvm-cluster-01 + zone: ch-zrh-ix-01 + state: enabled + +# Ensure a cluster is absent +- local_action: + module: cs_cluster + name: kvm-cluster-01 + zone: ch-zrh-ix-01 + state: absent +''' + +RETURN = ''' +--- +id: + description: UUID of the cluster. + returned: success + type: string + sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6 +name: + description: Name of the cluster. + returned: success + type: string + sample: cluster01 +allocation_state: + description: State of the cluster. + returned: success + type: string + sample: Enabled +cluster_type: + description: Type of the cluster. + returned: success + type: string + sample: ExternalManaged +cpu_overcommit_ratio: + description: The CPU overcommit ratio of the cluster. + returned: success + type: string + sample: 1.0 +memory_overcommit_ratio: + description: The memory overcommit ratio of the cluster. + returned: success + type: string + sample: 1.0 +managed_state: + description: Whether this cluster is managed by CloudStack. + returned: success + type: string + sample: Managed +ovm3_vip: + description: Ovm3 VIP to use for pooling and/or clustering + returned: success + type: string + sample: 10.10.10.101 +hypervisor: + description: Hypervisor of the cluster + returned: success + type: string + sample: VMware +zone: + description: Name of zone the cluster is in. + returned: success + type: string + sample: ch-gva-2 +pod: + description: Name of pod the cluster is in. + returned: success + type: string + sample: pod01 +''' + +try: + from cs import CloudStack, CloudStackException, read_config + has_lib_cs = True +except ImportError: + has_lib_cs = False + +# import cloudstack common +from ansible.module_utils.cloudstack import * + +class AnsibleCloudStackCluster(AnsibleCloudStack): + + def __init__(self, module): + super(AnsibleCloudStackCluster, self).__init__(module) + self.returns = { + 'allocationstate': 'allocation_state', + 'hypervisortype': 'hypervisor', + 'clustertype': 'cluster_type', + 'podname': 'pod', + 'managedstate': 'managed_state', + 'memoryovercommitratio': 'memory_overcommit_ratio', + 'cpuovercommitratio': 'cpu_overcommit_ratio', + 'ovm3vip': 'ovm3_vip', + } + self.cluster = None + + + def _get_common_cluster_args(self): + args = {} + args['clustername'] = self.module.params.get('name') + args['hypervisor'] = self.module.params.get('hypervisor') + args['clustertype'] = self.module.params.get('cluster_type') + + state = self.module.params.get('state') + if state in [ 'enabled', 'disabled']: + args['allocationstate'] = state.capitalize() + return args + + + def get_pod(self, key=None): + args = {} + args['name'] = self.module.params.get('pod') + args['zoneid'] = self.get_zone(key='id') + pods = self.cs.listPods(**args) + if pods: + return self._get_by_key(key, pods['pod'][0]) + self.module.fail_json(msg="Pod %s not found in zone %s." % (self.module.params.get('pod'), self.get_zone(key='name'))) + + + def get_cluster(self): + if not self.cluster: + args = {} + + uuid = self.module.params.get('id') + if uuid: + args['id'] = uuid + clusters = self.cs.listClusters(**args) + if clusters: + self.cluster = clusters['cluster'][0] + return self.cluster + + args['name'] = self.module.params.get('name') + clusters = self.cs.listClusters(**args) + if clusters: + self.cluster = clusters['cluster'][0] + # fix differnt return from API then request argument given + self.cluster['hypervisor'] = self.cluster['hypervisortype'] + self.cluster['clustername'] = self.cluster['name'] + return self.cluster + + + def present_cluster(self): + cluster = self.get_cluster() + if cluster: + cluster = self._update_cluster() + else: + cluster = self._create_cluster() + return cluster + + + def _create_cluster(self): + required_params = [ + 'cluster_type', + 'hypervisor', + ] + self.module.fail_on_missing_params(required_params=required_params) + + args = self._get_common_cluster_args() + args['zoneid'] = self.get_zone(key='id') + args['podid'] = self.get_pod(key='id') + args['url'] = self.module.params.get('url') + args['username'] = self.module.params.get('username') + args['password'] = self.module.params.get('password') + args['guestvswitchname'] = self.module.params.get('guest_vswitch_name') + args['guestvswitchtype'] = self.module.params.get('guest_vswitch_type') + args['publicvswitchtype'] = self.module.params.get('public_vswitch_name') + args['publicvswitchtype'] = self.module.params.get('public_vswitch_type') + args['vsmipaddress'] = self.module.params.get('vms_ip_address') + args['vsmusername'] = self.module.params.get('vms_username') + args['vmspassword'] = self.module.params.get('vms_password') + args['ovm3cluster'] = self.module.params.get('ovm3_cluster') + args['ovm3pool'] = self.module.params.get('ovm3_pool') + args['ovm3vip'] = self.module.params.get('ovm3_vip') + + self.result['changed'] = True + + cluster = None + if not self.module.check_mode: + res = self.cs.addCluster(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + # API returns a list as result CLOUDSTACK-9205 + if isinstance(res['cluster'], list): + cluster = res['cluster'][0] + else: + cluster = res['cluster'] + return cluster + + + def _update_cluster(self): + cluster = self.get_cluster() + + args = self._get_common_cluster_args() + args['id'] = cluster['id'] + + if self.has_changed(args, cluster): + self.result['changed'] = True + + if not self.module.check_mode: + res = self.cs.updateCluster(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + cluster = res['cluster'] + return cluster + + + def absent_cluster(self): + cluster = self.get_cluster() + if cluster: + self.result['changed'] = True + + args = {} + args['id'] = cluster['id'] + + if not self.module.check_mode: + res = self.cs.deleteCluster(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + return cluster + + +def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + name = dict(required=True), + zone = dict(default=None), + pod = dict(default=None), + cluster_type = dict(choices=['CloudManaged', 'ExternalManaged'], default=None), + hypervisor = dict(choices=CS_HYPERVISORS, default=None), + state = dict(choices=['present', 'enabled', 'disabled', 'absent'], default='present'), + url = dict(default=None), + username = dict(default=None), + password = dict(default=None, no_log=True), + guest_vswitch_name = dict(default=None), + guest_vswitch_type = dict(choices=['vmwaresvs', 'vmwaredvs'], default=None), + public_vswitch_name = dict(default=None), + public_vswitch_type = dict(choices=['vmwaresvs', 'vmwaredvs'], default=None), + vms_ip_address = dict(default=None), + vms_username = dict(default=None), + vms_password = dict(default=None, no_log=True), + ovm3_cluster = dict(default=None), + ovm3_pool = dict(default=None), + ovm3_vip = dict(default=None), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=cs_required_together(), + supports_check_mode=True + ) + + if not has_lib_cs: + module.fail_json(msg="python library cs required: pip install cs") + + try: + acs_cluster = AnsibleCloudStackCluster(module) + + state = module.params.get('state') + if state in ['absent']: + cluster = acs_cluster.absent_cluster() + else: + cluster = acs_cluster.present_cluster() + + result = acs_cluster.get_result(cluster) + + except CloudStackException as e: + module.fail_json(msg='CloudStackException: %s' % str(e)) + + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From dbf260b77900eb2055946f5d81a49fd91b69f572 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Tue, 2 Feb 2016 19:42:57 +0100 Subject: [PATCH 1190/2522] cloudstack: new module cs_pod --- cloud/cloudstack/cs_pod.py | 310 +++++++++++++++++++++++++++++++++++++ 1 file changed, 310 insertions(+) create mode 100644 cloud/cloudstack/cs_pod.py diff --git a/cloud/cloudstack/cs_pod.py b/cloud/cloudstack/cs_pod.py new file mode 100644 index 00000000000..8bf33ec6a09 --- /dev/null +++ b/cloud/cloudstack/cs_pod.py @@ -0,0 +1,310 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2016, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: cs_pod +short_description: Manages pods on Apache CloudStack based clouds. +description: + - Create, update, delete pods. +version_added: "2.1" +author: "René Moser (@resmo)" +options: + name: + description: + - Name of the pod. + required: true + id: + description: + - uuid of the exising pod. + default: null + required: false + start_ip: + description: + - Starting IP address for the Pod. + - Required on C(state=present) + default: null + required: false + end_ip: + description: + - Ending IP address for the Pod. + default: null + required: false + netmask: + description: + - Netmask for the Pod. + - Required on C(state=present) + default: null + required: false + gateway: + description: + - Gateway for the Pod. + - Required on C(state=present) + default: null + required: false + zone: + description: + - Name of the zone in which the pod belongs to. + - If not set, default zone is used. + required: false + default: null + state: + description: + - State of the pod. + required: false + default: 'present' + choices: [ 'present', 'enabled', 'disabled', 'absent' ] +extends_documentation_fragment: cloudstack +''' + +EXAMPLES = ''' +# Ensure a pod is present +- local_action: + module: cs_pod + name: pod1 + zone: ch-zrh-ix-01 + start_ip: 10.100.10.101 + gateway: 10.100.10.1 + netmask: 255.255.255.0 + +# Ensure a pod is disabled +- local_action: + module: cs_pod + name: pod1 + zone: ch-zrh-ix-01 + state: disabled + +# Ensure a pod is enabled +- local_action: + module: cs_pod + name: pod1 + zone: ch-zrh-ix-01 + state: enabled + +# Ensure a pod is absent +- local_action: + module: cs_pod + name: pod1 + zone: ch-zrh-ix-01 + state: absent +''' + +RETURN = ''' +--- +id: + description: UUID of the pod. + returned: success + type: string + sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6 +name: + description: Name of the pod. + returned: success + type: string + sample: pod01 +start_ip: + description: Starting IP of the pod. + returned: success + type: string + sample: 10.100.1.101 +end_ip: + description: Ending IP of the pod. + returned: success + type: string + sample: 10.100.1.254 +netmask: + description: Netmask of the pod. + returned: success + type: string + sample: 255.255.255.0 +gateway: + description: Gateway of the pod. + returned: success + type: string + sample: 10.100.1.1 +allocation_state: + description: State of the pod. + returned: success + type: string + sample: Enabled +zone: + description: Name of zone the pod is in. + returned: success + type: string + sample: ch-gva-2 +''' + +try: + from cs import CloudStack, CloudStackException, read_config + has_lib_cs = True +except ImportError: + has_lib_cs = False + +# import cloudstack common +from ansible.module_utils.cloudstack import * + +class AnsibleCloudStackPod(AnsibleCloudStack): + + def __init__(self, module): + super(AnsibleCloudStackPod, self).__init__(module) + self.returns = { + 'endip': 'end_ip', + 'startip': 'start_ip', + 'gateway': 'gateway', + 'netmask': 'netmask', + 'allocationstate': 'allocation_state', + } + self.pod = None + + + def _get_common_pod_args(self): + args = {} + args['name'] = self.module.params.get('name') + args['zoneid'] = self.get_zone(key='id') + args['startip'] = self.module.params.get('start_ip') + args['endip'] = self.module.params.get('end_ip') + args['netmask'] = self.module.params.get('netmask') + args['gateway'] = self.module.params.get('gateway') + state = self.module.params.get('state') + if state in [ 'enabled', 'disabled']: + args['allocationstate'] = state.capitalize() + return args + + + def get_pod(self): + if not self.pod: + args = {} + + uuid = self.module.params.get('id') + if uuid: + args['id'] = uuid + args['zoneid'] = self.get_zone(key='id') + pods = self.cs.listPods(**args) + if pods: + self.pod = pods['pod'][0] + return self.pod + + args['name'] = self.module.params.get('name') + args['zoneid'] = self.get_zone(key='id') + pods = self.cs.listPods(**args) + if pods: + self.pod = pods['pod'][0] + return self.pod + + + def present_pod(self): + pod = self.get_pod() + if pod: + pod = self._update_pod() + else: + pod = self._create_pod() + return pod + + + def _create_pod(self): + required_params = [ + 'start_ip', + 'netmask', + 'gateway', + ] + self.module.fail_on_missing_params(required_params=required_params) + + pod = None + self.result['changed'] = True + args = self._get_common_pod_args() + if not self.module.check_mode: + res = self.cs.createPod(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + pod = res['pod'] + return pod + + + def _update_pod(self): + pod = self.get_pod() + args = self._get_common_pod_args() + args['id'] = pod['id'] + + if self.has_changed(args, pod): + self.result['changed'] = True + + if not self.module.check_mode: + res = self.cs.updatePod(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + pod = res['pod'] + return pod + + + def absent_pod(self): + pod = self.get_pod() + if pod: + self.result['changed'] = True + + args = {} + args['id'] = pod['id'] + + if not self.module.check_mode: + res = self.cs.deletePod(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + return pod + + +def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + id = dict(default=None), + name = dict(required=True), + gateway = dict(default=None), + netmask = dict(default=None), + start_ip = dict(default=None), + end_ip = dict(default=None), + zone = dict(default=None), + state = dict(choices=['present', 'enabled', 'disabled', 'absent'], default='present'), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=cs_required_together(), + supports_check_mode=True + ) + + if not has_lib_cs: + module.fail_json(msg="python library cs required: pip install cs") + + try: + acs_pod = AnsibleCloudStackPod(module) + state = module.params.get('state') + if state in ['absent']: + pod = acs_pod.absent_pod() + else: + pod = acs_pod.present_pod() + + result = acs_pod.get_result(pod) + + except CloudStackException as e: + module.fail_json(msg='CloudStackException: %s' % str(e)) + + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From 15f2a328a24dd9ae4bdbe9ee5c24d87aac41a8a0 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Tue, 2 Feb 2016 19:49:27 +0100 Subject: [PATCH 1191/2522] cloudstack: new module cs_zone --- cloud/cloudstack/cs_zone.py | 411 ++++++++++++++++++++++++++++++++++++ 1 file changed, 411 insertions(+) create mode 100644 cloud/cloudstack/cs_zone.py diff --git a/cloud/cloudstack/cs_zone.py b/cloud/cloudstack/cs_zone.py new file mode 100644 index 00000000000..84aad34726c --- /dev/null +++ b/cloud/cloudstack/cs_zone.py @@ -0,0 +1,411 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2016, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: cs_zone +short_description: Manages zones on Apache CloudStack based clouds. +description: + - Create, update and remove zones. +version_added: "2.1" +author: "René Moser (@resmo)" +options: + name: + description: + - Name of the zone. + required: true + id: + description: + - uuid of the exising zone. + default: null + required: false + state: + description: + - State of the zone. + required: false + default: 'present' + choices: [ 'present', 'enabled', 'disabled', 'absent' ] + domain: + description: + - Domain the zone is related to. + - Zone is a public zone if not set. + required: false + default: null + network_domain: + description: + - Network domain for the zone. + required: false + default: null + network_type: + description: + - Network type of the zone. + required: false + default: basic + choices: [ 'basic', 'advanced' ] + dns1: + description: + - First DNS for the zone. + - Required if C(state=present) + required: false + default: null + dns2: + description: + - Second DNS for the zone. + required: false + default: null + internal_dns1: + description: + - First internal DNS for the zone. + - If not set C(dns1) will be used on C(state=present). + required: false + default: null + internal_dns2: + description: + - Second internal DNS for the zone. + required: false + default: null + dns1_ipv6: + description: + - First DNS for IPv6 for the zone. + required: false + default: null + dns2_ipv6: + description: + - Second DNS for IPv6 for the zone. + required: false + default: null + guest_cidr_address: + description: + - Guest CIDR address for the zone. + required: false + default: null + dhcp_provider: + description: + - DHCP provider for the Zone. + required: false + default: null +extends_documentation_fragment: cloudstack +''' + +EXAMPLES = ''' +# Ensure a zone is present +- local_action: + module: cs_zone + name: ch-zrh-ix-01 + dns1: 8.8.8.8 + dns2: 8.8.4.4 + network_type: basic + +# Ensure a zone is disabled +- local_action: + module: cs_zone + name: ch-zrh-ix-01 + state: disabled + +# Ensure a zone is enabled +- local_action: + module: cs_zone + name: ch-zrh-ix-01 + state: enabled + +# Ensure a zone is absent +- local_action: + module: cs_zone + name: ch-zrh-ix-01 + state: absent +''' + +RETURN = ''' +--- +id: + description: UUID of the zone. + returned: success + type: string + sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6 +name: + description: Name of the zone. + returned: success + type: string + sample: zone01 +dns1: + description: First DNS for the zone. + returned: success + type: string + sample: 8.8.8.8 +dns2: + description: Second DNS for the zone. + returned: success + type: string + sample: 8.8.4.4 +internal_dns1: + description: First internal DNS for the zone. + returned: success + type: string + sample: 8.8.8.8 +internal_dns2: + description: Second internal DNS for the zone. + returned: success + type: string + sample: 8.8.4.4 +dns1_ipv6: + description: First IPv6 DNS for the zone. + returned: success + type: string + sample: "2001:4860:4860::8888" +dns2_ipv6: + description: Second IPv6 DNS for the zone. + returned: success + type: string + sample: "2001:4860:4860::8844" +allocation_state: + description: State of the zone. + returned: success + type: string + sample: Enabled +domain: + description: Domain the zone is related to. + returned: success + type: string + sample: ROOT +network_domain: + description: Network domain for the zone. + returned: success + type: string + sample: example.com +network_type: + description: Network type for the zone. + returned: success + type: string + sample: basic +local_storage_enabled: + description: Local storage offering enabled. + returned: success + type: bool + sample: false +securitygroups_enabled: + description: Security groups support is enabled. + returned: success + type: bool + sample: false +guest_cidr_address: + description: Guest CIDR address for the zone + returned: success + type: string + sample: 10.1.1.0/24 +dhcp_provider: + description: DHCP provider for the zone + returned: success + type: string + sample: VirtualRouter +zone_token: + description: Zone token + returned: success + type: string + sample: ccb0a60c-79c8-3230-ab8b-8bdbe8c45bb7 +tags: + description: List of resource tags associated with the zone. + returned: success + type: dict + sample: [ { "key": "foo", "value": "bar" } ] +''' + +try: + from cs import CloudStack, CloudStackException, read_config + has_lib_cs = True +except ImportError: + has_lib_cs = False + +# import cloudstack common +from ansible.module_utils.cloudstack import * + +class AnsibleCloudStackZone(AnsibleCloudStack): + + def __init__(self, module): + super(AnsibleCloudStackZone, self).__init__(module) + self.returns = { + 'dns1': 'dns1', + 'dns2': 'dns2', + 'internaldns1': 'internal_dns1', + 'internaldns2': 'internal_dns2', + 'ipv6dns1': 'dns1_ipv6', + 'ipv6dns2': 'dns2_ipv6', + 'domain': 'network_domain', + 'networktype': 'network_type', + 'securitygroupsenabled': 'securitygroups_enabled', + 'localstorageenabled': 'local_storage_enabled', + 'guestcidraddress': 'guest_cidr_address', + 'dhcpprovider': 'dhcp_provider', + 'allocationstate': 'allocation_state', + 'zonetoken': 'zone_token', + } + self.zone = None + + + def _get_common_zone_args(self): + args = {} + args['name'] = self.module.params.get('name') + args['dns1'] = self.module.params.get('dns1') + args['dns2'] = self.module.params.get('dns2') + args['internaldns1'] = self.get_or_fallback('internal_dns1', 'dns1') + args['internaldns2'] = self.get_or_fallback('internal_dns2', 'dns2') + args['ipv6dns1'] = self.module.params.get('dns1_ipv6') + args['ipv6dns2'] = self.module.params.get('dns2_ipv6') + args['networktype'] = self.module.params.get('network_type') + args['domain'] = self.module.params.get('network_domain') + args['localstorageenabled'] = self.module.params.get('local_storage_enabled') + args['guestcidraddress'] = self.module.params.get('guest_cidr_address') + args['dhcpprovider'] = self.module.params.get('dhcp_provider') + state = self.module.params.get('state') + if state in [ 'enabled', 'disabled']: + args['allocationstate'] = state.capitalize() + return args + + + def get_zone(self): + if not self.zone: + args = {} + + uuid = self.module.params.get('id') + if uuid: + args['id'] = uuid + zones = self.cs.listZones(**args) + if zones: + self.zone = zones['zone'][0] + return self.zone + + args['name'] = self.module.params.get('name') + zones = self.cs.listZones(**args) + if zones: + self.zone = zones['zone'][0] + return self.zone + + + def present_zone(self): + zone = self.get_zone() + if zone: + zone = self._update_zone() + else: + zone = self._create_zone() + return zone + + + def _create_zone(self): + required_params = [ + 'dns1', + ] + self.module.fail_on_missing_params(required_params=required_params) + + self.result['changed'] = True + + args = self._get_common_zone_args() + args['domainid'] = self.get_domain(key='id') + args['securitygroupenabled'] = self.module.params.get('securitygroups_enabled') + + zone = None + if not self.module.check_mode: + res = self.cs.createZone(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + zone = res['zone'] + return zone + + + def _update_zone(self): + zone = self.get_zone() + + args = self._get_common_zone_args() + args['id'] = zone['id'] + + if self.has_changed(args, zone): + self.result['changed'] = True + + if not self.module.check_mode: + res = self.cs.updateZone(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + zone = res['zone'] + return zone + + + def absent_zone(self): + zone = self.get_zone() + if zone: + self.result['changed'] = True + + args = {} + args['id'] = zone['id'] + + if not self.module.check_mode: + res = self.cs.deleteZone(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + return zone + + +def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + id = dict(default=None), + name = dict(required=True), + dns1 = dict(default=None), + dns2 = dict(default=None), + internal_dns1 = dict(default=None), + internal_dns2 = dict(default=None), + dns1_ipv6 = dict(default=None), + dns2_ipv6 = dict(default=None), + network_type = dict(default='basic', choices=['Basic', 'basic', 'Advanced', 'advanced']), + network_domain = dict(default=None), + guest_cidr_address = dict(default=None), + dhcp_provider = dict(default=None), + local_storage_enabled = dict(default=None), + securitygroups_enabled = dict(default=None), + state = dict(choices=['present', 'enabled', 'disabled', 'absent'], default='present'), + domain = dict(default=None), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=cs_required_together(), + supports_check_mode=True + ) + + if not has_lib_cs: + module.fail_json(msg="python library cs required: pip install cs") + + try: + acs_zone = AnsibleCloudStackZone(module) + + state = module.params.get('state') + if state in ['absent']: + zone = acs_zone.absent_zone() + else: + zone = acs_zone.present_zone() + + result = acs_zone.get_result(zone) + + except CloudStackException as e: + module.fail_json(msg='CloudStackException: %s' % str(e)) + + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From 41a2542f0013c50dcbbea1ced34582d125d699e4 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Sun, 27 Dec 2015 16:35:33 -0500 Subject: [PATCH 1192/2522] Ensure ec2_win_password doesn't leak file handle Currently the module doesn't explicitly close the file handle. This wraps the reading of the private key in a try/finally block to ensure the file is properly closed. --- cloud/amazon/ec2_win_password.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/ec2_win_password.py b/cloud/amazon/ec2_win_password.py index e3a012291e3..4ddf4f8f4cc 100644 --- a/cloud/amazon/ec2_win_password.py +++ b/cloud/amazon/ec2_win_password.py @@ -140,8 +140,11 @@ def main(): if wait and datetime.datetime.now() >= end: module.fail_json(msg = "wait for password timeout after %d seconds" % wait_timeout) - f = open(key_file, 'r') - key = RSA.importKey(f.read(), key_passphrase) + try: + f = open(key_file, 'r') + key = RSA.importKey(f.read(), key_passphrase) + finally: + f.close() cipher = PKCS1_v1_5.new(key) sentinel = 'password decryption failed!!!' From df92a0be0a583abfb095c0042605fcc42a3de1af Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Wed, 10 Feb 2016 10:46:16 -0500 Subject: [PATCH 1193/2522] fixed version added --- database/postgresql/postgresql_ext.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/postgresql/postgresql_ext.py b/database/postgresql/postgresql_ext.py index 3f4a745846c..d3079630846 100644 --- a/database/postgresql/postgresql_ext.py +++ b/database/postgresql/postgresql_ext.py @@ -22,7 +22,7 @@ short_description: Add or remove PostgreSQL extensions from a database. description: - Add or remove PostgreSQL extensions from a database. -version_added: "0.1" +version_added: "1.9" options: name: description: From bba1dac0f07ac754d2e2395e619d102e3d4c6201 Mon Sep 17 00:00:00 2001 From: Alex Kalinin Date: Wed, 10 Feb 2016 18:52:08 -0800 Subject: [PATCH 1194/2522] Fix opening libvirt esx connection --- cloud/misc/virt.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cloud/misc/virt.py b/cloud/misc/virt.py index b59c7ed3de3..65791e43e9f 100644 --- a/cloud/misc/virt.py +++ b/cloud/misc/virt.py @@ -128,6 +128,9 @@ def __init__(self, uri, module): if "xen" in stdout: conn = libvirt.open(None) + elif "esx" in uri: + auth = [[libvirt.VIR_CRED_AUTHNAME, libvirt.VIR_CRED_NOECHOPROMPT], [], None] + conn = libvirt.openAuth(uri, auth) else: conn = libvirt.open(uri) From 808177e2c92e379d3c55340c05cd1bb42dca2d3c Mon Sep 17 00:00:00 2001 From: Konstantin Shalygin Date: Thu, 11 Feb 2016 18:34:44 +0600 Subject: [PATCH 1195/2522] Fix recurse delete. Add force update_cache feature. --- packaging/os/pacman.py | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/packaging/os/pacman.py b/packaging/os/pacman.py index 7aa5bf45eaf..81b9c907772 100644 --- a/packaging/os/pacman.py +++ b/packaging/os/pacman.py @@ -60,7 +60,9 @@ force: description: - - Force remove package, without any checks. + - When removing package - force remove package, without any + checks. When update_cache - force redownload repo + databases. required: false default: no choices: ["yes", "no"] @@ -143,13 +145,18 @@ def query_package(module, pacman_path, name, state="present"): # Return True to indicate that the package is installed locally, and the result of the version number comparison # to determine if the package is up-to-date. return True, (lversion == rversion), False - + # package is installed but cannot fetch remote Version. Last True stands for the error return True, True, True def update_package_db(module, pacman_path): - cmd = "%s -Sy" % (pacman_path) + if module.params["force"]: + args = "Syy" + else: + args = "Sy" + + cmd = "%s -%s" % (pacman_path, args) rc, stdout, stderr = module.run_command(cmd, check_rc=False) if rc == 0: @@ -175,14 +182,13 @@ def upgrade(module, pacman_path): module.exit_json(changed=False, msg='Nothing to upgrade') def remove_packages(module, pacman_path, packages): - if module.params["recurse"]: - args = "Rs" - else: - args = "R" - -def remove_packages(module, pacman_path, packages): - if module.params["force"]: - args = "Rdd" + if module.params["recurse"] or module.params["force"]: + if module.params["recurse"]: + args = "Rs" + if module.params["force"]: + args = "Rdd" + if module.params["recurse"] and module.params["force"]: + args = "Rdds" else: args = "R" @@ -219,7 +225,7 @@ def install_packages(module, pacman_path, state, packages, package_files): installed, updated, latestError = query_package(module, pacman_path, package) if latestError and state == 'latest': package_err.append(package) - + if installed and (state == 'present' or (state == 'latest' and updated)): continue @@ -235,15 +241,15 @@ def install_packages(module, pacman_path, state, packages, package_files): module.fail_json(msg="failed to install %s" % (package)) install_c += 1 - + if state == 'latest' and len(package_err) > 0: message = "But could not ensure 'latest' state for %s package(s) as remote version could not be fetched." % (package_err) - + if install_c > 0: module.exit_json(changed=True, msg="installed %s package(s). %s" % (install_c, message)) - + module.exit_json(changed=False, msg="package(s) already installed. %s" % (message)) - + def check_packages(module, pacman_path, packages, state): would_be_changed = [] for package in packages: From 7c99a60f2bda35914e21053e93d1d9412dd0e42a Mon Sep 17 00:00:00 2001 From: Andrea Scarpino Date: Thu, 11 Feb 2016 16:43:12 +0100 Subject: [PATCH 1196/2522] win_unzip: overwrite any existing file --- windows/win_unzip.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/windows/win_unzip.ps1 b/windows/win_unzip.ps1 index 0e5485dddb9..f547c0081fa 100644 --- a/windows/win_unzip.ps1 +++ b/windows/win_unzip.ps1 @@ -61,7 +61,8 @@ If ($ext -eq ".zip" -And $recurse -eq $false) { $shell = New-Object -ComObject Shell.Application $zipPkg = $shell.NameSpace($src) $destPath = $shell.NameSpace($dest) - $destPath.CopyHere($zipPkg.Items()) + # 20 means do not display any dialog (4) and overwrite any file (16) + $destPath.CopyHere($zipPkg.Items(), 20) $result.changed = $true } Catch { From d011149baf63dbfb05511a6ede7db75ccd769c71 Mon Sep 17 00:00:00 2001 From: Jan Chaloupka Date: Fri, 12 Feb 2016 15:51:48 +0100 Subject: [PATCH 1197/2522] dnf module: package not installed with state=latest dnf: name=PACKAGE state=latest is reponsible for two use cases: - to install a package if not already installed. - to update the package to the latest if already installed. The latter use cases is not handled properly as base.upgrade does not throw dnf.exceptions.MarkingError if a package is not installed. Setting base.conf.best = True ensures a package is installed or updated to the latest when calling base.install. Sign-off: jsilhan@redhat.com Sign-off: jchaloup@redhat.com --- packaging/os/dnf.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index 56039258ed5..efd17ef1eb5 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -274,11 +274,10 @@ def ensure(module, base, state, names): # If not already installed, try to install. base.group_install(group, const.GROUP_PACKAGE_TYPES) for pkg_spec in pkg_specs: - try: - base.upgrade(pkg_spec) - except dnf.exceptions.MarkingError: - # If not already installed, try to install. - _mark_package_install(module, base, pkg_spec) + # best effort causes to install the latest package + # even if not previously installed + base.conf.best = True + base.install(pkg_spec) else: # state == absent From 14ff9e50309fd0fb5a0e00c87eaecb26845d392a Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Sun, 14 Feb 2016 11:33:15 -0500 Subject: [PATCH 1198/2522] merged duplicate entries for vdc_name --- cloud/vmware/vca_vapp.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/cloud/vmware/vca_vapp.py b/cloud/vmware/vca_vapp.py index 1c37c350abe..68ed5f255db 100644 --- a/cloud/vmware/vca_vapp.py +++ b/cloud/vmware/vca_vapp.py @@ -32,10 +32,6 @@ description: - The name of the vCloud Air vApp instance required: yes - vdc_name: - description: - - The name of the virtual data center (VDC) that contains the vAPP - required: yes template_name: description: - The name of the vApp template to use to create the vApp instance. If @@ -127,7 +123,7 @@ choices: [ "vca", "vchs", "vcd" ] vdc_name: description: - - The name of the vdc where the vm should be created. + - The name of the virtual data center (VDC) where the vm should be created or contains the vAPP. required: false default: None ''' From d0c607c7a40cc4f78fdde120a87c50e569349507 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 15 Feb 2016 14:17:50 -0500 Subject: [PATCH 1199/2522] added follow docs back, removed from shared --- files/blockinfile.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/files/blockinfile.py b/files/blockinfile.py index a8499547639..1ae7a0bfa63 100644 --- a/files/blockinfile.py +++ b/files/blockinfile.py @@ -96,6 +96,12 @@ description: - Create a backup file including the timestamp information so you can get the original file back if you somehow clobbered it incorrectly. + follow: + required: false + default: "no" + choices: [ "yes", "no" ] + description: + - 'This flag indicates that filesystem links, if they exist, should be followed.' """ EXAMPLES = r""" From 0af9622891426ae58b8419e2842f760e6b3fbba7 Mon Sep 17 00:00:00 2001 From: Brad Wilson Date: Mon, 15 Feb 2016 13:11:02 -0800 Subject: [PATCH 1200/2522] Issue #1668: rabbitmq_user: Ansible HEAD incorrectly treats permissions as a string instead of a list --- messaging/rabbitmq_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messaging/rabbitmq_user.py b/messaging/rabbitmq_user.py index 3714bc3099d..ba77e47c998 100644 --- a/messaging/rabbitmq_user.py +++ b/messaging/rabbitmq_user.py @@ -228,7 +228,7 @@ def main(): user=dict(required=True, aliases=['username', 'name']), password=dict(default=None), tags=dict(default=None), - permissions=dict(default=list()), + permissions=dict(default=list(), type='list'), vhost=dict(default='/'), configure_priv=dict(default='^$'), write_priv=dict(default='^$'), From 671c55da51e82edc77583f4dbd6201384ae80d52 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 16 Feb 2016 14:18:07 -0600 Subject: [PATCH 1201/2522] Indicate proxy_host and proxy_port were added in 2.1 --- packaging/elasticsearch_plugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packaging/elasticsearch_plugin.py b/packaging/elasticsearch_plugin.py index 5b7d8f89780..5b20b1337de 100644 --- a/packaging/elasticsearch_plugin.py +++ b/packaging/elasticsearch_plugin.py @@ -66,11 +66,13 @@ - Proxy host to use during plugin installation required: False default: None + version_added: "2.1" proxy_port: description: - Proxy port to use during plugin installation required: False - default: None + default: None + version_added: "2.1" version: description: - Version of the plugin to be installed. From a1af38427413dc1e2e939c150738dc751240bf91 Mon Sep 17 00:00:00 2001 From: James Moore Date: Wed, 17 Feb 2016 18:04:39 -0800 Subject: [PATCH 1202/2522] Added a source parameter for setting the JIT client name --- monitoring/sensu_check.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/monitoring/sensu_check.py b/monitoring/sensu_check.py index f7f0562f48c..e880e9239d9 100644 --- a/monitoring/sensu_check.py +++ b/monitoring/sensu_check.py @@ -156,6 +156,12 @@ - You can't rewrite others module parameters using this required: false default: {} + source: + version_added: "2.1" + description: + - The check source, used to create a JIT Sensu client for an external resource (e.g. a network switch). + required: false + default: null requirements: [ ] author: "Anders Ingemann (@andsens)" ''' @@ -251,6 +257,7 @@ def sensu_check(module, path, name, state='present', backup=False): 'aggregate', 'low_flap_threshold', 'high_flap_threshold', + 'source', ] for opt in simple_opts: if module.params[opt] is not None: @@ -353,6 +360,7 @@ def main(): 'low_flap_threshold': {'type': 'int'}, 'high_flap_threshold': {'type': 'int'}, 'custom': {'type': 'dict'}, + 'source': {'type': 'str'}, } required_together = [['subdue_begin', 'subdue_end']] From a523ce7e001adde458f6f52532550b86a802f549 Mon Sep 17 00:00:00 2001 From: Ritesh Khadgaray Date: Thu, 18 Feb 2016 17:38:38 +0530 Subject: [PATCH 1203/2522] files/blockinfile.py : ERROR: version_added for new option (follow) should be 2.1. Currently 0.0 --- files/blockinfile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/files/blockinfile.py b/files/blockinfile.py index 1ae7a0bfa63..a40f57a863c 100644 --- a/files/blockinfile.py +++ b/files/blockinfile.py @@ -102,6 +102,7 @@ choices: [ "yes", "no" ] description: - 'This flag indicates that filesystem links, if they exist, should be followed.' + version_added: "2.1" """ EXAMPLES = r""" From a1f53f3a4377f1b454754593327d06fef3219f7e Mon Sep 17 00:00:00 2001 From: Andrea Scarpino Date: Thu, 18 Feb 2016 17:59:23 +0100 Subject: [PATCH 1204/2522] Fix issue #1406 about win_firewall_rule I changed the logic here to always use 'netsh ... show rule' keywords as keys for $fwsettings map. While the translation (e.g. Enabled -> enable) is performed when invoking 'netsh ... add rule' command. I tested rule creation and rule creation when the rule was already existing on Windows Server 2012. --- windows/win_firewall_rule.ps1 | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/windows/win_firewall_rule.ps1 b/windows/win_firewall_rule.ps1 index 5012cb041da..63ac538e376 100644 --- a/windows/win_firewall_rule.ps1 +++ b/windows/win_firewall_rule.ps1 @@ -23,8 +23,8 @@ function getFirewallRule ($fwsettings) { try { - #$output = Get-NetFirewallRule -name $($fwsettings.name); - $rawoutput=@(netsh advfirewall firewall show rule name="$($fwsettings.Name)") + #$output = Get-NetFirewallRule -name $($fwsettings.'Rule Name'); + $rawoutput=@(netsh advfirewall firewall show rule name="$($fwsettings.'Rule Name')") if (!($rawoutput -eq 'No rules match the specified criteria.')){ $rawoutput | Where {$_ -match '^([^:]+):\s*(\S.*)$'} | Foreach -Begin { $FirstRun = $true; @@ -51,10 +51,10 @@ function getFirewallRule ($fwsettings) { $msg=@(); if ($($output|measure).count -gt 0) { $exists=$true; - $msg += @("The rule '" + $fwsettings.name + "' exists."); + $msg += @("The rule '" + $fwsettings.'Rule Name' + "' exists."); if ($($output|measure).count -gt 1) { $multi=$true - $msg += @("The rule '" + $fwsettings.name + "' has multiple entries."); + $msg += @("The rule '" + $fwsettings.'Rule Name' + "' has multiple entries."); ForEach($rule in $output.GetEnumerator()) { ForEach($fwsetting in $fwsettings.GetEnumerator()) { if ( $rule.$fwsetting -ne $fwsettings.$fwsetting) { @@ -73,11 +73,7 @@ function getFirewallRule ($fwsettings) { if (($fwsetting.Key -eq 'RemoteIP') -and ($output.$($fwsetting.Key) -eq ($fwsettings.$($fwsetting.Key)+'-'+$fwsettings.$($fwsetting.Key)))) { $donothing=$false - } elseif ((($fwsetting.Key -eq 'Name') -or ($fwsetting.Key -eq 'DisplayName')) -and ($output."Rule Name" -eq $fwsettings.$($fwsetting.Key))) { - $donothing=$false - } elseif (($fwsetting.Key -eq 'Profile') -and ($output."Profiles" -eq $fwsettings.$($fwsetting.Key))) { - $donothing=$false - } elseif (($fwsetting.Key -eq 'Enable') -and ($output."Enabled" -eq $fwsettings.$($fwsetting.Key))) { + } elseif (($fwsetting.Key -eq 'DisplayName') -and ($output."Rule Name" -eq $fwsettings.$($fwsetting.Key))) { $donothing=$false } else { $diff=$true; @@ -117,11 +113,17 @@ function getFirewallRule ($fwsettings) { function createFireWallRule ($fwsettings) { $msg=@() - $execString="netsh advfirewall firewall add rule " + $execString="netsh advfirewall firewall add rule" ForEach ($fwsetting in $fwsettings.GetEnumerator()) { if ($fwsetting.key -eq 'Direction') { $key='dir' + } elseif ($fwsetting.key -eq 'Rule Name') { + $key='name' + } elseif ($fwsetting.key -eq 'Enabled') { + $key='enable' + } elseif ($fwsetting.key -eq 'Profiles') { + $key='profile' } else { $key=$($fwsetting.key).ToLower() }; @@ -159,7 +161,7 @@ function createFireWallRule ($fwsettings) { function removeFireWallRule ($fwsettings) { $msg=@() try { - $rawoutput=@(netsh advfirewall firewall delete rule name="$($fwsettings.name)") + $rawoutput=@(netsh advfirewall firewall delete rule name="$($fwsettings.'Rule Name')") $rawoutput | Where {$_ -match '^([^:]+):\s*(\S.*)$'} | Foreach -Begin { $FirstRun = $true; $HashProps = @{}; @@ -211,9 +213,9 @@ $misArg = '' # Check the arguments if ($enable -ne $null) { if ($enable -eq $true) { - $fwsettings.Add("Enable", "yes"); + $fwsettings.Add("Enabled", "yes"); } elseif ($enable -eq $false) { - $fwsettings.Add("Enable", "no"); + $fwsettings.Add("Enabled", "no"); } else { $misArg+="enable"; $msg+=@("for the enable parameter only yes and no is allowed"); @@ -229,7 +231,7 @@ if ($name -eq ""){ $misArg+="Name"; $msg+=@("name is a required argument"); } else { - $fwsettings.Add("Name", $name) + $fwsettings.Add("Rule Name", $name) #$fwsettings.Add("displayname", $name) }; if ((($direction.ToLower() -ne "In") -And ($direction.ToLower() -ne "Out")) -And ($state -eq "present")){ @@ -263,7 +265,7 @@ foreach ($arg in $args){ }; $winprofile=Get-Attr $params "profile" "current"; -$fwsettings.Add("profile", $winprofile) +$fwsettings.Add("Profiles", $winprofile) if ($misArg){ $result=New-Object psobject @{ From 1a00da7c4919db0c964150304f80fafc1a95b115 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 18 Feb 2016 05:17:23 -0800 Subject: [PATCH 1205/2522] minor docfixes --- network/a10/a10_server.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/network/a10/a10_server.py b/network/a10/a10_server.py index 2ad66c23588..3c3ab5dbbc7 100644 --- a/network/a10/a10_server.py +++ b/network/a10/a10_server.py @@ -36,42 +36,32 @@ description: - hostname or ip of your A10 Networks device required: true - default: null - aliases: [] - choices: [] username: description: - admin account of your A10 Networks device required: true - default: null aliases: ['user', 'admin'] - choices: [] password: description: - admin password of your A10 Networks device required: true - default: null aliases: ['pass', 'pwd'] - choices: [] server_name: description: - slb server name required: true - default: null aliases: ['server'] - choices: [] server_ip: description: - slb server IP address required: false default: null aliases: ['ip', 'address'] - choices: [] server_status: description: - slb virtual server status required: false - default: enable + default: enabled aliases: ['status'] choices: ['enabled', 'disabled'] server_ports: @@ -82,14 +72,11 @@ required when C(state) is C(present). required: false default: null - aliases: [] - choices: [] state: description: - create, update or remove slb server required: false default: present - aliases: [] choices: ['present', 'absent'] ''' From 950dfc3d21fa56e54e9360d795f3e942665a019c Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 18 Feb 2016 09:27:15 -0800 Subject: [PATCH 1206/2522] initial addition of issue/pr temlpates --- ISSUE_TEMPLATE.md | 53 ++++++++++++++++++++++++++++++++++++++++ PULL_REQUEST_TEMPLATE.md | 31 +++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 ISSUE_TEMPLATE.md create mode 100644 PULL_REQUEST_TEMPLATE.md diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 00000000000..0dcb075b7d6 --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,53 @@ +##### Issue Type: + +Please pick one and delete the rest: + - Bug Report + - Feature Idea + - Documentation Report + +##### Plugin Name: + +Name of the plugin/module/task + +##### Ansible Version: + +``` +(Paste verbatim output from “ansible --version” here) +``` + +##### Ansible Configuration: + +Please mention any settings you've changed/added/removed in ansible.cfg +(or using the ANSIBLE_* environment variables). + +##### Environment: + +Please mention the OS you are running Ansible from, and the OS you are +managing, or say “N/A” for anything that isn't platform-specific. + +##### Summary: + +Please explain the problem briefly. + +##### Steps To Reproduce: + +For bugs, please show exactly how to reproduce the problem. For new +features, show how the feature would be used. + +``` +(Paste example playbooks or commands here) +``` + +You can also paste gist.github.com links for larger files. + +##### Expected Results: + +What did you expect to happen when running the steps above? + +##### Actual Results: + +What actually happened? + +``` +(Paste verbatim command output here) +``` diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..8f3791f3c91 --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,31 @@ +##### Issue Type: + +Please pick one and delete the rest: + - Feature Pull Request + - New Module Pull Request + - Bugfix Pull Request + - Docs Pull Request + +##### Plugin Name: + +Name of the plugin/module/task + +##### Ansible Version: + +``` +(Paste verbatim output from “ansible --version” here) +``` + +##### Summary: + +Please describe the change and the reason for it. + +(If you're fixing an existing issue, please include "Fixes #nnn" in your +commit message and your description; but you should still explain what +the change does.) + +##### Example output: + +``` +(Paste verbatim command output here if necessary) +``` From 3690366be32fa7a3dbfae47a1ef5886f55c95019 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 18 Feb 2016 09:28:02 -0800 Subject: [PATCH 1207/2522] now point to local template --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 38b95840a77..bd06fcd804f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,7 +30,7 @@ The Github issue tracker is not the best place for questions for various reasons If you'd like to file a bug =========================== -Read the community page above, but in particular, make sure you copy [this issue template](https://github.com/ansible/ansible/blob/devel/ISSUE_TEMPLATE.md) into your ticket description. We have a friendly neighborhood bot that will remind you if you forget :) This template helps us organize tickets faster and prevents asking some repeated questions, so it's very helpful to us and we appreciate your help with it. +Read the community page above, but in particular, make sure you copy [this issue template](https://github.com/ansible/ansible-modules-extras/blob/devel/ISSUE_TEMPLATE.md) into your ticket description. We have a friendly neighborhood bot that will remind you if you forget :) This template helps us organize tickets faster and prevents asking some repeated questions, so it's very helpful to us and we appreciate your help with it. Also please make sure you are testing on the latest released version of Ansible or the development branch. From da8c9b56a9014c8571f1d055ce3bf1b10e22b778 Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Fri, 19 Feb 2016 02:09:51 +0100 Subject: [PATCH 1208/2522] Fix typo --- network/snmp_facts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/snmp_facts.py b/network/snmp_facts.py index f89b98b1db3..28546dfc71d 100644 --- a/network/snmp_facts.py +++ b/network/snmp_facts.py @@ -21,7 +21,7 @@ module: snmp_facts version_added: "1.9" author: "Patrick Ogenstad (@ogenstad)" -short_description: Retrive facts for a device using SNMP. +short_description: Retrieve facts for a device using SNMP. description: - Retrieve facts for a device using SNMP, the facts will be inserted to the ansible_facts key. From 362760413fc2710ac50eab323e01e7bfb0076dee Mon Sep 17 00:00:00 2001 From: Ton Kersten Date: Fri, 19 Feb 2016 10:43:55 +0100 Subject: [PATCH 1209/2522] Fix facter path --- system/facter.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/system/facter.py b/system/facter.py index 6c09877fcbe..b594836df9c 100644 --- a/system/facter.py +++ b/system/facter.py @@ -47,7 +47,10 @@ def main(): argument_spec = dict() ) - cmd = ["/usr/bin/env", "facter", "--puppet", "--json"] + facter_path = module.get_bin_path('facter', opt_dirs=['/opt/puppetlabs/bin']) + + cmd = [facter_path, "--puppet", "--json"] + rc, out, err = module.run_command(cmd, check_rc=True) module.exit_json(**json.loads(out)) From 278b9870170009d67434e03efe5a51d4fd812f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Mart=C3=ADn?= Date: Fri, 19 Feb 2016 20:01:08 +0100 Subject: [PATCH 1210/2522] Replace deprecated zabbix api method 'exists' to support zabbix 3.0 --- monitoring/zabbix_host.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monitoring/zabbix_host.py b/monitoring/zabbix_host.py index 5b6748a3e94..d0e0fdc61e9 100644 --- a/monitoring/zabbix_host.py +++ b/monitoring/zabbix_host.py @@ -162,13 +162,13 @@ def __init__(self, module, zbx): # exist host def is_host_exist(self, host_name): - result = self._zapi.host.exists({'host': host_name}) + result = self._zapi.host.get({'filter': {'host': host_name}}) return result # check if host group exists def check_host_group_exist(self, group_names): for group_name in group_names: - result = self._zapi.hostgroup.exists({'name': group_name}) + result = self._zapi.hostgroup.get({'filter': {'name': group_name}}) if not result: self._module.fail_json(msg="Hostgroup not found: %s" % group_name) return True From 31ecde6b87ddbf39785b4fceacd0319e053ddc4e Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Sun, 21 Feb 2016 20:13:07 -0500 Subject: [PATCH 1211/2522] Fix documentation for sns_topic module Currently the documentation does not correspond to the Ansible standards for module documentation. This should bring it into compliance. --- cloud/amazon/sns_topic.py | 56 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/sns_topic.py b/cloud/amazon/sns_topic.py index 92d63d02c18..9b4ae3f0516 100755 --- a/cloud/amazon/sns_topic.py +++ b/cloud/amazon/sns_topic.py @@ -1,5 +1,19 @@ #!/usr/bin/python # -*- coding: utf-8 -*- +# +# This is a free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This Ansible library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this library. If not, see . + DOCUMENTATION = """ module: sns_topic @@ -12,30 +26,35 @@ name: description: - The name or ARN of the SNS topic to converge - required: true + required: True state: description: - Whether to create or destroy an SNS topic - required: false + required: False default: present choices: ["absent", "present"] display_name: description: - Display name of the topic required: False + default: None policy: description: - Policy to apply to the SNS topic required: False + default: None delivery_policy: description: - Delivery policy to apply to the SNS topic required: False + default: None subscriptions: description: - List of subscriptions to apply to the topic. Note that AWS requires subscriptions to be confirmed, so you will need to confirm any new subscriptions. + required: False + default: [] purge_subscriptions: description: - "Whether to purge any subscriptions not listed here. NOTE: AWS does not @@ -43,6 +62,7 @@ exist and would be purged, they are silently skipped. This means that somebody could come back later and confirm the subscription. Sorry. Blame Amazon." + required: False default: True extends_documentation_fragment: aws requirements: [ "boto" ] @@ -74,6 +94,38 @@ """ +RETURN = ''' +topic_created: + description: Whether the topic was newly created + type: bool + returned: changed and state == present + sample: True + +attributes_set: + description: The attributes which were changed + type: list + returned: state == "present" + sample: ["policy", "delivery_policy"] + +subscriptions_added: + description: The subscriptions added to the topic + type: list + returned: state == "present" + sample: [["sms", "my_mobile_number"], ["sms", "my_mobile_2"]] + +subscriptions_deleted: + description: The subscriptions deleted from the topic + type: list + returned: state == "present" + sample: [["sms", "my_mobile_number"], ["sms", "my_mobile_2"]] + +sns_arn: + description: The ARN of the topic you are modifying + type: string + returned: state == "present" + sample: "arn:aws:sns:us-east-1:123456789012:my_topic_name" +''' + import sys import time import json From 526ee48c0dca015bb2b3e97359db870f0d98065a Mon Sep 17 00:00:00 2001 From: Nick Aslanidis Date: Mon, 22 Feb 2016 16:16:54 +1000 Subject: [PATCH 1212/2522] New AWS module for managing ec2 VPC virtual gateways --- cloud/amazon/ec2_vpc_vgw.py | 554 ++++++++++++++++++++++++++++++++++++ 1 file changed, 554 insertions(+) create mode 100644 cloud/amazon/ec2_vpc_vgw.py diff --git a/cloud/amazon/ec2_vpc_vgw.py b/cloud/amazon/ec2_vpc_vgw.py new file mode 100644 index 00000000000..0ce52b76ff8 --- /dev/null +++ b/cloud/amazon/ec2_vpc_vgw.py @@ -0,0 +1,554 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +module: ec2_vpc_vgw +short_description: Create and delete AWS VPN Virtual Gateways. +description: + - Creates AWS VPN Virtual Gateways + - Deletes AWS VPN Virtual Gateways + - Attaches Virtual Gateways to VPCs + - Detaches Virtual Gateways from VPCs +version_added: "2.1" +requirements: [ boto3 ] +options: + state: + description: + - present to ensure resource is created. + - absent to remove resource + required: false + default: present + choices: [ "present", "absent"] + name: + description: + - name of the vgw to be created or deleted + required: + - true when combined with a state of 'present' + - false when combined with a state of 'absent' + type: + description: + - type of the virtual gateway to be created + required: + - true when combined with a state of 'present' + - false when combined with a state of 'absent' + vpn_gateway_id: + description: + - vpn gateway id of an existing virtual gateway + required: false + vpc_id: + description: + - the vpc-id of a vpc to attach or detach + required: false + wait_timeout: + description: + - number of seconds to wait for status during vpc attach and detach + required: false + default: 320 + tags: + description: + - dictionary of resource tags of the form: { tag1: value1, tag2: value2 } + required: false +author: Nick Aslanidis (@naslanidis) +extends_documentation_fragment: aws +''' + +EXAMPLES = ''' +- name: Create a new vgw attached to a specific VPC + ec2_vpc_vgw: + state: present + region: ap-southeast-2 + profile: personal + vpc_id: vpc-12345678 + name: personal-testing + type: ipsec.1 + register: created_vgw + +- name: Create a new unattached vgw + ec2_vpc_vgw: + state: present + region: ap-southeast-2 + profile: personal + name: personal-testing + type: ipsec.1 + tags: + environment: production + owner: ABC + register: created_vgw + +- name: Remove a new vgw using the name + ec2_vpc_vgw: + state: absent + region: ap-southeast-2 + profile: personal + name: personal-testing + type: ipsec.1 + register: deleted_vgw + +- name: Remove a new vgw using the vpn_gateway_id + ec2_vpc_vgw: + state: absent + region: ap-southeast-2 + profile: personal + vpn_gateway_id: vgw-3a9aa123 + register: deleted_vgw +''' + +try: + import json + import time + import botocore + import boto3 + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + + +def wait_for_status(client, module, vpn_gateway_id, status): + polling_increment_secs = 15 + max_retries = (module.params.get('wait_timeout') / polling_increment_secs) + status_achieved = False + + for x in range(0, max_retries): + try: + response = find_vgw(client, module, vpn_gateway_id) + if response[0]['VpcAttachments'][0]['State'] == status: + status_achieved = True + break + else: + time.sleep(polling_increment_secs) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + + result = response + return status_achieved, result + + +def attach_vgw(client, module, vpn_gateway_id): + params = dict() + params['VpcId'] = module.params.get('vpc_id') + + try: + response = client.attach_vpn_gateway(VpnGatewayId=vpn_gateway_id, VpcId=params['VpcId']) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + + status_achieved, vgw = wait_for_status(client, module, [vpn_gateway_id], 'attached') + if not status_achieved: + module.fail_json(msg='Error waiting for vpc to attach to vgw - please check the AWS console') + + result = response + return result + + +def detach_vgw(client, module, vpn_gateway_id, vpc_id=None): + params = dict() + params['VpcId'] = module.params.get('vpc_id') + + if vpc_id: + try: + response = client.detach_vpn_gateway(VpnGatewayId=vpn_gateway_id, VpcId=vpc_id) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + else: + try: + response = client.detach_vpn_gateway(VpnGatewayId=vpn_gateway_id, VpcId=params['VpcId']) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + + status_achieved, vgw = wait_for_status(client, module, [vpn_gateway_id], 'detached') + if not status_achieved: + module.fail_json(msg='Error waiting for vpc to detach from vgw - please check the AWS console') + + result = response + return result + + +def create_vgw(client, module): + params = dict() + params['Type'] = module.params.get('type') + + try: + response = client.create_vpn_gateway(Type=params['Type']) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + + result = response + return result + + +def delete_vgw(client, module, vpn_gateway_id): + + try: + response = client.delete_vpn_gateway(VpnGatewayId=vpn_gateway_id) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + + #return the deleted VpnGatewayId as this is not included in the above response + result = vpn_gateway_id + return result + + +def create_tags(client, module, vpn_gateway_id): + params = dict() + + try: + response = client.create_tags(Resources=[vpn_gateway_id],Tags=load_tags(module)) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + + result = response + return result + + +def delete_tags(client, module, vpn_gateway_id, tags_to_delete=None): + params = dict() + + if tags_to_delete: + try: + response = client.delete_tags(Resources=[vpn_gateway_id], Tags=tags_to_delete) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + else: + try: + response = client.delete_tags(Resources=[vpn_gateway_id]) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + + result = response + return result + + +def load_tags(module): + tags = [] + + if module.params.get('tags'): + for name, value in module.params.get('tags').iteritems(): + tags.append({'Key': name, 'Value': str(value)}) + tags.append({'Key': "Name", 'Value': module.params.get('name')}) + else: + tags.append({'Key': "Name", 'Value': module.params.get('name')}) + return tags + + +def find_tags(client, module, resource_id=None): + + if resource_id: + try: + response = client.describe_tags(Filters=[ + {'Name': 'resource-id', 'Values': [resource_id]} + ]) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + + result = response + return result + + +def check_tags(client, module, existing_vgw, vpn_gateway_id): + params = dict() + params['Tags'] = module.params.get('tags') + vgw = existing_vgw + changed = False + tags_list = {} + + #format tags for comparison + for tags in existing_vgw[0]['Tags']: + if tags['Key'] != 'Name': + tags_list[tags['Key']] = tags['Value'] + + # if existing tags don't match the tags arg, delete existing and recreate with new list + if params['Tags'] != None and tags_list != params['Tags']: + delete_tags(client, module, vpn_gateway_id) + create_tags(client, module, vpn_gateway_id) + vgw = find_vgw(client, module) + changed = True + + #if no tag args are supplied, delete any existing tags with the exception of the name tag + if params['Tags'] == None and tags_list != {}: + tags_to_delete = [] + for tags in existing_vgw[0]['Tags']: + if tags['Key'] != 'Name': + tags_to_delete.append(tags) + + delete_tags(client, module, vpn_gateway_id, tags_to_delete) + vgw = find_vgw(client, module) + changed = True + + return vgw, changed + + +def find_vpc(client, module): + params = dict() + params['vpc_id'] = module.params.get('vpc_id') + + if params['vpc_id']: + try: + response = client.describe_vpcs(VpcIds=[params['vpc_id']]) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + + result = response + return result + + +def find_vgw(client, module, vpn_gateway_id=None): + params = dict() + params['Name'] = module.params.get('name') + params['Type'] = module.params.get('type') + params['State'] = module.params.get('state') + + if params['State'] == 'present': + try: + response = client.describe_vpn_gateways(Filters=[ + {'Name': 'type', 'Values': [params['Type']]}, + {'Name': 'tag:Name', 'Values': [params['Name']]} + ]) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + + else: + if vpn_gateway_id: + try: + response = client.describe_vpn_gateways(VpnGatewayIds=vpn_gateway_id) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + + else: + try: + response = client.describe_vpn_gateways(Filters=[ + {'Name': 'type', 'Values': [params['Type']]}, + {'Name': 'tag:Name', 'Values': [params['Name']]} + ]) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + + result = response['VpnGateways'] + return result + + +def ensure_vgw_present(client, module): + +# If an existing vgw name and type matches our args, then a match is considered to have been +# found and we will not create another vgw. + + changed = False + params = dict() + result = dict() + params['Name'] = module.params.get('name') + params['VpcId'] = module.params.get('vpc_id') + params['Type'] = module.params.get('type') + params['Tags'] = module.params.get('tags') + params['VpnGatewayIds'] = module.params.get('vpn_gateway_id') + + # Check that a name argument has been supplied. + if not module.params.get('name'): + module.fail_json(msg='A name is required when a status of \'present\' is suppled') + + # check if a gateway matching our module args already exists + existing_vgw = find_vgw(client, module) + + if existing_vgw != [] and existing_vgw[0]['State'] != 'deleted': + vpn_gateway_id = existing_vgw[0]['VpnGatewayId'] + vgw, changed = check_tags(client, module, existing_vgw, vpn_gateway_id) + + if not changed: + + # if a vpc_id was provided, check if it exists and if it's attached + if params['VpcId']: + + # check that the vpc_id exists. If not, an exception is thrown + vpc = find_vpc(client, module) + current_vpc_attachments = existing_vgw[0]['VpcAttachments'] + + if current_vpc_attachments != [] and current_vpc_attachments[0]['State'] == 'attached': + if current_vpc_attachments[0]['VpcId'] == params['VpcId'] and current_vpc_attachments[0]['State'] == 'attached': + changed = False + else: + + # detach the existing vpc from the virtual gateway + vpc_to_detach = current_vpc_attachments[0]['VpcId'] + detach_vgw(client, module, vpn_gateway_id, vpc_to_detach) + time.sleep(5) + attached_vgw = attach_vgw(client, module, vpn_gateway_id) + changed = True + else: + # attach the vgw to the supplied vpc + attached_vgw = attach_vgw(client, module, vpn_gateway_id) + changed = True + + # if params['VpcId'] is not provided, check the vgw is attached to a vpc. if so, detach it. + else: + existing_vgw = find_vgw(client, module, [vpn_gateway_id]) + + if existing_vgw[0]['VpcAttachments'] != []: + if existing_vgw[0]['VpcAttachments'][0]['State'] == 'attached': + # detach the vpc from the vgw + vpc_to_detach = existing_vgw[0]['VpcAttachments'][0]['VpcId'] + detach_vgw(client, module, vpn_gateway_id, vpc_to_detach) + changed = True + + vgw = find_vgw(client, module, [vpn_gateway_id]) + + else: + # create a new vgw + new_vgw = create_vgw(client, module) + changed = True + vpn_gateway_id = new_vgw['VpnGateway']['VpnGatewayId'] + + # tag the new virtual gateway + create_tags(client, module, vpn_gateway_id) + + # return current state of the vgw + vgw = find_vgw(client, module, [vpn_gateway_id]) + + # if a vpc-id was supplied, attempt to attach it to the vgw + if params['VpcId']: + attached_vgw = attach_vgw(client, module, vpn_gateway_id) + changed = True + vgw = find_vgw(client, module, [vpn_gateway_id]) + + result = vgw + return changed, result + + +def ensure_vgw_absent(client, module): + +# If an existing vgw name and type matches our args, then a match is considered to have been +# found and we will take steps to delete it. + + changed = False + params = dict() + result = dict() + params['Name'] = module.params.get('name') + params['VpcId'] = module.params.get('vpc_id') + params['Type'] = module.params.get('type') + params['Tags'] = module.params.get('tags') + params['VpnGatewayIds'] = module.params.get('vpn_gateway_id') + + # check if a gateway matching our module args already exists + if params['VpnGatewayIds']: + existing_vgw_with_id = find_vgw(client, module, [params['VpnGatewayIds']]) + if existing_vgw_with_id != [] and existing_vgw_with_id[0]['State'] != 'deleted': + existing_vgw = existing_vgw_with_id + if existing_vgw[0]['VpcAttachments'] != [] and existing_vgw[0]['VpcAttachments'][0]['State'] == 'attached': + if params['VpcId']: + if params['VpcId'] != existing_vgw[0]['VpcAttachments'][0]['VpcId']: + module.fail_json(msg='The vpc-id provided does not match the vpc-id currently attached - please check the AWS console') + + else: + # detach the vpc from the vgw + detach_vgw(client, module, params['VpnGatewayIds'], params['VpcId']) + deleted_vgw = delete_vgw(client, module, params['VpnGatewayIds']) + changed = True + + else: + # attempt to detach any attached vpcs + vpc_to_detach = existing_vgw[0]['VpcAttachments'][0]['VpcId'] + detach_vgw(client, module, params['VpnGatewayIds'], vpc_to_detach) + deleted_vgw = delete_vgw(client, module, params['VpnGatewayIds']) + changed = True + + else: + # no vpc's are attached so attempt to delete the vgw + deleted_vgw = delete_vgw(client, module, params['VpnGatewayIds']) + changed = True + + else: + changed = False + deleted_vgw = None + + else: + #Check that a name and type argument has been supplied if no vgw-id + if not module.params.get('name') or not module.params.get('type'): + module.fail_json(msg='A name and type is required when no vgw-id and a status of \'absent\' is suppled') + + existing_vgw = find_vgw(client, module) + if existing_vgw != [] and existing_vgw[0]['State'] != 'deleted': + vpn_gateway_id = existing_vgw[0]['VpnGatewayId'] + if existing_vgw[0]['VpcAttachments'] != [] and existing_vgw[0]['VpcAttachments'][0]['State'] == 'attached': + if params['VpcId']: + if params['VpcId'] != existing_vgw[0]['VpcAttachments'][0]['VpcId']: + module.fail_json(msg='The vpc-id provided does not match the vpc-id currently attached - please check the AWS console') + + else: + # detach the vpc from the vgw + detach_vgw(client, module, vpn_gateway_id, params['VpcId']) + + #now that the vpc has been detached, delete the vgw + deleted_vgw = delete_vgw(client, module, vpn_gateway_id) + changed = True + + else: + # attempt to detach any attached vpcs + vpc_to_detach = existing_vgw[0]['VpcAttachments'][0]['VpcId'] + detach_vgw(client, module, vpn_gateway_id, vpc_to_detach) + changed = True + + #now that the vpc has been detached, delete the vgw + deleted_vgw = delete_vgw(client, module, vpn_gateway_id) + + else: + # no vpc's are attached so attempt to delete the vgw + deleted_vgw = delete_vgw(client, module, vpn_gateway_id) + changed = True + + else: + changed = False + deleted_vgw = None + + result = deleted_vgw + return changed, result + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + state=dict(default='present', choices=['present', 'absent']), + region=dict(required=True), + name=dict(), + vpn_gateway_id=dict(), + vpc_id=dict(), + wait_timeout=dict(type='int', default=320, required=False), + type=dict(), + tags=dict(), + ) + ) + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO3: + module.fail_json(msg='json and boto3 is required.') + + state = module.params.get('state').lower() + + try: + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + client = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_kwargs) + except botocore.exceptions.NoCredentialsError, e: + module.fail_json(msg="Can't authorize connection - "+str(e)) + + if state == 'present': + (changed, results) = ensure_vgw_present(client, module) + else: + (changed, results) = ensure_vgw_absent(client, module) + module.exit_json(changed=changed, result=results) + + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From 20df1189b98e78afbe2cf910e29c6da514263099 Mon Sep 17 00:00:00 2001 From: naslanidis Date: Mon, 22 Feb 2016 20:32:18 +1000 Subject: [PATCH 1213/2522] Fixed issue with tag changes affecting vpc attach --- cloud/amazon/ec2_vpc_vgw.py | 76 ++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/cloud/amazon/ec2_vpc_vgw.py b/cloud/amazon/ec2_vpc_vgw.py index 0ce52b76ff8..4c7bebd323a 100644 --- a/cloud/amazon/ec2_vpc_vgw.py +++ b/cloud/amazon/ec2_vpc_vgw.py @@ -148,7 +148,7 @@ def attach_vgw(client, module, vpn_gateway_id): status_achieved, vgw = wait_for_status(client, module, [vpn_gateway_id], 'attached') if not status_achieved: module.fail_json(msg='Error waiting for vpc to attach to vgw - please check the AWS console') - + result = response return result @@ -195,7 +195,7 @@ def delete_vgw(client, module, vpn_gateway_id): response = client.delete_vpn_gateway(VpnGatewayId=vpn_gateway_id) except botocore.exceptions.ClientError as e: module.fail_json(msg=str(e)) - + #return the deleted VpnGatewayId as this is not included in the above response result = vpn_gateway_id return result @@ -318,7 +318,7 @@ def find_vgw(client, module, vpn_gateway_id=None): ]) except botocore.exceptions.ClientError as e: module.fail_json(msg=str(e)) - + else: if vpn_gateway_id: try: @@ -334,7 +334,7 @@ def find_vgw(client, module, vpn_gateway_id=None): ]) except botocore.exceptions.ClientError as e: module.fail_json(msg=str(e)) - + result = response['VpnGateways'] return result @@ -363,44 +363,44 @@ def ensure_vgw_present(client, module): if existing_vgw != [] and existing_vgw[0]['State'] != 'deleted': vpn_gateway_id = existing_vgw[0]['VpnGatewayId'] vgw, changed = check_tags(client, module, existing_vgw, vpn_gateway_id) - - if not changed: - - # if a vpc_id was provided, check if it exists and if it's attached - if params['VpcId']: - - # check that the vpc_id exists. If not, an exception is thrown - vpc = find_vpc(client, module) - current_vpc_attachments = existing_vgw[0]['VpcAttachments'] - - if current_vpc_attachments != [] and current_vpc_attachments[0]['State'] == 'attached': - if current_vpc_attachments[0]['VpcId'] == params['VpcId'] and current_vpc_attachments[0]['State'] == 'attached': - changed = False - else: - - # detach the existing vpc from the virtual gateway - vpc_to_detach = current_vpc_attachments[0]['VpcId'] - detach_vgw(client, module, vpn_gateway_id, vpc_to_detach) - time.sleep(5) - attached_vgw = attach_vgw(client, module, vpn_gateway_id) - changed = True + + # if a vpc_id was provided, check if it exists and if it's attached + if params['VpcId']: + + # check that the vpc_id exists. If not, an exception is thrown + vpc = find_vpc(client, module) + current_vpc_attachments = existing_vgw[0]['VpcAttachments'] + + if current_vpc_attachments != [] and current_vpc_attachments[0]['State'] == 'attached': + if current_vpc_attachments[0]['VpcId'] == params['VpcId'] and current_vpc_attachments[0]['State'] == 'attached': + changed = False else: - # attach the vgw to the supplied vpc + + # detach the existing vpc from the virtual gateway + vpc_to_detach = current_vpc_attachments[0]['VpcId'] + detach_vgw(client, module, vpn_gateway_id, vpc_to_detach) + time.sleep(5) attached_vgw = attach_vgw(client, module, vpn_gateway_id) + vgw = find_vgw(client, module, [vpn_gateway_id]) changed = True - - # if params['VpcId'] is not provided, check the vgw is attached to a vpc. if so, detach it. else: - existing_vgw = find_vgw(client, module, [vpn_gateway_id]) + # attach the vgw to the supplied vpc + attached_vgw = attach_vgw(client, module, vpn_gateway_id) + vgw = find_vgw(client, module, [vpn_gateway_id]) + changed = True - if existing_vgw[0]['VpcAttachments'] != []: - if existing_vgw[0]['VpcAttachments'][0]['State'] == 'attached': - # detach the vpc from the vgw - vpc_to_detach = existing_vgw[0]['VpcAttachments'][0]['VpcId'] - detach_vgw(client, module, vpn_gateway_id, vpc_to_detach) - changed = True + # if params['VpcId'] is not provided, check the vgw is attached to a vpc. if so, detach it. + else: + existing_vgw = find_vgw(client, module, [vpn_gateway_id]) - vgw = find_vgw(client, module, [vpn_gateway_id]) + if existing_vgw[0]['VpcAttachments'] != []: + if existing_vgw[0]['VpcAttachments'][0]['State'] == 'attached': + # detach the vpc from the vgw + vpc_to_detach = existing_vgw[0]['VpcAttachments'][0]['VpcId'] + detach_vgw(client, module, vpn_gateway_id, vpc_to_detach) + changed = True + + vgw = find_vgw(client, module, [vpn_gateway_id]) else: # create a new vgw @@ -468,7 +468,7 @@ def ensure_vgw_absent(client, module): else: changed = False - deleted_vgw = None + deleted_vgw = "Nothing to do" else: #Check that a name and type argument has been supplied if no vgw-id @@ -551,4 +551,4 @@ def main(): from ansible.module_utils.ec2 import * if __name__ == '__main__': - main() + main() \ No newline at end of file From b4163e52c55c789897cf0f1ebde357160716b9ab Mon Sep 17 00:00:00 2001 From: naslanidis Date: Mon, 22 Feb 2016 21:29:29 +1000 Subject: [PATCH 1214/2522] Added return section to the documentation --- cloud/amazon/ec2_vpc_vgw.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cloud/amazon/ec2_vpc_vgw.py b/cloud/amazon/ec2_vpc_vgw.py index 4c7bebd323a..59336b11607 100644 --- a/cloud/amazon/ec2_vpc_vgw.py +++ b/cloud/amazon/ec2_vpc_vgw.py @@ -106,6 +106,13 @@ register: deleted_vgw ''' +RETURN = ''' +result: + description: The result of the create, or delete action. + returned: success + type: dictionary +''' + try: import json import time From f881eb10d9dde41473fa1addac4277eae944c50a Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 22 Feb 2016 11:14:40 -0600 Subject: [PATCH 1215/2522] Add actual version to version_added for svc module --- system/svc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/svc.py b/system/svc.py index 36b4df5f9f7..e060e7cca20 100644 --- a/system/svc.py +++ b/system/svc.py @@ -22,7 +22,7 @@ --- module: svc author: "Brian Coca (@bcoca)" -version_added: +version_added: "1.9" short_description: Manage daemontools services. description: - Controls daemontools services on remote hosts using the svc utility. From 1862a813db43d345bbdf9b94a9bda2858aa155c4 Mon Sep 17 00:00:00 2001 From: Travis J Parker Date: Mon, 22 Feb 2016 12:14:03 -0800 Subject: [PATCH 1216/2522] fixes documented command for svc sending SIGUSR1 --- system/svc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/svc.py b/system/svc.py index e060e7cca20..0d3a83f6305 100644 --- a/system/svc.py +++ b/system/svc.py @@ -38,7 +38,7 @@ - C(Started)/C(stopped) are idempotent actions that will not run commands unless necessary. C(restarted) will always bounce the svc (svc -t) and C(killed) will always bounce the svc (svc -k). - C(reloaded) will send a sigusr1 (svc -u). + C(reloaded) will send a sigusr1 (svc -1). C(once) will run a normally downed svc once (svc -o), not really an idempotent operation. downed: From fbf9da21195cd0fda979e5aacf4c1f8ede922f2f Mon Sep 17 00:00:00 2001 From: Fernando J Pando Date: Mon, 22 Feb 2016 17:53:25 -0500 Subject: [PATCH 1217/2522] Fix SNS topic attribute typo Enables adding SNS topic policy. 'Policy' attribute is capitalized. --- cloud/amazon/sns_topic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/sns_topic.py b/cloud/amazon/sns_topic.py index 9b4ae3f0516..567ae607ba2 100755 --- a/cloud/amazon/sns_topic.py +++ b/cloud/amazon/sns_topic.py @@ -252,7 +252,7 @@ def main(): connection.set_topic_attributes(arn_topic, 'DisplayName', display_name) - if policy and policy != json.loads(topic_attributes['policy']): + if policy and policy != json.loads(topic_attributes['Policy']): changed = True attributes_set.append('policy') if not check_mode: From 6193ed4b0d75c8bfa4e65440a87413a23c9a514a Mon Sep 17 00:00:00 2001 From: Emilien Macchi Date: Mon, 8 Feb 2016 21:04:08 -0500 Subject: [PATCH 1218/2522] system/puppet: add --certname parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit certname [1] can be a useful parameter when we need to specify a certificate name different from the default value [2] in Puppet. Ex: the hosts have different names, in advanced network isolation setups. Also, it can be used when we want to run Puppet with a specific node definition and not using hostname or fqdn to match the nodes where we want to run Puppet [3] (not recommended by Puppetlabs though). [1] https://docs.puppetlabs.com/puppet/latest/reference/configuration.html#certname [2] Defaults to the node’s fully qualified domain name [3] http://docs.puppetlabs.com/puppet/latest/reference/lang_node_definitions.html#naming --- system/puppet.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/system/puppet.py b/system/puppet.py index c26b96db6df..16dad06a533 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -74,6 +74,12 @@ default: stdout choices: [ 'stdout', 'syslog' ] version_added: "2.1" + certname: + description: + - The name to use when handling certificates. + required: false + default: None + version_added: "2.1" requirements: [ puppet ] author: "Monty Taylor (@emonty)" ''' @@ -87,6 +93,9 @@ # Run puppet using a different environment - puppet: environment=testing + +# Run puppet using a specific certname +- puppet: certname=agent01.example.com ''' @@ -127,6 +136,7 @@ def main(): facts=dict(default=None), facter_basename=dict(default='ansible'), environment=dict(required=False, default=None), + certname=dict(required=False, default=None), ), supports_check_mode=True, mutually_exclusive=[ @@ -189,6 +199,8 @@ def main(): cmd += " --show_diff" if p['environment']: cmd += " --environment '%s'" % p['environment'] + if p['certname']: + cmd += " --certname='%s'" % p['certname'] if module.check_mode: cmd += " --noop" else: @@ -199,6 +211,8 @@ def main(): cmd += "--logdest syslog " if p['environment']: cmd += "--environment '%s' " % p['environment'] + if p['certname']: + cmd += " --certname='%s'" % p['certname'] if module.check_mode: cmd += "--noop " else: From b8bc7ed9fff82e5f5ddd8c67c5c94bf0d64f8aac Mon Sep 17 00:00:00 2001 From: Justin Good Date: Tue, 9 Feb 2016 12:41:30 +0000 Subject: [PATCH 1219/2522] Add support for recursive znode deletion --- clustering/znode.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/clustering/znode.py b/clustering/znode.py index d5913c772b3..aff1cd1d224 100644 --- a/clustering/znode.py +++ b/clustering/znode.py @@ -50,6 +50,12 @@ - The amount of time to wait for a node to appear. default: 300 required: false + recursive: + description: + - Recursively delete node and all its children. + default: False + required: false + version_added: "2.1" requirements: - kazoo >= 2.1 - python >= 2.6 @@ -90,7 +96,8 @@ def main(): value=dict(required=False, default=None, type='str'), op=dict(required=False, default=None, choices=['get', 'wait', 'list']), state=dict(choices=['present', 'absent']), - timeout=dict(required=False, default=300, type='int') + timeout=dict(required=False, default=300, type='int'), + recursive=dict(required=False, default=False, type='bool') ), supports_check_mode=False ) @@ -175,7 +182,7 @@ def wait(self): def _absent(self, znode): if self.exists(znode): - self.zk.delete(znode) + self.zk.delete(znode, recursive=self.module.params['recursive']) return True, {'changed': True, 'msg': 'The znode was deleted.'} else: return True, {'changed': False, 'msg': 'The znode does not exist.'} From 4798b53b0637170fea31073e1fd8d48db419b96f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darek=20Kaczy=C5=84ski?= Date: Wed, 24 Feb 2016 11:52:19 +0100 Subject: [PATCH 1220/2522] ecs_task module documentation fixes --- cloud/amazon/ecs_task.py | 56 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/cloud/amazon/ecs_task.py b/cloud/amazon/ecs_task.py index 16cf4fb2d2b..f263ef0da92 100644 --- a/cloud/amazon/ecs_task.py +++ b/cloud/amazon/ecs_task.py @@ -22,6 +22,7 @@ - Creates or deletes instances of task definitions. version_added: "2.0" author: Mark Chance(@Java1Guy) +requirements: [ json, boto, botocore, boto3 ] options: operation: description: @@ -95,8 +96,61 @@ RETURN = ''' task: description: details about the tast that was started + returned: success type: complex - sample: "TODO: include sample" + contains: + taskArn: + description: The Amazon Resource Name (ARN) that identifies the task. + returned: always + type: string + clusterArn: + description: The Amazon Resource Name (ARN) of the of the cluster that hosts the task. + returned: only when details is true + type: string + taskDefinitionArn: + description: The Amazon Resource Name (ARN) of the task definition. + returned: only when details is true + type: string + containerInstanceArn: + description: The Amazon Resource Name (ARN) of the container running the task. + returned: only when details is true + type: string + overrides: + description: The container overrides set for this task. + returned: only when details is true + type: list of complex + lastStatus: + description: The last recorded status of the task. + returned: only when details is true + type: string + desiredStatus: + description: The desired status of the task. + returned: only when details is true + type: string + containers: + description: The container details. + returned: only when details is true + type: list of complex + startedBy: + description: The used who started the task. + returned: only when details is true + type: string + stoppedReason: + description: The reason why the task was stopped. + returned: only when details is true + type: string + createdAt: + description: The timestamp of when the task was created. + returned: only when details is true + type: string + startedAt: + description: The timestamp of when the task was started. + returned: only when details is true + type: string + stoppedAt: + description: The timestamp of when the task was stopped. + returned: only when details is true + type: string ''' try: import boto From f4a9247232282ba303f16082ec774f11c5cb2096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darek=20Kaczy=C5=84ski?= Date: Wed, 24 Feb 2016 11:56:56 +0100 Subject: [PATCH 1221/2522] ecs_service_facts documentation fixes #1483. Workaround for datetime fileds #1348. --- cloud/amazon/ecs_service_facts.py | 110 +++++++++++++++++++++++------- 1 file changed, 85 insertions(+), 25 deletions(-) diff --git a/cloud/amazon/ecs_service_facts.py b/cloud/amazon/ecs_service_facts.py index 5e702f27e11..e6629ab08f0 100644 --- a/cloud/amazon/ecs_service_facts.py +++ b/cloud/amazon/ecs_service_facts.py @@ -23,7 +23,10 @@ description: - Lists or describes services in ecs. version_added: "2.1" -author: Mark Chance (@java1guy) +author: + - "Mark Chance (@java1guy)" + - "Darek Kaczynski (@kaczynskid)" +requirements: [ json, boto, botocore, boto3 ] options: details: description: @@ -59,23 +62,69 @@ cluster: test-cluster ''' -# Disabled the RETURN as it was breaking docs building. Someone needs to fix -# this -RETURN = '''# ''' -''' -services: When details is false, returns an array of service ARNs, else an array of these fields - clusterArn: The Amazon Resource Name (ARN) of the of the cluster that hosts the service. - desiredCount: The desired number of instantiations of the task definition to keep running on the service. - loadBalancers: A list of load balancer objects - loadBalancerName: the name - containerName: The name of the container to associate with the load balancer. - containerPort: The port on the container to associate with the load balancer. - pendingCount: The number of tasks in the cluster that are in the PENDING state. - runningCount: The number of tasks in the cluster that are in the RUNNING state. - serviceArn: The Amazon Resource Name (ARN) that identifies the service. The ARN contains the arn:aws:ecs namespace, followed by the region of the service, the AWS account ID of the service owner, the service namespace, and then the service name. For example, arn:aws:ecs:region :012345678910 :service/my-service . - serviceName: A user-generated string used to identify the service - status: The valid values are ACTIVE, DRAINING, or INACTIVE. - taskDefinition: The ARN of a task definition to use for tasks in the service. +RETURN = ''' +services: + description: When details is false, returns an array of service ARNs, otherwise an array of complex objects as described below. + returned: success + type: list of complex + contains: + clusterArn: + description: The Amazon Resource Name (ARN) of the of the cluster that hosts the service. + returned: always + type: string + desiredCount: + description: The desired number of instantiations of the task definition to keep running on the service. + returned: always + type: int + loadBalancers: + description: A list of load balancer objects + returned: always + type: complex + contains: + loadBalancerName: + description: the name + returned: always + type: string + containerName: + description: The name of the container to associate with the load balancer. + returned: always + type: string + containerPort: + description: The port on the container to associate with the load balancer. + returned: always + type: int + pendingCount: + description: The number of tasks in the cluster that are in the PENDING state. + returned: always + type: int + runningCount: + description: The number of tasks in the cluster that are in the RUNNING state. + returned: always + type: int + serviceArn: + description: The Amazon Resource Name (ARN) that identifies the service. The ARN contains the arn:aws:ecs namespace, followed by the region of the service, the AWS account ID of the service owner, the service namespace, and then the service name. For example, arn:aws:ecs:region :012345678910 :service/my-service . + returned: always + type: string + serviceName: + description: A user-generated string used to identify the service + returned: always + type: string + status: + description: The valid values are ACTIVE, DRAINING, or INACTIVE. + returned: always + type: string + taskDefinition: + description: The ARN of a task definition to use for tasks in the service. + returned: always + type: string + deployments: + description: list of service deployments + returned: always + type: list of complex + events: + description: lost of service events + returned: always + type: list of complex ''' try: import boto @@ -91,7 +140,7 @@ HAS_BOTO3 = False class EcsServiceManager: - """Handles ECS Clusters""" + """Handles ECS Services""" def __init__(self, module): self.module = module @@ -128,11 +177,26 @@ def describe_services(self, cluster, services): fn_args['cluster'] = cluster fn_args['services']=services.split(",") response = self.ecs.describe_services(**fn_args) - relevant_response = dict(services = response['services']) + relevant_response = dict(services = map(self.extract_service_from, response['services'])) if 'failures' in response and len(response['failures'])>0: relevant_response['services_not_running'] = response['failures'] return relevant_response + def extract_service_from(self, service): + # some fields are datetime which is not JSON serializable + # make them strings + if 'deployments' in service: + for d in service['deployments']: + if 'createdAt' in d: + d['createdAt'] = str(d['createdAt']) + if 'updatedAt' in d: + d['updatedAt'] = str(d['updatedAt']) + if 'events' in service: + for e in service['events']: + if 'createdAt' in e: + e['createdAt'] = str(e['createdAt']) + return service + def main(): argument_spec = ec2_argument_spec() @@ -159,13 +223,9 @@ def main(): if 'service' not in module.params or not module.params['service']: module.fail_json(msg="service must be specified for ecs_service_facts") ecs_facts = task_mgr.describe_services(module.params['cluster'], module.params['service']) - # the bad news is the result has datetime fields that aren't JSON serializable - # nuk'em! - for service in ecs_facts['services']: - del service['deployments'] - del service['events'] else: ecs_facts = task_mgr.list_services(module.params['cluster']) + ecs_facts_result = dict(changed=False, ansible_facts=ecs_facts) module.exit_json(**ecs_facts_result) From df482bfadd9ecc5e94cec661028cd4c533b310c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darek=20Kaczy=C5=84ski?= Date: Wed, 24 Feb 2016 11:55:36 +0100 Subject: [PATCH 1222/2522] ecs_service will now compare whole model and update it if any difference found. Documentation #1483. Workaround for datetime fileds #1348. --- cloud/amazon/ecs_service.py | 173 +++++++++++++++++++++++++++--------- 1 file changed, 133 insertions(+), 40 deletions(-) diff --git a/cloud/amazon/ecs_service.py b/cloud/amazon/ecs_service.py index d77c4f060ba..2c13a52eff9 100644 --- a/cloud/amazon/ecs_service.py +++ b/cloud/amazon/ecs_service.py @@ -26,7 +26,10 @@ dependencies: - An IAM role must have been created version_added: "2.1" -author: Mark Chance (@java1guy) +author: + - "Mark Chance (@java1guy)" + - "Darek Kaczynski (@kaczynskid)" +requirements: [ json, boto, botocore, boto3 ] options: state: description: @@ -99,26 +102,78 @@ cluster: new_cluster ''' -# Disabled the RETURN as it was breaking docs building. Someone needs to fix -# this -RETURN = '''# ''' -''' -# Create service -service: On create service, it returns the new values; on delete service, it returns the values for the service being deleted. - clusterArn: The Amazon Resource Name (ARN) of the of the cluster that hosts the service. - desiredCount: The desired number of instantiations of the task definition to keep running on the service. - loadBalancers: A list of load balancer objects - loadBalancerName: the name - containerName: The name of the container to associate with the load balancer. - containerPort: The port on the container to associate with the load balancer. - pendingCount: The number of tasks in the cluster that are in the PENDING state. - runningCount: The number of tasks in the cluster that are in the RUNNING state. - serviceArn: The Amazon Resource Name (ARN) that identifies the service. The ARN contains the arn:aws:ecs namespace, followed by the region of the service, the AWS account ID of the service owner, the service namespace, and then the service name. For example, arn:aws:ecs:region :012345678910 :service/my-service . - serviceName: A user-generated string used to identify the service - status: The valid values are ACTIVE, DRAINING, or INACTIVE. - taskDefinition: The ARN of a task definition to use for tasks in the service. -# Delete service -ansible_facts: When deleting a service, the values described above for the service prior to its deletion are returned. +RETURN = ''' +service: + description: Details of created service. + returned: when creating a service + type: complex + contains: + clusterArn: + description: The Amazon Resource Name (ARN) of the of the cluster that hosts the service. + returned: always + type: string + desiredCount: + description: The desired number of instantiations of the task definition to keep running on the service. + returned: always + type: int + loadBalancers: + description: A list of load balancer objects + returned: always + type: complex + contains: + loadBalancerName: + description: the name + returned: always + type: string + containerName: + description: The name of the container to associate with the load balancer. + returned: always + type: string + containerPort: + description: The port on the container to associate with the load balancer. + returned: always + type: int + pendingCount: + description: The number of tasks in the cluster that are in the PENDING state. + returned: always + type: int + runningCount: + description: The number of tasks in the cluster that are in the RUNNING state. + returned: always + type: int + serviceArn: + description: The Amazon Resource Name (ARN) that identifies the service. The ARN contains the arn:aws:ecs namespace, followed by the region of the service, the AWS account ID of the service owner, the service namespace, and then the service name. For example, arn:aws:ecs:region :012345678910 :service/my-service . + returned: always + type: string + serviceName: + description: A user-generated string used to identify the service + returned: always + type: string + status: + description: The valid values are ACTIVE, DRAINING, or INACTIVE. + returned: always + type: string + taskDefinition: + description: The ARN of a task definition to use for tasks in the service. + returned: always + type: string + deployments: + description: list of service deployments + returned: always + type: list of complex + events: + description: lost of service events + returned: always + type: list of complex +ansible_facts: + description: Facts about deleted service. + returned: when deleting a service + type: complex + contains: + service: + description: Details of deleted service in the same structure described above for service creation. + returned: when service existed and was deleted + type: complex ''' try: import boto @@ -182,6 +237,18 @@ def describe_service(self, cluster_name, service_name): return c raise StandardError("Unknown problem describing service %s." % service_name) + def is_matching_service(self, expected, existing): + if expected['task_definition'] != existing['taskDefinition']: + return False + + if (expected['load_balancers'] or []) != existing['loadBalancers']: + return False + + if (expected['desired_count'] or 0) != existing['desiredCount']: + return False + + return True + def create_service(self, service_name, cluster_name, task_definition, load_balancers, desired_count, client_token, role): response = self.ecs.create_service( @@ -192,9 +259,20 @@ def create_service(self, service_name, cluster_name, task_definition, desiredCount=desired_count, clientToken=client_token, role=role) + return self.jsonize(response['service']) + + def update_service(self, service_name, cluster_name, task_definition, + load_balancers, desired_count, client_token, role): + response = self.ecs.update_service( + cluster=cluster_name, + service=service_name, + taskDefinition=task_definition, + desiredCount=desired_count) + return self.jsonize(response['service']) + + def jsonize(self, service): # some fields are datetime which is not JSON serializable # make them strings - service = response['service'] if 'deployments' in service: for d in service['deployments']: if 'createdAt' in d: @@ -248,11 +326,20 @@ def main(): results = dict(changed=False ) if module.params['state'] == 'present': + + results['expected'] = module.params + results['existing'] = existing + + matching = False + update = False if existing and 'status' in existing and existing['status']=="ACTIVE": - del existing['deployments'] - del existing['events'] - results['service']=existing - else: + if service_mgr.is_matching_service(module.params, existing): + matching = True + results['service'] = service_mgr.jsonize(existing) + else: + update = True + + if not matching: if not module.check_mode: if module.params['load_balancers'] is None: loadBalancers = [] @@ -266,20 +353,26 @@ def main(): clientToken = '' else: clientToken = module.params['client_token'] - # doesn't exist. create it. - response = service_mgr.create_service(module.params['name'], - module.params['cluster'], - module.params['task_definition'], - loadBalancers, - module.params['desired_count'], - clientToken, - role) - # the bad news is the result has datetime fields that aren't JSON serializable - # nuk'em! - - del response['deployments'] - del response['events'] - + + if update: + # update required + response = service_mgr.update_service(module.params['name'], + module.params['cluster'], + module.params['task_definition'], + loadBalancers, + module.params['desired_count'], + clientToken, + role) + else: + # doesn't exist. create it. + response = service_mgr.create_service(module.params['name'], + module.params['cluster'], + module.params['task_definition'], + loadBalancers, + module.params['desired_count'], + clientToken, + role) + results['service'] = response results['changed'] = True From 1084bb31b802e05c722e7114c24184c1d7b541a9 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Wed, 24 Feb 2016 12:03:05 +0100 Subject: [PATCH 1223/2522] Add a datacenter parameter, fix #1693 --- clustering/consul_session.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/clustering/consul_session.py b/clustering/consul_session.py index c298ea7fa57..423a6ecef1f 100644 --- a/clustering/consul_session.py +++ b/clustering/consul_session.py @@ -248,7 +248,8 @@ def main(): name=dict(required=False), node=dict(required=False), state=dict(default='present', - choices=['present', 'absent', 'info', 'node', 'list']) + choices=['present', 'absent', 'info', 'node', 'list']), + datacenter=dict(required=False) ) module = AnsibleModule(argument_spec, supports_check_mode=False) From 9e61b49d5867a57713cb8d4868c9eb1a326c5972 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Wed, 24 Feb 2016 12:14:00 +0100 Subject: [PATCH 1224/2522] Pyflakes complain about unused import, so remove it --- clustering/consul_session.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/clustering/consul_session.py b/clustering/consul_session.py index c298ea7fa57..93e5452dbb7 100644 --- a/clustering/consul_session.py +++ b/clustering/consul_session.py @@ -113,8 +113,6 @@ consul_session: state=list ''' -import sys - try: import consul from requests.exceptions import ConnectionError From 05ac6edd457d96ca0f005fe43a537d2f4fb0d936 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Wed, 24 Feb 2016 12:15:02 +0100 Subject: [PATCH 1225/2522] Rename consul variable to consul_client Since the module is also named consul, pyflakes emit a warning about it since the variable shadow the module. --- clustering/consul_session.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/clustering/consul_session.py b/clustering/consul_session.py index 93e5452dbb7..5070c348641 100644 --- a/clustering/consul_session.py +++ b/clustering/consul_session.py @@ -136,10 +136,10 @@ def lookup_sessions(module): datacenter = module.params.get('datacenter') state = module.params.get('state') - consul = get_consul_api(module) + consul_client = get_consul_api(module) try: if state == 'list': - sessions_list = consul.session.list(dc=datacenter) + sessions_list = consul_client.session.list(dc=datacenter) #ditch the index, this can be grabbed from the results if sessions_list and sessions_list[1]: sessions_list = sessions_list[1] @@ -150,7 +150,7 @@ def lookup_sessions(module): if not node: module.fail_json( msg="node name is required to retrieve sessions for node") - sessions = consul.session.node(node, dc=datacenter) + sessions = consul_client.session.node(node, dc=datacenter) module.exit_json(changed=True, node=node, sessions=sessions) @@ -160,7 +160,7 @@ def lookup_sessions(module): module.fail_json( msg="session_id is required to retrieve indvidual session info") - session_by_id = consul.session.info(session_id, dc=datacenter) + session_by_id = consul_client.session.info(session_id, dc=datacenter) module.exit_json(changed=True, session_id=session_id, sessions=session_by_id) @@ -178,12 +178,12 @@ def update_session(module): datacenter = module.params.get('datacenter') node = module.params.get('node') - consul = get_consul_api(module) + consul_client = get_consul_api(module) changed = True try: - session = consul.session.create( + session = consul_client.session.create( name=name, node=node, lock_delay=validate_duration('delay', delay), @@ -207,11 +207,11 @@ def remove_session(module): module.fail_json(msg="""A session id must be supplied in order to remove a session.""") - consul = get_consul_api(module) + consul_client = get_consul_api(module) changed = False try: - session = consul.session.destroy(session_id) + session = consul_client.session.destroy(session_id) module.exit_json(changed=True, session_id=session_id) From 250494eaefcd475c69cc339654951683512a0e3f Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Wed, 24 Feb 2016 12:17:44 +0100 Subject: [PATCH 1226/2522] Remove unused variables Session_id is unused in update_session, changed is always specifically set in all exit_json call, and consul_client.session.destroy return True or False, and is unused later (nor checked) --- clustering/consul_session.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/clustering/consul_session.py b/clustering/consul_session.py index 5070c348641..c9ffee806e0 100644 --- a/clustering/consul_session.py +++ b/clustering/consul_session.py @@ -172,14 +172,12 @@ def lookup_sessions(module): def update_session(module): name = module.params.get('name') - session_id = module.params.get('id') delay = module.params.get('delay') checks = module.params.get('checks') datacenter = module.params.get('datacenter') node = module.params.get('node') consul_client = get_consul_api(module) - changed = True try: @@ -208,10 +206,9 @@ def remove_session(module): remove a session.""") consul_client = get_consul_api(module) - changed = False try: - session = consul_client.session.destroy(session_id) + consul_client.session.destroy(session_id) module.exit_json(changed=True, session_id=session_id) From f63ef1fee83aa272d4c8be88539b4c6e296efb08 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Wed, 24 Feb 2016 12:43:37 +0100 Subject: [PATCH 1227/2522] Add documentation to explain that pvs will be created if needed Fix #1720 --- system/lvg.py | 1 + 1 file changed, 1 insertion(+) diff --git a/system/lvg.py b/system/lvg.py index 68c9d7575ac..d22f3750b7a 100644 --- a/system/lvg.py +++ b/system/lvg.py @@ -35,6 +35,7 @@ pvs: description: - List of comma-separated devices to use as physical devices in this volume group. Required when creating or resizing volume group. + - The module will take care of running pvcreate if needed. required: false pesize: description: From fade5b7936342bd289e20da7413617780bb330b6 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Wed, 24 Feb 2016 08:39:53 -0500 Subject: [PATCH 1228/2522] added docs to blockinfile with_ interactions fixes #1592 --- files/blockinfile.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/files/blockinfile.py b/files/blockinfile.py index a40f57a863c..c2e449b2edc 100644 --- a/files/blockinfile.py +++ b/files/blockinfile.py @@ -38,6 +38,7 @@ surrounded by customizable marker lines. notes: - This module supports check mode. + - When using 'with_' loops be aware that if you do not set a unique mark the block will be overwritten on each iteration. options: dest: aliases: [ name, destfile ] @@ -136,6 +137,17 @@ dest: /var/www/html/index.html marker: "" content: "" + +- name: insert/update "Match User" configuation block in /etc/ssh/sshd_config + blockinfile: + dest: /etc/hosts + block: | + {{item.name}} {{item.ip}} + marker: "# {mark} ANSIBLE MANAGED BLOCK {{item.name}}" + with_items: + - { name: host1, ip: 10.10.1.10 } + - { name: host2, ip: 10.10.1.11 } + - { name: host3, ip: 10.10.1.12 } """ From 2ef35c33d0b196f8609b35b56fb1ef2d2d923d3c Mon Sep 17 00:00:00 2001 From: Gabriel Burkholder Date: Wed, 24 Feb 2016 16:14:42 -0800 Subject: [PATCH 1229/2522] Fixes route53_facts to use max_items parameter with record_sets query. --- cloud/amazon/route53_facts.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cloud/amazon/route53_facts.py b/cloud/amazon/route53_facts.py index 9de2e4a68cc..95c1491a66f 100644 --- a/cloud/amazon/route53_facts.py +++ b/cloud/amazon/route53_facts.py @@ -316,6 +316,9 @@ def record_sets_details(client, module): else: module.fail_json(msg="Hosted Zone Id is required") + if module.params.get('max_items'): + params['MaxItems'] = module.params.get('max_items') + if module.params.get('start_record_name'): params['StartRecordName'] = module.params.get('start_record_name') From 2a750bb8db9b79bb073d90c297b21aacf3cf8e85 Mon Sep 17 00:00:00 2001 From: Eike Frost Date: Fri, 10 Jul 2015 17:41:44 +0200 Subject: [PATCH 1230/2522] return as unchanged if macro update is unnecessary --- monitoring/zabbix_hostmacro.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monitoring/zabbix_hostmacro.py b/monitoring/zabbix_hostmacro.py index e8d65370760..3b66d456da1 100644 --- a/monitoring/zabbix_hostmacro.py +++ b/monitoring/zabbix_hostmacro.py @@ -149,6 +149,8 @@ def create_host_macro(self, macro_name, macro_value, host_id): # update host macro def update_host_macro(self, host_macro_obj, macro_name, macro_value): host_macro_id = host_macro_obj['hostmacroid'] + if host_macro_obj['macro'] == '{$'+macro_name+'}' and host_macro_obj['value'] == macro_value: + self._module.exit_json(changed=False, result="Host macro %s already up to date" % macro_name) try: if self._module.check_mode: self._module.exit_json(changed=True) From edbad801231668f67e1078c45643c1654ac31e16 Mon Sep 17 00:00:00 2001 From: Guillaume Dufour Date: Thu, 25 Feb 2016 11:01:34 +0100 Subject: [PATCH 1231/2522] fix #1731 : mongodb_user always says changed --- database/misc/mongodb_user.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index 12d348e9a92..44659221bc4 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -148,9 +148,9 @@ # MongoDB module specific support methods. # -def user_find(client, user): +def user_find(client, user, db_name): for mongo_user in client["admin"].system.users.find(): - if mongo_user['user'] == user: + if mongo_user['user'] == user and mongo_user['db'] == db_name: return mongo_user return False @@ -158,6 +158,7 @@ def user_add(module, client, db_name, user, password, roles): #pymono's user_add is a _create_or_update_user so we won't know if it was changed or updated #without reproducing a lot of the logic in database.py of pymongo db = client[db_name] + if roles is None: db.add_user(user, password, False) else: @@ -170,7 +171,7 @@ def user_add(module, client, db_name, user, password, roles): module.fail_json(msg=err_msg) def user_remove(module, client, db_name, user): - exists = user_find(client, user) + exists = user_find(client, user, db_name) if exists: db = client[db_name] db.remove_user(user) @@ -223,7 +224,7 @@ def main(): login_host = module.params['login_host'] login_port = module.params['login_port'] login_database = module.params['login_database'] - + replica_set = module.params['replica_set'] db_name = module.params['database'] user = module.params['name'] @@ -261,14 +262,22 @@ def main(): if password is None and update_password == 'always': module.fail_json(msg='password parameter required when adding a user unless update_password is set to on_create') - if update_password != 'always' and user_find(client, user): + uinfo = user_find(client, user, db_name) + if update_password != 'always' and uinfo: password = None + if list(map((lambda x: x['role']), uinfo['roles'])) == roles: + module.exit_json(changed=False, user=user) try: user_add(module, client, db_name, user, password, roles) except OperationFailure, e: module.fail_json(msg='Unable to add or update user: %s' % str(e)) + # Here we can check password change if mongo provide a query for that : https://jira.mongodb.org/browse/SERVER-22848 + #newuinfo = user_find(client, user, db_name) + #if uinfo['role'] == newuinfo['role'] and CheckPasswordHere: + # module.exit_json(changed=False, user=user) + elif state == 'absent': try: user_remove(module, client, db_name, user) From a3641cac4ef27493293a1df56b0c9743f4ae58f2 Mon Sep 17 00:00:00 2001 From: Borys Borysenko Date: Wed, 24 Feb 2016 01:54:45 +0200 Subject: [PATCH 1232/2522] The size option is required for lvol module with state=present --- system/lvol.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system/lvol.py b/system/lvol.py index 284c37ace5e..e52b23bbcb7 100644 --- a/system/lvol.py +++ b/system/lvol.py @@ -48,7 +48,8 @@ choices: [ "present", "absent" ] default: present description: - - Control if the logical volume exists. + - Control if the logical volume exists. If C(present) the C(size) option + is required. required: false force: version_added: "1.5" From fc0c41a3066e87430c2cd9af021b2f626e980729 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Tue, 2 Feb 2016 19:40:09 +0100 Subject: [PATCH 1233/2522] cloudstack: new module cs_instance_facts --- cloud/cloudstack/cs_instance_facts.py | 282 ++++++++++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 cloud/cloudstack/cs_instance_facts.py diff --git a/cloud/cloudstack/cs_instance_facts.py b/cloud/cloudstack/cs_instance_facts.py new file mode 100644 index 00000000000..bfed5c8572f --- /dev/null +++ b/cloud/cloudstack/cs_instance_facts.py @@ -0,0 +1,282 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2016, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: cs_instance_facts +short_description: Gathering facts from the API of instances from Apache CloudStack based clouds. +description: + - Gathering facts from the API of an instance. +version_added: "2.1" +author: "René Moser (@resmo)" +options: + name: + description: + - Name or display name of the instance. + required: true + domain: + description: + - Domain the instance is related to. + required: false + default: null + account: + description: + - Account the instance is related to. + required: false + default: null + project: + description: + - Project the instance is related to. + required: false + default: null +extends_documentation_fragment: cloudstack +''' + +EXAMPLES = ''' +- local_action: + module: cs_instance_facts + name: web-vm-1 + +- debug: var=cloudstack_instance +''' + +RETURN = ''' +--- +cloudstack_instance.id: + description: UUID of the instance. + returned: success + type: string + sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6 +cloudstack_instance.name: + description: Name of the instance. + returned: success + type: string + sample: web-01 +cloudstack_instance.display_name: + description: Display name of the instance. + returned: success + type: string + sample: web-01 +cloudstack_instance.group: + description: Group name of the instance is related. + returned: success + type: string + sample: web +created: + description: Date of the instance was created. + returned: success + type: string + sample: 2014-12-01T14:57:57+0100 +cloudstack_instance.password_enabled: + description: True if password setting is enabled. + returned: success + type: boolean + sample: true +cloudstack_instance.password: + description: The password of the instance if exists. + returned: success + type: string + sample: Ge2oe7Do +cloudstack_instance.ssh_key: + description: Name of SSH key deployed to instance. + returned: success + type: string + sample: key@work +cloudstack_instance.domain: + description: Domain the instance is related to. + returned: success + type: string + sample: example domain +cloudstack_instance.account: + description: Account the instance is related to. + returned: success + type: string + sample: example account +cloudstack_instance.project: + description: Name of project the instance is related to. + returned: success + type: string + sample: Production +cloudstack_instance.default_ip: + description: Default IP address of the instance. + returned: success + type: string + sample: 10.23.37.42 +cloudstack_instance.public_ip: + description: Public IP address with instance via static NAT rule. + returned: success + type: string + sample: 1.2.3.4 +cloudstack_instance.iso: + description: Name of ISO the instance was deployed with. + returned: success + type: string + sample: Debian-8-64bit +cloudstack_instance.template: + description: Name of template the instance was deployed with. + returned: success + type: string + sample: Debian-8-64bit +cloudstack_instance.service_offering: + description: Name of the service offering the instance has. + returned: success + type: string + sample: 2cpu_2gb +cloudstack_instance.zone: + description: Name of zone the instance is in. + returned: success + type: string + sample: ch-gva-2 +cloudstack_instance.state: + description: State of the instance. + returned: success + type: string + sample: Running +cloudstack_instance.security_groups: + description: Security groups the instance is in. + returned: success + type: list + sample: '[ "default" ]' +cloudstack_instance.affinity_groups: + description: Affinity groups the instance is in. + returned: success + type: list + sample: '[ "webservers" ]' +cloudstack_instance.tags: + description: List of resource tags associated with the instance. + returned: success + type: dict + sample: '[ { "key": "foo", "value": "bar" } ]' +cloudstack_instance.hypervisor: + description: Hypervisor related to this instance. + returned: success + type: string + sample: KVM +cloudstack_instance.instance_name: + description: Internal name of the instance (ROOT admin only). + returned: success + type: string + sample: i-44-3992-VM +''' + +import base64 + +try: + from cs import CloudStack, CloudStackException, read_config + has_lib_cs = True +except ImportError: + has_lib_cs = False + +# import cloudstack common +from ansible.module_utils.cloudstack import * + +class AnsibleCloudStackInstanceFacts(AnsibleCloudStack): + + def __init__(self, module): + super(AnsibleCloudStackInstanceFacts, self).__init__(module) + self.instance = None + self.returns = { + 'group': 'group', + 'hypervisor': 'hypervisor', + 'instancename': 'instance_name', + 'publicip': 'public_ip', + 'passwordenabled': 'password_enabled', + 'password': 'password', + 'serviceofferingname': 'service_offering', + 'isoname': 'iso', + 'templatename': 'template', + 'keypair': 'ssh_key', + } + self.facts = { + 'cloudstack_instance': None, + } + + + def get_instance(self): + instance = self.instance + if not instance: + instance_name = self.module.params.get('name') + + args = {} + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') + args['projectid'] = self.get_project(key='id') + # Do not pass zoneid, as the instance name must be unique across zones. + instances = self.cs.listVirtualMachines(**args) + if instances: + for v in instances['virtualmachine']: + if instance_name.lower() in [ v['name'].lower(), v['displayname'].lower(), v['id'] ]: + self.instance = v + break + return self.instance + + + def run(self): + instance = self.get_instance() + if not instance: + self.module.fail_json(msg="Instance not found: %s" % self.module.params.get('name')) + self.facts['cloudstack_instance'] = self.get_result(instance) + return self.facts + + + def get_result(self, instance): + super(AnsibleCloudStackInstanceFacts, self).get_result(instance) + if instance: + if 'securitygroup' in instance: + security_groups = [] + for securitygroup in instance['securitygroup']: + security_groups.append(securitygroup['name']) + self.result['security_groups'] = security_groups + if 'affinitygroup' in instance: + affinity_groups = [] + for affinitygroup in instance['affinitygroup']: + affinity_groups.append(affinitygroup['name']) + self.result['affinity_groups'] = affinity_groups + if 'nic' in instance: + for nic in instance['nic']: + if nic['isdefault'] and 'ipaddress' in nic: + self.result['default_ip'] = nic['ipaddress'] + return self.result + + +def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + name = dict(required=True), + domain = dict(default=None), + account = dict(default=None), + project = dict(default=None), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=False, + ) + + if not has_lib_cs: + module.fail_json(msg="python library cs required: pip install cs") + + cs_instance_facts = AnsibleCloudStackInstanceFacts(module=module).run() + cs_facts_result = dict(changed=False, ansible_facts=cs_instance_facts) + module.exit_json(**cs_facts_result) + +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From 3b354ddf00b2b7f243f8bb87a00eef5814a0a4af Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Thu, 4 Feb 2016 23:48:24 +0100 Subject: [PATCH 1234/2522] cloudstack: new module cs_zone_facts --- cloud/cloudstack/cs_zone_facts.py | 209 ++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 cloud/cloudstack/cs_zone_facts.py diff --git a/cloud/cloudstack/cs_zone_facts.py b/cloud/cloudstack/cs_zone_facts.py new file mode 100644 index 00000000000..99897967311 --- /dev/null +++ b/cloud/cloudstack/cs_zone_facts.py @@ -0,0 +1,209 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2016, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: cs_zone_facts +short_description: Gathering facts of zones from Apache CloudStack based clouds. +description: + - Gathering facts from the API of a zone. +version_added: "2.1" +author: "René Moser (@resmo)" +options: + name: + description: + - Name of the zone. + required: true +extends_documentation_fragment: cloudstack +''' + +EXAMPLES = ''' +- local_action: + module: cs_zone_facts + name: ch-gva-1 + +- debug: var=cloudstack_zone +''' + +RETURN = ''' +--- +cloudstack_zone.id: + description: UUID of the zone. + returned: success + type: string + sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6 +cloudstack_zone.name: + description: Name of the zone. + returned: success + type: string + sample: zone01 +cloudstack_zone.dns1: + description: First DNS for the zone. + returned: success + type: string + sample: 8.8.8.8 +cloudstack_zone.dns2: + description: Second DNS for the zone. + returned: success + type: string + sample: 8.8.4.4 +cloudstack_zone.internal_dns1: + description: First internal DNS for the zone. + returned: success + type: string + sample: 8.8.8.8 +cloudstack_zone.internal_dns2: + description: Second internal DNS for the zone. + returned: success + type: string + sample: 8.8.4.4 +cloudstack_zone.dns1_ipv6: + description: First IPv6 DNS for the zone. + returned: success + type: string + sample: "2001:4860:4860::8888" +cloudstack_zone.dns2_ipv6: + description: Second IPv6 DNS for the zone. + returned: success + type: string + sample: "2001:4860:4860::8844" +cloudstack_zone.allocation_state: + description: State of the zone. + returned: success + type: string + sample: Enabled +cloudstack_zone.domain: + description: Domain the zone is related to. + returned: success + type: string + sample: ROOT +cloudstack_zone.network_domain: + description: Network domain for the zone. + returned: success + type: string + sample: example.com +cloudstack_zone.network_type: + description: Network type for the zone. + returned: success + type: string + sample: basic +cloudstack_zone.local_storage_enabled: + description: Local storage offering enabled. + returned: success + type: bool + sample: false +cloudstack_zone.securitygroups_enabled: + description: Security groups support is enabled. + returned: success + type: bool + sample: false +cloudstack_zone.guest_cidr_address: + description: Guest CIDR address for the zone + returned: success + type: string + sample: 10.1.1.0/24 +cloudstack_zone.dhcp_provider: + description: DHCP provider for the zone + returned: success + type: string + sample: VirtualRouter +cloudstack_zone.zone_token: + description: Zone token + returned: success + type: string + sample: ccb0a60c-79c8-3230-ab8b-8bdbe8c45bb7 +cloudstack_zone.tags: + description: List of resource tags associated with the zone. + returned: success + type: dict + sample: [ { "key": "foo", "value": "bar" } ] +''' + +import base64 + +try: + from cs import CloudStack, CloudStackException, read_config + has_lib_cs = True +except ImportError: + has_lib_cs = False + +# import cloudstack common +from ansible.module_utils.cloudstack import * + +class AnsibleCloudStackZoneFacts(AnsibleCloudStack): + + def __init__(self, module): + super(AnsibleCloudStackZoneFacts, self).__init__(module) + self.returns = { + 'dns1': 'dns1', + 'dns2': 'dns2', + 'internaldns1': 'internal_dns1', + 'internaldns2': 'internal_dns2', + 'ipv6dns1': 'dns1_ipv6', + 'ipv6dns2': 'dns2_ipv6', + 'domain': 'network_domain', + 'networktype': 'network_type', + 'securitygroupsenabled': 'securitygroups_enabled', + 'localstorageenabled': 'local_storage_enabled', + 'guestcidraddress': 'guest_cidr_address', + 'dhcpprovider': 'dhcp_provider', + 'allocationstate': 'allocation_state', + 'zonetoken': 'zone_token', + } + self.facts = { + 'cloudstack_zone': None, + } + + + def get_zone(self): + if not self.zone: + # TODO: add param key signature in get_zone() + self.module.params['zone'] = self.module.params.get('name') + super(AnsibleCloudStackZoneFacts, self).get_zone() + return self.zone + + + def run(self): + zone = self.get_zone() + self.facts['cloudstack_zone'] = self.get_result(zone) + return self.facts + + +def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + name = dict(required=True), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=False, + ) + + if not has_lib_cs: + module.fail_json(msg="python library cs required: pip install cs") + + cs_zone_facts = AnsibleCloudStackZoneFacts(module=module).run() + cs_facts_result = dict(changed=False, ansible_facts=cs_zone_facts) + module.exit_json(**cs_facts_result) + +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From 29daa6ffe08c138f09fac3702f8fbe71863eb583 Mon Sep 17 00:00:00 2001 From: Guillaume Dufour Date: Fri, 26 Feb 2016 15:08:04 +0100 Subject: [PATCH 1235/2522] avoid problem with old mongo version without roles --- database/misc/mongodb_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index 44659221bc4..848371884d6 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -265,7 +265,7 @@ def main(): uinfo = user_find(client, user, db_name) if update_password != 'always' and uinfo: password = None - if list(map((lambda x: x['role']), uinfo['roles'])) == roles: + if 'roles' in uinfo and list(map((lambda x: x['role']), uinfo['roles'])) == roles: module.exit_json(changed=False, user=user) try: From c5ce6848128b54886a60095ae3e75c0f0ed2a6c2 Mon Sep 17 00:00:00 2001 From: Matt Ferrante Date: Thu, 31 Dec 2015 17:19:23 -0500 Subject: [PATCH 1236/2522] dynamo db indexes --- cloud/amazon/dynamodb_table.py | 180 ++++++++++++++++++++++++++++----- 1 file changed, 155 insertions(+), 25 deletions(-) diff --git a/cloud/amazon/dynamodb_table.py b/cloud/amazon/dynamodb_table.py index a39ecdd3f48..24cfccd6687 100644 --- a/cloud/amazon/dynamodb_table.py +++ b/cloud/amazon/dynamodb_table.py @@ -24,9 +24,8 @@ - Returns the status of the specified table. version_added: "2.0" author: Alan Loi (@loia) -version_added: "2.0" requirements: - - "boto >= 2.13.2" + - "boto >= 2.37.0" options: state: description: @@ -71,6 +70,15 @@ - Write throughput capacity (units) to provision. required: false default: 1 + indexes: + description: + - list of dictionaries describing indexes to add to the table. global indexes can be updated. local indexes don't support updates or have throughput. + - required options: ['name', 'type', 'hash_key_name'] + - valid types: ['all', 'global_all', 'global_include', 'global_keys_only', 'include', 'keys_only'] + - other options: ['hash_key_type', 'range_key_name', 'range_key_type', 'includes', 'read_capacity', 'write_capacity'] + required: false + default: [] + version_added: "2.1" extends_documentation_fragment: - aws - ec2 @@ -95,6 +103,21 @@ read_capacity: 10 write_capacity: 10 +# set index on existing dynamo table +- dynamodb_table: + name: my-table + region: us-east-1 + indexes: + - name: NamedIndex + type: global_include + hash_key_name: id + range_key_name: create_time + includes: + - other_field + - other_field2 + read_capacity: 10 + write_capacity: 10 + # Delete dynamo table - dynamodb_table: name: my-table @@ -114,20 +137,25 @@ import boto import boto.dynamodb2 from boto.dynamodb2.table import Table - from boto.dynamodb2.fields import HashKey, RangeKey + from boto.dynamodb2.fields import HashKey, RangeKey, AllIndex, GlobalAllIndex, GlobalIncludeIndex, GlobalKeysOnlyIndex, IncludeIndex, KeysOnlyIndex from boto.dynamodb2.types import STRING, NUMBER, BINARY from boto.exception import BotoServerError, NoAuthHandlerFound, JSONResponseError + from boto.dynamodb2.exceptions import ValidationException HAS_BOTO = True + DYNAMO_TYPE_MAP = { + 'STRING': STRING, + 'NUMBER': NUMBER, + 'BINARY': BINARY + } + except ImportError: HAS_BOTO = False - -DYNAMO_TYPE_MAP = { - 'STRING': STRING, - 'NUMBER': NUMBER, - 'BINARY': BINARY -} +DYNAMO_TYPE_DEFAULT = 'STRING' +INDEX_REQUIRED_OPTIONS = ['name', 'type', 'hash_key_name'] +INDEX_OPTIONS = INDEX_REQUIRED_OPTIONS + ['hash_key_type', 'range_key_name', 'range_key_type', 'includes', 'read_capacity', 'write_capacity'] +INDEX_TYPE_OPTIONS = ['all', 'global_all', 'global_include', 'global_keys_only', 'include', 'keys_only'] def create_or_update_dynamo_table(connection, module): @@ -138,21 +166,20 @@ def create_or_update_dynamo_table(connection, module): range_key_type = module.params.get('range_key_type') read_capacity = module.params.get('read_capacity') write_capacity = module.params.get('write_capacity') + all_indexes = module.params.get('indexes') + + for index in all_indexes: + validate_index(index, module) + + schema = get_schema_param(hash_key_name, hash_key_type, range_key_name, range_key_type) - if range_key_name: - schema = [ - HashKey(hash_key_name, DYNAMO_TYPE_MAP.get(hash_key_type)), - RangeKey(range_key_name, DYNAMO_TYPE_MAP.get(range_key_type)) - ] - else: - schema = [ - HashKey(hash_key_name, DYNAMO_TYPE_MAP.get(hash_key_type)) - ] throughput = { 'read': read_capacity, 'write': write_capacity } + indexes, global_indexes = get_indexes(all_indexes) + result = dict( region=module.params.get('region'), table_name=table_name, @@ -162,16 +189,18 @@ def create_or_update_dynamo_table(connection, module): range_key_type=range_key_type, read_capacity=read_capacity, write_capacity=write_capacity, + indexes=all_indexes, ) try: table = Table(table_name, connection=connection) + if dynamo_table_exists(table): - result['changed'] = update_dynamo_table(table, throughput=throughput, check_mode=module.check_mode) + result['changed'] = update_dynamo_table(table, throughput=throughput, check_mode=module.check_mode, global_indexes=global_indexes) else: if not module.check_mode: - Table.create(table_name, connection=connection, schema=schema, throughput=throughput) + Table.create(table_name, connection=connection, schema=schema, throughput=throughput, indexes=indexes, global_indexes=global_indexes) result['changed'] = True if not module.check_mode: @@ -222,16 +251,42 @@ def dynamo_table_exists(table): raise e -def update_dynamo_table(table, throughput=None, check_mode=False): +def update_dynamo_table(table, throughput=None, check_mode=False, global_indexes=None): table.describe() # populate table details - + throughput_changed = False + global_indexes_changed = False if has_throughput_changed(table, throughput): if not check_mode: - return table.update(throughput=throughput) + throughput_changed = table.update(throughput=throughput) + else: + throughput_changed = True + + removed_indexes, added_indexes, index_throughput_changes = get_changed_global_indexes(table, global_indexes) + if removed_indexes: + if not check_mode: + for name, index in removed_indexes.iteritems(): + global_indexes_changed = table.delete_global_secondary_index(name) or global_indexes_changed + else: + global_indexes_changed = True + + if added_indexes: + if not check_mode: + for name, index in added_indexes.iteritems(): + global_indexes_changed = table.create_global_secondary_index(global_index=index) or global_indexes_changed else: - return True + global_indexes_changed = True - return False + if index_throughput_changes: + if not check_mode: + # todo: remove try once boto has https://github.com/boto/boto/pull/3447 fixed + try: + global_indexes_changed = table.update_global_secondary_index(global_indexes=index_throughput_changes) or global_indexes_changed + except ValidationException as e: + pass + else: + global_indexes_changed = True + + return throughput_changed or global_indexes_changed def has_throughput_changed(table, new_throughput): @@ -242,6 +297,80 @@ def has_throughput_changed(table, new_throughput): new_throughput['write'] != table.throughput['write'] +def get_schema_param(hash_key_name, hash_key_type, range_key_name, range_key_type): + if range_key_name: + schema = [ + HashKey(hash_key_name, DYNAMO_TYPE_MAP.get(hash_key_type, DYNAMO_TYPE_MAP[DYNAMO_TYPE_DEFAULT])), + RangeKey(range_key_name, DYNAMO_TYPE_MAP.get(range_key_type, DYNAMO_TYPE_MAP[DYNAMO_TYPE_DEFAULT])) + ] + else: + schema = [ + HashKey(hash_key_name, DYNAMO_TYPE_MAP.get(hash_key_type, DYNAMO_TYPE_MAP[DYNAMO_TYPE_DEFAULT])) + ] + return schema + + +def get_changed_global_indexes(table, global_indexes): + table.describe() + + table_index_info = dict((index.name, index.schema()) for index in table.global_indexes) + table_index_objects = dict((index.name, index) for index in table.global_indexes) + set_index_info = dict((index.name, index.schema()) for index in global_indexes) + set_index_objects = dict((index.name, index) for index in global_indexes) + + removed_indexes = dict((name, index) for name, index in table_index_info.iteritems() if name not in set_index_info) + added_indexes = dict((name, set_index_objects[name]) for name, index in set_index_info.iteritems() if name not in table_index_info) + # todo: uncomment once boto has https://github.com/boto/boto/pull/3447 fixed + # index_throughput_changes = dict((name, index.throughput) for name, index in set_index_objects.iteritems() if name not in added_indexes and (index.throughput['read'] != str(table_index_objects[name].throughput['read']) or index.throughput['write'] != str(table_index_objects[name].throughput['write']))) + # todo: remove once boto has https://github.com/boto/boto/pull/3447 fixed + index_throughput_changes = dict((name, index.throughput) for name, index in set_index_objects.iteritems() if name not in added_indexes) + + return removed_indexes, added_indexes, index_throughput_changes + + +def validate_index(index, module): + for key, val in index.iteritems(): + if key not in INDEX_OPTIONS: + module.fail_json(msg='%s is not a valid option for an index' % key) + for required_option in INDEX_REQUIRED_OPTIONS: + if required_option not in index: + module.fail_json(msg='%s is a required option for an index' % required_option) + if index['type'] not in INDEX_TYPE_OPTIONS: + module.fail_json(msg='%s is not a valid index type, must be one of %s' % (index['type'], INDEX_TYPE_OPTIONS)) + +def get_indexes(all_indexes): + indexes = [] + global_indexes = [] + for index in all_indexes: + name = index['name'] + schema = get_schema_param(index.get('hash_key_name'), index.get('hash_key_type'), index.get('range_key_name'), index.get('range_key_type')) + throughput = { + 'read': index.get('read_capacity', 1), + 'write': index.get('write_capacity', 1) + } + + if index['type'] == 'all': + indexes.append(AllIndex(name, parts=schema)) + + elif index['type'] == 'global_all': + global_indexes.append(GlobalAllIndex(name, parts=schema, throughput=throughput)) + + elif index['type'] == 'global_include': + global_indexes.append(GlobalIncludeIndex(name, parts=schema, throughput=throughput, includes=index['includes'])) + + elif index['type'] == 'global_keys_only': + global_indexes.append(GlobalKeysOnlyIndex(name, parts=schema, throughput=throughput)) + + elif index['type'] == 'include': + indexes.append(IncludeIndex(name, parts=schema, includes=index['includes'])) + + elif index['type'] == 'keys_only': + indexes.append(KeysOnlyIndex(name, parts=schema)) + + return indexes, global_indexes + + + def main(): argument_spec = ec2_argument_spec() argument_spec.update(dict( @@ -253,6 +382,7 @@ def main(): range_key_type=dict(default='STRING', type='str', choices=['STRING', 'NUMBER', 'BINARY']), read_capacity=dict(default=1, type='int'), write_capacity=dict(default=1, type='int'), + indexes=dict(default=[], type='list'), )) module = AnsibleModule( From c6ebfa548019a48095140c3f95b68ba8cede212e Mon Sep 17 00:00:00 2001 From: Guillaume Dufour Date: Sun, 28 Feb 2016 08:05:20 +0100 Subject: [PATCH 1237/2522] use python fallback to avoid error on old mongo version without roles --- database/misc/mongodb_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index 848371884d6..e58857586eb 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -265,7 +265,7 @@ def main(): uinfo = user_find(client, user, db_name) if update_password != 'always' and uinfo: password = None - if 'roles' in uinfo and list(map((lambda x: x['role']), uinfo['roles'])) == roles: + if list(map((lambda x: x['role']), uinfo.get('roles', []))) == roles: module.exit_json(changed=False, user=user) try: From 8c3aeac4a3a571017d6d521cbd282ef7289f0f2b Mon Sep 17 00:00:00 2001 From: Guillaume Dufour Date: Sun, 28 Feb 2016 07:25:45 +0100 Subject: [PATCH 1238/2522] fix #1747 mongodb_user support check mode --- database/misc/mongodb_user.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index 12d348e9a92..71ce961febb 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -172,6 +172,8 @@ def user_add(module, client, db_name, user, password, roles): def user_remove(module, client, db_name, user): exists = user_find(client, user) if exists: + if module.check_mode: + module.exit_json(changed=True, user=user) db = client[db_name] db.remove_user(user) else: @@ -212,7 +214,8 @@ def main(): roles=dict(default=None, type='list'), state=dict(default='present', choices=['absent', 'present']), update_password=dict(default="always", choices=["always", "on_create"]), - ) + ), + supports_check_mode=True ) if not pymongo_found: @@ -264,6 +267,9 @@ def main(): if update_password != 'always' and user_find(client, user): password = None + if module.check_mode: + module.exit_json(changed=True, user=user) + try: user_add(module, client, db_name, user, password, roles) except OperationFailure, e: From 738f4cb27d85590850b6b186853479301c619b7d Mon Sep 17 00:00:00 2001 From: nonshankus Date: Sun, 28 Feb 2016 14:36:56 +0100 Subject: [PATCH 1239/2522] Adding missing attributes regarding the hosted zone. --- cloud/amazon/ec2_elb_facts.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cloud/amazon/ec2_elb_facts.py b/cloud/amazon/ec2_elb_facts.py index 549b87bac07..566447805f6 100644 --- a/cloud/amazon/ec2_elb_facts.py +++ b/cloud/amazon/ec2_elb_facts.py @@ -136,6 +136,8 @@ def get_elb_info(connection,elb): 'name': elb.name, 'zones': elb.availability_zones, 'dns_name': elb.dns_name, + 'canonical_hosted_zone_name': elb.canonical_hosted_zone_name, + 'canonical_hosted_zone_name_id': elb.canonical_hosted_zone_name_id, 'instances': [instance.id for instance in elb.instances], 'listeners': get_elb_listeners(elb.listeners), 'scheme': elb.scheme, From 47466242a335abfa2464a8601d6bb52cad1e10cb Mon Sep 17 00:00:00 2001 From: Eike Frost Date: Sun, 28 Feb 2016 19:18:39 +0000 Subject: [PATCH 1240/2522] Add explicit argument_spec types to avoid misinterpretation and subsequent errors (i.e. getting an int instead of the expected str) --- monitoring/zabbix_group.py | 8 ++++---- monitoring/zabbix_host.py | 18 +++++++++--------- monitoring/zabbix_hostmacro.py | 12 ++++++------ monitoring/zabbix_maintenance.py | 10 +++++----- monitoring/zabbix_screen.py | 6 +++--- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/monitoring/zabbix_group.py b/monitoring/zabbix_group.py index 4aad1218789..730f791190d 100644 --- a/monitoring/zabbix_group.py +++ b/monitoring/zabbix_group.py @@ -150,10 +150,10 @@ def get_group_ids(self, host_groups): def main(): module = AnsibleModule( argument_spec=dict( - server_url=dict(required=True, aliases=['url']), - login_user=dict(required=True), - login_password=dict(required=True, no_log=True), - host_groups=dict(required=True, aliases=['host_group']), + server_url=dict(type='str', required=True, aliases=['url']), + login_user=dict(type='str', required=True), + login_password=dict(type='str', required=True, no_log=True), + host_groups=dict(type='list', required=True, aliases=['host_group']), state=dict(default="present", choices=['present','absent']), timeout=dict(type='int', default=10) ), diff --git a/monitoring/zabbix_host.py b/monitoring/zabbix_host.py index 5b6748a3e94..f89569d4b31 100644 --- a/monitoring/zabbix_host.py +++ b/monitoring/zabbix_host.py @@ -367,18 +367,18 @@ def link_or_clear_template(self, host_id, template_id_list): def main(): module = AnsibleModule( argument_spec=dict( - server_url=dict(required=True, aliases=['url']), - login_user=dict(required=True), - login_password=dict(required=True, no_log=True), - host_name=dict(required=True), - host_groups=dict(required=False), - link_templates=dict(required=False), + server_url=dict(type='str', required=True, aliases=['url']), + login_user=dict(rtype='str', equired=True), + login_password=dict(type='str', required=True, no_log=True), + host_name=dict(type='str', required=True), + host_groups=dict(type='list', required=False), + link_templates=dict(type='list', required=False), status=dict(default="enabled", choices=['enabled', 'disabled']), state=dict(default="present", choices=['present', 'absent']), timeout=dict(type='int', default=10), - interfaces=dict(required=False), - force=dict(default=True, type='bool'), - proxy=dict(required=False) + interfaces=dict(type='list', required=False), + force=dict(type='bool', default=True), + proxy=dict(type='str', required=False) ), supports_check_mode=True ) diff --git a/monitoring/zabbix_hostmacro.py b/monitoring/zabbix_hostmacro.py index e8d65370760..7895d8855cf 100644 --- a/monitoring/zabbix_hostmacro.py +++ b/monitoring/zabbix_hostmacro.py @@ -171,12 +171,12 @@ def delete_host_macro(self, host_macro_obj, macro_name): def main(): module = AnsibleModule( argument_spec=dict( - server_url=dict(required=True, aliases=['url']), - login_user=dict(required=True), - login_password=dict(required=True, no_log=True), - host_name=dict(required=True), - macro_name=dict(required=True), - macro_value=dict(required=True), + server_url=dict(type='str', required=True, aliases=['url']), + login_user=dict(type='str', required=True), + login_password=dict(type='str', required=True, no_log=True), + host_name=dict(type='str', required=True), + macro_name=dict(type='str', required=True), + macro_value=dict(type='str', required=True), state=dict(default="present", choices=['present', 'absent']), timeout=dict(type='int', default=10) ), diff --git a/monitoring/zabbix_maintenance.py b/monitoring/zabbix_maintenance.py index 2d611382919..d8b65165815 100644 --- a/monitoring/zabbix_maintenance.py +++ b/monitoring/zabbix_maintenance.py @@ -266,14 +266,14 @@ def main(): module = AnsibleModule( argument_spec=dict( state=dict(required=False, default='present', choices=['present', 'absent']), - server_url=dict(required=True, default=None, aliases=['url']), + server_url=dict(type='str', required=True, default=None, aliases=['url']), host_names=dict(type='list', required=False, default=None, aliases=['host_name']), minutes=dict(type='int', required=False, default=10), host_groups=dict(type='list', required=False, default=None, aliases=['host_group']), - login_user=dict(required=True), - login_password=dict(required=True, no_log=True), - name=dict(required=True), - desc=dict(required=False, default="Created by Ansible"), + login_user=dict(type='str', required=True), + login_password=dict(type='str', required=True, no_log=True), + name=dict(type='str', required=True), + desc=dict(type='str', required=False, default="Created by Ansible"), collect_data=dict(type='bool', required=False, default=True), ), supports_check_mode=True, diff --git a/monitoring/zabbix_screen.py b/monitoring/zabbix_screen.py index 1896899c3a3..f904c550aeb 100644 --- a/monitoring/zabbix_screen.py +++ b/monitoring/zabbix_screen.py @@ -315,9 +315,9 @@ def create_screen_items(self, screen_id, hosts, graph_name_list, width, height, def main(): module = AnsibleModule( argument_spec=dict( - server_url=dict(required=True, aliases=['url']), - login_user=dict(required=True), - login_password=dict(required=True, no_log=True), + server_url=dict(type='str', required=True, aliases=['url']), + login_user=dict(type='str', required=True), + login_password=dict(type='str', required=True, no_log=True), timeout=dict(type='int', default=10), screens=dict(type='list', required=True) ), From 6acc369377feac2750b9c5b4bbc28124117533e1 Mon Sep 17 00:00:00 2001 From: Eike Frost Date: Sun, 28 Feb 2016 20:12:01 +0000 Subject: [PATCH 1241/2522] Check whether interface-list exits before querying its length --- monitoring/zabbix_host.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/monitoring/zabbix_host.py b/monitoring/zabbix_host.py index 5b6748a3e94..267457c4dd6 100644 --- a/monitoring/zabbix_host.py +++ b/monitoring/zabbix_host.py @@ -289,9 +289,11 @@ def get_host_groups_by_host_id(self, host_id): # check the exist_interfaces whether it equals the interfaces or not def check_interface_properties(self, exist_interface_list, interfaces): interfaces_port_list = [] - if len(interfaces) >= 1: - for interface in interfaces: - interfaces_port_list.append(int(interface['port'])) + + if interfaces is not None: + if len(interfaces) >= 1: + for interface in interfaces: + interfaces_port_list.append(int(interface['port'])) exist_interface_ports = [] if len(exist_interface_list) >= 1: From 31acf905a55eaeda0f86f9451aad4725c43b1379 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 29 Feb 2016 09:37:51 -0600 Subject: [PATCH 1242/2522] Exclude modules that are importing requests --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b788d1b820e..86ce1a3a7c0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,5 +16,5 @@ script: - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py' . - python2.6 -m compileall -fq . - python2.7 -m compileall -fq . - - ansible-validate-modules --exclude 'cloud/amazon/sns_topic\.py|messaging/rabbitmq_binding\.py|messaging/rabbitmq_exchange\.py|messaging/rabbitmq_queue\.py|monitoring/circonus_annotation\.py|network/snmp_facts\.py|notification/sns\.py' . + - ansible-validate-modules --exclude 'cloud/amazon/sns_topic\.py|cloud/centurylink/clc_aa_policy\.py|cloud/centurylink/clc_alert_policy\.py|cloud/centurylink/clc_blueprint_package\.py|cloud/centurylink/clc_firewall_policy\.py|cloud/centurylink/clc_group\.py|cloud/centurylink/clc_loadbalancer\.py|cloud/centurylink/clc_modify_server\.py|cloud/centurylink/clc_publicip\.py|cloud/centurylink/clc_server\.py|cloud/centurylink/clc_server_snapshot\.py|cloud/docker/docker_login\.py|messaging/rabbitmq_binding\.py|messaging/rabbitmq_exchange\.py|messaging/rabbitmq_queue\.py|monitoring/circonus_annotation\.py|network/snmp_facts\.py|notification/sns\.py' . #- ./test-docs.sh extras From 369af1c8c93818f4efc4e56d5bfe1130199748cb Mon Sep 17 00:00:00 2001 From: Calvin Walton Date: Mon, 29 Feb 2016 11:16:15 -0500 Subject: [PATCH 1243/2522] zabbix_maintenance: Stop using api removed in zabbix 3.0 --- monitoring/zabbix_maintenance.py | 37 +++++++++----------------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/monitoring/zabbix_maintenance.py b/monitoring/zabbix_maintenance.py index 2d611382919..4581cacd44a 100644 --- a/monitoring/zabbix_maintenance.py +++ b/monitoring/zabbix_maintenance.py @@ -202,18 +202,6 @@ def delete_maintenance(zbx, maintenance_id): return 0, None, None -def check_maintenance(zbx, name): - try: - result = zbx.maintenance.exists( - { - "name": name - } - ) - except BaseException as e: - return 1, None, str(e) - return 0, result, None - - def get_group_ids(zbx, host_groups): group_ids = [] for group in host_groups: @@ -325,11 +313,11 @@ def main(): else: host_ids = [] - (rc, exists, error) = check_maintenance(zbx, name) + (rc, maintenance, error) = get_maintenance_id(zbx, name) if rc != 0: module.fail_json(msg="Failed to check maintenance %s existance: %s" % (name, error)) - if not exists: + if not maintenance: if not host_names and not host_groups: module.fail_json(msg="At least one host_name or host_group must be defined for each created maintenance.") @@ -344,24 +332,19 @@ def main(): if state == "absent": - (rc, exists, error) = check_maintenance(zbx, name) + (rc, maintenance, error) = get_maintenance_id(zbx, name) if rc != 0: module.fail_json(msg="Failed to check maintenance %s existance: %s" % (name, error)) - if exists: - (rc, maintenance, error) = get_maintenance_id(zbx, name) - if rc != 0: - module.fail_json(msg="Failed to get maintenance id: %s" % error) - - if maintenance: - if module.check_mode: + if maintenance: + if module.check_mode: + changed = True + else: + (rc, _, error) = delete_maintenance(zbx, maintenance) + if rc == 0: changed = True else: - (rc, _, error) = delete_maintenance(zbx, maintenance) - if rc == 0: - changed = True - else: - module.fail_json(msg="Failed to remove maintenance: %s" % error) + module.fail_json(msg="Failed to remove maintenance: %s" % error) module.exit_json(changed=changed) From c5fa64998cf836e831f80590c3faf3f3aeafed3f Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Mon, 29 Feb 2016 23:37:46 +0100 Subject: [PATCH 1244/2522] rename yumrepo to yum_repository --- packaging/os/{yumrepo.py => yum_repository.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packaging/os/{yumrepo.py => yum_repository.py} (100%) diff --git a/packaging/os/yumrepo.py b/packaging/os/yum_repository.py similarity index 100% rename from packaging/os/yumrepo.py rename to packaging/os/yum_repository.py From 7c38f8ff7543270ad227f16a90ed6bfe412300cc Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Mon, 29 Feb 2016 23:40:00 +0100 Subject: [PATCH 1245/2522] doc: replace yumrepo with yum_repository --- packaging/os/yum_repository.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packaging/os/yum_repository.py b/packaging/os/yum_repository.py index 0c6ec1c5c05..6047a8b2ef4 100644 --- a/packaging/os/yum_repository.py +++ b/packaging/os/yum_repository.py @@ -25,7 +25,7 @@ DOCUMENTATION = ''' --- -module: yumrepo +module: yum_repository author: Jiri Tyr (@jtyr) version_added: '2.1' short_description: Add and remove YUM repositories @@ -302,20 +302,20 @@ EXAMPLES = ''' - name: Add repository - yumrepo: + yum_repository: name: epel description: EPEL YUM repo baseurl: http://download.fedoraproject.org/pub/epel/$releasever/$basearch/ - name: Add multiple repositories into the same file (1/2) - yumrepo: + yum_repository: name: epel description: EPEL YUM repo file: external_repos baseurl: http://download.fedoraproject.org/pub/epel/$releasever/$basearch/ gpgcheck: no - name: Add multiple repositories into the same file (2/2) - yumrepo: + yum_repository: name: rpmforge description: RPMforge YUM repo file: external_repos @@ -324,18 +324,18 @@ enabled: no - name: Remove repository - yumrepo: + yum_repository: name: epel state: absent - name: Remove repository from a specific repo file - yumrepo: + yum_repository: name: epel file: external_repos state: absent # -# Allow to overwrite the yumrepo parameters by defining the parameters +# Allow to overwrite the yum_repository parameters by defining the parameters # as a variable in the defaults or vars file: # # my_role_somerepo_params: @@ -345,7 +345,7 @@ # gpgkey: null # - name: Add Some repo - yumrepo: + yum_repository: name: somerepo description: Some YUM repo baseurl: http://server.com/path/to/the/repo From 2810e6542a776db97aff18b9196e034f1546bfab Mon Sep 17 00:00:00 2001 From: Eike Frost Date: Mon, 29 Feb 2016 22:58:23 +0000 Subject: [PATCH 1246/2522] Replace deprecated exists API for Zabbix 3.0 compatibility --- monitoring/zabbix_group.py | 2 +- monitoring/zabbix_hostmacro.py | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/monitoring/zabbix_group.py b/monitoring/zabbix_group.py index 730f791190d..4fc9631af9c 100644 --- a/monitoring/zabbix_group.py +++ b/monitoring/zabbix_group.py @@ -114,7 +114,7 @@ def create_host_group(self, group_names): try: group_add_list = [] for group_name in group_names: - result = self._zapi.hostgroup.exists({'name': group_name}) + result = self._zapi.hostgroup.get({'filter': {'name': group_name}}) if not result: try: if self._module.check_mode: diff --git a/monitoring/zabbix_hostmacro.py b/monitoring/zabbix_hostmacro.py index 7895d8855cf..59193e257c4 100644 --- a/monitoring/zabbix_hostmacro.py +++ b/monitoring/zabbix_hostmacro.py @@ -108,11 +108,6 @@ def __init__(self, module, zbx): self._module = module self._zapi = zbx - # exist host - def is_host_exist(self, host_name): - result = self._zapi.host.exists({'host': host_name}) - return result - # get host id by host name def get_host_id(self, host_name): try: From 7547b1db8a512f1853afb7d64de49dea0924f9d6 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 1 Mar 2016 11:12:30 -0600 Subject: [PATCH 1247/2522] Fix version_added for recently added modules --- windows/win_regmerge.py | 3 ++- windows/win_timezone.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/windows/win_regmerge.py b/windows/win_regmerge.py index 53952e71d12..854d6b3aa0e 100644 --- a/windows/win_regmerge.py +++ b/windows/win_regmerge.py @@ -24,7 +24,7 @@ DOCUMENTATION = ''' --- module: win_regmerge -version_added: "2.0" +version_added: "2.1" short_description: Merges the contents of a registry file into the windows registry description: - Wraps the reg.exe command to import the contents of a registry file. @@ -68,3 +68,4 @@ compare_to: HKLM:\SOFTWARE\myCompany ''' +RETURN = '''# ''' diff --git a/windows/win_timezone.py b/windows/win_timezone.py index abe52be1680..ed238ff201a 100644 --- a/windows/win_timezone.py +++ b/windows/win_timezone.py @@ -24,7 +24,7 @@ DOCUMENTATION = ''' --- module: win_timezone -version_added: "2.0" +version_added: "2.1" short_description: Sets Windows machine timezone description: - Sets machine time to the specified timezone, the module will check if the provided timezone is supported on the machine. @@ -45,3 +45,5 @@ win_timezone: timezone: "Central Standard Time" ''' + +RETURN = '''# ''' From 7df0aff7ddad5507f6f2a6097c6971876d3b68d4 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 1 Mar 2016 14:22:08 -0600 Subject: [PATCH 1248/2522] DOCUMENTATION fixes for a few modules --- cloud/amazon/ec2_vpc_dhcp_options.py | 3 +-- windows/win_regmerge.py | 2 +- windows/win_timezone.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/cloud/amazon/ec2_vpc_dhcp_options.py b/cloud/amazon/ec2_vpc_dhcp_options.py index 813316cf974..106f91259c2 100644 --- a/cloud/amazon/ec2_vpc_dhcp_options.py +++ b/cloud/amazon/ec2_vpc_dhcp_options.py @@ -29,8 +29,7 @@ domain_name: description: - The domain name to set in the DHCP option sets - required: - - false + required: false default: "" dns_servers: description: diff --git a/windows/win_regmerge.py b/windows/win_regmerge.py index 854d6b3aa0e..3e2f51fe869 100644 --- a/windows/win_regmerge.py +++ b/windows/win_regmerge.py @@ -31,7 +31,7 @@ - Suitable for use with registry files created using M(win_template). - Windows registry files have a specific format and must be constructed correctly with carriage return and line feed line endings otherwise they will not be merged. - Exported registry files often start with a Byte Order Mark which must be removed if the file is to templated using M(win_template). - - Registry file format is described here: https://support.microsoft.com/en-us/kb/310516 + - Registry file format is described at U(https://support.microsoft.com/en-us/kb/310516) - See also M(win_template), M(win_regedit) options: path: diff --git a/windows/win_timezone.py b/windows/win_timezone.py index ed238ff201a..2f7cf1fdc4b 100644 --- a/windows/win_timezone.py +++ b/windows/win_timezone.py @@ -31,7 +31,7 @@ options: timezone: description: - - Timezone to set to. Example: Central Standard Time + - Timezone to set to. Example Central Standard Time required: true default: null aliases: [] From f5329eb3370da969de6bbf6f6b33a3c8b43e7538 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 1 Mar 2016 14:22:28 -0600 Subject: [PATCH 1249/2522] Don't call sys.exit in sns_topic, use HAS_BOTO to fail --- .travis.yml | 2 +- cloud/amazon/sns_topic.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 86ce1a3a7c0..5e09a9a447f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,5 +16,5 @@ script: - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py' . - python2.6 -m compileall -fq . - python2.7 -m compileall -fq . - - ansible-validate-modules --exclude 'cloud/amazon/sns_topic\.py|cloud/centurylink/clc_aa_policy\.py|cloud/centurylink/clc_alert_policy\.py|cloud/centurylink/clc_blueprint_package\.py|cloud/centurylink/clc_firewall_policy\.py|cloud/centurylink/clc_group\.py|cloud/centurylink/clc_loadbalancer\.py|cloud/centurylink/clc_modify_server\.py|cloud/centurylink/clc_publicip\.py|cloud/centurylink/clc_server\.py|cloud/centurylink/clc_server_snapshot\.py|cloud/docker/docker_login\.py|messaging/rabbitmq_binding\.py|messaging/rabbitmq_exchange\.py|messaging/rabbitmq_queue\.py|monitoring/circonus_annotation\.py|network/snmp_facts\.py|notification/sns\.py' . + - ansible-validate-modules --exclude 'cloud/centurylink/clc_aa_policy\.py|cloud/centurylink/clc_alert_policy\.py|cloud/centurylink/clc_blueprint_package\.py|cloud/centurylink/clc_firewall_policy\.py|cloud/centurylink/clc_group\.py|cloud/centurylink/clc_loadbalancer\.py|cloud/centurylink/clc_modify_server\.py|cloud/centurylink/clc_publicip\.py|cloud/centurylink/clc_server\.py|cloud/centurylink/clc_server_snapshot\.py|cloud/docker/docker_login\.py|messaging/rabbitmq_binding\.py|messaging/rabbitmq_exchange\.py|messaging/rabbitmq_queue\.py|monitoring/circonus_annotation\.py|network/snmp_facts\.py|notification/sns\.py' . #- ./test-docs.sh extras diff --git a/cloud/amazon/sns_topic.py b/cloud/amazon/sns_topic.py index 9b4ae3f0516..71a66318290 100755 --- a/cloud/amazon/sns_topic.py +++ b/cloud/amazon/sns_topic.py @@ -134,9 +134,9 @@ try: import boto import boto.sns + HAS_BOTO = True except ImportError: - print "failed=True msg='boto required for this module'" - sys.exit(1) + HAS_BOTO = False def canonicalize_endpoint(protocol, endpoint): @@ -186,6 +186,9 @@ def main(): module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + if not HAS_BOTO: + module.fail_json(msg='boto required for this module') + name = module.params.get('name') state = module.params.get('state') display_name = module.params.get('display_name') From 767dd4bdc6d2e382787c412c6d52f2b9470783a9 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 1 Mar 2016 16:18:30 -0600 Subject: [PATCH 1250/2522] Choices should be a list of true/false not the string BOOLEANS --- cloud/webfaction/webfaction_site.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cloud/webfaction/webfaction_site.py b/cloud/webfaction/webfaction_site.py index 0226a2a2f92..1f1a4bf9030 100644 --- a/cloud/webfaction/webfaction_site.py +++ b/cloud/webfaction/webfaction_site.py @@ -59,7 +59,9 @@ description: - Whether or not to use HTTPS required: false - choices: BOOLEANS + choices: + - true + - false default: 'false' site_apps: From 2a082deca872b053760644a42ec3a4bbcdf0abbc Mon Sep 17 00:00:00 2001 From: Andy Nelson Date: Tue, 9 Feb 2016 12:59:44 +0000 Subject: [PATCH 1251/2522] Updated ec2_vpc_dhcp_options --- cloud/amazon/ec2_vpc_dhcp_options.py | 335 +++++++++++++++++++-------- 1 file changed, 241 insertions(+), 94 deletions(-) diff --git a/cloud/amazon/ec2_vpc_dhcp_options.py b/cloud/amazon/ec2_vpc_dhcp_options.py index 106f91259c2..99b6fbbd6a2 100644 --- a/cloud/amazon/ec2_vpc_dhcp_options.py +++ b/cloud/amazon/ec2_vpc_dhcp_options.py @@ -16,13 +16,21 @@ DOCUMENTATION = """ --- module: ec2_vpc_dhcp_options -short_description: Ensures the DHCP options for the given VPC match what's +short_description: Manages DHCP Options, and can ensure the DHCP options for the given VPC match what's requested description: - - Converges the DHCP option set for the given VPC to the variables requested. + - This module removes, or creates DHCP option sets, and can associate them to a VPC. + Optionally, a new DHCP Options set can be created that converges a VPC's existing + DHCP option set with values provided. + When dhcp_options_id is provided, the module will + 1. remove (with state='absent') + 2. ensure tags are applied (if state='present' and tags are provided + 3. attach it to a VPC (if state='present' and a vpc_id is provided. If any of the optional values are missing, they will either be treated - as a no-op (i.e., inherit what already exists for the VPC) or a purge of - existing options. Most of the options should be self-explanatory. + as a no-op (i.e., inherit what already exists for the VPC) + To remove existing options while inheriting, supply an empty value + (e.g. set ntp_servers to [] if you want to remove them from the VPC's options) + Most of the options should be self-explanatory. author: "Joel Thompson (@joelthompson)" version_added: 2.1 options: @@ -30,37 +38,40 @@ description: - The domain name to set in the DHCP option sets required: false - default: "" + default: None dns_servers: description: - A list of hosts to set the DNS servers for the VPC to. (Should be a list of IP addresses rather than host names.) required: false - default: [] + default: None ntp_servers: description: - List of hosts to advertise as NTP servers for the VPC. required: false - default: [] + default: None netbios_name_servers: description: - List of hosts to advertise as NetBIOS servers. required: false - default: [] + default: None netbios_node_type: description: - - NetBIOS node type to advertise in the DHCP options. The - default is 2, per AWS recommendation + - NetBIOS node type to advertise in the DHCP options. + The AWS recommendation is to use 2 (when using netbios name services) http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_DHCP_Options.html required: false - default: 2 + default: None vpc_id: description: - - VPC ID to associate with the requested DHCP option set - required: true + - VPC ID to associate with the requested DHCP option set. + If no vpc id is provided, and no matching option set is found then a new + DHCP option set is created. + required: false + default: None delete_old: description: - - Whether to delete the old VPC DHCP option set when creating a new one. + - Whether to delete the old VPC DHCP option set when associating a new one. This is primarily useful for debugging/development purposes when you want to quickly roll back to the old option set. Note that this setting will be ignored, and the old DHCP option set will be preserved, if it @@ -74,6 +85,28 @@ reset them to be empty. required: false default: false + tags: + description: + - Tags to be applied to a VPC options set if a new one is created, or + if the resource_id is provided. (options must match) + required: False + default: None + aliases: [ 'resource_tags'] + dhcp_options_id: + description: + - The resource_id of an existing DHCP options set. + If this is specified, then it will override other settings, except tags + (which will be updated to match) + required: False + default: None + state: + description: + - create/assign or remove the DHCP options. + If state is set to absent, then a DHCP options set matched either + by id, or tags and options will be removed if possible. + required: False + default: present + choices: [ 'absent', 'present' ] extends_documentation_fragment: aws requirements: - boto @@ -81,19 +114,26 @@ RETURN = """ new_options: - description: The new DHCP options associated with your VPC - returned: changed - type: dict - sample: - domain-name-servers: - - 10.0.0.1 - - 10.0.1.1 - netbois-name-servers: - - 10.0.0.1 - - 10.0.1.1 - ntp-servers: None - netbios-node-type: 2 - domain-name: "my.example.com" + description: The DHCP options created, associated or found + returned: when appropriate + type: dict + sample: + domain-name-servers: + - 10.0.0.1 + - 10.0.1.1 + netbois-name-servers: + - 10.0.0.1 + - 10.0.1.1 + netbios-node-type: 2 + domain-name: "my.example.com" +dhcp_options_id: + description: The aws resource id of the primary DCHP options set created, found or removed + type: string + returned: when available +changed: + description: Whether the dhcp options were changed + type: bool + returned: always """ EXAMPLES = """ @@ -127,105 +167,212 @@ vpc_id: vpc-123456 inherit_existing: True delete_old: False + + +## Create a DHCP option set with 4.4.4.4 and 8.8.8.8 as the specified DNS servers, with tags +## but do not assign to a VPC +- ec2_vpc_dhcp_options: + region: us-east-1 + dns_servers: + - 4.4.4.4 + - 8.8.8.8 + tags: + Name: google servers + Environment: Test + +## Delete a DHCP options set that matches the tags and options specified +- ec2_vpc_dhcp_options: + region: us-east-1 + dns_servers: + - 4.4.4.4 + - 8.8.8.8 + tags: + Name: google servers + Environment: Test + state: absent + +## Associate a DHCP options set with a VPC by ID +- ec2_vpc_dhcp_options: + region: us-east-1 + dhcp_options_id: dopt-12345678 + vpc_id: vpc-123456 + """ import boto.vpc +import boto.ec2 +from boto.exception import EC2ResponseError import socket import collections -def _get_associated_dhcp_options(vpc_id, vpc_connection): +def get_resource_tags(vpc_conn, resource_id): + return dict((t.name, t.value) for t in vpc_conn.get_all_tags(filters={'resource-id': resource_id})) + +def ensure_tags(vpc_conn, resource_id, tags, add_only, check_mode): + try: + cur_tags = get_resource_tags(vpc_conn, resource_id) + if tags == cur_tags: + return {'changed': False, 'tags': cur_tags} + + to_delete = dict((k, cur_tags[k]) for k in cur_tags if k not in tags) + if to_delete and not add_only: + vpc_conn.delete_tags(resource_id, to_delete, dry_run=check_mode) + + to_add = dict((k, tags[k]) for k in tags if k not in cur_tags) + if to_add: + vpc_conn.create_tags(resource_id, to_add, dry_run=check_mode) + + latest_tags = get_resource_tags(vpc_conn, resource_id) + return {'changed': True, 'tags': latest_tags} + except EC2ResponseError as e: + module.fail_json(msg=get_error_message(e.args[2])) + +def fetch_dhcp_options_for_vpc(vpc_conn, vpc_id): """ Returns the DHCP options object currently associated with the requested VPC ID using the VPC connection variable. """ - vpcs = vpc_connection.get_all_vpcs(vpc_ids=[vpc_id]) - if len(vpcs) != 1: - return None - dhcp_options = vpc_connection.get_all_dhcp_options(dhcp_options_ids=[vpcs[0].dhcp_options_id]) + vpcs = vpc_conn.get_all_vpcs(vpc_ids=[vpc_id]) + if len(vpcs) != 1 or vpcs[0].dhcp_options_id == "default": + return None + dhcp_options = vpc_conn.get_all_dhcp_options(dhcp_options_ids=[vpcs[0].dhcp_options_id]) if len(dhcp_options) != 1: - return None + return None return dhcp_options[0] +def match_dhcp_options(vpc_conn, tags=None, options=None): + """ + Finds a DHCP Options object that optionally matches the tags and options provided + """ + dhcp_options = vpc_conn.get_all_dhcp_options() + for dopts in dhcp_options: + if (not tags) or get_resource_tags(vpc_conn, dopts.id) == tags: + if (not options) or dopts.options == options: + return(True, dopts) + return(False, None) -def _get_vpcs_by_dhcp_options(dhcp_options_id, vpc_connection): - return vpc_connection.get_all_vpcs(filters={'dhcpOptionsId': dhcp_options_id}) - - -def _get_updated_option(requested, existing, inherit): - if inherit and (not requested or requested == ['']): - return existing +def remove_dhcp_options_by_id(vpc_conn, dhcp_options_id): + associations = vpc_conn.get_all_vpcs(filters={'dhcpOptionsId': dhcp_options_id}) + if len(associations) > 0: + return False else: - return requested - + vpc_conn.delete_dhcp_options(dhcp_options_id) + return True def main(): argument_spec = ec2_argument_spec() argument_spec.update(dict( - domain_name=dict(type='str', default=''), - dns_servers=dict(type='list', default=[]), - ntp_servers=dict(type='list', default=[]), - netbios_name_servers=dict(type='list', default=[]), - netbios_node_type=dict(type='int', default=2), - vpc_id=dict(type='str', required=True), + dhcp_options_id=dict(type='str', default=None), + domain_name=dict(type='str', default=None), + dns_servers=dict(type='list', default=None), + ntp_servers=dict(type='list', default=None), + netbios_name_servers=dict(type='list', default=None), + netbios_node_type=dict(type='int', default=None), + vpc_id=dict(type='str', default=None), delete_old=dict(type='bool', default=True), - inherit_existing=dict(type='bool', default=False) + inherit_existing=dict(type='bool', default=False), + tags=dict(type='dict', default=None, aliases=['resource_tags']), + state=dict(type='str', default='present', choices=['present', 'absent']) ) ) module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) params = module.params + found = False + changed = False + new_options = collections.defaultdict(lambda: None) region, ec2_url, boto_params = get_aws_connection_info(module) connection = connect_to_aws(boto.vpc, region, **boto_params) - - inherit_existing = params['inherit_existing'] - - existing_options = _get_associated_dhcp_options(params['vpc_id'], connection) - new_options = collections.defaultdict(lambda: None) - - new_options['domain-name-servers'] = _get_updated_option( params['dns_servers'], - existing_options.options.get('domain-name-servers'), inherit_existing) - - new_options['netbios-name-servers'] = _get_updated_option(params['netbios_name_servers'], - existing_options.options.get('netbios-name-servers'), inherit_existing) - - - new_options['ntp-servers'] = _get_updated_option(params['ntp_servers'], - existing_options.options.get('ntp-servers'), inherit_existing) - - # HACK: Why do I make the next two lists? The boto api returns a list if present, so - # I need this to properly compare so == works. - - # HACK: netbios-node-type is an int, but boto returns a string. So, asking for an int from Ansible - # for data validation, but still need to cast it to a string - new_options['netbios-node-type'] = _get_updated_option( - [str(params['netbios_node_type'])], existing_options.options.get('netbios-node-type'), - inherit_existing) - - new_options['domain-name'] = _get_updated_option( - [params['domain_name']], existing_options.options.get('domain-name'), inherit_existing) - - if existing_options and new_options == existing_options.options: - module.exit_json(changed=False) - - if new_options['netbios-node-type']: - new_options['netbios-node-type'] = new_options['netbios-node-type'][0] - - if new_options['domain-name']: - new_options['domain-name'] = new_options['domain-name'][0] - - if not module.check_mode: - dhcp_option = connection.create_dhcp_options(new_options['domain-name'], - new_options['domain-name-servers'], new_options['ntp-servers'], - new_options['netbios-name-servers'], new_options['netbios-node-type']) + + existing_options = None + + # First check if we were given a dhcp_options_id + if not params['dhcp_options_id']: + # No, so create new_options from the parameters + if params['dns_servers'] != None: + new_options['domain-name-servers'] = params['dns_servers'] + if params['netbios_name_servers'] != None: + new_options['netbios-name-servers'] = params['netbios_name_servers'] + if params['ntp_servers'] != None: + new_options['ntp-servers'] = params['ntp_servers'] + if params['domain_name'] != None: + # needs to be a list for comparison with boto objects later + new_options['domain-name'] = [ params['domain_name'] ] + if params['netbios_node_type'] != None: + # needs to be a list for comparison with boto objects later + new_options['netbios-node-type'] = [ str(params['netbios_node_type']) ] + # If we were given a vpc_id then we need to look at the options on that + if params['vpc_id']: + existing_options = fetch_dhcp_options_for_vpc(connection, params['vpc_id']) + # if we've been asked to inherit existing options, do that now + if params['inherit_existing']: + if existing_options: + for option in [ 'domain-name-servers', 'netbios-name-servers', 'ntp-servers', 'domain-name', 'netbios-node-type']: + if existing_options.options.get(option) and new_options[option] != [] and (not new_options[option] or [''] == new_options[option]): + new_options[option] = existing_options.options.get(option) + + # Do the vpc's dhcp options already match what we're asked for? if so we are done + if existing_options and new_options == existing_options.options: + module.exit_json(changed=changed, new_options=new_options, dhcp_options_id=existing_options.id) + + # If no vpc_id was given, or the options don't match then look for an existing set using tags + found, dhcp_option = match_dhcp_options(connection, params['tags'], new_options) + + # Now let's cover the case where there are existing options that we were told about by id + # If a dhcp_options_id was supplied we don't look at options inside, just set tags (if given) + else: + supplied_options = connection.get_all_dhcp_options(filters={'dhcp-options-id':params['dhcp_options_id']}) + if len(supplied_options) != 1: + if params['state'] != 'absent': + module.fail_json(msg=" a dhcp_options_id was supplied, but does not exist") + else: + found = True + dhcp_option = supplied_options[0] + if params['state'] != 'absent' and params['tags']: + ensure_tags(connection, dhcp_option.id, params['tags'], False, module.check_mode) + + # Now we have the dhcp options set, let's do the necessary + + # if we found options we were asked to remove then try to do so + if params['state'] == 'absent': + if not module.check_mode: + if found: + changed = remove_dhcp_options_by_id(connection, dhcp_option.id) + module.exit_json(changed=changed, new_options={}) + + # otherwise if we haven't found the required options we have something to do + elif not module.check_mode and not found: + + # create some dhcp options if we weren't able to use existing ones + if not found: + # Convert netbios-node-type and domain-name back to strings + if new_options['netbios-node-type']: + new_options['netbios-node-type'] = new_options['netbios-node-type'][0] + if new_options['domain-name']: + new_options['domain-name'] = new_options['domain-name'][0] + + # create the new dhcp options set requested + dhcp_option = connection.create_dhcp_options( + new_options['domain-name'], + new_options['domain-name-servers'], + new_options['ntp-servers'], + new_options['netbios-name-servers'], + new_options['netbios-node-type']) + changed = True + if params['tags']: + ensure_tags(connection, dhcp_option.id, params['tags'], False, module.check_mode) + + # If we were given a vpc_id, then attach the options we now have to that before we finish + if params['vpc_id'] and not module.check_mode: + changed = True connection.associate_dhcp_options(dhcp_option.id, params['vpc_id']) + # and remove old ones if that was requested if params['delete_old'] and existing_options: - other_vpcs = _get_vpcs_by_dhcp_options(existing_options.id, connection) - if len(other_vpcs) == 0 or (len(other_vpcs) == 1 and other_vpcs[0].id == params['vpc_id']): - connection.delete_dhcp_options(existing_options.id) + remove_dhcp_options_by_id(connection, existing_options.id) - module.exit_json(changed=True, new_options=new_options) + module.exit_json(changed=changed, new_options=new_options, dhcp_options_id=dhcp_option.id) from ansible.module_utils.basic import * From 72b1ad46b99ec25c90d0c8362051b48fca055900 Mon Sep 17 00:00:00 2001 From: Marcos Diez Date: Wed, 2 Mar 2016 12:04:28 +0200 Subject: [PATCH 1252/2522] Updated database/misc/mongodb_user.py, the docs now explain how to add a read user to the local/oplog db --- database/misc/mongodb_user.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index 12d348e9a92..2bc29586d07 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -82,8 +82,9 @@ roles: version_added: "1.3" description: - - "The database user roles valid values are one or more of the following: read, 'readWrite', 'dbAdmin', 'userAdmin', 'clusterAdmin', 'readAnyDatabase', 'readWriteAnyDatabase', 'userAdminAnyDatabase', 'dbAdminAnyDatabase'" - - This param requires mongodb 2.4+ and pymongo 2.5+ + - "The database user roles valid values could either be one or more of the following strings: 'read', 'readWrite', 'dbAdmin', 'userAdmin', 'clusterAdmin', 'readAnyDatabase', 'readWriteAnyDatabase', 'userAdminAnyDatabase', 'dbAdminAnyDatabase'" + - "Or the following dictionary '{ db: DATABASE_NAME, role: ROLE_NAME }'." + - "This param requires pymongo 2.5+. If it is a string, mongodb 2.4+ is also required. If it is a dictionary, mongo 2.6+ is required." required: false default: "readWrite" state: @@ -125,6 +126,22 @@ # add a user to database in a replica set, the primary server is automatically discovered and written to - mongodb_user: database=burgers name=bob replica_set=belcher password=12345 roles='readWriteAnyDatabase' state=present + +# add a user 'oplog_reader' with read only access to the 'local' database on the replica_set 'belcher'. This is usefull for oplog access (MONGO_OPLOG_URL). +# please notice the credentials must be added to the 'admin' database because the 'local' database is not syncronized and can't receive user credentials +# To login with such user, the connection string should be MONGO_OPLOG_URL="mongodb://oplog_reader:oplog_reader_password@server1,server2/local?authSource=admin" +# This syntax requires mongodb 2.6+ and pymongo 2.5+ +- mongodb_user: + login_user: root + login_password: root_password + database: admin + user: oplog_reader + password: oplog_reader_password + state: present + replica_set: belcher + roles: + - { db: "local" , role: "read" } + ''' import ConfigParser @@ -223,7 +240,7 @@ def main(): login_host = module.params['login_host'] login_port = module.params['login_port'] login_database = module.params['login_database'] - + replica_set = module.params['replica_set'] db_name = module.params['database'] user = module.params['name'] From 1c36ff10a0d141162f5754e9376c052f766e6872 Mon Sep 17 00:00:00 2001 From: Corwin Brown Date: Wed, 2 Mar 2016 08:36:53 -0600 Subject: [PATCH 1253/2522] Converting result to snake_case before returning --- windows/win_uri.ps1 | 15 +++++++++++++-- windows/win_uri.py | 18 +++++++++--------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/windows/win_uri.ps1 b/windows/win_uri.ps1 index 3dd1d491bf1..b02418e8912 100644 --- a/windows/win_uri.ps1 +++ b/windows/win_uri.ps1 @@ -1,7 +1,7 @@ #!powershell # This file is part of Ansible # -# Copyright 2015, Corwin Brown +# Copyright 2015, Corwin Brown # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -25,6 +25,15 @@ $result = New-Object psobject @{ win_uri = New-Object psobject } +# Functions ############################################### + +Function ConvertTo-SnakeCase($input_string) { + $snake_case = $input_string -csplit "(? +# (c) 2015, Corwin Brown # # This file is part of Ansible # @@ -27,7 +27,7 @@ version_added: "2.1" short_description: Interacts with webservices. description: - - Interacts with HTTP and HTTPS services. + - Interacts with HTTP and HTTPS web services and supports Digest, Basic and WSSE HTTP authentication mechanisms. options: url: description: @@ -55,7 +55,7 @@ - The body of the HTTP request/response to the web service. headers: description: - - Key Value pairs for headers. Example "Host: www.somesite.com" + - 'Key Value pairs for headers. Example "Host: www.somesite.com"' use_basic_parsing: description: - This module relies upon 'Invoke-WebRequest', which by default uses the Internet Explorer Engine to parse a webpage. There's an edge-case where if a user hasn't run IE before, this will fail. The only advantage to using the Internet Explorer praser is that you can traverse the DOM in a powershell script. That isn't useful for Ansible, so by default we toggle 'UseBasicParsing'. However, you can toggle that off here. @@ -81,7 +81,7 @@ url: http://my.internal.server.com method: GET headers: - host: "www.somesite.com + host: "www.somesite.com" # Do a HEAD request on an endpoint --- @@ -120,27 +120,27 @@ returned: always type: bool sample: True -StatusCode: +status_code: description: The HTTP Status Code of the response. returned: success type: int sample: 200 -StatusDescription: +status_description: description: A summery of the status. returned: success type: string stample: "OK" -RawContent: +raw_content: description: The raw content of the HTTP response. returned: success type: string sample: 'HTTP/1.1 200 OK\nX-XSS-Protection: 1; mode=block\nX-Frame-Options: SAMEORIGIN\nAlternate-Protocol: 443:quic,p=1\nAlt-Svc: quic="www.google.com:443"; ma=2592000; v="30,29,28,27,26,25",quic=":443"; ma=2...' -Headers: +headers: description: The Headers of the response. returned: success type: dict sample: {"Content-Type": "application/json"} -RawContentLength: +raw_content_length: description: The byte size of the response. returned: success type: int From 1e05ee5b3b896651f8b733eb7a23ced6517b6b64 Mon Sep 17 00:00:00 2001 From: Fernando J Pando Date: Wed, 2 Mar 2016 12:30:07 -0500 Subject: [PATCH 1254/2522] author added --- cloud/amazon/sns_topic.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cloud/amazon/sns_topic.py b/cloud/amazon/sns_topic.py index 567ae607ba2..4b3832cc43c 100755 --- a/cloud/amazon/sns_topic.py +++ b/cloud/amazon/sns_topic.py @@ -21,7 +21,9 @@ description: - The M(sns_topic) module allows you to create, delete, and manage subscriptions for AWS SNS topics. version_added: 2.0 -author: "Joel Thompson (@joelthompson)" +author: + - "Joel Thompson (@joelthompson)" + - "Fernando Jose Pando (@nand0p)" options: name: description: From 68d906e8c425e55c4c8efe3c48831e8f77b8f9ad Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Thu, 3 Mar 2016 00:50:11 +0100 Subject: [PATCH 1255/2522] Fix instantiation of openstack_cloud object in os_project The os_project module instantiates the openstack cloud object by passing the module params kwargs. As the params contain a key named 'domain_id', this is used for domain in the OpenStack connection, instead of the domain value the user specifies on the OSCC clouds.yaml or OpenStack envvars. This fix corrects this by popping the 'domain_id' key, so it we keep the value but it's not passed later on module.params. --- cloud/openstack/os_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/openstack/os_project.py b/cloud/openstack/os_project.py index b159264ebd4..b900d42f164 100644 --- a/cloud/openstack/os_project.py +++ b/cloud/openstack/os_project.py @@ -156,7 +156,7 @@ def main(): name = module.params['name'] description = module.params['description'] - domain = module.params['domain_id'] + domain = module.params.pop('domain_id') enabled = module.params['enabled'] state = module.params['state'] From 23c4e8de4aaf2f167fda974bf269697ab303fc67 Mon Sep 17 00:00:00 2001 From: Jiri Tyr Date: Wed, 2 Mar 2016 16:45:09 +0000 Subject: [PATCH 1256/2522] Adding more options to the yum_repository module --- packaging/os/yum_repository.py | 282 +++++++++++++++++++++++++-------- 1 file changed, 220 insertions(+), 62 deletions(-) diff --git a/packaging/os/yum_repository.py b/packaging/os/yum_repository.py index 6047a8b2ef4..41e3e9e58af 100644 --- a/packaging/os/yum_repository.py +++ b/packaging/os/yum_repository.py @@ -1,7 +1,7 @@ #!/usr/bin/python # encoding: utf-8 -# (c) 2015, Jiri Tyr +# (c) 2015-2016, Jiri Tyr # # This file is part of Ansible # @@ -33,6 +33,13 @@ - Add or remove YUM repositories in RPM-based Linux distributions. options: + async: + required: false + choices: ['yes', 'no'] + default: 'yes' + description: + - If set to C(yes) Yum will download packages and metadata from this + repo in parallel, if possible. bandwidth: required: false default: 0 @@ -45,22 +52,40 @@ throttling). baseurl: required: false - default: None + default: null description: - URL to the directory where the yum repository's 'repodata' directory lives. - - This or the I(mirrorlist) parameter is required. + - This or the I(mirrorlist) parameter is required if I(state) is set to + C(present). cost: required: false default: 1000 description: - Relative cost of accessing this repository. Useful for weighing one repo's packages as greater/less than any other. + deltarpm_metadata_percentage: + required: false + default: 100 + description: + - When the relative size of deltarpm metadata vs pkgs is larger than + this, deltarpm metadata is not downloaded from the repo. Note that you + can give values over C(100), so C(200) means that the metadata is + required to be half the size of the packages. Use C(0) to turn off + this check, and always download metadata. + deltarpm_percentage: + required: false + default: 75 + description: + - When the relative size of delta vs pkg is larger than this, delta is + not used. Use C(0) to turn off delta rpm processing. Local repositories + (with file:// I(baseurl)) have delta rpms turned off by default. description: required: false - default: None + default: null description: - A human readable string describing the repository. + - This parameter is only required if I(state) is set to C(present). enabled: required: false choices: ['yes', 'no'] @@ -76,7 +101,7 @@ repository. exclude: required: false - default: None + default: null description: - List of packages to exclude from updates or installs. This should be a space separated list. Shell globs using wildcards (eg. C(*) and C(?)) @@ -90,16 +115,16 @@ - C(roundrobin) randomly selects a URL out of the list of URLs to start with and proceeds through each of them as it encounters a failure contacting the host. - - C(priority) starts from the first baseurl listed and reads through them - sequentially. + - C(priority) starts from the first I(baseurl) listed and reads through + them sequentially. file: required: false - default: None + default: null description: - File to use to save the repo in. Defaults to the value of I(name). gpgcakey: required: false - default: None + default: null description: - A URL pointing to the ASCII-armored CA key file for the repository. gpgcheck: @@ -111,7 +136,7 @@ packages. gpgkey: required: false - default: None + default: null description: - A URL pointing to the ASCII-armored GPG key file for the repository. http_caching: @@ -125,15 +150,31 @@ - C(packages) means that only RPM package downloads should be cached (but not repository metadata downloads). - C(none) means that no HTTP downloads should be cached. + include: + required: false + default: null + description: + - Include external configuration file. Both, local path and URL is + supported. Configuration file will be inserted at the position of the + I(include=) line. Included files may contain further include lines. + Yum will abort with an error if an inclusion loop is detected. includepkgs: required: false - default: None + default: null description: - List of packages you want to only use from a repository. This should be a space separated list. Shell globs using wildcards (eg. C(*) and C(?)) are allowed. Substitution variables (e.g. C($releasever)) are honored here. - The list can also be a regular YAML array. + ip_resolve: + required: false + choices: [4, 6, IPv4, IPv6, whatever] + default: whatever + description: + - Determines how yum resolves host names. + - C(4) or C(IPv4) - resolve to IPv4 addresses only. + - C(6) or C(IPv6) - resolve to IPv6 addresses only. keepalive: required: false choices: ['yes', 'no'] @@ -142,25 +183,54 @@ - This tells yum whether or not HTTP/1.1 keepalive should be used with this repository. This can improve transfer speeds by using one connection when downloading multiple files from a repository. + keepcache: + required: false + choices: ['0', '1'] + default: '1' + description: + - Either C(1) or C(0). Determines whether or not yum keeps the cache of + headers and packages after successful installation. metadata_expire: required: false default: 21600 description: - Time (in seconds) after which the metadata will expire. - Default value is 6 hours. + metadata_expire_filter: + required: false + choices: [never, 'read-only:past', 'read-only:present', 'read-only:future'] + default: 'read-only:present' + description: + - Filter the I(metadata_expire) time, allowing a trade of speed for + accuracy if a command doesn't require it. Each yum command can specify + that it requires a certain level of timeliness quality from the remote + repos. from "I'm about to install/upgrade, so this better be current" + to "Anything that's available is good enough". + - C(never) - Nothing is filtered, always obey I(metadata_expire). + - C(read-only:past) - Commands that only care about past information are + filtered from metadata expiring. Eg. I(yum history) info (if history + needs to lookup anything about a previous transaction, then by + definition the remote package was available in the past). + - C(read-only:present) - Commands that are balanced between past and + future. Eg. I(yum list yum). + - C(read-only:future) - Commands that are likely to result in running + other commands which will require the latest metadata. Eg. + I(yum check-update). + - Note that this option does not override "yum clean expire-cache". metalink: required: false - default: None + default: null description: - Specifies a URL to a metalink file for the repomd.xml, a list of mirrors for the entire repository are generated by converting the - mirrors for the repomd.xml file to a baseurl. + mirrors for the repomd.xml file to a I(baseurl). mirrorlist: required: false - default: None + default: null description: - Specifies a URL to a file containing a list of baseurls. - - This or the I(baseurl) parameter is required. + - This or the I(baseurl) parameter is required if I(state) is set to + C(present). mirrorlist_expire: required: false default: 21600 @@ -172,17 +242,26 @@ required: true description: - Unique repository ID. + - This parameter is only required if I(state) is set to C(present) or + C(absent). params: required: false - default: None + default: null description: - - Option used to allow the user to overwrite any of the other options. To - remove an option, set the value of the option to C(null). + - Option used to allow the user to overwrite any of the other options. + To remove an option, set the value of the option to C(null). password: required: false - default: None + default: null description: - Password to use with the username for basic authentication. + priority: + required: false + default: 99 + description: + - Enforce ordered protection of repositories. The value is an integer + from 1 to 99. + - This option only works if the YUM Priorities plugin is installed. protect: required: false choices: ['yes', 'no'] @@ -191,17 +270,17 @@ - Protect packages from updates from other repositories. proxy: required: false - default: None + default: null description: - URL to the proxy server that yum should use. proxy_password: required: false - default: None + default: null description: - Username to use for proxy. proxy_username: required: false - default: None + default: null description: - Password for this proxy. repo_gpgcheck: @@ -222,6 +301,13 @@ description: - Set the number of times any attempt to retrieve a file should retry before returning an error. Setting this to C(0) makes yum try forever. + s3_enabled: + required: false + choices: ['yes', 'no'] + default: 'no' + description: + - Enables support for S3 repositories. + - This option only works if the YUM S3 plugin is installed. skip_if_unavailable: required: false choices: ['yes', 'no'] @@ -230,12 +316,6 @@ - If set to C(yes) yum will continue running if this repository cannot be contacted for any reason. This should be set carefully as all repos are consulted for any given command. - sslcacert: - required: false - default: None - description: - - Path to the directory containing the databases of the certificate - authorities yum should use to verify SSL certificates. ssl_check_cert_permissions: required: false choices: ['yes', 'no'] @@ -244,18 +324,24 @@ - Whether yum should check the permissions on the paths for the certificates on the repository (both remote and local). - If we can't read any of the files then yum will force - I(skip_if_unavailable) to be true. This is most useful for non-root + I(skip_if_unavailable) to be C(yes). This is most useful for non-root processes which use yum on repos that have client cert files which are readable only by root. + sslcacert: + required: false + default: null + description: + - Path to the directory containing the databases of the certificate + authorities yum should use to verify SSL certificates. sslclientcert: required: false - default: None + default: null description: - Path to the SSL client certificate yum should use to connect to repos/remote sites. sslclientkey: required: false - default: None + default: null description: - Path to the SSL client key yum should use to connect to repos/remote sites. @@ -270,10 +356,10 @@ choices: [absent, present] default: present description: - - A source string state. + - State of the repo file. throttle: required: false - default: None + default: null description: - Enable bandwidth throttling for downloads. - This option can be expressed as a absolute data rate in bytes/sec. An @@ -283,9 +369,16 @@ default: 30 description: - Number of seconds to wait for a connection before timing out. + ui_repoid_vars: + required: false + default: releasever basearch + description: + - When a repository id is displayed, append these yum variables to the + string if they are used in the I(baseurl)/etc. Variables are appended + in the order listed (and found). username: required: false - default: None + default: null description: - Username to use for basic authentication to a repo or really any url. @@ -377,14 +470,51 @@ class YumRepo(object): # List of parameters which will be allowed in the repo file output allowed_params = [ - 'bandwidth', 'baseurl', 'cost', 'enabled', 'enablegroups', 'exclude', - 'failovermethod', 'gpgcakey', 'gpgcheck', 'gpgkey', 'http_caching', - 'includepkgs', 'keepalive', 'metadata_expire', 'metalink', - 'mirrorlist', 'mirrorlist_expire', 'name', 'password', 'protect', - 'proxy', 'proxy_password', 'proxy_username', 'repo_gpgcheck', - 'retries', 'skip_if_unavailable', 'sslcacert', - 'ssl_check_cert_permissions', 'sslclientcert', 'sslclientkey', - 'sslverify', 'throttle', 'timeout', 'username'] + 'async', + 'bandwidth', + 'baseurl', + 'cost', + 'deltarpm_metadata_percentage', + 'deltarpm_percentage', + 'enabled', + 'enablegroups', + 'exclude', + 'failovermethod', + 'gpgcakey', + 'gpgcheck', + 'gpgkey', + 'http_caching', + 'ignore_repo_files', + 'include', + 'includepkgs', + 'ip_resolve', + 'keepalive', + 'keepcache', + 'metadata_expire', + 'metadata_expire_filter', + 'metalink', + 'mirrorlist', + 'mirrorlist_expire', + 'name', + 'password', + 'priority', + 'protect', + 'proxy', + 'proxy_password', + 'proxy_username', + 'repo_gpgcheck', + 'retries', + 's3_enabled', + 'skip_if_unavailable', + 'sslcacert', + 'ssl_check_cert_permissions', + 'sslclientcert', + 'sslclientkey', + 'sslverify', + 'throttle', + 'timeout', + 'ui_repoid_vars', + 'username'] # List of parameters which can be a list list_params = ['exclude', 'includepkgs'] @@ -401,15 +531,11 @@ def __init__(self, module): repos_dir = self.params['reposdir'] if not os.path.isdir(repos_dir): self.module.fail_json( - msg='Repo directory "%s" does not exist.' % repos_dir) - - # Get the given or the default repo file name - repo_file = self.params['repoid'] - if self.params['file'] is not None: - repo_file = self.params['file'] + msg="Repo directory '%s' does not exist." % repos_dir) # Set dest; also used to set dest parameter for the FS attributes - self.params['dest'] = os.path.join(repos_dir, "%s.repo" % repo_file) + self.params['dest'] = os.path.join( + repos_dir, "%s.repo" % self.params['file']) # Read the repo file if it exists if os.path.isfile(self.params['dest']): @@ -449,28 +575,29 @@ def save(self): # Write data into the file try: fd = open(self.params['dest'], 'wb') - except IOError: + except IOError, e: self.module.fail_json( - msg='Cannot open repo file %s.' % - self.params['dest']) + msg="Cannot open repo file %s." % self.params['dest'], + details=str(e)) + + self.repofile.write(fd) try: - try: - self.repofile.write(fd) - except Error: - self.module.fail_json( - msg='Cannot write repo file %s.' % - self.params['dest']) - finally: fd.close() + except IOError, e: + self.module.fail_json( + msg="Cannot write repo file %s." % self.params['dest'], + details=str(e)) else: # Remove the file if there are not repos try: os.remove(self.params['dest']) - except OSError: + except OSError, e: self.module.fail_json( - msg='Cannot remove empty repo file %s.' % - self.params['dest']) + msg=( + "Cannot remove empty repo file %s." % + self.params['dest']), + details=str(e)) def remove(self): # Remove section if exists @@ -496,9 +623,12 @@ def main(): # Module settings module = AnsibleModule( argument_spec=dict( + async=dict(type='bool'), bandwidth=dict(), baseurl=dict(), cost=dict(), + deltarpm_metadata_percentage=dict(), + deltarpm_percentage=dict(), description=dict(), enabled=dict(type='bool'), enablegroups=dict(type='bool'), @@ -509,15 +639,26 @@ def main(): gpgcheck=dict(type='bool'), gpgkey=dict(), http_caching=dict(choices=['all', 'packages', 'none']), + ignore_repo_files=dict(default=[]), + include=dict(), includepkgs=dict(), + ip_resolve=dict(choices=['4', '6', 'IPv4', 'IPv6', 'whatever']), keepalive=dict(type='bool'), + keepcache=dict(choices=['0', '1']), metadata_expire=dict(), + metadata_expire_filter=dict( + choices=[ + 'never', + 'read-only:past', + 'read-only:present', + 'read-only:future']), metalink=dict(), mirrorlist=dict(), mirrorlist_expire=dict(), name=dict(required=True), params=dict(), password=dict(no_log=True), + priority=dict(), protect=dict(type='bool'), proxy=dict(), proxy_password=dict(no_log=True), @@ -525,6 +666,7 @@ def main(): repo_gpgcheck=dict(type='bool'), reposdir=dict(default='/etc/yum.repos.d'), retries=dict(), + s3_enabled=dict(type='bool'), skip_if_unavailable=dict(type='bool'), sslcacert=dict(), ssl_check_cert_permissions=dict(type='bool'), @@ -534,6 +676,7 @@ def main(): state=dict(choices=['present', 'absent'], default='present'), throttle=dict(), timeout=dict(), + ui_repoid_vars=dict(), username=dict(), ), add_file_common_args=True, @@ -549,11 +692,26 @@ def main(): name = module.params['name'] state = module.params['state'] + # Check if required parameters are present + if state == 'present': + if ( + module.params['baseurl'] is None and + module.params['mirrorlist'] is None): + module.fail_json( + msg="Parameter 'baseurl' or 'mirrorlist' is required.") + if module.params['description'] is None: + module.fail_json( + msg="Parameter 'description' is required.") + # Rename "name" and "description" to ensure correct key sorting module.params['repoid'] = module.params['name'] module.params['name'] = module.params['description'] del module.params['description'] + # Define repo file name if it doesn't exist + if module.params['file'] is None: + module.params['file'] = module.params['repoid'] + # Instantiate the YumRepo object yumrepo = YumRepo(module) From 84def4398f83b838503e8047c9e4a17698f96e7a Mon Sep 17 00:00:00 2001 From: Doug Luce Date: Thu, 3 Mar 2016 14:57:12 -0800 Subject: [PATCH 1257/2522] cronvar.py: support absolute destinations Mainly so /etc/crontab can be written to. --- system/cronvar.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) mode change 100644 => 100755 system/cronvar.py diff --git a/system/cronvar.py b/system/cronvar.py old mode 100644 new mode 100755 index b3b373e9dc3..1800cbd4bd4 --- a/system/cronvar.py +++ b/system/cronvar.py @@ -70,7 +70,9 @@ default: root cron_file: description: - - If specified, uses this file in cron.d instead of an individual user's crontab. + - If specified, uses this file instead of an individual user's crontab. + Without a leading /, this is assumed to be in /etc/cron.d. With a leading + /, this is taken as absolute. required: false default: null backup: @@ -126,7 +128,10 @@ def __init__(self, module, user=None, cron_file=None): self.wordchars = ''.join(chr(x) for x in range(128) if chr(x) not in ('=', "'", '"', )) if cron_file: - self.cron_file = '/etc/cron.d/%s' % cron_file + self.cron_file = "" + if cron_file[0] != '/': + self.cron_file = '/etc/cron.d/' + self.cron_file = self.cron_file + cron_file else: self.cron_file = None From 0298dac401e3981ecb1ba3ed43ef6e2d07f9882e Mon Sep 17 00:00:00 2001 From: Doug Luce Date: Thu, 3 Mar 2016 15:10:19 -0800 Subject: [PATCH 1258/2522] Use os module for checking absolute/joining paths --- system/cronvar.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/system/cronvar.py b/system/cronvar.py index 1800cbd4bd4..1068739c0d0 100755 --- a/system/cronvar.py +++ b/system/cronvar.py @@ -129,9 +129,10 @@ def __init__(self, module, user=None, cron_file=None): if cron_file: self.cron_file = "" - if cron_file[0] != '/': - self.cron_file = '/etc/cron.d/' - self.cron_file = self.cron_file + cron_file + if os.path.isabs(cron_file): + self.cron_file = cron_file + else: + self.cron_file = os.path.join('/etc/cron.d', cron_file) else: self.cron_file = None From 012085263fc269e0b564e9c700064be8bb296e7e Mon Sep 17 00:00:00 2001 From: Jiri Tyr Date: Fri, 4 Mar 2016 00:34:54 +0000 Subject: [PATCH 1259/2522] Removing parameter from yum_repository module --- packaging/os/yum_repository.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/packaging/os/yum_repository.py b/packaging/os/yum_repository.py index 41e3e9e58af..686c8739ba1 100644 --- a/packaging/os/yum_repository.py +++ b/packaging/os/yum_repository.py @@ -484,7 +484,6 @@ class YumRepo(object): 'gpgcheck', 'gpgkey', 'http_caching', - 'ignore_repo_files', 'include', 'includepkgs', 'ip_resolve', @@ -639,7 +638,6 @@ def main(): gpgcheck=dict(type='bool'), gpgkey=dict(), http_caching=dict(choices=['all', 'packages', 'none']), - ignore_repo_files=dict(default=[]), include=dict(), includepkgs=dict(), ip_resolve=dict(choices=['4', '6', 'IPv4', 'IPv6', 'whatever']), From 9835b6a47733ca69e7230cce73188bde62c3868f Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Fri, 4 Mar 2016 09:42:38 -0800 Subject: [PATCH 1260/2522] Fail due to no dnf module installed earlier as we use a dnf utility function to determine if we have permission to install packages. --- packaging/os/dnf.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index efd17ef1eb5..2bd279785f6 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -129,7 +129,7 @@ def _fail_if_no_dnf(module): """Fail if unable to import dnf.""" if not HAS_DNF: module.fail_json( - msg="`python-dnf` is not installed, but it is required for the Ansible dnf module.") + msg="`python2-dnf` is not installed, but it is required for the Ansible dnf module.") def _configure_base(module, base, conf_file, disable_gpg_check): @@ -176,7 +176,6 @@ def _specify_repositories(base, disablerepo, enablerepo): def _base(module, conf_file, disable_gpg_check, disablerepo, enablerepo): """Return a fully configured dnf Base object.""" - _fail_if_no_dnf(module) base = dnf.Base() _configure_base(module, base, conf_file, disable_gpg_check) _specify_repositories(base, disablerepo, enablerepo) @@ -331,6 +330,8 @@ def main(): mutually_exclusive=[['name', 'list']], supports_check_mode=True) params = module.params + + _fail_if_no_dnf(module) if params['list']: base = _base( module, params['conf_file'], params['disable_gpg_check'], From b5390824c25912f8909345710ef6aa5ce7b7081f Mon Sep 17 00:00:00 2001 From: Chris Tooley Date: Sat, 5 Mar 2016 00:37:41 +0000 Subject: [PATCH 1261/2522] Add https support for consul clustering modules --- clustering/consul.py | 14 ++++++++++++++ clustering/consul_acl.py | 14 ++++++++++++++ clustering/consul_kv.py | 14 ++++++++++++++ clustering/consul_session.py | 12 ++++++++++++ 4 files changed, 54 insertions(+) diff --git a/clustering/consul.py b/clustering/consul.py index 627f7fb66af..8929a0e78b0 100644 --- a/clustering/consul.py +++ b/clustering/consul.py @@ -71,6 +71,16 @@ - the port on which the consul agent is running required: false default: 8500 + scheme: + description: + - the protocol scheme on which the consul agent is running + required: false + default: http + verify: + description: + - whether to verify the tls certificate of the consul agent + required: false + default: True notes: description: - Notes to attach to check when registering it. @@ -308,6 +318,8 @@ def remove_service(module, service_id): def get_consul_api(module, token=None): return consul.Consul(host=module.params.get('host'), port=module.params.get('port'), + scheme=module.params.get('scheme'), + verify=module.params.get('verify'), token=module.params.get('token')) @@ -503,6 +515,8 @@ def main(): argument_spec=dict( host=dict(default='localhost'), port=dict(default=8500, type='int'), + scheme=dict(required=False, default='http'), + verify=dict(required=False, default=True, type='bool'), check_id=dict(required=False), check_name=dict(required=False), check_node=dict(required=False), diff --git a/clustering/consul_acl.py b/clustering/consul_acl.py index 17d59ea62a8..2e8149a2c92 100644 --- a/clustering/consul_acl.py +++ b/clustering/consul_acl.py @@ -69,6 +69,16 @@ - the port on which the consul agent is running required: false default: 8500 + scheme: + description: + - the protocol scheme on which the consul agent is running + required: false + default: http + verify: + description: + - whether to verify the tls certificate of the consul agent + required: false + default: True """ EXAMPLES = ''' @@ -300,6 +310,8 @@ def get_consul_api(module, token=None): token = module.params.get('token') return consul.Consul(host=module.params.get('host'), port=module.params.get('port'), + scheme=module.params.get('scheme'), + verify=module.params.get('verify'), token=token) def test_dependencies(module): @@ -315,6 +327,8 @@ def main(): argument_spec = dict( mgmt_token=dict(required=True, no_log=True), host=dict(default='localhost'), + scheme=dict(required=False, default='http'), + verify=dict(required=False, default=True), name=dict(required=False), port=dict(default=8500, type='int'), rules=dict(default=None, required=False, type='list'), diff --git a/clustering/consul_kv.py b/clustering/consul_kv.py index b61c0ee1841..deab7a57a1f 100644 --- a/clustering/consul_kv.py +++ b/clustering/consul_kv.py @@ -99,6 +99,16 @@ - the port on which the consul agent is running required: false default: 8500 + scheme: + description: + - the protocol scheme on which the consul agent is running + required: false + default: http + verify: + description: + - whether to verify the tls certificate of the consul agent + required: false + default: True """ @@ -218,6 +228,8 @@ def remove_value(module): def get_consul_api(module, token=None): return consul.Consul(host=module.params.get('host'), port=module.params.get('port'), + scheme=module.params.get('scheme'), + verify=module.params.get('verify'), token=module.params.get('token')) def test_dependencies(module): @@ -232,6 +244,8 @@ def main(): flags=dict(required=False), key=dict(required=True), host=dict(default='localhost'), + scheme=dict(required=False, default='http'), + verify=dict(required=False, default=True), port=dict(default=8500, type='int'), recurse=dict(required=False, type='bool'), retrieve=dict(required=False, default=True), diff --git a/clustering/consul_session.py b/clustering/consul_session.py index c298ea7fa57..23d683e8ab5 100644 --- a/clustering/consul_session.py +++ b/clustering/consul_session.py @@ -88,6 +88,16 @@ - the port on which the consul agent is running required: false default: 8500 + scheme: + description: + - the protocol scheme on which the consul agent is running + required: false + default: http + verify: + description: + - whether to verify the tls certificate of the consul agent + required: false + default: True """ EXAMPLES = ''' @@ -244,6 +254,8 @@ def main(): delay=dict(required=False,type='str', default='15s'), host=dict(default='localhost'), port=dict(default=8500, type='int'), + scheme=dict(required=False, default='http'), + verify=dict(required=False, default=True), id=dict(required=False), name=dict(required=False), node=dict(required=False), From 516f7d98e755e1cbec7daf2e3370d0e6f1354f04 Mon Sep 17 00:00:00 2001 From: Chris Tooley Date: Sat, 5 Mar 2016 00:55:08 +0000 Subject: [PATCH 1262/2522] Add version_added to documentation --- clustering/consul.py | 2 ++ clustering/consul_acl.py | 2 ++ clustering/consul_kv.py | 2 ++ clustering/consul_session.py | 2 ++ 4 files changed, 8 insertions(+) diff --git a/clustering/consul.py b/clustering/consul.py index 8929a0e78b0..e24279586e6 100644 --- a/clustering/consul.py +++ b/clustering/consul.py @@ -76,11 +76,13 @@ - the protocol scheme on which the consul agent is running required: false default: http + version_added: "2.1" verify: description: - whether to verify the tls certificate of the consul agent required: false default: True + version_added: "2.1" notes: description: - Notes to attach to check when registering it. diff --git a/clustering/consul_acl.py b/clustering/consul_acl.py index 2e8149a2c92..e07db83232d 100644 --- a/clustering/consul_acl.py +++ b/clustering/consul_acl.py @@ -74,11 +74,13 @@ - the protocol scheme on which the consul agent is running required: false default: http + version_added: "2.1" verify: description: - whether to verify the tls certificate of the consul agent required: false default: True + version_added: "2.1" """ EXAMPLES = ''' diff --git a/clustering/consul_kv.py b/clustering/consul_kv.py index deab7a57a1f..45c93d672a0 100644 --- a/clustering/consul_kv.py +++ b/clustering/consul_kv.py @@ -104,11 +104,13 @@ - the protocol scheme on which the consul agent is running required: false default: http + version_added: "2.1" verify: description: - whether to verify the tls certificate of the consul agent required: false default: True + version_added: "2.1" """ diff --git a/clustering/consul_session.py b/clustering/consul_session.py index 23d683e8ab5..625c31c7979 100644 --- a/clustering/consul_session.py +++ b/clustering/consul_session.py @@ -93,11 +93,13 @@ - the protocol scheme on which the consul agent is running required: false default: http + version_added: "2.1" verify: description: - whether to verify the tls certificate of the consul agent required: false default: True + version_added: "2.1" """ EXAMPLES = ''' From deb72106d2b675adbd74b72bd530a58691f0c76e Mon Sep 17 00:00:00 2001 From: Jonathan Mainguy Date: Sat, 5 Mar 2016 14:08:28 -0500 Subject: [PATCH 1263/2522] fixes bug where puppet fails if logdest is not specified --- system/puppet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/puppet.py b/system/puppet.py index c26b96db6df..852a6f9d61b 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -119,7 +119,7 @@ def main(): puppetmaster=dict(required=False, default=None), manifest=dict(required=False, default=None), logdest=dict( - required=False, default=['stdout'], + required=False, default='stdout', choices=['stdout', 'syslog']), show_diff=dict( # internal code to work with --diff, do not use From 26e2c1bf116be4392a3ee993a8e60a15f643aaed Mon Sep 17 00:00:00 2001 From: Nick Aslanidis Date: Sun, 6 Mar 2016 16:09:14 +1000 Subject: [PATCH 1264/2522] updated extends_documentation_fragment and final cr --- cloud/amazon/ec2_vpc_vgw.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/ec2_vpc_vgw.py b/cloud/amazon/ec2_vpc_vgw.py index 59336b11607..7e5893ee27f 100644 --- a/cloud/amazon/ec2_vpc_vgw.py +++ b/cloud/amazon/ec2_vpc_vgw.py @@ -62,7 +62,9 @@ - dictionary of resource tags of the form: { tag1: value1, tag2: value2 } required: false author: Nick Aslanidis (@naslanidis) -extends_documentation_fragment: aws +extends_documentation_fragment: + - aws + - ec2 ''' EXAMPLES = ''' @@ -558,4 +560,5 @@ def main(): from ansible.module_utils.ec2 import * if __name__ == '__main__': - main() \ No newline at end of file + main() + From fa8eb632f8f1a1e77ec71a5c7041e53f6fad815d Mon Sep 17 00:00:00 2001 From: Nick Aslanidis Date: Sun, 6 Mar 2016 16:15:38 +1000 Subject: [PATCH 1265/2522] attempt to fix doc fragment --- cloud/amazon/ec2_vpc_vgw.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/ec2_vpc_vgw.py b/cloud/amazon/ec2_vpc_vgw.py index 7e5893ee27f..e641ed711be 100644 --- a/cloud/amazon/ec2_vpc_vgw.py +++ b/cloud/amazon/ec2_vpc_vgw.py @@ -63,8 +63,8 @@ required: false author: Nick Aslanidis (@naslanidis) extends_documentation_fragment: - - aws - - ec2 + - aws + - ec2 ''' EXAMPLES = ''' From 9eb9d1c74b4b22cc09a10bcdd776e889690f7eb2 Mon Sep 17 00:00:00 2001 From: Nick Aslanidis Date: Sun, 6 Mar 2016 16:36:13 +1000 Subject: [PATCH 1266/2522] corrected required to be bool instead of list --- cloud/amazon/ec2_vpc_vgw.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/cloud/amazon/ec2_vpc_vgw.py b/cloud/amazon/ec2_vpc_vgw.py index e641ed711be..58108c2dbf0 100644 --- a/cloud/amazon/ec2_vpc_vgw.py +++ b/cloud/amazon/ec2_vpc_vgw.py @@ -35,15 +35,11 @@ name: description: - name of the vgw to be created or deleted - required: - - true when combined with a state of 'present' - - false when combined with a state of 'absent' + required: false type: description: - type of the virtual gateway to be created - required: - - true when combined with a state of 'present' - - false when combined with a state of 'absent' + required: false vpn_gateway_id: description: - vpn gateway id of an existing virtual gateway @@ -63,8 +59,8 @@ required: false author: Nick Aslanidis (@naslanidis) extends_documentation_fragment: - - aws - - ec2 + - aws + - ec2 ''' EXAMPLES = ''' From 2f56b3c8b45bd6eeb01342825026475071fa232e Mon Sep 17 00:00:00 2001 From: Nick Aslanidis Date: Sun, 6 Mar 2016 16:41:34 +1000 Subject: [PATCH 1267/2522] corrected invalid tag description for CI checks --- cloud/amazon/ec2_vpc_vgw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_vpc_vgw.py b/cloud/amazon/ec2_vpc_vgw.py index 58108c2dbf0..6c8a80e9872 100644 --- a/cloud/amazon/ec2_vpc_vgw.py +++ b/cloud/amazon/ec2_vpc_vgw.py @@ -55,7 +55,7 @@ default: 320 tags: description: - - dictionary of resource tags of the form: { tag1: value1, tag2: value2 } + - dictionary of resource tags required: false author: Nick Aslanidis (@naslanidis) extends_documentation_fragment: From ce68b123827de37831606dd793d0c43ec83462b9 Mon Sep 17 00:00:00 2001 From: Ricky Cook Date: Mon, 7 Mar 2016 14:20:33 +1100 Subject: [PATCH 1268/2522] New json_fail syntax for dnsimple module --- network/dnsimple.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/dnsimple.py b/network/dnsimple.py index 5cecfbd8169..48b0003cb44 100644 --- a/network/dnsimple.py +++ b/network/dnsimple.py @@ -159,7 +159,7 @@ def main(): ) if not HAS_DNSIMPLE: - module.fail_json("dnsimple required for this module") + module.fail_json(msg="dnsimple required for this module") account_email = module.params.get('account_email') account_api_token = module.params.get('account_api_token') From 548645e031dcaa7e98df51948554b93ffc0551e8 Mon Sep 17 00:00:00 2001 From: Sam Liu Date: Tue, 8 Mar 2016 14:14:25 +0800 Subject: [PATCH 1269/2522] Fixed: exception swallowing --- windows/win_file_version.ps1 | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/windows/win_file_version.ps1 b/windows/win_file_version.ps1 index 4ee8c6e3f6c..b9fa39c0dc7 100644 --- a/windows/win_file_version.ps1 +++ b/windows/win_file_version.ps1 @@ -42,32 +42,34 @@ Else{ } Try { - $file_version = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($path).FileVersion + $_version_fields = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($path) + $file_version = $_version_fields.FileVersion If ($file_version -eq $null){ $file_version = '' } - $product_version = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($path).ProductVersion + $product_version = $_version_fields.ProductVersion If ($product_version -eq $null){ $product_version= '' } - $file_major_part = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($path).FileMajorPart + $file_major_part = $_version_fields.FileMajorPart If ($file_major_part -eq $null){ $file_major_part= '' } - $file_minor_part = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($path).FileMinorPart + $file_minor_part = $_version_fields.FileMinorPart If ($file_minor_part -eq $null){ $file_minor_part= '' } - $file_build_part = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($path).FileBuildPart + $file_build_part = $_version_fields.FileBuildPart If ($file_build_part -eq $null){ $file_build_part = '' } - $file_private_part = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($path).FilePrivatePart - If ($file_private_part -eq $null){ + $file_private_part = $_version_fields.FilePrivatePart + If ($file_private_part -eq $null) $file_private_part = '' } } Catch{ + Fail-Json $result "Error: $_.Exception.Message" } Set-Attr $result.win_file_version "path" $path.toString() From 7d4e2698b0a82d4011138acbe792665ec9783a14 Mon Sep 17 00:00:00 2001 From: Sam Liu Date: Tue, 8 Mar 2016 15:24:02 +0800 Subject: [PATCH 1270/2522] fix some error for passing CI build. --- windows/win_file_version.ps1 | 2 +- windows/win_file_version.py | 49 ++++++++++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/windows/win_file_version.ps1 b/windows/win_file_version.ps1 index b9fa39c0dc7..eefa221e88d 100644 --- a/windows/win_file_version.ps1 +++ b/windows/win_file_version.ps1 @@ -16,7 +16,7 @@ #You should have received a copy of the GNU General Public License #along with this program. If not, see . -# WAIT_JSON +# WANT_JSON # POWERSHELL_COMMON $params = Parse-Args $args; diff --git a/windows/win_file_version.py b/windows/win_file_version.py index 7688773e6a7..4f23c55053a 100644 --- a/windows/win_file_version.py +++ b/windows/win_file_version.py @@ -20,16 +20,17 @@ DOCUMENTATION = ''' --- module: win_file_version -version_added: "2.0" +version_added: "2.1" short_descriptions: Get DLL or EXE file build version description: - Get DLL or EXE file build version - change state alway be false options: - path: - description: - - File to get version(provide absolute path) - + path: + description: + - File to get version(provide absolute path) + required: true + aliases: [] author: Sam Liu ''' @@ -44,3 +45,41 @@ - debug: msg="{{exe_file_version}}" ''' + +RETURN = """ +win_file_version.path: + description: file path + returned: always + type: string + +win_file_version.file_version: + description: file version number. + returned: no error + type: string + +win_file_version.product_version: + description: the version of the product this file is distributed with. + returned: no error + type: string + +win_file_version.file_major_part: + description: the major part of the version number. + returned: no error + type: string + +win_file_version.file_minor_part: + description: the minor part of the version number of the file. + returned: no error + type: string + +win_file_version.file_build_part: + description: build number of the file. + returned: no error + type: string + +win_file_version.file_private_part: + description: file private part number. + returned: no error + type: string + +""" From ce9aed9c52a2fcee6ff6de86b8a7baee4f5ac24b Mon Sep 17 00:00:00 2001 From: Rob White Date: Mon, 1 Feb 2016 21:21:00 +1100 Subject: [PATCH 1271/2522] Allow SNS topics to be created without subscriptions. Also added better error handling around boto calls. --- cloud/amazon/sns_topic.py | 75 +++++++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 23 deletions(-) diff --git a/cloud/amazon/sns_topic.py b/cloud/amazon/sns_topic.py index 71a66318290..5dd6d32ed59 100755 --- a/cloud/amazon/sns_topic.py +++ b/cloud/amazon/sns_topic.py @@ -132,8 +132,8 @@ import re try: - import boto import boto.sns + from boto.exception import BotoServerError HAS_BOTO = True except ImportError: HAS_BOTO = False @@ -146,12 +146,15 @@ def canonicalize_endpoint(protocol, endpoint): return endpoint - -def get_all_topics(connection): +def get_all_topics(connection, module): next_token = None topics = [] while True: - response = connection.get_all_topics(next_token) + try: + response = connection.get_all_topics(next_token) + except BotoServerError, e: + module.fail_json(msg=e.message) + topics.extend(response['ListTopicsResponse']['ListTopicsResult']['Topics']) next_token = \ response['ListTopicsResponse']['ListTopicsResult']['NextToken'] @@ -160,15 +163,16 @@ def get_all_topics(connection): return [t['TopicArn'] for t in topics] -def arn_topic_lookup(connection, short_topic): +def arn_topic_lookup(connection, short_topic, module): # topic names cannot have colons, so this captures the full topic name - all_topics = get_all_topics(connection) + all_topics = get_all_topics(connection, module) lookup_topic = ':%s' % short_topic for topic in all_topics: if topic.endswith(lookup_topic): return topic return None + def main(): argument_spec = ec2_argument_spec() argument_spec.update( @@ -179,7 +183,7 @@ def main(): display_name=dict(type='str', required=False), policy=dict(type='dict', required=False), delivery_policy=dict(type='dict', required=False), - subscriptions=dict(type='list', required=False), + subscriptions=dict(default=[], type='list', required=False), purge_subscriptions=dict(type='bool', default=True), ) ) @@ -214,7 +218,7 @@ def main(): # topics cannot contain ':', so thats the decider if ':' in name: - all_topics = get_all_topics(connection) + all_topics = get_all_topics(connection, module) if name in all_topics: arn_topic = name elif state == 'absent': @@ -223,7 +227,7 @@ def main(): module.fail_json(msg="specified an ARN for a topic but it doesn't" " exist") else: - arn_topic = arn_topic_lookup(connection, name) + arn_topic = arn_topic_lookup(connection, name, module) if not arn_topic: if state == 'absent': module.exit_json(changed=False) @@ -234,15 +238,22 @@ def main(): changed=True topic_created = True - connection.create_topic(name) - arn_topic = arn_topic_lookup(connection, name) + try: + connection.create_topic(name) + except BotoServerError, e: + module.fail_json(msg=e.message) + arn_topic = arn_topic_lookup(connection, name, module) while not arn_topic: time.sleep(3) - arn_topic = arn_topic_lookup(connection, name) + arn_topic = arn_topic_lookup(connection, name, module) if arn_topic and state == "absent": if not check_mode: - connection.delete_topic(arn_topic) + try: + connection.delete_topic(arn_topic) + except BotoServerError, e: + module.fail_json(msg=e.message) + module.exit_json(changed=True) topic_attributes = connection.get_topic_attributes(arn_topic) \ @@ -252,30 +263,40 @@ def main(): changed = True attributes_set.append('display_name') if not check_mode: - connection.set_topic_attributes(arn_topic, 'DisplayName', - display_name) + try: + connection.set_topic_attributes(arn_topic, 'DisplayName', display_name) + except BotoServerError, e: + module.fail_json(msg=e.message) if policy and policy != json.loads(topic_attributes['policy']): changed = True attributes_set.append('policy') if not check_mode: - connection.set_topic_attributes(arn_topic, 'Policy', - json.dumps(policy)) + try: + connection.set_topic_attributes(arn_topic, 'Policy', json.dumps(policy)) + except BotoServerError, e: + module.fail_json(msg=e.message) if delivery_policy and ('DeliveryPolicy' not in topic_attributes or \ delivery_policy != json.loads(topic_attributes['DeliveryPolicy'])): changed = True attributes_set.append('delivery_policy') if not check_mode: - connection.set_topic_attributes(arn_topic, 'DeliveryPolicy', - json.dumps(delivery_policy)) + try: + connection.set_topic_attributes(arn_topic, 'DeliveryPolicy',json.dumps(delivery_policy)) + except BotoServerError, e: + module.fail_json(msg=e.message) next_token = None aws_subscriptions = [] while True: - response = connection.get_all_subscriptions_by_topic(arn_topic, + try: + response = connection.get_all_subscriptions_by_topic(arn_topic, next_token) + except BotoServerError, e: + module.fail_json(msg=e.message) + aws_subscriptions.extend(response['ListSubscriptionsByTopicResponse'] \ ['ListSubscriptionsByTopicResult']['Subscriptions']) next_token = response['ListSubscriptionsByTopicResponse'] \ @@ -286,6 +307,7 @@ def main(): desired_subscriptions = [(sub['protocol'], canonicalize_endpoint(sub['protocol'], sub['endpoint'])) for sub in subscriptions] + aws_subscriptions_list = [] for sub in aws_subscriptions: @@ -296,14 +318,20 @@ def main(): changed = True subscriptions_deleted.append(sub_key) if not check_mode: - connection.unsubscribe(sub['SubscriptionArn']) + try: + connection.unsubscribe(sub['SubscriptionArn']) + except BotoServerError, e: + module.fail_json(msg=e.message) for (protocol, endpoint) in desired_subscriptions: if (protocol, endpoint) not in aws_subscriptions_list: changed = True subscriptions_added.append(sub) if not check_mode: - connection.subscribe(arn_topic, protocol, endpoint) + try: + connection.subscribe(arn_topic, protocol, endpoint) + except BotoServerError, e: + module.fail_json(msg=e.message) module.exit_json(changed=changed, topic_created=topic_created, attributes_set=attributes_set, @@ -313,4 +341,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * -main() +if __name__ == '__main__': + main() From 68c8c160820b0e6de1b167118956e6e54c374e90 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Wed, 17 Feb 2016 08:58:28 -0500 Subject: [PATCH 1272/2522] Add os_ironic_inspect module Addition of an os_ironic_inspect module to leverage the OpenStack Baremetal inspector add-on to ironic or ironic driver out-of-band hardware introspection, if supported and configured. --- cloud/openstack/os_ironic_inspect.py | 169 +++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 cloud/openstack/os_ironic_inspect.py diff --git a/cloud/openstack/os_ironic_inspect.py b/cloud/openstack/os_ironic_inspect.py new file mode 100644 index 00000000000..5299da09335 --- /dev/null +++ b/cloud/openstack/os_ironic_inspect.py @@ -0,0 +1,169 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2015-2016, Hewlett Packard Enterprise Development Company LP +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + +try: + import shade + HAS_SHADE = True +except ImportError: + HAS_SHADE = False + +from distutils.version import StrictVersion + +DOCUMENTATION = ''' +--- +module: os_ironic_inspect +short_description: Explicitly triggers baremetal node introspection in ironic. +extends_documentation_fragment: openstack +author: "Julia Kreger (@juliakreger)" +version_added: "2.1" +description: + - Requests Ironic to set a node into inspect state in order to collect metadata regarding the node. + This command may be out of band or in-band depending on the ironic driver configuration. + This is only possible on nodes in 'manageable' and 'available' state. +options: + mac: + description: + - unique mac address that is used to attempt to identify the host. + required: false + default: None + uuid: + description: + - globally unique identifier (UUID) to identify the host. + required: false + default: None + name: + description: + - unique name identifier to identify the host in Ironic. + required: false + default: None + ironic_url: + description: + - If noauth mode is utilized, this is required to be set to the endpoint URL for the Ironic API. + Use with "auth" and "auth_type" settings set to None. + required: false + default: None + timeout: + description: + - A timeout in seconds to tell the role to wait for the node to complete introspection if wait is set to True. + required: false + default: 1200 + +requirements: ["shade"] +''' + +RETURN = ''' +ansible_facts: + description: Dictionary of new facts representing discovered properties of the node.. + returned: changed + type: dictionary + contains: + memory_mb: + description: Amount of node memory as updated in the node properties + type: string + sample: "1024" + cpu_arch: + description: Detected CPU architecture type + type: string + sample: "x86_64" + local_gb: + description: Total size of local disk storage as updaed in node properties. + type: string + sample: "10" + cpus: + description: Count of cpu cores defined in the updated node properties. + type: string + sample: "1" +''' + +EXAMPLES = ''' +# Invoke node inspection +- os_ironic_inspect: + name: "testnode1" +''' + + +def _choose_id_value(module): + if module.params['uuid']: + return module.params['uuid'] + if module.params['name']: + return module.params['name'] + return None + + +def main(): + argument_spec = openstack_full_argument_spec( + auth_type=dict(required=False), + uuid=dict(required=False), + name=dict(required=False), + mac=dict(required=False), + ironic_url=dict(required=False), + timeout=dict(default=1200, type='int', required=False), + ) + module_kwargs = openstack_module_kwargs() + module = AnsibleModule(argument_spec, **module_kwargs) + + if not HAS_SHADE: + module.fail_json(msg='shade is required for this module') + if StrictVersion(shade.__version__) < StrictVersion('1.0.0'): + module.fail_json(msg="To utilize this module, the installed version of" + "the shade library MUST be >=1.0.0") + + if (module.params['auth_type'] in [None, 'None'] and + module.params['ironic_url'] is None): + module.fail_json(msg="Authentication appears to be disabled, " + "Please define an ironic_url parameter") + + if (module.params['ironic_url'] and + module.params['auth_type'] in [None, 'None']): + module.params['auth'] = dict( + endpoint=module.params['ironic_url'] + ) + + try: + cloud = shade.operator_cloud(**module.params) + + if module.params['name'] or module.params['uuid']: + server = cloud.get_machine(_choose_id_value(module)) + elif module.params['mac']: + server = cloud.get_machine_by_mac(module.params['mac']) + else: + module.fail_json(msg="The worlds did not align, " + "the host was not found as " + "no name, uuid, or mac was " + "defined.") + if server: + cloud.inspect_machine(server['uuid'], module.params['wait']) + # TODO(TheJulia): diff properties, ?and ports? and determine + # if a change occured. In theory, the node is always changed + # if introspection is able to update the record. + module.exit_json(changed=True, + ansible_facts=server['properties']) + + else: + module.fail_json(msg="node not found.") + + except shade.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + + +# this is magic, see lib/ansible/module_common.py +from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * + +if __name__ == "__main__": + main() From 6c7be7d97d9320df1d192c33efb718fe0ecfd018 Mon Sep 17 00:00:00 2001 From: saez0pub Date: Sat, 13 Feb 2016 15:37:08 +0100 Subject: [PATCH 1273/2522] Prevent reinstallation in case of group package --- packaging/os/pacman.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/os/pacman.py b/packaging/os/pacman.py index 81b9c907772..e288a1206fb 100644 --- a/packaging/os/pacman.py +++ b/packaging/os/pacman.py @@ -234,7 +234,7 @@ def install_packages(module, pacman_path, state, packages, package_files): else: params = '-S %s' % package - cmd = "%s %s --noconfirm" % (pacman_path, params) + cmd = "%s %s --noconfirm --needed" % (pacman_path, params) rc, stdout, stderr = module.run_command(cmd, check_rc=False) if rc != 0: From 282deb3291c38acb69160a1cc12c71723dd15be5 Mon Sep 17 00:00:00 2001 From: Will Keeling Date: Thu, 11 Feb 2016 20:44:30 +0000 Subject: [PATCH 1274/2522] Better handling of package groups in pacman module --- packaging/os/pacman.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packaging/os/pacman.py b/packaging/os/pacman.py index 81b9c907772..2e444c1448c 100644 --- a/packaging/os/pacman.py +++ b/packaging/os/pacman.py @@ -267,6 +267,25 @@ def check_packages(module, pacman_path, packages, state): module.exit_json(changed=False, msg="package(s) already %s" % state) +def expand_package_groups(module, pacman_path, pkgs): + expanded = [] + + for pkg in pkgs: + cmd = "%s -Sgq %s" % (pacman_path, pkg) + rc, stdout, stderr = module.run_command(cmd, check_rc=False) + + if rc == 0: + # A group was found matching the name, so expand it + for name in stdout.split('\n'): + name = name.strip() + if name: + expanded.append(name) + else: + expanded.append(pkg) + + return expanded + + def main(): module = AnsibleModule( argument_spec = dict( @@ -305,7 +324,7 @@ def main(): upgrade(module, pacman_path) if p['name']: - pkgs = p['name'] + pkgs = expand_package_groups(module, pacman_path, p['name']) pkg_files = [] for i, pkg in enumerate(pkgs): From b5d75234a3570ccae75245e7f55c737b7c53dd10 Mon Sep 17 00:00:00 2001 From: Jonas Vermeulen Date: Fri, 25 Dec 2015 14:27:48 +0100 Subject: [PATCH 1275/2522] Added resizing based on %values. Included support for VG|PVS|FREE --- system/lvol.py | 95 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 86 insertions(+), 9 deletions(-) diff --git a/system/lvol.py b/system/lvol.py index 284c37ace5e..9432271b0da 100644 --- a/system/lvol.py +++ b/system/lvol.py @@ -42,8 +42,8 @@ - The size of the logical volume, according to lvcreate(8) --size, by default in megabytes or optionally with one of [bBsSkKmMgGtTpPeE] units; or according to lvcreate(8) --extents as a percentage of [VG|PVS|FREE]; - resizing is not supported with percentages. Float values must begin - with a digit. + Float values must begin with a digit. + Resizing using percentage values was not supported prior to 2.1. state: choices: [ "present", "absent" ] default: present @@ -87,6 +87,15 @@ # Extend the logical volume to 1024m. - lvol: vg=firefly lv=test size=1024 +# Extend the logical volume to consume all remaining space in the volume group +- lvol: vg=firefly lv=test size=+100%FREE + +# Extend the logical volume to take all remaining space of the PVs +- lvol: vg=firefly lv=test size=100%PVS + +# Resize the logical volume to % of VG +- lvol: vg-firefly lv=test size=80%VG force=yes + # Reduce the logical volume to 512m - lvol: vg=firefly lv=test size=512 force=yes @@ -98,23 +107,35 @@ ''' import re +import logging -decimal_point = re.compile(r"(\.|,)") +decimal_point = re.compile(r"(\d+)") def mkversion(major, minor, patch): return (1000 * 1000 * int(major)) + (1000 * int(minor)) + int(patch) - def parse_lvs(data): lvs = [] for line in data.splitlines(): parts = line.strip().split(';') lvs.append({ 'name': parts[0], - 'size': int(decimal_point.split(parts[1])[0]), + 'size': int(decimal_point.match(parts[1]).group(1)) }) return lvs +def parse_vgs(data): + vgs = [] + for line in data.splitlines(): + parts = line.strip().split(';') + vgs.append({ + 'name': parts[0], + 'size': int(decimal_point.match(parts[1]).group(1)), + 'free': int(decimal_point.match(parts[2]).group(1)), + 'ext_size': int(decimal_point.match(parts[3]).group(1)) + }) + return vgs + def get_lvm_version(module): ver_cmd = module.get_bin_path("lvm", required=True) @@ -197,6 +218,21 @@ def main(): else: unit = size_unit + # Get information on volume group requested + vgs_cmd = module.get_bin_path("vgs", required=True) + rc, current_vgs, err = module.run_command( + "%s --noheadings -o vg_name,size,free,vg_extent_size --units %s --separator ';' %s" % (vgs_cmd, unit, vg)) + + if rc != 0: + if state == 'absent': + module.exit_json(changed=False, stdout="Volume group %s does not exist." % vg, stderr=False) + else: + module.fail_json(msg="Volume group %s does not exist." % vg, rc=rc, err=err) + + vgs = parse_vgs(current_vgs) + this_vg = vgs[0] + + # Get information on logical volume requested lvs_cmd = module.get_bin_path("lvs", required=True) rc, current_lvs, err = module.run_command( "%s --noheadings --nosuffix -o lv_name,size --units %s --separator ';' %s" % (lvs_cmd, unit, vg)) @@ -260,17 +296,58 @@ def main(): module.fail_json(msg="Failed to remove logical volume %s" % (lv), rc=rc, err=err) elif size_opt == 'l': - module.exit_json(changed=False, msg="Resizing extents with percentage not supported.") + ### Resize LV based on % value + tool = None + size_free = this_vg['free'] + if size_whole == 'VG' or size_whole == 'PVS': + size_requested = size_percent * this_vg['size'] / 100 + else: # size_whole == 'FREE': + size_requested = size_percent * this_vg['free'] / 100 + if '+' in size: + size_requested += this_lv['size'] + if this_lv['size'] < size_requested: + if (size_free > 0) and (('+' not in size) or (size_free >= (size_requested - this_lv['size']))): + tool = module.get_bin_path("lvextend", required=True) + else: + module.fail_json(msg="Logical Volume %s could not be extended. Not enough free space left (%s%s required / %s%s available)" % (this_lv['name'], (size_requested - this_lv['size']), unit, size_free, unit)) + elif this_lv['size'] > size_requested + this_vg['ext_size']: # more than an extent too large + if size_requested == 0: + module.fail_json(msg="Sorry, no shrinking of %s to 0 permitted." % (this_lv['name'])) + elif not force: + module.fail_json(msg="Sorry, no shrinking of %s without force=yes" % (this_lv['name'])) + else: + tool = module.get_bin_path("lvreduce", required=True) + tool = '%s %s' % (tool, '--force') + + if tool: + if module.check_mode: + changed = True + else: + cmd = "%s -%s %s%s %s/%s" % (tool, size_opt, size, size_unit, vg, this_lv['name']) + rc, out, err = module.run_command(cmd) + if "Reached maximum COW size" in out: + module.fail_json(msg="Unable to resize %s to %s%s" % (lv, size, size_unit), rc=rc, err=err, out=out) + elif rc == 0: + changed = True + msg="Volume %s resized to %s%s" % (this_lv['name'], size_requested, unit) + elif "matches existing size" in err: + module.exit_json(changed=False, vg=vg, lv=this_lv['name'], size=this_lv['size']) + else: + module.fail_json(msg="Unable to resize %s to %s%s" % (lv, size, size_unit), rc=rc, err=err) + else: - ### resize LV + ### resize LV based on absolute values tool = None if int(size) > this_lv['size']: tool = module.get_bin_path("lvextend", required=True) elif int(size) < this_lv['size']: + if int(size) == 0: + module.fail_json(msg="Sorry, no shrinking of %s to 0 permitted." % (this_lv['name'])) if not force: module.fail_json(msg="Sorry, no shrinking of %s without force=yes." % (this_lv['name'])) - tool = module.get_bin_path("lvreduce", required=True) - tool = '%s %s' % (tool, '--force') + else: + tool = module.get_bin_path("lvreduce", required=True) + tool = '%s %s' % (tool, '--force') if tool: if module.check_mode: From 27b28e78b2cb5e10be9a48ce665aa40d4a21c10a Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 8 Mar 2016 21:03:05 -0600 Subject: [PATCH 1276/2522] Catch errors related to insufficient (old) versions of pexpect. Fixes #13660 --- commands/expect.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/commands/expect.py b/commands/expect.py index 5a7d7dba83b..4592179456b 100644 --- a/commands/expect.py +++ b/commands/expect.py @@ -188,8 +188,23 @@ def main(): startd = datetime.datetime.now() try: - out, rc = pexpect.runu(args, timeout=timeout, withexitstatus=True, - events=events, cwd=chdir, echo=echo) + try: + # Prefer pexpect.run from pexpect>=4 + out, rc = pexpect.run(args, timeout=timeout, withexitstatus=True, + events=events, cwd=chdir, echo=echo, + encoding='utf-8') + except TypeError: + # Use pexpect.runu in pexpect>=3.3,<4 + out, rc = pexpect.runu(args, timeout=timeout, withexitstatus=True, + events=events, cwd=chdir, echo=echo) + except (TypeError, AttributeError), e: + # This should catch all insufficient versions of pexpect + # We deem them insufficient for their lack of ability to specify + # to not echo responses via the run/runu functions, which would + # potentially leak sensentive information + module.fail_json(msg='Insufficient version of pexpect installed ' + '(%s), this module requires pexpect>=3.3. ' + 'Error was %s' % (pexpect.__version__, e)) except pexpect.ExceptionPexpect, e: module.fail_json(msg='%s' % e) From c553e70ed2da6a1e568c1ff002eff92884b6af7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darek=20Kaczy=C5=84ski?= Date: Wed, 9 Mar 2016 14:37:06 +0100 Subject: [PATCH 1277/2522] Removed debug return values --- cloud/amazon/ecs_service.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/cloud/amazon/ecs_service.py b/cloud/amazon/ecs_service.py index 2c13a52eff9..94d7078c82d 100644 --- a/cloud/amazon/ecs_service.py +++ b/cloud/amazon/ecs_service.py @@ -327,9 +327,6 @@ def main(): results = dict(changed=False ) if module.params['state'] == 'present': - results['expected'] = module.params - results['existing'] = existing - matching = False update = False if existing and 'status' in existing and existing['status']=="ACTIVE": From c9e3c57ee94361df58b2719fe4f7cc814b20c200 Mon Sep 17 00:00:00 2001 From: David Hocky Date: Wed, 9 Mar 2016 11:04:58 -0500 Subject: [PATCH 1278/2522] add support for setting dscp marks with iptables module --- system/iptables.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/system/iptables.py b/system/iptables.py index 0e8260d226f..9d4706c27a3 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -198,6 +198,16 @@ rule also specifies one of the following protocols: tcp, udp, dccp or sctp." required: false + set_dscp_mark: + description: + - "This allows specifying a DSCP mark to be added to packets. + It takes either an integer or hex value. Mutually exclusive with + C(dscp_mark_class)." + required: false + set_dscp_mark_class: + - "This allows specifying a predefined DiffServ class which will be + translated to the corresponding DSCP mark. Mutually exclusive with + C(dscp_mark)." comment: description: - "This specifies a comment that will be added to the rule" @@ -230,6 +240,12 @@ # Allow related and established connections - iptables: chain=INPUT ctstate=ESTABLISHED,RELATED jump=ACCEPT become: yes + +# Tag all outbound tcp packets with DSCP mark 8 +- iptables: chain=OUTPUT jump=DSCP table=mangle set_dscp_mark=8 protocol=tcp + +# Tag all outbound tcp packets with DSCP DiffServ class CS1 +- iptables: chain=OUTPUT jump=DSCP table=mangle set_dscp_mark_class=CS1 protocol=tcp ''' @@ -267,6 +283,8 @@ def construct_rule(params): append_param(rule, params['source_port'], '--source-port', False) append_param(rule, params['destination_port'], '--destination-port', False) append_param(rule, params['to_ports'], '--to-ports', False) + append_param(rule, params['set_dscp_mark'], '--set-dscp', False) + append_param(rule, params['set_dscp_mark_class'], '--set-dscp-class', False) append_match(rule, params['comment'], 'comment') append_param(rule, params['comment'], '--comment', False) append_match(rule, params['ctstate'], 'state') @@ -322,11 +340,16 @@ def main(): source_port=dict(required=False, default=None, type='str'), destination_port=dict(required=False, default=None, type='str'), to_ports=dict(required=False, default=None, type='str'), + set_dscp_mark=dict(required=False,default=None, type='str'), + set_dscp_mark_class=dict(required=False,default=None, type='str'), comment=dict(required=False, default=None, type='str'), ctstate=dict(required=False, default=[], type='list'), limit=dict(required=False, default=None, type='str'), limit_burst=dict(required=False, default=None, type='str'), ), + mutually_exclusive=( + ['set_dscp_mark', 'set_dscp_mark_class'], + ), ) args = dict( changed=False, From b89f0f44d05a13a4c789d813e3b11ee131497674 Mon Sep 17 00:00:00 2001 From: David Hocky Date: Wed, 9 Mar 2016 11:16:27 -0500 Subject: [PATCH 1279/2522] fix doc issue --- system/iptables.py | 1 + 1 file changed, 1 insertion(+) diff --git a/system/iptables.py b/system/iptables.py index 9d4706c27a3..082fa81fe02 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -205,6 +205,7 @@ C(dscp_mark_class)." required: false set_dscp_mark_class: + description: - "This allows specifying a predefined DiffServ class which will be translated to the corresponding DSCP mark. Mutually exclusive with C(dscp_mark)." From 404267b06442f5b16f5b53af96ef388d0f58fdf4 Mon Sep 17 00:00:00 2001 From: David Hocky Date: Wed, 9 Mar 2016 11:20:34 -0500 Subject: [PATCH 1280/2522] add version added to new params --- system/iptables.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/system/iptables.py b/system/iptables.py index 082fa81fe02..872886f510d 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -199,12 +199,14 @@ sctp." required: false set_dscp_mark: + version_added: "2.1" description: - "This allows specifying a DSCP mark to be added to packets. It takes either an integer or hex value. Mutually exclusive with C(dscp_mark_class)." required: false set_dscp_mark_class: + version_added: "2.1" description: - "This allows specifying a predefined DiffServ class which will be translated to the corresponding DSCP mark. Mutually exclusive with From e8916a649163a45c7df53c5bf0fca0aae77aae96 Mon Sep 17 00:00:00 2001 From: liquidat Date: Thu, 10 Mar 2016 14:56:36 +0100 Subject: [PATCH 1281/2522] correct win_firewall state parameter - original parameter comment was probably copy&paste error - new comment highlights that firewall rules can be added or removed altering this parameter --- windows/win_firewall_rule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_firewall_rule.py b/windows/win_firewall_rule.py index 64ec3050474..03611a60ef4 100644 --- a/windows/win_firewall_rule.py +++ b/windows/win_firewall_rule.py @@ -34,7 +34,7 @@ choices: ['yes', 'no'] state: description: - - create/remove/update or powermanage your VM + - should this rule be added or removed default: "present" required: true choices: ['present', 'absent'] From 0cdd66fa235d3d00c54221b314584ab46e9d3259 Mon Sep 17 00:00:00 2001 From: liquidat Date: Thu, 10 Mar 2016 15:14:27 +0100 Subject: [PATCH 1282/2522] remove legacy action style from examples - "action" style invoking is a legacy way to call modules - the examples were updated to the typical style of calling complex modules: ovirt: parameter1: value1 parameter2: value2 ... --- cloud/misc/ovirt.py | 78 ++++++++++++++++++++++----------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/cloud/misc/ovirt.py b/cloud/misc/ovirt.py index 10f8e5c0a44..760d4ffc62c 100644 --- a/cloud/misc/ovirt.py +++ b/cloud/misc/ovirt.py @@ -159,51 +159,51 @@ EXAMPLES = ''' # Basic example provisioning from image. -action: ovirt > - user=admin@internal - url=https://ovirt.example.com - instance_name=ansiblevm04 - password=secret - image=centos_64 - zone=cluster01 - resource_type=template" +ovirt: + user: admin@internal + url: https://ovirt.example.com + instance_name: ansiblevm04 + password: secret + image: centos_64 + zone: cluster01 + resource_type: template" # Full example to create new instance from scratch -action: ovirt > - instance_name=testansible - resource_type=new - instance_type=server - user=admin@internal - password=secret - url=https://ovirt.example.com - instance_disksize=10 - zone=cluster01 - region=datacenter1 - instance_cpus=1 - instance_nic=nic1 - instance_network=rhevm - instance_mem=1000 - disk_alloc=thin - sdomain=FIBER01 - instance_cores=1 - instance_os=rhel_6x64 - disk_int=virtio" +ovirt: + instance_name: testansible + resource_type: new + instance_type: server + user: admin@internal + password: secret + url: https://ovirt.example.com + instance_disksize: 10 + zone: cluster01 + region: datacenter1 + instance_cpus: 1 + instance_nic: nic1 + instance_network: rhevm + instance_mem: 1000 + disk_alloc: thin + sdomain: FIBER01 + instance_cores: 1 + instance_os: rhel_6x64 + disk_int: virtio" # stopping an instance -action: ovirt > - instance_name=testansible - state=stopped - user=admin@internal - password=secret - url=https://ovirt.example.com +ovirt: + instance_name: testansible + state: stopped + user: admin@internal + password: secret + url: https://ovirt.example.com # starting an instance -action: ovirt > - instance_name=testansible - state=started - user=admin@internal - password=secret - url=https://ovirt.example.com +ovirt: + instance_name: testansible + state: started + user: admin@internal + password: secret + url: https://ovirt.example.com ''' From c6598e3672eab4addf99bfd4b2a41e20073bc311 Mon Sep 17 00:00:00 2001 From: Matt Colton Date: Thu, 19 Nov 2015 06:12:54 -0600 Subject: [PATCH 1283/2522] Added Softlayer Module --- cloud/softlayer/__init__.py | 0 cloud/softlayer/sl.py | 348 ++++++++++++++++++++++++++++++++++++ 2 files changed, 348 insertions(+) create mode 100644 cloud/softlayer/__init__.py create mode 100644 cloud/softlayer/sl.py diff --git a/cloud/softlayer/__init__.py b/cloud/softlayer/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cloud/softlayer/sl.py b/cloud/softlayer/sl.py new file mode 100644 index 00000000000..f5a48bd4e93 --- /dev/null +++ b/cloud/softlayer/sl.py @@ -0,0 +1,348 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: SoftLayer +short_description: create or cancel a virtual instance in SoftLayer +description: + - Creates or cancels SoftLayer instances. When created, optionally waits for it to be 'running'. +version_added: "2.1" +options: + instance_id: + description: + - Instance Id of the virtual instance to perform action option + required: false + default: null + hostname: + description: + - Hostname to be provided to a virtual instance + required: false + default: null + domain: + description: + - Domain name to be provided to a virtual instance + required: false + default: null + datacenter: + description: + - Datacenter for the virtual instance to be deployed + required: false + default: null + tags: + description: + - Tag or list of tags to be provided to a virtual instance + required: false + default: null + hourly: + description: + - Flag to determine if the instance should be hourly billed + required: false + default: true + private: + description: + - Flag to determine if the instance should be private only + required: false + default: false + dedicated: + description: + - Falg to determine if the instance should be deployed in dedicated space + required: false + default: false + local_disk: + description: + - Flag to determine if local disk should be used for the new instance + required: false + default: true + cpus: + description: + - Count of cpus to be assigned to new virtual instance + required: true + default: null + memory: + description: + - Amount of memory to be assigned to new virtual instance + required: true + default: null + disks: + description: + - List of disk sizes to be assigned to new virtual instance + required: true + default: [25] + os_code: + description: + - OS Code to be used for new virtual instance + required: false + default: null + image_id: + description: + - Image Template to be used for new virtual instance + required: false + default: null + nic_speed: + description: + - NIC Speed to be assigned to new virtual instance + required: false + default: 10 + public_vlan: + description: + - VLAN by its Id to be assigned to the public NIC + required: false + default: null + private_vlan: + description: + - VLAN by its Id to be assigned to the private NIC + required: false + default: null + ssh_keys: + description: + - List of ssh keys by their Id to be assigned to a virtual instance + required: false + default: null + post_uri: + description: + - URL of a post provisioning script ot be loaded and exectued on virtual instance + required: false + default: null + state: + description: + - Create, or cancel a virtual instance. Specify "present" for create, "absent" to cancel. + required: false + default: 'present' + wait: + description: + - Flag used to wait for active status before returning + required: false + default: true + wait_timeout: + description: + - time in seconds before wait returns + required: false + default: 600 + +requirements: + - "python >= 2.6" + - "softlayer >= 4.1.1" +author: "Matt Colton (@mcltn)" +''' + +EXAMPLES = ''' +- name: Build instance + hosts: localhost + gather_facts: False + tasks: + - name: Build instance request + local_action: + module: sl + hostname: instance-1 + domain: anydomain.com + datacenter: dal09 + tags: ansible-module-test + hourly: True + private: False + dedicated: False + local_disk: True + cpus: 1 + memory: 1024 + disks: [25] + os_code: UBUNTU_LATEST + wait: False + +- name: Build additional instances + hosts: localhost + gather_facts: False + tasks: + - name: Build instances request + local_action: + module: sl + hostname: "{{ item.hostname }}" + domain: "{{ item.domain }}" + datacenter: "{{ item.datacenter }}" + tags: "{{ item.tags }}" + hourly: "{{ item.hourly }}" + private: "{{ item.private }}" + dedicated: "{{ item.dedicated }}" + local_disk: "{{ item.local_disk }}" + cpus: "{{ item.cpus }}" + memory: "{{ item.memory }}" + disks: "{{ item.disks }}" + os_code: "{{ item.os_code }}" + ssh_keys: "{{ item.ssh_keys }}" + wait: "{{ item.wait }}" + with_items: + - { hostname: 'instance-2', domain: 'anydomain.com', datacenter: 'dal09', tags: ['ansible-module-test', 'ansible-module-test-slaves'], hourly: True, private: False, dedicated: False, local_disk: True, cpus: 1, memory: 1024, disks: [25,100], os_code: 'UBUNTU_LATEST', ssh_keys: [], wait: True } + - { hostname: 'instance-3', domain: 'anydomain.com', datacenter: 'dal09', tags: ['ansible-module-test', 'ansible-module-test-slaves'], hourly: True, private: False, dedicated: False, local_disk: True, cpus: 1, memory: 1024, disks: [25,100], os_code: 'UBUNTU_LATEST', ssh_keys: [], wait: True } + + +- name: Cancel instances + hosts: localhost + gather_facts: False + tasks: + - name: Cancel by tag + local_action: + module: sl + state: absent + tags: ansible-module-test +''' + +# TODO: Disabled RETURN as it is breaking the build for docs. Needs to be fixed. +RETURN = '''# ''' + +import time + +STATES = ['present', 'absent'] +DATACENTERS = ['ams01','ams03','dal01','dal05','dal06','dal09','fra02','hkg02','hou02','lon2','mel01','mex01','mil01','mon01','par01','sjc01','sjc03','sao01','sea01','sng01','syd01','tok02','tor01','wdc01','wdc04'] +CPU_SIZES = [1,2,4,8,16] +MEMORY_SIZES = [1024,2048,4096,6144,8192,12288,16384,32768,49152,65536] +INITIALDISK_SIZES = [25,100] +LOCALDISK_SIZES = [25,100,150,200,300] +SANDISK_SIZES = [10,20,25,30,40,50,75,100,125,150,175,200,250,300,350,400,500,750,1000,1500,2000] +NIC_SPEEDS = [10,100,1000] + +try: + import SoftLayer + from SoftLayer import VSManager + + HAS_SL = True + vsManager = VSManager(SoftLayer.create_client_from_env()) +except ImportError: + HAS_SL = False + + +def create_virtual_instance(module): + + # Check if OS or Image Template is provided (Can't be both, defaults to OS) + if (module.params.get('os_code') != None and module.params.get('os_code') != ''): + module.params['image_id'] = '' + elif (module.params.get('image_id') != None and module.params.get('image_id') != ''): + module.params['os_code'] = '' + module.params['disks'] = [] # Blank out disks since it will use the template + else: + return False, None + + tags = module.params.get('tags') + if isinstance(tags, list): + tags = ','.join(map(str, module.params.get('tags'))) + + instance = vsManager.create_instance( + hostname = module.params.get('hostname'), + domain = module.params.get('domain'), + cpus = module.params.get('cpus'), + memory = module.params.get('memory'), + hourly = module.params.get('hourly'), + datacenter = module.params.get('datacenter'), + os_code = module.params.get('os_code'), + image_id = module.params.get('image_id'), + local_disk = module.params.get('local_disk'), + disks = module.params.get('disks'), + ssh_keys = module.params.get('ssh_keys'), + nic_speed = module.params.get('nic_speed'), + private = module.params.get('private'), + public_vlan = module.params.get('public_vlan'), + private_vlan = module.params.get('private_vlan'), + dedicated = module.params.get('dedicated'), + post_uri = module.params.get('post_uri'), + tags = tags) + + if instance != None and instance['id'] > 0: + return True, instance + else: + return False, None + + +def wait_for_instance(module,id): + instance = None + completed = False + wait_timeout = time.time() + module.params.get('wait_time') + while not completed and wait_timeout > time.time(): + try: + completed = vsManager.wait_for_ready(id, 10, 2) + if completed: + instance = vsManager.get_instance(id) + except: + completed = False + + return completed, instance + + +def cancel_instance(module): + canceled = True + if module.params.get('instance_id') == None and (module.params.get('tags') or module.params.get('hostname') or module.params.get('domain')): + tags = module.params.get('tags') + if isinstance(tags, basestring): + tags = [module.params.get('tags')] + instances = vsManager.list_instances(tags = tags, hostname = module.params.get('hostname'), domain = module.params.get('domain')) + for instance in instances: + try: + vsManager.cancel_instance(instance['id']) + except: + canceled = False + elif module.params.get('instance_id') and module.params.get('instance_id') != 0: + try: + vsManager.cancel_instance(instance['id']) + except: + canceled = False + else: + return False, None + + return canceled, None + + +def main(): + module = AnsibleModule( + argument_spec=dict( + instance_id=dict(), + hostname=dict(), + domain=dict(), + datacenter=dict(choices=DATACENTERS), + tags=dict(), + hourly=dict(type='bool', default=True), + private=dict(type='bool', default=False), + dedicated=dict(type='bool', default=False), + local_disk=dict(type='bool', default=True), + cpus=dict(type='int', choices=CPU_SIZES), + memory=dict(type='int', choices=MEMORY_SIZES), + disks=dict(type='list', default=[25]), + os_code=dict(), + image_id=dict(), + nic_speed=dict(type='int', choices=NIC_SPEEDS), + public_vlan=dict(), + private_vlan=dict(), + ssh_keys=dict(type='list', default=[]), + post_uri=dict(), + state=dict(default='present', choices=STATES), + wait=dict(type='bool', default=True), + wait_time=dict(type='int', default=600) + ) + ) + + if not HAS_SL: + module.fail_json(msg='softlayer python library required for this module') + + if module.params.get('state') == 'absent': + (changed, instance) = cancel_instance(module) + + elif module.params.get('state') == 'present': + (changed, instance) = create_virtual_instance(module) + if module.params.get('wait') == True: + (changed, instance) = wait_for_instance(module, instance['id']) + + module.exit_json(changed=changed, instance=json.loads(json.dumps(instance, default=lambda o: o.__dict__))) + +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() \ No newline at end of file From 19034bae622748799f967bea785ffe95c8c69b69 Mon Sep 17 00:00:00 2001 From: Tyler Cross Date: Thu, 10 Mar 2016 13:41:04 -0500 Subject: [PATCH 1284/2522] Add note server 2012 note to win_scheduled_task. This change adds a note to the win_scheduled_task module docs that indicates Windows Server 2012 or later is required. This is because the module relies on the Get-ScheduledTask cmdlet, which is a part of the Server 2012 OS. Previous versions, like Server 2008, simply can't work with this module. --- windows/win_scheduled_task.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/windows/win_scheduled_task.py b/windows/win_scheduled_task.py index e26cbc00cf0..3c6ef9d28a9 100644 --- a/windows/win_scheduled_task.py +++ b/windows/win_scheduled_task.py @@ -25,6 +25,8 @@ short_description: Manage scheduled tasks description: - Manage scheduled tasks +notes: + - This module requires Windows Server 2012 or later. options: name: description: From 6456484eb33ef5886ae549155a2b974f32a1db62 Mon Sep 17 00:00:00 2001 From: nitzmahone Date: Thu, 10 Mar 2016 13:02:33 -0800 Subject: [PATCH 1285/2522] fix missing bracket in win_file_version --- windows/win_file_version.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_file_version.ps1 b/windows/win_file_version.ps1 index eefa221e88d..d3e17f14800 100644 --- a/windows/win_file_version.ps1 +++ b/windows/win_file_version.ps1 @@ -64,7 +64,7 @@ Try { $file_build_part = '' } $file_private_part = $_version_fields.FilePrivatePart - If ($file_private_part -eq $null) + If ($file_private_part -eq $null){ $file_private_part = '' } } From c8137a47e284c8cdafe128d5a6a9a01864df4bbd Mon Sep 17 00:00:00 2001 From: nitzmahone Date: Thu, 10 Mar 2016 13:16:07 -0800 Subject: [PATCH 1286/2522] fix default arg handling and error messages in win_file_version --- windows/win_file_version.ps1 | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/windows/win_file_version.ps1 b/windows/win_file_version.ps1 index d3e17f14800..2e2f341c461 100644 --- a/windows/win_file_version.ps1 +++ b/windows/win_file_version.ps1 @@ -26,19 +26,14 @@ $result = New-Object psobject @{ changed = $false } +$path = Get-AnsibleParam $params "path" -failifempty $true -resultobj $result -If ($params.path) { - $path = $params.path.ToString() - If (-Not (Test-Path -Path $path -PathType Leaf)){ - Fail-Json $result "Specfied path: $path not exists or not a file" - } - $ext = [System.IO.Path]::GetExtension($path) - If ( $ext -notin '.exe', '.dll'){ - Fail-Json $result "Specfied path: $path is not a vaild file type, Must be DLL or EXE." - } +If (-Not (Test-Path -Path $path -PathType Leaf)){ + Fail-Json $result "Specfied path $path does exist or is not a file." } -Else{ - Fail-Json $result "Specfied path: $path not define." +$ext = [System.IO.Path]::GetExtension($path) +If ( $ext -notin '.exe', '.dll'){ + Fail-Json $result "Specfied path $path is not a vaild file type; must be DLL or EXE." } Try { From f1b6eeabb23538ac59d1e8e62fb076a727d6b6c7 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Thu, 10 Mar 2016 14:11:16 -0800 Subject: [PATCH 1287/2522] Add optional mysql connect timeout. --- database/mysql/mysql_replication.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/database/mysql/mysql_replication.py b/database/mysql/mysql_replication.py index 8729514760e..bdfcdbf1391 100644 --- a/database/mysql/mysql_replication.py +++ b/database/mysql/mysql_replication.py @@ -205,6 +205,7 @@ def main(): master_ssl_cert=dict(default=None), master_ssl_key=dict(default=None), master_ssl_cipher=dict(default=None), + connect_timeout=dict(default=30, type='int'), config_file=dict(default="~/.my.cnf"), ssl_cert=dict(default=None), ssl_key=dict(default=None), @@ -235,6 +236,7 @@ def main(): ssl_cert = module.params["ssl_cert"] ssl_key = module.params["ssl_key"] ssl_ca = module.params["ssl_ca"] + connect_timeout = module.params['connect_timeout'] config_file = module.params['config_file'] config_file = os.path.expanduser(os.path.expandvars(config_file)) @@ -247,7 +249,8 @@ def main(): login_user = module.params["login_user"] try: - cursor = mysql_connect(module, login_user, login_password, config_file, ssl_cert, ssl_key, ssl_ca, None, 'MySQLdb.cursors.DictCursor') + cursor = mysql_connect(module, login_user, login_password, config_file, ssl_cert, ssl_key, ssl_ca, None, 'MySQLdb.cursors.DictCursor', + connect_timeout=connect_timeout) except Exception, e: 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)) From 41a719ce71a02e171c289070c9eb3bae80f36831 Mon Sep 17 00:00:00 2001 From: Corwin Brown Date: Fri, 11 Mar 2016 07:32:35 -0600 Subject: [PATCH 1288/2522] Updated Documentation to include site_id param --- windows/win_iis_website.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/windows/win_iis_website.py b/windows/win_iis_website.py index 8921afe5970..71553401feb 100644 --- a/windows/win_iis_website.py +++ b/windows/win_iis_website.py @@ -21,7 +21,7 @@ DOCUMENTATION = ''' --- module: win_iis_website -version_added: "2.0" +version_added: "2.1" short_description: Configures a IIS Web site. description: - Creates, Removes and configures a IIS Web site @@ -32,6 +32,11 @@ required: true default: null aliases: [] + site_id: + description: + - Explicitly set the IIS ID for a site. + required: false + default: null state: description: - State of the web site From d27110b8df2af53d0565328231e5ac3b8c780364 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Wed, 24 Feb 2016 19:26:00 -0500 Subject: [PATCH 1289/2522] updated to match core module forms --- ISSUE_TEMPLATE.md | 30 +++++++++++++++--------------- PULL_REQUEST_TEMPLATE.md | 20 +++++++------------- 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 0dcb075b7d6..5870836fcc6 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -1,53 +1,53 @@ ##### Issue Type: -Please pick one and delete the rest: + - Bug Report - Feature Idea - Documentation Report ##### Plugin Name: -Name of the plugin/module/task + ##### Ansible Version: ``` -(Paste verbatim output from “ansible --version” here) + ``` ##### Ansible Configuration: -Please mention any settings you've changed/added/removed in ansible.cfg -(or using the ANSIBLE_* environment variables). + ##### Environment: -Please mention the OS you are running Ansible from, and the OS you are -managing, or say “N/A” for anything that isn't platform-specific. + ##### Summary: -Please explain the problem briefly. + ##### Steps To Reproduce: -For bugs, please show exactly how to reproduce the problem. For new -features, show how the feature would be used. + ``` -(Paste example playbooks or commands here) + ``` -You can also paste gist.github.com links for larger files. + ##### Expected Results: -What did you expect to happen when running the steps above? + ##### Actual Results: -What actually happened? + ``` -(Paste verbatim command output here) + ``` diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index 8f3791f3c91..f821123acd4 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ ##### Issue Type: -Please pick one and delete the rest: + - Feature Pull Request - New Module Pull Request - Bugfix Pull Request @@ -8,24 +8,18 @@ Please pick one and delete the rest: ##### Plugin Name: -Name of the plugin/module/task - -##### Ansible Version: - -``` -(Paste verbatim output from “ansible --version” here) -``` + ##### Summary: -Please describe the change and the reason for it. + -(If you're fixing an existing issue, please include "Fixes #nnn" in your + -##### Example output: +##### Example: ``` -(Paste verbatim command output here if necessary) + ``` From df217fe892ddcec2e1fa595d9dc83c5976ba22d6 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Fri, 11 Mar 2016 10:09:05 -0500 Subject: [PATCH 1290/2522] added new puppet path to bin search fixes #1835 --- system/puppet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/puppet.py b/system/puppet.py index 852a6f9d61b..0b83e292370 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -136,7 +136,7 @@ def main(): p = module.params global PUPPET_CMD - PUPPET_CMD = module.get_bin_path("puppet", False) + PUPPET_CMD = module.get_bin_path("puppet", False, ['/opt/puppetlabs/bin']) if not PUPPET_CMD: module.fail_json( From 44effbca580459e7aac3b5ce3121e9fd0a935f40 Mon Sep 17 00:00:00 2001 From: nitzmahone Date: Fri, 11 Mar 2016 10:06:30 -0800 Subject: [PATCH 1291/2522] added doc note to win_iis_website site_id arg --- windows/win_iis_website.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_iis_website.py b/windows/win_iis_website.py index 71553401feb..4bbb3afe0d5 100644 --- a/windows/win_iis_website.py +++ b/windows/win_iis_website.py @@ -34,7 +34,7 @@ aliases: [] site_id: description: - - Explicitly set the IIS ID for a site. + - Explicitly set the IIS numeric ID for a site. Note that this value cannot be changed after the website has been created. required: false default: null state: From a7faa0124f5456f69628fd6c0179687122de25fd Mon Sep 17 00:00:00 2001 From: mxpt user Date: Fri, 11 Mar 2016 13:16:58 -0800 Subject: [PATCH 1292/2522] Fixing typo in bigip_monitors Correcting set_template_int_property to set_template_integer_property --- network/f5/bigip_monitor_http.py | 2 +- network/f5/bigip_monitor_tcp.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/network/f5/bigip_monitor_http.py b/network/f5/bigip_monitor_http.py index 9e39aadd109..cfafbad9be1 100644 --- a/network/f5/bigip_monitor_http.py +++ b/network/f5/bigip_monitor_http.py @@ -250,7 +250,7 @@ def check_integer_property(api, monitor, int_property): def set_integer_property(api, monitor, int_property): - api.LocalLB.Monitor.set_template_int_property(template_names=[monitor], values=[int_property]) + api.LocalLB.Monitor.set_template_integer_property(template_names=[monitor], values=[int_property]) def update_monitor_properties(api, module, monitor, template_string_properties, template_integer_properties): diff --git a/network/f5/bigip_monitor_tcp.py b/network/f5/bigip_monitor_tcp.py index 2dffa4509d8..564f4564283 100644 --- a/network/f5/bigip_monitor_tcp.py +++ b/network/f5/bigip_monitor_tcp.py @@ -269,7 +269,7 @@ def check_integer_property(api, monitor, int_property): def set_integer_property(api, monitor, int_property): - api.LocalLB.Monitor.set_template_int_property(template_names=[monitor], values=[int_property]) + api.LocalLB.Monitor.set_template_integer_property(template_names=[monitor], values=[int_property]) def update_monitor_properties(api, module, monitor, template_string_properties, template_integer_properties): From 547fd41352ee8b73992597765bf73077b79738b9 Mon Sep 17 00:00:00 2001 From: David Justice Date: Fri, 4 Dec 2015 15:06:27 -0800 Subject: [PATCH 1293/2522] add azure resource manager template deployment module --- cloud/azure/azure_deployment.py | 672 ++++++++++++++++++++++++++++++++ 1 file changed, 672 insertions(+) create mode 100644 cloud/azure/azure_deployment.py diff --git a/cloud/azure/azure_deployment.py b/cloud/azure/azure_deployment.py new file mode 100644 index 00000000000..4e97b0b694e --- /dev/null +++ b/cloud/azure/azure_deployment.py @@ -0,0 +1,672 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +DOCUMENTATION = ''' +--- +module: azure_deployment +short_description: Create or destroy Azure Resource Manager template deployments +version_added: "2.0" +description: + - Create or destroy Azure Resource Manager template deployments via the Azure SDK for Python. + You can find some quick start templates in GitHub here: https://github.com/azure/azure-quickstart-templates. + If you would like to find out more information about Azure Resource Manager templates, see: https://azure.microsoft.com/en-us/documentation/articles/resource-group-template-deploy/. +options: + subscription_id: + description: + - The Azure subscription to deploy the template into. + required: true + resource_group_name: + description: + - The resource group name to use or create to host the deployed template + required: true + state: + description: + - If state is "present", template will be created. If state is "present" and if deployment exists, it will be updated. + If state is "absent", stack will be removed. + required: true + template: + description: + - A hash containg the templates inline. This parameter is mutually exclusive with 'template_link'. + Either one of them is required if "state" parameter is "present". + required: false + default: None + template_link: + description: + - Uri of file containing the template body. This parameter is mutually exclusive with 'template'. Either one + of them is required if "state" parameter is "present". + required: false + default: None + parameters: + description: + - A hash of all the required template variables for the deployment template. This parameter is mutually exclusive with 'parameters_link'. + Either one of them is required if "state" parameter is "present". + required: false + default: None + parameters_link: + description: + - Uri of file containing the parameters body. This parameter is mutually exclusive with 'parameters'. Either + one of them is required if "state" parameter is "present". + required: false + default: None + location: + description: + - The geo-locations in which the resource group will be located. + require: false + default: West US + +author: "David Justice (@devigned)" +''' + +EXAMPLES = ''' +# Destroy a template deployment +- name: Destroy Azure Deploy + azure_deploy: + state: absent + subscription_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + resource_group_name: dev-ops-cle + +# Create or update a template deployment based on uris to paramters and a template +- name: Create Azure Deploy + azure_deploy: + state: present + subscription_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + resource_group_name: dev-ops-cle + parameters_link: 'https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-simple-linux-vm/azuredeploy.parameters.json' + template_link: 'https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-simple-linux-vm/azuredeploy.json' + +# Create or update a template deployment based on a uri to the template and parameters specified inline. +# This deploys a VM with SSH support for a given public key, then stores the result in 'azure_vms'. The result is then used +# to create a new host group. This host group is then used to wait for each instance to respond to the public IP SSH. +--- +- hosts: localhost + tasks: + - name: Destroy Azure Deploy + azure_deployment: + state: absent + subscription_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + resource_group_name: dev-ops-cle + + - name: Create Azure Deploy + azure_deployment: + state: present + subscription_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + resource_group_name: dev-ops-cle + parameters: + newStorageAccountName: + value: devopsclestorage1 + adminUsername: + value: devopscle + dnsNameForPublicIP: + value: devopscleazure + location: + value: West US + vmSize: + value: Standard_A2 + vmName: + value: ansibleSshVm + sshKeyData: + value: YOUR_SSH_PUBLIC_KEY + template_link: 'https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-vm-sshkey/azuredeploy.json' + register: azure + - name: Add new instance to host group + add_host: hostname={{ item['ips'][0].public_ip }} groupname=azure_vms + with_items: azure.instances + +- hosts: azure_vms + user: devopscle + tasks: + - name: Wait for SSH to come up + wait_for: port=22 timeout=2000 state=started + - name: echo the hostname of the vm + shell: hostname + +# Deploy an Azure WebApp running a hello world'ish node app +- name: Create Azure WebApp Deployment at http://devopscleweb.azurewebsites.net/hello.js + azure_deployment: + state: present + subscription_id: cbbdaed0-fea9-4693-bf0c-d446ac93c030 + resource_group_name: dev-ops-cle-webapp + parameters: + repoURL: + value: 'https://github.com/devigned/az-roadshow-oss.git' + siteName: + value: devopscleweb + hostingPlanName: + value: someplan + siteLocation: + value: westus + sku: + value: Standard + template_link: 'https://raw.githubusercontent.com/azure/azure-quickstart-templates/master/201-web-app-github-deploy/azuredeploy.json' + +# Create or update a template deployment based on an inline template and parameters +- name: Create Azure Deploy + azure_deploy: + state: present + subscription_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + resource_group_name: dev-ops-cle + + template: + $schema: "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#" + contentVersion: "1.0.0.0" + parameters: + newStorageAccountName: + type: "string" + metadata: + description: "Unique DNS Name for the Storage Account where the Virtual Machine's disks will be placed." + adminUsername: + type: "string" + metadata: + description: "User name for the Virtual Machine." + adminPassword: + type: "securestring" + metadata: + description: "Password for the Virtual Machine." + dnsNameForPublicIP: + type: "string" + metadata: + description: "Unique DNS Name for the Public IP used to access the Virtual Machine." + ubuntuOSVersion: + type: "string" + defaultValue: "14.04.2-LTS" + allowedValues: + - "12.04.5-LTS" + - "14.04.2-LTS" + - "15.04" + metadata: + description: "The Ubuntu version for the VM. This will pick a fully patched image of this given Ubuntu version. Allowed values: 12.04.5-LTS, 14.04.2-LTS, 15.04." + variables: + location: "West US" + imagePublisher: "Canonical" + imageOffer: "UbuntuServer" + OSDiskName: "osdiskforlinuxsimple" + nicName: "myVMNic" + addressPrefix: "10.0.0.0/16" + subnetName: "Subnet" + subnetPrefix: "10.0.0.0/24" + storageAccountType: "Standard_LRS" + publicIPAddressName: "myPublicIP" + publicIPAddressType: "Dynamic" + vmStorageAccountContainerName: "vhds" + vmName: "MyUbuntuVM" + vmSize: "Standard_D1" + virtualNetworkName: "MyVNET" + vnetID: "[resourceId('Microsoft.Network/virtualNetworks',variables('virtualNetworkName'))]" + subnetRef: "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]" + resources: + - + type: "Microsoft.Storage/storageAccounts" + name: "[parameters('newStorageAccountName')]" + apiVersion: "2015-05-01-preview" + location: "[variables('location')]" + properties: + accountType: "[variables('storageAccountType')]" + - + apiVersion: "2015-05-01-preview" + type: "Microsoft.Network/publicIPAddresses" + name: "[variables('publicIPAddressName')]" + location: "[variables('location')]" + properties: + publicIPAllocationMethod: "[variables('publicIPAddressType')]" + dnsSettings: + domainNameLabel: "[parameters('dnsNameForPublicIP')]" + - + type: "Microsoft.Network/virtualNetworks" + apiVersion: "2015-05-01-preview" + name: "[variables('virtualNetworkName')]" + location: "[variables('location')]" + properties: + addressSpace: + addressPrefixes: + - "[variables('addressPrefix')]" + subnets: + - + name: "[variables('subnetName')]" + properties: + addressPrefix: "[variables('subnetPrefix')]" + - + type: "Microsoft.Network/networkInterfaces" + apiVersion: "2015-05-01-preview" + name: "[variables('nicName')]" + location: "[variables('location')]" + dependsOn: + - "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]" + - "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]" + properties: + ipConfigurations: + - + name: "ipconfig1" + properties: + privateIPAllocationMethod: "Dynamic" + publicIPAddress: + id: "[resourceId('Microsoft.Network/publicIPAddresses',variables('publicIPAddressName'))]" + subnet: + id: "[variables('subnetRef')]" + - + type: "Microsoft.Compute/virtualMachines" + apiVersion: "2015-06-15" + name: "[variables('vmName')]" + location: "[variables('location')]" + dependsOn: + - "[concat('Microsoft.Storage/storageAccounts/', parameters('newStorageAccountName'))]" + - "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]" + properties: + hardwareProfile: + vmSize: "[variables('vmSize')]" + osProfile: + computername: "[variables('vmName')]" + adminUsername: "[parameters('adminUsername')]" + adminPassword: "[parameters('adminPassword')]" + storageProfile: + imageReference: + publisher: "[variables('imagePublisher')]" + offer: "[variables('imageOffer')]" + sku: "[parameters('ubuntuOSVersion')]" + version: "latest" + osDisk: + name: "osdisk" + vhd: + uri: "[concat('http://',parameters('newStorageAccountName'),'.blob.core.windows.net/',variables('vmStorageAccountContainerName'),'/',variables('OSDiskName'),'.vhd')]" + caching: "ReadWrite" + createOption: "FromImage" + networkProfile: + networkInterfaces: + - + id: "[resourceId('Microsoft.Network/networkInterfaces',variables('nicName'))]" + diagnosticsProfile: + bootDiagnostics: + enabled: "true" + storageUri: "[concat('http://',parameters('newStorageAccountName'),'.blob.core.windows.net')]" + parameters: + newStorageAccountName: + value: devopsclestorage + adminUsername: + value: devopscle + adminPassword: + value: Password1! + dnsNameForPublicIP: + value: devopscleazure +''' + +try: + import time + import yaml + import requests + import azure + from itertools import chain + from azure.mgmt.common import SubscriptionCloudCredentials + from azure.mgmt.resource import ResourceManagementClient + + HAS_DEPS = True +except ImportError: + HAS_DEPS = False + +AZURE_URL = "https://management.azure.com" +DEPLOY_URL_FORMAT = "/subscriptions/{}/resourcegroups/{}/providers/microsoft.resources/deployments/{}?api-version={}" +RES_GROUP_URL_FORMAT = "/subscriptions/{}/resourcegroups/{}?api-version={}" +ARM_API_VERSION = "2015-01-01" +NETWORK_API_VERSION = "2015-06-15" + + +def get_token(domain_or_tenant, client_id, client_secret): + """ + Get an Azure Active Directory token for a service principal + :param domain_or_tenant: The domain or tenant id of your Azure Active Directory instance + :param client_id: The client id of your application in Azure Active Directory + :param client_secret: One of the application secrets created in your Azure Active Directory application + :return: an authenticated bearer token to be used with requests to the API + """ + # the client id we can borrow from azure xplat cli + grant_type = 'client_credentials' + token_url = 'https://login.microsoftonline.com/{}/oauth2/token'.format(domain_or_tenant) + + payload = { + 'grant_type': grant_type, + 'client_id': client_id, + 'client_secret': client_secret, + 'resource': 'https://management.core.windows.net/' + } + + res = requests.post(token_url, data=payload) + return res.json()['access_token'] if res.status_code == 200 else None + + +def get_azure_connection_info(module): + azure_url = module.params.get('azure_url') + tenant_or_domain = module.params.get('tenant_or_domain') + client_id = module.params.get('client_id') + client_secret = module.params.get('client_secret') + security_token = module.params.get('security_token') + resource_group_name = module.params.get('resource_group_name') + subscription_id = module.params.get('subscription_id') + + if not azure_url: + if 'AZURE_URL' in os.environ: + azure_url = os.environ['AZURE_URL'] + else: + azure_url = None + + if not subscription_id: + if 'AZURE_SUBSCRIPTION_ID' in os.environ: + subscription_id = os.environ['AZURE_SUBSCRIPTION_ID'] + else: + subscription_id = None + + if not resource_group_name: + if 'AZURE_RESOURCE_GROUP_NAME' in os.environ: + resource_group_name = os.environ['AZURE_RESOURCE_GROUP_NAME'] + else: + resource_group_name = None + + if not security_token: + if 'AZURE_SECURITY_TOKEN' in os.environ: + security_token = os.environ['AZURE_SECURITY_TOKEN'] + else: + security_token = None + + if not tenant_or_domain: + if 'AZURE_TENANT_ID' in os.environ: + tenant_or_domain = os.environ['AZURE_TENANT_ID'] + elif 'AZURE_DOMAIN' in os.environ: + tenant_or_domain = os.environ['AZURE_DOMAIN'] + else: + tenant_or_domain = None + + if not client_id: + if 'AZURE_CLIENT_ID' in os.environ: + client_id = os.environ['AZURE_CLIENT_ID'] + else: + client_id = None + + if not client_secret: + if 'AZURE_CLIENT_SECRET' in os.environ: + client_secret = os.environ['AZURE_CLIENT_SECRET'] + else: + client_secret = None + + return dict(azure_url=azure_url, + tenant_or_domain=tenant_or_domain, + client_id=client_id, + client_secret=client_secret, + security_token=security_token, + resource_group_name=resource_group_name, + subscription_id=subscription_id) + + +def build_deployment_body(module): + """ + Build the deployment body from the module parameters + :param module: Ansible module containing the validated configuration for the deployment template + :return: body as dict + """ + properties = dict(mode='Incremental') + properties['templateLink'] = \ + dict(uri=module.params.get('template_link'), + contentVersion=module.params.get('content_version')) + + properties['parametersLink'] = \ + dict(uri=module.params.get('parameters_link'), + contentVersion=module.params.get('content_version')) + + return dict(properties=properties) + + +def follow_deployment(client, group_name, deployment): + state = deployment.properties.provisioning_state + if state == azure.mgmt.common.OperationStatus.Failed or \ + state == azure.mgmt.common.OperationStatus.Succeeded or \ + state == "Canceled" or \ + state == "Deleted": + return deployment + else: + time.sleep(30) + result = client.deployments.get(group_name, deployment.name) + return follow_deployment(client, group_name, result.deployment) + + +def follow_delete(client, location): + result = client.get_long_running_operation_status(location) + if result.status == azure.mgmt.common.OperationStatus.Succeeded: + return True + elif result.status == azure.mgmt.common.OperationStatus.Failed: + return False + else: + time.sleep(30) + return follow_delete(client, location) + + +def deploy_template(module, client, conn_info): + """ + Deploy the targeted template and parameters + :param module: Ansible module containing the validated configuration for the deployment template + :param client: resource management client for azure + :param conn_info: connection info needed + :return: + """ + + deployment_name = conn_info["deployment_name"] + group_name = conn_info["resource_group_name"] + + deploy_parameter = azure.mgmt.resource.DeploymentProperties() + deploy_parameter.mode = "Complete" + + if module.params.get('parameters_link') is None: + deploy_parameter.parameters = json.dumps(module.params.get('parameters'), ensure_ascii=False) + else: + parameters_link = azure.mgmt.resource.ParametersLink() + parameters_link.uri = module.params.get('parameters_link') + deploy_parameter.parameters_link = parameters_link + + if module.params.get('template_link') is None: + deploy_parameter.template = json.dumps(module.params.get('template'), ensure_ascii=False) + else: + template_link = azure.mgmt.resource.TemplateLink() + template_link.uri = module.params.get('template_link') + deploy_parameter.template_link = template_link + + deployment = azure.mgmt.resource.Deployment(properties=deploy_parameter) + params = azure.mgmt.resource.ResourceGroup(location=module.params.get('location'), tags=module.params.get('tags')) + try: + client.resource_groups.create_or_update(group_name, params) + result = client.deployments.create_or_update(group_name, deployment_name, deployment) + return follow_deployment(client, group_name, result.deployment) + except azure.common.AzureHttpError as e: + module.fail_json(msg='Deploy create failed with status code: %s and message: "%s"' % (e.status_code, e.message)) + + +def deploy_url(subscription_id, resource_group_name, deployment_name, api_version=ARM_API_VERSION): + return AZURE_URL + DEPLOY_URL_FORMAT.format(subscription_id, resource_group_name, deployment_name, api_version) + + +def res_group_url(subscription_id, resource_group_name, api_version=ARM_API_VERSION): + return AZURE_URL + RES_GROUP_URL_FORMAT.format(subscription_id, resource_group_name, api_version) + + +def default_headers(token, with_content=False): + headers = {'Authorization': 'Bearer {}'.format(token), 'Accept': 'application/json'} + if with_content: + headers['Content-Type'] = 'application/json' + return headers + + +def destroy_resource_group(module, client, conn_info): + """ + Destroy the targeted resource group + :param module: ansible module + :param client: resource management client for azure + :param conn_info: connection info needed + :return: if the result caused a change in the deployment + """ + + try: + client.resource_groups.get(conn_info['resource_group_name']) + except azure.common.AzureMissingResourceHttpError: + return False + + try: + url = res_group_url(conn_info['subscription_id'], conn_info['resource_group_name']) + res = requests.delete(url, headers=default_headers(conn_info['security_token'])) + if res.status_code == 404 or res.status_code == 204: + return False + + if res.status_code == 202: + location = res.headers['location'] + follow_delete(client, location) + return True + + if res.status_code == requests.codes.ok: + return True + else: + module.fail_json( + msg='Delete resource group and deploy failed with status code: %s and message: %s' % (res.status_code, res.text)) + except azure.common.AzureHttpError as e: + if e.status_code == 404 or e.status_code == 204: + return True + else: + module.fail_json( + msg='Delete resource group and deploy failed with status code: %s and message: %s' % (e.status_code, e.message)) + + +def get_dependencies(dep_tree, resource_type): + matches = [value for value in dep_tree.values() if value['dep'].resource_type == resource_type] + for child_tree in [value['children'] for value in dep_tree.values()]: + matches += get_dependencies(child_tree, resource_type) + return matches + + +def build_hierarchy(module, dependencies, tree=None): + tree = dict(top=True) if tree is None else tree + for dep in dependencies: + if dep.resource_name not in tree: + tree[dep.resource_name] = dict(dep=dep, children=dict()) + if isinstance(dep, azure.mgmt.resource.Dependency) and dep.depends_on is not None and len(dep.depends_on) > 0: + build_hierarchy(module, dep.depends_on, tree[dep.resource_name]['children']) + + if 'top' in tree: + tree.pop('top', None) + keys = list(tree.keys()) + for key1 in keys: + for key2 in keys: + if key2 in tree and key1 in tree[key2]['children']: + tree[key2]['children'][key1] = tree[key1] + tree.pop(key1) + return tree + + +class ResourceId: + def __init__(self, **kwargs): + self.resource_name = kwargs.get('resource_name') + self.resource_provider_api_version = kwargs.get('api_version') + self.resource_provider_namespace = kwargs.get('resource_namespace') + self.resource_type = kwargs.get('resource_type') + self.parent_resource_path = kwargs.get('parent_resource_path') + pass + + +def get_resource_details(client, group, name, namespace, resource_type, api_version): + res_id = ResourceId(resource_name=name, api_version=api_version, resource_namespace=namespace, + resource_type=resource_type) + return client.resources.get(group, res_id).resource + + +def get_ip_dict(ip): + p = json.loads(ip.properties) + d = p['dnsSettings'] + return dict(name=ip.name, + id=ip.id, + public_ip=p['ipAddress'], + public_ip_allocation_method=p['publicIPAllocationMethod'], + dns_settings=d) + + +def get_instances(module, client, group, deployment): + dep_tree = build_hierarchy(module, deployment.properties.dependencies) + vms = get_dependencies(dep_tree, resource_type="Microsoft.Compute/virtualMachines") + + vms_and_ips = [(vm, get_dependencies(vm['children'], "Microsoft.Network/publicIPAddresses")) for vm in vms] + vms_and_ips = [(vm['dep'], [get_resource_details(client, + group, + ip['dep'].resource_name, + "Microsoft.Network", + "publicIPAddresses", + NETWORK_API_VERSION) for ip in ip_list]) for vm, ip_list in vms_and_ips if len(ip_list) > 0] + + return [dict(vm_name=vm.resource_name, ips=[get_ip_dict(ip) for ip in ips]) for vm, ips in vms_and_ips] + + +def main(): + argument_spec = dict( + azure_url=dict(default=AZURE_URL), + subscription_id=dict(required=True), + client_secret=dict(no_log=True), + client_id=dict(), + tenant_or_domain=dict(), + security_token=dict(aliases=['access_token'], no_log=True), + resource_group_name=dict(required=True), + state=dict(default='present', choices=['present', 'absent']), + template=dict(default=None, type='dict'), + parameters=dict(default=None, type='dict'), + template_link=dict(default=None), + parameters_link=dict(default=None), + location=dict(default="West US") + ) + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=[['template_link', 'template'], ['parameters_link', 'parameters']], + ) + + if not HAS_DEPS: + module.fail_json(msg='requests and azure are required for this module') + + conn_info = get_azure_connection_info(module) + + if conn_info['security_token'] is None and \ + (conn_info['client_id'] is None or conn_info['client_secret'] is None or conn_info[ + 'tenant_or_domain'] is None): + module.fail_json(msg='security token or client_id, client_secret and tenant_or_domain is required') + + if conn_info['security_token'] is None: + conn_info['security_token'] = get_token(conn_info['tenant_or_domain'], + conn_info['client_id'], + conn_info['client_secret']) + + if conn_info['security_token'] is None: + module.fail_json(msg='failed to retrieve a security token from Azure Active Directory') + + credentials = SubscriptionCloudCredentials(module.params.get('subscription_id'), conn_info['security_token']) + resource_client = ResourceManagementClient(credentials) + conn_info['deployment_name'] = 'ansible-arm' + + if module.params.get('state') == 'present': + deployment = deploy_template(module, resource_client, conn_info) + data = dict(name=deployment.name, + group_name=conn_info['resource_group_name'], + id=deployment.id, + outputs=deployment.properties.outputs, + instances=get_instances(module, resource_client, conn_info['resource_group_name'], deployment), + changed=True, + msg='deployment created') + module.exit_json(**data) + else: + destroy_resource_group(module, resource_client, conn_info) + module.exit_json(changed=True, msg='deployment deleted') + + +# import module snippets +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From 4fb6a7e468fc8b988a7f3ab156470417f7dc5b6b Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Tue, 8 Mar 2016 12:03:10 -0800 Subject: [PATCH 1294/2522] Azure plugin using Azure Python SDK 2.0.0rc1 --- cloud/azure/azure_deployment.py | 188 +++++++++++--------------------- 1 file changed, 65 insertions(+), 123 deletions(-) diff --git a/cloud/azure/azure_deployment.py b/cloud/azure/azure_deployment.py index 4e97b0b694e..17b944f8878 100644 --- a/cloud/azure/azure_deployment.py +++ b/cloud/azure/azure_deployment.py @@ -306,18 +306,24 @@ import requests import azure from itertools import chain - from azure.mgmt.common import SubscriptionCloudCredentials - from azure.mgmt.resource import ResourceManagementClient + from azure.common.credentials import BasicTokenAuthentication + from azure.common.exceptions import CloudError + from azure.mgmt.resource.resources.models import ( + DeploymentProperties, + ParametersLink, + TemplateLink, + Deployment, + ResourceGroup, + Dependency + ) + from azure.mgmt.resource.resources import ResourceManagementClient, ResourceManagementClientConfiguration + from azure.mgmt.network import NetworkManagementClient, NetworkManagementClientConfiguration HAS_DEPS = True except ImportError: HAS_DEPS = False AZURE_URL = "https://management.azure.com" -DEPLOY_URL_FORMAT = "/subscriptions/{}/resourcegroups/{}/providers/microsoft.resources/deployments/{}?api-version={}" -RES_GROUP_URL_FORMAT = "/subscriptions/{}/resourcegroups/{}?api-version={}" -ARM_API_VERSION = "2015-01-01" -NETWORK_API_VERSION = "2015-06-15" def get_token(domain_or_tenant, client_id, client_secret): @@ -423,30 +429,6 @@ def build_deployment_body(module): return dict(properties=properties) -def follow_deployment(client, group_name, deployment): - state = deployment.properties.provisioning_state - if state == azure.mgmt.common.OperationStatus.Failed or \ - state == azure.mgmt.common.OperationStatus.Succeeded or \ - state == "Canceled" or \ - state == "Deleted": - return deployment - else: - time.sleep(30) - result = client.deployments.get(group_name, deployment.name) - return follow_deployment(client, group_name, result.deployment) - - -def follow_delete(client, location): - result = client.get_long_running_operation_status(location) - if result.status == azure.mgmt.common.OperationStatus.Succeeded: - return True - elif result.status == azure.mgmt.common.OperationStatus.Failed: - return False - else: - time.sleep(30) - return follow_delete(client, location) - - def deploy_template(module, client, conn_info): """ Deploy the targeted template and parameters @@ -459,48 +441,34 @@ def deploy_template(module, client, conn_info): deployment_name = conn_info["deployment_name"] group_name = conn_info["resource_group_name"] - deploy_parameter = azure.mgmt.resource.DeploymentProperties() - deploy_parameter.mode = "Complete" + deploy_parameter = DeploymentProperties() + deploy_parameter.mode = module.params.get('deployment_mode') if module.params.get('parameters_link') is None: - deploy_parameter.parameters = json.dumps(module.params.get('parameters'), ensure_ascii=False) + deploy_parameter.parameters = module.params.get('parameters') else: - parameters_link = azure.mgmt.resource.ParametersLink() - parameters_link.uri = module.params.get('parameters_link') + parameters_link = ParametersLink( + uri = module.params.get('parameters_link') + ) deploy_parameter.parameters_link = parameters_link if module.params.get('template_link') is None: - deploy_parameter.template = json.dumps(module.params.get('template'), ensure_ascii=False) + deploy_parameter.template = module.params.get('template') else: - template_link = azure.mgmt.resource.TemplateLink() - template_link.uri = module.params.get('template_link') + template_link = TemplateLink( + uri = module.params.get('template_link') + ) deploy_parameter.template_link = template_link - deployment = azure.mgmt.resource.Deployment(properties=deploy_parameter) - params = azure.mgmt.resource.ResourceGroup(location=module.params.get('location'), tags=module.params.get('tags')) + params = ResourceGroup(location=module.params.get('location'), tags=module.params.get('tags')) try: client.resource_groups.create_or_update(group_name, params) - result = client.deployments.create_or_update(group_name, deployment_name, deployment) - return follow_deployment(client, group_name, result.deployment) - except azure.common.AzureHttpError as e: + result = client.deployments.create_or_update(group_name, deployment_name, deploy_parameter) + return result.result() # Blocking wait, return the Deployment object + except CloudError as e: module.fail_json(msg='Deploy create failed with status code: %s and message: "%s"' % (e.status_code, e.message)) -def deploy_url(subscription_id, resource_group_name, deployment_name, api_version=ARM_API_VERSION): - return AZURE_URL + DEPLOY_URL_FORMAT.format(subscription_id, resource_group_name, deployment_name, api_version) - - -def res_group_url(subscription_id, resource_group_name, api_version=ARM_API_VERSION): - return AZURE_URL + RES_GROUP_URL_FORMAT.format(subscription_id, resource_group_name, api_version) - - -def default_headers(token, with_content=False): - headers = {'Authorization': 'Bearer {}'.format(token), 'Accept': 'application/json'} - if with_content: - headers['Content-Type'] = 'application/json' - return headers - - def destroy_resource_group(module, client, conn_info): """ Destroy the targeted resource group @@ -511,27 +479,9 @@ def destroy_resource_group(module, client, conn_info): """ try: - client.resource_groups.get(conn_info['resource_group_name']) - except azure.common.AzureMissingResourceHttpError: - return False - - try: - url = res_group_url(conn_info['subscription_id'], conn_info['resource_group_name']) - res = requests.delete(url, headers=default_headers(conn_info['security_token'])) - if res.status_code == 404 or res.status_code == 204: - return False - - if res.status_code == 202: - location = res.headers['location'] - follow_delete(client, location) - return True - - if res.status_code == requests.codes.ok: - return True - else: - module.fail_json( - msg='Delete resource group and deploy failed with status code: %s and message: %s' % (res.status_code, res.text)) - except azure.common.AzureHttpError as e: + result = client.resource_groups.delete(conn_info['resource_group_name']) + result.wait() # Blocking wait till the delete is finished + except CloudError as e: if e.status_code == 404 or e.status_code == 204: return True else: @@ -546,67 +496,55 @@ def get_dependencies(dep_tree, resource_type): return matches -def build_hierarchy(module, dependencies, tree=None): +def build_hierarchy(dependencies, tree=None): tree = dict(top=True) if tree is None else tree for dep in dependencies: if dep.resource_name not in tree: tree[dep.resource_name] = dict(dep=dep, children=dict()) - if isinstance(dep, azure.mgmt.resource.Dependency) and dep.depends_on is not None and len(dep.depends_on) > 0: - build_hierarchy(module, dep.depends_on, tree[dep.resource_name]['children']) + if isinstance(dep, Dependency) and dep.depends_on is not None and len(dep.depends_on) > 0: + build_hierarchy(dep.depends_on, tree[dep.resource_name]['children']) if 'top' in tree: tree.pop('top', None) keys = list(tree.keys()) for key1 in keys: for key2 in keys: - if key2 in tree and key1 in tree[key2]['children']: + if key2 in tree and key1 in tree[key2]['children'] and key1 in tree: tree[key2]['children'][key1] = tree[key1] tree.pop(key1) return tree -class ResourceId: - def __init__(self, **kwargs): - self.resource_name = kwargs.get('resource_name') - self.resource_provider_api_version = kwargs.get('api_version') - self.resource_provider_namespace = kwargs.get('resource_namespace') - self.resource_type = kwargs.get('resource_type') - self.parent_resource_path = kwargs.get('parent_resource_path') - pass - - -def get_resource_details(client, group, name, namespace, resource_type, api_version): - res_id = ResourceId(resource_name=name, api_version=api_version, resource_namespace=namespace, - resource_type=resource_type) - return client.resources.get(group, res_id).resource - - def get_ip_dict(ip): - p = json.loads(ip.properties) - d = p['dnsSettings'] return dict(name=ip.name, id=ip.id, - public_ip=p['ipAddress'], - public_ip_allocation_method=p['publicIPAllocationMethod'], - dns_settings=d) + public_ip=ip.ip_address, + public_ip_allocation_method=str(ip.public_ip_allocation_method), + dns_settings={ + 'domain_name_label':ip.dns_settings.domain_name_label, + 'fqdn':ip.dns_settings.fqdn + }) + +def nic_to_public_ips_instance(client, group, nics): + return [client.public_ip_addresses.get(group, public_ip_id.split('/')[-1]) + for nic_obj in [client.network_interfaces.get(group, nic['dep'].resource_name) for nic in nics] + for public_ip_id in [ip_conf_instance.public_ip_address.id for ip_conf_instance in nic_obj.ip_configurations if ip_conf_instance.public_ip_address]] -def get_instances(module, client, group, deployment): - dep_tree = build_hierarchy(module, deployment.properties.dependencies) + +def get_instances(client, group, deployment): + dep_tree = build_hierarchy(deployment.properties.dependencies) vms = get_dependencies(dep_tree, resource_type="Microsoft.Compute/virtualMachines") - vms_and_ips = [(vm, get_dependencies(vm['children'], "Microsoft.Network/publicIPAddresses")) for vm in vms] - vms_and_ips = [(vm['dep'], [get_resource_details(client, - group, - ip['dep'].resource_name, - "Microsoft.Network", - "publicIPAddresses", - NETWORK_API_VERSION) for ip in ip_list]) for vm, ip_list in vms_and_ips if len(ip_list) > 0] + vms_and_nics = [(vm, get_dependencies(vm['children'], "Microsoft.Network/networkInterfaces")) for vm in vms] + vms_and_ips = [(vm['dep'], nic_to_public_ips_instance(client, group, nics)) for vm, nics in vms_and_nics] - return [dict(vm_name=vm.resource_name, ips=[get_ip_dict(ip) for ip in ips]) for vm, ips in vms_and_ips] + return [dict(vm_name=vm.resource_name, ips=[get_ip_dict(ip) for ip in ips]) for vm, ips in vms_and_ips if len(ips) > 0] def main(): + # import module snippets + from ansible.module_utils.basic import AnsibleModule argument_spec = dict( azure_url=dict(default=AZURE_URL), subscription_id=dict(required=True), @@ -620,7 +558,9 @@ def main(): parameters=dict(default=None, type='dict'), template_link=dict(default=None), parameters_link=dict(default=None), - location=dict(default="West US") + location=dict(default="West US"), + deployment_mode=dict(default='Complete', choices=['Complete', 'Incremental']), + deployment_name=dict(default="ansible-arm") ) module = AnsibleModule( @@ -646,9 +586,15 @@ def main(): if conn_info['security_token'] is None: module.fail_json(msg='failed to retrieve a security token from Azure Active Directory') - credentials = SubscriptionCloudCredentials(module.params.get('subscription_id'), conn_info['security_token']) - resource_client = ResourceManagementClient(credentials) - conn_info['deployment_name'] = 'ansible-arm' + credentials = BasicTokenAuthentication( + token = { + 'access_token':conn_info['security_token'] + } + ) + subscription_id = module.params.get('subscription_id') + resource_client = ResourceManagementClient(ResourceManagementClientConfiguration(credentials, subscription_id)) + network_client = NetworkManagementClient(NetworkManagementClientConfiguration(credentials, subscription_id)) + conn_info['deployment_name'] = module.params.get('deployment_name') if module.params.get('state') == 'present': deployment = deploy_template(module, resource_client, conn_info) @@ -656,7 +602,7 @@ def main(): group_name=conn_info['resource_group_name'], id=deployment.id, outputs=deployment.properties.outputs, - instances=get_instances(module, resource_client, conn_info['resource_group_name'], deployment), + instances=get_instances(network_client, conn_info['resource_group_name'], deployment), changed=True, msg='deployment created') module.exit_json(**data) @@ -664,9 +610,5 @@ def main(): destroy_resource_group(module, resource_client, conn_info) module.exit_json(changed=True, msg='deployment deleted') - -# import module snippets -from ansible.module_utils.basic import * - if __name__ == '__main__': main() From 34557ee6ecf484ba46e75b381e99e74b603f25f1 Mon Sep 17 00:00:00 2001 From: nitzmahone Date: Fri, 11 Mar 2016 14:37:46 -0800 Subject: [PATCH 1295/2522] fix version_added issues in win_iis_website --- windows/win_iis_website.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/windows/win_iis_website.py b/windows/win_iis_website.py index 4bbb3afe0d5..b158fb8d8ac 100644 --- a/windows/win_iis_website.py +++ b/windows/win_iis_website.py @@ -21,7 +21,7 @@ DOCUMENTATION = ''' --- module: win_iis_website -version_added: "2.1" +version_added: "2.0" short_description: Configures a IIS Web site. description: - Creates, Removes and configures a IIS Web site @@ -36,6 +36,7 @@ description: - Explicitly set the IIS numeric ID for a site. Note that this value cannot be changed after the website has been created. required: false + version_added: "2.1" default: null state: description: From dd50d741dd7ac7b4ad23dc3996c84ea010676641 Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Fri, 11 Mar 2016 16:44:01 -0800 Subject: [PATCH 1296/2522] Add update from @obsoleted --- cloud/azure/azure_deployment.py | 64 ++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/cloud/azure/azure_deployment.py b/cloud/azure/azure_deployment.py index 17b944f8878..b0aafb1c20a 100644 --- a/cloud/azure/azure_deployment.py +++ b/cloud/azure/azure_deployment.py @@ -428,6 +428,36 @@ def build_deployment_body(module): return dict(properties=properties) +def get_failed_nested_operations(client, resource_group, current_operations): + new_operations = [] + for operation in current_operations: + if operation.properties.provisioning_state == 'Failed': + new_operations.append(operation) + if operation.properties.target_resource and 'Microsoft.Resources/deployments' in operation.properties.target_resource.id: + nested_deployment = operation.properties.target_resource.resource_name + nested_operations = client.deployment_operations.list(resource_group, nested_deployment) + new_nested_operations = get_failed_nested_operations(client, resource_group, nested_operations) + new_operations += new_nested_operations + + return new_operations + +def get_failed_deployment_operations(module, client, resource_group, deployment_name): + operations = client.deployment_operations.list(resource_group, deployment_name) + return [ + dict( + id=op.id, + operation_id=op.operation_id, + status_code=op.properties.status_code, + status_message=op.properties.status_message, + target_resource = dict( + id=op.properties.target_resource.id, + resource_name=op.properties.target_resource.resource_name, + resource_type=op.properties.target_resource.resource_type + ) if op.properties.target_resource else None, + provisioning_state=op.properties.provisioning_state, + ) + for op in get_failed_nested_operations(client, resource_group, operations) + ] def deploy_template(module, client, conn_info): """ @@ -464,11 +494,20 @@ def deploy_template(module, client, conn_info): try: client.resource_groups.create_or_update(group_name, params) result = client.deployments.create_or_update(group_name, deployment_name, deploy_parameter) - return result.result() # Blocking wait, return the Deployment object + deployment_result = result.result() # Blocking wait, return the Deployment object + if module.params.get('wait_for_deployment_completion'): + while not deployment_result.properties.provisioning_state in {'Canceled', 'Failed', 'Deleted', 'Succeeded'}: + deployment_result = client.deployments.get(group_name, deployment_name) + time.sleep(module.params.get('wait_for_deployment_polling_period')) + + if deployment_result.properties.provisioning_state == 'Succeeded': + return deployment_result + + failed_deployment_operations = get_failed_deployment_operations(module, client, group_name, deployment_name) + module.fail_json(msg='Deployment failed. Deployment id: %s' % (deployment_result.id), failed_deployment_operations=failed_deployment_operations) except CloudError as e: module.fail_json(msg='Deploy create failed with status code: %s and message: "%s"' % (e.status_code, e.message)) - def destroy_resource_group(module, client, conn_info): """ Destroy the targeted resource group @@ -516,14 +555,18 @@ def build_hierarchy(dependencies, tree=None): def get_ip_dict(ip): - return dict(name=ip.name, + ip_dict = dict(name=ip.name, id=ip.id, public_ip=ip.ip_address, - public_ip_allocation_method=str(ip.public_ip_allocation_method), - dns_settings={ + public_ip_allocation_method=str(ip.public_ip_allocation_method)) + + if ip.dns_settings: + ip_dict['dns_settings'] = { 'domain_name_label':ip.dns_settings.domain_name_label, 'fqdn':ip.dns_settings.fqdn - }) + } + + return ip_dict def nic_to_public_ips_instance(client, group, nics): @@ -547,7 +590,7 @@ def main(): from ansible.module_utils.basic import AnsibleModule argument_spec = dict( azure_url=dict(default=AZURE_URL), - subscription_id=dict(required=True), + subscription_id=dict(), client_secret=dict(no_log=True), client_id=dict(), tenant_or_domain=dict(), @@ -560,7 +603,9 @@ def main(): parameters_link=dict(default=None), location=dict(default="West US"), deployment_mode=dict(default='Complete', choices=['Complete', 'Incremental']), - deployment_name=dict(default="ansible-arm") + deployment_name=dict(default="ansible-arm"), + wait_for_deployment_completion=dict(default=True), + wait_for_deployment_polling_period=dict(default=30) ) module = AnsibleModule( @@ -591,7 +636,7 @@ def main(): 'access_token':conn_info['security_token'] } ) - subscription_id = module.params.get('subscription_id') + subscription_id = conn_info['subscription_id'] resource_client = ResourceManagementClient(ResourceManagementClientConfiguration(credentials, subscription_id)) network_client = NetworkManagementClient(NetworkManagementClientConfiguration(credentials, subscription_id)) conn_info['deployment_name'] = module.params.get('deployment_name') @@ -612,3 +657,4 @@ def main(): if __name__ == '__main__': main() + From be66e9d2971db2b5915b4807719b1fb72ec5b9fd Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Sat, 12 Mar 2016 15:55:59 -0500 Subject: [PATCH 1297/2522] add container name to return and document return fixes #1848 --- cloud/lxc/lxc_container.py | 45 +++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index 6c177552d6b..15347038c69 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -381,6 +381,48 @@ - test-container-new-archive-destroyed-clone """ +RETURN=""" +lxc_container: + description: container information + returned: success + type: list + contains: + name: + description: name of the lxc container + returned: success + type: string + sample: test_host + init_pid: + description: pid of the lxc init process + returned: success + type: int + sample: 19786 + interfaces: + description: list of the container's network interfaces + returned: success + type: list + sample: [ "eth0", "lo" ] + ips: + description: list of ips + returned: success + type: list + sample: [ "10.0.3.3" ] + state: + description: resulting state of the container + returned: success + type: string + sample: "running" + archive: + description: resulting state of the container + returned: success, when archive is true + type: string + sample: "/tmp/test-container-config.tar", + clone: + description: if the container was cloned + returned: success, when clone_name is specified + type: boolean + sample: True +""" try: import lxc @@ -880,7 +922,8 @@ def _container_data(self): 'interfaces': self.container.get_interfaces(), 'ips': self.container.get_ips(), 'state': self._get_state(), - 'init_pid': int(self.container.init_pid) + 'init_pid': int(self.container.init_pid), + 'name' : self.container_name, } def _unfreeze(self): From 040b358770379d795ef07d4e5869437d46a7d68d Mon Sep 17 00:00:00 2001 From: Ritesh Khadgaray Date: Sun, 13 Mar 2016 12:58:51 +0530 Subject: [PATCH 1298/2522] Fix test failure for lxc_container TRACE: while parsing a block mapping in "", line 33, column 13: description: resulting state of ... ^ expected , but found ',' in "lxc_container.RETURN", line 419, column 53: ... "/tmp/test-container-config.tar", ERROR: RETURN is not valid YAML. Line 419 column 53 --- cloud/lxc/lxc_container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index 15347038c69..ab207012329 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -416,7 +416,7 @@ description: resulting state of the container returned: success, when archive is true type: string - sample: "/tmp/test-container-config.tar", + sample: "/tmp/test-container-config.tar" clone: description: if the container was cloned returned: success, when clone_name is specified From 1a29807e96c9e455af75f46b8500a3ecf33ecb11 Mon Sep 17 00:00:00 2001 From: Ritesh Khadgaray Date: Mon, 14 Mar 2016 22:27:44 +0530 Subject: [PATCH 1299/2522] zabbix_host : add the ability to set inventory_mode --- monitoring/zabbix_host.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/monitoring/zabbix_host.py b/monitoring/zabbix_host.py index c75e53ee10f..0ac216fe080 100644 --- a/monitoring/zabbix_host.py +++ b/monitoring/zabbix_host.py @@ -61,6 +61,13 @@ - List of templates linked to the host. required: false default: None + inventory_mode: + description: + - Configure the inventory mode. + choices: ['automatic', 'manual', 'disabled'] + required: false + default: None + version_added: '2.1' status: description: - Monitoring status of the host. @@ -116,6 +123,7 @@ - Example template2 status: enabled state: present + inventory_mode: automatic interfaces: - type: 1 main: 1 @@ -367,6 +375,28 @@ def link_or_clear_template(self, host_id, template_id_list): except Exception, e: self._module.fail_json(msg="Failed to link template to host: %s" % e) + # Update the host inventory_mode + def update_inventory_mode(self, host_id, inventory_mode): + + # nothing was set, do nothing + if not inventory_mode: + return + + if inventory_mode == "automatic": + inventory_mode = int(1) + elif inventory_mode == "manual": + inventory_mode = int(0) + elif inventory_mode == "disabled": + inventory_mode = int(-1) + + # watch for - https://support.zabbix.com/browse/ZBX-6033 + request_str = {'hostid': host_id, 'inventory_mode': inventory_mode} + try: + if self._module.check_mode: + self._module.exit_json(changed=True) + self._zapi.host.update(request_str) + except Exception, e: + self._module.fail_json(msg="Failed to set inventory_mode to host: %s" % e) def main(): module = AnsibleModule( @@ -379,6 +409,7 @@ def main(): link_templates=dict(type='list', required=False), status=dict(default="enabled", choices=['enabled', 'disabled']), state=dict(default="present", choices=['present', 'absent']), + inventory_mode=dict(required=False, choices=['automatic', 'manual', 'disabled']), timeout=dict(type='int', default=10), interfaces=dict(type='list', required=False), force=dict(type='bool', default=True), @@ -396,6 +427,7 @@ def main(): host_name = module.params['host_name'] host_groups = module.params['host_groups'] link_templates = module.params['link_templates'] + inventory_mode = module.params['inventory_mode'] status = module.params['status'] state = module.params['state'] timeout = module.params['timeout'] @@ -479,6 +511,7 @@ def main(): exist_interfaces_copy, zabbix_host_obj, proxy_id): host.update_host(host_name, group_ids, status, host_id, interfaces, exist_interfaces, proxy_id) host.link_or_clear_template(host_id, template_ids) + host.update_inventory_mode(host_id, inventory_mode) module.exit_json(changed=True, result="Successfully update host %s (%s) and linked with template '%s'" % (host_name, ip, link_templates)) @@ -500,6 +533,7 @@ def main(): # create host host_id = host.add_host(host_name, group_ids, status, interfaces, proxy_id) host.link_or_clear_template(host_id, template_ids) + host.update_inventory_mode(host_id, inventory_mode) module.exit_json(changed=True, result="Successfully added host %s (%s) and linked with template '%s'" % ( host_name, ip, link_templates)) From a714e1d9f90a586647b49e4821ffbcdb23e7d61b Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Mon, 14 Mar 2016 10:20:36 -0700 Subject: [PATCH 1300/2522] Fix YAML documentation --- cloud/azure/azure_deployment.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/azure/azure_deployment.py b/cloud/azure/azure_deployment.py index b0aafb1c20a..8550669fa09 100644 --- a/cloud/azure/azure_deployment.py +++ b/cloud/azure/azure_deployment.py @@ -20,8 +20,8 @@ version_added: "2.0" description: - Create or destroy Azure Resource Manager template deployments via the Azure SDK for Python. - You can find some quick start templates in GitHub here: https://github.com/azure/azure-quickstart-templates. - If you would like to find out more information about Azure Resource Manager templates, see: https://azure.microsoft.com/en-us/documentation/articles/resource-group-template-deploy/. + You can find some quick start templates in GitHub here https://github.com/azure/azure-quickstart-templates. + If you would like to find out more information about Azure Resource Manager templates, see https://azure.microsoft.com/en-us/documentation/articles/resource-group-template-deploy/. options: subscription_id: description: @@ -66,7 +66,7 @@ require: false default: West US -author: "David Justice (@devigned)" +author: "David Justice (@devigned) / Laurent Mazuel (@lmazuel) / Andre Price (@obsoleted)" ''' EXAMPLES = ''' From 36b9fe9ac9b7614369932e3dd4a47155ca97735e Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Mon, 14 Mar 2016 10:52:45 -0700 Subject: [PATCH 1301/2522] Py2.6 compatible + minor fixes --- cloud/azure/azure_deployment.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cloud/azure/azure_deployment.py b/cloud/azure/azure_deployment.py index 8550669fa09..776d5c2286f 100644 --- a/cloud/azure/azure_deployment.py +++ b/cloud/azure/azure_deployment.py @@ -17,7 +17,7 @@ --- module: azure_deployment short_description: Create or destroy Azure Resource Manager template deployments -version_added: "2.0" +version_added: "2.1" description: - Create or destroy Azure Resource Manager template deployments via the Azure SDK for Python. You can find some quick start templates in GitHub here https://github.com/azure/azure-quickstart-templates. @@ -496,7 +496,7 @@ def deploy_template(module, client, conn_info): result = client.deployments.create_or_update(group_name, deployment_name, deploy_parameter) deployment_result = result.result() # Blocking wait, return the Deployment object if module.params.get('wait_for_deployment_completion'): - while not deployment_result.properties.provisioning_state in {'Canceled', 'Failed', 'Deleted', 'Succeeded'}: + while not deployment_result.properties.provisioning_state in ['Canceled', 'Failed', 'Deleted', 'Succeeded']: deployment_result = client.deployments.get(group_name, deployment_name) time.sleep(module.params.get('wait_for_deployment_polling_period')) @@ -585,9 +585,10 @@ def get_instances(client, group, deployment): return [dict(vm_name=vm.resource_name, ips=[get_ip_dict(ip) for ip in ips]) for vm, ips in vms_and_ips if len(ips) > 0] +# import module snippets +from ansible.module_utils.basic import AnsibleModule + def main(): - # import module snippets - from ansible.module_utils.basic import AnsibleModule argument_spec = dict( azure_url=dict(default=AZURE_URL), subscription_id=dict(), From 027ae690c4362327c6d7d8f4d92b67dd84d332c5 Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Mon, 14 Mar 2016 11:01:30 -0700 Subject: [PATCH 1302/2522] Fixes after Travis feedback --- cloud/azure/__init__.py | 0 cloud/azure/azure_deployment.py | 9 ++++++--- 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 cloud/azure/__init__.py diff --git a/cloud/azure/__init__.py b/cloud/azure/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cloud/azure/azure_deployment.py b/cloud/azure/azure_deployment.py index 776d5c2286f..c7b9f2ccea1 100644 --- a/cloud/azure/azure_deployment.py +++ b/cloud/azure/azure_deployment.py @@ -300,6 +300,9 @@ value: devopscleazure ''' +RETURN = ''' +''' + try: import time import yaml @@ -585,9 +588,6 @@ def get_instances(client, group, deployment): return [dict(vm_name=vm.resource_name, ips=[get_ip_dict(ip) for ip in ips]) for vm, ips in vms_and_ips if len(ips) > 0] -# import module snippets -from ansible.module_utils.basic import AnsibleModule - def main(): argument_spec = dict( azure_url=dict(default=AZURE_URL), @@ -656,6 +656,9 @@ def main(): destroy_resource_group(module, resource_client, conn_info) module.exit_json(changed=True, msg='deployment deleted') +# import module snippets +from ansible.module_utils.basic import * + if __name__ == '__main__': main() From 112355d96550f671521f64e1edba8299046fe02c Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Mon, 14 Mar 2016 11:22:33 -0700 Subject: [PATCH 1303/2522] Updated to use ServicePrincipalCredentials class and get rid of requests --- cloud/azure/azure_deployment.py | 72 ++++++--------------------------- 1 file changed, 12 insertions(+), 60 deletions(-) diff --git a/cloud/azure/azure_deployment.py b/cloud/azure/azure_deployment.py index c7b9f2ccea1..24e2c3fbe09 100644 --- a/cloud/azure/azure_deployment.py +++ b/cloud/azure/azure_deployment.py @@ -306,10 +306,8 @@ try: import time import yaml - import requests - import azure from itertools import chain - from azure.common.credentials import BasicTokenAuthentication + from azure.common.credentials import ServicePrincipalCredentials from azure.common.exceptions import CloudError from azure.mgmt.resource.resources.models import ( DeploymentProperties, @@ -329,35 +327,11 @@ AZURE_URL = "https://management.azure.com" -def get_token(domain_or_tenant, client_id, client_secret): - """ - Get an Azure Active Directory token for a service principal - :param domain_or_tenant: The domain or tenant id of your Azure Active Directory instance - :param client_id: The client id of your application in Azure Active Directory - :param client_secret: One of the application secrets created in your Azure Active Directory application - :return: an authenticated bearer token to be used with requests to the API - """ - # the client id we can borrow from azure xplat cli - grant_type = 'client_credentials' - token_url = 'https://login.microsoftonline.com/{}/oauth2/token'.format(domain_or_tenant) - - payload = { - 'grant_type': grant_type, - 'client_id': client_id, - 'client_secret': client_secret, - 'resource': 'https://management.core.windows.net/' - } - - res = requests.post(token_url, data=payload) - return res.json()['access_token'] if res.status_code == 200 else None - - def get_azure_connection_info(module): azure_url = module.params.get('azure_url') - tenant_or_domain = module.params.get('tenant_or_domain') + tenant_id = module.params.get('tenant_id') client_id = module.params.get('client_id') client_secret = module.params.get('client_secret') - security_token = module.params.get('security_token') resource_group_name = module.params.get('resource_group_name') subscription_id = module.params.get('subscription_id') @@ -379,19 +353,13 @@ def get_azure_connection_info(module): else: resource_group_name = None - if not security_token: - if 'AZURE_SECURITY_TOKEN' in os.environ: - security_token = os.environ['AZURE_SECURITY_TOKEN'] - else: - security_token = None - - if not tenant_or_domain: + if not tenant_id: if 'AZURE_TENANT_ID' in os.environ: - tenant_or_domain = os.environ['AZURE_TENANT_ID'] + tenant_id = os.environ['AZURE_TENANT_ID'] elif 'AZURE_DOMAIN' in os.environ: - tenant_or_domain = os.environ['AZURE_DOMAIN'] + tenant_id = os.environ['AZURE_DOMAIN'] else: - tenant_or_domain = None + tenant_id = None if not client_id: if 'AZURE_CLIENT_ID' in os.environ: @@ -406,10 +374,9 @@ def get_azure_connection_info(module): client_secret = None return dict(azure_url=azure_url, - tenant_or_domain=tenant_or_domain, + tenant_id=tenant_id, client_id=client_id, client_secret=client_secret, - security_token=security_token, resource_group_name=resource_group_name, subscription_id=subscription_id) @@ -593,9 +560,8 @@ def main(): azure_url=dict(default=AZURE_URL), subscription_id=dict(), client_secret=dict(no_log=True), - client_id=dict(), - tenant_or_domain=dict(), - security_token=dict(aliases=['access_token'], no_log=True), + client_id=dict(required=True), + tenant_id=dict(required=True), resource_group_name=dict(required=True), state=dict(default='present', choices=['present', 'absent']), template=dict(default=None, type='dict'), @@ -619,24 +585,10 @@ def main(): conn_info = get_azure_connection_info(module) - if conn_info['security_token'] is None and \ - (conn_info['client_id'] is None or conn_info['client_secret'] is None or conn_info[ - 'tenant_or_domain'] is None): - module.fail_json(msg='security token or client_id, client_secret and tenant_or_domain is required') - - if conn_info['security_token'] is None: - conn_info['security_token'] = get_token(conn_info['tenant_or_domain'], - conn_info['client_id'], - conn_info['client_secret']) + credentials = ServicePrincipalCredentials(client_id=conn_info['client_id'], + secret=conn_info['client_secret'], + tenant=conn_info['tenant_id']) - if conn_info['security_token'] is None: - module.fail_json(msg='failed to retrieve a security token from Azure Active Directory') - - credentials = BasicTokenAuthentication( - token = { - 'access_token':conn_info['security_token'] - } - ) subscription_id = conn_info['subscription_id'] resource_client = ResourceManagementClient(ResourceManagementClientConfiguration(credentials, subscription_id)) network_client = NetworkManagementClient(NetworkManagementClientConfiguration(credentials, subscription_id)) From c69c0f8fd7999d788835a27f420767047fea00fe Mon Sep 17 00:00:00 2001 From: Andre Price Date: Mon, 14 Mar 2016 12:45:06 -0700 Subject: [PATCH 1304/2522] Get failed deployments when create fails also --- cloud/azure/azure_deployment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/azure/azure_deployment.py b/cloud/azure/azure_deployment.py index 24e2c3fbe09..79875581d04 100644 --- a/cloud/azure/azure_deployment.py +++ b/cloud/azure/azure_deployment.py @@ -476,7 +476,8 @@ def deploy_template(module, client, conn_info): failed_deployment_operations = get_failed_deployment_operations(module, client, group_name, deployment_name) module.fail_json(msg='Deployment failed. Deployment id: %s' % (deployment_result.id), failed_deployment_operations=failed_deployment_operations) except CloudError as e: - module.fail_json(msg='Deploy create failed with status code: %s and message: "%s"' % (e.status_code, e.message)) + failed_deployment_operations = get_failed_deployment_operations(module, client, group_name, deployment_name) + module.fail_json(msg='Deploy create failed with status code: %s and message: "%s"' % (e.status_code, e.message),failed_deployment_operations=failed_deployment_operations) def destroy_resource_group(module, client, conn_info): """ @@ -613,4 +614,3 @@ def main(): if __name__ == '__main__': main() - From f46e19371823bd158dd361441608f036e00cfadf Mon Sep 17 00:00:00 2001 From: "t.goto" Date: Tue, 15 Mar 2016 16:12:56 +0900 Subject: [PATCH 1305/2522] change host.delete() parameter for newer ZBX api. As of Zabbix API 2.4, host.delete() will not takes parameter with `hostid` property but only the array of it. https://www.zabbix.com/documentation/2.2/manual/api/reference/host/delete fix #1800 --- monitoring/zabbix_host.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/monitoring/zabbix_host.py b/monitoring/zabbix_host.py index c75e53ee10f..f8ca93a6c45 100644 --- a/monitoring/zabbix_host.py +++ b/monitoring/zabbix_host.py @@ -26,7 +26,7 @@ description: - This module allows you to create, modify and delete Zabbix host entries and associated group and template data. version_added: "2.0" -author: +author: - "(@cove)" - "Tony Minfei Ding" - "Harrison Gu (@harrisongu)" @@ -240,7 +240,7 @@ def delete_host(self, host_id, host_name): try: if self._module.check_mode: self._module.exit_json(changed=True) - self._zapi.host.delete({'hostid': host_id}) + self._zapi.host.delete([host_id]) except Exception, e: self._module.fail_json(msg="Failed to delete host %s: %s" % (host_name, e)) @@ -440,7 +440,7 @@ def main(): proxy_id = host.get_proxyid_by_proxy_name(proxy) else: proxy_id = None - + # get host id by host name zabbix_host_obj = host.get_host_by_host_name(host_name) host_id = zabbix_host_obj['hostid'] @@ -505,4 +505,3 @@ def main(): from ansible.module_utils.basic import * main() - From c19a721765fa86570ca7c689f99a2f83e1fdc624 Mon Sep 17 00:00:00 2001 From: "t.goto" Date: Tue, 15 Mar 2016 16:19:08 +0900 Subject: [PATCH 1306/2522] add exit_json add exit_json code to succesfully exit, when you want to delete the already deleted host. Without this, playbook fails with `Specify at least one group for creating host` which is not correct message. --- monitoring/zabbix_host.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/monitoring/zabbix_host.py b/monitoring/zabbix_host.py index f8ca93a6c45..f3bc5e37527 100644 --- a/monitoring/zabbix_host.py +++ b/monitoring/zabbix_host.py @@ -485,6 +485,10 @@ def main(): else: module.exit_json(changed=False) else: + if state == "absent": + # the host is already deleted. + module.exit_json(changed=False) + # Use proxy specified, or set to 0 when adding new host if proxy: proxy_id = host.get_proxyid_by_proxy_name(proxy) From a0ee1f37d1976fcf1e12fce7b5e116adf0e14704 Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Tue, 15 Mar 2016 16:13:13 -0700 Subject: [PATCH 1307/2522] Add user-agent information for analytics --- cloud/azure/azure_deployment.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cloud/azure/azure_deployment.py b/cloud/azure/azure_deployment.py index 79875581d04..e28663b55f6 100644 --- a/cloud/azure/azure_deployment.py +++ b/cloud/azure/azure_deployment.py @@ -591,8 +591,12 @@ def main(): tenant=conn_info['tenant_id']) subscription_id = conn_info['subscription_id'] - resource_client = ResourceManagementClient(ResourceManagementClientConfiguration(credentials, subscription_id)) - network_client = NetworkManagementClient(NetworkManagementClientConfiguration(credentials, subscription_id)) + resource_configuration = ResourceManagementClientConfiguration(credentials, subscription_id) + resource_configuration.add_user_agent('Ansible-Deploy') + resource_client = ResourceManagementClient(resource_configuration) + network_configuration = NetworkManagementClientConfiguration(credentials, subscription_id) + network_configuration.add_user_agent('Ansible-Deploy') + network_client = NetworkManagementClient(network_configuration) conn_info['deployment_name'] = module.params.get('deployment_name') if module.params.get('state') == 'present': From f9b96b9a8add347679044dd9f2737a8721cdf7f3 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Tue, 15 Mar 2016 17:44:29 -0700 Subject: [PATCH 1308/2522] Fix module docs --- windows/win_file_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_file_version.py b/windows/win_file_version.py index 4f23c55053a..4f2ecc0d615 100644 --- a/windows/win_file_version.py +++ b/windows/win_file_version.py @@ -21,7 +21,7 @@ --- module: win_file_version version_added: "2.1" -short_descriptions: Get DLL or EXE file build version +short_description: Get DLL or EXE file build version description: - Get DLL or EXE file build version - change state alway be false From cd3daf5763910faf8e83169e74254daf986f3a83 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Wed, 16 Mar 2016 19:06:56 +0100 Subject: [PATCH 1309/2522] iptables: add defaults to docs --- system/iptables.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/system/iptables.py b/system/iptables.py index 872886f510d..6d2214a3105 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -79,6 +79,7 @@ match with all protocols and is taken as default when this option is omitted. required: false + default: null source: description: - Source specification. Address can be either a network name, @@ -99,6 +100,7 @@ Thus, a mask of 24 is equivalent to 255.255.255.0. A "!" argument before the address specification inverts the sense of the address. required: false + default: null destination: description: - Destination specification. Address can be either a network name, @@ -119,6 +121,7 @@ Thus, a mask of 24 is equivalent to 255.255.255.0. A "!" argument before the address specification inverts the sense of the address. required: false + default: null match: description: - Specifies a match to use, that is, an extension module that tests for @@ -127,6 +130,7 @@ specified as an array and work in short-circuit fashion, i.e. if one extension yields false, evaluation will stop. required: false + default: [] jump: description: - This specifies the target of the rule; i.e., what to do if the packet @@ -137,12 +141,14 @@ is not used), then matching the rule will have no effect on the packet's fate, but the counters on the rule will be incremented. required: false + default: null goto: description: - This specifies that the processing should continue in a user specified chain. Unlike the jump argument return will not continue processing in this chain but instead in the chain that called us via jump. required: false + default: null in_interface: description: - Name of an interface via which a packet was received (only for packets @@ -152,6 +158,7 @@ this name will match. If this option is omitted, any interface name will match. required: false + default: null out_interface: description: - Name of an interface via which a packet is going to be sent (for @@ -161,6 +168,7 @@ with this name will match. If this option is omitted, any interface name will match. required: false + default: null fragment: description: - This means that the rule only refers to second and further fragments @@ -170,11 +178,13 @@ fragment argument, the rule will only match head fragments, or unfragmented packets. required: false + default: null set_counters: description: - This enables the administrator to initialize the packet and byte counters of a rule (during INSERT, APPEND, REPLACE operations). required: false + default: null source_port: description: - "Source port or port range specification. This can either be a service @@ -183,6 +193,7 @@ if the last is omitted, '65535' is assumed. If the first port is greater than the second one they will be swapped." required: false + default: null destination_port: description: - "Destination port or port range specification. This can either be @@ -191,6 +202,7 @@ '0' is assumed; if the last is omitted, '65535' is assumed. If the first port is greater than the second one they will be swapped." required: false + default: null to_ports: description: - "This specifies a destination port or range of ports to use: without @@ -198,6 +210,7 @@ rule also specifies one of the following protocols: tcp, udp, dccp or sctp." required: false + default: null set_dscp_mark: version_added: "2.1" description: @@ -205,30 +218,37 @@ It takes either an integer or hex value. Mutually exclusive with C(dscp_mark_class)." required: false + default: null set_dscp_mark_class: version_added: "2.1" description: - "This allows specifying a predefined DiffServ class which will be translated to the corresponding DSCP mark. Mutually exclusive with C(dscp_mark)." + required: false + default: null comment: description: - "This specifies a comment that will be added to the rule" required: false + default: null ctstate: description: - "ctstate is a list of the connection states to match in the conntrack module. Possible states are: 'INVALID', 'NEW', 'ESTABLISHED', 'RELATED', 'UNTRACKED', 'SNAT', 'DNAT'" required: false + default: [] limit: description: - "Specifies the maximum average number of matches to allow per second. The number can specify units explicitly, using `/second', `/minute', `/hour' or `/day', or parts of them (so `5/second' is the same as `5/s')." required: false + default: null limit_burst: version_added: "2.1" description: - "Specifies the maximum burst before the above limit kicks in." required: false + default: null ''' EXAMPLES = ''' From 5ff957322a66dcae902aace216d9eb06d9f91d8a Mon Sep 17 00:00:00 2001 From: Marcos Diez Date: Wed, 16 Mar 2016 21:59:24 +0200 Subject: [PATCH 1310/2522] mongodb_user: fix checking if the roles of an oplog reader user changed --- database/misc/mongodb_user.py | 39 ++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index e58857586eb..bc636f52397 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -193,6 +193,43 @@ def load_mongocnf(): return creds + + +def check_if_roles_changed(uinfo, roles, db_name): +# The reason for such complicated method is a user which can read the oplog on a replicaset +# This user must have access to the local DB, but since this DB does not have users +# and is not synchronized among replica sets, the user must be stored on the admin db +# { +# "_id" : "admin.oplog_reader", +# "user" : "oplog_reader", +# "db" : "admin", +# "roles" : [ +# { +# "role" : "read", +# "db" : "local" +# } +# ] +# } + + def make_sure_roles_are_a_list_of_dict(roles, db_name): + output = list() + for role in roles: + if isinstance(role, basestring): + new_role = { "role": role, "db": db_name } + output.append(new_role) + else: + output.append(role) + return output + + roles_as_list_of_dict = make_sure_roles_are_a_list_of_dict(roles, db_name) + uinfo_roles = uinfo.get('roles', []) + + if sorted(roles_as_list_of_dict) == sorted(uinfo_roles): + return False + return True + + + # ========================================= # Module execution. # @@ -265,7 +302,7 @@ def main(): uinfo = user_find(client, user, db_name) if update_password != 'always' and uinfo: password = None - if list(map((lambda x: x['role']), uinfo.get('roles', []))) == roles: + if not check_if_roles_changed(uinfo, roles, db_name): module.exit_json(changed=False, user=user) try: From 7ec5209e18439fd984e1b4705e83dc3a2509185c Mon Sep 17 00:00:00 2001 From: Marcos Diez Date: Wed, 16 Mar 2016 22:07:58 +0200 Subject: [PATCH 1311/2522] mongodb_user.py: changes on comments --- database/misc/mongodb_user.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index bc636f52397..829dcb7e6b5 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -196,17 +196,18 @@ def load_mongocnf(): def check_if_roles_changed(uinfo, roles, db_name): -# The reason for such complicated method is a user which can read the oplog on a replicaset -# This user must have access to the local DB, but since this DB does not have users +# We must be aware of users which can read the oplog on a replicaset +# Such users must have access to the local DB, but since this DB does not store users credentials # and is not synchronized among replica sets, the user must be stored on the admin db +# Therefore their structure is the following : # { # "_id" : "admin.oplog_reader", # "user" : "oplog_reader", -# "db" : "admin", +# "db" : "admin", # <-- admin DB # "roles" : [ # { # "role" : "read", -# "db" : "local" +# "db" : "local" # <-- local DB # } # ] # } From c289aa4cb94feb5ebab34413476e33371ed80b64 Mon Sep 17 00:00:00 2001 From: Emilien Macchi Date: Tue, 9 Feb 2016 09:30:22 -0500 Subject: [PATCH 1312/2522] system/puppet: allow to run puppet -e -e or --execute [1] allows to execute a specific piece of Puppet code such a class. For example, in puppet you would run: puppet apply -e 'include ::mymodule' Will be in ansible: puppet: execute='include ::mymodule' [1] http://docs.puppetlabs.com/puppet/latest/reference/man/apply.html#OPTIONS --- system/puppet.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/system/puppet.py b/system/puppet.py index 4fba59a1862..0b3210f542a 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -80,6 +80,13 @@ required: false default: None version_added: "2.1" + execute: + description: + - Execute a specific piece of Puppet code. It has no effect with + a puppetmaster. + required: false + default: None + version_added: "2.1" requirements: [ puppet ] author: "Monty Taylor (@emonty)" ''' @@ -96,6 +103,9 @@ # Run puppet using a specific certname - puppet: certname=agent01.example.com +# Run puppet using a specific piece of Puppet code. Has no effect with a +# puppetmaster. +- puppet: execute='include ::mymodule' ''' @@ -137,10 +147,12 @@ def main(): facter_basename=dict(default='ansible'), environment=dict(required=False, default=None), certname=dict(required=False, default=None), + execute=dict(required=False, default=None), ), supports_check_mode=True, mutually_exclusive=[ ('puppetmaster', 'manifest'), + ('puppetmaster', 'manifest', 'execute'), ], ) p = module.params @@ -213,6 +225,8 @@ def main(): cmd += "--environment '%s' " % p['environment'] if p['certname']: cmd += " --certname='%s'" % p['certname'] + if p['execute']: + cmd += " --execute '%s'" % p['execute'] if module.check_mode: cmd += "--noop " else: From 6b2bc9df04b66c67c42c67c19df96af9b47b0bde Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Thu, 17 Mar 2016 18:07:47 +0100 Subject: [PATCH 1313/2522] Fix #1809, use the proper method to fail --- system/firewalld.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/firewalld.py b/system/firewalld.py index 771d178d9d5..2ebd8b91555 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -260,7 +260,7 @@ def main(): supports_check_mode=True ) if module.params['source'] == None and module.params['permanent'] == None: - module.fail(msg='permanent is a required parameter') + module.fail_json(msg='permanent is a required parameter') if not HAS_FIREWALLD: module.fail_json(msg='firewalld and its python 2 module are required for this module') From 9acc1410827ea784ecea31726ae10018f8ba9ed5 Mon Sep 17 00:00:00 2001 From: Dennis Conrad Date: Thu, 17 Mar 2016 17:08:40 +0000 Subject: [PATCH 1314/2522] Fix for existing ENIs w/ multiple security groups Do a sorted comparison of the list of security groups supplied via `module.params.get('security_groups')` and the list of security groups fetched via `get_sec_group_list(eni.groups)`. This fixes an incorrect "The specified address is already in use" error if the order of security groups in those lists differ. --- cloud/amazon/ec2_eni.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_eni.py b/cloud/amazon/ec2_eni.py index 5a6bd1f1b4d..30d05f85554 100644 --- a/cloud/amazon/ec2_eni.py +++ b/cloud/amazon/ec2_eni.py @@ -324,7 +324,7 @@ def compare_eni(connection, module): for eni in all_eni: remote_security_groups = get_sec_group_list(eni.groups) - if (eni.subnet_id == subnet_id) and (eni.private_ip_address == private_ip_address) and (eni.description == description) and (remote_security_groups == security_groups): + if (eni.subnet_id == subnet_id) and (eni.private_ip_address == private_ip_address) and (eni.description == description) and (sorted(remote_security_groups) == sorted(security_groups)): return eni except BotoServerError as e: From 2f0edbeccb09b4099a305f765e738fa49a7cf7a1 Mon Sep 17 00:00:00 2001 From: Paul Seiffert Date: Thu, 17 Mar 2016 20:08:55 +0100 Subject: [PATCH 1315/2522] Allow Datadog metric alerts to define multiple thresholds --- monitoring/datadog_monitor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py index 71a17e18223..8dbbe646638 100644 --- a/monitoring/datadog_monitor.py +++ b/monitoring/datadog_monitor.py @@ -95,7 +95,7 @@ required: false default: False thresholds: - description: ["A dictionary of thresholds by status. Because service checks can have multiple thresholds, we don't define them directly in the query."] + description: ["A dictionary of thresholds by status. This option is only available for service checks and metric alerts. Because each of them can have multiple thresholds, we don't define them directly in the query."] required: false default: {'ok': 1, 'critical': 1, 'warning': 1} ''' @@ -152,7 +152,7 @@ def main(): renotify_interval=dict(required=False, default=None), escalation_message=dict(required=False, default=None), notify_audit=dict(required=False, default=False, type='bool'), - thresholds=dict(required=False, type='dict', default={'ok': 1, 'critical': 1, 'warning': 1}), + thresholds=dict(required=False, type='dict', default=None), ) ) @@ -228,6 +228,8 @@ def install_monitor(module): } if module.params['type'] == "service check": + options["thresholds"] = module.params['thresholds'] or {'ok': 1, 'critical': 1, 'warning': 1} + if module.params['type'] == "metric alert" and module.params['thresholds'] is not None: options["thresholds"] = module.params['thresholds'] monitor = _get_monitor(module) From 0a9ac470df49cfee5365bdc35c30fe976611ce91 Mon Sep 17 00:00:00 2001 From: Alex Kalinin Date: Thu, 17 Mar 2016 17:00:00 -0700 Subject: [PATCH 1316/2522] Fix vmware_portgroup throwing an error if port group already exists --- cloud/vmware/vmware_portgroup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cloud/vmware/vmware_portgroup.py b/cloud/vmware/vmware_portgroup.py index 30e1e212617..591aa9240d0 100644 --- a/cloud/vmware/vmware_portgroup.py +++ b/cloud/vmware/vmware_portgroup.py @@ -106,6 +106,9 @@ def main(): raise SystemExit("Unable to locate Physical Host.") host_system = host.keys()[0] + if find_host_portgroup_by_name(host_system, portgroup_name): + module.exit_json(changed=False) + changed = create_port_group(host_system, portgroup_name, vlan_id, switch_name) module.exit_json(changed=changed) From 33e1d9d1cb41a4dacaf9c649d6889ae6ac0afcbf Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Fri, 18 Mar 2016 09:05:20 -0700 Subject: [PATCH 1317/2522] Doc fixes --- cloud/openstack/os_flavor_facts.py | 8 ++++---- files/blockinfile.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cloud/openstack/os_flavor_facts.py b/cloud/openstack/os_flavor_facts.py index e4c65f8ff23..3795abd08d0 100644 --- a/cloud/openstack/os_flavor_facts.py +++ b/cloud/openstack/os_flavor_facts.py @@ -54,12 +54,12 @@ - "A string used for filtering flavors based on the amount of RAM (in MB) desired. This string accepts the following special values: 'MIN' (return flavors with the minimum amount of RAM), and 'MAX' - (return flavors with the maximum amount of RAM). + (return flavors with the maximum amount of RAM)." - A specific amount of RAM may also be specified. Any flavors with this - exact amount of RAM will be returned. + - "A specific amount of RAM may also be specified. Any flavors with this + exact amount of RAM will be returned." - A range of acceptable RAM may be given using a special syntax. Simply + - "A range of acceptable RAM may be given using a special syntax. Simply prefix the amount of RAM with one of these acceptable range values: '<', '>', '<=', '>='. These values represent less than, greater than, less than or equal to, and greater than or equal to, respectively." diff --git a/files/blockinfile.py b/files/blockinfile.py index c2e449b2edc..8e54ae50ffd 100644 --- a/files/blockinfile.py +++ b/files/blockinfile.py @@ -38,7 +38,7 @@ surrounded by customizable marker lines. notes: - This module supports check mode. - - When using 'with_' loops be aware that if you do not set a unique mark the block will be overwritten on each iteration. + - When using 'with_*' loops be aware that if you do not set a unique mark the block will be overwritten on each iteration. options: dest: aliases: [ name, destfile ] From 9f9615f62a66379d496e7b8eb55283cde89a41b6 Mon Sep 17 00:00:00 2001 From: nonshankus Date: Fri, 18 Mar 2016 19:07:54 +0100 Subject: [PATCH 1318/2522] Fixing win_updates example for listing available updates. --- windows/win_updates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_updates.py b/windows/win_updates.py index 84d7d54c311..efdd1146add 100644 --- a/windows/win_updates.py +++ b/windows/win_updates.py @@ -77,7 +77,7 @@ win_updates: category_names=SecurityUpdates # Search-only, return list of found updates (if any), log to c:\ansible_wu.txt - win_updates: category_names=SecurityUpdates status=searched log_path=c:/ansible_wu.txt + win_updates: category_names=SecurityUpdates state=searched log_path=c:/ansible_wu.txt ''' RETURN = ''' From 8b63aea89b850cb4e60951136a7edf3a6c475c88 Mon Sep 17 00:00:00 2001 From: Michael Gruener Date: Tue, 1 Mar 2016 22:32:19 +0100 Subject: [PATCH 1319/2522] Module to manage Cloudflare DNS records --- network/cloudflare_dns.py | 536 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 536 insertions(+) create mode 100644 network/cloudflare_dns.py diff --git a/network/cloudflare_dns.py b/network/cloudflare_dns.py new file mode 100644 index 00000000000..7b380450457 --- /dev/null +++ b/network/cloudflare_dns.py @@ -0,0 +1,536 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016 Michael Gruener +# +# This file is (intends to be) part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import json +import urllib + +DOCUMENTATION = ''' +--- +module: cloudflare_dns +author: "Michael Gruener (@mgruener)" +version_added: "2.1" +short_description: manage Cloudflare DNS records +description: + - "Manages dns records via the Cloudflare API, see the docs: U(https://api.cloudflare.com/)" +options: + account_api_token: + description: + - "Account API token. You can obtain your API key from the bottom of the Cloudflare 'My Account' page, found here: U(https://www.cloudflare.com/a/account)" + required: true + account_email: + description: + - "Account email." + required: true + port: + description: Service port. Required for C(type=SRV) + required: false + default: null + priority: + description: Record priority. Required for C(type=MX) and C(type=SRV) + required: false + default: "1" + proto: + description: Service protocol. Required for C(type=SRV) + required: false + choices: [ 'tcp', 'udp' ] + default: null + record: + description: + - Record to add. Required if C(state=present). Default is C(@) (e.g. the zone name) + required: false + default: "@" + aliases: [ "name" ] + service: + description: Record service. Required for C(type=SRV) + required: false + default: null + solo: + description: + - Whether the record should be the only one for that record type and record name. Only use with C(state=present) + - This will delete all other records with the same record name and type. + required: false + default: null + state: + description: + - Whether the record(s) should exist or not + required: false + choices: [ 'present', 'absent' ] + default: present + timeout: + description: + - Timeout for Cloudflare API calls + required: false + default: 30 + ttl: + description: + - The TTL to give the new record. Min 1 (automatic), max 2147483647 + required: false + default: 1 (automatic) + type: + description: + - The type of DNS record to create. Required if C(state=present) + required: false + choices: [ 'A', 'AAAA', 'CNAME', 'TXT', 'SRV', 'MX', 'NS', 'SPF' ] + default: null + value: + description: + - The record value. Required for C(state=present) + required: false + default: null + aliases: [ "content" ] + weight: + description: Service weight. Required for C(type=SRV) + required: false + default: "1" + zone: + description: + - The name of the Zone to work with (e.g. "example.com"). The Zone must already exist. + required: true + aliases: ["domain"] +''' + +EXAMPLES = ''' +# create a test.my.com A record to point to 127.0.0.01 +- cloudflare_dns: + zone: my.com + record: test + type: A + value: 127.0.0.1 + account_email: test@example.com + account_api_token: dummyapitoken + register: record + +# create a my.com CNAME record to example.com +- cloudflare_dns: + zone: my.com + type: CNAME + value: example.com + state: present + account_email: test@example.com + account_api_token: dummyapitoken + +# change it's ttl +- cloudflare_dns: + zone: my.com + type: CNAME + value: example.com + ttl: 600 + state: present + account_email: test@example.com + account_api_token: dummyapitoken + +# and delete the record +- cloudflare_dns: + zone: my.com + type: CNAME + value: example.com + state: absent + account_email: test@example.com + account_api_token: dummyapitoken + +# create TXT record "test.my.com" with value "unique value" +# delete all other TXT records named "test.my.com" +- cloudflare_dns: + domain: my.com + record: test + type: TXT + value: unique value + state: present + solo: true + account_email: test@example.com + account_api_token: dummyapitoken + +# create a SRV record _foo._tcp.my.com +- cloudflare_dns: + domain: my.com + service: foo + proto: tcp + port: 3500 + priority: 10 + weight: 20 + type: SRV + value: fooserver.my.com +''' + +RETURN = ''' +records: + description: > + List containing the records for a zone or the data for a newly created record. + For details see https://api.cloudflare.com/#dns-records-for-a-zone-properties. + returned: success/changed after record creation + type: list +''' + +class CloudflareAPI(object): + + cf_api_endpoint = 'https://api.cloudflare.com/client/v4' + changed = False + + def __init__(self, module): + self.module = module + self.account_api_token = module.params['account_api_token'] + self.account_email = module.params['account_email'] + self.port = module.params['port'] + self.priority = module.params['priority'] + self.proto = module.params['proto'] + self.record = module.params['record'] + self.service = module.params['service'] + self.is_solo = module.params['solo'] + self.state = module.params['state'] + self.timeout = module.params['timeout'] + self.ttl = module.params['ttl'] + self.type = module.params['type'] + self.value = module.params['value'] + self.weight = module.params['weight'] + self.zone = module.params['zone'] + + if self.record == '@': + self.record = self.zone + + if (self.type in ['CNAME','NS','MX','SRV']) and (self.value is not None): + self.value = self.value.rstrip('.') + + if (self.type == 'SRV'): + if (self.proto is not None) and (not self.proto.startswith('_')): + self.proto = '_' + self.proto + if (self.service is not None) and (not self.service.startswith('_')): + self.service = '_' + self.service + + if not self.record.endswith(self.zone): + self.record = self.record + '.' + self.zone + + def _cf_simple_api_call(self,api_call,method='GET',payload=None): + headers = { 'X-Auth-Email': self.account_email, + 'X-Auth-Key': self.account_api_token, + 'Content-Type': 'application/json' } + data = None + if payload: + try: + data = json.dumps(payload) + except Exception, e: + self.module.fail_json(msg="Failed to encode payload as JSON: {0}".format(e)) + + resp, info = fetch_url(self.module, + self.cf_api_endpoint + api_call, + headers=headers, + data=data, + method=method, + timeout=self.timeout) + + if info['status'] not in [200,304,400,401,403,429,405,415]: + self.module.fail_json(msg="Failed API call {0}; got unexpected HTTP code {1}".format(api_call,info['status'])) + + error_msg = '' + if info['status'] == 401: + # Unauthorized + error_msg = "API user does not have permission; Status: {0}; Method: {1}: Call: {2}".format(info['status'],method,api_call) + elif info['status'] == 403: + # Forbidden + error_msg = "API request not authenticated; Status: {0}; Method: {1}: Call: {2}".format(info['status'],method,api_call) + elif info['status'] == 429: + # Too many requests + error_msg = "API client is rate limited; Status: {0}; Method: {1}: Call: {2}".format(info['status'],method,api_call) + elif info['status'] == 405: + # Method not allowed + error_msg = "API incorrect HTTP method provided; Status: {0}; Method: {1}: Call: {2}".format(info['status'],method,api_call) + elif info['status'] == 415: + # Unsupported Media Type + error_msg = "API request is not valid JSON; Status: {0}; Method: {1}: Call: {2}".format(info['status'],method,api_call) + elif info ['status'] == 400: + # Bad Request + error_msg = "API bad request; Status: {0}; Method: {1}: Call: {2}".format(info['status'],method,api_call) + + result = None + try: + content = resp.read() + result = json.loads(content) + except AttributeError: + error_msg += "; The API response was empty" + except JSONDecodeError: + error_msg += "; Failed to parse API response: {0}".format(content) + + # received an error status but no data with details on what failed + if (info['status'] not in [200,304]) and (result is None): + self.module.fail_json(msg=error_msg) + + if not result['success']: + error_msg += "; Error details: " + for error in result['errors']: + error_msg += "code: {0}, error: {1}; ".format(error['code'],error['message']) + if 'error_chain' in error: + for chain_error in error['error_chain']: + error_msg += "code: {0}, error: {1}; ".format(chain_error['code'],chain_error['message']) + self.module.fail_json(msg=error_msg) + + return result, info['status'] + + def _cf_api_call(self,api_call,method='GET',payload=None): + result, status = self._cf_simple_api_call(api_call,method,payload) + + data = result['result'] + + if 'result_info' in result: + pagination = result['result_info'] + if pagination['total_pages'] > 1: + next_page = int(pagination['page']) + 1 + parameters = ['page={0}'.format(next_page)] + # strip "page" parameter from call parameters (if there are any) + if '?' in api_call: + raw_api_call,query = api_call.split('?',1) + parameters += [param for param in query.split('&') if not param.startswith('page')] + else: + raw_api_call = api_call + while next_page <= pagination['total_pages']: + raw_api_call += '?' + '&'.join(parameters) + result, status = self._cf_simple_api_call(raw_api_call,method,payload) + data += result['result'] + next_page += 1 + + return data, status + + def _get_zone_id(self,zone=None): + if not zone: + zone = self.zone + + zones = self.get_zones(zone) + if len(zones) > 1: + self.module.fail_json(msg="More than one zone matches {0}".format(zone)) + + if len(zones) < 1: + self.module.fail_json(msg="No zone found with name {0}".format(zone)) + + return zones[0]['id'] + + def get_zones(self,name=None): + if not name: + name = self.zone + param = '' + if name: + param = '?' + urllib.urlencode({'name' : name}) + zones,status = self._cf_api_call('/zones' + param) + return zones + + def get_dns_records(self,zone_name=None,type=None,record=None,value=''): + if not zone_name: + zone_name = self.zone + if not type: + type = self.type + if not record: + record = self.record + # necessary because None as value means to override user + # set module value + if (not value) and (value is not None): + value = self.value + + + zone_id = self._get_zone_id() + api_call = '/zones/{0}/dns_records'.format(zone_id) + query = {} + if type: + query['type'] = type + if record: + query['name'] = record + if value: + query['content'] = value + if query: + api_call += '?' + urllib.urlencode(query) + + records,status = self._cf_api_call(api_call) + return records + + def delete_dns_records(self,**kwargs): + params = {} + for param in ['port','proto','service','solo','type','record','value','weight','zone']: + if param in kwargs: + params[param] = kwargs[param] + else: + params[param] = getattr(self,param) + + records = [] + search_value = params['value'] + search_record = params['record'] + if params['type'] == 'SRV': + search_value = str(params['weight']) + '\t' + str(params['port']) + '\t' + params['value'] + search_record = params['service'] + '.' + params['proto'] + '.' + params['record'] + if solo: + search_value = None + + records = self.get_dns_records(params['zone'],params['type'],search_record,search_value) + + for rr in records: + if solo: + if not ((rr['type'] == params['type']) and (rr['name'] == params['record']) and (rr['content'] == params['value'])): + self.changed = True + if not self.module.check_mode: + result, info = self._cf_api_call('/zones/{0}/dns_records/{1}'.format(rr['zone_id'],rr['id']),'DELETE') + else: + self.changed = True + if not self.module.check_mode: + result, info = self._cf_api_call('/zones/{0}/dns_records/{1}'.format(rr['zone_id'],rr['id']),'DELETE') + return self.changed + + def ensure_dns_record(self,**kwargs): + params = {} + for param in ['port','priority','proto','service','ttl','type','record','value','weight','zone']: + if param in kwargs: + params[param] = kwargs[param] + else: + params[param] = getattr(self,param) + + search_value = params['value'] + search_record = params['record'] + new_record = None + if (params['type'] is None) or (params['record'] is None): + self.module.fail_json(msg="You must provide a type and a record to create a new record") + + if (params['type'] in [ 'A','AAAA','CNAME','TXT','MX','NS','SPF']): + if not params['value']: + self.module.fail_json(msg="You must provide a non-empty value to create this record type") + new_record = { + "type": params['type'], + "name": params['record'], + "content": params['value'], + "ttl": params['ttl'] + } + + if params['type'] == 'MX': + for attr in [params['priority'],params['value']]: + if (attr is None) or (attr == ''): + self.module.fail_json(msg="You must provide priority and a value to create this record type") + new_record = { + "type": params['type'], + "name": params['record'], + "content": params['value'], + "priority": params['priority'], + "ttl": params['ttl'] + } + + if params['type'] == 'SRV': + for attr in [params['port'],params['priority'],params['proto'],params['service'],params['weight'],params['value']]: + if (attr is None) or (attr == ''): + self.module.fail_json(msg="You must provide port, priority, proto, service, weight and a value to create this record type") + srv_data = { + "target": params['value'], + "port": params['port'], + "weight": params['weight'], + "priority": params['priority'], + "name": params['record'], + "proto": params['proto'], + "service": params['service'] + } + new_record = { "type": params['type'], "ttl": params['ttl'], 'data': srv_data } + search_value = str(params['weight']) + '\t' + str(params['port']) + '\t' + params['value'] + search_record = params['service'] + '.' + params['proto'] + '.' + params['record'] + + zone_id = self._get_zone_id(params['zone']) + records = self.get_dns_records(params['zone'],params['type'],search_record,search_value) + # in theory this should be impossible as cloudflare does not allow + # the creation of duplicate records but lets cover it anyways + if len(records) > 1: + return records,self.changed + # record already exists, check if ttl must be updated + if len(records) == 1: + cur_record = records[0] + do_update = False + if (params['ttl'] is not None) and (cur_record['ttl'] != params['ttl'] ): + cur_record['ttl'] = params['ttl'] + do_update = True + if (params['priority'] is not None) and ('priority' in cur_record) and (cur_record['priority'] != params['priority']): + cur_record['priority'] = params['priority'] + do_update = True + if ('data' in new_record) and ('data' in cur_record): + if (cur_record['data'] > new_record['data']) - (cur_record['data'] < new_record['data']): + cur_record['data'] = new_record['data'] + do_update = True + if do_update: + if not self.module.check_mode: + result, info = self._cf_api_call('/zones/{0}/dns_records/{1}'.format(zone_id,records[0]['id']),'PUT',new_record) + self.changed = True + return result,self.changed + else: + return records,self.changed + if not self.module.check_mode: + result, info = self._cf_api_call('/zones/{0}/dns_records'.format(zone_id),'POST',new_record) + self.changed = True + return result,self.changed + +def main(): + module = AnsibleModule( + argument_spec = dict( + account_api_token = dict(required=True, no_log=True, type='str'), + account_email = dict(required=True, type='str'), + port = dict(required=False, default=None, type='int'), + priority = dict(required=False, default=1, type='int'), + proto = dict(required=False, default=None, choices=[ 'tcp', 'udp' ], type='str'), + record = dict(required=False, default='@', aliases=['name'], type='str'), + service = dict(required=False, default=None, type='str'), + solo = dict(required=False, default=None, type='bool'), + state = dict(required=False, default='present', choices=['present', 'absent'], type='str'), + timeout = dict(required=False, default=30, type='int'), + ttl = dict(required=False, default=1, type='int'), + type = dict(required=False, default=None, choices=[ 'A', 'AAAA', 'CNAME', 'TXT', 'SRV', 'MX', 'NS', 'SPF' ], type='str'), + value = dict(required=False, default=None, aliases=['content'], type='str'), + weight = dict(required=False, default=1, type='int'), + zone = dict(required=True, default=None, aliases=['domain'], type='str'), + ), + supports_check_mode = True, + required_if = ([ + ('state','present',['record','type']), + ('type','MX',['priority','value']), + ('type','SRV',['port','priority','proto','service','value','weight']), + ('type','A',['value']), + ('type','AAAA',['value']), + ('type','CNAME',['value']), + ('type','TXT',['value']), + ('type','NS',['value']), + ('type','SPF',['value']) + ] + ), + required_one_of = ( + [['record','value','type']] + ) + ) + + changed = False + cf_api = CloudflareAPI(module) + + # sanity checks + if cf_api.is_solo and cf_api.state == 'absent': + module.fail_json(msg="solo=true can only be used with state=present") + + # perform add, delete or update (only the TTL can be updated) of one or + # more records + if cf_api.state == 'present': + # delete all records matching record name + type + if cf_api.is_solo: + changed = cf_api.delete_dns_records(solo=cf_api.is_solo) + result,changed = cf_api.ensure_dns_record() + module.exit_json(changed=changed,result={'records': result}) + else: + # force solo to False, just to be sure + changed = cf_api.delete_dns_records(solo=False) + module.exit_json(changed=changed) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * + +if __name__ == '__main__': + main() From 233869abc9d35a6478d668d7f0538f945c2fce2c Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 19 Mar 2016 00:02:33 +0100 Subject: [PATCH 1320/2522] cloudflare_dns: minor code improvments Tweaked some things on top of the well done PR #1768 - json import - fix expetion handling - fix indentation --- network/cloudflare_dns.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/network/cloudflare_dns.py b/network/cloudflare_dns.py index 7b380450457..a5d03ba5479 100644 --- a/network/cloudflare_dns.py +++ b/network/cloudflare_dns.py @@ -18,7 +18,14 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -import json +try: + import json +except ImportError: + try: + import simplejson as json + except ImportError: + # Let snippet from module_utils/basic.py return a proper error in this case + pass import urllib DOCUMENTATION = ''' @@ -263,7 +270,7 @@ def _cf_simple_api_call(self,api_call,method='GET',payload=None): result = json.loads(content) except AttributeError: error_msg += "; The API response was empty" - except JSONDecodeError: + except json.JSONDecodeError: error_msg += "; Failed to parse API response: {0}".format(content) # received an error status but no data with details on what failed @@ -358,10 +365,10 @@ def get_dns_records(self,zone_name=None,type=None,record=None,value=''): def delete_dns_records(self,**kwargs): params = {} for param in ['port','proto','service','solo','type','record','value','weight','zone']: - if param in kwargs: - params[param] = kwargs[param] - else: - params[param] = getattr(self,param) + if param in kwargs: + params[param] = kwargs[param] + else: + params[param] = getattr(self,param) records = [] search_value = params['value'] From f80865d32add73087deb91ea178e5d96a17ed939 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 19 Mar 2016 00:59:13 +0100 Subject: [PATCH 1321/2522] lvol: remove unused import See #1425 --- system/lvol.py | 1 - 1 file changed, 1 deletion(-) diff --git a/system/lvol.py b/system/lvol.py index 5b401a66014..fe5cee57569 100644 --- a/system/lvol.py +++ b/system/lvol.py @@ -108,7 +108,6 @@ ''' import re -import logging decimal_point = re.compile(r"(\d+)") From 87599fd2ee4ab8a20c95950b165a94f41afcf817 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 19 Mar 2016 13:17:43 +0100 Subject: [PATCH 1322/2522] osx_defaults: doc fix, add version_added for host agrument See #1364 --- system/osx_defaults.py | 1 + 1 file changed, 1 insertion(+) diff --git a/system/osx_defaults.py b/system/osx_defaults.py index 06c1fb0c4c7..2415e76f736 100644 --- a/system/osx_defaults.py +++ b/system/osx_defaults.py @@ -39,6 +39,7 @@ "-currentHost" switch of the defaults commandline tool. required: false default: null + version_added: "2.1" key: description: - The key of the user preference From 4396c26af56d93ed7260fcf5392fd22e83086e33 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sat, 19 Mar 2016 13:40:58 +0100 Subject: [PATCH 1323/2522] Reindent with_items, fix #1849 --- files/blockinfile.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/files/blockinfile.py b/files/blockinfile.py index c2e449b2edc..e31799da759 100644 --- a/files/blockinfile.py +++ b/files/blockinfile.py @@ -144,10 +144,10 @@ block: | {{item.name}} {{item.ip}} marker: "# {mark} ANSIBLE MANAGED BLOCK {{item.name}}" - with_items: - - { name: host1, ip: 10.10.1.10 } - - { name: host2, ip: 10.10.1.11 } - - { name: host3, ip: 10.10.1.12 } + with_items: + - { name: host1, ip: 10.10.1.10 } + - { name: host2, ip: 10.10.1.11 } + - { name: host3, ip: 10.10.1.12 } """ From ea56151a67537907e80947f2c8b1104377efdfd8 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 19 Mar 2016 14:06:54 +0100 Subject: [PATCH 1324/2522] ec2_vpc_dhcp_options: doc fix, add version_added to new args See #1640 --- cloud/amazon/ec2_vpc_dhcp_options.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cloud/amazon/ec2_vpc_dhcp_options.py b/cloud/amazon/ec2_vpc_dhcp_options.py index 99b6fbbd6a2..198d0637177 100644 --- a/cloud/amazon/ec2_vpc_dhcp_options.py +++ b/cloud/amazon/ec2_vpc_dhcp_options.py @@ -92,6 +92,7 @@ required: False default: None aliases: [ 'resource_tags'] + version_added: "2.1" dhcp_options_id: description: - The resource_id of an existing DHCP options set. @@ -99,6 +100,7 @@ (which will be updated to match) required: False default: None + version_added: "2.1" state: description: - create/assign or remove the DHCP options. @@ -107,6 +109,7 @@ required: False default: present choices: [ 'absent', 'present' ] + version_added: "2.1" extends_documentation_fragment: aws requirements: - boto From 27b3b43c52b81e7d8fea5ff62ee9293b7f00c808 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sat, 19 Mar 2016 23:59:34 +0100 Subject: [PATCH 1325/2522] Fix ssl to be a bool, required to fix #1732 May also fix #1869 --- database/misc/mongodb_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index 2bc29586d07..6c0e2196d29 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -225,7 +225,7 @@ def main(): database=dict(required=True, aliases=['db']), name=dict(required=True, aliases=['user']), password=dict(aliases=['pass']), - ssl=dict(default=False), + ssl=dict(default=False, type='bool'), roles=dict(default=None, type='list'), state=dict(default='present', choices=['absent', 'present']), update_password=dict(default="always", choices=["always", "on_create"]), From 8c53e654f765412d5471860c43a4f06afb2272ba Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sun, 20 Mar 2016 00:41:14 +0100 Subject: [PATCH 1326/2522] Add proper type to path and link Since both of them are path, it should be checked using the proper type. --- system/alternatives.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/alternatives.py b/system/alternatives.py index 90e2237f86c..e81a3e83065 100644 --- a/system/alternatives.py +++ b/system/alternatives.py @@ -67,8 +67,8 @@ def main(): module = AnsibleModule( argument_spec = dict( name = dict(required=True), - path = dict(required=True), - link = dict(required=False), + path = dict(required=True, type='path'), + link = dict(required=False, type='path'), ), supports_check_mode=True, ) From ed9b83744b5be525fc7496bbd498b7211d3712e6 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sun, 20 Mar 2016 00:49:56 +0100 Subject: [PATCH 1327/2522] Use proper type for riak config_dir argument --- database/misc/riak.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/misc/riak.py b/database/misc/riak.py index 1f1cd11e922..ccdec82f0af 100644 --- a/database/misc/riak.py +++ b/database/misc/riak.py @@ -125,7 +125,7 @@ def main(): argument_spec=dict( command=dict(required=False, default=None, choices=[ 'ping', 'kv_test', 'join', 'plan', 'commit']), - config_dir=dict(default='/etc/riak'), + config_dir=dict(default='/etc/riak', type='path'), http_conn=dict(required=False, default='127.0.0.1:8098'), target_node=dict(default='riak@127.0.0.1', required=False), wait_for_handoffs=dict(default=False, type='int'), From 7f386385752a9ba9154fd0ce3f40d6dcd7685687 Mon Sep 17 00:00:00 2001 From: Andrea Scarpino Date: Sun, 20 Mar 2016 12:29:26 +0100 Subject: [PATCH 1328/2522] win_unzip: Use absolute path for src and dest win_unzip fails to extract files when either src or dest contains complex paths such as "..\..\" or "C:\\Program Files" (double slashes). Fix this by fetching absolute path of both before invoking CopyHere method. --- windows/win_unzip.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/windows/win_unzip.ps1 b/windows/win_unzip.ps1 index f547c0081fa..59fbd33166c 100644 --- a/windows/win_unzip.ps1 +++ b/windows/win_unzip.ps1 @@ -59,8 +59,8 @@ $rm = ConvertTo-Bool (Get-AnsibleParam -obj $params -name "rm" -default "false") If ($ext -eq ".zip" -And $recurse -eq $false) { Try { $shell = New-Object -ComObject Shell.Application - $zipPkg = $shell.NameSpace($src) - $destPath = $shell.NameSpace($dest) + $zipPkg = $shell.NameSpace([IO.Path]::GetFullPath($src)) + $destPath = $shell.NameSpace([IO.Path]::GetFullPath($dest)) # 20 means do not display any dialog (4) and overwrite any file (16) $destPath.CopyHere($zipPkg.Items(), 20) $result.changed = $true From 1c461f7c59e21bc59e4fdeca8cd23f9a885b957c Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sun, 20 Mar 2016 01:02:09 +0100 Subject: [PATCH 1329/2522] Fix type used by the module Set int for the various port (and so avoid to convert them later) Set no_log=True for the login_password Verify that db is a int, so avoid a conversion --- database/misc/redis.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/database/misc/redis.py b/database/misc/redis.py index 42e364a8e61..be012e6d79a 100644 --- a/database/misc/redis.py +++ b/database/misc/redis.py @@ -166,13 +166,13 @@ def main(): module = AnsibleModule( argument_spec = dict( command=dict(default=None, choices=['slave', 'flush', 'config']), - login_password=dict(default=None), + login_password=dict(default=None, no_log=True), login_host=dict(default='localhost'), - login_port=dict(default='6379'), + login_port=dict(default=6379, type='int'), master_host=dict(default=None), - master_port=dict(default=None), + master_port=dict(default=None, type='int'), slave_mode=dict(default='slave', choices=['master', 'slave']), - db=dict(default=None), + db=dict(default=None, type='int'), flush_mode=dict(default='all', choices=['all', 'db']), name=dict(default=None), value=dict(default=None) @@ -185,17 +185,13 @@ def main(): login_password = module.params['login_password'] login_host = module.params['login_host'] - login_port = int(module.params['login_port']) + login_port = module.params['login_port'] command = module.params['command'] # Slave Command section ----------- if command == "slave": master_host = module.params['master_host'] master_port = module.params['master_port'] - try: - master_port = int(module.params['master_port']) - except Exception: - pass mode = module.params['slave_mode'] #Check if we have all the data @@ -257,15 +253,12 @@ def main(): # flush Command section ----------- elif command == "flush": - try: - db = int(module.params['db']) - except Exception: - db = 0 + db = module.params['db'] mode = module.params['flush_mode'] #Check if we have all the data if mode == "db": - if type(db) != int: + if db is None: module.fail_json( msg="In db mode the db number must be provided") From 0df2191624b358f4a5353094fa82cef18a84aaee Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Mon, 21 Mar 2016 00:23:12 +0100 Subject: [PATCH 1330/2522] Add better type checking for elasticsearch_plugin --- packaging/elasticsearch_plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packaging/elasticsearch_plugin.py b/packaging/elasticsearch_plugin.py index a41333afe10..02fc674de1c 100644 --- a/packaging/elasticsearch_plugin.py +++ b/packaging/elasticsearch_plugin.py @@ -134,8 +134,8 @@ def main(): state=dict(default="present", choices=package_state_map.keys()), url=dict(default=None), timeout=dict(default="1m"), - plugin_bin=dict(default="/usr/share/elasticsearch/bin/plugin"), - plugin_dir=dict(default="/usr/share/elasticsearch/plugins/"), + plugin_bin=dict(default="/usr/share/elasticsearch/bin/plugin", type="path"), + plugin_dir=dict(default="/usr/share/elasticsearch/plugins/", type="path"), proxy_host=dict(default=None), proxy_port=dict(default=None), version=dict(default=None) From 73e4b48bba3fb1616d493e720f62c88396e467f1 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Mon, 21 Mar 2016 00:24:35 +0100 Subject: [PATCH 1331/2522] Use no_log for the password for maven_artifact module --- packaging/language/maven_artifact.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/language/maven_artifact.py b/packaging/language/maven_artifact.py index fe311ea53de..203f09dacc3 100644 --- a/packaging/language/maven_artifact.py +++ b/packaging/language/maven_artifact.py @@ -299,7 +299,7 @@ def main(): extension = dict(default='jar'), repository_url = dict(default=None), username = dict(default=None), - password = dict(default=None), + password = dict(default=None, no_log=True), state = dict(default="present", choices=["present","absent"]), # TODO - Implement a "latest" state dest = dict(type="path", default=None), validate_certs = dict(required=False, default=True, type='bool'), From 90d7a023a04b4a38cba910294e929fb67946e950 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 21 Mar 2016 08:25:41 +0000 Subject: [PATCH 1332/2522] Added return documentation to win_regmerge module --- windows/win_regmerge.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/windows/win_regmerge.py b/windows/win_regmerge.py index 3e2f51fe869..6507b84b9c2 100644 --- a/windows/win_regmerge.py +++ b/windows/win_regmerge.py @@ -68,4 +68,20 @@ compare_to: HKLM:\SOFTWARE\myCompany ''' -RETURN = '''# ''' +RETURN = ''' +compare_to_key_found: + description: whether the parent registry key has been found for comparison + returned: when comparison key not found in registry + type: boolean + sample: false +difference_count: + description: number of differences between the registry and the file + returned: changed + type: integer + sample: 1 +compared: + description: whether a comparison has taken place between the registry and the file + returned: when a comparison key has been supplied and comparison has been attempted + type: boolean + sample: true +''' From 1c097f9495c70fa0067a9edfb0ffa5fdd873cfad Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Mon, 21 Mar 2016 17:49:51 +0100 Subject: [PATCH 1333/2522] suggestion by @nitzmahone, to not use Get-Attr in combination with ConvertTo-Bool --- windows/win_owner.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/windows/win_owner.ps1 b/windows/win_owner.ps1 index d781dd011d8..076ab846052 100644 --- a/windows/win_owner.ps1 +++ b/windows/win_owner.ps1 @@ -88,7 +88,8 @@ Set-Attr $result "changed" $false; $path = Get-Attr $params "path" -failifempty $true $user = Get-Attr $params "user" -failifempty $true -$recurse = Get-Attr $params "recurse" "no" -validateSet "no","yes" -resultobj $result | ConvertTo-Bool +$recurse = Get-Attr $params "recurse" "no" -validateSet "no","yes" -resultobj $result +$recurse = $recurse | ConvertTo-Bool If (-Not (Test-Path -Path $path)) { Fail-Json $result "$path file or directory does not exist on the host" From 879410a94e56c5a010106fc0b7763dbadb006d9a Mon Sep 17 00:00:00 2001 From: Michael Gruener Date: Mon, 21 Mar 2016 21:47:26 +0100 Subject: [PATCH 1334/2522] cloudflare_dns: Fix wrong variable name --- network/cloudflare_dns.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/network/cloudflare_dns.py b/network/cloudflare_dns.py index a5d03ba5479..8772c2ac659 100644 --- a/network/cloudflare_dns.py +++ b/network/cloudflare_dns.py @@ -376,13 +376,13 @@ def delete_dns_records(self,**kwargs): if params['type'] == 'SRV': search_value = str(params['weight']) + '\t' + str(params['port']) + '\t' + params['value'] search_record = params['service'] + '.' + params['proto'] + '.' + params['record'] - if solo: + if params['solo']: search_value = None records = self.get_dns_records(params['zone'],params['type'],search_record,search_value) for rr in records: - if solo: + if params['solo']: if not ((rr['type'] == params['type']) and (rr['name'] == params['record']) and (rr['content'] == params['value'])): self.changed = True if not self.module.check_mode: From 2ce5b4c5261f1559a55bff0b35d107d52d83d01a Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Tue, 22 Mar 2016 09:07:09 +0100 Subject: [PATCH 1335/2522] suggestion by @nitzmahone to not use Get-Attr in combination with ConvertTo-Bool, improved documentation regarding organize --- windows/win_acl_inheritance.ps1 | 3 ++- windows/win_acl_inheritance.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/windows/win_acl_inheritance.ps1 b/windows/win_acl_inheritance.ps1 index 0d808bb8c49..1933a3a5dd4 100644 --- a/windows/win_acl_inheritance.ps1 +++ b/windows/win_acl_inheritance.ps1 @@ -27,7 +27,8 @@ Set-Attr $result "changed" $false; $path = Get-Attr $params "path" -failifempty $true $state = Get-Attr $params "state" "absent" -validateSet "present","absent" -resultobj $result -$reorganize = Get-Attr $params "reorganize" "no" -validateSet "no","yes" -resultobj $result | ConvertTo-Bool +$reorganize = Get-Attr $params "reorganize" "no" -validateSet "no","yes" -resultobj $result +$reorganize = $reorganize | ConvertTo-Bool If (-Not (Test-Path -Path $path)) { Fail-Json $result "$path file or directory does not exist on the host" diff --git a/windows/win_acl_inheritance.py b/windows/win_acl_inheritance.py index 0837bab3205..a4bb90a47b3 100644 --- a/windows/win_acl_inheritance.py +++ b/windows/win_acl_inheritance.py @@ -43,7 +43,8 @@ default: absent reorganize: description: - - For P(state) = I(absent), indicates if the inherited ACE's should be copied. For P(state) = I(present), indicates if the inherited ACE's should be simplified. + - For P(state) = I(absent), indicates if the inherited ACE's should be copied from the parent directory. This is necessary (in combination with removal) for a simple ACL instead of using multiple ACE deny entries. + - For P(state) = I(present), indicates if the inherited ACE's should be deduplicated compared to the parent directory. This removes complexity of the ACL structure. required: false choices: - no From 09da03ca0ceb707df68b5851896c475cc13c6c75 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Tue, 22 Mar 2016 12:08:50 +0100 Subject: [PATCH 1336/2522] Add the proper type for the various path argument --- packaging/language/bundler.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packaging/language/bundler.py b/packaging/language/bundler.py index 2408741f880..7c36d5a873d 100644 --- a/packaging/language/bundler.py +++ b/packaging/language/bundler.py @@ -140,15 +140,15 @@ def main(): argument_spec=dict( executable=dict(default=None, required=False), state=dict(default='present', required=False, choices=['present', 'latest']), - chdir=dict(default=None, required=False), + chdir=dict(default=None, required=False, type='path'), exclude_groups=dict(default=None, required=False, type='list'), clean=dict(default=False, required=False, type='bool'), - gemfile=dict(default=None, required=False), + gemfile=dict(default=None, required=False, type='path'), local=dict(default=False, required=False, type='bool'), deployment_mode=dict(default=False, required=False, type='bool'), user_install=dict(default=True, required=False, type='bool'), - gem_path=dict(default=None, required=False), - binstub_directory=dict(default=None, required=False), + gem_path=dict(default=None, required=False, type='path'), + binstub_directory=dict(default=None, required=False, type='path'), extra_args=dict(default=None, required=False), ), supports_check_mode=True From 7dae3faf0f180417e2e7713f3de8f05020554c7b Mon Sep 17 00:00:00 2001 From: James Slagle Date: Tue, 22 Mar 2016 07:40:50 -0400 Subject: [PATCH 1337/2522] Add quotes and equals for set option documentation set is an option for the openvswitch_port module, however the documentation example omitted the equals sign and quotes around the option value. --- network/openvswitch_port.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/openvswitch_port.py b/network/openvswitch_port.py index 5fbbe8480dd..69e64ea8f9d 100644 --- a/network/openvswitch_port.py +++ b/network/openvswitch_port.py @@ -71,7 +71,7 @@ # Creates port eth6 and set ofport equal to 6. - openvswitch_port: bridge=bridge-loop port=eth6 state=present - set Interface eth6 ofport_request=6 + set="Interface eth6 ofport_request=6" # Assign interface id server1-vifeth6 and mac address 52:54:00:30:6d:11 # to port vifeth6 and setup port to be managed by a controller. From b560a764c0a9aab9e74d8864e00ab4bb8eb1e340 Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Tue, 22 Mar 2016 11:25:57 +0000 Subject: [PATCH 1338/2522] Allow passing domain name on os_project --- cloud/openstack/os_project.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/cloud/openstack/os_project.py b/cloud/openstack/os_project.py index b900d42f164..630b26c4112 100644 --- a/cloud/openstack/os_project.py +++ b/cloud/openstack/os_project.py @@ -161,6 +161,22 @@ def main(): state = module.params['state'] try: + if domain: + opcloud = shade.operator_cloud(**module.params) + try: + # We assume admin is passing domain id + dom = opcloud.get_domain(domain)['id'] + domain = dom + except: + # If we fail, maybe admin is passing a domain name. + # Note that domains have unique names, just like id. + try: + dom = opcloud.search_domains(filters={'name': domain})[0]['id'] + domain = dom + except: + # Ok, let's hope the user is non-admin and passing a sane id + pass + cloud = shade.openstack_cloud(**module.params) project = cloud.get_project(name) From d0501864ab56811313f1bb97ad457b3558b728b5 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Wed, 23 Mar 2016 00:43:37 +0100 Subject: [PATCH 1339/2522] dynamodb_table: doc fix --- cloud/amazon/dynamodb_table.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/amazon/dynamodb_table.py b/cloud/amazon/dynamodb_table.py index 94609167494..b0c335a16ab 100644 --- a/cloud/amazon/dynamodb_table.py +++ b/cloud/amazon/dynamodb_table.py @@ -72,9 +72,9 @@ indexes: description: - list of dictionaries describing indexes to add to the table. global indexes can be updated. local indexes don't support updates or have throughput. - - required options: ['name', 'type', 'hash_key_name'] - - valid types: ['all', 'global_all', 'global_include', 'global_keys_only', 'include', 'keys_only'] - - other options: ['hash_key_type', 'range_key_name', 'range_key_type', 'includes', 'read_capacity', 'write_capacity'] + - "required options: ['name', 'type', 'hash_key_name']" + - "valid types: ['all', 'global_all', 'global_include', 'global_keys_only', 'include', 'keys_only']" + - "other options: ['hash_key_type', 'range_key_name', 'range_key_type', 'includes', 'read_capacity', 'write_capacity']" required: false default: [] version_added: "2.1" From a92a0e56f2df44f91237730b3251e4dd6413a3eb Mon Sep 17 00:00:00 2001 From: Matthew Gamble Date: Wed, 23 Mar 2016 18:39:26 +1100 Subject: [PATCH 1340/2522] Remove dead code from pacman module The manual check to see if get_bin_path() returned anything is redundant, because we pass True to the required parameter of get_bin_path(). This automatically causes the task to fail if the pacman binary isn't available. Therefore, the code within the if statement being removed is never called. --- packaging/os/pacman.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/packaging/os/pacman.py b/packaging/os/pacman.py index 419def43a7c..a81f6801ffc 100644 --- a/packaging/os/pacman.py +++ b/packaging/os/pacman.py @@ -301,9 +301,6 @@ def main(): pacman_path = module.get_bin_path('pacman', True) - if not os.path.exists(pacman_path): - module.fail_json(msg="cannot find pacman, in path %s" % (pacman_path)) - p = module.params # normalize the state parameter From 12890b14b2482c85d425298ccfc71b893d44a041 Mon Sep 17 00:00:00 2001 From: Pavel Sychev Date: Tue, 26 Jan 2016 16:10:52 +0300 Subject: [PATCH 1341/2522] Added reject_with and uid_owner support. --- system/iptables.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/system/iptables.py b/system/iptables.py index 6d2214a3105..ebf399101a1 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -291,6 +291,11 @@ def append_match(rule, param, match): rule.extend(['-m', match]) +def append_jump(rule, param, jump): + if param: + rule.extend(['-j', jump]) + + def construct_rule(params): rule = [] append_param(rule, params['protocol'], '-p', False) @@ -315,6 +320,10 @@ def construct_rule(params): append_match(rule, params['limit'] or params['limit_burst'], 'limit') append_param(rule, params['limit'], '--limit', False) append_param(rule, params['limit_burst'], '--limit-burst', False) + append_match(rule, params['uid_owner'], 'owner') + append_param(rule, params['uid_owner'], '--uid-owner', False) + append_jump(rule, params['reject_with'], 'REJECT') + append_param(rule, params['reject_with'], '--reject-with', False) return rule @@ -369,6 +378,8 @@ def main(): ctstate=dict(required=False, default=[], type='list'), limit=dict(required=False, default=None, type='str'), limit_burst=dict(required=False, default=None, type='str'), + uid_owner=dict(required=False, default=None, type='str'), + reject_with=dict(required=False, default=None, type='str'), ), mutually_exclusive=( ['set_dscp_mark', 'set_dscp_mark_class'], From c15dcf888bee05109247218d90358dc608bbb98e Mon Sep 17 00:00:00 2001 From: Pavel Sychev Date: Thu, 28 Jan 2016 11:27:31 +0300 Subject: [PATCH 1342/2522] Added docs for reject_with and uid_owner. --- system/iptables.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/system/iptables.py b/system/iptables.py index ebf399101a1..4a85d7316ee 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -249,6 +249,14 @@ - "Specifies the maximum burst before the above limit kicks in." required: false default: null + uid_owner: + description: + - "Specifies the UID or username to use in match by owner rule." + required: false + reject_with: + description: + - "Specifies the error packet type to return while rejecting." + required: false ''' EXAMPLES = ''' From eda178a9884160a1a276ceaeea2e4c5e460a4085 Mon Sep 17 00:00:00 2001 From: Pavel Sychev Date: Wed, 23 Mar 2016 13:46:50 +0300 Subject: [PATCH 1343/2522] Added version restriction for uid_owner and reject_with. --- system/iptables.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/system/iptables.py b/system/iptables.py index 4a85d7316ee..10167f17655 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -250,10 +250,12 @@ required: false default: null uid_owner: + version_added: "2.1" description: - "Specifies the UID or username to use in match by owner rule." required: false reject_with: + version_added: "2.1" description: - "Specifies the error packet type to return while rejecting." required: false From 04982da9b8bea61c84a1a0b43f12bed85a720919 Mon Sep 17 00:00:00 2001 From: Chris Tooley Date: Wed, 23 Mar 2016 13:23:30 +0000 Subject: [PATCH 1344/2522] Modify consul certificate validation bypass keyword from 'verify' to 'validate_certs' --- clustering/consul.py | 6 +++--- clustering/consul_acl.py | 6 +++--- clustering/consul_kv.py | 6 +++--- clustering/consul_session.py | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/clustering/consul.py b/clustering/consul.py index e24279586e6..f0df2368b51 100644 --- a/clustering/consul.py +++ b/clustering/consul.py @@ -77,7 +77,7 @@ required: false default: http version_added: "2.1" - verify: + validate_certs: description: - whether to verify the tls certificate of the consul agent required: false @@ -321,7 +321,7 @@ def get_consul_api(module, token=None): return consul.Consul(host=module.params.get('host'), port=module.params.get('port'), scheme=module.params.get('scheme'), - verify=module.params.get('verify'), + validate_certs=module.params.get('validate_certs'), token=module.params.get('token')) @@ -518,7 +518,7 @@ def main(): host=dict(default='localhost'), port=dict(default=8500, type='int'), scheme=dict(required=False, default='http'), - verify=dict(required=False, default=True, type='bool'), + validate_certs=dict(required=False, default=True, type='bool'), check_id=dict(required=False), check_name=dict(required=False), check_node=dict(required=False), diff --git a/clustering/consul_acl.py b/clustering/consul_acl.py index e07db83232d..b1c7763a550 100644 --- a/clustering/consul_acl.py +++ b/clustering/consul_acl.py @@ -75,7 +75,7 @@ required: false default: http version_added: "2.1" - verify: + validate_certs: description: - whether to verify the tls certificate of the consul agent required: false @@ -313,7 +313,7 @@ def get_consul_api(module, token=None): return consul.Consul(host=module.params.get('host'), port=module.params.get('port'), scheme=module.params.get('scheme'), - verify=module.params.get('verify'), + validate_certs=module.params.get('validate_certs'), token=token) def test_dependencies(module): @@ -330,7 +330,7 @@ def main(): mgmt_token=dict(required=True, no_log=True), host=dict(default='localhost'), scheme=dict(required=False, default='http'), - verify=dict(required=False, default=True), + validate_certs=dict(required=False, default=True), name=dict(required=False), port=dict(default=8500, type='int'), rules=dict(default=None, required=False, type='list'), diff --git a/clustering/consul_kv.py b/clustering/consul_kv.py index 45c93d672a0..9358b79749a 100644 --- a/clustering/consul_kv.py +++ b/clustering/consul_kv.py @@ -105,7 +105,7 @@ required: false default: http version_added: "2.1" - verify: + validate_certs: description: - whether to verify the tls certificate of the consul agent required: false @@ -231,7 +231,7 @@ def get_consul_api(module, token=None): return consul.Consul(host=module.params.get('host'), port=module.params.get('port'), scheme=module.params.get('scheme'), - verify=module.params.get('verify'), + validate_certs=module.params.get('validate_certs'), token=module.params.get('token')) def test_dependencies(module): @@ -247,7 +247,7 @@ def main(): key=dict(required=True), host=dict(default='localhost'), scheme=dict(required=False, default='http'), - verify=dict(required=False, default=True), + validate_certs=dict(required=False, default=True), port=dict(default=8500, type='int'), recurse=dict(required=False, type='bool'), retrieve=dict(required=False, default=True), diff --git a/clustering/consul_session.py b/clustering/consul_session.py index 625c31c7979..3ba3b6f1619 100644 --- a/clustering/consul_session.py +++ b/clustering/consul_session.py @@ -94,7 +94,7 @@ required: false default: http version_added: "2.1" - verify: + validate_certs: description: - whether to verify the tls certificate of the consul agent required: false @@ -257,7 +257,7 @@ def main(): host=dict(default='localhost'), port=dict(default=8500, type='int'), scheme=dict(required=False, default='http'), - verify=dict(required=False, default=True), + validate_certs=dict(required=False, default=True), id=dict(required=False), name=dict(required=False), node=dict(required=False), From 40baa74b2e0862022f93a8a39b58bbd83be76a59 Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Wed, 23 Mar 2016 15:47:53 +0100 Subject: [PATCH 1345/2522] Update the issue and pull-request templates in sync with ansible/ansible --- .github/ISSUE_TEMPLATE.md | 52 +++++++++++++++++++++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 27 ++++++++++++++++ ISSUE_TEMPLATE.md | 53 -------------------------------- PULL_REQUEST_TEMPLATE.md | 25 --------------- 4 files changed, 79 insertions(+), 78 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md delete mode 100644 ISSUE_TEMPLATE.md delete mode 100644 PULL_REQUEST_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000000..feb687200ed --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,52 @@ + + +##### ISSUE TYPE + + - Bug Report + - Feature Idea + - Documentation Report + +##### COMPONENT NAME + + +##### ANSIBLE VERSION +``` + +``` + +##### CONFIGURATION + + +##### OS / ENVIRONMENT + + +##### SUMMARY + + +##### STEPS TO REPRODUCE + + +``` + +``` + + + +##### EXPECTED RESULTS + + +##### ACTUAL RESULTS + + +``` + +``` diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..d8b8e17cbd5 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,27 @@ +##### ISSUE TYPE + + - Feature Pull Request + - New Module Pull Request + - Bugfix Pull Request + - Docs Pull Request + +##### COMPONENT NAME + + +##### ANSIBLE VERSION +``` + +``` + +##### SUMMARY + + + + +``` + +``` diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md deleted file mode 100644 index 5870836fcc6..00000000000 --- a/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,53 +0,0 @@ -##### Issue Type: - - - - Bug Report - - Feature Idea - - Documentation Report - -##### Plugin Name: - - - -##### Ansible Version: - -``` - -``` - -##### Ansible Configuration: - - - -##### Environment: - - - -##### Summary: - - - -##### Steps To Reproduce: - - - -``` - -``` - - - -##### Expected Results: - - - -##### Actual Results: - - - -``` - -``` diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index f821123acd4..00000000000 --- a/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,25 +0,0 @@ -##### Issue Type: - - - - Feature Pull Request - - New Module Pull Request - - Bugfix Pull Request - - Docs Pull Request - -##### Plugin Name: - - - -##### Summary: - - - - - -##### Example: - -``` - -``` From 745df06abc47f5a9ace62feec4c9dd05b8c29a08 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Wed, 23 Mar 2016 12:10:15 -0700 Subject: [PATCH 1346/2522] renamed sl to sl_vm and updated docs namespace for softlayer modules should now be sl_ --- cloud/softlayer/{sl.py => sl_vm.py} | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) rename cloud/softlayer/{sl.py => sl_vm.py} (99%) diff --git a/cloud/softlayer/sl.py b/cloud/softlayer/sl_vm.py similarity index 99% rename from cloud/softlayer/sl.py rename to cloud/softlayer/sl_vm.py index f5a48bd4e93..8ef808d9a2e 100644 --- a/cloud/softlayer/sl.py +++ b/cloud/softlayer/sl_vm.py @@ -146,7 +146,7 @@ tasks: - name: Build instance request local_action: - module: sl + module: sl_vm hostname: instance-1 domain: anydomain.com datacenter: dal09 @@ -167,7 +167,7 @@ tasks: - name: Build instances request local_action: - module: sl + module: sl_vm hostname: "{{ item.hostname }}" domain: "{{ item.domain }}" datacenter: "{{ item.datacenter }}" @@ -193,7 +193,7 @@ tasks: - name: Cancel by tag local_action: - module: sl + module: sl_vm state: absent tags: ansible-module-test ''' @@ -203,6 +203,7 @@ import time +#TODO: get this info from API STATES = ['present', 'absent'] DATACENTERS = ['ams01','ams03','dal01','dal05','dal06','dal09','fra02','hkg02','hou02','lon2','mel01','mex01','mil01','mon01','par01','sjc01','sjc03','sao01','sea01','sng01','syd01','tok02','tor01','wdc01','wdc04'] CPU_SIZES = [1,2,4,8,16] @@ -345,4 +346,4 @@ def main(): from ansible.module_utils.basic import * if __name__ == '__main__': - main() \ No newline at end of file + main() From 3b95400a593a240038282d4168002c12ec6cb432 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 22 Mar 2016 15:27:23 -0700 Subject: [PATCH 1347/2522] change name to be a list type remove implicit split that expects a , separated string, let list type deal with multiple possible compatible input types. also removed unused imports --- packaging/os/pkgin.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packaging/os/pkgin.py b/packaging/os/pkgin.py index cdba6a9218b..f75adf8299c 100644 --- a/packaging/os/pkgin.py +++ b/packaging/os/pkgin.py @@ -63,10 +63,6 @@ ''' -import shlex -import os -import sys -import pipes import re def query_package(module, pkgin_path, name): @@ -214,14 +210,14 @@ def main(): module = AnsibleModule( argument_spec = dict( state = dict(default="present", choices=["present","absent"]), - name = dict(aliases=["pkg"], required=True)), + name = dict(aliases=["pkg"], required=True, type='list')), supports_check_mode = True) pkgin_path = module.get_bin_path('pkgin', True, ['/opt/local/bin']) p = module.params - pkgs = p["name"].split(",") + pkgs = p["name"] if p["state"] == "present": install_packages(module, pkgin_path, pkgs) From edf697b8bdd3cb90db7459bc05224ff8e05ec9c0 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 24 Mar 2016 08:45:00 -0400 Subject: [PATCH 1348/2522] Add shade version check to os_flavor_facts The range_search() API was added to the shade library in version 1.5.0 so let's check for that and let the user know they need to upgrade if they try to use it. --- cloud/openstack/os_flavor_facts.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cloud/openstack/os_flavor_facts.py b/cloud/openstack/os_flavor_facts.py index 3795abd08d0..05a3782be7e 100644 --- a/cloud/openstack/os_flavor_facts.py +++ b/cloud/openstack/os_flavor_facts.py @@ -23,6 +23,9 @@ except ImportError: HAS_SHADE = False +from distutils.version import StrictVersion + + DOCUMENTATION = ''' --- module: os_flavor_facts @@ -200,6 +203,9 @@ def main(): if ram: filters['ram'] = ram if filters: + # Range search added in 1.5.0 + if StrictVersion(shade.__version__) < StrictVersion('1.5.0'): + module.fail_json(msg="Shade >= 1.5.0 needed for this functionality") flavors = cloud.range_search(flavors, filters) if limit is not None: From a0aef208b61fb871bf6b2a11958f44d99e70efea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Wed, 16 Sep 2015 16:25:26 +0200 Subject: [PATCH 1349/2522] module: system/make --- system/make.py | 118 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 system/make.py diff --git a/system/make.py b/system/make.py new file mode 100644 index 00000000000..e66e3fbab6e --- /dev/null +++ b/system/make.py @@ -0,0 +1,118 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Linus Unnebäck +# +# This file is part of Ansible +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + +# import module snippets +from ansible.module_utils.basic import * + +DOCUMENTATION = ''' +--- +module: make +short_description: Run targets in a Makefile +requirements: [] +version_added: "2.1" +author: Linus Unnebäck (@LinusU) +description: Run targets in a Makefile. +options: + target: + description: The target to run + required: false + params: + description: Any extra parameters to pass to make + required: false + chdir: + description: cd into this directory before running make + required: true +''' + +EXAMPLES = ''' +# Build the default target +- make: chdir=/home/ubuntu/cool-project + +# Run `install` target as root +- make: chdir=/home/ubuntu/cool-project target=install + become: yes + +# Pass in extra arguments to build +- make: + chdir: /home/ubuntu/cool-project + target: all + params: + NUM_THREADS: 4 + BACKEND: lapack +''' + + +def format_params(params): + return [k + '=' + str(v) for k, v in params.iteritems()] + + +def push_arguments(cmd, args): + if args['target'] != None: + cmd.append(args['target']) + if args['params'] != None: + cmd.extend(format_params(args['params'])) + return cmd + + +def check_changed(make_path, module, args): + cmd = push_arguments([make_path, '--question'], args) + rc, _, __ = module.run_command(cmd, check_rc=False, cwd=args['chdir']) + return (rc != 0) + + +def run_make(make_path, module, args): + cmd = push_arguments([make_path], args) + module.run_command(cmd, check_rc=True, cwd=args['chdir']) + + +def main(): + module = AnsibleModule( + supports_check_mode=True, + argument_spec=dict( + target=dict(required=False, default=None, type='str'), + params=dict(required=False, default=None, type='dict'), + chdir=dict(required=True, default=None, type='str'), + ), + ) + args = dict( + changed=False, + failed=False, + target=module.params['target'], + params=module.params['params'], + chdir=module.params['chdir'], + ) + make_path = module.get_bin_path('make', True) + + # Check if target is up to date + args['changed'] = check_changed(make_path, module, args) + + # Check only; don't modify + if module.check_mode: + module.exit_json(changed=args['changed']) + + # Target is already up to date + if args['changed'] == False: + module.exit_json(**args) + + run_make(make_path, module, args) + module.exit_json(**args) + +if __name__ == '__main__': + main() From b328feccb156d47d1f7334248860d22bb2dee853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Fri, 25 Mar 2016 12:18:58 +0100 Subject: [PATCH 1350/2522] make: add empty return docs --- system/make.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/system/make.py b/system/make.py index e66e3fbab6e..d375082336e 100644 --- a/system/make.py +++ b/system/make.py @@ -58,6 +58,10 @@ BACKEND: lapack ''' +# TODO: Disabled the RETURN as it was breaking docs building. Someone needs to +# fix this +RETURN = '''# ''' + def format_params(params): return [k + '=' + str(v) for k, v in params.iteritems()] From 7def4b01f5f9407c3b8bd474d9653bc3fc888cc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Fri, 25 Mar 2016 12:19:09 +0100 Subject: [PATCH 1351/2522] make: move down ansible import --- system/make.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/system/make.py b/system/make.py index d375082336e..9ac47124279 100644 --- a/system/make.py +++ b/system/make.py @@ -18,9 +18,6 @@ # You should have received a copy of the GNU General Public License # along with this software. If not, see . -# import module snippets -from ansible.module_utils.basic import * - DOCUMENTATION = ''' --- module: make @@ -118,5 +115,7 @@ def main(): run_make(make_path, module, args) module.exit_json(**args) +from ansible.module_utils.basic import * + if __name__ == '__main__': main() From 7477fe51418dbb890faba8966282ab83e597e6af Mon Sep 17 00:00:00 2001 From: Michael Gruener Date: Fri, 25 Mar 2016 18:07:50 +0100 Subject: [PATCH 1352/2522] cloudflare_dns: Fix SRV record idempotency --- network/cloudflare_dns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/cloudflare_dns.py b/network/cloudflare_dns.py index 8772c2ac659..3e5ba50b522 100644 --- a/network/cloudflare_dns.py +++ b/network/cloudflare_dns.py @@ -438,7 +438,7 @@ def ensure_dns_record(self,**kwargs): "port": params['port'], "weight": params['weight'], "priority": params['priority'], - "name": params['record'], + "name": params['record'][:-len('.' + params['zone'])], "proto": params['proto'], "service": params['service'] } From 396d44c4b3b37660b2223369488c5c506cb66af2 Mon Sep 17 00:00:00 2001 From: Michael Gruener Date: Fri, 25 Mar 2016 18:23:52 +0100 Subject: [PATCH 1353/2522] cloudflare_dns: Fix solo SRV record creation --- network/cloudflare_dns.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/network/cloudflare_dns.py b/network/cloudflare_dns.py index 3e5ba50b522..5f0955f018c 100644 --- a/network/cloudflare_dns.py +++ b/network/cloudflare_dns.py @@ -371,19 +371,21 @@ def delete_dns_records(self,**kwargs): params[param] = getattr(self,param) records = [] - search_value = params['value'] + content = params['value'] search_record = params['record'] if params['type'] == 'SRV': - search_value = str(params['weight']) + '\t' + str(params['port']) + '\t' + params['value'] + content = str(params['weight']) + '\t' + str(params['port']) + '\t' + params['value'] search_record = params['service'] + '.' + params['proto'] + '.' + params['record'] if params['solo']: search_value = None + else: + search_value = content records = self.get_dns_records(params['zone'],params['type'],search_record,search_value) for rr in records: if params['solo']: - if not ((rr['type'] == params['type']) and (rr['name'] == params['record']) and (rr['content'] == params['value'])): + if not ((rr['type'] == params['type']) and (rr['name'] == search_record) and (rr['content'] == content)): self.changed = True if not self.module.check_mode: result, info = self._cf_api_call('/zones/{0}/dns_records/{1}'.format(rr['zone_id'],rr['id']),'DELETE') From cd1114a2bd005d0f4bd0b5e9b90d68da509553be Mon Sep 17 00:00:00 2001 From: Julien Recurt Date: Fri, 25 Mar 2016 19:00:17 +0100 Subject: [PATCH 1354/2522] Add option to use ZabbixApi via auth basic protection --- monitoring/zabbix_group.py | 18 +++++++++++++++++- monitoring/zabbix_host.py | 22 +++++++++++++++++++--- monitoring/zabbix_hostmacro.py | 18 +++++++++++++++++- monitoring/zabbix_maintenance.py | 26 +++++++++++++++++++++++++- monitoring/zabbix_screen.py | 22 +++++++++++++++++++--- 5 files changed, 97 insertions(+), 9 deletions(-) diff --git a/monitoring/zabbix_group.py b/monitoring/zabbix_group.py index 4fc9631af9c..a19c49794f9 100644 --- a/monitoring/zabbix_group.py +++ b/monitoring/zabbix_group.py @@ -49,6 +49,18 @@ description: - Zabbix user password. required: true + http_login_user: + description: + - Basic Auth login + required: false + default: None + version_added: "2.1" + http_login_password: + description: + - Basic Auth password + required: false + default: None + version_added: "2.1" state: description: - Create or delete host group. @@ -153,6 +165,8 @@ def main(): server_url=dict(type='str', required=True, aliases=['url']), login_user=dict(type='str', required=True), login_password=dict(type='str', required=True, no_log=True), + http_login_user=dict(type='str',required=False, default=None), + http_login_password=dict(type='str',required=False, default=None, no_log=True), host_groups=dict(type='list', required=True, aliases=['host_group']), state=dict(default="present", choices=['present','absent']), timeout=dict(type='int', default=10) @@ -166,6 +180,8 @@ def main(): server_url = module.params['server_url'] login_user = module.params['login_user'] login_password = module.params['login_password'] + http_login_user = module.params['http_login_user'] + http_login_password = module.params['http_login_password'] host_groups = module.params['host_groups'] state = module.params['state'] timeout = module.params['timeout'] @@ -174,7 +190,7 @@ def main(): # login to zabbix try: - zbx = ZabbixAPI(server_url, timeout=timeout) + zbx = ZabbixAPI(server_url, timeout=timeout, user=http_login_user, passwd=http_login_password) zbx.login(login_user, login_password) except Exception, e: module.fail_json(msg="Failed to connect to Zabbix server: %s" % e) diff --git a/monitoring/zabbix_host.py b/monitoring/zabbix_host.py index cded12b911b..70d323138c4 100644 --- a/monitoring/zabbix_host.py +++ b/monitoring/zabbix_host.py @@ -47,6 +47,18 @@ description: - Zabbix user password. required: true + http_login_user: + description: + - Basic Auth login + required: false + default: None + version_added: "2.1" + http_login_password: + description: + - Basic Auth password + required: false + default: None + version_added: "2.1" host_name: description: - Name of the host in Zabbix. @@ -158,8 +170,8 @@ class ZabbixAPIExtends(ZabbixAPI): hostinterface = None - def __init__(self, server, timeout, **kwargs): - ZabbixAPI.__init__(self, server, timeout=timeout) + def __init__(self, server, timeout, user, passwd, **kwargs): + ZabbixAPI.__init__(self, server, timeout=timeout, user=user, passwd=passwd) self.hostinterface = ZabbixAPISubClass(self, dict({"prefix": "hostinterface"}, **kwargs)) @@ -405,6 +417,8 @@ def main(): login_user=dict(rtype='str', equired=True), login_password=dict(type='str', required=True, no_log=True), host_name=dict(type='str', required=True), + http_login_user=dict(type='str', required=False, default=None), + http_login_password=dict(type='str', required=False, default=None, no_log=True), host_groups=dict(type='list', required=False), link_templates=dict(type='list', required=False), status=dict(default="enabled", choices=['enabled', 'disabled']), @@ -424,6 +438,8 @@ def main(): server_url = module.params['server_url'] login_user = module.params['login_user'] login_password = module.params['login_password'] + http_login_user = module.params['http_login_user'] + http_login_password = module.params['http_login_password'] host_name = module.params['host_name'] host_groups = module.params['host_groups'] link_templates = module.params['link_templates'] @@ -441,7 +457,7 @@ def main(): zbx = None # login to zabbix try: - zbx = ZabbixAPIExtends(server_url, timeout=timeout) + zbx = ZabbixAPIExtends(server_url, timeout=timeout, user=http_login_user, passwd=http_login_password) zbx.login(login_user, login_password) except Exception, e: module.fail_json(msg="Failed to connect to Zabbix server: %s" % e) diff --git a/monitoring/zabbix_hostmacro.py b/monitoring/zabbix_hostmacro.py index c0b497a9d20..144e3d30a7d 100644 --- a/monitoring/zabbix_hostmacro.py +++ b/monitoring/zabbix_hostmacro.py @@ -46,6 +46,18 @@ description: - Zabbix user password. required: true + http_login_user: + description: + - Basic Auth login + required: false + default: None + version_added: "2.1" + http_login_password: + description: + - Basic Auth password + required: false + default: None + version_added: "2.1" host_name: description: - Name of the host. @@ -171,6 +183,8 @@ def main(): server_url=dict(type='str', required=True, aliases=['url']), login_user=dict(type='str', required=True), login_password=dict(type='str', required=True, no_log=True), + http_login_user=dict(type='str', required=False, default=None), + http_login_password=dict(type='str', required=False, default=None, no_log=True), host_name=dict(type='str', required=True), macro_name=dict(type='str', required=True), macro_value=dict(type='str', required=True), @@ -186,6 +200,8 @@ def main(): server_url = module.params['server_url'] login_user = module.params['login_user'] login_password = module.params['login_password'] + http_login_user = module.params['http_login_user'] + http_login_password = module.params['http_login_password'] host_name = module.params['host_name'] macro_name = (module.params['macro_name']).upper() macro_value = module.params['macro_value'] @@ -195,7 +211,7 @@ def main(): zbx = None # login to zabbix try: - zbx = ZabbixAPIExtends(server_url, timeout=timeout) + zbx = ZabbixAPIExtends(server_url, timeout=timeout, user=http_login_user, passwd=http_login_password) zbx.login(login_user, login_password) except Exception, e: module.fail_json(msg="Failed to connect to Zabbix server: %s" % e) diff --git a/monitoring/zabbix_maintenance.py b/monitoring/zabbix_maintenance.py index fe26bd144e0..1d7caad30a9 100644 --- a/monitoring/zabbix_maintenance.py +++ b/monitoring/zabbix_maintenance.py @@ -52,6 +52,18 @@ description: - Zabbix user password. required: true + http_login_user: + description: + - Basic Auth login + required: false + default: None + version_added: "2.1" + http_login_password: + description: + - Basic Auth password + required: false + default: None + version_added: "2.1" host_names: description: - Hosts to manage maintenance window for. @@ -91,6 +103,11 @@ - Type of maintenance. With data collection, or without. required: false default: "true" + timeout: + description: + - The timeout of API request (seconds). + default: 10 + version_added: "2.1" notes: - Useful for setting hosts in maintenance mode before big update, and removing maintenance window after update. @@ -260,9 +277,12 @@ def main(): host_groups=dict(type='list', required=False, default=None, aliases=['host_group']), login_user=dict(type='str', required=True), login_password=dict(type='str', required=True, no_log=True), + http_login_user=dict(type='str', required=False, default=None), + http_login_password=dict(type='str', required=False, default=None, no_log=True), name=dict(type='str', required=True), desc=dict(type='str', required=False, default="Created by Ansible"), collect_data=dict(type='bool', required=False, default=True), + timeout=dict(type='int', default=10), ), supports_check_mode=True, ) @@ -275,18 +295,22 @@ def main(): state = module.params['state'] login_user = module.params['login_user'] login_password = module.params['login_password'] + http_login_user = module.params['http_login_user'] + http_login_password = module.params['http_login_password'] minutes = module.params['minutes'] name = module.params['name'] desc = module.params['desc'] server_url = module.params['server_url'] collect_data = module.params['collect_data'] + timeout = module.params['timeout'] + if collect_data: maintenance_type = 0 else: maintenance_type = 1 try: - zbx = ZabbixAPI(server_url) + zbx = ZabbixAPI(server_url, timeout=timeout, user=http_login_user, passwd=http_login_password) zbx.login(login_user, login_password) except BaseException as e: module.fail_json(msg="Failed to connect to Zabbix server: %s" % e) diff --git a/monitoring/zabbix_screen.py b/monitoring/zabbix_screen.py index f904c550aeb..ffdcb21b5f3 100644 --- a/monitoring/zabbix_screen.py +++ b/monitoring/zabbix_screen.py @@ -48,6 +48,18 @@ description: - Zabbix user password. required: true + http_login_user: + description: + - Basic Auth login + required: false + default: None + version_added: "2.1" + http_login_password: + description: + - Basic Auth password + required: false + default: None + version_added: "2.1" timeout: description: - The timeout of API request (seconds). @@ -142,8 +154,8 @@ class ZabbixAPIExtends(ZabbixAPI): screenitem = None - def __init__(self, server, timeout, **kwargs): - ZabbixAPI.__init__(self, server, timeout=timeout) + def __init__(self, server, timeout, user, passwd, **kwargs): + ZabbixAPI.__init__(self, server, timeout=timeout, user=user, passwd=passwd) self.screenitem = ZabbixAPISubClass(self, dict({"prefix": "screenitem"}, **kwargs)) @@ -318,6 +330,8 @@ def main(): server_url=dict(type='str', required=True, aliases=['url']), login_user=dict(type='str', required=True), login_password=dict(type='str', required=True, no_log=True), + http_login_user=dict(type='str', required=False, default=None), + http_login_password=dict(type='str', required=False, default=None, no_log=True), timeout=dict(type='int', default=10), screens=dict(type='list', required=True) ), @@ -330,13 +344,15 @@ def main(): server_url = module.params['server_url'] login_user = module.params['login_user'] login_password = module.params['login_password'] + http_login_user = module.params['http_login_user'] + http_login_password = module.params['http_login_password'] timeout = module.params['timeout'] screens = module.params['screens'] zbx = None # login to zabbix try: - zbx = ZabbixAPIExtends(server_url, timeout=timeout) + zbx = ZabbixAPIExtends(server_url, timeout=timeout, user=http_login_user, passwd=http_login_password) zbx.login(login_user, login_password) except Exception, e: module.fail_json(msg="Failed to connect to Zabbix server: %s" % e) From 71961134be66e7b24983449629e82111c827d2ea Mon Sep 17 00:00:00 2001 From: Michael Gruener Date: Fri, 25 Mar 2016 19:19:11 +0100 Subject: [PATCH 1355/2522] cloudflare_dns: Allow CNAME content updates --- network/cloudflare_dns.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/network/cloudflare_dns.py b/network/cloudflare_dns.py index 5f0955f018c..88207ecf8d7 100644 --- a/network/cloudflare_dns.py +++ b/network/cloudflare_dns.py @@ -412,6 +412,14 @@ def ensure_dns_record(self,**kwargs): if (params['type'] in [ 'A','AAAA','CNAME','TXT','MX','NS','SPF']): if not params['value']: self.module.fail_json(msg="You must provide a non-empty value to create this record type") + + # there can only be one CNAME per record + # ignoring the value when searching for existing + # CNAME records allows us to update the value if it + # changes + if params['type'] == 'CNAME': + search_value = None + new_record = { "type": params['type'], "name": params['record'], @@ -468,6 +476,9 @@ def ensure_dns_record(self,**kwargs): if (cur_record['data'] > new_record['data']) - (cur_record['data'] < new_record['data']): cur_record['data'] = new_record['data'] do_update = True + if (type == 'CNAME') and (cur_record['content'] != new_record['content']): + cur_record['content'] = new_record['content'] + do_update = True if do_update: if not self.module.check_mode: result, info = self._cf_api_call('/zones/{0}/dns_records/{1}'.format(zone_id,records[0]['id']),'PUT',new_record) From 82989ce473cc3a9915a367e0d8a3ec9ec14468ea Mon Sep 17 00:00:00 2001 From: Michael Gruener Date: Fri, 25 Mar 2016 19:41:18 +0100 Subject: [PATCH 1356/2522] cloudflare_dns: Cleanup record update handling --- network/cloudflare_dns.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/network/cloudflare_dns.py b/network/cloudflare_dns.py index 88207ecf8d7..7ec106e9de0 100644 --- a/network/cloudflare_dns.py +++ b/network/cloudflare_dns.py @@ -461,23 +461,19 @@ def ensure_dns_record(self,**kwargs): # in theory this should be impossible as cloudflare does not allow # the creation of duplicate records but lets cover it anyways if len(records) > 1: - return records,self.changed - # record already exists, check if ttl must be updated + self.module.fail_json(msg="More than one record already exists for the given attributes. That should be impossible, please open an issue!") + # record already exists, check if it must be updated if len(records) == 1: cur_record = records[0] do_update = False if (params['ttl'] is not None) and (cur_record['ttl'] != params['ttl'] ): - cur_record['ttl'] = params['ttl'] do_update = True if (params['priority'] is not None) and ('priority' in cur_record) and (cur_record['priority'] != params['priority']): - cur_record['priority'] = params['priority'] do_update = True if ('data' in new_record) and ('data' in cur_record): if (cur_record['data'] > new_record['data']) - (cur_record['data'] < new_record['data']): - cur_record['data'] = new_record['data'] do_update = True if (type == 'CNAME') and (cur_record['content'] != new_record['content']): - cur_record['content'] = new_record['content'] do_update = True if do_update: if not self.module.check_mode: From 6bcd3d624b1c7b96ffdad1de8a4c2c11ea7599c8 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 26 Jan 2016 13:28:05 -0500 Subject: [PATCH 1357/2522] Add OpenStack os_user_role module --- cloud/openstack/os_user_role.py | 205 ++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 cloud/openstack/os_user_role.py diff --git a/cloud/openstack/os_user_role.py b/cloud/openstack/os_user_role.py new file mode 100644 index 00000000000..feb268fe95a --- /dev/null +++ b/cloud/openstack/os_user_role.py @@ -0,0 +1,205 @@ +#!/usr/bin/python +# Copyright (c) 2016 IBM +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + + +try: + import shade + HAS_SHADE = True +except ImportError: + HAS_SHADE = False + +from distutils.version import StrictVersion + + +DOCUMENTATION = ''' +--- +module: os_user_role +short_description: Associate OpenStack Identity users and roles +extends_documentation_fragment: openstack +author: "Monty Taylor (@emonty), David Shrewsbury (@Shrews)" +version_added: "2.1" +description: + - Grant and revoke roles in either project or domain context for + OpenStack Identity Users. +options: + role: + description: + - Name or ID for the role. + required: true + user: + description: + - Name or ID for the user. If I(user) is not specified, then + I(group) is required. Both may not be specified. + required: false + default: null + group: + description: + - Name or ID for the group. Valid only with keystone version 3. + If I(group) is not specified, then I(user) is required. Both + may not be specified. + required: false + default: null + project: + description: + - Name or ID of the project to scope the role assocation to. + If you are using keystone version 2, then this value is required. + required: false + default: null + domain: + description: + - ID of the domain to scope the role association to. Valid only with + keystone version 3, and required if I(project) is not specified. + required: false + default: null + state: + description: + - Should the roles be present or absent on the user. + choices: [present, absent] + default: present +requirements: + - "python >= 2.6" + - "shade" +''' + +EXAMPLES = ''' +# Grant an admin role on the user admin in the project project1 +- os_user_role: + cloud: mycloud + user: admin + role: admin + project: project1 + +# Revoke the admin role from the user barney in the newyork domain +- os_user_role: + cloud: mycloud + state: absent + user: barney + role: admin + domain: newyork +''' + + +def _system_state_change(state, assignment): + if state == 'present' and not assignment: + return True + elif state == 'absent' and assignment: + return True + return False + + +def _build_kwargs(user, group, project, domain): + kwargs = {} + if user: + kwargs['user'] = user + if group: + kwargs['group'] = group + if project: + kwargs['project'] = project + if domain: + kwargs['domain'] = domain + return kwargs + + +def main(): + argument_spec = openstack_full_argument_spec( + role=dict(required=True), + user=dict(required=False), + group=dict(required=False), + project=dict(required=False), + domain=dict(required=False), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = openstack_module_kwargs( + required_one_of=[ + ['user', 'group'] + ]) + module = AnsibleModule(argument_spec, + supports_check_mode=True, + **module_kwargs) + + # role grant/revoke API introduced in 1.5.0 + if not HAS_SHADE or (StrictVersion(shade.__version__) < StrictVersion('1.5.0')): + module.fail_json(msg='shade 1.5.0 or higher is required for this module') + + role = module.params.pop('role') + user = module.params.pop('user') + group = module.params.pop('group') + project = module.params.pop('project') + domain = module.params.pop('domain') + state = module.params.pop('state') + + try: + cloud = shade.operator_cloud(**module.params) + + filters = {} + + r = cloud.get_role(role) + if r is None: + module.fail_json(msg="Role %s is not valid" % role) + filters['role'] = r['id'] + + if user: + u = cloud.get_user(user) + if u is None: + module.fail_json(msg="User %s is not valid" % user) + filters['user'] = u['id'] + if group: + g = cloud.get_group(group) + if g is None: + module.fail_json(msg="Group %s is not valid" % group) + filters['group'] = g['id'] + if project: + p = cloud.get_project(project) + if p is None: + module.fail_json(msg="Project %s is not valid" % project) + filters['project'] = p['id'] + if domain: + d = cloud.get_domain(domain) + if d is None: + module.fail_json(msg="Domain %s is not valid" % domain) + filters['domain'] = d['id'] + + assignment = cloud.list_role_assignments(filters=filters) + + if module.check_mode: + module.exit_json(changed=_system_state_change(state, assignment)) + + changed = False + + if state == 'present': + if not assignment: + kwargs = _build_kwargs(user, group, project, domain) + cloud.grant_role(role, **kwargs) + changed = True + + elif state == 'absent': + if assignment: + kwargs = _build_kwargs(user, group, project, domain) + cloud.revoke_role(role, **kwargs) + changed=True + + module.exit_json(changed=changed) + + except shade.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + + +from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * + +if __name__ == '__main__': + main() From 9db7e2a4555950c38e491ec74b6b77025e3b254c Mon Sep 17 00:00:00 2001 From: Michael Gruener Date: Fri, 25 Mar 2016 21:04:19 +0100 Subject: [PATCH 1358/2522] cloudflare_dns: normalize return value and docs --- network/cloudflare_dns.py | 99 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 92 insertions(+), 7 deletions(-) diff --git a/network/cloudflare_dns.py b/network/cloudflare_dns.py index 7ec106e9de0..9afbeccff76 100644 --- a/network/cloudflare_dns.py +++ b/network/cloudflare_dns.py @@ -177,12 +177,94 @@ ''' RETURN = ''' -records: - description: > - List containing the records for a zone or the data for a newly created record. - For details see https://api.cloudflare.com/#dns-records-for-a-zone-properties. - returned: success/changed after record creation - type: list +record: + description: dictionary containing the record data + returned: success, except on record deletion + type: dictionary + contains: + content: + description: the record content (details depend on record type) + returned: success + type: string + sample: 192.168.100.20 + created_on: + description: the record creation date + returned: success + type: string + sample: 2016-03-25T19:09:42.516553Z + data: + description: additional record data + returned: success, if type is SRV + type: dictionary + sample: { + name: "jabber", + port: 8080, + priority: 10, + proto: "_tcp", + service: "_xmpp", + target: "jabberhost.sample.com", + weight: 5, + } + id: + description: the record id + returned: success + type: string + sample: f9efb0549e96abcb750de63b38c9576e + locked: + description: No documentation available + returned: success + type: boolean + sample: False + meta: + description: No documentation available + returned: success + type: dictionary + sample: { auto_added: false } + modified_on: + description: record modification date + returned: success + type: string + sample: 2016-03-25T19:09:42.516553Z + name: + description: the record name as FQDN (including _service and _proto for SRV) + returned: success + type: string + sample: www.sample.com + priority: + description: priority of the MX record + returned: success, if type is MX + type: int + sample: 10 + proxiable: + description: whether this record can be proxied through cloudflare + returned: success + type: boolean + sample: False + proxied: + description: whether the record is proxied through cloudflare + returned: success + type: boolean + sample: False + ttl: + description: the time-to-live for the record + returned: success + type: int + sample: 300 + type: + description: the record type + returned: success + type: string + sample: A + zone_id: + description: the id of the zone containing the record + returned: success + type: string + sample: abcede0bf9f0066f94029d2e6b73856a + zone_name: + description: the name of the zone containing the record + returned: success + type: string + sample: sample.com ''' class CloudflareAPI(object): @@ -538,7 +620,10 @@ def main(): if cf_api.is_solo: changed = cf_api.delete_dns_records(solo=cf_api.is_solo) result,changed = cf_api.ensure_dns_record() - module.exit_json(changed=changed,result={'records': result}) + if isinstance(result,list): + module.exit_json(changed=changed,result={'record': result[0]}) + else: + module.exit_json(changed=changed,result={'record': result}) else: # force solo to False, just to be sure changed = cf_api.delete_dns_records(solo=False) From 46cebbb83d8d7e86eabad97eeb681515f47a7408 Mon Sep 17 00:00:00 2001 From: Michael Gruener Date: Fri, 25 Mar 2016 21:08:25 +0100 Subject: [PATCH 1359/2522] cloudflare_dns: Cosmetic cleanup --- network/cloudflare_dns.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/network/cloudflare_dns.py b/network/cloudflare_dns.py index 9afbeccff76..238e7dff98b 100644 --- a/network/cloudflare_dns.py +++ b/network/cloudflare_dns.py @@ -3,7 +3,7 @@ # (c) 2016 Michael Gruener # -# This file is (intends to be) part of Ansible +# This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -114,7 +114,7 @@ ''' EXAMPLES = ''' -# create a test.my.com A record to point to 127.0.0.01 +# create a test.my.com A record to point to 127.0.0.1 - cloudflare_dns: zone: my.com record: test @@ -428,7 +428,6 @@ def get_dns_records(self,zone_name=None,type=None,record=None,value=''): if (not value) and (value is not None): value = self.value - zone_id = self._get_zone_id() api_call = '/zones/{0}/dns_records'.format(zone_id) query = {} From c2263147702ebe6ee3e1babbde1dc94fa94305f8 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sat, 26 Mar 2016 09:19:47 +0100 Subject: [PATCH 1360/2522] Add proper type to cpanm arguments from_path, locallib, executable should be path to benefits from path expansion for ~user. --- packaging/language/cpanm.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packaging/language/cpanm.py b/packaging/language/cpanm.py index a017aaf5791..919677466ab 100644 --- a/packaging/language/cpanm.py +++ b/packaging/language/cpanm.py @@ -170,15 +170,15 @@ def _get_cpanm_path(module): def main(): arg_spec = dict( name=dict(default=None, required=False, aliases=['pkg']), - from_path=dict(default=None, required=False), + from_path=dict(default=None, required=False, type='path'), notest=dict(default=False, type='bool'), - locallib=dict(default=None, required=False), + locallib=dict(default=None, required=False, type='path'), mirror=dict(default=None, required=False), mirror_only=dict(default=False, type='bool'), installdeps=dict(default=False, type='bool'), system_lib=dict(default=False, type='bool', aliases=['use_sudo']), version=dict(default=None, required=False), - executable=dict(required=False, type='str'), + executable=dict(required=False, type='path'), ) module = AnsibleModule( From 2e8cd5cd74f18cbebadd0aa6a397f4ac8ba9ec77 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sun, 27 Mar 2016 21:08:05 +0200 Subject: [PATCH 1361/2522] Use type 'path' for rootdir, for pkgng --- packaging/os/pkgng.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/os/pkgng.py b/packaging/os/pkgng.py index ad097aae0df..8936032c3b1 100644 --- a/packaging/os/pkgng.py +++ b/packaging/os/pkgng.py @@ -271,7 +271,7 @@ def main(): cached = dict(default=False, type='bool'), annotation = dict(default="", required=False), pkgsite = dict(default="", required=False), - rootdir = dict(default="", required=False)), + rootdir = dict(default="", required=False, type='path')), supports_check_mode = True) pkgng_path = module.get_bin_path('pkg', True) From 9853caa536a57b43d1d7b96a506c75b21ca6d062 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Mon, 28 Mar 2016 09:14:57 +0200 Subject: [PATCH 1362/2522] Use boolean instead of "yes" + choice for most option This enable a more standard behavior with others modules --- packaging/os/portage.py | 66 ++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/packaging/os/portage.py b/packaging/os/portage.py index 7be55db3ca8..4e8507fedf5 100644 --- a/packaging/os/portage.py +++ b/packaging/os/portage.py @@ -76,29 +76,29 @@ description: - Do not add the packages to the world file (--oneshot) required: false - default: null - choices: [ "yes" ] + default: False + choices: [ "yes", "no" ] noreplace: description: - Do not re-emerge installed packages (--noreplace) required: false - default: null - choices: [ "yes" ] + default: False + choices: [ "yes", "no" ] nodeps: description: - Only merge packages but not their dependencies (--nodeps) required: false - default: null - choices: [ "yes" ] + default: False + choices: [ "yes", "no" ] onlydeps: description: - Only merge packages' dependencies but not the packages (--onlydeps) required: false - default: null - choices: [ "yes" ] + default: False + choices: [ "yes", "no" ] depclean: description: @@ -106,22 +106,22 @@ - If no package is specified, clean up the world's dependencies - Otherwise, --depclean serves as a dependency aware version of --unmerge required: false - default: null - choices: [ "yes" ] + default: False + choices: [ "yes", "no" ] quiet: description: - Run emerge in quiet mode (--quiet) required: false - default: null - choices: [ "yes" ] + default: False + choices: [ "yes", "no" ] verbose: description: - Run emerge in verbose mode (--verbose) required: false - default: null - choices: [ "yes" ] + default: False + choices: [ "yes", "no" ] sync: description: @@ -130,21 +130,21 @@ - If web, perform "emerge-webrsync" required: false default: null - choices: [ "yes", "web" ] + choices: [ "yes", "web", "no" ] getbinpkg: description: - Prefer packages specified at PORTAGE_BINHOST in make.conf required: false - default: null - choices: [ "yes" ] + default: False + choices: [ "yes", "no" ] usepkgonly: description: - Merge only binaries (no compiling). This sets getbinpkg=yes. required: false - deafult: null - choices: [ "yes" ] + default: False + choices: [ "yes", "no" ] requirements: [ gentoolkit ] author: @@ -401,21 +401,21 @@ def main(): default=portage_present_states[0], choices=portage_present_states + portage_absent_states, ), - update=dict(default=None, choices=['yes']), - deep=dict(default=None, choices=['yes']), - newuse=dict(default=None, choices=['yes']), - changed_use=dict(default=None, choices=['yes']), - oneshot=dict(default=None, choices=['yes']), - noreplace=dict(default=None, choices=['yes']), - nodeps=dict(default=None, choices=['yes']), - onlydeps=dict(default=None, choices=['yes']), - depclean=dict(default=None, choices=['yes']), - quiet=dict(default=None, choices=['yes']), - verbose=dict(default=None, choices=['yes']), + update=dict(default=False, type='bool'), + deep=dict(default=False, type='bool'), + newuse=dict(default=False, type='bool'), + changed_use=dict(default=False, type='bool'), + oneshot=dict(default=False, type='bool'), + noreplace=dict(default=False, type='bool'), + nodeps=dict(default=False, type='bool'), + onlydeps=dict(default=False, type='bool'), + depclean=dict(default=False, type='bool'), + quiet=dict(default=False, type='bool'), + verbose=dict(default=False, type='bool'), sync=dict(default=None, choices=['yes', 'web']), - getbinpkg=dict(default=None, choices=['yes']), - usepkgonly=dict(default=None, choices=['yes']), - usepkg=dict(default=None, choices=['yes']), + getbinpkg=dict(default=False, type='bool'), + usepkgonly=dict(default=False, type='bool'), + usepkg=dict(default=False, type='bool'), ), required_one_of=[['package', 'sync', 'depclean']], mutually_exclusive=[['nodeps', 'onlydeps'], ['quiet', 'verbose']], From 3e31c2408d66e9d32284f766ce63cebf9683fbc5 Mon Sep 17 00:00:00 2001 From: Jasper Lievisse Adriaanse Date: Thu, 10 Mar 2016 09:37:21 +0100 Subject: [PATCH 1363/2522] support for 'update_cache' in pkgin module --- packaging/os/pkgin.py | 44 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) mode change 100644 => 100755 packaging/os/pkgin.py diff --git a/packaging/os/pkgin.py b/packaging/os/pkgin.py old mode 100644 new mode 100755 index f75adf8299c..5277f218242 --- a/packaging/os/pkgin.py +++ b/packaging/os/pkgin.py @@ -42,24 +42,38 @@ description: - Name of package to install/remove; - multiple names may be given, separated by commas - required: true + required: false + default: null state: description: - Intended state of the package choices: [ 'present', 'absent' ] required: false default: present + update_cache: + description: + - Update repository database. Can be run with other steps or on it's own. + required: false + default: no + choices: [ "yes", "no" ] + version_added: "2.1" ''' EXAMPLES = ''' # install package foo - pkgin: name=foo state=present +# Update database and install "foo" package +- pkgin: name=foo update_cache=yes + # remove package foo - pkgin: name=foo state=absent # remove packages foo and bar - pkgin: name=foo,bar state=absent + +# Update repositories as a separate step +- pkgin: update_cache=yes ''' @@ -148,7 +162,13 @@ def format_action_message(module, action, count): return message + "s" -def format_pkgin_command(module, pkgin_path, command, package): +def format_pkgin_command(module, pkgin_path, command, package=None): + # Not all commands take a package argument, so cover this up by passing + # an empty string. Some commands (e.g. 'update') will ignore extra + # arguments, however this behaviour cannot be relied on for others. + if package is None: + package = "" + vars = { "pkgin": pkgin_path, "command": command, "package": package } @@ -204,13 +224,23 @@ def install_packages(module, pkgin_path, packages): module.exit_json(changed=False, msg="package(s) already present") +def update_package_db(module, pkgin_path): + rc, out, err = module.run_command( + format_pkgin_command(module, pkgin_path, "update")) + + if rc == 0: + return True + else: + module.fail_json(msg="could not update package db") def main(): module = AnsibleModule( argument_spec = dict( state = dict(default="present", choices=["present","absent"]), - name = dict(aliases=["pkg"], required=True, type='list')), + name = dict(aliases=["pkg"], type='list'), + update_cache = dict(default='no', type='bool')), + required_one_of = [['name', 'update_cache']], supports_check_mode = True) pkgin_path = module.get_bin_path('pkgin', True, ['/opt/local/bin']) @@ -219,6 +249,11 @@ def main(): pkgs = p["name"] + if p["update_cache"]: + update_package_db(module, pkgin_path) + if not p['name']: + module.exit_json(changed=True, msg='updated repository database') + if p["state"] == "present": install_packages(module, pkgin_path, pkgs) @@ -228,4 +263,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() From b7dad3494e62e18232e1b6523a626293a8c7cb58 Mon Sep 17 00:00:00 2001 From: Jason Witkowski Date: Tue, 29 Mar 2016 17:01:52 -0400 Subject: [PATCH 1364/2522] The current module supporting F5 BIGIP pool creation does not support a setup where the port number must be zero to signify the pool will listen on multiple ports. This change implements that functionality and fixes an illogical conditional. --- network/f5/bigip_pool.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/network/f5/bigip_pool.py b/network/f5/bigip_pool.py index f65a13797fb..47ae941c44a 100644 --- a/network/f5/bigip_pool.py +++ b/network/f5/bigip_pool.py @@ -393,11 +393,11 @@ def main(): # sanity check user supplied values - if (host and not port) or (port and not host): + if (host and port is None) or (port is not None and not host): module.fail_json(msg="both host and port must be supplied") - if 1 > port > 65535: - module.fail_json(msg="valid ports must be in range 1 - 65535") + if 0 > port or port > 65535: + module.fail_json(msg="valid ports must be in range 0 - 65535") if monitors: if len(monitors) == 1: @@ -508,6 +508,10 @@ def main(): if not module.check_mode: add_pool_member(api, pool, address, port) result = {'changed': True} + if (host and port == 0) and not member_exists(api, pool, address, port): + if not module.check_mode: + add_pool_member(api, pool, address, port) + result = {'changed': True} except Exception, e: module.fail_json(msg="received exception: %s" % e) From 25d7126852bb6e442b8a290a1c5079b2263b25e0 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Wed, 30 Mar 2016 12:56:20 +0200 Subject: [PATCH 1365/2522] openstack: doc: add return doc, fixes build --- cloud/openstack/os_user_role.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cloud/openstack/os_user_role.py b/cloud/openstack/os_user_role.py index feb268fe95a..c034908f866 100644 --- a/cloud/openstack/os_user_role.py +++ b/cloud/openstack/os_user_role.py @@ -91,6 +91,9 @@ domain: newyork ''' +RETURN = ''' +# +''' def _system_state_change(state, assignment): if state == 'present' and not assignment: From 71b0067aa6dcfeef0e3505ce63075b24d3d2146a Mon Sep 17 00:00:00 2001 From: Evgeni Golov Date: Wed, 30 Mar 2016 17:07:10 +0200 Subject: [PATCH 1366/2522] do not set a default config for lxc containers otherwise deploying user-containers fail as these require information from ~/.config/lxc/default.conf that the LXC tools will load if no --config was supplied Signed-off-by: Evgeni Golov --- cloud/lxc/lxc_container.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index ab207012329..5ceac066857 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -57,7 +57,6 @@ description: - Path to the LXC configuration file. required: false - default: /etc/lxc/default.conf lv_name: description: - Name of the logical volume, defaults to the container name. @@ -1687,7 +1686,6 @@ def main(): ), config=dict( type='str', - default='/etc/lxc/default.conf' ), vg_name=dict( type='str', From 8a27e785db498e5135c446ab00a1365694e42d75 Mon Sep 17 00:00:00 2001 From: Matt Hite Date: Wed, 30 Mar 2016 09:29:34 -0700 Subject: [PATCH 1367/2522] Allow port 0 as a valid pool member port --- network/f5/bigip_pool_member.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/network/f5/bigip_pool_member.py b/network/f5/bigip_pool_member.py index c0337180e5b..81bcffdb4c0 100644 --- a/network/f5/bigip_pool_member.py +++ b/network/f5/bigip_pool_member.py @@ -341,11 +341,11 @@ def main(): # sanity check user supplied values - if (host and not port) or (port and not host): + if (host and port is None) or (port is not None and not host): module.fail_json(msg="both host and port must be supplied") - if 1 > port > 65535: - module.fail_json(msg="valid ports must be in range 1 - 65535") + if 0 > port or port > 65535: + module.fail_json(msg="valid ports must be in range 0 - 65535") try: api = bigip_api(server, user, password, validate_certs) @@ -427,4 +427,3 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.f5 import * main() - From d9b8043b4ad1ec8d7436056aadc7eb0c25be16ca Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Wed, 30 Mar 2016 21:49:58 +0200 Subject: [PATCH 1368/2522] Use type='path' for reposdir, since that's a path --- packaging/os/yum_repository.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/os/yum_repository.py b/packaging/os/yum_repository.py index 686c8739ba1..a0060934cc4 100644 --- a/packaging/os/yum_repository.py +++ b/packaging/os/yum_repository.py @@ -662,7 +662,7 @@ def main(): proxy_password=dict(no_log=True), proxy_username=dict(), repo_gpgcheck=dict(type='bool'), - reposdir=dict(default='/etc/yum.repos.d'), + reposdir=dict(default='/etc/yum.repos.d', type='path'), retries=dict(), s3_enabled=dict(type='bool'), skip_if_unavailable=dict(type='bool'), From 950e2d94843f01217f6e7a03752f94caeb59aaf4 Mon Sep 17 00:00:00 2001 From: John Barker Date: Thu, 31 Mar 2016 20:15:32 +0100 Subject: [PATCH 1369/2522] restore version_added in dynamodb_table.py --- cloud/amazon/dynamodb_table.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud/amazon/dynamodb_table.py b/cloud/amazon/dynamodb_table.py index b0c335a16ab..ceafbdea9b6 100644 --- a/cloud/amazon/dynamodb_table.py +++ b/cloud/amazon/dynamodb_table.py @@ -18,6 +18,7 @@ --- module: dynamodb_table short_description: Create, update or delete AWS Dynamo DB tables. +version_added: "2.0" description: - Create or delete AWS Dynamo DB tables. - Can update the provisioned throughput on existing tables. From da84e2e9b83be6ebebbfd3be6776f391622c02fe Mon Sep 17 00:00:00 2001 From: Chris Porter Date: Thu, 31 Mar 2016 22:55:44 +0100 Subject: [PATCH 1370/2522] fix security vulnerability in lxc module octal/decimal confusion makes file world-writable before executing it --- cloud/lxc/lxc_container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index ab207012329..50d3cedd596 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -571,7 +571,7 @@ def create_script(command): f.close() # Ensure the script is executable. - os.chmod(script_file, 1755) + os.chmod(script_file, 0700) # Get temporary directory. tempdir = tempfile.gettempdir() From 031f98e86c93885d7cd4b7dee81d61037468fd71 Mon Sep 17 00:00:00 2001 From: David Hocky Date: Thu, 31 Mar 2016 18:37:37 -0400 Subject: [PATCH 1371/2522] fix dscp marking documentation in iptables module --- system/iptables.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/iptables.py b/system/iptables.py index 10167f17655..c12817005dc 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -216,7 +216,7 @@ description: - "This allows specifying a DSCP mark to be added to packets. It takes either an integer or hex value. Mutually exclusive with - C(dscp_mark_class)." + C(set_dscp_mark_class)." required: false default: null set_dscp_mark_class: @@ -224,7 +224,7 @@ description: - "This allows specifying a predefined DiffServ class which will be translated to the corresponding DSCP mark. Mutually exclusive with - C(dscp_mark)." + C(set_dscp_mark)." required: false default: null comment: From 185bcbd8f78baac2a7b2db57fd62179dda5f19b8 Mon Sep 17 00:00:00 2001 From: Evgeni Golov Date: Fri, 1 Apr 2016 11:04:35 +0200 Subject: [PATCH 1372/2522] explicitly set "default: null" in the docs --- cloud/lxc/lxc_container.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index 5ceac066857..57cda6157ce 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -57,6 +57,7 @@ description: - Path to the LXC configuration file. required: false + default: null lv_name: description: - Name of the logical volume, defaults to the container name. From d4c73059fe23db2ae0b5ebd43240ce2eca81715b Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Fri, 1 Apr 2016 13:28:25 -0500 Subject: [PATCH 1373/2522] Rebase PRs against $TRAVIS_BRANCH before performing tests --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 5e09a9a447f..4dc70de2f64 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,10 @@ addons: packages: - python2.4 - python2.6 +before_install: + - git config user.name "ansible" + - git config user.email "ansible@ansible.com" + - if [[ "$TRAVIS_PULL_REQUEST" != "false" ]]; then git rebase $TRAVIS_BRANCH; fi; install: - pip install git+https://github.com/ansible/ansible.git@devel#egg=ansible - pip install git+https://github.com/sivel/ansible-testing.git#egg=ansible_testing From f3c168594a396ec753d48b0963c50b740dc9639d Mon Sep 17 00:00:00 2001 From: Chulki Lee Date: Fri, 1 Apr 2016 17:45:33 -0700 Subject: [PATCH 1374/2522] osx_defaults: fix datetime Fix #1742 --- system/osx_defaults.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/osx_defaults.py b/system/osx_defaults.py index 2415e76f736..0e980b30394 100644 --- a/system/osx_defaults.py +++ b/system/osx_defaults.py @@ -83,7 +83,7 @@ - osx_defaults: domain=com.geekchimp.macable key=ExampleKeyToRemove state=absent ''' -from datetime import datetime +import datetime # exceptions --------------------------------------------------------------- {{{ class OSXDefaultsException(Exception): @@ -141,7 +141,7 @@ def _convert_type(self, type, value): raise OSXDefaultsException("Invalid boolean value: {0}".format(repr(value))) elif type == "date": try: - return datetime.strptime(value.split("+")[0].strip(), "%Y-%m-%d %H:%M:%S") + return datetime.datetime.strptime(value.split("+")[0].strip(), "%Y-%m-%d %H:%M:%S") except ValueError: raise OSXDefaultsException( "Invalid date value: {0}. Required format yyy-mm-dd hh:mm:ss.".format(repr(value)) @@ -240,7 +240,7 @@ def write(self): value = str(self.value) elif self.array_add and self.current_value is not None: value = list(set(self.value) - set(self.current_value)) - elif isinstance(self.value, datetime): + elif isinstance(self.value, datetime.datetime): value = self.value.strftime('%Y-%m-%d %H:%M:%S') else: value = self.value From 7c3999a92a1cd856ff9bc8913a93ff1aee8bffc3 Mon Sep 17 00:00:00 2001 From: Evgeni Golov Date: Sat, 2 Apr 2016 10:20:42 +0200 Subject: [PATCH 1375/2522] do not use a predictable filenames in the LXC plugin * do not use a predictable filename for the LXC attach script * don't use predictable filenames for LXC attach script logging * don't set a predictable archive_path this should prevent symlink attacks which could result in * data corruption * data leakage * privilege escalation --- cloud/lxc/lxc_container.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index adacece555f..ae583fe4d72 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -144,7 +144,7 @@ description: - Path the save the archived container. If the path does not exist the archive method will attempt to create it. - default: /tmp + default: null archive_compression: choices: - gzip @@ -557,13 +557,8 @@ def create_script(command): import subprocess import tempfile - # Ensure that the directory /opt exists. - if not path.isdir('/opt'): - os.mkdir('/opt') - - # Create the script. - script_file = path.join('/opt', '.lxc-attach-script') - f = open(script_file, 'wb') + (fd, script_file) = tempfile.mkstemp(prefix='lxc-attach-script') + f = os.fdopen(fd, 'wb') try: f.write(ATTACH_TEMPLATE % {'container_command': command}) f.flush() @@ -573,14 +568,11 @@ def create_script(command): # Ensure the script is executable. os.chmod(script_file, 0700) - # Get temporary directory. - tempdir = tempfile.gettempdir() - # Output log file. - stdout_file = open(path.join(tempdir, 'lxc-attach-script.log'), 'ab') + stdout_file = os.fdopen(tempfile.mkstemp(prefix='lxc-attach-script-log')[0], 'ab') # Error log file. - stderr_file = open(path.join(tempdir, 'lxc-attach-script.err'), 'ab') + stderr_file = os.fdopen(tempfile.mkstemp(prefix='lxc-attach-script-err')[0], 'ab') # Execute the script command. try: @@ -1747,7 +1739,6 @@ def main(): ), archive_path=dict( type='str', - default='/tmp' ), archive_compression=dict( choices=LXC_COMPRESSION_MAP.keys(), @@ -1755,6 +1746,9 @@ def main(): ) ), supports_check_mode=False, + required_if = ([ + ('archive', True, ['archive_path']) + ]), ) if not HAS_LXC: From 5696e6c33aa246ff620bd383f82281aafac40571 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sun, 3 Apr 2016 10:34:10 +0200 Subject: [PATCH 1376/2522] Do not leak passwords in case of error in cloudstack modules --- cloud/cloudstack/cs_account.py | 2 +- cloud/cloudstack/cs_user.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/cloudstack/cs_account.py b/cloud/cloudstack/cs_account.py index 80ab6748bb0..0313006f894 100644 --- a/cloud/cloudstack/cs_account.py +++ b/cloud/cloudstack/cs_account.py @@ -354,7 +354,7 @@ def main(): first_name = dict(default=None), last_name = dict(default=None), username = dict(default=None), - password = dict(default=None), + password = dict(default=None, no_log=True), timezone = dict(default=None), poll_async = dict(type='bool', default=True), )) diff --git a/cloud/cloudstack/cs_user.py b/cloud/cloudstack/cs_user.py index a0be4634904..0b2a1fddc63 100644 --- a/cloud/cloudstack/cs_user.py +++ b/cloud/cloudstack/cs_user.py @@ -413,7 +413,7 @@ def main(): email = dict(default=None), first_name = dict(default=None), last_name = dict(default=None), - password = dict(default=None), + password = dict(default=None, no_log=True), timezone = dict(default=None), poll_async = dict(type='bool', default=True), )) From b5333ba08c6795b075bd6d3f80aaac586ff0be52 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Mon, 4 Apr 2016 15:14:56 +0200 Subject: [PATCH 1377/2522] Set no log for jabber.py password --- notification/jabber.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notification/jabber.py b/notification/jabber.py index 6d9c19b789c..840954658f8 100644 --- a/notification/jabber.py +++ b/notification/jabber.py @@ -101,7 +101,7 @@ def main(): module = AnsibleModule( argument_spec=dict( user=dict(required=True), - password=dict(required=True), + password=dict(required=True, no_log=True), to=dict(required=True), msg=dict(required=True), host=dict(required=False), From 95e07d2f510493ae5624d6273d7fbc10ad474e4d Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Mon, 4 Apr 2016 15:18:34 +0200 Subject: [PATCH 1378/2522] Use no_log=True for campfire module to avoid leaks --- notification/campfire.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notification/campfire.py b/notification/campfire.py index 7615f551f7c..7871747becd 100644 --- a/notification/campfire.py +++ b/notification/campfire.py @@ -73,7 +73,7 @@ def main(): module = AnsibleModule( argument_spec=dict( subscription=dict(required=True), - token=dict(required=True), + token=dict(required=True, no_log=True), room=dict(required=True), msg=dict(required=True), notify=dict(required=False, From 719b9b229bec4d6589a6a7a7c801001cd7931f58 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Mon, 4 Apr 2016 15:31:12 +0200 Subject: [PATCH 1379/2522] Prevent password leaks in notification/irc --- notification/irc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notification/irc.py b/notification/irc.py index d87d26d367e..95cd5bba8c2 100644 --- a/notification/irc.py +++ b/notification/irc.py @@ -259,9 +259,9 @@ def main(): "light_gray", "none"]), style=dict(default="none", choices=["underline", "reverse", "bold", "italic", "none"]), channel=dict(required=False), - key=dict(), + key=dict(no_log=True), topic=dict(), - passwd=dict(), + passwd=dict(no_log=True), timeout=dict(type='int', default=30), part=dict(type='bool', default=True), use_ssl=dict(type='bool', default=False) From 6bfd2846f853b9beaeb01da6206d8ffa4abe7a4c Mon Sep 17 00:00:00 2001 From: Evgeni Golov Date: Mon, 4 Apr 2016 17:28:22 +0200 Subject: [PATCH 1380/2522] don't create world-readable archives of LXC containers with the default umask tar will create a world-readable archive of the container, which may contain sensitive data Signed-off-by: Evgeni Golov --- cloud/lxc/lxc_container.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index ae583fe4d72..fb24fbf7644 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -1366,6 +1366,8 @@ def _create_tar(self, source_dir): :type source_dir: ``str`` """ + old_umask = os.umask(0077) + archive_path = self.module.params.get('archive_path') if not os.path.isdir(archive_path): os.makedirs(archive_path) @@ -1396,6 +1398,9 @@ def _create_tar(self, source_dir): build_command=build_command, unsafe_shell=True ) + + os.umask(old_umask) + if rc != 0: self.failure( err=err, From 204b4bab56a25f13b0eeaa33f5b247fd2ea79ca3 Mon Sep 17 00:00:00 2001 From: Andy Baker Date: Mon, 4 Apr 2016 19:18:00 +0100 Subject: [PATCH 1381/2522] type should be 'list' not the default of 'string' --- cloud/webfaction/webfaction_site.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/webfaction/webfaction_site.py b/cloud/webfaction/webfaction_site.py index 1f1a4bf9030..bd5504b6b46 100644 --- a/cloud/webfaction/webfaction_site.py +++ b/cloud/webfaction/webfaction_site.py @@ -115,8 +115,8 @@ def main(): # You can specify an IP address or hostname. host = dict(required=True), https = dict(required=False, type='bool', default=False), - subdomains = dict(required=False, default=[]), - site_apps = dict(required=False, default=[]), + subdomains = dict(required=False, type='list', default=[]), + site_apps = dict(required=False, type='list', default=[]), login_name = dict(required=True), login_password = dict(required=True), ), From e2138c7e14b17cf3cccf8a67749dd1a21ce190a1 Mon Sep 17 00:00:00 2001 From: Aaron Brady Date: Wed, 6 Apr 2016 11:30:59 +0100 Subject: [PATCH 1382/2522] Add `to_destination` parameter --- system/iptables.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/system/iptables.py b/system/iptables.py index c12817005dc..a714fe4699a 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -211,6 +211,13 @@ sctp." required: false default: null + to_destination: + version_added: "2.1" + description: + - "This specifies a destination address to use with DNAT: without + this, the destination address is never altered." + required: false + default: null set_dscp_mark: version_added: "2.1" description: @@ -313,6 +320,7 @@ def construct_rule(params): append_param(rule, params['destination'], '-d', False) append_param(rule, params['match'], '-m', True) append_param(rule, params['jump'], '-j', False) + append_param(rule, params['to_destination'], '--to-destination', False) append_param(rule, params['goto'], '-g', False) append_param(rule, params['in_interface'], '-i', False) append_param(rule, params['out_interface'], '-o', False) @@ -372,6 +380,7 @@ def main(): protocol=dict(required=False, default=None, type='str'), source=dict(required=False, default=None, type='str'), destination=dict(required=False, default=None, type='str'), + to_destination=dict(required=False, default=None, type='str'), match=dict(required=False, default=[], type='list'), jump=dict(required=False, default=None, type='str'), goto=dict(required=False, default=None, type='str'), From 84f2aa6167f2b8e537990067169929ca8823fb31 Mon Sep 17 00:00:00 2001 From: Rob Date: Wed, 6 Apr 2016 23:37:52 +1000 Subject: [PATCH 1383/2522] Updated Amazon module guidelines regarding boto3 * Updated Amazon module guidelines regarding boto3 * Spelling correction --- cloud/amazon/GUIDELINES.md | 230 +++++++++++++++++++++++++++++-------- 1 file changed, 182 insertions(+), 48 deletions(-) diff --git a/cloud/amazon/GUIDELINES.md b/cloud/amazon/GUIDELINES.md index ee5aea90ef7..0c831946be6 100644 --- a/cloud/amazon/GUIDELINES.md +++ b/cloud/amazon/GUIDELINES.md @@ -1,8 +1,21 @@ -Guidelines for AWS modules --------------------------- +# Guidelines for AWS modules -Naming your module -================== +## Getting Started + +Since Ansible 2.0, it is a requirement that all new AWS modules are written to use boto3. + +Prior to 2.0, modules may of been written in boto or boto3. Modules written using boto can continue to be extended using boto. + +Backward compatibility of older modules must be maintained. + +## Bug fixing + +If you are writing a bugfix for a module that uses boto, you should continue to use boto to maintain backward compatibility. + +If you are adding new functionality to an existing module that uses boto but the new functionality requires boto3, you +must maintain backward compatibility of the module and ensure the module still works without boto3. + +## Naming your module Base the name of the module on the part of AWS that you actually use. (A good rule of thumb is to take @@ -13,76 +26,197 @@ known abbreviation due to it being a major component of AWS, that's fine, but don't create new ones independently (e.g. VPC, ELB, etc. are fine) -Using boto -========== +## Adding new features + +Try and keep backward compatibility with relatively recent +versions of boto. That means that if want to implement some +functionality that uses a new feature of boto, it should only +fail if that feature actually needs to be run, with a message +saying which version of boto is needed. -Wrap the `import` statements in a try block and fail the -module later on if the import fails +Use feature testing (e.g. `hasattr('boto.module', 'shiny_new_method')`) +to check whether boto supports a feature rather than version checking +e.g. from the `ec2` module: +```python +if boto_supports_profile_name_arg(ec2): + params['instance_profile_name'] = instance_profile_name +else: + if instance_profile_name is not None: + module.fail_json(msg="instance_profile_name parameter requires boto version 2.5.0 or higher") ``` + +## Using boto and boto3 + +### Importing + +Wrap import statements in a try block and fail the module later if the import fails + +#### boto + +```python try: - import boto - import boto.module.that.you.use + import boto.ec2 + from boto.exception import BotoServerError HAS_BOTO = True except ImportError: HAS_BOTO = False - - def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - module_specific_parameter=dict(), - ) - ) - - module = AnsibleModule( - argument_spec=argument_spec, - ) + if not HAS_BOTO: module.fail_json(msg='boto required for this module') ``` +#### boto3 -Try and keep backward compatibility with relatively recent -versions of boto. That means that if want to implement some -functionality that uses a new feature of boto, it should only -fail if that feature actually needs to be run, with a message -saying which version of boto is needed. +```python +try: + import boto3 + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False -Use feature testing (e.g. `hasattr('boto.module', 'shiny_new_method')`) -to check whether boto supports a feature rather than version checking +def main(): -e.g. from the `ec2` module: + if not HAS_BOTO: + module.fail_json(msg='boto required for this module') ``` -if boto_supports_profile_name_arg(ec2): - params['instance_profile_name'] = instance_profile_name -else: - if instance_profile_name is not None: - module.fail_json( - msg="instance_profile_name parameter requires Boto version 2.5.0 or higher") + +#### boto and boto3 combined + +If you want to add boto3 functionality to a module written using boto, you must maintain backward compatibility. +Ensure that you clearly document if a new parameter requires boto3. Import boto3 at the top of the +module as normal and then use the HAS_BOTO3 bool when necessary, before the new feature. + +```python +try: + import boto + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +try: + import boto3 + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + +if my_new_feauture_Parameter_is_set: + if HAS_BOTO3: + # do feature + else: + module.fail_json(msg="boto3 is required for this feature") ``` +### Connecting to AWS + +To connect to AWS, you should use `get_aws_connection_info` and then +`connect_to_aws`. + +The reason for using `get_aws_connection_info` and `connect_to_aws` rather than doing it +yourself is that they handle some of the more esoteric connection +options such as security tokens and boto profiles. + +Some boto services require region to be specified. You should check for the region parameter if required. -Connecting to AWS -================= +#### boto -For EC2 you can just use +An example of connecting to ec2: +```python +region, ec2_url, aws_connect_params = get_aws_connection_info(module) +if region: + try: + connection = connect_to_aws(boto.ec2, region, **aws_connect_params) + except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: + module.fail_json(msg=str(e)) +else: + module.fail_json(msg="region must be specified") ``` -ec2 = ec2_connect(module) + +#### boto3 + +An example of connecting to ec2 is shown below. Note that there is no 'NoAuthHandlerFound' exception handling like in boto. +Instead, an AuthFailure exception will be thrown when you use 'connection'. See exception handling. + +```python +region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) +if region: + connection = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_params) +else: + module.fail_json(msg="region must be specified") ``` -For other modules, you should use `get_aws_connection_info` and then -`connect_to_aws`. To connect to an example `xyz` service: +### Exception Handling + +You should wrap any boto call in a try block. If an exception is thrown, it is up to you decide how to handle it +but usually calling fail_json with the error message will suffice. + +#### boto + +```python +# Import BotoServerError +try: + import boto.ec2 + from boto.exception import BotoServerError + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +# Connect to AWS +... +# Make a call to AWS +try: + result = connection.aws_call() +except BotoServerError, e: + module.fail_json(msg=e.message) ``` -region, ec2_url, aws_connect_params = get_aws_connection_info(module) -xyz = connect_to_aws(boto.xyz, region, **aws_connect_params) + +#### boto3 + +For more information on botocore exception handling see [http://botocore.readthedocs.org/en/latest/client_upgrades.html#error-handling] + +```python +# Import ClientError from botocore +try: + from botocore.exceptions import ClientError + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + +# Connect to AWS +... + +# Make a call to AWS +try: + result = connection.aws_call() +except ClientError, e: + module.fail_json(msg=e.message) ``` -The reason for using `get_aws_connection_info` and `connect_to_aws` -(and even `ec2_connect` uses those under the hood) rather than doing it -yourself is that they handle some of the more esoteric connection -options such as security tokens and boto profiles. +### Helper functions + +Along with the connection functions in Ansible ec2.py module_utils, there are some other useful functions detailed below. + +#### camel_dict_to_snake_dict + +boto3 returns results in a dict. The keys of the dict are in CamelCase format. In keeping +with Ansible format, this function will convert the keys to snake_case. + +#### ansible_dict_to_boto3_filter_list + +Converts a an Ansible list of filters to a boto3 friendly list of dicts. This is useful for +any boto3 _facts modules. + +#### boto3_tag_list_to_ansible_dict + +Converts a boto3 tag list to an Ansible dict. Boto3 returns tags as a list of dicts containing keys called +'Key' and 'Value'. This function converts this list in to a single dict where the dict key is the tag +key and the dict value is the tag value. + +#### ansible_dict_to_boto3_tag_list + +Opposite of above. Converts an Ansible dict to a boto3 tag list of dicts. + From ed3515970258149b09b7f91fbdec9437b4c5987b Mon Sep 17 00:00:00 2001 From: Brian Beggs Date: Wed, 6 Apr 2016 17:07:03 -0400 Subject: [PATCH 1384/2522] hipchat api v2 rooms are now url escaped --- notification/hipchat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notification/hipchat.py b/notification/hipchat.py index 2ff40be3f24..eb6b469ffb6 100644 --- a/notification/hipchat.py +++ b/notification/hipchat.py @@ -147,7 +147,7 @@ def send_msg_v2(module, token, room, msg_from, msg, msg_format='text', POST_URL = api + NOTIFY_URI_V2 - url = POST_URL.replace('{id_or_name}', room) + url = POST_URL.replace('{id_or_name}', urllib.pathname2url(room)) data = json.dumps(body) if module.check_mode: From c215bff12ef47d8f0a01c086f531f6185443f0a5 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Thu, 7 Apr 2016 14:58:15 +0200 Subject: [PATCH 1385/2522] Mark password as 'no_log', to avoid leaking it --- notification/mqtt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notification/mqtt.py b/notification/mqtt.py index c618ab69ae3..14713c2b1ea 100644 --- a/notification/mqtt.py +++ b/notification/mqtt.py @@ -113,14 +113,14 @@ def main(): module = AnsibleModule( argument_spec=dict( server = dict(default = 'localhost'), - port = dict(default = 1883), + port = dict(default = 1883, type='int'), topic = dict(required = True), payload = dict(required = True), client_id = dict(default = None), qos = dict(default="0", choices=["0", "1", "2"]), retain = dict(default=False, type='bool'), username = dict(default = None), - password = dict(default = None), + password = dict(default = None, no_log=True), ), supports_check_mode=True ) From 3385bf5ef2ebf5ca94629fe916eb609ef1f378b1 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Thu, 7 Apr 2016 14:59:11 +0200 Subject: [PATCH 1386/2522] Do not leak mail password by error --- notification/mail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notification/mail.py b/notification/mail.py index e63a9536996..c8b2bb30c78 100644 --- a/notification/mail.py +++ b/notification/mail.py @@ -178,7 +178,7 @@ def main(): module = AnsibleModule( argument_spec = dict( username = dict(default=None), - password = dict(default=None), + password = dict(default=None, no_log=True), host = dict(default='localhost'), port = dict(default='25'), sender = dict(default='root', aliases=['from']), From 7120fb4b01af0af0cecce2e03e7f604c5a7913fa Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Thu, 7 Apr 2016 17:25:04 +0200 Subject: [PATCH 1387/2522] Properly label path argument with type='path' (#1940) --- cloud/lxc/lxc_container.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index fb24fbf7644..ea4952f6b03 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -1683,7 +1683,8 @@ def main(): type='str' ), config=dict( - type='str', + type='path', + default='/etc/lxc/default.conf' ), vg_name=dict( type='str', @@ -1701,7 +1702,7 @@ def main(): default='5G' ), directory=dict( - type='str' + type='path' ), zfs_root=dict( type='str' @@ -1710,7 +1711,7 @@ def main(): type='str' ), lxc_path=dict( - type='str' + type='path' ), state=dict( choices=LXC_ANSIBLE_STATES.keys(), @@ -1743,7 +1744,7 @@ def main(): default='false' ), archive_path=dict( - type='str', + type='path', ), archive_compression=dict( choices=LXC_COMPRESSION_MAP.keys(), From bc198cc33a202bf98f06bd8c24ff69e1f0e9038c Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Thu, 7 Apr 2016 18:47:57 +0200 Subject: [PATCH 1388/2522] Avoid token leak by marking it as sensitive with no_log (#1966) --- notification/flowdock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notification/flowdock.py b/notification/flowdock.py index 34dad8db375..24fee07af13 100644 --- a/notification/flowdock.py +++ b/notification/flowdock.py @@ -113,7 +113,7 @@ def main(): module = AnsibleModule( argument_spec=dict( - token=dict(required=True), + token=dict(required=True, no_log=True), msg=dict(required=True), type=dict(required=True, choices=["inbox","chat"]), external_user_name=dict(required=False), From 8192ad24d564e3034e5d33c44abd3884febdd3cb Mon Sep 17 00:00:00 2001 From: = Date: Thu, 7 Apr 2016 18:22:23 +0100 Subject: [PATCH 1389/2522] adding the ability to manage binary registry data --- windows/win_regedit.ps1 | 39 +++++++++++++++++++++++++++++++++++++++ windows/win_regedit.py | 11 ++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/windows/win_regedit.ps1 b/windows/win_regedit.ps1 index fe060e101c6..806cc4ad5e4 100644 --- a/windows/win_regedit.ps1 +++ b/windows/win_regedit.ps1 @@ -56,6 +56,45 @@ Function Test-RegistryValueData { } } + +# Simplified version of Convert-HexStringToByteArray from +# https://cyber-defense.sans.org/blog/2010/02/11/powershell-byte-array-hex-convert +# Expects a hex in the format you get when you run reg.exe export, +# and converts to a byte array so powershell can modify binary registry entries +function Convert-RegExportHexStringToByteArray +{ + Param ( + [parameter(Mandatory=$true))] [String] $String + ) + +# remove 'hex:' from the front of the string if present +$String = $String.ToLower() -replace '^hex\:', '' + +#remove whitespace and any other non-hex crud. +$String = $String.ToLower() -replace '[^a-f0-9\\,x\-\:]','' + +# turn commas into colons +$String = $String -replace ',',':' + +#Maybe there's nothing left over to convert... +if ($String.Length -eq 0) { ,@() ; return } + +#Split string with or without colon delimiters. +if ($String.Length -eq 1) +{ ,@([System.Convert]::ToByte($String,16)) } +elseif (($String.Length % 2 -eq 0) -and ($String.IndexOf(":") -eq -1)) +{ ,@($String -split '([a-f0-9]{2})' | foreach-object { if ($_) {[System.Convert]::ToByte($_,16)}}) } +elseif ($String.IndexOf(":") -ne -1) +{ ,@($String -split ':+' | foreach-object {[System.Convert]::ToByte($_,16)}) } +else +{ ,@() } + +} + +if($registryDataType -eq "binary" -and $registryData -ne $null) { + $registryData = Convert-RegExportHexStringToByteArray($registryData) +} + if($state -eq "present") { if ((Test-Path $registryKey) -and $registryValue -ne $null) { diff --git a/windows/win_regedit.py b/windows/win_regedit.py index 5087a5eaa8f..3317d6e8dc4 100644 --- a/windows/win_regedit.py +++ b/windows/win_regedit.py @@ -43,7 +43,7 @@ aliases: [] data: description: - - Registry Value Data + - Registry Value Data. Binary data should be expressed as comma separated hex values. An easy way to generate this is to run regedit.exe and use the 'Export' option to save the registry values to a file. In the file binary values will look something like this: hex:be,ef,be,ef. The 'hex:' prefix is optional. required: false default: null aliases: [] @@ -94,6 +94,15 @@ data: 1337 datatype: dword + # Creates Registry Key called MyCompany, + # a value within MyCompany Key called "hello", and + # binary data for the value "hello" as type "binary". + win_regedit: + key: HKCU:\Software\MyCompany + value: hello + data: hex:be,ef,be,ef,be,ef,be,ef,be,ef + datatype: binary + # Delete Registry Key MyCompany # NOTE: Not specifying a value will delete the root key which means # all values will be deleted From 93db039783428e9c4f6da34d0b9e60f579bc9960 Mon Sep 17 00:00:00 2001 From: nitzmahone Date: Thu, 7 Apr 2016 17:19:36 -0700 Subject: [PATCH 1390/2522] win_updates shouldn't install hidden updates --- windows/win_updates.ps1 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/windows/win_updates.ps1 b/windows/win_updates.ps1 index 890e3670d86..fbf260ac0fe 100644 --- a/windows/win_updates.ps1 +++ b/windows/win_updates.ps1 @@ -121,6 +121,11 @@ $job_body = { $update.AcceptEula() } + if($update.IsHidden) { + Write-DebugLog "Skipping hidden update $($update.Title)" + continue + } + Write-DebugLog "Adding update $($update.Identity.UpdateID) - $($update.Title)" $res = $updates_to_install.Add($update) From 5abb914315864b714395863af99c0ac1bd66a7ba Mon Sep 17 00:00:00 2001 From: nitzmahone Date: Thu, 7 Apr 2016 17:38:32 -0700 Subject: [PATCH 1391/2522] win_updates fix to use documented InstanceGuid property name --- windows/win_updates.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_updates.ps1 b/windows/win_updates.ps1 index fbf260ac0fe..0c76dcad664 100644 --- a/windows/win_updates.ps1 +++ b/windows/win_updates.ps1 @@ -280,7 +280,7 @@ Function DestroyScheduledJob { $running_tasks = @($schedserv.GetRunningTasks(0) | Where-Object { $_.Name -eq $job_name }) Foreach($task_to_stop in $running_tasks) { - Write-DebugLog "Stopping running task $($task_to_stop.InstanceId)..." + Write-DebugLog "Stopping running task $($task_to_stop.InstanceGuid)..." $task_to_stop.Stop() } From d914b3fa849c332ec7f606388a47ab9ecaaeed51 Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Wed, 6 Apr 2016 09:13:28 +0000 Subject: [PATCH 1392/2522] Add os_keystone_domain_facts module This module gathers one or more OpenStack domains facts --- cloud/openstack/os_keystone_domain_facts.py | 137 ++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 cloud/openstack/os_keystone_domain_facts.py diff --git a/cloud/openstack/os_keystone_domain_facts.py b/cloud/openstack/os_keystone_domain_facts.py new file mode 100644 index 00000000000..5df2f2b7977 --- /dev/null +++ b/cloud/openstack/os_keystone_domain_facts.py @@ -0,0 +1,137 @@ +#!/usr/bin/python +# Copyright (c) 2016 Hewlett-Packard Enterprise Corporation +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + + +try: + import shade + HAS_SHADE = True +except ImportError: + HAS_SHADE = False + +DOCUMENTATION = ''' +--- +module: os_keystone_domain_facts +short_description: Retrieve facts about one or more OpenStack domains +extends_documentation_fragment: openstack +version_added: "2.1" +author: "Ricardo Carrillo Cruz (@rcarrillocruz)" +description: + - Retrieve facts about a one or more OpenStack domains +requirements: + - "python >= 2.6" + - "shade" +options: + name: + description: + - Name or ID of the domain + required: true + filters: + description: + - A dictionary of meta data to use for further filtering. Elements of + this dictionary may be additional dictionaries. + required: false + default: None +''' + +EXAMPLES = ''' +# Gather facts about previously created domain +- os_keystone_domain_facts: + cloud: awesomecloud +- debug: var=openstack_domains + +# Gather facts about a previously created domain by name +- os_keystone_domain_facts: + cloud: awesomecloud + name: demodomain +- debug: var=openstack_domains + +# Gather facts about a previously created domain with filter +- os_keystone_domain_facts + cloud: awesomecloud + name: demodomain + filters: + enabled: False +- debug: var=openstack_domains +''' + + +RETURN = ''' +openstack_domains: + description: has all the OpenStack facts about domains + returned: always, but can be null + type: complex + contains: + id: + description: Unique UUID. + returned: success + type: string + name: + description: Name given to the domain. + returned: success + type: string + description: + description: Description of the domain. + returned: success + type: string + enabled: + description: Flag to indicate if the domain is enabled. + returned: success + type: bool +''' + +def main(): + + argument_spec = openstack_full_argument_spec( + name=dict(required=False, default=None), + filters=dict(required=False, type='dict', default=None), + ) + module_kwargs = openstack_module_kwargs( + mutually_exclusive=[ + ['name', 'filters'], + ] + ) + module = AnsibleModule(argument_spec, **module_kwargs) + + if not HAS_SHADE: + module.fail_json(msg='shade is required for this module') + + try: + name = module.params['name'] + filters = module.params['filters'] + + opcloud = shade.operator_cloud(**module.params) + + if name: + # Let's suppose user is passing domain ID + try: + domains = cloud.get_domain(name) + except: + domains = opcloud.search_domains(filters={'name': name}) + + else: + domains = opcloud.search_domains(filters) + + module.exit_json(changed=False, ansible_facts=dict( + openstack_domains=domains)) + + except shade.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * + +if __name__ == '__main__': + main() From 34045fddb13f6ffd80810a41d3d9786558afff69 Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Wed, 6 Apr 2016 10:22:19 +0000 Subject: [PATCH 1393/2522] Add os_user_facts module This module gather facts about one or more OpenStack users --- cloud/openstack/os_user_facts.py | 172 +++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 cloud/openstack/os_user_facts.py diff --git a/cloud/openstack/os_user_facts.py b/cloud/openstack/os_user_facts.py new file mode 100644 index 00000000000..db8cebe4757 --- /dev/null +++ b/cloud/openstack/os_user_facts.py @@ -0,0 +1,172 @@ +#!/usr/bin/python +# Copyright (c) 2016 Hewlett-Packard Enterprise Corporation +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + + +try: + import shade + HAS_SHADE = True +except ImportError: + HAS_SHADE = False + +DOCUMENTATION = ''' +--- +module: os_user_facts +short_description: Retrieve facts about one or more OpenStack users +extends_documentation_fragment: openstack +version_added: "2.1" +author: "Ricardo Carrillo Cruz (@rcarrillocruz)" +description: + - Retrieve facts about a one or more OpenStack users +requirements: + - "python >= 2.6" + - "shade" +options: + name: + description: + - Name or ID of the user + required: true + domain: + description: + - Name or ID of the domain containing the user if the cloud supports domains + required: false + default: None + filters: + description: + - A dictionary of meta data to use for further filtering. Elements of + this dictionary may be additional dictionaries. + required: false + default: None +''' + +EXAMPLES = ''' +# Gather facts about previously created users +- os_user_facts: + cloud: awesomecloud +- debug: var=openstack_users + +# Gather facts about a previously created user by name +- os_user_facts: + cloud: awesomecloud + name: demouser +- debug: var=openstack_users + +# Gather facts about a previously created user in a specific domain +- os_user_facts + cloud: awesomecloud + name: demouser + domain: admindomain +- debug: var=openstack_users + +# Gather facts about a previously created user in a specific domain + with filter +- os_user_facts + cloud: awesomecloud + name: demouser + domain: admindomain + filters: + enabled: False +- debug: var=openstack_users +''' + + +RETURN = ''' +openstack_users: + description: has all the OpenStack facts about users + returned: always, but can be null + type: complex + contains: + id: + description: Unique UUID. + returned: success + type: string + name: + description: Name given to the user. + returned: success + type: string + enabled: + description: Flag to indicate if the user is enabled + returned: success + type: bool + domain_id: + description: Domain ID containing the user + returned: success + type: string + default_project_id: + description: Default project ID of the user + returned: success + type: string + email: + description: Email of the user + returned: success + type: string + username: + description: Username of the user + returned: success + type: string +''' + +def main(): + + argument_spec = openstack_full_argument_spec( + name=dict(required=False, default=None), + domain=dict(required=False, default=None), + filters=dict(required=False, type='dict', default=None), + ) + + module = AnsibleModule(argument_spec) + + if not HAS_SHADE: + module.fail_json(msg='shade is required for this module') + + try: + name = module.params['name'] + domain = module.params['domain'] + filters = module.params['filters'] + + opcloud = shade.operator_cloud(**module.params) + + if domain: + try: + # We assume admin is passing domain id + dom = opcloud.get_domain(domain)['id'] + domain = dom + except: + # If we fail, maybe admin is passing a domain name. + # Note that domains have unique names, just like id. + dom = opcloud.search_domains(filters={'name': domain}) + if dom: + domain = dom[0]['id'] + else: + module.fail_json(msg='Domain name or ID does not exist') + + if not filters: + filters = {} + + filters['domain_id'] = domain + + users = opcloud.search_users(name, + filters) + module.exit_json(changed=False, ansible_facts=dict( + openstack_users=users)) + + except shade.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * + +if __name__ == '__main__': + main() From 197ee8bef42424b62852af1bf81eed6cad134933 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sun, 10 Apr 2016 08:33:11 +0200 Subject: [PATCH 1394/2522] Client_secret is supposed to be kept secret, so mark it as no_log (#1995) --- notification/typetalk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notification/typetalk.py b/notification/typetalk.py index 8a2dad3d6a2..4a31e3ef89a 100644 --- a/notification/typetalk.py +++ b/notification/typetalk.py @@ -104,7 +104,7 @@ def main(): module = AnsibleModule( argument_spec=dict( client_id=dict(required=True), - client_secret=dict(required=True), + client_secret=dict(required=True, no_log=True), topic=dict(required=True, type='int'), msg=dict(required=True), ), From f1175693f6f34a6f925d4567afacbb829ddb4b78 Mon Sep 17 00:00:00 2001 From: Vlad Gusev Date: Sun, 10 Apr 2016 12:14:48 +0300 Subject: [PATCH 1395/2522] system/puppet: add --tags parameter (#1916) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * system/puppet: add --tags parameter --tags [1] is used to apply a part of the node’s catalog. In puppet: puppet agent --tags update,monitoring In ansible: puppet: tags=update,monitoring [1] https://docs.puppetlabs.com/puppet/latest/reference/lang_tags.html#restricting-catalog-runs * Add example of tag usage. * system/puppet: add list type for a tags dict. --- system/puppet.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/system/puppet.py b/system/puppet.py index 0b3210f542a..147cb9a42c1 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -80,6 +80,12 @@ required: false default: None version_added: "2.1" + tags: + description: + - A comma-separated list of puppet tags to be used. + required: false + default: None + version_added: "2.1" execute: description: - Execute a specific piece of Puppet code. It has no effect with @@ -106,6 +112,9 @@ # Run puppet using a specific piece of Puppet code. Has no effect with a # puppetmaster. - puppet: execute='include ::mymodule' + +# Run puppet using a specific tags +- puppet: tags=update,nginx ''' @@ -147,6 +156,7 @@ def main(): facter_basename=dict(default='ansible'), environment=dict(required=False, default=None), certname=dict(required=False, default=None), + tags=dict(required=False, default=None, type='list'), execute=dict(required=False, default=None), ), supports_check_mode=True, @@ -211,6 +221,8 @@ def main(): cmd += " --show_diff" if p['environment']: cmd += " --environment '%s'" % p['environment'] + if p['tags']: + cmd += " --tags '%s'" % ','.join(p['tags']) if p['certname']: cmd += " --certname='%s'" % p['certname'] if module.check_mode: @@ -227,6 +239,8 @@ def main(): cmd += " --certname='%s'" % p['certname'] if p['execute']: cmd += " --execute '%s'" % p['execute'] + if p['tags']: + cmd += " --tags '%s'" % ','.join(p['tags']) if module.check_mode: cmd += "--noop " else: From c03e77a63a37ac7ab9ad12c3e42e633547aa1688 Mon Sep 17 00:00:00 2001 From: Evgeni Golov Date: Sun, 10 Apr 2016 13:33:48 +0200 Subject: [PATCH 1396/2522] strip whitespace from key and value before inserting it into the config before the following would produce four entries: container_config: - "lxc.network.flags=up" - "lxc.network.flags =up" - "lxc.network.flags= up" - "lxc.network.flags = up" let's strip the whitespace and insert only one "lxc.network.flags = up" into the final config Signed-off-by: Evgeni Golov --- cloud/lxc/lxc_container.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index ea4952f6b03..3cbff3314e8 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -745,6 +745,8 @@ def _config(self): config_change = False for key, value in parsed_options: + key = key.strip() + value = value.strip() new_entry = '%s = %s\n' % (key, value) for option_line in container_config: # Look for key in config From 8db3a639837e836dd02030c5177301bb699e5de7 Mon Sep 17 00:00:00 2001 From: Evgeni Golov Date: Sun, 10 Apr 2016 13:37:00 +0200 Subject: [PATCH 1397/2522] fix handling of config options that share the same prefix container_config: - "lxc.network.ipv4.gateway=auto" - "lxc.network.ipv4=192.0.2.1" might try to override lxc.network.ipv4.gateway in the second entry as both start with "lxc.network.ipv4". use a regular expression to find a line that contains (optional) whitespace and an = after the key. Signed-off-by: Evgeni Golov --- cloud/lxc/lxc_container.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index 3cbff3314e8..d19101dd208 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -424,6 +424,8 @@ sample: True """ +import re + try: import lxc except ImportError: @@ -748,9 +750,10 @@ def _config(self): key = key.strip() value = value.strip() new_entry = '%s = %s\n' % (key, value) + keyre = re.compile(r'%s(\s+)?=' % key) for option_line in container_config: # Look for key in config - if option_line.startswith(key): + if keyre.match(option_line): _, _value = option_line.split('=', 1) config_value = ' '.join(_value.split()) line_index = container_config.index(option_line) From 773d402eac894a12c708236732997761620cf9cd Mon Sep 17 00:00:00 2001 From: kubilus1 Date: Mon, 11 Apr 2016 01:55:07 -0400 Subject: [PATCH 1398/2522] Unchecked index causes IndexError. --- system/crypttab.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/system/crypttab.py b/system/crypttab.py index d483e339631..842b5bc7d09 100755 --- a/system/crypttab.py +++ b/system/crypttab.py @@ -205,6 +205,8 @@ def __str__(self): for line in self._lines: lines.append(str(line)) crypttab = '\n'.join(lines) + if len(crypttab) == 0: + crypttab += '\n' if crypttab[-1] != '\n': crypttab += '\n' return crypttab From 0405c54dbaf113afcd2df484e54d22377aff6e55 Mon Sep 17 00:00:00 2001 From: Matt Hite Date: Sun, 10 Apr 2016 23:00:30 -0700 Subject: [PATCH 1399/2522] New preserve_node parameter to skip unreferenced node removal --- network/f5/bigip_pool_member.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/network/f5/bigip_pool_member.py b/network/f5/bigip_pool_member.py index 81bcffdb4c0..a0a1172801e 100644 --- a/network/f5/bigip_pool_member.py +++ b/network/f5/bigip_pool_member.py @@ -114,6 +114,13 @@ - Pool member ratio weight. Valid values range from 1 through 100. New pool members -- unless overriden with this value -- default to 1. required: false default: null + preserve_node: + description: + - When state is absent and the pool member is no longer referenced in other pools, the default behavior removes the unused node object. Setting this to 'yes' disables this behavior. + required: false + default: 'no' + choices: ['yes', 'no'] + version_added: 2.1 ''' EXAMPLES = ''' @@ -317,7 +324,8 @@ def main(): connection_limit = dict(type='int'), description = dict(type='str'), rate_limit = dict(type='int'), - ratio = dict(type='int') + ratio = dict(type='int'), + preserve_node = dict(type='bool', default=False) ) ) @@ -337,6 +345,7 @@ def main(): host = module.params['host'] address = fq_name(partition, host) port = module.params['port'] + preserve_node = module.params['preserve_node'] # sanity check user supplied values @@ -357,8 +366,11 @@ def main(): if member_exists(api, pool, address, port): if not module.check_mode: remove_pool_member(api, pool, address, port) - deleted = delete_node_address(api, address) - result = {'changed': True, 'deleted': deleted} + if preserve_node: + result = {'changed': True} + else: + deleted = delete_node_address(api, address) + result = {'changed': True, 'deleted': deleted} else: result = {'changed': True} From f99576749a551cfaabc475a6c1b4a2811785f994 Mon Sep 17 00:00:00 2001 From: Him You Ten Date: Mon, 11 Apr 2016 02:21:15 -0400 Subject: [PATCH 1400/2522] added stdout and stderr outputs (#1900) * added stdout and stderr outputs Added stdout and stderr outputs of the results from composer as the current msg output strips \n so very hard to read when debugging * using stdout for fail_json using stdout for fail_json so we get the stdout_lines array --- packaging/language/composer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packaging/language/composer.py b/packaging/language/composer.py index 5d1ec7b1014..d5e6467e9c5 100644 --- a/packaging/language/composer.py +++ b/packaging/language/composer.py @@ -220,11 +220,11 @@ def main(): if rc != 0: output = parse_out(err) - module.fail_json(msg=output) + module.fail_json(msg=output, stdout=err) else: # Composer version > 1.0.0-alpha9 now use stderr for standard notification messages output = parse_out(out + err) - module.exit_json(changed=has_changed(output), msg=output) + module.exit_json(changed=has_changed(output), msg=output, stdout=out+err) # import module snippets from ansible.module_utils.basic import * From 44bfca315a98e7fd12ce73c60220c64ce870fbb2 Mon Sep 17 00:00:00 2001 From: Joerg Fiedler Date: Mon, 11 Apr 2016 08:25:30 +0200 Subject: [PATCH 1401/2522] add chroot flag to pkgng in order to allow installation of packages into chroot environments, e.g. jails (#1218) --- packaging/os/pkgng.py | 72 +++++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/packaging/os/pkgng.py b/packaging/os/pkgng.py index 8936032c3b1..44212714ef3 100644 --- a/packaging/os/pkgng.py +++ b/packaging/os/pkgng.py @@ -67,6 +67,13 @@ description: - for pkgng versions 1.5 and later, pkg will install all packages within the specified root directory + - can not be used together with 'chroot' option + required: false + chroot: + version_added: "2.1" + description: + - pkg will chroot in the specified environment + - can not be used together with 'rootdir' option required: false author: "bleader (@bleader)" notes: @@ -90,9 +97,9 @@ import re import sys -def query_package(module, pkgng_path, name, rootdir_arg): +def query_package(module, pkgng_path, name, dir_arg): - rc, out, err = module.run_command("%s %s info -g -e %s" % (pkgng_path, rootdir_arg, name)) + rc, out, err = module.run_command("%s %s info -g -e %s" % (pkgng_path, dir_arg, name)) if rc == 0: return True @@ -116,19 +123,19 @@ def pkgng_older_than(module, pkgng_path, compare_version): return not new_pkgng -def remove_packages(module, pkgng_path, packages, rootdir_arg): +def remove_packages(module, pkgng_path, packages, dir_arg): remove_c = 0 # Using a for loop incase of error, we can report the package that failed for package in packages: # Query the package first, to see if we even need to remove - if not query_package(module, pkgng_path, package, rootdir_arg): + if not query_package(module, pkgng_path, package, dir_arg): continue if not module.check_mode: - rc, out, err = module.run_command("%s %s delete -y %s" % (pkgng_path, rootdir_arg, package)) + rc, out, err = module.run_command("%s %s delete -y %s" % (pkgng_path, dir_arg, package)) - if not module.check_mode and query_package(module, pkgng_path, package, rootdir_arg): + if not module.check_mode and query_package(module, pkgng_path, package, dir_arg): module.fail_json(msg="failed to remove %s: %s" % (package, out)) remove_c += 1 @@ -140,7 +147,7 @@ def remove_packages(module, pkgng_path, packages, rootdir_arg): return (False, "package(s) already absent") -def install_packages(module, pkgng_path, packages, cached, pkgsite, rootdir_arg): +def install_packages(module, pkgng_path, packages, cached, pkgsite, dir_arg): install_c = 0 @@ -160,21 +167,21 @@ def install_packages(module, pkgng_path, packages, cached, pkgsite, rootdir_arg) if old_pkgng: rc, out, err = module.run_command("%s %s update" % (pkgsite, pkgng_path)) else: - rc, out, err = module.run_command("%s update" % (pkgng_path)) + rc, out, err = module.run_command("%s %s update" % (pkgng_path, dir_arg)) if rc != 0: module.fail_json(msg="Could not update catalogue") for package in packages: - if query_package(module, pkgng_path, package, rootdir_arg): + if query_package(module, pkgng_path, package, dir_arg): continue if not module.check_mode: if old_pkgng: rc, out, err = module.run_command("%s %s %s install -g -U -y %s" % (batch_var, pkgsite, pkgng_path, package)) else: - rc, out, err = module.run_command("%s %s %s install %s -g -U -y %s" % (batch_var, pkgng_path, rootdir_arg, pkgsite, package)) + rc, out, err = module.run_command("%s %s %s install %s -g -U -y %s" % (batch_var, pkgng_path, dir_arg, pkgsite, package)) - if not module.check_mode and not query_package(module, pkgng_path, package, rootdir_arg): + if not module.check_mode and not query_package(module, pkgng_path, package, dir_arg): module.fail_json(msg="failed to install %s: %s" % (package, out), stderr=err) install_c += 1 @@ -184,20 +191,20 @@ def install_packages(module, pkgng_path, packages, cached, pkgsite, rootdir_arg) return (False, "package(s) already present") -def annotation_query(module, pkgng_path, package, tag, rootdir_arg): - rc, out, err = module.run_command("%s %s info -g -A %s" % (pkgng_path, rootdir_arg, package)) +def annotation_query(module, pkgng_path, package, tag, dir_arg): + rc, out, err = module.run_command("%s %s info -g -A %s" % (pkgng_path, dir_arg, package)) match = re.search(r'^\s*(?P%s)\s*:\s*(?P\w+)' % tag, out, flags=re.MULTILINE) if match: return match.group('value') return False -def annotation_add(module, pkgng_path, package, tag, value, rootdir_arg): - _value = annotation_query(module, pkgng_path, package, tag, rootdir_arg) +def annotation_add(module, pkgng_path, package, tag, value, dir_arg): + _value = annotation_query(module, pkgng_path, package, tag, dir_arg) if not _value: # Annotation does not exist, add it. rc, out, err = module.run_command('%s %s annotate -y -A %s %s "%s"' - % (pkgng_path, rootdir_arg, package, tag, value)) + % (pkgng_path, dir_arg, package, tag, value)) if rc != 0: module.fail_json("could not annotate %s: %s" % (package, out), stderr=err) @@ -212,19 +219,19 @@ def annotation_add(module, pkgng_path, package, tag, value, rootdir_arg): # Annotation exists, nothing to do return False -def annotation_delete(module, pkgng_path, package, tag, value, rootdir_arg): - _value = annotation_query(module, pkgng_path, package, tag, rootdir_arg) +def annotation_delete(module, pkgng_path, package, tag, value, dir_arg): + _value = annotation_query(module, pkgng_path, package, tag, dir_arg) if _value: rc, out, err = module.run_command('%s %s annotate -y -D %s %s' - % (pkgng_path, rootdir_arg, package, tag)) + % (pkgng_path, dir_arg, package, tag)) if rc != 0: module.fail_json("could not delete annotation to %s: %s" % (package, out), stderr=err) return True return False -def annotation_modify(module, pkgng_path, package, tag, value, rootdir_arg): - _value = annotation_query(module, pkgng_path, package, tag, rootdir_arg) +def annotation_modify(module, pkgng_path, package, tag, value, dir_arg): + _value = annotation_query(module, pkgng_path, package, tag, dir_arg) if not value: # No such tag module.fail_json("could not change annotation to %s: tag %s does not exist" @@ -234,14 +241,14 @@ def annotation_modify(module, pkgng_path, package, tag, value, rootdir_arg): return False else: rc,out,err = module.run_command('%s %s annotate -y -M %s %s "%s"' - % (pkgng_path, rootdir_arg, package, tag, value)) + % (pkgng_path, dir_arg, package, tag, value)) if rc != 0: module.fail_json("could not change annotation annotation to %s: %s" % (package, out), stderr=err) return True -def annotate_packages(module, pkgng_path, packages, annotation, rootdir_arg): +def annotate_packages(module, pkgng_path, packages, annotation, dir_arg): annotate_c = 0 annotations = map(lambda _annotation: re.match(r'(?P[\+-:])(?P\w+)(=(?P\w+))?', @@ -271,8 +278,10 @@ def main(): cached = dict(default=False, type='bool'), annotation = dict(default="", required=False), pkgsite = dict(default="", required=False), - rootdir = dict(default="", required=False, type='path')), - supports_check_mode = True) + rootdir = dict(default="", required=False, type='path'), + chroot = dict(default="", required=False, type='path')), + supports_check_mode = True, + mutually_exclusive =[["rootdir", "chroot"]]) pkgng_path = module.get_bin_path('pkg', True) @@ -282,27 +291,30 @@ def main(): changed = False msgs = [] - rootdir_arg = "" + dir_arg = "" if p["rootdir"] != "": old_pkgng = pkgng_older_than(module, pkgng_path, [1, 5, 0]) if old_pkgng: module.fail_json(msg="To use option 'rootdir' pkg version must be 1.5 or greater") else: - rootdir_arg = "--rootdir %s" % (p["rootdir"]) + dir_arg = "--rootdir %s" % (p["rootdir"]) + + if p["chroot"] != "": + dir_arg = '--chroot %s' % (p["chroot"]) if p["state"] == "present": - _changed, _msg = install_packages(module, pkgng_path, pkgs, p["cached"], p["pkgsite"], rootdir_arg) + _changed, _msg = install_packages(module, pkgng_path, pkgs, p["cached"], p["pkgsite"], dir_arg) changed = changed or _changed msgs.append(_msg) elif p["state"] == "absent": - _changed, _msg = remove_packages(module, pkgng_path, pkgs, rootdir_arg) + _changed, _msg = remove_packages(module, pkgng_path, pkgs, dir_arg) changed = changed or _changed msgs.append(_msg) if p["annotation"]: - _changed, _msg = annotate_packages(module, pkgng_path, pkgs, p["annotation"], rootdir_arg) + _changed, _msg = annotate_packages(module, pkgng_path, pkgs, p["annotation"], dir_arg) changed = changed or _changed msgs.append(_msg) From 5e15cc887b2cfd4a4fd18d66154ceb2ae7fda128 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Mon, 11 Apr 2016 17:35:14 +0200 Subject: [PATCH 1402/2522] Auth_toekn is a secret, shouldn't be logged (#1999) --- notification/twilio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notification/twilio.py b/notification/twilio.py index 9ed1a09e12e..216520620f9 100644 --- a/notification/twilio.py +++ b/notification/twilio.py @@ -139,7 +139,7 @@ def main(): module = AnsibleModule( argument_spec=dict( account_sid=dict(required=True), - auth_token=dict(required=True), + auth_token=dict(required=True, no_log=True), msg=dict(required=True), from_number=dict(required=True), to_number=dict(required=True), From c58f5d2137cf7af59657664273b8845750f34cd0 Mon Sep 17 00:00:00 2001 From: Matt Hite Date: Mon, 11 Apr 2016 10:57:55 -0700 Subject: [PATCH 1403/2522] Fixes issue #1992 -- fixes issue adding and deleting pools (#1994) --- network/f5/bigip_pool.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/network/f5/bigip_pool.py b/network/f5/bigip_pool.py index 47ae941c44a..014545c7c3b 100644 --- a/network/f5/bigip_pool.py +++ b/network/f5/bigip_pool.py @@ -396,7 +396,7 @@ def main(): if (host and port is None) or (port is not None and not host): module.fail_json(msg="both host and port must be supplied") - if 0 > port or port > 65535: + if port is not None and (0 > port or port > 65535): module.fail_json(msg="valid ports must be in range 0 - 65535") if monitors: @@ -522,4 +522,3 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.f5 import * main() - From 2d78c23dc0dab96dd6a5edcb7afbe9730e072d89 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Mon, 11 Apr 2016 20:01:27 +0200 Subject: [PATCH 1404/2522] cloudstack: cs_template: fix cross_zones template removal --- cloud/cloudstack/cs_template.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cloud/cloudstack/cs_template.py b/cloud/cloudstack/cs_template.py index 8690a6e1756..c61b0a990cc 100644 --- a/cloud/cloudstack/cs_template.py +++ b/cloud/cloudstack/cs_template.py @@ -89,8 +89,8 @@ default: false cross_zones: description: - - Whether the template should be syned across zones. - - Only used if C(state) is present. + - Whether the template should be syned or removed across zones. + - Only used if C(state) is present or absent. required: false default: false project: @@ -220,6 +220,7 @@ - local_action: module: cs_template name: systemvm-4.2 + cross_zones: yes state: absent ''' @@ -560,7 +561,9 @@ def remove_template(self): args = {} args['id'] = template['id'] - args['zoneid'] = self.get_zone(key='id') + + if not self.module.params.get('cross_zones'): + args['zoneid'] = self.get_zone(key='id') if not self.module.check_mode: res = self.cs.deleteTemplate(**args) @@ -620,6 +623,7 @@ def main(): required_together=required_together, mutually_exclusive = ( ['url', 'vm'], + ['zone', 'cross_zones'], ), supports_check_mode=True ) From 0b9c8213adc758243f03c6e90c389530d288563b Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Mon, 11 Apr 2016 20:01:36 +0200 Subject: [PATCH 1405/2522] cloudstack: fix doc, display_text not required --- cloud/cloudstack/cs_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/cloudstack/cs_template.py b/cloud/cloudstack/cs_template.py index c61b0a990cc..537d83e3c2e 100644 --- a/cloud/cloudstack/cs_template.py +++ b/cloud/cloudstack/cs_template.py @@ -168,7 +168,7 @@ display_text: description: - Display text of the template. - required: true + required: false default: null state: description: From 1d0df46475e6194eedaa52622e778e0fee5c7378 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Mon, 11 Apr 2016 20:02:03 +0200 Subject: [PATCH 1406/2522] cloudstack: cs_template: fix state=extracted * url arg is optional but we enforced it * url is in a required together, but args only relevant while registering --- cloud/cloudstack/cs_template.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/cloud/cloudstack/cs_template.py b/cloud/cloudstack/cs_template.py index 537d83e3c2e..e53c8e286e4 100644 --- a/cloud/cloudstack/cs_template.py +++ b/cloud/cloudstack/cs_template.py @@ -470,6 +470,12 @@ def create_template(self): def register_template(self): + required_params = [ + 'format', + 'url', + 'hypervisor', + ] + self.module.fail_on_missing_params(required_params=required_params) template = self.get_template() if not template: self.result['changed'] = True @@ -537,9 +543,6 @@ def extract_template(self): args['mode'] = self.module.params.get('mode') args['zoneid'] = self.get_zone(key='id') - if not args['url']: - self.module.fail_json(msg="Missing required arguments: url") - self.result['changed'] = True if not self.module.check_mode: @@ -613,14 +616,9 @@ def main(): poll_async = dict(type='bool', default=True), )) - required_together = cs_required_together() - required_together.extend([ - ['format', 'url', 'hypervisor'], - ]) - module = AnsibleModule( argument_spec=argument_spec, - required_together=required_together, + required_together=cs_required_together(), mutually_exclusive = ( ['url', 'vm'], ['zone', 'cross_zones'], From ac9a48ad1e83c1ad4851af3cd9b1bd53958edf8f Mon Sep 17 00:00:00 2001 From: Pavol Ipoth Date: Mon, 11 Apr 2016 20:18:14 +0200 Subject: [PATCH 1407/2522] Added pvs parameter to lvol module --- system/lvol.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/system/lvol.py b/system/lvol.py index fe5cee57569..d098843614c 100644 --- a/system/lvol.py +++ b/system/lvol.py @@ -48,7 +48,7 @@ choices: [ "present", "absent" ] default: present description: - - Control if the logical volume exists. If C(present) the C(size) option + - Control if the logical volume exists. If C(present) the C(size) option is required. required: false force: @@ -76,6 +76,9 @@ # Create a logical volume of 512m. - lvol: vg=firefly lv=test size=512 +# Create a logical volume of 512m with disks /dev/sda and /dev/sdb +- lvol: vg=firefly lv=test size=512 pvs=/dev/sda,/dev/sdb + # Create a logical volume of 512g. - lvol: vg=firefly lv=test size=512g @@ -158,6 +161,7 @@ def main(): state=dict(choices=["absent", "present"], default='present'), force=dict(type='bool', default='no'), snapshot=dict(type='str', default=None), + pvs=dict(type='str') ), supports_check_mode=True, ) @@ -181,6 +185,12 @@ def main(): size_opt = 'L' size_unit = 'm' snapshot = module.params['snapshot'] + pvs = module.params['pvs'] + + if pvs is None: + pvs = "" + else: + pvs = pvs.replace(",", " ") if opts is None: opts = "" @@ -230,7 +240,7 @@ def main(): module.fail_json(msg="Volume group %s does not exist." % vg, rc=rc, err=err) vgs = parse_vgs(current_vgs) - this_vg = vgs[0] + this_vg = vgs[0] # Get information on logical volume requested lvs_cmd = module.get_bin_path("lvs", required=True) @@ -275,7 +285,7 @@ def main(): if snapshot is not None: cmd = "%s %s -%s %s%s -s -n %s %s %s/%s" % (lvcreate_cmd, yesopt, size_opt, size, size_unit, snapshot, opts, vg, lv) else: - cmd = "%s %s -n %s -%s %s%s %s %s" % (lvcreate_cmd, yesopt, lv, size_opt, size, size_unit, opts, vg) + cmd = "%s %s -n %s -%s %s%s %s %s %s" % (lvcreate_cmd, yesopt, lv, size_opt, size, size_unit, opts, vg, pvs) rc, _, err = module.run_command(cmd) if rc == 0: changed = True @@ -306,7 +316,7 @@ def main(): if '+' in size: size_requested += this_lv['size'] if this_lv['size'] < size_requested: - if (size_free > 0) and (('+' not in size) or (size_free >= (size_requested - this_lv['size']))): + if (size_free > 0) and (('+' not in size) or (size_free >= (size_requested - this_lv['size']))): tool = module.get_bin_path("lvextend", required=True) else: module.fail_json(msg="Logical Volume %s could not be extended. Not enough free space left (%s%s required / %s%s available)" % (this_lv['name'], (size_requested - this_lv['size']), unit, size_free, unit)) @@ -323,7 +333,7 @@ def main(): if module.check_mode: changed = True else: - cmd = "%s -%s %s%s %s/%s" % (tool, size_opt, size, size_unit, vg, this_lv['name']) + cmd = "%s -%s %s%s %s/%s %s" % (tool, size_opt, size, size_unit, vg, this_lv['name'], pvs) rc, out, err = module.run_command(cmd) if "Reached maximum COW size" in out: module.fail_json(msg="Unable to resize %s to %s%s" % (lv, size, size_unit), rc=rc, err=err, out=out) @@ -353,7 +363,7 @@ def main(): if module.check_mode: changed = True else: - cmd = "%s -%s %s%s %s/%s" % (tool, size_opt, size, size_unit, vg, this_lv['name']) + cmd = "%s -%s %s%s %s/%s %s" % (tool, size_opt, size, size_unit, vg, this_lv['name'], pvs) rc, out, err = module.run_command(cmd) if "Reached maximum COW size" in out: module.fail_json(msg="Unable to resize %s to %s%s" % (lv, size, size_unit), rc=rc, err=err, out=out) From 7be55e188a1fb29396ef599014798b2e35d459c2 Mon Sep 17 00:00:00 2001 From: Pavol Ipoth Date: Mon, 11 Apr 2016 21:55:40 +0200 Subject: [PATCH 1408/2522] Fixes #2008 Lvol module is not indempodent for cache lv's --- system/lvol.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/system/lvol.py b/system/lvol.py index d098843614c..5cd082e96b0 100644 --- a/system/lvol.py +++ b/system/lvol.py @@ -79,6 +79,9 @@ # Create a logical volume of 512m with disks /dev/sda and /dev/sdb - lvol: vg=firefly lv=test size=512 pvs=/dev/sda,/dev/sdb +# Create cache pool logical volume +- lvol: vg=firefly lv=lvcache size=512m opts='--type cache-pool' + # Create a logical volume of 512g. - lvol: vg=firefly lv=test size=512g @@ -122,7 +125,7 @@ def parse_lvs(data): for line in data.splitlines(): parts = line.strip().split(';') lvs.append({ - 'name': parts[0], + 'name': parts[0].replace('[','').replace(']',''), 'size': int(decimal_point.match(parts[1]).group(1)) }) return lvs @@ -245,7 +248,7 @@ def main(): # Get information on logical volume requested lvs_cmd = module.get_bin_path("lvs", required=True) rc, current_lvs, err = module.run_command( - "%s --noheadings --nosuffix -o lv_name,size --units %s --separator ';' %s" % (lvs_cmd, unit, vg)) + "%s -a --noheadings --nosuffix -o lv_name,size --units %s --separator ';' %s" % (lvs_cmd, unit, vg)) if rc != 0: if state == 'absent': From 98514ace6e9261e37caa5211b5f83e8e195d99cb Mon Sep 17 00:00:00 2001 From: Evgeni Golov Date: Tue, 12 Apr 2016 07:17:12 +0200 Subject: [PATCH 1409/2522] do not set LXC default config this was accidentally re-introduced in 7120fb4b Signed-off-by: Evgeni Golov --- cloud/lxc/lxc_container.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index ea4952f6b03..678360afaec 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -1684,7 +1684,6 @@ def main(): ), config=dict( type='path', - default='/etc/lxc/default.conf' ), vg_name=dict( type='str', From 6785f3b424c34ade1cf6c6fd23da764f6e278480 Mon Sep 17 00:00:00 2001 From: stoned Date: Tue, 12 Apr 2016 07:21:28 +0200 Subject: [PATCH 1410/2522] =?UTF-8?q?cpanm:=20search=20both=20its=20stderr?= =?UTF-8?q?=20and=20its=20stdout=20for=20the=20message=20'is=20up=20t?= =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Note that since cpanm version 1.6926 its messages are sent to stdout when previously they were sent to stderr. Also there is no need to initialize out_cpanm and err_cpanm and check for their truthiness as module.run_command() and str.find() take care of that. --- packaging/language/cpanm.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packaging/language/cpanm.py b/packaging/language/cpanm.py index 919677466ab..769ea5f02fa 100644 --- a/packaging/language/cpanm.py +++ b/packaging/language/cpanm.py @@ -202,7 +202,6 @@ def main(): installed = _is_package_installed(module, name, locallib, cpanm, version) if not installed: - out_cpanm = err_cpanm = '' cmd = _build_cmd_line(name, from_path, notest, locallib, mirror, mirror_only, installdeps, cpanm, use_sudo) rc_cpanm, out_cpanm, err_cpanm = module.run_command(cmd, check_rc=False) @@ -210,7 +209,7 @@ def main(): if rc_cpanm != 0: module.fail_json(msg=err_cpanm, cmd=cmd) - if err_cpanm and 'is up to date' not in err_cpanm: + if (err_cpanm.find('is up to date') == -1 and out_cpanm.find('is up to date') == -1): changed = True module.exit_json(changed=changed, binary=cpanm, name=name) From 2dbfdaa88b447d6599543a3daa7b2023fff8598f Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Tue, 12 Apr 2016 08:13:24 +0200 Subject: [PATCH 1411/2522] Remove dead code (#1303) The review on https://github.com/ansible/ansible-modules-extras/pull/1303 show the problem was already fixed, so we just need to remove the code. --- system/firewalld.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/system/firewalld.py b/system/firewalld.py index 29054f37702..2638ff759e8 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -328,14 +328,6 @@ def main(): timeout = module.params['timeout'] interface = module.params['interface'] - ## Check for firewalld running - try: - if fw.connected == False: - module.fail_json(msg='firewalld service must be running') - except AttributeError: - module.fail_json(msg="firewalld connection can't be established,\ - version likely too old. Requires firewalld >= 2.0.11") - modification_count = 0 if service != None: modification_count += 1 From 01a15f8a0b94f1a09901880d90e2901a209bf08f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Szczygie=C5=82?= Date: Tue, 12 Apr 2016 11:11:33 +0200 Subject: [PATCH 1412/2522] VMware datacenter module shouldn't hold pyvmomi context in Ansible module object (#1568) * VMware datacenter module rewritten to don't hold pyvmomi context and objects in Ansible module object fixed exceptions handling added datacenter destroy result, moved checks changed wrong value wrong value again... need some sleep * check_mode fixes * state defaults to present, default changed to true * module check fixes --- cloud/vmware/vmware_datacenter.py | 99 +++++++++++++------------------ 1 file changed, 40 insertions(+), 59 deletions(-) diff --git a/cloud/vmware/vmware_datacenter.py b/cloud/vmware/vmware_datacenter.py index aa85782bbbe..77685616e51 100644 --- a/cloud/vmware/vmware_datacenter.py +++ b/cloud/vmware/vmware_datacenter.py @@ -25,9 +25,9 @@ description: - Manage VMware vSphere Datacenters version_added: 2.0 -author: "Joseph Callen (@jcpowermac)" +author: "Joseph Callen (@jcpowermac), Kamil Szczygiel (@kamsz)" notes: - - Tested on vSphere 5.5 + - Tested on vSphere 6.0 requirements: - "python >= 2.6" - PyVmomi @@ -54,7 +54,7 @@ description: - If the datacenter should be present or absent choices: ['present', 'absent'] - required: True + default: present extends_documentation_fragment: vmware.documentation ''' @@ -64,7 +64,7 @@ local_action: > vmware_datacenter hostname="{{ ansible_ssh_host }}" username=root password=vmware - datacenter_name="datacenter" + datacenter_name="datacenter" state=present ''' try: @@ -74,18 +74,28 @@ HAS_PYVMOMI = False -def state_create_datacenter(module): - datacenter_name = module.params['datacenter_name'] - content = module.params['content'] - changed = True - datacenter = None +def get_datacenter(context, module): + try: + datacenter_name = module.params.get('datacenter_name') + datacenter = find_datacenter_by_name(context, datacenter_name) + return datacenter + except vmodl.RuntimeFault as runtime_fault: + module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + module.fail_json(msg=method_fault.msg) + - folder = content.rootFolder +def create_datacenter(context, module): + datacenter_name = module.params.get('datacenter_name') + folder = context.rootFolder try: - if not module.check_mode: - datacenter = folder.CreateDatacenter(name=datacenter_name) - module.exit_json(changed=changed, result=str(datacenter)) + datacenter = get_datacenter(context, module) + if not datacenter: + changed = True + if not module.check_mode: + folder.CreateDatacenter(name=datacenter_name) + module.exit_json(changed=changed) except vim.fault.DuplicateName: module.fail_json(msg="A datacenter with the name %s already exists" % datacenter_name) except vim.fault.InvalidName: @@ -99,34 +109,16 @@ def state_create_datacenter(module): module.fail_json(msg=method_fault.msg) -def check_datacenter_state(module): - datacenter_name = module.params['datacenter_name'] - - try: - content = connect_to_api(module) - datacenter = find_datacenter_by_name(content, datacenter_name) - module.params['content'] = content - - if datacenter is None: - return 'absent' - else: - module.params['datacenter'] = datacenter - return 'present' - except vmodl.RuntimeFault as runtime_fault: - module.fail_json(msg=runtime_fault.msg) - except vmodl.MethodFault as method_fault: - module.fail_json(msg=method_fault.msg) - - -def state_destroy_datacenter(module): - datacenter = module.params['datacenter'] - changed = True +def destroy_datacenter(context, module): result = None try: - if not module.check_mode: - task = datacenter.Destroy_Task() - changed, result = wait_for_task(task) + datacenter = get_datacenter(context, module) + if datacenter: + changed = True + if not module.check_mode: + task = datacenter.Destroy_Task() + changed, result = wait_for_task(task) module.exit_json(changed=changed, result=result) except vim.fault.VimFault as vim_fault: module.fail_json(msg=vim_fault.msg) @@ -136,39 +128,28 @@ def state_destroy_datacenter(module): module.fail_json(msg=method_fault.msg) -def state_exit_unchanged(module): - module.exit_json(changed=False) - - def main(): argument_spec = vmware_argument_spec() argument_spec.update( dict( - datacenter_name=dict(required=True, type='str'), - state=dict(required=True, choices=['present', 'absent'], type='str'), - ) + datacenter_name=dict(required=True, type='str'), + state=dict(default='present', choices=['present', 'absent'], type='str') ) + ) module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) if not HAS_PYVMOMI: module.fail_json(msg='pyvmomi is required for this module') - datacenter_states = { - 'absent': { - 'present': state_destroy_datacenter, - 'absent': state_exit_unchanged, - }, - 'present': { - 'present': state_exit_unchanged, - 'absent': state_create_datacenter, - } - } - desired_state = module.params['state'] - current_state = check_datacenter_state(module) - - datacenter_states[desired_state][current_state](module) + context = connect_to_api(module) + state = module.params.get('state') + + if state == 'present': + create_datacenter(context, module) + if state == 'absent': + destroy_datacenter(context, module) from ansible.module_utils.basic import * from ansible.module_utils.vmware import * From 38cb5c61305624e8eb795fe4cb7a0c07ed09e40c Mon Sep 17 00:00:00 2001 From: Andrea Scarpino Date: Tue, 12 Apr 2016 14:07:32 +0200 Subject: [PATCH 1413/2522] The enable parameter is a boolean, then convert to a boolean. (#1607) At the moment, this only works when 'enable' is equals to 'yes' or 'no'. While I'm on it, I also fixed a typo in the example and added a required parameter. --- windows/win_firewall_rule.ps1 | 1 + windows/win_firewall_rule.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/windows/win_firewall_rule.ps1 b/windows/win_firewall_rule.ps1 index 63ac538e376..21f96bcf33f 100644 --- a/windows/win_firewall_rule.ps1 +++ b/windows/win_firewall_rule.ps1 @@ -212,6 +212,7 @@ $action=Get-Attr $params "action" ""; $misArg = '' # Check the arguments if ($enable -ne $null) { + $enable=ConvertTo-Bool $enable; if ($enable -eq $true) { $fwsettings.Add("Enabled", "yes"); } elseif ($enable -eq $false) { diff --git a/windows/win_firewall_rule.py b/windows/win_firewall_rule.py index 03611a60ef4..2f90e2a6730 100644 --- a/windows/win_firewall_rule.py +++ b/windows/win_firewall_rule.py @@ -114,10 +114,11 @@ action: win_firewall_rule args: name: smtp - enabled: yes + enable: yes state: present localport: 25 action: allow + direction: In protocol: TCP ''' From fa65f4dc2b44d213e6d3bd23ca1750f6c7d5171d Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Tue, 12 Apr 2016 16:27:18 +0200 Subject: [PATCH 1414/2522] Mark token as no_log, since that's used for auth (#2011) --- notification/hipchat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notification/hipchat.py b/notification/hipchat.py index eb6b469ffb6..f7543aa5592 100644 --- a/notification/hipchat.py +++ b/notification/hipchat.py @@ -169,7 +169,7 @@ def main(): module = AnsibleModule( argument_spec=dict( - token=dict(required=True), + token=dict(required=True, no_log=True), room=dict(required=True), msg=dict(required=True), msg_from=dict(default="Ansible", aliases=['from']), From 3c9310d6086ee0a924bed6344fd8b07de8e8918b Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 12 Apr 2016 12:25:59 -0400 Subject: [PATCH 1415/2522] New OpenStack module os_port_facts (#1986) --- cloud/openstack/os_port_facts.py | 225 +++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 cloud/openstack/os_port_facts.py diff --git a/cloud/openstack/os_port_facts.py b/cloud/openstack/os_port_facts.py new file mode 100644 index 00000000000..c987fed0c3c --- /dev/null +++ b/cloud/openstack/os_port_facts.py @@ -0,0 +1,225 @@ +#!/usr/bin/python + +# Copyright (c) 2016 IBM +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + +try: + import shade + HAS_SHADE = True +except ImportError: + HAS_SHADE = False + +DOCUMENTATION = ''' +module: os_port_facts +short_description: Retrieve facts about ports within OpenStack. +version_added: "2.1" +author: "David Shrewsbury (@Shrews)" +description: + - Retrieve facts about ports from OpenStack. +notes: + - Facts are placed in the C(openstack_ports) variable. +requirements: + - "python >= 2.6" + - "shade" +options: + port: + description: + - Unique name or ID of a port. + required: false + default: null + filters: + description: + - A dictionary of meta data to use for further filtering. Elements + of this dictionary will be matched against the returned port + dictionaries. Matching is currently limited to strings within + the port dictionary, or strings within nested dictionaries. + required: false + default: null +extends_documentation_fragment: openstack +''' + +EXAMPLES = ''' +# Gather facts about all ports +- os_port_facts: + cloud: mycloud + +# Gather facts about a single port +- os_port_facts: + cloud: mycloud + port: 6140317d-e676-31e1-8a4a-b1913814a471 + +# Gather facts about all ports that have device_id set to a specific value +# and with a status of ACTIVE. +- os_port_facts: + cloud: mycloud + filters: + device_id: 1038a010-3a37-4a9d-82ea-652f1da36597 + status: ACTIVE +''' + +RETURN = ''' +openstack_ports: + description: List of port dictionaries. A subset of the dictionary keys + listed below may be returned, depending on your cloud provider. + returned: always, but can be null + type: complex + contains: + admin_state_up: + description: The administrative state of the router, which is + up (true) or down (false). + returned: success + type: boolean + sample: true + allowed_address_pairs: + description: A set of zero or more allowed address pairs. An + address pair consists of an IP address and MAC address. + returned: success + type: list + sample: [] + "binding:host_id": + description: The UUID of the host where the port is allocated. + returned: success + type: string + sample: "b4bd682d-234a-4091-aa5b-4b025a6a7759" + "binding:profile": + description: A dictionary the enables the application running on + the host to pass and receive VIF port-specific + information to the plug-in. + returned: success + type: dict + sample: {} + "binding:vif_details": + description: A dictionary that enables the application to pass + information about functions that the Networking API + provides. + returned: success + type: dict + sample: {"port_filter": true} + "binding:vif_type": + description: The VIF type for the port. + returned: success + type: dict + sample: "ovs" + "binding:vnic_type": + description: The virtual network interface card (vNIC) type that is + bound to the neutron port. + returned: success + type: string + sample: "normal" + device_id: + description: The UUID of the device that uses this port. + returned: success + type: string + sample: "b4bd682d-234a-4091-aa5b-4b025a6a7759" + device_owner: + description: The UUID of the entity that uses this port. + returned: success + type: string + sample: "network:router_interface" + dns_assignment: + description: DNS assignment information. + returned: success + type: list + dns_name: + description: DNS name + returned: success + type: string + sample: "" + extra_dhcp_opts: + description: A set of zero or more extra DHCP option pairs. + An option pair consists of an option value and name. + returned: success + type: list + sample: [] + fixed_ips: + description: The IP addresses for the port. Includes the IP address + and UUID of the subnet. + returned: success + type: list + id: + description: The UUID of the port. + returned: success + type: string + sample: "3ec25c97-7052-4ab8-a8ba-92faf84148de" + ip_address: + description: The IP address. + returned: success + type: string + sample: "127.0.0.1" + mac_address: + description: The MAC address. + returned: success + type: string + sample: "fa:16:30:5f:10:f1" + name: + description: The port name. + returned: success + type: string + sample: "port_name" + network_id: + description: The UUID of the attached network. + returned: success + type: string + sample: "dd1ede4f-3952-4131-aab6-3b8902268c7d" + port_security_enabled: + description: The port security status. The status is enabled (true) or disabled (false). + returned: success + type: boolean + sample: false + security_groups: + description: The UUIDs of any attached security groups. + returned: success + type: list + status: + description: The port status. + returned: success + type: string + sample: "ACTIVE" + tenant_id: + description: The UUID of the tenant who owns the network. + returned: success + type: string + sample: "51fce036d7984ba6af4f6c849f65ef00" +''' + + +def main(): + argument_spec = openstack_full_argument_spec( + port=dict(required=False), + filters=dict(type='dict', required=False), + ) + module_kwargs = openstack_module_kwargs() + module = AnsibleModule(argument_spec, **module_kwargs) + + if not HAS_SHADE: + module.fail_json(msg='shade is required for this module') + + port = module.params.pop('port') + filters = module.params.pop('filters') + + try: + cloud = shade.openstack_cloud(**module.params) + ports = cloud.search_ports(port, filters) + module.exit_json(changed=False, ansible_facts=dict( + openstack_ports=ports)) + + except shade.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * + +if __name__ == '__main__': + main() From 30a46ee542767676c12474c73b56763f2912be4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Tue, 12 Apr 2016 18:46:02 +0200 Subject: [PATCH 1416/2522] cloudstack: cs_instance: fix template not found (#2005) Let users decide which filter should be used to find the template. --- cloud/cloudstack/cs_instance.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 9674b589da4..eeac04162e1 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -87,6 +87,15 @@ - Mutually exclusive with C(template) option. required: false default: null + template_filter: + description: + - Name of the filter used to search for the template or iso. + - Used for params C(iso) or C(template) on C(state=present). + required: false + default: 'executable' + choices: [ 'featured', 'self', 'selfexecutable', 'sharedexecutable', 'executable', 'community' ] + aliases: [ 'iso_filter' ] + version_added: '2.1' hypervisor: description: - Name the hypervisor to be used for creating the new instance. @@ -450,7 +459,7 @@ def get_template_or_iso(self, key=None): if self.template: return self._get_by_key(key, self.template) - args['templatefilter'] = 'executable' + args['templatefilter'] = self.module.params.get('template_filter') templates = self.cs.listTemplates(**args) if templates: for t in templates['template']: @@ -462,7 +471,7 @@ def get_template_or_iso(self, key=None): elif iso: if self.iso: return self._get_by_key(key, self.iso) - args['isofilter'] = 'executable' + args['isofilter'] = self.module.params.get('template_filter') isos = self.cs.listIsos(**args) if isos: for i in isos['iso']: @@ -913,6 +922,7 @@ def main(): memory = dict(default=None, type='int'), template = dict(default=None), iso = dict(default=None), + template_filter = dict(default="executable", aliases=['iso_filter'], choices=['featured', 'self', 'selfexecutable', 'sharedexecutable', 'executable', 'community']), networks = dict(type='list', aliases=[ 'network' ], default=None), ip_to_networks = dict(type='list', aliases=['ip_to_network'], default=None), ip_address = dict(defaul=None), From 0fa30f8d9323696607a70f8a4287bc787253be65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Tue, 12 Apr 2016 18:46:52 +0200 Subject: [PATCH 1417/2522] cloudstack, cs_firewall: fix network not found error in return results (#2006) Only a small issue in results. In case of type is ingress, we rely on ip address, but in results we also return the network. Resolving the ip address works without zone params. If the ip address is not located in the default zone and zone param is not set, the network won't be found because default zone was used for the network query listing. However since network param is not used for type ingress we skip the return of the network in results. --- cloud/cloudstack/cs_firewall.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/cloud/cloudstack/cs_firewall.py b/cloud/cloudstack/cs_firewall.py index 7a6bfb6c093..b2e5a68a7a0 100644 --- a/cloud/cloudstack/cs_firewall.py +++ b/cloud/cloudstack/cs_firewall.py @@ -234,6 +234,7 @@ def __init__(self, module): 'icmptype': 'icmp_type', } self.firewall_rule = None + self.network = None def get_firewall_rule(self): @@ -309,10 +310,11 @@ def _type_cidr_match(self, rule, cidr): return cidr == rule['cidrlist'] - def get_network(self, key=None, network=None): - if not network: - network = self.module.params.get('network') + def get_network(self, key=None): + if self.network: + return self._get_by_key(key, self.network) + network = self.module.params.get('network') if not network: return None @@ -328,6 +330,7 @@ def get_network(self, key=None, network=None): for n in networks['network']: if network in [ n['displaytext'], n['name'], n['id'] ]: + self.network = n return self._get_by_key(key, n) break self.module.fail_json(msg="Network '%s' not found" % network) @@ -392,8 +395,8 @@ def get_result(self, firewall_rule): super(AnsibleCloudStackFirewall, self).get_result(firewall_rule) if firewall_rule: self.result['type'] = self.module.params.get('type') - if 'networkid' in firewall_rule: - self.result['network'] = self.get_network(key='displaytext', network=firewall_rule['networkid']) + if self.result['type'] == 'egress': + self.result['network'] = self.get_network(key='displaytext') return self.result From 50d159fa1fa1fbf097a0270adadb85424d1a5a31 Mon Sep 17 00:00:00 2001 From: Werner Dijkerman Date: Tue, 12 Apr 2016 19:18:12 +0200 Subject: [PATCH 1418/2522] New module for creating gitlab users (#966) --- source_control/gitlab_user.py | 348 ++++++++++++++++++++++++++++++++++ 1 file changed, 348 insertions(+) create mode 100644 source_control/gitlab_user.py diff --git a/source_control/gitlab_user.py b/source_control/gitlab_user.py new file mode 100644 index 00000000000..9f6fc0db2a3 --- /dev/null +++ b/source_control/gitlab_user.py @@ -0,0 +1,348 @@ +#!/usr/bin/python +# (c) 2015, Werner Dijkerman (ikben@werner-dijkerman.nl) +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: gitlab_user +short_description: Creates/updates/deletes Gitlab Users +description: + - When the user does not exists in Gitlab, it will be created. + - When the user does exists and state=absent, the user will be deleted. + - When changes are made to user, the user will be updated. +version_added: "2.1" +author: "Werner Dijkerman (@dj-wasabi)" +requirements: + - pyapi-gitlab python module +options: + server_url: + description: + - Url of Gitlab server, with protocol (http or https). + required: true + validate_certs: + description: + - When using https if SSL certificate needs to be verified. + required: false + default: true + aliases: + - verify_ssl + login_user: + description: + - Gitlab user name. + required: false + default: null + login_password: + description: + - Gitlab password for login_user + required: false + default: null + login_token: + description: + - Gitlab token for logging in. + required: false + default: null + name: + description: + - Name of the user you want to create + required: true + username: + description: + - The username of the user. + required: true + password: + description: + - The password of the user. + required: true + email: + description: + - The email that belongs to the user. + required: true + sshkey_name: + description: + - The name of the sshkey + required: false + default: null + sshkey_file: + description: + - The ssh key itself. + required: false + default: null + group: + description: + - Add user as an member to this group. + required: false + default: null + access_level: + description: + - The access level to the group. One of the following can be used. + - guest + - reporter + - developer + - master + - owner + required: false + default: null + state: + description: + - create or delete group. + - Possible values are present and absent. + required: false + default: present + choices: ["present", "absent"] +''' + +EXAMPLES = ''' +- name: "Delete Gitlab User" + local_action: gitlab_user + server_url="http://gitlab.dj-wasabi.local" + validate_certs=false + login_token="WnUzDsxjy8230-Dy_k" + username=myusername + state=absent + +- name: "Create Gitlab User" + local_action: gitlab_user + server_url="https://gitlab.dj-wasabi.local" + validate_certs=true + login_user=dj-wasabi + login_password="MySecretPassword" + name=My Name + username=myusername + password=mysecretpassword + email=me@home.com + sshkey_name=MySSH + sshkey_file=ssh-rsa AAAAB3NzaC1yc... + state=present +''' + +RETURN = '''# ''' + +try: + import gitlab + HAS_GITLAB_PACKAGE = True +except: + HAS_GITLAB_PACKAGE = False + + +class GitLabUser(object): + def __init__(self, module, git): + self._module = module + self._gitlab = git + + def addToGroup(self, group_id, user_id, access_level): + if access_level == "guest": + level = 10 + elif access_level == "reporter": + level = 20 + elif access_level == "developer": + level = 30 + elif access_level == "master": + level = 40 + elif access_level == "owner": + level = 50 + return self._gitlab.addgroupmember(group_id, user_id, level) + + def createOrUpdateUser(self, user_name, user_username, user_password, user_email, user_sshkey_name, user_sshkey_file, group_name, access_level): + group_id = '' + arguments = {"name": user_name, + "username": user_username, + "email": user_email} + + if group_name is not None: + if self.existsGroup(group_name): + group_id = self.getGroupId(group_name) + + if self.existsUser(user_username): + self.updateUser(group_id, user_sshkey_name, user_sshkey_file, access_level, arguments) + else: + if self._module.check_mode: + self._module.exit_json(changed=True) + self.createUser(group_id, user_password, user_sshkey_name, user_sshkey_file, access_level, arguments) + + def createUser(self, group_id, user_password, user_sshkey_name, user_sshkey_file, access_level, arguments): + user_changed = False + + # Create the user + user_username = arguments['username'] + user_name = arguments['name'] + user_email = arguments['email'] + if self._gitlab.createuser(password=user_password, **arguments): + user_id = self.getUserId(user_username) + if self._gitlab.addsshkeyuser(user_id=user_id, title=user_sshkey_name, key=user_sshkey_file): + user_changed = True + # Add the user to the group if group_id is not empty + if group_id != '': + if self.addToGroup(group_id, user_id, access_level): + user_changed = True + user_changed = True + + # Exit with change to true or false + if user_changed: + self._module.exit_json(changed=True, result="Created the user") + else: + self._module.exit_json(changed=False) + + def deleteUser(self, user_username): + user_id = self.getUserId(user_username) + + if self._gitlab.deleteuser(user_id): + self._module.exit_json(changed=True, result="Successfully deleted user %s" % user_username) + else: + self._module.exit_json(changed=False, result="User %s already deleted or something went wrong" % user_username) + + def existsGroup(self, group_name): + for group in self._gitlab.getall(self._gitlab.getgroups): + if group['name'] == group_name: + return True + return False + + def existsUser(self, username): + found_user = self._gitlab.getusers(search=username) + for user in found_user: + if user['id'] != '': + return True + return False + + def getGroupId(self, group_name): + for group in self._gitlab.getall(self._gitlab.getgroups): + if group['name'] == group_name: + return group['id'] + + def getUserId(self, username): + found_user = self._gitlab.getusers(search=username) + for user in found_user: + if user['id'] != '': + return user['id'] + + def updateUser(self, group_id, user_sshkey_name, user_sshkey_file, access_level, arguments): + user_changed = False + user_username = arguments['username'] + user_id = self.getUserId(user_username) + user_data = self._gitlab.getuser(user_id=user_id) + + # Lets check if we need to update the user + for arg_key, arg_value in arguments.items(): + if user_data[arg_key] != arg_value: + user_changed = True + + if user_changed: + if self._module.check_mode: + self._module.exit_json(changed=True) + self._gitlab.edituser(user_id=user_id, **arguments) + user_changed = True + if self._module.check_mode or self._gitlab.addsshkeyuser(user_id=user_id, title=user_sshkey_name, key=user_sshkey_file): + user_changed = True + if group_id != '': + if self._module.check_mode or self.addToGroup(group_id, user_id, access_level): + user_changed = True + if user_changed: + self._module.exit_json(changed=True, result="The user %s is updated" % user_username) + else: + self._module.exit_json(changed=False, result="The user %s is already up2date" % user_username) + + +def main(): + global user_id + module = AnsibleModule( + argument_spec=dict( + server_url=dict(required=True), + validate_certs=dict(required=False, default=True, type=bool, aliases=['verify_ssl']), + login_user=dict(required=False, no_log=True), + login_password=dict(required=False, no_log=True), + login_token=dict(required=False, no_log=True), + name=dict(required=True), + username=dict(required=True), + password=dict(required=True), + email=dict(required=True), + sshkey_name=dict(required=False), + sshkey_file=dict(required=False), + group=dict(required=False), + access_level=dict(required=False, choices=["guest", "reporter", "developer", "master", "owner"]), + state=dict(default="present", choices=["present", "absent"]), + ), + supports_check_mode=True + ) + + if not HAS_GITLAB_PACKAGE: + module.fail_json(msg="Missing required gitlab module (check docs or install with: pip install pyapi-gitlab") + + server_url = module.params['server_url'] + verify_ssl = module.params['validate_certs'] + login_user = module.params['login_user'] + login_password = module.params['login_password'] + login_token = module.params['login_token'] + user_name = module.params['name'] + user_username = module.params['username'] + user_password = module.params['password'] + user_email = module.params['email'] + user_sshkey_name = module.params['sshkey_name'] + user_sshkey_file = module.params['sshkey_file'] + group_name = module.params['group'] + access_level = module.params['access_level'] + state = module.params['state'] + + # We need both login_user and login_password or login_token, otherwise we fail. + if login_user is not None and login_password is not None: + use_credentials = True + elif login_token is not None: + use_credentials = False + else: + module.fail_json(msg="No login credentials are given. Use login_user with login_password, or login_token") + + # Check if vars are none + if user_sshkey_file is not None and user_sshkey_name is not None: + use_sshkey = True + else: + use_sshkey = False + + if group_name is not None and access_level is not None: + add_to_group = True + group_name = group_name.lower() + else: + add_to_group = False + + user_username = user_username.lower() + + # Lets make an connection to the Gitlab server_url, with either login_user and login_password + # or with login_token + try: + if use_credentials: + git = gitlab.Gitlab(host=server_url) + git.login(user=login_user, password=login_password) + else: + git = gitlab.Gitlab(server_url, token=login_token, verify_ssl=verify_ssl) + except Exception, e: + module.fail_json(msg="Failed to connect to Gitlab server: %s " % e) + + # Validate if group exists and take action based on "state" + user = GitLabUser(module, git) + + # Check if user exists, if not exists and state = absent, we exit nicely. + if not user.existsUser(user_username) and state == "absent": + module.exit_json(changed=False, result="User already deleted or does not exists") + else: + # User exists, + if state == "absent": + user.deleteUser(user_username) + else: + user.createOrUpdateUser(user_name, user_username, user_password, user_email, user_sshkey_name, user_sshkey_file, group_name, access_level) + + +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From 50179aca6969eacbb9ecae3585cd87dc049a5dfc Mon Sep 17 00:00:00 2001 From: Werner Dijkerman Date: Tue, 12 Apr 2016 19:19:25 +0200 Subject: [PATCH 1419/2522] New module for creating gitlab groups (#967) --- source_control/gitlab_group.py | 215 +++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 source_control/gitlab_group.py diff --git a/source_control/gitlab_group.py b/source_control/gitlab_group.py new file mode 100644 index 00000000000..83bc77857f0 --- /dev/null +++ b/source_control/gitlab_group.py @@ -0,0 +1,215 @@ +#!/usr/bin/python +# (c) 2015, Werner Dijkerman (ikben@werner-dijkerman.nl) +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: gitlab_group +short_description: Creates/updates/deletes Gitlab Groups +description: + - When the group does not exists in Gitlab, it will be created. + - When the group does exists and state=absent, the group will be deleted. +version_added: "2.1" +author: "Werner Dijkerman (@dj-wasabi)" +requirements: + - pyapi-gitlab python module +options: + server_url: + description: + - Url of Gitlab server, with protocol (http or https). + required: true + validate_certs: + description: + - When using https if SSL certificate needs to be verified. + required: false + default: true + aliases: + - verify_ssl + login_user: + description: + - Gitlab user name. + required: false + default: null + login_password: + description: + - Gitlab password for login_user + required: false + default: null + login_token: + description: + - Gitlab token for logging in. + required: false + default: null + name: + description: + - Name of the group you want to create. + required: true + path: + description: + - The path of the group you want to create, this will be server_url/group_path + - If not supplied, the group_name will be used. + required: false + default: null + state: + description: + - create or delete group. + - Possible values are present and absent. + required: false + default: "present" + choices: ["present", "absent"] +''' + +EXAMPLES = ''' +- name: "Delete Gitlab Group" + local_action: gitlab_group + server_url="http://gitlab.dj-wasabi.local" + validate_certs=false + login_token="WnUzDsxjy8230-Dy_k" + name=my_first_group + state=absent + +- name: "Create Gitlab Group" + local_action: gitlab_group + server_url="https://gitlab.dj-wasabi.local" + validate_certs=true + login_user=dj-wasabi + login_password="MySecretPassword" + name=my_first_group + path=my_first_group + state=present +''' + +RETURN = '''# ''' + +try: + import gitlab + HAS_GITLAB_PACKAGE = True +except: + HAS_GITLAB_PACKAGE = False + + +class GitLabGroup(object): + def __init__(self, module, git): + self._module = module + self._gitlab = git + + def createGroup(self, group_name, group_path): + if self._module.check_mode: + self._module.exit_json(changed=True) + return self._gitlab.creategroup(group_name, group_path) + + def deleteGroup(self, group_name): + is_group_empty = True + group_id = self.idGroup(group_name) + + for project in self._gitlab.getall(self._gitlab.getprojects): + owner = project['namespace']['name'] + if owner == group_name: + is_group_empty = False + + if is_group_empty: + if self._module.check_mode: + self._module.exit_json(changed=True) + return self._gitlab.deletegroup(group_id) + else: + self._module.fail_json(msg="There are still projects in this group. These needs to be moved or deleted before this group can be removed.") + + def existsGroup(self, group_name): + for group in self._gitlab.getall(self._gitlab.getgroups): + if group['name'] == group_name: + return True + return False + + def idGroup(self, group_name): + for group in self._gitlab.getall(self._gitlab.getgroups): + if group['name'] == group_name: + return group['id'] + + +def main(): + module = AnsibleModule( + argument_spec=dict( + server_url=dict(required=True), + validate_certs=dict(required=False, default=True, type=bool, aliases=['verify_ssl']), + login_user=dict(required=False, no_log=True), + login_password=dict(required=False, no_log=True), + login_token=dict(required=False, no_log=True), + name=dict(required=True), + path=dict(required=False), + state=dict(default="present", choices=["present", "absent"]), + ), + supports_check_mode=True + ) + + if not HAS_GITLAB_PACKAGE: + module.fail_json(msg="Missing requried gitlab module (check docs or install with: pip install pyapi-gitlab") + + server_url = module.params['server_url'] + verify_ssl = module.params['validate_certs'] + login_user = module.params['login_user'] + login_password = module.params['login_password'] + login_token = module.params['login_token'] + group_name = module.params['name'] + group_path = module.params['path'] + state = module.params['state'] + + # We need both login_user and login_password or login_token, otherwise we fail. + if login_user is not None and login_password is not None: + use_credentials = True + elif login_token is not None: + use_credentials = False + else: + module.fail_json(msg="No login credentials are given. Use login_user with login_password, or login_token") + + # Set group_path to group_name if it is empty. + if group_path is None: + group_path = group_name.replace(" ", "_") + + # Lets make an connection to the Gitlab server_url, with either login_user and login_password + # or with login_token + try: + if use_credentials: + git = gitlab.Gitlab(host=server_url) + git.login(user=login_user, password=login_password) + else: + git = gitlab.Gitlab(server_url, token=login_token, verify_ssl=verify_ssl) + except Exception, e: + module.fail_json(msg="Failed to connect to Gitlab server: %s " % e) + + # Validate if group exists and take action based on "state" + group = GitLabGroup(module, git) + group_name = group_name.lower() + group_exists = group.existsGroup(group_name) + + if group_exists and state == "absent": + group.deleteGroup(group_name) + module.exit_json(changed=True, result="Successfully deleted group %s" % group_name) + else: + if state == "absent": + module.exit_json(changed=False, result="Group deleted or does not exists") + else: + if group_exists: + module.exit_json(changed=False) + else: + if group.createGroup(group_name, group_path): + module.exit_json(changed=True, result="Successfully created or updated the group %s" % group_name) + + +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From ab2f4c4002ff244b99397efa550eb00275df94e7 Mon Sep 17 00:00:00 2001 From: Werner Dijkerman Date: Tue, 12 Apr 2016 19:20:45 +0200 Subject: [PATCH 1420/2522] New module for creating gitlab projects (#968) --- source_control/gitlab_project.py | 397 +++++++++++++++++++++++++++++++ 1 file changed, 397 insertions(+) create mode 100644 source_control/gitlab_project.py diff --git a/source_control/gitlab_project.py b/source_control/gitlab_project.py new file mode 100644 index 00000000000..602b9e832d7 --- /dev/null +++ b/source_control/gitlab_project.py @@ -0,0 +1,397 @@ +#!/usr/bin/python +# (c) 2015, Werner Dijkerman (ikben@werner-dijkerman.nl) +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: gitlab_project +short_description: Creates/updates/deletes Gitlab Projects +description: + - When the project does not exists in Gitlab, it will be created. + - When the project does exists and state=absent, the project will be deleted. + - When changes are made to the project, the project will be updated. +version_added: "2.1" +author: "Werner Dijkerman (@dj-wasabi)" +requirements: + - pyapi-gitlab python module +options: + server_url: + description: + - Url of Gitlab server, with protocol (http or https). + required: true + validate_certs: + description: + - When using https if SSL certificate needs to be verified. + required: false + default: true + aliases: + - verify_ssl + login_user: + description: + - Gitlab user name. + required: false + default: null + login_password: + description: + - Gitlab password for login_user + required: false + default: null + login_token: + description: + - Gitlab token for logging in. + required: false + default: null + group: + description: + - The name of the group of which this projects belongs to. + - When not provided, project will belong to user which is configured in 'login_user' or 'login_token' + - When provided with username, project will be created for this user. 'login_user' or 'login_token' needs admin rights. + required: false + default: null + name: + description: + - The name of the project + required: true + path: + description: + - The path of the project you want to create, this will be server_url//path + - If not supplied, name will be used. + required: false + default: null + description: + description: + - An description for the project. + required: false + default: null + issues_enabled: + description: + - Whether you want to create issues or not. + - Possible values are true and false. + required: false + default: true + merge_requests_enabled: + description: + - If merge requests can be made or not. + - Possible values are true and false. + required: false + default: true + wiki_enabled: + description: + - If an wiki for this project should be available or not. + - Possible values are true and false. + required: false + default: true + snippets_enabled: + description: + - If creating snippets should be available or not. + - Possible values are true and false. + required: false + default: true + public: + description: + - If the project is public available or not. + - Setting this to true is same as setting visibility_level to 20. + - Possible values are true and false. + required: false + default: false + visibility_level: + description: + - Private. visibility_level is 0. Project access must be granted explicitly for each user. + - Internal. visibility_level is 10. The project can be cloned by any logged in user. + - Public. visibility_level is 20. The project can be cloned without any authentication. + - Possible values are 0, 10 and 20. + required: false + default: 0 + import_url: + description: + - Git repository which will me imported into gitlab. + - Gitlab server needs read access to this git repository. + required: false + default: false + state: + description: + - create or delete project. + - Possible values are present and absent. + required: false + default: "present" + choices: ["present", "absent"] +''' + +EXAMPLES = ''' +- name: "Delete Gitlab Project" + local_action: gitlab_project + server_url="http://gitlab.dj-wasabi.local" + validate_certs=false + login_token="WnUzDsxjy8230-Dy_k" + name=my_first_project + state=absent + +- name: "Create Gitlab Project in group Ansible" + local_action: gitlab_project + server_url="https://gitlab.dj-wasabi.local" + validate_certs=true + login_user=dj-wasabi + login_password="MySecretPassword" + name=my_first_project + group=ansible + issues_enabled=false + wiki_enabled=true + snippets_enabled=true + import_url="http://git.example.com/example/lab.git" + state=present +''' + +RETURN = '''# ''' + +try: + import gitlab + HAS_GITLAB_PACKAGE = True +except: + HAS_GITLAB_PACKAGE = False + + +class GitLabProject(object): + def __init__(self, module, git): + self._module = module + self._gitlab = git + + def createOrUpdateProject(self, project_exists, group_name, import_url, arguments): + is_user = False + group_id = self.getGroupId(group_name) + if not group_id: + group_id = self.getUserId(group_name) + is_user = True + + if project_exists: + # Edit project + return self.updateProject(group_name, arguments) + else: + # Create project + if self._module.check_mode: + self._module.exit_json(changed=True) + return self.createProject(is_user, group_id, import_url, arguments) + + def createProject(self, is_user, user_id, import_url, arguments): + if is_user: + return self._gitlab.createprojectuser(user_id=user_id, import_url=import_url, **arguments) + else: + group_id = user_id + return self._gitlab.createproject(namespace_id=group_id, import_url=import_url, **arguments) + + def deleteProject(self, group_name, project_name): + if self.existsGroup(group_name): + project_owner = group_name + else: + project_owner = self._gitlab.currentuser()['username'] + + search_results = self._gitlab.searchproject(search=project_name) + for result in search_results: + owner = result['namespace']['name'] + if owner == project_owner: + return self._gitlab.deleteproject(result['id']) + + def existsProject(self, group_name, project_name): + if self.existsGroup(group_name): + project_owner = group_name + else: + project_owner = self._gitlab.currentuser()['username'] + + search_results = self._gitlab.searchproject(search=project_name) + for result in search_results: + owner = result['namespace']['name'] + if owner == project_owner: + return True + return False + + def existsGroup(self, group_name): + if group_name is not None: + # Find the group, if group not exists we try for user + for group in self._gitlab.getall(self._gitlab.getgroups): + if group['name'] == group_name: + return True + + user_name = group_name + user_data = self._gitlab.getusers(search=user_name) + for data in user_data: + if 'id' in user_data: + return True + return False + + def getGroupId(self, group_name): + if group_name is not None: + # Find the group, if group not exists we try for user + for group in self._gitlab.getall(self._gitlab.getgroups): + if group['name'] == group_name: + return group['id'] + + def getProjectId(self, group_name, project_name): + if self.existsGroup(group_name): + project_owner = group_name + else: + project_owner = self._gitlab.currentuser()['username'] + + search_results = self._gitlab.searchproject(search=project_name) + for result in search_results: + owner = result['namespace']['name'] + if owner == project_owner: + return result['id'] + + def getUserId(self, user_name): + user_data = self._gitlab.getusers(search=user_name) + + for data in user_data: + if 'id' in data: + return data['id'] + return self._gitlab.currentuser()['id'] + + def to_bool(self, value): + if value: + return 1 + else: + return 0 + + def updateProject(self, group_name, arguments): + project_changed = False + project_name = arguments['name'] + project_id = self.getProjectId(group_name, project_name) + project_data = self._gitlab.getproject(project_id=project_id) + + for arg_key, arg_value in arguments.items(): + project_data_value = project_data[arg_key] + + if isinstance(project_data_value, bool) or project_data_value is None: + to_bool = self.to_bool(project_data_value) + if to_bool != arg_value: + project_changed = True + continue + else: + if project_data_value != arg_value: + project_changed = True + + if project_changed: + if self._module.check_mode: + self._module.exit_json(changed=True) + return self._gitlab.editproject(project_id=project_id, **arguments) + else: + return False + + +def main(): + module = AnsibleModule( + argument_spec=dict( + server_url=dict(required=True), + validate_certs=dict(required=False, default=True, type=bool, aliases=['verify_ssl']), + login_user=dict(required=False, no_log=True), + login_password=dict(required=False, no_log=True), + login_token=dict(required=False, no_log=True), + group=dict(required=False), + name=dict(required=True), + path=dict(required=False), + description=dict(required=False), + issues_enabled=dict(default=True, type=bool), + merge_requests_enabled=dict(default=True, type=bool), + wiki_enabled=dict(default=True, type=bool), + snippets_enabled=dict(default=True, type=bool), + public=dict(default=False, type=bool), + visibility_level=dict(default="0", choices=["0", "10", "20"]), + import_url=dict(required=False), + state=dict(default="present", choices=["present", 'absent']), + ), + supports_check_mode=True + ) + + if not HAS_GITLAB_PACKAGE: + module.fail_json(msg="Missing required gitlab module (check docs or install with: pip install pyapi-gitlab") + + server_url = module.params['server_url'] + verify_ssl = module.params['validate_certs'] + login_user = module.params['login_user'] + login_password = module.params['login_password'] + login_token = module.params['login_token'] + group_name = module.params['group'] + project_name = module.params['name'] + project_path = module.params['path'] + description = module.params['description'] + issues_enabled = module.params['issues_enabled'] + merge_requests_enabled = module.params['merge_requests_enabled'] + wiki_enabled = module.params['wiki_enabled'] + snippets_enabled = module.params['snippets_enabled'] + public = module.params['public'] + visibility_level = module.params['visibility_level'] + import_url = module.params['import_url'] + state = module.params['state'] + + # We need both login_user and login_password or login_token, otherwise we fail. + if login_user is not None and login_password is not None: + use_credentials = True + elif login_token is not None: + use_credentials = False + else: + module.fail_json(msg="No login credentials are given. Use login_user with login_password, or login_token") + + # Set project_path to project_name if it is empty. + if project_path is None: + project_path = project_name.replace(" ", "_") + + # Gitlab API makes no difference between upper and lower cases, so we lower them. + project_name = project_name.lower() + project_path = project_path.lower() + if group_name is not None: + group_name = group_name.lower() + + # Lets make an connection to the Gitlab server_url, with either login_user and login_password + # or with login_token + try: + if use_credentials: + git = gitlab.Gitlab(host=server_url) + git.login(user=login_user, password=login_password) + else: + git = gitlab.Gitlab(server_url, token=login_token, verify_ssl=verify_ssl) + except Exception, e: + module.fail_json(msg="Failed to connect to Gitlab server: %s " % e) + + # Validate if project exists and take action based on "state" + project = GitLabProject(module, git) + project_exists = project.existsProject(group_name, project_name) + + # Creating the project dict + arguments = {"name": project_name, + "path": project_path, + "description": description, + "issues_enabled": project.to_bool(issues_enabled), + "merge_requests_enabled": project.to_bool(merge_requests_enabled), + "wiki_enabled": project.to_bool(wiki_enabled), + "snippets_enabled": project.to_bool(snippets_enabled), + "public": project.to_bool(public), + "visibility_level": int(visibility_level)} + + if project_exists and state == "absent": + project.deleteProject(group_name, project_name) + module.exit_json(changed=True, result="Successfully deleted project %s" % project_name) + else: + if state == "absent": + module.exit_json(changed=False, result="Project deleted or does not exists") + else: + if project.createOrUpdateProject(project_exists, group_name, import_url, arguments): + module.exit_json(changed=True, result="Successfully created or updated the project %s" % project_name) + else: + module.exit_json(changed=False) + +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From c65bc5f43d5fb480825a34060006a78929a254c6 Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Tue, 5 Apr 2016 17:14:23 +0000 Subject: [PATCH 1421/2522] Add os_project_facts module This module gathers facts about OpenStack projects --- cloud/openstack/os_project_facts.py | 163 ++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 cloud/openstack/os_project_facts.py diff --git a/cloud/openstack/os_project_facts.py b/cloud/openstack/os_project_facts.py new file mode 100644 index 00000000000..87d3a1e9d76 --- /dev/null +++ b/cloud/openstack/os_project_facts.py @@ -0,0 +1,163 @@ +#!/usr/bin/python +# Copyright (c) 2016 Hewlett-Packard Enterprise Corporation +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + + +try: + import shade + HAS_SHADE = True +except ImportError: + HAS_SHADE = False + +DOCUMENTATION = ''' +--- +module: os_project_facts +short_description: Retrieve facts about one or more OpenStack projects +extends_documentation_fragment: openstack +version_added: "2.1" +author: "Ricardo Carrillo Cruz (@rcarrillocruz)" +description: + - Retrieve facts about a one or more OpenStack projects +requirements: + - "python >= 2.6" + - "shade" +options: + name: + description: + - Name or ID of the project + required: true + domain: + description: + - Name or ID of the domain containing the project if the cloud supports domains + required: false + default: None + filters: + description: + - A dictionary of meta data to use for further filtering. Elements of + this dictionary may be additional dictionaries. + required: false + default: None +''' + +EXAMPLES = ''' +# Gather facts about previously created projects +- os_project_facts: + cloud: awesomecloud +- debug: var=openstack_projects + +# Gather facts about a previously created project by name +- os_project_facts: + cloud: awesomecloud + name: demoproject +- debug: var=openstack_projects + +# Gather facts about a previously created project in a specific domain +- os_project_facts + cloud: awesomecloud + name: demoproject + domain: admindomain +- debug: var=openstack_projects + +# Gather facts about a previously created project in a specific domain + with filter +- os_project_facts + cloud: awesomecloud + name: demoproject + domain: admindomain + filters: + enabled: False +- debug: var=openstack_projects +''' + + +RETURN = ''' +openstack_projects: + description: has all the OpenStack facts about projects + returned: always, but can be null + type: complex + contains: + id: + description: Unique UUID. + returned: success + type: string + name: + description: Name given to the project. + returned: success + type: string + description: + description: Description of the project + returned: success + type: string + enabled: + description: Flag to indicate if the project is enabled + returned: success + type: bool + domain_id: + description: Domain ID containing the project (keystone v3 clouds only) + returned: success + type: bool +''' + +def main(): + + argument_spec = openstack_full_argument_spec( + name=dict(required=False, default=None), + domain=dict(required=False, default=None), + filters=dict(required=False, type='dict', default=None), + ) + + module = AnsibleModule(argument_spec) + + if not HAS_SHADE: + module.fail_json(msg='shade is required for this module') + + try: + name = module.params['name'] + domain = module.params['domain'] + filters = module.params['filters'] + + opcloud = shade.operator_cloud(**module.params) + + if domain: + try: + # We assume admin is passing domain id + dom = opcloud.get_domain(domain)['id'] + domain = dom + except: + # If we fail, maybe admin is passing a domain name. + # Note that domains have unique names, just like id. + dom = opcloud.search_domains(filters={'name': domain}) + if dom: + domain = dom[0]['id'] + else: + module.fail_json(msg='Domain name or ID does not exist') + + if not filters: + filters = {} + + filters['domain_id'] = domain + + projects = opcloud.search_projects(name, filters) + module.exit_json(changed=False, ansible_facts=dict( + openstack_projects=projects)) + + except shade.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * + +if __name__ == '__main__': + main() From 10def11d39f787a810834610251201d68bdd765e Mon Sep 17 00:00:00 2001 From: Jens Carl Date: Tue, 12 Apr 2016 15:10:41 -0700 Subject: [PATCH 1422/2522] Fix code example (#2018) --- cloud/amazon/GUIDELINES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/GUIDELINES.md b/cloud/amazon/GUIDELINES.md index 0c831946be6..017ff9090db 100644 --- a/cloud/amazon/GUIDELINES.md +++ b/cloud/amazon/GUIDELINES.md @@ -79,7 +79,7 @@ except ImportError: def main(): - if not HAS_BOTO: + if not HAS_BOTO3: module.fail_json(msg='boto required for this module') ``` From 85c1440edea9d2cefdd85c686770df6ea523c060 Mon Sep 17 00:00:00 2001 From: Jasper Lievisse Adriaanse Date: Wed, 13 Apr 2016 11:02:42 +0200 Subject: [PATCH 1423/2522] Tweak and extend the pkgin module - make path to pkgin a global and stop passing it around; it's not going to change while ansible is running - add support for several new options: * upgrade * full_upgrade * force * clean - allow for update_cache to be run in the same task as upgrading/installing packages instead of needing a separate task for that --- packaging/os/pkgin.py | 158 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 131 insertions(+), 27 deletions(-) diff --git a/packaging/os/pkgin.py b/packaging/os/pkgin.py index 5277f218242..cd48385eefe 100755 --- a/packaging/os/pkgin.py +++ b/packaging/os/pkgin.py @@ -3,6 +3,7 @@ # Copyright (c) 2013 Shaun Zinck # Copyright (c) 2015 Lawrence Leonard Gilbert +# Copyright (c) 2016 Jasper Lievisse Adriaanse # # Written by Shaun Zinck # Based on pacman module written by Afterburn @@ -33,6 +34,7 @@ author: - "Larry Gilbert (L2G)" - "Shaun Zinck (@szinck)" + - "Jasper Lievisse Adriaanse (@jasperla)" notes: - "Known bug with pkgin < 0.8.0: if a package is removed and another package depends on it, the other package will be silently removed as @@ -57,6 +59,34 @@ default: no choices: [ "yes", "no" ] version_added: "2.1" + upgrade: + description: + - Upgrade main packages to their newer versions + required: false + default: no + choices: [ "yes", "no" ] + version_added: "2.1" + full_upgrade: + description: + - Upgrade all packages to their newer versions + required: false + default: no + choices: [ "yes", "no" ] + version_added: "2.1" + clean: + description: + - Clean packages cache + required: false + default: no + choices: [ "yes", "no" ] + version_added: "2.1" + force: + description: + - Force package reinstall + required: false + default: no + choices: [ "yes", "no" ] + version_added: "2.1" ''' EXAMPLES = ''' @@ -74,12 +104,24 @@ # Update repositories as a separate step - pkgin: update_cache=yes + +# Upgrade main packages (equivalent to C(pkgin upgrade)) +- pkgin: upgrade=yes + +# Upgrade all packages (equivalent to C(pkgin full-upgrade)) +- pkgin: full_upgrade=yes + +# Force-upgrade all packages (equivalent to C(pkgin -F full-upgrade)) +- pkgin: full_upgrade=yes force=yes + +# clean packages cache (equivalent to C(pkgin clean)) +- pkgin: clean=yes ''' import re -def query_package(module, pkgin_path, name): +def query_package(module, name): """Search for the package by name. Possible return values: @@ -89,7 +131,7 @@ def query_package(module, pkgin_path, name): """ # test whether '-p' (parsable) flag is supported. - rc, out, err = module.run_command("%s -p -v" % pkgin_path) + rc, out, err = module.run_command("%s -p -v" % PKGIN_PATH) if rc == 0: pflag = '-p' @@ -100,7 +142,7 @@ def query_package(module, pkgin_path, name): # Use "pkgin search" to find the package. The regular expression will # only match on the complete name. - rc, out, err = module.run_command("%s %s search \"^%s$\"" % (pkgin_path, pflag, name)) + rc, out, err = module.run_command("%s %s search \"^%s$\"" % (PKGIN_PATH, pflag, name)) # rc will not be 0 unless the search was a success if rc == 0: @@ -162,37 +204,43 @@ def format_action_message(module, action, count): return message + "s" -def format_pkgin_command(module, pkgin_path, command, package=None): +def format_pkgin_command(module, command, package=None): # Not all commands take a package argument, so cover this up by passing # an empty string. Some commands (e.g. 'update') will ignore extra # arguments, however this behaviour cannot be relied on for others. if package is None: package = "" - vars = { "pkgin": pkgin_path, + if module.params["force"]: + force = "-F" + else: + force = "" + + vars = { "pkgin": PKGIN_PATH, "command": command, - "package": package } + "package": package, + "force": force} if module.check_mode: return "%(pkgin)s -n %(command)s %(package)s" % vars else: - return "%(pkgin)s -y %(command)s %(package)s" % vars + return "%(pkgin)s -y %(force)s %(command)s %(package)s" % vars -def remove_packages(module, pkgin_path, packages): +def remove_packages(module, packages): remove_c = 0 # Using a for loop incase of error, we can report the package that failed for package in packages: # Query the package first, to see if we even need to remove - if not query_package(module, pkgin_path, package): + if not query_package(module, package): continue rc, out, err = module.run_command( - format_pkgin_command(module, pkgin_path, "remove", package)) + format_pkgin_command(module, "remove", package)) - if not module.check_mode and query_package(module, pkgin_path, package): + if not module.check_mode and query_package(module, package): module.fail_json(msg="failed to remove %s: %s" % (package, out)) remove_c += 1 @@ -203,18 +251,18 @@ def remove_packages(module, pkgin_path, packages): module.exit_json(changed=False, msg="package(s) already absent") -def install_packages(module, pkgin_path, packages): +def install_packages(module, packages): install_c = 0 for package in packages: - if query_package(module, pkgin_path, package): + if query_package(module, package): continue rc, out, err = module.run_command( - format_pkgin_command(module, pkgin_path, "install", package)) + format_pkgin_command(module, "install", package)) - if not module.check_mode and not query_package(module, pkgin_path, package): + if not module.check_mode and not query_package(module, package): module.fail_json(msg="failed to install %s: %s" % (package, out)) install_c += 1 @@ -224,41 +272,97 @@ def install_packages(module, pkgin_path, packages): module.exit_json(changed=False, msg="package(s) already present") -def update_package_db(module, pkgin_path): +def update_package_db(module): rc, out, err = module.run_command( - format_pkgin_command(module, pkgin_path, "update")) + format_pkgin_command(module, "update")) if rc == 0: - return True + if re.search('database for.*is up-to-date\n$', out): + return False, "datebase is up-to-date" + else: + return True, "updated repository database" else: module.fail_json(msg="could not update package db") +def do_upgrade_packages(module, full=False): + if full: + cmd = "full-upgrade" + else: + cmd = "upgrade" + + rc, out, err = module.run_command( + format_pkgin_command(module, cmd)) + + if rc == 0: + if re.search('^nothing to do.\n$', out): + module.exit_json(changed=False, msg="nothing left to upgrade") + else: + module.fail_json(msg="could not %s packages" % cmd) + +def upgrade_packages(module): + do_upgrade_packages(module) + +def full_upgrade_packages(module): + do_upgrade_packages(module, True) + +def clean_cache(module): + rc, out, err = module.run_command( + format_pkgin_command(module, "clean")) + + if rc == 0: + # There's no indication if 'clean' actually removed anything, + # so assume it did. + module.exit_json(changed=True, msg="cleaned caches") + else: + module.fail_json(msg="could not clean package cache") def main(): module = AnsibleModule( argument_spec = dict( state = dict(default="present", choices=["present","absent"]), name = dict(aliases=["pkg"], type='list'), - update_cache = dict(default='no', type='bool')), - required_one_of = [['name', 'update_cache']], + update_cache = dict(default='no', type='bool'), + upgrade = dict(default='no', type='bool'), + full_upgrade = dict(default='no', type='bool'), + clean = dict(default='no', type='bool'), + force = dict(default='no', type='bool')), + required_one_of = [['name', 'update_cache', 'upgrade', 'full_upgrade', 'clean']], supports_check_mode = True) - pkgin_path = module.get_bin_path('pkgin', True, ['/opt/local/bin']) + global PKGIN_PATH + PKGIN_PATH = module.get_bin_path('pkgin', True, ['/opt/local/bin']) - p = module.params + module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C') - pkgs = p["name"] + p = module.params if p["update_cache"]: - update_package_db(module, pkgin_path) + c, msg = update_package_db(module) + if not (p['name'] or p["upgrade"] or p["full_upgrade"]): + module.exit_json(changed=c, msg=msg) + + if p["upgrade"]: + upgrade_packages(module) if not p['name']: - module.exit_json(changed=True, msg='updated repository database') + module.exit_json(changed=True, msg='upgraded packages') + + if p["full_upgrade"]: + full_upgrade_packages(module) + if not p['name']: + module.exit_json(changed=True, msg='upgraded all packages') + + if p["clean"]: + clean_cache(module) + if not p['name']: + module.exit_json(changed=True, msg='cleaned caches') + + pkgs = p["name"] if p["state"] == "present": - install_packages(module, pkgin_path, pkgs) + install_packages(module, pkgs) elif p["state"] == "absent": - remove_packages(module, pkgin_path, pkgs) + remove_packages(module, pkgs) # import module snippets from ansible.module_utils.basic import * From 2b8debbc2bda8bb55f627e8b8047812aa59409af Mon Sep 17 00:00:00 2001 From: Jasper Lievisse Adriaanse Date: Wed, 13 Apr 2016 16:03:26 +0200 Subject: [PATCH 1424/2522] Sprinkle some LANG/LC_* where command output is parsed (#2019) --- packaging/os/homebrew.py | 3 +++ packaging/os/homebrew_cask.py | 3 +++ system/svc.py | 2 ++ 3 files changed, 8 insertions(+) mode change 100644 => 100755 packaging/os/homebrew.py mode change 100644 => 100755 packaging/os/homebrew_cask.py mode change 100644 => 100755 system/svc.py diff --git a/packaging/os/homebrew.py b/packaging/os/homebrew.py old mode 100644 new mode 100755 index 94d0ef865c4..077fd46dcc6 --- a/packaging/os/homebrew.py +++ b/packaging/os/homebrew.py @@ -813,6 +813,9 @@ def main(): ), supports_check_mode=True, ) + + module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C') + p = module.params if p['name']: diff --git a/packaging/os/homebrew_cask.py b/packaging/os/homebrew_cask.py old mode 100644 new mode 100755 index e1b721a97b4..aa7d7ed84b8 --- a/packaging/os/homebrew_cask.py +++ b/packaging/os/homebrew_cask.py @@ -481,6 +481,9 @@ def main(): ), supports_check_mode=True, ) + + module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C') + p = module.params if p['name']: diff --git a/system/svc.py b/system/svc.py old mode 100644 new mode 100755 index 0d3a83f6305..6cc8c1d21ef --- a/system/svc.py +++ b/system/svc.py @@ -249,6 +249,8 @@ def main(): supports_check_mode=True, ) + module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C') + state = module.params['state'] enabled = module.params['enabled'] downed = module.params['downed'] From 4b3ab52374cc6e17cc88e290f016e0a4d0057f6b Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Wed, 13 Apr 2016 20:32:47 +0200 Subject: [PATCH 1425/2522] Fix arguments for pushover module Since user_key and app_token are used for authentication, I suspect both of them should be kept secret. According to the API manual, https://pushover.net/api priority go from -2 to 2, so the argument should be constrained. --- notification/pushover.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/notification/pushover.py b/notification/pushover.py index 29afcaa6356..2cd973b1bcc 100644 --- a/notification/pushover.py +++ b/notification/pushover.py @@ -95,9 +95,9 @@ def main(): module = AnsibleModule( argument_spec=dict( msg=dict(required=True), - app_token=dict(required=True), - user_key=dict(required=True), - pri=dict(required=False, default=0), + app_token=dict(required=True, no_log=True), + user_key=dict(required=True, no_log=True), + pri=dict(required=False, default='0', choices=['-2','-1','0','1','2']), ), ) From 8c6a3e732edc2f70be63702c46d8d295553f5e33 Mon Sep 17 00:00:00 2001 From: "Christopher M. Fuhrman" Date: Thu, 14 Apr 2016 03:51:29 -0700 Subject: [PATCH 1426/2522] pkgin: Fix bad regexp which did not catch packages such as p5-SVN-Notify The previous version of my regexp did not take into account packages such as 'p5-Perl-Tidy' or 'p5-Test-Output', so use a greedy match up to the last occurrance of '-' for matching the package. This regex has been extensively tested using all packages as provided by pkgsrc-2016Q1[1]. Footnotes: [1] http://cvsweb.netbsd.org/bsdweb.cgi/pkgsrc/?only_with_tag=pkgsrc-2016Q1 --- packaging/os/pkgin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/os/pkgin.py b/packaging/os/pkgin.py index cd48385eefe..055891ebe08 100755 --- a/packaging/os/pkgin.py +++ b/packaging/os/pkgin.py @@ -164,7 +164,7 @@ def query_package(module, name): # Search for package, stripping version # (results in sth like 'gcc47-libs' or 'emacs24-nox11') - pkg_search_obj = re.search(r'^([a-zA-Z]+[0-9]*[\-]*\w*)-[0-9]', pkgname_with_version, re.M) + pkg_search_obj = re.search(r'^(.*?)\-[0-9][0-9.]*(nb[0-9]+)*', pkgname_with_version, re.M) # Do not proceed unless we have a match if not pkg_search_obj: From e8dbb4e4f290486dbcb114e35ec10205649d7a17 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Thu, 14 Apr 2016 21:15:07 +0200 Subject: [PATCH 1427/2522] Mark conf_file as a path, for various user expansion --- packaging/os/dnf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index 2bd279785f6..8df9401fa16 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -323,7 +323,7 @@ def main(): enablerepo=dict(type='list', default=[]), disablerepo=dict(type='list', default=[]), list=dict(), - conf_file=dict(default=None), + conf_file=dict(default=None, type='path'), disable_gpg_check=dict(default=False, type='bool'), ), required_one_of=[['name', 'list']], From 7a8d3cf392d4afcc263955192564774d722da466 Mon Sep 17 00:00:00 2001 From: = Date: Thu, 14 Apr 2016 21:22:10 +0100 Subject: [PATCH 1428/2522] Further fixes to support binary data. Added boolean return values and return documentation. --- windows/win_regedit.ps1 | 37 +++++++++++++++++++++++++++++++++++-- windows/win_regedit.py | 13 +++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/windows/win_regedit.ps1 b/windows/win_regedit.ps1 index 806cc4ad5e4..1b5c9cf9918 100644 --- a/windows/win_regedit.ps1 +++ b/windows/win_regedit.ps1 @@ -28,6 +28,8 @@ New-PSDrive -PSProvider registry -Root HKEY_CURRENT_CONFIG -Name HCCC -ErrorActi $params = Parse-Args $args; $result = New-Object PSObject; Set-Attr $result "changed" $false; +Set-Attr $result "data_changed" $false; +Set-Attr $result "data_type_changed" $false; $registryKey = Get-Attr -obj $params -name "key" -failifempty $true $registryValue = Get-Attr -obj $params -name "value" -default $null @@ -56,6 +58,31 @@ Function Test-RegistryValueData { } } +# Returns rue if registry data matches. +# Handles binary and string registry data +Function Compare-RegistryData { + Param ( + [parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()]$ReferenceData, + [parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()]$DifferenceData + ) + $refType = $ReferenceData.GetType().Name + + if ($refType -eq "String" ) { + if ($ReferenceData -eq $DifferenceData) { + return $true + } else { + return $false + } + } elseif ($refType -eq "Object[]") { + if (@(Compare-Object $ReferenceData $DifferenceData -SyncWindow 0).Length -eq 0) { + return $true + } else { + return $false + } + } +} # Simplified version of Convert-HexStringToByteArray from # https://cyber-defense.sans.org/blog/2010/02/11/powershell-byte-array-hex-convert @@ -64,7 +91,7 @@ Function Test-RegistryValueData { function Convert-RegExportHexStringToByteArray { Param ( - [parameter(Mandatory=$true))] [String] $String + [parameter(Mandatory=$true)] [String] $String ) # remove 'hex:' from the front of the string if present @@ -100,6 +127,9 @@ if($state -eq "present") { { if (Test-RegistryValueData -Path $registryKey -Value $registryValue) { + # handle binary data + $currentRegistryData =(Get-ItemProperty -Path $registryKey | Select-Object -ExpandProperty $registryValue) + if ($registryValue.ToLower() -eq "(default)") { # Special case handling for the key's default property. Because .GetValueKind() doesn't work for the (default) key property $oldRegistryDataType = "String" @@ -116,6 +146,8 @@ if($state -eq "present") { Remove-ItemProperty -Path $registryKey -Name $registryValue New-ItemProperty -Path $registryKey -Name $registryValue -Value $registryData -PropertyType $registryDataType $result.changed = $true + $result.data_changed = $true + $result.data_type_changed = $true } Catch { @@ -123,11 +155,12 @@ if($state -eq "present") { } } # Changes Only Data - elseif ((Get-ItemProperty -Path $registryKey | Select-Object -ExpandProperty $registryValue) -ne $registryData) + elseif (-Not (Compare-RegistryData -ReferenceData $currentRegistryData -DifferenceData $registryData)) { Try { Set-ItemProperty -Path $registryKey -Name $registryValue -Value $registryData $result.changed = $true + $result.data_changed = $true } Catch { diff --git a/windows/win_regedit.py b/windows/win_regedit.py index 3317d6e8dc4..f428d1fcbde 100644 --- a/windows/win_regedit.py +++ b/windows/win_regedit.py @@ -116,3 +116,16 @@ value: hello state: absent ''' +RETURN = ''' +data_changed: + description: whether this invocation changed the data in the registry value + returned: always + type: boolean + sample: false +data_type_changed: + description: whether this invocation changed the datatype of the registry value + returned: always + type: boolean + sample: true +''' + From 393e43b876523fa48c3e399743c0cf6f548dd0bb Mon Sep 17 00:00:00 2001 From: James Cammarata Date: Thu, 14 Apr 2016 12:30:34 -0400 Subject: [PATCH 1429/2522] Fixing/cleaning up kubernetes submission 1) Removed kubectl functionality. We'll move that into a different module in the future. Also removed post/put/patch/delete options, as they are not Ansible best practice. 2) Expanded error handling in areas where tracebacks were most likely, based on bad data from users, etc. 3) Added an 'insecure' option and made the password param optional, to enable the use of the local insecure port. 4) Allowed the data (both inline and from the file) to support multiple items via a list. This is common in YAML files where mutliple docs are used to create/remove multiple resources in one shot. 5) General bug fixing. --- clustering/kubernetes.py | 301 ++++++++++++++++----------------------- 1 file changed, 122 insertions(+), 179 deletions(-) diff --git a/clustering/kubernetes.py b/clustering/kubernetes.py index 2b4921a0136..7fc2ad92174 100644 --- a/clustering/kubernetes.py +++ b/clustering/kubernetes.py @@ -28,9 +28,6 @@ Only supports HTTP Basic Auth Only supports 'strategic merge' for update, http://goo.gl/fCPYxT SSL certs are not working, use 'validate_certs=off' to disable - This module can mimic the 'kubectl' Kubernetes client for commands - such as 'get', 'cluster-info', and 'version'. This is useful if you - want to fetch full object details for existing Kubernetes resources. options: api_endpoint: description: @@ -40,12 +37,14 @@ aliases: ["endpoint"] inline_data: description: - - The Kubernetes YAML data to send to the API I(endpoint). + - The Kubernetes YAML data to send to the API I(endpoint). This option is + mutually exclusive with C('file_reference'). required: true default: null file_reference: description: - Specify full path to a Kubernets YAML file to send to API I(endpoint). + This option is mutually exclusive with C('inline_data'). required: false default: null certificate_authority_data: @@ -56,53 +55,27 @@ 'match_hostname' that can match the IP address against the CA data. required: false default: null - kubectl_api_versions: - description: - - Mimic the 'kubectl api-versions' command, values are ignored. - required: false - default: null - kubectl_cluster_info: - description: - - Mimic the 'kubectl cluster-info' command, values are ignored. - required: false - default: null - kubectl_get: - description: - - Mimic the 'kubectl get' command. Specify the object(s) to fetch such - as 'pods' or 'replicationcontrollers/mycontroller'. It does not - support shortcuts (e.g. 'po', 'rc', 'svc'). - required: false - default: null - kubectl_namespace: - description: - - Specify the namespace to use for 'kubectl' commands. - required: false - default: "default" - kubectl_version: - description: - - Mimic the 'kubectl version' command, values are ignored. - required: false - default: null state: description: - - The desired action to take on the Kubernetes data, or 'kubectl' to - mimic some kubectl commands. + - The desired action to take on the Kubernetes data. required: true default: "present" - choices: ["present", "post", "absent", "delete", "update", "patch", - "replace", "put", "kubectl"] - url_password: + choices: ["present", "absent", "update", "replace"] + password: description: - - The HTTP Basic Auth password for the API I(endpoint). - required: true + - The HTTP Basic Auth password for the API I(endpoint). This should be set + unless using the C('insecure') option. default: null - aliases: ["password", "api_password"] - url_username: + username: description: - - The HTTP Basic Auth username for the API I(endpoint). - required: true + - The HTTP Basic Auth username for the API I(endpoint). This should be set + unless using the C('insecure') option. default: "admin" - aliases: ["username", "api_username"] + insecure: + description: + - "Reverts the connection to using HTTP instead of HTTPS. This option should + only be used when execuing the M('kubernetes') module local to the Kubernetes + cluster using the insecure local port (locahost:8080 by default)." validate_certs: description: - Enable/disable certificate validation. Note that this is set to @@ -110,7 +83,6 @@ hostname matching (exists in >= python3.5.0). required: false default: false - choices: BOOLEANS author: "Eric Johnson (@erjohnso) " ''' @@ -120,8 +92,8 @@ - name: Create a kubernetes namespace kubernetes: api_endpoint: 123.45.67.89 - url_username: admin - url_password: redacted + username: admin + password: redacted inline_data: kind: Namespace apiVersion: v1 @@ -139,28 +111,19 @@ - name: Create a kubernetes namespace kubernetes: api_endpoint: 123.45.67.89 - url_username: admin - url_password: redacted + username: admin + password: redacted file_reference: /path/to/create_namespace.yaml state: present -# Fetch info about the Kubernets cluster with a fake 'kubectl' command. -- name: Look up cluster info +# Do the same thing, but using the insecure localhost port +- name: Create a kubernetes namespace kubernetes: api_endpoint: 123.45.67.89 - url_username: admin - url_password: redacted - kubectl_cluster_info: 1 - state: kubectl + insecure: true + file_reference: /path/to/create_namespace.yaml + state: present -# Fetch info about the Kubernets pods with a fake 'kubectl' command. -- name: Look up pods - kubernetes: - api_endpoint: 123.45.67.89 - url_username: admin - url_password: redacted - kubectl_get: pods - state: kubectl ''' RETURN = ''' @@ -259,6 +222,7 @@ # - Set 'required=true' for certificate_authority_data and ensure that # ansible's SSLValidationHandler.get_ca_certs() can pick up this CA cert # - Set 'required=true' for the validate_certs param. + def decode_cert_data(module): return d = module.params.get("certificate_authority_data") @@ -270,75 +234,70 @@ def api_request(module, url, method="GET", headers=None, data=None): body = None if data: data = json.dumps(data) - response, info = fetch_url(module, url, method=method, headers=headers, - data=data) + response, info = fetch_url(module, url, method=method, headers=headers, data=data) + if int(info['status']) == -1: + module.fail_json(msg="Failed to execute the API request: %s" % info['msg'], url=url, method=method, headers=headers) if response is not None: body = json.loads(response.read()) return info, body -def k8s_kubectl_get(module, url): - req = module.params.get("kubectl_get") - info, body = api_request(module, url + "/" + req) - return False, body +def k8s_create_resource(module, url, data): + info, body = api_request(module, url, method="POST", data=data, headers={"Content-Type": "application/json"}) + if info['status'] == 409: + name = data["metadata"].get("name", None) + info, body = api_request(module, url + "/" + name) + return False, body + elif info['status'] >= 400: + module.fail_json(msg="failed to create the resource: %s" % info['msg'], url=url) + return True, body def k8s_delete_resource(module, url, data): - name = None - if 'metadata' in data: - name = data['metadata'].get('name') + name = data.get('metadata', {}).get('name') if name is None: - module.fail_json(msg="Missing a named resource in object metadata") - url = url + '/' + name + module.fail_json(msg="Missing a named resource in object metadata when trying to remove a resource") + url = url + '/' + name info, body = api_request(module, url, method="DELETE") if info['status'] == 404: - return False, {} - if info['status'] == 200: - return True, body - module.fail_json(msg="%s: fetching URL '%s'" % (info['msg'], url)) - - -def k8s_create_resource(module, url, data): - info, body = api_request(module, url, method="POST", data=data) - if info['status'] == 409: - name = data["metadata"].get("name", None) - info, body = api_request(module, url + "/" + name) - return False, body - return True, body + return False, "Resource name '%s' already absent" % name + elif info['status'] >= 400: + module.fail_json(msg="failed to delete the resource '%s': %s" % (name, info['msg']), url=url) + return True, "Successfully deleted resource name '%s'" % name def k8s_replace_resource(module, url, data): - name = None - if 'metadata' in data: - name = data['metadata'].get('name') + name = data.get('metadata', {}).get('name') if name is None: - module.fail_json(msg="Missing a named resource in object metadata") - url = url + '/' + name + module.fail_json(msg="Missing a named resource in object metadata when trying to replace a resource") - info, body = api_request(module, url, method="PUT", data=data) + headers = {"Content-Type": "application/json"} + url = url + '/' + name + info, body = api_request(module, url, method="PUT", data=data, headers=headers) if info['status'] == 409: name = data["metadata"].get("name", None) info, body = api_request(module, url + "/" + name) return False, body + elif info['status'] >= 400: + module.fail_json(msg="failed to replace the resource '%s': %s" % (name, info['msg']), url=url) return True, body def k8s_update_resource(module, url, data): - name = None - if 'metadata' in data: - name = data['metadata'].get('name') + name = data.get('metadata', {}).get('name') if name is None: - module.fail_json(msg="Missing a named resource in object metadata") - url = url + '/' + name + module.fail_json(msg="Missing a named resource in object metadata when trying to update a resource") headers = {"Content-Type": "application/strategic-merge-patch+json"} - info, body = api_request(module, url, method="PATCH", data=data, - headers=headers) + url = url + '/' + name + info, body = api_request(module, url, method="PATCH", data=data, headers=headers) if info['status'] == 409: name = data["metadata"].get("name", None) info, body = api_request(module, url + "/" + name) return False, body + elif info['status'] >= 400: + module.fail_json(msg="failed to update the resource '%s': %s" % (name, info['msg']), url=url) return True, body @@ -347,97 +306,81 @@ def main(): argument_spec=dict( http_agent=dict(default=USER_AGENT), - url_username=dict(default="admin"), - url_password=dict(required=True, no_log=True), + username=dict(default="admin"), + password=dict(default="", no_log=True), force_basic_auth=dict(default="yes"), - validate_certs=dict(default=False, choices=BOOLEANS), + validate_certs=dict(default=False, type='bool'), certificate_authority_data=dict(required=False), - - # fake 'kubectl' commands - kubectl_api_versions=dict(required=False), - kubectl_cluster_info=dict(required=False), - kubectl_get=dict(required=False), - kubectl_namespace=dict(required=False, default="default"), - kubectl_version=dict(required=False), - - # k8s API module variables + insecure=dict(default=False, type='bool'), api_endpoint=dict(required=True), file_reference=dict(required=False), inline_data=dict(required=False), - state=dict(default="present", - choices=["present", "post", - "absent", "delete", - "update", "put", - "replace", "patch", - "kubectl"]) - ) + state=dict(default="present", choices=["present", "absent", "update", "replace"]) + ), + mutually_exclusive = (('file_reference', 'inline_data'), ('username', 'insecure'), ('password', 'insecure')), + required_one_of = (('file_reference', 'inline_data'),), ) decode_cert_data(module) + api_endpoint = module.params.get('api_endpoint') + state = module.params.get('state') + insecure = module.params.get('insecure') + inline_data = module.params.get('inline_data') + file_reference = module.params.get('file_reference') + + if inline_data: + data = inline_data + else: + try: + f = open(file_reference, "r") + data = [x for x in yaml.load_all(f)] + f.close() + if not data: + module.fail_json(msg="No valid data could be found.") + except: + module.fail_json(msg="The file '%s' was not found or contained invalid YAML/JSON data" % file_reference) + + # set the transport type and build the target endpoint url + transport = 'https' + if insecure: + transport = 'http' + + target_endpoint = "%s://%s" % (transport, api_endpoint) + + body = [] changed = False - data = module.params.get('inline_data', {}) - if not data: - dfile = module.params.get('file_reference') - if dfile: - f = open(dfile, "r") - data = yaml.load(f) - - endpoint = "https://" + module.params.get('api_endpoint') - url = endpoint - - namespace = "default" - if data and 'metadata' in data: - namespace = data['metadata'].get('namespace', "default") - kind = data['kind'].lower() - url = endpoint + KIND_URL[kind] - url = url.replace("{namespace}", namespace) - - # check for 'kubectl' commands - kubectl_api_versions = module.params.get('kubectl_api_versions') - kubectl_cluster_info = module.params.get('kubectl_cluster_info') - kubectl_get = module.params.get('kubectl_get') - kubectl_namespace = module.params.get('kubectl_namespace') - kubectl_version = module.params.get('kubectl_version') - state = module.params.get('state') - if state in ['present', 'post']: - changed, body = k8s_create_resource(module, url, data) - module.exit_json(changed=changed, api_response=body) - - if state in ['absent', 'delete']: - changed, body = k8s_delete_resource(module, url, data) - module.exit_json(changed=changed, api_response=body) - - if state in ['replace', 'put']: - changed, body = k8s_replace_resource(module, url, data) - module.exit_json(changed=changed, api_response=body) - - if state in ['update', 'patch']: - changed, body = k8s_update_resource(module, url, data) - module.exit_json(changed=changed, api_response=body) - - if state == 'kubectl': - kurl = url + "/api/v1/namespaces/" + kubectl_namespace - if kubectl_get: - if kubectl_get.startswith("namespaces"): - kurl = url + "/api/v1" - changed, body = k8s_kubectl_get(module, kurl) - module.exit_json(changed=changed, api_response=body) - if kubectl_version: - info, body = api_request(module, url + "/version") - module.exit_json(changed=False, api_response=body) - if kubectl_api_versions: - info, body = api_request(module, url + "/api") - module.exit_json(changed=False, api_response=body) - if kubectl_cluster_info: - info, body = api_request(module, url + - "/api/v1/namespaces/kube-system" - "/services?labelSelector=kubernetes" - ".io/cluster-service=true") - module.exit_json(changed=False, api_response=body) - - module.fail_json(msg="Invalid state: '%s'" % state) + # make sure the data is a list + if not isinstance(data, list): + data = [ data ] + + for item in data: + namespace = "default" + if item and 'metadata' in item: + namespace = item.get('metadata', {}).get('namespace', "default") + kind = item.get('kind', '').lower() + try: + url = target_endpoint + KIND_URL[kind] + except KeyError: + module.fail_json("invalid resource kind specified in the data: '%s'" % kind) + url = url.replace("{namespace}", namespace) + else: + url = target_endpoint + + if state == 'present': + item_changed, item_body = k8s_create_resource(module, url, item) + elif state == 'absent': + item_changed, item_body = k8s_delete_resource(module, url, item) + elif state == 'replace': + item_changed, item_body = k8s_replace_resource(module, url, item) + elif state == 'update': + item_changed, item_body = k8s_update_resource(module, url, item) + + changed |= item_changed + body.append(item_body) + + module.exit_json(changed=changed, api_response=body) # import module snippets From bd0deed367b94c7aedf7d006d5a6f4d4a18ad62f Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Thu, 14 Apr 2016 23:37:01 +0200 Subject: [PATCH 1430/2522] Use type=path for pem_file, since that's a file (#1934) --- cloud/google/gce_img.py | 2 +- cloud/google/gce_tag.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/google/gce_img.py b/cloud/google/gce_img.py index b64f12febd0..bf3d9da5356 100644 --- a/cloud/google/gce_img.py +++ b/cloud/google/gce_img.py @@ -180,7 +180,7 @@ def main(): state=dict(default='present', choices=['present', 'absent']), zone=dict(default='us-central1-a'), service_account_email=dict(), - pem_file=dict(), + pem_file=dict(type='path'), project_id=dict(), timeout=dict(type='int', default=180) ) diff --git a/cloud/google/gce_tag.py b/cloud/google/gce_tag.py index 4f60f58f760..cb1f2a2c3ed 100644 --- a/cloud/google/gce_tag.py +++ b/cloud/google/gce_tag.py @@ -188,7 +188,7 @@ def main(): state=dict(default='present', choices=['present', 'absent']), zone=dict(default='us-central1-a'), service_account_email=dict(), - pem_file=dict(), + pem_file=dict(type='path'), project_id=dict(), ) ) From 84ca3ba7eecbcb9c1ebcfbcbb6ab98f97d1c097e Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Thu, 14 Apr 2016 23:44:28 +0200 Subject: [PATCH 1431/2522] Do not use a default value for -n parameter, fix #1400 (#1417) --- messaging/rabbitmq_user.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/messaging/rabbitmq_user.py b/messaging/rabbitmq_user.py index ba77e47c998..85921ce45c7 100644 --- a/messaging/rabbitmq_user.py +++ b/messaging/rabbitmq_user.py @@ -144,7 +144,9 @@ def __init__(self, module, username, password, tags, permissions, def _exec(self, args, run_in_check_mode=False): if not self.module.check_mode or (self.module.check_mode and run_in_check_mode): - cmd = [self._rabbitmqctl, '-q', '-n', self.node] + cmd = [self._rabbitmqctl, '-q'] + if self.node is not None: + cmd.append(['-n', self.node]) rc, out, err = self.module.run_command(cmd + args, check_rc=True) return out.splitlines() return list() @@ -235,7 +237,7 @@ def main(): read_priv=dict(default='^$'), force=dict(default='no', type='bool'), state=dict(default='present', choices=['present', 'absent']), - node=dict(default='rabbit') + node=dict(default=None) ) module = AnsibleModule( argument_spec=arg_spec, From 3afe117730c7c4e046254fdd77c25fd01761fd58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Jos=C3=A9=20Pando?= Date: Thu, 14 Apr 2016 17:58:44 -0400 Subject: [PATCH 1432/2522] Add SQS queue policy attachment functionality (#1716) * Add SQS queue policy attachment functionality SQS queue has no attribute 'Policy' until one is attached, so this special case must be handled uniquely SQS queue Policy can now be passed in as json --- cloud/amazon/sqs_queue.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/cloud/amazon/sqs_queue.py b/cloud/amazon/sqs_queue.py index de0ca7ebff1..a16db036b01 100644 --- a/cloud/amazon/sqs_queue.py +++ b/cloud/amazon/sqs_queue.py @@ -22,7 +22,9 @@ - Create or delete AWS SQS queues. - Update attributes on existing queues. version_added: "2.0" -author: Alan Loi (@loia) +author: + - Alan Loi (@loia) + - Fernando Jose Pando (@nand0p) requirements: - "boto >= 2.33.0" options: @@ -61,6 +63,12 @@ - The receive message wait time in seconds. required: false default: null + policy: + description: + - The json dict policy to attach to queue + required: false + default: null + version_added: "2.1" extends_documentation_fragment: - aws - ec2 @@ -76,6 +84,7 @@ maximum_message_size: 1024 delivery_delay: 30 receive_message_wait_time: 20 + policy: "{{ json_dict }}" # Delete SQS queue - sqs_queue: @@ -102,6 +111,7 @@ def create_or_update_sqs_queue(connection, module): maximum_message_size=module.params.get('maximum_message_size'), delivery_delay=module.params.get('delivery_delay'), receive_message_wait_time=module.params.get('receive_message_wait_time'), + policy=module.params.get('policy'), ) result = dict( @@ -136,7 +146,8 @@ def update_sqs_queue(queue, message_retention_period=None, maximum_message_size=None, delivery_delay=None, - receive_message_wait_time=None): + receive_message_wait_time=None, + policy=None): changed = False changed = set_queue_attribute(queue, 'VisibilityTimeout', default_visibility_timeout, @@ -149,6 +160,8 @@ def update_sqs_queue(queue, check_mode=check_mode) or changed changed = set_queue_attribute(queue, 'ReceiveMessageWaitTimeSeconds', receive_message_wait_time, check_mode=check_mode) or changed + changed = set_queue_attribute(queue, 'Policy', policy, + check_mode=check_mode) or changed return changed @@ -156,7 +169,17 @@ def set_queue_attribute(queue, attribute, value, check_mode=False): if not value: return False - existing_value = queue.get_attributes(attributes=attribute)[attribute] + try: + existing_value = queue.get_attributes(attributes=attribute)[attribute] + except: + existing_value = '' + + # convert dict attributes to JSON strings (sort keys for comparing) + if attribute is 'Policy': + value = json.dumps(value, sort_keys=True) + if existing_value: + existing_value = json.dumps(json.loads(existing_value), sort_keys=True) + if str(value) != existing_value: if not check_mode: queue.set_attribute(attribute, value) @@ -200,6 +223,7 @@ def main(): maximum_message_size=dict(type='int'), delivery_delay=dict(type='int'), receive_message_wait_time=dict(type='int'), + policy=dict(type='dict', required=False), )) module = AnsibleModule( From 2ecb1a37dcfb0e432c87a8610321c08659e771e1 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 15 Apr 2016 06:51:16 +0100 Subject: [PATCH 1433/2522] Documentation improvements --- windows/win_regedit.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/windows/win_regedit.py b/windows/win_regedit.py index f428d1fcbde..47dc0a6c56b 100644 --- a/windows/win_regedit.py +++ b/windows/win_regedit.py @@ -43,7 +43,7 @@ aliases: [] data: description: - - Registry Value Data. Binary data should be expressed as comma separated hex values. An easy way to generate this is to run regedit.exe and use the 'Export' option to save the registry values to a file. In the file binary values will look something like this: hex:be,ef,be,ef. The 'hex:' prefix is optional. + - Registry Value Data. Binary data should be expressed as comma separated hex values. An easy way to generate this is to run C(regedit.exe) and use the I(Export) option to save the registry values to a file. In the exported file binary values will look like C(hex:be,ef,be,ef). The C(hex:) prefix is optional. required: false default: null aliases: [] @@ -119,13 +119,12 @@ RETURN = ''' data_changed: description: whether this invocation changed the data in the registry value - returned: always + returned: success type: boolean - sample: false + sample: False data_type_changed: description: whether this invocation changed the datatype of the registry value - returned: always + returned: success type: boolean - sample: true + sample: True ''' - From 03af9ab49193181a6ab186af3a9fffe153e69aff Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Fri, 15 Apr 2016 15:45:37 +0200 Subject: [PATCH 1434/2522] Set api_key as no_log, since that's likely something that should be kept private (#2038) --- notification/pushbullet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notification/pushbullet.py b/notification/pushbullet.py index dfd89af577d..0d5ab7c4d48 100644 --- a/notification/pushbullet.py +++ b/notification/pushbullet.py @@ -108,7 +108,7 @@ def main(): module = AnsibleModule( argument_spec = dict( - api_key = dict(type='str', required=True), + api_key = dict(type='str', required=True, no_log=True), channel = dict(type='str', default=None), device = dict(type='str', default=None), push_type = dict(type='str', default="note", choices=['note', 'link']), From ff74fc00727290cab6ac8fca075a49b82f419420 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Fri, 15 Apr 2016 16:18:05 +0200 Subject: [PATCH 1435/2522] Remove the +x from crypttab and cronvar (#2039) While this change nothing, it is better to enforce consistency --- system/cronvar.py | 0 system/crypttab.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 system/cronvar.py mode change 100755 => 100644 system/crypttab.py diff --git a/system/cronvar.py b/system/cronvar.py old mode 100755 new mode 100644 diff --git a/system/crypttab.py b/system/crypttab.py old mode 100755 new mode 100644 From 8e7051ad9da4f9e98a18db70e79639a921845919 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Fri, 15 Apr 2016 16:27:47 +0200 Subject: [PATCH 1436/2522] Do not leak password by error for ovirt module (#1991) --- cloud/misc/ovirt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/misc/ovirt.py b/cloud/misc/ovirt.py index 760d4ffc62c..02596b441ea 100644 --- a/cloud/misc/ovirt.py +++ b/cloud/misc/ovirt.py @@ -333,7 +333,7 @@ def main(): user = dict(required=True), url = dict(required=True), instance_name = dict(required=True, aliases=['vmname']), - password = dict(required=True), + password = dict(required=True, no_log=True), image = dict(), resource_type = dict(choices=['new', 'template']), zone = dict(), From a61742e070f399af223e7faff585612f4f30ba99 Mon Sep 17 00:00:00 2001 From: Karim Boumedhel Date: Fri, 15 Apr 2016 20:23:37 +0200 Subject: [PATCH 1437/2522] Add cloudinit support to ovirt.py module --- cloud/misc/ovirt.py | 95 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 89 insertions(+), 6 deletions(-) diff --git a/cloud/misc/ovirt.py b/cloud/misc/ovirt.py index 02596b441ea..86c769bdb24 100644 --- a/cloud/misc/ovirt.py +++ b/cloud/misc/ovirt.py @@ -144,6 +144,48 @@ default: null required: false aliases: [] + instance_dns: + description: + - define the instance's Primary DNS server + required: false + aliases: [ dns ] + version_added: "2.1" + instance_domain: + description: + - define the instance's Domain + required: false + aliases: [ domain ] + version_added: "2.1" + instance_hostname: + description: + - define the instance's Hostname + required: false + aliases: [ hostname ] + version_added: "2.1" + instance_ip: + description: + - define the instance's IP + required: false + aliases: [ ip ] + version_added: "2.1" + instance_netmask: + description: + - define the instance's Netmask + required: false + aliases: [ netmask ] + version_added: "2.1" + instance_rootpw: + description: + - define the instance's Root password + required: false + aliases: [ rootpw ] + version_added: "2.1" + instance_key: + description: + - define the instance's Authorized key + required: false + aliases: [ key ] + version_added: "2.1" state: description: - create, terminate or remove instances @@ -205,6 +247,19 @@ password: secret url: https://ovirt.example.com +# starting an instance with cloud init information +ovirt: + instance_name: testansible + state: started + user: admin@internal + password: secret + url: https://ovirt.example.com + hostname: testansible + domain: ansible.local + ip: 192.168.1.100 + netmask: 255.255.255.0 + gateway: 192.168.1.1 + rootpw: bigsecret ''' @@ -273,9 +328,23 @@ def create_vm_template(conn, vmname, image, zone): # start instance -def vm_start(conn, vmname): +def vm_start(conn, vmname, hostname=None, ip=None, netmask=None, gateway=None, + domain=None, dns=None, rootpw=None, key=None): vm = conn.vms.get(name=vmname) - vm.start() + use_cloud_init = False + nics = None + if hostname or ip or netmask or gateway or domain or dns or rootpw or key: + use_cloud_init = True + if ip and netmask and gateway: + ipinfo = params.IP(address=ip, netmask=netmask, gateway=gateway) + nic = params.GuestNicConfiguration(name='eth0', boot_protocol='STATIC', ip=ipinfo, on_boot=True) + nics = params.Nics() + nics = params.GuestNicsConfiguration(nic_configuration=[nic]) + initialization=params.Initialization(regenerate_ssh_keys=True, host_name=hostname, domain=domain, user_name='root', + root_password=rootpw, nic_configurations=nics, dns_servers=dns, + authorized_ssh_keys=key) + action = params.Action(use_cloud_init=use_cloud_init, vm=params.VM(initialization=initialization)) + vm.start(action=action) # Stop instance def vm_stop(conn, vmname): @@ -302,7 +371,6 @@ def vm_remove(conn, vmname): # Get the VMs status def vm_status(conn, vmname): status = conn.vms.get(name=vmname).status.state - print "vm status is : %s" % status return status @@ -311,10 +379,8 @@ def get_vm(conn, vmname): vm = conn.vms.get(name=vmname) if vm == None: name = "empty" - print "vmname: %s" % name else: name = vm.get_name() - print "vmname: %s" % name return name # ------------------------------------------------------------------- # @@ -347,6 +413,14 @@ def main(): disk_int = dict(default='virtio', choices=['virtio', 'ide']), instance_os = dict(aliases=['vmos']), instance_cores = dict(default=1, aliases=['vmcores']), + instance_hostname = dict(aliases=['hostname']), + instance_ip = dict(aliases=['ip']), + instance_netmask = dict(aliases=['netmask']), + instance_gateway = dict(aliases=['gateway']), + instance_domain = dict(aliases=['domain']), + instance_dns = dict(aliases=['dns']), + instance_rootpw = dict(aliases=['rootpw']), + instance_key = dict(aliases=['key']), sdomain = dict(), region = dict(), ) @@ -375,6 +449,14 @@ def main(): vmcores = module.params['instance_cores'] # number of cores sdomain = module.params['sdomain'] # storage domain to store disk on region = module.params['region'] # oVirt Datacenter + hostname = module.params['instance_hostname'] + ip = module.params['instance_ip'] + netmask = module.params['instance_netmask'] + gateway = module.params['instance_gateway'] + domain = module.params['instance_domain'] + dns = module.params['instance_dns'] + rootpw = module.params['instance_rootpw'] + key = module.params['instance_key'] #initialize connection try: c = conn(url+"/api", user, password) @@ -405,7 +487,8 @@ def main(): if vm_status(c, vmname) == 'up': module.exit_json(changed=False, msg="VM %s is already running" % vmname) else: - vm_start(c, vmname) + #vm_start(c, vmname) + vm_start(c, vmname, hostname, ip, netmask, gateway, domain, dns, rootpw, key) module.exit_json(changed=True, msg="VM %s started" % vmname) if state == 'shutdown': From e3ca2c7aebe1835571db58b131dc983c7f3ca575 Mon Sep 17 00:00:00 2001 From: Pavol Ipoth Date: Fri, 15 Apr 2016 21:14:21 +0200 Subject: [PATCH 1438/2522] Adding new ansible module lvol_cache --- system/lvol_cache.py | 793 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 793 insertions(+) create mode 100644 system/lvol_cache.py diff --git a/system/lvol_cache.py b/system/lvol_cache.py new file mode 100644 index 00000000000..dd3f7ec2d18 --- /dev/null +++ b/system/lvol_cache.py @@ -0,0 +1,793 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Pavol Ipoth +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +author: + - "Pavol Ipoth " +module: lvol_cache +short_description: Configure Cache LVM logical volumes +description: + - This module creates, removes or resizes, converts cache logical volumes. +options: + vg: + description: + - The volume group this logical volume is part of. + required: true + lv: + description: + - The name of the logical volume. + required: true + size: + description: + - The size of the logical volume, by + default in megabytes or optionally with one of [bBsSkKmMgGtTpPeE] units; or + as a percentage of [VG|PVS|FREE]; +size is not implemented, as it is by + design not indempodent + Float values must begin with a digit. + state: + choices: [ "present", "absent" ] + default: present + description: + - Control if the logical volume exists. If C(present) the C(size) option + is required. + required: false + force: + choices: [ "yes", "no" ] + default: "no" + description: + - Shrink/extend/remove/conversion operations of volumes requires this switch. Ensures that + that filesystems get never corrupted/destroyed by mistake. + required: false + opts: + description: + - Free-form options to be passed to the lvcreate command + pvs: + description: + - Comma separated list of physical volumes /dev/sda,/dev/sdc + type: + choices: ["cache", "cache-pool", "normal"] + default: normal + description: + - Type of logical volume, cache is cached lvol, cache-pool is cache pool + supplying cached lvol, normal is lvol without any type. To create cache + lvol, cache pool must already exist + mode: + choices: ["writeback", "writethrough"] + default: writethrough + - This value is used when type is cache pool, specifies how data + are written to the lvol + pool: + description: + - This value is used when type is cache, specifies which cache pool will + be caching data for cache lvol. Must be in format vg/lv. Cache pool lvol + and cache lvol must be from same vg (this is restriction of lvm2). +notes: + - Filesystems on top of the volume are not resized. +''' + +EXAMPLES = ''' +# Create a logical volume of 512m. +- lvol: vg=firefly lv=test size=512m + +# Create a logical volume of 512g. +- lvol: vg=firefly lv=test size=512g + +# Create a logical volume the size of all remaining space in the volume group +- lvol: vg=firefly lv=test size=100%FREE + +# Create a logical volume with special options +- lvol: vg=firefly lv=test size=512g opts="-r 16" + +# Extend the logical volume to 1024m. +- lvol: vg=firefly lv=test size=1024m + +# Extend the logical volume to take all remaining space of the PVs +- lvol: vg=firefly lv=test size=100%PVS + +# Resize the logical volume to % of VG +- lvol: vg=firefly lv=test size=80%VG force=yes + +# Reduce the logical volume to 512m +- lvol: vg=firefly lv=test size=512m force=yes + +# Remove the logical volume. +- lvol: vg=firefly lv=test state=absent force=yes + +# Create cache pool logical volume of 512m. +- lvol: vg=firefly lv=testpool size=512m type=cache-pool + +# Create cache lvol testcached with cache pool firefly/testpool, +# cache pool must already exist +- lvol: vg=firefly lv=testcached size=1g type=cache pool=firefly/testpool + +# Extend cache pool to 2g, force must be yes +- lvol: vg=firefly lv=testpool size=2g type=cache-pool force=yes + +# Extend cache lvol to 5g, force must be yes +- lvol: vg=firefly lv=testcached size=5g type=cache pool=firefly/testpool force=yes + +# Convert cache lvol to normal lvol +- lvol: vg=firefly lv=testcached size=5g type=normal force=yes + +# Convert normal lvol to cache pool +- lvol: vg=firefly lv=testcached size=5g type=cache-pool force=yes + +# Convert cache pool lvol to cache lvol, you should notice that firefly/testpool +# still exists +- lvol: vg=firefly lv=testcached size=5g type=cache pool=firefly/testpool force=yes + +# Remove cache pool lvol, this also changes cache lvol to normal +- lvol: vg=firefly lv=testpool size=2g type=cache-pool state=absent force=yes +''' + +import re +# import module snippets +from ansible.module_utils.basic import * + +decimal_point = re.compile(r"(\d+\.?\d+)") + +class Vg(object): + + name = '' + size = None + unit = 'm' + free = None + vg_extent_size = None + module = None + + def __init__(self, module, vg_name, unit): + self.module = module + vg_info = self.get_vg_info(vg_name, unit) + + self.name = vg_info['name'] + self.size = vg_info['size'] + self.unit = unit + self.free = vg_info['free'] + self.vg_extent_size = vg_info['vg_extent_size'] + + def get_vg_info(self, vg_name, unit): + vgs_cmd = self.module.get_bin_path("vgs", required=True) + cmd = "%s --noheadings -o vg_name,size,free,vg_extent_size --units %s --separator ';' %s" % (vgs_cmd, unit, vg_name) + + rc, current_vgs, err = self.module.run_command(cmd) + + if rc != 0: + self.module.fail_json(msg="Volume group %s does not exist." % vg_name, rc=rc, err=err) + + parts = current_vgs.strip().split(';') + + return { + 'name': parts[0], + 'size': float(decimal_point.match(parts[1]).group(1)), + 'free': float(decimal_point.match(parts[2]).group(1)), + 'vg_extent_size': float(decimal_point.match(parts[3]).group(1)) + } + +class Lvol(object): + + name = '' + size = None + unit = 'm' + pool_lv = '' + vg = None + yes_opt = "" + cachemode = "" + module = None + + def __init__(self, module, vg_name, lv_name, unit): + self.vg = Vg(module, vg_name, unit) + self.module = module + + lv_info = self.get_lv_info(vg_name, lv_name, unit) + + self.name = lv_info['name'] + self.pool_lv = lv_info['pool_lv'] + self.size = lv_info['size'] + self.cachemode = lv_info['cachemode'] + self.unit = unit + + self.yes_opt = self.get_yes_opt(module) + + @classmethod + def exists(cls, module, vg_name, lv_name): + exists = False + lvs_cmd = module.get_bin_path("lvs", required=True) + + rc, current_lv, err = module.run_command( + "%s -a %s/%s" % (lvs_cmd, vg_name, lv_name) + ) + + if rc == 0: + exists = True + elif rc == 5 and "Failed to find logical volume" in err: + exists = False + else: + module.fail_json(msg="Unexpected error", rc=rc, err=err) + + return exists + + @classmethod + def is_cache_pool(cls, module, vg_name, lv_name): + is_cache_pool = False + lvs_cmd = module.get_bin_path("lvs", required=True) + + rc, current_lv, err = module.run_command( + "%s -a --noheadings --nosuffix -o lv_layout --separator ';' %s/%s" % (lvs_cmd, vg_name, lv_name) + ) + + if rc != 0: + module.fail_json(msg="Error", rc=rc, err=err) + + parts = current_lv.strip().split(';') + + if 'cache,pool' in parts[0]: + is_cache_pool = True + + return is_cache_pool + + @classmethod + def is_cache_lv(cls, module, vg_name, lv_name): + is_cache_lv = False + lvs_cmd = module.get_bin_path("lvs", required=True) + + rc, current_lv, err = module.run_command( + "%s -a --noheadings --nosuffix -o lv_layout --separator ';' %s/%s" % (lvs_cmd, vg_name, lv_name) + ) + + if rc != 0: + module.fail_json(msg="Error", rc=rc, err=err) + + parts = current_lv.strip().split(';') + + if 'cache' in parts[0] and 'pool' not in parts[0]: + is_cache_lv = True + + return is_cache_lv + + @classmethod + def mkversion(cls, major, minor, patch): + return (1000 * 1000 * int(major)) + (1000 * int(minor)) + int(patch) + + @classmethod + def get_lvm_version(cls, module): + ver_cmd = module.get_bin_path("lvm", required=True) + rc, out, err = module.run_command("%s version" % (ver_cmd)) + + if rc != 0: + return None + + m = re.search("LVM version:\s+(\d+)\.(\d+)\.(\d+).*(\d{4}-\d{2}-\d{2})", out) + + if not m: + return None + + return cls.mkversion(m.group(1), m.group(2), m.group(3)) + + def get_lv_info(self, vg_name, lv_name, unit): + lvs_cmd = self.module.get_bin_path("lvs", required=True) + + rc, current_lvs, err = self.module.run_command( + "%s -a --noheadings --nosuffix -o lv_name,size,pool_lv,cachemode --units %s --separator ';' %s/%s" % (lvs_cmd, unit, vg_name, lv_name) + ) + + if rc != 0: + self.module.fail_json(msg="Volume group or lvol does not exist." % vg_name, rc=rc, err=err) + + parts = current_lvs.strip().split(';') + + return { + 'name': parts[0].replace('[','').replace(']',''), + 'size': float(decimal_point.match(parts[1]).group(1)), + 'pool_lv': parts[2].replace('[','').replace(']',''), + 'cachemode': parts[3] + } + + @classmethod + def get_yes_opt(cls, module): + # Determine if the "--yes" option should be used + version_found = cls.get_lvm_version(module) + + if version_found == None: + module.fail_json(msg="Failed to get LVM version number") + + version_yesopt = cls.mkversion(2, 2, 99) # First LVM with the "--yes" option + + if version_found >= version_yesopt: + yesopt = "--yes" + else: + yesopt = "" + + return yesopt + + @classmethod + def get_size_opt(cls, requested_size): + if '%' in requested_size: + return 'l' + else: + return 'L' + + @classmethod + def calculate_size(cls, vg_free, vg_size, lv_size, size): + if '%' in size: + parts = size.split('%', 1) + size_percent = parts[0] + + size_whole = parts[1] + + if size_whole == 'VG' or size_whole == 'PVS': + size_requested = float(size_percent) * float(vg_size) / 100 + else: # size_whole == 'FREE': + size_requested = float(size_percent) * float(vg_free) / 100 + + if '+' in size: + size_requested += float(lv_size) + else: + size_requested = float(size[0:-1]) + + if '+' in size: + size_requested += float(lv_size) + + return size_requested + + @classmethod + def create_lv(cls, module, requested_size, unit, opts, vg_name, lv_name, pvs): + yes_opt = cls.get_yes_opt(module) + + lvcreate_cmd = module.get_bin_path("lvcreate", required=True) + + cmd = "%s %s -n %s -L %s%s %s %s %s" % (lvcreate_cmd, yes_opt, lv_name, requested_size, unit, opts, vg_name, pvs) + + rc, _, err = module.run_command(cmd) + + if rc != 0: + module.fail_json(msg="Creating logical volume '%s' failed" % lv_name, rc=rc, err=err) + + @classmethod + def delete_lv(cls, module, force, vg_name, lv_name): + if not force: + module.fail_json(msg="Sorry, no removal of logical volume %s without force=yes." % (lv_name)) + + lvremove_cmd = module.get_bin_path("lvremove", required=True) + rc, _, err = module.run_command("%s --force %s/%s" % (lvremove_cmd, vg_name, lv_name)) + + if rc !=0 : + module.fail_json(msg="Failed to remove logical volume %s/%s" % (vg_name, lv_name), rc=rc, err=err) + + def resize_common(self, tool, requested_size, pvs): + cmd = "%s -L %s%s %s/%s %s" % (tool, requested_size, self.unit, self.vg.name, self.name, pvs) + + rc, out, err = self.module.run_command(cmd) + + if rc != 0: + if "Reached maximum COW size" in out: + self.module.fail_json(msg="Unable to resize %s to %s%s" % (self.name, requested_size, self.unit), rc=rc, err=err, out=out) + elif "matches existing size" in err: + self.module.exit_json(changed=False, vg=self.vg.name, lv=self.name, size=self.size) + else: + self.module.fail_json(msg="Unable to resize %s to %s%s" % (self.name, requested_size, self.unit), rc=rc, err=err) + + def shrink_lv(self, force, requested_size, pvs): + if requested_size == 0: + self.module.fail_json(msg="Sorry, no shrinking of %s to 0 permitted." % (self.name)) + elif not force: + self.module.fail_json(msg="Sorry, no shrinking of %s without force=yes" % (self.name)) + else: + tool = self.module.get_bin_path("lvreduce", required=True) + tool = '%s %s' % (tool, '--force') + + self.resize_common(tool, requested_size, pvs) + + def extend_lv(self, requested_size, pvs): + if (self.vg.free > 0) and self.vg.free >= (requested_size - self.size): + tool = self.module.get_bin_path("lvextend", required=True) + else: + self.module.fail_json( + msg="Logical Volume %s could not be extended. Not enough free space left (%s%s required / %s%s available)" + % (self.name, (requested_size - self.size), self.unit, self.vg.free, self.unit) + ) + + self.resize_common(tool, requested_size, pvs) + + def resize_lv(self, force, requested_size, pvs): + if self.size < requested_size: + self.extend_lv(requested_size, pvs) + elif self.size > requested_size + self.vg.vg_extent_size: # more than an extent too large + self.shrink_lv(force, requested_size, pvs) + + self.__init__(self.module, self.vg.name, self.name, self.unit) + + def convert_to_cache(self, pool): + lvconvert_cmd = self.module.get_bin_path("lvconvert", required=True) + + rc, out, err = self.module.run_command( + "%s --type cache --cachepool %s %s/%s" % (lvconvert_cmd, pool, self.vg.name, self.name) + ) + + if rc != 0: + self.module.fail_json(msg="Converting logical volume to cache LV '%s' failed" % self.name, rc=rc, err=err) + + return CacheLvol(self.module, self.vg.name, self.name, self.unit) + + def convert_to_cache_pool(self, force, mode): + lvconvert_cmd = self.module.get_bin_path("lvconvert", required=True) + yes_opt = self.get_yes_opt(self.module) + + if not force: + self.module.fail_json(msg="Sorry, no conversion of %s to cache-pool without force=yes." % (self.name)) + + rc, out, err = self.module.run_command( + "%s %s --type cache-pool --cachemode %s %s/%s" % (lvconvert_cmd, yes_opt, mode, self.vg.name, self.name) + ) + + if rc != 0: + self.module.fail_json(msg="Converting logical volume to cache pool '%s' failed" % self.name, rc=rc, err=err) + + return CachePoolLvol(self.module, self.vg.name, self.name, self.unit) + +class CachePoolLvol(Lvol): + + @classmethod + def create_lv(cls, module, requested_size, unit, opts, vg_name, lv_name, pvs, mode): + opts += " --type cache-pool --cachemode %s" % (mode) + super(CachePoolLvol, cls).create_lv(module, requested_size, unit, opts, vg_name, lv_name, pvs) + + def get_cache_lvol_name(self): + cache_lvol_info = None + lvs_cmd = self.module.get_bin_path("lvs", required=True) + + rc, current_lvs, err = self.module.run_command( + "%s -a --noheadings --nosuffix -o lv_name,size,pool_lv --units %s --separator ';' %s" % (lvs_cmd, self.unit, self.vg.name)) + + if rc != 0: + if state == 'absent': + self.module.exit_json(changed=False, stdout="Volume group %s does not exist." % self.vg.name, stderr=False) + else: + self.module.fail_json(msg="Volume group %s does not exist." % self.vg.name, rc=rc, err=err) + + for line in current_lvs.splitlines(): + parts = line.strip().split(';') + pool = parts[2].replace('[','').replace(']','') + + if pool == self.name: + cache_lvol_info = parts[0].replace('[','').replace(']','') + + return cache_lvol_info + + def resize_lv(self, force, requested_size, pvs): + parent_cache_lvol_name = self.get_cache_lvol_name() + + self.delete_lv(self.module, force, self.vg.name, self.name) + self.create_lv(self.module, requested_size, self.unit, '', self.vg.name, self.name, pvs, self.cachemode) + + if parent_cache_lvol_name is not None: + parent_cache_lvol = Lvol(self.module, self.vg.name, parent_cache_lvol_name, self.unit) + parent_cache_lvol.convert_to_cache("%s/%s" % (self.vg.name, self.name)) + + self.__init__(self.module, self.vg.name, self.name, self.unit) + + def convert_to_cache(self, force, opts, pvs, pool): + self.delete_lv(self.module, force, self.vg.name, self.name) + CacheLvol.create_lv(self.module, self.size, self.unit, opts, self.vg.name, self.name, pvs, pool) + + return CacheLvol(self.module, self.vg.name, self.name, self.unit) + + def convert_to_normal(self, force, opts, pvs): + self.delete_lv(self.module, force, self.vg.name, self.name) + Lvol.create_lv(self.module, self.size, self.unit, opts, self.vg.name, self.name, pvs) + + return Lvol(self.module, self.vg.name, self.name, self.unit) + + def change_mode(self, force, opts, pvs, mode): + cache_lvol_name = self.get_cache_lvol_name() + + self.delete_lv(self.module, force, self.vg.name, self.name) + self.create_lv(self.module, self.size, self.unit, opts, self.vg.name, self.name, pvs, mode) + + if cache_lvol_name is not None: + lvol = Lvol(self.module, self.vg.name, cache_lvol_name, self.unit) + lvol.convert_to_cache("%s/%s" % (self.vg.name, self.name)) + + self.__init__(self.module, self.vg.name, self.name, self.unit) + +class CacheLvol(Lvol): + + @classmethod + def create_lv(cls, module, requested_size, unit, opts, vg_name, lv_name, pvs, pool): + opts += " --type cache --cachepool %s" % (pool) + super(CacheLvol, cls).create_lv(module, requested_size, unit, opts, vg_name, lv_name, pvs) + + def shrink_cache_lv(self, force, requested_size, pvs): + self.split_cache_lv() + self.shrink_lv(force, requested_size, pvs) + self.attach_cache_lv() + + def extend_cache_lv(self, requested_size, pvs): + self.split_cache_lv() + self.extend_lv(requested_size, pvs) + self.attach_cache_lv() + + def convert_to_normal(self): + lvconvert_cmd = self.module.get_bin_path("lvconvert", required=True) + lvsplit_cmd = "%s --splitcache %s/%s" % (lvconvert_cmd, self.vg.name, self.name) + + rc, _, err = self.module.run_command(lvsplit_cmd) + + if rc != 0: + self.module.fail_json(msg="Spliting cache from logical volume '%s' failed" % self.name, rc=rc, err=err) + + return Lvol(self.module, self.vg.name, self.name, self.unit) + + def split_cache_lv(self): + lvconvert_cmd = self.module.get_bin_path("lvconvert", required=True) + lvsplit_cmd = "%s --splitcache %s/%s" % (lvconvert_cmd, self.vg.name, self.name) + + rc, _, err = self.module.run_command(lvsplit_cmd) + + if rc != 0: + self.module.fail_json(msg="Spliting cache from logical volume '%s' failed" % self.name, rc=rc, err=err) + + def attach_cache_lv(self): + lvconvert_cmd = self.module.get_bin_path("lvconvert", required=True) + lvattach_cmd = "%s --type cache --cachepool %s %s/%s" % (lvconvert_cmd, self.pool_lv, self.vg.name, self.name) + + rc, _, err = self.module.run_command(lvattach_cmd) + + if rc != 0: + self.module.fail_json(msg="Attaching cache %s to logical volume %s failed" % (self.pool_lv, self.name), rc=rc, err=err) + + def resize_lv(self, force, requested_size, pvs): + if self.size < requested_size: + self.extend_cache_lv(requested_size, pvs) + elif self.size > requested_size + self.vg.vg_extent_size: # more than an extent too large + self.shrink_cache_lv(force, requested_size, pvs) + + def convert_to_cache_pool(self, force, mode): + lvol = self.convert_to_normal() + cachePoolLvol = lvol.convert_to_cache_pool(force, mode) + + return cachePoolLvol + + def change_pool(self, pool): + lvol = self.convert_to_normal() + lvol.convert_to_cache(pool) + + self.__init__(self.module, self.vg.name, self.name, self.unit) + +def validate_size(module, size): + if '%' in size: + size_parts = size.split('%', 1) + size_percent = int(size_parts[0]) + + if size_percent > 100: + module.fail_json(msg="Size percentage cannot be larger than 100%") + + size_whole = size_parts[1] + + if size_whole == 'ORIGIN': + module.fail_json(msg="Snapshot Volumes are not supported") + elif size_whole not in ['VG', 'PVS', 'FREE']: + module.fail_json(msg="Specify extents as a percentage of VG|PVS|FREE") + + if not '%' in size: + if size[-1].lower() in 'bskmgtpe': + size = size[0:-1] + else: + module.fail_json(msg="Invalid units for size: %s" % size) + + try: + float(size) + if not size[0].isdigit(): raise ValueError() + except ValueError: + module.fail_json(msg="Bad size specification of '%s'" % size) + +def main(): + module = AnsibleModule( + argument_spec=dict( + vg=dict(required=True), + lv=dict(required=True), + size=dict(type='str'), + opts=dict(type='str'), + state=dict(choices=["absent", "present"], default='present'), + force=dict(type='bool', default='no'), + pvs=dict(type='str'), + type=dict(choices=["cache", "cache-pool", "normal"], default="normal"), + mode=dict(choices=["writeback", "writethrough"], default='writethrough'), + pool=dict(type='str') + ), + supports_check_mode=True, + ) + + vg = module.params['vg'] + lv = module.params['lv'] + size = module.params['size'] + opts = module.params['opts'] + state = module.params['state'] + force = module.boolean(module.params['force']) + pvs = module.params['pvs'] + type = module.params['type'] + mode = module.params['mode'] + pool = module.params['pool'] + + if size: validate_size(module, size) + + if pvs is None: + pvs = "" + else: + pvs = pvs.replace(",", " ") + + if opts is None: + opts = "" + + # when no unit, megabytes by default + if '%' in size: + unit = 'm' + else: + unit = size[-1].lower() + + changed = False + msg = '' + + if type == 'cache' and not pool: + module.fail_json(msg="You must specify also pool when type is cache") + + if state == 'present' and not size: + if not Lvol.exists(module, vg, lv): + module.fail_json(msg="No size given.") + else: + module.exit_json(changed=False, vg=vg, lv=lv, size=vg) + + vg_obj = Vg(module, vg, unit) + + if not Lvol.exists(module, vg, lv): + if state == 'present': + if module.check_mode: + changed = True + else: + requested_size = Lvol.calculate_size(vg_obj.free, vg_obj.size, 0, size) + + if type != "normal": + if type == 'cache': + CacheLvol.create_lv(module, requested_size, unit, opts, vg, lv, pvs, pool) + elif type == 'cache-pool': + CachePoolLvol.create_lv(module, requested_size, unit, opts, vg, lv, pvs, mode) + else: + Lvol.create_lv(module, requested_size, unit, opts, vg, lv, pvs) + + changed = True + msg="Volume %s/%s created" % (vg, lv) + else: + if state == 'absent': + if module.check_mode: + module.exit_json(changed=True) + else: + if Lvol.is_cache_lv(module, vg, lv): + cacheLvol = CacheLvol(module, vg, lv, unit) + lvol = cacheLvol.convert_to_normal() + + changed = True + msg = "Volume %s converted to normal LV to %s." % (lv, size) + + Lvol.delete_lv(module, force, vg, lv) + + changed = True + msg += "Volume %s/%s deleted." % (vg, lv) + else: + if module.check_mode: + changed = True + else: + if type != "normal": + if type == 'cache': + if not Lvol.is_cache_lv(module, vg, lv): + if Lvol.is_cache_pool(module, vg, lv): + cachePoolLvol = CachePoolLvol(module, vg, lv, unit) + cacheLvol = cachePoolLvol.convert_to_cache(force, opts, pvs, pool) + else: + lvol = Lvol(module, vg, lv, unit) + cacheLvol = lvol.convert_to_cache(pool) + + changed = True + msg = "Volume %s converted to cache LV to %s." % (lv, size) + else: + cacheLvol = CacheLvol(module, vg, lv, unit) + + requested_size = cacheLvol.calculate_size(vg_obj.free, vg_obj.size, cacheLvol.size, size) + lvol_size_min = cacheLvol.size - vg_obj.vg_extent_size + lvol_size_max = cacheLvol.size + vg_obj.vg_extent_size + + current_pool = "%s/%s" % (cacheLvol.vg.name, cacheLvol.pool_lv) + + if pool != current_pool: + cacheLvol.change_pool(pool) + + changed = True + msg += "Changed pool of %s to %s." % (lv, pool) + + if not (lvol_size_min <= requested_size <= lvol_size_max): + cacheLvol.resize_lv(force, requested_size, pvs) + + changed = True + msg += "Volume %s resized to %s." % (lv, size) + + elif type == 'cache-pool': + if not Lvol.is_cache_pool(module, vg, lv): + if Lvol.is_cache_lv(module, vg, lv): + cacheLvol = CacheLvol(module, vg, lv, unit) + cachePoolLvol = cacheLvol.convert_to_cache_pool(force, mode) + else: + lvol = Lvol(module, vg, lv, unit) + cachePoolLvol = lvol.convert_to_cache_pool(force, mode) + + changed = True + msg = "Volume %s converted to cache pool to %s." % (lv, size) + else: + cachePoolLvol = CachePoolLvol(module, vg, lv, unit) + + requested_size = cachePoolLvol.calculate_size(vg_obj.free, vg_obj.size, cachePoolLvol.size, size) + lvol_size_min = cachePoolLvol.size - vg_obj.vg_extent_size + lvol_size_max = cachePoolLvol.size + vg_obj.vg_extent_size + + if mode != cachePoolLvol.cachemode: + cachePoolLvol.change_mode(force, opts, pvs, mode) + + changed = True + msg += "Changed pool mode of %s to %s." % (lv, mode) + + if not (lvol_size_min <= requested_size <= lvol_size_max): + cachePoolLvol.resize_lv(force, requested_size, pvs) + + changed = True + msg += "Volume %s resized to %s." % (lv, size) + else: + if Lvol.is_cache_lv(module, vg, lv): + cacheLvol = CacheLvol(module, vg, lv, unit) + lvol = cacheLvol.convert_to_normal() + + changed = True + msg = "Volume %s converted to normal LV to %s." % (lv, size) + elif Lvol.is_cache_pool(module, vg, lv): + cachePoolLvol = CachePoolLvol(module, vg, lv, unit) + cache_lvol_name = cachePoolLvol.get_cache_lvol_name() + + if cache_lvol_name is not None: + cacheLvol = CacheLvol(module, vg, cache_lvol_name, unit) + lvol = cacheLvol.convert_to_normal() + else: + lvol = cachePoolLvol.convert_to_normal(force, opts, pvs) + + changed = True + msg += "Volume %s converted to normal LV to %s." % (lv, size) + else: + lvol = Lvol(module, vg, lv, unit) + + requested_size = lvol.calculate_size(vg_obj.free, vg_obj.size, lvol.size, size) + lvol_size_min = lvol.size - vg_obj.vg_extent_size + lvol_size_max = lvol.size + vg_obj.vg_extent_size + + if not (lvol_size_min <= requested_size <= lvol_size_max): + lvol.resize_lv(force, requested_size, pvs) + + changed = True + msg += "Volume %s resized to %s." % (lv, size) + + module.exit_json(changed=changed, msg=msg) + +if __name__ == '__main__': + main() From 14057da87c0fc304b2e8fc971ad8d2985b79db89 Mon Sep 17 00:00:00 2001 From: Pavol Ipoth Date: Fri, 15 Apr 2016 21:22:03 +0200 Subject: [PATCH 1439/2522] Removing, unwanted pull --- system/lvol_cache.py | 793 ------------------------------------------- 1 file changed, 793 deletions(-) delete mode 100644 system/lvol_cache.py diff --git a/system/lvol_cache.py b/system/lvol_cache.py deleted file mode 100644 index dd3f7ec2d18..00000000000 --- a/system/lvol_cache.py +++ /dev/null @@ -1,793 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2016, Pavol Ipoth -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -DOCUMENTATION = ''' ---- -author: - - "Pavol Ipoth " -module: lvol_cache -short_description: Configure Cache LVM logical volumes -description: - - This module creates, removes or resizes, converts cache logical volumes. -options: - vg: - description: - - The volume group this logical volume is part of. - required: true - lv: - description: - - The name of the logical volume. - required: true - size: - description: - - The size of the logical volume, by - default in megabytes or optionally with one of [bBsSkKmMgGtTpPeE] units; or - as a percentage of [VG|PVS|FREE]; +size is not implemented, as it is by - design not indempodent - Float values must begin with a digit. - state: - choices: [ "present", "absent" ] - default: present - description: - - Control if the logical volume exists. If C(present) the C(size) option - is required. - required: false - force: - choices: [ "yes", "no" ] - default: "no" - description: - - Shrink/extend/remove/conversion operations of volumes requires this switch. Ensures that - that filesystems get never corrupted/destroyed by mistake. - required: false - opts: - description: - - Free-form options to be passed to the lvcreate command - pvs: - description: - - Comma separated list of physical volumes /dev/sda,/dev/sdc - type: - choices: ["cache", "cache-pool", "normal"] - default: normal - description: - - Type of logical volume, cache is cached lvol, cache-pool is cache pool - supplying cached lvol, normal is lvol without any type. To create cache - lvol, cache pool must already exist - mode: - choices: ["writeback", "writethrough"] - default: writethrough - - This value is used when type is cache pool, specifies how data - are written to the lvol - pool: - description: - - This value is used when type is cache, specifies which cache pool will - be caching data for cache lvol. Must be in format vg/lv. Cache pool lvol - and cache lvol must be from same vg (this is restriction of lvm2). -notes: - - Filesystems on top of the volume are not resized. -''' - -EXAMPLES = ''' -# Create a logical volume of 512m. -- lvol: vg=firefly lv=test size=512m - -# Create a logical volume of 512g. -- lvol: vg=firefly lv=test size=512g - -# Create a logical volume the size of all remaining space in the volume group -- lvol: vg=firefly lv=test size=100%FREE - -# Create a logical volume with special options -- lvol: vg=firefly lv=test size=512g opts="-r 16" - -# Extend the logical volume to 1024m. -- lvol: vg=firefly lv=test size=1024m - -# Extend the logical volume to take all remaining space of the PVs -- lvol: vg=firefly lv=test size=100%PVS - -# Resize the logical volume to % of VG -- lvol: vg=firefly lv=test size=80%VG force=yes - -# Reduce the logical volume to 512m -- lvol: vg=firefly lv=test size=512m force=yes - -# Remove the logical volume. -- lvol: vg=firefly lv=test state=absent force=yes - -# Create cache pool logical volume of 512m. -- lvol: vg=firefly lv=testpool size=512m type=cache-pool - -# Create cache lvol testcached with cache pool firefly/testpool, -# cache pool must already exist -- lvol: vg=firefly lv=testcached size=1g type=cache pool=firefly/testpool - -# Extend cache pool to 2g, force must be yes -- lvol: vg=firefly lv=testpool size=2g type=cache-pool force=yes - -# Extend cache lvol to 5g, force must be yes -- lvol: vg=firefly lv=testcached size=5g type=cache pool=firefly/testpool force=yes - -# Convert cache lvol to normal lvol -- lvol: vg=firefly lv=testcached size=5g type=normal force=yes - -# Convert normal lvol to cache pool -- lvol: vg=firefly lv=testcached size=5g type=cache-pool force=yes - -# Convert cache pool lvol to cache lvol, you should notice that firefly/testpool -# still exists -- lvol: vg=firefly lv=testcached size=5g type=cache pool=firefly/testpool force=yes - -# Remove cache pool lvol, this also changes cache lvol to normal -- lvol: vg=firefly lv=testpool size=2g type=cache-pool state=absent force=yes -''' - -import re -# import module snippets -from ansible.module_utils.basic import * - -decimal_point = re.compile(r"(\d+\.?\d+)") - -class Vg(object): - - name = '' - size = None - unit = 'm' - free = None - vg_extent_size = None - module = None - - def __init__(self, module, vg_name, unit): - self.module = module - vg_info = self.get_vg_info(vg_name, unit) - - self.name = vg_info['name'] - self.size = vg_info['size'] - self.unit = unit - self.free = vg_info['free'] - self.vg_extent_size = vg_info['vg_extent_size'] - - def get_vg_info(self, vg_name, unit): - vgs_cmd = self.module.get_bin_path("vgs", required=True) - cmd = "%s --noheadings -o vg_name,size,free,vg_extent_size --units %s --separator ';' %s" % (vgs_cmd, unit, vg_name) - - rc, current_vgs, err = self.module.run_command(cmd) - - if rc != 0: - self.module.fail_json(msg="Volume group %s does not exist." % vg_name, rc=rc, err=err) - - parts = current_vgs.strip().split(';') - - return { - 'name': parts[0], - 'size': float(decimal_point.match(parts[1]).group(1)), - 'free': float(decimal_point.match(parts[2]).group(1)), - 'vg_extent_size': float(decimal_point.match(parts[3]).group(1)) - } - -class Lvol(object): - - name = '' - size = None - unit = 'm' - pool_lv = '' - vg = None - yes_opt = "" - cachemode = "" - module = None - - def __init__(self, module, vg_name, lv_name, unit): - self.vg = Vg(module, vg_name, unit) - self.module = module - - lv_info = self.get_lv_info(vg_name, lv_name, unit) - - self.name = lv_info['name'] - self.pool_lv = lv_info['pool_lv'] - self.size = lv_info['size'] - self.cachemode = lv_info['cachemode'] - self.unit = unit - - self.yes_opt = self.get_yes_opt(module) - - @classmethod - def exists(cls, module, vg_name, lv_name): - exists = False - lvs_cmd = module.get_bin_path("lvs", required=True) - - rc, current_lv, err = module.run_command( - "%s -a %s/%s" % (lvs_cmd, vg_name, lv_name) - ) - - if rc == 0: - exists = True - elif rc == 5 and "Failed to find logical volume" in err: - exists = False - else: - module.fail_json(msg="Unexpected error", rc=rc, err=err) - - return exists - - @classmethod - def is_cache_pool(cls, module, vg_name, lv_name): - is_cache_pool = False - lvs_cmd = module.get_bin_path("lvs", required=True) - - rc, current_lv, err = module.run_command( - "%s -a --noheadings --nosuffix -o lv_layout --separator ';' %s/%s" % (lvs_cmd, vg_name, lv_name) - ) - - if rc != 0: - module.fail_json(msg="Error", rc=rc, err=err) - - parts = current_lv.strip().split(';') - - if 'cache,pool' in parts[0]: - is_cache_pool = True - - return is_cache_pool - - @classmethod - def is_cache_lv(cls, module, vg_name, lv_name): - is_cache_lv = False - lvs_cmd = module.get_bin_path("lvs", required=True) - - rc, current_lv, err = module.run_command( - "%s -a --noheadings --nosuffix -o lv_layout --separator ';' %s/%s" % (lvs_cmd, vg_name, lv_name) - ) - - if rc != 0: - module.fail_json(msg="Error", rc=rc, err=err) - - parts = current_lv.strip().split(';') - - if 'cache' in parts[0] and 'pool' not in parts[0]: - is_cache_lv = True - - return is_cache_lv - - @classmethod - def mkversion(cls, major, minor, patch): - return (1000 * 1000 * int(major)) + (1000 * int(minor)) + int(patch) - - @classmethod - def get_lvm_version(cls, module): - ver_cmd = module.get_bin_path("lvm", required=True) - rc, out, err = module.run_command("%s version" % (ver_cmd)) - - if rc != 0: - return None - - m = re.search("LVM version:\s+(\d+)\.(\d+)\.(\d+).*(\d{4}-\d{2}-\d{2})", out) - - if not m: - return None - - return cls.mkversion(m.group(1), m.group(2), m.group(3)) - - def get_lv_info(self, vg_name, lv_name, unit): - lvs_cmd = self.module.get_bin_path("lvs", required=True) - - rc, current_lvs, err = self.module.run_command( - "%s -a --noheadings --nosuffix -o lv_name,size,pool_lv,cachemode --units %s --separator ';' %s/%s" % (lvs_cmd, unit, vg_name, lv_name) - ) - - if rc != 0: - self.module.fail_json(msg="Volume group or lvol does not exist." % vg_name, rc=rc, err=err) - - parts = current_lvs.strip().split(';') - - return { - 'name': parts[0].replace('[','').replace(']',''), - 'size': float(decimal_point.match(parts[1]).group(1)), - 'pool_lv': parts[2].replace('[','').replace(']',''), - 'cachemode': parts[3] - } - - @classmethod - def get_yes_opt(cls, module): - # Determine if the "--yes" option should be used - version_found = cls.get_lvm_version(module) - - if version_found == None: - module.fail_json(msg="Failed to get LVM version number") - - version_yesopt = cls.mkversion(2, 2, 99) # First LVM with the "--yes" option - - if version_found >= version_yesopt: - yesopt = "--yes" - else: - yesopt = "" - - return yesopt - - @classmethod - def get_size_opt(cls, requested_size): - if '%' in requested_size: - return 'l' - else: - return 'L' - - @classmethod - def calculate_size(cls, vg_free, vg_size, lv_size, size): - if '%' in size: - parts = size.split('%', 1) - size_percent = parts[0] - - size_whole = parts[1] - - if size_whole == 'VG' or size_whole == 'PVS': - size_requested = float(size_percent) * float(vg_size) / 100 - else: # size_whole == 'FREE': - size_requested = float(size_percent) * float(vg_free) / 100 - - if '+' in size: - size_requested += float(lv_size) - else: - size_requested = float(size[0:-1]) - - if '+' in size: - size_requested += float(lv_size) - - return size_requested - - @classmethod - def create_lv(cls, module, requested_size, unit, opts, vg_name, lv_name, pvs): - yes_opt = cls.get_yes_opt(module) - - lvcreate_cmd = module.get_bin_path("lvcreate", required=True) - - cmd = "%s %s -n %s -L %s%s %s %s %s" % (lvcreate_cmd, yes_opt, lv_name, requested_size, unit, opts, vg_name, pvs) - - rc, _, err = module.run_command(cmd) - - if rc != 0: - module.fail_json(msg="Creating logical volume '%s' failed" % lv_name, rc=rc, err=err) - - @classmethod - def delete_lv(cls, module, force, vg_name, lv_name): - if not force: - module.fail_json(msg="Sorry, no removal of logical volume %s without force=yes." % (lv_name)) - - lvremove_cmd = module.get_bin_path("lvremove", required=True) - rc, _, err = module.run_command("%s --force %s/%s" % (lvremove_cmd, vg_name, lv_name)) - - if rc !=0 : - module.fail_json(msg="Failed to remove logical volume %s/%s" % (vg_name, lv_name), rc=rc, err=err) - - def resize_common(self, tool, requested_size, pvs): - cmd = "%s -L %s%s %s/%s %s" % (tool, requested_size, self.unit, self.vg.name, self.name, pvs) - - rc, out, err = self.module.run_command(cmd) - - if rc != 0: - if "Reached maximum COW size" in out: - self.module.fail_json(msg="Unable to resize %s to %s%s" % (self.name, requested_size, self.unit), rc=rc, err=err, out=out) - elif "matches existing size" in err: - self.module.exit_json(changed=False, vg=self.vg.name, lv=self.name, size=self.size) - else: - self.module.fail_json(msg="Unable to resize %s to %s%s" % (self.name, requested_size, self.unit), rc=rc, err=err) - - def shrink_lv(self, force, requested_size, pvs): - if requested_size == 0: - self.module.fail_json(msg="Sorry, no shrinking of %s to 0 permitted." % (self.name)) - elif not force: - self.module.fail_json(msg="Sorry, no shrinking of %s without force=yes" % (self.name)) - else: - tool = self.module.get_bin_path("lvreduce", required=True) - tool = '%s %s' % (tool, '--force') - - self.resize_common(tool, requested_size, pvs) - - def extend_lv(self, requested_size, pvs): - if (self.vg.free > 0) and self.vg.free >= (requested_size - self.size): - tool = self.module.get_bin_path("lvextend", required=True) - else: - self.module.fail_json( - msg="Logical Volume %s could not be extended. Not enough free space left (%s%s required / %s%s available)" - % (self.name, (requested_size - self.size), self.unit, self.vg.free, self.unit) - ) - - self.resize_common(tool, requested_size, pvs) - - def resize_lv(self, force, requested_size, pvs): - if self.size < requested_size: - self.extend_lv(requested_size, pvs) - elif self.size > requested_size + self.vg.vg_extent_size: # more than an extent too large - self.shrink_lv(force, requested_size, pvs) - - self.__init__(self.module, self.vg.name, self.name, self.unit) - - def convert_to_cache(self, pool): - lvconvert_cmd = self.module.get_bin_path("lvconvert", required=True) - - rc, out, err = self.module.run_command( - "%s --type cache --cachepool %s %s/%s" % (lvconvert_cmd, pool, self.vg.name, self.name) - ) - - if rc != 0: - self.module.fail_json(msg="Converting logical volume to cache LV '%s' failed" % self.name, rc=rc, err=err) - - return CacheLvol(self.module, self.vg.name, self.name, self.unit) - - def convert_to_cache_pool(self, force, mode): - lvconvert_cmd = self.module.get_bin_path("lvconvert", required=True) - yes_opt = self.get_yes_opt(self.module) - - if not force: - self.module.fail_json(msg="Sorry, no conversion of %s to cache-pool without force=yes." % (self.name)) - - rc, out, err = self.module.run_command( - "%s %s --type cache-pool --cachemode %s %s/%s" % (lvconvert_cmd, yes_opt, mode, self.vg.name, self.name) - ) - - if rc != 0: - self.module.fail_json(msg="Converting logical volume to cache pool '%s' failed" % self.name, rc=rc, err=err) - - return CachePoolLvol(self.module, self.vg.name, self.name, self.unit) - -class CachePoolLvol(Lvol): - - @classmethod - def create_lv(cls, module, requested_size, unit, opts, vg_name, lv_name, pvs, mode): - opts += " --type cache-pool --cachemode %s" % (mode) - super(CachePoolLvol, cls).create_lv(module, requested_size, unit, opts, vg_name, lv_name, pvs) - - def get_cache_lvol_name(self): - cache_lvol_info = None - lvs_cmd = self.module.get_bin_path("lvs", required=True) - - rc, current_lvs, err = self.module.run_command( - "%s -a --noheadings --nosuffix -o lv_name,size,pool_lv --units %s --separator ';' %s" % (lvs_cmd, self.unit, self.vg.name)) - - if rc != 0: - if state == 'absent': - self.module.exit_json(changed=False, stdout="Volume group %s does not exist." % self.vg.name, stderr=False) - else: - self.module.fail_json(msg="Volume group %s does not exist." % self.vg.name, rc=rc, err=err) - - for line in current_lvs.splitlines(): - parts = line.strip().split(';') - pool = parts[2].replace('[','').replace(']','') - - if pool == self.name: - cache_lvol_info = parts[0].replace('[','').replace(']','') - - return cache_lvol_info - - def resize_lv(self, force, requested_size, pvs): - parent_cache_lvol_name = self.get_cache_lvol_name() - - self.delete_lv(self.module, force, self.vg.name, self.name) - self.create_lv(self.module, requested_size, self.unit, '', self.vg.name, self.name, pvs, self.cachemode) - - if parent_cache_lvol_name is not None: - parent_cache_lvol = Lvol(self.module, self.vg.name, parent_cache_lvol_name, self.unit) - parent_cache_lvol.convert_to_cache("%s/%s" % (self.vg.name, self.name)) - - self.__init__(self.module, self.vg.name, self.name, self.unit) - - def convert_to_cache(self, force, opts, pvs, pool): - self.delete_lv(self.module, force, self.vg.name, self.name) - CacheLvol.create_lv(self.module, self.size, self.unit, opts, self.vg.name, self.name, pvs, pool) - - return CacheLvol(self.module, self.vg.name, self.name, self.unit) - - def convert_to_normal(self, force, opts, pvs): - self.delete_lv(self.module, force, self.vg.name, self.name) - Lvol.create_lv(self.module, self.size, self.unit, opts, self.vg.name, self.name, pvs) - - return Lvol(self.module, self.vg.name, self.name, self.unit) - - def change_mode(self, force, opts, pvs, mode): - cache_lvol_name = self.get_cache_lvol_name() - - self.delete_lv(self.module, force, self.vg.name, self.name) - self.create_lv(self.module, self.size, self.unit, opts, self.vg.name, self.name, pvs, mode) - - if cache_lvol_name is not None: - lvol = Lvol(self.module, self.vg.name, cache_lvol_name, self.unit) - lvol.convert_to_cache("%s/%s" % (self.vg.name, self.name)) - - self.__init__(self.module, self.vg.name, self.name, self.unit) - -class CacheLvol(Lvol): - - @classmethod - def create_lv(cls, module, requested_size, unit, opts, vg_name, lv_name, pvs, pool): - opts += " --type cache --cachepool %s" % (pool) - super(CacheLvol, cls).create_lv(module, requested_size, unit, opts, vg_name, lv_name, pvs) - - def shrink_cache_lv(self, force, requested_size, pvs): - self.split_cache_lv() - self.shrink_lv(force, requested_size, pvs) - self.attach_cache_lv() - - def extend_cache_lv(self, requested_size, pvs): - self.split_cache_lv() - self.extend_lv(requested_size, pvs) - self.attach_cache_lv() - - def convert_to_normal(self): - lvconvert_cmd = self.module.get_bin_path("lvconvert", required=True) - lvsplit_cmd = "%s --splitcache %s/%s" % (lvconvert_cmd, self.vg.name, self.name) - - rc, _, err = self.module.run_command(lvsplit_cmd) - - if rc != 0: - self.module.fail_json(msg="Spliting cache from logical volume '%s' failed" % self.name, rc=rc, err=err) - - return Lvol(self.module, self.vg.name, self.name, self.unit) - - def split_cache_lv(self): - lvconvert_cmd = self.module.get_bin_path("lvconvert", required=True) - lvsplit_cmd = "%s --splitcache %s/%s" % (lvconvert_cmd, self.vg.name, self.name) - - rc, _, err = self.module.run_command(lvsplit_cmd) - - if rc != 0: - self.module.fail_json(msg="Spliting cache from logical volume '%s' failed" % self.name, rc=rc, err=err) - - def attach_cache_lv(self): - lvconvert_cmd = self.module.get_bin_path("lvconvert", required=True) - lvattach_cmd = "%s --type cache --cachepool %s %s/%s" % (lvconvert_cmd, self.pool_lv, self.vg.name, self.name) - - rc, _, err = self.module.run_command(lvattach_cmd) - - if rc != 0: - self.module.fail_json(msg="Attaching cache %s to logical volume %s failed" % (self.pool_lv, self.name), rc=rc, err=err) - - def resize_lv(self, force, requested_size, pvs): - if self.size < requested_size: - self.extend_cache_lv(requested_size, pvs) - elif self.size > requested_size + self.vg.vg_extent_size: # more than an extent too large - self.shrink_cache_lv(force, requested_size, pvs) - - def convert_to_cache_pool(self, force, mode): - lvol = self.convert_to_normal() - cachePoolLvol = lvol.convert_to_cache_pool(force, mode) - - return cachePoolLvol - - def change_pool(self, pool): - lvol = self.convert_to_normal() - lvol.convert_to_cache(pool) - - self.__init__(self.module, self.vg.name, self.name, self.unit) - -def validate_size(module, size): - if '%' in size: - size_parts = size.split('%', 1) - size_percent = int(size_parts[0]) - - if size_percent > 100: - module.fail_json(msg="Size percentage cannot be larger than 100%") - - size_whole = size_parts[1] - - if size_whole == 'ORIGIN': - module.fail_json(msg="Snapshot Volumes are not supported") - elif size_whole not in ['VG', 'PVS', 'FREE']: - module.fail_json(msg="Specify extents as a percentage of VG|PVS|FREE") - - if not '%' in size: - if size[-1].lower() in 'bskmgtpe': - size = size[0:-1] - else: - module.fail_json(msg="Invalid units for size: %s" % size) - - try: - float(size) - if not size[0].isdigit(): raise ValueError() - except ValueError: - module.fail_json(msg="Bad size specification of '%s'" % size) - -def main(): - module = AnsibleModule( - argument_spec=dict( - vg=dict(required=True), - lv=dict(required=True), - size=dict(type='str'), - opts=dict(type='str'), - state=dict(choices=["absent", "present"], default='present'), - force=dict(type='bool', default='no'), - pvs=dict(type='str'), - type=dict(choices=["cache", "cache-pool", "normal"], default="normal"), - mode=dict(choices=["writeback", "writethrough"], default='writethrough'), - pool=dict(type='str') - ), - supports_check_mode=True, - ) - - vg = module.params['vg'] - lv = module.params['lv'] - size = module.params['size'] - opts = module.params['opts'] - state = module.params['state'] - force = module.boolean(module.params['force']) - pvs = module.params['pvs'] - type = module.params['type'] - mode = module.params['mode'] - pool = module.params['pool'] - - if size: validate_size(module, size) - - if pvs is None: - pvs = "" - else: - pvs = pvs.replace(",", " ") - - if opts is None: - opts = "" - - # when no unit, megabytes by default - if '%' in size: - unit = 'm' - else: - unit = size[-1].lower() - - changed = False - msg = '' - - if type == 'cache' and not pool: - module.fail_json(msg="You must specify also pool when type is cache") - - if state == 'present' and not size: - if not Lvol.exists(module, vg, lv): - module.fail_json(msg="No size given.") - else: - module.exit_json(changed=False, vg=vg, lv=lv, size=vg) - - vg_obj = Vg(module, vg, unit) - - if not Lvol.exists(module, vg, lv): - if state == 'present': - if module.check_mode: - changed = True - else: - requested_size = Lvol.calculate_size(vg_obj.free, vg_obj.size, 0, size) - - if type != "normal": - if type == 'cache': - CacheLvol.create_lv(module, requested_size, unit, opts, vg, lv, pvs, pool) - elif type == 'cache-pool': - CachePoolLvol.create_lv(module, requested_size, unit, opts, vg, lv, pvs, mode) - else: - Lvol.create_lv(module, requested_size, unit, opts, vg, lv, pvs) - - changed = True - msg="Volume %s/%s created" % (vg, lv) - else: - if state == 'absent': - if module.check_mode: - module.exit_json(changed=True) - else: - if Lvol.is_cache_lv(module, vg, lv): - cacheLvol = CacheLvol(module, vg, lv, unit) - lvol = cacheLvol.convert_to_normal() - - changed = True - msg = "Volume %s converted to normal LV to %s." % (lv, size) - - Lvol.delete_lv(module, force, vg, lv) - - changed = True - msg += "Volume %s/%s deleted." % (vg, lv) - else: - if module.check_mode: - changed = True - else: - if type != "normal": - if type == 'cache': - if not Lvol.is_cache_lv(module, vg, lv): - if Lvol.is_cache_pool(module, vg, lv): - cachePoolLvol = CachePoolLvol(module, vg, lv, unit) - cacheLvol = cachePoolLvol.convert_to_cache(force, opts, pvs, pool) - else: - lvol = Lvol(module, vg, lv, unit) - cacheLvol = lvol.convert_to_cache(pool) - - changed = True - msg = "Volume %s converted to cache LV to %s." % (lv, size) - else: - cacheLvol = CacheLvol(module, vg, lv, unit) - - requested_size = cacheLvol.calculate_size(vg_obj.free, vg_obj.size, cacheLvol.size, size) - lvol_size_min = cacheLvol.size - vg_obj.vg_extent_size - lvol_size_max = cacheLvol.size + vg_obj.vg_extent_size - - current_pool = "%s/%s" % (cacheLvol.vg.name, cacheLvol.pool_lv) - - if pool != current_pool: - cacheLvol.change_pool(pool) - - changed = True - msg += "Changed pool of %s to %s." % (lv, pool) - - if not (lvol_size_min <= requested_size <= lvol_size_max): - cacheLvol.resize_lv(force, requested_size, pvs) - - changed = True - msg += "Volume %s resized to %s." % (lv, size) - - elif type == 'cache-pool': - if not Lvol.is_cache_pool(module, vg, lv): - if Lvol.is_cache_lv(module, vg, lv): - cacheLvol = CacheLvol(module, vg, lv, unit) - cachePoolLvol = cacheLvol.convert_to_cache_pool(force, mode) - else: - lvol = Lvol(module, vg, lv, unit) - cachePoolLvol = lvol.convert_to_cache_pool(force, mode) - - changed = True - msg = "Volume %s converted to cache pool to %s." % (lv, size) - else: - cachePoolLvol = CachePoolLvol(module, vg, lv, unit) - - requested_size = cachePoolLvol.calculate_size(vg_obj.free, vg_obj.size, cachePoolLvol.size, size) - lvol_size_min = cachePoolLvol.size - vg_obj.vg_extent_size - lvol_size_max = cachePoolLvol.size + vg_obj.vg_extent_size - - if mode != cachePoolLvol.cachemode: - cachePoolLvol.change_mode(force, opts, pvs, mode) - - changed = True - msg += "Changed pool mode of %s to %s." % (lv, mode) - - if not (lvol_size_min <= requested_size <= lvol_size_max): - cachePoolLvol.resize_lv(force, requested_size, pvs) - - changed = True - msg += "Volume %s resized to %s." % (lv, size) - else: - if Lvol.is_cache_lv(module, vg, lv): - cacheLvol = CacheLvol(module, vg, lv, unit) - lvol = cacheLvol.convert_to_normal() - - changed = True - msg = "Volume %s converted to normal LV to %s." % (lv, size) - elif Lvol.is_cache_pool(module, vg, lv): - cachePoolLvol = CachePoolLvol(module, vg, lv, unit) - cache_lvol_name = cachePoolLvol.get_cache_lvol_name() - - if cache_lvol_name is not None: - cacheLvol = CacheLvol(module, vg, cache_lvol_name, unit) - lvol = cacheLvol.convert_to_normal() - else: - lvol = cachePoolLvol.convert_to_normal(force, opts, pvs) - - changed = True - msg += "Volume %s converted to normal LV to %s." % (lv, size) - else: - lvol = Lvol(module, vg, lv, unit) - - requested_size = lvol.calculate_size(vg_obj.free, vg_obj.size, lvol.size, size) - lvol_size_min = lvol.size - vg_obj.vg_extent_size - lvol_size_max = lvol.size + vg_obj.vg_extent_size - - if not (lvol_size_min <= requested_size <= lvol_size_max): - lvol.resize_lv(force, requested_size, pvs) - - changed = True - msg += "Volume %s resized to %s." % (lv, size) - - module.exit_json(changed=changed, msg=msg) - -if __name__ == '__main__': - main() From 359b2abebc1d2e038cb2dbbba3868a58ef229178 Mon Sep 17 00:00:00 2001 From: Jordan Cohen Date: Sat, 16 Apr 2016 08:10:49 -0400 Subject: [PATCH 1440/2522] idempotency fix (#2024) --- monitoring/datadog_monitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py index 8dbbe646638..c5aec1b561e 100644 --- a/monitoring/datadog_monitor.py +++ b/monitoring/datadog_monitor.py @@ -208,7 +208,7 @@ def _update_monitor(module, monitor, options): options=options) if 'errors' in msg: module.fail_json(msg=str(msg['errors'])) - elif _equal_dicts(msg, monitor, ['creator', 'overall_state']): + elif _equal_dicts(msg, monitor, ['creator', 'overall_state', 'modified']): module.exit_json(changed=False, msg=msg) else: module.exit_json(changed=True, msg=msg) From aa29a4fd9ce13600cc72c67583c5df2c1a297cf2 Mon Sep 17 00:00:00 2001 From: codehopper-uk Date: Sat, 16 Apr 2016 13:15:00 +0100 Subject: [PATCH 1441/2522] Basic ability to set masquerade options from ansible, according to current code design/layout (mostly) (#2017) * Support for masquerade settings Ability to enable and disable masquerade settings from ansible via: - firewalld: mapping=masquerade state=disabled permanent=true zone=dmz Placeholder added (mapping) to support masquerade and port_forward choices initially - port_forward not implemented yet. * Permanent and Immediate zone handling differentiated * Corrected naming abstraction for masquerading functionality Removed mapping tag with port_forward choices - not applicable! * Added version info for new masquerade option Pull Request #2017 failing due to missing version info --- system/firewalld.py | 92 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/system/firewalld.py b/system/firewalld.py index 2638ff759e8..c65e554edb8 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -80,6 +80,12 @@ - "The amount of time the rule should be in effect for when non-permanent." required: false default: 0 + masquerade: + description: + - 'The masquerade setting you would like to enable/disable to/from zones within firewalld' + required: false + default: null + version_added: "2.1" notes: - Not tested on any Debian based system. - Requires the python2 bindings of firewalld, who may not be installed by default if the distribution switched to python 3 @@ -95,6 +101,7 @@ - firewalld: rich_rule='rule service name="ftp" audit limit value="1/m" accept' permanent=true state=enabled - firewalld: source='192.168.1.0/24' zone=internal state=enabled - firewalld: zone=trusted interface=eth2 permanent=true state=enabled +- firewalld: masquerade=yes state=enabled permanent=true zone=dmz ''' import os @@ -114,6 +121,36 @@ except ImportError: HAS_FIREWALLD = False + +##################### +# masquerade handling +# +def get_masquerade_enabled(zone): + if fw.queryMasquerade(zone) == True: + return True + else: + return False + +def get_masquerade_enabled_permanent(zone): + fw_zone = fw.config().getZoneByName(zone) + fw_settings = fw_zone.getSettings() + if fw_settings.getMasquerade() == True: + return True + else: + return False + +def set_masquerade_enabled(zone): + fw.addMasquerade(zone) + +def set_masquerade_disabled(zone): + fw.removeMasquerade(zone) + +def set_masquerade_permanent(zone, masquerade): + fw_zone = fw.config().getZoneByName(zone) + fw_settings = fw_zone.getSettings() + fw_settings.setMasquerade(masquerade) + fw_zone.update(fw_settings) + ################ # port handling # @@ -287,6 +324,7 @@ def main(): state=dict(choices=['enabled', 'disabled'], required=True), timeout=dict(type='int',required=False,default=0), interface=dict(required=False,default=None), + masquerade=dict(required=False,default=None), ), supports_check_mode=True ) @@ -327,6 +365,15 @@ def main(): immediate = module.params['immediate'] timeout = module.params['timeout'] interface = module.params['interface'] + masquerade = module.params['masquerade'] + + ## Check for firewalld running + try: + if fw.connected == False: + module.fail_json(msg='firewalld service must be running') + except AttributeError: + module.fail_json(msg="firewalld connection can't be established,\ + version likely too old. Requires firewalld >= 2.0.11") modification_count = 0 if service != None: @@ -337,6 +384,8 @@ def main(): modification_count += 1 if interface != None: modification_count += 1 + if masquerade != None: + modification_count += 1 if modification_count > 1: module.fail_json(msg='can only operate on port, service, rich_rule or interface at once') @@ -504,6 +553,49 @@ def main(): changed=True msgs.append("Removed %s from zone %s" % (interface, zone)) + if masquerade != None: + + if permanent: + is_enabled = get_masquerade_enabled_permanent(zone) + msgs.append('Permanent operation') + + if desired_state == "enabled": + if is_enabled == False: + if module.check_mode: + module.exit_json(changed=True) + + set_masquerade_permanent(zone, True) + changed=True + msgs.append("Added masquerade to zone %s" % (zone)) + elif desired_state == "disabled": + if is_enabled == True: + if module.check_mode: + module.exit_json(changed=True) + + set_masquerade_permanent(zone, False) + changed=True + msgs.append("Removed masquerade from zone %s" % (zone)) + if immediate or not permanent: + is_enabled = get_masquerade_enabled(zone) + msgs.append('Non-permanent operation') + + if desired_state == "enabled": + if is_enabled == False: + if module.check_mode: + module.exit_json(changed=True) + + set_masquerade_enabled(zone) + changed=True + msgs.append("Added masquerade to zone %s" % (zone)) + elif desired_state == "disabled": + if is_enabled == True: + if module.check_mode: + module.exit_json(changed=True) + + set_masquerade_disabled(zone) + changed=True + msgs.append("Removed masquerade from zone %s" % (zone)) + module.exit_json(changed=changed, msg=', '.join(msgs)) From 05068630ca48f01716e89947c89fb0e345f9ea10 Mon Sep 17 00:00:00 2001 From: Jay Jahns Date: Sun, 17 Apr 2016 03:42:31 -0500 Subject: [PATCH 1442/2522] Add Maintenance Mode support for VMware (#1754) * add vmware maintenance mode support * changed version number in documentation * updated version_added to 2.0 since CI is failing * changed version to 2.0 due to CI - error asking for 2.1 * added RETURN * updated formatting of return values and added some to clarify actions taken --- cloud/vmware/vmware_maintenancemode.py | 212 +++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 cloud/vmware/vmware_maintenancemode.py diff --git a/cloud/vmware/vmware_maintenancemode.py b/cloud/vmware/vmware_maintenancemode.py new file mode 100644 index 00000000000..0af69e38057 --- /dev/null +++ b/cloud/vmware/vmware_maintenancemode.py @@ -0,0 +1,212 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, VMware, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + + +DOCUMENTATION = ''' +--- +module: vmware_maintenancemode +short_description: Place a host into maintenance mode +description: + - Place an ESXI host into maintenance mode + - Support for VSAN compliant maintenance mode when selected +author: "Jay Jahns " +version_added: "2.1" +notes: + - Tested on vSphere 5.5 and 6.0 +requirements: + - "python >= 2.6" + - PyVmomi +options: + esxi_hostname: + description: + - Name of the host as defined in vCenter + required: True + vsan_mode: + description: + - Specify which VSAN compliant mode to enter + choices: + - 'ensureObjectAccessibility' + - 'evacuateAllData' + - 'noAction' + required: False + evacuate: + description: + - If True, evacuate all powered off VMs + choices: + - True + - False + default: False + required: False + timeout: + description: + - Specify a timeout for the operation + required: False + default: 0 + state: + description: + - Enter or exit maintenance mode + choices: + - present + - absent + default: present + required: False +extends_documentation_fragment: vmware.documentation +''' + +EXAMPLES = ''' +- name: Enter VSAN-Compliant Maintenance Mode + local_action: + module: vmware_maintenancemode + hostname: vc_host + username: vc_user + password: vc_pass + esxi_hostname: esxi.host.example + vsan: ensureObjectAccessibility + evacuate: yes + timeout: 3600 + state: present +''' +RETURN = ''' +hostsystem: + description: Name of vim reference + returned: always + type: string + sample: "'vim.HostSystem:host-236'" +hostname: + description: Name of host in vCenter + returned: always + type: string + sample: "esxi.local.domain" +status: + description: Action taken + return: always + type: string + sample: "ENTER" +''' + +try: + from pyVmomi import vim + HAS_PYVMOMI = True + +except ImportError: + HAS_PYVMOMI = False + + +def EnterMaintenanceMode(module, host): + + if host.runtime.inMaintenanceMode: + module.exit_json( + changed=False, + hostsystem=str(host), + hostname=module.params['esxi_hostname'], + status='NO_ACTION', + msg='Host already in maintenance mode') + + spec = vim.host.MaintenanceSpec() + + if module.params['vsan']: + spec.vsanMode = vim.vsan.host.DecommissionMode() + spec.vsanMode.objectAction = module.params['vsan'] + + try: + task = host.EnterMaintenanceMode_Task( + module.params['timeout'], + module.params['evacuate'], + spec) + + success, result = wait_for_task(task) + + return dict(changed=success, + hostsystem=str(host), + hostname=module.params['esxi_hostname'], + status='ENTER', + msg='Host entered maintenance mode') + + except TaskError: + module.fail_json( + msg='Host failed to enter maintenance mode') + + +def ExitMaintenanceMode(module, host): + if not host.runtime.inMaintenanceMode: + module.exit_json( + changed=False, + hostsystem=str(host), + hostname=module.params['esxi_hostname'], + status='NO_ACTION', + msg='Host not in maintenance mode') + + try: + task = host.ExitMaintenanceMode_Task( + module.params['timeout']) + + success, result = wait_for_task(task) + + return dict(changed=success, + hostsystem=str(host), + hostname=module.params['esxi_hostname'], + status='EXIT', + msg='Host exited maintenance mode') + + except TaskError: + module.fail_json( + msg='Host failed to exit maintenance mode') + + +def main(): + spec = vmware_argument_spec() + spec.update(dict( + esxi_hostname=dict(required=True), + vsan=dict(required=False, choices=['ensureObjectAccessibility', + 'evacuateAllData', + 'noAction']), + evacuate=dict(required=False, type='bool', default=False), + timeout=dict(required=False, default=0), + state=dict(required=False, + default='present', + choices=['present', 'absent']))) + + module = AnsibleModule(argument_spec=spec) + + if not HAS_PYVMOMI: + module.fail_json(msg='pyvmomi is required for this module') + + content = connect_to_api(module) + host = find_hostsystem_by_name(content, module.params['esxi_hostname']) + + if not host: + module.fail_json( + msg='Host not found in vCenter') + + if module.params['state'] == 'present': + result = EnterMaintenanceMode(module, host) + + elif module.params['state'] == 'absent': + result = ExitMaintenanceMode(module, host) + + module.exit_json(**result) + + +from ansible.module_utils.basic import * +from ansible.module_utils.vmware import * + + +if __name__ == '__main__': + main() From 3ba9e817feda0b264d6a484327a9bcb8d4d32ae8 Mon Sep 17 00:00:00 2001 From: Jiri Tyr Date: Mon, 18 Apr 2016 16:01:24 +0100 Subject: [PATCH 1443/2522] Describing the _none_ value of the proxy option (#2053) --- packaging/os/yum_repository.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packaging/os/yum_repository.py b/packaging/os/yum_repository.py index a0060934cc4..97bb768b34a 100644 --- a/packaging/os/yum_repository.py +++ b/packaging/os/yum_repository.py @@ -272,7 +272,8 @@ required: false default: null description: - - URL to the proxy server that yum should use. + - URL to the proxy server that yum should use. Set to C(_none_) to disable + the global proxy setting. proxy_password: required: false default: null From f9f00ef404a529e872eeab2086d6ff355cc41062 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Mon, 18 Apr 2016 17:19:14 +0200 Subject: [PATCH 1444/2522] Mark the token as sensitive, since it shouldn't be printed (#2043) --- monitoring/bigpanda.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/bigpanda.py b/monitoring/bigpanda.py index 0139f3a598e..c045424569a 100644 --- a/monitoring/bigpanda.py +++ b/monitoring/bigpanda.py @@ -109,7 +109,7 @@ def main(): argument_spec=dict( component=dict(required=True, aliases=['name']), version=dict(required=True), - token=dict(required=True), + token=dict(required=True, no_log=True), state=dict(required=True, choices=['started', 'finished', 'failed']), hosts=dict(required=False, default=[socket.gethostname()], aliases=['host']), env=dict(required=False), From e8fdba759356cf7ad48a478a35d2ff40877d3548 Mon Sep 17 00:00:00 2001 From: Robin Roth Date: Mon, 18 Apr 2016 17:47:17 +0200 Subject: [PATCH 1445/2522] Zypper repository rewrite (#1990) * Remove support for ancient zypper versions Even SLES11 has zypper 1.x. * zypper_repository: don't silently ignore repo changes So far when a repo URL changes this got silently ignored (leading to incorrect package installations) due to this code: elif 'already exists. Please use another alias' in stderr: changed = False Removing this reveals that we correctly detect that a repo definition has changes (via repo_subset) but don't indicate this as change but as a nonexistent repo. This makes us currenlty bail out silently in the above statement. To fix this distinguish between non existent and modified repos and remove the repo first in case of modifications (since there is no force option in zypper to overwrite it and 'zypper mr' uses different arguments). To do this we have to identify a repo by name, alias or url. * Don't fail on empty values This unbreaks deleting repositories * refactor zypper_repository module * add properties enabled and priority * allow changing of one property and correctly report changed * allow overwrite of multiple repositories by alias and URL * cleanup of unused code and more structuring * respect enabled option * make zypper_repository conform to python2.4 * allow repo deletion only by alias * check for non-existant url field and use alias instead * remove empty notes and aliases * add version_added for priority and overwrite_multiple * add version requirement on zypper and distribution * zypper 1.0 is enough and exists * make suse versions note, not requirement based on comment by @alxgu --- packaging/os/zypper_repository.py | 219 +++++++++++++++--------------- 1 file changed, 112 insertions(+), 107 deletions(-) diff --git a/packaging/os/zypper_repository.py b/packaging/os/zypper_repository.py index 446723ef042..0e4e805856d 100644 --- a/packaging/os/zypper_repository.py +++ b/packaging/os/zypper_repository.py @@ -58,16 +58,28 @@ required: false default: "no" choices: [ "yes", "no" ] - aliases: [] refresh: description: - Enable autorefresh of the repository. required: false default: "yes" choices: [ "yes", "no" ] - aliases: [] -notes: [] -requirements: [ zypper ] + priority: + description: + - Set priority of repository. Packages will always be installed + from the repository with the smallest priority number. + required: false + version_added: "2.1" + overwrite_multiple: + description: + - Overwrite multiple repository entries, if repositories with both name and + URL already exist. + required: false + default: "no" + choices: [ "yes", "no" ] + version_added: "2.1" +requirements: + - "zypper >= 1.0 # included in openSuSE >= 11.1 or SuSE Linux Enterprise Server/Desktop >= 11.0" ''' EXAMPLES = ''' @@ -83,18 +95,10 @@ REPO_OPTS = ['alias', 'name', 'priority', 'enabled', 'autorefresh', 'gpgcheck'] -def zypper_version(module): - """Return (rc, message) tuple""" - cmd = ['/usr/bin/zypper', '-V'] - rc, stdout, stderr = module.run_command(cmd, check_rc=False) - if rc == 0: - return rc, stdout - else: - return rc, stderr - def _parse_repos(module): - """parses the output of zypper -x lr and returns a parse repo dictionary""" + """parses the output of zypper -x lr and return a parse repo dictionary""" cmd = ['/usr/bin/zypper', '-x', 'lr'] + from xml.dom.minidom import parseString as parseXML rc, stdout, stderr = module.run_command(cmd, check_rc=False) if rc == 0: @@ -120,81 +124,81 @@ def _parse_repos(module): d['stdout'] = stdout module.fail_json(msg='Failed to execute "%s"' % " ".join(cmd), **d) -def _parse_repos_old(module): - """parses the output of zypper sl and returns a parse repo dictionary""" - cmd = ['/usr/bin/zypper', 'sl'] - repos = [] - rc, stdout, stderr = module.run_command(cmd, check_rc=True) - for line in stdout.split('\n'): - matched = re.search(r'\d+\s+\|\s+(?P\w+)\s+\|\s+(?P\w+)\s+\|\s+(?P\w+)\s+\|\s+(?P\w+)\s+\|\s+(?P.*)', line) - if matched == None: - continue - - m = matched.groupdict() - m['alias']= m['name'] - m['priority'] = 100 - m['gpgcheck'] = 1 - repos.append(m) - - return repos - -def repo_exists(module, old_zypper, **kwargs): - - def repo_subset(realrepo, repocmp): - for k in repocmp: - if k not in realrepo: - return False - - for k, v in realrepo.items(): - if k in repocmp: - if v.rstrip("/") != repocmp[k].rstrip("/"): - return False - return True - - if old_zypper: - repos = _parse_repos_old(module) - else: - repos = _parse_repos(module) - - for repo in repos: - if repo_subset(repo, kwargs): +def _repo_changes(realrepo, repocmp): + for k in repocmp: + if repocmp[k] and k not in realrepo: return True + + for k, v in realrepo.items(): + if k in repocmp and repocmp[k]: + valold = str(repocmp[k] or "") + valnew = v or "" + if k == "url": + valold, valnew = valold.rstrip("/"), valnew.rstrip("/") + if valold != valnew: + return True return False +def repo_exists(module, repodata, overwrite_multiple): + existing_repos = _parse_repos(module) -def add_repo(module, repo, alias, description, disable_gpg_check, old_zypper, refresh): - if old_zypper: - cmd = ['/usr/bin/zypper', 'sa'] + # look for repos that have matching alias or url to the one searched + repos = [] + for kw in ['alias', 'url']: + name = repodata[kw] + for oldr in existing_repos: + if repodata[kw] == oldr[kw] and oldr not in repos: + repos.append(oldr) + + if len(repos) == 0: + # Repo does not exist yet + return (False, False, None) + elif len(repos) == 1: + # Found an existing repo, look for changes + has_changes = _repo_changes(repos[0], repodata) + return (True, has_changes, repos) + elif len(repos) == 2 and overwrite_multiple: + # Found two repos and want to overwrite_multiple + return (True, True, repos) else: - cmd = ['/usr/bin/zypper', 'ar', '--check'] + # either more than 2 repos (shouldn't happen) + # or overwrite_multiple is not active + module.fail_json(msg='More than one repo matched "%s": "%s"' % (name, repos)) - if repo.startswith("file:/") and old_zypper: - cmd.extend(['-t', 'Plaindir']) - else: - cmd.extend(['-t', 'plaindir']) +def modify_repo(module, repodata, old_repos): + repo = repodata['url'] + cmd = ['/usr/bin/zypper', 'ar', '--check'] + if repodata['name']: + cmd.extend(['--name', repodata['name']]) + + if repodata['priority']: + cmd.extend(['--priority', str(repodata['priority'])]) - if description: - cmd.extend(['--name', description]) + if repodata['enabled'] == '0': + cmd.append('--disable') - if disable_gpg_check and not old_zypper: + if repodata['gpgcheck'] == '1': + cmd.append('--gpgcheck') + else: cmd.append('--no-gpgcheck') - if refresh: + if repodata['autorefresh'] == '1': cmd.append('--refresh') cmd.append(repo) if not repo.endswith('.repo'): - cmd.append(alias) + cmd.append(repodata['alias']) + + if old_repos is not None: + for oldrepo in old_repos: + remove_repo(module, oldrepo['url']) rc, stdout, stderr = module.run_command(cmd, check_rc=False) changed = rc == 0 if rc == 0: changed = True - elif 'already exists. Please use another alias' in stderr: - changed = False else: - #module.fail_json(msg=stderr if stderr else stdout) if stderr: module.fail_json(msg=stderr) else: @@ -203,16 +207,8 @@ def add_repo(module, repo, alias, description, disable_gpg_check, old_zypper, re return changed -def remove_repo(module, repo, alias, old_zypper): - - if old_zypper: - cmd = ['/usr/bin/zypper', 'sd'] - else: - cmd = ['/usr/bin/zypper', 'rr'] - if alias: - cmd.append(alias) - else: - cmd.append(repo) +def remove_repo(module, repo): + cmd = ['/usr/bin/zypper', 'rr', repo] rc, stdout, stderr = module.run_command(cmd, check_rc=True) changed = rc == 0 @@ -237,59 +233,68 @@ def main(): description=dict(required=False), disable_gpg_check = dict(required=False, default='no', type='bool'), refresh = dict(required=False, default='yes', type='bool'), + priority = dict(required=False, type='int'), + enabled = dict(required=False, default='yes', type='bool'), + overwrite_multiple = dict(required=False, default='no', type='bool'), ), supports_check_mode=False, ) repo = module.params['repo'] + alias = module.params['name'] state = module.params['state'] - name = module.params['name'] - description = module.params['description'] - disable_gpg_check = module.params['disable_gpg_check'] - refresh = module.params['refresh'] + overwrite_multiple = module.params['overwrite_multiple'] + + repodata = { + 'url': repo, + 'alias': alias, + 'name': module.params['description'], + 'priority': module.params['priority'], + } + # rewrite bools in the language that zypper lr -x provides for easier comparison + if module.params['enabled']: + repodata['enabled'] = '1' + else: + repodata['enabled'] = '0' + if module.params['disable_gpg_check']: + repodata['gpgcheck'] = '0' + else: + repodata['gpgcheck'] = '1' + if module.params['refresh']: + repodata['autorefresh'] = '1' + else: + repodata['autorefresh'] = '0' def exit_unchanged(): - module.exit_json(changed=False, repo=repo, state=state, name=name) - - rc, out = zypper_version(module) - match = re.match(r'zypper\s+(\d+)\.(\d+)\.(\d+)', out) - if not match or int(match.group(1)) > 0: - old_zypper = False - else: - old_zypper = True + module.exit_json(changed=False, repodata=repodata, state=state) # Check run-time module parameters if state == 'present' and not repo: module.fail_json(msg='Module option state=present requires repo') - if state == 'absent' and not repo and not name: + if state == 'absent' and not repo and not alias: module.fail_json(msg='Alias or repo parameter required when state=absent') if repo and repo.endswith('.repo'): - if name: - module.fail_json(msg='Incompatible option: \'name\'. Do not use name when adding repo files') + if alias: + module.fail_json(msg='Incompatible option: \'name\'. Do not use name when adding .repo files') else: - if not name and state == "present": - module.fail_json(msg='Name required when adding non-repo files:') + if not alias and state == "present": + module.fail_json(msg='Name required when adding non-repo files.') - if repo and repo.endswith('.repo'): - exists = repo_exists(module, old_zypper, url=repo, alias=name) - elif repo: - exists = repo_exists(module, old_zypper, url=repo) - else: - exists = repo_exists(module, old_zypper, alias=name) + exists, mod, old_repos = repo_exists(module, repodata, overwrite_multiple) if state == 'present': - if exists: + if exists and not mod: exit_unchanged() - - changed = add_repo(module, repo, name, description, disable_gpg_check, old_zypper, refresh) + changed = modify_repo(module, repodata, old_repos) elif state == 'absent': if not exists: exit_unchanged() + if not repo: + repo=alias + changed = remove_repo(module, repo) - changed = remove_repo(module, repo, name, old_zypper) - - module.exit_json(changed=changed, repo=repo, state=state) + module.exit_json(changed=changed, repodata=repodata, state=state) # import module snippets from ansible.module_utils.basic import * From 1aecfc1e19b8c453227430c715cbde5fc7e3de6d Mon Sep 17 00:00:00 2001 From: Victor Costan Date: Mon, 18 Apr 2016 21:17:08 -0700 Subject: [PATCH 1446/2522] amazon/GUIDELINES.md: Fix copy-paste typo (#2060) --- cloud/amazon/GUIDELINES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/GUIDELINES.md b/cloud/amazon/GUIDELINES.md index 017ff9090db..c3416240e22 100644 --- a/cloud/amazon/GUIDELINES.md +++ b/cloud/amazon/GUIDELINES.md @@ -80,7 +80,7 @@ except ImportError: def main(): if not HAS_BOTO3: - module.fail_json(msg='boto required for this module') + module.fail_json(msg='boto3 required for this module') ``` #### boto and boto3 combined From e8391d69855b42d8108fd921f7d6375714494a0e Mon Sep 17 00:00:00 2001 From: Adam Romanek Date: Tue, 19 Apr 2016 17:20:51 +0200 Subject: [PATCH 1447/2522] Fixed #237 - improved embedded JSON support handling (#1530) --- messaging/rabbitmq_parameter.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/messaging/rabbitmq_parameter.py b/messaging/rabbitmq_parameter.py index 6be18bdce3d..60b9811a9cb 100644 --- a/messaging/rabbitmq_parameter.py +++ b/messaging/rabbitmq_parameter.py @@ -126,6 +126,8 @@ def main(): component = module.params['component'] name = module.params['name'] value = module.params['value'] + if not isinstance(value, str): + value = json.dumps(value) vhost = module.params['vhost'] state = module.params['state'] node = module.params['node'] From 706cbf69cae0c226eafbafc0a0328a40ef207548 Mon Sep 17 00:00:00 2001 From: p53 Date: Tue, 19 Apr 2016 19:31:23 +0200 Subject: [PATCH 1448/2522] Add pvs parameter to documentation Add pvs parameter to documentation --- system/lvol.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/system/lvol.py b/system/lvol.py index 5cd082e96b0..e152e0ee36d 100644 --- a/system/lvol.py +++ b/system/lvol.py @@ -68,6 +68,11 @@ description: - The name of the snapshot volume required: false + pvs: + version_added: "2.1" + description: + - Comma separated list of physical volumes e.g. /dev/sda,/dev/sdb + required: false notes: - Filesystems on top of the volume are not resized. ''' From c3d8b074324386d7507f44ca81d224ad2bbc978e Mon Sep 17 00:00:00 2001 From: Rob Date: Wed, 20 Apr 2016 08:07:59 +1000 Subject: [PATCH 1449/2522] New module - ec2_snapshot_facts (#1088) --- cloud/amazon/ec2_snapshot_facts.py | 226 +++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 cloud/amazon/ec2_snapshot_facts.py diff --git a/cloud/amazon/ec2_snapshot_facts.py b/cloud/amazon/ec2_snapshot_facts.py new file mode 100644 index 00000000000..9904eb8591d --- /dev/null +++ b/cloud/amazon/ec2_snapshot_facts.py @@ -0,0 +1,226 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ec2_snapshot_facts +short_description: Gather facts about ec2 volume snapshots in AWS +description: + - Gather facts about ec2 volume snapshots in AWS +version_added: "2.1" +author: "Rob White (@wimnat)" +options: + snapshot_ids: + description: + - If you specify one or more snapshot IDs, only snapshots that have the specified IDs are returned. + required: false + default: [] + owner_ids: + description: + - If you specify one or more snapshot owners, only snapshots from the specified owners and for which you have \ + access are returned. + required: false + default: [] + restorable_by_user_ids: + description: + - If you specify a list of restorable users, only snapshots with create snapshot permissions for those users are \ + returned. + required: false + default: [] + filters: + description: + - A dict of filters to apply. Each dict item consists of a filter key and a filter value. See \ + U(http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeSnapshots.html) for possible filters. Filter \ + names and values are case sensitive. + required: false + default: {} +notes: + - By default, the module will return all snapshots, including public ones. To limit results to snapshots owned by \ + the account use the filter 'owner-id'. + +extends_documentation_fragment: + - aws + - ec2 +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Gather facts about all snapshots, including public ones +- ec2_snapshot_facts: + +# Gather facts about all snapshots owned by the account 0123456789 +- ec2_snapshot_facts: + filters: + owner-id: 0123456789 + +# Or alternatively... +- ec2_snapshot_facts: + owner_ids: + - 0123456789 + +# Gather facts about a particular snapshot using ID +- ec2_snapshot_facts: + filters: + snapshot-id: snap-00112233 + +# Or alternatively... +- ec2_snapshot_facts: + snapshot_ids: + - snap-00112233 + +# Gather facts about any snapshot with a tag key Name and value Example +- ec2_snapshot_facts: + filters: + "tag:Name": Example + +# Gather facts about any snapshot with an error status +- ec2_snapshot_facts: + filters: + status: error + +''' + +RETURN = ''' +snapshot_id: + description: The ID of the snapshot. Each snapshot receives a unique identifier when it is created. + type: string + sample: snap-01234567 +volume_id: + description: The ID of the volume that was used to create the snapshot. + type: string + sample: vol-01234567 +state: + description: The snapshot state (completed, pending or error). + type: string + sample: completed +state_message: + description: Encrypted Amazon EBS snapshots are copied asynchronously. If a snapshot copy operation fails (for example, if the proper AWS Key Management Service (AWS KMS) permissions are not obtained) this field displays error state details to help you diagnose why the error occurred. + type: string + sample: +start_time: + description: The time stamp when the snapshot was initiated. + type: datetime + sample: 2015-02-12T02:14:02+00:00 +progress: + description: The progress of the snapshot, as a percentage. + type: string + sample: 100% +owner_id: + description: The AWS account ID of the EBS snapshot owner. + type: string + sample: 099720109477 +description: + description: The description for the snapshot. + type: string + sample: My important backup +volume_size: + description: The size of the volume, in GiB. + type: integer + sample: 8 +owner_alias: + description: The AWS account alias (for example, amazon, self) or AWS account ID that owns the snapshot. + type: string + sample: 033440102211 +tags: + description: Any tags assigned to the snapshot. + type: list + sample: "{ 'my_tag_key': 'my_tag_value' }" +encrypted: + description: Indicates whether the snapshot is encrypted. + type: boolean + sample: True +kms_key_id: + description: The full ARN of the AWS Key Management Service (AWS KMS) customer master key (CMK) that was used to \ + protect the volume encryption key for the parent volume. + type: string + sample: 74c9742a-a1b2-45cb-b3fe-abcdef123456 +data_encryption_key_id: + description: The data encryption key identifier for the snapshot. This value is a unique identifier that \ + corresponds to the data encryption key that was used to encrypt the original volume or snapshot copy. + type: string + sample: "arn:aws:kms:ap-southeast-2:012345678900:key/74c9742a-a1b2-45cb-b3fe-abcdef123456" + +''' + +try: + import boto3 + from botocore.exceptions import ClientError, NoCredentialsError + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + + +def list_ec2_snapshots(connection, module): + + snapshot_ids = module.params.get("snapshot_ids") + owner_ids = module.params.get("owner_ids") + restorable_by_user_ids = module.params.get("restorable_by_user_ids") + filters = ansible_dict_to_boto3_filter_list(module.params.get("filters")) + + try: + snapshots = connection.describe_snapshots(SnapshotIds=snapshot_ids, OwnerIds=owner_ids, RestorableByUserIds=restorable_by_user_ids, Filters=filters) + except ClientError, e: + module.fail_json(msg=e.message) + + # Turn the boto3 result in to ansible_friendly_snaked_names + snaked_snapshots = [] + for snapshot in snapshots['Snapshots']: + snaked_snapshots.append(camel_dict_to_snake_dict(snapshot)) + + # Turn the boto3 result in to ansible friendly tag dictionary + for snapshot in snaked_snapshots: + if 'tags' in snapshot: + snapshot['tags'] = boto3_tag_list_to_ansible_dict(snapshot['tags']) + + module.exit_json(snapshots=snaked_snapshots) + + +def main(): + + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + snapshot_ids=dict(default=[], type='list'), + owner_ids=dict(default=[], type='list'), + restorable_by_user_ids=dict(default=[], type='list'), + filters=dict(default={}, type='dict') + ) + ) + + module = AnsibleModule(argument_spec=argument_spec, + mutually_exclusive=[ + ['snapshot_ids', 'owner_ids', 'restorable_by_user_ids', 'filters'] + ] + ) + + if not HAS_BOTO3: + module.fail_json(msg='boto3 required for this module') + + region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) + + if region: + connection = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_params) + else: + module.fail_json(msg="region must be specified") + + list_ec2_snapshots(connection, module) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From 55c6aee5d17b05f210089c4fdf15632300891bd6 Mon Sep 17 00:00:00 2001 From: Dreamcat4 Date: Wed, 20 Apr 2016 14:12:11 +0100 Subject: [PATCH 1450/2522] fix: win-scheduled-task strict-mode fixes --- windows/win_scheduled_task.ps1 | 45 ++++++++++++---------------------- 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/windows/win_scheduled_task.ps1 b/windows/win_scheduled_task.ps1 index b63bd130134..7d409050ae9 100644 --- a/windows/win_scheduled_task.ps1 +++ b/windows/win_scheduled_task.ps1 @@ -23,6 +23,13 @@ $ErrorActionPreference = "Stop" # POWERSHELL_COMMON $params = Parse-Args $args; + +$days_of_week = Get-Attr $params "days_of_week" $null; +$enabled = Get-Attr $params "enabled" $true | ConvertTo-Bool; +$description = Get-Attr $params "description" " "; +$path = Get-Attr $params "path" $null; +$argument = Get-Attr $params "argument" $null; + $result = New-Object PSObject; Set-Attr $result "changed" $false; @@ -40,33 +47,17 @@ if($state -eq "present") { $time = Get-Attr -obj $params -name time -failifempty $true -resultobj $result $user = Get-Attr -obj $params -name user -failifempty $true -resultobj $result } -if ($params.days_of_week) -{ - $days_of_week = $params.days_of_week -} -elseif ($frequency -eq "weekly") -{ - Fail-Json $result "missing required argument: days_of_week" -} -# Vars with defaults -if ($params.enabled) -{ - $enabled = $params.enabled | ConvertTo-Bool -} -else -{ - $enabled = $true #default -} -if ($params.description) +# Mandatory Vars +if ($frequency -eq "weekly") { - $description = $params.description -} -else -{ - $description = " " #default + if (!($days_of_week)) + { + Fail-Json $result "missing required argument: days_of_week" + } } -if ($params.path) + +if ($path) { $path = "\{0}\" -f $params.path } @@ -75,12 +66,6 @@ else $path = "\" #default } -# Optional vars -if ($params.argument) -{ - $argument = $params.argument -} - try { $task = Get-ScheduledTask -TaskPath "$path" | Where-Object {$_.TaskName -eq "$name"} From 8734e8f397275c54134e67a2f7ee504e22aebc21 Mon Sep 17 00:00:00 2001 From: Dreamcat4 Date: Wed, 20 Apr 2016 14:13:03 +0100 Subject: [PATCH 1451/2522] fix: win-environment strict-mode fixes --- windows/win_environment.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/windows/win_environment.ps1 b/windows/win_environment.ps1 index 1398524cfbb..bece081282d 100644 --- a/windows/win_environment.ps1 +++ b/windows/win_environment.ps1 @@ -20,11 +20,12 @@ # POWERSHELL_COMMON $params = Parse-Args $args; +$state = Get-Attr $params "state" $null; $result = New-Object PSObject; Set-Attr $result "changed" $false; -If ($params.state) { - $state = $params.state.ToString().ToLower() +If ($state) { + $state = $state.ToString().ToLower() If (($state -ne 'present') -and ($state -ne 'absent') ) { Fail-Json $result "state is '$state'; must be 'present', or 'absent'" } From 9bc12dc9dd72ffde64ab7c1b8b7c1bb2678b814d Mon Sep 17 00:00:00 2001 From: Dreamcat4 Date: Wed, 20 Apr 2016 21:44:29 +0100 Subject: [PATCH 1452/2522] win-firewall-rule: temp disable strict-mode for the time being --- windows/win_firewall_rule.ps1 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/windows/win_firewall_rule.ps1 b/windows/win_firewall_rule.ps1 index 21f96bcf33f..92d75921547 100644 --- a/windows/win_firewall_rule.ps1 +++ b/windows/win_firewall_rule.ps1 @@ -20,6 +20,9 @@ # WANT_JSON # POWERSHELL_COMMON +# temporarily disable strictmode, for this module only +Set-StrictMode -Off + function getFirewallRule ($fwsettings) { try { From 54a6a470b56fc2381a420900e21542df7a6789a5 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 20 Apr 2016 14:36:43 -0700 Subject: [PATCH 1453/2522] Make main() only run when invoked as a script (style cleanup) --- cloud/lxc/lxc_container.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index 2abbb41aedf..da9c486a868 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -1776,4 +1776,5 @@ def main(): # import module bits from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() From 14c323cc8e0e7175e3f0c5b09ea3a3589d6560a2 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Thu, 21 Apr 2016 13:24:19 +0200 Subject: [PATCH 1454/2522] Fix default url in airbrake_deployment doc (#2078) --- monitoring/airbrake_deployment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/airbrake_deployment.py b/monitoring/airbrake_deployment.py index a58df024182..61ad74c1cd2 100644 --- a/monitoring/airbrake_deployment.py +++ b/monitoring/airbrake_deployment.py @@ -51,7 +51,7 @@ description: - Optional URL to submit the notification to. Use to send notifications to Airbrake-compliant tools like Errbit. required: false - default: "https://airbrake.io/deploys" + default: "https://airbrake.io/deploys.txt" version_added: "1.5" validate_certs: description: From fe2dc427d4dca356e702571a81790e53a513ff33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Magnus=20Landr=C3=B8?= Date: Thu, 21 Apr 2016 21:51:55 +0200 Subject: [PATCH 1455/2522] =?UTF-8?q?Honouring=20verify=5Fssl=20when=20usi?= =?UTF-8?q?ng=20username/password=20for=20authentication=20=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source_control/gitlab_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source_control/gitlab_project.py b/source_control/gitlab_project.py index 602b9e832d7..4f016bc1232 100644 --- a/source_control/gitlab_project.py +++ b/source_control/gitlab_project.py @@ -357,7 +357,7 @@ def main(): # or with login_token try: if use_credentials: - git = gitlab.Gitlab(host=server_url) + git = gitlab.Gitlab(host=server_url, verify_ssl=verify_ssl) git.login(user=login_user, password=login_password) else: git = gitlab.Gitlab(server_url, token=login_token, verify_ssl=verify_ssl) From ba74516640ac99c8a2249ee2d5dde3067206d874 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Thu, 21 Apr 2016 21:53:37 +0200 Subject: [PATCH 1456/2522] Fix aibrake --- monitoring/airbrake_deployment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/airbrake_deployment.py b/monitoring/airbrake_deployment.py index 61ad74c1cd2..262c3d2b445 100644 --- a/monitoring/airbrake_deployment.py +++ b/monitoring/airbrake_deployment.py @@ -81,7 +81,7 @@ def main(): module = AnsibleModule( argument_spec=dict( - token=dict(required=True), + token=dict(required=True, no_log=True), environment=dict(required=True), user=dict(required=False), repo=dict(required=False), From 3031105e78320c8245e851f994a337ebeb55705f Mon Sep 17 00:00:00 2001 From: Emil Bostijancic Date: Thu, 21 Apr 2016 22:02:33 +0200 Subject: [PATCH 1457/2522] Fixes maven_artifact - verify_md5 only called for SNAPSHOT * fixed https://github.com/ansible/ansible-modules-extras/issues/2066 * fixes https://github.com/ansible/ansible-modules-extras/issues/2066 --- packaging/language/maven_artifact.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packaging/language/maven_artifact.py b/packaging/language/maven_artifact.py index 203f09dacc3..77edd449afa 100644 --- a/packaging/language/maven_artifact.py +++ b/packaging/language/maven_artifact.py @@ -54,17 +54,17 @@ required: false default: latest classifier: - description: + description: - The maven classifier coordinate required: false default: null extension: - description: + description: - The maven type/extension coordinate required: false default: jar repository_url: - description: + description: - The URL of the Maven Repository to download from required: false default: http://repo1.maven.org/maven2 @@ -90,7 +90,7 @@ default: present choices: [present,absent] validate_certs: - description: + description: - If C(no), SSL certificates will not be validated. This should only be set to C(no) when no other option exists. required: false default: 'yes' @@ -202,7 +202,10 @@ def find_uri_for_artifact(self, artifact): buildNumber = xml.xpath("/metadata/versioning/snapshot/buildNumber/text()")[0] return self._uri_for_artifact(artifact, artifact.version.replace("SNAPSHOT", timestamp + "-" + buildNumber)) else: - return self._uri_for_artifact(artifact) + if artifact.version == "latest": + artifact.version = self._find_latest_version_available(artifact) + + return self._uri_for_artifact(artifact, artifact.version) def _uri_for_artifact(self, artifact, version=None): if artifact.is_snapshot() and not version: @@ -331,11 +334,8 @@ def main(): prev_state = "absent" if os.path.isdir(dest): dest = dest + "/" + artifact_id + "-" + version + "." + extension - if os.path.lexists(dest): - if not artifact.is_snapshot(): - prev_state = "present" - elif downloader.verify_md5(dest, downloader.find_uri_for_artifact(artifact) + '.md5'): - prev_state = "present" + if os.path.lexists(dest) and downloader.verify_md5(dest, downloader.find_uri_for_artifact(artifact) + '.md5'): + prev_state = "present" else: path = os.path.dirname(dest) if not os.path.exists(path): From 357cbd73f5ae693f13afc476003720e0b6ede2e6 Mon Sep 17 00:00:00 2001 From: Enric Lluelles Date: Wed, 27 May 2015 13:00:53 +0200 Subject: [PATCH 1458/2522] Adding install_options to homebrew_cask --- packaging/os/homebrew_cask.py | 36 ++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/packaging/os/homebrew_cask.py b/packaging/os/homebrew_cask.py index aa7d7ed84b8..20d9d8b61f9 100755 --- a/packaging/os/homebrew_cask.py +++ b/packaging/os/homebrew_cask.py @@ -19,7 +19,9 @@ DOCUMENTATION = ''' --- module: homebrew_cask -author: "Daniel Jaouen (@danieljaouen)" +author: + - "Daniel Jaouen (@danieljaouen)" + - "Enric Lluelles (@enriclluelles)" short_description: Install/uninstall homebrew casks. description: - Manages Homebrew casks. @@ -35,10 +37,16 @@ choices: [ 'present', 'absent' ] required: false default: present + install_options: + description: + - options flags to install a package + required: false + default: null ''' EXAMPLES = ''' - homebrew_cask: name=alfred state=present - homebrew_cask: name=alfred state=absent +- homebrew_cask: name=alfred state=absent install_options="appdir=/Applications" ''' import os.path @@ -251,10 +259,11 @@ def current_cask(self, cask): return cask # /class properties -------------------------------------------- }}} - def __init__(self, module, path=None, casks=None, state=None): + def __init__(self, module, path=None, casks=None, state=None, + install_options=None): self._setup_status_vars() self._setup_instance_vars(module=module, path=path, casks=casks, - state=state) + state=state, install_options=install_options) self._prep() @@ -395,9 +404,12 @@ def _install_current_cask(self): ) raise HomebrewCaskException(self.message) - cmd = [opt - for opt in (self.brew_path, 'cask', 'install', self.current_cask) - if opt] + opts = ( + [self.brew_path, 'cask', 'install', self.current_cask] + + self.install_options + ) + + cmd = [opt for opt in opts if opt] rc, out, err = self.module.run_command(cmd, path_prefix=self.path[0]) @@ -478,6 +490,11 @@ def main(): "absent", "removed", "uninstalled", ], ), + install_options=dict( + default=None, + aliases=['options'], + type='list', + ) ), supports_check_mode=True, ) @@ -503,8 +520,13 @@ def main(): if state in ('absent', 'removed', 'uninstalled'): state = 'absent' + + p['install_options'] = p['install_options'] or [] + install_options = ['--{0}'.format(install_option) + for install_option in p['install_options']] + brew_cask = HomebrewCask(module=module, path=path, casks=casks, - state=state) + state=state, install_options=install_options) (failed, changed, message) = brew_cask.run() if failed: module.fail_json(msg=message) From 8128464adcf5dfd6e5d8d9cc2994211c41f82d2f Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Thu, 21 Apr 2016 19:26:47 -0500 Subject: [PATCH 1459/2522] Add additional examples for parameter `install_options` in homebrew_cask --- packaging/os/homebrew_cask.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packaging/os/homebrew_cask.py b/packaging/os/homebrew_cask.py index 20d9d8b61f9..c85c04dde6f 100755 --- a/packaging/os/homebrew_cask.py +++ b/packaging/os/homebrew_cask.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # (c) 2013, Daniel Jaouen +# (c) 2016, Indrajit Raychaudhuri # # This module is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -20,6 +21,7 @@ --- module: homebrew_cask author: + - "Indrajit Raychaudhuri (@indrajitr)" - "Daniel Jaouen (@danieljaouen)" - "Enric Lluelles (@enriclluelles)" short_description: Install/uninstall homebrew casks. @@ -46,7 +48,9 @@ EXAMPLES = ''' - homebrew_cask: name=alfred state=present - homebrew_cask: name=alfred state=absent -- homebrew_cask: name=alfred state=absent install_options="appdir=/Applications" +- homebrew_cask: name=alfred state=present install_options="appdir=/Applications" +- homebrew_cask: name=alfred state=present install_options="--debug appdir=/Applications" +- homebrew_cask: name=alfred state=absent install_options="--force" ''' import os.path @@ -538,4 +542,3 @@ def main(): if __name__ == '__main__': main() - From c5ed63a972660499618468921356dd0e114f59a6 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Fri, 22 Apr 2016 00:19:34 -0500 Subject: [PATCH 1460/2522] Add `version_added` doc property for `install_options` in homebrew_cask --- packaging/os/homebrew_cask.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packaging/os/homebrew_cask.py b/packaging/os/homebrew_cask.py index c85c04dde6f..d61719a5494 100755 --- a/packaging/os/homebrew_cask.py +++ b/packaging/os/homebrew_cask.py @@ -44,6 +44,7 @@ - options flags to install a package required: false default: null + version_added: "2.1" ''' EXAMPLES = ''' - homebrew_cask: name=alfred state=present From ed4dd65057ea16eabe8223f2c374286ff7b1f8a7 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Fri, 22 Apr 2016 17:29:51 +0200 Subject: [PATCH 1461/2522] Mark api_key as no_log, since that's supposed to be kept secret --- monitoring/datadog_event.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/datadog_event.py b/monitoring/datadog_event.py index 25e8ce052b6..67ec8c64280 100644 --- a/monitoring/datadog_event.py +++ b/monitoring/datadog_event.py @@ -95,7 +95,7 @@ def main(): module = AnsibleModule( argument_spec=dict( - api_key=dict(required=True), + api_key=dict(required=True, no_log=True), title=dict(required=True), text=dict(required=True), date_happened=dict(required=False, default=None, type='int'), From 1b18c74918e8a3eda485c2165ca4d2119de8a6f0 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Fri, 22 Apr 2016 17:30:25 +0200 Subject: [PATCH 1462/2522] Remove unused import of 'socket' module --- monitoring/datadog_event.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/monitoring/datadog_event.py b/monitoring/datadog_event.py index 67ec8c64280..a863c57f461 100644 --- a/monitoring/datadog_event.py +++ b/monitoring/datadog_event.py @@ -90,8 +90,6 @@ tags=aa,bb,#host:{{ inventory_hostname }} ''' -import socket - def main(): module = AnsibleModule( argument_spec=dict( From 7d9b73ec5ab987f2b16da67a3a2abb25f9792443 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 22 Apr 2016 18:37:31 +0100 Subject: [PATCH 1463/2522] fix problem where you couldn't compare empty strings in win_regedit following my previous change --- windows/win_regedit.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/windows/win_regedit.ps1 b/windows/win_regedit.ps1 index 1b5c9cf9918..c98c79ce8ef 100644 --- a/windows/win_regedit.ps1 +++ b/windows/win_regedit.ps1 @@ -63,9 +63,9 @@ Function Test-RegistryValueData { Function Compare-RegistryData { Param ( [parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()]$ReferenceData, + [AllowEmptyString()]$ReferenceData, [parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()]$DifferenceData + [AllowEmptyString()]$DifferenceData ) $refType = $ReferenceData.GetType().Name From 8d7d07020117c7bca9f66d3d17f72260a389ee31 Mon Sep 17 00:00:00 2001 From: Chris Lamb Date: Sat, 23 Apr 2016 07:01:19 +0100 Subject: [PATCH 1464/2522] =?UTF-8?q?system/ufw.py:=20Add=20security=20war?= =?UTF-8?q?ning=20re.=20removing=20ufw=20application=20prof=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It's not particularly obvious that removing an application will remove it from ufw's own state, potentially leaving ports open on your box if you upload your configuration. Whilst this applies to a lot of things in Ansible, firewall rules might cross some sort of line that justifies such a warning in his instance. Signed-off-by: Chris Lamb --- system/ufw.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/system/ufw.py b/system/ufw.py index cd148edf2ef..89376e7c22e 100644 --- a/system/ufw.py +++ b/system/ufw.py @@ -142,7 +142,9 @@ # for details. Typical usage is: ufw: rule=limit port=ssh proto=tcp -# Allow OpenSSH +# Allow OpenSSH. (Note that as ufw manages its own state, simply removing +# a rule=allow task can leave those ports exposed. Either use delete=yes +# or a separate state=reset task) ufw: rule=allow name=OpenSSH # Delete OpenSSH rule From e24c3b93fed4f71390efec2418f0b1b34f54c114 Mon Sep 17 00:00:00 2001 From: Ritesh Khadgaray Date: Sat, 23 Apr 2016 11:49:17 +0530 Subject: [PATCH 1465/2522] vmware_vm_shell: speed up vm_name search (#1909) --- cloud/vmware/vmware_vm_shell.py | 64 ++++++++++++++++----------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/cloud/vmware/vmware_vm_shell.py b/cloud/vmware/vmware_vm_shell.py index 8c5752b3e31..a2de750714c 100644 --- a/cloud/vmware/vmware_vm_shell.py +++ b/cloud/vmware/vmware_vm_shell.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# (c) 2015, Ritesh Khadgaray +# (c) 2015, 2016 Ritesh Khadgaray # # This file is part of Ansible # @@ -36,6 +36,12 @@ datacenter: description: - The datacenter hosting the VM + - Will help speed up search + required: False + cluster: + description: + - The cluster hosting the VM + - Will help speed up search required: False vm_id: description: @@ -89,14 +95,14 @@ username: myUsername password: mySecret datacenter: myDatacenter - vm_id: DNSnameOfVM + vm_id: NameOfVM vm_username: root vm_password: superSecret vm_shell: /bin/echo vm_shell_args: " $var >> myFile " vm_shell_env: - "PATH=/bin" - - "var=test" + - "VAR=test" vm_shell_cwd: "/tmp" ''' @@ -107,28 +113,6 @@ except ImportError: HAS_PYVMOMI = False -def find_vm(content, vm_id, vm_id_type="dns_name", datacenter=None): - si = content.searchIndex - vm = None - - if datacenter: - datacenter = find_datacenter_by_name(content, datacenter) - - if vm_id_type == 'dns_name': - vm = si.FindByDnsName(datacenter=datacenter, dnsName=vm_id, vmSearch=True) - elif vm_id_type == 'inventory_path': - vm = si.FindByInventoryPath(inventoryPath=vm_id) - if type(vm) != type(vim.VirtualMachine): - vm = None - elif vm_id_type == 'uuid': - vm = si.FindByUuid(datacenter=datacenter, uuid=vm_id, vmSearch=True) - elif vm_id_type == 'vm_name': - for machine in get_all_objs(content, [vim.VirtualMachine]): - if machine.name == vm_id: - vm = machine - - return vm - # https://github.com/vmware/pyvmomi-community-samples/blob/master/samples/execute_program_in_vm.py def execute_command(content, vm, vm_username, vm_password, program_path, args="", env=None, cwd=None): @@ -142,6 +126,7 @@ def main(): argument_spec = vmware_argument_spec() argument_spec.update(dict(datacenter=dict(default=None, type='str'), + cluster=dict(default=None, type='str'), vm_id=dict(required=True, type='str'), vm_id_type=dict(default='vm_name', type='str', choices=['inventory_path', 'uuid', 'dns_name', 'vm_name']), vm_username=dict(required=False, type='str'), @@ -154,26 +139,41 @@ def main(): module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False) if not HAS_PYVMOMI: - module.fail_json(msg='pyvmomi is required for this module') + module.fail_json(changed=False, msg='pyvmomi is required for this module') + + datacenter_name = p['datacenter'] + cluster_name = p['cluster'] try: p = module.params content = connect_to_api(module) - vm = find_vm(content, p['vm_id'], p['vm_id_type'], p['datacenter']) + datacenter = None + if datacenter_name: + datacenter = find_datacenter_by_name(content, datacenter_name) + if not datacenter: + module.fail_json(changed=False, msg="datacenter not found") + + cluster = None + if cluster_name: + cluster = find_cluster_by_name(content, cluster_name, datacenter) + if not cluster: + module.fail_json(changed=False, msg="cluster not found") + + vm = find_vm_by_id(content, p['vm_id'], p['vm_id_type'], datacenter, cluster) if not vm: - module.fail_json(msg='failed to find VM') + module.fail_json(msg='VM not found') msg = execute_command(content, vm, p['vm_username'], p['vm_password'], p['vm_shell'], p['vm_shell_args'], p['vm_shell_env'], p['vm_shell_cwd']) - module.exit_json(changed=False, virtual_machines=vm.name, msg=msg) + module.exit_json(changed=True, uuid=vm.summary.config.uuid, msg=msg) except vmodl.RuntimeFault as runtime_fault: - module.fail_json(msg=runtime_fault.msg) + module.fail_json(changed=False, msg=runtime_fault.msg) except vmodl.MethodFault as method_fault: - module.fail_json(msg=method_fault.msg) + module.fail_json(changed=False, msg=method_fault.msg) except Exception as e: - module.fail_json(msg=str(e)) + module.fail_json(changed=False, msg=str(e)) from ansible.module_utils.vmware import * from ansible.module_utils.basic import * From de22b721db288e1c3894b3d763442813e8bcc4f2 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 23 Apr 2016 08:27:21 +0200 Subject: [PATCH 1466/2522] vmware_vm_shell: doc: add missing defaults --- cloud/vmware/vmware_vm_shell.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cloud/vmware/vmware_vm_shell.py b/cloud/vmware/vmware_vm_shell.py index a2de750714c..a2d5c71098e 100644 --- a/cloud/vmware/vmware_vm_shell.py +++ b/cloud/vmware/vmware_vm_shell.py @@ -38,11 +38,13 @@ - The datacenter hosting the VM - Will help speed up search required: False + default: None cluster: description: - The cluster hosting the VM - Will help speed up search required: False + default: None vm_id: description: - The identification for the VM @@ -57,21 +59,24 @@ - 'inventory_path' - 'vm_name' required: False + default: None vm_username: description: - The user to connect to the VM. required: False + default: None vm_password: description: - The password used to login to the VM. required: False + default: None vm_shell: description: - The absolute path to the program to start. On Linux this is executed via bash. required: True vm_shell_args: description: - - The argument to the program. + - The argument to the program. required: False default: None vm_shell_env: @@ -164,7 +169,7 @@ def main(): if not vm: module.fail_json(msg='VM not found') - msg = execute_command(content, vm, p['vm_username'], p['vm_password'], + msg = execute_command(content, vm, p['vm_username'], p['vm_password'], p['vm_shell'], p['vm_shell_args'], p['vm_shell_env'], p['vm_shell_cwd']) module.exit_json(changed=True, uuid=vm.summary.config.uuid, msg=msg) From d871df6c27a5d5baacb7c155c3db15b61237e97b Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Sun, 24 Apr 2016 21:14:30 -0700 Subject: [PATCH 1467/2522] Switch from deprecated ANSIBLE_VERSION to ansible.__version__ --- files/blockinfile.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/files/blockinfile.py b/files/blockinfile.py index 25f8cd0e80e..3ff1bc7c720 100644 --- a/files/blockinfile.py +++ b/files/blockinfile.py @@ -18,10 +18,6 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -import re -import os -import tempfile - DOCUMENTATION = """ --- module: blockinfile @@ -150,6 +146,12 @@ - { name: host3, ip: 10.10.1.12 } """ +import re +import os +import tempfile + +from ansible import __version__ + def write_changes(module, contents, dest): @@ -244,7 +246,7 @@ def main(): marker1 = re.sub(r'{mark}', 'END', marker) if present and block: # Escape seqeuences like '\n' need to be handled in Ansible 1.x - if ANSIBLE_VERSION.startswith('1.'): + if __version__.startswith('1.'): block = re.sub('', block, '') blocklines = [marker0] + block.splitlines() + [marker1] else: From 014297b86802f43e435144535bb45d0df68bf96a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Gal=C3=ADn=20Figueras?= Date: Mon, 25 Apr 2016 08:09:51 +0200 Subject: [PATCH 1468/2522] Fixed netif params when create lxc container (#2064) --- cloud/misc/proxmox.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cloud/misc/proxmox.py b/cloud/misc/proxmox.py index 1b908d99c15..9647db4bf8e 100644 --- a/cloud/misc/proxmox.py +++ b/cloud/misc/proxmox.py @@ -213,6 +213,9 @@ def create_instance(module, proxmox, vmid, node, disk, storage, cpus, memory, sw if VZ_TYPE =='lxc': kwargs['cpulimit']=cpus kwargs['rootfs']=disk + if 'netif' in kwargs: + kwargs.update(kwargs['netif']) + del kwargs['netif'] else: kwargs['cpus']=cpus kwargs['disk']=disk From d3a34c0b2c91c9661ec63514c1589e7bd0e60fba Mon Sep 17 00:00:00 2001 From: Ner'zhul Date: Mon, 25 Apr 2016 08:39:42 +0200 Subject: [PATCH 1469/2522] Add mongodb parameter module (#1596) * Add mongodb parameter module This module permit to configure mongodb live parameters to tune the running engine --- database/misc/mongodb_parameter.py | 228 +++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 database/misc/mongodb_parameter.py diff --git a/database/misc/mongodb_parameter.py b/database/misc/mongodb_parameter.py new file mode 100644 index 00000000000..4904be3db32 --- /dev/null +++ b/database/misc/mongodb_parameter.py @@ -0,0 +1,228 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +(c) 2016, Loic Blot +Sponsored by Infopro Digital. http://www.infopro-digital.com/ +Sponsored by E.T.A.I. http://www.etai.fr/ + +This file is part of Ansible + +Ansible is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Ansible is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Ansible. If not, see . +""" + +DOCUMENTATION = ''' +--- +module: mongodb_parameter +short_description: Change an administrative parameter on a MongoDB server. +description: + - Change an administrative parameter on a MongoDB server. +version_added: "2.1" +options: + 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: + - The host running the database + required: false + default: localhost + login_port: + description: + - The port to connect to + required: false + default: 27017 + login_database: + description: + - The database where login credentials are stored + required: false + default: null + replica_set: + description: + - Replica set to connect to (automatically connects to primary for writes) + required: false + default: null + database: + description: + - The name of the database to add/remove the user from + required: true + ssl: + description: + - Whether to use an SSL connection when connecting to the database + required: false + default: false + param: + description: + - MongoDB administrative parameter to modify + required: true + value: + description: + - MongoDB administrative parameter value to set + required: true + param_type: + description: + - Define the parameter value (str, int) + required: false + default: str + +notes: + - Requires the pymongo Python package on the remote host, version 2.4.2+. This + can be installed using pip or the OS package manager. @see http://api.mongodb.org/python/current/installation.html +requirements: [ "pymongo" ] +author: "Loic Blot (@nerzhul)" +''' + +EXAMPLES = ''' +# Set MongoDB syncdelay to 60 (this is an int) +- mongodb_parameter: param="syncdelay" value=60 param_type="int" +''' + +RETURN = ''' +before: + description: value before modification + returned: success + type: string +after: + description: value after modification + returned: success + type: string +''' + +import ConfigParser + +try: + from pymongo.errors import ConnectionFailure + from pymongo.errors import OperationFailure + from pymongo import version as PyMongoVersion + from pymongo import MongoClient +except ImportError: + try: # for older PyMongo 2.2 + from pymongo import Connection as MongoClient + except ImportError: + pymongo_found = False + else: + pymongo_found = True +else: + pymongo_found = True + + +# ========================================= +# MongoDB module specific support methods. +# + +def load_mongocnf(): + config = ConfigParser.RawConfigParser() + mongocnf = os.path.expanduser('~/.mongodb.cnf') + + try: + config.readfp(open(mongocnf)) + creds = dict( + user=config.get('client', 'user'), + password=config.get('client', 'pass') + ) + except (ConfigParser.NoOptionError, IOError): + return False + + return creds + + +# ========================================= +# Module execution. +# + +def main(): + module = AnsibleModule( + argument_spec=dict( + login_user=dict(default=None), + login_password=dict(default=None, no_log=True), + login_host=dict(default='localhost'), + login_port=dict(default=27017, type='int'), + login_database=dict(default=None), + replica_set=dict(default=None), + param=dict(default=None, required=True), + value=dict(default=None, required=True), + param_type=dict(default="str", choices=['str', 'int']), + ssl=dict(default=False, type='bool'), + ) + ) + + if not pymongo_found: + module.fail_json(msg='the python pymongo module is required') + + login_user = module.params['login_user'] + login_password = module.params['login_password'] + login_host = module.params['login_host'] + login_port = module.params['login_port'] + login_database = module.params['login_database'] + + replica_set = module.params['replica_set'] + ssl = module.params['ssl'] + + param = module.params['param'] + param_type = module.params['param_type'] + value = module.params['value'] + + # Verify parameter is coherent with specified type + try: + if param_type == 'int': + value = int(value) + except ValueError, e: + module.fail_json(msg="value '%s' is not %s" % (value, param_type)) + + try: + if replica_set: + client = MongoClient(login_host, int(login_port), replicaset=replica_set, ssl=ssl) + else: + client = MongoClient(login_host, int(login_port), ssl=ssl) + + if login_user is None and login_password is None: + mongocnf_creds = load_mongocnf() + if mongocnf_creds is not False: + login_user = mongocnf_creds['user'] + login_password = mongocnf_creds['password'] + 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') + + if login_user is not None and login_password is not None: + client.admin.authenticate(login_user, login_password, source=login_database) + + except ConnectionFailure, e: + module.fail_json(msg='unable to connect to database: %s' % str(e)) + + db = client.admin + + try: + after_value = db.command("setParameter", **{param: int(value)}) + except OperationFailure, e: + module.fail_json(msg="unable to change parameter: %s" % str(e)) + + if "was" not in after_value: + module.exit_json(changed=True, msg="Unable to determine old value, assume it changed.") + else: + module.exit_json(changed=(value != after_value["was"]), before=after_value["was"], + after=value) + + +# import module snippets +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From 08f5a3b6d0e0bbabae278418d02974d446ee4c50 Mon Sep 17 00:00:00 2001 From: Sun JianKang Date: Mon, 25 Apr 2016 15:58:07 +0800 Subject: [PATCH 1470/2522] add service address when register service (#1299) --- clustering/consul.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/clustering/consul.py b/clustering/consul.py index f0df2368b51..4d05ebc4acc 100644 --- a/clustering/consul.py +++ b/clustering/consul.py @@ -93,6 +93,11 @@ - the port on which the service is listening required for registration of a service, i.e. if service_name or service_id is set required: false + service_address: + description: + - the address on which the service is serving required for + registration of a service + required: false tags: description: - a list of tags that will be attached to the service registration. @@ -178,6 +183,12 @@ interval: 60s http: /status + - name: register nginx with address + consul: + service_name: nginx + service_port: 80 + service_address: 127.0.0.1 + - name: register nginx with some service tags consul: service_name: nginx @@ -360,6 +371,7 @@ def parse_service(module): return ConsulService( module.params.get('service_id'), module.params.get('service_name'), + module.params.get('service_address'), module.params.get('service_port'), module.params.get('tags'), ) @@ -368,13 +380,14 @@ def parse_service(module): module.fail_json( msg="service_name supplied but no service_port, a port is required to configure a service. Did you configure the 'port' argument meaning 'service_port'?") -class ConsulService(): +class ConsulService(): - def __init__(self, service_id=None, name=None, port=-1, + def __init__(self, service_id=None, name=None, address=None, port=-1, tags=None, loaded=None): self.id = self.name = name if service_id: self.id = service_id + self.address = address self.port = port self.tags = tags self.checks = [] @@ -391,6 +404,7 @@ def register(self, consul_api): consul_api.agent.service.register( self.name, service_id=self.id, + address=self.address, port=self.port, tags=self.tags, check=check.check) @@ -398,6 +412,7 @@ def register(self, consul_api): consul_api.agent.service.register( self.name, service_id=self.id, + address=self.address, port=self.port, tags=self.tags) @@ -527,6 +542,7 @@ def main(): script=dict(required=False), service_id=dict(required=False), service_name=dict(required=False), + service_address=dict(required=False, type='str', default='localhost'), service_port=dict(required=False, type='int'), state=dict(default='present', choices=['present', 'absent']), interval=dict(required=False, type='str'), From 3feb69f614c96d16532e26ce0cd76525f0f1565b Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Mon, 25 Apr 2016 10:09:15 +0200 Subject: [PATCH 1471/2522] consul: add docs, fix typos, minor style fix --- clustering/consul.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/clustering/consul.py b/clustering/consul.py index 4d05ebc4acc..e96e7524aeb 100644 --- a/clustering/consul.py +++ b/clustering/consul.py @@ -98,6 +98,8 @@ - the address on which the service is serving required for registration of a service required: false + default: localhost + version_added: "2.1" tags: description: - a list of tags that will be attached to the service registration. @@ -188,7 +190,7 @@ service_name: nginx service_port: 80 service_address: 127.0.0.1 - + - name: register nginx with some service tags consul: service_name: nginx @@ -211,8 +213,6 @@ ''' -import sys - try: import consul from requests.exceptions import ConnectionError @@ -298,8 +298,8 @@ def add_service(module, service): consul_api = get_consul_api(module) existing = get_service_by_id(consul_api, service.id) - # there is no way to retreive the details of checks so if a check is present - # in the service it must be reregistered + # there is no way to retrieve the details of checks so if a check is present + # in the service it must be re-registered if service.has_checks() or not existing or not existing == service: service.register(consul_api) @@ -472,7 +472,7 @@ def __init__(self, check_id, name, node=None, host='localhost', self.check = consul.Check.ttl(self.ttl) if http: - if interval == None: + if interval is None: raise Exception('http check must specify interval') self.check = consul.Check.http(http, self.interval, self.timeout) @@ -517,7 +517,7 @@ def to_dict(self): def _add(self, data, key, attr=None): try: - if attr == None: + if attr is None: attr = key data[key] = getattr(self, attr) except: From e4a4259bc2a75428769062b4a2c93b2f97b62039 Mon Sep 17 00:00:00 2001 From: Joshua Gilman Date: Mon, 25 Apr 2016 04:18:24 -0700 Subject: [PATCH 1472/2522] Change disk type to str to allow correct usage of rootfs for LXC (#2091) --- cloud/misc/proxmox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/misc/proxmox.py b/cloud/misc/proxmox.py index 9647db4bf8e..a0d2b8bbcd5 100644 --- a/cloud/misc/proxmox.py +++ b/cloud/misc/proxmox.py @@ -290,7 +290,7 @@ def main(): password = dict(no_log=True), hostname = dict(), ostemplate = dict(), - disk = dict(type='int', default=3), + disk = dict(type='str', default='3'), cpus = dict(type='int', default=1), memory = dict(type='int', default=512), swap = dict(type='int', default=0), From 142742964e696d96b57c5bbce618d976c6d9a79d Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Mon, 25 Apr 2016 08:56:04 -0500 Subject: [PATCH 1473/2522] Fix homebrew_cask examples --- packaging/os/homebrew_cask.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packaging/os/homebrew_cask.py b/packaging/os/homebrew_cask.py index d61719a5494..7ebe84300c0 100755 --- a/packaging/os/homebrew_cask.py +++ b/packaging/os/homebrew_cask.py @@ -50,8 +50,8 @@ - homebrew_cask: name=alfred state=present - homebrew_cask: name=alfred state=absent - homebrew_cask: name=alfred state=present install_options="appdir=/Applications" -- homebrew_cask: name=alfred state=present install_options="--debug appdir=/Applications" -- homebrew_cask: name=alfred state=absent install_options="--force" +- homebrew_cask: name=alfred state=present install_options="debug,appdir=/Applications" +- homebrew_cask: name=alfred state=absent install_options="force" ''' import os.path From 86f08bfcda17e95fb284f9a8b7c0a3cbf518173b Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Mon, 25 Apr 2016 12:34:18 -0700 Subject: [PATCH 1474/2522] Adds the __main__ conditional check (#2098) As is done in other ansible modules, this adds the __main__ check to the module so that the module code itself can be used as a library. For instance, when testing the code. --- network/f5/bigip_facts.py | 1 - network/f5/bigip_monitor_http.py | 3 ++- network/f5/bigip_monitor_tcp.py | 3 ++- network/f5/bigip_node.py | 3 ++- network/f5/bigip_pool.py | 4 +++- network/f5/bigip_pool_member.py | 4 +++- network/f5/bigip_virtual_server.py | 1 - 7 files changed, 12 insertions(+), 7 deletions(-) diff --git a/network/f5/bigip_facts.py b/network/f5/bigip_facts.py index 9a2f5abdbf4..0f121b2a3aa 100644 --- a/network/f5/bigip_facts.py +++ b/network/f5/bigip_facts.py @@ -1687,4 +1687,3 @@ def main(): if __name__ == '__main__': main() - diff --git a/network/f5/bigip_monitor_http.py b/network/f5/bigip_monitor_http.py index cfafbad9be1..166b7ae32e0 100644 --- a/network/f5/bigip_monitor_http.py +++ b/network/f5/bigip_monitor_http.py @@ -445,5 +445,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.f5 import * -main() +if __name__ == '__main__': + main() diff --git a/network/f5/bigip_monitor_tcp.py b/network/f5/bigip_monitor_tcp.py index 564f4564283..b06c8978106 100644 --- a/network/f5/bigip_monitor_tcp.py +++ b/network/f5/bigip_monitor_tcp.py @@ -469,5 +469,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.f5 import * -main() +if __name__ == '__main__': + main() diff --git a/network/f5/bigip_node.py b/network/f5/bigip_node.py index 89c17e57735..c6c32a38858 100644 --- a/network/f5/bigip_node.py +++ b/network/f5/bigip_node.py @@ -379,5 +379,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.f5 import * -main() +if __name__ == '__main__': + main() diff --git a/network/f5/bigip_pool.py b/network/f5/bigip_pool.py index 014545c7c3b..85fbc5b9c0d 100644 --- a/network/f5/bigip_pool.py +++ b/network/f5/bigip_pool.py @@ -521,4 +521,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.f5 import * -main() + +if __name__ == '__main__': + main() diff --git a/network/f5/bigip_pool_member.py b/network/f5/bigip_pool_member.py index a0a1172801e..50abef420e7 100644 --- a/network/f5/bigip_pool_member.py +++ b/network/f5/bigip_pool_member.py @@ -438,4 +438,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.f5 import * -main() + +if __name__ == '__main__': + main() diff --git a/network/f5/bigip_virtual_server.py b/network/f5/bigip_virtual_server.py index 26772f4bca7..b654e5f84f8 100644 --- a/network/f5/bigip_virtual_server.py +++ b/network/f5/bigip_virtual_server.py @@ -469,4 +469,3 @@ def main(): if __name__ == '__main__': main() - From 99f5e394add7f7525fd3cae8cff53ca86ebaaa2e Mon Sep 17 00:00:00 2001 From: chouseknecht Date: Tue, 26 Apr 2016 01:32:09 -0400 Subject: [PATCH 1475/2522] Rename azure_deploy to azure_rm_deployment. Refactor to use azure_common. --- cloud/azure/azure_deployment.py | 620 --------------------------- cloud/azure/azure_rm_deployment.py | 646 +++++++++++++++++++++++++++++ 2 files changed, 646 insertions(+), 620 deletions(-) delete mode 100644 cloud/azure/azure_deployment.py create mode 100644 cloud/azure/azure_rm_deployment.py diff --git a/cloud/azure/azure_deployment.py b/cloud/azure/azure_deployment.py deleted file mode 100644 index e28663b55f6..00000000000 --- a/cloud/azure/azure_deployment.py +++ /dev/null @@ -1,620 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . -DOCUMENTATION = ''' ---- -module: azure_deployment -short_description: Create or destroy Azure Resource Manager template deployments -version_added: "2.1" -description: - - Create or destroy Azure Resource Manager template deployments via the Azure SDK for Python. - You can find some quick start templates in GitHub here https://github.com/azure/azure-quickstart-templates. - If you would like to find out more information about Azure Resource Manager templates, see https://azure.microsoft.com/en-us/documentation/articles/resource-group-template-deploy/. -options: - subscription_id: - description: - - The Azure subscription to deploy the template into. - required: true - resource_group_name: - description: - - The resource group name to use or create to host the deployed template - required: true - state: - description: - - If state is "present", template will be created. If state is "present" and if deployment exists, it will be updated. - If state is "absent", stack will be removed. - required: true - template: - description: - - A hash containg the templates inline. This parameter is mutually exclusive with 'template_link'. - Either one of them is required if "state" parameter is "present". - required: false - default: None - template_link: - description: - - Uri of file containing the template body. This parameter is mutually exclusive with 'template'. Either one - of them is required if "state" parameter is "present". - required: false - default: None - parameters: - description: - - A hash of all the required template variables for the deployment template. This parameter is mutually exclusive with 'parameters_link'. - Either one of them is required if "state" parameter is "present". - required: false - default: None - parameters_link: - description: - - Uri of file containing the parameters body. This parameter is mutually exclusive with 'parameters'. Either - one of them is required if "state" parameter is "present". - required: false - default: None - location: - description: - - The geo-locations in which the resource group will be located. - require: false - default: West US - -author: "David Justice (@devigned) / Laurent Mazuel (@lmazuel) / Andre Price (@obsoleted)" -''' - -EXAMPLES = ''' -# Destroy a template deployment -- name: Destroy Azure Deploy - azure_deploy: - state: absent - subscription_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - resource_group_name: dev-ops-cle - -# Create or update a template deployment based on uris to paramters and a template -- name: Create Azure Deploy - azure_deploy: - state: present - subscription_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - resource_group_name: dev-ops-cle - parameters_link: 'https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-simple-linux-vm/azuredeploy.parameters.json' - template_link: 'https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-simple-linux-vm/azuredeploy.json' - -# Create or update a template deployment based on a uri to the template and parameters specified inline. -# This deploys a VM with SSH support for a given public key, then stores the result in 'azure_vms'. The result is then used -# to create a new host group. This host group is then used to wait for each instance to respond to the public IP SSH. ---- -- hosts: localhost - tasks: - - name: Destroy Azure Deploy - azure_deployment: - state: absent - subscription_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - resource_group_name: dev-ops-cle - - - name: Create Azure Deploy - azure_deployment: - state: present - subscription_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - resource_group_name: dev-ops-cle - parameters: - newStorageAccountName: - value: devopsclestorage1 - adminUsername: - value: devopscle - dnsNameForPublicIP: - value: devopscleazure - location: - value: West US - vmSize: - value: Standard_A2 - vmName: - value: ansibleSshVm - sshKeyData: - value: YOUR_SSH_PUBLIC_KEY - template_link: 'https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-vm-sshkey/azuredeploy.json' - register: azure - - name: Add new instance to host group - add_host: hostname={{ item['ips'][0].public_ip }} groupname=azure_vms - with_items: azure.instances - -- hosts: azure_vms - user: devopscle - tasks: - - name: Wait for SSH to come up - wait_for: port=22 timeout=2000 state=started - - name: echo the hostname of the vm - shell: hostname - -# Deploy an Azure WebApp running a hello world'ish node app -- name: Create Azure WebApp Deployment at http://devopscleweb.azurewebsites.net/hello.js - azure_deployment: - state: present - subscription_id: cbbdaed0-fea9-4693-bf0c-d446ac93c030 - resource_group_name: dev-ops-cle-webapp - parameters: - repoURL: - value: 'https://github.com/devigned/az-roadshow-oss.git' - siteName: - value: devopscleweb - hostingPlanName: - value: someplan - siteLocation: - value: westus - sku: - value: Standard - template_link: 'https://raw.githubusercontent.com/azure/azure-quickstart-templates/master/201-web-app-github-deploy/azuredeploy.json' - -# Create or update a template deployment based on an inline template and parameters -- name: Create Azure Deploy - azure_deploy: - state: present - subscription_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - resource_group_name: dev-ops-cle - - template: - $schema: "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#" - contentVersion: "1.0.0.0" - parameters: - newStorageAccountName: - type: "string" - metadata: - description: "Unique DNS Name for the Storage Account where the Virtual Machine's disks will be placed." - adminUsername: - type: "string" - metadata: - description: "User name for the Virtual Machine." - adminPassword: - type: "securestring" - metadata: - description: "Password for the Virtual Machine." - dnsNameForPublicIP: - type: "string" - metadata: - description: "Unique DNS Name for the Public IP used to access the Virtual Machine." - ubuntuOSVersion: - type: "string" - defaultValue: "14.04.2-LTS" - allowedValues: - - "12.04.5-LTS" - - "14.04.2-LTS" - - "15.04" - metadata: - description: "The Ubuntu version for the VM. This will pick a fully patched image of this given Ubuntu version. Allowed values: 12.04.5-LTS, 14.04.2-LTS, 15.04." - variables: - location: "West US" - imagePublisher: "Canonical" - imageOffer: "UbuntuServer" - OSDiskName: "osdiskforlinuxsimple" - nicName: "myVMNic" - addressPrefix: "10.0.0.0/16" - subnetName: "Subnet" - subnetPrefix: "10.0.0.0/24" - storageAccountType: "Standard_LRS" - publicIPAddressName: "myPublicIP" - publicIPAddressType: "Dynamic" - vmStorageAccountContainerName: "vhds" - vmName: "MyUbuntuVM" - vmSize: "Standard_D1" - virtualNetworkName: "MyVNET" - vnetID: "[resourceId('Microsoft.Network/virtualNetworks',variables('virtualNetworkName'))]" - subnetRef: "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]" - resources: - - - type: "Microsoft.Storage/storageAccounts" - name: "[parameters('newStorageAccountName')]" - apiVersion: "2015-05-01-preview" - location: "[variables('location')]" - properties: - accountType: "[variables('storageAccountType')]" - - - apiVersion: "2015-05-01-preview" - type: "Microsoft.Network/publicIPAddresses" - name: "[variables('publicIPAddressName')]" - location: "[variables('location')]" - properties: - publicIPAllocationMethod: "[variables('publicIPAddressType')]" - dnsSettings: - domainNameLabel: "[parameters('dnsNameForPublicIP')]" - - - type: "Microsoft.Network/virtualNetworks" - apiVersion: "2015-05-01-preview" - name: "[variables('virtualNetworkName')]" - location: "[variables('location')]" - properties: - addressSpace: - addressPrefixes: - - "[variables('addressPrefix')]" - subnets: - - - name: "[variables('subnetName')]" - properties: - addressPrefix: "[variables('subnetPrefix')]" - - - type: "Microsoft.Network/networkInterfaces" - apiVersion: "2015-05-01-preview" - name: "[variables('nicName')]" - location: "[variables('location')]" - dependsOn: - - "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]" - - "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]" - properties: - ipConfigurations: - - - name: "ipconfig1" - properties: - privateIPAllocationMethod: "Dynamic" - publicIPAddress: - id: "[resourceId('Microsoft.Network/publicIPAddresses',variables('publicIPAddressName'))]" - subnet: - id: "[variables('subnetRef')]" - - - type: "Microsoft.Compute/virtualMachines" - apiVersion: "2015-06-15" - name: "[variables('vmName')]" - location: "[variables('location')]" - dependsOn: - - "[concat('Microsoft.Storage/storageAccounts/', parameters('newStorageAccountName'))]" - - "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]" - properties: - hardwareProfile: - vmSize: "[variables('vmSize')]" - osProfile: - computername: "[variables('vmName')]" - adminUsername: "[parameters('adminUsername')]" - adminPassword: "[parameters('adminPassword')]" - storageProfile: - imageReference: - publisher: "[variables('imagePublisher')]" - offer: "[variables('imageOffer')]" - sku: "[parameters('ubuntuOSVersion')]" - version: "latest" - osDisk: - name: "osdisk" - vhd: - uri: "[concat('http://',parameters('newStorageAccountName'),'.blob.core.windows.net/',variables('vmStorageAccountContainerName'),'/',variables('OSDiskName'),'.vhd')]" - caching: "ReadWrite" - createOption: "FromImage" - networkProfile: - networkInterfaces: - - - id: "[resourceId('Microsoft.Network/networkInterfaces',variables('nicName'))]" - diagnosticsProfile: - bootDiagnostics: - enabled: "true" - storageUri: "[concat('http://',parameters('newStorageAccountName'),'.blob.core.windows.net')]" - parameters: - newStorageAccountName: - value: devopsclestorage - adminUsername: - value: devopscle - adminPassword: - value: Password1! - dnsNameForPublicIP: - value: devopscleazure -''' - -RETURN = ''' -''' - -try: - import time - import yaml - from itertools import chain - from azure.common.credentials import ServicePrincipalCredentials - from azure.common.exceptions import CloudError - from azure.mgmt.resource.resources.models import ( - DeploymentProperties, - ParametersLink, - TemplateLink, - Deployment, - ResourceGroup, - Dependency - ) - from azure.mgmt.resource.resources import ResourceManagementClient, ResourceManagementClientConfiguration - from azure.mgmt.network import NetworkManagementClient, NetworkManagementClientConfiguration - - HAS_DEPS = True -except ImportError: - HAS_DEPS = False - -AZURE_URL = "https://management.azure.com" - - -def get_azure_connection_info(module): - azure_url = module.params.get('azure_url') - tenant_id = module.params.get('tenant_id') - client_id = module.params.get('client_id') - client_secret = module.params.get('client_secret') - resource_group_name = module.params.get('resource_group_name') - subscription_id = module.params.get('subscription_id') - - if not azure_url: - if 'AZURE_URL' in os.environ: - azure_url = os.environ['AZURE_URL'] - else: - azure_url = None - - if not subscription_id: - if 'AZURE_SUBSCRIPTION_ID' in os.environ: - subscription_id = os.environ['AZURE_SUBSCRIPTION_ID'] - else: - subscription_id = None - - if not resource_group_name: - if 'AZURE_RESOURCE_GROUP_NAME' in os.environ: - resource_group_name = os.environ['AZURE_RESOURCE_GROUP_NAME'] - else: - resource_group_name = None - - if not tenant_id: - if 'AZURE_TENANT_ID' in os.environ: - tenant_id = os.environ['AZURE_TENANT_ID'] - elif 'AZURE_DOMAIN' in os.environ: - tenant_id = os.environ['AZURE_DOMAIN'] - else: - tenant_id = None - - if not client_id: - if 'AZURE_CLIENT_ID' in os.environ: - client_id = os.environ['AZURE_CLIENT_ID'] - else: - client_id = None - - if not client_secret: - if 'AZURE_CLIENT_SECRET' in os.environ: - client_secret = os.environ['AZURE_CLIENT_SECRET'] - else: - client_secret = None - - return dict(azure_url=azure_url, - tenant_id=tenant_id, - client_id=client_id, - client_secret=client_secret, - resource_group_name=resource_group_name, - subscription_id=subscription_id) - - -def build_deployment_body(module): - """ - Build the deployment body from the module parameters - :param module: Ansible module containing the validated configuration for the deployment template - :return: body as dict - """ - properties = dict(mode='Incremental') - properties['templateLink'] = \ - dict(uri=module.params.get('template_link'), - contentVersion=module.params.get('content_version')) - - properties['parametersLink'] = \ - dict(uri=module.params.get('parameters_link'), - contentVersion=module.params.get('content_version')) - - return dict(properties=properties) - -def get_failed_nested_operations(client, resource_group, current_operations): - new_operations = [] - for operation in current_operations: - if operation.properties.provisioning_state == 'Failed': - new_operations.append(operation) - if operation.properties.target_resource and 'Microsoft.Resources/deployments' in operation.properties.target_resource.id: - nested_deployment = operation.properties.target_resource.resource_name - nested_operations = client.deployment_operations.list(resource_group, nested_deployment) - new_nested_operations = get_failed_nested_operations(client, resource_group, nested_operations) - new_operations += new_nested_operations - - return new_operations - -def get_failed_deployment_operations(module, client, resource_group, deployment_name): - operations = client.deployment_operations.list(resource_group, deployment_name) - return [ - dict( - id=op.id, - operation_id=op.operation_id, - status_code=op.properties.status_code, - status_message=op.properties.status_message, - target_resource = dict( - id=op.properties.target_resource.id, - resource_name=op.properties.target_resource.resource_name, - resource_type=op.properties.target_resource.resource_type - ) if op.properties.target_resource else None, - provisioning_state=op.properties.provisioning_state, - ) - for op in get_failed_nested_operations(client, resource_group, operations) - ] - -def deploy_template(module, client, conn_info): - """ - Deploy the targeted template and parameters - :param module: Ansible module containing the validated configuration for the deployment template - :param client: resource management client for azure - :param conn_info: connection info needed - :return: - """ - - deployment_name = conn_info["deployment_name"] - group_name = conn_info["resource_group_name"] - - deploy_parameter = DeploymentProperties() - deploy_parameter.mode = module.params.get('deployment_mode') - - if module.params.get('parameters_link') is None: - deploy_parameter.parameters = module.params.get('parameters') - else: - parameters_link = ParametersLink( - uri = module.params.get('parameters_link') - ) - deploy_parameter.parameters_link = parameters_link - - if module.params.get('template_link') is None: - deploy_parameter.template = module.params.get('template') - else: - template_link = TemplateLink( - uri = module.params.get('template_link') - ) - deploy_parameter.template_link = template_link - - params = ResourceGroup(location=module.params.get('location'), tags=module.params.get('tags')) - try: - client.resource_groups.create_or_update(group_name, params) - result = client.deployments.create_or_update(group_name, deployment_name, deploy_parameter) - deployment_result = result.result() # Blocking wait, return the Deployment object - if module.params.get('wait_for_deployment_completion'): - while not deployment_result.properties.provisioning_state in ['Canceled', 'Failed', 'Deleted', 'Succeeded']: - deployment_result = client.deployments.get(group_name, deployment_name) - time.sleep(module.params.get('wait_for_deployment_polling_period')) - - if deployment_result.properties.provisioning_state == 'Succeeded': - return deployment_result - - failed_deployment_operations = get_failed_deployment_operations(module, client, group_name, deployment_name) - module.fail_json(msg='Deployment failed. Deployment id: %s' % (deployment_result.id), failed_deployment_operations=failed_deployment_operations) - except CloudError as e: - failed_deployment_operations = get_failed_deployment_operations(module, client, group_name, deployment_name) - module.fail_json(msg='Deploy create failed with status code: %s and message: "%s"' % (e.status_code, e.message),failed_deployment_operations=failed_deployment_operations) - -def destroy_resource_group(module, client, conn_info): - """ - Destroy the targeted resource group - :param module: ansible module - :param client: resource management client for azure - :param conn_info: connection info needed - :return: if the result caused a change in the deployment - """ - - try: - result = client.resource_groups.delete(conn_info['resource_group_name']) - result.wait() # Blocking wait till the delete is finished - except CloudError as e: - if e.status_code == 404 or e.status_code == 204: - return True - else: - module.fail_json( - msg='Delete resource group and deploy failed with status code: %s and message: %s' % (e.status_code, e.message)) - - -def get_dependencies(dep_tree, resource_type): - matches = [value for value in dep_tree.values() if value['dep'].resource_type == resource_type] - for child_tree in [value['children'] for value in dep_tree.values()]: - matches += get_dependencies(child_tree, resource_type) - return matches - - -def build_hierarchy(dependencies, tree=None): - tree = dict(top=True) if tree is None else tree - for dep in dependencies: - if dep.resource_name not in tree: - tree[dep.resource_name] = dict(dep=dep, children=dict()) - if isinstance(dep, Dependency) and dep.depends_on is not None and len(dep.depends_on) > 0: - build_hierarchy(dep.depends_on, tree[dep.resource_name]['children']) - - if 'top' in tree: - tree.pop('top', None) - keys = list(tree.keys()) - for key1 in keys: - for key2 in keys: - if key2 in tree and key1 in tree[key2]['children'] and key1 in tree: - tree[key2]['children'][key1] = tree[key1] - tree.pop(key1) - return tree - - -def get_ip_dict(ip): - ip_dict = dict(name=ip.name, - id=ip.id, - public_ip=ip.ip_address, - public_ip_allocation_method=str(ip.public_ip_allocation_method)) - - if ip.dns_settings: - ip_dict['dns_settings'] = { - 'domain_name_label':ip.dns_settings.domain_name_label, - 'fqdn':ip.dns_settings.fqdn - } - - return ip_dict - - -def nic_to_public_ips_instance(client, group, nics): - return [client.public_ip_addresses.get(group, public_ip_id.split('/')[-1]) - for nic_obj in [client.network_interfaces.get(group, nic['dep'].resource_name) for nic in nics] - for public_ip_id in [ip_conf_instance.public_ip_address.id for ip_conf_instance in nic_obj.ip_configurations if ip_conf_instance.public_ip_address]] - - -def get_instances(client, group, deployment): - dep_tree = build_hierarchy(deployment.properties.dependencies) - vms = get_dependencies(dep_tree, resource_type="Microsoft.Compute/virtualMachines") - - vms_and_nics = [(vm, get_dependencies(vm['children'], "Microsoft.Network/networkInterfaces")) for vm in vms] - vms_and_ips = [(vm['dep'], nic_to_public_ips_instance(client, group, nics)) for vm, nics in vms_and_nics] - - return [dict(vm_name=vm.resource_name, ips=[get_ip_dict(ip) for ip in ips]) for vm, ips in vms_and_ips if len(ips) > 0] - - -def main(): - argument_spec = dict( - azure_url=dict(default=AZURE_URL), - subscription_id=dict(), - client_secret=dict(no_log=True), - client_id=dict(required=True), - tenant_id=dict(required=True), - resource_group_name=dict(required=True), - state=dict(default='present', choices=['present', 'absent']), - template=dict(default=None, type='dict'), - parameters=dict(default=None, type='dict'), - template_link=dict(default=None), - parameters_link=dict(default=None), - location=dict(default="West US"), - deployment_mode=dict(default='Complete', choices=['Complete', 'Incremental']), - deployment_name=dict(default="ansible-arm"), - wait_for_deployment_completion=dict(default=True), - wait_for_deployment_polling_period=dict(default=30) - ) - - module = AnsibleModule( - argument_spec=argument_spec, - mutually_exclusive=[['template_link', 'template'], ['parameters_link', 'parameters']], - ) - - if not HAS_DEPS: - module.fail_json(msg='requests and azure are required for this module') - - conn_info = get_azure_connection_info(module) - - credentials = ServicePrincipalCredentials(client_id=conn_info['client_id'], - secret=conn_info['client_secret'], - tenant=conn_info['tenant_id']) - - subscription_id = conn_info['subscription_id'] - resource_configuration = ResourceManagementClientConfiguration(credentials, subscription_id) - resource_configuration.add_user_agent('Ansible-Deploy') - resource_client = ResourceManagementClient(resource_configuration) - network_configuration = NetworkManagementClientConfiguration(credentials, subscription_id) - network_configuration.add_user_agent('Ansible-Deploy') - network_client = NetworkManagementClient(network_configuration) - conn_info['deployment_name'] = module.params.get('deployment_name') - - if module.params.get('state') == 'present': - deployment = deploy_template(module, resource_client, conn_info) - data = dict(name=deployment.name, - group_name=conn_info['resource_group_name'], - id=deployment.id, - outputs=deployment.properties.outputs, - instances=get_instances(network_client, conn_info['resource_group_name'], deployment), - changed=True, - msg='deployment created') - module.exit_json(**data) - else: - destroy_resource_group(module, resource_client, conn_info) - module.exit_json(changed=True, msg='deployment deleted') - -# import module snippets -from ansible.module_utils.basic import * - -if __name__ == '__main__': - main() diff --git a/cloud/azure/azure_rm_deployment.py b/cloud/azure/azure_rm_deployment.py new file mode 100644 index 00000000000..022c240a92c --- /dev/null +++ b/cloud/azure/azure_rm_deployment.py @@ -0,0 +1,646 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +DOCUMENTATION = ''' +--- +module: azure_rm_deployment + +short_description: Create or destroy Azure Resource Manager template deployments + +version_added: "2.1" + +description: + - "Create or destroy Azure Resource Manager template deployments via the Azure SDK for Python. + You can find some quick start templates in GitHub here https://github.com/azure/azure-quickstart-templates. + For more information on Azue resource manager templates see https://azure.microsoft.com/en-us/documentation/articles/resource-group-template-deploy/." + +options: + resource_group_name: + description: + - The resource group name to use or create to host the deployed template + required: true + location: + description: + - The geo-locations in which the resource group will be located. + required: false + default: westus + state: + description: + - If state is "present", template will be created. If state is "present" and if deployment exists, it will be + updated. If state is "absent", stack will be removed. + default: present + choices: + - present + - absent + template: + description: + - A hash containing the templates inline. This parameter is mutually exclusive with 'template_link'. + Either one of them is required if "state" parameter is "present". + required: false + default: None + template_link: + description: + - Uri of file containing the template body. This parameter is mutually exclusive with 'template'. Either one + of them is required if "state" parameter is "present". + required: false + default: None + parameters: + description: + - A hash of all the required template variables for the deployment template. This parameter is mutually exclusive + with 'parameters_link'. Either one of them is required if "state" parameter is "present". + required: false + default: None + parameters_link: + description: + - Uri of file containing the parameters body. This parameter is mutually exclusive with 'parameters'. Either + one of them is required if "state" parameter is "present". + required: false + default: None + +extends_documentation_fragment: + - azure + +author: + - David Justice (@devigned) + - Laurent Mazuel (@lmazuel) + - Andre Price (@obsoleted) + +''' + +EXAMPLES = ''' +# Destroy a template deployment +- name: Destroy Azure Deploy + azure_rm_deployment: + state: absent + subscription_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + resource_group_name: dev-ops-cle + +# Create or update a template deployment based on uris using parameter and template links +- name: Create Azure Deploy + azure_rm_deployment: + state: present + resource_group_name: dev-ops-cle + template_link: 'https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-vm-simple-linux/azuredeploy.json' + parameters_link: 'https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-vm-simple-linux/azuredeploy.parameters.json' + +# Create or update a template deployment based on a uri to the template and parameters specified inline. +# This deploys a VM with SSH support for a given public key, then stores the result in 'azure_vms'. The result is then +# used to create a new host group. This host group is then used to wait for each instance to respond to the public IP SSH. +--- +- hosts: localhost + connection: local + gather_facts: no + tasks: + - name: Destroy Azure Deploy + azure_rm_deployment: + state: absent + subscription_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + resource_group_name: dev-ops-cle + + - name: Create Azure Deploy + azure_rm_deployment: + state: present + subscription_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + resource_group_name: dev-ops-cle + parameters: + newStorageAccountName: + value: devopsclestorage1 + adminUsername: + value: devopscle + dnsNameForPublicIP: + value: devopscleazure + location: + value: West US + vmSize: + value: Standard_A2 + vmName: + value: ansibleSshVm + sshKeyData: + value: YOUR_SSH_PUBLIC_KEY + template_link: 'https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-vm-sshkey/azuredeploy.json' + register: azure + + - name: Add new instance to host group + add_host: hostname={{ item['ips'][0].public_ip }} groupname=azure_vms + with_items: azure.deployment.instances + + - hosts: azure_vms + user: devopscle + tasks: + - name: Wait for SSH to come up + wait_for: port=22 timeout=2000 state=started + - name: echo the hostname of the vm + shell: hostname + +# Deploy an Azure WebApp running a hello world'ish node app +- name: Create Azure WebApp Deployment at http://devopscleweb.azurewebsites.net/hello.js + azure_rm_deployment: + state: present + subscription_id: cbbdaed0-fea9-4693-bf0c-d446ac93c030 + resource_group_name: dev-ops-cle-webapp + parameters: + repoURL: + value: 'https://github.com/devigned/az-roadshow-oss.git' + siteName: + value: devopscleweb + hostingPlanName: + value: someplan + siteLocation: + value: westus + sku: + value: Standard + template_link: 'https://raw.githubusercontent.com/azure/azure-quickstart-templates/master/201-web-app-github-deploy/azuredeploy.json' + +# Create or update a template deployment based on an inline template and parameters +- name: Create Azure Deploy + azure_rm_deploy: + state: present + subscription_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + resource_group_name: dev-ops-cle + + template: + $schema: "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#" + contentVersion: "1.0.0.0" + parameters: + newStorageAccountName: + type: "string" + metadata: + description: "Unique DNS Name for the Storage Account where the Virtual Machine's disks will be placed." + adminUsername: + type: "string" + metadata: + description: "User name for the Virtual Machine." + adminPassword: + type: "securestring" + metadata: + description: "Password for the Virtual Machine." + dnsNameForPublicIP: + type: "string" + metadata: + description: "Unique DNS Name for the Public IP used to access the Virtual Machine." + ubuntuOSVersion: + type: "string" + defaultValue: "14.04.2-LTS" + allowedValues: + - "12.04.5-LTS" + - "14.04.2-LTS" + - "15.04" + metadata: + description: "The Ubuntu version for the VM. This will pick a fully patched image of this given Ubuntu version. Allowed values: 12.04.5-LTS, 14.04.2-LTS, 15.04." + variables: + location: "West US" + imagePublisher: "Canonical" + imageOffer: "UbuntuServer" + OSDiskName: "osdiskforlinuxsimple" + nicName: "myVMNic" + addressPrefix: "10.0.0.0/16" + subnetName: "Subnet" + subnetPrefix: "10.0.0.0/24" + storageAccountType: "Standard_LRS" + publicIPAddressName: "myPublicIP" + publicIPAddressType: "Dynamic" + vmStorageAccountContainerName: "vhds" + vmName: "MyUbuntuVM" + vmSize: "Standard_D1" + virtualNetworkName: "MyVNET" + vnetID: "[resourceId('Microsoft.Network/virtualNetworks',variables('virtualNetworkName'))]" + subnetRef: "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]" + resources: + - + type: "Microsoft.Storage/storageAccounts" + name: "[parameters('newStorageAccountName')]" + apiVersion: "2015-05-01-preview" + location: "[variables('location')]" + properties: + accountType: "[variables('storageAccountType')]" + - + apiVersion: "2015-05-01-preview" + type: "Microsoft.Network/publicIPAddresses" + name: "[variables('publicIPAddressName')]" + location: "[variables('location')]" + properties: + publicIPAllocationMethod: "[variables('publicIPAddressType')]" + dnsSettings: + domainNameLabel: "[parameters('dnsNameForPublicIP')]" + - + type: "Microsoft.Network/virtualNetworks" + apiVersion: "2015-05-01-preview" + name: "[variables('virtualNetworkName')]" + location: "[variables('location')]" + properties: + addressSpace: + addressPrefixes: + - "[variables('addressPrefix')]" + subnets: + - + name: "[variables('subnetName')]" + properties: + addressPrefix: "[variables('subnetPrefix')]" + - + type: "Microsoft.Network/networkInterfaces" + apiVersion: "2015-05-01-preview" + name: "[variables('nicName')]" + location: "[variables('location')]" + dependsOn: + - "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]" + - "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]" + properties: + ipConfigurations: + - + name: "ipconfig1" + properties: + privateIPAllocationMethod: "Dynamic" + publicIPAddress: + id: "[resourceId('Microsoft.Network/publicIPAddresses',variables('publicIPAddressName'))]" + subnet: + id: "[variables('subnetRef')]" + - + type: "Microsoft.Compute/virtualMachines" + apiVersion: "2015-06-15" + name: "[variables('vmName')]" + location: "[variables('location')]" + dependsOn: + - "[concat('Microsoft.Storage/storageAccounts/', parameters('newStorageAccountName'))]" + - "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]" + properties: + hardwareProfile: + vmSize: "[variables('vmSize')]" + osProfile: + computername: "[variables('vmName')]" + adminUsername: "[parameters('adminUsername')]" + adminPassword: "[parameters('adminPassword')]" + storageProfile: + imageReference: + publisher: "[variables('imagePublisher')]" + offer: "[variables('imageOffer')]" + sku: "[parameters('ubuntuOSVersion')]" + version: "latest" + osDisk: + name: "osdisk" + vhd: + uri: "[concat('http://',parameters('newStorageAccountName'),'.blob.core.windows.net/',variables('vmStorageAccountContainerName'),'/',variables('OSDiskName'),'.vhd')]" + caching: "ReadWrite" + createOption: "FromImage" + networkProfile: + networkInterfaces: + - + id: "[resourceId('Microsoft.Network/networkInterfaces',variables('nicName'))]" + diagnosticsProfile: + bootDiagnostics: + enabled: "true" + storageUri: "[concat('http://',parameters('newStorageAccountName'),'.blob.core.windows.net')]" + parameters: + newStorageAccountName: + value: devopsclestorage + adminUsername: + value: devopscle + adminPassword: + value: Password1! + dnsNameForPublicIP: + value: devopscleazure +''' + +RETURN = ''' +msg: + description: String indicating if the deployment was created or deleted + returned: always + type: string + sample: "deployment created" +deployment: + description: Deployment details + type: dict + returned: always + sample:{ + "group_name": "Test_Deployment", + "id": "/subscriptions/3f7e29ba-24e0-42f6-8d9c-5149a14bda37/resourceGroups/Test_Deployment/providers/Microsoft.Resources/deployments/ansible-arm", + "instances": [ + { + "ips": [ + { + "dns_settings": { + "domain_name_label": "testvm9910001", + "fqdn": "testvm9910001.westus.cloudapp.azure.com" + }, + "id": "/subscriptions/3f7e29ba-24e0-42f6-8d9c-5149a14bda37/resourceGroups/Test_Deployment/providers/Microsoft.Network/publicIPAddresses/myPublicIP", + "name": "myPublicIP", + "public_ip": "13.91.99.232", + "public_ip_allocation_method": "IPAllocationMethod.dynamic" + } + ], + "vm_name": "MyUbuntuVM" + } + ], + "name": "ansible-arm", + "outputs": { + "hostname": { + "type": "String", + "value": "testvm9910001.westus.cloudapp.azure.com" + }, + "sshCommand": { + "type": "String", + "value": "ssh chouseknecht@testvm9910001.westus.cloudapp.azure.com" + } + } + } +''' + +import time +import yaml + +from ansible.module_utils.basic import * +from ansible.module_utils.azure_rm_common import * + +try: + from itertools import chain + from azure.common.credentials import ServicePrincipalCredentials + from azure.common.exceptions import CloudError + from azure.mgmt.resource.resources.models import (DeploymentProperties, + ParametersLink, + TemplateLink, + Deployment, + ResourceGroup, + Dependency) + from azure.mgmt.resource.resources import ResourceManagementClient, ResourceManagementClientConfiguration + from azure.mgmt.network import NetworkManagementClient, NetworkManagementClientConfiguration + +except ImportError: + # This is handled in azure_rm_common + pass + + +class AzureRMDeploymentManager(AzureRMModuleBase): + + def __init__(self): + + self.module_arg_spec = dict( + resource_group_name=dict(type='str', required=True, aliases=['resource_group']), + state=dict(type='str', default='present', choices=['present', 'absent']), + template=dict(type='dict', default=None), + parameters=dict(type='dict', default=None), + template_link=dict(type='str', default=None), + parameters_link=dict(type='str', default=None), + location=dict(type='str', default="westus"), + deployment_mode=dict(type='str', default='complete', choices=['complete', 'incremental']), + deployment_name=dict(type='str', default="ansible-arm"), + wait_for_deployment_completion=dict(type='bool', default=True), + wait_for_deployment_polling_period=dict(type='int', default=30) + ) + + mutually_exclusive = [('template', 'template_link'), + ('parameters', 'parameters_link')] + + self.resource_group_name = None + self.state = None + self.template = None + self.parameters = None + self.template_link = None + self.parameters_link = None + self.location = None + self.deployment_mode = None + self.deployment_name = None + self.wait_for_deployment_completion = None + self.wait_for_deployment_polling_period = None + self.tags = None + + self.results = dict( + deployment=dict(), + changed=False, + msg="" + ) + + super(AzureRMDeploymentManager, self).__init__(derived_arg_spec=self.module_arg_spec, + mutually_exclusive=mutually_exclusive, + supports_check_mode=False) + + def exec_module(self, **kwargs): + + for key in self.module_arg_spec.keys() + ['tags']: + setattr(self, key, kwargs[key]) + + if self.state == 'present': + deployment = self.deploy_template() + self.results['deployment'] = dict( + name=deployment.name, + group_name=self.resource_group_name, + id=deployment.id, + outputs=deployment.properties.outputs, + instances=self._get_instances(deployment) + ) + self.results['changed'] = True + self.results['msg'] = 'deployment created' + else: + if self.resource_group_exists(self.resource_group_name): + self.destroy_resource_group() + self.results['changed'] = True + self.results['msg'] = "deployment deleted" + + return self.results + + def deploy_template(self): + """ + Deploy the targeted template and parameters + :param module: Ansible module containing the validated configuration for the deployment template + :param client: resource management client for azure + :param conn_info: connection info needed + :return: + """ + + deploy_parameter = DeploymentProperties() + deploy_parameter.mode = self.deployment_mode + if not self.parameters_link: + deploy_parameter.parameters = self.parameters + else: + deploy_parameter.parameters_link = ParametersLink( + uri=self.parameters_link + ) + if not self.template_link: + deploy_parameter.template = self.template + else: + deploy_parameter.template_link = TemplateLink( + uri=self.template_link + ) + + params = ResourceGroup(location=self.location, tags=self.tags) + + try: + self.rm_client.resource_groups.create_or_update(self.resource_group_name, params) + except CloudError as exc: + self.fail("Resource group create_or_update failed with status code: %s and message: %s" % + (exc.status_code, exc.message)) + try: + result = self.rm_client.deployments.create_or_update(self.resource_group_name, + self.deployment_name, + deploy_parameter) + + deployment_result = self.get_poller_result(result) + if self.wait_for_deployment_completion: + while deployment_result.properties.provisioning_state not in ['Canceled', 'Failed', 'Deleted', + 'Succeeded']: + time.sleep(self.wait_for_deployment_polling_period) + deployment_result = self.rm_client.deployments.get(self.resource_group_name, self.deployment_name) + except CloudError as exc: + failed_deployment_operations = self._get_failed_deployment_operations(self.deployment_name) + self.log("Deployment failed %s: %s" % (exc.status_code, exc.message)) + self.fail("Deployment failed with status code: %s and message: %s" % (exc.status_code, exc.message), + failed_deployment_operations=failed_deployment_operations) + + if self.wait_for_deployment_completion and deployment_result.properties.provisioning_state != 'Succeeded': + self.log("provisioning state: %s" % deployment_result.properties.provisioning_state) + failed_deployment_operations = self._get_failed_deployment_operations(self.deployment_name) + self.fail('Deployment failed. Deployment id: %s' % deployment_result.id, + failed_deployment_operations=failed_deployment_operations) + + return deployment_result + + def destroy_resource_group(self): + """ + Destroy the targeted resource group + """ + try: + result = self.rm_client.resource_groups.delete(self.resource_group_name) + result.wait() # Blocking wait till the delete is finished + except CloudError as e: + if e.status_code == 404 or e.status_code == 204: + return + else: + self.fail("Delete resource group and deploy failed with status code: %s and message: %s" % + (e.status_code, e.message)) + + def resource_group_exists(self, resource_group): + ''' + Return True/False based on existence of requested resource group. + + :param resource_group: string. Name of a resource group. + :return: boolean + ''' + try: + self.rm_client.resource_groups.get(resource_group) + except CloudError: + return False + return True + + def _get_failed_nested_operations(self, current_operations): + new_operations = [] + for operation in current_operations: + if operation.properties.provisioning_state == 'Failed': + new_operations.append(operation) + if operation.properties.target_resource and \ + 'Microsoft.Resources/deployments' in operation.properties.target_resource.id: + nested_deployment = operation.properties.target_resource.resource_name + try: + nested_operations = self.rm_client.deployment_operations.list(self.resource_group_name, + nested_deployment) + except CloudError as exc: + self.fail("List nested deployment operations failed with status code: %s and message: %s" % + (e.status_code, e.message)) + new_nested_operations = self._get_failed_nested_operations(nested_operations) + new_operations += new_nested_operations + return new_operations + + def _get_failed_deployment_operations(self, deployment_name): + results = [] + # time.sleep(15) # there is a race condition between when we ask for deployment status and when the + # # status is available. + + try: + operations = self.rm_client.deployment_operations.list(self.resource_group_name, deployment_name) + except CloudError as exc: + self.fail("Get deployment failed with status code: %s and message: %s" % + (exc.status_code, exc.message)) + try: + results = [ + dict( + id=op.id, + operation_id=op.operation_id, + status_code=op.properties.status_code, + status_message=op.properties.status_message, + target_resource=dict( + id=op.properties.target_resource.id, + resource_name=op.properties.target_resource.resource_name, + resource_type=op.properties.target_resource.resource_type + ) if op.properties.target_resource else None, + provisioning_state=op.properties.provisioning_state, + ) + for op in self._get_failed_nested_operations(operations) + ] + except: + # If we fail here, the original error gets lost and user receives wrong error message/stacktrace + pass + self.log(dict(failed_deployment_operations=results), pretty_print=True) + return results + + def _get_instances(self, deployment): + dep_tree = self._build_hierarchy(deployment.properties.dependencies) + vms = self._get_dependencies(dep_tree, resource_type="Microsoft.Compute/virtualMachines") + vms_and_nics = [(vm, self._get_dependencies(vm['children'], "Microsoft.Network/networkInterfaces")) + for vm in vms] + vms_and_ips = [(vm['dep'], self._nic_to_public_ips_instance(nics)) + for vm, nics in vms_and_nics] + return [dict(vm_name=vm.resource_name, ips=[self._get_ip_dict(ip) + for ip in ips]) for vm, ips in vms_and_ips if len(ips) > 0] + + def _get_dependencies(self, dep_tree, resource_type): + matches = [value for value in dep_tree.values() if value['dep'].resource_type == resource_type] + for child_tree in [value['children'] for value in dep_tree.values()]: + matches += self._get_dependencies(child_tree, resource_type) + return matches + + def _build_hierarchy(self, dependencies, tree=None): + tree = dict(top=True) if tree is None else tree + for dep in dependencies: + if dep.resource_name not in tree: + tree[dep.resource_name] = dict(dep=dep, children=dict()) + if isinstance(dep, Dependency) and dep.depends_on is not None and len(dep.depends_on) > 0: + self._build_hierarchy(dep.depends_on, tree[dep.resource_name]['children']) + + if 'top' in tree: + tree.pop('top', None) + keys = list(tree.keys()) + for key1 in keys: + for key2 in keys: + if key2 in tree and key1 in tree[key2]['children'] and key1 in tree: + tree[key2]['children'][key1] = tree[key1] + tree.pop(key1) + return tree + + def _get_ip_dict(self, ip): + ip_dict = dict(name=ip.name, + id=ip.id, + public_ip=ip.ip_address, + public_ip_allocation_method=str(ip.public_ip_allocation_method) + ) + if ip.dns_settings: + ip_dict['dns_settings'] = { + 'domain_name_label':ip.dns_settings.domain_name_label, + 'fqdn':ip.dns_settings.fqdn + } + return ip_dict + + def _nic_to_public_ips_instance(self, nics): + return [self.network_client.public_ip_addresses.get(self.resource_group_name, public_ip_id.split('/')[-1]) + for nic_obj in [self.network_client.network_interfaces.get(self.resource_group_name, + nic['dep'].resource_name) for nic in nics] + for public_ip_id in [ip_conf_instance.public_ip_address.id + for ip_conf_instance in nic_obj.ip_configurations + if ip_conf_instance.public_ip_address]] + + +def main(): + AzureRMDeploymentManager() + + +if __name__ == '__main__': + main() \ No newline at end of file From d5ab3dc0f38927f426f70c22e7143077c5b4e8cf Mon Sep 17 00:00:00 2001 From: Markus Ostertag Date: Tue, 26 Apr 2016 14:39:00 +0200 Subject: [PATCH 1476/2522] Change success status code to 202 As I already mentioned here: https://github.com/ansible/ansible-modules-extras/commit/a1b11826625b7f48d517b088651dc5ed4d6eb9d6#diff-d04a476e5d71372918cb6e7e5b39a683R120 @jimi-c added some "hidden" additional check in his urllib commit and broke the whole module for everybody as Datadog answers with an 202 in case of success (http://docs.datadoghq.com/api/#troubleshooting). --- monitoring/datadog_event.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/datadog_event.py b/monitoring/datadog_event.py index 25e8ce052b6..5d083eab0f7 100644 --- a/monitoring/datadog_event.py +++ b/monitoring/datadog_event.py @@ -142,7 +142,7 @@ def post_event(module): headers = {"Content-Type": "application/json"} (response, info) = fetch_url(module, uri, data=json_body, headers=headers) - if info['status'] == 200: + if info['status'] == 202: response_body = response.read() response_json = module.from_json(response_body) if response_json['status'] == 'ok': From 344dff4350489eea56285fca4e5c0ee6cf73cb2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Tue, 26 Apr 2016 15:10:46 +0200 Subject: [PATCH 1477/2522] docs: fix make docs (#2107) --- system/make.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/system/make.py b/system/make.py index 9ac47124279..ee8d07be744 100644 --- a/system/make.py +++ b/system/make.py @@ -22,19 +22,25 @@ --- module: make short_description: Run targets in a Makefile -requirements: [] +requirements: [ make ] version_added: "2.1" author: Linus Unnebäck (@LinusU) -description: Run targets in a Makefile. +description: + - Run targets in a Makefile. options: target: - description: The target to run + description: + - The target to run required: false + default: none params: - description: Any extra parameters to pass to make + description: + - Any extra parameters to pass to make required: false + default: none chdir: - description: cd into this directory before running make + description: + - cd into this directory before running make required: true ''' From 59821bbc30b1feb09d7d960614c778e32024ee8b Mon Sep 17 00:00:00 2001 From: James Cammarata Date: Tue, 26 Apr 2016 10:00:16 -0400 Subject: [PATCH 1478/2522] Removing docker_login as it's now in core --- cloud/docker/docker_login.py | 261 ----------------------------------- 1 file changed, 261 deletions(-) delete mode 100644 cloud/docker/docker_login.py diff --git a/cloud/docker/docker_login.py b/cloud/docker/docker_login.py deleted file mode 100644 index 964d507a16e..00000000000 --- a/cloud/docker/docker_login.py +++ /dev/null @@ -1,261 +0,0 @@ -#!/usr/bin/python -# - -# (c) 2015, Olaf Kilian -# -# This file is part of Ansible -# -# This module is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This software is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this software. If not, see . - -###################################################################### - -DOCUMENTATION = ''' ---- -module: docker_login -author: Olaf Kilian -version_added: "2.0" -short_description: Manage Docker registry logins -description: - - Ansible version of the "docker login" CLI command. - - This module allows you to login to a Docker registry without directly pulling an image or performing any other actions. - - It will write your login credentials to your local .dockercfg file that is compatible to the Docker CLI client as well as docker-py and all other Docker related modules that are based on docker-py. -options: - registry: - description: - - "URL of the registry, defaults to: https://index.docker.io/v1/" - required: false - default: "https://index.docker.io/v1/" - username: - description: - - The username for the registry account - required: true - password: - description: - - The plaintext password for the registry account - required: true - email: - description: - - The email address for the registry account. Note that private registries usually don't need this, but if you want to log into your Docker Hub account (default behaviour) you need to specify this in order to be able to log in. - required: false - default: None - reauth: - description: - - Whether refresh existing authentication on the Docker server (boolean) - required: false - default: false - dockercfg_path: - description: - - Use a custom path for the .dockercfg file - required: false - default: ~/.docker/config.json - docker_url: - description: - - Refers to the protocol+hostname+port where the Docker server is hosted - required: false - default: unix://var/run/docker.sock - timeout: - description: - - The HTTP request timeout in seconds - required: false - default: 600 - -requirements: [ "python >= 2.6", "docker-py >= 1.1.0" ] -''' - -EXAMPLES = ''' -Login to a Docker registry without performing any other action. Make sure that the user you are using is either in the docker group which owns the Docker socket or use sudo to perform login actions: - -- name: login to DockerHub remote registry using your account - docker_login: - username: docker - password: rekcod - email: docker@docker.io - -- name: login to private Docker remote registry and force reauthentification - docker_login: - registry: your.private.registry.io - username: yourself - password: secrets3 - reauth: yes - -- name: login to DockerHub remote registry using a custom dockercfg file location - docker_login: - username: docker - password: rekcod - email: docker@docker.io - dockercfg_path: /tmp/.mydockercfg - -''' - -import os.path -import json -import base64 -from urlparse import urlparse -from distutils.version import LooseVersion - -try: - import docker.client - from docker.errors import APIError as DockerAPIError - has_lib_docker = True - if LooseVersion(docker.__version__) >= LooseVersion("1.1.0"): - has_correct_lib_docker_version = True - else: - has_correct_lib_docker_version = False -except ImportError: - has_lib_docker = False - -try: - import requests - has_lib_requests = True -except ImportError: - has_lib_requests = False - - -class DockerLoginManager: - - def __init__(self, module): - - self.module = module - self.registry = self.module.params.get('registry') - self.username = self.module.params.get('username') - self.password = self.module.params.get('password') - self.email = self.module.params.get('email') - self.reauth = self.module.params.get('reauth') - self.dockercfg_path = os.path.expanduser(self.module.params.get('dockercfg_path')) - - docker_url = urlparse(module.params.get('docker_url')) - self.client = docker.Client(base_url=docker_url.geturl(), timeout=module.params.get('timeout')) - - self.changed = False - self.response = False - self.log = list() - - def login(self): - - if self.reauth: - self.log.append("Enforcing reauthentification") - - # Connect to registry and login if not already logged in or reauth is enforced. - try: - self.response = self.client.login( - self.username, - password=self.password, - email=self.email, - registry=self.registry, - reauth=self.reauth, - dockercfg_path=self.dockercfg_path - ) - except DockerAPIError as e: - self.module.fail_json(msg="Docker API Error: %s" % e.explanation) - except Exception as e: - self.module.fail_json(msg="failed to login to the remote registry", error=repr(e)) - - # Get status from registry response. - if "Status" in self.response: - self.log.append(self.response["Status"]) - - # Update the dockercfg if not in check mode. - if not self.module.check_mode: - self.update_dockercfg() - - # This is what the underlaying docker-py unfortunately doesn't do (yet). - def update_dockercfg(self): - - # Create dockercfg file if it does not exist. - if not os.path.exists(self.dockercfg_path): - dockercfg_path_dir = os.path.dirname(self.dockercfg_path) - if not os.path.exists(dockercfg_path_dir): - os.makedirs(dockercfg_path_dir) - open(self.dockercfg_path, "w") - self.log.append("Created new Docker config file at %s" % self.dockercfg_path) - else: - self.log.append("Found existing Docker config file at %s" % self.dockercfg_path) - - # Build a dict for the existing dockercfg. - try: - docker_config = json.load(open(self.dockercfg_path, "r")) - except ValueError: - docker_config = dict() - if "auths" not in docker_config: - docker_config["auths"] = dict() - if self.registry not in docker_config["auths"]: - docker_config["auths"][self.registry] = dict() - - # Calculate docker credentials based on current parameters. - new_docker_config = dict( - auth = base64.b64encode(self.username + b':' + self.password), - email = self.email - ) - - # Update config if persisted credentials differ from current credentials. - if new_docker_config != docker_config["auths"][self.registry]: - docker_config["auths"][self.registry] = new_docker_config - try: - json.dump(docker_config, open(self.dockercfg_path, "w"), indent=4, sort_keys=True) - except Exception as e: - self.module.fail_json(msg="failed to write auth details to file", error=repr(e)) - self.log.append("Updated Docker config with new credentials.") - self.changed = True - - # Compatible to docker-py auth.decode_docker_auth() - def encode_docker_auth(self, auth): - s = base64.b64decode(auth) - login, pwd = s.split(b':', 1) - return login.decode('ascii'), pwd.decode('ascii') - - def get_msg(self): - return ". ".join(self.log) - - def has_changed(self): - return self.changed - - -def main(): - - module = AnsibleModule( - argument_spec = dict( - registry = dict(required=False, default='https://index.docker.io/v1/'), - username = dict(required=True), - password = dict(required=True, no_log=True), - email = dict(required=False, default=None), - reauth = dict(required=False, default=False, type='bool'), - dockercfg_path = dict(required=False, default='~/.docker/config.json'), - docker_url = dict(default='unix://var/run/docker.sock'), - timeout = dict(default=10, type='int') - ), - supports_check_mode=True - ) - - if not has_lib_docker: - module.fail_json(msg="python library docker-py required: pip install docker-py>=1.1.0") - - if not has_correct_lib_docker_version: - module.fail_json(msg="your version of docker-py is outdated: pip install docker-py>=1.1.0") - - if not has_lib_requests: - module.fail_json(msg="python library requests required: pip install requests") - - try: - manager = DockerLoginManager(module) - manager.login() - module.exit_json(changed=manager.has_changed(), msg=manager.get_msg()) - - except Exception as e: - module.fail_json(msg="Module execution has failed due to an unexpected error", error=repr(e)) - -# import module snippets -from ansible.module_utils.basic import * - -if __name__ == '__main__': - main() From 470460acfcdc6198325919739287a486ce461408 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 26 Apr 2016 10:00:11 -0400 Subject: [PATCH 1479/2522] promoted to core, including fixes --- cloud/docker/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 cloud/docker/__init__.py diff --git a/cloud/docker/__init__.py b/cloud/docker/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 From d198d025de7aaecb0ae63335d31947da017a1b00 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 26 Apr 2016 18:26:50 +0100 Subject: [PATCH 1480/2522] Fix for https://github.com/ansible/ansible-modules-extras/issues/2090 --- windows/win_regedit.ps1 | 4 ++-- windows/win_regedit.py | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/windows/win_regedit.ps1 b/windows/win_regedit.ps1 index c98c79ce8ef..f4975dea224 100644 --- a/windows/win_regedit.ps1 +++ b/windows/win_regedit.ps1 @@ -118,8 +118,8 @@ else } -if($registryDataType -eq "binary" -and $registryData -ne $null) { - $registryData = Convert-RegExportHexStringToByteArray($registryData) +if($registryDataType -eq "binary" -and $registryData -ne $null -and $registryData.GetType().Name -eq 'String') { + $registryData = Convert-RegExportHexStringToByteArray($registryData) } if($state -eq "present") { diff --git a/windows/win_regedit.py b/windows/win_regedit.py index 47dc0a6c56b..8845f8ceb9f 100644 --- a/windows/win_regedit.py +++ b/windows/win_regedit.py @@ -43,7 +43,7 @@ aliases: [] data: description: - - Registry Value Data. Binary data should be expressed as comma separated hex values. An easy way to generate this is to run C(regedit.exe) and use the I(Export) option to save the registry values to a file. In the exported file binary values will look like C(hex:be,ef,be,ef). The C(hex:) prefix is optional. + - Registry Value Data. Binary data should be expressed a yaml byte array or as comma separated hex values. An easy way to generate this is to run C(regedit.exe) and use the I(Export) option to save the registry values to a file. In the exported file binary values will look like C(hex:be,ef,be,ef). The C(hex:) prefix is optional. required: false default: null aliases: [] @@ -96,13 +96,24 @@ # Creates Registry Key called MyCompany, # a value within MyCompany Key called "hello", and - # binary data for the value "hello" as type "binary". + # binary data for the value "hello" as type "binary" + # data expressed as comma separated list win_regedit: key: HKCU:\Software\MyCompany value: hello data: hex:be,ef,be,ef,be,ef,be,ef,be,ef datatype: binary + # Creates Registry Key called MyCompany, + # a value within MyCompany Key called "hello", and + # binary data for the value "hello" as type "binary" + # data expressed as yaml array of bytes + win_regedit: + key: HKCU:\Software\MyCompany + value: hello + data: [0xbe,0xef,0xbe,0xef,0xbe,0xef,0xbe,0xef,0xbe,0xef] + datatype: binary + # Delete Registry Key MyCompany # NOTE: Not specifying a value will delete the root key which means # all values will be deleted From 95018b5fe89911924eec18265a9659fb5b03eea7 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Wed, 27 Apr 2016 16:47:44 +0200 Subject: [PATCH 1481/2522] Add partial doc on return value of virt (#2116) --- cloud/misc/virt.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/cloud/misc/virt.py b/cloud/misc/virt.py index 65791e43e9f..be1a7f9ec62 100644 --- a/cloud/misc/virt.py +++ b/cloud/misc/virt.py @@ -84,6 +84,23 @@ virt: name=foo state=running uri=lxc:/// ''' +RETURN = ''' +# for list_vms command +list_vms: + description: The list of vms defined on the remote system + type: dictionary + returned: success + sample: [ + "build.example.org", + "dev.example.org" + ] +# for status command +status: + description: The status of the VM, among running, crashed, paused and shutdown + type: string + sample: "success" + returned: success +''' VIRT_FAILED = 1 VIRT_SUCCESS = 0 VIRT_UNAVAILABLE=2 From a1cc951d6f56dc76086c7ad21456342f2642ebf6 Mon Sep 17 00:00:00 2001 From: chouseknecht Date: Wed, 27 Apr 2016 10:48:20 -0400 Subject: [PATCH 1482/2522] Updated per PR comments --- cloud/azure/azure_rm_deployment.py | 82 ++++++++++++++---------------- 1 file changed, 38 insertions(+), 44 deletions(-) diff --git a/cloud/azure/azure_rm_deployment.py b/cloud/azure/azure_rm_deployment.py index 022c240a92c..aeb2454eb92 100644 --- a/cloud/azure/azure_rm_deployment.py +++ b/cloud/azure/azure_rm_deployment.py @@ -32,6 +32,7 @@ description: - The resource group name to use or create to host the deployed template required: true + default: null location: description: - The geo-locations in which the resource group will be located. @@ -42,6 +43,7 @@ - If state is "present", template will be created. If state is "present" and if deployment exists, it will be updated. If state is "absent", stack will be removed. default: present + required: true choices: - present - absent @@ -50,25 +52,25 @@ - A hash containing the templates inline. This parameter is mutually exclusive with 'template_link'. Either one of them is required if "state" parameter is "present". required: false - default: None + default: null template_link: description: - Uri of file containing the template body. This parameter is mutually exclusive with 'template'. Either one of them is required if "state" parameter is "present". required: false - default: None + default: null parameters: description: - A hash of all the required template variables for the deployment template. This parameter is mutually exclusive with 'parameters_link'. Either one of them is required if "state" parameter is "present". required: false - default: None + default: null parameters_link: description: - Uri of file containing the parameters body. This parameter is mutually exclusive with 'parameters'. Either one of them is required if "state" parameter is "present". required: false - default: None + default: null extends_documentation_fragment: - azure @@ -314,51 +316,40 @@ ''' RETURN = ''' -msg: - description: String indicating if the deployment was created or deleted - returned: always - type: string - sample: "deployment created" deployment: description: Deployment details type: dict returned: always - sample:{ - "group_name": "Test_Deployment", - "id": "/subscriptions/3f7e29ba-24e0-42f6-8d9c-5149a14bda37/resourceGroups/Test_Deployment/providers/Microsoft.Resources/deployments/ansible-arm", - "instances": [ - { - "ips": [ - { - "dns_settings": { - "domain_name_label": "testvm9910001", - "fqdn": "testvm9910001.westus.cloudapp.azure.com" - }, - "id": "/subscriptions/3f7e29ba-24e0-42f6-8d9c-5149a14bda37/resourceGroups/Test_Deployment/providers/Microsoft.Network/publicIPAddresses/myPublicIP", - "name": "myPublicIP", - "public_ip": "13.91.99.232", - "public_ip_allocation_method": "IPAllocationMethod.dynamic" - } - ], - "vm_name": "MyUbuntuVM" - } - ], - "name": "ansible-arm", - "outputs": { - "hostname": { - "type": "String", - "value": "testvm9910001.westus.cloudapp.azure.com" - }, - "sshCommand": { - "type": "String", - "value": "ssh chouseknecht@testvm9910001.westus.cloudapp.azure.com" - } - } - } + sample: + group_name: + description: Name of the resource group + type: string + returned: always + id: + description: The Azure ID of the deployment + type: string + returned: always + instances: + description: Provides the public IP addresses for each VM instance. + type: list + returned: always + name: + description: Name of the deployment + type: string + returned: always + outputs: + description: Dictionary of outputs received from the deployment + type: dict + returned: always ''' -import time -import yaml +PREREQ_IMPORT_ERROR = None + +try: + import time + import yaml +except ImportError as exc: + IMPORT_ERROR = "Error importing module prerequisites: %s" % exc from ansible.module_utils.basic import * from ansible.module_utils.azure_rm_common import * @@ -427,6 +418,9 @@ def __init__(self): def exec_module(self, **kwargs): + if PREREQ_IMPORT_ERROR: + self.fail(PREREQ_IMPORT_ERROR) + for key in self.module_arg_spec.keys() + ['tags']: setattr(self, key, kwargs[key]) @@ -440,7 +434,7 @@ def exec_module(self, **kwargs): instances=self._get_instances(deployment) ) self.results['changed'] = True - self.results['msg'] = 'deployment created' + self.results['msg'] = 'deployment succeeded' else: if self.resource_group_exists(self.resource_group_name): self.destroy_resource_group() From df055265d4aea09d47dc9200f5c3907503cca209 Mon Sep 17 00:00:00 2001 From: Jordan Cohen Date: Wed, 27 Apr 2016 12:32:27 -0400 Subject: [PATCH 1483/2522] message template variable fix Due to ansible/jinja2 templating, it is difficult to use the monitor message template variables as they need to be surrounded by `{{` and `}}`, this change addresses that issue by allowing the user to use `[[` and `]]` instead. --- monitoring/datadog_monitor.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py index c5aec1b561e..34021d607f0 100644 --- a/monitoring/datadog_monitor.py +++ b/monitoring/datadog_monitor.py @@ -63,7 +63,7 @@ description: ["The name of the alert."] required: true message: - description: ["A message to include with notifications for this monitor. Email notifications can be sent to specific users by using the same '@username' notation as events."] + description: ["A message to include with notifications for this monitor. Email notifications can be sent to specific users by using the same '@username' notation as events. Monitor message template variables can be accessed by using double square brackets, i.e '[[' and ']]'."] required: false default: null silenced: @@ -176,6 +176,9 @@ def main(): elif module.params['state'] == 'unmute': unmute_monitor(module) +def _fix_template_vars(message): + return message.replace('[[', '{{').replace(']]', '}}') + def _get_monitor(module): for monitor in api.Monitor.get_all(): @@ -187,7 +190,7 @@ def _get_monitor(module): def _post_monitor(module, options): try: msg = api.Monitor.create(type=module.params['type'], query=module.params['query'], - name=module.params['name'], message=module.params['message'], + name=module.params['name'], message=_fix_template_vars(module.params['message']), options=options) if 'errors' in msg: module.fail_json(msg=str(msg['errors'])) @@ -204,7 +207,7 @@ def _equal_dicts(a, b, ignore_keys): def _update_monitor(module, monitor, options): try: msg = api.Monitor.update(id=monitor['id'], query=module.params['query'], - name=module.params['name'], message=module.params['message'], + name=module.params['name'], message=_fix_template_vars(module.params['message']), options=options) if 'errors' in msg: module.fail_json(msg=str(msg['errors'])) From d91baee7c91409f5beedb8439ed98af065b8f152 Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 28 Apr 2016 04:36:11 +1000 Subject: [PATCH 1484/2522] Add git_config module (#1945) * Add git_config module This module can be used for reading and writing git configuration at all three scopes (local, global and system). It supports --diff and --check out of the box. This module is based off of the following gist: https://gist.github.com/mgedmin/b38c74e2d25cb4f47908 I tidied it up and added support for the following: - Reading values on top of writing them - Reading and writing values at any scope The original author is credited in the documentation for the module. * Respond to review feedback - Improve documentation by adding choices for parameters, requirements for module, and add missing description for scope parameter. - Fail gracefully when git is not installed (followed example of puppet module). - Remove trailing whitespace. * Change repo parameter to type 'path' This ensures that all paths are automatically expanded appropriately. * Set locale to C before running commands to ensure consistent error messages This is important to ensure error message parsing occurs correctly. * Adjust comment --- system/git_config.py | 219 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 system/git_config.py diff --git a/system/git_config.py b/system/git_config.py new file mode 100644 index 00000000000..1e229b7bc1b --- /dev/null +++ b/system/git_config.py @@ -0,0 +1,219 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Marius Gedminas +# (c) 2016, Matthew Gamble +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: git_config +author: + - "Matthew Gamble" + - "Marius Gedminas" +version_added: 2.1 +requirements: ['git'] +short_description: Read and write git configuration +description: + - The M(git_config) module changes git configuration by invoking 'git config'. + This is needed if you don't want to use M(template) for the entire git + config file (e.g. because you need to change just C(user.email) in + /etc/.git/config). Solutions involving M(command) are cumbersone or + don't work correctly in check mode. +options: + list_all: + description: + - List all settings (optionally limited to a given I(scope)) + required: false + choices: [ "yes", "no" ] + default: no + name: + description: + - The name of the setting. If no value is supplied, the value will + be read from the config if it has been set. + required: false + default: null + repo: + description: + - Path to a git repository for reading and writing values from a + specific repo. + required: false + default: null + scope: + description: + - Specify which scope to read/set values from. This is required + when setting config values. If this is set to local, you must + also specify the repo parameter. It defaults to system only when + not using I(list_all)=yes. + required: false + choices: [ "local", "global", "system" ] + default: null + value: + description: + - When specifying the name of a single setting, supply a value to + set that setting to the given value. + required: false + default: null +''' + +EXAMPLES = ''' +# Set some settings in ~/.gitconfig +- git_config: name=alias.ci scope=global value=commit +- git_config: name=alias.st scope=global value=status + +# Or system-wide: +- git_config: name=alias.remotev scope=system value="remote -v" +- git_config: name=core.editor scope=global value=vim +# scope=system is the default +- git_config: name=alias.diffc value="diff --cached" +- git_config: name=color.ui value=auto + +# Make etckeeper not complain when invoked by cron +- git_config: name=user.email repo=/etc scope=local value="root@{{ ansible_fqdn }}" + +# Read individual values from git config +- git_config: name=alias.ci scope=global +# scope=system is also assumed when reading values, unless list_all=yes +- git_config: name=alias.diffc + +# Read all values from git config +- git_config: list_all=yes scope=global +# When list_all=yes and no scope is specified, you get configuration from all scopes +- git_config: list_all=yes +# Specify a repository to include local settings +- git_config: list_all=yes repo=/path/to/repo.git +''' + +RETURN = ''' +--- +config_value: + description: When list_all=no and value is not set, a string containing the value of the setting in name + returned: success + type: string + sample: "vim" + +config_values: + description: When list_all=yes, a dict containing key/value pairs of multiple configuration settings + returned: success + type: dictionary + sample: + core.editor: "vim" + color.ui: "auto" + alias.diffc: "diff --cached" + alias.remotev: "remote -v" +''' + + +def main(): + module = AnsibleModule( + argument_spec=dict( + list_all=dict(required=False, type='bool', default=False), + name=dict(type='str'), + repo=dict(type='path'), + scope=dict(required=False, type='str', choices=['local', 'global', 'system']), + value=dict(required=False) + ), + mutually_exclusive=[['list_all', 'name'], ['list_all', 'value']], + required_if=[('scope', 'local', ['repo'])], + required_one_of=[['list_all', 'name']], + supports_check_mode=True, + ) + git_path = module.get_bin_path('git') + if not git_path: + module.fail_json(msg="Could not find git. Please ensure it is installed.") + + params = module.params + # We check error message for a pattern, so we need to make sure the messages appear in the form we're expecting. + # Set the locale to C to ensure consistent messages. + module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C') + + if params['name']: + name = params['name'] + else: + name = None + + if params['scope']: + scope = params['scope'] + elif params['list_all']: + scope = None + else: + scope = 'system' + + if params['value']: + new_value = params['value'] + else: + new_value = None + + args = [git_path, "config"] + if params['list_all']: + args.append('-l') + if scope: + args.append("--" + scope) + if name: + args.append(name) + + if scope == 'local': + dir = params['repo'] + elif params['list_all'] and params['repo']: + # Include local settings from a specific repo when listing all available settings + dir = params['repo'] + else: + # Run from root directory to avoid accidentally picking up any local config settings + dir = "/" + + (rc, out, err) = module.run_command(' '.join(args), cwd=dir) + if params['list_all'] and scope and rc == 128 and 'unable to read config file' in err: + # This just means nothing has been set at the given scope + module.exit_json(changed=False, msg='', config_values={}) + elif rc >= 2: + # If the return code is 1, it just means the option hasn't been set yet, which is fine. + module.fail_json(rc=rc, msg=err, cmd=' '.join(args)) + + if params['list_all']: + values = out.rstrip().splitlines() + config_values = {} + for value in values: + k, v = value.split('=', 1) + config_values[k] = v + module.exit_json(changed=False, msg='', config_values=config_values) + elif not new_value: + module.exit_json(changed=False, msg='', config_value=out.rstrip()) + else: + old_value = out.rstrip() + if old_value == new_value: + module.exit_json(changed=False, msg="") + + if not module.check_mode: + new_value_quoted = "'" + new_value + "'" + (rc, out, err) = module.run_command(' '.join(args + [new_value_quoted]), cwd=dir) + if err: + module.fail_json(rc=rc, msg=err, cmd=' '.join(args + [new_value_quoted])) + module.exit_json( + msg='setting changed', + diff=dict( + before_header=' '.join(args), + before=old_value + "\n", + after_header=' '.join(args), + after=new_value + "\n" + ), + changed=True + ) + +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From 09a1015bd759cb2c8e18f6267cbd29fb28dbdaa2 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Wed, 27 Apr 2016 14:36:56 -0400 Subject: [PATCH 1485/2522] updated version added --- system/git_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/git_config.py b/system/git_config.py index 1e229b7bc1b..4a3a6e9a58e 100644 --- a/system/git_config.py +++ b/system/git_config.py @@ -25,7 +25,7 @@ author: - "Matthew Gamble" - "Marius Gedminas" -version_added: 2.1 +version_added: 2.2 requirements: ['git'] short_description: Read and write git configuration description: From 71d83b77bcd009c1a92017af6e97cbbd838ac666 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Thu, 28 Apr 2016 08:27:21 -0700 Subject: [PATCH 1486/2522] Recategorize git_config and shift version to 2.1 --- {system => source_control}/git_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename {system => source_control}/git_config.py (99%) diff --git a/system/git_config.py b/source_control/git_config.py similarity index 99% rename from system/git_config.py rename to source_control/git_config.py index 4a3a6e9a58e..1e229b7bc1b 100644 --- a/system/git_config.py +++ b/source_control/git_config.py @@ -25,7 +25,7 @@ author: - "Matthew Gamble" - "Marius Gedminas" -version_added: 2.2 +version_added: 2.1 requirements: ['git'] short_description: Read and write git configuration description: From fbd00675f07a89e45508fd478517322e08597b84 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 28 Apr 2016 17:42:41 -0400 Subject: [PATCH 1487/2522] updated version added for pvs --- system/lvol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/lvol.py b/system/lvol.py index e152e0ee36d..609e1d3a0cf 100644 --- a/system/lvol.py +++ b/system/lvol.py @@ -69,7 +69,7 @@ - The name of the snapshot volume required: false pvs: - version_added: "2.1" + version_added: "2.2" description: - Comma separated list of physical volumes e.g. /dev/sda,/dev/sdb required: false From e07a52b49908edb24e91651b08e7fc7928fd1354 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Fri, 29 Apr 2016 09:56:28 +0200 Subject: [PATCH 1488/2522] Mark api_key as no_log to avoid potential leaks (#2048) --- monitoring/circonus_annotation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/circonus_annotation.py b/monitoring/circonus_annotation.py index ae5c98c87a1..9c5fbbb0fd6 100644 --- a/monitoring/circonus_annotation.py +++ b/monitoring/circonus_annotation.py @@ -133,7 +133,7 @@ def main(): title=dict(required=True), description=dict(required=True), duration=dict(required=False, type='int'), - api_key=dict(required=True) + api_key=dict(required=True, no_log=True) ) ) annotation = create_annotation(module) From f7c421088adebea4966729adf38ae37955e9f9b3 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Fri, 29 Apr 2016 09:57:52 +0200 Subject: [PATCH 1489/2522] Use type=path for arguments, and remove code doing the conversion (#1903) --- packaging/language/npm.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packaging/language/npm.py b/packaging/language/npm.py index b0882ddec08..53ce1e77c5e 100644 --- a/packaging/language/npm.py +++ b/packaging/language/npm.py @@ -154,7 +154,6 @@ def _exec(self, args, run_in_check_mode=False, check_rc=True): #If path is specified, cd into that path and run the command. cwd = None if self.path: - self.path = os.path.abspath(os.path.expanduser(self.path)) if not os.path.exists(self.path): os.makedirs(self.path) if not os.path.isdir(self.path): @@ -212,10 +211,10 @@ def list_outdated(self): def main(): arg_spec = dict( name=dict(default=None), - path=dict(default=None), + path=dict(default=None, type='path'), version=dict(default=None), production=dict(default='no', type='bool'), - executable=dict(default=None), + executable=dict(default=None, type='path'), registry=dict(default=None), state=dict(default='present', choices=['present', 'absent', 'latest']), ignore_scripts=dict(default=False, type='bool'), From 94e6c326085a762a113344459de4129fb5ad8de4 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Fri, 29 Apr 2016 10:10:27 +0200 Subject: [PATCH 1490/2522] azure_rm_deployment: fix docs and move import utils near main() fixes build --- cloud/azure/azure_rm_deployment.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cloud/azure/azure_rm_deployment.py b/cloud/azure/azure_rm_deployment.py index aeb2454eb92..2d72436232f 100644 --- a/cloud/azure/azure_rm_deployment.py +++ b/cloud/azure/azure_rm_deployment.py @@ -32,7 +32,6 @@ description: - The resource group name to use or create to host the deployed template required: true - default: null location: description: - The geo-locations in which the resource group will be located. @@ -43,7 +42,7 @@ - If state is "present", template will be created. If state is "present" and if deployment exists, it will be updated. If state is "absent", stack will be removed. default: present - required: true + required: false choices: - present - absent @@ -351,7 +350,6 @@ except ImportError as exc: IMPORT_ERROR = "Error importing module prerequisites: %s" % exc -from ansible.module_utils.basic import * from ansible.module_utils.azure_rm_common import * try: @@ -635,6 +633,7 @@ def _nic_to_public_ips_instance(self, nics): def main(): AzureRMDeploymentManager() - +from ansible.module_utils.basic import * if __name__ == '__main__': - main() \ No newline at end of file + main() + From 3a7e4b58349511eca1d6ce9c12781f1299fd8e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Fri, 29 Apr 2016 10:50:36 +0200 Subject: [PATCH 1491/2522] readme: add build state image from travis --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9a0ddb6c898..a9165059a68 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Build Status](https://travis-ci.org/ansible/ansible-modules-extras.svg?branch=devel)](https://travis-ci.org/ansible/ansible-modules-extras) + ansible-modules-extras ====================== From ea8547c86ad8ad1f4c7fdef6549ac64c28c002be Mon Sep 17 00:00:00 2001 From: Brad Davidson Date: Fri, 29 Apr 2016 20:29:14 +0000 Subject: [PATCH 1492/2522] Fix argument spec for type and tags; return VGW info instead of raw response --- cloud/amazon/ec2_vpc_vgw.py | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/cloud/amazon/ec2_vpc_vgw.py b/cloud/amazon/ec2_vpc_vgw.py index 6c8a80e9872..466b81c8762 100644 --- a/cloud/amazon/ec2_vpc_vgw.py +++ b/cloud/amazon/ec2_vpc_vgw.py @@ -40,6 +40,7 @@ description: - type of the virtual gateway to be created required: false + choices: [ "ipsec.1" ] vpn_gateway_id: description: - vpn gateway id of an existing virtual gateway @@ -57,6 +58,8 @@ description: - dictionary of resource tags required: false + default: null + aliases: [ "resource_tags" ] author: Nick Aslanidis (@naslanidis) extends_documentation_fragment: - aws @@ -120,6 +123,26 @@ except ImportError: HAS_BOTO3 = False +def get_vgw_info(vgws): + if not isinstance(vgws, list): + return + + for vgw in vgws: + vgw_info = { + 'id': vgw['VpnGatewayId'], + 'type': vgw['Type'], + 'state': vgw['State'], + 'vpc_id': None, + 'tags': dict() + } + + for tag in vgw['Tags']: + vgw_info['tags'][tag['Key']] = tag['Value'] + + if len(vgw['VpcAttachments']) != 0: + vgw_info['vpc_id'] = vgw['VpcAttachments'][0]['VpcId'] + + return vgw_info def wait_for_status(client, module, vpn_gateway_id, status): polling_increment_secs = 15 @@ -425,7 +448,7 @@ def ensure_vgw_present(client, module): changed = True vgw = find_vgw(client, module, [vpn_gateway_id]) - result = vgw + result = get_vgw_info(vgw) return changed, result @@ -514,7 +537,7 @@ def ensure_vgw_absent(client, module): changed = False deleted_vgw = None - result = deleted_vgw + result = get_vgw_info(deleted_vgw) return changed, result @@ -526,9 +549,9 @@ def main(): name=dict(), vpn_gateway_id=dict(), vpc_id=dict(), - wait_timeout=dict(type='int', default=320, required=False), - type=dict(), - tags=dict(), + wait_timeout=dict(type='int', default=320), + type=dict(default='ipsec.1', choices=['ipsec.1']), + tags=dict(default=None, required=False, type='dict', aliases=['resource_tags']), ) ) module = AnsibleModule(argument_spec=argument_spec) @@ -548,7 +571,7 @@ def main(): (changed, results) = ensure_vgw_present(client, module) else: (changed, results) = ensure_vgw_absent(client, module) - module.exit_json(changed=changed, result=results) + module.exit_json(changed=changed, vgw=results) # import module snippets From 1846de28095794f96914f10136af51f4e2676710 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Fri, 29 Apr 2016 20:33:06 -0700 Subject: [PATCH 1493/2522] Switch blockinfile to using the latest best way to get ansible version --- files/blockinfile.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/files/blockinfile.py b/files/blockinfile.py index 3ff1bc7c720..eadf3a622dd 100644 --- a/files/blockinfile.py +++ b/files/blockinfile.py @@ -150,8 +150,6 @@ import os import tempfile -from ansible import __version__ - def write_changes(module, contents, dest): @@ -246,7 +244,7 @@ def main(): marker1 = re.sub(r'{mark}', 'END', marker) if present and block: # Escape seqeuences like '\n' need to be handled in Ansible 1.x - if __version__.startswith('1.'): + if module.constants['ANSIBLE_VERSION'].startswith('1.'): block = re.sub('', block, '') blocklines = [marker0] + block.splitlines() + [marker1] else: From c0013af14cb75d4ff189ee5c445c32a14d471bb0 Mon Sep 17 00:00:00 2001 From: Jordan Cohen Date: Mon, 2 May 2016 07:30:15 -0400 Subject: [PATCH 1494/2522] doc update to demostrate message template vars --- monitoring/datadog_monitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py index 34021d607f0..657c3b64c97 100644 --- a/monitoring/datadog_monitor.py +++ b/monitoring/datadog_monitor.py @@ -107,7 +107,7 @@ name: "Test monitor" state: "present" query: "datadog.agent.up".over("host:host1").last(2).count_by_status()" - message: "Some message." + message: "Host [[host.name]] with IP [[host.ip]] is failing to report to datadog." api_key: "9775a026f1ca7d1c6c5af9d94d9595a4" app_key: "87ce4a24b5553d2e482ea8a8500e71b8ad4554ff" From 28ae506c12a78740130b67a7f6ebef54088746dd Mon Sep 17 00:00:00 2001 From: Rudi Meyer Date: Mon, 2 May 2016 14:48:03 +0200 Subject: [PATCH 1495/2522] Jira will return a HTTP status code 201 on some actions, fx. 'comment'. (#2115) --- web_infrastructure/jira.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 web_infrastructure/jira.py diff --git a/web_infrastructure/jira.py b/web_infrastructure/jira.py old mode 100644 new mode 100755 index dded069f743..42d5e092974 --- a/web_infrastructure/jira.py +++ b/web_infrastructure/jira.py @@ -187,7 +187,7 @@ def request(url, user, passwd, data=None, method=None): headers={'Content-Type':'application/json', 'Authorization':"Basic %s" % auth}) - if info['status'] not in (200, 204): + if info['status'] not in (200, 201, 204): module.fail_json(msg=info['msg']) body = response.read() From 12f1c85aa36d46fc4593df2e4202f6c3f069004b Mon Sep 17 00:00:00 2001 From: krzwalko Date: Mon, 2 May 2016 14:50:11 +0200 Subject: [PATCH 1496/2522] Fix datacenter_name and cluster_name module params (#2142) --- cloud/vmware/vmware_vm_shell.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cloud/vmware/vmware_vm_shell.py b/cloud/vmware/vmware_vm_shell.py index a2d5c71098e..da4a7f62217 100644 --- a/cloud/vmware/vmware_vm_shell.py +++ b/cloud/vmware/vmware_vm_shell.py @@ -146,11 +146,11 @@ def main(): if not HAS_PYVMOMI: module.fail_json(changed=False, msg='pyvmomi is required for this module') - datacenter_name = p['datacenter'] - cluster_name = p['cluster'] try: p = module.params + datacenter_name = p['datacenter'] + cluster_name = p['cluster'] content = connect_to_api(module) datacenter = None @@ -185,4 +185,3 @@ def main(): if __name__ == '__main__': main() - From 9f2bc2853d1658850a56f3190a3599854dcbacef Mon Sep 17 00:00:00 2001 From: Kevin Hildebrand Date: Mon, 2 May 2016 11:16:07 -0400 Subject: [PATCH 1497/2522] Fix the interface handling code to allow permanent and non-permanent operations. Also avoid using add_interface because it breaks in cases where the interface is already bound to a different zone. --- system/firewalld.py | 80 +++++++++++++++++++++++++++++++++------------ 1 file changed, 60 insertions(+), 20 deletions(-) diff --git a/system/firewalld.py b/system/firewalld.py index c65e554edb8..89e821a48c7 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -213,6 +213,18 @@ def remove_source(zone, source): # interface handling # def get_interface(zone, interface): + if interface in fw.getInterfaces(zone): + return True + else: + return False + +def change_zone_of_interface(zone, interface): + fw.changeZoneOfInterface(zone, interface) + +def remove_interface(zone, interface): + fw.removeInterface(zone, interface) + +def get_interface_permanent(zone, interface): fw_zone = fw.config().getZoneByName(zone) fw_settings = fw_zone.getSettings() if interface in fw_settings.getInterfaces(): @@ -220,13 +232,20 @@ def get_interface(zone, interface): else: return False -def add_interface(zone, interface): +def change_zone_of_interface_permanent(zone, interface): fw_zone = fw.config().getZoneByName(zone) fw_settings = fw_zone.getSettings() - fw_settings.addInterface(interface) - fw_zone.update(fw_settings) - -def remove_interface(zone, interface): + old_zone_name = fw.config().getZoneOfInterface(interface) + if old_zone_name != zone: + if old_zone_name: + old_zone_obj = fw.config().getZoneByName(old_zone_name) + old_zone_settings = old_zone_obj.getSettings() + old_zone_settings.removeInterface(interface) # remove from old + old_zone_obj.update(old_zone_settings) + fw_settings.addInterface(interface) # add to new + fw_zone.update(fw_settings) + +def remove_interface_permanent(zone, interface): fw_zone = fw.config().getZoneByName(zone) fw_settings = fw_zone.getSettings() fw_settings.removeInterface(interface) @@ -535,23 +554,44 @@ def main(): msgs.append("Changed rich_rule %s to %s" % (rich_rule, desired_state)) if interface != None: - is_enabled = get_interface(zone, interface) - if desired_state == "enabled": - if is_enabled == False: - if module.check_mode: - module.exit_json(changed=True) + if permanent: + is_enabled = get_interface_permanent(zone, interface) + msgs.append('Permanent operation') + if desired_state == "enabled": + if is_enabled == False: + if module.check_mode: + module.exit_json(changed=True) - add_interface(zone, interface) - changed=True - msgs.append("Added %s to zone %s" % (interface, zone)) - elif desired_state == "disabled": - if is_enabled == True: - if module.check_mode: - module.exit_json(changed=True) + change_zone_of_interface_permanent(zone, interface) + changed=True + msgs.append("Changed %s to zone %s" % (interface, zone)) + elif desired_state == "disabled": + if is_enabled == True: + if module.check_mode: + module.exit_json(changed=True) - remove_interface(zone, interface) - changed=True - msgs.append("Removed %s from zone %s" % (interface, zone)) + remove_interface_permanent(zone, interface) + changed=True + msgs.append("Removed %s from zone %s" % (interface, zone)) + if immediate or not permanent: + is_enabled = get_interface(zone, interface) + msgs.append('Non-permanent operation') + if desired_state == "enabled": + if is_enabled == False: + if module.check_mode: + module.exit_json(changed=True) + + change_zone_of_interface(zone, interface) + changed=True + msgs.append("Changed %s to zone %s" % (interface, zone)) + elif desired_state == "disabled": + if is_enabled == True: + if module.check_mode: + module.exit_json(changed=True) + + remove_interface(zone, interface) + changed=True + msgs.append("Removed %s from zone %s" % (interface, zone)) if masquerade != None: From 7fd4180857f856a59792724e02e95dd99c067083 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Mon, 2 May 2016 19:21:02 +0200 Subject: [PATCH 1498/2522] Fix arguments and docs (#2147) - oauthkey shouldn't be logged - action should be restricted and checked and the doc should correspond to the code --- source_control/github_hooks.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source_control/github_hooks.py b/source_control/github_hooks.py index 9f664875587..8d3c120a787 100644 --- a/source_control/github_hooks.py +++ b/source_control/github_hooks.py @@ -57,7 +57,7 @@ description: - This tells the githooks module what you want it to do. required: true - choices: [ "create", "cleanall" ] + choices: [ "create", "cleanall", "list", "clean504" ] validate_certs: description: - If C(no), SSL certificates for the target repo will not be validated. This should only be used @@ -152,9 +152,9 @@ def _delete(module, hookurl, oauthkey, repo, user, hookid): def main(): module = AnsibleModule( argument_spec=dict( - action=dict(required=True), + action=dict(required=True, choices=['list','clean504','cleanall','create']), hookurl=dict(required=False), - oauthkey=dict(required=True), + oauthkey=dict(required=True, no_log=True), repo=dict(required=True), user=dict(required=True), validate_certs=dict(default='yes', type='bool'), From 675d778b50257a72290a68f4f644a4f630e85ce4 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Tue, 3 May 2016 10:10:28 -0700 Subject: [PATCH 1499/2522] Fix documentation --- cloud/vmware/vmware_vm_shell.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cloud/vmware/vmware_vm_shell.py b/cloud/vmware/vmware_vm_shell.py index da4a7f62217..80b4df192bd 100644 --- a/cloud/vmware/vmware_vm_shell.py +++ b/cloud/vmware/vmware_vm_shell.py @@ -59,7 +59,6 @@ - 'inventory_path' - 'vm_name' required: False - default: None vm_username: description: - The user to connect to the VM. From 9d705a42081f308b01132da1188a3a86a0c2c6fb Mon Sep 17 00:00:00 2001 From: Adrian Likins Date: Wed, 4 May 2016 01:27:17 -0400 Subject: [PATCH 1500/2522] Expect 204 status when using hipchat v2 api. When posting to the room notication api with hipchat v2 api, the expected return code is 204, as per: https://www.hipchat.com/docs/apiv2/method/send_room_notification fixes #2143 --- notification/hipchat.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/notification/hipchat.py b/notification/hipchat.py index f7543aa5592..8749b0b742e 100644 --- a/notification/hipchat.py +++ b/notification/hipchat.py @@ -155,7 +155,10 @@ def send_msg_v2(module, token, room, msg_from, msg, msg_format='text', module.exit_json(changed=False) response, info = fetch_url(module, url, data=data, headers=headers, method='POST') - if info['status'] == 200: + + # https://www.hipchat.com/docs/apiv2/method/send_room_notification shows + # 204 to be the expected result code. + if info['status'] in [200, 204]: return response.read() else: module.fail_json(msg="failed to send message, return status=%s" % str(info['status'])) From 336d47574663d44e9c5e0260f6c1b39af66bb36a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Luiz?= Date: Wed, 4 May 2016 06:29:15 +0100 Subject: [PATCH 1501/2522] =?UTF-8?q?fixed=20not=20resolving=20latest=20to?= =?UTF-8?q?=20snapshot=20for=20MD5=20check=20when=20the=20file=20ex?= =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packaging/language/maven_artifact.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packaging/language/maven_artifact.py b/packaging/language/maven_artifact.py index 77edd449afa..c71c98d4ac8 100644 --- a/packaging/language/maven_artifact.py +++ b/packaging/language/maven_artifact.py @@ -195,17 +195,17 @@ def _find_latest_version_available(self, artifact): return v[0] def find_uri_for_artifact(self, artifact): + if artifact.version == "latest": + artifact.version = self._find_latest_version_available(artifact) + if artifact.is_snapshot(): path = "/%s/maven-metadata.xml" % (artifact.path()) xml = self._request(self.base + path, "Failed to download maven-metadata.xml", lambda r: etree.parse(r)) timestamp = xml.xpath("/metadata/versioning/snapshot/timestamp/text()")[0] buildNumber = xml.xpath("/metadata/versioning/snapshot/buildNumber/text()")[0] return self._uri_for_artifact(artifact, artifact.version.replace("SNAPSHOT", timestamp + "-" + buildNumber)) - else: - if artifact.version == "latest": - artifact.version = self._find_latest_version_available(artifact) - return self._uri_for_artifact(artifact, artifact.version) + return self._uri_for_artifact(artifact, artifact.version) def _uri_for_artifact(self, artifact, version=None): if artifact.is_snapshot() and not version: From 47fddc83be645730506eab1c903d7b7f4eadfd59 Mon Sep 17 00:00:00 2001 From: Rob Date: Wed, 4 May 2016 15:30:30 +1000 Subject: [PATCH 1502/2522] =?UTF-8?q?Added=20doc=20for=20new=20get=5Fec2?= =?UTF-8?q?=5Fsecurity=5Fgroup=5Fids=5Ffrom=5Fnames=20function=20in=20m?= =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cloud/amazon/GUIDELINES.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cloud/amazon/GUIDELINES.md b/cloud/amazon/GUIDELINES.md index c3416240e22..225f61f3a49 100644 --- a/cloud/amazon/GUIDELINES.md +++ b/cloud/amazon/GUIDELINES.md @@ -220,3 +220,8 @@ key and the dict value is the tag value. Opposite of above. Converts an Ansible dict to a boto3 tag list of dicts. +### get_ec2_security_group_ids_from_names + +Pass this function a list of security group names or combination of security group names and IDs and this function will +return a list of IDs. You should also pass the VPC ID if known because security group names are not necessarily unique +across VPCs. \ No newline at end of file From bb68df525c78c48951cd925b5982c9dff45222fb Mon Sep 17 00:00:00 2001 From: Robin Roth Date: Wed, 4 May 2016 07:47:47 +0200 Subject: [PATCH 1503/2522] refactor zypper module * refactor zypper module Cleanup: * remove mention of old_zypper (no longer supported) * requirement goes up to zypper 1.0, SLES 11.0, openSUSE 11.1 * allows to use newer features (xml output) * already done for zypper_repository * use zypper instead of rpm to get old version information, based on work by @jasonmader * don't use rpm, zypper can do everything itself * run zypper only twice, first to determine current state, then to apply changes New features: * determine change by parsing zypper xmlout * determine failure by checking return code * allow simulataneous installation/removal of packages (using '-' and '+' prefix) * allows to swap out alternatives without removing packages depending on them * implement checkmode, using zypper --dry-run * implement diffmode * implement 'name=* state=latest' and 'name=* state=latest type=patch' * add force parameter, handed to zypper to allow downgrade or change of vendor/architecture Fixes/Replaces: * fixes #1627, give changed=False on installed patches * fixes #2094, handling URLs for packages * fixes #1461, fixes #546, allow state=latest name='*' * fixes #299, changed=False on second install, actually this was fixed earlier, but it is explicitly tested now * fixes #1824, add type=application * fixes #1256, install rpm from path, this is done by passing URLs and paths directly to zypper * fix typo in package_update_all * minor fixes * remove commented code block * bump version added to 2.2 * deal with zypper return codes 103 and 106 --- packaging/os/zypper.py | 425 +++++++++++++++++++++++------------------ 1 file changed, 237 insertions(+), 188 deletions(-) diff --git a/packaging/os/zypper.py b/packaging/os/zypper.py index 43e8919d5a2..30d69aa03d8 100644 --- a/packaging/os/zypper.py +++ b/packaging/os/zypper.py @@ -26,7 +26,7 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -import re +from xml.dom.minidom import parseString as parseXML DOCUMENTATION = ''' --- @@ -34,6 +34,8 @@ author: - "Patrick Callahan (@dirtyharrycallahan)" - "Alexander Gubin (@alxgu)" + - "Thomas O'Donnell (@andytom)" + - "Robin Roth (@robinro)" version_added: "1.2" short_description: Manage packages on SUSE and openSUSE description: @@ -41,7 +43,7 @@ options: name: description: - - package name or package specifier with version C(name) or C(name-1.0). You can also pass a url or a local path to a rpm file. + - package name or package specifier with version C(name) or C(name-1.0). You can also pass a url or a local path to a rpm file. When using state=latest, this can be '*', which updates all installed packages. required: true aliases: [ 'pkg' ] state: @@ -56,7 +58,7 @@ description: - The type of package to be operated on. required: false - choices: [ package, patch, pattern, product, srcpackage ] + choices: [ package, patch, pattern, product, srcpackage, application ] default: "package" version_added: "2.0" disable_gpg_check: @@ -67,7 +69,6 @@ required: false default: "no" choices: [ "yes", "no" ] - aliases: [] disable_recommends: version_added: "1.8" description: @@ -75,10 +76,18 @@ required: false default: "yes" choices: [ "yes", "no" ] + force: + version_added: "2.2" + description: + - Adds C(--force) option to I(zypper). Allows to downgrade packages and change vendor or architecture. + required: false + default: "no" + choices: [ "yes", "no" ] -notes: [] # informational: requirements for nodes -requirements: [ zypper, rpm ] +requirements: + - "zypper >= 1.0 # included in openSuSE >= 11.1 or SuSE Linux Enterprise Server/Desktop >= 11.0" + - rpm ''' EXAMPLES = ''' @@ -88,6 +97,9 @@ # Install apache2 with recommended packages - zypper: name=apache2 state=present disable_recommends=no +# Apply a given patch +- zypper: name=openSUSE-2016-128 state=present type=patch + # Remove the "nmap" package - zypper: name=nmap state=absent @@ -96,168 +108,222 @@ # Install local rpm file - zypper: name=/tmp/fancy-software.rpm state=present + +# Update all packages +- zypper: name=* state=latest + +# Apply all available patches +- zypper: name=* state=latest type=patch ''' -# Function used for getting zypper version -def zypper_version(module): - """Return (rc, message) tuple""" - cmd = ['/usr/bin/zypper', '-V'] - rc, stdout, stderr = module.run_command(cmd, check_rc=False) - if rc == 0: - return rc, stdout - else: - return rc, stderr -# Function used for getting versions of currently installed packages. -def get_current_version(m, packages): - cmd = ['/bin/rpm', '-q', '--qf', '%{NAME} %{VERSION}-%{RELEASE}\n'] - cmd.extend(packages) +def get_want_state(m, names, remove=False): + packages_install = [] + packages_remove = [] + urls = [] + for name in names: + if '://' in name or name.endswith('.rpm'): + urls.append(name) + elif name.startswith('-') or name.startswith('~'): + packages_remove.append(name[1:]) + elif name.startswith('+'): + packages_install.append(name[1:]) + else: + if remove: + packages_remove.append(name) + else: + packages_install.append(name) + return packages_install, packages_remove, urls - rc, stdout, stderr = m.run_command(cmd, check_rc=False) - current_version = {} - rpmoutput_re = re.compile('^(\S+) (\S+)$') - - for stdoutline in stdout.splitlines(): - match = rpmoutput_re.match(stdoutline) - if match == None: - return None - package = match.group(1) - version = match.group(2) - current_version[package] = version - - for package in packages: - if package not in current_version: - print package + ' was not returned by rpm \n' - return None - - return current_version - - -# Function used to find out if a package is currently installed. -def get_package_state(m, packages): - for i in range(0, len(packages)): - # Check state of a local rpm-file - if ".rpm" in packages[i]: - # Check if rpm file is available - package = packages[i] - if not os.path.isfile(package) and not '://' in package: - stderr = "No Package file matching '%s' found on system" % package - m.fail_json(msg=stderr, rc=1) - # Get packagename from rpm file - cmd = ['/bin/rpm', '--query', '--qf', '%{NAME}', '--package'] - cmd.append(package) - rc, stdout, stderr = m.run_command(cmd, check_rc=False) - packages[i] = stdout - - cmd = ['/bin/rpm', '--query', '--qf', 'package %{NAME} is installed\n'] +def get_installed_state(m, packages): + "get installed state of packages" + + cmd = get_cmd(m, 'search') + cmd.extend(['--match-exact', '--verbose', '--installed-only']) cmd.extend(packages) + return parse_zypper_xml(m, cmd, fail_not_found=False)[0] + +def parse_zypper_xml(m, cmd, fail_not_found=True, packages=None): rc, stdout, stderr = m.run_command(cmd, check_rc=False) - installed_state = {} - rpmoutput_re = re.compile('^package (\S+) (.*)$') - for stdoutline in stdout.splitlines(): - match = rpmoutput_re.match(stdoutline) - if match == None: - continue - package = match.group(1) - result = match.group(2) - if result == 'is installed': - installed_state[package] = True + dom = parseXML(stdout) + if rc == 104: + # exit code 104 is ZYPPER_EXIT_INF_CAP_NOT_FOUND (no packages found) + if fail_not_found: + errmsg = dom.getElementsByTagName('message')[-1].childNodes[0].data + m.fail_json(msg=errmsg, rc=rc, stdout=stdout, stderr=stderr, cmd=cmd) else: - installed_state[package] = False - - return installed_state - -# Function used to make sure a package is present. -def package_present(m, name, installed_state, package_type, disable_gpg_check, disable_recommends, old_zypper): - packages = [] - for package in name: - if package not in installed_state or installed_state[package] is False: - packages.append(package) - if len(packages) != 0: - cmd = ['/usr/bin/zypper', '--non-interactive'] - # add global options before zypper command - if disable_gpg_check: - cmd.append('--no-gpg-checks') - cmd.extend(['install', '--auto-agree-with-licenses', '-t', package_type]) - # add install parameter - if disable_recommends and not old_zypper: - cmd.append('--no-recommends') - cmd.extend(packages) - rc, stdout, stderr = m.run_command(cmd, check_rc=False) + return {}, rc, stdout, stderr + elif rc in [0, 106, 103]: + # zypper exit codes + # 0: success + # 106: signature verification failed + # 103: zypper was upgraded, run same command again + if packages is None: + firstrun = True + packages = {} + solvable_list = dom.getElementsByTagName('solvable') + for solvable in solvable_list: + name = solvable.getAttribute('name') + packages[name] = {} + packages[name]['version'] = solvable.getAttribute('edition') + packages[name]['oldversion'] = solvable.getAttribute('edition-old') + status = solvable.getAttribute('status') + packages[name]['installed'] = status == "installed" + packages[name]['group'] = solvable.parentNode.nodeName + if rc == 103 and firstrun: + # if this was the first run and it failed with 103 + # run zypper again with the same command to complete update + return parse_zypper_xml(m, cmd, fail_not_found=fail_not_found, packages=packages) + + return packages, rc, stdout, stderr + m.fail_json(msg='Zypper run command failed with return code %s.'%rc, rc=rc, stdout=stdout, stderr=stderr, cmd=cmd) + + +def get_cmd(m, subcommand): + "puts together the basic zypper command arguments with those passed to the module" + is_install = subcommand in ['install', 'update', 'patch'] + cmd = ['/usr/bin/zypper', '--quiet', '--non-interactive', '--xmlout'] + + # add global options before zypper command + if is_install and m.params['disable_gpg_check']: + cmd.append('--no-gpg-checks') - if rc == 0: - changed=True - else: - changed=False + cmd.append(subcommand) + if subcommand != 'patch': + cmd.extend(['--type', m.params['type']]) + if m.check_mode and subcommand != 'search': + cmd.append('--dry-run') + if is_install: + cmd.append('--auto-agree-with-licenses') + if m.params['disable_recommends']: + cmd.append('--no-recommends') + if m.params['force']: + cmd.append('--force') + return cmd + + +def set_diff(m, retvals, result): + packages = {'installed': [], 'removed': [], 'upgraded': []} + for p in result: + group = result[p]['group'] + if group == 'to-upgrade': + versions = ' (' + result[p]['oldversion'] + ' => ' + result[p]['version'] + ')' + packages['upgraded'].append(p + versions) + elif group == 'to-install': + packages['installed'].append(p) + elif group == 'to-remove': + packages['removed'].append(p) + + output = '' + for state in packages: + if packages[state]: + output += state + ': ' + ', '.join(packages[state]) + '\n' + if 'diff' not in retvals: + retvals['diff'] = {} + if 'prepared' not in retvals['diff']: + retvals['diff']['prepared'] = output else: - rc = 0 - stdout = '' - stderr = '' - changed=False - - return (rc, stdout, stderr, changed) - -# Function used to make sure a package is the latest available version. -def package_latest(m, name, installed_state, package_type, disable_gpg_check, disable_recommends, old_zypper): - - # first of all, make sure all the packages are installed - (rc, stdout, stderr, changed) = package_present(m, name, installed_state, package_type, disable_gpg_check, disable_recommends, old_zypper) - - # return if an error occured while installation - # otherwise error messages will be lost and user doesn`t see any error - if rc: - return (rc, stdout, stderr, changed) + retvals['diff']['prepared'] += '\n' + output + + +def package_present(m, name, want_latest): + "install and update (if want_latest) the packages in name_install, while removing the packages in name_remove" + retvals = {'rc': 0, 'stdout': '', 'stderr': '', 'changed': False, 'failed': False} + name_install, name_remove, urls = get_want_state(m, name) + + if not want_latest: + # for state=present: filter out already installed packages + prerun_state = get_installed_state(m, name_install + name_remove) + # generate lists of packages to install or remove + name_install = [p for p in name_install if p not in prerun_state] + name_remove = [p for p in name_remove if p in prerun_state] + if not name_install and not name_remove and not urls: + # nothing to install/remove and nothing to update + return retvals + + # zypper install also updates packages + cmd = get_cmd(m, 'install') + cmd.append('--') + cmd.extend(urls) + + # allow for + or - prefixes in install/remove lists + # do this in one zypper run to allow for dependency-resolution + # for example "-exim postfix" runs without removing packages depending on mailserver + cmd.extend(name_install) + cmd.extend(['-%s' % p for p in name_remove]) + + retvals['cmd'] = cmd + result, retvals['rc'], retvals['stdout'], retvals['stderr'] = parse_zypper_xml(m, cmd) + if retvals['rc'] == 0: + # installed all packages successfully + # checking the output is not straight-forward because zypper rewrites 'capabilities' + # could run get_installed_state and recheck, but this takes time + if result: + retvals['changed'] = True + else: + retvals['failed'] = True + # return retvals + if m._diff: + set_diff(m, retvals, result) - # if we've already made a change, we don't have to check whether a version changed - if not changed: - pre_upgrade_versions = get_current_version(m, name) + return retvals - cmd = ['/usr/bin/zypper', '--non-interactive'] - if disable_gpg_check: - cmd.append('--no-gpg-checks') - - if old_zypper: - cmd.extend(['install', '--auto-agree-with-licenses', '-t', package_type]) +def package_update_all(m, do_patch): + "run update or patch on all available packages" + retvals = {'rc': 0, 'stdout': '', 'stderr': '', 'changed': False, 'failed': False} + if do_patch: + cmdname = 'patch' + else: + cmdname = 'update' + + cmd = get_cmd(m, cmdname) + retvals['cmd'] = cmd + result, retvals['rc'], retvals['stdout'], retvals['stderr'] = parse_zypper_xml(m, cmd) + if retvals['rc'] == 0: + if result: + retvals['changed'] = True else: - cmd.extend(['update', '--auto-agree-with-licenses', '-t', package_type]) + retvals['failed'] = True + if m._diff: + set_diff(m, retvals, result) + return retvals - cmd.extend(name) - rc, stdout, stderr = m.run_command(cmd, check_rc=False) - # if we've already made a change, we don't have to check whether a version changed - if not changed: - post_upgrade_versions = get_current_version(m, name) - if pre_upgrade_versions != post_upgrade_versions: - changed = True - - return (rc, stdout, stderr, changed) - -# Function used to make sure a package is not installed. -def package_absent(m, name, installed_state, package_type, old_zypper): - packages = [] - for package in name: - if package not in installed_state or installed_state[package] is True: - packages.append(package) - if len(packages) != 0: - cmd = ['/usr/bin/zypper', '--non-interactive', 'remove', '-t', package_type] - cmd.extend(packages) - rc, stdout, stderr = m.run_command(cmd) - - if rc == 0: - changed=True - else: - changed=False +def package_absent(m, name): + "remove the packages in name" + retvals = {'rc': 0, 'stdout': '', 'stderr': '', 'changed': False, 'failed': False} + # Get package state + name_install, name_remove, urls = get_want_state(m, name, remove=True) + if name_install: + m.fail_json(msg="Can not combine '+' prefix with state=remove/absent.") + if urls: + m.fail_json(msg="Can not remove via URL.") + if m.params['type'] == 'patch': + m.fail_json(msg="Can not remove patches.") + prerun_state = get_installed_state(m, name_remove) + name_remove = [p for p in name_remove if p in prerun_state] + if not name_remove: + return retvals + + cmd = get_cmd(m, 'remove') + cmd.extend(name_remove) + + retvals['cmd'] = cmd + result, retvals['rc'], retvals['stdout'], retvals['stderr'] = parse_zypper_xml(m, cmd) + if retvals['rc'] == 0: + # removed packages successfully + if result: + retvals['changed'] = True else: - rc = 0 - stdout = '' - stderr = '' - changed=False + retvals['failed'] = True + if m._diff: + set_diff(m, retvals, result) - return (rc, stdout, stderr, changed) + return retvals # =========================================== # Main control flow @@ -267,57 +333,40 @@ def main(): argument_spec = dict( name = dict(required=True, aliases=['pkg'], type='list'), state = dict(required=False, default='present', choices=['absent', 'installed', 'latest', 'present', 'removed']), - type = dict(required=False, default='package', choices=['package', 'patch', 'pattern', 'product', 'srcpackage']), + type = dict(required=False, default='package', choices=['package', 'patch', 'pattern', 'product', 'srcpackage', 'application']), disable_gpg_check = dict(required=False, default='no', type='bool'), disable_recommends = dict(required=False, default='yes', type='bool'), + force = dict(required=False, default='no', type='bool'), ), - supports_check_mode = False + supports_check_mode = True ) + name = module.params['name'] + state = module.params['state'] - params = module.params - - name = params['name'] - state = params['state'] - type_ = params['type'] - disable_gpg_check = params['disable_gpg_check'] - disable_recommends = params['disable_recommends'] - - rc = 0 - stdout = '' - stderr = '' - result = {} - result['name'] = name - result['state'] = state - - rc, out = zypper_version(module) - match = re.match(r'zypper\s+(\d+)\.(\d+)\.(\d+)', out) - if not match or int(match.group(1)) > 0: - old_zypper = False + # Perform requested action + if name == ['*'] and state == 'latest': + if module.params['type'] == 'package': + retvals = package_update_all(module, False) + elif module.params['type'] == 'patch': + retvals = package_update_all(module, True) else: - old_zypper = True + if state in ['absent', 'removed']: + retvals = package_absent(module, name) + elif state in ['installed', 'present', 'latest']: + retvals = package_present(module, name, state == 'latest') - # Get package state - installed_state = get_package_state(module, name) + failed = retvals['failed'] + del retvals['failed'] - # Perform requested action - if state in ['installed', 'present']: - (rc, stdout, stderr, changed) = package_present(module, name, installed_state, type_, disable_gpg_check, disable_recommends, old_zypper) - elif state in ['absent', 'removed']: - (rc, stdout, stderr, changed) = package_absent(module, name, installed_state, type_, old_zypper) - elif state == 'latest': - (rc, stdout, stderr, changed) = package_latest(module, name, installed_state, type_, disable_gpg_check, disable_recommends, old_zypper) - - if rc != 0: - if stderr: - module.fail_json(msg=stderr, rc=rc) - else: - module.fail_json(msg=stdout, rc=rc) + if failed: + module.fail_json(msg="Zypper run failed.", **retvals) - result['changed'] = changed - result['rc'] = rc + if not retvals['changed']: + del retvals['stdout'] + del retvals['stderr'] - module.exit_json(**result) + module.exit_json(name=name, state=state, **retvals) # import module snippets from ansible.module_utils.basic import * From c4005983329d92f34af3cad427ea7a748836de80 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Tue, 3 May 2016 22:50:45 -0700 Subject: [PATCH 1504/2522] Reverse the unpack list operation Instead of doing an unpack, deliberately specify which parameters you want to use. This allows us to flexibly add more parameters to the f5_argument_spec without having to rewrite all the modules that use it. Functionally this commit changes nothing, it just provides for a different way of accessing the parameters to the module --- network/f5/bigip_monitor_http.py | 15 ++++++++++++++- network/f5/bigip_monitor_tcp.py | 17 +++++++++++++++-- network/f5/bigip_node.py | 17 +++++++++++++++-- network/f5/bigip_pool.py | 15 ++++++++++++++- network/f5/bigip_pool_member.py | 18 ++++++++++++++++-- network/f5/bigip_virtual_server.py | 16 +++++++++++++++- 6 files changed, 89 insertions(+), 9 deletions(-) diff --git a/network/f5/bigip_monitor_http.py b/network/f5/bigip_monitor_http.py index 166b7ae32e0..dcdaf4b65b6 100644 --- a/network/f5/bigip_monitor_http.py +++ b/network/f5/bigip_monitor_http.py @@ -317,7 +317,20 @@ def main(): supports_check_mode=True ) - (server,user,password,state,partition,validate_certs) = f5_parse_arguments(module) + if not bigsuds_found: + module.fail_json(msg="the python bigsuds module is required") + + if module.params['validate_certs']: + import ssl + if not hasattr(ssl, 'SSLContext'): + module.fail_json(msg='bigsuds does not support verifying certificates with python < 2.7.9. Either update python or set validate_certs=False on the task') + + server = module.params['server'] + user = module.params['user'] + password = module.params['password'] + state = module.params['state'] + partition = module.params['partition'] + validate_certs = module.params['validate_certs'] parent_partition = module.params['parent_partition'] name = module.params['name'] diff --git a/network/f5/bigip_monitor_tcp.py b/network/f5/bigip_monitor_tcp.py index b06c8978106..0ef6a9add26 100644 --- a/network/f5/bigip_monitor_tcp.py +++ b/network/f5/bigip_monitor_tcp.py @@ -315,7 +315,7 @@ def set_ipport(api, monitor, ipport): def main(): # begin monitor specific stuff - argument_spec=f5_argument_spec(); + argument_spec=f5_argument_spec() argument_spec.update(dict( name = dict(required=True), type = dict(default=DEFAULT_TEMPLATE_TYPE_CHOICE, choices=TEMPLATE_TYPE_CHOICES), @@ -336,7 +336,20 @@ def main(): supports_check_mode=True ) - (server,user,password,state,partition,validate_certs) = f5_parse_arguments(module) + if not bigsuds_found: + module.fail_json(msg="the python bigsuds module is required") + + if module.params['validate_certs']: + import ssl + if not hasattr(ssl, 'SSLContext'): + module.fail_json(msg='bigsuds does not support verifying certificates with python < 2.7.9. Either update python or set validate_certs=False on the task') + + server = module.params['server'] + user = module.params['user'] + password = module.params['password'] + state = module.params['state'] + partition = module.params['partition'] + validate_certs = module.params['validate_certs'] parent_partition = module.params['parent_partition'] name = module.params['name'] diff --git a/network/f5/bigip_node.py b/network/f5/bigip_node.py index c6c32a38858..bc209af6ae6 100644 --- a/network/f5/bigip_node.py +++ b/network/f5/bigip_node.py @@ -263,7 +263,7 @@ def get_node_monitor_status(api, name): def main(): - argument_spec=f5_argument_spec(); + argument_spec=f5_argument_spec() argument_spec.update(dict( session_state = dict(type='str', choices=['enabled', 'disabled']), monitor_state = dict(type='str', choices=['enabled', 'disabled']), @@ -278,7 +278,20 @@ def main(): supports_check_mode=True ) - (server,user,password,state,partition,validate_certs) = f5_parse_arguments(module) + if not bigsuds_found: + module.fail_json(msg="the python bigsuds module is required") + + if module.params['validate_certs']: + import ssl + if not hasattr(ssl, 'SSLContext'): + module.fail_json(msg='bigsuds does not support verifying certificates with python < 2.7.9. Either update python or set validate_certs=False on the task') + + server = module.params['server'] + user = module.params['user'] + password = module.params['password'] + state = module.params['state'] + partition = module.params['partition'] + validate_certs = module.params['validate_certs'] session_state = module.params['session_state'] monitor_state = module.params['monitor_state'] diff --git a/network/f5/bigip_pool.py b/network/f5/bigip_pool.py index 85fbc5b9c0d..a8dafb3bbef 100644 --- a/network/f5/bigip_pool.py +++ b/network/f5/bigip_pool.py @@ -367,7 +367,20 @@ def main(): supports_check_mode=True ) - (server,user,password,state,partition,validate_certs) = f5_parse_arguments(module) + if not bigsuds_found: + module.fail_json(msg="the python bigsuds module is required") + + if module.params['validate_certs']: + import ssl + if not hasattr(ssl, 'SSLContext'): + module.fail_json(msg='bigsuds does not support verifying certificates with python < 2.7.9. Either update python or set validate_certs=False on the task') + + server = module.params['server'] + user = module.params['user'] + password = module.params['password'] + state = module.params['state'] + partition = module.params['partition'] + validate_certs = module.params['validate_certs'] name = module.params['name'] pool = fq_name(partition,name) diff --git a/network/f5/bigip_pool_member.py b/network/f5/bigip_pool_member.py index 50abef420e7..9f0965cbd84 100644 --- a/network/f5/bigip_pool_member.py +++ b/network/f5/bigip_pool_member.py @@ -314,7 +314,7 @@ def get_member_monitor_status(api, pool, address, port): return result def main(): - argument_spec = f5_argument_spec(); + argument_spec = f5_argument_spec() argument_spec.update(dict( session_state = dict(type='str', choices=['enabled', 'disabled']), monitor_state = dict(type='str', choices=['enabled', 'disabled']), @@ -334,7 +334,21 @@ def main(): supports_check_mode=True ) - (server,user,password,state,partition,validate_certs) = f5_parse_arguments(module) + if not bigsuds_found: + module.fail_json(msg="the python bigsuds module is required") + + if module.params['validate_certs']: + import ssl + if not hasattr(ssl, 'SSLContext'): + module.fail_json(msg='bigsuds does not support verifying certificates with python < 2.7.9. Either update python or set validate_certs=False on the task') + + server = module.params['server'] + user = module.params['user'] + password = module.params['password'] + state = module.params['state'] + partition = module.params['partition'] + validate_certs = module.params['validate_certs'] + session_state = module.params['session_state'] monitor_state = module.params['monitor_state'] pool = fq_name(partition, module.params['pool']) diff --git a/network/f5/bigip_virtual_server.py b/network/f5/bigip_virtual_server.py index b654e5f84f8..891ff5ecd34 100644 --- a/network/f5/bigip_virtual_server.py +++ b/network/f5/bigip_virtual_server.py @@ -378,7 +378,21 @@ def main(): supports_check_mode=True ) - (server,user,password,state,partition,validate_certs) = f5_parse_arguments(module) + if not bigsuds_found: + module.fail_json(msg="the python bigsuds module is required") + + if module.params['validate_certs']: + import ssl + if not hasattr(ssl, 'SSLContext'): + module.fail_json(msg='bigsuds does not support verifying certificates with python < 2.7.9. Either update python or set validate_certs=False on the task') + + server = module.params['server'] + user = module.params['user'] + password = module.params['password'] + state = module.params['state'] + partition = module.params['partition'] + validate_certs = module.params['validate_certs'] + name = fq_name(partition,module.params['name']) destination=module.params['destination'] port=module.params['port'] From 806ebef383ae418c47da5349cda15aa1447213bc Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Wed, 4 May 2016 08:29:46 +0200 Subject: [PATCH 1505/2522] homebrew_cask: fix doc version_added for install_options See #2086 --- packaging/os/homebrew_cask.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/os/homebrew_cask.py b/packaging/os/homebrew_cask.py index 7ebe84300c0..7b16b71f187 100755 --- a/packaging/os/homebrew_cask.py +++ b/packaging/os/homebrew_cask.py @@ -44,7 +44,7 @@ - options flags to install a package required: false default: null - version_added: "2.1" + version_added: "2.2" ''' EXAMPLES = ''' - homebrew_cask: name=alfred state=present From 46fb2f8d14596d3cabea44efd7335333d0083ac0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Thu, 5 May 2016 11:11:36 +0200 Subject: [PATCH 1506/2522] cs_template: fix missing docs (#2165) --- cloud/cloudstack/cs_template.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/cloud/cloudstack/cs_template.py b/cloud/cloudstack/cs_template.py index e53c8e286e4..ebc0a4ba803 100644 --- a/cloud/cloudstack/cs_template.py +++ b/cloud/cloudstack/cs_template.py @@ -93,6 +93,23 @@ - Only used if C(state) is present or absent. required: false default: false + mode: + description: + - Mode for the template extraction. + - Only used if C(state=extracted). + required: false + default: 'http_download' + choices: [ 'http_download', 'ftp_upload' ] + domain: + description: + - Domain the template, snapshot or VM is related to. + required: false + default: null + account: + description: + - Account the template, snapshot or VM is related to. + required: false + default: null project: description: - Name of the project the template to be registered in. From cfc6d1cf62ac39d739f773e7a76879e6079b59ee Mon Sep 17 00:00:00 2001 From: Lakshmi Balu Date: Thu, 5 May 2016 03:24:30 -0700 Subject: [PATCH 1507/2522] Update vmware_datacenter.py (#2164) Fixed the syntac issue variable references before definiton --- cloud/vmware/vmware_datacenter.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cloud/vmware/vmware_datacenter.py b/cloud/vmware/vmware_datacenter.py index 77685616e51..ef2fd2f1f73 100644 --- a/cloud/vmware/vmware_datacenter.py +++ b/cloud/vmware/vmware_datacenter.py @@ -91,6 +91,7 @@ def create_datacenter(context, module): try: datacenter = get_datacenter(context, module) + changed = False if not datacenter: changed = True if not module.check_mode: @@ -114,6 +115,7 @@ def destroy_datacenter(context, module): try: datacenter = get_datacenter(context, module) + changed = False if datacenter: changed = True if not module.check_mode: From 72a4185edcc5bd6b940956aa43d664b19f4adf47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Thu, 5 May 2016 12:27:38 +0200 Subject: [PATCH 1508/2522] travis: exlcude cs_template to fix build failure (#2168) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4dc70de2f64..00811615031 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,5 +20,5 @@ script: - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py' . - python2.6 -m compileall -fq . - python2.7 -m compileall -fq . - - ansible-validate-modules --exclude 'cloud/centurylink/clc_aa_policy\.py|cloud/centurylink/clc_alert_policy\.py|cloud/centurylink/clc_blueprint_package\.py|cloud/centurylink/clc_firewall_policy\.py|cloud/centurylink/clc_group\.py|cloud/centurylink/clc_loadbalancer\.py|cloud/centurylink/clc_modify_server\.py|cloud/centurylink/clc_publicip\.py|cloud/centurylink/clc_server\.py|cloud/centurylink/clc_server_snapshot\.py|cloud/docker/docker_login\.py|messaging/rabbitmq_binding\.py|messaging/rabbitmq_exchange\.py|messaging/rabbitmq_queue\.py|monitoring/circonus_annotation\.py|network/snmp_facts\.py|notification/sns\.py' . + - ansible-validate-modules --exclude 'cloud/cloudstack/cs_template\.py|cloud/centurylink/clc_aa_policy\.py|cloud/centurylink/clc_alert_policy\.py|cloud/centurylink/clc_blueprint_package\.py|cloud/centurylink/clc_firewall_policy\.py|cloud/centurylink/clc_group\.py|cloud/centurylink/clc_loadbalancer\.py|cloud/centurylink/clc_modify_server\.py|cloud/centurylink/clc_publicip\.py|cloud/centurylink/clc_server\.py|cloud/centurylink/clc_server_snapshot\.py|cloud/docker/docker_login\.py|messaging/rabbitmq_binding\.py|messaging/rabbitmq_exchange\.py|messaging/rabbitmq_queue\.py|monitoring/circonus_annotation\.py|network/snmp_facts\.py|notification/sns\.py|cloud/cloudstack/cs_template\.py' . #- ./test-docs.sh extras From 77eee2b6cac05f2cb182c5c15adecc24d96b0c53 Mon Sep 17 00:00:00 2001 From: Maxime Montinet Date: Thu, 5 May 2016 14:43:36 +0200 Subject: [PATCH 1509/2522] rabbitmq_user: Properly initialize _permissions (#2163) Fixes #2162 --- messaging/rabbitmq_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messaging/rabbitmq_user.py b/messaging/rabbitmq_user.py index 85921ce45c7..2e902eaa696 100644 --- a/messaging/rabbitmq_user.py +++ b/messaging/rabbitmq_user.py @@ -139,7 +139,7 @@ def __init__(self, module, username, password, tags, permissions, self.bulk_permissions = bulk_permissions self._tags = None - self._permissions = None + self._permissions = [] self._rabbitmqctl = module.get_bin_path('rabbitmqctl', True) def _exec(self, args, run_in_check_mode=False): From 959bbfbf53f0a5480d0144b621f26eeda4d20f12 Mon Sep 17 00:00:00 2001 From: Rob Date: Fri, 6 May 2016 08:26:40 +1000 Subject: [PATCH 1510/2522] Add secondary IP support and allow specifying sec groups by name (#2161) --- cloud/amazon/ec2_eni.py | 146 +++++++++++++++++++++++++++++++--------- 1 file changed, 115 insertions(+), 31 deletions(-) diff --git a/cloud/amazon/ec2_eni.py b/cloud/amazon/ec2_eni.py index 30d05f85554..58f1b0caf72 100644 --- a/cloud/amazon/ec2_eni.py +++ b/cloud/amazon/ec2_eni.py @@ -20,7 +20,7 @@ description: - Create and optionally attach an Elastic Network Interface (ENI) to an instance. If an ENI ID is provided, an attempt is made to update the existing ENI. By passing 'None' as the instance_id, an ENI can be detached from an instance. version_added: "2.0" -author: Rob White, wimnat [at] gmail.com, @wimnat +author: "Rob White (@wimnat)" options: eni_id: description: @@ -48,7 +48,8 @@ default: null security_groups: description: - - List of security groups associated with the interface. Only used when state=present. + - List of security groups associated with the interface. Only used when state=present. Since version 2.2, you \ + can specify security groups by ID or by name or a combination of both. Prior to 2.2, you can specify only by ID. required: false default: null state: @@ -75,6 +76,16 @@ description: - By default, interfaces perform source/destination checks. NAT instances however need this check to be disabled. You can only specify this flag when the interface is being modified, not on creation. required: false + secondary_private_ip_addresses: + description: + - A list of IP addresses to assign as secondary IP addresses to the network interface. This option is mutually exclusive of secondary_private_ip_address_count + required: false + version_added: 2.2 + secondary_private_ip_address_count: + description: + - The number of secondary IP addresses to assign to the network interface. This option is mutually exclusive of secondary_private_ip_addresses + required: false + version_added: 2.2 extends_documentation_fragment: - aws - ec2 @@ -97,6 +108,29 @@ subnet_id: subnet-xxxxxxxx state: present +# Create an ENI with two secondary addresses +- ec2_eni: + subnet_id: subnet-xxxxxxxx + state: present + secondary_private_ip_address_count: 2 + +# Assign a secondary IP address to an existing ENI +# This will purge any existing IPs +- ec2_eni: + subnet_id: subnet-xxxxxxxx + eni_id: eni-yyyyyyyy + state: present + secondary_private_ip_addresses: + - 172.16.1.1 + +# Remove any secondary IP addresses from an existing ENI +- ec2_eni: + subnet_id: subnet-xxxxxxxx + eni_id: eni-yyyyyyyy + state: present + secondary_private_ip_addresses: + - + # Destroy an ENI, detaching it from any instance if necessary - ec2_eni: eni_id: eni-xxxxxxx @@ -133,26 +167,24 @@ ''' import time -import xml.etree.ElementTree as ET import re try: import boto.ec2 + import boto.vpc from boto.exception import BotoServerError HAS_BOTO = True except ImportError: HAS_BOTO = False -def get_error_message(xml_string): - - root = ET.fromstring(xml_string) - for message in root.findall('.//Message'): - return message.text - - def get_eni_info(interface): + # Private addresses + private_addresses = [] + for ip in interface.private_ip_addresses: + private_addresses.append({ 'private_ip_address': ip.private_ip_address, 'primary_address': ip.primary }) + interface_info = {'id': interface.id, 'subnet_id': interface.subnet_id, 'vpc_id': interface.vpc_id, @@ -163,6 +195,7 @@ def get_eni_info(interface): 'private_ip_address': interface.private_ip_address, 'source_dest_check': interface.source_dest_check, 'groups': dict((group.id, group.name) for group in interface.groups), + 'private_ip_addresses': private_addresses } if interface.attachment is not None: @@ -176,6 +209,7 @@ def get_eni_info(interface): return interface_info + def wait_for_eni(eni, status): while True: @@ -190,7 +224,7 @@ def wait_for_eni(eni, status): break -def create_eni(connection, module): +def create_eni(connection, vpc_id, module): instance_id = module.params.get("instance_id") if instance_id == 'None': @@ -199,7 +233,9 @@ def create_eni(connection, module): subnet_id = module.params.get('subnet_id') private_ip_address = module.params.get('private_ip_address') description = module.params.get('description') - security_groups = module.params.get('security_groups') + security_groups = get_ec2_security_group_ids_from_names(module.params.get('security_groups'), connection, vpc_id=vpc_id, boto3=False) + secondary_private_ip_addresses = module.params.get("secondary_private_ip_addresses") + secondary_private_ip_address_count = module.params.get("secondary_private_ip_address_count") changed = False try: @@ -215,15 +251,30 @@ def create_eni(connection, module): # Wait to allow creation / attachment to finish wait_for_eni(eni, "attached") eni.update() + + if secondary_private_ip_address_count is not None: + try: + connection.assign_private_ip_addresses(network_interface_id=eni.id, secondary_private_ip_address_count=secondary_private_ip_address_count) + except BotoServerError: + eni.delete() + raise + + if secondary_private_ip_addresses is not None: + try: + connection.assign_private_ip_addresses(network_interface_id=eni.id, private_ip_addresses=secondary_private_ip_addresses) + except BotoServerError: + eni.delete() + raise + changed = True except BotoServerError as e: - module.fail_json(msg=get_error_message(e.args[2])) + module.fail_json(msg=e.message) module.exit_json(changed=changed, interface=get_eni_info(eni)) -def modify_eni(connection, module): +def modify_eni(connection, vpc_id, module): eni_id = module.params.get("eni_id") instance_id = module.params.get("instance_id") @@ -234,10 +285,12 @@ def modify_eni(connection, module): do_detach = False device_index = module.params.get("device_index") description = module.params.get('description') - security_groups = module.params.get('security_groups') + security_groups = get_ec2_security_group_ids_from_names(module.params.get('security_groups'), connection, vpc_id=vpc_id, boto3=False) force_detach = module.params.get("force_detach") source_dest_check = module.params.get("source_dest_check") delete_on_termination = module.params.get("delete_on_termination") + secondary_private_ip_addresses = module.params.get("secondary_private_ip_addresses") + secondary_private_ip_address_count = module.params.get("secondary_private_ip_address_count") changed = False try: @@ -263,6 +316,24 @@ def modify_eni(connection, module): changed = True else: module.fail_json(msg="Can not modify delete_on_termination as the interface is not attached") + + current_secondary_addresses = [i.private_ip_address for i in eni.private_ip_addresses if not i.primary] + if secondary_private_ip_addresses is not None: + secondary_addresses_to_remove = list(set(current_secondary_addresses) - set(secondary_private_ip_addresses)) + if secondary_addresses_to_remove: + connection.unassign_private_ip_addresses(network_interface_id=eni.id, private_ip_addresses=list(set(current_secondary_addresses) - set(secondary_private_ip_addresses)), dry_run=False) + connection.assign_private_ip_addresses(network_interface_id=eni.id, private_ip_addresses=secondary_private_ip_addresses, secondary_private_ip_address_count=None, allow_reassignment=False, dry_run=False) + if secondary_private_ip_address_count is not None: + current_secondary_address_count = len(current_secondary_addresses) + + if secondary_private_ip_address_count > current_secondary_address_count: + connection.assign_private_ip_addresses(network_interface_id=eni.id, private_ip_addresses=None, secondary_private_ip_address_count=(secondary_private_ip_address_count - current_secondary_address_count), allow_reassignment=False, dry_run=False) + changed = True + elif secondary_private_ip_address_count < current_secondary_address_count: + # How many of these addresses do we want to remove + secondary_addresses_to_remove_count = current_secondary_address_count - secondary_private_ip_address_count + connection.unassign_private_ip_addresses(network_interface_id=eni.id, private_ip_addresses=current_secondary_addresses[:secondary_addresses_to_remove_count], dry_run=False) + if eni.attachment is not None and instance_id is None and do_detach is True: eni.detach(force_detach) wait_for_eni(eni, "detached") @@ -274,8 +345,7 @@ def modify_eni(connection, module): changed = True except BotoServerError as e: - print e - module.fail_json(msg=get_error_message(e.args[2])) + module.fail_json(msg=e.message) eni.update() module.exit_json(changed=changed, interface=get_eni_info(eni)) @@ -304,12 +374,12 @@ def delete_eni(connection, module): module.exit_json(changed=changed) except BotoServerError as e: - msg = get_error_message(e.args[2]) regex = re.compile('The networkInterface ID \'.*\' does not exist') - if regex.search(msg) is not None: + if regex.search(e.message) is not None: module.exit_json(changed=False) else: - module.fail_json(msg=get_error_message(e.args[2])) + module.fail_json(msg=e.message) + def compare_eni(connection, module): @@ -328,10 +398,11 @@ def compare_eni(connection, module): return eni except BotoServerError as e: - module.fail_json(msg=get_error_message(e.args[2])) + module.fail_json(msg=e.message) return None + def get_sec_group_list(groups): # Build list of remote security groups @@ -342,6 +413,14 @@ def get_sec_group_list(groups): return remote_security_groups +def _get_vpc_id(conn, subnet_id): + + try: + return conn.get_all_subnets(subnet_ids=[subnet_id])[0].vpc_id + except BotoServerError as e: + module.fail_json(msg=e.message) + + def main(): argument_spec = ec2_argument_spec() argument_spec.update( @@ -356,11 +435,18 @@ def main(): state = dict(default='present', choices=['present', 'absent']), force_detach = dict(default='no', type='bool'), source_dest_check = dict(default=None, type='bool'), - delete_on_termination = dict(default=None, type='bool') + delete_on_termination = dict(default=None, type='bool'), + secondary_private_ip_addresses = dict(default=None, type='list'), + secondary_private_ip_address_count = dict(default=None, type='int') ) ) - module = AnsibleModule(argument_spec=argument_spec) + module = AnsibleModule(argument_spec=argument_spec, + required_if = ([ + ('state', 'present', ['subnet_id']), + ('state', 'absent', ['eni_id']), + ]) + ) if not HAS_BOTO: module.fail_json(msg='boto required for this module') @@ -370,6 +456,7 @@ def main(): if region: try: connection = connect_to_aws(boto.ec2, region, **aws_connect_params) + vpc_connection = connect_to_aws(boto.vpc, region, **aws_connect_params) except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: module.fail_json(msg=str(e)) else: @@ -379,17 +466,14 @@ def main(): eni_id = module.params.get("eni_id") if state == 'present': + subnet_id = module.params.get("subnet_id") + vpc_id = _get_vpc_id(vpc_connection, subnet_id) if eni_id is None: - if module.params.get("subnet_id") is None: - module.fail_json(msg="subnet_id must be specified when state=present") - create_eni(connection, module) + create_eni(connection, vpc_id, module) else: - modify_eni(connection, module) + modify_eni(connection, vpc_id, module) elif state == 'absent': - if eni_id is None: - module.fail_json(msg="eni_id must be specified") - else: - delete_eni(connection, module) + delete_eni(connection, module) from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * From f193c1b96a6ba5f20d0c50ff4513b3ce99b4ed32 Mon Sep 17 00:00:00 2001 From: Kamil Szczygiel Date: Tue, 12 Apr 2016 14:56:46 +0200 Subject: [PATCH 1511/2522] influxdb database module --- .travis.yml | 2 +- database/influxdb/__init__.py | 0 database/influxdb/influxdb_database.py | 194 +++++++++++++++++++++++++ 3 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 database/influxdb/__init__.py create mode 100644 database/influxdb/influxdb_database.py diff --git a/.travis.yml b/.travis.yml index 00811615031..74e75708316 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ install: - pip install git+https://github.com/ansible/ansible.git@devel#egg=ansible - pip install git+https://github.com/sivel/ansible-testing.git#egg=ansible_testing script: - - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py' . + - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py|database/influxdb/influxdb.*\.py' . - python2.6 -m compileall -fq . - python2.7 -m compileall -fq . - ansible-validate-modules --exclude 'cloud/cloudstack/cs_template\.py|cloud/centurylink/clc_aa_policy\.py|cloud/centurylink/clc_alert_policy\.py|cloud/centurylink/clc_blueprint_package\.py|cloud/centurylink/clc_firewall_policy\.py|cloud/centurylink/clc_group\.py|cloud/centurylink/clc_loadbalancer\.py|cloud/centurylink/clc_modify_server\.py|cloud/centurylink/clc_publicip\.py|cloud/centurylink/clc_server\.py|cloud/centurylink/clc_server_snapshot\.py|cloud/docker/docker_login\.py|messaging/rabbitmq_binding\.py|messaging/rabbitmq_exchange\.py|messaging/rabbitmq_queue\.py|monitoring/circonus_annotation\.py|network/snmp_facts\.py|notification/sns\.py|cloud/cloudstack/cs_template\.py' . diff --git a/database/influxdb/__init__.py b/database/influxdb/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/database/influxdb/influxdb_database.py b/database/influxdb/influxdb_database.py new file mode 100644 index 00000000000..3bde398fbc6 --- /dev/null +++ b/database/influxdb/influxdb_database.py @@ -0,0 +1,194 @@ +#!/usr/bin/python + +# (c) 2016, Kamil Szczygiel +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: influxdb_database +short_description: Manage InfluxDB databases +description: + - Manage InfluxDB databases +version_added: 2.2 +author: "Kamil Szczygiel (@kamsz)" +requirements: + - "python >= 2.6" + - "influxdb >= 0.9" +options: + hostname: + description: + - The hostname or IP address on which InfluxDB server is listening + required: true + username: + description: + - Username that will be used to authenticate against InfluxDB server + default: root + required: false + password: + description: + - Password that will be used to authenticate against InfluxDB server + default: root + required: false + port: + description: + - The port on which InfluxDB server is listening + default: 8086 + required: false + database_name: + description: + - Name of the database that will be created/destroyed + required: true + state: + description: + - Determines if the database should be created or destroyed + choices: ['present', 'absent'] + default: present + required: false +''' + +EXAMPLES = ''' +# Example influxdb_database command from Ansible Playbooks +- name: Create database + influxdb_database: + hostname: "{{influxdb_ip_address}}" + database_name: "{{influxdb_database_name}}" + state: present + +- name: Destroy database + influxdb_database: + hostname: "{{influxdb_ip_address}}" + database_name: "{{influxdb_database_name}}" + state: absent + +- name: Create database using custom credentials + influxdb_database: + hostname: "{{influxdb_ip_address}}" + username: "{{influxdb_username}}" + password: "{{influxdb_password}}" + database_name: "{{influxdb_database_name}}" + state: present +''' + +RETURN = ''' +#only defaults +''' + +try: + import requests.exceptions + from influxdb import InfluxDBClient + from influxdb import exceptions + HAS_INFLUXDB = True +except ImportError: + HAS_INFLUXDB = False + + +def influxdb_argument_spec(): + return dict( + hostname=dict(required=True, type='str'), + port=dict(default=8086, type='int'), + username=dict(default='root', type='str'), + password=dict(default='root', type='str', no_log=True), + database_name=dict(required=True, type='str') + ) + + +def connect_to_influxdb(module): + hostname = module.params['hostname'] + port = module.params['port'] + username = module.params['username'] + password = module.params['password'] + database_name = module.params['database_name'] + + client = InfluxDBClient( + host=hostname, + port=port, + username=username, + password=password, + database=database_name + ) + return client + + +def find_database(module, client, database_name): + database = None + + try: + databases = client.get_list_database() + for db in databases: + if db['name'] == database_name: + database = db + break + except requests.exceptions.ConnectionError as e: + module.fail_json(msg=str(e)) + return database + + +def create_database(module, client, database_name): + if not module.check_mode: + try: + client.create_database(database_name) + except requests.exceptions.ConnectionError as e: + module.fail_json(msg=str(e)) + + module.exit_json(changed=True) + + +def drop_database(module, client, database_name): + if not module.check_mode: + try: + client.drop_database(database_name) + except exceptions.InfluxDBClientError as e: + module.fail_json(msg=e.content) + + module.exit_json(changed=True) + + +def main(): + argument_spec = influxdb_argument_spec() + argument_spec.update( + state=dict(default='present', type='str', choices=['present', 'absent']) + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True + ) + + if not HAS_INFLUXDB: + module.fail_json(msg='influxdb python package is required for this module') + + state = module.params['state'] + database_name = module.params['database_name'] + + client = connect_to_influxdb(module) + database = find_database(module, client, database_name) + + if state == 'present': + if database: + module.exit_json(changed=False) + else: + create_database(module, client, database_name) + + if state == 'absent': + if database: + drop_database(module, client, database_name) + else: + module.exit_json(changed=False) + +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From 2e09202aae9e8f7934e4b05a8305f7c1352a61e6 Mon Sep 17 00:00:00 2001 From: nitzmahone Date: Thu, 5 May 2016 15:41:46 -0700 Subject: [PATCH 1512/2522] back version added to 2.1 --- database/influxdb/influxdb_database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/influxdb/influxdb_database.py b/database/influxdb/influxdb_database.py index 3bde398fbc6..7cedc44d4da 100644 --- a/database/influxdb/influxdb_database.py +++ b/database/influxdb/influxdb_database.py @@ -23,7 +23,7 @@ short_description: Manage InfluxDB databases description: - Manage InfluxDB databases -version_added: 2.2 +version_added: 2.1 author: "Kamil Szczygiel (@kamsz)" requirements: - "python >= 2.6" From 85feaa6409330a6bc75050aab0e3b84194dfd958 Mon Sep 17 00:00:00 2001 From: sebfere Date: Fri, 6 May 2016 01:49:00 +0200 Subject: [PATCH 1513/2522] add monitor configuration to module "bigip_node" (#2054) (squashed merge commits w/ new github fun, let's see if it worked) --- network/f5/bigip_node.py | 93 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/network/f5/bigip_node.py b/network/f5/bigip_node.py index bc209af6ae6..3f57251218a 100644 --- a/network/f5/bigip_node.py +++ b/network/f5/bigip_node.py @@ -99,6 +99,30 @@ required: false default: null choices: [] + monitor_type: + description: + - Monitor rule type when monitors > 1 + version_added: "2.2" + required: False + default: null + choices: ['and_list', 'm_of_n'] + aliases: [] + quorum: + description: + - Monitor quorum value when monitor_type is m_of_n + version_added: "2.2" + required: False + default: null + choices: [] + aliases: [] + monitors: + description: + - Monitor template name list. Always use the full path to the monitor. + version_added: "2.2" + required: False + default: null + choices: [] + aliases: [] host: description: - "Node IP. Required when state=present and node does not exist. Error when state=absent." @@ -141,6 +165,18 @@ # Alternatively, you could have specified a name with the # name parameter when state=present. + - name: Add node with a single 'ping' monitor + bigip_node: + server: lb.mydomain.com + user: admin + password: mysecret + state: present + partition: Common + host: "{{ ansible_default_ipv4["address"] }}" + name: mytestserver + monitors: + - /Common/icmp + - name: Modify node description local_action: > bigip_node @@ -261,15 +297,32 @@ def get_node_monitor_status(api, name): result = result.split("MONITOR_STATUS_")[-1].lower() return result +def get_monitors(api, name): + result = api.LocalLB.NodeAddressV2.get_monitor_rule(nodes=[name])[0] + monitor_type = result['type'].split("MONITOR_RULE_TYPE_")[-1].lower() + quorum = result['quorum'] + monitor_templates = result['monitor_templates'] + return (monitor_type, quorum, monitor_templates) + +def set_monitors(api, name, monitor_type, quorum, monitor_templates): + monitor_type = "MONITOR_RULE_TYPE_%s" % monitor_type.strip().upper() + monitor_rule = {'type': monitor_type, 'quorum': quorum, 'monitor_templates': monitor_templates} + api.LocalLB.NodeAddressV2.set_monitor_rule(nodes=[name], + monitor_rules=[monitor_rule]) def main(): + monitor_type_choices = ['and_list', 'm_of_n'] + argument_spec=f5_argument_spec() argument_spec.update(dict( session_state = dict(type='str', choices=['enabled', 'disabled']), monitor_state = dict(type='str', choices=['enabled', 'disabled']), name = dict(type='str', required=True), host = dict(type='str', aliases=['address', 'ip']), - description = dict(type='str') + description = dict(type='str'), + monitor_type = dict(type='str', choices=monitor_type_choices), + quorum = dict(type='int'), + monitors = dict(type='list') ) ) @@ -299,10 +352,40 @@ def main(): name = module.params['name'] address = fq_name(partition, name) description = module.params['description'] + monitor_type = module.params['monitor_type'] + if monitor_type: + monitor_type = monitor_type.lower() + quorum = module.params['quorum'] + monitors = module.params['monitors'] + if monitors: + monitors = [] + for monitor in module.params['monitors']: + monitors.append(fq_name(partition, monitor)) + + # sanity check user supplied values if state == 'absent' and host is not None: module.fail_json(msg="host parameter invalid when state=absent") + if monitors: + if len(monitors) == 1: + # set default required values for single monitor + quorum = 0 + monitor_type = 'single' + elif len(monitors) > 1: + if not monitor_type: + module.fail_json(msg="monitor_type required for monitors > 1") + if monitor_type == 'm_of_n' and not quorum: + module.fail_json(msg="quorum value required for monitor_type m_of_n") + if monitor_type != 'm_of_n': + quorum = 0 + elif monitor_type: + # no monitors specified but monitor_type exists + module.fail_json(msg="monitor_type require monitors parameter") + elif quorum is not None: + # no monitors specified but quorum exists + module.fail_json(msg="quorum requires monitors parameter") + try: api = bigip_api(server, user, password, validate_certs) result = {'changed': False} # default @@ -340,6 +423,8 @@ def main(): if description is not None: set_node_description(api, address, description) result = {'changed': True} + if monitors: + set_monitors(api, address, monitor_type, quorum, monitors) else: # check-mode return value result = {'changed': True} @@ -383,6 +468,12 @@ def main(): if not module.check_mode: set_node_description(api, address, description) result = {'changed': True} + if monitors: + t_monitor_type, t_quorum, t_monitor_templates = get_monitors(api, address) + if (t_monitor_type != monitor_type) or (t_quorum != quorum) or (set(t_monitor_templates) != set(monitors)): + if not module.check_mode: + set_monitors(api, address, monitor_type, quorum, monitors) + result = {'changed': True} except Exception, e: module.fail_json(msg="received exception: %s" % e) From d1b16cd0075b4428df2f7036beade8c18e154cde Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Fri, 6 May 2016 16:48:41 +0200 Subject: [PATCH 1514/2522] Use type='path' for dest (#2175) --- source_control/bzr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source_control/bzr.py b/source_control/bzr.py index 0fc6ac28584..e6cfe9f1ea8 100644 --- a/source_control/bzr.py +++ b/source_control/bzr.py @@ -143,7 +143,7 @@ def switch_version(self): def main(): module = AnsibleModule( argument_spec = dict( - dest=dict(required=True), + dest=dict(required=True, type='path'), name=dict(required=True, aliases=['parent']), version=dict(default='head'), force=dict(default='no', type='bool'), @@ -151,7 +151,7 @@ def main(): ) ) - dest = os.path.abspath(os.path.expanduser(module.params['dest'])) + dest = module.params['dest'] parent = module.params['name'] version = module.params['version'] force = module.params['force'] From 431591c2b45f28ac4033e04953d8ecfc360ba575 Mon Sep 17 00:00:00 2001 From: Kamil Szczygiel Date: Thu, 14 Apr 2016 16:03:17 +0200 Subject: [PATCH 1515/2522] influxdb retention policy module --- .../influxdb/influxdb_retention_policy.py | 237 ++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 database/influxdb/influxdb_retention_policy.py diff --git a/database/influxdb/influxdb_retention_policy.py b/database/influxdb/influxdb_retention_policy.py new file mode 100644 index 00000000000..4f960003bc3 --- /dev/null +++ b/database/influxdb/influxdb_retention_policy.py @@ -0,0 +1,237 @@ +#!/usr/bin/python + +# (c) 2016, Kamil Szczygiel +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: influxdb_retention_policy +short_description: Manage InfluxDB retention policies +description: + - Manage InfluxDB retention policies +version_added: 2.1 +author: "Kamil Szczygiel (@kamsz)" +requirements: + - "python >= 2.6" + - "influxdb >= 0.9" +options: + hostname: + description: + - The hostname or IP address on which InfluxDB server is listening + required: true + username: + description: + - Username that will be used to authenticate against InfluxDB server + default: root + required: false + password: + description: + - Password that will be used to authenticate against InfluxDB server + default: root + required: false + port: + description: + - The port on which InfluxDB server is listening + default: 8086 + required: false + database_name: + description: + - Name of the database where retention policy will be created + required: true + policy_name: + description: + - Name of the retention policy + required: true + duration: + description: + - Determines how long InfluxDB should keep the data + required: true + replication: + description: + - Determines how many independent copies of each point are stored in the cluster + required: true + default: + description: + - Sets the retention policy as default retention policy + required: true +''' + +EXAMPLES = ''' +# Example influxdb_retention_policy command from Ansible Playbooks +- name: create 1 hour retention policy + influxdb_retention_policy: + hostname: "{{influxdb_ip_address}}" + database_name: "{{influxdb_database_name}}" + policy_name: test + duration: 1h + replication: 1 + +- name: create 1 day retention policy + influxdb_retention_policy: + hostname: "{{influxdb_ip_address}}" + database_name: "{{influxdb_database_name}}" + policy_name: test + duration: 1d + replication: 1 + +- name: create 1 week retention policy + influxdb_retention_policy: + hostname: "{{influxdb_ip_address}}" + database_name: "{{influxdb_database_name}}" + policy_name: test + duration: 1w + replication: 1 + +- name: create infinite retention policy + influxdb_retention_policy: + hostname: "{{influxdb_ip_address}}" + database_name: "{{influxdb_database_name}}" + policy_name: test + duration: INF + replication: 1 +''' + +RETURN = ''' +#only defaults +''' + +import re +try: + import requests.exceptions + from influxdb import InfluxDBClient + from influxdb import exceptions + HAS_INFLUXDB = True +except ImportError: + HAS_INFLUXDB = False + + +def influxdb_argument_spec(): + return dict( + hostname=dict(required=True, type='str'), + port=dict(default=8086, type='int'), + username=dict(default='root', type='str'), + password=dict(default='root', type='str', no_log=True), + database_name=dict(default=None, type='str') + ) + + +def connect_to_influxdb(module): + hostname = module.params['hostname'] + port = module.params['port'] + username = module.params['username'] + password = module.params['password'] + database_name = module.params['database_name'] + + client = InfluxDBClient( + host=hostname, + port=port, + username=username, + password=password, + database=database_name + ) + return client + + +def find_retention_policy(module, client): + database_name = module.params['database_name'] + policy_name = module.params['policy_name'] + retention_policy = None + + try: + retention_policies = client.get_list_retention_policies(database=database_name) + for policy in retention_policies: + if policy['name'] == policy_name: + retention_policy = policy + break + except requests.exceptions.ConnectionError as e: + module.fail_json(msg=str(e)) + return retention_policy + + +def create_retention_policy(module, client): + database_name = module.params['database_name'] + policy_name = module.params['policy_name'] + duration = module.params['duration'] + replication = module.params['replication'] + default = module.params['default'] + + if not module.check_mode: + try: + client.create_retention_policy(policy_name, duration, replication, database_name, default) + except exceptions.InfluxDBClientError as e: + module.fail_json(msg=e.content) + module.exit_json(changed=True) + + +def alter_retention_policy(module, client, retention_policy): + database_name = module.params['database_name'] + policy_name = module.params['policy_name'] + duration = module.params['duration'] + replication = module.params['replication'] + default = module.params['default'] + duration_regexp = re.compile('(\d+)([hdw]{1})|(^INF$){1}') + changed = False + + duration_lookup = duration_regexp.search(duration) + + if duration_lookup.group(2) == 'h': + influxdb_duration_format = '%s0m0s' % duration + elif duration_lookup.group(2) == 'd': + influxdb_duration_format = '%sh0m0s' % (int(duration_lookup.group(1)) * 24) + elif duration_lookup.group(2) == 'w': + influxdb_duration_format = '%sh0m0s' % (int(duration_lookup.group(1)) * 24 * 7) + elif duration == 'INF': + influxdb_duration_format = 'INF' + + if not retention_policy['duration'] == influxdb_duration_format or not retention_policy['replicaN'] == int(replication) or not retention_policy['default'] == default: + if not module.check_mode: + try: + client.alter_retention_policy(policy_name, database_name, duration, replication, default) + except exceptions.InfluxDBClientError as e: + module.fail_json(msg=e.content) + changed = True + module.exit_json(changed=changed) + + +def main(): + argument_spec = influxdb_argument_spec() + argument_spec.update( + policy_name=dict(required=True, type='str'), + duration=dict(required=True, type='str'), + replication=dict(required=True, type='int'), + default=dict(default=False, type='bool') + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True + ) + + if not HAS_INFLUXDB: + module.fail_json(msg='influxdb python package is required for this module') + + client = connect_to_influxdb(module) + retention_policy = find_retention_policy(module, client) + + if retention_policy: + alter_retention_policy(module, client, retention_policy) + else: + create_retention_policy(module, client) + +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From b4de5ec2084f501e1aa8558acd26a4753c494c80 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Fri, 6 May 2016 14:00:07 -0500 Subject: [PATCH 1516/2522] Reduce exclusions for ansible-testing due to fixes and changes in ansible-testing --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 00811615031..c210c071943 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,5 +20,5 @@ script: - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py' . - python2.6 -m compileall -fq . - python2.7 -m compileall -fq . - - ansible-validate-modules --exclude 'cloud/cloudstack/cs_template\.py|cloud/centurylink/clc_aa_policy\.py|cloud/centurylink/clc_alert_policy\.py|cloud/centurylink/clc_blueprint_package\.py|cloud/centurylink/clc_firewall_policy\.py|cloud/centurylink/clc_group\.py|cloud/centurylink/clc_loadbalancer\.py|cloud/centurylink/clc_modify_server\.py|cloud/centurylink/clc_publicip\.py|cloud/centurylink/clc_server\.py|cloud/centurylink/clc_server_snapshot\.py|cloud/docker/docker_login\.py|messaging/rabbitmq_binding\.py|messaging/rabbitmq_exchange\.py|messaging/rabbitmq_queue\.py|monitoring/circonus_annotation\.py|network/snmp_facts\.py|notification/sns\.py|cloud/cloudstack/cs_template\.py' . + - ansible-validate-modules --exclude 'cloud/cloudstack/cs_template\.py' . #- ./test-docs.sh extras From 081845b35350dd99f31c3750d9d3769a556f6946 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Fri, 6 May 2016 14:00:32 -0500 Subject: [PATCH 1517/2522] Fix HAS_BOTO fail_json call for ec2_ami_copy --- cloud/amazon/ec2_ami_copy.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/amazon/ec2_ami_copy.py b/cloud/amazon/ec2_ami_copy.py index 49afed7bf98..72c804bc1d1 100644 --- a/cloud/amazon/ec2_ami_copy.py +++ b/cloud/amazon/ec2_ami_copy.py @@ -88,9 +88,6 @@ HAS_BOTO = True except ImportError: HAS_BOTO = False - -if not HAS_BOTO: - module.fail_json(msg='boto required for this module') def copy_image(module, ec2): """ @@ -180,6 +177,9 @@ def main(): module = AnsibleModule(argument_spec=argument_spec) + if not HAS_BOTO: + module.fail_json(msg='boto required for this module') + try: ec2 = ec2_connect(module) except boto.exception.NoAuthHandlerFound, e: From 61b9b48c608c5ef816e2394ef5bd790e165e5134 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Fri, 6 May 2016 14:02:23 -0500 Subject: [PATCH 1518/2522] No exclusions necessary --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c210c071943..1edbffdbd8d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,5 +20,5 @@ script: - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py' . - python2.6 -m compileall -fq . - python2.7 -m compileall -fq . - - ansible-validate-modules --exclude 'cloud/cloudstack/cs_template\.py' . + - ansible-validate-modules . #- ./test-docs.sh extras From 8be42e067625f1d35dcaae10e19021e8cebdc0ce Mon Sep 17 00:00:00 2001 From: Nick Aslanidis Date: Sat, 7 May 2016 21:00:23 +1000 Subject: [PATCH 1519/2522] corrected version to 2.2. Ensure no vpc-id is returned if detached --- cloud/amazon/ec2_vpc_vgw.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/amazon/ec2_vpc_vgw.py b/cloud/amazon/ec2_vpc_vgw.py index 466b81c8762..9861e7d0b7e 100644 --- a/cloud/amazon/ec2_vpc_vgw.py +++ b/cloud/amazon/ec2_vpc_vgw.py @@ -22,7 +22,7 @@ - Deletes AWS VPN Virtual Gateways - Attaches Virtual Gateways to VPCs - Detaches Virtual Gateways from VPCs -version_added: "2.1" +version_added: "2.2" requirements: [ boto3 ] options: state: @@ -139,7 +139,7 @@ def get_vgw_info(vgws): for tag in vgw['Tags']: vgw_info['tags'][tag['Key']] = tag['Value'] - if len(vgw['VpcAttachments']) != 0: + if len(vgw['VpcAttachments']) != 0 and vgw['VpcAttachments'][0]['State'] == 'attached': vgw_info['vpc_id'] = vgw['VpcAttachments'][0]['VpcId'] return vgw_info @@ -537,7 +537,7 @@ def ensure_vgw_absent(client, module): changed = False deleted_vgw = None - result = get_vgw_info(deleted_vgw) + result = deleted_vgw return changed, result From 557d37d3f1da134fc44e9677b8ea5eb4233fd9fc Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sun, 8 May 2016 11:33:54 +0200 Subject: [PATCH 1520/2522] Use type='path' rather than str, so path is expanded correctly --- system/known_hosts.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/system/known_hosts.py b/system/known_hosts.py index b68e85e0e77..b4c26e0efa5 100644 --- a/system/known_hosts.py +++ b/system/known_hosts.py @@ -85,9 +85,7 @@ def enforce_state(module, params): host = params["name"] key = params.get("key",None) port = params.get("port",None) - #expand the path parameter; otherwise module.add_path_info - #(called by exit_json) unhelpfully says the unexpanded path is absent. - path = os.path.expanduser(params.get("path")) + path = params.get("path") state = params.get("state") #Find the ssh-keygen binary sshkeygen = module.get_bin_path("ssh-keygen",True) @@ -240,7 +238,7 @@ def main(): argument_spec = dict( name = dict(required=True, type='str', aliases=['host']), key = dict(required=False, type='str'), - path = dict(default="~/.ssh/known_hosts", type='str'), + path = dict(default="~/.ssh/known_hosts", type='path'), state = dict(default='present', choices=['absent','present']), ), supports_check_mode = True From 5e008b928ee0cd6b9e5968f1c0006b3d47561e4f Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sun, 8 May 2016 01:01:58 +0200 Subject: [PATCH 1521/2522] cloudstack: new module cs_router --- cloud/cloudstack/cs_router.py | 383 ++++++++++++++++++++++++++++++++++ 1 file changed, 383 insertions(+) create mode 100644 cloud/cloudstack/cs_router.py diff --git a/cloud/cloudstack/cs_router.py b/cloud/cloudstack/cs_router.py new file mode 100644 index 00000000000..7209ef19a7f --- /dev/null +++ b/cloud/cloudstack/cs_router.py @@ -0,0 +1,383 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2016, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: cs_router +short_description: Manages routers on Apache CloudStack based clouds. +description: + - Start, restart, stop and destroy routers. + - C(state=present) is not able to create routers, use M(cs_network) instead. +version_added: "2.2" +author: "René Moser (@resmo)" +options: + name: + description: + - Name of the router. + required: true + service_offering: + description: + - Name or id of the service offering of the router. + required: false + default: null + domain: + description: + - Domain the router is related to. + required: false + default: null + account: + description: + - Account the router is related to. + required: false + default: null + project: + description: + - Name of the project the router is related to. + required: false + default: null + state: + description: + - State of the router. + required: false + default: 'present' + choices: [ 'present', 'absent', 'started', 'stopped', 'restarted' ] +extends_documentation_fragment: cloudstack +''' + +EXAMPLES = ''' +# Ensure the router has the desired service offering, no matter if +# the router is running or not. +- local_action: + module: cs_router + name: r-40-VM + service_offering: System Offering for Software Router + +# Ensure started +- local_action: + module: cs_router + name: r-40-VM + state: started + +# Ensure started with desired service offering. +# If the service offerings changes, router will be rebooted. +- local_action: + module: cs_router + name: r-40-VM + service_offering: System Offering for Software Router + state: started + +# Ensure stopped +- local_action: + module: cs_router + name: r-40-VM + state: stopped + +# Remove a router +- local_action: + module: cs_router + name: r-40-VM + state: absent +''' + +RETURN = ''' +--- +id: + description: UUID of the router. + returned: success + type: string + sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6 +name: + description: Name of the router. + returned: success + type: string + sample: r-40-VM +created: + description: Date of the router was created. + returned: success + type: string + sample: 2014-12-01T14:57:57+0100 +template_version: + description: Version of the system VM template. + returned: success + type: string + sample: 4.5.1 +requires_upgrade: + description: Whether the router needs to be upgraded to the new template. + returned: success + type: bool + sample: false +redundant_state: + description: Redundant state of the router. + returned: success + type: string + sample: UNKNOWN +role: + description: Role of the router. + returned: success + type: string + sample: VIRTUAL_ROUTER +zone: + description: Name of zone the router is in. + returned: success + type: string + sample: ch-gva-2 +service_offering: + description: Name of the service offering the router has. + returned: success + type: string + sample: System Offering For Software Router +state: + description: State of the router. + returned: success + type: string + sample: Active +domain: + description: Domain the router is related to. + returned: success + type: string + sample: ROOT +account: + description: Account the router is related to. + returned: success + type: string + sample: admin +''' + +try: + from cs import CloudStack, CloudStackException, read_config + has_lib_cs = True +except ImportError: + has_lib_cs = False + +# import cloudstack common +from ansible.module_utils.cloudstack import * + +class AnsibleCloudStackRouter(AnsibleCloudStack): + + def __init__(self, module): + super(AnsibleCloudStackRouter, self).__init__(module) + self.returns = { + 'serviceofferingname': 'service_offering', + 'version': 'template_version', + 'requiresupgrade': 'requires_upgrade', + 'redundantstate': 'redundant_state', + 'role': 'role' + } + self.router = None + + + def get_service_offering_id(self): + service_offering = self.module.params.get('service_offering') + if not service_offering: + return None + + args = {} + args['issystem'] = True + + service_offerings = self.cs.listServiceOfferings(**args) + if service_offerings: + for s in service_offerings['serviceoffering']: + if service_offering in [ s['name'], s['id'] ]: + return s['id'] + self.module.fail_json(msg="Service offering '%s' not found" % service_offering) + + def get_router(self): + if not self.router: + router = self.module.params.get('name') + + args = {} + args['projectid'] = self.get_project(key='id') + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') + + routers = self.cs.listRouters(**args) + if routers: + for r in routers['router']: + if router.lower() in [ r['name'].lower(), r['id']]: + self.router = r + break + return self.router + + def start_router(self): + router = self.get_router() + if not router: + self.module.fail_json(msg="Router not found") + + if router['state'].lower() != "running": + self.result['changed'] = True + + args = {} + args['id'] = router['id'] + + if not self.module.check_mode: + res = self.cs.startRouter(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + + poll_async = self.module.params.get('poll_async') + if poll_async: + router = self._poll_job(res, 'router') + return router + + def stop_router(self): + router = self.get_router() + if not router: + self.module.fail_json(msg="Router not found") + + if router['state'].lower() != "stopped": + self.result['changed'] = True + + args = {} + args['id'] = router['id'] + + if not self.module.check_mode: + res = self.cs.stopRouter(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + + poll_async = self.module.params.get('poll_async') + if poll_async: + router = self._poll_job(res, 'router') + return router + + def reboot_router(self): + router = self.get_router() + if not router: + self.module.fail_json(msg="Router not found") + + self.result['changed'] = True + + args = {} + args['id'] = router['id'] + + if not self.module.check_mode: + res = self.cs.rebootRouter(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + + poll_async = self.module.params.get('poll_async') + if poll_async: + router = self._poll_job(res, 'router') + return router + + def absent_router(self): + router = self.get_router() + if router: + self.result['changed'] = True + + args = {} + args['id'] = router['id'] + + if not self.module.check_mode: + res = self.cs.destroyRouter(**args) + + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + + poll_async = self.module.params.get('poll_async') + if poll_async: + self._poll_job(res, 'router') + return router + + + def present_router(self): + router = self.get_router() + if not router: + self.module.fail_json(msg="Router can not be created using the API, see cs_network.") + + args = {} + args['id'] = router['id'] + args['serviceofferingid'] = self.get_service_offering_id() + + state = self.module.params.get('state') + + if self.has_changed(args, router): + self.result['changed'] = True + + if not self.module.check_mode: + current_state = router['state'].lower() + + self.stop_router() + router = self.cs.changeServiceForRouter(**args) + + if 'errortext' in router: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + + if state in [ 'restarted', 'started' ]: + router = self.start_router() + + # if state=present we get to the state before the service + # offering change. + elif state == "present" and current_state == "running": + router = self.start_router() + + elif state == "started": + router = self.start_router() + + elif state == "stopped": + router = self.stop_router() + + elif state == "restarted": + router = self.reboot_router() + + return router + + +def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + name = dict(required=True), + service_offering = dict(default=None), + state = dict(choices=['present', 'started', 'stopped', 'restarted', 'absent'], default="present"), + domain = dict(default=None), + account = dict(default=None), + project = dict(default=None), + poll_async = dict(type='bool', default=True), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=cs_required_together(), + supports_check_mode=True + ) + + if not has_lib_cs: + module.fail_json(msg="python library cs required: pip install cs") + + try: + acs_router = AnsibleCloudStackRouter(module) + + state = module.params.get('state') + if state in ['absent']: + router = acs_router.absent_router() + else: + router = acs_router.present_router() + + result = acs_router.get_result(router) + + except CloudStackException as e: + module.fail_json(msg='CloudStackException: %s' % str(e)) + + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From 239c60ec13be647bb8b01b36484e622a036004bc Mon Sep 17 00:00:00 2001 From: Ben Knight Date: Mon, 9 May 2016 07:30:04 +1000 Subject: [PATCH 1522/2522] Add reselect_tries option to big_pool module (#2156) --- network/f5/bigip_pool.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/network/f5/bigip_pool.py b/network/f5/bigip_pool.py index a8dafb3bbef..4e413f9fd07 100644 --- a/network/f5/bigip_pool.py +++ b/network/f5/bigip_pool.py @@ -133,6 +133,14 @@ default: null choices: [] aliases: [] + reselect_tries: + description: + - Sets the number of times the system tries to contact a pool member after a passive failure + version_added: "2.2" + required: False + default: null + choices: [] + aliases: [] service_down_action: description: - Sets the action to take when node goes down in pool @@ -284,6 +292,13 @@ def get_slow_ramp_time(api, pool): def set_slow_ramp_time(api, pool, seconds): api.LocalLB.Pool.set_slow_ramp_time(pool_names=[pool], values=[seconds]) +def get_reselect_tries(api, pool): + result = api.LocalLB.Pool.get_reselect_tries(pool_names=[pool])[0] + return result + +def set_reselect_tries(api, pool, tries): + api.LocalLB.Pool.set_reselect_tries(pool_names=[pool], values=[tries]) + def get_action_on_service_down(api, pool): result = api.LocalLB.Pool.get_action_on_service_down(pool_names=[pool])[0] result = result.split("SERVICE_DOWN_ACTION_")[-1].lower() @@ -356,6 +371,7 @@ def main(): quorum = dict(type='int'), monitors = dict(type='list'), slow_ramp_time = dict(type='int'), + reselect_tries = dict(type='int'), service_down_action = dict(type='str', choices=service_down_choices), host = dict(type='str', aliases=['address']), port = dict(type='int') @@ -397,6 +413,7 @@ def main(): for monitor in module.params['monitors']: monitors.append(fq_name(partition, monitor)) slow_ramp_time = module.params['slow_ramp_time'] + reselect_tries = module.params['reselect_tries'] service_down_action = module.params['service_down_action'] if service_down_action: service_down_action = service_down_action.lower() @@ -487,6 +504,8 @@ def main(): set_monitors(api, pool, monitor_type, quorum, monitors) if slow_ramp_time: set_slow_ramp_time(api, pool, slow_ramp_time) + if reselect_tries: + set_reselect_tries(api, pool, reselect_tries) if service_down_action: set_action_on_service_down(api, pool, service_down_action) if host and port: @@ -513,6 +532,10 @@ def main(): if not module.check_mode: set_slow_ramp_time(api, pool, slow_ramp_time) result = {'changed': True} + if reselect_tries and reselect_tries != get_reselect_tries(api, pool): + if not module.check_mode: + set_reselect_tries(api, pool, reselect_tries) + result = {'changed': True} if service_down_action and service_down_action != get_action_on_service_down(api, pool): if not module.check_mode: set_action_on_service_down(api, pool, service_down_action) From 7b7af3bcf9082120fa6aed3c109364920800d84d Mon Sep 17 00:00:00 2001 From: David Keijser Date: Sun, 8 May 2016 23:33:25 +0200 Subject: [PATCH 1523/2522] Make it possible to manage rules of f5 vs (#1821) --- network/f5/bigip_virtual_server.py | 41 +++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/network/f5/bigip_virtual_server.py b/network/f5/bigip_virtual_server.py index 891ff5ecd34..0cee9f68a01 100644 --- a/network/f5/bigip_virtual_server.py +++ b/network/f5/bigip_virtual_server.py @@ -87,6 +87,12 @@ - "List of all Profiles (HTTP,ClientSSL,ServerSSL,etc) that must be used by the virtual server" required: false default: None + all_rules: + version_added: "2.2" + description: + - "List of rules to be applied in priority order" + required: false + default: None pool: description: - "Default pool for the virtual server" @@ -213,11 +219,40 @@ def vs_create(api,name,destination,port,pool): def vs_remove(api,name): api.LocalLB.VirtualServer.delete_virtual_server(virtual_servers = [name ]) + +def get_rules(api,name): + return api.LocalLB.VirtualServer.get_rule(virtual_servers = [name])[0] + + +def set_rules(api,name,rules_list): + updated=False + if rules_list is None: + return False + rules_list = list(enumerate(rules_list)) + try: + current_rules=map(lambda x: (x['priority'], x['rule_name']), get_rules(api,name)) + to_add_rules=[] + for i, x in rules_list: + if (i ,x) not in current_rules: + to_add_rules.append({'priority': i, 'rule_name': x}) + to_del_rules=[] + for i, x in current_rules: + if (i, x) not in rules_list: + to_del_rules.append({'priority': i, 'rule_name': x}) + if len(to_del_rules)>0: + api.LocalLB.VirtualServer.remove_rule(virtual_servers = [name],rules = [to_del_rules]) + updated=True + if len(to_add_rules)>0: + api.LocalLB.VirtualServer.add_rule(virtual_servers = [name],rules= [to_add_rules]) + updated=True + return updated + except bigsuds.OperationFailed, e: + raise Exception('Error on setting profiles : %s' % e) + def get_profiles(api,name): return api.LocalLB.VirtualServer.get_profile(virtual_servers = [name])[0] - def set_profiles(api,name,profiles_list): updated=False try: @@ -366,6 +401,7 @@ def main(): destination = dict(type='str', aliases=['address', 'ip']), port = dict(type='int'), all_profiles = dict(type='list'), + all_rules = dict(type='list'), pool=dict(type='str'), description = dict(type='str'), snat=dict(type='str'), @@ -397,6 +433,7 @@ def main(): destination=module.params['destination'] port=module.params['port'] all_profiles=fq_list_names(partition,module.params['all_profiles']) + all_rules=fq_list_names(partition,module.params['all_rules']) pool=fq_name(partition,module.params['pool']) description = module.params['description'] snat = module.params['snat'] @@ -440,6 +477,7 @@ def main(): try: vs_create(api,name,destination,port,pool) set_profiles(api,name,all_profiles) + set_rules(api,name,all_rules) set_snat(api,name,snat) set_description(api,name,description) set_default_persistence_profiles(api,name,default_persistence_profile) @@ -464,6 +502,7 @@ def main(): result['changed']|=set_description(api,name,description) result['changed']|=set_snat(api,name,snat) result['changed']|=set_profiles(api,name,all_profiles) + result['changed']|=set_rules(api,name,all_rules) result['changed']|=set_default_persistence_profiles(api,name,default_persistence_profile) result['changed']|=set_state(api,name,state) api.System.Session.submit_transaction() From bb965eebee04126bb7d2ce81ef99e5556ce44842 Mon Sep 17 00:00:00 2001 From: Nik LaBelle Date: Mon, 9 May 2016 04:00:05 -0700 Subject: [PATCH 1524/2522] change netif type to dict and update example (#2187) --- cloud/misc/proxmox.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cloud/misc/proxmox.py b/cloud/misc/proxmox.py index a0d2b8bbcd5..bd73a037c25 100644 --- a/cloud/misc/proxmox.py +++ b/cloud/misc/proxmox.py @@ -98,7 +98,7 @@ - specifies network interfaces for the container default: null required: false - type: string + type: A hash/dictionary defining interfaces ip_address: description: - specifies the address the container will be assigned @@ -171,6 +171,9 @@ # Create new container with minimal options use environment PROXMOX_PASSWORD variable(you should export it before) - proxmox: vmid=100 node='uk-mc02' api_user='root@pam' api_host='node1' password='123456' hostname='example.org' ostemplate='local:vztmpl/ubuntu-14.04-x86_64.tar.gz' +# Create new container with minimal options defining network interface with dhcp +- proxmox: vmid=100 node='uk-mc02' api_user='root@pam' api_password='1q2w3e' api_host='node1' password='123456' hostname='example.org' ostemplate='local:vztmpl/ubuntu-14.04-x86_64.tar.gz' netif='{"net0":"name=eth0,ip=dhcp,ip6=dhcp,bridge=vmbr0"}' + # Start container - proxmox: vmid=100 api_user='root@pam' api_password='1q2w3e' api_host='node1' state=started @@ -294,7 +297,7 @@ def main(): cpus = dict(type='int', default=1), memory = dict(type='int', default=512), swap = dict(type='int', default=0), - netif = dict(), + netif = dict(type='dict'), ip_address = dict(), onboot = dict(type='bool', default='no'), storage = dict(default='local'), From 371c411ac375459a3fb0babf5457f5336d884a19 Mon Sep 17 00:00:00 2001 From: Charles V Bock Date: Mon, 9 May 2016 10:11:17 -0700 Subject: [PATCH 1525/2522] high_flap_threshold documentation correction Changing description of high_flap_threshold to properly reflect its function. --- monitoring/sensu_check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/sensu_check.py b/monitoring/sensu_check.py index e880e9239d9..7cf38509669 100644 --- a/monitoring/sensu_check.py +++ b/monitoring/sensu_check.py @@ -146,7 +146,7 @@ default: null high_flap_threshold: description: - - The low threshhold for flap detection + - The high threshhold for flap detection required: false default: null custom: From eab4b6a3e9a9a998f1f05de69903cb1819cd55d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Mon, 9 May 2016 21:39:00 +0200 Subject: [PATCH 1526/2522] cs_loadbalancer_rule_member: doc fixes (#2184) --- .../cloudstack/cs_loadbalancer_rule_member.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cloud/cloudstack/cs_loadbalancer_rule_member.py b/cloud/cloudstack/cs_loadbalancer_rule_member.py index 757c00674ef..dd821cafa9c 100644 --- a/cloud/cloudstack/cs_loadbalancer_rule_member.py +++ b/cloud/cloudstack/cs_loadbalancer_rule_member.py @@ -50,7 +50,7 @@ state: description: - Should the VMs be present or absent from the rule. - required: true + required: false default: 'present' choices: [ 'present', 'absent' ] project: @@ -101,19 +101,19 @@ pre_tasks: - name: Remove from load balancer local_action: - module: cs_loadbalancer_rule_member - name: balance_http - vm: "{{ ansible_hostname }}" - state: absent + module: cs_loadbalancer_rule_member + name: balance_http + vm: "{{ ansible_hostname }}" + state: absent tasks: # Perform update post_tasks: - name: Add to load balancer local_action: - module: cs_loadbalancer_rule_member - name: balance_http - vm: "{{ ansible_hostname }}" - state: present + module: cs_loadbalancer_rule_member + name: balance_http + vm: "{{ ansible_hostname }}" + state: present ''' RETURN = ''' From 816673dd6f5df0f9f27753c1c2cadf22ea70befb Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Tue, 10 May 2016 12:23:08 +0200 Subject: [PATCH 1527/2522] Use path type for blockinfile 'dest' argument (#2192) --- files/blockinfile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/files/blockinfile.py b/files/blockinfile.py index eadf3a622dd..0fc0fc73cb4 100644 --- a/files/blockinfile.py +++ b/files/blockinfile.py @@ -188,7 +188,7 @@ def check_file_attrs(module, changed, message): def main(): module = AnsibleModule( argument_spec=dict( - dest=dict(required=True, aliases=['name', 'destfile']), + dest=dict(required=True, aliases=['name', 'destfile'], type='path'), state=dict(default='present', choices=['absent', 'present']), marker=dict(default='# {mark} ANSIBLE MANAGED BLOCK', type='str'), block=dict(default='', type='str', aliases=['content']), @@ -204,7 +204,7 @@ def main(): ) params = module.params - dest = os.path.expanduser(params['dest']) + dest = params['dest'] if module.boolean(params.get('follow', None)): dest = os.path.realpath(dest) From d00d0d08463c6a56e997cb4927dfe949f4ca337c Mon Sep 17 00:00:00 2001 From: Ash Berlin Date: Tue, 10 May 2016 16:00:13 +0100 Subject: [PATCH 1528/2522] Add missing urlparse import to s3_bucket (#2110) This was triggered when S3_URL environment variable was set. --- cloud/amazon/s3_bucket.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud/amazon/s3_bucket.py b/cloud/amazon/s3_bucket.py index 22e68927016..ec838d299f1 100644 --- a/cloud/amazon/s3_bucket.py +++ b/cloud/amazon/s3_bucket.py @@ -97,6 +97,7 @@ ''' import xml.etree.ElementTree as ET +import urlparse try: import boto.ec2 From 2665acb2578bdcb6edf224a5790b83cfb3bad694 Mon Sep 17 00:00:00 2001 From: Michael Baydoun Date: Tue, 10 May 2016 12:32:08 -0400 Subject: [PATCH 1529/2522] created ec2_customer_gateway module (#1868) --- cloud/amazon/ec2_customer_gateway.py | 256 +++++++++++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 cloud/amazon/ec2_customer_gateway.py diff --git a/cloud/amazon/ec2_customer_gateway.py b/cloud/amazon/ec2_customer_gateway.py new file mode 100644 index 00000000000..072fec71178 --- /dev/null +++ b/cloud/amazon/ec2_customer_gateway.py @@ -0,0 +1,256 @@ +#!/usr/bin/python +# +# This is a free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This Ansible library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this library. If not, see . + +DOCUMENTATION = ''' +--- +module: ec2_customer_gateway +short_description: Manage an AWS customer gateway +description: + - Manage an AWS customer gateway +version_added: "2.2" +author: Michael Baydoun (@MichaelBaydoun) +requirements: [ botocore, boto3 ] +notes: + - You cannot create more than one customer gateway with the same IP address. If you run an identical request more than one time, the first request creates the customer gateway, and subsequent requests return information about the existing customer gateway. The subsequent requests do not create new customer gateway resources. +options: + bgp_asn: + description: + - Border Gateway Protocol (BGP) Autonomous System Number (ASN), required when state=present. + required: false + default: null + ip_address: + description: + - Internet-routable IP address for customers gateway, must be a static address. + required: true + name: + description: + - Name of the customer gateway. + required: true + state: + description: + - Create or terminate the Customer Gateway. + required: false + default: present + choices: [ 'present', 'absent' ] +extends_documentation_fragment: + - aws + - ec2 +''' + +EXAMPLES = ''' + +# Create Customer Gateway +- ec2_customer_gateway: + bgp_asn: 12345 + ip_address: 1.2.3.4 + name: IndianapolisOffice + region: us-east-1 + register: cgw + +# Delete Customer Gateway +- ec2_customer_gateway: + ip_address: 1.2.3.4 + name: IndianapolisOffice + state: absent + region: us-east-1 + register: cgw +''' + +RETURN = ''' +gateway.customer_gateways: + description: details about the gateway that was created. + returned: success + type: complex + contains: + bgp_asn: + description: The Border Gateway Autonomous System Number. + returned: when exists and gateway is available. + sample: 65123 + type: string + customer_gateway_id: + description: gateway id assigned by amazon. + returned: when exists and gateway is available. + sample: cgw-cb6386a2 + type: string + ip_address: + description: ip address of your gateway device. + returned: when exists and gateway is available. + sample: 1.2.3.4 + type: string + state: + description: state of gateway. + returned: when gateway exists and is available. + state: available + type: string + tags: + description: any tags on the gateway. + returned: when gateway exists and is available, and when tags exist. + state: available + type: string + type: + description: encryption type. + returned: when gateway exists and is available. + sample: ipsec.1 + type: string +''' + +try: + from botocore.exceptions import ClientError + HAS_BOTOCORE = True +except ImportError: + HAS_BOTOCORE = False + +try: + import boto3 + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + +class Ec2CustomerGatewayManager: + + def __init__(self, module): + self.module = module + + try: + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + if not region: + module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") + self.ec2 = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_kwargs) + except ClientError, e: + module.fail_json(msg=e.message) + + def ensure_cgw_absent(self, gw_id): + response = self.ec2.delete_customer_gateway( + DryRun=False, + CustomerGatewayId=gw_id + ) + return response + + def ensure_cgw_present(self, bgp_asn, ip_address): + response = self.ec2.create_customer_gateway( + DryRun=False, + Type='ipsec.1', + PublicIp=ip_address, + BgpAsn=bgp_asn, + ) + return response + + def tag_cgw_name(self, gw_id, name): + response = self.ec2.create_tags( + DryRun=False, + Resources=[ + gw_id, + ], + Tags=[ + { + 'Key': 'Name', + 'Value': name + }, + ] + ) + return response + + def describe_gateways(self, ip_address): + response = self.ec2.describe_customer_gateways( + DryRun=False, + Filters=[ + { + 'Name': 'state', + 'Values': [ + 'available', + ] + }, + { + 'Name': 'ip-address', + 'Values': [ + ip_address, + ] + } + ] + ) + return response + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + bgp_asn = dict(required=False, type='int'), + ip_address = dict(required=True), + name = dict(required=True), + state = dict(default='present', choices=['present', 'absent']), + ) + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + if not HAS_BOTOCORE: + module.fail_json(msg='botocore is required.') + + if not HAS_BOTO3: + module.fail_json(msg='boto3 is required.') + + gw_mgr = Ec2CustomerGatewayManager(module) + + bgp_asn = module.params.get('bgp_asn') + ip_address = module.params.get('ip_address') + name = module.params.get('name') + + existing = gw_mgr.describe_gateways(module.params['ip_address']) + + results = dict(changed=False) + if module.params['state'] == 'present': + if existing['CustomerGateways']: + results['gateway']=existing + if existing['CustomerGateways'][0]['Tags']: + tag_array = existing['CustomerGateways'][0]['Tags'] + for key, value in enumerate(tag_array): + if value['Key'] == 'Name': + current_name = value['Value'] + if current_name != name: + results['name'] = gw_mgr.tag_cgw_name( + results['gateway']['CustomerGateways'][0]['CustomerGatewayId'], + module.params['name'], + ) + results['changed'] = True + else: + if not module.check_mode: + results['gateway'] = gw_mgr.ensure_cgw_present( + module.params['bgp_asn'], + module.params['ip_address'], + ) + results['name'] = gw_mgr.tag_cgw_name( + results['gateway']['CustomerGateway']['CustomerGatewayId'], + module.params['name'], + ) + results['changed'] = True + + elif module.params['state'] == 'absent': + if existing['CustomerGateways']: + results['gateway']=existing + if not module.check_mode: + results['gateway'] = gw_mgr.ensure_cgw_absent( + existing['CustomerGateways'][0]['CustomerGatewayId'] + ) + results['changed'] = True + + pretty_results = camel_dict_to_snake_dict(results) + module.exit_json(**pretty_results) + +# import module methods +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From 9677961b8c4e2a80017c3a2c97feeca73bc5b6ab Mon Sep 17 00:00:00 2001 From: Rob Date: Wed, 11 May 2016 20:33:52 +1000 Subject: [PATCH 1530/2522] Added better example for boto3 exception handling (#2204) --- cloud/amazon/GUIDELINES.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/GUIDELINES.md b/cloud/amazon/GUIDELINES.md index 225f61f3a49..dbfd3ee1285 100644 --- a/cloud/amazon/GUIDELINES.md +++ b/cloud/amazon/GUIDELINES.md @@ -178,6 +178,8 @@ except BotoServerError, e: For more information on botocore exception handling see [http://botocore.readthedocs.org/en/latest/client_upgrades.html#error-handling] +Boto3 provides lots of useful info when an exception is thrown so pass this to the user along with the message. + ```python # Import ClientError from botocore try: @@ -193,7 +195,20 @@ except ImportError: try: result = connection.aws_call() except ClientError, e: - module.fail_json(msg=e.message) + module.fail_json(msg=e.message, **camel_dict_to_snake_dict(e.response)) +``` + +If you need to perform an action based on the error boto3 returned, use the error code. + +```python +# Make a call to AWS +try: + result = connection.aws_call() +except ClientError, e: + if e.response['Error']['Code'] == 'NoSuchEntity': + return None + else: + module.fail_json(msg=e.message, **camel_dict_to_snake_dict(e.response)) ``` ### Helper functions @@ -220,7 +235,7 @@ key and the dict value is the tag value. Opposite of above. Converts an Ansible dict to a boto3 tag list of dicts. -### get_ec2_security_group_ids_from_names +#### get_ec2_security_group_ids_from_names Pass this function a list of security group names or combination of security group names and IDs and this function will return a list of IDs. You should also pass the VPC ID if known because security group names are not necessarily unique From cd03f10b9ccb9f972a4cf84bc3e756870257da59 Mon Sep 17 00:00:00 2001 From: Ian Levesque Date: Wed, 11 May 2016 13:28:00 -0400 Subject: [PATCH 1531/2522] Fix session-based kv acquire/release (#2181) * Fix session-based kv acquire/release * add example of using session in doc --- clustering/consul_kv.py | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/clustering/consul_kv.py b/clustering/consul_kv.py index 9358b79749a..4dbf6072905 100644 --- a/clustering/consul_kv.py +++ b/clustering/consul_kv.py @@ -130,6 +130,13 @@ consul_kv: key: ansible/groups/dc1/somenode value: 'top_secret' + + - name: Register a key/value pair with an associated session + consul_kv: + key: stg/node/server_birthday + value: 20160509 + session: "{{ sessionid }}" + state: acquire ''' import sys @@ -157,6 +164,8 @@ def execute(module): def lock(module, state): + consul_api = get_consul_api(module) + session = module.params.get('session') key = module.params.get('key') value = module.params.get('value') @@ -166,18 +175,22 @@ def lock(module, state): msg='%s of lock for %s requested but no session supplied' % (state, key)) - if state == 'acquire': - successful = consul_api.kv.put(key, value, - cas=module.params.get('cas'), - acquire=session, - flags=module.params.get('flags')) - else: - successful = consul_api.kv.put(key, value, - cas=module.params.get('cas'), - release=session, - flags=module.params.get('flags')) + index, existing = consul_api.kv.get(key) - module.exit_json(changed=successful, + changed = not existing or (existing and existing['Value'] != value) + if changed and not module.check_mode: + if state == 'acquire': + changed = consul_api.kv.put(key, value, + cas=module.params.get('cas'), + acquire=session, + flags=module.params.get('flags')) + else: + changed = consul_api.kv.put(key, value, + cas=module.params.get('cas'), + release=session, + flags=module.params.get('flags')) + + module.exit_json(changed=changed, index=index, key=key) @@ -251,9 +264,10 @@ def main(): port=dict(default=8500, type='int'), recurse=dict(required=False, type='bool'), retrieve=dict(required=False, default=True), - state=dict(default='present', choices=['present', 'absent']), + state=dict(default='present', choices=['present', 'absent', 'acquire', 'release']), token=dict(required=False, default='anonymous', no_log=True), - value=dict(required=False) + value=dict(required=False), + session=dict(required=False) ) module = AnsibleModule(argument_spec, supports_check_mode=False) From a12696d598aa9977ea2d78f044561798f0693126 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Thu, 12 May 2016 18:20:54 -0700 Subject: [PATCH 1532/2522] the ansible version constant is now in a different place --- files/blockinfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/blockinfile.py b/files/blockinfile.py index 0fc0fc73cb4..37d89ca2c88 100644 --- a/files/blockinfile.py +++ b/files/blockinfile.py @@ -244,7 +244,7 @@ def main(): marker1 = re.sub(r'{mark}', 'END', marker) if present and block: # Escape seqeuences like '\n' need to be handled in Ansible 1.x - if module.constants['ANSIBLE_VERSION'].startswith('1.'): + if module.ansible_version.startswith('1.'): block = re.sub('', block, '') blocklines = [marker0] + block.splitlines() + [marker1] else: From bbd53572af78c6fea669f943f5efb21e777c149f Mon Sep 17 00:00:00 2001 From: Yannig Date: Fri, 13 May 2016 11:28:41 +0200 Subject: [PATCH 1533/2522] New lvol option: shrink. (#2135) If shrink is set to false and size is lower than current lv size, dont try to shrink logical volume. --- system/lvol.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/system/lvol.py b/system/lvol.py index 609e1d3a0cf..71cb717c233 100644 --- a/system/lvol.py +++ b/system/lvol.py @@ -73,6 +73,12 @@ description: - Comma separated list of physical volumes e.g. /dev/sda,/dev/sdb required: false + shrink: + version_added: "2.2" + description: + - shrink if current size is higher than size requested + required: false + default: yes notes: - Filesystems on top of the volume are not resized. ''' @@ -111,6 +117,9 @@ # Reduce the logical volume to 512m - lvol: vg=firefly lv=test size=512 force=yes +# Set the logical volume to 512m and do not try to shrink if size is lower than current one +- lvol: vg=firefly lv=test size=512 shrink=no + # Remove the logical volume. - lvol: vg=firefly lv=test state=absent force=yes @@ -168,6 +177,7 @@ def main(): opts=dict(type='str'), state=dict(choices=["absent", "present"], default='present'), force=dict(type='bool', default='no'), + shrink=dict(type='bool', default='yes'), snapshot=dict(type='str', default=None), pvs=dict(type='str') ), @@ -190,6 +200,7 @@ def main(): opts = module.params['opts'] state = module.params['state'] force = module.boolean(module.params['force']) + shrink = module.boolean(module.params['shrink']) size_opt = 'L' size_unit = 'm' snapshot = module.params['snapshot'] @@ -328,7 +339,7 @@ def main(): tool = module.get_bin_path("lvextend", required=True) else: module.fail_json(msg="Logical Volume %s could not be extended. Not enough free space left (%s%s required / %s%s available)" % (this_lv['name'], (size_requested - this_lv['size']), unit, size_free, unit)) - elif this_lv['size'] > size_requested + this_vg['ext_size']: # more than an extent too large + elif shrink and this_lv['size'] > size_requested + this_vg['ext_size']: # more than an extent too large if size_requested == 0: module.fail_json(msg="Sorry, no shrinking of %s to 0 permitted." % (this_lv['name'])) elif not force: @@ -358,7 +369,7 @@ def main(): tool = None if int(size) > this_lv['size']: tool = module.get_bin_path("lvextend", required=True) - elif int(size) < this_lv['size']: + elif shrink and int(size) < this_lv['size']: if int(size) == 0: module.fail_json(msg="Sorry, no shrinking of %s to 0 permitted." % (this_lv['name'])) if not force: From 747f6f673653a0e6a97b24acac8dd4675d7ac6a8 Mon Sep 17 00:00:00 2001 From: Pavel Samokha Date: Fri, 13 May 2016 15:52:49 +0300 Subject: [PATCH 1534/2522] iptables module - add icmp_type --- system/iptables.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/system/iptables.py b/system/iptables.py index a714fe4699a..98ce0137938 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -266,6 +266,11 @@ description: - "Specifies the error packet type to return while rejecting." required: false + icmp_type: + version_added: "2.2" + description: + - "This allows specification of the ICMP type, which can be a numeric ICMP type, type/code pair, or one of the ICMP type names shown by the command 'iptables -p icmp -h'" + required: false ''' EXAMPLES = ''' @@ -342,6 +347,7 @@ def construct_rule(params): append_param(rule, params['uid_owner'], '--uid-owner', False) append_jump(rule, params['reject_with'], 'REJECT') append_param(rule, params['reject_with'], '--reject-with', False) + append_param(rule, params['icmp_type'], '--icmp_type', False) return rule @@ -399,6 +405,7 @@ def main(): limit_burst=dict(required=False, default=None, type='str'), uid_owner=dict(required=False, default=None, type='str'), reject_with=dict(required=False, default=None, type='str'), + icmp_type=dict(required=False, default=None, type='str'), ), mutually_exclusive=( ['set_dscp_mark', 'set_dscp_mark_class'], From 7092118119af79003ec8ce15312c723046c730ce Mon Sep 17 00:00:00 2001 From: Pavel Samokha Date: Fri, 13 May 2016 16:49:58 +0300 Subject: [PATCH 1535/2522] fix icmp-type --- system/iptables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/iptables.py b/system/iptables.py index 98ce0137938..4373a7a5e21 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -347,7 +347,7 @@ def construct_rule(params): append_param(rule, params['uid_owner'], '--uid-owner', False) append_jump(rule, params['reject_with'], 'REJECT') append_param(rule, params['reject_with'], '--reject-with', False) - append_param(rule, params['icmp_type'], '--icmp_type', False) + append_param(rule, params['icmp_type'], '--icmp-type', False) return rule From a0566037b4000ddaa20b09dea15ff603e2cf29ea Mon Sep 17 00:00:00 2001 From: Pavel Samokha Date: Fri, 13 May 2016 17:14:44 +0300 Subject: [PATCH 1536/2522] iptables module - icmp-type better doc style --- system/iptables.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/system/iptables.py b/system/iptables.py index 4373a7a5e21..f2298570965 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -269,7 +269,9 @@ icmp_type: version_added: "2.2" description: - - "This allows specification of the ICMP type, which can be a numeric ICMP type, type/code pair, or one of the ICMP type names shown by the command 'iptables -p icmp -h'" + - "This allows specification of the ICMP type, which can be a numeric ICMP type, + type/code pair, or one of the ICMP type names shown by the command + 'iptables -p icmp -h'" required: false ''' From f953d5dc0c1418dcdc5db72e8fb630b6aa83997f Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Fri, 13 May 2016 11:01:32 -0700 Subject: [PATCH 1537/2522] Docs fixes --- monitoring/zabbix_maintenance.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monitoring/zabbix_maintenance.py b/monitoring/zabbix_maintenance.py index 1d7caad30a9..89f792ce5d0 100644 --- a/monitoring/zabbix_maintenance.py +++ b/monitoring/zabbix_maintenance.py @@ -108,6 +108,7 @@ - The timeout of API request (seconds). default: 10 version_added: "2.1" + required: false notes: - Useful for setting hosts in maintenance mode before big update, and removing maintenance window after update. From 7618fd8749c9d71493bb9f8a67c4231bc0c17bd1 Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Sat, 14 May 2016 10:40:49 +0200 Subject: [PATCH 1538/2522] Fix check-mode incorrectly returning changed (#2220) The lvol module has a different logic in check-mode for knowing when a change is induced. And this logic is *only* based on a size check. However during a normal run, it is the lvreduce or lvextend tool that decides when a change is performed (or when the requested and existing sizes differ). So while in check-mode the module reports a change, in real run-mode it does not in fact changes anything an reports ok. One solution would be to implement the exact size-comparison logic that is implemented in lvextend and lvreduce, but we opted to use the `--test` option to each command to verify if a change is induced or not. In effect both check-mode and run-mode use the exact same logic and conclusion. --- system/lvol.py | 73 +++++++++++++++++++++++++------------------------- 1 file changed, 36 insertions(+), 37 deletions(-) diff --git a/system/lvol.py b/system/lvol.py index 71cb717c233..75d8c56ac9a 100644 --- a/system/lvol.py +++ b/system/lvol.py @@ -214,6 +214,12 @@ def main(): if opts is None: opts = "" + # Add --test option when running in check-mode + if module.check_mode: + test_opt = ' --test' + else: + test_opt = '' + if size: # LVCREATE(8) -l --extents option with percentage if '%' in size: @@ -297,28 +303,23 @@ def main(): if this_lv is None: if state == 'present': ### create LV - if module.check_mode: + lvcreate_cmd = module.get_bin_path("lvcreate", required=True) + if snapshot is not None: + cmd = "%s %s %s -%s %s%s -s -n %s %s %s/%s" % (lvcreate_cmd, test_opt, yesopt, size_opt, size, size_unit, snapshot, opts, vg, lv) + else: + cmd = "%s %s %s -n %s -%s %s%s %s %s %s" % (lvcreate_cmd, test_opt, yesopt, lv, size_opt, size, size_unit, opts, vg, pvs) + rc, _, err = module.run_command(cmd) + if rc == 0: changed = True else: - lvcreate_cmd = module.get_bin_path("lvcreate", required=True) - if snapshot is not None: - cmd = "%s %s -%s %s%s -s -n %s %s %s/%s" % (lvcreate_cmd, yesopt, size_opt, size, size_unit, snapshot, opts, vg, lv) - else: - cmd = "%s %s -n %s -%s %s%s %s %s %s" % (lvcreate_cmd, yesopt, lv, size_opt, size, size_unit, opts, vg, pvs) - rc, _, err = module.run_command(cmd) - if rc == 0: - changed = True - else: - module.fail_json(msg="Creating logical volume '%s' failed" % lv, rc=rc, err=err) + module.fail_json(msg="Creating logical volume '%s' failed" % lv, rc=rc, err=err) else: if state == 'absent': ### remove LV - if module.check_mode: - module.exit_json(changed=True) if not force: module.fail_json(msg="Sorry, no removal of logical volume %s without force=yes." % (this_lv['name'])) lvremove_cmd = module.get_bin_path("lvremove", required=True) - rc, _, err = module.run_command("%s --force %s/%s" % (lvremove_cmd, vg, this_lv['name'])) + rc, _, err = module.run_command("%s %s --force %s/%s" % (lvremove_cmd, test_opt, vg, this_lv['name'])) if rc == 0: module.exit_json(changed=True) else: @@ -349,20 +350,19 @@ def main(): tool = '%s %s' % (tool, '--force') if tool: - if module.check_mode: + cmd = "%s %s -%s %s%s %s/%s %s" % (tool, test_opt, size_opt, size, size_unit, vg, this_lv['name'], pvs) + rc, out, err = module.run_command(cmd) + if "Reached maximum COW size" in out: + module.fail_json(msg="Unable to resize %s to %s%s" % (lv, size, size_unit), rc=rc, err=err, out=out) + elif rc == 0: changed = True + msg="Volume %s resized to %s%s" % (this_lv['name'], size_requested, unit) + elif "matches existing size" in err: + module.exit_json(changed=False, vg=vg, lv=this_lv['name'], size=this_lv['size']) + elif "not larger than existing size" in err: + module.exit_json(changed=False, vg=vg, lv=this_lv['name'], size=this_lv['size'], msg="Original size is larger than requested size", err=err) else: - cmd = "%s -%s %s%s %s/%s %s" % (tool, size_opt, size, size_unit, vg, this_lv['name'], pvs) - rc, out, err = module.run_command(cmd) - if "Reached maximum COW size" in out: - module.fail_json(msg="Unable to resize %s to %s%s" % (lv, size, size_unit), rc=rc, err=err, out=out) - elif rc == 0: - changed = True - msg="Volume %s resized to %s%s" % (this_lv['name'], size_requested, unit) - elif "matches existing size" in err: - module.exit_json(changed=False, vg=vg, lv=this_lv['name'], size=this_lv['size']) - else: - module.fail_json(msg="Unable to resize %s to %s%s" % (lv, size, size_unit), rc=rc, err=err) + module.fail_json(msg="Unable to resize %s to %s%s" % (lv, size, size_unit), rc=rc, err=err) else: ### resize LV based on absolute values @@ -379,19 +379,18 @@ def main(): tool = '%s %s' % (tool, '--force') if tool: - if module.check_mode: + cmd = "%s %s -%s %s%s %s/%s %s" % (tool, test_opt, size_opt, size, size_unit, vg, this_lv['name'], pvs) + rc, out, err = module.run_command(cmd) + if "Reached maximum COW size" in out: + module.fail_json(msg="Unable to resize %s to %s%s" % (lv, size, size_unit), rc=rc, err=err, out=out) + elif rc == 0: changed = True + elif "matches existing size" in err: + module.exit_json(changed=False, vg=vg, lv=this_lv['name'], size=this_lv['size']) + elif "not larger than existing size" in err: + module.exit_json(changed=False, vg=vg, lv=this_lv['name'], size=this_lv['size'], msg="Original size is larger than requested size", err=err) else: - cmd = "%s -%s %s%s %s/%s %s" % (tool, size_opt, size, size_unit, vg, this_lv['name'], pvs) - rc, out, err = module.run_command(cmd) - if "Reached maximum COW size" in out: - module.fail_json(msg="Unable to resize %s to %s%s" % (lv, size, size_unit), rc=rc, err=err, out=out) - elif rc == 0: - changed = True - elif "matches existing size" in err: - module.exit_json(changed=False, vg=vg, lv=this_lv['name'], size=this_lv['size']) - else: - module.fail_json(msg="Unable to resize %s to %s%s" % (lv, size, size_unit), rc=rc, err=err) + module.fail_json(msg="Unable to resize %s to %s%s" % (lv, size, size_unit), rc=rc, err=err) module.exit_json(changed=changed, msg=msg) From 8c1b67292923cc6b6c0cff5eb014f1958cdaf70f Mon Sep 17 00:00:00 2001 From: Constantin Date: Sat, 14 May 2016 09:45:05 +0100 Subject: [PATCH 1539/2522] Added support for Standard - Infrequent Access stoarage class (#2134) --- cloud/amazon/s3_lifecycle.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/cloud/amazon/s3_lifecycle.py b/cloud/amazon/s3_lifecycle.py index bdfdc49da91..25415395361 100644 --- a/cloud/amazon/s3_lifecycle.py +++ b/cloud/amazon/s3_lifecycle.py @@ -65,10 +65,11 @@ choices: [ 'enabled', 'disabled' ] storage_class: description: - - "The storage class to transition to. Currently there is only one valid value - 'glacier'." + - "The storage class to transition to. Currently there are two supported values - 'glacier' or 'standard_ia'." + - "The 'standard_ia' class is only being available from Ansible version 2.2." required: false default: glacier - choices: [ 'glacier' ] + choices: [ 'glacier', 'standard_ia'] transition_date: description: - "Indicates the lifetime of the objects that are subject to the rule by the date they will transition to a different storage class. The value must be ISO-8601 format, the time must be midnight and a GMT timezone must be specified. If transition_days is not specified, this parameter is required." @@ -127,6 +128,15 @@ prefix: /logs/ state: absent +# Configure a lifecycle rule to transition all backup files older than 31 days in /backups/ to standard infrequent access class. +- s3_lifecycle: + name: mybucket + prefix: /backups/ + storage_class: standard_ia + transition_days: 31 + state: present + status: enabled + ''' import xml.etree.ElementTree as ET @@ -140,6 +150,7 @@ HAS_DATEUTIL = False try: + import boto import boto.ec2 from boto.s3.connection import OrdinaryCallingFormat, Location from boto.s3.lifecycle import Lifecycle, Rule, Expiration, Transition @@ -343,15 +354,15 @@ def main(): argument_spec = ec2_argument_spec() argument_spec.update( dict( - name = dict(required=True), + name = dict(required=True, type='str'), expiration_days = dict(default=None, required=False, type='int'), expiration_date = dict(default=None, required=False, type='str'), prefix = dict(default=None, required=False), requester_pays = dict(default='no', type='bool'), - rule_id = dict(required=False), + rule_id = dict(required=False, type='str'), state = dict(default='present', choices=['present', 'absent']), status = dict(default='enabled', choices=['enabled', 'disabled']), - storage_class = dict(default='glacier', choices=['glacier']), + storage_class = dict(default='glacier', type='str', choices=['glacier', 'standard_ia']), transition_days = dict(default=None, required=False, type='int'), transition_date = dict(default=None, required=False, type='str') ) @@ -392,6 +403,7 @@ def main(): expiration_date = module.params.get("expiration_date") transition_date = module.params.get("transition_date") state = module.params.get("state") + storage_class = module.params.get("storage_class") # If expiration_date set, check string is valid if expiration_date is not None: @@ -406,6 +418,10 @@ def main(): except ValueError, e: module.fail_json(msg="expiration_date is not a valid ISO-8601 format. The time must be midnight and a timezone of GMT must be included") + boto_required_version = (2,40,0) + if storage_class == 'standard_ia' and tuple(map(int, (boto.__version__.split(".")))) < boto_required_version: + module.fail_json(msg="'standard_ia' class requires boto >= 2.40.0") + if state == 'present': create_lifecycle_rule(connection, module) elif state == 'absent': From 3a90f78ccae065a05330f3ee83d083de02718c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Lalinsk=C3=BD?= Date: Sat, 14 May 2016 11:07:24 +0200 Subject: [PATCH 1540/2522] monitoring/zabbix_host: Fix (no) proxy handling When updating a host with no proxy explicitly set, the host was always reported as changed, because it was comparing `"0"` and `None`. --- monitoring/zabbix_host.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/monitoring/zabbix_host.py b/monitoring/zabbix_host.py index 70d323138c4..e6fec0b0252 100644 --- a/monitoring/zabbix_host.py +++ b/monitoring/zabbix_host.py @@ -362,8 +362,9 @@ def check_all_properties(self, host_id, host_groups, status, interfaces, templat if set(list(template_ids)) != set(exist_template_ids): return True - if host['proxy_hostid'] != proxy_id: - return True + if proxy_id is not None: + if host['proxy_hostid'] != proxy_id: + return True return False From 33281cc93ee7a0b12cf872946f43996858b730b0 Mon Sep 17 00:00:00 2001 From: Sergei Antipov Date: Mon, 16 May 2016 22:25:52 +0700 Subject: [PATCH 1541/2522] Fixed problem with pymongo compatibility (#1249) * Fixed problem with pymongo compatibility Fixes #11 --- database/misc/mongodb_user.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index b1bf41bdb1b..cbaebcbfd27 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -165,6 +165,17 @@ # MongoDB module specific support methods. # +def check_compatibility(module, client): + srv_info = client.server_info() + if LooseVersion(srv_info['version']) >= LooseVersion('3.2') and LooseVersion(PyMongoVersion) <= LooseVersion('3.2'): + module.fail_json(msg=' (Note: you must use pymongo 3.2+ with MongoDB >= 3.2)') + elif LooseVersion(srv_info['version']) >= LooseVersion('3.0') and LooseVersion(PyMongoVersion) <= LooseVersion('2.8'): + module.fail_json(msg=' (Note: you must use pymongo 2.8+ with MongoDB 3.0)') + elif LooseVersion(srv_info['version']) >= LooseVersion('2.6') and LooseVersion(PyMongoVersion) <= LooseVersion('2.7'): + module.fail_json(msg=' (Note: you must use pymongo 2.7+ with MongoDB 2.6)') + elif LooseVersion(PyMongoVersion) <= LooseVersion('2.5'): + module.fail_json(msg=' (Note: you must be on mongodb 2.4+ and pymongo 2.5+ to use the roles param)') + def user_find(client, user, db_name): for mongo_user in client["admin"].system.users.find(): if mongo_user['user'] == user and mongo_user['db'] == db_name: @@ -183,8 +194,6 @@ def user_add(module, client, db_name, user, password, roles): db.add_user(user, password, None, roles=roles) except OperationFailure, e: err_msg = str(e) - if LooseVersion(PyMongoVersion) <= LooseVersion('2.5'): - err_msg = err_msg + ' (Note: you must be on mongodb 2.4+ and pymongo 2.5+ to use the roles param)' module.fail_json(msg=err_msg) def user_remove(module, client, db_name, user): @@ -316,6 +325,8 @@ def main(): except ConnectionFailure, e: module.fail_json(msg='unable to connect to database: %s' % str(e)) + check_compatibility(module, client) + if state == 'present': if password is None and update_password == 'always': module.fail_json(msg='password parameter required when adding a user unless update_password is set to on_create') From 677a2dd66d5e1788eacc5d849ce6e1cc7d799833 Mon Sep 17 00:00:00 2001 From: Michael Perzel Date: Fri, 29 May 2015 14:16:05 -0500 Subject: [PATCH 1542/2522] Module to manage f5 virtual servers --- f5/bigip_gtm_virtual_server.py | 244 +++++++++++++++++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 f5/bigip_gtm_virtual_server.py diff --git a/f5/bigip_gtm_virtual_server.py b/f5/bigip_gtm_virtual_server.py new file mode 100644 index 00000000000..0f3e04877cf --- /dev/null +++ b/f5/bigip_gtm_virtual_server.py @@ -0,0 +1,244 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2015, Michael Perzel +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: bigip_gtm_virtual_server +short_description: "Manages F5 BIG-IP GTM virtual servers" +description: + - "Manages F5 BIG-IP GTM virtual servers" +version_added: "2.2" +author: 'Michael Perzel' +notes: + - "Requires BIG-IP software version >= 11.4" + - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" + - "Best run as a local_action in your playbook" + - "Tested with manager and above account privilege level" + +requirements: + - bigsuds +options: + server: + description: + - BIG-IP host + required: true + user: + description: + - BIG-IP username + required: true + password: + description: + - BIG-IP password + required: true + state: + description: + - Virtual server state + required: false + default: present + choices: ['present', 'absent','enabled','disabled'] + virtual_server_name: + description: + - Virtual server name + required: True + virtual_server_server: + description: + - Virtual server server + required: true + host: + description: + - Virtual server host + required: false + default: None + aliases: ['address'] + port: + description: + - Virtual server port + required: false + default: None +''' + +EXAMPLES = ''' + - name: Enable virtual server + local_action: > + bigip_gtm_virtual_server + server=192.168.0.1 + user=admin + password=mysecret + virtual_server_name=myname + virtual_server_server=myserver + state=enabled +''' + +RETURN = '''# ''' + +try: + import bigsuds +except ImportError: + bigsuds_found = False +else: + bigsuds_found = True + + +def bigip_api(server, user, password): + api = bigsuds.BIGIP(hostname=server, username=user, password=password) + return api + + +def server_exists(api, server): + # hack to determine if virtual server exists + result = False + try: + api.GlobalLB.Server.get_object_status([server]) + result = True + except bigsuds.OperationFailed, e: + if "was not found" in str(e): + result = False + else: + # genuine exception + raise + return result + + +def virtual_server_exists(api, name, server): + # hack to determine if virtual server exists + result = False + try: + virtual_server_id = {'name': name, 'server': server} + api.GlobalLB.VirtualServerV2.get_object_status([virtual_server_id]) + result = True + except bigsuds.OperationFailed, e: + if "was not found" in str(e): + result = False + else: + # genuine exception + raise + return result + + +def add_virtual_server(api, virtual_server_name, virtual_server_server, address, port): + addresses = {'address': address, 'port': port} + virtual_server_id = {'name': virtual_server_name, 'server': virtual_server_server} + api.GlobalLB.VirtualServerV2.create([virtual_server_id], [addresses]) + + +def remove_virtual_server(api, virtual_server_name, virtual_server_server): + virtual_server_id = {'name': virtual_server_name, 'server': virtual_server_server} + api.GlobalLB.VirtualServerV2.delete_virtual_server([virtual_server_id]) + + +def get_virtual_server_state(api, name, server): + virtual_server_id = {'name': name, 'server': server} + state = api.GlobalLB.VirtualServerV2.get_enabled_state([virtual_server_id]) + state = state[0].split('STATE_')[1].lower() + return state + + +def set_virtual_server_state(api, name, server, state): + virtual_server_id = {'name': name, 'server': server} + state = "STATE_%s" % state.strip().upper() + api.GlobalLB.VirtualServerV2.set_enabled_state([virtual_server_id], [state]) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + server=dict(type='str', required=True), + user=dict(type='str', required=True), + password=dict(type='str', required=True, no_log=True), + state=dict(type='str', default='present', choices=['present', 'absent', 'enabled', 'disabled']), + host=dict(type='str', default=None, aliases=['address']), + port=dict(type='int', default=None), + virtual_server_name=dict(type='str', required=True), + virtual_server_server=dict(type='str', required=True) + ), + supports_check_mode=True + ) + + if not bigsuds_found: + module.fail_json(msg="the python bigsuds module is required") + + server = module.params['server'] + user = module.params['user'] + password = module.params['password'] + virtual_server_name = module.params['virtual_server_name'] + virtual_server_server = module.params['virtual_server_server'] + state = module.params['state'] + address = module.params['host'] + port = module.params['port'] + + result = {'changed': False} # default + + try: + api = bigip_api(server, user, password) + + if state == 'absent': + if virtual_server_exists(api, virtual_server_name, virtual_server_server): + if not module.check_mode: + remove_virtual_server(api, virtual_server_name, virtual_server_server) + result = {'changed': True} + else: + # check-mode return value + result = {'changed': True} + elif state == 'present': + if virtual_server_name and virtual_server_server and address and port: + if not virtual_server_exists(api, virtual_server_name, virtual_server_server): + if not module.check_mode: + if server_exists(api, virtual_server_server): + add_virtual_server(api, virtual_server_name, virtual_server_server, address, port) + result = {'changed': True} + else: + module.fail_json(msg="server does not exist") + else: + # check-mode return value + result = {'changed': True} + else: + # virtual server exists -- potentially modify attributes --future feature + result = {'changed': False} + else: + module.fail_json(msg="Address and port are required to create virtual server") + elif state == 'enabled': + if not virtual_server_exists(api, virtual_server_name, virtual_server_server): + module.fail_json(msg="virtual server does not exist") + if state != get_virtual_server_state(api, virtual_server_name, virtual_server_server): + if not module.check_mode: + set_virtual_server_state(api, virtual_server_name, virtual_server_server, state) + result = {'changed': True} + else: + result = {'changed': True} + elif state == 'disabled': + if not virtual_server_exists(api, virtual_server_name, virtual_server_server): + module.fail_json(msg="virtual server does not exist") + if state != get_virtual_server_state(api, virtual_server_name, virtual_server_server): + if not module.check_mode: + set_virtual_server_state(api, virtual_server_name, virtual_server_server, state) + result = {'changed': True} + else: + result = {'changed': True} + + except Exception, e: + module.fail_json(msg="received exception: %s" % e) + + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From 735c1b6219f13f73597c3944a50b4e505417b612 Mon Sep 17 00:00:00 2001 From: Daniel Vigueras Date: Mon, 16 May 2016 22:25:52 +0200 Subject: [PATCH 1543/2522] Add insert support to iptables. (#1180) Add insert support to iptables. --- system/iptables.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/system/iptables.py b/system/iptables.py index f2298570965..d874161cdfa 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -56,6 +56,14 @@ required: false default: present choices: [ "present", "absent" ] + action: + version_added: "2.2" + description: + - Whether the rule should be appended at the bottom or inserted at the + top. If the rule already exists the chain won't be modified. + required: false + default: append + choices: [ "append", "insert" ] ip_version: description: - Which version of the IP protocol this rule should apply to. @@ -372,6 +380,11 @@ def append_rule(iptables_path, module, params): module.run_command(cmd, check_rc=True) +def insert_rule(iptables_path, module, params): + cmd = push_arguments(iptables_path, '-I', params) + module.run_command(cmd, check_rc=True) + + def remove_rule(iptables_path, module, params): cmd = push_arguments(iptables_path, '-D', params) module.run_command(cmd, check_rc=True) @@ -383,6 +396,7 @@ def main(): argument_spec=dict( table=dict(required=False, default='filter', choices=['filter', 'nat', 'mangle', 'raw', 'security']), state=dict(required=False, default='present', choices=['present', 'absent']), + action=dict(required=False, default='append', type='str', choices=['append', 'insert']), ip_version=dict(required=False, default='ipv4', choices=['ipv4', 'ipv6']), chain=dict(required=True, default=None, type='str'), protocol=dict(required=False, default=None, type='str'), @@ -422,6 +436,7 @@ def main(): rule=' '.join(construct_rule(module.params)), state=module.params['state'], ) + insert = (module.params['action'] == 'insert') ip_version = module.params['ip_version'] iptables_path = module.get_bin_path(BINS[ip_version], True) rule_is_present = check_present(iptables_path, module, module.params) @@ -439,7 +454,10 @@ def main(): module.exit_json(**args) if should_be_present: - append_rule(iptables_path, module, module.params) + if insert: + insert_rule(iptables_path, module, module.params) + else: + append_rule(iptables_path, module, module.params) else: remove_rule(iptables_path, module, module.params) From 4856fa6adfdc74f53ad136819a47a2b37f368ad1 Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Tue, 17 May 2016 13:25:40 +0200 Subject: [PATCH 1544/2522] Fix CI due to missing __init__.py on f5 folder (#2247) The CI for extras is failing on ansible-validate-modules due to f5 folder missing __init__.py. Adding an empty one to fix it. --- f5/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 f5/__init__.py diff --git a/f5/__init__.py b/f5/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From e91848dfd590663f8a034e12cc4676bd0ef7154c Mon Sep 17 00:00:00 2001 From: Shubham Date: Tue, 17 May 2016 20:10:10 +0530 Subject: [PATCH 1545/2522] Fix broken link for issue template (#2248) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bd06fcd804f..bd531784d3e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,7 +30,7 @@ The Github issue tracker is not the best place for questions for various reasons If you'd like to file a bug =========================== -Read the community page above, but in particular, make sure you copy [this issue template](https://github.com/ansible/ansible-modules-extras/blob/devel/ISSUE_TEMPLATE.md) into your ticket description. We have a friendly neighborhood bot that will remind you if you forget :) This template helps us organize tickets faster and prevents asking some repeated questions, so it's very helpful to us and we appreciate your help with it. +Read the community page above, but in particular, make sure you copy [this issue template](https://github.com/ansible/ansible-modules-extras/blob/devel/.github/ISSUE_TEMPLATE.md) into your ticket description. We have a friendly neighborhood bot that will remind you if you forget :) This template helps us organize tickets faster and prevents asking some repeated questions, so it's very helpful to us and we appreciate your help with it. Also please make sure you are testing on the latest released version of Ansible or the development branch. From 7e97b2131bc503893e2e3b51e21d3cfcfdfd2186 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Tue, 17 May 2016 19:32:14 +0200 Subject: [PATCH 1546/2522] Add support for checking module on python3, like on core (#2235) --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 70defe5a26f..461c710e578 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ addons: packages: - python2.4 - python2.6 + - python3.5 before_install: - git config user.name "ansible" - git config user.email "ansible@ansible.com" @@ -20,5 +21,7 @@ script: - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py|database/influxdb/influxdb.*\.py' . - python2.6 -m compileall -fq . - python2.7 -m compileall -fq . + - python3.4 -m compileall -fq system/at.py + - python3.5 -m compileall -fq system/at.py - ansible-validate-modules . #- ./test-docs.sh extras From c8864c32202edb9b80fee3116f0cde5d090295f3 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Tue, 17 May 2016 19:33:12 +0200 Subject: [PATCH 1547/2522] Use a python3 compatible notation for octal (#2238) --- cloud/lxc/lxc_container.py | 4 ++-- monitoring/boundary_meter.py | 2 +- system/cronvar.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index da9c486a868..bd77425e3e7 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -568,7 +568,7 @@ def create_script(command): f.close() # Ensure the script is executable. - os.chmod(script_file, 0700) + os.chmod(script_file, int('0700',8)) # Output log file. stdout_file = os.fdopen(tempfile.mkstemp(prefix='lxc-attach-script-log')[0], 'ab') @@ -1371,7 +1371,7 @@ def _create_tar(self, source_dir): :type source_dir: ``str`` """ - old_umask = os.umask(0077) + old_umask = os.umask(int('0077',8)) archive_path = self.module.params.get('archive_path') if not os.path.isdir(archive_path): diff --git a/monitoring/boundary_meter.py b/monitoring/boundary_meter.py index ef681704f04..3729b606a1c 100644 --- a/monitoring/boundary_meter.py +++ b/monitoring/boundary_meter.py @@ -220,7 +220,7 @@ def download_request(module, name, apiid, apikey, cert_type): cert_file = open(cert_file_path, 'w') cert_file.write(body) cert_file.close() - os.chmod(cert_file_path, 0600) + os.chmod(cert_file_path, int('0600', 8)) except: module.fail_json("Could not write to certificate file") diff --git a/system/cronvar.py b/system/cronvar.py index 1068739c0d0..64ea2cb1f2b 100644 --- a/system/cronvar.py +++ b/system/cronvar.py @@ -363,7 +363,7 @@ def main(): res_args = dict() # Ensure all files generated are only writable by the owning user. Primarily relevant for the cron_file option. - os.umask(022) + os.umask(int('022',8)) cronvar = CronVar(module, user, cron_file) module.debug('cronvar instantiated - name: "%s"' % name) From 84ec0c8fafbac43d64ce9adf3a741c97b433f64b Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Tue, 17 May 2016 19:34:01 +0200 Subject: [PATCH 1548/2522] Port vca_nat and vca_fw to py3 compatible syntax (#2243) Since they both depend on library that cannot run on python2.4, cf https://github.com/ansible/ansible/pull/15870, we can use directly the python 2.6 syntax, as seen on the porting doc. --- cloud/vmware/vca_fw.py | 2 +- cloud/vmware/vca_nat.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/vmware/vca_fw.py b/cloud/vmware/vca_fw.py index 30bb16b6a27..617430abf25 100644 --- a/cloud/vmware/vca_fw.py +++ b/cloud/vmware/vca_fw.py @@ -184,7 +184,7 @@ def main(): try: desired_rules = validate_fw_rules(fw_rules) - except VcaError, e: + except VcaError as e: module.fail_json(msg=e.message) result = dict(changed=False) diff --git a/cloud/vmware/vca_nat.py b/cloud/vmware/vca_nat.py index b7516d5448f..7dfa0cd3a67 100644 --- a/cloud/vmware/vca_nat.py +++ b/cloud/vmware/vca_nat.py @@ -154,7 +154,7 @@ def main(): try: desired_rules = validate_nat_rules(nat_rules) - except VcaError, e: + except VcaError as e: module.fail_json(msg=e.message) rules = gateway.get_nat_rules() From 8ecc3d2516c92429367e2d0c408a74b3394d58cd Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Tue, 17 May 2016 10:39:23 -0700 Subject: [PATCH 1549/2522] Port vspherer_copy to pass syntax checks on python3 --- cloud/vmware/vsphere_copy.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/cloud/vmware/vsphere_copy.py b/cloud/vmware/vsphere_copy.py index b51efa2d777..931645235f8 100644 --- a/cloud/vmware/vsphere_copy.py +++ b/cloud/vmware/vsphere_copy.py @@ -82,6 +82,9 @@ import errno import socket +from ansible.module_utils.basic import AnsibleModule, get_exception +from ansible.module_utils.urls import open_url + def vmware_path(datastore, datacenter, path): ''' Constructs a URL path that VSphere accepts reliably ''' path = "/folder/%s" % path.lstrip("/") @@ -140,13 +143,15 @@ def main(): r = open_url(url, data=data, headers=headers, method='PUT', url_username=login, url_password=password, validate_certs=validate_certs, force_basic_auth=True) - except socket.error, e: + except socket.error: + e = get_exception() if isinstance(e.args, tuple) and e[0] == errno.ECONNRESET: # VSphere resets connection if the file is in use and cannot be replaced module.fail_json(msg='Failed to upload, image probably in use', status=None, errno=e[0], reason=str(e), url=url) else: module.fail_json(msg=str(e), status=None, errno=e[0], reason=str(e), url=url) - except Exception, e: + except Exception: + e = get_exception() error_code = -1 try: if isinstance(e[0], int): @@ -167,8 +172,5 @@ def main(): module.fail_json(msg='Failed to upload', errno=None, status=status, reason=r.msg, length=length, headers=dict(r.headers), chunked=chunked, url=url) -# Import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * if __name__ == '__main__': main() From e710dc47fe35fa2e05f57c184f34e2763f9ac864 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Tue, 17 May 2016 10:39:47 -0700 Subject: [PATCH 1550/2522] Add vmware and lxc to python3 checks --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 461c710e578..32cc679056e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,7 @@ script: - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py|database/influxdb/influxdb.*\.py' . - python2.6 -m compileall -fq . - python2.7 -m compileall -fq . - - python3.4 -m compileall -fq system/at.py - - python3.5 -m compileall -fq system/at.py + - python3.4 -m compileall -fq system/at.py cloud/vmware cloud/lxc + - python3.5 -m compileall -fq system/at.py cloud/vmware cloud/lxc - ansible-validate-modules . #- ./test-docs.sh extras From b742ab126374fd0109fa7f6a9869094c34bb1b83 Mon Sep 17 00:00:00 2001 From: kubilus1 Date: Wed, 18 May 2016 03:26:58 -0400 Subject: [PATCH 1551/2522] Check to see existence of same named/location system before creation. (#2150) --- cloud/softlayer/sl_vm.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/cloud/softlayer/sl_vm.py b/cloud/softlayer/sl_vm.py index 8ef808d9a2e..44772fbd902 100644 --- a/cloud/softlayer/sl_vm.py +++ b/cloud/softlayer/sl_vm.py @@ -225,6 +225,16 @@ def create_virtual_instance(module): + instances = vsManager.list_instances( + hostname = module.params.get('hostname'), + domain = module.params.get('domain'), + datacenter = module.params.get('datacenter') + ) + + if instances: + return False, None + + # Check if OS or Image Template is provided (Can't be both, defaults to OS) if (module.params.get('os_code') != None and module.params.get('os_code') != ''): module.params['image_id'] = '' @@ -303,6 +313,7 @@ def cancel_instance(module): def main(): + module = AnsibleModule( argument_spec=dict( instance_id=dict(), @@ -338,7 +349,7 @@ def main(): elif module.params.get('state') == 'present': (changed, instance) = create_virtual_instance(module) - if module.params.get('wait') == True: + if module.params.get('wait') == True and instance: (changed, instance) = wait_for_instance(module, instance['id']) module.exit_json(changed=changed, instance=json.loads(json.dumps(instance, default=lambda o: o.__dict__))) From 2a5812a0e8d09df8bd07f63a129e58898cf1b0d8 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 18 May 2016 06:22:36 -0700 Subject: [PATCH 1552/2522] Move the import of get_exception to pycompat24 --- cloud/vmware/vsphere_copy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cloud/vmware/vsphere_copy.py b/cloud/vmware/vsphere_copy.py index 931645235f8..41971fa977d 100644 --- a/cloud/vmware/vsphere_copy.py +++ b/cloud/vmware/vsphere_copy.py @@ -82,7 +82,8 @@ import errno import socket -from ansible.module_utils.basic import AnsibleModule, get_exception +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception from ansible.module_utils.urls import open_url def vmware_path(datastore, datacenter, path): From 010286aafd5dde45c3317ee4c26edd9915edb5dd Mon Sep 17 00:00:00 2001 From: Victor Costan Date: Wed, 18 May 2016 11:10:52 -0700 Subject: [PATCH 1553/2522] Add aws_mfa_devices module for multi-factor authentication on AWS. (#1987) --- cloud/amazon/aws_mfa_devices.py | 135 ++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 cloud/amazon/aws_mfa_devices.py diff --git a/cloud/amazon/aws_mfa_devices.py b/cloud/amazon/aws_mfa_devices.py new file mode 100644 index 00000000000..237c9c66b3f --- /dev/null +++ b/cloud/amazon/aws_mfa_devices.py @@ -0,0 +1,135 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: aws_mfa_devices +short_description: List the MFA (Multi-Factor Authentication) devices registered for a user +description: + - List the MFA (Multi-Factor Authentication) devices registered for a user +version_added: "2.2" +author: Victor Costan (@pwnall) +options: + user_name: + description: + - The name of the user whose MFA devices will be listed + required: false + default: null +extends_documentation_fragment: + - aws + - ec2 +requirements: + - boto3 + - botocore +''' + +RETURN = """ +devices: + description: The MFA devices registered for the given user + returned: always + type: list + sample: + - enable_date: "2016-03-11T23:25:36+00:00" + serial_number: arn:aws:iam::085120003701:mfa/pwnall + user_name: pwnall + - enable_date: "2016-03-11T23:25:37+00:00" + serial_number: arn:aws:iam::085120003702:mfa/pwnall + user_name: pwnall +changed: + description: True if listing the devices succeeds + type: bool + returned: always +""" + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# List MFA devices (more details: http://docs.aws.amazon.com/IAM/latest/APIReference/API_ListMFADevices.html) +aws_mfa_devices: +register: mfa_devices + +# Assume an existing role (more details: http://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html) +sts_assume_role: + mfa_serial_number: "{{ mfa_devices.devices[0].serial_number }}" + role_arn: "arn:aws:iam::123456789012:role/someRole" + role_session_name: "someRoleSession" +register: assumed_role +''' + +try: + import boto3 + from botocore.exceptions import ClientError + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + + +def normalize_mfa_device(mfa_device): + serial_number = mfa_device.get('SerialNumber', None) + user_name = mfa_device.get('UserName', None) + enable_date = mfa_device.get('EnableDate', None) + return { + 'serial_number': serial_number, + 'user_name': user_name, + 'enable_date': enable_date + } + +def list_mfa_devices(connection, module): + user_name = module.params.get('user_name') + changed = False + + args = {} + if user_name is not None: + args['UserName'] = user_name + try: + response = connection.list_mfa_devices(**args) + changed = True + except ClientError, e: + module.fail_json(msg=e) + + mfa_devices = response.get('MFADevices', []) + devices = [normalize_mfa_device(mfa_device) for mfa_device in mfa_devices] + + module.exit_json(changed=changed, devices=devices) + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + user_name = dict(required=False, default=None) + ) + ) + + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO3: + module.fail_json(msg='boto3 and botocore are required.') + + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + if region: + connection = boto3_conn(module, conn_type='client', resource='iam', region=region, endpoint=ec2_url, **aws_connect_kwargs) + else: + module.fail_json(msg="region must be specified") + + list_mfa_devices(connection, module) + + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From 76dee3d872906c2f5cdeadb2b3cef28340aacad6 Mon Sep 17 00:00:00 2001 From: Victor Costan Date: Wed, 18 May 2016 11:13:49 -0700 Subject: [PATCH 1554/2522] Add sts_session_token module for short-lived AWS credentials. (#1988) --- cloud/amazon/sts_session_token.py | 159 ++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 cloud/amazon/sts_session_token.py diff --git a/cloud/amazon/sts_session_token.py b/cloud/amazon/sts_session_token.py new file mode 100644 index 00000000000..dc284deaecd --- /dev/null +++ b/cloud/amazon/sts_session_token.py @@ -0,0 +1,159 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: sts_session_token +short_description: Obtain a session token from the AWS Security Token Service +description: + - Obtain a session token from the AWS Security Token Service +version_added: "2.2" +author: Victor Costan (@pwnall) +options: + duration_seconds: + description: + - The duration, in seconds, of the session token. See http://docs.aws.amazon.com/STS/latest/APIReference/API_GetSessionToken.html#API_GetSessionToken_RequestParameters for acceptable and default values. + required: false + default: null + mfa_serial_number: + description: + - The identification number of the MFA device that is associated with the user who is making the GetSessionToken call. + required: false + default: null + mfa_token: + description: + - The value provided by the MFA device, if the trust policy of the user requires MFA. + required: false + default: null +notes: + - In order to use the session token in a following playbook task you must pass the I(access_key), I(access_secret) and I(access_token). +extends_documentation_fragment: + - aws + - ec2 +requirements: + - boto3 + - botocore +''' + +RETURN = """ +sts_creds: + description: The Credentials object returned by the AWS Security Token Service + returned: always + type: list + sample: + access_key: ASXXXXXXXXXXXXXXXXXX + expiration: "2016-04-08T11:59:47+00:00" + secret_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + session_token: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +changed: + description: True if obtaining the credentials succeeds + type: bool + returned: always +""" + + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Get a session token (more details: http://docs.aws.amazon.com/STS/latest/APIReference/API_GetSessionToken.html) +sts_session_token: + duration: 3600 +register: session_credentials + +# Use the session token obtained above to tag an instance in account 123456789012 +ec2_tag: + aws_access_key: "{{ session_credentials.sts_creds.access_key }}" + aws_secret_key: "{{ session_credentials.sts_creds.secret_key }}" + security_token: "{{ session_credentials.sts_creds.session_token }}" + resource: i-xyzxyz01 + state: present + tags: + MyNewTag: value + +''' + +try: + import boto3 + from botocore.exceptions import ClientError + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + +def normalize_credentials(credentials): + access_key = credentials.get('AccessKeyId', None) + secret_key = credentials.get('SecretAccessKey', None) + session_token = credentials.get('SessionToken', None) + expiration = credentials.get('Expiration', None) + return { + 'access_key': access_key, + 'secret_key': secret_key, + 'session_token': session_token, + 'expiration': expiration + } + +def get_session_token(connection, module): + duration_seconds = module.params.get('duration_seconds') + mfa_serial_number = module.params.get('mfa_serial_number') + mfa_token = module.params.get('mfa_token') + changed = False + + args = {} + if duration_seconds is not None: + args['DurationSeconds'] = duration_seconds + if mfa_serial_number is not None: + args['SerialNumber'] = mfa_serial_number + if mfa_token is not None: + args['TokenCode'] = mfa_token + + try: + response = connection.get_session_token(**args) + changed = True + except ClientError, e: + module.fail_json(msg=e) + + credentials = normalize_credentials(response.get('Credentials', {})) + module.exit_json(changed=changed, sts_creds=credentials) + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + duration_seconds = dict(required=False, default=None, type='int'), + mfa_serial_number = dict(required=False, default=None), + mfa_token = dict(required=False, default=None) + ) + ) + + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO3: + module.fail_json(msg='boto3 and botocore are required.') + + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + if region: + connection = boto3_conn(module, conn_type='client', resource='sts', region=region, endpoint=ec2_url, **aws_connect_kwargs) + else: + module.fail_json(msg="region must be specified") + + get_session_token(connection, module) + + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From 8fb8ec2e98d38f129d99710464aac43e8b80a972 Mon Sep 17 00:00:00 2001 From: Rob Date: Thu, 19 May 2016 15:55:54 +1000 Subject: [PATCH 1555/2522] Add idempotence to ec2_eni when private_ip_address is supplied (#2172) --- cloud/amazon/ec2_eni.py | 71 +++++++++++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/cloud/amazon/ec2_eni.py b/cloud/amazon/ec2_eni.py index 58f1b0caf72..8403cbbbe7b 100644 --- a/cloud/amazon/ec2_eni.py +++ b/cloud/amazon/ec2_eni.py @@ -274,9 +274,12 @@ def create_eni(connection, vpc_id, module): module.exit_json(changed=changed, interface=get_eni_info(eni)) -def modify_eni(connection, vpc_id, module): +def modify_eni(connection, vpc_id, module, looked_up_eni_id): - eni_id = module.params.get("eni_id") + if looked_up_eni_id is None: + eni_id = module.params.get("eni_id") + else: + eni_id = looked_up_eni_id instance_id = module.params.get("instance_id") if instance_id == 'None': instance_id = None @@ -413,39 +416,58 @@ def get_sec_group_list(groups): return remote_security_groups -def _get_vpc_id(conn, subnet_id): +def _get_vpc_id(connection, module, subnet_id): try: - return conn.get_all_subnets(subnet_ids=[subnet_id])[0].vpc_id + return connection.get_all_subnets(subnet_ids=[subnet_id])[0].vpc_id except BotoServerError as e: module.fail_json(msg=e.message) +def get_eni_id_by_ip(connection, module): + + subnet_id = module.params.get('subnet_id') + private_ip_address = module.params.get('private_ip_address') + + try: + all_eni = connection.get_all_network_interfaces(filters={'private-ip-address': private_ip_address, 'subnet-id': subnet_id}) + except BotoServerError as e: + module.fail_json(msg=e.message) + + if all_eni: + return all_eni[0].id + else: + return None + + def main(): argument_spec = ec2_argument_spec() argument_spec.update( dict( - eni_id = dict(default=None), - instance_id = dict(default=None), - private_ip_address = dict(), - subnet_id = dict(), - description = dict(), - security_groups = dict(type='list'), - device_index = dict(default=0, type='int'), - state = dict(default='present', choices=['present', 'absent']), - force_detach = dict(default='no', type='bool'), - source_dest_check = dict(default=None, type='bool'), - delete_on_termination = dict(default=None, type='bool'), - secondary_private_ip_addresses = dict(default=None, type='list'), - secondary_private_ip_address_count = dict(default=None, type='int') + eni_id=dict(default=None, type='str'), + instance_id=dict(default=None, type='str'), + private_ip_address=dict(type='str'), + subnet_id=dict(type='str'), + description=dict(type='str'), + security_groups=dict(default=[], type='list'), + device_index=dict(default=0, type='int'), + state=dict(default='present', choices=['present', 'absent']), + force_detach=dict(default='no', type='bool'), + source_dest_check=dict(default=None, type='bool'), + delete_on_termination=dict(default=None, type='bool'), + secondary_private_ip_addresses=dict(default=None, type='list'), + secondary_private_ip_address_count=dict(default=None, type='int') ) ) module = AnsibleModule(argument_spec=argument_spec, - required_if = ([ + mutually_exclusive=[ + ['secondary_private_ip_addresses', 'secondary_private_ip_address_count'] + ], + required_if=([ ('state', 'present', ['subnet_id']), ('state', 'absent', ['eni_id']), - ]) + ]) ) if not HAS_BOTO: @@ -464,22 +486,23 @@ def main(): state = module.params.get("state") eni_id = module.params.get("eni_id") + private_ip_address = module.params.get('private_ip_address') if state == 'present': subnet_id = module.params.get("subnet_id") - vpc_id = _get_vpc_id(vpc_connection, subnet_id) + vpc_id = _get_vpc_id(vpc_connection, module, subnet_id) + # If private_ip_address is not None, look up to see if an ENI already exists with that IP + if eni_id is None and private_ip_address is not None: + eni_id = get_eni_id_by_ip(connection, module) if eni_id is None: create_eni(connection, vpc_id, module) else: - modify_eni(connection, vpc_id, module) + modify_eni(connection, vpc_id, module, eni_id) elif state == 'absent': delete_eni(connection, module) from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * -# this is magic, see lib/ansible/module_common.py -#<> - if __name__ == '__main__': main() From ce5a9b6c5f1fba02fc1f5ce7217f1e497e3121c4 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 19 May 2016 08:21:33 +0200 Subject: [PATCH 1556/2522] zypper: (Bugfix) Change command option --verbose to --details which is supported since 2008 (#2224) --- packaging/os/zypper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/os/zypper.py b/packaging/os/zypper.py index 30d69aa03d8..f90b4c250da 100644 --- a/packaging/os/zypper.py +++ b/packaging/os/zypper.py @@ -140,7 +140,7 @@ def get_installed_state(m, packages): "get installed state of packages" cmd = get_cmd(m, 'search') - cmd.extend(['--match-exact', '--verbose', '--installed-only']) + cmd.extend(['--match-exact', '--details', '--installed-only']) cmd.extend(packages) return parse_zypper_xml(m, cmd, fail_not_found=False)[0] From 33f3612e5c089936603a58f9cb5f5688bec2e91b Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Thu, 19 May 2016 15:22:39 -0700 Subject: [PATCH 1557/2522] Move running py3 compile test from whitelist to blacklist (#2254) --- .travis.yml | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 123 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 32cc679056e..142d833c715 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,127 @@ addons: - python2.4 - python2.6 - python3.5 +env: + global: + - PY3_EXCLUDE_LIST="cloud/amazon/aws_mfa_devices.py + cloud/amazon/cloudtrail.py + cloud/amazon/dynamodb_table.py + cloud/amazon/ec2_ami_copy.py + cloud/amazon/ec2_customer_gateway.py + cloud/amazon/ec2_elb_facts.py + cloud/amazon/ec2_eni_facts.py + cloud/amazon/ec2_eni.py + cloud/amazon/ec2_remote_facts.py + cloud/amazon/ec2_snapshot_facts.py + cloud/amazon/ec2_vol_facts.py + cloud/amazon/ec2_vpc_igw.py + cloud/amazon/ec2_vpc_net_facts.py + cloud/amazon/ec2_vpc_route_table_facts.py + cloud/amazon/ec2_vpc_route_table.py + cloud/amazon/ec2_vpc_subnet_facts.py + cloud/amazon/ec2_vpc_subnet.py + cloud/amazon/ecs_cluster.py + cloud/amazon/ecs_service_facts.py + cloud/amazon/ecs_service.py + cloud/amazon/ecs_taskdefinition.py + cloud/amazon/ecs_task.py + cloud/amazon/route53_facts.py + cloud/amazon/route53_health_check.py + cloud/amazon/route53_zone.py + cloud/amazon/s3_bucket.py + cloud/amazon/s3_lifecycle.py + cloud/amazon/s3_logging.py + cloud/amazon/sns_topic.py + cloud/amazon/sqs_queue.py + cloud/amazon/sts_assume_role.py + cloud/amazon/sts_session_token.py + cloud/centurylink/clc_aa_policy.py + cloud/centurylink/clc_group.py + cloud/centurylink/clc_publicip.py + cloud/google/gce_img.py + cloud/google/gce_tag.py + cloud/misc/ovirt.py + cloud/misc/proxmox.py + cloud/misc/proxmox_template.py + cloud/misc/virt_net.py + cloud/misc/virt_pool.py + cloud/misc/virt.py + cloud/profitbricks/profitbricks.py + cloud/profitbricks/profitbricks_volume.py + cloud/rackspace/rax_clb_ssl.py + cloud/xenserver_facts.py + clustering/consul_acl.py + clustering/consul_kv.py + clustering/consul.py + clustering/consul_session.py + commands/expect.py + database/misc/mongodb_parameter.py + database/misc/mongodb_user.py + database/misc/redis.py + database/mysql/mysql_replication.py + database/postgresql/postgresql_ext.py + database/postgresql/postgresql_lang.py + database/vertica/vertica_configuration.py + database/vertica/vertica_facts.py + database/vertica/vertica_role.py + database/vertica/vertica_schema.py + database/vertica/vertica_user.py + f5/bigip_gtm_virtual_server.py + files/patch.py + monitoring/bigpanda.py + monitoring/boundary_meter.py + monitoring/circonus_annotation.py + monitoring/datadog_monitor.py + monitoring/rollbar_deployment.py + monitoring/sensu_check.py + monitoring/stackdriver.py + monitoring/zabbix_group.py + monitoring/zabbix_hostmacro.py + monitoring/zabbix_host.py + monitoring/zabbix_screen.py + network/citrix/netscaler.py + network/cloudflare_dns.py + network/dnsimple.py + network/dnsmadeeasy.py + network/f5/bigip_facts.py + network/f5/bigip_gtm_wide_ip.py + network/f5/bigip_monitor_http.py + network/f5/bigip_monitor_tcp.py + network/f5/bigip_node.py + network/f5/bigip_pool_member.py + network/f5/bigip_pool.py + network/f5/bigip_virtual_server.py + network/nmcli.py + network/openvswitch_bridge.py + network/openvswitch_port.py + notification/hipchat.py + notification/irc.py + notification/jabber.py + notification/mail.py + notification/mqtt.py + notification/sns.py + notification/typetalk.py + packaging/os/layman.py + packaging/os/yum_repository.py + source_control/gitlab_group.py + source_control/gitlab_project.py + source_control/gitlab_user.py + system/alternatives.py + system/cronvar.py + system/crypttab.py + system/getent.py + system/gluster_volume.py + system/known_hosts.py + system/locale_gen.py + system/modprobe.py + system/osx_defaults.py + system/selinux_permissive.py + system/seport.py + system/svc.py + web_infrastructure/deploy_helper.py + web_infrastructure/ejabberd_user.py + web_infrastructure/jira.py + windows/win_unzip.py" before_install: - git config user.name "ansible" - git config user.email "ansible@ansible.com" @@ -21,7 +142,7 @@ script: - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py|database/influxdb/influxdb.*\.py' . - python2.6 -m compileall -fq . - python2.7 -m compileall -fq . - - python3.4 -m compileall -fq system/at.py cloud/vmware cloud/lxc - - python3.5 -m compileall -fq system/at.py cloud/vmware cloud/lxc + - python3.4 -m compileall -fq . -x $(echo "$PY3_EXCLUDE_LIST"| tr ' ' '|') + - python3.5 -m compileall -fq . -x $(echo "$PY3_EXCLUDE_LIST"| tr ' ' '|') - ansible-validate-modules . #- ./test-docs.sh extras From 896f6dcd34e3016e7de0ea69258cfa308ff4c373 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Fri, 20 May 2016 10:25:10 +0200 Subject: [PATCH 1558/2522] blockinfile will always add newline at EOF (#2261) --- files/blockinfile.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/files/blockinfile.py b/files/blockinfile.py index 37d89ca2c88..81834dfd2da 100644 --- a/files/blockinfile.py +++ b/files/blockinfile.py @@ -280,7 +280,9 @@ def main(): lines[n0:n0] = blocklines if lines: - result = '\n'.join(lines)+'\n' + result = '\n'.join(lines) + if original.endswith('\n'): + result += '\n' else: result = '' if original == result: From 14a0b7b9b7355ef9a6d15c48002629a8c697696c Mon Sep 17 00:00:00 2001 From: Orion Poplawski Date: Fri, 20 May 2016 06:46:52 -0600 Subject: [PATCH 1559/2522] Fix gw4/gw6 typo (#1841) --- network/nmcli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/network/nmcli.py b/network/nmcli.py index bfdd1caf76b..2c553425e94 100644 --- a/network/nmcli.py +++ b/network/nmcli.py @@ -625,7 +625,7 @@ def modify_connection_team(self): cmd.append(self.ip6) if self.gw6 is not None: cmd.append('ipv6.gateway') - cmd.append(self.gw4) + cmd.append(self.gw6) if self.dns6 is not None: cmd.append('ipv6.dns') cmd.append(self.dns6) @@ -745,7 +745,7 @@ def modify_connection_bond(self): cmd.append(self.ip6) if self.gw6 is not None: cmd.append('ipv6.gateway') - cmd.append(self.gw4) + cmd.append(self.gw6) if self.dns6 is not None: cmd.append('ipv6.dns') cmd.append(self.dns6) @@ -846,7 +846,7 @@ def modify_connection_ethernet(self): cmd.append(self.ip6) if self.gw6 is not None: cmd.append('ipv6.gateway') - cmd.append(self.gw4) + cmd.append(self.gw6) if self.dns6 is not None: cmd.append('ipv6.dns') cmd.append(self.dns6) From 29be1310e16b96edcef7a6817c5581d29f77a8b8 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Fri, 20 May 2016 09:19:10 -0400 Subject: [PATCH 1560/2522] add missing author info --- system/pam_limits.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/system/pam_limits.py b/system/pam_limits.py index 1c56d852bbb..30a31b01b9d 100644 --- a/system/pam_limits.py +++ b/system/pam_limits.py @@ -27,6 +27,8 @@ --- module: pam_limits version_added: "2.0" +authors: + - "Sebastien Rohaut (@usawa)" short_description: Modify Linux PAM limits description: - The M(pam_limits) module modify PAM limits, default in /etc/security/limits.conf. From cdffb3664205f2e01a859251f4d3534d1016f05f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Sat, 21 May 2016 00:38:48 +0200 Subject: [PATCH 1561/2522] softlayer: fix module name in doc (#2266) --- cloud/softlayer/sl_vm.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/softlayer/sl_vm.py b/cloud/softlayer/sl_vm.py index 44772fbd902..994416db245 100644 --- a/cloud/softlayer/sl_vm.py +++ b/cloud/softlayer/sl_vm.py @@ -16,7 +16,7 @@ DOCUMENTATION = ''' --- -module: SoftLayer +module: sl_vm short_description: create or cancel a virtual instance in SoftLayer description: - Creates or cancels SoftLayer instances. When created, optionally waits for it to be 'running'. @@ -230,7 +230,7 @@ def create_virtual_instance(module): domain = module.params.get('domain'), datacenter = module.params.get('datacenter') ) - + if instances: return False, None @@ -308,7 +308,7 @@ def cancel_instance(module): canceled = False else: return False, None - + return canceled, None From 3d54bdd4e14bcbe941ab8a28a7d865eababfb4d5 Mon Sep 17 00:00:00 2001 From: Mariano Lasala Date: Sat, 21 May 2016 00:39:17 +0200 Subject: [PATCH 1562/2522] Update sl_vm.py (#2267) There was a mistype in DATACENTER list with 'lon2', changed to 'lon02'. --- cloud/softlayer/sl_vm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/softlayer/sl_vm.py b/cloud/softlayer/sl_vm.py index 994416db245..d82b1da72d9 100644 --- a/cloud/softlayer/sl_vm.py +++ b/cloud/softlayer/sl_vm.py @@ -205,7 +205,7 @@ #TODO: get this info from API STATES = ['present', 'absent'] -DATACENTERS = ['ams01','ams03','dal01','dal05','dal06','dal09','fra02','hkg02','hou02','lon2','mel01','mex01','mil01','mon01','par01','sjc01','sjc03','sao01','sea01','sng01','syd01','tok02','tor01','wdc01','wdc04'] +DATACENTERS = ['ams01','ams03','dal01','dal05','dal06','dal09','fra02','hkg02','hou02','lon02','mel01','mex01','mil01','mon01','par01','sjc01','sjc03','sao01','sea01','sng01','syd01','tok02','tor01','wdc01','wdc04'] CPU_SIZES = [1,2,4,8,16] MEMORY_SIZES = [1024,2048,4096,6144,8192,12288,16384,32768,49152,65536] INITIALDISK_SIZES = [25,100] From d2900e856b7f8a27631638dd4024c22b947dc27a Mon Sep 17 00:00:00 2001 From: Corwin Brown Date: Fri, 20 May 2016 19:25:24 -0500 Subject: [PATCH 1563/2522] Add Win Robocopy module (#1078) * Added more robust error handling * Add Win Synchronize module Renamed win_synchronize to win_robocopy Updating email address Adding "flags" argument. Adding a "flags" argument that will allow the user to pass args directly to robocopy. If "flags" is set, recurse and purge will be ignored. Add return code to output Added bits to support check mode Fixing typo in Documentation Updated Documentation to have "RETURNED" field Updated win_robocopy.py to have the RETURNED field. I also noticed that win_robocopy.ps1 wasn't really using the "changed" attribute, so I went in and made sure it was being set appropriately. Forcing bool type for recurse and purge flag Updated "version_added" --- windows/win_robocopy.ps1 | 147 +++++++++++++++++++++++++++++++++++++++ windows/win_robocopy.py | 143 +++++++++++++++++++++++++++++++++++++ 2 files changed, 290 insertions(+) create mode 100644 windows/win_robocopy.ps1 create mode 100644 windows/win_robocopy.py diff --git a/windows/win_robocopy.ps1 b/windows/win_robocopy.ps1 new file mode 100644 index 00000000000..69cf9ee3e3a --- /dev/null +++ b/windows/win_robocopy.ps1 @@ -0,0 +1,147 @@ +#!powershell +# This file is part of Ansible +# +# Copyright 2015, Corwin Brown +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# WANT_JSON +# POWERSHELL_COMMON + +$params = Parse-Args $args; + +$result = New-Object psobject @{ + win_robocopy = New-Object psobject @{ + recurse = $false + purge = $false + } + changed = $false +} + +$src = Get-AnsibleParam -obj $params -name "src" -failifempty $true +$dest = Get-AnsibleParam -obj $params -name "dest" -failifempty $true +$purge = ConvertTo-Bool (Get-AnsibleParam -obj $params -name "purge" -default $false) +$recurse = ConvertTo-Bool (Get-AnsibleParam -obj $params -name "recurse" -default $false) +$flags = Get-AnsibleParam -obj $params -name "flags" -default $null +$_ansible_check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -default $false + +# Search for an Error Message +# Robocopy seems to display an error after 3 '-----' separator lines +Function SearchForError($cmd_output, $default_msg) { + $separator_count = 0 + $error_msg = $default_msg + ForEach ($line in $cmd_output) { + if (-Not $line) { + continue + } + + if ($separator_count -ne 3) { + if (Select-String -InputObject $line -pattern "^(\s+)?(\-+)(\s+)?$") { + $separator_count += 1 + } + } + Else { + If (Select-String -InputObject $line -pattern "error") { + $error_msg = $line + break + } + } + } + + return $error_msg +} + +# Build Arguments +$robocopy_opts = @() + +if (-Not (Test-Path $src)) { + Fail-Json $result "$src does not exist!" +} + +$robocopy_opts += $src +Set-Attr $result.win_robocopy "src" $src + +$robocopy_opts += $dest +Set-Attr $result.win_robocopy "dest" $dest + +if ($flags -eq $null) { + if ($purge) { + $robocopy_opts += "/purge" + } + + if ($recurse) { + $robocopy_opts += "/e" + } +} +Else { + $robocopy_opts += $flags +} + +Set-Attr $result.win_robocopy "purge" $purge +Set-Attr $result.win_robocopy "recurse" $recurse +Set-Attr $result.win_robocopy "flags" $flags + +$robocopy_output = "" +$rc = 0 +If ($_ansible_check_mode -eq $true) { + $robocopy_output = "Would have copied the contents of $src to $dest" + $rc = 0 +} +Else { + Try { + &robocopy $robocopy_opts | Tee-Object -Variable robocopy_output | Out-Null + $rc = $LASTEXITCODE + } + Catch { + $ErrorMessage = $_.Exception.Message + Fail-Json $result "Error synchronizing $src to $dest! Msg: $ErrorMessage" + } +} + +Set-Attr $result.win_robocopy "return_code" $rc +Set-Attr $result.win_robocopy "output" $robocopy_output + +$cmd_msg = "Success" +If ($rc -eq 0) { + $cmd_msg = "No files copied." +} +ElseIf ($rc -eq 1) { + $cmd_msg = "Files copied successfully!" + $changed = $true +} +ElseIf ($rc -eq 2) { + $cmd_msg = "Extra files or directories were detected!" + $changed = $true +} +ElseIf ($rc -eq 4) { + $cmd_msg = "Some mismatched files or directories were detected!" + $changed = $true +} +ElseIf ($rc -eq 8) { + $error_msg = SearchForError $robocopy_output "Some files or directories could not be copied!" + Fail-Json $result $error_msg +} +ElseIf ($rc -eq 10) { + $error_msg = SearchForError $robocopy_output "Serious Error! No files were copied! Do you have permissions to access $src and $dest?" + Fail-Json $result $error_msg +} +ElseIf ($rc -eq 16) { + $error_msg = SearchForError $robocopy_output "Fatal Error!" + Fail-Json $result $error_msg +} + +Set-Attr $result.win_robocopy "msg" $cmd_msg +Set-Attr $result.win_robocopy "changed" $changed + +Exit-Json $result diff --git a/windows/win_robocopy.py b/windows/win_robocopy.py new file mode 100644 index 00000000000..d627918e521 --- /dev/null +++ b/windows/win_robocopy.py @@ -0,0 +1,143 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Corwin Brown +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# this is a windows documentation stub. actual code lives in the .ps1 +# file of the same name + +DOCUMENTATION = """ +--- +module: win_robocopy +version_added: "2.2" +short_description: Synchronizes the contents of two directories using Robocopy. +description: + - Synchronizes the contents of two directories on the remote machine. Under the hood this just calls out to RoboCopy, since that should be available on most modern Windows Systems. +options: + src: + description: + - Source file/directory to sync. + required: true + dest: + description: + - Destination file/directory to sync (Will receive contents of src). + required: true + recurse: + description: + - Includes all subdirectories (Toggles the `/e` flag to RoboCopy). If "flags" is set, this will be ignored. + choices: + - true + - false + defaults: false + required: false + purge: + description: + - Deletes any files/directories found in the destination that do not exist in the source (Toggles the `/purge` flag to RoboCopy). If "flags" is set, this will be ignored. + choices: + - true + - false + defaults: false + required: false + flags: + description: + - Directly supply Robocopy flags. If set, purge and recurse will be ignored. + default: None + required: false +author: Corwin Brown (@blakfeld) +notes: + - This is not a complete port of the "synchronize" module. Unlike the "synchronize" module this only performs the sync/copy on the remote machine, not from the master to the remote machine. + - This module does not currently support all Robocopy flags. + - Works on Windows 7, Windows 8, Windows Server 2k8, and Windows Server 2k12 +""" + +EXAMPLES = """ +# Syncs the contents of one diretory to another. +$ ansible -i hosts all -m win_robocopy -a "src=C:\\DirectoryOne dest=C:\\DirectoryTwo" + +# Sync the contents of one directory to another, including subdirectories. +$ ansible -i hosts all -m win_robocopy -a "src=C:\\DirectoryOne dest=C:\\DirectoryTwo recurse=true" + +# Sync the contents of one directory to another, and remove any files/directories found in destination that do not exist in the source. +$ ansible -i hosts all -m win_robocopy -a "src=C:\\DirectoryOne dest=C:\\DirectoryTwo purge=true" + +# Sample sync +--- +- name: Sync Two Directories + win_robocopy: + src: "C:\\DirectoryOne + dest: "C:\\DirectoryTwo" + recurse: true + purge: true + +--- +- name: Sync Two Directories + win_robocopy: + src: "C:\\DirectoryOne + dest: "C:\\DirectoryTwo" + recurse: true + purge: true + flags: '/XD SOME_DIR /XF SOME_FILE /MT:32' +""" + +RETURN = ''' +src: + description: The Source file/directory of the sync. + returned: always + type: string + sample: "c:/Some/Path" +dest: + description: The Destination file/directory of the sync. + returned: always + type: string + sample: "c:/Some/Path" +recurse: + description: Whether or not the recurse flag was toggled. + returned: always + type: bool + sample: False +purge: + description: Whether or not the purge flag was toggled. + returned: always + type: bool + sample: False +flags: + description: Any flags passed in by the user. + returned: always + type: string + sample: "/e /purge" +return_code: + description: The return code retuned by robocopy. + returned: success + type: int + sample: 1 +output: + description: The output of running the robocopy command. + returned: success + type: string + sample: "-------------------------------------------------------------------------------\n ROBOCOPY :: Robust File Copy for Windows \n-------------------------------------------------------------------------------\n" +msg: + description: Output intrepreted into a concise message. + returned: always + type: string + sample: No files copied! +changed: + description: Whether or not any changes were made. + returned: always + type: bool + sample: False +''' From b11029adca11ef9ce15714e0b3cb47d468e4c0e2 Mon Sep 17 00:00:00 2001 From: Nicolas Landais Date: Fri, 20 May 2016 20:31:46 -0400 Subject: [PATCH 1564/2522] Fix problem with 'restarted' state not restarting the apppool when it is in running state (#1451) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix problem with 'restarted' state not restarting the apppool when it is in running state * Implemeting code review comments Comment from previous commit “You basically want to start the pool if it's stopped and requested state is started or restarted, otherwise if requested state is restarted, restart it.” This commit implements the behavior stated in the PR comment --- windows/win_iis_webapppool.ps1 | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/windows/win_iis_webapppool.ps1 b/windows/win_iis_webapppool.ps1 index 858a151f2a3..4172dc2f336 100644 --- a/windows/win_iis_webapppool.ps1 +++ b/windows/win_iis_webapppool.ps1 @@ -90,10 +90,18 @@ try { Stop-WebAppPool -Name $name -ErrorAction Stop $result.changed = $TRUE } - if ((($state -eq 'started') -and ($pool.State -eq 'Stopped')) -or ($state -eq 'restarted')) { + if ((($state -eq 'started') -and ($pool.State -eq 'Stopped'))) { Start-WebAppPool -Name $name -ErrorAction Stop $result.changed = $TRUE } + if ($state -eq 'restarted') { + switch ($pool.State) + { + 'Stopped' { Start-WebAppPool -Name $name -ErrorAction Stop } + default { Restart-WebAppPool -Name $name -ErrorAction Stop } + } + $result.changed = $TRUE + } } } catch { Fail-Json $result $_.Exception.Message @@ -112,4 +120,4 @@ if ($pool) $pool.Attributes | ForEach { $result.info.attributes.Add($_.Name, $_.Value)}; } -Exit-Json $result +Exit-Json $result \ No newline at end of file From 7e08d0101066e4cc537e420e4af32b3a2b669c8f Mon Sep 17 00:00:00 2001 From: Hagai Kariti Date: Mon, 23 May 2016 10:29:26 +0300 Subject: [PATCH 1565/2522] bigpanda: Remove docs using complex args (#2275) As they're going to be deprecated, don't recommend using them. --- monitoring/bigpanda.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/monitoring/bigpanda.py b/monitoring/bigpanda.py index c045424569a..df8e55fd745 100644 --- a/monitoring/bigpanda.py +++ b/monitoring/bigpanda.py @@ -83,19 +83,11 @@ ... - bigpanda: component=myapp version=1.3 token={{ bigpanda_token }} state=finished -or using a deployment object: -- bigpanda: component=myapp version=1.3 token={{ bigpanda_token }} state=started - register: deployment - -- bigpanda: state=finished - args: deployment - -If outside servers aren't reachable from your machine, use local_action and pass the hostname: -- local_action: bigpanda component=myapp version=1.3 hosts={{ansible_hostname}} token={{ bigpanda_token }} state=started +If outside servers aren't reachable from your machine, use local_action and override hosts: +- local_action: bigpanda component=myapp version=1.3 token={{ bigpanda_token }} hosts={{ansible_hostname}} state=started register: deployment ... -- local_action: bigpanda state=finished - args: deployment +- local_action: bigpanda component=deployment.component version=deployment.version token=deployment.token state=finished ''' # =========================================== From a780dbf4e191ba10e33a235559036454cce3aceb Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Mon, 23 May 2016 15:44:57 +0200 Subject: [PATCH 1566/2522] Port patch.py to python3/python2.4 syntax (#2276) --- .travis.yml | 1 - files/patch.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 142d833c715..0435132fef5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -76,7 +76,6 @@ env: database/vertica/vertica_schema.py database/vertica/vertica_user.py f5/bigip_gtm_virtual_server.py - files/patch.py monitoring/bigpanda.py monitoring/boundary_meter.py monitoring/circonus_annotation.py diff --git a/files/patch.py b/files/patch.py index af3178ba3a3..123d667fdbf 100644 --- a/files/patch.py +++ b/files/patch.py @@ -185,7 +185,8 @@ def main(): apply_patch( patch_func, p.src, p.basedir, dest_file=p.dest, binary=p.binary, strip=p.strip, dry_run=module.check_mode, backup=p.backup ) changed = True - except PatchError, e: + except PatchError: + e = get_exception() module.fail_json(msg=str(e)) module.exit_json(changed=changed) From b9ab1f9f5c797dbad2d30218a17de1422e379031 Mon Sep 17 00:00:00 2001 From: Rob Date: Tue, 24 May 2016 00:31:41 +1000 Subject: [PATCH 1567/2522] Add section on how to return boto3 CamelCased results (#2279) --- cloud/amazon/GUIDELINES.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/cloud/amazon/GUIDELINES.md b/cloud/amazon/GUIDELINES.md index dbfd3ee1285..b8ca836b79a 100644 --- a/cloud/amazon/GUIDELINES.md +++ b/cloud/amazon/GUIDELINES.md @@ -211,6 +211,28 @@ except ClientError, e: module.fail_json(msg=e.message, **camel_dict_to_snake_dict(e.response)) ``` +### Returning Values + +When you make a call using boto3, you will probably get back some useful information that you should return in the module. + +As well as information related to the call itself, you will also have some response metadata. It is OK to return this to +the user as well as they may find it useful. + +Boto3 returns all values CamelCased. Ansible follows Python standards for variable names and uses snake_case. There is a +helper function in module_utils/ec2.py called `camel_dict_to_snake_dict` that allows you to easily convert the boto3 +response to snake_case. + +You should use this helper function and avoid changing the names of values returned by Boto3. E.g. if boto3 returns a +value called 'SecretAccessKey' do not change it to 'AccessKey'. + +```python +# Make a call to AWS +result = connection.aws_call() + +# Return the result to the user +module.exit_json(changed=True, **camel_dict_to_snake_dict(result)) +``` + ### Helper functions Along with the connection functions in Ansible ec2.py module_utils, there are some other useful functions detailed below. From d54905ecee6926f2c863eeeabab52b5c6d85970b Mon Sep 17 00:00:00 2001 From: Leo Antunes Date: Mon, 23 May 2016 16:33:52 +0200 Subject: [PATCH 1568/2522] Allow multiple keys per host + minor improvements (#716) * known_hosts: clarify key format in documentation Add a small clarification to the documentation about the format of the "key" parameter. Should make #664 less of a issue for newcomers. * known_hosts: normalize key entry to simplify input Keys are normalized before comparing input with present keys. This should make it easier to deal with some corner cases, such as having a hashed entry for some host and trying to add it as non-hashed. * known_hosts: allow multiple entries per host In order to support multiple key types and allow the installed ssh version to decide which is more secure, the module now only overwrites an existing entry in known_hosts if the provided key is of the same type. Old keys of different types must be explicitly removed. Setting state to "absent" and providing no key will remove all entries for the host. --- system/known_hosts.py | 116 ++++++++++++++++++++++++++---------------- 1 file changed, 71 insertions(+), 45 deletions(-) diff --git a/system/known_hosts.py b/system/known_hosts.py index b4c26e0efa5..f7d36b9b3d2 100644 --- a/system/known_hosts.py +++ b/system/known_hosts.py @@ -23,7 +23,7 @@ module: known_hosts short_description: Add or remove a host from the C(known_hosts) file description: - - The M(known_hosts) module lets you add or remove a host from the C(known_hosts) file. + - The M(known_hosts) module lets you add or remove a host keys from the C(known_hosts) file. Multiple entries per host are allowed, but only one for each key type supported by ssh. This is useful if you're going to want to use the M(git) module over ssh, for example. If you have a very large number of host keys to manage, you will find the M(template) module more useful. version_added: "1.9" @@ -36,7 +36,7 @@ default: null key: description: - - The SSH public host key, as a string (required if state=present, optional when state=absent, in which case all keys for the host are removed) + - The SSH public host key, as a string (required if state=present, optional when state=absent, in which case all keys for the host are removed). The key must be in the right format for ssh (see ssh(1), section "SSH_KNOWN_HOSTS FILE FORMAT") required: false default: null path: @@ -46,7 +46,7 @@ default: "(homedir)+/.ssh/known_hosts" state: description: - - I(present) to add the host, I(absent) to remove it. + - I(present) to add the host key, I(absent) to remove it. choices: [ "present", "absent" ] required: no default: present @@ -76,6 +76,7 @@ import os.path import tempfile import errno +import re def enforce_state(module, params): """ @@ -99,24 +100,24 @@ def enforce_state(module, params): sanity_check(module,host,key,sshkeygen) - current,replace=search_for_host_key(module,host,key,path,sshkeygen) + found,replace_or_add,found_line=search_for_host_key(module,host,key,path,sshkeygen) - #We will change state if current==True & state!="present" - #or current==False & state=="present" - #i.e (current) XOR (state=="present") + #We will change state if found==True & state!="present" + #or found==False & state=="present" + #i.e found XOR (state=="present") #Alternatively, if replace is true (i.e. key present, and we must change it) if module.check_mode: - module.exit_json(changed = replace or ((state=="present") != current)) + module.exit_json(changed = replace_or_add or (state=="present") != found) #Now do the work. - #First, remove an extant entry if required - if replace==True or (current==True and state=="absent"): - module.run_command([sshkeygen,'-R',host,'-f',path], - check_rc=True) + #Only remove whole host if found and no key provided + if found and key is None and state=="absent": + module.run_command([sshkeygen,'-R',host,'-f',path], check_rc=True) params['changed'] = True + #Next, add a new (or replacing) entry - if replace==True or (current==False and state=="present"): + if replace_or_add or found != (state=="present"): try: inf=open(path,"r") except IOError, e: @@ -128,10 +129,13 @@ def enforce_state(module, params): try: outf=tempfile.NamedTemporaryFile(dir=os.path.dirname(path)) if inf is not None: - for line in inf: + for line_number, line in enumerate(inf, start=1): + if found_line==line_number and (replace_or_add or state=='absent'): + continue # skip this line to replace its key outf.write(line) inf.close() - outf.write(key) + if state == 'present': + outf.write(key) outf.flush() module.atomic_move(outf.name,path) except (IOError,OSError),e: @@ -183,54 +187,76 @@ def sanity_check(module,host,key,sshkeygen): module.fail_json(msg="Host parameter does not match hashed host field in supplied key") def search_for_host_key(module,host,key,path,sshkeygen): - '''search_for_host_key(module,host,key,path,sshkeygen) -> (current,replace) + '''search_for_host_key(module,host,key,path,sshkeygen) -> (found,replace_or_add,found_line) - Looks up host in the known_hosts file path; if it's there, looks to see + Looks up host and keytype in the known_hosts file path; if it's there, looks to see if one of those entries matches key. Returns: - current (Boolean): is host found in path? - replace (Boolean): is the key in path different to that supplied by user? - if current=False, then replace is always False. + found (Boolean): is host found in path? + replace_or_add (Boolean): is the key in path different to that supplied by user? + found_line (int or None): the line where a key of the same type was found + if found=False, then replace is always False. sshkeygen is the path to ssh-keygen, found earlier with get_bin_path ''' - replace=False if os.path.exists(path)==False: - return False, False + return False, False, None #openssh >=6.4 has changed ssh-keygen behaviour such that it returns #1 if no host is found, whereas previously it returned 0 rc,stdout,stderr=module.run_command([sshkeygen,'-F',host,'-f',path], check_rc=False) if stdout=='' and stderr=='' and (rc==0 or rc==1): - return False, False #host not found, no other errors + return False, False, None #host not found, no other errors if rc!=0: #something went wrong module.fail_json(msg="ssh-keygen failed (rc=%d,stdout='%s',stderr='%s')" % (rc,stdout,stderr)) -#If user supplied no key, we don't want to try and replace anything with it + #If user supplied no key, we don't want to try and replace anything with it if key is None: - return True, False + return True, False, None lines=stdout.split('\n') - k=key.strip() #trim trailing newline - #ssh-keygen returns only the host we ask about in the host field, - #even if the key entry has multiple hosts. Emulate this behaviour here, - #otherwise we get false negatives. - #Only necessary for unhashed entries. - if k[0] !='|': - k=k.split() - #The optional "marker" field, used for @cert-authority or @revoked - if k[0][0] == '@': - k[1]=host - else: - k[0]=host - k=' '.join(k) + new_key = normalize_known_hosts_key(key, host) + for l in lines: - if l=='': - continue - if l[0]=='#': #comment + if l=='': continue - if k==l: #found a match - return True, False #current, not-replace - #No match found, return current and replace - return True, True + elif l[0]=='#': # info output from ssh-keygen; contains the line number where key was found + try: + # This output format has been hardcoded in ssh-keygen since at least OpenSSH 4.0 + # It always outputs the non-localized comment before the found key + found_line = int(re.search(r'found: line (\d+)', l).group(1)) + except IndexError, e: + module.fail_json(msg="failed to parse output of ssh-keygen for line number: '%s'" % l) + else: + found_key = normalize_known_hosts_key(l,host) + if new_key==found_key: #found a match + return True, False, found_line #found exactly the same key, don't replace + elif new_key['type'] == found_key['type']: # found a different key for the same key type + return True, True, found_line + #No match found, return found and replace, but no line + return True, True, None + +def normalize_known_hosts_key(key, host): + ''' + Transform a key, either taken from a known_host file or provided by the + user, into a normalized form. + The host part (which might include multiple hostnames or be hashed) gets + replaced by the provided host. Also, any spurious information gets removed + from the end (like the username@host tag usually present in hostkeys, but + absent in known_hosts files) + ''' + k=key.strip() #trim trailing newline + k=key.split() + d = dict() + #The optional "marker" field, used for @cert-authority or @revoked + if k[0][0] == '@': + d['options'] = k[0] + d['host']=host + d['type']=k[2] + d['key']=k[3] + else: + d['host']=host + d['type']=k[1] + d['key']=k[2] + return d def main(): From ecee427cbc268929d61ed0683031e1216fa79698 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 23 May 2016 10:35:03 -0400 Subject: [PATCH 1569/2522] added note about behaviour change in 2.2 --- system/known_hosts.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/system/known_hosts.py b/system/known_hosts.py index f7d36b9b3d2..d1890af5182 100644 --- a/system/known_hosts.py +++ b/system/known_hosts.py @@ -23,9 +23,10 @@ module: known_hosts short_description: Add or remove a host from the C(known_hosts) file description: - - The M(known_hosts) module lets you add or remove a host keys from the C(known_hosts) file. Multiple entries per host are allowed, but only one for each key type supported by ssh. - This is useful if you're going to want to use the M(git) module over ssh, for example. - If you have a very large number of host keys to manage, you will find the M(template) module more useful. + - The M(known_hosts) module lets you add or remove a host keys from the C(known_hosts) file. + - Starting at Ansible 2.2, multiple entries per host are allowed, but only one for each key type supported by ssh. + This is useful if you're going to want to use the M(git) module over ssh, for example. + - If you have a very large number of host keys to manage, you will find the M(template) module more useful. version_added: "1.9" options: name: From 0d54d1ffe45649c098b161c9b7f88e6c98f20802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20=C5=A0tevko?= Date: Mon, 23 May 2016 16:35:55 +0200 Subject: [PATCH 1570/2522] Add module for retrieving SmartOS image facts (#1276) --- cloud/smartos/smartos_image_facts.py | 114 +++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 cloud/smartos/smartos_image_facts.py diff --git a/cloud/smartos/smartos_image_facts.py b/cloud/smartos/smartos_image_facts.py new file mode 100644 index 00000000000..1b9926080a2 --- /dev/null +++ b/cloud/smartos/smartos_image_facts.py @@ -0,0 +1,114 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Adam Števko +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +DOCUMENTATION = ''' +--- +module: smartos_image_facts +short_description: Get SmartOS image details. +description: + - Retrieve facts about all installed images on SmartOS. Facts will be + inserted to the ansible_facts key. +version_added: "2.0" +author: Adam Števko (@xen0l) +options: + filters: + description: + - Criteria for selecting image. Can be any value from image + manifest and 'published_date', 'published', 'source', 'clones', + and 'size'. More informaton can be found at U(https://smartos.org/man/1m/imgadm) + under 'imgadm list'. + required: false + default: None +''' + +EXAMPLES = ''' +# Return facts about all installed images. +smartos_image_facts: + +# Return all private active Linux images. +smartos_image_facts: filters="os=linux state=active public=false" + +# Show, how many clones does every image have. +smartos_image_facts: + +debug: msg="{{ smartos_images[item]['name'] }}-{{smartos_images[item]['version'] }} + has {{ smartos_images[item]['clones'] }} VM(s)" +with_items: smartos_images.keys() +''' + + +try: + import json +except ImportError: + import simplejson as json + + +class ImageFacts(object): + + def __init__(self, module): + self.module = module + + self.filters = module.params['filters'] + + def return_all_installed_images(self): + cmd = [self.module.get_bin_path('imgadm')] + + cmd.append('list') + cmd.append('-j') + + if self.filters: + cmd.append(self.filters) + + (rc, out, err) = self.module.run_command(cmd) + + if rc != 0: + self.module.exit_json( + msg='Failed to get all installed images', stderr=err) + + images = json.loads(out) + + result = {} + for image in images: + result[image['manifest']['uuid']] = image['manifest'] + # Merge additional attributes with the image manifest. + for attrib in ['clones', 'source', 'zpool']: + result[image['manifest']['uuid']][attrib] = image[attrib] + + return result + + +def main(): + module = AnsibleModule( + argument_spec=dict( + filters=dict(default=None), + ), + supports_check_mode=False, + ) + + image_facts = ImageFacts(module) + + data = {} + data['smartos_images'] = image_facts.return_all_installed_images() + + module.exit_json(ansible_facts=data) + +from ansible.module_utils.basic import * +main() From eb6ba749c22ba83ae576373334a8553f9d34c264 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 23 May 2016 10:36:47 -0400 Subject: [PATCH 1571/2522] added return docs --- cloud/smartos/smartos_image_facts.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cloud/smartos/smartos_image_facts.py b/cloud/smartos/smartos_image_facts.py index 1b9926080a2..57ab62b9ce9 100644 --- a/cloud/smartos/smartos_image_facts.py +++ b/cloud/smartos/smartos_image_facts.py @@ -54,6 +54,9 @@ with_items: smartos_images.keys() ''' +RETURN = ''' +# this module returns ansible_facts +''' try: import json From 0ba34435cf6896a6bcf1039cb04fde0d426fad4e Mon Sep 17 00:00:00 2001 From: Ryan Sydnor Date: Mon, 23 May 2016 10:39:02 -0400 Subject: [PATCH 1572/2522] Add encryption capability to AMI copy (#1409) --- cloud/amazon/ec2_ami_copy.py | 80 ++++++++++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 16 deletions(-) diff --git a/cloud/amazon/ec2_ami_copy.py b/cloud/amazon/ec2_ami_copy.py index 72c804bc1d1..88eba110899 100644 --- a/cloud/amazon/ec2_ami_copy.py +++ b/cloud/amazon/ec2_ami_copy.py @@ -1,4 +1,5 @@ #!/usr/bin/python +# -*- coding: utf-8 -*- # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify @@ -40,6 +41,18 @@ - An optional human-readable string describing the contents and purpose of the new AMI. required: false default: null + encrypted: + description: + - Whether or not to encrypt the target image + required: false + default: null + version_added: "2.2" + kms_key_id: + description: + - KMS key id used to encrypt image. If not specified, uses default EBS Customer Master Key (CMK) for your account. + required: false + default: null + version_added: "2.2" wait: description: - wait for the copied AMI to be in state 'available' before returning. @@ -65,30 +78,60 @@ EXAMPLES = ''' # Basic AMI Copy -- local_action: - module: ec2_ami_copy - source_region: eu-west-1 - dest_region: us-east-1 +- ec2_ami_copy: + source_region: us-east-1 + region: eu-west-1 + source_image_id: ami-xxxxxxx + +# AMI copy wait until available +- ec2_ami_copy: + source_region: us-east-1 + region: eu-west-1 source_image_id: ami-xxxxxxx - name: SuperService-new-AMI - description: latest patch - tags: '{"Name":"SuperService-new-AMI", "type":"SuperService"}' wait: yes register: image_id -''' +# Named AMI copy +- ec2_ami_copy: + source_region: us-east-1 + region: eu-west-1 + source_image_id: ami-xxxxxxx + name: My-Awesome-AMI + description: latest patch -import sys -import time +# Tagged AMI copy +- ec2_ami_copy: + source_region: us-east-1 + region: eu-west-1 + source_image_id: ami-xxxxxxx + tags: + Name: My-Super-AMI + Patch: 1.2.3 + +# Encrypted AMI copy +- ec2_ami_copy: + source_region: us-east-1 + region: eu-west-1 + source_image_id: ami-xxxxxxx + encrypted: yes + +# Encrypted AMI copy with specified key +- ec2_ami_copy: + source_region: us-east-1 + region: eu-west-1 + source_image_id: ami-xxxxxxx + encrypted: yes + kms_key_id: arn:aws:kms:us-east-1:XXXXXXXXXXXX:key/746de6ea-50a4-4bcb-8fbc-e3b29f2d367b +''' try: import boto import boto.ec2 - from boto.vpc import VPCConnection HAS_BOTO = True except ImportError: HAS_BOTO = False + def copy_image(module, ec2): """ Copies an AMI @@ -101,6 +144,8 @@ def copy_image(module, ec2): source_image_id = module.params.get('source_image_id') name = module.params.get('name') description = module.params.get('description') + encrypted = module.params.get('encrypted') + kms_key_id = module.params.get('kms_key_id') tags = module.params.get('tags') wait_timeout = int(module.params.get('wait_timeout')) wait = module.params.get('wait') @@ -109,7 +154,9 @@ def copy_image(module, ec2): params = {'source_region': source_region, 'source_image_id': source_image_id, 'name': name, - 'description': description + 'description': description, + 'encrypted': encrypted, + 'kms_key_id': kms_key_id } image_id = ec2.copy_image(**params).image_id @@ -125,7 +172,7 @@ def copy_image(module, ec2): module.exit_json(msg="AMI copy operation complete", image_id=image_id, state=img.state, changed=True) -# register tags to the copied AMI in dest_region +# register tags to the copied AMI def register_tags_if_any(module, ec2, tags, image_id): if tags: try: @@ -171,6 +218,8 @@ def main(): source_image_id=dict(required=True), name=dict(), description=dict(default=""), + encrypted=dict(type='bool', required=False), + kms_key_id=dict(type='str', required=False), wait=dict(type='bool', default=False), wait_timeout=dict(default=1200), tags=dict(type='dict'))) @@ -187,11 +236,10 @@ def main(): try: region, ec2_url, boto_params = get_aws_connection_info(module) - vpc = connect_to_aws(boto.vpc, region, **boto_params) except boto.exception.NoAuthHandlerFound, e: - module.fail_json(msg = str(e)) + module.fail_json(msg=str(e)) - if not region: + if not region: module.fail_json(msg="region must be specified") copy_image(module, ec2) From 6d52d84af7fd63944525b9ec95cb24c01f2cb48d Mon Sep 17 00:00:00 2001 From: Mike Mochan Date: Tue, 24 May 2016 00:42:11 +1000 Subject: [PATCH 1573/2522] New AWS module for managing VPC Network ACLs (#1502) * New AWS module for managing VPC Networks ACLs Moved return outside of try block botocore.exceptions to support python 2.5 For some reason Travis is using Python V2.4 to run the tests - My code is valid duplicate file * Fixed NameError Exception- module not being passed when calling some boto3 client methods * Fixes a bug reported by @dennisconrad, where the nacl is not created when subnets list is empty * nacl property changed to name and fixes a bug where nacl is not deleted when subnets list is empty * Updates to version and requirements * Fix 'vpc' param to 'vpc_id' to match documentation and convention --- cloud/amazon/ec2_vpc_nacl.py | 546 +++++++++++++++++++++++++++++++++++ 1 file changed, 546 insertions(+) create mode 100644 cloud/amazon/ec2_vpc_nacl.py diff --git a/cloud/amazon/ec2_vpc_nacl.py b/cloud/amazon/ec2_vpc_nacl.py new file mode 100644 index 00000000000..73eafbc8482 --- /dev/null +++ b/cloud/amazon/ec2_vpc_nacl.py @@ -0,0 +1,546 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +DOCUMENTATION = ''' +module: ec2_vpc_nacl +short_description: create and delete Network ACLs. +description: + - Read the AWS documentation for Network ACLS + U(http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_ACLs.html) +version_added: "2.2" +options: + name: + description: + - Tagged name identifying a network ACL. + required: true + vpc_id: + description: + - VPC id of the requesting VPC. + required: true + subnets: + description: + - The list of subnets that should be associated with the network ACL. + - Must be specified as a list + - Each subnet can be specified as subnet ID, or its tagged name. + required: false + egress: + description: + - A list of rules for outgoing traffic. + - Each rule must be specified as a list. + required: false + ingress: + description: + - List of rules for incoming traffic. + - Each rule must be specified as a list. + required: false + tags: + description: + - Dictionary of tags to look for and apply when creating a network ACL. + required: false + state: + description: + - Creates or modifies an existing NACL + - Deletes a NACL and reassociates subnets to the default NACL + required: false + choices: ['present', 'absent'] + default: present +author: Mike Mochan(@mmochan) +extends_documentation_fragment: aws +requirements: [ botocore, boto3, json ] +''' + +EXAMPLES = ''' + +# Complete example to create and delete a network ACL +# that allows SSH, HTTP and ICMP in, and all traffic out. +- name: "Create and associate production DMZ network ACL with DMZ subnets" + ec2_vpc_nacl: + vpc_id: vpc-12345678 + name: prod-dmz-nacl + region: ap-southeast-2 + subnets: ['prod-dmz-1', 'prod-dmz-2'] + tags: + CostCode: CC1234 + Project: phoenix + Description: production DMZ + ingress: [ + # rule no, protocol, allow/deny, cidr, icmp_code, icmp_type, + # port from, port to + [100, 'tcp', 'allow', '0.0.0.0/0', null, null, 22, 22], + [200, 'tcp', 'allow', '0.0.0.0/0', null, null, 80, 80], + [300, 'icmp', 'allow', '0.0.0.0/0', 0, 8], + ] + egress: [ + [100, 'all', 'allow', '0.0.0.0/0', null, null, null, null] + ] + state: 'present' + +- name: "Remove the ingress and egress rules - defaults to deny all" + ec2_vpc_nacl: + vpc_id: vpc-12345678 + name: prod-dmz-nacl + region: ap-southeast-2 + subnets: + - prod-dmz-1 + - prod-dmz-2 + tags: + CostCode: CC1234 + Project: phoenix + Description: production DMZ + state: present + +- name: "Remove the NACL subnet associations and tags" + ec2_vpc_nacl: + vpc_id: 'vpc-12345678' + name: prod-dmz-nacl + region: ap-southeast-2 + state: present + +- name: "Delete nacl and subnet associations" + ec2_vpc_nacl: + vpc_id: vpc-12345678 + name: prod-dmz-nacl + state: absent +''' +RETURN = ''' +task: + description: The result of the create, or delete action. + returned: success + type: dictionary +''' + +try: + import json + import botocore + import boto3 + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + + +# Common fields for the default rule that is contained within every VPC NACL. +DEFAULT_RULE_FIELDS = { + 'RuleNumber': 32767, + 'RuleAction': 'deny', + 'CidrBlock': '0.0.0.0/0', + 'Protocol': '-1' +} + +DEFAULT_INGRESS = dict(DEFAULT_RULE_FIELDS.items() + [('Egress', False)]) +DEFAULT_EGRESS = dict(DEFAULT_RULE_FIELDS.items() + [('Egress', True)]) + +# VPC-supported IANA protocol numbers +# http://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml +PROTOCOL_NUMBERS = {'all': -1, 'icmp': 1, 'tcp': 6, 'udp': 17, } + + +#Utility methods +def icmp_present(entry): + if len(entry) == 6 and entry[1] == 'icmp' or entry[1] == 1: + return True + + +def load_tags(module): + tags = [] + if module.params.get('tags'): + for name, value in module.params.get('tags').iteritems(): + tags.append({'Key': name, 'Value': str(value)}) + tags.append({'Key': "Name", 'Value': module.params.get('name')}) + else: + tags.append({'Key': "Name", 'Value': module.params.get('name')}) + return tags + + +def subnets_removed(nacl_id, subnets, client, module): + results = find_acl_by_id(nacl_id, client, module) + associations = results['NetworkAcls'][0]['Associations'] + subnet_ids = [assoc['SubnetId'] for assoc in associations] + return [subnet for subnet in subnet_ids if subnet not in subnets] + + +def subnets_added(nacl_id, subnets, client, module): + results = find_acl_by_id(nacl_id, client, module) + associations = results['NetworkAcls'][0]['Associations'] + subnet_ids = [assoc['SubnetId'] for assoc in associations] + return [subnet for subnet in subnets if subnet not in subnet_ids] + + +def subnets_changed(nacl, client, module): + changed = False + response = {} + vpc_id = module.params.get('vpc_id') + nacl_id = nacl['NetworkAcls'][0]['NetworkAclId'] + subnets = subnets_to_associate(nacl, client, module) + if not subnets: + default_nacl_id = find_default_vpc_nacl(vpc_id, client, module)[0] + subnets = find_subnet_ids_by_nacl_id(nacl_id, client, module) + if subnets: + replace_network_acl_association(default_nacl_id, subnets, client, module) + changed = True + return changed + changed = False + return changed + subs_added = subnets_added(nacl_id, subnets, client, module) + if subs_added: + replace_network_acl_association(nacl_id, subs_added, client, module) + changed = True + subs_removed = subnets_removed(nacl_id, subnets, client, module) + if subs_removed: + default_nacl_id = find_default_vpc_nacl(vpc_id, client, module)[0] + replace_network_acl_association(default_nacl_id, subs_removed, client, module) + changed = True + return changed + + +def nacls_changed(nacl, client, module): + changed = False + params = dict() + params['egress'] = module.params.get('egress') + params['ingress'] = module.params.get('ingress') + + nacl_id = nacl['NetworkAcls'][0]['NetworkAclId'] + nacl = describe_network_acl(client, module) + entries = nacl['NetworkAcls'][0]['Entries'] + tmp_egress = [entry for entry in entries if entry['Egress'] is True and DEFAULT_EGRESS !=entry] + tmp_ingress = [entry for entry in entries if entry['Egress'] is False] + egress = [rule for rule in tmp_egress if DEFAULT_EGRESS != rule] + ingress = [rule for rule in tmp_ingress if DEFAULT_INGRESS != rule] + if rules_changed(egress, params['egress'], True, nacl_id, client, module): + changed = True + if rules_changed(ingress, params['ingress'], False, nacl_id, client, module): + changed = True + return changed + + +def tags_changed(nacl_id, client, module): + changed = False + tags = dict() + if module.params.get('tags'): + tags = module.params.get('tags') + tags['Name'] = module.params.get('name') + nacl = find_acl_by_id(nacl_id, client, module) + if nacl['NetworkAcls']: + nacl_values = [t.values() for t in nacl['NetworkAcls'][0]['Tags']] + nacl_tags = [item for sublist in nacl_values for item in sublist] + tag_values = [[key, str(value)] for key, value in tags.iteritems()] + tags = [item for sublist in tag_values for item in sublist] + if sorted(nacl_tags) == sorted(tags): + changed = False + return changed + else: + delete_tags(nacl_id, client, module) + create_tags(nacl_id, client, module) + changed = True + return changed + return changed + + +def rules_changed(aws_rules, param_rules, Egress, nacl_id, client, module): + changed = False + rules = list() + for entry in param_rules: + rules.append(process_rule_entry(entry, Egress)) + if rules == aws_rules: + return changed + else: + removed_rules = [x for x in aws_rules if x not in rules] + if removed_rules: + params = dict() + for rule in removed_rules: + params['NetworkAclId'] = nacl_id + params['RuleNumber'] = rule['RuleNumber'] + params['Egress'] = Egress + delete_network_acl_entry(params, client, module) + changed = True + added_rules = [x for x in rules if x not in aws_rules] + if added_rules: + for rule in added_rules: + rule['NetworkAclId'] = nacl_id + create_network_acl_entry(rule, client, module) + changed = True + return changed + + +def process_rule_entry(entry, Egress): + params = dict() + params['RuleNumber'] = entry[0] + params['Protocol'] = str(PROTOCOL_NUMBERS[entry[1]]) + params['RuleAction'] = entry[2] + params['Egress'] = Egress + params['CidrBlock'] = entry[3] + if icmp_present(entry): + params['IcmpTypeCode'] = {"Type": int(entry[4]), "Code": int(entry[5])} + else: + if entry[6] or entry[7]: + params['PortRange'] = {"From": entry[6], 'To': entry[7]} + return params + + +def restore_default_associations(assoc_ids, default_nacl_id, client, module): + if assoc_ids: + params = dict() + params['NetworkAclId'] = default_nacl_id[0] + for assoc_id in assoc_ids: + params['AssociationId'] = assoc_id + restore_default_acl_association(params, client, module) + return True + + +def construct_acl_entries(nacl, client, module): + for entry in module.params.get('ingress'): + params = process_rule_entry(entry, Egress=False) + params['NetworkAclId'] = nacl['NetworkAcl']['NetworkAclId'] + create_network_acl_entry(params, client, module) + for rule in module.params.get('egress'): + params = process_rule_entry(rule, Egress=True) + params['NetworkAclId'] = nacl['NetworkAcl']['NetworkAclId'] + create_network_acl_entry(params, client, module) + + +## Module invocations +def setup_network_acl(client, module): + changed = False + nacl = describe_network_acl(client, module) + if not nacl['NetworkAcls']: + nacl = create_network_acl(module.params.get('vpc_id'), client, module) + nacl_id = nacl['NetworkAcl']['NetworkAclId'] + create_tags(nacl_id, client, module) + subnets = subnets_to_associate(nacl, client, module) + replace_network_acl_association(nacl_id, subnets, client, module) + construct_acl_entries(nacl, client, module) + changed = True + return(changed, nacl['NetworkAcl']['NetworkAclId']) + else: + changed = False + nacl_id = nacl['NetworkAcls'][0]['NetworkAclId'] + subnet_result = subnets_changed(nacl, client, module) + nacl_result = nacls_changed(nacl, client, module) + tag_result = tags_changed(nacl_id, client, module) + if subnet_result is True or nacl_result is True or tag_result is True: + changed = True + return(changed, nacl_id) + return (changed, nacl_id) + + +def remove_network_acl(client, module): + changed = False + result = dict() + vpc_id = module.params.get('vpc_id') + nacl = describe_network_acl(client, module) + if nacl['NetworkAcls']: + nacl_id = nacl['NetworkAcls'][0]['NetworkAclId'] + associations = nacl['NetworkAcls'][0]['Associations'] + assoc_ids = [a['NetworkAclAssociationId'] for a in associations] + default_nacl_id = find_default_vpc_nacl(vpc_id, client, module) + if not default_nacl_id: + result = {vpc_id: "Default NACL ID not found - Check the VPC ID"} + return changed, result + if restore_default_associations(assoc_ids, default_nacl_id, client, module): + delete_network_acl(nacl_id, client, module) + changed = True + result[nacl_id] = "Successfully deleted" + return changed, result + if not assoc_ids: + delete_network_acl(nacl_id, client, module) + changed = True + result[nacl_id] = "Successfully deleted" + return changed, result + return changed, result + + +#Boto3 client methods +def create_network_acl(vpc_id, client, module): + try: + nacl = client.create_network_acl(VpcId=vpc_id) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + return nacl + + +def create_network_acl_entry(params, client, module): + try: + result = client.create_network_acl_entry(**params) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + return result + + +def create_tags(nacl_id, client, module): + try: + delete_tags(nacl_id, client, module) + client.create_tags(Resources=[nacl_id], Tags=load_tags(module)) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + + +def delete_network_acl(nacl_id, client, module): + try: + client.delete_network_acl(NetworkAclId=nacl_id) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + + +def delete_network_acl_entry(params, client, module): + try: + client.delete_network_acl_entry(**params) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + + +def delete_tags(nacl_id, client, module): + try: + client.delete_tags(Resources=[nacl_id]) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + + +def describe_acl_associations(subnets, client, module): + if not subnets: + return [] + try: + results = client.describe_network_acls(Filters=[ + {'Name': 'association.subnet-id', 'Values': subnets} + ]) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + associations = results['NetworkAcls'][0]['Associations'] + return [a['NetworkAclAssociationId'] for a in associations if a['SubnetId'] in subnets] + + +def describe_network_acl(client, module): + try: + nacl = client.describe_network_acls(Filters=[ + {'Name': 'tag:Name', 'Values': [module.params.get('name')]} + ]) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + return nacl + + +def find_acl_by_id(nacl_id, client, module): + try: + return client.describe_network_acls(NetworkAclIds=[nacl_id]) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + + +def find_default_vpc_nacl(vpc_id, client, module): + try: + response = client.describe_network_acls(Filters=[ + {'Name': 'vpc-id', 'Values': [vpc_id]}]) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + nacls = response['NetworkAcls'] + return [n['NetworkAclId'] for n in nacls if n['IsDefault'] == True] + + +def find_subnet_ids_by_nacl_id(nacl_id, client, module): + try: + results = client.describe_network_acls(Filters=[ + {'Name': 'association.network-acl-id', 'Values': [nacl_id]} + ]) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + if results['NetworkAcls']: + associations = results['NetworkAcls'][0]['Associations'] + return [s['SubnetId'] for s in associations if s['SubnetId']] + else: + return [] + + +def replace_network_acl_association(nacl_id, subnets, client, module): + params = dict() + params['NetworkAclId'] = nacl_id + for association in describe_acl_associations(subnets, client, module): + params['AssociationId'] = association + try: + client.replace_network_acl_association(**params) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + + +def replace_network_acl_entry(entries, Egress, nacl_id, client, module): + params = dict() + for entry in entries: + params = entry + params['NetworkAclId'] = nacl_id + try: + client.replace_network_acl_entry(**params) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + + +def restore_default_acl_association(params, client, module): + try: + client.replace_network_acl_association(**params) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + + +def subnets_to_associate(nacl, client, module): + params = list(module.params.get('subnets')) + if not params: + return [] + if params[0].startswith("subnet-"): + try: + subnets = client.describe_subnets(Filters=[ + {'Name': 'subnet-id', 'Values': params}]) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + else: + try: + subnets = client.describe_subnets(Filters=[ + {'Name': 'tag:Name', 'Values': params}]) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + return [s['SubnetId'] for s in subnets['Subnets'] if s['SubnetId']] + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + vpc_id=dict(required=True), + name=dict(required=True), + subnets=dict(required=False, type='list', default=list()), + tags=dict(required=False, type='dict'), + ingress=dict(required=False, type='list', default=list()), + egress=dict(required=False, type='list', default=list(),), + state=dict(default='present', choices=['present', 'absent']), + ), + ) + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO3: + module.fail_json(msg='json, botocore and boto3 are required.') + state = module.params.get('state').lower() + try: + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + client = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_kwargs) + except botocore.exceptions.NoCredentialsError, e: + module.fail_json(msg="Can't authorize connection - "+str(e)) + + invocations = { + "present": setup_network_acl, + "absent": remove_network_acl + } + (changed, results) = invocations[state](client, module) + module.exit_json(changed=changed, nacl_id=results) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From 4afae893e1514c9726d4e03769a38d5face8a7bb Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Mon, 23 May 2016 17:18:05 +0200 Subject: [PATCH 1574/2522] Convert module to work with python 2.4 and fix a bug (#2251) - Avoiding the use of 'with ... as ...' and 'except ... as ...' constructs. - Make use of posixpath.join() rather than concatenating slashes ourselves (To avoid having consecutive slashes which broke something on our side) --- packaging/language/maven_artifact.py | 32 +++++++++++++++------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/packaging/language/maven_artifact.py b/packaging/language/maven_artifact.py index c71c98d4ac8..f5295185451 100644 --- a/packaging/language/maven_artifact.py +++ b/packaging/language/maven_artifact.py @@ -25,6 +25,7 @@ import os import hashlib import sys +import posixpath DOCUMENTATION = ''' --- @@ -133,9 +134,9 @@ def is_snapshot(self): return self.version and self.version.endswith("SNAPSHOT") def path(self, with_version=True): - base = self.group_id.replace(".", "/") + "/" + self.artifact_id + base = posixpath.join(self.group_id.replace(".", "/"), self.artifact_id) if with_version and self.version: - return base + "/" + self.version + return posixpath.join(base, self.version) else: return base @@ -182,8 +183,7 @@ def parse(input): class MavenDownloader: def __init__(self, module, base="http://repo1.maven.org/maven2"): self.module = module - if base.endswith("/"): - base = base.rstrip("/") + base = base.rstrip("/") self.base = base self.user_agent = "Maven Artifact Downloader/1.0" @@ -213,9 +213,9 @@ def _uri_for_artifact(self, artifact, version=None): elif not artifact.is_snapshot(): version = artifact.version if artifact.classifier: - return self.base + "/" + artifact.path() + "/" + artifact.artifact_id + "-" + version + "-" + artifact.classifier + "." + artifact.extension + return posixpath.join(self.base, artifact.path(), artifact.artifact_id + "-" + version + "-" + artifact.classifier + "." + artifact.extension) - return self.base + "/" + artifact.path() + "/" + artifact.artifact_id + "-" + version + "." + artifact.extension + return posixpath.join(self.base, artifact.path(), artifact.artifact_id + "-" + version + "." + artifact.extension) def _request(self, url, failmsg, f): # Hack to add parameters in the way that fetch_url expects @@ -240,9 +240,10 @@ def download(self, artifact, filename=None): if not self.verify_md5(filename, url + ".md5"): response = self._request(url, "Failed to download artifact " + str(artifact), lambda r: r) if response: - with open(filename, 'w') as f: - # f.write(response.read()) - self._write_chunks(response, f, report_hook=self.chunk_report) + f = open(filename, 'w') + # f.write(response.read()) + self._write_chunks(response, f, report_hook=self.chunk_report) + f.close() return True else: return False @@ -286,9 +287,10 @@ def verify_md5(self, file, remote_md5): def _local_md5(self, file): md5 = hashlib.md5() - with open(file, 'rb') as f: - for chunk in iter(lambda: f.read(8192), ''): - md5.update(chunk) + f = open(file, 'rb') + for chunk in iter(lambda: f.read(8192), ''): + md5.update(chunk) + f.close() return md5.hexdigest() @@ -328,12 +330,12 @@ def main(): try: artifact = Artifact(group_id, artifact_id, version, classifier, extension) - except ValueError as e: + except ValueError, e: module.fail_json(msg=e.args[0]) prev_state = "absent" if os.path.isdir(dest): - dest = dest + "/" + artifact_id + "-" + version + "." + extension + dest = posixpath.join(dest, artifact_id + "-" + version + "." + extension) if os.path.lexists(dest) and downloader.verify_md5(dest, downloader.find_uri_for_artifact(artifact) + '.md5'): prev_state = "present" else: @@ -349,7 +351,7 @@ def main(): module.exit_json(state=state, dest=dest, group_id=group_id, artifact_id=artifact_id, version=version, classifier=classifier, extension=extension, repository_url=repository_url, changed=True) else: module.fail_json(msg="Unable to download the artifact") - except ValueError as e: + except ValueError, e: module.fail_json(msg=e.args[0]) From 0d8eefe197acc4f6e42c768f151e3f079c8a3c43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Jos=C3=A9=20Pando?= Date: Mon, 23 May 2016 11:39:20 -0400 Subject: [PATCH 1575/2522] fixup sns topic subscriptions (#2232) * fixup sns topic subscriptions * return docs --- cloud/amazon/sns_topic.py | 422 ++++++++++++++++++++++---------------- 1 file changed, 241 insertions(+), 181 deletions(-) mode change 100755 => 100644 cloud/amazon/sns_topic.py diff --git a/cloud/amazon/sns_topic.py b/cloud/amazon/sns_topic.py old mode 100755 new mode 100644 index 2161fdaed94..f4916693edc --- a/cloud/amazon/sns_topic.py +++ b/cloud/amazon/sns_topic.py @@ -97,35 +97,29 @@ """ RETURN = ''' -topic_created: - description: Whether the topic was newly created - type: bool - returned: changed and state == present - sample: True - -attributes_set: - description: The attributes which were changed - type: list - returned: state == "present" - sample: ["policy", "delivery_policy"] - -subscriptions_added: - description: The subscriptions added to the topic - type: list - returned: state == "present" - sample: [["sms", "my_mobile_number"], ["sms", "my_mobile_2"]] - -subscriptions_deleted: - description: The subscriptions deleted from the topic - type: list - returned: state == "present" - sample: [["sms", "my_mobile_number"], ["sms", "my_mobile_2"]] - sns_arn: description: The ARN of the topic you are modifying type: string - returned: state == "present" sample: "arn:aws:sns:us-east-1:123456789012:my_topic_name" + +sns_topic: + description: Dict of sns topic details + type: dict + sample: + name: sns-topic-name + state: present + display_name: default + policy: {} + delivery_policy: {} + subscriptions_new: [] + subscriptions_existing: [] + subscriptions_deleted: [] + subscriptions_added: [] + subscriptions_purge': false + check_mode: false + topic_created: false + topic_deleted: false + attributes_set: [] ''' import sys @@ -141,38 +135,210 @@ HAS_BOTO = False -def canonicalize_endpoint(protocol, endpoint): - if protocol == 'sms': - import re - return re.sub('[^0-9]*', '', endpoint) - return endpoint - - -def get_all_topics(connection, module): - next_token = None - topics = [] - while True: +class SnsTopicManager(object): + """ Handles SNS Topic creation and destruction """ + + def __init__(self, + module, + name, + state, + display_name, + policy, + delivery_policy, + subscriptions, + purge_subscriptions, + check_mode, + region, + **aws_connect_params): + + self.region = region + self.aws_connect_params = aws_connect_params + self.connection = self._get_boto_connection() + self.changed = False + self.module = module + self.name = name + self.state = state + self.display_name = display_name + self.policy = policy + self.delivery_policy = delivery_policy + self.subscriptions = subscriptions + self.subscriptions_existing = [] + self.subscriptions_deleted = [] + self.subscriptions_added = [] + self.purge_subscriptions = purge_subscriptions + self.check_mode = check_mode + self.topic_created = False + self.topic_deleted = False + self.arn_topic = None + self.attributes_set = [] + + def _get_boto_connection(self): try: - response = connection.get_all_topics(next_token) - except BotoServerError, e: - module.fail_json(msg=e.message) + return connect_to_aws(boto.sns, self.region, + **self.aws_connect_params) + except BotoServerError, err: + self.module.fail_json(msg=err.message) + + def _get_all_topics(self): + next_token = None + topics = [] + while True: + try: + response = self.connection.get_all_topics(next_token) + except BotoServerError, err: + module.fail_json(msg=err.message) + topics.extend(response['ListTopicsResponse']['ListTopicsResult']['Topics']) + next_token = response['ListTopicsResponse']['ListTopicsResult']['NextToken'] + if not next_token: + break + return [t['TopicArn'] for t in topics] + + + def _arn_topic_lookup(self): + # topic names cannot have colons, so this captures the full topic name + all_topics = self._get_all_topics() + lookup_topic = ':%s' % self.name + for topic in all_topics: + if topic.endswith(lookup_topic): + return topic + + + def _create_topic(self): + self.changed = True + self.topic_created = True + if not self.check_mode: + self.connection.create_topic(self.name) + self.arn_topic = self._arn_topic_lookup() + while not self.arn_topic: + time.sleep(3) + self.arn_topic = self._arn_topic_lookup() - topics.extend(response['ListTopicsResponse']['ListTopicsResult']['Topics']) - next_token = \ - response['ListTopicsResponse']['ListTopicsResult']['NextToken'] - if not next_token: - break - return [t['TopicArn'] for t in topics] + def _set_topic_attrs(self): + topic_attributes = self.connection.get_topic_attributes(self.arn_topic) \ + ['GetTopicAttributesResponse'] ['GetTopicAttributesResult'] \ + ['Attributes'] + + if self.display_name and self.display_name != topic_attributes['DisplayName']: + self.changed = True + self.attributes_set.append('display_name') + if not self.check_mode: + self.connection.set_topic_attributes(self.arn_topic, 'DisplayName', + self.display_name) + + if self.policy and self.policy != json.loads(topic_attributes['Policy']): + self.changed = True + self.attributes_set.append('policy') + if not self.check_mode: + self.connection.set_topic_attributes(self.arn_topic, 'Policy', + json.dumps(self.policy)) + + if self.delivery_policy and ('DeliveryPolicy' not in topic_attributes or \ + self.delivery_policy != json.loads(topic_attributes['DeliveryPolicy'])): + self.changed = True + self.attributes_set.append('delivery_policy') + if not self.check_mode: + self.connection.set_topic_attributes(self.arn_topic, 'DeliveryPolicy', + json.dumps(self.delivery_policy)) + + + def _canonicalize_endpoint(self, protocol, endpoint): + if protocol == 'sms': + return re.sub('[^0-9]*', '', endpoint) + return endpoint + + + def _get_topic_subs(self): + next_token = None + while True: + response = self.connection.get_all_subscriptions_by_topic(self.arn_topic, next_token) + self.subscriptions_existing.extend(response['ListSubscriptionsByTopicResponse'] \ + ['ListSubscriptionsByTopicResult']['Subscriptions']) + next_token = response['ListSubscriptionsByTopicResponse'] \ + ['ListSubscriptionsByTopicResult']['NextToken'] + if not next_token: + break + + def _set_topic_subs(self): + subscriptions_existing_list = [] + desired_subscriptions = [(sub['protocol'], + self._canonicalize_endpoint(sub['protocol'], sub['endpoint'])) for sub in + self.subscriptions] + + if self.subscriptions_existing: + for sub in self.subscriptions_existing: + sub_key = (sub['Protocol'], sub['Endpoint']) + subscriptions_existing_list.append(sub_key) + if self.purge_subscriptions and sub_key not in desired_subscriptions and \ + sub['SubscriptionArn'] != 'PendingConfirmation': + self.changed = True + self.subscriptions_deleted.append(sub_key) + if not self.check_mode: + self.connection.unsubscribe(sub['SubscriptionArn']) + + for (protocol, endpoint) in desired_subscriptions: + if (protocol, endpoint) not in subscriptions_existing_list: + self.changed = True + self.subscriptions_added.append(sub) + if not self.check_mode: + self.connection.subscribe(self.arn_topic, protocol, endpoint) + + + def _delete_subscriptions(self): + # NOTE: subscriptions in 'PendingConfirmation' timeout in 3 days + # https://forums.aws.amazon.com/thread.jspa?threadID=85993 + for sub in self.subscriptions_existing: + if sub['SubscriptionArn'] != 'PendingConfirmation': + self.subscriptions_deleted.append(sub['SubscriptionArn']) + self.changed = True + if not self.check_mode: + self.connection.unsubscribe(sub['SubscriptionArn']) + + + def _delete_topic(self): + self.topic_deleted = True + self.changed = True + if not self.check_mode: + self.connection.delete_topic(self.arn_topic) + + + def ensure_ok(self): + self.arn_topic = self._arn_topic_lookup() + if not self.arn_topic: + self._create_topic() + self._set_topic_attrs() + self._get_topic_subs() + self._set_topic_subs() + + def ensure_gone(self): + self.arn_topic = self._arn_topic_lookup() + if self.arn_topic: + self._get_topic_subs() + if self.subscriptions_existing: + self._delete_subscriptions() + self._delete_topic() + + + def get_info(self): + info = { + 'name': self.name, + 'state': self.state, + 'display_name': self.display_name, + 'policy': self.policy, + 'delivery_policy': self.delivery_policy, + 'subscriptions_new': self.subscriptions, + 'subscriptions_existing': self.subscriptions_existing, + 'subscriptions_deleted': self.subscriptions_deleted, + 'subscriptions_added': self.subscriptions_added, + 'subscriptions_purge': self.purge_subscriptions, + 'check_mode': self.check_mode, + 'topic_created': self.topic_created, + 'topic_deleted': self.topic_deleted, + 'attributes_set': self.attributes_set + } + + return info -def arn_topic_lookup(connection, short_topic, module): - # topic names cannot have colons, so this captures the full topic name - all_topics = get_all_topics(connection, module) - lookup_topic = ':%s' % short_topic - for topic in all_topics: - if topic.endswith(lookup_topic): - return topic - return None def main(): @@ -190,7 +356,8 @@ def main(): ) ) - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) if not HAS_BOTO: module.fail_json(msg='boto required for this module') @@ -203,142 +370,35 @@ def main(): subscriptions = module.params.get('subscriptions') purge_subscriptions = module.params.get('purge_subscriptions') check_mode = module.check_mode - changed = False - - topic_created = False - attributes_set = [] - subscriptions_added = [] - subscriptions_deleted = [] region, ec2_url, aws_connect_params = get_aws_connection_info(module) if not region: module.fail_json(msg="region must be specified") - try: - connection = connect_to_aws(boto.sns, region, **aws_connect_params) - except boto.exception.NoAuthHandlerFound, e: - module.fail_json(msg=str(e)) - - # topics cannot contain ':', so thats the decider - if ':' in name: - all_topics = get_all_topics(connection, module) - if name in all_topics: - arn_topic = name - elif state == 'absent': - module.exit_json(changed=False) - else: - module.fail_json(msg="specified an ARN for a topic but it doesn't" - " exist") - else: - arn_topic = arn_topic_lookup(connection, name, module) - if not arn_topic: - if state == 'absent': - module.exit_json(changed=False) - elif check_mode: - module.exit_json(changed=True, topic_created=True, - subscriptions_added=subscriptions, - subscriptions_deleted=[]) - - changed=True - topic_created = True - try: - connection.create_topic(name) - except BotoServerError, e: - module.fail_json(msg=e.message) - arn_topic = arn_topic_lookup(connection, name, module) - while not arn_topic: - time.sleep(3) - arn_topic = arn_topic_lookup(connection, name, module) - - if arn_topic and state == "absent": - if not check_mode: - try: - connection.delete_topic(arn_topic) - except BotoServerError, e: - module.fail_json(msg=e.message) - module.exit_json(changed=True) + sns_topic = SnsTopicManager(module, + name, + state, + display_name, + policy, + delivery_policy, + subscriptions, + purge_subscriptions, + check_mode, + region, + **aws_connect_params) - topic_attributes = connection.get_topic_attributes(arn_topic) \ - ['GetTopicAttributesResponse'] ['GetTopicAttributesResult'] \ - ['Attributes'] - if display_name and display_name != topic_attributes['DisplayName']: - changed = True - attributes_set.append('display_name') - if not check_mode: - try: - connection.set_topic_attributes(arn_topic, 'DisplayName', display_name) - except BotoServerError, e: - module.fail_json(msg=e.message) - - if policy and policy != json.loads(topic_attributes['Policy']): - changed = True - attributes_set.append('policy') - if not check_mode: - try: - connection.set_topic_attributes(arn_topic, 'Policy', json.dumps(policy)) - except BotoServerError, e: - module.fail_json(msg=e.message) - - if delivery_policy and ('DeliveryPolicy' not in topic_attributes or \ - delivery_policy != json.loads(topic_attributes['DeliveryPolicy'])): - changed = True - attributes_set.append('delivery_policy') - if not check_mode: - try: - connection.set_topic_attributes(arn_topic, 'DeliveryPolicy',json.dumps(delivery_policy)) - except BotoServerError, e: - module.fail_json(msg=e.message) + if state == 'present': + sns_topic.ensure_ok() + elif state == 'absent': + sns_topic.ensure_gone() - next_token = None - aws_subscriptions = [] - while True: - try: - response = connection.get_all_subscriptions_by_topic(arn_topic, - next_token) - except BotoServerError, e: - module.fail_json(msg=e.message) + sns_facts = dict(changed=sns_topic.changed, + sns_arn=sns_topic.arn_topic, + sns_topic=sns_topic.get_info()) + + module.exit_json(**sns_facts) - aws_subscriptions.extend(response['ListSubscriptionsByTopicResponse'] \ - ['ListSubscriptionsByTopicResult']['Subscriptions']) - next_token = response['ListSubscriptionsByTopicResponse'] \ - ['ListSubscriptionsByTopicResult']['NextToken'] - if not next_token: - break - - desired_subscriptions = [(sub['protocol'], - canonicalize_endpoint(sub['protocol'], sub['endpoint'])) for sub in - subscriptions] - - aws_subscriptions_list = [] - - for sub in aws_subscriptions: - sub_key = (sub['Protocol'], sub['Endpoint']) - aws_subscriptions_list.append(sub_key) - if purge_subscriptions and sub_key not in desired_subscriptions and \ - sub['SubscriptionArn'] != 'PendingConfirmation': - changed = True - subscriptions_deleted.append(sub_key) - if not check_mode: - try: - connection.unsubscribe(sub['SubscriptionArn']) - except BotoServerError, e: - module.fail_json(msg=e.message) - - for (protocol, endpoint) in desired_subscriptions: - if (protocol, endpoint) not in aws_subscriptions_list: - changed = True - subscriptions_added.append(sub) - if not check_mode: - try: - connection.subscribe(arn_topic, protocol, endpoint) - except BotoServerError, e: - module.fail_json(msg=e.message) - - module.exit_json(changed=changed, topic_created=topic_created, - attributes_set=attributes_set, - subscriptions_added=subscriptions_added, - subscriptions_deleted=subscriptions_deleted, sns_arn=arn_topic) from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * From 342af8b249dfda0f4be1a77d4e8744ad0d3f728c Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 23 May 2016 13:06:14 -0700 Subject: [PATCH 1576/2522] Add __init__.py and update version_added to fix module for ansible-validate checks --- cloud/smartos/__init__.py | 0 cloud/smartos/smartos_image_facts.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 cloud/smartos/__init__.py diff --git a/cloud/smartos/__init__.py b/cloud/smartos/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cloud/smartos/smartos_image_facts.py b/cloud/smartos/smartos_image_facts.py index 57ab62b9ce9..189389de720 100644 --- a/cloud/smartos/smartos_image_facts.py +++ b/cloud/smartos/smartos_image_facts.py @@ -26,7 +26,7 @@ description: - Retrieve facts about all installed images on SmartOS. Facts will be inserted to the ansible_facts key. -version_added: "2.0" +version_added: "2.2" author: Adam Števko (@xen0l) options: filters: From e35d5f2c54134cf7220557f95339466d620942f3 Mon Sep 17 00:00:00 2001 From: jhawkesworth Date: Mon, 23 May 2016 21:14:49 +0100 Subject: [PATCH 1577/2522] Add info about where you can find product ids for changed checking on win_package module (#2281) --- windows/win_package.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_package.py b/windows/win_package.py index 946b730b97b..2fc2b53c9c8 100644 --- a/windows/win_package.py +++ b/windows/win_package.py @@ -28,7 +28,7 @@ author: Trond Hindenes short_description: Installs/Uninstalls a installable package, either from local file system or url description: - - Installs or uninstalls a package + - Installs or uninstalls a package. Optionally uses a product_id to check if the package needs installing. You can find product ids for installed programs in the windows registry either in ``HKLM:Software\Microsoft\Windows\CurrentVersion\Uninstall`` or for 32 bit programs ``HKLM:Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall`` options: path: description: From a055d7240ac11fa80868f28163ceb4db9aa5eb57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Tue, 24 May 2016 13:24:04 +0200 Subject: [PATCH 1578/2522] fix build and doc cleanups (#2286) --- .travis.yml | 2 ++ windows/win_package.py | 28 ++++++++++++---------------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0435132fef5..62c8b7174aa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,6 +24,7 @@ env: cloud/amazon/ec2_snapshot_facts.py cloud/amazon/ec2_vol_facts.py cloud/amazon/ec2_vpc_igw.py + cloud/amazon/ec2_vpc_nacl.py cloud/amazon/ec2_vpc_net_facts.py cloud/amazon/ec2_vpc_route_table_facts.py cloud/amazon/ec2_vpc_route_table.py @@ -109,6 +110,7 @@ env: notification/mqtt.py notification/sns.py notification/typetalk.py + packaging/language/maven_artifact.py packaging/os/layman.py packaging/os/yum_repository.py source_control/gitlab_group.py diff --git a/windows/win_package.py b/windows/win_package.py index 2fc2b53c9c8..633f6c6d339 100644 --- a/windows/win_package.py +++ b/windows/win_package.py @@ -28,31 +28,28 @@ author: Trond Hindenes short_description: Installs/Uninstalls a installable package, either from local file system or url description: - - Installs or uninstalls a package. Optionally uses a product_id to check if the package needs installing. You can find product ids for installed programs in the windows registry either in ``HKLM:Software\Microsoft\Windows\CurrentVersion\Uninstall`` or for 32 bit programs ``HKLM:Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall`` + - Installs or uninstalls a package. + - 'Optionally uses a product_id to check if the package needs installing. You can find product ids for installed programs in the windows registry either in C(HKLM:Software\\Microsoft\\Windows\CurrentVersion\\Uninstall) or for 32 bit programs C(HKLM:Software\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall)' options: path: description: - Location of the package to be installed (either on file system, network share or url) required: true - default: null - aliases: [] name: description: - name of the package. Just for logging reasons, will use the value of path if name isn't specified required: false default: null - aliases: [] product_id: description: - product id of the installed package (used for checking if already installed) required: true - default: null aliases: [productid] arguments: description: - Any arguments the installer needs default: null - aliases: [] + required: false state: description: - Install or Uninstall @@ -60,28 +57,27 @@ - present - absent default: present + required: false aliases: [ensure] user_name: description: - Username of an account with access to the package if its located on a file share. Only needed if the winrm user doesn't have access to the package. Also specify user_password for this to function properly. default: null - aliases: [] + required: false user_password: description: - Password of an account with access to the package if its located on a file share. Only needed if the winrm user doesn't have access to the package. Also specify user_name for this to function properly. default: null - aliases: [] + required: false ''' EXAMPLES = ''' # Playbook example - - name: Install the vc thingy - win_package: - name="Microsoft Visual C thingy" - path="http://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x64.exe" - Product_Id="{CF2BEA3C-26EA-32F8-AA9B-331F7E34BA97}" - Arguments="/install /passive /norestart" - - +- name: Install the vc thingy + win_package: + name="Microsoft Visual C thingy" + path="http://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x64.exe" + Product_Id="{CF2BEA3C-26EA-32F8-AA9B-331F7E34BA97}" + Arguments="/install /passive /norestart" ''' From d1a4f703ce493db6e0dfa8b37bb225ca7f53651c Mon Sep 17 00:00:00 2001 From: Adrian Likins Date: Tue, 24 May 2016 11:55:14 -0400 Subject: [PATCH 1579/2522] Fix pkgutil "upgrade_catalog must be one of" err (#2149) The arg spec for update_catalog include 'type=bool' and 'choices=["yes", "no"] which can never both be true. Remove the 'choices' directive, and update doc string. Fixes #2144 --- packaging/os/pkgutil.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packaging/os/pkgutil.py b/packaging/os/pkgutil.py index 0cdc3dee5e8..35ccb4e1906 100644 --- a/packaging/os/pkgutil.py +++ b/packaging/os/pkgutil.py @@ -54,8 +54,7 @@ description: - If you want to refresh your catalog from the mirror, set this to (C(yes)). required: false - choices: ["yes", "no"] - default: no + default: False version_added: "2.1" ''' @@ -130,7 +129,7 @@ def main(): name = dict(required = True), state = dict(required = True, choices=['present', 'absent','latest']), site = dict(default = None), - update_catalog = dict(required = False, default = "no", type='bool', choices=["yes","no"]), + update_catalog = dict(required = False, default = False, type='bool'), ), supports_check_mode=True ) From b6b04795c3fdeccf17eb0556d3f26855755ece2a Mon Sep 17 00:00:00 2001 From: dougluce Date: Tue, 24 May 2016 22:54:07 -0700 Subject: [PATCH 1580/2522] Don't bomb if original didn't exist (#2295) If we don't have an existing file, original ends up as None. Bug introduced in 70fa125 --- files/blockinfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 files/blockinfile.py diff --git a/files/blockinfile.py b/files/blockinfile.py old mode 100644 new mode 100755 index 81834dfd2da..da67e2d4b37 --- a/files/blockinfile.py +++ b/files/blockinfile.py @@ -281,7 +281,7 @@ def main(): if lines: result = '\n'.join(lines) - if original.endswith('\n'): + if original and original.endswith('\n'): result += '\n' else: result = '' From 400484f69fd475beb0a2b32c2d60b89b3e2d9067 Mon Sep 17 00:00:00 2001 From: Mike Mochan Date: Tue, 29 Dec 2015 19:48:00 +1000 Subject: [PATCH 1581/2522] Initial commit for cross account VPC peering module --- cloud/amazon/ec2_vpc_peer.py | 302 +++++++++++++++++++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 cloud/amazon/ec2_vpc_peer.py diff --git a/cloud/amazon/ec2_vpc_peer.py b/cloud/amazon/ec2_vpc_peer.py new file mode 100644 index 00000000000..9fb888dfdf8 --- /dev/null +++ b/cloud/amazon/ec2_vpc_peer.py @@ -0,0 +1,302 @@ +#!/usr/bin/python +# +DOCUMENTATION = ''' +module: ec2_vpc_peer +short_description: create or remove a peering connection between to ec2 VPCs. +description: + - +options: + vpc_id: + description: + - VPC id of the requesting VPC. + required: false + peer_vpc_id: + description: + - VPC id of the accepting VPC. + required: false + peer_owner_id: + description: + - The AWS account number for cross account peering. + required: false + state: + description: + - Create, delete, accept, reject a peering connection. + required: false + default: present + choices: ['present', 'absent', 'accept', 'reject'] + region: + description: + - The AWS region to use. Must be specified if ec2_url is not used. If not specified then the value of the EC2_REGION environment variable, if any, is used. + required: false + default: null + aliases: ['aws_region', 'ec2_region'] + profile: + description: + - boto3 profile name. + required: false + default: None + aws_secret_key: + description: + - AWS secret key. If not set then the value of the AWS_SECRET_KEY environment variable is used. + required: false + default: None + aliases: ['ec2_secret_key', 'secret_key'] + aws_access_key: + description: + - AWS access key. If not set then the value of the AWS_ACCESS_KEY environment variable is used. + required: false + default: None + aliases: ['ec2_access_key', 'access_key'] +author: Mike Mochan(@mmochan) +extends_documentation_fragment: aws +''' + +EXAMPLES = ''' +# Complete example to create and accept a local peering connection. +- name: Create local account VPC peering Connection + ec2_vpc_peer: + region: ap-southeast-2 + vpc_id: vpc-12345678 + peer_vpc_id: vpc-87654321 + state: present + register: vpc_peer + +- name: Accept local VPC peering request + ec2_vpc_peer: + region: ap-southeast-2 + peering_id: "{{ vpc_peer.peering_id }}" + state: accept + register: action_peer + +# Complete example to delete a local peering connection. +- name: Create local account VPC peering Connection + ec2_vpc_peer: + region: ap-southeast-2 + vpc_id: vpc-12345678 + peer_vpc_id: vpc-87654321 + state: present + register: vpc_peer + +- name: delete a local VPC peering Connection + ec2_vpc_peer: + region: ap-southeast-2 + peering_id: "{{ vpc_peer.peering_id }}" + state: absent + register: vpc_peer + + # Complete example to create and accept a cross account peering connection. +- name: Create cross account VPC peering Connection + ec2_vpc_peer: + region: ap-southeast-2 + vpc_id: vpc-12345678 + peer_vpc_id: vpc-87654321 + peer_vpc_id: vpc-ce26b7ab + peer_owner_id: 123456789102 + state: present + register: vpc_peer + +- name: Accept peering connection from remote account + ec2_vpc_peer: + region: ap-southeast-2 + peering_id: "{{ vpc_peer.peering_id }}" + profile: bot03_profile_for_cross_account + state: accept + register: vpc_peer + +# Complete example to create and reject a local peering connection. +- name: Create local account VPC peering Connection + ec2_vpc_peer: + region: ap-southeast-2 + vpc_id: vpc-12345678 + peer_vpc_id: vpc-87654321 + state: present + register: vpc_peer + +- name: Reject a local VPC peering Connection + ec2_vpc_peer: + region: ap-southeast-2 + peering_id: "{{ vpc_peer.peering_id }}" + state: reject + +# Complete example to create and accept a cross account peering connection. +- name: Create cross account VPC peering Connection + ec2_vpc_peer: + region: ap-southeast-2 + vpc_id: vpc-12345678 + peer_vpc_id: vpc-87654321 + peer_vpc_id: vpc-ce26b7ab + peer_owner_id: 123456789102 + state: present + register: vpc_peer + +- name: Accept a cross account VPC peering connection request + ec2_vpc_peer: + region: ap-southeast-2 + peering_id: "{{ vpc_peer.peering_id }}" + profile: bot03_profile_for_cross_account + state: accept + + +# Complete example to create and reject a cross account peering connection. +- name: Create cross account VPC peering Connection + ec2_vpc_peer: + region: ap-southeast-2 + vpc_id: vpc-12345678 + peer_vpc_id: vpc-87654321 + peer_vpc_id: vpc-ce26b7ab + peer_owner_id: 123456789102 + state: present + register: vpc_peer + +- name: Reject a cross account VPC peering Connection + ec2_vpc_peer: + region: ap-southeast-2 + peering_id: "{{ vpc_peer.peering_id }}" + profile: bot03_profile_for_cross_account + state: reject + +''' +RETURN = ''' +task: + description: details about the tast that was started + type: complex + sample: "TODO: include sample" +''' + +try: + import json + import datetime + import boto + import botocore + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +try: + import boto3 + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + +import q + + +def describe_peering_connections(vpc_id, peer_vpc_id, client): + result = client.describe_vpc_peering_connections(Filters=[ + {'Name': 'requester-vpc-info.vpc-id', 'Values': [vpc_id]}, + {'Name': 'accepter-vpc-info.vpc-id', 'Values': [peer_vpc_id]} + ]) + if result['VpcPeeringConnections'] == []: + result = client.describe_vpc_peering_connections(Filters=[ + {'Name': 'requester-vpc-info.vpc-id', 'Values': [peer_vpc_id]}, + {'Name': 'accepter-vpc-info.vpc-id', 'Values': [vpc_id]} + ]) + return result + + +def is_active(peering_conn): + return peering_conn['Status']['Code'] == 'active' + + +def is_pending(peering_conn): + return peering_conn['Status']['Code'] == 'pending-acceptance' + + +def peer_status(resource, module): + peer_id = module.params.get('peering_id') + vpc_peering_connection = resource.VpcPeeringConnection(peer_id) + return vpc_peering_connection.status['Message'] + + +def create_peer_connection(client, module): + changed = False + vpc_id = module.params.get('vpc_id') + peer_vpc_id = module.params.get('peer_vpc_id') + peer_owner_id = module.params.get('peer_owner_id', False) + peering_conns = describe_peering_connections(vpc_id, peer_vpc_id, client) + for peering_conn in peering_conns['VpcPeeringConnections']: + if is_active(peering_conn): + return (False, peering_conn['VpcPeeringConnectionId']) + if is_pending(peering_conn): + return (False, peering_conn['VpcPeeringConnectionId']) + if not peer_owner_id: + try: + peering_conn = client.create_vpc_peering_connection(VpcId=vpc_id, PeerVpcId=peer_vpc_id) + return (True, peering_conn['VpcPeeringConnection']['VpcPeeringConnectionId']) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + else: + try: + peering_conn = client.create_vpc_peering_connection(VpcId=vpc_id, PeerVpcId=peer_vpc_id, PeerOwnerId=str(peer_owner_id)) + return (True, peering_conn['VpcPeeringConnection']['VpcPeeringConnectionId']) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + + +def accept_reject_delete(state, client, resource, module): + changed = False + peer_id = module.params.get('peering_id') + if state == "accept": + if peer_status(resource, module) == "Active": + return (False, peer_id) + try: + client.accept_vpc_peering_connection(VpcPeeringConnectionId=peer_id) + return (True, peer_id) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + if state == "reject": + if peer_status(resource, module) != "Active": + try: + client.reject_vpc_peering_connection(VpcPeeringConnectionId=peer_id) + return (True, peer_id) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + else: + return (False, peer_id) + if state == "absent": + try: + client.delete_vpc_peering_connection(VpcPeeringConnectionId=peer_id) + return (True, peer_id) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + return (changed, "") + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + region=dict(), + vpc_id=dict(), + peer_vpc_id=dict(), + peer_owner_id=dict(), + peering_id=dict(), + profile=dict(), + state=dict(default='present', choices=['present', 'absent', 'accept', 'reject']) + ) + ) + module = AnsibleModule(argument_spec=argument_spec) + + if not (HAS_BOTO or HAS_BOTO3): + module.fail_json(msg='json and boto/boto3 is required.') + state = module.params.get('state').lower() + try: + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + client = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_kwargs) + resource = boto3_conn(module, conn_type='resource', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_kwargs) + except botocore.exceptions.NoCredentialsError, e: + module.fail_json(msg="Can't authorize connection - "+str(e)) + + if state == 'present': + (changed, results) = create_peer_connection(client, module) + module.exit_json(changed=changed, peering_id=results) + else: + (changed, results) = accept_reject_delete(state, client, resource, module) + module.exit_json(changed=changed, peering_id=results) + + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From 92dea03181c704e4433c1321b4f1100f16c96e88 Mon Sep 17 00:00:00 2001 From: Mike Mochan Date: Tue, 29 Dec 2015 20:05:16 +1000 Subject: [PATCH 1582/2522] Version added, GPLv3 License header added --- cloud/amazon/ec2_vpc_peer.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/ec2_vpc_peer.py b/cloud/amazon/ec2_vpc_peer.py index 9fb888dfdf8..7e6261c7b97 100644 --- a/cloud/amazon/ec2_vpc_peer.py +++ b/cloud/amazon/ec2_vpc_peer.py @@ -1,10 +1,25 @@ #!/usr/bin/python +# This file is part of Ansible # +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . DOCUMENTATION = ''' module: ec2_vpc_peer -short_description: create or remove a peering connection between to ec2 VPCs. +short_description: create, delete, accept, and reject VPC peering connections between two VPCs. description: - - + - Read the AWS documentation for VPC Peering Connections + U(http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/vpc-peering.html) +version_added: "2.1" options: vpc_id: description: From f84af4873349b3ee2734052b80d6f51e9ae873af Mon Sep 17 00:00:00 2001 From: Mike Mochan Date: Tue, 29 Dec 2015 20:12:28 +1000 Subject: [PATCH 1583/2522] removed debug package --- cloud/amazon/ec2_vpc_peer.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cloud/amazon/ec2_vpc_peer.py b/cloud/amazon/ec2_vpc_peer.py index 7e6261c7b97..d504c111ee9 100644 --- a/cloud/amazon/ec2_vpc_peer.py +++ b/cloud/amazon/ec2_vpc_peer.py @@ -19,7 +19,7 @@ description: - Read the AWS documentation for VPC Peering Connections U(http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/vpc-peering.html) -version_added: "2.1" +version_added: "2.1" options: vpc_id: description: @@ -192,9 +192,7 @@ HAS_BOTO3 = True except ImportError: HAS_BOTO3 = False - -import q - + def describe_peering_connections(vpc_id, peer_vpc_id, client): result = client.describe_vpc_peering_connections(Filters=[ From 34ae687ae3d1c8fd7c70711968af6ea161d4a1a4 Mon Sep 17 00:00:00 2001 From: Mike Mochan Date: Tue, 29 Dec 2015 21:38:14 +1000 Subject: [PATCH 1584/2522] cloud/amazon/ec2_vpc_peer.py --- cloud/amazon/ec2_vpc_peer.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/cloud/amazon/ec2_vpc_peer.py b/cloud/amazon/ec2_vpc_peer.py index d504c111ee9..d68dec96334 100644 --- a/cloud/amazon/ec2_vpc_peer.py +++ b/cloud/amazon/ec2_vpc_peer.py @@ -104,8 +104,7 @@ ec2_vpc_peer: region: ap-southeast-2 vpc_id: vpc-12345678 - peer_vpc_id: vpc-87654321 - peer_vpc_id: vpc-ce26b7ab + peer_vpc_id: vpc-12345678 peer_owner_id: 123456789102 state: present register: vpc_peer @@ -138,8 +137,7 @@ ec2_vpc_peer: region: ap-southeast-2 vpc_id: vpc-12345678 - peer_vpc_id: vpc-87654321 - peer_vpc_id: vpc-ce26b7ab + peer_vpc_id: vpc-12345678 peer_owner_id: 123456789102 state: present register: vpc_peer @@ -157,8 +155,7 @@ ec2_vpc_peer: region: ap-southeast-2 vpc_id: vpc-12345678 - peer_vpc_id: vpc-87654321 - peer_vpc_id: vpc-ce26b7ab + peer_vpc_id: vpc-12345678 peer_owner_id: 123456789102 state: present register: vpc_peer From f6c7bdf9c531363c7a4e46082f311df28d320d30 Mon Sep 17 00:00:00 2001 From: Mike Mochan Date: Sat, 9 Jan 2016 15:38:39 +1000 Subject: [PATCH 1585/2522] updates to Documentation - Removed refs to Boto, added params dict() and removed obsolete if statements --- cloud/amazon/ec2_vpc_peer.py | 73 ++++++++++++------------------------ 1 file changed, 23 insertions(+), 50 deletions(-) diff --git a/cloud/amazon/ec2_vpc_peer.py b/cloud/amazon/ec2_vpc_peer.py index d68dec96334..9093a7c60bd 100644 --- a/cloud/amazon/ec2_vpc_peer.py +++ b/cloud/amazon/ec2_vpc_peer.py @@ -39,29 +39,6 @@ required: false default: present choices: ['present', 'absent', 'accept', 'reject'] - region: - description: - - The AWS region to use. Must be specified if ec2_url is not used. If not specified then the value of the EC2_REGION environment variable, if any, is used. - required: false - default: null - aliases: ['aws_region', 'ec2_region'] - profile: - description: - - boto3 profile name. - required: false - default: None - aws_secret_key: - description: - - AWS secret key. If not set then the value of the AWS_SECRET_KEY environment variable is used. - required: false - default: None - aliases: ['ec2_secret_key', 'secret_key'] - aws_access_key: - description: - - AWS access key. If not set then the value of the AWS_ACCESS_KEY environment variable is used. - required: false - default: None - aliases: ['ec2_access_key', 'access_key'] author: Mike Mochan(@mmochan) extends_documentation_fragment: aws ''' @@ -170,26 +147,19 @@ ''' RETURN = ''' task: - description: details about the tast that was started - type: complex - sample: "TODO: include sample" + description: The result of the create, accept, reject or delete action. + returned: success + type: dictionary ''' try: import json - import datetime - import boto import botocore - HAS_BOTO = True -except ImportError: - HAS_BOTO = False - -try: import boto3 HAS_BOTO3 = True except ImportError: HAS_BOTO3 = False - + def describe_peering_connections(vpc_id, peer_vpc_id, client): result = client.describe_vpc_peering_connections(Filters=[ @@ -220,6 +190,13 @@ def peer_status(resource, module): def create_peer_connection(client, module): changed = False + params = dict() + params['VpcId'] = module.params.get('vpc_id') + params['PeerVpcId'] = module.params.get('peer_vpc_id') + if module.params.get('peer_owner_id'): + params['PeerOwnerId'] = str(module.params.get('peer_owner_id')) + params['DryRun'] = module.check_mode + vpc_id = module.params.get('vpc_id') peer_vpc_id = module.params.get('peer_vpc_id') peer_owner_id = module.params.get('peer_owner_id', False) @@ -229,43 +206,40 @@ def create_peer_connection(client, module): return (False, peering_conn['VpcPeeringConnectionId']) if is_pending(peering_conn): return (False, peering_conn['VpcPeeringConnectionId']) - if not peer_owner_id: - try: - peering_conn = client.create_vpc_peering_connection(VpcId=vpc_id, PeerVpcId=peer_vpc_id) - return (True, peering_conn['VpcPeeringConnection']['VpcPeeringConnectionId']) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=str(e)) - else: try: - peering_conn = client.create_vpc_peering_connection(VpcId=vpc_id, PeerVpcId=peer_vpc_id, PeerOwnerId=str(peer_owner_id)) + peering_conn = client.create_vpc_peering_connection(**params) return (True, peering_conn['VpcPeeringConnection']['VpcPeeringConnectionId']) except botocore.exceptions.ClientError as e: - module.fail_json(msg=str(e)) + module.fail_json(msg=str(e)) def accept_reject_delete(state, client, resource, module): changed = False + params = dict() + params['VpcPeeringConnectionId'] = module.params.get('peering_id') + params['DryRun'] = module.check_mode + peer_id = module.params.get('peering_id') if state == "accept": if peer_status(resource, module) == "Active": return (False, peer_id) try: - client.accept_vpc_peering_connection(VpcPeeringConnectionId=peer_id) + client.accept_vpc_peering_connection(**params) return (True, peer_id) except botocore.exceptions.ClientError as e: module.fail_json(msg=str(e)) if state == "reject": if peer_status(resource, module) != "Active": try: - client.reject_vpc_peering_connection(VpcPeeringConnectionId=peer_id) + client.reject_vpc_peering_connection(**params) return (True, peer_id) except botocore.exceptions.ClientError as e: module.fail_json(msg=str(e)) else: - return (False, peer_id) + return (False, peer_id) if state == "absent": try: - client.delete_vpc_peering_connection(VpcPeeringConnectionId=peer_id) + client.delete_vpc_peering_connection(**params) return (True, peer_id) except botocore.exceptions.ClientError as e: module.fail_json(msg=str(e)) @@ -275,18 +249,17 @@ def accept_reject_delete(state, client, resource, module): def main(): argument_spec = ec2_argument_spec() argument_spec.update(dict( - region=dict(), vpc_id=dict(), peer_vpc_id=dict(), - peer_owner_id=dict(), peering_id=dict(), + peer_owner_id=dict(), profile=dict(), state=dict(default='present', choices=['present', 'absent', 'accept', 'reject']) ) ) module = AnsibleModule(argument_spec=argument_spec) - if not (HAS_BOTO or HAS_BOTO3): + if not HAS_BOTO3: module.fail_json(msg='json and boto/boto3 is required.') state = module.params.get('state').lower() try: From 10ce4d8b88eb8d37d2004b962307863547a775c6 Mon Sep 17 00:00:00 2001 From: Mike Mochan Date: Sat, 9 Jan 2016 17:09:49 +1000 Subject: [PATCH 1586/2522] refactored to use dict invocation for calling client peerings functions --- cloud/amazon/ec2_vpc_peer.py | 49 +++++++++++------------------------- 1 file changed, 15 insertions(+), 34 deletions(-) diff --git a/cloud/amazon/ec2_vpc_peer.py b/cloud/amazon/ec2_vpc_peer.py index 9093a7c60bd..49ec5320aba 100644 --- a/cloud/amazon/ec2_vpc_peer.py +++ b/cloud/amazon/ec2_vpc_peer.py @@ -182,12 +182,6 @@ def is_pending(peering_conn): return peering_conn['Status']['Code'] == 'pending-acceptance' -def peer_status(resource, module): - peer_id = module.params.get('peering_id') - vpc_peering_connection = resource.VpcPeeringConnection(peer_id) - return vpc_peering_connection.status['Message'] - - def create_peer_connection(client, module): changed = False params = dict() @@ -203,12 +197,13 @@ def create_peer_connection(client, module): peering_conns = describe_peering_connections(vpc_id, peer_vpc_id, client) for peering_conn in peering_conns['VpcPeeringConnections']: if is_active(peering_conn): - return (False, peering_conn['VpcPeeringConnectionId']) + return (changed, peering_conn['VpcPeeringConnectionId']) if is_pending(peering_conn): - return (False, peering_conn['VpcPeeringConnectionId']) + return (changed, peering_conn['VpcPeeringConnectionId']) try: peering_conn = client.create_vpc_peering_connection(**params) - return (True, peering_conn['VpcPeeringConnection']['VpcPeeringConnectionId']) + changed = True + return (changed, peering_conn['VpcPeeringConnection']['VpcPeeringConnectionId']) except botocore.exceptions.ClientError as e: module.fail_json(msg=str(e)) @@ -218,32 +213,18 @@ def accept_reject_delete(state, client, resource, module): params = dict() params['VpcPeeringConnectionId'] = module.params.get('peering_id') params['DryRun'] = module.check_mode + invocations = { + 'accept': client.accept_vpc_peering_connection, + 'reject': client.reject_vpc_peering_connection, + 'absent': client.delete_vpc_peering_connection + } + try: + invocations[state](**params) + changed = True + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) - peer_id = module.params.get('peering_id') - if state == "accept": - if peer_status(resource, module) == "Active": - return (False, peer_id) - try: - client.accept_vpc_peering_connection(**params) - return (True, peer_id) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=str(e)) - if state == "reject": - if peer_status(resource, module) != "Active": - try: - client.reject_vpc_peering_connection(**params) - return (True, peer_id) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=str(e)) - else: - return (False, peer_id) - if state == "absent": - try: - client.delete_vpc_peering_connection(**params) - return (True, peer_id) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=str(e)) - return (changed, "") + return changed, params['VpcPeeringConnectionId'] def main(): From 2392395a47837fbcdcb16e51b7071e0169c0e96f Mon Sep 17 00:00:00 2001 From: Mike Mochan Date: Sat, 9 Jan 2016 17:13:53 +1000 Subject: [PATCH 1587/2522] removed obsolete ec2 resource object --- cloud/amazon/ec2_vpc_peer.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cloud/amazon/ec2_vpc_peer.py b/cloud/amazon/ec2_vpc_peer.py index 49ec5320aba..a3e5b37dce7 100644 --- a/cloud/amazon/ec2_vpc_peer.py +++ b/cloud/amazon/ec2_vpc_peer.py @@ -208,7 +208,7 @@ def create_peer_connection(client, module): module.fail_json(msg=str(e)) -def accept_reject_delete(state, client, resource, module): +def accept_reject_delete(state, client, module): changed = False params = dict() params['VpcPeeringConnectionId'] = module.params.get('peering_id') @@ -246,7 +246,6 @@ def main(): try: region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) client = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_kwargs) - resource = boto3_conn(module, conn_type='resource', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_kwargs) except botocore.exceptions.NoCredentialsError, e: module.fail_json(msg="Can't authorize connection - "+str(e)) @@ -254,7 +253,7 @@ def main(): (changed, results) = create_peer_connection(client, module) module.exit_json(changed=changed, peering_id=results) else: - (changed, results) = accept_reject_delete(state, client, resource, module) + (changed, results) = accept_reject_delete(state, client, module) module.exit_json(changed=changed, peering_id=results) From 2291fc00d2abb52362a537453e32331954ccc554 Mon Sep 17 00:00:00 2001 From: Mike Mochan Date: Sat, 9 Jan 2016 20:03:30 +1000 Subject: [PATCH 1588/2522] update to capture peer status --- cloud/amazon/ec2_vpc_peer.py | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/cloud/amazon/ec2_vpc_peer.py b/cloud/amazon/ec2_vpc_peer.py index a3e5b37dce7..bd5bf80a1b3 100644 --- a/cloud/amazon/ec2_vpc_peer.py +++ b/cloud/amazon/ec2_vpc_peer.py @@ -200,12 +200,19 @@ def create_peer_connection(client, module): return (changed, peering_conn['VpcPeeringConnectionId']) if is_pending(peering_conn): return (changed, peering_conn['VpcPeeringConnectionId']) - try: - peering_conn = client.create_vpc_peering_connection(**params) - changed = True - return (changed, peering_conn['VpcPeeringConnection']['VpcPeeringConnectionId']) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=str(e)) + try: + peering_conn = client.create_vpc_peering_connection(**params) + changed = True + return (changed, peering_conn['VpcPeeringConnection']['VpcPeeringConnectionId']) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + + +def peer_status(client, module): + params = dict() + params['VpcPeeringConnectionIds'] = [module.params.get('peering_id')] + vpc_peering_connection = client.describe_vpc_peering_connections(**params) + return vpc_peering_connection['VpcPeeringConnections'][0]['Status']['Code'] def accept_reject_delete(state, client, module): @@ -218,12 +225,13 @@ def accept_reject_delete(state, client, module): 'reject': client.reject_vpc_peering_connection, 'absent': client.delete_vpc_peering_connection } - try: - invocations[state](**params) - changed = True - except botocore.exceptions.ClientError as e: - module.fail_json(msg=str(e)) - + if state == 'absent' or peer_status(client, module) != 'active': + try: + invocations[state](**params) + changed = True + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + return changed, params['VpcPeeringConnectionId'] @@ -241,11 +249,11 @@ def main(): module = AnsibleModule(argument_spec=argument_spec) if not HAS_BOTO3: - module.fail_json(msg='json and boto/boto3 is required.') + module.fail_json(msg='json and boto3 is required.') state = module.params.get('state').lower() try: region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - client = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_kwargs) + client = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_kwargs) except botocore.exceptions.NoCredentialsError, e: module.fail_json(msg="Can't authorize connection - "+str(e)) From 8ab4963e788b5ee1b8a4b5f5dce3dd4ee6767b86 Mon Sep 17 00:00:00 2001 From: Mike Mochan Date: Sat, 9 Jan 2016 20:20:32 +1000 Subject: [PATCH 1589/2522] Remove unused vars and pass params dict to describe_peering_connections --- cloud/amazon/ec2_vpc_peer.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/cloud/amazon/ec2_vpc_peer.py b/cloud/amazon/ec2_vpc_peer.py index bd5bf80a1b3..8d9c44f6e70 100644 --- a/cloud/amazon/ec2_vpc_peer.py +++ b/cloud/amazon/ec2_vpc_peer.py @@ -161,10 +161,10 @@ HAS_BOTO3 = False -def describe_peering_connections(vpc_id, peer_vpc_id, client): +def describe_peering_connections(params, client): result = client.describe_vpc_peering_connections(Filters=[ - {'Name': 'requester-vpc-info.vpc-id', 'Values': [vpc_id]}, - {'Name': 'accepter-vpc-info.vpc-id', 'Values': [peer_vpc_id]} + {'Name': 'requester-vpc-info.vpc-id', 'Values': [params['VpcId']]}, + {'Name': 'accepter-vpc-info.vpc-id', 'Values': [params['PeerVpcId']]} ]) if result['VpcPeeringConnections'] == []: result = client.describe_vpc_peering_connections(Filters=[ @@ -190,11 +190,7 @@ def create_peer_connection(client, module): if module.params.get('peer_owner_id'): params['PeerOwnerId'] = str(module.params.get('peer_owner_id')) params['DryRun'] = module.check_mode - - vpc_id = module.params.get('vpc_id') - peer_vpc_id = module.params.get('peer_vpc_id') - peer_owner_id = module.params.get('peer_owner_id', False) - peering_conns = describe_peering_connections(vpc_id, peer_vpc_id, client) + peering_conns = describe_peering_connections(params, client) for peering_conn in peering_conns['VpcPeeringConnections']: if is_active(peering_conn): return (changed, peering_conn['VpcPeeringConnectionId']) From a58a12fc1f0b96c6e49778b027418ad7e241bbf9 Mon Sep 17 00:00:00 2001 From: Mike Mochan Date: Sat, 9 Jan 2016 20:26:33 +1000 Subject: [PATCH 1590/2522] All calls to describe_vpc_peering_connections need to use the params dict --- cloud/amazon/ec2_vpc_peer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/ec2_vpc_peer.py b/cloud/amazon/ec2_vpc_peer.py index 8d9c44f6e70..6dc19c8b08a 100644 --- a/cloud/amazon/ec2_vpc_peer.py +++ b/cloud/amazon/ec2_vpc_peer.py @@ -168,8 +168,8 @@ def describe_peering_connections(params, client): ]) if result['VpcPeeringConnections'] == []: result = client.describe_vpc_peering_connections(Filters=[ - {'Name': 'requester-vpc-info.vpc-id', 'Values': [peer_vpc_id]}, - {'Name': 'accepter-vpc-info.vpc-id', 'Values': [vpc_id]} + {'Name': 'requester-vpc-info.vpc-id', 'Values': [params['PeerVpcId']]}, + {'Name': 'accepter-vpc-info.vpc-id', 'Values': [params['VpcId']]} ]) return result From 2934495efc2a42abf793a26fff24a76dfea99863 Mon Sep 17 00:00:00 2001 From: Mike Mochan Date: Mon, 28 Mar 2016 15:07:39 +1000 Subject: [PATCH 1591/2522] Added tagging functionality --- cloud/amazon/ec2_vpc_peer.py | 88 +++++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_vpc_peer.py b/cloud/amazon/ec2_vpc_peer.py index 6dc19c8b08a..a534b7973b7 100644 --- a/cloud/amazon/ec2_vpc_peer.py +++ b/cloud/amazon/ec2_vpc_peer.py @@ -33,6 +33,10 @@ description: - The AWS account number for cross account peering. required: false + tags: + description: + - Dictionary of tags to look for and apply when creating a Peering Connection. + required: false state: description: - Create, delete, accept, reject a peering connection. @@ -51,6 +55,10 @@ vpc_id: vpc-12345678 peer_vpc_id: vpc-87654321 state: present + tags: + Name: Peering conenction for VPC 21 to VPC 22 + CostCode: CC1234 + Project: phoenix register: vpc_peer - name: Accept local VPC peering request @@ -67,6 +75,10 @@ vpc_id: vpc-12345678 peer_vpc_id: vpc-87654321 state: present + tags: + Name: Peering conenction for VPC 21 to VPC 22 + CostCode: CC1234 + Project: phoenix register: vpc_peer - name: delete a local VPC peering Connection @@ -84,6 +96,10 @@ peer_vpc_id: vpc-12345678 peer_owner_id: 123456789102 state: present + tags: + Name: Peering conenction for VPC 21 to VPC 22 + CostCode: CC1234 + Project: phoenix register: vpc_peer - name: Accept peering connection from remote account @@ -101,6 +117,10 @@ vpc_id: vpc-12345678 peer_vpc_id: vpc-87654321 state: present + tags: + Name: Peering conenction for VPC 21 to VPC 22 + CostCode: CC1234 + Project: phoenix register: vpc_peer - name: Reject a local VPC peering Connection @@ -117,6 +137,10 @@ peer_vpc_id: vpc-12345678 peer_owner_id: 123456789102 state: present + tags: + Name: Peering conenction for VPC 21 to VPC 22 + CostCode: CC1234 + Project: phoenix register: vpc_peer - name: Accept a cross account VPC peering connection request @@ -135,6 +159,10 @@ peer_vpc_id: vpc-12345678 peer_owner_id: 123456789102 state: present + tags: + Name: Peering conenction for VPC 21 to VPC 22 + CostCode: CC1234 + Project: phoenix register: vpc_peer - name: Reject a cross account VPC peering Connection @@ -161,6 +189,28 @@ HAS_BOTO3 = False +def tags_changed(pcx_id, client, module): + changed = False + tags = dict() + if module.params.get('tags'): + tags = module.params.get('tags') + pcx = find_pcx_by_id(pcx_id, client, module) + if pcx['VpcPeeringConnections']: + pcx_values = [t.values() for t in pcx['VpcPeeringConnections'][0]['Tags']] + pcx_tags = [item for sublist in pcx_values for item in sublist] + tag_values = [[key, str(value)] for key, value in tags.iteritems()] + tags = [item for sublist in tag_values for item in sublist] + if sorted(pcx_tags) == sorted(tags): + changed = False + return changed + else: + delete_tags(pcx_id, client, module) + create_tags(pcx_id, client, module) + changed = True + return changed + return changed + + def describe_peering_connections(params, client): result = client.describe_vpc_peering_connections(Filters=[ {'Name': 'requester-vpc-info.vpc-id', 'Values': [params['VpcId']]}, @@ -192,12 +242,18 @@ def create_peer_connection(client, module): params['DryRun'] = module.check_mode peering_conns = describe_peering_connections(params, client) for peering_conn in peering_conns['VpcPeeringConnections']: + pcx_id = peering_conn['VpcPeeringConnectionId'] + if tags_changed(pcx_id, client, module): + changed = True if is_active(peering_conn): return (changed, peering_conn['VpcPeeringConnectionId']) if is_pending(peering_conn): return (changed, peering_conn['VpcPeeringConnectionId']) try: peering_conn = client.create_vpc_peering_connection(**params) + pcx_id = peering_conn['VpcPeeringConnection']['VpcPeeringConnectionId'] + if module.params.get('tags'): + create_tags(pcx_id, client, module) changed = True return (changed, peering_conn['VpcPeeringConnection']['VpcPeeringConnectionId']) except botocore.exceptions.ClientError as e: @@ -227,10 +283,39 @@ def accept_reject_delete(state, client, module): changed = True except botocore.exceptions.ClientError as e: module.fail_json(msg=str(e)) - return changed, params['VpcPeeringConnectionId'] +def load_tags(module): + tags = [] + if module.params.get('tags'): + for name, value in module.params.get('tags').iteritems(): + tags.append({'Key': name, 'Value': str(value)}) + return tags + + +def create_tags(pcx_id, client, module): + try: + delete_tags(pcx_id, client, module) + client.create_tags(Resources=[pcx_id], Tags=load_tags(module)) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + + +def delete_tags(pcx_id, client, module): + try: + client.delete_tags(Resources=[pcx_id]) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + + +def find_pcx_by_id(pcx_id, client, module): + try: + return client.describe_vpc_peering_connections(VpcPeeringConnectionIds=[pcx_id]) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + + def main(): argument_spec = ec2_argument_spec() argument_spec.update(dict( @@ -238,6 +323,7 @@ def main(): peer_vpc_id=dict(), peering_id=dict(), peer_owner_id=dict(), + tags=dict(required=False, type='dict'), profile=dict(), state=dict(default='present', choices=['present', 'absent', 'accept', 'reject']) ) From 9a303f1628bb82049457683716044a8f35b85957 Mon Sep 17 00:00:00 2001 From: Mike Mochan Date: Sun, 8 May 2016 14:07:21 +1000 Subject: [PATCH 1592/2522] Updates to version and requirements --- cloud/amazon/ec2_vpc_peer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/ec2_vpc_peer.py b/cloud/amazon/ec2_vpc_peer.py index a534b7973b7..b8160d4676e 100644 --- a/cloud/amazon/ec2_vpc_peer.py +++ b/cloud/amazon/ec2_vpc_peer.py @@ -19,7 +19,7 @@ description: - Read the AWS documentation for VPC Peering Connections U(http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/vpc-peering.html) -version_added: "2.1" +version_added: "2.2" options: vpc_id: description: @@ -45,6 +45,7 @@ choices: ['present', 'absent', 'accept', 'reject'] author: Mike Mochan(@mmochan) extends_documentation_fragment: aws +requirements: [ botocore, boto3, json ] ''' EXAMPLES = ''' @@ -331,7 +332,7 @@ def main(): module = AnsibleModule(argument_spec=argument_spec) if not HAS_BOTO3: - module.fail_json(msg='json and boto3 is required.') + module.fail_json(msg='json, botocore and boto3 are required.') state = module.params.get('state').lower() try: region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) From cab7437e32142b643c94f1206f58e558875d6b1f Mon Sep 17 00:00:00 2001 From: Mike Mochan Date: Sun, 8 May 2016 14:52:31 +1000 Subject: [PATCH 1593/2522] added tagging for the remote account --- cloud/amazon/ec2_vpc_peer.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_vpc_peer.py b/cloud/amazon/ec2_vpc_peer.py index b8160d4676e..b21c2b708b9 100644 --- a/cloud/amazon/ec2_vpc_peer.py +++ b/cloud/amazon/ec2_vpc_peer.py @@ -150,7 +150,10 @@ peering_id: "{{ vpc_peer.peering_id }}" profile: bot03_profile_for_cross_account state: accept - + tags: + Name: Peering conenction for VPC 21 to VPC 22 + CostCode: CC1234 + Project: phoenix # Complete example to create and reject a cross account peering connection. - name: Create cross account VPC peering Connection @@ -281,6 +284,8 @@ def accept_reject_delete(state, client, module): if state == 'absent' or peer_status(client, module) != 'active': try: invocations[state](**params) + if module.params.get('tags'): + create_tags(params['VpcPeeringConnectionId'], client, module) changed = True except botocore.exceptions.ClientError as e: module.fail_json(msg=str(e)) From 698d2a55c50d16724f4e00c0b939b264814c40a4 Mon Sep 17 00:00:00 2001 From: Mike Mochan Date: Mon, 9 May 2016 10:28:05 +1000 Subject: [PATCH 1594/2522] update remote tags if changed --- cloud/amazon/ec2_vpc_peer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cloud/amazon/ec2_vpc_peer.py b/cloud/amazon/ec2_vpc_peer.py index b21c2b708b9..4ea313c67ea 100644 --- a/cloud/amazon/ec2_vpc_peer.py +++ b/cloud/amazon/ec2_vpc_peer.py @@ -289,6 +289,8 @@ def accept_reject_delete(state, client, module): changed = True except botocore.exceptions.ClientError as e: module.fail_json(msg=str(e)) + if tags_changed(params['VpcPeeringConnectionId'], client, module): + changed = True return changed, params['VpcPeeringConnectionId'] From 5b2bf6f08a7fa3492d2e11aa9ec507661ab3ccc6 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 25 May 2016 11:48:16 -0700 Subject: [PATCH 1595/2522] Fix python3 syntax compilation --- cloud/amazon/ec2_vpc_peer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/ec2_vpc_peer.py b/cloud/amazon/ec2_vpc_peer.py index 4ea313c67ea..3eb6582d0f7 100644 --- a/cloud/amazon/ec2_vpc_peer.py +++ b/cloud/amazon/ec2_vpc_peer.py @@ -343,8 +343,8 @@ def main(): state = module.params.get('state').lower() try: region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - client = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except botocore.exceptions.NoCredentialsError, e: + client = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_kwargs) + except botocore.exceptions.NoCredentialsError as e: module.fail_json(msg="Can't authorize connection - "+str(e)) if state == 'present': From 76cbe306451d0ecc95c6ac1787c03cd4cf22b0d8 Mon Sep 17 00:00:00 2001 From: Benjamin Curtis Date: Tue, 30 Sep 2014 11:39:36 -0700 Subject: [PATCH 1596/2522] Add honeybadger_deployment module --- monitoring/honeybadger_deployment.py | 139 +++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 monitoring/honeybadger_deployment.py diff --git a/monitoring/honeybadger_deployment.py b/monitoring/honeybadger_deployment.py new file mode 100644 index 00000000000..457daa981f4 --- /dev/null +++ b/monitoring/honeybadger_deployment.py @@ -0,0 +1,139 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2014 Benjamin Curtis +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: honeybadger_deployment +author: "Benjamin Curtis (@stympy)" +version_added: "2.1" +short_description: Notify Honeybadger.io about app deployments +description: + - Notify Honeybadger.io about app deployments (see http://docs.honeybadger.io/article/188-deployment-tracking) +options: + token: + description: + - API token. + required: true + environment: + description: + - The environment name, typically 'production', 'staging', etc. + required: true + user: + description: + - The username of the person doing the deployment + required: false + default: None + repo: + description: + - URL of the project repository + required: false + default: None + revision: + description: + - A hash, number, tag, or other identifier showing what revision was deployed + required: false + default: None + url: + description: + - Optional URL to submit the notification to. + required: false + default: "https://api.honeybadger.io/v1/deploys" + validate_certs: + description: + - If C(no), SSL certificates for the target url will not be validated. This should only be used + on personally controlled sites using self-signed certificates. + required: false + default: 'yes' + choices: ['yes', 'no'] + +requirements: [] +''' + +EXAMPLES = ''' +- honeybadger_deployment: token=AAAAAA + environment='staging' + user='ansible' + revision=b6826b8 + repo=git@github.com:user/repo.git +''' + +RETURN = '''# ''' + +import urllib + +# =========================================== +# Module execution. +# + +def main(): + + module = AnsibleModule( + argument_spec=dict( + token=dict(required=True, no_log=True), + environment=dict(required=True), + user=dict(required=False), + repo=dict(required=False), + revision=dict(required=False), + url=dict(required=False, default='https://api.honeybadger.io/v1/deploys'), + validate_certs=dict(default='yes', type='bool'), + ), + supports_check_mode=True + ) + + params = {} + + if module.params["environment"]: + params["deploy[environment]"] = module.params["environment"] + + if module.params["user"]: + params["deploy[local_username]"] = module.params["user"] + + if module.params["repo"]: + params["deploy[repository]"] = module.params["repo"] + + if module.params["revision"]: + params["deploy[revision]"] = module.params["revision"] + + params["api_key"] = module.params["token"] + + url = module.params.get('url') + + # If we're in check mode, just exit pretending like we succeeded + if module.check_mode: + module.exit_json(changed=True) + + try: + data = urllib.urlencode(params) + response, info = fetch_url(module, url, data=data) + except Exception, e: + module.fail_json(msg='Unable to notify Honeybadger: %s' % e) + else: + if info['status'] == 200: + module.exit_json(changed=True) + else: + module.fail_json(msg="HTTP result code: %d connecting to %s" % (info['status'], url)) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * + +if __name__ == '__main__': + main() + From 4b4557eb97bcabe5d920c44eb4f60a2deb8c5d67 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 25 May 2016 11:56:51 -0700 Subject: [PATCH 1597/2522] Fix exception catching for python3 --- monitoring/honeybadger_deployment.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/monitoring/honeybadger_deployment.py b/monitoring/honeybadger_deployment.py index 457daa981f4..3a6d2df7c8c 100644 --- a/monitoring/honeybadger_deployment.py +++ b/monitoring/honeybadger_deployment.py @@ -22,7 +22,7 @@ --- module: honeybadger_deployment author: "Benjamin Curtis (@stympy)" -version_added: "2.1" +version_added: "2.2" short_description: Notify Honeybadger.io about app deployments description: - Notify Honeybadger.io about app deployments (see http://docs.honeybadger.io/article/188-deployment-tracking) @@ -78,6 +78,11 @@ import urllib +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import * + # =========================================== # Module execution. # @@ -122,7 +127,8 @@ def main(): try: data = urllib.urlencode(params) response, info = fetch_url(module, url, data=data) - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg='Unable to notify Honeybadger: %s' % e) else: if info['status'] == 200: @@ -130,10 +136,6 @@ def main(): else: module.fail_json(msg="HTTP result code: %d connecting to %s" % (info['status'], url)) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * - if __name__ == '__main__': main() From b827b7398c791325f54199edb7bd6615c2e14eba Mon Sep 17 00:00:00 2001 From: Firat Arig Date: Thu, 8 Jan 2015 13:58:31 +0200 Subject: [PATCH 1598/2522] Working mssql db controller Using pymssql string interpolation Cursor does string interpolation at script execution Handled edge cases in import and delete updated ansible version number --- database/mssql/__init__.py | 0 database/mssql/mssql_db.py | 211 +++++++++++++++++++++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 database/mssql/__init__.py create mode 100644 database/mssql/mssql_db.py diff --git a/database/mssql/__init__.py b/database/mssql/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/database/mssql/mssql_db.py b/database/mssql/mssql_db.py new file mode 100644 index 00000000000..1aac49caaa3 --- /dev/null +++ b/database/mssql/mssql_db.py @@ -0,0 +1,211 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Ansible module to manage mssql databases +# (c) 2014, Vedit Firat Arig +# Outline and parts are reused from Mark Theunissen's mysql_db module +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: mssql_db +short_description: Add or remove MSSQL databases from a remote host. +description: + - Add or remove MSSQL databases from a remote host. +version_added: "2.1" +options: + name: + description: + - name of the database to add or remove + 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 + login_port: + description: + - Port of the MSSQL server. Requires login_host be defined as other then localhost if login_port is used + required: false + default: 1433 + state: + description: + - The database state + required: false + default: present + choices: [ "present", "absent", "import" ] + target: + description: + - Location, on the remote host, of the dump file to read from or write to. Uncompressed SQL + files (C(.sql)) files are supported. + required: false +notes: + - Requires the pymssql Python package on the remote host. For Ubuntu, this + is as easy as pip install pymssql (See M(pip).) +requirements: [ pymssql ] +author: Vedit Firat Arig +''' + +EXAMPLES = ''' +# Create a new database with name 'jackdata' +- mssql_db: name=jackdata state=present +# Copy database dump file to remote host and restore it to database 'my_db' +- copy: src=dump.sql dest=/tmp +- mssql_db: name=my_db state=import target=/tmp/dump.sql +''' + +import os +try: + import pymssql +except ImportError: + mssql_found = False +else: + mssql_found = True + + +def db_exists(conn, cursor, db): + cursor.execute("SELECT name FROM master.sys.databases WHERE name = N'%s'", db) + conn.commit() + return bool(cursor.rowcount) + + +def db_create(conn, cursor, db): + conn.autocommit(True) + cursor.execute("CREATE DATABASE %s", db) + conn.autocommit(False) + return db_exists(conn, cursor, db) + + +def db_delete(conn, cursor, db): + conn.autocommit(True) + try: + single_user = "alter database %s set single_user with rollback immediate" % db + cursor.execute(single_user) + except: + pass + cursor.execute("DROP DATABASE %s", db) + conn.autocommit(False) + return not db_exists(conn, cursor, db) + + +def db_import(conn, cursor, module, db, target): + if os.path.isfile(target): + with open(target, 'r') as backup: + sqlQuery = "USE %s\n" + for line in backup: + if line is None: + break + elif line.startswith('GO'): + cursor.execute(sqlQuery, db) + sqlQuery = "USE %s\n" + else: + sqlQuery += line + cursor.execute(sqlQuery, db) + conn.commit() + return 0, "import successful", "" + else: + return 1, "cannot find target file", "cannot find target file" + + +def main(): + module = AnsibleModule( + argument_spec=dict( + name=dict(required=True, aliases=['db']), + login_user=dict(required=True), + login_password=dict(required=True), + login_host=dict(required=True), + login_port=dict(default="1433"), + target=dict(default=None), + state=dict( + default='present', choices=['present', 'absent', 'import']) + ) + ) + + if not mssql_found: + module.fail_json(msg="pymssql python module is required") + + db = module.params['name'] + state = module.params['state'] + target = module.params["target"] + + login_user = module.params['login_user'] + login_password = module.params['login_password'] + login_host = module.params['login_host'] + login_port = module.params['login_port'] + login_querystring = "%s:%s" % (login_host, login_port) + + if 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: + conn = pymssql.connect(user=login_user, password=login_password, host=login_querystring, database='master') + cursor = conn.cursor() + except Exception, e: + if "Unknown database" in str(e): + errno, errstr = e.args + module.fail_json(msg="ERROR: %s %s" % (errno, errstr)) + else: + module.fail_json(msg="unable to connect, check login_user and login_password are correct, or alternatively check ~/.my.cnf contains credentials") + + changed = False + if db_exists(conn, cursor, db): + if state == "absent": + try: + changed = db_delete(conn, cursor, db) + except Exception, e: + module.fail_json(msg="error deleting database: " + str(e)) + elif state == "import": + rc, stdout, stderr = db_import(conn, cursor, module, db, target) + if rc != 0: + module.fail_json(msg="%s" % stderr) + else: + module.exit_json(changed=True, db=db, msg=stdout) + else: + if state == "present": + try: + changed = db_create(conn, cursor, db) + except Exception, e: + module.fail_json(msg="error creating database: " + str(e)) + elif state == "import": + try: + changed = db_create(conn, cursor, db) + except Exception, e: + module.fail_json(msg="error creating database: " + str(e)) + rc, stdout, stderr = db_import(conn, cursor, module, db, target) + if rc != 0: + module.fail_json(msg="%s" % stderr) + else: + module.exit_json(changed=True, db=db, msg=stdout) + + module.exit_json(changed=changed, db=db) + +# import module snippets +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() + From e7e36209832bc6de3eb4854045da5961073c839a Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Fri, 19 Feb 2016 09:14:52 +0100 Subject: [PATCH 1599/2522] allow empty user for kerberos ticket usage fix syntax problems: * it is possible that sql injection is done, therefore the [DBNAME] syntax is used. * it is not possible to use default escape on cursor.execute for DBNAME, since it will insert single quotes around the name and this will cause syntax problems / single quotes within the actual DBNAME implement autocommit setting, since some content can not be imported within transaction fix for automatic tests fix problems with named instances, corrected error message regarding configuration file remove unused placeholder --- database/mssql/mssql_db.py | 65 ++++++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/database/mssql/mssql_db.py b/database/mssql/mssql_db.py index 1aac49caaa3..413d4627c2e 100644 --- a/database/mssql/mssql_db.py +++ b/database/mssql/mssql_db.py @@ -26,7 +26,7 @@ short_description: Add or remove MSSQL databases from a remote host. description: - Add or remove MSSQL databases from a remote host. -version_added: "2.1" +version_added: "2.2" options: name: description: @@ -64,6 +64,12 @@ - Location, on the remote host, of the dump file to read from or write to. Uncompressed SQL files (C(.sql)) files are supported. required: false + autocommit: + description: + - Automatically commit the change only if the import succeed. Sometimes it is necessary to use autocommit=true, since some content can't be changed within a transaction. + required: false + default: false + choices: [ "false", "true" ] notes: - Requires the pymssql Python package on the remote host. For Ubuntu, this is as easy as pip install pymssql (See M(pip).) @@ -79,6 +85,10 @@ - mssql_db: name=my_db state=import target=/tmp/dump.sql ''' +RETURN = ''' +# +''' + import os try: import pymssql @@ -89,44 +99,41 @@ def db_exists(conn, cursor, db): - cursor.execute("SELECT name FROM master.sys.databases WHERE name = N'%s'", db) + cursor.execute("SELECT name FROM master.sys.databases WHERE name = %s", db) conn.commit() return bool(cursor.rowcount) def db_create(conn, cursor, db): - conn.autocommit(True) - cursor.execute("CREATE DATABASE %s", db) - conn.autocommit(False) + cursor.execute("CREATE DATABASE [%s]" % db) return db_exists(conn, cursor, db) def db_delete(conn, cursor, db): - conn.autocommit(True) try: - single_user = "alter database %s set single_user with rollback immediate" % db - cursor.execute(single_user) + cursor.execute("ALTER DATABASE [%s] SET single_user WITH ROLLBACK IMMEDIATE" % db) except: pass - cursor.execute("DROP DATABASE %s", db) - conn.autocommit(False) + cursor.execute("DROP DATABASE [%s]" % db) return not db_exists(conn, cursor, db) - def db_import(conn, cursor, module, db, target): if os.path.isfile(target): - with open(target, 'r') as backup: - sqlQuery = "USE %s\n" + backup = open(target, 'r') + try: + sqlQuery = "USE [%s]\n" % db for line in backup: if line is None: break elif line.startswith('GO'): - cursor.execute(sqlQuery, db) - sqlQuery = "USE %s\n" + cursor.execute(sqlQuery) + sqlQuery = "USE [%s]\n" % db else: sqlQuery += line - cursor.execute(sqlQuery, db) + cursor.execute(sqlQuery) conn.commit() + finally: + backup.close() return 0, "import successful", "" else: return 1, "cannot find target file", "cannot find target file" @@ -136,11 +143,12 @@ def main(): module = AnsibleModule( argument_spec=dict( name=dict(required=True, aliases=['db']), - login_user=dict(required=True), - login_password=dict(required=True), + login_user=dict(default=''), + login_password=dict(default=''), login_host=dict(required=True), - login_port=dict(default="1433"), + login_port=dict(default='1433'), target=dict(default=None), + autocommit=dict(type='bool', default=False), state=dict( default='present', choices=['present', 'absent', 'import']) ) @@ -151,16 +159,20 @@ def main(): db = module.params['name'] state = module.params['state'] + autocommit = module.params['autocommit'] target = module.params["target"] login_user = module.params['login_user'] login_password = module.params['login_password'] login_host = module.params['login_host'] login_port = module.params['login_port'] - login_querystring = "%s:%s" % (login_host, login_port) + + login_querystring = login_host + if login_port != "1433": + login_querystring = "%s:%s" % (login_host, login_port) - if 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") + if login_user != "" and login_password == "": + module.fail_json(msg="when supplying login_user arguments login_password must be provided") try: conn = pymssql.connect(user=login_user, password=login_password, host=login_querystring, database='master') @@ -170,9 +182,11 @@ def main(): errno, errstr = e.args module.fail_json(msg="ERROR: %s %s" % (errno, errstr)) else: - module.fail_json(msg="unable to connect, check login_user and login_password are correct, or alternatively check ~/.my.cnf contains credentials") + module.fail_json(msg="unable to connect, check login_user and login_password are correct, or alternatively check your @sysconfdir@/freetds.conf / ${HOME}/.freetds.conf") + conn.autocommit(True) changed = False + if db_exists(conn, cursor, db): if state == "absent": try: @@ -180,7 +194,9 @@ def main(): except Exception, e: module.fail_json(msg="error deleting database: " + str(e)) elif state == "import": + conn.autocommit(autocommit) rc, stdout, stderr = db_import(conn, cursor, module, db, target) + if rc != 0: module.fail_json(msg="%s" % stderr) else: @@ -196,7 +212,10 @@ def main(): changed = db_create(conn, cursor, db) except Exception, e: module.fail_json(msg="error creating database: " + str(e)) + + conn.autocommit(autocommit) rc, stdout, stderr = db_import(conn, cursor, module, db, target) + if rc != 0: module.fail_json(msg="%s" % stderr) else: From 0e4a023a7e883743e92ddfcd097b3a60d86b1608 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 25 May 2016 12:09:49 -0700 Subject: [PATCH 1600/2522] The pymssql library requires python 2.7 or greater so port the syntax of this file to use python3-style exception handling --- database/mssql/mssql_db.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/database/mssql/mssql_db.py b/database/mssql/mssql_db.py index 413d4627c2e..45642c579f7 100644 --- a/database/mssql/mssql_db.py +++ b/database/mssql/mssql_db.py @@ -73,7 +73,9 @@ notes: - Requires the pymssql Python package on the remote host. For Ubuntu, this is as easy as pip install pymssql (See M(pip).) -requirements: [ pymssql ] +requirements: + - python >= 2.7 + - pymssql author: Vedit Firat Arig ''' @@ -177,7 +179,7 @@ def main(): try: conn = pymssql.connect(user=login_user, password=login_password, host=login_querystring, database='master') cursor = conn.cursor() - except Exception, e: + except Exception as e: if "Unknown database" in str(e): errno, errstr = e.args module.fail_json(msg="ERROR: %s %s" % (errno, errstr)) @@ -191,7 +193,7 @@ def main(): if state == "absent": try: changed = db_delete(conn, cursor, db) - except Exception, e: + except Exception as e: module.fail_json(msg="error deleting database: " + str(e)) elif state == "import": conn.autocommit(autocommit) @@ -205,12 +207,12 @@ def main(): if state == "present": try: changed = db_create(conn, cursor, db) - except Exception, e: + except Exception as e: module.fail_json(msg="error creating database: " + str(e)) elif state == "import": try: changed = db_create(conn, cursor, db) - except Exception, e: + except Exception as e: module.fail_json(msg="error creating database: " + str(e)) conn.autocommit(autocommit) From ca61b2dd9d172644a5af7d9602cfeeab6e1b85ab Mon Sep 17 00:00:00 2001 From: Paul Durivage Date: Wed, 25 May 2016 15:15:00 -0500 Subject: [PATCH 1601/2522] fix command list to extend, not append --- messaging/rabbitmq_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messaging/rabbitmq_user.py b/messaging/rabbitmq_user.py index 2e902eaa696..103650e2c94 100644 --- a/messaging/rabbitmq_user.py +++ b/messaging/rabbitmq_user.py @@ -146,7 +146,7 @@ def _exec(self, args, run_in_check_mode=False): if not self.module.check_mode or (self.module.check_mode and run_in_check_mode): cmd = [self._rabbitmqctl, '-q'] if self.node is not None: - cmd.append(['-n', self.node]) + cmd.extend(['-n', self.node]) rc, out, err = self.module.run_command(cmd + args, check_rc=True) return out.splitlines() return list() From 63134a85d2adc763d8c420b5c314d98efaecb82a Mon Sep 17 00:00:00 2001 From: Andrew Miller Date: Thu, 26 May 2016 16:21:12 +0900 Subject: [PATCH 1602/2522] Cpanm module could not use less than Python 2.6 Removed str.format() dependency to allow cpanm module to work on nodes with versions of Python less than 2.6. --- packaging/language/cpanm.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packaging/language/cpanm.py b/packaging/language/cpanm.py index 769ea5f02fa..790a4939156 100644 --- a/packaging/language/cpanm.py +++ b/packaging/language/cpanm.py @@ -135,27 +135,27 @@ def _build_cmd_line(name, from_path, notest, locallib, mirror, mirror_only, inst # this code should use "%s" like everything else and just return early but not fixing all of it now. # don't copy stuff like this if from_path: - cmd = "{cpanm} {path}".format(cpanm=cpanm, path=from_path) + cmd = cpanm + " " + from_path else: - cmd = "{cpanm} {name}".format(cpanm=cpanm, name=name) + cmd = cpanm + " " + name if notest is True: - cmd = "{cmd} -n".format(cmd=cmd) + cmd = cmd + " -n" if locallib is not None: - cmd = "{cmd} -l {locallib}".format(cmd=cmd, locallib=locallib) + cmd = cmd + " -l " + locallib if mirror is not None: - cmd = "{cmd} --mirror {mirror}".format(cmd=cmd, mirror=mirror) + cmd = cmd + " --mirror " + mirror if mirror_only is True: - cmd = "{cmd} --mirror-only".format(cmd=cmd) + cmd = cmd + " --mirror-only" if installdeps is True: - cmd = "{cmd} --installdeps".format(cmd=cmd) + cmd = cmd + " --installdeps" if use_sudo is True: - cmd = "{cmd} --sudo".format(cmd=cmd) + cmd = cmd + " --sudo" return cmd From 98039323e21487e7e4ac9f6f89454de75319d7da Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Thu, 26 May 2016 10:06:36 -0500 Subject: [PATCH 1603/2522] Exclude mssql_db.py from py24 syntax checks --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 62c8b7174aa..9b54c2ddbe0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -140,7 +140,7 @@ install: - pip install git+https://github.com/ansible/ansible.git@devel#egg=ansible - pip install git+https://github.com/sivel/ansible-testing.git#egg=ansible_testing script: - - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py|database/influxdb/influxdb.*\.py' . + - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py|database/influxdb/influxdb.*\.py|database/mssql/mssql_db\.py' . - python2.6 -m compileall -fq . - python2.7 -m compileall -fq . - python3.4 -m compileall -fq . -x $(echo "$PY3_EXCLUDE_LIST"| tr ' ' '|') From 3ca06bf1c809b3cf21e690e28252ceb8b778079d Mon Sep 17 00:00:00 2001 From: Elena Washington Date: Thu, 26 May 2016 12:51:31 -0400 Subject: [PATCH 1604/2522] iptables: option to configure Source NAT (#2292) * Clean up trailing whitespace * Add `--to-source` option to allow Source NAT (fix for #2291) --- system/iptables.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/system/iptables.py b/system/iptables.py index d874161cdfa..f0f458a5d60 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -226,6 +226,13 @@ this, the destination address is never altered." required: false default: null + to_source: + version_added: "2.2" + description: + - "This specifies a source address to use with SNAT: without + this, the source address is never altered." + required: false + default: null set_dscp_mark: version_added: "2.1" description: @@ -277,8 +284,8 @@ icmp_type: version_added: "2.2" description: - - "This allows specification of the ICMP type, which can be a numeric ICMP type, - type/code pair, or one of the ICMP type names shown by the command + - "This allows specification of the ICMP type, which can be a numeric ICMP type, + type/code pair, or one of the ICMP type names shown by the command 'iptables -p icmp -h'" required: false ''' @@ -336,6 +343,7 @@ def construct_rule(params): append_param(rule, params['match'], '-m', True) append_param(rule, params['jump'], '-j', False) append_param(rule, params['to_destination'], '--to-destination', False) + append_param(rule, params['to_source'], '--to-source', False) append_param(rule, params['goto'], '-g', False) append_param(rule, params['in_interface'], '-i', False) append_param(rule, params['out_interface'], '-o', False) @@ -401,6 +409,7 @@ def main(): chain=dict(required=True, default=None, type='str'), protocol=dict(required=False, default=None, type='str'), source=dict(required=False, default=None, type='str'), + to_source=dict(required=False, default=None, type='str'), destination=dict(required=False, default=None, type='str'), to_destination=dict(required=False, default=None, type='str'), match=dict(required=False, default=[], type='list'), From cb4173957c0d0a590a870afa7329d01f6c648e1f Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Thu, 26 May 2016 19:54:30 -0400 Subject: [PATCH 1605/2522] Initial commit of extras/archive module. This manages compressed files or archives of many compressed files. You can maintain or update .gz, .bz2 compressed files, .zip archives, or tarballs compressed with gzip or bzip2. Possible use cases: * Back up user home directories * Ensure large text files are always compressed * Archive trees for distribution --- files/archive.py | 285 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 files/archive.py diff --git a/files/archive.py b/files/archive.py new file mode 100644 index 00000000000..540840b1dac --- /dev/null +++ b/files/archive.py @@ -0,0 +1,285 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +DOCUMENTATION = ''' +--- +module: archive +version_added: 2.2 +short_description: Creates a compressed archive of one or more files or trees. +extends_documentation_fragment: files +description: + - The M(archive) module packs an archive. It is the opposite of the unarchive module. By default, it assumes the compression source exists on the target. It will not copy the source file from the local system to the target before archiving - set copy=yes to pack an archive which does not already exist on the target. The source files are deleted after archiving. +options: + path: + description: + - Remote absolute path, glob, or list of paths or globs for the file or files to archive or compress. + required: false + default: null + compression: + description: + - "The type of compression to use. Can be 'gz', 'bz2', or 'zip'. + choices: [ 'gz', 'bz2', 'zip' ] + creates: + description: + - The file name of the destination archive. When it already exists, this step will B(not) be run. This is required when 'path' refers to multiple files by either specifying a glob, a directory or multiple paths in a list. + required: false + default: null +author: "Ben Doherty (@bendoh)" +notes: + - requires tarfile, zipfile, gzip, and bzip2 packages on target host + - can product I(gzip), I(bzip2) and I(zip) compressed files or archives + - removes source files by default +''' + +EXAMPLES = ''' +# Compress directory /path/to/foo/ into /path/to/foo.tgz +- archive: path=/path/to/foo creates=/path/to/foo.tgz + +# Compress regular file /path/to/foo into /path/to/foo.gz +- archive: path=/path/to/foo + +# Create a zip archive of /path/to/foo +- archive: path=/path/to/foo compression=zip + +# Create a bz2 archive of multiple files, rooted at /path +- archive: + path: + - /path/to/foo + - /path/wong/foo + creates: /path/file.tar.bz2 + compression: bz2 +''' + +import stat +import os +import errno +import glob +import shutil +import gzip +import bz2 +import zipfile +import tarfile + +def main(): + module = AnsibleModule( + argument_spec = dict( + path = dict(required=True), + compression = dict(choices=['gz', 'bz2', 'zip'], default='gz', required=False), + creates = dict(required=False), + remove = dict(required=False, default=True, type='bool'), + ), + add_file_common_args=True, + supports_check_mode=True, + ) + + params = module.params + paths = params['path'] + creates = params['creates'] + remove = params['remove'] + expanded_paths = [] + compression = params['compression'] + globby = False + changed = False + state = 'absent' + + # Simple or archive file compression (inapplicable with 'zip') + archive = False + successes = [] + + if isinstance(paths, basestring): + paths = [paths] + + for i, path in enumerate(paths): + path = os.path.expanduser(params['path']) + + # Detect glob-like characters + if any((c in set('*?')) for c in path): + expanded_paths = expanded_paths + glob.glob(path) + else: + expanded_paths.append(path) + + if len(expanded_paths) == 0: + module.fail_json(path, msg='Error, no source paths were found') + + # If we actually matched multiple files or TRIED to, then + # treat this as a multi-file archive + archive = globby or len(expanded_paths) > 1 or any(os.path.isdir(path) for path in expanded_paths) + + # Default created file name (for single-file archives) to + # . + if not archive and not creates: + creates = '%s.%s' % (expanded_paths[0], compression) + + # Force archives to specify 'creates' + if archive and not creates: + module.fail_json(creates=creates, path=', '.join(paths), msg='Error, must specify "creates" when archiving multiple files or trees') + + archive_paths = [] + missing = [] + arcroot = '' + + for path in expanded_paths: + # Use the longest common directory name among all the files + # as the archive root path + if arcroot == '': + arcroot = os.path.dirname(path) + else: + for i in xrange(len(arcroot)): + if path[i] != arcroot[i]: + break + + if i < len(arcroot): + arcroot = os.path.dirname(arcroot[0:i+1]) + + if path == creates: + # Don't allow the archive to specify itself! this is an error. + module.fail_json(path=', '.join(paths), msg='Error, created archive would be included in archive') + + if os.path.lexists(path): + archive_paths.append(path) + else: + missing.append(path) + + # No source files were found but the named archive exists: are we 'compress' or 'archive' now? + if len(missing) == len(expanded_paths) and creates and os.path.exists(creates): + # Just check the filename to know if it's an archive or simple compressed file + if re.search(r'(\.tar\.gz|\.tgz|.tbz2|\.tar\.bz2|\.zip)$', os.path.basename(creates), re.IGNORECASE): + state = 'archive' + else: + state = 'compress' + + # Multiple files, or globbiness + elif archive: + if len(archive_paths) == 0: + # No source files were found, but the archive is there. + if os.path.lexists(creates): + state = 'archive' + elif len(missing) > 0: + # SOME source files were found, but not all of them + state = 'incomplete' + + archive = None + size = 0 + errors = [] + + if os.path.lexists(creates): + size = os.path.getsize(creates) + + if state != 'archive': + try: + if compression == 'gz' or compression == 'bz2': + archive = tarfile.open(creates, 'w|' + compression) + + for path in archive_paths: + archive.add(path, path[len(arcroot):]) + successes.append(path) + + elif compression == 'zip': + archive = zipfile.ZipFile(creates, 'wb') + + for path in archive_paths: + archive.write(path, path[len(arcroot):]) + successes.append(path) + + except OSError: + e = get_exception() + module.fail_json(msg='Error when writing zip archive at %s: %s' % (creates, str(e))) + + if archive: + archive.close() + + if state != 'archive' and remove: + for path in successes: + try: + if os.path.isdir(path): + shutil.rmtree(path) + else: + os.remove(path) + except OSError: + e = get_exception() + errors.append(path) + + if len(errors) > 0: + module.fail_json(creates=creates, msg='Error deleting some source files: ' + str(e), files=errors) + + # Rudimentary check: If size changed then file changed. Not perfect, but easy. + if os.path.getsize(creates) != size: + changed = True + + if len(successes) and state != 'incomplete': + state = 'archive' + + # Simple, single-file compression + else: + path = expanded_paths[0] + + # No source or compressed file + if not (os.path.exists(path) or os.path.lexists(creates)): + state = 'absent' + + # if it already exists and the source file isn't there, consider this done + elif not os.path.lexists(path) and os.path.lexists(creates): + state = 'compress' + + else: + if module.check_mode: + if not os.path.exists(creates): + changed = True + else: + size = 0 + f_in = f_out = archive = None + + if os.path.lexists(creates): + size = os.path.getsize(creates) + + try: + if compression == 'zip': + archive = zipfile.ZipFile(creates, 'wb') + archive.write(path, path[len(arcroot):]) + archive.close() + state = 'archive' # because all zip files are archives + + else: + f_in = open(path, 'rb') + + if compression == 'gz': + f_out = gzip.open(creates, 'wb') + elif compression == 'bz2': + f_out = bz2.BZ2File(creates, 'wb') + else: + raise OSError("Invalid compression") + + shutil.copyfileobj(f_in, f_out) + + except OSError: + e = get_exception() + + module.fail_json(path=path, creates=creates, msg='Unable to write to compressed file: %s' % str(e)) + + if archive: + archive.close() + if f_in: + f_in.close() + if f_out: + f_out.close() + + # Rudimentary check: If size changed then file changed. Not perfect, but easy. + if os.path.getsize(creates) != size: + changed = True + + state = 'compress' + + if remove: + try: + os.remove(path) + + except OSError: + e = get_exception() + module.fail_json(path=path, msg='Unable to remove source file: %s' % str(e)) + + module.exit_json(archived=successes, creates=creates, changed=changed, state=state, arcroot=arcroot, missing=missing, expanded_paths=expanded_paths) + +# import module snippets +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From 431d8c9a8f14e783e8d99c9888aa71ae9e326125 Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Thu, 26 May 2016 20:36:14 -0400 Subject: [PATCH 1606/2522] Drop extra double-quote from documentation --- files/archive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/archive.py b/files/archive.py index 540840b1dac..f6791ff53d5 100644 --- a/files/archive.py +++ b/files/archive.py @@ -17,7 +17,7 @@ default: null compression: description: - - "The type of compression to use. Can be 'gz', 'bz2', or 'zip'. + - The type of compression to use. Can be 'gz', 'bz2', or 'zip'. choices: [ 'gz', 'bz2', 'zip' ] creates: description: From e9b85326a653c9c9e325a8fe1b1c7714c05a29ad Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Thu, 26 May 2016 21:33:27 -0400 Subject: [PATCH 1607/2522] Fix write mode for ZipFiles ('wb' is invalid!) --- files/archive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/archive.py b/files/archive.py index f6791ff53d5..76dcb9cf084 100644 --- a/files/archive.py +++ b/files/archive.py @@ -175,7 +175,7 @@ def main(): successes.append(path) elif compression == 'zip': - archive = zipfile.ZipFile(creates, 'wb') + archive = zipfile.ZipFile(creates, 'w') for path in archive_paths: archive.write(path, path[len(arcroot):]) From 9cde150bd12b771ba607b9e62918c57ca724ef88 Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Thu, 26 May 2016 21:38:31 -0400 Subject: [PATCH 1608/2522] Add RETURN documentation --- files/archive.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/files/archive.py b/files/archive.py index 76dcb9cf084..e64a9197bfe 100644 --- a/files/archive.py +++ b/files/archive.py @@ -50,6 +50,27 @@ compression: bz2 ''' +RETURN = ''' +state: + description: The current state of the archived file. + type: string + returned: always +missing: + description: Any files that were missing from the source. + type: list + returned: success +archived: + description: Any files that were compressed or added to the archive. + type: list + returned: success +arcroot: + description: The archive root. + type: string +expanded_paths: + description: The list of matching paths from paths argument. + type: list +''' + import stat import os import errno From ecd60f48398ed7adacccc29801896c95dab0ae97 Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Thu, 26 May 2016 21:38:36 -0400 Subject: [PATCH 1609/2522] Add compressed file source to successes when succeeds! --- files/archive.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/files/archive.py b/files/archive.py index e64a9197bfe..42814027938 100644 --- a/files/archive.py +++ b/files/archive.py @@ -272,6 +272,8 @@ def main(): shutil.copyfileobj(f_in, f_out) + successes.append(path) + except OSError: e = get_exception() From f482cb4790fead81f1146fd9cc89a91e1c0ad12e Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Thu, 26 May 2016 21:41:40 -0400 Subject: [PATCH 1610/2522] Add license --- files/archive.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/files/archive.py b/files/archive.py index 42814027938..fecd7a45813 100644 --- a/files/archive.py +++ b/files/archive.py @@ -1,6 +1,26 @@ #!/usr/bin/python # -*- coding: utf-8 -*- +""" +(c) 2016, Ben Doherty +Sponsored by Oomph, Inc. http://www.oomphinc.com + +This file is part of Ansible + +Ansible is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Ansible is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Ansible. If not, see . +""" + DOCUMENTATION = ''' --- module: archive From cca70b7c9131627681bed77f4fbee5bb6e4823af Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Thu, 26 May 2016 23:09:35 -0400 Subject: [PATCH 1611/2522] Fix up for zip files and nesting logic. * Don't include the archive in the archive if it falls within an archived path * If remove=True and the archive would be in an archived path, fail. * Fix single-file zip file compression * Add more documentation about 'state' return --- files/archive.py | 49 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/files/archive.py b/files/archive.py index fecd7a45813..2dba54a300b 100644 --- a/files/archive.py +++ b/files/archive.py @@ -72,7 +72,12 @@ RETURN = ''' state: - description: The current state of the archived file. + description: + The current state of the archived file. + If 'absent', then no source files were found and the archive does not exist. + If 'compress', then the file source file is in the compressed state. + If 'archive', then the source file or paths are currently archived. + If 'incomplete', then an archive was created, but not all source paths were found. type: string returned: always missing: @@ -98,6 +103,7 @@ import shutil import gzip import bz2 +import filecmp import zipfile import tarfile @@ -157,6 +163,7 @@ def main(): archive_paths = [] missing = [] + exclude = [] arcroot = '' for path in expanded_paths: @@ -172,9 +179,9 @@ def main(): if i < len(arcroot): arcroot = os.path.dirname(arcroot[0:i+1]) - if path == creates: - # Don't allow the archive to specify itself! this is an error. - module.fail_json(path=', '.join(paths), msg='Error, created archive would be included in archive') + # Don't allow archives to be created anywhere within paths to be removed + if remove and os.path.isdir(path) and creates.startswith(path): + module.fail_json(path=', '.join(paths), msg='Error, created archive can not be contained in source paths when remove=True') if os.path.lexists(path): archive_paths.append(path) @@ -208,18 +215,40 @@ def main(): if state != 'archive': try: + # Easier compression using tarfile module if compression == 'gz' or compression == 'bz2': archive = tarfile.open(creates, 'w|' + compression) for path in archive_paths: - archive.add(path, path[len(arcroot):]) + basename = '' + + # Prefix trees in the archive with their basename, unless specifically prevented with '.' + if os.path.isdir(path) and not path.endswith(os.sep + '.'): + basename = os.path.basename(path) + os.sep + + archive.add(path, path[len(arcroot):], filter=lambda f: f if f.name != creates else None) successes.append(path) + # Slightly more difficult (and less efficient!) compression using zipfile module elif compression == 'zip': - archive = zipfile.ZipFile(creates, 'w') + archive = zipfile.ZipFile(creates, 'w', zipfile.ZIP_DEFLATED) for path in archive_paths: - archive.write(path, path[len(arcroot):]) + basename = '' + + # Prefix trees in the archive with their basename, unless specifically prevented with '.' + if os.path.isdir(path) and not path.endswith(os.sep + '.'): + basename = os.path.basename(path) + os.sep + + for dirpath, dirnames, filenames in os.walk(path, topdown=True): + for dirname in dirnames: + archive.write(dirpath + os.sep + dirname, basename + dirname) + for filename in filenames: + fullpath = dirpath + os.sep + filename + + if not filecmp.cmp(fullpath, creates): + archive.write(fullpath, basename + filename) + successes.append(path) except OSError: @@ -228,8 +257,10 @@ def main(): if archive: archive.close() + state = 'archive' + - if state != 'archive' and remove: + if state == 'archive' and remove: for path in successes: try: if os.path.isdir(path): @@ -275,7 +306,7 @@ def main(): try: if compression == 'zip': - archive = zipfile.ZipFile(creates, 'wb') + archive = zipfile.ZipFile(creates, 'w', zipfile.ZIP_DEFLATED) archive.write(path, path[len(arcroot):]) archive.close() state = 'archive' # because all zip files are archives From d3e041d1a23c6dfbd8722212e565190382cce4e7 Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Thu, 26 May 2016 23:42:03 -0400 Subject: [PATCH 1612/2522] Accept 'path' as a list argument, expose path and expanded_path, Use correct variable in expanduser --- files/archive.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/files/archive.py b/files/archive.py index 2dba54a300b..87840ec07cd 100644 --- a/files/archive.py +++ b/files/archive.py @@ -110,7 +110,7 @@ def main(): module = AnsibleModule( argument_spec = dict( - path = dict(required=True), + path = dict(type='list', required=True), compression = dict(choices=['gz', 'bz2', 'zip'], default='gz', required=False), creates = dict(required=False), remove = dict(required=False, default=True, type='bool'), @@ -133,11 +133,8 @@ def main(): archive = False successes = [] - if isinstance(paths, basestring): - paths = [paths] - for i, path in enumerate(paths): - path = os.path.expanduser(params['path']) + path = os.path.expanduser(path) # Detect glob-like characters if any((c in set('*?')) for c in path): @@ -146,7 +143,7 @@ def main(): expanded_paths.append(path) if len(expanded_paths) == 0: - module.fail_json(path, msg='Error, no source paths were found') + module.fail_json(path=', '.join(paths), expanded_paths=', '.join(expanded_paths), msg='Error, no source paths were found') # If we actually matched multiple files or TRIED to, then # treat this as a multi-file archive @@ -170,7 +167,7 @@ def main(): # Use the longest common directory name among all the files # as the archive root path if arcroot == '': - arcroot = os.path.dirname(path) + arcroot = os.path.dirname(path) + os.sep else: for i in xrange(len(arcroot)): if path[i] != arcroot[i]: @@ -259,8 +256,7 @@ def main(): archive.close() state = 'archive' - - if state == 'archive' and remove: + if state in ['archive', 'incomplete'] and remove: for path in successes: try: if os.path.isdir(path): From 6db9cafdec666b288cf3554be8fa81d3e5405900 Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Thu, 26 May 2016 23:49:32 -0400 Subject: [PATCH 1613/2522] Don't use if else syntax --- files/archive.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/files/archive.py b/files/archive.py index 87840ec07cd..3b4a512ba4f 100644 --- a/files/archive.py +++ b/files/archive.py @@ -223,7 +223,11 @@ def main(): if os.path.isdir(path) and not path.endswith(os.sep + '.'): basename = os.path.basename(path) + os.sep - archive.add(path, path[len(arcroot):], filter=lambda f: f if f.name != creates else None) + filter_create = lambda f: + if filecmp.cmp(f.name, creates): + return f + + archive.add(path, path[len(arcroot):], filter=filter_create) successes.append(path) # Slightly more difficult (and less efficient!) compression using zipfile module From ae35ce5641cef696573529761d0e90fc66912b82 Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Thu, 26 May 2016 23:58:17 -0400 Subject: [PATCH 1614/2522] Make remove default to false. It's less frightening. --- files/archive.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/files/archive.py b/files/archive.py index 3b4a512ba4f..e4931b9877f 100644 --- a/files/archive.py +++ b/files/archive.py @@ -28,7 +28,7 @@ short_description: Creates a compressed archive of one or more files or trees. extends_documentation_fragment: files description: - - The M(archive) module packs an archive. It is the opposite of the unarchive module. By default, it assumes the compression source exists on the target. It will not copy the source file from the local system to the target before archiving - set copy=yes to pack an archive which does not already exist on the target. The source files are deleted after archiving. + - The M(archive) module packs an archive. It is the opposite of the unarchive module. By default, it assumes the compression source exists on the target. It will not copy the source file from the local system to the target before archiving. Source files can be deleted after archival by specifying remove=True. options: path: description: @@ -41,22 +41,28 @@ choices: [ 'gz', 'bz2', 'zip' ] creates: description: - - The file name of the destination archive. When it already exists, this step will B(not) be run. This is required when 'path' refers to multiple files by either specifying a glob, a directory or multiple paths in a list. - required: false + - The file name of the destination archive. This is required when 'path' refers to multiple files by either specifying a glob, a directory or multiple paths in a list. + required: false, unless multiple source paths or globs are specified default: null + remove: + description: + - Remove any added source files and trees after adding to archive. + type: bool + required: false + default: false + author: "Ben Doherty (@bendoh)" notes: - requires tarfile, zipfile, gzip, and bzip2 packages on target host - - can product I(gzip), I(bzip2) and I(zip) compressed files or archives - - removes source files by default + - can produce I(gzip), I(bzip2) and I(zip) compressed files or archives ''' EXAMPLES = ''' # Compress directory /path/to/foo/ into /path/to/foo.tgz - archive: path=/path/to/foo creates=/path/to/foo.tgz -# Compress regular file /path/to/foo into /path/to/foo.gz -- archive: path=/path/to/foo +# Compress regular file /path/to/foo into /path/to/foo.gz and remove it +- archive: path=/path/to/foo remove=True # Create a zip archive of /path/to/foo - archive: path=/path/to/foo compression=zip @@ -113,7 +119,7 @@ def main(): path = dict(type='list', required=True), compression = dict(choices=['gz', 'bz2', 'zip'], default='gz', required=False), creates = dict(required=False), - remove = dict(required=False, default=True, type='bool'), + remove = dict(required=False, default=False, type='bool'), ), add_file_common_args=True, supports_check_mode=True, From 20bfb1339d06b6b576e1d7b15c21834d51f015b2 Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Fri, 27 May 2016 00:00:59 -0400 Subject: [PATCH 1615/2522] Use different syntax in lambda --- files/archive.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/files/archive.py b/files/archive.py index e4931b9877f..811a9aaa000 100644 --- a/files/archive.py +++ b/files/archive.py @@ -229,11 +229,7 @@ def main(): if os.path.isdir(path) and not path.endswith(os.sep + '.'): basename = os.path.basename(path) + os.sep - filter_create = lambda f: - if filecmp.cmp(f.name, creates): - return f - - archive.add(path, path[len(arcroot):], filter=filter_create) + archive.add(path, path[len(arcroot):], filter=lambda f: not filecmp.cmp(f.name, creates) and f) successes.append(path) # Slightly more difficult (and less efficient!) compression using zipfile module From 6e0aac888b736b4edd13d130cb0e24af01da842e Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Fri, 27 May 2016 00:07:15 -0400 Subject: [PATCH 1616/2522] Documentation updates --- files/archive.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/files/archive.py b/files/archive.py index 811a9aaa000..8a9a082f2a0 100644 --- a/files/archive.py +++ b/files/archive.py @@ -28,21 +28,20 @@ short_description: Creates a compressed archive of one or more files or trees. extends_documentation_fragment: files description: - - The M(archive) module packs an archive. It is the opposite of the unarchive module. By default, it assumes the compression source exists on the target. It will not copy the source file from the local system to the target before archiving. Source files can be deleted after archival by specifying remove=True. + - The M(archive) module packs an archive. It is the opposite of the unarchive module. By default, it assumes the compression source exists on the target. It will not copy the source file from the local system to the target before archiving. Source files can be deleted after archival by specifying C(remove)=I(True). options: path: description: - Remote absolute path, glob, or list of paths or globs for the file or files to archive or compress. - required: false - default: null + required: true compression: description: - The type of compression to use. Can be 'gz', 'bz2', or 'zip'. choices: [ 'gz', 'bz2', 'zip' ] creates: description: - - The file name of the destination archive. This is required when 'path' refers to multiple files by either specifying a glob, a directory or multiple paths in a list. - required: false, unless multiple source paths or globs are specified + - The file name of the destination archive. This is required when C(path) refers to multiple files by either specifying a glob, a directory or multiple paths in a list. + required: false default: null remove: description: From e388fb4006e1d8f7828527a02c445bc633b5b367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Fri, 27 May 2016 15:46:56 +0200 Subject: [PATCH 1617/2522] consul: fix param name for verify SSL (#2194) Introduced in #1793, fixes #2114, needs backport to 2.1 --- clustering/consul.py | 2 +- clustering/consul_acl.py | 2 +- clustering/consul_kv.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/clustering/consul.py b/clustering/consul.py index e96e7524aeb..27c3e84260c 100644 --- a/clustering/consul.py +++ b/clustering/consul.py @@ -332,7 +332,7 @@ def get_consul_api(module, token=None): return consul.Consul(host=module.params.get('host'), port=module.params.get('port'), scheme=module.params.get('scheme'), - validate_certs=module.params.get('validate_certs'), + verify=module.params.get('validate_certs'), token=module.params.get('token')) diff --git a/clustering/consul_acl.py b/clustering/consul_acl.py index b1c7763a550..34c569b250c 100644 --- a/clustering/consul_acl.py +++ b/clustering/consul_acl.py @@ -313,7 +313,7 @@ def get_consul_api(module, token=None): return consul.Consul(host=module.params.get('host'), port=module.params.get('port'), scheme=module.params.get('scheme'), - validate_certs=module.params.get('validate_certs'), + verify=module.params.get('validate_certs'), token=token) def test_dependencies(module): diff --git a/clustering/consul_kv.py b/clustering/consul_kv.py index 4dbf6072905..8163cbd986b 100644 --- a/clustering/consul_kv.py +++ b/clustering/consul_kv.py @@ -244,7 +244,7 @@ def get_consul_api(module, token=None): return consul.Consul(host=module.params.get('host'), port=module.params.get('port'), scheme=module.params.get('scheme'), - validate_certs=module.params.get('validate_certs'), + verify=module.params.get('validate_certs'), token=module.params.get('token')) def test_dependencies(module): From e316888f21af2c52ea1c7bf61eb7512fb65d38fd Mon Sep 17 00:00:00 2001 From: Dmitry Marakasov Date: Fri, 27 May 2016 17:49:54 +0400 Subject: [PATCH 1618/2522] Freebsd pkgng autoremove support (#2324) * Whitespace cleanup * Add autoremove capability to pkgng * Add "default" and "choices" documentnation items for autoremove --- packaging/os/pkgng.py | 44 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/packaging/os/pkgng.py b/packaging/os/pkgng.py index 44212714ef3..6117cb571ca 100644 --- a/packaging/os/pkgng.py +++ b/packaging/os/pkgng.py @@ -75,6 +75,13 @@ - pkg will chroot in the specified environment - can not be used together with 'rootdir' option required: false + autoremove: + version_added: "2.2" + description: + - remove automatically installed packages which are no longer needed + required: false + choices: [ "yes", "no" ] + default: no author: "bleader (@bleader)" notes: - When using pkgsite, be careful that already in cache packages won't be downloaded again. @@ -124,7 +131,7 @@ def pkgng_older_than(module, pkgng_path, compare_version): def remove_packages(module, pkgng_path, packages, dir_arg): - + remove_c = 0 # Using a for loop incase of error, we can report the package that failed for package in packages: @@ -137,7 +144,7 @@ def remove_packages(module, pkgng_path, packages, dir_arg): if not module.check_mode and query_package(module, pkgng_path, package, dir_arg): module.fail_json(msg="failed to remove %s: %s" % (package, out)) - + remove_c += 1 if remove_c > 0: @@ -185,7 +192,7 @@ def install_packages(module, pkgng_path, packages, cached, pkgsite, dir_arg): module.fail_json(msg="failed to install %s: %s" % (package, out), stderr=err) install_c += 1 - + if install_c > 0: return (True, "added %s package(s)" % (install_c)) @@ -270,6 +277,23 @@ def annotate_packages(module, pkgng_path, packages, annotation, dir_arg): return (True, "added %s annotations." % annotate_c) return (False, "changed no annotations") +def autoremove_packages(module, pkgng_path, dir_arg): + rc, out, err = module.run_command("%s %s autoremove -n" % (pkgng_path, dir_arg)) + + autoremove_c = 0 + + match = re.search('^Deinstallation has been requested for the following ([0-9]+) packages', out, re.MULTILINE) + if match: + autoremove_c = int(match.group(1)) + + if autoremove_c == 0: + return False, "no package(s) to autoremove" + + if not module.check_mode: + rc, out, err = module.run_command("%s %s autoremove -y" % (pkgng_path, dir_arg)) + + return True, "autoremoved %d package(s)" % (autoremove_c) + def main(): module = AnsibleModule( argument_spec = dict( @@ -279,7 +303,8 @@ def main(): annotation = dict(default="", required=False), pkgsite = dict(default="", required=False), rootdir = dict(default="", required=False, type='path'), - chroot = dict(default="", required=False, type='path')), + chroot = dict(default="", required=False, type='path'), + autoremove = dict(default=False, type='bool')), supports_check_mode = True, mutually_exclusive =[["rootdir", "chroot"]]) @@ -301,7 +326,7 @@ def main(): dir_arg = "--rootdir %s" % (p["rootdir"]) if p["chroot"] != "": - dir_arg = '--chroot %s' % (p["chroot"]) + dir_arg = '--chroot %s' % (p["chroot"]) if p["state"] == "present": _changed, _msg = install_packages(module, pkgng_path, pkgs, p["cached"], p["pkgsite"], dir_arg) @@ -313,6 +338,11 @@ def main(): changed = changed or _changed msgs.append(_msg) + if p["autoremove"]: + _changed, _msg = autoremove_packages(module, pkgng_path, dir_arg) + changed = changed or _changed + msgs.append(_msg) + if p["annotation"]: _changed, _msg = annotate_packages(module, pkgng_path, pkgs, p["annotation"], dir_arg) changed = changed or _changed @@ -324,5 +354,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * - -main() + +main() From 53ffd5f82b32ea963ab7e602d65849752ff1af5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Fri, 27 May 2016 20:53:59 +0200 Subject: [PATCH 1619/2522] cloudstack: new module cs_snapshot_policy (#2303) --- cloud/cloudstack/cs_snapshot_policy.py | 323 +++++++++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 cloud/cloudstack/cs_snapshot_policy.py diff --git a/cloud/cloudstack/cs_snapshot_policy.py b/cloud/cloudstack/cs_snapshot_policy.py new file mode 100644 index 00000000000..01efa107bea --- /dev/null +++ b/cloud/cloudstack/cs_snapshot_policy.py @@ -0,0 +1,323 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2016, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: cs_snapshot_policy +short_description: Manages volume snapshot policies on Apache CloudStack based clouds. +description: + - Create, update and delete volume snapshot policies. +version_added: '2.2' +author: "René Moser (@resmo)" +options: + volume: + description: + - Name of the volume. + required: true + interval_type: + description: + - Interval of the snapshot. + required: false + default: 'daily' + choices: [ 'hourly', 'daily', 'weekly', 'monthly' ] + aliases: [ 'interval' ] + max_snaps: + description: + - Max number of snapshots. + required: false + default: 8 + aliases: [ 'max' ] + schedule: + description: + - Time the snapshot is scheduled. Required if C(state=present). + - 'Format for C(interval_type=HOURLY): C(MM)' + - 'Format for C(interval_type=DAILY): C(MM:HH)' + - 'Format for C(interval_type=WEEKLY): C(MM:HH:DD (1-7))' + - 'Format for C(interval_type=MONTHLY): C(MM:HH:DD (1-28))' + required: false + default: null + time_zone: + description: + - Specifies a timezone for this command. + required: false + default: 'UTC' + aliases: [ 'timezone' ] + state: + description: + - State of the resource. + required: false + default: 'present' + choices: [ 'present', 'absent' ] + domain: + description: + - Domain the resource is related to. + required: false + default: null + account: + description: + - Account the resource is related to. + required: false + default: null + project: + description: + - Name of the project the resource is related to. + required: false + default: null +extends_documentation_fragment: cloudstack +''' + +EXAMPLES = ''' +# Ensure a snapshot policy daily at 1h00 UTC +- local_action: + module: cs_snapshot_policy + volume: ROOT-478 + schedule: '00:1' + max_snaps: 3 + +# Ensure a snapshot policy hourly at minute 5 UTC +- local_action: + module: cs_snapshot_policy + volume: ROOT-478 + schedule: '5' + interval_type: hourly + max_snaps: 1 + +# Ensure a snapshot policy weekly on Sunday at 05h00, TZ Europe/Zurich +- local_action: + module: cs_snapshot_policy + volume: ROOT-478 + schedule: '00:5:1' + interval_type: weekly + max_snaps: 1 + time_zone: 'Europe/Zurich' + +# Ensure a snapshot policy is absent +- local_action: + module: cs_snapshot_policy + volume: ROOT-478 + interval_type: hourly + state: absent +''' + +RETURN = ''' +--- +id: + description: UUID of the snapshot policy. + returned: success + type: string + sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f +interval_type: + description: interval type of the snapshot policy. + returned: success + type: string + sample: daily +schedule: + description: schedule of the snapshot policy. + returned: success + type: string + sample: +max_snaps: + description: maximum number of snapshots retained. + returned: success + type: int + sample: 10 +time_zone: + description: the time zone of the snapshot policy. + returned: success + type: string + sample: Etc/UTC +volume: + description: the volume of the snapshot policy. + returned: success + type: string + sample: Etc/UTC +zone: + description: Name of zone the volume is related to. + returned: success + type: string + sample: ch-gva-2 +project: + description: Name of project the volume is related to. + returned: success + type: string + sample: Production +account: + description: Account the volume is related to. + returned: success + type: string + sample: example account +domain: + description: Domain the volume is related to. + returned: success + type: string + sample: example domain +''' + +# import cloudstack common +from ansible.module_utils.cloudstack import * + + +class AnsibleCloudStackSnapshotPolicy(AnsibleCloudStack): + + def __init__(self, module): + super(AnsibleCloudStackSnapshotPolicy, self).__init__(module) + self.returns = { + 'schedule': 'schedule', + 'timezone': 'time_zone', + 'maxsnaps': 'max_snaps', + } + self.interval_types = { + 'hourly': 0, + 'daily': 1, + 'weekly': 2, + 'monthly': 3, + } + self.volume = None + + def get_interval_type(self): + interval_type = self.module.params.get('interval_type') + return self.interval_types[interval_type] + + def get_volume(self, key=None): + if self.volume: + return self._get_by_key(key, self.volume) + + args = { + 'name': self.module.params.get('volume'), + 'account': self.get_account(key='name'), + 'domainid': self.get_domain(key='id'), + 'projectid': self.get_project(key='id'), + } + volumes = self.cs.listVolumes(**args) + if volumes: + self.volume = volumes['volume'][0] + return self._get_by_key(key, self.volume) + return None + + def get_snapshot_policy(self): + args = { + 'volumeid': self.get_volume(key='id') + } + policies = self.cs.listSnapshotPolicies(**args) + if policies: + for policy in policies['snapshotpolicy']: + if policy['intervaltype'] == self.get_interval_type(): + return policy + return None + + def present_snapshot_policy(self): + required_params = [ + 'schedule', + ] + self.module.fail_on_missing_params(required_params=required_params) + + policy = self.get_snapshot_policy() + args = { + 'intervaltype': self.module.params.get('interval_type'), + 'schedule': self.module.params.get('schedule'), + 'maxsnaps': self.module.params.get('max_snaps'), + 'timezone': self.module.params.get('time_zone'), + 'volumeid': self.get_volume(key='id') + } + if not policy or (policy and self.has_changed(policy, args)): + self.result['changed'] = True + if not self.module.check_mode: + res = self.cs.createSnapshotPolicy(**args) + policy = res['snapshotpolicy'] + if 'errortext' in policy: + self.module.fail_json(msg="Failed: '%s'" % policy['errortext']) + return policy + + def absent_snapshot_policy(self): + policy = self.get_snapshot_policy() + if policy: + self.result['changed'] = True + args = { + 'id': policy['id'] + } + if not self.module.check_mode: + res = self.cs.deleteSnapshotPolicies(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % policy['errortext']) + return policy + + def get_result(self, policy): + super(AnsibleCloudStackSnapshotPolicy, self).get_result(policy) + if policy and 'intervaltype' in policy: + for key, value in self.interval_types.items(): + if value == policy['intervaltype']: + self.result['interval_type'] = key + break + volume = self.get_volume() + if volume: + volume_results = { + 'volume': volume.get('name'), + 'zone': volume.get('zonename'), + 'project': volume.get('project'), + 'account': volume.get('account'), + 'domain': volume.get('domain'), + } + self.result.update(volume_results) + return self.result + + +def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + volume=dict(required=True), + interval_type=dict(default='daily', choices=['hourly', 'daily', 'weekly', 'monthly'], aliases=['interval']), + schedule=dict(default=None), + time_zone=dict(default='UTC', aliases=['timezone']), + max_snaps=dict(type='int', default=8, aliases=['max']), + state=dict(choices=['present', 'absent'], default='present'), + zone=dict(default=None), + domain=dict(default=None), + account=dict(default=None), + project=dict(default=None), + poll_async=dict(type='bool', default=True), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=cs_required_together(), + supports_check_mode=True + ) + + try: + acs_snapshot_policy = AnsibleCloudStackSnapshotPolicy(module) + + state = module.params.get('state') + if state in ['absent']: + policy = acs_snapshot_policy.absent_snapshot_policy() + else: + policy = acs_snapshot_policy.present_snapshot_policy() + + result = acs_snapshot_policy.get_result(policy) + + except CloudStackException as e: + module.fail_json(msg='CloudStackException: %s' % str(e)) + + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From 5a7a9a0a30b139f38a92323409717b0a2c491cd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Fri, 27 May 2016 20:55:03 +0200 Subject: [PATCH 1620/2522] cloudstack: cs_template: fix typos (#2294) --- cloud/cloudstack/cs_template.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/cloudstack/cs_template.py b/cloud/cloudstack/cs_template.py index ebc0a4ba803..daee15c1e25 100644 --- a/cloud/cloudstack/cs_template.py +++ b/cloud/cloudstack/cs_template.py @@ -89,7 +89,7 @@ default: false cross_zones: description: - - Whether the template should be syned or removed across zones. + - Whether the template should be synced or removed across zones. - Only used if C(state) is present or absent. required: false default: false @@ -132,7 +132,7 @@ - Name the hypervisor to be used for creating the new template. - Relevant when using C(state=present). required: false - default: none + default: null choices: [ 'KVM', 'VMware', 'BareMetal', 'XenServer', 'LXC', 'HyperV', 'UCS', 'OVM' ] requires_hvm: description: From 039005bfaa08ef2a506c96fb8c690bb22ba51285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Fri, 27 May 2016 20:56:00 +0200 Subject: [PATCH 1621/2522] cloudstack: cs_firewall: get_network moved to utils, cleanup (#2293) --- cloud/cloudstack/cs_firewall.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/cloud/cloudstack/cs_firewall.py b/cloud/cloudstack/cs_firewall.py index b2e5a68a7a0..958c13d4aba 100644 --- a/cloud/cloudstack/cs_firewall.py +++ b/cloud/cloudstack/cs_firewall.py @@ -310,32 +310,6 @@ def _type_cidr_match(self, rule, cidr): return cidr == rule['cidrlist'] - def get_network(self, key=None): - if self.network: - return self._get_by_key(key, self.network) - - network = self.module.params.get('network') - if not network: - return None - - args = {} - args['account'] = self.get_account('name') - args['domainid'] = self.get_domain('id') - args['projectid'] = self.get_project('id') - args['zoneid'] = self.get_zone('id') - - networks = self.cs.listNetworks(**args) - if not networks: - self.module.fail_json(msg="No networks available") - - for n in networks['network']: - if network in [ n['displaytext'], n['name'], n['id'] ]: - self.network = n - return self._get_by_key(key, n) - break - self.module.fail_json(msg="Network '%s' not found" % network) - - def create_firewall_rule(self): firewall_rule = self.get_firewall_rule() if not firewall_rule: From abe406f074389fe9fadbe916657a43a667be0202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Fri, 27 May 2016 20:57:09 +0200 Subject: [PATCH 1622/2522] cloudstack: cs_staticnat: add vpc support (#2285) * cloudstack: cs_staticnat: add network arg, used for VPC support * cloudstack: cs_staticnat: removed unused code --- cloud/cloudstack/cs_staticnat.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/cloud/cloudstack/cs_staticnat.py b/cloud/cloudstack/cs_staticnat.py index f6b5d3f9bae..5e406851ecf 100644 --- a/cloud/cloudstack/cs_staticnat.py +++ b/cloud/cloudstack/cs_staticnat.py @@ -42,6 +42,12 @@ - VM guest NIC secondary IP address for the static NAT. required: false default: false + network: + description: + - Network the IP address is related to. + required: false + default: null + version_added: "2.2" state: description: - State of the static NAT. @@ -198,6 +204,7 @@ def create_static_nat(self, ip_address): args['virtualmachineid'] = self.get_vm(key='id') args['ipaddressid'] = ip_address['id'] args['vmguestip'] = self.get_vm_guest_ip() + args['networkid'] = self.get_network(key='id') if not self.module.check_mode: res = self.cs.enableStaticNat(**args) if 'errortext' in res: @@ -223,7 +230,7 @@ def update_static_nat(self, ip_address): res = self.cs.disableStaticNat(ipaddressid=ip_address['id']) if 'errortext' in res: self.module.fail_json(msg="Failed: '%s'" % res['errortext']) - res = self._poll_job(res, 'staticnat') + self._poll_job(res, 'staticnat') res = self.cs.enableStaticNat(**args) if 'errortext' in res: self.module.fail_json(msg="Failed: '%s'" % res['errortext']) @@ -253,17 +260,17 @@ def absent_static_nat(self): self.module.fail_json(msg="Failed: '%s'" % res['errortext']) poll_async = self.module.params.get('poll_async') if poll_async: - res = self._poll_job(res, 'staticnat') + self._poll_job(res, 'staticnat') return ip_address - def main(): argument_spec = cs_argument_spec() argument_spec.update(dict( ip_address = dict(required=True), vm = dict(default=None), vm_guest_ip = dict(default=None), + network = dict(default=None), state = dict(choices=['present', 'absent'], default='present'), zone = dict(default=None), domain = dict(default=None), From 57e1497fcff70bb86873562d23dd65de565c4ac0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Fri, 27 May 2016 21:02:29 +0200 Subject: [PATCH 1623/2522] cloudstack: cs_ip_address: add vpc support (#2283) * cloudstack: cs_ip_address: add vpc support * cloudstack: cs_ip_address: remove unused/unreachable code --- cloud/cloudstack/cs_ip_address.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/cloud/cloudstack/cs_ip_address.py b/cloud/cloudstack/cs_ip_address.py index c5a65d6272c..237a67fbcdb 100644 --- a/cloud/cloudstack/cs_ip_address.py +++ b/cloud/cloudstack/cs_ip_address.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # # (c) 2015, Darren Worrall +# (c) 2015, René Moser # # This file is part of Ansible # @@ -27,7 +28,9 @@ limitations this is not an idempotent call, so be sure to only conditionally call this when C(state=present) version_added: '2.0' -author: "Darren Worrall @dazworrall" +author: + - "Darren Worrall (@dazworrall)" + - "René Moser (@resmo)" options: ip_address: description: @@ -45,6 +48,12 @@ - Network the IP address is related to. required: false default: null + vpc: + description: + - VPC the IP address is related to. + required: false + default: null + version_added: "2.2" account: description: - Account the IP address is related to. @@ -159,7 +168,6 @@ def get_network(self, key=None, network=None): for n in networks['network']: if network in [ n['displaytext'], n['name'], n['id'] ]: return self._get_by_key(key, n) - break self.module.fail_json(msg="Network '%s' not found" % network) @@ -177,6 +185,7 @@ def get_ip_address(self, key=None): args['account'] = self.get_account(key='name') args['domainid'] = self.get_domain(key='id') args['projectid'] = self.get_project(key='id') + args['vpcid'] = self.get_vpc(key='id') ip_addresses = self.cs.listPublicIpAddresses(**args) if ip_addresses: @@ -219,7 +228,7 @@ def disassociate_ip_address(self): self.module.fail_json(msg="Failed: '%s'" % res['errortext']) poll_async = self.module.params.get('poll_async') if poll_async: - res = self._poll_job(res, 'ipaddress') + self._poll_job(res, 'ipaddress') return ip_address @@ -228,10 +237,11 @@ def main(): argument_spec.update(dict( ip_address = dict(required=False), state = dict(choices=['present', 'absent'], default='present'), + vpc = dict(default=None), + network = dict(default=None), zone = dict(default=None), domain = dict(default=None), account = dict(default=None), - network = dict(default=None), project = dict(default=None), poll_async = dict(type='bool', default=True), )) From 1e3c0ba1cfeee5687f991134e6d410ade5021485 Mon Sep 17 00:00:00 2001 From: Adrian Likins Date: Fri, 27 May 2016 19:52:28 -0400 Subject: [PATCH 1624/2522] Fix bad merge of #555 (mv bigip_gtm_virtual_server) (#2302) f5/ was the wrong directory. Move it to network/f5 and remove f5/. --- f5/__init__.py | 0 {f5 => network/f5}/bigip_gtm_virtual_server.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 f5/__init__.py rename {f5 => network/f5}/bigip_gtm_virtual_server.py (100%) diff --git a/f5/__init__.py b/f5/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/f5/bigip_gtm_virtual_server.py b/network/f5/bigip_gtm_virtual_server.py similarity index 100% rename from f5/bigip_gtm_virtual_server.py rename to network/f5/bigip_gtm_virtual_server.py From 8e20fcfafe65a226d60dbb5aefd24aa91cbc8432 Mon Sep 17 00:00:00 2001 From: Stefan Horning Date: Sat, 28 May 2016 01:56:02 +0200 Subject: [PATCH 1625/2522] =?UTF-8?q?Streamlined=20ec2=5Felb=5Ffacts=20mod?= =?UTF-8?q?ule=20return=20values=20with=20naming=20in=20ec2=5Felb=E2=80=A6?= =?UTF-8?q?=20(#2081)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Streamlined ec2_elb_facts module return values with naming in ec2_elb_lb (hosted zones) * Keep the old return values for hosted zone to keep backwards compatibility --- cloud/amazon/ec2_elb_facts.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cloud/amazon/ec2_elb_facts.py b/cloud/amazon/ec2_elb_facts.py index 566447805f6..02ea0f90626 100644 --- a/cloud/amazon/ec2_elb_facts.py +++ b/cloud/amazon/ec2_elb_facts.py @@ -138,6 +138,8 @@ def get_elb_info(connection,elb): 'dns_name': elb.dns_name, 'canonical_hosted_zone_name': elb.canonical_hosted_zone_name, 'canonical_hosted_zone_name_id': elb.canonical_hosted_zone_name_id, + 'hosted_zone_name': elb.canonical_hosted_zone_name, + 'hosted_zone_id': elb.canonical_hosted_zone_name_id, 'instances': [instance.id for instance in elb.instances], 'listeners': get_elb_listeners(elb.listeners), 'scheme': elb.scheme, From ef620c7de3d59a905f980049593518a9d65e137e Mon Sep 17 00:00:00 2001 From: Benjamin Doherty Date: Sat, 28 May 2016 09:02:43 -0400 Subject: [PATCH 1626/2522] Add 'default' to docs for 'compression' option --- files/archive.py | 1 + 1 file changed, 1 insertion(+) diff --git a/files/archive.py b/files/archive.py index 8a9a082f2a0..6eee99bc590 100644 --- a/files/archive.py +++ b/files/archive.py @@ -38,6 +38,7 @@ description: - The type of compression to use. Can be 'gz', 'bz2', or 'zip'. choices: [ 'gz', 'bz2', 'zip' ] + default: 'gz' creates: description: - The file name of the destination archive. This is required when C(path) refers to multiple files by either specifying a glob, a directory or multiple paths in a list. From 4d82ff99e2eb6c367e384fa0fdf42b6f5c3d22b5 Mon Sep 17 00:00:00 2001 From: jjshoe Date: Mon, 30 May 2016 20:33:16 -0500 Subject: [PATCH 1627/2522] A couple of touch ups (#2288) I peeked at #2281 a little late, thought this might help some as well. --- windows/win_package.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/windows/win_package.py b/windows/win_package.py index 633f6c6d339..47711369c1c 100644 --- a/windows/win_package.py +++ b/windows/win_package.py @@ -26,7 +26,7 @@ module: win_package version_added: "1.7" author: Trond Hindenes -short_description: Installs/Uninstalls a installable package, either from local file system or url +short_description: Installs/Uninstalls an installable package, either from local file system or url description: - Installs or uninstalls a package. - 'Optionally uses a product_id to check if the package needs installing. You can find product ids for installed programs in the windows registry either in C(HKLM:Software\\Microsoft\\Windows\CurrentVersion\\Uninstall) or for 32 bit programs C(HKLM:Software\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall)' @@ -37,12 +37,13 @@ required: true name: description: - - name of the package. Just for logging reasons, will use the value of path if name isn't specified + - Name of the package, if name isn't specified the path will be used for log messages required: false default: null product_id: description: - product id of the installed package (used for checking if already installed) + - You can find product ids for installed programs in the windows registry either in C(HKLM:Software\\Microsoft\\Windows\CurrentVersion\\Uninstall) or for 32 bit programs C(HKLM:Software\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall)' required: true aliases: [productid] arguments: From 8fac01a3cca168ced59152c04ee8bca16e67586f Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 31 May 2016 08:38:43 -0400 Subject: [PATCH 1628/2522] minor fixes removed unused imports rearranged basic import added if/main for testing/importability --- packaging/os/pkgng.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packaging/os/pkgng.py b/packaging/os/pkgng.py index 6117cb571ca..2b2f5e0b467 100644 --- a/packaging/os/pkgng.py +++ b/packaging/os/pkgng.py @@ -99,10 +99,8 @@ ''' -import shlex -import os import re -import sys +from ansible.module_utils.basic import AnsibleModule def query_package(module, pkgng_path, name, dir_arg): @@ -351,8 +349,5 @@ def main(): module.exit_json(changed=changed, msg=", ".join(msgs)) - -# import module snippets -from ansible.module_utils.basic import * - -main() +if __name__ == '__main__': + main() From a003b8d0817e6458e8d0762f6e9d488bd17a67aa Mon Sep 17 00:00:00 2001 From: Gerrit Germis Date: Tue, 31 May 2016 18:26:25 +0200 Subject: [PATCH 1629/2522] fail when backend host is not found (#1385) --- network/haproxy.py | 217 +++++++++++++++++++++------------------------ 1 file changed, 100 insertions(+), 117 deletions(-) diff --git a/network/haproxy.py b/network/haproxy.py index 4cc1c1c618b..f9b7661243d 100644 --- a/network/haproxy.py +++ b/network/haproxy.py @@ -60,6 +60,12 @@ required: true default: null choices: [ "enabled", "disabled" ] + fail_on_not_found: + description: + - Fail whenever trying to enable/disable a backend host that does not exist + required: false + default: false + version_added: "2.2" wait: description: - Wait until the server reports a status of 'UP' when `state=enabled`, or @@ -105,6 +111,9 @@ # disable backend server in 'www' backend pool and drop open sessions to it - haproxy: state=disabled host={{ inventory_hostname }} backend=www socket=/var/run/haproxy.sock shutdown_sessions=true +# disable server without backend pool name (apply to all available backend pool) but fail when the backend host is not found +- haproxy: state=disabled host={{ inventory_hostname }} fail_on_not_found=yes + # enable server in 'www' backend pool - haproxy: state=enabled host={{ inventory_hostname }} backend=www @@ -123,6 +132,7 @@ import socket import csv import time +from string import Template DEFAULT_SOCKET_LOCATION="/var/run/haproxy.sock" @@ -156,23 +166,17 @@ def __init__(self, module): self.weight = self.module.params['weight'] self.socket = self.module.params['socket'] self.shutdown_sessions = self.module.params['shutdown_sessions'] + self.fail_on_not_found = self.module.params['fail_on_not_found'] self.wait = self.module.params['wait'] self.wait_retries = self.module.params['wait_retries'] self.wait_interval = self.module.params['wait_interval'] - self.command_results = [] - self.status_servers = [] - self.status_weights = [] - self.previous_weights = [] - self.previous_states = [] - self.current_states = [] - self.current_weights = [] + self.command_results = {} def execute(self, cmd, timeout=200, capture_output=True): """ Executes a HAProxy command by sending a message to a HAProxy's local UNIX socket and waiting up to 'timeout' milliseconds for the response. """ - self.client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.client.connect(self.socket) self.client.sendall('%s\n' % cmd) @@ -183,10 +187,67 @@ def execute(self, cmd, timeout=200, capture_output=True): result += buf buf = self.client.recv(RECV_SIZE) if capture_output: - self.command_results = result.strip() + self.capture_command_output(cmd, result.strip()) self.client.close() return result + + def capture_command_output(self, cmd, output): + """ + Capture the output for a command + """ + if not 'command' in self.command_results.keys(): + self.command_results['command'] = [] + self.command_results['command'].append(cmd) + if not 'output' in self.command_results.keys(): + self.command_results['output'] = [] + self.command_results['output'].append(output) + + + def discover_all_backends(self): + """ + Discover all entries with svname = 'BACKEND' and return a list of their corresponding + pxnames + """ + data = self.execute('show stat', 200, False).lstrip('# ') + r = csv.DictReader(data.splitlines()) + return map(lambda d: d['pxname'], filter(lambda d: d['svname'] == 'BACKEND', r)) + + + def execute_for_backends(self, cmd, pxname, svname, wait_for_status = None): + """ + Run some command on the specified backends. If no backends are provided they will + be discovered automatically (all backends) + """ + # Discover backends if none are given + if pxname is None: + backends = self.discover_all_backends() + else: + backends = [pxname] + + # Run the command for each requested backend + for backend in backends: + # Fail when backends were not found + state = self.get_state_for(backend, svname) + if (self.fail_on_not_found or self.wait) and state is None: + self.module.fail_json(msg="The specified backend '%s/%s' was not found!" % (backend, svname)) + + self.execute(Template(cmd).substitute(pxname = backend, svname = svname)) + if self.wait: + self.wait_until_status(backend, svname, wait_for_status) + + + def get_state_for(self, pxname, svname): + """ + Find the state of specific services. When pxname is not set, get all backends for a specific host. + Returns a list of dictionaries containing the status and weight for those services. + """ + data = self.execute('show stat', 200, False).lstrip('# ') + r = csv.DictReader(data.splitlines()) + state = map(lambda d: { 'status': d['status'], 'weight': d['weight'] }, filter(lambda d: (pxname is None or d['pxname'] == pxname) and d['svname'] == svname, r)) + return state or None + + def wait_until_status(self, pxname, svname, status): """ Wait for a service to reach the specified status. Try RETRIES times @@ -195,49 +256,16 @@ def wait_until_status(self, pxname, svname, status): not found, the module will fail. """ for i in range(1, self.wait_retries): - data = self.execute('show stat', 200, False).lstrip('# ') - r = csv.DictReader(data.splitlines()) - found = False - for row in r: - if row['pxname'] == pxname and row['svname'] == svname: - found = True - if row['status'] == status: - return True; - else: - time.sleep(self.wait_interval) - - if not found: - self.module.fail_json(msg="unable to find server %s/%s" % (pxname, svname)) + state = self.get_state_for(pxname, svname) + + # We can assume there will only be 1 element in state because both svname and pxname are always set when we get here + if state[0]['status'] == status: + return True + else: + time.sleep(self.wait_interval) self.module.fail_json(msg="server %s/%s not status '%s' after %d retries. Aborting." % (pxname, svname, status, self.wait_retries)) - def get_current_state(self, host, backend): - """ - Gets the each original state value from show stat. - Runs before and after to determine if values are changed. - This relies on weight always being the next element after - status in "show stat" as well as status states remaining - as indicated in status_states and haproxy documentation. - """ - - output = self.execute('show stat') - output = output.lstrip('# ').strip() - output = output.split(',') - result = output - status_states = [ 'UP','DOWN','DRAIN','NOLB','MAINT' ] - self.status_server = [] - status_weight_pos = [] - self.status_weight = [] - - for check, status in enumerate(result): - if status in status_states: - self.status_server.append(status) - status_weight_pos.append(check + 1) - - for weight in status_weight_pos: - self.status_weight.append(result[weight]) - - return{'self.status_server':self.status_server, 'self.status_weight':self.status_weight} def enabled(self, host, backend, weight): """ @@ -245,33 +273,11 @@ def enabled(self, host, backend, weight): also supports to get current weight for server (default) and set the weight for haproxy backend server when provides. """ - svname = host - if self.backend is None: - output = self.execute('show stat') - #sanitize and make a list of lines - output = output.lstrip('# ').strip() - output = output.split('\n') - result = output - - for line in result: - if 'BACKEND' in line: - result = line.split(',')[0] - pxname = result - cmd = "get weight %s/%s ; enable server %s/%s" % (pxname, svname, pxname, svname) - if weight: - cmd += "; set weight %s/%s %s" % (pxname, svname, weight) - self.execute(cmd) - if self.wait: - self.wait_until_status(pxname, svname, 'UP') + cmd = "get weight $pxname/$svname; enable server $pxname/$svname" + if weight: + cmd += "; set weight $pxname/$svname %s" % weight + self.execute_for_backends(cmd, backend, host, 'UP') - else: - pxname = backend - cmd = "get weight %s/%s ; enable server %s/%s" % (pxname, svname, pxname, svname) - if weight: - cmd += "; set weight %s/%s %s" % (pxname, svname, weight) - self.execute(cmd) - if self.wait: - self.wait_until_status(pxname, svname, 'UP') def disabled(self, host, backend, shutdown_sessions): """ @@ -279,64 +285,40 @@ def disabled(self, host, backend, shutdown_sessions): performed on the server until it leaves maintenance, also it shutdown sessions while disabling backend host server. """ - svname = host - if self.backend is None: - output = self.execute('show stat') - #sanitize and make a list of lines - output = output.lstrip('# ').strip() - output = output.split('\n') - result = output - - for line in result: - if 'BACKEND' in line: - result = line.split(',')[0] - pxname = result - cmd = "get weight %s/%s ; disable server %s/%s" % (pxname, svname, pxname, svname) - if shutdown_sessions: - cmd += "; shutdown sessions server %s/%s" % (pxname, svname) - self.execute(cmd) - if self.wait: - self.wait_until_status(pxname, svname, 'MAINT') + cmd = "get weight $pxname/$svname; disable server $pxname/$svname" + if shutdown_sessions: + cmd += "; shutdown sessions server $pxname/$svname" + self.execute_for_backends(cmd, backend, host, 'MAINT') - else: - pxname = backend - cmd = "get weight %s/%s ; disable server %s/%s" % (pxname, svname, pxname, svname) - if shutdown_sessions: - cmd += "; shutdown sessions server %s/%s" % (pxname, svname) - self.execute(cmd) - if self.wait: - self.wait_until_status(pxname, svname, 'MAINT') def act(self): """ Figure out what you want to do from ansible, and then do it. """ - - self.get_current_state(self.host, self.backend) - self.previous_states = ','.join(self.status_server) - self.previous_weights = ','.join(self.status_weight) + # Get the state before the run + state_before = self.get_state_for(self.backend, self.host) + self.command_results['state_before'] = state_before # toggle enable/disbale server if self.state == 'enabled': self.enabled(self.host, self.backend, self.weight) - elif self.state == 'disabled': self.disabled(self.host, self.backend, self.shutdown_sessions) - else: self.module.fail_json(msg="unknown state specified: '%s'" % self.state) - self.get_current_state(self.host, self.backend) - self.current_states = ','.join(self.status_server) - self.current_weights = ','.join(self.status_weight) - + # Get the state after the run + state_after = self.get_state_for(self.backend, self.host) + self.command_results['state_after'] = state_after - if self.current_weights != self.previous_weights: - self.module.exit_json(stdout=self.command_results, changed=True) - elif self.current_states != self.previous_states: - self.module.exit_json(stdout=self.command_results, changed=True) + # Report change status + if state_before != state_after: + self.command_results['changed'] = True + self.module.exit_json(**self.command_results) else: - self.module.exit_json(stdout=self.command_results, changed=False) + self.command_results['changed'] = False + self.module.exit_json(**self.command_results) + def main(): @@ -349,11 +331,11 @@ def main(): weight=dict(required=False, default=None), socket = dict(required=False, default=DEFAULT_SOCKET_LOCATION), shutdown_sessions=dict(required=False, default=False), + fail_on_not_found=dict(required=False, default=False, type='bool'), wait=dict(required=False, default=False, type='bool'), wait_retries=dict(required=False, default=WAIT_RETRIES, type='int'), wait_interval=dict(required=False, default=WAIT_INTERVAL, type='int'), ), - ) if not socket: @@ -366,3 +348,4 @@ def main(): from ansible.module_utils.basic import * main() + From b0aec50b9a0434ecf92942dcf2721edc2b60be8c Mon Sep 17 00:00:00 2001 From: Dmitry Marakasov Date: Tue, 31 May 2016 20:58:55 +0400 Subject: [PATCH 1630/2522] Improve documentation for pkgng module (#2338) According to module checklist: - Descriptions should always start with a Capital letter and end with a full stop. - Ensure that you make use of U() for urls, C() for files and options, I() for params, M() for modules. --- packaging/os/pkgng.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/packaging/os/pkgng.py b/packaging/os/pkgng.py index 2b2f5e0b467..5583bb18ee5 100644 --- a/packaging/os/pkgng.py +++ b/packaging/os/pkgng.py @@ -32,53 +32,53 @@ options: name: description: - - name of package to install/remove + - Name of package to install/remove. required: true state: description: - - state of the package + - State of the package. choices: [ 'present', 'absent' ] required: false default: present cached: description: - - use local package base or try to fetch an updated one + - Use local package base instead of fetching an updated one. choices: [ 'yes', 'no' ] required: false default: no annotation: description: - - a comma-separated list of keyvalue-pairs of the form - <+/-/:>[=]. A '+' denotes adding an annotation, a - '-' denotes removing an annotation, and ':' denotes modifying an + - A comma-separated list of keyvalue-pairs of the form + C(<+/-/:>[=]). A C(+) denotes adding an annotation, a + C(-) denotes removing an annotation, and C(:) denotes modifying an annotation. If setting or modifying annotations, a value must be provided. required: false version_added: "1.6" pkgsite: description: - - for pkgng versions before 1.1.4, specify packagesite to use - for downloading packages, if not specified, use settings from - /usr/local/etc/pkg.conf - for newer pkgng versions, specify a the name of a repository - configured in /usr/local/etc/pkg/repos + - For pkgng versions before 1.1.4, specify packagesite to use + for downloading packages. If not specified, use settings from + C(/usr/local/etc/pkg.conf). + - For newer pkgng versions, specify a the name of a repository + configured in C(/usr/local/etc/pkg/repos). required: false rootdir: description: - - for pkgng versions 1.5 and later, pkg will install all packages - within the specified root directory - - can not be used together with 'chroot' option + - For pkgng versions 1.5 and later, pkg will install all packages + within the specified root directory. + - Can not be used together with I(chroot) option. required: false chroot: version_added: "2.1" description: - - pkg will chroot in the specified environment - - can not be used together with 'rootdir' option + - Pkg will chroot in the specified environment. + - Can not be used together with I(rootdir) option. required: false autoremove: version_added: "2.2" description: - - remove automatically installed packages which are no longer needed + - Remove automatically installed packages which are no longer needed. required: false choices: [ "yes", "no" ] default: no From d5e861b3529d1e06281f0df755e55f6183d55c29 Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Tue, 31 May 2016 18:30:47 -0400 Subject: [PATCH 1631/2522] Reword comments slightly --- files/archive.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/files/archive.py b/files/archive.py index 8a9a082f2a0..53df2d109d4 100644 --- a/files/archive.py +++ b/files/archive.py @@ -32,7 +32,7 @@ options: path: description: - - Remote absolute path, glob, or list of paths or globs for the file or files to archive or compress. + - Remote absolute path, glob, or list of paths or globs for the file or files to compress or archive. required: true compression: description: @@ -134,7 +134,7 @@ def main(): changed = False state = 'absent' - # Simple or archive file compression (inapplicable with 'zip') + # Simple or archive file compression (inapplicable with 'zip' since it's always an archive) archive = False successes = [] From 726c4d9ba7a91c0938e94dacd18cd1102f056156 Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Tue, 31 May 2016 18:31:07 -0400 Subject: [PATCH 1632/2522] Some refactoring: * rename archive -> arcfile (where it's a file descriptor) * additional return * simplify logic around 'archive?' flag * maintain os separator after arcroot * use function instead of lambda for filter, ensure file exists before file.cmp'ing it * track errored files and fail if there are any --- files/archive.py | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/files/archive.py b/files/archive.py index 53df2d109d4..3ef0dcb20cc 100644 --- a/files/archive.py +++ b/files/archive.py @@ -148,11 +148,11 @@ def main(): expanded_paths.append(path) if len(expanded_paths) == 0: - module.fail_json(path=', '.join(paths), expanded_paths=', '.join(expanded_paths), msg='Error, no source paths were found') + return module.fail_json(path=', '.join(paths), expanded_paths=', '.join(expanded_paths), msg='Error, no source paths were found') # If we actually matched multiple files or TRIED to, then # treat this as a multi-file archive - archive = globby or len(expanded_paths) > 1 or any(os.path.isdir(path) for path in expanded_paths) + archive = globby or os.path.isdir(expanded_paths[0]) or len(expanded_paths) > 1 # Default created file name (for single-file archives) to # . @@ -181,6 +181,8 @@ def main(): if i < len(arcroot): arcroot = os.path.dirname(arcroot[0:i+1]) + arcroot += os.sep + # Don't allow archives to be created anywhere within paths to be removed if remove and os.path.isdir(path) and creates.startswith(path): module.fail_json(path=', '.join(paths), msg='Error, created archive can not be contained in source paths when remove=True') @@ -219,7 +221,9 @@ def main(): try: # Easier compression using tarfile module if compression == 'gz' or compression == 'bz2': - archive = tarfile.open(creates, 'w|' + compression) + arcfile = tarfile.open(creates, 'w|' + compression) + + arcfile.add(arcroot, os.path.basename(arcroot), recursive=False) for path in archive_paths: basename = '' @@ -228,12 +232,23 @@ def main(): if os.path.isdir(path) and not path.endswith(os.sep + '.'): basename = os.path.basename(path) + os.sep - archive.add(path, path[len(arcroot):], filter=lambda f: not filecmp.cmp(f.name, creates) and f) - successes.append(path) + try: + def exclude_creates(f): + if os.path.exists(f.name) and not filecmp.cmp(f.name, creates): + return f + + return None + + arcfile.add(path, basename + path[len(arcroot):], filter=exclude_creates) + successes.append(path) + + except: + e = get_exception() + errors.append('error adding %s: %s' % (path, str(e))) # Slightly more difficult (and less efficient!) compression using zipfile module elif compression == 'zip': - archive = zipfile.ZipFile(creates, 'w', zipfile.ZIP_DEFLATED) + arcfile = zipfile.ZipFile(creates, 'w', zipfile.ZIP_DEFLATED) for path in archive_paths: basename = '' @@ -244,23 +259,26 @@ def main(): for dirpath, dirnames, filenames in os.walk(path, topdown=True): for dirname in dirnames: - archive.write(dirpath + os.sep + dirname, basename + dirname) + arcfile.write(dirpath + os.sep + dirname, basename + dirname) for filename in filenames: fullpath = dirpath + os.sep + filename if not filecmp.cmp(fullpath, creates): - archive.write(fullpath, basename + filename) + arcfile.write(fullpath, basename + filename) successes.append(path) except OSError: e = get_exception() - module.fail_json(msg='Error when writing zip archive at %s: %s' % (creates, str(e))) + module.fail_json(msg='Error when writing %s archive at %s: %s' % (compression == 'zip' and 'zip' or ('tar.' + compression), creates, str(e))) - if archive: - archive.close() + if arcfile: + arcfile.close() state = 'archive' + if len(errors) > 0: + module.fail_json(msg='Errors when writing archive at %s: %s' % (creates, '; '.join(errors))) + if state in ['archive', 'incomplete'] and remove: for path in successes: try: From 0a056eccbf84ac2886432e86d04833a56ad62d7b Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Tue, 31 May 2016 23:42:37 -0400 Subject: [PATCH 1633/2522] Refactor zip and tarfile loops together, branch where calls are different This fixed a few bugs and simplified the code --- files/archive.py | 76 +++++++++++++++++++++++------------------------- 1 file changed, 36 insertions(+), 40 deletions(-) diff --git a/files/archive.py b/files/archive.py index 04f9e1f5922..21b3022f69b 100644 --- a/files/archive.py +++ b/files/archive.py @@ -220,58 +220,54 @@ def main(): if state != 'archive': try: - # Easier compression using tarfile module - if compression == 'gz' or compression == 'bz2': - arcfile = tarfile.open(creates, 'w|' + compression) - - arcfile.add(arcroot, os.path.basename(arcroot), recursive=False) - for path in archive_paths: - basename = '' - - # Prefix trees in the archive with their basename, unless specifically prevented with '.' - if os.path.isdir(path) and not path.endswith(os.sep + '.'): - basename = os.path.basename(path) + os.sep - - try: - def exclude_creates(f): - if os.path.exists(f.name) and not filecmp.cmp(f.name, creates): - return f + # Slightly more difficult (and less efficient!) compression using zipfile module + if compression == 'zip': + arcfile = zipfile.ZipFile(creates, 'w', zipfile.ZIP_DEFLATED) - return None + # Easier compression using tarfile module + elif compression == 'gz' or compression == 'bz2': + arcfile = tarfile.open(creates, 'w|' + compression) - arcfile.add(path, basename + path[len(arcroot):], filter=exclude_creates) - successes.append(path) + for path in archive_paths: + basename = '' - except: - e = get_exception() - errors.append('error adding %s: %s' % (path, str(e))) + # Prefix trees in the archive with their basename, unless specifically prevented with '.' + if os.path.isdir(path) and not path.endswith(os.sep + '.'): + basename = os.path.basename(path) + os.sep - # Slightly more difficult (and less efficient!) compression using zipfile module - elif compression == 'zip': - arcfile = zipfile.ZipFile(creates, 'w', zipfile.ZIP_DEFLATED) + for dirpath, dirnames, filenames in os.walk(path, topdown=True): + for dirname in dirnames: + fullpath = dirpath + os.sep + dirname - for path in archive_paths: - basename = '' + try: + if compression == 'zip': + arcfile.write(fullpath, basename + dirname) + else: + arcfile.add(fullpath, basename + dirname, recursive=False) - # Prefix trees in the archive with their basename, unless specifically prevented with '.' - if os.path.isdir(path) and not path.endswith(os.sep + '.'): - basename = os.path.basename(path) + os.sep + except Exception: + e = get_exception() + errors.append('%s: %s' % (fullpath, str(e))) - for dirpath, dirnames, filenames in os.walk(path, topdown=True): - for dirname in dirnames: - arcfile.write(dirpath + os.sep + dirname, basename + dirname) - for filename in filenames: - fullpath = dirpath + os.sep + filename + for filename in filenames: + fullpath = dirpath + os.sep + filename - if not filecmp.cmp(fullpath, creates): - arcfile.write(fullpath, basename + filename) + if not filecmp.cmp(fullpath, creates): + try: + if compression == 'zip': + arcfile.write(fullpath, basename + filename) + else: + arcfile.add(fullpath, basename + filename, recursive=False) - successes.append(path) + successes.append(fullpath) + except Exception: + e = get_exception() + errors.append('Adding %s: %s' % (path, str(e))) - except OSError: + except Exception: e = get_exception() - module.fail_json(msg='Error when writing %s archive at %s: %s' % (compression == 'zip' and 'zip' or ('tar.' + compression), creates, str(e))) + return module.fail_json(msg='Error when writing %s archive at %s: %s' % (compression == 'zip' and 'zip' or ('tar.' + compression), creates, str(e))) if arcfile: arcfile.close() From 5a278a8ecd3e62b7805902d084efab58abc33593 Mon Sep 17 00:00:00 2001 From: P Stark Date: Wed, 1 Jun 2016 10:02:46 +0200 Subject: [PATCH 1634/2522] add type declaration to the port parameter of the irc module. #AnsibleZH (#2349) --- notification/irc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notification/irc.py b/notification/irc.py index 95cd5bba8c2..92f285df241 100644 --- a/notification/irc.py +++ b/notification/irc.py @@ -247,7 +247,7 @@ def main(): module = AnsibleModule( argument_spec=dict( server=dict(default='localhost'), - port=dict(default=6667), + port=dict(type='int', default=6667), nick=dict(default='ansible'), nick_to=dict(required=False, type='list'), msg=dict(required=True), From b57b0473cf55dc9c23bb691b09306dd282a428da Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Wed, 1 Jun 2016 08:16:31 -0400 Subject: [PATCH 1635/2522] Change 'creates' parameter to 'dest' --- files/archive.py | 68 ++++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/files/archive.py b/files/archive.py index 21b3022f69b..11e9d155f76 100644 --- a/files/archive.py +++ b/files/archive.py @@ -39,7 +39,7 @@ - The type of compression to use. Can be 'gz', 'bz2', or 'zip'. choices: [ 'gz', 'bz2', 'zip' ] default: 'gz' - creates: + dest: description: - The file name of the destination archive. This is required when C(path) refers to multiple files by either specifying a glob, a directory or multiple paths in a list. required: false @@ -59,7 +59,7 @@ EXAMPLES = ''' # Compress directory /path/to/foo/ into /path/to/foo.tgz -- archive: path=/path/to/foo creates=/path/to/foo.tgz +- archive: path=/path/to/foo dest=/path/to/foo.tgz # Compress regular file /path/to/foo into /path/to/foo.gz and remove it - archive: path=/path/to/foo remove=True @@ -72,7 +72,7 @@ path: - /path/to/foo - /path/wong/foo - creates: /path/file.tar.bz2 + dest: /path/file.tar.bz2 compression: bz2 ''' @@ -118,7 +118,7 @@ def main(): argument_spec = dict( path = dict(type='list', required=True), compression = dict(choices=['gz', 'bz2', 'zip'], default='gz', required=False), - creates = dict(required=False), + dest = dict(required=False), remove = dict(required=False, default=False, type='bool'), ), add_file_common_args=True, @@ -127,7 +127,7 @@ def main(): params = module.params paths = params['path'] - creates = params['creates'] + dest = params['dest'] remove = params['remove'] expanded_paths = [] compression = params['compression'] @@ -157,12 +157,12 @@ def main(): # Default created file name (for single-file archives) to # . - if not archive and not creates: - creates = '%s.%s' % (expanded_paths[0], compression) + if not archive and not dest: + dest = '%s.%s' % (expanded_paths[0], compression) - # Force archives to specify 'creates' - if archive and not creates: - module.fail_json(creates=creates, path=', '.join(paths), msg='Error, must specify "creates" when archiving multiple files or trees') + # Force archives to specify 'dest' + if archive and not dest: + module.fail_json(dest=dest, path=', '.join(paths), msg='Error, must specify "dest" when archiving multiple files or trees') archive_paths = [] missing = [] @@ -185,7 +185,7 @@ def main(): arcroot += os.sep # Don't allow archives to be created anywhere within paths to be removed - if remove and os.path.isdir(path) and creates.startswith(path): + if remove and os.path.isdir(path) and dest.startswith(path): module.fail_json(path=', '.join(paths), msg='Error, created archive can not be contained in source paths when remove=True') if os.path.lexists(path): @@ -194,9 +194,9 @@ def main(): missing.append(path) # No source files were found but the named archive exists: are we 'compress' or 'archive' now? - if len(missing) == len(expanded_paths) and creates and os.path.exists(creates): + if len(missing) == len(expanded_paths) and dest and os.path.exists(dest): # Just check the filename to know if it's an archive or simple compressed file - if re.search(r'(\.tar\.gz|\.tgz|.tbz2|\.tar\.bz2|\.zip)$', os.path.basename(creates), re.IGNORECASE): + if re.search(r'(\.tar\.gz|\.tgz|.tbz2|\.tar\.bz2|\.zip)$', os.path.basename(dest), re.IGNORECASE): state = 'archive' else: state = 'compress' @@ -205,7 +205,7 @@ def main(): elif archive: if len(archive_paths) == 0: # No source files were found, but the archive is there. - if os.path.lexists(creates): + if os.path.lexists(dest): state = 'archive' elif len(missing) > 0: # SOME source files were found, but not all of them @@ -215,19 +215,19 @@ def main(): size = 0 errors = [] - if os.path.lexists(creates): - size = os.path.getsize(creates) + if os.path.lexists(dest): + size = os.path.getsize(dest) if state != 'archive': try: # Slightly more difficult (and less efficient!) compression using zipfile module if compression == 'zip': - arcfile = zipfile.ZipFile(creates, 'w', zipfile.ZIP_DEFLATED) + arcfile = zipfile.ZipFile(dest, 'w', zipfile.ZIP_DEFLATED) # Easier compression using tarfile module elif compression == 'gz' or compression == 'bz2': - arcfile = tarfile.open(creates, 'w|' + compression) + arcfile = tarfile.open(dest, 'w|' + compression) for path in archive_paths: basename = '' @@ -253,7 +253,7 @@ def main(): for filename in filenames: fullpath = dirpath + os.sep + filename - if not filecmp.cmp(fullpath, creates): + if not filecmp.cmp(fullpath, dest): try: if compression == 'zip': arcfile.write(fullpath, basename + filename) @@ -267,14 +267,14 @@ def main(): except Exception: e = get_exception() - return module.fail_json(msg='Error when writing %s archive at %s: %s' % (compression == 'zip' and 'zip' or ('tar.' + compression), creates, str(e))) + return module.fail_json(msg='Error when writing %s archive at %s: %s' % (compression == 'zip' and 'zip' or ('tar.' + compression), dest, str(e))) if arcfile: arcfile.close() state = 'archive' if len(errors) > 0: - module.fail_json(msg='Errors when writing archive at %s: %s' % (creates, '; '.join(errors))) + module.fail_json(msg='Errors when writing archive at %s: %s' % (dest, '; '.join(errors))) if state in ['archive', 'incomplete'] and remove: for path in successes: @@ -288,10 +288,10 @@ def main(): errors.append(path) if len(errors) > 0: - module.fail_json(creates=creates, msg='Error deleting some source files: ' + str(e), files=errors) + module.fail_json(dest=dest, msg='Error deleting some source files: ' + str(e), files=errors) # Rudimentary check: If size changed then file changed. Not perfect, but easy. - if os.path.getsize(creates) != size: + if os.path.getsize(dest) != size: changed = True if len(successes) and state != 'incomplete': @@ -302,27 +302,27 @@ def main(): path = expanded_paths[0] # No source or compressed file - if not (os.path.exists(path) or os.path.lexists(creates)): + if not (os.path.exists(path) or os.path.lexists(dest)): state = 'absent' # if it already exists and the source file isn't there, consider this done - elif not os.path.lexists(path) and os.path.lexists(creates): + elif not os.path.lexists(path) and os.path.lexists(dest): state = 'compress' else: if module.check_mode: - if not os.path.exists(creates): + if not os.path.exists(dest): changed = True else: size = 0 f_in = f_out = archive = None - if os.path.lexists(creates): - size = os.path.getsize(creates) + if os.path.lexists(dest): + size = os.path.getsize(dest) try: if compression == 'zip': - archive = zipfile.ZipFile(creates, 'w', zipfile.ZIP_DEFLATED) + archive = zipfile.ZipFile(dest, 'w', zipfile.ZIP_DEFLATED) archive.write(path, path[len(arcroot):]) archive.close() state = 'archive' # because all zip files are archives @@ -331,9 +331,9 @@ def main(): f_in = open(path, 'rb') if compression == 'gz': - f_out = gzip.open(creates, 'wb') + f_out = gzip.open(dest, 'wb') elif compression == 'bz2': - f_out = bz2.BZ2File(creates, 'wb') + f_out = bz2.BZ2File(dest, 'wb') else: raise OSError("Invalid compression") @@ -344,7 +344,7 @@ def main(): except OSError: e = get_exception() - module.fail_json(path=path, creates=creates, msg='Unable to write to compressed file: %s' % str(e)) + module.fail_json(path=path, dest=dest, msg='Unable to write to compressed file: %s' % str(e)) if archive: archive.close() @@ -354,7 +354,7 @@ def main(): f_out.close() # Rudimentary check: If size changed then file changed. Not perfect, but easy. - if os.path.getsize(creates) != size: + if os.path.getsize(dest) != size: changed = True state = 'compress' @@ -367,7 +367,7 @@ def main(): e = get_exception() module.fail_json(path=path, msg='Unable to remove source file: %s' % str(e)) - module.exit_json(archived=successes, creates=creates, changed=changed, state=state, arcroot=arcroot, missing=missing, expanded_paths=expanded_paths) + module.exit_json(archived=successes, dest=dest, changed=changed, state=state, arcroot=arcroot, missing=missing, expanded_paths=expanded_paths) # import module snippets from ansible.module_utils.basic import * From b9971f131a3ab6dae5d860d2e99e7b7336b8c077 Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Wed, 1 Jun 2016 08:29:14 -0400 Subject: [PATCH 1636/2522] Rename 'archive' -> 'arcfile' in compress branch --- files/archive.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/files/archive.py b/files/archive.py index 11e9d155f76..a80fdf2a732 100644 --- a/files/archive.py +++ b/files/archive.py @@ -315,16 +315,16 @@ def main(): changed = True else: size = 0 - f_in = f_out = archive = None + f_in = f_out = arcfile = None if os.path.lexists(dest): size = os.path.getsize(dest) try: if compression == 'zip': - archive = zipfile.ZipFile(dest, 'w', zipfile.ZIP_DEFLATED) - archive.write(path, path[len(arcroot):]) - archive.close() + arcfile = zipfile.ZipFile(dest, 'w', zipfile.ZIP_DEFLATED) + arcfile.write(path, path[len(arcroot):]) + arcfile.close() state = 'archive' # because all zip files are archives else: @@ -346,8 +346,8 @@ def main(): module.fail_json(path=path, dest=dest, msg='Unable to write to compressed file: %s' % str(e)) - if archive: - archive.close() + if arcfile: + arcfile.close() if f_in: f_in.close() if f_out: From ddfd32774bf54500317a7ac86ecc867390b9f84e Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Wed, 1 Jun 2016 08:29:28 -0400 Subject: [PATCH 1637/2522] Don't try to walk over files when building archive --- files/archive.py | 59 ++++++++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/files/archive.py b/files/archive.py index a80fdf2a732..756fea4a3e1 100644 --- a/files/archive.py +++ b/files/archive.py @@ -230,40 +230,49 @@ def main(): arcfile = tarfile.open(dest, 'w|' + compression) for path in archive_paths: - basename = '' - - # Prefix trees in the archive with their basename, unless specifically prevented with '.' - if os.path.isdir(path) and not path.endswith(os.sep + '.'): - basename = os.path.basename(path) + os.sep - - for dirpath, dirnames, filenames in os.walk(path, topdown=True): - for dirname in dirnames: - fullpath = dirpath + os.sep + dirname - - try: - if compression == 'zip': - arcfile.write(fullpath, basename + dirname) - else: - arcfile.add(fullpath, basename + dirname, recursive=False) + if os.path.isdir(path): + basename = '' - except Exception: - e = get_exception() - errors.append('%s: %s' % (fullpath, str(e))) + # Prefix trees in the archive with their basename, unless specifically prevented with '.' + if not path.endswith(os.sep + '.'): + basename = os.path.basename(path) + os.sep - for filename in filenames: - fullpath = dirpath + os.sep + filename + # Recurse into directories + for dirpath, dirnames, filenames in os.walk(path, topdown=True): + for dirname in dirnames: + fullpath = dirpath + os.sep + dirname - if not filecmp.cmp(fullpath, dest): try: if compression == 'zip': - arcfile.write(fullpath, basename + filename) + arcfile.write(fullpath, basename + dirname) else: - arcfile.add(fullpath, basename + filename, recursive=False) + arcfile.add(fullpath, basename + dirname, recursive=False) - successes.append(fullpath) except Exception: e = get_exception() - errors.append('Adding %s: %s' % (path, str(e))) + errors.append('%s: %s' % (fullpath, str(e))) + + for filename in filenames: + fullpath = dirpath + os.sep + filename + + if not filecmp.cmp(fullpath, dest): + try: + if compression == 'zip': + arcfile.write(fullpath, basename + filename) + else: + arcfile.add(fullpath, basename + filename, recursive=False) + + successes.append(fullpath) + except Exception: + e = get_exception() + errors.append('Adding %s: %s' % (path, str(e))) + else: + if compression == 'zip': + arcfile.write(path, path[len(arcroot):]) + else: + arcfile.add(path, path[len(arcroot):], recursive=False) + + successes.append(path) except Exception: e = get_exception() From a38b510aa80a856d6d59986372150a210504b0b1 Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Wed, 1 Jun 2016 08:59:28 -0400 Subject: [PATCH 1638/2522] Refactor computation of archive filenames, clearer archive filename --- files/archive.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/files/archive.py b/files/archive.py index 756fea4a3e1..26394b03ad2 100644 --- a/files/archive.py +++ b/files/archive.py @@ -231,36 +231,35 @@ def main(): for path in archive_paths: if os.path.isdir(path): - basename = '' - - # Prefix trees in the archive with their basename, unless specifically prevented with '.' - if not path.endswith(os.sep + '.'): - basename = os.path.basename(path) + os.sep - # Recurse into directories for dirpath, dirnames, filenames in os.walk(path, topdown=True): + if not dirpath.endswith(os.sep): + dirpath += os.sep + for dirname in dirnames: - fullpath = dirpath + os.sep + dirname + fullpath = dirpath + dirname + arcname = fullpath[len(arcroot):] try: if compression == 'zip': - arcfile.write(fullpath, basename + dirname) + arcfile.write(fullpath, arcname) else: - arcfile.add(fullpath, basename + dirname, recursive=False) + arcfile.add(fullpath, arcname, recursive=False) except Exception: e = get_exception() errors.append('%s: %s' % (fullpath, str(e))) for filename in filenames: - fullpath = dirpath + os.sep + filename + fullpath = dirpath + filename + arcname = fullpath[len(arcroot):] if not filecmp.cmp(fullpath, dest): try: if compression == 'zip': - arcfile.write(fullpath, basename + filename) + arcfile.write(fullpath, arcname) else: - arcfile.add(fullpath, basename + filename, recursive=False) + arcfile.add(fullpath, arcname, recursive=False) successes.append(fullpath) except Exception: From 07ca593c80d4b996cca7e4e2fe0b4f45a4745644 Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Wed, 1 Jun 2016 09:07:18 -0400 Subject: [PATCH 1639/2522] expanduser() on dest --- files/archive.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/files/archive.py b/files/archive.py index 26394b03ad2..96658461e9a 100644 --- a/files/archive.py +++ b/files/archive.py @@ -157,7 +157,9 @@ def main(): # Default created file name (for single-file archives) to # . - if not archive and not dest: + if dest: + dest = os.path.expanduser(dest) + elif not archive: dest = '%s.%s' % (expanded_paths[0], compression) # Force archives to specify 'dest' From 4497cb487443ea500d596180518cb63b3e8b3045 Mon Sep 17 00:00:00 2001 From: Leandro Lisboa Penz Date: Wed, 1 Jun 2016 13:17:38 -0300 Subject: [PATCH 1640/2522] netconf module with edit-config operation (#2103) * netconf module with edit-config operation --- network/netconf/__init__.py | 0 network/netconf/netconf_config.py | 221 ++++++++++++++++++++++++++++++ 2 files changed, 221 insertions(+) create mode 100644 network/netconf/__init__.py create mode 100755 network/netconf/netconf_config.py diff --git a/network/netconf/__init__.py b/network/netconf/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/network/netconf/netconf_config.py b/network/netconf/netconf_config.py new file mode 100755 index 00000000000..43baa63a5da --- /dev/null +++ b/network/netconf/netconf_config.py @@ -0,0 +1,221 @@ +#!/usr/bin/python + +# (c) 2016, Leandro Lisboa Penz +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: netconf_config +author: "Leandro Lisboa Penz (@lpenz)" +short_description: netconf device configuration +description: + - Netconf is a network management protocol developed and standardized by + the IETF. It is documented in RFC 6241. + + - This module allows the user to send a configuration XML file to a netconf + device, and detects if there was a configuration change. +notes: + - This module supports devices with and without the the candidate and + confirmed-commit capabilities. It always use the safer feature. +version_added: "2.2" +options: + host: + description: + - the hostname or ip address of the netconf device + required: true + port: + description: + - the netconf port + default: 830 + required: false + hostkey_verify: + description: + - if true, the ssh host key of the device must match a ssh key present on the host + - if false, the ssh host key of the device is not checked + default: true + required: false + username: + description: + - the username to authenticate with + required: true + password: + description: + - password of the user to authenticate with + required: true + xml: + description: + - the XML content to send to the device + required: true + + +requirements: + - "python >= 2.6" + - "ncclient" +''' + +EXAMPLES = ''' +- name: set ntp server in the device + netconf_config: + host: 10.0.0.1 + username: admin + password: admin + xml: | + + + + true + + ntp1 +
127.0.0.1
+
+
+
+
+ +- name: wipe ntp configuration + netconf_config: + host: 10.0.0.1 + username: admin + password: admin + xml: | + + + + false + + ntp1 + + + + + +''' + +RETURN = ''' +server_capabilities: + description: list of capabilities of the server + returned: success + type: list of strings + sample: ['urn:ietf:params:netconf:base:1.1','urn:ietf:params:netconf:capability:confirmed-commit:1.0','urn:ietf:params:netconf:capability:candidate:1.0'] + +''' + +import xml.dom.minidom +try: + import ncclient.manager + HAS_NCCLIENT = True +except ImportError: + HAS_NCCLIENT = False + + +import logging + + +def netconf_edit_config(m, xml, commit, retkwargs): + if ":candidate" in m.server_capabilities: + datastore = 'candidate' + else: + datastore = 'running' + m.lock(target=datastore) + try: + m.discard_changes() + config_before = m.get_config(source=datastore) + m.edit_config(target=datastore, config=xml) + config_after = m.get_config(source=datastore) + changed = config_before.data_xml != config_after.data_xml + if changed and commit: + if ":confirmed-commit" in m.server_capabilities: + m.commit(confirmed=True) + m.commit() + else: + m.commit() + return changed + finally: + m.unlock(target=datastore) + + +# ------------------------------------------------------------------- # +# Main + + +def main(): + + module = AnsibleModule( + argument_spec=dict( + host=dict(type='str', required=True), + port=dict(type='int', default=830), + hostkey_verify=dict(type='bool', default=True), + username=dict(type='str', required=True, no_log=True), + password=dict(type='str', required=True, no_log=True), + xml=dict(type='str', required=True), + ) + ) + + if not HAS_NCCLIENT: + module.fail_json(msg='could not import the python library ' + 'ncclient required by this module') + + try: + xml.dom.minidom.parseString(module.params['xml']) + except: + e = get_exception() + module.fail_json( + msg='error parsing XML: ' + + str(e) + ) + return + + nckwargs = dict( + host=module.params['host'], + port=module.params['port'], + hostkey_verify=module.params['hostkey_verify'], + username=module.params['username'], + password=module.params['password'], + ) + retkwargs = dict() + + try: + m = ncclient.manager.connect(**nckwargs) + except ncclient.transport.errors.AuthenticationError: + module.fail_json( + msg='authentication failed while connecting to device' + ) + except: + e = get_exception() + module.fail_json( + msg='error connecting to the device: ' + + str(e) + ) + return + retkwargs['server_capabilities'] = list(m.server_capabilities) + try: + changed = netconf_edit_config( + m=m, + xml=module.params['xml'], + commit=True, + retkwargs=retkwargs, + ) + finally: + m.close_session() + module.exit_json(changed=changed, **retkwargs) + + +# import module snippets +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From 3d6da378640fd18c9307044f3e7a5129017429af Mon Sep 17 00:00:00 2001 From: trondhindenes Date: Wed, 1 Jun 2016 21:18:56 +0200 Subject: [PATCH 1641/2522] azure_rm_deploy docfix (#2354) --- cloud/azure/azure_rm_deployment.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cloud/azure/azure_rm_deployment.py b/cloud/azure/azure_rm_deployment.py index 2d72436232f..dea06276775 100644 --- a/cloud/azure/azure_rm_deployment.py +++ b/cloud/azure/azure_rm_deployment.py @@ -37,6 +37,15 @@ - The geo-locations in which the resource group will be located. required: false default: westus + deployment_mode: + description: + - In incremental mode, resources are deployed without deleting existing resources that are not included in the template. + In complete mode resources are deployed and existing resources in the resource group not included in the template are deleted. + required: false + default: complete + choices: + - complete + - incremental state: description: - If state is "present", template will be created. If state is "present" and if deployment exists, it will be From bb6504c8d351ea188712c6c4976fe521ec3f189c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Gr=C3=BCner?= Date: Wed, 1 Jun 2016 23:02:05 +0200 Subject: [PATCH 1642/2522] Add Let's Encrypt module to manage ssl certificates (#1962) Fixes #1545 --- web_infrastructure/letsencrypt.py | 785 ++++++++++++++++++++++++++++++ 1 file changed, 785 insertions(+) create mode 100644 web_infrastructure/letsencrypt.py diff --git a/web_infrastructure/letsencrypt.py b/web_infrastructure/letsencrypt.py new file mode 100644 index 00000000000..35d521a8509 --- /dev/null +++ b/web_infrastructure/letsencrypt.py @@ -0,0 +1,785 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016 Michael Gruener +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import binascii +import copy +import textwrap +from datetime import datetime + +DOCUMENTATION = ''' +--- +module: letsencrypt +author: "Michael Gruener (@mgruener)" +version_added: "2.2" +short_description: Create SSL certificates with Let's Encrypt +description: + - "Create and renew SSL certificates with Let's Encrypt. Let’s Encrypt is a + free, automated, and open certificate authority (CA), run for the + public’s benefit. For details see U(https://letsencrypt.org). The current + implementation supports the http-01, tls-sni-02 and dns-01 challenges." + - "To use this module, it has to be executed at least twice. Either as two + different tasks in the same run or during multiple runs." + - "Between these two tasks you have to fulfill the required steps for the + choosen challenge by whatever means necessary. For http-01 that means + creating the necessary challenge file on the destination webserver. For + dns-01 the necessary dns record has to be created. tls-sni-02 requires + you to create a SSL certificate with the appropriate subjectAlternativeNames. + It is I(not) the responsibility of this module to perform these steps." + - "For details on how to fulfill these challenges, you might have to read through + U(https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-7)" + - "Although the defaults are choosen so that the module can be used with + the Let's Encrypt CA, the module can be used with any service using the ACME + protocol." +options: + account_key: + description: + - "File containing the the Let's Encrypt account RSA key." + - "Can be created with C(openssl rsa ...)." + required: true + account_email: + description: + - "The email address associated with this account." + - "It will be used for certificate expiration warnings." + required: false + default: null + acme_directory: + description: + - "The ACME directory to use. This is the entry point URL to access + CA server API." + - "For safety reasons the default is set to the Let's Encrypt staging server. + This will create technically correct, but untrusted certifiactes." + required: false + default: https://acme-staging.api.letsencrypt.org/directory + agreement: + description: + - "URI to a terms of service document you agree to when using the + ACME service at C(acme_directory)." + required: false + default: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf' + challenge: + description: The challenge to be performed. + required: false + choices: [ 'http-01', 'dns-01', 'tls-sni-02'] + default: 'http-01' + csr: + description: + - "File containing the CSR for the new certificate." + - "Can be created with C(openssl csr ...)." + - "The CSR may contain multiple Subject Alternate Names, but each one + will lead to an individual challenge that must be fulfilled for the + CSR to be signed." + required: true + alias: ['src'] + data: + description: + - "The data to validate ongoing challenges." + - "The value that must be used here will be provided by a previous use + of this module." + required: false + default: null + dest: + description: The destination file for the certificate. + required: true + alias: ['cert'] + remaining_days: + description: + - "The number of days the certificate must have left being valid before it + will be renewed." + required: false + default: 10 +''' + +EXAMPLES = ''' +- letsencrypt: + account_key: /etc/pki/cert/private/account.key + csr: /etc/pki/cert/csr/sample.com.csr + dest: /etc/httpd/ssl/sample.com.crt + register: sample_com_challenge + +# perform the necessary steps to fulfill the challenge +# for example: +# +# - copy: +# dest: /var/www/html/{{ sample_com_http_challenge['challenge_data']['sample.com']['http-01']['resource'] }} +# content: "{{ sample_com_http_challenge['challenge_data']['sample.com']['http-01']['resource_value'] }}" +# when: sample_com_challenge|changed + +- letsencrypt: + account_key: /etc/pki/cert/private/account.key + csr: /etc/pki/cert/csr/sample.com.csr + dest: /etc/httpd/ssl/sample.com.crt + data: "{{ sample_com_challenge }}" +''' + +RETURN = ''' +cert_days: + description: the number of days the certificate remains valid. + returned: success +challenge_data: + description: per domain / challenge type challenge data + returned: changed + type: dictionary + contains: + resource: + description: the challenge resource that must be created for validation + returned: changed + type: string + sample: .well-known/acme-challenge/evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA + resource_value: + description: the value the resource has to produce for the validation + returned: changed + type: string + sample: IlirfxKKXA...17Dt3juxGJ-PCt92wr-oA +authorizations: + description: ACME authorization data. + returned: changed + type: list + contains: + authorization: + description: ACME authorization object. See https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.1.2 + returned: success + type: dict +''' + +def nopad_b64(data): + return base64.urlsafe_b64encode(data).decode('utf8').replace("=", "") + +def simple_get(module,url): + resp, info = fetch_url(module, url, method='GET') + + result = None + try: + content = resp.read() + if info['content-type'].startswith('application/json'): + result = module.from_json(content.decode('utf8')) + else: + result = content + except AttributeError: + result = None + except ValueError: + module.fail_json(msg="Failed to parse the ACME response: {0} {1}".format(url,content)) + + if info['status'] >= 400: + module.fail_json(msg="ACME request failed: CODE: {0} RESULT:{1}".format(info['status'],result)) + return result + +def get_cert_days(module,cert_file): + ''' + Return the days the certificate in cert_file remains valid and -1 + if the file was not found. + ''' + _cert_file = os.path.expanduser(cert_file) + if not os.path.exists(_cert_file): + return -1 + + openssl_bin = module.get_bin_path('openssl', True) + openssl_cert_cmd = [openssl_bin, "x509", "-in", _cert_file, "-noout", "-text"] + _, out, _ = module.run_command(openssl_cert_cmd,check_rc=True) + try: + not_after_str = re.search(r"\s+Not After\s*:\s+(.*)",out.decode('utf8')).group(1) + not_after = datetime.datetime.fromtimestamp(time.mktime(time.strptime(not_after_str,'%b %d %H:%M:%S %Y %Z'))) + except AttributeError: + module.fail_json(msg="No 'Not after' date found in {0}".format(cert_file)) + except ValueError: + module.fail_json(msg="Faild to parse 'Not after' date of {0}".format(cert_file)) + now = datetime.datetime.utcnow() + return (not_after - now).days + +# function source: network/basics/uri.py +def write_file(module, dest, content): + ''' + Write content to destination file dest, only if the content + has changed. + ''' + changed = False + # create a tempfile with some test content + _, tmpsrc = tempfile.mkstemp() + f = open(tmpsrc, 'wb') + try: + f.write(content) + except Exception, err: + os.remove(tmpsrc) + module.fail_json(msg="failed to create temporary content file: %s" % str(err)) + f.close() + checksum_src = None + checksum_dest = None + # raise an error if there is no tmpsrc file + if not os.path.exists(tmpsrc): + os.remove(tmpsrc) + module.fail_json(msg="Source %s does not exist" % (tmpsrc)) + if not os.access(tmpsrc, os.R_OK): + os.remove(tmpsrc) + module.fail_json( msg="Source %s not readable" % (tmpsrc)) + checksum_src = module.sha1(tmpsrc) + # check if there is no dest file + if os.path.exists(dest): + # raise an error if copy has no permission on dest + if not os.access(dest, os.W_OK): + os.remove(tmpsrc) + module.fail_json(msg="Destination %s not writable" % (dest)) + if not os.access(dest, os.R_OK): + os.remove(tmpsrc) + module.fail_json(msg="Destination %s not readable" % (dest)) + checksum_dest = module.sha1(dest) + else: + if not os.access(os.path.dirname(dest), os.W_OK): + os.remove(tmpsrc) + module.fail_json(msg="Destination dir %s not writable" % (os.path.dirname(dest))) + if checksum_src != checksum_dest: + try: + shutil.copyfile(tmpsrc, dest) + changed = True + except Exception, err: + os.remove(tmpsrc) + module.fail_json(msg="failed to copy %s to %s: %s" % (tmpsrc, dest, str(err))) + os.remove(tmpsrc) + return changed + +class ACMEDirectory(object): + ''' + The ACME server directory. Gives access to the available resources + and the Replay-Nonce for a given uri. This only works for + uris that permit GET requests (so normally not the ones that + require authentication). + https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.2 + ''' + def __init__(self, module): + self.module = module + self.directory_root = module.params['acme_directory'] + + self.directory = simple_get(self.module,self.directory_root) + + def __getitem__(self, key): return self.directory[key] + + def get_nonce(self,resource=None): + url = self.directory_root + if resource is not None: + url = resource + _, info = fetch_url(self.module, url, method='HEAD') + if info['status'] != 200: + self.module.fail_json(msg="Failed to get replay-nonce, got status {0}".format(info['status'])) + return info['replay-nonce'] + +class ACMEAccount(object): + ''' + ACME account object. Handles the authorized communication with the + ACME server. Provides access to accound bound information like + the currently active authorizations and valid certificates + ''' + def __init__(self,module): + self.module = module + self.agreement = module.params['agreement'] + self.key = os.path.expanduser(module.params['account_key']) + self.email = module.params['account_email'] + self.data = module.params['data'] + self.directory = ACMEDirectory(module) + self.uri = None + self.changed = False + + self._authz_list_uri = None + self._certs_list_uri = None + + if not os.path.exists(self.key): + module.fail_json(msg="Account key %s not found" % (self.key)) + + self._openssl_bin = module.get_bin_path('openssl', True) + + pub_hex, pub_exp = self._parse_account_key(self.key) + self.jws_header = { + "alg": "RS256", + "jwk": { + "e": nopad_b64(binascii.unhexlify(pub_exp.encode("utf-8"))), + "kty": "RSA", + "n": nopad_b64(binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))), + }, + } + self.init_account() + + def get_keyauthorization(self,token): + ''' + Returns the key authorization for the given token + https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-7.1 + ''' + accountkey_json = json.dumps(self.jws_header['jwk'], sort_keys=True, separators=(',', ':')) + thumbprint = nopad_b64(hashlib.sha256(accountkey_json.encode('utf8')).digest()) + return "{0}.{1}".format(token, thumbprint) + + def _parse_account_key(self,key): + ''' + Parses an RSA key file in PEM format and returns the modulus + and public exponent of the key + ''' + openssl_keydump_cmd = [self._openssl_bin, "rsa", "-in", key, "-noout", "-text"] + _, out, _ = self.module.run_command(openssl_keydump_cmd,check_rc=True) + + pub_hex, pub_exp = re.search( + r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)", + out.decode('utf8'), re.MULTILINE|re.DOTALL).groups() + pub_exp = "{0:x}".format(int(pub_exp)) + if len(pub_exp) % 2: + pub_exp = "0{0}".format(pub_exp) + + return pub_hex, pub_exp + + def send_signed_request(self, url, payload): + ''' + Sends a JWS signed HTTP POST request to the ACME server and returns + the response as dictionary + https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-5.2 + ''' + protected = copy.deepcopy(self.jws_header) + protected["nonce"] = self.directory.get_nonce() + + try: + payload64 = nopad_b64(self.module.jsonify(payload).encode('utf8')) + protected64 = nopad_b64(self.module.jsonify(protected).encode('utf8')) + except Exception, e: + self.module.fail_json(msg="Failed to encode payload / headers as JSON: {0}".format(e)) + + openssl_sign_cmd = [self._openssl_bin, "dgst", "-sha256", "-sign", self.key] + sign_payload = "{0}.{1}".format(protected64, payload64).encode('utf8') + _, out, _ = self.module.run_command(openssl_sign_cmd,data=sign_payload,check_rc=True, binary_data=True) + + data = self.module.jsonify({ + "header": self.jws_header, + "protected": protected64, + "payload": payload64, + "signature": nopad_b64(out), + }) + + resp, info = fetch_url(self.module, url, data=data, method='POST') + result = None + try: + content = resp.read() + if info['content-type'].startswith('application/json'): + result = self.module.from_json(content.decode('utf8')) + else: + result = content + except AttributeError: + result = None + except ValueError: + self.module.fail_json(msg="Failed to parse the ACME response: {0} {1}".format(url,content)) + + return result,info + + def _new_reg(self,contact=[]): + ''' + Registers a new ACME account. Returns True if the account was + created and False if it already existed (e.g. it was not newly + created) + https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.3 + ''' + if self.uri is not None: + return True + + new_reg = { + 'resource': 'new-reg', + 'agreement': self.agreement, + 'contact': contact + } + + result, info = self.send_signed_request(self.directory['new-reg'], new_reg) + if 'location' in info: + self.uri = info['location'] + + if info['status'] in [200,201]: + # Account did not exist + self.changed = True + return True + elif info['status'] == 409: + # Account did exist + return False + else: + self.module.fail_json(msg="Error registering: {0} {1}".format(info['status'], result)) + + def init_account(self): + ''' + Create or update an account on the ACME server. As the only way + (without knowing an account URI) to test if an account exists + is to try and create one with the provided account key, this + method will always result in an account being present (except + on error situations). If the account already exists, it will + update the contact information. + https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.3 + ''' + + contact = [] + if self.email: + contact.append('mailto:' + self.email) + + # if this is not a new registration (e.g. existing account) + if not self._new_reg(contact): + # pre-existing account, get account data... + result, _ = self.send_signed_request(self.uri, {'resource':'reg'}) + + # XXX: letsencrypt/boulder#1435 + if 'authorizations' in result: + self._authz_list_uri = result['authorizations'] + if 'certificates' in result: + self._certs_list_uri = result['certificates'] + + # ...and check if update is necessary + do_update = False + if 'contact' in result: + if cmp(contact,result['contact']) != 0: + do_update = True + elif len(contact) > 0: + do_update = True + + if do_update: + upd_reg = result + upd_reg['contact'] = contact + result, _ = self.send_signed_request(self.uri, upd_reg) + self.changed = True + + def get_authorizations(self): + ''' + Return a list of currently active authorizations + https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.4 + ''' + authz_list = {'authorizations': []} + if self._authz_list_uri is None: + # XXX: letsencrypt/boulder#1435 + # Workaround, retrieve the known authorization urls + # from the data attribute + # It is also a way to limit the queried authorizations, which + # might become relevant at some point + if (self.data is not None) and ('authorizations' in self.data): + for auth in self.data['authorizations']: + authz_list['authorizations'].append(auth['uri']) + else: + return [] + else: + # TODO: need to handle pagination + authz_list = simple_get(self.module, self._authz_list_uri) + + authz = [] + for auth_uri in authz_list['authorizations']: + auth = simple_get(self.module,auth_uri) + auth['uri'] = auth_uri + authz.append(auth) + + return authz + +class ACMEClient(object): + ''' + ACME client class. Uses an ACME account object and a CSR to + start and validate ACME challenges and download the respective + certificates. + ''' + def __init__(self,module): + self.module = module + self.challenge = module.params['challenge'] + self.csr = os.path.expanduser(module.params['csr']) + self.dest = os.path.expanduser(module.params['dest']) + self.account = ACMEAccount(module) + self.directory = self.account.directory + self.authorizations = self.account.get_authorizations() + self.cert_days = -1 + self.changed = self.account.changed + + if not os.path.exists(self.csr): + module.fail_json(msg="CSR %s not found" % (self.csr)) + + self._openssl_bin = module.get_bin_path('openssl', True) + self.domains = self._get_csr_domains() + + def _get_csr_domains(self): + ''' + Parse the CSR and return the list of requested domains + ''' + openssl_csr_cmd = [self._openssl_bin, "req", "-in", self.csr, "-noout", "-text"] + _, out, _ = self.module.run_command(openssl_csr_cmd,check_rc=True) + + domains = set([]) + common_name = re.search(r"Subject:.*? CN=([^\s,;/]+)", out.decode('utf8')) + if common_name is not None: + domains.add(common_name.group(1)) + subject_alt_names = re.search(r"X509v3 Subject Alternative Name: \n +([^\n]+)\n", out.decode('utf8'), re.MULTILINE|re.DOTALL) + if subject_alt_names is not None: + for san in subject_alt_names.group(1).split(", "): + if san.startswith("DNS:"): + domains.add(san[4:]) + return domains + + + def _get_domain_auth(self,domain): + ''' + Get the status string of the first authorization for the given domain. + Return None if no active authorization for the given domain was found. + ''' + if self.authorizations is None: + return None + + for auth in self.authorizations: + if (auth['identifier']['type'] == 'dns') and (auth['identifier']['value'] == domain): + return auth + return None + + def _add_or_update_auth(self,auth): + ''' + Add or update the given authroization in the global authorizations list. + Return True if the auth was updated/added and False if no change was + necessary. + ''' + for index,cur_auth in enumerate(self.authorizations): + if (cur_auth['uri'] == auth['uri']): + # does the auth parameter contain updated data? + if cmp(cur_auth,auth) != 0: + # yes, update our current authorization list + self.authorizations[index] = auth + return True + else: + return False + # this is a new authorization, add it to the list of current + # authorizations + self.authorizations.append(auth) + return True + + def _new_authz(self,domain): + ''' + Create a new authorization for the given domain. + Return the authorization object of the new authorization + https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.4 + ''' + if self.account.uri is None: + return + + new_authz = { + "resource": "new-authz", + "identifier": {"type": "dns", "value": domain}, + } + + result, info = self.account.send_signed_request(self.directory['new-authz'], new_authz) + if info['status'] not in [200,201]: + self.module.fail_json(msg="Error requesting challenges: CODE: {0} RESULT: {1}".format(info['status'], result)) + else: + result['uri'] = info['location'] + return result + + def _get_challenge_data(self,auth): + ''' + Returns a dict with the data for all proposed (and supported) challenges + of the given authorization. + ''' + + data = {} + # no need to choose a specific challenge here as this module + # is not responsible for fulfilling the challenges. Calculate + # and return the required information for each challenge. + for challenge in auth['challenges']: + type = challenge['type'] + token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge['token']) + keyauthorization = self.account.get_keyauthorization(token) + + # NOTE: tls-sni-01 is not supported by choice + # too complex to be usefull and tls-sni-02 is an alternative + # as soon as it is implemented server side + if type == 'http-01': + # https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-7.2 + resource = '.well-known/acme-challenge/' + token + value = keyauthorization + elif type == 'tls-sni-02': + # https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-7.3 + token_digest = hashlib.sha256(token.encode('utf8')).hexdigest() + ka_digest = hashlib.sha256(keyauthorization.encode('utf8')).hexdigest() + len_token_digest = len(token_digest) + len_ka_digest = len(ka_digest) + resource = 'subjectAlternativeNames' + value = [ + "{0}.{1}.token.acme.invalid".format(token_digest[:len_token_digest/2],token_digest[len_token_digest/2:]), + "{0}.{1}.ka.acme.invalid".format(ka_digest[:len_ka_digest/2],ka_digest[len_ka_digest/2:]), + ] + elif type == 'dns-01': + # https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-7.4 + resource = '_acme-challenge' + value = nopad_b64(hashlib.sha256(keyauthorization).digest()).encode('utf8') + else: + continue + + data[type] = { 'resource': resource, 'resource_value': value } + return data + + def _validate_challenges(self,auth): + ''' + Validate the authorization provided in the auth dict. Returns True + when the validation was successfull and False when it was not. + ''' + for challenge in auth['challenges']: + if self.challenge != challenge['type']: + continue + + uri = challenge['uri'] + token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge['token']) + keyauthorization = self.account.get_keyauthorization(token) + + challenge_response = { + "resource": "challenge", + "keyAuthorization": keyauthorization, + } + result, info = self.account.send_signed_request(uri, challenge_response) + if info['status'] != 200: + self.module.fail_json(msg="Error validating challenge: CODE: {0} RESULT: {1}".format(info['status'], result)) + + status = '' + + while status not in ['valid','invalid','revoked']: + result = simple_get(self.module,auth['uri']) + result['uri'] = auth['uri'] + if self._add_or_update_auth(result): + self.changed = True + # draft-ietf-acme-acme-02 + # "status (required, string): ... + # If this field is missing, then the default value is "pending"." + if 'status' not in result: + status = 'pending' + else: + status = result['status'] + time.sleep(2) + + if status == 'invalid': + error_details = '' + # multiple challenges could have failed at this point, gather error + # details for all of them before failing + for challenge in result['challenges']: + if challenge['status'] == 'invalid': + error_details += ' CHALLENGE: {0}'.format(challenge['type']) + if 'error' in challenge: + error_details += ' DETAILS: {0};'.format(challenge['error']['detail']) + else: + error_details += ';' + self.module.fail_json(msg="Authorization for {0} returned invalid: {1}".format(result['identifier']['value'],error_details)) + + return status == 'valid' + + def _new_cert(self): + ''' + Create a new certificate based on the csr. + Return the certificate object as dict + https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.5 + ''' + openssl_csr_cmd = [self._openssl_bin, "req", "-in", self.csr, "-outform", "DER"] + _, out, _ = self.module.run_command(openssl_csr_cmd,check_rc=True) + + new_cert = { + "resource": "new-cert", + "csr": nopad_b64(out), + } + result, info = self.account.send_signed_request(self.directory['new-cert'], new_cert) + if info['status'] not in [200,201]: + self.module.fail_json(msg="Error new cert: CODE: {0} RESULT: {1}".format(info['status'], result)) + else: + return {'cert': result, 'uri': info['location']} + + def _der_to_pem(self,der_cert): + ''' + Convert the DER format certificate in der_cert to a PEM format + certificate and return it. + ''' + return """-----BEGIN CERTIFICATE-----\n{0}\n-----END CERTIFICATE-----\n""".format( + "\n".join(textwrap.wrap(base64.b64encode(der_cert).decode('utf8'), 64))) + + def do_challenges(self): + ''' + Create new authorizations for all domains of the CSR and return + the challenge details for the choosen challenge type. + ''' + data = {} + for domain in self.domains: + auth = self._get_domain_auth(domain) + if auth is None: + new_auth = self._new_authz(domain) + self._add_or_update_auth(new_auth) + data[domain] = self._get_challenge_data(new_auth) + self.changed = True + elif (auth['status'] == 'pending') or ('status' not in auth): + # draft-ietf-acme-acme-02 + # "status (required, string): ... + # If this field is missing, then the default value is "pending"." + self._validate_challenges(auth) + # _validate_challenges updates the global authrozation dict, + # so get the current version of the authorization we are working + # on to retrieve the challenge data + data[domain] = self._get_challenge_data(self._get_domain_auth(domain)) + + return data + + def get_certificate(self): + ''' + Request a new certificate and write it to the destination file. + Only do this if a destination file was provided and if all authorizations + for the domains of the csr are valid. No Return value. + ''' + if self.dest is None: + return + + for domain in self.domains: + auth = self._get_domain_auth(domain) + if auth is None or ('status' not in auth) or (auth['status'] != 'valid'): + return + + cert = self._new_cert() + if cert['cert'] is not None: + pem_cert = self._der_to_pem(cert['cert']) + if write_file(self.module,self.dest,pem_cert): + self.cert_days = get_cert_days(self.module,self.dest) + self.changed = True + +def main(): + module = AnsibleModule( + argument_spec = dict( + account_key = dict(required=True, type='str'), + account_email = dict(required=False, default=None, type='str'), + acme_directory = dict(required=False, default='https://acme-staging.api.letsencrypt.org/directory', type='str'), + agreement = dict(required=False, default='https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf', type='str'), + challenge = dict(required=False, default='http-01', choices=['http-01', 'dns-01', 'tls-sni-02'], type='str'), + csr = dict(required=True, aliases=['src'], type='str'), + data = dict(required=False, no_log=True, default=None, type='dict'), + dest = dict(required=True, aliases=['cert'], type='str'), + remaining_days = dict(required=False, default=10, type='int'), + ), + supports_check_mode = True, + ) + + cert_days = get_cert_days(module,module.params['dest']) + if cert_days < module.params['remaining_days']: + # If checkmode is active, base the changed state solely on the status + # of the certificate file as all other actions (accessing an account, checking + # the authorization status...) would lead to potential changes of the current + # state + if module.check_mode: + module.exit_json(changed=True,authorizations={}, + challenge_data={},cert_days=cert_days) + else: + client = ACMEClient(module) + client.cert_days = cert_days + data = client.do_challenges() + client.get_certificate() + module.exit_json(changed=client.changed,authorizations=client.authorizations, + challenge_data=data,cert_days=client.cert_days) + else: + module.exit_json(changed=False,cert_days=cert_days) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * + +if __name__ == '__main__': + main() From 4874ceefa59ac85a57943ac59c90c3b23d7550f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Thu, 2 Jun 2016 09:31:08 +0200 Subject: [PATCH 1643/2522] letsencrypt: fix tests failures (#2360) --- .travis.yml | 2 +- web_infrastructure/letsencrypt.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9b54c2ddbe0..f9e21227fcf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -140,7 +140,7 @@ install: - pip install git+https://github.com/ansible/ansible.git@devel#egg=ansible - pip install git+https://github.com/sivel/ansible-testing.git#egg=ansible_testing script: - - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py|database/influxdb/influxdb.*\.py|database/mssql/mssql_db\.py' . + - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py|database/influxdb/influxdb.*\.py|database/mssql/mssql_db\.py|/letsencrypt\.py' . - python2.6 -m compileall -fq . - python2.7 -m compileall -fq . - python3.4 -m compileall -fq . -x $(echo "$PY3_EXCLUDE_LIST"| tr ' ' '|') diff --git a/web_infrastructure/letsencrypt.py b/web_infrastructure/letsencrypt.py index 35d521a8509..81cbb28e518 100644 --- a/web_infrastructure/letsencrypt.py +++ b/web_infrastructure/letsencrypt.py @@ -47,6 +47,8 @@ - "Although the defaults are choosen so that the module can be used with the Let's Encrypt CA, the module can be used with any service using the ACME protocol." +requirements: + - "python >= 2.6" options: account_key: description: @@ -214,7 +216,7 @@ def write_file(module, dest, content): f = open(tmpsrc, 'wb') try: f.write(content) - except Exception, err: + except Exception as err: os.remove(tmpsrc) module.fail_json(msg="failed to create temporary content file: %s" % str(err)) f.close() @@ -246,7 +248,7 @@ def write_file(module, dest, content): try: shutil.copyfile(tmpsrc, dest) changed = True - except Exception, err: + except Exception as err: os.remove(tmpsrc) module.fail_json(msg="failed to copy %s to %s: %s" % (tmpsrc, dest, str(err))) os.remove(tmpsrc) @@ -350,7 +352,7 @@ def send_signed_request(self, url, payload): try: payload64 = nopad_b64(self.module.jsonify(payload).encode('utf8')) protected64 = nopad_b64(self.module.jsonify(protected).encode('utf8')) - except Exception, e: + except Exception as e: self.module.fail_json(msg="Failed to encode payload / headers as JSON: {0}".format(e)) openssl_sign_cmd = [self._openssl_bin, "dgst", "-sha256", "-sign", self.key] From d0568790886f7d3973b31b55c5e045152514c604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Thu, 2 Jun 2016 09:46:05 +0200 Subject: [PATCH 1644/2522] travis: workaround false negative test failure (#2362) travis: workaround false negative test failure --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f9e21227fcf..21a0637fc6c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -145,5 +145,5 @@ script: - python2.7 -m compileall -fq . - python3.4 -m compileall -fq . -x $(echo "$PY3_EXCLUDE_LIST"| tr ' ' '|') - python3.5 -m compileall -fq . -x $(echo "$PY3_EXCLUDE_LIST"| tr ' ' '|') - - ansible-validate-modules . + - ansible-validate-modules . --exclude 'cloud/azure/azure_rm_deployment\.py' #- ./test-docs.sh extras From 473e5d7969d2ada634030f3ce769c50dba7ce2e8 Mon Sep 17 00:00:00 2001 From: Nicolas Boutet Date: Thu, 2 Jun 2016 16:55:27 +0900 Subject: [PATCH 1645/2522] Fix typo in documentation (#2359) --- cloud/amazon/route53_zone.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/route53_zone.py b/cloud/amazon/route53_zone.py index ca8034fac07..fa48b18e273 100644 --- a/cloud/amazon/route53_zone.py +++ b/cloud/amazon/route53_zone.py @@ -60,7 +60,7 @@ - route53_zone: zone=example.com state=absent - name: private zone for devel - route53_zome: zone=devel.example.com state=present vpc_id={{myvpc_id}} comment='developer domain' + route53_zone: zone=devel.example.com state=present vpc_id={{myvpc_id}} comment='developer domain' # more complex example - name: register output after creating zone in parameterized region From c03ea71174fd72ea9b97cb72d361c15ad5fa08f8 Mon Sep 17 00:00:00 2001 From: Louis-Michel Couture Date: Thu, 2 Jun 2016 04:27:23 -0400 Subject: [PATCH 1646/2522] Update bundler example to match the param name (#2322) --- packaging/language/bundler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/language/bundler.py b/packaging/language/bundler.py index 7c36d5a873d..152b51810a0 100644 --- a/packaging/language/bundler.py +++ b/packaging/language/bundler.py @@ -118,7 +118,7 @@ - bundler: state=present exclude_groups=production # Only install gems from the default and production groups -- bundler: state=present deployment=yes +- bundler: state=present deployment_mode=yes # Installs gems using a Gemfile in another directory - bundler: state=present gemfile=../rails_project/Gemfile From 46df503964b3a800a5cb0be8c861c8c96ab41c3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Thu, 2 Jun 2016 13:00:10 +0200 Subject: [PATCH 1647/2522] cloudstack: cs_snapshot_policy: remove unused arg specs and doc cleanup (#2361) --- cloud/cloudstack/cs_snapshot_policy.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/cloud/cloudstack/cs_snapshot_policy.py b/cloud/cloudstack/cs_snapshot_policy.py index 01efa107bea..ce8b2344f36 100644 --- a/cloud/cloudstack/cs_snapshot_policy.py +++ b/cloud/cloudstack/cs_snapshot_policy.py @@ -61,23 +61,23 @@ aliases: [ 'timezone' ] state: description: - - State of the resource. + - State of the snapshot policy. required: false default: 'present' choices: [ 'present', 'absent' ] domain: description: - - Domain the resource is related to. + - Domain the volume is related to. required: false default: null account: description: - - Account the resource is related to. + - Account the volume is related to. required: false default: null project: description: - - Name of the project the resource is related to. + - Name of the project the volume is related to. required: false default: null extends_documentation_fragment: cloudstack @@ -287,11 +287,9 @@ def main(): time_zone=dict(default='UTC', aliases=['timezone']), max_snaps=dict(type='int', default=8, aliases=['max']), state=dict(choices=['present', 'absent'], default='present'), - zone=dict(default=None), domain=dict(default=None), account=dict(default=None), project=dict(default=None), - poll_async=dict(type='bool', default=True), )) module = AnsibleModule( From 04ae0a3ebb143ea6e8332ab967966c5ee297e065 Mon Sep 17 00:00:00 2001 From: Chris Weber Date: Thu, 2 Jun 2016 11:58:38 -0700 Subject: [PATCH 1648/2522] Fixed exceptions to use python 2.4 helper function and added import also works on python 3 (#2363) --- web_infrastructure/deploy_helper.py | 9 ++++++--- web_infrastructure/ejabberd_user.py | 19 ++++++++++++------- web_infrastructure/jira.py | 10 +++++++--- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/web_infrastructure/deploy_helper.py b/web_infrastructure/deploy_helper.py index ebf5b54a8bd..b956e38d260 100644 --- a/web_infrastructure/deploy_helper.py +++ b/web_infrastructure/deploy_helper.py @@ -226,6 +226,9 @@ - debug: var=deploy_helper ''' +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception class DeployHelper(object): @@ -282,7 +285,8 @@ def delete_path(self, path): if not self.module.check_mode: try: shutil.rmtree(path, ignore_errors=False) - except Exception, e: + except Exception: + e = get_exception() self.module.fail_json(msg="rmtree failed: %s" % str(e)) return True @@ -468,8 +472,7 @@ def main(): module.exit_json(**result) -# import module snippets -from ansible.module_utils.basic import * + if __name__ == '__main__': main() diff --git a/web_infrastructure/ejabberd_user.py b/web_infrastructure/ejabberd_user.py index bf86806ad52..85e5eda8e5f 100644 --- a/web_infrastructure/ejabberd_user.py +++ b/web_infrastructure/ejabberd_user.py @@ -68,6 +68,8 @@ action: ejabberd_user username=test host=server state=absent ''' import syslog +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.basic import * class EjabberdUserException(Exception): """ Base exeption for EjabberdUser class object """ @@ -98,7 +100,8 @@ def changed(self): try: options = [self.user, self.host, self.pwd] (rc, out, err) = self.run_command('check_password', options) - except EjabberdUserException, e: + except EjabberdUserException: + e = get_exception() (rc, out, err) = (1, None, "required attribute(s) missing") return rc @@ -111,7 +114,8 @@ def exists(self): try: options = [self.user, self.host] (rc, out, err) = self.run_command('check_account', options) - except EjabberdUserException, e: + except EjabberdUserException: + e = get_exception() (rc, out, err) = (1, None, "required attribute(s) missing") return not bool(int(rc)) @@ -139,7 +143,8 @@ def update(self): try: options = [self.user, self.host, self.pwd] (rc, out, err) = self.run_command('change_password', options) - except EjabberdUserException, e: + except EjabberdUserException: + e = get_exception() (rc, out, err) = (1, None, "required attribute(s) missing") return (rc, out, err) @@ -150,7 +155,8 @@ def create(self): try: options = [self.user, self.host, self.pwd] (rc, out, err) = self.run_command('register', options) - except EjabberdUserException, e: + except EjabberdUserException: + e = get_exception() (rc, out, err) = (1, None, "required attribute(s) missing") return (rc, out, err) @@ -160,7 +166,8 @@ def delete(self): try: options = [self.user, self.host] (rc, out, err) = self.run_command('unregister', options) - except EjabberdUserException, e: + except EjabberdUserException: + e = get_exception() (rc, out, err) = (1, None, "required attribute(s) missing") return (rc, out, err) @@ -209,6 +216,4 @@ def main(): module.exit_json(**result) -# import module snippets -from ansible.module_utils.basic import * main() diff --git a/web_infrastructure/jira.py b/web_infrastructure/jira.py index 42d5e092974..0053e0a32cd 100755 --- a/web_infrastructure/jira.py +++ b/web_infrastructure/jira.py @@ -171,6 +171,10 @@ import base64 +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * +from ansible.module_utils.pycompat24 import get_exception + def request(url, user, passwd, data=None, method=None): if data: data = json.dumps(data) @@ -343,13 +347,13 @@ def main(): ret = method(restbase, user, passwd, module.params) - except Exception, e: + except Exception: + e = get_exception() return module.fail_json(msg=e.message) module.exit_json(changed=True, meta=ret) -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * + main() From 3444cd1813612fb230425ef41f111dc3a5f38b4e Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Thu, 2 Jun 2016 11:59:21 -0700 Subject: [PATCH 1649/2522] Remove the web_infrastructure modules from the py3 blacklist --- .travis.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 21a0637fc6c..00d5d0a3aea 100644 --- a/.travis.yml +++ b/.travis.yml @@ -128,9 +128,6 @@ env: system/selinux_permissive.py system/seport.py system/svc.py - web_infrastructure/deploy_helper.py - web_infrastructure/ejabberd_user.py - web_infrastructure/jira.py windows/win_unzip.py" before_install: - git config user.name "ansible" From 9e277aabb033b3ec823b4a2db7c0f7e315eaaf0b Mon Sep 17 00:00:00 2001 From: Chris Weber Date: Fri, 3 Jun 2016 06:23:55 -0700 Subject: [PATCH 1650/2522] Fixed modules/system py files for 2.4 to 3.5 exceptions (#2367) --- system/alternatives.py | 8 +++++--- system/cronvar.py | 10 ++++++---- system/crypttab.py | 8 +++++--- system/getent.py | 8 +++++--- system/gluster_volume.py | 7 ++++--- system/known_hosts.py | 16 ++++++++++------ system/locale_gen.py | 9 ++++++--- system/modprobe.py | 7 ++++--- system/osx_defaults.py | 8 +++++--- system/selinux_permissive.py | 17 ++++++++++------- system/seport.py | 33 ++++++++++++++++++++++----------- system/svc.py | 20 +++++++++++++------- 12 files changed, 95 insertions(+), 56 deletions(-) diff --git a/system/alternatives.py b/system/alternatives.py index e81a3e83065..c056348f1d5 100644 --- a/system/alternatives.py +++ b/system/alternatives.py @@ -61,6 +61,9 @@ DEFAULT_LINK_PRIORITY = 50 import re +from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception + def main(): @@ -135,12 +138,11 @@ def main(): ) module.exit_json(changed=True) - except subprocess.CalledProcessError, cpe: + except subprocess.CalledProcessError: + e = get_exception() module.fail_json(msg=str(dir(cpe))) else: module.exit_json(changed=False) -# import module snippets -from ansible.module_utils.basic import * main() diff --git a/system/cronvar.py b/system/cronvar.py index 64ea2cb1f2b..df172be8bdc 100644 --- a/system/cronvar.py +++ b/system/cronvar.py @@ -106,6 +106,8 @@ import platform import pipes import shlex +from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception CRONCMD = "/usr/bin/crontab" @@ -147,7 +149,8 @@ def read(self): f = open(self.cron_file, 'r') self.lines = f.read().splitlines() f.close() - except IOError, e: + except IOError: + e = get_exception # cron file does not exist return except: @@ -203,7 +206,8 @@ def remove_variable_file(self): try: os.unlink(self.cron_file) return True - except OSError, e: + except OSError: + e = get_exception # cron file does not exist return False except: @@ -425,7 +429,5 @@ def main(): # --- should never get here module.exit_json(msg="Unable to execute cronvar task.") -# import module snippets -from ansible.module_utils.basic import * main() diff --git a/system/crypttab.py b/system/crypttab.py index 842b5bc7d09..4b36200c526 100644 --- a/system/crypttab.py +++ b/system/crypttab.py @@ -82,6 +82,9 @@ when: '/dev/mapper/luks-' in {{ item.device }} ''' +from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception + def main(): module = AnsibleModule( @@ -126,7 +129,8 @@ def main(): try: crypttab = Crypttab(path) existing_line = crypttab.match(name) - except Exception, e: + except Exception: + e = get_exception module.fail_json(msg="failed to open and parse crypttab file: %s" % e, **module.params) @@ -358,6 +362,4 @@ def __str__(self): ret.append('%s=%s' % (k, v)) return ','.join(ret) -# import module snippets -from ansible.module_utils.basic import * main() diff --git a/system/getent.py b/system/getent.py index 7df9e1d795f..85893f13e8a 100644 --- a/system/getent.py +++ b/system/getent.py @@ -80,6 +80,9 @@ ''' +from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception + def main(): module = AnsibleModule( argument_spec = dict( @@ -110,7 +113,8 @@ def main(): try: rc, out, err = module.run_command(cmd) - except Exception, e: + except Exception: + e = get_exception module.fail_json(msg=str(e)) msg = "Unexpected failure!" @@ -136,8 +140,6 @@ def main(): module.fail_json(msg=msg) -# import module snippets -from ansible.module_utils.basic import * main() diff --git a/system/gluster_volume.py b/system/gluster_volume.py index e0b1ef01708..1f968271154 100644 --- a/system/gluster_volume.py +++ b/system/gluster_volume.py @@ -135,6 +135,8 @@ import shutil import time import socket +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.basic import * glusterbin = '' @@ -147,7 +149,8 @@ def run_gluster(gargs, **kwargs): rc, out, err = module.run_command(args, **kwargs) if rc != 0: module.fail_json(msg='error running gluster (%s) command (rc=%d): %s' % (' '.join(args), rc, out or err)) - except Exception, e: + except Exception: + e = get_exception module.fail_json(msg='error running gluster (%s) command: %s' % (' '.join(args), str(e))) return out @@ -456,6 +459,4 @@ def main(): module.exit_json(changed=changed, ansible_facts=facts) -# import module snippets -from ansible.module_utils.basic import * main() diff --git a/system/known_hosts.py b/system/known_hosts.py index d1890af5182..e7d9df59ccf 100644 --- a/system/known_hosts.py +++ b/system/known_hosts.py @@ -78,6 +78,8 @@ import tempfile import errno import re +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.basic import * def enforce_state(module, params): """ @@ -121,7 +123,8 @@ def enforce_state(module, params): if replace_or_add or found != (state=="present"): try: inf=open(path,"r") - except IOError, e: + except IOError: + e = get_exception if e.errno == errno.ENOENT: inf=None else: @@ -139,7 +142,8 @@ def enforce_state(module, params): outf.write(key) outf.flush() module.atomic_move(outf.name,path) - except (IOError,OSError),e: + except (IOError,OSError): + e = get_exception() module.fail_json(msg="Failed to write to file %s: %s" % \ (path,str(e))) @@ -173,7 +177,8 @@ def sanity_check(module,host,key,sshkeygen): outf=tempfile.NamedTemporaryFile() outf.write(key) outf.flush() - except IOError,e: + except IOError: + e = get_exception() module.fail_json(msg="Failed to write to temporary file %s: %s" % \ (outf.name,str(e))) rc,stdout,stderr=module.run_command([sshkeygen,'-F',host, @@ -224,7 +229,8 @@ def search_for_host_key(module,host,key,path,sshkeygen): # This output format has been hardcoded in ssh-keygen since at least OpenSSH 4.0 # It always outputs the non-localized comment before the found key found_line = int(re.search(r'found: line (\d+)', l).group(1)) - except IndexError, e: + except IndexError: + e = get_exception() module.fail_json(msg="failed to parse output of ssh-keygen for line number: '%s'" % l) else: found_key = normalize_known_hosts_key(l,host) @@ -274,6 +280,4 @@ def main(): results = enforce_state(module,module.params) module.exit_json(**results) -# import module snippets -from ansible.module_utils.basic import * main() diff --git a/system/locale_gen.py b/system/locale_gen.py index e17ed5581da..cde724a3ad7 100644 --- a/system/locale_gen.py +++ b/system/locale_gen.py @@ -19,6 +19,7 @@ import os.path from subprocess import Popen, PIPE, call import re +from ansible.module_utils.pycompat24 import get_exception DOCUMENTATION = ''' --- @@ -65,6 +66,9 @@ # location module specific support methods. # +from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception + def is_available(name, ubuntuMode): """Check if the given locale is available on the system. This is done by checking either : @@ -225,12 +229,11 @@ def main(): apply_change(state, name) else: apply_change_ubuntu(state, name) - except EnvironmentError, e: + except EnvironmentError: + e = get_exception() module.fail_json(msg=e.strerror, exitValue=e.errno) module.exit_json(name=name, changed=changed, msg="OK") -# import module snippets -from ansible.module_utils.basic import * main() diff --git a/system/modprobe.py b/system/modprobe.py index 405d5ea22c3..94c1a70437b 100644 --- a/system/modprobe.py +++ b/system/modprobe.py @@ -57,6 +57,8 @@ - modprobe: name=dummy state=present params="numdummies=2" ''' +from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception import shlex @@ -87,7 +89,8 @@ def main(): present = True break modules.close() - except IOError, e: + except IOError: + e = get_exception() module.fail_json(msg=str(e), **args) # Check only; don't modify @@ -118,6 +121,4 @@ def main(): module.exit_json(**args) -# import module snippets -from ansible.module_utils.basic import * main() diff --git a/system/osx_defaults.py b/system/osx_defaults.py index 0e980b30394..6ce1c894d9a 100644 --- a/system/osx_defaults.py +++ b/system/osx_defaults.py @@ -84,6 +84,8 @@ ''' import datetime +from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception # exceptions --------------------------------------------------------------- {{{ class OSXDefaultsException(Exception): @@ -275,7 +277,7 @@ def run(self): # Handle absent state if self.state == "absent": - print "Absent state detected!" + print ("Absent state detected!") if self.current_value is None: return False if self.module.check_mode: @@ -376,10 +378,10 @@ def main(): array_add=array_add, value=value, state=state, path=path) changed = defaults.run() module.exit_json(changed=changed) - except OSXDefaultsException, e: + except OSXDefaultsException: + e = get_exception() module.fail_json(msg=e.message) # /main ------------------------------------------------------------------- }}} -from ansible.module_utils.basic import * main() diff --git a/system/selinux_permissive.py b/system/selinux_permissive.py index 1e2a5c6c996..ced9716cc01 100644 --- a/system/selinux_permissive.py +++ b/system/selinux_permissive.py @@ -65,6 +65,8 @@ HAVE_SEOBJECT = True except ImportError: pass +from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception def main(): @@ -90,7 +92,8 @@ def main(): try: permissive_domains = seobject.permissiveRecords(store) - except ValueError, e: + except ValueError: + e = get_exception() module.fail_json(domain=domain, msg=str(e)) # not supported on EL 6 @@ -99,7 +102,8 @@ def main(): try: all_domains = permissive_domains.get_all() - except ValueError, e: + except ValueError: + e = get_exception() module.fail_json(domain=domain, msg=str(e)) if permissive: @@ -107,7 +111,8 @@ def main(): if not module.check_mode: try: permissive_domains.add(domain) - except ValueError, e: + except ValueError: + e = get_exception() module.fail_json(domain=domain, msg=str(e)) changed = True else: @@ -115,7 +120,8 @@ def main(): if not module.check_mode: try: permissive_domains.delete(domain) - except ValueError, e: + except ValueError: + e = get_exception() module.fail_json(domain=domain, msg=str(e)) changed = True @@ -123,8 +129,5 @@ def main(): permissive=permissive, domain=domain) -################################################# -# import module snippets -from ansible.module_utils.basic import * main() diff --git a/system/seport.py b/system/seport.py index 7192f2726a2..27183f06621 100644 --- a/system/seport.py +++ b/system/seport.py @@ -80,6 +80,8 @@ except ImportError: HAVE_SEOBJECT=False +from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception def semanage_port_exists(seport, port, proto): """ Get the SELinux port type definition from policy. Return None if it does @@ -141,15 +143,20 @@ def semanage_port_add(module, ports, proto, setype, do_reload, serange='s0', ses seport.add(port, proto, serange, setype) change = change or not exists - except ValueError, e: + except ValueError: + e = get_exception() module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) - except IOError, e: + except IOError: + e = get_exception() module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) - except KeyError, e: + except KeyError: + e = get_exception() module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) - except OSError, e: + except OSError: + e = get_exception() module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) - except RuntimeError, e: + except RuntimeError: + e = get_exception() module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) return change @@ -186,15 +193,20 @@ def semanage_port_del(module, ports, proto, do_reload, sestore=''): seport.delete(port, proto) change = change or not exists - except ValueError, e: + except ValueError: + e = get_exception() module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) - except IOError,e: + except IOError: + e = get_exception() module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) - except KeyError, e: + except KeyError: + e = get_exception() module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) - except OSError, e: + except OSError: + e = get_exception() module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) - except RuntimeError, e: + except RuntimeError: + e = get_exception() module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) return change @@ -257,5 +269,4 @@ def main(): module.exit_json(**result) -from ansible.module_utils.basic import * main() diff --git a/system/svc.py b/system/svc.py index 6cc8c1d21ef..e82b0591d59 100755 --- a/system/svc.py +++ b/system/svc.py @@ -87,6 +87,8 @@ import platform import shlex +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.basic import * def _load_dist_subclass(cls, *args, **kwargs): ''' @@ -152,7 +154,8 @@ def enable(self): if os.path.exists(self.src_full): try: os.symlink(self.src_full, self.svc_full) - except OSError, e: + except OSError: + e = get_exception() self.module.fail_json(path=self.src_full, msg='Error while linking: %s' % str(e)) else: self.module.fail_json(msg="Could not find source for service to enable (%s)." % self.src_full) @@ -160,7 +163,8 @@ def enable(self): def disable(self): try: os.unlink(self.svc_full) - except OSError, e: + except OSError: + e = get_exception() self.module.fail_json(path=self.svc_full, msg='Error while unlinking: %s' % str(e)) self.execute_command([self.svc_cmd,'-dx',self.src_full]) @@ -221,7 +225,8 @@ def kill(self): def execute_command(self, cmd): try: (rc, out, err) = self.module.run_command(' '.join(cmd)) - except Exception, e: + except Exception: + e = get_exception() self.module.fail_json(msg="failed to execute: %s" % str(e)) return (rc, out, err) @@ -267,7 +272,8 @@ def main(): svc.enable() else: svc.disable() - except (OSError, IOError), e: + except (OSError, IOError): + e = get_exception() module.fail_json(msg="Could change service link: %s" % str(e)) if state is not None and state != svc.state: @@ -284,13 +290,13 @@ def main(): open(d_file, "a").close() else: os.unlink(d_file) - except (OSError, IOError), e: + except (OSError, IOError): + e = get_exception() module.fail_json(msg="Could change downed file: %s " % (str(e))) module.exit_json(changed=changed, svc=svc.report()) -# this is magic, not normal python include -from ansible.module_utils.basic import * + main() From d1174cc8b729aa00b9587b675aee788e0fd39035 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Fri, 3 Jun 2016 06:25:29 -0700 Subject: [PATCH 1651/2522] Only import get_exception once in locale_gen.py --- system/locale_gen.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/system/locale_gen.py b/system/locale_gen.py index cde724a3ad7..9aa732f57c3 100644 --- a/system/locale_gen.py +++ b/system/locale_gen.py @@ -15,11 +15,6 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -import os -import os.path -from subprocess import Popen, PIPE, call -import re -from ansible.module_utils.pycompat24 import get_exception DOCUMENTATION = ''' --- @@ -49,6 +44,14 @@ - locale_gen: name=de_CH.UTF-8 state=present ''' +import os +import os.path +from subprocess import Popen, PIPE, call +import re + +from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception + LOCALE_NORMALIZATION = { ".utf8": ".UTF-8", ".eucjp": ".EUC-JP", @@ -66,9 +69,6 @@ # location module specific support methods. # -from ansible.module_utils.basic import * -from ansible.module_utils.pycompat24 import get_exception - def is_available(name, ubuntuMode): """Check if the given locale is available on the system. This is done by checking either : From 5ac13bc0c974a4d60305e723129a5eb319a721b3 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Fri, 3 Jun 2016 06:31:00 -0700 Subject: [PATCH 1652/2522] Remove system modules from the python3 blacklist as they are now compatible. --- .travis.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 00d5d0a3aea..6a92f3eae0a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -116,18 +116,6 @@ env: source_control/gitlab_group.py source_control/gitlab_project.py source_control/gitlab_user.py - system/alternatives.py - system/cronvar.py - system/crypttab.py - system/getent.py - system/gluster_volume.py - system/known_hosts.py - system/locale_gen.py - system/modprobe.py - system/osx_defaults.py - system/selinux_permissive.py - system/seport.py - system/svc.py windows/win_unzip.py" before_install: - git config user.name "ansible" From 7549976dc07fbaf7aac4a3fe0fef21ed454bbd33 Mon Sep 17 00:00:00 2001 From: Jiri Tyr Date: Fri, 3 Jun 2016 20:59:28 +0100 Subject: [PATCH 1653/2522] Fixing wrong type of params option in yum_repository module (#2371) This commit fixes incorrect type of the "params" option of the "yum_repository" module. Without this fix the value of the "params" option is read as a string instead of as a dictionary which makes it impossible to define any additional YUM repository parameters. --- packaging/os/yum_repository.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/os/yum_repository.py b/packaging/os/yum_repository.py index 97bb768b34a..5ea046eb9ea 100644 --- a/packaging/os/yum_repository.py +++ b/packaging/os/yum_repository.py @@ -655,7 +655,7 @@ def main(): mirrorlist=dict(), mirrorlist_expire=dict(), name=dict(required=True), - params=dict(), + params=dict(type='dict'), password=dict(no_log=True), priority=dict(), protect=dict(type='bool'), From df8d41ba0fc3cc2fd757852d18c8f807b540936d Mon Sep 17 00:00:00 2001 From: Gerik Bonaert Date: Fri, 3 Jun 2016 22:02:58 +0200 Subject: [PATCH 1654/2522] Define 'type' in argument_spec of openvswitch_port (#2355) The external_ids 'type' was not defined in the argument spec of openvswitch_port. This lead 'external_ids' to be converted to a string, when the value was not defined. Further down the code this was leading to an exception in some cases. By defining the type all is right. --- network/openvswitch_port.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/openvswitch_port.py b/network/openvswitch_port.py index 69e64ea8f9d..3ce6f7f2f12 100644 --- a/network/openvswitch_port.py +++ b/network/openvswitch_port.py @@ -238,7 +238,7 @@ def main(): 'state': {'default': 'present', 'choices': ['present', 'absent']}, 'timeout': {'default': 5, 'type': 'int'}, 'set': {'required': False, 'default': None}, - 'external_ids': {'default': {}, 'required': False}, + 'external_ids': {'default': {}, 'required': False, 'type': 'dict'}, }, supports_check_mode=True, ) From 44d0ccae0538231b70f7c131052de5850cdd08eb Mon Sep 17 00:00:00 2001 From: Greg DeKoenigsberg Date: Fri, 3 Jun 2016 17:06:40 -0400 Subject: [PATCH 1655/2522] small fix Process is now shipit, not +1 --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bd531784d3e..c0714604f5f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ Please see [this web page](http://docs.ansible.com/community.html) for informati If you'd like to contribute code to an existing module ====================================================== -Each module in Extras is maintained by the owner of that module; each module's owner is indicated in the documentation section of the module itself. Any pull request for a module that is given a +1 by the owner in the comments will be merged by the Ansible team. +Each module in Extras is maintained by the owner of that module; each module's owner is indicated in the documentation section of the module itself. Any pull request for a module that is given a "shipit" by the owner in the comments will be merged by the Ansible team. If you'd like to contribute a new module ======================================== From beff8c509889eb38c93cf26019fd144df0d841be Mon Sep 17 00:00:00 2001 From: Greg DeKoenigsberg Date: Fri, 3 Jun 2016 17:08:58 -0400 Subject: [PATCH 1656/2522] Create MAINTAINERS.md --- MAINTAINERS.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 MAINTAINERS.md diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 00000000000..f6c49b505e4 --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1 @@ +FIXME (new maintainer guidelines) From 409c8b4909dfb61c44ec88db02016bddfc9d0a1c Mon Sep 17 00:00:00 2001 From: Greg DeKoenigsberg Date: Fri, 3 Jun 2016 17:10:20 -0400 Subject: [PATCH 1657/2522] New maintainers guidelines Copied over from rbergeron/thingsandstuff --- MAINTAINERS.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/MAINTAINERS.md b/MAINTAINERS.md index f6c49b505e4..9e1e53cfb08 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -1 +1,60 @@ -FIXME (new maintainer guidelines) +# Module Maintainer Guidelines + +Thank you for being a maintainer of one of the modules in ansible-modules-extras! This guide provides module maintainers an overview of their responsibilities, resources for additional information, and links to helpful tools. + +In addition to the information below, module maintainers should be familiar with: +* General Ansible community development practices (http://docs.ansible.com/ansible/community.html) +* Documentation on module development (http://docs.ansible.com/ansible/developing_modules.html) +* Any namespace-specific module guidelines (identified as GUIDELINES.md in the appropriate file tree). + +*** + +# Maintainer Responsibilities + +When you contribute a new module to the ansible-modules-extras repository, you become the maintainer for that module once it has been merged. Maintainership empowers you with the authority to accept, reject, or request revisions to pull requests on your module -- but as they say, "with great power comes great responsibility." + +Maintainers of Ansible modules are expected to provide feedback, responses, or actions on pull requests or issues to the module(s) they maintain in a reasonably timely manner. + +The Ansible community hopes that you will find that maintaining your module is as rewarding for you as having the module is for the wider community. + +*** + +# Pull Requests and Issues + +## Pull Requests + +Module pull requests are located in the [ansible-modules-extras repository](https://github.com/ansible/ansible-modules-extras/pulls). + +Because of the high volume of pull requests, notification of PRs to specific modules are routed by an automated bot to the appropriate maintainer for handling. It is recommended that you set an appropriate notification process to receive notifications which mention your GitHub ID. + +## Issues + +Issues for modules, including bug reports, documentation bug reports, and feature requests, are tracked in the [ansible-modules-extras repository](https://github.com/ansible/ansible-modules-extras/issues). + +At this time, we do not have an automated process by which Issues are handled. If you are a maintainer of a specific module, it is recommended that you periodically search module issues for issues which mention your module's name (or some variation on that name), as well as setting an appropriate notification process for receiving notification of mentions of your GitHub ID. + +*** + +# Extras maintainers list + +The full list of maintainers for modules in ansible-modules-extras is located here: +https://github.com/ansible/ansibullbot/blob/master/MAINTAINERS-EXTRAS.txt + +## Changing Maintainership + +Communities change over time, and no one maintains a module forever. If you'd like to propose an additional maintainer for your module, please submit a PR to the maintainers file with the Github ID of the new maintainer. + +If you'd like to step down as a maintainer, please submit a PR to the maintainers file removing your Github ID from the module in question. If that would leave the module with no maintainers, put "ansible" as the maintainer. This will indicate that the module is temporarily without a maintainer, and the Ansible community team will search for a new maintainer. + +*** + +# Tools and other Resources + +## Useful tools +* https://ansible.sivel.net/pr/byfile.html -- a full list of all open Pull Requests, organized by file. +* https://github.com/sivel/ansible-testing -- these are the tests that run in Travis against all PRs for extras modules, so it's a good idea to run these tests locally first. + +## Other Resources + +* Module maintainer list: https://github.com/ansible/ansibullbot/blob/master/MAINTAINERS-EXTRAS.txt +* Ansibullbot: https://github.com/ansible/ansibullbot From 66b60ce7cd4441f4bad90af5ac71721c1c0c3d4f Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Sat, 4 Jun 2016 10:47:42 -0700 Subject: [PATCH 1658/2522] Make documentation of win_unzip work on python3 as well --- windows/win_unzip.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/windows/win_unzip.py b/windows/win_unzip.py index aa0180baf74..b24e6c6b29d 100644 --- a/windows/win_unzip.py +++ b/windows/win_unzip.py @@ -65,9 +65,9 @@ author: Phil Schwartz ''' -EXAMPLES = ''' +EXAMPLES = r''' # This unzips a library that was downloaded with win_get_url, and removes the file after extraction -$ ansible -i hosts -m win_unzip -a "src=C:\\LibraryToUnzip.zip dest=C:\\Lib rm=true" all +$ ansible -i hosts -m win_unzip -a "src=C:\LibraryToUnzip.zip dest=C:\Lib rm=true" all # Playbook example # Simple unzip @@ -95,12 +95,12 @@ - name: Grab PSCX msi win_get_url: url: 'http://download-codeplex.sec.s-msft.com/Download/Release?ProjectName=pscx&DownloadId=923562&FileTime=130585918034470000&Build=20959' - dest: 'C:\\pscx.msi' + dest: 'C:\pscx.msi' - name: Install PSCX win_msi: - path: 'C:\\pscx.msi' + path: 'C:\pscx.msi' - name: Unzip gz log win_unzip: - src: "C:\\Logs\\application-error-logs.gz" - dest: "C:\\ExtractedLogs\\application-error-logs" + src: "C:\Logs\application-error-logs.gz" + dest: "C:\ExtractedLogs\application-error-logs" ''' From a95a1a2e08d000eca759e81f591c0970ab204657 Mon Sep 17 00:00:00 2001 From: Hrishikesh Barua Date: Sun, 5 Jun 2016 02:46:53 +0530 Subject: [PATCH 1659/2522] Fix for #2366 removed print statement (#2375) --- system/osx_defaults.py | 1 - 1 file changed, 1 deletion(-) diff --git a/system/osx_defaults.py b/system/osx_defaults.py index 6ce1c894d9a..93d81305860 100644 --- a/system/osx_defaults.py +++ b/system/osx_defaults.py @@ -277,7 +277,6 @@ def run(self): # Handle absent state if self.state == "absent": - print ("Absent state detected!") if self.current_value is None: return False if self.module.check_mode: From 9f58e7325c78f49f1c20fa4a7b072ef8e2b73790 Mon Sep 17 00:00:00 2001 From: Chris Weber Date: Sat, 4 Jun 2016 14:21:04 -0700 Subject: [PATCH 1660/2522] Fixed exception compatablity for py3 (and 2.4 in yum_repository.py) (#2369) * Fixed exception compatablity for py3 (and 2.4 in yum_repository.py) * Moved Import --- packaging/language/maven_artifact.py | 10 +++++----- packaging/os/layman.py | 4 ++-- packaging/os/yum_repository.py | 10 +++++++--- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packaging/language/maven_artifact.py b/packaging/language/maven_artifact.py index f5295185451..1a33d1b9891 100644 --- a/packaging/language/maven_artifact.py +++ b/packaging/language/maven_artifact.py @@ -26,6 +26,8 @@ import hashlib import sys import posixpath +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * DOCUMENTATION = ''' --- @@ -330,7 +332,7 @@ def main(): try: artifact = Artifact(group_id, artifact_id, version, classifier, extension) - except ValueError, e: + except ValueError as e: module.fail_json(msg=e.args[0]) prev_state = "absent" @@ -351,12 +353,10 @@ def main(): module.exit_json(state=state, dest=dest, group_id=group_id, artifact_id=artifact_id, version=version, classifier=classifier, extension=extension, repository_url=repository_url, changed=True) else: module.fail_json(msg="Unable to download the artifact") - except ValueError, e: + except ValueError as e: module.fail_json(msg=e.args[0]) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * + if __name__ == '__main__': main() diff --git a/packaging/os/layman.py b/packaging/os/layman.py index f9ace121201..ac6acd12d4b 100644 --- a/packaging/os/layman.py +++ b/packaging/os/layman.py @@ -120,7 +120,7 @@ def download_url(module, url, dest): try: with open(dest, 'w') as f: shutil.copyfileobj(response, f) - except IOError, e: + except IOError as e: raise ModuleError("Failed to write: %s" % str(e)) @@ -248,7 +248,7 @@ def main(): else: changed = uninstall_overlay(module, name) - except ModuleError, e: + except ModuleError as e: module.fail_json(msg=e.message) else: module.exit_json(changed=changed, name=name) diff --git a/packaging/os/yum_repository.py b/packaging/os/yum_repository.py index 5ea046eb9ea..dfdd665ed2f 100644 --- a/packaging/os/yum_repository.py +++ b/packaging/os/yum_repository.py @@ -21,6 +21,7 @@ import ConfigParser import os +from ansible.module_utils.pycompat24 import get_exception DOCUMENTATION = ''' @@ -575,7 +576,8 @@ def save(self): # Write data into the file try: fd = open(self.params['dest'], 'wb') - except IOError, e: + except IOError: + e = get_exception() self.module.fail_json( msg="Cannot open repo file %s." % self.params['dest'], details=str(e)) @@ -584,7 +586,8 @@ def save(self): try: fd.close() - except IOError, e: + except IOError: + e = get_exception() self.module.fail_json( msg="Cannot write repo file %s." % self.params['dest'], details=str(e)) @@ -592,7 +595,8 @@ def save(self): # Remove the file if there are not repos try: os.remove(self.params['dest']) - except OSError, e: + except OSError: + e = get_exception() self.module.fail_json( msg=( "Cannot remove empty repo file %s." % From 4b4a8025e16c218c270dde2b0126a3a8eb748fb4 Mon Sep 17 00:00:00 2001 From: Laurent Mazuel Date: Sat, 4 Jun 2016 15:14:16 -0700 Subject: [PATCH 1661/2522] Adapt Code to Azure SDK 2.0.0RC4 (#2319) --- cloud/azure/azure_rm_deployment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/azure/azure_rm_deployment.py b/cloud/azure/azure_rm_deployment.py index dea06276775..bc24d889ac9 100644 --- a/cloud/azure/azure_rm_deployment.py +++ b/cloud/azure/azure_rm_deployment.py @@ -371,8 +371,8 @@ Deployment, ResourceGroup, Dependency) - from azure.mgmt.resource.resources import ResourceManagementClient, ResourceManagementClientConfiguration - from azure.mgmt.network import NetworkManagementClient, NetworkManagementClientConfiguration + from azure.mgmt.resource.resources import ResourceManagementClient + from azure.mgmt.network import NetworkManagementClient except ImportError: # This is handled in azure_rm_common From a0bd87f04ebdd15a1b1c16550780a40597b4a5b9 Mon Sep 17 00:00:00 2001 From: nitzmahone Date: Sat, 4 Jun 2016 16:12:21 -0700 Subject: [PATCH 1662/2522] various fixes to azure_rm_deployment bugfix for lost results on incomplete poll (sub-optimal, but works) add docs for undocumented module args (will temporarily break module validator) --- cloud/azure/azure_rm_deployment.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/cloud/azure/azure_rm_deployment.py b/cloud/azure/azure_rm_deployment.py index bc24d889ac9..f3743366e9e 100644 --- a/cloud/azure/azure_rm_deployment.py +++ b/cloud/azure/azure_rm_deployment.py @@ -79,6 +79,28 @@ one of them is required if "state" parameter is "present". required: false default: null + deployment_mode: + description: + - Specifies whether the deployment template should delete resources not specified in the template (complete) + or ignore them (incremental). + default: complete + choices: + - complete + - incremental + deployment_name: + description: + - The name of the deployment to be tracked in the resource group deployment history. Re-using a deployment name + will overwrite the previous value in the resource group's deployment history. + default: ansible-arm + wait_for_deployment_completion: + description: + - Whether or not to block until the deployment has completed. + default: yes + choices: ['yes', 'no'] + wait_for_deployment_polling_period: + description: + - Time (in seconds) to wait between polls when waiting for deployment completion. + default: 10 extends_documentation_fragment: - azure @@ -394,7 +416,7 @@ def __init__(self): deployment_mode=dict(type='str', default='complete', choices=['complete', 'incremental']), deployment_name=dict(type='str', default="ansible-arm"), wait_for_deployment_completion=dict(type='bool', default=True), - wait_for_deployment_polling_period=dict(type='int', default=30) + wait_for_deployment_polling_period=dict(type='int', default=10) ) mutually_exclusive = [('template', 'template_link'), @@ -488,7 +510,7 @@ def deploy_template(self): deployment_result = self.get_poller_result(result) if self.wait_for_deployment_completion: - while deployment_result.properties.provisioning_state not in ['Canceled', 'Failed', 'Deleted', + while deployment_result.properties is None or deployment_result.properties.provisioning_state not in ['Canceled', 'Failed', 'Deleted', 'Succeeded']: time.sleep(self.wait_for_deployment_polling_period) deployment_result = self.rm_client.deployments.get(self.resource_group_name, self.deployment_name) From 4f042c8cfa95060e564a56fb090b045402e50fd6 Mon Sep 17 00:00:00 2001 From: Chris Weber Date: Sat, 4 Jun 2016 16:31:54 -0700 Subject: [PATCH 1663/2522] Fixed exception handeling for Python 2.4 and python 3 compatablity (#2364) --- source_control/gitlab_group.py | 7 +++++-- source_control/gitlab_project.py | 8 ++++++-- source_control/gitlab_user.py | 7 +++++-- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/source_control/gitlab_group.py b/source_control/gitlab_group.py index 83bc77857f0..10886902064 100644 --- a/source_control/gitlab_group.py +++ b/source_control/gitlab_group.py @@ -101,6 +101,8 @@ except: HAS_GITLAB_PACKAGE = False +from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception class GitLabGroup(object): def __init__(self, module, git): @@ -187,7 +189,8 @@ def main(): git.login(user=login_user, password=login_password) else: git = gitlab.Gitlab(server_url, token=login_token, verify_ssl=verify_ssl) - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg="Failed to connect to Gitlab server: %s " % e) # Validate if group exists and take action based on "state" @@ -209,7 +212,7 @@ def main(): module.exit_json(changed=True, result="Successfully created or updated the group %s" % group_name) -from ansible.module_utils.basic import * + if __name__ == '__main__': main() diff --git a/source_control/gitlab_project.py b/source_control/gitlab_project.py index 4f016bc1232..302965fce26 100644 --- a/source_control/gitlab_project.py +++ b/source_control/gitlab_project.py @@ -163,6 +163,9 @@ except: HAS_GITLAB_PACKAGE = False +from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception + class GitLabProject(object): def __init__(self, module, git): @@ -361,7 +364,8 @@ def main(): git.login(user=login_user, password=login_password) else: git = gitlab.Gitlab(server_url, token=login_token, verify_ssl=verify_ssl) - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg="Failed to connect to Gitlab server: %s " % e) # Validate if project exists and take action based on "state" @@ -391,7 +395,7 @@ def main(): else: module.exit_json(changed=False) -from ansible.module_utils.basic import * + if __name__ == '__main__': main() diff --git a/source_control/gitlab_user.py b/source_control/gitlab_user.py index 9f6fc0db2a3..f7253db456d 100644 --- a/source_control/gitlab_user.py +++ b/source_control/gitlab_user.py @@ -137,6 +137,9 @@ except: HAS_GITLAB_PACKAGE = False +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.basic import * + class GitLabUser(object): def __init__(self, module, git): @@ -325,7 +328,8 @@ def main(): git.login(user=login_user, password=login_password) else: git = gitlab.Gitlab(server_url, token=login_token, verify_ssl=verify_ssl) - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg="Failed to connect to Gitlab server: %s " % e) # Validate if group exists and take action based on "state" @@ -342,7 +346,6 @@ def main(): user.createOrUpdateUser(user_name, user_username, user_password, user_email, user_sshkey_name, user_sshkey_file, group_name, access_level) -from ansible.module_utils.basic import * if __name__ == '__main__': main() From 9e565c4af098e9036cd630d8cdfd8c11949503bb Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Sat, 4 Jun 2016 16:35:48 -0700 Subject: [PATCH 1664/2522] Remove from travis blacklist, modules that are compiling with py3 --- .travis.yml | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6a92f3eae0a..17f286b4af8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -76,7 +76,6 @@ env: database/vertica/vertica_role.py database/vertica/vertica_schema.py database/vertica/vertica_user.py - f5/bigip_gtm_virtual_server.py monitoring/bigpanda.py monitoring/boundary_meter.py monitoring/circonus_annotation.py @@ -93,6 +92,7 @@ env: network/dnsimple.py network/dnsmadeeasy.py network/f5/bigip_facts.py + network/f5/bigip_gtm_virtual_server.py network/f5/bigip_gtm_wide_ip.py network/f5/bigip_monitor_http.py network/f5/bigip_monitor_tcp.py @@ -109,14 +109,7 @@ env: notification/mail.py notification/mqtt.py notification/sns.py - notification/typetalk.py - packaging/language/maven_artifact.py - packaging/os/layman.py - packaging/os/yum_repository.py - source_control/gitlab_group.py - source_control/gitlab_project.py - source_control/gitlab_user.py - windows/win_unzip.py" + notification/typetalk.py" before_install: - git config user.name "ansible" - git config user.email "ansible@ansible.com" From 575fc6e87b8fe10159c0ba653805d048640eeafb Mon Sep 17 00:00:00 2001 From: Anders Ingemann Date: Mon, 6 Jun 2016 16:56:33 +0200 Subject: [PATCH 1665/2522] Add sensu_subscription module (#205) --- monitoring/sensu_subscription.py | 159 +++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 monitoring/sensu_subscription.py diff --git a/monitoring/sensu_subscription.py b/monitoring/sensu_subscription.py new file mode 100644 index 00000000000..d845d72c9d5 --- /dev/null +++ b/monitoring/sensu_subscription.py @@ -0,0 +1,159 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2014, Anders Ingemann +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +DOCUMENTATION = ''' +--- +module: sensu_subscription +short_description: Manage Sensu subscriptions +version_added: 2.2 +description: + - Manage which I(sensu channels) a machine should subscribe to +options: + name: + description: + - The name of the channel + required: true + state: + description: + - Whether the machine should subscribe or unsubscribe from the channel + choices: [ 'present', 'absent' ] + required: false + default: present + path: + description: + - Path to the subscriptions json file + required: false + default: /etc/sensu/conf.d/subscriptions.json + backup: + description: + - Create a backup file (if yes), including the timestamp information so you + - can get the original file back if you somehow clobbered it incorrectly. + choices: [ 'yes', 'no' ] + required: false + default: no +requirements: [ ] +author: Anders Ingemann +''' + +RETURN = ''' +reasons: + description: the reasons why the moule changed or did not change something + returned: success + type: list + sample: ["channel subscription was absent and state is `present'"] +''' + +EXAMPLES = ''' +# Subscribe to the nginx channel +- name: subscribe to nginx checks + sensu_subscription: name=nginx + +# Unsubscribe from the common checks channel +- name: unsubscribe from common checks + sensu_subscription: name=common state=absent +''' + + +def sensu_subscription(module, path, name, state='present', backup=False): + changed = False + reasons = [] + + try: + import json + except ImportError: + import simplejson as json + + try: + config = json.load(open(path)) + except IOError, e: + if e.errno is 2: # File not found, non-fatal + if state == 'absent': + reasons.append('file did not exist and state is `absent\'') + return changed, reasons + config = {} + else: + module.fail_json(msg=str(e)) + except ValueError: + msg = '{path} contains invalid JSON'.format(path=path) + module.fail_json(msg=msg) + + if 'client' not in config: + if state == 'absent': + reasons.append('`client\' did not exist and state is `absent\'') + return changed, reasons + config['client'] = {} + changed = True + reasons.append('`client\' did not exist') + + if 'subscriptions' not in config['client']: + if state == 'absent': + reasons.append('`client.subscriptions\' did not exist and state is `absent\'') + return changed + config['client']['subscriptions'] = [] + changed = True + reasons.append('`client.subscriptions\' did not exist') + + if name not in config['client']['subscriptions']: + if state == 'absent': + reasons.append('channel subscription was absent') + return changed + config['client']['subscriptions'].append(name) + changed = True + reasons.append('channel subscription was absent and state is `present\'') + else: + if state == 'absent': + config['client']['subscriptions'].remove(name) + changed = True + reasons.append('channel subscription was present and state is `absent\'') + + if changed and not module.check_mode: + if backup: + module.backup_local(path) + try: + open(path, 'w').write(json.dumps(config, indent=2) + '\n') + except IOError, e: + module.fail_json(msg=str(e)) + + return changed, reasons + + +def main(): + arg_spec = {'name': {'type': 'str', 'required': True}, + 'path': {'type': 'str', 'default': '/etc/sensu/conf.d/subscriptions.json'}, + 'state': {'type': 'str', 'default': 'present', 'choices': ['present', 'absent']}, + 'backup': {'type': 'str', 'default': 'no', 'type': 'bool'}, + } + + module = AnsibleModule(argument_spec=arg_spec, + supports_check_mode=True) + + path = module.params['path'] + name = module.params['name'] + state = module.params['state'] + backup = module.params['backup'] + + changed, reasons = sensu_subscription(module, path, name, state, backup) + + module.exit_json(path=path, name=name, changed=changed, msg='OK', reasons=reasons) + +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From 5d900b7a7766021dbf0ce88f0798c5f6267dea2d Mon Sep 17 00:00:00 2001 From: Artem Alexandrov Date: Tue, 7 Jun 2016 00:53:12 +0400 Subject: [PATCH 1666/2522] zabbix_hostmacro: Fixed basic auth (#2330) (#2331) --- monitoring/zabbix_hostmacro.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monitoring/zabbix_hostmacro.py b/monitoring/zabbix_hostmacro.py index 144e3d30a7d..c0e3f8c2280 100644 --- a/monitoring/zabbix_hostmacro.py +++ b/monitoring/zabbix_hostmacro.py @@ -111,8 +111,8 @@ # Extend the ZabbixAPI # Since the zabbix-api python module too old (version 1.0, no higher version so far). class ZabbixAPIExtends(ZabbixAPI): - def __init__(self, server, timeout, **kwargs): - ZabbixAPI.__init__(self, server, timeout=timeout) + def __init__(self, server, timeout, user, passwd, **kwargs): + ZabbixAPI.__init__(self, server, timeout=timeout, user=user, passwd=passwd) class HostMacro(object): From 483c96681ebfc42345e3724ecba61cc061ad0035 Mon Sep 17 00:00:00 2001 From: Anders Ingemann Date: Tue, 7 Jun 2016 04:17:23 +0200 Subject: [PATCH 1667/2522] Sensu subscription bugfixes (#2380) * Fix syntax for exception catching * Friendlier error message as per suggestion by @bcoca --- monitoring/sensu_subscription.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/monitoring/sensu_subscription.py b/monitoring/sensu_subscription.py index d845d72c9d5..192b474ee48 100644 --- a/monitoring/sensu_subscription.py +++ b/monitoring/sensu_subscription.py @@ -83,7 +83,8 @@ def sensu_subscription(module, path, name, state='present', backup=False): try: config = json.load(open(path)) - except IOError, e: + except IOError: + e = get_exception() if e.errno is 2: # File not found, non-fatal if state == 'absent': reasons.append('file did not exist and state is `absent\'') @@ -129,8 +130,9 @@ def sensu_subscription(module, path, name, state='present', backup=False): module.backup_local(path) try: open(path, 'w').write(json.dumps(config, indent=2) + '\n') - except IOError, e: - module.fail_json(msg=str(e)) + except IOError: + e = get_exception() + module.fail_json(msg='Failed to write to file %s: %s' % (path, str(e))) return changed, reasons From 9bbcc09a14a9ffa5bf43bc399d871706c936b502 Mon Sep 17 00:00:00 2001 From: SamYaple Date: Wed, 2 Mar 2016 22:11:05 +0000 Subject: [PATCH 1668/2522] Create OpenStack identity services module New module using shade to create and manage OpenStack identity services --- cloud/openstack/os_keystone_service.py | 210 +++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 cloud/openstack/os_keystone_service.py diff --git a/cloud/openstack/os_keystone_service.py b/cloud/openstack/os_keystone_service.py new file mode 100644 index 00000000000..4e3e46cc5cf --- /dev/null +++ b/cloud/openstack/os_keystone_service.py @@ -0,0 +1,210 @@ +#!/usr/bin/python +# Copyright 2016 Sam Yaple +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + +try: + import shade + HAS_SHADE = True +except ImportError: + HAS_SHADE = False + +from distutils.version import StrictVersion + +DOCUMENTATION = ''' +--- +module: os_keystone_service +short_description: Manage OpenStack Identity services +extends_documentation_fragment: openstack +author: "Sam Yaple (@SamYaple)" +version_added: "2.2" +description: + - Create, update, or delete OpenStack Identity service. If a service + with the supplied name already exists, it will be updated with the + new description and enabled attributes. +options: + name: + description: + - Name of the service + required: true + description: + description: + - Description of the service + required: false + default: None + enabled: + description: + - Is the service enabled + required: false + default: True + service_type: + description: + - The type of service + required: true + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present +requirements: + - "python >= 2.6" + - "shade" +''' + +EXAMPLES = ''' +# Create a service for glance +- os_keystone_service: + cloud: mycloud + state: present + name: glance + service_type: image + description: OpenStack Image Service +# Delete a service +- os_keystone_service: + cloud: mycloud + state: absent + name: glance + service_type: image +''' + +RETURN = ''' +service: + description: Dictionary describing the service. + returned: On success when I(state) is 'present' + type: dictionary + contains: + id: + description: Service ID. + type: string + sample: "3292f020780b4d5baf27ff7e1d224c44" + name: + description: Service name. + type: string + sample: "glance" + service_type: + description: Service type. + type: string + sample: "image" + description: + description: Service description. + type: string + sample: "OpenStack Image Service" + enabled: + description: Service status. + type: boolean + sample: True +id: + description: The service ID. + returned: On success when I(state) is 'present' + type: string + sample: "3292f020780b4d5baf27ff7e1d224c44" +''' + + +def _needs_update(module, service): + if service.enabled != module.params['enabled']: + return True + if service.description is not None and \ + service.description != module.params['description']: + return True + return False + + +def _system_state_change(module, service): + state = module.params['state'] + if state == 'absent' and service: + return True + + if state == 'present': + if service is None: + return True + return _needs_update(module, service) + + return False + + +def main(): + argument_spec = openstack_full_argument_spec( + description=dict(default=None), + enabled=dict(default=True, type='bool'), + name=dict(required=True), + service_type=dict(required=True), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = openstack_module_kwargs() + module = AnsibleModule(argument_spec, + supports_check_mode=True, + **module_kwargs) + + if not HAS_SHADE: + module.fail_json(msg='shade is required for this module') + if StrictVersion(shade.__version__) < StrictVersion('1.6.0'): + module.fail_json(msg="To utilize this module, the installed version of" + "the shade library MUST be >=1.6.0") + + description = module.params['description'] + enabled = module.params['enabled'] + name = module.params['name'] + state = module.params['state'] + service_type = module.params['service_type'] + + try: + cloud = shade.operator_cloud(**module.params) + + services = cloud.search_services(name_or_id=name, + filters=dict(type=service_type)) + + if len(services) > 1: + module.fail_json(msg='Service name %s and type %s are not unique' % + (name, service_type)) + elif len(services) == 1: + service = services[0] + else: + service = None + + if module.check_mode: + module.exit_json(changed=_system_state_change(module, service)) + + if state == 'present': + if service is None: + service = cloud.create_service(name=name, + description=description, type=service_type, enabled=True) + changed = True + else: + if _needs_update(module, service): + service = cloud.update_service( + service.id, name=name, type=service_type, enabled=enabled, + description=description) + changed = True + else: + changed = False + module.exit_json(changed=changed, service=service, id=service.id) + + elif state == 'absent': + if service is None: + changed=False + else: + cloud.delete_service(service.id) + changed=True + module.exit_json(changed=changed) + + except shade.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + + +from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * +if __name__ == '__main__': + main() From cb3e9d1545b1d1591212a4d5863fcc0dc8f8b7d3 Mon Sep 17 00:00:00 2001 From: Thilo-Alexander Ginkel Date: Tue, 7 Jun 2016 23:03:20 +0200 Subject: [PATCH 1669/2522] Fix gitlab_* module boolean parameter declaration (#2385) Without these fixes using the modules would result in the following error: implementation error: unknown type requested for validate_certs --- source_control/gitlab_group.py | 2 +- source_control/gitlab_project.py | 12 ++++++------ source_control/gitlab_user.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/source_control/gitlab_group.py b/source_control/gitlab_group.py index 10886902064..a5fa98d13f1 100644 --- a/source_control/gitlab_group.py +++ b/source_control/gitlab_group.py @@ -146,7 +146,7 @@ def main(): module = AnsibleModule( argument_spec=dict( server_url=dict(required=True), - validate_certs=dict(required=False, default=True, type=bool, aliases=['verify_ssl']), + validate_certs=dict(required=False, default=True, type='bool', aliases=['verify_ssl']), login_user=dict(required=False, no_log=True), login_password=dict(required=False, no_log=True), login_token=dict(required=False, no_log=True), diff --git a/source_control/gitlab_project.py b/source_control/gitlab_project.py index 302965fce26..da21589186c 100644 --- a/source_control/gitlab_project.py +++ b/source_control/gitlab_project.py @@ -297,7 +297,7 @@ def main(): module = AnsibleModule( argument_spec=dict( server_url=dict(required=True), - validate_certs=dict(required=False, default=True, type=bool, aliases=['verify_ssl']), + validate_certs=dict(required=False, default=True, type='bool', aliases=['verify_ssl']), login_user=dict(required=False, no_log=True), login_password=dict(required=False, no_log=True), login_token=dict(required=False, no_log=True), @@ -305,11 +305,11 @@ def main(): name=dict(required=True), path=dict(required=False), description=dict(required=False), - issues_enabled=dict(default=True, type=bool), - merge_requests_enabled=dict(default=True, type=bool), - wiki_enabled=dict(default=True, type=bool), - snippets_enabled=dict(default=True, type=bool), - public=dict(default=False, type=bool), + issues_enabled=dict(default=True, type='bool'), + merge_requests_enabled=dict(default=True, type='bool'), + wiki_enabled=dict(default=True, type='bool'), + snippets_enabled=dict(default=True, type='bool'), + public=dict(default=False, type='bool'), visibility_level=dict(default="0", choices=["0", "10", "20"]), import_url=dict(required=False), state=dict(default="present", choices=["present", 'absent']), diff --git a/source_control/gitlab_user.py b/source_control/gitlab_user.py index f7253db456d..d9b40401b0d 100644 --- a/source_control/gitlab_user.py +++ b/source_control/gitlab_user.py @@ -263,7 +263,7 @@ def main(): module = AnsibleModule( argument_spec=dict( server_url=dict(required=True), - validate_certs=dict(required=False, default=True, type=bool, aliases=['verify_ssl']), + validate_certs=dict(required=False, default=True, type='bool', aliases=['verify_ssl']), login_user=dict(required=False, no_log=True), login_password=dict(required=False, no_log=True), login_token=dict(required=False, no_log=True), From 6ee1cfc5cf601cdb2a97cc1c91e619148d9dd13d Mon Sep 17 00:00:00 2001 From: nitzmahone Date: Tue, 7 Jun 2016 17:07:32 -0700 Subject: [PATCH 1670/2522] remove duplicate deployment_mode docstring submodule ref wasn't updated, so missed that this had recently been added --- cloud/azure/azure_rm_deployment.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/cloud/azure/azure_rm_deployment.py b/cloud/azure/azure_rm_deployment.py index f3743366e9e..82b5fd117f5 100644 --- a/cloud/azure/azure_rm_deployment.py +++ b/cloud/azure/azure_rm_deployment.py @@ -79,14 +79,6 @@ one of them is required if "state" parameter is "present". required: false default: null - deployment_mode: - description: - - Specifies whether the deployment template should delete resources not specified in the template (complete) - or ignore them (incremental). - default: complete - choices: - - complete - - incremental deployment_name: description: - The name of the deployment to be tracked in the resource group deployment history. Re-using a deployment name From 6fd21ae67c8541a9a376b3ffed16548f0b8bd296 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Wed, 8 Jun 2016 15:11:03 +0200 Subject: [PATCH 1671/2522] cloudstack: cs_project: add tag support --- cloud/cloudstack/cs_project.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cloud/cloudstack/cs_project.py b/cloud/cloudstack/cs_project.py index 6b37923d90d..390dafbaef4 100644 --- a/cloud/cloudstack/cs_project.py +++ b/cloud/cloudstack/cs_project.py @@ -53,6 +53,13 @@ - Account the project is related to. required: false default: null + tags: + description: + - List of tags. Tags are a list of dictionaries having keys C(key) and C(value). + - "If you want to delete all tags, set a empty list e.g. C(tags: [])." + required: false + default: null + version_added: "2.2" poll_async: description: - Poll async jobs until job has finished. @@ -66,6 +73,9 @@ - local_action: module: cs_project name: web + tags: + - { key: admin, value: john } + - { key: foo, value: bar } # Rename a project - local_action: @@ -167,6 +177,10 @@ def present_project(self): project = self.create_project(project) else: project = self.update_project(project) + if project: + project = self.ensure_tags(resource=project, resource_type='project') + # refresh resource + self.project = project return project @@ -267,6 +281,7 @@ def main(): domain = dict(default=None), account = dict(default=None), poll_async = dict(type='bool', default=True), + tags=dict(type='list', aliases=['tag'], default=None), )) module = AnsibleModule( From 93f47524d9d2a8f77c9c6ecac050514da7392335 Mon Sep 17 00:00:00 2001 From: tazle Date: Wed, 8 Jun 2016 17:22:52 +0300 Subject: [PATCH 1672/2522] Fixed /etc/hosts example for blockinfile (#2387) - Fixed name - Fixed name/ip order in template --- files/blockinfile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/files/blockinfile.py b/files/blockinfile.py index da67e2d4b37..258284ea5af 100755 --- a/files/blockinfile.py +++ b/files/blockinfile.py @@ -134,11 +134,11 @@ marker: "" content: "" -- name: insert/update "Match User" configuation block in /etc/ssh/sshd_config +- name: Add mappings to /etc/hosts blockinfile: dest: /etc/hosts block: | - {{item.name}} {{item.ip}} + {{item.ip}} {{item.name}} marker: "# {mark} ANSIBLE MANAGED BLOCK {{item.name}}" with_items: - { name: host1, ip: 10.10.1.10 } From e49858b09e851c8030a1a78333d32dfac8c8c2af Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Wed, 8 Jun 2016 17:45:38 +0200 Subject: [PATCH 1673/2522] sefcontext: New module to manage SELinux file context mappings (semanage fcontext) (#2221) New SELinux module sefcontext to set SELinux file context mappings This module implements `semanage fcontext` in an idempotent way. It supports check-mode and diff-mode. --- system/sefcontext.py | 246 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 system/sefcontext.py diff --git a/system/sefcontext.py b/system/sefcontext.py new file mode 100644 index 00000000000..6977ec622e9 --- /dev/null +++ b/system/sefcontext.py @@ -0,0 +1,246 @@ +#!/usr/bin/python + +# (c) 2016, Dag Wieers +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: sefcontext +short_description: Manages SELinux file context mapping definitions +description: + - Manages SELinux file context mapping definitions + - Similar to the C(semanage fcontext) command +version_added: "2.2" +options: + target: + description: + - Target path (expression). + required: true + default: null + aliases: ['path'] + ftype: + description: + - File type. + required: false + default: a + setype: + description: + - SELinux type for the specified target. + required: true + default: null + seuser: + description: + - SELinux user for the specified target. + required: false + default: null + selevel: + description: + - SELinux range for the specified target. + required: false + default: null + aliases: ['serange'] + state: + description: + - Desired boolean value. + required: false + default: present + choices: [ 'present', 'absent' ] + reload: + description: + - Reload SELinux policy after commit. + required: false + default: yes +notes: + - The changes are persistent across reboots +requirements: [ 'libselinux-python', 'policycoreutils-python' ] +author: Dag Wieers +''' + +EXAMPLES = ''' +# Allow apache to modify files in /srv/git_repos +- sefcontext: target='/srv/git_repos(/.*)?' setype=httpd_git_rw_content_t state=present +''' + +RETURN = ''' +# Default return values +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception + +try: + import selinux + HAVE_SELINUX=True +except ImportError: + HAVE_SELINUX=False + +try: + import seobject + HAVE_SEOBJECT=True +except ImportError: + HAVE_SEOBJECT=False + +### Make backward compatible +option_to_file_type_str = { + 'a': 'all files', + 'b': 'block device', + 'c': 'character device', + 'd': 'directory', + 'f': 'regular file', + 'l': 'symbolic link', + 's': 'socket file', + 'p': 'named pipe', +} + +def semanage_fcontext_exists(sefcontext, target, ftype): + ''' Get the SELinux file context mapping definition from policy. Return None if it does not exist. ''' + record = (target, ftype) + records = sefcontext.get_all() + try: + return records[record] + except KeyError: + return None + +def semanage_fcontext_modify(module, result, target, ftype, setype, do_reload, serange, seuser, sestore=''): + ''' Add or modify SELinux file context mapping definition to the policy. ''' + + changed = False + prepared_diff = '' + + try: + sefcontext = seobject.fcontextRecords(sestore) + sefcontext.set_reload(do_reload) + exists = semanage_fcontext_exists(sefcontext, target, ftype) + if exists: + # Modify existing entry + orig_seuser, orig_serole, orig_setype, orig_serange = exists + + if seuser is None: + seuser = orig_seuser + if serange is None: + serange = orig_serange + + if setype != orig_setype or seuser != orig_seuser or serange != orig_serange: + if not module.check_mode: + sefcontext.modify(target, setype, ftype, serange, seuser) + changed = True + + if module._diff: + prepared_diff += '# Change to semanage file context mappings\n' + prepared_diff += '-%s %s %s:%s:%s:%s\n' % (target, ftype, orig_seuser, orig_serole, orig_setype, orig_serange) + prepared_diff += '+%s %s %s:%s:%s:%s\n' % (target, ftype, seuser, orig_serole, setype, serange) + else: + # Add missing entry + if seuser is None: + seuser = 'system_u' + if serange is None: + serange = 's0' + + if not module.check_mode: + sefcontext.add(target, setype, ftype, serange, seuser) + changed = True + + if module._diff: + prepared_diff += '# Addition to semanage file context mappings\n' + prepared_diff += '+%s %s %s:%s:%s:%s\n' % (target, ftype, seuser, 'object_r', setype, serange) + + except Exception: + e = get_exception() + module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) + + if module._diff and prepared_diff: + result['diff'] = dict(prepared=prepared_diff) + + module.exit_json(changed=changed, seuser=seuser, serange=serange, **result) + +def semanage_fcontext_delete(module, result, target, ftype, do_reload, sestore=''): + ''' Delete SELinux file context mapping definition from the policy. ''' + + changed = False + prepared_diff = '' + + try: + sefcontext = seobject.fcontextRecords(sestore) + sefcontext.set_reload(do_reload) + exists = semanage_fcontext_exists(sefcontext, target, ftype) + if exists: + # Remove existing entry + orig_seuser, orig_serole, orig_setype, orig_serange = exists + + if not module.check_mode: + sefcontext.delete(target, ftype) + changed = True + + if module._diff: + prepared_diff += '# Deletion to semanage file context mappings\n' + prepared_diff += '-%s %s %s:%s:%s:%s\n' % (target, ftype, exists[0], exists[1], exists[2], exists[3]) + + except Exception: + e = get_exception() + module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) + + if module._diff and prepared_diff: + result['diff'] = dict(prepared=prepared_diff) + + module.exit_json(changed=changed, **result) + + +def main(): + module = AnsibleModule( + argument_spec = dict( + target = dict(required=True, aliases=['path']), + ftype = dict(required=False, choices=option_to_file_type_str.keys(), default='a'), + setype = dict(required=True), + seuser = dict(required=False, default=None), + selevel = dict(required=False, default=None, aliases=['serange']), + state = dict(required=False, choices=['present', 'absent'], default='present'), + reload = dict(required=False, type='bool', default='yes'), + ), + supports_check_mode = True, + ) + if not HAVE_SELINUX: + module.fail_json(msg="This module requires libselinux-python") + + if not HAVE_SEOBJECT: + module.fail_json(msg="This module requires policycoreutils-python") + + if not selinux.is_selinux_enabled(): + module.fail_json(msg="SELinux is disabled on this host.") + + target = module.params['target'] + ftype = module.params['ftype'] + setype = module.params['setype'] + seuser = module.params['seuser'] + serange = module.params['selevel'] + state = module.params['state'] + do_reload = module.params['reload'] + + result = dict(target=target, ftype=ftype, setype=setype, state=state) + + # Convert file types to (internally used) strings + ftype = option_to_file_type_str[ftype] + + if state == 'present': + semanage_fcontext_modify(module, result, target, ftype, setype, do_reload, serange, seuser) + elif state == 'absent': + semanage_fcontext_delete(module, result, target, ftype, do_reload) + else: + module.fail_json(msg='Invalid value of argument "state": {0}'.format(state)) + + +if __name__ == '__main__': + main() \ No newline at end of file From 62d7ded9ba92920ed48543e99437b70c63ca66e1 Mon Sep 17 00:00:00 2001 From: Alex Lee Date: Sun, 17 Jan 2016 14:01:09 -0800 Subject: [PATCH 1674/2522] adding public ip address --- cloud/amazon/ec2_remote_facts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud/amazon/ec2_remote_facts.py b/cloud/amazon/ec2_remote_facts.py index 28fc2c97d63..4b5dc3c87b1 100644 --- a/cloud/amazon/ec2_remote_facts.py +++ b/cloud/amazon/ec2_remote_facts.py @@ -113,6 +113,7 @@ def get_instance_info(instance): 'region': instance.region.name, 'persistent': instance.persistent, 'private_ip_address': instance.private_ip_address, + 'public_ip_adress': instance.ip_address, 'state': instance._state.name, 'vpc_id': instance.vpc_id, } From 9391103f01eaf7828a52c3725f5caff2575fa221 Mon Sep 17 00:00:00 2001 From: Alex Lee Date: Sun, 17 Jan 2016 14:02:57 -0800 Subject: [PATCH 1675/2522] correcting spelling mistake --- cloud/amazon/ec2_remote_facts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_remote_facts.py b/cloud/amazon/ec2_remote_facts.py index 4b5dc3c87b1..d6733896cae 100644 --- a/cloud/amazon/ec2_remote_facts.py +++ b/cloud/amazon/ec2_remote_facts.py @@ -113,7 +113,7 @@ def get_instance_info(instance): 'region': instance.region.name, 'persistent': instance.persistent, 'private_ip_address': instance.private_ip_address, - 'public_ip_adress': instance.ip_address, + 'public_ip_address': instance.ip_address, 'state': instance._state.name, 'vpc_id': instance.vpc_id, } From 0e9a8206287ddeb19c2a9f12425c8e6ddfa81aeb Mon Sep 17 00:00:00 2001 From: Rob Date: Thu, 9 Jun 2016 03:43:53 +1000 Subject: [PATCH 1676/2522] Use helper function for new module and rename (#2277) --- ...mfa_devices.py => iam_mfa_device_facts.py} | 35 +++++-------------- 1 file changed, 9 insertions(+), 26 deletions(-) rename cloud/amazon/{aws_mfa_devices.py => iam_mfa_device_facts.py} (77%) diff --git a/cloud/amazon/aws_mfa_devices.py b/cloud/amazon/iam_mfa_device_facts.py similarity index 77% rename from cloud/amazon/aws_mfa_devices.py rename to cloud/amazon/iam_mfa_device_facts.py index 237c9c66b3f..2b97d0bee46 100644 --- a/cloud/amazon/aws_mfa_devices.py +++ b/cloud/amazon/iam_mfa_device_facts.py @@ -16,7 +16,7 @@ DOCUMENTATION = ''' --- -module: aws_mfa_devices +module: iam_mfa_device_facts short_description: List the MFA (Multi-Factor Authentication) devices registered for a user description: - List the MFA (Multi-Factor Authentication) devices registered for a user @@ -37,7 +37,7 @@ ''' RETURN = """ -devices: +mfa_devices: description: The MFA devices registered for the given user returned: always type: list @@ -48,22 +48,18 @@ - enable_date: "2016-03-11T23:25:37+00:00" serial_number: arn:aws:iam::085120003702:mfa/pwnall user_name: pwnall -changed: - description: True if listing the devices succeeds - type: bool - returned: always """ EXAMPLES = ''' # Note: These examples do not set authentication details, see the AWS Guide for details. # List MFA devices (more details: http://docs.aws.amazon.com/IAM/latest/APIReference/API_ListMFADevices.html) -aws_mfa_devices: +iam_mfa_device_facts: register: mfa_devices # Assume an existing role (more details: http://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html) sts_assume_role: - mfa_serial_number: "{{ mfa_devices.devices[0].serial_number }}" + mfa_serial_number: "{{ mfa_devices.mfa_devices[0].serial_number }}" role_arn: "arn:aws:iam::123456789012:role/someRole" role_session_name: "someRoleSession" register: assumed_role @@ -77,16 +73,6 @@ HAS_BOTO3 = False -def normalize_mfa_device(mfa_device): - serial_number = mfa_device.get('SerialNumber', None) - user_name = mfa_device.get('UserName', None) - enable_date = mfa_device.get('EnableDate', None) - return { - 'serial_number': serial_number, - 'user_name': user_name, - 'enable_date': enable_date - } - def list_mfa_devices(connection, module): user_name = module.params.get('user_name') changed = False @@ -96,27 +82,24 @@ def list_mfa_devices(connection, module): args['UserName'] = user_name try: response = connection.list_mfa_devices(**args) - changed = True - except ClientError, e: - module.fail_json(msg=e) + except ClientError as e: + module.fail_json(msg=e.message, **camel_dict_to_snake_dict(e.response)) - mfa_devices = response.get('MFADevices', []) - devices = [normalize_mfa_device(mfa_device) for mfa_device in mfa_devices] + module.exit_json(changed=changed, **camel_dict_to_snake_dict(response)) - module.exit_json(changed=changed, devices=devices) def main(): argument_spec = ec2_argument_spec() argument_spec.update( dict( - user_name = dict(required=False, default=None) + user_name=dict(required=False, default=None) ) ) module = AnsibleModule(argument_spec=argument_spec) if not HAS_BOTO3: - module.fail_json(msg='boto3 and botocore are required.') + module.fail_json(msg='boto3 required for this module') region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) if region: From 93b59ba852a8b160d3294447d27aa492a5a2890c Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Thu, 9 Jun 2016 17:00:00 +0200 Subject: [PATCH 1677/2522] Update GitHub templates to reflect ansible/ansible (#2397) Update the GitHub templates to what is used for some time on ansible/ansible For more information, see ansible/ansible#15961 --- .github/ISSUE_TEMPLATE.md | 9 ++++++--- .github/PULL_REQUEST_TEMPLATE.md | 13 +++++++------ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index feb687200ed..7cc5b860273 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -10,8 +10,9 @@ ##### ANSIBLE VERSION + ``` - + ``` ##### CONFIGURATION @@ -35,8 +36,9 @@ For bugs, show exactly how to reproduce the problem. For new features, show how the feature would be used. --> + ``` - + ``` @@ -47,6 +49,7 @@ For new features, show how the feature would be used. ##### ACTUAL RESULTS + ``` - + ``` diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d8b8e17cbd5..5cfd027103a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -6,22 +6,23 @@ - Docs Pull Request ##### COMPONENT NAME - + ##### ANSIBLE VERSION + ``` - + ``` ##### SUMMARY + ``` - + ``` From 883ccbefe5c5e068de7d808862a9de332d78dbf8 Mon Sep 17 00:00:00 2001 From: Ilya Zonov Date: Thu, 9 Jun 2016 23:03:09 +0400 Subject: [PATCH 1678/2522] Fix rabbitmq parameter changed check (#2237) This commit fixes following issue: "Changed" flag is always true when var is used for value module param. --- messaging/rabbitmq_parameter.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/messaging/rabbitmq_parameter.py b/messaging/rabbitmq_parameter.py index 60b9811a9cb..9022910928b 100644 --- a/messaging/rabbitmq_parameter.py +++ b/messaging/rabbitmq_parameter.py @@ -96,12 +96,17 @@ def get(self): component, name, value = param_item.split('\t') if component == self.component and name == self.name: - self._value = value + self._value = json.loads(value) return True return False def set(self): - self._exec(['set_parameter', '-p', self.vhost, self.component, self.name, self.value]) + self._exec(['set_parameter', + '-p', + self.vhost, + self.component, + self.name, + json.dumps(self.value)]) def delete(self): self._exec(['clear_parameter', '-p', self.vhost, self.component, self.name]) @@ -126,8 +131,8 @@ def main(): component = module.params['component'] name = module.params['name'] value = module.params['value'] - if not isinstance(value, str): - value = json.dumps(value) + if isinstance(value, str): + value = json.loads(value) vhost = module.params['vhost'] state = module.params['state'] node = module.params['node'] From ff29da944ed05ce41a5b8bd81033387777e0bbc5 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Wed, 8 Jun 2016 15:14:06 +0200 Subject: [PATCH 1679/2522] cloudstack: cs_project: fix state=active/suspended does not create/update project --- cloud/cloudstack/cs_project.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/cloud/cloudstack/cs_project.py b/cloud/cloudstack/cs_project.py index 390dafbaef4..943067ef5a4 100644 --- a/cloud/cloudstack/cs_project.py +++ b/cloud/cloudstack/cs_project.py @@ -224,11 +224,8 @@ def create_project(self, project): return project - def state_project(self, state=None): - project = self.get_project() - - if not project: - self.module.fail_json(msg="No project named '%s' found." % self.module.params('name')) + def state_project(self, state='active'): + project = self.present_project() if project['state'].lower() != state: self.result['changed'] = True From efcfe21732fbcb894f723e135411adf9fa831519 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Wed, 8 Jun 2016 15:35:36 +0200 Subject: [PATCH 1680/2522] cloudstack: remove duplicate import cs handling, already in utils. --- cloud/cloudstack/cs_account.py | 9 --------- cloud/cloudstack/cs_affinitygroup.py | 9 --------- cloud/cloudstack/cs_cluster.py | 9 --------- cloud/cloudstack/cs_configuration.py | 9 --------- cloud/cloudstack/cs_domain.py | 9 --------- cloud/cloudstack/cs_firewall.py | 9 --------- cloud/cloudstack/cs_instance.py | 9 --------- cloud/cloudstack/cs_instance_facts.py | 9 --------- cloud/cloudstack/cs_instancegroup.py | 9 --------- cloud/cloudstack/cs_ip_address.py | 10 ---------- cloud/cloudstack/cs_iso.py | 9 --------- cloud/cloudstack/cs_loadbalancer_rule.py | 9 --------- cloud/cloudstack/cs_loadbalancer_rule_member.py | 9 --------- cloud/cloudstack/cs_network.py | 9 --------- cloud/cloudstack/cs_pod.py | 9 --------- cloud/cloudstack/cs_portforward.py | 9 --------- cloud/cloudstack/cs_project.py | 9 --------- cloud/cloudstack/cs_resourcelimit.py | 9 --------- cloud/cloudstack/cs_router.py | 9 --------- cloud/cloudstack/cs_securitygroup.py | 9 --------- cloud/cloudstack/cs_securitygroup_rule.py | 9 --------- cloud/cloudstack/cs_sshkeypair.py | 10 ---------- cloud/cloudstack/cs_staticnat.py | 10 ---------- cloud/cloudstack/cs_template.py | 9 --------- cloud/cloudstack/cs_user.py | 9 --------- cloud/cloudstack/cs_vmsnapshot.py | 9 --------- cloud/cloudstack/cs_volume.py | 9 --------- cloud/cloudstack/cs_zone.py | 9 --------- cloud/cloudstack/cs_zone_facts.py | 9 --------- 29 files changed, 264 deletions(-) diff --git a/cloud/cloudstack/cs_account.py b/cloud/cloudstack/cs_account.py index 0313006f894..ae977dea028 100644 --- a/cloud/cloudstack/cs_account.py +++ b/cloud/cloudstack/cs_account.py @@ -172,12 +172,6 @@ sample: ROOT ''' -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -365,9 +359,6 @@ def main(): supports_check_mode=True ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - try: acs_acc = AnsibleCloudStackAccount(module) diff --git a/cloud/cloudstack/cs_affinitygroup.py b/cloud/cloudstack/cs_affinitygroup.py index 9ca801a8f4c..d476d25a76c 100644 --- a/cloud/cloudstack/cs_affinitygroup.py +++ b/cloud/cloudstack/cs_affinitygroup.py @@ -123,12 +123,6 @@ sample: example account ''' -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -239,9 +233,6 @@ def main(): supports_check_mode=True ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - try: acs_ag = AnsibleCloudStackAffinityGroup(module) diff --git a/cloud/cloudstack/cs_cluster.py b/cloud/cloudstack/cs_cluster.py index 6041d65081f..66aec535a84 100644 --- a/cloud/cloudstack/cs_cluster.py +++ b/cloud/cloudstack/cs_cluster.py @@ -226,12 +226,6 @@ sample: pod01 ''' -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -406,9 +400,6 @@ def main(): supports_check_mode=True ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - try: acs_cluster = AnsibleCloudStackCluster(module) diff --git a/cloud/cloudstack/cs_configuration.py b/cloud/cloudstack/cs_configuration.py index b3e68c6a788..9c62daeba7d 100644 --- a/cloud/cloudstack/cs_configuration.py +++ b/cloud/cloudstack/cs_configuration.py @@ -148,12 +148,6 @@ sample: storage01 ''' -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -278,9 +272,6 @@ def main(): supports_check_mode=True ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - try: acs_configuration = AnsibleCloudStackConfiguration(module) configuration = acs_configuration.present_configuration() diff --git a/cloud/cloudstack/cs_domain.py b/cloud/cloudstack/cs_domain.py index 0d041d73dd0..a9b3aa085b3 100644 --- a/cloud/cloudstack/cs_domain.py +++ b/cloud/cloudstack/cs_domain.py @@ -106,12 +106,6 @@ sample: example.local ''' -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -254,9 +248,6 @@ def main(): supports_check_mode=True ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - try: acs_dom = AnsibleCloudStackDomain(module) diff --git a/cloud/cloudstack/cs_firewall.py b/cloud/cloudstack/cs_firewall.py index 958c13d4aba..c782c93e3b4 100644 --- a/cloud/cloudstack/cs_firewall.py +++ b/cloud/cloudstack/cs_firewall.py @@ -210,12 +210,6 @@ sample: my_network ''' -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -413,9 +407,6 @@ def main(): supports_check_mode=True ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - try: acs_fw = AnsibleCloudStackFirewall(module) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index eeac04162e1..202796f0bc2 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -396,12 +396,6 @@ import base64 -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -962,9 +956,6 @@ def main(): supports_check_mode=True ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - try: acs_instance = AnsibleCloudStackInstance(module) diff --git a/cloud/cloudstack/cs_instance_facts.py b/cloud/cloudstack/cs_instance_facts.py index bfed5c8572f..f405debca3f 100644 --- a/cloud/cloudstack/cs_instance_facts.py +++ b/cloud/cloudstack/cs_instance_facts.py @@ -178,12 +178,6 @@ import base64 -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -270,9 +264,6 @@ def main(): supports_check_mode=False, ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - cs_instance_facts = AnsibleCloudStackInstanceFacts(module=module).run() cs_facts_result = dict(changed=False, ansible_facts=cs_instance_facts) module.exit_json(**cs_facts_result) diff --git a/cloud/cloudstack/cs_instancegroup.py b/cloud/cloudstack/cs_instancegroup.py index bece79013ee..323e0391213 100644 --- a/cloud/cloudstack/cs_instancegroup.py +++ b/cloud/cloudstack/cs_instancegroup.py @@ -102,12 +102,6 @@ sample: example project ''' -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -185,9 +179,6 @@ def main(): supports_check_mode=True ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - try: acs_ig = AnsibleCloudStackInstanceGroup(module) diff --git a/cloud/cloudstack/cs_ip_address.py b/cloud/cloudstack/cs_ip_address.py index 237a67fbcdb..bb6344de0b6 100644 --- a/cloud/cloudstack/cs_ip_address.py +++ b/cloud/cloudstack/cs_ip_address.py @@ -127,13 +127,6 @@ sample: example domain ''' - -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -252,9 +245,6 @@ def main(): supports_check_mode=True ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - try: acs_ip_address = AnsibleCloudStackIPAddress(module) diff --git a/cloud/cloudstack/cs_iso.py b/cloud/cloudstack/cs_iso.py index 5508fdd21fa..a61fb180781 100644 --- a/cloud/cloudstack/cs_iso.py +++ b/cloud/cloudstack/cs_iso.py @@ -197,12 +197,6 @@ sample: example project ''' -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -319,9 +313,6 @@ def main(): supports_check_mode=True ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - try: acs_iso = AnsibleCloudStackIso(module) diff --git a/cloud/cloudstack/cs_loadbalancer_rule.py b/cloud/cloudstack/cs_loadbalancer_rule.py index 8d16c058855..cc8a029d3a4 100644 --- a/cloud/cloudstack/cs_loadbalancer_rule.py +++ b/cloud/cloudstack/cs_loadbalancer_rule.py @@ -217,12 +217,6 @@ sample: "Add" ''' -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -364,9 +358,6 @@ def main(): supports_check_mode=True ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - try: acs_lb_rule = AnsibleCloudStackLBRule(module) diff --git a/cloud/cloudstack/cs_loadbalancer_rule_member.py b/cloud/cloudstack/cs_loadbalancer_rule_member.py index dd821cafa9c..c5410491a16 100644 --- a/cloud/cloudstack/cs_loadbalancer_rule_member.py +++ b/cloud/cloudstack/cs_loadbalancer_rule_member.py @@ -200,12 +200,6 @@ sample: "Add" ''' -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -344,9 +338,6 @@ def main(): supports_check_mode=True ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - try: acs_lb_rule_member = AnsibleCloudStackLBRuleMember(module) diff --git a/cloud/cloudstack/cs_network.py b/cloud/cloudstack/cs_network.py index fa1c7a68870..ee8888ff8e8 100644 --- a/cloud/cloudstack/cs_network.py +++ b/cloud/cloudstack/cs_network.py @@ -318,12 +318,6 @@ sample: DefaultIsolatedNetworkOfferingWithSourceNatService ''' -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -560,9 +554,6 @@ def main(): supports_check_mode=True ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - try: acs_network = AnsibleCloudStackNetwork(module) diff --git a/cloud/cloudstack/cs_pod.py b/cloud/cloudstack/cs_pod.py index 8bf33ec6a09..e78eb2844cf 100644 --- a/cloud/cloudstack/cs_pod.py +++ b/cloud/cloudstack/cs_pod.py @@ -150,12 +150,6 @@ sample: ch-gva-2 ''' -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -286,9 +280,6 @@ def main(): supports_check_mode=True ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - try: acs_pod = AnsibleCloudStackPod(module) state = module.params.get('state') diff --git a/cloud/cloudstack/cs_portforward.py b/cloud/cloudstack/cs_portforward.py index 526d4616417..79c57e5502b 100644 --- a/cloud/cloudstack/cs_portforward.py +++ b/cloud/cloudstack/cs_portforward.py @@ -203,12 +203,6 @@ sample: 10.101.65.152 ''' -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -391,9 +385,6 @@ def main(): supports_check_mode=True ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - try: acs_pf = AnsibleCloudStackPortforwarding(module) state = module.params.get('state') diff --git a/cloud/cloudstack/cs_project.py b/cloud/cloudstack/cs_project.py index 943067ef5a4..f9b40b9ecd1 100644 --- a/cloud/cloudstack/cs_project.py +++ b/cloud/cloudstack/cs_project.py @@ -141,12 +141,6 @@ sample: '[ { "key": "foo", "value": "bar" } ]' ''' -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -287,9 +281,6 @@ def main(): supports_check_mode=True ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - try: acs_project = AnsibleCloudStackProject(module) diff --git a/cloud/cloudstack/cs_resourcelimit.py b/cloud/cloudstack/cs_resourcelimit.py index b53b3fb233d..40567165c5b 100644 --- a/cloud/cloudstack/cs_resourcelimit.py +++ b/cloud/cloudstack/cs_resourcelimit.py @@ -115,12 +115,6 @@ sample: example project ''' -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -206,9 +200,6 @@ def main(): supports_check_mode=True ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - try: acs_resource_limit = AnsibleCloudStackResourceLimit(module) resource_limit = acs_resource_limit.update_resource_limit() diff --git a/cloud/cloudstack/cs_router.py b/cloud/cloudstack/cs_router.py index 7209ef19a7f..29ac096c15d 100644 --- a/cloud/cloudstack/cs_router.py +++ b/cloud/cloudstack/cs_router.py @@ -160,12 +160,6 @@ sample: admin ''' -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -358,9 +352,6 @@ def main(): supports_check_mode=True ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - try: acs_router = AnsibleCloudStackRouter(module) diff --git a/cloud/cloudstack/cs_securitygroup.py b/cloud/cloudstack/cs_securitygroup.py index 2b0f901429d..edf4d533f42 100644 --- a/cloud/cloudstack/cs_securitygroup.py +++ b/cloud/cloudstack/cs_securitygroup.py @@ -113,12 +113,6 @@ sample: example account ''' -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -203,9 +197,6 @@ def main(): supports_check_mode=True ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - try: acs_sg = AnsibleCloudStackSecurityGroup(module) diff --git a/cloud/cloudstack/cs_securitygroup_rule.py b/cloud/cloudstack/cs_securitygroup_rule.py index 2a451933a01..eee4048ed2c 100644 --- a/cloud/cloudstack/cs_securitygroup_rule.py +++ b/cloud/cloudstack/cs_securitygroup_rule.py @@ -181,12 +181,6 @@ sample: 80 ''' -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -405,9 +399,6 @@ def main(): supports_check_mode=True ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - try: acs_sg_rule = AnsibleCloudStackSecurityGroupRule(module) diff --git a/cloud/cloudstack/cs_sshkeypair.py b/cloud/cloudstack/cs_sshkeypair.py index 7794303f019..c0c73d9f3bc 100644 --- a/cloud/cloudstack/cs_sshkeypair.py +++ b/cloud/cloudstack/cs_sshkeypair.py @@ -99,13 +99,6 @@ sample: "-----BEGIN RSA PRIVATE KEY-----\nMIICXQIBAAKBgQCkeFYjI+4k8bWfIRMzp4pCzhlopNydbbwRu824P5ilD4ATWMUG\nvEtuCQ2Mp5k5Bma30CdYHgh2/SbxC5RxXSUKTUJtTKpoJUy8PAhb1nn9dnfkC2oU\naRVi9NRUgypTIZxMpgooHOxvAzWxbZCyh1W+91Ld3FNaGxTLqTgeevY84wIDAQAB\nAoGAcwQwgLyUwsNB1vmjWwE0QEmvHS4FlhZyahhi4hGfZvbzAxSWHIK7YUT1c8KU\n9XsThEIN8aJ3GvcoL3OAqNKRnoNb14neejVHkYRadhxqc0GVN6AUIyCqoEMpvhFI\nQrinM572ORzv5ffRjCTbvZcYlW+sqFKNo5e8pYIB8TigpFECQQDu7bg9vkvg8xPs\nkP1K+EH0vsR6vUfy+m3euXjnbJtiP7RoTkZk0JQMOmexgy1qQhISWT0e451wd62v\nJ7M0trl5AkEAsDivJnMIlCCCypwPN4tdNUYpe9dtidR1zLmb3SA7wXk5xMUgLZI9\ncWPjBCMt0KKShdDhQ+hjXAyKQLF7iAPuOwJABjdHCMwvmy2XwhrPjCjDRoPEBtFv\n0sFzJE08+QBZVogDwIbwy+SlRWArnHGmN9J6N+H8dhZD3U4vxZPJ1MBAOQJBAJxO\nCv1dt1Q76gbwmYa49LnWO+F+2cgRTVODpr5iYt5fOmBQQRRqzFkRMkFvOqn+KVzM\nQ6LKM6dn8BEl295vLhUCQQCVDWzoSk3GjL3sOjfAUTyAj8VAXM69llaptxWWySPM\nE9pA+8rYmHfohYFx7FD5/KWCO+sfmxTNB48X0uwyE8tO\n-----END RSA PRIVATE KEY-----\n" ''' - -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - try: import sshpubkeys has_lib_sshpubkeys = True @@ -221,9 +214,6 @@ def main(): supports_check_mode=True ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - if not has_lib_sshpubkeys: module.fail_json(msg="python library sshpubkeys required: pip install sshpubkeys") diff --git a/cloud/cloudstack/cs_staticnat.py b/cloud/cloudstack/cs_staticnat.py index 5e406851ecf..bf3ed171564 100644 --- a/cloud/cloudstack/cs_staticnat.py +++ b/cloud/cloudstack/cs_staticnat.py @@ -146,13 +146,6 @@ sample: example domain ''' - -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -285,9 +278,6 @@ def main(): supports_check_mode=True ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - try: acs_static_nat = AnsibleCloudStackStaticNat(module) diff --git a/cloud/cloudstack/cs_template.py b/cloud/cloudstack/cs_template.py index daee15c1e25..01003e7b876 100644 --- a/cloud/cloudstack/cs_template.py +++ b/cloud/cloudstack/cs_template.py @@ -375,12 +375,6 @@ sample: Production ''' -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -643,9 +637,6 @@ def main(): supports_check_mode=True ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - try: acs_tpl = AnsibleCloudStackTemplate(module) diff --git a/cloud/cloudstack/cs_user.py b/cloud/cloudstack/cs_user.py index 0b2a1fddc63..cc233cba2cf 100644 --- a/cloud/cloudstack/cs_user.py +++ b/cloud/cloudstack/cs_user.py @@ -197,12 +197,6 @@ sample: ROOT ''' -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -424,9 +418,6 @@ def main(): supports_check_mode=True ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - try: acs_acc = AnsibleCloudStackUser(module) diff --git a/cloud/cloudstack/cs_vmsnapshot.py b/cloud/cloudstack/cs_vmsnapshot.py index bec9e5132e3..9a00cc91a5c 100644 --- a/cloud/cloudstack/cs_vmsnapshot.py +++ b/cloud/cloudstack/cs_vmsnapshot.py @@ -162,12 +162,6 @@ sample: Production ''' -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -282,9 +276,6 @@ def main(): supports_check_mode=True ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - try: acs_vmsnapshot = AnsibleCloudStackVmSnapshot(module) diff --git a/cloud/cloudstack/cs_volume.py b/cloud/cloudstack/cs_volume.py index b10d34a24ee..cb87b3622e2 100644 --- a/cloud/cloudstack/cs_volume.py +++ b/cloud/cloudstack/cs_volume.py @@ -230,12 +230,6 @@ sample: 1 ''' -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -469,9 +463,6 @@ def main(): supports_check_mode=True ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - try: acs_vol = AnsibleCloudStackVolume(module) diff --git a/cloud/cloudstack/cs_zone.py b/cloud/cloudstack/cs_zone.py index 84aad34726c..2a343e0b970 100644 --- a/cloud/cloudstack/cs_zone.py +++ b/cloud/cloudstack/cs_zone.py @@ -226,12 +226,6 @@ sample: [ { "key": "foo", "value": "bar" } ] ''' -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -386,9 +380,6 @@ def main(): supports_check_mode=True ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - try: acs_zone = AnsibleCloudStackZone(module) diff --git a/cloud/cloudstack/cs_zone_facts.py b/cloud/cloudstack/cs_zone_facts.py index 99897967311..7b5076659fd 100644 --- a/cloud/cloudstack/cs_zone_facts.py +++ b/cloud/cloudstack/cs_zone_facts.py @@ -138,12 +138,6 @@ import base64 -try: - from cs import CloudStack, CloudStackException, read_config - has_lib_cs = True -except ImportError: - has_lib_cs = False - # import cloudstack common from ansible.module_utils.cloudstack import * @@ -197,9 +191,6 @@ def main(): supports_check_mode=False, ) - if not has_lib_cs: - module.fail_json(msg="python library cs required: pip install cs") - cs_zone_facts = AnsibleCloudStackZoneFacts(module=module).run() cs_facts_result = dict(changed=False, ansible_facts=cs_zone_facts) module.exit_json(**cs_facts_result) From 122beec048d8ce357d9c3b282a08c96e85e956c4 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Wed, 8 Jun 2016 17:54:21 +0200 Subject: [PATCH 1681/2522] cloudstack: cs_account: overhaul code style --- cloud/cloudstack/cs_account.py | 105 +++++++++++++++------------------ 1 file changed, 47 insertions(+), 58 deletions(-) diff --git a/cloud/cloudstack/cs_account.py b/cloud/cloudstack/cs_account.py index ae977dea028..e8cc2920f99 100644 --- a/cloud/cloudstack/cs_account.py +++ b/cloud/cloudstack/cs_account.py @@ -190,28 +190,26 @@ def __init__(self, module): 'domain_admin': 2, } - def get_account_type(self): account_type = self.module.params.get('account_type') return self.account_types[account_type] - def get_account(self): if not self.account: - args = {} - args['listall'] = True - args['domainid'] = self.get_domain('id') + args = { + 'listall': True, + 'domainid': self.get_domain(key='id'), + } accounts = self.cs.listAccounts(**args) if accounts: account_name = self.module.params.get('name') for a in accounts['account']: - if account_name in [ a['name'] ]: + if account_name == a['name']: self.account = a break return self.account - def enable_account(self): account = self.get_account() if not account: @@ -219,10 +217,11 @@ def enable_account(self): if account['state'].lower() != 'enabled': self.result['changed'] = True - args = {} - args['id'] = account['id'] - args['account'] = self.module.params.get('name') - args['domainid'] = self.get_domain('id') + args = { + 'id': account['id'], + 'account': self.module.params.get('name'), + 'domainid': self.get_domain(key='id') + } if not self.module.check_mode: res = self.cs.enableAccount(**args) if 'errortext' in res: @@ -230,15 +229,12 @@ def enable_account(self): account = res['account'] return account - def lock_account(self): return self.lock_or_disable_account(lock=True) - def disable_account(self): return self.lock_or_disable_account() - def lock_or_disable_account(self, lock=False): account = self.get_account() if not account: @@ -248,14 +244,15 @@ def lock_or_disable_account(self, lock=False): if lock and account['state'].lower() == 'disabled': account = self.enable_account() - if lock and account['state'].lower() != 'locked' \ - or not lock and account['state'].lower() != 'disabled': + if (lock and account['state'].lower() != 'locked' or + not lock and account['state'].lower() != 'disabled'): self.result['changed'] = True - args = {} - args['id'] = account['id'] - args['account'] = self.module.params.get('name') - args['domainid'] = self.get_domain('id') - args['lock'] = lock + args = { + 'id': account['id'], + 'account': self.module.params.get('name'), + 'domainid': self.get_domain(key='id'), + 'lock': lock, + } if not self.module.check_mode: account = self.cs.disableAccount(**args) @@ -267,39 +264,33 @@ def lock_or_disable_account(self, lock=False): account = self._poll_job(account, 'account') return account - def present_account(self): - missing_params = [] - - missing_params = [] - for required_params in [ + required_params = [ 'email', 'username', 'password', 'first_name', 'last_name', - ]: - if not self.module.params.get(required_params): - missing_params.append(required_params) - if missing_params: - self.module.fail_json(msg="missing required arguments: %s" % ','.join(missing_params)) + ] + self.module.fail_on_missing_params(required_params=required_params) account = self.get_account() if not account: self.result['changed'] = True - args = {} - args['account'] = self.module.params.get('name') - args['domainid'] = self.get_domain('id') - args['accounttype'] = self.get_account_type() - args['networkdomain'] = self.module.params.get('network_domain') - args['username'] = self.module.params.get('username') - args['password'] = self.module.params.get('password') - args['firstname'] = self.module.params.get('first_name') - args['lastname'] = self.module.params.get('last_name') - args['email'] = self.module.params.get('email') - args['timezone'] = self.module.params.get('timezone') + args = { + 'account': self.module.params.get('name'), + 'domainid': self.get_domain(key='id'), + 'accounttype': self.get_account_type(), + 'networkdomain': self.module.params.get('network_domain'), + 'username': self.module.params.get('username'), + 'password': self.module.params.get('password'), + 'firstname': self.module.params.get('first_name'), + 'lastname': self.module.params.get('last_name'), + 'email': self.module.params.get('email'), + 'timezone': self.module.params.get('timezone') + } if not self.module.check_mode: res = self.cs.createAccount(**args) if 'errortext' in res: @@ -307,7 +298,6 @@ def present_account(self): account = res['account'] return account - def absent_account(self): account = self.get_account() if account: @@ -321,15 +311,14 @@ def absent_account(self): poll_async = self.module.params.get('poll_async') if poll_async: - res = self._poll_job(res, 'account') + self._poll_job(res, 'account') return account - def get_result(self, account): super(AnsibleCloudStackAccount, self).get_result(account) if account: if 'accounttype' in account: - for key,value in self.account_types.items(): + for key, value in self.account_types.items(): if value == account['accounttype']: self.result['account_type'] = key break @@ -339,18 +328,18 @@ def get_result(self, account): def main(): argument_spec = cs_argument_spec() argument_spec.update(dict( - name = dict(required=True), - state = dict(choices=['present', 'absent', 'enabled', 'disabled', 'locked', 'unlocked'], default='present'), - account_type = dict(choices=['user', 'root_admin', 'domain_admin'], default='user'), - network_domain = dict(default=None), - domain = dict(default='ROOT'), - email = dict(default=None), - first_name = dict(default=None), - last_name = dict(default=None), - username = dict(default=None), - password = dict(default=None, no_log=True), - timezone = dict(default=None), - poll_async = dict(type='bool', default=True), + name=dict(required=True), + state=dict(choices=['present', 'absent', 'enabled', 'disabled', 'locked', 'unlocked'], default='present'), + account_type=dict(choices=['user', 'root_admin', 'domain_admin'], default='user'), + network_domain=dict(default=None), + domain=dict(default='ROOT'), + email=dict(default=None), + first_name=dict(default=None), + last_name=dict(default=None), + username=dict(default=None), + password=dict(default=None, no_log=True), + timezone=dict(default=None), + poll_async=dict(type='bool', default=True), )) module = AnsibleModule( From a3f57a8cf4c0f4217702f08c5fff1c0f5c32d398 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Wed, 8 Jun 2016 18:01:29 +0200 Subject: [PATCH 1682/2522] cloudstack: cs_affinitygroup: overhaul code style --- cloud/cloudstack/cs_affinitygroup.py | 62 +++++++++++++--------------- 1 file changed, 29 insertions(+), 33 deletions(-) diff --git a/cloud/cloudstack/cs_affinitygroup.py b/cloud/cloudstack/cs_affinitygroup.py index d476d25a76c..ce18c767ce7 100644 --- a/cloud/cloudstack/cs_affinitygroup.py +++ b/cloud/cloudstack/cs_affinitygroup.py @@ -136,22 +136,20 @@ def __init__(self, module): } self.affinity_group = None - def get_affinity_group(self): if not self.affinity_group: - args = {} - args['projectid'] = self.get_project(key='id') - args['account'] = self.get_account('name') - args['domainid'] = self.get_domain('id') - args['name'] = self.module.params.get('name') - + args = { + 'projectid': self.get_project(key='id'), + 'account': self.get_account(key='name'), + 'domainid': self.get_domain(key='id'), + 'name': self.module.params.get('name'), + } affinity_groups = self.cs.listAffinityGroups(**args) if affinity_groups: self.affinity_group = affinity_groups['affinitygroup'][0] return self.affinity_group - def get_affinity_type(self): affinity_type = self.module.params.get('affinty_type') @@ -165,20 +163,19 @@ def get_affinity_type(self): return a['type'] self.module.fail_json(msg="affinity group type '%s' not found" % affinity_type) - def create_affinity_group(self): affinity_group = self.get_affinity_group() if not affinity_group: self.result['changed'] = True - args = {} - args['name'] = self.module.params.get('name') - args['type'] = self.get_affinity_type() - args['description'] = self.module.params.get('description') - args['projectid'] = self.get_project(key='id') - args['account'] = self.get_account('name') - args['domainid'] = self.get_domain('id') - + args = { + 'name': self.module.params.get('name'), + 'type': self.get_affinity_type(), + 'description': self.module.params.get('description'), + 'projectid': self.get_project(key='id'), + 'account': self.get_account(key='name'), + 'domainid': self.get_domain(key='id'), + } if not self.module.check_mode: res = self.cs.createAffinityGroup(**args) @@ -190,18 +187,17 @@ def create_affinity_group(self): affinity_group = self._poll_job(res, 'affinitygroup') return affinity_group - def remove_affinity_group(self): affinity_group = self.get_affinity_group() if affinity_group: self.result['changed'] = True - args = {} - args['name'] = self.module.params.get('name') - args['projectid'] = self.get_project(key='id') - args['account'] = self.get_account('name') - args['domainid'] = self.get_domain('id') - + args = { + 'name': self.module.params.get('name'), + 'projectid': self.get_project(key='id'), + 'account': self.get_account(key='name'), + 'domainid': self.get_domain(key='id'), + } if not self.module.check_mode: res = self.cs.deleteAffinityGroup(**args) @@ -210,21 +206,21 @@ def remove_affinity_group(self): poll_async = self.module.params.get('poll_async') if res and poll_async: - res = self._poll_job(res, 'affinitygroup') + self._poll_job(res, 'affinitygroup') return affinity_group def main(): argument_spec = cs_argument_spec() argument_spec.update(dict( - name = dict(required=True), - affinty_type = dict(default=None), - description = dict(default=None), - state = dict(choices=['present', 'absent'], default='present'), - domain = dict(default=None), - account = dict(default=None), - project = dict(default=None), - poll_async = dict(type='bool', default=True), + name=dict(required=True), + affinty_type=dict(default=None), + description=dict(default=None), + state=dict(choices=['present', 'absent'], default='present'), + domain=dict(default=None), + account=dict(default=None), + project=dict(default=None), + poll_async=dict(type='bool', default=True), )) module = AnsibleModule( From c693be53f54213c485737508b29e04ba542d8ac4 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Thu, 9 Jun 2016 21:22:19 +0200 Subject: [PATCH 1683/2522] cloudstack: cs_cluster: overhaul code style --- cloud/cloudstack/cs_cluster.py | 71 ++++++++++++++++------------------ 1 file changed, 33 insertions(+), 38 deletions(-) diff --git a/cloud/cloudstack/cs_cluster.py b/cloud/cloudstack/cs_cluster.py index 66aec535a84..4834c07b65d 100644 --- a/cloud/cloudstack/cs_cluster.py +++ b/cloud/cloudstack/cs_cluster.py @@ -229,6 +229,7 @@ # import cloudstack common from ansible.module_utils.cloudstack import * + class AnsibleCloudStackCluster(AnsibleCloudStack): def __init__(self, module): @@ -245,29 +246,27 @@ def __init__(self, module): } self.cluster = None - def _get_common_cluster_args(self): - args = {} - args['clustername'] = self.module.params.get('name') - args['hypervisor'] = self.module.params.get('hypervisor') - args['clustertype'] = self.module.params.get('cluster_type') - + args = { + 'clustername': self.module.params.get('name'), + 'hypervisor': self.module.params.get('hypervisor'), + 'clustertype': self.module.params.get('cluster_type'), + } state = self.module.params.get('state') - if state in [ 'enabled', 'disabled']: + if state in ['enabled', 'disabled']: args['allocationstate'] = state.capitalize() return args - def get_pod(self, key=None): - args = {} - args['name'] = self.module.params.get('pod') - args['zoneid'] = self.get_zone(key='id') + args = { + 'name': self.module.params.get('pod'), + 'zoneid': self.get_zone(key='id'), + } pods = self.cs.listPods(**args) if pods: return self._get_by_key(key, pods['pod'][0]) self.module.fail_json(msg="Pod %s not found in zone %s." % (self.module.params.get('pod'), self.get_zone(key='name'))) - def get_cluster(self): if not self.cluster: args = {} @@ -289,7 +288,6 @@ def get_cluster(self): self.cluster['clustername'] = self.cluster['name'] return self.cluster - def present_cluster(self): cluster = self.get_cluster() if cluster: @@ -298,7 +296,6 @@ def present_cluster(self): cluster = self._create_cluster() return cluster - def _create_cluster(self): required_params = [ 'cluster_type', @@ -337,7 +334,6 @@ def _create_cluster(self): cluster = res['cluster'] return cluster - def _update_cluster(self): cluster = self.get_cluster() @@ -354,15 +350,14 @@ def _update_cluster(self): cluster = res['cluster'] return cluster - def absent_cluster(self): cluster = self.get_cluster() if cluster: self.result['changed'] = True - args = {} - args['id'] = cluster['id'] - + args = { + 'id': cluster['id'], + } if not self.module.check_mode: res = self.cs.deleteCluster(**args) if 'errortext' in res: @@ -373,25 +368,25 @@ def absent_cluster(self): def main(): argument_spec = cs_argument_spec() argument_spec.update(dict( - name = dict(required=True), - zone = dict(default=None), - pod = dict(default=None), - cluster_type = dict(choices=['CloudManaged', 'ExternalManaged'], default=None), - hypervisor = dict(choices=CS_HYPERVISORS, default=None), - state = dict(choices=['present', 'enabled', 'disabled', 'absent'], default='present'), - url = dict(default=None), - username = dict(default=None), - password = dict(default=None, no_log=True), - guest_vswitch_name = dict(default=None), - guest_vswitch_type = dict(choices=['vmwaresvs', 'vmwaredvs'], default=None), - public_vswitch_name = dict(default=None), - public_vswitch_type = dict(choices=['vmwaresvs', 'vmwaredvs'], default=None), - vms_ip_address = dict(default=None), - vms_username = dict(default=None), - vms_password = dict(default=None, no_log=True), - ovm3_cluster = dict(default=None), - ovm3_pool = dict(default=None), - ovm3_vip = dict(default=None), + name=dict(required=True), + zone=dict(default=None), + pod=dict(default=None), + cluster_type=dict(choices=['CloudManaged', 'ExternalManaged'], default=None), + hypervisor=dict(choices=CS_HYPERVISORS, default=None), + state=dict(choices=['present', 'enabled', 'disabled', 'absent'], default='present'), + url=dict(default=None), + username=dict(default=None), + password=dict(default=None, no_log=True), + guest_vswitch_name=dict(default=None), + guest_vswitch_type=dict(choices=['vmwaresvs', 'vmwaredvs'], default=None), + public_vswitch_name=dict(default=None), + public_vswitch_type=dict(choices=['vmwaresvs', 'vmwaredvs'], default=None), + vms_ip_address=dict(default=None), + vms_username=dict(default=None), + vms_password=dict(default=None, no_log=True), + ovm3_cluster=dict(default=None), + ovm3_pool=dict(default=None), + ovm3_vip=dict(default=None), )) module = AnsibleModule( From 8a235e4e531bf29a3387416a0258e18ce243bd7c Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Thu, 9 Jun 2016 22:39:03 +0200 Subject: [PATCH 1684/2522] cloudstack: use poll_job() from utils --- cloud/cloudstack/cs_account.py | 4 ++-- cloud/cloudstack/cs_affinitygroup.py | 4 ++-- cloud/cloudstack/cs_domain.py | 2 +- cloud/cloudstack/cs_firewall.py | 4 ++-- cloud/cloudstack/cs_instance.py | 18 +++++++++--------- cloud/cloudstack/cs_ip_address.py | 4 ++-- cloud/cloudstack/cs_loadbalancer_rule.py | 2 +- cloud/cloudstack/cs_network.py | 6 +++--- cloud/cloudstack/cs_portforward.py | 6 +++--- cloud/cloudstack/cs_project.py | 8 ++++---- cloud/cloudstack/cs_router.py | 8 ++++---- cloud/cloudstack/cs_securitygroup_rule.py | 4 ++-- cloud/cloudstack/cs_staticnat.py | 4 ++-- cloud/cloudstack/cs_template.py | 6 +++--- cloud/cloudstack/cs_user.py | 2 +- cloud/cloudstack/cs_vmsnapshot.py | 6 +++--- 16 files changed, 44 insertions(+), 44 deletions(-) diff --git a/cloud/cloudstack/cs_account.py b/cloud/cloudstack/cs_account.py index e8cc2920f99..d4b27dea798 100644 --- a/cloud/cloudstack/cs_account.py +++ b/cloud/cloudstack/cs_account.py @@ -261,7 +261,7 @@ def lock_or_disable_account(self, lock=False): poll_async = self.module.params.get('poll_async') if poll_async: - account = self._poll_job(account, 'account') + account = self.poll_job(account, 'account') return account def present_account(self): @@ -311,7 +311,7 @@ def absent_account(self): poll_async = self.module.params.get('poll_async') if poll_async: - self._poll_job(res, 'account') + self.poll_job(res, 'account') return account def get_result(self, account): diff --git a/cloud/cloudstack/cs_affinitygroup.py b/cloud/cloudstack/cs_affinitygroup.py index ce18c767ce7..2ffe2bace14 100644 --- a/cloud/cloudstack/cs_affinitygroup.py +++ b/cloud/cloudstack/cs_affinitygroup.py @@ -184,7 +184,7 @@ def create_affinity_group(self): poll_async = self.module.params.get('poll_async') if res and poll_async: - affinity_group = self._poll_job(res, 'affinitygroup') + affinity_group = self.poll_job(res, 'affinitygroup') return affinity_group def remove_affinity_group(self): @@ -206,7 +206,7 @@ def remove_affinity_group(self): poll_async = self.module.params.get('poll_async') if res and poll_async: - self._poll_job(res, 'affinitygroup') + self.poll_job(res, 'affinitygroup') return affinity_group diff --git a/cloud/cloudstack/cs_domain.py b/cloud/cloudstack/cs_domain.py index a9b3aa085b3..556abc6ffcc 100644 --- a/cloud/cloudstack/cs_domain.py +++ b/cloud/cloudstack/cs_domain.py @@ -227,7 +227,7 @@ def absent_domain(self): poll_async = self.module.params.get('poll_async') if poll_async: - res = self._poll_job(res, 'domain') + res = self.poll_job(res, 'domain') return domain diff --git a/cloud/cloudstack/cs_firewall.py b/cloud/cloudstack/cs_firewall.py index c782c93e3b4..1a677da4dfd 100644 --- a/cloud/cloudstack/cs_firewall.py +++ b/cloud/cloudstack/cs_firewall.py @@ -331,7 +331,7 @@ def create_firewall_rule(self): poll_async = self.module.params.get('poll_async') if poll_async: - firewall_rule = self._poll_job(res, 'firewallrule') + firewall_rule = self.poll_job(res, 'firewallrule') return firewall_rule @@ -355,7 +355,7 @@ def remove_firewall_rule(self): poll_async = self.module.params.get('poll_async') if poll_async: - res = self._poll_job(res, 'firewallrule') + res = self.poll_job(res, 'firewallrule') return firewall_rule diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 202796f0bc2..9e1a06d1c6f 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -666,7 +666,7 @@ def deploy_instance(self, start_vm=True): poll_async = self.module.params.get('poll_async') if poll_async: - instance = self._poll_job(instance, 'virtualmachine') + instance = self.poll_job(instance, 'virtualmachine') return instance @@ -715,7 +715,7 @@ def update_instance(self, instance, start_vm=True): # Ensure VM has stopped instance = self.stop_instance() - instance = self._poll_job(instance, 'virtualmachine') + instance = self.poll_job(instance, 'virtualmachine') self.instance = instance # Change service offering @@ -742,7 +742,7 @@ def update_instance(self, instance, start_vm=True): if 'errortext' in instance: self.module.fail_json(msg="Failed: '%s'" % instance['errortext']) - instance = self._poll_job(instance, 'virtualmachine') + instance = self.poll_job(instance, 'virtualmachine') self.instance = instance # Start VM again if it was running before @@ -775,7 +775,7 @@ def absent_instance(self): poll_async = self.module.params.get('poll_async') if poll_async: - instance = self._poll_job(res, 'virtualmachine') + instance = self.poll_job(res, 'virtualmachine') return instance @@ -798,7 +798,7 @@ def expunge_instance(self): poll_async = self.module.params.get('poll_async') if poll_async: - res = self._poll_job(res, 'virtualmachine') + res = self.poll_job(res, 'virtualmachine') return instance @@ -819,7 +819,7 @@ def stop_instance(self): poll_async = self.module.params.get('poll_async') if poll_async: - instance = self._poll_job(instance, 'virtualmachine') + instance = self.poll_job(instance, 'virtualmachine') return instance @@ -840,7 +840,7 @@ def start_instance(self): poll_async = self.module.params.get('poll_async') if poll_async: - instance = self._poll_job(instance, 'virtualmachine') + instance = self.poll_job(instance, 'virtualmachine') return instance @@ -858,7 +858,7 @@ def restart_instance(self): poll_async = self.module.params.get('poll_async') if poll_async: - instance = self._poll_job(instance, 'virtualmachine') + instance = self.poll_job(instance, 'virtualmachine') elif instance['state'].lower() in [ 'stopping', 'stopped' ]: instance = self.start_instance() @@ -879,7 +879,7 @@ def restore_instance(self): poll_async = self.module.params.get('poll_async') if poll_async: - instance = self._poll_job(res, 'virtualmachine') + instance = self.poll_job(res, 'virtualmachine') return instance diff --git a/cloud/cloudstack/cs_ip_address.py b/cloud/cloudstack/cs_ip_address.py index bb6344de0b6..4d4eae2f787 100644 --- a/cloud/cloudstack/cs_ip_address.py +++ b/cloud/cloudstack/cs_ip_address.py @@ -202,7 +202,7 @@ def associate_ip_address(self): poll_async = self.module.params.get('poll_async') if poll_async: - res = self._poll_job(res, 'ipaddress') + res = self.poll_job(res, 'ipaddress') ip_address = res return ip_address @@ -221,7 +221,7 @@ def disassociate_ip_address(self): self.module.fail_json(msg="Failed: '%s'" % res['errortext']) poll_async = self.module.params.get('poll_async') if poll_async: - self._poll_job(res, 'ipaddress') + self.poll_job(res, 'ipaddress') return ip_address diff --git a/cloud/cloudstack/cs_loadbalancer_rule.py b/cloud/cloudstack/cs_loadbalancer_rule.py index cc8a029d3a4..83eb8883602 100644 --- a/cloud/cloudstack/cs_loadbalancer_rule.py +++ b/cloud/cloudstack/cs_loadbalancer_rule.py @@ -327,7 +327,7 @@ def absent_lb_rule(self): self.module.fail_json(msg="Failed: '%s'" % res['errortext']) poll_async = self.module.params.get('poll_async') if poll_async: - res = self._poll_job(res, 'loadbalancer') + res = self.poll_job(res, 'loadbalancer') return rule diff --git a/cloud/cloudstack/cs_network.py b/cloud/cloudstack/cs_network.py index ee8888ff8e8..2f4a2acaca0 100644 --- a/cloud/cloudstack/cs_network.py +++ b/cloud/cloudstack/cs_network.py @@ -432,7 +432,7 @@ def update_network(self, network): poll_async = self.module.params.get('poll_async') if network and poll_async: - network = self._poll_job(network, 'network') + network = self.poll_job(network, 'network') return network @@ -490,7 +490,7 @@ def restart_network(self): poll_async = self.module.params.get('poll_async') if network and poll_async: - network = self._poll_job(network, 'network') + network = self.poll_job(network, 'network') return network @@ -510,7 +510,7 @@ def absent_network(self): poll_async = self.module.params.get('poll_async') if res and poll_async: - res = self._poll_job(res, 'network') + res = self.poll_job(res, 'network') return network diff --git a/cloud/cloudstack/cs_portforward.py b/cloud/cloudstack/cs_portforward.py index 79c57e5502b..8a818b78745 100644 --- a/cloud/cloudstack/cs_portforward.py +++ b/cloud/cloudstack/cs_portforward.py @@ -305,7 +305,7 @@ def create_portforwarding_rule(self): portforwarding_rule = self.cs.createPortForwardingRule(**args) poll_async = self.module.params.get('poll_async') if poll_async: - portforwarding_rule = self._poll_job(portforwarding_rule, 'portforwardingrule') + portforwarding_rule = self.poll_job(portforwarding_rule, 'portforwardingrule') return portforwarding_rule @@ -329,7 +329,7 @@ def update_portforwarding_rule(self, portforwarding_rule): portforwarding_rule = self.cs.createPortForwardingRule(**args) poll_async = self.module.params.get('poll_async') if poll_async: - portforwarding_rule = self._poll_job(portforwarding_rule, 'portforwardingrule') + portforwarding_rule = self.poll_job(portforwarding_rule, 'portforwardingrule') return portforwarding_rule @@ -345,7 +345,7 @@ def absent_portforwarding_rule(self): res = self.cs.deletePortForwardingRule(**args) poll_async = self.module.params.get('poll_async') if poll_async: - self._poll_job(res, 'portforwardingrule') + self.poll_job(res, 'portforwardingrule') return portforwarding_rule diff --git a/cloud/cloudstack/cs_project.py b/cloud/cloudstack/cs_project.py index f9b40b9ecd1..3635268e6ae 100644 --- a/cloud/cloudstack/cs_project.py +++ b/cloud/cloudstack/cs_project.py @@ -193,7 +193,7 @@ def update_project(self, project): poll_async = self.module.params.get('poll_async') if project and poll_async: - project = self._poll_job(project, 'project') + project = self.poll_job(project, 'project') return project @@ -214,7 +214,7 @@ def create_project(self, project): poll_async = self.module.params.get('poll_async') if project and poll_async: - project = self._poll_job(project, 'project') + project = self.poll_job(project, 'project') return project @@ -238,7 +238,7 @@ def state_project(self, state='active'): poll_async = self.module.params.get('poll_async') if project and poll_async: - project = self._poll_job(project, 'project') + project = self.poll_job(project, 'project') return project @@ -258,7 +258,7 @@ def absent_project(self): poll_async = self.module.params.get('poll_async') if res and poll_async: - res = self._poll_job(res, 'project') + res = self.poll_job(res, 'project') return project diff --git a/cloud/cloudstack/cs_router.py b/cloud/cloudstack/cs_router.py index 29ac096c15d..73575c80010 100644 --- a/cloud/cloudstack/cs_router.py +++ b/cloud/cloudstack/cs_router.py @@ -227,7 +227,7 @@ def start_router(self): poll_async = self.module.params.get('poll_async') if poll_async: - router = self._poll_job(res, 'router') + router = self.poll_job(res, 'router') return router def stop_router(self): @@ -248,7 +248,7 @@ def stop_router(self): poll_async = self.module.params.get('poll_async') if poll_async: - router = self._poll_job(res, 'router') + router = self.poll_job(res, 'router') return router def reboot_router(self): @@ -268,7 +268,7 @@ def reboot_router(self): poll_async = self.module.params.get('poll_async') if poll_async: - router = self._poll_job(res, 'router') + router = self.poll_job(res, 'router') return router def absent_router(self): @@ -287,7 +287,7 @@ def absent_router(self): poll_async = self.module.params.get('poll_async') if poll_async: - self._poll_job(res, 'router') + self.poll_job(res, 'router') return router diff --git a/cloud/cloudstack/cs_securitygroup_rule.py b/cloud/cloudstack/cs_securitygroup_rule.py index eee4048ed2c..5ac22960b57 100644 --- a/cloud/cloudstack/cs_securitygroup_rule.py +++ b/cloud/cloudstack/cs_securitygroup_rule.py @@ -323,7 +323,7 @@ def add_rule(self): poll_async = self.module.params.get('poll_async') if res and poll_async: - security_group = self._poll_job(res, 'securitygroup') + security_group = self.poll_job(res, 'securitygroup') key = sg_type + "rule" # ingressrule / egressrule if key in security_group: rule = security_group[key][0] @@ -354,7 +354,7 @@ def remove_rule(self): poll_async = self.module.params.get('poll_async') if res and poll_async: - res = self._poll_job(res, 'securitygroup') + res = self.poll_job(res, 'securitygroup') return rule diff --git a/cloud/cloudstack/cs_staticnat.py b/cloud/cloudstack/cs_staticnat.py index bf3ed171564..cf8213a8813 100644 --- a/cloud/cloudstack/cs_staticnat.py +++ b/cloud/cloudstack/cs_staticnat.py @@ -223,7 +223,7 @@ def update_static_nat(self, ip_address): res = self.cs.disableStaticNat(ipaddressid=ip_address['id']) if 'errortext' in res: self.module.fail_json(msg="Failed: '%s'" % res['errortext']) - self._poll_job(res, 'staticnat') + self.poll_job(res, 'staticnat') res = self.cs.enableStaticNat(**args) if 'errortext' in res: self.module.fail_json(msg="Failed: '%s'" % res['errortext']) @@ -253,7 +253,7 @@ def absent_static_nat(self): self.module.fail_json(msg="Failed: '%s'" % res['errortext']) poll_async = self.module.params.get('poll_async') if poll_async: - self._poll_job(res, 'staticnat') + self.poll_job(res, 'staticnat') return ip_address diff --git a/cloud/cloudstack/cs_template.py b/cloud/cloudstack/cs_template.py index 01003e7b876..3db11755184 100644 --- a/cloud/cloudstack/cs_template.py +++ b/cloud/cloudstack/cs_template.py @@ -476,7 +476,7 @@ def create_template(self): poll_async = self.module.params.get('poll_async') if poll_async: - template = self._poll_job(template, 'template') + template = self.poll_job(template, 'template') return template @@ -564,7 +564,7 @@ def extract_template(self): poll_async = self.module.params.get('poll_async') if poll_async: - template = self._poll_job(template, 'template') + template = self.poll_job(template, 'template') return template @@ -587,7 +587,7 @@ def remove_template(self): poll_async = self.module.params.get('poll_async') if poll_async: - res = self._poll_job(res, 'template') + res = self.poll_job(res, 'template') return template diff --git a/cloud/cloudstack/cs_user.py b/cloud/cloudstack/cs_user.py index cc233cba2cf..bee4653d163 100644 --- a/cloud/cloudstack/cs_user.py +++ b/cloud/cloudstack/cs_user.py @@ -297,7 +297,7 @@ def disable_user(self): poll_async = self.module.params.get('poll_async') if poll_async: - user = self._poll_job(user, 'user') + user = self.poll_job(user, 'user') return user diff --git a/cloud/cloudstack/cs_vmsnapshot.py b/cloud/cloudstack/cs_vmsnapshot.py index 9a00cc91a5c..29d19149935 100644 --- a/cloud/cloudstack/cs_vmsnapshot.py +++ b/cloud/cloudstack/cs_vmsnapshot.py @@ -209,7 +209,7 @@ def create_snapshot(self): poll_async = self.module.params.get('poll_async') if res and poll_async: - snapshot = self._poll_job(res, 'vmsnapshot') + snapshot = self.poll_job(res, 'vmsnapshot') return snapshot @@ -226,7 +226,7 @@ def remove_snapshot(self): poll_async = self.module.params.get('poll_async') if res and poll_async: - res = self._poll_job(res, 'vmsnapshot') + res = self.poll_job(res, 'vmsnapshot') return snapshot @@ -243,7 +243,7 @@ def revert_vm_to_snapshot(self): poll_async = self.module.params.get('poll_async') if res and poll_async: - res = self._poll_job(res, 'vmsnapshot') + res = self.poll_job(res, 'vmsnapshot') return snapshot self.module.fail_json(msg="snapshot not found, could not revert VM") From aef2da24016ae139d37a25e4abb87138352f1f50 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Thu, 9 Jun 2016 22:41:32 +0200 Subject: [PATCH 1685/2522] cloudstack: use has_changed() from utils --- cloud/cloudstack/cs_domain.py | 2 +- cloud/cloudstack/cs_instance.py | 6 +++--- cloud/cloudstack/cs_network.py | 2 +- cloud/cloudstack/cs_portforward.py | 2 +- cloud/cloudstack/cs_project.py | 2 +- cloud/cloudstack/cs_staticnat.py | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cloud/cloudstack/cs_domain.py b/cloud/cloudstack/cs_domain.py index 556abc6ffcc..17c93a84614 100644 --- a/cloud/cloudstack/cs_domain.py +++ b/cloud/cloudstack/cs_domain.py @@ -201,7 +201,7 @@ def update_domain(self, domain): args['id'] = domain['id'] args['networkdomain'] = self.module.params.get('network_domain') - if self._has_changed(args, domain): + if self.has_changed(args, domain): self.result['changed'] = True if not self.module.check_mode: res = self.cs.updateDomain(**args) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 9e1a06d1c6f..03c703c4782 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -676,7 +676,7 @@ def update_instance(self, instance, start_vm=True): args_service_offering['id'] = instance['id'] if self.module.params.get('service_offering'): args_service_offering['serviceofferingid'] = self.get_service_offering_id() - service_offering_changed = self._has_changed(args_service_offering, instance) + service_offering_changed = self.has_changed(args_service_offering, instance) # Instance data args_instance_update = {} @@ -687,7 +687,7 @@ def update_instance(self, instance, start_vm=True): args_instance_update['group'] = self.module.params.get('group') if self.module.params.get('display_name'): args_instance_update['displayname'] = self.module.params.get('display_name') - instance_changed = self._has_changed(args_instance_update, instance) + instance_changed = self.has_changed(args_instance_update, instance) # SSH key data args_ssh_key = {} @@ -695,7 +695,7 @@ def update_instance(self, instance, start_vm=True): args_ssh_key['projectid'] = self.get_project(key='id') if self.module.params.get('ssh_key'): args_ssh_key['keypair'] = self.module.params.get('ssh_key') - ssh_key_changed = self._has_changed(args_ssh_key, instance) + ssh_key_changed = self.has_changed(args_ssh_key, instance) security_groups_changed = self.security_groups_has_changed() diff --git a/cloud/cloudstack/cs_network.py b/cloud/cloudstack/cs_network.py index 2f4a2acaca0..69206d8105f 100644 --- a/cloud/cloudstack/cs_network.py +++ b/cloud/cloudstack/cs_network.py @@ -422,7 +422,7 @@ def update_network(self, network): args = self._get_args() args['id'] = network['id'] - if self._has_changed(args, network): + if self.has_changed(args, network): self.result['changed'] = True if not self.module.check_mode: network = self.cs.updateNetwork(**args) diff --git a/cloud/cloudstack/cs_portforward.py b/cloud/cloudstack/cs_portforward.py index 8a818b78745..330caaa1bc1 100644 --- a/cloud/cloudstack/cs_portforward.py +++ b/cloud/cloudstack/cs_portforward.py @@ -320,7 +320,7 @@ def update_portforwarding_rule(self, portforwarding_rule): args['ipaddressid'] = self.get_ip_address(key='id') args['virtualmachineid'] = self.get_vm(key='id') - if self._has_changed(args, portforwarding_rule): + if self.has_changed(args, portforwarding_rule): self.result['changed'] = True if not self.module.check_mode: # API broken in 4.2.1?, workaround using remove/create instead of update diff --git a/cloud/cloudstack/cs_project.py b/cloud/cloudstack/cs_project.py index 3635268e6ae..6f3d41b3914 100644 --- a/cloud/cloudstack/cs_project.py +++ b/cloud/cloudstack/cs_project.py @@ -183,7 +183,7 @@ def update_project(self, project): args['id'] = project['id'] args['displaytext'] = self.get_or_fallback('display_text', 'name') - if self._has_changed(args, project): + if self.has_changed(args, project): self.result['changed'] = True if not self.module.check_mode: project = self.cs.updateProject(**args) diff --git a/cloud/cloudstack/cs_staticnat.py b/cloud/cloudstack/cs_staticnat.py index cf8213a8813..71cf0493a0f 100644 --- a/cloud/cloudstack/cs_staticnat.py +++ b/cloud/cloudstack/cs_staticnat.py @@ -217,7 +217,7 @@ def update_static_nat(self, ip_address): # make an alias, so we can use _has_changed() ip_address['vmguestip'] = ip_address['vmipaddress'] - if self._has_changed(args, ip_address): + if self.has_changed(args, ip_address): self.result['changed'] = True if not self.module.check_mode: res = self.cs.disableStaticNat(ipaddressid=ip_address['id']) From 00fe782722cb1c407acfdf223a281d4e83df6d3b Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Thu, 9 Jun 2016 22:54:25 +0200 Subject: [PATCH 1686/2522] cloudstack: cs_staticnat: fix static nat was always changed (disabled/enabled) Fixes firewall rules get lost after use of cs_staticnat. --- cloud/cloudstack/cs_staticnat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/cloudstack/cs_staticnat.py b/cloud/cloudstack/cs_staticnat.py index 71cf0493a0f..26c5ce022c0 100644 --- a/cloud/cloudstack/cs_staticnat.py +++ b/cloud/cloudstack/cs_staticnat.py @@ -217,7 +217,7 @@ def update_static_nat(self, ip_address): # make an alias, so we can use _has_changed() ip_address['vmguestip'] = ip_address['vmipaddress'] - if self.has_changed(args, ip_address): + if self.has_changed(args, ip_address, ['vmguestip', 'virtualmachineid']): self.result['changed'] = True if not self.module.check_mode: res = self.cs.disableStaticNat(ipaddressid=ip_address['id']) From 206cfb3125c1d24f378b2f4ce60baf6cd79eac81 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Thu, 9 Jun 2016 22:58:09 +0200 Subject: [PATCH 1687/2522] cloudstack: move duplicate common code to utils --- cloud/cloudstack/cs_portforward.py | 27 --------------------------- cloud/cloudstack/cs_staticnat.py | 29 ----------------------------- 2 files changed, 56 deletions(-) diff --git a/cloud/cloudstack/cs_portforward.py b/cloud/cloudstack/cs_portforward.py index 330caaa1bc1..3c492c54618 100644 --- a/cloud/cloudstack/cs_portforward.py +++ b/cloud/cloudstack/cs_portforward.py @@ -227,33 +227,6 @@ def __init__(self, module): 'privateendport': 'private_end_port', } self.portforwarding_rule = None - self.vm_default_nic = None - - - def get_vm_guest_ip(self): - vm_guest_ip = self.module.params.get('vm_guest_ip') - default_nic = self.get_vm_default_nic() - - if not vm_guest_ip: - return default_nic['ipaddress'] - - for secondary_ip in default_nic['secondaryip']: - if vm_guest_ip == secondary_ip['ipaddress']: - return vm_guest_ip - self.module.fail_json(msg="Secondary IP '%s' not assigned to VM" % vm_guest_ip) - - - def get_vm_default_nic(self): - if self.vm_default_nic: - return self.vm_default_nic - - nics = self.cs.listNics(virtualmachineid=self.get_vm(key='id')) - if nics: - for n in nics['nic']: - if n['isdefault']: - self.vm_default_nic = n - return self.vm_default_nic - self.module.fail_json(msg="No default IP address of VM '%s' found" % self.module.params.get('vm')) def get_portforwarding_rule(self): diff --git a/cloud/cloudstack/cs_staticnat.py b/cloud/cloudstack/cs_staticnat.py index 26c5ce022c0..1d721612b2d 100644 --- a/cloud/cloudstack/cs_staticnat.py +++ b/cloud/cloudstack/cs_staticnat.py @@ -160,35 +160,6 @@ def __init__(self, module): 'ipaddress': 'ip_address', 'vmipaddress': 'vm_guest_ip', } - self.vm_default_nic = None - - -# TODO: move it to cloudstack utils, also used in cs_portforward - def get_vm_guest_ip(self): - vm_guest_ip = self.module.params.get('vm_guest_ip') - default_nic = self.get_vm_default_nic() - - if not vm_guest_ip: - return default_nic['ipaddress'] - - for secondary_ip in default_nic['secondaryip']: - if vm_guest_ip == secondary_ip['ipaddress']: - return vm_guest_ip - self.module.fail_json(msg="Secondary IP '%s' not assigned to VM" % vm_guest_ip) - - -# TODO: move it to cloudstack utils, also used in cs_portforward - def get_vm_default_nic(self): - if self.vm_default_nic: - return self.vm_default_nic - - nics = self.cs.listNics(virtualmachineid=self.get_vm(key='id')) - if nics: - for n in nics['nic']: - if n['isdefault']: - self.vm_default_nic = n - return self.vm_default_nic - self.module.fail_json(msg="No default IP address of VM '%s' found" % self.module.params.get('vm')) def create_static_nat(self, ip_address): From 1563b727a435a961f853b8e73f51a41474a0aca5 Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Fri, 10 Jun 2016 17:44:18 +0200 Subject: [PATCH 1688/2522] Adapt module to use new module._name property (#2408) This is in line with the change from ansible/ansible#16087 --- web_infrastructure/ejabberd_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_infrastructure/ejabberd_user.py b/web_infrastructure/ejabberd_user.py index 85e5eda8e5f..e89918a2486 100644 --- a/web_infrastructure/ejabberd_user.py +++ b/web_infrastructure/ejabberd_user.py @@ -122,7 +122,7 @@ def exists(self): def log(self, entry): """ This method will log information to the local syslog facility """ if self.logging: - syslog.openlog('ansible-%s' % os.path.basename(__file__)) + syslog.openlog('ansible-%s' % self.module._name) syslog.syslog(syslog.LOG_NOTICE, entry) def run_command(self, cmd, options): From 3ca125401834c90427113c15e17466f220ffb5aa Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Fri, 10 Jun 2016 19:24:43 +0200 Subject: [PATCH 1689/2522] Validate return code and fail properly (#2334) This fixes #2333 --- windows/win_regmerge.ps1 | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/windows/win_regmerge.ps1 b/windows/win_regmerge.ps1 index 3bd1547968b..87e73a69773 100644 --- a/windows/win_regmerge.ps1 +++ b/windows/win_regmerge.ps1 @@ -69,9 +69,14 @@ If ( $do_comparison -eq $True ) { { # Something is different, actually do reg merge $reg_import_args = @("IMPORT", "$path") - & reg.exe $reg_import_args - Set-Attr $result "changed" $True - Set-Attr $result "difference_count" $comparison_result.count + $ret = & reg.exe $reg_import_args 2>&1 + If ($LASTEXITCODE -eq 0) { + Set-Attr $result "changed" $True + Set-Attr $result "difference_count" $comparison_result.count + } Else { + Set-Attr $result "rc" $LASTEXITCODE + Fail-Json $result "$ret" + } } Else { Set-Attr $result "difference_count" 0 } @@ -82,9 +87,14 @@ If ( $do_comparison -eq $True ) { } Else { # not comparing, merge and report changed $reg_import_args = @("IMPORT", "$path") - & reg.exe $reg_import_args - Set-Attr $result "changed" $True - Set-Attr $result "compared" $False + $ret = & reg.exe $reg_import_args 2>&1 + If ( $LASTEXITCODE -eq 0 ) { + Set-Attr $result "changed" $True + Set-Attr $result "compared" $False + } Else { + Set-Attr $result "rc" $LASTEXITCODE + Fail-Json $result "$ret" + } } Exit-Json $result From 43760b2c4a346d9d4bf1a8d5cf2fec3b974c2864 Mon Sep 17 00:00:00 2001 From: Rob Powell Date: Sat, 11 Jun 2016 20:12:46 +0100 Subject: [PATCH 1690/2522] Update win_firewall_rule.py (#2337) added profile examples as my firewall task would pass yet no firewall rule was created until I added profile: Domain,Private,Public When setting a Firewall rule on Windows Server 2008 R2 manually, these three are selected as default, useful to have in the documentation maybe? --- windows/win_firewall_rule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_firewall_rule.py b/windows/win_firewall_rule.py index 2f90e2a6730..d833c2fa24d 100644 --- a/windows/win_firewall_rule.py +++ b/windows/win_firewall_rule.py @@ -97,7 +97,7 @@ required: false profile: description: - - the profile this rule applies to + - the profile this rule applies to, e.g. Domain,Private,Public default: null required: false force: From ff0e1df82c9d9ed263c748d9f2426bef65d72898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Luiz?= Date: Mon, 13 Jun 2016 07:34:52 +0100 Subject: [PATCH 1691/2522] added s3 download support to maven module (#2317) * added s3 download support * removed extraneous import --- packaging/language/maven_artifact.py | 40 ++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/packaging/language/maven_artifact.py b/packaging/language/maven_artifact.py index 1a33d1b9891..38802653c3c 100644 --- a/packaging/language/maven_artifact.py +++ b/packaging/language/maven_artifact.py @@ -28,6 +28,11 @@ import posixpath from ansible.module_utils.basic import * from ansible.module_utils.urls import * +try: + import boto3 + HAS_BOTO = True +except ImportError: + HAS_BOTO = False DOCUMENTATION = ''' --- @@ -42,6 +47,7 @@ requirements: - "python >= 2.6" - lxml + - boto if using a S3 repository (s3://...) options: group_id: description: @@ -68,19 +74,21 @@ default: jar repository_url: description: - - The URL of the Maven Repository to download from + - The URL of the Maven Repository to download from. Use s3://... if the repository is hosted on Amazon S3 required: false default: http://repo1.maven.org/maven2 username: description: - - The username to authenticate as to the Maven Repository + - The username to authenticate as to the Maven Repository. Use AWS secret key of the repository is hosted on S3 required: false default: null + aliases: [ "aws_secret_key" ] password: description: - - The password to authenticate with to the Maven Repository + - The password to authenticate with to the Maven Repository. Use AWS secret access key of the repository is hosted on S3 required: false default: null + aliases: [ "aws_secret_access_key" ] dest: description: - The path where the artifact should be written to @@ -185,7 +193,8 @@ def parse(input): class MavenDownloader: def __init__(self, module, base="http://repo1.maven.org/maven2"): self.module = module - base = base.rstrip("/") + if base.endswith("/"): + base = base.rstrip("/") self.base = base self.user_agent = "Maven Artifact Downloader/1.0" @@ -220,14 +229,23 @@ def _uri_for_artifact(self, artifact, version=None): return posixpath.join(self.base, artifact.path(), artifact.artifact_id + "-" + version + "." + artifact.extension) def _request(self, url, failmsg, f): + url_to_use = url + parsed_url = urlparse.urlparse(url) + if parsed_url.scheme=='s3': + parsed_url = urlparse.urlparse(url) + bucket_name = parsed_url.netloc[:parsed_url.netloc.find('.')] + key_name = parsed_url.path[1:] + client = boto3.client('s3',aws_access_key_id=self.module.params.get('username', ''), aws_secret_access_key=self.module.params.get('password', '')) + url_to_use = client.generate_presigned_url('get_object',Params={'Bucket':bucket_name,'Key':key_name},ExpiresIn=10) + # Hack to add parameters in the way that fetch_url expects self.module.params['url_username'] = self.module.params.get('username', '') self.module.params['url_password'] = self.module.params.get('password', '') self.module.params['http_agent'] = self.module.params.get('user_agent', None) - response, info = fetch_url(self.module, url) + response, info = fetch_url(self.module, url_to_use) if info['status'] != 200: - raise ValueError(failmsg + " because of " + info['msg'] + "for URL " + url) + raise ValueError(failmsg + " because of " + info['msg'] + "for URL " + url_to_use) else: return f(response) @@ -305,14 +323,20 @@ def main(): classifier = dict(default=None), extension = dict(default='jar'), repository_url = dict(default=None), - username = dict(default=None), - password = dict(default=None, no_log=True), + username = dict(default=None,aliases=['aws_secret_key']), + password = dict(default=None, no_log=True,aliases=['aws_secret_access_key']), state = dict(default="present", choices=["present","absent"]), # TODO - Implement a "latest" state dest = dict(type="path", default=None), validate_certs = dict(required=False, default=True, type='bool'), ) ) + + parsed_url = urlparse.urlparse(module.params["repository_url"]) + + if parsed_url.scheme=='s3' and not HAS_BOTO: + module.fail_json(msg='boto3 required for this module, when using s3:// repository URLs') + group_id = module.params["group_id"] artifact_id = module.params["artifact_id"] version = module.params["version"] From d256c7f373e1ab59d4527c06b65cf2c85e0a5b78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Mon, 13 Jun 2016 08:48:03 +0200 Subject: [PATCH 1692/2522] doc: maven_artifact: add a note about s3 support since version 2.2 (#2417) See #2317 --- packaging/language/maven_artifact.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packaging/language/maven_artifact.py b/packaging/language/maven_artifact.py index 38802653c3c..6f6454fb590 100644 --- a/packaging/language/maven_artifact.py +++ b/packaging/language/maven_artifact.py @@ -74,7 +74,8 @@ default: jar repository_url: description: - - The URL of the Maven Repository to download from. Use s3://... if the repository is hosted on Amazon S3 + - The URL of the Maven Repository to download from. + - Use s3://... if the repository is hosted on Amazon S3, added in version 2.2. required: false default: http://repo1.maven.org/maven2 username: From a82e991453ed2fa654b103f5ba573b4809b3494a Mon Sep 17 00:00:00 2001 From: sxpert Date: Mon, 13 Jun 2016 17:22:10 +0200 Subject: [PATCH 1693/2522] Virt net mods batch 1 (#1686) * add a new modify command for now, allows adding or modifying hosts in the dhcp subsystem * fix some pep8 things that escaped * add modify in the list in the doc * added mention of adding modify in version 2.1 * handle the test mode case for modify * modify the code for finer check mode support --- cloud/misc/virt_net.py | 59 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/cloud/misc/virt_net.py b/cloud/misc/virt_net.py index 21cdca5fbd7..055a7e6b3b0 100755 --- a/cloud/misc/virt_net.py +++ b/cloud/misc/virt_net.py @@ -47,10 +47,11 @@ required: false choices: [ "define", "create", "start", "stop", "destroy", "undefine", "get_xml", "list_nets", "facts", - "info", "status"] + "info", "status", "modify"] description: - in addition to state management, various non-idempotent commands are available. See examples. + Modify was added in version 2.1 autostart: required: false choices: ["yes", "no"] @@ -134,7 +135,8 @@ ALL_COMMANDS = [] ENTRY_COMMANDS = ['create', 'status', 'start', 'stop', - 'undefine', 'destroy', 'get_xml', 'define'] + 'undefine', 'destroy', 'get_xml', 'define', + 'modify' ] HOST_COMMANDS = [ 'list_nets', 'facts', 'info' ] ALL_COMMANDS.extend(ENTRY_COMMANDS) ALL_COMMANDS.extend(HOST_COMMANDS) @@ -206,6 +208,48 @@ def create(self, entryid): if not state: return self.module.exit_json(changed=True) + def modify(self, entryid, xml): + network = self.find_entry(entryid) + # identify what type of entry is given in the xml + new_data = etree.fromstring(xml) + old_data = etree.fromstring(network.XMLDesc(0)) + if new_data.tag == 'host': + mac_addr = new_data.get('mac') + hosts = old_data.xpath('/network/ip/dhcp/host') + # find the one mac we're looking for + host = None + for h in hosts: + if h.get('mac') == mac_addr: + host = h + break + if host is None: + # add the host + if not self.module.check_mode: + res = network.update (libvirt.VIR_NETWORK_UPDATE_COMMAND_ADD_LAST, + libvirt.VIR_NETWORK_SECTION_IP_DHCP_HOST, + -1, xml, libvirt.VIR_NETWORK_UPDATE_AFFECT_CURRENT) + else: + # pretend there was a change + res = 0 + if res == 0: + return True + else: + # change the host + if host.get('name') == new_data.get('name') and host.get('ip') == new_data.get('ip'): + return False + else: + if not self.module.check_mode: + res = network.update (libvirt.VIR_NETWORK_UPDATE_COMMAND_MODIFY, + libvirt.VIR_NETWORK_SECTION_IP_DHCP_HOST, + -1, xml, libvirt.VIR_NETWORK_UPDATE_AFFECT_CURRENT) + else: + # pretend there was a change + res = 0 + if res == 0: + return True + # command, section, parentIndex, xml, flags=0 + self.module.fail_json(msg='updating this is not supported yet '+unicode(xml)) + def destroy(self, entryid): if not self.module.check_mode: return self.find_entry(entryid).destroy() @@ -344,6 +388,9 @@ def set_autostart(self, entryid, state): def create(self, entryid): return self.conn.create(entryid) + + def modify(self, entryid, xml): + return self.conn.modify(entryid, xml) def start(self, entryid): return self.conn.create(entryid) @@ -460,14 +507,18 @@ def core(module): if command in ENTRY_COMMANDS: if not name: module.fail_json(msg = "%s requires 1 argument: name" % command) - if command == 'define': + if command in ('define', 'modify'): if not xml: - module.fail_json(msg = "define requires xml argument") + module.fail_json(msg = command+" requires xml argument") try: v.get_net(name) except EntryNotFound: v.define(name, xml) res = {'changed': True, 'created': name} + else: + if command == 'modify': + mod = v.modify(name, xml) + res = {'changed': mod, 'modified': name} return VIRT_SUCCESS, res res = getattr(v, command)(name) if type(res) != dict: From 8ea7ae46908234b6943787d9304d7df961ef30b7 Mon Sep 17 00:00:00 2001 From: Deepakkothandan Date: Mon, 13 Jun 2016 18:09:29 +0200 Subject: [PATCH 1694/2522] Ansible Module for Sending Rocket Chat Notifications (#2222) --- notification/rocketchat.py | 247 +++++++++++++++++++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 notification/rocketchat.py diff --git a/notification/rocketchat.py b/notification/rocketchat.py new file mode 100644 index 00000000000..b98ddd97c65 --- /dev/null +++ b/notification/rocketchat.py @@ -0,0 +1,247 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Deepak Kothandan +# (c) 2015, Stefan Berggren +# (c) 2014, Ramon de la Fuente +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = """ +module: rocketchat +short_description: Send notifications to Rocket Chat +description: + - The M(rocketchat) module sends notifications to Rocket Chat via the Incoming WebHook integration +version_added: "2.2" +author: "Ramon de la Fuente (@ramondelafuente)" +options: + domain: + description: + - The domain for your environment without protocol. (i.e. + C(subdomain.domain.com or chat.domain.tld)) + required: true + default: None + token: + description: + - Rocket Chat Incoming Webhook integration token. This provides + authentication to Rocket Chat's Incoming webhook for posting + messages. + required: true + protocol: + description: + - Specify the protocol used to send notification messages before the webhook url. (i.e. http or https) + required: false + default: https + choices: + - 'http' + - 'https' + msg: + description: + - Message to be sent. + required: false + default: None + channel: + description: + - Channel to send the message to. If absent, the message goes to the channel selected for the I(token) + specifed during the creation of webhook. + required: false + default: None + username: + description: + - This is the sender of the message. + required: false + default: "Ansible" + icon_url: + description: + - URL for the message sender's icon (default C(http://www.ansible.com/favicon.ico)) + required: false + icon_emoji: + description: + - Emoji for the message sender. The representation for the available emojis can be + got from Rocket Chat. (for example :thumbsup:) (if I(icon_emoji) is set, I(icon_url) will not be used) + required: false + default: None + link_names: + description: + - Automatically create links for channels and usernames in I(msg). + required: false + default: 1 + choices: + - 1 + - 0 + validate_certs: + description: + - If C(no), SSL certificates will not be validated. This should only be used + on personally controlled sites using self-signed certificates. + required: false + default: 'yes' + choices: + - 'yes' + - 'no' + color: + description: + - Allow text to use default colors - use the default of 'normal' to not send a custom color bar at the start of the message + required: false + default: 'normal' + choices: + - 'normal' + - 'good' + - 'warning' + - 'danger' + attachments: + description: + - Define a list of attachments. + required: false + default: None +""" + +EXAMPLES = """ +- name: Send notification message via Rocket Chat + local_action: + module: rocketchat + token: thetoken/generatedby/rocketchat + msg: "{{ inventory_hostname }} completed" + +- name: Send notification message via Rocket Chat all options + local_action: + module: rocketchat + token: thetoken/generatedby/rocketchat + msg: "{{ inventory_hostname }} completed" + channel: "#ansible" + username: "Ansible on {{ inventory_hostname }}" + icon_url: "http://www.example.com/some-image-file.png" + link_names: 0 + +- name: insert a color bar in front of the message for visibility purposes and use the default webhook icon and name configured in rocketchat + rocketchat: + token: thetoken/generatedby/rocketchat + msg: "{{ inventory_hostname }} is alive!" + color: good + username: "" + icon_url: "" + +- name: Use the attachments API + rocketchat: + token: thetoken/generatedby/rocketchat + attachments: + - text: "Display my system load on host A and B" + color: "#ff00dd" + title: "System load" + fields: + - title: "System A" + value: "load average: 0,74, 0,66, 0,63" + short: "true" + - title: "System B" + value: "load average: 5,16, 4,64, 2,43" + short: "true" + +""" + +RETURN = """ +changed: + description: A flag indicating if any change was made or not. + returned: success + type: boolean + sample: false +""" + +ROCKETCHAT_INCOMING_WEBHOOK = '%s://%s/hooks/%s' + +def build_payload_for_rocketchat(module, text, channel, username, icon_url, icon_emoji, link_names, color, attachments): + payload = {} + if color == "normal" and text is not None: + payload = dict(text=text) + elif text is not None: + payload = dict(attachments=[dict(text=text, color=color)]) + if channel is not None: + if (channel[0] == '#') or (channel[0] == '@'): + payload['channel'] = channel + else: + payload['channel'] = '#' + channel + if username is not None: + payload['username'] = username + if icon_emoji is not None: + payload['icon_emoji'] = icon_emoji + else: + payload['icon_url'] = icon_url + if link_names is not None: + payload['link_names'] = link_names + + if attachments is not None: + if 'attachments' not in payload: + payload['attachments'] = [] + + if attachments is not None: + for attachment in attachments: + if 'fallback' not in attachment: + attachment['fallback'] = attachment['text'] + payload['attachments'].append(attachment) + + payload="payload=" + module.jsonify(payload) + return payload + +def do_notify_rocketchat(module, domain, token, protocol, payload): + + if token.count('/') < 1: + module.fail_json(msg="Invalid Token specified, provide a valid token") + + rocketchat_incoming_webhook = ROCKETCHAT_INCOMING_WEBHOOK % (protocol, domain, token) + + response, info = fetch_url(module, rocketchat_incoming_webhook, data=payload) + if info['status'] != 200: + module.fail_json(msg="failed to send message, return status=%s" % str(info['status'])) + +def main(): + module = AnsibleModule( + argument_spec = dict( + domain = dict(type='str', required=False, default=None), + token = dict(type='str', required=True, no_log=True), + protocol = dict(type='str', default='https', choices=['http', 'https']), + msg = dict(type='str', required=False, default=None), + channel = dict(type='str', default=None), + username = dict(type='str', default='Ansible'), + icon_url = dict(type='str', default='http://www.ansible.com/favicon.ico'), + icon_emoji = dict(type='str', default=None), + link_names = dict(type='int', default=1, choices=[0,1]), + validate_certs = dict(default='yes', type='bool'), + color = dict(type='str', default='normal', choices=['normal', 'good', 'warning', 'danger']), + attachments = dict(type='list', required=False, default=None) + ) + ) + + domain = module.params['domain'] + token = module.params['token'] + protocol = module.params['protocol'] + text = module.params['msg'] + channel = module.params['channel'] + username = module.params['username'] + icon_url = module.params['icon_url'] + icon_emoji = module.params['icon_emoji'] + link_names = module.params['link_names'] + color = module.params['color'] + attachments = module.params['attachments'] + + payload = build_payload_for_rocketchat(module, text, channel, username, icon_url, icon_emoji, link_names, color, attachments) + do_notify_rocketchat(module, domain, token, protocol, payload) + + module.exit_json(msg="OK") + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * + +if __name__ == '__main__': + main() From b13b954085e824224235c192a2a87bf54fa92256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Mon, 13 Jun 2016 18:27:03 +0200 Subject: [PATCH 1695/2522] doc: rocketchat: minor doc fix (#2420) --- notification/rocketchat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notification/rocketchat.py b/notification/rocketchat.py index b98ddd97c65..898ea6a1209 100644 --- a/notification/rocketchat.py +++ b/notification/rocketchat.py @@ -33,7 +33,6 @@ - The domain for your environment without protocol. (i.e. C(subdomain.domain.com or chat.domain.tld)) required: true - default: None token: description: - Rocket Chat Incoming Webhook integration token. This provides @@ -66,8 +65,9 @@ default: "Ansible" icon_url: description: - - URL for the message sender's icon (default C(http://www.ansible.com/favicon.ico)) + - URL for the message sender's icon. required: false + default: "http://www.ansible.com/favicon.ico" icon_emoji: description: - Emoji for the message sender. The representation for the available emojis can be From e032aeaedda5bd4f655ce95b703d5a3c15ec235a Mon Sep 17 00:00:00 2001 From: Lujeni Date: Mon, 13 Jun 2016 19:36:57 +0200 Subject: [PATCH 1696/2522] Support the ssl_cert_reqs directive (#1122) --- database/misc/mongodb_user.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index cbaebcbfd27..703df319a83 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -79,6 +79,13 @@ description: - Whether to use an SSL connection when connecting to the database default: False + ssl_cert_reqs: + version_added: "2.2" + description: + - Specifies whether a certificate is required from the other side of the connection, and whether it will be validated if provided. + required: false + default: "CERT_REQUIRED" + choices: ["CERT_REQUIRED", "CERT_OPTIONAL", "CERT_NONE"] roles: version_added: "1.3" description: @@ -144,6 +151,7 @@ ''' +import ssl as ssl_lib import ConfigParser from distutils.version import LooseVersion try: @@ -279,6 +287,7 @@ def main(): roles=dict(default=None, type='list'), state=dict(default='present', choices=['absent', 'present']), update_password=dict(default="always", choices=["always", "on_create"]), + ssl_cert_reqs=dict(default='CERT_REQUIRED', choices=['CERT_NONE', 'CERT_OPTIONAL', 'CERT_REQUIRED']), ), supports_check_mode=True ) @@ -297,15 +306,19 @@ def main(): user = module.params['name'] password = module.params['password'] ssl = module.params['ssl'] + ssl_cert_reqs = getattr(ssl_lib, module.params['ssl_cert_reqs']) roles = module.params['roles'] state = module.params['state'] update_password = module.params['update_password'] try: if replica_set: - client = MongoClient(login_host, int(login_port), replicaset=replica_set, ssl=ssl) + client = MongoClient(login_host, int(login_port), + replicaset=replica_set, ssl=ssl, + ssl_cert_reqs=ssl_cert_reqs) else: - client = MongoClient(login_host, int(login_port), ssl=ssl) + client = MongoClient(login_host, int(login_port), ssl=ssl, + ssl_cert_reqs=ssl_cert_reqs) if login_user is None and login_password is None: mongocnf_creds = load_mongocnf() From 062c7607a4aee8b7fac192dc7d694e55ff8dbb0d Mon Sep 17 00:00:00 2001 From: Constantin Date: Mon, 13 Jun 2016 18:43:38 +0100 Subject: [PATCH 1697/2522] Added block_device_mapping in the returned output (#1553) --- cloud/amazon/ec2_remote_facts.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/cloud/amazon/ec2_remote_facts.py b/cloud/amazon/ec2_remote_facts.py index 28fc2c97d63..55c00910b7d 100644 --- a/cloud/amazon/ec2_remote_facts.py +++ b/cloud/amazon/ec2_remote_facts.py @@ -83,6 +83,21 @@ def get_instance_info(instance): except AttributeError: source_dest_check = None + # Get block device mapping + try: + bdm_dict = [] + bdm = getattr(instance, 'block_device_mapping') + for device_name in bdm.keys(): + bdm_dict.append({ + 'device_name': device_name, + 'status': bdm[device_name].status, + 'volume_id': bdm[device_name].volume_id, + 'delete_on_termination': bdm[device_name].delete_on_termination, + 'attach_time': bdm[device_name].attach_time + }) + except AttributeError: + pass + instance_info = { 'id': instance.id, 'kernel': instance.kernel, 'instance_profile': instance.instance_profile, @@ -115,6 +130,7 @@ def get_instance_info(instance): 'private_ip_address': instance.private_ip_address, 'state': instance._state.name, 'vpc_id': instance.vpc_id, + 'block_device_mapping': bdm_dict, } return instance_info From 2ad6f02c7b23d2ebe3a815328e06e88a0f4a5b33 Mon Sep 17 00:00:00 2001 From: Adrian Moisey Date: Mon, 13 Jun 2016 19:47:21 +0200 Subject: [PATCH 1698/2522] Add initial github_release module (#1755) Add initial github_release module. --- source_control/github_release.py | 121 +++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 source_control/github_release.py diff --git a/source_control/github_release.py b/source_control/github_release.py new file mode 100644 index 00000000000..daeb005e87c --- /dev/null +++ b/source_control/github_release.py @@ -0,0 +1,121 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + + +DOCUMENTATION = ''' +--- +module: github_release +short_description: Interact with GitHub Releases +description: + - Fetch metadata about Github Releases +version_added: 2.2 +options: + token: + required: true + description: + - Github Personal Access Token for authenticating + user: + required: true + description: + - The GitHub account that owns the repository + repo: + required: true + description: + - Repository name + action: + required: true + description: + - Action to perform + choices: [ 'latest_release' ] + +author: + - "Adrian Moisey (@adrianmoisey)" +requirements: + - "github3.py >= 1.0.0a3" +''' + +EXAMPLES = ''' +- name: Get latest release of test/test + github: + token: tokenabc1234567890 + user: testuser + repo: testrepo + action: latest_release +''' + +RETURN = ''' +latest_release: + description: Version of the latest release + type: string + returned: success + sample: 1.1.0 +''' + +try: + import github3 + + HAS_GITHUB_API = True +except ImportError: + HAS_GITHUB_API = False + + +def main(): + module = AnsibleModule( + argument_spec=dict( + repo=dict(required=True), + user=dict(required=True), + token=dict(required=True, no_log=True), + action=dict(required=True, choices=['latest_release']), + ), + supports_check_mode=True + ) + + if not HAS_GITHUB_API: + module.fail_json(msg='Missing requried github3 module (check docs or install with: pip install github3)') + + repo = module.params['repo'] + user = module.params['user'] + login_token = module.params['token'] + action = module.params['action'] + + # login to github + try: + gh = github3.login(token=str(login_token)) + # test if we're actually logged in + gh.me() + except github3.AuthenticationFailed: + e = get_exception() + module.fail_json(msg='Failed to connect to Github: %s' % e) + + repository = gh.repository(str(user), str(repo)) + + if not repository: + module.fail_json(msg="Repository %s/%s doesn't exist" % (user, repo)) + + if action == 'latest_release': + release = repository.latest_release() + if release: + module.exit_json(tag=release.tag_name) + else: + module.exit_json(tag=None) + + +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From 88dc108b690e6005f7a4f7ba5ff8049328200342 Mon Sep 17 00:00:00 2001 From: Ian Levesque Date: Mon, 13 Jun 2016 14:50:19 -0400 Subject: [PATCH 1699/2522] add 'behavior' attribute to consul_session (#2183) add 'behavior' attribute to consul_session --- clustering/consul_session.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/clustering/consul_session.py b/clustering/consul_session.py index f0ebd11b3b4..d2c24e12a3b 100644 --- a/clustering/consul_session.py +++ b/clustering/consul_session.py @@ -100,6 +100,14 @@ required: false default: True version_added: "2.1" + behavior: + description: + - the optional behavior that can be attached to the session when it + is created. This can be set to either ‘release’ or ‘delete’. This + controls the behavior when a session is invalidated. + default: release + required: false + version_added: "2.2" """ EXAMPLES = ''' @@ -188,6 +196,7 @@ def update_session(module): checks = module.params.get('checks') datacenter = module.params.get('datacenter') node = module.params.get('node') + behavior = module.params.get('behavior') consul_client = get_consul_api(module) @@ -195,6 +204,7 @@ def update_session(module): session = consul_client.session.create( name=name, + behavior=behavior, node=node, lock_delay=validate_duration('delay', delay), dc=datacenter, @@ -203,6 +213,7 @@ def update_session(module): module.exit_json(changed=True, session_id=session, name=name, + behavior=behavior, delay=delay, checks=checks, node=node) @@ -249,6 +260,8 @@ def main(): argument_spec = dict( checks=dict(default=None, required=False, type='list'), delay=dict(required=False,type='str', default='15s'), + behavior=dict(required=False,type='str', default='release', + choices=['release', 'delete']), host=dict(default='localhost'), port=dict(default=8500, type='int'), scheme=dict(required=False, default='http'), From 1e405bb472ad10cecc2f5f67564ebd1554df3d77 Mon Sep 17 00:00:00 2001 From: Bede Carroll Date: Tue, 14 Jun 2016 04:21:03 +0930 Subject: [PATCH 1700/2522] Add vMotion Module (#2342) --- cloud/vmware/vmware_vmotion.py | 150 +++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 cloud/vmware/vmware_vmotion.py diff --git a/cloud/vmware/vmware_vmotion.py b/cloud/vmware/vmware_vmotion.py new file mode 100644 index 00000000000..43e8a5d5d0b --- /dev/null +++ b/cloud/vmware/vmware_vmotion.py @@ -0,0 +1,150 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Bede Carroll +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: vmware_vmotion +short_description: Move a virtual machine using vMotion +description: + - Using VMware vCenter, move a virtual machine using vMotion to a different + host. +version_added: 2.2 +author: "Bede Carroll (@bedecarroll)" +notes: + - Tested on vSphere 6.0 +requirements: + - "python >= 2.6" + - pyVmomi +options: + vm_name: + description: + - Name of the VM to perform a vMotion on + required: True + aliases: ['vm'] + destination_host: + description: + - Name of the end host the VM should be running on + required: True + aliases: ['destination'] +extends_documentation_fragment: vmware.documentation +''' + +EXAMPLES = ''' +Example from Ansible playbook + + - name: Perform vMotion of VM + local_action: + module: vmware_vmotion + hostname: 'vcenter_hostname' + username: 'vcenter_username' + password: 'vcenter_password' + validate_certs: False + vm_name: 'vm_name_as_per_vcenter' + destination_host: 'destination_host_as_per_vcenter' +''' + +RETURN = ''' +running_host: + description: List the host the virtual machine is registered to + returned: + - changed + - success + type: string + sample: 'host1.example.com' +''' + +try: + from pyVmomi import vim + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + + +def migrate_vm(vm_object, host_object): + """ + Migrate virtual machine and return the task. + """ + relocate_spec = vim.vm.RelocateSpec(host=host_object) + task_object = vm_object.Relocate(relocate_spec) + return task_object + +def main(): + + argument_spec = vmware_argument_spec() + argument_spec.update( + dict( + vm_name=dict(required=True, aliases=['vm'], type='str'), + destination_host=dict(required=True, aliases=['destination'], type='str'), + ) + ) + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + if not HAS_PYVMOMI: + module.fail_json(msg='pyVmomi is required for this module') + + content = connect_to_api(module=module) + + vm_object = find_vm_by_name(content=content, vm_name=module.params['vm_name']) + host_object = find_hostsystem_by_name(content=content, hostname=module.params['destination_host']) + + # Setup result + result = { + 'changed': False + } + + # Check if we could find the VM or Host + if not vm_object: + module.fail_json(msg='Cannot find virtual machine') + if not host_object: + module.fail_json(msg='Cannot find host') + + # Make sure VM isn't already at the destination + if vm_object.runtime.host.name == module.params['destination_host']: + module.exit_json(**result) + + if not module.check_mode: + # Migrate VM and get Task object back + task_object = migrate_vm(vm_object=vm_object, host_object=host_object) + + # Wait for task to complete + wait_for_task(task_object) + + # If task was a success the VM has moved, update running_host and complete module + if task_object.info.state == vim.TaskInfo.State.success: + vm_object = find_vm_by_name(content=content, vm_name=module.params['vm_name']) + result['running_host'] = vm_object.runtime.host.name + result['changed'] = True + module.exit_json(**result) + else: + if task_object.info.error is None: + module.fail_json(msg='Unable to migrate VM due to an error, please check vCenter') + else: + module.fail_json(msg='Unable to migrate VM due to an error: %s' % task_object.info.error) + else: + # If we are in check mode return a result as if move was performed + result['running_host'] = module.params['destination_host'] + result['changed'] = True + module.exit_json(**result) + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.vmware import * + +if __name__ == '__main__': + main() From a8611671ec91c70cdcbbc03cc9c538d8aef05a9e Mon Sep 17 00:00:00 2001 From: Artem Feofanov Date: Wed, 6 Apr 2016 19:08:10 +0300 Subject: [PATCH 1701/2522] add telegram notification module --- notification/telegram.py | 103 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 notification/telegram.py diff --git a/notification/telegram.py b/notification/telegram.py new file mode 100644 index 00000000000..254a1bf12f2 --- /dev/null +++ b/notification/telegram.py @@ -0,0 +1,103 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Artem Feofanov +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + + +DOCUMENTATION = """ + +module: telegram +version_added: "2.2" +author: "Artem Feofanov (@tyouxa)" + +short_description: module for sending notifications via telegram + +description: + - Send notifications via telegram bot, to a verified group or user +notes: + - You will require a telegram account and create telegram bot to use this module. +options: + msg: + description: + - What message you wish to send. + required: true + token: + description: + - Token identifying your telegram bot. + required: true + chat_id: + description: + - Telegram group or user chat_id + required: true + +""" + +EXAMPLES = """ + +send a message to chat in playbook +- telegram: token=bot9999999:XXXXXXXXXXXXXXXXXXXXXXX + chat_id=000000 + msg="Ansible task finished" + +""" + +RETURN = """ + +msg: + description: The message you attempted to send + returned: success + type: string + sample: "Ansible task finished" + + +""" + +import urllib + +def main(): + + module = AnsibleModule( + argument_spec = dict( + token = dict(type='str',required=True,no_log=True), + chat_id = dict(type='str',required=True,no_log=True), + msg = dict(type='str',required=True)), + supports_check_mode=True + ) + + token = urllib.quote(module.params.get('token')) + chat_id = urllib.quote(module.params.get('chat_id')) + msg = urllib.quote(module.params.get('msg')) + + url = 'https://api.telegram.org/' + token + '/sendMessage?text=' + msg + '&chat_id=' + chat_id + + if module.check_mode: + module.exit_json(changed=False) + + response, info = fetch_url(module, url) + if info['status'] == 200: + module.exit_json(changed=True) + else: + module.fail_json(msg="failed to send message, return status=%s" % str(info['status'])) + + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * +if __name__ == '__main__': + main() From 146c969c07e1da37d6902c20b457b6eeaa903ea9 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 13 Jun 2016 12:05:00 -0700 Subject: [PATCH 1702/2522] Removed one module from the py3 syntax check blacklist --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 17f286b4af8..92df9527f1a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,8 +12,7 @@ addons: - python3.5 env: global: - - PY3_EXCLUDE_LIST="cloud/amazon/aws_mfa_devices.py - cloud/amazon/cloudtrail.py + - PY3_EXCLUDE_LIST="cloud/amazon/cloudtrail.py cloud/amazon/dynamodb_table.py cloud/amazon/ec2_ami_copy.py cloud/amazon/ec2_customer_gateway.py From 65fe9eb2b018993841620e506482dc699a892263 Mon Sep 17 00:00:00 2001 From: Rob Date: Wed, 15 Jun 2016 06:09:54 +1000 Subject: [PATCH 1703/2522] Add boto3 support to ec2_eni_facts (#2425) --- cloud/amazon/ec2_eni_facts.py | 54 +++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/cloud/amazon/ec2_eni_facts.py b/cloud/amazon/ec2_eni_facts.py index da4ebef3d4d..0efe06094b2 100644 --- a/cloud/amazon/ec2_eni_facts.py +++ b/cloud/amazon/ec2_eni_facts.py @@ -53,6 +53,34 @@ except ImportError: HAS_BOTO = False +try: + import boto3 + from botocore.exceptions import ClientError, NoCredentialsError + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + + +def list_ec2_snapshots_boto3(connection, module): + + if module.params.get("filters") is None: + filters = [] + else: + filters = ansible_dict_to_boto3_filter_list(module.params.get("filters")) + + try: + network_interfaces_result = connection.describe_network_interfaces(Filters=filters) + except (ClientError, NoCredentialsError) as e: + module.fail_json(msg=e.message) + + # Turn the boto3 result in to ansible_friendly_snaked_names + snaked_network_interfaces_result = camel_dict_to_snake_dict(network_interfaces_result) + for network_interfaces in snaked_network_interfaces_result['network_interfaces']: + network_interfaces['tag_set'] = boto3_tag_list_to_ansible_dict(network_interfaces['tag_set']) + + module.exit_json(**snaked_network_interfaces_result) + + def get_eni_info(interface): # Private addresses @@ -114,17 +142,27 @@ def main(): if not HAS_BOTO: module.fail_json(msg='boto required for this module') - region, ec2_url, aws_connect_params = get_aws_connection_info(module) + if HAS_BOTO3: + region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) + + if region: + connection = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_params) + else: + module.fail_json(msg="region must be specified") - if region: - try: - connection = connect_to_aws(boto.ec2, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: - module.fail_json(msg=str(e)) + list_ec2_snapshots_boto3(connection, module) else: - module.fail_json(msg="region must be specified") + region, ec2_url, aws_connect_params = get_aws_connection_info(module) + + if region: + try: + connection = connect_to_aws(boto.ec2, region, **aws_connect_params) + except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: + module.fail_json(msg=str(e)) + else: + module.fail_json(msg="region must be specified") - list_eni(connection, module) + list_eni(connection, module) from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * From ac32a1b8081b7b30bca169a43936fb41e240f32e Mon Sep 17 00:00:00 2001 From: Andrea Scarpino Date: Wed, 15 Jun 2016 20:26:03 +0200 Subject: [PATCH 1704/2522] Fix win_environment: strict-mode fix for 'value' (#2404) --- windows/win_environment.ps1 | 35 +++++++++-------------------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/windows/win_environment.ps1 b/windows/win_environment.ps1 index bece081282d..f1acfe19356 100644 --- a/windows/win_environment.ps1 +++ b/windows/win_environment.ps1 @@ -20,37 +20,18 @@ # POWERSHELL_COMMON $params = Parse-Args $args; -$state = Get-Attr $params "state" $null; -$result = New-Object PSObject; -Set-Attr $result "changed" $false; +$state = Get-AnsibleParam -obj $params -name "state" -default "present" -validateSet "present","absent" +$name = Get-AnsibleParam -obj $params -name "name" -failifempty $true +$level = Get-AnsibleParam -obj $params -name "level" -validateSet "machine","process","user" -failifempty $true +$value = Get-AnsibleParam -obj $params -name "value" -If ($state) { - $state = $state.ToString().ToLower() - If (($state -ne 'present') -and ($state -ne 'absent') ) { - Fail-Json $result "state is '$state'; must be 'present', or 'absent'" - } -} else { - $state = 'present' -} - -If ($params.name) -{ - $name = $params.name -} else { - Fail-Json $result "missing required argument: name" -} - -$value = $params.value - -If ($params.level) { - $level = $params.level.ToString().ToLower() - If (( $level -ne 'machine') -and ( $level -ne 'user' ) -and ( $level -ne 'process')) { - Fail-Json $result "level is '$level'; must be 'machine', 'user', or 'process'" - } +If ($level) { + $level = $level.ToString().ToLower() } $before_value = [Environment]::GetEnvironmentVariable($name, $level) +$state = $state.ToString().ToLower() if ($state -eq "present" ) { [Environment]::SetEnvironmentVariable($name, $value, $level) } Elseif ($state -eq "absent") { @@ -59,6 +40,8 @@ if ($state -eq "present" ) { $after_value = [Environment]::GetEnvironmentVariable($name, $level) +$result = New-Object PSObject; +Set-Attr $result "changed" $false; Set-Attr $result "name" $name; Set-Attr $result "before_value" $before_value; Set-Attr $result "value" $after_value; From eed9d601b5b727586e16f3afa5824031dfbc88df Mon Sep 17 00:00:00 2001 From: nitzmahone Date: Wed, 15 Jun 2016 11:55:12 -0700 Subject: [PATCH 1705/2522] StrictMode fixes for win_iis_webbinding StrictMode fixes for win_scheduled_task StrictMode fixes for win_webpicmd --- windows/win_iis_webbinding.ps1 | 43 ++++++++++++++-------------------- windows/win_scheduled_task.ps1 | 34 +++++++++++++-------------- windows/win_webpicmd.ps1 | 9 +------ 3 files changed, 35 insertions(+), 51 deletions(-) diff --git a/windows/win_iis_webbinding.ps1 b/windows/win_iis_webbinding.ps1 index bdff43fc63c..dfd9cdb958b 100644 --- a/windows/win_iis_webbinding.ps1 +++ b/windows/win_iis_webbinding.ps1 @@ -23,42 +23,35 @@ $params = Parse-Args $args; -# Name parameter -$name = Get-Attr $params "name" $FALSE; -If ($name -eq $FALSE) { - Fail-Json (New-Object psobject) "missing required argument: name"; -} - -# State parameter -$state = Get-Attr $params "state" $FALSE; -$valid_states = ($FALSE, 'present', 'absent'); -If ($state -NotIn $valid_states) { - Fail-Json $result "state is '$state'; must be $($valid_states)" -} +$name = Get-AnsibleParam $params -name "name" -failifempty $true +$state = Get-AnsibleParam $params "state" -default "present" -validateSet "present","absent" +$host_header = Get-AnsibleParam $params -name "host_header" +$protocol = Get-AnsibleParam $params -name "protocol" +$port = Get-AnsibleParam $params -name "port" +$ip = Get-AnsibleParam $params -name "ip" +$certificatehash = Get-AnsibleParam $params -name "certificate_hash" -default $false +$certificateStoreName = Get-AnsibleParam $params -name "certificate_store_name" -default "MY" $binding_parameters = New-Object psobject @{ Name = $name }; -If ($params.host_header) { - $binding_parameters.HostHeader = $params.host_header +If ($host_header) { + $binding_parameters.HostHeader = $host_header } -If ($params.protocol) { - $binding_parameters.Protocol = $params.protocol +If ($protocol) { + $binding_parameters.Protocol = $protocol } -If ($params.port) { - $binding_parameters.Port = $params.port +If ($port) { + $binding_parameters.Port = $port } -If ($params.ip) { - $binding_parameters.IPAddress = $params.ip +If ($ip) { + $binding_parameters.IPAddress = $ip } -$certificateHash = Get-Attr $params "certificate_hash" $FALSE; -$certificateStoreName = Get-Attr $params "certificate_store_name" "MY"; - # Ensure WebAdministration module is loaded if ((Get-Module "WebAdministration" -ErrorAction SilentlyContinue) -eq $null){ Import-Module WebAdministration @@ -98,12 +91,12 @@ try { # Select certificat if($certificateHash -ne $FALSE) { - $ip = $binding_parameters.IPAddress + $ip = $binding_parameters["IPAddress"] if((!$ip) -or ($ip -eq "*")) { $ip = "0.0.0.0" } - $port = $binding_parameters.Port + $port = $binding_parameters["Port"] if(!$port) { $port = 443 } diff --git a/windows/win_scheduled_task.ps1 b/windows/win_scheduled_task.ps1 index 7d409050ae9..6490d5562c3 100644 --- a/windows/win_scheduled_task.ps1 +++ b/windows/win_scheduled_task.ps1 @@ -24,29 +24,27 @@ $ErrorActionPreference = "Stop" $params = Parse-Args $args; -$days_of_week = Get-Attr $params "days_of_week" $null; -$enabled = Get-Attr $params "enabled" $true | ConvertTo-Bool; -$description = Get-Attr $params "description" " "; -$path = Get-Attr $params "path" $null; -$argument = Get-Attr $params "argument" $null; +$days_of_week = Get-AnsibleParam $params -anem "days_of_week" +$enabled = Get-AnsibleParam $params -name "enabled" -default $true +$enabled = $enabled | ConvertTo-Bool +$description = Get-AnsibleParam $params -name "description" -default " " +$path = Get-AnsibleParam $params -name "path" +$argument = Get-AnsibleParam $params -name "argument" $result = New-Object PSObject; Set-Attr $result "changed" $false; #Required vars -$name = Get-Attr -obj $params -name name -failifempty $true -resultobj $result -$state = Get-Attr -obj $params -name state -failifempty $true -resultobj $result -if( ($state -ne "present") -and ($state -ne "absent") ) { - Fail-Json $result "state must be present or absent" -} +$name = Get-AnsibleParam -obj $params -name name -failifempty $true -resultobj $result +$state = Get-AnsibleParam -obj $params -name state -failifempty $true -resultobj $result -validateSet "present","absent" #Vars conditionally required -if($state -eq "present") { - $execute = Get-Attr -obj $params -name execute -failifempty $true -resultobj $result - $frequency = Get-Attr -obj $params -name frequency -failifempty $true -resultobj $result - $time = Get-Attr -obj $params -name time -failifempty $true -resultobj $result - $user = Get-Attr -obj $params -name user -failifempty $true -resultobj $result -} +$present_args_required = $state -eq "present" +$execute = Get-AnsibleParam -obj $params -name execute -failifempty $present_args_required -resultobj $result +$frequency = Get-AnsibleParam -obj $params -name frequency -failifempty $present_args_required -resultobj $result +$time = Get-AnsibleParam -obj $params -name time -failifempty $present_args_required -resultobj $result +$user = Get-AnsibleParam -obj $params -name user -failifempty $present_args_required -resultobj $result + # Mandatory Vars if ($frequency -eq "weekly") @@ -59,7 +57,7 @@ if ($frequency -eq "weekly") if ($path) { - $path = "\{0}\" -f $params.path + $path = "\{0}\" -f $path } else { @@ -70,7 +68,7 @@ try { $task = Get-ScheduledTask -TaskPath "$path" | Where-Object {$_.TaskName -eq "$name"} # Correlate task state to enable variable, used to calculate if state needs to be changed - $taskState = $task.State + $taskState = if ($task) { $task.State } else { $null } if ($taskState -eq "Ready"){ $taskState = $true } diff --git a/windows/win_webpicmd.ps1 b/windows/win_webpicmd.ps1 index 3bef13f6574..a8624739d7c 100644 --- a/windows/win_webpicmd.ps1 +++ b/windows/win_webpicmd.ps1 @@ -25,14 +25,7 @@ $params = Parse-Args $args; $result = New-Object PSObject; Set-Attr $result "changed" $false; -If ($params.name) -{ - $package = $params.name -} -Else -{ - Fail-Json $result "missing required argument: name" -} +$package = Get-AnsibleParam $params -name "name" -failifempty $true Function Find-Command { From 127ddc1f059ff5898cd56a4207989041a2d02379 Mon Sep 17 00:00:00 2001 From: Strahinja Kustudic Date: Thu, 16 Jun 2016 18:46:12 +0200 Subject: [PATCH 1706/2522] Fixes check mode error on Python 2.4 and wrong changed state (#2438) * Fixes check mode error on Python 2.4 and wrong changed state * Changes code as suggested by @bcoca --- database/postgresql/postgresql_ext.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/database/postgresql/postgresql_ext.py b/database/postgresql/postgresql_ext.py index d3079630846..684f3b2c32a 100644 --- a/database/postgresql/postgresql_ext.py +++ b/database/postgresql/postgresql_ext.py @@ -164,23 +164,22 @@ def main(): try: if module.check_mode: - if state == "absent": + if state == "present": changed = not ext_exists(cursor, ext) - elif state == "present": + elif state == "absent": changed = ext_exists(cursor, ext) - module.exit_json(changed=changed,ext=ext) - - if state == "absent": - changed = ext_delete(cursor, ext) - - elif state == "present": - changed = ext_create(cursor, ext) + else: + if state == "absent": + changed = ext_delete(cursor, ext) + + elif state == "present": + changed = ext_create(cursor, ext) except NotSupportedError, e: module.fail_json(msg=str(e)) except Exception, e: module.fail_json(msg="Database query failed: %s" % e) - module.exit_json(changed=changed, db=db) + module.exit_json(changed=changed, db=db, ext=ext) # import module snippets from ansible.module_utils.basic import * From 07ed6bbd56b7798714efef8af91b7d3fe744a895 Mon Sep 17 00:00:00 2001 From: jhawkesworth Date: Thu, 16 Jun 2016 20:32:53 +0100 Subject: [PATCH 1707/2522] Various fixes to win_regedit and documentation (#2436) --- windows/win_regedit.ps1 | 18 ++++++++++++------ windows/win_regedit.py | 6 ++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/windows/win_regedit.ps1 b/windows/win_regedit.ps1 index f4975dea224..723a6c7b239 100644 --- a/windows/win_regedit.ps1 +++ b/windows/win_regedit.ps1 @@ -42,6 +42,13 @@ If ($state -eq "present" -and $registryData -eq $null -and $registryValue -ne $n Fail-Json $result "missing required argument: data" } +# check the registry key is in powershell ps-drive format: HKLM, HKCU, HKU, HKCR, HCCC +If (-not ($registryKey -match "^H[KC][CLU][MURC]{0,1}:\\")) +{ + Fail-Json $result "key: $registryKey is not a valid powershell path, see module documentation for examples." +} + + Function Test-RegistryValueData { Param ( [parameter(Mandatory=$true)] @@ -58,8 +65,8 @@ Function Test-RegistryValueData { } } -# Returns rue if registry data matches. -# Handles binary and string registry data +# Returns true if registry data matches. +# Handles binary, integer(dword) and string registry data Function Compare-RegistryData { Param ( [parameter(Mandatory=$true)] @@ -67,15 +74,14 @@ Function Compare-RegistryData { [parameter(Mandatory=$true)] [AllowEmptyString()]$DifferenceData ) - $refType = $ReferenceData.GetType().Name - if ($refType -eq "String" ) { + if ($ReferenceData -is [String] -or $ReferenceData -is [int]) { if ($ReferenceData -eq $DifferenceData) { return $true } else { return $false } - } elseif ($refType -eq "Object[]") { + } elseif ($ReferenceData -is [Object[]]) { if (@(Compare-Object $ReferenceData $DifferenceData -SyncWindow 0).Length -eq 0) { return $true } else { @@ -118,7 +124,7 @@ else } -if($registryDataType -eq "binary" -and $registryData -ne $null -and $registryData.GetType().Name -eq 'String') { +if($registryDataType -eq "binary" -and $registryData -ne $null -and $registryData -is [String]) { $registryData = Convert-RegExportHexStringToByteArray($registryData) } diff --git a/windows/win_regedit.py b/windows/win_regedit.py index 8845f8ceb9f..d9de288e687 100644 --- a/windows/win_regedit.py +++ b/windows/win_regedit.py @@ -126,6 +126,12 @@ key: HKCU:\Software\MyCompany value: hello state: absent + + # Ensure registry paths containing spaces are quoted. + # Creates Registry Key called 'My Company'. + win_regedit: + key: 'HKCU:\Software\My Company' + ''' RETURN = ''' data_changed: From 6cb682938451826abe5357e7924a76b0d2c493a0 Mon Sep 17 00:00:00 2001 From: Baptiste Mille-Mathias Date: Fri, 17 Jun 2016 14:50:15 +0200 Subject: [PATCH 1708/2522] Implement mounts in proxmox module (#2426) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implement mounts in proxmox module mounts in proxmox are the additionnal disk devices set in a guests. We handle the mounts the same way that netif devices, using a dictionnary with keys being mp0, mp1,… * Add version_added Seems to be a requirement but I didn't see that anywhere. Hope it'll fix the travis-ci issue --- cloud/misc/proxmox.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cloud/misc/proxmox.py b/cloud/misc/proxmox.py index bd73a037c25..f02ff2e292b 100644 --- a/cloud/misc/proxmox.py +++ b/cloud/misc/proxmox.py @@ -99,6 +99,13 @@ default: null required: false type: A hash/dictionary defining interfaces + mounts: + description: + - specifies additional mounts (separate disks) for the container + default: null + required: false + type: A hash/dictionary defining mount points + version_added: "2.2" ip_address: description: - specifies the address the container will be assigned @@ -174,6 +181,9 @@ # Create new container with minimal options defining network interface with dhcp - proxmox: vmid=100 node='uk-mc02' api_user='root@pam' api_password='1q2w3e' api_host='node1' password='123456' hostname='example.org' ostemplate='local:vztmpl/ubuntu-14.04-x86_64.tar.gz' netif='{"net0":"name=eth0,ip=dhcp,ip6=dhcp,bridge=vmbr0"}' +# Create new container with minimal options defining a mount +- proxmox: vmid=100 node='uk-mc02' api_user='root@pam' api_password='1q2w3e' api_host='node1' password='123456' hostname='example.org' ostemplate='local:vztmpl/ubuntu-14.04-x86_64.tar.gz' mounts='{"mp0":"local:8,mp=/mnt/test/"}' + # Start container - proxmox: vmid=100 api_user='root@pam' api_password='1q2w3e' api_host='node1' state=started @@ -219,6 +229,9 @@ def create_instance(module, proxmox, vmid, node, disk, storage, cpus, memory, sw if 'netif' in kwargs: kwargs.update(kwargs['netif']) del kwargs['netif'] + if 'mounts' in kwargs: + kwargs.update(kwargs['mounts']) + del kwargs['mounts'] else: kwargs['cpus']=cpus kwargs['disk']=disk @@ -298,6 +311,7 @@ def main(): memory = dict(type='int', default=512), swap = dict(type='int', default=0), netif = dict(type='dict'), + mounts = dict(type='dict'), ip_address = dict(), onboot = dict(type='bool', default='no'), storage = dict(default='local'), @@ -359,6 +373,7 @@ def main(): hostname = module.params['hostname'], ostemplate = module.params['ostemplate'], netif = module.params['netif'], + mounts = module.params['mounts'], ip_address = module.params['ip_address'], onboot = int(module.params['onboot']), cpuunits = module.params['cpuunits'], From a87b2e38a033bd77b1594c3a405953cc908c3231 Mon Sep 17 00:00:00 2001 From: "Javier M. Mellid" Date: Wed, 3 Feb 2016 21:22:43 +0100 Subject: [PATCH 1709/2522] Add Ceph RGW S3 compatibility Ceph Object Gateway (Ceph RGW) is an object storage interface built on top of librados to provide applications with a RESTful gateway to Ceph Storage Clusters: http://docs.ceph.com/docs/master/radosgw/ This patch adds the required bits to handle buckets with the RGW S3 RESTful API properly. It sticks to the AWS behaviour where possible while avoiding not yet implemented features in the Ceph RGW API. Signed-off-by: Javier M. Mellid --- cloud/amazon/s3_bucket.py | 82 ++++++++++++++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 10 deletions(-) diff --git a/cloud/amazon/s3_bucket.py b/cloud/amazon/s3_bucket.py index ec838d299f1..fa2e4b92e64 100644 --- a/cloud/amazon/s3_bucket.py +++ b/cloud/amazon/s3_bucket.py @@ -16,9 +16,9 @@ DOCUMENTATION = ''' --- module: s3_bucket -short_description: Manage s3 buckets in AWS +short_description: Manage S3 buckets in AWS, Ceph, Walrus and FakeS3 description: - - Manage s3 buckets in AWS + - Manage S3 buckets in AWS, Ceph, Walrus and FakeS3 version_added: "2.0" author: "Rob White (@wimnat)" options: @@ -40,9 +40,13 @@ default: null s3_url: description: - - S3 URL endpoint for usage with Eucalypus, fakes3, etc. Otherwise assumes AWS + - S3 URL endpoint for usage with Ceph, Eucalypus, fakes3, etc. Otherwise assumes AWS default: null aliases: [ S3_URL ] + ceph: + description: + - Enable API compatibility with Ceph. It takes into account the S3 API subset working with Ceph in order to provide the same module behaviour where possible. + version_added: "2.2" requester_pays: description: - With Requester Pays buckets, the requester instead of the bucket owner pays the cost of the request and the data download from the bucket. @@ -78,6 +82,12 @@ - s3_bucket: name: mys3bucket +# Create a simple s3 bucket on Ceph Rados Gateway +- s3_bucket: + name: mys3bucket + s3_url: http://your-ceph-rados-gateway-server.xxx + ceph: true + # Remove an s3 bucket and any keys it contains - s3_bucket: name: mys3bucket @@ -130,8 +140,8 @@ def create_tags_container(tags): tags_obj.add_tag_set(tag_set) return tags_obj -def create_bucket(connection, module, location): - +def _create_bucket(connection, module, location): + policy = module.params.get("policy") name = module.params.get("name") requester_pays = module.params.get("requester_pays") @@ -258,7 +268,7 @@ def create_bucket(connection, module, location): module.exit_json(changed=changed, name=bucket.name, versioning=versioning_status, requester_pays=requester_pays_status, policy=current_policy, tags=current_tags_dict) -def destroy_bucket(connection, module): +def _destroy_bucket(connection, module): force = module.params.get("force") name = module.params.get("name") @@ -290,6 +300,39 @@ def destroy_bucket(connection, module): module.exit_json(changed=changed) +def _create_bucket_ceph(connection, module, location): + + name = module.params.get("name") + + changed = False + + try: + bucket = connection.get_bucket(name) + except S3ResponseError, e: + try: + bucket = connection.create_bucket(name, location=location) + changed = True + except S3CreateError, e: + module.fail_json(msg=e.message) + + module.exit_json(changed=changed) + +def _destroy_bucket_ceph(connection, module): + + _destroy_bucket(connection, module) + +def create_bucket(connection, module, location, flavour='aws'): + if flavour == 'ceph': + _create_bucket_ceph(connection, module, location) + else: + _create_bucket(connection, module, location) + +def destroy_bucket(connection, module, flavour='aws'): + if flavour == 'ceph': + _destroy_bucket_ceph(connection, module) + else: + _destroy_bucket(connection, module) + def is_fakes3(s3_url): """ Return True if s3_url has scheme fakes3:// """ if s3_url is not None: @@ -319,7 +362,8 @@ def main(): s3_url = dict(aliases=['S3_URL']), state = dict(default='present', choices=['present', 'absent']), tags = dict(required=None, default={}, type='dict'), - versioning = dict(default='no', type='bool') + versioning = dict(default='no', type='bool'), + ceph = dict(default='no', type='bool') ) ) @@ -344,10 +388,27 @@ def main(): if not s3_url and 'S3_URL' in os.environ: s3_url = os.environ['S3_URL'] + ceph = module.params.get('ceph') + + if ceph and not s3_url: + module.fail_json(msg='ceph flavour requires s3_url') + + flavour = 'aws' + # Look at s3_url and tweak connection settings # if connecting to Walrus or fakes3 try: - if is_fakes3(s3_url): + if s3_url and ceph: + ceph = urlparse.urlparse(s3_url) + connection = boto.connect_s3( + host=ceph.hostname, + port=ceph.port, + is_secure=ceph.scheme == 'https', + calling_format=OrdinaryCallingFormat(), + **aws_connect_params + ) + flavour = 'ceph' + elif is_fakes3(s3_url): fakes3 = urlparse.urlparse(s3_url) connection = S3Connection( is_secure=fakes3.scheme == 'fakes3s', @@ -376,12 +437,13 @@ def main(): state = module.params.get("state") if state == 'present': - create_bucket(connection, module, location) + create_bucket(connection, module, location, flavour=flavour) elif state == 'absent': - destroy_bucket(connection, module) + destroy_bucket(connection, module, flavour=flavour) from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * +import urlparse if __name__ == '__main__': main() From 78b48296232c01408544b1866f51664d24559c70 Mon Sep 17 00:00:00 2001 From: "Javier M. Mellid" Date: Sat, 18 Jun 2016 00:06:13 +0200 Subject: [PATCH 1710/2522] Adapt exception syntax to work under python3 in s3_bucket.py Signed-off-by: Javier M. Mellid --- cloud/amazon/s3_bucket.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/cloud/amazon/s3_bucket.py b/cloud/amazon/s3_bucket.py index fa2e4b92e64..7d07f3b856d 100644 --- a/cloud/amazon/s3_bucket.py +++ b/cloud/amazon/s3_bucket.py @@ -151,11 +151,11 @@ def _create_bucket(connection, module, location): try: bucket = connection.get_bucket(name) - except S3ResponseError, e: + except S3ResponseError as e: try: bucket = connection.create_bucket(name, location=location) changed = True - except S3CreateError, e: + except S3CreateError as e: module.fail_json(msg=e.message) # Versioning @@ -165,7 +165,7 @@ def _create_bucket(connection, module, location): bucket.configure_versioning(versioning) changed = True versioning_status = bucket.get_versioning_status() - except S3ResponseError, e: + except S3ResponseError as e: module.fail_json(msg=e.message) elif not versioning_status and not versioning: # do nothing @@ -195,7 +195,7 @@ def _create_bucket(connection, module, location): # Policy try: current_policy = bucket.get_policy() - except S3ResponseError, e: + except S3ResponseError as e: if e.error_code == "NoSuchBucketPolicy": current_policy = None else: @@ -210,7 +210,7 @@ def _create_bucket(connection, module, location): bucket.set_policy(policy) changed = True current_policy = bucket.get_policy() - except S3ResponseError, e: + except S3ResponseError as e: module.fail_json(msg=e.message) elif current_policy is None and policy is not None: @@ -220,7 +220,7 @@ def _create_bucket(connection, module, location): bucket.set_policy(policy) changed = True current_policy = bucket.get_policy() - except S3ResponseError, e: + except S3ResponseError as e: module.fail_json(msg=e.message) elif current_policy is not None and policy is None: @@ -228,7 +228,7 @@ def _create_bucket(connection, module, location): bucket.delete_policy() changed = True current_policy = bucket.get_policy() - except S3ResponseError, e: + except S3ResponseError as e: if e.error_code == "NoSuchBucketPolicy": current_policy = None else: @@ -242,7 +242,7 @@ def _create_bucket(connection, module, location): try: current_tags = bucket.get_tags() tag_set = TagSet() - except S3ResponseError, e: + except S3ResponseError as e: if e.error_code == "NoSuchTagSet": current_tags = None else: @@ -263,7 +263,7 @@ def _create_bucket(connection, module, location): bucket.delete_tags() current_tags_dict = tags changed = True - except S3ResponseError, e: + except S3ResponseError as e: module.fail_json(msg=e.message) module.exit_json(changed=changed, name=bucket.name, versioning=versioning_status, requester_pays=requester_pays_status, policy=current_policy, tags=current_tags_dict) @@ -276,7 +276,7 @@ def _destroy_bucket(connection, module): try: bucket = connection.get_bucket(name) - except S3ResponseError, e: + except S3ResponseError as e: if e.error_code != "NoSuchBucket": module.fail_json(msg=e.message) else: @@ -289,13 +289,13 @@ def _destroy_bucket(connection, module): for key in bucket.list(): key.delete() - except BotoServerError, e: + except BotoServerError as e: module.fail_json(msg=e.message) try: bucket = connection.delete_bucket(name) changed = True - except S3ResponseError, e: + except S3ResponseError as e: module.fail_json(msg=e.message) module.exit_json(changed=changed) @@ -308,11 +308,11 @@ def _create_bucket_ceph(connection, module, location): try: bucket = connection.get_bucket(name) - except S3ResponseError, e: + except S3ResponseError as e: try: bucket = connection.create_bucket(name, location=location) changed = True - except S3CreateError, e: + except S3CreateError as e: module.fail_json(msg=e.message) module.exit_json(changed=changed) @@ -426,9 +426,9 @@ def main(): if connection is None: connection = boto.connect_s3(**aws_connect_params) - except boto.exception.NoAuthHandlerFound, e: + except boto.exception.NoAuthHandlerFound as e: module.fail_json(msg='No Authentication Handler found: %s ' % str(e)) - except Exception, e: + except Exception as e: module.fail_json(msg='Failed to connect to S3: %s' % str(e)) if connection is None: # this should never happen From 2c1530b647c95f871aea502a441c7fd66dafd863 Mon Sep 17 00:00:00 2001 From: "Javier M. Mellid" Date: Sat, 18 Jun 2016 00:10:18 +0200 Subject: [PATCH 1711/2522] Imports should be near the top of the file Prior to 2.1, imports of module_utils was actually a preprocessor-like substitution. So importing at the bottom helped preserve line numbers when debugging. We'll be moving these to the top of files as time goes on. Signed-off-by: Javier M. Mellid --- cloud/amazon/s3_bucket.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cloud/amazon/s3_bucket.py b/cloud/amazon/s3_bucket.py index 7d07f3b856d..30c0e154242 100644 --- a/cloud/amazon/s3_bucket.py +++ b/cloud/amazon/s3_bucket.py @@ -109,6 +109,9 @@ import xml.etree.ElementTree as ET import urlparse +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + try: import boto.ec2 from boto.s3.connection import OrdinaryCallingFormat, Location @@ -441,9 +444,5 @@ def main(): elif state == 'absent': destroy_bucket(connection, module, flavour=flavour) -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * -import urlparse - if __name__ == '__main__': main() From 800da3ae16a7bc360f5c8c64421a323813d6d298 Mon Sep 17 00:00:00 2001 From: Joel Thompson Date: Sun, 19 Jun 2016 11:58:44 -0400 Subject: [PATCH 1712/2522] Fix default service_address in consul module (#2454) PR #1299 introduced the service_address parameter but specified a default value of localhost. This is a breaking change; prior to that, the consul module would always assume that the service should advertise the address that the Consul agent was listening on. With this change, the consul module will now default to advertising localhost to all nodes for the service, which isn't the desired behavior. This changes the default back to None which is the implicit default prior to #1299. --- clustering/consul.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/clustering/consul.py b/clustering/consul.py index 27c3e84260c..a5e4a5140dd 100644 --- a/clustering/consul.py +++ b/clustering/consul.py @@ -95,10 +95,12 @@ required: false service_address: description: - - the address on which the service is serving required for - registration of a service + - the address to advertise that the service will be listening on. + This value will be passed as the I(Address) parameter to Consul's + U(/v1/agent/service/register) API method, so refer to the Consul API + documentation for further details. required: false - default: localhost + default: None version_added: "2.1" tags: description: @@ -185,11 +187,11 @@ interval: 60s http: /status - - name: register nginx with address + - name: register external service nginx available at 10.1.5.23 consul: service_name: nginx service_port: 80 - service_address: 127.0.0.1 + service_address: 10.1.5.23 - name: register nginx with some service tags consul: @@ -542,7 +544,7 @@ def main(): script=dict(required=False), service_id=dict(required=False), service_name=dict(required=False), - service_address=dict(required=False, type='str', default='localhost'), + service_address=dict(required=False, type='str', default=None), service_port=dict(required=False, type='int'), state=dict(default='present', choices=['present', 'absent']), interval=dict(required=False, type='str'), From 0daedc05f522a1df27fe34e2e6bb01dc87a8cbfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Mon, 20 Jun 2016 12:25:47 +0200 Subject: [PATCH 1713/2522] cloudstack: cs_volume: fix volume may be NoneType in check_mode (#2455) --- cloud/cloudstack/cs_volume.py | 39 ++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/cloud/cloudstack/cs_volume.py b/cloud/cloudstack/cs_volume.py index cb87b3622e2..9897b62c6df 100644 --- a/cloud/cloudstack/cs_volume.py +++ b/cloud/cloudstack/cs_volume.py @@ -339,27 +339,28 @@ def present_volume(self): def attached_volume(self): volume = self.present_volume() - if volume.get('virtualmachineid') != self.get_vm(key='id'): - self.result['changed'] = True - - if not self.module.check_mode: - volume = self.detached_volume() - - if 'attached' not in volume: - self.result['changed'] = True + if volume: + if volume.get('virtualmachineid') != self.get_vm(key='id'): + self.result['changed'] = True - args = {} - args['id'] = volume['id'] - args['virtualmachineid'] = self.get_vm(key='id') - args['deviceid'] = self.module.params.get('device_id') + if not self.module.check_mode: + volume = self.detached_volume() - if not self.module.check_mode: - res = self.cs.attachVolume(**args) - if 'errortext' in res: - self.module.fail_json(msg="Failed: '%s'" % res['errortext']) - poll_async = self.module.params.get('poll_async') - if poll_async: - volume = self.poll_job(res, 'volume') + if 'attached' not in volume: + self.result['changed'] = True + + args = {} + args['id'] = volume['id'] + args['virtualmachineid'] = self.get_vm(key='id') + args['deviceid'] = self.module.params.get('device_id') + + if not self.module.check_mode: + res = self.cs.attachVolume(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + poll_async = self.module.params.get('poll_async') + if poll_async: + volume = self.poll_job(res, 'volume') return volume From a28d7a00b899e88d9d0b349939ae85f18052bf5c Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 20 Jun 2016 08:58:33 -0400 Subject: [PATCH 1714/2522] added 'seen' type to debconf --- system/debconf.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/system/debconf.py b/system/debconf.py index b249986a947..cc4681ebb3b 100644 --- a/system/debconf.py +++ b/system/debconf.py @@ -51,11 +51,11 @@ aliases: ['setting', 'selection'] vtype: description: - - The type of the value supplied + - The type of the value supplied. + - C(seen) was added in 2.2. required: false default: null - choices: [string, password, boolean, select, multiselect, note, error, title, text] - aliases: [] + choices: [string, password, boolean, select, multiselect, note, error, title, text, seen] value: description: - Value to set the configuration to @@ -67,7 +67,6 @@ - Do not set 'seen' flag when pre-seeding required: false default: False - aliases: [] author: "Brian Coca (@bcoca)" ''' @@ -119,7 +118,7 @@ def main(): argument_spec = dict( name = dict(required=True, aliases=['pkg'], type='str'), question = dict(required=False, aliases=['setting', 'selection'], type='str'), - vtype = dict(required=False, type='str', choices=['string', 'password', 'boolean', 'select', 'multiselect', 'note', 'error', 'title', 'text']), + vtype = dict(required=False, type='str', choices=['string', 'password', 'boolean', 'select', 'multiselect', 'note', 'error', 'title', 'text', 'seen']), value= dict(required=False, type='str'), unseen = dict(required=False, type='bool'), ), From 45ca94af0a8fcb87a93a550b45138991a1324a45 Mon Sep 17 00:00:00 2001 From: Benjamin Copeland Date: Mon, 20 Jun 2016 17:50:23 +0100 Subject: [PATCH 1715/2522] Adding statusio_maintenance module (#1394) --- monitoring/statusio_maintenance.py | 478 +++++++++++++++++++++++++++++ 1 file changed, 478 insertions(+) create mode 100644 monitoring/statusio_maintenance.py diff --git a/monitoring/statusio_maintenance.py b/monitoring/statusio_maintenance.py new file mode 100644 index 00000000000..893545e4057 --- /dev/null +++ b/monitoring/statusio_maintenance.py @@ -0,0 +1,478 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Benjamin Copeland (@bhcopeland) +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' + +module: statusio_maintenance +short_description: Create maintenance windows for your status.io dashboard +description: + - Creates a maintenance window for status.io + - Deletes a maintenance window for status.io +notes: + - You can use the apiary API url (http://docs.statusio.apiary.io/) to + capture API traffic + - Use start_date and start_time with minutes to set future maintenance window +version_added: "2.2" +author: Benjamin Copeland (@bhcopeland) +options: + title: + description: + - A descriptive title for the maintenance window + required: false + default: "A new maintenance window" + desc: + description: + - Message describing the maintenance window + required: false + default: "Created by Ansible" + state: + description: + - Desired state of the package. + required: false + default: "present" + choices: ["present", "absent"] + api_id: + description: + - Your unique API ID from status.io + required: true + api_key: + description: + - Your unique API Key from status.io + required: true + statuspage: + description: + - Your unique StatusPage ID from status.io + required: true + url: + description: + - Status.io API URL. A private apiary can be used instead. + required: false + default: "https://api.status.io" + components: + description: + - The given name of your component (server name) + required: false + aliases: ['component'] + default: None + containers: + description: + - The given name of your container (data center) + required: false + aliases: ['container'] + default: None + all_infrastructure_affected: + description: + - If it affects all components and containers + required: false + default: false + automation: + description: + - Automatically start and end the maintenance window + required: false + default: false + maintenance_notify_now: + description: + - Notify subscribers now + required: false + default: false + maintenance_notify_72_hr: + description: + - Notify subscribers 72 hours before maintenance start time + required: false + default: false + maintenance_notify_24_hr: + description: + - Notify subscribers 24 hours before maintenance start time + required: false + default: false + maintenance_notify_1_hr: + description: + - Notify subscribers 1 hour before maintenance start time + required: false + default: false + maintenance_id: + description: + - The maintenance id number when deleting a maintenance window + required: false + default: None + minutes: + description: + - The length of time in UTC that the maintenance will run \ + (starting from playbook runtime) + required: false + default: 10 + start_date: + description: + - Date maintenance is expected to start (Month/Day/Year) (UTC) + - End Date is worked out from start_date + minutes + required: false + default: None + start_time: + description: + - Time maintenance is expected to start (Hour:Minutes) (UTC) + - End Time is worked out from start_time + minutes + required: false + default: None +''' + +EXAMPLES = ''' +# Create a maintenance window for 10 minutes on server1.example.com, with +automation to stop the maintenance. +- statusio_maintenance: + title: "Router Upgrade from ansible" + desc: "Performing a Router Upgrade" + components: "server1.example.com" + api_id: "api_id" + api_key: "api_key" + statuspage: "statuspage_id" + maintenance_notify_1_hr: true + automation: true + +# Create a maintenance window for 60 minutes on multiple hosts +- name: "Create maintenance window for server1 and server2" + local_action: + module: statusio_maintenance + title: "Routine maintenance" + desc: "Some security updates" + components: + - "server1.example.com + - "server2.example.com" + minutes: "60" + api_id: "api_id" + api_key: "api_key" + statuspage: "statuspage_id" + maintenance_notify_1_hr: true + automation: true + +# Create a future maintenance window for 24 hours to all hosts inside the +# Primary Data Center +- statusio_maintenance: + title: Data center downtime + desc: Performing a Upgrade to our data center + components: "Primary Data Center" + api_id: "api_id" + api_key: "api_key" + statuspage: "statuspage_id" + start_date: "01/01/2016" + start_time: "12:00" + minutes: 1440 + +# Delete a maintenance window +- statusio_maintenance: + title: "Remove a maintenance window" + maintenance_id: "561f90faf74bc94a4700087b" + statuspage: "statuspage_id" + api_id: "api_id" + api_key: "api_key" + state: absent + +''' +# TODO: Add RETURN documentation. +RETURN = ''' # ''' + +import datetime + + +def get_api_auth_headers(api_id, api_key, url, statuspage): + + headers = { + "x-api-id": api_id, + "x-api-key": api_key, + "Content-Type": "application/json" + } + + try: + response = open_url( + url + "/v2/component/list/" + statuspage, headers=headers) + data = json.loads(response.read()) + if data['status']['message'] == 'Authentication failed': + return 1, None, None, "Authentication failed: " \ + "Check api_id/api_key and statuspage id." + else: + auth_headers = headers + auth_content = data + except: + return 1, None, None, e + return 0, auth_headers, auth_content, None + + +def get_component_ids(auth_content, components): + host_ids = [] + lower_components = [x.lower() for x in components] + for result in auth_content["result"]: + if result['name'].lower() in lower_components: + data = { + "component_id": result["_id"], + "container_id": result["containers"][0]["_id"] + } + host_ids.append(data) + lower_components.remove(result['name'].lower()) + if len(lower_components): + # items not found in the api + return 1, None, lower_components + return 0, host_ids, None + + +def get_container_ids(auth_content, containers): + host_ids = [] + lower_containers = [x.lower() for x in containers] + for result in auth_content["result"]: + if result["containers"][0]["name"].lower() in lower_containers: + data = { + "component_id": result["_id"], + "container_id": result["containers"][0]["_id"] + } + host_ids.append(data) + lower_containers.remove(result["containers"][0]["name"].lower()) + + if len(lower_containers): + # items not found in the api + return 1, None, lower_containers + return 0, host_ids, None + + +def get_date_time(start_date, start_time, minutes): + returned_date = [] + if start_date and start_time: + try: + datetime.datetime.strptime(start_date, '%m/%d/%Y') + returned_date.append(start_date) + except (NameError, ValueError): + return 1, None, "Not a valid start_date format." + try: + datetime.datetime.strptime(start_time, '%H:%M') + returned_date.append(start_time) + except (NameError, ValueError): + return 1, None, "Not a valid start_time format." + try: + # Work out end date/time based on minutes + date_time_start = datetime.datetime.strptime( + start_time + start_date, '%H:%M%m/%d/%Y') + delta = date_time_start + datetime.timedelta(minutes=minutes) + returned_date.append(delta.strftime("%m/%d/%Y")) + returned_date.append(delta.strftime("%H:%M")) + except (NameError, ValueError): + return 1, None, "Couldn't work out a valid date" + else: + now = datetime.datetime.utcnow() + delta = now + datetime.timedelta(minutes=minutes) + # start_date + returned_date.append(now.strftime("%m/%d/%Y")) + returned_date.append(now.strftime("%H:%M")) + # end_date + returned_date.append(delta.strftime("%m/%d/%Y")) + returned_date.append(delta.strftime("%H:%M")) + return 0, returned_date, None + + +def create_maintenance(auth_headers, url, statuspage, host_ids, + all_infrastructure_affected, automation, title, desc, + returned_date, maintenance_notify_now, + maintenance_notify_72_hr, maintenance_notify_24_hr, + maintenance_notify_1_hr): + returned_dates = [[x] for x in returned_date] + component_id = [] + container_id = [] + for val in host_ids: + component_id.append(val['component_id']) + container_id.append(val['container_id']) + try: + values = json.dumps({ + "statuspage_id": statuspage, + "components": component_id, + "containers": container_id, + "all_infrastructure_affected": + str(int(all_infrastructure_affected)), + "automation": str(int(automation)), + "maintenance_name": title, + "maintenance_details": desc, + "date_planned_start": returned_dates[0], + "time_planned_start": returned_dates[1], + "date_planned_end": returned_dates[2], + "time_planned_end": returned_dates[3], + "maintenance_notify_now": str(int(maintenance_notify_now)), + "maintenance_notify_72_hr": str(int(maintenance_notify_72_hr)), + "maintenance_notify_24_hr": str(int(maintenance_notify_24_hr)), + "maintenance_notify_1_hr": str(int(maintenance_notify_1_hr)) + }) + response = open_url( + url + "/v2/maintenance/schedule", data=values, + headers=auth_headers) + data = json.loads(response.read()) + + if data["status"]["error"] == "yes": + return 1, None, data["status"]["message"] + except Exception, e: + return 1, None, str(e) + return 0, None, None + + +def delete_maintenance(auth_headers, url, statuspage, maintenance_id): + try: + values = json.dumps({ + "statuspage_id": statuspage, + "maintenance_id": maintenance_id, + }) + response = open_url( + url=url + "/v2/maintenance/delete", + data=values, + headers=auth_headers) + data = json.loads(response.read()) + if data["status"]["error"] == "yes": + return 1, None, "Invalid maintenance_id" + except Exception, e: + return 1, None, str(e) + return 0, None, None + + +def main(): + module = AnsibleModule( + argument_spec=dict( + api_id=dict(required=True), + api_key=dict(required=True, no_log=True), + statuspage=dict(required=True), + state=dict(required=False, default='present', + choices=['present', 'absent']), + url=dict(default='https://api.status.io', required=False), + components=dict(type='list', required=False, default=None, + aliases=['component']), + containers=dict(type='list', required=False, default=None, + aliases=['container']), + all_infrastructure_affected=dict(type='bool', default=False, + required=False), + automation=dict(type='bool', default=False, required=False), + title=dict(required=False, default='A new maintenance window'), + desc=dict(required=False, default='Created by Ansible'), + minutes=dict(type='int', required=False, default=10), + maintenance_notify_now=dict(type='bool', default=False, + required=False), + maintenance_notify_72_hr=dict(type='bool', default=False, + required=False), + maintenance_notify_24_hr=dict(type='bool', default=False, + required=False), + maintenance_notify_1_hr=dict(type='bool', default=False, + required=False), + maintenance_id=dict(required=False, default=None), + start_date=dict(default=None, required=False), + start_time=dict(default=None, required=False) + ), + supports_check_mode=True, + ) + + api_id = module.params['api_id'] + api_key = module.params['api_key'] + statuspage = module.params['statuspage'] + state = module.params['state'] + url = module.params['url'] + components = module.params['components'] + containers = module.params['containers'] + all_infrastructure_affected = module.params['all_infrastructure_affected'] + automation = module.params['automation'] + title = module.params['title'] + desc = module.params['desc'] + minutes = module.params['minutes'] + maintenance_notify_now = module.params['maintenance_notify_now'] + maintenance_notify_72_hr = module.params['maintenance_notify_72_hr'] + maintenance_notify_24_hr = module.params['maintenance_notify_24_hr'] + maintenance_notify_1_hr = module.params['maintenance_notify_1_hr'] + maintenance_id = module.params['maintenance_id'] + start_date = module.params['start_date'] + start_time = module.params['start_time'] + + if state == "present": + + if api_id and api_key: + (rc, auth_headers, auth_content, error) = \ + get_api_auth_headers(api_id, api_key, url, statuspage) + if rc != 0: + module.fail_json(msg="Failed to get auth keys: %s" % error) + else: + auth_headers = {} + auth_content = {} + + if minutes or start_time and start_date: + (rc, returned_date, error) = get_date_time( + start_date, start_time, minutes) + if rc != 0: + module.fail_json(msg="Failed to set date/time: %s" % error) + + if not components and not containers: + return module.fail_json(msg="A Component or Container must be " + "defined") + elif components and containers: + return module.fail_json(msg="Components and containers cannot " + "be used together") + else: + if components: + (rc, host_ids, error) = get_component_ids(auth_content, + components) + if rc != 0: + module.fail_json(msg="Failed to find component %s" % error) + + if containers: + (rc, host_ids, error) = get_container_ids(auth_content, + containers) + if rc != 0: + module.fail_json(msg="Failed to find container %s" % error) + + if module.check_mode: + module.exit_json(changed=True) + else: + (rc, _, error) = create_maintenance( + auth_headers, url, statuspage, host_ids, + all_infrastructure_affected, automation, + title, desc, returned_date, maintenance_notify_now, + maintenance_notify_72_hr, maintenance_notify_24_hr, + maintenance_notify_1_hr) + if rc == 0: + module.exit_json(changed=True, result="Successfully created " + "maintenance") + else: + module.fail_json(msg="Failed to create maintenance: %s" + % error) + + if state == "absent": + + if api_id and api_key: + (rc, auth_headers, auth_content, error) = \ + get_api_auth_headers(api_id, api_key, url, statuspage) + if rc != 0: + module.fail_json(msg="Failed to get auth keys: %s" % error) + else: + auth_headers = {} + + if module.check_mode: + module.exit_json(changed=True) + else: + (rc, _, error) = delete_maintenance( + auth_headers, url, statuspage, maintenance_id) + if rc == 0: + module.exit_json( + changed=True, + result="Successfully deleted maintenance" + ) + else: + module.fail_json( + msg="Failed to delete maintenance: %s" % error) + +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * +if __name__ == '__main__': + main() From 1da89cd5a1f4b6ceea51e0248eafa802eb60cd1c Mon Sep 17 00:00:00 2001 From: Stefan Horning Date: Mon, 20 Jun 2016 19:10:56 +0200 Subject: [PATCH 1716/2522] Get EIP association information for ENI if EIP is associated (#2082) --- cloud/amazon/ec2_eni_facts.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cloud/amazon/ec2_eni_facts.py b/cloud/amazon/ec2_eni_facts.py index 0efe06094b2..8b385dabc83 100644 --- a/cloud/amazon/ec2_eni_facts.py +++ b/cloud/amazon/ec2_eni_facts.py @@ -101,6 +101,12 @@ def get_eni_info(interface): 'private_ip_addresses': private_addresses } + if hasattr(interface, 'publicDnsName'): + interface_info['association'] = {'public_ip_address': interface.publicIp, + 'public_dns_name': interface.publicDnsName, + 'ip_owner_id': interface.ipOwnerId + } + if interface.attachment is not None: interface_info['attachment'] = {'attachment_id': interface.attachment.id, 'instance_id': interface.attachment.instance_id, From 5cd3e328f3d5459d39a6f88bd331f362dca96d5f Mon Sep 17 00:00:00 2001 From: nitzmahone Date: Mon, 20 Jun 2016 12:59:48 -0700 Subject: [PATCH 1717/2522] add win_chocolatey support for nonzero success exit codes --- windows/win_chocolatey.ps1 | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/windows/win_chocolatey.ps1 b/windows/win_chocolatey.ps1 index f47b3ce97dd..301ffe1a64b 100644 --- a/windows/win_chocolatey.ps1 +++ b/windows/win_chocolatey.ps1 @@ -39,6 +39,10 @@ $installargs = Get-Attr -obj $params -name install_args -default $null $packageparams = Get-Attr -obj $params -name params -default $null $ignoredependencies = Get-Attr -obj $params -name ignore_dependencies -default "false" | ConvertTo-Bool +# as of chocolatey 0.9.10, nonzero success exit codes can be returned +# see https://github.com/chocolatey/choco/issues/512#issuecomment-214284461 +$successexitcodes = (0,1605,1614,1641,3010) + if ("present","absent" -notcontains $state) { Fail-Json $result "state is $state; must be present or absent" @@ -159,7 +163,7 @@ Function Choco-Upgrade $results = invoke-expression $cmd - if ($LastExitCode -ne 0) + if ($LastExitCode -notin $successexitcodes) { Set-Attr $result "choco_error_cmd" $cmd Set-Attr $result "choco_error_log" "$results" @@ -244,7 +248,7 @@ Function Choco-Install $results = invoke-expression $cmd - if ($LastExitCode -ne 0) + if ($LastExitCode -notin $successexitcodes) { Set-Attr $result "choco_error_cmd" $cmd Set-Attr $result "choco_error_log" "$results" @@ -286,7 +290,7 @@ Function Choco-Uninstall $results = invoke-expression $cmd - if ($LastExitCode -ne 0) + if ($LastExitCode -notin $successexitcodes) { Set-Attr $result "choco_error_cmd" $cmd Set-Attr $result "choco_error_log" "$results" From 81c7630c011296a8cf34e75b47afec807dc0ec8e Mon Sep 17 00:00:00 2001 From: nitzmahone Date: Mon, 20 Jun 2016 13:13:09 -0700 Subject: [PATCH 1718/2522] fix statusio_maintenance exception handling --- monitoring/statusio_maintenance.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/monitoring/statusio_maintenance.py b/monitoring/statusio_maintenance.py index 893545e4057..c2b93db5c92 100644 --- a/monitoring/statusio_maintenance.py +++ b/monitoring/statusio_maintenance.py @@ -319,7 +319,8 @@ def create_maintenance(auth_headers, url, statuspage, host_ids, if data["status"]["error"] == "yes": return 1, None, data["status"]["message"] - except Exception, e: + except Exception: + e = get_exception() return 1, None, str(e) return 0, None, None @@ -337,7 +338,8 @@ def delete_maintenance(auth_headers, url, statuspage, maintenance_id): data = json.loads(response.read()) if data["status"]["error"] == "yes": return 1, None, "Invalid maintenance_id" - except Exception, e: + except Exception: + e = get_exception() return 1, None, str(e) return 0, None, None From 713aaa1d4a84fbded7b7992cfbf5e2b48617d873 Mon Sep 17 00:00:00 2001 From: Tim Small Date: Mon, 20 Jun 2016 22:30:10 +0100 Subject: [PATCH 1719/2522] Allow parameters to be passed for uninstall (required by some packages) (#2269) --- windows/win_chocolatey.ps1 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/windows/win_chocolatey.ps1 b/windows/win_chocolatey.ps1 index 301ffe1a64b..e725519b991 100644 --- a/windows/win_chocolatey.ps1 +++ b/windows/win_chocolatey.ps1 @@ -288,6 +288,11 @@ Function Choco-Uninstall $cmd += " -force" } + if ($packageparams) + { + $cmd += " -params '$packageparams'" + } + $results = invoke-expression $cmd if ($LastExitCode -notin $successexitcodes) From cb94edd17fc9d747e9f30886327384e9988587b2 Mon Sep 17 00:00:00 2001 From: Shinichi TAMURA Date: Tue, 21 Jun 2016 06:34:16 +0900 Subject: [PATCH 1720/2522] Added timezone module (#2414) --- system/timezone.py | 460 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 460 insertions(+) create mode 100644 system/timezone.py diff --git a/system/timezone.py b/system/timezone.py new file mode 100644 index 00000000000..2af3170e971 --- /dev/null +++ b/system/timezone.py @@ -0,0 +1,460 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Shinichi TAMURA (@tmshn) +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import re + +DOCUMENTATION = ''' +--- +module: timezone +short_description: Configure timezone setting +description: + - | + This module configures the timezone setting, both of the system clock + and of the hardware clock. I(Currently only Linux platform is supported.) + It is recommended to restart C(crond) after changing the timezone, + otherwise the jobs may run at the wrong time. + It uses the C(timedatectl) command if available. Otherwise, it edits + C(/etc/sysconfig/clock) or C(/etc/timezone) for the system clock, + and uses the C(hwclock) command for the hardware clock. + If you want to set up the NTP, use M(service) module. +version_added: "2.2.0" +options: + name: + description: + - | + Name of the timezone for the system clock. + Default is to keep current setting. + required: false + hwclock: + description: + - | + Whether the hardware clock is in UTC or in local timezone. + Default is to keep current setting. + Note that this option is recommended not to change and may fail + to configure, especially on virtual envoironments such as AWS. + required: false + aliases: ['rtc'] +author: "Shinichi TAMURA @tmshn" +''' + +RETURN = ''' +diff: + description: The differences about the given arguments. + returned: success + type: dictionary + contains: + before: + description: The values before change + type: dict + after: + description: The values after change + type: dict +''' + +EXAMPLES = ''' +- name: set timezone to Asia/Tokyo + timezone: name=Asia/Tokyo +''' + + +class Timezone(object): + """This is a generic Timezone manipulation class that is subclassed based on platform. + + A subclass may wish to override the following action methods: + - get(key, phase) ... get the value from the system at `phase` + - set(key, value) ... set the value to the curren system + """ + + def __new__(cls, module): + """Return the platform-specific subclass. + + It does not use load_platform_subclass() because it need to judge based + on whether the `timedatectl` command exists. + + Args: + module: The AnsibleModule. + """ + if get_platform() == 'Linux': + if module.get_bin_path('timedatectl') is not None: + return super(Timezone, SystemdTimezone).__new__(SystemdTimezone) + else: + return super(Timezone, NosystemdTimezone).__new__(NosystemdTimezone) + else: + # Not supported yet + return super(Timezone, Timezone).__new__(Timezone) + + def __init__(self, module): + """Initialize of the class. + + Args: + module: The AnsibleModule. + """ + super(Timezone, self).__init__() + self.msg = [] + # `self.value` holds the values for each params on each phases. + # Initially there's only info of "planned" phase, but the + # `self.check()` function will fill out it. + self.value = dict() + for key in module.argument_spec.iterkeys(): + value = module.params[key] + if value is not None: + self.value[key] = dict(planned=value) + self.module = module + + def abort(self, msg): + """Abort the process with error message. + + This is just the wrapper of module.fail_json(). + + Args: + msg: The error message. + """ + error_msg = ['Error message:', msg] + if len(self.msg) > 0: + error_msg.append('Other message(s):') + error_msg.extend(self.msg) + self.module.fail_json(msg='\n'.join(error_msg)) + + def execute(self, *commands, **kwargs): + """Execute the shell command. + + This is just the wrapper of module.run_command(). + + Args: + *commands: The command to execute. + It will be concatinated with single space. + **kwargs: Only 'log' key is checked. + If kwargs['log'] is true, record the command to self.msg. + + Returns: + stdout: Standard output of the command. + """ + command = ' '.join(commands) + (rc, stdout, stderr) = self.module.run_command(command, check_rc=True) + if kwargs.get('log', False): + self.msg.append('executed `{0}`'.format(command)) + return stdout + + def diff(self, phase1='before', phase2='after'): + """Calculate the difference between given 2 phases. + + Args: + phase1, phase2: The names of phase to compare. + + Returns: + diff: The difference of value between phase1 and phase2. + This is in the format which can be used with the + `--diff` option of ansible-playbook. + """ + diff = {phase1: {}, phase2: {}} + for key, value in self.value.iteritems(): + diff[phase1][key] = value[phase1] + diff[phase2][key] = value[phase2] + return diff + + def check(self, phase): + """Check the state in given phase and set it to `self.value`. + + Args: + phase: The name of the phase to check. + + Returns: + NO RETURN VALUE + """ + if phase == 'planned': + return + for key, value in self.value.iteritems(): + value[phase] = self.get(key, phase) + + def change(self): + """Make the changes effect based on `self.value`.""" + for key, value in self.value.iteritems(): + if value['before'] != value['planned']: + self.set(key, value['planned']) + + # =========================================== + # Platform specific methods (must be replaced by subclass). + + def get(self, key, phase): + """Get the value for the key at the given phase. + + Called from self.check(). + + Args: + key: The key to get the value + phase: The phase to get the value + + Return: + value: The value for the key at the given phase. + """ + self.abort('get(key, phase) is not implemented on target platform') + + def set(self, key, value): + """Set the value for the key (of course, for the phase 'after'). + + Called from self.change(). + + Args: + key: Key to set the value + value: Value to set + """ + self.abort('set(key, value) is not implemented on target platform') + + +class SystemdTimezone(Timezone): + """This is a Timezone manipulation class systemd-powered Linux. + + It uses the `timedatectl` command to check/set all arguments. + """ + + regexps = dict( + hwclock=re.compile(r'^\s*RTC in local TZ\s*:\s*([^\s]+)', re.MULTILINE), + name =re.compile(r'^\s*Time ?zone\s*:\s*([^\s]+)', re.MULTILINE) + ) + + subcmds = dict( + hwclock='set-local-rtc', + name ='set-timezone' + ) + + def __init__(self, module): + super(SystemdTimezone, self).__init__(module) + self.timedatectl = module.get_bin_path('timedatectl', required=True) + self.status = dict() + # Validate given timezone + if 'name' in self.value: + tz = self.value['name']['planned'] + tzfile = '/usr/share/zoneinfo/{0}'.format(tz) + if not os.path.isfile(tzfile): + self.abort('given timezone "{0}" is not available'.format(tz)) + + def _get_status(self, phase): + if phase not in self.status: + self.status[phase] = self.execute(self.timedatectl, 'status') + return self.status[phase] + + def get(self, key, phase): + status = self._get_status(phase) + value = self.regexps[key].search(status).group(1) + if key == 'hwclock': + # For key='hwclock'; convert yes/no -> local/UTC + if self.module.boolean(value): + value = 'local' + else: + value = 'UTC' + return value + + def set(self, key, value): + # For key='hwclock'; convert UTC/local -> yes/no + if key == 'hwclock': + if value == 'local': + value = 'yes' + else: + value = 'no' + self.execute(self.timedatectl, self.subcmds[key], value, log=True) + + +class NosystemdTimezone(Timezone): + """This is a Timezone manipulation class for non systemd-powered Linux. + + For timezone setting, it edits the following file and reflect changes: + - /etc/sysconfig/clock ... RHEL/CentOS + - /etc/timezone ... Debian/Ubnutu + For hwclock setting, it executes `hwclock --systohc` command with the + '--utc' or '--localtime' option. + """ + + conf_files = dict( + name =None, # To be set in __init__ + hwclock=None, # To be set in __init__ + adjtime='/etc/adjtime' + ) + + regexps = dict( + name =None, # To be set in __init__ + hwclock=re.compile(r'^UTC\s*=\s*([^\s]+)', re.MULTILINE), + adjtime=re.compile(r'^(UTC|LOCAL)$', re.MULTILINE) + ) + + def __init__(self, module): + super(NosystemdTimezone, self).__init__(module) + # Validate given timezone + if 'name' in self.value: + tz = self.value['name']['planned'] + tzfile = '/usr/share/zoneinfo/{0}'.format(tz) + if not os.path.isfile(tzfile): + self.abort('given timezone "{0}" is not available'.format(tz)) + self.update_timezone = self.module.get_bin_path('cp', required=True) + self.update_timezone += ' {0} /etc/localtime'.format(tzfile) + self.update_hwclock = self.module.get_bin_path('hwclock', required=True) + # Distribution-specific configurations + if self.module.get_bin_path('dpkg-reconfigure') is not None: + # Debian/Ubuntu + self.update_timezone = self.module.get_bin_path('dpkg-reconfigure', required=True) + self.update_timezone += ' --frontend noninteractive tzdata' + self.conf_files['name'] = '/etc/timezone', + self.conf_files['hwclock'] = '/etc/default/rcS', + self.regexps['name'] = re.compile(r'^([^\s]+)', re.MULTILINE) + self.tzline_format = '{0}\n' + else: + # RHEL/CentOS + if self.module.get_bin_path('tzdata-update') is not None: + self.update_timezone = self.module.get_bin_path('tzdata-update', required=True) + # else: + # self.update_timezone = 'cp ...' <- configured above + self.conf_files['name'] = '/etc/sysconfig/clock' + self.conf_files['hwclock'] = '/etc/sysconfig/clock' + self.regexps['name'] = re.compile(r'^ZONE\s*=\s*"?([^"\s]+)"?', re.MULTILINE) + self.tzline_format = 'ZONE="{0}"\n' + self.update_hwclock = self.module.get_bin_path('hwclock', required=True) + + def _edit_file(self, filename, regexp, value): + """Replace the first matched line with given `value`. + + If `regexp` matched more than once, other than the first line will be deleted. + + Args: + filename: The name of the file to edit. + regexp: The regular expression to search with. + value: The line which will be inserted. + """ + # Read the file + try: + file = open(filename, 'r') + except IOError: + self.abort('cannot read "{0}"'.format(filename)) + else: + lines = file.readlines() + file.close() + # Find the all matched lines + matched_indices = [] + for i, line in enumerate(lines): + if regexp.search(line): + matched_indices.append(i) + if len(matched_indices) > 0: + insert_line = matched_indices[0] + else: + insert_line = 0 + # Remove all matched lines + for i in matched_indices[::-1]: + del lines[i] + # ...and insert the value + lines.insert(insert_line, value) + # Write the changes + try: + file = open(filename, 'w') + except IOError: + self.abort('cannot write to "{0}"'.format(filename)) + else: + file.writelines(lines) + file.close() + self.msg.append('Added 1 line and deleted {0} line(s) on {1}'.format(len(matched_indices), filename)) + + def get(self, key, phase): + if key == 'hwclock' and os.path.isfile('/etc/adjtime'): + # If /etc/adjtime exists, use that file. + key = 'adjtime' + try: + file = open(self.conf_files[key], mode='r') + except IOError: + self.abort('cannot read configuration file "{0}" for {1}'.format(filename, key)) + else: + status = file.read() + file.close() + try: + value = self.regexps[key].search(status).group(1) + except AttributeError: + self.abort('cannot find the valid value from configuration file "{0}" for {1}'.format(filename, key)) + else: + if key == 'hwclock': + # For key='hwclock'; convert yes/no -> UTC/local + if self.module.boolean(value): + value = 'UTC' + else: + value = 'local' + elif key == 'adjtime': + # For key='adjtime'; convert LOCAL -> local + if value != 'UTC': + value = value.lower() + return value + + def set_timezone(self, value): + self._edit_file(filename=self.conf_files['name'], + regexp=self.regexps['name'], + value=self.tzline_format.format(value)) + self.execute(self.update_timezone) + + def set_hwclock(self, value): + if value == 'local': + option = '--localtime' + else: + option = '--utc' + self.execute(self.update_hwclock, '--systohc', option, log=True) + + def set(self, key, value): + if key == 'name': + self.set_timezone(value) + elif key == 'hwclock': + self.set_hwclock(value) + else: + self.abort('unknown parameter "{0}"'.format(key)) + + +def main(): + # Construct 'module' and 'tz' + arg_spec = dict( + hwclock=dict(choices=['UTC', 'local'], aliases=['rtc']), + name =dict(), + ) + module = AnsibleModule( + argument_spec=arg_spec, + required_one_of=[arg_spec.keys()], + supports_check_mode=True + ) + tz = Timezone(module) + + # Check the current state + tz.check(phase='before') + if module.check_mode: + diff = tz.diff('before', 'planned') + # In check mode, 'planned' state is treated as 'after' state + diff['after'] = diff.pop('planned') + else: + # Make change + tz.change() + # Check the current state + tz.check(phase='after') + # Examine if the current state matches planned state + (after, planned) = tz.diff('after', 'planned').values() + if after != planned: + tz.abort('still not desired state, though changes have made') + diff = tz.diff('before', 'after') + + changed = (diff['before'] != diff['after']) + if len(tz.msg) > 0: + module.exit_json(changed=changed, diff=diff, msg='\n'.join(tz.msg)) + else: + module.exit_json(changed=changed, diff=diff) + + +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From 939294391562585218e07fc7ae0eb9f1efe588ff Mon Sep 17 00:00:00 2001 From: Andrea Scarpino Date: Mon, 20 Jun 2016 23:35:27 +0200 Subject: [PATCH 1721/2522] win_firewall_rule: strictmode fixes (#2432) I set the default values to `netsh advfirewall firewall add rule` defaults. --- windows/win_firewall_rule.ps1 | 105 ++++++++++++---------------------- windows/win_firewall_rule.py | 21 ++++--- 2 files changed, 48 insertions(+), 78 deletions(-) diff --git a/windows/win_firewall_rule.ps1 b/windows/win_firewall_rule.ps1 index 92d75921547..ae60bcc4aa3 100644 --- a/windows/win_firewall_rule.ps1 +++ b/windows/win_firewall_rule.ps1 @@ -20,9 +20,6 @@ # WANT_JSON # POWERSHELL_COMMON -# temporarily disable strictmode, for this module only -Set-StrictMode -Off - function getFirewallRule ($fwsettings) { try { @@ -205,80 +202,54 @@ $fwsettings=@{} # Variabelise the arguments $params=Parse-Args $args; -$enable=Get-Attr $params "enable" $null; -$state=Get-Attr $params "state" "present"; -$name=Get-Attr $params "name" ""; -$direction=Get-Attr $params "direction" ""; -$force=Get-Attr $params "force" $false; -$action=Get-Attr $params "action" ""; +$name = Get-AnsibleParam -obj $params -name "name" -failifempty $true +$direction = Get-AnsibleParam -obj $params -name "direction" -failifempty $true -validateSet "in","out" +$action = Get-AnsibleParam -obj $params -name "action" -failifempty $true -validateSet "allow","block","bypass" +$program = Get-AnsibleParam -obj $params -name "program" +$service = Get-AnsibleParam -obj $params -name "service" -default "any" +$description = Get-AnsibleParam -obj $params -name "description" +$enable = ConvertTo-Bool (Get-AnsibleParam -obj $params -name "enable" -default "true") +$winprofile = Get-AnsibleParam -obj $params -name "profile" -default "any" +$localip = Get-AnsibleParam -obj $params -name "localip" -default "any" +$remoteip = Get-AnsibleParam -obj $params -name "remoteip" -default "any" +$localport = Get-AnsibleParam -obj $params -name "localport" -default "any" +$remoteport = Get-AnsibleParam -obj $params -name "remoteport" -default "any" +$protocol = Get-AnsibleParam -obj $params -name "protocol" -default "any" + +$state = Get-AnsibleParam -obj $params -name "state" -failifempty $true -validateSet "present","absent" +$force = ConvertTo-Bool (Get-AnsibleParam -obj $params -name "force" -default "false") -$misArg = '' # Check the arguments -if ($enable -ne $null) { - $enable=ConvertTo-Bool $enable; - if ($enable -eq $true) { - $fwsettings.Add("Enabled", "yes"); - } elseif ($enable -eq $false) { - $fwsettings.Add("Enabled", "no"); - } else { - $misArg+="enable"; - $msg+=@("for the enable parameter only yes and no is allowed"); - }; +If ($enable -eq $true) { + $fwsettings.Add("Enabled", "yes"); +} Else { + $fwsettings.Add("Enabled", "no"); }; -if (($state -ne "present") -And ($state -ne "absent")){ - $misArg+="state"; - $msg+=@("for the state parameter only present and absent is allowed"); -}; +$fwsettings.Add("Rule Name", $name) +#$fwsettings.Add("displayname", $name) -if ($name -eq ""){ - $misArg+="Name"; - $msg+=@("name is a required argument"); -} else { - $fwsettings.Add("Rule Name", $name) - #$fwsettings.Add("displayname", $name) -}; -if ((($direction.ToLower() -ne "In") -And ($direction.ToLower() -ne "Out")) -And ($state -eq "present")){ - $misArg+="Direction"; - $msg+=@("for the Direction parameter only the values 'In' and 'Out' are allowed"); -} else { +$state = $state.ToString().ToLower() +If ($state -eq "present")){ $fwsettings.Add("Direction", $direction) -}; -if ((($action.ToLower() -ne "allow") -And ($action.ToLower() -ne "block")) -And ($state -eq "present")){ - $misArg+="Action"; - $msg+=@("for the Action parameter only the values 'allow' and 'block' are allowed"); -} else { $fwsettings.Add("Action", $action) }; -$args=@( - "Description", - "LocalIP", - "RemoteIP", - "LocalPort", - "RemotePort", - "Program", - "Service", - "Protocol" -) -foreach ($arg in $args){ - New-Variable -Name $arg -Value $(Get-Attr $params $arg ""); - if ((Get-Variable -Name $arg -ValueOnly) -ne ""){ - $fwsettings.Add($arg, $(Get-Variable -Name $arg -ValueOnly)); - }; -}; +If ($description) { + $fwsettings.Add("Description", $description); +} -$winprofile=Get-Attr $params "profile" "current"; -$fwsettings.Add("Profiles", $winprofile) +If ($program) { + $fwsettings.Add("Program", $program); +} -if ($misArg){ - $result=New-Object psobject @{ - changed=$false - failed=$true - msg=$msg - }; - Exit-Json($result); -}; +$fwsettings.Add("LocalIP", $localip); +$fwsettings.Add("RemoteIP", $remoteip); +$fwsettings.Add("LocalPort", $localport); +$fwsettings.Add("RemotePort", $remoteport); +$fwsettings.Add("Service", $service); +$fwsettings.Add("Protocol", $protocol); +$fwsettings.Add("Profiles", $winprofile) $output=@() $capture=getFirewallRule ($fwsettings); @@ -299,7 +270,7 @@ if ($capture.failed -eq $true) { } -switch ($state.ToLower()){ +switch ($state){ "present" { if ($capture.exists -eq $false) { $capture=createFireWallRule($fwsettings); diff --git a/windows/win_firewall_rule.py b/windows/win_firewall_rule.py index d833c2fa24d..3ed0f7e3e7b 100644 --- a/windows/win_firewall_rule.py +++ b/windows/win_firewall_rule.py @@ -29,9 +29,8 @@ enable: description: - is this firewall rule enabled or disabled - default: null + default: true required: false - choices: ['yes', 'no'] state: description: - should this rule be added or removed @@ -48,13 +47,13 @@ - is this rule for inbound or outbound trafic default: null required: true - choices: [ 'In', 'Out' ] + choices: ['in', 'out'] action: description: - what to do with the items this rule is for default: null required: true - choices: [ 'allow', 'block' ] + choices: ['allow', 'block', 'bypass'] description: description: - description for the firewall rule @@ -63,22 +62,22 @@ localip: description: - the local ip address this rule applies to - default: null + default: 'any' required: false remoteip: description: - the remote ip address/range this rule applies to - default: null + default: 'any' required: false localport: description: - the local port this rule applies to - default: null + default: 'any' required: false remoteport: description: - the remote port this rule applies to - default: null + default: 'any' required: false program: description: @@ -88,17 +87,17 @@ service: description: - the service this rule applies to - default: null + default: 'any' required: false protocol: description: - the protocol this rule applies to - default: null + default: 'any' required: false profile: description: - the profile this rule applies to, e.g. Domain,Private,Public - default: null + default: 'any' required: false force: description: From ba6aa8b013593f466cd95a214fec13e94d1b2e25 Mon Sep 17 00:00:00 2001 From: Shinichi TAMURA Date: Wed, 22 Jun 2016 21:53:16 +0900 Subject: [PATCH 1722/2522] Added 'answer' as an alias of 'value' on debconf module (#2464) --- system/debconf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/debconf.py b/system/debconf.py index cc4681ebb3b..22f4cb3fc3d 100644 --- a/system/debconf.py +++ b/system/debconf.py @@ -119,7 +119,7 @@ def main(): name = dict(required=True, aliases=['pkg'], type='str'), question = dict(required=False, aliases=['setting', 'selection'], type='str'), vtype = dict(required=False, type='str', choices=['string', 'password', 'boolean', 'select', 'multiselect', 'note', 'error', 'title', 'text', 'seen']), - value= dict(required=False, type='str'), + value = dict(required=False, type='str', aliases=['answer']), unseen = dict(required=False, type='bool'), ), required_together = ( ['question','vtype', 'value'],), From 9df4f6a4c79e54e79a14e2932be821c4450b0473 Mon Sep 17 00:00:00 2001 From: nitzmahone Date: Wed, 22 Jun 2016 17:18:26 -0700 Subject: [PATCH 1723/2522] fix win_updates to prevent Get-Member failure on bogus job output --- windows/win_updates.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/windows/win_updates.ps1 b/windows/win_updates.ps1 index 0c76dcad664..a74e68f3663 100644 --- a/windows/win_updates.ps1 +++ b/windows/win_updates.ps1 @@ -342,7 +342,7 @@ Function RunAsScheduledJob { $sw = [System.Diagnostics.Stopwatch]::StartNew() # NB: output from scheduled jobs is delayed after completion (including the sub-objects after the primary Output object is available) - While (($job.Output -eq $null -or -not ($job.Output | Get-Member -Name Keys) -or -not $job.Output.Keys.Contains('job_output')) -and $sw.ElapsedMilliseconds -lt 15000) { + While (($job.Output -eq $null -or -not ($job.Output | Get-Member -Name Keys -ErrorAction Ignore) -or -not $job.Output.Keys.Contains('job_output')) -and $sw.ElapsedMilliseconds -lt 15000) { Write-DebugLog "Waiting for job output to populate..." Start-Sleep -Milliseconds 500 } @@ -377,6 +377,7 @@ Function Log-Forensics { Write-DebugLog "Arguments: $job_args | out-string" Write-DebugLog "OS Version: $([environment]::OSVersion.Version | out-string)" Write-DebugLog "Running as user: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)" + Write-DebugLog "Powershell version: $($PSVersionTable | out-string)" # FUTURE: log auth method (kerb, password, etc) } From 709114d55fe35242bc1f2e81309a72930386ee5c Mon Sep 17 00:00:00 2001 From: nitzmahone Date: Wed, 22 Jun 2016 17:26:41 -0700 Subject: [PATCH 1724/2522] fix ec2_vpc_vgw exception handling issues --- cloud/amazon/ec2_vpc_vgw.py | 45 ++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/cloud/amazon/ec2_vpc_vgw.py b/cloud/amazon/ec2_vpc_vgw.py index 9861e7d0b7e..c3e4d1f1ce4 100644 --- a/cloud/amazon/ec2_vpc_vgw.py +++ b/cloud/amazon/ec2_vpc_vgw.py @@ -157,7 +157,8 @@ def wait_for_status(client, module, vpn_gateway_id, status): break else: time.sleep(polling_increment_secs) - except botocore.exceptions.ClientError as e: + except botocore.exceptions.ClientError: + e = get_exception() module.fail_json(msg=str(e)) result = response @@ -170,7 +171,8 @@ def attach_vgw(client, module, vpn_gateway_id): try: response = client.attach_vpn_gateway(VpnGatewayId=vpn_gateway_id, VpcId=params['VpcId']) - except botocore.exceptions.ClientError as e: + except botocore.exceptions.ClientError: + e = get_exception() module.fail_json(msg=str(e)) status_achieved, vgw = wait_for_status(client, module, [vpn_gateway_id], 'attached') @@ -188,12 +190,14 @@ def detach_vgw(client, module, vpn_gateway_id, vpc_id=None): if vpc_id: try: response = client.detach_vpn_gateway(VpnGatewayId=vpn_gateway_id, VpcId=vpc_id) - except botocore.exceptions.ClientError as e: + except botocore.exceptions.ClientError: + e = get_exception() module.fail_json(msg=str(e)) else: try: response = client.detach_vpn_gateway(VpnGatewayId=vpn_gateway_id, VpcId=params['VpcId']) - except botocore.exceptions.ClientError as e: + except botocore.exceptions.ClientError: + e = get_exception() module.fail_json(msg=str(e)) status_achieved, vgw = wait_for_status(client, module, [vpn_gateway_id], 'detached') @@ -210,7 +214,8 @@ def create_vgw(client, module): try: response = client.create_vpn_gateway(Type=params['Type']) - except botocore.exceptions.ClientError as e: + except botocore.exceptions.ClientError: + e = get_exception() module.fail_json(msg=str(e)) result = response @@ -221,7 +226,8 @@ def delete_vgw(client, module, vpn_gateway_id): try: response = client.delete_vpn_gateway(VpnGatewayId=vpn_gateway_id) - except botocore.exceptions.ClientError as e: + except botocore.exceptions.ClientError: + e = get_exception() module.fail_json(msg=str(e)) #return the deleted VpnGatewayId as this is not included in the above response @@ -234,7 +240,8 @@ def create_tags(client, module, vpn_gateway_id): try: response = client.create_tags(Resources=[vpn_gateway_id],Tags=load_tags(module)) - except botocore.exceptions.ClientError as e: + except botocore.exceptions.ClientError: + e = get_exception() module.fail_json(msg=str(e)) result = response @@ -247,12 +254,14 @@ def delete_tags(client, module, vpn_gateway_id, tags_to_delete=None): if tags_to_delete: try: response = client.delete_tags(Resources=[vpn_gateway_id], Tags=tags_to_delete) - except botocore.exceptions.ClientError as e: + except botocore.exceptions.ClientError: + e = get_exception() module.fail_json(msg=str(e)) else: try: response = client.delete_tags(Resources=[vpn_gateway_id]) - except botocore.exceptions.ClientError as e: + except botocore.exceptions.ClientError: + e = get_exception() module.fail_json(msg=str(e)) result = response @@ -278,7 +287,8 @@ def find_tags(client, module, resource_id=None): response = client.describe_tags(Filters=[ {'Name': 'resource-id', 'Values': [resource_id]} ]) - except botocore.exceptions.ClientError as e: + except botocore.exceptions.ClientError: + e = get_exception() module.fail_json(msg=str(e)) result = response @@ -325,7 +335,8 @@ def find_vpc(client, module): if params['vpc_id']: try: response = client.describe_vpcs(VpcIds=[params['vpc_id']]) - except botocore.exceptions.ClientError as e: + except botocore.exceptions.ClientError: + e = get_exception() module.fail_json(msg=str(e)) result = response @@ -344,14 +355,16 @@ def find_vgw(client, module, vpn_gateway_id=None): {'Name': 'type', 'Values': [params['Type']]}, {'Name': 'tag:Name', 'Values': [params['Name']]} ]) - except botocore.exceptions.ClientError as e: + except botocore.exceptions.ClientError: + e = get_exception() module.fail_json(msg=str(e)) else: if vpn_gateway_id: try: response = client.describe_vpn_gateways(VpnGatewayIds=vpn_gateway_id) - except botocore.exceptions.ClientError as e: + except botocore.exceptions.ClientError: + e = get_exception() module.fail_json(msg=str(e)) else: @@ -360,7 +373,8 @@ def find_vgw(client, module, vpn_gateway_id=None): {'Name': 'type', 'Values': [params['Type']]}, {'Name': 'tag:Name', 'Values': [params['Name']]} ]) - except botocore.exceptions.ClientError as e: + except botocore.exceptions.ClientError: + e = get_exception() module.fail_json(msg=str(e)) result = response['VpnGateways'] @@ -564,7 +578,8 @@ def main(): try: region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) client = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except botocore.exceptions.NoCredentialsError, e: + except botocore.exceptions.NoCredentialsError: + e = get_exception() module.fail_json(msg="Can't authorize connection - "+str(e)) if state == 'present': From 2be4e15a0a52a324218ced18572bcc596fff10cb Mon Sep 17 00:00:00 2001 From: Trond Hindenes Date: Thu, 23 Jun 2016 08:45:29 +0200 Subject: [PATCH 1725/2522] bugfix code error in win_firewall --- windows/win_firewall_rule.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_firewall_rule.ps1 b/windows/win_firewall_rule.ps1 index ae60bcc4aa3..31aa2741cdb 100644 --- a/windows/win_firewall_rule.ps1 +++ b/windows/win_firewall_rule.ps1 @@ -230,7 +230,7 @@ $fwsettings.Add("Rule Name", $name) #$fwsettings.Add("displayname", $name) $state = $state.ToString().ToLower() -If ($state -eq "present")){ +If ($state -eq "present"){ $fwsettings.Add("Direction", $direction) $fwsettings.Add("Action", $action) }; From 9d37820b088b19f4dbf708e8e1803f926baeb7ad Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Thu, 23 Jun 2016 02:33:49 -0700 Subject: [PATCH 1726/2522] Adds server port argument to legacy modules (#2444) This patch adds support for the server_port module. It additionally updates the documentation in the module for it. The changes were tested in the f5-ansible repository to ensure no breaking changes were made. This argument allows modules to be used on BIG-IPs that are listening on non-standard ports. --- network/f5/bigip_facts.py | 17 ++++++++--- network/f5/bigip_gtm_virtual_server.py | 41 +++++++++++++++----------- network/f5/bigip_gtm_wide_ip.py | 40 +++++++++++++++++-------- network/f5/bigip_monitor_http.py | 13 ++++++-- network/f5/bigip_monitor_tcp.py | 13 ++++++-- network/f5/bigip_node.py | 16 ++++++++-- network/f5/bigip_pool.py | 17 ++++++++--- network/f5/bigip_pool_member.py | 13 ++++++-- network/f5/bigip_virtual_server.py | 15 ++++++++-- 9 files changed, 135 insertions(+), 50 deletions(-) diff --git a/network/f5/bigip_facts.py b/network/f5/bigip_facts.py index 0f121b2a3aa..44221ca350e 100644 --- a/network/f5/bigip_facts.py +++ b/network/f5/bigip_facts.py @@ -25,7 +25,9 @@ description: - "Collect facts from F5 BIG-IP devices via iControl SOAP API" version_added: "1.6" -author: "Matt Hite (@mhite)" +author: + - Matt Hite (@mhite) + - Tim Rupp (@caphrim007) notes: - "Requires BIG-IP software version >= 11.4" - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" @@ -42,6 +44,12 @@ default: null choices: [] aliases: [] + server_port: + description: + - BIG-IP server port + required: false + default: 443 + version_added: "2.2" user: description: - BIG-IP username @@ -137,8 +145,8 @@ class F5(object): api: iControl API instance. """ - def __init__(self, host, user, password, session=False, validate_certs=True): - self.api = bigip_api(host, user, password, validate_certs) + def __init__(self, host, user, password, session=False, validate_certs=True, port=443): + self.api = bigip_api(host, user, password, validate_certs, port) if session: self.start_session() @@ -1593,6 +1601,7 @@ def main(): module.fail_json(msg="the python suds and bigsuds modules are required") server = module.params['server'] + server_port = module.params['server_port'] user = module.params['user'] password = module.params['password'] validate_certs = module.params['validate_certs'] @@ -1622,7 +1631,7 @@ def main(): facts = {} if len(include) > 0: - f5 = F5(server, user, password, session, validate_certs) + f5 = F5(server, user, password, session, validate_certs, server_port) saved_active_folder = f5.get_active_folder() saved_recursive_query_state = f5.get_recursive_query_state() if saved_active_folder != "/": diff --git a/network/f5/bigip_gtm_virtual_server.py b/network/f5/bigip_gtm_virtual_server.py index 0f3e04877cf..8d0657f25f6 100644 --- a/network/f5/bigip_gtm_virtual_server.py +++ b/network/f5/bigip_gtm_virtual_server.py @@ -25,7 +25,9 @@ description: - "Manages F5 BIG-IP GTM virtual servers" version_added: "2.2" -author: 'Michael Perzel' +author: + - Michael Perzel (@perzizzle) + - Tim Rupp (@caphrim007) notes: - "Requires BIG-IP software version >= 11.4" - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" @@ -39,6 +41,11 @@ description: - BIG-IP host required: true + server_port: + description: + - BIG-IP server port + required: false + default: 443 user: description: - BIG-IP username @@ -96,11 +103,6 @@ bigsuds_found = True -def bigip_api(server, user, password): - api = bigsuds.BIGIP(hostname=server, username=user, password=password) - return api - - def server_exists(api, server): # hack to determine if virtual server exists result = False @@ -157,17 +159,19 @@ def set_virtual_server_state(api, name, server, state): def main(): + argument_spec = f5_argument_spec() + + meta_args = dict( + state=dict(type='str', default='present', choices=['present', 'absent', 'enabled', 'disabled']), + host=dict(type='str', default=None, aliases=['address']), + port=dict(type='int', default=None), + virtual_server_name=dict(type='str', required=True), + virtual_server_server=dict(type='str', required=True) + ) + argument_spec.update(meta_args) + module = AnsibleModule( - argument_spec=dict( - server=dict(type='str', required=True), - user=dict(type='str', required=True), - password=dict(type='str', required=True, no_log=True), - state=dict(type='str', default='present', choices=['present', 'absent', 'enabled', 'disabled']), - host=dict(type='str', default=None, aliases=['address']), - port=dict(type='int', default=None), - virtual_server_name=dict(type='str', required=True), - virtual_server_server=dict(type='str', required=True) - ), + argument_spec=argument_spec, supports_check_mode=True ) @@ -175,6 +179,8 @@ def main(): module.fail_json(msg="the python bigsuds module is required") server = module.params['server'] + server_port = module.params['server_port'] + validate_certs = module.params['validate_certs'] user = module.params['user'] password = module.params['password'] virtual_server_name = module.params['virtual_server_name'] @@ -186,7 +192,7 @@ def main(): result = {'changed': False} # default try: - api = bigip_api(server, user, password) + api = bigip_api(server, user, password, validate_certs, port=server_port) if state == 'absent': if virtual_server_exists(api, virtual_server_name, virtual_server_server): @@ -239,6 +245,7 @@ def main(): # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.f5 import * if __name__ == '__main__': main() diff --git a/network/f5/bigip_gtm_wide_ip.py b/network/f5/bigip_gtm_wide_ip.py index 120921b2f7c..f03a0416e3e 100644 --- a/network/f5/bigip_gtm_wide_ip.py +++ b/network/f5/bigip_gtm_wide_ip.py @@ -25,7 +25,9 @@ description: - "Manages F5 BIG-IP GTM wide ip" version_added: "2.0" -author: 'Michael Perzel' +author: + - Michael Perzel (@perzizzle) + - Tim Rupp (@caphrim007) notes: - "Requires BIG-IP software version >= 11.4" - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" @@ -39,6 +41,12 @@ description: - BIG-IP host required: true + server_port: + description: + - BIG-IP server port + required: false + default: 443 + version_added: "2.2" user: description: - BIG-IP username @@ -56,6 +64,13 @@ 'vs_capacity', 'least_conn', 'lowest_rtt', 'lowest_hops', 'packet_rate', 'cpu', 'hit_ratio', 'qos', 'bps', 'drop_packet', 'explicit_ip', 'connection_rate', 'vs_score'] + validate_certs: + description: + - If C(no), SSL certificates will not be validated. This should only be + used on personally controlled sites using self-signed certificates. + required: false + default: true + version_added: "2.2" wide_ip: description: - Wide IP name @@ -80,10 +95,6 @@ else: bigsuds_found = True -def bigip_api(server, user, password): - api = bigsuds.BIGIP(hostname=server, username=user, password=password) - return api - def get_wide_ip_lb_method(api, wide_ip): lb_method = api.GlobalLB.WideIP.get_lb_method(wide_ips=[wide_ip])[0] lb_method = lb_method.strip().replace('LB_METHOD_', '').lower() @@ -114,21 +125,21 @@ def set_wide_ip_lb_method(api, wide_ip, lb_method): api.GlobalLB.WideIP.set_lb_method(wide_ips=[wide_ip], lb_methods=[lb_method]) def main(): + argument_spec = f5_argument_spec() lb_method_choices = ['return_to_dns', 'null', 'round_robin', 'ratio', 'topology', 'static_persist', 'global_availability', 'vs_capacity', 'least_conn', 'lowest_rtt', 'lowest_hops', 'packet_rate', 'cpu', 'hit_ratio', 'qos', 'bps', 'drop_packet', 'explicit_ip', 'connection_rate', 'vs_score'] + meta_args = dict( + lb_method = dict(type='str', required=True, choices=lb_method_choices), + wide_ip = dict(type='str', required=True) + ) + argument_spec.update(meta_args) module = AnsibleModule( - argument_spec = dict( - server = dict(type='str', required=True), - user = dict(type='str', required=True), - password = dict(type='str', required=True), - lb_method = dict(type='str', required=True, choices=lb_method_choices), - wide_ip = dict(type='str', required=True) - ), + argument_spec=argument_spec, supports_check_mode=True ) @@ -136,15 +147,17 @@ def main(): module.fail_json(msg="the python bigsuds module is required") server = module.params['server'] + server_port = module.params['server_port'] user = module.params['user'] password = module.params['password'] wide_ip = module.params['wide_ip'] lb_method = module.params['lb_method'] + validate_certs = module.params['validate_certs'] result = {'changed': False} # default try: - api = bigip_api(server, user, password) + api = bigip_api(server, user, password, validate_certs, port=server_port) if not wide_ip_exists(api, wide_ip): module.fail_json(msg="wide ip %s does not exist" % wide_ip) @@ -163,6 +176,7 @@ def main(): # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.f5 import * if __name__ == '__main__': main() diff --git a/network/f5/bigip_monitor_http.py b/network/f5/bigip_monitor_http.py index dcdaf4b65b6..0c6b15936b1 100644 --- a/network/f5/bigip_monitor_http.py +++ b/network/f5/bigip_monitor_http.py @@ -27,7 +27,9 @@ description: - "Manages F5 BIG-IP LTM monitors via iControl SOAP API" version_added: "1.4" -author: "Serge van Ginderachter (@srvg)" +author: + - Serge van Ginderachter (@srvg) + - Tim Rupp (@caphrim007) notes: - "Requires BIG-IP software version >= 11" - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" @@ -41,6 +43,12 @@ - BIG-IP host required: true default: null + server_port: + description: + - BIG-IP server port + required: false + default: 443 + version_added: "2.2" user: description: - BIG-IP username @@ -326,6 +334,7 @@ def main(): module.fail_json(msg='bigsuds does not support verifying certificates with python < 2.7.9. Either update python or set validate_certs=False on the task') server = module.params['server'] + server_port = module.params['server_port'] user = module.params['user'] password = module.params['password'] state = module.params['state'] @@ -347,7 +356,7 @@ def main(): # end monitor specific stuff - api = bigip_api(server, user, password, validate_certs) + api = bigip_api(server, user, password, validate_certs, port=server_port) monitor_exists = check_monitor_exists(module, api, monitor, parent) diff --git a/network/f5/bigip_monitor_tcp.py b/network/f5/bigip_monitor_tcp.py index 0ef6a9add26..5de83fd14b9 100644 --- a/network/f5/bigip_monitor_tcp.py +++ b/network/f5/bigip_monitor_tcp.py @@ -25,7 +25,9 @@ description: - "Manages F5 BIG-IP LTM tcp monitors via iControl SOAP API" version_added: "1.4" -author: "Serge van Ginderachter (@srvg)" +author: + - Serge van Ginderachter (@srvg) + - Tim Rupp (@caphrim007) notes: - "Requires BIG-IP software version >= 11" - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" @@ -39,6 +41,12 @@ - BIG-IP host required: true default: null + server_port: + description: + - BIG-IP server port + required: false + default: 443 + version_added: "2.2" user: description: - BIG-IP username @@ -345,6 +353,7 @@ def main(): module.fail_json(msg='bigsuds does not support verifying certificates with python < 2.7.9. Either update python or set validate_certs=False on the task') server = module.params['server'] + server_port = module.params['server_port'] user = module.params['user'] password = module.params['password'] state = module.params['state'] @@ -370,7 +379,7 @@ def main(): # end monitor specific stuff - api = bigip_api(server, user, password, validate_certs) + api = bigip_api(server, user, password, validate_certs, port=server_port) monitor_exists = check_monitor_exists(module, api, monitor, parent) diff --git a/network/f5/bigip_node.py b/network/f5/bigip_node.py index 3f57251218a..7a648341c0c 100644 --- a/network/f5/bigip_node.py +++ b/network/f5/bigip_node.py @@ -25,7 +25,9 @@ description: - "Manages F5 BIG-IP LTM nodes via iControl SOAP API" version_added: "1.4" -author: "Matt Hite (@mhite)" +author: + - Matt Hite (@mhite) + - Tim Rupp (@caphrim007) notes: - "Requires BIG-IP software version >= 11" - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" @@ -40,6 +42,12 @@ default: null choices: [] aliases: [] + server_port: + description: + - BIG-IP server port + required: false + default: 443 + version_added: "2.2" user: description: - BIG-IP username @@ -313,7 +321,8 @@ def set_monitors(api, name, monitor_type, quorum, monitor_templates): def main(): monitor_type_choices = ['and_list', 'm_of_n'] - argument_spec=f5_argument_spec() + argument_spec = f5_argument_spec() + argument_spec.update(dict( session_state = dict(type='str', choices=['enabled', 'disabled']), monitor_state = dict(type='str', choices=['enabled', 'disabled']), @@ -340,6 +349,7 @@ def main(): module.fail_json(msg='bigsuds does not support verifying certificates with python < 2.7.9. Either update python or set validate_certs=False on the task') server = module.params['server'] + server_port = module.params['server_port'] user = module.params['user'] password = module.params['password'] state = module.params['state'] @@ -387,7 +397,7 @@ def main(): module.fail_json(msg="quorum requires monitors parameter") try: - api = bigip_api(server, user, password, validate_certs) + api = bigip_api(server, user, password, validate_certs, port=server_port) result = {'changed': False} # default if state == 'absent': diff --git a/network/f5/bigip_pool.py b/network/f5/bigip_pool.py index 4e413f9fd07..8f3a36e265a 100644 --- a/network/f5/bigip_pool.py +++ b/network/f5/bigip_pool.py @@ -25,7 +25,9 @@ description: - "Manages F5 BIG-IP LTM pools via iControl SOAP API" version_added: "1.2" -author: "Matt Hite (@mhite)" +author: + - Matt Hite (@mhite) + - Tim Rupp (@caphrim007) notes: - "Requires BIG-IP software version >= 11" - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" @@ -40,6 +42,12 @@ default: null choices: [] aliases: [] + server_port: + description: + - BIG-IP server port + required: false + default: 443 + version_added: "2.2" user: description: - BIG-IP username @@ -95,7 +103,7 @@ 'least_connection_node_address', 'fastest_node_address', 'observed_node_address', 'predictive_node_address', 'dynamic_ratio', 'fastest_app_response', 'least_sessions', - 'dynamic_ratio_member', 'l3_addr', 'unknown', + 'dynamic_ratio_member', 'l3_addr', 'weighted_least_connection_member', 'weighted_least_connection_node_address', 'ratio_session', 'ratio_least_connection_member', @@ -353,7 +361,7 @@ def main(): 'fastest_node_address', 'observed_node_address', 'predictive_node_address', 'dynamic_ratio', 'fastest_app_response', 'least_sessions', - 'dynamic_ratio_member', 'l3_addr', 'unknown', + 'dynamic_ratio_member', 'l3_addr', 'weighted_least_connection_member', 'weighted_least_connection_node_address', 'ratio_session', 'ratio_least_connection_member', @@ -392,6 +400,7 @@ def main(): module.fail_json(msg='bigsuds does not support verifying certificates with python < 2.7.9. Either update python or set validate_certs=False on the task') server = module.params['server'] + server_port = module.params['server_port'] user = module.params['user'] password = module.params['password'] state = module.params['state'] @@ -449,7 +458,7 @@ def main(): module.fail_json(msg="quorum requires monitors parameter") try: - api = bigip_api(server, user, password, validate_certs) + api = bigip_api(server, user, password, validate_certs, port=server_port) result = {'changed': False} # default if state == 'absent': diff --git a/network/f5/bigip_pool_member.py b/network/f5/bigip_pool_member.py index 9f0965cbd84..24914c992f4 100644 --- a/network/f5/bigip_pool_member.py +++ b/network/f5/bigip_pool_member.py @@ -25,7 +25,9 @@ description: - "Manages F5 BIG-IP LTM pool members via iControl SOAP API" version_added: "1.4" -author: "Matt Hite (@mhite)" +author: + - Matt Hite (@mhite) + - Tim Rupp (@caphrim007) notes: - "Requires BIG-IP software version >= 11" - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" @@ -39,6 +41,12 @@ description: - BIG-IP host required: true + server_port: + description: + - BIG-IP server port + required: false + default: 443 + version_added: "2.2" user: description: - BIG-IP username @@ -343,6 +351,7 @@ def main(): module.fail_json(msg='bigsuds does not support verifying certificates with python < 2.7.9. Either update python or set validate_certs=False on the task') server = module.params['server'] + server_port = module.params['server_port'] user = module.params['user'] password = module.params['password'] state = module.params['state'] @@ -371,7 +380,7 @@ def main(): module.fail_json(msg="valid ports must be in range 0 - 65535") try: - api = bigip_api(server, user, password, validate_certs) + api = bigip_api(server, user, password, validate_certs, port=server_port) if not pool_exists(api, pool): module.fail_json(msg="pool %s does not exist" % pool) result = {'changed': False} # default diff --git a/network/f5/bigip_virtual_server.py b/network/f5/bigip_virtual_server.py index 0cee9f68a01..7aab53e4279 100644 --- a/network/f5/bigip_virtual_server.py +++ b/network/f5/bigip_virtual_server.py @@ -25,7 +25,9 @@ description: - "Manages F5 BIG-IP LTM virtual servers via iControl SOAP API" version_added: "2.1" -author: Etienne Carriere (@Etienne-Carriere) +author: + - Etienne Carriere (@Etienne-Carriere) + - Tim Rupp (@caphrim007) notes: - "Requires BIG-IP software version >= 11" - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" @@ -37,6 +39,12 @@ description: - BIG-IP host required: true + server_port: + description: + - BIG-IP server port + required: false + default: 443 + version_added: "2.2" user: description: - BIG-IP username @@ -151,7 +159,7 @@ name: myvirtualserver port: 8080 - - name: Delete pool + - name: Delete virtual server local_action: module: bigip_virtual_server server: lb.mydomain.net @@ -423,6 +431,7 @@ def main(): module.fail_json(msg='bigsuds does not support verifying certificates with python < 2.7.9. Either update python or set validate_certs=False on the task') server = module.params['server'] + server_port = module.params['server_port'] user = module.params['user'] password = module.params['password'] state = module.params['state'] @@ -443,7 +452,7 @@ def main(): module.fail_json(msg="valid ports must be in range 1 - 65535") try: - api = bigip_api(server, user, password, validate_certs) + api = bigip_api(server, user, password, validate_certs, port=server_port) result = {'changed': False} # default if state == 'absent': From 4debfcc24186b386617b31686c6c0ba56da70330 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Gr=C3=BCner?= Date: Thu, 23 Jun 2016 22:15:55 +0200 Subject: [PATCH 1727/2522] cloudflare_dns: Improve error handling (#2470) Use the new "body" field of the info dict in case of a HTTPError. --- network/cloudflare_dns.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/network/cloudflare_dns.py b/network/cloudflare_dns.py index 238e7dff98b..e98c7cd48b5 100644 --- a/network/cloudflare_dns.py +++ b/network/cloudflare_dns.py @@ -349,11 +349,17 @@ def _cf_simple_api_call(self,api_call,method='GET',payload=None): result = None try: content = resp.read() - result = json.loads(content) except AttributeError: - error_msg += "; The API response was empty" - except json.JSONDecodeError: - error_msg += "; Failed to parse API response: {0}".format(content) + if info['body']: + content = info['body'] + else: + error_msg += "; The API response was empty" + + if content: + try: + result = json.loads(content) + except json.JSONDecodeError: + error_msg += "; Failed to parse API response: {0}".format(content) # received an error status but no data with details on what failed if (info['status'] not in [200,304]) and (result is None): From 1c36665545ab3ceb7b5edb998a9b83eb8f6be133 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Fri, 24 Jun 2016 08:01:17 -0700 Subject: [PATCH 1728/2522] Add f5 modules to python 2.4 exclusion list (#2474) I am taking over much of the development of modules old and new for F5 and to meet the coding conventions for our modules, I am aiming at newer python versions. Therefore, I will be excluding python 2.4 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 92df9527f1a..7d24ae03b2b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -117,7 +117,7 @@ install: - pip install git+https://github.com/ansible/ansible.git@devel#egg=ansible - pip install git+https://github.com/sivel/ansible-testing.git#egg=ansible_testing script: - - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py|database/influxdb/influxdb.*\.py|database/mssql/mssql_db\.py|/letsencrypt\.py' . + - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py|database/influxdb/influxdb.*\.py|database/mssql/mssql_db\.py|/letsencrypt\.py|network/f5/bigip.*\.py' . - python2.6 -m compileall -fq . - python2.7 -m compileall -fq . - python3.4 -m compileall -fq . -x $(echo "$PY3_EXCLUDE_LIST"| tr ' ' '|') From 00afca99e5ea468938836696b39efebc9cffdcb4 Mon Sep 17 00:00:00 2001 From: Andrey Arapov Date: Sat, 25 Jun 2016 13:54:42 +0200 Subject: [PATCH 1729/2522] system: crypttab: fix typo (#2476) --- system/crypttab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/crypttab.py b/system/crypttab.py index 4b36200c526..e8e0b5835f4 100644 --- a/system/crypttab.py +++ b/system/crypttab.py @@ -73,7 +73,7 @@ ''' EXAMPLES = ''' -- name: Set the options explicitly a deivce which must already exist +- name: Set the options explicitly a device which must already exist crypttab: name=luks-home state=present opts=discard,cipher=aes-cbc-essiv:sha256 - name: Add the 'discard' option to any existing options for all devices From 76f4a1c5d8fe96cee26d8a6d69809bf80096530d Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Wed, 11 May 2016 00:48:36 +0900 Subject: [PATCH 1730/2522] Add lxd_container module --- cloud/lxd/lxd_container.py | 361 +++++++++++++++++++++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 cloud/lxd/lxd_container.py diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py new file mode 100644 index 00000000000..d845d2304f3 --- /dev/null +++ b/cloud/lxd/lxd_container.py @@ -0,0 +1,361 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Hiroaki Nakamura +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + + +DOCUMENTATION = """ +--- +module: lxd_container +short_description: Manage LXD Containers +version_added: 2.2.0 +description: + - Management of LXD containers +author: "Hiroaki Nakamura (@hnakamur)" +options: + name: + description: + - Name of a container. + required: true + config: + description: + - a config dictionary for creating a container. + See https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1 + - required when the container is not created yet and the state is + not absent. + required: false + state: + choices: + - started + - stopped + - restarted + - absent + - frozen + description: + - Define the state of a container. + required: false + default: started + timeout_for_addresses: + description: + - a timeout of waiting for IPv4 addresses are set to the all network + interfaces in the container after starting or restarting. + - if this values is equal to or less than 0, ansible does not + wait for IPv4 addresses. + required: false + default: 0 +requirements: + - 'lxd >= 2.0 # OS package' + - 'python >= 2.6 # OS Package' + - 'pylxd >= 2.0 # OS or PIP Package from https://github.com/lxc/pylxd' +notes: + - Containers must have a unique name. If you attempt to create a container + with a name that already existed in the users namespace the module will + simply return as "unchanged". + - If your distro does not have a package for "python-pylxd", which is a + requirement for this module, it can be installed from source at + "https://github.com/lxc/pylxd" or installed via pip using the package + name pylxd. +""" + +EXAMPLES = """ +- name: Create a started container + lxd_container: + name: cent01 + source: { type: image, alias: centos/7/amd64 } + state: started + +- name: Create a stopped container + lxd_container: + name: cent01 + source: { type: image, alias: centos/7/amd64 } + state: stopped + +- name: Restart a container + lxd_container: + name: cent01 + source: { type: image, alias: centos/7/amd64 } + state: restarted +""" + +RETURN=""" +lxd_container: + description: container information + returned: success + type: object + contains: + addresses: + description: mapping from the network device name to a list + of IPv4 addresses in the container + returned: when state is started or restarted + type: object + sample: {"eth0": ["10.155.92.191"]} + old_state: + description: the old state of the container + returned: when state is started or restarted + sample: "stopped" + logs": ["started"], + description: list of actions performed for the container + returned: success + type: list + sample: ["created", "started"] +""" + +from distutils.spawn import find_executable + +try: + from pylxd.client import Client +except ImportError: + HAS_PYLXD = False +else: + HAS_PYLXD = True + + +# LXD_ANSIBLE_STATES is a map of states that contain values of methods used +# when a particular state is evoked. +LXD_ANSIBLE_STATES = { + 'started': '_started', + 'stopped': '_stopped', + 'restarted': '_restarted', + 'absent': '_destroyed', + 'frozen': '_frozen' +} + +# ANSIBLE_LXD_STATES is a map of states of lxd containers to the Ansible +# lxc_container module state parameter value. +ANSIBLE_LXD_STATES = { + 'Running': 'started', + 'Stopped': 'stopped', + 'Frozen': 'frozen', +} + +try: + callable(all) +except NameError: + # For python <2.5 + # This definition is copied from https://docs.python.org/2/library/functions.html#all + def all(iterable): + for element in iterable: + if not element: + return False + return True + +class LxdContainerManagement(object): + def __init__(self, module): + """Management of LXC containers via Ansible. + + :param module: Processed Ansible Module. + :type module: ``object`` + """ + self.module = module + self.container_name = self.module.params['name'] + self.config = self.module.params.get('config', None) + self.state = self.module.params['state'] + self.timeout_for_addresses = self.module.params['timeout_for_addresses'] + self.addresses = None + self.client = Client() + self.logs = [] + + def _create_container(self): + config = self.config.copy() + config['name'] = self.container_name + self.client.containers.create(config, wait=True) + # NOTE: get container again for the updated state + self.container = self._get_container() + self.logs.append('created') + + def _start_container(self): + self.container.start(wait=True) + self.logs.append('started') + + def _stop_container(self): + self.container.stop(wait=True) + self.logs.append('stopped') + + def _restart_container(self): + self.container.restart(wait=True) + self.logs.append('restarted') + + def _delete_container(self): + self.container.delete(wait=True) + self.logs.append('deleted') + + def _freeze_container(self): + self.container.freeze(wait=True) + self.logs.append('freezed') + + def _unfreeze_container(self): + self.container.unfreeze(wait=True) + self.logs.append('unfreezed') + + def _get_container(self): + try: + return self.client.containers.get(self.container_name) + except NameError: + return None + + @staticmethod + def _container_to_module_state(container): + if container is None: + return "absent" + else: + return ANSIBLE_LXD_STATES[container.status] + + def _container_ipv4_addresses(self, ignore_devices=['lo']): + container = self._get_container() + network = container is not None and container.state().network or {} + network = dict((k, v) for k, v in network.iteritems() if k not in ignore_devices) or {} + addresses = dict((k, [a['address'] for a in v['addresses'] if a['family'] == 'inet']) for k, v in network.iteritems()) or {} + return addresses + + @staticmethod + def _has_all_ipv4_addresses(addresses): + return len(addresses) > 0 and all([len(v) > 0 for v in addresses.itervalues()]) + + def _get_addresses(self): + if self.timeout_for_addresses <= 0: + return + due = datetime.datetime.now() + datetime.timedelta(seconds=self.timeout_for_addresses) + while datetime.datetime.now() < due: + time.sleep(1) + addresses = self._container_ipv4_addresses() + if self._has_all_ipv4_addresses(addresses): + self.addresses = addresses + return + self._on_timeout() + + def _started(self): + """Ensure a container is started. + + If the container does not exist the container will be created. + """ + if self.container is None: + self._create_container() + self._start_container() + else: + if self.container.status == 'Frozen': + self._unfreeze_container() + if self.container.status != 'Running': + self._start_container() + self._get_addresses() + + def _stopped(self): + if self.container is None: + self._create_container() + else: + if self.container.status == 'Frozen': + self._unfreeze_container() + if self.container.status != 'Stopped': + self._stop_container() + + def _restarted(self): + if self.container is None: + self._create_container() + self._start_container() + else: + if self.container.status == 'Frozen': + self._unfreeze_container() + if self.container.status == 'Running': + self._restart_container() + else: + self._start_container() + self._get_addresses() + + def _destroyed(self): + if self.container is not None: + if self.container.status == 'Frozen': + self._unfreeze_container() + if self.container.status == 'Running': + self._stop_container() + self._delete_container() + + def _frozen(self): + if self.container is None: + self._create_container() + self._start_container() + self._freeze_container() + else: + if self.container.status != 'Frozen': + if self.container.status != 'Running': + self._start_container() + self._freeze_container() + + def _on_timeout(self): + state_changed = len(self.logs) > 0 + self.module.fail_json( + failed=True, + msg='timeout for getting addresses', + changed=state_changed, + logs=self.logs) + + def run(self): + """Run the main method.""" + + self.container = self._get_container() + self.old_state = self._container_to_module_state(self.container) + + action = getattr(self, LXD_ANSIBLE_STATES[self.state]) + action() + + state_changed = len(self.logs) > 0 + result_json = { + "changed" : state_changed, + "old_state" : self.old_state, + "logs" : self.logs + } + if self.addresses is not None: + result_json['addresses'] = self.addresses + self.module.exit_json(**result_json) + + +def main(): + """Ansible Main module.""" + + module = AnsibleModule( + argument_spec=dict( + name=dict( + type='str', + required=True + ), + config=dict( + type='dict', + ), + state=dict( + choices=LXD_ANSIBLE_STATES.keys(), + default='started' + ), + timeout_for_addresses=dict( + type='int', + default=0 + ) + ), + supports_check_mode=False, + ) + + if not HAS_PYLXD: + module.fail_json( + msg='The `pylxd` module is not importable. Check the requirements.' + ) + + lxd_manage = LxdContainerManagement(module=module) + lxd_manage.run() + + +# import module bits +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From 5e10ca5c3da192dc029d5de8e7cb10fc6b9851b1 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Wed, 11 May 2016 07:33:14 +0900 Subject: [PATCH 1731/2522] Add __init__.py to cloud/lxd --- cloud/lxd/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 cloud/lxd/__init__.py diff --git a/cloud/lxd/__init__.py b/cloud/lxd/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From f72b0288c0c7df77a31518c99bcd3651260e9c8e Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Wed, 11 May 2016 07:37:34 +0900 Subject: [PATCH 1732/2522] Fix RETURN document to be a valid yaml --- cloud/lxd/lxd_container.py | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index d845d2304f3..6aa7bdae703 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -94,25 +94,24 @@ RETURN=""" lxd_container: - description: container information - returned: success - type: object - contains: - addresses: - description: mapping from the network device name to a list - of IPv4 addresses in the container - returned: when state is started or restarted - type: object - sample: {"eth0": ["10.155.92.191"]} - old_state: - description: the old state of the container - returned: when state is started or restarted - sample: "stopped" - logs": ["started"], - description: list of actions performed for the container - returned: success - type: list - sample: ["created", "started"] + description: container information + returned: success + type: object + contains: + addresses: + description: mapping from the network device name to a list of IPv4 addresses in the container + returned: when state is started or restarted + type: object + sample: {"eth0": ["10.155.92.191"]} + old_state: + description: the old state of the container + returned: when state is started or restarted + sample: "stopped" + logs: + description: list of actions performed for the container + returned: success + type: list + sample: ["created", "started"] """ from distutils.spawn import find_executable From 3e8fa8ef829dde4f379d28751059f797751e5014 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Thu, 12 May 2016 20:50:18 +0900 Subject: [PATCH 1733/2522] Fix indent --- cloud/lxd/lxd_container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 6aa7bdae703..8754b6f6eec 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -315,7 +315,7 @@ def run(self): "changed" : state_changed, "old_state" : self.old_state, "logs" : self.logs - } + } if self.addresses is not None: result_json['addresses'] = self.addresses self.module.exit_json(**result_json) From ed47f5759879c4ef6434ff9cd219df21fc05cb25 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Thu, 12 May 2016 20:52:16 +0900 Subject: [PATCH 1734/2522] Fix English in documentation --- cloud/lxd/lxd_container.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 8754b6f6eec..f5fcb2bcb94 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -34,9 +34,9 @@ required: true config: description: - - a config dictionary for creating a container. + - A config dictionary for creating a container. See https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1 - - required when the container is not created yet and the state is + - Required when the container is not created yet and the state is not absent. required: false state: @@ -52,9 +52,9 @@ default: started timeout_for_addresses: description: - - a timeout of waiting for IPv4 addresses are set to the all network + - A timeout of waiting for IPv4 addresses are set to the all network interfaces in the container after starting or restarting. - - if this values is equal to or less than 0, ansible does not + - If this value is equal to or less than 0, Ansible does not wait for IPv4 addresses. required: false default: 0 From 95f5a3550a9ea3ce6071943812cdf1da7d3c0b3a Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Thu, 12 May 2016 20:57:16 +0900 Subject: [PATCH 1735/2522] Simplify requirements --- cloud/lxd/lxd_container.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index f5fcb2bcb94..1232f88ef96 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -59,17 +59,11 @@ required: false default: 0 requirements: - - 'lxd >= 2.0 # OS package' - - 'python >= 2.6 # OS Package' - - 'pylxd >= 2.0 # OS or PIP Package from https://github.com/lxc/pylxd' + - 'pylxd >= 2.0' notes: - Containers must have a unique name. If you attempt to create a container with a name that already existed in the users namespace the module will simply return as "unchanged". - - If your distro does not have a package for "python-pylxd", which is a - requirement for this module, it can be installed from source at - "https://github.com/lxc/pylxd" or installed via pip using the package - name pylxd. """ EXAMPLES = """ From 9a8d1253fad38402e46968176833dbbbe97bccd3 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Thu, 12 May 2016 23:17:17 +0900 Subject: [PATCH 1736/2522] Fix examples --- cloud/lxd/lxd_container.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 1232f88ef96..226e3bf253a 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -70,19 +70,19 @@ - name: Create a started container lxd_container: name: cent01 - source: { type: image, alias: centos/7/amd64 } - state: started + config: { source: { type: image, alias: centos/7/amd64 } } + state: restarted - name: Create a stopped container lxd_container: name: cent01 - source: { type: image, alias: centos/7/amd64 } + config: { source: { type: image, alias: centos/7/amd64 } } state: stopped - name: Restart a container lxd_container: name: cent01 - source: { type: image, alias: centos/7/amd64 } + config: { source: { type: image, alias: centos/7/amd64 } } state: restarted """ From 53f482308d639eb412fa136bc3adbb568358311f Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Thu, 12 May 2016 23:27:53 +0900 Subject: [PATCH 1737/2522] Present clearer messages for connection error --- cloud/lxd/lxd_container.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 226e3bf253a..64671cf9ee8 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -117,6 +117,7 @@ else: HAS_PYLXD = True +from requests.exceptions import ConnectionError # LXD_ANSIBLE_STATES is a map of states that contain values of methods used # when a particular state is evoked. @@ -200,6 +201,8 @@ def _get_container(self): return self.client.containers.get(self.container_name) except NameError: return None + except ConnectionError: + self.module.fail_json(msg="Cannot connect to lxd server") @staticmethod def _container_to_module_state(container): From 5da4699f83d9e2a2333a66f63e190aa60a66d2b1 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Tue, 17 May 2016 04:12:58 +0900 Subject: [PATCH 1738/2522] Document about copying files and using an remote image --- cloud/lxd/lxd_container.py | 83 ++++++++++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 17 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 64671cf9ee8..2fd2683758a 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -64,26 +64,75 @@ - Containers must have a unique name. If you attempt to create a container with a name that already existed in the users namespace the module will simply return as "unchanged". + - There are two ways to can run commands in containers. + - Use the command module, for example: + - name: Install python in the created container + command: lxc exec my-ubuntu -- apt install -y python + - Use the ansible lxd connection plugin bundled in Ansible 2.1 or later. + - In order to use this method, first you need to install python in the container + with the above method. See the first example below. + - You can copy a file in the localhost to the created container + running `command: lxc file push filename container_name/dir/filename` + on localhost. See the first example below. + - You can copy a file in the creatd container to the localhost + running `command: lxc file pull container_name/dir/filename filename`. + See the first example below. """ EXAMPLES = """ -- name: Create a started container - lxd_container: - name: cent01 - config: { source: { type: image, alias: centos/7/amd64 } } - state: restarted - -- name: Create a stopped container - lxd_container: - name: cent01 - config: { source: { type: image, alias: centos/7/amd64 } } - state: stopped - -- name: Restart a container - lxd_container: - name: cent01 - config: { source: { type: image, alias: centos/7/amd64 } } - state: restarted +- hosts: localhost + connection: local + tasks: + - name: Create a started container + lxd_container: + name: my-ubuntu + state: started + config: + source: + type: image + mode: pull + server: https://images.linuxcontainers.org + protocol: lxd + alias: "ubuntu/xenial/amd64" + profiles: ["default"] + - name: Install python in the created container "nettest" + command: lxc exec my-ubuntu -- apt install -y python + - name: Copy somefile.txt to /tmp/renamed.txt in the created container "nettest" + command: lxc file push somefile.txt nettest/tmp/renamed.txt + - name: Copy /etc/hosts in the created container "nettest" to localhost with name "nettest-hosts" + command: lxc file pull nettest/etc/hosts nettest-hosts + +- hosts: localhost + connection: local + tasks: + - name: Create a stopped container + lxd_container: + name: my-ubuntu + state: stopped + config: + source: + type: image + mode: pull + server: https://images.linuxcontainers.org + protocol: lxd + alias: "ubuntu/xenial/amd64" + profiles: ["default"] + +- hosts: localhost + connection: local + tasks: + - name: Restart a container + lxd_container: + name: my-ubuntu + state: restarted + config: + source: + type: image + mode: pull + server: https://images.linuxcontainers.org + protocol: lxd + alias: "ubuntu/xenial/amd64" + profiles: ["default"] """ RETURN=""" From d2fa7c75da9bb592b49de8964b11b504e338ef56 Mon Sep 17 00:00:00 2001 From: jpic Date: Tue, 17 May 2016 16:08:16 +0200 Subject: [PATCH 1739/2522] Remove example from notes, syntax fix --- cloud/lxd/lxd_container.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 2fd2683758a..3a74a254ce0 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -64,18 +64,15 @@ - Containers must have a unique name. If you attempt to create a container with a name that already existed in the users namespace the module will simply return as "unchanged". - - There are two ways to can run commands in containers. - - Use the command module, for example: - - name: Install python in the created container - command: lxc exec my-ubuntu -- apt install -y python - - Use the ansible lxd connection plugin bundled in Ansible 2.1 or later. - - In order to use this method, first you need to install python in the container - with the above method. See the first example below. - - You can copy a file in the localhost to the created container - running `command: lxc file push filename container_name/dir/filename` + - There are two ways to can run commands in containers, using the command + module or using the ansible lxd connection plugin bundled in Ansible >= + 2.1, the later requires python to be installed in the container which can + be done with the command module. + - You can copy a file from the host to the container + with `command=lxc file push filename container_name/dir/filename` on localhost. See the first example below. - You can copy a file in the creatd container to the localhost - running `command: lxc file pull container_name/dir/filename filename`. + with `command=lxc file pull container_name/dir/filename filename`. See the first example below. """ From 05700edc0b5818ada9cb43bb12165c4090555417 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Sat, 21 May 2016 00:04:19 +0900 Subject: [PATCH 1740/2522] Update document about copying files to containers --- cloud/lxd/lxd_container.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 3a74a254ce0..126d84a185f 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -69,8 +69,8 @@ 2.1, the later requires python to be installed in the container which can be done with the command module. - You can copy a file from the host to the container - with `command=lxc file push filename container_name/dir/filename` - on localhost. See the first example below. + with the Ansible `copy` and `template` module and the `lxd` connection plugin. + See the example below. - You can copy a file in the creatd container to the localhost with `command=lxc file pull container_name/dir/filename filename`. See the first example below. @@ -82,7 +82,7 @@ tasks: - name: Create a started container lxd_container: - name: my-ubuntu + name: mycontainer state: started config: source: @@ -92,19 +92,28 @@ protocol: lxd alias: "ubuntu/xenial/amd64" profiles: ["default"] - - name: Install python in the created container "nettest" - command: lxc exec my-ubuntu -- apt install -y python - - name: Copy somefile.txt to /tmp/renamed.txt in the created container "nettest" - command: lxc file push somefile.txt nettest/tmp/renamed.txt - - name: Copy /etc/hosts in the created container "nettest" to localhost with name "nettest-hosts" - command: lxc file pull nettest/etc/hosts nettest-hosts + - name: Install python in the created container "mycontainer" + command: lxc exec mycontainer -- apt install -y python + - name: Copy /etc/hosts in the created container "mycontainer" to localhost with name "mycontainer-hosts" + command: lxc file pull mycontainer/etc/hosts mycontainer-hosts + + +# Note your container must be in the inventory for the below example. +# +# [containers] +# mycontainer ansible_connection=lxd +# +- hosts: + - mycontainer + tasks: + - template: src=foo.j2 dest=/etc/bar - hosts: localhost connection: local tasks: - name: Create a stopped container lxd_container: - name: my-ubuntu + name: mycontainer state: stopped config: source: @@ -120,7 +129,7 @@ tasks: - name: Restart a container lxd_container: - name: my-ubuntu + name: mycontainer state: restarted config: source: From f786a3e1134b410644b5fd3bc6032e89e2a83bbb Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Sat, 4 Jun 2016 14:11:15 +0900 Subject: [PATCH 1741/2522] Remove dependency to pylxd and use requests_unixsocket directly --- cloud/lxd/lxd_container.py | 248 ++++++++++++++++++++++--------------- 1 file changed, 146 insertions(+), 102 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 126d84a185f..6541d5b4a84 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -50,16 +50,34 @@ - Define the state of a container. required: false default: started - timeout_for_addresses: + timeout: description: - - A timeout of waiting for IPv4 addresses are set to the all network - interfaces in the container after starting or restarting. - - If this value is equal to or less than 0, Ansible does not - wait for IPv4 addresses. + - A timeout of one LXC REST API call. + - This is also used as a timeout for waiting until IPv4 addresses + are set to the all network interfaces in the container after + starting or restarting. required: false - default: 0 + default: 30 + wait_for_ipv4_addresses: + description: + - If this is true, the lxd_module waits until IPv4 addresses + are set to the all network interfaces in the container after + starting or restarting. + required: false + default: false + force_stop: + description: + - If this is true, the lxd_module forces to stop the container + when it stops or restarts the container. + required: false + default: false + unix_socket_path: + description: + - The unix domain socket path for the LXD server. + required: false + default: /var/lib/lxd/unix.socket requirements: - - 'pylxd >= 2.0' + - 'requests_unixsocket' notes: - Containers must have a unique name. If you attempt to create a container with a name that already existed in the users namespace the module will @@ -156,23 +174,22 @@ description: the old state of the container returned: when state is started or restarted sample: "stopped" - logs: + actions: description: list of actions performed for the container returned: success type: list - sample: ["created", "started"] + sample: ["create", "start"] """ -from distutils.spawn import find_executable +from ansible.compat.six.moves.urllib.parse import quote try: - from pylxd.client import Client + import requests_unixsocket + from requests.exceptions import ConnectionError except ImportError: - HAS_PYLXD = False + HAS_REQUETS_UNIXSOCKET = False else: - HAS_PYLXD = True - -from requests.exceptions import ConnectionError + HAS_REQUETS_UNIXSOCKET = True # LXD_ANSIBLE_STATES is a map of states that contain values of methods used # when a particular state is evoked. @@ -214,61 +231,91 @@ def __init__(self, module): self.container_name = self.module.params['name'] self.config = self.module.params.get('config', None) self.state = self.module.params['state'] - self.timeout_for_addresses = self.module.params['timeout_for_addresses'] + self.timeout = self.module.params['timeout'] + self.wait_for_ipv4_addresses = self.module.params['wait_for_ipv4_addresses'] + self.force_stop = self.module.params['force_stop'] self.addresses = None - self.client = Client() - self.logs = [] + self.unix_socket_path = self.module.params['unix_socket_path'] + self.base_url = 'http+unix://{0}'.format(quote(self.unix_socket_path, safe='')) + self.session = requests_unixsocket.Session() + self.actions = [] + + def _send_request(self, method, path, json=None, ok_error_codes=None): + try: + url = self.base_url + path + resp = self.session.request(method, url, json=json, timeout=self.timeout) + resp_json = resp.json() + resp_type = resp_json.get('type', None) + if resp_type == 'error': + if ok_error_codes is not None and resp_json['error_code'] in ok_error_codes: + return resp_json + self.module.fail_json( + msg='error response', + request={'method': method, 'url': url, 'json': json, 'timeout': self.timeout}, + response={'json': resp_json} + ) + return resp_json + except ConnectionError: + self.module.fail_json(msg='cannot connect to the LXD server', unix_socket_path=self.unix_socket_path) + + def _operate_and_wait(self, method, path, json=None): + resp_json = self._send_request(method, path, json=json) + if resp_json['type'] == 'async': + path = '{0}/wait?timeout={1}'.format(resp_json['operation'], self.timeout) + resp_json = self._send_request('GET', path) + return resp_json + + def _get_container_state_json(self): + return self._send_request( + 'GET', '/1.0/containers/{0}/state'.format(self.container_name), + ok_error_codes=[404] + ) + + @staticmethod + def _container_state_json_to_module_state(resp_json): + if resp_json['type'] == 'error': + return 'absent' + return ANSIBLE_LXD_STATES[resp_json['metadata']['status']] + + def _change_state(self, action, force_stop=False): + json={'action': action, 'timeout': self.timeout} + if force_stop: + json['force'] = True + return self._operate_and_wait('PUT', '/1.0/containers/{0}/state'.format(self.container_name), json) def _create_container(self): config = self.config.copy() config['name'] = self.container_name - self.client.containers.create(config, wait=True) - # NOTE: get container again for the updated state - self.container = self._get_container() - self.logs.append('created') + self._operate_and_wait('POST', '/1.0/containers', config) + self.actions.append('creat') def _start_container(self): - self.container.start(wait=True) - self.logs.append('started') + self._change_state('start') + self.actions.append('start') def _stop_container(self): - self.container.stop(wait=True) - self.logs.append('stopped') + self._change_state('stop', self.force_stop) + self.actions.append('stop') def _restart_container(self): - self.container.restart(wait=True) - self.logs.append('restarted') + self._change_state('restart', self.force_stop) + self.actions.append('restart') def _delete_container(self): - self.container.delete(wait=True) - self.logs.append('deleted') + return self._operate_and_wait('DELETE', '/1.0/containers/{0}'.format(self.container_name)) + self.actions.append('delete') def _freeze_container(self): - self.container.freeze(wait=True) - self.logs.append('freezed') + self._change_state('freeze') + self.actions.append('freeze') def _unfreeze_container(self): - self.container.unfreeze(wait=True) - self.logs.append('unfreezed') - - def _get_container(self): - try: - return self.client.containers.get(self.container_name) - except NameError: - return None - except ConnectionError: - self.module.fail_json(msg="Cannot connect to lxd server") - - @staticmethod - def _container_to_module_state(container): - if container is None: - return "absent" - else: - return ANSIBLE_LXD_STATES[container.status] + self._change_state('unfreeze') + self.actions.append('unfreez') def _container_ipv4_addresses(self, ignore_devices=['lo']): - container = self._get_container() - network = container is not None and container.state().network or {} + resp_json = self._get_container_state_json() + network = resp_json['metadata']['network'] or {} network = dict((k, v) for k, v in network.iteritems() if k not in ignore_devices) or {} addresses = dict((k, [a['address'] for a in v['addresses'] if a['family'] == 'inet']) for k, v in network.iteritems()) or {} return addresses @@ -278,95 +325,80 @@ def _has_all_ipv4_addresses(addresses): return len(addresses) > 0 and all([len(v) > 0 for v in addresses.itervalues()]) def _get_addresses(self): - if self.timeout_for_addresses <= 0: - return - due = datetime.datetime.now() + datetime.timedelta(seconds=self.timeout_for_addresses) + due = datetime.datetime.now() + datetime.timedelta(seconds=self.timeout) while datetime.datetime.now() < due: time.sleep(1) addresses = self._container_ipv4_addresses() if self._has_all_ipv4_addresses(addresses): self.addresses = addresses return - self._on_timeout() - def _started(self): - """Ensure a container is started. + state_changed = len(self.actions) > 0 + self.module.fail_json( + failed=True, + msg='timeout for getting IPv4 addresses', + changed=state_changed, + logs=self.actions) - If the container does not exist the container will be created. - """ - if self.container is None: + def _started(self): + if self.old_state == 'absent': self._create_container() self._start_container() else: - if self.container.status == 'Frozen': + if self.old_state == 'frozen': self._unfreeze_container() - if self.container.status != 'Running': - self._start_container() - self._get_addresses() + if self.wait_for_ipv4_addresses: + self._get_addresses() def _stopped(self): - if self.container is None: + if self.old_state == 'absent': self._create_container() else: - if self.container.status == 'Frozen': + if self.old_state == 'frozen': self._unfreeze_container() - if self.container.status != 'Stopped': - self._stop_container() + self._stop_container() def _restarted(self): - if self.container is None: + if self.old_state == 'absent': self._create_container() self._start_container() else: - if self.container.status == 'Frozen': + if self.old_state == 'frozen': self._unfreeze_container() - if self.container.status == 'Running': - self._restart_container() - else: - self._start_container() - self._get_addresses() + self._restart_container() + if self.wait_for_ipv4_addresses: + self._get_addresses() def _destroyed(self): - if self.container is not None: - if self.container.status == 'Frozen': + if self.old_state != 'absent': + if self.old_state == 'frozen': self._unfreeze_container() - if self.container.status == 'Running': - self._stop_container() + self._stop_container() self._delete_container() def _frozen(self): - if self.container is None: + if self.old_state == 'absent': self._create_container() self._start_container() self._freeze_container() else: - if self.container.status != 'Frozen': - if self.container.status != 'Running': - self._start_container() - self._freeze_container() - - def _on_timeout(self): - state_changed = len(self.logs) > 0 - self.module.fail_json( - failed=True, - msg='timeout for getting addresses', - changed=state_changed, - logs=self.logs) + if self.old_state == 'stopped': + self._start_container() + self._freeze_container() def run(self): """Run the main method.""" - self.container = self._get_container() - self.old_state = self._container_to_module_state(self.container) + self.old_state = self._container_state_json_to_module_state(self._get_container_state_json()) action = getattr(self, LXD_ANSIBLE_STATES[self.state]) action() - state_changed = len(self.logs) > 0 + state_changed = len(self.actions) > 0 result_json = { - "changed" : state_changed, - "old_state" : self.old_state, - "logs" : self.logs + 'changed': state_changed, + 'old_state': self.old_state, + 'actions': self.actions } if self.addresses is not None: result_json['addresses'] = self.addresses @@ -389,17 +421,29 @@ def main(): choices=LXD_ANSIBLE_STATES.keys(), default='started' ), - timeout_for_addresses=dict( + timeout=dict( type='int', - default=0 + default=30 + ), + wait_for_ipv4_addresses=dict( + type='bool', + default=True + ), + force_stop=dict( + type='bool', + default=False + ), + unix_socket_path=dict( + type='str', + default='/var/lib/lxd/unix.socket' ) ), supports_check_mode=False, ) - if not HAS_PYLXD: + if not HAS_REQUETS_UNIXSOCKET: module.fail_json( - msg='The `pylxd` module is not importable. Check the requirements.' + msg='The `requests_unixsocket` module is not importable. Check the requirements.' ) lxd_manage = LxdContainerManagement(module=module) From f78e6f49e8251f0135ddf3ae4236b8b0932a60bd Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Sat, 4 Jun 2016 16:43:21 +0900 Subject: [PATCH 1742/2522] Apply config to the existing container --- cloud/lxd/lxd_container.py | 118 +++++++++++++++++++++++++++++++++---- 1 file changed, 107 insertions(+), 11 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 6541d5b4a84..65a0ff6a373 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -38,6 +38,19 @@ See https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1 - Required when the container is not created yet and the state is not absent. + - If the container already exists and its metadata obtained from + GET /1.0/containers/ + https://github.com/lxc/lxd/blob/master/doc/rest-api.md#10containersname + are different, they this module tries to apply the configurations. + The following keys in config will be compared and applied. + - architecture + - config + - The key starts with 'volatile.' are ignored for comparison. + - devices + - ephemeral + - profiles + - Not all config values are supported to apply the existing container. + Maybe you need to delete and recreate a container. required: false state: choices: @@ -238,13 +251,22 @@ def __init__(self, module): self.unix_socket_path = self.module.params['unix_socket_path'] self.base_url = 'http+unix://{0}'.format(quote(self.unix_socket_path, safe='')) self.session = requests_unixsocket.Session() + self.logs = [] self.actions = [] + def _path_to_url(self, path): + return self.base_url + path + def _send_request(self, method, path, json=None, ok_error_codes=None): try: - url = self.base_url + path + url = self._path_to_url(path) resp = self.session.request(method, url, json=json, timeout=self.timeout) resp_json = resp.json() + self.logs.append({ + 'type': 'sent request', + 'request': {'method': method, 'url': url, 'json': json, 'timeout': self.timeout}, + 'response': {'json': resp_json} + }) resp_type = resp_json.get('type', None) if resp_type == 'error': if ok_error_codes is not None and resp_json['error_code'] in ok_error_codes: @@ -252,7 +274,8 @@ def _send_request(self, method, path, json=None, ok_error_codes=None): self.module.fail_json( msg='error response', request={'method': method, 'url': url, 'json': json, 'timeout': self.timeout}, - response={'json': resp_json} + response={'json': resp_json}, + logs=self.logs ) return resp_json except ConnectionError: @@ -263,8 +286,22 @@ def _operate_and_wait(self, method, path, json=None): if resp_json['type'] == 'async': path = '{0}/wait?timeout={1}'.format(resp_json['operation'], self.timeout) resp_json = self._send_request('GET', path) + if resp_json['metadata']['status'] != 'Success': + url = self._path_to_url(path) + self.module.fail_json( + msg='error response for waiting opearation', + request={'method': method, 'url': url, 'json': json, 'timeout': self.timeout}, + response={'json': resp_json}, + logs=self.logs + ) return resp_json + def _get_container_json(self): + return self._send_request( + 'GET', '/1.0/containers/{0}'.format(self.container_name), + ok_error_codes=[404] + ) + def _get_container_state_json(self): return self._send_request( 'GET', '/1.0/containers/{0}/state'.format(self.container_name), @@ -272,7 +309,7 @@ def _get_container_state_json(self): ) @staticmethod - def _container_state_json_to_module_state(resp_json): + def _container_json_to_module_state(resp_json): if resp_json['type'] == 'error': return 'absent' return ANSIBLE_LXD_STATES[resp_json['metadata']['status']] @@ -316,8 +353,8 @@ def _unfreeze_container(self): def _container_ipv4_addresses(self, ignore_devices=['lo']): resp_json = self._get_container_state_json() network = resp_json['metadata']['network'] or {} - network = dict((k, v) for k, v in network.iteritems() if k not in ignore_devices) or {} - addresses = dict((k, [a['address'] for a in v['addresses'] if a['family'] == 'inet']) for k, v in network.iteritems()) or {} + network = dict((k, v) for k, v in network.items() if k not in ignore_devices) or {} + addresses = dict((k, [a['address'] for a in v['addresses'] if a['family'] == 'inet']) for k, v in network.items()) or {} return addresses @staticmethod @@ -338,7 +375,8 @@ def _get_addresses(self): failed=True, msg='timeout for getting IPv4 addresses', changed=state_changed, - logs=self.actions) + actions=self.actions, + logs=self.logs) def _started(self): if self.old_state == 'absent': @@ -347,6 +385,10 @@ def _started(self): else: if self.old_state == 'frozen': self._unfreeze_container() + elif self.old_state == 'stopped': + self._start_container() + if self._needs_to_apply_configs(): + self._apply_configs() if self.wait_for_ipv4_addresses: self._get_addresses() @@ -354,9 +396,17 @@ def _stopped(self): if self.old_state == 'absent': self._create_container() else: - if self.old_state == 'frozen': - self._unfreeze_container() - self._stop_container() + if self.old_state == 'stopped': + if self._needs_to_apply_configs(): + self._start_container() + self._apply_configs() + self._stop_container() + else: + if self.old_state == 'frozen': + self._unfreeze_container() + if self._needs_to_apply_configs(): + self._apply_configs() + self._stop_container() def _restarted(self): if self.old_state == 'absent': @@ -365,6 +415,8 @@ def _restarted(self): else: if self.old_state == 'frozen': self._unfreeze_container() + if self._needs_to_apply_configs(): + self._apply_configs() self._restart_container() if self.wait_for_ipv4_addresses: self._get_addresses() @@ -384,12 +436,55 @@ def _frozen(self): else: if self.old_state == 'stopped': self._start_container() + if self._needs_to_apply_configs(): + self._apply_configs() self._freeze_container() + def _needs_to_change_config(self, key): + if key not in self.config: + return False + if key == 'config': + old_configs = dict((k, v) for k, v in self.old_container_json['metadata'][key].items() if k.startswith('volatile.')) + else: + old_configs = self.old_container_json['metadata'][key] + return self.config[key] != old_configs + + def _needs_to_apply_configs(self): + return ( + self._needs_to_change_config('architecture') or + self._needs_to_change_config('config') or + self._needs_to_change_config('ephemeral') or + self._needs_to_change_config('devices') or + self._needs_to_change_config('profiles') + ) + + def _apply_configs(self): + old_metadata = self.old_container_json['metadata'] + json = { + 'architecture': old_metadata['architecture'], + 'config': old_metadata['config'], + 'devices': old_metadata['devices'], + 'profiles': old_metadata['profiles'] + } + if self._needs_to_change_config('architecture'): + json['architecture'] = self.config['architecture'] + if self._needs_to_change_config('config'): + for k, v in self.config['config'].items(): + json['config'][k] = v + if self._needs_to_change_config('ephemeral'): + json['ephemeral'] = self.config['ephemeral'] + if self._needs_to_change_config('devices'): + json['devices'] = self.config['devices'] + if self._needs_to_change_config('profiles'): + json['profiles'] = self.config['profiles'] + self._operate_and_wait('PUT', '/1.0/containers/{0}'.format(self.container_name), json) + self.actions.append('apply_configs') + def run(self): """Run the main method.""" - self.old_state = self._container_state_json_to_module_state(self._get_container_state_json()) + self.old_container_json = self._get_container_json() + self.old_state = self._container_json_to_module_state(self.old_container_json) action = getattr(self, LXD_ANSIBLE_STATES[self.state]) action() @@ -398,6 +493,7 @@ def run(self): result_json = { 'changed': state_changed, 'old_state': self.old_state, + 'logs': self.logs, 'actions': self.actions } if self.addresses is not None: @@ -427,7 +523,7 @@ def main(): ), wait_for_ipv4_addresses=dict( type='bool', - default=True + default=False ), force_stop=dict( type='bool', From cf6e3b9ea89e0e35ec8f9906ad0397af6d37de62 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Sat, 25 Jun 2016 18:04:29 +0900 Subject: [PATCH 1743/2522] Fix bug in comparing config --- cloud/lxd/lxd_container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 65a0ff6a373..294fdefa22c 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -444,7 +444,7 @@ def _needs_to_change_config(self, key): if key not in self.config: return False if key == 'config': - old_configs = dict((k, v) for k, v in self.old_container_json['metadata'][key].items() if k.startswith('volatile.')) + old_configs = dict((k, v) for k, v in self.old_container_json['metadata'][key].items() if not k.startswith('volatile.')) else: old_configs = self.old_container_json['metadata'][key] return self.config[key] != old_configs From 82302e80d4ac3b7b8d62e91729cd10e9f54fbac9 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Sat, 25 Jun 2016 18:08:13 +0900 Subject: [PATCH 1744/2522] Add documentation about logs in returned object --- cloud/lxd/lxd_container.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 294fdefa22c..e8bdd68c028 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -187,6 +187,10 @@ description: the old state of the container returned: when state is started or restarted sample: "stopped" + logs: + descriptions: the logs of requests and responses + returned: when requests are sent + sample: [{"request": {"json": null, "method": "GET", "timeout": 30, "url": "http+unix://%2Fvar%2Flib%2Flxd%2Funix.socket/1.0/containers/myubuntu"}, "response": {"json": {"metadata": {"architecture": "x86_64", "config": {"limits.cpu": "2", "volatile.base_image": "ed0fb49ea8c3698c96c14157dff05b8c55eab2db438d3b043af1037836f1fa2b", "volatile.eth0.hwaddr": "00:16:3e:f6:fa:b0", "volatile.last_state.idmap": "[{\"Isuid\":true,\"Isgid\":false,\"Hostid\":100000,\"Nsid\":0,\"Maprange\":65536},{\"Isuid\":false,\"Isgid\":true,\"Hostid\":100000,\"Nsid\":0,\"Maprange\":65536}]"}, "created_at": "2016-06-25T18:03:37+09:00", "devices": {"root": {"path": "/", "type": "disk"}}, "ephemeral": false, "expanded_config": {"limits.cpu": "2", "volatile.base_image": "ed0fb49ea8c3698c96c14157dff05b8c55eab2db438d3b043af1037836f1fa2b", "volatile.eth0.hwaddr": "00:16:3e:f6:fa:b0", "volatile.last_state.idmap": "[{\"Isuid\":true,\"Isgid\":false,\"Hostid\":100000,\"Nsid\":0,\"Maprange\":65536},{\"Isuid\":false,\"Isgid\":true,\"Hostid\":100000,\"Nsid\":0,\"Maprange\":65536}]"}, "expanded_devices": {"eth0": {"name": "eth0", "nictype": "bridged", "parent": "lxdbr0", "type": "nic"}, "root": {"path": "/", "type": "disk"}}, "name": "myubuntu", "profiles": ["default"], "stateful": false, "status": "Running", "status_code": 103}, "status": "Success", "status_code": 200, "type": "sync"}}, "type": "sent request"}, {"request": {"json": {"architecture": "x86_64", "config": {"limits.cpu": "2", "volatile.base_image": "ed0fb49ea8c3698c96c14157dff05b8c55eab2db438d3b043af1037836f1fa2b", "volatile.eth0.hwaddr": "00:16:3e:f6:fa:b0", "volatile.last_state.idmap": "[{\"Isuid\":true,\"Isgid\":false,\"Hostid\":100000,\"Nsid\":0,\"Maprange\":65536},{\"Isuid\":false,\"Isgid\":true,\"Hostid\":100000,\"Nsid\":0,\"Maprange\":65536}]"}, "devices": {"root": {"path": "/", "type": "disk"}}, "profiles": ["default"]}, "method": "PUT", "timeout": 30, "url": "http+unix://%2Fvar%2Flib%2Flxd%2Funix.socket/1.0/containers/myubuntu"}, "response": {"json": {"metadata": {"class": "task", "created_at": "2016-06-25T18:03:54.082548079+09:00", "err": "", "id": "e0f6faf3-0cbb-4bd2-9a51-8a3d034d839d", "may_cancel": false, "metadata": null, "resources": {"containers": ["/1.0/containers/myubuntu"]}, "status": "Running", "status_code": 103, "updated_at": "2016-06-25T18:03:54.082548079+09:00"}, "operation": "/1.0/operations/e0f6faf3-0cbb-4bd2-9a51-8a3d034d839d", "status": "Operation created", "status_code": 100, "type": "async"}}, "type": "sent request"}, {"request": {"json": null, "method": "GET", "timeout": 30, "url": "http+unix://%2Fvar%2Flib%2Flxd%2Funix.socket/1.0/operations/e0f6faf3-0cbb-4bd2-9a51-8a3d034d839d/wait?timeout=30"}, "response": {"json": {"metadata": {"class": "task", "created_at": "2016-06-25T18:03:54.082548079+09:00", "err": "", "id": "e0f6faf3-0cbb-4bd2-9a51-8a3d034d839d", "may_cancel": false, "metadata": null, "resources": {"containers": ["/1.0/containers/myubuntu"]}, "status": "Success", "status_code": 200, "updated_at": "2016-06-25T18:03:54.082548079+09:00"}, "status": "Success", "status_code": 200, "type": "sync"}}, "type": "sent request"}] actions: description: list of actions performed for the container returned: success From 2ee5c0433541377272fa823908b066086e72d8b9 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Sun, 26 Jun 2016 08:30:38 +0900 Subject: [PATCH 1745/2522] Use httplib instead of requests_unixsocket --- cloud/lxd/lxd_container.py | 94 +++++++++++++++++++++++--------------- 1 file changed, 56 insertions(+), 38 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index e8bdd68c028..a55f6937481 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -89,8 +89,6 @@ - The unix domain socket path for the LXD server. required: false default: /var/lib/lxd/unix.socket -requirements: - - 'requests_unixsocket' notes: - Containers must have a unique name. If you attempt to create a container with a name that already existed in the users namespace the module will @@ -198,15 +196,43 @@ sample: ["create", "start"] """ -from ansible.compat.six.moves.urllib.parse import quote +try: + import json +except ImportError: + import simplejson as json +# httplib/http.client connection using unix domain socket +import socket try: - import requests_unixsocket - from requests.exceptions import ConnectionError + import httplib + + class UnixHTTPConnection(httplib.HTTPConnection): + def __init__(self, path, host='localhost', port=None, strict=None, + timeout=None): + httplib.HTTPConnection.__init__(self, host, port=port, strict=strict, + timeout=timeout) + self.path = path + + def connect(self): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(self.path) + self.sock = sock except ImportError: - HAS_REQUETS_UNIXSOCKET = False -else: - HAS_REQUETS_UNIXSOCKET = True + # Python 3 + import http.client + + class UnixHTTPConnection(http.client.HTTPConnection): + def __init__(self, path, host='localhost', port=None, + timeout=None): + http.client.HTTPConnection.__init__(self, host, port=port, + timeout=timeout) + self.path = path + + def connect(self): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(self.path) + self.sock = sock + # LXD_ANSIBLE_STATES is a map of states that contain values of methods used # when a particular state is evoked. @@ -253,22 +279,19 @@ def __init__(self, module): self.force_stop = self.module.params['force_stop'] self.addresses = None self.unix_socket_path = self.module.params['unix_socket_path'] - self.base_url = 'http+unix://{0}'.format(quote(self.unix_socket_path, safe='')) - self.session = requests_unixsocket.Session() + self.connection = UnixHTTPConnection(self.unix_socket_path, timeout=self.timeout) self.logs = [] self.actions = [] - def _path_to_url(self, path): - return self.base_url + path - - def _send_request(self, method, path, json=None, ok_error_codes=None): + def _send_request(self, method, url, body_json=None, ok_error_codes=None): try: - url = self._path_to_url(path) - resp = self.session.request(method, url, json=json, timeout=self.timeout) - resp_json = resp.json() + body = json.dumps(body_json) + self.connection.request(method, url, body=body) + resp = self.connection.getresponse() + resp_json = json.loads(resp.read()) self.logs.append({ 'type': 'sent request', - 'request': {'method': method, 'url': url, 'json': json, 'timeout': self.timeout}, + 'request': {'method': method, 'url': url, 'json': body_json, 'timeout': self.timeout}, 'response': {'json': resp_json} }) resp_type = resp_json.get('type', None) @@ -277,16 +300,16 @@ def _send_request(self, method, path, json=None, ok_error_codes=None): return resp_json self.module.fail_json( msg='error response', - request={'method': method, 'url': url, 'json': json, 'timeout': self.timeout}, + request={'method': method, 'url': url, 'json': body_json, 'timeout': self.timeout}, response={'json': resp_json}, logs=self.logs ) return resp_json - except ConnectionError: + except socket.error: self.module.fail_json(msg='cannot connect to the LXD server', unix_socket_path=self.unix_socket_path) - def _operate_and_wait(self, method, path, json=None): - resp_json = self._send_request(method, path, json=json) + def _operate_and_wait(self, method, path, body_json=None): + resp_json = self._send_request(method, path, body_json=body_json) if resp_json['type'] == 'async': path = '{0}/wait?timeout={1}'.format(resp_json['operation'], self.timeout) resp_json = self._send_request('GET', path) @@ -294,7 +317,7 @@ def _operate_and_wait(self, method, path, json=None): url = self._path_to_url(path) self.module.fail_json( msg='error response for waiting opearation', - request={'method': method, 'url': url, 'json': json, 'timeout': self.timeout}, + request={'method': method, 'url': url, 'json': body_json, 'timeout': self.timeout}, response={'json': resp_json}, logs=self.logs ) @@ -319,10 +342,10 @@ def _container_json_to_module_state(resp_json): return ANSIBLE_LXD_STATES[resp_json['metadata']['status']] def _change_state(self, action, force_stop=False): - json={'action': action, 'timeout': self.timeout} + body_json={'action': action, 'timeout': self.timeout} if force_stop: - json['force'] = True - return self._operate_and_wait('PUT', '/1.0/containers/{0}/state'.format(self.container_name), json) + body_json['force'] = True + return self._operate_and_wait('PUT', '/1.0/containers/{0}/state'.format(self.container_name), body_json=body_json) def _create_container(self): config = self.config.copy() @@ -464,24 +487,24 @@ def _needs_to_apply_configs(self): def _apply_configs(self): old_metadata = self.old_container_json['metadata'] - json = { + body_json = { 'architecture': old_metadata['architecture'], 'config': old_metadata['config'], 'devices': old_metadata['devices'], 'profiles': old_metadata['profiles'] } if self._needs_to_change_config('architecture'): - json['architecture'] = self.config['architecture'] + body_json['architecture'] = self.config['architecture'] if self._needs_to_change_config('config'): for k, v in self.config['config'].items(): - json['config'][k] = v + body_json['config'][k] = v if self._needs_to_change_config('ephemeral'): - json['ephemeral'] = self.config['ephemeral'] + body_json['ephemeral'] = self.config['ephemeral'] if self._needs_to_change_config('devices'): - json['devices'] = self.config['devices'] + body_json['devices'] = self.config['devices'] if self._needs_to_change_config('profiles'): - json['profiles'] = self.config['profiles'] - self._operate_and_wait('PUT', '/1.0/containers/{0}'.format(self.container_name), json) + body_json['profiles'] = self.config['profiles'] + self._operate_and_wait('PUT', '/1.0/containers/{0}'.format(self.container_name), body_json=body_json) self.actions.append('apply_configs') def run(self): @@ -541,11 +564,6 @@ def main(): supports_check_mode=False, ) - if not HAS_REQUETS_UNIXSOCKET: - module.fail_json( - msg='The `requests_unixsocket` module is not importable. Check the requirements.' - ) - lxd_manage = LxdContainerManagement(module=module) lxd_manage.run() From 6d0a3d72271fd1104174a8de17d15cb0dbdec91f Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Sun, 26 Jun 2016 09:03:35 +0900 Subject: [PATCH 1746/2522] Remove sample of logs in returned object --- cloud/lxd/lxd_container.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index a55f6937481..2ed21c9c4dc 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -188,7 +188,6 @@ logs: descriptions: the logs of requests and responses returned: when requests are sent - sample: [{"request": {"json": null, "method": "GET", "timeout": 30, "url": "http+unix://%2Fvar%2Flib%2Flxd%2Funix.socket/1.0/containers/myubuntu"}, "response": {"json": {"metadata": {"architecture": "x86_64", "config": {"limits.cpu": "2", "volatile.base_image": "ed0fb49ea8c3698c96c14157dff05b8c55eab2db438d3b043af1037836f1fa2b", "volatile.eth0.hwaddr": "00:16:3e:f6:fa:b0", "volatile.last_state.idmap": "[{\"Isuid\":true,\"Isgid\":false,\"Hostid\":100000,\"Nsid\":0,\"Maprange\":65536},{\"Isuid\":false,\"Isgid\":true,\"Hostid\":100000,\"Nsid\":0,\"Maprange\":65536}]"}, "created_at": "2016-06-25T18:03:37+09:00", "devices": {"root": {"path": "/", "type": "disk"}}, "ephemeral": false, "expanded_config": {"limits.cpu": "2", "volatile.base_image": "ed0fb49ea8c3698c96c14157dff05b8c55eab2db438d3b043af1037836f1fa2b", "volatile.eth0.hwaddr": "00:16:3e:f6:fa:b0", "volatile.last_state.idmap": "[{\"Isuid\":true,\"Isgid\":false,\"Hostid\":100000,\"Nsid\":0,\"Maprange\":65536},{\"Isuid\":false,\"Isgid\":true,\"Hostid\":100000,\"Nsid\":0,\"Maprange\":65536}]"}, "expanded_devices": {"eth0": {"name": "eth0", "nictype": "bridged", "parent": "lxdbr0", "type": "nic"}, "root": {"path": "/", "type": "disk"}}, "name": "myubuntu", "profiles": ["default"], "stateful": false, "status": "Running", "status_code": 103}, "status": "Success", "status_code": 200, "type": "sync"}}, "type": "sent request"}, {"request": {"json": {"architecture": "x86_64", "config": {"limits.cpu": "2", "volatile.base_image": "ed0fb49ea8c3698c96c14157dff05b8c55eab2db438d3b043af1037836f1fa2b", "volatile.eth0.hwaddr": "00:16:3e:f6:fa:b0", "volatile.last_state.idmap": "[{\"Isuid\":true,\"Isgid\":false,\"Hostid\":100000,\"Nsid\":0,\"Maprange\":65536},{\"Isuid\":false,\"Isgid\":true,\"Hostid\":100000,\"Nsid\":0,\"Maprange\":65536}]"}, "devices": {"root": {"path": "/", "type": "disk"}}, "profiles": ["default"]}, "method": "PUT", "timeout": 30, "url": "http+unix://%2Fvar%2Flib%2Flxd%2Funix.socket/1.0/containers/myubuntu"}, "response": {"json": {"metadata": {"class": "task", "created_at": "2016-06-25T18:03:54.082548079+09:00", "err": "", "id": "e0f6faf3-0cbb-4bd2-9a51-8a3d034d839d", "may_cancel": false, "metadata": null, "resources": {"containers": ["/1.0/containers/myubuntu"]}, "status": "Running", "status_code": 103, "updated_at": "2016-06-25T18:03:54.082548079+09:00"}, "operation": "/1.0/operations/e0f6faf3-0cbb-4bd2-9a51-8a3d034d839d", "status": "Operation created", "status_code": 100, "type": "async"}}, "type": "sent request"}, {"request": {"json": null, "method": "GET", "timeout": 30, "url": "http+unix://%2Fvar%2Flib%2Flxd%2Funix.socket/1.0/operations/e0f6faf3-0cbb-4bd2-9a51-8a3d034d839d/wait?timeout=30"}, "response": {"json": {"metadata": {"class": "task", "created_at": "2016-06-25T18:03:54.082548079+09:00", "err": "", "id": "e0f6faf3-0cbb-4bd2-9a51-8a3d034d839d", "may_cancel": false, "metadata": null, "resources": {"containers": ["/1.0/containers/myubuntu"]}, "status": "Success", "status_code": 200, "updated_at": "2016-06-25T18:03:54.082548079+09:00"}, "status": "Success", "status_code": 200, "type": "sync"}}, "type": "sent request"}] actions: description: list of actions performed for the container returned: success From e4d02a6a91a9627292da3fd1731cae03f90357ec Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Sun, 26 Jun 2016 09:15:06 +0900 Subject: [PATCH 1747/2522] No meaningful change just to trigger Travis --- cloud/lxd/lxd_container.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 2ed21c9c4dc..947d928a5f5 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -566,7 +566,6 @@ def main(): lxd_manage = LxdContainerManagement(module=module) lxd_manage.run() - # import module bits from ansible.module_utils.basic import * if __name__ == '__main__': From 30463320318ba027ecde5eb87ec34c8e7420d00c Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Sun, 26 Jun 2016 09:20:49 +0900 Subject: [PATCH 1748/2522] Expand tab with 8 spaces --- cloud/lxd/lxd_container.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 947d928a5f5..897da792d39 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -284,9 +284,9 @@ def __init__(self, module): def _send_request(self, method, url, body_json=None, ok_error_codes=None): try: - body = json.dumps(body_json) + body = json.dumps(body_json) self.connection.request(method, url, body=body) - resp = self.connection.getresponse() + resp = self.connection.getresponse() resp_json = json.loads(resp.read()) self.logs.append({ 'type': 'sent request', From f68c1a1071c7e4a9280f0b571e9d04e7a426cbc8 Mon Sep 17 00:00:00 2001 From: jpic Date: Mon, 27 Jun 2016 13:35:19 +0200 Subject: [PATCH 1749/2522] Refactor --- cloud/lxd/lxd_container.py | 38 ++++++++++++-------------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 897da792d39..b9cf4e60106 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -203,34 +203,20 @@ # httplib/http.client connection using unix domain socket import socket try: - import httplib - - class UnixHTTPConnection(httplib.HTTPConnection): - def __init__(self, path, host='localhost', port=None, strict=None, - timeout=None): - httplib.HTTPConnection.__init__(self, host, port=port, strict=strict, - timeout=timeout) - self.path = path - - def connect(self): - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.connect(self.path) - self.sock = sock + from httplib import HTTPConnection except ImportError: # Python 3 - import http.client - - class UnixHTTPConnection(http.client.HTTPConnection): - def __init__(self, path, host='localhost', port=None, - timeout=None): - http.client.HTTPConnection.__init__(self, host, port=port, - timeout=timeout) - self.path = path - - def connect(self): - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.connect(self.path) - self.sock = sock + from http.client import HTTPConnection + +class UnixHTTPConnection(HTTPConnection): + def __init__(self, path, timeout=None): + super(UnixHTTPConnection, self).__init__('localhost', timeout=timeout) + self.path = path + + def connect(self): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(self.path) + self.sock = sock # LXD_ANSIBLE_STATES is a map of states that contain values of methods used From 8319d935ca4ac8beab87997f84632368b118c1f9 Mon Sep 17 00:00:00 2001 From: Brad Davidson Date: Mon, 9 May 2016 17:07:39 -0700 Subject: [PATCH 1750/2522] New module ec2_vpc_nacl_facts --- cloud/amazon/ec2_vpc_nacl_facts.py | 201 +++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 cloud/amazon/ec2_vpc_nacl_facts.py diff --git a/cloud/amazon/ec2_vpc_nacl_facts.py b/cloud/amazon/ec2_vpc_nacl_facts.py new file mode 100644 index 00000000000..b809642c714 --- /dev/null +++ b/cloud/amazon/ec2_vpc_nacl_facts.py @@ -0,0 +1,201 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ec2_vpc_nacl_facts +short_description: Gather facts about Network ACLs in an AWS VPC +description: + - Gather facts about Network ACLs in an AWS VPC +version_added: "2.2" +author: "Brad Davidson (@brandond)" +requires: [ boto3 ] +options: + nacl_ids: + description: + - A list of Network ACL IDs to retrieve facts about. + required: false + default: [] + filters: + description: + - A dict of filters to apply. Each dict item consists of a filter key and a filter value. See \ + U(http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeNetworkAcls.html) for possible filters. Filter \ + names and values are case sensitive. + required: false + default: {} +notes: + - By default, the module will return all Network ACLs. + +extends_documentation_fragment: + - aws + - ec2 +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Gather facts about all Network ACLs: +- name: Get All NACLs + register: all_nacls + ec2_vpc_nacl_facts: + region: us-west-2 + +# Retrieve default Network ACLs: +- name: Get Default NACLs + register: default_nacls + ec2_vpc_nacl_facts: + region: us-west-2 + filters: + 'default': 'true' +''' + +RETURN = ''' +nacl: + description: Returns an array of complex objects as described below. + returned: success + type: list of complex + contains: + nacl_id: + description: The ID of the Network Access Control List. + returned: always + type: string + vpc_id: + description: The ID of the VPC that the NACL is attached to. + returned: always + type: string + is_default: + description: True if the NACL is the default for its VPC. + returned: always + type: boolean + tags: + description: A dict of tags associated with the NACL. + returned: always + type: dict + subnets: + description: A list of subnet IDs that are associated with the NACL. + returned: always + type: list of string + ingress: + description: A list of NACL ingress rules. + returned: always + type: list of list + egress: + description: A list of NACL egress rules. + returned: always + type: list of list +''' + +try: + import boto3 + from botocore.exceptions import ClientError, NoCredentialsError + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + +# VPC-supported IANA protocol numbers +# http://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml +PROTOCOL_NAMES = {'-1': 'all', '1': 'icmp', '6': 'tcp', '17': 'udp'} + +def list_ec2_vpc_nacls(connection, module): + + nacl_ids = module.params.get("nacl_ids") + filters = ansible_dict_to_boto3_filter_list(module.params.get("filters")) + + try: + nacls = connection.describe_network_acls(NetworkAclIds=nacl_ids, Filters=filters) + except (ClientError, NoCredentialsError) as e: + module.fail_json(msg=e.message, **camel_dict_to_snake_dict(e.response)) + + # Turn the boto3 result in to ansible_friendly_snaked_names + snaked_nacls = [] + for nacl in nacls['NetworkAcls']: + snaked_nacls.append(camel_dict_to_snake_dict(nacl)) + + # Turn the boto3 result in to ansible friendly tag dictionary + for nacl in snaked_nacls: + if 'tags' in nacl: + nacl['tags'] = boto3_tag_list_to_ansible_dict(nacl['tags']) + if 'entries' in nacl: + nacl['egress'] = [nacl_entry_to_list(e) for e in nacl['entries'] + if e['rule_number'] != 32767 and e['egress']] + nacl['ingress'] = [nacl_entry_to_list(e) for e in nacl['entries'] + if e['rule_number'] != 32767 and not e['egress']] + del nacl['entries'] + if 'associations' in nacl: + nacl['subnets'] = [a['subnet_id'] for a in nacl['associations']] + del nacl['associations'] + if 'network_acl_id' in nacl: + nacl['nacl_id'] = nacl['network_acl_id'] + del nacl['network_acl_id'] + + module.exit_json(nacls=snaked_nacls) + +def nacl_entry_to_list(entry): + + elist = [entry['rule_number'], + PROTOCOL_NAMES[entry['protocol']], + entry['rule_action'], + entry['cidr_block'] + ] + if entry['protocol'] == '1': + elist = elist + [-1, -1] + else: + elist = elist + [None, None, None, None] + + if 'icmp_type_code' in entry: + elist[4] = entry['icmp_type_code']['type'] + elist[5] = entry['icmp_type_code']['code'] + + if 'port_range' in entry: + elist[6] = entry['port_range']['from'] + elist[7] = entry['port_range']['to'] + + return elist + +def main(): + + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + nacl_ids=dict(default=[], type='list'), + filters=dict(default={}, type='dict') + ) + ) + + module = AnsibleModule(argument_spec=argument_spec, + mutually_exclusive=[ + ['nacl_ids', 'filters'] + ] + ) + + if not HAS_BOTO3: + module.fail_json(msg='boto3 required for this module') + + region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) + + if region: + connection = boto3_conn(module, conn_type='client', resource='ec2', + region=region, endpoint=ec2_url, **aws_connect_params) + else: + module.fail_json(msg="region must be specified") + + list_ec2_vpc_nacls(connection, module) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From d25c487ac8349885c766c7ae69e294625f21e813 Mon Sep 17 00:00:00 2001 From: Shota Date: Tue, 28 Jun 2016 12:03:56 +0900 Subject: [PATCH 1751/2522] Fix some typos (#2490) --- system/timezone.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/system/timezone.py b/system/timezone.py index 2af3170e971..4a6820ba262 100644 --- a/system/timezone.py +++ b/system/timezone.py @@ -48,7 +48,7 @@ Whether the hardware clock is in UTC or in local timezone. Default is to keep current setting. Note that this option is recommended not to change and may fail - to configure, especially on virtual envoironments such as AWS. + to configure, especially on virtual environments such as AWS. required: false aliases: ['rtc'] author: "Shinichi TAMURA @tmshn" @@ -79,7 +79,7 @@ class Timezone(object): A subclass may wish to override the following action methods: - get(key, phase) ... get the value from the system at `phase` - - set(key, value) ... set the value to the curren system + - set(key, value) ... set the value to the current system """ def __new__(cls, module): @@ -139,7 +139,7 @@ def execute(self, *commands, **kwargs): Args: *commands: The command to execute. - It will be concatinated with single space. + It will be concatenated with single space. **kwargs: Only 'log' key is checked. If kwargs['log'] is true, record the command to self.msg. @@ -276,7 +276,7 @@ class NosystemdTimezone(Timezone): For timezone setting, it edits the following file and reflect changes: - /etc/sysconfig/clock ... RHEL/CentOS - - /etc/timezone ... Debian/Ubnutu + - /etc/timezone ... Debian/Ubuntu For hwclock setting, it executes `hwclock --systohc` command with the '--utc' or '--localtime' option. """ From 7ba6915b498e89f213c68f3276fc05fd0147525e Mon Sep 17 00:00:00 2001 From: jpic Date: Tue, 28 Jun 2016 12:38:40 +0200 Subject: [PATCH 1752/2522] Can't use super on old style classes --- cloud/lxd/lxd_container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index b9cf4e60106..782a51a5f82 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -210,7 +210,7 @@ class UnixHTTPConnection(HTTPConnection): def __init__(self, path, timeout=None): - super(UnixHTTPConnection, self).__init__('localhost', timeout=timeout) + HTTPConnection.__init__(self, 'localhost', timeout=timeout) self.path = path def connect(self): From f46d3086d615cb4d36710ae5124d3ff28d06adc1 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Tue, 28 Jun 2016 22:52:40 +0900 Subject: [PATCH 1753/2522] Split config parameter to config, devices, profiles, source et al --- cloud/lxd/lxd_container.py | 132 +++++++++++++++++++++++-------------- 1 file changed, 83 insertions(+), 49 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 782a51a5f82..9e4caf758e2 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -32,26 +32,43 @@ description: - Name of a container. required: true + architecture: + description: + - The archiecture for the container (e.g. "x86_64" or "i686"). + See https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1 + required: false config: description: - - A config dictionary for creating a container. + - The config for the container (e.g. '{"limits.cpu": "2"}'). See https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1 - - Required when the container is not created yet and the state is - not absent. - - If the container already exists and its metadata obtained from + - If the container already exists and its "config" value in metadata + obtained from GET /1.0/containers/ https://github.com/lxc/lxd/blob/master/doc/rest-api.md#10containersname are different, they this module tries to apply the configurations. - The following keys in config will be compared and applied. - - architecture - - config - - The key starts with 'volatile.' are ignored for comparison. - - devices - - ephemeral - - profiles + - The key starts with 'volatile.' are ignored for this comparison. - Not all config values are supported to apply the existing container. Maybe you need to delete and recreate a container. required: false + devices: + description: + - The devices for the container + (e.g. '{ "rootfs": { "path": "/dev/kvm", "type": "unix-char" }'). + See https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1 + required: false + ephemeral: + description: + - Whether or not the container is ephemeral (e.g. true or false). + See https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1 + required: false + source: + description: + - The source for the container + (e.g. '{ "type": "image", "mode": "pull", + "server": "https://images.linuxcontainers.org", "protocol": "lxd", + "alias": "ubuntu/xenial/amd64" }'). + See https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1 + required: false state: choices: - started @@ -113,14 +130,13 @@ lxd_container: name: mycontainer state: started - config: - source: - type: image - mode: pull - server: https://images.linuxcontainers.org - protocol: lxd - alias: "ubuntu/xenial/amd64" - profiles: ["default"] + source: + type: image + mode: pull + server: https://images.linuxcontainers.org + protocol: lxd + alias: "ubuntu/xenial/amd64" + profiles: ["default"] - name: Install python in the created container "mycontainer" command: lxc exec mycontainer -- apt install -y python - name: Copy /etc/hosts in the created container "mycontainer" to localhost with name "mycontainer-hosts" @@ -144,14 +160,13 @@ lxd_container: name: mycontainer state: stopped - config: - source: - type: image - mode: pull - server: https://images.linuxcontainers.org - protocol: lxd - alias: "ubuntu/xenial/amd64" - profiles: ["default"] + source: + type: image + mode: pull + server: https://images.linuxcontainers.org + protocol: lxd + alias: "ubuntu/xenial/amd64" + profiles: ["default"] - hosts: localhost connection: local @@ -160,14 +175,13 @@ lxd_container: name: mycontainer state: restarted - config: - source: - type: image - mode: pull - server: https://images.linuxcontainers.org - protocol: lxd - alias: "ubuntu/xenial/amd64" - profiles: ["default"] + source: + type: image + mode: pull + server: https://images.linuxcontainers.org + protocol: lxd + alias: "ubuntu/xenial/amd64" + profiles: ["default"] """ RETURN=""" @@ -257,7 +271,13 @@ def __init__(self, module): """ self.module = module self.container_name = self.module.params['name'] - self.config = self.module.params.get('config', None) + + self.container_config = {} + for attr in ['architecture', 'config', 'devices', 'ephemeral', 'profiles', 'source']: + param_val = self.module.params.get(attr, None) + if param_val is not None: + self.container_config[attr] = param_val + self.state = self.module.params['state'] self.timeout = self.module.params['timeout'] self.wait_for_ipv4_addresses = self.module.params['wait_for_ipv4_addresses'] @@ -296,13 +316,12 @@ def _send_request(self, method, url, body_json=None, ok_error_codes=None): def _operate_and_wait(self, method, path, body_json=None): resp_json = self._send_request(method, path, body_json=body_json) if resp_json['type'] == 'async': - path = '{0}/wait?timeout={1}'.format(resp_json['operation'], self.timeout) - resp_json = self._send_request('GET', path) + url = '{0}/wait?timeout={1}'.format(resp_json['operation'], self.timeout) + resp_json = self._send_request('GET', url) if resp_json['metadata']['status'] != 'Success': - url = self._path_to_url(path) self.module.fail_json( msg='error response for waiting opearation', - request={'method': method, 'url': url, 'json': body_json, 'timeout': self.timeout}, + request={'method': method, 'url': url, 'timeout': self.timeout}, response={'json': resp_json}, logs=self.logs ) @@ -333,10 +352,10 @@ def _change_state(self, action, force_stop=False): return self._operate_and_wait('PUT', '/1.0/containers/{0}/state'.format(self.container_name), body_json=body_json) def _create_container(self): - config = self.config.copy() + config = self.container_config.copy() config['name'] = self.container_name self._operate_and_wait('POST', '/1.0/containers', config) - self.actions.append('creat') + self.actions.append('create') def _start_container(self): self._change_state('start') @@ -453,13 +472,13 @@ def _frozen(self): self._freeze_container() def _needs_to_change_config(self, key): - if key not in self.config: + if key not in self.container_config: return False if key == 'config': old_configs = dict((k, v) for k, v in self.old_container_json['metadata'][key].items() if not k.startswith('volatile.')) else: old_configs = self.old_container_json['metadata'][key] - return self.config[key] != old_configs + return self.container_config[key] != old_configs def _needs_to_apply_configs(self): return ( @@ -479,16 +498,16 @@ def _apply_configs(self): 'profiles': old_metadata['profiles'] } if self._needs_to_change_config('architecture'): - body_json['architecture'] = self.config['architecture'] + body_json['architecture'] = self.container_config['architecture'] if self._needs_to_change_config('config'): - for k, v in self.config['config'].items(): + for k, v in self.container_config['config'].items(): body_json['config'][k] = v if self._needs_to_change_config('ephemeral'): - body_json['ephemeral'] = self.config['ephemeral'] + body_json['ephemeral'] = self.container_config['ephemeral'] if self._needs_to_change_config('devices'): - body_json['devices'] = self.config['devices'] + body_json['devices'] = self.container_config['devices'] if self._needs_to_change_config('profiles'): - body_json['profiles'] = self.config['profiles'] + body_json['profiles'] = self.container_config['profiles'] self._operate_and_wait('PUT', '/1.0/containers/{0}'.format(self.container_name), body_json=body_json) self.actions.append('apply_configs') @@ -522,9 +541,24 @@ def main(): type='str', required=True ), + architecture=dict( + type='str', + ), config=dict( type='dict', ), + devices=dict( + type='dict', + ), + ephemeral=dict( + type='bool', + ), + profiles=dict( + type='list', + ), + source=dict( + type='dict', + ), state=dict( choices=LXD_ANSIBLE_STATES.keys(), default='started' From fd9a6a7f26b48b4159e63a0f4d5f65f9c9ff6f36 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Tue, 28 Jun 2016 23:02:08 +0900 Subject: [PATCH 1754/2522] Fix invalid YAML in document --- cloud/lxd/lxd_container.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 9e4caf758e2..482e75bb1e2 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -39,7 +39,8 @@ required: false config: description: - - The config for the container (e.g. '{"limits.cpu": "2"}'). + - > + The config for the container (e.g. {"limits.cpu": "2"}). See https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1 - If the container already exists and its "config" value in metadata obtained from @@ -52,8 +53,9 @@ required: false devices: description: - - The devices for the container - (e.g. '{ "rootfs": { "path": "/dev/kvm", "type": "unix-char" }'). + - > + The devices for the container + (e.g. { "rootfs": { "path": "/dev/kvm", "type": "unix-char" }). See https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1 required: false ephemeral: @@ -63,10 +65,13 @@ required: false source: description: - - The source for the container - (e.g. '{ "type": "image", "mode": "pull", - "server": "https://images.linuxcontainers.org", "protocol": "lxd", - "alias": "ubuntu/xenial/amd64" }'). + - > + The source for the container + (e.g. { "type": "image", + "mode": "pull", + "server": "https://images.linuxcontainers.org", + "protocol": "lxd", + "alias": "ubuntu/xenial/amd64" }). See https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1 required: false state: From c82cfe8d281cb5472e2e1527d4b979e3145d1773 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Wed, 29 Jun 2016 00:24:56 +0900 Subject: [PATCH 1755/2522] Support for creating, modifying, renaming and deleting a profile --- cloud/lxd/lxd_container.py | 226 +++++++++++++++++++++++++++++-------- 1 file changed, 179 insertions(+), 47 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 482e75bb1e2..0578eadb716 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -32,6 +32,14 @@ description: - Name of a container. required: true + type: + choices: + - container + - profile + description: + - The resource type. + required: false + default: container architecture: description: - The archiecture for the container (e.g. "x86_64" or "i686"). @@ -74,15 +82,24 @@ "alias": "ubuntu/xenial/amd64" }). See https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1 required: false + new_name: + description: + - A new name of a profile. + - If this parameter is specified a profile will be renamed to this name. + required: false state: choices: + - present - started - stopped - restarted - absent - frozen description: - - Define the state of a container. + - Define the state of a container or profile. + - Valid choices for type=container are started, stopped, restarted, + absent, or frozen. + - Valid choices for type=profile are present or absent. required: false default: started timeout: @@ -187,6 +204,22 @@ protocol: lxd alias: "ubuntu/xenial/amd64" profiles: ["default"] + +- hosts: localhost + connection: local + tasks: + - name: create macvlan profile + lxd_container: + type: profile + name: macvlan + state: present + config: {} + description: 'my macvlan profile' + devices: + eth0: + nictype: macvlan + parent: br0 + type: nic """ RETURN=""" @@ -241,6 +274,7 @@ def connect(self): # LXD_ANSIBLE_STATES is a map of states that contain values of methods used # when a particular state is evoked. LXD_ANSIBLE_STATES = { + 'present': '', # TODO: Separate state for profile 'started': '_started', 'stopped': '_stopped', 'restarted': '_restarted', @@ -256,6 +290,12 @@ def connect(self): 'Frozen': 'frozen', } +# CONFIG_PARAMS is a map from a resource type to config attribute names. +CONFIG_PARAMS = { + 'container': ['architecture', 'config', 'devices', 'ephemeral', 'profiles', 'source'], + 'profile': ['config', 'description', 'devices'] +} + try: callable(all) except NameError: @@ -275,15 +315,12 @@ def __init__(self, module): :type module: ``object`` """ self.module = module - self.container_name = self.module.params['name'] - - self.container_config = {} - for attr in ['architecture', 'config', 'devices', 'ephemeral', 'profiles', 'source']: - param_val = self.module.params.get(attr, None) - if param_val is not None: - self.container_config[attr] = param_val - + self.name = self.module.params['name'] + self.type = self.module.params['type'] + self._build_config() + # TODO: check state value according to type self.state = self.module.params['state'] + self.new_name = self.module.params.get('new_name', None) self.timeout = self.module.params['timeout'] self.wait_for_ipv4_addresses = self.module.params['wait_for_ipv4_addresses'] self.force_stop = self.module.params['force_stop'] @@ -293,6 +330,13 @@ def __init__(self, module): self.logs = [] self.actions = [] + def _build_config(self): + self.config = {} + for attr in CONFIG_PARAMS[self.type]: + param_val = self.module.params.get(attr, None) + if param_val is not None: + self.config[attr] = param_val + def _send_request(self, method, url, body_json=None, ok_error_codes=None): try: body = json.dumps(body_json) @@ -334,13 +378,13 @@ def _operate_and_wait(self, method, path, body_json=None): def _get_container_json(self): return self._send_request( - 'GET', '/1.0/containers/{0}'.format(self.container_name), + 'GET', '/1.0/containers/{0}'.format(self.name), ok_error_codes=[404] ) def _get_container_state_json(self): return self._send_request( - 'GET', '/1.0/containers/{0}/state'.format(self.container_name), + 'GET', '/1.0/containers/{0}/state'.format(self.name), ok_error_codes=[404] ) @@ -354,11 +398,11 @@ def _change_state(self, action, force_stop=False): body_json={'action': action, 'timeout': self.timeout} if force_stop: body_json['force'] = True - return self._operate_and_wait('PUT', '/1.0/containers/{0}/state'.format(self.container_name), body_json=body_json) + return self._operate_and_wait('PUT', '/1.0/containers/{0}/state'.format(self.name), body_json=body_json) def _create_container(self): - config = self.container_config.copy() - config['name'] = self.container_name + config = self.config.copy() + config['name'] = self.name self._operate_and_wait('POST', '/1.0/containers', config) self.actions.append('create') @@ -375,7 +419,7 @@ def _restart_container(self): self.actions.append('restart') def _delete_container(self): - return self._operate_and_wait('DELETE', '/1.0/containers/{0}'.format(self.container_name)) + return self._operate_and_wait('DELETE', '/1.0/containers/{0}'.format(self.name)) self.actions.append('delete') def _freeze_container(self): @@ -423,8 +467,8 @@ def _started(self): self._unfreeze_container() elif self.old_state == 'stopped': self._start_container() - if self._needs_to_apply_configs(): - self._apply_configs() + if self._needs_to_apply_container_configs(): + self._apply_container_configs() if self.wait_for_ipv4_addresses: self._get_addresses() @@ -433,15 +477,15 @@ def _stopped(self): self._create_container() else: if self.old_state == 'stopped': - if self._needs_to_apply_configs(): + if self._needs_to_apply_container_configs(): self._start_container() - self._apply_configs() + self._apply_container_configs() self._stop_container() else: if self.old_state == 'frozen': self._unfreeze_container() - if self._needs_to_apply_configs(): - self._apply_configs() + if self._needs_to_apply_container_configs(): + self._apply_container_configs() self._stop_container() def _restarted(self): @@ -451,8 +495,8 @@ def _restarted(self): else: if self.old_state == 'frozen': self._unfreeze_container() - if self._needs_to_apply_configs(): - self._apply_configs() + if self._needs_to_apply_container_configs(): + self._apply_container_configs() self._restart_container() if self.wait_for_ipv4_addresses: self._get_addresses() @@ -472,29 +516,29 @@ def _frozen(self): else: if self.old_state == 'stopped': self._start_container() - if self._needs_to_apply_configs(): - self._apply_configs() + if self._needs_to_apply_container_configs(): + self._apply_container_configs() self._freeze_container() - def _needs_to_change_config(self, key): - if key not in self.container_config: + def _needs_to_change_container_config(self, key): + if key not in self.config: return False if key == 'config': old_configs = dict((k, v) for k, v in self.old_container_json['metadata'][key].items() if not k.startswith('volatile.')) else: old_configs = self.old_container_json['metadata'][key] - return self.container_config[key] != old_configs + return self.config[key] != old_configs - def _needs_to_apply_configs(self): + def _needs_to_apply_container_configs(self): return ( - self._needs_to_change_config('architecture') or - self._needs_to_change_config('config') or - self._needs_to_change_config('ephemeral') or - self._needs_to_change_config('devices') or - self._needs_to_change_config('profiles') + self._needs_to_change_container_config('architecture') or + self._needs_to_change_container_config('config') or + self._needs_to_change_container_config('ephemeral') or + self._needs_to_change_container_config('devices') or + self._needs_to_change_container_config('profiles') ) - def _apply_configs(self): + def _apply_container_configs(self): old_metadata = self.old_container_json['metadata'] body_json = { 'architecture': old_metadata['architecture'], @@ -503,27 +547,104 @@ def _apply_configs(self): 'profiles': old_metadata['profiles'] } if self._needs_to_change_config('architecture'): - body_json['architecture'] = self.container_config['architecture'] + body_json['architecture'] = self.config['architecture'] if self._needs_to_change_config('config'): - for k, v in self.container_config['config'].items(): + for k, v in self.config['config'].items(): body_json['config'][k] = v if self._needs_to_change_config('ephemeral'): - body_json['ephemeral'] = self.container_config['ephemeral'] + body_json['ephemeral'] = self.config['ephemeral'] if self._needs_to_change_config('devices'): - body_json['devices'] = self.container_config['devices'] + body_json['devices'] = self.config['devices'] if self._needs_to_change_config('profiles'): - body_json['profiles'] = self.container_config['profiles'] - self._operate_and_wait('PUT', '/1.0/containers/{0}'.format(self.container_name), body_json=body_json) - self.actions.append('apply_configs') + body_json['profiles'] = self.config['profiles'] + self._operate_and_wait('PUT', '/1.0/containers/{0}'.format(self.name), body_json=body_json) + self.actions.append('apply_container_configs') + + def _get_profile_json(self): + return self._send_request( + 'GET', '/1.0/profiles/{0}'.format(self.name), + ok_error_codes=[404] + ) + + @staticmethod + def _profile_json_to_module_state(resp_json): + if resp_json['type'] == 'error': + return 'absent' + return 'present' + + def _update_profile(self): + if self.state == 'present': + if self.old_state == 'absent': + if self.new_name is None: + self._create_profile() + else: + self.module.fail_json( + failed=True, + msg='new_name must not be set when the profile does not exist and the specified state is present', + changed=False) + else: + if self.new_name is not None and self.new_name != self.name: + self._rename_profile() + if self._needs_to_apply_profile_configs(): + self._apply_profile_configs() + elif self.state == 'absent': + if self.old_state == 'present': + if self.new_name is None: + self._delete_profile() + else: + self.module.fail_json( + failed=True, + msg='new_name must not be set when the profile exists and the specified state is absent', + changed=False) + + def _create_profile(self): + config = self.config.copy() + config['name'] = self.name + self._send_request('POST', '/1.0/profiles', config) + self.actions.append('create') + + def _rename_profile(self): + config = { 'name': self.new_name } + self._send_request('POST', '/1.0/profiles/{}'.format(self.name), config) + self.actions.append('rename') + self.name = self.new_name + + def _needs_to_change_profile_config(self, key): + if key not in self.config: + return False + old_configs = self.old_profile_json['metadata'].get(key, None) + return self.config[key] != old_configs + + def _needs_to_apply_profile_configs(self): + return ( + self._needs_to_change_profile_config('config') or + self._needs_to_change_profile_config('description') or + self._needs_to_change_profile_config('devices') + ) + + def _apply_profile_configs(self): + config = self.old_profile_json.copy() + for k, v in self.config.iteritems(): + config[k] = v + self._send_request('PUT', '/1.0/profiles/{}'.format(self.name), config) + self.actions.append('apply_profile_configs') + + def _delete_profile(self): + self._send_request('DELETE', '/1.0/profiles/{}'.format(self.name)) + self.actions.append('delete') def run(self): """Run the main method.""" - self.old_container_json = self._get_container_json() - self.old_state = self._container_json_to_module_state(self.old_container_json) - - action = getattr(self, LXD_ANSIBLE_STATES[self.state]) - action() + if self.type == 'container': + self.old_container_json = self._get_container_json() + self.old_state = self._container_json_to_module_state(self.old_container_json) + action = getattr(self, LXD_ANSIBLE_STATES[self.state]) + action() + elif self.type == 'profile': + self.old_profile_json = self._get_profile_json() + self.old_state = self._profile_json_to_module_state(self.old_profile_json) + self._update_profile() state_changed = len(self.actions) > 0 result_json = { @@ -546,12 +667,23 @@ def main(): type='str', required=True ), + new_name=dict( + type='str', + ), + type=dict( + type='str', + choices=CONFIG_PARAMS.keys(), + default='container' + ), architecture=dict( type='str', ), config=dict( type='dict', ), + description=dict( + type='str', + ), devices=dict( type='dict', ), From 4bbbad63785f7452844556a5561e41c61c1679ab Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Wed, 29 Jun 2016 00:09:37 -0700 Subject: [PATCH 1756/2522] Adds coding conventions for the bigip-virtual-server module (#2473) A number of coding conventions have been adopted for new F5 modules that are in development. To ensure common usage across the modules, this module needed to be updated to reflect those conventions. --- network/f5/bigip_virtual_server.py | 671 ++++++++++++++++------------- 1 file changed, 374 insertions(+), 297 deletions(-) diff --git a/network/f5/bigip_virtual_server.py b/network/f5/bigip_virtual_server.py index 7aab53e4279..93e13c2e77d 100644 --- a/network/f5/bigip_virtual_server.py +++ b/network/f5/bigip_virtual_server.py @@ -23,172 +23,181 @@ module: bigip_virtual_server short_description: "Manages F5 BIG-IP LTM virtual servers" description: - - "Manages F5 BIG-IP LTM virtual servers via iControl SOAP API" + - "Manages F5 BIG-IP LTM virtual servers via iControl SOAP API" version_added: "2.1" author: - - Etienne Carriere (@Etienne-Carriere) - - Tim Rupp (@caphrim007) + - Etienne Carriere (@Etienne-Carriere) + - Tim Rupp (@caphrim007) notes: - - "Requires BIG-IP software version >= 11" - - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" - - "Best run as a local_action in your playbook" + - "Requires BIG-IP software version >= 11" + - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" + - "Best run as a local_action in your playbook" requirements: - - bigsuds + - bigsuds options: - server: - description: - - BIG-IP host - required: true - server_port: - description: - - BIG-IP server port - required: false - default: 443 - version_added: "2.2" - user: - description: - - BIG-IP username - required: true - password: - description: - - BIG-IP password - required: true - validate_certs: - description: - - If C(no), SSL certificates will not be validated. This should only be used - on personally controlled sites using self-signed certificates. - required: false - default: 'yes' - choices: ['yes', 'no'] - state: - description: - - Virtual Server state - - Absent, delete the VS if present - - present (and its synonym enabled), create if needed the VS and set state to enabled - - disabled, create if needed the VS and set state to disabled - required: false - default: present - choices: ['present', 'absent', 'enabled', 'disabled'] - aliases: [] - partition: - description: - - Partition - required: false - default: 'Common' - name: - description: - - "Virtual server name." - required: true - aliases: ['vs'] - destination: - description: - - "Destination IP of the virtual server (only host is currently supported) . Required when state=present and vs does not exist." - required: true - aliases: ['address', 'ip'] - port: - description: - - "Port of the virtual server . Required when state=present and vs does not exist" - required: false - default: None - all_profiles: - description: - - "List of all Profiles (HTTP,ClientSSL,ServerSSL,etc) that must be used by the virtual server" - required: false - default: None - all_rules: - version_added: "2.2" - description: - - "List of rules to be applied in priority order" - required: false - default: None - pool: - description: - - "Default pool for the virtual server" - required: false - default: None - snat: - description: - - "Source network address policy" - required: false - default: None - default_persistence_profile: - description: - - "Default Profile which manages the session persistence" - required: false - default: None + server: description: - description: - - "Virtual server description." - required: false - default: None + - BIG-IP host + required: true + server_port: + description: + - BIG-IP server port + required: false + default: 443 + version_added: "2.2" + user: + description: + - BIG-IP username + required: true + password: + description: + - BIG-IP password + required: true + validate_certs: + description: + - If C(no), SSL certificates will not be validated. This should only be used + on personally controlled sites using self-signed certificates. + required: false + default: 'yes' + choices: + - yes + - no + state: + description: + - Virtual Server state + - Absent, delete the VS if present + - C(present) (and its synonym enabled), create if needed the VS and set + state to enabled + - C(disabled), create if needed the VS and set state to disabled + required: false + default: present + choices: + - present + - absent + - enabled + - disabled + aliases: [] + partition: + description: + - Partition + required: false + default: 'Common' + name: + description: + - Virtual server name + required: true + aliases: + - vs + destination: + description: + - Destination IP of the virtual server (only host is currently supported). + Required when state=present and vs does not exist. + required: true + aliases: + - address + - ip + port: + description: + - Port of the virtual server . Required when state=present and vs does not exist + required: false + default: None + all_profiles: + description: + - List of all Profiles (HTTP,ClientSSL,ServerSSL,etc) that must be used + by the virtual server + required: false + default: None + all_rules: + version_added: "2.2" + description: + - List of rules to be applied in priority order + required: false + default: None + pool: + description: + - Default pool for the virtual server + required: false + default: None + snat: + description: + - Source network address policy + required: false + default: None + default_persistence_profile: + description: + - Default Profile which manages the session persistence + required: false + default: None + description: + description: + - Virtual server description + required: false + default: None ''' EXAMPLES = ''' - -## playbook task examples: - ---- -# file bigip-test.yml -# ... - - name: Add VS - local_action: - module: bigip_virtual_server - server: lb.mydomain.net - user: admin - password: secret - state: present - partition: MyPartition - name: myvirtualserver - destination: "{{ ansible_default_ipv4['address'] }}" - port: 443 - pool: "{{ mypool }}" - snat: Automap - description: Test Virtual Server - all_profiles: - - http - - clientssl - - - name: Modify Port of the Virtual Server - local_action: - module: bigip_virtual_server - server: lb.mydomain.net - user: admin - password: secret - state: present - partition: MyPartition - name: myvirtualserver - port: 8080 - - - name: Delete virtual server - local_action: - module: bigip_virtual_server - server: lb.mydomain.net - user: admin - password: secret - state: absent - partition: MyPartition - name: myvirtualserver +- name: Add virtual server + bigip_virtual_server: + server: lb.mydomain.net + user: admin + password: secret + state: present + partition: MyPartition + name: myvirtualserver + destination: "{{ ansible_default_ipv4['address'] }}" + port: 443 + pool: "{{ mypool }}" + snat: Automap + description: Test Virtual Server + all_profiles: + - http + - clientssl + delegate_to: localhost + +- name: Modify Port of the Virtual Server + bigip_virtual_server: + server: lb.mydomain.net + user: admin + password: secret + state: present + partition: MyPartition + name: myvirtualserver + port: 8080 + delegate_to: localhost + +- name: Delete virtual server + bigip_virtual_server: + server: lb.mydomain.net + user: admin + password: secret + state: absent + partition: MyPartition + name: myvirtualserver + delegate_to: localhost ''' RETURN = ''' --- deleted: - description: Name of a virtual server that was deleted - returned: virtual server was successfully deleted on state=absent - type: string + description: Name of a virtual server that was deleted + returned: changed + type: string + sample: "my-virtual-server" ''' -# ========================== -# bigip_virtual_server module specific -# - # map of state values -STATES={'enabled': 'STATE_ENABLED', - 'disabled': 'STATE_DISABLED'} -STATUSES={'enabled': 'SESSION_STATUS_ENABLED', - 'disabled': 'SESSION_STATUS_DISABLED', - 'offline': 'SESSION_STATUS_FORCED_DISABLED'} +STATES = { + 'enabled': 'STATE_ENABLED', + 'disabled': 'STATE_DISABLED' +} + +STATUSES = { + 'enabled': 'SESSION_STATUS_ENABLED', + 'disabled': 'SESSION_STATUS_DISABLED', + 'offline': 'SESSION_STATUS_FORCED_DISABLED' +} + def vs_exists(api, vs): # hack to determine if pool exists @@ -196,7 +205,7 @@ def vs_exists(api, vs): try: api.LocalLB.VirtualServer.get_object_status(virtual_servers=[vs]) result = True - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed as e: if "was not found" in str(e): result = False else: @@ -204,8 +213,9 @@ def vs_exists(api, vs): raise return result -def vs_create(api,name,destination,port,pool): - _profiles=[[{'profile_context': 'PROFILE_CONTEXT_TYPE_ALL', 'profile_name': 'tcp'}]] + +def vs_create(api, name, destination, port, pool): + _profiles = [[{'profile_context': 'PROFILE_CONTEXT_TYPE_ALL', 'profile_name': 'tcp'}]] created = False # a bit of a hack to handle concurrent runs of this module. # even though we've checked the vs doesn't exist, @@ -214,211 +224,278 @@ def vs_create(api,name,destination,port,pool): # about it! try: api.LocalLB.VirtualServer.create( - definitions = [{'name': [name], 'address': [destination], 'port': port, 'protocol': 'PROTOCOL_TCP'}], - wildmasks = ['255.255.255.255'], - resources = [{'type': 'RESOURCE_TYPE_POOL', 'default_pool_name': pool}], - profiles = _profiles) + definitions=[{'name': [name], 'address': [destination], 'port': port, 'protocol': 'PROTOCOL_TCP'}], + wildmasks=['255.255.255.255'], + resources=[{'type': 'RESOURCE_TYPE_POOL', 'default_pool_name': pool}], + profiles=_profiles) created = True return created - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed as e: if "already exists" not in str(e): raise Exception('Error on creating Virtual Server : %s' % e) -def vs_remove(api,name): - api.LocalLB.VirtualServer.delete_virtual_server(virtual_servers = [name ]) + +def vs_remove(api, name): + api.LocalLB.VirtualServer.delete_virtual_server( + virtual_servers=[name] + ) -def get_rules(api,name): - return api.LocalLB.VirtualServer.get_rule(virtual_servers = [name])[0] +def get_rules(api, name): + return api.LocalLB.VirtualServer.get_rule( + virtual_servers=[name] + )[0] -def set_rules(api,name,rules_list): - updated=False +def set_rules(api, name, rules_list): + updated = False if rules_list is None: return False rules_list = list(enumerate(rules_list)) try: - current_rules=map(lambda x: (x['priority'], x['rule_name']), get_rules(api,name)) - to_add_rules=[] + current_rules = map(lambda x: (x['priority'], x['rule_name']), get_rules(api, name)) + to_add_rules = [] for i, x in rules_list: - if (i ,x) not in current_rules: + if (i, x) not in current_rules: to_add_rules.append({'priority': i, 'rule_name': x}) - to_del_rules=[] + to_del_rules = [] for i, x in current_rules: if (i, x) not in rules_list: to_del_rules.append({'priority': i, 'rule_name': x}) - if len(to_del_rules)>0: - api.LocalLB.VirtualServer.remove_rule(virtual_servers = [name],rules = [to_del_rules]) - updated=True - if len(to_add_rules)>0: - api.LocalLB.VirtualServer.add_rule(virtual_servers = [name],rules= [to_add_rules]) - updated=True + if len(to_del_rules) > 0: + api.LocalLB.VirtualServer.remove_rule( + virtual_servers=[name], + rules=[to_del_rules] + ) + updated = True + if len(to_add_rules) > 0: + api.LocalLB.VirtualServer.add_rule( + virtual_servers=[name], + rules=[to_add_rules] + ) + updated = True return updated - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed as e: raise Exception('Error on setting profiles : %s' % e) -def get_profiles(api,name): - return api.LocalLB.VirtualServer.get_profile(virtual_servers = [name])[0] + +def get_profiles(api, name): + return api.LocalLB.VirtualServer.get_profile( + virtual_servers=[name] + )[0] -def set_profiles(api,name,profiles_list): - updated=False +def set_profiles(api, name, profiles_list): + updated = False try: if profiles_list is None: return False - current_profiles=map(lambda x:x['profile_name'], get_profiles(api,name)) - to_add_profiles=[] + current_profiles = map(lambda x: x['profile_name'], get_profiles(api, name)) + to_add_profiles = [] for x in profiles_list: if x not in current_profiles: to_add_profiles.append({'profile_context': 'PROFILE_CONTEXT_TYPE_ALL', 'profile_name': x}) - to_del_profiles=[] + to_del_profiles = [] for x in current_profiles: - if (x not in profiles_list) and (x!= "/Common/tcp"): + if (x not in profiles_list) and (x != "/Common/tcp"): to_del_profiles.append({'profile_context': 'PROFILE_CONTEXT_TYPE_ALL', 'profile_name': x}) - if len(to_del_profiles)>0: - api.LocalLB.VirtualServer.remove_profile(virtual_servers = [name],profiles = [to_del_profiles]) - updated=True - if len(to_add_profiles)>0: - api.LocalLB.VirtualServer.add_profile(virtual_servers = [name],profiles= [to_add_profiles]) - updated=True + if len(to_del_profiles) > 0: + api.LocalLB.VirtualServer.remove_profile( + virtual_servers=[name], + profiles=[to_del_profiles] + ) + updated = True + if len(to_add_profiles) > 0: + api.LocalLB.VirtualServer.add_profile( + virtual_servers=[name], + profiles=[to_add_profiles] + ) + updated = True return updated - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed as e: raise Exception('Error on setting profiles : %s' % e) -def set_snat(api,name,snat): + +def set_snat(api, name, snat): updated = False try: - current_state=get_snat_type(api,name) + current_state = get_snat_type(api, name) if snat is None: return updated elif snat == 'None' and current_state != 'SRC_TRANS_NONE': - api.LocalLB.VirtualServer.set_source_address_translation_none(virtual_servers = [name]) + api.LocalLB.VirtualServer.set_source_address_translation_none( + virtual_servers=[name] + ) updated = True elif snat == 'Automap' and current_state != 'SRC_TRANS_AUTOMAP': - api.LocalLB.VirtualServer.set_source_address_translation_automap(virtual_servers = [name]) + api.LocalLB.VirtualServer.set_source_address_translation_automap( + virtual_servers=[name] + ) updated = True return updated - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed as e: raise Exception('Error on setting snat : %s' % e) -def get_snat_type(api,name): - return api.LocalLB.VirtualServer.get_source_address_translation_type(virtual_servers = [name])[0] +def get_snat_type(api, name): + return api.LocalLB.VirtualServer.get_source_address_translation_type( + virtual_servers=[name] + )[0] -def get_pool(api,name): - return api.LocalLB.VirtualServer.get_default_pool_name(virtual_servers = [name])[0] -def set_pool(api,name,pool): - updated=False +def get_pool(api, name): + return api.LocalLB.VirtualServer.get_default_pool_name( + virtual_servers=[name] + )[0] + + +def set_pool(api, name, pool): + updated = False try: - current_pool = get_pool (api,name) + current_pool = get_pool(api, name) if pool is not None and (pool != current_pool): - api.LocalLB.VirtualServer.set_default_pool_name(virtual_servers = [name],default_pools = [pool]) - updated=True + api.LocalLB.VirtualServer.set_default_pool_name( + virtual_servers=[name], + default_pools=[pool] + ) + updated = True return updated - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed as e: raise Exception('Error on setting pool : %s' % e) -def get_destination(api,name): - return api.LocalLB.VirtualServer.get_destination_v2(virtual_servers = [name])[0] +def get_destination(api, name): + return api.LocalLB.VirtualServer.get_destination_v2( + virtual_servers=[name] + )[0] + -def set_destination(api,name,destination): - updated=False +def set_destination(api, name, destination): + updated = False try: - current_destination = get_destination(api,name) - if destination is not None and destination != current_destination['address']: - api.LocalLB.VirtualServer.set_destination_v2(virtual_servers = [name],destinations=[{'address': destination, 'port': current_destination['port']}]) - updated=True + current_destination = get_destination(api, name) + if destination is not None and destination != current_destination['address']: + api.LocalLB.VirtualServer.set_destination_v2( + virtual_servers=[name], + destinations=[{'address': destination, 'port': current_destination['port']}] + ) + updated = True return updated - except bigsuds.OperationFailed, e: - raise Exception('Error on setting destination : %s'% e ) + except bigsuds.OperationFailed as e: + raise Exception('Error on setting destination : %s' % e) -def set_port(api,name,port): - updated=False +def set_port(api, name, port): + updated = False try: - current_destination = get_destination(api,name) - if port is not None and port != current_destination['port']: - api.LocalLB.VirtualServer.set_destination_v2(virtual_servers = [name],destinations=[{'address': current_destination['address'], 'port': port}]) - updated=True + current_destination = get_destination(api, name) + if port is not None and port != current_destination['port']: + api.LocalLB.VirtualServer.set_destination_v2( + virtual_servers=[name], + destinations=[{'address': current_destination['address'], 'port': port}] + ) + updated = True return updated - except bigsuds.OperationFailed, e: - raise Exception('Error on setting port : %s'% e ) + except bigsuds.OperationFailed as e: + raise Exception('Error on setting port : %s' % e) + -def get_state(api,name): - return api.LocalLB.VirtualServer.get_enabled_state(virtual_servers = [name])[0] +def get_state(api, name): + return api.LocalLB.VirtualServer.get_enabled_state( + virtual_servers=[name] + )[0] -def set_state(api,name,state): - updated=False + +def set_state(api, name, state): + updated = False try: - current_state=get_state(api,name) + current_state = get_state(api, name) # We consider that being present is equivalent to enabled if state == 'present': - state='enabled' + state = 'enabled' if STATES[state] != current_state: - api.LocalLB.VirtualServer.set_enabled_state(virtual_servers=[name],states=[STATES[state]]) - updated=True + api.LocalLB.VirtualServer.set_enabled_state( + virtual_servers=[name], + states=[STATES[state]] + ) + updated = True return updated - except bigsuds.OperationFailed, e: - raise Exception('Error on setting state : %s'% e ) + except bigsuds.OperationFailed as e: + raise Exception('Error on setting state : %s' % e) + -def get_description(api,name): - return api.LocalLB.VirtualServer.get_description(virtual_servers = [name])[0] +def get_description(api, name): + return api.LocalLB.VirtualServer.get_description( + virtual_servers=[name] + )[0] -def set_description(api,name,description): - updated=False + +def set_description(api, name, description): + updated = False try: - current_description = get_description(api,name) + current_description = get_description(api, name) if description is not None and current_description != description: - api.LocalLB.VirtualServer.set_description(virtual_servers =[name],descriptions=[description]) - updated=True + api.LocalLB.VirtualServer.set_description( + virtual_servers=[name], + descriptions=[description] + ) + updated = True return updated - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed as e: raise Exception('Error on setting description : %s ' % e) -def get_persistence_profiles(api,name): - return api.LocalLB.VirtualServer.get_persistence_profile(virtual_servers = [name])[0] -def set_default_persistence_profiles(api,name,persistence_profile): - updated=False +def get_persistence_profiles(api, name): + return api.LocalLB.VirtualServer.get_persistence_profile( + virtual_servers=[name] + )[0] + + +def set_default_persistence_profiles(api, name, persistence_profile): + updated = False if persistence_profile is None: return updated try: - current_persistence_profiles = get_persistence_profiles(api,name) - default=None + current_persistence_profiles = get_persistence_profiles(api, name) + default = None for profile in current_persistence_profiles: if profile['default_profile']: - default=profile['profile_name'] + default = profile['profile_name'] break if default is not None and default != persistence_profile: - api.LocalLB.VirtualServer.remove_persistence_profile(virtual_servers=[name],profiles=[[{'profile_name':default,'default_profile' : True}]]) + api.LocalLB.VirtualServer.remove_persistence_profile( + virtual_servers=[name], + profiles=[[{'profile_name': default, 'default_profile': True}]] + ) if default != persistence_profile: - api.LocalLB.VirtualServer.add_persistence_profile(virtual_servers=[name],profiles=[[{'profile_name':persistence_profile,'default_profile' : True}]]) - updated=True + api.LocalLB.VirtualServer.add_persistence_profile( + virtual_servers=[name], + profiles=[[{'profile_name': persistence_profile, 'default_profile': True}]] + ) + updated = True return updated - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed as e: raise Exception('Error on setting default persistence profile : %s' % e) + def main(): argument_spec = f5_argument_spec() - argument_spec.update( dict( - state = dict(type='str', default='present', - choices=['present', 'absent', 'disabled', 'enabled']), - name = dict(type='str', required=True,aliases=['vs']), - destination = dict(type='str', aliases=['address', 'ip']), - port = dict(type='int'), - all_profiles = dict(type='list'), - all_rules = dict(type='list'), - pool=dict(type='str'), - description = dict(type='str'), - snat=dict(type='str'), - default_persistence_profile=dict(type='str') - ) - ) + argument_spec.update(dict( + state=dict(type='str', default='present', + choices=['present', 'absent', 'disabled', 'enabled']), + name=dict(type='str', required=True, aliases=['vs']), + destination=dict(type='str', aliases=['address', 'ip']), + port=dict(type='int'), + all_profiles=dict(type='list'), + all_rules=dict(type='list'), + pool=dict(type='str'), + description=dict(type='str'), + snat=dict(type='str'), + default_persistence_profile=dict(type='str') + )) module = AnsibleModule( - argument_spec = argument_spec, + argument_spec=argument_spec, supports_check_mode=True ) @@ -438,34 +515,34 @@ def main(): partition = module.params['partition'] validate_certs = module.params['validate_certs'] - name = fq_name(partition,module.params['name']) - destination=module.params['destination'] - port=module.params['port'] - all_profiles=fq_list_names(partition,module.params['all_profiles']) - all_rules=fq_list_names(partition,module.params['all_rules']) - pool=fq_name(partition,module.params['pool']) + name = fq_name(partition, module.params['name']) + destination = module.params['destination'] + port = module.params['port'] + all_profiles = fq_list_names(partition, module.params['all_profiles']) + all_rules = fq_list_names(partition, module.params['all_rules']) + pool = fq_name(partition, module.params['pool']) description = module.params['description'] snat = module.params['snat'] - default_persistence_profile=fq_name(partition,module.params['default_persistence_profile']) + default_persistence_profile = fq_name(partition, module.params['default_persistence_profile']) if 1 > port > 65535: module.fail_json(msg="valid ports must be in range 1 - 65535") - + try: api = bigip_api(server, user, password, validate_certs, port=server_port) result = {'changed': False} # default if state == 'absent': if not module.check_mode: - if vs_exists(api,name): + if vs_exists(api, name): # hack to handle concurrent runs of module # pool might be gone before we actually remove try: - vs_remove(api,name) - result = {'changed' : True, 'deleted' : name } - except bigsuds.OperationFailed, e: + vs_remove(api, name) + result = {'changed': True, 'deleted': name} + except bigsuds.OperationFailed as e: if "was not found" in str(e): - result['changed']= False + result['changed'] = False else: raise else: @@ -484,15 +561,15 @@ def main(): # this catches the exception and does something smart # about it! try: - vs_create(api,name,destination,port,pool) - set_profiles(api,name,all_profiles) - set_rules(api,name,all_rules) - set_snat(api,name,snat) - set_description(api,name,description) - set_default_persistence_profiles(api,name,default_persistence_profile) - set_state(api,name,state) + vs_create(api, name, destination, port, pool) + set_profiles(api, name, all_profiles) + set_rules(api, name, all_rules) + set_snat(api, name, snat) + set_description(api, name, description) + set_default_persistence_profiles(api, name, default_persistence_profile) + set_state(api, name, state) result = {'changed': True} - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed as e: raise Exception('Error on creating Virtual Server : %s' % e) else: # check-mode return value @@ -505,23 +582,23 @@ def main(): # Have a transaction for all the changes try: api.System.Session.start_transaction() - result['changed']|=set_destination(api,name,fq_name(partition,destination)) - result['changed']|=set_port(api,name,port) - result['changed']|=set_pool(api,name,pool) - result['changed']|=set_description(api,name,description) - result['changed']|=set_snat(api,name,snat) - result['changed']|=set_profiles(api,name,all_profiles) - result['changed']|=set_rules(api,name,all_rules) - result['changed']|=set_default_persistence_profiles(api,name,default_persistence_profile) - result['changed']|=set_state(api,name,state) + result['changed'] |= set_destination(api, name, fq_name(partition, destination)) + result['changed'] |= set_port(api, name, port) + result['changed'] |= set_pool(api, name, pool) + result['changed'] |= set_description(api, name, description) + result['changed'] |= set_snat(api, name, snat) + result['changed'] |= set_profiles(api, name, all_profiles) + result['changed'] |= set_rules(api, name, all_rules) + result['changed'] |= set_default_persistence_profiles(api, name, default_persistence_profile) + result['changed'] |= set_state(api, name, state) api.System.Session.submit_transaction() - except Exception,e: + except Exception as e: raise Exception("Error on updating Virtual Server : %s" % e) else: # check-mode return value result = {'changed': True} - except Exception, e: + except Exception as e: module.fail_json(msg="received exception: %s" % e) module.exit_json(**result) From feb08d9a84a9305017d2b1dd945c783de94a3868 Mon Sep 17 00:00:00 2001 From: _srsh_ Date: Wed, 29 Jun 2016 09:12:04 +0200 Subject: [PATCH 1757/2522] module was failing on python 2.7 after last update (#2459) --- packaging/language/maven_artifact.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packaging/language/maven_artifact.py b/packaging/language/maven_artifact.py index 6f6454fb590..f5a88c7b220 100644 --- a/packaging/language/maven_artifact.py +++ b/packaging/language/maven_artifact.py @@ -26,6 +26,7 @@ import hashlib import sys import posixpath +import urlparse from ansible.module_utils.basic import * from ansible.module_utils.urls import * try: @@ -231,9 +232,9 @@ def _uri_for_artifact(self, artifact, version=None): def _request(self, url, failmsg, f): url_to_use = url - parsed_url = urlparse.urlparse(url) + parsed_url = urlparse(url) if parsed_url.scheme=='s3': - parsed_url = urlparse.urlparse(url) + parsed_url = urlparse(url) bucket_name = parsed_url.netloc[:parsed_url.netloc.find('.')] key_name = parsed_url.path[1:] client = boto3.client('s3',aws_access_key_id=self.module.params.get('username', ''), aws_secret_access_key=self.module.params.get('password', '')) @@ -332,8 +333,10 @@ def main(): ) ) - - parsed_url = urlparse.urlparse(module.params["repository_url"]) + try: + parsed_url = urlparse(module.params["repository_url"]) + except AttributeError as e: + module.fail_json(msg='url parsing went wrong %s' % e) if parsed_url.scheme=='s3' and not HAS_BOTO: module.fail_json(msg='boto3 required for this module, when using s3:// repository URLs') @@ -384,4 +387,4 @@ def main(): if __name__ == '__main__': - main() + main() \ No newline at end of file From f5b7b3b2f2519f1d696b03b9c3f2e3c9b4fed2ac Mon Sep 17 00:00:00 2001 From: Nicholas Morsman Date: Wed, 29 Jun 2016 14:12:50 +0200 Subject: [PATCH 1758/2522] Bugfix influxdb/retention_policy: infinite retention is returned as 0 (#2453) * influxdb: retention_policy: infinite retention is returned as 0 from influxdb * influxdb: retention_policy: database_name argument should be required --- database/influxdb/influxdb_retention_policy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/database/influxdb/influxdb_retention_policy.py b/database/influxdb/influxdb_retention_policy.py index 4f960003bc3..ec4c32da215 100644 --- a/database/influxdb/influxdb_retention_policy.py +++ b/database/influxdb/influxdb_retention_policy.py @@ -125,7 +125,7 @@ def influxdb_argument_spec(): port=dict(default=8086, type='int'), username=dict(default='root', type='str'), password=dict(default='root', type='str', no_log=True), - database_name=dict(default=None, type='str') + database_name=dict(required=True, type='str') ) @@ -195,7 +195,7 @@ def alter_retention_policy(module, client, retention_policy): elif duration_lookup.group(2) == 'w': influxdb_duration_format = '%sh0m0s' % (int(duration_lookup.group(1)) * 24 * 7) elif duration == 'INF': - influxdb_duration_format = 'INF' + influxdb_duration_format = '0' if not retention_policy['duration'] == influxdb_duration_format or not retention_policy['replicaN'] == int(replication) or not retention_policy['default'] == default: if not module.check_mode: From 3afe1dcef5f8193a6fafad9233f5cb4bf7fe0c01 Mon Sep 17 00:00:00 2001 From: jpic Date: Wed, 29 Jun 2016 15:00:51 +0200 Subject: [PATCH 1759/2522] s/_needs_to_change_config/_needs_to_change_container_config/ --- cloud/lxd/lxd_container.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 0578eadb716..f0e71ac0e4f 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -546,16 +546,16 @@ def _apply_container_configs(self): 'devices': old_metadata['devices'], 'profiles': old_metadata['profiles'] } - if self._needs_to_change_config('architecture'): + if self._needs_to_change_container_config('architecture'): body_json['architecture'] = self.config['architecture'] - if self._needs_to_change_config('config'): + if self._needs_to_change_container_config('config'): for k, v in self.config['config'].items(): body_json['config'][k] = v - if self._needs_to_change_config('ephemeral'): + if self._needs_to_change_container_config('ephemeral'): body_json['ephemeral'] = self.config['ephemeral'] - if self._needs_to_change_config('devices'): + if self._needs_to_change_container_config('devices'): body_json['devices'] = self.config['devices'] - if self._needs_to_change_config('profiles'): + if self._needs_to_change_container_config('profiles'): body_json['profiles'] = self.config['profiles'] self._operate_and_wait('PUT', '/1.0/containers/{0}'.format(self.name), body_json=body_json) self.actions.append('apply_container_configs') From 61020a87dd57a56de26160ca8140dd5ada584bd3 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Thu, 30 Jun 2016 01:01:07 +0900 Subject: [PATCH 1760/2522] Add support for connecting via https with a client certificate --- cloud/lxd/lxd_container.py | 96 +++++++++++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 6 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index f0e71ac0e4f..04198c043ac 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -128,6 +128,34 @@ - The unix domain socket path for the LXD server. required: false default: /var/lib/lxd/unix.socket + url: + description: + - The https URL for the LXD server. + - If url is set, this module connects to the LXD server via https. + If url it not set, this module connects to the LXD server via + unix domain socket specified with unix_socket_path. + key_file: + description: + - The client certificate key file path. + required: false + default: > + '{}/.config/lxc/client.key'.format(os.environ['HOME']) + cert_file: + description: + - The client certificate file path. + required: false + default: > + '{}/.config/lxc/client.crt'.format(os.environ['HOME']) + trust_password: + description: + - The client trusted password. + - You need to set this password on the LXD server before + running this module using the following command. + lxc config set core.trust_password + See https://www.stgraber.org/2016/04/18/lxd-api-direct-interaction/ + - If trust_password is set, this module send a request for + authentication before sending any requests. + required: false notes: - Containers must have a unique name. If you attempt to create a container with a name that already existed in the users namespace the module will @@ -205,11 +233,17 @@ alias: "ubuntu/xenial/amd64" profiles: ["default"] +# An example for connecting to the LXD server using https - hosts: localhost connection: local tasks: - name: create macvlan profile lxd_container: + url: https://127.0.0.1:8443 + # These cert_file and key_file values are equal to the default values. + #cert_file: "{{ lookup('env', 'HOME') }}/.config/lxc/client.crt" + #key_file: "{{ lookup('env', 'HOME') }}/.config/lxc/client.key" + trust_password: mypassword type: profile name: macvlan state: present @@ -247,6 +281,8 @@ sample: ["create", "start"] """ +import os + try: import json except ImportError: @@ -254,11 +290,12 @@ # httplib/http.client connection using unix domain socket import socket +import ssl try: - from httplib import HTTPConnection + from httplib import HTTPConnection, HTTPSConnection except ImportError: # Python 3 - from http.client import HTTPConnection + from http.client import HTTPConnection, HTTPSConnection class UnixHTTPConnection(HTTPConnection): def __init__(self, path, timeout=None): @@ -270,6 +307,12 @@ def connect(self): sock.connect(self.path) self.sock = sock +from ansible.module_utils.urls import generic_urlparse +try: + from urlparse import urlparse +except ImportError: + # Python 3 + from url.parse import urlparse # LXD_ANSIBLE_STATES is a map of states that contain values of methods used # when a particular state is evoked. @@ -326,7 +369,17 @@ def __init__(self, module): self.force_stop = self.module.params['force_stop'] self.addresses = None self.unix_socket_path = self.module.params['unix_socket_path'] - self.connection = UnixHTTPConnection(self.unix_socket_path, timeout=self.timeout) + self.url = self.module.params.get('url', None) + self.key_file = self.module.params.get('key_file', None) + self.cert_file = self.module.params.get('cert_file', None) + self.trust_password = self.module.params.get('trust_password', None) + if self.url is None: + self.connection = UnixHTTPConnection(self.unix_socket_path, timeout=self.timeout) + else: + parts = generic_urlparse(urlparse(self.url)) + ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + ctx.load_cert_chain(self.cert_file, keyfile=self.key_file) + self.connection = HTTPSConnection(parts.get('netloc'), context=ctx, timeout=self.timeout) self.logs = [] self.actions = [] @@ -337,6 +390,10 @@ def _build_config(self): if param_val is not None: self.config[attr] = param_val + def _authenticate(self): + body_json = {'type': 'client', 'password': self.trust_password} + self._send_request('POST', '/1.0/certificates', body_json=body_json) + def _send_request(self, method, url, body_json=None, ok_error_codes=None): try: body = json.dumps(body_json) @@ -359,8 +416,18 @@ def _send_request(self, method, url, body_json=None, ok_error_codes=None): logs=self.logs ) return resp_json - except socket.error: - self.module.fail_json(msg='cannot connect to the LXD server', unix_socket_path=self.unix_socket_path) + except socket.error as e: + if self.url is None: + self.module.fail_json( + msg='cannot connect to the LXD server', + unix_socket_path=self.unix_socket_path, error=e + ) + else: + self.module.fail_json( + msg='cannot connect to the LXD server', + url=self.url, key_file=self.key_file, cert_file=self.cert_file, + error=e + ) def _operate_and_wait(self, method, path, body_json=None): resp_json = self._send_request(method, path, body_json=body_json) @@ -604,7 +671,7 @@ def _create_profile(self): self.actions.append('create') def _rename_profile(self): - config = { 'name': self.new_name } + config = {'name': self.new_name} self._send_request('POST', '/1.0/profiles/{}'.format(self.name), config) self.actions.append('rename') self.name = self.new_name @@ -636,6 +703,9 @@ def _delete_profile(self): def run(self): """Run the main method.""" + if self.trust_password is not None: + self._authenticate() + if self.type == 'container': self.old_container_json = self._get_container_json() self.old_state = self._container_json_to_module_state(self.old_container_json) @@ -715,6 +785,20 @@ def main(): unix_socket_path=dict( type='str', default='/var/lib/lxd/unix.socket' + ), + url=dict( + type='str', + ), + key_file=dict( + type='str', + default='{}/.config/lxc/client.key'.format(os.environ['HOME']) + ), + cert_file=dict( + type='str', + default='{}/.config/lxc/client.crt'.format(os.environ['HOME']) + ), + trust_password=dict( + type='str', ) ), supports_check_mode=False, From eb7488854b93c4398e5a9afd245d83a857e9b8d3 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Thu, 30 Jun 2016 01:59:37 +0900 Subject: [PATCH 1761/2522] Check argument choices according to type value --- cloud/lxd/lxd_container.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 04198c043ac..9d4c7420aef 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -317,7 +317,6 @@ def connect(self): # LXD_ANSIBLE_STATES is a map of states that contain values of methods used # when a particular state is evoked. LXD_ANSIBLE_STATES = { - 'present': '', # TODO: Separate state for profile 'started': '_started', 'stopped': '_stopped', 'restarted': '_restarted', @@ -325,6 +324,11 @@ def connect(self): 'frozen': '_frozen' } +# PROFILE_STATES is a list for states supported for type=profiles +PROFILES_STATES = [ + 'present', 'absent' +] + # ANSIBLE_LXD_STATES is a map of states of lxd containers to the Ansible # lxc_container module state parameter value. ANSIBLE_LXD_STATES = { @@ -361,8 +365,13 @@ def __init__(self, module): self.name = self.module.params['name'] self.type = self.module.params['type'] self._build_config() - # TODO: check state value according to type + self.state = self.module.params['state'] + if self.type == 'container': + self._check_argument_choices('state', self.state, LXD_ANSIBLE_STATES.keys()) + elif self.type == 'profile': + self._check_argument_choices('state', self.state, PROFILES_STATES) + self.new_name = self.module.params.get('new_name', None) self.timeout = self.module.params['timeout'] self.wait_for_ipv4_addresses = self.module.params['wait_for_ipv4_addresses'] @@ -383,6 +392,12 @@ def __init__(self, module): self.logs = [] self.actions = [] + def _check_argument_choices(self, name, value, choices): + if value not in choices: + choices_str=",".join([str(c) for c in choices]) + msg="value of %s must be one of: %s, got: %s" % (name, choices_str, value) + self.module.fail_json(msg=msg) + def _build_config(self): self.config = {} for attr in CONFIG_PARAMS[self.type]: @@ -767,7 +782,7 @@ def main(): type='dict', ), state=dict( - choices=LXD_ANSIBLE_STATES.keys(), + choices=list(set(LXD_ANSIBLE_STATES.keys()) | set(PROFILES_STATES)), default='started' ), timeout=dict( From 39d0088af8b4d03df38b464d9cb87864cd1c2885 Mon Sep 17 00:00:00 2001 From: Patrik Lundin Date: Wed, 29 Jun 2016 23:27:31 +0200 Subject: [PATCH 1762/2522] openbsd_pkg: support "pkgname%branch" syntax. * Such package names requires at least OpenBSD 6.0. * Rework get_package_state() to use 'pkg_info -Iq inst:' instead of 'pkg_info -e' because it understands the branch syntax. It also means we can get rid of some additional special handling. This was suggested by Marc Espie: http://marc.info/?l=openbsd-tech&m=146659756711614&w=2 * Drop get_current_name() because the use of 'pkg_info -Iq inst:' in get_package_state() means we already have that information available without needing to do custom parsing. This was also necessary because a name such as "postfix%stable" does not in itself contain the version information necessary for the custom parsing. pkg_info -Iq translates such a name to the actual package name seamlessly. * Add support for finding more than one package for the supplied package name which may happen if we only supply a stem. --- packaging/os/openbsd_pkg.py | 95 +++++++++++++++++++------------------ 1 file changed, 48 insertions(+), 47 deletions(-) diff --git a/packaging/os/openbsd_pkg.py b/packaging/os/openbsd_pkg.py index 9700e831892..354b7463093 100644 --- a/packaging/os/openbsd_pkg.py +++ b/packaging/os/openbsd_pkg.py @@ -19,10 +19,13 @@ # along with Ansible. If not, see . import os +import platform import re import shlex import sqlite3 +from distutils.version import StrictVersion + DOCUMENTATION = ''' --- module: openbsd_pkg @@ -82,6 +85,9 @@ # Specify the default flavour to avoid ambiguity errors - openbsd_pkg: name=vim-- state=present +# Specify a package branch (requires at least OpenBSD 6.0) +- openbsd_pkg: name=python%3.5 state=present + # Update all packages on the system - openbsd_pkg: name=* state=latest ''' @@ -94,47 +100,22 @@ def execute_command(cmd, module): cmd_args = shlex.split(cmd) return module.run_command(cmd_args) -# Function used for getting the name of a currently installed package. -def get_current_name(name, pkg_spec, module): - info_cmd = 'pkg_info' - (rc, stdout, stderr) = execute_command("%s" % (info_cmd), module) - if rc != 0: - return (rc, stdout, stderr) - - if pkg_spec['version']: - pattern = "^%s" % name - elif pkg_spec['flavor']: - pattern = "^%s-.*-%s\s" % (pkg_spec['stem'], pkg_spec['flavor']) - else: - pattern = "^%s-" % pkg_spec['stem'] - - module.debug("get_current_name(): pattern = %s" % pattern) - - for line in stdout.splitlines(): - module.debug("get_current_name: line = %s" % line) - match = re.search(pattern, line) - if match: - current_name = line.split()[0] - - return current_name - # Function used to find out if a package is currently installed. def get_package_state(name, pkg_spec, module): - info_cmd = 'pkg_info -e' + info_cmd = 'pkg_info -Iq' - if pkg_spec['version']: - command = "%s %s" % (info_cmd, name) - elif pkg_spec['flavor']: - command = "%s %s-*-%s" % (info_cmd, pkg_spec['stem'], pkg_spec['flavor']) - else: - command = "%s %s-*" % (info_cmd, pkg_spec['stem']) + command = "%s inst:%s" % (info_cmd, name) rc, stdout, stderr = execute_command(command, module) - if (stderr): + if stderr: module.fail_json(msg="failed in get_package_state(): " + stderr) - if rc == 0: + if stdout: + # If the requested package name is just a stem, like "python", we may + # find multiple packages with that name. + pkg_spec['installed_names'] = [line.rstrip() for line in stdout.splitlines()] + module.debug("get_package_state(): installed_names = %s" % pkg_spec['installed_names']) return True else: return False @@ -173,8 +154,14 @@ def package_present(name, installed_state, pkg_spec, module): # specific version is supplied or not. # # When a specific version is supplied the return code will be 0 when - # a package is found and 1 when it is not, if a version is not - # supplied the tool will exit 0 in both cases: + # a package is found and 1 when it is not. If a version is not + # supplied the tool will exit 0 in both cases. + # + # It is important to note that "version" relates to the + # packages-specs(7) notion of a version. If using the branch syntax + # (like "python%3.5") the version number is considered part of the + # stem, and the pkg_add behavior behaves the same as if the name did + # not contain a version (which it strictly speaking does not). if pkg_spec['version'] or build is True: # Depend on the return code. module.debug("package_present(): depending on return code") @@ -231,25 +218,21 @@ def package_latest(name, installed_state, pkg_spec, module): if installed_state is True: - # Fetch name of currently installed package. - pre_upgrade_name = get_current_name(name, pkg_spec, module) - - module.debug("package_latest(): pre_upgrade_name = %s" % pre_upgrade_name) - # Attempt to upgrade the package. (rc, stdout, stderr) = execute_command("%s %s" % (upgrade_cmd, name), module) # Look for output looking something like "nmap-6.01->6.25: ok" to see if # something changed (or would have changed). Use \W to delimit the match # from progress meter output. - match = re.search("\W%s->.+: ok\W" % pre_upgrade_name, stdout) - if match: - if module.check_mode: - module.exit_json(changed=True) + changed = False + for installed_name in pkg_spec['installed_names']: + module.debug("package_latest(): checking for pre-upgrade package name: %s" % installed_name) + match = re.search("\W%s->.+: ok\W" % installed_name, stdout) + if match: + if module.check_mode: + module.exit_json(changed=True) - changed = True - else: - changed = False + changed = True # FIXME: This part is problematic. Based on the issues mentioned (and # handled) in package_present() it is not safe to blindly trust stderr @@ -301,7 +284,12 @@ def package_absent(name, installed_state, module): # Function used to parse the package name based on packages-specs(7). # The general name structure is "stem-version[-flavors]". +# +# Names containing "%" are a special variation not part of the +# packages-specs(7) syntax. See pkg_add(1) on OpenBSD 6.0 or later for a +# description. def parse_package_name(name, pkg_spec, module): + module.debug("parse_package_name(): parsing name: %s" % name) # Do some initial matches so we can base the more advanced regex on that. version_match = re.search("-[0-9]", name) versionless_match = re.search("--", name) @@ -350,6 +338,19 @@ def parse_package_name(name, pkg_spec, module): else: module.fail_json(msg="Unable to parse package name at else: " + name) + # If the stem contains an "%" then it needs special treatment. + branch_match = re.search("%", pkg_spec['stem']) + if branch_match: + + branch_release = "6.0" + + if version_match or versionless_match: + module.fail_json(msg="Package name using 'branch' syntax also has a version or is version-less: " + name) + if StrictVersion(platform.release()) < StrictVersion(branch_release): + module.fail_json(msg="Package name using 'branch' syntax requires at least OpenBSD %s: %s" % (branch_release, name)) + + pkg_spec['style'] = 'branch' + # Sanity check that there are no trailing dashes in flavor. # Try to stop strange stuff early so we can be strict later. if pkg_spec['flavor']: From f4b40926b4f8bb847b950ba832f96b0860021725 Mon Sep 17 00:00:00 2001 From: Patrik Lundin Date: Wed, 29 Jun 2016 23:57:31 +0200 Subject: [PATCH 1763/2522] openbsd_pkg: fix build=true corner case. * Fix bug where we were actually checking for the availability of the requested package name and not 'sqlports' even if that was the goal. * Add check that the sqlports database file exists before using it. * Sprinkle some debug messages for an easier time following the code when developing. --- packaging/os/openbsd_pkg.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packaging/os/openbsd_pkg.py b/packaging/os/openbsd_pkg.py index 354b7463093..ff9ef672ca7 100644 --- a/packaging/os/openbsd_pkg.py +++ b/packaging/os/openbsd_pkg.py @@ -365,9 +365,14 @@ def get_package_source_path(name, pkg_spec, module): return 'databases/sqlports' else: # try for an exact match first - conn = sqlite3.connect('/usr/local/share/sqlports') + sqlports_db_file = '/usr/local/share/sqlports' + if not os.path.isfile(sqlports_db_file): + module.fail_json(msg="sqlports file '%s' is missing" % sqlports_db_file) + + conn = sqlite3.connect(sqlports_db_file) first_part_of_query = 'SELECT fullpkgpath, fullpkgname FROM ports WHERE fullpkgname' query = first_part_of_query + ' = ?' + module.debug("package_package_source_path(): query: %s" % query) cursor = conn.execute(query, (name,)) results = cursor.fetchall() @@ -377,11 +382,14 @@ def get_package_source_path(name, pkg_spec, module): query = first_part_of_query + ' LIKE ?' if pkg_spec['flavor']: looking_for += pkg_spec['flavor_separator'] + pkg_spec['flavor'] + module.debug("package_package_source_path(): flavor query: %s" % query) cursor = conn.execute(query, (looking_for,)) elif pkg_spec['style'] == 'versionless': query += ' AND fullpkgname NOT LIKE ?' + module.debug("package_package_source_path(): versionless query: %s" % query) cursor = conn.execute(query, (looking_for, "%s-%%" % looking_for,)) else: + module.debug("package_package_source_path(): query: %s" % query) cursor = conn.execute(query, (looking_for,)) results = cursor.fetchall() @@ -465,8 +473,9 @@ def main(): # build sqlports if its not installed yet pkg_spec = {} parse_package_name('sqlports', pkg_spec, module) - installed_state = get_package_state(name, pkg_spec, module) + installed_state = get_package_state('sqlports', pkg_spec, module) if not installed_state: + module.debug("main(): installing sqlports") package_present('sqlports', installed_state, pkg_spec, module) if name == '*': From 7dcac77df5ea7f7d4ad3ce785a26472e65d8ff07 Mon Sep 17 00:00:00 2001 From: Patrik Lundin Date: Thu, 30 Jun 2016 00:38:57 +0200 Subject: [PATCH 1764/2522] openbsd_pkg: no need to call .rstrip. --- packaging/os/openbsd_pkg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/os/openbsd_pkg.py b/packaging/os/openbsd_pkg.py index ff9ef672ca7..305b7c06454 100644 --- a/packaging/os/openbsd_pkg.py +++ b/packaging/os/openbsd_pkg.py @@ -114,7 +114,7 @@ def get_package_state(name, pkg_spec, module): if stdout: # If the requested package name is just a stem, like "python", we may # find multiple packages with that name. - pkg_spec['installed_names'] = [line.rstrip() for line in stdout.splitlines()] + pkg_spec['installed_names'] = [name for name in stdout.splitlines()] module.debug("get_package_state(): installed_names = %s" % pkg_spec['installed_names']) return True else: From 2490f87522bf11d06c588bca8e6d9e667fa73257 Mon Sep 17 00:00:00 2001 From: Stephen Granger Date: Wed, 29 Jun 2016 22:16:05 -0700 Subject: [PATCH 1765/2522] Included an example using the jinja 2 map function to return a list of (#2458) subnet_ids. --- cloud/amazon/ec2_vpc_subnet_facts.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cloud/amazon/ec2_vpc_subnet_facts.py b/cloud/amazon/ec2_vpc_subnet_facts.py index 804c48ef50f..c3637292248 100644 --- a/cloud/amazon/ec2_vpc_subnet_facts.py +++ b/cloud/amazon/ec2_vpc_subnet_facts.py @@ -53,6 +53,21 @@ filters: vpc-id: vpc-abcdef00 +# Gather facts about a set of VPC subnets, publicA, publicB and publicC within a +# VPC with ID vpc-abcdef00 and then use the jinja map function to return the +# subnet_ids as a list. + +- ec2_vpc_subnet_facts: + filters: + vpc-id: vpc-abcdef00 + "tag:Name": "{{ item }}" + with_items: + - publicA + - publicB + - publicC + +- set_fact: + subnet_ids: "{{ subnet_facts.results|map(attribute='subnets.0.id')|list }}" ''' try: From 9231241c8bbf6a5eb9721cad88c08df6774691f3 Mon Sep 17 00:00:00 2001 From: Shane Koster Date: Thu, 30 Jun 2016 00:17:49 -0500 Subject: [PATCH 1766/2522] Pass the lxc_path when checking if container exists (#2457) fixes #887 --- cloud/lxc/lxc_container.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index bd77425e3e7..906b1d754c6 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -603,6 +603,7 @@ def __init__(self, module): self.state = self.module.params.get('state', None) self.state_change = False self.lxc_vg = None + self.lxc_path = self.module.params.get('lxc_path', None) self.container_name = self.module.params['name'] self.container = self.get_container_bind() self.archive_info = None @@ -627,7 +628,7 @@ def _roundup(num): return num @staticmethod - def _container_exists(container_name): + def _container_exists(container_name, lxc_path=None): """Check if a container exists. :param container_name: Name of the container. @@ -635,7 +636,7 @@ def _container_exists(container_name): :returns: True or False if the container is found. :rtype: ``bol`` """ - if [i for i in lxc.list_containers() if i == container_name]: + if [i for i in lxc.list_containers(config_path=lxc_path) if i == container_name]: return True else: return False @@ -944,7 +945,7 @@ def _get_state(self): :rtype: ``str`` """ - if self._container_exists(container_name=self.container_name): + if self._container_exists(container_name=self.container_name, lxc_path=self.lxc_path): return str(self.container.state).lower() else: return str('absent') @@ -1009,7 +1010,7 @@ def _check_clone(self): clone_name = self.module.params.get('clone_name') if clone_name: - if not self._container_exists(container_name=clone_name): + if not self._container_exists(container_name=clone_name, lxc_path=self.lxc_path): self.clone_info = { 'cloned': self._container_create_clone() } @@ -1026,7 +1027,7 @@ def _destroyed(self, timeout=60): """ for _ in xrange(timeout): - if not self._container_exists(container_name=self.container_name): + if not self._container_exists(container_name=self.container_name, lxc_path=self.lxc_path): break # Check if the container needs to have an archive created. @@ -1065,7 +1066,7 @@ def _frozen(self, count=0): """ self.check_count(count=count, method='frozen') - if self._container_exists(container_name=self.container_name): + if self._container_exists(container_name=self.container_name, lxc_path=self.lxc_path): self._execute_command() # Perform any configuration updates @@ -1102,7 +1103,7 @@ def _restarted(self, count=0): """ self.check_count(count=count, method='restart') - if self._container_exists(container_name=self.container_name): + if self._container_exists(container_name=self.container_name, lxc_path=self.lxc_path): self._execute_command() # Perform any configuration updates @@ -1135,7 +1136,7 @@ def _stopped(self, count=0): """ self.check_count(count=count, method='stop') - if self._container_exists(container_name=self.container_name): + if self._container_exists(container_name=self.container_name, lxc_path=self.lxc_path): self._execute_command() # Perform any configuration updates @@ -1165,7 +1166,7 @@ def _started(self, count=0): """ self.check_count(count=count, method='start') - if self._container_exists(container_name=self.container_name): + if self._container_exists(container_name=self.container_name, lxc_path=self.lxc_path): container_state = self._get_state() if container_state == 'running': pass From 5dc0b934eb0cac2ff75dc6b71b5f3900b113022f Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Thu, 30 Jun 2016 21:02:16 +0900 Subject: [PATCH 1767/2522] Use timeout only for chaging state and getting IPv4 addresses --- cloud/lxd/lxd_container.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 9d4c7420aef..9156de79544 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -104,7 +104,7 @@ default: started timeout: description: - - A timeout of one LXC REST API call. + - A timeout for changing the state of the container. - This is also used as a timeout for waiting until IPv4 addresses are set to the all network interfaces in the container after starting or restarting. @@ -383,12 +383,12 @@ def __init__(self, module): self.cert_file = self.module.params.get('cert_file', None) self.trust_password = self.module.params.get('trust_password', None) if self.url is None: - self.connection = UnixHTTPConnection(self.unix_socket_path, timeout=self.timeout) + self.connection = UnixHTTPConnection(self.unix_socket_path) else: parts = generic_urlparse(urlparse(self.url)) ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ctx.load_cert_chain(self.cert_file, keyfile=self.key_file) - self.connection = HTTPSConnection(parts.get('netloc'), context=ctx, timeout=self.timeout) + self.connection = HTTPSConnection(parts.get('netloc'), context=ctx) self.logs = [] self.actions = [] @@ -447,12 +447,12 @@ def _send_request(self, method, url, body_json=None, ok_error_codes=None): def _operate_and_wait(self, method, path, body_json=None): resp_json = self._send_request(method, path, body_json=body_json) if resp_json['type'] == 'async': - url = '{0}/wait?timeout={1}'.format(resp_json['operation'], self.timeout) + url = '{0}/wait'.format(resp_json['operation']) resp_json = self._send_request('GET', url) if resp_json['metadata']['status'] != 'Success': self.module.fail_json( msg='error response for waiting opearation', - request={'method': method, 'url': url, 'timeout': self.timeout}, + request={'method': method, 'url': url}, response={'json': resp_json}, logs=self.logs ) From 308e0984f7c19aeb00d90506dd81a93ef87aa281 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Thu, 30 Jun 2016 21:14:48 +0900 Subject: [PATCH 1768/2522] Add debug parameetr and put logs in result when debug is true or module failed --- cloud/lxd/lxd_container.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 9156de79544..33965a83d20 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -156,6 +156,12 @@ - If trust_password is set, this module send a request for authentication before sending any requests. required: false + debug: + description: + - If this flag is true, the logs key are added to the result object + which keeps all the requests and responses for calling APIs. + required: false + default: false notes: - Containers must have a unique name. If you attempt to create a container with a name that already existed in the users namespace the module will @@ -263,19 +269,20 @@ type: object contains: addresses: - description: mapping from the network device name to a list of IPv4 addresses in the container + description: Mapping from the network device name to a list of IPv4 addresses in the container returned: when state is started or restarted type: object sample: {"eth0": ["10.155.92.191"]} old_state: - description: the old state of the container + description: The old state of the container returned: when state is started or restarted sample: "stopped" logs: - descriptions: the logs of requests and responses + descriptions: The logs of requests and responses. This key exists only when you set the + debug paramter to true or this module failed. returned: when requests are sent actions: - description: list of actions performed for the container + description: List of actions performed for the container. returned: success type: list sample: ["create", "start"] @@ -382,6 +389,7 @@ def __init__(self, module): self.key_file = self.module.params.get('key_file', None) self.cert_file = self.module.params.get('cert_file', None) self.trust_password = self.module.params.get('trust_password', None) + self.debug = self.module.params['debug'] if self.url is None: self.connection = UnixHTTPConnection(self.unix_socket_path) else: @@ -735,9 +743,10 @@ def run(self): result_json = { 'changed': state_changed, 'old_state': self.old_state, - 'logs': self.logs, 'actions': self.actions } + if self.debug: + result_json['logs'] = self.logs if self.addresses is not None: result_json['addresses'] = self.addresses self.module.exit_json(**result_json) @@ -814,6 +823,10 @@ def main(): ), trust_password=dict( type='str', + ), + debug=dict( + type='bool', + default=False ) ), supports_check_mode=False, From cc8b54d3cc31da9fd36453c2fa3865c01a56cab3 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Thu, 30 Jun 2016 21:42:05 +0900 Subject: [PATCH 1769/2522] Use metadata.err for error msg and return logs only when debug is true --- cloud/lxd/lxd_container.py | 54 ++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 33965a83d20..08171a52cd7 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -278,9 +278,8 @@ returned: when state is started or restarted sample: "stopped" logs: - descriptions: The logs of requests and responses. This key exists only when you set the - debug paramter to true or this module failed. - returned: when requests are sent + descriptions: The logs of requests and responses. + returned: when the debug parameter is true and any requests were sent. actions: description: List of actions performed for the container. returned: success @@ -432,12 +431,12 @@ def _send_request(self, method, url, body_json=None, ok_error_codes=None): if resp_type == 'error': if ok_error_codes is not None and resp_json['error_code'] in ok_error_codes: return resp_json - self.module.fail_json( - msg='error response', - request={'method': method, 'url': url, 'json': body_json, 'timeout': self.timeout}, - response={'json': resp_json}, - logs=self.logs - ) + fail_params = { + 'msg': self._get_err_from_resp_json(resp_json), + } + if self.debug: + fail_params['logs'] = self.logs + self.module.fail_json(**fail_params) return resp_json except socket.error as e: if self.url is None: @@ -452,18 +451,27 @@ def _send_request(self, method, url, body_json=None, ok_error_codes=None): error=e ) + @staticmethod + def _get_err_from_resp_json(resp_json): + metadata = resp_json.get('metadata', None) + if metadata is not None: + err = metadata.get('err', None) + if err is None: + err = resp_json.get('error', None) + return err + def _operate_and_wait(self, method, path, body_json=None): resp_json = self._send_request(method, path, body_json=body_json) if resp_json['type'] == 'async': url = '{0}/wait'.format(resp_json['operation']) resp_json = self._send_request('GET', url) if resp_json['metadata']['status'] != 'Success': - self.module.fail_json( - msg='error response for waiting opearation', - request={'method': method, 'url': url}, - response={'json': resp_json}, - logs=self.logs - ) + fail_params = { + 'msg': self._get_err_from_resp_json(resp_json), + } + if self.debug: + fail_params['logs'] = self.logs + self.module.fail_json(**fail_params) return resp_json def _get_container_json(self): @@ -541,12 +549,14 @@ def _get_addresses(self): return state_changed = len(self.actions) > 0 - self.module.fail_json( - failed=True, - msg='timeout for getting IPv4 addresses', - changed=state_changed, - actions=self.actions, - logs=self.logs) + fail_params = { + 'msg': 'timeout for getting IPv4 addresses', + 'changed': state_changed, + 'actions': self.actions + } + if self.debug: + fail_params['logs'] = self.logs + self.module.fail_json(**fail_params) def _started(self): if self.old_state == 'absent': @@ -669,7 +679,6 @@ def _update_profile(self): self._create_profile() else: self.module.fail_json( - failed=True, msg='new_name must not be set when the profile does not exist and the specified state is present', changed=False) else: @@ -683,7 +692,6 @@ def _update_profile(self): self._delete_profile() else: self.module.fail_json( - failed=True, msg='new_name must not be set when the profile exists and the specified state is absent', changed=False) From 8ba41ee6a244cb07b8b18ed7deadd189f591346b Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Thu, 30 Jun 2016 22:36:58 +0900 Subject: [PATCH 1770/2522] Unify unix_socket_path and url to the url parameter --- cloud/lxd/lxd_container.py | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 08171a52cd7..30729ecf09c 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -123,17 +123,11 @@ when it stops or restarts the container. required: false default: false - unix_socket_path: - description: - - The unix domain socket path for the LXD server. - required: false - default: /var/lib/lxd/unix.socket url: description: - - The https URL for the LXD server. - - If url is set, this module connects to the LXD server via https. - If url it not set, this module connects to the LXD server via - unix domain socket specified with unix_socket_path. + - The unix domain socket path or the https URL for the LXD server. + required: false + default: unix:/var/lib/lxd/unix.socket key_file: description: - The client certificate key file path. @@ -383,19 +377,21 @@ def __init__(self, module): self.wait_for_ipv4_addresses = self.module.params['wait_for_ipv4_addresses'] self.force_stop = self.module.params['force_stop'] self.addresses = None - self.unix_socket_path = self.module.params['unix_socket_path'] - self.url = self.module.params.get('url', None) + self.url = self.module.params['url'] self.key_file = self.module.params.get('key_file', None) self.cert_file = self.module.params.get('cert_file', None) self.trust_password = self.module.params.get('trust_password', None) self.debug = self.module.params['debug'] - if self.url is None: - self.connection = UnixHTTPConnection(self.unix_socket_path) - else: + if self.url.startswith('https:'): parts = generic_urlparse(urlparse(self.url)) ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ctx.load_cert_chain(self.cert_file, keyfile=self.key_file) self.connection = HTTPSConnection(parts.get('netloc'), context=ctx) + elif self.url.startswith('unix:'): + unix_socket_path = self.url[len('unix:'):] + self.connection = UnixHTTPConnection(unix_socket_path) + else: + self.module.fail_json(msg='URL scheme must be unix: or https:') self.logs = [] self.actions = [] @@ -814,12 +810,9 @@ def main(): type='bool', default=False ), - unix_socket_path=dict( - type='str', - default='/var/lib/lxd/unix.socket' - ), url=dict( type='str', + default='unix:/var/lib/lxd/unix.socket' ), key_file=dict( type='str', From 93540b726969cfe35c4499ce87c8430106b0887e Mon Sep 17 00:00:00 2001 From: David Fischer Date: Thu, 30 Jun 2016 15:42:14 +0200 Subject: [PATCH 1771/2522] Fix 'function' has no attribute 'errno' (#2502) --- system/known_hosts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/known_hosts.py b/system/known_hosts.py index e7d9df59ccf..a355b6db5fe 100644 --- a/system/known_hosts.py +++ b/system/known_hosts.py @@ -124,7 +124,7 @@ def enforce_state(module, params): try: inf=open(path,"r") except IOError: - e = get_exception + e = get_exception() if e.errno == errno.ENOENT: inf=None else: From d42e51a88435d747972285430a0230cbd321b042 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 30 Jun 2016 09:52:18 -0400 Subject: [PATCH 1772/2522] added missing msg= fixes #2498 --- clustering/kubernetes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clustering/kubernetes.py b/clustering/kubernetes.py index 7fc2ad92174..183fb0b837e 100644 --- a/clustering/kubernetes.py +++ b/clustering/kubernetes.py @@ -363,7 +363,7 @@ def main(): try: url = target_endpoint + KIND_URL[kind] except KeyError: - module.fail_json("invalid resource kind specified in the data: '%s'" % kind) + module.fail_json(msg="invalid resource kind specified in the data: '%s'" % kind) url = url.replace("{namespace}", namespace) else: url = target_endpoint From bf07cf8d3bd2fc6af23eb724bb34bc189d7bd6d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Thu, 30 Jun 2016 16:37:21 +0200 Subject: [PATCH 1773/2522] fix typo in get_exception(), extends fix #2502 (#2507) introduced with 9e277aabb033b3ec823b4a2db7c0f7e315eaaf0b --- system/cronvar.py | 4 ++-- system/crypttab.py | 2 +- system/getent.py | 2 +- system/gluster_volume.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/system/cronvar.py b/system/cronvar.py index df172be8bdc..21f92be964e 100644 --- a/system/cronvar.py +++ b/system/cronvar.py @@ -150,7 +150,7 @@ def read(self): self.lines = f.read().splitlines() f.close() except IOError: - e = get_exception + e = get_exception() # cron file does not exist return except: @@ -207,7 +207,7 @@ def remove_variable_file(self): os.unlink(self.cron_file) return True except OSError: - e = get_exception + e = get_exception() # cron file does not exist return False except: diff --git a/system/crypttab.py b/system/crypttab.py index e8e0b5835f4..ea9698a12c2 100644 --- a/system/crypttab.py +++ b/system/crypttab.py @@ -130,7 +130,7 @@ def main(): crypttab = Crypttab(path) existing_line = crypttab.match(name) except Exception: - e = get_exception + e = get_exception() module.fail_json(msg="failed to open and parse crypttab file: %s" % e, **module.params) diff --git a/system/getent.py b/system/getent.py index 85893f13e8a..37bfc244dea 100644 --- a/system/getent.py +++ b/system/getent.py @@ -114,7 +114,7 @@ def main(): try: rc, out, err = module.run_command(cmd) except Exception: - e = get_exception + e = get_exception() module.fail_json(msg=str(e)) msg = "Unexpected failure!" diff --git a/system/gluster_volume.py b/system/gluster_volume.py index 1f968271154..f7fae041299 100644 --- a/system/gluster_volume.py +++ b/system/gluster_volume.py @@ -150,7 +150,7 @@ def run_gluster(gargs, **kwargs): if rc != 0: module.fail_json(msg='error running gluster (%s) command (rc=%d): %s' % (' '.join(args), rc, out or err)) except Exception: - e = get_exception + e = get_exception() module.fail_json(msg='error running gluster (%s) command: %s' % (' '.join(args), str(e))) return out From 7d930930e8f0dcd95e94e7ceec91b5011c096636 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Fri, 1 Jul 2016 00:03:46 +0900 Subject: [PATCH 1774/2522] Create LXDClient and LXDClientException --- cloud/lxd/__init__.py | 132 ++++++++++++++++++++++ cloud/lxd/lxd_container.py | 220 +++++++++++-------------------------- 2 files changed, 197 insertions(+), 155 deletions(-) diff --git a/cloud/lxd/__init__.py b/cloud/lxd/__init__.py index e69de29bb2d..23161ef6312 100644 --- a/cloud/lxd/__init__.py +++ b/cloud/lxd/__init__.py @@ -0,0 +1,132 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Hiroaki Nakamura +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +try: + import json +except ImportError: + import simplejson as json + +# httplib/http.client connection using unix domain socket +import socket +import ssl +try: + from httplib import HTTPConnection, HTTPSConnection +except ImportError: + # Python 3 + from http.client import HTTPConnection, HTTPSConnection + +class UnixHTTPConnection(HTTPConnection): + def __init__(self, path): + HTTPConnection.__init__(self, 'localhost') + self.path = path + + def connect(self): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(self.path) + self.sock = sock + +from ansible.module_utils.urls import generic_urlparse +try: + from urlparse import urlparse +except ImportError: + # Python 3 + from url.parse import urlparse + +class LXDClientException(Exception): + def __init__(self, msg, **kwargs): + self.msg = msg + self.kwargs = kwargs + +class LXDClient(object): + def __init__(self, url, key_file=None, cert_file=None, debug=False): + """LXD Client. + + :param url: The URL of the LXD server. (e.g. unix:/var/lib/lxd/unix.socket or https://127.0.0.1) + :type url: ``str`` + :param key_file: The path of the client certificate key file. + :type key_file: ``str`` + :param cert_file: The path of the client certificate file. + :type cert_file: ``str`` + :param debug: The debug flag. The request and response are stored in logs when debug is true. + :type debug: ``bool`` + """ + self.url = url + self.debug = debug + self.logs = [] + if url.startswith('https:'): + self.cert_file = cert_file + self.key_file = key_file + parts = generic_urlparse(urlparse(self.url)) + ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + ctx.load_cert_chain(cert_file, keyfile=key_file) + self.connection = HTTPSConnection(parts.get('netloc'), context=ctx) + elif url.startswith('unix:'): + unix_socket_path = url[len('unix:'):] + self.connection = UnixHTTPConnection(unix_socket_path) + else: + raise LXDClientException('URL scheme must be unix: or https:') + + def do(self, method, url, body_json=None, ok_error_codes=None, timeout=None): + resp_json = self._send_request(method, url, body_json=body_json, ok_error_codes=ok_error_codes, timeout=timeout) + if resp_json['type'] == 'async': + url = '{0}/wait'.format(resp_json['operation']) + resp_json = self._send_request('GET', url) + if resp_json['metadata']['status'] != 'Success': + self._raise_err_from_json(resp_json) + return resp_json + + def authenticate(self, trust_password): + body_json = {'type': 'client', 'password': trust_password} + return self._send_request('POST', '/1.0/certificates', body_json=body_json) + + def _send_request(self, method, url, body_json=None, ok_error_codes=None, timeout=None): + try: + body = json.dumps(body_json) + self.connection.request(method, url, body=body) + resp = self.connection.getresponse() + resp_json = json.loads(resp.read()) + self.logs.append({ + 'type': 'sent request', + 'request': {'method': method, 'url': url, 'json': body_json, 'timeout': timeout}, + 'response': {'json': resp_json} + }) + resp_type = resp_json.get('type', None) + if resp_type == 'error': + if ok_error_codes is not None and resp_json['error_code'] in ok_error_codes: + return resp_json + self._raise_err_from_json(resp_json) + return resp_json + except socket.error as e: + raise LXDClientException('cannot connect to the LXD server', err=e) + + def _raise_err_from_json(self, resp_json): + err_params = {} + if self.debug: + err_params['logs'] = self.logs + raise LXDClientException(self._get_err_from_resp_json(resp_json), **err_params) + + @staticmethod + def _get_err_from_resp_json(resp_json): + metadata = resp_json.get('metadata', None) + if metadata is not None: + err = metadata.get('err', None) + if err is None: + err = resp_json.get('error', None) + return err diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 30729ecf09c..39f659e7fd2 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -282,37 +282,7 @@ """ import os - -try: - import json -except ImportError: - import simplejson as json - -# httplib/http.client connection using unix domain socket -import socket -import ssl -try: - from httplib import HTTPConnection, HTTPSConnection -except ImportError: - # Python 3 - from http.client import HTTPConnection, HTTPSConnection - -class UnixHTTPConnection(HTTPConnection): - def __init__(self, path, timeout=None): - HTTPConnection.__init__(self, 'localhost', timeout=timeout) - self.path = path - - def connect(self): - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.connect(self.path) - self.sock = sock - -from ansible.module_utils.urls import generic_urlparse -try: - from urlparse import urlparse -except ImportError: - # Python 3 - from url.parse import urlparse +from ansible.modules.extras.cloud.lxd import LXDClient, LXDClientException # LXD_ANSIBLE_STATES is a map of states that contain values of methods used # when a particular state is evoked. @@ -377,22 +347,19 @@ def __init__(self, module): self.wait_for_ipv4_addresses = self.module.params['wait_for_ipv4_addresses'] self.force_stop = self.module.params['force_stop'] self.addresses = None + self.url = self.module.params['url'] self.key_file = self.module.params.get('key_file', None) self.cert_file = self.module.params.get('cert_file', None) - self.trust_password = self.module.params.get('trust_password', None) self.debug = self.module.params['debug'] - if self.url.startswith('https:'): - parts = generic_urlparse(urlparse(self.url)) - ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) - ctx.load_cert_chain(self.cert_file, keyfile=self.key_file) - self.connection = HTTPSConnection(parts.get('netloc'), context=ctx) - elif self.url.startswith('unix:'): - unix_socket_path = self.url[len('unix:'):] - self.connection = UnixHTTPConnection(unix_socket_path) - else: - self.module.fail_json(msg='URL scheme must be unix: or https:') - self.logs = [] + try: + self.client = LXDClient( + self.url, key_file=self.key_file, cert_file=self.cert_file, + debug=self.debug + ) + except LXDClientException as e: + self.module.fail_json(msg=e.msg) + self.trust_password = self.module.params.get('trust_password', None) self.actions = [] def _check_argument_choices(self, name, value, choices): @@ -408,76 +375,14 @@ def _build_config(self): if param_val is not None: self.config[attr] = param_val - def _authenticate(self): - body_json = {'type': 'client', 'password': self.trust_password} - self._send_request('POST', '/1.0/certificates', body_json=body_json) - - def _send_request(self, method, url, body_json=None, ok_error_codes=None): - try: - body = json.dumps(body_json) - self.connection.request(method, url, body=body) - resp = self.connection.getresponse() - resp_json = json.loads(resp.read()) - self.logs.append({ - 'type': 'sent request', - 'request': {'method': method, 'url': url, 'json': body_json, 'timeout': self.timeout}, - 'response': {'json': resp_json} - }) - resp_type = resp_json.get('type', None) - if resp_type == 'error': - if ok_error_codes is not None and resp_json['error_code'] in ok_error_codes: - return resp_json - fail_params = { - 'msg': self._get_err_from_resp_json(resp_json), - } - if self.debug: - fail_params['logs'] = self.logs - self.module.fail_json(**fail_params) - return resp_json - except socket.error as e: - if self.url is None: - self.module.fail_json( - msg='cannot connect to the LXD server', - unix_socket_path=self.unix_socket_path, error=e - ) - else: - self.module.fail_json( - msg='cannot connect to the LXD server', - url=self.url, key_file=self.key_file, cert_file=self.cert_file, - error=e - ) - - @staticmethod - def _get_err_from_resp_json(resp_json): - metadata = resp_json.get('metadata', None) - if metadata is not None: - err = metadata.get('err', None) - if err is None: - err = resp_json.get('error', None) - return err - - def _operate_and_wait(self, method, path, body_json=None): - resp_json = self._send_request(method, path, body_json=body_json) - if resp_json['type'] == 'async': - url = '{0}/wait'.format(resp_json['operation']) - resp_json = self._send_request('GET', url) - if resp_json['metadata']['status'] != 'Success': - fail_params = { - 'msg': self._get_err_from_resp_json(resp_json), - } - if self.debug: - fail_params['logs'] = self.logs - self.module.fail_json(**fail_params) - return resp_json - def _get_container_json(self): - return self._send_request( + return self.client.do( 'GET', '/1.0/containers/{0}'.format(self.name), ok_error_codes=[404] ) def _get_container_state_json(self): - return self._send_request( + return self.client.do( 'GET', '/1.0/containers/{0}/state'.format(self.name), ok_error_codes=[404] ) @@ -492,12 +397,12 @@ def _change_state(self, action, force_stop=False): body_json={'action': action, 'timeout': self.timeout} if force_stop: body_json['force'] = True - return self._operate_and_wait('PUT', '/1.0/containers/{0}/state'.format(self.name), body_json=body_json) + return self.client.do('PUT', '/1.0/containers/{0}/state'.format(self.name), body_json=body_json) def _create_container(self): config = self.config.copy() config['name'] = self.name - self._operate_and_wait('POST', '/1.0/containers', config) + self.client.do('POST', '/1.0/containers', config) self.actions.append('create') def _start_container(self): @@ -536,23 +441,17 @@ def _has_all_ipv4_addresses(addresses): return len(addresses) > 0 and all([len(v) > 0 for v in addresses.itervalues()]) def _get_addresses(self): - due = datetime.datetime.now() + datetime.timedelta(seconds=self.timeout) - while datetime.datetime.now() < due: - time.sleep(1) - addresses = self._container_ipv4_addresses() - if self._has_all_ipv4_addresses(addresses): - self.addresses = addresses - return - - state_changed = len(self.actions) > 0 - fail_params = { - 'msg': 'timeout for getting IPv4 addresses', - 'changed': state_changed, - 'actions': self.actions - } - if self.debug: - fail_params['logs'] = self.logs - self.module.fail_json(**fail_params) + try: + due = datetime.datetime.now() + datetime.timedelta(seconds=self.timeout) + while datetime.datetime.now() < due: + time.sleep(1) + addresses = self._container_ipv4_addresses() + if self._has_all_ipv4_addresses(addresses): + self.addresses = addresses + return + except LXDClientException as e: + e.msg = 'timeout for getting IPv4 addresses' + raise def _started(self): if self.old_state == 'absent': @@ -657,7 +556,7 @@ def _apply_container_configs(self): self.actions.append('apply_container_configs') def _get_profile_json(self): - return self._send_request( + return self.client.do( 'GET', '/1.0/profiles/{0}'.format(self.name), ok_error_codes=[404] ) @@ -694,12 +593,12 @@ def _update_profile(self): def _create_profile(self): config = self.config.copy() config['name'] = self.name - self._send_request('POST', '/1.0/profiles', config) + self.client.do('POST', '/1.0/profiles', config) self.actions.append('create') def _rename_profile(self): config = {'name': self.new_name} - self._send_request('POST', '/1.0/profiles/{}'.format(self.name), config) + self.client.do('POST', '/1.0/profiles/{}'.format(self.name), config) self.actions.append('rename') self.name = self.new_name @@ -720,40 +619,51 @@ def _apply_profile_configs(self): config = self.old_profile_json.copy() for k, v in self.config.iteritems(): config[k] = v - self._send_request('PUT', '/1.0/profiles/{}'.format(self.name), config) + self.client.do('PUT', '/1.0/profiles/{}'.format(self.name), config) self.actions.append('apply_profile_configs') def _delete_profile(self): - self._send_request('DELETE', '/1.0/profiles/{}'.format(self.name)) + self.client.do('DELETE', '/1.0/profiles/{}'.format(self.name)) self.actions.append('delete') def run(self): """Run the main method.""" - if self.trust_password is not None: - self._authenticate() - - if self.type == 'container': - self.old_container_json = self._get_container_json() - self.old_state = self._container_json_to_module_state(self.old_container_json) - action = getattr(self, LXD_ANSIBLE_STATES[self.state]) - action() - elif self.type == 'profile': - self.old_profile_json = self._get_profile_json() - self.old_state = self._profile_json_to_module_state(self.old_profile_json) - self._update_profile() - - state_changed = len(self.actions) > 0 - result_json = { - 'changed': state_changed, - 'old_state': self.old_state, - 'actions': self.actions - } - if self.debug: - result_json['logs'] = self.logs - if self.addresses is not None: - result_json['addresses'] = self.addresses - self.module.exit_json(**result_json) + try: + if self.trust_password is not None: + self.client.authenticate(self.trust_password) + + if self.type == 'container': + self.old_container_json = self._get_container_json() + self.old_state = self._container_json_to_module_state(self.old_container_json) + action = getattr(self, LXD_ANSIBLE_STATES[self.state]) + action() + elif self.type == 'profile': + self.old_profile_json = self._get_profile_json() + self.old_state = self._profile_json_to_module_state(self.old_profile_json) + self._update_profile() + + state_changed = len(self.actions) > 0 + result_json = { + 'changed': state_changed, + 'old_state': self.old_state, + 'actions': self.actions + } + if self.client.debug: + result_json['logs'] = self.client.logs + if self.addresses is not None: + result_json['addresses'] = self.addresses + self.module.exit_json(**result_json) + except LXDClientException as e: + state_changed = len(self.actions) > 0 + fail_params = { + 'msg': e.msg, + 'changed': state_changed, + 'actions': self.actions + } + if self.client.debug: + fail_params['logs'] = e.kwargs['logs'] + self.module.fail_json(**fail_params) def main(): From f3ec45d27000957af4d5637b810ede796ccb76ee Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Fri, 1 Jul 2016 00:28:21 +0900 Subject: [PATCH 1775/2522] Add lxd_profile module --- cloud/lxd/lxd_profile.py | 389 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 389 insertions(+) create mode 100644 cloud/lxd/lxd_profile.py diff --git a/cloud/lxd/lxd_profile.py b/cloud/lxd/lxd_profile.py new file mode 100644 index 00000000000..351e280e902 --- /dev/null +++ b/cloud/lxd/lxd_profile.py @@ -0,0 +1,389 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Hiroaki Nakamura +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + + +DOCUMENTATION = """ +--- +module: lxd_profile +short_description: Manage LXD profiles +version_added: 2.2.0 +description: + - Management of LXD profiles +author: "Hiroaki Nakamura (@hnakamur)" +options: + name: + description: + - Name of a profile. + required: true + config: + description: + - > + The config for the container (e.g. {"limits.memory": "4GB"}). + See https://github.com/lxc/lxd/blob/master/doc/rest-api.md#patch-3 + - If the profile already exists and its "config" value in metadata + obtained from + GET /1.0/profiles/ + https://github.com/lxc/lxd/blob/master/doc/rest-api.md#get-19 + are different, they this module tries to apply the configurations. + - Not all config values are supported to apply the existing profile. + Maybe you need to delete and recreate a profile. + required: false + devices: + description: + - > + The devices for the profile + (e.g. {"rootfs": {"path": "/dev/kvm", "type": "unix-char"}). + See https://github.com/lxc/lxd/blob/master/doc/rest-api.md#patch-3 + required: false + new_name: + description: + - A new name of a profile. + - If this parameter is specified a profile will be renamed to this name. + See https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-11 + required: false + state: + choices: + - present + - absent + description: + - Define the state of a profile. + required: false + default: present + url: + description: + - The unix domain socket path or the https URL for the LXD server. + required: false + default: unix:/var/lib/lxd/unix.socket + key_file: + description: + - The client certificate key file path. + required: false + default: > + '{}/.config/lxc/client.key'.format(os.environ['HOME']) + cert_file: + description: + - The client certificate file path. + required: false + default: > + '{}/.config/lxc/client.crt'.format(os.environ['HOME']) + trust_password: + description: + - The client trusted password. + - You need to set this password on the LXD server before + running this module using the following command. + lxc config set core.trust_password + See https://www.stgraber.org/2016/04/18/lxd-api-direct-interaction/ + - If trust_password is set, this module send a request for + authentication before sending any requests. + required: false + debug: + description: + - If this flag is true, the logs key are added to the result object + which keeps all the requests and responses for calling APIs. + required: false + default: false +notes: + - Profiles must have a unique name. If you attempt to create a profile + with a name that already existed in the users namespace the module will + simply return as "unchanged". +""" + +EXAMPLES = """ +# An example for creating a profile +- hosts: localhost + connection: local + tasks: + - name: Create a profile + lxd_profile: + name: macvlan + state: present + config: {} + description: 'my macvlan profile' + devices: + eth0: + nictype: macvlan + parent: br0 + type: nic + +# An example for creating a profile via http connection +- hosts: localhost + connection: local + tasks: + - name: create macvlan profile + lxd_profile: + url: https://127.0.0.1:8443 + # These cert_file and key_file values are equal to the default values. + #cert_file: "{{ lookup('env', 'HOME') }}/.config/lxc/client.crt" + #key_file: "{{ lookup('env', 'HOME') }}/.config/lxc/client.key" + trust_password: mypassword + name: macvlan + state: present + config: {} + description: 'my macvlan profile' + devices: + eth0: + nictype: macvlan + parent: br0 + type: nic + +# An example for deleting a profile +- hosts: localhost + connection: local + tasks: + - name: Delete a profile + lxd_profile: + name: macvlan + state: absent + +# An example for renaming a profile +- hosts: localhost + connection: local + tasks: + - name: Rename a profile + lxd_profile: + name: macvlan + new_name: macvlan2 + state: present +""" + +RETURN=""" +lxd_profile: + description: profile information + returned: success + type: object + contains: + old_state: + description: The old state of the profile + sample: "absent" + logs: + descriptions: The logs of requests and responses. + returned: when the debug parameter is true and any requests were sent. + actions: + description: List of actions performed for the profile. + returned: success + type: list + sample: ["create"] +""" + +import os +from ansible.modules.extras.cloud.lxd import LXDClient, LXDClientException + +# PROFILE_STATES is a list for states supported +PROFILES_STATES = [ + 'present', 'absent' +] + +# CONFIG_PARAMS is a list of config attribute names. +CONFIG_PARAMS = [ + 'config', 'description', 'devices' +] + +class LXDProfileManagement(object): + def __init__(self, module): + """Management of LXC containers via Ansible. + + :param module: Processed Ansible Module. + :type module: ``object`` + """ + self.module = module + self.name = self.module.params['name'] + self._build_config() + self.state = self.module.params['state'] + self.new_name = self.module.params.get('new_name', None) + + self.url = self.module.params['url'] + self.key_file = self.module.params.get('key_file', None) + self.cert_file = self.module.params.get('cert_file', None) + self.debug = self.module.params['debug'] + try: + self.client = LXDClient( + self.url, key_file=self.key_file, cert_file=self.cert_file, + debug=self.debug + ) + except LXDClientException as e: + self.module.fail_json(msg=e.msg) + self.trust_password = self.module.params.get('trust_password', None) + self.actions = [] + + def _build_config(self): + self.config = {} + for attr in CONFIG_PARAMS: + param_val = self.module.params.get(attr, None) + if param_val is not None: + self.config[attr] = param_val + + def _get_profile_json(self): + return self.client.do( + 'GET', '/1.0/profiles/{0}'.format(self.name), + ok_error_codes=[404] + ) + + @staticmethod + def _profile_json_to_module_state(resp_json): + if resp_json['type'] == 'error': + return 'absent' + return 'present' + + def _update_profile(self): + if self.state == 'present': + if self.old_state == 'absent': + if self.new_name is None: + self._create_profile() + else: + self.module.fail_json( + msg='new_name must not be set when the profile does not exist and the specified state is present', + changed=False) + else: + if self.new_name is not None and self.new_name != self.name: + self._rename_profile() + if self._needs_to_apply_profile_configs(): + self._apply_profile_configs() + elif self.state == 'absent': + if self.old_state == 'present': + if self.new_name is None: + self._delete_profile() + else: + self.module.fail_json( + msg='new_name must not be set when the profile exists and the specified state is absent', + changed=False) + + def _create_profile(self): + config = self.config.copy() + config['name'] = self.name + self.client.do('POST', '/1.0/profiles', config) + self.actions.append('create') + + def _rename_profile(self): + config = {'name': self.new_name} + self.client.do('POST', '/1.0/profiles/{}'.format(self.name), config) + self.actions.append('rename') + self.name = self.new_name + + def _needs_to_change_profile_config(self, key): + if key not in self.config: + return False + old_configs = self.old_profile_json['metadata'].get(key, None) + return self.config[key] != old_configs + + def _needs_to_apply_profile_configs(self): + return ( + self._needs_to_change_profile_config('config') or + self._needs_to_change_profile_config('description') or + self._needs_to_change_profile_config('devices') + ) + + def _apply_profile_configs(self): + config = self.old_profile_json.copy() + for k, v in self.config.iteritems(): + config[k] = v + self.client.do('PUT', '/1.0/profiles/{}'.format(self.name), config) + self.actions.append('apply_profile_configs') + + def _delete_profile(self): + self.client.do('DELETE', '/1.0/profiles/{}'.format(self.name)) + self.actions.append('delete') + + def run(self): + """Run the main method.""" + + try: + if self.trust_password is not None: + self.client.authenticate(self.trust_password) + + self.old_profile_json = self._get_profile_json() + self.old_state = self._profile_json_to_module_state(self.old_profile_json) + self._update_profile() + + state_changed = len(self.actions) > 0 + result_json = { + 'changed': state_changed, + 'old_state': self.old_state, + 'actions': self.actions + } + if self.client.debug: + result_json['logs'] = self.client.logs + self.module.exit_json(**result_json) + except LXDClientException as e: + state_changed = len(self.actions) > 0 + fail_params = { + 'msg': e.msg, + 'changed': state_changed, + 'actions': self.actions + } + if self.client.debug: + fail_params['logs'] = e.kwargs['logs'] + self.module.fail_json(**fail_params) + + +def main(): + """Ansible Main module.""" + + module = AnsibleModule( + argument_spec=dict( + name=dict( + type='str', + required=True + ), + new_name=dict( + type='str', + ), + config=dict( + type='dict', + ), + description=dict( + type='str', + ), + devices=dict( + type='dict', + ), + state=dict( + choices=PROFILES_STATES, + default='present' + ), + url=dict( + type='str', + default='unix:/var/lib/lxd/unix.socket' + ), + key_file=dict( + type='str', + default='{}/.config/lxc/client.key'.format(os.environ['HOME']) + ), + cert_file=dict( + type='str', + default='{}/.config/lxc/client.crt'.format(os.environ['HOME']) + ), + trust_password=dict( + type='str', + ), + debug=dict( + type='bool', + default=False + ) + ), + supports_check_mode=False, + ) + + lxd_manage = LXDProfileManagement(module=module) + lxd_manage.run() + +# import module bits +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From c118bcab5cad9037d6bf31778668799ba899dea3 Mon Sep 17 00:00:00 2001 From: Patrik Lundin Date: Thu, 30 Jun 2016 17:30:28 +0200 Subject: [PATCH 1776/2522] Add a break and extra debug log for clarity. --- packaging/os/openbsd_pkg.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packaging/os/openbsd_pkg.py b/packaging/os/openbsd_pkg.py index 305b7c06454..2e31d16b426 100644 --- a/packaging/os/openbsd_pkg.py +++ b/packaging/os/openbsd_pkg.py @@ -229,10 +229,12 @@ def package_latest(name, installed_state, pkg_spec, module): module.debug("package_latest(): checking for pre-upgrade package name: %s" % installed_name) match = re.search("\W%s->.+: ok\W" % installed_name, stdout) if match: + module.debug("package_latest(): package name match: %s" % installed_name) if module.check_mode: module.exit_json(changed=True) changed = True + break # FIXME: This part is problematic. Based on the issues mentioned (and # handled) in package_present() it is not safe to blindly trust stderr From 2d353dc98cb7fe9f5082902a1a7fc2e89e4ebe69 Mon Sep 17 00:00:00 2001 From: Patrik Lundin Date: Thu, 30 Jun 2016 17:41:17 +0200 Subject: [PATCH 1777/2522] Improve debug logging for build code. --- packaging/os/openbsd_pkg.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packaging/os/openbsd_pkg.py b/packaging/os/openbsd_pkg.py index 2e31d16b426..eae57a695c7 100644 --- a/packaging/os/openbsd_pkg.py +++ b/packaging/os/openbsd_pkg.py @@ -374,7 +374,7 @@ def get_package_source_path(name, pkg_spec, module): conn = sqlite3.connect(sqlports_db_file) first_part_of_query = 'SELECT fullpkgpath, fullpkgname FROM ports WHERE fullpkgname' query = first_part_of_query + ' = ?' - module.debug("package_package_source_path(): query: %s" % query) + module.debug("package_package_source_path(): exact query: %s" % query) cursor = conn.execute(query, (name,)) results = cursor.fetchall() @@ -384,14 +384,14 @@ def get_package_source_path(name, pkg_spec, module): query = first_part_of_query + ' LIKE ?' if pkg_spec['flavor']: looking_for += pkg_spec['flavor_separator'] + pkg_spec['flavor'] - module.debug("package_package_source_path(): flavor query: %s" % query) + module.debug("package_package_source_path(): fuzzy flavor query: %s" % query) cursor = conn.execute(query, (looking_for,)) elif pkg_spec['style'] == 'versionless': query += ' AND fullpkgname NOT LIKE ?' - module.debug("package_package_source_path(): versionless query: %s" % query) + module.debug("package_package_source_path(): fuzzy versionless query: %s" % query) cursor = conn.execute(query, (looking_for, "%s-%%" % looking_for,)) else: - module.debug("package_package_source_path(): query: %s" % query) + module.debug("package_package_source_path(): fuzzy query: %s" % query) cursor = conn.execute(query, (looking_for,)) results = cursor.fetchall() From 7662401f4541fb648c5634059b1d77c2c677ea81 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Fri, 1 Jul 2016 00:42:50 +0900 Subject: [PATCH 1778/2522] Remove support for type=profile from lxd_container module --- cloud/lxd/lxd_container.py | 242 +++++++++---------------------------- 1 file changed, 58 insertions(+), 184 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 39f659e7fd2..d5b18f64764 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -32,14 +32,6 @@ description: - Name of a container. required: true - type: - choices: - - container - - profile - description: - - The resource type. - required: false - default: container architecture: description: - The archiecture for the container (e.g. "x86_64" or "i686"). @@ -82,24 +74,15 @@ "alias": "ubuntu/xenial/amd64" }). See https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1 required: false - new_name: - description: - - A new name of a profile. - - If this parameter is specified a profile will be renamed to this name. - required: false state: choices: - - present - started - stopped - restarted - absent - frozen description: - - Define the state of a container or profile. - - Valid choices for type=container are started, stopped, restarted, - absent, or frozen. - - Valid choices for type=profile are present or absent. + - Define the state of a container. required: false default: started timeout: @@ -173,6 +156,7 @@ """ EXAMPLES = """ +# An example for creating a Ubuntu container and install python - hosts: localhost connection: local tasks: @@ -185,39 +169,33 @@ mode: pull server: https://images.linuxcontainers.org protocol: lxd - alias: "ubuntu/xenial/amd64" + alias: ubuntu/xenial/amd64 profiles: ["default"] - - name: Install python in the created container "mycontainer" - command: lxc exec mycontainer -- apt install -y python - - name: Copy /etc/hosts in the created container "mycontainer" to localhost with name "mycontainer-hosts" - command: lxc file pull mycontainer/etc/hosts mycontainer-hosts - - -# Note your container must be in the inventory for the below example. -# -# [containers] -# mycontainer ansible_connection=lxd -# -- hosts: - - mycontainer - tasks: - - template: src=foo.j2 dest=/etc/bar - + wait_for_ipv4_addresses: true + timeout: 600 + + - name: check python is installed in container + delegate_to: mycontainer + raw: dpkg-query -W -f='${Status}' python + register: python_install_check + failed_when: python_install_check.rc not in [0, 1] + changed_when: false + + - name: install python in container + delegate_to: mycontainer + raw: apt-get install -y python + when: python_install_check.rc == 1 + +# An example for deleting a container - hosts: localhost connection: local tasks: - - name: Create a stopped container + - name: Restart a container lxd_container: name: mycontainer - state: stopped - source: - type: image - mode: pull - server: https://images.linuxcontainers.org - protocol: lxd - alias: "ubuntu/xenial/amd64" - profiles: ["default"] + state: restarted +# An example for restarting a container - hosts: localhost connection: local tasks: @@ -225,35 +203,34 @@ lxd_container: name: mycontainer state: restarted - source: - type: image - mode: pull - server: https://images.linuxcontainers.org - protocol: lxd - alias: "ubuntu/xenial/amd64" - profiles: ["default"] -# An example for connecting to the LXD server using https +# An example for restarting a container using https to connect to the LXD server - hosts: localhost connection: local tasks: - - name: create macvlan profile - lxd_container: - url: https://127.0.0.1:8443 - # These cert_file and key_file values are equal to the default values. - #cert_file: "{{ lookup('env', 'HOME') }}/.config/lxc/client.crt" - #key_file: "{{ lookup('env', 'HOME') }}/.config/lxc/client.key" - trust_password: mypassword - type: profile - name: macvlan - state: present - config: {} - description: 'my macvlan profile' - devices: - eth0: - nictype: macvlan - parent: br0 - type: nic + - name: Restart a container + lxd_container: + url: https://127.0.0.1:8443 + # These cert_file and key_file values are equal to the default values. + #cert_file: "{{ lookup('env', 'HOME') }}/.config/lxc/client.crt" + #key_file: "{{ lookup('env', 'HOME') }}/.config/lxc/client.key" + trust_password: mypassword + name: mycontainer + state: restarted + +# Note your container must be in the inventory for the below example. +# +# [containers] +# mycontainer ansible_connection=lxd +# +- hosts: + - mycontainer + tasks: + - name: copy /etc/hosts in the created container to localhost with name "mycontainer-hosts" + fetch: + src: /etc/hosts + dest: /tmp/mycontainer-hosts + flat: true """ RETURN=""" @@ -294,11 +271,6 @@ 'frozen': '_frozen' } -# PROFILE_STATES is a list for states supported for type=profiles -PROFILES_STATES = [ - 'present', 'absent' -] - # ANSIBLE_LXD_STATES is a map of states of lxd containers to the Ansible # lxc_container module state parameter value. ANSIBLE_LXD_STATES = { @@ -307,11 +279,10 @@ 'Frozen': 'frozen', } -# CONFIG_PARAMS is a map from a resource type to config attribute names. -CONFIG_PARAMS = { - 'container': ['architecture', 'config', 'devices', 'ephemeral', 'profiles', 'source'], - 'profile': ['config', 'description', 'devices'] -} +# CONFIG_PARAMS is a list of config attribute names. +CONFIG_PARAMS = [ + 'architecture', 'config', 'devices', 'ephemeral', 'profiles', 'source' +] try: callable(all) @@ -324,7 +295,7 @@ def all(iterable): return False return True -class LxdContainerManagement(object): +class LXDContainerManagement(object): def __init__(self, module): """Management of LXC containers via Ansible. @@ -333,16 +304,10 @@ def __init__(self, module): """ self.module = module self.name = self.module.params['name'] - self.type = self.module.params['type'] self._build_config() self.state = self.module.params['state'] - if self.type == 'container': - self._check_argument_choices('state', self.state, LXD_ANSIBLE_STATES.keys()) - elif self.type == 'profile': - self._check_argument_choices('state', self.state, PROFILES_STATES) - self.new_name = self.module.params.get('new_name', None) self.timeout = self.module.params['timeout'] self.wait_for_ipv4_addresses = self.module.params['wait_for_ipv4_addresses'] self.force_stop = self.module.params['force_stop'] @@ -362,15 +327,9 @@ def __init__(self, module): self.trust_password = self.module.params.get('trust_password', None) self.actions = [] - def _check_argument_choices(self, name, value, choices): - if value not in choices: - choices_str=",".join([str(c) for c in choices]) - msg="value of %s must be one of: %s, got: %s" % (name, choices_str, value) - self.module.fail_json(msg=msg) - def _build_config(self): self.config = {} - for attr in CONFIG_PARAMS[self.type]: + for attr in CONFIG_PARAMS: param_val = self.module.params.get(attr, None) if param_val is not None: self.config[attr] = param_val @@ -555,77 +514,6 @@ def _apply_container_configs(self): self._operate_and_wait('PUT', '/1.0/containers/{0}'.format(self.name), body_json=body_json) self.actions.append('apply_container_configs') - def _get_profile_json(self): - return self.client.do( - 'GET', '/1.0/profiles/{0}'.format(self.name), - ok_error_codes=[404] - ) - - @staticmethod - def _profile_json_to_module_state(resp_json): - if resp_json['type'] == 'error': - return 'absent' - return 'present' - - def _update_profile(self): - if self.state == 'present': - if self.old_state == 'absent': - if self.new_name is None: - self._create_profile() - else: - self.module.fail_json( - msg='new_name must not be set when the profile does not exist and the specified state is present', - changed=False) - else: - if self.new_name is not None and self.new_name != self.name: - self._rename_profile() - if self._needs_to_apply_profile_configs(): - self._apply_profile_configs() - elif self.state == 'absent': - if self.old_state == 'present': - if self.new_name is None: - self._delete_profile() - else: - self.module.fail_json( - msg='new_name must not be set when the profile exists and the specified state is absent', - changed=False) - - def _create_profile(self): - config = self.config.copy() - config['name'] = self.name - self.client.do('POST', '/1.0/profiles', config) - self.actions.append('create') - - def _rename_profile(self): - config = {'name': self.new_name} - self.client.do('POST', '/1.0/profiles/{}'.format(self.name), config) - self.actions.append('rename') - self.name = self.new_name - - def _needs_to_change_profile_config(self, key): - if key not in self.config: - return False - old_configs = self.old_profile_json['metadata'].get(key, None) - return self.config[key] != old_configs - - def _needs_to_apply_profile_configs(self): - return ( - self._needs_to_change_profile_config('config') or - self._needs_to_change_profile_config('description') or - self._needs_to_change_profile_config('devices') - ) - - def _apply_profile_configs(self): - config = self.old_profile_json.copy() - for k, v in self.config.iteritems(): - config[k] = v - self.client.do('PUT', '/1.0/profiles/{}'.format(self.name), config) - self.actions.append('apply_profile_configs') - - def _delete_profile(self): - self.client.do('DELETE', '/1.0/profiles/{}'.format(self.name)) - self.actions.append('delete') - def run(self): """Run the main method.""" @@ -633,15 +521,10 @@ def run(self): if self.trust_password is not None: self.client.authenticate(self.trust_password) - if self.type == 'container': - self.old_container_json = self._get_container_json() - self.old_state = self._container_json_to_module_state(self.old_container_json) - action = getattr(self, LXD_ANSIBLE_STATES[self.state]) - action() - elif self.type == 'profile': - self.old_profile_json = self._get_profile_json() - self.old_state = self._profile_json_to_module_state(self.old_profile_json) - self._update_profile() + self.old_container_json = self._get_container_json() + self.old_state = self._container_json_to_module_state(self.old_container_json) + action = getattr(self, LXD_ANSIBLE_STATES[self.state]) + action() state_changed = len(self.actions) > 0 result_json = { @@ -665,7 +548,6 @@ def run(self): fail_params['logs'] = e.kwargs['logs'] self.module.fail_json(**fail_params) - def main(): """Ansible Main module.""" @@ -675,14 +557,6 @@ def main(): type='str', required=True ), - new_name=dict( - type='str', - ), - type=dict( - type='str', - choices=CONFIG_PARAMS.keys(), - default='container' - ), architecture=dict( type='str', ), @@ -705,7 +579,7 @@ def main(): type='dict', ), state=dict( - choices=list(set(LXD_ANSIBLE_STATES.keys()) | set(PROFILES_STATES)), + choices=LXD_ANSIBLE_STATES.keys(), default='started' ), timeout=dict( @@ -743,7 +617,7 @@ def main(): supports_check_mode=False, ) - lxd_manage = LxdContainerManagement(module=module) + lxd_manage = LXDContainerManagement(module=module) lxd_manage.run() # import module bits From dd14d142240b821691c2865f2ab6a078ccb6cd9f Mon Sep 17 00:00:00 2001 From: Patrik Lundin Date: Thu, 30 Jun 2016 18:13:40 +0200 Subject: [PATCH 1779/2522] No support for build=true with 'branch' syntax. --- packaging/os/openbsd_pkg.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packaging/os/openbsd_pkg.py b/packaging/os/openbsd_pkg.py index eae57a695c7..de37a769b05 100644 --- a/packaging/os/openbsd_pkg.py +++ b/packaging/os/openbsd_pkg.py @@ -491,6 +491,11 @@ def main(): pkg_spec = {} parse_package_name(name, pkg_spec, module) + # Not sure how the branch syntax is supposed to play together + # with build mode. Disable it for now. + if pkg_spec['style'] == 'branch' and module.params['build'] is True: + module.fail_json(msg="the combination of 'branch' syntax and build=%s is not supported: %s" % (module.params['build'], name)) + # Get package state. installed_state = get_package_state(name, pkg_spec, module) From 0b55ce4cc3b2e42a6a15c9bdce5ac2ecca823e8e Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Fri, 1 Jul 2016 01:32:47 +0900 Subject: [PATCH 1780/2522] Fix sending request --- cloud/lxd/lxd_container.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index d5b18f64764..fefc7c14170 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -377,7 +377,7 @@ def _restart_container(self): self.actions.append('restart') def _delete_container(self): - return self._operate_and_wait('DELETE', '/1.0/containers/{0}'.format(self.name)) + return self.client.do('DELETE', '/1.0/containers/{0}'.format(self.name)) self.actions.append('delete') def _freeze_container(self): @@ -511,7 +511,7 @@ def _apply_container_configs(self): body_json['devices'] = self.config['devices'] if self._needs_to_change_container_config('profiles'): body_json['profiles'] = self.config['profiles'] - self._operate_and_wait('PUT', '/1.0/containers/{0}'.format(self.name), body_json=body_json) + self.client.do('PUT', '/1.0/containers/{0}'.format(self.name), body_json=body_json) self.actions.append('apply_container_configs') def run(self): From 59364500bc42014b9dd7f6edfec0ec05322abcc4 Mon Sep 17 00:00:00 2001 From: Patrik Lundin Date: Thu, 30 Jun 2016 18:51:35 +0200 Subject: [PATCH 1781/2522] Improve debug logging some more. --- packaging/os/openbsd_pkg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/os/openbsd_pkg.py b/packaging/os/openbsd_pkg.py index de37a769b05..c5beab1d8aa 100644 --- a/packaging/os/openbsd_pkg.py +++ b/packaging/os/openbsd_pkg.py @@ -229,7 +229,7 @@ def package_latest(name, installed_state, pkg_spec, module): module.debug("package_latest(): checking for pre-upgrade package name: %s" % installed_name) match = re.search("\W%s->.+: ok\W" % installed_name, stdout) if match: - module.debug("package_latest(): package name match: %s" % installed_name) + module.debug("package_latest(): pre-upgrade package name match: %s" % installed_name) if module.check_mode: module.exit_json(changed=True) From d55f22a2c6b0276b6e2e98b0fe441f68ffc88c04 Mon Sep 17 00:00:00 2001 From: Patrik Lundin Date: Thu, 30 Jun 2016 18:55:43 +0200 Subject: [PATCH 1782/2522] Make fail messages all use lowercase messages. --- packaging/os/openbsd_pkg.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packaging/os/openbsd_pkg.py b/packaging/os/openbsd_pkg.py index c5beab1d8aa..cc7db96138e 100644 --- a/packaging/os/openbsd_pkg.py +++ b/packaging/os/openbsd_pkg.py @@ -299,7 +299,7 @@ def parse_package_name(name, pkg_spec, module): # Stop if someone is giving us a name that both has a version and is # version-less at the same time. if version_match and versionless_match: - module.fail_json(msg="Package name both has a version and is version-less: " + name) + module.fail_json(msg="package name both has a version and is version-less: " + name) # If name includes a version. if version_match: @@ -312,7 +312,7 @@ def parse_package_name(name, pkg_spec, module): pkg_spec['flavor'] = match.group('flavor') pkg_spec['style'] = 'version' else: - module.fail_json(msg="Unable to parse package name at version_match: " + name) + module.fail_json(msg="unable to parse package name at version_match: " + name) # If name includes no version but is version-less ("--"). elif versionless_match: @@ -325,7 +325,7 @@ def parse_package_name(name, pkg_spec, module): pkg_spec['flavor'] = match.group('flavor') pkg_spec['style'] = 'versionless' else: - module.fail_json(msg="Unable to parse package name at versionless_match: " + name) + module.fail_json(msg="unable to parse package name at versionless_match: " + name) # If name includes no version, and is not version-less, it is all a stem. else: @@ -338,7 +338,7 @@ def parse_package_name(name, pkg_spec, module): pkg_spec['flavor'] = None pkg_spec['style'] = 'stem' else: - module.fail_json(msg="Unable to parse package name at else: " + name) + module.fail_json(msg="unable to parse package name at else: " + name) # If the stem contains an "%" then it needs special treatment. branch_match = re.search("%", pkg_spec['stem']) @@ -347,9 +347,9 @@ def parse_package_name(name, pkg_spec, module): branch_release = "6.0" if version_match or versionless_match: - module.fail_json(msg="Package name using 'branch' syntax also has a version or is version-less: " + name) + module.fail_json(msg="package name using 'branch' syntax also has a version or is version-less: " + name) if StrictVersion(platform.release()) < StrictVersion(branch_release): - module.fail_json(msg="Package name using 'branch' syntax requires at least OpenBSD %s: %s" % (branch_release, name)) + module.fail_json(msg="package name using 'branch' syntax requires at least OpenBSD %s: %s" % (branch_release, name)) pkg_spec['style'] = 'branch' @@ -358,7 +358,7 @@ def parse_package_name(name, pkg_spec, module): if pkg_spec['flavor']: match = re.search("-$", pkg_spec['flavor']) if match: - module.fail_json(msg="Trailing dash in flavor: " + pkg_spec['flavor']) + module.fail_json(msg="trailing dash in flavor: " + pkg_spec['flavor']) # Function used for figuring out the port path. def get_package_source_path(name, pkg_spec, module): From 602915c2649868da7ec7a9bf5b76d9d8b24b16e8 Mon Sep 17 00:00:00 2001 From: Constantin Date: Fri, 1 Jul 2016 09:56:03 +0100 Subject: [PATCH 1783/2522] Documented returned structure. (#2510) --- cloud/amazon/route53_zone.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/cloud/amazon/route53_zone.py b/cloud/amazon/route53_zone.py index fa48b18e273..328d48dbf67 100644 --- a/cloud/amazon/route53_zone.py +++ b/cloud/amazon/route53_zone.py @@ -75,6 +75,39 @@ ''' +RETURN=''' +comment: + description: optional hosted zone comment + returned: when hosted zone exists + type: string + sample: "Private zone" +name: + description: hosted zone name + returned: when hosted zone exists + type: string + sample: "private.local." +private_zone: + description: whether hosted zone is private or public + returned: when hosted zone exists + type: bool + sample: true +vpc_id: + description: id of vpc attached to private hosted zone + returned: for private hosted zone + type: string + sample: "vpc-1d36c84f" +vpc_region: + description: region of vpc attached to private hosted zone + returned: for private hosted zone + type: string + sample: "eu-west-1" +zone_id: + description: hosted zone id + returned: when hosted zone exists + type: string + sample: "Z6JQG9820BEFMW" +''' + import time try: From 0140e50d551967ebc74238cacd9697ae2bbd2c02 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Fri, 1 Jul 2016 23:23:14 +0900 Subject: [PATCH 1784/2522] Remove debug parameter and add logs in return object when invoked with -vvvv --- cloud/lxd/lxd_container.py | 15 +++------------ cloud/lxd/lxd_profile.py | 14 ++------------ 2 files changed, 5 insertions(+), 24 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index fefc7c14170..77fa7bc1402 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -133,12 +133,6 @@ - If trust_password is set, this module send a request for authentication before sending any requests. required: false - debug: - description: - - If this flag is true, the logs key are added to the result object - which keeps all the requests and responses for calling APIs. - required: false - default: false notes: - Containers must have a unique name. If you attempt to create a container with a name that already existed in the users namespace the module will @@ -250,7 +244,7 @@ sample: "stopped" logs: descriptions: The logs of requests and responses. - returned: when the debug parameter is true and any requests were sent. + returned: when ansible-playbook is invoked with -vvvv. actions: description: List of actions performed for the container. returned: success @@ -316,7 +310,7 @@ def __init__(self, module): self.url = self.module.params['url'] self.key_file = self.module.params.get('key_file', None) self.cert_file = self.module.params.get('cert_file', None) - self.debug = self.module.params['debug'] + self.debug = self.module._verbosity >= 4 try: self.client = LXDClient( self.url, key_file=self.key_file, cert_file=self.cert_file, @@ -528,6 +522,7 @@ def run(self): state_changed = len(self.actions) > 0 result_json = { + 'log_verbosity': self.module._verbosity, 'changed': state_changed, 'old_state': self.old_state, 'actions': self.actions @@ -608,10 +603,6 @@ def main(): ), trust_password=dict( type='str', - ), - debug=dict( - type='bool', - default=False ) ), supports_check_mode=False, diff --git a/cloud/lxd/lxd_profile.py b/cloud/lxd/lxd_profile.py index 351e280e902..8158a401643 100644 --- a/cloud/lxd/lxd_profile.py +++ b/cloud/lxd/lxd_profile.py @@ -93,12 +93,6 @@ - If trust_password is set, this module send a request for authentication before sending any requests. required: false - debug: - description: - - If this flag is true, the logs key are added to the result object - which keeps all the requests and responses for calling APIs. - required: false - default: false notes: - Profiles must have a unique name. If you attempt to create a profile with a name that already existed in the users namespace the module will @@ -174,7 +168,7 @@ sample: "absent" logs: descriptions: The logs of requests and responses. - returned: when the debug parameter is true and any requests were sent. + returned: when ansible-playbook is invoked with -vvvv. actions: description: List of actions performed for the profile. returned: success @@ -211,7 +205,7 @@ def __init__(self, module): self.url = self.module.params['url'] self.key_file = self.module.params.get('key_file', None) self.cert_file = self.module.params.get('cert_file', None) - self.debug = self.module.params['debug'] + self.debug = self.module._verbosity >= 4 try: self.client = LXDClient( self.url, key_file=self.key_file, cert_file=self.cert_file, @@ -371,10 +365,6 @@ def main(): ), trust_password=dict( type='str', - ), - debug=dict( - type='bool', - default=False ) ), supports_check_mode=False, From 1453d55c2307bc36d482ebc82e744cc0746ec5b8 Mon Sep 17 00:00:00 2001 From: Patrik Lundin Date: Fri, 1 Jul 2016 18:38:19 +0200 Subject: [PATCH 1785/2522] Improve debug log some more. --- packaging/os/openbsd_pkg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/os/openbsd_pkg.py b/packaging/os/openbsd_pkg.py index cc7db96138e..59fdd35c26b 100644 --- a/packaging/os/openbsd_pkg.py +++ b/packaging/os/openbsd_pkg.py @@ -477,7 +477,7 @@ def main(): parse_package_name('sqlports', pkg_spec, module) installed_state = get_package_state('sqlports', pkg_spec, module) if not installed_state: - module.debug("main(): installing sqlports") + module.debug("main(): installing 'sqlports' because build=%s" % module.params['build']) package_present('sqlports', installed_state, pkg_spec, module) if name == '*': From 5588cc9c0273c42ac12c14a699619ecb27658716 Mon Sep 17 00:00:00 2001 From: camradal Date: Sun, 3 Jul 2016 01:49:42 -0700 Subject: [PATCH 1786/2522] Add configuration options to VMWare portgroup (#2390) * Add configuration options to VMWare portgroup * Add version_added to newly added vmware portgroup params * Update vmware_porgroup to use network_policy node for settings * Update documentation for vmware_porgroup network policy --- cloud/vmware/vmware_portgroup.py | 45 +++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/cloud/vmware/vmware_portgroup.py b/cloud/vmware/vmware_portgroup.py index 591aa9240d0..c367a976f23 100644 --- a/cloud/vmware/vmware_portgroup.py +++ b/cloud/vmware/vmware_portgroup.py @@ -44,6 +44,14 @@ description: - VLAN ID to assign to portgroup required: True + network_policy: + description: + - Network policy specifies layer 2 security settings for a + portgroup such as promiscuous mode, where guest adapter listens + to all the packets, MAC address changes and forged transmits. + Settings are promiscuous_mode, forged_transmits, mac_changes + required: False + version_added: "2.2" extends_documentation_fragment: vmware.documentation ''' @@ -59,6 +67,17 @@ switch_name: vswitch_name portgroup_name: portgroup_name vlan_id: vlan_id + + - name: Add Portgroup with Promiscuous Mode Enabled + local_action: + module: vmware_portgroup + hostname: esxi_hostname + username: esxi_username + password: esxi_password + switch_name: vswitch_name + portgroup_name: portgroup_name + network_policy: + promiscuous_mode: True ''' try: @@ -68,7 +87,20 @@ HAS_PYVMOMI = False -def create_port_group(host_system, portgroup_name, vlan_id, vswitch_name): +def create_network_policy(promiscuous_mode, forged_transmits, mac_changes): + + security_policy = vim.host.NetworkPolicy.SecurityPolicy() + if promiscuous_mode: + security_policy.allowPromiscuous = promiscuous_mode + if forged_transmits: + security_policy.forgedTransmits = forged_transmits + if mac_changes: + security_policy.macChanges = mac_changes + network_policy = vim.host.NetworkPolicy(security=security_policy) + return network_policy + + +def create_port_group(host_system, portgroup_name, vlan_id, vswitch_name, network_policy): config = vim.host.NetworkConfig() config.portgroup = [vim.host.PortGroup.Config()] @@ -77,7 +109,7 @@ def create_port_group(host_system, portgroup_name, vlan_id, vswitch_name): config.portgroup[0].spec.name = portgroup_name config.portgroup[0].spec.vlanId = vlan_id config.portgroup[0].spec.vswitchName = vswitch_name - config.portgroup[0].spec.policy = vim.host.NetworkPolicy() + config.portgroup[0].spec.policy = network_policy host_network_config_result = host_system.configManager.networkSystem.UpdateNetworkConfig(config, "modify") return True @@ -88,7 +120,8 @@ def main(): argument_spec = vmware_argument_spec() argument_spec.update(dict(portgroup_name=dict(required=True, type='str'), switch_name=dict(required=True, type='str'), - vlan_id=dict(required=True, type='int'))) + vlan_id=dict(required=True, type='int'), + network_policy=dict(required=False, type='dict', default={}))) module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False) @@ -98,6 +131,9 @@ def main(): portgroup_name = module.params['portgroup_name'] switch_name = module.params['switch_name'] vlan_id = module.params['vlan_id'] + promiscuous_mode = module.params['network_policy'].get('promiscuous_mode', None) + forged_transmits = module.params['network_policy'].get('forged_transmits', None) + mac_changes = module.params['network_policy'].get('mac_changes', None) try: content = connect_to_api(module) @@ -109,7 +145,8 @@ def main(): if find_host_portgroup_by_name(host_system, portgroup_name): module.exit_json(changed=False) - changed = create_port_group(host_system, portgroup_name, vlan_id, switch_name) + network_policy = create_network_policy(promiscuous_mode, forged_transmits, mac_changes) + changed = create_port_group(host_system, portgroup_name, vlan_id, switch_name, network_policy) module.exit_json(changed=changed) except vmodl.RuntimeFault as runtime_fault: From e0b3e2f7905b9b0951c1441148c33a03a9e40f78 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Sun, 3 Jul 2016 09:19:53 -0700 Subject: [PATCH 1787/2522] Adds coding conventions to the bigip_facts module (#2515) A number of coding conventions have been adopted for new F5 modules that are in development. To ensure common usage across the modules, this module needed to be updated to reflect those conventions. No functional code changes were made. --- network/f5/bigip_facts.py | 234 +++++++++++++++++++++----------------- 1 file changed, 129 insertions(+), 105 deletions(-) diff --git a/network/f5/bigip_facts.py b/network/f5/bigip_facts.py index 44221ca350e..eaa4bf85957 100644 --- a/network/f5/bigip_facts.py +++ b/network/f5/bigip_facts.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - +# # (c) 2013, Matt Hite # # This file is part of Ansible @@ -21,107 +21,113 @@ DOCUMENTATION = ''' --- module: bigip_facts -short_description: "Collect facts from F5 BIG-IP devices" +short_description: Collect facts from F5 BIG-IP devices description: - - "Collect facts from F5 BIG-IP devices via iControl SOAP API" + - Collect facts from F5 BIG-IP devices via iControl SOAP API version_added: "1.6" author: - - Matt Hite (@mhite) - - Tim Rupp (@caphrim007) + - Matt Hite (@mhite) + - Tim Rupp (@caphrim007) notes: - - "Requires BIG-IP software version >= 11.4" - - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" - - "Best run as a local_action in your playbook" - - "Tested with manager and above account privilege level" - + - Requires BIG-IP software version >= 11.4 + - F5 developed module 'bigsuds' required (see http://devcentral.f5.com) + - Best run as a local_action in your playbook + - Tested with manager and above account privilege level requirements: - - bigsuds + - bigsuds options: - server: - description: - - BIG-IP host - required: true - default: null - choices: [] - aliases: [] - server_port: - description: - - BIG-IP server port - required: false - default: 443 - version_added: "2.2" - user: - description: - - BIG-IP username - required: true - default: null - choices: [] - aliases: [] - password: - description: - - BIG-IP password - required: true - default: null - choices: [] - aliases: [] - validate_certs: - description: - - If C(no), SSL certificates will not be validated. This should only be used - on personally controlled sites. Prior to 2.0, this module would always - validate on python >= 2.7.9 and never validate on python <= 2.7.8 - required: false - default: 'yes' - choices: ['yes', 'no'] - version_added: 2.0 - session: - description: - - BIG-IP session support; may be useful to avoid concurrency - issues in certain circumstances. - required: false - default: true - choices: [] - aliases: [] - include: - description: - - Fact category or list of categories to collect - required: true - default: null - choices: ['address_class', 'certificate', 'client_ssl_profile', - 'device', 'device_group', 'interface', 'key', 'node', 'pool', - 'rule', 'self_ip', 'software', 'system_info', 'traffic_group', - 'trunk', 'virtual_address', 'virtual_server', 'vlan'] - aliases: [] - filter: - description: - - Shell-style glob matching string used to filter fact keys. Not - applicable for software and system_info fact categories. - required: false - default: null - choices: [] - aliases: [] + server: + description: + - BIG-IP host + required: true + default: null + choices: [] + aliases: [] + server_port: + description: + - BIG-IP server port + required: false + default: 443 + version_added: "2.2" + user: + description: + - BIG-IP username + required: true + default: null + choices: [] + aliases: [] + password: + description: + - BIG-IP password + required: true + default: null + choices: [] + aliases: [] + validate_certs: + description: + - If C(no), SSL certificates will not be validated. This should only be used + on personally controlled sites. Prior to 2.0, this module would always + validate on python >= 2.7.9 and never validate on python <= 2.7.8 + required: false + default: yes + choices: + - yes + - no + version_added: 2.0 + session: + description: + - BIG-IP session support; may be useful to avoid concurrency + issues in certain circumstances. + required: false + default: true + choices: [] + aliases: [] + include: + description: + - Fact category or list of categories to collect + required: true + default: null + choices: + - address_class + - certificate + - client_ssl_profile + - device + - device_group + - interface + - key + - node + - pool + - rule + - self_ip + - software + - system_info + - traffic_group + - trunk + - virtual_address + - virtual_server + - vlan + aliases: [] + filter: + description: + - Shell-style glob matching string used to filter fact keys. Not + applicable for software and system_info fact categories. + required: false + default: null + choices: [] + aliases: [] ''' EXAMPLES = ''' - -## playbook task examples: - ---- -# file bigip-test.yml -# ... -- hosts: bigip-test - tasks: - - name: Collect BIG-IP facts - local_action: > - bigip_facts - server=lb.mydomain.com - user=admin - password=mysecret - include=interface,vlan - +- name: Collect BIG-IP facts + bigip_facts: + server: "lb.mydomain.com" + user: "admin" + password: "secret" + include: "interface,vlan" + delegate_to: localhost ''' try: - import bigsuds from suds import MethodNotFound, WebFault except ImportError: bigsuds_found = False @@ -129,12 +135,9 @@ bigsuds_found = True import fnmatch -import traceback import re +import traceback -# =========================================== -# bigip_facts module specific support methods. -# class F5(object): """F5 iControl class. @@ -976,6 +979,7 @@ def get_verification_status(self): def get_definition(self): return [x['rule_definition'] for x in self.api.LocalLB.Rule.query_rule(rule_names=self.rules)] + class Nodes(object): """Nodes class. @@ -1392,6 +1396,7 @@ def generate_dict(api_obj, fields): result_dict[j] = temp return result_dict + def generate_simple_dict(api_obj, fields): result_dict = {} for field in fields: @@ -1403,6 +1408,7 @@ def generate_simple_dict(api_obj, fields): result_dict[field] = api_response return result_dict + def generate_interface_dict(f5, regex): interfaces = Interfaces(f5.get_api(), regex) fields = ['active_media', 'actual_flow_control', 'bundle_state', @@ -1417,6 +1423,7 @@ def generate_interface_dict(f5, regex): 'stp_protocol_detection_reset_state'] return generate_dict(interfaces, fields) + def generate_self_ip_dict(f5, regex): self_ips = SelfIPs(f5.get_api(), regex) fields = ['address', 'allow_access_list', 'description', @@ -1425,6 +1432,7 @@ def generate_self_ip_dict(f5, regex): 'vlan', 'is_traffic_group_inherited'] return generate_dict(self_ips, fields) + def generate_trunk_dict(f5, regex): trunks = Trunks(f5.get_api(), regex) fields = ['active_lacp_state', 'configured_member_count', 'description', @@ -1434,6 +1442,7 @@ def generate_trunk_dict(f5, regex): 'stp_protocol_detection_reset_state'] return generate_dict(trunks, fields) + def generate_vlan_dict(f5, regex): vlans = Vlans(f5.get_api(), regex) fields = ['auto_lasthop', 'cmp_hash_algorithm', 'description', @@ -1445,6 +1454,7 @@ def generate_vlan_dict(f5, regex): 'source_check_state', 'true_mac_address', 'vlan_id'] return generate_dict(vlans, fields) + def generate_vs_dict(f5, regex): virtual_servers = VirtualServers(f5.get_api(), regex) fields = ['actual_hardware_acceleration', 'authentication_profile', @@ -1465,6 +1475,7 @@ def generate_vs_dict(f5, regex): 'translate_port_state', 'type', 'vlan', 'wildmask'] return generate_dict(virtual_servers, fields) + def generate_pool_dict(f5, regex): pools = Pools(f5.get_api(), regex) fields = ['action_on_service_down', 'active_member_count', @@ -1481,6 +1492,7 @@ def generate_pool_dict(f5, regex): 'simple_timeout', 'slow_ramp_time'] return generate_dict(pools, fields) + def generate_device_dict(f5, regex): devices = Devices(f5.get_api(), regex) fields = ['active_modules', 'base_mac_address', 'blade_addresses', @@ -1493,14 +1505,16 @@ def generate_device_dict(f5, regex): 'timelimited_modules', 'timezone', 'unicast_addresses'] return generate_dict(devices, fields) + def generate_device_group_dict(f5, regex): device_groups = DeviceGroups(f5.get_api(), regex) - fields = ['all_preferred_active', 'autosync_enabled_state','description', + fields = ['all_preferred_active', 'autosync_enabled_state', 'description', 'device', 'full_load_on_sync_state', 'incremental_config_sync_size_maximum', 'network_failover_enabled_state', 'sync_status', 'type'] return generate_dict(device_groups, fields) + def generate_traffic_group_dict(f5, regex): traffic_groups = TrafficGroups(f5.get_api(), regex) fields = ['auto_failback_enabled_state', 'auto_failback_time', @@ -1509,12 +1523,14 @@ def generate_traffic_group_dict(f5, regex): 'unit_id'] return generate_dict(traffic_groups, fields) + def generate_rule_dict(f5, regex): rules = Rules(f5.get_api(), regex) fields = ['definition', 'description', 'ignore_vertification', 'verification_status'] return generate_dict(rules, fields) + def generate_node_dict(f5, regex): nodes = Nodes(f5.get_api(), regex) fields = ['address', 'connection_limit', 'description', 'dynamic_ratio', @@ -1522,6 +1538,7 @@ def generate_node_dict(f5, regex): 'object_status', 'rate_limit', 'ratio', 'session_status'] return generate_dict(nodes, fields) + def generate_virtual_address_dict(f5, regex): virtual_addresses = VirtualAddresses(f5.get_api(), regex) fields = ['address', 'arp_state', 'auto_delete_state', 'connection_limit', @@ -1530,19 +1547,23 @@ def generate_virtual_address_dict(f5, regex): 'route_advertisement_state', 'traffic_group'] return generate_dict(virtual_addresses, fields) + def generate_address_class_dict(f5, regex): address_classes = AddressClasses(f5.get_api(), regex) fields = ['address_class', 'description'] return generate_dict(address_classes, fields) + def generate_certificate_dict(f5, regex): certificates = Certificates(f5.get_api(), regex) return dict(zip(certificates.get_list(), certificates.get_certificate_list())) + def generate_key_dict(f5, regex): keys = Keys(f5.get_api(), regex) return dict(zip(keys.get_list(), keys.get_key_list())) + def generate_client_ssl_profile_dict(f5, regex): profiles = ProfileClientSSL(f5.get_api(), regex) fields = ['alert_timeout', 'allow_nonssl_state', 'authenticate_depth', @@ -1566,6 +1587,7 @@ def generate_client_ssl_profile_dict(f5, regex): 'unclean_shutdown_state', 'is_base_profile', 'is_system_profile'] return generate_dict(profiles, fields) + def generate_system_info_dict(f5): system_info = SystemInfo(f5.get_api()) fields = ['base_mac_address', @@ -1578,6 +1600,7 @@ def generate_system_info_dict(f5): 'time_zone', 'uptime'] return generate_simple_dict(system_info, fields) + def generate_software_list(f5): software = Software(f5.get_api()) software_list = software.get_all_software_status() @@ -1585,16 +1608,17 @@ def generate_software_list(f5): def main(): + argument_spec = f5_argument_spec() + + meta_args = dict( + session=dict(type='bool', default=False), + include=dict(type='list', required=True), + filter=dict(type='str', required=False), + ) + argument_spec.update(meta_args) + module = AnsibleModule( - argument_spec = dict( - server = dict(type='str', required=True), - user = dict(type='str', required=True), - password = dict(type='str', required=True), - validate_certs = dict(default='yes', type='bool'), - session = dict(type='bool', default=False), - include = dict(type='list', required=True), - filter = dict(type='str', required=False), - ) + argument_spec=argument_spec ) if not bigsuds_found: @@ -1685,7 +1709,7 @@ def main(): result = {'ansible_facts': facts} - except Exception, e: + except Exception as e: module.fail_json(msg="received exception: %s\ntraceback: %s" % (e, traceback.format_exc())) module.exit_json(**result) From dcee274ebba96bdf1bb18972b8975039dd0fd7eb Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Tue, 5 Jul 2016 21:34:27 +0900 Subject: [PATCH 1788/2522] Fix initializing err in _get_err_from_resp_json --- cloud/lxd/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud/lxd/__init__.py b/cloud/lxd/__init__.py index 23161ef6312..46b2fda6ed9 100644 --- a/cloud/lxd/__init__.py +++ b/cloud/lxd/__init__.py @@ -124,6 +124,7 @@ def _raise_err_from_json(self, resp_json): @staticmethod def _get_err_from_resp_json(resp_json): + err = None metadata = resp_json.get('metadata', None) if metadata is not None: err = metadata.get('err', None) From afbaffa1c580329f624f6383f525afd8bee77906 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Tue, 5 Jul 2016 21:37:41 +0900 Subject: [PATCH 1789/2522] Change command to check python is installed --- cloud/lxd/lxd_container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 77fa7bc1402..12a64775215 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -170,7 +170,7 @@ - name: check python is installed in container delegate_to: mycontainer - raw: dpkg-query -W -f='${Status}' python + raw: dpkg -s python register: python_install_check failed_when: python_install_check.rc not in [0, 1] changed_when: false From b7e21d161e8b4cd87135286b037307c0caf92f82 Mon Sep 17 00:00:00 2001 From: blinkiz Date: Wed, 6 Jul 2016 07:14:31 +0200 Subject: [PATCH 1790/2522] Update openvswitch_port with tag feature (#2522) * Update openvswitch_port with tag feature Possibility to create a port with VLAN tag. * Update openvswitch_port.py --- network/openvswitch_port.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/network/openvswitch_port.py b/network/openvswitch_port.py index 3ce6f7f2f12..d23fdc28386 100644 --- a/network/openvswitch_port.py +++ b/network/openvswitch_port.py @@ -40,6 +40,11 @@ required: true description: - Name of port to manage on the bridge + tag: + version_added: 2.2 + required: false + description: + - VLAN tag for this port state: required: false default: "present" @@ -73,6 +78,10 @@ - openvswitch_port: bridge=bridge-loop port=eth6 state=present set="Interface eth6 ofport_request=6" +# Creates port vlan10 with tag 10 on bridge br-ex +- openvswitch_port: bridge=br-ex port=vlan10 tag=10 state=present + set="Interface vlan10 type=internal" + # Assign interface id server1-vifeth6 and mac address 52:54:00:30:6d:11 # to port vifeth6 and setup port to be managed by a controller. - openvswitch_port: bridge=br-int port=vifeth6 state=present @@ -118,6 +127,7 @@ def __init__(self, module): self.module = module self.bridge = module.params['bridge'] self.port = module.params['port'] + self.tag = module.params['tag'] self.state = module.params['state'] self.timeout = module.params['timeout'] self.set_opt = module.params.get('set', None) @@ -167,6 +177,8 @@ def set(self, set_opt): def add(self): '''Add the port''' cmd = ['add-port', self.bridge, self.port] + if self.tag: + cmd += ["tag=" + self.tag] if self.set and self.set_opt: cmd += ["--", "set"] cmd += self.set_opt.split(" ") @@ -235,6 +247,7 @@ def main(): argument_spec={ 'bridge': {'required': True}, 'port': {'required': True}, + 'tag': {'required': False}, 'state': {'default': 'present', 'choices': ['present', 'absent']}, 'timeout': {'default': 5, 'type': 'int'}, 'set': {'required': False, 'default': None}, From 346ec3ce3c0488142845ad052409ca57d632bc54 Mon Sep 17 00:00:00 2001 From: Shinichi TAMURA Date: Wed, 6 Jul 2016 14:20:23 +0900 Subject: [PATCH 1791/2522] Added fallback for parse_error(string) on elasticsearch_plugin module. (#2517) --- packaging/elasticsearch_plugin.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packaging/elasticsearch_plugin.py b/packaging/elasticsearch_plugin.py index 02fc674de1c..7472c033158 100644 --- a/packaging/elasticsearch_plugin.py +++ b/packaging/elasticsearch_plugin.py @@ -118,7 +118,10 @@ def is_plugin_present(plugin_dir, working_dir): def parse_error(string): reason = "reason: " - return string[string.index(reason) + len(reason):].strip() + try: + return string[string.index(reason) + len(reason):].strip() + except ValueError: + return string def main(): From c320caf2853cf72254a37fc7a81af1db41e1c2d8 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Thu, 7 Jul 2016 00:34:51 +0900 Subject: [PATCH 1792/2522] Fix lxd_container and lxd_profile document to be rendered properly --- cloud/lxd/lxd_container.py | 87 ++++++++++++++++++-------------------- cloud/lxd/lxd_profile.py | 63 +++++++++++++-------------- 2 files changed, 69 insertions(+), 81 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 12a64775215..17a8a0fa233 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -19,7 +19,7 @@ # along with Ansible. If not, see . -DOCUMENTATION = """ +DOCUMENTATION = ''' --- module: lxd_container short_description: Manage LXD Containers @@ -35,17 +35,16 @@ architecture: description: - The archiecture for the container (e.g. "x86_64" or "i686"). - See https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1 + See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1) required: false config: description: - - > - The config for the container (e.g. {"limits.cpu": "2"}). - See https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1 + - 'The config for the container (e.g. {"limits.cpu": "2"}). + See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1)' - If the container already exists and its "config" value in metadata obtained from GET /1.0/containers/ - https://github.com/lxc/lxd/blob/master/doc/rest-api.md#10containersname + U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#10containersname) are different, they this module tries to apply the configurations. - The key starts with 'volatile.' are ignored for this comparison. - Not all config values are supported to apply the existing container. @@ -53,26 +52,24 @@ required: false devices: description: - - > - The devices for the container + - 'The devices for the container (e.g. { "rootfs": { "path": "/dev/kvm", "type": "unix-char" }). - See https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1 + See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1)' required: false ephemeral: description: - Whether or not the container is ephemeral (e.g. true or false). - See https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1 + See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1) required: false source: description: - - > - The source for the container + - 'The source for the container (e.g. { "type": "image", "mode": "pull", "server": "https://images.linuxcontainers.org", "protocol": "lxd", "alias": "ubuntu/xenial/amd64" }). - See https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1 + See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1)' required: false state: choices: @@ -95,14 +92,14 @@ default: 30 wait_for_ipv4_addresses: description: - - If this is true, the lxd_module waits until IPv4 addresses + - If this is true, the M(lxd_container) waits until IPv4 addresses are set to the all network interfaces in the container after starting or restarting. required: false default: false force_stop: description: - - If this is true, the lxd_module forces to stop the container + - If this is true, the M(lxd_container) forces to stop the container when it stops or restarts the container. required: false default: false @@ -115,21 +112,19 @@ description: - The client certificate key file path. required: false - default: > - '{}/.config/lxc/client.key'.format(os.environ['HOME']) + default: '"{}/.config/lxc/client.key" .format(os.environ["HOME"])' cert_file: description: - The client certificate file path. required: false - default: > - '{}/.config/lxc/client.crt'.format(os.environ['HOME']) + default: '"{}/.config/lxc/client.crt" .format(os.environ["HOME"])' trust_password: description: - The client trusted password. - You need to set this password on the LXD server before running this module using the following command. lxc config set core.trust_password - See https://www.stgraber.org/2016/04/18/lxd-api-direct-interaction/ + See U(https://www.stgraber.org/2016/04/18/lxd-api-direct-interaction/) - If trust_password is set, this module send a request for authentication before sending any requests. required: false @@ -142,14 +137,14 @@ 2.1, the later requires python to be installed in the container which can be done with the command module. - You can copy a file from the host to the container - with the Ansible `copy` and `template` module and the `lxd` connection plugin. + with the Ansible M(copy) and M(templater) module and the `lxd` connection plugin. See the example below. - You can copy a file in the creatd container to the localhost with `command=lxc file pull container_name/dir/filename filename`. See the first example below. -""" +''' -EXAMPLES = """ +EXAMPLES = ''' # An example for creating a Ubuntu container and install python - hosts: localhost connection: local @@ -225,32 +220,30 @@ src: /etc/hosts dest: /tmp/mycontainer-hosts flat: true -""" +''' -RETURN=""" -lxd_container: - description: container information - returned: success +RETURN=''' +addresses: + description: Mapping from the network device name to a list of IPv4 addresses in the container + returned: when state is started or restarted type: object - contains: - addresses: - description: Mapping from the network device name to a list of IPv4 addresses in the container - returned: when state is started or restarted - type: object - sample: {"eth0": ["10.155.92.191"]} - old_state: - description: The old state of the container - returned: when state is started or restarted - sample: "stopped" - logs: - descriptions: The logs of requests and responses. - returned: when ansible-playbook is invoked with -vvvv. - actions: - description: List of actions performed for the container. - returned: success - type: list - sample: ["create", "start"] -""" + sample: {"eth0": ["10.155.92.191"]} +old_state: + description: The old state of the container + returned: when state is started or restarted + type: string + sample: "stopped" +logs: + description: The logs of requests and responses. + returned: when ansible-playbook is invoked with -vvvv. + type: list + sample: "(too long to be placed here)" +actions: + description: List of actions performed for the container. + returned: success + type: list + sample: '["create", "start"]' +''' import os from ansible.modules.extras.cloud.lxd import LXDClient, LXDClientException diff --git a/cloud/lxd/lxd_profile.py b/cloud/lxd/lxd_profile.py index 8158a401643..2584908623c 100644 --- a/cloud/lxd/lxd_profile.py +++ b/cloud/lxd/lxd_profile.py @@ -19,7 +19,7 @@ # along with Ansible. If not, see . -DOCUMENTATION = """ +DOCUMENTATION = ''' --- module: lxd_profile short_description: Manage LXD profiles @@ -34,29 +34,27 @@ required: true config: description: - - > - The config for the container (e.g. {"limits.memory": "4GB"}). - See https://github.com/lxc/lxd/blob/master/doc/rest-api.md#patch-3 + - 'The config for the container (e.g. {"limits.memory": "4GB"}). + See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#patch-3)' - If the profile already exists and its "config" value in metadata obtained from GET /1.0/profiles/ - https://github.com/lxc/lxd/blob/master/doc/rest-api.md#get-19 + U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#get-19) are different, they this module tries to apply the configurations. - Not all config values are supported to apply the existing profile. Maybe you need to delete and recreate a profile. required: false devices: description: - - > - The devices for the profile + - 'The devices for the profile (e.g. {"rootfs": {"path": "/dev/kvm", "type": "unix-char"}). - See https://github.com/lxc/lxd/blob/master/doc/rest-api.md#patch-3 + See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#patch-3)' required: false new_name: description: - A new name of a profile. - If this parameter is specified a profile will be renamed to this name. - See https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-11 + See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-11) required: false state: choices: @@ -75,21 +73,19 @@ description: - The client certificate key file path. required: false - default: > - '{}/.config/lxc/client.key'.format(os.environ['HOME']) + default: '"{}/.config/lxc/client.key" .format(os.environ["HOME"])' cert_file: description: - The client certificate file path. required: false - default: > - '{}/.config/lxc/client.crt'.format(os.environ['HOME']) + default: '"{}/.config/lxc/client.crt" .format(os.environ["HOME"])' trust_password: description: - The client trusted password. - You need to set this password on the LXD server before running this module using the following command. lxc config set core.trust_password - See https://www.stgraber.org/2016/04/18/lxd-api-direct-interaction/ + See U(https://www.stgraber.org/2016/04/18/lxd-api-direct-interaction/) - If trust_password is set, this module send a request for authentication before sending any requests. required: false @@ -97,9 +93,9 @@ - Profiles must have a unique name. If you attempt to create a profile with a name that already existed in the users namespace the module will simply return as "unchanged". -""" +''' -EXAMPLES = """ +EXAMPLES = ''' # An example for creating a profile - hosts: localhost connection: local @@ -155,26 +151,25 @@ name: macvlan new_name: macvlan2 state: present -""" +''' -RETURN=""" -lxd_profile: - description: profile information +RETURN=''' +old_state: + description: The old state of the profile returned: success - type: object - contains: - old_state: - description: The old state of the profile - sample: "absent" - logs: - descriptions: The logs of requests and responses. - returned: when ansible-playbook is invoked with -vvvv. - actions: - description: List of actions performed for the profile. - returned: success - type: list - sample: ["create"] -""" + type: string + sample: "absent" +logs: + description: The logs of requests and responses. + returned: when ansible-playbook is invoked with -vvvv. + type: list + sample: "(too long to be placed here)" +actions: + description: List of actions performed for the profile. + returned: success + type: list + sample: '["create"]' +''' import os from ansible.modules.extras.cloud.lxd import LXDClient, LXDClientException From 0f01acd6c4ddadb912016b1cc4a1d53d5aa66885 Mon Sep 17 00:00:00 2001 From: jonathanbouvier Date: Wed, 6 Jul 2016 12:41:52 -0400 Subject: [PATCH 1793/2522] added support for deleting nagios downtime (#2497) --- monitoring/nagios.py | 86 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/monitoring/nagios.py b/monitoring/nagios.py index 5b6e9796b1d..689e9f0903c 100644 --- a/monitoring/nagios.py +++ b/monitoring/nagios.py @@ -31,8 +31,9 @@ description: - Action to take. - servicegroup options were added in 2.0. + - delete_downtime options were added in 2.2. required: true - choices: [ "downtime", "enable_alerts", "disable_alerts", "silence", "unsilence", + choices: [ "downtime", "delete_downtime", "enable_alerts", "disable_alerts", "silence", "unsilence", "silence_nagios", "unsilence_nagios", "command", "servicegroup_service_downtime", "servicegroup_host_downtime" ] host: @@ -109,6 +110,12 @@ # set 30 minutes downtime for all host in servicegroup foo - nagios: action=servicegroup_host_downtime minutes=30 servicegroup=foo host={{ inventory_hostname }} +# delete all downtime for a given host +- nagios: action=delete_downtime host={{ inventory_hostname }} service=all + +# delete all downtime for HOST with a particular comment +- nagios: action=delete_downtime host={{ inventory_hostname }} service=host comment="Planned maintenance" + # enable SMART disk alerts - nagios: action=enable_alerts service=smart host={{ inventory_hostname }} @@ -181,6 +188,7 @@ def which_cmdfile(): def main(): ACTION_CHOICES = [ 'downtime', + 'delete_downtime', 'silence', 'unsilence', 'enable_alerts', @@ -242,6 +250,12 @@ def main(): except Exception: module.fail_json(msg='invalid entry for minutes') + ###################################################################### + if action == 'delete_downtime': + # Make sure there's an actual service selected + if not services: + module.fail_json(msg='no service selected to set downtime for') + ###################################################################### if action in ['servicegroup_service_downtime', 'servicegroup_host_downtime']: @@ -383,6 +397,47 @@ def _fmt_dt_str(self, cmd, host, duration, author=None, return dt_str + def _fmt_dt_del_str(self, cmd, host, svc=None, start=None, comment=None): + """ + Format an external-command downtime deletion string. + + cmd - Nagios command ID + host - Host to remove scheduled downtime from + comment - Reason downtime was added (upgrade, reboot, etc) + start - Start of downtime in seconds since 12:00AM Jan 1 1970 + svc - Service to remove downtime for, omit to remove all downtime for the host + + Syntax: [submitted] COMMAND;; + [];[];[] + """ + + entry_time = self._now() + hdr = "[%s] %s;%s;" % (entry_time, cmd, host) + + if comment is None: + comment = self.comment + + dt_del_args = [] + if svc is not None: + dt_del_args.append(svc) + else: + dt_del_args.append('') + + if start is not None: + dt_del_args.append(str(start)) + else: + dt_del_args.append('') + + if comment is not None: + dt_del_args.append(comment) + else: + dt_del_args.append('') + + dt_del_arg_str = ";".join(dt_del_args) + dt_del_str = hdr + dt_del_arg_str + "\n" + + return dt_del_str + def _fmt_notif_str(self, cmd, host=None, svc=None): """ Format an external-command notification string. @@ -462,6 +517,26 @@ def schedule_host_svc_downtime(self, host, minutes=30): dt_cmd_str = self._fmt_dt_str(cmd, host, minutes) self._write_command(dt_cmd_str) + def delete_host_downtime(self, host, services=None, comment=None): + """ + This command is used to remove scheduled downtime for a particular + host. + + Syntax: DEL_DOWNTIME_BY_HOST_NAME;; + [];[];[] + """ + + cmd = "DEL_DOWNTIME_BY_HOST_NAME" + + if services is None: + dt_del_cmd_str = self._fmt_dt_del_str(cmd, host, comment=comment) + self._write_command(dt_del_cmd_str) + else: + for service in services: + dt_del_cmd_str = self._fmt_dt_del_str(cmd, host, svc=service, comment=comment) + self._write_command(dt_del_cmd_str) + + def schedule_hostgroup_host_downtime(self, hostgroup, minutes=30): """ This command is used to schedule downtime for all hosts in a @@ -891,6 +966,15 @@ def act(self): self.schedule_svc_downtime(self.host, services=self.services, minutes=self.minutes) + + elif self.action == 'delete_downtime': + if self.services=='host': + self.delete_host_downtime(self.host) + elif self.services=='all': + self.delete_host_downtime(self.host, comment='') + else: + self.delete_host_downtime(self.host, services=self.services) + elif self.action == "servicegroup_host_downtime": if self.servicegroup: self.schedule_servicegroup_host_downtime(servicegroup = self.servicegroup, minutes = self.minutes) From 8502d2595a34906c713acba4d4e6b2dd992708f3 Mon Sep 17 00:00:00 2001 From: nitzmahone Date: Thu, 7 Jul 2016 10:56:57 -0700 Subject: [PATCH 1794/2522] fix azure_rm_deployment for rc5 --- cloud/azure/azure_rm_deployment.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cloud/azure/azure_rm_deployment.py b/cloud/azure/azure_rm_deployment.py index 82b5fd117f5..b908aa48936 100644 --- a/cloud/azure/azure_rm_deployment.py +++ b/cloud/azure/azure_rm_deployment.py @@ -473,8 +473,7 @@ def deploy_template(self): :return: """ - deploy_parameter = DeploymentProperties() - deploy_parameter.mode = self.deployment_mode + deploy_parameter = DeploymentProperties(self.deployment_mode) if not self.parameters_link: deploy_parameter.parameters = self.parameters else: From 46e7d2716a1bf89667ff841145972ce5f79854c2 Mon Sep 17 00:00:00 2001 From: Blake Covarrubias Date: Thu, 7 Jul 2016 15:33:24 -0700 Subject: [PATCH 1795/2522] =?UTF-8?q?Define=20external=5Fids=20=E2=80=99ty?= =?UTF-8?q?pe'=20in=20openvswitch=5Fbridge=20(#2523)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The external_ids 'type' was not defined in the argument spec of openvswitch_bridge. This caused 'external_ids' to be converted to a string leading to an error when later calling exp_external_ids.items(). --- network/openvswitch_bridge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/openvswitch_bridge.py b/network/openvswitch_bridge.py index 411b95b9dc1..68528dd478a 100644 --- a/network/openvswitch_bridge.py +++ b/network/openvswitch_bridge.py @@ -249,7 +249,7 @@ def main(): 'bridge': {'required': True}, 'state': {'default': 'present', 'choices': ['present', 'absent']}, 'timeout': {'default': 5, 'type': 'int'}, - 'external_ids': {'default': None}, + 'external_ids': {'default': None, 'type': 'dict'}, 'fail_mode': {'default': None}, }, supports_check_mode=True, From 47b9ede3be4b98f5041b2f72941ea74593310234 Mon Sep 17 00:00:00 2001 From: Andreas Nafpliotis Date: Fri, 8 Jul 2016 00:39:59 +0200 Subject: [PATCH 1796/2522] New module: vmware_local_users_manager (#2447) * Add files via upload * Add files via upload * Fixed build errors * Fixed some identation errors * Documentation corrections * Documentation updates --- cloud/vmware/vmware_local_user_manager.py | 197 ++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 cloud/vmware/vmware_local_user_manager.py diff --git a/cloud/vmware/vmware_local_user_manager.py b/cloud/vmware/vmware_local_user_manager.py new file mode 100644 index 00000000000..27ca703c206 --- /dev/null +++ b/cloud/vmware/vmware_local_user_manager.py @@ -0,0 +1,197 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright IBM Corp. 2016 +# Author(s): Andreas Nafpliotis + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see = 2.6" + - PyVmomi installed +options: + local_user_name: + description: + - The local user name to be changed + required: True + local_user_password: + description: + - The password to be set + required: False + local_user_description: + description: + - Description for the user + required: False + state: + description: + - Indicate desired state of the user. If the user already exists when C(state=present), the user info is updated + choices: ['present', 'absent'] + default: present +extends_documentation_fragment: vmware.documentation +''' + +EXAMPLES = ''' +# Example vmware_local_user_manager command from Ansible Playbooks +- name: Add local user to ESXi + local_action: + module: vmware_local_user_manager + hostname: esxi_hostname + username: root + password: vmware + local_user_name: foo +''' + +RETURN = ''' +changed: + description: A flag indicating if any change was made or not. + returned: success + type: boolean + sample: True +''' + +try: + from pyVmomi import vim, vmodl + HAS_PYVMOMI = True +except ImportError: + HAS_PYVMOMI = False + + +class VMwareLocalUserManager(object): + def __init__(self, module): + self.module = module + self.content = connect_to_api(self.module) + self.local_user_name = self.module.params['local_user_name'] + self.local_user_password = self.module.params['local_user_password'] + self.local_user_description = self.module.params['local_user_description'] + self.state = self.module.params['state'] + + def process_state(self): + try: + local_account_manager_states = { + 'absent': { + 'present': self.state_remove_user, + 'absent': self.state_exit_unchanged, + }, + 'present': { + 'present': self.state_update_user, + 'absent': self.state_create_user, + } + } + + local_account_manager_states[self.state][self.check_local_user_manager_state()]() + except vmodl.RuntimeFault as runtime_fault: + self.module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + self.module.fail_json(msg=method_fault.msg) + except Exception as e: + self.module.fail_json(msg=str(e)) + + + def check_local_user_manager_state(self): + user_account = self.find_user_account() + if not user_account: + return 'absent' + else: + return 'present' + + + def find_user_account(self): + searchStr = self.local_user_name + exactMatch = True + findUsers = True + findGroups = False + user_account = self.content.userDirectory.RetrieveUserGroups(None, searchStr, None, None, exactMatch, findUsers, findGroups) + return user_account + + + def create_account_spec(self): + account_spec = vim.host.LocalAccountManager.AccountSpecification() + account_spec.id = self.local_user_name + account_spec.password = self.local_user_password + account_spec.description = self.local_user_description + return account_spec + + + def state_create_user(self): + account_spec = self.create_account_spec() + + try: + task = self.content.accountManager.CreateUser(account_spec) + self.module.exit_json(changed=True) + except vmodl.RuntimeFault as runtime_fault: + module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + module.fail_json(msg=method_fault.msg) + + def state_update_user(self): + account_spec = self.create_account_spec() + + try: + task = self.content.accountManager.UpdateUser(account_spec) + self.module.exit_json(changed=True) + except vmodl.RuntimeFault as runtime_fault: + module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + module.fail_json(msg=method_fault.msg) + + + def state_remove_user(self): + try: + task = self.content.accountManager.RemoveUser(self.local_user_name) + self.module.exit_json(changed=True) + except vmodl.RuntimeFault as runtime_fault: + module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + module.fail_json(msg=method_fault.msg) + + + def state_exit_unchanged(self): + self.module.exit_json(changed=False) + + + +def main(): + + argument_spec = vmware_argument_spec() + argument_spec.update(dict(local_user_name=dict(required=True, type='str'), + local_user_password=dict(required=False, type='str', no_log=True), + local_user_description=dict(required=False, type='str'), + state=dict(default='present', choices=['present', 'absent'], type='str'))) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False) + + if not HAS_PYVMOMI: + module.fail_json(msg='pyvmomi is required for this module') + + vmware_local_user_manager = VMwareLocalUserManager(module) + vmware_local_user_manager.process_state() + +from ansible.module_utils.vmware import * +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From d5ebe033e34322dd7f3ff725715ea2133b915425 Mon Sep 17 00:00:00 2001 From: John Kerkstra Date: Thu, 7 Jul 2016 17:43:12 -0500 Subject: [PATCH 1797/2522] ec2_vpc_route_table: fixes: #2377 (#2421) fixed fatal error when propagating_vgw_ids is None --- cloud/amazon/ec2_vpc_route_table.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index 1e9de6ec177..58a8795372b 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -319,7 +319,8 @@ def ensure_routes(vpc_conn, route_table, route_specs, propagating_vgw_ids, # VGWs in place. routes_to_delete = [r for r in routes_to_match if r.gateway_id != 'local' - and r.gateway_id not in propagating_vgw_ids] + and (propagating_vgw_ids is not None + and r.gateway_id not in propagating_vgw_ids)] changed = routes_to_delete or route_specs_to_create if changed: From 188a01ff1a241fe239516bea603f3e773b23bcd8 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Thu, 7 Jul 2016 22:56:09 -0700 Subject: [PATCH 1798/2522] Adds style conventions to bigip_pool_member (#2535) A number of coding conventions have been adopted for new F5 modules that are in development. To ensure common usage across the modules, this module needed to be updated to reflect those conventions. No functional code changes were made. --- network/f5/bigip_pool_member.py | 505 ++++++++++++++++++-------------- 1 file changed, 286 insertions(+), 219 deletions(-) diff --git a/network/f5/bigip_pool_member.py b/network/f5/bigip_pool_member.py index 24914c992f4..99fc515ede9 100644 --- a/network/f5/bigip_pool_member.py +++ b/network/f5/bigip_pool_member.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - +# # (c) 2013, Matt Hite # # This file is part of Ansible @@ -21,204 +21,212 @@ DOCUMENTATION = ''' --- module: bigip_pool_member -short_description: "Manages F5 BIG-IP LTM pool members" +short_description: Manages F5 BIG-IP LTM pool members description: - - "Manages F5 BIG-IP LTM pool members via iControl SOAP API" -version_added: "1.4" + - Manages F5 BIG-IP LTM pool members via iControl SOAP API +version_added: 1.4 author: - - Matt Hite (@mhite) - - Tim Rupp (@caphrim007) + - Matt Hite (@mhite) + - Tim Rupp (@caphrim007) notes: - - "Requires BIG-IP software version >= 11" - - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" - - "Best run as a local_action in your playbook" - - "Supersedes bigip_pool for managing pool members" - + - Requires BIG-IP software version >= 11 + - F5 developed module 'bigsuds' required (see http://devcentral.f5.com) + - Best run as a local_action in your playbook + - Supersedes bigip_pool for managing pool members requirements: - - bigsuds + - bigsuds options: - server: - description: - - BIG-IP host - required: true - server_port: - description: - - BIG-IP server port - required: false - default: 443 - version_added: "2.2" - user: - description: - - BIG-IP username - required: true - password: - description: - - BIG-IP password - required: true - validate_certs: - description: - - If C(no), SSL certificates will not be validated. This should only be used - on personally controlled sites. Prior to 2.0, this module would always - validate on python >= 2.7.9 and never validate on python <= 2.7.8 - required: false - default: 'yes' - choices: ['yes', 'no'] - version_added: 2.0 - state: - description: - - Pool member state - required: true - default: present - choices: ['present', 'absent'] - session_state: - description: - - Set new session availability status for pool member - version_added: "2.0" - required: false - default: null - choices: ['enabled', 'disabled'] - monitor_state: - description: - - Set monitor availability status for pool member - version_added: "2.0" - required: false - default: null - choices: ['enabled', 'disabled'] - pool: - description: - - Pool name. This pool must exist. - required: true - partition: - description: - - Partition - required: false - default: 'Common' - host: - description: - - Pool member IP - required: true - aliases: ['address', 'name'] - port: - description: - - Pool member port - required: true - connection_limit: - description: - - Pool member connection limit. Setting this to 0 disables the limit. - required: false - default: null + server: + description: + - BIG-IP host + required: true + server_port: + description: + - BIG-IP server port + required: false + default: 443 + version_added: "2.2" + user: + description: + - BIG-IP username + required: true + password: + description: + - BIG-IP password + required: true + validate_certs: + description: + - If C(no), SSL certificates will not be validated. This should only be used + on personally controlled sites. Prior to 2.0, this module would always + validate on python >= 2.7.9 and never validate on python <= 2.7.8 + required: false + default: 'yes' + choices: + - yes + - no + version_added: 2.0 + state: + description: + - Pool member state + required: true + default: present + choices: + - present + - absent + session_state: + description: + - Set new session availability status for pool member + version_added: 2.0 + required: false + default: null + choices: + - enabled + - disabled + monitor_state: + description: + - Set monitor availability status for pool member + version_added: 2.0 + required: false + default: null + choices: + - enabled + - disabled + pool: + description: + - Pool name. This pool must exist. + required: true + partition: + description: + - Partition + required: false + default: 'Common' + host: + description: + - Pool member IP + required: true + aliases: + - address + - name + port: description: - description: - - Pool member description - required: false - default: null - rate_limit: - description: - - Pool member rate limit (connections-per-second). Setting this to 0 disables the limit. - required: false - default: null - ratio: - description: - - Pool member ratio weight. Valid values range from 1 through 100. New pool members -- unless overriden with this value -- default to 1. - required: false - default: null - preserve_node: - description: - - When state is absent and the pool member is no longer referenced in other pools, the default behavior removes the unused node object. Setting this to 'yes' disables this behavior. - required: false - default: 'no' - choices: ['yes', 'no'] - version_added: 2.1 + - Pool member port + required: true + connection_limit: + description: + - Pool member connection limit. Setting this to 0 disables the limit. + required: false + default: null + description: + description: + - Pool member description + required: false + default: null + rate_limit: + description: + - Pool member rate limit (connections-per-second). Setting this to 0 + disables the limit. + required: false + default: null + ratio: + description: + - Pool member ratio weight. Valid values range from 1 through 100. + New pool members -- unless overriden with this value -- default + to 1. + required: false + default: null + preserve_node: + description: + - When state is absent and the pool member is no longer referenced + in other pools, the default behavior removes the unused node + o bject. Setting this to 'yes' disables this behavior. + required: false + default: 'no' + choices: + - yes + - no + version_added: 2.1 ''' EXAMPLES = ''' - -## playbook task examples: - ---- -# file bigip-test.yml -# ... -- hosts: bigip-test - tasks: - - name: Add pool member - local_action: > - bigip_pool_member - server=lb.mydomain.com - user=admin - password=mysecret - state=present - pool=matthite-pool - partition=matthite - host="{{ ansible_default_ipv4["address"] }}" - port=80 - description="web server" - connection_limit=100 - rate_limit=50 - ratio=2 - - - name: Modify pool member ratio and description - local_action: > - bigip_pool_member - server=lb.mydomain.com - user=admin - password=mysecret - state=present - pool=matthite-pool - partition=matthite - host="{{ ansible_default_ipv4["address"] }}" - port=80 - ratio=1 - description="nginx server" - - - name: Remove pool member from pool - local_action: > - bigip_pool_member - server=lb.mydomain.com - user=admin - password=mysecret - state=absent - pool=matthite-pool - partition=matthite - host="{{ ansible_default_ipv4["address"] }}" - port=80 - - - # The BIG-IP GUI doesn't map directly to the API calls for "Pool -> - # Members -> State". The following states map to API monitor - # and session states. - # - # Enabled (all traffic allowed): - # monitor_state=enabled, session_state=enabled - # Disabled (only persistent or active connections allowed): - # monitor_state=enabled, session_state=disabled - # Forced offline (only active connections allowed): - # monitor_state=disabled, session_state=disabled - # - # See https://devcentral.f5.com/questions/icontrol-equivalent-call-for-b-node-down - - - name: Force pool member offline - local_action: > - bigip_pool_member - server=lb.mydomain.com - user=admin - password=mysecret - state=present - session_state=disabled - monitor_state=disabled - pool=matthite-pool - partition=matthite - host="{{ ansible_default_ipv4["address"] }}" - port=80 - +- name: Add pool member + bigip_pool_member: + server: "lb.mydomain.com" + user: "admin" + password: "secret" + state: "present" + pool: "my-pool" + partition: "Common" + host: "{{ ansible_default_ipv4["address"] }}" + port: 80 + description: "web server" + connection_limit: 100 + rate_limit: 50 + ratio: 2 + delegate_to: localhost + +- name: Modify pool member ratio and description + bigip_pool_member: + server: "lb.mydomain.com" + user: "admin" + password: "secret" + state: "present" + pool: "my-pool" + partition: "Common" + host: "{{ ansible_default_ipv4["address"] }}" + port: 80 + ratio: 1 + description: "nginx server" + delegate_to: localhost + +- name: Remove pool member from pool + bigip_pool_member: + server: "lb.mydomain.com" + user: "admin" + password: "secret" + state: "absent" + pool: "my-pool" + partition: "Common" + host: "{{ ansible_default_ipv4["address"] }}" + port: 80 + delegate_to: localhost + + +# The BIG-IP GUI doesn't map directly to the API calls for "Pool -> +# Members -> State". The following states map to API monitor +# and session states. +# +# Enabled (all traffic allowed): +# monitor_state=enabled, session_state=enabled +# Disabled (only persistent or active connections allowed): +# monitor_state=enabled, session_state=disabled +# Forced offline (only active connections allowed): +# monitor_state=disabled, session_state=disabled +# +# See https://devcentral.f5.com/questions/icontrol-equivalent-call-for-b-node-down + +- name: Force pool member offline + bigip_pool_member: + server: "lb.mydomain.com" + user: "admin" + password: "secret" + state: "present" + session_state: "disabled" + monitor_state: "disabled" + pool: "my-pool" + partition: "Common" + host: "{{ ansible_default_ipv4["address"] }}" + port: 80 + delegate_to: localhost ''' + def pool_exists(api, pool): # hack to determine if pool exists result = False try: api.LocalLB.Pool.get_object_status(pool_names=[pool]) result = True - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed as e: if "was not found" in str(e): result = False else: @@ -226,6 +234,7 @@ def pool_exists(api, pool): raise return result + def member_exists(api, pool, address, port): # hack to determine if member exists result = False @@ -234,7 +243,7 @@ def member_exists(api, pool, address, port): api.LocalLB.Pool.get_member_object_status(pool_names=[pool], members=[members]) result = True - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed as e: if "was not found" in str(e): result = False else: @@ -242,12 +251,13 @@ def member_exists(api, pool, address, port): raise return result + def delete_node_address(api, address): result = False try: api.LocalLB.NodeAddressV2.delete_node_address(nodes=[address]) result = True - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed as e: if "is referenced by a member of pool" in str(e): result = False else: @@ -255,96 +265,157 @@ def delete_node_address(api, address): raise return result + def remove_pool_member(api, pool, address, port): members = [{'address': address, 'port': port}] - api.LocalLB.Pool.remove_member_v2(pool_names=[pool], members=[members]) + api.LocalLB.Pool.remove_member_v2( + pool_names=[pool], + members=[members] + ) + def add_pool_member(api, pool, address, port): members = [{'address': address, 'port': port}] - api.LocalLB.Pool.add_member_v2(pool_names=[pool], members=[members]) + api.LocalLB.Pool.add_member_v2( + pool_names=[pool], + members=[members] + ) + def get_connection_limit(api, pool, address, port): members = [{'address': address, 'port': port}] - result = api.LocalLB.Pool.get_member_connection_limit(pool_names=[pool], members=[members])[0][0] + result = api.LocalLB.Pool.get_member_connection_limit( + pool_names=[pool], + members=[members] + )[0][0] return result + def set_connection_limit(api, pool, address, port, limit): members = [{'address': address, 'port': port}] - api.LocalLB.Pool.set_member_connection_limit(pool_names=[pool], members=[members], limits=[[limit]]) + api.LocalLB.Pool.set_member_connection_limit( + pool_names=[pool], + members=[members], + limits=[[limit]] + ) + def get_description(api, pool, address, port): members = [{'address': address, 'port': port}] - result = api.LocalLB.Pool.get_member_description(pool_names=[pool], members=[members])[0][0] + result = api.LocalLB.Pool.get_member_description( + pool_names=[pool], + members=[members] + )[0][0] return result + def set_description(api, pool, address, port, description): members = [{'address': address, 'port': port}] - api.LocalLB.Pool.set_member_description(pool_names=[pool], members=[members], descriptions=[[description]]) + api.LocalLB.Pool.set_member_description( + pool_names=[pool], + members=[members], + descriptions=[[description]] + ) + def get_rate_limit(api, pool, address, port): members = [{'address': address, 'port': port}] - result = api.LocalLB.Pool.get_member_rate_limit(pool_names=[pool], members=[members])[0][0] + result = api.LocalLB.Pool.get_member_rate_limit( + pool_names=[pool], + members=[members] + )[0][0] return result + def set_rate_limit(api, pool, address, port, limit): members = [{'address': address, 'port': port}] - api.LocalLB.Pool.set_member_rate_limit(pool_names=[pool], members=[members], limits=[[limit]]) + api.LocalLB.Pool.set_member_rate_limit( + pool_names=[pool], + members=[members], + limits=[[limit]] + ) + def get_ratio(api, pool, address, port): members = [{'address': address, 'port': port}] - result = api.LocalLB.Pool.get_member_ratio(pool_names=[pool], members=[members])[0][0] + result = api.LocalLB.Pool.get_member_ratio( + pool_names=[pool], + members=[members] + )[0][0] return result + def set_ratio(api, pool, address, port, ratio): members = [{'address': address, 'port': port}] - api.LocalLB.Pool.set_member_ratio(pool_names=[pool], members=[members], ratios=[[ratio]]) + api.LocalLB.Pool.set_member_ratio( + pool_names=[pool], + members=[members], + ratios=[[ratio]] + ) + def set_member_session_enabled_state(api, pool, address, port, session_state): members = [{'address': address, 'port': port}] session_state = ["STATE_%s" % session_state.strip().upper()] - api.LocalLB.Pool.set_member_session_enabled_state(pool_names=[pool], members=[members], session_states=[session_state]) + api.LocalLB.Pool.set_member_session_enabled_state( + pool_names=[pool], + members=[members], + session_states=[session_state] + ) + def get_member_session_status(api, pool, address, port): members = [{'address': address, 'port': port}] - result = api.LocalLB.Pool.get_member_session_status(pool_names=[pool], members=[members])[0][0] + result = api.LocalLB.Pool.get_member_session_status( + pool_names=[pool], + members=[members] + )[0][0] result = result.split("SESSION_STATUS_")[-1].lower() return result + def set_member_monitor_state(api, pool, address, port, monitor_state): members = [{'address': address, 'port': port}] monitor_state = ["STATE_%s" % monitor_state.strip().upper()] - api.LocalLB.Pool.set_member_monitor_state(pool_names=[pool], members=[members], monitor_states=[monitor_state]) + api.LocalLB.Pool.set_member_monitor_state( + pool_names=[pool], + members=[members], + monitor_states=[monitor_state] + ) + def get_member_monitor_status(api, pool, address, port): members = [{'address': address, 'port': port}] - result = api.LocalLB.Pool.get_member_monitor_status(pool_names=[pool], members=[members])[0][0] + result = api.LocalLB.Pool.get_member_monitor_status( + pool_names=[pool], + members=[members] + )[0][0] result = result.split("MONITOR_STATUS_")[-1].lower() return result + def main(): argument_spec = f5_argument_spec() - argument_spec.update(dict( - session_state = dict(type='str', choices=['enabled', 'disabled']), - monitor_state = dict(type='str', choices=['enabled', 'disabled']), - pool = dict(type='str', required=True), - host = dict(type='str', required=True, aliases=['address', 'name']), - port = dict(type='int', required=True), - connection_limit = dict(type='int'), - description = dict(type='str'), - rate_limit = dict(type='int'), - ratio = dict(type='int'), - preserve_node = dict(type='bool', default=False) - ) + + meta_args = dict( + session_state=dict(type='str', choices=['enabled', 'disabled']), + monitor_state=dict(type='str', choices=['enabled', 'disabled']), + pool=dict(type='str', required=True), + host=dict(type='str', required=True, aliases=['address', 'name']), + port=dict(type='int', required=True), + connection_limit=dict(type='int'), + description=dict(type='str'), + rate_limit=dict(type='int'), + ratio=dict(type='int'), + preserve_node=dict(type='bool', default=False) ) + argument_spec.update(meta_args) module = AnsibleModule( - argument_spec = argument_spec, + argument_spec=argument_spec, supports_check_mode=True ) - if not bigsuds_found: - module.fail_json(msg="the python bigsuds module is required") - if module.params['validate_certs']: import ssl if not hasattr(ssl, 'SSLContext'): @@ -370,9 +441,6 @@ def main(): port = module.params['port'] preserve_node = module.params['preserve_node'] - - # sanity check user supplied values - if (host and port is None) or (port is not None and not host): module.fail_json(msg="both host and port must be supplied") @@ -453,12 +521,11 @@ def main(): set_member_monitor_state(api, pool, address, port, monitor_state) result = {'changed': True} - except Exception, e: + except Exception as e: module.fail_json(msg="received exception: %s" % e) module.exit_json(**result) -# import module snippets from ansible.module_utils.basic import * from ansible.module_utils.f5 import * From 5a3dc054bd897e24749d69da41dcb9168c31c538 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Fri, 8 Jul 2016 10:54:31 +0200 Subject: [PATCH 1799/2522] doc: remove common return values from various modules --- cloud/centurylink/clc_aa_policy.py | 5 ----- cloud/centurylink/clc_alert_policy.py | 5 ----- cloud/centurylink/clc_blueprint_package.py | 5 ----- cloud/centurylink/clc_firewall_policy.py | 5 ----- cloud/centurylink/clc_group.py | 5 ----- cloud/centurylink/clc_loadbalancer.py | 5 ----- cloud/centurylink/clc_modify_server.py | 5 ----- cloud/centurylink/clc_publicip.py | 5 ----- cloud/centurylink/clc_server.py | 5 ----- cloud/centurylink/clc_server_snapshot.py | 5 ----- cloud/vmware/vmware_local_user_manager.py | 16 +++++----------- 11 files changed, 5 insertions(+), 61 deletions(-) diff --git a/cloud/centurylink/clc_aa_policy.py b/cloud/centurylink/clc_aa_policy.py index 7596651181a..681e71cb3a1 100644 --- a/cloud/centurylink/clc_aa_policy.py +++ b/cloud/centurylink/clc_aa_policy.py @@ -100,11 +100,6 @@ ''' RETURN = ''' -changed: - description: A flag indicating if any change was made or not - returned: success - type: boolean - sample: True policy: description: The anti affinity policy information returned: success diff --git a/cloud/centurylink/clc_alert_policy.py b/cloud/centurylink/clc_alert_policy.py index a4f07371dac..b8817b6618a 100644 --- a/cloud/centurylink/clc_alert_policy.py +++ b/cloud/centurylink/clc_alert_policy.py @@ -132,11 +132,6 @@ ''' RETURN = ''' -changed: - description: A flag indicating if any change was made or not - returned: success - type: boolean - sample: True policy: description: The alert policy information returned: success diff --git a/cloud/centurylink/clc_blueprint_package.py b/cloud/centurylink/clc_blueprint_package.py index 2b0a9774b62..4e8a392495a 100644 --- a/cloud/centurylink/clc_blueprint_package.py +++ b/cloud/centurylink/clc_blueprint_package.py @@ -81,11 +81,6 @@ ''' RETURN = ''' -changed: - description: A flag indicating if any change was made or not - returned: success - type: boolean - sample: True server_ids: description: The list of server ids that are changed returned: success diff --git a/cloud/centurylink/clc_firewall_policy.py b/cloud/centurylink/clc_firewall_policy.py index b37123b4ec0..c26128a40ba 100644 --- a/cloud/centurylink/clc_firewall_policy.py +++ b/cloud/centurylink/clc_firewall_policy.py @@ -130,11 +130,6 @@ ''' RETURN = ''' -changed: - description: A flag indicating if any change was made or not - returned: success - type: boolean - sample: True firewall_policy_id: description: The fire wall policy id returned: success diff --git a/cloud/centurylink/clc_group.py b/cloud/centurylink/clc_group.py index 01fbbe2e67e..f30b37d6ec1 100644 --- a/cloud/centurylink/clc_group.py +++ b/cloud/centurylink/clc_group.py @@ -112,11 +112,6 @@ ''' RETURN = ''' -changed: - description: A flag indicating if any change was made or not - returned: success - type: boolean - sample: True group: description: The group information returned: success diff --git a/cloud/centurylink/clc_loadbalancer.py b/cloud/centurylink/clc_loadbalancer.py index 7771a7ea362..abb421c755e 100644 --- a/cloud/centurylink/clc_loadbalancer.py +++ b/cloud/centurylink/clc_loadbalancer.py @@ -174,11 +174,6 @@ ''' RETURN = ''' -changed: - description: A flag indicating if any change was made or not - returned: success - type: boolean - sample: True loadbalancer: description: The load balancer result object from CLC returned: success diff --git a/cloud/centurylink/clc_modify_server.py b/cloud/centurylink/clc_modify_server.py index a7ccbaefc47..a676248ffd0 100644 --- a/cloud/centurylink/clc_modify_server.py +++ b/cloud/centurylink/clc_modify_server.py @@ -155,11 +155,6 @@ ''' RETURN = ''' -changed: - description: A flag indicating if any change was made or not - returned: success - type: boolean - sample: True server_ids: description: The list of server ids that are changed returned: success diff --git a/cloud/centurylink/clc_publicip.py b/cloud/centurylink/clc_publicip.py index 12cfda3c5d1..9c21a9a6156 100644 --- a/cloud/centurylink/clc_publicip.py +++ b/cloud/centurylink/clc_publicip.py @@ -111,11 +111,6 @@ ''' RETURN = ''' -changed: - description: A flag indicating if any change was made or not - returned: success - type: boolean - sample: True server_ids: description: The list of server ids that are changed returned: success diff --git a/cloud/centurylink/clc_server.py b/cloud/centurylink/clc_server.py index 42cd70ae8e8..3cb44040126 100644 --- a/cloud/centurylink/clc_server.py +++ b/cloud/centurylink/clc_server.py @@ -277,11 +277,6 @@ ''' RETURN = ''' -changed: - description: A flag indicating if any change was made or not - returned: success - type: boolean - sample: True server_ids: description: The list of server ids that are created returned: success diff --git a/cloud/centurylink/clc_server_snapshot.py b/cloud/centurylink/clc_server_snapshot.py index 6d6963e2097..6c7e8920e46 100644 --- a/cloud/centurylink/clc_server_snapshot.py +++ b/cloud/centurylink/clc_server_snapshot.py @@ -94,11 +94,6 @@ ''' RETURN = ''' -changed: - description: A flag indicating if any change was made or not - returned: success - type: boolean - sample: True server_ids: description: The list of server ids that are changed returned: success diff --git a/cloud/vmware/vmware_local_user_manager.py b/cloud/vmware/vmware_local_user_manager.py index 27ca703c206..c7e8ecb9311 100644 --- a/cloud/vmware/vmware_local_user_manager.py +++ b/cloud/vmware/vmware_local_user_manager.py @@ -60,18 +60,12 @@ local_action: module: vmware_local_user_manager hostname: esxi_hostname - username: root + username: root password: vmware - local_user_name: foo + local_user_name: foo ''' -RETURN = ''' -changed: - description: A flag indicating if any change was made or not. - returned: success - type: boolean - sample: True -''' +RETURN = '''# ''' try: from pyVmomi import vim, vmodl @@ -181,12 +175,12 @@ def main(): local_user_password=dict(required=False, type='str', no_log=True), local_user_description=dict(required=False, type='str'), state=dict(default='present', choices=['present', 'absent'], type='str'))) - + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False) if not HAS_PYVMOMI: module.fail_json(msg='pyvmomi is required for this module') - + vmware_local_user_manager = VMwareLocalUserManager(module) vmware_local_user_manager.process_state() From 26af6c2d2553cd99d61639cf71a252fe0cbf60ab Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Fri, 8 Jul 2016 08:28:30 -0700 Subject: [PATCH 1800/2522] Add module integration tests on shippable. --- shippable.yml | 48 +++++++++++++++++++++++++++ test/utils/shippable/ci.sh | 7 ++++ test/utils/shippable/integration.sh | 51 +++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 shippable.yml create mode 100755 test/utils/shippable/ci.sh create mode 100755 test/utils/shippable/integration.sh diff --git a/shippable.yml b/shippable.yml new file mode 100644 index 00000000000..5385520ba30 --- /dev/null +++ b/shippable.yml @@ -0,0 +1,48 @@ +language: python + +env: + matrix: + - TEST=none + +matrix: + exclude: + - env: TEST=none + include: + - env: TEST=integration IMAGE=ansible/ansible:centos6 + - env: TEST=integration IMAGE=ansible/ansible:centos7 + - env: TEST=integration IMAGE=ansible/ansible:fedora-rawhide + - env: TEST=integration IMAGE=ansible/ansible:fedora23 + - env: TEST=integration IMAGE=ansible/ansible:opensuseleap + - env: TEST=integration IMAGE=ansible/ansible:ubuntu1204 PRIVILEGED=true + - env: TEST=integration IMAGE=ansible/ansible:ubuntu1404 PRIVILEGED=true + - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604 +build: + pre_ci_boot: + options: "--privileged=false --net=bridge" + ci: + - test/utils/shippable/ci.sh + +integrations: + notifications: + - integrationName: email + type: email + on_success: never + on_failure: never + on_start: never + on_pull_request: never + - integrationName: irc + type: irc + recipients: + - "chat.freenode.net#ansible-notices" + on_success: change + on_failure: always + on_start: never + on_pull_request: always + - integrationName: slack + type: slack + recipients: + - "#shippable" + on_success: change + on_failure: always + on_start: never + on_pull_request: never diff --git a/test/utils/shippable/ci.sh b/test/utils/shippable/ci.sh new file mode 100755 index 00000000000..5c0f847e661 --- /dev/null +++ b/test/utils/shippable/ci.sh @@ -0,0 +1,7 @@ +#!/bin/bash -eux + +set -o pipefail + +source_root=$(python -c "from os import path; print(path.abspath(path.join(path.dirname('$0'), '../../..')))") + +"${source_root}/test/utils/shippable/${TEST}.sh" 2>&1 | gawk '{ print strftime("%Y-%m-%d %H:%M:%S"), $0; fflush(); }' diff --git a/test/utils/shippable/integration.sh b/test/utils/shippable/integration.sh new file mode 100755 index 00000000000..f67fd59db67 --- /dev/null +++ b/test/utils/shippable/integration.sh @@ -0,0 +1,51 @@ +#!/bin/bash -eux + +set -o pipefail + +ansible_repo_url="https://github.com/ansible/ansible.git" + +is_pr="${IS_PULL_REQUEST}" +build_dir="${SHIPPABLE_BUILD_DIR}" +repo="${REPO_NAME}" + +if [ "${is_pr}" != "true" ]; then + echo "Module integration tests are only supported on pull requests." + exit 1 +fi + +case "${repo}" in + "ansible-modules-core") + this_module_group="core" + other_module_group="extras" + ;; + "ansible-modules-extras") + this_module_group="extras" + other_module_group="core" + ;; + *) + echo "Unsupported repo name: ${repo}" + exit 1 + ;; +esac + +modules_tmp_dir="${build_dir}.tmp" +this_modules_dir="${build_dir}/lib/ansible/modules/${this_module_group}" +other_modules_dir="${build_dir}/lib/ansible/modules/${other_module_group}" + +cd / +mv "${build_dir}" "${modules_tmp_dir}" +git clone "${ansible_repo_url}" "${build_dir}" +cd "${build_dir}" +rmdir "${this_modules_dir}" +mv "${modules_tmp_dir}" "${this_modules_dir}" +mv "${this_modules_dir}/shippable" "${build_dir}" +git submodule init "${other_modules_dir}" +git submodule sync "${other_modules_dir}" +git submodule update "${other_modules_dir}" + +pip install -r test/utils/shippable/modules/generate-tests-requirements.txt --upgrade +pip list + +source hacking/env-setup + +test/utils/shippable/modules/generate-tests "${this_module_group}" --verbose | /bin/bash -eux From 4756c7149662202bd4c447302ad322113b22f57d Mon Sep 17 00:00:00 2001 From: Zack Lalanne Date: Sat, 9 Jul 2016 02:08:44 -0500 Subject: [PATCH 1801/2522] Fixed #632 added alternatives priority (#1175) --- system/alternatives.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/system/alternatives.py b/system/alternatives.py index c056348f1d5..c2a4065e75e 100644 --- a/system/alternatives.py +++ b/system/alternatives.py @@ -47,6 +47,10 @@ - The path to the symbolic link that should point to the real executable. - This option is required on RHEL-based distributions required: false + priority: + description: + - The priority of the alternative + required: false requirements: [ update-alternatives ] ''' @@ -56,9 +60,10 @@ - name: alternatives link created alternatives: name=hadoop-conf link=/etc/hadoop/conf path=/etc/hadoop/conf.ansible -''' -DEFAULT_LINK_PRIORITY = 50 +- name: make java 32 bit an alternative with low priority + alternatives: name=java path=/usr/lib/jvm/java-7-openjdk-i386/jre/bin/java priority=-10 +''' import re from ansible.module_utils.basic import * @@ -72,6 +77,8 @@ def main(): name = dict(required=True), path = dict(required=True, type='path'), link = dict(required=False, type='path'), + priority = dict(required=False, type='int', + default=50), ), supports_check_mode=True, ) @@ -80,6 +87,7 @@ def main(): name = params['name'] path = params['path'] link = params['link'] + priority = params['priority'] UPDATE_ALTERNATIVES = module.get_bin_path('update-alternatives',True) @@ -127,7 +135,7 @@ def main(): module.fail_json(msg="Needed to install the alternative, but unable to do so as we are missing the link") module.run_command( - [UPDATE_ALTERNATIVES, '--install', link, name, path, str(DEFAULT_LINK_PRIORITY)], + [UPDATE_ALTERNATIVES, '--install', link, name, path, str(priority)], check_rc=True ) From f7b18a331bda24432c8a25c7d2f691ba60fd58d4 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 9 Jul 2016 09:11:24 +0200 Subject: [PATCH 1802/2522] alternatives: add missing version_added and default doc Also see #1175 --- system/alternatives.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/system/alternatives.py b/system/alternatives.py index c2a4065e75e..09c8d8ad3e6 100644 --- a/system/alternatives.py +++ b/system/alternatives.py @@ -51,6 +51,8 @@ description: - The priority of the alternative required: false + default: 50 + version_added: "2.2" requirements: [ update-alternatives ] ''' From b85303f02378832f1671f275baeda01b630cf649 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Sat, 9 Jul 2016 00:21:10 -0700 Subject: [PATCH 1803/2522] Adds style conventions to bigip_pool (#2537) A number of coding conventions have been adopted for new F5 modules that are in development. To ensure common usage across the modules, this module needed to be updated to reflect those conventions. No functional code changes were made. --- network/f5/bigip_pool.py | 497 +++++++++++++++++++++------------------ 1 file changed, 262 insertions(+), 235 deletions(-) diff --git a/network/f5/bigip_pool.py b/network/f5/bigip_pool.py index 8f3a36e265a..3966742fd3e 100644 --- a/network/f5/bigip_pool.py +++ b/network/f5/bigip_pool.py @@ -23,235 +23,245 @@ module: bigip_pool short_description: "Manages F5 BIG-IP LTM pools" description: - - "Manages F5 BIG-IP LTM pools via iControl SOAP API" -version_added: "1.2" + - Manages F5 BIG-IP LTM pools via iControl SOAP API +version_added: 1.2 author: - - Matt Hite (@mhite) - - Tim Rupp (@caphrim007) + - Matt Hite (@mhite) + - Tim Rupp (@caphrim007) notes: - - "Requires BIG-IP software version >= 11" - - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" - - "Best run as a local_action in your playbook" + - Requires BIG-IP software version >= 11 + - F5 developed module 'bigsuds' required (see http://devcentral.f5.com) + - Best run as a local_action in your playbook requirements: - - bigsuds + - bigsuds options: - server: - description: - - BIG-IP host - required: true - default: null - choices: [] - aliases: [] - server_port: - description: - - BIG-IP server port - required: false - default: 443 - version_added: "2.2" - user: - description: - - BIG-IP username - required: true - default: null - choices: [] - aliases: [] - password: - description: - - BIG-IP password - required: true - default: null - choices: [] - aliases: [] - validate_certs: - description: - - If C(no), SSL certificates will not be validated. This should only be used - on personally controlled sites. Prior to 2.0, this module would always - validate on python >= 2.7.9 and never validate on python <= 2.7.8 - required: false - default: 'yes' - choices: ['yes', 'no'] - version_added: 2.0 - state: - description: - - Pool/pool member state - required: false - default: present - choices: ['present', 'absent'] - aliases: [] - name: - description: - - Pool name - required: true - default: null - choices: [] - aliases: ['pool'] - partition: - description: - - Partition of pool/pool member - required: false - default: 'Common' - choices: [] - aliases: [] - lb_method: - description: - - Load balancing method - version_added: "1.3" - required: False - default: 'round_robin' - choices: ['round_robin', 'ratio_member', 'least_connection_member', - 'observed_member', 'predictive_member', 'ratio_node_address', - 'least_connection_node_address', 'fastest_node_address', - 'observed_node_address', 'predictive_node_address', - 'dynamic_ratio', 'fastest_app_response', 'least_sessions', - 'dynamic_ratio_member', 'l3_addr', - 'weighted_least_connection_member', - 'weighted_least_connection_node_address', - 'ratio_session', 'ratio_least_connection_member', - 'ratio_least_connection_node_address'] - aliases: [] - monitor_type: - description: - - Monitor rule type when monitors > 1 - version_added: "1.3" - required: False - default: null - choices: ['and_list', 'm_of_n'] - aliases: [] - quorum: - description: - - Monitor quorum value when monitor_type is m_of_n - version_added: "1.3" - required: False - default: null - choices: [] - aliases: [] - monitors: - description: - - Monitor template name list. Always use the full path to the monitor. - version_added: "1.3" - required: False - default: null - choices: [] - aliases: [] - slow_ramp_time: - description: - - Sets the ramp-up time (in seconds) to gradually ramp up the load on newly added or freshly detected up pool members - version_added: "1.3" - required: False - default: null - choices: [] - aliases: [] - reselect_tries: - description: - - Sets the number of times the system tries to contact a pool member after a passive failure - version_added: "2.2" - required: False - default: null - choices: [] - aliases: [] - service_down_action: - description: - - Sets the action to take when node goes down in pool - version_added: "1.3" - required: False - default: null - choices: ['none', 'reset', 'drop', 'reselect'] - aliases: [] - host: - description: - - "Pool member IP" - required: False - default: null - choices: [] - aliases: ['address'] - port: - description: - - "Pool member port" - required: False - default: null - choices: [] - aliases: [] + server: + description: + - BIG-IP host + required: true + default: null + choices: [] + aliases: [] + server_port: + description: + - BIG-IP server port + required: false + default: 443 + version_added: "2.2" + user: + description: + - BIG-IP username + required: true + default: null + choices: [] + aliases: [] + password: + description: + - BIG-IP password + required: true + default: null + choices: [] + aliases: [] + validate_certs: + description: + - If C(no), SSL certificates will not be validated. This should only be used + on personally controlled sites. Prior to 2.0, this module would always + validate on python >= 2.7.9 and never validate on python <= 2.7.8 + required: false + default: 'yes' + choices: + - yes + - no + version_added: 2.0 + state: + description: + - Pool/pool member state + required: false + default: present + choices: + - present + - absent + aliases: [] + name: + description: + - Pool name + required: true + default: null + choices: [] + aliases: + - pool + partition: + description: + - Partition of pool/pool member + required: false + default: 'Common' + choices: [] + aliases: [] + lb_method: + description: + - Load balancing method + version_added: "1.3" + required: False + default: 'round_robin' + choices: + - round_robin + - ratio_member + - least_connection_member + - observed_member + - predictive_member + - ratio_node_address + - least_connection_node_address + - fastest_node_address + - observed_node_address + - predictive_node_address + - dynamic_ratio + - fastest_app_response + - least_sessions + - dynamic_ratio_member + - l3_addr + - weighted_least_connection_member + - weighted_least_connection_node_address + - ratio_session + - ratio_least_connection_member + - ratio_least_connection_node_address + aliases: [] + monitor_type: + description: + - Monitor rule type when monitors > 1 + version_added: "1.3" + required: False + default: null + choices: ['and_list', 'm_of_n'] + aliases: [] + quorum: + description: + - Monitor quorum value when monitor_type is m_of_n + version_added: "1.3" + required: False + default: null + choices: [] + aliases: [] + monitors: + description: + - Monitor template name list. Always use the full path to the monitor. + version_added: "1.3" + required: False + default: null + choices: [] + aliases: [] + slow_ramp_time: + description: + - Sets the ramp-up time (in seconds) to gradually ramp up the load on + newly added or freshly detected up pool members + version_added: "1.3" + required: False + default: null + choices: [] + aliases: [] + reselect_tries: + description: + - Sets the number of times the system tries to contact a pool member + after a passive failure + version_added: "2.2" + required: False + default: null + choices: [] + aliases: [] + service_down_action: + description: + - Sets the action to take when node goes down in pool + version_added: "1.3" + required: False + default: null + choices: + - none + - reset + - drop + - reselect + aliases: [] + host: + description: + - "Pool member IP" + required: False + default: null + choices: [] + aliases: + - address + port: + description: + - Pool member port + required: False + default: null + choices: [] + aliases: [] ''' EXAMPLES = ''' +- name: Create pool + bigip_pool: + server: "lb.mydomain.com" + user: "admin" + password: "secret" + state: "present" + name: "my-pool" + partition: "Common" + lb_method: "least_connection_member" + slow_ramp_time: 120 + delegate_to: localhost + +- name: Modify load balancer method + bigip_pool: + server: "lb.mydomain.com" + user: "admin" + password: "secret" + state: "present" + name: "my-pool" + partition: "Common" + lb_method: "round_robin" + +- name: Add pool member + bigip_pool: + server: "lb.mydomain.com" + user: "admin" + password: "secret" + state: "present" + name: "my-pool" + partition: "Common" + host: "{{ ansible_default_ipv4["address"] }}" + port: 80 + +- name: Remove pool member from pool + bigip_pool: + server: "lb.mydomain.com" + user: "admin" + password: "secret" + state: "absent" + name: "my-pool" + partition: "Common" + host: "{{ ansible_default_ipv4["address"] }}" + port: 80 + +- name: Delete pool + bigip_pool: + server: "lb.mydomain.com" + user: "admin" + password: "secret" + state: "absent" + name: "my-pool" + partition: "Common" +''' -## playbook task examples: - ---- -# file bigip-test.yml -# ... -- hosts: localhost - tasks: - - name: Create pool - local_action: > - bigip_pool - server=lb.mydomain.com - user=admin - password=mysecret - state=present - name=matthite-pool - partition=matthite - lb_method=least_connection_member - slow_ramp_time=120 - - - name: Modify load balancer method - local_action: > - bigip_pool - server=lb.mydomain.com - user=admin - password=mysecret - state=present - name=matthite-pool - partition=matthite - lb_method=round_robin - -- hosts: bigip-test - tasks: - - name: Add pool member - local_action: > - bigip_pool - server=lb.mydomain.com - user=admin - password=mysecret - state=present - name=matthite-pool - partition=matthite - host="{{ ansible_default_ipv4["address"] }}" - port=80 - - - name: Remove pool member from pool - local_action: > - bigip_pool - server=lb.mydomain.com - user=admin - password=mysecret - state=absent - name=matthite-pool - partition=matthite - host="{{ ansible_default_ipv4["address"] }}" - port=80 - -- hosts: localhost - tasks: - - name: Delete pool - local_action: > - bigip_pool - server=lb.mydomain.com - user=admin - password=mysecret - state=absent - name=matthite-pool - partition=matthite - +RETURN = ''' ''' + def pool_exists(api, pool): # hack to determine if pool exists result = False try: api.LocalLB.Pool.get_object_status(pool_names=[pool]) result = True - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed as e: if "was not found" in str(e): result = False else: @@ -259,6 +269,7 @@ def pool_exists(api, pool): raise return result + def create_pool(api, pool, lb_method): # create requires lb_method but we don't want to default # to a value on subsequent runs @@ -268,18 +279,22 @@ def create_pool(api, pool, lb_method): api.LocalLB.Pool.create_v2(pool_names=[pool], lb_methods=[lb_method], members=[[]]) + def remove_pool(api, pool): api.LocalLB.Pool.delete_pool(pool_names=[pool]) + def get_lb_method(api, pool): lb_method = api.LocalLB.Pool.get_lb_method(pool_names=[pool])[0] lb_method = lb_method.strip().replace('LB_METHOD_', '').lower() return lb_method + def set_lb_method(api, pool, lb_method): lb_method = "LB_METHOD_%s" % lb_method.strip().upper() api.LocalLB.Pool.set_lb_method(pool_names=[pool], lb_methods=[lb_method]) + def get_monitors(api, pool): result = api.LocalLB.Pool.get_monitor_association(pool_names=[pool])[0]['monitor_rule'] monitor_type = result['type'].split("MONITOR_RULE_TYPE_")[-1].lower() @@ -287,35 +302,43 @@ def get_monitors(api, pool): monitor_templates = result['monitor_templates'] return (monitor_type, quorum, monitor_templates) + def set_monitors(api, pool, monitor_type, quorum, monitor_templates): monitor_type = "MONITOR_RULE_TYPE_%s" % monitor_type.strip().upper() monitor_rule = {'type': monitor_type, 'quorum': quorum, 'monitor_templates': monitor_templates} monitor_association = {'pool_name': pool, 'monitor_rule': monitor_rule} api.LocalLB.Pool.set_monitor_association(monitor_associations=[monitor_association]) + def get_slow_ramp_time(api, pool): result = api.LocalLB.Pool.get_slow_ramp_time(pool_names=[pool])[0] return result + def set_slow_ramp_time(api, pool, seconds): api.LocalLB.Pool.set_slow_ramp_time(pool_names=[pool], values=[seconds]) + def get_reselect_tries(api, pool): result = api.LocalLB.Pool.get_reselect_tries(pool_names=[pool])[0] return result + def set_reselect_tries(api, pool, tries): api.LocalLB.Pool.set_reselect_tries(pool_names=[pool], values=[tries]) + def get_action_on_service_down(api, pool): result = api.LocalLB.Pool.get_action_on_service_down(pool_names=[pool])[0] result = result.split("SERVICE_DOWN_ACTION_")[-1].lower() return result + def set_action_on_service_down(api, pool, action): action = "SERVICE_DOWN_ACTION_%s" % action.strip().upper() api.LocalLB.Pool.set_action_on_service_down(pool_names=[pool], actions=[action]) + def member_exists(api, pool, address, port): # hack to determine if member exists result = False @@ -324,7 +347,7 @@ def member_exists(api, pool, address, port): api.LocalLB.Pool.get_member_object_status(pool_names=[pool], members=[members]) result = True - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed as e: if "was not found" in str(e): result = False else: @@ -332,12 +355,13 @@ def member_exists(api, pool, address, port): raise return result + def delete_node_address(api, address): result = False try: api.LocalLB.NodeAddressV2.delete_node_address(nodes=[address]) result = True - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed as e: if "is referenced by a member of pool" in str(e): result = False else: @@ -345,14 +369,17 @@ def delete_node_address(api, address): raise return result + def remove_pool_member(api, pool, address, port): members = [{'address': address, 'port': port}] api.LocalLB.Pool.remove_member_v2(pool_names=[pool], members=[members]) + def add_pool_member(api, pool, address, port): members = [{'address': address, 'port': port}] api.LocalLB.Pool.add_member_v2(pool_names=[pool], members=[members]) + def main(): lb_method_choices = ['round_robin', 'ratio_member', 'least_connection_member', 'observed_member', @@ -371,23 +398,24 @@ def main(): service_down_choices = ['none', 'reset', 'drop', 'reselect'] - argument_spec=f5_argument_spec(); - argument_spec.update(dict( - name = dict(type='str', required=True, aliases=['pool']), - lb_method = dict(type='str', choices=lb_method_choices), - monitor_type = dict(type='str', choices=monitor_type_choices), - quorum = dict(type='int'), - monitors = dict(type='list'), - slow_ramp_time = dict(type='int'), - reselect_tries = dict(type='int'), - service_down_action = dict(type='str', choices=service_down_choices), - host = dict(type='str', aliases=['address']), - port = dict(type='int') - ) + argument_spec = f5_argument_spec() + + meta_args = dict( + name=dict(type='str', required=True, aliases=['pool']), + lb_method=dict(type='str', choices=lb_method_choices), + monitor_type=dict(type='str', choices=monitor_type_choices), + quorum=dict(type='int'), + monitors=dict(type='list'), + slow_ramp_time=dict(type='int'), + reselect_tries=dict(type='int'), + service_down_action=dict(type='str', choices=service_down_choices), + host=dict(type='str', aliases=['address']), + port=dict(type='int') ) + argument_spec.update(meta_args) module = AnsibleModule( - argument_spec = argument_spec, + argument_spec=argument_spec, supports_check_mode=True ) @@ -408,7 +436,7 @@ def main(): validate_certs = module.params['validate_certs'] name = module.params['name'] - pool = fq_name(partition,name) + pool = fq_name(partition, name) lb_method = module.params['lb_method'] if lb_method: lb_method = lb_method.lower() @@ -427,7 +455,7 @@ def main(): if service_down_action: service_down_action = service_down_action.lower() host = module.params['host'] - address = fq_name(partition,host) + address = fq_name(partition, host) port = module.params['port'] # sanity check user supplied values @@ -479,7 +507,7 @@ def main(): try: remove_pool(api, pool) result = {'changed': True} - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed as e: if "was not found" in str(e): result = {'changed': False} else: @@ -502,7 +530,7 @@ def main(): try: create_pool(api, pool, lb_method) result = {'changed': True} - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed as e: if "already exists" in str(e): update = True else: @@ -558,12 +586,11 @@ def main(): add_pool_member(api, pool, address, port) result = {'changed': True} - except Exception, e: + except Exception as e: module.fail_json(msg="received exception: %s" % e) module.exit_json(**result) -# import module snippets from ansible.module_utils.basic import * from ansible.module_utils.f5 import * From 3480d3d1676a0ef05c46e59f78b6fd3da9a8c089 Mon Sep 17 00:00:00 2001 From: Robin Roth Date: Sat, 9 Jul 2016 11:38:12 +0200 Subject: [PATCH 1804/2522] allow overwrite_multiple with more than 2 repos (#2481) 2.1 introduces overwrite_multiple, which can overwrite more than one exisiting repo (with matching alias or URL). Allow more than 2 repos to be overwritten, since openSuSE allow more than one repo with the same URL if the alias is different. --- packaging/os/zypper_repository.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packaging/os/zypper_repository.py b/packaging/os/zypper_repository.py index 0e4e805856d..54a641c7b5f 100644 --- a/packaging/os/zypper_repository.py +++ b/packaging/os/zypper_repository.py @@ -157,13 +157,15 @@ def repo_exists(module, repodata, overwrite_multiple): # Found an existing repo, look for changes has_changes = _repo_changes(repos[0], repodata) return (True, has_changes, repos) - elif len(repos) == 2 and overwrite_multiple: - # Found two repos and want to overwrite_multiple - return (True, True, repos) - else: - # either more than 2 repos (shouldn't happen) - # or overwrite_multiple is not active - module.fail_json(msg='More than one repo matched "%s": "%s"' % (name, repos)) + elif len(repos) >= 2: + if overwrite_multiple: + # Found two repos and want to overwrite_multiple + return (True, True, repos) + else: + errmsg = 'More than one repo matched "%s": "%s".' % (name, repos) + errmsg += ' Use overwrite_multiple to allow more than one repo to be overwritten' + module.fail_json(msg=errmsg) + def modify_repo(module, repodata, old_repos): repo = repodata['url'] From a70cfeed1524dda0e799939546fd1babc9b14676 Mon Sep 17 00:00:00 2001 From: Nate Prewitt Date: Sat, 9 Jul 2016 12:30:56 -0600 Subject: [PATCH 1805/2522] removing | from description so docs render properly in RTD (#2533) --- system/timezone.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/system/timezone.py b/system/timezone.py index 4a6820ba262..c65d7049338 100644 --- a/system/timezone.py +++ b/system/timezone.py @@ -25,8 +25,7 @@ module: timezone short_description: Configure timezone setting description: - - | - This module configures the timezone setting, both of the system clock + - This module configures the timezone setting, both of the system clock and of the hardware clock. I(Currently only Linux platform is supported.) It is recommended to restart C(crond) after changing the timezone, otherwise the jobs may run at the wrong time. @@ -38,14 +37,12 @@ options: name: description: - - | - Name of the timezone for the system clock. + - Name of the timezone for the system clock. Default is to keep current setting. required: false hwclock: description: - - | - Whether the hardware clock is in UTC or in local timezone. + - Whether the hardware clock is in UTC or in local timezone. Default is to keep current setting. Note that this option is recommended not to change and may fail to configure, especially on virtual environments such as AWS. From f1cdafae1272e1f94264623d555113ca357da237 Mon Sep 17 00:00:00 2001 From: Kevin Borgolte Date: Sun, 10 Jul 2016 01:17:31 -0700 Subject: [PATCH 1806/2522] Fix Kubernetes API auth regression from 393e43b8 (#2503) * Fix Kubernetes API auth regression from 393e43b8 Commit 393e43b8 renames url_username and url_password to username and password, which breaks authentication to a Kubernetes API endpoint as fetch_url() in ansible.module_utils.urls relies on url_username and url_password being set. * Add aliases for clustering/kubernetes.py - username as alias for url_username - password as alias for url_password --- clustering/kubernetes.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/clustering/kubernetes.py b/clustering/kubernetes.py index 183fb0b837e..12dfee1071c 100644 --- a/clustering/kubernetes.py +++ b/clustering/kubernetes.py @@ -61,16 +61,18 @@ required: true default: "present" choices: ["present", "absent", "update", "replace"] - password: + url_password: description: - The HTTP Basic Auth password for the API I(endpoint). This should be set unless using the C('insecure') option. default: null - username: + aliases: ["password"] + url_username: description: - The HTTP Basic Auth username for the API I(endpoint). This should be set unless using the C('insecure') option. default: "admin" + aliases: ["username"] insecure: description: - "Reverts the connection to using HTTP instead of HTTPS. This option should @@ -92,8 +94,8 @@ - name: Create a kubernetes namespace kubernetes: api_endpoint: 123.45.67.89 - username: admin - password: redacted + url_username: admin + url_password: redacted inline_data: kind: Namespace apiVersion: v1 @@ -111,8 +113,8 @@ - name: Create a kubernetes namespace kubernetes: api_endpoint: 123.45.67.89 - username: admin - password: redacted + url_username: admin + url_password: redacted file_reference: /path/to/create_namespace.yaml state: present @@ -306,8 +308,8 @@ def main(): argument_spec=dict( http_agent=dict(default=USER_AGENT), - username=dict(default="admin"), - password=dict(default="", no_log=True), + url_username=dict(default="admin", aliases=["username"]), + url_password=dict(default="", no_log=True, aliases=["password"]), force_basic_auth=dict(default="yes"), validate_certs=dict(default=False, type='bool'), certificate_authority_data=dict(required=False), @@ -317,7 +319,9 @@ def main(): inline_data=dict(required=False), state=dict(default="present", choices=["present", "absent", "update", "replace"]) ), - mutually_exclusive = (('file_reference', 'inline_data'), ('username', 'insecure'), ('password', 'insecure')), + mutually_exclusive = (('file_reference', 'inline_data'), + ('url_username', 'insecure'), + ('url_password', 'insecure')), required_one_of = (('file_reference', 'inline_data'),), ) From 067b9084153b9cd01fe36999ff57cb8daef8cbd3 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sun, 10 Jul 2016 10:40:42 +0200 Subject: [PATCH 1807/2522] Revert "travis: workaround false negative test failure (#2362)" This reverts commit d0568790886f7d3973b31b55c5e045152514c604. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7d24ae03b2b..05a60a8dcb2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -122,5 +122,5 @@ script: - python2.7 -m compileall -fq . - python3.4 -m compileall -fq . -x $(echo "$PY3_EXCLUDE_LIST"| tr ' ' '|') - python3.5 -m compileall -fq . -x $(echo "$PY3_EXCLUDE_LIST"| tr ' ' '|') - - ansible-validate-modules . --exclude 'cloud/azure/azure_rm_deployment\.py' + - ansible-validate-modules . #- ./test-docs.sh extras From 92301f7451925ecbbd13cedd49bf342d6915a5cf Mon Sep 17 00:00:00 2001 From: Peter Oliver Date: Sun, 10 Jul 2016 23:22:03 +0100 Subject: [PATCH 1808/2522] Handle empty IPS publishers (#2047) It turns out it's possible to set up a publisher with no URIs. Handle this gracefully. --- packaging/os/pkg5_publisher.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packaging/os/pkg5_publisher.py b/packaging/os/pkg5_publisher.py index 08c33464e7f..79eccd2ec08 100644 --- a/packaging/os/pkg5_publisher.py +++ b/packaging/os/pkg5_publisher.py @@ -180,13 +180,14 @@ def get_publishers(module): publishers[name]['origin'] = [] publishers[name]['mirror'] = [] - publishers[name][values['type']].append(values['uri']) + if values['type'] is not None: + publishers[name][values['type']].append(values['uri']) return publishers def unstringify(val): - if val == "-": + if val == "-" or val == '': return None elif val == "true": return True From 482b1a640e95274b1a6f41ec21efc333ac4076b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Mon, 11 Jul 2016 01:10:44 +0200 Subject: [PATCH 1809/2522] consul_acl: fix docs, arg_spec not matching docs (#2544) --- clustering/consul_acl.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/clustering/consul_acl.py b/clustering/consul_acl.py index 34c569b250c..67ca63184f9 100644 --- a/clustering/consul_acl.py +++ b/clustering/consul_acl.py @@ -37,14 +37,16 @@ - a management token is required to manipulate the acl lists state: description: - - whether the ACL pair should be present or absent, defaults to present + - whether the ACL pair should be present or absent required: false choices: ['present', 'absent'] - type: + default: present + token_type: description: - the type of token that should be created, either management or - client, defaults to client + client choices: ['client', 'management'] + default: client name: description: - the name that should be associated with the acl key, this is opaque From a4207029de937cb3010557cc1dd0b2503a1cf17a Mon Sep 17 00:00:00 2001 From: Patrick Ogenstad Date: Mon, 11 Jul 2016 18:53:33 +0200 Subject: [PATCH 1810/2522] New module asa_acl (#2309) --- network/asa/__init__.py | 0 network/asa/asa_acl.py | 216 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 network/asa/__init__.py create mode 100644 network/asa/asa_acl.py diff --git a/network/asa/__init__.py b/network/asa/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/network/asa/asa_acl.py b/network/asa/asa_acl.py new file mode 100644 index 00000000000..6ef3b7d61db --- /dev/null +++ b/network/asa/asa_acl.py @@ -0,0 +1,216 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +DOCUMENTATION = """ +--- +module: asa_acl +version_added: "2.2" +author: "Patrick Ogenstad (@ogenstad)" +short_description: Manage access-lists on a Cisco ASA +description: + - This module allows you to work with access-lists on a Cisco ASA device. +extends_documentation_fragment: asa +options: + lines: + description: + - The ordered set of commands that should be configured in the + section. The commands must be the exact same commands as found + in the device running-config. Be sure to note the configuration + command syntanx as some commands are automatically modified by the + device config parser. + required: true + before: + description: + - The ordered set of commands to push on to the command stack if + a change needs to be made. This allows the playbook designer + the opportunity to perform configuration commands prior to pushing + any changes without affecting how the set of commands are matched + against the system + required: false + default: null + after: + description: + - The ordered set of commands to append to the end of the command + stack if a changed needs to be made. Just like with I(before) this + allows the playbook designer to append a set of commands to be + executed after the command set. + required: false + default: null + match: + description: + - Instructs the module on the way to perform the matching of + the set of commands against the current device config. If + match is set to I(line), commands are matched line by line. If + match is set to I(strict), command lines are matched with respect + to position. Finally if match is set to I(exact), command lines + must be an equal match. + required: false + default: line + choices: ['line', 'strict', 'exact'] + replace: + description: + - Instructs the module on the way to perform the configuration + on the device. If the replace argument is set to I(line) then + the modified lines are pushed to the device in configuration + mode. If the replace argument is set to I(block) then the entire + command block is pushed to the device in configuration mode if any + line is not correct + required: false + default: line + choices: ['line', 'block'] + force: + description: + - The force argument instructs the module to not consider the + current devices running-config. When set to true, this will + cause the module to push the contents of I(src) into the device + without first checking if already configured. + required: false + default: false + choices: ['yes', 'no'] + config: + description: + - The module, by default, will connect to the remote device and + retrieve the current running-config to use as a base for comparing + against the contents of source. There are times when it is not + desirable to have the task get the current running-config for + every task in a playbook. The I(config) argument allows the + implementer to pass in the configuruation to use as the base + config for comparision. + required: false + default: null +""" + +EXAMPLES = """ + +- asa_acl: + lines: + - access-list ACL-ANSIBLE extended permit tcp any any eq 82 + - access-list ACL-ANSIBLE extended permit tcp any any eq www + - access-list ACL-ANSIBLE extended permit tcp any any eq 97 + - access-list ACL-ANSIBLE extended permit tcp any any eq 98 + - access-list ACL-ANSIBLE extended permit tcp any any eq 99 + before: clear configure access-list ACL-ANSIBLE + match: strict + replace: block + +- asa_acl: + lines: + - access-list ACL-OUTSIDE extended permit tcp any any eq www + - access-list ACL-OUTSIDE extended permit tcp any any eq https + context: customer_a +""" + +RETURN = """ +updates: + description: The set of commands that will be pushed to the remote device + returned: always + type: list + sample: ['...', '...'] + +responses: + description: The set of responses from issuing the commands on the device + retured: when not check_mode + type: list + sample: ['...', '...'] +""" + + +def get_config(module): + config = module.params['config'] or dict() + if not config and not module.params['force']: + config = module.config + return config + + +def check_input_acl(lines, module): + first_line = True + for line in lines: + ace = line.split() + if ace[0] != 'access-list': + module.fail_json(msg='All lines/commands must begin with "access-list" %s is not permitted' % ace[0]) + if len(ace) <= 1: + module.fail_json(msg='All lines/commainds must contain the name of the access-list') + if first_line: + acl_name = ace[1] + else: + if acl_name != ace[1]: + module.fail_json(msg='All lines/commands must use the same access-list %s is not %s' % (ace[1], acl_name)) + first_line = False + + return 'access-list %s' % acl_name + +def main(): + + argument_spec = dict( + lines=dict(aliases=['commands'], required=True, type='list'), + before=dict(type='list'), + after=dict(type='list'), + match=dict(default='line', choices=['line', 'strict', 'exact']), + replace=dict(default='line', choices=['line', 'block']), + force=dict(default=False, type='bool'), + config=dict() + ) + + module = get_module(argument_spec=argument_spec, + supports_check_mode=True) + + lines = module.params['lines'] + + before = module.params['before'] + after = module.params['after'] + + match = module.params['match'] + replace = module.params['replace'] + + module.filter = check_input_acl(lines, module) + if not module.params['force']: + contents = get_config(module) + config = NetworkConfig(contents=contents, indent=1) + + candidate = NetworkConfig(indent=1) + candidate.add(lines) + + commands = candidate.difference(config, match=match, replace=replace) + else: + commands = [] + commands.extend(lines) + + result = dict(changed=False) + + if commands: + if before: + commands[:0] = before + + if after: + commands.extend(after) + + if not module.check_mode: + commands = [str(c).strip() for c in commands] + response = module.configure(commands) + result['responses'] = response + result['changed'] = True + + result['updates'] = commands + module.exit_json(**result) + +from ansible.module_utils.basic import * +from ansible.module_utils.shell import * +from ansible.module_utils.netcfg import * +from ansible.module_utils.asa import * +if __name__ == '__main__': + main() From 0b9f3100b1b58964d05cbd04bb24edcd93284f72 Mon Sep 17 00:00:00 2001 From: Patrick Ogenstad Date: Mon, 11 Jul 2016 18:54:31 +0200 Subject: [PATCH 1811/2522] New module asa_template (#2308) --- network/asa/asa_template.py | 175 ++++++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 network/asa/asa_template.py diff --git a/network/asa/asa_template.py b/network/asa/asa_template.py new file mode 100644 index 00000000000..9644fa71f88 --- /dev/null +++ b/network/asa/asa_template.py @@ -0,0 +1,175 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +DOCUMENTATION = """ +--- +module: asa_template +version_added: "2.2" +author: "Peter Sprygada (@privateip) & Patrick Ogenstad (@ogenstad)" +short_description: Manage Cisco ASA device configurations over SSH +description: + - Manages Cisco ASA network device configurations over SSH. This module + allows implementors to work with the device running-config. It + provides a way to push a set of commands onto a network device + by evaluting the current running-config and only pushing configuration + commands that are not already configured. The config source can + be a set of commands or a template. +extends_documentation_fragment: asa +options: + src: + description: + - The path to the config source. The source can be either a + file with config or a template that will be merged during + runtime. By default the task will first search for the source + file in role or playbook root folder in templates unless a full + path to the file is given. + required: true + force: + description: + - The force argument instructs the module not to consider the + current device running-config. When set to true, this will + cause the module to push the contents of I(src) into the device + without first checking if already configured. + required: false + default: false + choices: [ "true", "false" ] + include_defaults: + description: + - The module, by default, will collect the current device + running-config to use as a base for comparision to the commands + in I(src). Setting this value to true will cause the command + issued to add any necessary flags to collect all defaults as + well as the device configuration. If the destination device + does not support such a flag, this argument is silently ignored. + required: false + default: false + choices: [ "true", "false" ] + backup: + description: + - When this argument is configured true, the module will backup + the running-config from the node prior to making any changes. + The backup file will be written to backup_{{ hostname }} in + the root of the playbook directory. + required: false + default: false + choices: [ "true", "false" ] + config: + description: + - The module, by default, will connect to the remote device and + retrieve the current running-config to use as a base for comparing + against the contents of source. There are times when it is not + desirable to have the task get the current running-config for + every task. The I(config) argument allows the implementer to + pass in the configuruation to use as the base config for + comparision. + required: false + default: null +""" + +EXAMPLES = """ +- name: push a configuration onto the device + asa_template: + host: hostname + username: foo + src: config.j2 + +- name: forceable push a configuration onto the device + asa_template: + host: hostname + username: foo + src: config.j2 + force: yes + +- name: provide the base configuration for comparision + asa_template: + host: hostname + username: foo + src: candidate_config.txt + config: current_config.txt +""" + +RETURN = """ +updates: + description: The set of commands that will be pushed to the remote device + returned: always + type: list + sample: ['...', '...'] + +responses: + description: The set of responses from issuing the commands on the device + retured: when not check_mode + type: list + sample: ['...', '...'] +""" + + +def get_config(module): + config = module.params['config'] or dict() + if not config and not module.params['force']: + config = module.config + return config + + +def main(): + """ main entry point for module execution + """ + + argument_spec = dict( + src=dict(), + force=dict(default=False, type='bool'), + include_defaults=dict(default=True, type='bool'), + backup=dict(default=False, type='bool'), + config=dict(), + ) + + mutually_exclusive = [('config', 'backup'), ('config', 'force')] + + module = get_module(argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) + + result = dict(changed=False) + + candidate = NetworkConfig(contents=module.params['src'], indent=1) + + contents = get_config(module) + if contents: + config = NetworkConfig(contents=contents, indent=1) + result['_backup'] = contents + + if not module.params['force']: + commands = candidate.difference(config) + else: + commands = str(candidate).split('\n') + + if commands: + if not module.check_mode: + commands = [str(c).strip() for c in commands] + response = module.configure(commands) + result['responses'] = response + result['changed'] = True + + result['updates'] = commands + module.exit_json(**result) + + +from ansible.module_utils.basic import * +from ansible.module_utils.shell import * +from ansible.module_utils.netcfg import * +from ansible.module_utils.asa import * +if __name__ == '__main__': + main() From 200654e45d4075196f41d4e46914b732622cb0eb Mon Sep 17 00:00:00 2001 From: Patrick Ogenstad Date: Mon, 11 Jul 2016 18:55:03 +0200 Subject: [PATCH 1812/2522] New module asa_config (#2307) --- network/asa/asa_config.py | 221 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 network/asa/asa_config.py diff --git a/network/asa/asa_config.py b/network/asa/asa_config.py new file mode 100644 index 00000000000..7c7d1248b41 --- /dev/null +++ b/network/asa/asa_config.py @@ -0,0 +1,221 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +DOCUMENTATION = """ +--- +module: asa_config +version_added: "2.2" +author: "Peter Sprygada (@privateip) & Patrick Ogenstad (@ogenstad)" +short_description: Manage Cisco ASA configuration sections +description: + - Cisco ASA configurations use a simple block indent file sytanx + for segementing configuration into sections. This module provides + an implementation for working with ASA configuration sections in + a deterministic way. +extends_documentation_fragment: asa +options: + lines: + description: + - The ordered set of commands that should be configured in the + section. The commands must be the exact same commands as found + in the device running-config. Be sure to note the configuration + command syntanx as some commands are automatically modified by the + device config parser. + required: true + parents: + description: + - The ordered set of parents that uniquely identify the section + the commands should be checked against. If the parents argument + is omitted, the commands are checked against the set of top + level or global commands. + required: false + default: null + before: + description: + - The ordered set of commands to push on to the command stack if + a change needs to be made. This allows the playbook designer + the opportunity to perform configuration commands prior to pushing + any changes without affecting how the set of commands are matched + against the system + required: false + default: null + after: + description: + - The ordered set of commands to append to the end of the command + stack if a changed needs to be made. Just like with I(before) this + allows the playbook designer to append a set of commands to be + executed after the command set. + required: false + default: null + match: + description: + - Instructs the module on the way to perform the matching of + the set of commands against the current device config. If + match is set to I(line), commands are matched line by line. If + match is set to I(strict), command lines are matched with respect + to position. Finally if match is set to I(exact), command lines + must be an equal match. + required: false + default: line + choices: ['line', 'strict', 'exact'] + replace: + description: + - Instructs the module on the way to perform the configuration + on the device. If the replace argument is set to I(line) then + the modified lines are pushed to the device in configuration + mode. If the replace argument is set to I(block) then the entire + command block is pushed to the device in configuration mode if any + line is not correct + required: false + default: line + choices: ['line', 'block'] + force: + description: + - The force argument instructs the module to not consider the + current devices running-config. When set to true, this will + cause the module to push the contents of I(src) into the device + without first checking if already configured. + required: false + default: false + choices: ['yes', 'no'] + config: + description: + - The module, by default, will connect to the remote device and + retrieve the current running-config to use as a base for comparing + against the contents of source. There are times when it is not + desirable to have the task get the current running-config for + every task in a playbook. The I(config) argument allows the + implementer to pass in the configuruation to use as the base + config for comparision. + required: false + default: null +""" + +EXAMPLES = """ +- asa_config: + lines: + - network-object host 10.80.30.18 + - network-object host 10.80.30.19 + - network-object host 10.80.30.20 + parents: ['object-group network OG-MONITORED-SERVERS'] + +- asa_config: + host: "{{ inventory_hostname }}" + lines: + - message-length maximum client auto + - message-length maximum 512 + match: line + parents: ['policy-map type inspect dns PM-DNS', 'parameters'] + authorize: yes + auth_pass: cisco + username: admin + password: cisco + context: ansible + +- asa_config: + provider: "{{ cli }}" + host: "{{ inventory_hostname }}" + show_command: 'more system:running-config' + lines: + - ikev1 pre-shared-key MyS3cretVPNK3y + parents: tunnel-group 1.1.1.1 ipsec-attributes + +""" + +RETURN = """ +updates: + description: The set of commands that will be pushed to the remote device + returned: always + type: list + sample: ['...', '...'] + +responses: + description: The set of responses from issuing the commands on the device + retured: when not check_mode + type: list + sample: ['...', '...'] +""" + +def get_config(module): + config = module.params['config'] or dict() + if not config and not module.params['force']: + config = module.config + return config + + +def main(): + + argument_spec = dict( + lines=dict(aliases=['commands'], required=True, type='list'), + parents=dict(type='list'), + before=dict(type='list'), + after=dict(type='list'), + match=dict(default='line', choices=['line', 'strict', 'exact']), + replace=dict(default='line', choices=['line', 'block']), + force=dict(default=False, type='bool'), + config=dict() + ) + + module = get_module(argument_spec=argument_spec, + supports_check_mode=True) + + lines = module.params['lines'] + parents = module.params['parents'] or list() + + before = module.params['before'] + after = module.params['after'] + + match = module.params['match'] + replace = module.params['replace'] + + if not module.params['force']: + contents = get_config(module) + config = NetworkConfig(contents=contents, indent=1) + + candidate = NetworkConfig(indent=1) + candidate.add(lines, parents=parents) + + commands = candidate.difference(config, path=parents, match=match, replace=replace) + else: + commands = parents + commands.extend(lines) + + result = dict(changed=False) + + if commands: + if before: + commands[:0] = before + + if after: + commands.extend(after) + + if not module.check_mode: + commands = [str(c).strip() for c in commands] + response = module.configure(commands) + result['responses'] = response + result['changed'] = True + + result['updates'] = commands + module.exit_json(**result) + +from ansible.module_utils.basic import * +from ansible.module_utils.shell import * +from ansible.module_utils.netcfg import * +from ansible.module_utils.asa import * +if __name__ == '__main__': + main() From 8285bbda6a608d28e4f56b94da8c0832a35b92ba Mon Sep 17 00:00:00 2001 From: Patrick Ogenstad Date: Mon, 11 Jul 2016 18:55:45 +0200 Subject: [PATCH 1813/2522] New module asa_command (#2306) --- network/asa/asa_command.py | 172 +++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 network/asa/asa_command.py diff --git a/network/asa/asa_command.py b/network/asa/asa_command.py new file mode 100644 index 00000000000..ceae55a76df --- /dev/null +++ b/network/asa/asa_command.py @@ -0,0 +1,172 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + + +DOCUMENTATION = """ +--- +module: asa_command +version_added: "2.2" +author: "Peter Sprygada (@privateip) & Patrick Ogenstad (@ogenstad)" +short_description: Run arbitrary commands on Cisco ASA devices. +description: + - Sends arbitrary commands to an ASA node and returns the results + read from the device. The M(asa_command) module includes an + argument that will cause the module to wait for a specific condition + before returning or timing out if the condition is not met. +extends_documentation_fragment: asa +options: + commands: + description: + - List of commands to send to the remote ios device over the + configured provider. The resulting output from the command + is returned. If the I(waitfor) argument is provided, the + module is not returned until the condition is satisfied or + the number of retires as expired. + required: true + waitfor: + description: + - List of conditions to evaluate against the output of the + command. The task will wait for a each condition to be true + before moving forward. If the conditional is not true + within the configured number of retries, the task fails. + See examples. + required: false + default: null + retries: + description: + - Specifies the number of retries a command should by tried + before it is considered failed. The command is run on the + target device every retry and evaluated against the + waitfor conditions. + required: false + default: 10 + interval: + description: + - Configures the interval in seconds to wait between retries + of the command. If the command does not pass the specified + conditions, the interval indicates how long to wait before + trying the command again. + required: false + default: 1 + +""" + +EXAMPLES = """ + +- asa_command: + commands: + - show version + register: output + +- asa_command: + commands: + - show asp drop + - show memory + register: output + +- asa_command: + commands: + - show version + context: system +""" + +RETURN = """ +stdout: + description: the set of responses from the commands + returned: always + type: list + sample: ['...', '...'] + +stdout_lines: + description: The value of stdout split into a list + returned: always + type: list + sample: [['...', '...'], ['...'], ['...']] + +failed_conditions: + description: the conditionals that failed + retured: failed + type: list + sample: ['...', '...'] +""" + +import time +import shlex +import re + + +def to_lines(stdout): + for item in stdout: + if isinstance(item, basestring): + item = str(item).split('\n') + yield item + + +def main(): + spec = dict( + commands=dict(type='list'), + waitfor=dict(type='list'), + retries=dict(default=10, type='int'), + interval=dict(default=1, type='int') + ) + + module = get_module(argument_spec=spec, + supports_check_mode=True) + + commands = module.params['commands'] + + retries = module.params['retries'] + interval = module.params['interval'] + + try: + queue = set() + for entry in (module.params['waitfor'] or list()): + queue.add(Conditional(entry)) + except AttributeError: + exc = get_exception() + module.fail_json(msg=exc.message) + + result = dict(changed=False) + + while retries > 0: + response = module.execute(commands) + result['stdout'] = response + + for item in list(queue): + if item(response): + queue.remove(item) + + if not queue: + break + + time.sleep(interval) + retries -= 1 + else: + failed_conditions = [item.raw for item in queue] + module.fail_json(msg='timeout waiting for value', failed_conditions=failed_conditions) + + result['stdout_lines'] = list(to_lines(result['stdout'])) + return module.exit_json(**result) + +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * +from ansible.module_utils.shell import * +from ansible.module_utils.netcfg import * +from ansible.module_utils.asa import * +if __name__ == '__main__': + main() From c6a45234e094922786de78a83db3028804cc9141 Mon Sep 17 00:00:00 2001 From: mathieu bultel Date: Mon, 11 Jul 2016 19:36:00 +0200 Subject: [PATCH 1814/2522] Add os_stack module for create, update and delete stack (#2002) * Add os_stack module for create and delete stack * Add ansible module for creating and deleting heat stack * Parameters: - stack name - template - environment_files (list) - parameters (dict) - timeout - rollback - state: In a near futur I would like to improve this module by providing a way updating the stack if already exist. Shade doesn't offer this functionality AFAIK * Add update stack feature * Update added_version and return doc * Add copyright for os_stack module * Add wait user choice and minor fixes * Remove Todo for Shade 1.8.0 and bad line example * Add documentation for the return values * Fix type on return value * Fix yaml syntax * Cast message to string instead * add missing check mode --- cloud/openstack/os_stack.py | 262 ++++++++++++++++++++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 cloud/openstack/os_stack.py diff --git a/cloud/openstack/os_stack.py b/cloud/openstack/os_stack.py new file mode 100644 index 00000000000..503ae635dbb --- /dev/null +++ b/cloud/openstack/os_stack.py @@ -0,0 +1,262 @@ +#!/usr/bin/python +#coding: utf-8 -*- + +# (c) 2016, Mathieu Bultel +# (c) 2016, Steve Baker +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + +from time import sleep +from distutils.version import StrictVersion +try: + import shade + HAS_SHADE = True +except ImportError: + HAS_SHADE = False + +DOCUMENTATION = ''' +--- +module: os_stack +short_description: Add/Remove Heat Stack +extends_documentation_fragment: openstack +version_added: "2.2" +author: "Mathieu Bultel (matbu), Steve Baker (steveb)" +description: + - Add or Remove a Stack to an OpenStack Heat +options: + state: + description: + - Indicate desired state of the resource + choices: ['present', 'absent'] + required: false + default: present + name: + description: + - Name of the stack that should be created, name could be char and digit, no space + required: true + template: + description: + - Path of the template file to use for the stack creation + required: false + default: None + environment: + description: + - List of environment files that should be used for the stack creation + required: false + default: None + parameters: + description: + - Dictionary of parameters for the stack creation + required: false + default: None + rollback: + description: + - Rollback stack creation + required: false + default: false + timeout: + description: + - Maximum number of seconds to wait for the stack creation + required: false + default: 3600 +requirements: + - "python >= 2.6" + - "shade" +''' +EXAMPLES = ''' +--- +- name: create stack + ignore_errors: True + register: stack_create + os_stack: + name: "{{ stack_name }}" + state: present + template: "/path/to/my_stack.yaml" + environment: + - /path/to/resource-registry.yaml + - /path/to/environment.yaml + parameters: + bmc_flavor: m1.medium + bmc_image: CentOS + key_name: default + private_net: {{ private_net_param }} + node_count: 2 + name: undercloud + image: CentOS + my_flavor: m1.large + external_net: {{ external_net_param }} +''' + +RETURN = ''' +id: + description: Stack ID. + type: string + sample: "97a3f543-8136-4570-920e-fd7605c989d6" + +stack: + action: + description: Action, could be Create or Update. + type: string + sample: "CREATE" + creation_time: + description: Time when the action has been made. + type: string + sample: "2016-07-05T17:38:12Z" + description: + description: Description of the Stack provided in the heat template. + type: string + sample: "HOT template to create a new instance and networks" + id: + description: Stack ID. + type: string + sample: "97a3f543-8136-4570-920e-fd7605c989d6" + name: + description: Name of the Stack + type: string + sample: "test-stack" + identifier: + description: Identifier of the current Stack action. + type: string + sample: "test-stack/97a3f543-8136-4570-920e-fd7605c989d6" + links: + description: Links to the current Stack. + type: list of dict + sample: "[{'href': 'http://foo:8004/v1/7f6a/stacks/test-stack/97a3f543-8136-4570-920e-fd7605c989d6']" + outputs: + description: Output returned by the Stack. + type: list of dict + sample: "{'description': 'IP address of server1 in private network', + 'output_key': 'server1_private_ip', + 'output_value': '10.1.10.103'}" + parameters: + description: Parameters of the current Stack + type: dict + sample: "{'OS::project_id': '7f6a3a3e01164a4eb4eecb2ab7742101', + 'OS::stack_id': '97a3f543-8136-4570-920e-fd7605c989d6', + 'OS::stack_name': 'test-stack', + 'stack_status': 'CREATE_COMPLETE', + 'stack_status_reason': 'Stack CREATE completed successfully', + 'status': 'COMPLETE', + 'template_description': 'HOT template to create a new instance and networks', + 'timeout_mins': 60, + 'updated_time': null}" +''' + +def _create_stack(module, stack, cloud): + try: + stack = cloud.create_stack(module.params['name'], + template_file=module.params['template'], + environment_files=module.params['environment'], + timeout=module.params['timeout'], + wait=True, + rollback=module.params['rollback'], + **module.params['parameters']) + + stack = cloud.get_stack(stack.id, None) + if stack.stack_status == 'CREATE_COMPLETE': + return stack + else: + return False + module.fail_json(msg = "Failure in creating stack: ".format(stack)) + except shade.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + +def _update_stack(module, stack, cloud): + try: + stack = cloud.update_stack( + module.params['name'], + template_file=module.params['template'], + environment_files=module.params['environment'], + timeout=module.params['timeout'], + rollback=module.params['rollback'], + wait=module.params['wait']) + + if stack['stack_status'] == 'UPDATE_COMPLETE': + return stack + else: + module.fail_json(msg = "Failure in updating stack: %s" % + stack['stack_status_reason']) + except shade.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + +def _system_state_change(module, stack, cloud): + state = module.params['state'] + if state == 'present': + if not stack: + return True + if state == 'absent' and stack: + return True + return False + +def main(): + + argument_spec = openstack_full_argument_spec( + name=dict(required=True), + template=dict(default=None), + environment=dict(default=None, type='list'), + parameters=dict(default={}, type='dict'), + rollback=dict(default=False, type='bool'), + timeout=dict(default=3600, type='int'), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = openstack_module_kwargs() + module = AnsibleModule(argument_spec, + supports_check_mode=True, + **module_kwargs) + + # stack API introduced in 1.8.0 + if not HAS_SHADE or (StrictVersion(shade.__version__) < StrictVersion('1.8.0')): + module.fail_json(msg='shade 1.8.0 or higher is required for this module') + + state = module.params['state'] + name = module.params['name'] + # Check for required parameters when state == 'present' + if state == 'present': + for p in ['template']: + if not module.params[p]: + module.fail_json(msg='%s required with present state' % p) + + try: + cloud = shade.openstack_cloud(**module.params) + stack = cloud.get_stack(name) + + if module.check_mode: + module.exit_json(changed=_system_state_change(module, stack, + cloud)) + + if state == 'present': + if not stack: + stack = _create_stack(module, stack, cloud) + else: + stack = _update_stack(module, stack, cloud) + changed = True + module.exit_json(changed=changed, + stack=stack, + id=stack.id) + elif state == 'absent': + if not stack: + changed = False + else: + changed = True + if not cloud.delete_stack(name, wait=module.params['wait']): + module.fail_json(msg='delete stack failed for stack: %s' % name) + module.exit_json(changed=changed) + except shade.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * +if __name__ == '__main__': + main() From 8fe06a86f7fea5862478aac9be788fb7a43b892f Mon Sep 17 00:00:00 2001 From: Robin Roth Date: Tue, 12 Jul 2016 22:52:43 +0200 Subject: [PATCH 1815/2522] Check for zypper version before using options (#2549) * priority needs zypper version >= 1.12.25 * gpgcheck needs zypper version >= 1.6.2 * output warnings if version not sufficient for option * fixes #2548 --- packaging/os/zypper_repository.py | 47 +++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/packaging/os/zypper_repository.py b/packaging/os/zypper_repository.py index 54a641c7b5f..aac7d870965 100644 --- a/packaging/os/zypper_repository.py +++ b/packaging/os/zypper_repository.py @@ -55,6 +55,7 @@ - Whether to disable GPG signature checking of all packages. Has an effect only if state is I(present). + - Needs zypper version >= 1.6.2. required: false default: "no" choices: [ "yes", "no" ] @@ -68,6 +69,7 @@ description: - Set priority of repository. Packages will always be installed from the repository with the smallest priority number. + - Needs zypper version >= 1.12.25. required: false version_added: "2.1" overwrite_multiple: @@ -95,6 +97,8 @@ REPO_OPTS = ['alias', 'name', 'priority', 'enabled', 'autorefresh', 'gpgcheck'] +from distutils.version import LooseVersion + def _parse_repos(module): """parses the output of zypper -x lr and return a parse repo dictionary""" cmd = ['/usr/bin/zypper', '-x', 'lr'] @@ -167,22 +171,33 @@ def repo_exists(module, repodata, overwrite_multiple): module.fail_json(msg=errmsg) -def modify_repo(module, repodata, old_repos): +def modify_repo(module, repodata, old_repos, zypper_version, warnings): repo = repodata['url'] cmd = ['/usr/bin/zypper', 'ar', '--check'] if repodata['name']: cmd.extend(['--name', repodata['name']]) + # priority on addrepo available since 1.12.25 + # https://github.com/openSUSE/zypper/blob/b9b3cb6db76c47dc4c47e26f6a4d2d4a0d12b06d/package/zypper.changes#L327-L336 if repodata['priority']: - cmd.extend(['--priority', str(repodata['priority'])]) + if zypper_version >= LooseVersion('1.12.25'): + cmd.extend(['--priority', str(repodata['priority'])]) + else: + warnings.append("Setting priority only available for zypper >= 1.12.25. Ignoring priority argument.") if repodata['enabled'] == '0': cmd.append('--disable') - if repodata['gpgcheck'] == '1': - cmd.append('--gpgcheck') + # gpgcheck available since 1.6.2 + # https://github.com/openSUSE/zypper/blob/b9b3cb6db76c47dc4c47e26f6a4d2d4a0d12b06d/package/zypper.changes#L2446-L2449 + # the default changed in the past, so don't assume a default here and show warning for old zypper versions + if zypper_version >= LooseVersion('1.6.2'): + if repodata['gpgcheck'] == '1': + cmd.append('--gpgcheck') + else: + cmd.append('--no-gpgcheck') else: - cmd.append('--no-gpgcheck') + warnings.append("Enabling/disabling gpgcheck only available for zypper >= 1.6.2. Using zypper default value.") if repodata['autorefresh'] == '1': cmd.append('--refresh') @@ -217,6 +232,13 @@ def remove_repo(module, repo): return changed +def get_zypper_version(module): + rc, stdout, stderr = module.run_command(['/usr/bin/zypper', '--version']) + if rc != 0 or not stdout.startswith('zypper '): + return LooseVersion('1.0') + return LooseVersion(stdout.split()[1]) + + def fail_if_rc_is_null(module, rc, stdout, stderr): if rc != 0: #module.fail_json(msg=stderr if stderr else stdout) @@ -233,11 +255,11 @@ def main(): repo=dict(required=False), state=dict(choices=['present', 'absent'], default='present'), description=dict(required=False), - disable_gpg_check = dict(required=False, default='no', type='bool'), - refresh = dict(required=False, default='yes', type='bool'), + disable_gpg_check = dict(required=False, default=False, type='bool'), + refresh = dict(required=False, default=True, type='bool'), priority = dict(required=False, type='int'), - enabled = dict(required=False, default='yes', type='bool'), - overwrite_multiple = dict(required=False, default='no', type='bool'), + enabled = dict(required=False, default=True, type='bool'), + overwrite_multiple = dict(required=False, default=False, type='bool'), ), supports_check_mode=False, ) @@ -247,6 +269,9 @@ def main(): state = module.params['state'] overwrite_multiple = module.params['overwrite_multiple'] + zypper_version = get_zypper_version(module) + warnings = [] # collect warning messages for final output + repodata = { 'url': repo, 'alias': alias, @@ -288,7 +313,7 @@ def exit_unchanged(): if state == 'present': if exists and not mod: exit_unchanged() - changed = modify_repo(module, repodata, old_repos) + changed = modify_repo(module, repodata, old_repos, zypper_version, warnings) elif state == 'absent': if not exists: exit_unchanged() @@ -296,7 +321,7 @@ def exit_unchanged(): repo=alias changed = remove_repo(module, repo) - module.exit_json(changed=changed, repodata=repodata, state=state) + module.exit_json(changed=changed, repodata=repodata, state=state, warnings=warnings) # import module snippets from ansible.module_utils.basic import * From f2b255ffa3c02b2f8aed9fdcb84d08fe513fd8b3 Mon Sep 17 00:00:00 2001 From: Andrii Radyk Date: Wed, 13 Jul 2016 08:28:05 +0200 Subject: [PATCH 1816/2522] Add zypper refresh support in zypper module (#2411) * added zypper refresh support * removed trailing symbols * added forced zypper refresh support similar to update_cache for apt module * removed unnecessary blocks and cleaned up the logic for refresh * added update_cache as alias for refresh to be similar to apt/yum module * update zypper module according to comments --- packaging/os/zypper.py | 47 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/packaging/os/zypper.py b/packaging/os/zypper.py index f90b4c250da..6297a85992e 100644 --- a/packaging/os/zypper.py +++ b/packaging/os/zypper.py @@ -36,6 +36,7 @@ - "Alexander Gubin (@alxgu)" - "Thomas O'Donnell (@andytom)" - "Robin Roth (@robinro)" + - "Andrii Radyk (@AnderEnder)" version_added: "1.2" short_description: Manage packages on SUSE and openSUSE description: @@ -72,7 +73,7 @@ disable_recommends: version_added: "1.8" description: - - Corresponds to the C(--no-recommends) option for I(zypper). Default behavior (C(yes)) modifies zypper's default behavior; C(no) does install recommended packages. + - Corresponds to the C(--no-recommends) option for I(zypper). Default behavior (C(yes)) modifies zypper's default behavior; C(no) does install recommended packages. required: false default: "yes" choices: [ "yes", "no" ] @@ -83,9 +84,18 @@ required: false default: "no" choices: [ "yes", "no" ] + update_cache: + version_added: "2.2" + description: + - Run the equivalent of C(zypper refresh) before the operation. + required: false + default: "no" + choices: [ "yes", "no" ] + aliases: [ "refresh" ] + # informational: requirements for nodes -requirements: +requirements: - "zypper >= 1.0 # included in openSuSE >= 11.1 or SuSE Linux Enterprise Server/Desktop >= 11.0" - rpm ''' @@ -114,6 +124,9 @@ # Apply all available patches - zypper: name=* state=latest type=patch + +# Refresh repositories and update package "openssl" +- zypper: name=openssl state=present update_cache=yes ''' @@ -160,7 +173,7 @@ def parse_zypper_xml(m, cmd, fail_not_found=True, packages=None): # zypper exit codes # 0: success # 106: signature verification failed - # 103: zypper was upgraded, run same command again + # 103: zypper was upgraded, run same command again if packages is None: firstrun = True packages = {} @@ -185,14 +198,15 @@ def parse_zypper_xml(m, cmd, fail_not_found=True, packages=None): def get_cmd(m, subcommand): "puts together the basic zypper command arguments with those passed to the module" is_install = subcommand in ['install', 'update', 'patch'] + is_refresh = subcommand == 'refresh' cmd = ['/usr/bin/zypper', '--quiet', '--non-interactive', '--xmlout'] # add global options before zypper command - if is_install and m.params['disable_gpg_check']: + if (is_install or is_refresh) and m.params['disable_gpg_check']: cmd.append('--no-gpg-checks') cmd.append(subcommand) - if subcommand != 'patch': + if subcommand != 'patch' and not is_refresh: cmd.extend(['--type', m.params['type']]) if m.check_mode and subcommand != 'search': cmd.append('--dry-run') @@ -325,6 +339,18 @@ def package_absent(m, name): return retvals + +def repo_refresh(m): + "update the repositories" + retvals = {'rc': 0, 'stdout': '', 'stderr': '', 'changed': False, 'failed': False} + + cmd = get_cmd(m, 'refresh') + + retvals['cmd'] = cmd + result, retvals['rc'], retvals['stdout'], retvals['stderr'] = parse_zypper_xml(m, cmd) + + return retvals + # =========================================== # Main control flow @@ -337,12 +363,21 @@ def main(): disable_gpg_check = dict(required=False, default='no', type='bool'), disable_recommends = dict(required=False, default='yes', type='bool'), force = dict(required=False, default='no', type='bool'), + update_cache = dict(required=False, aliases=['refresh'], default='no', type='bool'), ), supports_check_mode = True ) name = module.params['name'] state = module.params['state'] + update_cache = module.params['update_cache'] + + # Refresh repositories + if update_cache: + retvals = repo_refresh(module) + + if retvals['rc'] != 0: + module.fail_json(msg="Zypper refresh run failed.", **retvals) # Perform requested action if name == ['*'] and state == 'latest': @@ -366,7 +401,7 @@ def main(): del retvals['stdout'] del retvals['stderr'] - module.exit_json(name=name, state=state, **retvals) + module.exit_json(name=name, state=state, update_cache=update_cache, **retvals) # import module snippets from ansible.module_utils.basic import * From 2078c4b4da6958df3f0e26d8aae60991c6b448b1 Mon Sep 17 00:00:00 2001 From: Sudheer Satyanarayana Date: Wed, 13 Jul 2016 19:56:13 +0530 Subject: [PATCH 1817/2522] which not who (#2557) minor text fix --- system/firewalld.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/firewalld.py b/system/firewalld.py index 89e821a48c7..ff5d32d84c4 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -88,7 +88,7 @@ version_added: "2.1" notes: - Not tested on any Debian based system. - - Requires the python2 bindings of firewalld, who may not be installed by default if the distribution switched to python 3 + - Requires the python2 bindings of firewalld, which may not be installed by default if the distribution switched to python 3 requirements: [ 'firewalld >= 0.2.11' ] author: "Adam Miller (@maxamillion)" ''' From 54ce5c88d502c0126496f584c5476979ab1ea4dc Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Thu, 14 Jul 2016 15:24:08 +0200 Subject: [PATCH 1818/2522] wakeonlan: New module to send out magic WOL packets (#2271) * New module wakeonlan to send out magic WOL packets For a local project managing desktop Windows systems at an elementary school, we want to send out wake-on-lan packets to all systems before continuing using Ansible. That is the purpose of this module. PS We can make this module idempotent by implementing arping support using scapy. At some point I may add this, at this time I simply plan on using wait_for to check if the system is online. * Improved documentation and notes * Improve the documentation a bit * Fix Travis warnings and review remarks * Fix exception handling to support both python2 and python3 * Documentation changes --- network/wakeonlan.py | 126 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 network/wakeonlan.py diff --git a/network/wakeonlan.py b/network/wakeonlan.py new file mode 100644 index 00000000000..11fecdceaad --- /dev/null +++ b/network/wakeonlan.py @@ -0,0 +1,126 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Dag Wieers +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: wakeonlan +version_added: 2.2 +short_description: Send a magic Wake-on-LAN (WoL) broadcast packet +description: + - The M(wakeonlan) module sends magic Wake-on-LAN (WoL) broadcast packets. +options: + mac: + description: + - MAC address to send Wake-on-LAN broadcast packet for + required: true + default: null + broadcast: + description: + - Network broadcast address to use for broadcasting magic Wake-on-LAN packet + required: false + default: 255.255.255.255 + port: + description: + - UDP port to use for magic Wake-on-LAN packet + required: false + default: 7 +author: "Dag Wieers (@dagwieers)" +todo: + - Add arping support to check whether the system is up (before and after) + - Enable check-mode support (when we have arping support) + - Does not have SecureOn password support +notes: + - This module sends a magic packet, without knowing whether it worked + - Only works if the target system was properly configured for Wake-on-LAN (in the BIOS and/or the OS) + - Some BIOSes have a different (configurable) Wake-on-LAN boot order (i.e. PXE first) when turned off +''' + +EXAMPLES = ''' +# Send a magic Wake-on-LAN packet to 00:CA:FE:BA:BE:00 +- local_action: wakeonlan mac=00:CA:FE:BA:BE:00 broadcast=192.168.1.255 + +- wakeonlan: mac=00:CA:FE:BA:BE:00 port=9 + delegate_to: localhost +''' + +RETURN=''' +# Default return values +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +import socket +import struct + + +def wakeonlan(module, mac, broadcast, port): + """ Send a magic Wake-on-LAN packet. """ + + mac_orig = mac + + # Remove possible seperator from MAC address + if len(mac) == 12 + 5: + mac = mac.replace(mac[2], '') + + # If we don't end up with 12 hexadecimal characters, fail + if len(mac) != 12: + module.fail_json(msg="Incorrect MAC address length: %s" % mac_orig) + + # Test if it converts to an integer, otherwise fail + try: + int(mac, 16) + except ValueError: + module.fail_json(msg="Incorrect MAC address format: %s" % mac_orig) + + # Create payload for magic packet + data = '' + padding = ''.join(['FFFFFFFFFFFF', mac * 20]) + for i in range(0, len(padding), 2): + data = ''.join([data, struct.pack('B', int(padding[i: i + 2], 16))]) + + # Broadcast payload to network + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + try: + sock.sendto(data, (broadcast, port)) + except socket.error: + e = get_exception() + module.fail_json(msg=str(e)) + + +def main(): + module = AnsibleModule( + argument_spec = dict( + mac = dict(required=True, type='str'), + broadcast = dict(required=False, default='255.255.255.255'), + port = dict(required=False, type='int', default=7), + ), + ) + + mac = module.params.get('mac') + broadcast = module.params.get('broadcast') + port = module.params.get('port') + + wakeonlan(module, mac, broadcast, port) + module.exit_json(changed=True) + + +if __name__ == '__main__': + main() \ No newline at end of file From e32897f4d97c64910e7ae02a9d3e60e83dc80249 Mon Sep 17 00:00:00 2001 From: Bulat Gaifullin Date: Thu, 14 Jul 2016 18:38:20 +0400 Subject: [PATCH 1819/2522] Add ipmi modules for power and boot management (#2550) * Add ipmi modules for power and boot management * ipmi_power - module for power management Parameters: - name - port - user - password - state - timeout * ipmi_boot - module for boot order management Parameters: - name - port - user - password - bootdev - state - persist - uefi * Fixed copyright * Supported check mode Also added description for RETURN * Added ipmi to list of excludes of tests for python2.4 * added no_log=True for secrets * added type for port and mark bootdev as required field --- .travis.yml | 2 +- bmc/__init__.py | 0 bmc/ipmi/__init__.py | 0 bmc/ipmi/ipmi_boot.py | 186 +++++++++++++++++++++++++++++++++++++++++ bmc/ipmi/ipmi_power.py | 138 ++++++++++++++++++++++++++++++ 5 files changed, 325 insertions(+), 1 deletion(-) create mode 100644 bmc/__init__.py create mode 100644 bmc/ipmi/__init__.py create mode 100644 bmc/ipmi/ipmi_boot.py create mode 100644 bmc/ipmi/ipmi_power.py diff --git a/.travis.yml b/.travis.yml index 05a60a8dcb2..7f1ae7126cb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -117,7 +117,7 @@ install: - pip install git+https://github.com/ansible/ansible.git@devel#egg=ansible - pip install git+https://github.com/sivel/ansible-testing.git#egg=ansible_testing script: - - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py|database/influxdb/influxdb.*\.py|database/mssql/mssql_db\.py|/letsencrypt\.py|network/f5/bigip.*\.py' . + - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py|database/influxdb/influxdb.*\.py|database/mssql/mssql_db\.py|/letsencrypt\.py|network/f5/bigip.*\.py|bmc/ipmi/.*\.py' . - python2.6 -m compileall -fq . - python2.7 -m compileall -fq . - python3.4 -m compileall -fq . -x $(echo "$PY3_EXCLUDE_LIST"| tr ' ' '|') diff --git a/bmc/__init__.py b/bmc/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/bmc/ipmi/__init__.py b/bmc/ipmi/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/bmc/ipmi/ipmi_boot.py b/bmc/ipmi/ipmi_boot.py new file mode 100644 index 00000000000..e8f13d8bd7c --- /dev/null +++ b/bmc/ipmi/ipmi_boot.py @@ -0,0 +1,186 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +try: + from pyghmi.ipmi import command +except ImportError: + command = None + +from ansible.module_utils.basic import * + + +DOCUMENTATION = ''' +--- +module: ipmi_boot +short_description: Management of order of boot devices +description: + - Use this module to manage order of boot devices +version_added: "2.2" +options: + name: + description: + - Hostname or ip address of the BMC. + required: true + port: + description: + - Remote RMCP port. + required: false + type: int + default: 623 + user: + description: + - Username to use to connect to the BMC. + required: true + password: + description: + - Password to connect to the BMC. + required: true + default: null + bootdev: + description: + - Set boot device to use on next reboot + required: true + choices: + - network -- Request network boot + - hd -- Boot from hard drive + - safe -- Boot from hard drive, requesting 'safe mode' + - optical -- boot from CD/DVD/BD drive + - setup -- Boot into setup utility + - default -- remove any IPMI directed boot device request + state: + description: + - Whether to ensure that boot devices is desired. + default: present + choices: + - present -- Request system turn on + - absent -- Request system turn on + persistent: + description: + - If set, ask that system firmware uses this device beyond next boot. + Be aware many systems do not honor this. + required: false + type: boolean + default: false + uefiboot: + description: + - If set, request UEFI boot explicitly. + Strictly speaking, the spec suggests that if not set, the system should BIOS boot and offers no "don't care" option. + In practice, this flag not being set does not preclude UEFI boot on any system I've encountered. + required: false + type: boolean + default: false +requirements: + - "python >= 2.6" + - pyghmi +author: "Bulat Gaifullin (gaifullinbf@gmail.com)" +''' + +RETURN = ''' +bootdev: + description: The boot device name which will be used beyond next boot. + returned: success + type: string + sample: default +persistent: + description: If True, system firmware will use this device beyond next boot. + returned: success + type: bool + sample: false +uefimode: + description: If True, system firmware will use UEFI boot explicitly beyond next boot. + returned: success + type: bool + sample: false +''' + +EXAMPLES = ''' +# Ensure bootdevice is HD. +- ipmi_boot: name="test.testdomain.com" user="admin" password="password" bootdev="hd" +# Ensure bootdevice is not Network +- ipmi_boot: name="test.testdomain.com" user="admin" password="password" bootdev="network" state=absent +''' + +# ================================================== + + +def main(): + module = AnsibleModule( + argument_spec=dict( + name=dict(required=True), + port=dict(default=623, type='int'), + user=dict(required=True, no_log=True), + password=dict(required=True, no_log=True), + state=dict(default='present', choices=['present', 'absent']), + bootdev=dict(required=True, choices=['network', 'hd', 'safe', 'optical', 'setup', 'default']), + persistent=dict(default=False, type='bool'), + uefiboot=dict(default=False, type='bool') + ), + supports_check_mode=True, + ) + + if command is None: + module.fail_json(msg='the python pyghmi module is required') + + name = module.params['name'] + port = module.params['port'] + user = module.params['user'] + password = module.params['password'] + state = module.params['state'] + bootdev = module.params['bootdev'] + persistent = module.params['persistent'] + uefiboot = module.params['uefiboot'] + request = dict() + + if state == 'absent' and bootdev == 'default': + module.fail_json(msg="The bootdev 'default' cannot be used with state 'absent'.") + + # --- run command --- + try: + ipmi_cmd = command.Command( + bmc=name, userid=user, password=password, port=port + ) + module.debug('ipmi instantiated - name: "%s"' % name) + current = ipmi_cmd.get_bootdev() + # uefimode may not supported by BMC, so use desired value as default + current.setdefault('uefimode', uefiboot) + if state == 'present' and current != dict(bootdev=bootdev, persistent=persistent, uefimode=uefiboot): + request = dict(bootdev=bootdev, uefiboot=uefiboot, persist=persistent) + elif state == 'absent' and current['bootdev'] == bootdev: + request = dict(bootdev='default') + else: + module.exit_json(changed=False, **current) + + if module.check_mode: + response = dict(bootdev=request['bootdev']) + else: + response = ipmi_cmd.set_bootdev(**request) + + if 'error' in response: + module.fail_json(msg=response['error']) + + if 'persist' in request: + response['persistent'] = request['persist'] + if 'uefiboot' in request: + response['uefimode'] = request['uefiboot'] + + module.exit_json(changed=True, **response) + except Exception as e: + module.fail_json(msg=str(e)) + +if __name__ == '__main__': + main() diff --git a/bmc/ipmi/ipmi_power.py b/bmc/ipmi/ipmi_power.py new file mode 100644 index 00000000000..c6cc8df0301 --- /dev/null +++ b/bmc/ipmi/ipmi_power.py @@ -0,0 +1,138 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +try: + from pyghmi.ipmi import command +except ImportError: + command = None + +from ansible.module_utils.basic import * + + +DOCUMENTATION = ''' +--- +module: ipmi_power +short_description: Power management for machine +description: + - Use this module for power management +version_added: "2.2" +options: + name: + description: + - Hostname or ip address of the BMC. + required: true + port: + description: + - Remote RMCP port. + required: false + type: int + default: 623 + user: + description: + - Username to use to connect to the BMC. + required: true + password: + description: + - Password to connect to the BMC. + required: true + default: null + state: + description: + - Whether to ensure that the machine in desired state. + required: true + choices: + - on -- Request system turn on + - off -- Request system turn off without waiting for OS to shutdown + - shutdown -- Have system request OS proper shutdown + - reset -- Request system reset without waiting for OS + - boot -- If system is off, then 'on', else 'reset' + timeout: + description: + - Maximum number of seconds before interrupt request. + required: false + type: int + default: 300 +requirements: + - "python >= 2.6" + - pyghmi +author: "Bulat Gaifullin (gaifullinbf@gmail.com)" +''' + +RETURN = ''' +powerstate: + description: The current power state of the machine. + returned: success + type: string + sample: on +''' + +EXAMPLES = ''' +# Ensure machine is powered on. +- ipmi_power: name="test.testdomain.com" user="admin" password="password" state="on" +''' + +# ================================================== + + +def main(): + module = AnsibleModule( + argument_spec=dict( + name=dict(required=True), + port=dict(default=623, type='int'), + state=dict(required=True, choices=['on', 'off', 'shutdown', 'reset', 'boot']), + user=dict(required=True, no_log=True), + password=dict(required=True, no_log=True), + timeout=dict(default=300, type='int'), + ), + supports_check_mode=True, + ) + + if command is None: + module.fail_json(msg='the python pyghmi module is required') + + name = module.params['name'] + port = module.params['port'] + user = module.params['user'] + password = module.params['password'] + state = module.params['state'] + timeout = module.params['timeout'] + + # --- run command --- + try: + ipmi_cmd = command.Command( + bmc=name, userid=user, password=password, port=port + ) + module.debug('ipmi instantiated - name: "%s"' % name) + + current = ipmi_cmd.get_power() + if current['powerstate'] != state: + response = {'powerstate': state} if module.check_mode else ipmi_cmd.set_power(state, wait=timeout) + changed = True + else: + response = current + changed = False + + if 'error' in response: + module.fail_json(msg=response['error']) + + module.exit_json(changed=changed, **response) + except Exception as e: + module.fail_json(msg=str(e)) + +if __name__ == '__main__': + main() From c574dbee540968f625cfad9266c641286a64cec7 Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Wed, 2 Mar 2016 13:45:36 -0800 Subject: [PATCH 1820/2522] iam_server_certificate_facts: Retrieve attributes from server certificate This module will allow you to retrieve all the attributes related to a server certificate. --- cloud/amazon/iam_server_certificate_facts.py | 164 +++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 cloud/amazon/iam_server_certificate_facts.py diff --git a/cloud/amazon/iam_server_certificate_facts.py b/cloud/amazon/iam_server_certificate_facts.py new file mode 100644 index 00000000000..d19b6884873 --- /dev/null +++ b/cloud/amazon/iam_server_certificate_facts.py @@ -0,0 +1,164 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: iam_server_certificate_facts +short_description: Retrieve the facts of a server certificate +description: + - Retrieve the attributes of a server certificate +version_added: "2.2" +author: "Allen Sanabria (@linuxdynasty)" +requirements: [boto3, botocore] +options: + name: + description: + - The name of the server certificate you are retrieveing attributes for. + required: true +extends_documentation_fragment: + - aws + - ec2 +''' + +EXAMPLES = ''' +# Retrieve server certificate +- iam_server_certificate_facts: + name: production-cert + register: server_cert + +# Fail if the server certificate name was not found +- iam_server_certificate_facts: + name: production-cert + register: server_cert + failed_when: "{{ server_cert.results | length == 0 }}" +''' + +RETURN = ''' +server_certificate_id: + description: The 21 character certificate id + returned: success + type: str + sample: "ADWAJXWTZAXIPIMQHMJPO" +certificate_body: + description: The asn1der encoded PEM string + returned: success + type: str + sample: "-----BEGIN CERTIFICATE-----\nbunch of random data\n-----END CERTIFICATE-----" +server_certificate_name: + description: The name of the server certificate + returned: success + type: str + sample: "server-cert-name" +arn: + description: The Amazon resource name of the server certificate + returned: success + type: str + sample: "arn:aws:iam::911277865346:server-certificate/server-cert-name" +path: + description: The path of the server certificate + returned: success + type: str + sample: "/" +expiration: + description: The date and time this server certificate will expire, in ISO 8601 format. + returned: success + type: str + sample: "2017-06-15T12:00:00+00:00" +upload_date: + description: The date and time this server certificate was uploaded, in ISO 8601 format. + returned: success + type: str + sample: "2015-04-25T00:36:40+00:00" +''' +try: + import boto3 + import botocore.exceptions + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + + +def get_server_cert(iam, name=None): + """Retrieve the attributes of a server certificate if it exists + Args: + iam (botocore.client.IAM): The boto3 iam instance. + + Kwargs: + name (str): The name of the server certificate. + + Basic Usage: + >>> import boto3 + >>> iam = boto3.client('iam') + >>> name = "server-cert-name" + >>> results = get_server_cert(iam, name) + [ + { + "upload_date": "2015-04-25T00:36:40+00:00", + "server_certificate_id": "ADWAJXWTZAXIPIMQHMJPO", + "certificate_body": "-----BEGIN CERTIFICATE-----\nbunch of random data\n-----END CERTIFICATE-----", + "server_certificate_name": "server-cert-name", + "expiration": "2017-06-15T12:00:00+00:00", + "path": "/", + "arn": "arn:aws:iam::911277865346:server-certificate/server-cert-name" + } + ] + """ + results = [] + try: + server_cert = iam.get_server_certificate(ServerCertificateName=name)['ServerCertificate'] + cert_md = server_cert['ServerCertificateMetadata'] + cert_data = { + 'certificate_body': server_cert['CertificateBody'], + 'server_certificate_id': cert_md['ServerCertificateId'], + 'server_certificate_name': cert_md['ServerCertificateName'], + 'arn': cert_md['Arn'], + 'path': cert_md['Path'], + 'expiration': cert_md['Expiration'].isoformat(), + 'upload_date': cert_md['UploadDate'].isoformat(), + } + results.append(cert_data) + + except botocore.exceptions.ClientError: + pass + return results + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + name=dict(required=True, type='str'), + )) + + module = AnsibleModule(argument_spec=argument_spec,) + + if not HAS_BOTO3: + module.fail_json(msg='boto3 required for this module') + + try: + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + iam = boto3_conn(module, conn_type='client', resource='iam', region=region, endpoint=ec2_url, **aws_connect_kwargs) + except botocore.exceptions.ClientError, e: + module.fail_json(msg="Boto3 Client Error - " + str(e.msg)) + cert_name = module.params.get('name') + results = get_server_cert(iam, cert_name) + module.exit_json(results=results) + + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From 4615a6cc766d19422cfa82648f475823ba21c37c Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Fri, 8 Jul 2016 13:02:33 -0700 Subject: [PATCH 1821/2522] iam_server_certificate_facts: change `except` to python 2.6 syntax --- cloud/amazon/iam_server_certificate_facts.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/cloud/amazon/iam_server_certificate_facts.py b/cloud/amazon/iam_server_certificate_facts.py index d19b6884873..0c334f41944 100644 --- a/cloud/amazon/iam_server_certificate_facts.py +++ b/cloud/amazon/iam_server_certificate_facts.py @@ -26,11 +26,12 @@ options: name: description: - - The name of the server certificate you are retrieveing attributes for. + - The name of the server certificate you are retrieving attributes for. required: true extends_documentation_fragment: - aws - ec2 +requirements: ['boto3'] ''' EXAMPLES = ''' @@ -83,6 +84,8 @@ type: str sample: "2015-04-25T00:36:40+00:00" ''' + + try: import boto3 import botocore.exceptions @@ -147,10 +150,11 @@ def main(): module.fail_json(msg='boto3 required for this module') try: - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - iam = boto3_conn(module, conn_type='client', resource='iam', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except botocore.exceptions.ClientError, e: - module.fail_json(msg="Boto3 Client Error - " + str(e.msg)) + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + iam = boto3_conn(module, conn_type='client', resource='iam', region=region, endpoint=ec2_url, **aws_connect_kwargs) + except botocore.exceptions.ClientError as e: + module.fail_json(msg="Boto3 Client Error - " + str(e.msg)) + cert_name = module.params.get('name') results = get_server_cert(iam, cert_name) module.exit_json(results=results) From f36ec115fcfa43751e40efe68a30e5ac58fc4b3a Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Thu, 14 Jul 2016 11:21:32 -0700 Subject: [PATCH 1822/2522] iam_server_certificate_facts: list all certs If a server name isn't passed, retrieve all server certificates by default. Change return value to a dict with the server_cert_name being the key. --- cloud/amazon/iam_server_certificate_facts.py | 59 +++++++++++--------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/cloud/amazon/iam_server_certificate_facts.py b/cloud/amazon/iam_server_certificate_facts.py index 0c334f41944..088e88a3de4 100644 --- a/cloud/amazon/iam_server_certificate_facts.py +++ b/cloud/amazon/iam_server_certificate_facts.py @@ -94,8 +94,8 @@ HAS_BOTO3 = False -def get_server_cert(iam, name=None): - """Retrieve the attributes of a server certificate if it exists +def get_server_certs(iam, name=None): + """Retrieve the attributes of a server certificate if it exists or all certs. Args: iam (botocore.client.IAM): The boto3 iam instance. @@ -107,41 +107,46 @@ def get_server_cert(iam, name=None): >>> iam = boto3.client('iam') >>> name = "server-cert-name" >>> results = get_server_cert(iam, name) - [ - { - "upload_date": "2015-04-25T00:36:40+00:00", - "server_certificate_id": "ADWAJXWTZAXIPIMQHMJPO", - "certificate_body": "-----BEGIN CERTIFICATE-----\nbunch of random data\n-----END CERTIFICATE-----", - "server_certificate_name": "server-cert-name", - "expiration": "2017-06-15T12:00:00+00:00", - "path": "/", - "arn": "arn:aws:iam::911277865346:server-certificate/server-cert-name" - } - ] + { + "upload_date": "2015-04-25T00:36:40+00:00", + "server_certificate_id": "ADWAJXWTZAXIPIMQHMJPO", + "certificate_body": "-----BEGIN CERTIFICATE-----\nbunch of random data\n-----END CERTIFICATE-----", + "server_certificate_name": "server-cert-name", + "expiration": "2017-06-15T12:00:00+00:00", + "path": "/", + "arn": "arn:aws:iam::911277865346:server-certificate/server-cert-name" + } """ - results = [] + results = dict() try: - server_cert = iam.get_server_certificate(ServerCertificateName=name)['ServerCertificate'] - cert_md = server_cert['ServerCertificateMetadata'] - cert_data = { - 'certificate_body': server_cert['CertificateBody'], - 'server_certificate_id': cert_md['ServerCertificateId'], - 'server_certificate_name': cert_md['ServerCertificateName'], - 'arn': cert_md['Arn'], - 'path': cert_md['Path'], - 'expiration': cert_md['Expiration'].isoformat(), - 'upload_date': cert_md['UploadDate'].isoformat(), - } - results.append(cert_data) + if name: + server_certs = [iam.get_server_certificate(ServerCertificateName=name)['ServerCertificate']] + else: + server_certs = iam.list_server_certificates()['ServerCertificateMetadataList'] + + for server_cert in server_certs: + if not name: + server_cert = iam.get_server_certificate(ServerCertificateName=server_cert['ServerCertificateName'])['ServerCertificate'] + cert_md = server_cert['ServerCertificateMetadata'] + results[cert_md['ServerCertificateName']] = { + 'certificate_body': server_cert['CertificateBody'], + 'server_certificate_id': cert_md['ServerCertificateId'], + 'server_certificate_name': cert_md['ServerCertificateName'], + 'arn': cert_md['Arn'], + 'path': cert_md['Path'], + 'expiration': cert_md['Expiration'].isoformat(), + 'upload_date': cert_md['UploadDate'].isoformat(), + } except botocore.exceptions.ClientError: pass + return results def main(): argument_spec = ec2_argument_spec() argument_spec.update(dict( - name=dict(required=True, type='str'), + name=dict(type='str'), )) module = AnsibleModule(argument_spec=argument_spec,) From 1a7d26a1b85996f4791f6aec2b74b84f536f9fe4 Mon Sep 17 00:00:00 2001 From: "Ryan S. Brown" Date: Thu, 14 Jul 2016 16:21:18 -0400 Subject: [PATCH 1823/2522] iam_server_certificate_facts: Correct call to `get_server_certs` --- cloud/amazon/iam_server_certificate_facts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/iam_server_certificate_facts.py b/cloud/amazon/iam_server_certificate_facts.py index 088e88a3de4..da0c0beabf6 100644 --- a/cloud/amazon/iam_server_certificate_facts.py +++ b/cloud/amazon/iam_server_certificate_facts.py @@ -106,7 +106,7 @@ def get_server_certs(iam, name=None): >>> import boto3 >>> iam = boto3.client('iam') >>> name = "server-cert-name" - >>> results = get_server_cert(iam, name) + >>> results = get_server_certs(iam, name) { "upload_date": "2015-04-25T00:36:40+00:00", "server_certificate_id": "ADWAJXWTZAXIPIMQHMJPO", @@ -161,7 +161,7 @@ def main(): module.fail_json(msg="Boto3 Client Error - " + str(e.msg)) cert_name = module.params.get('name') - results = get_server_cert(iam, cert_name) + results = get_server_certs(iam, cert_name) module.exit_json(results=results) From 4e1c3a58b376687e4f028e387a5296520372818b Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Wed, 23 Mar 2016 19:09:36 -0700 Subject: [PATCH 1824/2522] Create, Delete, and Modify a Kinesis Stream. * Create a Kinesis Stream. * Tag a Kinesis Stream. * Update the Retention Period of a Kinesis Stream. * Delete a Kinesis Stream. * Wait for a Kinesis Stream to be in an ACTIVE State. --- cloud/amazon/kinesis_stream.py | 1053 ++++++++++++++++++++++++++++++++ 1 file changed, 1053 insertions(+) create mode 100644 cloud/amazon/kinesis_stream.py diff --git a/cloud/amazon/kinesis_stream.py b/cloud/amazon/kinesis_stream.py new file mode 100644 index 00000000000..cc280655a52 --- /dev/null +++ b/cloud/amazon/kinesis_stream.py @@ -0,0 +1,1053 @@ +#!/usr/bin/python +# +# This is a free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This Ansible library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this library. If not, see . + +DOCUMENTATION = ''' +--- +module: kinesis_stream +short_description: Manage a Kinesis Stream. +description: + - Create or Delete a Kinesis Stream. + - Update the retention period of a Kinesis Stream. + - Update Tags on a Kinesis Stream. +version_added: "2.1" +author: Allen Sanabria (@linuxdynasty) +options: + name: + description: + - "The name of the Kinesis Stream you are managing." + default: None + required: true + shards: + description: + - "The number of shards you want to have with this stream. This can not + be modified after being created." + - "This is required when state == present" + required: false + default: None + retention_period: + description: + - "The default retention period is 24 hours and can not be less than 24 + hours." + - "The retention period can be modified during any point in time." + required: false + default: None + state: + description: + - "Create or Delete the Kinesis Stream." + required: false + default: present + choices: [ 'present', 'absent' ] + wait: + description: + - Wait for operation to complete before returning + required: false + default: true + wait_timeout: + description: + - How many seconds to wait for an operation to complete before timing out + required: false + default: 300 + tags: + description: + - "A dictionary of resource tags of the form: { tag1: value1, tag2: value2 }." + required: false + default: null + aliases: [ "resource_tags" ] +extends_documentation_fragment: + - aws + - ec2 +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Basic creation example: +- name: Set up Kinesis Stream with 10 shards and wait for the stream to become ACTIVE + kinesis_stream: + name: test-stream + shards: 10 + wait: yes + wait_timeout: 600 + register: test_stream + +# Basic creation example with tags: +- name: Set up Kinesis Stream with 10 shards, tag the environment, and wait for the stream to become ACTIVE + kinesis_stream: + name: test-stream + shards: 10 + tags: + Env: development + wait: yes + wait_timeout: 600 + register: test_stream + +# Basic creation example with tags and increase the retention period from the default 24 hours to 48 hours: +- name: Set up Kinesis Stream with 10 shards, tag the environment, increase the retention period and wait for the stream to become ACTIVE + kinesis_stream: + name: test-stream + retention_period: 48 + shards: 10 + tags: + Env: development + wait: yes + wait_timeout: 600 + register: test_stream + +# Basic delete example: +- name: Delete Kinesis Stream test-stream and wait for it to finish deleting. + kinesis_stream: + name: test-stream + wait: yes + wait_timeout: 600 + register: test_stream +''' + +RETURN = ''' +stream_name: + description: The name of the Kinesis Stream. + returned: when state == present. + type: string + sample: "test-stream" +stream_arn: + description: The amazon resource identifier + returned: when state == present. + type: string + sample: "arn:aws:kinesis:east-side:123456789:stream/test-stream" +stream_status: + description: The current state of the Kinesis Stream. + returned: when state == present. + type: string + sample: "ACTIVE" +retention_period_hours: + description: Number of hours messages will be kept for a Kinesis Stream. + returned: when state == present. + type: int + sample: 24 +tags: + description: Dictionary containing all the tags associated with the Kinesis stream. + returned: when state == present. + type: dict + sample: { + "Name": "Splunk", + "Env": "development" + } +''' + +try: + import botocore + import boto3 + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + +import re +import datetime +import time +from functools import reduce + +def convert_to_lower(data): + """Convert all uppercase keys in dict with lowercase_ + Args: + data (dict): Dictionary with keys that have upper cases in them + Example.. FooBar == foo_bar + if a val is of type datetime.datetime, it will be converted to + the ISO 8601 + + Basic Usage: + >>> test = {'FooBar': []} + >>> test = convert_to_lower(test) + { + 'foo_bar': [] + } + + Returns: + Dictionary + """ + results = dict() + if isinstance(data, dict): + for key, val in data.items(): + key = re.sub(r'(([A-Z]{1,3}){1})', r'_\1', key).lower() + if key[0] == '_': + key = key[1:] + if isinstance(val, datetime.datetime): + results[key] = val.isoformat() + elif isinstance(val, dict): + results[key] = convert_to_lower(val) + elif isinstance(val, list): + converted = list() + for item in val: + converted.append(convert_to_lower(item)) + results[key] = converted + else: + results[key] = val + return results + +def make_tags_in_proper_format(tags): + """Take a dictionary of tags and convert them into the AWS Tags format. + Args: + tags (list): The tags you want applied. + + Basic Usage: + >>> tags = [{u'Key': 'env', u'Value': 'development'}] + >>> make_tags_in_proper_format(tags) + { + "env": "development", + } + + Returns: + Dict + """ + formatted_tags = dict() + for tag in tags: + formatted_tags[tag.get('Key')] = tag.get('Value') + + return formatted_tags + +def make_tags_in_aws_format(tags): + """Take a dictionary of tags and convert them into the AWS Tags format. + Args: + tags (dict): The tags you want applied. + + Basic Usage: + >>> tags = {'env': 'development', 'service': 'web'} + >>> make_tags_in_proper_format(tags) + [ + { + "Value": "web", + "Key": "service" + }, + { + "Value": "development", + "key": "env" + } + ] + + Returns: + List + """ + formatted_tags = list() + for key, val in tags.items(): + formatted_tags.append({ + 'Key': key, + 'Value': val + }) + + return formatted_tags + +def get_tags(client, stream_name, check_mode=False): + """Retrieve the tags for a Kinesis Stream. + Args: + client (botocore.client.EC2): Boto3 client. + stream_name (str): Name of the Kinesis stream. + + Kwargs: + check_mode (bool): This will pass DryRun as one of the parameters to the aws api. + default=False + + Basic Usage: + >>> client = boto3.client('kinesis') + >>> stream_name = 'test-stream' + >> get_tags(client, stream_name) + + Returns: + Tuple (bool, str, dict) + """ + err_msg = '' + success = False + params = { + 'StreamName': stream_name, + } + results = dict() + try: + if not check_mode: + results = ( + client.list_tags_for_stream(**params)['Tags'] + ) + else: + results = [ + { + 'Key': 'DryRunMode', + 'Value': 'true' + }, + ] + success = True + except botocore.exceptions.ClientError, e: + err_msg = str(e) + + return success, err_msg, results + +def find_stream(client, stream_name, limit=1, check_mode=False): + """Retrieve a Kinesis Stream. + Args: + client (botocore.client.EC2): Boto3 client. + stream_name (str): Name of the Kinesis stream. + + Kwargs: + limit (int): Limit the number of shards to return within a stream. + default=1 + check_mode (bool): This will pass DryRun as one of the parameters to the aws api. + default=False + + Basic Usage: + >>> client = boto3.client('kinesis') + >>> stream_name = 'test-stream' + + Returns: + Tuple (bool, str, dict) + """ + err_msg = '' + success = False + params = { + 'StreamName': stream_name, + 'Limit': limit + } + results = dict() + try: + if not check_mode: + results = ( + client.describe_stream(**params)['StreamDescription'] + ) + results.pop('Shards') + else: + results = { + 'HasMoreShards': True, + 'RetentionPeriodHours': 24, + 'StreamName': stream_name, + 'StreamARN': 'arn:aws:kinesis:east-side:123456789:stream/{0}'.format(stream_name), + 'StreamStatus': u'ACTIVE' + } + success = True + except botocore.exceptions.ClientError, e: + err_msg = str(e) + + return success, err_msg, results + +def wait_for_status(client, stream_name, status, wait_timeout=300, + check_mode=False): + """Wait for the the status to change for a Kinesis Stream. + Args: + client (botocore.client.EC2): Boto3 client + stream_name (str): The name of the kinesis stream. + status (str): The status to wait for. + examples. status=available, status=deleted + + Kwargs: + wait_timeout (int): Number of seconds to wait, until this timeout is reached. + check_mode (bool): This will pass DryRun as one of the parameters to the aws api. + default=False + + Basic Usage: + >>> client = boto3.client('kinesis') + >>> stream_name = 'test-stream' + >>> wait_for_status(client, stream_name, 'ACTIVE', 300) + + Returns: + Tuple (bool, str, dict) + """ + polling_increment_secs = 5 + wait_timeout = time.time() + wait_timeout + status_achieved = False + stream = dict() + err_msg = "" + + if not check_mode: + while wait_timeout > time.time(): + try: + find_success, find_msg, stream = ( + find_stream(client, stream_name) + ) + if status != 'DELETING': + if find_success and stream: + if stream.get('StreamStatus') == status: + status_achieved = True + break + elif status == 'DELETING': + if not find_success: + status_achieved = True + break + else: + time.sleep(polling_increment_secs) + except botocore.exceptions.ClientError as e: + err_msg = str(e) + + else: + status_achieved = True + find_success, find_msg, stream = ( + find_stream(client, stream_name, check_mode=check_mode) + ) + + if not status_achieved: + err_msg = "Wait time out reached, while waiting for results" + + return status_achieved, err_msg, stream + +def tags_action(client, stream_name, tags, action='create', check_mode=False): + """Create or delete multiple tags from a Kinesis Stream. + Args: + client (botocore.client.EC2): Boto3 client. + resource_id (str): The Amazon resource id. + tags (list): List of dictionaries. + examples.. [{Name: "", Values: [""]}] + + Kwargs: + action (str): The action to perform. + valid actions == create and delete + default=create + check_mode (bool): This will pass DryRun as one of the parameters to the aws api. + default=False + + Basic Usage: + >>> client = boto3.client('ec2') + >>> resource_id = 'pcx-123345678' + >>> tags = {'env': 'development'} + >>> update_tags(client, resource_id, tags) + [True, ''] + + Returns: + List (bool, str) + """ + success = False + err_msg = "" + params = {'StreamName': stream_name} + try: + if not check_mode: + if action == 'create': + params['Tags'] = tags + client.add_tags_to_stream(**params) + success = True + elif action == 'delete': + params['TagKeys'] = tags.keys() + client.remove_tags_from_stream(**params) + success = True + else: + err_msg = 'Invalid action {0}'.format(action) + else: + if action == 'create': + success = True + elif action == 'delete': + success = True + else: + err_msg = 'Invalid action {0}'.format(action) + + except botocore.exceptions.ClientError, e: + err_msg = str(e) + + return success, err_msg + +def recreate_tags_from_list(list_of_tags): + """Recreate tags from a list of tuples into the Amazon Tag format. + Args: + list_of_tags (list): List of tuples. + + Basic Usage: + >>> list_of_tags = [('Env', 'Development')] + >>> recreate_tags_from_list(list_of_tags) + [ + { + "Value": "Development", + "Key": "Env" + } + ] + + Returns: + List + """ + tags = list() + i = 0 + list_of_tags = list_of_tags + for i in range(len(list_of_tags)): + key_name = list_of_tags[i][0] + key_val = list_of_tags[i][1] + tags.append( + { + 'Key': key_name, + 'Value': key_val + } + ) + return tags + +def update_tags(client, stream_name, tags, check_mode=False): + """Update tags for an amazon resource. + Args: + resource_id (str): The Amazon resource id. + tags (dict): Dictionary of tags you want applied to the Kinesis stream. + + Kwargs: + check_mode (bool): This will pass DryRun as one of the parameters to the aws api. + default=False + + Basic Usage: + >>> client = boto3.client('ec2') + >>> stream_name = 'test-stream' + >>> tags = {'env': 'development'} + >>> update_tags(client, stream_name, tags) + [True, ''] + + Return: + Tuple (bool, str) + """ + success = False + err_msg = '' + tag_success, tag_msg, current_tags = get_tags(client, stream_name) + if current_tags: + tags = make_tags_in_aws_format(tags) + current_tags_set = ( + set( + reduce( + lambda x, y: x + y, + [make_tags_in_proper_format(current_tags).items()] + ) + ) + ) + + new_tags_set = ( + set( + reduce( + lambda x, y: x + y, + [make_tags_in_proper_format(tags).items()] + ) + ) + ) + tags_to_delete = list(current_tags_set.difference(new_tags_set)) + tags_to_update = list(new_tags_set.difference(current_tags_set)) + if tags_to_delete: + tags_to_delete = make_tags_in_proper_format( + recreate_tags_from_list(tags_to_delete) + ) + delete_success, delete_msg = ( + tags_action( + client, stream_name, tags_to_delete, action='delete', + check_mode=check_mode + ) + ) + if not delete_success: + return delete_success, delete_msg + if tags_to_update: + tags = make_tags_in_proper_format( + recreate_tags_from_list(tags_to_update) + ) + else: + return True, 'Tags do not need to be updated' + + if tags: + create_success, create_msg = ( + tags_action( + client, stream_name, tags, action='create', + check_mode=check_mode + ) + ) + return create_success, create_msg + + return success, err_msg + +def stream_action(client, stream_name, shard_count=1, action='create', + timeout=300, check_mode=False): + """Create or Delete an Amazon Kinesis Stream. + Args: + client (botocore.client.EC2): Boto3 client. + stream_name (str): The name of the kinesis stream. + + Kwargs: + shard_count (int): Number of shards this stream will use. + action (str): The action to perform. + valid actions == create and delete + default=create + check_mode (bool): This will pass DryRun as one of the parameters to the aws api. + default=False + + Basic Usage: + >>> client = boto3.client('kinesis') + >>> stream_name = 'test-stream' + >>> shard_count = 20 + >>> stream_action(client, stream_name, shard_count, action='create') + + Returns: + List (bool, str) + """ + success = False + err_msg = '' + params = { + 'StreamName': stream_name + } + try: + if not check_mode: + if action == 'create': + params['ShardCount'] = shard_count + client.create_stream(**params) + success = True + elif action == 'delete': + client.delete_stream(**params) + success = True + else: + err_msg = 'Invalid action {0}'.format(action) + else: + if action == 'create': + success = True + elif action == 'delete': + success = True + else: + err_msg = 'Invalid action {0}'.format(action) + + except botocore.exceptions.ClientError, e: + err_msg = str(e) + + return success, err_msg + +def retention_action(client, stream_name, retention_period=24, + action='increase', check_mode=False): + """Increase or Decreaste the retention of messages in the Kinesis stream. + Args: + client (botocore.client.EC2): Boto3 client. + stream_name (str): The + + Kwargs: + retention_period (int): This is how long messages will be kept before + they are discarded. This can not be less than 24 hours. + action (str): The action to perform. + valid actions == create and delete + default=create + check_mode (bool): This will pass DryRun as one of the parameters to the aws api. + default=False + + Basic Usage: + >>> client = boto3.client('kinesis') + >>> stream_name = 'test-stream' + >>> retention_period = 48 + >>> stream_action(client, stream_name, retention_period, action='create') + + Returns: + Tuple (bool, str) + """ + success = False + err_msg = '' + params = { + 'StreamName': stream_name + } + try: + if not check_mode: + if action == 'increase': + params['RetentionPeriodHours'] = retention_period + client.increase_stream_retention_period(**params) + success = True + elif action == 'decrease': + params['RetentionPeriodHours'] = retention_period + client.decrease_stream_retention_period(**params) + success = True + else: + err_msg = 'Invalid action {0}'.format(action) + else: + if action == 'increase': + success = True + elif action == 'decrease': + success = True + else: + err_msg = 'Invalid action {0}'.format(action) + + except botocore.exceptions.ClientError, e: + err_msg = str(e) + + return success, err_msg + +def update(client, current_stream, stream_name, retention_period=None, + tags=None, wait=False, wait_timeout=300, check_mode=False): + """Update an Amazon Kinesis Stream. + Args: + client (botocore.client.EC2): Boto3 client. + stream_name (str): The name of the kinesis stream. + + Kwargs: + retention_period (int): This is how long messages will be kept before + they are discarded. This can not be less than 24 hours. + tags (dict): The tags you want applied. + wait (bool): Wait until Stream is ACTIVE. + default=False + wait_timeout (int): How long to wait until this operation is considered failed. + default=300 + check_mode (bool): This will pass DryRun as one of the parameters to the aws api. + default=False + + Basic Usage: + >>> client = boto3.client('kinesis') + >>> current_stream = { + 'HasMoreShards': True, + 'RetentionPeriodHours': 24, + 'StreamName': 'test-stream', + 'StreamARN': 'arn:aws:kinesis:us-west-2:123456789:stream/test-stream', + 'StreamStatus': "ACTIVE' + } + >>> stream_name = 'test-stream' + >>> retention_period = 48 + >>> stream_action(client, current_stream, stream_name, + retention_period, action='create' ) + + Returns: + Tuple (bool, bool, str, dict) + """ + success = False + changed = False + err_msg = '' + if retention_period: + if wait: + wait_success, wait_msg, current_stream = ( + wait_for_status( + client, stream_name, 'ACTIVE', wait_timeout, + check_mode=check_mode + ) + ) + if not wait_success: + return wait_success, True, wait_msg + + if current_stream['StreamStatus'] == 'ACTIVE': + if retention_period > current_stream['RetentionPeriodHours']: + retention_changed, retention_msg = ( + retention_action( + client, stream_name, retention_period, action='increase', + check_mode=check_mode + ) + ) + if retention_changed: + success = True + + elif retention_period < current_stream['RetentionPeriodHours']: + retention_changed, retention_msg = ( + retention_action( + client, stream_name, retention_period, action='decrease', + check_mode=check_mode + ) + ) + if retention_changed: + success = True + + elif retention_period == current_stream['RetentionPeriodHours']: + retention_changed = False + retention_msg = ( + 'Retention {0} is the same as {1}' + .format( + retention_period, + current_stream['RetentionPeriodHours'] + ) + ) + success = True + + changed = retention_changed + err_msg = retention_msg + if changed and wait: + wait_success, wait_msg, current_stream = ( + wait_for_status( + client, stream_name, 'ACTIVE', wait_timeout, + check_mode=check_mode + ) + ) + if not wait_success: + return wait_success, True, wait_msg + elif changed and not wait: + stream_found, stream_msg, current_stream = ( + find_stream(client, stream_name, check_mode=check_mode) + ) + if stream_found: + if current_stream['StreamStatus'] != 'ACTIVE': + err_msg = ( + 'Retention Period for {0} is in the process of updating' + .format(stream_name) + ) + return success, changed, err_msg + else: + err_msg = ( + 'StreamStatus has to be ACTIVE in order to modify the retention period. Current status is {0}' + .format(current_stream['StreamStatus']) + ) + return success, changed, err_msg + + if tags: + changed, err_msg = update_tags(client, stream_name, tags, check_mode) + if changed: + success = True + if wait: + success, err_msg, _ = ( + wait_for_status( + client, stream_name, 'ACTIVE', wait_timeout, + check_mode=check_mode + ) + ) + if success and changed: + err_msg = 'Kinesis Stream {0} updated successfully'.format(stream_name) + elif success and not changed: + err_msg = 'Kinesis Stream {0} did not changed'.format(stream_name) + + return success, changed, err_msg + +def create_stream(client, stream_name, number_of_shards=1, retention_period=None, + tags=None, wait=False, wait_timeout=300, check_mode=None): + """Create an Amazon Kinesis Stream. + Args: + client (botocore.client.EC2): Boto3 client. + stream_name (str): The name of the kinesis stream. + + Kwargs: + number_of_shards (int): Number of shards this stream will use. + default=1 + retention_period (int): Can not be less than 24 hours + default=None + tags (dict): The tags you want applied. + default=None + wait (bool): Wait until Stream is ACTIVE. + default=False + wait_timeout (int): How long to wait until this operation is considered failed. + default=300 + check_mode (bool): This will pass DryRun as one of the parameters to the aws api. + default=False + + Basic Usage: + >>> client = boto3.client('kinesis') + >>> stream_name = 'test-stream' + >>> number_of_shards = 10 + >>> tags = {'env': 'test'} + >>> create_stream(client, stream_name, number_of_shards, tags=tags) + + Returns: + Tuple (bool, bool, str, dict) + """ + success = False + changed = False + err_msg = '' + results = dict() + + stream_found, stream_msg, current_stream = ( + find_stream(client, stream_name, check_mode=check_mode) + ) + if stream_found and current_stream['StreamStatus'] == 'DELETING' and wait: + wait_success, wait_msg, current_stream = ( + wait_for_status( + client, stream_name, 'ACTIVE', wait_timeout, check_mode + ) + ) + if stream_found and current_stream['StreamStatus'] != 'DELETING': + success, changed, err_msg = update( + client, current_stream, stream_name, retention_period, tags, + wait, wait_timeout, check_mode + ) + else: + create_success, create_msg = ( + stream_action( + client, stream_name, number_of_shards, action='create', + check_mode=check_mode + ) + ) + if create_success: + changed = True + if wait: + wait_success, wait_msg, results = ( + wait_for_status( + client, stream_name, 'ACTIVE', wait_timeout, check_mode + ) + ) + err_msg = ( + 'Kinesis Stream {0} is in the process of being created' + .format(stream_name) + ) + if not wait_success: + return wait_success, True, wait_msg, results + else: + err_msg = ( + 'Kinesis Stream {0} created successfully' + .format(stream_name) + ) + + if tags: + changed, err_msg = ( + tags_action( + client, stream_name, tags, action='create', + check_mode=check_mode + ) + ) + if changed: + success = True + if not success: + return success, changed, err_msg, results + + stream_found, stream_msg, current_stream = ( + find_stream(client, stream_name, check_mode=check_mode) + ) + if retention_period and current_stream['StreamStatus'] == 'ACTIVE': + changed, err_msg = ( + retention_action( + client, stream_name, retention_period, action='increase', + check_mode=check_mode + ) + ) + if changed: + success = True + if not success: + return success, changed, err_msg, results + else: + err_msg = ( + 'StreamStatus has to be ACTIVE in order to modify the retention period. Current status is {0}' + .format(current_stream['StreamStatus']) + ) + success = create_success + changed = True + + if success: + _, _, results = ( + find_stream(client, stream_name, check_mode=check_mode) + ) + _, _, current_tags = ( + get_tags(client, stream_name, check_mode=check_mode) + ) + if current_tags and not check_mode: + current_tags = make_tags_in_proper_format(current_tags) + results['Tags'] = current_tags + elif check_mode and tags: + results['Tags'] = tags + else: + results['Tags'] = dict() + results = convert_to_lower(results) + + return success, changed, err_msg, results + +def delete_stream(client, stream_name, wait=False, wait_timeout=300, + check_mode=False): + """Delete an Amazon Kinesis Stream. + Args: + client (botocore.client.EC2): Boto3 client. + stream_name (str): The name of the kinesis stream. + + Kwargs: + wait (bool): Wait until Stream is ACTIVE. + default=False + wait_timeout (int): How long to wait until this operation is considered failed. + default=300 + check_mode (bool): This will pass DryRun as one of the parameters to the aws api. + default=False + + Basic Usage: + >>> client = boto3.client('kinesis') + >>> stream_name = 'test-stream' + >>> delete_stream(client, stream_name) + + Returns: + Tuple (bool, bool, str, dict) + """ + success = False + changed = False + err_msg = '' + results = dict() + stream_found, stream_msg, current_stream = find_stream(client, stream_name) + if stream_found: + success, err_msg = ( + stream_action( + client, stream_name, action='delete', check_mode=check_mode + ) + ) + if success: + changed = True + if wait: + success, err_msg, results = ( + wait_for_status( + client, stream_name, 'DELETING', wait_timeout, + check_mode + ) + ) + err_msg = 'Stream {0} deleted successfully'.format(stream_name) + if not success: + return success, True, err_msg, results + else: + err_msg = ( + 'Stream {0} is in the process of being deleted' + .format(stream_name) + ) + else: + success = True + changed = False + err_msg = 'Stream {0} does not exist'.format(stream_name) + + return success, changed, err_msg, results + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + name = dict(default=None, required=True), + shards = dict(default=None, required=False, type='int'), + retention_period = dict(default=None, required=False, type='int'), + tags = dict(default=None, required=False, type='dict', aliases=['resource_tags']), + wait = dict(default=True, required=False, type='bool'), + wait_timeout = dict(default=300, required=False, type='int'), + state = dict(default='present', choices=['present', 'absent']), + ) + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + retention_period = module.params.get('retention_period') + stream_name = module.params.get('name') + shards = module.params.get('shards') + state = module.params.get('state') + tags = module.params.get('tags') + wait = module.params.get('wait') + wait_timeout = module.params.get('wait_timeout') + + if state == 'present' and not shards: + module.fail_json(msg='shards is required when state == present.') + + if not HAS_BOTO3: + module.fail_json(msg='boto3 is required.') + + check_mode = module.check_mode + try: + region, ec2_url, aws_connect_kwargs = ( + get_aws_connection_info(module, boto3=True) + ) + client = ( + boto3_conn( + module, conn_type='client', resource='kinesis', + region=region, endpoint=ec2_url, **aws_connect_kwargs + ) + ) + except botocore.exceptions.ClientError, e: + err_msg = 'Boto3 Client Error - {0}'.format(str(e.msg)) + module.fail_json( + success=False, changed=False, result={}, msg=err_msg + ) + + if state == 'present': + success, changed, err_msg, results = ( + create_stream( + client, stream_name, shards, retention_period, tags, + wait, wait_timeout, check_mode + ) + ) + elif state == 'absent': + success, changed, err_msg, results = ( + delete_stream(client, stream_name, wait, wait_timeout, check_mode) + ) + + if success: + module.exit_json( + success=success, changed=changed, msg=err_msg, **results + ) + else: + module.fail_json( + success=success, changed=changed, msg=err_msg, result=results + ) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From 514e285d1af7e10252e657c9497816e90d86475f Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Wed, 23 Mar 2016 19:21:24 -0700 Subject: [PATCH 1825/2522] update doc string --- cloud/amazon/kinesis_stream.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud/amazon/kinesis_stream.py b/cloud/amazon/kinesis_stream.py index cc280655a52..adc1fbe8c80 100644 --- a/cloud/amazon/kinesis_stream.py +++ b/cloud/amazon/kinesis_stream.py @@ -109,6 +109,7 @@ - name: Delete Kinesis Stream test-stream and wait for it to finish deleting. kinesis_stream: name: test-stream + state: absent wait: yes wait_timeout: 600 register: test_stream From 988f468457978011ceee6d545fba0a23cbd2943d Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Thu, 24 Mar 2016 13:33:19 -0700 Subject: [PATCH 1826/2522] Added test to kinesis_stream module. * Update kinesis_stream based on tests. * Added tests for kinesis_stream. --- cloud/amazon/kinesis_stream.py | 55 ++-- cloud/amazon/test_kinesis_stream.py | 471 ++++++++++++++++++++++++++++ 2 files changed, 497 insertions(+), 29 deletions(-) create mode 100644 cloud/amazon/test_kinesis_stream.py diff --git a/cloud/amazon/kinesis_stream.py b/cloud/amazon/kinesis_stream.py index adc1fbe8c80..222a50dd0b3 100644 --- a/cloud/amazon/kinesis_stream.py +++ b/cloud/amazon/kinesis_stream.py @@ -201,7 +201,7 @@ def make_tags_in_proper_format(tags): tags (list): The tags you want applied. Basic Usage: - >>> tags = [{u'Key': 'env', u'Value': 'development'}] + >>> tags = [{'Key': 'env', 'Value': 'development'}] >>> make_tags_in_proper_format(tags) { "env": "development", @@ -327,7 +327,7 @@ def find_stream(client, stream_name, limit=1, check_mode=False): 'RetentionPeriodHours': 24, 'StreamName': stream_name, 'StreamARN': 'arn:aws:kinesis:east-side:123456789:stream/{0}'.format(stream_name), - 'StreamStatus': u'ACTIVE' + 'StreamStatus': 'ACTIVE' } success = True except botocore.exceptions.ClientError, e: @@ -363,31 +363,24 @@ def wait_for_status(client, stream_name, status, wait_timeout=300, stream = dict() err_msg = "" - if not check_mode: - while wait_timeout > time.time(): - try: - find_success, find_msg, stream = ( - find_stream(client, stream_name) - ) - if status != 'DELETING': - if find_success and stream: - if stream.get('StreamStatus') == status: - status_achieved = True - break - elif status == 'DELETING': - if not find_success: + while wait_timeout > time.time(): + try: + find_success, find_msg, stream = ( + find_stream(client, stream_name, check_mode=check_mode) + ) + if status != 'DELETING': + if find_success and stream: + if stream.get('StreamStatus') == status: status_achieved = True break - else: - time.sleep(polling_increment_secs) - except botocore.exceptions.ClientError as e: - err_msg = str(e) - - else: - status_achieved = True - find_success, find_msg, stream = ( - find_stream(client, stream_name, check_mode=check_mode) - ) + elif status == 'DELETING': + if not find_success: + status_achieved = True + break + else: + time.sleep(polling_increment_secs) + except botocore.exceptions.ClientError as e: + err_msg = str(e) if not status_achieved: err_msg = "Wait time out reached, while waiting for results" @@ -694,7 +687,7 @@ def update(client, current_stream, stream_name, retention_period=None, retention_period, action='create' ) Returns: - Tuple (bool, bool, str, dict) + Tuple (bool, bool, str) """ success = False changed = False @@ -783,9 +776,9 @@ def update(client, current_stream, stream_name, retention_period=None, ) ) if success and changed: - err_msg = 'Kinesis Stream {0} updated successfully'.format(stream_name) + err_msg = 'Kinesis Stream {0} updated successfully.'.format(stream_name) elif success and not changed: - err_msg = 'Kinesis Stream {0} did not changed'.format(stream_name) + err_msg = 'Kinesis Stream {0} did not changed.'.format(stream_name) return success, changed, err_msg @@ -1003,7 +996,11 @@ def main(): wait_timeout = module.params.get('wait_timeout') if state == 'present' and not shards: - module.fail_json(msg='shards is required when state == present.') + module.fail_json(msg='Shards is required when state == present.') + + if retention_period: + if retention_period < 24: + module.fail_json(msg='Retention period can not be less than 24 hours.') if not HAS_BOTO3: module.fail_json(msg='boto3 is required.') diff --git a/cloud/amazon/test_kinesis_stream.py b/cloud/amazon/test_kinesis_stream.py new file mode 100644 index 00000000000..5b8fefd40db --- /dev/null +++ b/cloud/amazon/test_kinesis_stream.py @@ -0,0 +1,471 @@ +#!/usr/bin/python + +import unittest + +from collections import namedtuple +from ansible.parsing.dataloader import DataLoader +from ansible.vars import VariableManager +from ansible.inventory import Inventory +from ansible.playbook.play import Play +from ansible.executor.task_queue_manager import TaskQueueManager + +import kinesis_stream +import boto3 + +Options = ( + namedtuple( + 'Options', [ + 'connection', 'module_path', 'forks', 'become', 'become_method', + 'become_user', 'remote_user', 'private_key_file', 'ssh_common_args', + 'sftp_extra_args', 'scp_extra_args', 'ssh_extra_args', 'verbosity', + 'check' + ] + ) +) +# initialize needed objects +variable_manager = VariableManager() +loader = DataLoader() +options = ( + Options( + connection='local', + module_path='./', + forks=1, become=None, become_method=None, become_user=None, check=True, + remote_user=None, private_key_file=None, ssh_common_args=None, + sftp_extra_args=None, scp_extra_args=None, ssh_extra_args=None, + verbosity=10 + ) +) +passwords = dict(vault_pass='') + +# create inventory and pass to var manager +inventory = Inventory(loader=loader, variable_manager=variable_manager, host_list='localhost') +variable_manager.set_inventory(inventory) + +def run(play): + tqm = None + results = None + try: + tqm = TaskQueueManager( + inventory=inventory, + variable_manager=variable_manager, + loader=loader, + options=options, + passwords=passwords, + stdout_callback='default', + ) + results = tqm.run(play) + finally: + if tqm is not None: + tqm.cleanup() + return tqm, results + +class AnsibleKinesisStreamTasks(unittest.TestCase): + + def test_a_create_stream_1(self): + play_source = dict( + name = "Create Kinesis Stream with 10 Shards", + hosts = 'localhost', + gather_facts = 'no', + tasks = [ + dict( + action=dict( + module='kinesis_stream', + name='stream-test', + shards=10, + wait='yes' + ), + register='stream' + ) + ] + ) + play = Play().load(play_source, variable_manager=variable_manager, loader=loader) + tqm, results = run(play) + self.failUnless(tqm._stats.ok['localhost'] == 1) + + def test_a_create_stream_2(self): + play_source = dict( + name = "Create Kinesis Stream with 10 Shards and create a tag called environment", + hosts = 'localhost', + gather_facts = 'no', + tasks = [ + dict( + action=dict( + module='kinesis_stream', + name='stream-test', + shards=10, + tags=dict( + env='development' + ), + wait='yes' + ), + register='stream' + ) + ] + ) + play = Play().load(play_source, variable_manager=variable_manager, loader=loader) + tqm, results = run(play) + self.failUnless(tqm._stats.ok['localhost'] == 1) + + def test_a_create_stream_3(self): + play_source = dict( + name = "Create Kinesis Stream with 10 Shards and create a tag called environment and Change the default retention period from 24 to 48", + hosts = 'localhost', + gather_facts = 'no', + tasks = [ + dict( + action=dict( + module='kinesis_stream', + name='stream-test', + retention_period=48, + shards=10, + tags=dict( + env='development' + ), + wait='yes' + ), + register='stream' + ) + ] + ) + play = Play().load(play_source, variable_manager=variable_manager, loader=loader) + tqm, results = run(play) + self.failUnless(tqm._stats.ok['localhost'] == 1) + + def test_b_create_stream_1(self): + play_source = dict( + name = "Create Kinesis Stream with out specifying the number of shards", + hosts = 'localhost', + gather_facts = 'no', + tasks = [ + dict( + action=dict( + module='kinesis_stream', + name='stream-test', + wait='yes' + ), + register='stream' + ) + ] + ) + play = Play().load(play_source, variable_manager=variable_manager, loader=loader) + tqm, results = run(play) + self.failUnless(tqm._stats.failures['localhost'] == 1) + + def test_b_create_stream_2(self): + play_source = dict( + name = "Create Kinesis Stream with specifying the retention period less than 24 hours", + hosts = 'localhost', + gather_facts = 'no', + tasks = [ + dict( + action=dict( + module='kinesis_stream', + name='stream-test', + retention_period=23, + shards=10, + wait='yes' + ), + register='stream' + ) + ] + ) + play = Play().load(play_source, variable_manager=variable_manager, loader=loader) + tqm, results = run(play) + self.failUnless(tqm._stats.failures['localhost'] == 1) + + def test_c_delete_stream_(self): + play_source = dict( + name = "Delete Kinesis Stream test-stream", + hosts = 'localhost', + gather_facts = 'no', + tasks = [ + dict( + action=dict( + module='kinesis_stream', + name='stream-test', + state='absent', + wait='yes' + ) + ) + ] + ) + play = Play().load(play_source, variable_manager=variable_manager, loader=loader) + tqm, results = run(play) + self.failUnless(tqm._stats.ok['localhost'] == 1) + + +class AnsibleKinesisStreamFunctions(unittest.TestCase): + + def test_convert_to_lower(self): + example = { + 'HasMoreShards': True, + 'RetentionPeriodHours': 24, + 'StreamName': 'test', + 'StreamARN': 'arn:aws:kinesis:east-side:123456789:stream/test', + 'StreamStatus': 'ACTIVE' + } + converted_example = kinesis_stream.convert_to_lower(example) + keys = converted_example.keys() + keys.sort() + for i in range(len(keys)): + if i == 0: + self.assertEqual(keys[i], 'has_more_shards') + if i == 1: + self.assertEqual(keys[i], 'retention_period_hours') + if i == 2: + self.assertEqual(keys[i], 'stream_arn') + if i == 3: + self.assertEqual(keys[i], 'stream_name') + if i == 4: + self.assertEqual(keys[i], 'stream_status') + + def test_make_tags_in_aws_format(self): + example = { + 'env': 'development' + } + should_return = [ + { + 'Key': 'env', + 'Value': 'development' + } + ] + aws_tags = kinesis_stream.make_tags_in_aws_format(example) + self.assertEqual(aws_tags, should_return) + + def test_make_tags_in_proper_format(self): + example = [ + { + 'Key': 'env', + 'Value': 'development' + }, + { + 'Key': 'service', + 'Value': 'web' + } + ] + should_return = { + 'env': 'development', + 'service': 'web' + } + proper_tags = kinesis_stream.make_tags_in_proper_format(example) + self.assertEqual(proper_tags, should_return) + + def test_recreate_tags_from_list(self): + example = [('environment', 'development'), ('service', 'web')] + should_return = [ + { + 'Key': 'environment', + 'Value': 'development' + }, + { + 'Key': 'service', + 'Value': 'web' + } + ] + aws_tags = kinesis_stream.recreate_tags_from_list(example) + self.assertEqual(aws_tags, should_return) + + def test_get_tags(self): + client = boto3.client('kinesis', region_name='us-west-2') + success, err_msg, tags = kinesis_stream.get_tags(client, 'test', True) + self.assertTrue(success) + should_return = [ + { + 'Key': 'DryRunMode', + 'Value': 'true' + } + ] + self.assertEqual(tags, should_return) + + def test_find_stream(self): + client = boto3.client('kinesis', region_name='us-west-2') + success, err_msg, stream = ( + kinesis_stream.find_stream(client, 'test', check_mode=True) + ) + should_return = { + 'HasMoreShards': True, + 'RetentionPeriodHours': 24, + 'StreamName': 'test', + 'StreamARN': 'arn:aws:kinesis:east-side:123456789:stream/test', + 'StreamStatus': 'ACTIVE' + } + self.assertTrue(success) + self.assertEqual(stream, should_return) + + def test_wait_for_status(self): + client = boto3.client('kinesis', region_name='us-west-2') + success, err_msg, stream = ( + kinesis_stream.wait_for_status( + client, 'test', 'ACTIVE', check_mode=True + ) + ) + should_return = { + 'HasMoreShards': True, + 'RetentionPeriodHours': 24, + 'StreamName': 'test', + 'StreamARN': 'arn:aws:kinesis:east-side:123456789:stream/test', + 'StreamStatus': 'ACTIVE' + } + self.assertTrue(success) + self.assertEqual(stream, should_return) + + def test_tags_action_create(self): + client = boto3.client('kinesis', region_name='us-west-2') + tags = { + 'env': 'development', + 'service': 'web' + } + success, err_msg = ( + kinesis_stream.tags_action( + client, 'test', tags, 'create', check_mode=True + ) + ) + self.assertTrue(success) + + def test_tags_action_delete(self): + client = boto3.client('kinesis', region_name='us-west-2') + tags = { + 'env': 'development', + 'service': 'web' + } + success, err_msg = ( + kinesis_stream.tags_action( + client, 'test', tags, 'delete', check_mode=True + ) + ) + self.assertTrue(success) + + def test_tags_action_invalid(self): + client = boto3.client('kinesis', region_name='us-west-2') + tags = { + 'env': 'development', + 'service': 'web' + } + success, err_msg = ( + kinesis_stream.tags_action( + client, 'test', tags, 'append', check_mode=True + ) + ) + self.assertFalse(success) + + def test_update_tags(self): + client = boto3.client('kinesis', region_name='us-west-2') + tags = { + 'env': 'development', + 'service': 'web' + } + success, err_msg = ( + kinesis_stream.update_tags( + client, 'test', tags, check_mode=True + ) + ) + self.assertTrue(success) + + def test_stream_action_create(self): + client = boto3.client('kinesis', region_name='us-west-2') + success, err_msg = ( + kinesis_stream.stream_action( + client, 'test', 10, 'create', check_mode=True + ) + ) + self.assertTrue(success) + + def test_stream_action_delete(self): + client = boto3.client('kinesis', region_name='us-west-2') + success, err_msg = ( + kinesis_stream.stream_action( + client, 'test', 10, 'delete', check_mode=True + ) + ) + self.assertTrue(success) + + def test_stream_action_invalid(self): + client = boto3.client('kinesis', region_name='us-west-2') + success, err_msg = ( + kinesis_stream.stream_action( + client, 'test', 10, 'append', check_mode=True + ) + ) + self.assertFalse(success) + + def test_retention_action_increase(self): + client = boto3.client('kinesis', region_name='us-west-2') + success, err_msg = ( + kinesis_stream.retention_action( + client, 'test', 48, 'increase', check_mode=True + ) + ) + self.assertTrue(success) + + def test_retention_action_decrease(self): + client = boto3.client('kinesis', region_name='us-west-2') + success, err_msg = ( + kinesis_stream.retention_action( + client, 'test', 24, 'decrease', check_mode=True + ) + ) + self.assertTrue(success) + + def test_retention_action_invalid(self): + client = boto3.client('kinesis', region_name='us-west-2') + success, err_msg = ( + kinesis_stream.retention_action( + client, 'test', 24, 'create', check_mode=True + ) + ) + self.assertFalse(success) + + def test_update(self): + client = boto3.client('kinesis', region_name='us-west-2') + current_stream = { + 'HasMoreShards': True, + 'RetentionPeriodHours': 24, + 'StreamName': 'test', + 'StreamARN': 'arn:aws:kinesis:east-side:123456789:stream/test', + 'StreamStatus': 'ACTIVE' + } + tags = { + 'env': 'development', + 'service': 'web' + } + success, changed, err_msg = ( + kinesis_stream.update( + client, current_stream, 'test', retention_period=48, + tags=tags, check_mode=True + ) + ) + self.assertTrue(success) + self.assertTrue(changed) + self.assertEqual(err_msg, 'Kinesis Stream test updated successfully.') + + def test_create_stream(self): + client = boto3.client('kinesis', region_name='us-west-2') + tags = { + 'env': 'development', + 'service': 'web' + } + success, changed, err_msg, results = ( + kinesis_stream.create_stream( + client, 'test', number_of_shards=10, retention_period=48, + tags=tags, check_mode=True + ) + ) + should_return = { + 'has_more_shards': True, + 'retention_period_hours': 24, + 'stream_name': 'test', + 'stream_arn': 'arn:aws:kinesis:east-side:123456789:stream/test', + 'stream_status': 'ACTIVE', + 'tags': tags, + } + self.assertTrue(success) + self.assertTrue(changed) + self.assertEqual(results, should_return) + self.assertEqual(err_msg, 'Kinesis Stream test updated successfully.') + + +def main(): + unittest.main() + +if __name__ == '__main__': + main() From 8c1277a8b78f2136f0b1312c1068d237d91d2816 Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Thu, 24 Mar 2016 22:20:58 -0700 Subject: [PATCH 1827/2522] Removed test as they will not be ran by Ansible. * I will include tests in my personal repo which will contain all modules written by me with their associated tests. --- cloud/amazon/test_kinesis_stream.py | 471 ---------------------------- 1 file changed, 471 deletions(-) delete mode 100644 cloud/amazon/test_kinesis_stream.py diff --git a/cloud/amazon/test_kinesis_stream.py b/cloud/amazon/test_kinesis_stream.py deleted file mode 100644 index 5b8fefd40db..00000000000 --- a/cloud/amazon/test_kinesis_stream.py +++ /dev/null @@ -1,471 +0,0 @@ -#!/usr/bin/python - -import unittest - -from collections import namedtuple -from ansible.parsing.dataloader import DataLoader -from ansible.vars import VariableManager -from ansible.inventory import Inventory -from ansible.playbook.play import Play -from ansible.executor.task_queue_manager import TaskQueueManager - -import kinesis_stream -import boto3 - -Options = ( - namedtuple( - 'Options', [ - 'connection', 'module_path', 'forks', 'become', 'become_method', - 'become_user', 'remote_user', 'private_key_file', 'ssh_common_args', - 'sftp_extra_args', 'scp_extra_args', 'ssh_extra_args', 'verbosity', - 'check' - ] - ) -) -# initialize needed objects -variable_manager = VariableManager() -loader = DataLoader() -options = ( - Options( - connection='local', - module_path='./', - forks=1, become=None, become_method=None, become_user=None, check=True, - remote_user=None, private_key_file=None, ssh_common_args=None, - sftp_extra_args=None, scp_extra_args=None, ssh_extra_args=None, - verbosity=10 - ) -) -passwords = dict(vault_pass='') - -# create inventory and pass to var manager -inventory = Inventory(loader=loader, variable_manager=variable_manager, host_list='localhost') -variable_manager.set_inventory(inventory) - -def run(play): - tqm = None - results = None - try: - tqm = TaskQueueManager( - inventory=inventory, - variable_manager=variable_manager, - loader=loader, - options=options, - passwords=passwords, - stdout_callback='default', - ) - results = tqm.run(play) - finally: - if tqm is not None: - tqm.cleanup() - return tqm, results - -class AnsibleKinesisStreamTasks(unittest.TestCase): - - def test_a_create_stream_1(self): - play_source = dict( - name = "Create Kinesis Stream with 10 Shards", - hosts = 'localhost', - gather_facts = 'no', - tasks = [ - dict( - action=dict( - module='kinesis_stream', - name='stream-test', - shards=10, - wait='yes' - ), - register='stream' - ) - ] - ) - play = Play().load(play_source, variable_manager=variable_manager, loader=loader) - tqm, results = run(play) - self.failUnless(tqm._stats.ok['localhost'] == 1) - - def test_a_create_stream_2(self): - play_source = dict( - name = "Create Kinesis Stream with 10 Shards and create a tag called environment", - hosts = 'localhost', - gather_facts = 'no', - tasks = [ - dict( - action=dict( - module='kinesis_stream', - name='stream-test', - shards=10, - tags=dict( - env='development' - ), - wait='yes' - ), - register='stream' - ) - ] - ) - play = Play().load(play_source, variable_manager=variable_manager, loader=loader) - tqm, results = run(play) - self.failUnless(tqm._stats.ok['localhost'] == 1) - - def test_a_create_stream_3(self): - play_source = dict( - name = "Create Kinesis Stream with 10 Shards and create a tag called environment and Change the default retention period from 24 to 48", - hosts = 'localhost', - gather_facts = 'no', - tasks = [ - dict( - action=dict( - module='kinesis_stream', - name='stream-test', - retention_period=48, - shards=10, - tags=dict( - env='development' - ), - wait='yes' - ), - register='stream' - ) - ] - ) - play = Play().load(play_source, variable_manager=variable_manager, loader=loader) - tqm, results = run(play) - self.failUnless(tqm._stats.ok['localhost'] == 1) - - def test_b_create_stream_1(self): - play_source = dict( - name = "Create Kinesis Stream with out specifying the number of shards", - hosts = 'localhost', - gather_facts = 'no', - tasks = [ - dict( - action=dict( - module='kinesis_stream', - name='stream-test', - wait='yes' - ), - register='stream' - ) - ] - ) - play = Play().load(play_source, variable_manager=variable_manager, loader=loader) - tqm, results = run(play) - self.failUnless(tqm._stats.failures['localhost'] == 1) - - def test_b_create_stream_2(self): - play_source = dict( - name = "Create Kinesis Stream with specifying the retention period less than 24 hours", - hosts = 'localhost', - gather_facts = 'no', - tasks = [ - dict( - action=dict( - module='kinesis_stream', - name='stream-test', - retention_period=23, - shards=10, - wait='yes' - ), - register='stream' - ) - ] - ) - play = Play().load(play_source, variable_manager=variable_manager, loader=loader) - tqm, results = run(play) - self.failUnless(tqm._stats.failures['localhost'] == 1) - - def test_c_delete_stream_(self): - play_source = dict( - name = "Delete Kinesis Stream test-stream", - hosts = 'localhost', - gather_facts = 'no', - tasks = [ - dict( - action=dict( - module='kinesis_stream', - name='stream-test', - state='absent', - wait='yes' - ) - ) - ] - ) - play = Play().load(play_source, variable_manager=variable_manager, loader=loader) - tqm, results = run(play) - self.failUnless(tqm._stats.ok['localhost'] == 1) - - -class AnsibleKinesisStreamFunctions(unittest.TestCase): - - def test_convert_to_lower(self): - example = { - 'HasMoreShards': True, - 'RetentionPeriodHours': 24, - 'StreamName': 'test', - 'StreamARN': 'arn:aws:kinesis:east-side:123456789:stream/test', - 'StreamStatus': 'ACTIVE' - } - converted_example = kinesis_stream.convert_to_lower(example) - keys = converted_example.keys() - keys.sort() - for i in range(len(keys)): - if i == 0: - self.assertEqual(keys[i], 'has_more_shards') - if i == 1: - self.assertEqual(keys[i], 'retention_period_hours') - if i == 2: - self.assertEqual(keys[i], 'stream_arn') - if i == 3: - self.assertEqual(keys[i], 'stream_name') - if i == 4: - self.assertEqual(keys[i], 'stream_status') - - def test_make_tags_in_aws_format(self): - example = { - 'env': 'development' - } - should_return = [ - { - 'Key': 'env', - 'Value': 'development' - } - ] - aws_tags = kinesis_stream.make_tags_in_aws_format(example) - self.assertEqual(aws_tags, should_return) - - def test_make_tags_in_proper_format(self): - example = [ - { - 'Key': 'env', - 'Value': 'development' - }, - { - 'Key': 'service', - 'Value': 'web' - } - ] - should_return = { - 'env': 'development', - 'service': 'web' - } - proper_tags = kinesis_stream.make_tags_in_proper_format(example) - self.assertEqual(proper_tags, should_return) - - def test_recreate_tags_from_list(self): - example = [('environment', 'development'), ('service', 'web')] - should_return = [ - { - 'Key': 'environment', - 'Value': 'development' - }, - { - 'Key': 'service', - 'Value': 'web' - } - ] - aws_tags = kinesis_stream.recreate_tags_from_list(example) - self.assertEqual(aws_tags, should_return) - - def test_get_tags(self): - client = boto3.client('kinesis', region_name='us-west-2') - success, err_msg, tags = kinesis_stream.get_tags(client, 'test', True) - self.assertTrue(success) - should_return = [ - { - 'Key': 'DryRunMode', - 'Value': 'true' - } - ] - self.assertEqual(tags, should_return) - - def test_find_stream(self): - client = boto3.client('kinesis', region_name='us-west-2') - success, err_msg, stream = ( - kinesis_stream.find_stream(client, 'test', check_mode=True) - ) - should_return = { - 'HasMoreShards': True, - 'RetentionPeriodHours': 24, - 'StreamName': 'test', - 'StreamARN': 'arn:aws:kinesis:east-side:123456789:stream/test', - 'StreamStatus': 'ACTIVE' - } - self.assertTrue(success) - self.assertEqual(stream, should_return) - - def test_wait_for_status(self): - client = boto3.client('kinesis', region_name='us-west-2') - success, err_msg, stream = ( - kinesis_stream.wait_for_status( - client, 'test', 'ACTIVE', check_mode=True - ) - ) - should_return = { - 'HasMoreShards': True, - 'RetentionPeriodHours': 24, - 'StreamName': 'test', - 'StreamARN': 'arn:aws:kinesis:east-side:123456789:stream/test', - 'StreamStatus': 'ACTIVE' - } - self.assertTrue(success) - self.assertEqual(stream, should_return) - - def test_tags_action_create(self): - client = boto3.client('kinesis', region_name='us-west-2') - tags = { - 'env': 'development', - 'service': 'web' - } - success, err_msg = ( - kinesis_stream.tags_action( - client, 'test', tags, 'create', check_mode=True - ) - ) - self.assertTrue(success) - - def test_tags_action_delete(self): - client = boto3.client('kinesis', region_name='us-west-2') - tags = { - 'env': 'development', - 'service': 'web' - } - success, err_msg = ( - kinesis_stream.tags_action( - client, 'test', tags, 'delete', check_mode=True - ) - ) - self.assertTrue(success) - - def test_tags_action_invalid(self): - client = boto3.client('kinesis', region_name='us-west-2') - tags = { - 'env': 'development', - 'service': 'web' - } - success, err_msg = ( - kinesis_stream.tags_action( - client, 'test', tags, 'append', check_mode=True - ) - ) - self.assertFalse(success) - - def test_update_tags(self): - client = boto3.client('kinesis', region_name='us-west-2') - tags = { - 'env': 'development', - 'service': 'web' - } - success, err_msg = ( - kinesis_stream.update_tags( - client, 'test', tags, check_mode=True - ) - ) - self.assertTrue(success) - - def test_stream_action_create(self): - client = boto3.client('kinesis', region_name='us-west-2') - success, err_msg = ( - kinesis_stream.stream_action( - client, 'test', 10, 'create', check_mode=True - ) - ) - self.assertTrue(success) - - def test_stream_action_delete(self): - client = boto3.client('kinesis', region_name='us-west-2') - success, err_msg = ( - kinesis_stream.stream_action( - client, 'test', 10, 'delete', check_mode=True - ) - ) - self.assertTrue(success) - - def test_stream_action_invalid(self): - client = boto3.client('kinesis', region_name='us-west-2') - success, err_msg = ( - kinesis_stream.stream_action( - client, 'test', 10, 'append', check_mode=True - ) - ) - self.assertFalse(success) - - def test_retention_action_increase(self): - client = boto3.client('kinesis', region_name='us-west-2') - success, err_msg = ( - kinesis_stream.retention_action( - client, 'test', 48, 'increase', check_mode=True - ) - ) - self.assertTrue(success) - - def test_retention_action_decrease(self): - client = boto3.client('kinesis', region_name='us-west-2') - success, err_msg = ( - kinesis_stream.retention_action( - client, 'test', 24, 'decrease', check_mode=True - ) - ) - self.assertTrue(success) - - def test_retention_action_invalid(self): - client = boto3.client('kinesis', region_name='us-west-2') - success, err_msg = ( - kinesis_stream.retention_action( - client, 'test', 24, 'create', check_mode=True - ) - ) - self.assertFalse(success) - - def test_update(self): - client = boto3.client('kinesis', region_name='us-west-2') - current_stream = { - 'HasMoreShards': True, - 'RetentionPeriodHours': 24, - 'StreamName': 'test', - 'StreamARN': 'arn:aws:kinesis:east-side:123456789:stream/test', - 'StreamStatus': 'ACTIVE' - } - tags = { - 'env': 'development', - 'service': 'web' - } - success, changed, err_msg = ( - kinesis_stream.update( - client, current_stream, 'test', retention_period=48, - tags=tags, check_mode=True - ) - ) - self.assertTrue(success) - self.assertTrue(changed) - self.assertEqual(err_msg, 'Kinesis Stream test updated successfully.') - - def test_create_stream(self): - client = boto3.client('kinesis', region_name='us-west-2') - tags = { - 'env': 'development', - 'service': 'web' - } - success, changed, err_msg, results = ( - kinesis_stream.create_stream( - client, 'test', number_of_shards=10, retention_period=48, - tags=tags, check_mode=True - ) - ) - should_return = { - 'has_more_shards': True, - 'retention_period_hours': 24, - 'stream_name': 'test', - 'stream_arn': 'arn:aws:kinesis:east-side:123456789:stream/test', - 'stream_status': 'ACTIVE', - 'tags': tags, - } - self.assertTrue(success) - self.assertTrue(changed) - self.assertEqual(results, should_return) - self.assertEqual(err_msg, 'Kinesis Stream test updated successfully.') - - -def main(): - unittest.main() - -if __name__ == '__main__': - main() From 72988aab146292e373e7a631343230f5f9964a74 Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Fri, 25 Mar 2016 12:38:10 -0700 Subject: [PATCH 1828/2522] updated module to accept check_mode in every boto call --- cloud/amazon/kinesis_stream.py | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/cloud/amazon/kinesis_stream.py b/cloud/amazon/kinesis_stream.py index 222a50dd0b3..6fbd1ab5402 100644 --- a/cloud/amazon/kinesis_stream.py +++ b/cloud/amazon/kinesis_stream.py @@ -368,15 +368,21 @@ def wait_for_status(client, stream_name, status, wait_timeout=300, find_success, find_msg, stream = ( find_stream(client, stream_name, check_mode=check_mode) ) - if status != 'DELETING': + if check_mode: + status_achieved = True + break + + elif status != 'DELETING': if find_success and stream: if stream.get('StreamStatus') == status: status_achieved = True break - elif status == 'DELETING': + + elif status == 'DELETING' and not check_mode: if not find_success: status_achieved = True break + else: time.sleep(polling_increment_secs) except botocore.exceptions.ClientError as e: @@ -494,7 +500,9 @@ def update_tags(client, stream_name, tags, check_mode=False): """ success = False err_msg = '' - tag_success, tag_msg, current_tags = get_tags(client, stream_name) + tag_success, tag_msg, current_tags = ( + get_tags(client, stream_name, check_mode=check_mode) + ) if current_tags: tags = make_tags_in_aws_format(tags) current_tags_set = ( @@ -765,7 +773,9 @@ def update(client, current_stream, stream_name, retention_period=None, return success, changed, err_msg if tags: - changed, err_msg = update_tags(client, stream_name, tags, check_mode) + changed, err_msg = ( + update_tags(client, stream_name, tags, check_mode=check_mode) + ) if changed: success = True if wait: @@ -783,7 +793,7 @@ def update(client, current_stream, stream_name, retention_period=None, return success, changed, err_msg def create_stream(client, stream_name, number_of_shards=1, retention_period=None, - tags=None, wait=False, wait_timeout=300, check_mode=None): + tags=None, wait=False, wait_timeout=300, check_mode=False): """Create an Amazon Kinesis Stream. Args: client (botocore.client.EC2): Boto3 client. @@ -824,13 +834,14 @@ def create_stream(client, stream_name, number_of_shards=1, retention_period=None if stream_found and current_stream['StreamStatus'] == 'DELETING' and wait: wait_success, wait_msg, current_stream = ( wait_for_status( - client, stream_name, 'ACTIVE', wait_timeout, check_mode + client, stream_name, 'ACTIVE', wait_timeout, + check_mode=check_mode ) ) if stream_found and current_stream['StreamStatus'] != 'DELETING': success, changed, err_msg = update( client, current_stream, stream_name, retention_period, tags, - wait, wait_timeout, check_mode + wait, wait_timeout, check_mode=check_mode ) else: create_success, create_msg = ( @@ -844,7 +855,8 @@ def create_stream(client, stream_name, number_of_shards=1, retention_period=None if wait: wait_success, wait_msg, results = ( wait_for_status( - client, stream_name, 'ACTIVE', wait_timeout, check_mode + client, stream_name, 'ACTIVE', wait_timeout, + check_mode=check_mode ) ) err_msg = ( @@ -938,7 +950,9 @@ def delete_stream(client, stream_name, wait=False, wait_timeout=300, changed = False err_msg = '' results = dict() - stream_found, stream_msg, current_stream = find_stream(client, stream_name) + stream_found, stream_msg, current_stream = ( + find_stream(client, stream_name, check_mode=check_mode) + ) if stream_found: success, err_msg = ( stream_action( @@ -951,7 +965,7 @@ def delete_stream(client, stream_name, wait=False, wait_timeout=300, success, err_msg, results = ( wait_for_status( client, stream_name, 'DELETING', wait_timeout, - check_mode + check_mode=check_mode ) ) err_msg = 'Stream {0} deleted successfully'.format(stream_name) From 1cc5ea7418ad61a58b127f876699149abadb02f5 Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Sun, 27 Mar 2016 16:16:52 -0700 Subject: [PATCH 1829/2522] Including unit tests. * Including unit tests as per https://groups.google.com/forum/#!topic/ansible-devel/ejY4CjKeC34 * This test suite is automatically run in https://github.com/linuxdynasty/ld-ansible-modules --- test/unit/cloud/amazon/test_kinesis_stream.py | 491 ++++++++++++++++++ 1 file changed, 491 insertions(+) create mode 100644 test/unit/cloud/amazon/test_kinesis_stream.py diff --git a/test/unit/cloud/amazon/test_kinesis_stream.py b/test/unit/cloud/amazon/test_kinesis_stream.py new file mode 100644 index 00000000000..5404ef99716 --- /dev/null +++ b/test/unit/cloud/amazon/test_kinesis_stream.py @@ -0,0 +1,491 @@ +#!/usr/bin/python + +import boto3 +import unittest + +from collections import namedtuple +from ansible.parsing.dataloader import DataLoader +from ansible.vars import VariableManager +from ansible.inventory import Inventory +from ansible.playbook.play import Play +from ansible.executor.task_queue_manager import TaskQueueManager + +import cloud.amazon.kinesis_stream as kinesis_stream + +Options = ( + namedtuple( + 'Options', [ + 'connection', 'module_path', 'forks', 'become', 'become_method', + 'become_user', 'remote_user', 'private_key_file', 'ssh_common_args', + 'sftp_extra_args', 'scp_extra_args', 'ssh_extra_args', 'verbosity', + 'check' + ] + ) +) +# initialize needed objects +variable_manager = VariableManager() +loader = DataLoader() +options = ( + Options( + connection='local', + module_path='cloud/amazon', + forks=1, become=None, become_method=None, become_user=None, check=True, + remote_user=None, private_key_file=None, ssh_common_args=None, + sftp_extra_args=None, scp_extra_args=None, ssh_extra_args=None, + verbosity=10 + ) +) +passwords = dict(vault_pass='') + +aws_region = 'us-west-2' + +# create inventory and pass to var manager +inventory = Inventory(loader=loader, variable_manager=variable_manager, host_list='localhost') +variable_manager.set_inventory(inventory) + +def run(play): + tqm = None + results = None + try: + tqm = TaskQueueManager( + inventory=inventory, + variable_manager=variable_manager, + loader=loader, + options=options, + passwords=passwords, + stdout_callback='default', + ) + results = tqm.run(play) + finally: + if tqm is not None: + tqm.cleanup() + return tqm, results + +class AnsibleKinesisStreamTasks(unittest.TestCase): + + def test_a_create_stream_1(self): + play_source = dict( + name = "Create Kinesis Stream with 10 Shards", + hosts = 'localhost', + gather_facts = 'no', + tasks = [ + dict( + action=dict( + module='kinesis_stream', + args=dict( + name='stream-test', + shards=10, + wait='yes', + region=aws_region, + ) + ), + register='stream', + ) + ] + ) + play = Play().load(play_source, variable_manager=variable_manager, loader=loader) + tqm, results = run(play) + self.failUnless(tqm._stats.ok['localhost'] == 1) + + def test_a_create_stream_2(self): + play_source = dict( + name = "Create Kinesis Stream with 10 Shards and create a tag called environment", + hosts = 'localhost', + gather_facts = 'no', + tasks = [ + dict( + action=dict( + module='kinesis_stream', + args=dict( + name='stream-test', + region=aws_region, + shards=10, + tags=dict( + env='development' + ), + wait='yes' + ) + ), + register='stream' + ) + ] + ) + play = Play().load(play_source, variable_manager=variable_manager, loader=loader) + tqm, results = run(play) + self.failUnless(tqm._stats.ok['localhost'] == 1) + + def test_a_create_stream_3(self): + play_source = dict( + name = "Create Kinesis Stream with 10 Shards and create a tag called environment and Change the default retention period from 24 to 48", + hosts = 'localhost', + gather_facts = 'no', + tasks = [ + dict( + action=dict( + module='kinesis_stream', + args=dict( + name='stream-test', + retention_period=48, + region=aws_region, + shards=10, + tags=dict( + env='development' + ), + wait='yes' + ) + ), + register='stream' + ) + ] + ) + play = Play().load(play_source, variable_manager=variable_manager, loader=loader) + tqm, results = run(play) + self.failUnless(tqm._stats.ok['localhost'] == 1) + + def test_b_create_stream_1(self): + play_source = dict( + name = "Create Kinesis Stream with out specifying the number of shards", + hosts = 'localhost', + gather_facts = 'no', + tasks = [ + dict( + action=dict( + module='kinesis_stream', + args=dict( + name='stream-test', + region=aws_region, + wait='yes' + ) + ), + register='stream' + ) + ] + ) + play = Play().load(play_source, variable_manager=variable_manager, loader=loader) + tqm, results = run(play) + self.failUnless(tqm._stats.failures['localhost'] == 1) + + def test_b_create_stream_2(self): + play_source = dict( + name = "Create Kinesis Stream with specifying the retention period less than 24 hours", + hosts = 'localhost', + gather_facts = 'no', + tasks = [ + dict( + action=dict( + module='kinesis_stream', + args=dict( + name='stream-test', + region=aws_region, + retention_period=23, + shards=10, + wait='yes' + ) + ), + register='stream' + ) + ] + ) + play = Play().load(play_source, variable_manager=variable_manager, loader=loader) + tqm, results = run(play) + self.failUnless(tqm._stats.failures['localhost'] == 1) + + def test_c_delete_stream_(self): + play_source = dict( + name = "Delete Kinesis Stream test-stream", + hosts = 'localhost', + gather_facts = 'no', + tasks = [ + dict( + action=dict( + module='kinesis_stream', + args=dict( + name='stream-test', + region=aws_region, + state='absent', + wait='yes' + ) + ) + ) + ] + ) + play = Play().load(play_source, variable_manager=variable_manager, loader=loader) + tqm, results = run(play) + self.failUnless(tqm._stats.ok['localhost'] == 1) + + +class AnsibleKinesisStreamFunctions(unittest.TestCase): + + def test_convert_to_lower(self): + example = { + 'HasMoreShards': True, + 'RetentionPeriodHours': 24, + 'StreamName': 'test', + 'StreamARN': 'arn:aws:kinesis:east-side:123456789:stream/test', + 'StreamStatus': 'ACTIVE' + } + converted_example = kinesis_stream.convert_to_lower(example) + keys = converted_example.keys() + keys.sort() + for i in range(len(keys)): + if i == 0: + self.assertEqual(keys[i], 'has_more_shards') + if i == 1: + self.assertEqual(keys[i], 'retention_period_hours') + if i == 2: + self.assertEqual(keys[i], 'stream_arn') + if i == 3: + self.assertEqual(keys[i], 'stream_name') + if i == 4: + self.assertEqual(keys[i], 'stream_status') + + def test_make_tags_in_aws_format(self): + example = { + 'env': 'development' + } + should_return = [ + { + 'Key': 'env', + 'Value': 'development' + } + ] + aws_tags = kinesis_stream.make_tags_in_aws_format(example) + self.assertEqual(aws_tags, should_return) + + def test_make_tags_in_proper_format(self): + example = [ + { + 'Key': 'env', + 'Value': 'development' + }, + { + 'Key': 'service', + 'Value': 'web' + } + ] + should_return = { + 'env': 'development', + 'service': 'web' + } + proper_tags = kinesis_stream.make_tags_in_proper_format(example) + self.assertEqual(proper_tags, should_return) + + def test_recreate_tags_from_list(self): + example = [('environment', 'development'), ('service', 'web')] + should_return = [ + { + 'Key': 'environment', + 'Value': 'development' + }, + { + 'Key': 'service', + 'Value': 'web' + } + ] + aws_tags = kinesis_stream.recreate_tags_from_list(example) + self.assertEqual(aws_tags, should_return) + + def test_get_tags(self): + client = boto3.client('kinesis', region_name='us-west-2') + success, err_msg, tags = kinesis_stream.get_tags(client, 'test', True) + self.assertTrue(success) + should_return = [ + { + 'Key': 'DryRunMode', + 'Value': 'true' + } + ] + self.assertEqual(tags, should_return) + + def test_find_stream(self): + client = boto3.client('kinesis', region_name='us-west-2') + success, err_msg, stream = ( + kinesis_stream.find_stream(client, 'test', check_mode=True) + ) + should_return = { + 'HasMoreShards': True, + 'RetentionPeriodHours': 24, + 'StreamName': 'test', + 'StreamARN': 'arn:aws:kinesis:east-side:123456789:stream/test', + 'StreamStatus': 'ACTIVE' + } + self.assertTrue(success) + self.assertEqual(stream, should_return) + + def test_wait_for_status(self): + client = boto3.client('kinesis', region_name='us-west-2') + success, err_msg, stream = ( + kinesis_stream.wait_for_status( + client, 'test', 'ACTIVE', check_mode=True + ) + ) + should_return = { + 'HasMoreShards': True, + 'RetentionPeriodHours': 24, + 'StreamName': 'test', + 'StreamARN': 'arn:aws:kinesis:east-side:123456789:stream/test', + 'StreamStatus': 'ACTIVE' + } + self.assertTrue(success) + self.assertEqual(stream, should_return) + + def test_tags_action_create(self): + client = boto3.client('kinesis', region_name='us-west-2') + tags = { + 'env': 'development', + 'service': 'web' + } + success, err_msg = ( + kinesis_stream.tags_action( + client, 'test', tags, 'create', check_mode=True + ) + ) + self.assertTrue(success) + + def test_tags_action_delete(self): + client = boto3.client('kinesis', region_name='us-west-2') + tags = { + 'env': 'development', + 'service': 'web' + } + success, err_msg = ( + kinesis_stream.tags_action( + client, 'test', tags, 'delete', check_mode=True + ) + ) + self.assertTrue(success) + + def test_tags_action_invalid(self): + client = boto3.client('kinesis', region_name='us-west-2') + tags = { + 'env': 'development', + 'service': 'web' + } + success, err_msg = ( + kinesis_stream.tags_action( + client, 'test', tags, 'append', check_mode=True + ) + ) + self.assertFalse(success) + + def test_update_tags(self): + client = boto3.client('kinesis', region_name='us-west-2') + tags = { + 'env': 'development', + 'service': 'web' + } + success, err_msg = ( + kinesis_stream.update_tags( + client, 'test', tags, check_mode=True + ) + ) + self.assertTrue(success) + + def test_stream_action_create(self): + client = boto3.client('kinesis', region_name='us-west-2') + success, err_msg = ( + kinesis_stream.stream_action( + client, 'test', 10, 'create', check_mode=True + ) + ) + self.assertTrue(success) + + def test_stream_action_delete(self): + client = boto3.client('kinesis', region_name='us-west-2') + success, err_msg = ( + kinesis_stream.stream_action( + client, 'test', 10, 'delete', check_mode=True + ) + ) + self.assertTrue(success) + + def test_stream_action_invalid(self): + client = boto3.client('kinesis', region_name='us-west-2') + success, err_msg = ( + kinesis_stream.stream_action( + client, 'test', 10, 'append', check_mode=True + ) + ) + self.assertFalse(success) + + def test_retention_action_increase(self): + client = boto3.client('kinesis', region_name='us-west-2') + success, err_msg = ( + kinesis_stream.retention_action( + client, 'test', 48, 'increase', check_mode=True + ) + ) + self.assertTrue(success) + + def test_retention_action_decrease(self): + client = boto3.client('kinesis', region_name='us-west-2') + success, err_msg = ( + kinesis_stream.retention_action( + client, 'test', 24, 'decrease', check_mode=True + ) + ) + self.assertTrue(success) + + def test_retention_action_invalid(self): + client = boto3.client('kinesis', region_name='us-west-2') + success, err_msg = ( + kinesis_stream.retention_action( + client, 'test', 24, 'create', check_mode=True + ) + ) + self.assertFalse(success) + + def test_update(self): + client = boto3.client('kinesis', region_name='us-west-2') + current_stream = { + 'HasMoreShards': True, + 'RetentionPeriodHours': 24, + 'StreamName': 'test', + 'StreamARN': 'arn:aws:kinesis:east-side:123456789:stream/test', + 'StreamStatus': 'ACTIVE' + } + tags = { + 'env': 'development', + 'service': 'web' + } + success, changed, err_msg = ( + kinesis_stream.update( + client, current_stream, 'test', retention_period=48, + tags=tags, check_mode=True + ) + ) + self.assertTrue(success) + self.assertTrue(changed) + self.assertEqual(err_msg, 'Kinesis Stream test updated successfully.') + + def test_create_stream(self): + client = boto3.client('kinesis', region_name='us-west-2') + tags = { + 'env': 'development', + 'service': 'web' + } + success, changed, err_msg, results = ( + kinesis_stream.create_stream( + client, 'test', number_of_shards=10, retention_period=48, + tags=tags, check_mode=True + ) + ) + should_return = { + 'has_more_shards': True, + 'retention_period_hours': 24, + 'stream_name': 'test', + 'stream_arn': 'arn:aws:kinesis:east-side:123456789:stream/test', + 'stream_status': 'ACTIVE', + 'tags': tags, + } + self.assertTrue(success) + self.assertTrue(changed) + self.assertEqual(results, should_return) + self.assertEqual(err_msg, 'Kinesis Stream test updated successfully.') + + +def main(): + unittest.main() + +if __name__ == '__main__': + main() From e9fcb8b28620268bc57407b010bfbecd08eb8dd1 Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Mon, 28 Mar 2016 07:39:15 -0700 Subject: [PATCH 1830/2522] Removed Ansible API based tests from this PR --- test/unit/cloud/amazon/test_kinesis_stream.py | 236 ++---------------- 1 file changed, 15 insertions(+), 221 deletions(-) diff --git a/test/unit/cloud/amazon/test_kinesis_stream.py b/test/unit/cloud/amazon/test_kinesis_stream.py index 5404ef99716..280ec5e2de6 100644 --- a/test/unit/cloud/amazon/test_kinesis_stream.py +++ b/test/unit/cloud/amazon/test_kinesis_stream.py @@ -3,216 +3,10 @@ import boto3 import unittest -from collections import namedtuple -from ansible.parsing.dataloader import DataLoader -from ansible.vars import VariableManager -from ansible.inventory import Inventory -from ansible.playbook.play import Play -from ansible.executor.task_queue_manager import TaskQueueManager - import cloud.amazon.kinesis_stream as kinesis_stream -Options = ( - namedtuple( - 'Options', [ - 'connection', 'module_path', 'forks', 'become', 'become_method', - 'become_user', 'remote_user', 'private_key_file', 'ssh_common_args', - 'sftp_extra_args', 'scp_extra_args', 'ssh_extra_args', 'verbosity', - 'check' - ] - ) -) -# initialize needed objects -variable_manager = VariableManager() -loader = DataLoader() -options = ( - Options( - connection='local', - module_path='cloud/amazon', - forks=1, become=None, become_method=None, become_user=None, check=True, - remote_user=None, private_key_file=None, ssh_common_args=None, - sftp_extra_args=None, scp_extra_args=None, ssh_extra_args=None, - verbosity=10 - ) -) -passwords = dict(vault_pass='') - aws_region = 'us-west-2' -# create inventory and pass to var manager -inventory = Inventory(loader=loader, variable_manager=variable_manager, host_list='localhost') -variable_manager.set_inventory(inventory) - -def run(play): - tqm = None - results = None - try: - tqm = TaskQueueManager( - inventory=inventory, - variable_manager=variable_manager, - loader=loader, - options=options, - passwords=passwords, - stdout_callback='default', - ) - results = tqm.run(play) - finally: - if tqm is not None: - tqm.cleanup() - return tqm, results - -class AnsibleKinesisStreamTasks(unittest.TestCase): - - def test_a_create_stream_1(self): - play_source = dict( - name = "Create Kinesis Stream with 10 Shards", - hosts = 'localhost', - gather_facts = 'no', - tasks = [ - dict( - action=dict( - module='kinesis_stream', - args=dict( - name='stream-test', - shards=10, - wait='yes', - region=aws_region, - ) - ), - register='stream', - ) - ] - ) - play = Play().load(play_source, variable_manager=variable_manager, loader=loader) - tqm, results = run(play) - self.failUnless(tqm._stats.ok['localhost'] == 1) - - def test_a_create_stream_2(self): - play_source = dict( - name = "Create Kinesis Stream with 10 Shards and create a tag called environment", - hosts = 'localhost', - gather_facts = 'no', - tasks = [ - dict( - action=dict( - module='kinesis_stream', - args=dict( - name='stream-test', - region=aws_region, - shards=10, - tags=dict( - env='development' - ), - wait='yes' - ) - ), - register='stream' - ) - ] - ) - play = Play().load(play_source, variable_manager=variable_manager, loader=loader) - tqm, results = run(play) - self.failUnless(tqm._stats.ok['localhost'] == 1) - - def test_a_create_stream_3(self): - play_source = dict( - name = "Create Kinesis Stream with 10 Shards and create a tag called environment and Change the default retention period from 24 to 48", - hosts = 'localhost', - gather_facts = 'no', - tasks = [ - dict( - action=dict( - module='kinesis_stream', - args=dict( - name='stream-test', - retention_period=48, - region=aws_region, - shards=10, - tags=dict( - env='development' - ), - wait='yes' - ) - ), - register='stream' - ) - ] - ) - play = Play().load(play_source, variable_manager=variable_manager, loader=loader) - tqm, results = run(play) - self.failUnless(tqm._stats.ok['localhost'] == 1) - - def test_b_create_stream_1(self): - play_source = dict( - name = "Create Kinesis Stream with out specifying the number of shards", - hosts = 'localhost', - gather_facts = 'no', - tasks = [ - dict( - action=dict( - module='kinesis_stream', - args=dict( - name='stream-test', - region=aws_region, - wait='yes' - ) - ), - register='stream' - ) - ] - ) - play = Play().load(play_source, variable_manager=variable_manager, loader=loader) - tqm, results = run(play) - self.failUnless(tqm._stats.failures['localhost'] == 1) - - def test_b_create_stream_2(self): - play_source = dict( - name = "Create Kinesis Stream with specifying the retention period less than 24 hours", - hosts = 'localhost', - gather_facts = 'no', - tasks = [ - dict( - action=dict( - module='kinesis_stream', - args=dict( - name='stream-test', - region=aws_region, - retention_period=23, - shards=10, - wait='yes' - ) - ), - register='stream' - ) - ] - ) - play = Play().load(play_source, variable_manager=variable_manager, loader=loader) - tqm, results = run(play) - self.failUnless(tqm._stats.failures['localhost'] == 1) - - def test_c_delete_stream_(self): - play_source = dict( - name = "Delete Kinesis Stream test-stream", - hosts = 'localhost', - gather_facts = 'no', - tasks = [ - dict( - action=dict( - module='kinesis_stream', - args=dict( - name='stream-test', - region=aws_region, - state='absent', - wait='yes' - ) - ) - ) - ] - ) - play = Play().load(play_source, variable_manager=variable_manager, loader=loader) - tqm, results = run(play) - self.failUnless(tqm._stats.ok['localhost'] == 1) - class AnsibleKinesisStreamFunctions(unittest.TestCase): @@ -286,7 +80,7 @@ def test_recreate_tags_from_list(self): self.assertEqual(aws_tags, should_return) def test_get_tags(self): - client = boto3.client('kinesis', region_name='us-west-2') + client = boto3.client('kinesis', region_name=aws_region) success, err_msg, tags = kinesis_stream.get_tags(client, 'test', True) self.assertTrue(success) should_return = [ @@ -298,7 +92,7 @@ def test_get_tags(self): self.assertEqual(tags, should_return) def test_find_stream(self): - client = boto3.client('kinesis', region_name='us-west-2') + client = boto3.client('kinesis', region_name=aws_region) success, err_msg, stream = ( kinesis_stream.find_stream(client, 'test', check_mode=True) ) @@ -313,7 +107,7 @@ def test_find_stream(self): self.assertEqual(stream, should_return) def test_wait_for_status(self): - client = boto3.client('kinesis', region_name='us-west-2') + client = boto3.client('kinesis', region_name=aws_region) success, err_msg, stream = ( kinesis_stream.wait_for_status( client, 'test', 'ACTIVE', check_mode=True @@ -330,7 +124,7 @@ def test_wait_for_status(self): self.assertEqual(stream, should_return) def test_tags_action_create(self): - client = boto3.client('kinesis', region_name='us-west-2') + client = boto3.client('kinesis', region_name=aws_region) tags = { 'env': 'development', 'service': 'web' @@ -343,7 +137,7 @@ def test_tags_action_create(self): self.assertTrue(success) def test_tags_action_delete(self): - client = boto3.client('kinesis', region_name='us-west-2') + client = boto3.client('kinesis', region_name=aws_region) tags = { 'env': 'development', 'service': 'web' @@ -356,7 +150,7 @@ def test_tags_action_delete(self): self.assertTrue(success) def test_tags_action_invalid(self): - client = boto3.client('kinesis', region_name='us-west-2') + client = boto3.client('kinesis', region_name=aws_region) tags = { 'env': 'development', 'service': 'web' @@ -369,7 +163,7 @@ def test_tags_action_invalid(self): self.assertFalse(success) def test_update_tags(self): - client = boto3.client('kinesis', region_name='us-west-2') + client = boto3.client('kinesis', region_name=aws_region) tags = { 'env': 'development', 'service': 'web' @@ -382,7 +176,7 @@ def test_update_tags(self): self.assertTrue(success) def test_stream_action_create(self): - client = boto3.client('kinesis', region_name='us-west-2') + client = boto3.client('kinesis', region_name=aws_region) success, err_msg = ( kinesis_stream.stream_action( client, 'test', 10, 'create', check_mode=True @@ -391,7 +185,7 @@ def test_stream_action_create(self): self.assertTrue(success) def test_stream_action_delete(self): - client = boto3.client('kinesis', region_name='us-west-2') + client = boto3.client('kinesis', region_name=aws_region) success, err_msg = ( kinesis_stream.stream_action( client, 'test', 10, 'delete', check_mode=True @@ -400,7 +194,7 @@ def test_stream_action_delete(self): self.assertTrue(success) def test_stream_action_invalid(self): - client = boto3.client('kinesis', region_name='us-west-2') + client = boto3.client('kinesis', region_name=aws_region) success, err_msg = ( kinesis_stream.stream_action( client, 'test', 10, 'append', check_mode=True @@ -409,7 +203,7 @@ def test_stream_action_invalid(self): self.assertFalse(success) def test_retention_action_increase(self): - client = boto3.client('kinesis', region_name='us-west-2') + client = boto3.client('kinesis', region_name=aws_region) success, err_msg = ( kinesis_stream.retention_action( client, 'test', 48, 'increase', check_mode=True @@ -418,7 +212,7 @@ def test_retention_action_increase(self): self.assertTrue(success) def test_retention_action_decrease(self): - client = boto3.client('kinesis', region_name='us-west-2') + client = boto3.client('kinesis', region_name=aws_region) success, err_msg = ( kinesis_stream.retention_action( client, 'test', 24, 'decrease', check_mode=True @@ -427,7 +221,7 @@ def test_retention_action_decrease(self): self.assertTrue(success) def test_retention_action_invalid(self): - client = boto3.client('kinesis', region_name='us-west-2') + client = boto3.client('kinesis', region_name=aws_region) success, err_msg = ( kinesis_stream.retention_action( client, 'test', 24, 'create', check_mode=True @@ -436,7 +230,7 @@ def test_retention_action_invalid(self): self.assertFalse(success) def test_update(self): - client = boto3.client('kinesis', region_name='us-west-2') + client = boto3.client('kinesis', region_name=aws_region) current_stream = { 'HasMoreShards': True, 'RetentionPeriodHours': 24, @@ -459,7 +253,7 @@ def test_update(self): self.assertEqual(err_msg, 'Kinesis Stream test updated successfully.') def test_create_stream(self): - client = boto3.client('kinesis', region_name='us-west-2') + client = boto3.client('kinesis', region_name=aws_region) tags = { 'env': 'development', 'service': 'web' From 7cacd7dd2ace6543fc84c9a2937b962fb4af4ce3 Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Fri, 8 Apr 2016 17:37:12 -0700 Subject: [PATCH 1831/2522] Module requires boto due to ec2.py --- cloud/amazon/kinesis_stream.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud/amazon/kinesis_stream.py b/cloud/amazon/kinesis_stream.py index 6fbd1ab5402..9e46c829c94 100644 --- a/cloud/amazon/kinesis_stream.py +++ b/cloud/amazon/kinesis_stream.py @@ -147,6 +147,7 @@ ''' try: + import boto import botocore import boto3 HAS_BOTO3 = True From 8a17506058dc322f5a85b6ff60fa3e5b86e088de Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Wed, 4 May 2016 16:05:37 -0700 Subject: [PATCH 1832/2522] version bump --- cloud/amazon/kinesis_stream.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/kinesis_stream.py b/cloud/amazon/kinesis_stream.py index 9e46c829c94..a9139053894 100644 --- a/cloud/amazon/kinesis_stream.py +++ b/cloud/amazon/kinesis_stream.py @@ -21,7 +21,7 @@ - Create or Delete a Kinesis Stream. - Update the retention period of a Kinesis Stream. - Update Tags on a Kinesis Stream. -version_added: "2.1" +version_added: "2.2" author: Allen Sanabria (@linuxdynasty) options: name: From e9f7fb092754e6a2aa1c540bc39d963594b141dd Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Thu, 14 Jul 2016 16:37:06 -0700 Subject: [PATCH 1833/2522] Now when number of shards is different than what is the stream currently, it will fail.\n\nShards can not be changed on an already created stream --- cloud/amazon/kinesis_stream.py | 79 +++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/cloud/amazon/kinesis_stream.py b/cloud/amazon/kinesis_stream.py index a9139053894..1ba25e69860 100644 --- a/cloud/amazon/kinesis_stream.py +++ b/cloud/amazon/kinesis_stream.py @@ -147,7 +147,6 @@ ''' try: - import boto import botocore import boto3 HAS_BOTO3 = True @@ -285,20 +284,18 @@ def get_tags(client, stream_name, check_mode=False): }, ] success = True - except botocore.exceptions.ClientError, e: + except botocore.exceptions.ClientError as e: err_msg = str(e) return success, err_msg, results -def find_stream(client, stream_name, limit=1, check_mode=False): +def find_stream(client, stream_name, check_mode=False): """Retrieve a Kinesis Stream. Args: client (botocore.client.EC2): Boto3 client. stream_name (str): Name of the Kinesis stream. Kwargs: - limit (int): Limit the number of shards to return within a stream. - default=1 check_mode (bool): This will pass DryRun as one of the parameters to the aws api. default=False @@ -313,15 +310,20 @@ def find_stream(client, stream_name, limit=1, check_mode=False): success = False params = { 'StreamName': stream_name, - 'Limit': limit } results = dict() + has_more_shards = True + shards = list() try: if not check_mode: - results = ( - client.describe_stream(**params)['StreamDescription'] - ) - results.pop('Shards') + while has_more_shards: + results = ( + client.describe_stream(**params)['StreamDescription'] + ) + shards.extend(results.pop('Shards')) + has_more_shards = results['HasMoreShards'] + results['Shards'] = shards + results['ShardsCount'] = len(shards) else: results = { 'HasMoreShards': True, @@ -331,7 +333,7 @@ def find_stream(client, stream_name, limit=1, check_mode=False): 'StreamStatus': 'ACTIVE' } success = True - except botocore.exceptions.ClientError, e: + except botocore.exceptions.ClientError as e: err_msg = str(e) return success, err_msg, results @@ -391,6 +393,8 @@ def wait_for_status(client, stream_name, status, wait_timeout=300, if not status_achieved: err_msg = "Wait time out reached, while waiting for results" + else: + err_msg = "Status {0} achieved successfully".format(status) return status_achieved, err_msg, stream @@ -442,7 +446,7 @@ def tags_action(client, stream_name, tags, action='create', check_mode=False): else: err_msg = 'Invalid action {0}'.format(action) - except botocore.exceptions.ClientError, e: + except botocore.exceptions.ClientError as e: err_msg = str(e) return success, err_msg @@ -500,6 +504,7 @@ def update_tags(client, stream_name, tags, check_mode=False): Tuple (bool, str) """ success = False + changed = False err_msg = '' tag_success, tag_msg, current_tags = ( get_tags(client, stream_name, check_mode=check_mode) @@ -536,13 +541,13 @@ def update_tags(client, stream_name, tags, check_mode=False): ) ) if not delete_success: - return delete_success, delete_msg + return delete_success, changed, delete_msg if tags_to_update: tags = make_tags_in_proper_format( recreate_tags_from_list(tags_to_update) ) else: - return True, 'Tags do not need to be updated' + return True, changed, 'Tags do not need to be updated' if tags: create_success, create_msg = ( @@ -551,9 +556,11 @@ def update_tags(client, stream_name, tags, check_mode=False): check_mode=check_mode ) ) - return create_success, create_msg + if create_success: + changed = True + return create_success, changed, create_msg - return success, err_msg + return success, changed, err_msg def stream_action(client, stream_name, shard_count=1, action='create', timeout=300, check_mode=False): @@ -603,7 +610,7 @@ def stream_action(client, stream_name, shard_count=1, action='create', else: err_msg = 'Invalid action {0}'.format(action) - except botocore.exceptions.ClientError, e: + except botocore.exceptions.ClientError as e: err_msg = str(e) return success, err_msg @@ -644,10 +651,18 @@ def retention_action(client, stream_name, retention_period=24, params['RetentionPeriodHours'] = retention_period client.increase_stream_retention_period(**params) success = True + err_msg = ( + 'Retention Period increased successfully to {0}' + .format(retention_period) + ) elif action == 'decrease': params['RetentionPeriodHours'] = retention_period client.decrease_stream_retention_period(**params) success = True + err_msg = ( + 'Retention Period decreased successfully to {0}' + .format(retention_period) + ) else: err_msg = 'Invalid action {0}'.format(action) else: @@ -658,7 +673,7 @@ def retention_action(client, stream_name, retention_period=24, else: err_msg = 'Invalid action {0}'.format(action) - except botocore.exceptions.ClientError, e: + except botocore.exceptions.ClientError as e: err_msg = str(e) return success, err_msg @@ -698,7 +713,7 @@ def update(client, current_stream, stream_name, retention_period=None, Returns: Tuple (bool, bool, str) """ - success = False + success = True changed = False err_msg = '' if retention_period: @@ -710,9 +725,10 @@ def update(client, current_stream, stream_name, retention_period=None, ) ) if not wait_success: - return wait_success, True, wait_msg + return wait_success, False, wait_msg if current_stream['StreamStatus'] == 'ACTIVE': + retention_changed = False if retention_period > current_stream['RetentionPeriodHours']: retention_changed, retention_msg = ( retention_action( @@ -720,8 +736,6 @@ def update(client, current_stream, stream_name, retention_period=None, check_mode=check_mode ) ) - if retention_changed: - success = True elif retention_period < current_stream['RetentionPeriodHours']: retention_changed, retention_msg = ( @@ -730,11 +744,8 @@ def update(client, current_stream, stream_name, retention_period=None, check_mode=check_mode ) ) - if retention_changed: - success = True elif retention_period == current_stream['RetentionPeriodHours']: - retention_changed = False retention_msg = ( 'Retention {0} is the same as {1}' .format( @@ -744,7 +755,10 @@ def update(client, current_stream, stream_name, retention_period=None, ) success = True - changed = retention_changed + if retention_changed: + success = True + changed = True + err_msg = retention_msg if changed and wait: wait_success, wait_msg, current_stream = ( @@ -754,7 +768,7 @@ def update(client, current_stream, stream_name, retention_period=None, ) ) if not wait_success: - return wait_success, True, wait_msg + return wait_success, False, wait_msg elif changed and not wait: stream_found, stream_msg, current_stream = ( find_stream(client, stream_name, check_mode=check_mode) @@ -774,11 +788,9 @@ def update(client, current_stream, stream_name, retention_period=None, return success, changed, err_msg if tags: - changed, err_msg = ( + _, _, err_msg = ( update_tags(client, stream_name, tags, check_mode=check_mode) ) - if changed: - success = True if wait: success, err_msg, _ = ( wait_for_status( @@ -832,6 +844,11 @@ def create_stream(client, stream_name, number_of_shards=1, retention_period=None stream_found, stream_msg, current_stream = ( find_stream(client, stream_name, check_mode=check_mode) ) + if stream_found: + if current_stream['ShardsCount'] != number_of_shards: + err_msg = 'Can not change the number of shards in a Kinesis Stream' + return success, changed, err_msg, results + if stream_found and current_stream['StreamStatus'] == 'DELETING' and wait: wait_success, wait_msg, current_stream = ( wait_for_status( @@ -1031,7 +1048,7 @@ def main(): region=region, endpoint=ec2_url, **aws_connect_kwargs ) ) - except botocore.exceptions.ClientError, e: + except botocore.exceptions.ClientError as e: err_msg = 'Boto3 Client Error - {0}'.format(str(e.msg)) module.fail_json( success=False, changed=False, result={}, msg=err_msg From 584a06eec7e035a4b9e571aeaf8ebc561d6b2fcd Mon Sep 17 00:00:00 2001 From: Robin Roth Date: Sat, 16 Jul 2016 09:38:30 +0200 Subject: [PATCH 1834/2522] zypper cleanup checks for failure/diff (#2569) * zypper cleanup checks for failure/diff * move check for changed/failed from functions back to main * handle all cases identially * generate diff together * fix module name --- packaging/os/zypper.py | 68 +++++++++++++----------------------------- 1 file changed, 21 insertions(+), 47 deletions(-) diff --git a/packaging/os/zypper.py b/packaging/os/zypper.py index 6297a85992e..2ad014ec972 100644 --- a/packaging/os/zypper.py +++ b/packaging/os/zypper.py @@ -220,6 +220,7 @@ def get_cmd(m, subcommand): def set_diff(m, retvals, result): + # TODO: if there is only one package, set before/after to version numbers packages = {'installed': [], 'removed': [], 'upgraded': []} for p in result: group = result[p]['group'] @@ -245,7 +246,7 @@ def set_diff(m, retvals, result): def package_present(m, name, want_latest): "install and update (if want_latest) the packages in name_install, while removing the packages in name_remove" - retvals = {'rc': 0, 'stdout': '', 'stderr': '', 'changed': False, 'failed': False} + retvals = {'rc': 0, 'stdout': '', 'stderr': ''} name_install, name_remove, urls = get_want_state(m, name) if not want_latest: @@ -256,7 +257,7 @@ def package_present(m, name, want_latest): name_remove = [p for p in name_remove if p in prerun_state] if not name_install and not name_remove and not urls: # nothing to install/remove and nothing to update - return retvals + return None, retvals # zypper install also updates packages cmd = get_cmd(m, 'install') @@ -271,25 +272,15 @@ def package_present(m, name, want_latest): retvals['cmd'] = cmd result, retvals['rc'], retvals['stdout'], retvals['stderr'] = parse_zypper_xml(m, cmd) - if retvals['rc'] == 0: - # installed all packages successfully - # checking the output is not straight-forward because zypper rewrites 'capabilities' - # could run get_installed_state and recheck, but this takes time - if result: - retvals['changed'] = True - else: - retvals['failed'] = True - # return retvals - if m._diff: - set_diff(m, retvals, result) - return retvals + return result, retvals -def package_update_all(m, do_patch): +def package_update_all(m): "run update or patch on all available packages" - retvals = {'rc': 0, 'stdout': '', 'stderr': '', 'changed': False, 'failed': False} - if do_patch: + + retvals = {'rc': 0, 'stdout': '', 'stderr': ''} + if m.params['type'] == 'patch': cmdname = 'patch' else: cmdname = 'update' @@ -297,19 +288,12 @@ def package_update_all(m, do_patch): cmd = get_cmd(m, cmdname) retvals['cmd'] = cmd result, retvals['rc'], retvals['stdout'], retvals['stderr'] = parse_zypper_xml(m, cmd) - if retvals['rc'] == 0: - if result: - retvals['changed'] = True - else: - retvals['failed'] = True - if m._diff: - set_diff(m, retvals, result) - return retvals + return result, retvals def package_absent(m, name): "remove the packages in name" - retvals = {'rc': 0, 'stdout': '', 'stderr': '', 'changed': False, 'failed': False} + retvals = {'rc': 0, 'stdout': '', 'stderr': ''} # Get package state name_install, name_remove, urls = get_want_state(m, name, remove=True) if name_install: @@ -321,28 +305,19 @@ def package_absent(m, name): prerun_state = get_installed_state(m, name_remove) name_remove = [p for p in name_remove if p in prerun_state] if not name_remove: - return retvals + return None, retvals cmd = get_cmd(m, 'remove') cmd.extend(name_remove) retvals['cmd'] = cmd result, retvals['rc'], retvals['stdout'], retvals['stderr'] = parse_zypper_xml(m, cmd) - if retvals['rc'] == 0: - # removed packages successfully - if result: - retvals['changed'] = True - else: - retvals['failed'] = True - if m._diff: - set_diff(m, retvals, result) - - return retvals + return result, retvals def repo_refresh(m): "update the repositories" - retvals = {'rc': 0, 'stdout': '', 'stderr': '', 'changed': False, 'failed': False} + retvals = {'rc': 0, 'stdout': '', 'stderr': ''} cmd = get_cmd(m, 'refresh') @@ -381,20 +356,19 @@ def main(): # Perform requested action if name == ['*'] and state == 'latest': - if module.params['type'] == 'package': - retvals = package_update_all(module, False) - elif module.params['type'] == 'patch': - retvals = package_update_all(module, True) + packages_changed, retvals = package_update_all(module) else: if state in ['absent', 'removed']: - retvals = package_absent(module, name) + packages_changed, retvals = package_absent(module, name) elif state in ['installed', 'present', 'latest']: - retvals = package_present(module, name, state == 'latest') + packages_changed, retvals = package_present(module, name, state == 'latest') + + retvals['changed'] = retvals['rc'] == 0 and packages_changed - failed = retvals['failed'] - del retvals['failed'] + if module._diff: + set_diff(module, retvals, packages_changed) - if failed: + if retvals['rc'] != 0: module.fail_json(msg="Zypper run failed.", **retvals) if not retvals['changed']: From 277a2b5df14c78db8e494d366800bbf9eb4c8e50 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Sat, 16 Jul 2016 00:41:57 -0700 Subject: [PATCH 1835/2522] Adds style conventions to bigip_monitor_http (#2564) A number of coding conventions have been adopted for new F5 modules that are in development. To ensure common usage across the modules, this module needed to be updated to reflect those conventions. No functional code changes were made. --- network/f5/bigip_monitor_http.py | 424 ++++++++++++++++--------------- 1 file changed, 213 insertions(+), 211 deletions(-) diff --git a/network/f5/bigip_monitor_http.py b/network/f5/bigip_monitor_http.py index 0c6b15936b1..86096e95a2c 100644 --- a/network/f5/bigip_monitor_http.py +++ b/network/f5/bigip_monitor_http.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - +# # (c) 2013, serge van Ginderachter # based on Matt Hite's bigip_pool module # (c) 2013, Matt Hite @@ -25,160 +25,163 @@ module: bigip_monitor_http short_description: "Manages F5 BIG-IP LTM http monitors" description: - - "Manages F5 BIG-IP LTM monitors via iControl SOAP API" + - Manages F5 BIG-IP LTM monitors via iControl SOAP API version_added: "1.4" author: - - Serge van Ginderachter (@srvg) - - Tim Rupp (@caphrim007) + - Serge van Ginderachter (@srvg) + - Tim Rupp (@caphrim007) notes: - - "Requires BIG-IP software version >= 11" - - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" - - "Best run as a local_action in your playbook" - - "Monitor API documentation: https://devcentral.f5.com/wiki/iControl.LocalLB__Monitor.ashx" + - "Requires BIG-IP software version >= 11" + - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" + - "Best run as a local_action in your playbook" + - "Monitor API documentation: https://devcentral.f5.com/wiki/iControl.LocalLB__Monitor.ashx" requirements: - - bigsuds + - bigsuds options: - server: - description: - - BIG-IP host - required: true - default: null - server_port: - description: - - BIG-IP server port - required: false - default: 443 - version_added: "2.2" - user: - description: - - BIG-IP username - required: true - default: null - password: - description: - - BIG-IP password - required: true - default: null - validate_certs: - description: - - If C(no), SSL certificates will not be validated. This should only be used - on personally controlled sites. Prior to 2.0, this module would always - validate on python >= 2.7.9 and never validate on python <= 2.7.8 - required: false - default: 'yes' - choices: ['yes', 'no'] - version_added: 2.0 - state: - description: - - Monitor state - required: false - default: 'present' - choices: ['present', 'absent'] - name: - description: - - Monitor name - required: true - default: null - aliases: ['monitor'] - partition: - description: - - Partition for the monitor - required: false - default: 'Common' - parent: - description: - - The parent template of this monitor template - required: false - default: 'http' - parent_partition: - description: - - Partition for the parent monitor - required: false - default: 'Common' - send: - description: - - The send string for the monitor call - required: true - default: none - receive: - description: - - The receive string for the monitor call - required: true - default: none - receive_disable: - description: - - The receive disable string for the monitor call - required: true - default: none - ip: - description: - - IP address part of the ipport definition. The default API setting - is "0.0.0.0". - required: false - default: none - port: - description: - - port address part op the ipport definition. The default API - setting is 0. - required: false - default: none - interval: - description: - - The interval specifying how frequently the monitor instance - of this template will run. By default, this interval is used for up and - down states. The default API setting is 5. - required: false - default: none - timeout: - description: - - The number of seconds in which the node or service must respond to - the monitor request. If the target responds within the set time - period, it is considered up. If the target does not respond within - the set time period, it is considered down. You can change this - number to any number you want, however, it should be 3 times the - interval number of seconds plus 1 second. The default API setting - is 16. - required: false - default: none - time_until_up: - description: - - Specifies the amount of time in seconds after the first successful - response before a node will be marked up. A value of 0 will cause a - node to be marked up immediately after a valid response is received - from the node. The default API setting is 0. - required: false - default: none + server: + description: + - BIG-IP host + required: true + default: null + server_port: + description: + - BIG-IP server port + required: false + default: 443 + version_added: "2.2" + user: + description: + - BIG-IP username + required: true + default: null + password: + description: + - BIG-IP password + required: true + default: null + validate_certs: + description: + - If C(no), SSL certificates will not be validated. This should only be used + on personally controlled sites. Prior to 2.0, this module would always + validate on python >= 2.7.9 and never validate on python <= 2.7.8 + required: false + default: 'yes' + choices: + - yes + - no + version_added: 2.0 + state: + description: + - Monitor state + required: false + default: 'present' + choices: + - present + - absent + name: + description: + - Monitor name + required: true + default: null + aliases: + - monitor + partition: + description: + - Partition for the monitor + required: false + default: 'Common' + parent: + description: + - The parent template of this monitor template + required: false + default: 'http' + parent_partition: + description: + - Partition for the parent monitor + required: false + default: 'Common' + send: + description: + - The send string for the monitor call + required: true + default: none + receive: + description: + - The receive string for the monitor call + required: true + default: none + receive_disable: + description: + - The receive disable string for the monitor call + required: true + default: none + ip: + description: + - IP address part of the ipport definition. The default API setting + is "0.0.0.0". + required: false + default: none + port: + description: + - Port address part of the ip/port definition. The default API + setting is 0. + required: false + default: none + interval: + description: + - The interval specifying how frequently the monitor instance + of this template will run. By default, this interval is used for up and + down states. The default API setting is 5. + required: false + default: none + timeout: + description: + - The number of seconds in which the node or service must respond to + the monitor request. If the target responds within the set time + period, it is considered up. If the target does not respond within + the set time period, it is considered down. You can change this + number to any number you want, however, it should be 3 times the + interval number of seconds plus 1 second. The default API setting + is 16. + required: false + default: none + time_until_up: + description: + - Specifies the amount of time in seconds after the first successful + response before a node will be marked up. A value of 0 will cause a + node to be marked up immediately after a valid response is received + from the node. The default API setting is 0. + required: false + default: none ''' EXAMPLES = ''' - name: BIGIP F5 | Create HTTP Monitor - local_action: - module: bigip_monitor_http - state: present - server: "{{ f5server }}" - user: "{{ f5user }}" - password: "{{ f5password }}" - name: "{{ item.monitorname }}" - send: "{{ item.send }}" - receive: "{{ item.receive }}" - with_items: f5monitors + bigip_monitor_http: + state: "present" + server: "lb.mydomain.com" + user: "admin" + password: "secret" + name: "my_http_monitor" + send: "http string to send" + receive: "http string to receive" + delegate_to: localhost + - name: BIGIP F5 | Remove HTTP Monitor - local_action: - module: bigip_monitor_http - state: absent - server: "{{ f5server }}" - user: "{{ f5user }}" - password: "{{ f5password }}" - name: "{{ monitorname }}" + bigip_monitor_http: + state: "absent" + server: "lb.mydomain.com" + user: "admin" + password: "secret" + name: "my_http_monitor" + delegate_to: localhost ''' TEMPLATE_TYPE = 'TTYPE_HTTP' DEFAULT_PARENT_TYPE = 'http' - def check_monitor_exists(module, api, monitor, parent): - # hack to determine if monitor exists result = False try: @@ -188,7 +191,7 @@ def check_monitor_exists(module, api, monitor, parent): result = True else: module.fail_json(msg='Monitor already exists, but has a different type (%s) or parent(%s)' % (ttype, parent)) - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed as e: if "was not found" in str(e): result = False else: @@ -198,10 +201,15 @@ def check_monitor_exists(module, api, monitor, parent): def create_monitor(api, monitor, template_attributes): - try: - api.LocalLB.Monitor.create_template(templates=[{'template_name': monitor, 'template_type': TEMPLATE_TYPE}], template_attributes=[template_attributes]) - except bigsuds.OperationFailed, e: + api.LocalLB.Monitor.create_template( + templates=[{ + 'template_name': monitor, + 'template_type': TEMPLATE_TYPE + }], + template_attributes=[template_attributes] + ) + except bigsuds.OperationFailed as e: if "already exists" in str(e): return False else: @@ -211,10 +219,9 @@ def create_monitor(api, monitor, template_attributes): def delete_monitor(api, monitor): - try: api.LocalLB.Monitor.delete_template(template_names=[monitor]) - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed as e: # maybe it was deleted since we checked if "was not found" in str(e): return False @@ -225,10 +232,12 @@ def delete_monitor(api, monitor): def check_string_property(api, monitor, str_property): - try: - return str_property == api.LocalLB.Monitor.get_template_string_property([monitor], [str_property['type']])[0] - except bigsuds.OperationFailed, e: + template_prop = api.LocalLB.Monitor.get_template_string_property( + [monitor], [str_property['type']] + )[0] + return str_property == template_prop + except bigsuds.OperationFailed as e: # happens in check mode if not created yet if "was not found" in str(e): return True @@ -238,15 +247,19 @@ def check_string_property(api, monitor, str_property): def set_string_property(api, monitor, str_property): - - api.LocalLB.Monitor.set_template_string_property(template_names=[monitor], values=[str_property]) + api.LocalLB.Monitor.set_template_string_property( + template_names=[monitor], + values=[str_property] + ) def check_integer_property(api, monitor, int_property): - try: - return int_property == api.LocalLB.Monitor.get_template_integer_property([monitor], [int_property['type']])[0] - except bigsuds.OperationFailed, e: + template_prop = api.LocalLB.Monitor.get_template_integer_property( + [monitor], [int_property['type']] + )[0] + return int_property == template_prop + except bigsuds.OperationFailed as e: # happens in check mode if not created yet if "was not found" in str(e): return True @@ -255,10 +268,11 @@ def check_integer_property(api, monitor, int_property): raise - def set_integer_property(api, monitor, int_property): - - api.LocalLB.Monitor.set_template_integer_property(template_names=[monitor], values=[int_property]) + api.LocalLB.Monitor.set_template_integer_property( + template_names=[monitor], + values=[int_property] + ) def update_monitor_properties(api, module, monitor, template_string_properties, template_integer_properties): @@ -278,61 +292,46 @@ def update_monitor_properties(api, module, monitor, template_string_properties, def get_ipport(api, monitor): - return api.LocalLB.Monitor.get_template_destination(template_names=[monitor])[0] def set_ipport(api, monitor, ipport): - try: - api.LocalLB.Monitor.set_template_destination(template_names=[monitor], destinations=[ipport]) + api.LocalLB.Monitor.set_template_destination( + template_names=[monitor], destinations=[ipport] + ) return True, "" - - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed as e: if "Cannot modify the address type of monitor" in str(e): return False, "Cannot modify the address type of monitor if already assigned to a pool." else: # genuine exception raise -# =========================================== -# main loop -# -# writing a module for other monitor types should -# only need an updated main() (and monitor specific functions) def main(): - - # begin monitor specific stuff - argument_spec=f5_argument_spec(); - argument_spec.update( dict( - name = dict(required=True), - parent = dict(default=DEFAULT_PARENT_TYPE), - parent_partition = dict(default='Common'), - send = dict(required=False), - receive = dict(required=False), - receive_disable = dict(required=False), - ip = dict(required=False), - port = dict(required=False, type='int'), - interval = dict(required=False, type='int'), - timeout = dict(required=False, type='int'), - time_until_up = dict(required=False, type='int', default=0) - ) + argument_spec = f5_argument_spec() + + meta_args = dict( + name=dict(required=True), + parent=dict(default=DEFAULT_PARENT_TYPE), + parent_partition=dict(default='Common'), + send=dict(required=False), + receive=dict(required=False), + receive_disable=dict(required=False), + ip=dict(required=False), + port=dict(required=False, type='int'), + interval=dict(required=False, type='int'), + timeout=dict(required=False, type='int'), + time_until_up=dict(required=False, type='int', default=0) ) + argument_spec.update(meta_args) module = AnsibleModule( - argument_spec = argument_spec, + argument_spec=argument_spec, supports_check_mode=True ) - if not bigsuds_found: - module.fail_json(msg="the python bigsuds module is required") - - if module.params['validate_certs']: - import ssl - if not hasattr(ssl, 'SSLContext'): - module.fail_json(msg='bigsuds does not support verifying certificates with python < 2.7.9. Either update python or set validate_certs=False on the task') - server = module.params['server'] server_port = module.params['server_port'] user = module.params['user'] @@ -359,15 +358,14 @@ def main(): api = bigip_api(server, user, password, validate_certs, port=server_port) monitor_exists = check_monitor_exists(module, api, monitor, parent) - # ipport is a special setting - if monitor_exists: # make sure to not update current settings if not asked + if monitor_exists: cur_ipport = get_ipport(api, monitor) if ip is None: ip = cur_ipport['ipport']['address'] if port is None: port = cur_ipport['ipport']['port'] - else: # use API defaults if not defined to create it + else: if interval is None: interval = 5 if timeout is None: @@ -412,19 +410,26 @@ def main(): {'type': 'STYPE_RECEIVE_DRAIN', 'value': receive_disable}] - template_integer_properties = [{'type': 'ITYPE_INTERVAL', - 'value': interval}, - {'type': 'ITYPE_TIMEOUT', - 'value': timeout}, - {'type': 'ITYPE_TIME_UNTIL_UP', - 'value': time_until_up}] + template_integer_properties = [ + { + 'type': 'ITYPE_INTERVAL', + 'value': interval + }, + { + 'type': 'ITYPE_TIMEOUT', + 'value': timeout + }, + { + 'type': 'ITYPE_TIME_UNTIL_UP', + 'value': time_until_up + } + ] # main logic, monitor generic try: result = {'changed': False} # default - if state == 'absent': if monitor_exists: if not module.check_mode: @@ -433,10 +438,9 @@ def main(): result['changed'] |= delete_monitor(api, monitor) else: result['changed'] |= True - - else: # state present - ## check for monitor itself - if not monitor_exists: # create it + else: + # check for monitor itself + if not monitor_exists: if not module.check_mode: # again, check changed status here b/c race conditions # if other task already created it @@ -444,22 +448,20 @@ def main(): else: result['changed'] |= True - ## check for monitor parameters + # check for monitor parameters # whether it already existed, or was just created, now update # the update functions need to check for check mode but # cannot update settings if it doesn't exist which happens in check mode result['changed'] |= update_monitor_properties(api, module, monitor, - template_string_properties, - template_integer_properties) + template_string_properties, + template_integer_properties) # we just have to update the ipport if monitor already exists and it's different if monitor_exists and cur_ipport != ipport: set_ipport(api, monitor, ipport) result['changed'] |= True - #else: monitor doesn't exist (check mode) or ipport is already ok - - - except Exception, e: + # else: monitor doesn't exist (check mode) or ipport is already ok + except Exception as e: module.fail_json(msg="received exception: %s" % e) module.exit_json(**result) From 4af591daef607116707036db90776c8984028258 Mon Sep 17 00:00:00 2001 From: Kaz Cheng Date: Sat, 16 Jul 2016 17:53:09 +1000 Subject: [PATCH 1836/2522] Add ability to create event and query acl rules for a given acl token (#2076) --- clustering/consul_acl.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/clustering/consul_acl.py b/clustering/consul_acl.py index 67ca63184f9..a30ba8ab4bd 100644 --- a/clustering/consul_acl.py +++ b/clustering/consul_acl.py @@ -236,8 +236,12 @@ def yml_to_rules(module, yml_rules): rules.add_rule('key', Rule(rule['key'], rule['policy'])) elif ('service' in rule and 'policy' in rule): rules.add_rule('service', Rule(rule['service'], rule['policy'])) + elif ('event' in rule and 'policy' in rule): + rules.add_rule('event', Rule(rule['event'], rule['policy'])) + elif ('query' in rule and 'policy' in rule): + rules.add_rule('query', Rule(rule['query'], rule['policy'])) else: - module.fail_json(msg="a rule requires a key/service and a policy.") + module.fail_json(msg="a rule requires a key/service/event or query and a policy.") return rules template = '''%s "%s" { @@ -245,7 +249,7 @@ def yml_to_rules(module, yml_rules): } ''' -RULE_TYPES = ['key', 'service'] +RULE_TYPES = ['key', 'service', 'event', 'query'] class Rules: From dfb6cccff4eea099c0f04a294f61ca0439841928 Mon Sep 17 00:00:00 2001 From: Colin Hutchinson Date: Wed, 20 Jul 2016 01:56:41 -0400 Subject: [PATCH 1837/2522] consul.py doc fix (#2589) Small fix to the examples section of consul.py --- clustering/consul.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clustering/consul.py b/clustering/consul.py index a5e4a5140dd..002aca3e7b6 100644 --- a/clustering/consul.py +++ b/clustering/consul.py @@ -182,7 +182,7 @@ - name: register nginx with an http check consul: - name: nginx + service_name: nginx service_port: 80 interval: 60s http: /status From ed74ab8caf2b8feafce89525bef1c977b37f29c6 Mon Sep 17 00:00:00 2001 From: gyurco Date: Wed, 20 Jul 2016 07:58:35 +0200 Subject: [PATCH 1838/2522] mongodb_user: fix ssl_cert_reqs exception (#2573) If ssl is not enabled, but ssl_cert_reqs is passed to pymongo, an exception occures. Fixes: #2571 --- database/misc/mongodb_user.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index 703df319a83..07e54bc8ef0 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -306,7 +306,9 @@ def main(): user = module.params['name'] password = module.params['password'] ssl = module.params['ssl'] - ssl_cert_reqs = getattr(ssl_lib, module.params['ssl_cert_reqs']) + ssl_cert_reqs = None + if ssl: + ssl_cert_reqs = getattr(ssl_lib, module.params['ssl_cert_reqs']) roles = module.params['roles'] state = module.params['state'] update_password = module.params['update_password'] From fd235289ddc33c3aabb5e4e2f59ba09691f37bd8 Mon Sep 17 00:00:00 2001 From: Donovan Jones Date: Wed, 20 Jul 2016 17:59:48 +1200 Subject: [PATCH 1839/2522] Update comment for ttl parameter to indicate 2-119 seconds is invalid (#2546) --- network/cloudflare_dns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/cloudflare_dns.py b/network/cloudflare_dns.py index e98c7cd48b5..e8a1eab8b6f 100644 --- a/network/cloudflare_dns.py +++ b/network/cloudflare_dns.py @@ -87,7 +87,7 @@ default: 30 ttl: description: - - The TTL to give the new record. Min 1 (automatic), max 2147483647 + - The TTL to give the new record. Must be between 120 and 2,147,483,647 seconds, or 1 for automatic. required: false default: 1 (automatic) type: From 38d364103dc2a08cbb9a42e9ab6115b0702688bc Mon Sep 17 00:00:00 2001 From: lorijoan Date: Wed, 20 Jul 2016 12:57:35 +0100 Subject: [PATCH 1840/2522] Update cs_volume module to fix typo on force attribute (#2592) fixes #2590 --- cloud/cloudstack/cs_volume.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/cloudstack/cs_volume.py b/cloud/cloudstack/cs_volume.py index 9897b62c6df..c2a542741d6 100644 --- a/cloud/cloudstack/cs_volume.py +++ b/cloud/cloudstack/cs_volume.py @@ -387,7 +387,7 @@ def absent_volume(self): volume = self.get_volume() if volume: - if 'attached' in volume and not self.module.param.get('force'): + if 'attached' in volume and not self.module.params.get('force'): self.module.fail_json(msg="Volume '%s' is attached, use force=true for detaching and removing the volume." % volume.get('name')) self.result['changed'] = True From 960fcaf67af5dc9be1bb9e8904fa8137464fd392 Mon Sep 17 00:00:00 2001 From: gyurco Date: Thu, 21 Jul 2016 08:23:05 +0200 Subject: [PATCH 1841/2522] mongodb_user: properly guard user adding with try...except (#2582) The user adding part is not properly guarded by a try...except block, so pymongo exceptions can escape from it. Also there's a double-guarding where roles are given. Fixes: #2575 --- database/misc/mongodb_user.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index 07e54bc8ef0..c17226a5d12 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -198,11 +198,7 @@ def user_add(module, client, db_name, user, password, roles): if roles is None: db.add_user(user, password, False) else: - try: - db.add_user(user, password, None, roles=roles) - except OperationFailure, e: - err_msg = str(e) - module.fail_json(msg=err_msg) + db.add_user(user, password, None, roles=roles) def user_remove(module, client, db_name, user): exists = user_find(client, user, db_name) @@ -346,16 +342,16 @@ def main(): if password is None and update_password == 'always': module.fail_json(msg='password parameter required when adding a user unless update_password is set to on_create') - uinfo = user_find(client, user, db_name) - if update_password != 'always' and uinfo: - password = None - if not check_if_roles_changed(uinfo, roles, db_name): - module.exit_json(changed=False, user=user) + try: + uinfo = user_find(client, user, db_name) + if update_password != 'always' and uinfo: + password = None + if not check_if_roles_changed(uinfo, roles, db_name): + module.exit_json(changed=False, user=user) - if module.check_mode: - module.exit_json(changed=True, user=user) + if module.check_mode: + module.exit_json(changed=True, user=user) - try: user_add(module, client, db_name, user, password, roles) except OperationFailure, e: module.fail_json(msg='Unable to add or update user: %s' % str(e)) From 7bc7f9f0780008565e9ee4d52937caf4d906aefb Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Thu, 21 Jul 2016 17:40:57 +0200 Subject: [PATCH 1842/2522] Make os_user_role respect domain when querying for projects (#2520) --- cloud/openstack/os_user_role.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/cloud/openstack/os_user_role.py b/cloud/openstack/os_user_role.py index c034908f866..22f41830c61 100644 --- a/cloud/openstack/os_user_role.py +++ b/cloud/openstack/os_user_role.py @@ -165,16 +165,20 @@ def main(): if g is None: module.fail_json(msg="Group %s is not valid" % group) filters['group'] = g['id'] - if project: - p = cloud.get_project(project) - if p is None: - module.fail_json(msg="Project %s is not valid" % project) - filters['project'] = p['id'] if domain: d = cloud.get_domain(domain) if d is None: module.fail_json(msg="Domain %s is not valid" % domain) filters['domain'] = d['id'] + if project: + if domain: + p = cloud.get_project(project, domain_id=filters['domain']) + else: + p = cloud.get_project(project) + + if p is None: + module.fail_json(msg="Project %s is not valid" % project) + filters['project'] = p['id'] assignment = cloud.list_role_assignments(filters=filters) From 651a25f1a3a152182dbe67c5e1dc203026aa88a5 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Thu, 21 Jul 2016 13:04:15 -0400 Subject: [PATCH 1843/2522] Remove duplicate requirements. --- cloud/amazon/iam_server_certificate_facts.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cloud/amazon/iam_server_certificate_facts.py b/cloud/amazon/iam_server_certificate_facts.py index da0c0beabf6..259b5153204 100644 --- a/cloud/amazon/iam_server_certificate_facts.py +++ b/cloud/amazon/iam_server_certificate_facts.py @@ -31,7 +31,6 @@ extends_documentation_fragment: - aws - ec2 -requirements: ['boto3'] ''' EXAMPLES = ''' From bef9a1c14faa3ac32023ac158e248354e24fe7e3 Mon Sep 17 00:00:00 2001 From: David Edmonds Date: Fri, 22 Jul 2016 22:03:40 +0100 Subject: [PATCH 1844/2522] Pass through YAML parsed object instead of string. (#2347) --- clustering/kubernetes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/clustering/kubernetes.py b/clustering/kubernetes.py index 12dfee1071c..18372cb62d9 100644 --- a/clustering/kubernetes.py +++ b/clustering/kubernetes.py @@ -334,7 +334,10 @@ def main(): file_reference = module.params.get('file_reference') if inline_data: - data = inline_data + if not isinstance(inline_data, dict) and not isinstance(inline_data, list): + data = yaml.load(inline_data) + else: + data = inline_data else: try: f = open(file_reference, "r") From b02a24ffed8c2670bd9fceabbb51e6bd50856404 Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Mon, 25 Jul 2016 20:05:58 +0200 Subject: [PATCH 1845/2522] Ensure os_project checks the right domain (#2519) --- cloud/openstack/os_project.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cloud/openstack/os_project.py b/cloud/openstack/os_project.py index 630b26c4112..eeaa101e660 100644 --- a/cloud/openstack/os_project.py +++ b/cloud/openstack/os_project.py @@ -111,7 +111,7 @@ def _needs_update(module, project): keys = ('description', 'enabled') for key in keys: if module.params[key] is not None and module.params[key] != project.get(key): - return True + return True return False @@ -176,9 +176,13 @@ def main(): except: # Ok, let's hope the user is non-admin and passing a sane id pass - + cloud = shade.openstack_cloud(**module.params) - project = cloud.get_project(name) + + if domain: + project = cloud.get_project(name, domain_id=domain) + else: + project = cloud.get_project(name) if module.check_mode: module.exit_json(changed=_system_state_change(module, project)) From 669f99d841978b6d1677bc49438a8dc2b6f34213 Mon Sep 17 00:00:00 2001 From: Sander Dijkhuis Date: Mon, 25 Jul 2016 21:41:41 +0200 Subject: [PATCH 1846/2522] Fix reference in known_hosts doc (#2563) The format is described in sshd(1), not ssh(1). --- system/known_hosts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/known_hosts.py b/system/known_hosts.py index a355b6db5fe..0c9f24f4c2c 100644 --- a/system/known_hosts.py +++ b/system/known_hosts.py @@ -37,7 +37,7 @@ default: null key: description: - - The SSH public host key, as a string (required if state=present, optional when state=absent, in which case all keys for the host are removed). The key must be in the right format for ssh (see ssh(1), section "SSH_KNOWN_HOSTS FILE FORMAT") + - The SSH public host key, as a string (required if state=present, optional when state=absent, in which case all keys for the host are removed). The key must be in the right format for ssh (see sshd(1), section "SSH_KNOWN_HOSTS FILE FORMAT") required: false default: null path: From db979dde748cc3829cdcdee47f4526e06428fdde Mon Sep 17 00:00:00 2001 From: Kaz Cheng Date: Tue, 26 Jul 2016 05:48:27 +1000 Subject: [PATCH 1847/2522] Fix a number of issues around detecting nat gateways, how (#1511) routes_to_delete is detected, propagating_vgw_ids and checking if gateway_id exists. --- cloud/amazon/ec2_vpc_route_table.py | 62 +++++++++++++++++++---------- 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index 58a8795372b..7f64a1b3129 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -40,8 +40,12 @@ default: null routes: description: - - "List of routes in the route table. Routes are specified as dicts containing the keys 'dest' and one of 'gateway_id', 'instance_id', 'interface_id', or 'vpc_peering_connection_id'. If 'gateway_id' is specified, you can refer to the VPC's IGW by using the value 'igw'." - required: true + - "List of routes in the route table. + Routes are specified as dicts containing the keys 'dest' and one of 'gateway_id', + 'instance_id', 'interface_id', or 'vpc_peering_connection_id'. + If 'gateway_id' is specified, you can refer to the VPC's IGW by using the value 'igw'. Routes are required for present states." + required: false + default: None state: description: - "Create or destroy the VPC route table" @@ -282,6 +286,18 @@ def route_spec_matches_route(route_spec, route): 'interface_id': 'interface_id', 'vpc_peering_connection_id': 'vpc_peering_connection_id', } + + # This is a workaround to catch managed NAT gateways as they do not show + # up in any of the returned values when describing route tables. + # The caveat of doing it this way is that if there was an existing + # route for another nat gateway in this route table there is not a way to + # change to another nat gateway id. Long term solution would be to utilise + # boto3 which is a very big task for this module or to update boto. + if route_spec.get('gateway_id') and 'nat-' in route_spec['gateway_id']: + if route.destination_cidr_block == route_spec['destination_cidr_block']: + if all((not route.gateway_id, not route.instance_id, not route.interface_id, not route.vpc_peering_connection_id)): + return True + for k in key_attr_map.iterkeys(): if k in route_spec: if route_spec[k] != getattr(route, k): @@ -317,27 +333,31 @@ def ensure_routes(vpc_conn, route_table, route_specs, propagating_vgw_ids, # correct than checking whether the route uses a propagating VGW. # The current logic will leave non-propagated routes using propagating # VGWs in place. - routes_to_delete = [r for r in routes_to_match - if r.gateway_id != 'local' - and (propagating_vgw_ids is not None - and r.gateway_id not in propagating_vgw_ids)] + routes_to_delete = [] + for r in routes_to_match: + if r.gateway_id: + if r.gateway_id != 'local' and not r.gateway_id.startswith('vpce-'): + if not propagating_vgw_ids or r.gateway_id not in propagating_vgw_ids: + routes_to_delete.append(r) + else: + routes_to_delete.append(r) - changed = routes_to_delete or route_specs_to_create + changed = bool(routes_to_delete or route_specs_to_create) if changed: - for route_spec in route_specs_to_create: + for route in routes_to_delete: try: - vpc_conn.create_route(route_table.id, - dry_run=check_mode, - **route_spec) + vpc_conn.delete_route(route_table.id, + route.destination_cidr_block, + dry_run=check_mode) except EC2ResponseError as e: if e.error_code == 'DryRunOperation': pass - for route in routes_to_delete: + for route_spec in route_specs_to_create: try: - vpc_conn.delete_route(route_table.id, - route.destination_cidr_block, - dry_run=check_mode) + vpc_conn.create_route(route_table.id, + dry_run=check_mode, + **route_spec) except EC2ResponseError as e: if e.error_code == 'DryRunOperation': pass @@ -463,18 +483,20 @@ def get_route_table_info(route_table): return route_table_info -def create_route_spec(connection, routes, vpc_id): + +def create_route_spec(connection, module, vpc_id): + routes = module.params.get('routes') for route_spec in routes: rename_key(route_spec, 'dest', 'destination_cidr_block') - if 'gateway_id' in route_spec and route_spec['gateway_id'] and \ - route_spec['gateway_id'].lower() == 'igw': + if route_spec.get('gateway_id') and route_spec['gateway_id'].lower() == 'igw': igw = find_igw(connection, vpc_id) route_spec['gateway_id'] = igw return routes + def ensure_route_table_present(connection, module): lookup = module.params.get('lookup') @@ -484,7 +506,7 @@ def ensure_route_table_present(connection, module): tags = module.params.get('tags') vpc_id = module.params.get('vpc_id') try: - routes = create_route_spec(connection, module.params.get('routes'), vpc_id) + routes = create_route_spec(connection, module, vpc_id) except AnsibleIgwSearchException as e: module.fail_json(msg=e[0]) @@ -565,7 +587,7 @@ def main(): lookup = dict(default='tag', required=False, choices=['tag', 'id']), propagating_vgw_ids = dict(default=None, required=False, type='list'), route_table_id = dict(default=None, required=False), - routes = dict(default=None, required=False, type='list'), + routes = dict(default=[], required=False, type='list'), state = dict(default='present', choices=['present', 'absent']), subnets = dict(default=None, required=False, type='list'), tags = dict(default=None, required=False, type='dict', aliases=['resource_tags']), From 95dc240735795fb4234b7774a3a5ed2681c78d17 Mon Sep 17 00:00:00 2001 From: Alex Paul Date: Wed, 27 Jul 2016 01:10:42 -0700 Subject: [PATCH 1848/2522] Add datadog_monitor param for datadog tags (#2541) * Add datadog_monitor param for datadog tags * Rename tags, add version_added --- monitoring/datadog_monitor.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py index 657c3b64c97..6a7a73b908e 100644 --- a/monitoring/datadog_monitor.py +++ b/monitoring/datadog_monitor.py @@ -34,7 +34,7 @@ - "Manages monitors within Datadog" - "Options like described on http://docs.datadoghq.com/api/" version_added: "2.0" -author: "Sebastian Kornehl (@skornehl)" +author: "Sebastian Kornehl (@skornehl)" notes: [] requirements: [datadog] options: @@ -48,6 +48,11 @@ description: ["The designated state of the monitor."] required: true choices: ['present', 'absent', 'muted', 'unmuted'] + tags: + description: ["A list of tags to associate with your monitor when creating or updating. This can help you categorize and filter monitors."] + required: false + default: None + version_added: 2.2 type: description: - "The type of the monitor." @@ -153,6 +158,7 @@ def main(): escalation_message=dict(required=False, default=None), notify_audit=dict(required=False, default=False, type='bool'), thresholds=dict(required=False, type='dict', default=None), + tags=dict(required=False, type='list', default=None) ) ) @@ -189,9 +195,12 @@ def _get_monitor(module): def _post_monitor(module, options): try: - msg = api.Monitor.create(type=module.params['type'], query=module.params['query'], - name=module.params['name'], message=_fix_template_vars(module.params['message']), - options=options) + kwargs = dict(type=module.params['type'], query=module.params['query'], + name=module.params['name'], message=_fix_template_vars(module.params['message']), + options=options) + if module.params['tags'] is not None: + kwargs['tags'] = module.params['tags'] + msg = api.Monitor.create(**kwargs) if 'errors' in msg: module.fail_json(msg=str(msg['errors'])) else: @@ -206,9 +215,13 @@ def _equal_dicts(a, b, ignore_keys): def _update_monitor(module, monitor, options): try: - msg = api.Monitor.update(id=monitor['id'], query=module.params['query'], - name=module.params['name'], message=_fix_template_vars(module.params['message']), - options=options) + kwargs = dict(id=monitor['id'], query=module.params['query'], + name=module.params['name'], message=_fix_template_vars(module.params['message']), + options=options) + if module.params['tags'] is not None: + kwargs['tags'] = module.params['tags'] + msg = api.Monitor.update(**kwargs) + if 'errors' in msg: module.fail_json(msg=str(msg['errors'])) elif _equal_dicts(msg, monitor, ['creator', 'overall_state', 'modified']): From 1e469477a8464345885b68df95c4ff263485d8ab Mon Sep 17 00:00:00 2001 From: Naoya Nakazawa Date: Wed, 27 Jul 2016 17:11:55 +0900 Subject: [PATCH 1849/2522] [monitor] datadog_monitor add no_log secret key (#2525) --- monitoring/datadog_monitor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py index 6a7a73b908e..6003664fd2d 100644 --- a/monitoring/datadog_monitor.py +++ b/monitoring/datadog_monitor.py @@ -143,8 +143,8 @@ def main(): module = AnsibleModule( argument_spec=dict( - api_key=dict(required=True), - app_key=dict(required=True), + api_key=dict(required=True, no_log=True), + app_key=dict(required=True, no_log=True), state=dict(required=True, choises=['present', 'absent', 'mute', 'unmute']), type=dict(required=False, choises=['metric alert', 'service check', 'event alert']), name=dict(required=True), From df79ca63528b14e06caad3c1a4a38c7531f228ab Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 28 Apr 2016 17:35:37 -0400 Subject: [PATCH 1850/2522] fixes and refactoring of s3_bucket policy should now accept and handle correctly both data structures or JSON strings removed unused tag_set var refactored code to make conditions clearer rebased to allow for ceph changes, left ceph update on todo list --- cloud/amazon/s3_bucket.py | 153 +++++++++++++++++--------------------- 1 file changed, 70 insertions(+), 83 deletions(-) diff --git a/cloud/amazon/s3_bucket.py b/cloud/amazon/s3_bucket.py index 30c0e154242..f8582e0b5f1 100644 --- a/cloud/amazon/s3_bucket.py +++ b/cloud/amazon/s3_bucket.py @@ -103,9 +103,10 @@ tags: example: tag1 another: tag2 - + ''' +import os import xml.etree.ElementTree as ET import urlparse @@ -122,16 +123,13 @@ HAS_BOTO = False def get_request_payment_status(bucket): - + response = bucket.get_request_payment() root = ET.fromstring(response) for message in root.findall('.//{http://s3.amazonaws.com/doc/2006-03-01/}Payer'): payer = message.text - - if payer == "BucketOwner": - return False - else: - return True + + return (payer != "BucketOwner") def create_tags_container(tags): @@ -143,7 +141,7 @@ def create_tags_container(tags): tags_obj.add_tag_set(tag_set) return tags_obj -def _create_bucket(connection, module, location): +def _create_or_update_bucket(connection, module, location): policy = module.params.get("policy") name = module.params.get("name") @@ -151,7 +149,7 @@ def _create_bucket(connection, module, location): tags = module.params.get("tags") versioning = module.params.get("versioning") changed = False - + try: bucket = connection.get_bucket(name) except S3ResponseError as e: @@ -160,42 +158,38 @@ def _create_bucket(connection, module, location): changed = True except S3CreateError as e: module.fail_json(msg=e.message) - + # Versioning versioning_status = bucket.get_versioning_status() - if not versioning_status and versioning: - try: - bucket.configure_versioning(versioning) - changed = True - versioning_status = bucket.get_versioning_status() - except S3ResponseError as e: - module.fail_json(msg=e.message) - elif not versioning_status and not versioning: - # do nothing - pass - else: - if versioning_status['Versioning'] == "Enabled" and not versioning: - bucket.configure_versioning(versioning) - changed = True - versioning_status = bucket.get_versioning_status() - elif ( (versioning_status['Versioning'] == "Disabled" and versioning) or (versioning_status['Versioning'] == "Suspended" and versioning) ): - bucket.configure_versioning(versioning) - changed = True - versioning_status = bucket.get_versioning_status() - + if not versioning_status: + if versioning: + try: + bucket.configure_versioning(versioning) + changed = True + versioning_status = bucket.get_versioning_status() + except S3ResponseError as e: + module.fail_json(msg=e.message) + elif versioning_status['Versioning'] == "Enabled" and not versioning: + bucket.configure_versioning(versioning) + changed = True + versioning_status = bucket.get_versioning_status() + elif ( (versioning_status['Versioning'] == "Disabled" and versioning) or (versioning_status['Versioning'] == "Suspended" and versioning) ): + bucket.configure_versioning(versioning) + changed = True + versioning_status = bucket.get_versioning_status() + # Requester pays requester_pays_status = get_request_payment_status(bucket) if requester_pays_status != requester_pays: if requester_pays: - bucket.set_request_payment(payer='Requester') - changed = True - requester_pays_status = get_request_payment_status(bucket) + payer='Requester' else: - bucket.set_request_payment(payer='BucketOwner') - changed = True - requester_pays_status = get_request_payment_status(bucket) + payer='BucketOwner' + bucket.set_request_payment(payer=payer) + changed = True + requester_pays_status = get_request_payment_status(bucket) - # Policy + # Policy try: current_policy = bucket.get_policy() except S3ResponseError as e: @@ -203,30 +197,25 @@ def _create_bucket(connection, module, location): current_policy = None else: module.fail_json(msg=e.message) - - if current_policy is not None and policy is not None: - if policy is not None: - policy = json.dumps(policy) - - if json.loads(current_policy) != json.loads(policy): + + if policy is not None: + # Deal with policy if either JSON formatted string or just data structure + if isinstance(policy, basestring): + compare_policy = json.dumps(policy) + load_policy = policy + else: + compare_policy = policy + load_policy = json.loads(policy) + + if current_policy is None or json.loads(current_policy) != compare_policy: try: - bucket.set_policy(policy) + bucket.set_policy(load_policy) changed = True current_policy = bucket.get_policy() except S3ResponseError as e: module.fail_json(msg=e.message) + elif current_policy is not None: - elif current_policy is None and policy is not None: - policy = json.dumps(policy) - - try: - bucket.set_policy(policy) - changed = True - current_policy = bucket.get_policy() - except S3ResponseError as e: - module.fail_json(msg=e.message) - - elif current_policy is not None and policy is None: try: bucket.delete_policy() changed = True @@ -236,23 +225,17 @@ def _create_bucket(connection, module, location): current_policy = None else: module.fail_json(msg=e.message) - - #### - ## Fix up json of policy so it's not escaped - #### - # Tags try: current_tags = bucket.get_tags() - tag_set = TagSet() except S3ResponseError as e: if e.error_code == "NoSuchTagSet": current_tags = None else: module.fail_json(msg=e.message) - + if current_tags is not None or tags is not None: - + if current_tags is None: current_tags_dict = {} else: @@ -270,13 +253,13 @@ def _create_bucket(connection, module, location): module.fail_json(msg=e.message) module.exit_json(changed=changed, name=bucket.name, versioning=versioning_status, requester_pays=requester_pays_status, policy=current_policy, tags=current_tags_dict) - + def _destroy_bucket(connection, module): - + force = module.params.get("force") name = module.params.get("name") changed = False - + try: bucket = connection.get_bucket(name) except S3ResponseError as e: @@ -285,25 +268,26 @@ def _destroy_bucket(connection, module): else: # Bucket already absent module.exit_json(changed=changed) - + if force: try: # Empty the bucket for key in bucket.list(): key.delete() - + except BotoServerError as e: module.fail_json(msg=e.message) - + try: bucket = connection.delete_bucket(name) changed = True except S3ResponseError as e: module.fail_json(msg=e.message) - + module.exit_json(changed=changed) -def _create_bucket_ceph(connection, module, location): +def _create_or_update_bucket_ceph(connection, module, location): + #TODO: add update name = module.params.get("name") @@ -318,17 +302,20 @@ def _create_bucket_ceph(connection, module, location): except S3CreateError as e: module.fail_json(msg=e.message) - module.exit_json(changed=changed) + if bucket: + module.exit_json(changed=changed) + else: + module.fail_json(msg='Unable to create bucket, no error from the API') def _destroy_bucket_ceph(connection, module): _destroy_bucket(connection, module) -def create_bucket(connection, module, location, flavour='aws'): +def create_or_update_bucket(connection, module, location, flavour='aws'): if flavour == 'ceph': - _create_bucket_ceph(connection, module, location) + _create_or_update_bucket_ceph(connection, module, location) else: - _create_bucket(connection, module, location) + _create_or_update_bucket(connection, module, location) def destroy_bucket(connection, module, flavour='aws'): if flavour == 'ceph': @@ -354,27 +341,27 @@ def is_walrus(s3_url): return False def main(): - + argument_spec = ec2_argument_spec() argument_spec.update( dict( force = dict(required=False, default='no', type='bool'), - policy = dict(required=False, default=None), - name = dict(required=True), + policy = dict(required=False), + name = dict(required=True, type='str'), requester_pays = dict(default='no', type='bool'), - s3_url = dict(aliases=['S3_URL']), - state = dict(default='present', choices=['present', 'absent']), + s3_url = dict(aliases=['S3_URL'], type='str'), + state = dict(default='present', type='str', choices=['present', 'absent']), tags = dict(required=None, default={}, type='dict'), versioning = dict(default='no', type='bool'), ceph = dict(default='no', type='bool') ) ) - + module = AnsibleModule(argument_spec=argument_spec) if not HAS_BOTO: module.fail_json(msg='boto required for this module') - + region, ec2_url, aws_connect_params = get_aws_connection_info(module) if region in ('us-east-1', '', None): @@ -440,7 +427,7 @@ def main(): state = module.params.get("state") if state == 'present': - create_bucket(connection, module, location, flavour=flavour) + create_or_update_bucket(connection, module, location) elif state == 'absent': destroy_bucket(connection, module, flavour=flavour) From 6402d36af3db7cc556cca0dd6fa31e75a8e24013 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 26 Jul 2016 09:52:16 -0400 Subject: [PATCH 1851/2522] now using type=json which takes care of str/dict --- cloud/amazon/s3_bucket.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/cloud/amazon/s3_bucket.py b/cloud/amazon/s3_bucket.py index f8582e0b5f1..7b283bf313e 100644 --- a/cloud/amazon/s3_bucket.py +++ b/cloud/amazon/s3_bucket.py @@ -199,13 +199,7 @@ def _create_or_update_bucket(connection, module, location): module.fail_json(msg=e.message) if policy is not None: - # Deal with policy if either JSON formatted string or just data structure - if isinstance(policy, basestring): - compare_policy = json.dumps(policy) - load_policy = policy - else: - compare_policy = policy - load_policy = json.loads(policy) + compare_policy = json.loads(policy) if current_policy is None or json.loads(current_policy) != compare_policy: try: @@ -346,7 +340,7 @@ def main(): argument_spec.update( dict( force = dict(required=False, default='no', type='bool'), - policy = dict(required=False), + policy = dict(required=False, type='json'), name = dict(required=True, type='str'), requester_pays = dict(default='no', type='bool'), s3_url = dict(aliases=['S3_URL'], type='str'), From 8449fb3c900f1c20f7a39af713210c355ef2f27b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc?= Date: Wed, 27 Jul 2016 23:16:15 +0200 Subject: [PATCH 1852/2522] Fix argument type to bool. By default shutdown_sessions is always true then it should'nt. (#2596) --- network/haproxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/haproxy.py b/network/haproxy.py index f9b7661243d..2fc11987d50 100644 --- a/network/haproxy.py +++ b/network/haproxy.py @@ -330,7 +330,7 @@ def main(): backend=dict(required=False, default=None), weight=dict(required=False, default=None), socket = dict(required=False, default=DEFAULT_SOCKET_LOCATION), - shutdown_sessions=dict(required=False, default=False), + shutdown_sessions=dict(required=False, default=False, type='bool'), fail_on_not_found=dict(required=False, default=False, type='bool'), wait=dict(required=False, default=False, type='bool'), wait_retries=dict(required=False, default=WAIT_RETRIES, type='int'), From 866b04784d9413a8aff21960f6088336210117a6 Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Thu, 28 Jul 2016 00:27:05 +0200 Subject: [PATCH 1853/2522] Add os_recordset module (#2240) This module allows to manage OpenStack Designate recordsets. --- cloud/openstack/os_recordset.py | 242 ++++++++++++++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 cloud/openstack/os_recordset.py diff --git a/cloud/openstack/os_recordset.py b/cloud/openstack/os_recordset.py new file mode 100644 index 00000000000..0e860207166 --- /dev/null +++ b/cloud/openstack/os_recordset.py @@ -0,0 +1,242 @@ +#!/usr/bin/python +# Copyright (c) 2016 Hewlett-Packard Enterprise +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + + +try: + import shade + HAS_SHADE = True +except ImportError: + HAS_SHADE = False + +from distutils.version import StrictVersion + +DOCUMENTATION = ''' +--- +module: os_recordset +short_description: Manage OpenStack DNS recordsets +extends_documentation_fragment: openstack +version_added: "2.2" +author: "Ricardo Carrillo Cruz (@rcarrillocruz)" +description: + - Manage OpenStack DNS recordsets. Recordsets can be created, deleted or + updated. Only the I(records), I(description), and I(ttl) values + can be updated. +options: + zone: + description: + - Zone managing the recordset + required: true + name: + description: + - Name of the recordset + required: true + recordset_type: + description: + - Recordset type + required: true + records: + description: + - List of recordset definitions + required: true + description: + description: + - Description of the recordset + required: false + default: None + ttl: + description: + - TTL (Time To Live) value in seconds + required: false + default: None + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present +requirements: + - "python >= 2.6" + - "shade" +''' + +EXAMPLES = ''' +# Create a recordset named "www.example.net." +- os_recordset: + cloud: mycloud + state: present + zone: example.net. + name: www + recordset_type: primary + records: ['10.1.1.1'] + description: test recordset + ttl: 3600 + +# Update the TTL on existing "www.example.net." recordset +- os_recordset: + cloud: mycloud + state: present + zone: example.net. + name: www + ttl: 7200 + +# Delete recorset named "www.example.net." +- os_recordset: + cloud: mycloud + state: absent + zone: example.net. + name: www +''' + +RETURN = ''' +recordset: + description: Dictionary describing the recordset. + returned: On success when I(state) is 'present'. + type: dictionary + contains: + id: + description: Unique recordset ID + type: string + sample: "c1c530a3-3619-46f3-b0f6-236927b2618c" + name: + description: Recordset name + type: string + sample: "www.example.net." + zone_id: + description: Zone id + type: string + sample: 9508e177-41d8-434e-962c-6fe6ca880af7 + type: + description: Recordset type + type: string + sample: "A" + description: + description: Recordset description + type: string + sample: "Test description" + ttl: + description: Zone TTL value + type: int + sample: 3600 + records: + description: Recordset records + type: list + sample: ['10.0.0.1'] +''' + + +def _system_state_change(state, records, description, ttl, zone, recordset): + if state == 'present': + if recordset is None: + return True + if records is not None and recordset.records != records: + return True + if description is not None and recordset.description != description: + return True + if ttl is not None and recordset.ttl != ttl: + return True + if state == 'absent' and recordset: + return True + return False + +def main(): + argument_spec = openstack_full_argument_spec( + zone=dict(required=True), + name=dict(required=True), + recordset_type=dict(required=False), + records=dict(required=False, type='list'), + description=dict(required=False, default=None), + ttl=dict(required=False, default=None, type='int'), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = openstack_module_kwargs() + module = AnsibleModule(argument_spec, + required_if=[ + ('state', 'present', + ['recordset_type', 'records'])], + supports_check_mode=True, + **module_kwargs) + + if not HAS_SHADE: + module.fail_json(msg='shade is required for this module') + if StrictVersion(shade.__version__) <= StrictVersion('1.8.0'): + module.fail_json(msg="To utilize this module, the installed version of " + "the shade library MUST be >1.8.0") + + zone = module.params.get('zone') + name = module.params.get('name') + state = module.params.get('state') + + try: + cloud = shade.openstack_cloud(**module.params) + recordset = cloud.get_recordset(zone, name + '.' + zone) + + + if state == 'present': + recordset_type = module.params.get('recordset_type') + records = module.params.get('records') + description = module.params.get('description') + ttl = module.params.get('ttl') + + if module.check_mode: + module.exit_json(changed=_system_state_change(state, + records, description, + ttl, zone, + recordset)) + + if recordset is None: + recordset = cloud.create_recordset( + zone=zone, name=name, recordset_type=recordset_type, + records=records, description=description, ttl=ttl) + changed = True + else: + if records is None: + records = [] + + pre_update_recordset = recordset + changed = _system_state_change(state, records, + description, ttl, + zone, pre_update_recordset) + if changed: + zone = cloud.update_recordset( + zone, name + '.' + zone, + records=records, + description=description, + ttl=ttl) + module.exit_json(changed=changed, recordset=recordset) + + elif state == 'absent': + if module.check_mode: + module.exit_json(changed=_system_state_change(state, + None, None, + None, + None, recordset)) + + if recordset is None: + changed=False + else: + cloud.delete_recordset(zone, name + '.' + zone) + changed=True + module.exit_json(changed=changed) + + except shade.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + + +from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * + +if __name__ == '__main__': + main() From 8ad0c52a822a42f0ef6e4b12d265e7c576e987a3 Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Thu, 28 Jul 2016 00:27:20 +0200 Subject: [PATCH 1854/2522] Add os_zone module (#2173) This module allows to create OpenStack Designate zone objects --- cloud/openstack/os_zone.py | 237 +++++++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 cloud/openstack/os_zone.py diff --git a/cloud/openstack/os_zone.py b/cloud/openstack/os_zone.py new file mode 100644 index 00000000000..0a0e7ed3dae --- /dev/null +++ b/cloud/openstack/os_zone.py @@ -0,0 +1,237 @@ +#!/usr/bin/python +# Copyright (c) 2016 Hewlett-Packard Enterprise +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + + +try: + import shade + HAS_SHADE = True +except ImportError: + HAS_SHADE = False + +from distutils.version import StrictVersion + +DOCUMENTATION = ''' +--- +module: os_zone +short_description: Manage OpenStack DNS zones +extends_documentation_fragment: openstack +version_added: "2.2" +author: "Ricardo Carrillo Cruz (@rcarrillocruz)" +description: + - Manage OpenStack DNS zones. Zones can be created, deleted or + updated. Only the I(email), I(description), I(ttl) and I(masters) values + can be updated. +options: + name: + description: + - Zone name + required: true + zone_type: + description: + - Zone type + choices: [primary, secondary] + default: None + email: + description: + - Email of the zone owner (only applies if zone_type is primary) + required: false + description: + description: + - Zone description + required: false + default: None + ttl: + description: + - TTL (Time To Live) value in seconds + required: false + default: None + masters: + description: + - Master nameservers (only applies if zone_type is secondary) + required: false + default: None + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present +requirements: + - "python >= 2.6" + - "shade" +''' + +EXAMPLES = ''' +# Create a zone named "example.net" +- os_zone: + cloud: mycloud + state: present + name: example.net. + zone_type: primary + email: test@example.net + description: Test zone + ttl: 3600 + +# Update the TTL on existing "example.net." zone +- os_zone: + cloud: mycloud + state: present + name: example.net. + ttl: 7200 + +# Delete zone named "example.net." +- os_zone: + cloud: mycloud + state: absent + name: example.net. +''' + +RETURN = ''' +zone: + description: Dictionary describing the zone. + returned: On success when I(state) is 'present'. + type: dictionary + contains: + id: + description: Unique zone ID + type: string + sample: "c1c530a3-3619-46f3-b0f6-236927b2618c" + name: + description: Zone name + type: string + sample: "example.net." + type: + description: Zone type + type: string + sample: "PRIMARY" + email: + description: Zone owner email + type: string + sample: "test@example.net" + description: + description: Zone description + type: string + sample: "Test description" + ttl: + description: Zone TTL value + type: int + sample: 3600 + masters: + description: Zone master nameservers + type: list + sample: [] +''' + + +def _system_state_change(state, email, description, ttl, masters, zone): + if state == 'present': + if not zone: + return True + if email is not None and zone.email != email: + return True + if description is not None and zone.description != description: + return True + if ttl is not None and zone.ttl != ttl: + return True + if masters is not None and zone.masters != masters: + return True + if state == 'absent' and zone: + return True + return False + +def main(): + argument_spec = openstack_full_argument_spec( + name=dict(required=True), + zone_type=dict(required=False, choice=['primary', 'secondary']), + email=dict(required=False, default=None), + description=dict(required=False, default=None), + ttl=dict(required=False, default=None, type='int'), + masters=dict(required=False, default=None, type='list'), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = openstack_module_kwargs() + module = AnsibleModule(argument_spec, + supports_check_mode=True, + **module_kwargs) + + if not HAS_SHADE: + module.fail_json(msg='shade is required for this module') + if StrictVersion(shade.__version__) < StrictVersion('1.8.0'): + module.fail_json(msg="To utilize this module, the installed version of" + "the shade library MUST be >=1.8.0") + + name = module.params.get('name') + state = module.params.get('state') + + try: + cloud = shade.openstack_cloud(**module.params) + zone = cloud.get_zone(name) + + + if state == 'present': + zone_type = module.params.get('zone_type') + email = module.params.get('email') + description = module.params.get('description') + ttl = module.params.get('ttl') + masters = module.params.get('masters') + + if module.check_mode: + module.exit_json(changed=_system_state_change(state, email, + description, ttl, + masters, zone)) + + if zone is None: + zone = cloud.create_zone( + name=name, zone_type=zone_type, email=email, + description=description, ttl=ttl, masters=masters) + changed = True + else: + if masters is None: + masters = [] + + pre_update_zone = zone + changed = _system_state_change(state, email, + description, ttl, + masters, pre_update_zone) + if changed: + zone = cloud.update_zone( + name, email=email, + description=description, + ttl=ttl, masters=masters) + module.exit_json(changed=changed, zone=zone) + + elif state == 'absent': + if module.check_mode: + module.exit_json(changed=_system_state_change(state, None, + None, None, + None, zone)) + + if zone is None: + changed=False + else: + cloud.delete_zone(name) + changed=True + module.exit_json(changed=changed) + + except shade.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + + +from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * + +if __name__ == '__main__': + main() From a767da11393d495db589be2947f4065df5a4356d Mon Sep 17 00:00:00 2001 From: pascalheraud Date: Thu, 3 Dec 2015 23:59:14 +0100 Subject: [PATCH 1855/2522] Added OVH Ip loadbalancing module for managing backends --- cloud/ovh/__init__.py | 0 cloud/ovh/ovh_ip_loadbalancing_backend.py | 166 ++++++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 cloud/ovh/__init__.py create mode 100644 cloud/ovh/ovh_ip_loadbalancing_backend.py diff --git a/cloud/ovh/__init__.py b/cloud/ovh/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cloud/ovh/ovh_ip_loadbalancing_backend.py b/cloud/ovh/ovh_ip_loadbalancing_backend.py new file mode 100644 index 00000000000..ce100b90ac6 --- /dev/null +++ b/cloud/ovh/ovh_ip_loadbalancing_backend.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +DOCUMENTATION = ''' +--- +module: ovh_ip_loadbalancing_backend +short_description: Manage OVH IP LoadBalancing backends +description: + - Manage OVH (French European hosting provider) LoadBalancing IP backends +version_added: "1.9" +author: Pascal HERAUD @pascalheraud +notes: + - Uses the python OVH Api U(https://github.com/ovh/python-ovh). You have to create an application (a key and secret) with a consummer key as described into U(https://eu.api.ovh.com/g934.first_step_with_api) +requirements: + - ovh > 0.35 +options: + name: + required: true + description: + - Name of the LoadBalancing internal name (ip-X.X.X.X) + backend: + required: true + description: + - The IP address of the backend to update / modify / delete + state: + required: false + default: present + choices: ['present', 'absent'] + description: + - Determines wether the backend is to be created/modified or deleted + probe: + required: false + default: none + choices: ['none', 'http', 'icmp' , 'oco'] + description: + - Determines the type of probe to use for this backend + weight: + required: false + default: 8 + description: + - Determines the weight for this backend + endpoint: + required: true + description: + - The endpoint to use ( for instance ovh-eu) + application_key: + required: true + description: + - The applicationKey to use + application_secret: + required: true + description: + - The application secret to use + consumer_key: + required: true + description: + - The consumer key to use + +''' + +EXAMPLES = ''' +# Adds or modify a backend to a loadbalancing +- ovh_ip_loadbalancing name=ip-1.1.1.1 ip=212.1.1.1 state=present probe=none weight=8 endpoint=ovh-eu application_key=yourkey application_secret=yoursecret consumer_key=yourconsumerkey + +# Removes a backend from a loadbalancing +- ovh_ip_loadbalancing name=ip-1.1.1.1 ip=212.1.1.1 state=absent endpoint=ovh-eu application_key=yourkey application_secret=yoursecret consumer_key=yourconsumerkey +''' + +import sys +import ovh + +def getOvhClient(ansibleModule): + endpoint = ansibleModule.params.get('endpoint') + application_key = ansibleModule.params.get('application_key') + application_secret = ansibleModule.params.get('application_secret') + consumer_key = ansibleModule.params.get('consumer_key') + + return ovh.Client( + endpoint=endpoint, + application_key=application_key, + application_secret=application_secret, + consumer_key=consumer_key + ) + +def waitForNoTask(client, name): + while len(client.get('/ip/loadBalancing/{}/task'.format(name)))>0: + time.sleep(1) # Delay for 1 sec + +def main(): + module = AnsibleModule( + argument_spec = dict( + name = dict(required=True), + backend = dict(required=True), + weight = dict(default='8'), + probe = dict(default='none', choices =['none', 'http', 'icmp' , 'oco']), + state = dict(default='present', choices=['present', 'absent']), + endpoint = dict(required=True), + application_key = dict(required=True), + application_secret = dict(required=True), + consumer_key = dict(required=True), + ) + ) + + # Get parameters + name = module.params.get('name') + state = module.params.get('state') + backend = module.params.get('backend') + weight = long(module.params.get('weight')) + probe = module.params.get('probe') + + # Connect to OVH API + client = getOvhClient(module) + + # Check that the load balancing exists + loadBalancings = client.get('/ip/loadBalancing') + if not name in loadBalancings: + module.fail_json(msg='IP LoadBalancing {} does not exist'.format(name)) + + # Check that no task is pending before going on + waitForNoTask(client, name) + + backends = client.get('/ip/loadBalancing/{}/backend'.format(name)) + + backendExists = backend in backends + moduleChanged = False + if (state=="absent") : + if (backendExists) : + # Remove backend + client.delete('/ip/loadBalancing/{}/backend/{}'.format(name, backend)) + waitForNoTask(client, name) + moduleChanged = True + else : + moduleChanged = False + if (state=="present") : + if (backendExists) : + moduleChanged = False + # Get properties + backendProperties = client.get('/ip/loadBalancing/{}/backend/{}'.format(name, backend)) + if (backendProperties['weight'] != weight): + # Change weight + client.post('/ip/loadBalancing/{}/backend/{}/setWeight'.format(name, backend), weight=weight) + waitForNoTask(client, name) + moduleChanged = True + if (backendProperties['probe'] != probe): + # Change probe + backendProperties['probe'] = probe + client.put('/ip/loadBalancing/{}/backend/{}'.format(name, backend), probe=probe ) + waitForNoTask(client, name) + moduleChanged = True + + else : + # Creates backend + client.post('/ip/loadBalancing/{}/backend'.format(name), ipBackend=backend, probe=probe, weight=weight) + waitForNoTask(client, name) + moduleChanged = True + + module.exit_json(changed=moduleChanged) + + # We should never reach here + module.fail_json(msg='Internal ovh_ip_loadbalancing_backend module error') + + +# import module snippets +from ansible.module_utils.basic import * + +main() From 37900daf7919e1f2c4ce19f48420cda37b639c89 Mon Sep 17 00:00:00 2001 From: pascalheraud Date: Sat, 5 Dec 2015 17:57:57 +0100 Subject: [PATCH 1856/2522] Fixed module from review inputs : - Caught the exception from import ovh to provide a proper message to the user - Removed unuseful brackets - Added a else to check the state instead of two if - Changed the module to be added to 2.0 - Added exceptions handling for all APIs calls with a clear message including the return from the API. And : - Fixed dependency of OVH api to 0.3.5 --- cloud/ovh/ovh_ip_loadbalancing_backend.py | 62 ++++++++++++++++------- 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/cloud/ovh/ovh_ip_loadbalancing_backend.py b/cloud/ovh/ovh_ip_loadbalancing_backend.py index ce100b90ac6..0375d65b87c 100644 --- a/cloud/ovh/ovh_ip_loadbalancing_backend.py +++ b/cloud/ovh/ovh_ip_loadbalancing_backend.py @@ -1,5 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +from ovh.exceptions import APIError DOCUMENTATION = ''' --- @@ -7,12 +8,12 @@ short_description: Manage OVH IP LoadBalancing backends description: - Manage OVH (French European hosting provider) LoadBalancing IP backends -version_added: "1.9" +version_added: "2.0" author: Pascal HERAUD @pascalheraud notes: - Uses the python OVH Api U(https://github.com/ovh/python-ovh). You have to create an application (a key and secret) with a consummer key as described into U(https://eu.api.ovh.com/g934.first_step_with_api) requirements: - - ovh > 0.35 + - ovh > 0.3.5 options: name: required: true @@ -67,7 +68,12 @@ ''' import sys -import ovh +try: + import ovh + import ovh.exceptions + HAS_OVH = True +except ImportError: + HAS_OVH = False def getOvhClient(ansibleModule): endpoint = ansibleModule.params.get('endpoint') @@ -100,6 +106,9 @@ def main(): consumer_key = dict(required=True), ) ) + + if not HAS_OVH: + module.fail_json(msg='ovh-api python module is required to run this module ') # Get parameters name = module.params.get('name') @@ -112,46 +121,65 @@ def main(): client = getOvhClient(module) # Check that the load balancing exists - loadBalancings = client.get('/ip/loadBalancing') + try : + loadBalancings = client.get('/ip/loadBalancing') + except APIError as apiError: + module.fail_json(msg='Unable to call OVH api for getting the list of loadBalancing, check application key, secret, consumerkey and parameters. Error returned by OVH api was : {}'.format(apiError)) + if not name in loadBalancings: module.fail_json(msg='IP LoadBalancing {} does not exist'.format(name)) # Check that no task is pending before going on waitForNoTask(client, name) - backends = client.get('/ip/loadBalancing/{}/backend'.format(name)) + try : + backends = client.get('/ip/loadBalancing/{}/backend'.format(name)) + except APIError as apiError: + module.fail_json(msg='Unable to call OVH api for getting the list of backends of the loadBalancing, check application key, secret, consumerkey and parameters. Error returned by OVH api was : {}'.format(apiError)) backendExists = backend in backends moduleChanged = False - if (state=="absent") : - if (backendExists) : + if state=="absent" : + if backendExists : # Remove backend - client.delete('/ip/loadBalancing/{}/backend/{}'.format(name, backend)) - waitForNoTask(client, name) + try : + client.delete('/ip/loadBalancing/{}/backend/{}'.format(name, backend)) + waitForNoTask(client, name) + except : + module.fail_json(msg='Unable to call OVH api for deleting the backend, check application key, secret, consumerkey and parameters. Error returned by OVH api was : {}'.format(apiError)) moduleChanged = True else : moduleChanged = False - if (state=="present") : - if (backendExists) : + else : + if backendExists : moduleChanged = False # Get properties backendProperties = client.get('/ip/loadBalancing/{}/backend/{}'.format(name, backend)) if (backendProperties['weight'] != weight): # Change weight - client.post('/ip/loadBalancing/{}/backend/{}/setWeight'.format(name, backend), weight=weight) - waitForNoTask(client, name) + try : + client.post('/ip/loadBalancing/{}/backend/{}/setWeight'.format(name, backend), weight=weight) + waitForNoTask(client, name) + except APIError as apiError: + module.fail_json(msg='Unable to call OVH api for updating the weight of the backend, check application key, secret, consumerkey and parameters. Error returned by OVH api was : {}'.format(apiError)) moduleChanged = True if (backendProperties['probe'] != probe): # Change probe backendProperties['probe'] = probe - client.put('/ip/loadBalancing/{}/backend/{}'.format(name, backend), probe=probe ) - waitForNoTask(client, name) + try: + client.put('/ip/loadBalancing/{}/backend/{}'.format(name, backend), probe=probe ) + waitForNoTask(client, name) + except APIError as apiError: + module.fail_json(msg='Unable to call OVH api for updating the propbe of the backend, check application key, secret, consumerkey and parameters. Error returned by OVH api was : {}'.format(apiError)) moduleChanged = True else : # Creates backend - client.post('/ip/loadBalancing/{}/backend'.format(name), ipBackend=backend, probe=probe, weight=weight) - waitForNoTask(client, name) + try: + client.post('/ip/loadBalancing/{}/backend'.format(name), ipBackend=backend, probe=probe, weight=weight) + waitForNoTask(client, name) + except APIError as apiError: + module.fail_json(msg='Unable to call OVH api for creating the backend, check application key, secret, consumerkey and parameters. Error returned by OVH api was : {}'.format(apiError)) moduleChanged = True module.exit_json(changed=moduleChanged) From f7a50130837c13ef437e141702f0387353bf93f5 Mon Sep 17 00:00:00 2001 From: pascalheraud Date: Sun, 6 Dec 2015 18:47:10 +0100 Subject: [PATCH 1857/2522] Added a timeout param to prevent infinite loop while waiting for completion of a task. --- cloud/ovh/ovh_ip_loadbalancing_backend.py | 36 +++++++++++++++++------ 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/cloud/ovh/ovh_ip_loadbalancing_backend.py b/cloud/ovh/ovh_ip_loadbalancing_backend.py index 0375d65b87c..4af85c3241b 100644 --- a/cloud/ovh/ovh_ip_loadbalancing_backend.py +++ b/cloud/ovh/ovh_ip_loadbalancing_backend.py @@ -56,6 +56,12 @@ required: true description: - The consumer key to use + timeout: + required: false + type: "int" + default: "120 + descriptin: + - The timeout in seconds used to wait for a task to be completed. Default is 120 seconds. ''' @@ -88,22 +94,28 @@ def getOvhClient(ansibleModule): consumer_key=consumer_key ) -def waitForNoTask(client, name): +def waitForNoTask(client, name, timeout): + currentTimeout = timeout; while len(client.get('/ip/loadBalancing/{}/task'.format(name)))>0: time.sleep(1) # Delay for 1 sec + currentTimeout-=1 + if currentTimeout < 0: + return False + return True def main(): module = AnsibleModule( argument_spec = dict( name = dict(required=True), backend = dict(required=True), - weight = dict(default='8'), + weight = dict(default='8', type='int'), probe = dict(default='none', choices =['none', 'http', 'icmp' , 'oco']), state = dict(default='present', choices=['present', 'absent']), endpoint = dict(required=True), application_key = dict(required=True), application_secret = dict(required=True), consumer_key = dict(required=True), + timeout = dict(default=120, type='int') ) ) @@ -116,6 +128,7 @@ def main(): backend = module.params.get('backend') weight = long(module.params.get('weight')) probe = module.params.get('probe') + timeout = module.params.get('timeout') # Connect to OVH API client = getOvhClient(module) @@ -130,7 +143,8 @@ def main(): module.fail_json(msg='IP LoadBalancing {} does not exist'.format(name)) # Check that no task is pending before going on - waitForNoTask(client, name) + if not waitForNoTask(client, name, timeout): + module.fail_json(msg='Timeout of {} seconds while waiting for no pending tasks before executing the module '.format(timeout)) try : backends = client.get('/ip/loadBalancing/{}/backend'.format(name)) @@ -144,8 +158,9 @@ def main(): # Remove backend try : client.delete('/ip/loadBalancing/{}/backend/{}'.format(name, backend)) - waitForNoTask(client, name) - except : + if not waitForNoTask(client, name, timeout): + module.fail_json(msg='Timeout of {} seconds while waiting for completion of removing backend task'.format(timeout)) + except APIError as apiError: module.fail_json(msg='Unable to call OVH api for deleting the backend, check application key, secret, consumerkey and parameters. Error returned by OVH api was : {}'.format(apiError)) moduleChanged = True else : @@ -158,8 +173,9 @@ def main(): if (backendProperties['weight'] != weight): # Change weight try : - client.post('/ip/loadBalancing/{}/backend/{}/setWeight'.format(name, backend), weight=weight) - waitForNoTask(client, name) + client.post('/ip/loadBalancing/{}/backend/{}/setWeight'.format(name, backend), weight=weight) + if not waitForNoTask(client, name, timeout): + module.fail_json(msg='Timeout of {} seconds while waiting for completion of setWeight to backend task'.format(timeout)) except APIError as apiError: module.fail_json(msg='Unable to call OVH api for updating the weight of the backend, check application key, secret, consumerkey and parameters. Error returned by OVH api was : {}'.format(apiError)) moduleChanged = True @@ -168,7 +184,8 @@ def main(): backendProperties['probe'] = probe try: client.put('/ip/loadBalancing/{}/backend/{}'.format(name, backend), probe=probe ) - waitForNoTask(client, name) + if not waitForNoTask(client, name, timeout): + module.fail_json(msg='Timeout of {} seconds while waiting for completion of setProbe to backend task'.format(timeout)) except APIError as apiError: module.fail_json(msg='Unable to call OVH api for updating the propbe of the backend, check application key, secret, consumerkey and parameters. Error returned by OVH api was : {}'.format(apiError)) moduleChanged = True @@ -177,7 +194,8 @@ def main(): # Creates backend try: client.post('/ip/loadBalancing/{}/backend'.format(name), ipBackend=backend, probe=probe, weight=weight) - waitForNoTask(client, name) + if not waitForNoTask(client, name, timeout): + module.fail_json(msg='Timeout of {} seconds while waiting for completion of backend creation task'.format(timeout)) except APIError as apiError: module.fail_json(msg='Unable to call OVH api for creating the backend, check application key, secret, consumerkey and parameters. Error returned by OVH api was : {}'.format(apiError)) moduleChanged = True From 1972df5a710fcbb3e3655eba2c632c17c92131ce Mon Sep 17 00:00:00 2001 From: pascalheraud Date: Sun, 6 Dec 2015 19:04:51 +0100 Subject: [PATCH 1858/2522] Removed unnecessary moduleChanged=False Added missing exceptions handling --- cloud/ovh/ovh_ip_loadbalancing_backend.py | 25 +++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/cloud/ovh/ovh_ip_loadbalancing_backend.py b/cloud/ovh/ovh_ip_loadbalancing_backend.py index 4af85c3241b..f2dcc1d76ac 100644 --- a/cloud/ovh/ovh_ip_loadbalancing_backend.py +++ b/cloud/ovh/ovh_ip_loadbalancing_backend.py @@ -101,7 +101,7 @@ def waitForNoTask(client, name, timeout): currentTimeout-=1 if currentTimeout < 0: return False - return True + return True def main(): module = AnsibleModule( @@ -143,8 +143,11 @@ def main(): module.fail_json(msg='IP LoadBalancing {} does not exist'.format(name)) # Check that no task is pending before going on - if not waitForNoTask(client, name, timeout): - module.fail_json(msg='Timeout of {} seconds while waiting for no pending tasks before executing the module '.format(timeout)) + try : + if not waitForNoTask(client, name, timeout): + module.fail_json(msg='Timeout of {} seconds while waiting for no pending tasks before executing the module '.format(timeout)) + except APIError as apiError: + module.fail_json(msg='Unable to call OVH api for getting the list of pending tasks of the loadBalancing, check application key, secret, consumerkey and parameters. Error returned by OVH api was : {}'.format(apiError)) try : backends = client.get('/ip/loadBalancing/{}/backend'.format(name)) @@ -163,13 +166,14 @@ def main(): except APIError as apiError: module.fail_json(msg='Unable to call OVH api for deleting the backend, check application key, secret, consumerkey and parameters. Error returned by OVH api was : {}'.format(apiError)) moduleChanged = True - else : - moduleChanged = False else : if backendExists : - moduleChanged = False # Get properties - backendProperties = client.get('/ip/loadBalancing/{}/backend/{}'.format(name, backend)) + try : + backendProperties = client.get('/ip/loadBalancing/{}/backend/{}'.format(name, backend)) + except APIError as apiError: + module.fail_json(msg='Unable to call OVH api for getting the backend properties, check application key, secret, consumerkey and parameters. Error returned by OVH api was : {}'.format(apiError)) + if (backendProperties['weight'] != weight): # Change weight try : @@ -179,6 +183,7 @@ def main(): except APIError as apiError: module.fail_json(msg='Unable to call OVH api for updating the weight of the backend, check application key, secret, consumerkey and parameters. Error returned by OVH api was : {}'.format(apiError)) moduleChanged = True + if (backendProperties['probe'] != probe): # Change probe backendProperties['probe'] = probe @@ -193,7 +198,11 @@ def main(): else : # Creates backend try: - client.post('/ip/loadBalancing/{}/backend'.format(name), ipBackend=backend, probe=probe, weight=weight) + try: + client.post('/ip/loadBalancing/{}/backend'.format(name), ipBackend=backend, probe=probe, weight=weight) + except APIError as apiError: + module.fail_json(msg='Unable to call OVH api for creating the backend, check application key, secret, consumerkey and parameters. Error returned by OVH api was : {}'.format(apiError)) + if not waitForNoTask(client, name, timeout): module.fail_json(msg='Timeout of {} seconds while waiting for completion of backend creation task'.format(timeout)) except APIError as apiError: From 7c9fdba5adcc04d46c2ca4ee6c26230eee9feaad Mon Sep 17 00:00:00 2001 From: pascalheraud Date: Tue, 8 Dec 2015 00:20:18 +0100 Subject: [PATCH 1859/2522] Fixed the module to be compliant with pep8 --- cloud/ovh/ovh_ip_loadbalancing_backend.py | 244 ++++++++++++++-------- 1 file changed, 154 insertions(+), 90 deletions(-) diff --git a/cloud/ovh/ovh_ip_loadbalancing_backend.py b/cloud/ovh/ovh_ip_loadbalancing_backend.py index f2dcc1d76ac..e3b80617b74 100644 --- a/cloud/ovh/ovh_ip_loadbalancing_backend.py +++ b/cloud/ovh/ovh_ip_loadbalancing_backend.py @@ -1,5 +1,16 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +import sys +try: + import ovh + import ovh.exceptions + HAS_OVH = True +except ImportError: + HAS_OVH = False + +# import module snippets +from ansible.module_utils.basic import * +# and APIError from ovh api from ovh.exceptions import APIError DOCUMENTATION = ''' @@ -11,8 +22,10 @@ version_added: "2.0" author: Pascal HERAUD @pascalheraud notes: - - Uses the python OVH Api U(https://github.com/ovh/python-ovh). You have to create an application (a key and secret) with a consummer key as described into U(https://eu.api.ovh.com/g934.first_step_with_api) -requirements: + - Uses the python OVH Api U(https://github.com/ovh/python-ovh). \ + You have to create an application (a key and secret) with a consummer \ + key as described into U(https://eu.api.ovh.com/g934.first_step_with_api) +requirements: - ovh > 0.3.5 options: name: @@ -28,7 +41,8 @@ default: present choices: ['present', 'absent'] description: - - Determines wether the backend is to be created/modified or deleted + - Determines wether the backend is to be created/modified \ + or deleted probe: required: false default: none @@ -59,33 +73,32 @@ timeout: required: false type: "int" - default: "120 - descriptin: - - The timeout in seconds used to wait for a task to be completed. Default is 120 seconds. - + default: 120 + description: + - The timeout in seconds used to wait for a task to be \ + completed. Default is 120 seconds. + ''' EXAMPLES = ''' # Adds or modify a backend to a loadbalancing -- ovh_ip_loadbalancing name=ip-1.1.1.1 ip=212.1.1.1 state=present probe=none weight=8 endpoint=ovh-eu application_key=yourkey application_secret=yoursecret consumer_key=yourconsumerkey +- ovh_ip_loadbalancing name=ip-1.1.1.1 ip=212.1.1.1 state=present \ +probe=none weight=8 \ +endpoint=ovh-eu application_key=yourkey \ +application_secret=yoursecret consumer_key=yourconsumerkey # Removes a backend from a loadbalancing -- ovh_ip_loadbalancing name=ip-1.1.1.1 ip=212.1.1.1 state=absent endpoint=ovh-eu application_key=yourkey application_secret=yoursecret consumer_key=yourconsumerkey +- ovh_ip_loadbalancing name=ip-1.1.1.1 ip=212.1.1.1 state=absent \ +endpoint=ovh-eu application_key=yourkey \ +application_secret=yoursecret consumer_key=yourconsumerkey ''' -import sys -try: - import ovh - import ovh.exceptions - HAS_OVH = True -except ImportError: - HAS_OVH = False def getOvhClient(ansibleModule): - endpoint = ansibleModule.params.get('endpoint') - application_key = ansibleModule.params.get('application_key') - application_secret = ansibleModule.params.get('application_secret') - consumer_key = ansibleModule.params.get('consumer_key') + endpoint = ansibleModule.params.get('endpoint') + application_key = ansibleModule.params.get('application_key') + application_secret = ansibleModule.params.get('application_secret') + consumer_key = ansibleModule.params.get('consumer_key') return ovh.Client( endpoint=endpoint, @@ -94,128 +107,179 @@ def getOvhClient(ansibleModule): consumer_key=consumer_key ) + def waitForNoTask(client, name, timeout): - currentTimeout = timeout; - while len(client.get('/ip/loadBalancing/{}/task'.format(name)))>0: + currentTimeout = timeout + while len(client.get('/ip/loadBalancing/{}/task'.format(name))) > 0: time.sleep(1) # Delay for 1 sec - currentTimeout-=1 + currentTimeout -= 1 if currentTimeout < 0: return False return True - + + def main(): module = AnsibleModule( - argument_spec = dict( - name = dict(required=True), - backend = dict(required=True), - weight = dict(default='8', type='int'), - probe = dict(default='none', choices =['none', 'http', 'icmp' , 'oco']), - state = dict(default='present', choices=['present', 'absent']), - endpoint = dict(required=True), - application_key = dict(required=True), - application_secret = dict(required=True), - consumer_key = dict(required=True), - timeout = dict(default=120, type='int') + argument_spec=dict( + name=dict(required=True), + backend=dict(required=True), + weight=dict(default='8', type='int'), + probe=dict(default='none', + choices=['none', 'http', 'icmp', 'oco']), + state=dict(default='present', choices=['present', 'absent']), + endpoint=dict(required=True), + application_key=dict(required=True), + application_secret=dict(required=True), + consumer_key=dict(required=True), + timeout=dict(default=120, type='int') ) ) - + if not HAS_OVH: - module.fail_json(msg='ovh-api python module is required to run this module ') + module.fail_json(msg='ovh-api python module\ + is required to run this module ') # Get parameters - name = module.params.get('name') - state = module.params.get('state') - backend = module.params.get('backend') - weight = long(module.params.get('weight')) - probe = module.params.get('probe') + name = module.params.get('name') + state = module.params.get('state') + backend = module.params.get('backend') + weight = long(module.params.get('weight')) + probe = module.params.get('probe') timeout = module.params.get('timeout') # Connect to OVH API client = getOvhClient(module) # Check that the load balancing exists - try : + try: loadBalancings = client.get('/ip/loadBalancing') except APIError as apiError: - module.fail_json(msg='Unable to call OVH api for getting the list of loadBalancing, check application key, secret, consumerkey and parameters. Error returned by OVH api was : {}'.format(apiError)) - - if not name in loadBalancings: + module.fail_json( + msg='Unable to call OVH api for getting the list of loadBalancing, \ + check application key, secret, consumerkey and parameters. \ + Error returned by OVH api was : {}'.format(apiError)) + + if name not in loadBalancings: module.fail_json(msg='IP LoadBalancing {} does not exist'.format(name)) # Check that no task is pending before going on - try : - if not waitForNoTask(client, name, timeout): - module.fail_json(msg='Timeout of {} seconds while waiting for no pending tasks before executing the module '.format(timeout)) + try: + if not waitForNoTask(client, name, timeout): + module.fail_json( + msg='Timeout of {} seconds while waiting for no pending \ + tasks before executing the module '.format(timeout)) except APIError as apiError: - module.fail_json(msg='Unable to call OVH api for getting the list of pending tasks of the loadBalancing, check application key, secret, consumerkey and parameters. Error returned by OVH api was : {}'.format(apiError)) + module.fail_json( + msg='Unable to call OVH api for getting the list of pending tasks \ + of the loadBalancing, check application key, secret, consumerkey \ + and parameters. Error returned by OVH api was : {}\ + '.format(apiError)) - try : + try: backends = client.get('/ip/loadBalancing/{}/backend'.format(name)) except APIError as apiError: - module.fail_json(msg='Unable to call OVH api for getting the list of backends of the loadBalancing, check application key, secret, consumerkey and parameters. Error returned by OVH api was : {}'.format(apiError)) - + module.fail_json( + msg='Unable to call OVH api for getting the list of backends \ + of the loadBalancing, check application key, secret, consumerkey \ + and parameters. Error returned by OVH api was : {}\ + '.format(apiError)) + backendExists = backend in backends moduleChanged = False - if state=="absent" : - if backendExists : + if state == "absent": + if backendExists: # Remove backend - try : - client.delete('/ip/loadBalancing/{}/backend/{}'.format(name, backend)) - if not waitForNoTask(client, name, timeout): - module.fail_json(msg='Timeout of {} seconds while waiting for completion of removing backend task'.format(timeout)) + try: + client.delete( + '/ip/loadBalancing/{}/backend/{}'.format(name, backend)) + if not waitForNoTask(client, name, timeout): + module.fail_json( + msg='Timeout of {} seconds while waiting for completion \ + of removing backend task'.format(timeout)) except APIError as apiError: - module.fail_json(msg='Unable to call OVH api for deleting the backend, check application key, secret, consumerkey and parameters. Error returned by OVH api was : {}'.format(apiError)) + module.fail_json( + msg='Unable to call OVH api for deleting the backend, \ + check application key, secret, consumerkey and \ + parameters. Error returned by OVH api was : {}\ + '.format(apiError)) moduleChanged = True - else : - if backendExists : + else: + if backendExists: # Get properties - try : - backendProperties = client.get('/ip/loadBalancing/{}/backend/{}'.format(name, backend)) + try: + backendProperties = client.get( + '/ip/loadBalancing/{}/backend/{}'.format(name, backend)) except APIError as apiError: - module.fail_json(msg='Unable to call OVH api for getting the backend properties, check application key, secret, consumerkey and parameters. Error returned by OVH api was : {}'.format(apiError)) - + module.fail_json( + msg='Unable to call OVH api for getting the backend properties, \ + check application key, secret, consumerkey and \ + parameters. Error returned by OVH api was : {}\ + '.format(apiError)) + if (backendProperties['weight'] != weight): # Change weight - try : - client.post('/ip/loadBalancing/{}/backend/{}/setWeight'.format(name, backend), weight=weight) - if not waitForNoTask(client, name, timeout): - module.fail_json(msg='Timeout of {} seconds while waiting for completion of setWeight to backend task'.format(timeout)) + try: + client.post( + '/ip/loadBalancing/{}/backend/{}/setWeight\ + '.format(name, backend), weight=weight) + if not waitForNoTask(client, name, timeout): + module.fail_json( + msg='Timeout of {} seconds while waiting for completion \ + of setWeight to backend task'.format(timeout)) except APIError as apiError: - module.fail_json(msg='Unable to call OVH api for updating the weight of the backend, check application key, secret, consumerkey and parameters. Error returned by OVH api was : {}'.format(apiError)) + module.fail_json( + msg='Unable to call OVH api for updating the weight of the \ + backend, check application key, secret, consumerkey \ + and parameters. Error returned by OVH api was : {}\ + '.format(apiError)) moduleChanged = True - + if (backendProperties['probe'] != probe): # Change probe backendProperties['probe'] = probe - try: - client.put('/ip/loadBalancing/{}/backend/{}'.format(name, backend), probe=probe ) - if not waitForNoTask(client, name, timeout): - module.fail_json(msg='Timeout of {} seconds while waiting for completion of setProbe to backend task'.format(timeout)) + try: + client.put( + '/ip/loadBalancing/{}/backend/{}\ + '.format(name, backend), probe=probe) + if not waitForNoTask(client, name, timeout): + module.fail_json( + msg='Timeout of {} seconds while waiting for completion of \ + setProbe to backend task'.format(timeout)) except APIError as apiError: - module.fail_json(msg='Unable to call OVH api for updating the propbe of the backend, check application key, secret, consumerkey and parameters. Error returned by OVH api was : {}'.format(apiError)) + module.fail_json( + msg='Unable to call OVH api for updating the propbe of \ + the backend, check application key, secret, \ + consumerkey and parameters. Error returned by OVH api \ + was : {}\ + '.format(apiError)) moduleChanged = True - - else : + + else: # Creates backend try: try: - client.post('/ip/loadBalancing/{}/backend'.format(name), ipBackend=backend, probe=probe, weight=weight) + client.post('/ip/loadBalancing/{}/backend'.format(name), + ipBackend=backend, probe=probe, weight=weight) except APIError as apiError: - module.fail_json(msg='Unable to call OVH api for creating the backend, check application key, secret, consumerkey and parameters. Error returned by OVH api was : {}'.format(apiError)) - - if not waitForNoTask(client, name, timeout): - module.fail_json(msg='Timeout of {} seconds while waiting for completion of backend creation task'.format(timeout)) + module.fail_json( + msg='Unable to call OVH api for creating the backend, check \ + application key, secret, consumerkey and parameters. \ + Error returned by OVH api was : {}'.format(apiError)) + + if not waitForNoTask(client, name, timeout): + module.fail_json( + msg='Timeout of {} seconds while waiting for completion of \ + backend creation task'.format(timeout)) except APIError as apiError: - module.fail_json(msg='Unable to call OVH api for creating the backend, check application key, secret, consumerkey and parameters. Error returned by OVH api was : {}'.format(apiError)) + module.fail_json( + msg='Unable to call OVH api for creating the backend, check \ + application key, secret, consumerkey and parameters. \ + Error returned by OVH api was : {}'.format(apiError)) moduleChanged = True - + module.exit_json(changed=moduleChanged) # We should never reach here module.fail_json(msg='Internal ovh_ip_loadbalancing_backend module error') - -# import module snippets -from ansible.module_utils.basic import * - main() From 40b78fbf727dd43a9ece1a30db4ed63f35812e45 Mon Sep 17 00:00:00 2001 From: pascalheraud Date: Wed, 9 Dec 2015 21:42:29 +0100 Subject: [PATCH 1860/2522] Fixed bad location and import Fixed bad type of default value for timeout --- cloud/ovh/ovh_ip_loadbalancing_backend.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cloud/ovh/ovh_ip_loadbalancing_backend.py b/cloud/ovh/ovh_ip_loadbalancing_backend.py index e3b80617b74..1d38909f612 100644 --- a/cloud/ovh/ovh_ip_loadbalancing_backend.py +++ b/cloud/ovh/ovh_ip_loadbalancing_backend.py @@ -4,14 +4,13 @@ try: import ovh import ovh.exceptions + from ovh.exceptions import APIError HAS_OVH = True except ImportError: HAS_OVH = False # import module snippets from ansible.module_utils.basic import * -# and APIError from ovh api -from ovh.exceptions import APIError DOCUMENTATION = ''' --- @@ -123,7 +122,7 @@ def main(): argument_spec=dict( name=dict(required=True), backend=dict(required=True), - weight=dict(default='8', type='int'), + weight=dict(default=8, type='int'), probe=dict(default='none', choices=['none', 'http', 'icmp', 'oco']), state=dict(default='present', choices=['present', 'absent']), From 6b91834d9ff1075d90da687a32f57a45833f69b7 Mon Sep 17 00:00:00 2001 From: pascalheraud Date: Thu, 10 Dec 2015 09:11:03 +0100 Subject: [PATCH 1861/2522] Changed licence and main() --- cloud/ovh/ovh_ip_loadbalancing_backend.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/cloud/ovh/ovh_ip_loadbalancing_backend.py b/cloud/ovh/ovh_ip_loadbalancing_backend.py index 1d38909f612..22bdeba91c9 100644 --- a/cloud/ovh/ovh_ip_loadbalancing_backend.py +++ b/cloud/ovh/ovh_ip_loadbalancing_backend.py @@ -1,5 +1,18 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . import sys try: import ovh @@ -281,4 +294,5 @@ def main(): # We should never reach here module.fail_json(msg='Internal ovh_ip_loadbalancing_backend module error') -main() +if __name__ == '__main__': + main() From 13b164791eb839c07f648dea3528e1180059155e Mon Sep 17 00:00:00 2001 From: pascalheraud Date: Thu, 10 Dec 2015 09:21:46 +0100 Subject: [PATCH 1862/2522] Update ovh_ip_loadbalancing_backend.py --- cloud/ovh/ovh_ip_loadbalancing_backend.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/ovh/ovh_ip_loadbalancing_backend.py b/cloud/ovh/ovh_ip_loadbalancing_backend.py index 22bdeba91c9..f2e81baaafa 100644 --- a/cloud/ovh/ovh_ip_loadbalancing_backend.py +++ b/cloud/ovh/ovh_ip_loadbalancing_backend.py @@ -140,9 +140,9 @@ def main(): choices=['none', 'http', 'icmp', 'oco']), state=dict(default='present', choices=['present', 'absent']), endpoint=dict(required=True), - application_key=dict(required=True), - application_secret=dict(required=True), - consumer_key=dict(required=True), + application_key=dict(required=True, no_log=True), + application_secret=dict(required=True, no_log=True), + consumer_key=dict(required=True, no_log=True), timeout=dict(default=120, type='int') ) ) From a28287d0b4c5eee5c1a86bb49ba2df84b6c003f7 Mon Sep 17 00:00:00 2001 From: pascalheraud Date: Wed, 30 Mar 2016 20:51:12 +0200 Subject: [PATCH 1863/2522] Update ovh_ip_loadbalancing_backend.py Fixed documentation from "ip" to "backend" and enhanced text. --- cloud/ovh/ovh_ip_loadbalancing_backend.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/cloud/ovh/ovh_ip_loadbalancing_backend.py b/cloud/ovh/ovh_ip_loadbalancing_backend.py index f2e81baaafa..658715c60a1 100644 --- a/cloud/ovh/ovh_ip_loadbalancing_backend.py +++ b/cloud/ovh/ovh_ip_loadbalancing_backend.py @@ -93,15 +93,17 @@ ''' EXAMPLES = ''' -# Adds or modify a backend to a loadbalancing -- ovh_ip_loadbalancing name=ip-1.1.1.1 ip=212.1.1.1 state=present \ -probe=none weight=8 \ +# Adds or modify the backend '212.1.1.1' to a \ +loadbalancing 'ip-1.1.1.1' +- ovh_ip_loadbalancing name=ip-1.1.1.1 backend=212.1.1.1 \ +state=present probe=none weight=8 \ endpoint=ovh-eu application_key=yourkey \ application_secret=yoursecret consumer_key=yourconsumerkey -# Removes a backend from a loadbalancing -- ovh_ip_loadbalancing name=ip-1.1.1.1 ip=212.1.1.1 state=absent \ -endpoint=ovh-eu application_key=yourkey \ +# Removes a backend '212.1.1.1' from a loadbalancing \ +'ip-1.1.1.1' +- ovh_ip_loadbalancing name=ip-1.1.1.1 backend=212.1.1.1 +state=absent endpoint=ovh-eu application_key=yourkey \ application_secret=yoursecret consumer_key=yourconsumerkey ''' From 91d7f1b34dfbe9e9effaf0ea2dc601b5bc3cde99 Mon Sep 17 00:00:00 2001 From: pascalheraud Date: Wed, 30 Mar 2016 20:54:47 +0200 Subject: [PATCH 1864/2522] Update ovh_ip_loadbalancing_backend.py Changed to ansible 2.1 --- cloud/ovh/ovh_ip_loadbalancing_backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/ovh/ovh_ip_loadbalancing_backend.py b/cloud/ovh/ovh_ip_loadbalancing_backend.py index 658715c60a1..adcf863dc5b 100644 --- a/cloud/ovh/ovh_ip_loadbalancing_backend.py +++ b/cloud/ovh/ovh_ip_loadbalancing_backend.py @@ -31,7 +31,7 @@ short_description: Manage OVH IP LoadBalancing backends description: - Manage OVH (French European hosting provider) LoadBalancing IP backends -version_added: "2.0" +version_added: "2.1" author: Pascal HERAUD @pascalheraud notes: - Uses the python OVH Api U(https://github.com/ovh/python-ovh). \ From b9a7fbbf4172903e9e095473617763f1ae360ed8 Mon Sep 17 00:00:00 2001 From: pascalheraud Date: Wed, 30 Mar 2016 21:39:50 +0200 Subject: [PATCH 1865/2522] Fixed RETURN documentation --- cloud/ovh/ovh_ip_loadbalancing_backend.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cloud/ovh/ovh_ip_loadbalancing_backend.py b/cloud/ovh/ovh_ip_loadbalancing_backend.py index adcf863dc5b..62658cbddb5 100644 --- a/cloud/ovh/ovh_ip_loadbalancing_backend.py +++ b/cloud/ovh/ovh_ip_loadbalancing_backend.py @@ -107,6 +107,8 @@ application_secret=yoursecret consumer_key=yourconsumerkey ''' +RETURN = ''' +''' def getOvhClient(ansibleModule): endpoint = ansibleModule.params.get('endpoint') From 9058f1fb76b53f2175f5c0226eaf41dfec1c3b97 Mon Sep 17 00:00:00 2001 From: pascalheraud Date: Wed, 30 Mar 2016 21:51:07 +0200 Subject: [PATCH 1866/2522] Changed order of import and documentation to fix the build --- cloud/ovh/ovh_ip_loadbalancing_backend.py | 24 +++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/cloud/ovh/ovh_ip_loadbalancing_backend.py b/cloud/ovh/ovh_ip_loadbalancing_backend.py index 62658cbddb5..791a6422cc8 100644 --- a/cloud/ovh/ovh_ip_loadbalancing_backend.py +++ b/cloud/ovh/ovh_ip_loadbalancing_backend.py @@ -13,18 +13,6 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -import sys -try: - import ovh - import ovh.exceptions - from ovh.exceptions import APIError - HAS_OVH = True -except ImportError: - HAS_OVH = False - -# import module snippets -from ansible.module_utils.basic import * - DOCUMENTATION = ''' --- module: ovh_ip_loadbalancing_backend @@ -110,6 +98,15 @@ RETURN = ''' ''' +import sys +try: + import ovh + import ovh.exceptions + from ovh.exceptions import APIError + HAS_OVH = True +except ImportError: + HAS_OVH = False + def getOvhClient(ansibleModule): endpoint = ansibleModule.params.get('endpoint') application_key = ansibleModule.params.get('application_key') @@ -298,5 +295,8 @@ def main(): # We should never reach here module.fail_json(msg='Internal ovh_ip_loadbalancing_backend module error') +# import module snippets +from ansible.module_utils.basic import * + if __name__ == '__main__': main() From 39f36103d7ab57766ca3d766560cd339b0593ae1 Mon Sep 17 00:00:00 2001 From: pascalheraud Date: Sun, 8 May 2016 22:36:59 +0200 Subject: [PATCH 1867/2522] Fixed multiline string formatting issues --- cloud/ovh/ovh_ip_loadbalancing_backend.py | 153 +++++++++++----------- 1 file changed, 78 insertions(+), 75 deletions(-) diff --git a/cloud/ovh/ovh_ip_loadbalancing_backend.py b/cloud/ovh/ovh_ip_loadbalancing_backend.py index 791a6422cc8..48c7d47fbc2 100644 --- a/cloud/ovh/ovh_ip_loadbalancing_backend.py +++ b/cloud/ovh/ovh_ip_loadbalancing_backend.py @@ -19,12 +19,12 @@ short_description: Manage OVH IP LoadBalancing backends description: - Manage OVH (French European hosting provider) LoadBalancing IP backends -version_added: "2.1" +version_added: "2.2" author: Pascal HERAUD @pascalheraud notes: - - Uses the python OVH Api U(https://github.com/ovh/python-ovh). \ - You have to create an application (a key and secret) with a consummer \ - key as described into U(https://eu.api.ovh.com/g934.first_step_with_api) + - Uses the python OVH Api U(https://github.com/ovh/python-ovh). + You have to create an application (a key and secret) with a consummer + key as described into U(https://eu.api.ovh.com/g934.first_step_with_api) requirements: - ovh > 0.3.5 options: @@ -41,8 +41,8 @@ default: present choices: ['present', 'absent'] description: - - Determines wether the backend is to be created/modified \ - or deleted + - Determines wether the backend is to be created/modified + or deleted probe: required: false default: none @@ -75,24 +75,24 @@ type: "int" default: 120 description: - - The timeout in seconds used to wait for a task to be \ - completed. Default is 120 seconds. + - The timeout in seconds used to wait for a task to be + completed. Default is 120 seconds. ''' EXAMPLES = ''' -# Adds or modify the backend '212.1.1.1' to a \ -loadbalancing 'ip-1.1.1.1' -- ovh_ip_loadbalancing name=ip-1.1.1.1 backend=212.1.1.1 \ -state=present probe=none weight=8 \ -endpoint=ovh-eu application_key=yourkey \ -application_secret=yoursecret consumer_key=yourconsumerkey +# Adds or modify the backend '212.1.1.1' to a +# loadbalancing 'ip-1.1.1.1' +- ovh_ip_loadbalancing name=ip-1.1.1.1 backend=212.1.1.1 + state=present probe=none weight=8 + endpoint=ovh-eu application_key=yourkey + application_secret=yoursecret consumer_key=yourconsumerkey -# Removes a backend '212.1.1.1' from a loadbalancing \ -'ip-1.1.1.1' -- ovh_ip_loadbalancing name=ip-1.1.1.1 backend=212.1.1.1 -state=absent endpoint=ovh-eu application_key=yourkey \ -application_secret=yoursecret consumer_key=yourconsumerkey +# Removes a backend '212.1.1.1' from a loadbalancing +# 'ip-1.1.1.1' +- ovh_ip_loadbalancing name=ip-1.1.1.1 backend=212.1.1.1 + state=absent endpoint=ovh-eu application_key=yourkey + application_secret=yoursecret consumer_key=yourconsumerkey ''' RETURN = ''' @@ -123,7 +123,7 @@ def getOvhClient(ansibleModule): def waitForNoTask(client, name, timeout): currentTimeout = timeout - while len(client.get('/ip/loadBalancing/{}/task'.format(name))) > 0: + while len(client.get('/ip/loadBalancing/{0}/task'.format(name))) > 0: time.sleep(1) # Delay for 1 sec currentTimeout -= 1 if currentTimeout < 0: @@ -149,8 +149,8 @@ def main(): ) if not HAS_OVH: - module.fail_json(msg='ovh-api python module\ - is required to run this module ') + module.fail_json(msg='ovh-api python module' + 'is required to run this module ') # Get parameters name = module.params.get('name') @@ -168,34 +168,34 @@ def main(): loadBalancings = client.get('/ip/loadBalancing') except APIError as apiError: module.fail_json( - msg='Unable to call OVH api for getting the list of loadBalancing, \ - check application key, secret, consumerkey and parameters. \ - Error returned by OVH api was : {}'.format(apiError)) + msg='Unable to call OVH api for getting the list of loadBalancing, ' + 'check application key, secret, consumerkey and parameters. ' + 'Error returned by OVH api was : {0}'.format(apiError)) if name not in loadBalancings: - module.fail_json(msg='IP LoadBalancing {} does not exist'.format(name)) + module.fail_json(msg='IP LoadBalancing {0} does not exist'.format(name)) # Check that no task is pending before going on try: if not waitForNoTask(client, name, timeout): module.fail_json( - msg='Timeout of {} seconds while waiting for no pending \ - tasks before executing the module '.format(timeout)) + msg='Timeout of {0} seconds while waiting for no pending ' + 'tasks before executing the module '.format(timeout)) except APIError as apiError: module.fail_json( - msg='Unable to call OVH api for getting the list of pending tasks \ - of the loadBalancing, check application key, secret, consumerkey \ - and parameters. Error returned by OVH api was : {}\ - '.format(apiError)) + msg='Unable to call OVH api for getting the list of pending tasks ' + 'of the loadBalancing, check application key, secret, consumerkey ' + 'and parameters. Error returned by OVH api was : {0}' + .format(apiError)) try: - backends = client.get('/ip/loadBalancing/{}/backend'.format(name)) + backends = client.get('/ip/loadBalancing/{0}/backend'.format(name)) except APIError as apiError: module.fail_json( - msg='Unable to call OVH api for getting the list of backends \ - of the loadBalancing, check application key, secret, consumerkey \ - and parameters. Error returned by OVH api was : {}\ - '.format(apiError)) + msg='Unable to call OVH api for getting the list of backends ' + 'of the loadBalancing, check application key, secret, consumerkey ' + 'and parameters. Error returned by OVH api was : {0}' + .format(apiError)) backendExists = backend in backends moduleChanged = False @@ -204,47 +204,48 @@ def main(): # Remove backend try: client.delete( - '/ip/loadBalancing/{}/backend/{}'.format(name, backend)) + '/ip/loadBalancing/{0}/backend/{1}'.format(name, backend)) if not waitForNoTask(client, name, timeout): module.fail_json( - msg='Timeout of {} seconds while waiting for completion \ - of removing backend task'.format(timeout)) + msg='Timeout of {0} seconds while waiting for completion ' + 'of removing backend task'.format(timeout)) except APIError as apiError: module.fail_json( - msg='Unable to call OVH api for deleting the backend, \ - check application key, secret, consumerkey and \ - parameters. Error returned by OVH api was : {}\ - '.format(apiError)) + msg='Unable to call OVH api for deleting the backend, ' + 'check application key, secret, consumerkey and ' + 'parameters. Error returned by OVH api was : {0}' + .format(apiError)) moduleChanged = True else: if backendExists: # Get properties try: backendProperties = client.get( - '/ip/loadBalancing/{}/backend/{}'.format(name, backend)) + '/ip/loadBalancing/{0}/backend/{1}'.format(name, backend)) except APIError as apiError: module.fail_json( - msg='Unable to call OVH api for getting the backend properties, \ - check application key, secret, consumerkey and \ - parameters. Error returned by OVH api was : {}\ - '.format(apiError)) + msg='Unable to call OVH api for getting the backend properties, ' + 'check application key, secret, consumerkey and ' + 'parameters. Error returned by OVH api was : {0}' + .format(apiError)) if (backendProperties['weight'] != weight): # Change weight try: client.post( - '/ip/loadBalancing/{}/backend/{}/setWeight\ - '.format(name, backend), weight=weight) + '/ip/loadBalancing/{0}/backend/{1}/setWeight' + .format(name, backend), weight=weight) if not waitForNoTask(client, name, timeout): module.fail_json( - msg='Timeout of {} seconds while waiting for completion \ - of setWeight to backend task'.format(timeout)) + msg='Timeout of {0} seconds while waiting for completion ' + 'of setWeight to backend task' + .format(timeout)) except APIError as apiError: module.fail_json( - msg='Unable to call OVH api for updating the weight of the \ - backend, check application key, secret, consumerkey \ - and parameters. Error returned by OVH api was : {}\ - '.format(apiError)) + msg='Unable to call OVH api for updating the weight of the ' + 'backend, check application key, secret, consumerkey ' + 'and parameters. Error returned by OVH api was : {0}' + .format(apiError)) moduleChanged = True if (backendProperties['probe'] != probe): @@ -252,42 +253,44 @@ def main(): backendProperties['probe'] = probe try: client.put( - '/ip/loadBalancing/{}/backend/{}\ - '.format(name, backend), probe=probe) + '/ip/loadBalancing/{0}/backend/{1}' + .format(name, backend), probe=probe) if not waitForNoTask(client, name, timeout): module.fail_json( - msg='Timeout of {} seconds while waiting for completion of \ - setProbe to backend task'.format(timeout)) + msg='Timeout of {0} seconds while waiting for completion of ' + 'setProbe to backend task' + .format(timeout)) except APIError as apiError: module.fail_json( - msg='Unable to call OVH api for updating the propbe of \ - the backend, check application key, secret, \ - consumerkey and parameters. Error returned by OVH api \ - was : {}\ - '.format(apiError)) + msg='Unable to call OVH api for updating the propbe of ' + 'the backend, check application key, secret, ' + 'consumerkey and parameters. Error returned by OVH api ' + 'was : {0}' + .format(apiError)) moduleChanged = True else: # Creates backend try: try: - client.post('/ip/loadBalancing/{}/backend'.format(name), + client.post('/ip/loadBalancing/{0}/backend'.format(name), ipBackend=backend, probe=probe, weight=weight) except APIError as apiError: module.fail_json( - msg='Unable to call OVH api for creating the backend, check \ - application key, secret, consumerkey and parameters. \ - Error returned by OVH api was : {}'.format(apiError)) + msg='Unable to call OVH api for creating the backend, check ' + 'application key, secret, consumerkey and parameters. ' + 'Error returned by OVH api was : {0}' + .format(apiError)) if not waitForNoTask(client, name, timeout): module.fail_json( - msg='Timeout of {} seconds while waiting for completion of \ - backend creation task'.format(timeout)) + msg='Timeout of {0} seconds while waiting for completion of ' + 'backend creation task'.format(timeout)) except APIError as apiError: module.fail_json( - msg='Unable to call OVH api for creating the backend, check \ - application key, secret, consumerkey and parameters. \ - Error returned by OVH api was : {}'.format(apiError)) + msg='Unable to call OVH api for creating the backend, check ' + 'application key, secret, consumerkey and parameters. ' + 'Error returned by OVH api was : {0}'.format(apiError)) moduleChanged = True module.exit_json(changed=moduleChanged) From c6938e42ef968510fb3c90d58f7fc8b6ae61659a Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 27 Jul 2016 15:47:25 -0700 Subject: [PATCH 1868/2522] Update examples in the documentation to yaml syntax --- cloud/ovh/ovh_ip_loadbalancing_backend.py | 31 ++++++++++++++--------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/cloud/ovh/ovh_ip_loadbalancing_backend.py b/cloud/ovh/ovh_ip_loadbalancing_backend.py index 48c7d47fbc2..2d9f551ee85 100644 --- a/cloud/ovh/ovh_ip_loadbalancing_backend.py +++ b/cloud/ovh/ovh_ip_loadbalancing_backend.py @@ -83,16 +83,26 @@ EXAMPLES = ''' # Adds or modify the backend '212.1.1.1' to a # loadbalancing 'ip-1.1.1.1' -- ovh_ip_loadbalancing name=ip-1.1.1.1 backend=212.1.1.1 - state=present probe=none weight=8 - endpoint=ovh-eu application_key=yourkey - application_secret=yoursecret consumer_key=yourconsumerkey +- ovh_ip_loadbalancing: + name: ip-1.1.1.1 + backend: 212.1.1.1 + state: present + probe: none + weight: 8 + endpoint: ovh-eu + application_key: yourkey + application_secret: yoursecret + consumer_key: yourconsumerkey -# Removes a backend '212.1.1.1' from a loadbalancing -# 'ip-1.1.1.1' -- ovh_ip_loadbalancing name=ip-1.1.1.1 backend=212.1.1.1 - state=absent endpoint=ovh-eu application_key=yourkey - application_secret=yoursecret consumer_key=yourconsumerkey +# Removes a backend '212.1.1.1' from a loadbalancing 'ip-1.1.1.1' +- ovh_ip_loadbalancing: + name: ip-1.1.1.1 + backend: 212.1.1.1 + state: absent + endpoint: ovh-eu + application_key: yourkey + application_secret: yoursecret + consumer_key: yourconsumerkey ''' RETURN = ''' @@ -295,9 +305,6 @@ def main(): module.exit_json(changed=moduleChanged) - # We should never reach here - module.fail_json(msg='Internal ovh_ip_loadbalancing_backend module error') - # import module snippets from ansible.module_utils.basic import * From c8f911e05c9d18c5caf8bfb930de58502663963e Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 27 Jul 2016 15:50:07 -0700 Subject: [PATCH 1869/2522] Update imports --- cloud/ovh/ovh_ip_loadbalancing_backend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/ovh/ovh_ip_loadbalancing_backend.py b/cloud/ovh/ovh_ip_loadbalancing_backend.py index 2d9f551ee85..7f2c5d5963f 100644 --- a/cloud/ovh/ovh_ip_loadbalancing_backend.py +++ b/cloud/ovh/ovh_ip_loadbalancing_backend.py @@ -108,7 +108,7 @@ RETURN = ''' ''' -import sys +import time try: import ovh import ovh.exceptions @@ -306,7 +306,7 @@ def main(): module.exit_json(changed=moduleChanged) # import module snippets -from ansible.module_utils.basic import * +from ansible.module_utils.basic import AnsibleModule if __name__ == '__main__': main() From be367cc2de2ebb88ea3ca18b33db4b0dfc65cb30 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Thu, 28 Jul 2016 08:05:40 -0700 Subject: [PATCH 1870/2522] Bugfixes and code style (#2627) A number of coding conventions have been adopted for new F5 modules that are in development. To ensure common usage across the modules, this module needed to be updated to reflect those conventions. Additionally, this patch fixes a couple bugs in the module that were preventing it from being idempotent. --- network/f5/bigip_monitor_tcp.py | 526 +++++++++++++++++--------------- 1 file changed, 273 insertions(+), 253 deletions(-) diff --git a/network/f5/bigip_monitor_tcp.py b/network/f5/bigip_monitor_tcp.py index 5de83fd14b9..f7443e2878e 100644 --- a/network/f5/bigip_monitor_tcp.py +++ b/network/f5/bigip_monitor_tcp.py @@ -23,171 +23,176 @@ module: bigip_monitor_tcp short_description: "Manages F5 BIG-IP LTM tcp monitors" description: - - "Manages F5 BIG-IP LTM tcp monitors via iControl SOAP API" + - "Manages F5 BIG-IP LTM tcp monitors via iControl SOAP API" version_added: "1.4" author: - - Serge van Ginderachter (@srvg) - - Tim Rupp (@caphrim007) + - Serge van Ginderachter (@srvg) + - Tim Rupp (@caphrim007) notes: - - "Requires BIG-IP software version >= 11" - - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" - - "Best run as a local_action in your playbook" - - "Monitor API documentation: https://devcentral.f5.com/wiki/iControl.LocalLB__Monitor.ashx" + - "Requires BIG-IP software version >= 11" + - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" + - "Best run as a local_action in your playbook" + - "Monitor API documentation: https://devcentral.f5.com/wiki/iControl.LocalLB__Monitor.ashx" requirements: - - bigsuds + - bigsuds options: - server: - description: - - BIG-IP host - required: true - default: null - server_port: - description: - - BIG-IP server port - required: false - default: 443 - version_added: "2.2" - user: - description: - - BIG-IP username - required: true - default: null - password: - description: - - BIG-IP password - required: true - default: null - validate_certs: - description: - - If C(no), SSL certificates will not be validated. This should only be used - on personally controlled sites. Prior to 2.0, this module would always - validate on python >= 2.7.9 and never validate on python <= 2.7.8 - required: false - default: 'yes' - choices: ['yes', 'no'] - version_added: 2.0 - state: - description: - - Monitor state - required: false - default: 'present' - choices: ['present', 'absent'] - name: - description: - - Monitor name - required: true - default: null - aliases: ['monitor'] - partition: - description: - - Partition for the monitor - required: false - default: 'Common' - type: - description: - - The template type of this monitor template - required: false - default: 'tcp' - choices: [ 'TTYPE_TCP', 'TTYPE_TCP_ECHO', 'TTYPE_TCP_HALF_OPEN'] - parent: - description: - - The parent template of this monitor template - required: false - default: 'tcp' - choices: [ 'tcp', 'tcp_echo', 'tcp_half_open'] - parent_partition: - description: - - Partition for the parent monitor - required: false - default: 'Common' - send: - description: - - The send string for the monitor call - required: true - default: none - receive: - description: - - The receive string for the monitor call - required: true - default: none - ip: - description: - - IP address part of the ipport definition. The default API setting - is "0.0.0.0". - required: false - default: none - port: - description: - - port address part op the ipport definition. The default API - setting is 0. - required: false - default: none - interval: - description: - - The interval specifying how frequently the monitor instance - of this template will run. By default, this interval is used for up and - down states. The default API setting is 5. - required: false - default: none - timeout: - description: - - The number of seconds in which the node or service must respond to - the monitor request. If the target responds within the set time - period, it is considered up. If the target does not respond within - the set time period, it is considered down. You can change this - number to any number you want, however, it should be 3 times the - interval number of seconds plus 1 second. The default API setting - is 16. - required: false - default: none - time_until_up: - description: - - Specifies the amount of time in seconds after the first successful - response before a node will be marked up. A value of 0 will cause a - node to be marked up immediately after a valid response is received - from the node. The default API setting is 0. - required: false - default: none + server: + description: + - BIG-IP host + required: true + default: null + server_port: + description: + - BIG-IP server port + required: false + default: 443 + version_added: "2.2" + user: + description: + - BIG-IP username + required: true + default: null + password: + description: + - BIG-IP password + required: true + default: null + validate_certs: + description: + - If C(no), SSL certificates will not be validated. This should only be used + on personally controlled sites. Prior to 2.0, this module would always + validate on python >= 2.7.9 and never validate on python <= 2.7.8 + required: false + default: 'yes' + choices: + - yes + - no + version_added: 2.0 + state: + description: + - Monitor state + required: false + default: 'present' + choices: + - present + - absent + name: + description: + - Monitor name + required: true + default: null + aliases: + - monitor + partition: + description: + - Partition for the monitor + required: false + default: 'Common' + type: + description: + - The template type of this monitor template + required: false + default: 'tcp' + choices: + - TTYPE_TCP + - TTYPE_TCP_ECHO + - TTYPE_TCP_HALF_OPEN + parent: + description: + - The parent template of this monitor template + required: false + default: 'tcp' + choices: + - tcp + - tcp_echo + - tcp_half_open + parent_partition: + description: + - Partition for the parent monitor + required: false + default: 'Common' + send: + description: + - The send string for the monitor call + required: true + default: none + receive: + description: + - The receive string for the monitor call + required: true + default: none + ip: + description: + - IP address part of the ipport definition. The default API setting + is "0.0.0.0". + required: false + default: none + port: + description: + - Port address part op the ipport definition. The default API + setting is 0. + required: false + default: none + interval: + description: + - The interval specifying how frequently the monitor instance + of this template will run. By default, this interval is used for up and + down states. The default API setting is 5. + required: false + default: none + timeout: + description: + - The number of seconds in which the node or service must respond to + the monitor request. If the target responds within the set time + period, it is considered up. If the target does not respond within + the set time period, it is considered down. You can change this + number to any number you want, however, it should be 3 times the + interval number of seconds plus 1 second. The default API setting + is 16. + required: false + default: none + time_until_up: + description: + - Specifies the amount of time in seconds after the first successful + response before a node will be marked up. A value of 0 will cause a + node to be marked up immediately after a valid response is received + from the node. The default API setting is 0. + required: false + default: none ''' EXAMPLES = ''' - -- name: BIGIP F5 | Create TCP Monitor - local_action: - module: bigip_monitor_tcp - state: present - server: "{{ f5server }}" - user: "{{ f5user }}" - password: "{{ f5password }}" - name: "{{ item.monitorname }}" - type: tcp - send: "{{ item.send }}" - receive: "{{ item.receive }}" - with_items: f5monitors-tcp -- name: BIGIP F5 | Create TCP half open Monitor - local_action: - module: bigip_monitor_tcp - state: present - server: "{{ f5server }}" - user: "{{ f5user }}" - password: "{{ f5password }}" - name: "{{ item.monitorname }}" - type: tcp - send: "{{ item.send }}" - receive: "{{ item.receive }}" - with_items: f5monitors-halftcp -- name: BIGIP F5 | Remove TCP Monitor - local_action: - module: bigip_monitor_tcp - state: absent - server: "{{ f5server }}" - user: "{{ f5user }}" - password: "{{ f5password }}" - name: "{{ monitorname }}" - with_flattened: - - f5monitors-tcp - - f5monitors-halftcp - +- name: Create TCP Monitor + bigip_monitor_tcp: + state: "present" + server: "lb.mydomain.com" + user: "admin" + password: "secret" + name: "my_tcp_monitor" + type: "tcp" + send: "tcp string to send" + receive: "tcp string to receive" + delegate_to: localhost + +- name: Create TCP half open Monitor + bigip_monitor_tcp: + state: "present" + server: "lb.mydomain.com" + user: "admin" + password: "secret" + name: "my_tcp_monitor" + type: "tcp" + send: "tcp string to send" + receive: "http string to receive" + delegate_to: localhost + +- name: Remove TCP Monitor + bigip_monitor_tcp: + state: "absent" + server: "lb.mydomain.com" + user: "admin" + password: "secret" + name: "my_tcp_monitor" ''' TEMPLATE_TYPE = DEFAULT_TEMPLATE_TYPE = 'TTYPE_TCP' @@ -196,7 +201,6 @@ def check_monitor_exists(module, api, monitor, parent): - # hack to determine if monitor exists result = False try: @@ -206,7 +210,7 @@ def check_monitor_exists(module, api, monitor, parent): result = True else: module.fail_json(msg='Monitor already exists, but has a different type (%s) or parent(%s)' % (ttype, parent)) - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed as e: if "was not found" in str(e): result = False else: @@ -216,10 +220,15 @@ def check_monitor_exists(module, api, monitor, parent): def create_monitor(api, monitor, template_attributes): - try: - api.LocalLB.Monitor.create_template(templates=[{'template_name': monitor, 'template_type': TEMPLATE_TYPE}], template_attributes=[template_attributes]) - except bigsuds.OperationFailed, e: + api.LocalLB.Monitor.create_template( + templates=[{ + 'template_name': monitor, + 'template_type': TEMPLATE_TYPE + }], + template_attributes=[template_attributes] + ) + except bigsuds.OperationFailed as e: if "already exists" in str(e): return False else: @@ -229,10 +238,9 @@ def create_monitor(api, monitor, template_attributes): def delete_monitor(api, monitor): - try: api.LocalLB.Monitor.delete_template(template_names=[monitor]) - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed as e: # maybe it was deleted since we checked if "was not found" in str(e): return False @@ -243,41 +251,46 @@ def delete_monitor(api, monitor): def check_string_property(api, monitor, str_property): - try: - return str_property == api.LocalLB.Monitor.get_template_string_property([monitor], [str_property['type']])[0] - except bigsuds.OperationFailed, e: + template_prop = api.LocalLB.Monitor.get_template_string_property( + [monitor], [str_property['type']] + )[0] + return str_property == template_prop + except bigsuds.OperationFailed as e: # happens in check mode if not created yet if "was not found" in str(e): return True else: # genuine exception raise - return True def set_string_property(api, monitor, str_property): - - api.LocalLB.Monitor.set_template_string_property(template_names=[monitor], values=[str_property]) + api.LocalLB.Monitor.set_template_string_property( + template_names=[monitor], + values=[str_property] + ) def check_integer_property(api, monitor, int_property): - try: - return int_property == api.LocalLB.Monitor.get_template_integer_property([monitor], [int_property['type']])[0] - except bigsuds.OperationFailed, e: + return int_property == api.LocalLB.Monitor.get_template_integer_property( + [monitor], [int_property['type']] + )[0] + except bigsuds.OperationFailed as e: # happens in check mode if not created yet if "was not found" in str(e): return True else: # genuine exception raise - return True def set_integer_property(api, monitor, int_property): - - api.LocalLB.Monitor.set_template_integer_property(template_names=[monitor], values=[int_property]) + api.LocalLB.Monitor.set_template_integer_property( + template_names=[monitor], + values=[int_property] + ) def update_monitor_properties(api, module, monitor, template_string_properties, template_integer_properties): @@ -287,6 +300,7 @@ def update_monitor_properties(api, module, monitor, template_string_properties, if not module.check_mode: set_string_property(api, monitor, str_property) changed = True + for int_property in template_integer_properties: if int_property['value'] is not None and not check_integer_property(api, monitor, int_property): if not module.check_mode: @@ -297,56 +311,47 @@ def update_monitor_properties(api, module, monitor, template_string_properties, def get_ipport(api, monitor): - return api.LocalLB.Monitor.get_template_destination(template_names=[monitor])[0] def set_ipport(api, monitor, ipport): - try: - api.LocalLB.Monitor.set_template_destination(template_names=[monitor], destinations=[ipport]) + api.LocalLB.Monitor.set_template_destination( + template_names=[monitor], destinations=[ipport] + ) return True, "" - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed as e: if "Cannot modify the address type of monitor" in str(e): return False, "Cannot modify the address type of monitor if already assigned to a pool." else: # genuine exception raise -# =========================================== -# main loop -# -# writing a module for other monitor types should -# only need an updated main() (and monitor specific functions) def main(): - - # begin monitor specific stuff - argument_spec=f5_argument_spec() - argument_spec.update(dict( - name = dict(required=True), - type = dict(default=DEFAULT_TEMPLATE_TYPE_CHOICE, choices=TEMPLATE_TYPE_CHOICES), - parent = dict(default=DEFAULT_PARENT), - parent_partition = dict(default='Common'), - send = dict(required=False), - receive = dict(required=False), - ip = dict(required=False), - port = dict(required=False, type='int'), - interval = dict(required=False, type='int'), - timeout = dict(required=False, type='int'), - time_until_up = dict(required=False, type='int', default=0) - ) + argument_spec = f5_argument_spec() + + meta_args = dict( + name=dict(required=True), + type=dict(default=DEFAULT_TEMPLATE_TYPE_CHOICE, choices=TEMPLATE_TYPE_CHOICES), + parent=dict(default=DEFAULT_PARENT), + parent_partition=dict(default='Common'), + send=dict(required=False), + receive=dict(required=False), + ip=dict(required=False), + port=dict(required=False, type='int'), + interval=dict(required=False, type='int'), + timeout=dict(required=False, type='int'), + time_until_up=dict(required=False, type='int', default=0) ) + argument_spec.update(meta_args) module = AnsibleModule( - argument_spec = argument_spec, + argument_spec=argument_spec, supports_check_mode=True ) - if not bigsuds_found: - module.fail_json(msg="the python bigsuds module is required") - if module.params['validate_certs']: import ssl if not hasattr(ssl, 'SSLContext'): @@ -382,26 +387,27 @@ def main(): api = bigip_api(server, user, password, validate_certs, port=server_port) monitor_exists = check_monitor_exists(module, api, monitor, parent) - # ipport is a special setting - if monitor_exists: # make sure to not update current settings if not asked + if monitor_exists: + # make sure to not update current settings if not asked cur_ipport = get_ipport(api, monitor) if ip is None: ip = cur_ipport['ipport']['address'] if port is None: port = cur_ipport['ipport']['port'] - else: # use API defaults if not defined to create it - if interval is None: + else: + # use API defaults if not defined to create it + if interval is None: interval = 5 - if timeout is None: + if timeout is None: timeout = 16 - if ip is None: + if ip is None: ip = '0.0.0.0' - if port is None: + if port is None: port = 0 - if send is None: + if send is None: send = '' - if receive is None: + if receive is None: receive = '' # define and set address type @@ -414,76 +420,90 @@ def main(): else: address_type = 'ATYPE_UNSET' - ipport = {'address_type': address_type, - 'ipport': {'address': ip, - 'port': port}} - - template_attributes = {'parent_template': parent, - 'interval': interval, - 'timeout': timeout, - 'dest_ipport': ipport, - 'is_read_only': False, - 'is_directly_usable': True} + ipport = { + 'address_type': address_type, + 'ipport': { + 'address': ip, + 'port': port + } + } + + template_attributes = { + 'parent_template': parent, + 'interval': interval, + 'timeout': timeout, + 'dest_ipport': ipport, + 'is_read_only': False, + 'is_directly_usable': True + } # monitor specific stuff if type == 'TTYPE_TCP': - template_string_properties = [{'type': 'STYPE_SEND', - 'value': send}, - {'type': 'STYPE_RECEIVE', - 'value': receive}] + template_string_properties = [ + { + 'type': 'STYPE_SEND', + 'value': send + }, + { + 'type': 'STYPE_RECEIVE', + 'value': receive + } + ] else: template_string_properties = [] - template_integer_properties = [{'type': 'ITYPE_INTERVAL', - 'value': interval}, - {'type': 'ITYPE_TIMEOUT', - 'value': timeout}, - {'type': 'ITYPE_TIME_UNTIL_UP', - 'value': interval}] + template_integer_properties = [ + { + 'type': 'ITYPE_INTERVAL', + 'value': interval + }, + { + 'type': 'ITYPE_TIMEOUT', + 'value': timeout + }, + { + 'type': 'ITYPE_TIME_UNTIL_UP', + 'value': time_until_up + } + ] # main logic, monitor generic try: result = {'changed': False} # default - if state == 'absent': if monitor_exists: if not module.check_mode: - # possible race condition if same task + # possible race condition if same task # on other node deleted it first result['changed'] |= delete_monitor(api, monitor) else: result['changed'] |= True - - else: # state present - ## check for monitor itself - if not monitor_exists: # create it - if not module.check_mode: + else: + # check for monitor itself + if not monitor_exists: + if not module.check_mode: # again, check changed status here b/c race conditions # if other task already created it result['changed'] |= create_monitor(api, monitor, template_attributes) - else: + else: result['changed'] |= True - ## check for monitor parameters + # check for monitor parameters # whether it already existed, or was just created, now update # the update functions need to check for check mode but # cannot update settings if it doesn't exist which happens in check mode - if monitor_exists and not module.check_mode: - result['changed'] |= update_monitor_properties(api, module, monitor, - template_string_properties, - template_integer_properties) - # else assume nothing changed + result['changed'] |= update_monitor_properties(api, module, monitor, + template_string_properties, + template_integer_properties) # we just have to update the ipport if monitor already exists and it's different if monitor_exists and cur_ipport != ipport: - set_ipport(api, monitor, ipport) + set_ipport(api, monitor, ipport) result['changed'] |= True - #else: monitor doesn't exist (check mode) or ipport is already ok - - - except Exception, e: + # else: monitor doesn't exist (check mode) or ipport is already ok + except Exception as e: module.fail_json(msg="received exception: %s" % e) module.exit_json(**result) From 3533ae2647d59a7e19e6bf372e448472f8567665 Mon Sep 17 00:00:00 2001 From: Aaron Brady Date: Thu, 28 Jul 2016 16:31:51 +0100 Subject: [PATCH 1871/2522] Add `active` and `inactive` states to the lvol module (#1974) * Add `active` and `inactive` states to the lvol module * Honor the previous state of the changed variable * Move active/inactive states to active boolean parameter * Bump version_added to make Travis happy * Avoid bailing early is size isn't specified * Add invocation examples * Move "no size" up for code clarity --- system/lvol.py | 45 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/system/lvol.py b/system/lvol.py index 75d8c56ac9a..817a4e66eab 100644 --- a/system/lvol.py +++ b/system/lvol.py @@ -48,8 +48,15 @@ choices: [ "present", "absent" ] default: present description: - - Control if the logical volume exists. If C(present) the C(size) option - is required. + - Control if the logical volume exists. If C(present) and the + volume does not already exist then the C(size) option is required. + required: false + active: + version_added: "2.2" + choices: [ "yes", "no" ] + default: "yes" + description: + - Whether the volume is activate and visible to the host. required: false force: version_added: "1.5" @@ -125,6 +132,12 @@ # Create a snapshot volume of the test logical volume. - lvol: vg=firefly lv=test snapshot=snap1 size=100m + +# Deactivate a logical volume +- lvol: vg=firefly lv=test active=false + +# Create a deactivated logical volume +- lvol: vg=firefly lv=test size=512g active=false ''' import re @@ -140,7 +153,8 @@ def parse_lvs(data): parts = line.strip().split(';') lvs.append({ 'name': parts[0].replace('[','').replace(']',''), - 'size': int(decimal_point.match(parts[1]).group(1)) + 'size': int(decimal_point.match(parts[1]).group(1)), + 'active': (parts[2][4] == 'a') }) return lvs @@ -178,6 +192,7 @@ def main(): state=dict(choices=["absent", "present"], default='present'), force=dict(type='bool', default='no'), shrink=dict(type='bool', default='yes'), + active=dict(type='bool', default='yes'), snapshot=dict(type='str', default=None), pvs=dict(type='str') ), @@ -201,6 +216,7 @@ def main(): state = module.params['state'] force = module.boolean(module.params['force']) shrink = module.boolean(module.params['shrink']) + active = module.boolean(module.params['active']) size_opt = 'L' size_unit = 'm' snapshot = module.params['snapshot'] @@ -270,7 +286,7 @@ def main(): # Get information on logical volume requested lvs_cmd = module.get_bin_path("lvs", required=True) rc, current_lvs, err = module.run_command( - "%s -a --noheadings --nosuffix -o lv_name,size --units %s --separator ';' %s" % (lvs_cmd, unit, vg)) + "%s -a --noheadings --nosuffix -o lv_name,size,lv_attr --units %s --separator ';' %s" % (lvs_cmd, unit, vg)) if rc != 0: if state == 'absent': @@ -296,8 +312,6 @@ def main(): if state == 'present' and not size: if this_lv is None: module.fail_json(msg="No size given.") - else: - module.exit_json(changed=False, vg=vg, lv=this_lv['name'], size=this_lv['size']) msg = '' if this_lv is None: @@ -325,6 +339,9 @@ def main(): else: module.fail_json(msg="Failed to remove logical volume %s" % (lv), rc=rc, err=err) + elif not size: + pass + elif size_opt == 'l': ### Resize LV based on % value tool = None @@ -392,6 +409,22 @@ def main(): else: module.fail_json(msg="Unable to resize %s to %s%s" % (lv, size, size_unit), rc=rc, err=err) + if this_lv is not None: + if active: + lvchange_cmd = module.get_bin_path("lvchange", required=True) + rc, _, err = module.run_command("%s -ay %s/%s" % (lvchange_cmd, vg, this_lv['name'])) + if rc == 0: + module.exit_json(changed=((not this_lv['active']) or changed), vg=vg, lv=this_lv['name'], size=this_lv['size']) + else: + module.fail_json(msg="Failed to activate logical volume %s" % (lv), rc=rc, err=err) + else: + lvchange_cmd = module.get_bin_path("lvchange", required=True) + rc, _, err = module.run_command("%s -an %s/%s" % (lvchange_cmd, vg, this_lv['name'])) + if rc == 0: + module.exit_json(changed=(this_lv['active'] or changed), vg=vg, lv=this_lv['name'], size=this_lv['size']) + else: + module.fail_json(msg="Failed to deactivate logical volume %s" % (lv), rc=rc, err=err) + module.exit_json(changed=changed, msg=msg) # import module snippets From cd2dbed79c0815d1303d57006e0858e780897207 Mon Sep 17 00:00:00 2001 From: Ruslan Kiianchuk Date: Thu, 28 Jul 2016 08:52:32 -0700 Subject: [PATCH 1872/2522] Add support for reiserfs (#2551) * Add support for reiserfs Create commands mapping for Reiserfs tools. --- system/filesystem.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/system/filesystem.py b/system/filesystem.py index ad9e0d1a52d..10fa5afbb1b 100644 --- a/system/filesystem.py +++ b/system/filesystem.py @@ -30,6 +30,7 @@ fstype: description: - File System type to be created. + - reiserfs support was added in 2.2. required: true dev: description: @@ -142,6 +143,13 @@ def main(): 'force_flag' : '-F', 'fsinfo': 'tune2fs', }, + 'reiserfs' : { + 'mkfs' : 'mkfs.reiserfs', + 'grow' : 'resize_reiserfs', + 'grow_flag' : None, + 'force_flag' : '-f', + 'fsinfo': 'reiserfstune', + }, 'ext4dev' : { 'mkfs' : 'mkfs.ext4', 'grow' : 'resize2fs', From fc417a5ab06d43b6d65d0ccd73bb12b7f81e6a0a Mon Sep 17 00:00:00 2001 From: Anton Ovchinnikov Date: Thu, 28 Jul 2016 18:12:14 +0200 Subject: [PATCH 1873/2522] Fix check mode for blockinfile when 'create: yes' is specified (#2413) Make the module more semantically similar to lineinfile when the destination does not exist. This fixes #2021. --- files/blockinfile.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/files/blockinfile.py b/files/blockinfile.py index 258284ea5af..38e719a9707 100755 --- a/files/blockinfile.py +++ b/files/blockinfile.py @@ -212,7 +212,8 @@ def main(): module.fail_json(rc=256, msg='Destination %s is a directory !' % dest) - if not os.path.exists(dest): + path_exists = os.path.exists(dest) + if not path_exists: if not module.boolean(params['create']): module.fail_json(rc=257, msg='Destination %s does not exist !' % dest) @@ -230,6 +231,9 @@ def main(): marker = params['marker'] present = params['state'] == 'present' + if not present and not path_exists: + module.exit_json(changed=False, msg="File not present") + if insertbefore is None and insertafter is None: insertafter = 'EOF' @@ -299,10 +303,13 @@ def main(): changed = True if changed and not module.check_mode: - if module.boolean(params['backup']) and os.path.exists(dest): + if module.boolean(params['backup']) and path_exists: module.backup_local(dest) write_changes(module, result, dest) + if module.check_mode and not path_exists: + module.exit_json(changed=changed, msg=msg) + msg, changed = check_file_attrs(module, changed, msg) module.exit_json(changed=changed, msg=msg) From 9fda16070fcff4e49fad0b222c00b4eb95238e72 Mon Sep 17 00:00:00 2001 From: William Albert Date: Thu, 28 Jul 2016 09:56:35 -0700 Subject: [PATCH 1874/2522] Add modules to support Google Cloud DNS (#2252) This commit adds modules that can manipulate Google Cloud DNS. The modules can create and delete zones, as well as records within zones. --- cloud/google/gcdns_record.py | 790 +++++++++++++++++++++++++++++++++++ cloud/google/gcdns_zone.py | 381 +++++++++++++++++ 2 files changed, 1171 insertions(+) create mode 100644 cloud/google/gcdns_record.py create mode 100644 cloud/google/gcdns_zone.py diff --git a/cloud/google/gcdns_record.py b/cloud/google/gcdns_record.py new file mode 100644 index 00000000000..ebccfca5dcb --- /dev/null +++ b/cloud/google/gcdns_record.py @@ -0,0 +1,790 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2015 CallFire Inc. +# +# This file is part of Ansible. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +################################################################################ +# Documentation +################################################################################ + +DOCUMENTATION = ''' +--- +module: gcdns_record +short_description: Creates or removes resource records in Google Cloud DNS +description: + - Creates or removes resource records in Google Cloud DNS. +version_added: "2.2" +author: "William Albert (@walbert947)" +requirements: + - "python >= 2.6" + - "apache-libcloud >= 0.19.0" +options: + state: + description: + - Whether the given resource record should or should not be present. + required: false + choices: ["present", "absent"] + default: "present" + record: + description: + - The fully-qualified domain name of the resource record. + required: true + aliases: ['name'] + zone: + description: + - The DNS domain name of the zone (e.g., example.com). + - One of either I(zone) or I(zone_id) must be specified as an + option, or the module will fail. + - If both I(zone) and I(zone_id) are specifed, I(zone_id) will be + used. + required: false + zone_id: + description: + - The Google Cloud ID of the zone (e.g., example-com). + - One of either I(zone) or I(zone_id) must be specified as an + option, or the module will fail. + - These usually take the form of domain names with the dots replaced + with dashes. A zone ID will never have any dots in it. + - I(zone_id) can be faster than I(zone) in projects with a large + number of zones. + - If both I(zone) and I(zone_id) are specifed, I(zone_id) will be + used. + required: false + type: + description: + - The type of resource record to add. + required: true + choices: [ 'A', 'AAAA', 'CNAME', 'SRV', 'TXT', 'SOA', 'NS', 'MX', 'SPF', 'PTR' ] + values: + description: + - The values to use for the resource record. + - I(values) must be specified if I(state) is C(present) or + I(overwrite) is C(True), or the module will fail. + - Valid values vary based on the record's I(type). In addition, + resource records that contain a DNS domain name in the value + field (e.g., CNAME, PTR, SRV, .etc) MUST include a trailing dot + in the value. + - Individual string values for TXT records must be enclosed in + double quotes. + - For resource records that have the same name but different + values (e.g., multiple A records), they must be defined as + multiple list entries in a single record. + required: false + aliases: ['value'] + ttl: + description: + - The amount of time in seconds that a resource record will remain + cached by a caching resolver. + required: false + default: 300 + overwrite: + description: + - Whether an attempt to overwrite an existing record should succeed + or fail. The behavior of this option depends on I(state). + - If I(state) is C(present) and I(overwrite) is C(True), this + module will replace an existing resource record of the same name + with the provided I(values). If I(state) is C(present) and + I(overwrite) is C(False), this module will fail if there is an + existing resource record with the same name and type, but + different resource data. + - If I(state) is C(absent) and I(overwrite) is C(True), this + module will remove the given resource record unconditionally. + If I(state) is C(absent) and I(overwrite) is C(False), this + module will fail if the provided values do not match exactly + with the existing resource record's values. + required: false + choices: [True, False] + default: False + service_account_email: + description: + - The e-mail address for a service account with access to Google + Cloud DNS. + required: false + default: null + pem_file: + description: + - The path to the PEM file associated with the service account + email. + - This option is deprecated and may be removed in a future release. + Use I(credentials_file) instead. + required: false + default: null + credentials_file: + description: + - The path to the JSON file associated with the service account + email. + required: false + default: null + project_id: + description: + - The Google Cloud Platform project ID to use. + required: false + default: null +notes: + - See also M(gcdns_zone). + - This modules's underlying library does not support in-place updates for + DNS resource records. Instead, resource records are quickly deleted and + recreated. + - SOA records are technically supported, but their functionality is limited + to verifying that a zone's existing SOA record matches a pre-determined + value. The SOA record cannot be updated. + - Root NS records cannot be updated. + - NAPTR records are not supported. +''' + +EXAMPLES = ''' +# Create an A record. +- gcdns_record: + record: 'www1.example.com' + zone: 'example.com' + type: A + value: '1.2.3.4' + +# Update an existing record. +- gcdns_record: + record: 'www1.example.com' + zone: 'example.com' + type: A + overwrite: true + value: '5.6.7.8' + +# Remove an A record. +- gcdns_record: + record: 'www1.example.com' + zone_id: 'example-com' + state: absent + type: A + value: '5.6.7.8' + +# Create a CNAME record. +- gcdns_record: + record: 'www.example.com' + zone_id: 'example-com' + type: CNAME + value: 'www.example.com.' # Note the trailing dot + +# Create an MX record with a custom TTL. +- gcdns_record: + record: 'example.com' + zone: 'example.com' + type: MX + ttl: 3600 + value: '10 mail.example.com.' # Note the trailing dot + +# Create multiple A records with the same name. +- gcdns_record: + record: 'api.example.com' + zone_id: 'example-com' + type: A + values: + - '10.1.2.3' + - '10.4.5.6' + - '10.7.8.9' + - '192.168.5.10' + +# Change the value of an existing record with multiple values. +- gcdns_record: + record: 'api.example.com' + zone: 'example.com' + type: A + overwrite: true + values: # WARNING: All values in a record will be replaced + - '10.1.2.3' + - '10.5.5.7' # The changed record + - '10.7.8.9' + - '192.168.5.10' + +# Safely remove a multi-line record. +- gcdns_record: + record: 'api.example.com' + zone_id: 'example-com' + state: absent + type: A + values: # NOTE: All of the values must match exactly + - '10.1.2.3' + - '10.5.5.7' + - '10.7.8.9' + - '192.168.5.10' + +# Unconditionally remove a record. +- gcdns_record: + record: 'api.example.com' + zone_id: 'example-com' + state: absent + overwrite: true # overwrite is true, so no values are needed + type: A + +# Create an AAAA record +- gcdns_record: + record: 'www1.example.com' + zone: 'example.com' + type: AAAA + value: 'fd00:db8::1' + +# Create a PTR record +- gcdns_record: + record: '10.5.168.192.in-addr.arpa' + zone: '5.168.192.in-addr.arpa' + type: PTR + value: 'api.example.com.' # Note the trailing dot. + +# Create an NS record +- gcdns_record: + record: 'subdomain.example.com' + zone: 'example.com' + type: NS + ttl: 21600 + values: + - 'ns-cloud-d1.googledomains.com.' # Note the trailing dots on values + - 'ns-cloud-d2.googledomains.com.' + - 'ns-cloud-d3.googledomains.com.' + - 'ns-cloud-d4.googledomains.com.' + +# Create a TXT record +- gcdns_record: + record: 'example.com' + zone_id: 'example-com' + type: TXT + values: + - '"v=spf1 include:_spf.google.com -all"' # A single-string TXT value + - '"hello " "world"' # A multi-string TXT value +''' + +RETURN = ''' +overwrite: + description: Whether to the module was allowed to overwrite the record + returned: success + type: boolean + sample: True +record: + description: Fully-qualified domain name of the resource record + returned: success + type: string + sample: mail.example.com. +state: + description: Whether the record is present or absent + returned: success + type: string + sample: present +ttl: + description: The time-to-live of the resource record + returned: success + type: int + sample: 300 +type: + description: The type of the resource record + returned: success + type: string + sample: A +values: + description: The resource record values + returned: success + type: list + sample: ['5.6.7.8', '9.10.11.12'] +zone: + description: The dns name of the zone + returned: success + type: string + sample: example.com. +zone_id: + description: The Google Cloud DNS ID of the zone + returned: success + type: string + sample: example-com +''' + + +################################################################################ +# Imports +################################################################################ + +import socket +from distutils.version import LooseVersion + +try: + from libcloud import __version__ as LIBCLOUD_VERSION + from libcloud.common.google import InvalidRequestError + from libcloud.common.types import LibcloudError + from libcloud.dns.types import Provider + from libcloud.dns.types import RecordDoesNotExistError + from libcloud.dns.types import ZoneDoesNotExistError + HAS_LIBCLOUD = True +except ImportError: + HAS_LIBCLOUD = False + + +################################################################################ +# Constants +################################################################################ + +# Apache libcloud 0.19.0 was the first to contain the non-beta Google Cloud DNS +# v1 API. Earlier versions contained the beta v1 API, which has since been +# deprecated and decommissioned. +MINIMUM_LIBCLOUD_VERSION = '0.19.0' + +# The libcloud Google Cloud DNS provider. +PROVIDER = Provider.GOOGLE + +# The records that libcloud's Google Cloud DNS provider supports. +# +# Libcloud has a RECORD_TYPE_MAP dictionary in the provider that also contains +# this information and is the authoritative source on which records are +# supported, but accessing the dictionary requires creating a Google Cloud DNS +# driver object, which is done in a helper module. +# +# I'm hard-coding the supported record types here, because they (hopefully!) +# shouldn't change much, and it allows me to use it as a "choices" parameter +# in an AnsibleModule argument_spec. +SUPPORTED_RECORD_TYPES = [ 'A', 'AAAA', 'CNAME', 'SRV', 'TXT', 'SOA', 'NS', 'MX', 'SPF', 'PTR' ] + + +################################################################################ +# Functions +################################################################################ + +def create_record(module, gcdns, zone, record): + """Creates or overwrites a resource record.""" + + overwrite = module.boolean(module.params['overwrite']) + record_name = module.params['record'] + record_type = module.params['type'] + ttl = module.params['ttl'] + values = module.params['values'] + data = dict(ttl=ttl, rrdatas=values) + + # Google Cloud DNS wants the trailing dot on all DNS names. + if record_name[-1] != '.': + record_name = record_name + '.' + + # If we found a record, we need to check if the values match. + if record is not None: + # If the record matches, we obviously don't have to change anything. + if _records_match(record.data['ttl'], record.data['rrdatas'], ttl, values): + return False + + # The record doesn't match, so we need to check if we can overwrite it. + if not overwrite: + module.fail_json( + msg = 'cannot overwrite existing record, overwrite protection enabled', + changed = False + ) + + # The record either doesn't exist, or it exists and we can overwrite it. + if record is None and not module.check_mode: + # There's no existing record, so we'll just create it. + try: + gcdns.create_record(record_name, zone, record_type, data) + except InvalidRequestError as error: + if error.code == 'invalid': + # The resource record name and type are valid by themselves, but + # not when combined (e.g., an 'A' record with "www.example.com" + # as its value). + module.fail_json( + msg = 'value is invalid for the given type: ' + + "%s, got value: %s" % (record_type, values), + changed = False + ) + + elif error.code == 'cnameResourceRecordSetConflict': + # We're attempting to create a CNAME resource record when we + # already have another type of resource record with the name + # domain name. + module.fail_json( + msg = "non-CNAME resource record already exists: %s" % record_name, + changed = False + ) + + else: + # The error is something else that we don't know how to handle, + # so we'll just re-raise the exception. + raise + + elif record is not None and not module.check_mode: + # The Google provider in libcloud doesn't support updating a record in + # place, so if the record already exists, we need to delete it and + # recreate it using the new information. + gcdns.delete_record(record) + + try: + gcdns.create_record(record_name, zone, record_type, data) + except InvalidRequestError: + # Something blew up when creating the record. This will usually be a + # result of invalid value data in the new record. Unfortunately, we + # already changed the state of the record by deleting the old one, + # so we'll try to roll back before failing out. + try: + gcdns.create_record(record.name, record.zone, record.type, record.data) + module.fail_json( + msg = 'error updating record, the original record was restored', + changed = False + ) + except LibcloudError: + # We deleted the old record, couldn't create the new record, and + # couldn't roll back. That really sucks. We'll dump the original + # record to the failure output so the user can resore it if + # necessary. + module.fail_json( + msg = 'error updating record, and could not restore original record, ' + + "original name: %s " % record.name + + "original zone: %s " % record.zone + + "original type: %s " % record.type + + "original data: %s" % record.data, + changed = True) + + return True + + +def remove_record(module, gcdns, record): + """Remove a resource record.""" + + overwrite = module.boolean(module.params['overwrite']) + ttl = module.params['ttl'] + values = module.params['values'] + + # If there is no record, we're obviously done. + if record is None: + return False + + # If there is an existing record, do our values match the values of the + # existing record? + if not overwrite: + if not _records_match(record.data['ttl'], record.data['rrdatas'], ttl, values): + module.fail_json( + msg = 'cannot delete due to non-matching ttl or values: ' + + "ttl: %d, values: %s " % (ttl, values) + + "original ttl: %d, original values: %s" % (record.data['ttl'], record.data['rrdatas']), + changed = False + ) + + # If we got to this point, we're okay to delete the record. + if not module.check_mode: + gcdns.delete_record(record) + + return True + + +def _get_record(gcdns, zone, record_type, record_name): + """Gets the record object for a given FQDN.""" + + # The record ID is a combination of its type and FQDN. For example, the + # ID of an A record for www.example.com would be 'A:www.example.com.' + record_id = "%s:%s" % (record_type, record_name) + + try: + return gcdns.get_record(zone.id, record_id) + except RecordDoesNotExistError: + return None + + +def _get_zone(gcdns, zone_name, zone_id): + """Gets the zone object for a given domain name.""" + + if zone_id is not None: + try: + return gcdns.get_zone(zone_id) + except ZoneDoesNotExistError: + return None + + # To create a zone, we need to supply a domain name. However, to delete a + # zone, we need to supply a zone ID. Zone ID's are often based on domain + # names, but that's not guaranteed, so we'll iterate through the list of + # zones to see if we can find a matching domain name. + available_zones = gcdns.iterate_zones() + found_zone = None + + for zone in available_zones: + if zone.domain == zone_name: + found_zone = zone + break + + return found_zone + + +def _records_match(old_ttl, old_values, new_ttl, new_values): + """Checks to see if original and new TTL and values match.""" + + matches = True + + if old_ttl != new_ttl: + matches = False + if old_values != new_values: + matches = False + + return matches + + +def _sanity_check(module): + """Run sanity checks that don't depend on info from the zone/record.""" + + overwrite = module.params['overwrite'] + record_name = module.params['record'] + record_type = module.params['type'] + state = module.params['state'] + ttl = module.params['ttl'] + values = module.params['values'] + + # Apache libcloud needs to be installed and at least the minimum version. + if not HAS_LIBCLOUD: + module.fail_json( + msg = 'This module requires Apache libcloud %s or greater' % MINIMUM_LIBCLOUD_VERSION, + changed = False + ) + elif LooseVersion(LIBCLOUD_VERSION) < MINIMUM_LIBCLOUD_VERSION: + module.fail_json( + msg = 'This module requires Apache libcloud %s or greater' % MINIMUM_LIBCLOUD_VERSION, + changed = False + ) + + # A negative TTL is not permitted (how would they even work?!). + if ttl < 0: + module.fail_json( + msg = 'TTL cannot be less than zero, got: %d' % ttl, + changed = False + ) + + # Deleting SOA records is not permitted. + if record_type == 'SOA' and state == 'absent': + module.fail_json(msg='cannot delete SOA records', changed=False) + + # Updating SOA records is not permitted. + if record_type == 'SOA' and state == 'present' and overwrite: + module.fail_json(msg='cannot update SOA records', changed=False) + + # Some sanity checks depend on what value was supplied. + if values is not None and (state == 'present' or not overwrite): + # A records must contain valid IPv4 addresses. + if record_type == 'A': + for value in values: + try: + socket.inet_aton(value) + except socket.error: + module.fail_json( + msg = 'invalid A record value, got: %s' % value, + changed = False + ) + + # AAAA records must contain valid IPv6 addresses. + if record_type == 'AAAA': + for value in values: + try: + socket.inet_pton(socket.AF_INET6, value) + except socket.error: + module.fail_json( + msg = 'invalid AAAA record value, got: %s' % value, + changed = False + ) + + # CNAME and SOA records can't have multiple values. + if record_type in ['CNAME', 'SOA'] and len(values) > 1: + module.fail_json( + msg = 'CNAME or SOA records cannot have more than one value, ' + + "got: %s" % values, + changed = False + ) + + # Google Cloud DNS does not support wildcard NS records. + if record_type == 'NS' and record_name[0] == '*': + module.fail_json( + msg = "wildcard NS records not allowed, got: %s" % record_name, + changed = False + ) + + # Values for txt records must begin and end with a double quote. + if record_type == 'TXT': + for value in values: + if value[0] != '"' and value[-1] != '"': + module.fail_json( + msg = 'TXT values must be enclosed in double quotes, ' + + 'got: %s' % value, + changed = False + ) + + +def _additional_sanity_checks(module, zone): + """Run input sanity checks that depend on info from the zone/record.""" + + overwrite = module.params['overwrite'] + record_name = module.params['record'] + record_type = module.params['type'] + state = module.params['state'] + + # CNAME records are not allowed to have the same name as the root domain. + if record_type == 'CNAME' and record_name == zone.domain: + module.fail_json( + msg = 'CNAME records cannot match the zone name', + changed = False + ) + + # The root domain must always have an NS record. + if record_type == 'NS' and record_name == zone.domain and state == 'absent': + module.fail_json( + msg = 'cannot delete root NS records', + changed = False + ) + + # Updating NS records with the name as the root domain is not allowed + # because libcloud does not support in-place updates and root domain NS + # records cannot be removed. + if record_type == 'NS' and record_name == zone.domain and overwrite: + module.fail_json( + msg = 'cannot update existing root NS records', + changed = False + ) + + # SOA records with names that don't match the root domain are not permitted + # (and wouldn't make sense anyway). + if record_type == 'SOA' and record_name != zone.domain: + module.fail_json( + msg = 'non-root SOA records are not permitted, got: %s' % record_name, + changed = False + ) + + +################################################################################ +# Main +################################################################################ + +def main(): + """Main function""" + + module = AnsibleModule( + argument_spec = dict( + state = dict(default='present', choices=['present', 'absent'], type='str'), + record = dict(required=True, aliases=['name'], type='str'), + zone = dict(type='str'), + zone_id = dict(type='str'), + type = dict(required=True, choices=SUPPORTED_RECORD_TYPES, type='str'), + values = dict(aliases=['value'], type='list'), + ttl = dict(default=300, type='int'), + overwrite = dict(default=False, type='bool'), + service_account_email = dict(type='str'), + pem_file = dict(type='path'), + credentials_file = dict(type='path'), + project_id = dict(type='str') + ), + required_if = [ + ('state', 'present', ['values']), + ('overwrite', False, ['values']) + ], + required_one_of = [['zone', 'zone_id']], + supports_check_mode = True + ) + + _sanity_check(module) + + record_name = module.params['record'] + record_type = module.params['type'] + state = module.params['state'] + ttl = module.params['ttl'] + zone_name = module.params['zone'] + zone_id = module.params['zone_id'] + + json_output = dict( + state = state, + record = record_name, + zone = zone_name, + zone_id = zone_id, + type = record_type, + values = module.params['values'], + ttl = ttl, + overwrite = module.boolean(module.params['overwrite']) + ) + + # Google Cloud DNS wants the trailing dot on all DNS names. + if zone_name is not None and zone_name[-1] != '.': + zone_name = zone_name + '.' + if record_name[-1] != '.': + record_name = record_name + '.' + + # Build a connection object that we can use to connect with Google Cloud + # DNS. + gcdns = gcdns_connect(module, provider=PROVIDER) + + # We need to check that the zone we're creating a record for actually + # exists. + zone = _get_zone(gcdns, zone_name, zone_id) + if zone is None and zone_name is not None: + module.fail_json( + msg = 'zone name was not found: %s' % zone_name, + changed = False + ) + elif zone is None and zone_id is not None: + module.fail_json( + msg = 'zone id was not found: %s' % zone_id, + changed = False + ) + + # Populate the returns with the actual zone information. + json_output['zone'] = zone.domain + json_output['zone_id'] = zone.id + + # We also need to check if the record we want to create or remove actually + # exists. + try: + record = _get_record(gcdns, zone, record_type, record_name) + except InvalidRequestError: + # We gave Google Cloud DNS an invalid DNS record name. + module.fail_json( + msg = 'record name is invalid: %s' % record_name, + changed = False + ) + + _additional_sanity_checks(module, zone) + + diff = dict() + + # Build the 'before' diff + if record is None: + diff['before'] = '' + diff['before_header'] = '' + else: + diff['before'] = dict( + record = record.data['name'], + type = record.data['type'], + values = record.data['rrdatas'], + ttl = record.data['ttl'] + ) + diff['before_header'] = "%s:%s" % (record_type, record_name) + + # Create, remove, or modify the record. + if state == 'present': + diff['after'] = dict( + record = record_name, + type = record_type, + values = module.params['values'], + ttl = ttl + ) + diff['after_header'] = "%s:%s" % (record_type, record_name) + + changed = create_record(module, gcdns, zone, record) + + elif state == 'absent': + diff['after'] = '' + diff['after_header'] = '' + + changed = remove_record(module, gcdns, record) + + module.exit_json(changed=changed, diff=diff, **json_output) + + +from ansible.module_utils.basic import * +from ansible.module_utils.gcdns import * + +if __name__ == '__main__': + main() diff --git a/cloud/google/gcdns_zone.py b/cloud/google/gcdns_zone.py new file mode 100644 index 00000000000..4b7bd16985b --- /dev/null +++ b/cloud/google/gcdns_zone.py @@ -0,0 +1,381 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2015 CallFire Inc. +# +# This file is part of Ansible. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +################################################################################ +# Documentation +################################################################################ + +DOCUMENTATION = ''' +--- +module: gcdns_zone +short_description: Creates or removes zones in Google Cloud DNS +description: + - Creates or removes managed zones in Google Cloud DNS. +version_added: "2.2" +author: "William Albert (@walbert947)" +requirements: + - "python >= 2.6" + - "apache-libcloud >= 0.19.0" +options: + state: + description: + - Whether the given zone should or should not be present. + required: false + choices: ["present", "absent"] + default: "present" + zone: + description: + - The DNS domain name of the zone. + - This is NOT the Google Cloud DNS zone ID (e.g., example-com). If + you attempt to specify a zone ID, this module will attempt to + create a TLD and will fail. + required: true + aliases: ['name'] + description: + description: + - An arbitrary text string to use for the zone description. + required: false + default: "" + service_account_email: + description: + - The e-mail address for a service account with access to Google + Cloud DNS. + required: false + default: null + pem_file: + description: + - The path to the PEM file associated with the service account + email. + - This option is deprecated and may be removed in a future release. + Use I(credentials_file) instead. + required: false + default: null + credentials_file: + description: + - The path to the JSON file associated with the service account + email. + required: false + default: null + project_id: + description: + - The Google Cloud Platform project ID to use. + required: false + default: null +notes: + - See also M(gcdns_record). + - Zones that are newly created must still be set up with a domain registrar + before they can be used. +''' + +EXAMPLES = ''' +# Basic zone creation example. +- name: Create a basic zone with the minimum number of parameters. + gcdns_zone: zone=example.com + +# Zone removal example. +- name: Remove a zone. + gcdns_zone: zone=example.com state=absent + +# Zone creation with description +- name: Creating a zone with a description + gcdns_zone: zone=example.com description="This is an awesome zone" +''' + +RETURN = ''' +description: + description: The zone's description + returned: success + type: string + sample: This is an awesome zone +state: + description: Whether the zone is present or absent + returned: success + type: string + sample: present +zone: + description: The zone's DNS name + returned: success + type: string + sample: example.com. +''' + + +################################################################################ +# Imports +################################################################################ + +from distutils.version import LooseVersion + +try: + from libcloud import __version__ as LIBCLOUD_VERSION + from libcloud.common.google import InvalidRequestError + from libcloud.common.google import ResourceExistsError + from libcloud.common.google import ResourceNotFoundError + from libcloud.dns.types import Provider + HAS_LIBCLOUD = True +except ImportError: + HAS_LIBCLOUD = False + + +################################################################################ +# Constants +################################################################################ + +# Apache libcloud 0.19.0 was the first to contain the non-beta Google Cloud DNS +# v1 API. Earlier versions contained the beta v1 API, which has since been +# deprecated and decommissioned. +MINIMUM_LIBCLOUD_VERSION = '0.19.0' + +# The libcloud Google Cloud DNS provider. +PROVIDER = Provider.GOOGLE + +# The URL used to verify ownership of a zone in Google Cloud DNS. +ZONE_VERIFICATION_URL= 'https://www.google.com/webmasters/verification/' + +################################################################################ +# Functions +################################################################################ + +def create_zone(module, gcdns, zone): + """Creates a new Google Cloud DNS zone.""" + + description = module.params['description'] + extra = dict(description = description) + zone_name = module.params['zone'] + + # Google Cloud DNS wants the trailing dot on the domain name. + if zone_name[-1] != '.': + zone_name = zone_name + '.' + + # If we got a zone back, then the domain exists. + if zone is not None: + return False + + # The zone doesn't exist yet. + try: + if not module.check_mode: + gcdns.create_zone(domain=zone_name, extra=extra) + return True + + except ResourceExistsError: + # The zone already exists. We checked for this already, so either + # Google is lying, or someone was a ninja and created the zone + # within milliseconds of us checking for its existence. In any case, + # the zone has already been created, so we have nothing more to do. + return False + + except InvalidRequestError as error: + if error.code == 'invalid': + # The zone name or a parameter might be completely invalid. This is + # typically caused by an illegal DNS name (e.g. foo..com). + module.fail_json( + msg = "zone name is not a valid DNS name: %s" % zone_name, + changed = False + ) + + elif error.code == 'managedZoneDnsNameNotAvailable': + # Google Cloud DNS will refuse to create zones with certain domain + # names, such as TLDs, ccTLDs, or special domain names such as + # example.com. + module.fail_json( + msg = "zone name is reserved or already in use: %s" % zone_name, + changed = False + ) + + elif error.code == 'verifyManagedZoneDnsNameOwnership': + # This domain name needs to be verified before Google will create + # it. This occurs when a user attempts to create a zone which shares + # a domain name with a zone hosted elsewhere in Google Cloud DNS. + module.fail_json( + msg = "ownership of zone %s needs to be verified at %s" % (zone_name, ZONE_VERIFICATION_URL), + changed = False + ) + + else: + # The error is something else that we don't know how to handle, + # so we'll just re-raise the exception. + raise + + +def remove_zone(module, gcdns, zone): + """Removes an existing Google Cloud DNS zone.""" + + # If there's no zone, then we're obviously done. + if zone is None: + return False + + # An empty zone will have two resource records: + # 1. An NS record with a list of authoritative name servers + # 2. An SOA record + # If any additional resource records are present, Google Cloud DNS will + # refuse to remove the zone. + if len(zone.list_records()) > 2: + module.fail_json( + msg = "zone is not empty and cannot be removed: %s" % zone.domain, + changed = False + ) + + try: + if not module.check_mode: + gcdns.delete_zone(zone) + return True + + except ResourceNotFoundError: + # When we performed our check, the zone existed. It may have been + # deleted by something else. It's gone, so whatever. + return False + + except InvalidRequestError as error: + if error.code == 'containerNotEmpty': + # When we performed our check, the zone existed and was empty. In + # the milliseconds between the check and the removal command, + # records were added to the zone. + module.fail_json( + msg = "zone is not empty and cannot be removed: %s" % zone.domain, + changed = False + ) + + else: + # The error is something else that we don't know how to handle, + # so we'll just re-raise the exception. + raise + + +def _get_zone(gcdns, zone_name): + """Gets the zone object for a given domain name.""" + + # To create a zone, we need to supply a zone name. However, to delete a + # zone, we need to supply a zone ID. Zone ID's are often based on zone + # names, but that's not guaranteed, so we'll iterate through the list of + # zones to see if we can find a matching name. + available_zones = gcdns.iterate_zones() + found_zone = None + + for zone in available_zones: + if zone.domain == zone_name: + found_zone = zone + break + + return found_zone + +def _sanity_check(module): + """Run module sanity checks.""" + + zone_name = module.params['zone'] + + # Apache libcloud needs to be installed and at least the minimum version. + if not HAS_LIBCLOUD: + module.fail_json( + msg = 'This module requires Apache libcloud %s or greater' % MINIMUM_LIBCLOUD_VERSION, + changed = False + ) + elif LooseVersion(LIBCLOUD_VERSION) < MINIMUM_LIBCLOUD_VERSION: + module.fail_json( + msg = 'This module requires Apache libcloud %s or greater' % MINIMUM_LIBCLOUD_VERSION, + changed = False + ) + + # Google Cloud DNS does not support the creation of TLDs. + if '.' not in zone_name or len([label for label in zone_name.split('.') if label]) == 1: + module.fail_json( + msg = 'cannot create top-level domain: %s' % zone_name, + changed = False + ) + +################################################################################ +# Main +################################################################################ + +def main(): + """Main function""" + + module = AnsibleModule( + argument_spec = dict( + state = dict(default='present', choices=['present', 'absent'], type='str'), + zone = dict(required=True, aliases=['name'], type='str'), + description = dict(default='', type='str'), + service_account_email = dict(type='str'), + pem_file = dict(type='path'), + credentials_file = dict(type='path'), + project_id = dict(type='str') + ), + supports_check_mode = True + ) + + _sanity_check(module) + + zone_name = module.params['zone'] + state = module.params['state'] + + # Google Cloud DNS wants the trailing dot on the domain name. + if zone_name[-1] != '.': + zone_name = zone_name + '.' + + json_output = dict( + state = state, + zone = zone_name, + description = module.params['description'] + ) + + # Build a connection object that was can use to connect with Google + # Cloud DNS. + gcdns = gcdns_connect(module, provider=PROVIDER) + + # We need to check if the zone we're attempting to create already exists. + zone = _get_zone(gcdns, zone_name) + + diff = dict() + + # Build the 'before' diff + if zone is None: + diff['before'] = '' + diff['before_header'] = '' + else: + diff['before'] = dict( + zone = zone.domain, + description = zone.extra['description'] + ) + diff['before_header'] = zone_name + + # Create or remove the zone. + if state == 'present': + diff['after'] = dict( + zone = zone_name, + description = module.params['description'] + ) + diff['after_header'] = zone_name + + changed = create_zone(module, gcdns, zone) + + elif state == 'absent': + diff['after'] = '' + diff['after_header'] = '' + + changed = remove_zone(module, gcdns, zone) + + module.exit_json(changed=changed, diff=diff, **json_output) + + +from ansible.module_utils.basic import * +from ansible.module_utils.gcdns import * + +if __name__ == '__main__': + main() From c46eb7d64ba79a126004e05ac380b56db029c79f Mon Sep 17 00:00:00 2001 From: Robert Estelle Date: Thu, 28 Jul 2016 13:12:05 -0400 Subject: [PATCH 1875/2522] New module - github_key (#692) Create github_key module for managing GitHub keys. This module creates, removes, or updates GitHub access keys. --- source_control/github_key.py | 224 +++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 source_control/github_key.py diff --git a/source_control/github_key.py b/source_control/github_key.py new file mode 100644 index 00000000000..ef7ea59d5e6 --- /dev/null +++ b/source_control/github_key.py @@ -0,0 +1,224 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +module: github_key +short_description: Manage GitHub access keys. +description: + - Creates, removes, or updates GitHub access keys. +version_added: "2.0" +options: + token: + description: + - GitHub Access Token with permission to list and create public keys. + required: true + name: + description: + - SSH key name + required: true + pubkey: + description: + - SSH public key value. Required when C(state=present). + required: false + default: none + state: + description: + - Whether to remove a key, ensure that it exists, or update its value. + choices: ['present', 'absent'] + default: 'present' + required: false + force: + description: + - The default is C(yes), which will replace the existing remote key + if it's different than C(pubkey). If C(no), the key will only be + set if no key with the given C(name) exists. + required: false + choices: ['yes', 'no'] + default: 'yes' + +author: Robert Estelle (@erydo) +''' + +EXAMPLES = ''' +- name: Read SSH public key to authorize + shell: cat /home/foo/.ssh/id_rsa.pub + register: ssh_pub_key + +- name: Authorize key with GitHub + local_action: + module: github_key + name: 'Access Key for Some Machine' + token: '{{github_access_token}}' + pubkey: '{{ssh_pub_key.stdout}}' +''' + + +import sys # noqa +import json +import re + + +API_BASE = 'https://api.github.com' + + +class GitHubResponse(object): + def __init__(self, response, info): + self.content = response.read() + self.info = info + + def json(self): + return json.loads(self.content) + + def links(self): + links = {} + if 'link' in self.info: + link_header = re.info['link'] + matches = re.findall('<([^>]+)>; rel="([^"]+)"', link_header) + for url, rel in matches: + links[rel] = url + return links + + +class GitHubSession(object): + def __init__(self, module, token): + self.module = module + self.token = token + + def request(self, method, url, data=None): + headers = { + 'Authorization': 'token {}'.format(self.token), + 'Content-Type': 'application/json', + 'Accept': 'application/vnd.github.v3+json', + } + response, info = fetch_url( + self.module, url, method=method, data=data, headers=headers) + if not (200 <= info['status'] < 400): + self.module.fail_json( + msg=(" failed to send request %s to %s: %s" + % (method, url, info['msg']))) + return GitHubResponse(response, info) + + +def get_all_keys(session): + url = API_BASE + '/user/keys' + while url: + r = session.request('GET', url) + for key in r.json(): + yield key + + url = r.links().get('next') + + +def create_key(session, name, pubkey, check_mode): + if check_mode: + from datetime import datetime + now = datetime.utcnow() + return { + 'id': 0, + 'key': pubkey, + 'title': name, + 'url': 'http://example.com/CHECK_MODE_GITHUB_KEY', + 'created_at': datetime.strftime(now, '%Y-%m-%dT%H:%M:%SZ'), + 'read_only': False, + 'verified': False + } + else: + return session.request( + 'POST', + API_BASE + '/user/keys', + data=json.dumps({'title': name, 'key': pubkey})).json() + + +def delete_keys(session, to_delete, check_mode): + if check_mode: + return + + for key in to_delete: + session.request('DELETE', API_BASE + '/user/keys/{[id]}'.format(key)) + + +def ensure_key_absent(session, name, check_mode): + to_delete = [key for key in get_all_keys(session) if key['title'] == name] + delete_keys(session, to_delete, check_mode=check_mode) + + return {'changed': bool(to_delete), 'deleted_keys': to_delete} + + +def ensure_key_present(session, name, pubkey, force, check_mode): + matching_keys = [k for k in get_all_keys(session) if k['title'] == name] + deleted_keys = [] + + if matching_keys and force and matching_keys[0]['key'] != pubkey: + delete_keys(session, matching_keys, check_mode=check_mode) + (deleted_keys, matching_keys) = (matching_keys, []) + + if not matching_keys: + key = create_key(session, name, pubkey, check_mode=check_mode) + else: + key = matching_keys[0] + + return { + 'changed': bool(deleted_keys or not matching_keys), + 'deleted_keys': deleted_keys, + 'matching_keys': matching_keys, + 'key': key + } + + +def main(): + argument_spec = { + 'token': {'required': True}, + 'name': {'required': True}, + 'pubkey': {}, + 'state': {'choices': ['present', 'absent'], 'default': 'present'}, + 'force': {'default': True, 'type': 'bool'}, + } + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + token = module.params['token'] + name = module.params['name'] + state = module.params['state'] + force = module.params['force'] + pubkey = module.params.get('pubkey') + + if pubkey: + pubkey_parts = pubkey.split(' ') + # Keys consist of a protocol, the key data, and an optional comment. + if len(pubkey_parts) < 2: + module.fail_json(msg='"pubkey" parameter has an invalid format') + + # Strip out comment so we can compare to the keys GitHub returns. + pubkey = ' '.join(pubkey_parts[:2]) + elif state == 'present': + module.fail_json(msg='"pubkey" is required when state=present') + + session = GitHubSession(module, token) + if state == 'present': + result = ensure_key_present(session, name, pubkey, force=force, + check_mode=module.check_mode) + elif state == 'absent': + result = ensure_key_absent(session, name, check_mode=module.check_mode) + + module.exit_json(**result) + +from ansible.module_utils.basic import * # noqa +from ansible.module_utils.urls import * # noqa + +if __name__ == '__main__': + main() From f6841eb51fb57f2121bed54d12b1e994d5feb592 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 28 Jul 2016 15:19:14 -0400 Subject: [PATCH 1876/2522] send json string to api instead of dict --- cloud/amazon/s3_bucket.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/s3_bucket.py b/cloud/amazon/s3_bucket.py index 7b283bf313e..35d4077c39a 100644 --- a/cloud/amazon/s3_bucket.py +++ b/cloud/amazon/s3_bucket.py @@ -203,7 +203,7 @@ def _create_or_update_bucket(connection, module, location): if current_policy is None or json.loads(current_policy) != compare_policy: try: - bucket.set_policy(load_policy) + bucket.set_policy(policy) changed = True current_policy = bucket.get_policy() except S3ResponseError as e: From a69aa6008160014b902a075be12c7abbf19a0d3c Mon Sep 17 00:00:00 2001 From: "Ryan S. Brown" Date: Thu, 28 Jul 2016 16:01:08 -0400 Subject: [PATCH 1877/2522] Correct version_added for `github_key` module - was 2.0, now 2.2 --- source_control/github_key.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source_control/github_key.py b/source_control/github_key.py index ef7ea59d5e6..8f8279e6e6d 100644 --- a/source_control/github_key.py +++ b/source_control/github_key.py @@ -19,7 +19,7 @@ short_description: Manage GitHub access keys. description: - Creates, removes, or updates GitHub access keys. -version_added: "2.0" +version_added: "2.2" options: token: description: From 9006c8018db094eb63818f0b083b2dccce668d97 Mon Sep 17 00:00:00 2001 From: Adrian Likins Date: Thu, 28 Jul 2016 16:59:22 -0400 Subject: [PATCH 1878/2522] Use py2.4 compat string formatting in github_key (#2633) Replace the use of python 2.6+ string .format() method use with the python 2.4 compatible '%s' formatting to make the github_key module py2.4 compatible. --- source_control/github_key.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source_control/github_key.py b/source_control/github_key.py index 8f8279e6e6d..3b887b85372 100644 --- a/source_control/github_key.py +++ b/source_control/github_key.py @@ -99,7 +99,7 @@ def __init__(self, module, token): def request(self, method, url, data=None): headers = { - 'Authorization': 'token {}'.format(self.token), + 'Authorization': 'token %s' % self.token, 'Content-Type': 'application/json', 'Accept': 'application/vnd.github.v3+json', } @@ -147,7 +147,7 @@ def delete_keys(session, to_delete, check_mode): return for key in to_delete: - session.request('DELETE', API_BASE + '/user/keys/{[id]}'.format(key)) + session.request('DELETE', API_BASE + '/user/keys/%s' % key[id]) def ensure_key_absent(session, name, check_mode): From 41a0f22fe83c754813ffc0fbc7d7588b2e999559 Mon Sep 17 00:00:00 2001 From: Adrian Likins Date: Thu, 28 Jul 2016 17:00:41 -0400 Subject: [PATCH 1879/2522] Add a 'requirements:' field to homebrew doc (#2630) homebrew.py and homebrew_cask.py make use of python 2.5 and 2.6 features like string .format() method. --- packaging/os/homebrew.py | 2 ++ packaging/os/homebrew_cask.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packaging/os/homebrew.py b/packaging/os/homebrew.py index 077fd46dcc6..a91a8ab8fe3 100755 --- a/packaging/os/homebrew.py +++ b/packaging/os/homebrew.py @@ -27,6 +27,8 @@ - "Indrajit Raychaudhuri (@indrajitr)" - "Daniel Jaouen (@danieljaouen)" - "Andrew Dunham (@andrew-d)" +requirements: + - "python >= 2.6" short_description: Package manager for Homebrew description: - Manages Homebrew packages diff --git a/packaging/os/homebrew_cask.py b/packaging/os/homebrew_cask.py index 7b16b71f187..8353c1cece2 100755 --- a/packaging/os/homebrew_cask.py +++ b/packaging/os/homebrew_cask.py @@ -24,6 +24,8 @@ - "Indrajit Raychaudhuri (@indrajitr)" - "Daniel Jaouen (@danieljaouen)" - "Enric Lluelles (@enriclluelles)" +requirements: + - "python >= 2.6" short_description: Install/uninstall homebrew casks. description: - Manages Homebrew casks. From 28bb69bd41dafd7ce50c2fa5c8a94d4ecd8c109d Mon Sep 17 00:00:00 2001 From: Adrian Likins Date: Thu, 28 Jul 2016 17:02:00 -0400 Subject: [PATCH 1880/2522] Use %s string formatting in system/timezone.py (#2632) This module was using python 2.6 string .format(). To enable the module to run on python2.4, replace the .format formatting with '%s' based string formatting. There was also a use of a 'filename' variable in the NosystemdTimezone.get() method that was never set. An import of 'os' was also added for clarity. --- system/timezone.py | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/system/timezone.py b/system/timezone.py index c65d7049338..e5337bfd9c8 100644 --- a/system/timezone.py +++ b/system/timezone.py @@ -18,6 +18,7 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +import os import re DOCUMENTATION = ''' @@ -146,7 +147,7 @@ def execute(self, *commands, **kwargs): command = ' '.join(commands) (rc, stdout, stderr) = self.module.run_command(command, check_rc=True) if kwargs.get('log', False): - self.msg.append('executed `{0}`'.format(command)) + self.msg.append('executed `%s`' % command) return stdout def diff(self, phase1='before', phase2='after'): @@ -238,9 +239,9 @@ def __init__(self, module): # Validate given timezone if 'name' in self.value: tz = self.value['name']['planned'] - tzfile = '/usr/share/zoneinfo/{0}'.format(tz) + tzfile = '/usr/share/zoneinfo/%s' % tz if not os.path.isfile(tzfile): - self.abort('given timezone "{0}" is not available'.format(tz)) + self.abort('given timezone "%s" is not available' % tz) def _get_status(self, phase): if phase not in self.status: @@ -295,11 +296,11 @@ def __init__(self, module): # Validate given timezone if 'name' in self.value: tz = self.value['name']['planned'] - tzfile = '/usr/share/zoneinfo/{0}'.format(tz) + tzfile = '/usr/share/zoneinfo/%s' % tz if not os.path.isfile(tzfile): - self.abort('given timezone "{0}" is not available'.format(tz)) + self.abort('given timezone "%s" is not available' % tz) self.update_timezone = self.module.get_bin_path('cp', required=True) - self.update_timezone += ' {0} /etc/localtime'.format(tzfile) + self.update_timezone += ' %s /etc/localtime' % tzfile self.update_hwclock = self.module.get_bin_path('hwclock', required=True) # Distribution-specific configurations if self.module.get_bin_path('dpkg-reconfigure') is not None: @@ -309,7 +310,7 @@ def __init__(self, module): self.conf_files['name'] = '/etc/timezone', self.conf_files['hwclock'] = '/etc/default/rcS', self.regexps['name'] = re.compile(r'^([^\s]+)', re.MULTILINE) - self.tzline_format = '{0}\n' + self.tzline_format = '%s\n' else: # RHEL/CentOS if self.module.get_bin_path('tzdata-update') is not None: @@ -319,7 +320,7 @@ def __init__(self, module): self.conf_files['name'] = '/etc/sysconfig/clock' self.conf_files['hwclock'] = '/etc/sysconfig/clock' self.regexps['name'] = re.compile(r'^ZONE\s*=\s*"?([^"\s]+)"?', re.MULTILINE) - self.tzline_format = 'ZONE="{0}"\n' + self.tzline_format = 'ZONE="%s"\n' self.update_hwclock = self.module.get_bin_path('hwclock', required=True) def _edit_file(self, filename, regexp, value): @@ -336,7 +337,7 @@ def _edit_file(self, filename, regexp, value): try: file = open(filename, 'r') except IOError: - self.abort('cannot read "{0}"'.format(filename)) + self.abort('cannot read "%s"' % filename) else: lines = file.readlines() file.close() @@ -358,27 +359,30 @@ def _edit_file(self, filename, regexp, value): try: file = open(filename, 'w') except IOError: - self.abort('cannot write to "{0}"'.format(filename)) + self.abort('cannot write to "%s"' % filename) else: file.writelines(lines) file.close() - self.msg.append('Added 1 line and deleted {0} line(s) on {1}'.format(len(matched_indices), filename)) + self.msg.append('Added 1 line and deleted %s line(s) on %s' % (len(matched_indices), filename)) def get(self, key, phase): if key == 'hwclock' and os.path.isfile('/etc/adjtime'): # If /etc/adjtime exists, use that file. key = 'adjtime' + + filename = self.conf_files[key] + try: - file = open(self.conf_files[key], mode='r') + file = open(filename, mode='r') except IOError: - self.abort('cannot read configuration file "{0}" for {1}'.format(filename, key)) + self.abort('cannot read configuration file "%s" for %s' % (filename, key)) else: status = file.read() file.close() try: value = self.regexps[key].search(status).group(1) except AttributeError: - self.abort('cannot find the valid value from configuration file "{0}" for {1}'.format(filename, key)) + self.abort('cannot find the valid value from configuration file "%s" for %s' % (filename, key)) else: if key == 'hwclock': # For key='hwclock'; convert yes/no -> UTC/local @@ -395,7 +399,7 @@ def get(self, key, phase): def set_timezone(self, value): self._edit_file(filename=self.conf_files['name'], regexp=self.regexps['name'], - value=self.tzline_format.format(value)) + value=self.tzline_format.format % value) self.execute(self.update_timezone) def set_hwclock(self, value): @@ -411,7 +415,7 @@ def set(self, key, value): elif key == 'hwclock': self.set_hwclock(value) else: - self.abort('unknown parameter "{0}"'.format(key)) + self.abort('unknown parameter "%s"' % key) def main(): From f4e34d5c3ac2ba9592ad53586c6f65629cf6ce28 Mon Sep 17 00:00:00 2001 From: Adrian Likins Date: Thu, 28 Jul 2016 17:44:04 -0400 Subject: [PATCH 1881/2522] Add RETURN docs for github_key (#2634) --- source_control/github_key.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/source_control/github_key.py b/source_control/github_key.py index 3b887b85372..fb5f8f86562 100644 --- a/source_control/github_key.py +++ b/source_control/github_key.py @@ -52,6 +52,22 @@ author: Robert Estelle (@erydo) ''' +RETURN = ''' +deleted_keys: + description: Keys that were deleted, if any. + returned: When state=absent + type: list + +matching_keys: + description: Keys that match the specified name. + returned: When state=present + type: list + +key: + description: The key created. + returned: When state=present and a new key is created. +''' + EXAMPLES = ''' - name: Read SSH public key to authorize shell: cat /home/foo/.ssh/id_rsa.pub @@ -154,7 +170,8 @@ def ensure_key_absent(session, name, check_mode): to_delete = [key for key in get_all_keys(session) if key['title'] == name] delete_keys(session, to_delete, check_mode=check_mode) - return {'changed': bool(to_delete), 'deleted_keys': to_delete} + return {'changed': bool(to_delete), + 'deleted_keys': to_delete} def ensure_key_present(session, name, pubkey, force, check_mode): From baaaf7f96f7d4e8aa059b9263a9d4f2dd71c72eb Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Thu, 28 Jul 2016 23:12:32 -0700 Subject: [PATCH 1882/2522] Enable Windows tests on Shippable. (#2635) --- shippable.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/shippable.yml b/shippable.yml index 5385520ba30..f05d2481478 100644 --- a/shippable.yml +++ b/shippable.yml @@ -16,6 +16,11 @@ matrix: - env: TEST=integration IMAGE=ansible/ansible:ubuntu1204 PRIVILEGED=true - env: TEST=integration IMAGE=ansible/ansible:ubuntu1404 PRIVILEGED=true - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604 + + - env: TEST=integration PLATFORM=windows VERSION=2008-SP2 + - env: TEST=integration PLATFORM=windows VERSION=2008-R2_SP1 + - env: TEST=integration PLATFORM=windows VERSION=2012-RTM + - env: TEST=integration PLATFORM=windows VERSION=2012-R2_RTM build: pre_ci_boot: options: "--privileged=false --net=bridge" From 84298ab5ce38cc7456473bde826cd04b42f6290d Mon Sep 17 00:00:00 2001 From: Ryan Brown Date: Fri, 29 Jul 2016 12:31:55 -0400 Subject: [PATCH 1883/2522] Improve module docs (#2638) --- source_control/github_key.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/source_control/github_key.py b/source_control/github_key.py index fb5f8f86562..815be9dc94b 100644 --- a/source_control/github_key.py +++ b/source_control/github_key.py @@ -54,18 +54,20 @@ RETURN = ''' deleted_keys: - description: Keys that were deleted, if any. - returned: When state=absent + description: An array of key objects that were deleted. Only present on state=absent type: list - + returned: When state=absent + sample: [{'id': 0, 'key': 'BASE64 encoded key', 'url': 'http://example.com/github key', 'created_at': 'YYYY-MM-DDTHH:MM:SZ', 'read_only': False}] matching_keys: - description: Keys that match the specified name. - returned: When state=present + description: An array of keys matching the specified name. Only present on state=present type: list - + returned: When state=present + sample: [{'id': 0, 'key': 'BASE64 encoded key', 'url': 'http://example.com/github key', 'created_at': 'YYYY-MM-DDTHH:MM:SZ', 'read_only': False}] key: - description: The key created. - returned: When state=present and a new key is created. + description: Metadata about the key just created. Only present on state=present + type: dict + returned: success + sample: {'id': 0, 'key': 'BASE64 encoded key', 'url': 'http://example.com/github key', 'created_at': 'YYYY-MM-DDTHH:MM:SZ', 'read_only': False} ''' EXAMPLES = ''' From ccec9b1645458d60a07a4a5b1bb8759a1535c8f0 Mon Sep 17 00:00:00 2001 From: Jonathan Davila Date: Fri, 29 Jul 2016 22:17:56 -0400 Subject: [PATCH 1884/2522] Updates to Sendgrid Module (#1275) --- notification/sendgrid.py | 162 ++++++++++++++++++++++++++++++++------- 1 file changed, 135 insertions(+), 27 deletions(-) diff --git a/notification/sendgrid.py b/notification/sendgrid.py index 1bac1e5f724..7e4593a9a90 100644 --- a/notification/sendgrid.py +++ b/notification/sendgrid.py @@ -32,13 +32,17 @@ - "Like the other notification modules, this one requires an external dependency to work. In this case, you'll need an active SendGrid account." + - "In order to use api_key, cc, bcc, attachments, from_name, html_body, headers + you must pip install sendgrid" +requirements: + - sendgrid python library options: username: description: - username for logging into the SendGrid account required: true password: - description: + description: - password that corresponds to the username required: true from_address: @@ -53,6 +57,35 @@ description: - the desired subject for the email required: true + api_key: + description: + - sendgrid API key to use instead of username/password + version_added: 2.1 + cc: + description: + - a list of email addresses to cc + version_added: 2.1 + bcc: + description: + - a list of email addresses to bcc + version_added: 2.1 + attachments: + description: + - a list of relative or explicit paths of files you want to attach (7MB limit as per SendGrid docs) + version_added: 2.1 + from_name: + description: + - the name you want to appear in the from field, i.e 'John Doe' + version_added: 2.1 + html_body: + description: + - whether the body is html content that should be rendered + version_added: 2.1 + choices: [True, False] + headers: + description: + - a dict to pass on as headers + version_added: 2.1 author: "Matt Makai (@makaimc)" ''' @@ -87,26 +120,72 @@ # import urllib -def post_sendgrid_api(module, username, password, from_address, to_addresses, - subject, body): - SENDGRID_URI = "https://api.sendgrid.com/api/mail.send.json" - AGENT = "Ansible" - data = {'api_user': username, 'api_key':password, - 'from':from_address, 'subject': subject, 'text': body} - encoded_data = urllib.urlencode(data) - to_addresses_api = '' - for recipient in to_addresses: - if isinstance(recipient, unicode): - recipient = recipient.encode('utf-8') - to_addresses_api += '&to[]=%s' % recipient - encoded_data += to_addresses_api - - headers = { 'User-Agent': AGENT, - 'Content-type': 'application/x-www-form-urlencoded', - 'Accept': 'application/json'} - return fetch_url(module, SENDGRID_URI, data=encoded_data, headers=headers, method='POST') - +try: + import sendgrid + HAS_SENDGRID = True +except ImportError: + HAS_SENDGRID = False +def post_sendgrid_api(module, username, password, from_address, to_addresses, + subject, body, api_key=None, cc=None, bcc=None, attachments=None, + html_body=False, from_name=None, headers=None): + + if not HAS_SENDGRID: + SENDGRID_URI = "https://api.sendgrid.com/api/mail.send.json" + AGENT = "Ansible" + data = {'api_user': username, 'api_key':password, + 'from':from_address, 'subject': subject, 'text': body} + encoded_data = urllib.urlencode(data) + to_addresses_api = '' + for recipient in to_addresses: + if isinstance(recipient, unicode): + recipient = recipient.encode('utf-8') + to_addresses_api += '&to[]=%s' % recipient + encoded_data += to_addresses_api + + headers = { 'User-Agent': AGENT, + 'Content-type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json'} + return fetch_url(module, SENDGRID_URI, data=encoded_data, headers=headers, method='POST') + else: + + if api_key: + sg = sendgrid.SendGridClient(api_key) + else: + sg = sendgrid.SendGridClient(username, password) + + message = sendgrid.Mail() + message.set_subject(subject) + + for recip in to_addresses: + message.add_to(recip) + + if cc: + for recip in cc: + message.add_cc(recip) + if bcc: + for recip in bcc: + message.add_bcc(recip) + + if headers: + message.set_headers(headers) + + if attachments: + for f in attachments: + name = os.path.basename(f) + message.add_attachment(name, f) + + if from_name: + message.set_from('%s <%s.' % (from_name, from_address)) + else: + message.set_from(from_address) + + if html_body: + message.set_html(body) + else: + message.set_text(body) + + return sg.send(message) # ======================================= # Main # @@ -114,28 +193,57 @@ def post_sendgrid_api(module, username, password, from_address, to_addresses, def main(): module = AnsibleModule( argument_spec=dict( - username=dict(required=True), - password=dict(required=True, no_log=True), + username=dict(required=False), + password=dict(required=False, no_log=True), + api_key=dict(required=False, no_log=True), + bcc=dict(required=False, type='list'), + cc=dict(required=False, type='list'), + headers=dict(required=False, type='dict'), from_address=dict(required=True), + from_name=dict(required=False), to_addresses=dict(required=True, type='list'), subject=dict(required=True), body=dict(required=True), + html_body=dict(required=False, default=False, type='bool'), + attachments=dict(required=False, type='list') ), - supports_check_mode=True + supports_check_mode=True, + mutually_exclusive = [ + ['api_key', 'password'], + ['api_key', 'username'] + ], + required_together = [['username', 'password']], ) username = module.params['username'] password = module.params['password'] + api_key = module.params['api_key'] + bcc = module.params['bcc'] + cc = module.params['cc'] + headers = module.params['headers'] + from_name = module.params['from_name'] from_address = module.params['from_address'] to_addresses = module.params['to_addresses'] subject = module.params['subject'] body = module.params['body'] + html_body = module.params['html_body'] + attachments = module.params['attachments'] - response, info = post_sendgrid_api(module, username, password, - from_address, to_addresses, subject, body) - if info['status'] != 200: - module.fail_json(msg="unable to send email through SendGrid API: %s" % info['msg']) + sendgrid_lib_args = [api_key, bcc, cc, headers, from_name, html_body, attachments] + + if any(lib_arg != None for lib_arg in sendgrid_lib_args) and not HAS_SENDGRID: + module.fail_json(msg='You must install the sendgrid python library if you want to use any of the following arguments: api_key, bcc, cc, headers, from_name, html_body, attachments') + response, info = post_sendgrid_api(module, username, password, + from_address, to_addresses, subject, body, attachments=attachments, + bcc=bcc, cc=cc, headers=headers, html_body=html_body, api_key=api_key) + + if not HAS_SENDGRID: + if info['status'] != 200: + module.fail_json(msg="unable to send email through SendGrid API: %s" % info['msg']) + else: + if response != 200: + module.fail_json(msg="unable to send email through SendGrid API: %s" % info['message']) module.exit_json(msg=subject, changed=False) From 1a74d68a37d37bc23a9d8b84c4213417905968ec Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Fri, 29 Jul 2016 22:32:13 -0400 Subject: [PATCH 1885/2522] updated docs, version is 2.2 and explain changes --- notification/sendgrid.py | 42 ++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/notification/sendgrid.py b/notification/sendgrid.py index 7e4593a9a90..34f7ebca5bf 100644 --- a/notification/sendgrid.py +++ b/notification/sendgrid.py @@ -39,12 +39,16 @@ options: username: description: - - username for logging into the SendGrid account - required: true + - username for logging into the SendGrid account. + - Since 2.2 it is only required if api_key is not supplied. + required: false + default: null password: description: - password that corresponds to the username - required: true + - Since 2.2 it is only required if api_key is not supplied. + required: false + default: null from_address: description: - the address in the "from" field for the email @@ -60,34 +64,48 @@ api_key: description: - sendgrid API key to use instead of username/password - version_added: 2.1 + version_added: 2.2 + required: false + default: null cc: description: - a list of email addresses to cc - version_added: 2.1 + version_added: 2.2 + required: false + default: null bcc: description: - a list of email addresses to bcc - version_added: 2.1 + version_added: 2.2 + required: false + default: null attachments: description: - a list of relative or explicit paths of files you want to attach (7MB limit as per SendGrid docs) - version_added: 2.1 + version_added: 2.2 + required: false + default: null from_name: description: - the name you want to appear in the from field, i.e 'John Doe' - version_added: 2.1 + version_added: 2.2 + required: false + default: null html_body: description: - whether the body is html content that should be rendered - version_added: 2.1 - choices: [True, False] + version_added: 2.2 + required: false + default: false headers: description: - a dict to pass on as headers - version_added: 2.1 - + version_added: 2.2 + required: false + default: null author: "Matt Makai (@makaimc)" +notes: + - since 2.2 username and password are not required if you supply an api_key. ''' EXAMPLES = ''' From 6b5ad394dad1cf4a5c23eed3eb14ef4fbed595e3 Mon Sep 17 00:00:00 2001 From: James Tanner Date: Mon, 1 Aug 2016 18:55:22 -0400 Subject: [PATCH 1886/2522] Add template deployer --- cloud/vmware/vmware_template_deploy.py | 846 +++++++++++++++++++++++++ 1 file changed, 846 insertions(+) create mode 100644 cloud/vmware/vmware_template_deploy.py diff --git a/cloud/vmware/vmware_template_deploy.py b/cloud/vmware/vmware_template_deploy.py new file mode 100644 index 00000000000..82ef01b7bab --- /dev/null +++ b/cloud/vmware/vmware_template_deploy.py @@ -0,0 +1,846 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: vmware_template_deploy +short_description: Deploy a template to a new virtualmachine in vcenter +description: + - Uses the pyvmomi Clone() method to copy a template to a new virtualmachine in vcenter +version_added: 2.2 +author: James Tanner (@jctanner) +notes: + - Tested on vSphere 6.0 +requirements: + - "python >= 2.6" + - PyVmomi +options: + guest: + description: + - Name of the newly deployed guest + required: True + template: + description: + - Name of the template to deploy + required: True + vm_folder: + description: + - Destination folder path for the new guest + required: False + vm_hardware: + description: + - FIXME + required: False + vm_nic: + description: + - A list of nics to add + required: True + power_on_after_clone: + description: + - Poweron the VM after it is cloned + required: False + wait_for_ip_address: + description: + - Wait until vcenter detects an IP address for the guest + required: False + force: + description: + - Ignore warnings and complete the actions + required: False + datacenter_name: + description: + - Destination datacenter for the deploy operation + required: True + esxi_hostname: + description: + - The esxi hostname where the VM will run. + required: True +extends_documentation_fragment: vmware.documentation +''' + +EXAMPLES = ''' +Example from Ansible playbook + - name: create the VM + vmware_template_deploy: + validate_certs: False + hostname: 192.168.1.209 + username: administrator@vsphere.local + password: vmware + guest: testvm_2 + vm_folder: testvms + vm_disk: + - size_gb: 10 + type: thin + datastore: g73_datastore + vm_nic: + - type: vmxnet3 + network: VM Network + network_type: standard + vm_hardware: + memory_mb: 512 + num_cpus: 1 + osid: centos64guest + scsi: paravirtual + datacenter_name: datacenter1 + esxi_hostname: 192.168.1.117 + template_src: template_el7 + power_on_after_clone: yes + wait_for_ip_address: yes + register: deploy +''' + +try: + import json +except ImportError: + import simplejson as json + +HAS_PYVMOMI = False +try: + import pyVmomi + from pyVmomi import vim + from pyVim.connect import SmartConnect, Disconnect + HAS_PYVMOMI = True +except ImportError: + pass + +import atexit +import os +import ssl +import time +from pprint import pprint +from ansible.module_utils.urls import fetch_url + +class PyVmomiHelper(object): + + def __init__(self, module): + + if not HAS_PYVMOMI: + module.fail_json(msg='pyvmomi module required') + + self.module = module + self.params = module.params + self.si = None + self.smartconnect() + self.datacenter = None + + def smartconnect(self): + kwargs = {'host': self.params['hostname'], + 'user': self.params['username'], + 'pwd': self.params['password']} + + if hasattr(ssl, 'SSLContext'): + context = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + context.verify_mode = ssl.CERT_NONE + kwargs['sslContext'] = context + + # CONNECT TO THE SERVER + try: + self.si = SmartConnect(**kwargs) + except Exception: + err = get_exception() + self.module.fail_json(msg="Cannot connect to %s: %s" % + (kwargs['host'], err)) + atexit.register(Disconnect, self.si) + self.content = self.si.RetrieveContent() + + def _build_folder_tree(self, folder, tree={}, treepath=None): + + tree = {'virtualmachines': [], + 'subfolders': {}, + 'name': folder.name} + + children = None + if hasattr(folder, 'childEntity'): + children = folder.childEntity + + if children: + for child in children: + if child == folder or child in tree: + continue + if type(child) == vim.Folder: + #ctree = self._build_folder_tree(child, tree={}) + ctree = self._build_folder_tree(child) + tree['subfolders'][child] = dict.copy(ctree) + elif type(child) == vim.VirtualMachine: + tree['virtualmachines'].append(child) + else: + if type(folder) == vim.VirtualMachine: + return folder + return tree + + + def _build_folder_map(self, folder, vmap={}, inpath='/'): + + ''' Build a searchable index for vms+uuids+folders ''' + + if type(folder) == tuple: + folder = folder[1] + + if not 'names' in vmap: + vmap['names'] = {} + if not 'uuids' in vmap: + vmap['uuids'] = {} + if not 'paths' in vmap: + vmap['paths'] = {} + + if inpath == '/': + thispath = '/vm' + else: + thispath = os.path.join(inpath, folder['name']) + + for item in folder.items(): + k = item[0] + v = item[1] + if k == 'name': + pass + elif k == 'subfolders': + for x in v.items(): + vmap = self._build_folder_map(x, vmap=vmap, inpath=thispath) + elif k == 'virtualmachines': + for x in v: + if not x.config.name in vmap['names']: + vmap['names'][x.config.name] = [] + vmap['names'][x.config.name].append(x.config.uuid) + vmap['uuids'][x.config.uuid] = x.config.name + if not thispath in vmap['paths']: + vmap['paths'][thispath] = [] + vmap['paths'][thispath].append(x.config.uuid) + + return vmap + + def getfolders(self): + + if not self.datacenter: + self.datacenter = get_obj(self.content, [vim.Datacenter], + self.params['esxi']['datacenter']) + self.folders = self._build_folder_tree(self.datacenter.vmFolder) + self.folder_map = self._build_folder_map(self.folders) + #pprint(self.folder_map) + #sys.exit(1) + return (self.folders, self.folder_map) + + + def getvm(self, name=None, uuid=None, folder=None, firstmatch=False): + + # https://www.vmware.com/support/developer/vc-sdk/visdk2xpubs/ReferenceGuide/vim.SearchIndex.html + # self.si.content.searchIndex.FindByInventoryPath('DC1/vm/test_folder') + + vm = None + folder_path = None + + if uuid: + vm = self.si.content.searchIndex.FindByUuid(uuid=uuid, vmSearch=True) + + elif folder: + + matches = [] + folder_paths = [] + + datacenter = None + if 'esxi' in self.params: + if 'datacenter' in self.params['esxi']: + datacenter = self.params['esxi']['datacenter'] + + if datacenter: + folder_paths.append('%s/vm/%s' % (datacenter, folder)) + else: + # get a list of datacenters + datacenters = get_all_objs(self.content, [vim.Datacenter]) + datacenters = [x.name for x in datacenters] + for dc in datacenters: + folder_paths.append('%s/vm/%s' % (dc, folder)) + + for folder_path in folder_paths: + fObj = self.si.content.searchIndex.FindByInventoryPath(folder_path) + for cObj in fObj.childEntity: + if not type(cObj) == vim.VirtualMachine: + continue + if cObj.name == name: + #vm = cObj + #break + matches.append(cObj) + if len(matches) > 1 and not firstmatch: + assert len(matches) <= 1, "more than 1 vm exists by the name %s in folder %s. Please specify a uuid, a datacenter or firstmatch=true" % name + elif len(matches) > 0: + vm = matches[0] + #else: + #import epdb; epdb.st() + + else: + if firstmatch: + vm = get_obj(self.content, [vim.VirtualMachine], name) + else: + matches = [] + vmList = get_all_objs(self.content, [vim.VirtualMachine]) + for thisvm in vmList: + if thisvm.config == None: + import epdb; epdb.st() + if thisvm.config.name == name: + matches.append(thisvm) + # FIXME - fail this properly + #import epdb; epdb.st() + assert len(matches) <= 1, "more than 1 vm exists by the name %s. Please specify a folder, a uuid, or firstmatch=true" % name + if matches: + vm = matches[0] + + return vm + + + def set_powerstate(self, vm, state, force): + """ + Set the power status for a VM determined by the current and + requested states. force is forceful + """ + facts = self.gather_facts(vm) + expected_state = state.replace('_', '').lower() + current_state = facts['hw_power_status'].lower() + result = {} + + # Need Force + if not force and current_state not in ['poweredon', 'poweredoff']: + return "VM is in %s power state. Force is required!" % current_state + + # State is already true + if current_state == expected_state: + result['changed'] = False + result['failed'] = False + + else: + + task = None + + try: + if expected_state == 'poweredoff': + task = vm.PowerOff() + + elif expected_state == 'poweredon': + task = vm.PowerOn() + + elif expected_state == 'restarted': + if current_state in ('poweredon', 'poweringon', 'resetting'): + task = vm.Reset() + else: + result = {'changed': False, 'failed': True, + 'msg': "Cannot restart VM in the current state %s" % current_state} + + except Exception: + result = {'changed': False, 'failed': True, + 'msg': get_exception()} + + if task: + self.wait_for_task(task) + if task.info.state == 'error': + result = {'changed': False, 'failed': True, 'msg': task.info.error.msg} + else: + result = {'changed': True, 'failed': False} + + # need to get new metadata if changed + if result['changed']: + newvm = self.getvm(uuid=vm.config.uuid) + facts = self.gather_facts(newvm) + result['instance'] = facts + return result + + + def gather_facts(self, vm): + + ''' Gather facts from vim.VirtualMachine object. ''' + + facts = { + 'module_hw': True, + 'hw_name': vm.config.name, + 'hw_power_status': vm.summary.runtime.powerState, + 'hw_guest_full_name': vm.summary.guest.guestFullName, + 'hw_guest_id': vm.summary.guest.guestId, + 'hw_product_uuid': vm.config.uuid, + 'hw_processor_count': vm.config.hardware.numCPU, + 'hw_memtotal_mb': vm.config.hardware.memoryMB, + 'hw_interfaces':[], + 'ipv4': None, + 'ipv6': None, + } + + netDict = {} + for device in vm.guest.net: + mac = device.macAddress + ips = list(device.ipAddress) + netDict[mac] = ips + #facts['network'] = {} + #facts['network']['ipaddress_v4'] = None + #facts['network']['ipaddress_v6'] = None + for k,v in netDict.iteritems(): + for ipaddress in v: + if ipaddress: + if '::' in ipaddress: + facts['ipv6'] = ipaddress + else: + facts['ipv4'] = ipaddress + + for idx,entry in enumerate(vm.config.hardware.device): + + if not hasattr(entry, 'macAddress'): + continue + + factname = 'hw_eth' + str(idx) + facts[factname] = { + 'addresstype': entry.addressType, + 'label': entry.deviceInfo.label, + 'macaddress': entry.macAddress, + 'ipaddresses': netDict.get(entry.macAddress, None), + 'macaddress_dash': entry.macAddress.replace(':', '-'), + 'summary': entry.deviceInfo.summary, + } + facts['hw_interfaces'].append('eth'+str(idx)) + + #import epdb; epdb.st() + return facts + + + def remove_vm(self, vm): + # https://www.vmware.com/support/developer/converter-sdk/conv60_apireference/vim.ManagedEntity.html#destroy + task = vm.Destroy() + self.wait_for_task(task) + + if task.info.state == 'error': + return ({'changed': False, 'failed': True, 'msg': task.info.error.msg}) + else: + return ({'changed': True, 'failed': False}) + + + def deploy_template(self, poweron=False, wait_for_ip=False): + + # https://github.com/vmware/pyvmomi-community-samples/blob/master/samples/clone_vm.py + + # FIXME: + # - clusters + # - multiple datacenters + # - resource pools + # - multiple templates by the same name + # - static IPs + + datacenters = get_all_objs(self.content, [vim.Datacenter]) + datacenter = get_obj(self.content, [vim.Datacenter], + self.params['datacenter_name']) + + # folder is a required clone argument + if len(datacenters) > 1: + # FIXME: need to find the folder in the right DC. + raise "multi-dc with folders is not yet implemented" + else: + destfolder = get_obj(self.content, [vim.Folder], self.params['vm_folder']) + + datastore_name = self.params['vm_disk'][0]['datastore'] + datastore = get_obj(self.content, [vim.Datastore], datastore_name) + + + # cluster or hostsystem ... ? + #cluster = get_obj(self.content, [vim.ClusterComputeResource], self.params['esxi']['hostname']) + hostsystem = get_obj(self.content, [vim.HostSystem], self.params['esxi_hostname']) + + resource_pools = get_all_objs(self.content, [vim.ResourcePool]) + + relospec = vim.vm.RelocateSpec() + relospec.datastore = datastore + + # fixme ... use the pool from the cluster if given + relospec.pool = resource_pools[0] + relospec.host = hostsystem + + clonespec = vim.vm.CloneSpec() + clonespec.location = relospec + + print "cloning VM..." + template = get_obj(self.content, [vim.VirtualMachine], self.params['template_src']) + task = template.Clone(folder=destfolder, name=self.params['guest'], spec=clonespec) + self.wait_for_task(task) + + if task.info.state == 'error': + return ({'changed': False, 'failed': True, 'msg': task.info.error.msg}) + else: + + #import epdb; epdb.st() + vm = task.info.result + + #if wait_for_ip and not poweron: + # print "powering on the VM ..." + # self.set_powerstate(vm, 'poweredon') + + if wait_for_ip: + print "powering on the VM ..." + self.set_powerstate(vm, 'poweredon', force=False) + print "waiting for IP ..." + self.wait_for_vm_ip(vm) + + vm_facts = self.gather_facts(vm) + #import epdb; epdb.st() + return ({'changed': True, 'failed': False, 'instance': vm_facts}) + + + def wait_for_task(self, task): + # https://www.vmware.com/support/developer/vc-sdk/visdk25pubs/ReferenceGuide/vim.Task.html + # https://www.vmware.com/support/developer/vc-sdk/visdk25pubs/ReferenceGuide/vim.TaskInfo.html + # https://github.com/virtdevninja/pyvmomi-community-samples/blob/master/samples/tools/tasks.py + while task.info.state not in ['success', 'error']: + print(task.info.state) + time.sleep(1) + + def wait_for_vm_ip(self, vm, poll=100, sleep=5): + ips = None + facts = {} + thispoll = 0 + while not ips and thispoll <= poll: + print "polling for IP" + newvm = self.getvm(uuid=vm.config.uuid) + facts = self.gather_facts(newvm) + print "\t%s %s" % (facts['ipv4'], facts['ipv6']) + if facts['ipv4'] or facts['ipv6']: + ips = True + else: + time.sleep(sleep) + thispoll += 1 + + #import epdb; epdb.st() + return facts + + + def fetch_file_from_guest(self, vm, username, password, src, dest): + + ''' Use VMWare's filemanager api to fetch a file over http ''' + + result = {'failed': False} + + tools_status = vm.guest.toolsStatus + if (tools_status == 'toolsNotInstalled' or + tools_status == 'toolsNotRunning'): + result['failed'] = True + result['msg'] = "VMwareTools is not installed or is not running in the guest" + return result + + # https://github.com/vmware/pyvmomi/blob/master/docs/vim/vm/guest/NamePasswordAuthentication.rst + creds = vim.vm.guest.NamePasswordAuthentication( + username=username, password=password + ) + + # https://github.com/vmware/pyvmomi/blob/master/docs/vim/vm/guest/FileManager/FileTransferInformation.rst + fti = self.content.guestOperationsManager.fileManager. \ + InitiateFileTransferFromGuest(vm, creds, src) + + result['size'] = fti.size + result['url'] = fti.url + + # Use module_utils to fetch the remote url returned from the api + rsp, info = fetch_url(self.module, fti.url, use_proxy=False, + force=True, last_mod_time=None, + timeout=10, headers=None) + + # save all of the transfer data + for k,v in info.iteritems(): + result[k] = v + + # exit early if xfer failed + if info['status'] != 200: + result['failed'] = True + return result + + # attempt to read the content and write it + try: + with open(dest, 'wb') as f: + f.write(rsp.read()) + except Exception as e: + result['failed'] = True + result['msg'] = str(e) + + return result + + + def push_file_to_guest(self, vm, username, password, src, dest, overwrite=True): + + ''' Use VMWare's filemanager api to push a file over http ''' + + result = {'failed': False} + + tools_status = vm.guest.toolsStatus + if (tools_status == 'toolsNotInstalled' or + tools_status == 'toolsNotRunning'): + result['failed'] = True + result['msg'] = "VMwareTools is not installed or is not running in the guest" + return result + + # https://github.com/vmware/pyvmomi/blob/master/docs/vim/vm/guest/NamePasswordAuthentication.rst + creds = vim.vm.guest.NamePasswordAuthentication( + username=username, password=password + ) + + # the api requires a filesize in bytes + filesize = None + fdata = None + try: + #filesize = os.path.getsize(src) + filesize = os.stat(src).st_size + fdata = None + with open(src, 'rb') as f: + fdata = f.read() + result['local_filesize'] = filesize + except Exception as e: + result['failed'] = True + result['msg'] = "Unable to read src file: %s" % str(e) + return result + + # https://www.vmware.com/support/developer/converter-sdk/conv60_apireference/vim.vm.guest.FileManager.html#initiateFileTransferToGuest + file_attribute = vim.vm.guest.FileManager.FileAttributes() + url = self.content.guestOperationsManager.fileManager. \ + InitiateFileTransferToGuest(vm, creds, dest, file_attribute, + filesize, overwrite) + + # PUT the filedata to the url ... + rsp, info = fetch_url(self.module, url, method="put", data=fdata, + use_proxy=False, force=True, last_mod_time=None, + timeout=10, headers=None) + + result['msg'] = str(rsp.read()) + + # save all of the transfer data + for k,v in info.iteritems(): + result[k] = v + + return result + + + def run_command_in_guest(self, vm, username, password, program_path, program_args, program_cwd, program_env): + + result = {'failed': False} + + tools_status = vm.guest.toolsStatus + if (tools_status == 'toolsNotInstalled' or + tools_status == 'toolsNotRunning'): + result['failed'] = True + result['msg'] = "VMwareTools is not installed or is not running in the guest" + return result + + # https://github.com/vmware/pyvmomi/blob/master/docs/vim/vm/guest/NamePasswordAuthentication.rst + creds = vim.vm.guest.NamePasswordAuthentication( + username=username, password=password + ) + + res = None + pdata = None + try: + # https://github.com/vmware/pyvmomi/blob/master/docs/vim/vm/guest/ProcessManager.rst + pm = self.content.guestOperationsManager.processManager + # https://www.vmware.com/support/developer/converter-sdk/conv51_apireference/vim.vm.guest.ProcessManager.ProgramSpec.html + ps = vim.vm.guest.ProcessManager.ProgramSpec( + #programPath=program, + #arguments=args + programPath=program_path, + arguments=program_args, + workingDirectory=program_cwd, + ) + res = pm.StartProgramInGuest(vm, creds, ps) + result['pid'] = res + pdata = pm.ListProcessesInGuest(vm, creds, [res]) + + # wait for pid to finish + while not pdata[0].endTime: + time.sleep(1) + pdata = pm.ListProcessesInGuest(vm, creds, [res]) + result['owner'] = pdata[0].owner + result['startTime'] = pdata[0].startTime.isoformat() + result['endTime'] = pdata[0].endTime.isoformat() + result['exitCode'] = pdata[0].exitCode + if result['exitCode'] != 0: + result['failed'] = True + result['msg'] = "program exited non-zero" + else: + result['msg'] = "program completed successfully" + + except Exception as e: + result['msg'] = str(e) + result['failed'] = True + + return result + + +def get_obj(content, vimtype, name): + """ + Return an object by name, if name is None the + first found object is returned + """ + obj = None + container = content.viewManager.CreateContainerView( + content.rootFolder, vimtype, True) + for c in container.view: + if name: + if c.name == name: + obj = c + break + else: + obj = c + break + + container.Destroy() + return obj + + +def get_all_objs(content, vimtype): + """ + Get all the vsphere objects associated with a given type + """ + obj = [] + container = content.viewManager.CreateContainerView(content.rootFolder, vimtype, True) + for c in container.view: + obj.append(c) + container.Destroy() + return obj + + +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 main(): + + vm = None + + module = AnsibleModule( + argument_spec=dict( + hostname=dict( + type='str', + default=os.environ.get('VMWARE_HOST') + ), + username=dict( + type='str', + default=os.environ.get('VMWARE_USER') + ), + password=dict( + type='str', no_log=True, + default=os.environ.get('VMWARE_PASSWORD') + ), + state=dict( + required=False, + choices=[ + 'powered_on', + 'powered_off', + 'present', + 'absent', + 'restarted', + 'reconfigured' + ], + default='present'), + template_src=dict(required=False, type='str'), + guest=dict(required=True, type='str'), + vm_folder=dict(required=False, type='str', default=None), + vm_disk=dict(required=False, type='list', default=[]), + vm_nic=dict(required=False, type='list', default=[]), + vm_hardware=dict(required=False, type='dict', default={}), + vm_hw_version=dict(required=False, default=None, type='str'), + force=dict(required=False, type='bool', default=False), + firstmatch=dict(required=False, type='bool', default=False), + datacenter_name=dict(required=False, type='str', default=None), + esxi_hostname=dict(required=False, type='str', default=None), + validate_certs=dict(required=False, type='bool', default=True), + power_on_after_clone=dict(required=False, type='bool', default=True), + wait_for_ip_address=dict(required=False, type='bool', default=True) + ), + supports_check_mode=True, + mutually_exclusive=[], + required_together=[ + ['state', 'force'], + [ + 'vm_disk', + 'vm_nic', + 'vm_hardware', + 'esxi_hostname' + ], + ['template_src'], + ], + ) + + pyv = PyVmomiHelper(module) + + # Check if the VM exists before continuing + vm = pyv.getvm(name=module.params['guest'], + folder=module.params['vm_folder'], + firstmatch=module.params['firstmatch']) + + # VM already exists + if vm: + # Run for facts only + if module.params['vmware_guest_facts']: + try: + module.exit_json(ansible_facts=pyv.gather_facts(vm)) + except Exception: + e = get_exception() + module.fail_json( + msg="Fact gather failed with exception %s" % e) + + # VM doesn't exist + else: + + # Create it ... + result = pyv.deploy_template(poweron=module.params['power_on_after_clone'], + wait_for_ip=module.params['wait_for_ip_address']) + + + if result['failed']: + module.fail_json(**result) + else: + module.exit_json(**result) + + +# this is magic, see lib/ansible/module_common.py +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From 4943b4e82e6d38131a9cc8bbef2a67370dd8c1d2 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Mon, 1 Aug 2016 16:13:07 -0700 Subject: [PATCH 1887/2522] Enable FreeBSD tests on Shippable. (#2648) --- shippable.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shippable.yml b/shippable.yml index f05d2481478..90087b94d49 100644 --- a/shippable.yml +++ b/shippable.yml @@ -21,6 +21,8 @@ matrix: - env: TEST=integration PLATFORM=windows VERSION=2008-R2_SP1 - env: TEST=integration PLATFORM=windows VERSION=2012-RTM - env: TEST=integration PLATFORM=windows VERSION=2012-R2_RTM + + - env: TEST=integration PLATFORM=freebsd VERSION=10.3-STABLE build: pre_ci_boot: options: "--privileged=false --net=bridge" From c51b1549a284a41d32b15f751c9381cb7ada2361 Mon Sep 17 00:00:00 2001 From: James Tanner Date: Mon, 1 Aug 2016 21:34:33 -0400 Subject: [PATCH 1888/2522] Add return data example --- cloud/vmware/vmware_template_deploy.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/cloud/vmware/vmware_template_deploy.py b/cloud/vmware/vmware_template_deploy.py index 82ef01b7bab..1e675cbe264 100644 --- a/cloud/vmware/vmware_template_deploy.py +++ b/cloud/vmware/vmware_template_deploy.py @@ -43,7 +43,7 @@ required: False vm_hardware: description: - - FIXME + - Attributes such as cpus, memroy, osid, and disk controller required: False vm_nic: description: @@ -103,6 +103,14 @@ register: deploy ''' +RETURN = """ +instance: + descripton: metadata about the new virtualmachine + returned: always + type: dict + sample: None +""" + try: import json except ImportError: From 3caee773cbec24e30eac1b905ce9a4d696711840 Mon Sep 17 00:00:00 2001 From: James Tanner Date: Mon, 1 Aug 2016 21:40:12 -0400 Subject: [PATCH 1889/2522] Rename module --- .../{vmware_template_deploy.py => vmware_deploy_template.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename cloud/vmware/{vmware_template_deploy.py => vmware_deploy_template.py} (99%) diff --git a/cloud/vmware/vmware_template_deploy.py b/cloud/vmware/vmware_deploy_template.py similarity index 99% rename from cloud/vmware/vmware_template_deploy.py rename to cloud/vmware/vmware_deploy_template.py index 1e675cbe264..920e28d87f7 100644 --- a/cloud/vmware/vmware_template_deploy.py +++ b/cloud/vmware/vmware_deploy_template.py @@ -17,7 +17,7 @@ DOCUMENTATION = ''' --- -module: vmware_template_deploy +module: vmware_deploy_template short_description: Deploy a template to a new virtualmachine in vcenter description: - Uses the pyvmomi Clone() method to copy a template to a new virtualmachine in vcenter @@ -75,7 +75,7 @@ EXAMPLES = ''' Example from Ansible playbook - name: create the VM - vmware_template_deploy: + vmware_deploy_template: validate_certs: False hostname: 192.168.1.209 username: administrator@vsphere.local From 07fb05a852cf48333a8b750623381bbeeee27f99 Mon Sep 17 00:00:00 2001 From: James Tanner Date: Mon, 1 Aug 2016 22:20:05 -0400 Subject: [PATCH 1890/2522] Add the guest state module --- cloud/vmware/vmware_guest_state.py | 823 +++++++++++++++++++++++++++++ 1 file changed, 823 insertions(+) create mode 100644 cloud/vmware/vmware_guest_state.py diff --git a/cloud/vmware/vmware_guest_state.py b/cloud/vmware/vmware_guest_state.py new file mode 100644 index 00000000000..298d1098d84 --- /dev/null +++ b/cloud/vmware/vmware_guest_state.py @@ -0,0 +1,823 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +module: vmware_guest_state +short_description: manage the state of a vmware virtualmachine in vcenter +description: + - Uses pyvmomi to poweron/poweroff/delete/restart a virtualmachine +version_added: 2.2 +author: James Tanner (@jctanner) +notes: + - Tested on vSphere 6.0 +requirements: + - "python >= 2.6" + - PyVmomi +options: + guest: + description: + - Name of the newly deployed guest + required: True + state: + description: + - What state should the machine be in? + - restarted/absent/poweredon/poweredoff + required: True + vm_uuid: + description: + - UUID of the instance to manage if known + required: False + vm_folder: + description: + - Folder path for the guest if known + required: False + firstmatch: + description: + - If multiple vms match, use the first found + required: False + force: + description: + - Ignore warnings and complete the actions + required: False + datacenter_name: + description: + - Destination datacenter for the deploy operation + required: True +extends_documentation_fragment: vmware.documentation +''' + +EXAMPLES = ''' +''' + +try: + import json +except ImportError: + import simplejson as json + +HAS_PYVMOMI = False +try: + import pyVmomi + from pyVmomi import vim + from pyVim.connect import SmartConnect, Disconnect + HAS_PYVMOMI = True +except ImportError: + pass + +import atexit +import os +import ssl +import time +from pprint import pprint + +from ansible.module_utils.urls import fetch_url + + +class PyVmomiHelper(object): + + def __init__(self, module): + + if not HAS_PYVMOMI: + module.fail_json(msg='pyvmomi module required') + + self.module = module + self.params = module.params + self.si = None + self.smartconnect() + self.datacenter = None + + def smartconnect(self): + kwargs = {'host': self.params['hostname'], + 'user': self.params['username'], + 'pwd': self.params['password']} + + if hasattr(ssl, 'SSLContext'): + context = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + context.verify_mode = ssl.CERT_NONE + kwargs['sslContext'] = context + + # CONNECT TO THE SERVER + try: + self.si = SmartConnect(**kwargs) + except Exception: + err = get_exception() + self.module.fail_json(msg="Cannot connect to %s: %s" % + (kwargs['host'], err)) + atexit.register(Disconnect, self.si) + self.content = self.si.RetrieveContent() + + def _build_folder_tree(self, folder, tree={}, treepath=None): + + tree = {'virtualmachines': [], + 'subfolders': {}, + 'name': folder.name} + + children = None + if hasattr(folder, 'childEntity'): + children = folder.childEntity + + if children: + for child in children: + if child == folder or child in tree: + continue + if type(child) == vim.Folder: + #ctree = self._build_folder_tree(child, tree={}) + ctree = self._build_folder_tree(child) + tree['subfolders'][child] = dict.copy(ctree) + elif type(child) == vim.VirtualMachine: + tree['virtualmachines'].append(child) + else: + if type(folder) == vim.VirtualMachine: + return folder + return tree + + + def _build_folder_map(self, folder, vmap={}, inpath='/'): + + ''' Build a searchable index for vms+uuids+folders ''' + + if type(folder) == tuple: + folder = folder[1] + + if not 'names' in vmap: + vmap['names'] = {} + if not 'uuids' in vmap: + vmap['uuids'] = {} + if not 'paths' in vmap: + vmap['paths'] = {} + + if inpath == '/': + thispath = '/vm' + else: + thispath = os.path.join(inpath, folder['name']) + + for item in folder.items(): + k = item[0] + v = item[1] + if k == 'name': + pass + elif k == 'subfolders': + for x in v.items(): + vmap = self._build_folder_map(x, vmap=vmap, inpath=thispath) + elif k == 'virtualmachines': + for x in v: + if not x.config.name in vmap['names']: + vmap['names'][x.config.name] = [] + vmap['names'][x.config.name].append(x.config.uuid) + vmap['uuids'][x.config.uuid] = x.config.name + if not thispath in vmap['paths']: + vmap['paths'][thispath] = [] + vmap['paths'][thispath].append(x.config.uuid) + + return vmap + + def getfolders(self): + + if not self.datacenter: + self.datacenter = get_obj(self.content, [vim.Datacenter], + self.params['esxi']['datacenter']) + self.folders = self._build_folder_tree(self.datacenter.vmFolder) + self.folder_map = self._build_folder_map(self.folders) + #pprint(self.folder_map) + #sys.exit(1) + return (self.folders, self.folder_map) + + + def getvm(self, name=None, uuid=None, folder=None, firstmatch=False): + + # https://www.vmware.com/support/developer/vc-sdk/visdk2xpubs/ReferenceGuide/vim.SearchIndex.html + # self.si.content.searchIndex.FindByInventoryPath('DC1/vm/test_folder') + + vm = None + folder_path = None + + if uuid: + vm = self.si.content.searchIndex.FindByUuid(uuid=uuid, vmSearch=True) + + elif folder: + + matches = [] + folder_paths = [] + + datacenter = None + if 'esxi' in self.params: + if 'datacenter' in self.params['esxi']: + datacenter = self.params['esxi']['datacenter'] + + if datacenter: + folder_paths.append('%s/vm/%s' % (datacenter, folder)) + else: + # get a list of datacenters + datacenters = get_all_objs(self.content, [vim.Datacenter]) + datacenters = [x.name for x in datacenters] + for dc in datacenters: + folder_paths.append('%s/vm/%s' % (dc, folder)) + + for folder_path in folder_paths: + fObj = self.si.content.searchIndex.FindByInventoryPath(folder_path) + for cObj in fObj.childEntity: + if not type(cObj) == vim.VirtualMachine: + continue + if cObj.name == name: + #vm = cObj + #break + matches.append(cObj) + if len(matches) > 1 and not firstmatch: + assert len(matches) <= 1, "more than 1 vm exists by the name %s in folder %s. Please specify a uuid, a datacenter or firstmatch=true" % name + elif len(matches) > 0: + vm = matches[0] + #else: + #import epdb; epdb.st() + + else: + if firstmatch: + vm = get_obj(self.content, [vim.VirtualMachine], name) + else: + matches = [] + vmList = get_all_objs(self.content, [vim.VirtualMachine]) + for thisvm in vmList: + if thisvm.config == None: + import epdb; epdb.st() + if thisvm.config.name == name: + matches.append(thisvm) + # FIXME - fail this properly + #import epdb; epdb.st() + assert len(matches) <= 1, "more than 1 vm exists by the name %s. Please specify a folder, a uuid, or firstmatch=true" % name + if matches: + vm = matches[0] + + return vm + + + def set_powerstate(self, vm, state, force): + """ + Set the power status for a VM determined by the current and + requested states. force is forceful + """ + facts = self.gather_facts(vm) + expected_state = state.replace('_', '').lower() + current_state = facts['hw_power_status'].lower() + result = {} + + # Need Force + if not force and current_state not in ['poweredon', 'poweredoff']: + return "VM is in %s power state. Force is required!" % current_state + + # State is already true + if current_state == expected_state: + result['changed'] = False + result['failed'] = False + + else: + + task = None + + try: + if expected_state == 'poweredoff': + task = vm.PowerOff() + + elif expected_state == 'poweredon': + task = vm.PowerOn() + + elif expected_state == 'restarted': + if current_state in ('poweredon', 'poweringon', 'resetting'): + task = vm.Reset() + else: + result = {'changed': False, 'failed': True, + 'msg': "Cannot restart VM in the current state %s" % current_state} + + except Exception: + result = {'changed': False, 'failed': True, + 'msg': get_exception()} + + if task: + self.wait_for_task(task) + if task.info.state == 'error': + result = {'changed': False, 'failed': True, 'msg': task.info.error.msg} + else: + result = {'changed': True, 'failed': False} + + # need to get new metadata if changed + if result['changed']: + newvm = self.getvm(uuid=vm.config.uuid) + facts = self.gather_facts(newvm) + result['instance'] = facts + return result + + + def gather_facts(self, vm): + + ''' Gather facts from vim.VirtualMachine object. ''' + + facts = { + 'module_hw': True, + 'hw_name': vm.config.name, + 'hw_power_status': vm.summary.runtime.powerState, + 'hw_guest_full_name': vm.summary.guest.guestFullName, + 'hw_guest_id': vm.summary.guest.guestId, + 'hw_product_uuid': vm.config.uuid, + 'hw_processor_count': vm.config.hardware.numCPU, + 'hw_memtotal_mb': vm.config.hardware.memoryMB, + 'hw_interfaces':[], + 'ipv4': None, + 'ipv6': None, + } + + netDict = {} + for device in vm.guest.net: + mac = device.macAddress + ips = list(device.ipAddress) + netDict[mac] = ips + #facts['network'] = {} + #facts['network']['ipaddress_v4'] = None + #facts['network']['ipaddress_v6'] = None + for k,v in netDict.iteritems(): + for ipaddress in v: + if ipaddress: + if '::' in ipaddress: + facts['ipv6'] = ipaddress + else: + facts['ipv4'] = ipaddress + + for idx,entry in enumerate(vm.config.hardware.device): + + if not hasattr(entry, 'macAddress'): + continue + + factname = 'hw_eth' + str(idx) + facts[factname] = { + 'addresstype': entry.addressType, + 'label': entry.deviceInfo.label, + 'macaddress': entry.macAddress, + 'ipaddresses': netDict.get(entry.macAddress, None), + 'macaddress_dash': entry.macAddress.replace(':', '-'), + 'summary': entry.deviceInfo.summary, + } + facts['hw_interfaces'].append('eth'+str(idx)) + + #import epdb; epdb.st() + return facts + + + def remove_vm(self, vm): + # https://www.vmware.com/support/developer/converter-sdk/conv60_apireference/vim.ManagedEntity.html#destroy + task = vm.Destroy() + self.wait_for_task(task) + + if task.info.state == 'error': + return ({'changed': False, 'failed': True, 'msg': task.info.error.msg}) + else: + return ({'changed': True, 'failed': False}) + + + def deploy_template(self, poweron=False, wait_for_ip=False): + + # https://github.com/vmware/pyvmomi-community-samples/blob/master/samples/clone_vm.py + + ''' + deploy_template( + vsphere_client=viserver, + esxi=esxi, + resource_pool=resource_pool, + guest=guest, + template_src=template_src, + module=module, + cluster_name=cluster, + snapshot_to_clone=snapshot_to_clone, + power_on_after_clone=power_on_after_clone, + vm_extra_config=vm_extra_config + ) + ''' + + # FIXME: + # - clusters + # - resource pools + # - multiple templates by the same name + # - static IPs + + datacenters = get_all_objs(self.content, [vim.Datacenter]) + datacenter = get_obj(self.content, [vim.Datacenter], + self.params['esxi']['datacenter']) + + # folder is a required clone argument + if len(datacenters) > 1: + # FIXME: need to find the folder in the right DC. + raise "multi-dc with folders is not yet implemented" + else: + destfolder = get_obj(self.content, [vim.Folder], self.params['vm_folder']) + + datastore_name = self.params['vm_disk']['disk1']['datastore'] + datastore = get_obj(self.content, [vim.Datastore], datastore_name) + + + # cluster or hostsystem ... ? + #cluster = get_obj(self.content, [vim.ClusterComputeResource], self.params['esxi']['hostname']) + hostsystem = get_obj(self.content, [vim.HostSystem], self.params['esxi']['hostname']) + #import epdb; epdb.st() + + resource_pools = get_all_objs(self.content, [vim.ResourcePool]) + #import epdb; epdb.st() + + relospec = vim.vm.RelocateSpec() + relospec.datastore = datastore + + # fixme ... use the pool from the cluster if given + relospec.pool = resource_pools[0] + relospec.host = hostsystem + #import epdb; epdb.st() + + clonespec = vim.vm.CloneSpec() + clonespec.location = relospec + #clonespec.powerOn = power_on + + print "cloning VM..." + template = get_obj(self.content, [vim.VirtualMachine], self.params['template_src']) + task = template.Clone(folder=destfolder, name=self.params['guest'], spec=clonespec) + self.wait_for_task(task) + + if task.info.state == 'error': + return ({'changed': False, 'failed': True, 'msg': task.info.error.msg}) + else: + + #import epdb; epdb.st() + vm = task.info.result + + #if wait_for_ip and not poweron: + # print "powering on the VM ..." + # self.set_powerstate(vm, 'poweredon') + + if wait_for_ip: + print "powering on the VM ..." + self.set_powerstate(vm, 'poweredon', force=False) + print "waiting for IP ..." + self.wait_for_vm_ip(vm) + + vm_facts = self.gather_facts(vm) + #import epdb; epdb.st() + return ({'changed': True, 'failed': False, 'instance': vm_facts}) + + + def wait_for_task(self, task): + # https://www.vmware.com/support/developer/vc-sdk/visdk25pubs/ReferenceGuide/vim.Task.html + # https://www.vmware.com/support/developer/vc-sdk/visdk25pubs/ReferenceGuide/vim.TaskInfo.html + # https://github.com/virtdevninja/pyvmomi-community-samples/blob/master/samples/tools/tasks.py + while task.info.state not in ['success', 'error']: + print(task.info.state) + time.sleep(1) + + def wait_for_vm_ip(self, vm, poll=100, sleep=5): + ips = None + facts = {} + thispoll = 0 + while not ips and thispoll <= poll: + print "polling for IP" + newvm = self.getvm(uuid=vm.config.uuid) + facts = self.gather_facts(newvm) + print "\t%s %s" % (facts['ipv4'], facts['ipv6']) + if facts['ipv4'] or facts['ipv6']: + ips = True + else: + time.sleep(sleep) + thispoll += 1 + + #import epdb; epdb.st() + return facts + + + def fetch_file_from_guest(self, vm, username, password, src, dest): + + ''' Use VMWare's filemanager api to fetch a file over http ''' + + result = {'failed': False} + + tools_status = vm.guest.toolsStatus + if (tools_status == 'toolsNotInstalled' or + tools_status == 'toolsNotRunning'): + result['failed'] = True + result['msg'] = "VMwareTools is not installed or is not running in the guest" + return result + + # https://github.com/vmware/pyvmomi/blob/master/docs/vim/vm/guest/NamePasswordAuthentication.rst + creds = vim.vm.guest.NamePasswordAuthentication( + username=username, password=password + ) + + # https://github.com/vmware/pyvmomi/blob/master/docs/vim/vm/guest/FileManager/FileTransferInformation.rst + fti = self.content.guestOperationsManager.fileManager. \ + InitiateFileTransferFromGuest(vm, creds, src) + + result['size'] = fti.size + result['url'] = fti.url + + # Use module_utils to fetch the remote url returned from the api + rsp, info = fetch_url(self.module, fti.url, use_proxy=False, + force=True, last_mod_time=None, + timeout=10, headers=None) + + # save all of the transfer data + for k,v in info.iteritems(): + result[k] = v + + # exit early if xfer failed + if info['status'] != 200: + result['failed'] = True + return result + + # attempt to read the content and write it + try: + with open(dest, 'wb') as f: + f.write(rsp.read()) + except Exception as e: + result['failed'] = True + result['msg'] = str(e) + + return result + + + def push_file_to_guest(self, vm, username, password, src, dest, overwrite=True): + + ''' Use VMWare's filemanager api to push a file over http ''' + + result = {'failed': False} + + tools_status = vm.guest.toolsStatus + if (tools_status == 'toolsNotInstalled' or + tools_status == 'toolsNotRunning'): + result['failed'] = True + result['msg'] = "VMwareTools is not installed or is not running in the guest" + return result + + # https://github.com/vmware/pyvmomi/blob/master/docs/vim/vm/guest/NamePasswordAuthentication.rst + creds = vim.vm.guest.NamePasswordAuthentication( + username=username, password=password + ) + + # the api requires a filesize in bytes + filesize = None + fdata = None + try: + #filesize = os.path.getsize(src) + filesize = os.stat(src).st_size + fdata = None + with open(src, 'rb') as f: + fdata = f.read() + result['local_filesize'] = filesize + except Exception as e: + result['failed'] = True + result['msg'] = "Unable to read src file: %s" % str(e) + return result + + # https://www.vmware.com/support/developer/converter-sdk/conv60_apireference/vim.vm.guest.FileManager.html#initiateFileTransferToGuest + file_attribute = vim.vm.guest.FileManager.FileAttributes() + url = self.content.guestOperationsManager.fileManager. \ + InitiateFileTransferToGuest(vm, creds, dest, file_attribute, + filesize, overwrite) + + # PUT the filedata to the url ... + rsp, info = fetch_url(self.module, url, method="put", data=fdata, + use_proxy=False, force=True, last_mod_time=None, + timeout=10, headers=None) + + result['msg'] = str(rsp.read()) + + # save all of the transfer data + for k,v in info.iteritems(): + result[k] = v + + return result + + + def run_command_in_guest(self, vm, username, password, program_path, program_args, program_cwd, program_env): + + result = {'failed': False} + + tools_status = vm.guest.toolsStatus + if (tools_status == 'toolsNotInstalled' or + tools_status == 'toolsNotRunning'): + result['failed'] = True + result['msg'] = "VMwareTools is not installed or is not running in the guest" + return result + + # https://github.com/vmware/pyvmomi/blob/master/docs/vim/vm/guest/NamePasswordAuthentication.rst + creds = vim.vm.guest.NamePasswordAuthentication( + username=username, password=password + ) + + res = None + pdata = None + try: + # https://github.com/vmware/pyvmomi/blob/master/docs/vim/vm/guest/ProcessManager.rst + pm = self.content.guestOperationsManager.processManager + # https://www.vmware.com/support/developer/converter-sdk/conv51_apireference/vim.vm.guest.ProcessManager.ProgramSpec.html + ps = vim.vm.guest.ProcessManager.ProgramSpec( + #programPath=program, + #arguments=args + programPath=program_path, + arguments=program_args, + workingDirectory=program_cwd, + ) + res = pm.StartProgramInGuest(vm, creds, ps) + result['pid'] = res + pdata = pm.ListProcessesInGuest(vm, creds, [res]) + + # wait for pid to finish + while not pdata[0].endTime: + time.sleep(1) + pdata = pm.ListProcessesInGuest(vm, creds, [res]) + result['owner'] = pdata[0].owner + result['startTime'] = pdata[0].startTime.isoformat() + result['endTime'] = pdata[0].endTime.isoformat() + result['exitCode'] = pdata[0].exitCode + if result['exitCode'] != 0: + result['failed'] = True + result['msg'] = "program exited non-zero" + else: + result['msg'] = "program completed successfully" + + except Exception as e: + result['msg'] = str(e) + result['failed'] = True + + return result + + +def get_obj(content, vimtype, name): + """ + Return an object by name, if name is None the + first found object is returned + """ + obj = None + container = content.viewManager.CreateContainerView( + content.rootFolder, vimtype, True) + for c in container.view: + if name: + if c.name == name: + obj = c + break + else: + obj = c + break + + container.Destroy() + return obj + + +def get_all_objs(content, vimtype): + """ + Get all the vsphere objects associated with a given type + """ + obj = [] + container = content.viewManager.CreateContainerView(content.rootFolder, vimtype, True) + for c in container.view: + obj.append(c) + container.Destroy() + return obj + + +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 main(): + + module = AnsibleModule( + argument_spec=dict( + validate_certs=dict(required=False, type='bool', default=True), + hostname=dict( + type='str', + default=os.environ.get('VMWARE_HOST') + ), + username=dict( + type='str', + default=os.environ.get('VMWARE_USER') + ), + password=dict( + type='str', no_log=True, + default=os.environ.get('VMWARE_PASSWORD') + ), + state=dict( + required=True, + choices=[ + 'powered_on', + 'powered_off', + 'present', + 'absent', + 'restarted', + ], + ), + guest=dict(required=True, type='str'), + vm_folder=dict(required=False, type='str', default=None), + vm_uuid=dict(required=False, type='str', default=None), + firstmatch=dict(required=False, type='bool', default=False), + force=dict(required=False, type='bool', default=False), + datacenter=dict(required=False, type='str', default=None), + ), + supports_check_mode=True, + mutually_exclusive=[], + required_together=[], + ) + + pyv = PyVmomiHelper(module) + + # Check if the VM exists before continuing + vm = pyv.getvm(name=module.params['guest'], + folder=module.params['vm_folder'], + uuid=module.params['vm_uuid'], + firstmatch=module.params['firstmatch']) + + if vm: + # Power Changes + if module.params['state'] in ['powered_on', 'powered_off', 'restarted']: + result = pyv.set_powerstate(vm, module.params['state'], module.params['force']) + + # Failure + if isinstance(result, basestring): + result = {'changed': False, 'failed': True, 'msg': result} + + # Just check if there + elif module.params['state'] == 'present': + result = {'changed': False} + + elif module.params['state'] == 'absent': + result = pyv.remove_vm(vm) + + # VM doesn't exist + else: + + if module.params['state'] == 'present': + result = {'failed': True, 'msg': "vm does not exist"} + + elif module.params['state'] in ['restarted', 'reconfigured']: + result = {'changed': False, 'failed': True, + 'msg': "No such VM %s. States [restarted, reconfigured] required an existing VM" % guest } + + elif module.params['state'] == 'absent': + result = {'changed': False, 'failed': False, + 'msg': "vm %s not present" % module.params['guest']} + + elif module.params['state'] in ['powered_off', 'powered_on']: + result = {'changed': False, 'failed': True, + 'msg': "No such VM %s. States [powered_off, powered_on] required an existing VM" % module.params['guest'] } + + if result['failed']: + module.fail_json(**result) + else: + module.exit_json(**result) + + +# this is magic, see lib/ansible/module_common.py +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() + From 5d2d0e0045e7935b8fd55bdfcd31202c8e59544b Mon Sep 17 00:00:00 2001 From: James Tanner Date: Mon, 1 Aug 2016 22:38:41 -0400 Subject: [PATCH 1891/2522] fix tabs --- cloud/vmware/vmware_deploy_template.py | 56 +++++++++++--------------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/cloud/vmware/vmware_deploy_template.py b/cloud/vmware/vmware_deploy_template.py index 920e28d87f7..4ca05fc6b76 100644 --- a/cloud/vmware/vmware_deploy_template.py +++ b/cloud/vmware/vmware_deploy_template.py @@ -309,28 +309,25 @@ def getvm(self, name=None, uuid=None, folder=None, firstmatch=False): def set_powerstate(self, vm, state, force): - """ - Set the power status for a VM determined by the current and - requested states. force is forceful - """ + """ + Set the power status for a VM determined by the current and + requested states. force is forceful + """ facts = self.gather_facts(vm) expected_state = state.replace('_', '').lower() current_state = facts['hw_power_status'].lower() result = {} - # Need Force - if not force and current_state not in ['poweredon', 'poweredoff']: - return "VM is in %s power state. Force is required!" % current_state + # Need Force + if not force and current_state not in ['poweredon', 'poweredoff']: + return "VM is in %s power state. Force is required!" % current_state - # State is already true - if current_state == expected_state: + # State is already true + if current_state == expected_state: result['changed'] = False result['failed'] = False - - else: - + else: task = None - try: if expected_state == 'poweredoff': task = vm.PowerOff() @@ -387,9 +384,6 @@ def gather_facts(self, vm): mac = device.macAddress ips = list(device.ipAddress) netDict[mac] = ips - #facts['network'] = {} - #facts['network']['ipaddress_v4'] = None - #facts['network']['ipaddress_v6'] = None for k,v in netDict.iteritems(): for ipaddress in v: if ipaddress: @@ -398,23 +392,21 @@ def gather_facts(self, vm): else: facts['ipv4'] = ipaddress - for idx,entry in enumerate(vm.config.hardware.device): + for idx,entry in enumerate(vm.config.hardware.device): + if not hasattr(entry, 'macAddress'): + continue + + factname = 'hw_eth' + str(idx) + facts[factname] = { + 'addresstype': entry.addressType, + 'label': entry.deviceInfo.label, + 'macaddress': entry.macAddress, + 'ipaddresses': netDict.get(entry.macAddress, None), + 'macaddress_dash': entry.macAddress.replace(':', '-'), + 'summary': entry.deviceInfo.summary, + } + facts['hw_interfaces'].append('eth'+str(idx)) - if not hasattr(entry, 'macAddress'): - continue - - factname = 'hw_eth' + str(idx) - facts[factname] = { - 'addresstype': entry.addressType, - 'label': entry.deviceInfo.label, - 'macaddress': entry.macAddress, - 'ipaddresses': netDict.get(entry.macAddress, None), - 'macaddress_dash': entry.macAddress.replace(':', '-'), - 'summary': entry.deviceInfo.summary, - } - facts['hw_interfaces'].append('eth'+str(idx)) - - #import epdb; epdb.st() return facts From a3f415a892b6da42ad4ad8f007307e323e8ae8a5 Mon Sep 17 00:00:00 2001 From: James Tanner Date: Mon, 1 Aug 2016 22:47:26 -0400 Subject: [PATCH 1892/2522] fix tabs --- cloud/vmware/vmware_guest_state.py | 56 +++++++++++++----------------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/cloud/vmware/vmware_guest_state.py b/cloud/vmware/vmware_guest_state.py index 298d1098d84..ed8a3b2c986 100644 --- a/cloud/vmware/vmware_guest_state.py +++ b/cloud/vmware/vmware_guest_state.py @@ -263,28 +263,25 @@ def getvm(self, name=None, uuid=None, folder=None, firstmatch=False): def set_powerstate(self, vm, state, force): - """ - Set the power status for a VM determined by the current and - requested states. force is forceful - """ + """ + Set the power status for a VM determined by the current and + requested states. force is forceful + """ facts = self.gather_facts(vm) expected_state = state.replace('_', '').lower() current_state = facts['hw_power_status'].lower() result = {} - # Need Force - if not force and current_state not in ['poweredon', 'poweredoff']: - return "VM is in %s power state. Force is required!" % current_state + # Need Force + if not force and current_state not in ['poweredon', 'poweredoff']: + return "VM is in %s power state. Force is required!" % current_state - # State is already true - if current_state == expected_state: + # State is already true + if current_state == expected_state: result['changed'] = False result['failed'] = False - - else: - + else: task = None - try: if expected_state == 'poweredoff': task = vm.PowerOff() @@ -341,9 +338,6 @@ def gather_facts(self, vm): mac = device.macAddress ips = list(device.ipAddress) netDict[mac] = ips - #facts['network'] = {} - #facts['network']['ipaddress_v4'] = None - #facts['network']['ipaddress_v6'] = None for k,v in netDict.iteritems(): for ipaddress in v: if ipaddress: @@ -352,23 +346,21 @@ def gather_facts(self, vm): else: facts['ipv4'] = ipaddress - for idx,entry in enumerate(vm.config.hardware.device): + for idx,entry in enumerate(vm.config.hardware.device): + if not hasattr(entry, 'macAddress'): + continue + + factname = 'hw_eth' + str(idx) + facts[factname] = { + 'addresstype': entry.addressType, + 'label': entry.deviceInfo.label, + 'macaddress': entry.macAddress, + 'ipaddresses': netDict.get(entry.macAddress, None), + 'macaddress_dash': entry.macAddress.replace(':', '-'), + 'summary': entry.deviceInfo.summary, + } + facts['hw_interfaces'].append('eth'+str(idx)) - if not hasattr(entry, 'macAddress'): - continue - - factname = 'hw_eth' + str(idx) - facts[factname] = { - 'addresstype': entry.addressType, - 'label': entry.deviceInfo.label, - 'macaddress': entry.macAddress, - 'ipaddresses': netDict.get(entry.macAddress, None), - 'macaddress_dash': entry.macAddress.replace(':', '-'), - 'summary': entry.deviceInfo.summary, - } - facts['hw_interfaces'].append('eth'+str(idx)) - - #import epdb; epdb.st() return facts From cf61825ae5a70616bd283779c9aec353a66324d3 Mon Sep 17 00:00:00 2001 From: James Tanner Date: Mon, 1 Aug 2016 23:04:00 -0400 Subject: [PATCH 1893/2522] Remove print statements --- cloud/vmware/vmware_deploy_template.py | 18 +----------------- cloud/vmware/vmware_guest_state.py | 17 ----------------- 2 files changed, 1 insertion(+), 34 deletions(-) diff --git a/cloud/vmware/vmware_deploy_template.py b/cloud/vmware/vmware_deploy_template.py index 4ca05fc6b76..86f36b0a676 100644 --- a/cloud/vmware/vmware_deploy_template.py +++ b/cloud/vmware/vmware_deploy_template.py @@ -129,7 +129,7 @@ import os import ssl import time -from pprint import pprint + from ansible.module_utils.urls import fetch_url class PyVmomiHelper(object): @@ -237,8 +237,6 @@ def getfolders(self): self.params['esxi']['datacenter']) self.folders = self._build_folder_tree(self.datacenter.vmFolder) self.folder_map = self._build_folder_map(self.folders) - #pprint(self.folder_map) - #sys.exit(1) return (self.folders, self.folder_map) @@ -463,7 +461,6 @@ def deploy_template(self, poweron=False, wait_for_ip=False): clonespec = vim.vm.CloneSpec() clonespec.location = relospec - print "cloning VM..." template = get_obj(self.content, [vim.VirtualMachine], self.params['template_src']) task = template.Clone(folder=destfolder, name=self.params['guest'], spec=clonespec) self.wait_for_task(task) @@ -472,21 +469,11 @@ def deploy_template(self, poweron=False, wait_for_ip=False): return ({'changed': False, 'failed': True, 'msg': task.info.error.msg}) else: - #import epdb; epdb.st() vm = task.info.result - - #if wait_for_ip and not poweron: - # print "powering on the VM ..." - # self.set_powerstate(vm, 'poweredon') - if wait_for_ip: - print "powering on the VM ..." self.set_powerstate(vm, 'poweredon', force=False) - print "waiting for IP ..." self.wait_for_vm_ip(vm) - vm_facts = self.gather_facts(vm) - #import epdb; epdb.st() return ({'changed': True, 'failed': False, 'instance': vm_facts}) @@ -495,7 +482,6 @@ def wait_for_task(self, task): # https://www.vmware.com/support/developer/vc-sdk/visdk25pubs/ReferenceGuide/vim.TaskInfo.html # https://github.com/virtdevninja/pyvmomi-community-samples/blob/master/samples/tools/tasks.py while task.info.state not in ['success', 'error']: - print(task.info.state) time.sleep(1) def wait_for_vm_ip(self, vm, poll=100, sleep=5): @@ -503,10 +489,8 @@ def wait_for_vm_ip(self, vm, poll=100, sleep=5): facts = {} thispoll = 0 while not ips and thispoll <= poll: - print "polling for IP" newvm = self.getvm(uuid=vm.config.uuid) facts = self.gather_facts(newvm) - print "\t%s %s" % (facts['ipv4'], facts['ipv6']) if facts['ipv4'] or facts['ipv6']: ips = True else: diff --git a/cloud/vmware/vmware_guest_state.py b/cloud/vmware/vmware_guest_state.py index ed8a3b2c986..af79026f3db 100644 --- a/cloud/vmware/vmware_guest_state.py +++ b/cloud/vmware/vmware_guest_state.py @@ -81,7 +81,6 @@ import os import ssl import time -from pprint import pprint from ansible.module_utils.urls import fetch_url @@ -191,8 +190,6 @@ def getfolders(self): self.params['esxi']['datacenter']) self.folders = self._build_folder_tree(self.datacenter.vmFolder) self.folder_map = self._build_folder_map(self.folders) - #pprint(self.folder_map) - #sys.exit(1) return (self.folders, self.folder_map) @@ -435,7 +432,6 @@ def deploy_template(self, poweron=False, wait_for_ip=False): clonespec.location = relospec #clonespec.powerOn = power_on - print "cloning VM..." template = get_obj(self.content, [vim.VirtualMachine], self.params['template_src']) task = template.Clone(folder=destfolder, name=self.params['guest'], spec=clonespec) self.wait_for_task(task) @@ -444,21 +440,11 @@ def deploy_template(self, poweron=False, wait_for_ip=False): return ({'changed': False, 'failed': True, 'msg': task.info.error.msg}) else: - #import epdb; epdb.st() vm = task.info.result - - #if wait_for_ip and not poweron: - # print "powering on the VM ..." - # self.set_powerstate(vm, 'poweredon') - if wait_for_ip: - print "powering on the VM ..." self.set_powerstate(vm, 'poweredon', force=False) - print "waiting for IP ..." self.wait_for_vm_ip(vm) - vm_facts = self.gather_facts(vm) - #import epdb; epdb.st() return ({'changed': True, 'failed': False, 'instance': vm_facts}) @@ -467,7 +453,6 @@ def wait_for_task(self, task): # https://www.vmware.com/support/developer/vc-sdk/visdk25pubs/ReferenceGuide/vim.TaskInfo.html # https://github.com/virtdevninja/pyvmomi-community-samples/blob/master/samples/tools/tasks.py while task.info.state not in ['success', 'error']: - print(task.info.state) time.sleep(1) def wait_for_vm_ip(self, vm, poll=100, sleep=5): @@ -475,10 +460,8 @@ def wait_for_vm_ip(self, vm, poll=100, sleep=5): facts = {} thispoll = 0 while not ips and thispoll <= poll: - print "polling for IP" newvm = self.getvm(uuid=vm.config.uuid) facts = self.gather_facts(newvm) - print "\t%s %s" % (facts['ipv4'], facts['ipv6']) if facts['ipv4'] or facts['ipv6']: ips = True else: From 6cebd509d78ede52ee38dd7fff352735c2eceae6 Mon Sep 17 00:00:00 2001 From: James Tanner Date: Mon, 1 Aug 2016 23:08:30 -0400 Subject: [PATCH 1894/2522] add examples --- cloud/vmware/vmware_guest_state.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/cloud/vmware/vmware_guest_state.py b/cloud/vmware/vmware_guest_state.py index af79026f3db..f4964c63787 100644 --- a/cloud/vmware/vmware_guest_state.py +++ b/cloud/vmware/vmware_guest_state.py @@ -61,6 +61,32 @@ ''' EXAMPLES = ''' +Examples from an ansible playbook ... + - name: poweroff the VM + vmware_guest_state: + validate_certs: False + hostname: 192.168.1.209 + username: administrator@vsphere.local + password: vmware + guest: testvm_2 + vm_folder: testvms + state: powered_off + ignore_errors: True + + - name: remove the VM + vmware_guest_state: + validate_certs: False + hostname: 192.168.1.209 + username: administrator@vsphere.local + password: vmware + guest: testvm_2 + vm_folder: testvms + state: absent + ignore_errors: True +''' + +RETURN = ''' +state=absent ''' try: From 13215eeb7cb6a236851c324b2e5b012037c4663b Mon Sep 17 00:00:00 2001 From: James Tanner Date: Tue, 2 Aug 2016 11:28:03 -0400 Subject: [PATCH 1895/2522] Consolidate to one module and use new arg spec --- ...are_deploy_template.py => vmware_guest.py} | 190 ++-- cloud/vmware/vmware_guest_state.py | 824 ------------------ 2 files changed, 114 insertions(+), 900 deletions(-) rename cloud/vmware/{vmware_deploy_template.py => vmware_guest.py} (83%) delete mode 100644 cloud/vmware/vmware_guest_state.py diff --git a/cloud/vmware/vmware_deploy_template.py b/cloud/vmware/vmware_guest.py similarity index 83% rename from cloud/vmware/vmware_deploy_template.py rename to cloud/vmware/vmware_guest.py index 86f36b0a676..1f12a6c4f10 100644 --- a/cloud/vmware/vmware_deploy_template.py +++ b/cloud/vmware/vmware_guest.py @@ -17,10 +17,13 @@ DOCUMENTATION = ''' --- -module: vmware_deploy_template -short_description: Deploy a template to a new virtualmachine in vcenter +module: vmware_guest +short_description: Manages virtualmachines in vcenter description: - - Uses the pyvmomi Clone() method to copy a template to a new virtualmachine in vcenter + - Uses pyvmomi to ... + - copy a template to a new virtualmachine + - poweron/poweroff/restart a virtualmachine + - remove a virtualmachine version_added: 2.2 author: James Tanner (@jctanner) notes: @@ -29,30 +32,43 @@ - "python >= 2.6" - PyVmomi options: - guest: + state: + description: + - What state should the virtualmachine be in? + required: True + choices: ['present', 'absent', 'poweredon', 'poweredoff', 'restarted', 'suspended'] + name: description: - Name of the newly deployed guest required: True + name_match: + description: + - If multiple vms matching the name, use the first or last found + required: False + default: 'first' + choices: ['first', 'last'] + uuid: + description: + - UUID of the instance to manage if known, this is vmware's unique identifier. + - This is required if name is not supplied. + required: False template: description: - - Name of the template to deploy - required: True - vm_folder: + - Name of the template to deploy, if needed to create the guest (state=present). + - If the guest exists already this setting will be ignored. + required: False + folder: description: - Destination folder path for the new guest required: False - vm_hardware: + hardware: description: - Attributes such as cpus, memroy, osid, and disk controller required: False - vm_nic: + nic: description: - A list of nics to add required: True - power_on_after_clone: - description: - - Poweron the VM after it is cloned - required: False wait_for_ip_address: description: - Wait until vcenter detects an IP address for the guest @@ -61,7 +77,7 @@ description: - Ignore warnings and complete the actions required: False - datacenter_name: + datacenter: description: - Destination datacenter for the deploy operation required: True @@ -75,30 +91,30 @@ EXAMPLES = ''' Example from Ansible playbook - name: create the VM - vmware_deploy_template: + vmware_guest: validate_certs: False hostname: 192.168.1.209 username: administrator@vsphere.local password: vmware - guest: testvm_2 - vm_folder: testvms - vm_disk: + name: testvm_2 + state: poweredon + folder: testvms + disk: - size_gb: 10 type: thin datastore: g73_datastore - vm_nic: + nic: - type: vmxnet3 network: VM Network network_type: standard - vm_hardware: + hardware: memory_mb: 512 num_cpus: 1 osid: centos64guest scsi: paravirtual - datacenter_name: datacenter1 + datacenter: datacenter1 esxi_hostname: 192.168.1.117 - template_src: template_el7 - power_on_after_clone: yes + template: template_el7 wait_for_ip_address: yes register: deploy ''' @@ -240,7 +256,7 @@ def getfolders(self): return (self.folders, self.folder_map) - def getvm(self, name=None, uuid=None, folder=None, firstmatch=False): + def getvm(self, name=None, uuid=None, folder=None, name_match=None): # https://www.vmware.com/support/developer/vc-sdk/visdk2xpubs/ReferenceGuide/vim.SearchIndex.html # self.si.content.searchIndex.FindByInventoryPath('DC1/vm/test_folder') @@ -276,32 +292,35 @@ def getvm(self, name=None, uuid=None, folder=None, firstmatch=False): if not type(cObj) == vim.VirtualMachine: continue if cObj.name == name: - #vm = cObj - #break matches.append(cObj) - if len(matches) > 1 and not firstmatch: - assert len(matches) <= 1, "more than 1 vm exists by the name %s in folder %s. Please specify a uuid, a datacenter or firstmatch=true" % name + if len(matches) > 1 and not name_match: + module.fail_json(msg='more than 1 vm exists by the name %s in folder %s. Please specify a uuid, a datacenter or name_match' \ + % (folder, name)) elif len(matches) > 0: vm = matches[0] - #else: - #import epdb; epdb.st() - else: - if firstmatch: - vm = get_obj(self.content, [vim.VirtualMachine], name) - else: + vmList = get_all_objs(self.content, [vim.VirtualMachine]) + if name_match: + if name_match == 'first': + vm = get_obj(self.content, [vim.VirtualMachine], name) + elif name_match == 'last': + matches = [] + vmList = get_all_objs(self.content, [vim.VirtualMachine]) + for thisvm in vmList: + if thisvm.config.name == name: + matches.append(thisvm) + if matches: + vm = matches[-1] + else: matches = [] vmList = get_all_objs(self.content, [vim.VirtualMachine]) for thisvm in vmList: - if thisvm.config == None: - import epdb; epdb.st() if thisvm.config.name == name: matches.append(thisvm) - # FIXME - fail this properly - #import epdb; epdb.st() - assert len(matches) <= 1, "more than 1 vm exists by the name %s. Please specify a folder, a uuid, or firstmatch=true" % name - if matches: - vm = matches[0] + if len(matches) > 1: + module.fail_json(msg='more than 1 vm exists by the name %s. Please specify a uuid, or a folder, or a datacenter or name_match' % name) + if matches: + vm = matches[0] return vm @@ -432,16 +451,20 @@ def deploy_template(self, poweron=False, wait_for_ip=False): datacenters = get_all_objs(self.content, [vim.Datacenter]) datacenter = get_obj(self.content, [vim.Datacenter], - self.params['datacenter_name']) + self.params['datacenter']) # folder is a required clone argument if len(datacenters) > 1: # FIXME: need to find the folder in the right DC. raise "multi-dc with folders is not yet implemented" else: - destfolder = get_obj(self.content, [vim.Folder], self.params['vm_folder']) + destfolder = get_obj( + self.content, + [vim.Folder], + self.params['folder'] + ) - datastore_name = self.params['vm_disk'][0]['datastore'] + datastore_name = self.params['disk'][0]['datastore'] datastore = get_obj(self.content, [vim.Datastore], datastore_name) @@ -461,8 +484,8 @@ def deploy_template(self, poweron=False, wait_for_ip=False): clonespec = vim.vm.CloneSpec() clonespec.location = relospec - template = get_obj(self.content, [vim.VirtualMachine], self.params['template_src']) - task = template.Clone(folder=destfolder, name=self.params['guest'], spec=clonespec) + template = get_obj(self.content, [vim.VirtualMachine], self.params['template']) + task = template.Clone(folder=destfolder, name=self.params['name'], spec=clonespec) self.wait_for_task(task) if task.info.state == 'error': @@ -754,56 +777,60 @@ def main(): state=dict( required=False, choices=[ - 'powered_on', - 'powered_off', + 'poweredon', + 'poweredoff', 'present', 'absent', 'restarted', 'reconfigured' ], default='present'), - template_src=dict(required=False, type='str'), - guest=dict(required=True, type='str'), - vm_folder=dict(required=False, type='str', default=None), - vm_disk=dict(required=False, type='list', default=[]), - vm_nic=dict(required=False, type='list', default=[]), - vm_hardware=dict(required=False, type='dict', default={}), - vm_hw_version=dict(required=False, default=None, type='str'), + validate_certs=dict(required=False, type='bool', default=True), + template_src=dict(required=False, type='str', aliases=['template']), + name=dict(required=True, type='str'), + name_match=dict(required=False, type='str', default='first'), + uuid=dict(required=False, type='str'), + folder=dict(required=False, type='str', default=None, aliases=['folder']), + disk=dict(required=False, type='list', default=[]), + nic=dict(required=False, type='list', default=[]), + hardware=dict(required=False, type='dict', default={}), force=dict(required=False, type='bool', default=False), - firstmatch=dict(required=False, type='bool', default=False), - datacenter_name=dict(required=False, type='str', default=None), + datacenter=dict(required=False, type='str', default=None), esxi_hostname=dict(required=False, type='str', default=None), - validate_certs=dict(required=False, type='bool', default=True), - power_on_after_clone=dict(required=False, type='bool', default=True), wait_for_ip_address=dict(required=False, type='bool', default=True) ), supports_check_mode=True, mutually_exclusive=[], required_together=[ ['state', 'force'], - [ - 'vm_disk', - 'vm_nic', - 'vm_hardware', - 'esxi_hostname' - ], - ['template_src'], + ['template'], ], ) pyv = PyVmomiHelper(module) # Check if the VM exists before continuing - vm = pyv.getvm(name=module.params['guest'], - folder=module.params['vm_folder'], - firstmatch=module.params['firstmatch']) + vm = pyv.getvm(name=module.params['name'], + folder=module.params['folder'], + uuid=module.params['uuid'], + name_match=module.params['name_match']) # VM already exists if vm: - # Run for facts only - if module.params['vmware_guest_facts']: + + if module.params['state'] == 'absent': + # destroy it + if module.params['force']: + # has to be poweredoff first + result = pyv.set_powerstate(vm, 'poweredoff', module.params['force']) + result = pyv.remove_vm(vm) + elif module.params['state'] in ['poweredon', 'poweredoff', 'restarted']: + # set powerstate + result = pyv.set_powerstate(vm, module.params['state'], module.params['force']) + else: + # Run for facts only try: - module.exit_json(ansible_facts=pyv.gather_facts(vm)) + module.exit_json(instance=pyv.gather_facts(vm)) except Exception: e = get_exception() module.fail_json( @@ -811,11 +838,22 @@ def main(): # VM doesn't exist else: + create_states = ['poweredon', 'poweredoff', 'present', 'restarted'] + if module.params['state'] in create_states: + poweron = (module.params['state'] != 'poweredoff') + # Create it ... + result = pyv.deploy_template( + poweron=poweron, + wait_for_ip=module.params['wait_for_ip_address'] + ) + elif module.params['state'] == 'absent': + result = {'changed': False, 'failed': False} + else: + result = {'changed': False, 'failed': False} - # Create it ... - result = pyv.deploy_template(poweron=module.params['power_on_after_clone'], - wait_for_ip=module.params['wait_for_ip_address']) - + # FIXME + if not 'failed' in result: + result['failed'] = False if result['failed']: module.fail_json(**result) diff --git a/cloud/vmware/vmware_guest_state.py b/cloud/vmware/vmware_guest_state.py deleted file mode 100644 index f4964c63787..00000000000 --- a/cloud/vmware/vmware_guest_state.py +++ /dev/null @@ -1,824 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -DOCUMENTATION = ''' -module: vmware_guest_state -short_description: manage the state of a vmware virtualmachine in vcenter -description: - - Uses pyvmomi to poweron/poweroff/delete/restart a virtualmachine -version_added: 2.2 -author: James Tanner (@jctanner) -notes: - - Tested on vSphere 6.0 -requirements: - - "python >= 2.6" - - PyVmomi -options: - guest: - description: - - Name of the newly deployed guest - required: True - state: - description: - - What state should the machine be in? - - restarted/absent/poweredon/poweredoff - required: True - vm_uuid: - description: - - UUID of the instance to manage if known - required: False - vm_folder: - description: - - Folder path for the guest if known - required: False - firstmatch: - description: - - If multiple vms match, use the first found - required: False - force: - description: - - Ignore warnings and complete the actions - required: False - datacenter_name: - description: - - Destination datacenter for the deploy operation - required: True -extends_documentation_fragment: vmware.documentation -''' - -EXAMPLES = ''' -Examples from an ansible playbook ... - - name: poweroff the VM - vmware_guest_state: - validate_certs: False - hostname: 192.168.1.209 - username: administrator@vsphere.local - password: vmware - guest: testvm_2 - vm_folder: testvms - state: powered_off - ignore_errors: True - - - name: remove the VM - vmware_guest_state: - validate_certs: False - hostname: 192.168.1.209 - username: administrator@vsphere.local - password: vmware - guest: testvm_2 - vm_folder: testvms - state: absent - ignore_errors: True -''' - -RETURN = ''' -state=absent -''' - -try: - import json -except ImportError: - import simplejson as json - -HAS_PYVMOMI = False -try: - import pyVmomi - from pyVmomi import vim - from pyVim.connect import SmartConnect, Disconnect - HAS_PYVMOMI = True -except ImportError: - pass - -import atexit -import os -import ssl -import time - -from ansible.module_utils.urls import fetch_url - - -class PyVmomiHelper(object): - - def __init__(self, module): - - if not HAS_PYVMOMI: - module.fail_json(msg='pyvmomi module required') - - self.module = module - self.params = module.params - self.si = None - self.smartconnect() - self.datacenter = None - - def smartconnect(self): - kwargs = {'host': self.params['hostname'], - 'user': self.params['username'], - 'pwd': self.params['password']} - - if hasattr(ssl, 'SSLContext'): - context = ssl.SSLContext(ssl.PROTOCOL_TLSv1) - context.verify_mode = ssl.CERT_NONE - kwargs['sslContext'] = context - - # CONNECT TO THE SERVER - try: - self.si = SmartConnect(**kwargs) - except Exception: - err = get_exception() - self.module.fail_json(msg="Cannot connect to %s: %s" % - (kwargs['host'], err)) - atexit.register(Disconnect, self.si) - self.content = self.si.RetrieveContent() - - def _build_folder_tree(self, folder, tree={}, treepath=None): - - tree = {'virtualmachines': [], - 'subfolders': {}, - 'name': folder.name} - - children = None - if hasattr(folder, 'childEntity'): - children = folder.childEntity - - if children: - for child in children: - if child == folder or child in tree: - continue - if type(child) == vim.Folder: - #ctree = self._build_folder_tree(child, tree={}) - ctree = self._build_folder_tree(child) - tree['subfolders'][child] = dict.copy(ctree) - elif type(child) == vim.VirtualMachine: - tree['virtualmachines'].append(child) - else: - if type(folder) == vim.VirtualMachine: - return folder - return tree - - - def _build_folder_map(self, folder, vmap={}, inpath='/'): - - ''' Build a searchable index for vms+uuids+folders ''' - - if type(folder) == tuple: - folder = folder[1] - - if not 'names' in vmap: - vmap['names'] = {} - if not 'uuids' in vmap: - vmap['uuids'] = {} - if not 'paths' in vmap: - vmap['paths'] = {} - - if inpath == '/': - thispath = '/vm' - else: - thispath = os.path.join(inpath, folder['name']) - - for item in folder.items(): - k = item[0] - v = item[1] - if k == 'name': - pass - elif k == 'subfolders': - for x in v.items(): - vmap = self._build_folder_map(x, vmap=vmap, inpath=thispath) - elif k == 'virtualmachines': - for x in v: - if not x.config.name in vmap['names']: - vmap['names'][x.config.name] = [] - vmap['names'][x.config.name].append(x.config.uuid) - vmap['uuids'][x.config.uuid] = x.config.name - if not thispath in vmap['paths']: - vmap['paths'][thispath] = [] - vmap['paths'][thispath].append(x.config.uuid) - - return vmap - - def getfolders(self): - - if not self.datacenter: - self.datacenter = get_obj(self.content, [vim.Datacenter], - self.params['esxi']['datacenter']) - self.folders = self._build_folder_tree(self.datacenter.vmFolder) - self.folder_map = self._build_folder_map(self.folders) - return (self.folders, self.folder_map) - - - def getvm(self, name=None, uuid=None, folder=None, firstmatch=False): - - # https://www.vmware.com/support/developer/vc-sdk/visdk2xpubs/ReferenceGuide/vim.SearchIndex.html - # self.si.content.searchIndex.FindByInventoryPath('DC1/vm/test_folder') - - vm = None - folder_path = None - - if uuid: - vm = self.si.content.searchIndex.FindByUuid(uuid=uuid, vmSearch=True) - - elif folder: - - matches = [] - folder_paths = [] - - datacenter = None - if 'esxi' in self.params: - if 'datacenter' in self.params['esxi']: - datacenter = self.params['esxi']['datacenter'] - - if datacenter: - folder_paths.append('%s/vm/%s' % (datacenter, folder)) - else: - # get a list of datacenters - datacenters = get_all_objs(self.content, [vim.Datacenter]) - datacenters = [x.name for x in datacenters] - for dc in datacenters: - folder_paths.append('%s/vm/%s' % (dc, folder)) - - for folder_path in folder_paths: - fObj = self.si.content.searchIndex.FindByInventoryPath(folder_path) - for cObj in fObj.childEntity: - if not type(cObj) == vim.VirtualMachine: - continue - if cObj.name == name: - #vm = cObj - #break - matches.append(cObj) - if len(matches) > 1 and not firstmatch: - assert len(matches) <= 1, "more than 1 vm exists by the name %s in folder %s. Please specify a uuid, a datacenter or firstmatch=true" % name - elif len(matches) > 0: - vm = matches[0] - #else: - #import epdb; epdb.st() - - else: - if firstmatch: - vm = get_obj(self.content, [vim.VirtualMachine], name) - else: - matches = [] - vmList = get_all_objs(self.content, [vim.VirtualMachine]) - for thisvm in vmList: - if thisvm.config == None: - import epdb; epdb.st() - if thisvm.config.name == name: - matches.append(thisvm) - # FIXME - fail this properly - #import epdb; epdb.st() - assert len(matches) <= 1, "more than 1 vm exists by the name %s. Please specify a folder, a uuid, or firstmatch=true" % name - if matches: - vm = matches[0] - - return vm - - - def set_powerstate(self, vm, state, force): - """ - Set the power status for a VM determined by the current and - requested states. force is forceful - """ - facts = self.gather_facts(vm) - expected_state = state.replace('_', '').lower() - current_state = facts['hw_power_status'].lower() - result = {} - - # Need Force - if not force and current_state not in ['poweredon', 'poweredoff']: - return "VM is in %s power state. Force is required!" % current_state - - # State is already true - if current_state == expected_state: - result['changed'] = False - result['failed'] = False - else: - task = None - try: - if expected_state == 'poweredoff': - task = vm.PowerOff() - - elif expected_state == 'poweredon': - task = vm.PowerOn() - - elif expected_state == 'restarted': - if current_state in ('poweredon', 'poweringon', 'resetting'): - task = vm.Reset() - else: - result = {'changed': False, 'failed': True, - 'msg': "Cannot restart VM in the current state %s" % current_state} - - except Exception: - result = {'changed': False, 'failed': True, - 'msg': get_exception()} - - if task: - self.wait_for_task(task) - if task.info.state == 'error': - result = {'changed': False, 'failed': True, 'msg': task.info.error.msg} - else: - result = {'changed': True, 'failed': False} - - # need to get new metadata if changed - if result['changed']: - newvm = self.getvm(uuid=vm.config.uuid) - facts = self.gather_facts(newvm) - result['instance'] = facts - return result - - - def gather_facts(self, vm): - - ''' Gather facts from vim.VirtualMachine object. ''' - - facts = { - 'module_hw': True, - 'hw_name': vm.config.name, - 'hw_power_status': vm.summary.runtime.powerState, - 'hw_guest_full_name': vm.summary.guest.guestFullName, - 'hw_guest_id': vm.summary.guest.guestId, - 'hw_product_uuid': vm.config.uuid, - 'hw_processor_count': vm.config.hardware.numCPU, - 'hw_memtotal_mb': vm.config.hardware.memoryMB, - 'hw_interfaces':[], - 'ipv4': None, - 'ipv6': None, - } - - netDict = {} - for device in vm.guest.net: - mac = device.macAddress - ips = list(device.ipAddress) - netDict[mac] = ips - for k,v in netDict.iteritems(): - for ipaddress in v: - if ipaddress: - if '::' in ipaddress: - facts['ipv6'] = ipaddress - else: - facts['ipv4'] = ipaddress - - for idx,entry in enumerate(vm.config.hardware.device): - if not hasattr(entry, 'macAddress'): - continue - - factname = 'hw_eth' + str(idx) - facts[factname] = { - 'addresstype': entry.addressType, - 'label': entry.deviceInfo.label, - 'macaddress': entry.macAddress, - 'ipaddresses': netDict.get(entry.macAddress, None), - 'macaddress_dash': entry.macAddress.replace(':', '-'), - 'summary': entry.deviceInfo.summary, - } - facts['hw_interfaces'].append('eth'+str(idx)) - - return facts - - - def remove_vm(self, vm): - # https://www.vmware.com/support/developer/converter-sdk/conv60_apireference/vim.ManagedEntity.html#destroy - task = vm.Destroy() - self.wait_for_task(task) - - if task.info.state == 'error': - return ({'changed': False, 'failed': True, 'msg': task.info.error.msg}) - else: - return ({'changed': True, 'failed': False}) - - - def deploy_template(self, poweron=False, wait_for_ip=False): - - # https://github.com/vmware/pyvmomi-community-samples/blob/master/samples/clone_vm.py - - ''' - deploy_template( - vsphere_client=viserver, - esxi=esxi, - resource_pool=resource_pool, - guest=guest, - template_src=template_src, - module=module, - cluster_name=cluster, - snapshot_to_clone=snapshot_to_clone, - power_on_after_clone=power_on_after_clone, - vm_extra_config=vm_extra_config - ) - ''' - - # FIXME: - # - clusters - # - resource pools - # - multiple templates by the same name - # - static IPs - - datacenters = get_all_objs(self.content, [vim.Datacenter]) - datacenter = get_obj(self.content, [vim.Datacenter], - self.params['esxi']['datacenter']) - - # folder is a required clone argument - if len(datacenters) > 1: - # FIXME: need to find the folder in the right DC. - raise "multi-dc with folders is not yet implemented" - else: - destfolder = get_obj(self.content, [vim.Folder], self.params['vm_folder']) - - datastore_name = self.params['vm_disk']['disk1']['datastore'] - datastore = get_obj(self.content, [vim.Datastore], datastore_name) - - - # cluster or hostsystem ... ? - #cluster = get_obj(self.content, [vim.ClusterComputeResource], self.params['esxi']['hostname']) - hostsystem = get_obj(self.content, [vim.HostSystem], self.params['esxi']['hostname']) - #import epdb; epdb.st() - - resource_pools = get_all_objs(self.content, [vim.ResourcePool]) - #import epdb; epdb.st() - - relospec = vim.vm.RelocateSpec() - relospec.datastore = datastore - - # fixme ... use the pool from the cluster if given - relospec.pool = resource_pools[0] - relospec.host = hostsystem - #import epdb; epdb.st() - - clonespec = vim.vm.CloneSpec() - clonespec.location = relospec - #clonespec.powerOn = power_on - - template = get_obj(self.content, [vim.VirtualMachine], self.params['template_src']) - task = template.Clone(folder=destfolder, name=self.params['guest'], spec=clonespec) - self.wait_for_task(task) - - if task.info.state == 'error': - return ({'changed': False, 'failed': True, 'msg': task.info.error.msg}) - else: - - vm = task.info.result - if wait_for_ip: - self.set_powerstate(vm, 'poweredon', force=False) - self.wait_for_vm_ip(vm) - vm_facts = self.gather_facts(vm) - return ({'changed': True, 'failed': False, 'instance': vm_facts}) - - - def wait_for_task(self, task): - # https://www.vmware.com/support/developer/vc-sdk/visdk25pubs/ReferenceGuide/vim.Task.html - # https://www.vmware.com/support/developer/vc-sdk/visdk25pubs/ReferenceGuide/vim.TaskInfo.html - # https://github.com/virtdevninja/pyvmomi-community-samples/blob/master/samples/tools/tasks.py - while task.info.state not in ['success', 'error']: - time.sleep(1) - - def wait_for_vm_ip(self, vm, poll=100, sleep=5): - ips = None - facts = {} - thispoll = 0 - while not ips and thispoll <= poll: - newvm = self.getvm(uuid=vm.config.uuid) - facts = self.gather_facts(newvm) - if facts['ipv4'] or facts['ipv6']: - ips = True - else: - time.sleep(sleep) - thispoll += 1 - - #import epdb; epdb.st() - return facts - - - def fetch_file_from_guest(self, vm, username, password, src, dest): - - ''' Use VMWare's filemanager api to fetch a file over http ''' - - result = {'failed': False} - - tools_status = vm.guest.toolsStatus - if (tools_status == 'toolsNotInstalled' or - tools_status == 'toolsNotRunning'): - result['failed'] = True - result['msg'] = "VMwareTools is not installed or is not running in the guest" - return result - - # https://github.com/vmware/pyvmomi/blob/master/docs/vim/vm/guest/NamePasswordAuthentication.rst - creds = vim.vm.guest.NamePasswordAuthentication( - username=username, password=password - ) - - # https://github.com/vmware/pyvmomi/blob/master/docs/vim/vm/guest/FileManager/FileTransferInformation.rst - fti = self.content.guestOperationsManager.fileManager. \ - InitiateFileTransferFromGuest(vm, creds, src) - - result['size'] = fti.size - result['url'] = fti.url - - # Use module_utils to fetch the remote url returned from the api - rsp, info = fetch_url(self.module, fti.url, use_proxy=False, - force=True, last_mod_time=None, - timeout=10, headers=None) - - # save all of the transfer data - for k,v in info.iteritems(): - result[k] = v - - # exit early if xfer failed - if info['status'] != 200: - result['failed'] = True - return result - - # attempt to read the content and write it - try: - with open(dest, 'wb') as f: - f.write(rsp.read()) - except Exception as e: - result['failed'] = True - result['msg'] = str(e) - - return result - - - def push_file_to_guest(self, vm, username, password, src, dest, overwrite=True): - - ''' Use VMWare's filemanager api to push a file over http ''' - - result = {'failed': False} - - tools_status = vm.guest.toolsStatus - if (tools_status == 'toolsNotInstalled' or - tools_status == 'toolsNotRunning'): - result['failed'] = True - result['msg'] = "VMwareTools is not installed or is not running in the guest" - return result - - # https://github.com/vmware/pyvmomi/blob/master/docs/vim/vm/guest/NamePasswordAuthentication.rst - creds = vim.vm.guest.NamePasswordAuthentication( - username=username, password=password - ) - - # the api requires a filesize in bytes - filesize = None - fdata = None - try: - #filesize = os.path.getsize(src) - filesize = os.stat(src).st_size - fdata = None - with open(src, 'rb') as f: - fdata = f.read() - result['local_filesize'] = filesize - except Exception as e: - result['failed'] = True - result['msg'] = "Unable to read src file: %s" % str(e) - return result - - # https://www.vmware.com/support/developer/converter-sdk/conv60_apireference/vim.vm.guest.FileManager.html#initiateFileTransferToGuest - file_attribute = vim.vm.guest.FileManager.FileAttributes() - url = self.content.guestOperationsManager.fileManager. \ - InitiateFileTransferToGuest(vm, creds, dest, file_attribute, - filesize, overwrite) - - # PUT the filedata to the url ... - rsp, info = fetch_url(self.module, url, method="put", data=fdata, - use_proxy=False, force=True, last_mod_time=None, - timeout=10, headers=None) - - result['msg'] = str(rsp.read()) - - # save all of the transfer data - for k,v in info.iteritems(): - result[k] = v - - return result - - - def run_command_in_guest(self, vm, username, password, program_path, program_args, program_cwd, program_env): - - result = {'failed': False} - - tools_status = vm.guest.toolsStatus - if (tools_status == 'toolsNotInstalled' or - tools_status == 'toolsNotRunning'): - result['failed'] = True - result['msg'] = "VMwareTools is not installed or is not running in the guest" - return result - - # https://github.com/vmware/pyvmomi/blob/master/docs/vim/vm/guest/NamePasswordAuthentication.rst - creds = vim.vm.guest.NamePasswordAuthentication( - username=username, password=password - ) - - res = None - pdata = None - try: - # https://github.com/vmware/pyvmomi/blob/master/docs/vim/vm/guest/ProcessManager.rst - pm = self.content.guestOperationsManager.processManager - # https://www.vmware.com/support/developer/converter-sdk/conv51_apireference/vim.vm.guest.ProcessManager.ProgramSpec.html - ps = vim.vm.guest.ProcessManager.ProgramSpec( - #programPath=program, - #arguments=args - programPath=program_path, - arguments=program_args, - workingDirectory=program_cwd, - ) - res = pm.StartProgramInGuest(vm, creds, ps) - result['pid'] = res - pdata = pm.ListProcessesInGuest(vm, creds, [res]) - - # wait for pid to finish - while not pdata[0].endTime: - time.sleep(1) - pdata = pm.ListProcessesInGuest(vm, creds, [res]) - result['owner'] = pdata[0].owner - result['startTime'] = pdata[0].startTime.isoformat() - result['endTime'] = pdata[0].endTime.isoformat() - result['exitCode'] = pdata[0].exitCode - if result['exitCode'] != 0: - result['failed'] = True - result['msg'] = "program exited non-zero" - else: - result['msg'] = "program completed successfully" - - except Exception as e: - result['msg'] = str(e) - result['failed'] = True - - return result - - -def get_obj(content, vimtype, name): - """ - Return an object by name, if name is None the - first found object is returned - """ - obj = None - container = content.viewManager.CreateContainerView( - content.rootFolder, vimtype, True) - for c in container.view: - if name: - if c.name == name: - obj = c - break - else: - obj = c - break - - container.Destroy() - return obj - - -def get_all_objs(content, vimtype): - """ - Get all the vsphere objects associated with a given type - """ - obj = [] - container = content.viewManager.CreateContainerView(content.rootFolder, vimtype, True) - for c in container.view: - obj.append(c) - container.Destroy() - return obj - - -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 main(): - - module = AnsibleModule( - argument_spec=dict( - validate_certs=dict(required=False, type='bool', default=True), - hostname=dict( - type='str', - default=os.environ.get('VMWARE_HOST') - ), - username=dict( - type='str', - default=os.environ.get('VMWARE_USER') - ), - password=dict( - type='str', no_log=True, - default=os.environ.get('VMWARE_PASSWORD') - ), - state=dict( - required=True, - choices=[ - 'powered_on', - 'powered_off', - 'present', - 'absent', - 'restarted', - ], - ), - guest=dict(required=True, type='str'), - vm_folder=dict(required=False, type='str', default=None), - vm_uuid=dict(required=False, type='str', default=None), - firstmatch=dict(required=False, type='bool', default=False), - force=dict(required=False, type='bool', default=False), - datacenter=dict(required=False, type='str', default=None), - ), - supports_check_mode=True, - mutually_exclusive=[], - required_together=[], - ) - - pyv = PyVmomiHelper(module) - - # Check if the VM exists before continuing - vm = pyv.getvm(name=module.params['guest'], - folder=module.params['vm_folder'], - uuid=module.params['vm_uuid'], - firstmatch=module.params['firstmatch']) - - if vm: - # Power Changes - if module.params['state'] in ['powered_on', 'powered_off', 'restarted']: - result = pyv.set_powerstate(vm, module.params['state'], module.params['force']) - - # Failure - if isinstance(result, basestring): - result = {'changed': False, 'failed': True, 'msg': result} - - # Just check if there - elif module.params['state'] == 'present': - result = {'changed': False} - - elif module.params['state'] == 'absent': - result = pyv.remove_vm(vm) - - # VM doesn't exist - else: - - if module.params['state'] == 'present': - result = {'failed': True, 'msg': "vm does not exist"} - - elif module.params['state'] in ['restarted', 'reconfigured']: - result = {'changed': False, 'failed': True, - 'msg': "No such VM %s. States [restarted, reconfigured] required an existing VM" % guest } - - elif module.params['state'] == 'absent': - result = {'changed': False, 'failed': False, - 'msg': "vm %s not present" % module.params['guest']} - - elif module.params['state'] in ['powered_off', 'powered_on']: - result = {'changed': False, 'failed': True, - 'msg': "No such VM %s. States [powered_off, powered_on] required an existing VM" % module.params['guest'] } - - if result['failed']: - module.fail_json(**result) - else: - module.exit_json(**result) - - -# this is magic, see lib/ansible/module_common.py -from ansible.module_utils.basic import * - -if __name__ == '__main__': - main() - From 679f30dd209f69f7658b9f60040932ca4f4c91cd Mon Sep 17 00:00:00 2001 From: Jeff Wozniak Date: Tue, 2 Aug 2016 11:13:39 -0700 Subject: [PATCH 1896/2522] Add logicmonitor and logicmonitor_facts modules --- monitoring/logicmonitor.py | 2170 ++++++++++++++++++++++++++++++ monitoring/logicmonitor_facts.py | 633 +++++++++ 2 files changed, 2803 insertions(+) create mode 100644 monitoring/logicmonitor.py create mode 100644 monitoring/logicmonitor_facts.py diff --git a/monitoring/logicmonitor.py b/monitoring/logicmonitor.py new file mode 100644 index 00000000000..be016327350 --- /dev/null +++ b/monitoring/logicmonitor.py @@ -0,0 +1,2170 @@ +#!/usr/bin/python + +"""LogicMonitor Ansible module for managing Collectors, Hosts and Hostgroups + Copyright (C) 2015 LogicMonitor + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA""" + +import datetime +import os +import platform +import socket +import sys +import types +import urllib + +HAS_LIB_JSON = True +try: + import json + # Detect the python-json library which is incompatible + # Look for simplejson if that's the case + try: + if ( + not isinstance(json.loads, types.FunctionType) or + not isinstance(json.dumps, types.FunctionType) + ): + raise ImportError + except AttributeError: + raise ImportError +except ImportError: + try: + import simplejson as json + except ImportError: + print( + '\n{"msg": "Error: ansible requires the stdlib json or ' + + 'simplejson module, neither was found!", "failed": true}' + ) + HAS_LIB_JSON = False + except SyntaxError: + print( + '\n{"msg": "SyntaxError: probably due to installed simplejson ' + + 'being for a different python version", "failed": true}' + ) + HAS_LIB_JSON = False + +RETURN = ''' +--- +success: + description: flag indicating that execution was successful + returned: success + type: boolean + sample: True +... +''' + + +DOCUMENTATION = ''' +--- +module: logicmonitor +short_description: Manage your LogicMonitor account through Ansible Playbooks +description: + - LogicMonitor is a hosted, full-stack, infrastructure monitoring platform. + - This module manages hosts, host groups, and collectors within your LogicMonitor account. +version_added: "2.2" +author: Ethan Culler-Mayeno, Jeff Wozniak +notes: + - You must have an existing LogicMonitor account for this module to function. +requirements: ["An existing LogicMonitor account", "Linux"] +options: + target: + description: + - The type of LogicMonitor object you wish to manage. + - "Collector: Perform actions on a LogicMonitor collector" + - NOTE You should use Ansible service modules such as 'service' or 'supervisorctl' for managing the Collector 'logicmonitor-agent' and 'logicmonitor-watchdog' services. Specifically, you'll probably want to start these services after a Collector add and stop these services before a Collector remove. + - "Host: Perform actions on a host device" + - "Hostgroup: Perform actions on a LogicMonitor host group" + - NOTE Host and Hostgroup tasks should always be performed via local_action. There are no benefits to running these tasks on the remote host and doing so will typically cause problems. + required: true + default: null + choices: ['collector', 'host', 'datsource', 'hostgroup'] + action: + description: + - The action you wish to perform on target + - "Add: Add an object to your LogicMonitor account" + - "Remove: Remove an object from your LogicMonitor account" + - "Update: Update properties, description, or groups (target=host) for an object in your LogicMonitor account" + - "SDT: Schedule downtime for an object in your LogicMonitor account" + required: true + default: null + choices: ['add', 'remove', 'update', 'sdt'] + company: + description: + - The LogicMonitor account company name. If you would log in to your account at "superheroes.logicmonitor.com" you would use "superheroes" + required: true + default: null + user: + description: + - A LogicMonitor user name. The module will authenticate and perform actions on behalf of this user + required: true + default: null + password: + description: + - The password of the specified LogicMonitor user + required: true + default: null + collector: + description: + - The fully qualified domain name of a collector in your LogicMonitor account. + - This is required for the creation of a LogicMonitor host (target=host action=add) + - This is required for updating, removing or scheduling downtime for hosts if 'displayname' isn't specified (target=host action=update action=remove action=sdt) + required: false + default: null + hostname: + description: + - The hostname of a host in your LogicMonitor account, or the desired hostname of a device to manage. + - Optional for managing hosts (target=host) + required: false + default: 'hostname -f' + displayname: + description: + - The display name of a host in your LogicMonitor account or the desired display name of a device to manage. + - Optional for managing hosts (target=host) + required: false + default: 'hostname -f' + description: + description: + - The long text description of the object in your LogicMonitor account + - Optional for managing hosts and host groups (target=host or target=hostgroup; action=add or action=update) + required: false + default: "" + properties: + description: + - A dictionary of properties to set on the LogicMonitor host or host group. + - Optional for managing hosts and host groups (target=host or target=hostgroup; action=add or action=update) + - This parameter will add or update existing properties in your LogicMonitor account or + required: false + default: {} + groups: + description: + - A list of groups that the host should be a member of. + - Optional for managing hosts (target=host; action=add or action=update) + required: false + default: [] + id: + description: + - ID of the datasource to target + - Required for management of LogicMonitor datasources (target=datasource) + required: false + default: null + fullpath: + description: + - The fullpath of the host group object you would like to manage + - Recommend running on a single Ansible host + - Required for management of LogicMonitor host groups (target=hostgroup) + required: false + default: null + alertenable: + description: + - A boolean flag to turn alerting on or off for an object + - Optional for managing all hosts (action=add or action=update) + required: false + default: true + choices: [true, false] + starttime: + description: + - The time that the Scheduled Down Time (SDT) should begin + - Optional for managing SDT (action=sdt) + - Y-m-d H:M + required: false + default: Now + duration: + description: + - The duration (minutes) of the Scheduled Down Time (SDT) + - Optional for putting an object into SDT (action=sdt) + required: false + default: 30 +... +''' +EXAMPLES = ''' + # example of adding a new LogicMonitor collector to these devices + --- + - hosts: collectors + remote_user: '{{ username }}' + vars: + company: 'mycompany' + user: 'myusername' + password: 'mypassword' + tasks: + - name: Deploy/verify LogicMonitor collectors + become: yes + logicmonitor: + target=collector + action=add + company={{ company }} + user={{ user }} + password={{ password }} + + #example of adding a list of hosts into monitoring + --- + - hosts: hosts + remote_user: '{{ username }}' + vars: + company: 'mycompany' + user: 'myusername' + password: 'mypassword' + tasks: + - name: Deploy LogicMonitor Host + # All tasks except for target=collector should use local_action + local_action: > + logicmonitor + target=host + action=add + collector='mycompany-Collector' + company='{{ company }}' + user='{{ user }}' + password='{{ password }}' + groups="/servers/production,/datacenter1" + properties="{'snmp.community':'secret','dc':'1', 'type':'prod'}" + + #example of putting a datasource in SDT + --- + - hosts: localhost + remote_user: '{{ username }}' + vars: + company: 'mycompany' + user: 'myusername' + password: 'mypassword' + tasks: + - name: SDT a datasource + # All tasks except for target=collector should use local_action + local_action: > + logicmonitor + target=datasource + action=sdt + id='123' + duration=3000 + starttime='2017-03-04 05:06' + company='{{ company }}' + user='{{ user }}' + password='{{ password }}' + + #example of creating a hostgroup + --- + - hosts: localhost + remote_user: '{{ username }}' + vars: + company: 'mycompany' + user: 'myusername' + password: 'mypassword' + tasks: + - name: Create a host group + # All tasks except for target=collector should use local_action + local_action: > + logicmonitor + target=hostgroup + action=add + fullpath='/servers/development' + company='{{ company }}' + user='{{ user }}' + password='{{ password }}' + properties="{'snmp.community':'commstring', 'type':'dev'}" + + #example of putting a list of hosts into SDT + --- + - hosts: hosts + remote_user: '{{ username }}' + vars: + company: 'mycompany' + user: 'myusername' + password: 'mypassword' + tasks: + - name: SDT hosts + # All tasks except for target=collector should use local_action + local_action: > + logicmonitor + target=host + action=sdt + duration=3000 + starttime='2016-11-10 09:08' + company='{{ company }}' + user='{{ user }}' + password='{{ password }}' + collector='mycompany-Collector' + + #example of putting a host group in SDT + --- + - hosts: localhost + remote_user: '{{ username }}' + vars: + company: 'mycompany' + user: 'myusername' + password: 'mypassword' + tasks: + - name: SDT a host group + # All tasks except for target=collector should use local_action + local_action: > + logicmonitor + target=hostgroup + action=sdt + fullpath='/servers/development' + duration=3000 + starttime='2017-03-04 05:06' + company='{{ company }}' + user='{{ user }}' + password='{{ password }}' + + #example of updating a list of hosts + --- + - hosts: hosts + remote_user: '{{ username }}' + vars: + company: 'mycompany' + user: 'myusername' + password: 'mypassword' + tasks: + - name: Update a list of hosts + # All tasks except for target=collector should use local_action + local_action: > + logicmonitor + target=host + action=update + company='{{ company }}' + user='{{ user }}' + password='{{ password }}' + collector='mycompany-Collector' + groups="/servers/production,/datacenter5" + properties="{'snmp.community':'commstring','dc':'5'}" + + #example of updating a hostgroup + --- + - hosts: hosts + remote_user: '{{ username }}' + vars: + company: 'mycompany' + user: 'myusername' + password: 'mypassword' + tasks: + - name: Update a host group + # All tasks except for target=collector should use local_action + local_action: > + logicmonitor + target=hostgroup + action=update + fullpath='/servers/development' + company='{{ company }}' + user='{{ user }}' + password='{{ password }}' + properties="{'snmp.community':'hg', 'type':'dev', 'status':'test'}" + + #example of removing a list of hosts from monitoring + --- + - hosts: hosts + remote_user: '{{ username }}' + vars: + company: 'mycompany' + user: 'myusername' + password: 'mypassword' + tasks: + - name: Remove LogicMonitor hosts + # All tasks except for target=collector should use local_action + local_action: > + logicmonitor + target=host + action=remove + company='{{ company }}' + user='{{ user }}' + password='{{ password }}' + collector='mycompany-Collector' + + #example of removing a host group + --- + - hosts: hosts + remote_user: '{{ username }}' + vars: + company: 'mycompany' + user: 'myusername' + password: 'mypassword' + tasks: + - name: Remove LogicMonitor development servers hostgroup + # All tasks except for target=collector should use local_action + local_action: > + logicmonitor + target=hostgroup + action=remove + company='{{ company }}' + user='{{ user }}' + password='{{ password }}' + fullpath='/servers/development' + - name: Remove LogicMonitor servers hostgroup + # All tasks except for target=collector should use local_action + local_action: > + logicmonitor + target=hostgroup + action=remove + company='{{ company }}' + user='{{ user }}' + password='{{ password }}' + fullpath='/servers' + - name: Remove LogicMonitor datacenter1 hostgroup + # All tasks except for target=collector should use local_action + local_action: > + logicmonitor + target=hostgroup + action=remove + company='{{ company }}' + user='{{ user }}' + password='{{ password }}' + fullpath='/datacenter1' + - name: Remove LogicMonitor datacenter5 hostgroup + # All tasks except for target=collector should use local_action + local_action: > + logicmonitor + target=hostgroup + action=remove + company='{{ company }}' + user='{{ user }}' + password='{{ password }}' + fullpath='/datacenter5' + + ### example of removing a new LogicMonitor collector to these devices + --- + - hosts: collectors + remote_user: '{{ username }}' + vars: + company: 'mycompany' + user: 'myusername' + password: 'mypassword' + tasks: + - name: Remove LogicMonitor collectors + become: yes + logicmonitor: + target=collector + action=remove + company={{ company }} + user={{ user }} + password={{ password }} + + #complete example + --- + - hosts: localhost + remote_user: '{{ username }}' + vars: + company: 'mycompany' + user: 'myusername' + password: 'mypassword' + tasks: + - name: Create a host group + local_action: > + logicmonitor + target=hostgroup + action=add + fullpath='/servers/production/database' + company='{{ company }}' + user='{{ user }}' + password='{{ password }}' + properties="{'snmp.community':'commstring'}" + - name: SDT a host group + local_action: > + logicmonitor + target=hostgroup + action=sdt + fullpath='/servers/production/web' + duration=3000 + starttime='2012-03-04 05:06' + company='{{ company }}' + user='{{ user }}' + password='{{ password }}' + + - hosts: collectors + remote_user: '{{ username }}' + vars: + company: 'mycompany' + user: 'myusername' + password: 'mypassword' + tasks: + - name: Deploy/verify LogicMonitor collectors + logicmonitor: + target: collector + action: add + company: {{ company }} + user: {{ user }} + password: {{ password }} + - name: Place LogicMonitor collectors into 30 minute Scheduled downtime + logicmonitor: target=collector action=sdt company={{ company }} + user={{ user }} password={{ password }} + - name: Deploy LogicMonitor Host + local_action: > + logicmonitor + target=host + action=add + collector=agent1.ethandev.com + company='{{ company }}' + user='{{ user }}' + password='{{ password }}' + properties="{'snmp.community':'commstring', 'dc':'1'}" + groups="/servers/production/collectors, /datacenter1" + + - hosts: database-servers + remote_user: '{{ username }}' + vars: + company: 'mycompany' + user: 'myusername' + password: 'mypassword' + tasks: + - name: deploy logicmonitor hosts + local_action: > + logicmonitor + target=host + action=add + collector=monitoring.dev.com + company='{{ company }}' + user='{{ user }}' + password='{{ password }}' + properties="{'snmp.community':'commstring', 'type':'db', 'dc':'1'}" + groups="/servers/production/database, /datacenter1" + - name: schedule 5 hour downtime for 2012-11-10 09:08 + local_action: > + logicmonitor + target=host + action=sdt + duration=3000 + starttime='2012-11-10 09:08' + company='{{ company }}' + user='{{ user }}' + password='{{ password }}' +''' + + +class LogicMonitor(object): + + def __init__(self, module, **params): + self.__version__ = "1.0-python" + self.module = module + self.module.debug("Instantiating LogicMonitor object") + + self.check_mode = False + self.company = params["company"] + self.user = params["user"] + self.password = params["password"] + self.fqdn = socket.getfqdn() + self.lm_url = "logicmonitor.com/santaba" + self.urlopen = open_url # use the ansible provided open_url + self.__version__ = self.__version__ + "-ansible-module" + + def rpc(self, action, params): + """Make a call to the LogicMonitor RPC library + and return the response""" + self.module.debug("Running LogicMonitor.rpc") + + param_str = urllib.urlencode(params) + creds = urllib.urlencode( + {"c": self.company, + "u": self.user, + "p": self.password}) + + if param_str: + param_str = param_str + "&" + + param_str = param_str + creds + + try: + url = ("https://" + self.company + "." + self.lm_url + + "/rpc/" + action + "?" + param_str) + + # Set custom LogicMonitor header with version + headers = {"X-LM-User-Agent": self.__version__} + + # Set headers + f = self.urlopen(url, headers=headers) + + raw = f.read() + resp = json.loads(raw) + if resp["status"] == 403: + self.module.debug("Authentication failed.") + self.fail(msg="Error: " + resp["errmsg"]) + else: + return raw + except IOError: + self.fail(msg="Error: Unknown exception making RPC call") + + def do(self, action, params): + """Make a call to the LogicMonitor + server \"do\" function""" + self.module.debug("Running LogicMonitor.do...") + + param_str = urllib.urlencode(params) + creds = (urllib.urlencode( + {"c": self.company, + "u": self.user, + "p": self.password})) + + if param_str: + param_str = param_str + "&" + param_str = param_str + creds + + try: + self.module.debug("Attempting to open URL: " + + "https://" + self.company + "." + self.lm_url + + "/do/" + action + "?" + param_str) + f = self.urlopen( + "https://" + self.company + "." + self.lm_url + + "/do/" + action + "?" + param_str) + return f.read() + except IOError: + # self.module.debug("Error opening URL. " + ioe) + self.fail("Unknown exception opening URL") + + def get_collectors(self): + """Returns a JSON object containing a list of + LogicMonitor collectors""" + self.module.debug("Running LogicMonitor.get_collectors...") + + self.module.debug("Making RPC call to 'getAgents'") + resp = self.rpc("getAgents", {}) + resp_json = json.loads(resp) + + if resp_json["status"] is 200: + self.module.debug("RPC call succeeded") + return resp_json["data"] + else: + self.fail(msg=resp) + + def get_host_by_hostname(self, hostname, collector): + """Returns a host object for the host matching the + specified hostname""" + self.module.debug("Running LogicMonitor.get_host_by_hostname...") + + self.module.debug("Looking for hostname " + hostname) + self.module.debug("Making RPC call to 'getHosts'") + hostlist_json = json.loads(self.rpc("getHosts", {"hostGroupId": 1})) + + if collector: + if hostlist_json["status"] == 200: + self.module.debug("RPC call succeeded") + + hosts = hostlist_json["data"]["hosts"] + + self.module.debug( + "Looking for host matching: hostname " + hostname + + " and collector " + str(collector["id"])) + + for host in hosts: + if (host["hostName"] == hostname and + host["agentId"] == collector["id"]): + + self.module.debug("Host match found") + return host + self.module.debug("No host match found") + return None + else: + self.module.debug("RPC call failed") + self.module.debug(hostlist_json) + else: + self.module.debug("No collector specified") + return None + + def get_host_by_displayname(self, displayname): + """Returns a host object for the host matching the + specified display name""" + self.module.debug("Running LogicMonitor.get_host_by_displayname...") + + self.module.debug("Looking for displayname " + displayname) + self.module.debug("Making RPC call to 'getHost'") + host_json = (json.loads(self.rpc("getHost", + {"displayName": displayname}))) + + if host_json["status"] == 200: + self.module.debug("RPC call succeeded") + return host_json["data"] + else: + self.module.debug("RPC call failed") + self.module.debug(host_json) + return None + + def get_collector_by_description(self, description): + """Returns a JSON collector object for the collector + matching the specified FQDN (description)""" + self.module.debug( + "Running LogicMonitor.get_collector_by_description..." + ) + + collector_list = self.get_collectors() + if collector_list is not None: + self.module.debug("Looking for collector with description {0}" + + description) + for collector in collector_list: + if collector["description"] == description: + self.module.debug("Collector match found") + return collector + self.module.debug("No collector match found") + return None + + def get_group(self, fullpath): + """Returns a JSON group object for the group matching the + specified path""" + self.module.debug("Running LogicMonitor.get_group...") + + self.module.debug("Making RPC call to getHostGroups") + resp = json.loads(self.rpc("getHostGroups", {})) + + if resp["status"] == 200: + self.module.debug("RPC called succeeded") + groups = resp["data"] + + self.module.debug("Looking for group matching " + fullpath) + for group in groups: + if group["fullPath"] == fullpath.lstrip('/'): + self.module.debug("Group match found") + return group + + self.module.debug("No group match found") + return None + else: + self.module.debug("RPC call failed") + self.module.debug(resp) + + return None + + def create_group(self, fullpath): + """Recursively create a path of host groups. + Returns the id of the newly created hostgroup""" + self.module.debug("Running LogicMonitor.create_group...") + + res = self.get_group(fullpath) + if res: + self.module.debug("Group {0} exists." + fullpath) + return res["id"] + + if fullpath == "/": + self.module.debug("Specified group is root. Doing nothing.") + return 1 + else: + self.module.debug("Creating group named " + fullpath) + self.module.debug("System changed") + self.change = True + + if self.check_mode: + self.exit(changed=True) + + parentpath, name = fullpath.rsplit('/', 1) + parentgroup = self.get_group(parentpath) + + parentid = 1 + + if parentpath == "": + parentid = 1 + elif parentgroup: + parentid = parentgroup["id"] + else: + parentid = self.create_group(parentpath) + + h = None + + # Determine if we're creating a group from host or hostgroup class + if hasattr(self, '_build_host_group_hash'): + h = self._build_host_group_hash( + fullpath, + self.description, + self.properties, + self.alertenable) + h["name"] = name + h["parentId"] = parentid + else: + h = {"name": name, + "parentId": parentid, + "alertEnable": True, + "description": ""} + + self.module.debug("Making RPC call to 'addHostGroup'") + resp = json.loads( + self.rpc("addHostGroup", h)) + + if resp["status"] == 200: + self.module.debug("RPC call succeeded") + return resp["data"]["id"] + elif resp["errmsg"] == "The record already exists": + self.module.debug("The hostgroup already exists") + group = self.get_group(fullpath) + return group["id"] + else: + self.module.debug("RPC call failed") + self.fail( + msg="Error: unable to create new hostgroup \"" + + name + "\".\n" + resp["errmsg"]) + + def fail(self, msg): + self.module.fail_json(msg=msg, changed=self.change, failed=True) + + def exit(self, changed): + self.module.debug("Changed: " + changed) + self.module.exit_json(changed=changed, success=True) + + def output_info(self, info): + self.module.debug("Registering properties as Ansible facts") + self.module.exit_json(changed=False, ansible_facts=info) + + +class Collector(LogicMonitor): + + def __init__(self, params, module=None): + """Initializor for the LogicMonitor Collector object""" + self.change = False + self.params = params + + LogicMonitor.__init__(self, module, **params) + self.module.debug("Instantiating Collector object") + + if self.params['description']: + self.description = self.params['description'] + else: + self.description = self.fqdn + + self.info = self._get() + self.installdir = "/usr/local/logicmonitor" + self.platform = platform.system() + self.is_64bits = sys.maxsize > 2**32 + self.duration = self.params['duration'] + self.starttime = self.params['starttime'] + + if self.info is None: + self.id = None + else: + self.id = self.info["id"] + + def create(self): + """Idempotent function to make sure that there is + a running collector installed and registered""" + self.module.debug("Running Collector.create...") + + self._create() + self.get_installer_binary() + self.install() + + def remove(self): + """Idempotent function to make sure that there is + not a running collector installed and registered""" + self.module.debug("Running Collector.destroy...") + + self._unreigster() + self.uninstall() + + def get_installer_binary(self): + """Download the LogicMonitor collector installer binary""" + self.module.debug("Running Collector.get_installer_binary...") + + arch = 32 + + if self.is_64bits: + self.module.debug("64 bit system") + arch = 64 + else: + self.module.debug("32 bit system") + + if self.platform == "Linux" and self.id is not None: + self.module.debug("Platform is Linux") + self.module.debug("Agent ID is " + str(self.id)) + + installfilepath = (self.installdir + + "/logicmonitorsetup" + + str(self.id) + "_" + str(arch) + + ".bin") + + self.module.debug("Looking for existing installer at " + + installfilepath) + if not os.path.isfile(installfilepath): + self.module.debug("No previous installer found") + self.module.debug("System changed") + self.change = True + + if self.check_mode: + self.exit(changed=True) + + self.module.debug("Downloading installer file") + # attempt to create the install dir before download + self.module.run_command("mkdir " + self.installdir) + + try: + f = open(installfilepath, "w") + installer = (self.do("logicmonitorsetup", + {"id": self.id, + "arch": arch})) + f.write(installer) + f.closed + except: + self.fail(msg="Unable to open installer file for writing") + f.closed + else: + self.module.debug("Collector installer already exists") + return installfilepath + + elif self.id is None: + self.fail( + msg="Error: There is currently no collector " + + "associated with this device. To download " + + " the installer, first create a collector " + + "for this device.") + elif self.platform != "Linux": + self.fail( + msg="Error: LogicMonitor Collector must be " + + "installed on a Linux device.") + else: + self.fail( + msg="Error: Unable to retrieve the installer from the server") + + def install(self): + """Execute the LogicMonitor installer if not + already installed""" + self.module.debug("Running Collector.install...") + + if self.platform == "Linux": + self.module.debug("Platform is Linux") + + installer = self.get_installer_binary() + + if self.info is None: + self.module.debug("Retriving collector information") + self.info = self._get() + + if not os.path.exists(self.installdir + "/agent"): + self.module.debug("System changed") + self.change = True + + if self.check_mode: + self.exit(changed=True) + + self.module.debug("Setting installer file permissions") + os.chmod(installer, 484) # decimal for 0o744 + + self.module.debug("Executing installer") + ret_code, out, err = self.module.run_command(installer + " -y") + + if ret_code != 0: + self.fail(msg="Error: Unable to install collector: " + err) + else: + self.module.debug("Collector installed successfully") + else: + self.module.debug("Collector already installed") + else: + self.fail( + msg="Error: LogicMonitor Collector must be " + + "installed on a Linux device") + + def uninstall(self): + """Uninstall LogicMontitor collector from the system""" + self.module.debug("Running Collector.uninstall...") + + uninstallfile = self.installdir + "/agent/bin/uninstall.pl" + + if os.path.isfile(uninstallfile): + self.module.debug("Collector uninstall file exists") + self.module.debug("System changed") + self.change = True + + if self.check_mode: + self.exit(changed=True) + + self.module.debug("Running collector uninstaller") + ret_code, out, err = self.module.run_command(uninstallfile) + + if ret_code != 0: + self.fail( + msg="Error: Unable to uninstall collector: " + err) + else: + self.module.debug("Collector successfully uninstalled") + else: + if os.path.exists(self.installdir + "/agent"): + (self.fail( + msg="Unable to uninstall LogicMonitor " + + "Collector. Can not find LogicMonitor " + + "uninstaller.")) + + def sdt(self): + """Create a scheduled down time + (maintenance window) for this host""" + self.module.debug("Running Collector.sdt...") + + self.module.debug("System changed") + self.change = True + + if self.check_mode: + self.exit(changed=True) + + duration = self.duration + starttime = self.starttime + offsetstart = starttime + + if starttime: + self.module.debug("Start time specified") + start = datetime.datetime.strptime(starttime, '%Y-%m-%d %H:%M') + offsetstart = start + else: + self.module.debug("No start time specified. Using default.") + start = datetime.datetime.utcnow() + + # Use user UTC offset + self.module.debug("Making RPC call to 'getTimeZoneSetting'") + accountresp = json.loads(self.rpc("getTimeZoneSetting", {})) + + if accountresp["status"] == 200: + self.module.debug("RPC call succeeded") + + offset = accountresp["data"]["offset"] + offsetstart = start + datetime.timedelta(0, offset) + else: + self.fail(msg="Error: Unable to retrieve timezone offset") + + offsetend = offsetstart + datetime.timedelta(0, int(duration)*60) + + h = {"agentId": self.id, + "type": 1, + "notifyCC": True, + "year": offsetstart.year, + "month": offsetstart.month-1, + "day": offsetstart.day, + "hour": offsetstart.hour, + "minute": offsetstart.minute, + "endYear": offsetend.year, + "endMonth": offsetend.month-1, + "endDay": offsetend.day, + "endHour": offsetend.hour, + "endMinute": offsetend.minute} + + self.module.debug("Making RPC call to 'setAgentSDT'") + resp = json.loads(self.rpc("setAgentSDT", h)) + + if resp["status"] == 200: + self.module.debug("RPC call succeeded") + return resp["data"] + else: + self.module.debug("RPC call failed") + self.fail(msg=resp["errmsg"]) + + def site_facts(self): + """Output current properties information for the Collector""" + self.module.debug("Running Collector.site_facts...") + + if self.info: + self.module.debug("Collector exists") + props = self.get_properties(True) + + self.output_info(props) + else: + self.fail(msg="Error: Collector doesn't exit.") + + def _get(self): + """Returns a JSON object representing this collector""" + self.module.debug("Running Collector._get...") + collector_list = self.get_collectors() + + if collector_list is not None: + self.module.debug("Collectors returned") + for collector in collector_list: + if collector["description"] == self.description: + return collector + else: + self.module.debug("No collectors returned") + return None + + def _create(self): + """Create a new collector in the associated + LogicMonitor account""" + self.module.debug("Running Collector._create...") + + if self.platform == "Linux": + self.module.debug("Platform is Linux") + ret = self.info or self._get() + + if ret is None: + self.change = True + self.module.debug("System changed") + + if self.check_mode: + self.exit(changed=True) + + h = {"autogen": True, + "description": self.description} + + self.module.debug("Making RPC call to 'addAgent'") + create = (json.loads(self.rpc("addAgent", h))) + + if create["status"] is 200: + self.module.debug("RPC call succeeded") + self.info = create["data"] + self.id = create["data"]["id"] + return create["data"] + else: + self.fail(msg=create["errmsg"]) + else: + self.info = ret + self.id = ret["id"] + return ret + else: + self.fail( + msg="Error: LogicMonitor Collector must be " + + "installed on a Linux device.") + + def _unreigster(self): + """Delete this collector from the associated + LogicMonitor account""" + self.module.debug("Running Collector._unreigster...") + + if self.info is None: + self.module.debug("Retrieving collector information") + self.info = self._get() + + if self.info is not None: + self.module.debug("Collector found") + self.module.debug("System changed") + self.change = True + + if self.check_mode: + self.exit(changed=True) + + self.module.debug("Making RPC call to 'deleteAgent'") + delete = json.loads(self.rpc("deleteAgent", + {"id": self.id})) + + if delete["status"] is 200: + self.module.debug("RPC call succeeded") + return delete + else: + # The collector couldn't unregister. Start the service again + self.module.debug("Error unregistering collecting. " + + delete["errmsg"]) + self.fail(msg=delete["errmsg"]) + else: + self.module.debug("Collector not found") + return None + + +class Host(LogicMonitor): + + def __init__(self, params, module=None): + """Initializor for the LogicMonitor host object""" + self.change = False + self.params = params + self.collector = None + + LogicMonitor.__init__(self, module, **self.params) + self.module.debug("Instantiating Host object") + + if self.params["hostname"]: + self.module.debug("Hostname is " + self.params["hostname"]) + self.hostname = self.params['hostname'] + else: + self.module.debug("No hostname specified. Using " + self.fqdn) + self.hostname = self.fqdn + + if self.params["displayname"]: + self.module.debug("Display name is " + self.params["displayname"]) + self.displayname = self.params['displayname'] + else: + self.module.debug("No display name specified. Using " + self.fqdn) + self.displayname = self.fqdn + + # Attempt to host information via display name of host name + self.module.debug("Attempting to find host by displayname " + + self.displayname) + info = self.get_host_by_displayname(self.displayname) + + if info is not None: + self.module.debug("Host found by displayname") + # Used the host information to grab the collector description + # if not provided + if (not hasattr(self.params, "collector") and + "agentDescription" in info): + self.module.debug("Setting collector from host response. " + + "Collector " + info["agentDescription"]) + self.params["collector"] = info["agentDescription"] + else: + self.module.debug("Host not found by displayname") + + # At this point, a valid collector description is required for success + # Check that the description exists or fail + if self.params["collector"]: + self.module.debug( + "Collector specified is " + + self.params["collector"] + ) + self.collector = (self.get_collector_by_description( + self.params["collector"])) + else: + self.fail(msg="No collector specified.") + + # If the host wasn't found via displayname, attempt by hostname + if info is None: + self.module.debug("Attempting to find host by hostname " + + self.hostname) + info = self.get_host_by_hostname(self.hostname, self.collector) + + self.info = info + self.properties = self.params["properties"] + self.description = self.params["description"] + self.starttime = self.params["starttime"] + self.duration = self.params["duration"] + self.alertenable = self.params["alertenable"] + if self.params["groups"] is not None: + self.groups = self._strip_groups(self.params["groups"]) + else: + self.groups = None + + def create(self): + """Idemopotent function to create if missing, + update if changed, or skip""" + self.module.debug("Running Host.create...") + + self.update() + + def get_properties(self): + """Returns a hash of the properties + associated with this LogicMonitor host""" + self.module.debug("Running Host.get_properties...") + + if self.info: + self.module.debug("Making RPC call to 'getHostProperties'") + properties_json = (json.loads(self.rpc("getHostProperties", + {'hostId': self.info["id"], + "filterSystemProperties": True}))) + + if properties_json["status"] == 200: + self.module.debug("RPC call succeeded") + return properties_json["data"] + else: + self.module.debug("Error: there was an issue retrieving the " + + "host properties") + self.module.debug(properties_json["errmsg"]) + + self.fail(msg=properties_json["status"]) + else: + self.module.debug( + "Unable to find LogicMonitor host which matches " + + self.displayname + " (" + self.hostname + ")" + ) + return None + + def set_properties(self, propertyhash): + """update the host to have the properties + contained in the property hash""" + self.module.debug("Running Host.set_properties...") + self.module.debug("System changed") + self.change = True + + if self.check_mode: + self.exit(changed=True) + + self.module.debug("Assigning property hash to host object") + self.properties = propertyhash + + def add(self): + """Add this device to monitoring + in your LogicMonitor account""" + self.module.debug("Running Host.add...") + + if self.collector and not self.info: + self.module.debug("Host not registered. Registering.") + self.module.debug("System changed") + self.change = True + + if self.check_mode: + self.exit(changed=True) + + h = self._build_host_hash( + self.hostname, + self.displayname, + self.collector, + self.description, + self.groups, + self.properties, + self.alertenable) + + self.module.debug("Making RPC call to 'addHost'") + resp = json.loads(self.rpc("addHost", h)) + + if resp["status"] == 200: + self.module.debug("RPC call succeeded") + return resp["data"] + else: + self.module.debug("RPC call failed") + self.module.debug(resp) + return resp["errmsg"] + elif self.collector is None: + self.fail(msg="Specified collector doesn't exist") + else: + self.module.debug("Host already registered") + + def update(self): + """This method takes changes made to this host + and applies them to the corresponding host + in your LogicMonitor account.""" + self.module.debug("Running Host.update...") + + if self.info: + self.module.debug("Host already registed") + if self.is_changed(): + self.module.debug("System changed") + self.change = True + + if self.check_mode: + self.exit(changed=True) + + h = (self._build_host_hash( + self.hostname, + self.displayname, + self.collector, + self.description, + self.groups, + self.properties, + self.alertenable)) + h["id"] = self.info["id"] + h["opType"] = "replace" + + self.module.debug("Making RPC call to 'updateHost'") + resp = json.loads(self.rpc("updateHost", h)) + + if resp["status"] == 200: + self.module.debug("RPC call succeeded") + else: + self.module.debug("RPC call failed") + self.fail(msg="Error: unable to update the host.") + else: + self.module.debug( + "Host properties match supplied properties. " + + "No changes to make." + ) + return self.info + else: + self.module.debug("Host not registed. Registering") + self.module.debug("System changed") + self.change = True + + if self.check_mode: + self.exit(changed=True) + + return self.add() + + def remove(self): + """Remove this host from your LogicMonitor account""" + self.module.debug("Running Host.remove...") + + if self.info: + self.module.debug("Host registered") + self.module.debug("System changed") + self.change = True + + if self.check_mode: + self.exit(changed=True) + + self.module.debug("Making RPC call to 'deleteHost'") + resp = json.loads(self.rpc("deleteHost", + {"hostId": self.info["id"], + "deleteFromSystem": True, + "hostGroupId": 1})) + + if resp["status"] == 200: + self.module.debug(resp) + self.module.debug("RPC call succeeded") + return resp + else: + self.module.debug("RPC call failed") + self.module.debug(resp) + self.fail(msg=resp["errmsg"]) + + else: + self.module.debug("Host not registered") + + def is_changed(self): + """Return true if the host doesn't + match the LogicMonitor account""" + self.module.debug("Running Host.is_changed") + + ignore = ['system.categories', 'snmp.version'] + + hostresp = self.get_host_by_displayname(self.displayname) + + if hostresp is None: + hostresp = self.get_host_by_hostname(self.hostname, self.collector) + + if hostresp: + self.module.debug("Comparing simple host properties") + if hostresp["alertEnable"] != self.alertenable: + return True + + if hostresp["description"] != self.description: + return True + + if hostresp["displayedAs"] != self.displayname: + return True + + if (self.collector and + hasattr(self.collector, "id") and + hostresp["agentId"] != self.collector["id"]): + return True + + self.module.debug("Comparing groups.") + if self._compare_groups(hostresp) is True: + return True + + propresp = self.get_properties() + + if propresp: + self.module.debug("Comparing properties.") + if self._compare_props(propresp, ignore) is True: + return True + else: + self.fail( + msg="Error: Unknown error retrieving host properties") + + return False + else: + self.fail(msg="Error: Unknown error retrieving host information") + + def sdt(self): + """Create a scheduled down time + (maintenance window) for this host""" + self.module.debug("Running Host.sdt...") + if self.info: + self.module.debug("System changed") + self.change = True + + if self.check_mode: + self.exit(changed=True) + + duration = self.duration + starttime = self.starttime + offset = starttime + + if starttime: + self.module.debug("Start time specified") + start = datetime.datetime.strptime(starttime, '%Y-%m-%d %H:%M') + offsetstart = start + else: + self.module.debug("No start time specified. Using default.") + start = datetime.datetime.utcnow() + + # Use user UTC offset + self.module.debug("Making RPC call to 'getTimeZoneSetting'") + accountresp = (json.loads(self.rpc("getTimeZoneSetting", {}))) + + if accountresp["status"] == 200: + self.module.debug("RPC call succeeded") + + offset = accountresp["data"]["offset"] + offsetstart = start + datetime.timedelta(0, offset) + else: + self.fail( + msg="Error: Unable to retrieve timezone offset") + + offsetend = offsetstart + datetime.timedelta(0, int(duration)*60) + + h = {"hostId": self.info["id"], + "type": 1, + "year": offsetstart.year, + "month": offsetstart.month - 1, + "day": offsetstart.day, + "hour": offsetstart.hour, + "minute": offsetstart.minute, + "endYear": offsetend.year, + "endMonth": offsetend.month - 1, + "endDay": offsetend.day, + "endHour": offsetend.hour, + "endMinute": offsetend.minute} + + self.module.debug("Making RPC call to 'setHostSDT'") + resp = (json.loads(self.rpc("setHostSDT", h))) + + if resp["status"] == 200: + self.module.debug("RPC call succeeded") + return resp["data"] + else: + self.module.debug("RPC call failed") + self.fail(msg=resp["errmsg"]) + else: + self.fail(msg="Error: Host doesn't exit.") + + def site_facts(self): + """Output current properties information for the Host""" + self.module.debug("Running Host.site_facts...") + + if self.info: + self.module.debug("Host exists") + props = self.get_properties() + + self.output_info(props) + else: + self.fail(msg="Error: Host doesn't exit.") + + def _build_host_hash(self, + hostname, + displayname, + collector, + description, + groups, + properties, + alertenable): + """Return a property formated hash for the + creation of a host using the rpc function""" + self.module.debug("Running Host._build_host_hash...") + + h = {} + h["hostName"] = hostname + h["displayedAs"] = displayname + h["alertEnable"] = alertenable + + if collector: + self.module.debug("Collector property exists") + h["agentId"] = collector["id"] + else: + self.fail( + msg="Error: No collector found. Unable to build host hash.") + + if description: + h["description"] = description + + if groups is not None and groups is not []: + self.module.debug("Group property exists") + groupids = "" + + for group in groups: + groupids = groupids + str(self.create_group(group)) + "," + + h["hostGroupIds"] = groupids.rstrip(',') + + if properties is not None and properties is not {}: + self.module.debug("Properties hash exists") + propnum = 0 + for key, value in properties.iteritems(): + h["propName" + str(propnum)] = key + h["propValue" + str(propnum)] = value + propnum = propnum + 1 + + return h + + def _verify_property(self, propname): + """Check with LogicMonitor server to + verify property is unchanged""" + self.module.debug("Running Host._verify_property...") + + if self.info: + self.module.debug("Host is registered") + if propname not in self.properties: + self.module.debug("Property " + propname + " does not exist") + return False + else: + self.module.debug("Property " + propname + " exists") + h = {"hostId": self.info["id"], + "propName0": propname, + "propValue0": self.properties[propname]} + + self.module.debug("Making RCP call to 'verifyProperties'") + resp = json.loads(self.rpc('verifyProperties', h)) + + if resp["status"] == 200: + self.module.debug("RPC call succeeded") + return resp["data"]["match"] + else: + self.fail( + msg="Error: unable to get verification " + + "from server.\n%s" % resp["errmsg"]) + else: + self.fail( + msg="Error: Host doesn't exist. Unable to verify properties") + + def _compare_groups(self, hostresp): + """Function to compare the host's current + groups against provided groups""" + self.module.debug("Running Host._compare_groups") + + g = [] + fullpathinids = hostresp["fullPathInIds"] + self.module.debug("Building list of groups") + for path in fullpathinids: + if path != []: + h = {'hostGroupId': path[-1]} + + hgresp = json.loads(self.rpc("getHostGroup", h)) + + if (hgresp["status"] == 200 and + hgresp["data"]["appliesTo"] == ""): + + g.append(path[-1]) + + if self.groups is not None: + self.module.debug("Comparing group lists") + for group in self.groups: + groupjson = self.get_group(group) + + if groupjson is None: + self.module.debug("Group mismatch. No result.") + return True + elif groupjson['id'] not in g: + self.module.debug("Group mismatch. ID doesn't exist.") + return True + else: + g.remove(groupjson['id']) + + if g != []: + self.module.debug("Group mismatch. New ID exists.") + return True + self.module.debug("Groups match") + + def _compare_props(self, propresp, ignore): + """Function to compare the host's current + properties against provided properties""" + self.module.debug("Running Host._compare_props...") + p = {} + + self.module.debug("Creating list of properties") + for prop in propresp: + if prop["name"] not in ignore: + if ("*******" in prop["value"] and + self._verify_property(prop["name"])): + p[prop["name"]] = self.properties[prop["name"]] + else: + p[prop["name"]] = prop["value"] + + self.module.debug("Comparing properties") + # Iterate provided properties and compare to received properties + for prop in self.properties: + if (prop not in p or + p[prop] != self.properties[prop]): + self.module.debug("Properties mismatch") + return True + self.module.debug("Properties match") + + def _strip_groups(self, groups): + """Function to strip whitespace from group list. + This function provides the user some flexibility when + formatting group arguments """ + self.module.debug("Running Host._strip_groups...") + return map(lambda x: x.strip(), groups) + + +class Datasource(LogicMonitor): + + def __init__(self, params, module=None): + """Initializor for the LogicMonitor Datasource object""" + self.change = False + self.params = params + + LogicMonitor.__init__(self, module, **params) + self.module.debug("Instantiating Datasource object") + + self.id = self.params["id"] + self.starttime = self.params["starttime"] + self.duration = self.params["duration"] + + def sdt(self): + """Create a scheduled down time + (maintenance window) for this host""" + self.module.debug("Running Datasource.sdt...") + + self.module.debug("System changed") + self.change = True + + if self.check_mode: + self.exit(changed=True) + + duration = self.duration + starttime = self.starttime + offsetstart = starttime + + if starttime: + self.module.debug("Start time specified") + start = datetime.datetime.strptime(starttime, '%Y-%m-%d %H:%M') + offsetstart = start + else: + self.module.debug("No start time specified. Using default.") + start = datetime.datetime.utcnow() + + # Use user UTC offset + self.module.debug("Making RPC call to 'getTimeZoneSetting'") + accountresp = json.loads(self.rpc("getTimeZoneSetting", {})) + + if accountresp["status"] == 200: + self.module.debug("RPC call succeeded") + + offset = accountresp["data"]["offset"] + offsetstart = start + datetime.timedelta(0, offset) + else: + self.fail(msg="Error: Unable to retrieve timezone offset") + + offsetend = offsetstart + datetime.timedelta(0, int(duration)*60) + + h = {"hostDataSourceId": self.id, + "type": 1, + "notifyCC": True, + "year": offsetstart.year, + "month": offsetstart.month-1, + "day": offsetstart.day, + "hour": offsetstart.hour, + "minute": offsetstart.minute, + "endYear": offsetend.year, + "endMonth": offsetend.month-1, + "endDay": offsetend.day, + "endHour": offsetend.hour, + "endMinute": offsetend.minute} + + self.module.debug("Making RPC call to 'setHostDataSourceSDT'") + resp = json.loads(self.rpc("setHostDataSourceSDT", h)) + + if resp["status"] == 200: + self.module.debug("RPC call succeeded") + return resp["data"] + else: + self.module.debug("RPC call failed") + self.fail(msg=resp["errmsg"]) + + +class Hostgroup(LogicMonitor): + + def __init__(self, params, module=None): + """Initializor for the LogicMonitor host object""" + self.change = False + self.params = params + + LogicMonitor.__init__(self, module, **self.params) + self.module.debug("Instantiating Hostgroup object") + + self.fullpath = self.params["fullpath"] + self.info = self.get_group(self.fullpath) + self.properties = self.params["properties"] + self.description = self.params["description"] + self.starttime = self.params["starttime"] + self.duration = self.params["duration"] + self.alertenable = self.params["alertenable"] + + def create(self): + """Wrapper for self.update()""" + self.module.debug("Running Hostgroup.create...") + self.update() + + def get_properties(self, final=False): + """Returns a hash of the properties + associated with this LogicMonitor host""" + self.module.debug("Running Hostgroup.get_properties...") + + if self.info: + self.module.debug("Group found") + + self.module.debug("Making RPC call to 'getHostGroupProperties'") + properties_json = json.loads(self.rpc( + "getHostGroupProperties", + {'hostGroupId': self.info["id"], + "finalResult": final})) + + if properties_json["status"] == 200: + self.module.debug("RPC call succeeded") + return properties_json["data"] + else: + self.module.debug("RPC call failed") + self.fail(msg=properties_json["status"]) + else: + self.module.debug("Group not found") + return None + + def set_properties(self, propertyhash): + """Update the host to have the properties + contained in the property hash""" + self.module.debug("Running Hostgroup.set_properties") + + self.module.debug("System changed") + self.change = True + + if self.check_mode: + self.exit(changed=True) + + self.module.debug("Assigning property has to host object") + self.properties = propertyhash + + def add(self): + """Idempotent function to ensure that the host + group exists in your LogicMonitor account""" + self.module.debug("Running Hostgroup.add") + + if self.info is None: + self.module.debug("Group doesn't exist. Creating.") + self.module.debug("System changed") + self.change = True + + if self.check_mode: + self.exit(changed=True) + + self.create_group(self.fullpath) + self.info = self.get_group(self.fullpath) + + self.module.debug("Group created") + return self.info + else: + self.module.debug("Group already exists") + + def update(self): + """Idempotent function to ensure the host group settings + (alertenable, properties, etc) in the + LogicMonitor account match the current object.""" + self.module.debug("Running Hostgroup.update") + + if self.info: + if self.is_changed(): + self.module.debug("System changed") + self.change = True + + if self.check_mode: + self.exit(changed=True) + + h = self._build_host_group_hash( + self.fullpath, + self.description, + self.properties, + self.alertenable) + h["opType"] = "replace" + + if self.fullpath != "/": + h["id"] = self.info["id"] + + self.module.debug("Making RPC call to 'updateHostGroup'") + resp = json.loads(self.rpc("updateHostGroup", h)) + + if resp["status"] == 200: + self.module.debug("RPC call succeeded") + return resp["data"] + else: + self.module.debug("RPC call failed") + self.fail(msg="Error: Unable to update the " + + "host.\n" + resp["errmsg"]) + else: + self.module.debug( + "Group properties match supplied properties. " + + "No changes to make" + ) + return self.info + else: + self.module.debug("Group doesn't exist. Creating.") + + self.module.debug("System changed") + self.change = True + + if self.check_mode: + self.exit(changed=True) + + return self.add() + + def remove(self): + """Idempotent function to ensure the host group + does not exist in your LogicMonitor account""" + self.module.debug("Running Hostgroup.remove...") + + if self.info: + self.module.debug("Group exists") + self.module.debug("System changed") + self.change = True + + if self.check_mode: + self.exit(changed=True) + + self.module.debug("Making RPC call to 'deleteHostGroup'") + resp = json.loads(self.rpc("deleteHostGroup", + {"hgId": self.info["id"]})) + + if resp["status"] == 200: + self.module.debug(resp) + self.module.debug("RPC call succeeded") + return resp + elif resp["errmsg"] == "No such group": + self.module.debug("Group doesn't exist") + else: + self.module.debug("RPC call failed") + self.module.debug(resp) + self.fail(msg=resp["errmsg"]) + else: + self.module.debug("Group doesn't exist") + + def is_changed(self): + """Return true if the host doesn't match + the LogicMonitor account""" + self.module.debug("Running Hostgroup.is_changed...") + + ignore = [] + group = self.get_group(self.fullpath) + properties = self.get_properties() + + if properties is not None and group is not None: + self.module.debug("Comparing simple group properties") + if (group["alertEnable"] != self.alertenable or + group["description"] != self.description): + + return True + + p = {} + + self.module.debug("Creating list of properties") + for prop in properties: + if prop["name"] not in ignore: + if ("*******" in prop["value"] and + self._verify_property(prop["name"])): + + p[prop["name"]] = ( + self.properties[prop["name"]]) + else: + p[prop["name"]] = prop["value"] + + self.module.debug("Comparing properties") + if set(p) != set(self.properties): + return True + else: + self.module.debug("No property information received") + return False + + def sdt(self, duration=30, starttime=None): + """Create a scheduled down time + (maintenance window) for this host""" + self.module.debug("Running Hostgroup.sdt") + + self.module.debug("System changed") + self.change = True + + if self.check_mode: + self.exit(changed=True) + + duration = self.duration + starttime = self.starttime + offset = starttime + + if starttime: + self.module.debug("Start time specified") + start = datetime.datetime.strptime(starttime, '%Y-%m-%d %H:%M') + offsetstart = start + else: + self.module.debug("No start time specified. Using default.") + start = datetime.datetime.utcnow() + + # Use user UTC offset + self.module.debug("Making RPC call to 'getTimeZoneSetting'") + accountresp = json.loads(self.rpc("getTimeZoneSetting", {})) + + if accountresp["status"] == 200: + self.module.debug("RPC call succeeded") + + offset = accountresp["data"]["offset"] + offsetstart = start + datetime.timedelta(0, offset) + else: + self.fail( + msg="Error: Unable to retrieve timezone offset") + + offsetend = offsetstart + datetime.timedelta(0, int(duration)*60) + + h = {"hostGroupId": self.info["id"], + "type": 1, + "year": offsetstart.year, + "month": offsetstart.month-1, + "day": offsetstart.day, + "hour": offsetstart.hour, + "minute": offsetstart.minute, + "endYear": offsetend.year, + "endMonth": offsetend.month-1, + "endDay": offsetend.day, + "endHour": offsetend.hour, + "endMinute": offsetend.minute} + + self.module.debug("Making RPC call to setHostGroupSDT") + resp = json.loads(self.rpc("setHostGroupSDT", h)) + + if resp["status"] == 200: + self.module.debug("RPC call succeeded") + return resp["data"] + else: + self.module.debug("RPC call failed") + self.fail(msg=resp["errmsg"]) + + def site_facts(self): + """Output current properties information for the Hostgroup""" + self.module.debug("Running Hostgroup.site_facts...") + + if self.info: + self.module.debug("Group exists") + props = self.get_properties(True) + + self.output_info(props) + else: + self.fail(msg="Error: Group doesn't exit.") + + def _build_host_group_hash(self, + fullpath, + description, + properties, + alertenable): + """Return a property formated hash for the + creation of a hostgroup using the rpc function""" + self.module.debug("Running Hostgroup._build_host_hash") + + h = {} + h["alertEnable"] = alertenable + + if fullpath == "/": + self.module.debug("Group is root") + h["id"] = 1 + else: + self.module.debug("Determining group path") + parentpath, name = fullpath.rsplit('/', 1) + parent = self.get_group(parentpath) + + h["name"] = name + + if parent: + self.module.debug("Parent group " + + str(parent["id"]) + " found.") + h["parentID"] = parent["id"] + else: + self.module.debug("No parent group found. Using root.") + h["parentID"] = 1 + + if description: + self.module.debug("Description property exists") + h["description"] = description + + if properties != {}: + self.module.debug("Properties hash exists") + propnum = 0 + for key, value in properties.iteritems(): + h["propName" + str(propnum)] = key + h["propValue" + str(propnum)] = value + propnum = propnum + 1 + + return h + + def _verify_property(self, propname): + """Check with LogicMonitor server + to verify property is unchanged""" + self.module.debug("Running Hostgroup._verify_property") + + if self.info: + self.module.debug("Group exists") + if propname not in self.properties: + self.module.debug("Property " + propname + " does not exist") + return False + else: + self.module.debug("Property " + propname + " exists") + h = {"hostGroupId": self.info["id"], + "propName0": propname, + "propValue0": self.properties[propname]} + + self.module.debug("Making RCP call to 'verifyProperties'") + resp = json.loads(self.rpc('verifyProperties', h)) + + if resp["status"] == 200: + self.module.debug("RPC call succeeded") + return resp["data"]["match"] + else: + self.fail( + msg="Error: unable to get verification " + + "from server.\n%s" % resp["errmsg"]) + else: + self.fail( + msg="Error: Group doesn't exist. Unable to verify properties") + + +def selector(module): + """Figure out which object and which actions + to take given the right parameters""" + + if module.params["target"] == "collector": + target = Collector(module.params, module) + elif module.params["target"] == "host": + # Make sure required parameter collector is specified + if ((module.params["action"] == "add" or + module.params["displayname"] is None) and + module.params["collector"] is None): + module.fail_json( + msg="Parameter 'collector' required.") + + target = Host(module.params, module) + elif module.params["target"] == "datasource": + # Validate target specific required parameters + if module.params["id"] is not None: + # make sure a supported action was specified + if module.params["action"] == "sdt": + target = Datasource(module.params, module) + else: + errmsg = ("Error: Unexpected action \"" + + module.params["action"] + "\" was specified.") + module.fail_json(msg=errmsg) + + elif module.params["target"] == "hostgroup": + # Validate target specific required parameters + if module.params["fullpath"] is not None: + target = Hostgroup(module.params, module) + else: + module.fail_json( + msg="Parameter 'fullpath' required for target 'hostgroup'") + else: + module.fail_json( + msg="Error: Unexpected target \"" + module.params["target"] + + "\" was specified.") + + if module.params["action"].lower() == "add": + action = target.create + elif module.params["action"].lower() == "remove": + action = target.remove + elif module.params["action"].lower() == "sdt": + action = target.sdt + elif module.params["action"].lower() == "update": + action = target.update + else: + errmsg = ("Error: Unexpected action \"" + module.params["action"] + + "\" was specified.") + module.fail_json(msg=errmsg) + + action() + module.exit_json(changed=target.change) + + +def main(): + TARGETS = [ + "collector", + "host", + "datasource", + "hostgroup"] + + ACTIONS = [ + "add", + "remove", + "sdt", + "update"] + + module = AnsibleModule( + argument_spec=dict( + target=dict(required=True, default=None, choices=TARGETS), + action=dict(required=True, default=None, choices=ACTIONS), + company=dict(required=True, default=None), + user=dict(required=True, default=None), + password=dict(required=True, default=None, no_log=True), + + collector=dict(required=False, default=None), + hostname=dict(required=False, default=None), + displayname=dict(required=False, default=None), + id=dict(required=False, default=None), + description=dict(required=False, default=""), + fullpath=dict(required=False, default=None), + starttime=dict(required=False, default=None), + duration=dict(required=False, default=30), + properties=dict(required=False, default={}, type="dict"), + groups=dict(required=False, default=[], type="list"), + alertenable=dict(required=False, default="true", choices=BOOLEANS) + ), + supports_check_mode=True + ) + + if HAS_LIB_JSON is not True: + module.fail_json(msg="Unable to load JSON library") + + selector(module) + + +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * +from ansible.module_utils.urls import open_url + + +if __name__ == "__main__": + main() diff --git a/monitoring/logicmonitor_facts.py b/monitoring/logicmonitor_facts.py new file mode 100644 index 00000000000..7e9bf00cb5a --- /dev/null +++ b/monitoring/logicmonitor_facts.py @@ -0,0 +1,633 @@ +#!/usr/bin/python + +"""LogicMonitor Ansible module for managing Collectors, Hosts and Hostgroups + Copyright (C) 2015 LogicMonitor + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA""" + + +import socket +import sys +import types +import urllib + +HAS_LIB_JSON = True +try: + import json + # Detect the python-json library which is incompatible + # Look for simplejson if that's the case + try: + if ( + not isinstance(json.loads, types.FunctionType) or + not isinstance(json.dumps, types.FunctionType) + ): + raise ImportError + except AttributeError: + raise ImportError +except ImportError: + try: + import simplejson as json + except ImportError: + print( + '\n{"msg": "Error: ansible requires the stdlib json or ' + + 'simplejson module, neither was found!", "failed": true}' + ) + HAS_LIB_JSON = False + except SyntaxError: + print( + '\n{"msg": "SyntaxError: probably due to installed simplejson ' + + 'being for a different python version", "failed": true}' + ) + HAS_LIB_JSON = False + + +DOCUMENTATION = ''' +--- +module: logicmonitor_facts +short_description: Collect facts about LogicMonitor objects +description: + - LogicMonitor is a hosted, full-stack, infrastructure monitoring platform. + - This module collects facts about hosts abd host groups within your LogicMonitor account. +version_added: "2.2" +author: Ethan Culler-Mayeno, Jeff Wozniak +notes: + - You must have an existing LogicMonitor account for this module to function. +requirements: ["An existing LogicMonitor account", "Linux"] +options: + target: + description: + - The LogicMonitor object you wish to manage. + required: true + default: null + choices: ['host', 'hostgroup'] + company: + description: + - The LogicMonitor account company name. If you would log in to your account at "superheroes.logicmonitor.com" you would use "superheroes" + required: true + default: null + user: + description: + - A LogicMonitor user name. The module will authenticate and perform actions on behalf of this user + required: true + default: null + password: + description: + - The password for the chosen LogicMonitor User + - If an md5 hash is used, the digest flag must be set to true + required: true + default: null + collector: + description: + - The fully qualified domain name of a collector in your LogicMonitor account. + - This is optional for querying a LogicMonitor host when a displayname is specified + - This is required for querying a LogicMonitor host when a displayname is not specified + required: false + default: null + hostname: + description: + - The hostname of a host in your LogicMonitor account, or the desired hostname of a device to add into monitoring. + - Required for managing hosts (target=host) + required: false + default: 'hostname -f' + displayname: + description: + - The display name of a host in your LogicMonitor account or the desired display name of a device to add into monitoring. + required: false + default: 'hostname -f' + fullpath: + description: + - The fullpath of the hostgroup object you would like to manage + - Recommend running on a single ansible host + - Required for management of LogicMonitor host groups (target=hostgroup) + required: false + default: null +... +''' + +EXAMPLES = ''' +#example of querying a list of hosts +``` +--- +- hosts: hosts + user: root + vars: + company: 'yourcompany' + user: 'Luigi' + password: 'ImaLuigi,number1!' + tasks: + - name: query a list of hosts + # All tasks should use local_action + local_action: + logicmonitor_facts: + target: host + company: '{{ company }}' + user: '{{ user }}' + password: '{{ password }}' +``` + +#example of querying a hostgroup +``` +--- +- hosts: somemachine.superheroes.com + user: root + vars: + company: 'yourcompany' + user: 'mario' + password: 'itsame.Mario!' + tasks: + - name: query a host group + # All tasks should use local_action + local_action: + logicmonitor_facts: + target: hostgroup + fullpath: '/servers/production' + company: '{{ company }}' + user: '{{ user }}' + password: '{{ password }}' +``` +''' + + +RETURN = ''' +--- + ansible_facts: + description: LogicMonitor properties set for the specified object + returned: success + type: list of dicts containing name/value pairs + example: > + { + "name": "dc", + "value": "1" + }, + { + "name": "type", + "value": "prod" + }, + { + "name": "system.categories", + "value": "" + }, + { + "name": "snmp.community", + "value": "********" + } +... +''' + + +class LogicMonitor(object): + + def __init__(self, module, **params): + self.__version__ = "1.0-python" + self.module = module + self.module.debug("Instantiating LogicMonitor object") + + self.check_mode = False + self.company = params["company"] + self.user = params["user"] + self.password = params["password"] + self.fqdn = socket.getfqdn() + self.lm_url = "logicmonitor.com/santaba" + self.urlopen = open_url # use the ansible provided open_url + self.__version__ = self.__version__ + "-ansible-module" + + def rpc(self, action, params): + """Make a call to the LogicMonitor RPC library + and return the response""" + self.module.debug("Running LogicMonitor.rpc") + + param_str = urllib.urlencode(params) + creds = urllib.urlencode( + {"c": self.company, + "u": self.user, + "p": self.password}) + + if param_str: + param_str = param_str + "&" + + param_str = param_str + creds + + try: + url = ("https://" + self.company + "." + self.lm_url + + "/rpc/" + action + "?" + param_str) + + # Set custom LogicMonitor header with version + headers = {"X-LM-User-Agent": self.__version__} + + # Set headers + f = self.urlopen(url, headers=headers) + + raw = f.read() + resp = json.loads(raw) + if resp["status"] == 403: + self.module.debug("Authentication failed.") + self.fail(msg="Error: " + resp["errmsg"]) + else: + return raw + except IOError: + self.fail(msg="Error: Unknown exception making RPC call") + + def get_collectors(self): + """Returns a JSON object containing a list of + LogicMonitor collectors""" + self.module.debug("Running LogicMonitor.get_collectors...") + + self.module.debug("Making RPC call to 'getAgents'") + resp = self.rpc("getAgents", {}) + resp_json = json.loads(resp) + + if resp_json["status"] is 200: + self.module.debug("RPC call succeeded") + return resp_json["data"] + else: + self.fail(msg=resp) + + def get_host_by_hostname(self, hostname, collector): + """Returns a host object for the host matching the + specified hostname""" + self.module.debug("Running LogicMonitor.get_host_by_hostname...") + + self.module.debug("Looking for hostname " + hostname) + self.module.debug("Making RPC call to 'getHosts'") + hostlist_json = json.loads(self.rpc("getHosts", {"hostGroupId": 1})) + + if collector: + if hostlist_json["status"] == 200: + self.module.debug("RPC call succeeded") + + hosts = hostlist_json["data"]["hosts"] + + self.module.debug( + "Looking for host matching: hostname " + hostname + + " and collector " + str(collector["id"])) + + for host in hosts: + if (host["hostName"] == hostname and + host["agentId"] == collector["id"]): + + self.module.debug("Host match found") + return host + self.module.debug("No host match found") + return None + else: + self.module.debug("RPC call failed") + self.module.debug(hostlist_json) + else: + self.module.debug("No collector specified") + return None + + def get_host_by_displayname(self, displayname): + """Returns a host object for the host matching the + specified display name""" + self.module.debug("Running LogicMonitor.get_host_by_displayname...") + + self.module.debug("Looking for displayname " + displayname) + self.module.debug("Making RPC call to 'getHost'") + host_json = (json.loads(self.rpc("getHost", + {"displayName": displayname}))) + + if host_json["status"] == 200: + self.module.debug("RPC call succeeded") + return host_json["data"] + else: + self.module.debug("RPC call failed") + self.module.debug(host_json) + return None + + def get_collector_by_description(self, description): + """Returns a JSON collector object for the collector + matching the specified FQDN (description)""" + self.module.debug( + "Running LogicMonitor.get_collector_by_description..." + ) + + collector_list = self.get_collectors() + if collector_list is not None: + self.module.debug("Looking for collector with description " + + description) + for collector in collector_list: + if collector["description"] == description: + self.module.debug("Collector match found") + return collector + self.module.debug("No collector match found") + return None + + def get_group(self, fullpath): + """Returns a JSON group object for the group matching the + specified path""" + self.module.debug("Running LogicMonitor.get_group...") + + self.module.debug("Making RPC call to getHostGroups") + resp = json.loads(self.rpc("getHostGroups", {})) + + if resp["status"] == 200: + self.module.debug("RPC called succeeded") + groups = resp["data"] + + self.module.debug("Looking for group matching " + fullpath) + for group in groups: + if group["fullPath"] == fullpath.lstrip('/'): + self.module.debug("Group match found") + return group + + self.module.debug("No group match found") + return None + else: + self.module.debug("RPC call failed") + self.module.debug(resp) + + return None + + def create_group(self, fullpath): + """Recursively create a path of host groups. + Returns the id of the newly created hostgroup""" + self.module.debug("Running LogicMonitor.create_group...") + + res = self.get_group(fullpath) + if res: + self.module.debug("Group " + fullpath + " exists.") + return res["id"] + + if fullpath == "/": + self.module.debug("Specified group is root. Doing nothing.") + return 1 + else: + self.module.debug("Creating group named " + fullpath) + self.module.debug("System changed") + self.change = True + + if self.check_mode: + self.exit(changed=True) + + parentpath, name = fullpath.rsplit('/', 1) + parentgroup = self.get_group(parentpath) + + parentid = 1 + + if parentpath == "": + parentid = 1 + elif parentgroup: + parentid = parentgroup["id"] + else: + parentid = self.create_group(parentpath) + + h = None + + # Determine if we're creating a group from host or hostgroup class + if hasattr(self, '_build_host_group_hash'): + h = self._build_host_group_hash( + fullpath, + self.description, + self.properties, + self.alertenable) + h["name"] = name + h["parentId"] = parentid + else: + h = {"name": name, + "parentId": parentid, + "alertEnable": True, + "description": ""} + + self.module.debug("Making RPC call to 'addHostGroup'") + resp = json.loads( + self.rpc("addHostGroup", h)) + + if resp["status"] == 200: + self.module.debug("RPC call succeeded") + return resp["data"]["id"] + elif resp["errmsg"] == "The record already exists": + self.module.debug("The hostgroup already exists") + group = self.get_group(fullpath) + return group["id"] + else: + self.module.debug("RPC call failed") + self.fail( + msg="Error: unable to create new hostgroup \"" + name + + "\".\n" + resp["errmsg"]) + + def fail(self, msg): + self.module.fail_json(msg=msg, changed=self.change) + + def exit(self, changed): + self.module.debug("Changed: " + changed) + self.module.exit_json(changed=changed) + + def output_info(self, info): + self.module.debug("Registering properties as Ansible facts") + self.module.exit_json(changed=False, ansible_facts=info) + + +class Host(LogicMonitor): + + def __init__(self, params, module=None): + """Initializor for the LogicMonitor host object""" + self.change = False + self.params = params + self.collector = None + + LogicMonitor.__init__(self, module, **self.params) + self.module.debug("Instantiating Host object") + + if self.params["hostname"]: + self.module.debug("Hostname is " + self.params["hostname"]) + self.hostname = self.params['hostname'] + else: + self.module.debug("No hostname specified. Using " + self.fqdn) + self.hostname = self.fqdn + + if self.params["displayname"]: + self.module.debug("Display name is " + self.params["displayname"]) + self.displayname = self.params['displayname'] + else: + self.module.debug("No display name specified. Using " + self.fqdn) + self.displayname = self.fqdn + + # Attempt to host information via display name of host name + self.module.debug("Attempting to find host by displayname " + + self.displayname) + info = self.get_host_by_displayname(self.displayname) + + if info is not None: + self.module.debug("Host found by displayname") + # Used the host information to grab the collector description + # if not provided + if (not hasattr(self.params, "collector") and + "agentDescription" in info): + self.module.debug("Setting collector from host response. " + + "Collector " + info["agentDescription"]) + self.params["collector"] = info["agentDescription"] + else: + self.module.debug("Host not found by displayname") + + # At this point, a valid collector description is required for success + # Check that the description exists or fail + if self.params["collector"]: + self.module.debug("Collector specified is " + + self.params["collector"]) + self.collector = (self.get_collector_by_description( + self.params["collector"])) + else: + self.fail(msg="No collector specified.") + + # If the host wasn't found via displayname, attempt by hostname + if info is None: + self.module.debug("Attempting to find host by hostname " + + self.hostname) + info = self.get_host_by_hostname(self.hostname, self.collector) + + self.info = info + + def get_properties(self): + """Returns a hash of the properties + associated with this LogicMonitor host""" + self.module.debug("Running Host.get_properties...") + + if self.info: + self.module.debug("Making RPC call to 'getHostProperties'") + properties_json = (json.loads(self.rpc("getHostProperties", + {'hostId': self.info["id"], + "filterSystemProperties": True}))) + + if properties_json["status"] == 200: + self.module.debug("RPC call succeeded") + return properties_json["data"] + else: + self.module.debug("Error: there was an issue retrieving the " + + "host properties") + self.module.debug(properties_json["errmsg"]) + + self.fail(msg=properties_json["status"]) + else: + self.module.debug( + "Unable to find LogicMonitor host which matches " + + self.displayname + " (" + self.hostname + ")" + ) + return None + + def site_facts(self): + """Output current properties information for the Host""" + self.module.debug("Running Host.site_facts...") + + if self.info: + self.module.debug("Host exists") + props = self.get_properties() + + self.output_info(props) + else: + self.fail(msg="Error: Host doesn't exit.") + + +class Hostgroup(LogicMonitor): + + def __init__(self, params, module=None): + """Initializor for the LogicMonitor host object""" + self.change = False + self.params = params + + LogicMonitor.__init__(self, module, **self.params) + self.module.debug("Instantiating Hostgroup object") + + self.fullpath = self.params["fullpath"] + self.info = self.get_group(self.fullpath) + + def get_properties(self, final=False): + """Returns a hash of the properties + associated with this LogicMonitor host""" + self.module.debug("Running Hostgroup.get_properties...") + + if self.info: + self.module.debug("Group found") + + self.module.debug("Making RPC call to 'getHostGroupProperties'") + properties_json = json.loads(self.rpc( + "getHostGroupProperties", + {'hostGroupId': self.info["id"], + "finalResult": final})) + + if properties_json["status"] == 200: + self.module.debug("RPC call succeeded") + return properties_json["data"] + else: + self.module.debug("RPC call failed") + self.fail(msg=properties_json["status"]) + else: + self.module.debug("Group not found") + return None + + def site_facts(self): + """Output current properties information for the Hostgroup""" + self.module.debug("Running Hostgroup.site_facts...") + + if self.info: + self.module.debug("Group exists") + props = self.get_properties(True) + + self.output_info(props) + else: + self.fail(msg="Error: Group doesn't exit.") + + +def selector(module): + """Figure out which object and which actions + to take given the right parameters""" + + if module.params["target"] == "host": + target = Host(module.params, module) + target.site_facts() + elif module.params["target"] == "hostgroup": + # Validate target specific required parameters + if module.params["fullpath"] is not None: + target = Hostgroup(module.params, module) + target.site_facts() + else: + module.fail_json( + msg="Parameter 'fullpath' required for target 'hostgroup'") + else: + module.fail_json( + msg="Error: Unexpected target \"" + module.params["target"] + + "\" was specified.") + + +def main(): + TARGETS = [ + "host", + "hostgroup"] + + module = AnsibleModule( + argument_spec=dict( + target=dict(required=True, default=None, choices=TARGETS), + company=dict(required=True, default=None), + user=dict(required=True, default=None), + password=dict(required=True, default=None, no_log=True), + + collector=dict(require=False, default=None), + hostname=dict(required=False, default=None), + displayname=dict(required=False, default=None), + fullpath=dict(required=False, default=None) + ), + supports_check_mode=True + ) + + if HAS_LIB_JSON is not True: + module.fail_json(msg="Unable to load JSON library") + + selector(module) + +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * +from ansible.module_utils.urls import open_url + +if __name__ == "__main__": + main() From 57c142b6ed633938c77ce115f5d4573352e0ce4f Mon Sep 17 00:00:00 2001 From: Robin Schneider Date: Tue, 2 Aug 2016 21:12:51 +0200 Subject: [PATCH 1897/2522] Use HTTPS instead of legacy HTTP for ansible.com (#2636) Mechanical edit done by this "one-liner": git ls-files -z "$(git rev-parse --show-toplevel)" | xargs --null -I '{}' find '{}' -type f -print0 | xargs --null sed --in-place --regexp-extended 's#http://www\.ansible\.com#https://www.ansible.com#g;' Related to: https://github.com/ansible/ansible/issues/16869 --- notification/rocketchat.py | 4 ++-- notification/slack.py | 4 ++-- windows/win_uri.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/notification/rocketchat.py b/notification/rocketchat.py index 898ea6a1209..1239fc01d56 100644 --- a/notification/rocketchat.py +++ b/notification/rocketchat.py @@ -67,7 +67,7 @@ description: - URL for the message sender's icon. required: false - default: "http://www.ansible.com/favicon.ico" + default: "https://www.ansible.com/favicon.ico" icon_emoji: description: - Emoji for the message sender. The representation for the available emojis can be @@ -213,7 +213,7 @@ def main(): msg = dict(type='str', required=False, default=None), channel = dict(type='str', default=None), username = dict(type='str', default='Ansible'), - icon_url = dict(type='str', default='http://www.ansible.com/favicon.ico'), + icon_url = dict(type='str', default='https://www.ansible.com/favicon.ico'), icon_emoji = dict(type='str', default=None), link_names = dict(type='int', default=1, choices=[0,1]), validate_certs = dict(default='yes', type='bool'), diff --git a/notification/slack.py b/notification/slack.py index e01272c0703..40def3788a8 100644 --- a/notification/slack.py +++ b/notification/slack.py @@ -62,7 +62,7 @@ default: "Ansible" icon_url: description: - - Url for the message sender's icon (default C(http://www.ansible.com/favicon.ico)) + - Url for the message sender's icon (default C(https://www.ansible.com/favicon.ico)) required: false icon_emoji: description: @@ -223,7 +223,7 @@ def main(): msg = dict(type='str', required=False, default=None), channel = dict(type='str', default=None), username = dict(type='str', default='Ansible'), - icon_url = dict(type='str', default='http://www.ansible.com/favicon.ico'), + icon_url = dict(type='str', default='https://www.ansible.com/favicon.ico'), icon_emoji = dict(type='str', default=None), link_names = dict(type='int', default=1, choices=[0,1]), parse = dict(type='str', default=None, choices=['none', 'full']), diff --git a/windows/win_uri.py b/windows/win_uri.py index f4d0e459270..7045f70bd42 100644 --- a/windows/win_uri.py +++ b/windows/win_uri.py @@ -104,7 +104,7 @@ description: The Target URL returned: always type: string - sample: "http://www.ansible.com" + sample: "https://www.ansible.com" method: description: The HTTP method used. returned: always From b28daebc2b65824c05d3d0797bc4ab7e41b07c45 Mon Sep 17 00:00:00 2001 From: Andrea Scarpino Date: Wed, 3 Aug 2016 11:05:08 +0200 Subject: [PATCH 1898/2522] win_firewall_rule: fix "property X doesn't exist" After commit 9392943 more properties are always sets with their defaults values (e.g. service to 'any'). This causes no issue when the rule is created, but causes an error message that says "The property 'X' cannot be found on this object. Verify that the property exists." because the module checks for any property value that has changed, but `netsh advfirewall firewall show rule` does not list any property unless `verbose` is set. This patch solves this. Fixes #2624 --- windows/win_firewall_rule.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_firewall_rule.ps1 b/windows/win_firewall_rule.ps1 index 31aa2741cdb..a63cedec0c1 100644 --- a/windows/win_firewall_rule.ps1 +++ b/windows/win_firewall_rule.ps1 @@ -24,7 +24,7 @@ function getFirewallRule ($fwsettings) { try { #$output = Get-NetFirewallRule -name $($fwsettings.'Rule Name'); - $rawoutput=@(netsh advfirewall firewall show rule name="$($fwsettings.'Rule Name')") + $rawoutput=@(netsh advfirewall firewall show rule name="$($fwsettings.'Rule Name')" verbose) if (!($rawoutput -eq 'No rules match the specified criteria.')){ $rawoutput | Where {$_ -match '^([^:]+):\s*(\S.*)$'} | Foreach -Begin { $FirstRun = $true; From 27c4beb19db83d812b56a0635dc5a0373bc975bb Mon Sep 17 00:00:00 2001 From: Filipe Niero Felisbino Date: Thu, 2 Jun 2016 16:36:19 -0300 Subject: [PATCH 1899/2522] Fix the AMI creation/modification logic thus making it idempotent --- cloud/amazon/ec2_eni.py | 126 ++++++++++++++++++---------------------- 1 file changed, 57 insertions(+), 69 deletions(-) diff --git a/cloud/amazon/ec2_eni.py b/cloud/amazon/ec2_eni.py index 8403cbbbe7b..f9d0e528c6d 100644 --- a/cloud/amazon/ec2_eni.py +++ b/cloud/amazon/ec2_eni.py @@ -18,7 +18,9 @@ module: ec2_eni short_description: Create and optionally attach an Elastic Network Interface (ENI) to an instance description: - - Create and optionally attach an Elastic Network Interface (ENI) to an instance. If an ENI ID is provided, an attempt is made to update the existing ENI. By passing 'None' as the instance_id, an ENI can be detached from an instance. + - Create and optionally attach an Elastic Network Interface (ENI) to an + instance. If an ENI ID is provided, an attempt is made to update the + existing ENI. By passing state=detached, an ENI can be detached from its instance. version_added: "2.0" author: "Rob White (@wimnat)" options: @@ -29,7 +31,8 @@ default: null instance_id: description: - - Instance ID that you wish to attach ENI to. To detach an ENI from an instance, use 'None'. + - Instance ID that you wish to attach ENI to, if None the new ENI will be + created in detached state, existing ENI will keep current attachment state. required: false default: null private_ip_address: @@ -54,10 +57,10 @@ default: null state: description: - - Create or delete ENI. + - Create, delete or detach ENI from its instance. required: false default: present - choices: [ 'present', 'absent' ] + choices: [ 'present', 'absent', 'detached' ] device_index: description: - The index of the device for the network interface attachment on the instance. @@ -129,7 +132,7 @@ eni_id: eni-yyyyyyyy state: present secondary_private_ip_addresses: - - + - # Destroy an ENI, detaching it from any instance if necessary - ec2_eni: @@ -239,7 +242,7 @@ def create_eni(connection, vpc_id, module): changed = False try: - eni = compare_eni(connection, module) + eni = find_eni(connection, module) if eni is None: eni = connection.create_network_interface(subnet_id, private_ip_address, description, security_groups) if instance_id is not None: @@ -274,21 +277,13 @@ def create_eni(connection, vpc_id, module): module.exit_json(changed=changed, interface=get_eni_info(eni)) -def modify_eni(connection, vpc_id, module, looked_up_eni_id): +def modify_eni(connection, vpc_id, module, eni): - if looked_up_eni_id is None: - eni_id = module.params.get("eni_id") - else: - eni_id = looked_up_eni_id instance_id = module.params.get("instance_id") - if instance_id == 'None': - instance_id = None - do_detach = True - else: - do_detach = False + do_detach = module.params.get('state') == 'detached' device_index = module.params.get("device_index") description = module.params.get('description') - security_groups = get_ec2_security_group_ids_from_names(module.params.get('security_groups'), connection, vpc_id=vpc_id, boto3=False) + security_groups = module.params.get('security_groups') force_detach = module.params.get("force_detach") source_dest_check = module.params.get("source_dest_check") delete_on_termination = module.params.get("delete_on_termination") @@ -297,28 +292,23 @@ def modify_eni(connection, vpc_id, module, looked_up_eni_id): changed = False try: - # Get the eni with the eni_id specified - eni_result_set = connection.get_all_network_interfaces(eni_id) - eni = eni_result_set[0] if description is not None: if eni.description != description: connection.modify_network_interface_attribute(eni.id, "description", description) changed = True - if security_groups is not None: - if sorted(get_sec_group_list(eni.groups)) != sorted(security_groups): - connection.modify_network_interface_attribute(eni.id, "groupSet", security_groups) + if len(security_groups) > 0: + groups = get_ec2_security_group_ids_from_names(security_groups, connection, vpc_id=vpc_id, boto3=False) + if sorted(get_sec_group_list(eni.groups)) != sorted(groups): + connection.modify_network_interface_attribute(eni.id, "groupSet", groups) changed = True if source_dest_check is not None: if eni.source_dest_check != source_dest_check: connection.modify_network_interface_attribute(eni.id, "sourceDestCheck", source_dest_check) changed = True - if delete_on_termination is not None: - if eni.attachment is not None: - if eni.attachment.delete_on_termination is not delete_on_termination: - connection.modify_network_interface_attribute(eni.id, "deleteOnTermination", delete_on_termination, eni.attachment.id) - changed = True - else: - module.fail_json(msg="Can not modify delete_on_termination as the interface is not attached") + if delete_on_termination is not None and eni.attachment is not None: + if eni.attachment.delete_on_termination is not delete_on_termination: + connection.modify_network_interface_attribute(eni.id, "deleteOnTermination", delete_on_termination, eni.attachment.id) + changed = True current_secondary_addresses = [i.private_ip_address for i in eni.private_ip_addresses if not i.primary] if secondary_private_ip_addresses is not None: @@ -337,15 +327,10 @@ def modify_eni(connection, vpc_id, module, looked_up_eni_id): secondary_addresses_to_remove_count = current_secondary_address_count - secondary_private_ip_address_count connection.unassign_private_ip_addresses(network_interface_id=eni.id, private_ip_addresses=current_secondary_addresses[:secondary_addresses_to_remove_count], dry_run=False) - if eni.attachment is not None and instance_id is None and do_detach is True: - eni.detach(force_detach) - wait_for_eni(eni, "detached") + if instance_id is not None: + eni.attach(instance_id, device_index) + wait_for_eni(eni, "attached") changed = True - else: - if instance_id is not None: - eni.attach(instance_id, device_index) - wait_for_eni(eni, "attached") - changed = True except BotoServerError as e: module.fail_json(msg=e.message) @@ -384,21 +369,36 @@ def delete_eni(connection, module): module.fail_json(msg=e.message) -def compare_eni(connection, module): +def detach_eni(connection, module): + + eni = find_eni(connection, module) + if eni.attachment is not None: + eni.detach(force_detach) + wait_for_eni(eni, "detached") + eni.update() + module.exit_json(changed=True, interface=get_eni_info(eni)) + else: + module.exit_json(changed=False, interface=get_eni_info(eni)) + + +def find_eni(connection, module): eni_id = module.params.get("eni_id") subnet_id = module.params.get('subnet_id') private_ip_address = module.params.get('private_ip_address') - description = module.params.get('description') - security_groups = module.params.get('security_groups') try: - all_eni = connection.get_all_network_interfaces(eni_id) - - for eni in all_eni: - remote_security_groups = get_sec_group_list(eni.groups) - if (eni.subnet_id == subnet_id) and (eni.private_ip_address == private_ip_address) and (eni.description == description) and (sorted(remote_security_groups) == sorted(security_groups)): - return eni + filters = {} + if private_ip_address: + filters['private-ip-address'] = private_ip_address + if subnet_id: + filters['subnet-id'] = subnet_id + + eni_result = connection.get_all_network_interfaces(eni_id, filters=filters) + if len(eni_result) > 0: + return eni_result[0] + else: + return None except BotoServerError as e: module.fail_json(msg=e.message) @@ -424,22 +424,6 @@ def _get_vpc_id(connection, module, subnet_id): module.fail_json(msg=e.message) -def get_eni_id_by_ip(connection, module): - - subnet_id = module.params.get('subnet_id') - private_ip_address = module.params.get('private_ip_address') - - try: - all_eni = connection.get_all_network_interfaces(filters={'private-ip-address': private_ip_address, 'subnet-id': subnet_id}) - except BotoServerError as e: - module.fail_json(msg=e.message) - - if all_eni: - return all_eni[0].id - else: - return None - - def main(): argument_spec = ec2_argument_spec() argument_spec.update( @@ -451,7 +435,7 @@ def main(): description=dict(type='str'), security_groups=dict(default=[], type='list'), device_index=dict(default=0, type='int'), - state=dict(default='present', choices=['present', 'absent']), + state=dict(default='present', choices=['present', 'absent', 'detached']), force_detach=dict(default='no', type='bool'), source_dest_check=dict(default=None, type='bool'), delete_on_termination=dict(default=None, type='bool'), @@ -467,6 +451,7 @@ def main(): required_if=([ ('state', 'present', ['subnet_id']), ('state', 'absent', ['eni_id']), + ('state', 'detached', ['eni_id']), ]) ) @@ -491,16 +476,19 @@ def main(): if state == 'present': subnet_id = module.params.get("subnet_id") vpc_id = _get_vpc_id(vpc_connection, module, subnet_id) - # If private_ip_address is not None, look up to see if an ENI already exists with that IP - if eni_id is None and private_ip_address is not None: - eni_id = get_eni_id_by_ip(connection, module) - if eni_id is None: + + eni = find_eni(connection, module) + if eni is None: create_eni(connection, vpc_id, module) else: - modify_eni(connection, vpc_id, module, eni_id) + modify_eni(connection, vpc_id, module, eni) + elif state == 'absent': delete_eni(connection, module) + elif state == 'detached': + detach_eni(connection, module) + from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * From 178caff2ed4e47bc04f22516d4fd2e70a11584c2 Mon Sep 17 00:00:00 2001 From: Filipe Niero Felisbino Date: Thu, 2 Jun 2016 17:16:02 -0300 Subject: [PATCH 1900/2522] Fix docs --- cloud/amazon/ec2_eni.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cloud/amazon/ec2_eni.py b/cloud/amazon/ec2_eni.py index f9d0e528c6d..b4d90aa7d55 100644 --- a/cloud/amazon/ec2_eni.py +++ b/cloud/amazon/ec2_eni.py @@ -18,9 +18,8 @@ module: ec2_eni short_description: Create and optionally attach an Elastic Network Interface (ENI) to an instance description: - - Create and optionally attach an Elastic Network Interface (ENI) to an - instance. If an ENI ID is provided, an attempt is made to update the - existing ENI. By passing state=detached, an ENI can be detached from its instance. + - Create and optionally attach an Elastic Network Interface (ENI) to an instance. If an ENI ID is provided, \ + an attempt is made to update the existing ENI. By passing state=detached, an ENI can be detached from its instance. version_added: "2.0" author: "Rob White (@wimnat)" options: @@ -31,8 +30,8 @@ default: null instance_id: description: - - Instance ID that you wish to attach ENI to, if None the new ENI will be - created in detached state, existing ENI will keep current attachment state. + - Instance ID that you wish to attach ENI to, if None the new ENI will be created in detached state, existing \ + ENI will keep current attachment state. required: false default: null private_ip_address: From 84615249048f7b0d5c4a955afbb776315da43470 Mon Sep 17 00:00:00 2001 From: Filipe Niero Felisbino Date: Fri, 3 Jun 2016 16:10:22 -0300 Subject: [PATCH 1901/2522] Add RETURN docs --- cloud/amazon/ec2_eni.py | 54 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/cloud/amazon/ec2_eni.py b/cloud/amazon/ec2_eni.py index b4d90aa7d55..8fced6034f9 100644 --- a/cloud/amazon/ec2_eni.py +++ b/cloud/amazon/ec2_eni.py @@ -168,6 +168,60 @@ ''' + +RETURN = ''' +interface: + description: Network interface attributes + returned: when state != absent + type: dictionary + contains: + description: + description: interface description + type: string + sample: Firewall network interface + groups: + description: list of security groups + type: list of dictionaries + sample: [ { "sg-f8a8a9da": "default" } ] + id: + description: network interface id + type: string + sample: "eni-1d889198" + mac_address: + description: interface's physical address + type: string + sample: "06:9a:27:6a:6f:99" + owner_id: + description: aws account id + type: string + sample: 812381371 + private_ip_address: + description: primary ip address of this interface + type: string + sample: 10.20.30.40 + private_ip_addresses: + description: list of all private ip addresses associated to this interface + type: list of dictionaries + sample: [ { "primary_address": true, "private_ip_address": "10.20.30.40" } ] + source_dest_check: + description: value of source/dest check flag + type: boolean + sample: True + status: + description: network interface status + type: string + sample: "pending" + subnet_id: + description: which vpc subnet the interface is bound + type: string + sample: subnet-b0a0393c + vpc_id: + description: which vpc this network interface is bound + type: string + sample: vpc-9a9a9da + +''' + import time import re From 3099b607c1c88adff9a17031db1b9c031c7009ae Mon Sep 17 00:00:00 2001 From: Filipe Niero Felisbino Date: Tue, 14 Jun 2016 11:37:45 -0300 Subject: [PATCH 1902/2522] Add attached parameter to ec2_eni module --- cloud/amazon/ec2_eni.py | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/cloud/amazon/ec2_eni.py b/cloud/amazon/ec2_eni.py index 8fced6034f9..55a43f4da37 100644 --- a/cloud/amazon/ec2_eni.py +++ b/cloud/amazon/ec2_eni.py @@ -56,15 +56,21 @@ default: null state: description: - - Create, delete or detach ENI from its instance. + - Create or delete ENI required: false default: present - choices: [ 'present', 'absent', 'detached' ] + choices: [ 'present', 'absent' ] device_index: description: - The index of the device for the network interface attachment on the instance. required: false default: 0 + attached: + description: + - Specifies if network interface should be attached or detached from instance. If attached=yes and no \ + instance_id is given, attachment status won't change + required: false + default: yes force_detach: description: - Force detachment of the interface. This applies either when explicitly detaching the interface by setting instance_id to None or when deleting an interface with state=absent. @@ -283,6 +289,7 @@ def wait_for_eni(eni, status): def create_eni(connection, vpc_id, module): instance_id = module.params.get("instance_id") + attached = module.params.get("attached") if instance_id == 'None': instance_id = None device_index = module.params.get("device_index") @@ -298,7 +305,7 @@ def create_eni(connection, vpc_id, module): eni = find_eni(connection, module) if eni is None: eni = connection.create_network_interface(subnet_id, private_ip_address, description, security_groups) - if instance_id is not None: + if attached and instance_id is not None: try: eni.attach(instance_id, device_index) except BotoServerError: @@ -333,6 +340,7 @@ def create_eni(connection, vpc_id, module): def modify_eni(connection, vpc_id, module, eni): instance_id = module.params.get("instance_id") + attached = module.params.get("attached") do_detach = module.params.get('state') == 'detached' device_index = module.params.get("device_index") description = module.params.get('description') @@ -380,10 +388,13 @@ def modify_eni(connection, vpc_id, module, eni): secondary_addresses_to_remove_count = current_secondary_address_count - secondary_private_ip_address_count connection.unassign_private_ip_addresses(network_interface_id=eni.id, private_ip_addresses=current_secondary_addresses[:secondary_addresses_to_remove_count], dry_run=False) - if instance_id is not None: - eni.attach(instance_id, device_index) - wait_for_eni(eni, "attached") - changed = True + if attached: + if instance_id is not None: + eni.attach(instance_id, device_index) + wait_for_eni(eni, "attached") + changed = True + else: + detach_eni(eni, module) except BotoServerError as e: module.fail_json(msg=e.message) @@ -422,9 +433,9 @@ def delete_eni(connection, module): module.fail_json(msg=e.message) -def detach_eni(connection, module): +def detach_eni(eni, module): - eni = find_eni(connection, module) + force_detach = module.params.get("force_detach") if eni.attachment is not None: eni.detach(force_detach) wait_for_eni(eni, "detached") @@ -488,12 +499,13 @@ def main(): description=dict(type='str'), security_groups=dict(default=[], type='list'), device_index=dict(default=0, type='int'), - state=dict(default='present', choices=['present', 'absent', 'detached']), + state=dict(default='present', choices=['present', 'absent']), force_detach=dict(default='no', type='bool'), source_dest_check=dict(default=None, type='bool'), delete_on_termination=dict(default=None, type='bool'), secondary_private_ip_addresses=dict(default=None, type='list'), - secondary_private_ip_address_count=dict(default=None, type='int') + secondary_private_ip_address_count=dict(default=None, type='int'), + attached=dict(default=True, type='bool') ) ) @@ -503,8 +515,7 @@ def main(): ], required_if=([ ('state', 'present', ['subnet_id']), - ('state', 'absent', ['eni_id']), - ('state', 'detached', ['eni_id']), + ('state', 'absent', ['eni_id']) ]) ) @@ -539,9 +550,6 @@ def main(): elif state == 'absent': delete_eni(connection, module) - elif state == 'detached': - detach_eni(connection, module) - from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * From c81e88856abdc253a5b4653c72e57d3948239e14 Mon Sep 17 00:00:00 2001 From: Filipe Niero Felisbino Date: Tue, 14 Jun 2016 14:55:54 -0300 Subject: [PATCH 1903/2522] Add "version_added" to attached attribute --- cloud/amazon/ec2_eni.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud/amazon/ec2_eni.py b/cloud/amazon/ec2_eni.py index 55a43f4da37..378c2c6d2fd 100644 --- a/cloud/amazon/ec2_eni.py +++ b/cloud/amazon/ec2_eni.py @@ -71,6 +71,7 @@ instance_id is given, attachment status won't change required: false default: yes + version_added: 2.2 force_detach: description: - Force detachment of the interface. This applies either when explicitly detaching the interface by setting instance_id to None or when deleting an interface with state=absent. From 753ddf87ac83ba4d9786ecb5a8d46bbee30c85e4 Mon Sep 17 00:00:00 2001 From: Filipe Niero Felisbino Date: Wed, 15 Jun 2016 10:49:29 -0300 Subject: [PATCH 1904/2522] Change attached parameter default to None --- cloud/amazon/ec2_eni.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/cloud/amazon/ec2_eni.py b/cloud/amazon/ec2_eni.py index 378c2c6d2fd..b87e7c47304 100644 --- a/cloud/amazon/ec2_eni.py +++ b/cloud/amazon/ec2_eni.py @@ -18,8 +18,9 @@ module: ec2_eni short_description: Create and optionally attach an Elastic Network Interface (ENI) to an instance description: - - Create and optionally attach an Elastic Network Interface (ENI) to an instance. If an ENI ID is provided, \ - an attempt is made to update the existing ENI. By passing state=detached, an ENI can be detached from its instance. + - Create and optionally attach an Elastic Network Interface (ENI) to an instance. If an ENI ID or private_ip is \ + provided, the existing ENI (if any) will be modified. The 'attached' parameter controls the attachment status \ + of the network interface. version_added: "2.0" author: "Rob White (@wimnat)" options: @@ -30,8 +31,8 @@ default: null instance_id: description: - - Instance ID that you wish to attach ENI to, if None the new ENI will be created in detached state, existing \ - ENI will keep current attachment state. + - Instance ID that you wish to attach ENI to. Since version 2.2, use the 'attached' parameter to attach or \ + detach an ENI. Prior to 2.2, to detach an ENI from an instance, use 'None'. required: false default: null private_ip_address: @@ -67,8 +68,8 @@ default: 0 attached: description: - - Specifies if network interface should be attached or detached from instance. If attached=yes and no \ - instance_id is given, attachment status won't change + - Specifies if network interface should be attached or detached from instance. If ommited, attachment status \ + won't change required: false default: yes version_added: 2.2 @@ -306,7 +307,7 @@ def create_eni(connection, vpc_id, module): eni = find_eni(connection, module) if eni is None: eni = connection.create_network_interface(subnet_id, private_ip_address, description, security_groups) - if attached and instance_id is not None: + if attached == True and instance_id is not None: try: eni.attach(instance_id, device_index) except BotoServerError: @@ -389,12 +390,11 @@ def modify_eni(connection, vpc_id, module, eni): secondary_addresses_to_remove_count = current_secondary_address_count - secondary_private_ip_address_count connection.unassign_private_ip_addresses(network_interface_id=eni.id, private_ip_addresses=current_secondary_addresses[:secondary_addresses_to_remove_count], dry_run=False) - if attached: - if instance_id is not None: + if attached == True and instance_id is not None: eni.attach(instance_id, device_index) wait_for_eni(eni, "attached") changed = True - else: + elif attached == False: detach_eni(eni, module) except BotoServerError as e: @@ -506,7 +506,7 @@ def main(): delete_on_termination=dict(default=None, type='bool'), secondary_private_ip_addresses=dict(default=None, type='list'), secondary_private_ip_address_count=dict(default=None, type='int'), - attached=dict(default=True, type='bool') + attached=dict(default=None, type='bool') ) ) @@ -516,7 +516,8 @@ def main(): ], required_if=([ ('state', 'present', ['subnet_id']), - ('state', 'absent', ['eni_id']) + ('state', 'absent', ['eni_id']), + ('attached', True, ['instance_id']) ]) ) From e2e697c3ffc1066c1d11d63c5e44384949a47fef Mon Sep 17 00:00:00 2001 From: Filipe Niero Felisbino Date: Tue, 26 Jul 2016 11:38:21 -0300 Subject: [PATCH 1905/2522] Fix attachment issue ( thanks @gunzy83 ) --- cloud/amazon/ec2_eni.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/cloud/amazon/ec2_eni.py b/cloud/amazon/ec2_eni.py index b87e7c47304..ac05ba43a39 100644 --- a/cloud/amazon/ec2_eni.py +++ b/cloud/amazon/ec2_eni.py @@ -241,7 +241,6 @@ except ImportError: HAS_BOTO = False - def get_eni_info(interface): # Private addresses @@ -390,7 +389,10 @@ def modify_eni(connection, vpc_id, module, eni): secondary_addresses_to_remove_count = current_secondary_address_count - secondary_private_ip_address_count connection.unassign_private_ip_addresses(network_interface_id=eni.id, private_ip_addresses=current_secondary_addresses[:secondary_addresses_to_remove_count], dry_run=False) - if attached == True and instance_id is not None: + if attached == True: + if eni.attachment and eni.attachment.instance_id != instance_id: + detach_eni(eni, module) + if eni.attachment is None: eni.attach(instance_id, device_index) wait_for_eni(eni, "attached") changed = True @@ -451,13 +453,20 @@ def find_eni(connection, module): eni_id = module.params.get("eni_id") subnet_id = module.params.get('subnet_id') private_ip_address = module.params.get('private_ip_address') + instance_id = module.params.get('instance_id') + device_index = module.params.get('device_index') try: filters = {} - if private_ip_address: - filters['private-ip-address'] = private_ip_address if subnet_id: filters['subnet-id'] = subnet_id + if private_ip_address: + filters['private-ip-address'] = private_ip_address + else: + if instance_id: + filters['attachment.instance-id'] = instance_id + if device_index: + filters['attachment.device-index'] = device_index eni_result = connection.get_all_network_interfaces(eni_id, filters=filters) if len(eni_result) > 0: From f86c5f84657c4da2729df5792a8c515f0f4c7f47 Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Mon, 21 Mar 2016 06:07:04 -0700 Subject: [PATCH 1906/2522] Manage AWS Nat Gateways * Create an AWS Nat Gateway. * Delete an AWS Nat Gateway. * If Nat Gateway exist in subnet and the option is passed to not create one, it will then return the Nat Gateway object. --- cloud/amazon/ec2_vpc_nat_gateway.py | 930 ++++++++++++++++++++++++++++ 1 file changed, 930 insertions(+) create mode 100644 cloud/amazon/ec2_vpc_nat_gateway.py diff --git a/cloud/amazon/ec2_vpc_nat_gateway.py b/cloud/amazon/ec2_vpc_nat_gateway.py new file mode 100644 index 00000000000..9b1fb235a61 --- /dev/null +++ b/cloud/amazon/ec2_vpc_nat_gateway.py @@ -0,0 +1,930 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ec2_vpc_nat_gateway +short_description: Manage AWS VPC NAT Gateways +description: + - Ensure the state of AWS VPC NAT Gateways based on their id, allocation and subnet ids. +version_added: "2.1" +requirements: [boto3, botocore] +options: + state: + description: + - Ensure NAT Gateway is present or absent + required: false + default: "present" + choices: ["present", "absent"] + nat_gateway_id: + description: + - The id AWS dynamically allocates to the NAT Gateway on creation. + This is required when the absent option is present. + required: false + default: None + subnet_id: + description: + - The id of the subnet to create the NAT Gateway in. This is required + with the present option. + required: false + default: None + allocation_id: + description: + - The id of the elastic IP allocation. If this is not passed and the + eip_address is not passed. An EIP is generated for this Nat Gateway + required: false + default: None + eip_address: + description: + - The elasti ip address of the EIP you want attached to this Nat Gateway. + If this is not passed and the allocation_id is not passed. + An EIP is generated for this Nat Gateway + required: false + if_exist_do_not_create: + description: + - if a Nat Gateway exists already in the subnet_id, then do not create a new one. + required: false + default: false + release_eip: + description: + - Deallocate the EIP from the VPC. + - Option is only valid with the absent state. + required: false + default: true + wait: + description: + - Wait for operation to complete before returning + required: false + default: true + wait_timeout: + description: + - How many seconds to wait for an operation to complete before timing out + required: false + default: 300 + client_token: + description: + - Optional unique token to be used during create to ensure idempotency. + When specifying this option, ensure you specify the eip_address parameter + as well otherwise any subsequent runs will fail. + required: false + +author: + - "Allen Sanabria (@linuxdynasty)" + - "Jon Hadfield (@jonhadfield)" + - "Karen Cheng(@Etherdaemon)" +extends_documentation_fragment: + - aws + - ec2 +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +- name: Create new nat gateway with client token + ec2_vpc_nat_gateway: + state: present + subnet_id: subnet-12345678 + eip_address: 52.1.1.1 + region: ap-southeast-2 + client_token: abcd-12345678 + register: new_nat_gateway + +- name: Create new nat gateway allocation-id + ec2_vpc_nat_gateway: + state: present + subnet_id: subnet-12345678 + allocation_id: eipalloc-12345678 + region: ap-southeast-2 + register: new_nat_gateway + +- name: Create new nat gateway with when condition + ec2_vpc_nat_gateway: + state: present + subnet_id: subnet-12345678 + eip_address: 52.1.1.1 + region: ap-southeast-2 + register: new_nat_gateway + when: existing_nat_gateways.result == [] + +- name: Create new nat gateway and wait for available status + ec2_vpc_nat_gateway: + state: present + subnet_id: subnet-12345678 + eip_address: 52.1.1.1 + wait: yes + region: ap-southeast-2 + register: new_nat_gateway + +- name: Create new nat gateway and allocate new eip + ec2_vpc_nat_gateway: + state: present + subnet_id: subnet-12345678 + wait: yes + region: ap-southeast-2 + register: new_nat_gateway + +- name: Create new nat gateway and allocate new eip if a nat gateway does not yet exist in the subnet. + ec2_vpc_nat_gateway: + state: present + subnet_id: subnet-12345678 + wait: yes + region: ap-southeast-2 + if_exist_do_not_create: true + register: new_nat_gateway + +- name: Delete nat gateway using discovered nat gateways from facts module + ec2_vpc_nat_gateway: + state: absent + region: ap-southeast-2 + wait: yes + nat_gateway_id: "{{ item.NatGatewayId }}" + release_eip: yes + register: delete_nat_gateway_result + with_items: "{{ gateways_to_remove.result }}" + +- name: Delete nat gateway and wait for deleted status + ec2_vpc_nat_gateway: + state: absent + nat_gateway_id: nat-12345678 + wait: yes + wait_timeout: 500 + region: ap-southeast-2 + +- name: Delete nat gateway and release EIP + ec2_vpc_nat_gateway: + state: absent + nat_gateway_id: nat-12345678 + release_eip: yes + region: ap-southeast-2 +''' + +RETURN = ''' +create_time: + description: The ISO 8601 date time formatin UTC. + returned: In all cases. + type: string + sample: "2016-03-05T05:19:20.282000+00:00'" +nat_gateway_id: + description: id of the VPC NAT Gateway + returned: In all cases. + type: string + sample: "nat-0d1e3a878585988f8" +subnet_id: + description: id of the Subnet + returned: In all cases. + type: string + sample: "subnet-12345" +state: + description: The current state of the Nat Gateway. + returned: In all cases. + type: string + sample: "available" +vpc_id: + description: id of the VPC. + returned: In all cases. + type: string + sample: "vpc-12345" +nat_gateway_addresses: + description: List of dictionairies containing the public_ip, network_interface_id, private_ip, and allocation_id. + returned: In all cases. + type: string + sample: [ + { + 'public_ip': '52.52.52.52', + 'network_interface_id': 'eni-12345', + 'private_ip': '10.0.0.100', + 'allocation_id': 'eipalloc-12345' + } + ] +''' + +try: + import botocore + import boto3 + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + +import time +import datetime + +def convert_to_lower(data): + """Convert all uppercase keys in dict with lowercase_ + Args: + data (dict): Dictionary with keys that have upper cases in them + Example.. NatGatewayAddresses == nat_gateway_addresses + if a val is of type datetime.datetime, it will be converted to + the ISO 8601 + + Basic Usage: + >>> test = {'NatGatewaysAddresses': []} + >>> test = convert_to_lower(test) + { + 'nat_gateways_addresses': [] + } + + Returns: + Dictionary + """ + results = dict() + for key, val in data.items(): + key = re.sub('([A-Z]{1})', r'_\1', key).lower()[1:] + if isinstance(val, datetime.datetime): + results[key] = val.isoformat() + else: + results[key] = val + return results + +def formatted_nat_gw_output(data): + """Format the results of NatGateways into lowercase with underscores. + Args: + data (list): List of dictionaries with keys that have upper cases in + them. Example.. NatGatewayAddresses == nat_gateway_addresses + if a val is of type datetime.datetime, it will be converted to + the ISO 8601 + + Basic Usage: + >>> test = [ + { + 'VpcId': 'vpc-12345', + 'State': 'available', + 'NatGatewayId': 'nat-0b2f9f2ac3f51a653', + 'SubnetId': 'subnet-12345', + 'NatGatewayAddresses': [ + { + 'PublicIp': '52.52.52.52', + 'NetworkInterfaceId': 'eni-12345', + 'AllocationId': 'eipalloc-12345', + 'PrivateIp': '10.0.0.100' + } + ], + 'CreateTime': datetime.datetime(2016, 3, 5, 5, 19, 20, 282000, tzinfo=tzutc()) + } + ] + >>> test = formatted_nat_gw_output(test) + [ + { + 'nat_gateway_id': 'nat-0b2f9f2ac3f51a653', + 'subnet_id': 'subnet-12345', + 'nat_gateway_addresses': [ + { + 'public_ip': '52.52.52.52', + 'network_interface_id': 'eni-12345', + 'private_ip': '10.0.0.100', + 'allocation_id': 'eipalloc-12345' + } + ], + 'state': 'available', + 'create_time': '2016-03-05T05:19:20.282000+00:00', + 'vpc_id': 'vpc-12345' + } + ] + + Returns: + List + """ + results = list() + for gw in data: + output = dict() + ng_addresses = gw.pop('NatGatewayAddresses') + output = convert_to_lower(gw) + output['nat_gateway_addresses'] = [] + for address in ng_addresses: + gw_data = convert_to_lower(address) + output['nat_gateway_addresses'].append(gw_data) + results.append(output) + + return results + +def get_nat_gateways(client, subnet_id=None, nat_gateway_id=None): + """Retrieve a list of NAT Gateways + Args: + client (botocore.client.EC2): Boto3 client + + Kwargs: + subnet_id (str): The subnet_id the nat resides in. + nat_gateway_id (str): The Amazon nat id. + + Basic Usage: + >>> client = boto3.client('ec2') + >>> subnet_id = 'subnet-w4t12897' + >>> get_nat_gateways(client, subnet_id) + [ + true, + "", + { + "nat_gateway_id": "nat-03835afb6e31df79b", + "subnet_id": "subnet-w4t12897", + "nat_gateway_addresses": [ + { + "public_ip": "52.87.29.36", + "network_interface_id": "eni-5579742d", + "private_ip": "10.0.0.102", + "allocation_id": "eipalloc-36014da3" + } + ], + "state": "deleted", + "create_time": "2016-03-05T00:33:21.209000+00:00", + "delete_time": "2016-03-05T00:36:37.329000+00:00", + "vpc_id": "vpc-w68571b5" + } + + Returns: + Tuple (bool, str, list) + """ + params = dict() + err_msg = "" + gateways_retrieved = False + if nat_gateway_id: + params['NatGatewayIds'] = [nat_gateway_id] + else: + params['Filter'] = [ + { + 'Name': 'subnet-id', + 'Values': [subnet_id] + } + ] + + try: + gateways = client.describe_nat_gateways(**params)['NatGateways'] + existing_gateways = formatted_nat_gw_output(gateways) + gateways_retrieved = True + except botocore.exceptions.ClientError as e: + err_msg = str(e) + + return gateways_retrieved, err_msg, existing_gateways + +def wait_for_status(client, wait_timeout, nat_gateway_id, status): + """Wait for the Nat Gateway to reach a status + Args: + client (botocore.client.EC2): Boto3 client + wait_timeout (int): Number of seconds to wait, until this timeout is reached. + nat_gateway_id (str): The Amazon nat id. + status (str): The status to wait for. + examples. status=available, status=deleted + + Basic Usage: + >>> client = boto3.client('ec2') + >>> subnet_id = 'subnet-w4t12897' + >>> allocation_id = 'eipalloc-36014da3' + >>> wait_for_status(client, subnet_id, allocation_id) + [ + true, + "", + { + "nat_gateway_id": "nat-03835afb6e31df79b", + "subnet_id": "subnet-w4t12897", + "nat_gateway_addresses": [ + { + "public_ip": "52.87.29.36", + "network_interface_id": "eni-5579742d", + "private_ip": "10.0.0.102", + "allocation_id": "eipalloc-36014da3" + } + ], + "state": "deleted", + "create_time": "2016-03-05T00:33:21.209000+00:00", + "delete_time": "2016-03-05T00:36:37.329000+00:00", + "vpc_id": "vpc-w68571b5" + } + ] + + Returns: + Tuple (bool, str, list) + """ + polling_increment_secs = 5 + wait_timeout = time.time() + wait_timeout + status_achieved = False + nat_gateway = list() + err_msg = "" + + while wait_timeout > time.time(): + try: + gws_retrieved, err_msg, nat_gateway = ( + get_nat_gateways(client, nat_gateway_id=nat_gateway_id) + ) + if gws_retrieved and nat_gateway: + if nat_gateway[0].get('state') == status: + status_achieved = True + break + + elif nat_gateway[0].get('state') == 'failed': + err_msg = nat_gateway[0].get('failure_message') + break + + else: + time.sleep(polling_increment_secs) + except botocore.exceptions.ClientError as e: + err_msg = str(e) + + if not status_achieved: + err_msg = "Wait time out reached, while waiting for results" + + return status_achieved, err_msg, nat_gateway + +def gateway_in_subnet_exists(client, subnet_id, allocation_id=None): + """Retrieve all NAT Gateways for a subnet. + Args: + subnet_id (str): The subnet_id the nat resides in. + + Kwargs: + allocation_id (str): The eip Amazon identifier. + default = None + + Basic Usage: + >>> client = boto3.client('ec2') + >>> subnet_id = 'subnet-w4t12897' + >>> allocation_id = 'eipalloc-36014da3' + >>> gateway_in_subnet_exists(client, subnet_id, allocation_id) + ( + [ + { + "nat_gateway_id": "nat-03835afb6e31df79b", + "subnet_id": "subnet-w4t12897", + "nat_gateway_addresses": [ + { + "public_ip": "52.87.29.36", + "network_interface_id": "eni-5579742d", + "private_ip": "10.0.0.102", + "allocation_id": "eipalloc-36014da3" + } + ], + "state": "deleted", + "create_time": "2016-03-05T00:33:21.209000+00:00", + "delete_time": "2016-03-05T00:36:37.329000+00:00", + "vpc_id": "vpc-w68571b5" + } + ], + False + ) + + Returns: + Tuple (list, bool) + """ + allocation_id_exists = False + gateways = [] + gws_retrieved, _, gws = get_nat_gateways(client, subnet_id) + if not gws_retrieved: + return gateways, allocation_id_exists + for gw in gws: + for address in gw['nat_gateway_addresses']: + if gw.get('state') == 'available' or gw.get('state') == 'pending': + if allocation_id: + if address.get('allocation_id') == allocation_id: + allocation_id_exists = True + gateways.append(gw) + else: + gateways.append(gw) + + return gateways, allocation_id_exists + +def get_eip_allocation_id_by_address(client, eip_address, check_mode=False): + """Release an EIP from your EIP Pool + Args: + client (botocore.client.EC2): Boto3 client + eip_address (str): The Elastic IP Address of the EIP. + + Kwargs: + check_mode (bool): if set to true, do not run anything and + falsify the results. + + Basic Usage: + >>> client = boto3.client('ec2') + >>> eip_address = '52.87.29.36' + >>> get_eip_allocation_id_by_address(client, eip_address) + 'eipalloc-36014da3' + + Returns: + Tuple (str, str) + """ + params = { + 'PublicIps': [eip_address] + } + allocation_id = None + err_msg = "" + if check_mode: + return "eipalloc-123456", err_msg + try: + allocation = client.describe_addresses(**params)['Addresses'][0] + if not allocation.get('Domain') != 'vpc': + err_msg = ( + "EIP provided is a non-VPC EIP, please allocate a VPC scoped EIP" + ) + else: + allocation_id = allocation.get('AllocationId') + except botocore.exceptions.ClientError as e: + err_msg = str(e) + + return allocation_id, err_msg + +def allocate_eip_address(client, check_mode=False): + """Release an EIP from your EIP Pool + Args: + client (botocore.client.EC2): Boto3 client + + Kwargs: + check_mode (bool): if set to true, do not run anything and + falsify the results. + + Basic Usage: + >>> client = boto3.client('ec2') + >>> allocate_eip_address(client) + True + + Returns: + Tuple (bool, str) + """ + if check_mode: + return True, "eipalloc-123456" + + ip_allocated = False + params = { + 'Domain': 'vpc' + } + try: + new_eip = client.allocate_address(**params) + ip_allocated = True + except botocore.exceptions.ClientError: + pass + + return ip_allocated, new_eip['AllocationId'] + +def release_address(client, allocation_id, check_mode=False): + """Release an EIP from your EIP Pool + Args: + client (botocore.client.EC2): Boto3 client + allocation_id (str): The eip Amazon identifier. + + Kwargs: + check_mode (bool): if set to true, do not run anything and + falsify the results. + + Basic Usage: + >>> client = boto3.client('ec2') + >>> allocation_id = "eipalloc-123456" + >>> release_address(client, allocation_id) + True + + Returns: + Boolean + """ + if check_mode: + return True + + ip_released = False + params = { + 'AllocationId': allocation_id, + } + try: + client.release_address(**params) + ip_released = True + except botocore.exceptions.ClientError: + pass + + return ip_released + +def create(client, subnet_id, allocation_id, client_token=None, + wait=False, wait_timeout=0, if_exist_do_not_create=False): + """Create an Amazon NAT Gateway. + Args: + client (botocore.client.EC2): Boto3 client + subnet_id (str): The subnet_id the nat resides in. + allocation_id (str): The eip Amazon identifier. + + Kwargs: + if_exist_do_not_create (bool): if a nat gateway already exists in this + subnet, than do not create another one. + default = False + wait (bool): Wait for the nat to be in the deleted state before returning. + default = False + wait_timeout (int): Number of seconds to wait, until this timeout is reached. + default = 0 + client_token (str): + default = None + + Basic Usage: + >>> client = boto3.client('ec2') + >>> subnet_id = 'subnet-w4t12897' + >>> allocation_id = 'eipalloc-36014da3' + >>> create(client, subnet_id, allocation_id, if_exist_do_not_create=True, wait=True, wait_timeout=500) + [ + true, + "", + { + "nat_gateway_id": "nat-03835afb6e31df79b", + "subnet_id": "subnet-w4t12897", + "nat_gateway_addresses": [ + { + "public_ip": "52.87.29.36", + "network_interface_id": "eni-5579742d", + "private_ip": "10.0.0.102", + "allocation_id": "eipalloc-36014da3" + } + ], + "state": "deleted", + "create_time": "2016-03-05T00:33:21.209000+00:00", + "delete_time": "2016-03-05T00:36:37.329000+00:00", + "vpc_id": "vpc-w68571b5" + } + ] + + Returns: + Tuple (bool, str, list) + """ + params = { + 'SubnetId': subnet_id, + 'AllocationId': allocation_id + } + request_time = datetime.datetime.utcnow() + changed = False + token_provided = False + err_msg = "" + + if client_token: + token_provided = True + params['ClientToken'] = client_token + + try: + result = client.create_nat_gateway(**params)["NatGateway"] + changed = True + create_time = result['CreateTime'].replace(tzinfo=None) + if token_provided and (request_time > create_time): + changed = False + elif wait: + status_achieved, err_msg, result = ( + wait_for_status( + client, wait_timeout, result['NatGatewayId'], 'available' + ) + ) + except botocore.exceptions.ClientError as e: + if "IdempotentParameterMismatch" in e.message: + err_msg = ( + 'NAT Gateway does not support update and token has already been provided' + ) + else: + err_msg = str(e) + + return changed, err_msg, result + +def pre_create(client, subnet_id, allocation_id=None, eip_address=None, + if_exist_do_not_create=False, wait=False, wait_timeout=0, + client_token=None): + """Create an Amazon NAT Gateway. + Args: + client (botocore.client.EC2): Boto3 client + subnet_id (str): The subnet_id the nat resides in. + + Kwargs: + allocation_id (str): The eip Amazon identifier. + default = None + eip_address (str): The Elastic IP Address of the EIP. + default = None + if_exist_do_not_create (bool): if a nat gateway already exists in this + subnet, than do not create another one. + default = False + wait (bool): Wait for the nat to be in the deleted state before returning. + default = False + wait_timeout (int): Number of seconds to wait, until this timeout is reached. + default = 0 + client_token (str): + default = None + + Basic Usage: + >>> client = boto3.client('ec2') + >>> subnet_id = 'subnet-w4t12897' + >>> allocation_id = 'eipalloc-36014da3' + >>> pre_create(client, subnet_id, allocation_id, if_exist_do_not_create=True, wait=True, wait_timeout=500) + [ + true, + "", + { + "nat_gateway_id": "nat-03835afb6e31df79b", + "subnet_id": "subnet-w4t12897", + "nat_gateway_addresses": [ + { + "public_ip": "52.87.29.36", + "network_interface_id": "eni-5579742d", + "private_ip": "10.0.0.102", + "allocation_id": "eipalloc-36014da3" + } + ], + "state": "deleted", + "create_time": "2016-03-05T00:33:21.209000+00:00", + "delete_time": "2016-03-05T00:36:37.329000+00:00", + "vpc_id": "vpc-w68571b5" + } + ] + + Returns: + Tuple (bool, str, list) + """ + changed = False + err_msg = "" + results = list() + + if not allocation_id and not eip_address: + existing_gateways, allocation_id_exists = ( + gateway_in_subnet_exists(client, subnet_id) + ) + if len(existing_gateways) > 0 and if_exist_do_not_create: + results = existing_gateways + return changed, err_msg, results + else: + _, allocation_id = allocate_eip_address(client) + + elif eip_address or allocation_id: + if eip_address and not allocation_id: + allocation_id = get_eip_allocation_id_by_address(client) + + existing_gateways, allocation_id_exists = ( + gateway_in_subnet_exists(client, subnet_id, allocation_id) + ) + if len(existing_gateways) > 0 and (allocation_id_exists or if_exist_do_not_create): + results = existing_gateways + return changed, err_msg, results + + changed, err_msg, results = create( + client, subnet_id, allocation_id, client_token, + wait, wait_timeout,if_exist_do_not_create + ) + + return changed, err_msg, results + +def remove(client, nat_gateway_id, wait=False, wait_timeout=0, release_eip=False): + """Delete an Amazon NAT Gateway. + Args: + client (botocore.client.EC2): Boto3 client + nat_gateway_id (str): The Amazon nat id. + + Kwargs: + wait (bool): Wait for the nat to be in the deleted state before returning. + wait_timeout (int): Number of seconds to wait, until this timeout is reached. + release_eip (bool): Once the nat has been deleted, you can deallocate the eip from the vpc. + + Basic Usage: + >>> client = boto3.client('ec2') + >>> nat_gw_id = 'nat-03835afb6e31df79b' + >>> remove(client, nat_gw_id, wait=True, wait_timeout=500, release_eip=True) + [ + true, + "", + { + "nat_gateway_id": "nat-03835afb6e31df79b", + "subnet_id": "subnet-w4t12897", + "nat_gateway_addresses": [ + { + "public_ip": "52.87.29.36", + "network_interface_id": "eni-5579742d", + "private_ip": "10.0.0.102", + "allocation_id": "eipalloc-36014da3" + } + ], + "state": "deleted", + "create_time": "2016-03-05T00:33:21.209000+00:00", + "delete_time": "2016-03-05T00:36:37.329000+00:00", + "vpc_id": "vpc-w68571b5" + } + ] + + Returns: + Tuple (bool, str, list) + """ + params = { + 'NatGatewayId': nat_gateway_id + } + changed = False + err_msg = "" + results = list() + try: + exist, _, gw = get_nat_gateways(client, nat_gateway_id=nat_gateway_id) + if exist and len(gw) == 1: + results = gw[0] + result = client.delete_nat_gateway(**params) + result = convert_to_lower(result) + allocation_id = ( + results['nat_gateway_addresses'][0]['allocation_id'] + ) + changed = True + except botocore.exceptions.ClientError as e: + err_msg = str(e) + + if wait and not err_msg: + status_achieved, err_msg, results = ( + wait_for_status(client, wait_timeout, nat_gateway_id, 'deleted') + ) + + if release_eip: + eip_released = release_address(client, allocation_id) + if not eip_released: + err_msg = "Failed to release eip %s".format(allocation_id) + + return changed, err_msg, results + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + subnet_id=dict(), + eip_address=dict(), + allocation_id=dict(), + if_exist_do_not_create=dict(type='bool', default=False), + state=dict(default='present', choices=['present', 'absent']), + wait=dict(type='bool', default=True), + wait_timeout=dict(type='int', default=320, required=False), + release_eip=dict(type='bool', default=False), + nat_gateway_id=dict(), + client_token=dict(), + ) + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + mutually_exclusive=[ + ['allocation_id', 'eip_address'] + ] + ) + + # Validate Requirements + if not HAS_BOTO3: + module.fail_json(msg='botocore/boto3 is required.') + + state = module.params.get('state').lower() + check_mode = module.check_mode + subnet_id = module.params.get('subnet_id') + allocation_id = module.params.get('allocation_id') + eip_address = module.params.get('eip_address') + nat_gateway_id = module.params.get('nat_gateway_id') + wait = module.params.get('wait') + wait_timeout = module.params.get('wait_timeout') + release_eip = module.params.get('release_eip') + client_token = module.params.get('client_token') + if_exist_do_not_create = module.params.get('if_exist_do_not_create') + + try: + region, ec2_url, aws_connect_kwargs = ( + get_aws_connection_info(module, boto3=True) + ) + client = ( + boto3_conn( + module, conn_type='client', resource='ec2', + region=region, endpoint=ec2_url, **aws_connect_kwargs + ) + ) + except botocore.exceptions.ClientError, e: + module.fail_json(msg="Boto3 Client Error - " + str(e.msg)) + + changed = False + err_msg = None + + #Ensure resource is present + if state == 'present': + if not subnet_id: + module.fail_json(msg='subnet_id is required for creation') + + elif check_mode: + changed = True + results = 'Would have created NAT Gateway if not in check mode' + else: + changed, err_msg, results = ( + pre_create( + client, subnet_id, allocation_id, eip_address, + if_exist_do_not_create, wait, wait_timeout, + client_token + ) + ) + else: + if not nat_gateway_id: + module.fail_json(msg='nat_gateway_id is required for removal') + + elif check_mode: + changed = True + results = 'Would have deleted NAT Gateway if not in check mode' + else: + changed, err_msg, results = ( + remove(client, nat_gateway_id, wait, wait_timeout, release_eip) + ) + + if err_msg: + module.fail_json(msg=err_msg) + else: + module.exit_json(changed=changed, **results[0]) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() + From 349db85b00ec97f3274e4a4bb8c91c4422bb1c30 Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Wed, 23 Mar 2016 09:00:04 -0700 Subject: [PATCH 1907/2522] Fixed the missing argument to get_eip_allocation_id_by_address --- cloud/amazon/ec2_vpc_nat_gateway.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/ec2_vpc_nat_gateway.py b/cloud/amazon/ec2_vpc_nat_gateway.py index 9b1fb235a61..307a9646e5e 100644 --- a/cloud/amazon/ec2_vpc_nat_gateway.py +++ b/cloud/amazon/ec2_vpc_nat_gateway.py @@ -681,7 +681,7 @@ def create(client, subnet_id, allocation_id, client_token=None, def pre_create(client, subnet_id, allocation_id=None, eip_address=None, if_exist_do_not_create=False, wait=False, wait_timeout=0, - client_token=None): + client_token=None, check_mode=False): """Create an Amazon NAT Gateway. Args: client (botocore.client.EC2): Boto3 client @@ -747,7 +747,11 @@ def pre_create(client, subnet_id, allocation_id=None, eip_address=None, elif eip_address or allocation_id: if eip_address and not allocation_id: - allocation_id = get_eip_allocation_id_by_address(client) + allocation_id = ( + get_eip_allocation_id_by_address( + client, eip_address, check_mode=check_mode + ) + ) existing_gateways, allocation_id_exists = ( gateway_in_subnet_exists(client, subnet_id, allocation_id) From ee523be26c3d4677f0393dc9976ecd1778098324 Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Mon, 28 Mar 2016 20:38:52 -0700 Subject: [PATCH 1908/2522] Updated module to be compliant with test cases. * Added integration tests * Added unit tests --- cloud/amazon/ec2_vpc_nat_gateway.py | 553 +++++++++++------- test/integrations/group_vars/all.yml | 1 + .../roles/ec2_vpc_nat_gateway/tasks/main.yml | 76 +++ test/integrations/site.yml | 3 + .../cloud/amazon/test_ec2_vpc_nat_gateway.py | 486 +++++++++++++++ 5 files changed, 910 insertions(+), 209 deletions(-) create mode 100644 test/integrations/group_vars/all.yml create mode 100644 test/integrations/roles/ec2_vpc_nat_gateway/tasks/main.yml create mode 100644 test/integrations/site.yml create mode 100644 test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py diff --git a/cloud/amazon/ec2_vpc_nat_gateway.py b/cloud/amazon/ec2_vpc_nat_gateway.py index 307a9646e5e..a333a9ac925 100644 --- a/cloud/amazon/ec2_vpc_nat_gateway.py +++ b/cloud/amazon/ec2_vpc_nat_gateway.py @@ -68,7 +68,7 @@ description: - Wait for operation to complete before returning required: false - default: true + default: false wait_timeout: description: - How many seconds to wait for an operation to complete before timing out @@ -94,32 +94,23 @@ # Note: These examples do not set authentication details, see the AWS Guide for details. - name: Create new nat gateway with client token - ec2_vpc_nat_gateway: - state: present - subnet_id: subnet-12345678 - eip_address: 52.1.1.1 - region: ap-southeast-2 - client_token: abcd-12345678 - register: new_nat_gateway - -- name: Create new nat gateway allocation-id ec2_vpc_nat_gateway: state: present subnet_id: subnet-12345678 - allocation_id: eipalloc-12345678 + eip_address: 52.1.1.1 region: ap-southeast-2 + client_token: abcd-12345678 register: new_nat_gateway -- name: Create new nat gateway with when condition +- name: Create new nat gateway using an allocation-id ec2_vpc_nat_gateway: state: present subnet_id: subnet-12345678 - eip_address: 52.1.1.1 + allocation_id: eipalloc-12345678 region: ap-southeast-2 register: new_nat_gateway - when: existing_nat_gateways.result == [] -- name: Create new nat gateway and wait for available status +- name: Create new nat gateway, using an eip address and wait for available status ec2_vpc_nat_gateway: state: present subnet_id: subnet-12345678 @@ -218,98 +209,98 @@ except ImportError: HAS_BOTO3 = False -import time import datetime +import random +import time + +from dateutil.tz import tzutc + +DRY_RUN_GATEWAYS = [ + { + "nat_gateway_id": "nat-123456789", + "subnet_id": "subnet-123456789", + "nat_gateway_addresses": [ + { + "public_ip": "55.55.55.55", + "network_interface_id": "eni-1234567", + "private_ip": "10.0.0.102", + "allocation_id": "eipalloc-1234567" + } + ], + "state": "available", + "create_time": "2016-03-05T05:19:20.282000+00:00", + "vpc_id": "vpc-12345678" + } +] +DRY_RUN_GATEWAY_UNCONVERTED = [ + { + 'VpcId': 'vpc-12345678', + 'State': 'available', + 'NatGatewayId': 'nat-123456789', + 'SubnetId': 'subnet-123456789', + 'NatGatewayAddresses': [ + { + 'PublicIp': '55.55.55.55', + 'NetworkInterfaceId': 'eni-1234567', + 'AllocationId': 'eipalloc-1234567', + 'PrivateIp': '10.0.0.102' + } + ], + 'CreateTime': datetime.datetime(2016, 3, 5, 5, 19, 20, 282000, tzinfo=tzutc()) + } +] + +DRY_RUN_ALLOCATION_UNCONVERTED = { + 'Addresses': [ + { + 'PublicIp': '55.55.55.55', + 'Domain': 'vpc', + 'AllocationId': 'eipalloc-1234567' + } + ] +} + +DRY_RUN_MSGS = 'DryRun Mode:' def convert_to_lower(data): """Convert all uppercase keys in dict with lowercase_ Args: data (dict): Dictionary with keys that have upper cases in them - Example.. NatGatewayAddresses == nat_gateway_addresses + Example.. FooBar == foo_bar if a val is of type datetime.datetime, it will be converted to the ISO 8601 Basic Usage: - >>> test = {'NatGatewaysAddresses': []} + >>> test = {'FooBar': []} >>> test = convert_to_lower(test) { - 'nat_gateways_addresses': [] + 'foo_bar': [] } Returns: Dictionary """ results = dict() - for key, val in data.items(): - key = re.sub('([A-Z]{1})', r'_\1', key).lower()[1:] - if isinstance(val, datetime.datetime): - results[key] = val.isoformat() - else: - results[key] = val - return results - -def formatted_nat_gw_output(data): - """Format the results of NatGateways into lowercase with underscores. - Args: - data (list): List of dictionaries with keys that have upper cases in - them. Example.. NatGatewayAddresses == nat_gateway_addresses - if a val is of type datetime.datetime, it will be converted to - the ISO 8601 - - Basic Usage: - >>> test = [ - { - 'VpcId': 'vpc-12345', - 'State': 'available', - 'NatGatewayId': 'nat-0b2f9f2ac3f51a653', - 'SubnetId': 'subnet-12345', - 'NatGatewayAddresses': [ - { - 'PublicIp': '52.52.52.52', - 'NetworkInterfaceId': 'eni-12345', - 'AllocationId': 'eipalloc-12345', - 'PrivateIp': '10.0.0.100' - } - ], - 'CreateTime': datetime.datetime(2016, 3, 5, 5, 19, 20, 282000, tzinfo=tzutc()) - } - ] - >>> test = formatted_nat_gw_output(test) - [ - { - 'nat_gateway_id': 'nat-0b2f9f2ac3f51a653', - 'subnet_id': 'subnet-12345', - 'nat_gateway_addresses': [ - { - 'public_ip': '52.52.52.52', - 'network_interface_id': 'eni-12345', - 'private_ip': '10.0.0.100', - 'allocation_id': 'eipalloc-12345' - } - ], - 'state': 'available', - 'create_time': '2016-03-05T05:19:20.282000+00:00', - 'vpc_id': 'vpc-12345' - } - ] - - Returns: - List - """ - results = list() - for gw in data: - output = dict() - ng_addresses = gw.pop('NatGatewayAddresses') - output = convert_to_lower(gw) - output['nat_gateway_addresses'] = [] - for address in ng_addresses: - gw_data = convert_to_lower(address) - output['nat_gateway_addresses'].append(gw_data) - results.append(output) - + if isinstance(data, dict): + for key, val in data.items(): + key = re.sub(r'(([A-Z]{1,3}){1})', r'_\1', key).lower() + if key[0] == '_': + key = key[1:] + if isinstance(val, datetime.datetime): + results[key] = val.isoformat() + elif isinstance(val, dict): + results[key] = convert_to_lower(val) + elif isinstance(val, list): + converted = list() + for item in val: + converted.append(convert_to_lower(item)) + results[key] = converted + else: + results[key] = val return results -def get_nat_gateways(client, subnet_id=None, nat_gateway_id=None): +def get_nat_gateways(client, subnet_id=None, nat_gateway_id=None, + states=None, check_mode=False): """Retrieve a list of NAT Gateways Args: client (botocore.client.EC2): Boto3 client @@ -317,29 +308,31 @@ def get_nat_gateways(client, subnet_id=None, nat_gateway_id=None): Kwargs: subnet_id (str): The subnet_id the nat resides in. nat_gateway_id (str): The Amazon nat id. + states (list): States available (pending, failed, available, deleting, and deleted) + default=None Basic Usage: >>> client = boto3.client('ec2') - >>> subnet_id = 'subnet-w4t12897' + >>> subnet_id = 'subnet-12345678' >>> get_nat_gateways(client, subnet_id) [ true, "", { - "nat_gateway_id": "nat-03835afb6e31df79b", - "subnet_id": "subnet-w4t12897", + "nat_gateway_id": "nat-123456789", + "subnet_id": "subnet-123456789", "nat_gateway_addresses": [ { - "public_ip": "52.87.29.36", - "network_interface_id": "eni-5579742d", + "public_ip": "55.55.55.55", + "network_interface_id": "eni-1234567", "private_ip": "10.0.0.102", - "allocation_id": "eipalloc-36014da3" + "allocation_id": "eipalloc-1234567" } ], "state": "deleted", "create_time": "2016-03-05T00:33:21.209000+00:00", "delete_time": "2016-03-05T00:36:37.329000+00:00", - "vpc_id": "vpc-w68571b5" + "vpc_id": "vpc-12345678" } Returns: @@ -348,6 +341,8 @@ def get_nat_gateways(client, subnet_id=None, nat_gateway_id=None): params = dict() err_msg = "" gateways_retrieved = False + if not states: + states = ['available', 'pending'] if nat_gateway_id: params['NatGatewayIds'] = [nat_gateway_id] else: @@ -355,19 +350,39 @@ def get_nat_gateways(client, subnet_id=None, nat_gateway_id=None): { 'Name': 'subnet-id', 'Values': [subnet_id] + }, + { + 'Name': 'state', + 'Values': states } ] try: - gateways = client.describe_nat_gateways(**params)['NatGateways'] - existing_gateways = formatted_nat_gw_output(gateways) - gateways_retrieved = True - except botocore.exceptions.ClientError as e: - err_msg = str(e) + if not check_mode: + gateways = client.describe_nat_gateways(**params)['NatGateways'] + existing_gateways = list() + if gateways: + for gw in gateways: + existing_gateways.append(convert_to_lower(gw)) + gateways_retrieved = True + else: + gateways_retrieved = True + existing_gateways = [] + if nat_gateway_id: + if DRY_RUN_GATEWAYS[0]['nat_gateway_id'] == nat_gateway_id: + existing_gateways = DRY_RUN_GATEWAYS + elif subnet_id: + if DRY_RUN_GATEWAYS[0]['subnet_id'] == subnet_id: + existing_gateways = DRY_RUN_GATEWAYS + err_msg = '{0} Retrieving gateways'.format(DRY_RUN_MSGS) + + except botocore.exceptions.ClientError, e: + err_msg = str(e) return gateways_retrieved, err_msg, existing_gateways -def wait_for_status(client, wait_timeout, nat_gateway_id, status): +def wait_for_status(client, wait_timeout, nat_gateway_id, status, + check_mode=False): """Wait for the Nat Gateway to reach a status Args: client (botocore.client.EC2): Boto3 client @@ -378,27 +393,27 @@ def wait_for_status(client, wait_timeout, nat_gateway_id, status): Basic Usage: >>> client = boto3.client('ec2') - >>> subnet_id = 'subnet-w4t12897' - >>> allocation_id = 'eipalloc-36014da3' + >>> subnet_id = 'subnet-12345678' + >>> allocation_id = 'eipalloc-12345678' >>> wait_for_status(client, subnet_id, allocation_id) [ true, "", { - "nat_gateway_id": "nat-03835afb6e31df79b", - "subnet_id": "subnet-w4t12897", + "nat_gateway_id": "nat-123456789", + "subnet_id": "subnet-1234567", "nat_gateway_addresses": [ { - "public_ip": "52.87.29.36", - "network_interface_id": "eni-5579742d", + "public_ip": "55.55.55.55", + "network_interface_id": "eni-1234567", "private_ip": "10.0.0.102", - "allocation_id": "eipalloc-36014da3" + "allocation_id": "eipalloc-12345678" } ], "state": "deleted", "create_time": "2016-03-05T00:33:21.209000+00:00", "delete_time": "2016-03-05T00:36:37.329000+00:00", - "vpc_id": "vpc-w68571b5" + "vpc_id": "vpc-12345677" } ] @@ -409,25 +424,40 @@ def wait_for_status(client, wait_timeout, nat_gateway_id, status): wait_timeout = time.time() + wait_timeout status_achieved = False nat_gateway = list() + states = ['pending', 'failed', 'available', 'deleting', 'deleted'] err_msg = "" while wait_timeout > time.time(): try: gws_retrieved, err_msg, nat_gateway = ( - get_nat_gateways(client, nat_gateway_id=nat_gateway_id) + get_nat_gateways( + client, nat_gateway_id=nat_gateway_id, + states=states, check_mode=check_mode + ) ) if gws_retrieved and nat_gateway: - if nat_gateway[0].get('state') == status: + nat_gateway = nat_gateway[0] + if check_mode: + nat_gateway['state'] = status + + if nat_gateway.get('state') == status: status_achieved = True break - elif nat_gateway[0].get('state') == 'failed': - err_msg = nat_gateway[0].get('failure_message') + elif nat_gateway.get('state') == 'failed': + err_msg = nat_gateway.get('failure_message') break + elif nat_gateway.get('state') == 'pending': + if nat_gateway.has_key('failure_message'): + err_msg = nat_gateway.get('failure_message') + status_achieved = False + break + else: time.sleep(polling_increment_secs) - except botocore.exceptions.ClientError as e: + + except botocore.exceptions.ClientError, e: err_msg = str(e) if not status_achieved: @@ -435,7 +465,8 @@ def wait_for_status(client, wait_timeout, nat_gateway_id, status): return status_achieved, err_msg, nat_gateway -def gateway_in_subnet_exists(client, subnet_id, allocation_id=None): +def gateway_in_subnet_exists(client, subnet_id, allocation_id=None, + check_mode=False): """Retrieve all NAT Gateways for a subnet. Args: subnet_id (str): The subnet_id the nat resides in. @@ -446,26 +477,26 @@ def gateway_in_subnet_exists(client, subnet_id, allocation_id=None): Basic Usage: >>> client = boto3.client('ec2') - >>> subnet_id = 'subnet-w4t12897' - >>> allocation_id = 'eipalloc-36014da3' + >>> subnet_id = 'subnet-1234567' + >>> allocation_id = 'eipalloc-1234567' >>> gateway_in_subnet_exists(client, subnet_id, allocation_id) ( [ { - "nat_gateway_id": "nat-03835afb6e31df79b", - "subnet_id": "subnet-w4t12897", + "nat_gateway_id": "nat-123456789", + "subnet_id": "subnet-123456789", "nat_gateway_addresses": [ { - "public_ip": "52.87.29.36", - "network_interface_id": "eni-5579742d", + "public_ip": "55.55.55.55", + "network_interface_id": "eni-1234567", "private_ip": "10.0.0.102", - "allocation_id": "eipalloc-36014da3" + "allocation_id": "eipalloc-1234567" } ], "state": "deleted", "create_time": "2016-03-05T00:33:21.209000+00:00", "delete_time": "2016-03-05T00:36:37.329000+00:00", - "vpc_id": "vpc-w68571b5" + "vpc_id": "vpc-1234567" } ], False @@ -476,18 +507,22 @@ def gateway_in_subnet_exists(client, subnet_id, allocation_id=None): """ allocation_id_exists = False gateways = [] - gws_retrieved, _, gws = get_nat_gateways(client, subnet_id) + states = ['available', 'pending'] + gws_retrieved, _, gws = ( + get_nat_gateways( + client, subnet_id, states=states, check_mode=check_mode + ) + ) if not gws_retrieved: return gateways, allocation_id_exists for gw in gws: for address in gw['nat_gateway_addresses']: - if gw.get('state') == 'available' or gw.get('state') == 'pending': - if allocation_id: - if address.get('allocation_id') == allocation_id: - allocation_id_exists = True - gateways.append(gw) - else: + if allocation_id: + if address.get('allocation_id') == allocation_id: + allocation_id_exists = True gateways.append(gw) + else: + gateways.append(gw) return gateways, allocation_id_exists @@ -511,22 +546,40 @@ def get_eip_allocation_id_by_address(client, eip_address, check_mode=False): Tuple (str, str) """ params = { - 'PublicIps': [eip_address] + 'PublicIps': [eip_address], } allocation_id = None err_msg = "" - if check_mode: - return "eipalloc-123456", err_msg try: - allocation = client.describe_addresses(**params)['Addresses'][0] - if not allocation.get('Domain') != 'vpc': - err_msg = ( - "EIP provided is a non-VPC EIP, please allocate a VPC scoped EIP" + if not check_mode: + allocations = client.describe_addresses(**params)['Addresses'] + if len(allocations) == 1: + allocation = allocations[0] + else: + allocation = None + else: + dry_run_eip = ( + DRY_RUN_ALLOCATION_UNCONVERTED['Addresses'][0]['PublicIp'] ) + if dry_run_eip == eip_address: + allocation = DRY_RUN_ALLOCATION_UNCONVERTED['Addresses'][0] + else: + allocation = None + if allocation: + if allocation.get('Domain') != 'vpc': + err_msg = ( + "EIP {0} is a non-VPC EIP, please allocate a VPC scoped EIP" + .format(eip_address) + ) + else: + allocation_id = allocation.get('AllocationId') else: - allocation_id = allocation.get('AllocationId') - except botocore.exceptions.ClientError as e: - err_msg = str(e) + err_msg = ( + "EIP {0} does not exist".format(eip_address) + ) + + except botocore.exceptions.ClientError, e: + err_msg = str(e) return allocation_id, err_msg @@ -547,20 +600,28 @@ def allocate_eip_address(client, check_mode=False): Returns: Tuple (bool, str) """ - if check_mode: - return True, "eipalloc-123456" - ip_allocated = False + new_eip = None + err_msg = '' params = { - 'Domain': 'vpc' + 'Domain': 'vpc', } try: - new_eip = client.allocate_address(**params) - ip_allocated = True - except botocore.exceptions.ClientError: - pass + if check_mode: + ip_allocated = True + random_numbers = ( + ''.join(str(x) for x in random.sample(range(0, 9), 7)) + ) + new_eip = 'eipalloc-{0}'.format(random_numbers) + else: + new_eip = client.allocate_address(**params)['AllocationId'] + ip_allocated = True + err_msg = 'eipalloc id {0} created'.format(new_eip) + + except botocore.exceptions.ClientError, e: + err_msg = str(e) - return ip_allocated, new_eip['AllocationId'] + return ip_allocated, err_msg, new_eip def release_address(client, allocation_id, check_mode=False): """Release an EIP from your EIP Pool @@ -597,7 +658,8 @@ def release_address(client, allocation_id, check_mode=False): return ip_released def create(client, subnet_id, allocation_id, client_token=None, - wait=False, wait_timeout=0, if_exist_do_not_create=False): + wait=False, wait_timeout=0, if_exist_do_not_create=False, + check_mode=False): """Create an Amazon NAT Gateway. Args: client (botocore.client.EC2): Boto3 client @@ -617,27 +679,27 @@ def create(client, subnet_id, allocation_id, client_token=None, Basic Usage: >>> client = boto3.client('ec2') - >>> subnet_id = 'subnet-w4t12897' - >>> allocation_id = 'eipalloc-36014da3' + >>> subnet_id = 'subnet-1234567' + >>> allocation_id = 'eipalloc-1234567' >>> create(client, subnet_id, allocation_id, if_exist_do_not_create=True, wait=True, wait_timeout=500) [ true, "", { - "nat_gateway_id": "nat-03835afb6e31df79b", - "subnet_id": "subnet-w4t12897", + "nat_gateway_id": "nat-123456789", + "subnet_id": "subnet-1234567", "nat_gateway_addresses": [ { - "public_ip": "52.87.29.36", - "network_interface_id": "eni-5579742d", + "public_ip": "55.55.55.55", + "network_interface_id": "eni-1234567", "private_ip": "10.0.0.102", - "allocation_id": "eipalloc-36014da3" + "allocation_id": "eipalloc-1234567" } ], "state": "deleted", "create_time": "2016-03-05T00:33:21.209000+00:00", "delete_time": "2016-03-05T00:36:37.329000+00:00", - "vpc_id": "vpc-w68571b5" + "vpc_id": "vpc-1234567" } ] @@ -650,6 +712,7 @@ def create(client, subnet_id, allocation_id, client_token=None, } request_time = datetime.datetime.utcnow() changed = False + success = False token_provided = False err_msg = "" @@ -658,17 +721,34 @@ def create(client, subnet_id, allocation_id, client_token=None, params['ClientToken'] = client_token try: - result = client.create_nat_gateway(**params)["NatGateway"] + if not check_mode: + result = client.create_nat_gateway(**params)["NatGateway"] + else: + result = DRY_RUN_GATEWAY_UNCONVERTED[0] + result['CreateTime'] = datetime.datetime.utcnow() + result['NatGatewayAddresses'][0]['AllocationId'] = allocation_id + result['SubnetId'] = subnet_id + + success = True changed = True create_time = result['CreateTime'].replace(tzinfo=None) if token_provided and (request_time > create_time): changed = False elif wait: - status_achieved, err_msg, result = ( + success, err_msg, result = ( wait_for_status( - client, wait_timeout, result['NatGatewayId'], 'available' + client, wait_timeout, result['NatGatewayId'], 'available', + check_mode=check_mode ) ) + if success: + err_msg = ( + 'Nat gateway {0} created'.format(result['nat_gateway_id']) + ) + if check_mode: + result['nat_gateway_addresses'][0]['allocation_id'] = allocation_id + result['subnet_id'] = subnet_id + except botocore.exceptions.ClientError as e: if "IdempotentParameterMismatch" in e.message: err_msg = ( @@ -676,8 +756,10 @@ def create(client, subnet_id, allocation_id, client_token=None, ) else: err_msg = str(e) + success = False + changed = False - return changed, err_msg, result + return success, changed, err_msg, result def pre_create(client, subnet_id, allocation_id=None, eip_address=None, if_exist_do_not_create=False, wait=False, wait_timeout=0, @@ -729,45 +811,74 @@ def pre_create(client, subnet_id, allocation_id=None, eip_address=None, ] Returns: - Tuple (bool, str, list) + Tuple (bool, bool, str, list) """ + success = False changed = False err_msg = "" results = list() if not allocation_id and not eip_address: existing_gateways, allocation_id_exists = ( - gateway_in_subnet_exists(client, subnet_id) + gateway_in_subnet_exists(client, subnet_id, check_mode=check_mode) ) + if len(existing_gateways) > 0 and if_exist_do_not_create: - results = existing_gateways - return changed, err_msg, results + success = True + changed = False + results = existing_gateways[0] + err_msg = ( + 'Nat Gateway {0} already exists in subnet_id {1}' + .format( + existing_gateways[0]['nat_gateway_id'], subnet_id + ) + ) + return success, changed, err_msg, results else: - _, allocation_id = allocate_eip_address(client) + success, err_msg, allocation_id = ( + allocate_eip_address(client, check_mode=check_mode) + ) + if not success: + return success, 'False', err_msg, dict() elif eip_address or allocation_id: if eip_address and not allocation_id: - allocation_id = ( + allocation_id, err_msg = ( get_eip_allocation_id_by_address( client, eip_address, check_mode=check_mode ) ) + if not allocation_id: + success = False + changed = False + return success, changed, err_msg, dict() existing_gateways, allocation_id_exists = ( - gateway_in_subnet_exists(client, subnet_id, allocation_id) + gateway_in_subnet_exists( + client, subnet_id, allocation_id, check_mode=check_mode + ) ) if len(existing_gateways) > 0 and (allocation_id_exists or if_exist_do_not_create): - results = existing_gateways - return changed, err_msg, results + success = True + changed = False + results = existing_gateways[0] + err_msg = ( + 'Nat Gateway {0} already exists in subnet_id {1}' + .format( + existing_gateways[0]['nat_gateway_id'], subnet_id + ) + ) + return success, changed, err_msg, results - changed, err_msg, results = create( + success, changed, err_msg, results = create( client, subnet_id, allocation_id, client_token, - wait, wait_timeout,if_exist_do_not_create + wait, wait_timeout, if_exist_do_not_create, check_mode=check_mode ) - return changed, err_msg, results + return success, changed, err_msg, results -def remove(client, nat_gateway_id, wait=False, wait_timeout=0, release_eip=False): +def remove(client, nat_gateway_id, wait=False, wait_timeout=0, + release_eip=False, check_mode=False): """Delete an Amazon NAT Gateway. Args: client (botocore.client.EC2): Boto3 client @@ -809,47 +920,71 @@ def remove(client, nat_gateway_id, wait=False, wait_timeout=0, release_eip=False params = { 'NatGatewayId': nat_gateway_id } + success = False changed = False err_msg = "" results = list() + states = ['pending', 'available' ] try: - exist, _, gw = get_nat_gateways(client, nat_gateway_id=nat_gateway_id) + exist, _, gw = ( + get_nat_gateways( + client, nat_gateway_id=nat_gateway_id, + states=states, check_mode=check_mode + ) + ) if exist and len(gw) == 1: results = gw[0] - result = client.delete_nat_gateway(**params) - result = convert_to_lower(result) + if not check_mode: + client.delete_nat_gateway(**params) + allocation_id = ( results['nat_gateway_addresses'][0]['allocation_id'] ) changed = True + success = True + err_msg = ( + 'Nat gateway {0} is in a deleting state. Delete was successfull' + .format(nat_gateway_id) + ) + + if wait: + status_achieved, err_msg, results = ( + wait_for_status( + client, wait_timeout, nat_gateway_id, 'deleted', + check_mode=check_mode + ) + ) + if status_achieved: + err_msg = ( + 'Nat gateway {0} was deleted successfully' + .format(nat_gateway_id) + ) + except botocore.exceptions.ClientError as e: err_msg = str(e) - if wait and not err_msg: - status_achieved, err_msg, results = ( - wait_for_status(client, wait_timeout, nat_gateway_id, 'deleted') - ) - if release_eip: - eip_released = release_address(client, allocation_id) + eip_released = ( + release_address(client, allocation_id, check_mode=check_mode) + ) if not eip_released: err_msg = "Failed to release eip %s".format(allocation_id) - return changed, err_msg, results + return success, changed, err_msg, results def main(): argument_spec = ec2_argument_spec() argument_spec.update(dict( - subnet_id=dict(), - eip_address=dict(), - allocation_id=dict(), + subnet_id=dict(type='str'), + eip_address=dict(type='str'), + allocation_id=dict(type='str'), if_exist_do_not_create=dict(type='bool', default=False), state=dict(default='present', choices=['present', 'absent']), - wait=dict(type='bool', default=True), + wait=dict(type='bool', default=False), wait_timeout=dict(type='int', default=320, required=False), release_eip=dict(type='bool', default=False), - nat_gateway_id=dict(), - client_token=dict(), + nat_gateway_id=dict(type='str'), + client_token=dict(type='str'), ) ) module = AnsibleModule( @@ -890,40 +1025,40 @@ def main(): module.fail_json(msg="Boto3 Client Error - " + str(e.msg)) changed = False - err_msg = None + err_msg = '' #Ensure resource is present if state == 'present': if not subnet_id: module.fail_json(msg='subnet_id is required for creation') - elif check_mode: - changed = True - results = 'Would have created NAT Gateway if not in check mode' - else: - changed, err_msg, results = ( - pre_create( - client, subnet_id, allocation_id, eip_address, - if_exist_do_not_create, wait, wait_timeout, - client_token - ) + success, changed, err_msg, results = ( + pre_create( + client, subnet_id, allocation_id, eip_address, + if_exist_do_not_create, wait, wait_timeout, + client_token, check_mode=check_mode ) + ) else: if not nat_gateway_id: module.fail_json(msg='nat_gateway_id is required for removal') - elif check_mode: - changed = True - results = 'Would have deleted NAT Gateway if not in check mode' else: - changed, err_msg, results = ( - remove(client, nat_gateway_id, wait, wait_timeout, release_eip) + success, changed, err_msg, results = ( + remove( + client, nat_gateway_id, wait, wait_timeout, release_eip, + check_mode=check_mode + ) ) - if err_msg: - module.fail_json(msg=err_msg) + if not success: + module.exit_json( + msg=err_msg, success=success, changed=changed + ) else: - module.exit_json(changed=changed, **results[0]) + module.exit_json( + msg=err_msg, success=success, changed=changed, **results + ) # import module snippets from ansible.module_utils.basic import * diff --git a/test/integrations/group_vars/all.yml b/test/integrations/group_vars/all.yml new file mode 100644 index 00000000000..8a3ccba7168 --- /dev/null +++ b/test/integrations/group_vars/all.yml @@ -0,0 +1 @@ +test_subnet_id: 'subnet-123456789' diff --git a/test/integrations/roles/ec2_vpc_nat_gateway/tasks/main.yml b/test/integrations/roles/ec2_vpc_nat_gateway/tasks/main.yml new file mode 100644 index 00000000000..f5ad5f50fc8 --- /dev/null +++ b/test/integrations/roles/ec2_vpc_nat_gateway/tasks/main.yml @@ -0,0 +1,76 @@ +- name: Launching NAT Gateway and allocate a new eip. + ec2_vpc_nat_gateway: + region: us-west-2 + state: present + subnet_id: "{{ test_subnet_id }}" + wait: yes + wait_timeout: 600 + register: nat + +- debug: + var: nat +- fail: + msg: "Failed to create" + when: '"{{ nat["changed"] }}" != "True"' + +- name: Launch a new gateway only if one does not exist already in this subnet. + ec2_vpc_nat_gateway: + if_exist_do_not_create: yes + region: us-west-2 + state: present + subnet_id: "{{ test_subnet_id }}" + wait: yes + wait_timeout: 600 + register: nat_idempotent + +- debug: + var: nat_idempotent +- fail: + msg: "Failed to be idempotent" + when: '"{{ nat_idempotent["changed"] }}" == "True"' + +- name: Launching NAT Gateway and allocate a new eip even if one already exists in the subnet. + ec2_vpc_nat_gateway: + region: us-west-2 + state: present + subnet_id: "{{ test_subnet_id }}" + wait: yes + wait_timeout: 600 + register: new_nat + +- debug: + var: new_nat +- fail: + msg: "Failed to create" + when: '"{{ new_nat["changed"] }}" != "True"' + +- name: Launching NAT Gateway with allocation id, this call is idempotent and will not create anything. + ec2_vpc_nat_gateway: + allocation_id: eipalloc-1234567 + region: us-west-2 + state: present + subnet_id: "{{ test_subnet_id }}" + wait: yes + wait_timeout: 600 + register: nat_with_eipalloc + +- debug: + var: nat_with_eipalloc +- fail: + msg: 'Failed to be idempotent.' + when: '"{{ nat_with_eipalloc["changed"] }}" == "True"' + +- name: Delete the 1st nat gateway and do not wait for it to finish + ec2_vpc_nat_gateway: + region: us-west-2 + nat_gateway_id: "{{ nat.nat_gateway_id }}" + state: absent + +- name: Delete the nat_with_eipalloc and release the eip + ec2_vpc_nat_gateway: + region: us-west-2 + nat_gateway_id: "{{ new_nat.nat_gateway_id }}" + release_eip: yes + state: absent + wait: yes + wait_timeout: 600 diff --git a/test/integrations/site.yml b/test/integrations/site.yml new file mode 100644 index 00000000000..62416726ebc --- /dev/null +++ b/test/integrations/site.yml @@ -0,0 +1,3 @@ +- hosts: 127.0.0.1 + roles: + - { role: ec2_vpc_nat_gateway } diff --git a/test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py b/test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py new file mode 100644 index 00000000000..e2d3573499e --- /dev/null +++ b/test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py @@ -0,0 +1,486 @@ +#!/usr/bin/python + +import boto3 +import unittest + +from collections import namedtuple +from ansible.parsing.dataloader import DataLoader +from ansible.vars import VariableManager +from ansible.inventory import Inventory +from ansible.playbook.play import Play +from ansible.executor.task_queue_manager import TaskQueueManager + +import cloud.amazon.ec2_vpc_nat_gateway as ng + +Options = ( + namedtuple( + 'Options', [ + 'connection', 'module_path', 'forks', 'become', 'become_method', + 'become_user', 'remote_user', 'private_key_file', 'ssh_common_args', + 'sftp_extra_args', 'scp_extra_args', 'ssh_extra_args', 'verbosity', + 'check' + ] + ) +) +# initialize needed objects +variable_manager = VariableManager() +loader = DataLoader() +options = ( + Options( + connection='local', + module_path='cloud/amazon', + forks=1, become=None, become_method=None, become_user=None, check=True, + remote_user=None, private_key_file=None, ssh_common_args=None, + sftp_extra_args=None, scp_extra_args=None, ssh_extra_args=None, + verbosity=3 + ) +) +passwords = dict(vault_pass='') + +aws_region = 'us-west-2' + +# create inventory and pass to var manager +inventory = Inventory(loader=loader, variable_manager=variable_manager, host_list='localhost') +variable_manager.set_inventory(inventory) + +def run(play): + tqm = None + results = None + try: + tqm = TaskQueueManager( + inventory=inventory, + variable_manager=variable_manager, + loader=loader, + options=options, + passwords=passwords, + stdout_callback='default', + ) + results = tqm.run(play) + finally: + if tqm is not None: + tqm.cleanup() + return tqm, results + +class AnsibleVpcNatGatewayTasks(unittest.TestCase): + + def test_create_gateway_using_allocation_id(self): + play_source = dict( + name = "Create new nat gateway with eip allocation-id", + hosts = 'localhost', + gather_facts = 'no', + tasks = [ + dict( + action=dict( + module='ec2_vpc_nat_gateway', + args=dict( + subnet_id='subnet-12345678', + allocation_id='eipalloc-12345678', + wait='yes', + region=aws_region, + ) + ), + register='nat_gateway', + ), + dict( + action=dict( + module='debug', + args=dict( + msg='{{nat_gateway}}' + ) + ) + ) + ] + ) + play = Play().load(play_source, variable_manager=variable_manager, loader=loader) + tqm, results = run(play) + self.failUnless(tqm._stats.ok['localhost'] == 2) + self.failUnless(tqm._stats.changed['localhost'] == 1) + + def test_create_gateway_using_allocation_id_idempotent(self): + play_source = dict( + name = "Create new nat gateway with eip allocation-id", + hosts = 'localhost', + gather_facts = 'no', + tasks = [ + dict( + action=dict( + module='ec2_vpc_nat_gateway', + args=dict( + subnet_id='subnet-123456789', + allocation_id='eipalloc-1234567', + wait='yes', + region=aws_region, + ) + ), + register='nat_gateway', + ), + dict( + action=dict( + module='debug', + args=dict( + msg='{{nat_gateway}}' + ) + ) + ) + ] + ) + play = Play().load(play_source, variable_manager=variable_manager, loader=loader) + tqm, results = run(play) + self.failUnless(tqm._stats.ok['localhost'] == 2) + self.assertFalse(tqm._stats.changed.has_key('localhost')) + + def test_create_gateway_using_eip_address(self): + play_source = dict( + name = "Create new nat gateway with eip address", + hosts = 'localhost', + gather_facts = 'no', + tasks = [ + dict( + action=dict( + module='ec2_vpc_nat_gateway', + args=dict( + subnet_id='subnet-12345678', + eip_address='55.55.55.55', + wait='yes', + region=aws_region, + ) + ), + register='nat_gateway', + ), + dict( + action=dict( + module='debug', + args=dict( + msg='{{nat_gateway}}' + ) + ) + ) + ] + ) + play = Play().load(play_source, variable_manager=variable_manager, loader=loader) + tqm, results = run(play) + self.failUnless(tqm._stats.ok['localhost'] == 2) + self.failUnless(tqm._stats.changed['localhost'] == 1) + + def test_create_gateway_using_eip_address_idempotent(self): + play_source = dict( + name = "Create new nat gateway with eip address", + hosts = 'localhost', + gather_facts = 'no', + tasks = [ + dict( + action=dict( + module='ec2_vpc_nat_gateway', + args=dict( + subnet_id='subnet-123456789', + eip_address='55.55.55.55', + wait='yes', + region=aws_region, + ) + ), + register='nat_gateway', + ), + dict( + action=dict( + module='debug', + args=dict( + msg='{{nat_gateway}}' + ) + ) + ) + ] + ) + play = Play().load(play_source, variable_manager=variable_manager, loader=loader) + tqm, results = run(play) + self.failUnless(tqm._stats.ok['localhost'] == 2) + self.assertFalse(tqm._stats.changed.has_key('localhost')) + + def test_create_gateway_in_subnet_only_if_one_does_not_exist_already(self): + play_source = dict( + name = "Create new nat gateway only if one does not exist already", + hosts = 'localhost', + gather_facts = 'no', + tasks = [ + dict( + action=dict( + module='ec2_vpc_nat_gateway', + args=dict( + if_exist_do_not_create='yes', + subnet_id='subnet-123456789', + wait='yes', + region=aws_region, + ) + ), + register='nat_gateway', + ), + dict( + action=dict( + module='debug', + args=dict( + msg='{{nat_gateway}}' + ) + ) + ) + ] + ) + play = Play().load(play_source, variable_manager=variable_manager, loader=loader) + tqm, results = run(play) + self.failUnless(tqm._stats.ok['localhost'] == 2) + self.assertFalse(tqm._stats.changed.has_key('localhost')) + + def test_delete_gateway(self): + play_source = dict( + name = "Delete Nat Gateway", + hosts = 'localhost', + gather_facts = 'no', + tasks = [ + dict( + action=dict( + module='ec2_vpc_nat_gateway', + args=dict( + nat_gateway_id='nat-123456789', + state='absent', + wait='yes', + region=aws_region, + ) + ), + register='nat_gateway', + ), + dict( + action=dict( + module='debug', + args=dict( + msg='{{nat_gateway}}' + ) + ) + ) + ] + ) + play = Play().load(play_source, variable_manager=variable_manager, loader=loader) + tqm, results = run(play) + self.failUnless(tqm._stats.ok['localhost'] == 2) + self.assertTrue(tqm._stats.changed.has_key('localhost')) + +class AnsibleEc2VpcNatGatewayFunctions(unittest.TestCase): + + def test_convert_to_lower(self): + example = ng.DRY_RUN_GATEWAY_UNCONVERTED + converted_example = ng.convert_to_lower(example[0]) + keys = converted_example.keys() + keys.sort() + for i in range(len(keys)): + if i == 0: + self.assertEqual(keys[i], 'create_time') + if i == 1: + self.assertEqual(keys[i], 'nat_gateway_addresses') + gw_addresses_keys = converted_example[keys[i]][0].keys() + gw_addresses_keys.sort() + for j in range(len(gw_addresses_keys)): + if j == 0: + self.assertEqual(gw_addresses_keys[j], 'allocation_id') + if j == 1: + self.assertEqual(gw_addresses_keys[j], 'network_interface_id') + if j == 2: + self.assertEqual(gw_addresses_keys[j], 'private_ip') + if j == 3: + self.assertEqual(gw_addresses_keys[j], 'public_ip') + if i == 2: + self.assertEqual(keys[i], 'nat_gateway_id') + if i == 3: + self.assertEqual(keys[i], 'state') + if i == 4: + self.assertEqual(keys[i], 'subnet_id') + if i == 5: + self.assertEqual(keys[i], 'vpc_id') + + def test_get_nat_gateways(self): + client = boto3.client('ec2', region_name=aws_region) + success, err_msg, stream = ( + ng.get_nat_gateways(client, 'subnet-123456789', check_mode=True) + ) + should_return = ng.DRY_RUN_GATEWAYS + self.assertTrue(success) + self.assertEqual(stream, should_return) + + def test_get_nat_gateways_no_gateways_found(self): + client = boto3.client('ec2', region_name=aws_region) + success, err_msg, stream = ( + ng.get_nat_gateways(client, 'subnet-1234567', check_mode=True) + ) + self.assertTrue(success) + self.assertEqual(stream, []) + + def test_wait_for_status(self): + client = boto3.client('ec2', region_name=aws_region) + success, err_msg, gws = ( + ng.wait_for_status( + client, 5, 'nat-123456789', 'available', check_mode=True + ) + ) + should_return = ng.DRY_RUN_GATEWAYS[0] + self.assertTrue(success) + self.assertEqual(gws, should_return) + + def test_wait_for_status_to_timeout(self): + client = boto3.client('ec2', region_name=aws_region) + success, err_msg, gws = ( + ng.wait_for_status( + client, 2, 'nat-12345678', 'available', check_mode=True + ) + ) + self.assertFalse(success) + self.assertEqual(gws, []) + + def test_gateway_in_subnet_exists_with_allocation_id(self): + client = boto3.client('ec2', region_name=aws_region) + gws, err_msg = ( + ng.gateway_in_subnet_exists( + client, 'subnet-123456789', 'eipalloc-1234567', check_mode=True + ) + ) + should_return = ng.DRY_RUN_GATEWAYS + self.assertEqual(gws, should_return) + + def test_gateway_in_subnet_exists_with_allocation_id_does_not_exist(self): + client = boto3.client('ec2', region_name=aws_region) + gws, err_msg = ( + ng.gateway_in_subnet_exists( + client, 'subnet-123456789', 'eipalloc-123', check_mode=True + ) + ) + should_return = list() + self.assertEqual(gws, should_return) + + def test_gateway_in_subnet_exists_without_allocation_id(self): + client = boto3.client('ec2', region_name=aws_region) + gws, err_msg = ( + ng.gateway_in_subnet_exists( + client, 'subnet-123456789', check_mode=True + ) + ) + should_return = ng.DRY_RUN_GATEWAYS + self.assertEqual(gws, should_return) + + def test_get_eip_allocation_id_by_address(self): + client = boto3.client('ec2', region_name=aws_region) + allocation_id, _ = ( + ng.get_eip_allocation_id_by_address( + client, '55.55.55.55', check_mode=True + ) + ) + should_return = 'eipalloc-1234567' + self.assertEqual(allocation_id, should_return) + + def test_get_eip_allocation_id_by_address_does_not_exist(self): + client = boto3.client('ec2', region_name=aws_region) + allocation_id, err_msg = ( + ng.get_eip_allocation_id_by_address( + client, '52.52.52.52', check_mode=True + ) + ) + self.assertEqual(err_msg, 'EIP 52.52.52.52 does not exist') + self.assertIsNone(allocation_id) + + def test_allocate_eip_address(self): + client = boto3.client('ec2', region_name=aws_region) + success, err_msg, eip_id = ( + ng.allocate_eip_address( + client, check_mode=True + ) + ) + self.assertTrue(success) + + def test_release_address(self): + client = boto3.client('ec2', region_name=aws_region) + success = ( + ng.release_address( + client, 'eipalloc-1234567', check_mode=True + ) + ) + self.assertTrue(success) + + def test_create(self): + client = boto3.client('ec2', region_name=aws_region) + success, changed, err_msg, results = ( + ng.create( + client, 'subnet-123456', 'eipalloc-1234567', check_mode=True + ) + ) + self.assertTrue(success) + self.assertTrue(changed) + + def test_pre_create(self): + client = boto3.client('ec2', region_name=aws_region) + success, changed, err_msg, results = ( + ng.pre_create( + client, 'subnet-123456', check_mode=True + ) + ) + self.assertTrue(success) + self.assertTrue(changed) + + def test_pre_create_idemptotent_with_allocation_id(self): + client = boto3.client('ec2', region_name=aws_region) + success, changed, err_msg, results = ( + ng.pre_create( + client, 'subnet-123456789', allocation_id='eipalloc-1234567', check_mode=True + ) + ) + self.assertTrue(success) + self.assertFalse(changed) + + def test_pre_create_idemptotent_with_eip_address(self): + client = boto3.client('ec2', region_name=aws_region) + success, changed, err_msg, results = ( + ng.pre_create( + client, 'subnet-123456789', eip_address='55.55.55.55', check_mode=True + ) + ) + self.assertTrue(success) + self.assertFalse(changed) + + def test_pre_create_idemptotent_if_exist_do_not_create(self): + client = boto3.client('ec2', region_name=aws_region) + success, changed, err_msg, results = ( + ng.pre_create( + client, 'subnet-123456789', if_exist_do_not_create=True, check_mode=True + ) + ) + self.assertTrue(success) + self.assertFalse(changed) + + def test_delete(self): + client = boto3.client('ec2', region_name=aws_region) + success, changed, err_msg, _ = ( + ng.remove( + client, 'nat-123456789', check_mode=True + ) + ) + self.assertTrue(success) + self.assertTrue(changed) + + def test_delete_and_release_ip(self): + client = boto3.client('ec2', region_name=aws_region) + success, changed, err_msg, _ = ( + ng.remove( + client, 'nat-123456789', release_eip=True, check_mode=True + ) + ) + self.assertTrue(success) + self.assertTrue(changed) + + def test_delete_if_does_not_exist(self): + client = boto3.client('ec2', region_name=aws_region) + success, changed, err_msg, _ = ( + ng.remove( + client, 'nat-12345', check_mode=True + ) + ) + self.assertFalse(success) + self.assertFalse(changed) + +def main(): + unittest.main() + +if __name__ == '__main__': + main() From aa189b8d98bdb29913a38e7546b336194d84979e Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Fri, 8 Apr 2016 14:29:59 -0700 Subject: [PATCH 1909/2522] Added default result of None in catch statement --- cloud/amazon/ec2_vpc_nat_gateway.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cloud/amazon/ec2_vpc_nat_gateway.py b/cloud/amazon/ec2_vpc_nat_gateway.py index a333a9ac925..3d11e372463 100644 --- a/cloud/amazon/ec2_vpc_nat_gateway.py +++ b/cloud/amazon/ec2_vpc_nat_gateway.py @@ -264,6 +264,7 @@ def convert_to_lower(data): """Convert all uppercase keys in dict with lowercase_ + Args: data (dict): Dictionary with keys that have upper cases in them Example.. FooBar == foo_bar @@ -758,6 +759,7 @@ def create(client, subnet_id, allocation_id, client_token=None, err_msg = str(e) success = False changed = False + result = None return success, changed, err_msg, result From 79ea5532003ac0c1d5010020cbeb44329f24ded5 Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Fri, 8 Apr 2016 17:39:32 -0700 Subject: [PATCH 1910/2522] Module requires boto due to ec2.py --- cloud/amazon/ec2_vpc_nat_gateway.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud/amazon/ec2_vpc_nat_gateway.py b/cloud/amazon/ec2_vpc_nat_gateway.py index 3d11e372463..46b75ce3090 100644 --- a/cloud/amazon/ec2_vpc_nat_gateway.py +++ b/cloud/amazon/ec2_vpc_nat_gateway.py @@ -205,6 +205,7 @@ try: import botocore import boto3 + import boto HAS_BOTO3 = True except ImportError: HAS_BOTO3 = False From 2e42c7244762eaae2035ffca84ae90e03fb3fe96 Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Fri, 6 May 2016 13:28:26 -0700 Subject: [PATCH 1911/2522] version bump --- cloud/amazon/ec2_vpc_nat_gateway.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_vpc_nat_gateway.py b/cloud/amazon/ec2_vpc_nat_gateway.py index 46b75ce3090..69199f90537 100644 --- a/cloud/amazon/ec2_vpc_nat_gateway.py +++ b/cloud/amazon/ec2_vpc_nat_gateway.py @@ -20,7 +20,7 @@ short_description: Manage AWS VPC NAT Gateways description: - Ensure the state of AWS VPC NAT Gateways based on their id, allocation and subnet ids. -version_added: "2.1" +version_added: "2.2" requirements: [boto3, botocore] options: state: From 4e8e38f631dcb18396379abb01a30245a9f8a7ee Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Fri, 6 May 2016 13:39:36 -0700 Subject: [PATCH 1912/2522] remove boto --- cloud/amazon/ec2_vpc_nat_gateway.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cloud/amazon/ec2_vpc_nat_gateway.py b/cloud/amazon/ec2_vpc_nat_gateway.py index 69199f90537..ca920c52b28 100644 --- a/cloud/amazon/ec2_vpc_nat_gateway.py +++ b/cloud/amazon/ec2_vpc_nat_gateway.py @@ -205,7 +205,6 @@ try: import botocore import boto3 - import boto HAS_BOTO3 = True except ImportError: HAS_BOTO3 = False From 8af106378506e53f65a94113aef965a1cce9a57c Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Mon, 25 Jul 2016 14:08:40 -0700 Subject: [PATCH 1913/2522] Make sure to catch if no gateways exist --- cloud/amazon/ec2_vpc_nat_gateway.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cloud/amazon/ec2_vpc_nat_gateway.py b/cloud/amazon/ec2_vpc_nat_gateway.py index ca920c52b28..1b574840f96 100644 --- a/cloud/amazon/ec2_vpc_nat_gateway.py +++ b/cloud/amazon/ec2_vpc_nat_gateway.py @@ -342,6 +342,7 @@ def get_nat_gateways(client, subnet_id=None, nat_gateway_id=None, params = dict() err_msg = "" gateways_retrieved = False + existing_gateways = list() if not states: states = ['available', 'pending'] if nat_gateway_id: @@ -361,14 +362,12 @@ def get_nat_gateways(client, subnet_id=None, nat_gateway_id=None, try: if not check_mode: gateways = client.describe_nat_gateways(**params)['NatGateways'] - existing_gateways = list() if gateways: for gw in gateways: existing_gateways.append(convert_to_lower(gw)) gateways_retrieved = True else: gateways_retrieved = True - existing_gateways = [] if nat_gateway_id: if DRY_RUN_GATEWAYS[0]['nat_gateway_id'] == nat_gateway_id: existing_gateways = DRY_RUN_GATEWAYS From ab62c644bc9f91f025b7e77480b0258d55bbb307 Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Mon, 25 Jul 2016 14:19:35 -0700 Subject: [PATCH 1914/2522] updated catch statement to pass test (as e) --- cloud/amazon/ec2_vpc_nat_gateway.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cloud/amazon/ec2_vpc_nat_gateway.py b/cloud/amazon/ec2_vpc_nat_gateway.py index 1b574840f96..1364aa7875f 100644 --- a/cloud/amazon/ec2_vpc_nat_gateway.py +++ b/cloud/amazon/ec2_vpc_nat_gateway.py @@ -376,7 +376,7 @@ def get_nat_gateways(client, subnet_id=None, nat_gateway_id=None, existing_gateways = DRY_RUN_GATEWAYS err_msg = '{0} Retrieving gateways'.format(DRY_RUN_MSGS) - except botocore.exceptions.ClientError, e: + except botocore.exceptions.ClientError as e: err_msg = str(e) return gateways_retrieved, err_msg, existing_gateways @@ -457,7 +457,7 @@ def wait_for_status(client, wait_timeout, nat_gateway_id, status, else: time.sleep(polling_increment_secs) - except botocore.exceptions.ClientError, e: + except botocore.exceptions.ClientError as e: err_msg = str(e) if not status_achieved: @@ -578,7 +578,7 @@ def get_eip_allocation_id_by_address(client, eip_address, check_mode=False): "EIP {0} does not exist".format(eip_address) ) - except botocore.exceptions.ClientError, e: + except botocore.exceptions.ClientError as e: err_msg = str(e) return allocation_id, err_msg @@ -618,7 +618,7 @@ def allocate_eip_address(client, check_mode=False): ip_allocated = True err_msg = 'eipalloc id {0} created'.format(new_eip) - except botocore.exceptions.ClientError, e: + except botocore.exceptions.ClientError as e: err_msg = str(e) return ip_allocated, err_msg, new_eip @@ -1022,7 +1022,7 @@ def main(): region=region, endpoint=ec2_url, **aws_connect_kwargs ) ) - except botocore.exceptions.ClientError, e: + except botocore.exceptions.ClientError as e: module.fail_json(msg="Boto3 Client Error - " + str(e.msg)) changed = False From e9bbd3a4038f9572aa068096a84411b40a5d33f6 Mon Sep 17 00:00:00 2001 From: "Thierno IB. BARRY" Date: Thu, 4 Aug 2016 09:09:45 +0200 Subject: [PATCH 1915/2522] Add kibana_plugin module (#2621) * kibana_plugin: add the kibana_plugin module * kibana_plugin: update doc * kibana_plugin: add check mode and fix few coding style issues * kibana_plugin: use return instead conditional statement for check mode --- packaging/kibana_plugin.py | 237 +++++++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 packaging/kibana_plugin.py diff --git a/packaging/kibana_plugin.py b/packaging/kibana_plugin.py new file mode 100644 index 00000000000..f0ffcd9ddf7 --- /dev/null +++ b/packaging/kibana_plugin.py @@ -0,0 +1,237 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +Ansible module to manage elasticsearch shield role +(c) 2016, Thierno IB. BARRY @barryib +Sponsored by Polyconseil http://polyconseil.fr. + +This file is part of Ansible + +Ansible is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Ansible is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +You should have received a copy of the GNU General Public License +along with Ansible. If not, see . +""" + +import os + +DOCUMENTATION = ''' +--- +module: kibana_plugin +short_description: Manage Kibana plugins +description: + - Manages Kibana plugins. +version_added: "2.2" +author: Thierno IB. BARRY (@barryib) +options: + name: + description: + - Name of the plugin to install + required: True + state: + description: + - Desired state of a plugin. + required: False + choices: ["present", "absent"] + default: present + url: + description: + - Set exact URL to download the plugin from. + For local file, prefix its absolute path with file:// + required: False + default: None + timeout: + description: + - "Timeout setting: 30s, 1m, 1h..." + required: False + default: 1m + plugin_bin: + description: + - Location of the plugin binary + required: False + default: /opt/kibana/bin/kibana + plugin_dir: + description: + - Your configured plugin directory specified in Kibana + required: False + default: /opt/kibana/installedPlugins/ + version: + description: + - Version of the plugin to be installed. + If plugin exists with previous version, it will NOT be updated if C(force) is not set to yes + required: False + default: None + force: + description: + - Delete and re-install the plugin. Can be useful for plugins update + required: False + choices: ["yes", "no"] + default: no +''' + +EXAMPLES = ''' +# Install Elasticsearch head plugin +- kibana_plugin: state=present name="elasticsearch/marvel" + +# Install specific version of a plugin +- kibana_plugin: state=present name="elasticsearch/marvel" version="2.3.3" + +# Uninstall Elasticsearch head plugin +- kibana_plugin: state=absent name="elasticsearch/marvel" +''' + +RETURN = ''' +cmd: + description: the launched command during plugin mangement (install / remove) + returned: success + type: string +name: + description: the plugin name to install or remove + returned: success + type: string +url: + description: the url from where the plugin is installed from + returned: success + type: string +timeout: + description: the timout for plugin download + returned: success + type: string +stdout: + description: the command stdout + returned: success + type: string +stderr: + description: the command stderr + returned: success + type: string +state: + description: the state for the managed plugin + returned: success + type: string +''' + +PACKAGE_STATE_MAP = dict( + present="--install", + absent="--remove" +) + +def parse_plugin_repo(string): + elements = string.split("/") + + # We first consider the simplest form: pluginname + repo = elements[0] + + # We consider the form: username/pluginname + if len(elements) > 1: + repo = elements[1] + + # remove elasticsearch- prefix + # remove es- prefix + for string in ("elasticsearch-", "es-"): + if repo.startswith(string): + return repo[len(string):] + + return repo + +def is_plugin_present(plugin_dir, working_dir): + return os.path.isdir(os.path.join(working_dir, plugin_dir)) + +def parse_error(string): + reason = "reason: " + try: + return string[string.index(reason) + len(reason):].strip() + except ValueError: + return string + +def install_plugin(module, plugin_bin, plugin_name, url, timeout): + cmd_args = [plugin_bin, "plugin", PACKAGE_STATE_MAP["present"], plugin_name] + + if url: + cmd_args.append("--url %s" % url) + + if timeout: + cmd_args.append("--timeout %s" % timeout) + + cmd = " ".join(cmd_args) + + if module.check_mode: + return True, cmd, "check mode", "" + + rc, out, err = module.run_command(cmd) + if rc != 0: + reason = parse_error(out) + module.fail_json(msg=reason) + + return True, cmd, out, err + +def remove_plugin(module, plugin_bin, plugin_name): + cmd_args = [plugin_bin, "plugin", PACKAGE_STATE_MAP["absent"], plugin_name] + + cmd = " ".join(cmd_args) + + if module.check_mode: + return True, cmd, "check mode", "" + + rc, out, err = module.run_command(cmd) + if rc != 0: + reason = parse_error(out) + module.fail_json(msg=reason) + + return True, cmd, out, err + +def main(): + module = AnsibleModule( + argument_spec=dict( + name=dict(required=True), + state=dict(default="present", choices=PACKAGE_STATE_MAP.keys()), + url=dict(default=None), + timeout=dict(default="1m"), + plugin_bin=dict(default="/opt/kibana/bin/kibana", type="path"), + plugin_dir=dict(default="/opt/kibana/installedPlugins/", type="path"), + version=dict(default=None), + force=dict(default="no", type="bool") + ), + supports_check_mode=True, + ) + + name = module.params["name"] + state = module.params["state"] + url = module.params["url"] + timeout = module.params["timeout"] + plugin_bin = module.params["plugin_bin"] + plugin_dir = module.params["plugin_dir"] + version = module.params["version"] + force = module.params["force"] + + present = is_plugin_present(parse_plugin_repo(name), plugin_dir) + + # skip if the state is correct + if (present and state == "present" and not force) or (state == "absent" and not present and not force): + module.exit_json(changed=False, name=name, state=state) + + if (version): + name = name + '/' + version + + if state == "present": + if force: + remove_plugin(module, plugin_bin, name) + changed, cmd, out, err = install_plugin(module, plugin_bin, name, url, timeout) + + elif state == "absent": + changed, cmd, out, err = remove_plugin(module, plugin_bin, name) + + module.exit_json(changed=changed, cmd=cmd, name=name, state=state, url=url, timeout=timeout, stdout=out, stderr=err) + +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From 9a47088d672ce4bcd423aa74f1d433a690f5688f Mon Sep 17 00:00:00 2001 From: Serge van Ginderachter Date: Thu, 4 Aug 2016 17:17:10 +0200 Subject: [PATCH 1916/2522] Exception handling for MySQLdb warnings (#2594) Do not fail the module for warnings. Return warnings in the module result set. Fixes #719 Alternative to #720 and as discuseed over there. --- database/mysql/mysql_replication.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/database/mysql/mysql_replication.py b/database/mysql/mysql_replication.py index bdfcdbf1391..b9a7d13c824 100644 --- a/database/mysql/mysql_replication.py +++ b/database/mysql/mysql_replication.py @@ -274,6 +274,7 @@ def main(): elif mode in "changemaster": chm=[] chm_params = {} + result = {} if master_host: chm.append("MASTER_HOST=%(master_host)s") chm_params['master_host'] = master_host @@ -322,9 +323,12 @@ def main(): chm.append("MASTER_AUTO_POSITION = 1") try: changemaster(cursor, chm, chm_params) + except MySQLdb.Warning, e: + result['warning'] = str(e) except Exception, e: module.fail_json(msg='%s. Query == CHANGE MASTER TO %s' % (e, chm)) - module.exit_json(changed=True) + result['changed']=True + module.exit_json(**result) elif mode in "startslave": started = start_slave(cursor) if started is True: From fc764553260e02bb7b36bf7c601685206d6f93cf Mon Sep 17 00:00:00 2001 From: Serge van Ginderachter Date: Thu, 4 Aug 2016 17:27:39 +0200 Subject: [PATCH 1917/2522] return a proper result set for getmaster/getslave (#2595) * return a proper result set for getmaster/getslave when not on a master/slave. This allows for a cleaner error handling. * A more uniform return of result keys for getmaster/slave --- database/mysql/mysql_replication.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/database/mysql/mysql_replication.py b/database/mysql/mysql_replication.py index b9a7d13c824..8bc964cfdda 100644 --- a/database/mysql/mysql_replication.py +++ b/database/mysql/mysql_replication.py @@ -258,18 +258,20 @@ def main(): module.fail_json(msg="unable to find %s. Exception message: %s" % (config_file, e)) if mode in "getmaster": - masterstatus = get_master_status(cursor) - try: - module.exit_json( **masterstatus ) - except TypeError: - module.fail_json(msg="Server is not configured as mysql master") + status = get_master_status(cursor) + if not isinstance(status, dict): + status = dict(Is_Master=False, msg="Server is not configured as mysql master") + else: + status['Is_Master'] = True + module.exit_json(**status) elif mode in "getslave": - slavestatus = get_slave_status(cursor) - try: - module.exit_json( **slavestatus ) - except TypeError, e: - module.fail_json(msg="Server is not configured as mysql slave. ERROR: %s" % e) + status = get_slave_status(cursor) + if not isinstance(status, dict): + status = dict(Is_Slave=False, msg="Server is not configured as mysql slave") + else: + status['Is_Slave'] = True + module.exit_json(**status) elif mode in "changemaster": chm=[] From 123c70546e9cbe9fd2030c898d05b40f46b1129b Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Wed, 3 Aug 2016 12:50:16 -0700 Subject: [PATCH 1918/2522] clean up documentation --- cloud/amazon/ec2_vpc_nat_gateway.py | 86 ++++++++++++++++------------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/cloud/amazon/ec2_vpc_nat_gateway.py b/cloud/amazon/ec2_vpc_nat_gateway.py index 1364aa7875f..d1d38e0e030 100644 --- a/cloud/amazon/ec2_vpc_nat_gateway.py +++ b/cloud/amazon/ec2_vpc_nat_gateway.py @@ -17,7 +17,7 @@ DOCUMENTATION = ''' --- module: ec2_vpc_nat_gateway -short_description: Manage AWS VPC NAT Gateways +short_description: Manage AWS VPC NAT Gateways. description: - Ensure the state of AWS VPC NAT Gateways based on their id, allocation and subnet ids. version_added: "2.2" @@ -25,7 +25,7 @@ options: state: description: - - Ensure NAT Gateway is present or absent + - Ensure NAT Gateway is present or absent. required: false default: "present" choices: ["present", "absent"] @@ -44,18 +44,18 @@ allocation_id: description: - The id of the elastic IP allocation. If this is not passed and the - eip_address is not passed. An EIP is generated for this Nat Gateway + eip_address is not passed. An EIP is generated for this NAT Gateway. required: false default: None eip_address: description: - - The elasti ip address of the EIP you want attached to this Nat Gateway. - If this is not passed and the allocation_id is not passed. - An EIP is generated for this Nat Gateway + - The elastic IP address of the EIP you want attached to this NAT Gateway. + If this is not passed and the allocation_id is not passed, + an EIP is generated for this NAT Gateway. required: false if_exist_do_not_create: description: - - if a Nat Gateway exists already in the subnet_id, then do not create a new one. + - if a NAT Gateway exists already in the subnet_id, then do not create a new one. required: false default: false release_eip: @@ -66,12 +66,12 @@ default: true wait: description: - - Wait for operation to complete before returning + - Wait for operation to complete before returning. required: false default: false wait_timeout: description: - - How many seconds to wait for an operation to complete before timing out + - How many seconds to wait for an operation to complete before timing out. required: false default: 300 client_token: @@ -93,7 +93,7 @@ EXAMPLES = ''' # Note: These examples do not set authentication details, see the AWS Guide for details. -- name: Create new nat gateway with client token +- name: Create new nat gateway with client token. ec2_vpc_nat_gateway: state: present subnet_id: subnet-12345678 @@ -102,7 +102,7 @@ client_token: abcd-12345678 register: new_nat_gateway -- name: Create new nat gateway using an allocation-id +- name: Create new nat gateway using an allocation-id. ec2_vpc_nat_gateway: state: present subnet_id: subnet-12345678 @@ -110,7 +110,7 @@ region: ap-southeast-2 register: new_nat_gateway -- name: Create new nat gateway, using an eip address and wait for available status +- name: Create new nat gateway, using an EIP address and wait for available status. ec2_vpc_nat_gateway: state: present subnet_id: subnet-12345678 @@ -119,7 +119,7 @@ region: ap-southeast-2 register: new_nat_gateway -- name: Create new nat gateway and allocate new eip +- name: Create new nat gateway and allocate new EIP. ec2_vpc_nat_gateway: state: present subnet_id: subnet-12345678 @@ -127,7 +127,7 @@ region: ap-southeast-2 register: new_nat_gateway -- name: Create new nat gateway and allocate new eip if a nat gateway does not yet exist in the subnet. +- name: Create new nat gateway and allocate new EIP if a nat gateway does not yet exist in the subnet. ec2_vpc_nat_gateway: state: present subnet_id: subnet-12345678 @@ -136,7 +136,7 @@ if_exist_do_not_create: true register: new_nat_gateway -- name: Delete nat gateway using discovered nat gateways from facts module +- name: Delete nat gateway using discovered nat gateways from facts module. ec2_vpc_nat_gateway: state: absent region: ap-southeast-2 @@ -146,7 +146,7 @@ register: delete_nat_gateway_result with_items: "{{ gateways_to_remove.result }}" -- name: Delete nat gateway and wait for deleted status +- name: Delete nat gateway and wait for deleted status. ec2_vpc_nat_gateway: state: absent nat_gateway_id: nat-12345678 @@ -154,7 +154,7 @@ wait_timeout: 500 region: ap-southeast-2 -- name: Delete nat gateway and release EIP +- name: Delete nat gateway and release EIP. ec2_vpc_nat_gateway: state: absent nat_gateway_id: nat-12345678 @@ -179,7 +179,7 @@ type: string sample: "subnet-12345" state: - description: The current state of the Nat Gateway. + description: The current state of the NAT Gateway. returned: In all cases. type: string sample: "available" @@ -211,6 +211,7 @@ import datetime import random +import re import time from dateutil.tz import tzutc @@ -262,6 +263,7 @@ DRY_RUN_MSGS = 'DryRun Mode:' + def convert_to_lower(data): """Convert all uppercase keys in dict with lowercase_ @@ -300,6 +302,7 @@ def convert_to_lower(data): results[key] = val return results + def get_nat_gateways(client, subnet_id=None, nat_gateway_id=None, states=None, check_mode=False): """Retrieve a list of NAT Gateways @@ -381,9 +384,10 @@ def get_nat_gateways(client, subnet_id=None, nat_gateway_id=None, return gateways_retrieved, err_msg, existing_gateways + def wait_for_status(client, wait_timeout, nat_gateway_id, status, check_mode=False): - """Wait for the Nat Gateway to reach a status + """Wait for the NAT Gateway to reach a status Args: client (botocore.client.EC2): Boto3 client wait_timeout (int): Number of seconds to wait, until this timeout is reached. @@ -418,25 +422,25 @@ def wait_for_status(client, wait_timeout, nat_gateway_id, status, ] Returns: - Tuple (bool, str, list) + Tuple (bool, str, dict) """ polling_increment_secs = 5 wait_timeout = time.time() + wait_timeout status_achieved = False - nat_gateway = list() + nat_gateway = dict() states = ['pending', 'failed', 'available', 'deleting', 'deleted'] err_msg = "" while wait_timeout > time.time(): try: - gws_retrieved, err_msg, nat_gateway = ( + gws_retrieved, err_msg, nat_gateways = ( get_nat_gateways( client, nat_gateway_id=nat_gateway_id, states=states, check_mode=check_mode ) ) - if gws_retrieved and nat_gateway: - nat_gateway = nat_gateway[0] + if gws_retrieved and nat_gateways: + nat_gateway = nat_gateways[0] if check_mode: nat_gateway['state'] = status @@ -449,7 +453,7 @@ def wait_for_status(client, wait_timeout, nat_gateway_id, status, break elif nat_gateway.get('state') == 'pending': - if nat_gateway.has_key('failure_message'): + if 'failure_message' in nat_gateway: err_msg = nat_gateway.get('failure_message') status_achieved = False break @@ -465,6 +469,7 @@ def wait_for_status(client, wait_timeout, nat_gateway_id, status, return status_achieved, err_msg, nat_gateway + def gateway_in_subnet_exists(client, subnet_id, allocation_id=None, check_mode=False): """Retrieve all NAT Gateways for a subnet. @@ -472,7 +477,7 @@ def gateway_in_subnet_exists(client, subnet_id, allocation_id=None, subnet_id (str): The subnet_id the nat resides in. Kwargs: - allocation_id (str): The eip Amazon identifier. + allocation_id (str): The EIP Amazon identifier. default = None Basic Usage: @@ -526,6 +531,7 @@ def gateway_in_subnet_exists(client, subnet_id, allocation_id=None, return gateways, allocation_id_exists + def get_eip_allocation_id_by_address(client, eip_address, check_mode=False): """Release an EIP from your EIP Pool Args: @@ -583,6 +589,7 @@ def get_eip_allocation_id_by_address(client, eip_address, check_mode=False): return allocation_id, err_msg + def allocate_eip_address(client, check_mode=False): """Release an EIP from your EIP Pool Args: @@ -623,6 +630,7 @@ def allocate_eip_address(client, check_mode=False): return ip_allocated, err_msg, new_eip + def release_address(client, allocation_id, check_mode=False): """Release an EIP from your EIP Pool Args: @@ -657,6 +665,7 @@ def release_address(client, allocation_id, check_mode=False): return ip_released + def create(client, subnet_id, allocation_id, client_token=None, wait=False, wait_timeout=0, if_exist_do_not_create=False, check_mode=False): @@ -743,11 +752,8 @@ def create(client, subnet_id, allocation_id, client_token=None, ) if success: err_msg = ( - 'Nat gateway {0} created'.format(result['nat_gateway_id']) + 'NAT gateway {0} created'.format(result['nat_gateway_id']) ) - if check_mode: - result['nat_gateway_addresses'][0]['allocation_id'] = allocation_id - result['subnet_id'] = subnet_id except botocore.exceptions.ClientError as e: if "IdempotentParameterMismatch" in e.message: @@ -762,16 +768,17 @@ def create(client, subnet_id, allocation_id, client_token=None, return success, changed, err_msg, result + def pre_create(client, subnet_id, allocation_id=None, eip_address=None, - if_exist_do_not_create=False, wait=False, wait_timeout=0, - client_token=None, check_mode=False): + if_exist_do_not_create=False, wait=False, wait_timeout=0, + client_token=None, check_mode=False): """Create an Amazon NAT Gateway. Args: client (botocore.client.EC2): Boto3 client subnet_id (str): The subnet_id the nat resides in. Kwargs: - allocation_id (str): The eip Amazon identifier. + allocation_id (str): The EIP Amazon identifier. default = None eip_address (str): The Elastic IP Address of the EIP. default = None @@ -829,7 +836,7 @@ def pre_create(client, subnet_id, allocation_id=None, eip_address=None, changed = False results = existing_gateways[0] err_msg = ( - 'Nat Gateway {0} already exists in subnet_id {1}' + 'NAT Gateway {0} already exists in subnet_id {1}' .format( existing_gateways[0]['nat_gateway_id'], subnet_id ) @@ -864,7 +871,7 @@ def pre_create(client, subnet_id, allocation_id=None, eip_address=None, changed = False results = existing_gateways[0] err_msg = ( - 'Nat Gateway {0} already exists in subnet_id {1}' + 'NAT Gateway {0} already exists in subnet_id {1}' .format( existing_gateways[0]['nat_gateway_id'], subnet_id ) @@ -878,6 +885,7 @@ def pre_create(client, subnet_id, allocation_id=None, eip_address=None, return success, changed, err_msg, results + def remove(client, nat_gateway_id, wait=False, wait_timeout=0, release_eip=False, check_mode=False): """Delete an Amazon NAT Gateway. @@ -944,7 +952,7 @@ def remove(client, nat_gateway_id, wait=False, wait_timeout=0, changed = True success = True err_msg = ( - 'Nat gateway {0} is in a deleting state. Delete was successfull' + 'NAT gateway {0} is in a deleting state. Delete was successfull' .format(nat_gateway_id) ) @@ -957,7 +965,7 @@ def remove(client, nat_gateway_id, wait=False, wait_timeout=0, ) if status_achieved: err_msg = ( - 'Nat gateway {0} was deleted successfully' + 'NAT gateway {0} was deleted successfully' .format(nat_gateway_id) ) @@ -969,10 +977,11 @@ def remove(client, nat_gateway_id, wait=False, wait_timeout=0, release_address(client, allocation_id, check_mode=check_mode) ) if not eip_released: - err_msg = "Failed to release eip %s".format(allocation_id) + err_msg = "Failed to release EIP %s".format(allocation_id) return success, changed, err_msg, results + def main(): argument_spec = ec2_argument_spec() argument_spec.update(dict( @@ -1067,4 +1076,3 @@ def main(): if __name__ == '__main__': main() - From 461553bda80dafc33c006f58640cf1254ae057d7 Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Thu, 4 Aug 2016 13:10:11 -0700 Subject: [PATCH 1919/2522] updated tests to reflect dict vs list --- test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py b/test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py index e2d3573499e..7c4f163ad40 100644 --- a/test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py +++ b/test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py @@ -329,7 +329,7 @@ def test_wait_for_status_to_timeout(self): ) ) self.assertFalse(success) - self.assertEqual(gws, []) + self.assertEqual(gws, {}) def test_gateway_in_subnet_exists_with_allocation_id(self): client = boto3.client('ec2', region_name=aws_region) From dcf4d7e6e5d86c518df5f91cc236b23b30fcdac0 Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Thu, 4 Aug 2016 15:36:39 -0700 Subject: [PATCH 1920/2522] fail_json when error and not exit_json --- cloud/amazon/ec2_vpc_nat_gateway.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_vpc_nat_gateway.py b/cloud/amazon/ec2_vpc_nat_gateway.py index d1d38e0e030..b5874128f3d 100644 --- a/cloud/amazon/ec2_vpc_nat_gateway.py +++ b/cloud/amazon/ec2_vpc_nat_gateway.py @@ -1062,7 +1062,7 @@ def main(): ) if not success: - module.exit_json( + module.fail_json( msg=err_msg, success=success, changed=changed ) else: From 950d76af0b73aff58ab0b9042ee8789b0d1f7dfb Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Thu, 4 Aug 2016 18:14:36 -0700 Subject: [PATCH 1921/2522] fixed error message for releasing an ip when not waiting for the nat gateway to delete successfully 1st --- cloud/amazon/ec2_vpc_nat_gateway.py | 25 ++++++++++++------- .../cloud/amazon/test_ec2_vpc_nat_gateway.py | 2 +- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/cloud/amazon/ec2_vpc_nat_gateway.py b/cloud/amazon/ec2_vpc_nat_gateway.py index b5874128f3d..ee53d7bb138 100644 --- a/cloud/amazon/ec2_vpc_nat_gateway.py +++ b/cloud/amazon/ec2_vpc_nat_gateway.py @@ -62,6 +62,7 @@ description: - Deallocate the EIP from the VPC. - Option is only valid with the absent state. + - You should use this with the wait option. Since you can not release an address while a delete operation is happening. required: false default: true wait: @@ -159,6 +160,8 @@ state: absent nat_gateway_id: nat-12345678 release_eip: yes + wait: yes + wait_timeout: 300 region: ap-southeast-2 ''' @@ -648,10 +651,11 @@ def release_address(client, allocation_id, check_mode=False): True Returns: - Boolean + Boolean, string """ + err_msg = '' if check_mode: - return True + return True, '' ip_released = False params = { @@ -660,10 +664,10 @@ def release_address(client, allocation_id, check_mode=False): try: client.release_address(**params) ip_released = True - except botocore.exceptions.ClientError: - pass + except botocore.exceptions.ClientError as e: + err_msg = str(e) - return ip_released + return ip_released, err_msg def create(client, subnet_id, allocation_id, client_token=None, @@ -973,11 +977,15 @@ def remove(client, nat_gateway_id, wait=False, wait_timeout=0, err_msg = str(e) if release_eip: - eip_released = ( - release_address(client, allocation_id, check_mode=check_mode) + eip_released, eip_err = ( + release_address(client, allocation_id, check_mode) ) if not eip_released: - err_msg = "Failed to release EIP %s".format(allocation_id) + err_msg = ( + "{0}: Failed to release EIP {1}: {2}" + .format(err_msg, allocation_id, eip_err) + ) + success = False return success, changed, err_msg, results @@ -1037,7 +1045,6 @@ def main(): changed = False err_msg = '' - #Ensure resource is present if state == 'present': if not subnet_id: module.fail_json(msg='subnet_id is required for creation') diff --git a/test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py b/test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py index 7c4f163ad40..1b75c88a143 100644 --- a/test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py +++ b/test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py @@ -392,7 +392,7 @@ def test_allocate_eip_address(self): def test_release_address(self): client = boto3.client('ec2', region_name=aws_region) - success = ( + success, _ = ( ng.release_address( client, 'eipalloc-1234567', check_mode=True ) From 344dcc95c05fac9e503ecbdc74c6a46aac4a455c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Fri, 5 Aug 2016 15:26:30 +0200 Subject: [PATCH 1922/2522] iptables: remove duplicated documentation (#2673) Fixes #1527 --- system/iptables.py | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/system/iptables.py b/system/iptables.py index f0f458a5d60..51089575456 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -98,15 +98,7 @@ either a network mask or a plain number, specifying the number of 1's at the left side of the network mask. Thus, a mask of 24 is equivalent to 255.255.255.0. A "!" argument before the address specification - inverts the sense of the address.Source specification. Address can be - either a network name, a hostname, a network IP address (with /mask), - or a plain IP address. Hostnames will be resolved once only, before - the rule is submitted to the kernel. Please note that specifying any - name to be resolved with a remote query such as DNS is a really bad - idea. The mask can be either a network mask or a plain number, - specifying the number of 1's at the left side of the network mask. - Thus, a mask of 24 is equivalent to 255.255.255.0. A "!" argument - before the address specification inverts the sense of the address. + inverts the sense of the address. required: false default: null destination: @@ -119,15 +111,7 @@ either a network mask or a plain number, specifying the number of 1's at the left side of the network mask. Thus, a mask of 24 is equivalent to 255.255.255.0. A "!" argument before the address specification - inverts the sense of the address.Source specification. Address can be - either a network name, a hostname, a network IP address (with /mask), - or a plain IP address. Hostnames will be resolved once only, before - the rule is submitted to the kernel. Please note that specifying any - name to be resolved with a remote query such as DNS is a really bad - idea. The mask can be either a network mask or a plain number, - specifying the number of 1's at the left side of the network mask. - Thus, a mask of 24 is equivalent to 255.255.255.0. A "!" argument - before the address specification inverts the sense of the address. + inverts the sense of the address. required: false default: null match: From d3dbfa3c1379dd4cccde73deb33e7531ac743193 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Fri, 5 Aug 2016 10:48:08 -0500 Subject: [PATCH 1923/2522] List pacman options aliases in documentation (#2670) --- packaging/os/pacman.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packaging/os/pacman.py b/packaging/os/pacman.py index a81f6801ffc..74c474ad922 100644 --- a/packaging/os/pacman.py +++ b/packaging/os/pacman.py @@ -40,6 +40,7 @@ - Name of the package to install, upgrade, or remove. required: false default: null + aliases: [ 'pkg', 'package' ] state: description: @@ -75,6 +76,7 @@ required: false default: no choices: ["yes", "no"] + aliases: [ 'update-cache' ] upgrade: description: From d4b0732c9fe61b55d607d06f190a9d8663225cc4 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Fri, 5 Aug 2016 10:50:23 -0500 Subject: [PATCH 1924/2522] List homebrew options aliases in documentation (#2671) --- packaging/os/homebrew.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packaging/os/homebrew.py b/packaging/os/homebrew.py index a91a8ab8fe3..d30c12d774b 100755 --- a/packaging/os/homebrew.py +++ b/packaging/os/homebrew.py @@ -39,6 +39,7 @@ - name of package to install/remove required: false default: None + aliases: ['pkg', 'package', 'formula'] path: description: - "':' separated list of paths to search for 'brew' executable. Since A package (I(formula) in homebrew parlance) location is prefixed relative to the actual path of I(brew) command, providing an alternative I(brew) path enables managing different set of packages in an alternative location in the system." @@ -56,17 +57,20 @@ required: false default: no choices: [ "yes", "no" ] + aliases: ['update-brew'] upgrade_all: description: - upgrade all homebrew packages required: false default: no choices: [ "yes", "no" ] + aliases: ['upgrade'] install_options: description: - options flags to install a package required: false default: null + aliases: ['options'] version_added: "1.4" notes: [] ''' From 33716b18374059b18c08a8724b684ca48db3c41c Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Fri, 5 Aug 2016 11:28:02 -0700 Subject: [PATCH 1925/2522] Use open_url directly to avoid failing tests. (#2680) --- monitoring/logicmonitor.py | 5 ++--- monitoring/logicmonitor_facts.py | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/monitoring/logicmonitor.py b/monitoring/logicmonitor.py index be016327350..8d35f3bfbb3 100644 --- a/monitoring/logicmonitor.py +++ b/monitoring/logicmonitor.py @@ -550,7 +550,6 @@ def __init__(self, module, **params): self.password = params["password"] self.fqdn = socket.getfqdn() self.lm_url = "logicmonitor.com/santaba" - self.urlopen = open_url # use the ansible provided open_url self.__version__ = self.__version__ + "-ansible-module" def rpc(self, action, params): @@ -577,7 +576,7 @@ def rpc(self, action, params): headers = {"X-LM-User-Agent": self.__version__} # Set headers - f = self.urlopen(url, headers=headers) + f = open_url(url, headers=headers) raw = f.read() resp = json.loads(raw) @@ -608,7 +607,7 @@ def do(self, action, params): self.module.debug("Attempting to open URL: " + "https://" + self.company + "." + self.lm_url + "/do/" + action + "?" + param_str) - f = self.urlopen( + f = open_url( "https://" + self.company + "." + self.lm_url + "/do/" + action + "?" + param_str) return f.read() diff --git a/monitoring/logicmonitor_facts.py b/monitoring/logicmonitor_facts.py index 7e9bf00cb5a..cc91ca6122c 100644 --- a/monitoring/logicmonitor_facts.py +++ b/monitoring/logicmonitor_facts.py @@ -200,7 +200,6 @@ def __init__(self, module, **params): self.password = params["password"] self.fqdn = socket.getfqdn() self.lm_url = "logicmonitor.com/santaba" - self.urlopen = open_url # use the ansible provided open_url self.__version__ = self.__version__ + "-ansible-module" def rpc(self, action, params): @@ -227,7 +226,7 @@ def rpc(self, action, params): headers = {"X-LM-User-Agent": self.__version__} # Set headers - f = self.urlopen(url, headers=headers) + f = open_url(url, headers=headers) raw = f.read() resp = json.loads(raw) From 7a73e2a3aacbec060a50ad760168d629f2f1d455 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Fri, 5 Aug 2016 19:02:26 -0400 Subject: [PATCH 1926/2522] make sure nic is always defined (#2678) --- cloud/misc/ovirt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud/misc/ovirt.py b/cloud/misc/ovirt.py index 86c769bdb24..f4f77ca8ce8 100644 --- a/cloud/misc/ovirt.py +++ b/cloud/misc/ovirt.py @@ -333,6 +333,7 @@ def vm_start(conn, vmname, hostname=None, ip=None, netmask=None, gateway=None, vm = conn.vms.get(name=vmname) use_cloud_init = False nics = None + nic = None if hostname or ip or netmask or gateway or domain or dns or rootpw or key: use_cloud_init = True if ip and netmask and gateway: From 2d992fe750728ee175bf1721d0695f7b03526eaa Mon Sep 17 00:00:00 2001 From: trondhindenes Date: Sat, 6 Aug 2016 09:45:46 +0200 Subject: [PATCH 1927/2522] updated documentation for win_package (#2677) --- windows/win_package.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/windows/win_package.py b/windows/win_package.py index 47711369c1c..e8a91176c3e 100644 --- a/windows/win_package.py +++ b/windows/win_package.py @@ -80,5 +80,16 @@ path="http://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x64.exe" Product_Id="{CF2BEA3C-26EA-32F8-AA9B-331F7E34BA97}" Arguments="/install /passive /norestart" + +# Install/uninstall an msi-based package +- name: Install msi-based package (Remote Desktop Connection Manager) + win_package: + path: "https://download.microsoft.com/download/A/F/0/AF0071F3-B198-4A35-AA90-C68D103BDCCF/rdcman.msi" + product_id: "{0240359E-6A4C-4884-9E94-B397A02D893C}" +- name: Uninstall msi-based package + win_package: + path: "https://download.microsoft.com/download/A/F/0/AF0071F3-B198-4A35-AA90-C68D103BDCCF/rdcman.msi" + product_id: "{0240359E-6A4C-4884-9E94-B397A02D893C}" + state: absent ''' From 34b4c34306d44e16642111f50246317af7dc30c8 Mon Sep 17 00:00:00 2001 From: Massimo Gervasini Date: Sat, 6 Aug 2016 18:48:26 +0800 Subject: [PATCH 1928/2522] when write_config is no, we should not try to write any configuration changes --- network/a10/a10_server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/network/a10/a10_server.py b/network/a10/a10_server.py index 3c3ab5dbbc7..9716b43efbb 100644 --- a/network/a10/a10_server.py +++ b/network/a10/a10_server.py @@ -257,8 +257,8 @@ def status_needs_update(current_status, new_status): else: result = dict(msg="the server was not present") - # if the config has changed, or we want to force a save, save the config unless otherwise requested - if changed or write_config: + # if the config has changed, save the config unless otherwise requested + if changed and write_config: write_result = axapi_call(module, session_url + '&method=system.action.write_memory') if axapi_failure(write_result): module.fail_json(msg="failed to save the configuration: %s" % write_result['response']['err']['msg']) From 31a027e2cd68f78223d170fb82328f0de3b1abe4 Mon Sep 17 00:00:00 2001 From: ovcharenko Date: Sat, 6 Aug 2016 15:39:03 +0300 Subject: [PATCH 1929/2522] [FIX] Bug report: ufw: interface option causes an error (1.9.4) (#1491) (#2668) --- system/ufw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/ufw.py b/system/ufw.py index 89376e7c22e..6b7fb6a7657 100644 --- a/system/ufw.py +++ b/system/ufw.py @@ -225,7 +225,7 @@ def execute(cmd): if len(commands) < 1: module.fail_json(msg="Not any of the command arguments %s given" % commands) - if('interface' in params and 'direction' not in params): + if('interface' in params and params['direction'] is None): module.fail_json(msg="Direction must be specified when creating a rule on an interface") # Ensure ufw is available From 73a3cd6aebc85b0f0882b5aad2a83e2d61f54f80 Mon Sep 17 00:00:00 2001 From: Manuel Sousa Date: Sat, 6 Aug 2016 17:27:27 +0100 Subject: [PATCH 1930/2522] RabbitMQ-Binding - Allow empty routing key (#2674) Fixes: #1985 --- messaging/rabbitmq_binding.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/messaging/rabbitmq_binding.py b/messaging/rabbitmq_binding.py index ad7fa151461..2768095b9d7 100644 --- a/messaging/rabbitmq_binding.py +++ b/messaging/rabbitmq_binding.py @@ -127,6 +127,11 @@ def main(): else: dest_type="e" + if module.params['routing_key'] == "": + props = "~" + else: + props = urllib.quote(module.params['routing_key'],'') + url = "http://%s:%s/api/bindings/%s/e/%s/%s/%s/%s" % ( module.params['login_host'], module.params['login_port'], @@ -134,7 +139,7 @@ def main(): urllib.quote(module.params['name'],''), dest_type, urllib.quote(module.params['destination'],''), - urllib.quote(module.params['routing_key'],'') + props ) # Check if exchange already exists From f366f5a94319d209e166fc4e1d3879dcae495188 Mon Sep 17 00:00:00 2001 From: Kevin Brebanov Date: Sun, 7 Aug 2016 15:43:40 -0400 Subject: [PATCH 1931/2522] apk: Add support for updating virtual packages (#2686) Fixes: #2389 --- packaging/os/apk.py | 58 +++++++++++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/packaging/os/apk.py b/packaging/os/apk.py index 191f3b39b09..120fc981382 100644 --- a/packaging/os/apk.py +++ b/packaging/os/apk.py @@ -42,7 +42,7 @@ choices: [ "present", "absent", "latest" ] update_cache: description: - - Update repository indexes. Can be run with other steps or on it's own. + - Update repository indexes. Can be run with other steps or on it's own. required: false default: no choices: [ "yes", "no" ] @@ -114,6 +114,23 @@ def query_latest(module, name): return False return True +def query_virtual(module, name): + cmd = "%s -v info --description %s" % (APK_PATH, name) + rc, stdout, stderr = module.run_command(cmd, check_rc=False) + search_pattern = "^%s: virtual meta package" % (name) + if re.search(search_pattern, stdout): + return True + return False + +def get_dependencies(module, name): + cmd = "%s -v info --depends %s" % (APK_PATH, name) + rc, stdout, stderr = module.run_command(cmd, check_rc=False) + dependencies = stdout.split() + if len(dependencies) > 1: + return dependencies[1:] + else: + return [] + def upgrade_packages(module): if module.check_mode: cmd = "%s upgrade --simulate" % (APK_PATH) @@ -128,29 +145,40 @@ def upgrade_packages(module): def install_packages(module, names, state): upgrade = False - uninstalled = [] + to_install = [] + to_upgrade = [] for name in names: - if not query_package(module, name): - uninstalled.append(name) - elif state == 'latest' and not query_latest(module, name): - upgrade = True - if not uninstalled and not upgrade: + # Check if virtual package + if query_virtual(module, name): + # Get virtual package dependencies + dependencies = get_dependencies(module, name) + for dependency in dependencies: + if state == 'latest' and not query_latest(module, dependency): + to_upgrade.append(dependency) + else: + if not query_package(module, name): + to_install.append(name) + elif state == 'latest' and not query_latest(module, name): + to_upgrade.append(name) + if to_upgrade: + upgrade = True + if not to_install and not upgrade: module.exit_json(changed=False, msg="package(s) already installed") - names = " ".join(uninstalled) + packages = " ".join(to_install) + " ".join(to_upgrade) if upgrade: if module.check_mode: - cmd = "%s add --upgrade --simulate %s" % (APK_PATH, names) + cmd = "%s add --upgrade --simulate %s" % (APK_PATH, packages) else: - cmd = "%s add --upgrade %s" % (APK_PATH, names) + cmd = "%s add --upgrade %s" % (APK_PATH, packages) else: if module.check_mode: - cmd = "%s add --simulate %s" % (APK_PATH, names) + cmd = "%s add --simulate %s" % (APK_PATH, packages) else: - cmd = "%s add %s" % (APK_PATH, names) + cmd = "%s add %s" % (APK_PATH, packages) rc, stdout, stderr = module.run_command(cmd, check_rc=False) if rc != 0: - module.fail_json(msg="failed to install %s" % (names)) - module.exit_json(changed=True, msg="installed %s package(s)" % (names)) + module.fail_json(msg="failed to install %s" % (packages)) + module.exit_json(changed=True, msg="installed %s package(s)" % (packages)) def remove_packages(module, names): installed = [] @@ -168,7 +196,7 @@ def remove_packages(module, names): if rc != 0: module.fail_json(msg="failed to remove %s package(s)" % (names)) module.exit_json(changed=True, msg="removed %s package(s)" % (names)) - + # ========================================== # Main control flow. From 8fae447ee831b0a088e8cb2dfcbb029331f7ae7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Sun, 7 Aug 2016 21:44:07 +0200 Subject: [PATCH 1932/2522] apk: fix LANG != C while parsing stdout (#2689) --- packaging/os/apk.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packaging/os/apk.py b/packaging/os/apk.py index 120fc981382..c867d1160d1 100644 --- a/packaging/os/apk.py +++ b/packaging/os/apk.py @@ -212,6 +212,9 @@ def main(): supports_check_mode = True ) + # Set LANG env since we parse stdout + module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C') + global APK_PATH APK_PATH = module.get_bin_path('apk', required=True) From ebb8d73d57a0398ff86b950e69fc61a82d3c0d93 Mon Sep 17 00:00:00 2001 From: Dan Keder Date: Mon, 8 Aug 2016 14:49:38 +0200 Subject: [PATCH 1933/2522] seport: fix a bug when SELinux port definition was already there (#2009) (#2694) --- system/seport.py | 69 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 51 insertions(+), 18 deletions(-) diff --git a/system/seport.py b/system/seport.py index 27183f06621..242661a1430 100644 --- a/system/seport.py +++ b/system/seport.py @@ -83,9 +83,30 @@ from ansible.module_utils.basic import * from ansible.module_utils.pycompat24 import get_exception -def semanage_port_exists(seport, port, proto): - """ Get the SELinux port type definition from policy. Return None if it does - not exist. + +def semanage_port_get_ports(seport, setype, proto): + """ Get the list of ports that have the specified type definition. + + :param seport: Instance of seobject.portRecords + + :type setype: str + :param setype: SELinux type. + + :type proto: str + :param proto: Protocol ('tcp' or 'udp') + + :rtype: list + :return: List of ports that have the specified SELinux type. + """ + records = seport.get_all_by_type() + if (setype, proto) in records: + return records[(setype, proto)] + else: + return [] + + +def semanage_port_get_type(seport, port, proto): + """ Get the SELinux type of the specified port. :param seport: Instance of seobject.portRecords @@ -95,15 +116,19 @@ def semanage_port_exists(seport, port, proto): :type proto: str :param proto: Protocol ('tcp' or 'udp') - :rtype: bool - :return: True if the SELinux port type definition exists, False otherwise + :rtype: tuple + :return: Tuple containing the SELinux type and MLS/MCS level, or None if not found. """ ports = port.split('-', 1) if len(ports) == 1: ports.extend(ports) - ports = map(int, ports) - record = (ports[0], ports[1], proto) - return record in seport.get_all() + key = (int(ports[0]), int(ports[1]), proto) + + records = seport.get_all() + if key in records: + return records[key] + else: + return None def semanage_port_add(module, ports, proto, setype, do_reload, serange='s0', sestore=''): @@ -137,11 +162,15 @@ def semanage_port_add(module, ports, proto, setype, do_reload, serange='s0', ses seport = seobject.portRecords(sestore) seport.set_reload(do_reload) change = False + ports_by_type = semanage_port_get_ports(seport, setype, proto) for port in ports: - exists = semanage_port_exists(seport, port, proto) - if not exists and not module.check_mode: - seport.add(port, proto, serange, setype) - change = change or not exists + if port not in ports_by_type: + change = True + port_type = semanage_port_get_type(seport, port, proto) + if port_type is None and not module.check_mode: + seport.add(port, proto, serange, setype) + elif port_type is not None and not module.check_mode: + seport.modify(port, proto, serange, setype) except ValueError: e = get_exception() @@ -162,7 +191,7 @@ def semanage_port_add(module, ports, proto, setype, do_reload, serange='s0', ses return change -def semanage_port_del(module, ports, proto, do_reload, sestore=''): +def semanage_port_del(module, ports, proto, setype, do_reload, sestore=''): """ Delete SELinux port type definition from the policy. :type module: AnsibleModule @@ -174,6 +203,9 @@ def semanage_port_del(module, ports, proto, do_reload, sestore=''): :type proto: str :param proto: Protocol ('tcp' or 'udp') + :type setype: str + :param setype: SELinux type. + :type do_reload: bool :param do_reload: Whether to reload SELinux policy after commit @@ -187,11 +219,12 @@ def semanage_port_del(module, ports, proto, do_reload, sestore=''): seport = seobject.portRecords(sestore) seport.set_reload(do_reload) change = False + ports_by_type = semanage_port_get_ports(seport, setype, proto) for port in ports: - exists = semanage_port_exists(seport, port, proto) - if not exists and not module.check_mode: - seport.delete(port, proto) - change = change or not exists + if port in ports_by_type: + change = True + if not module.check_mode: + seport.delete(port, proto) except ValueError: e = get_exception() @@ -262,7 +295,7 @@ def main(): if state == 'present': result['changed'] = semanage_port_add(module, ports, proto, setype, do_reload) elif state == 'absent': - result['changed'] = semanage_port_del(module, ports, proto, do_reload) + result['changed'] = semanage_port_del(module, ports, proto, setype, do_reload) else: module.fail_json(msg='Invalid value of argument "state": {0}'.format(state)) From eaa71f51d652d803bff527a5941c027cb206d88e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20=C5=A0tevko?= Date: Mon, 8 Aug 2016 17:49:18 +0200 Subject: [PATCH 1934/2522] Add support for managing OpenZFS pools (#2642) --- system/zfs.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/system/zfs.py b/system/zfs.py index cf74c5b0b83..fb987017d13 100644 --- a/system/zfs.py +++ b/system/zfs.py @@ -33,7 +33,7 @@ required: true state: description: - - Whether to create (C(present)), or remove (C(absent)) a + - Whether to create (C(present)), or remove (C(absent)) a file system, snapshot or volume. All parents/children will be created/destroyed as needed to reach the desired state. choices: ['present', 'absent'] @@ -83,14 +83,27 @@ def __init__(self, module, name, properties): self.name = name self.properties = properties self.changed = False - self.is_solaris = os.uname()[0] == 'SunOS' - self.pool = name.split('/')[0] self.zfs_cmd = module.get_bin_path('zfs', True) self.zpool_cmd = module.get_bin_path('zpool', True) + self.pool = name.split('/')[0] + self.is_solaris = os.uname()[0] == 'SunOS' + self.is_openzfs = self.check_openzfs() self.enhanced_sharing = self.check_enhanced_sharing() + def check_openzfs(self): + cmd = [self.zpool_cmd] + cmd.extend(['get', 'version']) + cmd.append(self.pool) + (rc, out, err) = self.module.run_command(cmd, check_rc=True) + version = out.splitlines()[-1].split()[2] + if version == '-': + return True + if int(version) == 5000: + return True + return False + def check_enhanced_sharing(self): - if os.uname()[0] == 'SunOS': + if self.is_solaris and not self.is_openzfs: cmd = [self.zpool_cmd] cmd.extend(['get', 'version']) cmd.append(self.pool) From e8b29dd75065188b538f03629d8dd85034c3a00b Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Mon, 8 Aug 2016 23:35:36 -0700 Subject: [PATCH 1935/2522] Adds style conventions for bigip_node (#2697) A number of coding conventions have been adopted for new F5 modules that are in development. To ensure common usage across the modules, this module needed to be updated to reflect those conventions. No functional code changes were made. --- network/f5/bigip_node.py | 406 +++++++++++++++++++-------------------- 1 file changed, 203 insertions(+), 203 deletions(-) diff --git a/network/f5/bigip_node.py b/network/f5/bigip_node.py index 7a648341c0c..5f3a33f7838 100644 --- a/network/f5/bigip_node.py +++ b/network/f5/bigip_node.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - +# # (c) 2013, Matt Hite # # This file is part of Ansible @@ -23,148 +23,139 @@ module: bigip_node short_description: "Manages F5 BIG-IP LTM nodes" description: - - "Manages F5 BIG-IP LTM nodes via iControl SOAP API" + - "Manages F5 BIG-IP LTM nodes via iControl SOAP API" version_added: "1.4" author: - - Matt Hite (@mhite) - - Tim Rupp (@caphrim007) + - Matt Hite (@mhite) + - Tim Rupp (@caphrim007) notes: - - "Requires BIG-IP software version >= 11" - - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" - - "Best run as a local_action in your playbook" + - "Requires BIG-IP software version >= 11" + - "F5 developed module 'bigsuds' required (see http://devcentral.f5.com)" + - "Best run as a local_action in your playbook" requirements: - - bigsuds + - bigsuds options: - server: - description: - - BIG-IP host - required: true - default: null - choices: [] - aliases: [] - server_port: - description: - - BIG-IP server port - required: false - default: 443 - version_added: "2.2" - user: - description: - - BIG-IP username - required: true - default: null - choices: [] - aliases: [] - password: - description: - - BIG-IP password - required: true - default: null - choices: [] - aliases: [] - validate_certs: - description: - - If C(no), SSL certificates will not be validated. This should only be used - on personally controlled sites. Prior to 2.0, this module would always - validate on python >= 2.7.9 and never validate on python <= 2.7.8 - required: false - default: 'yes' - choices: ['yes', 'no'] - version_added: 2.0 - state: - description: - - Pool member state - required: true - default: present - choices: ['present', 'absent'] - aliases: [] - session_state: - description: - - Set new session availability status for node - version_added: "1.9" - required: false - default: null - choices: ['enabled', 'disabled'] - aliases: [] - monitor_state: - description: - - Set monitor availability status for node - version_added: "1.9" - required: false - default: null - choices: ['enabled', 'disabled'] - aliases: [] - partition: - description: - - Partition - required: false - default: 'Common' - choices: [] - aliases: [] - name: - description: - - "Node name" - required: false - default: null - choices: [] - monitor_type: - description: - - Monitor rule type when monitors > 1 - version_added: "2.2" - required: False - default: null - choices: ['and_list', 'm_of_n'] - aliases: [] - quorum: - description: - - Monitor quorum value when monitor_type is m_of_n - version_added: "2.2" - required: False - default: null - choices: [] - aliases: [] - monitors: - description: - - Monitor template name list. Always use the full path to the monitor. - version_added: "2.2" - required: False - default: null - choices: [] - aliases: [] - host: - description: - - "Node IP. Required when state=present and node does not exist. Error when state=absent." - required: true - default: null - choices: [] - aliases: ['address', 'ip'] + server: + description: + - BIG-IP host + required: true + default: null + choices: [] + aliases: [] + server_port: + description: + - BIG-IP server port + required: false + default: 443 + version_added: "2.2" + user: + description: + - BIG-IP username + required: true + default: null + choices: [] + aliases: [] + password: + description: + - BIG-IP password + required: true + default: null + choices: [] + aliases: [] + validate_certs: + description: + - If C(no), SSL certificates will not be validated. This should only be used + on personally controlled sites. Prior to 2.0, this module would always + validate on python >= 2.7.9 and never validate on python <= 2.7.8 + required: false + default: 'yes' + choices: ['yes', 'no'] + version_added: 2.0 + state: + description: + - Pool member state + required: true + default: present + choices: ['present', 'absent'] + aliases: [] + session_state: + description: + - Set new session availability status for node + version_added: "1.9" + required: false + default: null + choices: ['enabled', 'disabled'] + aliases: [] + monitor_state: + description: + - Set monitor availability status for node + version_added: "1.9" + required: false + default: null + choices: ['enabled', 'disabled'] + aliases: [] + partition: description: - description: - - "Node description." - required: false - default: null - choices: [] + - Partition + required: false + default: 'Common' + choices: [] + aliases: [] + name: + description: + - "Node name" + required: false + default: null + choices: [] + monitor_type: + description: + - Monitor rule type when monitors > 1 + version_added: "2.2" + required: False + default: null + choices: ['and_list', 'm_of_n'] + aliases: [] + quorum: + description: + - Monitor quorum value when monitor_type is m_of_n + version_added: "2.2" + required: False + default: null + choices: [] + aliases: [] + monitors: + description: + - Monitor template name list. Always use the full path to the monitor. + version_added: "2.2" + required: False + default: null + choices: [] + aliases: [] + host: + description: + - "Node IP. Required when state=present and node does not exist. Error when state=absent." + required: true + default: null + choices: [] + aliases: ['address', 'ip'] + description: + description: + - "Node description." + required: false + default: null + choices: [] ''' EXAMPLES = ''' - -## playbook task examples: - ---- -# file bigip-test.yml -# ... -- hosts: bigip-test - tasks: - - name: Add node - local_action: > - bigip_node - server=lb.mydomain.com - user=admin - password=mysecret - state=present - partition=matthite - host="{{ ansible_default_ipv4["address"] }}" - name="{{ ansible_default_ipv4["address"] }}" +- name: Add node + bigip_node: + server: "lb.mydomain.com" + user: "admin" + password: "secret" + state: "present" + partition: "Common" + host: "10.20.30.40" + name: "10.20.30.40" # Note that the BIG-IP automatically names the node using the # IP address specified in previous play's host parameter. @@ -173,38 +164,38 @@ # Alternatively, you could have specified a name with the # name parameter when state=present. - - name: Add node with a single 'ping' monitor - bigip_node: - server: lb.mydomain.com - user: admin - password: mysecret - state: present - partition: Common - host: "{{ ansible_default_ipv4["address"] }}" - name: mytestserver +- name: Add node with a single 'ping' monitor + bigip_node: + server: "lb.mydomain.com" + user: "admin" + password: "secret" + state: "present" + partition: "Common" + host: "10.20.30.40" + name: "mytestserver" monitors: - /Common/icmp - - - name: Modify node description - local_action: > - bigip_node - server=lb.mydomain.com - user=admin - password=mysecret - state=present - partition=matthite - name="{{ ansible_default_ipv4["address"] }}" - description="Our best server yet" - - - name: Delete node - local_action: > - bigip_node - server=lb.mydomain.com - user=admin - password=mysecret - state=absent - partition=matthite - name="{{ ansible_default_ipv4["address"] }}" + delegate_to: localhost + +- name: Modify node description + bigip_node: + server: "lb.mydomain.com" + user: "admin" + password: "secret" + state: "present" + partition: "Common" + name: "10.20.30.40" + description: "Our best server yet" + delegate_to: localhost + +- name: Delete node + bigip_node: + server: "lb.mydomain.com" + user: "admin" + password: "secret" + state: "absent" + partition: "Common" + name: "10.20.30.40" # The BIG-IP GUI doesn't map directly to the API calls for "Node -> # General Properties -> State". The following states map to API monitor @@ -219,27 +210,26 @@ # # See https://devcentral.f5.com/questions/icontrol-equivalent-call-for-b-node-down - - name: Force node offline - local_action: > - bigip_node - server=lb.mydomain.com - user=admin - password=mysecret - state=present - session_state=disabled - monitor_state=disabled - partition=matthite - name="{{ ansible_default_ipv4["address"] }}" - +- name: Force node offline + bigip_node: + server: "lb.mydomain.com" + user: "admin" + password: "mysecret" + state: "present" + session_state: "disabled" + monitor_state: "disabled" + partition: "Common" + name: "10.20.30.40" ''' + def node_exists(api, address): # hack to determine if node exists result = False try: api.LocalLB.NodeAddressV2.get_object_status(nodes=[address]) result = True - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed as e: if "was not found" in str(e): result = False else: @@ -247,12 +237,17 @@ def node_exists(api, address): raise return result + def create_node_address(api, address, name): try: - api.LocalLB.NodeAddressV2.create(nodes=[name], addresses=[address], limits=[0]) + api.LocalLB.NodeAddressV2.create( + nodes=[name], + addresses=[address], + limits=[0] + ) result = True desc = "" - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed as e: if "already exists" in str(e): result = False desc = "referenced name or IP already in use" @@ -261,15 +256,17 @@ def create_node_address(api, address, name): raise return (result, desc) + def get_node_address(api, name): return api.LocalLB.NodeAddressV2.get_address(nodes=[name])[0] + def delete_node_address(api, address): try: api.LocalLB.NodeAddressV2.delete_node_address(nodes=[address]) result = True desc = "" - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed as e: if "is referenced by a member of pool" in str(e): result = False desc = "node referenced by pool" @@ -278,33 +275,40 @@ def delete_node_address(api, address): raise return (result, desc) + def set_node_description(api, name, description): api.LocalLB.NodeAddressV2.set_description(nodes=[name], descriptions=[description]) + def get_node_description(api, name): return api.LocalLB.NodeAddressV2.get_description(nodes=[name])[0] + def set_node_session_enabled_state(api, name, session_state): session_state = "STATE_%s" % session_state.strip().upper() api.LocalLB.NodeAddressV2.set_session_enabled_state(nodes=[name], states=[session_state]) + def get_node_session_status(api, name): result = api.LocalLB.NodeAddressV2.get_session_status(nodes=[name])[0] result = result.split("SESSION_STATUS_")[-1].lower() return result + def set_node_monitor_state(api, name, monitor_state): monitor_state = "STATE_%s" % monitor_state.strip().upper() api.LocalLB.NodeAddressV2.set_monitor_state(nodes=[name], states=[monitor_state]) + def get_node_monitor_status(api, name): result = api.LocalLB.NodeAddressV2.get_monitor_status(nodes=[name])[0] result = result.split("MONITOR_STATUS_")[-1].lower() return result + def get_monitors(api, name): result = api.LocalLB.NodeAddressV2.get_monitor_rule(nodes=[name])[0] monitor_type = result['type'].split("MONITOR_RULE_TYPE_")[-1].lower() @@ -312,37 +316,36 @@ def get_monitors(api, name): monitor_templates = result['monitor_templates'] return (monitor_type, quorum, monitor_templates) + def set_monitors(api, name, monitor_type, quorum, monitor_templates): monitor_type = "MONITOR_RULE_TYPE_%s" % monitor_type.strip().upper() monitor_rule = {'type': monitor_type, 'quorum': quorum, 'monitor_templates': monitor_templates} api.LocalLB.NodeAddressV2.set_monitor_rule(nodes=[name], monitor_rules=[monitor_rule]) + def main(): monitor_type_choices = ['and_list', 'm_of_n'] argument_spec = f5_argument_spec() - argument_spec.update(dict( - session_state = dict(type='str', choices=['enabled', 'disabled']), - monitor_state = dict(type='str', choices=['enabled', 'disabled']), - name = dict(type='str', required=True), - host = dict(type='str', aliases=['address', 'ip']), - description = dict(type='str'), - monitor_type = dict(type='str', choices=monitor_type_choices), - quorum = dict(type='int'), - monitors = dict(type='list') - ) + meta_args = dict( + session_state=dict(type='str', choices=['enabled', 'disabled']), + monitor_state=dict(type='str', choices=['enabled', 'disabled']), + name=dict(type='str', required=True), + host=dict(type='str', aliases=['address', 'ip']), + description=dict(type='str'), + monitor_type=dict(type='str', choices=monitor_type_choices), + quorum=dict(type='int'), + monitors=dict(type='list') ) + argument_spec.update(meta_args) module = AnsibleModule( - argument_spec = argument_spec, + argument_spec=argument_spec, supports_check_mode=True ) - if not bigsuds_found: - module.fail_json(msg="the python bigsuds module is required") - if module.params['validate_certs']: import ssl if not hasattr(ssl, 'SSLContext'): @@ -373,7 +376,6 @@ def main(): monitors.append(fq_name(partition, monitor)) # sanity check user supplied values - if state == 'absent' and host is not None: module.fail_json(msg="host parameter invalid when state=absent") @@ -415,7 +417,7 @@ def main(): elif state == 'present': if not node_exists(api, address): if host is None: - module.fail_json(msg="host parameter required when " \ + module.fail_json(msg="host parameter required when " "state=present and node does not exist") if not module.check_mode: created, desc = create_node_address(api, address=host, name=address) @@ -442,8 +444,8 @@ def main(): # node exists -- potentially modify attributes if host is not None: if get_node_address(api, address) != host: - module.fail_json(msg="Changing the node address is " \ - "not supported by the API; " \ + module.fail_json(msg="Changing the node address is " + "not supported by the API; " "delete and recreate the node.") if session_state is not None: session_status = get_node_session_status(api, address) @@ -454,7 +456,7 @@ def main(): session_state) result = {'changed': True} elif session_state == 'disabled' and \ - session_status != 'force_disabled': + session_status != 'force_disabled': if not module.check_mode: set_node_session_enabled_state(api, address, session_state) @@ -468,7 +470,7 @@ def main(): monitor_state) result = {'changed': True} elif monitor_state == 'disabled' and \ - monitor_status != 'forced_down': + monitor_status != 'forced_down': if not module.check_mode: set_node_monitor_state(api, address, monitor_state) @@ -484,13 +486,11 @@ def main(): if not module.check_mode: set_monitors(api, address, monitor_type, quorum, monitors) result = {'changed': True} - - except Exception, e: + except Exception as e: module.fail_json(msg="received exception: %s" % e) module.exit_json(**result) -# import module snippets from ansible.module_utils.basic import * from ansible.module_utils.f5 import * From 93472c3c542a68f5be45d4c2381a9d8e82f2f04e Mon Sep 17 00:00:00 2001 From: Dagobert Michelsen Date: Tue, 9 Aug 2016 14:47:00 +0200 Subject: [PATCH 1936/2522] Do not return failure when the package is installed and nothing is done (#1852) --- packaging/os/svr4pkg.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packaging/os/svr4pkg.py b/packaging/os/svr4pkg.py index 5d8bac17eaa..807e00f543b 100644 --- a/packaging/os/svr4pkg.py +++ b/packaging/os/svr4pkg.py @@ -225,9 +225,10 @@ def main(): else: result['changed'] = False + # rc will be none when the package already was installed and no action took place # Only return failed=False when the returncode is known to be good as there may be more # undocumented failure return codes - if rc not in (0, 2, 10, 20): + if rc not in (None, 0, 2, 10, 20): result['failed'] = True else: result['failed'] = False From 35aa1a5a813519c37fdddce8ab27c31cdf9a106a Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 9 Aug 2016 10:36:45 -0500 Subject: [PATCH 1937/2522] If rc is null, assume that a timeout happened. Fixes #2484 (#2485) --- commands/expect.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/commands/expect.py b/commands/expect.py index 4592179456b..4b5e5e8d623 100644 --- a/commands/expect.py +++ b/commands/expect.py @@ -214,7 +214,7 @@ def main(): if out is None: out = '' - module.exit_json( + ret = dict( cmd=args, stdout=out.rstrip('\r\n'), rc=rc, @@ -224,6 +224,12 @@ def main(): changed=True, ) + if rc: + module.exit_json(**ret) + else: + ret['msg'] = 'command exceeded timeout' + module.fail_json(**ret) + # import module snippets from ansible.module_utils.basic import * From 5432c33324aa0d6c46e5a101339095ca7c45fd8b Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 9 Aug 2016 12:39:34 -0400 Subject: [PATCH 1938/2522] added unsafe_writes to blockinfile (#2701) depends on http://github.com/ansible/ansible/issues/17016 --- files/blockinfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/blockinfile.py b/files/blockinfile.py index 38e719a9707..a58e446bf7f 100755 --- a/files/blockinfile.py +++ b/files/blockinfile.py @@ -169,7 +169,7 @@ def write_changes(module, contents, dest): module.fail_json(msg='failed to validate: ' 'rc:%s error:%s' % (rc, err)) if valid: - module.atomic_move(tmpfile, dest) + module.atomic_move(tmpfile, dest, unsafe_writes=module.params['unsafe_writes']) def check_file_attrs(module, changed, message): From 99763bc91d8540705e2c1e2178f30d5c69e2a163 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Wed, 10 Aug 2016 01:50:53 -0500 Subject: [PATCH 1939/2522] Add path type in homebrew 'path' option (#2692) --- packaging/os/homebrew.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packaging/os/homebrew.py b/packaging/os/homebrew.py index d30c12d774b..fa61984e0ff 100755 --- a/packaging/os/homebrew.py +++ b/packaging/os/homebrew.py @@ -791,6 +791,7 @@ def main(): path=dict( default="/usr/local/bin", required=False, + type='path', ), state=dict( default="present", From bfbc0bd45821dc30b9ddf845ea3eef87bda7a990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthieu=20Barth=C3=A9lemy?= Date: Wed, 10 Aug 2016 01:52:18 -0500 Subject: [PATCH 1940/2522] Slack: Fix #2393 - Enable markdown parsing when using custom messsage color (#2626) --- notification/slack.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/notification/slack.py b/notification/slack.py index 40def3788a8..2ac609d451f 100644 --- a/notification/slack.py +++ b/notification/slack.py @@ -171,7 +171,8 @@ def build_payload_for_slack(module, text, channel, username, icon_url, icon_emoj if color == "normal" and text is not None: payload = dict(text=text) elif text is not None: - payload = dict(attachments=[dict(text=text, color=color)]) + # With a custom color we have to set the message as attachment, and explicitely turn markdown parsing on for it. + payload = dict(attachments=[dict(text=text, color=color, mrkdwn_in=["text"])]) if channel is not None: if (channel[0] == '#') or (channel[0] == '@'): payload['channel'] = channel From dc15d3569f5a7aabb2ac890b81827109e5b59d82 Mon Sep 17 00:00:00 2001 From: Robin Roth Date: Wed, 10 Aug 2016 08:56:39 +0200 Subject: [PATCH 1941/2522] allow to specify versions with zypper (#2328) * fixes #2158 * handles version-specifiers (>,<,>=,<=,=) correctly * adds option "oldpackage", which is passed to zypper * this is implied as soon as a version is specified * it can be used independently to allow downgrades coming from repos * add __main__ check * extend documentation on version specifier --- packaging/os/zypper.py | 97 +++++++++++++++++++++++++++++++++++------- 1 file changed, 81 insertions(+), 16 deletions(-) diff --git a/packaging/os/zypper.py b/packaging/os/zypper.py index 2ad014ec972..f958a44da4f 100644 --- a/packaging/os/zypper.py +++ b/packaging/os/zypper.py @@ -27,6 +27,7 @@ # along with Ansible. If not, see . from xml.dom.minidom import parseString as parseXML +import re DOCUMENTATION = ''' --- @@ -44,7 +45,10 @@ options: name: description: - - package name or package specifier with version C(name) or C(name-1.0). You can also pass a url or a local path to a rpm file. When using state=latest, this can be '*', which updates all installed packages. + - Package name C(name) or package specifier. + - Can include a version like C(name=1.0), C(name>3.4) or C(name<=2.7). If a version is given, C(oldpackage) is implied and zypper is allowed to update the package within the version range given. + - You can also pass a url or a local path to a rpm file. + - When using state=latest, this can be '*', which updates all installed packages. required: true aliases: [ 'pkg' ] state: @@ -92,7 +96,13 @@ default: "no" choices: [ "yes", "no" ] aliases: [ "refresh" ] - + oldpackage: + version_added: "2.2" + description: + - Adds C(--oldpackage) option to I(zypper). Allows to downgrade packages with less side-effects than force. This is implied as soon as a version is specified as part of the package name. + required: false + default: "no" + choices: [ "yes", "no" ] # informational: requirements for nodes requirements: @@ -127,25 +137,57 @@ # Refresh repositories and update package "openssl" - zypper: name=openssl state=present update_cache=yes + +# Install specific version (possible comparisons: <, >, <=, >=, =) +- zypper: name=docker>=1.10 state=installed ''' +def split_name_version(name): + """splits of the package name and desired version + + example formats: + - docker>=1.10 + - apache=2.4 + + Allowed version specifiers: <, >, <=, >=, = + Allowed version format: [0-9.-]* + + Also allows a prefix indicating remove "-", "~" or install "+" + """ + + prefix = '' + if name[0] in ['-', '~', '+']: + prefix = name[0] + name = name[1:] + + version_check = re.compile('^(.*?)((?:<|>|<=|>=|=)[0-9.-]*)?$') + try: + reres = version_check.match(name) + name, version = reres.groups() + return prefix, name, version + except: + return prefix, name, None + + def get_want_state(m, names, remove=False): - packages_install = [] - packages_remove = [] + packages_install = {} + packages_remove = {} urls = [] for name in names: if '://' in name or name.endswith('.rpm'): urls.append(name) - elif name.startswith('-') or name.startswith('~'): - packages_remove.append(name[1:]) - elif name.startswith('+'): - packages_install.append(name[1:]) else: - if remove: - packages_remove.append(name) + prefix, pname, version = split_name_version(name) + if prefix in ['-', '~']: + packages_remove[pname] = version + elif prefix == '+': + packages_install[pname] = version else: - packages_install.append(name) + if remove: + packages_remove[pname] = version + else: + packages_install[pname] = version return packages_install, packages_remove, urls @@ -216,6 +258,8 @@ def get_cmd(m, subcommand): cmd.append('--no-recommends') if m.params['force']: cmd.append('--force') + if m.params['oldpackage']: + cmd.append('--oldpackage') return cmd @@ -249,13 +293,23 @@ def package_present(m, name, want_latest): retvals = {'rc': 0, 'stdout': '', 'stderr': ''} name_install, name_remove, urls = get_want_state(m, name) + # if a version string is given, pass it to zypper + install_version = [p+name_install[p] for p in name_install if name_install[p]] + remove_version = [p+name_remove[p] for p in name_remove if name_remove[p]] + + # add oldpackage flag when a version is given to allow downgrades + if install_version or remove_version: + m.params['oldpackage'] = True + if not want_latest: # for state=present: filter out already installed packages - prerun_state = get_installed_state(m, name_install + name_remove) + install_and_remove = name_install.copy() + install_and_remove.update(name_remove) + prerun_state = get_installed_state(m, install_and_remove) # generate lists of packages to install or remove name_install = [p for p in name_install if p not in prerun_state] name_remove = [p for p in name_remove if p in prerun_state] - if not name_install and not name_remove and not urls: + if not any((name_install, name_remove, urls, install_version, remove_version)): # nothing to install/remove and nothing to update return None, retvals @@ -264,6 +318,10 @@ def package_present(m, name, want_latest): cmd.append('--') cmd.extend(urls) + # pass packages with version information + cmd.extend(install_version) + cmd.extend(['-%s' % p for p in remove_version]) + # allow for + or - prefixes in install/remove lists # do this in one zypper run to allow for dependency-resolution # for example "-exim postfix" runs without removing packages depending on mailserver @@ -303,12 +361,14 @@ def package_absent(m, name): if m.params['type'] == 'patch': m.fail_json(msg="Can not remove patches.") prerun_state = get_installed_state(m, name_remove) + remove_version = [p+name_remove[p] for p in name_remove if name_remove[p]] name_remove = [p for p in name_remove if p in prerun_state] - if not name_remove: + if not name_remove and not remove_version: return None, retvals cmd = get_cmd(m, 'remove') cmd.extend(name_remove) + cmd.extend(remove_version) retvals['cmd'] = cmd result, retvals['rc'], retvals['stdout'], retvals['stderr'] = parse_zypper_xml(m, cmd) @@ -339,6 +399,7 @@ def main(): disable_recommends = dict(required=False, default='yes', type='bool'), force = dict(required=False, default='no', type='bool'), update_cache = dict(required=False, aliases=['refresh'], default='no', type='bool'), + oldpackage = dict(required=False, default='no', type='bool'), ), supports_check_mode = True ) @@ -347,6 +408,9 @@ def main(): state = module.params['state'] update_cache = module.params['update_cache'] + # remove empty strings from package list + name = filter(None, name) + # Refresh repositories if update_cache: retvals = repo_refresh(module) @@ -378,5 +442,6 @@ def main(): module.exit_json(name=name, state=state, update_cache=update_cache, **retvals) # import module snippets -from ansible.module_utils.basic import * -main() +from ansible.module_utils.basic import AnsibleModule +if __name__ == "__main__": + main() From c600334a9d873327450ad6015055ce45e7632304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Jos=C3=A9=20Pando?= Date: Wed, 10 Aug 2016 03:01:31 -0400 Subject: [PATCH 1942/2522] elb facts fixup (#2099) * elb facts fixup * return tags and logic fixup * return tags and dont fail on nonexisting elb name --- cloud/amazon/ec2_elb_facts.py | 245 +++++++++++++++++++--------------- 1 file changed, 137 insertions(+), 108 deletions(-) diff --git a/cloud/amazon/ec2_elb_facts.py b/cloud/amazon/ec2_elb_facts.py index 02ea0f90626..e386439d1d5 100644 --- a/cloud/amazon/ec2_elb_facts.py +++ b/cloud/amazon/ec2_elb_facts.py @@ -20,7 +20,9 @@ description: - Gather facts about EC2 Elastic Load Balancers in AWS version_added: "2.0" -author: "Michael Schultz (github.com/mjschultz)" +author: + - "Michael Schultz (github.com/mjschultz)" + - "Fernando Jose Pando (@nand0p)" options: names: description: @@ -72,126 +74,149 @@ ''' -import xml.etree.ElementTree as ET - try: import boto.ec2.elb + from boto.ec2.tag import Tag from boto.exception import BotoServerError HAS_BOTO = True except ImportError: HAS_BOTO = False +class ElbInformation(object): + """ Handles ELB information """ + + def __init__(self, + module, + names, + region, + **aws_connect_params): -def get_error_message(xml_string): + self.module = module + self.names = names + self.region = region + self.aws_connect_params = aws_connect_params + self.connection = self._get_elb_connection() - root = ET.fromstring(xml_string) - for message in root.findall('.//Message'): - return message.text + def _get_tags(self, elbname): + params = {'LoadBalancerNames.member.1': elbname} + try: + elb_tags = self.connection.get_list('DescribeTags', params, [('member', Tag)]) + return dict((tag.Key, tag.Value) for tag in elb_tags if hasattr(tag, 'Key')) + except: + return {} + def _get_elb_connection(self): + try: + return connect_to_aws(boto.ec2.elb, self.region, **self.aws_connect_params) + except BotoServerError as err: + self.module.fail_json(msg=err.message) + + def _get_elb_listeners(self, listeners): + listener_list = [] + + for listener in listeners: + listener_dict = { + 'load_balancer_port': listener[0], + 'instance_port': listener[1], + 'protocol': listener[2], + } + + try: + ssl_certificate_id = listener[4] + except IndexError: + pass + else: + if ssl_certificate_id: + listener_dict['ssl_certificate_id'] = ssl_certificate_id + + listener_list.append(listener_dict) + + return listener_list + + def _get_health_check(self, health_check): + protocol, port_path = health_check.target.split(':') + try: + port, path = port_path.split('/', 1) + path = '/{}'.format(path) + except ValueError: + port = port_path + path = None + + health_check_dict = { + 'ping_protocol': protocol.lower(), + 'ping_port': int(port), + 'response_timeout': health_check.timeout, + 'interval': health_check.interval, + 'unhealthy_threshold': health_check.unhealthy_threshold, + 'healthy_threshold': health_check.healthy_threshold, + } -def get_elb_listeners(listeners): - listener_list = [] - for listener in listeners: - listener_dict = { - 'load_balancer_port': listener[0], - 'instance_port': listener[1], - 'protocol': listener[2], + if path: + health_check_dict['ping_path'] = path + return health_check_dict + + def _get_elb_info(self, elb): + elb_info = { + 'name': elb.name, + 'zones': elb.availability_zones, + 'dns_name': elb.dns_name, + 'canonical_hosted_zone_name': elb.canonical_hosted_zone_name, + 'canonical_hosted_zone_name_id': elb.canonical_hosted_zone_name_id, + 'hosted_zone_name': elb.canonical_hosted_zone_name, + 'hosted_zone_id': elb.canonical_hosted_zone_name_id, + 'instances': [instance.id for instance in elb.instances], + 'listeners': self._get_elb_listeners(elb.listeners), + 'scheme': elb.scheme, + 'security_groups': elb.security_groups, + 'health_check': self._get_health_check(elb.health_check), + 'subnets': elb.subnets, + 'instances_inservice': [], + 'instances_inservice_count': 0, + 'instances_outofservice': [], + 'instances_outofservice_count': 0, + 'instances_inservice_percent': 0.0, + 'tags': self._get_tags(elb.name) } + + if elb.vpc_id: + elb_info['vpc_id'] = elb.vpc_id + + if elb.instances: + try: + instance_health = self.connection.describe_instance_health(elb.name) + except BotoServerError as err: + self.module.fail_json(msg=err.message) + elb_info['instances_inservice'] = [inst.instance_id for inst in instance_health if inst.state == 'InService'] + elb_info['instances_inservice_count'] = len(elb_info['instances_inservice']) + elb_info['instances_outofservice'] = [inst.instance_id for inst in instance_health if inst.state == 'OutOfService'] + elb_info['instances_outofservice_count'] = len(elb_info['instances_outofservice']) + elb_info['instances_inservice_percent'] = float(elb_info['instances_inservice_count'])/( + float(elb_info['instances_inservice_count']) + + float(elb_info['instances_outofservice_count']))*100 + return elb_info + + + def list_elbs(self): + elb_array = [] + try: - ssl_certificate_id = listener[4] - except IndexError: - pass - else: - if ssl_certificate_id: - listener_dict['ssl_certificate_id'] = ssl_certificate_id - listener_list.append(listener_dict) - - return listener_list - - -def get_health_check(health_check): - protocol, port_path = health_check.target.split(':') - try: - port, path = port_path.split('/', 1) - path = '/{}'.format(path) - except ValueError: - port = port_path - path = None - - health_check_dict = { - 'ping_protocol': protocol.lower(), - 'ping_port': int(port), - 'response_timeout': health_check.timeout, - 'interval': health_check.interval, - 'unhealthy_threshold': health_check.unhealthy_threshold, - 'healthy_threshold': health_check.healthy_threshold, - } - if path: - health_check_dict['ping_path'] = path - return health_check_dict - - -def get_elb_info(connection,elb): - elb_info = { - 'name': elb.name, - 'zones': elb.availability_zones, - 'dns_name': elb.dns_name, - 'canonical_hosted_zone_name': elb.canonical_hosted_zone_name, - 'canonical_hosted_zone_name_id': elb.canonical_hosted_zone_name_id, - 'hosted_zone_name': elb.canonical_hosted_zone_name, - 'hosted_zone_id': elb.canonical_hosted_zone_name_id, - 'instances': [instance.id for instance in elb.instances], - 'listeners': get_elb_listeners(elb.listeners), - 'scheme': elb.scheme, - 'security_groups': elb.security_groups, - 'health_check': get_health_check(elb.health_check), - 'subnets': elb.subnets, - 'instances_inservice': [], - 'instances_inservice_count': 0, - 'instances_outofservice': [], - 'instances_outofservice_count': 0, - 'instances_inservice_percent': 0.0, - } - if elb.vpc_id: - elb_info['vpc_id'] = elb.vpc_id - if elb.instances: - instance_health = connection.describe_instance_health(elb.name) - elb_info['instances_inservice'] = [inst.instance_id for inst in instance_health if inst.state == 'InService'] - elb_info['instances_inservice_count'] = len(elb_info['instances_inservice']) - elb_info['instances_outofservice'] = [inst.instance_id for inst in instance_health if inst.state == 'OutOfService'] - elb_info['instances_outofservice_count'] = len(elb_info['instances_outofservice']) - elb_info['instances_inservice_percent'] = float(elb_info['instances_inservice_count'])/( - float(elb_info['instances_inservice_count']) + - float(elb_info['instances_outofservice_count']))*100 - - return elb_info - - -def list_elb(connection, module): - elb_names = module.params.get("names") - if not elb_names: - elb_names = None - - try: - all_elbs = connection.get_all_load_balancers(elb_names) - except BotoServerError as e: - module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message)) - - elb_array = [] - for elb in all_elbs: - elb_array.append(get_elb_info(connection,elb)) - - module.exit_json(elbs=elb_array) + all_elbs = self.connection.get_all_load_balancers() + except BotoServerError as err: + self.module.fail_json(msg = "%s: %s" % (err.error_code, err.error_message)) + + if all_elbs: + for existing_lb in all_elbs: + if existing_lb.name in self.names: + elb_array.append(self._get_elb_info(existing_lb)) + return elb_array def main(): argument_spec = ec2_argument_spec() - argument_spec.update( - dict( + argument_spec.update(dict( names={'default': None, 'type': 'list'} ) ) - module = AnsibleModule(argument_spec=argument_spec) if not HAS_BOTO: @@ -199,15 +224,19 @@ def main(): region, ec2_url, aws_connect_params = get_aws_connection_info(module) - if region: - try: - connection = connect_to_aws(boto.ec2.elb, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: - module.fail_json(msg=str(e)) - else: + if not region: module.fail_json(msg="region must be specified") - list_elb(connection, module) + names = module.params['names'] + elb_information = ElbInformation(module, + names, + region, + **aws_connect_params) + + ec2_facts_result = dict(changed=False, + elbs=elb_information.list_elbs()) + + module.exit_json(**ec2_facts_result) from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * From ea8cb33338618a79b23435eb852e8bc64a1d5da0 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Wed, 10 Aug 2016 06:28:18 -0500 Subject: [PATCH 1943/2522] Update homebrew_tap to support custom tap URL via optional 'url' option (#2672) This allows doing 'brew tap ' where the URL is not assumed to be on GitHub, and the protocol doesn't have to be HTTP. Any location and protocol that git can handle is fine. While at it, allow proper `list` type support for 'name' option and update module documentation for option aliases. --- packaging/os/homebrew_tap.py | 50 ++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/packaging/os/homebrew_tap.py b/packaging/os/homebrew_tap.py index e871adb3560..9264db8775e 100644 --- a/packaging/os/homebrew_tap.py +++ b/packaging/os/homebrew_tap.py @@ -2,6 +2,8 @@ # -*- coding: utf-8 -*- # (c) 2013, Daniel Jaouen +# (c) 2016, Indrajit Raychaudhuri +# # Based on homebrew (Andrew Dunham ) # # This file is part of Ansible @@ -24,16 +26,29 @@ DOCUMENTATION = ''' --- module: homebrew_tap -author: "Daniel Jaouen (@danieljaouen)" +author: + - "Indrajit Raychaudhuri (@indrajitr)" + - "Daniel Jaouen (@danieljaouen)" short_description: Tap a Homebrew repository. description: - Tap external Homebrew repositories. version_added: "1.6" options: - tap: + name: description: - - The repository to tap. + - The GitHub user/organization repository to tap. required: true + aliases: ['tap'] + url: + description: + - The optional git URL of the repository to tap. The URL is not + assumed to be on GitHub, and the protocol doesn't have to be HTTP. + Any location and protocol that git can handle is fine. + required: false + version_added: "2.2" + note: + - I(name) option may not be a list of multiple taps (but a single + tap instead) when this option is provided. state: description: - state of the repository. @@ -44,9 +59,10 @@ ''' EXAMPLES = ''' -homebrew_tap: tap=homebrew/dupes state=present -homebrew_tap: tap=homebrew/dupes state=absent -homebrew_tap: tap=homebrew/dupes,homebrew/science state=present +homebrew_tap: name=homebrew/dupes +homebrew_tap: name=homebrew/dupes state=absent +homebrew_tap: name=homebrew/dupes,homebrew/science state=present +homebrew_tap: name=telemachus/brew url=https://bitbucket.org/telemachus/brew ''' @@ -70,7 +86,7 @@ def already_tapped(module, brew_path, tap): return tap_name in taps -def add_tap(module, brew_path, tap): +def add_tap(module, brew_path, tap, url=None): '''Adds a single tap.''' failed, changed, msg = False, False, '' @@ -86,6 +102,7 @@ def add_tap(module, brew_path, tap): brew_path, 'tap', tap, + url, ]) if already_tapped(module, brew_path, tap): changed = True @@ -183,7 +200,8 @@ def remove_taps(module, brew_path, taps): def main(): module = AnsibleModule( argument_spec=dict( - name=dict(aliases=['tap'], required=True), + name=dict(aliases=['tap'], type='list', required=True), + url=dict(default=None, required=False), state=dict(default='present', choices=['present', 'absent']), ), supports_check_mode=True, @@ -195,10 +213,22 @@ def main(): opt_dirs=['/usr/local/bin'] ) - taps = module.params['name'].split(',') + taps = module.params['name'] + url = module.params['url'] if module.params['state'] == 'present': - failed, changed, msg = add_taps(module, brew_path, taps) + if url is None: + # No tap URL provided explicitly, continue with bulk addition + # of all the taps. + failed, changed, msg = add_taps(module, brew_path, taps) + else: + # When an tap URL is provided explicitly, we allow adding + # *single* tap only. Validate and proceed to add single tap. + if len(taps) > 1: + msg = "List of muliple taps may not be provided with 'url' option." + module.fail_json(msg=msg) + else: + failed, changed, msg = add_tap(module, brew_path, taps[0], url) if failed: module.fail_json(msg=msg) From 39153ea1548d208545a31d4fa581c70c22e90c98 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Wed, 10 Aug 2016 06:32:36 -0500 Subject: [PATCH 1944/2522] Update homebrew_cask with additional cask features and doc updates (#2682) Changes: - Document missing 'path' option and remove redundant brew_path manipulation - Add 'update_homebrew' option since 'brew cask update' as a synonym for 'brew update' is available nowadays - Add additional missing aliases documentation - Port additional improvements (expanded support for brews path, cask path patterns etc.) - Allow proper `list` type support for 'name' option. --- packaging/os/homebrew_cask.py | 81 ++++++++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 21 deletions(-) diff --git a/packaging/os/homebrew_cask.py b/packaging/os/homebrew_cask.py index 8353c1cece2..debcb788ea4 100755 --- a/packaging/os/homebrew_cask.py +++ b/packaging/os/homebrew_cask.py @@ -35,17 +35,33 @@ description: - name of cask to install/remove required: true + aliases: ['pkg', 'package', 'cask'] + path: + description: + - "':' separated list of paths to search for 'brew' executable." + required: false + default: '/usr/local/bin' state: description: - state of the cask choices: [ 'present', 'absent' ] required: false default: present + update_homebrew: + description: + - update homebrew itself first. Note that C(brew cask update) is + a synonym for C(brew update). + required: false + default: no + choices: [ "yes", "no" ] + aliases: ['update-brew'] + version_added: "2.2" install_options: description: - options flags to install a package required: false default: null + aliases: ['options'] version_added: "2.2" ''' EXAMPLES = ''' @@ -84,6 +100,7 @@ class HomebrewCask(object): \s # spaces : # colons {sep} # the OS-specific path separator + . # dots - # dashes '''.format(sep=os.path.sep) @@ -91,11 +108,14 @@ class HomebrewCask(object): \w # alphanumeric characters (i.e., [a-zA-Z0-9_]) \s # spaces {sep} # the OS-specific path separator + . # dots - # dashes '''.format(sep=os.path.sep) VALID_CASK_CHARS = r''' \w # alphanumeric characters (i.e., [a-zA-Z0-9_]) + . # dots + / # slash (for taps) - # dashes ''' @@ -113,6 +133,7 @@ def valid_path(cls, path): - a string containing only: - alphanumeric characters - dashes + - dots - spaces - colons - os.path.sep @@ -137,6 +158,7 @@ def valid_brew_path(cls, brew_path): - a string containing only: - alphanumeric characters - dashes + - dots - spaces - os.path.sep ''' @@ -185,6 +207,7 @@ def valid_module(cls, module): '''A valid module is an instance of AnsibleModule.''' return isinstance(module, AnsibleModule) + # /class validations ------------------------------------------- }}} # class properties --------------------------------------------- {{{ @@ -266,11 +289,14 @@ def current_cask(self, cask): return cask # /class properties -------------------------------------------- }}} - def __init__(self, module, path=None, casks=None, state=None, - install_options=None): + def __init__(self, module, path=path, casks=None, state=None, + update_homebrew=False, install_options=None): + if not install_options: + install_options = list() self._setup_status_vars() self._setup_instance_vars(module=module, path=path, casks=casks, - state=state, install_options=install_options) + state=state, update_homebrew=update_homebrew, + install_options=install_options,) self._prep() @@ -287,13 +313,8 @@ def _setup_instance_vars(self, **kwargs): setattr(self, key, val) def _prep(self): - self._prep_path() self._prep_brew_path() - def _prep_path(self): - if not self.path: - self.path = ['/usr/local/bin'] - def _prep_brew_path(self): if not self.module: self.brew_path = None @@ -340,8 +361,12 @@ def _current_cask_is_installed(self): self.message = 'Invalid cask: {0}.'.format(self.current_cask) raise HomebrewCaskException(self.message) - cmd = [self.brew_path, 'cask', 'list'] - rc, out, err = self.module.run_command(cmd, path_prefix=self.path[0]) + cmd = [ + "{brew_path}".format(brew_path=self.brew_path), + "cask", + "list" + ] + rc, out, err = self.module.run_command(cmd) if 'nothing to list' in err: return False @@ -356,6 +381,9 @@ def _current_cask_is_installed(self): # commands ----------------------------------------------------- {{{ def _run(self): + if self.update_homebrew: + self._update_homebrew() + if self.state == 'installed': return self._install_casks() elif self.state == 'absent': @@ -369,7 +397,7 @@ def _update_homebrew(self): rc, out, err = self.module.run_command([ self.brew_path, 'update', - ], path_prefix=self.path[0]) + ]) if rc == 0: if out and isinstance(out, basestring): already_updated = any( @@ -417,8 +445,7 @@ def _install_current_cask(self): ) cmd = [opt for opt in opts if opt] - - rc, out, err = self.module.run_command(cmd, path_prefix=self.path[0]) + rc, out, err = self.module.run_command(cmd) if self._current_cask_is_installed(): self.changed_count += 1 @@ -463,7 +490,7 @@ def _uninstall_current_cask(self): for opt in (self.brew_path, 'cask', 'uninstall', self.current_cask) if opt] - rc, out, err = self.module.run_command(cmd, path_prefix=self.path[0]) + rc, out, err = self.module.run_command(cmd) if not self._current_cask_is_installed(): self.changed_count += 1 @@ -488,8 +515,16 @@ def _uninstall_casks(self): def main(): module = AnsibleModule( argument_spec=dict( - name=dict(aliases=["cask"], required=False), - path=dict(required=False), + name=dict( + aliases=["pkg", "package", "cask"], + required=False, + type='list', + ), + path=dict( + default="/usr/local/bin", + required=False, + type='path', + ), state=dict( default="present", choices=[ @@ -497,6 +532,11 @@ def main(): "absent", "removed", "uninstalled", ], ), + update_homebrew=dict( + default=False, + aliases=["update-brew"], + type='bool', + ), install_options=dict( default=None, aliases=['options'], @@ -511,15 +551,13 @@ def main(): p = module.params if p['name']: - casks = p['name'].split(',') + casks = p['name'] else: casks = None path = p['path'] if path: path = path.split(':') - else: - path = ['/usr/local/bin'] state = p['state'] if state in ('present', 'installed'): @@ -527,13 +565,14 @@ def main(): if state in ('absent', 'removed', 'uninstalled'): state = 'absent' - + update_homebrew = p['update_homebrew'] p['install_options'] = p['install_options'] or [] install_options = ['--{0}'.format(install_option) for install_option in p['install_options']] brew_cask = HomebrewCask(module=module, path=path, casks=casks, - state=state, install_options=install_options) + state=state, update_homebrew=update_homebrew, + install_options=install_options) (failed, changed, message) = brew_cask.run() if failed: module.fail_json(msg=message) From 5fe51b44223a88e01e10c5cbf51755307b3f7ba8 Mon Sep 17 00:00:00 2001 From: jctanner Date: Wed, 10 Aug 2016 12:46:02 -0400 Subject: [PATCH 1945/2522] Fix vmware_guest disk and nic parameters and docstrings (#2705) * Fix vmware_guest disk and nic parameters and docstrings * vmware_guest: remove default for required params --- cloud/vmware/vmware_guest.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cloud/vmware/vmware_guest.py b/cloud/vmware/vmware_guest.py index 1f12a6c4f10..4754068ec24 100644 --- a/cloud/vmware/vmware_guest.py +++ b/cloud/vmware/vmware_guest.py @@ -65,6 +65,10 @@ description: - Attributes such as cpus, memroy, osid, and disk controller required: False + disk: + description: + - A list of disks to add + required: False nic: description: - A list of nics to add @@ -791,8 +795,8 @@ def main(): name_match=dict(required=False, type='str', default='first'), uuid=dict(required=False, type='str'), folder=dict(required=False, type='str', default=None, aliases=['folder']), - disk=dict(required=False, type='list', default=[]), - nic=dict(required=False, type='list', default=[]), + disk=dict(required=True, type='list'), + nic=dict(required=True, type='list'), hardware=dict(required=False, type='dict', default={}), force=dict(required=False, type='bool', default=False), datacenter=dict(required=False, type='str', default=None), From 8ba7f008778850748a8b90ebec201f4c9ebb1b27 Mon Sep 17 00:00:00 2001 From: Robyn Bergeron Date: Wed, 10 Aug 2016 11:07:45 -0700 Subject: [PATCH 1946/2522] creating GUIDELINES.md Made a copy from MAINTAINERS.md (and then need to delete maintainers.md). --- GUIDELINES.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 GUIDELINES.md diff --git a/GUIDELINES.md b/GUIDELINES.md new file mode 100644 index 00000000000..9e1e53cfb08 --- /dev/null +++ b/GUIDELINES.md @@ -0,0 +1,60 @@ +# Module Maintainer Guidelines + +Thank you for being a maintainer of one of the modules in ansible-modules-extras! This guide provides module maintainers an overview of their responsibilities, resources for additional information, and links to helpful tools. + +In addition to the information below, module maintainers should be familiar with: +* General Ansible community development practices (http://docs.ansible.com/ansible/community.html) +* Documentation on module development (http://docs.ansible.com/ansible/developing_modules.html) +* Any namespace-specific module guidelines (identified as GUIDELINES.md in the appropriate file tree). + +*** + +# Maintainer Responsibilities + +When you contribute a new module to the ansible-modules-extras repository, you become the maintainer for that module once it has been merged. Maintainership empowers you with the authority to accept, reject, or request revisions to pull requests on your module -- but as they say, "with great power comes great responsibility." + +Maintainers of Ansible modules are expected to provide feedback, responses, or actions on pull requests or issues to the module(s) they maintain in a reasonably timely manner. + +The Ansible community hopes that you will find that maintaining your module is as rewarding for you as having the module is for the wider community. + +*** + +# Pull Requests and Issues + +## Pull Requests + +Module pull requests are located in the [ansible-modules-extras repository](https://github.com/ansible/ansible-modules-extras/pulls). + +Because of the high volume of pull requests, notification of PRs to specific modules are routed by an automated bot to the appropriate maintainer for handling. It is recommended that you set an appropriate notification process to receive notifications which mention your GitHub ID. + +## Issues + +Issues for modules, including bug reports, documentation bug reports, and feature requests, are tracked in the [ansible-modules-extras repository](https://github.com/ansible/ansible-modules-extras/issues). + +At this time, we do not have an automated process by which Issues are handled. If you are a maintainer of a specific module, it is recommended that you periodically search module issues for issues which mention your module's name (or some variation on that name), as well as setting an appropriate notification process for receiving notification of mentions of your GitHub ID. + +*** + +# Extras maintainers list + +The full list of maintainers for modules in ansible-modules-extras is located here: +https://github.com/ansible/ansibullbot/blob/master/MAINTAINERS-EXTRAS.txt + +## Changing Maintainership + +Communities change over time, and no one maintains a module forever. If you'd like to propose an additional maintainer for your module, please submit a PR to the maintainers file with the Github ID of the new maintainer. + +If you'd like to step down as a maintainer, please submit a PR to the maintainers file removing your Github ID from the module in question. If that would leave the module with no maintainers, put "ansible" as the maintainer. This will indicate that the module is temporarily without a maintainer, and the Ansible community team will search for a new maintainer. + +*** + +# Tools and other Resources + +## Useful tools +* https://ansible.sivel.net/pr/byfile.html -- a full list of all open Pull Requests, organized by file. +* https://github.com/sivel/ansible-testing -- these are the tests that run in Travis against all PRs for extras modules, so it's a good idea to run these tests locally first. + +## Other Resources + +* Module maintainer list: https://github.com/ansible/ansibullbot/blob/master/MAINTAINERS-EXTRAS.txt +* Ansibullbot: https://github.com/ansible/ansibullbot From 90ae6e37616307d2ef5ab46d49f315ba3a314371 Mon Sep 17 00:00:00 2001 From: Robyn Bergeron Date: Wed, 10 Aug 2016 12:27:32 -0700 Subject: [PATCH 1947/2522] Update GUIDELINES.md --- GUIDELINES.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/GUIDELINES.md b/GUIDELINES.md index 9e1e53cfb08..ddd918d4737 100644 --- a/GUIDELINES.md +++ b/GUIDELINES.md @@ -15,11 +15,15 @@ When you contribute a new module to the ansible-modules-extras repository, you b Maintainers of Ansible modules are expected to provide feedback, responses, or actions on pull requests or issues to the module(s) they maintain in a reasonably timely manner. +It is also recommended that you occasionally revisit the [contribution guidelines](https://github.com/alikins/ansible-modules-extras/commit/c87795da5b0c95c67fea1608a5a2a4ec54cb3905), as they are continually refined. Occasionally, you may be requested to update your module to move it closer to the general accepted standard requirements; we hope for this to be infrequent, and will always be a request with a fair amount of lead time (ie: not by tomorrow!). + +Finally, following the ansible-devel mailing list can be a great way to participate in the broader Ansible community, and a place where you can influence the overall direction, quality, and goals of the Extras modules repository. If you're not on this relatively low-volume list, please join us here: https://groups.google.com/forum/#!forum/ansible-devel + The Ansible community hopes that you will find that maintaining your module is as rewarding for you as having the module is for the wider community. *** -# Pull Requests and Issues +# Pull Requests, Issues, and Workflow ## Pull Requests @@ -33,6 +37,14 @@ Issues for modules, including bug reports, documentation bug reports, and featur At this time, we do not have an automated process by which Issues are handled. If you are a maintainer of a specific module, it is recommended that you periodically search module issues for issues which mention your module's name (or some variation on that name), as well as setting an appropriate notification process for receiving notification of mentions of your GitHub ID. +## PR Workflow + +Automated routing of pull requests is handled by a tool called [Ansibullbot](https://github.com/ansible/ansibullbot). (You could say that he moooo-ves things around.) + +Being moderately familiar with how the workflow behind the bot operates can be helpful to you, and -- should things go awry -- your feedback can be helpful to the folks that continually help Ansibullbot to evolve. + +A detailed explanation of the PR workflow can be seen here: https://github.com/ansible/community/blob/master/PR-FLOW.md + *** # Extras maintainers list @@ -58,3 +70,4 @@ If you'd like to step down as a maintainer, please submit a PR to the maintainer * Module maintainer list: https://github.com/ansible/ansibullbot/blob/master/MAINTAINERS-EXTRAS.txt * Ansibullbot: https://github.com/ansible/ansibullbot +* Triage / pull request workflow and information, including definitions for Labels in GitHub: https://github.com/ansible/community/blob/master/PR-FLOW.md From d1355b20eed780f309d1c4d80bc73c4124512f1b Mon Sep 17 00:00:00 2001 From: Naoya Nakazawa Date: Thu, 11 Aug 2016 14:35:52 +0900 Subject: [PATCH 1948/2522] ready_for_review datadog_event module: Datadog API http status code 202 is ok. Ref: http://docs.datadoghq.com/api/ (#2117) * Use official datadog create event API. * Fix exception --- monitoring/datadog_event.py | 86 ++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/monitoring/datadog_event.py b/monitoring/datadog_event.py index de3b73c2103..88d921bf912 100644 --- a/monitoring/datadog_event.py +++ b/monitoring/datadog_event.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # # Author: Artūras 'arturaz' Šlajus +# Author: Naoya Nakazawa # # This module is proudly sponsored by iGeolise (www.igeolise.com) and # Tiny Lab Productions (www.tinylabproductions.com). @@ -21,6 +22,12 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +# Import Datadog +try: + from datadog import initialize, api + HAS_DATADOG = True +except: + HAS_DATADOG = False DOCUMENTATION = ''' --- @@ -30,7 +37,9 @@ - "Allows to post events to DataDog (www.datadoghq.com) service." - "Uses http://docs.datadoghq.com/api/#events API." version_added: "1.3" -author: "Artūras `arturaz` Šlajus (@arturaz)" +author: +- "Artūras `arturaz` Šlajus (@arturaz)" +- "Naoya Nakazawa (@n0ts)" notes: [] requirements: [] options: @@ -38,6 +47,10 @@ description: ["Your DataDog API key."] required: true default: null + app_key: + description: ["Your DataDog app key."] + required: true + version_added: "2.2" title: description: ["The event title."] required: true @@ -83,17 +96,21 @@ EXAMPLES = ''' # Post an event with low priority datadog_event: title="Testing from ansible" text="Test!" priority="low" - api_key="6873258723457823548234234234" + api_key: "9775a026f1ca7d1c6c5af9d94d9595a4" + app_key: "j4JyCYfefWHhgFgiZUqRm63AXHNZQyPGBfJtAzmN" # Post an event with several tags datadog_event: title="Testing from ansible" text="Test!" - api_key="6873258723457823548234234234" + api_key: "9775a026f1ca7d1c6c5af9d94d9595a4" + app_key: "j4JyCYfefWHhgFgiZUqRm63AXHNZQyPGBfJtAzmN" tags=aa,bb,#host:{{ inventory_hostname }} ''' +# Import Datadog def main(): module = AnsibleModule( argument_spec=dict( api_key=dict(required=True, no_log=True), + app_key=dict(required=True, no_log=True), title=dict(required=True), text=dict(required=True), date_happened=dict(required=False, default=None, type='int'), @@ -106,51 +123,42 @@ def main(): choices=['error', 'warning', 'info', 'success'] ), aggregation_key=dict(required=False, default=None), - source_type_name=dict( - required=False, default='my apps', - choices=['nagios', 'hudson', 'jenkins', 'user', 'my apps', - 'feed', 'chef', 'puppet', 'git', 'bitbucket', 'fabric', - 'capistrano'] - ), validate_certs = dict(default='yes', type='bool'), ) ) - post_event(module) + # Prepare Datadog + if not HAS_DATADOG: + module.fail_json(msg='datadogpy required for this module') -def post_event(module): - uri = "https://app.datadoghq.com/api/v1/events?api_key=%s" % module.params['api_key'] + options = { + 'api_key': module.params['api_key'], + 'app_key': module.params['app_key'] + } - body = dict( - title=module.params['title'], - text=module.params['text'], - priority=module.params['priority'], - alert_type=module.params['alert_type'] - ) - if module.params['date_happened'] != None: - body['date_happened'] = module.params['date_happened'] - if module.params['tags'] != None: - body['tags'] = module.params['tags'] - if module.params['aggregation_key'] != None: - body['aggregation_key'] = module.params['aggregation_key'] - if module.params['source_type_name'] != None: - body['source_type_name'] = module.params['source_type_name'] + initialize(**options) + + _post_event(module) + + +def _post_event(module): + try: + msg = api.Event.create(title=module.params['title'], + text=module.params['text'], + tags=module.params['tags'], + priority=module.params['priority'], + alert_type=module.params['alert_type'], + aggregation_key=module.params['aggregation_key'], + source_type_name='ansible') + if msg['status'] != 'ok': + module.fail_json(msg=msg) - json_body = module.jsonify(body) - headers = {"Content-Type": "application/json"} + module.exit_json(changed=True, msg=msg) + except Exception: + e = get_exception() + module.fail_json(msg=str(e)) - (response, info) = fetch_url(module, uri, data=json_body, headers=headers) - if info['status'] == 202: - response_body = response.read() - response_json = module.from_json(response_body) - if response_json['status'] == 'ok': - module.exit_json(changed=True) - else: - module.fail_json(msg=response) - else: - module.fail_json(**info) -# import module snippets from ansible.module_utils.basic import * from ansible.module_utils.urls import * if __name__ == '__main__': From db463e44b35dad1f4e74d6425cb66233857228c0 Mon Sep 17 00:00:00 2001 From: Jose Armesto Date: Thu, 11 Aug 2016 20:16:16 +0200 Subject: [PATCH 1949/2522] Added module to find Launch Configurations (#1023) * Added module to find Launch Configurations * Simplified parameters to search --- cloud/amazon/ec2_lc_find.py | 224 ++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 cloud/amazon/ec2_lc_find.py diff --git a/cloud/amazon/ec2_lc_find.py b/cloud/amazon/ec2_lc_find.py new file mode 100644 index 00000000000..32e0d0eb3a8 --- /dev/null +++ b/cloud/amazon/ec2_lc_find.py @@ -0,0 +1,224 @@ +#!/usr/bin/python +# encoding: utf-8 + +# (c) 2015, Jose Armesto +# +# This file is part of Ansible +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + +DOCUMENTATION = """ +--- +module: ec2_lc_find +short_description: Find AWS Autoscaling Launch Configurations +description: + - Returns list of matching Launch Configurations for a given name, along with other useful information + - Results can be sorted and sliced + - It depends on boto + - Based on the work by Tom Bamford (https://github.com/tombamford) + +version_added: "2.2" +author: "Jose Armesto (@fiunchinho)" +options: + region: + description: + - The AWS region to use. + required: true + aliases: ['aws_region', 'ec2_region'] + name_regex: + description: + - A Launch Configuration to match + - It'll be compiled as regex + required: True + sort_order: + description: + - Order in which to sort results. + choices: ['ascending', 'descending'] + default: 'ascending' + required: false + limit: + description: + - How many results to show. + - Corresponds to Python slice notation like list[:limit]. + default: null + required: false +requirements: + - "python >= 2.6" + - boto3 +""" + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Search for the Launch Configurations that start with "app" +- ec2_lc_find: + name_regex: app.* + sort_order: descending + limit: 2 +''' + +RETURN = ''' +image_id: + description: AMI id + returned: when Launch Configuration was found + type: string + sample: "ami-0d75df7e" +user_data: + description: User data used to start instance + returned: when Launch Configuration was found + type: string + user_data: "ZXhwb3J0IENMT1VE" +name: + description: Name of the AMI + returned: when Launch Configuration was found + type: string + sample: "myapp-v123" +arn: + description: Name of the AMI + returned: when Launch Configuration was found + type: string + sample: "arn:aws:autoscaling:eu-west-1:12345:launchConfiguration:d82f050e-e315:launchConfigurationName/yourproject" +instance_type: + description: Type of ec2 instance + returned: when Launch Configuration was found + type: string + sample: "t2.small" +created_time: + description: When it was created + returned: when Launch Configuration was found + type: string + sample: "2016-06-29T14:59:22.222000+00:00" +ebs_optimized: + description: Launch Configuration EBS optimized property + returned: when Launch Configuration was found + type: boolean + sample: False +instance_monitoring: + description: Launch Configuration instance monitoring property + returned: when Launch Configuration was found + type: string + sample: {"Enabled": false} +classic_link_vpc_security_groups: + description: Launch Configuration classic link vpc security groups property + returned: when Launch Configuration was found + type: list + sample: [] +block_device_mappings: + description: Launch Configuration block device mappings property + returned: when Launch Configuration was found + type: list + sample: [] +keyname: + description: Launch Configuration ssh key + returned: when Launch Configuration was found + type: string + sample: mykey +security_groups: + description: Launch Configuration security groups + returned: when Launch Configuration was found + type: list + sample: [] +kernel_id: + description: Launch Configuration kernel to use + returned: when Launch Configuration was found + type: string + sample: '' +ram_disk_id: + description: Launch Configuration ram disk property + returned: when Launch Configuration was found + type: string + sample: '' +associate_public_address: + description: Assign public address or not + returned: when Launch Configuration was found + type: boolean + sample: True +... +''' + + +def find_launch_configs(client, module): + name_regex = module.params.get('name_regex') + sort_order = module.params.get('sort_order') + limit = module.params.get('limit') + + paginator = client.get_paginator('describe_launch_configurations') + + response_iterator = paginator.paginate( + PaginationConfig={ + 'MaxItems': 1000, + 'PageSize': 100 + } + ) + + for response in response_iterator: + response['LaunchConfigurations'] = filter(lambda lc: re.compile(name_regex).match(lc['LaunchConfigurationName']), + response['LaunchConfigurations']) + + results = [] + for lc in response['LaunchConfigurations']: + data = { + 'name': lc['LaunchConfigurationName'], + 'arn': lc['LaunchConfigurationARN'], + 'created_time': lc['CreatedTime'], + 'user_data': lc['UserData'], + 'instance_type': lc['InstanceType'], + 'image_id': lc['ImageId'], + 'ebs_optimized': lc['EbsOptimized'], + 'instance_monitoring': lc['InstanceMonitoring'], + 'classic_link_vpc_security_groups': lc['ClassicLinkVPCSecurityGroups'], + 'block_device_mappings': lc['BlockDeviceMappings'], + 'keyname': lc['KeyName'], + 'security_groups': lc['SecurityGroups'], + 'kernel_id': lc['KernelId'], + 'ram_disk_id': lc['RamdiskId'], + 'associate_public_address': lc['AssociatePublicIpAddress'], + } + + results.append(data) + + results.sort(key=lambda e: e['name'], reverse=(sort_order == 'descending')) + + if limit: + results = results[:int(limit)] + + module.exit_json(changed=False, results=results) + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + region=dict(required=True, aliases=['aws_region', 'ec2_region']), + name_regex=dict(required=True), + sort_order=dict(required=False, default='ascending', choices=['ascending', 'descending']), + limit=dict(required=False, type='int'), + ) + ) + + module = AnsibleModule( + argument_spec=argument_spec, + ) + + region, ec2_url, aws_connect_params = get_aws_connection_info(module, True) + + client = boto3_conn(module=module, conn_type='client', resource='autoscaling', region=region, **aws_connect_params) + find_launch_configs(client, module) + + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From d4e389971048271862be2bb8b066bb4998e062ee Mon Sep 17 00:00:00 2001 From: Trevor Kensiski Date: Thu, 11 Aug 2016 14:18:06 -0700 Subject: [PATCH 1950/2522] Adding datadog monitor locked option. http://docs.datadoghq.com/api/#monitor-create (#2698) --- monitoring/datadog_monitor.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py index 6003664fd2d..208dc73305e 100644 --- a/monitoring/datadog_monitor.py +++ b/monitoring/datadog_monitor.py @@ -103,6 +103,11 @@ description: ["A dictionary of thresholds by status. This option is only available for service checks and metric alerts. Because each of them can have multiple thresholds, we don't define them directly in the query."] required: false default: {'ok': 1, 'critical': 1, 'warning': 1} + locked: + description: ["A boolean indicating whether changes to this monitor should be restricted to the creator or admins."] + required: false + default: False + version_added: 2.2 ''' EXAMPLES = ''' @@ -158,7 +163,8 @@ def main(): escalation_message=dict(required=False, default=None), notify_audit=dict(required=False, default=False, type='bool'), thresholds=dict(required=False, type='dict', default=None), - tags=dict(required=False, type='list', default=None) + tags=dict(required=False, type='list', default=None), + locked=dict(required=False, default=False, type='bool') ) ) @@ -241,6 +247,7 @@ def install_monitor(module): "renotify_interval": module.params['renotify_interval'], "escalation_message": module.params['escalation_message'], "notify_audit": module.boolean(module.params['notify_audit']), + "locked": module.boolean(module.params['locked']), } if module.params['type'] == "service check": From ce6d82e0074198253b26de4255ac23b796da2131 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Thu, 11 Aug 2016 16:04:37 -0700 Subject: [PATCH 1951/2522] Update call to generate-tests. --- test/utils/shippable/integration.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/utils/shippable/integration.sh b/test/utils/shippable/integration.sh index f67fd59db67..ee16e765c15 100755 --- a/test/utils/shippable/integration.sh +++ b/test/utils/shippable/integration.sh @@ -48,4 +48,8 @@ pip list source hacking/env-setup -test/utils/shippable/modules/generate-tests "${this_module_group}" --verbose | /bin/bash -eux +test/utils/shippable/modules/generate-tests "${this_module_group}" --verbose --output /tmp/integration.sh >/dev/null + +if [ -f /tmp/integration.sh ]; then + /bin/bash -eux /tmp/integration.sh +fi From 61d5fe148c5b85c3980c0dff032b3bd7d90ad7fa Mon Sep 17 00:00:00 2001 From: Onni Hakala Date: Fri, 12 Aug 2016 09:01:01 +0300 Subject: [PATCH 1952/2522] Added example to add a port range (#2712) I tried to google for this a bit and then figured out how it actually works. --- system/ufw.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/system/ufw.py b/system/ufw.py index 6b7fb6a7657..c70f51f88b8 100644 --- a/system/ufw.py +++ b/system/ufw.py @@ -153,6 +153,9 @@ # Deny all access to port 53: ufw: rule=deny port=53 +# Allow port range 60000-61000 +ufw: rule=allow port=60000:61000 + # Allow all access to tcp port 80: ufw: rule=allow port=80 proto=tcp From 101f9f5f460f74cecc716598e0afba4c267a2ea0 Mon Sep 17 00:00:00 2001 From: George Christou Date: Mon, 15 Aug 2016 08:02:21 +0100 Subject: [PATCH 1953/2522] Fix bug in counting subnets by Name tag (#1643) Fixes #1551 --- cloud/amazon/ec2_vpc_route_table.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index 7f64a1b3129..416e0b43040 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -187,11 +187,11 @@ def find_subnets(vpc_conn, vpc_id, identified_subnets): filters={'vpc_id': vpc_id, 'tag:Name': subnet_names}) for name in subnet_names: - matching = [s.tags.get('Name') == name for s in subnets_by_name] - if len(matching) == 0: + matching_count = len([1 for s in subnets_by_name if s.tags.get('Name') == name]) + if matching_count == 0: raise AnsibleSubnetSearchException( 'Subnet named "{0}" does not exist'.format(name)) - elif len(matching) > 1: + elif matching_count > 1: raise AnsibleSubnetSearchException( 'Multiple subnets named "{0}"'.format(name)) From a6b34973a8e969356214656223f630e3ed0d608a Mon Sep 17 00:00:00 2001 From: Adrian Likins Date: Mon, 15 Aug 2016 13:32:06 -0400 Subject: [PATCH 1954/2522] Fix error handling error for kube kind error. (#2483) If the provided kubernetes resource file references a 'kind' that is currently unsupported, the error handling is supposed to return with fail_json(msg="message"), but the msg keyword was missing causing the fail_json() method to assert. Fix is to add the msg kwarg. Fixes #2477 From 85aec2e07a3a60761dc8bdebb86fb8a58379d7d4 Mon Sep 17 00:00:00 2001 From: ovcharenko Date: Tue, 16 Aug 2016 16:09:17 +0300 Subject: [PATCH 1955/2522] [FIX] "Invalid interface clause" error in UFW module (#2559) (#2666) Fixes GH-2559 --- system/ufw.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/system/ufw.py b/system/ufw.py index c70f51f88b8..e88c1456975 100644 --- a/system/ufw.py +++ b/system/ufw.py @@ -263,10 +263,11 @@ def execute(cmd): cmd.append([module.boolean(params['route']), 'route']) cmd.append([params['insert'], "insert %s" % params['insert']]) cmd.append([value]) + cmd.append([params['direction'], "%s" % params['direction']]) + cmd.append([params['interface'], "on %s" % params['interface']]) cmd.append([module.boolean(params['log']), 'log']) - for (key, template) in [('direction', "%s" ), ('interface', "on %s" ), - ('from_ip', "from %s" ), ('from_port', "port %s" ), + for (key, template) in [('from_ip', "from %s" ), ('from_port', "port %s" ), ('to_ip', "to %s" ), ('to_port', "port %s" ), ('proto', "proto %s"), ('app', "app '%s'")]: From 06057a9eb7a340dc231d7245e3d3f8109d2066ef Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Tue, 16 Aug 2016 17:11:09 -0700 Subject: [PATCH 1956/2522] Adds bigip_vlan module (#2661) This module can be used to manage VLANs in BIG-IP on various software versions. It is part of a bootstrapping effort underway to provide modules necessary to bootstrap core settings in a BIG-IP. Tests for this module can be found here https://github.com/F5Networks/f5-ansible/blob/master/roles/__bigip_vlan/tasks/main.yaml Platforms this was tested on are - 11.5.4 HF1 - 11.6.0 - 12.0.0 - 12.1.0 HF1 --- network/f5/bigip_vlan.py | 388 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 388 insertions(+) create mode 100644 network/f5/bigip_vlan.py diff --git a/network/f5/bigip_vlan.py b/network/f5/bigip_vlan.py new file mode 100644 index 00000000000..1fe6947eb43 --- /dev/null +++ b/network/f5/bigip_vlan.py @@ -0,0 +1,388 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: bigip_vlan +short_description: Manage VLANs on a BIG-IP system +description: + - Manage VLANs on a BIG-IP system +version_added: "2.2" +options: + description: + description: + - The description to give to the VLAN. + interfaces: + description: + - Specifies a list of tagged or untagged interfaces and trunks that you + want to configure for the VLAN. Use tagged interfaces or trunks when + you want to assign a single interface or trunk to multiple VLANs. + name: + description: + - The VLAN to manage. If the special VLAN C(ALL) is specified with + the C(state) value of C(absent) then all VLANs will be removed. + required: true + state: + description: + - The state of the VLAN on the system. When C(present), guarantees + that the VLAN exists with the provided attributes. When C(absent), + removes the VLAN from the system. + required: false + default: present + choices: + - absent + - present + tag: + description: + - Tag number for the VLAN. The tag number can be any integer between 1 + and 4094. The system automatically assigns a tag number if you do not + specify a value. +notes: + - Requires the f5-sdk Python package on the host. This is as easy as pip + install f5-sdk. + - Requires BIG-IP versions >= 12.0.0 +extends_documentation_fragment: f5 +requirements: + - f5-sdk +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = ''' +- name: Create VLAN + bigip_vlan: + name: "net1" + password: "secret" + server: "lb.mydomain.com" + user: "admin" + validate_certs: "no" + delegate_to: localhost + +- name: Set VLAN tag + bigip_vlan: + name: "net1" + password: "secret" + server: "lb.mydomain.com" + tag: "2345" + user: "admin" + validate_certs: "no" + delegate_to: localhost + +- name: Add VLAN 2345 as tagged to interface 1.1 + bigip_vlan: + interfaces: + - 1.1 + name: "net1" + password: "secret" + server: "lb.mydomain.com" + tag: "2345" + user: "admin" + validate_certs: "no" + delegate_to: localhost +''' + +RETURN = ''' +description: + description: The description set on the VLAN + returned: changed + type: string + sample: foo VLAN +interfaces: + description: Interfaces that the VLAN is assigned to + returned: changed + type: list + sample: ['1.1','1.2'] +name: + description: The name of the VLAN + returned: changed + type: string + sample: net1 +partition: + description: The partition that the VLAN was created on + returned: changed + type: string + sample: Common +tag: + description: The ID of the VLAN + returned: changed + type: int + sample: 2345 +''' + +try: + from f5.bigip import ManagementRoot + from icontrol.session import iControlUnexpectedHTTPError + HAS_F5SDK = True +except ImportError: + HAS_F5SDK = False + + +class BigIpVlan(object): + def __init__(self, *args, **kwargs): + if not HAS_F5SDK: + raise F5ModuleError("The python f5-sdk module is required") + + # The params that change in the module + self.cparams = dict() + + # Stores the params that are sent to the module + self.params = kwargs + self.api = ManagementRoot(kwargs['server'], + kwargs['user'], + kwargs['password'], + port=kwargs['server_port']) + + def present(self): + changed = False + + if self.exists(): + changed = self.update() + else: + changed = self.create() + + return changed + + def absent(self): + changed = False + + if self.exists(): + changed = self.delete() + + return changed + + def read(self): + """Read information and transform it + + The values that are returned by BIG-IP in the f5-sdk can have encoding + attached to them as well as be completely missing in some cases. + + Therefore, this method will transform the data from the BIG-IP into a + format that is more easily consumable by the rest of the class and the + parameters that are supported by the module. + """ + p = dict() + name = self.params['name'] + partition = self.params['partition'] + r = self.api.tm.net.vlans.vlan.load( + name=name, + partition=partition + ) + ifcs = r.interfaces_s.get_collection() + if hasattr(r, 'tag'): + p['tag'] = int(r.tag) + if hasattr(r, 'description'): + p['description'] = str(r.description) + if len(ifcs) is not 0: + p['interfaces'] = list(set([str(x.name) for x in ifcs])) + p['name'] = name + return p + + def create(self): + params = dict() + + check_mode = self.params['check_mode'] + description = self.params['description'] + name = self.params['name'] + interfaces = self.params['interfaces'] + partition = self.params['partition'] + tag = self.params['tag'] + + if tag is not None: + params['tag'] = tag + + if interfaces is not None: + ifcs = self.api.tm.net.interfaces.get_collection() + ifcs = [str(x.name) for x in ifcs] + + if len(ifcs) is 0: + raise F5ModuleError( + 'No interfaces were found' + ) + + pinterfaces = [] + for ifc in interfaces: + ifc = str(ifc) + if ifc in ifcs: + pinterfaces.append(ifc) + if pinterfaces: + params['interfaces'] = pinterfaces + + if description is not None: + params['description'] = self.params['description'] + + params['name'] = name + params['partition'] = partition + + self.cparams = camel_dict_to_snake_dict(params) + if check_mode: + return True + + d = self.api.tm.net.vlans.vlan + d.create(**params) + + if self.exists(): + return True + else: + raise F5ModuleError("Failed to create the VLAN") + + def update(self): + changed = False + params = dict() + current = self.read() + + check_mode = self.params['check_mode'] + description = self.params['description'] + name = self.params['name'] + tag = self.params['tag'] + partition = self.params['partition'] + interfaces = self.params['interfaces'] + + if interfaces is not None: + ifcs = self.api.tm.net.interfaces.get_collection() + ifcs = [str(x.name) for x in ifcs] + + if len(ifcs) is 0: + raise F5ModuleError( + 'No interfaces were found' + ) + + for ifc in interfaces: + ifc = str(ifc) + if ifc in ifcs: + try: + pinterfaces.append(ifc) + except UnboundLocalError: + pinterfaces = [] + pinterfaces.append(ifc) + else: + raise F5ModuleError( + 'The specified interface "%s" was not found' % (ifc) + ) + + if 'interfaces' in current: + if pinterfaces != current['interfaces']: + params['interfaces'] = pinterfaces + else: + params['interfaces'] = pinterfaces + + if description is not None: + if 'description' in current: + if description != current['description']: + params['description'] = description + else: + params['description'] = description + + if tag is not None: + if 'tag' in current: + if tag != current['tag']: + params['tag'] = tag + else: + params['tag'] = tag + + if params: + changed = True + params['name'] = name + params['partition'] = partition + if check_mode: + return changed + self.cparams = camel_dict_to_snake_dict(params) + else: + return changed + + r = self.api.tm.net.vlans.vlan.load( + name=name, + partition=partition + ) + r.update(**params) + r.refresh() + + return True + + def delete(self): + params = dict() + check_mode = self.params['check_mode'] + + params['name'] = self.params['name'] + params['partition'] = self.params['partition'] + + self.cparams = camel_dict_to_snake_dict(params) + if check_mode: + return True + + dc = self.api.tm.net.vlans.vlan.load(**params) + dc.delete() + + if self.exists(): + raise F5ModuleError("Failed to delete the VLAN") + return True + + def exists(self): + name = self.params['name'] + partition = self.params['partition'] + return self.api.tm.net.vlans.vlan.exists( + name=name, + partition=partition + ) + + def flush(self): + result = dict() + state = self.params['state'] + + try: + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + except iControlUnexpectedHTTPError as e: + raise F5ModuleError(str(e)) + + result.update(**self.cparams) + result.update(dict(changed=changed)) + return result + + +def main(): + argument_spec = f5_argument_spec() + + meta_args = dict( + description=dict(required=False, default=None), + interfaces=dict(required=False, default=None, type='list'), + name=dict(required=True), + tag=dict(required=False, default=None, type='int') + ) + argument_spec.update(meta_args) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True + ) + + try: + obj = BigIpVlan(check_mode=module.check_mode, **module.params) + result = obj.flush() + + module.exit_json(**result) + except F5ModuleError as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import camel_dict_to_snake_dict +from ansible.module_utils.f5 import * + +if __name__ == '__main__': + main() From b6a2e8ad6760cd83ec51edc1ebae616f78c5e5db Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Tue, 16 Aug 2016 17:13:16 -0700 Subject: [PATCH 1957/2522] Introduces the bigip_sys_db module (#998) This module can be used to directly manipulate the system database variables in a BIG-IP. It supports both the iControl SOAP and iControl REST APIs, but default to the REST API. With this module, you can perform operations similar to those available in tmsh to set system variables such as turning off the default setup screen. This module is most useful in the initial provisioning of a BIG-IP --- network/f5/bigip_sys_db.py | 221 +++++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 network/f5/bigip_sys_db.py diff --git a/network/f5/bigip_sys_db.py b/network/f5/bigip_sys_db.py new file mode 100644 index 00000000000..54f5dd74fc9 --- /dev/null +++ b/network/f5/bigip_sys_db.py @@ -0,0 +1,221 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: bigip_sys_db +short_description: Manage BIG-IP system database variables +description: + - Manage BIG-IP system database variables +version_added: "2.2" +options: + key: + description: + - The database variable to manipulate. + required: true + state: + description: + - The state of the variable on the system. When C(present), guarantees + that an existing variable is set to C(value). When C(reset) sets the + variable back to the default value. At least one of value and state + C(reset) are required. + required: false + default: present + choices: + - present + - reset + value: + description: + - The value to set the key to. At least one of value and state C(reset) + are required. + required: false +notes: + - Requires the f5-sdk Python package on the host. This is as easy as pip + install f5-sdk. + - Requires BIG-IP version 12.0.0 or greater +extends_documentation_fragment: f5 +requirements: + - f5-sdk +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = ''' +- name: Set the boot.quiet DB variable on the BIG-IP + bigip_sys_db: + user: "admin" + password: "secret" + server: "lb.mydomain.com" + key: "boot.quiet" + value: "disable" + delegate_to: localhost + +- name: Disable the initial setup screen + bigip_sys_db: + user: "admin" + password: "secret" + server: "lb.mydomain.com" + key: "setup.run" + value: "false" + delegate_to: localhost + +- name: Reset the initial setup screen + bigip_sys_db: + user: "admin" + password: "secret" + server: "lb.mydomain.com" + key: "setup.run" + state: "reset" + delegate_to: localhost +''' + +RETURN = ''' +name: + description: The key in the system database that was specified + returned: changed and success + type: string + sample: "setup.run" +default_value: + description: The default value of the key + returned: changed and success + type: string + sample: "true" +value: + description: The value that you set the key to + returned: changed and success + type: string + sample: "false" +''' + +try: + from f5.bigip import ManagementRoot + HAS_F5SDK = True +except ImportError: + HAS_F5SDK = False + + +class BigIpSysDb(object): + def __init__(self, *args, **kwargs): + if not HAS_F5SDK: + raise F5ModuleError("The python f5-sdk module is required") + + self.params = kwargs + self.api = ManagementRoot(kwargs['server'], + kwargs['user'], + kwargs['password'], + port=kwargs['server_port']) + + def flush(self): + result = dict() + state = self.params['state'] + value = self.params['value'] + + if not state == 'reset' and not value: + raise F5ModuleError( + "When setting a key, a value must be supplied" + ) + + current = self.read() + + if self.params['check_mode']: + if value == current: + changed = False + else: + changed = True + else: + if state == "present": + changed = self.present() + elif state == "reset": + changed = self.reset() + current = self.read() + result.update( + name=current.name, + default_value=current.defaultValue, + value=current.value + ) + + result.update(dict(changed=changed)) + return result + + def read(self): + dbs = self.api.tm.sys.dbs.db.load( + name=self.params['key'] + ) + return dbs + + def present(self): + current = self.read() + + if current.value == self.params['value']: + return False + + current.update(value=self.params['value']) + current.refresh() + + if current.value != self.params['value']: + raise F5ModuleError( + "Failed to set the DB variable" + ) + return True + + def reset(self): + current = self.read() + + default = current.defaultValue + if current.value == default: + return False + + current.update(value=default) + current.refresh() + + if current.value != current.defaultValue: + raise F5ModuleError( + "Failed to reset the DB variable" + ) + + return True + + +def main(): + argument_spec = f5_argument_spec() + + meta_args = dict( + key=dict(required=True), + state=dict(default='present', choices=['present', 'reset']), + value=dict(required=False, default=None) + ) + argument_spec.update(meta_args) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True + ) + + try: + obj = BigIpSysDb(check_mode=module.check_mode, **module.params) + result = obj.flush() + + module.exit_json(**result) + except F5ModuleError as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.basic import * +from ansible.module_utils.f5 import * + +if __name__ == '__main__': + main() From f47e02d7e9ce7e2547a983b6aa51197c33de6a52 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Tue, 16 Aug 2016 17:14:05 -0700 Subject: [PATCH 1958/2522] Introduces the bigip_gtm_datacenter module (#1000) This module can be used to manipulate data centers in a BIG-IP. It supports both the iControl SOAP and iControl REST APIs, but default to the REST API. With this module, you can perform operations similar to those available in tmsh to create data centers and set the contact, location, and description of those data centers. This module is most useful in the initial provisioning of a BIG-IP --- network/f5/bigip_gtm_datacenter.py | 366 +++++++++++++++++++++++++++++ 1 file changed, 366 insertions(+) create mode 100644 network/f5/bigip_gtm_datacenter.py diff --git a/network/f5/bigip_gtm_datacenter.py b/network/f5/bigip_gtm_datacenter.py new file mode 100644 index 00000000000..90882b6f644 --- /dev/null +++ b/network/f5/bigip_gtm_datacenter.py @@ -0,0 +1,366 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: bigip_gtm_datacenter +short_description: Manage Datacenter configuration in BIG-IP +description: + - Manage BIG-IP data center configuration. A data center defines the location + where the physical network components reside, such as the server and link + objects that share the same subnet on the network. This module is able to + manipulate the data center definitions in a BIG-IP +version_added: "2.2" +options: + contact: + description: + - The name of the contact for the data center. + description: + description: + - The description of the data center. + enabled: + description: + - Whether the data center should be enabled. At least one of C(state) and + C(enabled) are required. + choices: + - yes + - no + location: + description: + - The location of the data center. + name: + description: + - The name of the data center. + required: true + state: + description: + - The state of the datacenter on the BIG-IP. When C(present), guarantees + that the data center exists. When C(absent) removes the data center + from the BIG-IP. C(enabled) will enable the data center and C(disabled) + will ensure the data center is disabled. At least one of state and + enabled are required. + choices: + - present + - absent +notes: + - Requires the f5-sdk Python package on the host. This is as easy as + pip install f5-sdk. +extends_documentation_fragment: f5 +requirements: + - f5-sdk +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = ''' +- name: Create data center "New York" + bigip_gtm_datacenter: + server: "big-ip" + name: "New York" + location: "222 West 23rd" + delegate_to: localhost +''' + +RETURN = ''' +contact: + description: The contact that was set on the datacenter + returned: changed + type: string + sample: "admin@root.local" +description: + description: The description that was set for the datacenter + returned: changed + type: string + sample: "Datacenter in NYC" +enabled: + description: Whether the datacenter is enabled or not + returned: changed + type: bool + sample: true +location: + description: The location that is set for the datacenter + returned: changed + type: string + sample: "222 West 23rd" +name: + description: Name of the datacenter being manipulated + returned: changed + type: string + sample: "foo" +''' + +try: + from f5.bigip import ManagementRoot + from icontrol.session import iControlUnexpectedHTTPError + HAS_F5SDK = True +except ImportError: + HAS_F5SDK = False + + +class BigIpGtmDatacenter(object): + def __init__(self, *args, **kwargs): + if not HAS_F5SDK: + raise F5ModuleError("The python f5-sdk module is required") + + # The params that change in the module + self.cparams = dict() + + # Stores the params that are sent to the module + self.params = kwargs + self.api = ManagementRoot(kwargs['server'], + kwargs['user'], + kwargs['password'], + port=kwargs['server_port']) + + def create(self): + params = dict() + + check_mode = self.params['check_mode'] + contact = self.params['contact'] + description = self.params['description'] + location = self.params['location'] + name = self.params['name'] + partition = self.params['partition'] + enabled = self.params['enabled'] + + # Specifically check for None because a person could supply empty + # values which would technically still be valid + if contact is not None: + params['contact'] = contact + + if description is not None: + params['description'] = description + + if location is not None: + params['location'] = location + + if enabled is not None: + params['enabled'] = True + else: + params['disabled'] = False + + params['name'] = name + params['partition'] = partition + + self.cparams = camel_dict_to_snake_dict(params) + if check_mode: + return True + + d = self.api.tm.gtm.datacenters.datacenter + d.create(**params) + + if not self.exists(): + raise F5ModuleError("Failed to create the datacenter") + return True + + def read(self): + """Read information and transform it + + The values that are returned by BIG-IP in the f5-sdk can have encoding + attached to them as well as be completely missing in some cases. + + Therefore, this method will transform the data from the BIG-IP into a + format that is more easily consumable by the rest of the class and the + parameters that are supported by the module. + """ + p = dict() + name = self.params['name'] + partition = self.params['partition'] + r = self.api.tm.gtm.datacenters.datacenter.load( + name=name, + partition=partition + ) + + if hasattr(r, 'servers'): + # Deliberately using sets to supress duplicates + p['servers'] = set([str(x) for x in r.servers]) + if hasattr(r, 'contact'): + p['contact'] = str(r.contact) + if hasattr(r, 'location'): + p['location'] = str(r.location) + if hasattr(r, 'description'): + p['description'] = str(r.description) + if r.enabled: + p['enabled'] = True + else: + p['enabled'] = False + p['name'] = name + return p + + def update(self): + changed = False + params = dict() + current = self.read() + + check_mode = self.params['check_mode'] + contact = self.params['contact'] + description = self.params['description'] + location = self.params['location'] + name = self.params['name'] + partition = self.params['partition'] + enabled = self.params['enabled'] + + if contact is not None: + if 'contact' in current: + if contact != current['contact']: + params['contact'] = contact + else: + params['contact'] = contact + + if description is not None: + if 'description' in current: + if description != current['description']: + params['description'] = description + else: + params['description'] = description + + if location is not None: + if 'location' in current: + if location != current['location']: + params['location'] = location + else: + params['location'] = location + + if enabled is not None: + if current['enabled'] != enabled: + if enabled is True: + params['enabled'] = True + params['disabled'] = False + else: + params['disabled'] = True + params['enabled'] = False + + if params: + changed = True + if check_mode: + return changed + self.cparams = camel_dict_to_snake_dict(params) + else: + return changed + + r = self.api.tm.gtm.datacenters.datacenter.load( + name=name, + partition=partition + ) + r.update(**params) + r.refresh() + + return True + + def delete(self): + params = dict() + check_mode = self.params['check_mode'] + + params['name'] = self.params['name'] + params['partition'] = self.params['partition'] + + self.cparams = camel_dict_to_snake_dict(params) + if check_mode: + return True + + dc = self.api.tm.gtm.datacenters.datacenter.load(**params) + dc.delete() + + if self.exists(): + raise F5ModuleError("Failed to delete the datacenter") + return True + + def present(self): + changed = False + + if self.exists(): + changed = self.update() + else: + changed = self.create() + + return changed + + def absent(self): + changed = False + + if self.exists(): + changed = self.delete() + + return changed + + def exists(self): + name = self.params['name'] + partition = self.params['partition'] + + return self.api.tm.gtm.datacenters.datacenter.exists( + name=name, + partition=partition + ) + + def flush(self): + result = dict() + state = self.params['state'] + enabled = self.params['enabled'] + + if state is None and enabled is None: + module.fail_json(msg="Neither 'state' nor 'enabled' set") + + try: + if state == "present": + changed = self.present() + + # Ensure that this field is not returned to the user since it + # is not a valid parameter to the module. + if 'disabled' in self.cparams: + del self.cparams['disabled'] + elif state == "absent": + changed = self.absent() + except iControlUnexpectedHTTPError as e: + raise F5ModuleError(str(e)) + + result.update(**self.cparams) + result.update(dict(changed=changed)) + return result + + +def main(): + argument_spec = f5_argument_spec() + + meta_args = dict( + contact=dict(required=False, default=None), + description=dict(required=False, default=None), + enabled=dict(required=False, type='bool', default=None, choices=BOOLEANS), + location=dict(required=False, default=None), + name=dict(required=True) + ) + argument_spec.update(meta_args) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True + ) + + try: + obj = BigIpGtmDatacenter(check_mode=module.check_mode, **module.params) + result = obj.flush() + + module.exit_json(**result) + except F5ModuleError as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import camel_dict_to_snake_dict +from ansible.module_utils.f5 import * + +if __name__ == '__main__': + main() From 5fbb0de36fa14a464b7eb5fd9fcd547f96b746e8 Mon Sep 17 00:00:00 2001 From: Shinichi TAMURA Date: Wed, 17 Aug 2016 23:32:49 +0900 Subject: [PATCH 1959/2522] Allow value to be bool where 'yes'/'no' are in choices (#2593) * Changed type of 'details' argument to bool on ecs_service_facts module. * Changed type of 'autostart' argument to bool on virt_* modules. * Changed types of 'autoconnect' and 'stp' argument to bool on nmcli module. ('create_connection_bridge(self)' and 'modify_connection_bridge(self)' are not implemented yet?) * Added conversion of 'value' argument when 'vtype' is boolean on debconf module. --- cloud/amazon/ecs_service_facts.py | 8 +++----- cloud/misc/virt_net.py | 8 ++++---- cloud/misc/virt_pool.py | 8 ++++---- network/nmcli.py | 22 ++++++++++++++-------- system/debconf.py | 5 +++++ 5 files changed, 30 insertions(+), 21 deletions(-) diff --git a/cloud/amazon/ecs_service_facts.py b/cloud/amazon/ecs_service_facts.py index e6629ab08f0..f363c56a872 100644 --- a/cloud/amazon/ecs_service_facts.py +++ b/cloud/amazon/ecs_service_facts.py @@ -55,7 +55,7 @@ - ecs_service_facts: cluster: test-cluster service: console-test-service - details: "true" + details: true # Basic listing example - ecs_service_facts: @@ -201,7 +201,7 @@ def main(): argument_spec = ec2_argument_spec() argument_spec.update(dict( - details=dict(required=False, choices=['true', 'false'] ), + details=dict(required=False, type='bool', default=False ), cluster=dict(required=False, type='str' ), service=dict(required=False, type='str' ) )) @@ -214,9 +214,7 @@ def main(): if not HAS_BOTO3: module.fail_json(msg='boto3 is required.') - show_details = False - if 'details' in module.params and module.params['details'] == 'true': - show_details = True + show_details = module.params.get('details', False) task_mgr = EcsServiceManager(module) if show_details: diff --git a/cloud/misc/virt_net.py b/cloud/misc/virt_net.py index 055a7e6b3b0..e2dd88f4d4a 100755 --- a/cloud/misc/virt_net.py +++ b/cloud/misc/virt_net.py @@ -534,16 +534,16 @@ def core(module): else: module.fail_json(msg="Command %s not recognized" % basecmd) - if autostart: + if autostart is not None: if not name: module.fail_json(msg = "state change requires a specified name") res['changed'] = False - if autostart == 'yes': + if autostart: if not v.get_autostart(name): res['changed'] = True res['msg'] = v.set_autostart(name, True) - elif autostart == 'no': + else: if v.get_autostart(name): res['changed'] = True res['msg'] = v.set_autostart(name, False) @@ -562,7 +562,7 @@ def main(): command = dict(choices=ALL_COMMANDS), uri = dict(default='qemu:///system'), xml = dict(), - autostart = dict(choices=['yes', 'no']) + autostart = dict(type='bool') ), supports_check_mode = True ) diff --git a/cloud/misc/virt_pool.py b/cloud/misc/virt_pool.py index 1089269fc84..b104ef548dc 100755 --- a/cloud/misc/virt_pool.py +++ b/cloud/misc/virt_pool.py @@ -629,16 +629,16 @@ def core(module): else: module.fail_json(msg="Command %s not recognized" % basecmd) - if autostart: + if autostart is not None: if not name: module.fail_json(msg = "state change requires a specified name") res['changed'] = False - if autostart == 'yes': + if autostart: if not v.get_autostart(name): res['changed'] = True res['msg'] = v.set_autostart(name, True) - elif autostart == 'no': + else: if v.get_autostart(name): res['changed'] = True res['msg'] = v.set_autostart(name, False) @@ -657,7 +657,7 @@ def main(): command = dict(choices=ALL_COMMANDS), uri = dict(default='qemu:///system'), xml = dict(), - autostart = dict(choices=['yes', 'no']), + autostart = dict(type='bool'), mode = dict(choices=ALL_MODES), ), supports_check_mode = True diff --git a/network/nmcli.py b/network/nmcli.py index 2c553425e94..a7746051099 100644 --- a/network/nmcli.py +++ b/network/nmcli.py @@ -514,6 +514,12 @@ def connection_to_string(self, config): return setting_list # print "" + def bool_to_string(self, boolean): + if boolean: + return "yes" + else: + return "no" + def list_connection_info(self): # Ask the settings service for the list of connections it provides bus=dbus.SystemBus() @@ -602,7 +608,7 @@ def create_connection_team(self): cmd.append(self.gw6) if self.autoconnect is not None: cmd.append('autoconnect') - cmd.append(self.autoconnect) + cmd.append(self.bool_to_string(self.autoconnect)) return cmd def modify_connection_team(self): @@ -631,7 +637,7 @@ def modify_connection_team(self): cmd.append(self.dns6) if self.autoconnect is not None: cmd.append('autoconnect') - cmd.append(self.autoconnect) + cmd.append(self.bool_to_string(self.autoconnect)) # Can't use MTU with team return cmd @@ -704,7 +710,7 @@ def create_connection_bond(self): cmd.append(self.gw6) if self.autoconnect is not None: cmd.append('autoconnect') - cmd.append(self.autoconnect) + cmd.append(self.bool_to_string(self.autoconnect)) if self.mode is not None: cmd.append('mode') cmd.append(self.mode) @@ -751,7 +757,7 @@ def modify_connection_bond(self): cmd.append(self.dns6) if self.autoconnect is not None: cmd.append('autoconnect') - cmd.append(self.autoconnect) + cmd.append(self.bool_to_string(self.autoconnect)) return cmd def create_connection_bond_slave(self): @@ -820,7 +826,7 @@ def create_connection_ethernet(self): cmd.append(self.gw6) if self.autoconnect is not None: cmd.append('autoconnect') - cmd.append(self.autoconnect) + cmd.append(self.bool_to_string(self.autoconnect)) return cmd def modify_connection_ethernet(self): @@ -855,7 +861,7 @@ def modify_connection_ethernet(self): cmd.append(self.mtu) if self.autoconnect is not None: cmd.append('autoconnect') - cmd.append(self.autoconnect) + cmd.append(self.bool_to_string(self.autoconnect)) return cmd def create_connection_bridge(self): @@ -964,7 +970,7 @@ def main(): # Parsing argument file module=AnsibleModule( argument_spec=dict( - autoconnect=dict(required=False, default=None, choices=['yes', 'no'], type='str'), + autoconnect=dict(required=False, default=None, type='bool'), state=dict(required=True, choices=['present', 'absent'], type='str'), conn_name=dict(required=True, type='str'), master=dict(required=False, default=None, type='str'), @@ -987,7 +993,7 @@ def main(): mtu=dict(required=False, default=None, type='str'), mac=dict(required=False, default=None, type='str'), # bridge specific vars - stp=dict(required=False, default='yes', choices=['yes', 'no'], type='str'), + stp=dict(required=False, default=True, type='bool'), priority=dict(required=False, default="128", type='str'), slavepriority=dict(required=False, default="32", type='str'), forwarddelay=dict(required=False, default="15", type='str'), diff --git a/system/debconf.py b/system/debconf.py index 22f4cb3fc3d..74818e908f4 100644 --- a/system/debconf.py +++ b/system/debconf.py @@ -108,6 +108,11 @@ def set_selection(module, pkg, question, vtype, value, unseen): if unseen: cmd.append('-u') + if vtype == 'boolean': + if value == 'True': + value = 'true' + elif value == 'False': + value = 'false' data = ' '.join([pkg, question, vtype, value]) return module.run_command(cmd, data=data) From 0d8a7b875e7e59ef197297b7b6d79f83de4f7c29 Mon Sep 17 00:00:00 2001 From: nitzmahone Date: Wed, 17 Aug 2016 15:47:44 -0700 Subject: [PATCH 1960/2522] isolate chocolatey bootstrapper execution fixes #2742 added output capture/return, exit code check to bootstrapper exec --- windows/win_chocolatey.ps1 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/windows/win_chocolatey.ps1 b/windows/win_chocolatey.ps1 index e725519b991..bf5418abbe7 100644 --- a/windows/win_chocolatey.ps1 +++ b/windows/win_chocolatey.ps1 @@ -59,7 +59,12 @@ Function Chocolatey-Install-Upgrade if ($ChocoAlreadyInstalled -eq $null) { #We need to install chocolatey - iex ((new-object net.webclient).DownloadString("https://chocolatey.org/install.ps1")) + $install_output = (new-object net.webclient).DownloadString("https://chocolatey.org/install.ps1") | powershell - + if ($LASTEXITCODE -ne 0) + { + Set-Attr $result "choco_bootstrap_output" $install_output + Fail-Json $result "Chocolatey bootstrap installation failed." + } $result.changed = $true $script:executable = "C:\ProgramData\chocolatey\bin\choco.exe" } From 7ba5f340c08f43938e2b4b1acd7465c24197ce44 Mon Sep 17 00:00:00 2001 From: nitzmahone Date: Wed, 17 Aug 2016 16:38:36 -0700 Subject: [PATCH 1961/2522] make the win_chocolatey force arg actually work fixes #1561 --- windows/win_chocolatey.ps1 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/windows/win_chocolatey.ps1 b/windows/win_chocolatey.ps1 index bf5418abbe7..546fc7621a4 100644 --- a/windows/win_chocolatey.ps1 +++ b/windows/win_chocolatey.ps1 @@ -214,9 +214,14 @@ Function Choco-Install Choco-Upgrade -package $package -version $version -source $source -force $force ` -installargs $installargs -packageparams $packageparams ` -ignoredependencies $ignoredependencies + + return } - return + if (-not $force) + { + return + } } $cmd = "$executable install -dv -y $package" From a0969e4bff441192f6146b473bc442bdf4d350f6 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 18 Aug 2016 09:47:38 -0400 Subject: [PATCH 1962/2522] fixed incorrect import deps (#2747) --- cloud/lxd/__init__.py | 133 ------------------------------------- cloud/lxd/lxd_container.py | 2 +- cloud/lxd/lxd_profile.py | 2 +- 3 files changed, 2 insertions(+), 135 deletions(-) diff --git a/cloud/lxd/__init__.py b/cloud/lxd/__init__.py index 46b2fda6ed9..e69de29bb2d 100644 --- a/cloud/lxd/__init__.py +++ b/cloud/lxd/__init__.py @@ -1,133 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2016, Hiroaki Nakamura -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -try: - import json -except ImportError: - import simplejson as json - -# httplib/http.client connection using unix domain socket -import socket -import ssl -try: - from httplib import HTTPConnection, HTTPSConnection -except ImportError: - # Python 3 - from http.client import HTTPConnection, HTTPSConnection - -class UnixHTTPConnection(HTTPConnection): - def __init__(self, path): - HTTPConnection.__init__(self, 'localhost') - self.path = path - - def connect(self): - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.connect(self.path) - self.sock = sock - -from ansible.module_utils.urls import generic_urlparse -try: - from urlparse import urlparse -except ImportError: - # Python 3 - from url.parse import urlparse - -class LXDClientException(Exception): - def __init__(self, msg, **kwargs): - self.msg = msg - self.kwargs = kwargs - -class LXDClient(object): - def __init__(self, url, key_file=None, cert_file=None, debug=False): - """LXD Client. - - :param url: The URL of the LXD server. (e.g. unix:/var/lib/lxd/unix.socket or https://127.0.0.1) - :type url: ``str`` - :param key_file: The path of the client certificate key file. - :type key_file: ``str`` - :param cert_file: The path of the client certificate file. - :type cert_file: ``str`` - :param debug: The debug flag. The request and response are stored in logs when debug is true. - :type debug: ``bool`` - """ - self.url = url - self.debug = debug - self.logs = [] - if url.startswith('https:'): - self.cert_file = cert_file - self.key_file = key_file - parts = generic_urlparse(urlparse(self.url)) - ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) - ctx.load_cert_chain(cert_file, keyfile=key_file) - self.connection = HTTPSConnection(parts.get('netloc'), context=ctx) - elif url.startswith('unix:'): - unix_socket_path = url[len('unix:'):] - self.connection = UnixHTTPConnection(unix_socket_path) - else: - raise LXDClientException('URL scheme must be unix: or https:') - - def do(self, method, url, body_json=None, ok_error_codes=None, timeout=None): - resp_json = self._send_request(method, url, body_json=body_json, ok_error_codes=ok_error_codes, timeout=timeout) - if resp_json['type'] == 'async': - url = '{0}/wait'.format(resp_json['operation']) - resp_json = self._send_request('GET', url) - if resp_json['metadata']['status'] != 'Success': - self._raise_err_from_json(resp_json) - return resp_json - - def authenticate(self, trust_password): - body_json = {'type': 'client', 'password': trust_password} - return self._send_request('POST', '/1.0/certificates', body_json=body_json) - - def _send_request(self, method, url, body_json=None, ok_error_codes=None, timeout=None): - try: - body = json.dumps(body_json) - self.connection.request(method, url, body=body) - resp = self.connection.getresponse() - resp_json = json.loads(resp.read()) - self.logs.append({ - 'type': 'sent request', - 'request': {'method': method, 'url': url, 'json': body_json, 'timeout': timeout}, - 'response': {'json': resp_json} - }) - resp_type = resp_json.get('type', None) - if resp_type == 'error': - if ok_error_codes is not None and resp_json['error_code'] in ok_error_codes: - return resp_json - self._raise_err_from_json(resp_json) - return resp_json - except socket.error as e: - raise LXDClientException('cannot connect to the LXD server', err=e) - - def _raise_err_from_json(self, resp_json): - err_params = {} - if self.debug: - err_params['logs'] = self.logs - raise LXDClientException(self._get_err_from_resp_json(resp_json), **err_params) - - @staticmethod - def _get_err_from_resp_json(resp_json): - err = None - metadata = resp_json.get('metadata', None) - if metadata is not None: - err = metadata.get('err', None) - if err is None: - err = resp_json.get('error', None) - return err diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 17a8a0fa233..692987ec7e4 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -246,7 +246,7 @@ ''' import os -from ansible.modules.extras.cloud.lxd import LXDClient, LXDClientException +from ansible.module_utils.lxd import LXDClient, LXDClientException # LXD_ANSIBLE_STATES is a map of states that contain values of methods used # when a particular state is evoked. diff --git a/cloud/lxd/lxd_profile.py b/cloud/lxd/lxd_profile.py index 2584908623c..c5fb825e75a 100644 --- a/cloud/lxd/lxd_profile.py +++ b/cloud/lxd/lxd_profile.py @@ -172,7 +172,7 @@ ''' import os -from ansible.modules.extras.cloud.lxd import LXDClient, LXDClientException +from ansible.module_utils.lxd import LXDClient, LXDClientException # PROFILE_STATES is a list for states supported PROFILES_STATES = [ From 1aeb9f8a8c6d54663bbad6db385f568c04182ec6 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 18 Aug 2016 09:49:56 -0400 Subject: [PATCH 1963/2522] updated version_added to be string --- cloud/lxd/lxd_container.py | 2 +- cloud/lxd/lxd_profile.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 692987ec7e4..c28a6234e22 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -23,7 +23,7 @@ --- module: lxd_container short_description: Manage LXD Containers -version_added: 2.2.0 +version_added: "2.2" description: - Management of LXD containers author: "Hiroaki Nakamura (@hnakamur)" diff --git a/cloud/lxd/lxd_profile.py b/cloud/lxd/lxd_profile.py index c5fb825e75a..272a88b1748 100644 --- a/cloud/lxd/lxd_profile.py +++ b/cloud/lxd/lxd_profile.py @@ -23,7 +23,7 @@ --- module: lxd_profile short_description: Manage LXD profiles -version_added: 2.2.0 +version_added: "2.2" description: - Management of LXD profiles author: "Hiroaki Nakamura (@hnakamur)" From aacbb97aa6fd90240b4f85508e401987ad676af2 Mon Sep 17 00:00:00 2001 From: Saravanan K R Date: Thu, 18 Aug 2016 20:35:48 +0530 Subject: [PATCH 1964/2522] Added modules to manage Atomic Host Platform (host and image) (#1902) * Added modules to manage Atomic Host Platform (host and image) * Fixed review comments * Fixed requirements and locale setting --- .travis.yml | 2 +- system/atomic_host.py | 105 +++++++++++++++++++++++++++++++ system/atomic_image.py | 137 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 system/atomic_host.py create mode 100644 system/atomic_image.py diff --git a/.travis.yml b/.travis.yml index 7f1ae7126cb..82142db2d45 100644 --- a/.travis.yml +++ b/.travis.yml @@ -117,7 +117,7 @@ install: - pip install git+https://github.com/ansible/ansible.git@devel#egg=ansible - pip install git+https://github.com/sivel/ansible-testing.git#egg=ansible_testing script: - - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py|database/influxdb/influxdb.*\.py|database/mssql/mssql_db\.py|/letsencrypt\.py|network/f5/bigip.*\.py|bmc/ipmi/.*\.py' . + - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py|database/influxdb/influxdb.*\.py|database/mssql/mssql_db\.py|/letsencrypt\.py|network/f5/bigip.*\.py|bmc/ipmi/.*\.py|system/atomic_.*\.py' . - python2.6 -m compileall -fq . - python2.7 -m compileall -fq . - python3.4 -m compileall -fq . -x $(echo "$PY3_EXCLUDE_LIST"| tr ' ' '|') diff --git a/system/atomic_host.py b/system/atomic_host.py new file mode 100644 index 00000000000..dc098e6721b --- /dev/null +++ b/system/atomic_host.py @@ -0,0 +1,105 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public licenses +# along with Ansible. If not, see . + +DOCUMENTATION=''' +--- +module: atomic_host +short_description: Manage the atomic host platform +description: + - Manage the atomic host platform + - Rebooting of Atomic host platform should be done outside this module +version_added: "2.2" +author: "Saravanan KR @krsacme" +notes: + - Host should be an atomic platform (verified by existence of '/run/ostree-booted' file) +requirements: + - atomic + - "python >= 2.6" +options: + revision: + description: + - The version number of the atomic host to be deployed. Providing ```latest``` will upgrade to the latest available version. + required: false + default: latest + aliases: ["version"] +''' + +EXAMPLES = ''' + +# Upgrade the atomic host platform to the latest version (atomic host upgrade) +- atomic_host: revision=latest + +# Deploy a specific revision as the atomic host (atomic host deploy 23.130) +- atomic_host: revision=23.130 + +''' + +RETURN = ''' +msg: + description: The command standard output + returned: always + type: string + sample: 'Already on latest' +''' + +def core(module): + revision = module.params['revision'] + args = [] + + module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C') + + if revision == 'latest': + args = ['atomic', 'host', 'upgrade'] + else: + args = ['atomic', 'host', 'deploy', revision] + + out = {} + err = {} + rc = 0 + + rc, out, err = module.run_command(args, check_rc=False) + + if rc == 77 and revision == 'latest': + module.exit_json(msg="Already on latest", changed=False) + elif rc != 0: + module.fail_json(rc=rc, msg=err) + else: + module.exit_json(msg=out, changed=True) + + +def main(): + module = AnsibleModule( + argument_spec = dict( + revision = dict(default='latest', required=False, aliases=["version"]), + ), + ) + + # Verify that the platform is atomic host + if not os.path.exists("/run/ostree-booted"): + module.fail_json(msg="Module atomic_host is applicable for Atomic Host Platforms only") + + try: + core(module) + except Exception as e: + module.fail_json(msg=str(e)) + + +# import module snippets +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() diff --git a/system/atomic_image.py b/system/atomic_image.py new file mode 100644 index 00000000000..cebd97a7d48 --- /dev/null +++ b/system/atomic_image.py @@ -0,0 +1,137 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION=''' +--- +module: atomic_image +short_description: Manage the container images on the atomic host platform +description: + - Manage the container images on the atomic host platform + - Allows to execute the commands on the container images +version_added: "2.2" +author: "Saravanan KR @krsacme" +notes: + - Host should be support ```atomic``` command +requirements: + - atomic + - "python >= 2.6" +options: + name: + description: + - Name of the container image + required: True + default: null + state: + description: + - The state of the container image. + - The state ```latest``` will ensure container image is upgraded to the latest version and forcefully restart container, if running. + required: False + choices: ["present", "absent", "latest"] + default: latest + started: + description: + - Start or Stop the continer + required: False + choices: ["yes", "no"] + default: yes +''' + +EXAMPLES = ''' + +# Execute the run command on rsyslog container image (atomic run rhel7/rsyslog) +- atomic_image: name=rhel7/rsyslog state=latest + +''' + +RETURN = ''' +msg: + description: The command standard output + returned: always + type: string + sample: [u'Using default tag: latest ...'] +''' + +def do_upgrade(module, image): + args = ['atomic', 'update', '--force', image] + rc, out, err = module.run_command(args, check_rc=False) + if rc != 0: # something went wrong emit the msg + module.fail_json(rc=rc, msg=err) + elif 'Image is up to date' in out: + return False + + return True + + +def core(module): + image = module.params['name'] + state = module.params['state'] + started = module.params['started'] + is_upgraded = False + + module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C') + + if state == 'present' or state == 'latest': + if state == 'latest': + is_upgraded = do_upgrade(module, image) + + if started: + args = ['atomic', 'run', image] + else: + args = ['atomic', 'install', image] + elif state == 'absent': + args = ['atomic', 'uninstall', image] + + out = {} + err = {} + rc = 0 + rc, out, err = module.run_command(args, check_rc=False) + + if rc < 0: + module.fail_json(rc=rc, msg=err) + elif rc == 1 and 'already present' in err: + module.exit_json(restult=err, changed=is_upgraded) + elif started and 'Container is running' in out: + module.exit_json(result=out, changed=is_upgraded) + else: + module.exit_json(msg=out, changed=True) + + +def main(): + module = AnsibleModule( + argument_spec = dict( + name = dict(default=None, required=True), + state = dict(default='latest', choices=['present', 'absent', 'latest']), + started = dict(default='yes', type='bool'), + ), + ) + + # Verify that the platform supports atomic command + rc, out, err = module.run_command('atomic -v', check_rc=False) + if rc != 0: + module.fail_json(msg="Error in running atomic command", err=err) + + try: + core(module) + except Exception as e: + module.fail_json(msg=str(e)) + + +# import module snippets +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From f4d5ee6ba97e03748c93cc6bfb07f18dd4383c63 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Wed, 17 Aug 2016 09:34:43 -0700 Subject: [PATCH 1965/2522] Adds the bigip_selfip module Another bootstrapping module, this module allows for one to manage self IP addresses on a BIG-IP. Tests for this module can be found here https://github.com/F5Networks/f5-ansible/blob/master/roles/__bigip_selfip/tasks/main.yaml Platforms this was tested on are 11.5.4 HF1 11.6.0 12.0.0 12.1.0 HF1 --- network/f5/bigip_selfip.py | 449 +++++++++++++++++++++++++++++++++++++ 1 file changed, 449 insertions(+) create mode 100644 network/f5/bigip_selfip.py diff --git a/network/f5/bigip_selfip.py b/network/f5/bigip_selfip.py new file mode 100644 index 00000000000..452e44bc680 --- /dev/null +++ b/network/f5/bigip_selfip.py @@ -0,0 +1,449 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: bigip_selfip +short_description: Manage Self-IPs on a BIG-IP system +description: + - Manage Self-IPs on a BIG-IP system +version_added: "2.2" +options: + address: + description: + - The IP addresses for the new self IP. This value is ignored upon update + as addresses themselves cannot be changed after they are created. + name: + description: + - The self IP to create. + required: true + default: Value of C(address) + netmask: + description: + - The netmasks for the self IP. + required: true + state: + description: + - The state of the variable on the system. When C(present), guarantees + that the Self-IP exists with the provided attributes. When C(absent), + removes the Self-IP from the system. + required: false + default: present + choices: + - absent + - present + traffic_group: + description: + - The traffic group for the self IP addresses in an active-active, + redundant load balancer configuration. + required: false + vlan: + description: + - The VLAN that the new self IPs will be on. + required: true +notes: + - Requires the f5-sdk Python package on the host. This is as easy as pip + install f5-sdk. + - Requires the netaddr Python package on the host. +extends_documentation_fragment: f5 +requirements: + - netaddr + - f5-sdk +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = ''' +- name: Create Self IP + bigip_selfip: + address: "10.10.10.10" + name: "self1" + netmask: "255.255.255.0" + password: "secret" + server: "lb.mydomain.com" + user: "admin" + validate_certs: "no" + vlan: "vlan1" + delegate_to: localhost + +- name: Delete Self IP + bigip_selfip: + name: "self1" + password: "secret" + server: "lb.mydomain.com" + state: "absent" + user: "admin" + validate_certs: "no" + delegate_to: localhost +''' + +RETURN = ''' +address: + description: The address for the Self IP + returned: created + type: string + sample: "192.168.10.10" +name: + description: The name of the Self IP + returned: + - created + - changed + - deleted + type: string + sample: "self1" +netmask: + description: The netmask of the Self IP + returned: + - changed + - created + type: string + sample: "255.255.255.0" +traffic_group: + description: The traffic group that the Self IP is a member of + return: + - changed + - created + type: string + sample: "traffic-group-local-only" +vlan: + description: The VLAN set on the Self IP + return: + - changed + - created + type: string + sample: "vlan1" +''' + +try: + from f5.bigip import ManagementRoot + from icontrol.session import iControlUnexpectedHTTPError + HAS_F5SDK = True +except ImportError: + HAS_F5SDK = False + +try: + from netaddr import IPNetwork, AddrFormatError + HAS_NETADDR = True +except ImportError: + HAS_NETADDR = False + +FLOAT = ['enabled', 'disabled'] +DEFAULT_TG = 'traffic-group-local-only' + + +class BigIpSelfIp(object): + def __init__(self, *args, **kwargs): + if not HAS_F5SDK: + raise F5ModuleError("The python f5-sdk module is required") + + # The params that change in the module + self.cparams = dict() + + # Stores the params that are sent to the module + self.params = kwargs + self.api = ManagementRoot(kwargs['server'], + kwargs['user'], + kwargs['password'], + port=kwargs['server_port']) + + def present(self): + changed = False + + if self.exists(): + changed = self.update() + else: + changed = self.create() + + return changed + + def absent(self): + changed = False + + if self.exists(): + changed = self.delete() + + return changed + + def read(self): + """Read information and transform it + + The values that are returned by BIG-IP in the f5-sdk can have encoding + attached to them as well as be completely missing in some cases. + + Therefore, this method will transform the data from the BIG-IP into a + format that is more easily consumable by the rest of the class and the + parameters that are supported by the module. + """ + p = dict() + name = self.params['name'] + partition = self.params['partition'] + r = self.api.tm.net.selfips.selfip.load( + name=name, + partition=partition + ) + + if hasattr(r, 'address'): + ipnet = IPNetwork(r.address) + p['address'] = str(ipnet.ip) + if hasattr(r, 'address'): + ipnet = IPNetwork(r.address) + p['netmask'] = str(ipnet.netmask) + if hasattr(r, 'trafficGroup'): + p['traffic_group'] = str(r.trafficGroup) + if hasattr(r, 'vlan'): + p['vlan'] = str(r.vlan) + p['name'] = name + return p + + def update(self): + changed = False + params = dict() + current = self.read() + + check_mode = self.params['check_mode'] + address = self.params['address'] + name = self.params['name'] + netmask = self.params['netmask'] + partition = self.params['partition'] + traffic_group = self.params['traffic_group'] + vlan = self.params['vlan'] + + if address is not None and address != current['address']: + raise F5ModuleError( + 'Self IP addresses cannot be updated' + ) + + if netmask is not None: + # I ignore the address value here even if they provide it because + # you are not allowed to change it. + try: + address = IPNetwork(current['address']) + + new_addr = "%s/%s" % (address.ip, netmask) + nipnet = IPNetwork(new_addr) + + cur_addr = "%s/%s" % (current['address'], current['netmask']) + cipnet = IPNetwork(cur_addr) + + if nipnet != cipnet: + address = "%s/%s" % (nipnet.ip, nipnet.prefixlen) + params['address'] = address + except AddrFormatError: + raise F5ModuleError( + 'The provided address/netmask value was invalid' + ) + + if traffic_group is not None: + groups = self.api.tm.cm.traffic_groups.get_collection() + params['trafficGroup'] = "/%s/%s" % (partition, traffic_group) + + if 'traffic_group' in current: + if traffic_group != current['traffic_group']: + params['trafficGroup'] = traffic_group + else: + params['trafficGroup'] = traffic_group + + if traffic_group not in groups: + raise F5ModuleError( + 'The specified traffic group was not found' + ) + + if vlan is not None: + vlans = self.get_vlans() + vlan = "/%s/%s" % (partition, vlan) + + if 'vlan' in current: + if vlan != current['vlan']: + params['vlan'] = vlan + else: + params['vlan'] = vlan + + if vlan not in vlans: + raise F5ModuleError( + 'The specified VLAN was not found' + ) + + if params: + changed = True + params['name'] = name + params['partition'] = partition + if check_mode: + return changed + self.cparams = camel_dict_to_snake_dict(params) + else: + return changed + + r = self.api.tm.net.selfips.selfip.load( + name=name, + partition=partition + ) + r.update(**params) + r.refresh() + + return True + + def get_vlans(self): + partition = self.params['partition'] + vlans = self.api.tm.net.vlans.get_collection() + return [str("/" + partition + "/" + x.name) for x in vlans] + + def create(self): + params = dict() + + check_mode = self.params['check_mode'] + address = self.params['address'] + name = self.params['name'] + netmask = self.params['netmask'] + partition = self.params['partition'] + traffic_group = self.params['traffic_group'] + vlan = self.params['vlan'] + + if address is None or netmask is None: + raise F5ModuleError( + 'An address and a netmask must be specififed' + ) + + if vlan is None: + raise F5ModuleError( + 'A VLAN name must be specified' + ) + else: + vlan = "/%s/%s" % (partition, vlan) + + try: + ipin = "%s/%s" % (address, netmask) + ipnet = IPNetwork(ipin) + params['address'] = "%s/%s" % (ipnet.ip, ipnet.prefixlen) + except AddrFormatError: + raise F5ModuleError( + 'The provided address/netmask value was invalid' + ) + + if traffic_group is None: + params['trafficGroup'] = "/%s/%s" % (partition, DEFAULT_TG) + else: + groups = self.api.tm.cm.traffic_groups.get_collection() + if traffic_group in groups: + params['trafficGroup'] = "/%s/%s" % (partition, traffic_group) + else: + raise F5ModuleError( + 'The specified traffic group was not found' + ) + + vlans = self.get_vlans() + if vlan in vlans: + params['vlan'] = vlan + else: + raise F5ModuleError( + 'The specified VLAN was not found' + ) + + params['name'] = name + params['partition'] = partition + + self.cparams = camel_dict_to_snake_dict(params) + if check_mode: + return True + + d = self.api.tm.net.selfips.selfip + d.create(**params) + + if self.exists(): + return True + else: + raise F5ModuleError("Failed to create the self IP") + + def delete(self): + params = dict() + check_mode = self.params['check_mode'] + + params['name'] = self.params['name'] + params['partition'] = self.params['partition'] + + self.cparams = camel_dict_to_snake_dict(params) + if check_mode: + return True + + dc = self.api.tm.net.selfips.selfip.load(**params) + dc.delete() + + if self.exists(): + raise F5ModuleError("Failed to delete the self IP") + return True + + def exists(self): + name = self.params['name'] + partition = self.params['partition'] + return self.api.tm.net.selfips.selfip.exists( + name=name, + partition=partition + ) + + def flush(self): + result = dict() + state = self.params['state'] + + try: + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + except iControlUnexpectedHTTPError as e: + raise F5ModuleError(str(e)) + + result.update(**self.cparams) + result.update(dict(changed=changed)) + return result + + +def main(): + argument_spec = f5_argument_spec() + + meta_args = dict( + address=dict(required=False, default=None), + name=dict(required=True), + netmask=dict(required=False, default=None), + traffic_group=dict(required=False, default=None), + vlan=dict(required=False, default=None) + ) + argument_spec.update(meta_args) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True + ) + + try: + if not HAS_NETADDR: + raise F5ModuleError( + "The netaddr python module is required." + ) + + obj = BigIpSelfIp(check_mode=module.check_mode, **module.params) + result = obj.flush() + + module.exit_json(**result) + except F5ModuleError as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import camel_dict_to_snake_dict +from ansible.module_utils.f5 import * + +if __name__ == '__main__': + main() From 6a7947db9f5f9ed4be7de413f4430cb6ca7d8d55 Mon Sep 17 00:00:00 2001 From: Greyeye Date: Fri, 19 Aug 2016 02:46:37 +1000 Subject: [PATCH 1966/2522] pass flag to choco, allowemptychecksums and ignorechecksums (#2722) --- windows/win_chocolatey.ps1 | 35 ++++++++++++++++++++++++++++++++++- windows/win_chocolatey.py | 12 ++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/windows/win_chocolatey.ps1 b/windows/win_chocolatey.ps1 index 546fc7621a4..7a3f14aabc3 100644 --- a/windows/win_chocolatey.ps1 +++ b/windows/win_chocolatey.ps1 @@ -37,6 +37,8 @@ $state = Get-Attr -obj $params -name state -default "present" $installargs = Get-Attr -obj $params -name install_args -default $null $packageparams = Get-Attr -obj $params -name params -default $null +$allowemptychecksums = Get-Attr -obj $params -name allow_empty_checksums -default "false" | ConvertTo-Bool +$ignorechecksums = Get-Attr -obj $params -name ignore_checksums -default "false" | ConvertTo-Bool $ignoredependencies = Get-Attr -obj $params -name ignore_dependencies -default "false" | ConvertTo-Bool # as of chocolatey 0.9.10, nonzero success exit codes can be returned @@ -126,6 +128,10 @@ Function Choco-Upgrade [Parameter(Mandatory=$false, Position=6)] [string]$packageparams, [Parameter(Mandatory=$false, Position=7)] + [bool]$allowemptychecksums, + [Parameter(Mandatory=$false, Position=8)] + [bool]$ignorechecksums, + [Parameter(Mandatory=$false, Position=9)] [bool]$ignoredependencies ) @@ -161,6 +167,16 @@ Function Choco-Upgrade $cmd += " -params '$packageparams'" } + if ($allowemptychecksums) + { + $cmd += " --allow-empty-checksums" + } + + if ($ignorechecksums) + { + $cmd += " --ignore-checksums" + } + if ($ignoredependencies) { $cmd += " -ignoredependencies" @@ -204,6 +220,10 @@ Function Choco-Install [Parameter(Mandatory=$false, Position=7)] [string]$packageparams, [Parameter(Mandatory=$false, Position=8)] + [bool]$allowemptychecksums, + [Parameter(Mandatory=$false, Position=9)] + [bool]$ignorechecksums, + [Parameter(Mandatory=$false, Position=10)] [bool]$ignoredependencies ) @@ -213,6 +233,7 @@ Function Choco-Install { Choco-Upgrade -package $package -version $version -source $source -force $force ` -installargs $installargs -packageparams $packageparams ` + -allowemptychecksums $allowemptychecksums -ignorechecksums $ignorechecksums ` -ignoredependencies $ignoredependencies return @@ -251,6 +272,16 @@ Function Choco-Install $cmd += " -params '$packageparams'" } + if ($allowemptychecksums) + { + $cmd += " --allow-empty-checksums" + } + + if ($ignorechecksums) + { + $cmd += " --ignore-checksums" + } + if ($ignoredependencies) { $cmd += " -ignoredependencies" @@ -322,7 +353,8 @@ Try { Choco-Install -package $package -version $version -source $source ` -force $force -upgrade $upgrade -installargs $installargs ` - -packageparams $packageparams -ignoredependencies $ignoredependencies + -packageparams $packageparams -allowemptychecksums $allowemptychecksums ` + -ignorechecksums $ignorechecksums -ignoredependencies $ignoredependencies } else { @@ -336,3 +368,4 @@ Catch Fail-Json $result $_.Exception.Message } + diff --git a/windows/win_chocolatey.py b/windows/win_chocolatey.py index 4fd512d1fbe..9bcc82a8462 100644 --- a/windows/win_chocolatey.py +++ b/windows/win_chocolatey.py @@ -80,6 +80,18 @@ require: false default: null version_added: '2.1' + allow_empty_checksums: + description: + - Allow empty Checksums to be used + require: false + default: false + version_added: '2.2' + ignore_checksums: + description: + - Ignore Checksums + require: false + default: false + version_added: '2.2' ignore_dependencies: description: - Ignore dependencies, only install/upgrade the package itself From b7bd32940269359ff8483cd5d4cac69333235e02 Mon Sep 17 00:00:00 2001 From: nitzmahone Date: Thu, 18 Aug 2016 10:31:29 -0700 Subject: [PATCH 1967/2522] clean up win_chocolatey doc bugs, remove redundant --- windows/win_chocolatey.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/windows/win_chocolatey.py b/windows/win_chocolatey.py index 9bcc82a8462..ac80ad9e18a 100644 --- a/windows/win_chocolatey.py +++ b/windows/win_chocolatey.py @@ -36,7 +36,6 @@ state: description: - State of the package on the system - required: false choices: - present - absent @@ -44,7 +43,6 @@ force: description: - Forces install of the package (even if it already exists). Using Force will cause ansible to always report that a change was made - required: false choices: - yes - no @@ -52,7 +50,6 @@ upgrade: description: - If package is already installed it, try to upgrade to the latest version or to the specified version - required: false choices: - yes - no @@ -61,24 +58,16 @@ description: - Specific version of the package to be installed - Ignored when state == 'absent' - required: false - default: null source: description: - Specify source rather than using default chocolatey repository - require: false - default: null install_args: description: - Arguments to pass to the native installer - require: false - default: null version_added: '2.1' params: description: - Parameters to pass to the package - require: false - default: null version_added: '2.1' allow_empty_checksums: description: @@ -95,7 +84,6 @@ ignore_dependencies: description: - Ignore dependencies, only install/upgrade the package itself - require: false default: false version_added: '2.1' author: "Trond Hindenes (@trondhindenes), Peter Mounce (@petemounce), Pepe Barbe (@elventear), Adam Keech (@smadam813)" From fa6e654eb2344607be27532f2d528b96798162a4 Mon Sep 17 00:00:00 2001 From: Rob White Date: Wed, 20 Jul 2016 12:49:42 +1000 Subject: [PATCH 1968/2522] New module - ec2_asg_facts Gathers facts about multiple of single AWS autoscaling groups. --- cloud/amazon/ec2_asg_facts.py | 351 ++++++++++++++++++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 cloud/amazon/ec2_asg_facts.py diff --git a/cloud/amazon/ec2_asg_facts.py b/cloud/amazon/ec2_asg_facts.py new file mode 100644 index 00000000000..631677f1381 --- /dev/null +++ b/cloud/amazon/ec2_asg_facts.py @@ -0,0 +1,351 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ec2_asg_facts +short_description: Gather facts about ec2 Auto Scaling Groups (ASGs) in AWS +description: + - Gather facts about ec2 Auto Scaling Groups (ASGs) in AWS +version_added: "2.2" +author: "Rob White (@wimnat)" +options: + name: + description: + - The prefix or name of the auto scaling group(s) you are searching for. + - "Note: This is a regular expression match with implicit '^' (beginning of string). Append '$' for a complete name match." + required: false + tags: + description: + - "A dictionary/hash of tags in the format { tag1_name: 'tag1_value', tag2_name: 'tag2_value' } to match against the auto scaling group(s) you are searching for." + required: false +extends_documentation_fragment: + - aws + - ec2 +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Find all groups +- ec2_asg_facts: + register: asgs + +# Find a group with matching name/prefix +- ec2_asg_facts: + name: public-webserver-asg + register: asgs + +# Find a group with matching tags +- ec2_asg_facts: + tags: + project: webapp + env: production + register: asgs + +# Find a group with matching name/prefix and tags +- ec2_asg_facts: + name: myproject + tags: + env: production + register: asgs + +# Fail if no groups are found +- ec2_asg_facts: + name: public-webserver-asg + register: asgs + failed_when: "{{ asgs.results | length == 0 }}" + +# Fail if more than 1 group is found +- ec2_asg_facts: + name: public-webserver-asg + register: asgs + failed_when: "{{ asgs.results | length > 1 }}" +''' + +RETURN = ''' +--- +auto_scaling_group_arn: + description: The Amazon Resource Name of the ASG + returned: success + type: string + sample: "arn:aws:autoscaling:us-west-2:1234567890:autoScalingGroup:10787c52-0bcb-427d-82ba-c8e4b008ed2e:autoScalingGroupName/public-webapp-production-1" +auto_scaling_group_name: + description: Name of autoscaling group + returned: success + type: str + sample: "public-webapp-production-1" +availability_zones: + description: List of Availability Zones that are enabled for this ASG. + returned: success + type: list + sample: ["us-west-2a", "us-west-2b", "us-west-2a"] +created_time: + description: The date and time this ASG was created, in ISO 8601 format. + returned: success + type: string + sample: "2015-11-25T00:05:36.309Z" +default_cooldown: + description: The default cooldown time in seconds. + returned: success + type: int + sample: 300 +desired_capacity: + description: The number of EC2 instances that should be running in this group. + returned: success + type: int + sample: 3 +health_check_period: + description: Length of time in seconds after a new EC2 instance comes into service that Auto Scaling starts checking its health. + returned: success + type: int + sample: 30 +health_check_type: + description: The service you want the health status from, one of "EC2" or "ELB". + returned: success + type: str + sample: "ELB" +instances: + description: List of EC2 instances and their status as it relates to the ASG. + returned: success + type: list + sample: [ + { + "availability_zone": "us-west-2a", + "health_status": "Healthy", + "instance_id": "i-es22ad25", + "launch_configuration_name": "public-webapp-production-1", + "lifecycle_state": "InService", + "protected_from_scale_in": "false" + } + ] +launch_configuration_name: + description: Name of launch configuration associated with the ASG. + returned: success + type: str + sample: "public-webapp-production-1" +load_balancer_names: + description: List of load balancers names attached to the ASG. + returned: success + type: list + sample: ["elb-webapp-prod"] +max_size: + description: Maximum size of group + returned: success + type: int + sample: 3 +min_size: + description: Minimum size of group + returned: success + type: int + sample: 1 +new_instances_protected_from_scale_in: + description: Whether or not new instances a protected from automatic scaling in. + returned: success + type: boolean + sample: "false" +placement_group: + description: Placement group into which instances are launched, if any. + returned: success + type: str + sample: None +status: + description: The current state of the group when DeleteAutoScalingGroup is in progress. + returned: success + type: str + sample: None +tags: + description: List of tags for the ASG, and whether or not each tag propagates to instances at launch. + returned: success + type: list + sample: [ + { + "key": "Name", + "value": "public-webapp-production-1", + "resource_id": "public-webapp-production-1", + "resource_type": "auto-scaling-group", + "propagate_at_launch": "true" + }, + { + "key": "env", + "value": "production", + "resource_id": "public-webapp-production-1", + "resource_type": "auto-scaling-group", + "propagate_at_launch": "true" + } + ] +termination_policies: + description: A list of termination policies for the group. + returned: success + type: str + sample: ["Default"] +''' + +try: + import boto3 + from botocore.exceptions import ClientError + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + +def match_asg_tags(tags_to_match, asg): + for key, value in tags_to_match.iteritems(): + for tag in asg['Tags']: + if key == tag['Key'] and value == tag['Value']: + break + else: return False + return True + +def find_asgs(conn, module, name=None, tags=None): + """ + Args: + conn (boto3.AutoScaling.Client): Valid Boto3 ASG client. + name (str): Optional name of the ASG you are looking for. + tags (dict): Optional dictionary of tags and values to search for. + + Basic Usage: + >>> name = 'public-webapp-production' + >>> tags = { 'env': 'production' } + >>> conn = boto3.client('autoscaling', region_name='us-west-2') + >>> results = find_asgs(name, conn) + + Returns: + List + [ + { + "auto_scaling_group_arn": "arn:aws:autoscaling:us-west-2:275977225706:autoScalingGroup:58abc686-9783-4528-b338-3ad6f1cbbbaf:autoScalingGroupName/public-webapp-production", + "auto_scaling_group_name": "public-webapp-production", + "availability_zones": ["us-west-2c", "us-west-2b", "us-west-2a"], + "created_time": "2016-02-02T23:28:42.481000+00:00", + "default_cooldown": 300, + "desired_capacity": 2, + "enabled_metrics": [], + "health_check_grace_period": 300, + "health_check_type": "ELB", + "instances": + [ + { + "availability_zone": "us-west-2c", + "health_status": "Healthy", + "instance_id": "i-047a12cb", + "launch_configuration_name": "public-webapp-production-1", + "lifecycle_state": "InService", + "protected_from_scale_in": false + }, + { + "availability_zone": "us-west-2a", + "health_status": "Healthy", + "instance_id": "i-7a29df2c", + "launch_configuration_name": "public-webapp-production-1", + "lifecycle_state": "InService", + "protected_from_scale_in": false + } + ], + "launch_configuration_name": "public-webapp-production-1", + "load_balancer_names": ["public-webapp-production-lb"], + "max_size": 4, + "min_size": 2, + "new_instances_protected_from_scale_in": false, + "placement_group": None, + "status": None, + "suspended_processes": [], + "tags": + [ + { + "key": "Name", + "propagate_at_launch": true, + "resource_id": "public-webapp-production", + "resource_type": "auto-scaling-group", + "value": "public-webapp-production" + }, + { + "key": "env", + "propagate_at_launch": true, + "resource_id": "public-webapp-production", + "resource_type": "auto-scaling-group", + "value": "production" + } + ], + "termination_policies": + [ + "Default" + ], + "vpc_zone_identifier": + [ + "subnet-a1b1c1d1", + "subnet-a2b2c2d2", + "subnet-a3b3c3d3" + ] + } + ] + """ + + try: + asgs = conn.describe_auto_scaling_groups() + except ClientError as e: + module.fail_json(msg=e.message, **camel_dict_to_snake_dict(e.response)) + + matched_asgs = [] + name_prog = re.compile(r'^' + name) + for asg in asgs['AutoScalingGroups']: + if name: + matched_name = name_prog.search(asg['auto_scaling_group_name']) + else: + matched_name = True + + if tags: + matched_tags = match_asg_tags(tags, asg) + else: + matched_tags = True + + if matched_name and matched_tags: + matched_asgs.append(camel_dict_to_snake_dict(asg)) + + return matched_asgs + + +def main(): + + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + name=dict(type='str'), + tags=dict(type='dict'), + ) + ) + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO3: + module.fail_json(msg='boto3 required for this module') + + asg_name = module.params.get('name') + asg_tags = module.params.get('tags') + + try: + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + autoscaling = boto3_conn(module, conn_type='client', resource='autoscaling', region=region, endpoint=ec2_url, **aws_connect_kwargs) + except ClientError as e: + module.fail_json(msg=e.message, **camel_dict_to_snake_dict(e.response)) + + results = find_asgs(autoscaling, module, name=asg_name, tags=asg_tags) + module.exit_json(results=results) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From 0833a1e8c06186ff75e94b8b4bf09159861e5951 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Fri, 19 Aug 2016 13:07:38 -0700 Subject: [PATCH 1969/2522] Another bootstrapping module, this module allows for one to manage route domains on a BIG-IP. Tests for this module can be found here https://github.com/F5Networks/f5-ansible/blob/master/roles/__bigip_routedomain/tasks/main.yaml Platforms this was tested on are 11.6.0 12.0.0 12.1.0 12.1.0 HF1 --- network/f5/bigip_routedomain.py | 509 ++++++++++++++++++++++++++++++++ 1 file changed, 509 insertions(+) create mode 100644 network/f5/bigip_routedomain.py diff --git a/network/f5/bigip_routedomain.py b/network/f5/bigip_routedomain.py new file mode 100644 index 00000000000..bb71f850f57 --- /dev/null +++ b/network/f5/bigip_routedomain.py @@ -0,0 +1,509 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: bigip_routedomain +short_description: Manage route domains on a BIG-IP +description: + - Manage route domains on a BIG-IP +version_added: "2.2" +options: + bwc_policy: + description: + - The bandwidth controller for the route domain. + connection_limit: + description: + - The maximum number of concurrent connections allowed for the + route domain. Setting this to C(0) turns off connection limits. + description: + description: + - Specifies descriptive text that identifies the route domain. + flow_eviction_policy: + description: + - The eviction policy to use with this route domain. Apply an eviction + policy to provide customized responses to flow overflows and slow + flows on the route domain. + id: + description: + - The unique identifying integer representing the route domain. + required: true + parent: + description: | + Specifies the route domain the system searches when it cannot + find a route in the configured domain. + routing_protocol: + description: + - Dynamic routing protocols for the system to use in the route domain. + choices: + - BFD + - BGP + - IS-IS + - OSPFv2 + - OSPFv3 + - PIM + - RIP + - RIPng + service_policy: + description: + - Service policy to associate with the route domain. + state: + description: + - Whether the route domain should exist or not. + required: false + default: present + choices: + - present + - absent + strict: + description: + - Specifies whether the system enforces cross-routing restrictions + or not. + choices: + - enabled + - disabled + vlans: + description: + - VLANs for the system to use in the route domain +notes: + - Requires the f5-sdk Python package on the host. This is as easy as + pip install f5-sdk. +extends_documentation_fragment: f5 +requirements: + - f5-sdk +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = ''' +- name: Create a route domain + bigip_routedomain: + id: "1234" + password: "secret" + server: "lb.mydomain.com" + state: "present" + user: "admin" + delegate_to: localhost + +- name: Set VLANs on the route domain + bigip_routedomain: + id: "1234" + password: "secret" + server: "lb.mydomain.com" + state: "present" + user: "admin" + vlans: + - net1 + - foo + delegate_to: localhost +''' + +RETURN = ''' +id: + description: The ID of the route domain that was changed + returned: changed + type: int + sample: 2 +description: + description: The description of the route domain + returned: changed + type: string + sample: "route domain foo" +strict: + description: The new strict isolation setting + returned: changed + type: string + sample: "enabled" +parent: + description: The new parent route domain + returned: changed + type: int + sample: 0 +vlans: + description: List of new VLANs the route domain is applied to + returned: changed + type: list + sample: ['/Common/http-tunnel', '/Common/socks-tunnel'] +routing_protocol: + description: List of routing protocols applied to the route domain + returned: changed + type: list + sample: ['bfd', 'bgp'] +bwc_policy: + description: The new bandwidth controller + returned: changed + type: string + sample: /Common/foo +connection_limit: + description: The new connection limit for the route domain + returned: changed + type: integer + sample: 100 +flow_eviction_policy: + description: The new eviction policy to use with this route domain + returned: changed + type: string + sample: /Common/default-eviction-policy +service_policy: + description: The new service policy to use with this route domain + returned: changed + type: string + sample: /Common-my-service-policy +''' + +try: + from f5.bigip import ManagementRoot + from icontrol.session import iControlUnexpectedHTTPError + HAS_F5SDK = True +except ImportError: + HAS_F5SDK = False + +PROTOCOLS = [ + 'BFD', 'BGP', 'IS-IS', 'OSPFv2', 'OSPFv3', 'PIM', 'RIP', 'RIPng' +] + +STRICTS = ['enabled', 'disabled'] + + +class BigIpRouteDomain(object): + def __init__(self, *args, **kwargs): + if not HAS_F5SDK: + raise F5ModuleError("The python f5-sdk module is required") + + # The params that change in the module + self.cparams = dict() + + kwargs['name'] = str(kwargs['id']) + + # Stores the params that are sent to the module + self.params = kwargs + self.api = ManagementRoot(kwargs['server'], + kwargs['user'], + kwargs['password'], + port=kwargs['server_port']) + + def absent(self): + if not self.exists(): + return False + + if self.params['check_mode']: + return True + + rd = self.api.tm.net.route_domains.route_domain.load( + name=self.params['name'] + ) + rd.delete() + + if self.exists(): + raise F5ModuleError("Failed to delete the route domain") + else: + return True + + def present(self): + if self.exists(): + return self.update() + else: + if self.params['check_mode']: + return True + return self.create() + + def read(self): + """Read information and transform it + + The values that are returned by BIG-IP in the f5-sdk can have encoding + attached to them as well as be completely missing in some cases. + + Therefore, this method will transform the data from the BIG-IP into a + format that is more easily consumable by the rest of the class and the + parameters that are supported by the module. + """ + p = dict() + r = self.api.tm.net.route_domains.route_domain.load( + name=self.params['name'] + ) + + p['id'] = int(r.id) + p['name'] = str(r.name) + + if hasattr(r, 'connectionLimit'): + p['connection_limit'] = int(r.connectionLimit) + if hasattr(r, 'description'): + p['description'] = str(r.description) + if hasattr(r, 'strict'): + p['strict'] = str(r.strict) + if hasattr(r, 'parent'): + p['parent'] = r.parent + if hasattr(r, 'vlans'): + p['vlans'] = list(set([str(x) for x in r.vlans])) + if hasattr(r, 'routingProtocol'): + p['routing_protocol'] = list(set([str(x) for x in r.routingProtocol])) + if hasattr(r, 'flowEvictionPolicy'): + p['flow_eviction_policy'] = str(r.flowEvictionPolicy) + if hasattr(r, 'bwcPolicy'): + p['bwc_policy'] = str(r.bwcPolicy) + if hasattr(r, 'servicePolicy'): + p['service_policy'] = str(r.servicePolicy) + return p + + def create(self): + params = dict() + params['id'] = self.params['id'] + params['name'] = self.params['name'] + + partition = self.params['partition'] + description = self.params['description'] + strict = self.params['strict'] + parent = self.params['parent'] + bwc_policy = self.params['bwc_policy'] + vlans = self.params['vlans'] + routing_protocol = self.params['routing_protocol'] + connection_limit = self.params['connection_limit'] + flow_eviction_policy = self.params['flow_eviction_policy'] + service_policy = self.params['service_policy'] + + if description is not None: + params['description'] = description + + if strict is not None: + params['strict'] = strict + + if parent is not None: + parent = '/%s/%s' % (partition, parent) + if parent in self.domains: + params['parent'] = parent + else: + raise F5ModuleError( + "The parent route domain was not found" + ) + + if bwc_policy is not None: + policy = '/%s/%s' % (partition, bwc_policy) + params['bwcPolicy'] = policy + + if vlans is not None: + params['vlans'] = [] + for vlan in vlans: + vname = '/%s/%s' % (partition, vlan) + params['vlans'].append(vname) + + if routing_protocol is not None: + params['routingProtocol'] = [] + for protocol in routing_protocol: + if protocol in PROTOCOLS: + params['routingProtocol'].append(protocol) + else: + raise F5ModuleError( + "routing_protocol must be one of: %s" % (PROTOCOLS) + ) + + if connection_limit is not None: + params['connectionLimit'] = connection_limit + + if flow_eviction_policy is not None: + policy = '/%s/%s' % (partition, flow_eviction_policy) + params['flowEvictionPolicy'] = policy + + if service_policy is not None: + policy = '/%s/%s' % (partition, service_policy) + params['servicePolicy'] = policy + + self.api.tm.net.route_domains.route_domain.create(**params) + exists = self.api.tm.net.route_domains.route_domain.exists( + name=self.params['name'] + ) + + if exists: + return True + else: + raise F5ModuleError( + "An error occurred while creating the route domain" + ) + + def update(self): + changed = False + params = dict() + current = self.read() + + check_mode = self.params['check_mode'] + partition = self.params['partition'] + description = self.params['description'] + strict = self.params['strict'] + parent = self.params['parent'] + bwc_policy = self.params['bwc_policy'] + vlans = self.params['vlans'] + routing_protocol = self.params['routing_protocol'] + connection_limit = self.params['connection_limit'] + flow_eviction_policy = self.params['flow_eviction_policy'] + service_policy = self.params['service_policy'] + + if description is not None: + if 'description' in current: + if description != current['description']: + params['description'] = description + else: + params['description'] = description + + if strict is not None: + if strict != current['strict']: + params['strict'] = strict + + if parent is not None: + parent = '/%s/%s' % (partition, parent) + if 'parent' in current: + if parent != current['parent']: + params['parent'] = parent + else: + params['parent'] = parent + + if bwc_policy is not None: + policy = '/%s/%s' % (partition, bwc_policy) + if 'bwc_policy' in current: + if policy != current['bwc_policy']: + params['bwcPolicy'] = policy + else: + params['bwcPolicy'] = policy + + if vlans is not None: + tmp = set() + for vlan in vlans: + vname = '/%s/%s' % (partition, vlan) + tmp.add(vname) + tmp = list(tmp) + if 'vlans' in current: + if tmp != current['vlans']: + params['vlans'] = tmp + else: + params['vlans'] = tmp + + if routing_protocol is not None: + tmp = set() + for protocol in routing_protocol: + if protocol in PROTOCOLS: + tmp.add(protocol) + else: + raise F5ModuleError( + "routing_protocol must be one of: %s" % (PROTOCOLS) + ) + tmp = list(tmp) + if 'routing_protocol' in current: + if tmp != current['routing_protocol']: + params['routingProtocol'] = tmp + else: + params['routingProtocol'] = tmp + + if connection_limit is not None: + if connection_limit != current['connection_limit']: + params['connectionLimit'] = connection_limit + + if flow_eviction_policy is not None: + policy = '/%s/%s' % (partition, flow_eviction_policy) + if 'flow_eviction_policy' in current: + if policy != current['flow_eviction_policy']: + params['flowEvictionPolicy'] = policy + else: + params['flowEvictionPolicy'] = policy + + if service_policy is not None: + policy = '/%s/%s' % (partition, service_policy) + if 'service_policy' in current: + if policy != current['service_policy']: + params['servicePolicy'] = policy + else: + params['servicePolicy'] = policy + + if params: + changed = True + self.cparams = camel_dict_to_snake_dict(params) + if check_mode: + return changed + else: + return changed + + try: + rd = self.api.tm.net.route_domains.route_domain.load( + name=self.params['name'] + ) + rd.update(**params) + rd.refresh() + except iControlUnexpectedHTTPError as e: + raise F5ModuleError(e) + + return True + + def exists(self): + return self.api.tm.net.route_domains.route_domain.exists( + name=self.params['name'] + ) + + def flush(self): + result = dict() + state = self.params['state'] + + if self.params['check_mode']: + if value == current: + changed = False + else: + changed = True + else: + if state == "present": + changed = self.present() + current = self.read() + result.update(current) + elif state == "absent": + changed = self.absent() + + result.update(dict(changed=changed)) + return result + + +def main(): + argument_spec = f5_argument_spec() + + meta_args = dict( + id=dict(required=True, type='int'), + description=dict(required=False, default=None), + strict=dict(required=False, default=None, choices=STRICTS), + parent=dict(required=False, type='int', default=None), + vlans=dict(required=False, default=None, type='list'), + routing_protocol=dict(required=False, default=None, type='list'), + bwc_policy=dict(required=False, type='str', default=None), + connection_limit=dict(required=False, type='int', default=None), + flow_eviction_policy=dict(required=False, type='str', default=None), + service_policy=dict(required=False, type='str', default=None) + ) + argument_spec.update(meta_args) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True + ) + + try: + obj = BigIpRouteDomain(check_mode=module.check_mode, **module.params) + result = obj.flush() + + module.exit_json(**result) + except F5ModuleError as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import camel_dict_to_snake_dict +from ansible.module_utils.f5 import * + +if __name__ == '__main__': + main() From 119efdcb8225c4212c6968ce98b5ace3c45360f7 Mon Sep 17 00:00:00 2001 From: whiter Date: Tue, 30 Jun 2015 12:45:02 +1000 Subject: [PATCH 1970/2522] New module `s3_website` to manage static sites on Amazon S3 --- cloud/amazon/s3_website.py | 293 +++++++++++++++++++++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 cloud/amazon/s3_website.py diff --git a/cloud/amazon/s3_website.py b/cloud/amazon/s3_website.py new file mode 100644 index 00000000000..93de7210953 --- /dev/null +++ b/cloud/amazon/s3_website.py @@ -0,0 +1,293 @@ +#!/usr/bin/python +# +# This is a free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This Ansible library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this library. If not, see . + +DOCUMENTATION = ''' +--- +module: s3_website +short_description: Configure an s3 bucket as a website +description: + - Configure an s3 bucket as a website +version_added: "2.2" +author: Rob White (@wimnat) +options: + name: + description: + - "Name of the s3 bucket" + required: true + default: null + error_key: + description: + - "The object key name to use when a 4XX class error occurs. To remove an error key, set to None." + required: false + default: null + redirect_all_requests: + description: + - "Describes the redirect behavior for every request to this s3 bucket website endpoint" + required: false + default: null + region: + description: + - "AWS region to create the bucket in. If not set then the value of the AWS_REGION and EC2_REGION environment variables are checked, followed by the aws_region and ec2_region settings in the Boto config file. If none of those are set the region defaults to the S3 Location: US Standard." + required: false + default: null + state: + description: + - "Add or remove s3 website configuration" + required: false + default: present + choices: [ 'present', 'absent' ] + suffix: + description: + - "Suffix that is appended to a request that is for a directory on the website endpoint (e.g. if the suffix is index.html and you make a request to samplebucket/images/ the data that is returned will be for the object with the key name images/index.html). The suffix must not include a slash character." + required: false + default: index.html + +extends_documentation_fragment: + - aws + - ec2 +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Configure an s3 bucket to redirect all requests to example.com +- s3_website: + name: mybucket.com + redirect_all_requests: example.com + state: present + +# Remove website configuration from an s3 bucket +- s3_website: + name: mybucket.com + state: absent + +# Configure an s3 bucket as a website with index and error pages +- s3_website: + name: mybucket.com + suffix: home.htm + error_key: errors/404.htm + state: present + +''' + +RETURN = ''' +index_document: + suffix: + description: suffix that is appended to a request that is for a directory on the website endpoint + returned: success + type: string + sample: index.html +error_document: + key: + description: object key name to use when a 4XX class error occurs + returned: when error_document parameter set + type: string + sample: error.html +redirect_all_requests_to: + host_name: + description: name of the host where requests will be redirected. + returned: when redirect all requests parameter set + type: string + sample: ansible.com +routing_rules: + routing_rule: + host_name: + description: name of the host where requests will be redirected. + returned: when host name set as part of redirect rule + type: string + sample: ansible.com + condition: + key_prefix_equals: + description: object key name prefix when the redirect is applied. For example, to redirect requests for ExamplePage.html, the key prefix will be ExamplePage.html + returned: when routing rule present + type: string + sample: docs/ + redirect: + replace_key_prefix_with: + description: object key prefix to use in the redirect request + returned: when routing rule present + type: string + sample: documents/ + +''' + +import time + +try: + from botocore.exceptions import ClientError, ParamValidationError, NoCredentialsError + import boto3 + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + +def _create_redirect_dict(url): + + redirect_dict = {} + url_split = url.split(':') + + # Did we split anything? + if len(url_split) == 2: + redirect_dict[u'Protocol'] = url_split[0] + redirect_dict[u'HostName'] = url_split[1].replace('//', '') + elif len(url_split) == 1: + redirect_dict[u'HostName'] = url_split[0] + else: + raise ValueError('Redirect URL appears invalid') + + return redirect_dict + + +def _create_website_configuration(suffix, error_key, redirect_all_requests): + + website_configuration = {} + + if error_key is not None: + website_configuration['ErrorDocument'] = { 'Key': error_key } + + if suffix is not None: + website_configuration['IndexDocument'] = { 'Suffix': suffix } + + if redirect_all_requests is not None: + website_configuration['RedirectAllRequestsTo'] = _create_redirect_dict(redirect_all_requests) + + return website_configuration + + +def enable_or_update_bucket_as_website(client_connection, resource_connection, module): + + bucket_name = module.params.get("name") + redirect_all_requests = module.params.get("redirect_all_requests") + # If redirect_all_requests is set then don't use the default suffix that has been set + if redirect_all_requests is not None: + suffix = None + else: + suffix = module.params.get("suffix") + error_key = module.params.get("error_key") + changed = False + + try: + bucket_website = resource_connection.BucketWebsite(bucket_name) + except ClientError as e: + module.fail_json(msg=e.message, **camel_dict_to_snake_dict(e.response)) + + try: + website_config = client_connection.get_bucket_website(Bucket=bucket_name) + except ClientError as e: + if e.response['Error']['Code'] == 'NoSuchWebsiteConfiguration': + website_config = None + else: + module.fail_json(msg=e.message, **camel_dict_to_snake_dict(e.response)) + + if website_config is None: + try: + bucket_website.put(WebsiteConfiguration=_create_website_configuration(suffix, error_key, redirect_all_requests)) + changed = True + except (ClientError, ParamValidationError) as e: + module.fail_json(msg=e.message, **camel_dict_to_snake_dict(e.response)) + except ValueError as e: + module.fail_json(msg=str(e)) + else: + try: + if (suffix is not None and website_config['IndexDocument']['Suffix'] != suffix) or \ + (error_key is not None and website_config['ErrorDocument']['Key'] != error_key) or \ + (redirect_all_requests is not None and website_config['RedirectAllRequestsTo'] != _create_redirect_dict(redirect_all_requests)): + + try: + bucket_website.put(WebsiteConfiguration=_create_website_configuration(suffix, error_key, redirect_all_requests)) + changed = True + except (ClientError, ParamValidationError) as e: + module.fail_json(msg=e.message, **camel_dict_to_snake_dict(e.response)) + except KeyError as e: + try: + bucket_website.put(WebsiteConfiguration=_create_website_configuration(suffix, error_key, redirect_all_requests)) + changed = True + except (ClientError, ParamValidationError) as e: + module.fail_json(msg=e.message, **camel_dict_to_snake_dict(e.response)) + except ValueError as e: + module.fail_json(msg=str(e)) + + # Wait 5 secs before getting the website_config again to give it time to update + time.sleep(5) + + website_config = client_connection.get_bucket_website(Bucket=bucket_name) + module.exit_json(changed=changed, **camel_dict_to_snake_dict(website_config)) + + +def disable_bucket_as_website(client_connection, module): + + changed = False + bucket_name = module.params.get("name") + + try: + client_connection.get_bucket_website(Bucket=bucket_name) + except ClientError as e: + if e.response['Error']['Code'] == 'NoSuchWebsiteConfiguration': + module.exit_json(changed=changed) + else: + module.fail_json(msg=e.message, **camel_dict_to_snake_dict(e.response)) + + try: + client_connection.delete_bucket_website(Bucket=bucket_name) + changed = True + except ClientError as e: + module.fail_json(msg=e.message, **camel_dict_to_snake_dict(e.response)) + + module.exit_json(changed=changed) + + +def main(): + + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + name=dict(type='str', required=True), + state=dict(type='str', required=True, choices=['present', 'absent']), + suffix=dict(type='str', required=False, default='index.html'), + error_key=dict(type='str', required=False), + redirect_all_requests=dict(type='str', required=False) + ) + ) + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive = [ + ['redirect_all_requests', 'suffix'], + ['redirect_all_requests', 'error_key'] + ]) + + if not HAS_BOTO3: + module.fail_json(msg='boto3 required for this module') + + region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) + + if region: + client_connection = boto3_conn(module, conn_type='client', resource='s3', region=region, endpoint=ec2_url, **aws_connect_params) + resource_connection = boto3_conn(module, conn_type='resource', resource='s3', region=region, endpoint=ec2_url, **aws_connect_params) + else: + module.fail_json(msg="region must be specified") + + state = module.params.get("state") + + if state == 'present': + enable_or_update_bucket_as_website(client_connection, resource_connection, module) + elif state == 'absent': + disable_bucket_as_website(client_connection, module) + + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From 19107086f4b89ff8c2b77ffa9d1fe98eae9c9b72 Mon Sep 17 00:00:00 2001 From: Angus Williams Date: Tue, 23 Aug 2016 08:26:40 +0100 Subject: [PATCH 1971/2522] bigip_pool_member: fix idempotency with session_state argument (#2745) --- network/f5/bigip_pool_member.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/f5/bigip_pool_member.py b/network/f5/bigip_pool_member.py index 99fc515ede9..25286f870c5 100644 --- a/network/f5/bigip_pool_member.py +++ b/network/f5/bigip_pool_member.py @@ -506,7 +506,7 @@ def main(): if not module.check_mode: set_member_session_enabled_state(api, pool, address, port, session_state) result = {'changed': True} - elif session_state == 'disabled' and session_status != 'force_disabled': + elif session_state == 'disabled' and session_status != 'forced_disabled': if not module.check_mode: set_member_session_enabled_state(api, pool, address, port, session_state) result = {'changed': True} From fa41ccd59b502cb7539bfa6b6946afdc0848329e Mon Sep 17 00:00:00 2001 From: Ethan Devenport Date: Fri, 29 Jul 2016 04:37:42 +0000 Subject: [PATCH 1972/2522] Additional provider features added and fixed some bugs. * Added support for SSH keys, image passwords, SSD disk type, and CPU family. * Adjusted server create so that IP address is returned in response. * Restructured remove server method(s) to handle change status properly, gracefully handle missing servers, and improve overall performance. * Prevent duplicate server names from being provisioned so removals can be handled appropriately. * Fixed a bug in the count increment being a string rather than an integer. * Fixed issue with create_volume returning invalid response. * Fixed type bug in volume instance_ids for volume removal and improved volume management. * Fixed type bug in instance_ids for proper server removal and moved boot volume creation into composite server build request. * General clean up. --- cloud/profitbricks/profitbricks.py | 318 ++++++++++++---------- cloud/profitbricks/profitbricks_volume.py | 59 +++- 2 files changed, 222 insertions(+), 155 deletions(-) diff --git a/cloud/profitbricks/profitbricks.py b/cloud/profitbricks/profitbricks.py index 556c652828e..0a1e299949d 100644 --- a/cloud/profitbricks/profitbricks.py +++ b/cloud/profitbricks/profitbricks.py @@ -31,13 +31,21 @@ description: - The name of the virtual machine. required: true - image: + image: description: - The system image ID for creating the virtual machine, e.g. a3eae284-a2fe-11e4-b187-5f1f641608c8. required: true + image_password: + description: + - Password set for the administrative user. + required: false + ssh_keys: + description: + - Public SSH keys allowing access to the virtual machine. + required: false datacenter: description: - - The Datacenter to provision this virtual machine. + - The datacenter to provision this virtual machine. required: false default: null cores: @@ -50,6 +58,12 @@ - The amount of memory to allocate to the virtual machine. required: false default: 2048 + cpu_family: + description: + - The CPU family type to allocate to the virtual machine. + required: false + default: AMD_OPTERON + choices: [ "AMD_OPTERON", "INTEL_XEON" ] volume_size: description: - The size in GB of the boot volume. @@ -72,7 +86,7 @@ default: 1 location: description: - - The datacenter location. Use only if you want to create the Datacenter or else this value is ignored. + - The datacenter location. Use only if you want to create the Datacenter or else this value is ignored. required: false default: us/las choices: [ "us/las", "us/lasdev", "de/fra", "de/fkb" ] @@ -129,7 +143,7 @@ # Note: These examples do not set authentication details, see the AWS Guide for details. -# Provisioning example. This will create three servers and enumerate their names. +# Provisioning example. This will create three servers and enumerate their names. - profitbricks: datacenter: Tardis One @@ -137,6 +151,7 @@ cores: 4 ram: 2048 volume_size: 50 + cpu_family: INTEL_XEON image: a3eae284-a2fe-11e4-b187-5f1f641608c8 location: us/las count: 3 @@ -218,11 +233,15 @@ def _wait_for_completion(profitbricks, promise, wait_timeout, msg): promise['requestId'] ) + '" to complete.') + def _create_machine(module, profitbricks, datacenter, name): - image = module.params.get('image') cores = module.params.get('cores') ram = module.params.get('ram') + cpu_family = module.params.get('cpu_family') volume_size = module.params.get('volume_size') + disk_type = module.params.get('disk_type') + image_password = module.params.get('image_password') + ssh_keys = module.params.get('ssh_keys') bus = module.params.get('bus') lan = module.params.get('lan') assign_public_ip = module.params.get('assign_public_ip') @@ -234,26 +253,6 @@ def _create_machine(module, profitbricks, datacenter, name): wait = module.params.get('wait') wait_timeout = module.params.get('wait_timeout') - try: - # Generate name, but grab first 10 chars so we don't - # screw up the uuid match routine. - v = Volume( - name=str(uuid.uuid4()).replace('-','')[:10], - size=volume_size, - image=image, - bus=bus) - - volume_response = profitbricks.create_volume( - datacenter_id=datacenter, volume=v) - - # We're forced to wait on the volume creation since - # server create relies upon this existing. - - _wait_for_completion(profitbricks, volume_response, - wait_timeout, "create_volume") - except Exception as e: - module.fail_json(msg="failed to create the new volume: %s" % str(e)) - if assign_public_ip: public_found = False @@ -269,81 +268,64 @@ def _create_machine(module, profitbricks, datacenter, name): public=True) lan_response = profitbricks.create_lan(datacenter, i) - - lan = lan_response['id'] - _wait_for_completion(profitbricks, lan_response, wait_timeout, "_create_machine") + lan = lan_response['id'] - try: - n = NIC( - lan=int(lan) - ) - - nics = [n] + v = Volume( + name=str(uuid.uuid4()).replace('-', '')[:10], + size=volume_size, + image=image, + image_password=image_password, + ssh_keys=ssh_keys, + disk_type=disk_type, + bus=bus) + + n = NIC( + lan=int(lan) + ) - s = Server( - name=name, - ram=ram, - cores=cores, - nics=nics, - boot_volume_id=volume_response['id'] - ) + s = Server( + name=name, + ram=ram, + cores=cores, + cpu_family=cpu_family, + create_volumes=[v], + nics=[n], + ) - server_response = profitbricks.create_server( + try: + create_server_response = profitbricks.create_server( datacenter_id=datacenter, server=s) - if wait: - _wait_for_completion(profitbricks, server_response, - wait_timeout, "create_virtual_machine") - + _wait_for_completion(profitbricks, create_server_response, + wait_timeout, "create_virtual_machine") - return (server_response) + server_response = profitbricks.get_server( + datacenter_id=datacenter, + server_id=create_server_response['id'], + depth=3 + ) except Exception as e: module.fail_json(msg="failed to create the new server: %s" % str(e)) + else: + return server_response -def _remove_machine(module, profitbricks, datacenter, name): - remove_boot_volume = module.params.get('remove_boot_volume') - wait = module.params.get('wait') - wait_timeout = module.params.get('wait_timeout') - changed = False - - # User provided the actual UUID instead of the name. - try: - if remove_boot_volume: - # Collect information needed for later. - server = profitbricks.get_server(datacenter, name) - volume_id = server['properties']['bootVolume']['href'].split('/')[7] - - server_response = profitbricks.delete_server(datacenter, name) - changed = True - - except Exception as e: - module.fail_json(msg="failed to terminate the virtual server: %s" % str(e)) - - # Remove the bootVolume - if remove_boot_volume: - try: - volume_response = profitbricks.delete_volume(datacenter, volume_id) - - except Exception as e: - module.fail_json(msg="failed to remove the virtual server's bootvolume: %s" % str(e)) - - return changed -def _startstop_machine(module, profitbricks, datacenter, name): +def _startstop_machine(module, profitbricks, datacenter_id, server_id): state = module.params.get('state') try: if state == 'running': - profitbricks.start_server(datacenter, name) + profitbricks.start_server(datacenter_id, server_id) else: - profitbricks.stop_server(datacenter, name) + profitbricks.stop_server(datacenter_id, server_id) return True except Exception as e: module.fail_json(msg="failed to start or stop the virtual machine %s: %s" % (name, str(e))) + def _create_datacenter(module, profitbricks): datacenter = module.params.get('datacenter') location = module.params.get('location') @@ -364,6 +346,7 @@ def _create_datacenter(module, profitbricks): except Exception as e: module.fail_json(msg="failed to create the new server(s): %s" % str(e)) + def create_virtual_machine(module, profitbricks): """ Create new virtual machine @@ -386,19 +369,15 @@ def create_virtual_machine(module, profitbricks): virtual_machines = [] virtual_machine_ids = [] - # Locate UUID for Datacenter - if not (uuid_match.match(datacenter)): - datacenter_list = profitbricks.list_datacenters() - for d in datacenter_list['items']: - dc = profitbricks.get_datacenter(d['id']) - if datacenter == dc['properties']['name']: - datacenter = d['id'] - datacenter_found = True - break + # Locate UUID for datacenter if referenced by name. + datacenter_list = profitbricks.list_datacenters() + datacenter_id = _get_datacenter_id(datacenter_list, datacenter) + if datacenter_id: + datacenter_found = True if not datacenter_found: datacenter_response = _create_datacenter(module, profitbricks) - datacenter = datacenter_response['id'] + datacenter_id = datacenter_response['id'] _wait_for_completion(profitbricks, datacenter_response, wait_timeout, "create_virtual_machine") @@ -415,24 +394,31 @@ def create_virtual_machine(module, profitbricks): else: module.fail_json(msg=e.message) - number_range = xrange(count_offset,count_offset + count + len(numbers)) + number_range = xrange(count_offset, count_offset + count + len(numbers)) available_numbers = list(set(number_range).difference(numbers)) names = [] numbers_to_use = available_numbers[:count] for number in numbers_to_use: names.append(name % number) else: - names = [name] * count + names = [name] - for name in names: - create_response = _create_machine(module, profitbricks, str(datacenter), name) - nics = profitbricks.list_nics(datacenter,create_response['id']) + # Prefetch a list of servers for later comparison. + server_list = profitbricks.list_servers(datacenter_id) + for name in names: + # Skip server creation if the server already exists. + if _get_server_id(server_list, name): + continue + + create_response = _create_machine(module, profitbricks, str(datacenter_id), name) + nics = profitbricks.list_nics(datacenter_id, create_response['id']) for n in nics['items']: if lan == n['properties']['lan']: - create_response.update({ 'public_ip': n['properties']['ips'][0] }) + create_response.update({'public_ip': n['properties']['ips'][0]}) virtual_machines.append(create_response) - failed = False + + failed = False results = { 'failed': failed, @@ -445,9 +431,10 @@ def create_virtual_machine(module, profitbricks): return results + def remove_virtual_machine(module, profitbricks): """ - Removes a virtual machine. + Removes a virtual machine. This will remove the virtual machine along with the bootVolume. @@ -459,36 +446,56 @@ def remove_virtual_machine(module, profitbricks): Returns: True if a new virtual server was deleted, false otherwise """ + datacenter = module.params.get('datacenter') + instance_ids = module.params.get('instance_ids') + remove_boot_volume = module.params.get('remove_boot_volume') + changed = False + if not isinstance(module.params.get('instance_ids'), list) or len(module.params.get('instance_ids')) < 1: module.fail_json(msg='instance_ids should be a list of virtual machine ids or names, aborting') - datacenter = module.params.get('datacenter') - instance_ids = module.params.get('instance_ids') + # Locate UUID for datacenter if referenced by name. + datacenter_list = profitbricks.list_datacenters() + datacenter_id = _get_datacenter_id(datacenter_list, datacenter) + if not datacenter_id: + module.fail_json(msg='Virtual data center \'%s\' not found.' % str(datacenter)) + + # Prefetch server list for later comparison. + server_list = profitbricks.list_servers(datacenter_id) + for instance in instance_ids: + # Locate UUID for server if referenced by name. + server_id = _get_server_id(server_list, instance) + if server_id: + # Remove the server's boot volume + if remove_boot_volume: + _remove_boot_volume(module, profitbricks, datacenter_id, server_id) + + # Remove the server + try: + server_response = profitbricks.delete_server(datacenter_id, server_id) + except Exception as e: + module.fail_json(msg="failed to terminate the virtual server: %s" % str(e)) + else: + changed = True - # Locate UUID for Datacenter - if not (uuid_match.match(datacenter)): - datacenter_list = profitbricks.list_datacenters() - for d in datacenter_list['items']: - dc = profitbricks.get_datacenter(d['id']) - if datacenter == dc['properties']['name']: - datacenter = d['id'] - break + return changed - for n in instance_ids: - if(uuid_match.match(n)): - _remove_machine(module, profitbricks, d['id'], n) - else: - servers = profitbricks.list_servers(d['id']) - for s in servers['items']: - if n == s['properties']['name']: - server_id = s['id'] +def _remove_boot_volume(module, profitbricks, datacenter_id, server_id): + """ + Remove the boot volume from the server + """ + try: + server = profitbricks.get_server(datacenter_id, server_id) + volume_id = server['properties']['bootVolume']['id'] + volume_response = profitbricks.delete_volume(datacenter_id, volume_id) + except Exception as e: + module.fail_json(msg="failed to remove the server's boot volume: %s" % str(e)) - _remove_machine(module, profitbricks, datacenter, server_id) def startstop_machine(module, profitbricks, state): """ - Starts or Stops a virtual machine. + Starts or Stops a virtual machine. module : AnsibleModule object profitbricks: authenticated profitbricks object. @@ -506,41 +513,32 @@ def startstop_machine(module, profitbricks, state): datacenter = module.params.get('datacenter') instance_ids = module.params.get('instance_ids') - # Locate UUID for Datacenter - if not (uuid_match.match(datacenter)): - datacenter_list = profitbricks.list_datacenters() - for d in datacenter_list['items']: - dc = profitbricks.get_datacenter(d['id']) - if datacenter == dc['properties']['name']: - datacenter = d['id'] - break - - for n in instance_ids: - if(uuid_match.match(n)): - _startstop_machine(module, profitbricks, datacenter, n) - + # Locate UUID for datacenter if referenced by name. + datacenter_list = profitbricks.list_datacenters() + datacenter_id = _get_datacenter_id(datacenter_list, datacenter) + if not datacenter_id: + module.fail_json(msg='Virtual data center \'%s\' not found.' % str(datacenter)) + + # Prefetch server list for later comparison. + server_list = profitbricks.list_servers(datacenter_id) + for instance in instance_ids: + # Locate UUID of server if referenced by name. + server_id = _get_server_id(server_list, instance) + if server_id: + _startstop_machine(module, profitbricks, datacenter_id, server_id) changed = True - else: - servers = profitbricks.list_servers(d['id']) - - for s in servers['items']: - if n == s['properties']['name']: - server_id = s['id'] - _startstop_machine(module, profitbricks, datacenter, server_id) - - changed = True if wait: wait_timeout = time.time() + wait_timeout while wait_timeout > time.time(): matched_instances = [] - for res in profitbricks.list_servers(datacenter)['items']: + for res in profitbricks.list_servers(datacenter_id)['items']: if state == 'running': if res['properties']['vmState'].lower() == state: matched_instances.append(res) elif state == 'stopped': if res['properties']['vmState'].lower() == 'shutoff': - matched_instances.append(res) + matched_instances.append(res) if len(matched_instances) < len(instance_ids): time.sleep(5) @@ -549,10 +547,31 @@ def startstop_machine(module, profitbricks, state): if wait_timeout <= time.time(): # waiting took too long - module.fail_json(msg = "wait for virtual machine state timeout on %s" % time.asctime()) + module.fail_json(msg="wait for virtual machine state timeout on %s" % time.asctime()) return (changed) + +def _get_datacenter_id(datacenters, identity): + """ + Fetch and return datacenter UUID by datacenter name if found. + """ + for datacenter in datacenters['items']: + if identity in (datacenter['properties']['name'], datacenter['id']): + return datacenter['id'] + return None + + +def _get_server_id(servers, identity): + """ + Fetch and return server UUID by server name if found. + """ + for server in servers['items']: + if identity in (server['properties']['name'], server['id']): + return server['id'] + return None + + def main(): module = AnsibleModule( argument_spec=dict( @@ -561,12 +580,16 @@ def main(): image=dict(), cores=dict(default=2), ram=dict(default=2048), + cpu_family=dict(default='AMD_OPTERON'), volume_size=dict(default=10), + disk_type=dict(default='HDD'), + image_password=dict(default=None), + ssh_keys=dict(type='list', default=[]), bus=dict(default='VIRTIO'), lan=dict(default=1), - count=dict(default=1), + count=dict(type='int', default=1), auto_increment=dict(type='bool', default=True), - instance_ids=dict(), + instance_ids=dict(type='list', default=[]), subscription_user=dict(), subscription_password=dict(), location=dict(choices=LOCATIONS, default='us/las'), @@ -594,7 +617,7 @@ def main(): if state == 'absent': if not module.params.get('datacenter'): - module.fail_json(msg='datacenter parameter is required ' + + module.fail_json(msg='datacenter parameter is required ' + 'for running or stopping machines.') try: @@ -605,7 +628,7 @@ def main(): elif state in ('running', 'stopped'): if not module.params.get('datacenter'): - module.fail_json(msg='datacenter parameter is required for ' + + module.fail_json(msg='datacenter parameter is required for ' + 'running or stopping machines.') try: (changed) = startstop_machine(module, profitbricks, state) @@ -619,10 +642,10 @@ def main(): if not module.params.get('image'): module.fail_json(msg='image parameter is required for new instance') if not module.params.get('subscription_user'): - module.fail_json(msg='subscription_user parameter is ' + + module.fail_json(msg='subscription_user parameter is ' + 'required for new instance') if not module.params.get('subscription_password'): - module.fail_json(msg='subscription_password parameter is ' + + module.fail_json(msg='subscription_password parameter is ' + 'required for new instance') try: @@ -634,4 +657,3 @@ def main(): from ansible.module_utils.basic import * main() - diff --git a/cloud/profitbricks/profitbricks_volume.py b/cloud/profitbricks/profitbricks_volume.py index 89a69d5e61a..802511cc930 100644 --- a/cloud/profitbricks/profitbricks_volume.py +++ b/cloud/profitbricks/profitbricks_volume.py @@ -45,11 +45,20 @@ description: - The system image ID for the volume, e.g. a3eae284-a2fe-11e4-b187-5f1f641608c8. This can also be a snapshot image ID. required: true + image_password: + description: + - Password set for the administrative user. + required: false + ssh_keys: + description: + - Public SSH keys allowing access to the virtual machine. + required: false disk_type: description: - - The disk type. Currently only HDD. + - The disk type of the volume. required: false default: HDD + choices: [ "HDD", "SSD" ] licence_type: description: - The licence type for the volume. This is used when the image is non-standard. @@ -163,6 +172,8 @@ def _create_volume(module, profitbricks, datacenter, name): size = module.params.get('size') bus = module.params.get('bus') image = module.params.get('image') + image_password = module.params.get('image_password') + ssh_keys = module.params.get('ssh_keys') disk_type = module.params.get('disk_type') licence_type = module.params.get('licence_type') wait_timeout = module.params.get('wait_timeout') @@ -174,6 +185,8 @@ def _create_volume(module, profitbricks, datacenter, name): size=size, bus=bus, image=image, + image_password=image_password, + ssh_keys=ssh_keys, disk_type=disk_type, licence_type=licence_type ) @@ -250,9 +263,10 @@ def create_volume(module, profitbricks): else: names = [name] * count - for name in names: + for name in names: create_response = _create_volume(module, profitbricks, str(datacenter), name) volumes.append(create_response) + _attach_volume(module, profitbricks, datacenter, create_response['id']) failed = False results = { @@ -308,19 +322,50 @@ def delete_volume(module, profitbricks): return changed +def _attach_volume(module, profitbricks, datacenter, volume): + """ + Attaches a volume. + + This will attach a volume to the server. + + module : AnsibleModule object + profitbricks: authenticated profitbricks object. + + Returns: + True if the volume was attached, false otherwise + """ + server = module.params.get('server') + + # Locate UUID for Server + if server: + if not (uuid_match.match(server)): + server_list = profitbricks.list_servers(datacenter) + for s in server_list['items']: + if server == s['properties']['name']: + server= s['id'] + break + + try: + return profitbricks.attach_volume(datacenter, server, volume) + except Exception as e: + module.fail_json(msg='failed to attach volume: %s' % str(e)) + def main(): module = AnsibleModule( argument_spec=dict( datacenter=dict(), + server=dict(), name=dict(), size=dict(default=10), bus=dict(default='VIRTIO'), image=dict(), + image_password=dict(default=None), + ssh_keys=dict(type='list', default=[]), disk_type=dict(default='HDD'), licence_type=dict(default='UNKNOWN'), - count=dict(default=1), + count=dict(type='int', default=1), auto_increment=dict(type='bool', default=True), - instance_ids=dict(), + instance_ids=dict(type='list', default=[]), subscription_user=dict(), subscription_password=dict(), wait=dict(type='bool', default=True), @@ -360,11 +405,11 @@ def main(): module.fail_json(msg='name parameter is required for new instance') try: - (failed, volume_dict_array) = create_volume(module, profitbricks) - module.exit_json(failed=failed, volumes=volume_dict_array) + (volume_dict_array) = create_volume(module, profitbricks) + module.exit_json(**volume_dict_array) except Exception as e: module.fail_json(msg='failed to set volume state: %s' % str(e)) from ansible.module_utils.basic import * -main() \ No newline at end of file +main() From 727eaa219d7c61797adb04c9938835b82696e73c Mon Sep 17 00:00:00 2001 From: Ethan Devenport Date: Tue, 23 Aug 2016 07:52:39 +0000 Subject: [PATCH 1973/2522] Removed us/lasdev datacenter which the cloud provider no longer maintains. --- cloud/profitbricks/profitbricks.py | 5 ++--- cloud/profitbricks/profitbricks_datacenter.py | 9 ++++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/cloud/profitbricks/profitbricks.py b/cloud/profitbricks/profitbricks.py index 0a1e299949d..e01d74777d4 100644 --- a/cloud/profitbricks/profitbricks.py +++ b/cloud/profitbricks/profitbricks.py @@ -89,7 +89,7 @@ - The datacenter location. Use only if you want to create the Datacenter or else this value is ignored. required: false default: us/las - choices: [ "us/las", "us/lasdev", "de/fra", "de/fkb" ] + choices: [ "us/las", "de/fra", "de/fkb" ] assign_public_ip: description: - This will assign the machine to the public LAN. If no LAN exists with public Internet access it is created. @@ -205,8 +205,7 @@ LOCATIONS = ['us/las', 'de/fra', - 'de/fkb', - 'us/lasdev'] + 'de/fkb'] uuid_match = re.compile( '[\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12}', re.I) diff --git a/cloud/profitbricks/profitbricks_datacenter.py b/cloud/profitbricks/profitbricks_datacenter.py index cd0e38ee383..0b21d3e4cd6 100644 --- a/cloud/profitbricks/profitbricks_datacenter.py +++ b/cloud/profitbricks/profitbricks_datacenter.py @@ -35,7 +35,7 @@ - The datacenter location. required: false default: us/las - choices: [ "us/las", "us/lasdev", "de/fra", "de/fkb" ] + choices: [ "us/las", "de/fra", "de/fkb" ] subscription_user: description: - The ProfitBricks username. Overrides the PB_SUBSCRIPTION_ID environement variable. @@ -94,8 +94,7 @@ LOCATIONS = ['us/las', 'de/fra', - 'de/fkb', - 'us/lasdev'] + 'de/fkb'] uuid_match = re.compile( '[\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12}', re.I) @@ -206,7 +205,7 @@ def main(): argument_spec=dict( name=dict(), description=dict(), - location=dict(choices=LOCATIONS, default='us/lasdev'), + location=dict(choices=LOCATIONS, default='us/las'), subscription_user=dict(), subscription_password=dict(), wait=dict(type='bool', default=True), @@ -256,4 +255,4 @@ def main(): from ansible.module_utils.basic import * -main() \ No newline at end of file +main() From 1631c717ff9cd0ae67c919d36400e39cd8e9140b Mon Sep 17 00:00:00 2001 From: Ethan Devenport Date: Tue, 23 Aug 2016 08:00:41 +0000 Subject: [PATCH 1974/2522] Included version_added for new options. --- cloud/profitbricks/profitbricks.py | 3 +++ cloud/profitbricks/profitbricks_volume.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/cloud/profitbricks/profitbricks.py b/cloud/profitbricks/profitbricks.py index e01d74777d4..b0791f37e3a 100644 --- a/cloud/profitbricks/profitbricks.py +++ b/cloud/profitbricks/profitbricks.py @@ -39,10 +39,12 @@ description: - Password set for the administrative user. required: false + version_added: '2.2' ssh_keys: description: - Public SSH keys allowing access to the virtual machine. required: false + version_added: '2.2' datacenter: description: - The datacenter to provision this virtual machine. @@ -64,6 +66,7 @@ required: false default: AMD_OPTERON choices: [ "AMD_OPTERON", "INTEL_XEON" ] + version_added: '2.2' volume_size: description: - The size in GB of the boot volume. diff --git a/cloud/profitbricks/profitbricks_volume.py b/cloud/profitbricks/profitbricks_volume.py index 802511cc930..6b7877f31cc 100644 --- a/cloud/profitbricks/profitbricks_volume.py +++ b/cloud/profitbricks/profitbricks_volume.py @@ -49,10 +49,12 @@ description: - Password set for the administrative user. required: false + version_added: '2.2' ssh_keys: description: - Public SSH keys allowing access to the virtual machine. required: false + version_added: '2.2' disk_type: description: - The disk type of the volume. From 8485b39ab305fe6073313a13ea547a0c7462415b Mon Sep 17 00:00:00 2001 From: ovcharenko Date: Tue, 23 Aug 2016 11:14:22 +0300 Subject: [PATCH 1975/2522] [FIX] ufw fails asking for a direction for rules without an interface specified [#2758] (#2759) --- system/ufw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/ufw.py b/system/ufw.py index e88c1456975..c692211d124 100644 --- a/system/ufw.py +++ b/system/ufw.py @@ -228,7 +228,7 @@ def execute(cmd): if len(commands) < 1: module.fail_json(msg="Not any of the command arguments %s given" % commands) - if('interface' in params and params['direction'] is None): + if(params['interface'] is not None and params['direction'] is None): module.fail_json(msg="Direction must be specified when creating a rule on an interface") # Ensure ufw is available From 0749ce6faa6af15027934f13d75fa3f351867679 Mon Sep 17 00:00:00 2001 From: Kevin Carter Date: Tue, 23 Aug 2016 03:19:17 -0500 Subject: [PATCH 1976/2522] Remove the subsystem lock loop (#2647) This change removes the loop watching for "/var/lock/subsys/lxc" from the lxc-container module. This change simply runs the command within a container using the lxc CLI tools which should be responcible for locking and unlocking on their own. Closes-Issue: https://github.com/ansible/ansible-modules-extras/issues/690 Signed-off-by: Kevin Carter --- cloud/lxc/lxc_container.py | 32 +++++--------------------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index 906b1d754c6..22c72f43447 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -682,45 +682,23 @@ def _get_vars(self, variables): else: return return_dict - def _run_command(self, build_command, unsafe_shell=False, timeout=600): + def _run_command(self, build_command, unsafe_shell=False): """Return information from running an Ansible Command. This will squash the build command list into a string and then execute the command via Ansible. The output is returned to the method. This output is returned as `return_code`, `stdout`, `stderr`. - Prior to running the command the method will look to see if the LXC - lockfile is present. If the lockfile "/var/lock/subsys/lxc" the method - will wait upto 10 minutes for it to be gone; polling every 5 seconds. - :param build_command: Used for the command and all options. :type build_command: ``list`` :param unsafe_shell: Enable or Disable unsafe sell commands. :type unsafe_shell: ``bol`` - :param timeout: Time before the container create process quites. - :type timeout: ``int`` """ - lockfile = '/var/lock/subsys/lxc' - - for _ in xrange(timeout): - if os.path.exists(lockfile): - time.sleep(1) - else: - return self.module.run_command( - ' '.join(build_command), - use_unsafe_shell=unsafe_shell - ) - else: - message = ( - 'The LXC subsystem is locked and after 5 minutes it never' - ' became unlocked. Lockfile [ %s ]' % lockfile - ) - self.failure( - error='LXC subsystem locked', - rc=0, - msg=message - ) + return self.module.run_command( + ' '.join(build_command), + use_unsafe_shell=unsafe_shell + ) def _config(self): """Configure an LXC container. From d05c96f92a8c8391cc866e06713917014902db55 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 23 Aug 2016 10:53:27 -0400 Subject: [PATCH 1977/2522] renamed bmc to remote_management to clarify --- {bmc => remote_management}/__init__.py | 0 {bmc => remote_management}/ipmi/__init__.py | 0 {bmc => remote_management}/ipmi/ipmi_boot.py | 0 {bmc => remote_management}/ipmi/ipmi_power.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename {bmc => remote_management}/__init__.py (100%) rename {bmc => remote_management}/ipmi/__init__.py (100%) rename {bmc => remote_management}/ipmi/ipmi_boot.py (100%) rename {bmc => remote_management}/ipmi/ipmi_power.py (100%) diff --git a/bmc/__init__.py b/remote_management/__init__.py similarity index 100% rename from bmc/__init__.py rename to remote_management/__init__.py diff --git a/bmc/ipmi/__init__.py b/remote_management/ipmi/__init__.py similarity index 100% rename from bmc/ipmi/__init__.py rename to remote_management/ipmi/__init__.py diff --git a/bmc/ipmi/ipmi_boot.py b/remote_management/ipmi/ipmi_boot.py similarity index 100% rename from bmc/ipmi/ipmi_boot.py rename to remote_management/ipmi/ipmi_boot.py diff --git a/bmc/ipmi/ipmi_power.py b/remote_management/ipmi/ipmi_power.py similarity index 100% rename from bmc/ipmi/ipmi_power.py rename to remote_management/ipmi/ipmi_power.py From 82771ed7550677be39ffbacf09fddd680ab2124b Mon Sep 17 00:00:00 2001 From: Adrian Likins Date: Tue, 23 Aug 2016 13:08:42 -0400 Subject: [PATCH 1978/2522] Add a 'requirements:' field to cloudflare_dns doc (#2631) cloudflare_dns.py makes use of the python 2.6 features (the string .format() method). --- network/cloudflare_dns.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/network/cloudflare_dns.py b/network/cloudflare_dns.py index e8a1eab8b6f..0756d5d8ffc 100644 --- a/network/cloudflare_dns.py +++ b/network/cloudflare_dns.py @@ -32,6 +32,8 @@ --- module: cloudflare_dns author: "Michael Gruener (@mgruener)" +requirements: + - "python >= 2.6" version_added: "2.1" short_description: manage Cloudflare DNS records description: From 8e9ac4a04d5bd345edfc89e9f1e7dc7dd59a998e Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 23 Aug 2016 14:43:24 -0400 Subject: [PATCH 1979/2522] updated blacklist --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 82142db2d45..1cd8b9b5d8c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -117,7 +117,7 @@ install: - pip install git+https://github.com/ansible/ansible.git@devel#egg=ansible - pip install git+https://github.com/sivel/ansible-testing.git#egg=ansible_testing script: - - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py|database/influxdb/influxdb.*\.py|database/mssql/mssql_db\.py|/letsencrypt\.py|network/f5/bigip.*\.py|bmc/ipmi/.*\.py|system/atomic_.*\.py' . + - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py|database/influxdb/influxdb.*\.py|database/mssql/mssql_db\.py|/letsencrypt\.py|network/f5/bigip.*\.py|remote_management/ipmi/.*\.py|system/atomic_.*\.py' . - python2.6 -m compileall -fq . - python2.7 -m compileall -fq . - python3.4 -m compileall -fq . -x $(echo "$PY3_EXCLUDE_LIST"| tr ' ' '|') From 5399deff0b0d5a98490a267d9c971548494bc75a Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Tue, 23 Aug 2016 10:08:41 -0700 Subject: [PATCH 1980/2522] Adds documentation fragment to bigip modules This patch removes the common documentation bits and replaces them with a doc fragment that already exists in core --- network/f5/bigip_facts.py | 39 +------------------------- network/f5/bigip_gtm_virtual_server.py | 18 +----------- network/f5/bigip_gtm_wide_ip.py | 26 +---------------- network/f5/bigip_monitor_http.py | 33 +--------------------- network/f5/bigip_monitor_tcp.py | 33 +--------------------- network/f5/bigip_node.py | 37 +----------------------- network/f5/bigip_pool.py | 39 +------------------------- network/f5/bigip_pool_member.py | 30 +------------------- network/f5/bigip_virtual_server.py | 28 +----------------- 9 files changed, 9 insertions(+), 274 deletions(-) diff --git a/network/f5/bigip_facts.py b/network/f5/bigip_facts.py index eaa4bf85957..970bb08d917 100644 --- a/network/f5/bigip_facts.py +++ b/network/f5/bigip_facts.py @@ -36,44 +36,6 @@ requirements: - bigsuds options: - server: - description: - - BIG-IP host - required: true - default: null - choices: [] - aliases: [] - server_port: - description: - - BIG-IP server port - required: false - default: 443 - version_added: "2.2" - user: - description: - - BIG-IP username - required: true - default: null - choices: [] - aliases: [] - password: - description: - - BIG-IP password - required: true - default: null - choices: [] - aliases: [] - validate_certs: - description: - - If C(no), SSL certificates will not be validated. This should only be used - on personally controlled sites. Prior to 2.0, this module would always - validate on python >= 2.7.9 and never validate on python <= 2.7.8 - required: false - default: yes - choices: - - yes - - no - version_added: 2.0 session: description: - BIG-IP session support; may be useful to avoid concurrency @@ -115,6 +77,7 @@ default: null choices: [] aliases: [] +extends_documentation_fragment: f5 ''' EXAMPLES = ''' diff --git a/network/f5/bigip_gtm_virtual_server.py b/network/f5/bigip_gtm_virtual_server.py index 8d0657f25f6..44da600d0af 100644 --- a/network/f5/bigip_gtm_virtual_server.py +++ b/network/f5/bigip_gtm_virtual_server.py @@ -37,23 +37,6 @@ requirements: - bigsuds options: - server: - description: - - BIG-IP host - required: true - server_port: - description: - - BIG-IP server port - required: false - default: 443 - user: - description: - - BIG-IP username - required: true - password: - description: - - BIG-IP password - required: true state: description: - Virtual server state @@ -79,6 +62,7 @@ - Virtual server port required: false default: None +extends_documentation_fragment: f5 ''' EXAMPLES = ''' diff --git a/network/f5/bigip_gtm_wide_ip.py b/network/f5/bigip_gtm_wide_ip.py index f03a0416e3e..d9e5fdc05c3 100644 --- a/network/f5/bigip_gtm_wide_ip.py +++ b/network/f5/bigip_gtm_wide_ip.py @@ -37,24 +37,6 @@ requirements: - bigsuds options: - server: - description: - - BIG-IP host - required: true - server_port: - description: - - BIG-IP server port - required: false - default: 443 - version_added: "2.2" - user: - description: - - BIG-IP username - required: true - password: - description: - - BIG-IP password - required: true lb_method: description: - LB method of wide ip @@ -64,17 +46,11 @@ 'vs_capacity', 'least_conn', 'lowest_rtt', 'lowest_hops', 'packet_rate', 'cpu', 'hit_ratio', 'qos', 'bps', 'drop_packet', 'explicit_ip', 'connection_rate', 'vs_score'] - validate_certs: - description: - - If C(no), SSL certificates will not be validated. This should only be - used on personally controlled sites using self-signed certificates. - required: false - default: true - version_added: "2.2" wide_ip: description: - Wide IP name required: true +extends_documentation_fragment: f5 ''' EXAMPLES = ''' diff --git a/network/f5/bigip_monitor_http.py b/network/f5/bigip_monitor_http.py index 86096e95a2c..3c303c3ce51 100644 --- a/network/f5/bigip_monitor_http.py +++ b/network/f5/bigip_monitor_http.py @@ -38,38 +38,6 @@ requirements: - bigsuds options: - server: - description: - - BIG-IP host - required: true - default: null - server_port: - description: - - BIG-IP server port - required: false - default: 443 - version_added: "2.2" - user: - description: - - BIG-IP username - required: true - default: null - password: - description: - - BIG-IP password - required: true - default: null - validate_certs: - description: - - If C(no), SSL certificates will not be validated. This should only be used - on personally controlled sites. Prior to 2.0, this module would always - validate on python >= 2.7.9 and never validate on python <= 2.7.8 - required: false - default: 'yes' - choices: - - yes - - no - version_added: 2.0 state: description: - Monitor state @@ -153,6 +121,7 @@ from the node. The default API setting is 0. required: false default: none +extends_documentation_fragment: f5 ''' EXAMPLES = ''' diff --git a/network/f5/bigip_monitor_tcp.py b/network/f5/bigip_monitor_tcp.py index f7443e2878e..45756b1ba28 100644 --- a/network/f5/bigip_monitor_tcp.py +++ b/network/f5/bigip_monitor_tcp.py @@ -36,38 +36,6 @@ requirements: - bigsuds options: - server: - description: - - BIG-IP host - required: true - default: null - server_port: - description: - - BIG-IP server port - required: false - default: 443 - version_added: "2.2" - user: - description: - - BIG-IP username - required: true - default: null - password: - description: - - BIG-IP password - required: true - default: null - validate_certs: - description: - - If C(no), SSL certificates will not be validated. This should only be used - on personally controlled sites. Prior to 2.0, this module would always - validate on python >= 2.7.9 and never validate on python <= 2.7.8 - required: false - default: 'yes' - choices: - - yes - - no - version_added: 2.0 state: description: - Monitor state @@ -159,6 +127,7 @@ from the node. The default API setting is 0. required: false default: none +extends_documentation_fragment: f5 ''' EXAMPLES = ''' diff --git a/network/f5/bigip_node.py b/network/f5/bigip_node.py index 5f3a33f7838..53a0b1973f2 100644 --- a/network/f5/bigip_node.py +++ b/network/f5/bigip_node.py @@ -35,42 +35,6 @@ requirements: - bigsuds options: - server: - description: - - BIG-IP host - required: true - default: null - choices: [] - aliases: [] - server_port: - description: - - BIG-IP server port - required: false - default: 443 - version_added: "2.2" - user: - description: - - BIG-IP username - required: true - default: null - choices: [] - aliases: [] - password: - description: - - BIG-IP password - required: true - default: null - choices: [] - aliases: [] - validate_certs: - description: - - If C(no), SSL certificates will not be validated. This should only be used - on personally controlled sites. Prior to 2.0, this module would always - validate on python >= 2.7.9 and never validate on python <= 2.7.8 - required: false - default: 'yes' - choices: ['yes', 'no'] - version_added: 2.0 state: description: - Pool member state @@ -144,6 +108,7 @@ required: false default: null choices: [] +extends_documentation_fragment: f5 ''' EXAMPLES = ''' diff --git a/network/f5/bigip_pool.py b/network/f5/bigip_pool.py index 3966742fd3e..69ee1c07508 100644 --- a/network/f5/bigip_pool.py +++ b/network/f5/bigip_pool.py @@ -35,44 +35,6 @@ requirements: - bigsuds options: - server: - description: - - BIG-IP host - required: true - default: null - choices: [] - aliases: [] - server_port: - description: - - BIG-IP server port - required: false - default: 443 - version_added: "2.2" - user: - description: - - BIG-IP username - required: true - default: null - choices: [] - aliases: [] - password: - description: - - BIG-IP password - required: true - default: null - choices: [] - aliases: [] - validate_certs: - description: - - If C(no), SSL certificates will not be validated. This should only be used - on personally controlled sites. Prior to 2.0, this module would always - validate on python >= 2.7.9 and never validate on python <= 2.7.8 - required: false - default: 'yes' - choices: - - yes - - no - version_added: 2.0 state: description: - Pool/pool member state @@ -194,6 +156,7 @@ default: null choices: [] aliases: [] +extends_documentation_fragment: f5 ''' EXAMPLES = ''' diff --git a/network/f5/bigip_pool_member.py b/network/f5/bigip_pool_member.py index 25286f870c5..f93ac271ec0 100644 --- a/network/f5/bigip_pool_member.py +++ b/network/f5/bigip_pool_member.py @@ -36,35 +36,6 @@ requirements: - bigsuds options: - server: - description: - - BIG-IP host - required: true - server_port: - description: - - BIG-IP server port - required: false - default: 443 - version_added: "2.2" - user: - description: - - BIG-IP username - required: true - password: - description: - - BIG-IP password - required: true - validate_certs: - description: - - If C(no), SSL certificates will not be validated. This should only be used - on personally controlled sites. Prior to 2.0, this module would always - validate on python >= 2.7.9 and never validate on python <= 2.7.8 - required: false - default: 'yes' - choices: - - yes - - no - version_added: 2.0 state: description: - Pool member state @@ -145,6 +116,7 @@ - yes - no version_added: 2.1 +extends_documentation_fragment: f5 ''' EXAMPLES = ''' diff --git a/network/f5/bigip_virtual_server.py b/network/f5/bigip_virtual_server.py index 93e13c2e77d..ce4c1745360 100644 --- a/network/f5/bigip_virtual_server.py +++ b/network/f5/bigip_virtual_server.py @@ -35,33 +35,6 @@ requirements: - bigsuds options: - server: - description: - - BIG-IP host - required: true - server_port: - description: - - BIG-IP server port - required: false - default: 443 - version_added: "2.2" - user: - description: - - BIG-IP username - required: true - password: - description: - - BIG-IP password - required: true - validate_certs: - description: - - If C(no), SSL certificates will not be validated. This should only be used - on personally controlled sites using self-signed certificates. - required: false - default: 'yes' - choices: - - yes - - no state: description: - Virtual Server state @@ -133,6 +106,7 @@ - Virtual server description required: false default: None +extends_documentation_fragment: f5 ''' EXAMPLES = ''' From fc91c93108a20ba5da74c79d6d4d31287455300a Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Tue, 23 Aug 2016 12:26:23 -0700 Subject: [PATCH 1981/2522] This module can be used as part of the bootstrapping of a BIG-IP. It allows one to configure the various DNS settings that are part of a BIG-IP. Tests for this module can be found here https://github.com/F5Networks/f5-ansible/blob/master/roles/__bigip_device_dns/tasks/main.yaml Platforms this was tested on are 11.6.0 12.0.0 12.1.0 12.1.0 HF1 --- network/f5/bigip_device_dns.py | 397 +++++++++++++++++++++++++++++++++ 1 file changed, 397 insertions(+) create mode 100644 network/f5/bigip_device_dns.py diff --git a/network/f5/bigip_device_dns.py b/network/f5/bigip_device_dns.py new file mode 100644 index 00000000000..c0625abd24a --- /dev/null +++ b/network/f5/bigip_device_dns.py @@ -0,0 +1,397 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: bigip_device_dns +short_description: Manage BIG-IP device DNS settings +description: + - Manage BIG-IP device DNS settings +version_added: "2.2" +options: + cache: + description: + - Specifies whether the system caches DNS lookups or performs the + operation each time a lookup is needed. Please note that this applies + only to Access Policy Manager features, such as ACLs, web application + rewrites, and authentication. + required: false + default: disable + choices: + - enable + - disable + name_servers: + description: + - A list of name serverz that the system uses to validate DNS lookups + forwarders: + description: + - A list of BIND servers that the system can use to perform DNS lookups + search: + description: + - A list of domains that the system searches for local domain lookups, + to resolve local host names. + ip_version: + description: + - Specifies whether the DNS specifies IP addresses using IPv4 or IPv6. + required: false + choices: + - 4 + - 6 + state: + description: + - The state of the variable on the system. When C(present), guarantees + that an existing variable is set to C(value). + required: false + default: present + choices: + - absent + - present +notes: + - Requires the f5-sdk Python package on the host. This is as easy as pip + install requests +extends_documentation_fragment: f5 +requirements: + - f5-sdk +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = ''' +- name: Set the DNS settings on the BIG-IP + bigip_device_dns: + name_servers: + - 208.67.222.222 + - 208.67.220.220 + search: + - localdomain + - lab.local + state: present + password: "secret" + server: "lb.mydomain.com" + user: "admin" + validate_certs: "no" + delegate_to: localhost +''' + +RETURN = ''' +cache: + description: The new value of the DNS caching + returned: changed + type: string + sample: "enabled" +name_servers: + description: List of name servers that were added or removed + returned: changed + type: list + sample: "['192.168.1.10', '172.17.12.10']" +forwarders: + description: List of forwarders that were added or removed + returned: changed + type: list + sample: "['192.168.1.10', '172.17.12.10']" +search: + description: List of search domains that were added or removed + returned: changed + type: list + sample: "['192.168.1.10', '172.17.12.10']" +ip_version: + description: IP version that was set that DNS will specify IP addresses in + returned: changed + type: int + sample: 4 +''' + +try: + from f5.bigip.contexts import TransactionContextManager + from f5.bigip import ManagementRoot + HAS_F5SDK = True +except ImportError: + HAS_F5SDK = False + + +REQUIRED = ['name_servers', 'search', 'forwarders', 'ip_version', 'cache'] +CACHE = ['disable', 'enable'] +IP = [4, 6] + + +class BigIpDeviceDns(object): + def __init__(self, *args, **kwargs): + if not HAS_F5SDK: + raise F5ModuleError("The python f5-sdk module is required") + + # The params that change in the module + self.cparams = dict() + + # Stores the params that are sent to the module + self.params = kwargs + self.api = ManagementRoot(kwargs['server'], + kwargs['user'], + kwargs['password'], + port=kwargs['server_port']) + + def flush(self): + result = dict() + changed = False + state = self.params['state'] + + if self.dhcp_enabled(): + raise F5ModuleError( + "DHCP on the mgmt interface must be disabled to make use of " + + "this module" + ) + + if state == 'absent': + changed = self.absent() + else: + changed = self.present() + + result.update(**self.cparams) + result.update(dict(changed=changed)) + return result + + def dhcp_enabled(self): + r = self.api.tm.sys.dbs.db.load(name='dhclient.mgmt') + if r.value == 'enable': + return True + else: + return False + + def read(self): + result = dict() + + cache = self.api.tm.sys.dbs.db.load(name='dns.cache') + proxy = self.api.tm.sys.dbs.db.load(name='dns.proxy.__iter__') + dns = self.api.tm.sys.dns.load() + + result['cache'] = str(cache.value) + result['forwarders'] = str(proxy.value).split(' ') + + if hasattr(dns, 'nameServers'): + result['name_servers'] = dns.nameServers + if hasattr(dns, 'search'): + result['search'] = dns.search + if hasattr(dns, 'include') and 'options inet6' in dns.include: + result['ip_version'] = 6 + else: + result['ip_version'] = 4 + return result + + def present(self): + params = dict() + current = self.read() + + # Temporary locations to hold the changed params + update = dict( + dns=None, + forwarders=None, + cache=None + ) + + nameservers = self.params['name_servers'] + search_domains = self.params['search'] + ip_version = self.params['ip_version'] + forwarders = self.params['forwarders'] + cache = self.params['cache'] + check_mode = self.params['check_mode'] + + if nameservers: + if 'name_servers' in current: + if nameservers != current['name_servers']: + params['nameServers'] = nameservers + else: + params['nameServers'] = nameservers + + if search_domains: + if 'search' in current: + if search_domains != current['search']: + params['search'] = search_domains + else: + params['search'] = search_domains + + if ip_version: + if 'ip_version' in current: + if ip_version != int(current['ip_version']): + if ip_version == 6: + params['include'] = 'options inet6' + elif ip_version == 4: + params['include'] = '' + else: + if ip_version == 6: + params['include'] = 'options inet6' + elif ip_version == 4: + params['include'] = '' + + if params: + self.cparams.update(camel_dict_to_snake_dict(params)) + + if 'include' in params: + del self.cparams['include'] + if params['include'] == '': + self.cparams['ip_version'] = 4 + else: + self.cparams['ip_version'] = 6 + + update['dns'] = params.copy() + params = dict() + + if forwarders: + if 'forwarders' in current: + if forwarders != current['forwarders']: + params['forwarders'] = forwarders + else: + params['forwarders'] = forwarders + + if params: + self.cparams.update(camel_dict_to_snake_dict(params)) + update['forwarders'] = ' '.join(params['forwarders']) + params = dict() + + if cache: + if 'cache' in current: + if cache != current['cache']: + params['cache'] = cache + + if params: + self.cparams.update(camel_dict_to_snake_dict(params)) + update['cache'] = params['cache'] + params = dict() + + if self.cparams: + changed = True + if check_mode: + return changed + else: + return False + + tx = self.api.tm.transactions.transaction + with TransactionContextManager(tx) as api: + cache = api.tm.sys.dbs.db.load(name='dns.cache') + proxy = api.tm.sys.dbs.db.load(name='dns.proxy.__iter__') + dns = api.tm.sys.dns.load() + + # Empty values can be supplied, but you cannot supply the + # None value, so we check for that specifically + if update['cache'] is not None: + cache.update(value=update['cache']) + if update['forwarders'] is not None: + proxy.update(value=update['forwarders']) + if update['dns'] is not None: + dns.update(**update['dns']) + return changed + + def absent(self): + params = dict() + current = self.read() + + # Temporary locations to hold the changed params + update = dict( + dns=None, + forwarders=None + ) + + nameservers = self.params['name_servers'] + search_domains = self.params['search'] + forwarders = self.params['forwarders'] + check_mode = self.params['check_mode'] + + if forwarders and 'forwarders' in current: + set_current = set(current['forwarders']) + set_new = set(forwarders) + + forwarders = set_current - set_new + if forwarders != set_current: + forwarders = list(forwarders) + params['forwarders'] = ' '.join(forwarders) + + if params: + changed = True + self.cparams.update(camel_dict_to_snake_dict(params)) + update['forwarders'] = params['forwarders'] + params = dict() + + if nameservers and 'name_servers' in current: + set_current = set(current['name_servers']) + set_new = set(nameservers) + + nameservers = set_current - set_new + if nameservers != set_current: + params['nameServers'] = list(nameservers) + + if search_domains and 'search' in current: + set_current = set(current['search']) + set_new = set(search_domains) + + search_domains = set_current - set_new + if search_domains != set_current: + params['search'] = list(search_domains) + + if params: + changed = True + self.cparams.update(camel_dict_to_snake_dict(params)) + update['dns'] = params.copy() + params = dict() + + if not self.cparams: + return False + + if check_mode: + return changed + + tx = self.api.tm.transactions.transaction + with TransactionContextManager(tx) as api: + proxy = api.tm.sys.dbs.db.load(name='dns.proxy.__iter__') + dns = api.tm.sys.dns.load() + + if update['forwarders'] is not None: + proxy.update(value=update['forwarders']) + if update['dns'] is not None: + dns.update(**update['dns']) + return changed + + +def main(): + argument_spec = f5_argument_spec() + + meta_args = dict( + cache=dict(required=False, choices=CACHE, default=None), + name_servers=dict(required=False, default=None, type='list'), + forwarders=dict(required=False, default=None, type='list'), + search=dict(required=False, default=None, type='list'), + ip_version=dict(required=False, default=None, choices=IP, type='int') + ) + argument_spec.update(meta_args) + module = AnsibleModule( + argument_spec=argument_spec, + required_one_of=[REQUIRED], + supports_check_mode=True + ) + + try: + obj = BigIpDeviceDns(check_mode=module.check_mode, **module.params) + result = obj.flush() + + module.exit_json(**result) + except F5ModuleError as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import camel_dict_to_snake_dict +from ansible.module_utils.f5 import * + +if __name__ == '__main__': + main() From cfff0ee5c2056c03babb461b3bcbb69820312c39 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Tue, 23 Aug 2016 15:24:03 -0700 Subject: [PATCH 1982/2522] Enable CI on Shippable for OS X. (#2779) --- shippable.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shippable.yml b/shippable.yml index 90087b94d49..c182c149488 100644 --- a/shippable.yml +++ b/shippable.yml @@ -23,6 +23,8 @@ matrix: - env: TEST=integration PLATFORM=windows VERSION=2012-R2_RTM - env: TEST=integration PLATFORM=freebsd VERSION=10.3-STABLE + + - env: TEST=integration PLATFORM=osx VERSION=10.11 build: pre_ci_boot: options: "--privileged=false --net=bridge" From cfff23fa4afab9276f7a097aa37598098900fd18 Mon Sep 17 00:00:00 2001 From: Richard Metzler Date: Wed, 24 Aug 2016 00:27:47 +0200 Subject: [PATCH 1983/2522] letsencrypt: update URL for agreement pdf (#2696) The Let's Encrypt Subscriber Agreement changed on Aug 01, 2016 https://letsencrypt.org/repository/ --- web_infrastructure/letsencrypt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web_infrastructure/letsencrypt.py b/web_infrastructure/letsencrypt.py index 81cbb28e518..2b7922852ce 100644 --- a/web_infrastructure/letsencrypt.py +++ b/web_infrastructure/letsencrypt.py @@ -74,7 +74,7 @@ - "URI to a terms of service document you agree to when using the ACME service at C(acme_directory)." required: false - default: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf' + default: 'https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf' challenge: description: The challenge to be performed. required: false @@ -750,7 +750,7 @@ def main(): account_key = dict(required=True, type='str'), account_email = dict(required=False, default=None, type='str'), acme_directory = dict(required=False, default='https://acme-staging.api.letsencrypt.org/directory', type='str'), - agreement = dict(required=False, default='https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf', type='str'), + agreement = dict(required=False, default='https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf', type='str'), challenge = dict(required=False, default='http-01', choices=['http-01', 'dns-01', 'tls-sni-02'], type='str'), csr = dict(required=True, aliases=['src'], type='str'), data = dict(required=False, no_log=True, default=None, type='dict'), From f25bf010532d53b325aa235ffba89550983e3e4a Mon Sep 17 00:00:00 2001 From: Jiri Tyr Date: Wed, 24 Aug 2016 03:13:55 +0200 Subject: [PATCH 1984/2522] Adding jenkins_plugin module (#1730) --- web_infrastructure/jenkins_plugin.py | 830 +++++++++++++++++++++++++++ 1 file changed, 830 insertions(+) create mode 100644 web_infrastructure/jenkins_plugin.py diff --git a/web_infrastructure/jenkins_plugin.py b/web_infrastructure/jenkins_plugin.py new file mode 100644 index 00000000000..4dc2c345a6b --- /dev/null +++ b/web_infrastructure/jenkins_plugin.py @@ -0,0 +1,830 @@ +#!/usr/bin/python +# encoding: utf-8 + +# (c) 2016, Jiri Tyr +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import fetch_url +from ansible.module_utils.urls import url_argument_spec +import base64 +import hashlib +import json +import os +import tempfile +import time +import urllib + + +DOCUMENTATION = ''' +--- +module: jenkins_plugin +author: Jiri Tyr (@jtyr) +version_added: '2.2' +short_description: Add or remove Jenkins plugin +description: + - Ansible module which helps to manage Jenkins plugins. + +options: + group: + required: false + default: jenkins + description: + - Name of the Jenkins group on the OS. + jenkins_home: + required: false + default: /var/lib/jenkins + description: + - Home directory of the Jenkins user. + mode: + required: false + default: '0664' + description: + - File mode applied on versioned plugins. + name: + required: true + description: + - Plugin name. + owner: + required: false + default: jenkins + description: + - Name of the Jenkins user on the OS. + params: + required: false + default: null + description: + - Option used to allow the user to overwrite any of the other options. To + remove an option, set the value of the option to C(null). + state: + required: false + choices: [absent, present, pinned, unpinned, enabled, disabled, latest] + default: present + description: + - Desired plugin state. + - If the C(latest) is set, the check for new version will be performed + every time. This is suitable to keep the plugin up-to-date. + timeout: + required: false + default: 30 + description: + - Server connection timeout in secs. + updates_expiration: + required: false + default: 86400 + description: + - Number of seconds after which a new copy of the I(update-center.json) + file is downloaded. This is used to avoid the need to download the + plugin to calculate its checksum when C(latest) is specified. + - Set it to C(0) if no cache file should be used. In that case, the + plugin file will always be downloaded to calculate its checksum when + C(latest) is specified. + updates_url: + required: false + default: https://updates.jenkins-ci.org + description: + - URL of the Update Centre. + - Used as the base URL to download the plugins and the + I(update-center.json) JSON file. + url: + required: false + default: http://localhost:8080 + description: + - URL of the Jenkins server. + version: + required: false + default: null + description: + - Plugin version number. + - If this option is specified, all plugin dependencies must be installed + manually. + - It might take longer to verify that the correct version is installed. + This is especially true if a specific version number is specified. + with_dependencies: + required: false + choices: ['yes', 'no'] + default: 'yes' + description: + - Defines whether to install plugin dependencies. + +notes: + - Plugin installation shoud be run under root or the same user which owns + the plugin files on the disk. Only if the plugin is not installed yet and + no version is specified, the API installation is performed which requires + only the Web UI credentials. + - It's necessary to notify the handler or call the I(service) module to + restart the Jenkins service after a new plugin was installed. + - Pinning works only if the plugin is installed and Jenkis service was + successfully restarted after the plugin installation. + - It is not possible to run the module remotely by changing the I(url) + parameter to point to the Jenkins server. The module must be used on the + host where Jenkins runs as it needs direct access to the plugin files. +''' + +EXAMPLES = ''' +- name: Install plugin + jenkins_plugin: + name: build-pipeline-plugin + +- name: Install plugin without its dependencies + jenkins_plugin: + name: build-pipeline-plugin + with_dependencies: no + +- name: Make sure the plugin is always up-to-date + jenkins_plugin: + name: token-macro + state: latest + +- name: Install specific version of the plugin + jenkins_plugin: + name: token-macro + version: 1.15 + +- name: Pin the plugin + jenkins_plugin: + name: token-macro + state: pinned + +- name: Unpin the plugin + jenkins_plugin: + name: token-macro + state: unpinned + +- name: Enable the plugin + jenkins_plugin: + name: token-macro + state: enabled + +- name: Disable the plugin + jenkins_plugin: + name: token-macro + state: disabled + +- name: Uninstall plugin + jenkins_plugin: + name: build-pipeline-plugin + state: absent + +# +# Example of how to use the params +# +# Define a variable and specify all default parameters you want to use across +# all jenkins_plugin calls: +# +# my_jenkins_params: +# url_username: admin +# url_password: p4ssw0rd +# url: http://localhost:8888 +# +- name: Install plugin + jenkins_plugin: + name: build-pipeline-plugin + params: "{{ my_jenkins_params }}" + +# +# Example of a Play which handles Jenkins restarts during the state changes +# +- name: Jenkins Master play + hosts: jenkins-master + vars: + my_jenkins_plugins: + token-macro: + enabled: yes + build-pipeline-plugin: + version: 1.4.9 + pinned: no + enabled: yes + tasks: + - name: Install plugins without a specific version + jenkins_plugin: + name: "{{ item.key }}" + register: my_jenkins_plugin_unversioned + when: > + 'version' not in item.value + with_dict: my_jenkins_plugins + + - name: Install plugins with a specific version + jenkins_plugin: + name: "{{ item.key }}" + version: "{{ item.value['version'] }}" + register: my_jenkins_plugin_versioned + when: > + 'version' in item.value + with_dict: my_jenkins_plugins + + - name: Initiate the fact + set_fact: + jenkins_restart_required: no + + - name: Check if restart is required by any of the versioned plugins + set_fact: + jenkins_restart_required: yes + when: item.changed + with_items: my_jenkins_plugin_versioned.results + + - name: Check if restart is required by any of the unversioned plugins + set_fact: + jenkins_restart_required: yes + when: item.changed + with_items: my_jenkins_plugin_unversioned.results + + - name: Restart Jenkins if required + service: + name: jenkins + state: restarted + when: jenkins_restart_required + + # Requires python-httplib2 to be installed on the guest + - name: Wait for Jenkins to start up + uri: + url: http://localhost:8080 + status_code: 200 + timeout: 5 + register: jenkins_service_status + # Keep trying for 5 mins in 5 sec intervals + retries: 60 + delay: 5 + until: > + 'status' in jenkins_service_status and + jenkins_service_status['status'] == 200 + when: jenkins_restart_required + + - name: Reset the fact + set_fact: + jenkins_restart_required: no + when: jenkins_restart_required + + - name: Plugin pinning + jenkins_plugin: + name: "{{ item.key }}" + state: "{{ 'pinned' if item.value['pinned'] else 'unpinned'}}" + when: > + 'pinned' in item.value + with_dict: my_jenkins_plugins + + - name: Plugin enabling + jenkins_plugin: + name: "{{ item.key }}" + state: "{{ 'enabled' if item.value['enabled'] else 'disabled'}}" + when: > + 'enabled' in item.value + with_dict: my_jenkins_plugins +''' + +RETURN = ''' +plugin: + description: plugin name + returned: success + type: string + sample: build-pipeline-plugin +state: + description: state of the target, after execution + returned: success + type: string + sample: "present" +''' + + +class JenkinsPlugin(object): + def __init__(self, module): + # To be able to call fail_json + self.module = module + + # Shortcuts for the params + self.params = self.module.params + self.url = self.params['url'] + self.timeout = self.params['timeout'] + + # Crumb + self.crumb = {} + + if self._csrf_enabled(): + self.crumb = self._get_crumb() + + # Get list of installed plugins + self._get_installed_plugins() + + def _csrf_enabled(self): + csrf_data = self._get_json_data( + "%s/%s" % (self.url, "api/json"), 'CSRF') + + return csrf_data["useCrumbs"] + + def _get_json_data(self, url, what, **kwargs): + # Get the JSON data + r = self._get_url_data(url, what, **kwargs) + + # Parse the JSON data + try: + json_data = json.load(r) + except Exception: + e = get_exception() + self.module.fail_json( + msg="Cannot parse %s JSON data." % what, + details=e.message) + + return json_data + + def _get_url_data( + self, url, what=None, msg_status=None, msg_exception=None, + **kwargs): + # Compose default messages + if msg_status is None: + msg_status = "Cannot get %s" % what + + if msg_exception is None: + msg_exception = "Retrieval of %s failed." % what + + # Get the URL data + try: + response, info = fetch_url( + self.module, url, timeout=self.timeout, **kwargs) + + if info['status'] != 200: + self.module.fail_json(msg=msg_status, details=info['msg']) + except Exception: + e = get_exception() + self.module.fail_json(msg=msg_exception, details=e.message) + + return response + + def _get_crumb(self): + crumb_data = self._get_json_data( + "%s/%s" % (self.url, "crumbIssuer/api/json"), 'Crumb') + + if 'crumbRequestField' in crumb_data and 'crumb' in crumb_data: + ret = { + crumb_data['crumbRequestField']: crumb_data['crumb'] + } + else: + self.module.fail_json( + msg="Required fields not found in the Crum response.", + details=crumb_data) + + return ret + + def _get_installed_plugins(self): + plugins_data = self._get_json_data( + "%s/%s" % (self.url, "pluginManager/api/json?depth=1"), + 'list of plugins') + + # Check if we got valid data + if 'plugins' not in plugins_data: + self.module.fail_json(msg="No valid plugin data found.") + + # Create final list of installed/pined plugins + self.is_installed = False + self.is_pinned = False + self.is_enabled = False + + for p in plugins_data['plugins']: + if p['shortName'] == self.params['name']: + self.is_installed = True + + if p['pinned']: + self.is_pinned = True + + if p['enabled']: + self.is_enabled = True + + break + + def install(self): + changed = False + plugin_file = ( + '%s/plugins/%s.jpi' % ( + self.params['jenkins_home'], + self.params['name'])) + + if not self.is_installed and self.params['version'] is None: + if not self.module.check_mode: + # Install the plugin (with dependencies) + install_script = ( + 'd = Jenkins.instance.updateCenter.getPlugin("%s")' + '.deploy(); d.get();' % self.params['name']) + + if self.params['with_dependencies']: + install_script = ( + 'Jenkins.instance.updateCenter.getPlugin("%s")' + '.getNeededDependencies().each{it.deploy()}; %s' % ( + self.params['name'], install_script)) + + script_data = { + 'script': install_script + } + script_data.update(self.crumb) + data = urllib.urlencode(script_data) + + # Send the installation request + r = self._get_url_data( + "%s/scriptText" % self.url, + msg_status="Cannot install plugin.", + msg_exception="Plugin installation has failed.", + data=data) + + changed = True + else: + # Check if the plugin directory exists + if not os.path.isdir(self.params['jenkins_home']): + self.module.fail_json( + msg="Jenkins home directory doesn't exist.") + + md5sum_old = None + if os.path.isfile(plugin_file): + # Make the checksum of the currently installed plugin + md5sum_old = hashlib.md5( + open(plugin_file, 'rb').read()).hexdigest() + + if self.params['version'] in [None, 'latest']: + # Take latest version + plugin_url = ( + "%s/latest/%s.hpi" % ( + self.params['updates_url'], + self.params['name'])) + else: + # Take specific version + plugin_url = ( + "{0}/download/plugins/" + "{1}/{2}/{1}.hpi".format( + self.params['updates_url'], + self.params['name'], + self.params['version'])) + + if ( + self.params['updates_expiration'] == 0 or + self.params['version'] not in [None, 'latest'] or + md5sum_old is None): + + # Download the plugin file directly + r = self._download_plugin(plugin_url) + + # Write downloaded plugin into file if checksums don't match + if md5sum_old is None: + # No previously installed plugin + if not self.module.check_mode: + self._write_file(plugin_file, r) + + changed = True + else: + # Get data for the MD5 + data = r.read() + + # Make new checksum + md5sum_new = hashlib.md5(data).hexdigest() + + # If the checksum is different from the currently installed + # plugin, store the new plugin + if md5sum_old != md5sum_new: + if not self.module.check_mode: + self._write_file(plugin_file, data) + + changed = True + else: + # Check for update from the updates JSON file + plugin_data = self._download_updates() + + try: + sha1_old = hashlib.sha1(open(plugin_file, 'rb').read()) + except Exception: + e = get_exception() + self.module.fail_json( + msg="Cannot calculate SHA1 of the old plugin.", + details=e.message) + + sha1sum_old = base64.b64encode(sha1_old.digest()) + + # If the latest version changed, download it + if sha1sum_old != plugin_data['sha1']: + if not self.module.check_mode: + r = self._download_plugin(plugin_url) + self._write_file(plugin_file, r) + + changed = True + + # Change file attributes if needed + if os.path.isfile(plugin_file): + params = { + 'dest': plugin_file + } + params.update(self.params) + file_args = self.module.load_file_common_arguments(params) + + if not self.module.check_mode: + # Not sure how to run this in the check mode + changed = self.module.set_fs_attributes_if_different( + file_args, changed) + else: + # See the comment above + changed = True + + return changed + + def _download_updates(self): + updates_filename = 'jenkins-plugin-cache.json' + updates_dir = os.path.expanduser('~/.ansible/tmp') + updates_file = "%s/%s" % (updates_dir, updates_filename) + download_updates = True + + # Check if we need to download new updates file + if os.path.isfile(updates_file): + # Get timestamp when the file was changed last time + ts_file = os.stat(updates_file).st_mtime + ts_now = time.time() + + if ts_now - ts_file < self.params['updates_expiration']: + download_updates = False + + updates_file_orig = updates_file + + # Download the updates file if needed + if download_updates: + url = "%s/update-center.json" % self.params['updates_url'] + + # Get the data + r = self._get_url_data( + url, + msg_status="Remote updates not found.", + msg_exception="Updates download failed.") + + # Write the updates file + updates_file = tempfile.mktemp() + + try: + fd = open(updates_file, 'wb') + except IOError: + e = get_exception() + self.module.fail_json( + msg="Cannot open the tmp updates file %s." % updates_file, + details=str(e)) + + fd.write(r.read()) + + try: + fd.close() + except IOError: + e = get_exception() + self.module.fail_json( + msg="Cannot close the tmp updates file %s." % updates_file, + detail=str(e)) + + # Open the updates file + try: + f = open(updates_file) + except IOError: + e = get_exception() + self.module.fail_json( + msg="Cannot open temporal updates file.", + details=str(e)) + + i = 0 + for line in f: + # Read only the second line + if i == 1: + try: + data = json.loads(line) + except Exception: + e = get_exception() + self.module.fail_json( + msg="Cannot load JSON data from the tmp updates file.", + details=e.message) + + break + + i += 1 + + # Move the updates file to the right place if we could read it + if download_updates: + # Make sure the destination directory exists + if not os.path.isdir(updates_dir): + try: + os.makedirs(updates_dir, int('0700', 8)) + except OSError: + e = get_exception() + self.module.fail_json( + msg="Cannot create temporal directory.", + details=e.message) + + self.module.atomic_move(updates_file, updates_file_orig) + + # Check if we have the plugin data available + if 'plugins' not in data or self.params['name'] not in data['plugins']: + self.module.fail_json( + msg="Cannot find plugin data in the updates file.") + + return data['plugins'][self.params['name']] + + def _download_plugin(self, plugin_url): + # Download the plugin + r = self._get_url_data( + plugin_url, + msg_status="Plugin not found.", + msg_exception="Plugin download failed.") + + return r + + def _write_file(self, f, data): + # Store the plugin into a temp file and then move it + tmp_f = tempfile.mktemp() + + try: + fd = open(tmp_f, 'wb') + except IOError: + e = get_exception() + self.module.fail_json( + msg='Cannot open the temporal plugin file %s.' % tmp_f, + details=str(e)) + + if isinstance(data, str): + d = data + else: + d = data.read() + + fd.write(d) + + try: + fd.close() + except IOError: + e = get_exception() + self.module.fail_json( + msg='Cannot close the temporal plugin file %s.' % tmp_f, + details=str(e)) + + # Move the file onto the right place + self.module.atomic_move(tmp_f, f) + + def uninstall(self): + changed = False + + # Perform the action + if self.is_installed: + if not self.module.check_mode: + self._pm_query('doUninstall', 'Uninstallation') + + changed = True + + return changed + + def pin(self): + return self._pinning('pin') + + def unpin(self): + return self._pinning('unpin') + + def _pinning(self, action): + changed = False + + # Check if the plugin is pinned/unpinned + if ( + action == 'pin' and not self.is_pinned or + action == 'unpin' and self.is_pinned): + + # Perform the action + if not self.module.check_mode: + self._pm_query(action, "%sning" % action.capitalize()) + + changed = True + + return changed + + def enable(self): + return self._enabling('enable') + + def disable(self): + return self._enabling('disable') + + def _enabling(self, action): + changed = False + + # Check if the plugin is pinned/unpinned + if ( + action == 'enable' and not self.is_enabled or + action == 'disable' and self.is_enabled): + + # Perform the action + if not self.module.check_mode: + self._pm_query( + "make%sd" % action.capitalize(), + "%sing" % action[:-1].capitalize()) + + changed = True + + return changed + + def _pm_query(self, action, msg): + url = "%s/pluginManager/plugin/%s/%s" % ( + self.params['url'], self.params['name'], action) + data = urllib.urlencode(self.crumb) + + # Send the request + self._get_url_data( + url, + msg_status="Plugin not found. %s" % url, + msg_exception="%s has failed." % msg, + data=data) + + +def main(): + # Module arguments + argument_spec = url_argument_spec() + argument_spec.update( + group=dict(default='jenkins'), + jenkins_home=dict(default='/var/lib/jenkins'), + mode=dict(default='0644', type='raw'), + name=dict(required=True), + owner=dict(default='jenkins'), + params=dict(type='dict'), + state=dict( + choices=[ + 'present', + 'absent', + 'pinned', + 'unpinned', + 'enabled', + 'disabled', + 'latest'], + default='present'), + timeout=dict(default=30, type="int"), + updates_expiration=dict(default=86400, type="int"), + updates_url=dict(default='https://updates.jenkins-ci.org'), + url=dict(default='http://localhost:8080'), + url_password=dict(no_log=True), + version=dict(), + with_dependencies=dict(default=True, type='bool'), + ) + # Module settings + module = AnsibleModule( + argument_spec=argument_spec, + add_file_common_args=True, + supports_check_mode=True, + ) + + # Update module parameters by user's parameters if defined + if 'params' in module.params and isinstance(module.params['params'], dict): + module.params.update(module.params['params']) + # Remove the params + module.params.pop('params', None) + + # Force basic authentication + module.params['force_basic_auth'] = True + + # Convert timeout to float + try: + module.params['timeout'] = float(module.params['timeout']) + except ValueError: + e = get_exception() + module.fail_json( + msg='Cannot convert %s to float.' % module.params['timeout'], + details=str(e)) + + # Set version to latest if state is latest + if module.params['state'] == 'latest': + module.params['state'] = 'present' + module.params['version'] = 'latest' + + # Create some shortcuts + name = module.params['name'] + state = module.params['state'] + + # Initial change state of the task + changed = False + + # Instantiate the JenkinsPlugin object + jp = JenkinsPlugin(module) + + # Perform action depending on the requested state + if state == 'present': + changed = jp.install() + elif state == 'absent': + changed = jp.uninstall() + elif state == 'pinned': + changed = jp.pin() + elif state == 'unpinned': + changed = jp.unpin() + elif state == 'enabled': + changed = jp.enable() + elif state == 'disabled': + changed = jp.disable() + + # Print status of the change + module.exit_json(changed=changed, plugin=name, state=state) + + +if __name__ == '__main__': + main() From 3ec172c3cab7bd04f5f346be5aaa8e99a17798b7 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Tue, 23 Aug 2016 20:51:13 -0700 Subject: [PATCH 1985/2522] New module bigip_irule This module can be used to maintain the iRules for both LTM and GTM on a BIG-IP. iRules should be supplied in their string form using normal strings (unlikely), file lookups (likely), or template lookups (likely). Tests for this module can be found here https://github.com/F5Networks/f5-ansible/blob/master/roles/__bigip_irule/tasks/main.yaml Platforms this was tested on are 11.6.1 12.0.0 12.1.0 --- network/f5/bigip_irule.py | 355 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 355 insertions(+) create mode 100644 network/f5/bigip_irule.py diff --git a/network/f5/bigip_irule.py b/network/f5/bigip_irule.py new file mode 100644 index 00000000000..b1ec9acef2a --- /dev/null +++ b/network/f5/bigip_irule.py @@ -0,0 +1,355 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: bigip_irule +short_description: Manage iRules across different modules on a BIG-IP. +description: + - Manage iRules across different modules on a BIG-IP. +version_added: "2.2" +options: + content: + description: + - When used instead of 'src', sets the contents of an iRule directly to + the specified value. This is for simple values, but can be used with + lookup plugins for anything complex or with formatting. Either one + of C(src) or C(content) must be provided. + module: + description: + - The BIG-IP module to add the iRule to. + required: true + choices: + - ltm + - gtm + partition: + description: + - The partition to create the iRule on. + required: false + default: Common + name: + description: + - The name of the iRule. + required: true + src: + description: + - The iRule file to interpret and upload to the BIG-IP. Either one + of C(src) or C(content) must be provided. + required: true + state: + description: + - Whether the iRule should exist or not. + required: false + default: present + choices: + - present + - absent +notes: + - Requires the f5-sdk Python package on the host. This is as easy as + pip install f5-sdk. +extends_documentation_fragment: f5 +requirements: + - f5-sdk +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = ''' +- name: Add the iRule contained in templated irule.tcl to the LTM module + bigip_irule: + content: "{{ lookup('template', 'irule-template.tcl') }}" + module: "ltm" + name: "MyiRule" + password: "secret" + server: "lb.mydomain.com" + state: "present" + user: "admin" + delegate_to: localhost + +- name: Add the iRule contained in static file irule.tcl to the LTM module + bigip_irule: + module: "ltm" + name: "MyiRule" + password: "secret" + server: "lb.mydomain.com" + src: "irule-static.tcl" + state: "present" + user: "admin" + delegate_to: localhost +''' + +RETURN = ''' +full_name: + description: Full name of the user + returned: changed and success + type: string + sample: "John Doe" +''' + +try: + from f5.bigip import ManagementRoot + from icontrol.session import iControlUnexpectedHTTPError + HAS_F5SDK = True +except ImportError: + HAS_F5SDK = False + +MODULES = ['gtm', 'ltm'] + + +class BigIpiRule(object): + def __init__(self, *args, **kwargs): + if not HAS_F5SDK: + raise F5ModuleError("The python f5-sdk module is required") + + if kwargs['state'] != 'absent': + if not kwargs['content'] and not kwargs['src']: + raise F5ModuleError( + "Either 'content' or 'src' must be provided" + ) + + source = kwargs['src'] + if source: + with open(source) as f: + kwargs['content'] = f.read() + + # The params that change in the module + self.cparams = dict() + + # Stores the params that are sent to the module + self.params = kwargs + self.api = ManagementRoot(kwargs['server'], + kwargs['user'], + kwargs['password'], + port=kwargs['server_port']) + + def flush(self): + result = dict() + state = self.params['state'] + + try: + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + except iControlUnexpectedHTTPError as e: + raise F5ModuleError(str(e)) + + result.update(**self.cparams) + result.update(dict(changed=changed)) + return result + + def read(self): + """Read information and transform it + + The values that are returned by BIG-IP in the f5-sdk can have encoding + attached to them as well as be completely missing in some cases. + + Therefore, this method will transform the data from the BIG-IP into a + format that is more easily consumable by the rest of the class and the + parameters that are supported by the module. + """ + p = dict() + name = self.params['name'] + partition = self.params['partition'] + module = self.params['module'] + + if module == 'ltm': + r = self.api.tm.ltm.rules.rule.load( + name=name, + partition=partition + ) + elif module == 'gtm': + r = self.api.tm.gtm.rules.rule.load( + name=name, + partition=partition + ) + + if hasattr(r, 'apiAnonymous'): + p['content'] = str(r.apiAnonymous) + p['name'] = name + return p + + def delete(self): + params = dict() + check_mode = self.params['check_mode'] + module = self.params['module'] + + params['name'] = self.params['name'] + params['partition'] = self.params['partition'] + + self.cparams = camel_dict_to_snake_dict(params) + if check_mode: + return True + + if module == 'ltm': + r = self.api.tm.ltm.rules.rule.load(**params) + r.delete() + elif module == 'gtm': + r = self.api.tm.gtm.rules.rule.load(**params) + r.delete() + + if self.exists(): + raise F5ModuleError("Failed to delete the iRule") + return True + + def exists(self): + name = self.params['name'] + partition = self.params['partition'] + module = self.params['module'] + + if module == 'ltm': + return self.api.tm.ltm.rules.rule.exists( + name=name, + partition=partition + ) + elif module == 'gtm': + return self.api.tm.gtm.rules.rule.exists( + name=name, + partition=partition + ) + + def present(self): + changed = False + + if self.exists(): + changed = self.update() + else: + changed = self.create() + + return changed + + def update(self): + params = dict() + current = self.read() + changed = False + + check_mode = self.params['check_mode'] + content = self.params['content'] + name = self.params['name'] + partition = self.params['partition'] + module = self.params['module'] + + if content is not None: + if 'content' in current: + if content != current['content']: + params['apiAnonymous'] = content + else: + params['apiAnonymous'] = content + + if params: + changed = True + params['name'] = name + params['partition'] = partition + if check_mode: + return changed + self.cparams = camel_dict_to_snake_dict(params) + else: + return changed + + if module == 'ltm': + d = self.api.tm.ltm.rules.rule.load( + name=name, + partition=partition + ) + d.update(**params) + d.refresh() + elif module == 'gtm': + d = self.api.tm.gtm.rules.rule.load( + name=name, + partition=partition + ) + d.update(**params) + d.refresh() + + return True + + def create(self): + params = dict() + + check_mode = self.params['check_mode'] + content = self.params['content'] + name = self.params['name'] + partition = self.params['partition'] + module = self.params['module'] + + if check_mode: + return True + + if content is not None: + params['apiAnonymous'] = content + + params['name'] = name + params['partition'] = partition + + self.cparams = camel_dict_to_snake_dict(params) + if check_mode: + return True + + if module == 'ltm': + d = self.api.tm.ltm.rules.rule + d.create(**params) + elif module == 'gtm': + d = self.api.tm.gtm.rules.rule + d.create(**params) + + if not self.exists(): + raise F5ModuleError("Failed to create the iRule") + return True + + def absent(self): + changed = False + + if self.exists(): + changed = self.delete() + + return changed + + +def main(): + argument_spec = f5_argument_spec() + + meta_args = dict( + content=dict(required=False, default=None), + src=dict(required=False, default=None), + name=dict(required=True), + module=dict(required=True, choices=MODULES) + ) + argument_spec.update(meta_args) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + mutually_exclusive=[ + ['content', 'src'] + ] + ) + + try: + obj = BigIpiRule(check_mode=module.check_mode, **module.params) + result = obj.flush() + + module.exit_json(**result) + except F5ModuleError as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import camel_dict_to_snake_dict +from ansible.module_utils.f5 import * + +if __name__ == '__main__': + main() From b8db87d20607a8d8a92f0a8df9e826807530549b Mon Sep 17 00:00:00 2001 From: Kevin Brebanov Date: Wed, 24 Aug 2016 02:54:44 -0400 Subject: [PATCH 1986/2522] apk: Fix mutual exclusivity (#2768) * apk: Fix mutual exclusivity Ensure that 'name' and 'upgrade' are mutually exclusive. Also add a note to the documentation to say so. Fixes: #2767 * Fix documentation --- packaging/os/apk.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packaging/os/apk.py b/packaging/os/apk.py index c867d1160d1..911e50e0942 100644 --- a/packaging/os/apk.py +++ b/packaging/os/apk.py @@ -52,6 +52,8 @@ required: false default: no choices: [ "yes", "no" ] +notes: + - '"name" and "upgrade" are mutually exclusive.' ''' EXAMPLES = ''' @@ -209,6 +211,7 @@ def main(): upgrade = dict(default='no', type='bool'), ), required_one_of = [['name', 'update_cache', 'upgrade']], + mutually_exclusive = [['name', 'upgrade']], supports_check_mode = True ) From 65eba72ee6485a1297c655b65333ca7c61303838 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 24 Aug 2016 08:40:31 -0700 Subject: [PATCH 1987/2522] Get hipchat, sns, and typetalk notification modules compiling on py3 (#2782) --- .travis.yml | 5 +---- notification/hipchat.py | 28 +++++++++++++++++----------- notification/sns.py | 24 ++++++++++++++++-------- notification/typetalk.py | 12 ++++++++---- 4 files changed, 42 insertions(+), 27 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1cd8b9b5d8c..5c6ba09b29c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -102,13 +102,10 @@ env: network/nmcli.py network/openvswitch_bridge.py network/openvswitch_port.py - notification/hipchat.py notification/irc.py notification/jabber.py notification/mail.py - notification/mqtt.py - notification/sns.py - notification/typetalk.py" + notification/mqtt.py" before_install: - git config user.name "ansible" - git config user.email "ansible@ansible.com" diff --git a/notification/hipchat.py b/notification/hipchat.py index 8749b0b742e..a07042bc3f8 100644 --- a/notification/hipchat.py +++ b/notification/hipchat.py @@ -97,6 +97,15 @@ # import urllib +try: + import json +except ImportError: + import simplejson as json + +# import module snippets +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import fetch_url DEFAULT_URI = "https://api.hipchat.com/v1" @@ -104,10 +113,10 @@ NOTIFY_URI_V2 = "/room/{id_or_name}/notification" + def send_msg_v1(module, token, room, msg_from, msg, msg_format='text', - color='yellow', notify=False, api=MSG_URI_V1): + color='yellow', notify=False, api=MSG_URI_V1): '''sending message to hipchat v1 server''' - print "Sending message to v1 server" params = {} params['room_id'] = room @@ -133,11 +142,10 @@ def send_msg_v1(module, token, room, msg_from, msg, msg_format='text', def send_msg_v2(module, token, room, msg_from, msg, msg_format='text', - color='yellow', notify=False, api=NOTIFY_URI_V2): + color='yellow', notify=False, api=NOTIFY_URI_V2): '''sending message to hipchat v2 server''' - print "Sending message to v2 server" - headers = {'Authorization':'Bearer %s' % token, 'Content-Type':'application/json'} + headers = {'Authorization': 'Bearer %s' % token, 'Content-Type': 'application/json'} body = dict() body['message'] = msg @@ -200,14 +208,12 @@ def main(): send_msg_v2(module, token, room, msg_from, msg, msg_format, color, notify, api) else: send_msg_v1(module, token, room, msg_from, msg, msg_format, color, notify, api) - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg="unable to send msg: %s" % e) changed = True module.exit_json(changed=changed, room=room, msg_from=msg_from, msg=msg) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * - -main() +if __name__ == '__main__': + main() diff --git a/notification/sns.py b/notification/sns.py index 6288ee86a30..4eb79e13ade 100644 --- a/notification/sns.py +++ b/notification/sns.py @@ -61,7 +61,7 @@ required: false aws_secret_key: description: - - AWS secret key. If not set then the value of the AWS_SECRET_KEY environment variable is used. + - AWS secret key. If not set then the value of the AWS_SECRET_KEY environment variable is used. required: false default: None aliases: ['ec2_secret_key', 'secret_key'] @@ -77,7 +77,8 @@ required: false aliases: ['aws_region', 'ec2_region'] -requirements: [ "boto" ] +requirements: + - "boto" """ EXAMPLES = """ @@ -97,10 +98,14 @@ topic: "deploy" """ -import sys +try: + import json +except ImportError: + import simplejson as json -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import ec2_argument_spec, connect_to_aws, get_aws_connection_info +from ansible.module_utils.pycompat24 import get_exception try: import boto @@ -156,7 +161,8 @@ def main(): module.fail_json(msg="region must be specified") try: connection = connect_to_aws(boto.sns, region, **aws_connect_params) - except boto.exception.NoAuthHandlerFound, e: + except boto.exception.NoAuthHandlerFound: + e = get_exception() module.fail_json(msg=str(e)) # .publish() takes full ARN topic id, but I'm lazy and type shortnames @@ -185,9 +191,11 @@ def main(): try: connection.publish(topic=arn_topic, subject=subject, message_structure='json', message=json_msg) - except boto.exception.BotoServerError, e: + except boto.exception.BotoServerError: + e = get_exception() module.fail_json(msg=str(e)) module.exit_json(msg="OK") -main() +if __name__ == '__main__': + main() diff --git a/notification/typetalk.py b/notification/typetalk.py index 4a31e3ef89a..2f91022936f 100644 --- a/notification/typetalk.py +++ b/notification/typetalk.py @@ -57,6 +57,11 @@ except ImportError: json = None +# import module snippets +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import fetch_url, ConnectionError + def do_request(module, url, params, headers=None): data = urllib.urlencode(params) @@ -72,6 +77,7 @@ def do_request(module, url, params, headers=None): raise exc return r + def get_access_token(module, client_id, client_secret): params = { 'client_id': client_id, @@ -95,7 +101,8 @@ def send_message(module, client_id, client_secret, topic, msg): } do_request(module, url, {'message': msg}, headers) return True, {'access_token': access_token} - except ConnectionError, e: + except ConnectionError: + e = get_exception() return False, e @@ -126,8 +133,5 @@ def main(): module.exit_json(changed=True, topic=topic, msg=msg) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * if __name__ == '__main__': main() From 325d497fb8281bbe7df049548da99629581ee03c Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Tue, 23 Aug 2016 11:07:15 -0700 Subject: [PATCH 1988/2522] Addition of bigip_device_sshd module This module can be used as part of the bootstrapping of a BIG-IP. It allows one to configure the various SSHD settings that are part of a BIG-IP. Tests for this module can be found here https://github.com/F5Networks/f5-ansible/blob/master/roles/__bigip_device_sshd/tasks/main.yaml Platforms this was tested on are 11.6.0 12.0.0 12.1.0 12.1.0 HF1 --- network/f5/bigip_device_sshd.py | 344 ++++++++++++++++++++++++++++++++ 1 file changed, 344 insertions(+) create mode 100644 network/f5/bigip_device_sshd.py diff --git a/network/f5/bigip_device_sshd.py b/network/f5/bigip_device_sshd.py new file mode 100644 index 00000000000..fd27127f897 --- /dev/null +++ b/network/f5/bigip_device_sshd.py @@ -0,0 +1,344 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: bigip_device_sshd +short_description: Manage the SSHD settings of a BIG-IP +description: + - Manage the SSHD settings of a BIG-IP +version_added: "2.2" +options: + allow: + description: + - Specifies, if you have enabled SSH access, the IP address or address + range for other systems that can use SSH to communicate with this + system. + choices: + - all + - IP address, such as 172.27.1.10 + - IP range, such as 172.27.*.* or 172.27.0.0/255.255.0.0 + banner: + description: + - Whether to enable the banner or not. + required: false + choices: + - enabled + - disabled + banner_text: + description: + - Specifies the text to include on the pre-login banner that displays + when a user attempts to login to the system using SSH. + required: false + inactivity_timeout: + description: + - Specifies the number of seconds before inactivity causes an SSH + session to log out. + required: false + log_level: + description: + - Specifies the minimum SSHD message level to include in the system log. + choices: + - debug + - debug1 + - debug2 + - debug3 + - error + - fatal + - info + - quiet + - verbose + login: + description: + - Specifies, when checked C(enabled), that the system accepts SSH + communications. + choices: + - enabled + - disabled + required: false + port: + description: + - Port that you want the SSH daemon to run on. + required: false +notes: + - Requires the f5-sdk Python package on the host This is as easy as pip + install f5-sdk. + - Requires BIG-IP version 12.0.0 or greater +extends_documentation_fragment: f5 +requirements: + - f5-sdk +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = ''' +- name: Set the banner for the SSHD service from a string + bigip_device_sshd: + banner: "enabled" + banner_text: "banner text goes here" + password: "secret" + server: "lb.mydomain.com" + user: "admin" + delegate_to: localhost + +- name: Set the banner for the SSHD service from a file + bigip_device_sshd: + banner: "enabled" + banner_text: "{{ lookup('file', '/path/to/file') }}" + password: "secret" + server: "lb.mydomain.com" + user: "admin" + delegate_to: localhost + +- name: Set the SSHD service to run on port 2222 + bigip_device_sshd: + password: "secret" + port: 2222 + server: "lb.mydomain.com" + user: "admin" + delegate_to: localhost +''' + +RETURN = ''' +allow: + description: | + Specifies, if you have enabled SSH access, the IP address or address + range for other systems that can use SSH to communicate with this + system. + returned: changed + type: list + sample: "192.168.*.*" +banner: + description: Whether the banner is enabled or not. + returned: changed + type: string + sample: "true" +banner_text: + description: | + Specifies the text included on the pre-login banner that + displays when a user attempts to login to the system using SSH. + returned: changed and success + type: string + sample: "This is a corporate device. Connecting to it without..." +inactivity_timeout: + description: | + The number of seconds before inactivity causes an SSH. + session to log out + returned: changed + type: int + sample: "10" +log_level: + description: The minimum SSHD message level to include in the system log. + returned: changed + type: string + sample: "debug" +login: + description: Specifies that the system accepts SSH communications or not. + return: changed + type: bool + sample: true +port: + description: Port that you want the SSH daemon to run on. + return: changed + type: int + sample: 22 +''' + +try: + from f5.bigip import ManagementRoot + from icontrol.session import iControlUnexpectedHTTPError + HAS_F5SDK = True +except ImportError: + HAS_F5SDK = False + +CHOICES = ['enabled', 'disabled'] +LEVELS = ['debug', 'debug1', 'debug2', 'debug3', 'error', 'fatal', 'info', + 'quiet', 'verbose'] + + +class BigIpDeviceSshd(object): + def __init__(self, *args, **kwargs): + if not HAS_F5SDK: + raise F5ModuleError("The python f5-sdk module is required") + + # The params that change in the module + self.cparams = dict() + + # Stores the params that are sent to the module + self.params = kwargs + self.api = ManagementRoot(kwargs['server'], + kwargs['user'], + kwargs['password'], + port=kwargs['server_port']) + + def update(self): + changed = False + current = self.read() + params = dict() + + allow = self.params['allow'] + banner = self.params['banner'] + banner_text = self.params['banner_text'] + timeout = self.params['inactivity_timeout'] + log_level = self.params['log_level'] + login = self.params['login'] + port = self.params['port'] + check_mode = self.params['check_mode'] + + if allow: + if 'allow' in current: + items = set(allow) + if items != current['allow']: + params['allow'] = list(items) + else: + params['allow'] = allow + + if banner: + if 'banner' in current: + if banner != current['banner']: + params['banner'] = banner + else: + params['banner'] = banner + + if banner_text: + if 'banner_text' in current: + if banner_text != current['banner_text']: + params['bannerText'] = banner_text + else: + params['bannerText'] = banner_text + + if timeout: + if 'inactivity_timeout' in current: + if timeout != current['inactivity_timeout']: + params['inactivityTimeout'] = timeout + else: + params['inactivityTimeout'] = timeout + + if log_level: + if 'log_level' in current: + if log_level != current['log_level']: + params['logLevel'] = log_level + else: + params['logLevel'] = log_level + + if login: + if 'login' in current: + if login != current['login']: + params['login'] = login + else: + params['login'] = login + + if port: + if 'port' in current: + if port != current['port']: + params['port'] = port + else: + params['port'] = port + + if params: + changed = True + if check_mode: + return changed + self.cparams = camel_dict_to_snake_dict(params) + else: + return changed + + r = self.api.tm.sys.sshd.load() + r.update(**params) + r.refresh() + + return changed + + def read(self): + """Read information and transform it + + The values that are returned by BIG-IP in the f5-sdk can have encoding + attached to them as well as be completely missing in some cases. + + Therefore, this method will transform the data from the BIG-IP into a + format that is more easily consumable by the rest of the class and the + parameters that are supported by the module. + """ + p = dict() + r = self.api.tm.sys.sshd.load() + + if hasattr(r, 'allow'): + # Deliberately using sets to supress duplicates + p['allow'] = set([str(x) for x in r.allow]) + if hasattr(r, 'banner'): + p['banner'] = str(r.banner) + if hasattr(r, 'bannerText'): + p['banner_text'] = str(r.bannerText) + if hasattr(r, 'inactivityTimeout'): + p['inactivity_timeout'] = str(r.inactivityTimeout) + if hasattr(r, 'logLevel'): + p['log_level'] = str(r.logLevel) + if hasattr(r, 'login'): + p['login'] = str(r.login) + if hasattr(r, 'port'): + p['port'] = int(r.port) + return p + + def flush(self): + result = dict() + changed = False + + try: + changed = self.update() + except iControlUnexpectedHTTPError as e: + raise F5ModuleError(str(e)) + + result.update(**self.cparams) + result.update(dict(changed=changed)) + return result + + +def main(): + argument_spec = f5_argument_spec() + + meta_args = dict( + allow=dict(required=False, default=None, type='list'), + banner=dict(required=False, default=None, choices=CHOICES), + banner_text=dict(required=False, default=None), + inactivity_timeout=dict(required=False, default=None, type='int'), + log_level=dict(required=False, default=None, choices=LEVELS), + login=dict(required=False, default=None, choices=CHOICES), + port=dict(required=False, default=None, type='int'), + state=dict(default='present', choices=['present']) + ) + argument_spec.update(meta_args) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True + ) + + try: + obj = BigIpDeviceSshd(check_mode=module.check_mode, **module.params) + result = obj.flush() + + module.exit_json(**result) + except F5ModuleError as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import camel_dict_to_snake_dict +from ansible.module_utils.f5 import * + +if __name__ == '__main__': + main() From de902d4308efcfed79f0cf48e832770087f5e178 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Tue, 23 Aug 2016 11:43:23 -0700 Subject: [PATCH 1989/2522] This module can be used as part of the bootstrapping of a BIG-IP. It allows one to configure the various NTP settings that are part of a BIG-IP. Tests for this module can be found here https://github.com/F5Networks/f5-ansible/blob/master/roles/__bigip_device_ntp/tasks/main.yaml Platforms this was tested on are 11.6.0 12.0.0 12.1.0 12.1.0 HF1 --- network/f5/bigip_device_ntp.py | 257 +++++++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 network/f5/bigip_device_ntp.py diff --git a/network/f5/bigip_device_ntp.py b/network/f5/bigip_device_ntp.py new file mode 100644 index 00000000000..2c591ebacf6 --- /dev/null +++ b/network/f5/bigip_device_ntp.py @@ -0,0 +1,257 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: bigip_device_ntp +short_description: Manage NTP servers on a BIG-IP +description: + - Manage NTP servers on a BIG-IP +version_added: "2.2" +options: + ntp_servers: + description: + - A list of NTP servers to set on the device. At least one of C(ntp_servers) + or C(timezone) is required. + required: false + default: [] + state: + description: + - The state of the NTP servers on the system. When C(present), guarantees + that the NTP servers are set on the system. When C(absent), removes the + specified NTP servers from the device configuration. + required: false + default: present + choices: + - absent + - present + timezone: + description: + - The timezone to set for NTP lookups. At least one of C(ntp_servers) or + C(timezone) is required. + default: UTC + required: false +notes: + - Requires the f5-sdk Python package on the host. This is as easy as pip + install f5-sdk. +extends_documentation_fragment: f5 +requirements: + - f5-sdk +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = ''' +- name: Set NTP server + bigip_device_ntp: + ntp_servers: + - "192.168.10.12" + password: "secret" + server: "lb.mydomain.com" + user: "admin" + validate_certs: "no" + delegate_to: localhost + +- name: Set timezone + bigip_device_ntp: + password: "secret" + server: "lb.mydomain.com" + timezone: "America/Los_Angeles" + user: "admin" + validate_certs: "no" + delegate_to: localhost +''' + +RETURN = ''' +ntp_servers: + description: The NTP servers that were set on the device + returned: changed + type: list + sample: ["192.168.10.10", "172.27.10.10"] +timezone: + description: The timezone that was set on the device + returned: changed + type: string + sample: "true" +''' + +try: + from f5.bigip import ManagementRoot + from icontrol.session import iControlUnexpectedHTTPError + HAS_F5SDK = True +except ImportError: + HAS_F5SDK = False + + +class BigIpDeviceNtp(object): + def __init__(self, *args, **kwargs): + if not HAS_F5SDK: + raise F5ModuleError("The python f5-sdk module is required") + + # The params that change in the module + self.cparams = dict() + + # Stores the params that are sent to the module + self.params = kwargs + self.api = ManagementRoot(kwargs['server'], + kwargs['user'], + kwargs['password'], + port=kwargs['server_port']) + + def flush(self): + result = dict() + changed = False + state = self.params['state'] + + try: + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + except iControlUnexpectedHTTPError as e: + raise F5ModuleError(str(e)) + + if 'servers' in self.cparams: + self.cparams['ntp_servers'] = self.cparams.pop('servers') + + result.update(**self.cparams) + result.update(dict(changed=changed)) + return result + + def read(self): + """Read information and transform it + + The values that are returned by BIG-IP in the f5-sdk can have encoding + attached to them as well as be completely missing in some cases. + + Therefore, this method will transform the data from the BIG-IP into a + format that is more easily consumable by the rest of the class and the + parameters that are supported by the module. + """ + p = dict() + r = self.api.tm.sys.ntp.load() + + if hasattr(r, 'servers'): + # Deliberately using sets to supress duplicates + p['servers'] = set([str(x) for x in r.servers]) + if hasattr(r, 'timezone'): + p['timezone'] = str(r.timezone) + return p + + def present(self): + changed = False + params = dict() + current = self.read() + + check_mode = self.params['check_mode'] + ntp_servers = self.params['ntp_servers'] + timezone = self.params['timezone'] + + # NTP servers can be set independently + if ntp_servers is not None: + if 'servers' in current: + items = set(ntp_servers) + if items != current['servers']: + params['servers'] = list(ntp_servers) + else: + params['servers'] = ntp_servers + + # Timezone can be set independently + if timezone is not None: + if 'timezone' in current and current['timezone'] != timezone: + params['timezone'] = timezone + + if params: + changed = True + self.cparams = camel_dict_to_snake_dict(params) + if check_mode: + return changed + else: + return changed + + r = self.api.tm.sys.ntp.load() + r.update(**params) + r.refresh() + + return changed + + def absent(self): + changed = False + params = dict() + current = self.read() + + check_mode = self.params['check_mode'] + ntp_servers = self.params['ntp_servers'] + + if not ntp_servers: + raise F5ModuleError( + "Absent can only be used when removing NTP servers" + ) + + if ntp_servers and 'servers' in current: + servers = current['servers'] + new_servers = [x for x in servers if x not in ntp_servers] + + if servers != new_servers: + params['servers'] = new_servers + + if params: + changed = True + self.cparams = camel_dict_to_snake_dict(params) + if check_mode: + return changed + else: + return changed + + r = self.api.tm.sys.ntp.load() + r.update(**params) + r.refresh() + return changed + + +def main(): + argument_spec = f5_argument_spec() + + meta_args = dict( + ntp_servers=dict(required=False, type='list', default=None), + timezone=dict(default=None, required=False) + ) + argument_spec.update(meta_args) + + module = AnsibleModule( + argument_spec=argument_spec, + required_one_of=[ + ['ntp_servers', 'timezone'] + ], + supports_check_mode=True + ) + + try: + obj = BigIpDeviceNtp(check_mode=module.check_mode, **module.params) + result = obj.flush() + + module.exit_json(**result) + except F5ModuleError as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import camel_dict_to_snake_dict +from ansible.module_utils.f5 import * + +if __name__ == '__main__': + main() From 85b8615af51a1bfce1510e792c1c379800cd148f Mon Sep 17 00:00:00 2001 From: Lingxian Kong Date: Thu, 25 Aug 2016 23:43:48 +1200 Subject: [PATCH 1990/2522] Add openstack os_server_group module (#2702) --- cloud/openstack/os_server_group.py | 182 +++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 cloud/openstack/os_server_group.py diff --git a/cloud/openstack/os_server_group.py b/cloud/openstack/os_server_group.py new file mode 100644 index 00000000000..155c4497cc2 --- /dev/null +++ b/cloud/openstack/os_server_group.py @@ -0,0 +1,182 @@ +#!/usr/bin/python + +# Copyright (c) 2016 Catalyst IT Limited +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + + +try: + import shade + HAS_SHADE = True +except ImportError: + HAS_SHADE = False + +DOCUMENTATION = ''' +--- +module: os_server_group +short_description: Manage OpenStack server groups +extends_documentation_fragment: openstack +version_added: "2.2" +author: "Lingxian Kong (@kong)" +description: + - Add or remove server groups from OpenStack. +options: + state: + description: + - Indicate desired state of the resource. When I(state) is 'present', + then I(policies) is required. + choices: ['present', 'absent'] + required: false + default: present + name: + description: + - Server group name. + required: true + policies: + description: + - A list of one or more policy names to associate with the server + group. The list must contain at least one policy name. The current + valid policy names are anti-affinity, affinity, soft-anti-affinity + and soft-affinity. + required: false +requirements: + - "python >= 2.6" + - "shade" +''' + +EXAMPLES = ''' +# Create a server group with 'affinity' policy. +- os_server_group: + state: present + auth: + auth_url: https://api.cloud.catalyst.net.nz:5000/v2.0 + username: admin + password: admin + project_name: admin + name: my_server_group + policies: + - affinity + +# Delete 'my_server_group' server group. +- os_server_group: + state: absent + auth: + auth_url: https://api.cloud.catalyst.net.nz:5000/v2.0 + username: admin + password: admin + project_name: admin + name: my_server_group +''' + +RETURN = ''' +id: + description: Unique UUID. + returned: success + type: string +name: + description: The name of the server group. + returned: success + type: string +policies: + description: A list of one or more policy names of the server group. + returned: success + type: list of strings +members: + description: A list of members in the server group. + returned: success + type: list of strings +metadata: + description: Metadata key and value pairs. + returned: success + type: dict +project_id: + description: The project ID who owns the server group. + returned: success + type: string +user_id: + description: The user ID who owns the server group. + returned: success + type: string +''' + + +def _system_state_change(state, server_group): + if state == 'present' and not server_group: + return True + if state == 'absent' and server_group: + return True + + return False + + +def main(): + argument_spec = openstack_full_argument_spec( + name=dict(required=True), + policies=dict(required=False, type='list'), + state=dict(default='present', choices=['absent', 'present']), + ) + module_kwargs = openstack_module_kwargs() + module = AnsibleModule( + argument_spec, + supports_check_mode=True, + **module_kwargs + ) + + if not HAS_SHADE: + module.fail_json(msg='shade is required for this module') + + name = module.params['name'] + policies = module.params['policies'] + state = module.params['state'] + + try: + cloud = shade.openstack_cloud(**module.params) + server_group = cloud.get_server_group(name) + + if module.check_mode: + module.exit_json( + changed=_system_state_change(state, server_group) + ) + + changed = False + if state == 'present': + if not server_group: + if not policies: + module.fail_json( + msg="Parameter 'policies' is required in Server Group " + "Create" + ) + server_group = cloud.create_server_group(name, policies) + changed = True + + module.exit_json( + changed=changed, + id=server_group['id'], + server_group=server_group + ) + if state == 'absent': + if server_group: + cloud.delete_server_group(server_group['id']) + changed = True + module.exit_json(changed=changed) + except shade.OpenStackCloudException as e: + module.fail_json(msg=str(e), extra_data=e.extra_data) + + +# this is magic, see lib/ansible/module_common.py +from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * + +if __name__ == '__main__': + main() From e5cf5137cfdee765a63e6c856160ba470e80f53e Mon Sep 17 00:00:00 2001 From: Manuel Sousa Date: Thu, 25 Aug 2016 17:45:17 +0100 Subject: [PATCH 1991/2522] rabbitmq_exchange - Update requirements to show minimum version of requests (#2785) --- messaging/rabbitmq_exchange.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/messaging/rabbitmq_exchange.py b/messaging/rabbitmq_exchange.py index 1e69d434502..7b55b5c6836 100644 --- a/messaging/rabbitmq_exchange.py +++ b/messaging/rabbitmq_exchange.py @@ -28,7 +28,7 @@ short_description: This module manages rabbitMQ exchanges description: - This module uses rabbitMQ Rest API to create/delete exchanges -requirements: [ python requests ] +requirements: [ "requests >= 1.0.0" ] options: name: description: @@ -135,7 +135,7 @@ def main(): urllib.quote(module.params['vhost'],''), urllib.quote(module.params['name'],'') ) - + # Check if exchange already exists r = requests.get( url, auth=(module.params['login_user'],module.params['login_password'])) From 93d552e6beaeec7b6308f0546cc24371fc0edf62 Mon Sep 17 00:00:00 2001 From: Manuel Sousa Date: Thu, 25 Aug 2016 17:45:37 +0100 Subject: [PATCH 1992/2522] rabbitmq_binding - Update requirements to show minimum version of requests (#2787) --- messaging/rabbitmq_binding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messaging/rabbitmq_binding.py b/messaging/rabbitmq_binding.py index 2768095b9d7..c1ca32a1ce4 100644 --- a/messaging/rabbitmq_binding.py +++ b/messaging/rabbitmq_binding.py @@ -28,7 +28,7 @@ short_description: This module manages rabbitMQ bindings description: - This module uses rabbitMQ Rest API to create/delete bindings -requirements: [ python requests ] +requirements: [ "requests >= 1.0.0" ] options: state: description: From 61d326a75344b7c660f414eac2ac92892f64d968 Mon Sep 17 00:00:00 2001 From: Manuel Sousa Date: Thu, 25 Aug 2016 17:46:33 +0100 Subject: [PATCH 1993/2522] rabbitmq_queue - Update requirements to show minimum version of requests (#2786) --- messaging/rabbitmq_queue.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/messaging/rabbitmq_queue.py b/messaging/rabbitmq_queue.py index be37ce7eaa3..afdd410349b 100644 --- a/messaging/rabbitmq_queue.py +++ b/messaging/rabbitmq_queue.py @@ -22,13 +22,13 @@ DOCUMENTATION = ''' --- module: rabbitmq_queue -author: "Manuel Sousa (@manuel-sousa)" +author: "Manuel Sousa (@manuel-sousa)" version_added: "2.0" short_description: This module manages rabbitMQ queues description: - This module uses rabbitMQ Rest API to create/delete queues -requirements: [ python requests ] +requirements: [ "requests >= 1.0.0" ] options: name: description: @@ -152,7 +152,7 @@ def main(): urllib.quote(module.params['vhost'],''), module.params['name'] ) - + # Check if queue already exists r = requests.get( url, auth=(module.params['login_user'],module.params['login_password'])) From a2d23983c09d037b0cc318ccdca69c8302ad4c2e Mon Sep 17 00:00:00 2001 From: Russell Teague Date: Thu, 25 Aug 2016 12:48:25 -0400 Subject: [PATCH 1994/2522] Fix vmware_dvs_portgroup destroy task (#2776) Fixes #2761 --- cloud/vmware/vmware_dvs_portgroup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/vmware/vmware_dvs_portgroup.py b/cloud/vmware/vmware_dvs_portgroup.py index 7c686ba3bb0..06b39672ed1 100644 --- a/cloud/vmware/vmware_dvs_portgroup.py +++ b/cloud/vmware/vmware_dvs_portgroup.py @@ -68,7 +68,7 @@ password: vcenter_password portgroup_name: Management switch_name: dvSwitch - vlan_id: 123 + vlan_id: 123 num_ports: 120 portgroup_type: earlyBinding state: present @@ -93,7 +93,7 @@ def __init__(self, module): self.dv_switch = None self.state = self.module.params['state'] self.content = connect_to_api(module) - + def process_state(self): try: dvspg_states = { @@ -143,7 +143,7 @@ def state_destroy_dvspg(self): result = None if not self.module.check_mode: - task = dvs_portgroup.Destroy_Task() + task = self.dvs_portgroup.Destroy_Task() changed, result = wait_for_task(task) self.module.exit_json(changed=changed, result=str(result)) From f29efb56264a9ad95b97765e367ef5b7915ab877 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Thu, 25 Aug 2016 10:18:22 -0700 Subject: [PATCH 1995/2522] Adds provision info to bigip facts (#2783) This patch adds provision information to the bigip_facts module through a "provision" include. --- network/f5/bigip_facts.py | 47 +++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/network/f5/bigip_facts.py b/network/f5/bigip_facts.py index 970bb08d917..ae5c97d351b 100644 --- a/network/f5/bigip_facts.py +++ b/network/f5/bigip_facts.py @@ -33,6 +33,7 @@ - F5 developed module 'bigsuds' required (see http://devcentral.f5.com) - Best run as a local_action in your playbook - Tested with manager and above account privilege level + - C(provision) facts were added in 2.2 requirements: - bigsuds options: @@ -59,6 +60,7 @@ - key - node - pool + - provision - rule - self_ip - software @@ -72,7 +74,7 @@ filter: description: - Shell-style glob matching string used to filter fact keys. Not - applicable for software and system_info fact categories. + applicable for software, provision, and system_info fact categories. required: false default: null choices: [] @@ -1340,6 +1342,35 @@ def get_uptime(self): return self.api.System.SystemInfo.get_uptime() +class ProvisionInfo(object): + """Provision information class. + + F5 BIG-IP provision information class. + + Attributes: + api: iControl API instance. + """ + + def __init__(self, api): + self.api = api + + def get_list(self): + result = [] + list = self.api.Management.Provision.get_list() + for item in list: + item = item.lower().replace('tmos_module_', '') + result.append(item) + return result + + def get_provisioned_list(self): + result = [] + list = self.api.Management.Provision.get_provisioned_list() + for item in list: + item = item.lower().replace('tmos_module_', '') + result.append(item) + return result + + def generate_dict(api_obj, fields): result_dict = {} lists = [] @@ -1570,6 +1601,12 @@ def generate_software_list(f5): return software_list +def generate_provision_dict(f5): + provisioned = ProvisionInfo(f5.get_api()) + fields = ['list', 'provisioned_list'] + return generate_simple_dict(provisioned, fields) + + def main(): argument_spec = f5_argument_spec() @@ -1607,9 +1644,9 @@ def main(): include = map(lambda x: x.lower(), module.params['include']) valid_includes = ('address_class', 'certificate', 'client_ssl_profile', 'device', 'device_group', 'interface', 'key', 'node', - 'pool', 'rule', 'self_ip', 'software', 'system_info', - 'traffic_group', 'trunk', 'virtual_address', - 'virtual_server', 'vlan') + 'pool', 'provision', 'rule', 'self_ip', 'software', + 'system_info', 'traffic_group', 'trunk', + 'virtual_address', 'virtual_server', 'vlan') include_test = map(lambda x: x in valid_includes, include) if not all(include_test): module.fail_json(msg="value of include must be one or more of: %s, got: %s" % (",".join(valid_includes), ",".join(include))) @@ -1638,6 +1675,8 @@ def main(): facts['virtual_server'] = generate_vs_dict(f5, regex) if 'pool' in include: facts['pool'] = generate_pool_dict(f5, regex) + if 'provision' in include: + facts['provision'] = generate_provision_dict(f5) if 'device' in include: facts['device'] = generate_device_dict(f5, regex) if 'device_group' in include: From 6f1753cccd4574b26708ed87cc6422de5a4d58fa Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Fri, 26 Aug 2016 04:14:06 -0700 Subject: [PATCH 1996/2522] Moved the atomic modules to cloud/atomic. (#2784) Similar category to docker and lxd --- .travis.yml | 2 +- cloud/atomic/__init__.py | 0 {system => cloud/atomic}/atomic_host.py | 0 {system => cloud/atomic}/atomic_image.py | 0 4 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 cloud/atomic/__init__.py rename {system => cloud/atomic}/atomic_host.py (100%) rename {system => cloud/atomic}/atomic_image.py (100%) diff --git a/.travis.yml b/.travis.yml index 5c6ba09b29c..ed64b06f64f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -114,7 +114,7 @@ install: - pip install git+https://github.com/ansible/ansible.git@devel#egg=ansible - pip install git+https://github.com/sivel/ansible-testing.git#egg=ansible_testing script: - - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py|database/influxdb/influxdb.*\.py|database/mssql/mssql_db\.py|/letsencrypt\.py|network/f5/bigip.*\.py|remote_management/ipmi/.*\.py|system/atomic_.*\.py' . + - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py|database/influxdb/influxdb.*\.py|database/mssql/mssql_db\.py|/letsencrypt\.py|network/f5/bigip.*\.py|remote_management/ipmi/.*\.py|cloud/atomic/atomic_.*\.py' . - python2.6 -m compileall -fq . - python2.7 -m compileall -fq . - python3.4 -m compileall -fq . -x $(echo "$PY3_EXCLUDE_LIST"| tr ' ' '|') diff --git a/cloud/atomic/__init__.py b/cloud/atomic/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/system/atomic_host.py b/cloud/atomic/atomic_host.py similarity index 100% rename from system/atomic_host.py rename to cloud/atomic/atomic_host.py diff --git a/system/atomic_image.py b/cloud/atomic/atomic_image.py similarity index 100% rename from system/atomic_image.py rename to cloud/atomic/atomic_image.py From 825a224d38006848c54019cbbb4591fc74a5f4bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20R=C3=BCetschi?= Date: Sat, 27 Aug 2016 01:07:32 +0200 Subject: [PATCH 1997/2522] Feature udm user (#2406) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Creating directory univention * UCS udm_user: added Signed-off-by: Tobias Rüetschi * UCS udm_user: updating, added support to modify user groups Signed-off-by: Tobias Rüetschi * UCS udm_user: add key homedrive Signed-off-by: Tobias Rüetschi * UCS udm_user: add key userexpiry Signed-off-by: Tobias Rüetschi * python styling Signed-off-by: Tobias Rüetschi * UCS udm_user: updated, add supports check mode Signed-off-by: Tobias Rüetschi * UCS udm_user: updated, add support to modify users * UCS udm_user: change string formating * UCS udm_user: add type definitions to the argument specification * UCS udm_user: only modify object if it has changed * UCS udm_user: if user not exists, changed is always true * UCS udm_user: import common code for univention from ansible.module_utils.univention_umc * UCS udm_user: add a lot more attributes * UCS udm_user: add license information * UCS udm_user: fix API serviceprovider and unixhome * UCS udm_user: add documentation * univention udm_user: import only AnsibleModule from ansible.module_utils.basic * univention udm_user: reorder documentation options * univention udm_user: fix documentation * univention udm_user: dont log password * univention udm_user: add more examples --- .travis.yml | 2 +- univention/__init__.py | 0 univention/udm_user.py | 530 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 531 insertions(+), 1 deletion(-) create mode 100644 univention/__init__.py create mode 100644 univention/udm_user.py diff --git a/.travis.yml b/.travis.yml index ed64b06f64f..690f1b3609e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -114,7 +114,7 @@ install: - pip install git+https://github.com/ansible/ansible.git@devel#egg=ansible - pip install git+https://github.com/sivel/ansible-testing.git#egg=ansible_testing script: - - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py|database/influxdb/influxdb.*\.py|database/mssql/mssql_db\.py|/letsencrypt\.py|network/f5/bigip.*\.py|remote_management/ipmi/.*\.py|cloud/atomic/atomic_.*\.py' . + - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py|database/influxdb/influxdb.*\.py|database/mssql/mssql_db\.py|/letsencrypt\.py|network/f5/bigip.*\.py|remote_management/ipmi/.*\.py|cloud/atomic/atomic_.*\.py|univention/.*\.py' . - python2.6 -m compileall -fq . - python2.7 -m compileall -fq . - python3.4 -m compileall -fq . -x $(echo "$PY3_EXCLUDE_LIST"| tr ' ' '|') diff --git a/univention/__init__.py b/univention/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/univention/udm_user.py b/univention/udm_user.py new file mode 100644 index 00000000000..0654c54aacd --- /dev/null +++ b/univention/udm_user.py @@ -0,0 +1,530 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- + +# Copyright (c) 2016, Adfinis SyGroup AG +# Tobias Rueetschi +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.univention_umc import ( + umc_module_for_add, + umc_module_for_edit, + ldap_search, + base_dn, +) +from datetime import date +from dateutil.relativedelta import relativedelta +import crypt + + +DOCUMENTATION = ''' +--- +module: udm_user +version_added: "2.2" +author: "Tobias Rueetschi (@2-B)" +short_description: Manage posix users on a univention corporate server +description: + - "This module allows to manage posix users on a univention corporate server (UCS). + It uses the python API of the UCS to create a new object or edit it." +requirements: + - Python >= 2.6 +options: + state: + required: false + default: "present" + choices: [ present, absent ] + description: + - Whether the user is present or not. + username: + required: true + description: + - User name + aliases: ['name'] + firstname: + required: false + description: + - First name. Required if C(state=present). + lastname: + required: false + description: + - Last name. Required if C(state=present). + password: + required: false + default: None + description: + - Password. Required if C(state=present). + birthday: + required: false + default: None + description: + - Birthday + city: + required: false + default: None + description: + - City of users business address. + country: + required: false + default: None + description: + - Country of users business address. + departmentNumber: + required: false + default: None + description: + - Department number of users business address. + description: + required: false + default: None + description: + - Description (not gecos) + displayName: + required: false + default: None + description: + - Display name (not gecos) + email: + required: false + default: [''] + description: + - A list of e-mail addresses. + employeeNumber: + required: false + default: None + description: + - Employee number + employeeType: + required: false + default: None + description: + - Employee type + gecos: + required: false + default: None + description: + - GECOS + groups: + required: false + default: [] + description: + - "POSIX groups, the LDAP DNs of the groups will be found with the + LDAP filter for each group as $GROUP: + C((&(objectClass=posixGroup)(cn=$GROUP)))." + homeShare: + required: false + default: None + description: + - "Home NFS share. Must be a LDAP DN, e.g. + C(cn=home,cn=shares,ou=school,dc=example,dc=com)." + homeSharePath: + required: false + default: None + description: + - Path to home NFS share, inside the homeShare. + homeTelephoneNumber: + required: false + default: [] + description: + - List of private telephone numbers. + homedrive: + required: false + default: None + description: + - Windows home drive, e.g. C("H:"). + mailAlternativeAddress: + required: false + default: [] + description: + - List of alternative e-mail addresses. + mailHomeServer: + required: false + default: None + description: + - FQDN of mail server + mailPrimaryAddress: + required: false + default: None + description: + - Primary e-mail address + mobileTelephoneNumber: + required: false + default: [] + description: + - Mobile phone number + organisation: + required: false + default: None + description: + - Organisation + pagerTelephonenumber: + required: false + default: [] + description: + - List of pager telephone numbers. + phone: + required: false + default: [] + description: + - List of telephone numbers. + postcode: + required: false + default: None + description: + - Postal code of users business address. + primaryGroup: + required: false + default: cn=Domain Users,cn=groups,$LDAP_BASE_DN + description: + - Primary group. This must be the group LDAP DN. + profilepath: + required: false + default: None + description: + - Windows profile directory + pwdChangeNextLogin: + required: false + default: None + choices: [ '0', '1' ] + description: + - Change password on next login. + roomNumber: + required: false + default: None + description: + - Room number of users business address. + sambaPrivileges: + required: false + default: [] + description: + - "Samba privilege, like allow printer administration, do domain + join." + sambaUserWorkstations: + required: false + default: [] + description: + - Allow the authentication only on this Microsoft Windows host. + sambahome: + required: false + default: None + description: + - Windows home path, e.g. C('\\\\$FQDN\\$USERNAME'). + scriptpath: + required: false + default: None + description: + - Windows logon script. + secretary: + required: false + default: [] + description: + - A list of superiors as LDAP DNs. + serviceprovider: + required: false + default: [''] + description: + - Enable user for the following service providers. + shell: + required: false + default: '/bin/bash' + description: + - Login shell + street: + required: false + default: None + description: + - Street of users business address. + title: + required: false + default: None + description: + - Title, e.g. C(Prof.). + unixhome: + required: false + default: '/home/$USERNAME' + description: + - Unix home directory + userexpiry: + required: false + default: Today + 1 year + description: + - Account expiry date, e.g. C(1999-12-31). + position: + required: false + default: '' + description: + - "Define the whole position of users object inside the LDAP tree, e.g. + C(cn=employee,cn=users,ou=school,dc=example,dc=com)." + ou: + required: false + default: '' + description: + - "Organizational Unit inside the LDAP Base DN, e.g. C(school) for + LDAP OU C(ou=school,dc=example,dc=com)." + subpath: + required: false + default: 'cn=users' + description: + - "LDAP subpath inside the organizational unit, e.g. + C(cn=teachers,cn=users) for LDAP container + C(cn=teachers,cn=users,dc=example,dc=com)." +''' + + +EXAMPLES = ''' +# Create a user on a UCS +- udm_user: name=FooBar + password=secure_password + firstname=Foo + lastname=Bar + +# Create a user with the DN +# C(uid=foo,cn=teachers,cn=users,ou=school,dc=school,dc=example,dc=com) +- udm_user: name=foo + password=secure_password + firstname=Foo + lastname=Bar + ou=school + subpath='cn=teachers,cn=users' +# or define the position +- udm_user: name=foo + password=secure_password + firstname=Foo + lastname=Bar + position='cn=teachers,cn=users,ou=school,dc=school,dc=example,dc=com' +''' + + +RETURN = '''# ''' + + +def main(): + expiry = date.strftime(date.today()+relativedelta(years=1), "%Y-%m-%d") + module = AnsibleModule( + argument_spec = dict( + birthday = dict(default=None, + type='str'), + city = dict(default=None, + type='str'), + country = dict(default=None, + type='str'), + departmentNumber = dict(default=None, + type='str'), + description = dict(default=None, + type='str'), + displayName = dict(default=None, + type='str'), + email = dict(default=[''], + type='list'), + employeeNumber = dict(default=None, + type='str'), + employeeType = dict(default=None, + type='str'), + firstname = dict(default=None, + type='str'), + gecos = dict(default=None, + type='str'), + groups = dict(default=[], + type='list'), + homeShare = dict(default=None, + type='str'), + homeSharePath = dict(default=None, + type='str'), + homeTelephoneNumber = dict(default=[], + type='list'), + homedrive = dict(default=None, + type='str'), + lastname = dict(default=None, + type='str'), + mailAlternativeAddress = dict(default=[], + type='list'), + mailHomeServer = dict(default=None, + type='str'), + mailPrimaryAddress = dict(default=None, + type='str'), + mobileTelephoneNumber = dict(default=[], + type='list'), + organisation = dict(default=None, + type='str'), + pagerTelephonenumber = dict(default=[], + type='list'), + password = dict(default=None, + type='str', + no_log=True), + phone = dict(default=[], + type='list'), + postcode = dict(default=None, + type='str'), + primaryGroup = dict(default=None, + type='str'), + profilepath = dict(default=None, + type='str'), + pwdChangeNextLogin = dict(default=None, + type='str', + choices=['0', '1']), + roomNumber = dict(default=None, + type='str'), + sambaPrivileges = dict(default=[], + type='list'), + sambaUserWorkstations = dict(default=[], + type='list'), + sambahome = dict(default=None, + type='str'), + scriptpath = dict(default=None, + type='str'), + secretary = dict(default=[], + type='list'), + serviceprovider = dict(default=[''], + type='list'), + shell = dict(default='/bin/bash', + type='str'), + street = dict(default=None, + type='str'), + title = dict(default=None, + type='str'), + unixhome = dict(default=None, + type='str'), + userexpiry = dict(default=expiry, + type='str'), + username = dict(required=True, + aliases=['name'], + type='str'), + position = dict(default='', + type='str'), + ou = dict(default='', + type='str'), + subpath = dict(default='cn=users', + type='str'), + state = dict(default='present', + choices=['present', 'absent'], + type='str') + ), + supports_check_mode=True, + required_if = ([ + ('state', 'present', ['firstname', 'lastname', 'password']) + ]) + ) + username = module.params['username'] + position = module.params['position'] + ou = module.params['ou'] + subpath = module.params['subpath'] + state = module.params['state'] + changed = False + + users = list(ldap_search( + '(&(objectClass=posixAccount)(uid={}))'.format(username), + attr=['uid'] + )) + if position != '': + container = position + else: + if ou != '': + ou = 'ou={},'.format(ou) + if subpath != '': + subpath = '{},'.format(subpath) + container = '{}{}{}'.format(subpath, ou, base_dn()) + user_dn = 'uid={},{}'.format(username, container) + + exists = bool(len(users)) + + if state == 'present': + try: + if not exists: + obj = umc_module_for_add('users/user', container) + else: + obj = umc_module_for_edit('users/user', user_dn) + + if module.params['displayName'] == None: + module.params['displayName'] = '{} {}'.format( + module.params['firstname'], + module.params['lastname'] + ) + if module.params['unixhome'] == None: + module.params['unixhome'] = '/home/{}'.format( + module.params['username'] + ) + for k in obj.keys(): + if (k != 'password' and + k != 'groups' and + module.params.has_key(k) and + module.params[k] != None): + obj[k] = module.params[k] + # handle some special values + obj['e-mail'] = module.params['email'] + password = module.params['password'] + if obj['password'] == None: + obj['password'] = password + else: + old_password = obj['password'].split('}', 2)[1] + if crypt.crypt(password, old_password) != old_password: + obj['password'] = password + + diff = obj.diff() + if exists: + for k in obj.keys(): + if obj.hasChanged(k): + changed = True + else: + changed = True + if not module.check_mode: + if not exists: + obj.create() + elif changed: + obj.modify() + except: + module.fail_json( + msg="Creating/editing user {} in {} failed".format(username, container) + ) + try: + groups = module.params['groups'] + if groups: + filter = '(&(objectClass=posixGroup)(|(cn={})))'.format(')(cn='.join(groups)) + group_dns = list(ldap_search(filter, attr=['dn'])) + for dn in group_dns: + grp = umc_module_for_edit('groups/group', dn[0]) + if user_dn not in grp['users']: + grp['users'].append(user_dn) + if not module.check_mode: + grp.modify() + changed = True + except: + module.fail_json( + msg="Adding groups to user {} failed".format(username) + ) + + if state == 'absent' and exists: + try: + obj = umc_module_for_edit('users/user', user_dn) + if not module.check_mode: + obj.remove() + changed = True + except: + module.fail_json( + msg="Removing user {} failed".format(username) + ) + + module.exit_json( + changed=changed, + username=username, + diff=diff, + container=container + ) + + +if __name__ == '__main__': + main() From 464cbb89f225578386a830624633a55e39054544 Mon Sep 17 00:00:00 2001 From: Ethan Devenport Date: Mon, 29 Aug 2016 05:56:26 +0000 Subject: [PATCH 1998/2522] Added support for firewall rules, consolidated resource UUID retrieval methods for server and NIC modules, and set LAN type to int. --- cloud/profitbricks/profitbricks.py | 58 ++- .../profitbricks_firewall_rule.py | 346 ++++++++++++++++++ cloud/profitbricks/profitbricks_nic.py | 105 +++--- 3 files changed, 414 insertions(+), 95 deletions(-) create mode 100644 cloud/profitbricks/profitbricks_firewall_rule.py diff --git a/cloud/profitbricks/profitbricks.py b/cloud/profitbricks/profitbricks.py index b0791f37e3a..0f4e8ff6fa2 100644 --- a/cloud/profitbricks/profitbricks.py +++ b/cloud/profitbricks/profitbricks.py @@ -195,14 +195,15 @@ ''' -import re import uuid import time HAS_PB_SDK = True try: - from profitbricks.client import ProfitBricksService, Volume, Server, Datacenter, NIC, LAN + from profitbricks.client import ( + ProfitBricksService, Volume, Server, Datacenter, NIC, LAN + ) except ImportError: HAS_PB_SDK = False @@ -210,9 +211,6 @@ 'de/fra', 'de/fkb'] -uuid_match = re.compile( - '[\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12}', re.I) - def _wait_for_completion(profitbricks, promise, wait_timeout, msg): if not promise: return @@ -223,9 +221,9 @@ def _wait_for_completion(profitbricks, promise, wait_timeout, msg): request_id=promise['requestId'], status=True) - if operation_result['metadata']['status'] == "DONE": + if operation_result['metadata']['status'] == 'DONE': return - elif operation_result['metadata']['status'] == "FAILED": + elif operation_result['metadata']['status'] == 'FAILED': raise Exception( 'Request failed to complete ' + msg + ' "' + str( promise['requestId']) + '" to complete.') @@ -245,6 +243,7 @@ def _create_machine(module, profitbricks, datacenter, name): image_password = module.params.get('image_password') ssh_keys = module.params.get('ssh_keys') bus = module.params.get('bus') + nic_name = module.params.get('nic_name') lan = module.params.get('lan') assign_public_ip = module.params.get('assign_public_ip') subscription_user = module.params.get('subscription_user') @@ -284,6 +283,7 @@ def _create_machine(module, profitbricks, datacenter, name): bus=bus) n = NIC( + name=nic_name, lan=int(lan) ) @@ -311,6 +311,7 @@ def _create_machine(module, profitbricks, datacenter, name): except Exception as e: module.fail_json(msg="failed to create the new server: %s" % str(e)) else: + server_response['nic'] = server_response['entities']['nics']['items'][0] return server_response @@ -373,7 +374,7 @@ def create_virtual_machine(module, profitbricks): # Locate UUID for datacenter if referenced by name. datacenter_list = profitbricks.list_datacenters() - datacenter_id = _get_datacenter_id(datacenter_list, datacenter) + datacenter_id = _get_resource_id(datacenter_list, datacenter) if datacenter_id: datacenter_found = True @@ -409,14 +410,13 @@ def create_virtual_machine(module, profitbricks): server_list = profitbricks.list_servers(datacenter_id) for name in names: # Skip server creation if the server already exists. - if _get_server_id(server_list, name): + if _get_resource_id(server_list, name): continue create_response = _create_machine(module, profitbricks, str(datacenter_id), name) - nics = profitbricks.list_nics(datacenter_id, create_response['id']) - for n in nics['items']: - if lan == n['properties']['lan']: - create_response.update({'public_ip': n['properties']['ips'][0]}) + for nic in create_response['entities']['nics']['items']: + if lan == nic['properties']['lan']: + create_response.update({'public_ip': nic['properties']['ips'][0]}) virtual_machines.append(create_response) @@ -458,7 +458,7 @@ def remove_virtual_machine(module, profitbricks): # Locate UUID for datacenter if referenced by name. datacenter_list = profitbricks.list_datacenters() - datacenter_id = _get_datacenter_id(datacenter_list, datacenter) + datacenter_id = _get_resource_id(datacenter_list, datacenter) if not datacenter_id: module.fail_json(msg='Virtual data center \'%s\' not found.' % str(datacenter)) @@ -466,7 +466,7 @@ def remove_virtual_machine(module, profitbricks): server_list = profitbricks.list_servers(datacenter_id) for instance in instance_ids: # Locate UUID for server if referenced by name. - server_id = _get_server_id(server_list, instance) + server_id = _get_resource_id(server_list, instance) if server_id: # Remove the server's boot volume if remove_boot_volume: @@ -517,7 +517,7 @@ def startstop_machine(module, profitbricks, state): # Locate UUID for datacenter if referenced by name. datacenter_list = profitbricks.list_datacenters() - datacenter_id = _get_datacenter_id(datacenter_list, datacenter) + datacenter_id = _get_resource_id(datacenter_list, datacenter) if not datacenter_id: module.fail_json(msg='Virtual data center \'%s\' not found.' % str(datacenter)) @@ -525,7 +525,7 @@ def startstop_machine(module, profitbricks, state): server_list = profitbricks.list_servers(datacenter_id) for instance in instance_ids: # Locate UUID of server if referenced by name. - server_id = _get_server_id(server_list, instance) + server_id = _get_resource_id(server_list, instance) if server_id: _startstop_machine(module, profitbricks, datacenter_id, server_id) changed = True @@ -554,23 +554,14 @@ def startstop_machine(module, profitbricks, state): return (changed) -def _get_datacenter_id(datacenters, identity): - """ - Fetch and return datacenter UUID by datacenter name if found. - """ - for datacenter in datacenters['items']: - if identity in (datacenter['properties']['name'], datacenter['id']): - return datacenter['id'] - return None - - -def _get_server_id(servers, identity): +def _get_resource_id(resources, identity): """ - Fetch and return server UUID by server name if found. + Fetch and return the UUID of a resource regardless of whether the name or + UUID is passed. """ - for server in servers['items']: - if identity in (server['properties']['name'], server['id']): - return server['id'] + for resource in resources['items']: + if identity in (resource['properties']['name'], resource['id']): + return resource['id'] return None @@ -588,7 +579,8 @@ def main(): image_password=dict(default=None), ssh_keys=dict(type='list', default=[]), bus=dict(default='VIRTIO'), - lan=dict(default=1), + nic_name=dict(default=str(uuid.uuid4()).replace('-', '')[:10]), + lan=dict(type='int', default=1), count=dict(type='int', default=1), auto_increment=dict(type='bool', default=True), instance_ids=dict(type='list', default=[]), diff --git a/cloud/profitbricks/profitbricks_firewall_rule.py b/cloud/profitbricks/profitbricks_firewall_rule.py new file mode 100644 index 00000000000..902d6f94cc4 --- /dev/null +++ b/cloud/profitbricks/profitbricks_firewall_rule.py @@ -0,0 +1,346 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: profitbricks_firewall_rule +short_description: Create or remove a firewall rule. +description: + - This module allows you to create or remove a firewlal rule. This module has a dependency on profitbricks >= 1.0.0 +version_added: "2.0" +options: + datacenter: + description: + - The datacenter name or UUID in which to operate. + required: true + server: + description: + - The server name or UUID. + required: true + nic: + description: + - The NIC name or UUID. + required: true + name: + description: + - The name or UUID of the firewall rule. + required: false + protocol + description: + - The protocol for the firewall rule. + choices: [ "TCP", "UDP", "ICMP" ] + required: true + mac_source + description: + - Only traffic originating from the respective MAC address is allowed. Valid format: aa:bb:cc:dd:ee:ff. No value allows all source MAC addresses. + required: false + source_ip + description: + - Only traffic originating from the respective IPv4 address is allowed. No value allows all source IPs. + required: false + target_ip + description: + - In case the target NIC has multiple IP addresses, only traffic directed to the respective IP address of the NIC is allowed. No value allows all target IPs. + required: false + port_range_start + description: + - Defines the start range of the allowed port (from 1 to 65534) if protocol TCP or UDP is chosen. Leave value empty to allow all ports. + required: false + port_range_end + description: + - Defines the end range of the allowed port (from 1 to 65534) if the protocol TCP or UDP is chosen. Leave value empty to allow all ports. + required: false + icmp_type + description: + - Defines the allowed type (from 0 to 254) if the protocol ICMP is chosen. No value allows all types. + required: false + icmp_code + description: + - Defines the allowed code (from 0 to 254) if protocol ICMP is chosen. No value allows all codes. + required: false + subscription_user: + description: + - The ProfitBricks username. Overrides the PB_SUBSCRIPTION_ID environement variable. + required: false + subscription_password: + description: + - THe ProfitBricks password. Overrides the PB_PASSWORD environement variable. + required: false + wait: + description: + - wait for the operation to complete before returning + required: false + default: "yes" + choices: [ "yes", "no" ] + wait_timeout: + description: + - how long before wait gives up, in seconds + default: 600 + state: + description: + - Indicate desired state of the resource + required: false + default: 'present' + choices: ["present", "absent"] + +requirements: [ "profitbricks" ] +author: Ethan Devenport (ethand@stackpointcloud.com) +''' + +EXAMPLES = ''' + +# Create a firewall rule +- name: Create SSH firewall rule + profitbricks_firewall_rule: + datacenter: Virtual Datacenter + server: node001 + nic: 7341c2454f + name: Allow SSH + protocol: TCP + source_ip: 0.0.0.0 + port_range_start: 22 + port_range_end: 22 + state: present + +- name: Create ping firewall rule + profitbricks_firewall_rule: + datacenter: Virtual Datacenter + server: node001 + nic: 7341c2454f + name: Allow Ping + protocol: ICMP + source_ip: 0.0.0.0 + icmp_type: 8 + icmp_code: 0 + state: present + +# Remove a firewall rule +- name: Remove public ping firewall rule + profitbricks_firewall_rule: + datacenter: Virtual Datacenter + server: node001 + nic: aa6c261b9c + name: Allow Ping + state: absent +''' + +# import uuid +import time + +HAS_PB_SDK = True + +try: + from profitbricks.client import ProfitBricksService, FirewallRule +except ImportError: + HAS_PB_SDK = False + + +def _wait_for_completion(profitbricks, promise, wait_timeout, msg): + if not promise: return + wait_timeout = time.time() + wait_timeout + while wait_timeout > time.time(): + time.sleep(5) + operation_result = profitbricks.get_request( + request_id=promise['requestId'], + status=True) + + if operation_result['metadata']['status'] == 'DONE': + return + elif operation_result['metadata']['status'] == 'FAILED': + raise Exception( + 'Request failed to complete ' + msg + ' "' + str( + promise['requestId']) + '" to complete.') + + raise Exception( + 'Timed out waiting for async operation ' + msg + ' "' + str( + promise['requestId'] + ) + '" to complete.') + + +def create_firewall_rule(module, profitbricks): + """ + Creates a firewall rule. + + module : AnsibleModule object + profitbricks: authenticated profitbricks object. + + Returns: + True if the firewal rule creates, false otherwise + """ + datacenter = module.params.get('datacenter') + server = module.params.get('server') + nic = module.params.get('nic') + name = module.params.get('name') + protocol = module.params.get('protocol') + source_mac = module.params.get('source_mac') + source_ip = module.params.get('source_ip') + target_ip = module.params.get('target_ip') + port_range_start = module.params.get('port_range_start') + port_range_end = module.params.get('port_range_end') + icmp_type = module.params.get('icmp_type') + icmp_code = module.params.get('icmp_code') + wait = module.params.get('wait') + wait_timeout = module.params.get('wait_timeout') + + # Locate UUID for virtual datacenter + datacenter_list = profitbricks.list_datacenters() + datacenter_id = _get_resource_id(datacenter_list, datacenter) + if not datacenter_id: + module.fail_json(msg='Virtual data center \'%s\' not found.' % str(datacenter)) + + # Locate UUID for server + server_list = profitbricks.list_servers(datacenter_id) + server_id = _get_resource_id(server_list, server) + + # Locate UUID for NIC + nic_list = profitbricks.list_nics(datacenter_id, server_id) + nic_id = _get_resource_id(nic_list, nic) + + try: + profitbricks.update_nic(datacenter_id, server_id, nic_id, + firewall_active=True) + except Exception as e: + module.fail_json(msg='Unable to activate the NIC firewall.' % str(e)) + + f = FirewallRule( + name=name, + protocol=protocol, + mac_source=source_mac, + source_ip=source_ip, + target_ip=target_ip, + port_range_start=port_range_start, + port_range_end=port_range_end, + icmp_type=icmp_type, + icmp_code=icmp_code + ) + + try: + firewall_rule_response = profitbricks.create_firewall_rule( + datacenter_id, server_id, nic_id, f + ) + + if wait: + _wait_for_completion(profitbricks, firewall_rule_response, + wait_timeout, "create_firewall_rule") + return firewall_rule_response + + except Exception as e: + module.fail_json(msg="failed to create the firewall rule: %s" % str(e)) + + +def delete_firewall_rule(module, profitbricks): + """ + Removes a firewall rule + + module : AnsibleModule object + profitbricks: authenticated profitbricks object. + + Returns: + True if the firewall rule was removed, false otherwise + """ + datacenter = module.params.get('datacenter') + server = module.params.get('server') + nic = module.params.get('nic') + name = module.params.get('name') + + # Locate UUID for virtual datacenter + datacenter_list = profitbricks.list_datacenters() + datacenter_id = _get_resource_id(datacenter_list, datacenter) + + # Locate UUID for server + server_list = profitbricks.list_servers(datacenter_id) + server_id = _get_resource_id(server_list, server) + + # Locate UUID for NIC + nic_list = profitbricks.list_nics(datacenter_id, server_id) + nic_id = _get_resource_id(nic_list, nic) + + # Locate UUID for firewall rule + firewall_rule_list = profitbricks.get_firewall_rules(datacenter_id, server_id, nic_id) + firewall_rule_id = _get_resource_id(firewall_rule_list, name) + + try: + firewall_rule_response = profitbricks.delete_firewall_rule( + datacenter_id, server_id, nic_id, firewall_rule_id + ) + return firewall_rule_response + except Exception as e: + module.fail_json(msg="failed to remove the firewall rule: %s" % str(e)) + + +def _get_resource_id(resource_list, identity): + """ + Fetch and return the UUID of a resource regardless of whether the name or + UUID is passed. + """ + for resource in resource_list['items']: + if identity in (resource['properties']['name'], resource['id']): + return resource['id'] + return None + + +def main(): + module = AnsibleModule( + argument_spec=dict( + datacenter=dict(type='str', required=True), + server=dict(type='str', required=True), + nic=dict(type='str', required=True), + name=dict(type='str', required=True), + protocol=dict(type='str', required=False), + source_mac=dict(type='str', default=None), + source_ip=dict(type='str', default=None), + target_ip=dict(type='str', default=None), + port_range_start=dict(type='int', default=None), + port_range_end=dict(type='int', default=None), + icmp_type=dict(type='int', default=None), + icmp_code=dict(type='int', default=None), + subscription_user=dict(type='str', required=True), + subscription_password=dict(type='str', required=True), + wait=dict(type='bool', default=True), + wait_timeout=dict(type='int', default=600), + state=dict(default='present'), + ) + ) + + if not HAS_PB_SDK: + module.fail_json(msg='profitbricks required for this module') + + subscription_user = module.params.get('subscription_user') + subscription_password = module.params.get('subscription_password') + + profitbricks = ProfitBricksService( + username=subscription_user, + password=subscription_password) + + state = module.params.get('state') + + if state == 'absent': + try: + (changed) = delete_firewall_rule(module, profitbricks) + module.exit_json(changed=changed) + except Exception as e: + module.fail_json(msg='failed to set firewall rule state: %s' % str(e)) + + elif state == 'present': + try: + (firewall_rule_dict) = create_firewall_rule(module, profitbricks) + module.exit_json(firewall_rules=firewall_rule_dict) + except Exception as e: + module.fail_json(msg='failed to set firewall rules state: %s' % str(e)) + +from ansible.module_utils.basic import * + +main() diff --git a/cloud/profitbricks/profitbricks_nic.py b/cloud/profitbricks/profitbricks_nic.py index 902d5266843..d13554cf43a 100644 --- a/cloud/profitbricks/profitbricks_nic.py +++ b/cloud/profitbricks/profitbricks_nic.py @@ -84,23 +84,18 @@ name: 7341c2454f wait_timeout: 500 state: absent - ''' -import re import uuid import time HAS_PB_SDK = True try: - from profitbricks.client import ProfitBricksService, NIC + from profitbricks.client import ProfitBricksService, NIC, FirewallRule except ImportError: HAS_PB_SDK = False -uuid_match = re.compile( - '[\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12}', re.I) - def _wait_for_completion(profitbricks, promise, wait_timeout, msg): if not promise: return @@ -111,9 +106,9 @@ def _wait_for_completion(profitbricks, promise, wait_timeout, msg): request_id=promise['requestId'], status=True) - if operation_result['metadata']['status'] == "DONE": + if operation_result['metadata']['status'] == 'DONE': return - elif operation_result['metadata']['status'] == "FAILED": + elif operation_result['metadata']['status'] == 'FAILED': raise Exception( 'Request failed to complete ' + msg + ' "' + str( promise['requestId']) + '" to complete.') @@ -123,6 +118,7 @@ def _wait_for_completion(profitbricks, promise, wait_timeout, msg): promise['requestId'] ) + '" to complete.') + def create_nic(module, profitbricks): """ Creates a NIC. @@ -141,28 +137,22 @@ def create_nic(module, profitbricks): wait_timeout = module.params.get('wait_timeout') # Locate UUID for Datacenter - if not (uuid_match.match(datacenter)): - datacenter_list = profitbricks.list_datacenters() - for d in datacenter_list['items']: - dc = profitbricks.get_datacenter(d['id']) - if datacenter == dc['properties']['name']: - datacenter = d['id'] - break + datacenter_list = profitbricks.list_datacenters() + datacenter_id = _get_resource_id(datacenter_list, datacenter) + if not datacenter_id: + module.fail_json(msg='Virtual data center \'%s\' not found.' % str(datacenter)) # Locate UUID for Server - if not (uuid_match.match(server)): - server_list = profitbricks.list_servers(datacenter) - for s in server_list['items']: - if server == s['properties']['name']: - server = s['id'] - break - try: - n = NIC( - name=name, - lan=lan - ) + server_list = profitbricks.list_servers(datacenter_id) + server_id = _get_resource_id(server_list, server) + + n = NIC( + name=name, + lan=lan + ) - nic_response = profitbricks.create_nic(datacenter, server, n) + try: + nic_response = profitbricks.create_nic(datacenter_id, server_id, n) if wait: _wait_for_completion(profitbricks, nic_response, @@ -173,6 +163,7 @@ def create_nic(module, profitbricks): except Exception as e: module.fail_json(msg="failed to create the NIC: %s" % str(e)) + def delete_nic(module, profitbricks): """ Removes a NIC @@ -188,53 +179,44 @@ def delete_nic(module, profitbricks): name = module.params.get('name') # Locate UUID for Datacenter - if not (uuid_match.match(datacenter)): - datacenter_list = profitbricks.list_datacenters() - for d in datacenter_list['items']: - dc = profitbricks.get_datacenter(d['id']) - if datacenter == dc['properties']['name']: - datacenter = d['id'] - break + datacenter_list = profitbricks.list_datacenters() + datacenter_id = _get_resource_id(datacenter_list, datacenter) + if not datacenter_id: + module.fail_json(msg='Virtual data center \'%s\' not found.' % str(datacenter)) # Locate UUID for Server - server_found = False - if not (uuid_match.match(server)): - server_list = profitbricks.list_servers(datacenter) - for s in server_list['items']: - if server == s['properties']['name']: - server_found = True - server = s['id'] - break - - if not server_found: - return False + server_list = profitbricks.list_servers(datacenter_id) + server_id = _get_resource_id(server_list, server) # Locate UUID for NIC - nic_found = False - if not (uuid_match.match(name)): - nic_list = profitbricks.list_nics(datacenter, server) - for n in nic_list['items']: - if name == n['properties']['name']: - nic_found = True - name = n['id'] - break - - if not nic_found: - return False + nic_list = profitbricks.list_nics(datacenter_id, server_id) + nic_id = _get_resource_id(nic_list, name) try: - nic_response = profitbricks.delete_nic(datacenter, server, name) + nic_response = profitbricks.delete_nic(datacenter_id, server_id, nic_id) return nic_response except Exception as e: module.fail_json(msg="failed to remove the NIC: %s" % str(e)) + +def _get_resource_id(resource_list, identity): + """ + Fetch and return the UUID of a resource regardless of whether the name or + UUID is passed. + """ + for resource in resource_list['items']: + if identity in (resource['properties']['name'], resource['id']): + return resource['id'] + return None + + def main(): module = AnsibleModule( argument_spec=dict( datacenter=dict(), server=dict(), - name=dict(default=str(uuid.uuid4()).replace('-','')[:10]), - lan=dict(), + name=dict(default=str(uuid.uuid4()).replace('-', '')[:10]), + lan=dict(type='int'), subscription_user=dict(), subscription_password=dict(), wait=dict(type='bool', default=True), @@ -255,7 +237,6 @@ def main(): if not module.params.get('server'): module.fail_json(msg='server parameter is required') - subscription_user = module.params.get('subscription_user') subscription_password = module.params.get('subscription_password') @@ -281,10 +262,10 @@ def main(): try: (nic_dict) = create_nic(module, profitbricks) - module.exit_json(nics=nic_dict) + module.exit_json(nic=nic_dict) except Exception as e: module.fail_json(msg='failed to set nic state: %s' % str(e)) from ansible.module_utils.basic import * -main() \ No newline at end of file +main() From ea46cbef347457bb0895ce258459de76fad8a476 Mon Sep 17 00:00:00 2001 From: Ethan Devenport Date: Mon, 29 Aug 2016 06:18:25 +0000 Subject: [PATCH 1999/2522] Minor documentation corrections. --- cloud/profitbricks/profitbricks.py | 4 ++++ cloud/profitbricks/profitbricks_firewall_rule.py | 16 ++++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/cloud/profitbricks/profitbricks.py b/cloud/profitbricks/profitbricks.py index 0f4e8ff6fa2..f8d6eb78fd0 100644 --- a/cloud/profitbricks/profitbricks.py +++ b/cloud/profitbricks/profitbricks.py @@ -98,6 +98,10 @@ - This will assign the machine to the public LAN. If no LAN exists with public Internet access it is created. required: false default: false + nic_name: + description: + - The name of the default NIC. + required: false lan: description: - The ID of the LAN you wish to add the servers to. diff --git a/cloud/profitbricks/profitbricks_firewall_rule.py b/cloud/profitbricks/profitbricks_firewall_rule.py index 902d6f94cc4..f04d2a7f2b1 100644 --- a/cloud/profitbricks/profitbricks_firewall_rule.py +++ b/cloud/profitbricks/profitbricks_firewall_rule.py @@ -38,36 +38,36 @@ description: - The name or UUID of the firewall rule. required: false - protocol + protocol: description: - The protocol for the firewall rule. choices: [ "TCP", "UDP", "ICMP" ] required: true - mac_source + mac_source: description: - Only traffic originating from the respective MAC address is allowed. Valid format: aa:bb:cc:dd:ee:ff. No value allows all source MAC addresses. required: false - source_ip + source_ip: description: - Only traffic originating from the respective IPv4 address is allowed. No value allows all source IPs. required: false - target_ip + target_ip: description: - In case the target NIC has multiple IP addresses, only traffic directed to the respective IP address of the NIC is allowed. No value allows all target IPs. required: false - port_range_start + port_range_start: description: - Defines the start range of the allowed port (from 1 to 65534) if protocol TCP or UDP is chosen. Leave value empty to allow all ports. required: false - port_range_end + port_range_end: description: - Defines the end range of the allowed port (from 1 to 65534) if the protocol TCP or UDP is chosen. Leave value empty to allow all ports. required: false - icmp_type + icmp_type: description: - Defines the allowed type (from 0 to 254) if the protocol ICMP is chosen. No value allows all types. required: false - icmp_code + icmp_code: description: - Defines the allowed code (from 0 to 254) if protocol ICMP is chosen. No value allows all codes. required: false From 681ad6f2cc47e86b3d28483c3e8bfe20e21016c6 Mon Sep 17 00:00:00 2001 From: Ethan Devenport Date: Mon, 29 Aug 2016 06:30:54 +0000 Subject: [PATCH 2000/2522] Some further documentation updates including version. --- cloud/profitbricks/profitbricks.py | 3 ++- cloud/profitbricks/profitbricks_firewall_rule.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cloud/profitbricks/profitbricks.py b/cloud/profitbricks/profitbricks.py index f8d6eb78fd0..f3016fd13ca 100644 --- a/cloud/profitbricks/profitbricks.py +++ b/cloud/profitbricks/profitbricks.py @@ -102,6 +102,7 @@ description: - The name of the default NIC. required: false + version_added: '2.2' lan: description: - The ID of the LAN you wish to add the servers to. @@ -114,7 +115,7 @@ default: null subscription_password: description: - - THe ProfitBricks password. Overrides the PB_PASSWORD environement variable. + - The ProfitBricks password. Overrides the PB_PASSWORD environement variable. required: false default: null wait: diff --git a/cloud/profitbricks/profitbricks_firewall_rule.py b/cloud/profitbricks/profitbricks_firewall_rule.py index f04d2a7f2b1..b28b6a357b2 100644 --- a/cloud/profitbricks/profitbricks_firewall_rule.py +++ b/cloud/profitbricks/profitbricks_firewall_rule.py @@ -20,7 +20,7 @@ short_description: Create or remove a firewall rule. description: - This module allows you to create or remove a firewlal rule. This module has a dependency on profitbricks >= 1.0.0 -version_added: "2.0" +version_added: "2.2" options: datacenter: description: @@ -45,7 +45,7 @@ required: true mac_source: description: - - Only traffic originating from the respective MAC address is allowed. Valid format: aa:bb:cc:dd:ee:ff. No value allows all source MAC addresses. + - Only traffic originating from the respective MAC address is allowed. No value allows all source MAC addresses. required: false source_ip: description: From 88dd7fd2503281c85890a68d3100594664fd7bf2 Mon Sep 17 00:00:00 2001 From: Ethan Devenport Date: Mon, 29 Aug 2016 06:50:35 +0000 Subject: [PATCH 2001/2522] Added RETURN documentation. --- .../profitbricks_firewall_rule.py | 59 ++++++++++++++++++- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/cloud/profitbricks/profitbricks_firewall_rule.py b/cloud/profitbricks/profitbricks_firewall_rule.py index b28b6a357b2..1fd5a3a8404 100644 --- a/cloud/profitbricks/profitbricks_firewall_rule.py +++ b/cloud/profitbricks/profitbricks_firewall_rule.py @@ -43,7 +43,7 @@ - The protocol for the firewall rule. choices: [ "TCP", "UDP", "ICMP" ] required: true - mac_source: + source_mac: description: - Only traffic originating from the respective MAC address is allowed. No value allows all source MAC addresses. required: false @@ -101,7 +101,6 @@ ''' EXAMPLES = ''' - # Create a firewall rule - name: Create SSH firewall rule profitbricks_firewall_rule: @@ -137,6 +136,60 @@ state: absent ''' +RETURN = ''' +--- +id: + description: UUID of the firewall rule. + returned: success + type: string + sample: be60aa97-d9c7-4c22-bebe-f5df7d6b675d +name: + description: Name of the firwall rule. + returned: success + type: string + sample: Allow SSH +protocol: + description: Protocol of the firewall rule. + returned: success + type: string + sample: TCP +source_mac: + description: MAC address of the firewall rule. + returned: success + type: string + sample: 02:01:97:d7:ed:49 +source_ip: + description: Source IP of the firewall rule. + returned: success + type: string + sample: tcp +target_ip: + description: Target IP of the firewal rule. + returned: success + type: string + sample: 10.0.0.1 +port_range_start: + description: Start port of the firewall rule. + returned: success + type: int + sample: 80 +port_range_end: + description: End port of the firewall rule. + returned: success + type: int + sample: 80 +icmp_type: + description: ICMP type of the firewall rule. + returned: success + type: int + sample: 8 +icmp_code: + description: ICMP code of the firewall rule. + returned: success + type: int + sample: 0 +''' + # import uuid import time @@ -218,7 +271,7 @@ def create_firewall_rule(module, profitbricks): f = FirewallRule( name=name, protocol=protocol, - mac_source=source_mac, + source_mac=source_mac, source_ip=source_ip, target_ip=target_ip, port_range_start=port_range_start, From 6521ad31b1f18d7d4e0b3bab4a68858bcaa654c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20R=C3=BCetschi?= Date: Tue, 24 May 2016 17:02:17 +0200 Subject: [PATCH 2002/2522] UCS udm_share: added --- univention/udm_share.py | 367 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 367 insertions(+) create mode 100644 univention/udm_share.py diff --git a/univention/udm_share.py b/univention/udm_share.py new file mode 100644 index 00000000000..a046f3e30d4 --- /dev/null +++ b/univention/udm_share.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +"""UCS access module""" + +import univention.uldap +import univention.config_registry +import univention.admin.uldap +import univention.admin.objects +import univention.admin.config +import re +import thread +import time +import ldap as orig_ldap +import socket + +__all__ = [ + 'ldap_search', + 'config_registry', + 'base_dn', + 'ldap', + 'config', + 'position_base_dn', + 'get_umc_admin_objects', +] + +config_registry = univention.config_registry.ConfigRegistry() +config_registry.load() +base_dn = config_registry["ldap/base"] + +try: + secret_file = open('/etc/ldap.secret', 'r') + bind_dn = 'cn=admin,{}'.format(base_dn) +except IOError: # pragma: no cover + secret_file = open('/etc/machine.secret', 'r') + bind_dn = config_registry["ldap/hostdn"] +pwd_line = secret_file.readline() +pwd = re.sub('\n', '', pwd_line) + +ldap = univention.admin.uldap.access( + host = config_registry['ldap/master'], + base = base_dn, + binddn = bind_dn, + bindpw = pwd, + start_tls = 1 +) +config = univention.admin.config.config() +univention.admin.modules.update() +position_base_dn = univention.admin.uldap.position(base_dn) +modules_by_name = {} + + +def ldap_dn_tree_parent(dn, count=1): + dn_array = dn.split(',') + dn_array[0:count] = [] + return ','.join(dn_array) + + +def ldap_search(filter, base=base_dn, attr=None): + """Replaces uldaps search and uses a generator. + !! Arguments are not the same.""" + msgid = ldap.lo.lo.search( + base, + orig_ldap.SCOPE_SUBTREE, + filterstr=filter, + attrlist=attr + ) + # I used to have a try: finally: here but there seems to be a bug in python + # which swallows the KeyboardInterrupt + # The abandon now doesn't make too much sense + while True: + result_type, result_data = ldap.lo.lo.result(msgid, all=0) + if not result_data: + break + if result_type is orig_ldap.RES_SEARCH_RESULT: # pragma: no cover + break + else: + if result_type is orig_ldap.RES_SEARCH_ENTRY: + for res in result_data: + yield res + ldap.lo.lo.abandon(msgid) + + +def module_name(module_name_): + """Returns an initialized UMC module, identified by the given name. + + The module is a module specification according to the udm commandline. + Example values are: + * users/user + * shares/share + * groups/group + + If the module does not exist, a KeyError is raised. + + The modules are cached, so they won't be re-initialized + in subsequent calls. + """ + + if module_name_ not in modules_by_name: + module = univention.admin.modules.get(module_name_) + univention.admin.modules.init(ldap, position_base_dn, module) + + modules_by_name[module_name_] = module + + return modules_by_name[module_name_] + + +def get_umc_admin_objects(): + """Convenience accessor for getting univention.admin.objects. + + This implements delayed importing, so the univention.* modules + are not loaded until this function is called. + """ + return univention.admin.objects + + +def umc_module_for_add(module, container_dn, superordinate=None): + """Returns an UMC module object prepared for creating a new entry. + + The module is a module specification according to the udm commandline. + Example values are: + * users/user + * shares/share + * groups/group + + The container_dn MUST be the dn of the container (not of the object to + be created itself!). + """ + mod = module_name(module) + + position = position_base_dn + position.setDn(container_dn) + + # config, ldap objects from common module + obj = mod.object(config, ldap, position, superordinate=superordinate) + obj.open() + + return obj + + +def umc_module_for_edit(module, object_dn, superordinate=None): + """Returns an UMC module object prepared for editing an existing entry. + + The module is a module specification according to the udm commandline. + Example values are: + * users/user + * shares/share + * groups/group + + The object_dn MUST be the dn of the object itself, not the container! + """ + mod = module_name(module) + + objects = get_umc_admin_objects() + + position = position_base_dn + position.setDn(ldap_dn_tree_parent(object_dn)) + + obj = objects.get( + mod, + config, + ldap, + position=position, + superordinate=superordinate, + dn=object_dn + ) + obj.open() + + return obj + + +def create_containers_and_parents(container_dn): + """Create a container and if needed the parents containers""" + import univention.admin.uexceptions as uexcp + assert container_dn.startswith("cn=") + try: + parent = ldap_dn_tree_parent(container_dn) + obj = umc_module_for_add( + 'container/cn', + parent + ) + obj['name'] = container_dn.split(',')[0].split('=')[1] + obj['description'] = "container created by import" + except uexcp.ldapError: + create_containers_and_parents(parent) + obj = umc_module_for_add( + 'container/cn', + parent + ) + obj['name'] = container_dn.split(',')[0].split('=')[1] + obj['description'] = "container created by import" + + +def main(): + module = AnsibleModule( + argument_spec = dict( + name = dict(required=True, + type='str'), + ou = dict(required=True, + type='str'), + owner = dict(type='str', + default='0'), + group = dict(type='str', + default='0'), + path = dict(type='path', + default=None), + directorymode = dict(type='str', + default='00755'), + host = dict(type='str', + default=None), + root_squash = dict(type='str', + default='1'), + subtree_checking = dict(type='str', + default='1'), + sync = dict(type='str', + default='sync'), + writeable = dict(type='str', + default='1'), + sambaBlockSize = dict(type='str', + default=None), + sambaBlockingLocks = dict(type='str', + default='1'), + sambaBrowseable = dict(type='str', + default='1'), + sambaCreateMode = dict(type='str', + default='0744'), + sambaCscPolicy = dict(type='str', + default='manual'), + sambaCustomSettings = dict(type='list', + default=[]), + sambaDirectoryMode = dict(type='str', + default='0755'), + sambaDirectorySecurityMode = dict(type='str', + default='0777'), + sambaDosFilemode = dict(type='str', + default='0'), + sambaFakeOplocks = dict(type='str', + default='0'), + sambaForceCreateMode = dict(type='str', + default='0'), + sambaForceDirectoryMode = dict(type='str', + default='0'), + sambaForceDirectorySecurityMode = dict(type='str', + default='0'), + sambaForceGroup = dict(type='str', + default=None), + sambaForceSecurityMode = dict(type='str', + default='0'), + sambaForceUser = dict(type='str', + default=None), + sambaHideFiles = dict(type='str', + default=None), + sambaHideUnreadable = dict(type='str', + default='0'), + sambaHostsAllow = dict(type='list', + default=[]), + sambaHostsDeny = dict(type='list', + default=[]), + sambaInheritAcls = dict(type='str', + default='1'), + sambaInheritOwner = dict(type='str', + default='0'), + sambaInheritPermissions = dict(type='str', + default='0'), + sambaInvalidUsers = dict(type='str', + default=None), + sambaLevel2Oplocks = dict(type='str', + default='1'), + sambaLocking = dict(type='str', + default='1'), + sambaMSDFSRoot = dict(type='str', + default='0'), + sambaName = dict(type='str', + default=''), + sambaNtAclSupport = dict(type='str', + default='1'), + sambaOplocks = dict(type='str', + default='1'), + sambaPostexec = dict(type='str', + default=None), + sambaPreexec = dict(type='str', + default=None), + sambaPublic = dict(type='str', + default='0'), + sambaSecurityMode = dict(type='str', + default='0777'), + sambaStrictLocking = dict(type='str', + default='Auto'), + sambaVFSObjects = dict(type='str', + default=None), + sambaValidUsers = dict(type='str', + default=None), + sambaWriteList = dict(type='str', + default=None), + sambaWriteable = dict(type='str', + default='1'), + nfs_hosts = dict(type='list', + default=[]), + nfsCustomSettings = dict(type='list', + default=[]), + state = dict(default='present', + choices=['present', 'absent'], + type='str') + ), + supports_check_mode=True + ) + name = module.params['name'] + state = module.params['state'] + changed = False + + obj = list(ldap_search( + '(&(objectClass=univentionShare)(cn={}))'.format(name), + attr=['cn'] + )) + + exists = bool(len(obj)) + container = 'cn=shares,ou={},{}'.format(module.params['ou'], base_dn) + dn = 'cn={},{}'.format(name, container) + + if state == 'present': + try: + if not exists: + obj = umc_module_for_add('shares/share', container) + else: + obj = umc_module_for_edit('shares/share', dn) + + module.params['printablename'] = '{} ({})'.format(name, module.params['host']) + for k in obj.keys(): + obj[k] = module.params[k] + + diff = obj.diff() + for k in obj.keys(): + if obj.hasChanged(k): + changed=True + if not module.check_mode: + if not exists: + obj.create() + elif changed: + obj.modify() + except BaseException as e: + module.fail_json( + msg='Creating/editing share {} in {} failed: {}'.format(name, container, e) + ) + + if state == 'absent' and exists: + try: + obj = umc_module_for_edit('shares/share', dn) + if not module.check_mode: + obj.remove() + changed = True + except: + module.fail_json( + msg='Removing share {} in {} failed: {}'.format(name, container, e) + ) + + module.exit_json( + changed=changed, + name=name, + diff=diff, + container=container + ) + + +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From a8881721735c570089568f92836c6da3b40a2710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20R=C3=BCetschi?= Date: Tue, 7 Jun 2016 14:48:58 +0200 Subject: [PATCH 2003/2522] UCS udm_share: fix creating of new shares --- univention/udm_share.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/univention/udm_share.py b/univention/udm_share.py index a046f3e30d4..deb76742c2c 100644 --- a/univention/udm_share.py +++ b/univention/udm_share.py @@ -271,7 +271,7 @@ def main(): sambaMSDFSRoot = dict(type='str', default='0'), sambaName = dict(type='str', - default=''), + default=None), sambaNtAclSupport = dict(type='str', default='1'), sambaOplocks = dict(type='str', @@ -329,15 +329,18 @@ def main(): obj[k] = module.params[k] diff = obj.diff() - for k in obj.keys(): - if obj.hasChanged(k): - changed=True + if exists: + for k in obj.keys(): + if obj.hasChanged(k): + changed=True + else: + changed=True if not module.check_mode: if not exists: obj.create() elif changed: obj.modify() - except BaseException as e: + except Exception as e: module.fail_json( msg='Creating/editing share {} in {} failed: {}'.format(name, container, e) ) From 3c4ec8ac43422a19b674779f42dcfecf0e6cca11 Mon Sep 17 00:00:00 2001 From: Tobias Rueetschi Date: Fri, 10 Jun 2016 12:42:44 +0200 Subject: [PATCH 2004/2522] univention udm_share: adapt to library univention_umc --- univention/udm_share.py | 197 ++-------------------------------------- 1 file changed, 8 insertions(+), 189 deletions(-) diff --git a/univention/udm_share.py b/univention/udm_share.py index deb76742c2c..c1a2204b384 100644 --- a/univention/udm_share.py +++ b/univention/udm_share.py @@ -1,194 +1,15 @@ #!/usr/bin/env python # -*- coding: UTF-8 -*- -"""UCS access module""" -import univention.uldap -import univention.config_registry -import univention.admin.uldap -import univention.admin.objects -import univention.admin.config -import re -import thread -import time -import ldap as orig_ldap -import socket - -__all__ = [ - 'ldap_search', - 'config_registry', - 'base_dn', - 'ldap', - 'config', - 'position_base_dn', - 'get_umc_admin_objects', -] - -config_registry = univention.config_registry.ConfigRegistry() -config_registry.load() -base_dn = config_registry["ldap/base"] - -try: - secret_file = open('/etc/ldap.secret', 'r') - bind_dn = 'cn=admin,{}'.format(base_dn) -except IOError: # pragma: no cover - secret_file = open('/etc/machine.secret', 'r') - bind_dn = config_registry["ldap/hostdn"] -pwd_line = secret_file.readline() -pwd = re.sub('\n', '', pwd_line) - -ldap = univention.admin.uldap.access( - host = config_registry['ldap/master'], - base = base_dn, - binddn = bind_dn, - bindpw = pwd, - start_tls = 1 +from ansible.module_utils.basic import * +from ansible.module_utils.univention_umc import ( + umc_module_for_add, + umc_module_for_edit, + ldap_search, + base_dn, ) -config = univention.admin.config.config() -univention.admin.modules.update() -position_base_dn = univention.admin.uldap.position(base_dn) -modules_by_name = {} - - -def ldap_dn_tree_parent(dn, count=1): - dn_array = dn.split(',') - dn_array[0:count] = [] - return ','.join(dn_array) - - -def ldap_search(filter, base=base_dn, attr=None): - """Replaces uldaps search and uses a generator. - !! Arguments are not the same.""" - msgid = ldap.lo.lo.search( - base, - orig_ldap.SCOPE_SUBTREE, - filterstr=filter, - attrlist=attr - ) - # I used to have a try: finally: here but there seems to be a bug in python - # which swallows the KeyboardInterrupt - # The abandon now doesn't make too much sense - while True: - result_type, result_data = ldap.lo.lo.result(msgid, all=0) - if not result_data: - break - if result_type is orig_ldap.RES_SEARCH_RESULT: # pragma: no cover - break - else: - if result_type is orig_ldap.RES_SEARCH_ENTRY: - for res in result_data: - yield res - ldap.lo.lo.abandon(msgid) - - -def module_name(module_name_): - """Returns an initialized UMC module, identified by the given name. - - The module is a module specification according to the udm commandline. - Example values are: - * users/user - * shares/share - * groups/group - - If the module does not exist, a KeyError is raised. - - The modules are cached, so they won't be re-initialized - in subsequent calls. - """ - - if module_name_ not in modules_by_name: - module = univention.admin.modules.get(module_name_) - univention.admin.modules.init(ldap, position_base_dn, module) - - modules_by_name[module_name_] = module - - return modules_by_name[module_name_] - - -def get_umc_admin_objects(): - """Convenience accessor for getting univention.admin.objects. - - This implements delayed importing, so the univention.* modules - are not loaded until this function is called. - """ - return univention.admin.objects - - -def umc_module_for_add(module, container_dn, superordinate=None): - """Returns an UMC module object prepared for creating a new entry. - - The module is a module specification according to the udm commandline. - Example values are: - * users/user - * shares/share - * groups/group - - The container_dn MUST be the dn of the container (not of the object to - be created itself!). - """ - mod = module_name(module) - - position = position_base_dn - position.setDn(container_dn) - - # config, ldap objects from common module - obj = mod.object(config, ldap, position, superordinate=superordinate) - obj.open() - - return obj - - -def umc_module_for_edit(module, object_dn, superordinate=None): - """Returns an UMC module object prepared for editing an existing entry. - - The module is a module specification according to the udm commandline. - Example values are: - * users/user - * shares/share - * groups/group - - The object_dn MUST be the dn of the object itself, not the container! - """ - mod = module_name(module) - - objects = get_umc_admin_objects() - - position = position_base_dn - position.setDn(ldap_dn_tree_parent(object_dn)) - - obj = objects.get( - mod, - config, - ldap, - position=position, - superordinate=superordinate, - dn=object_dn - ) - obj.open() - - return obj - - -def create_containers_and_parents(container_dn): - """Create a container and if needed the parents containers""" - import univention.admin.uexceptions as uexcp - assert container_dn.startswith("cn=") - try: - parent = ldap_dn_tree_parent(container_dn) - obj = umc_module_for_add( - 'container/cn', - parent - ) - obj['name'] = container_dn.split(',')[0].split('=')[1] - obj['description'] = "container created by import" - except uexcp.ldapError: - create_containers_and_parents(parent) - obj = umc_module_for_add( - 'container/cn', - parent - ) - obj['name'] = container_dn.split(',')[0].split('=')[1] - obj['description'] = "container created by import" +import socket def main(): @@ -314,7 +135,7 @@ def main(): )) exists = bool(len(obj)) - container = 'cn=shares,ou={},{}'.format(module.params['ou'], base_dn) + container = 'cn=shares,ou={},{}'.format(module.params['ou'], base_dn()) dn = 'cn={},{}'.format(name, container) if state == 'present': @@ -364,7 +185,5 @@ def main(): ) -from ansible.module_utils.basic import * - if __name__ == '__main__': main() From f4f218005a5e85ad2c60964adc3e6ce020419a0d Mon Sep 17 00:00:00 2001 From: Tobias Rueetschi Date: Fri, 10 Jun 2016 12:43:19 +0200 Subject: [PATCH 2005/2522] univention udm_share: fix shebang --- univention/udm_share.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/univention/udm_share.py b/univention/udm_share.py index c1a2204b384..2181b40f651 100644 --- a/univention/udm_share.py +++ b/univention/udm_share.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/python # -*- coding: UTF-8 -*- From 529eed2fe0eecf1c092db1e718bdc5c999865b6b Mon Sep 17 00:00:00 2001 From: Tobias Rueetschi Date: Fri, 10 Jun 2016 12:43:39 +0200 Subject: [PATCH 2006/2522] univention udm_share: add required_if to the API --- univention/udm_share.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/univention/udm_share.py b/univention/udm_share.py index 2181b40f651..44020c05955 100644 --- a/univention/udm_share.py +++ b/univention/udm_share.py @@ -123,7 +123,10 @@ def main(): choices=['present', 'absent'], type='str') ), - supports_check_mode=True + supports_check_mode=True, + required_if = ([ + ('state', 'present', ['path', 'host', 'sambaName']) + ]) ) name = module.params['name'] state = module.params['state'] From 3f3a193e35a3081e8ba7196d43253554b76dbe13 Mon Sep 17 00:00:00 2001 From: Tobias Rueetschi Date: Fri, 10 Jun 2016 12:44:26 +0200 Subject: [PATCH 2007/2522] univention udm_share: add license information --- univention/udm_share.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/univention/udm_share.py b/univention/udm_share.py index 44020c05955..44c44991727 100644 --- a/univention/udm_share.py +++ b/univention/udm_share.py @@ -1,6 +1,25 @@ #!/usr/bin/python # -*- coding: UTF-8 -*- +# Copyright (c) 2016, Adfinis SyGroup AG +# Tobias Rueetschi +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + from ansible.module_utils.basic import * from ansible.module_utils.univention_umc import ( From 533f056a2aee7e082f608507e532d34cab6a48aa Mon Sep 17 00:00:00 2001 From: Tobias Rueetschi Date: Fri, 10 Jun 2016 12:54:50 +0200 Subject: [PATCH 2008/2522] univention udm_share: import only AnsibleModule from ansible.module_utils.basic --- univention/udm_share.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/univention/udm_share.py b/univention/udm_share.py index 44c44991727..a90ea8ab2fa 100644 --- a/univention/udm_share.py +++ b/univention/udm_share.py @@ -21,7 +21,7 @@ # -from ansible.module_utils.basic import * +from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.univention_umc import ( umc_module_for_add, umc_module_for_edit, From 7b80df0a0d1fecc1c955c7fce4b4f79a225917bf Mon Sep 17 00:00:00 2001 From: Tobias Rueetschi Date: Fri, 10 Jun 2016 14:09:25 +0200 Subject: [PATCH 2009/2522] univention udm_share: add documenation --- univention/udm_share.py | 287 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 287 insertions(+) diff --git a/univention/udm_share.py b/univention/udm_share.py index a90ea8ab2fa..429f4f6c5ad 100644 --- a/univention/udm_share.py +++ b/univention/udm_share.py @@ -31,6 +31,293 @@ import socket +DOCUMENTATION = ''' +--- +module: udm_share +version_added: "2.2" +author: "Tobias Rueetschi (@2-B)" +short_description: Manage samba shares on a univention corporate server +description: + - "This module allows to manage samba shares on a univention corporate + server (UCS). + It uses the python API of the UCS to create a new object or edit it." +requirements: + - Python >= 2.6 +options: + state: + required: false + default: "present" + choices: [ present, absent ] + description: + - Whether the share is present or not. + name: + required: true + description: + - Name + host: + required: false + default: None + description: + - Host. Required if C(state=present). + ou: + required: true + description: + - Organisational unit, inside the LDAP Base DN. + owner: + required: false + default: 0 + description: + - Directory owner of the share's root directory. + group: + required: false + default: '0' + description: + - Directory owner group of the share's root directory. + path: + required: false + default: None + description: + - Directory. Required if C(state=present). + directorymode: + required: false + default: '00755' + description: + - Permissions for the share's root directory. + root_squash: + required: false + default: '1' + description: + - Modify user ID for root user (root squashing). + subtree_checking: + required: false + default: '1' + description: + - Subtree checking. + sync: + required: false + default: 'sync' + description: + - NFS synchronisation. + writeable: + required: false + default: '1' + description: + - NFS write access. + sambaBlockSize: + required: false + default: None + description: + - Blocking size. + sambaBlockingLocks: + required: false + default: '1' + description: + - Blocking locks. + sambaBrowseable: + required: false + default: '1' + description: + - Show in Windows network environment. + sambaCreateMode: + required: false + default: '0744' + description: + - File mode. + sambaCscPolicy: + required: false + default: 'manual' + description: + - Client-side caching policy. + sambaCustomSettings: + required: false + default: [] + description: + - Option name in smb.conf and its value. + sambaDirectoryMode: + required: false + default: '0755' + description: + - Directory mode. + sambaDirectorySecurityMode: + required: false + default: '0777' + description: + - Directory security mode. + sambaDosFilemode: + required: false + default: '0' + description: + - Users with write access may modify permissions. + sambaFakeOplocks: + required: false + default: '0' + description: + - Fake oplocks. + sambaForceCreateMode: + required: false + default: '0' + description: + - Force file mode. + sambaForceDirectoryMode: + required: false + default: '0' + description: + - Force directory mode. + sambaForceDirectorySecurityMode: + required: false + default: '0' + description: + - Force directory security mode. + sambaForceGroup: + required: false + default: None + description: + - Force group. + sambaForceSecurityMode: + required: false + default: '0' + description: + - Force security mode. + sambaForceUser: + required: false + default: None + description: + - Force user. + sambaHideFiles: + required: false + default: None + description: + - Hide files. + sambaHideUnreadable: + required: false + default: '0' + description: + - Hide unreadable files/directories. + sambaHostsAllow: + required: false + default: [] + description: + - Allowed host/network. + sambaHostsDeny: + required: false + default: [] + description: + - Denied host/network. + sambaInheritAcls: + required: false + default: '1' + description: + - Inherit ACLs. + sambaInheritOwner: + required: false + default: '0' + description: + - Create files/directories with the owner of the parent directory. + sambaInheritPermissions: + required: false + default: '0' + description: + - Create files/directories with permissions of the parent directory. + sambaInvalidUsers: + required: false + default: None + description: + - Invalid users or groups. + sambaLevel2Oplocks: + required: false + default: '1' + description: + - Level 2 oplocks. + sambaLocking: + required: false + default: '1' + description: + - Locking. + sambaMSDFSRoot: + required: false + default: '0' + description: + - MSDFS root. + sambaName: + required: false + default: None + description: + - Windows name. Required if C(state=present). + sambaNtAclSupport: + required: false + default: '1' + description: + - NT ACL support. + sambaOplocks: + required: false + default: '1' + description: + - Oplocks. + sambaPostexec: + required: false + default: None + description: + - Postexec script. + sambaPreexec: + required: false + default: None + description: + - Preexec script. + sambaPublic: + required: false + default: '0' + description: + - Allow anonymous read-only access with a guest user. + sambaSecurityMode: + required: false + default: '0777' + description: + - Security mode. + sambaStrictLocking: + required: false + default: 'Auto' + description: + - Strict locking. + sambaVFSObjects: + required: false + default: None + description: + - VFS objects. + sambaValidUsers: + required: false + default: None + description: + - Valid users or groups. + sambaWriteList: + required: false + default: None + description: + - Restrict write access to these users/groups. + sambaWriteable: + required: false + default: '1' + description: + - Samba write access. + nfs_hosts: + required: false + default: [] + description: + - Only allow access for this host, IP address or network. + nfsCustomSettings: + required: false + default: [] + description: + - Option name in exports file. +''' + + +EXAMPLES = ''' +''' + + +RETURN = '''# ''' + + def main(): module = AnsibleModule( argument_spec = dict( From 838cbd19c1ce495df3ccb796c21d210014690ce3 Mon Sep 17 00:00:00 2001 From: Tobias Rueetschi Date: Thu, 16 Jun 2016 16:27:01 +0200 Subject: [PATCH 2010/2522] univention udm_share: reorder documentation parameters --- univention/udm_share.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/univention/udm_share.py b/univention/udm_share.py index 429f4f6c5ad..8aedfa076dc 100644 --- a/univention/udm_share.py +++ b/univention/udm_share.py @@ -58,7 +58,18 @@ required: false default: None description: - - Host. Required if C(state=present). + - Host FQDN (server which provides the share), e.g. C({{ + ansible_fqdn }}). Required if C(state=present). + path: + required: false + default: None + description: + - Directory on the providing server, e.g. C(/home). Required if C(state=present). + sambaName: + required: false + default: None + description: + - Windows name. Required if C(state=present). ou: required: true description: @@ -73,11 +84,6 @@ default: '0' description: - Directory owner group of the share's root directory. - path: - required: false - default: None - description: - - Directory. Required if C(state=present). directorymode: required: false default: '00755' @@ -238,11 +244,6 @@ default: '0' description: - MSDFS root. - sambaName: - required: false - default: None - description: - - Windows name. Required if C(state=present). sambaNtAclSupport: required: false default: '1' From ac5182db2d145523631af55a41d521bad0e7d51d Mon Sep 17 00:00:00 2001 From: Tobias Rueetschi Date: Thu, 16 Jun 2016 17:16:21 +0200 Subject: [PATCH 2011/2522] univention udm_share: change some parameters to type bool --- univention/udm_share.py | 114 ++++++++++++++++++++++++---------------- 1 file changed, 70 insertions(+), 44 deletions(-) diff --git a/univention/udm_share.py b/univention/udm_share.py index 8aedfa076dc..10144568f1a 100644 --- a/univention/udm_share.py +++ b/univention/udm_share.py @@ -92,11 +92,13 @@ root_squash: required: false default: '1' + choices: [ '0', '1' ] description: - Modify user ID for root user (root squashing). subtree_checking: required: false default: '1' + choices: [ '0', '1' ] description: - Subtree checking. sync: @@ -107,6 +109,7 @@ writeable: required: false default: '1' + choices: [ '0', '1' ] description: - NFS write access. sambaBlockSize: @@ -117,11 +120,13 @@ sambaBlockingLocks: required: false default: '1' + choices: [ '0', '1' ] description: - Blocking locks. sambaBrowseable: required: false default: '1' + choices: [ '0', '1' ] description: - Show in Windows network environment. sambaCreateMode: @@ -152,26 +157,31 @@ sambaDosFilemode: required: false default: '0' + choices: [ '0', '1' ] description: - Users with write access may modify permissions. sambaFakeOplocks: required: false default: '0' + choices: [ '0', '1' ] description: - Fake oplocks. sambaForceCreateMode: required: false default: '0' + choices: [ '0', '1' ] description: - Force file mode. sambaForceDirectoryMode: required: false default: '0' + choices: [ '0', '1' ] description: - Force directory mode. sambaForceDirectorySecurityMode: required: false default: '0' + choices: [ '0', '1' ] description: - Force directory security mode. sambaForceGroup: @@ -182,6 +192,7 @@ sambaForceSecurityMode: required: false default: '0' + choices: [ '0', '1' ] description: - Force security mode. sambaForceUser: @@ -197,6 +208,7 @@ sambaHideUnreadable: required: false default: '0' + choices: [ '0', '1' ] description: - Hide unreadable files/directories. sambaHostsAllow: @@ -212,16 +224,19 @@ sambaInheritAcls: required: false default: '1' + choices: [ '0', '1' ] description: - Inherit ACLs. sambaInheritOwner: required: false default: '0' + choices: [ '0', '1' ] description: - Create files/directories with the owner of the parent directory. sambaInheritPermissions: required: false default: '0' + choices: [ '0', '1' ] description: - Create files/directories with permissions of the parent directory. sambaInvalidUsers: @@ -232,26 +247,31 @@ sambaLevel2Oplocks: required: false default: '1' + choices: [ '0', '1' ] description: - Level 2 oplocks. sambaLocking: required: false default: '1' + choices: [ '0', '1' ] description: - Locking. sambaMSDFSRoot: required: false default: '0' + choices: [ '0', '1' ] description: - MSDFS root. sambaNtAclSupport: required: false default: '1' + choices: [ '0', '1' ] description: - NT ACL support. sambaOplocks: required: false default: '1' + choices: [ '0', '1' ] description: - Oplocks. sambaPostexec: @@ -267,6 +287,7 @@ sambaPublic: required: false default: '0' + choices: [ '0', '1' ] description: - Allow anonymous read-only access with a guest user. sambaSecurityMode: @@ -297,6 +318,7 @@ sambaWriteable: required: false default: '1' + choices: [ '0', '1' ] description: - Samba write access. nfs_hosts: @@ -336,20 +358,20 @@ def main(): default='00755'), host = dict(type='str', default=None), - root_squash = dict(type='str', - default='1'), - subtree_checking = dict(type='str', - default='1'), + root_squash = dict(type='bool', + default=True), + subtree_checking = dict(type='bool', + default=True), sync = dict(type='str', default='sync'), - writeable = dict(type='str', - default='1'), + writeable = dict(type='bool', + default=True), sambaBlockSize = dict(type='str', default=None), - sambaBlockingLocks = dict(type='str', - default='1'), - sambaBrowseable = dict(type='str', - default='1'), + sambaBlockingLocks = dict(type='bool', + default=True), + sambaBrowseable = dict(type='bool', + default=True), sambaCreateMode = dict(type='str', default='0744'), sambaCscPolicy = dict(type='str', @@ -360,56 +382,56 @@ def main(): default='0755'), sambaDirectorySecurityMode = dict(type='str', default='0777'), - sambaDosFilemode = dict(type='str', - default='0'), - sambaFakeOplocks = dict(type='str', - default='0'), - sambaForceCreateMode = dict(type='str', - default='0'), - sambaForceDirectoryMode = dict(type='str', - default='0'), - sambaForceDirectorySecurityMode = dict(type='str', - default='0'), + sambaDosFilemode = dict(type='bool', + default=False), + sambaFakeOplocks = dict(type='bool', + default=False), + sambaForceCreateMode = dict(type='bool', + default=False), + sambaForceDirectoryMode = dict(type='bool', + default=False), + sambaForceDirectorySecurityMode = dict(type='bool', + default=False), sambaForceGroup = dict(type='str', default=None), - sambaForceSecurityMode = dict(type='str', - default='0'), + sambaForceSecurityMode = dict(type='bool', + default=False), sambaForceUser = dict(type='str', default=None), sambaHideFiles = dict(type='str', default=None), - sambaHideUnreadable = dict(type='str', - default='0'), + sambaHideUnreadable = dict(type='bool', + default=False), sambaHostsAllow = dict(type='list', default=[]), sambaHostsDeny = dict(type='list', default=[]), - sambaInheritAcls = dict(type='str', - default='1'), - sambaInheritOwner = dict(type='str', - default='0'), - sambaInheritPermissions = dict(type='str', - default='0'), + sambaInheritAcls = dict(type='bool', + default=True), + sambaInheritOwner = dict(type='bool', + default=False), + sambaInheritPermissions = dict(type='bool', + default=False), sambaInvalidUsers = dict(type='str', default=None), - sambaLevel2Oplocks = dict(type='str', - default='1'), - sambaLocking = dict(type='str', - default='1'), - sambaMSDFSRoot = dict(type='str', - default='0'), + sambaLevel2Oplocks = dict(type='bool', + default=True), + sambaLocking = dict(type='bool', + default=True), + sambaMSDFSRoot = dict(type='bool', + default=False), sambaName = dict(type='str', default=None), - sambaNtAclSupport = dict(type='str', - default='1'), - sambaOplocks = dict(type='str', - default='1'), + sambaNtAclSupport = dict(type='bool', + default=True), + sambaOplocks = dict(type='bool', + default=True), sambaPostexec = dict(type='str', default=None), sambaPreexec = dict(type='str', default=None), - sambaPublic = dict(type='str', - default='0'), + sambaPublic = dict(type='bool', + default=False), sambaSecurityMode = dict(type='str', default='0777'), sambaStrictLocking = dict(type='str', @@ -420,8 +442,8 @@ def main(): default=None), sambaWriteList = dict(type='str', default=None), - sambaWriteable = dict(type='str', - default='1'), + sambaWriteable = dict(type='bool', + default=True), nfs_hosts = dict(type='list', default=[]), nfsCustomSettings = dict(type='list', @@ -457,6 +479,10 @@ def main(): module.params['printablename'] = '{} ({})'.format(name, module.params['host']) for k in obj.keys(): + if module.params[k] == True: + module.params[k] = '1' + elif module.params[k] == False: + module.params[k] = '0' obj[k] = module.params[k] diff = obj.diff() From 644b3efd87ef64c8968832fc85f7e323e527ae9f Mon Sep 17 00:00:00 2001 From: Tobias Rueetschi Date: Thu, 16 Jun 2016 17:19:53 +0200 Subject: [PATCH 2012/2522] univention udm_share: add example --- univention/udm_share.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/univention/udm_share.py b/univention/udm_share.py index 10144568f1a..19bbf5bbf25 100644 --- a/univention/udm_share.py +++ b/univention/udm_share.py @@ -335,6 +335,11 @@ EXAMPLES = ''' +# Create a share named home on the server ucs.example.com with the path /home. +- udm_sahre: name=home + path=/home + host=ucs.example.com + sambaName=Home ''' From b5beed7e3483125f63cc1a448298432b8a071c21 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 29 Aug 2016 09:13:08 -0700 Subject: [PATCH 2013/2522] We've decided that python-3.5 is our minimum pyhton3 version (#2798) --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 690f1b3609e..ab48d103e62 100644 --- a/.travis.yml +++ b/.travis.yml @@ -117,7 +117,6 @@ script: - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py|database/influxdb/influxdb.*\.py|database/mssql/mssql_db\.py|/letsencrypt\.py|network/f5/bigip.*\.py|remote_management/ipmi/.*\.py|cloud/atomic/atomic_.*\.py|univention/.*\.py' . - python2.6 -m compileall -fq . - python2.7 -m compileall -fq . - - python3.4 -m compileall -fq . -x $(echo "$PY3_EXCLUDE_LIST"| tr ' ' '|') - python3.5 -m compileall -fq . -x $(echo "$PY3_EXCLUDE_LIST"| tr ' ' '|') - ansible-validate-modules . #- ./test-docs.sh extras From 2ef4a34eee091449d2a22312e3e15171f8c6d54c Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Mon, 29 Aug 2016 09:29:47 -0700 Subject: [PATCH 2014/2522] Fixes documentation bugs in bigip_irule (#2797) The return docs were incorrect for this module. This patch fixes them and adds some additional return values --- network/f5/bigip_irule.py | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/network/f5/bigip_irule.py b/network/f5/bigip_irule.py index b1ec9acef2a..5e99ec34faa 100644 --- a/network/f5/bigip_irule.py +++ b/network/f5/bigip_irule.py @@ -94,11 +94,31 @@ ''' RETURN = ''' -full_name: - description: Full name of the user +module: + description: The module that the iRule was added to returned: changed and success type: string - sample: "John Doe" + sample: "gtm" +src: + description: The filename that included the iRule source + returned: changed and success, when provided + type: string + sample: "/opt/src/irules/example1.tcl" +name: + description: The name of the iRule that was managed + returned: changed and success + type: string + sample: "my-irule" +content: + description: The content of the iRule that was managed + returned: changed and success + type: string + sample: "when LB_FAILED { set wipHost [LB::server addr] }" +partition: + description: The partition in which the iRule was managed + returned: changed and success + type: string + sample: "Common" ''' try: @@ -255,9 +275,14 @@ def update(self): changed = True params['name'] = name params['partition'] = partition + self.cparams = camel_dict_to_snake_dict(params) + if 'api_anonymous' in self.cparams: + self.cparams['content'] = self.cparams.pop('api_anonymous') + if self.params['src']: + self.cparams['src'] = self.params['src'] + if check_mode: return changed - self.cparams = camel_dict_to_snake_dict(params) else: return changed @@ -297,6 +322,11 @@ def create(self): params['partition'] = partition self.cparams = camel_dict_to_snake_dict(params) + if 'api_anonymous' in self.cparams: + self.cparams['content'] = self.cparams.pop('api_anonymous') + if self.params['src']: + self.cparams['src'] = self.params['src'] + if check_mode: return True From e9fd5ad5dc5a72ae9255fcff0c65f3653462d13a Mon Sep 17 00:00:00 2001 From: Ethan Devenport Date: Mon, 29 Aug 2016 19:45:57 +0000 Subject: [PATCH 2015/2522] Reverting recent commits back to initial PR and will move the new profitbricks_firewall_rule module and other recent changes to a new branch. Revert "Added support for firewall rules, consolidated resource UUID retrieval methods for server and NIC modules, and set LAN type to int." This reverts commit 464cbb89f225578386a830624633a55e39054544. --- cloud/profitbricks/profitbricks.py | 65 +-- .../profitbricks_firewall_rule.py | 399 ------------------ cloud/profitbricks/profitbricks_nic.py | 105 +++-- 3 files changed, 96 insertions(+), 473 deletions(-) delete mode 100644 cloud/profitbricks/profitbricks_firewall_rule.py diff --git a/cloud/profitbricks/profitbricks.py b/cloud/profitbricks/profitbricks.py index f3016fd13ca..b0791f37e3a 100644 --- a/cloud/profitbricks/profitbricks.py +++ b/cloud/profitbricks/profitbricks.py @@ -98,11 +98,6 @@ - This will assign the machine to the public LAN. If no LAN exists with public Internet access it is created. required: false default: false - nic_name: - description: - - The name of the default NIC. - required: false - version_added: '2.2' lan: description: - The ID of the LAN you wish to add the servers to. @@ -115,7 +110,7 @@ default: null subscription_password: description: - - The ProfitBricks password. Overrides the PB_PASSWORD environement variable. + - THe ProfitBricks password. Overrides the PB_PASSWORD environement variable. required: false default: null wait: @@ -200,15 +195,14 @@ ''' +import re import uuid import time HAS_PB_SDK = True try: - from profitbricks.client import ( - ProfitBricksService, Volume, Server, Datacenter, NIC, LAN - ) + from profitbricks.client import ProfitBricksService, Volume, Server, Datacenter, NIC, LAN except ImportError: HAS_PB_SDK = False @@ -216,6 +210,9 @@ 'de/fra', 'de/fkb'] +uuid_match = re.compile( + '[\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12}', re.I) + def _wait_for_completion(profitbricks, promise, wait_timeout, msg): if not promise: return @@ -226,9 +223,9 @@ def _wait_for_completion(profitbricks, promise, wait_timeout, msg): request_id=promise['requestId'], status=True) - if operation_result['metadata']['status'] == 'DONE': + if operation_result['metadata']['status'] == "DONE": return - elif operation_result['metadata']['status'] == 'FAILED': + elif operation_result['metadata']['status'] == "FAILED": raise Exception( 'Request failed to complete ' + msg + ' "' + str( promise['requestId']) + '" to complete.') @@ -248,7 +245,6 @@ def _create_machine(module, profitbricks, datacenter, name): image_password = module.params.get('image_password') ssh_keys = module.params.get('ssh_keys') bus = module.params.get('bus') - nic_name = module.params.get('nic_name') lan = module.params.get('lan') assign_public_ip = module.params.get('assign_public_ip') subscription_user = module.params.get('subscription_user') @@ -288,7 +284,6 @@ def _create_machine(module, profitbricks, datacenter, name): bus=bus) n = NIC( - name=nic_name, lan=int(lan) ) @@ -316,7 +311,6 @@ def _create_machine(module, profitbricks, datacenter, name): except Exception as e: module.fail_json(msg="failed to create the new server: %s" % str(e)) else: - server_response['nic'] = server_response['entities']['nics']['items'][0] return server_response @@ -379,7 +373,7 @@ def create_virtual_machine(module, profitbricks): # Locate UUID for datacenter if referenced by name. datacenter_list = profitbricks.list_datacenters() - datacenter_id = _get_resource_id(datacenter_list, datacenter) + datacenter_id = _get_datacenter_id(datacenter_list, datacenter) if datacenter_id: datacenter_found = True @@ -415,13 +409,14 @@ def create_virtual_machine(module, profitbricks): server_list = profitbricks.list_servers(datacenter_id) for name in names: # Skip server creation if the server already exists. - if _get_resource_id(server_list, name): + if _get_server_id(server_list, name): continue create_response = _create_machine(module, profitbricks, str(datacenter_id), name) - for nic in create_response['entities']['nics']['items']: - if lan == nic['properties']['lan']: - create_response.update({'public_ip': nic['properties']['ips'][0]}) + nics = profitbricks.list_nics(datacenter_id, create_response['id']) + for n in nics['items']: + if lan == n['properties']['lan']: + create_response.update({'public_ip': n['properties']['ips'][0]}) virtual_machines.append(create_response) @@ -463,7 +458,7 @@ def remove_virtual_machine(module, profitbricks): # Locate UUID for datacenter if referenced by name. datacenter_list = profitbricks.list_datacenters() - datacenter_id = _get_resource_id(datacenter_list, datacenter) + datacenter_id = _get_datacenter_id(datacenter_list, datacenter) if not datacenter_id: module.fail_json(msg='Virtual data center \'%s\' not found.' % str(datacenter)) @@ -471,7 +466,7 @@ def remove_virtual_machine(module, profitbricks): server_list = profitbricks.list_servers(datacenter_id) for instance in instance_ids: # Locate UUID for server if referenced by name. - server_id = _get_resource_id(server_list, instance) + server_id = _get_server_id(server_list, instance) if server_id: # Remove the server's boot volume if remove_boot_volume: @@ -522,7 +517,7 @@ def startstop_machine(module, profitbricks, state): # Locate UUID for datacenter if referenced by name. datacenter_list = profitbricks.list_datacenters() - datacenter_id = _get_resource_id(datacenter_list, datacenter) + datacenter_id = _get_datacenter_id(datacenter_list, datacenter) if not datacenter_id: module.fail_json(msg='Virtual data center \'%s\' not found.' % str(datacenter)) @@ -530,7 +525,7 @@ def startstop_machine(module, profitbricks, state): server_list = profitbricks.list_servers(datacenter_id) for instance in instance_ids: # Locate UUID of server if referenced by name. - server_id = _get_resource_id(server_list, instance) + server_id = _get_server_id(server_list, instance) if server_id: _startstop_machine(module, profitbricks, datacenter_id, server_id) changed = True @@ -559,14 +554,23 @@ def startstop_machine(module, profitbricks, state): return (changed) -def _get_resource_id(resources, identity): +def _get_datacenter_id(datacenters, identity): + """ + Fetch and return datacenter UUID by datacenter name if found. + """ + for datacenter in datacenters['items']: + if identity in (datacenter['properties']['name'], datacenter['id']): + return datacenter['id'] + return None + + +def _get_server_id(servers, identity): """ - Fetch and return the UUID of a resource regardless of whether the name or - UUID is passed. + Fetch and return server UUID by server name if found. """ - for resource in resources['items']: - if identity in (resource['properties']['name'], resource['id']): - return resource['id'] + for server in servers['items']: + if identity in (server['properties']['name'], server['id']): + return server['id'] return None @@ -584,8 +588,7 @@ def main(): image_password=dict(default=None), ssh_keys=dict(type='list', default=[]), bus=dict(default='VIRTIO'), - nic_name=dict(default=str(uuid.uuid4()).replace('-', '')[:10]), - lan=dict(type='int', default=1), + lan=dict(default=1), count=dict(type='int', default=1), auto_increment=dict(type='bool', default=True), instance_ids=dict(type='list', default=[]), diff --git a/cloud/profitbricks/profitbricks_firewall_rule.py b/cloud/profitbricks/profitbricks_firewall_rule.py deleted file mode 100644 index 1fd5a3a8404..00000000000 --- a/cloud/profitbricks/profitbricks_firewall_rule.py +++ /dev/null @@ -1,399 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -DOCUMENTATION = ''' ---- -module: profitbricks_firewall_rule -short_description: Create or remove a firewall rule. -description: - - This module allows you to create or remove a firewlal rule. This module has a dependency on profitbricks >= 1.0.0 -version_added: "2.2" -options: - datacenter: - description: - - The datacenter name or UUID in which to operate. - required: true - server: - description: - - The server name or UUID. - required: true - nic: - description: - - The NIC name or UUID. - required: true - name: - description: - - The name or UUID of the firewall rule. - required: false - protocol: - description: - - The protocol for the firewall rule. - choices: [ "TCP", "UDP", "ICMP" ] - required: true - source_mac: - description: - - Only traffic originating from the respective MAC address is allowed. No value allows all source MAC addresses. - required: false - source_ip: - description: - - Only traffic originating from the respective IPv4 address is allowed. No value allows all source IPs. - required: false - target_ip: - description: - - In case the target NIC has multiple IP addresses, only traffic directed to the respective IP address of the NIC is allowed. No value allows all target IPs. - required: false - port_range_start: - description: - - Defines the start range of the allowed port (from 1 to 65534) if protocol TCP or UDP is chosen. Leave value empty to allow all ports. - required: false - port_range_end: - description: - - Defines the end range of the allowed port (from 1 to 65534) if the protocol TCP or UDP is chosen. Leave value empty to allow all ports. - required: false - icmp_type: - description: - - Defines the allowed type (from 0 to 254) if the protocol ICMP is chosen. No value allows all types. - required: false - icmp_code: - description: - - Defines the allowed code (from 0 to 254) if protocol ICMP is chosen. No value allows all codes. - required: false - subscription_user: - description: - - The ProfitBricks username. Overrides the PB_SUBSCRIPTION_ID environement variable. - required: false - subscription_password: - description: - - THe ProfitBricks password. Overrides the PB_PASSWORD environement variable. - required: false - wait: - description: - - wait for the operation to complete before returning - required: false - default: "yes" - choices: [ "yes", "no" ] - wait_timeout: - description: - - how long before wait gives up, in seconds - default: 600 - state: - description: - - Indicate desired state of the resource - required: false - default: 'present' - choices: ["present", "absent"] - -requirements: [ "profitbricks" ] -author: Ethan Devenport (ethand@stackpointcloud.com) -''' - -EXAMPLES = ''' -# Create a firewall rule -- name: Create SSH firewall rule - profitbricks_firewall_rule: - datacenter: Virtual Datacenter - server: node001 - nic: 7341c2454f - name: Allow SSH - protocol: TCP - source_ip: 0.0.0.0 - port_range_start: 22 - port_range_end: 22 - state: present - -- name: Create ping firewall rule - profitbricks_firewall_rule: - datacenter: Virtual Datacenter - server: node001 - nic: 7341c2454f - name: Allow Ping - protocol: ICMP - source_ip: 0.0.0.0 - icmp_type: 8 - icmp_code: 0 - state: present - -# Remove a firewall rule -- name: Remove public ping firewall rule - profitbricks_firewall_rule: - datacenter: Virtual Datacenter - server: node001 - nic: aa6c261b9c - name: Allow Ping - state: absent -''' - -RETURN = ''' ---- -id: - description: UUID of the firewall rule. - returned: success - type: string - sample: be60aa97-d9c7-4c22-bebe-f5df7d6b675d -name: - description: Name of the firwall rule. - returned: success - type: string - sample: Allow SSH -protocol: - description: Protocol of the firewall rule. - returned: success - type: string - sample: TCP -source_mac: - description: MAC address of the firewall rule. - returned: success - type: string - sample: 02:01:97:d7:ed:49 -source_ip: - description: Source IP of the firewall rule. - returned: success - type: string - sample: tcp -target_ip: - description: Target IP of the firewal rule. - returned: success - type: string - sample: 10.0.0.1 -port_range_start: - description: Start port of the firewall rule. - returned: success - type: int - sample: 80 -port_range_end: - description: End port of the firewall rule. - returned: success - type: int - sample: 80 -icmp_type: - description: ICMP type of the firewall rule. - returned: success - type: int - sample: 8 -icmp_code: - description: ICMP code of the firewall rule. - returned: success - type: int - sample: 0 -''' - -# import uuid -import time - -HAS_PB_SDK = True - -try: - from profitbricks.client import ProfitBricksService, FirewallRule -except ImportError: - HAS_PB_SDK = False - - -def _wait_for_completion(profitbricks, promise, wait_timeout, msg): - if not promise: return - wait_timeout = time.time() + wait_timeout - while wait_timeout > time.time(): - time.sleep(5) - operation_result = profitbricks.get_request( - request_id=promise['requestId'], - status=True) - - if operation_result['metadata']['status'] == 'DONE': - return - elif operation_result['metadata']['status'] == 'FAILED': - raise Exception( - 'Request failed to complete ' + msg + ' "' + str( - promise['requestId']) + '" to complete.') - - raise Exception( - 'Timed out waiting for async operation ' + msg + ' "' + str( - promise['requestId'] - ) + '" to complete.') - - -def create_firewall_rule(module, profitbricks): - """ - Creates a firewall rule. - - module : AnsibleModule object - profitbricks: authenticated profitbricks object. - - Returns: - True if the firewal rule creates, false otherwise - """ - datacenter = module.params.get('datacenter') - server = module.params.get('server') - nic = module.params.get('nic') - name = module.params.get('name') - protocol = module.params.get('protocol') - source_mac = module.params.get('source_mac') - source_ip = module.params.get('source_ip') - target_ip = module.params.get('target_ip') - port_range_start = module.params.get('port_range_start') - port_range_end = module.params.get('port_range_end') - icmp_type = module.params.get('icmp_type') - icmp_code = module.params.get('icmp_code') - wait = module.params.get('wait') - wait_timeout = module.params.get('wait_timeout') - - # Locate UUID for virtual datacenter - datacenter_list = profitbricks.list_datacenters() - datacenter_id = _get_resource_id(datacenter_list, datacenter) - if not datacenter_id: - module.fail_json(msg='Virtual data center \'%s\' not found.' % str(datacenter)) - - # Locate UUID for server - server_list = profitbricks.list_servers(datacenter_id) - server_id = _get_resource_id(server_list, server) - - # Locate UUID for NIC - nic_list = profitbricks.list_nics(datacenter_id, server_id) - nic_id = _get_resource_id(nic_list, nic) - - try: - profitbricks.update_nic(datacenter_id, server_id, nic_id, - firewall_active=True) - except Exception as e: - module.fail_json(msg='Unable to activate the NIC firewall.' % str(e)) - - f = FirewallRule( - name=name, - protocol=protocol, - source_mac=source_mac, - source_ip=source_ip, - target_ip=target_ip, - port_range_start=port_range_start, - port_range_end=port_range_end, - icmp_type=icmp_type, - icmp_code=icmp_code - ) - - try: - firewall_rule_response = profitbricks.create_firewall_rule( - datacenter_id, server_id, nic_id, f - ) - - if wait: - _wait_for_completion(profitbricks, firewall_rule_response, - wait_timeout, "create_firewall_rule") - return firewall_rule_response - - except Exception as e: - module.fail_json(msg="failed to create the firewall rule: %s" % str(e)) - - -def delete_firewall_rule(module, profitbricks): - """ - Removes a firewall rule - - module : AnsibleModule object - profitbricks: authenticated profitbricks object. - - Returns: - True if the firewall rule was removed, false otherwise - """ - datacenter = module.params.get('datacenter') - server = module.params.get('server') - nic = module.params.get('nic') - name = module.params.get('name') - - # Locate UUID for virtual datacenter - datacenter_list = profitbricks.list_datacenters() - datacenter_id = _get_resource_id(datacenter_list, datacenter) - - # Locate UUID for server - server_list = profitbricks.list_servers(datacenter_id) - server_id = _get_resource_id(server_list, server) - - # Locate UUID for NIC - nic_list = profitbricks.list_nics(datacenter_id, server_id) - nic_id = _get_resource_id(nic_list, nic) - - # Locate UUID for firewall rule - firewall_rule_list = profitbricks.get_firewall_rules(datacenter_id, server_id, nic_id) - firewall_rule_id = _get_resource_id(firewall_rule_list, name) - - try: - firewall_rule_response = profitbricks.delete_firewall_rule( - datacenter_id, server_id, nic_id, firewall_rule_id - ) - return firewall_rule_response - except Exception as e: - module.fail_json(msg="failed to remove the firewall rule: %s" % str(e)) - - -def _get_resource_id(resource_list, identity): - """ - Fetch and return the UUID of a resource regardless of whether the name or - UUID is passed. - """ - for resource in resource_list['items']: - if identity in (resource['properties']['name'], resource['id']): - return resource['id'] - return None - - -def main(): - module = AnsibleModule( - argument_spec=dict( - datacenter=dict(type='str', required=True), - server=dict(type='str', required=True), - nic=dict(type='str', required=True), - name=dict(type='str', required=True), - protocol=dict(type='str', required=False), - source_mac=dict(type='str', default=None), - source_ip=dict(type='str', default=None), - target_ip=dict(type='str', default=None), - port_range_start=dict(type='int', default=None), - port_range_end=dict(type='int', default=None), - icmp_type=dict(type='int', default=None), - icmp_code=dict(type='int', default=None), - subscription_user=dict(type='str', required=True), - subscription_password=dict(type='str', required=True), - wait=dict(type='bool', default=True), - wait_timeout=dict(type='int', default=600), - state=dict(default='present'), - ) - ) - - if not HAS_PB_SDK: - module.fail_json(msg='profitbricks required for this module') - - subscription_user = module.params.get('subscription_user') - subscription_password = module.params.get('subscription_password') - - profitbricks = ProfitBricksService( - username=subscription_user, - password=subscription_password) - - state = module.params.get('state') - - if state == 'absent': - try: - (changed) = delete_firewall_rule(module, profitbricks) - module.exit_json(changed=changed) - except Exception as e: - module.fail_json(msg='failed to set firewall rule state: %s' % str(e)) - - elif state == 'present': - try: - (firewall_rule_dict) = create_firewall_rule(module, profitbricks) - module.exit_json(firewall_rules=firewall_rule_dict) - except Exception as e: - module.fail_json(msg='failed to set firewall rules state: %s' % str(e)) - -from ansible.module_utils.basic import * - -main() diff --git a/cloud/profitbricks/profitbricks_nic.py b/cloud/profitbricks/profitbricks_nic.py index d13554cf43a..902d5266843 100644 --- a/cloud/profitbricks/profitbricks_nic.py +++ b/cloud/profitbricks/profitbricks_nic.py @@ -84,18 +84,23 @@ name: 7341c2454f wait_timeout: 500 state: absent + ''' +import re import uuid import time HAS_PB_SDK = True try: - from profitbricks.client import ProfitBricksService, NIC, FirewallRule + from profitbricks.client import ProfitBricksService, NIC except ImportError: HAS_PB_SDK = False +uuid_match = re.compile( + '[\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12}', re.I) + def _wait_for_completion(profitbricks, promise, wait_timeout, msg): if not promise: return @@ -106,9 +111,9 @@ def _wait_for_completion(profitbricks, promise, wait_timeout, msg): request_id=promise['requestId'], status=True) - if operation_result['metadata']['status'] == 'DONE': + if operation_result['metadata']['status'] == "DONE": return - elif operation_result['metadata']['status'] == 'FAILED': + elif operation_result['metadata']['status'] == "FAILED": raise Exception( 'Request failed to complete ' + msg + ' "' + str( promise['requestId']) + '" to complete.') @@ -118,7 +123,6 @@ def _wait_for_completion(profitbricks, promise, wait_timeout, msg): promise['requestId'] ) + '" to complete.') - def create_nic(module, profitbricks): """ Creates a NIC. @@ -137,22 +141,28 @@ def create_nic(module, profitbricks): wait_timeout = module.params.get('wait_timeout') # Locate UUID for Datacenter - datacenter_list = profitbricks.list_datacenters() - datacenter_id = _get_resource_id(datacenter_list, datacenter) - if not datacenter_id: - module.fail_json(msg='Virtual data center \'%s\' not found.' % str(datacenter)) + if not (uuid_match.match(datacenter)): + datacenter_list = profitbricks.list_datacenters() + for d in datacenter_list['items']: + dc = profitbricks.get_datacenter(d['id']) + if datacenter == dc['properties']['name']: + datacenter = d['id'] + break # Locate UUID for Server - server_list = profitbricks.list_servers(datacenter_id) - server_id = _get_resource_id(server_list, server) - - n = NIC( - name=name, - lan=lan - ) - + if not (uuid_match.match(server)): + server_list = profitbricks.list_servers(datacenter) + for s in server_list['items']: + if server == s['properties']['name']: + server = s['id'] + break try: - nic_response = profitbricks.create_nic(datacenter_id, server_id, n) + n = NIC( + name=name, + lan=lan + ) + + nic_response = profitbricks.create_nic(datacenter, server, n) if wait: _wait_for_completion(profitbricks, nic_response, @@ -163,7 +173,6 @@ def create_nic(module, profitbricks): except Exception as e: module.fail_json(msg="failed to create the NIC: %s" % str(e)) - def delete_nic(module, profitbricks): """ Removes a NIC @@ -179,44 +188,53 @@ def delete_nic(module, profitbricks): name = module.params.get('name') # Locate UUID for Datacenter - datacenter_list = profitbricks.list_datacenters() - datacenter_id = _get_resource_id(datacenter_list, datacenter) - if not datacenter_id: - module.fail_json(msg='Virtual data center \'%s\' not found.' % str(datacenter)) + if not (uuid_match.match(datacenter)): + datacenter_list = profitbricks.list_datacenters() + for d in datacenter_list['items']: + dc = profitbricks.get_datacenter(d['id']) + if datacenter == dc['properties']['name']: + datacenter = d['id'] + break # Locate UUID for Server - server_list = profitbricks.list_servers(datacenter_id) - server_id = _get_resource_id(server_list, server) + server_found = False + if not (uuid_match.match(server)): + server_list = profitbricks.list_servers(datacenter) + for s in server_list['items']: + if server == s['properties']['name']: + server_found = True + server = s['id'] + break + + if not server_found: + return False # Locate UUID for NIC - nic_list = profitbricks.list_nics(datacenter_id, server_id) - nic_id = _get_resource_id(nic_list, name) + nic_found = False + if not (uuid_match.match(name)): + nic_list = profitbricks.list_nics(datacenter, server) + for n in nic_list['items']: + if name == n['properties']['name']: + nic_found = True + name = n['id'] + break + + if not nic_found: + return False try: - nic_response = profitbricks.delete_nic(datacenter_id, server_id, nic_id) + nic_response = profitbricks.delete_nic(datacenter, server, name) return nic_response except Exception as e: module.fail_json(msg="failed to remove the NIC: %s" % str(e)) - -def _get_resource_id(resource_list, identity): - """ - Fetch and return the UUID of a resource regardless of whether the name or - UUID is passed. - """ - for resource in resource_list['items']: - if identity in (resource['properties']['name'], resource['id']): - return resource['id'] - return None - - def main(): module = AnsibleModule( argument_spec=dict( datacenter=dict(), server=dict(), - name=dict(default=str(uuid.uuid4()).replace('-', '')[:10]), - lan=dict(type='int'), + name=dict(default=str(uuid.uuid4()).replace('-','')[:10]), + lan=dict(), subscription_user=dict(), subscription_password=dict(), wait=dict(type='bool', default=True), @@ -237,6 +255,7 @@ def main(): if not module.params.get('server'): module.fail_json(msg='server parameter is required') + subscription_user = module.params.get('subscription_user') subscription_password = module.params.get('subscription_password') @@ -262,10 +281,10 @@ def main(): try: (nic_dict) = create_nic(module, profitbricks) - module.exit_json(nic=nic_dict) + module.exit_json(nics=nic_dict) except Exception as e: module.fail_json(msg='failed to set nic state: %s' % str(e)) from ansible.module_utils.basic import * -main() +main() \ No newline at end of file From 5767885ae5ba5b9be94eda00e886d46940ed4438 Mon Sep 17 00:00:00 2001 From: bbooysen Date: Tue, 30 Aug 2016 13:06:32 +0100 Subject: [PATCH 2016/2522] typo fix (#2816) Changed 'memroy' to 'memory'. --- cloud/vmware/vmware_guest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/vmware/vmware_guest.py b/cloud/vmware/vmware_guest.py index 4754068ec24..da6511e1398 100644 --- a/cloud/vmware/vmware_guest.py +++ b/cloud/vmware/vmware_guest.py @@ -63,7 +63,7 @@ required: False hardware: description: - - Attributes such as cpus, memroy, osid, and disk controller + - Attributes such as cpus, memory, osid, and disk controller required: False disk: description: From 582e36aef9cfc4048b21324deef9e20ce690f97b Mon Sep 17 00:00:00 2001 From: Mike Liu Date: Tue, 30 Aug 2016 10:16:10 -0400 Subject: [PATCH 2017/2522] Iptables enhancements (#2789) * Add the flush parameter. When specified the flush parameter indicates that this module should remove all rules from the specified table. If no table parameter is specified then the default filter table is flushed. * Add support for setting chain policies. The module supports setting the policy of a given chain and table to the following target values, ACCEPT, DROP, QUEUE, and RETURN. This parameter ignores all other unrelated parameters. * Fix pep8 issues. * Fix missing quotation. * Make 'flush' and 'policy' parameters mutually exclusive. This combination is not supported by the wrapped iptables command. 'flush' and 'policy' however, can both take the 'chain' argument. --- system/iptables.py | 113 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 93 insertions(+), 20 deletions(-) diff --git a/system/iptables.py b/system/iptables.py index 51089575456..5d055182367 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -74,8 +74,8 @@ description: - "Chain to operate on. This option can either be the name of a user defined chain or any of the builtin chains: 'INPUT', 'FORWARD', - 'OUTPUT', 'PREROUTING', 'POSTROUTING', 'SECMARK', 'CONNSECMARK'" - required: true + 'OUTPUT', 'PREROUTING', 'POSTROUTING', 'SECMARK', 'CONNSECMARK'." + required: false protocol: description: - The protocol of the rule or of the packet to check. The specified @@ -240,13 +240,18 @@ default: null ctstate: description: - - "ctstate is a list of the connection states to match in the conntrack module. - Possible states are: 'INVALID', 'NEW', 'ESTABLISHED', 'RELATED', 'UNTRACKED', 'SNAT', 'DNAT'" + - "ctstate is a list of the connection states to match in the conntrack + module. + Possible states are: 'INVALID', 'NEW', 'ESTABLISHED', 'RELATED', + 'UNTRACKED', 'SNAT', 'DNAT'" required: false default: [] limit: description: - - "Specifies the maximum average number of matches to allow per second. The number can specify units explicitly, using `/second', `/minute', `/hour' or `/day', or parts of them (so `5/second' is the same as `5/s')." + - "Specifies the maximum average number of matches to allow per second. + The number can specify units explicitly, using `/second', `/minute', + `/hour' or `/day', or parts of them (so `5/second' is the same as + `5/s')." required: false default: null limit_burst: @@ -268,10 +273,24 @@ icmp_type: version_added: "2.2" description: - - "This allows specification of the ICMP type, which can be a numeric ICMP type, - type/code pair, or one of the ICMP type names shown by the command - 'iptables -p icmp -h'" + - "This allows specification of the ICMP type, which can be a numeric + ICMP type, type/code pair, or one of the ICMP type names shown by the + command 'iptables -p icmp -h'" + required: false + flush: + version_added: "2.2" + description: + - "Flushes the specified table and chain of all rules. If no chain is + specified then the entire table is purged. Ignores all other + parameters." required: false + policy: + version_added: "2.2" + description: + - "Set the policy for the chain to the given target. Valid targets are + ACCEPT, DROP, QUEUE, RETURN. Only built in chains can have policies. + This parameter requires the chain parameter. Ignores all other + parameters." ''' EXAMPLES = ''' @@ -337,7 +356,11 @@ def construct_rule(params): append_param(rule, params['destination_port'], '--destination-port', False) append_param(rule, params['to_ports'], '--to-ports', False) append_param(rule, params['set_dscp_mark'], '--set-dscp', False) - append_param(rule, params['set_dscp_mark_class'], '--set-dscp-class', False) + append_param( + rule, + params['set_dscp_mark_class'], + '--set-dscp-class', + False) append_match(rule, params['comment'], 'comment') append_param(rule, params['comment'], '--comment', False) append_match(rule, params['ctstate'], 'state') @@ -353,11 +376,12 @@ def construct_rule(params): return rule -def push_arguments(iptables_path, action, params): +def push_arguments(iptables_path, action, params, make_rule=True): cmd = [iptables_path] cmd.extend(['-t', params['table']]) cmd.extend([action, params['chain']]) - cmd.extend(construct_rule(params)) + if make_rule: + cmd.extend(construct_rule(params)) return cmd @@ -382,15 +406,39 @@ def remove_rule(iptables_path, module, params): module.run_command(cmd, check_rc=True) +def flush_table(iptables_path, module, params): + cmd = push_arguments(iptables_path, '-F', params, make_rule=False) + module.run_command(cmd, check_rc=True) + + +def set_chain_policy(iptables_path, module, params): + cmd = push_arguments(iptables_path, '-P', params, make_rule=False) + cmd.append(params['policy']) + module.run_command(cmd, check_rc=True) + + def main(): module = AnsibleModule( supports_check_mode=True, argument_spec=dict( - table=dict(required=False, default='filter', choices=['filter', 'nat', 'mangle', 'raw', 'security']), - state=dict(required=False, default='present', choices=['present', 'absent']), - action=dict(required=False, default='append', type='str', choices=['append', 'insert']), - ip_version=dict(required=False, default='ipv4', choices=['ipv4', 'ipv6']), - chain=dict(required=True, default=None, type='str'), + table=dict( + required=False, + default='filter', + choices=['filter', 'nat', 'mangle', 'raw', 'security']), + state=dict( + required=False, + default='present', + choices=['present', 'absent']), + action=dict( + required=False, + default='append', + type='str', + choices=['append', 'insert']), + ip_version=dict( + required=False, + default='ipv4', + choices=['ipv4', 'ipv6']), + chain=dict(required=False, default=None, type='str'), protocol=dict(required=False, default=None, type='str'), source=dict(required=False, default=None, type='str'), to_source=dict(required=False, default=None, type='str'), @@ -406,8 +454,8 @@ def main(): source_port=dict(required=False, default=None, type='str'), destination_port=dict(required=False, default=None, type='str'), to_ports=dict(required=False, default=None, type='str'), - set_dscp_mark=dict(required=False,default=None, type='str'), - set_dscp_mark_class=dict(required=False,default=None, type='str'), + set_dscp_mark=dict(required=False, default=None, type='str'), + set_dscp_mark_class=dict(required=False, default=None, type='str'), comment=dict(required=False, default=None, type='str'), ctstate=dict(required=False, default=[], type='list'), limit=dict(required=False, default=None, type='str'), @@ -415,9 +463,16 @@ def main(): uid_owner=dict(required=False, default=None, type='str'), reject_with=dict(required=False, default=None, type='str'), icmp_type=dict(required=False, default=None, type='str'), + flush=dict(required=False, default=False, type='bool'), + policy=dict( + required=False, + default=None, + type='str', + choices=['ACCEPT', 'DROP', 'QUEUE', 'RETURN']), ), mutually_exclusive=( ['set_dscp_mark', 'set_dscp_mark_class'], + ['flush', 'policy'], ), ) args = dict( @@ -426,12 +481,30 @@ def main(): ip_version=module.params['ip_version'], table=module.params['table'], chain=module.params['chain'], + flush=module.params['flush'], rule=' '.join(construct_rule(module.params)), state=module.params['state'], ) - insert = (module.params['action'] == 'insert') + ip_version = module.params['ip_version'] iptables_path = module.get_bin_path(BINS[ip_version], True) + + # Check if chain option is required + if args['flush'] is False and args['chain'] is None: + module.fail_json( + msg="Either chain or flush parameter must be specified.") + + # Flush the table + if args['flush'] is True: + flush_table(iptables_path, module, module.params) + module.exit_json(**args) + + # Set the policy + if module.params['policy']: + set_chain_policy(iptables_path, module, module.params) + module.exit_json(**args) + + insert = (module.params['action'] == 'insert') rule_is_present = check_present(iptables_path, module, module.params) should_be_present = (args['state'] == 'present') @@ -443,7 +516,7 @@ def main(): module.exit_json(changed=args['changed']) # Target is already up to date - if args['changed'] == False: + if args['changed'] is False: module.exit_json(**args) if should_be_present: From 035367170c67c9332c846b68e46668188d92c839 Mon Sep 17 00:00:00 2001 From: alxsey Date: Tue, 30 Aug 2016 10:17:30 -0400 Subject: [PATCH 2018/2522] Implement template storage selection (#2755) --- cloud/misc/proxmox.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cloud/misc/proxmox.py b/cloud/misc/proxmox.py index f02ff2e292b..d0df6b3f42a 100644 --- a/cloud/misc/proxmox.py +++ b/cloud/misc/proxmox.py @@ -214,8 +214,8 @@ def get_instance(proxmox, vmid): return [ vm for vm in proxmox.cluster.resources.get(type='vm') if vm['vmid'] == int(vmid) ] -def content_check(proxmox, node, ostemplate, storage): - return [ True for cnt in proxmox.nodes(node).storage(storage).content.get() if cnt['volid'] == ostemplate ] +def content_check(proxmox, node, ostemplate, template_store): + return [ True for cnt in proxmox.nodes(node).storage(template_store).content.get() if cnt['volid'] == ostemplate ] def node_check(proxmox, node): return [ True for nd in proxmox.nodes.get() if nd['node'] == node ] @@ -339,6 +339,8 @@ def main(): memory = module.params['memory'] swap = module.params['swap'] storage = module.params['storage'] + if module.params['ostemplate'] is not None: + template_store = module.params['ostemplate'].split(":")[0] timeout = module.params['timeout'] # If password not set get it from PROXMOX_PASSWORD env @@ -364,9 +366,9 @@ def main(): module.fail_json(msg='node, hostname, password and ostemplate are mandatory for creating vm') elif not node_check(proxmox, node): module.fail_json(msg="node '%s' not exists in cluster" % node) - elif not content_check(proxmox, node, module.params['ostemplate'], storage): + elif not content_check(proxmox, node, module.params['ostemplate'], template_store): module.fail_json(msg="ostemplate '%s' not exists on node %s and storage %s" - % (module.params['ostemplate'], node, storage)) + % (module.params['ostemplate'], node, template_store)) create_instance(module, proxmox, vmid, node, disk, storage, cpus, memory, swap, timeout, password = module.params['password'], From b58bd264e87ee7b71cbb019fb67aa6c63d4d8b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Tue, 30 Aug 2016 16:39:58 +0200 Subject: [PATCH 2019/2522] vmware_local_user_manager: fix global name 'module' is not defined (#2818) --- cloud/vmware/vmware_local_user_manager.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cloud/vmware/vmware_local_user_manager.py b/cloud/vmware/vmware_local_user_manager.py index c7e8ecb9311..ff7736fe883 100644 --- a/cloud/vmware/vmware_local_user_manager.py +++ b/cloud/vmware/vmware_local_user_manager.py @@ -137,9 +137,9 @@ def state_create_user(self): task = self.content.accountManager.CreateUser(account_spec) self.module.exit_json(changed=True) except vmodl.RuntimeFault as runtime_fault: - module.fail_json(msg=runtime_fault.msg) + self.module.fail_json(msg=runtime_fault.msg) except vmodl.MethodFault as method_fault: - module.fail_json(msg=method_fault.msg) + self.module.fail_json(msg=method_fault.msg) def state_update_user(self): account_spec = self.create_account_spec() @@ -148,9 +148,9 @@ def state_update_user(self): task = self.content.accountManager.UpdateUser(account_spec) self.module.exit_json(changed=True) except vmodl.RuntimeFault as runtime_fault: - module.fail_json(msg=runtime_fault.msg) + self.module.fail_json(msg=runtime_fault.msg) except vmodl.MethodFault as method_fault: - module.fail_json(msg=method_fault.msg) + self.module.fail_json(msg=method_fault.msg) def state_remove_user(self): @@ -158,9 +158,9 @@ def state_remove_user(self): task = self.content.accountManager.RemoveUser(self.local_user_name) self.module.exit_json(changed=True) except vmodl.RuntimeFault as runtime_fault: - module.fail_json(msg=runtime_fault.msg) + self.module.fail_json(msg=runtime_fault.msg) except vmodl.MethodFault as method_fault: - module.fail_json(msg=method_fault.msg) + self.module.fail_json(msg=method_fault.msg) def state_exit_unchanged(self): From 58c6ad6c9ec171c0714fec240c79f5d140ec27fe Mon Sep 17 00:00:00 2001 From: James Higgins Date: Tue, 30 Aug 2016 10:42:14 -0400 Subject: [PATCH 2020/2522] Just use netloc to identify bucket name for s3 locations (#2713) --- packaging/language/maven_artifact.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packaging/language/maven_artifact.py b/packaging/language/maven_artifact.py index f5a88c7b220..1136f7aaaff 100644 --- a/packaging/language/maven_artifact.py +++ b/packaging/language/maven_artifact.py @@ -235,7 +235,7 @@ def _request(self, url, failmsg, f): parsed_url = urlparse(url) if parsed_url.scheme=='s3': parsed_url = urlparse(url) - bucket_name = parsed_url.netloc[:parsed_url.netloc.find('.')] + bucket_name = parsed_url.netloc key_name = parsed_url.path[1:] client = boto3.client('s3',aws_access_key_id=self.module.params.get('username', ''), aws_secret_access_key=self.module.params.get('password', '')) url_to_use = client.generate_presigned_url('get_object',Params={'Bucket':bucket_name,'Key':key_name},ExpiresIn=10) @@ -387,4 +387,4 @@ def main(): if __name__ == '__main__': - main() \ No newline at end of file + main() From 8f48370dac91c21ab1ad85b3175a5e893640d061 Mon Sep 17 00:00:00 2001 From: Tobias Rueetschi Date: Tue, 30 Aug 2016 18:04:41 +0200 Subject: [PATCH 2021/2522] univention udm_share: pep8 --- univention/udm_share.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/univention/udm_share.py b/univention/udm_share.py index 19bbf5bbf25..d78304113e8 100644 --- a/univention/udm_share.py +++ b/univention/udm_share.py @@ -28,7 +28,6 @@ ldap_search, base_dn, ) -import socket DOCUMENTATION = ''' @@ -453,9 +452,9 @@ def main(): default=[]), nfsCustomSettings = dict(type='list', default=[]), - state = dict(default='present', - choices=['present', 'absent'], - type='str') + state = dict(default='present', + choices=['present', 'absent'], + type='str') ), supports_check_mode=True, required_if = ([ @@ -484,9 +483,9 @@ def main(): module.params['printablename'] = '{} ({})'.format(name, module.params['host']) for k in obj.keys(): - if module.params[k] == True: + if module.params[k] is True: module.params[k] = '1' - elif module.params[k] == False: + elif module.params[k] is False: module.params[k] = '0' obj[k] = module.params[k] @@ -494,17 +493,21 @@ def main(): if exists: for k in obj.keys(): if obj.hasChanged(k): - changed=True + changed = True else: - changed=True + changed = True if not module.check_mode: if not exists: obj.create() elif changed: obj.modify() - except Exception as e: + except BaseException as err: module.fail_json( - msg='Creating/editing share {} in {} failed: {}'.format(name, container, e) + msg='Creating/editing share {} in {} failed: {}'.format( + name, + container, + err, + ) ) if state == 'absent' and exists: @@ -513,9 +516,13 @@ def main(): if not module.check_mode: obj.remove() changed = True - except: + except BaseException as err: module.fail_json( - msg='Removing share {} in {} failed: {}'.format(name, container, e) + msg='Removing share {} in {} failed: {}'.format( + name, + container, + err, + ) ) module.exit_json( From 44e5f4bb17b108baf078f79e51b62383c5612a46 Mon Sep 17 00:00:00 2001 From: Tobias Rueetschi Date: Tue, 30 Aug 2016 19:04:42 +0200 Subject: [PATCH 2022/2522] univention udm_share: change documentation, use camel_case for parameters, old parameter names as alias --- univention/udm_share.py | 120 ++++++++++++++++++++++++++-------------- 1 file changed, 80 insertions(+), 40 deletions(-) diff --git a/univention/udm_share.py b/univention/udm_share.py index d78304113e8..a77fbcdee28 100644 --- a/univention/udm_share.py +++ b/univention/udm_share.py @@ -64,11 +64,12 @@ default: None description: - Directory on the providing server, e.g. C(/home). Required if C(state=present). - sambaName: + samba_name: required: false default: None description: - Windows name. Required if C(state=present). + aliases: [ sambaName ] ou: required: true description: @@ -111,225 +112,264 @@ choices: [ '0', '1' ] description: - NFS write access. - sambaBlockSize: + samba_block_size: required: false default: None description: - Blocking size. - sambaBlockingLocks: + aliases: [ sambaBlockSize ] + samba_blocking_locks: required: false default: '1' choices: [ '0', '1' ] description: - Blocking locks. - sambaBrowseable: + aliases: [ sambaBlockingLocks ] + samba_browseable: required: false default: '1' choices: [ '0', '1' ] description: - Show in Windows network environment. - sambaCreateMode: + aliases: [ sambaBrowseable ] + samba_create_mode: required: false default: '0744' description: - File mode. - sambaCscPolicy: + aliases: [ sambaCreateMode ] + samba_csc_policy: required: false default: 'manual' description: - Client-side caching policy. - sambaCustomSettings: + aliases: [ sambaCscPolicy ] + samba_custom_settings: required: false default: [] description: - Option name in smb.conf and its value. - sambaDirectoryMode: + aliases: [ sambaCustomSettings ] + samba_directory_mode: required: false default: '0755' description: - Directory mode. - sambaDirectorySecurityMode: + aliases: [ sambaDirectoryMode ] + samba_directory_security_mode: required: false default: '0777' description: - Directory security mode. - sambaDosFilemode: + aliases: [ sambaDirectorySecurityMode ] + samba_dos_filemode: required: false default: '0' choices: [ '0', '1' ] description: - Users with write access may modify permissions. - sambaFakeOplocks: + aliases: [ sambaDosFilemode ] + samba_fake_oplocks: required: false default: '0' choices: [ '0', '1' ] description: - Fake oplocks. - sambaForceCreateMode: + aliases: [ sambaFakeOplocks ] + samba_force_create_mode: required: false default: '0' choices: [ '0', '1' ] description: - Force file mode. - sambaForceDirectoryMode: + aliases: [ sambaForceCreateMode ] + samba_force_directory_mode: required: false default: '0' choices: [ '0', '1' ] description: - Force directory mode. - sambaForceDirectorySecurityMode: + aliases: [ sambaForceDirectoryMode ] + samba_force_directory_security_mode: required: false default: '0' choices: [ '0', '1' ] description: - Force directory security mode. - sambaForceGroup: + aliases: [ sambaForceDirectorySecurityMode ] + samba_force_group: required: false default: None description: - Force group. - sambaForceSecurityMode: + aliases: [ sambaForceGroup ] + samba_force_security_mode: required: false default: '0' choices: [ '0', '1' ] description: - Force security mode. - sambaForceUser: + aliases: [ sambaForceSecurityMode ] + samba_force_user: required: false default: None description: - Force user. - sambaHideFiles: + aliases: [ sambaForceUser ] + samba_hide_files: required: false default: None description: - Hide files. - sambaHideUnreadable: + aliases: [ sambaHideFiles ] + samba_hide_unreadable: required: false default: '0' choices: [ '0', '1' ] description: - Hide unreadable files/directories. - sambaHostsAllow: + aliases: [ sambaHideUnreadable ] + samba_hosts_allow: required: false default: [] description: - Allowed host/network. - sambaHostsDeny: + aliases: [ sambaHostsAllow ] + samba_hosts_deny: required: false default: [] description: - Denied host/network. - sambaInheritAcls: + aliases: [ sambaHostsDeny ] + samba_inherit_acls: required: false default: '1' choices: [ '0', '1' ] description: - Inherit ACLs. - sambaInheritOwner: + aliases: [ sambaInheritAcls ] + samba_inherit_owner: required: false default: '0' choices: [ '0', '1' ] description: - Create files/directories with the owner of the parent directory. - sambaInheritPermissions: + aliases: [ sambaInheritOwner ] + samba_inherit_permissions: required: false default: '0' choices: [ '0', '1' ] description: - Create files/directories with permissions of the parent directory. - sambaInvalidUsers: + aliases: [ sambaInheritPermissions ] + samba_invalid_users: required: false default: None description: - Invalid users or groups. - sambaLevel2Oplocks: + aliases: [ sambaInvalidUsers ] + samba_level_2_oplocks: required: false default: '1' choices: [ '0', '1' ] description: - Level 2 oplocks. - sambaLocking: + aliases: [ sambaLevel2Oplocks ] + samba_locking: required: false default: '1' choices: [ '0', '1' ] description: - Locking. - sambaMSDFSRoot: + aliases: [ sambaLocking ] + samba_msdfs_root: required: false default: '0' choices: [ '0', '1' ] description: - MSDFS root. - sambaNtAclSupport: + aliases: [ sambaMSDFSRoot ] + samba_nt_acl_support: required: false default: '1' choices: [ '0', '1' ] description: - NT ACL support. - sambaOplocks: + aliases: [ sambaNtAclSupport ] + samba_oplocks: required: false default: '1' choices: [ '0', '1' ] description: - Oplocks. - sambaPostexec: + aliases: [ sambaOplocks ] + samba_postexec: required: false default: None description: - Postexec script. - sambaPreexec: + aliases: [ sambaPostexec ] + samba_preexec: required: false default: None description: - Preexec script. - sambaPublic: + aliases: [ sambaPreexec ] + samba_public: required: false default: '0' choices: [ '0', '1' ] description: - Allow anonymous read-only access with a guest user. - sambaSecurityMode: + aliases: [ sambaPublic ] + samba_security_mode: required: false default: '0777' description: - Security mode. - sambaStrictLocking: + aliases: [ sambaSecurityMode ] + samba_strict_locking: required: false default: 'Auto' description: - Strict locking. - sambaVFSObjects: + aliases: [ sambaStrictLocking ] + samba_vfs_objects: required: false default: None description: - VFS objects. - sambaValidUsers: + aliases: [ sambaVFSObjects ] + samba_valid_users: required: false default: None description: - Valid users or groups. - sambaWriteList: + aliases: [ sambaValidUsers ] + samba_write_list: required: false default: None description: - Restrict write access to these users/groups. - sambaWriteable: + aliases: [ sambaWriteList ] + samba_writeable: required: false default: '1' choices: [ '0', '1' ] description: - Samba write access. + aliases: [ sambaWriteable ] nfs_hosts: required: false default: [] description: - Only allow access for this host, IP address or network. - nfsCustomSettings: + nfs_custom_settings: required: false default: [] description: - Option name in exports file. + aliases: [ nfsCustomSettings ] ''' From deb5975a8af4ef65bb97aa03f6abe2f0e2c32641 Mon Sep 17 00:00:00 2001 From: Tobias Rueetschi Date: Tue, 30 Aug 2016 19:12:24 +0200 Subject: [PATCH 2023/2522] univention udm_share: add aliases as documented --- univention/udm_share.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/univention/udm_share.py b/univention/udm_share.py index a77fbcdee28..8831c3d79f3 100644 --- a/univention/udm_share.py +++ b/univention/udm_share.py @@ -411,86 +411,126 @@ def main(): writeable = dict(type='bool', default=True), sambaBlockSize = dict(type='str', + aliases=['samba_block_size'], default=None), sambaBlockingLocks = dict(type='bool', + aliases=['samba_blocking_locks'], default=True), sambaBrowseable = dict(type='bool', + aliases=['samba_browsable'], default=True), sambaCreateMode = dict(type='str', + aliases=['samba_create_mode'], default='0744'), sambaCscPolicy = dict(type='str', + aliases=['samba_csc_policy'], default='manual'), sambaCustomSettings = dict(type='list', + aliases=['samba_custom_settings'], default=[]), sambaDirectoryMode = dict(type='str', + aliases=['samba_directory_mode'], default='0755'), sambaDirectorySecurityMode = dict(type='str', + aliases=['samba_directory_security_mode'], default='0777'), sambaDosFilemode = dict(type='bool', + aliases=['samba_dos_filemode'], default=False), sambaFakeOplocks = dict(type='bool', + aliases=['samba_fake_oplocks'], default=False), sambaForceCreateMode = dict(type='bool', + aliases=['samba_force_create_mode'], default=False), sambaForceDirectoryMode = dict(type='bool', + aliases=['samba_force_directory_mode'], default=False), sambaForceDirectorySecurityMode = dict(type='bool', + aliases=['samba_force_directory_security_mode'], default=False), sambaForceGroup = dict(type='str', + aliases=['samba_force_group'], default=None), sambaForceSecurityMode = dict(type='bool', + aliases=['samba_force_security_mode'], default=False), sambaForceUser = dict(type='str', + aliases=['samba_force_user'], default=None), sambaHideFiles = dict(type='str', + aliases=['samba_hide_files'], default=None), sambaHideUnreadable = dict(type='bool', + aliases=['samba_hide_unreadable'], default=False), sambaHostsAllow = dict(type='list', + aliases=['samba_hosts_allow'], default=[]), sambaHostsDeny = dict(type='list', + aliases=['samba_hosts_deny'], default=[]), sambaInheritAcls = dict(type='bool', + aliases=['samba_inherit_acls'], default=True), sambaInheritOwner = dict(type='bool', + aliases=['samba_inherit_owner'], default=False), sambaInheritPermissions = dict(type='bool', + aliases=['samba_inherit_permissions'], default=False), sambaInvalidUsers = dict(type='str', + aliases=['samba_invalid_users'], default=None), sambaLevel2Oplocks = dict(type='bool', + aliases=['samba_level_2_oplocks'], default=True), sambaLocking = dict(type='bool', + aliases=['samba_locking'], default=True), sambaMSDFSRoot = dict(type='bool', + aliases=['samba_msdfs_root'], default=False), sambaName = dict(type='str', + aliases=['samba_name'], default=None), sambaNtAclSupport = dict(type='bool', + aliases=['samba_nt_acl_support'], default=True), sambaOplocks = dict(type='bool', + aliases=['samba_oplocks'], default=True), sambaPostexec = dict(type='str', + aliases=['samba_postexec'], default=None), sambaPreexec = dict(type='str', + aliases=['samba_preexec'], default=None), sambaPublic = dict(type='bool', + aliases=['samba_public'], default=False), sambaSecurityMode = dict(type='str', + aliases=['samba_security_mode'], default='0777'), sambaStrictLocking = dict(type='str', + aliases=['samba_strict_locking'], default='Auto'), sambaVFSObjects = dict(type='str', + aliases=['samba_vfs_objects'], default=None), sambaValidUsers = dict(type='str', + aliases=['samba_valid_users'], default=None), sambaWriteList = dict(type='str', + aliases=['samba_write_list'], default=None), sambaWriteable = dict(type='bool', + aliases=['samba_writeable'], default=True), nfs_hosts = dict(type='list', default=[]), nfsCustomSettings = dict(type='list', + aliases=['nfs_custom_settings'], default=[]), state = dict(default='present', choices=['present', 'absent'], From be9a0f9854e4629a54fd13b4ef5e84419f16b53b Mon Sep 17 00:00:00 2001 From: naslanidis Date: Wed, 31 Aug 2016 04:32:21 +1000 Subject: [PATCH 2024/2522] new AWS module for ec2 dhcp option facts (#2001) new ec2 dhcp option facts module --- cloud/amazon/ec2_vpc_dhcp_options_facts.py | 167 +++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 cloud/amazon/ec2_vpc_dhcp_options_facts.py diff --git a/cloud/amazon/ec2_vpc_dhcp_options_facts.py b/cloud/amazon/ec2_vpc_dhcp_options_facts.py new file mode 100644 index 00000000000..8c59aeb5c9c --- /dev/null +++ b/cloud/amazon/ec2_vpc_dhcp_options_facts.py @@ -0,0 +1,167 @@ +#!/usr/bin/python +# +# This is a free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This Ansible library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this library. If not, see . + +DOCUMENTATION = ''' +--- +module: ec2_vpc_dhcp_options_facts +short_description: Gather facts about dhcp options sets in AWS +description: + - Gather facts about dhcp options sets in AWS +version_added: "2.2" +requirements: [ boto3 ] +author: "Nick Aslanidis (@naslanidis)" +options: + filters: + description: + - A dict of filters to apply. Each dict item consists of a filter key and a filter value. See U(http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeRouteTables.html) for possible filters. + required: false + default: null + DhcpOptionsIds: + description: + - Get details of specific DHCP Option ID + - Provide this value as a list + required: false + default: None +extends_documentation_fragment: + - aws + - ec2 +''' + +EXAMPLES = ''' +# # Note: These examples do not set authentication details, see the AWS Guide for details. + +- name: Gather facts about all DHCP Option sets for an account or profile + ec2_vpc_dhcp_options_facts: + region: ap-southeast-2 + profile: production + register: dhcp_facts + +- name: Gather facts about a filtered list of DHCP Option sets + ec2_vpc_dhcp_options_facts: + region: ap-southeast-2 + profile: production + filters: + "tag:Name": "abc-123" + register: dhcp_facts + +- name: Gather facts about a specific DHCP Option set by DhcpOptionId + ec2_vpc_dhcp_options_facts: + region: ap-southeast-2 + profile: production + DhcpOptionsIds: dopt-123fece2 + register: dhcp_facts + +''' + +RETURN = ''' +dhcp_options: + description: The dhcp option sets for the account + returned: always + type: list + +changed: + description: True if listing the dhcp options succeeds + type: bool + returned: always +''' + +import json + +try: + import botocore + import boto3 + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + + +def get_dhcp_options_info(dhcp_option): + dhcp_option_info = {'DhcpOptionsId': dhcp_option['DhcpOptionsId'], + 'DhcpConfigurations': dhcp_option['DhcpConfigurations'], + 'Tags': dhcp_option['Tags'] + } + return dhcp_option_info + + +def list_dhcp_options(client, module): + dryrun = module.params.get("DryRun") + all_dhcp_options_array = [] + params = dict() + + if module.params.get('filters'): + params['Filters'] = [] + for key, value in module.params.get('filters').iteritems(): + temp_dict = dict() + temp_dict['Name'] = key + if isinstance(value, basestring): + temp_dict['Values'] = [value] + else: + temp_dict['Values'] = value + params['Filters'].append(temp_dict) + + if module.params.get("DryRun"): + params['DryRun'] = module.params.get("DryRun") + + if module.params.get("DhcpOptionsIds"): + params['DhcpOptionsIds'] = module.params.get("DhcpOptionsIds") + + try: + all_dhcp_options = client.describe_dhcp_options(**params) + except botocore.exceptions.ClientError as e: + module.fail_json(msg=str(e)) + + for dhcp_option in all_dhcp_options['DhcpOptions']: + all_dhcp_options_array.append(get_dhcp_options_info(dhcp_option)) + + snaked_dhcp_options_array = [] + for dhcp_option in all_dhcp_options_array: + snaked_dhcp_options_array.append(camel_dict_to_snake_dict(dhcp_option)) + + module.exit_json(dhcp_options=snaked_dhcp_options_array) + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + filters = dict(type='dict', default=None, ), + DryRun = dict(type='bool', default=False), + DhcpOptionsIds = dict(type='list', default=None) + ) + ) + + module = AnsibleModule(argument_spec=argument_spec) + + # Validate Requirements + if not HAS_BOTO3: + module.fail_json(msg='json and botocore/boto3 is required.') + + try: + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + connection = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_kwargs) + except botocore.exceptions.NoCredentialsError, e: + module.fail_json(msg="Can't authorize connection - "+str(e)) + + # call your function here + results = list_dhcp_options(connection, module) + + module.exit_json(result=results) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From cd446baf39f0b14c166d32493bea9667320517a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20R=C3=BCetschi?= Date: Tue, 30 Aug 2016 20:34:11 +0200 Subject: [PATCH 2025/2522] Feature udm dns record (#2394) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * UCS udm_dns: added Signed-off-by: Tobias Rüetschi * UCS udm_dns_record: fix multiple entries in different zones Signed-off-by: Tobias Rüetschi * UCS udm_dns -> udm_dns_record: renamed Signed-off-by: Tobias Rüetschi * UCS udm_dns_record: updated, add supports check mode Signed-off-by: Tobias Rüetschi * UCS udm_dns_record: updated, add support to modify dns records * UCS udm_dns_record: change string formating * UCS udm_dns_record: add type definitions to the argument specification * UCS udm_dns_record: import common code for univention from ansible.module_utils.univention_umc * UCS udm_dns_record: add documentation * UCS udm_dns_record: update documenation * univention udm_dns_record: pylint * univention udm_dns_record: fix reverse zone entries --- univention/udm_dns_record.py | 182 +++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 univention/udm_dns_record.py diff --git a/univention/udm_dns_record.py b/univention/udm_dns_record.py new file mode 100644 index 00000000000..37c2468da3a --- /dev/null +++ b/univention/udm_dns_record.py @@ -0,0 +1,182 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- + +# Copyright (c) 2016, Adfinis SyGroup AG +# Tobias Rueetschi +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.univention_umc import ( + umc_module_for_add, + umc_module_for_edit, + ldap_search, + base_dn, + config, + uldap, +) +from univention.admin.handlers.dns import ( + forward_zone, + reverse_zone, +) + + +DOCUMENTATION = ''' +--- +module: udm_dns_record +version_added: "2.2" +author: "Tobias Rueetschi (@2-B)" +short_description: Manage dns entries on a univention corporate server +description: + - "This module allows to manage dns records on a univention corporate server (UCS). + It uses the python API of the UCS to create a new object or edit it." +requirements: + - Python >= 2.6 +options: + state: + required: false + default: "present" + choices: [ present, absent ] + description: + - Whether the dns record is present or not. + name: + required: true + description: + - "Name of the record, this is also the DNS record. E.g. www for + www.example.com." + zone: + required: true + description: + - Corresponding DNS zone for this record, e.g. example.com. + type: + required: true + choices: [ host_record, alias, ptr_record, srv_record, txt_record ] + description: + - "Define the record type. C(host_record) is a A or AAAA record, + C(alias) is a CNAME, C(ptr_record) is a PTR record, C(srv_record) + is a SRV record and C(txt_record) is a TXT record." + data: + required: false + default: [] + description: + - "Additional data for this record, e.g. ['a': '192.168.1.1']. + Required if C(state=present)." +''' + + +EXAMPLES = ''' +# Create a DNS record on a UCS +- udm_dns_zone: name=www + zone=example.com + type=host_record + data=['a': '192.168.1.1'] +''' + + +RETURN = '''# ''' + + +def main(): + module = AnsibleModule( + argument_spec = dict( + type = dict(required=True, + type='str'), + zone = dict(required=True, + type='str'), + name = dict(required=True, + type='str'), + data = dict(default=[], + type='dict'), + state = dict(default='present', + choices=['present', 'absent'], + type='str') + ), + supports_check_mode=True, + required_if = ([ + ('state', 'present', ['data']) + ]) + ) + type = module.params['type'] + zone = module.params['zone'] + name = module.params['name'] + data = module.params['data'] + state = module.params['state'] + changed = False + + obj = list(ldap_search( + '(&(objectClass=dNSZone)(zoneName={})(relativeDomainName={}))'.format(zone, name), + attr=['dNSZone'] + )) + + exists = bool(len(obj)) + container = 'zoneName={},cn=dns,{}'.format(zone, base_dn()) + dn = 'relativeDomainName={},{}'.format(name, container) + + if state == 'present': + try: + if not exists: + so = forward_zone.lookup( + config(), + uldap(), + '(zone={})'.format(zone), + scope='domain', + ) or reverse_zone.lookup( + config(), + uldap(), + '(zone={})'.format(zone), + scope='domain', + ) + obj = umc_module_for_add('dns/{}'.format(type), container, superordinate=so[0]) + else: + obj = umc_module_for_edit('dns/{}'.format(type), dn) + obj['name'] = name + for k, v in data.items(): + obj[k] = v + diff = obj.diff() + changed = obj.diff() != [] + if not module.check_mode: + if not exists: + obj.create() + else: + obj.modify() + except BaseException as e: + module.fail_json( + msg='Creating/editing dns entry {} in {} failed: {}'.format(name, container, e) + ) + + if state == 'absent' and exists: + try: + obj = umc_module_for_edit('dns/{}'.format(type), dn) + if not module.check_mode: + obj.remove() + changed = True + except BaseException as e: + module.fail_json( + msg='Removing dns entry {} in {} failed: {}'.format(name, container, e) + ) + + module.exit_json( + changed=changed, + name=name, + diff=diff, + container=container + ) + + +if __name__ == '__main__': + main() From fc18b967f234b35c5a11ef7550c01887f5fa8f35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20R=C3=BCetschi?= Date: Tue, 30 Aug 2016 20:38:16 +0200 Subject: [PATCH 2026/2522] Feature udm group (#2396) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * UCS udm_group: added Signed-off-by: Tobias Rüetschi * UCS udm_group: updated Signed-off-by: Tobias Rüetschi * UCS udm_group: add key description Signed-off-by: Tobias Rüetschi * python styling Signed-off-by: Tobias Rüetschi * UCS udm_group: updated, add supports check mode Signed-off-by: Tobias Rüetschi * UCS udm_group: updated, add support to modify groups * UCS udm_group: change string formating * UCS udm_group: add type definitions to the argument specification * UCS udm_group: import common code for univention from ansible.module_utils.univention * univention udm_group: add documentation * UCS udm_group: add requirement python >= 2.6 * univention udm_group: add more examples --- univention/udm_group.py | 176 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 univention/udm_group.py diff --git a/univention/udm_group.py b/univention/udm_group.py new file mode 100644 index 00000000000..588c7655241 --- /dev/null +++ b/univention/udm_group.py @@ -0,0 +1,176 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- + +# Copyright (c) 2016, Adfinis SyGroup AG +# Tobias Rueetschi +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.univention_umc import ( + umc_module_for_add, + umc_module_for_edit, + ldap_search, + base_dn, +) + + +DOCUMENTATION = ''' +--- +module: udm_group +version_added: "2.2" +author: "Tobias Rueetschi (@2-B)" +short_description: Manage of the posix group +description: + - "This module allows to manage user groups on a univention corporate server (UCS). + It uses the python API of the UCS to create a new object or edit it." +requirements: + - Python >= 2.6 +options: + state: + required: false + default: "present" + choices: [ present, absent ] + description: + - Whether the group is present or not. + name: + required: true + description: + - Name of the posix group. + description: + required: false + description: + - Group description. + position: + required: false + description: + - define the whole ldap position of the group, e.g. + C(cn=g123m-1A,cn=classes,cn=schueler,cn=groups,ou=schule,dc=example,dc=com). + ou: + required: false + description: + - LDAP OU, e.g. school for LDAP OU C(ou=school,dc=example,dc=com). + subpath: + required: false + description: + - Subpath inside the OU, e.g. C(cn=classes,cn=students,cn=groups). +''' + + +EXAMPLES = ''' +# Create a POSIX group +- udm_group: name=g123m-1A + +# Create a POSIX group with the exact DN +# C(cn=g123m-1A,cn=classes,cn=students,cn=groups,ou=school,dc=school,dc=example,dc=com) +- udm_group: name=g123m-1A + subpath='cn=classes,cn=students,cn=groups' + ou=school +# or +- udm_group: name=g123m-1A + position='cn=classes,cn=students,cn=groups,ou=school,dc=school,dc=example,dc=com' +''' + + +RETURN = '''# ''' + + +def main(): + module = AnsibleModule( + argument_spec = dict( + name = dict(required=True, + type='str'), + description = dict(default=None, + type='str'), + position = dict(default='', + type='str'), + ou = dict(default='', + type='str'), + subpath = dict(default='cn=groups', + type='str'), + state = dict(default='present', + choices=['present', 'absent'], + type='str') + ), + supports_check_mode=True + ) + name = module.params['name'] + description = module.params['description'] + position = module.params['position'] + ou = module.params['ou'] + subpath = module.params['subpath'] + state = module.params['state'] + changed = False + + groups = list(ldap_search( + '(&(objectClass=posixGroup)(cn={}))'.format(name), + attr=['cn'] + )) + if position != '': + container = position + else: + if ou != '': + ou = 'ou={},'.format(ou) + if subpath != '': + subpath = '{},'.format(subpath) + container = '{}{}{}'.format(subpath, ou, base_dn()) + group_dn = 'cn={},{}'.format(name, container) + + exists = bool(len(groups)) + + if state == 'present': + try: + if not exists: + grp = umc_module_for_add('groups/group', container) + else: + grp = umc_module_for_edit('groups/group', group_dn) + grp['name'] = name + grp['description'] = description + diff = grp.diff() + changed = grp.diff() != [] + if not module.check_mode: + if not exists: + grp.create() + else: + grp.modify() + except: + module.fail_json( + msg="Creating/editing group {} in {} failed".format(name, container) + ) + + if state == 'absent' and exists: + try: + grp = umc_module_for_edit('groups/group', group_dn) + if not module.check_mode: + grp.remove() + changed = True + except: + module.fail_json( + msg="Removing group {} failed".format(name) + ) + + module.exit_json( + changed=changed, + name=name, + diff=diff, + container=container + ) + + +if __name__ == '__main__': + main() From 4b7b70b35f1034af7da52830e276f73badf8190d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Tue, 30 Aug 2016 20:40:06 +0200 Subject: [PATCH 2027/2522] new modules for managing exoscale DNS (#2788) --- network/exoscale/__init__.py | 0 network/exoscale/exo_dns_domain.py | 255 +++++++++++++++++++ network/exoscale/exo_dns_record.py | 391 +++++++++++++++++++++++++++++ 3 files changed, 646 insertions(+) create mode 100644 network/exoscale/__init__.py create mode 100644 network/exoscale/exo_dns_domain.py create mode 100644 network/exoscale/exo_dns_record.py diff --git a/network/exoscale/__init__.py b/network/exoscale/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/network/exoscale/exo_dns_domain.py b/network/exoscale/exo_dns_domain.py new file mode 100644 index 00000000000..d886728bea5 --- /dev/null +++ b/network/exoscale/exo_dns_domain.py @@ -0,0 +1,255 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2016, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: exo_dns_domain +short_description: Manages domain records on Exoscale DNS API. +description: + - Create and remove domain records. +version_added: "2.2" +author: "René Moser (@resmo)" +options: + name: + description: + - Name of the record. + required: true + state: + description: + - State of the resource. + required: false + default: 'present' + choices: [ 'present', 'absent' ] + api_key: + description: + - API key of the Exoscale DNS API. + required: false + default: null + api_secret: + description: + - Secret key of the Exoscale DNS API. + required: false + default: null + api_timeout: + description: + - HTTP timeout to Exoscale DNS API. + required: false + default: 10 + api_region: + description: + - Name of the ini section in the C(cloustack.ini) file. + required: false + default: cloudstack + validate_certs: + description: + - Validate SSL certs of the Exoscale DNS API. + required: false + default: true +requirements: + - "python >= 2.6" +notes: + - As Exoscale DNS uses the same API key and secret for all services, we reuse the config used for Exscale Compute based on CloudStack. + The config is read from several locations, in the following order. + The C(CLOUDSTACK_KEY), C(CLOUDSTACK_SECRET) environment variables. + A C(CLOUDSTACK_CONFIG) environment variable pointing to an C(.ini) file, + A C(cloudstack.ini) file in the current working directory. + A C(.cloudstack.ini) file in the users home directory. + Optionally multiple credentials and endpoints can be specified using ini sections in C(cloudstack.ini). + Use the argument C(api_region) to select the section name, default section is C(cloudstack). + - This module does not support multiple A records and will complain properly if you try. + - More information Exoscale DNS can be found on https://community.exoscale.ch/documentation/dns/. + - This module supports check mode and diff. +''' + +EXAMPLES = ''' +# Create a domain. +- local_action: + module: exo_dns_domain + name: example.com + +# Remove a domain. +- local_action: + module: exo_dns_domain + name: example.com + state: absent +''' + +RETURN = ''' +--- +exo_dns_domain: + description: API domain results + returned: success + type: dictionary + contains: + account_id: + description: Your account ID + returned: success + type: int + sample: 34569 + auto_renew: + description: Whether domain is auto renewed or not + returned: success + type: bool + sample: false + created_at: + description: When the domain was created + returned: success + type: string + sample: "2016-08-12T15:24:23.989Z" + expires_on: + description: When the domain expires + returned: success + type: string + sample: "2016-08-12T15:24:23.989Z" + id: + description: ID of the domain + returned: success + type: int + sample: "2016-08-12T15:24:23.989Z" + lockable: + description: Whether the domain is lockable or not + returned: success + type: bool + sample: true + name: + description: Domain name + returned: success + type: string + sample: example.com + record_count: + description: Number of records related to this domain + returned: success + type: int + sample: 5 + registrant_id: + description: ID of the registrant + returned: success + type: int + sample: null + service_count: + description: Number of services + returned: success + type: int + sample: 0 + state: + description: State of the domain + returned: success + type: string + sample: "hosted" + token: + description: Token + returned: success + type: string + sample: "r4NzTRp6opIeFKfaFYvOd6MlhGyD07jl" + unicode_name: + description: Domain name as unicode + returned: success + type: string + sample: "example.com" + updated_at: + description: When the domain was updated last. + returned: success + type: string + sample: "2016-08-12T15:24:23.989Z" + user_id: + description: ID of the user + returned: success + type: int + sample: null + whois_protected: + description: Wheter the whois is protected or not + returned: success + type: bool + sample: false +''' + +# import exoscale common +from ansible.module_utils.exoscale import * + + +class ExoDnsDomain(ExoDns): + + def __init__(self, module): + super(ExoDnsDomain, self).__init__(module) + self.name = self.module.params.get('name').lower() + + def get_domain(self): + domains = self.api_query("/domains", "GET") + for z in domains: + if z['domain']['name'].lower() == self.name: + return z + return None + + def present_domain(self): + domain = self.get_domain() + data = { + 'domain': { + 'name': self.name, + } + } + if not domain: + self.result['diff']['after'] = data['domain'] + self.result['changed'] = True + if not self.module.check_mode: + domain = self.api_query("/domains", "POST", data) + return domain + + def absent_domain(self): + domain = self.get_domain() + if domain: + self.result['diff']['before'] = domain + self.result['changed'] = True + if not self.module.check_mode: + self.api_query("/domains/%s" % domain['domain']['name'], "DELETE") + return domain + + def get_result(self, resource): + if resource: + self.result['exo_dns_domain'] = resource['domain'] + return self.result + + +def main(): + argument_spec = exo_dns_argument_spec() + argument_spec.update(dict( + name=dict(required=True), + state=dict(choices=['present', 'absent'], default='present'), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=exo_dns_required_together(), + supports_check_mode=True + ) + + exo_dns_domain = ExoDnsDomain(module) + if module.params.get('state') == "present": + resource = exo_dns_domain.present_domain() + else: + resource = exo_dns_domain.absent_domain() + result = exo_dns_domain.get_result(resource) + + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() diff --git a/network/exoscale/exo_dns_record.py b/network/exoscale/exo_dns_record.py new file mode 100644 index 00000000000..6395990639e --- /dev/null +++ b/network/exoscale/exo_dns_record.py @@ -0,0 +1,391 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2016, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: exo_dns_record +short_description: Manages DNS records on Exoscale DNS. +description: + - Create, update and delete records. +version_added: "2.2" +author: "René Moser (@resmo)" +options: + name: + description: + - Name of the record. + required: false + default: "" + domain: + description: + - Domain the record is related to. + required: true + record_type: + description: + - Type of the record. + required: false + default: A + choices: ['A', 'ALIAS', 'CNAME', 'MX', 'SPF', 'URL', 'TXT', 'NS', 'SRV', 'NAPTR', 'PTR', 'AAAA', 'SSHFP', 'HINFO', 'POOL'] + aliases: ['rtype', 'type'] + content: + description: + - Content of the record. + - Required if C(state=present) or C(name="") + required: false + default: null + aliases: ['value', 'address'] + ttl: + description: + - TTL of the record in seconds. + required: false + default: 3600 + prio: + description: + - Priority of the record. + required: false + default: null + aliases: ['priority'] + multiple: + description: + - Whether there are more than one records with similar C(name). + - Only allowed with C(record_type=A). + - C(content) will not be updated as it is used as key to find the record. + required: false + default: null + aliases: ['priority'] + state: + description: + - State of the record. + required: false + default: 'present' + choices: [ 'present', 'absent' ] + api_key: + description: + - API key of the Exoscale DNS API. + required: false + default: null + api_secret: + description: + - Secret key of the Exoscale DNS API. + required: false + default: null + api_timeout: + description: + - HTTP timeout to Exoscale DNS API. + required: false + default: 10 + api_region: + description: + - Name of the ini section in the C(cloustack.ini) file. + required: false + default: cloudstack + validate_certs: + description: + - Validate SSL certs of the Exoscale DNS API. + required: false + default: true +requirements: + - "python >= 2.6" +notes: + - As Exoscale DNS uses the same API key and secret for all services, we reuse the config used for Exscale Compute based on CloudStack. + The config is read from several locations, in the following order. + The C(CLOUDSTACK_KEY), C(CLOUDSTACK_SECRET) environment variables. + A C(CLOUDSTACK_CONFIG) environment variable pointing to an C(.ini) file, + A C(cloudstack.ini) file in the current working directory. + A C(.cloudstack.ini) file in the users home directory. + Optionally multiple credentials and endpoints can be specified using ini sections in C(cloudstack.ini). + Use the argument C(api_region) to select the section name, default section is C(cloudstack). + - This module does not support multiple A records and will complain properly if you try. + - More information Exoscale DNS can be found on https://community.exoscale.ch/documentation/dns/. + - This module supports check mode and diff. +''' + +EXAMPLES = ''' +# Create or update an A record. +- local_action: + module: exo_dns_record + name: web-vm-1 + domain: example.com + content: 1.2.3.4 + +# Update an existing A record with a new IP. +- local_action: + module: exo_dns_record + name: web-vm-1 + domain: example.com + content: 1.2.3.5 + +# Create another A record with same name. +- local_action: + module: exo_dns_record + name: web-vm-1 + domain: example.com + content: 1.2.3.6 + multiple: yes + +# Create or update a CNAME record. +- local_action: + module: exo_dns_record + name: www + domain: example.com + record_type: CNAME + content: web-vm-1 + +# Create or update a MX record. +- local_action: + module: exo_dns_record + domain: example.com + record_type: MX + content: mx1.example.com + prio: 10 + +# delete a MX record. +- local_action: + module: exo_dns_record + domain: example.com + record_type: MX + content: mx1.example.com + state: absent + +# Remove a record. +- local_action: + module: exo_dns_record + name: www + domain: example.com + state: absent +''' + +RETURN = ''' +--- +exo_dns_record: + description: API record results + returned: success + type: dictionary + contains: + content: + description: value of the record + returned: success + type: string + sample: 1.2.3.4 + created_at: + description: When the record was created + returned: success + type: string + sample: "2016-08-12T15:24:23.989Z" + domain: + description: Name of the domain + returned: success + type: string + sample: example.com + domain_id: + description: ID of the domain + returned: success + type: int + sample: 254324 + id: + description: ID of the record + returned: success + type: int + sample: 254324 + name: + description: name of the record + returned: success + type: string + sample: www + parent_id: + description: ID of the parent + returned: success + type: int + sample: null + prio: + description: Priority of the record + returned: success + type: int + sample: 10 + record_type: + description: Priority of the record + returned: success + type: string + sample: A + system_record: + description: Whether the record is a system record or not + returned: success + type: bool + sample: false + ttl: + description: Time to live of the record + returned: success + type: int + sample: 3600 + updated_at: + description: When the record was updated + returned: success + type: string + sample: "2016-08-12T15:24:23.989Z" +''' + +# import exoscale common +from ansible.module_utils.exoscale import * + + +class ExoDnsRecord(ExoDns): + + def __init__(self, module): + super(ExoDnsRecord, self).__init__(module) + + self.content = self.module.params.get('content') + if self.content: + self.content = self.content.lower() + + self.domain = self.module.params.get('domain').lower() + self.name = self.module.params.get('name').lower() + if self.name == self.domain: + self.name = "" + + self.multiple = self.module.params.get('multiple') + self.record_type = self.module.params.get('record_type') + if self.multiple and self.record_type != 'A': + self.module.fail_json("Multiple is only usable with record_type A") + + + def _create_record(self, record): + self.result['changed'] = True + data = { + 'record': { + 'name': self.name, + 'record_type': self.record_type, + 'content': self.content, + 'ttl': self.module.params.get('ttl'), + 'prio': self.module.params.get('prio'), + } + } + self.result['diff']['after'] = data['record'] + if not self.module.check_mode: + record = self.api_query("/domains/%s/records" % self.domain, "POST", data) + return record + + def _update_record(self, record): + data = { + 'record': { + 'name': self.name, + 'content': self.content, + 'ttl': self.module.params.get('ttl'), + 'prio': self.module.params.get('prio'), + } + } + if self.has_changed(data['record'], record['record']): + self.result['changed'] = True + if not self.module.check_mode: + record = self.api_query("/domains/%s/records/%s" % (self.domain, record['record']['id']), "PUT", data) + return record + + def get_record(self): + domain = self.module.params.get('domain') + records = self.api_query("/domains/%s/records" % domain, "GET") + + record = None + for r in records: + found_record = None + if r['record']['record_type'] == self.record_type: + r_name = r['record']['name'].lower() + r_content = r['record']['content'].lower() + + # there are multiple A records but we found an exact match + if self.multiple and self.name == r_name and self.content == r_content: + record = r + break + + # We do not expect to found more then one record with that content + if not self.multiple and not self.name and self.content == r_content: + found_record = r + + # We do not expect to found more then one record with that name + elif not self.multiple and self.name and self.name == r_name: + found_record = r + + if record and found_record: + self.module.fail_json(msg="More than one record with your params. Use multiple=yes for more than one A record.") + if found_record: + record = found_record + return record + + def present_record(self): + record = self.get_record() + if not record: + record = self._create_record(record); + else: + record = self._update_record(record); + return record + + def absent_record(self): + record = self.get_record() + if record: + self.result['diff']['before'] = record + self.result['changed'] = True + if not self.module.check_mode: + self.api_query("/domains/%s/records/%s" % (self.domain, record['record']['id']), "DELETE") + return record + + def get_result(self, resource): + if resource: + self.result['exo_dns_record'] = resource['record'] + self.result['exo_dns_record']['domain'] = self.domain + return self.result + + +def main(): + argument_spec = exo_dns_argument_spec() + argument_spec.update(dict( + name=dict(default=""), + record_type=dict(choices=['A', 'ALIAS', 'CNAME', 'MX', 'SPF', 'URL', 'TXT', 'NS', 'SRV', 'NAPTR', 'PTR', 'AAAA', 'SSHFP', 'HINFO', 'POOL'], aliases=['rtype', 'type'], default='A'), + content=dict(aliases=['value', 'address']), + multiple=(dict(type='bool', default=False)), + ttl=dict(type='int', default=3600), + prio=dict(type='int', aliases=['priority']), + domain=dict(required=True), + state=dict(choices=['present', 'absent'], default='present'), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=exo_dns_required_together(), + required_if=[ + ['state', 'present', ['content']], + ['name', '', ['content']], + ], + required_one_of=[ + ['content', 'name'], + ], + supports_check_mode=True, + ) + + exo_dns_record = ExoDnsRecord(module) + if module.params.get('state') == "present": + resource = exo_dns_record.present_record() + else: + resource = exo_dns_record.absent_record() + + result = exo_dns_record.get_result(resource) + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From b9b781c37000a00c9153edd8cde4635b9b534a0f Mon Sep 17 00:00:00 2001 From: TimothyVandenbrande Date: Tue, 30 Aug 2016 20:43:23 +0200 Subject: [PATCH 2028/2522] This is an ansible module to control/create/adapt/remove VMs on a RHEV/oVirt environment. (#2202) --- cloud/misc/rhevm.py | 1530 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1530 insertions(+) create mode 100644 cloud/misc/rhevm.py diff --git a/cloud/misc/rhevm.py b/cloud/misc/rhevm.py new file mode 100644 index 00000000000..523f6f6c0b3 --- /dev/null +++ b/cloud/misc/rhevm.py @@ -0,0 +1,1530 @@ +#!/usr/bin/python + +# (c) 2016, Timothy Vandenbrande +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: rhevm +author: Timothy Vandenbrande +short_description: RHEV/oVirt automation +description: + - Allows you to create/remove/update or powermanage virtual machines on a RHEV/oVirt platform. +version_added: "2.2" +requirements: + - ovirtsdk +options: + user: + description: + - The user to authenticate with. + default: "admin@internal" + required: false + server: + description: + - The name/ip of your RHEV-m/oVirt instance. + default: "127.0.0.1" + required: false + port: + description: + - The port on which the API is reacheable. + default: "443" + required: false + insecure_api: + description: + - A boolean switch to make a secure or insecure connection to the server. + default: false + required: false + name: + description: + - The name of the VM. + cluster: + description: + - The rhev/ovirt cluster in which you want you VM to start. + required: false + datacenter: + description: + - The rhev/ovirt datacenter in which you want you VM to start. + required: false + default: "Default" + state: + description: + - This serves to create/remove/update or powermanage your VM. + default: "present" + required: false + choices: ['ping', 'present', 'absent', 'up', 'down', 'restarted', 'cd', 'info'] + image: + description: + - The template to use for the VM. + default: null + required: false + type: + description: + - To define if the VM is a server or desktop. + default: server + required: false + choices: [ 'server', 'desktop', 'host' ] + vmhost: + description: + - The host you wish your VM to run on. + required: false + vmcpu: + description: + - The number of CPUs you want in your VM. + default: "2" + required: false + cpu_share: + description: + - This parameter is used to configure the cpu share. + default: "0" + required: false + vmmem: + description: + - The amount of memory you want your VM to use (in GB). + default: "1" + required: false + osver: + description: + - The operationsystem option in RHEV/oVirt. + default: "rhel_6x64" + required: false + mempol: + description: + - The minimum amount of memory you wish to reserve for this system. + default: "1" + required: false + vm_ha: + description: + - To make your VM High Available. + default: true + required: false + disks: + description: + - This option uses complex arguments and is a list of disks with the options name, size and domain. + required: false + ifaces: + description: + - This option uses complex arguments and is a list of interfaces with the options name and vlan. + aliases: ['nics', 'interfaces'] + required: false + boot_order: + description: + - This option uses complex arguments and is a list of items that specify the bootorder. + default: ["network","hd"] + required: false + del_prot: + description: + - This option sets the delete protection checkbox. + default: true + required: false + cd_drive: + description: + - The CD you wish to have mounted on the VM when I(state = 'CD'). + default: null + required: false + timeout: + description: + - The timeout you wish to define for power actions. + - When I(state = 'up') + - When I(state = 'down') + - When I(state = 'restarted') + default: null + required: false +''' + +RETURN = ''' +vm: + description: Returns all of the VMs variables and execution. + returned: always + type: dict + sample: '{ + "boot_order": [ + "hd", + "network" + ], + "changed": true, + "changes": [ + "Delete Protection" + ], + "cluster": "C1", + "cpu_share": "0", + "created": false, + "datacenter": "Default", + "del_prot": true, + "disks": [ + { + "domain": "ssd-san", + "name": "OS", + "size": 40 + } + ], + "eth0": "00:1b:4a:1f:de:f4", + "eth1": "00:1b:4a:1f:de:f5", + "eth2": "00:1b:4a:1f:de:f6", + "exists": true, + "failed": false, + "ifaces": [ + { + "name": "eth0", + "vlan": "Management" + }, + { + "name": "eth1", + "vlan": "Internal" + }, + { + "name": "eth2", + "vlan": "External" + } + ], + "image": false, + "mempol": "0", + "msg": [ + "VM exists", + "cpu_share was already set to 0", + "VM high availability was already set to True", + "The boot order has already been set", + "VM delete protection has been set to True", + "Disk web2_Disk0_OS already exists", + "The VM starting host was already set to host416" + ], + "name": "web2", + "type": "server", + "uuid": "4ba5a1be-e60b-4368-9533-920f156c817b", + "vm_ha": true, + "vmcpu": "4", + "vmhost": "host416", + "vmmem": "16" + }' +''' + +EXAMPLES = ''' +# basic get info from VM + action: rhevm + args: + name: "demo" + user: "{{ rhev.admin.name }}" + password: "{{ rhev.admin.pass }}" + server: "rhevm01" + state: "info" + +# basic create example from image + action: rhevm + args: + name: "demo" + user: "{{ rhev.admin.name }}" + password: "{{ rhev.admin.pass }}" + server: "rhevm01" + state: "present" + image: "centos7_x64" + cluster: "centos" + +# power management + action: rhevm + args: + name: "uptime_server" + user: "{{ rhev.admin.name }}" + password: "{{ rhev.admin.pass }}" + server: "rhevm01" + cluster: "RH" + state: "down" + image: "centos7_x64" + cluster: "centos + +# multi disk, multi nic create example + action: rhevm + args: + name: "server007" + user: "{{ rhev.admin.name }}" + password: "{{ rhev.admin.pass }}" + server: "rhevm01" + cluster: "RH" + state: "present" + type: "server" + vmcpu: 4 + vmmem: 2 + ifaces: + - name: "eth0" + vlan: "vlan2202" + - name: "eth1" + vlan: "vlan36" + - name: "eth2" + vlan: "vlan38" + - name: "eth3" + vlan: "vlan2202" + disks: + - name: "root" + size: 10 + domain: "ssd-san" + - name: "swap" + size: 10 + domain: "15kiscsi-san" + - name: "opt" + size: 10 + domain: "15kiscsi-san" + - name: "var" + size: 10 + domain: "10kiscsi-san" + - name: "home" + size: 10 + domain: "sata-san" + boot_order: + - "network" + - "hd" + +# add a CD to the disk cd_drive + action: rhevm + args: + name: 'server007' + user: "{{ rhev.admin.name }}" + password: "{{ rhev.admin.pass }}" + state: 'cd' + cd_drive: 'rhev-tools-setup.iso' + +# new host deployment + host network configuration + action: rhevm + args: + name: "ovirt_node007" + password: "{{ rhevm.admin.pass }}" + type: "host" + state: present + cluster: "rhevm01" + ifaces: + - name: em1 + - name: em2 + - name: p3p1 + ip: '172.31.224.200' + netmask: '255.255.254.0' + - name: p3p2 + ip: '172.31.225.200' + netmask: '255.255.254.0' + - name: bond0 + bond: + - em1 + - em2 + network: 'rhevm' + ip: '172.31.222.200' + netmask: '255.255.255.0' + management: True + - name: bond0.36 + network: 'vlan36' + ip: '10.2.36.200' + netmask: '255.255.254.0' + gateway: '10.2.36.254' + - name: bond0.2202 + network: 'vlan2202' + - name: bond0.38 + network: 'vlan38' +''' + +import time +import sys +import traceback +import json + +try: + from ovirtsdk.api import API + from ovirtsdk.xml import params + HAS_SDK = True +except ImportError: + HAS_SDK = False + +RHEV_FAILED = 1 +RHEV_SUCCESS = 0 +RHEV_UNAVAILABLE = 2 + +RHEV_TYPE_OPTS = ['server', 'desktop', 'host'] +STATE_OPTS = ['ping', 'present', 'absent', 'up', 'down', 'restart', 'cd', 'info'] + +global msg, changed, failed +msg = [] +changed = False +failed = False + + +class RHEVConn(object): + 'Connection to RHEV-M' + def __init__(self, module): + self.module = module + + user = module.params.get('user') + password = module.params.get('password') + server = module.params.get('server') + port = module.params.get('port') + insecure_api = module.params.get('insecure_api') + + url = "https://%s:%s" % (server, port) + + try: + api = API(url=url, username=user, password=password, insecure=str(insecure_api)) + api.test() + self.conn = api + except: + raise Exception("Failed to connect to RHEV-M.") + + def __del__(self): + self.conn.disconnect() + + def createVMimage(self, name, cluster, template): + try: + vmparams = params.VM( + name=name, + cluster=self.conn.clusters.get(name=cluster), + template=self.conn.templates.get(name=template), + disks=params.Disks(clone=True) + ) + self.conn.vms.add(vmparams) + setMsg("VM is created") + setChanged() + return True + except Exception as e: + setMsg("Failed to create VM") + setMsg(str(e)) + setFailed() + return False + + def createVM(self, name, cluster, os, actiontype): + try: + vmparams = params.VM( + name=name, + cluster=self.conn.clusters.get(name=cluster), + os=params.OperatingSystem(type_=os), + template=self.conn.templates.get(name="Blank"), + type_=actiontype + ) + self.conn.vms.add(vmparams) + setMsg("VM is created") + setChanged() + return True + except Exception as e: + setMsg("Failed to create VM") + setMsg(str(e)) + setFailed() + return False + + def createDisk(self, vmname, diskname, disksize, diskdomain, diskinterface, diskformat, diskallocationtype, diskboot): + VM = self.get_VM(vmname) + + newdisk = params.Disk( + name=diskname, + size=1024 * 1024 * 1024 * int(disksize), + wipe_after_delete=True, + sparse=diskallocationtype, + interface=diskinterface, + format=diskformat, + bootable=diskboot, + storage_domains=params.StorageDomains( + storage_domain=[self.get_domain(diskdomain)] + ) + ) + + try: + VM.disks.add(newdisk) + VM.update() + setMsg("Successfully added disk " + diskname) + setChanged() + except Exception as e: + setFailed() + setMsg("Error attaching " + diskname + "disk, please recheck and remove any leftover configuration.") + setMsg(str(e)) + return False + + try: + currentdisk = VM.disks.get(name=diskname) + attempt = 1 + while currentdisk.status.state != 'ok': + currentdisk = VM.disks.get(name=diskname) + if attempt == 100: + setMsg("Error, disk %s, state %s" % (diskname, str(currentdisk.status.state))) + raise + else: + attempt += 1 + time.sleep(2) + setMsg("The disk " + diskname + " is ready.") + except Exception as e: + setFailed() + setMsg("Error getting the state of " + diskname + ".") + setMsg(str(e)) + return False + return True + + def createNIC(self, vmname, nicname, vlan, interface): + VM = self.get_VM(vmname) + CLUSTER = self.get_cluster_byid(VM.cluster.id) + DC = self.get_DC_byid(CLUSTER.data_center.id) + newnic = params.NIC( + name=nicname, + network=DC.networks.get(name=vlan), + interface=interface + ) + + try: + VM.nics.add(newnic) + VM.update() + setMsg("Successfully added iface " + nicname) + setChanged() + except Exception as e: + setFailed() + setMsg("Error attaching " + nicname + " iface, please recheck and remove any leftover configuration.") + setMsg(str(e)) + return False + + try: + currentnic = VM.nics.get(name=nicname) + attempt = 1 + while currentnic.active is not True: + currentnic = VM.nics.get(name=nicname) + if attempt == 100: + setMsg("Error, iface %s, state %s" % (nicname, str(currentnic.active))) + raise + else: + attempt += 1 + time.sleep(2) + setMsg("The iface " + nicname + " is ready.") + except Exception as e: + setFailed() + setMsg("Error getting the state of " + nicname + ".") + setMsg(str(e)) + return False + return True + + def get_DC(self, dc_name): + return self.conn.datacenters.get(name=dc_name) + + def get_DC_byid(self, dc_id): + return self.conn.datacenters.get(id=dc_id) + + def get_VM(self, vm_name): + return self.conn.vms.get(name=vm_name) + + def get_cluster_byid(self, cluster_id): + return self.conn.clusters.get(id=cluster_id) + + def get_cluster(self, cluster_name): + return self.conn.clusters.get(name=cluster_name) + + def get_domain_byid(self, dom_id): + return self.conn.storagedomains.get(id=dom_id) + + def get_domain(self, domain_name): + return self.conn.storagedomains.get(name=domain_name) + + def get_disk(self, disk): + return self.conn.disks.get(disk) + + def get_network(self, dc_name, network_name): + return self.get_DC(dc_name).networks.get(network_name) + + def get_network_byid(self, network_id): + return self.conn.networks.get(id=network_id) + + def get_NIC(self, vm_name, nic_name): + return self.get_VM(vm_name).nics.get(nic_name) + + def get_Host(self, host_name): + return self.conn.hosts.get(name=host_name) + + def get_Host_byid(self, host_id): + return self.conn.hosts.get(id=host_id) + + def set_Memory(self, name, memory): + VM = self.get_VM(name) + VM.memory = int(int(memory) * 1024 * 1024 * 1024) + try: + VM.update() + setMsg("The Memory has been updated.") + setChanged() + return True + except Exception as e: + setMsg("Failed to update memory.") + setMsg(str(e)) + setFailed() + return False + + def set_Memory_Policy(self, name, memory_policy): + VM = self.get_VM(name) + VM.memory_policy.guaranteed = int(int(memory_policy) * 1024 * 1024 * 1024) + try: + VM.update() + setMsg("The memory policy has been updated.") + setChanged() + return True + except Exception as e: + setMsg("Failed to update memory policy.") + setMsg(str(e)) + setFailed() + return False + + def set_CPU(self, name, cpu): + VM = self.get_VM(name) + VM.cpu.topology.cores = int(cpu) + try: + VM.update() + setMsg("The number of CPUs has been updated.") + setChanged() + return True + except Exception as e: + setMsg("Failed to update the number of CPUs.") + setMsg(str(e)) + setFailed() + return False + + def set_CPU_share(self, name, cpu_share): + VM = self.get_VM(name) + VM.cpu_shares = int(cpu_share) + try: + VM.update() + setMsg("The CPU share has been updated.") + setChanged() + return True + except Exception as e: + setMsg("Failed to update the CPU share.") + setMsg(str(e)) + setFailed() + return False + + def set_Disk(self, diskname, disksize, diskinterface, diskboot): + DISK = self.get_disk(diskname) + setMsg("Checking disk " + diskname) + if DISK.get_bootable() != diskboot: + try: + DISK.set_bootable(diskboot) + setMsg("Updated the boot option on the disk.") + setChanged() + except Exception as e: + setMsg("Failed to set the boot option on the disk.") + setMsg(str(e)) + setFailed() + return False + else: + setMsg("The boot option of the disk is correct") + if int(DISK.size) < (1024 * 1024 * 1024 * int(disksize)): + try: + DISK.size = (1024 * 1024 * 1024 * int(disksize)) + setMsg("Updated the size of the disk.") + setChanged() + except Exception as e: + setMsg("Failed to update the size of the disk.") + setMsg(str(e)) + setFailed() + return False + elif int(DISK.size) < (1024 * 1024 * 1024 * int(disksize)): + setMsg("Shrinking disks is not supported") + setMsg(str(e)) + setFailed() + return False + else: + setMsg("The size of the disk is correct") + if str(DISK.interface) != str(diskinterface): + try: + DISK.interface = diskinterface + setMsg("Updated the interface of the disk.") + setChanged() + except Exception as e: + setMsg("Failed to update the interface of the disk.") + setMsg(str(e)) + setFailed() + return False + else: + setMsg("The interface of the disk is correct") + return True + + def set_NIC(self, vmname, nicname, newname, vlan, interface): + NIC = self.get_NIC(vmname, nicname) + VM = self.get_VM(vmname) + CLUSTER = self.get_cluster_byid(VM.cluster.id) + DC = self.get_DC_byid(CLUSTER.data_center.id) + NETWORK = self.get_network(str(DC.name), vlan) + checkFail() + if NIC.name != newname: + NIC.name = newname + setMsg('Updating iface name to ' + newname) + setChanged() + if str(NIC.network.id) != str(NETWORK.id): + NIC.set_network(NETWORK) + setMsg('Updating iface network to ' + vlan) + setChanged() + if NIC.interface != interface: + NIC.interface = interface + setMsg('Updating iface interface to ' + interface) + setChanged() + try: + NIC.update() + setMsg('iface has succesfully been updated.') + except Exception as e: + setMsg("Failed to update the iface.") + setMsg(str(e)) + setFailed() + return False + return True + + def set_DeleteProtection(self, vmname, del_prot): + VM = self.get_VM(vmname) + VM.delete_protected = del_prot + try: + VM.update() + setChanged() + except Exception as e: + setMsg("Failed to update delete protection.") + setMsg(str(e)) + setFailed() + return False + return True + + def set_BootOrder(self, vmname, boot_order): + VM = self.get_VM(vmname) + bootorder = [] + for device in boot_order: + bootorder.append(params.Boot(dev=device)) + VM.os.boot = bootorder + + try: + VM.update() + setChanged() + except Exception as e: + setMsg("Failed to update the boot order.") + setMsg(str(e)) + setFailed() + return False + return True + + def set_Host(self, host_name, cluster, ifaces): + HOST = self.get_Host(host_name) + CLUSTER = self.get_cluster(cluster) + + if HOST is None: + setMsg("Host does not exist.") + ifacelist = dict() + networklist = [] + manageip = '' + + try: + for iface in ifaces: + try: + setMsg('creating host interface ' + iface['name']) + if 'management' in iface.keys(): + manageip = iface['ip'] + if 'boot_protocol' not in iface.keys(): + if 'ip' in iface.keys(): + iface['boot_protocol'] = 'static' + else: + iface['boot_protocol'] = 'none' + if 'ip' not in iface.keys(): + iface['ip'] = '' + if 'netmask' not in iface.keys(): + iface['netmask'] = '' + if 'gateway' not in iface.keys(): + iface['gateway'] = '' + + if 'network' in iface.keys(): + if 'bond' in iface.keys(): + bond = [] + for slave in iface['bond']: + bond.append(ifacelist[slave]) + try: + tmpiface = params.Bonding( + slaves = params.Slaves(host_nic = bond), + options = params.Options( + option = [ + params.Option(name = 'miimon', value = '100'), + params.Option(name = 'mode', value = '4') + ] + ) + ) + except Exception as e: + setMsg('Failed to create the bond for ' + iface['name']) + setFailed() + setMsg(str(e)) + return False + try: + tmpnetwork = params.HostNIC( + network = params.Network(name = iface['network']), + name = iface['name'], + boot_protocol = iface['boot_protocol'], + ip = params.IP( + address = iface['ip'], + netmask = iface['netmask'], + gateway = iface['gateway'] + ), + override_configuration = True, + bonding = tmpiface) + networklist.append(tmpnetwork) + setMsg('Applying network ' + iface['name']) + except Exception as e: + setMsg('Failed to set' + iface['name'] + ' as network interface') + setFailed() + setMsg(str(e)) + return False + else: + tmpnetwork = params.HostNIC( + network = params.Network(name = iface['network']), + name = iface['name'], + boot_protocol = iface['boot_protocol'], + ip = params.IP( + address = iface['ip'], + netmask = iface['netmask'], + gateway = iface['gateway'] + )) + networklist.append(tmpnetwork) + setMsg('Applying network ' + iface['name']) + else: + tmpiface = params.HostNIC( + name=iface['name'], + network=params.Network(), + boot_protocol=iface['boot_protocol'], + ip=params.IP( + address=iface['ip'], + netmask=iface['netmask'], + gateway=iface['gateway'] + )) + ifacelist[iface['name']] = tmpiface + except Exception as e: + setMsg('Failed to set ' + iface['name']) + setFailed() + setMsg(str(e)) + return False + except Exception as e: + setMsg('Failed to set networks') + setMsg(str(e)) + setFailed() + return False + + if manageip == '': + setMsg('No management network is defined') + setFailed() + return False + + try: + HOST = params.Host(name=host_name, address=manageip, cluster=CLUSTER, ssh=params.SSH(authentication_method='publickey')) + if self.conn.hosts.add(HOST): + setChanged() + HOST = self.get_Host(host_name) + state = HOST.status.state + while (state != 'non_operational' and state != 'up'): + HOST = self.get_Host(host_name) + state = HOST.status.state + time.sleep(1) + if state == 'non_responsive': + setMsg('Failed to add host to RHEVM') + setFailed() + return False + + setMsg('status host: up') + time.sleep(5) + + HOST = self.get_Host(host_name) + state = HOST.status.state + setMsg('State before setting to maintenance: ' + str(state)) + HOST.deactivate() + while state != 'maintenance': + HOST = self.get_Host(host_name) + state = HOST.status.state + time.sleep(1) + setMsg('status host: maintenance') + + try: + HOST.nics.setupnetworks(params.Action( + force=True, + check_connectivity = False, + host_nics = params.HostNics(host_nic = networklist) + )) + setMsg('nics are set') + except Exception as e: + setMsg('Failed to apply networkconfig') + setFailed() + setMsg(str(e)) + return False + + try: + HOST.commitnetconfig() + setMsg('Network config is saved') + except Exception as e: + setMsg('Failed to save networkconfig') + setFailed() + setMsg(str(e)) + return False + except Exception as e: + if 'The Host name is already in use' in str(e): + setMsg("Host already exists") + else: + setMsg("Failed to add host") + setFailed() + setMsg(str(e)) + return False + + HOST.activate() + while state != 'up': + HOST = self.get_Host(host_name) + state = HOST.status.state + time.sleep(1) + if state == 'non_responsive': + setMsg('Failed to apply networkconfig.') + setFailed() + return False + setMsg('status host: up') + else: + setMsg("Host exists.") + + return True + + def del_NIC(self, vmname, nicname): + return self.get_NIC(vmname, nicname).delete() + + def remove_VM(self, vmname): + VM = self.get_VM(vmname) + try: + VM.delete() + except Exception as e: + setMsg("Failed to remove VM.") + setMsg(str(e)) + setFailed() + return False + return True + + def start_VM(self, vmname, timeout): + VM = self.get_VM(vmname) + try: + VM.start() + except Exception as e: + setMsg("Failed to start VM.") + setMsg(str(e)) + setFailed() + return False + return self.wait_VM(vmname, "up", timeout) + + def wait_VM(self, vmname, state, timeout): + VM = self.get_VM(vmname) + while VM.status.state != state: + VM = self.get_VM(vmname) + time.sleep(10) + if timeout is not False: + timeout -= 10 + if timeout <= 0: + setMsg("Timeout expired") + setFailed() + return False + return True + + def stop_VM(self, vmname, timeout): + VM = self.get_VM(vmname) + try: + VM.stop() + except Exception as e: + setMsg("Failed to stop VM.") + setMsg(str(e)) + setFailed() + return False + return self.wait_VM(vmname, "down", timeout) + + def set_CD(self, vmname, cd_drive): + VM = self.get_VM(vmname) + try: + if str(VM.status.state) == 'down': + cdrom = params.CdRom(file=cd_iso) + VM.cdroms.add(cdrom) + setMsg("Attached the image.") + setChanged() + else: + cdrom = VM.cdroms.get(id="00000000-0000-0000-0000-000000000000") + cdrom.set_file(cd_iso) + cdrom.update(current=True) + setMsg("Attached the image.") + setChanged() + except Exception as e: + setMsg("Failed to attach image.") + setMsg(str(e)) + setFailed() + return False + return True + + def set_VM_Host(self, vmname, vmhost): + VM = self.get_VM(vmname) + HOST = self.get_Host(vmhost) + try: + VM.placement_policy.host = HOST + VM.update() + setMsg("Set startup host to " + vmhost) + setChanged() + except Exception as e: + setMsg("Failed to set startup host.") + setMsg(str(e)) + setFailed() + return False + return True + + def migrate_VM(self, vmname, vmhost): + VM = self.get_VM(vmname) + + HOST = self.get_Host_byid(VM.host.id) + if str(HOST.name) != vmhost: + try: + vm.migrate( + action=params.Action( + host=params.Host( + name=vmhost, + ) + ), + ) + setChanged() + setMsg("VM migrated to " + vmhost) + except Exception as e: + setMsg("Failed to set startup host.") + setMsg(str(e)) + setFailed() + return False + return True + + def remove_CD(self, vmname): + VM = self.get_VM(vmname) + try: + VM.cdroms.get(id="00000000-0000-0000-0000-000000000000").delete() + setMsg("Removed the image.") + setChanged() + except Exception as e: + setMsg("Failed to remove the image.") + setMsg(str(e)) + setFailed() + return False + return True + + +class RHEV(object): + def __init__(self, module): + self.module = module + + def __get_conn(self): + self.conn = RHEVConn(self.module) + return self.conn + + def test(self): + self.__get_conn() + return "OK" + + def getVM(self, name): + self.__get_conn() + VM = self.conn.get_VM(name) + if VM: + vminfo = dict() + vminfo['uuid'] = VM.id + vminfo['name'] = VM.name + vminfo['status'] = VM.status.state + vminfo['cpu_cores'] = VM.cpu.topology.cores + vminfo['cpu_sockets'] = VM.cpu.topology.sockets + vminfo['cpu_shares'] = VM.cpu_shares + vminfo['memory'] = (int(VM.memory) / 1024 / 1024 / 1024) + vminfo['mem_pol'] = (int(VM.memory_policy.guaranteed) / 1024 / 1024 / 1024) + vminfo['os'] = VM.get_os().type_ + vminfo['del_prot'] = VM.delete_protected + try: + vminfo['host'] = str(self.conn.get_Host_byid(str(VM.host.id)).name) + except Exception as e: + vminfo['host'] = None + vminfo['boot_order'] = [] + for boot_dev in VM.os.get_boot(): + vminfo['boot_order'].append(str(boot_dev.dev)) + vminfo['disks'] = [] + for DISK in VM.disks.list(): + disk = dict() + disk['name'] = DISK.name + disk['size'] = (int(DISK.size) / 1024 / 1024 / 1024) + disk['domain'] = str((self.conn.get_domain_byid(DISK.get_storage_domains().get_storage_domain()[0].id)).name) + disk['interface'] = DISK.interface + vminfo['disks'].append(disk) + vminfo['ifaces'] = [] + for NIC in VM.nics.list(): + iface = dict() + iface['name'] = str(NIC.name) + iface['vlan'] = str(self.conn.get_network_byid(NIC.get_network().id).name) + iface['interface'] = NIC.interface + iface['mac'] = NIC.mac.address + vminfo['ifaces'].append(iface) + vminfo[str(NIC.name)] = NIC.mac.address + CLUSTER = self.conn.get_cluster_byid(VM.cluster.id) + if CLUSTER: + vminfo['cluster'] = CLUSTER.name + else: + vminfo = False + return vminfo + + def createVMimage(self, name, cluster, template, disks): + self.__get_conn() + return self.conn.createVMimage(name, cluster, template, disks) + + def createVM(self, name, cluster, os, actiontype): + self.__get_conn() + return self.conn.createVM(name, cluster, os, actiontype) + + def setMemory(self, name, memory): + self.__get_conn() + return self.conn.set_Memory(name, memory) + + def setMemoryPolicy(self, name, memory_policy): + self.__get_conn() + return self.conn.set_Memory_Policy(name, memory_policy) + + def setCPU(self, name, cpu): + self.__get_conn() + return self.conn.set_CPU(name, cpu) + + def setCPUShare(self, name, cpu_share): + self.__get_conn() + return self.conn.set_CPU_share(name, cpu_share) + + def setDisks(self, name, disks): + self.__get_conn() + counter = 0 + bootselect = False + for disk in disks: + if 'bootable' in disk: + if disk['bootable'] is True: + bootselect = True + + for disk in disks: + diskname = name + "_Disk" + str(counter) + "_" + disk.get('name', '').replace('/', '_') + disksize = disk.get('size', 1) + diskdomain = disk.get('domain', None) + if diskdomain is None: + setMsg("`domain` is a required disk key.") + setFailed() + return False + diskinterface = disk.get('interface', 'virtio') + diskformat = disk.get('format', 'raw') + diskallocationtype = disk.get('thin', False) + diskboot = disk.get('bootable', False) + + if bootselect is False and counter == 0: + diskboot = True + + DISK = self.conn.get_disk(diskname) + + if DISK is None: + self.conn.createDisk(name, diskname, disksize, diskdomain, diskinterface, diskformat, diskallocationtype, diskboot) + else: + self.conn.set_Disk(diskname, disksize, diskinterface, diskboot) + checkFail() + counter += 1 + + return True + + def setNetworks(self, vmname, ifaces): + self.__get_conn() + VM = self.conn.get_VM(vmname) + + counter = 0 + length = len(ifaces) + + for NIC in VM.nics.list(): + if counter < length: + iface = ifaces[counter] + name = iface.get('name', None) + if name is None: + setMsg("`name` is a required iface key.") + setFailed() + elif str(name) != str(NIC.name): + setMsg("ifaces are in the wrong order, rebuilding everything.") + for NIC in VM.nics.list(): + self.conn.del_NIC(vmname, NIC.name) + self.setNetworks(vmname, ifaces) + checkFail() + return True + vlan = iface.get('vlan', None) + if vlan is None: + setMsg("`vlan` is a required iface key.") + setFailed() + checkFail() + interface = iface.get('interface', 'virtio') + self.conn.set_NIC(vmname, str(NIC.name), name, vlan, interface) + else: + self.conn.del_NIC(vmname, NIC.name) + counter += 1 + checkFail() + + while counter < length: + iface = ifaces[counter] + name = iface.get('name', None) + if name is None: + setMsg("`name` is a required iface key.") + setFailed() + vlan = iface.get('vlan', None) + if vlan is None: + setMsg("`vlan` is a required iface key.") + setFailed() + if failed is True: + return False + interface = iface.get('interface', 'virtio') + self.conn.createNIC(vmname, name, vlan, interface) + + counter += 1 + checkFail() + return True + + def setDeleteProtection(self, vmname, del_prot): + self.__get_conn() + VM = self.conn.get_VM(vmname) + if bool(VM.delete_protected) != bool(del_prot): + self.conn.set_DeleteProtection(vmname, del_prot) + checkFail() + setMsg("`delete protection` has been updated.") + else: + setMsg("`delete protection` already has the right value.") + return True + + def setBootOrder(self, vmname, boot_order): + self.__get_conn() + VM = self.conn.get_VM(vmname) + bootorder = [] + for boot_dev in VM.os.get_boot(): + bootorder.append(str(boot_dev.dev)) + + if boot_order != bootorder: + self.conn.set_BootOrder(vmname, boot_order) + setMsg('The boot order has been set') + else: + setMsg('The boot order has already been set') + return True + + def removeVM(self, vmname): + self.__get_conn() + self.setPower(vmname, "down", 300) + return self.conn.remove_VM(vmname) + + def setPower(self, vmname, state, timeout): + self.__get_conn() + VM = self.conn.get_VM(vmname) + if VM is None: + setMsg("VM does not exist.") + setFailed() + return False + + if state == VM.status.state: + setMsg("VM state was already " + state) + else: + if state == "up": + setMsg("VM is going to start") + self.conn.start_VM(vmname, timeout) + setChanged() + elif state == "down": + setMsg("VM is going to stop") + self.conn.stop_VM(vmname, timeout) + setChanged() + elif state == "restarted": + self.setPower(vmname, "down", timeout) + checkFail() + self.setPower(vmname, "up", timeout) + checkFail() + setMsg("the vm state is set to " + state) + return True + + def setCD(self, vmname, cd_drive): + self.__get_conn() + if cd_drive: + return self.conn.set_CD(vmname, cd_drive) + else: + return self.conn.remove_CD(vmname) + + def setVMHost(self, vmname, vmhost): + self.__get_conn() + return self.conn.set_VM_Host(vmname, vmhost) + + VM = self.conn.get_VM(vmname) + HOST = self.conn.get_Host(vmhost) + + if VM.placement_policy.host is None: + self.conn.set_VM_Host(vmname, vmhost) + elif str(VM.placement_policy.host.id) != str(HOST.id): + self.conn.set_VM_Host(vmname, vmhost) + else: + setMsg("VM's startup host was already set to " + vmhost) + checkFail() + + if str(VM.status.state) == "up": + self.conn.migrate_VM(vmname, vmhost) + checkFail() + + return True + + def setHost(self, hostname, cluster, ifaces): + self.__get_conn() + return self.conn.set_Host(hostname, cluster, ifaces) + + +def checkFail(): + if failed: + module.fail_json(msg=msg) + else: + return True + + +def setFailed(): + global failed + failed = True + + +def setChanged(): + global changed + changed = True + + +def setMsg(message): + global failed + msg.append(message) + + +def core(module): + + r = RHEV(module) + + state = module.params.get('state', 'present') + + if state == 'ping': + r.test() + return RHEV_SUCCESS, {"ping": "pong"} + elif state == 'info': + name = module.params.get('name') + if not name: + setMsg("`name` is a required argument.") + return RHEV_FAILED, msg + vminfo = r.getVM(name) + return RHEV_SUCCESS, {'changed': changed, 'msg': msg, 'vm': vminfo} + elif state == 'present': + created = False + name = module.params.get('name') + if not name: + setMsg("`name` is a required argument.") + return RHEV_FAILED, msg + actiontype = module.params.get('type') + if actiontype == 'server' or actiontype == 'desktop': + vminfo = r.getVM(name) + if vminfo: + setMsg('VM exists') + else: + # Create VM + cluster = module.params.get('cluster') + if cluster is None: + setMsg("cluster is a required argument.") + setFailed() + template = module.params.get('image') + if template: + disks = module.params.get('disks') + if disks is None: + setMsg("disks is a required argument.") + setFailed() + checkFail() + if r.createVMimage(name, cluster, template, disks) is False: + return RHEV_FAILED, vminfo + else: + os = module.params.get('osver') + if os is None: + setMsg("osver is a required argument.") + setFailed() + checkFail() + if r.createVM(name, cluster, os, actiontype) is False: + return RHEV_FAILED, vminfo + created = True + + # Set MEMORY and MEMORY POLICY + vminfo = r.getVM(name) + memory = module.params.get('vmmem') + if memory is not None: + memory_policy = module.params.get('mempol') + if int(memory_policy) == 0: + memory_policy = memory + mem_pol_nok = True + if int(vminfo['mem_pol']) == int(memory_policy): + setMsg("Memory is correct") + mem_pol_nok = False + + mem_nok = True + if int(vminfo['memory']) == int(memory): + setMsg("Memory is correct") + mem_nok = False + + if memory_policy > memory: + setMsg('memory_policy cannot have a higher value than memory.') + return RHEV_FAILED, msg + + if mem_nok and mem_pol_nok: + if int(memory_policy) > int(vminfo['memory']): + r.setMemory(vminfo['name'], memory) + r.setMemoryPolicy(vminfo['name'], memory_policy) + else: + r.setMemoryPolicy(vminfo['name'], memory_policy) + r.setMemory(vminfo['name'], memory) + elif mem_nok: + r.setMemory(vminfo['name'], memory) + elif mem_pol_nok: + r.setMemoryPolicy(vminfo['name'], memory_policy) + checkFail() + + # Set CPU + cpu = module.params.get('vmcpu') + if int(vminfo['cpu_cores']) == int(cpu): + setMsg("Number of CPUs is correct") + else: + if r.setCPU(vminfo['name'], cpu) is False: + return RHEV_FAILED, msg + + # Set CPU SHARE + cpu_share = module.params.get('cpu_share') + if cpu_share is not None: + if int(vminfo['cpu_shares']) == int(cpu_share): + setMsg("CPU share is correct.") + else: + if r.setCPUShare(vminfo['name'], cpu_share) is False: + return RHEV_FAILED, msg + + # Set DISKS + disks = module.params.get('disks') + if disks is not None: + if r.setDisks(vminfo['name'], disks) is False: + return RHEV_FAILED, msg + + # Set NETWORKS + ifaces = module.params.get('ifaces', None) + if ifaces is not None: + if r.setNetworks(vminfo['name'], ifaces) is False: + return RHEV_FAILED, msg + + # Set Delete Protection + del_prot = module.params.get('del_prot') + if r.setDeleteProtection(vminfo['name'], del_prot) is False: + return RHEV_FAILED, msg + + # Set Boot Order + boot_order = module.params.get('boot_order') + if r.setBootOrder(vminfo['name'], boot_order) is False: + return RHEV_FAILED, msg + + # Set VM Host + vmhost = module.params.get('vmhost') + if vmhost is not False and vmhost is not "False": + if r.setVMHost(vminfo['name'], vmhost) is False: + return RHEV_FAILED, msg + + vminfo = r.getVM(name) + vminfo['created'] = created + return RHEV_SUCCESS, {'changed': changed, 'msg': msg, 'vm': vminfo} + + if actiontype == 'host': + cluster = module.params.get('cluster') + if cluster is None: + setMsg("cluster is a required argument.") + setFailed() + ifaces = module.params.get('ifaces') + if ifaces is None: + setMsg("ifaces is a required argument.") + setFailed() + if r.setHost(name, cluster, ifaces) is False: + return RHEV_FAILED, msg + return RHEV_SUCCESS, {'changed': changed, 'msg': msg} + + elif state == 'absent': + name = module.params.get('name') + if not name: + setMsg("`name` is a required argument.") + return RHEV_FAILED, msg + actiontype = module.params.get('type') + if actiontype == 'server' or actiontype == 'desktop': + vminfo = r.getVM(name) + if vminfo: + setMsg('VM exists') + + # Set Delete Protection + del_prot = module.params.get('del_prot') + if r.setDeleteProtection(vminfo['name'], del_prot) is False: + return RHEV_FAILED, msg + + # Remove VM + if r.removeVM(vminfo['name']) is False: + return RHEV_FAILED, msg + setMsg('VM has been removed.') + vminfo['state'] = 'DELETED' + else: + setMsg('VM was already removed.') + return RHEV_SUCCESS, {'changed': changed, 'msg': msg, 'vm': vminfo} + + elif state == 'up' or state == 'down' or state == 'restarted': + name = module.params.get('name') + if not name: + setMsg("`name` is a required argument.") + return RHEV_FAILED, msg + timeout = module.params.get('timeout') + if r.setPower(name, state, timeout) is False: + return RHEV_FAILED, msg + vminfo = r.getVM(name) + return RHEV_SUCCESS, {'changed': changed, 'msg': msg, 'vm': vminfo} + + elif state == 'cd': + name = module.params.get('name') + cd_drive = module.params.get('cd_drive') + if r.setCD(name, cd_drive) is False: + return RHEV_FAILED, msg + return RHEV_SUCCESS, {'changed': changed, 'msg': msg} + + +def main(): + global module + module = AnsibleModule( + argument_spec = dict( + state = dict(default='present', choices=['ping', 'present', 'absent', 'up', 'down', 'restarted', 'cd', 'info']), + user = dict(default="admin@internal"), + password = dict(required=True), + server = dict(default="127.0.0.1"), + port = dict(default="443"), + insecure_api = dict(default=False, type='bool'), + name = dict(), + image = dict(default=False), + datacenter = dict(default="Default"), + type = dict(default="server", choices=['server', 'desktop', 'host']), + cluster = dict(default=''), + vmhost = dict(default=False), + vmcpu = dict(default="2"), + vmmem = dict(default="1"), + disks = dict(), + osver = dict(default="rhel_6x64"), + ifaces = dict(aliases=['nics', 'interfaces']), + timeout = dict(default=False), + mempol = dict(default="1"), + vm_ha = dict(default=True), + cpu_share = dict(default="0"), + boot_order = dict(default=["network", "hd"]), + del_prot = dict(default=True, type="bool"), + cd_drive = dict(default=False) + ), + ) + + if not HAS_SDK: + module.fail_json( + msg='The `ovirtsdk` module is not importable. Check the requirements.' + ) + + rc = RHEV_SUCCESS + try: + rc, result = core(module) + except Exception as e: + module.fail_json(msg=str(e)) + + if rc != 0: # something went wrong emit the msg + module.fail_json(rc=rc, msg=result) + else: + module.exit_json(**result) + + +# import module snippets +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From ae2fdd5b575108bd6691253a1454978120d4fbf2 Mon Sep 17 00:00:00 2001 From: Olivier Boukili Date: Tue, 30 Aug 2016 20:44:39 +0200 Subject: [PATCH 2029/2522] new module: apache2_mod_proxy (#2148) * module apache2_mod_proxy * Moved state notes to state description. --- web_infrastructure/apache2_mod_proxy.py | 429 ++++++++++++++++++++++++ 1 file changed, 429 insertions(+) create mode 100644 web_infrastructure/apache2_mod_proxy.py diff --git a/web_infrastructure/apache2_mod_proxy.py b/web_infrastructure/apache2_mod_proxy.py new file mode 100644 index 00000000000..0117c118bbd --- /dev/null +++ b/web_infrastructure/apache2_mod_proxy.py @@ -0,0 +1,429 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Olivier Boukili +# +# This file is part of Ansible. +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: apache2_mod_proxy +version_added: "2.2" +short_description: Set and/or get members' attributes of an Apache httpd 2.4 mod_proxy balancer pool +description: + - Set and/or get members' attributes of an Apache httpd 2.4 mod_proxy balancer + pool, using HTTP POST and GET requests. The httpd mod_proxy balancer-member + status page has to be enabled and accessible, as this module relies on parsing + this page. This module supports ansible check_mode, and requires BeautifulSoup + python module. +options: + balancer_url_suffix: + default: /balancer-manager/ + description: + - Suffix of the balancer pool url required to access the balancer pool + status page (e.g. balancer_vhost[:port]/balancer_url_suffix). + required: false + balancer_vhost: + default: None + description: + - (ipv4|ipv6|fqdn):port of the Apache httpd 2.4 mod_proxy balancer pool. + required: true + member_host: + default: None + description: + - (ipv4|ipv6|fqdn) of the balancer member to get or to set attributes to. + Port number is autodetected and should not be specified here. + If undefined, apache2_mod_proxy module will return a members list of + dictionaries of all the current balancer pool members' attributes. + required: false + state: + default: None + description: + - Desired state of the member host. + (absent|disabled),drained,hot_standby,ignore_errors can be + simultaneously invoked by separating them with a comma (e.g. state=drained,ignore_errors). + required: false + choices: ["present", "absent", "enabled", "disabled", "drained", "hot_standby", "ignore_errors"] + tls: + default: false + description: + - Use https to access balancer management page. + choices: ["true", "false"] + validate_certs: + default: true + description: + - Validate ssl/tls certificates. + choices: ["true", "false"] +''' + +EXAMPLES = ''' +# Get all current balancer pool members' attributes: +- apache2_mod_proxy: balancer_vhost=10.0.0.2 + +# Get a specific member's attributes: +- apache2_mod_proxy: balancer_vhost=myws.mydomain.org balancer_suffix="/lb/" member_host=node1.myws.mydomain.org + +# Enable all balancer pool members: +- apache2_mod_proxy: balancer_vhost="{{ myloadbalancer_host }}" + register: result +- apache2_mod_proxy: balancer_vhost="{{ myloadbalancer_host }}" member_host="{{ item.host }}" state=present + with_items: "{{ result.members }}" + +# Gracefully disable a member from a loadbalancer node: +- apache2_mod_proxy: balancer_vhost="{{ vhost_host }}" member_host="{{ member.host }}" state=drained delegate_to=myloadbalancernode +- wait_for: host="{{ member.host }}" port={{ member.port }} state=drained delegate_to=myloadbalancernode +- apache2_mod_proxy: balancer_vhost="{{ vhost_host }}" member_host="{{ member.host }}" state=absent delegate_to=myloadbalancernode +''' + +RETURN = ''' +member: + description: specific balancer member information dictionary, returned when apache2_mod_proxy module is invoked with member_host parameter. + type: dict + returned: success + sample: + {"attributes": + {"Busy": "0", + "Elected": "42", + "Factor": "1", + "From": "136K", + "Load": "0", + "Route": null, + "RouteRedir": null, + "Set": "0", + "Status": "Init Ok ", + "To": " 47K", + "Worker URL": null + }, + "balancer_url": "http://10.10.0.2/balancer-manager/", + "host": "10.10.0.20", + "management_url": "http://10.10.0.2/lb/?b=mywsbalancer&w=http://10.10.0.20:8080/ws&nonce=8925436c-79c6-4841-8936-e7d13b79239b", + "path": "/ws", + "port": 8080, + "protocol": "http", + "status": { + "disabled": false, + "drained": false, + "hot_standby": false, + "ignore_errors": false + } + } +members: + description: list of member (defined above) dictionaries, returned when apache2_mod_proxy is invoked with no member_host and state args. + returned: success + type: list + sample: + [{"attributes": { + "Busy": "0", + "Elected": "42", + "Factor": "1", + "From": "136K", + "Load": "0", + "Route": null, + "RouteRedir": null, + "Set": "0", + "Status": "Init Ok ", + "To": " 47K", + "Worker URL": null + }, + "balancer_url": "http://10.10.0.2/balancer-manager/", + "host": "10.10.0.20", + "management_url": "http://10.10.0.2/lb/?b=mywsbalancer&w=http://10.10.0.20:8080/ws&nonce=8925436c-79c6-4841-8936-e7d13b79239b", + "path": "/ws", + "port": 8080, + "protocol": "http", + "status": { + "disabled": false, + "drained": false, + "hot_standby": false, + "ignore_errors": false + } + }, + {"attributes": { + "Busy": "0", + "Elected": "42", + "Factor": "1", + "From": "136K", + "Load": "0", + "Route": null, + "RouteRedir": null, + "Set": "0", + "Status": "Init Ok ", + "To": " 47K", + "Worker URL": null + }, + "balancer_url": "http://10.10.0.2/balancer-manager/", + "host": "10.10.0.21", + "management_url": "http://10.10.0.2/lb/?b=mywsbalancer&w=http://10.10.0.21:8080/ws&nonce=8925436c-79c6-4841-8936-e7d13b79239b", + "path": "/ws", + "port": 8080, + "protocol": "http", + "status": { + "disabled": false, + "drained": false, + "hot_standby": false, + "ignore_errors": false} + } + ] +''' + +import re + +try: + from BeautifulSoup import BeautifulSoup +except ImportError: + HAS_BEAUTIFULSOUP = False +else: + HAS_BEAUTIFULSOUP = True + +# balancer member attributes extraction regexp: +EXPRESSION = r"(b=([\w\.\-]+)&w=(https?|ajp|wss?|ftp|[sf]cgi)://([\w\.\-]+):?(\d*)([/\w\.\-]*)&?[\w\-\=]*)" +# Apache2 server version extraction regexp: +APACHE_VERSION_EXPRESSION = r"Server Version: Apache/([\d.]+) \(([\w]+)\)" + +def regexp_extraction(string, _regexp, groups=1): + """ Returns the capture group (default=1) specified in the regexp, applied to the string """ + regexp_search = re.search(string=str(string), pattern=str(_regexp)) + if regexp_search: + if regexp_search.group(groups) != '': + return str(regexp_search.group(groups)) + return None + +class BalancerMember(object): + """ Apache 2.4 mod_proxy LB balancer member. + attributes: + read-only: + host -> member host (string), + management_url -> member management url (string), + protocol -> member protocol (string) + port -> member port (string), + path -> member location (string), + balancer_url -> url of this member's parent balancer (string), + attributes -> whole member attributes (dictionary) + module -> ansible module instance (AnsibleModule object). + writable: + status -> status of the member (dictionary) + """ + + def __init__(self, management_url, balancer_url, module): + self.host = regexp_extraction(management_url, str(EXPRESSION), 4) + self.management_url = str(management_url) + self.protocol = regexp_extraction(management_url, EXPRESSION, 3) + self.port = regexp_extraction(management_url, EXPRESSION, 5) + self.path = regexp_extraction(management_url, EXPRESSION, 6) + self.balancer_url = str(balancer_url) + self.module = module + + def get_member_attributes(self): + """ Returns a dictionary of a balancer member's attributes.""" + + balancer_member_page = fetch_url(self.module, self.management_url) + + try: + assert balancer_member_page[1]['status'] == 200 + except AssertionError: + self.module.fail_json(msg="Could not get balancer_member_page, check for connectivity! " + balancer_member_page[1]) + else: + try: + soup = BeautifulSoup(balancer_member_page[0]) + except TypeError: + self.module.fail_json(msg="Cannot parse balancer_member_page HTML! " + str(soup)) + else: + subsoup = soup.findAll('table')[1].findAll('tr') + keys = subsoup[0].findAll('th') + for valuesset in subsoup[1::1]: + if re.search(pattern=self.host, string=str(valuesset)): + values = valuesset.findAll('td') + return dict((keys[x].string, values[x].string) for x in range(0, len(keys))) + + def get_member_status(self): + """ Returns a dictionary of a balancer member's status attributes.""" + status_mapping = {'disabled':'Dis', + 'drained':'Drn', + 'hot_standby':'Stby', + 'ignore_errors':'Ign'} + status = {} + actual_status = str(self.attributes['Status']) + for mode in status_mapping.keys(): + if re.search(pattern=status_mapping[mode], string=actual_status): + status[mode] = True + else: + status[mode] = False + return status + + def set_member_status(self, values): + """ Sets a balancer member's status attributes amongst pre-mapped values.""" + values_mapping = {'disabled':'&w_status_D', + 'drained':'&w_status_N', + 'hot_standby':'&w_status_H', + 'ignore_errors':'&w_status_I'} + + request_body = regexp_extraction(self.management_url, EXPRESSION, 1) + for k in values_mapping.keys(): + if values[str(k)]: + request_body = request_body + str(values_mapping[k]) + '=1' + else: + request_body = request_body + str(values_mapping[k]) + '=0' + + response = fetch_url(self.module, self.management_url, data=str(request_body)) + try: + assert response[1]['status'] == 200 + except AssertionError: + self.module.fail_json(msg="Could not set the member status! " + self.host + " " + response[1]['status']) + + attributes = property(get_member_attributes) + status = property(get_member_status, set_member_status) + + +class Balancer(object): + """ Apache httpd 2.4 mod_proxy balancer object""" + def __init__(self, host, suffix, module, members=None, tls=False): + if tls: + self.base_url = str(str('https://') + str(host)) + self.url = str(str('https://') + str(host) + str(suffix)) + else: + self.base_url = str(str('http://') + str(host)) + self.url = str(str('http://') + str(host) + str(suffix)) + self.module = module + self.page = self.fetch_balancer_page() + if members is None: + self._members = [] + + def fetch_balancer_page(self): + """ Returns the balancer management html page as a string for later parsing.""" + page = fetch_url(self.module, str(self.url)) + try: + assert page[1]['status'] == 200 + except AssertionError: + self.module.fail_json(msg="Could not get balancer page! HTTP status response: " + str(page[1]['status'])) + else: + content = page[0].read() + apache_version = regexp_extraction(content, APACHE_VERSION_EXPRESSION, 1) + if not re.search(pattern=r"2\.4\.[\d]*", string=apache_version): + self.module.fail_json(msg="This module only acts on an Apache2 2.4+ instance, current Apache2 version: " + str(apache_version)) + return content + + def get_balancer_members(self): + """ Returns members of the balancer as a generator object for later iteration.""" + try: + soup = BeautifulSoup(self.page) + except TypeError: + self.module.fail_json(msg="Cannot parse balancer page HTML! " + str(self.page)) + else: + for element in soup.findAll('a')[1::1]: + balancer_member_suffix = str(element.get('href')) + try: + assert balancer_member_suffix is not '' + except AssertionError: + self.module.fail_json(msg="Argument 'balancer_member_suffix' is empty!") + else: + yield BalancerMember(str(self.base_url + balancer_member_suffix), str(self.url), self.module) + + members = property(get_balancer_members) + +def main(): + """ Initiates module.""" + module = AnsibleModule( + argument_spec=dict( + balancer_vhost=dict(required=True, default=None, type='str'), + balancer_url_suffix=dict(default="/balancer-manager/", type='str'), + member_host=dict(type='str'), + state=dict(type='str'), + tls=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool') + ), + supports_check_mode=True + ) + + if HAS_BEAUTIFULSOUP is False: + module.fail_json(msg="python module 'BeautifulSoup' is required!") + + if module.params['state'] != None: + states = module.params['state'].split(',') + if (len(states) > 1) and (("present" in states) or ("enabled" in states)): + module.fail_json(msg="state present/enabled is mutually exclusive with other states!") + else: + for _state in states: + if _state not in ['present', 'absent', 'enabled', 'disabled', 'drained', 'hot_standby', 'ignore_errors']: + module.fail_json(msg="State can only take values amongst 'present', 'absent', 'enabled', 'disabled', 'drained', 'hot_standby', 'ignore_errors'.") + else: + states = ['None'] + + mybalancer = Balancer(module.params['balancer_vhost'], + module.params['balancer_url_suffix'], + module=module, + tls=module.params['tls']) + + if module.params['member_host'] is None: + json_output_list = [] + for member in mybalancer.members: + json_output_list.append({ + "host": member.host, + "status": member.status, + "protocol": member.protocol, + "port": member.port, + "path": member.path, + "attributes": member.attributes, + "management_url": member.management_url, + "balancer_url": member.balancer_url + }) + module.exit_json( + changed=False, + members=json_output_list + ) + else: + changed = False + member_exists = False + member_status = {'disabled': False, 'drained': False, 'hot_standby': False, 'ignore_errors':False} + for mode in member_status.keys(): + for state in states: + if mode == state: + member_status[mode] = True + elif mode == 'disabled' and state == 'absent': + member_status[mode] = True + + for member in mybalancer.members: + if str(member.host) == str(module.params['member_host']): + member_exists = True + if module.params['state'] is not None: + member_status_before = member.status + if not module.check_mode: + member_status_after = member.status = member_status + else: + member_status_after = member_status + if member_status_before != member_status_after: + changed = True + json_output = { + "host": member.host, + "status": member.status, + "protocol": member.protocol, + "port": member.port, + "path": member.path, + "attributes": member.attributes, + "management_url": member.management_url, + "balancer_url": member.balancer_url + } + if member_exists: + module.exit_json( + changed=changed, + member=json_output + ) + else: + module.fail_json(msg=str(module.params['member_host']) + ' is not a member of the balancer ' + str(module.params['balancer_vhost']) + '!') + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.urls import fetch_url +if __name__ == '__main__': + main() From 2fda8831bdf2fc47c48cd5da2a1611b2b39bdc77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20=C5=A0tevko?= Date: Tue, 30 Aug 2016 20:46:53 +0200 Subject: [PATCH 2030/2522] Add modules to configure Solaris/illumos networking (1st batch) (#2416) * Add modules to configure Solaris/illumos networking (1st batch) * Add choices to temporary flags --- network/illumos/__init__.py | 0 network/illumos/dladm_etherstub.py | 171 ++++++++++ network/illumos/dladm_vnic.py | 258 +++++++++++++++ network/illumos/flowadm.py | 503 +++++++++++++++++++++++++++++ network/illumos/ipadm_if.py | 222 +++++++++++++ network/illumos/ipadm_prop.py | 264 +++++++++++++++ 6 files changed, 1418 insertions(+) create mode 100644 network/illumos/__init__.py create mode 100644 network/illumos/dladm_etherstub.py create mode 100644 network/illumos/dladm_vnic.py create mode 100644 network/illumos/flowadm.py create mode 100644 network/illumos/ipadm_if.py create mode 100644 network/illumos/ipadm_prop.py diff --git a/network/illumos/__init__.py b/network/illumos/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/network/illumos/dladm_etherstub.py b/network/illumos/dladm_etherstub.py new file mode 100644 index 00000000000..72b2e6759ff --- /dev/null +++ b/network/illumos/dladm_etherstub.py @@ -0,0 +1,171 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Adam Števko +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +DOCUMENTATION = ''' +--- +module: dladm_etherstub +short_description: Manage etherstubs on Solaris/illumos systems. +description: + - Create or delete etherstubs on Solaris/illumos systems. +version_added: "2.2" +author: Adam Števko (@xen0l) +options: + name: + description: + - Etherstub name. + required: true + temporary: + description: + - Specifies that the etherstub is temporary. Temporary etherstubs + do not persist across reboots. + required: false + default: false + choices: [ "true", "false" ] + state: + description: + - Create or delete Solaris/illumos etherstub. + required: false + default: "present" + choices: [ "present", "absent" ] +''' + +EXAMPLES = ''' +# Create 'stub0' etherstub +dladm_etherstub: name=stub0 state=present + +# Remove 'stub0 etherstub +dladm_etherstub: name=stub0 state=absent +''' + +RETURN = ''' +name: + description: etherstub name + returned: always + type: string + sample: "switch0" +state: + description: state of the target + returned: always + type: string + sample: "present" +temporary: + description: etherstub's persistence + returned: always + type: boolean + sample: "True" +''' + + +class Etherstub(object): + + def __init__(self, module): + self.module = module + + self.name = module.params['name'] + self.temporary = module.params['temporary'] + self.state = module.params['state'] + + def etherstub_exists(self): + cmd = [self.module.get_bin_path('dladm', True)] + + cmd.append('show-etherstub') + cmd.append(self.name) + + (rc, _, _) = self.module.run_command(cmd) + + if rc == 0: + return True + else: + return False + + def create_etherstub(self): + cmd = [self.module.get_bin_path('dladm', True)] + + cmd.append('create-etherstub') + + if self.temporary: + cmd.append('-t') + cmd.append(self.name) + + return self.module.run_command(cmd) + + def delete_etherstub(self): + cmd = [self.module.get_bin_path('dladm', True)] + + cmd.append('delete-etherstub') + + if self.temporary: + cmd.append('-t') + cmd.append(self.name) + + return self.module.run_command(cmd) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + name=dict(required=True), + temporary=dict(default=False, type='bool'), + state=dict(default='present', choices=['absent', 'present']), + ), + supports_check_mode=True + ) + + etherstub = Etherstub(module) + + rc = None + out = '' + err = '' + result = {} + result['name'] = etherstub.name + result['state'] = etherstub.state + result['temporary'] = etherstub.temporary + + if etherstub.state == 'absent': + if etherstub.etherstub_exists(): + if module.check_mode: + module.exit_json(changed=True) + (rc, out, err) = etherstub.delete_etherstub() + if rc != 0: + module.fail_json(name=etherstub.name, msg=err, rc=rc) + elif etherstub.state == 'present': + if not etherstub.etherstub_exists(): + if module.check_mode: + module.exit_json(changed=True) + (rc, out, err) = etherstub.create_etherstub() + + if rc is not None and rc != 0: + module.fail_json(name=etherstub.name, msg=err, rc=rc) + + if rc is None: + result['changed'] = False + else: + result['changed'] = True + + if out: + result['stdout'] = out + if err: + result['stderr'] = err + + module.exit_json(**result) + +from ansible.module_utils.basic import * +main() diff --git a/network/illumos/dladm_vnic.py b/network/illumos/dladm_vnic.py new file mode 100644 index 00000000000..b57edc00a9c --- /dev/null +++ b/network/illumos/dladm_vnic.py @@ -0,0 +1,258 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Adam Števko +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +DOCUMENTATION = ''' +--- +module: dladm_vnic +short_description: Manage VNICs on Solaris/illumos systems. +description: + - Create or delete VNICs on Solaris/illumos systems. +version_added: "2.2" +author: Adam Števko (@xen0l) +options: + name: + description: + - VNIC name. + required: true + link: + description: + - VNIC underlying link name. + required: true + temporary: + description: + - Specifies that the VNIC is temporary. Temporary VNICs + do not persist across reboots. + required: false + default: false + choices: [ "true", "false" ] + mac: + description: + - Sets the VNIC's MAC address. Must be valid unicast MAC address. + required: false + default: false + aliases: [ "macaddr" ] + vlan: + description: + - Enable VLAN tagging for this VNIC. The VLAN tag will have id + I(vlan). + required: false + default: false + aliases: [ "vlan_id" ] + state: + description: + - Create or delete Solaris/illumos VNIC. + required: false + default: "present" + choices: [ "present", "absent" ] +''' + +EXAMPLES = ''' +# Create 'vnic0' VNIC over 'bnx0' link +dladm_vnic: name=vnic0 link=bnx0 state=present + +# Create VNIC with specified MAC and VLAN tag over 'aggr0' +dladm_vnic: name=vnic1 link=aggr0 mac=2:33:af:12:ab:cd vlan=4 + +# Remove 'vnic0' VNIC +dladm_vnic: name=vnic0 link=bnx0 state=absent +''' + +RETURN = ''' +name: + description: VNIC name + returned: always + type: string + sample: "vnic0" +link: + description: VNIC underlying link name + returned: always + type: string + sample: "igb0" +state: + description: state of the target + returned: always + type: string + sample: "present" +temporary: + description: VNIC's persistence + returned: always + type: boolean + sample: "True" +mac: + description: MAC address to use for VNIC + returned: if mac is specified + type: string + sample: "00:aa:bc:fe:11:22" +vlan: + description: VLAN to use for VNIC + returned: success + type: int + sample: 42 +''' + +import re + + +class VNIC(object): + + UNICAST_MAC_REGEX = r'^[a-f0-9][2-9a-f0]:([a-f0-9]{2}:){4}[a-f0-9]{2}$' + + def __init__(self, module): + self.module = module + + self.name = module.params['name'] + self.link = module.params['link'] + self.mac = module.params['mac'] + self.vlan = module.params['vlan'] + self.temporary = module.params['temporary'] + self.state = module.params['state'] + + def vnic_exists(self): + cmd = [self.module.get_bin_path('dladm', True)] + + cmd.append('show-vnic') + cmd.append(self.name) + + (rc, _, _) = self.module.run_command(cmd) + + if rc == 0: + return True + else: + return False + + def create_vnic(self): + cmd = [self.module.get_bin_path('dladm', True)] + + cmd.append('create-vnic') + + if self.temporary: + cmd.append('-t') + + if self.mac: + cmd.append('-m') + cmd.append(self.mac) + + if self.vlan: + cmd.append('-v') + cmd.append(self.vlan) + + cmd.append('-l') + cmd.append(self.link) + cmd.append(self.name) + + return self.module.run_command(cmd) + + def delete_vnic(self): + cmd = [self.module.get_bin_path('dladm', True)] + + cmd.append('delete-vnic') + + if self.temporary: + cmd.append('-t') + cmd.append(self.name) + + return self.module.run_command(cmd) + + def is_valid_unicast_mac(self): + + mac_re = re.match(self.UNICAST_MAC_REGEX, self.mac) + + return mac_re is None + + def is_valid_vlan_id(self): + + return 0 <= self.vlan <= 4095 + + +def main(): + module = AnsibleModule( + argument_spec=dict( + name=dict(required=True), + link=dict(required=True), + mac=dict(default=None, aliases=['macaddr']), + vlan=dict(default=None, aliases=['vlan_id']), + temporary=dict(default=False, type='bool'), + state=dict(default='present', choices=['absent', 'present']), + ), + supports_check_mode=True + ) + + vnic = VNIC(module) + + rc = None + out = '' + err = '' + result = {} + result['name'] = vnic.name + result['link'] = vnic.link + result['state'] = vnic.state + result['temporary'] = vnic.temporary + + if vnic.mac is not None: + if vnic.is_valid_unicast_mac(): + module.fail_json(msg='Invalid unicast MAC address', + mac=vnic.mac, + name=vnic.name, + state=vnic.state, + link=vnic.link, + vlan=vnic.vlan) + result['mac'] = vnic.mac + + if vnic.vlan is not None: + if vnic.is_valid_vlan_id(): + module.fail_json(msg='Invalid VLAN tag', + mac=vnic.mac, + name=vnic.name, + state=vnic.state, + link=vnic.link, + vlan=vnic.vlan) + result['vlan'] = vnic.vlan + + if vnic.state == 'absent': + if vnic.vnic_exists(): + if module.check_mode: + module.exit_json(changed=True) + (rc, out, err) = vnic.delete_vnic() + if rc != 0: + module.fail_json(name=vnic.name, msg=err, rc=rc) + elif vnic.state == 'present': + if not vnic.vnic_exists(): + if module.check_mode: + module.exit_json(changed=True) + (rc, out, err) = vnic.create_vnic() + + if rc is not None and rc != 0: + module.fail_json(name=vnic.name, msg=err, rc=rc) + + if rc is None: + result['changed'] = False + else: + result['changed'] = True + + if out: + result['stdout'] = out + if err: + result['stderr'] = err + + module.exit_json(**result) + +from ansible.module_utils.basic import * +main() diff --git a/network/illumos/flowadm.py b/network/illumos/flowadm.py new file mode 100644 index 00000000000..73cc91af442 --- /dev/null +++ b/network/illumos/flowadm.py @@ -0,0 +1,503 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Adam Števko +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +DOCUMENTATION = ''' +--- +module: flowadm +short_description: Manage bandwidth resource control and priority for protocols, services and zones. +description: + - Create/modify/remove networking bandwidth and associated resources for a type of traffic on a particular link. +version_added: "2.2" +author: Adam Števko (@xen0l) +options: + name: + description: > + - A flow is defined as a set of attributes based on Layer 3 and Layer 4 + headers, which can be used to identify a protocol, service, or a zone. + required: true + aliases: [ 'flow' ] + link: + description: + - Specifiies a link to configure flow on. + required: false + local_ip: + description: + - Identifies a network flow by the local IP address. + required: false + remove_ip: + description: + - Identifies a network flow by the remote IP address. + required: false + transport: + description: > + - Specifies a Layer 4 protocol to be used. It is typically used in combination with I(local_port) to + identify the service that needs special attention. + required: false + local_port: + description: + - Identifies a service specified by the local port. + required: false + dsfield: + description: > + - Identifies the 8-bit differentiated services field (as defined in + RFC 2474). The optional dsfield_mask is used to state the bits of interest in + the differentiated services field when comparing with the dsfield + value. Both values must be in hexadecimal. + required: false + maxbw: + description: > + - Sets the full duplex bandwidth for the flow. The bandwidth is + specified as an integer with one of the scale suffixes(K, M, or G + for Kbps, Mbps, and Gbps). If no units are specified, the input + value will be read as Mbps. + required: false + priority: + description: + - Sets the relative priority for the flow. + required: false + default: 'medium' + choices: [ 'low', 'medium', 'high' ] + temporary: + description: + - Specifies that the configured flow is temporary. Temporary + flows do not persist across reboots. + required: false + default: false + choices: [ "true", "false" ] + state: + description: + - Create/delete/enable/disable an IP address on the network interface. + required: false + default: present + choices: [ 'absent', 'present', 'resetted' ] +''' + +EXAMPLES = ''' +# Limit SSH traffic to 100M via vnic0 interface +flowadm: link=vnic0 flow=ssh_out transport=tcp local_port=22 maxbw=100M state=present + +# Reset flow properties +flowadm: name=dns state=resetted + +# Configure policy for EF PHB (DSCP value of 101110 from RFC 2598) with a bandwidth of 500 Mbps and a high priority. +flowadm: link=bge0 dsfield=0x2e:0xfc maxbw=500M priority=high flow=efphb-flow state=present +''' + +RETURN = ''' +name: + description: flow name + returned: always + type: string + sample: "http_drop" +link: + description: flow's link + returned: if link is defined + type: string + sample: "vnic0" +state: + description: state of the target + returned: always + type: string + sample: "present" +temporary: + description: flow's persistence + returned: always + type: boolean + sample: "True" +priority: + description: flow's priority + returned: if priority is defined + type: string + sample: "low" +transport: + description: flow's transport + returned: if transport is defined + type: string + sample: "tcp" +maxbw: + description: flow's maximum bandwidth + returned: if maxbw is defined + type: string + sample: "100M" +local_Ip: + description: flow's local IP address + returned: if local_ip is defined + type: string + sample: "10.0.0.42" +local_port: + description: flow's local port + returned: if local_port is defined + type: int + sample: 1337 +remote_Ip: + description: flow's remote IP address + returned: if remote_ip is defined + type: string + sample: "10.0.0.42" +dsfield: + description: flow's differentiated services value + returned: if dsfield is defined + type: string + sample: "0x2e:0xfc" +''' + + +import socket + +SUPPORTED_TRANSPORTS = ['tcp', 'udp', 'sctp', 'icmp', 'icmpv6'] +SUPPORTED_PRIORITIES = ['low', 'medium', 'high'] + +SUPPORTED_ATTRIBUTES = ['local_ip', 'remote_ip', 'transport', 'local_port', 'dsfield'] +SUPPORTPED_PROPERTIES = ['maxbw', 'priority'] + + +class Flow(object): + + def __init__(self, module): + self.module = module + + self.name = module.params['name'] + self.link = module.params['link'] + self.local_ip = module.params['local_ip'] + self.remote_ip = module.params['remote_ip'] + self.transport = module.params['transport'] + self.local_port = module.params['local_port'] + self.dsfield = module.params['dsfield'] + self.maxbw = module.params['maxbw'] + self.priority = module.params['priority'] + self.temporary = module.params['temporary'] + self.state = module.params['state'] + + self._needs_updating = { + 'maxbw': False, + 'priority': False, + } + + @classmethod + def is_valid_port(cls, port): + return 1 <= int(port) <= 65535 + + @classmethod + def is_valid_address(cls, ip): + + if ip.count('/') == 1: + ip_address, netmask = ip.split('/') + else: + ip_address = ip + + if len(ip_address.split('.')) == 4: + try: + socket.inet_pton(socket.AF_INET, ip_address) + except socket.error: + return False + + if not 0 <= netmask <= 32: + return False + else: + try: + socket.inet_pton(socket.AF_INET6, ip_address) + except socket.error: + return False + + if not 0 <= netmask <= 128: + return False + + return True + + @classmethod + def is_hex(cls, number): + try: + int(number, 16) + except ValueError: + return False + + return True + + @classmethod + def is_valid_dsfield(cls, dsfield): + + dsmask = None + + if dsfield.count(':') == 1: + dsval = dsfield.split(':')[0] + else: + dsval, dsmask = dsfield.split(':') + + if dsmask and not 0x01 <= int(dsmask, 16) <= 0xff and not 0x01 <= int(dsval, 16) <= 0xff: + return False + elif not 0x01 <= int(dsval, 16) <= 0xff: + return False + + return True + + def flow_exists(self): + cmd = [self.module.get_bin_path('flowadm')] + + cmd.append('show-flow') + cmd.append(self.name) + + (rc, _, _) = self.module.run_command(cmd) + + if rc == 0: + return True + else: + return False + + def delete_flow(self): + cmd = [self.module.get_bin_path('flowadm')] + + cmd.append('remove-flow') + if self.temporary: + cmd.append('-t') + cmd.append(self.name) + + return self.module.run_command(cmd) + + def create_flow(self): + cmd = [self.module.get_bin_path('flowadm')] + + cmd.append('add-flow') + cmd.append('-l') + cmd.append(self.link) + + if self.local_ip: + cmd.append('-a') + cmd.append('local_ip=' + self.local_ip) + + if self.remote_ip: + cmd.append('-a') + cmd.append('remote_ip=' + self.remote_ip) + + if self.transport: + cmd.append('-a') + cmd.append('transport=' + self.transport) + + if self.local_port: + cmd.append('-a') + cmd.append('local_port=' + self.local_port) + + if self.dsfield: + cmd.append('-a') + cmd.append('dsfield=' + self.dsfield) + + if self.maxbw: + cmd.append('-p') + cmd.append('maxbw=' + self.maxbw) + + if self.priority: + cmd.append('-p') + cmd.append('priority=' + self.priority) + + if self.temporary: + cmd.append('-t') + cmd.append(self.name) + + return self.module.run_command(cmd) + + def _query_flow_props(self): + cmd = [self.module.get_bin_path('flowadm')] + + cmd.append('show-flowprop') + cmd.append('-c') + cmd.append('-o') + cmd.append('property,possible') + cmd.append(self.name) + + return self.module.run_command(cmd) + + def flow_needs_udpating(self): + (rc, out, err) = self._query_flow_props() + + NEEDS_UPDATING = False + + if rc == 0: + properties = (line.split(':') for line in out.rstrip().split('\n')) + for prop, value in properties: + if prop == 'maxbw' and self.maxbw != value: + self._needs_updating.update({prop: True}) + NEEDS_UPDATING = True + + elif prop == 'priority' and self.priority != value: + self._needs_updating.update({prop: True}) + NEEDS_UPDATING = True + + return NEEDS_UPDATING + else: + self.module.fail_json(msg='Error while checking flow properties: %s' % err, + stderr=err, + rc=rc) + + def update_flow(self): + cmd = [self.module.get_bin_path('flowadm')] + + cmd.append('set-flowprop') + + if self.maxbw and self._needs_updating['maxbw']: + cmd.append('-p') + cmd.append('maxbw=' + self.maxbw) + + if self.priority and self._needs_updating['priority']: + cmd.append('-p') + cmd.append('priority=' + self.priority) + + if self.temporary: + cmd.append('-t') + cmd.append(self.name) + + return self.module.run_command(cmd) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + name=dict(required=True, aliases=['flow']), + link=dict(required=False), + local_ip=dict(required=False), + remote_ip=dict(required=False), + transport=dict(required=False, choices=SUPPORTED_TRANSPORTS), + local_port=dict(required=False), + dsfield=dict(required=False), + maxbw=dict(required=False), + priority=dict(required=False, + default='medium', + choices=SUPPORTED_PRIORITIES), + temporary=dict(default=False, type='bool'), + state=dict(required=False, + default='present', + choices=['absent', 'present', 'resetted']), + ), + mutually_exclusive=[ + ('local_ip', 'remote_ip'), + ('local_ip', 'transport'), + ('local_ip', 'local_port'), + ('local_ip', 'dsfield'), + ('remote_ip', 'transport'), + ('remote_ip', 'local_port'), + ('remote_ip', 'dsfield'), + ('transport', 'dsfield'), + ('local_port', 'dsfield'), + ], + supports_check_mode=True + ) + + flow = Flow(module) + + rc = None + out = '' + err = '' + result = {} + result['name'] = flow.name + result['state'] = flow.state + result['temporary'] = flow.temporary + + if flow.link: + result['link'] = flow.link + + if flow.maxbw: + result['maxbw'] = flow.maxbw + + if flow.priority: + result['priority'] = flow.priority + + if flow.local_ip: + if flow.is_valid_address(flow.local_ip): + result['local_ip'] = flow.local_ip + + if flow.remote_ip: + if flow.is_valid_address(flow.remote_ip): + result['remote_ip'] = flow.remote_ip + + if flow.transport: + result['transport'] = flow.transport + + if flow.local_port: + if flow.is_valid_port(flow.local_port): + result['local_port'] = flow.local_port + else: + module.fail_json(msg='Invalid port: %s' % flow.local_port, + rc=1) + + if flow.dsfield: + if flow.is_valid_dsfield(flow.dsfield): + result['dsfield'] = flow.dsfield + else: + module.fail_json(msg='Invalid dsfield: %s' % flow.dsfield, + rc=1) + + if flow.state == 'absent': + if flow.flow_exists(): + if module.check_mode: + module.exit_json(changed=True) + + (rc, out, err) = flow.delete_flow() + if rc != 0: + module.fail_json(msg='Error while deleting flow: "%s"' % err, + name=flow.name, + stderr=err, + rc=rc) + + elif flow.state == 'present': + if not flow.flow_exists(): + if module.check_mode: + module.exit_json(changed=True) + + (rc, out, err) = flow.create_flow() + if rc != 0: + module.fail_json(msg='Error while creating flow: "%s"' % err, + name=flow.name, + stderr=err, + rc=rc) + else: + if flow.flow_needs_udpating(): + (rc, out, err) = flow.update_flow() + if rc != 0: + module.fail_json(msg='Error while updating flow: "%s"' % err, + name=flow.name, + stderr=err, + rc=rc) + + elif flow.state == 'resetted': + if flow.flow_exists(): + if module.check_mode: + module.exit_json(changed=True) + + (rc, out, err) = flow.reset_flow() + if rc != 0: + module.fail_json(msg='Error while resetting flow: "%s"' % err, + name=flow.name, + stderr=err, + rc=rc) + + if rc is None: + result['changed'] = False + else: + result['changed'] = True + + if out: + result['stdout'] = out + if err: + result['stderr'] = err + + module.exit_json(**result) + + +from ansible.module_utils.basic import * +main() diff --git a/network/illumos/ipadm_if.py b/network/illumos/ipadm_if.py new file mode 100644 index 00000000000..c7419848fc0 --- /dev/null +++ b/network/illumos/ipadm_if.py @@ -0,0 +1,222 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Adam Števko +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +DOCUMENTATION = ''' +--- +module: ipadm_if +short_description: Manage IP interfaces on Solaris/illumos systems. +description: + - Create, delete, enable or disable IP interfaces on Solaris/illumos + systems. +version_added: "2.2" +author: Adam Števko (@xen0l) +options: + name: + description: + - IP interface name. + required: true + temporary: + description: + - Specifies that the IP interface is temporary. Temporary IP + interfaces do not persist across reboots. + required: false + default: false + choices: [ "true", "false" ] + state: + description: + - Create or delete Solaris/illumos IP interfaces. + required: false + default: "present" + choices: [ "present", "absent", "enabled", "disabled" ] +''' + +EXAMPLES = ''' +# Create vnic0 interface +ipadm_if: name=vnic0 state=enabled + +# Disable vnic0 interface +ipadm_if: name=vnic0 state=disabled +''' + +RETURN = ''' +name: + description: IP interface name + returned: always + type: string + sample: "vnic0" +state: + description: state of the target + returned: always + type: string + sample: "present" +temporary: + description: persistence of a IP interface + returned: always + type: boolean + sample: "True" +''' + + +class IPInterface(object): + + def __init__(self, module): + self.module = module + + self.name = module.params['name'] + self.temporary = module.params['temporary'] + self.state = module.params['state'] + + def interface_exists(self): + cmd = [self.module.get_bin_path('ipadm', True)] + + cmd.append('show-if') + cmd.append(self.name) + + (rc, _, _) = self.module.run_command(cmd) + if rc == 0: + return True + else: + return False + + def interface_is_disabled(self): + cmd = [self.module.get_bin_path('ipadm', True)] + + cmd.append('show-if') + cmd.append('-o') + cmd.append('state') + cmd.append(self.name) + + (rc, out, err) = self.module.run_command(cmd) + if rc != 0: + self.module.fail_json(name=self.name, rc=rc, msg=err) + + return 'disabled' in out + + def create_interface(self): + cmd = [self.module.get_bin_path('ipadm', True)] + + cmd.append('create-if') + + if self.temporary: + cmd.append('-t') + + cmd.append(self.name) + + return self.module.run_command(cmd) + + def delete_interface(self): + cmd = [self.module.get_bin_path('ipadm', True)] + + cmd.append('delete-if') + + if self.temporary: + cmd.append('-t') + + cmd.append(self.name) + + return self.module.run_command(cmd) + + def enable_interface(self): + cmd = [self.module.get_bin_path('ipadm', True)] + + cmd.append('enable-if') + cmd.append('-t') + cmd.append(self.name) + + return self.module.run_command(cmd) + + def disable_interface(self): + cmd = [self.module.get_bin_path('ipadm', True)] + + cmd.append('disable-if') + cmd.append('-t') + cmd.append(self.name) + + return self.module.run_command(cmd) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + name=dict(required=True), + temporary=dict(default=False, type='bool'), + state=dict(default='present', choices=['absent', + 'present', + 'enabled', + 'disabled']), + ), + supports_check_mode=True + ) + + interface = IPInterface(module) + + rc = None + out = '' + err = '' + result = {} + result['name'] = interface.name + result['state'] = interface.state + result['temporary'] = interface.temporary + + if interface.state == 'absent': + if interface.interface_exists(): + if module.check_mode: + module.exit_json(changed=True) + (rc, out, err) = interface.delete_interface() + if rc != 0: + module.fail_json(name=interface.name, msg=err, rc=rc) + elif interface.state == 'present': + if not interface.interface_exists(): + if module.check_mode: + module.exit_json(changed=True) + (rc, out, err) = interface.create_interface() + + if rc is not None and rc != 0: + module.fail_json(name=interface.name, msg=err, rc=rc) + + elif interface.state == 'enabled': + if interface.interface_is_disabled(): + (rc, out, err) = interface.enable_interface() + + if rc is not None and rc != 0: + module.fail_json(name=interface.name, msg=err, rc=rc) + + elif interface.state == 'disabled': + if not interface.interface_is_disabled(): + (rc, out, err) = interface.disable_interface() + + if rc is not None and rc != 0: + module.fail_json(name=interface.name, msg=err, rc=rc) + + if rc is None: + result['changed'] = False + else: + result['changed'] = True + + if out: + result['stdout'] = out + if err: + result['stderr'] = err + + module.exit_json(**result) + +from ansible.module_utils.basic import * +main() diff --git a/network/illumos/ipadm_prop.py b/network/illumos/ipadm_prop.py new file mode 100644 index 00000000000..5399189ad35 --- /dev/null +++ b/network/illumos/ipadm_prop.py @@ -0,0 +1,264 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Adam Števko +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +DOCUMENTATION = ''' +--- +module: ipadm_prop +short_description: Manage protocol properties on Solaris/illumos systems. +description: + - Modify protocol properties on Solaris/illumos systems. +version_added: "2.2" +author: Adam Števko (@xen0l) +options: + protocol: + description: + - Specifies the procotol for which we want to manage properties. + required: true + property: + description: + - Specifies the name of property we want to manage. + required: true + value: + description: + - Specifies the value we want to set for the property. + required: false + temporary: + description: + - Specifies that the property value is temporary. Temporary + property values do not persist across reboots. + required: false + default: false + choices: [ "true", "false" ] + state: + description: + - Set or reset the property value. + required: false + default: present + choices: [ "present", "absent", "reset" ] +''' + +EXAMPLES = ''' +# Set TCP receive buffer size +ipadm_prop: protocol=tcp property=recv_buf value=65536 + +# Reset UDP send buffer size to the default value +ipadm_prop: protocol=udp property=send_buf state=reset +''' + +RETURN = ''' +protocol: + description: property's protocol + returned: always + type: string + sample: "TCP" +property: + description: name of the property + returned: always + type: string + sample: "recv_maxbuf" +state: + description: state of the target + returned: always + type: string + sample: "present" +temporary: + description: property's persistence + returned: always + type: boolean + sample: "True" +value: + description: value of the property + returned: always + type: int/string (depends on property) + sample: 1024/never +''' + +SUPPORTED_PROTOCOLS = ['ipv4', 'ipv6', 'icmp', 'tcp', 'udp', 'sctp'] + + +class Prop(object): + + def __init__(self, module): + self.module = module + + self.protocol = module.params['protocol'] + self.property = module.params['property'] + self.value = module.params['value'] + self.temporary = module.params['temporary'] + self.state = module.params['state'] + + def property_exists(self): + cmd = [self.module.get_bin_path('ipadm')] + + cmd.append('show-prop') + cmd.append('-p') + cmd.append(self.property) + cmd.append(self.protocol) + + (rc, _, _) = self.module.run_command(cmd) + + if rc == 0: + return True + else: + self.module.fail_json(msg='Unknown property "%s" for protocol %s' % + (self.property, self.protocol), + protocol=self.protocol, + property=self.property) + + def property_is_modified(self): + cmd = [self.module.get_bin_path('ipadm')] + + cmd.append('show-prop') + cmd.append('-c') + cmd.append('-o') + cmd.append('current,default') + cmd.append('-p') + cmd.append(self.property) + cmd.append(self.protocol) + + (rc, out, _) = self.module.run_command(cmd) + + out = out.rstrip() + (value, default) = out.split(':') + + if rc == 0 and value == default: + return True + else: + return False + + def property_is_set(self): + cmd = [self.module.get_bin_path('ipadm')] + + cmd.append('show-prop') + cmd.append('-c') + cmd.append('-o') + cmd.append('current') + cmd.append('-p') + cmd.append(self.property) + cmd.append(self.protocol) + + (rc, out, _) = self.module.run_command(cmd) + + out = out.rstrip() + + if rc == 0 and self.value == out: + return True + else: + return False + + def set_property(self): + cmd = [self.module.get_bin_path('ipadm')] + + cmd.append('set-prop') + + if self.temporary: + cmd.append('-t') + + cmd.append('-p') + cmd.append(self.property + "=" + self.value) + cmd.append(self.protocol) + + return self.module.run_command(cmd) + + def reset_property(self): + cmd = [self.module.get_bin_path('ipadm')] + + cmd.append('reset-prop') + + if self.temporary: + cmd.append('-t') + + cmd.append('-p') + cmd.append(self.property) + cmd.append(self.protocol) + + return self.module.run_command(cmd) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + protocol=dict(required=True, choices=SUPPORTED_PROTOCOLS), + property=dict(required=True), + value=dict(required=False), + temporary=dict(default=False, type='bool'), + state=dict( + default='present', choices=['absent', 'present', 'reset']), + ), + supports_check_mode=True + ) + + prop = Prop(module) + + rc = None + out = '' + err = '' + result = {} + result['protocol'] = prop.protocol + result['property'] = prop.property + result['state'] = prop.state + result['temporary'] = prop.temporary + if prop.value: + result['value'] = prop.value + + if prop.state == 'absent' or prop.state == 'reset': + if prop.property_exists(): + if not prop.property_is_modified(): + if module.check_mode: + module.exit_json(changed=True) + (rc, out, err) = prop.reset_property() + if rc != 0: + module.fail_json(protocol=prop.protocol, + property=prop.property, + msg=err, + rc=rc) + + elif prop.state == 'present': + if prop.value is None: + module.fail_json(msg='Value is mandatory with state "present"') + + if prop.property_exists(): + if not prop.property_is_set(): + if module.check_mode: + module.exit_json(changed=True) + + (rc, out, err) = prop.set_property() + if rc != 0: + module.fail_json(protocol=prop.protocol, + property=prop.property, + msg=err, + rc=rc) + + if rc is None: + result['changed'] = False + else: + result['changed'] = True + + if out: + result['stdout'] = out + if err: + result['stderr'] = err + + module.exit_json(**result) + + +from ansible.module_utils.basic import * +main() From a39da41bb26407d8ca9cb9aabf522c3cd46677d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20R=C3=BCetschi?= Date: Tue, 30 Aug 2016 20:47:30 +0200 Subject: [PATCH 2031/2522] Feature udm dns zone (#2382) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * UCS udm_dns_zone: added Signed-off-by: Tobias Rüetschi * UCS udm_dns_zone: updated, add supports check mode Signed-off-by: Tobias Rüetschi * UCS udm_dns_zone: updated, add support to modify dns zones * UCS udm_dns_zone: change string formating * UCS udm_dns_zone: add a function to convert the time to the biggest unit * UCS udm_dns_zone: add type definitions to the argument specification * UCS udm_dns_zone: update function convert_time * UCS udm_dns_zone: only modify object if it has changed * UCS udm_dns_zone: add documentation * UCS udm_dns_zone: fix checks * UCS udm_dns_zone: if dns zone not exists, changed is always true * UCS udm_dns_zone: documentation, add version_added * UCS udm_dns_zone: add license and fix travis for python 3 * UCS udm_dns_zone: import common code for univention from ansible.module_utils.univention * univention udm_dns_zone: adapt to library univention_umc * univention udm_dns_zone: lint * univention udm_dns_zone: add requirement python >= 2.6 to documentation * univention udm_dns_zone: dont import time, its unused --- univention/udm_dns_zone.py | 240 +++++++++++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 univention/udm_dns_zone.py diff --git a/univention/udm_dns_zone.py b/univention/udm_dns_zone.py new file mode 100644 index 00000000000..88fbf878688 --- /dev/null +++ b/univention/udm_dns_zone.py @@ -0,0 +1,240 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- + +# Copyright (c) 2016, Adfinis SyGroup AG +# Tobias Rueetschi +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.univention_umc import ( + umc_module_for_add, + umc_module_for_edit, + ldap_search, + base_dn, +) + + +DOCUMENTATION = ''' +--- +module: udm_dns_zone +version_added: "2.2" +author: "Tobias Rueetschi (@2-B)" +short_description: Manage dns zones on a univention corporate server +description: + - "This module allows to manage dns zones on a univention corporate server (UCS). + It uses the python API of the UCS to create a new object or edit it." +requirements: + - Python >= 2.6 +options: + state: + required: false + default: "present" + choices: [ present, absent ] + description: + - Whether the dns zone is present or not. + type: + required: true + choices: [ forward_zone, reverse_zone ] + description: + - Define if the zone is a forward or reverse DNS zone. + zone: + required: true + description: + - DNS zone name, e.g. C(example.com). + nameserver: + required: false + description: + - List of appropriate name servers. Required if C(state=present). + interfaces: + required: false + description: + - List of interface IP addresses, on which the server should + response this zone. Required if C(state=present). + + refresh: + required: false + default: 3600 + description: + - Interval before the zone should be refreshed. + retry: + required: false + default: 1800 + description: + - Interval that should elapse before a failed refresh should be retried. + expire: + required: false + default: 604800 + description: + - Specifies the upper limit on the time interval that can elapse before the zone is no longer authoritative. + ttl: + required: false + default: 600 + description: + - Minimum TTL field that should be exported with any RR from this zone. + + contact: + required: false + default: '' + description: + - Contact person in the SOA record. + mx: + required: false + default: [] + description: + - List of MX servers. (Must declared as A or AAAA records). +''' + + +EXAMPLES = ''' +# Create a DNS zone on a UCS +- udm_dns_zone: zone=example.com + type=forward_zone + nameserver=['ucs.example.com'] + interfaces=['192.168.1.1'] +''' + + +RETURN = '''# ''' + + +def convert_time(time): + """Convert a time in seconds into the biggest unit""" + units = [ + (24 * 60 * 60 , 'days'), + (60 * 60 , 'hours'), + (60 , 'minutes'), + (1 , 'seconds'), + ] + + if time == 0: + return ('0', 'seconds') + for unit in units: + if time >= unit[0]: + return ('{}'.format(time // unit[0]), unit[1]) + + +def main(): + module = AnsibleModule( + argument_spec = dict( + type = dict(required=True, + type='str'), + zone = dict(required=True, + aliases=['name'], + type='str'), + nameserver = dict(default=[], + type='list'), + interfaces = dict(default=[], + type='list'), + refresh = dict(default=3600, + type='int'), + retry = dict(default=1800, + type='int'), + expire = dict(default=604800, + type='int'), + ttl = dict(default=600, + type='int'), + contact = dict(default='', + type='str'), + mx = dict(default=[], + type='list'), + state = dict(default='present', + choices=['present', 'absent'], + type='str') + ), + supports_check_mode=True, + required_if = ([ + ('state', 'present', ['nameserver', 'interfaces']) + ]) + ) + type = module.params['type'] + zone = module.params['zone'] + nameserver = module.params['nameserver'] + interfaces = module.params['interfaces'] + refresh = module.params['refresh'] + retry = module.params['retry'] + expire = module.params['expire'] + ttl = module.params['ttl'] + contact = module.params['contact'] + mx = module.params['mx'] + state = module.params['state'] + changed = False + + obj = list(ldap_search( + '(&(objectClass=dNSZone)(zoneName={}))'.format(zone), + attr=['dNSZone'] + )) + + exists = bool(len(obj)) + container = 'cn=dns,{}'.format(base_dn()) + dn = 'zoneName={},{}'.format(zone, container) + if contact == '': + contact = 'root@{}.'.format(zone) + + if state == 'present': + try: + if not exists: + obj = umc_module_for_add('dns/{}'.format(type), container) + else: + obj = umc_module_for_edit('dns/{}'.format(type), dn) + obj['zone'] = zone + obj['nameserver'] = nameserver + obj['a'] = interfaces + obj['refresh'] = convert_time(refresh) + obj['retry'] = convert_time(retry) + obj['expire'] = convert_time(expire) + obj['ttl'] = convert_time(ttl) + obj['contact'] = contact + obj['mx'] = mx + diff = obj.diff() + if exists: + for k in obj.keys(): + if obj.hasChanged(k): + changed = True + else: + changed = True + if not module.check_mode: + if not exists: + obj.create() + elif changed: + obj.modify() + except Exception as e: + module.fail_json( + msg='Creating/editing dns zone {} failed: {}'.format(zone, e) + ) + + if state == 'absent' and exists: + try: + obj = umc_module_for_edit('dns/{}'.format(type), dn) + if not module.check_mode: + obj.remove() + changed = True + except Exception as e: + module.fail_json( + msg='Removing dns zone {} failed: {}'.format(zone, e) + ) + + module.exit_json( + changed=changed, + diff=diff, + zone=zone + ) + + +if __name__ == '__main__': + main() From 38b4263531f939a58b222f377ccaa6abe5644453 Mon Sep 17 00:00:00 2001 From: John R Barker Date: Tue, 30 Aug 2016 20:37:05 +0100 Subject: [PATCH 2032/2522] ec2_vpc_dhcp_options_facts.py no py3 Old PR was merged that hadn't been updated since we added the py3 checks --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index ab48d103e62..87522120ee3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,6 +29,7 @@ env: cloud/amazon/ec2_vpc_route_table.py cloud/amazon/ec2_vpc_subnet_facts.py cloud/amazon/ec2_vpc_subnet.py + cloud/amazon/ec2_vpc_dhcp_options_facts.py cloud/amazon/ecs_cluster.py cloud/amazon/ecs_service_facts.py cloud/amazon/ecs_service.py From 44c86245fff5edc937f84461754c0090f099f9b2 Mon Sep 17 00:00:00 2001 From: Joe Smith Date: Tue, 30 Aug 2016 15:55:41 -0400 Subject: [PATCH 2033/2522] Set explicit type for timeout (#2809) vmware_maintenancemode.py needs explicit type for timeout, otherwise it reads timeout as string and breaks. --- cloud/vmware/vmware_maintenancemode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/vmware/vmware_maintenancemode.py b/cloud/vmware/vmware_maintenancemode.py index 0af69e38057..84d774ec765 100644 --- a/cloud/vmware/vmware_maintenancemode.py +++ b/cloud/vmware/vmware_maintenancemode.py @@ -178,7 +178,7 @@ def main(): 'evacuateAllData', 'noAction']), evacuate=dict(required=False, type='bool', default=False), - timeout=dict(required=False, default=0), + timeout=dict(required=False, default=0, type='int'), state=dict(required=False, default='present', choices=['present', 'absent']))) From 2c78dea646975f7e32cc40bc05bb5c1eacb67359 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Tue, 30 Aug 2016 23:04:48 +0200 Subject: [PATCH 2034/2522] Require domain in rocketchat (#2803) * Set the domain as required * Add domain in the examples as well --- notification/rocketchat.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/notification/rocketchat.py b/notification/rocketchat.py index 1239fc01d56..ffce79712b1 100644 --- a/notification/rocketchat.py +++ b/notification/rocketchat.py @@ -113,11 +113,13 @@ local_action: module: rocketchat token: thetoken/generatedby/rocketchat + domain: chat.example.com msg: "{{ inventory_hostname }} completed" - name: Send notification message via Rocket Chat all options local_action: module: rocketchat + domain: chat.example.com token: thetoken/generatedby/rocketchat msg: "{{ inventory_hostname }} completed" channel: "#ansible" @@ -128,6 +130,7 @@ - name: insert a color bar in front of the message for visibility purposes and use the default webhook icon and name configured in rocketchat rocketchat: token: thetoken/generatedby/rocketchat + domain: chat.example.com msg: "{{ inventory_hostname }} is alive!" color: good username: "" @@ -136,6 +139,7 @@ - name: Use the attachments API rocketchat: token: thetoken/generatedby/rocketchat + domain: chat.example.com attachments: - text: "Display my system load on host A and B" color: "#ff00dd" @@ -207,7 +211,7 @@ def do_notify_rocketchat(module, domain, token, protocol, payload): def main(): module = AnsibleModule( argument_spec = dict( - domain = dict(type='str', required=False, default=None), + domain = dict(type='str', required=True, default=None), token = dict(type='str', required=True, no_log=True), protocol = dict(type='str', default='https', choices=['http', 'https']), msg = dict(type='str', required=False, default=None), From 1c4f346691654b47084fc932aae5dce79e0d3dd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Tue, 30 Aug 2016 23:38:07 +0200 Subject: [PATCH 2035/2522] ec2_vpc_dhcp_options_facts: fix exception handling, fixes build (#2819) --- cloud/amazon/ec2_vpc_dhcp_options_facts.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cloud/amazon/ec2_vpc_dhcp_options_facts.py b/cloud/amazon/ec2_vpc_dhcp_options_facts.py index 8c59aeb5c9c..a60a2104892 100644 --- a/cloud/amazon/ec2_vpc_dhcp_options_facts.py +++ b/cloud/amazon/ec2_vpc_dhcp_options_facts.py @@ -81,7 +81,7 @@ try: import botocore - import boto3 + import boto3 HAS_BOTO3 = True except ImportError: HAS_BOTO3 = False @@ -128,7 +128,7 @@ def list_dhcp_options(client, module): snaked_dhcp_options_array = [] for dhcp_option in all_dhcp_options_array: snaked_dhcp_options_array.append(camel_dict_to_snake_dict(dhcp_option)) - + module.exit_json(dhcp_options=snaked_dhcp_options_array) @@ -151,12 +151,12 @@ def main(): try: region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) connection = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except botocore.exceptions.NoCredentialsError, e: + except botocore.exceptions.NoCredentialsError as e: module.fail_json(msg="Can't authorize connection - "+str(e)) # call your function here results = list_dhcp_options(connection, module) - + module.exit_json(result=results) # import module snippets From f69d32ec44d635925830b6ba0d14fe8639062e1c Mon Sep 17 00:00:00 2001 From: afunix Date: Wed, 31 Aug 2016 08:27:07 +0300 Subject: [PATCH 2036/2522] gluster_volume module parses out additional hostnames provided by "gluster peer status" command [#1405] (#2811) --- system/gluster_volume.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/system/gluster_volume.py b/system/gluster_volume.py index f7fae041299..9df9bce1114 100644 --- a/system/gluster_volume.py +++ b/system/gluster_volume.py @@ -181,16 +181,24 @@ def get_peers(): hostname = None uuid = None state = None + shortNames = False for row in out.split('\n'): if ': ' in row: key, value = row.split(': ') if key.lower() == 'hostname': hostname = value + shortNames = False if key.lower() == 'uuid': uuid = value if key.lower() == 'state': state = value peers[hostname] = [ uuid, state ] + elif row.lower() == 'other names:': + shortNames = True + elif row != '' and shortNames == True: + peers[row] = [ uuid, state ] + elif row == '': + shortNames = False return peers def get_volumes(): From 9cd681a84140434ab804c1b431a517fbcc2ff14c Mon Sep 17 00:00:00 2001 From: Wong Hoi Sing Edison Date: Wed, 31 Aug 2016 13:34:31 +0800 Subject: [PATCH 2037/2522] Fix ansible/ansible-modules-extras#1682: add dispersed volume support for gluster_volume (#2708) --- system/gluster_volume.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/system/gluster_volume.py b/system/gluster_volume.py index 9df9bce1114..820b7ef2d5c 100644 --- a/system/gluster_volume.py +++ b/system/gluster_volume.py @@ -56,6 +56,18 @@ default: null description: - Stripe count for volume + disperses: + required: false + default: null + description: + - Disperse count for volume + version_added: "2.2" + redundancies: + required: false + default: null + description: + - Redundancy count for volume + version_added: "2.2" transport: required: false choices: [ 'tcp', 'rdma', 'tcp,rdma' ] @@ -272,7 +284,7 @@ def probe_all_peers(hosts, peers, myhostname): if host not in peers: probe(host, myhostname) -def create_volume(name, stripe, replica, transport, hosts, bricks, force): +def create_volume(name, stripe, replica, disperse, redundancy, transport, hosts, bricks, force): args = [ 'volume', 'create' ] args.append(name) if stripe: @@ -281,6 +293,12 @@ def create_volume(name, stripe, replica, transport, hosts, bricks, force): if replica: args.append('replica') args.append(str(replica)) + if disperse: + args.append('disperse') + args.append(str(disperse)) + if redundancy: + args.append('redundancy') + args.append(str(redundancy)) args.append('transport') args.append(transport) for brick in bricks: @@ -328,6 +346,8 @@ def main(): host=dict(required=False, default=None), stripes=dict(required=False, default=None, type='int'), replicas=dict(required=False, default=None, type='int'), + disperses=dict(required=False, default=None, type='int'), + redundancies=dict(required=False, default=None, type='int'), transport=dict(required=False, default='tcp', choices=[ 'tcp', 'rdma', 'tcp,rdma' ]), bricks=dict(required=False, default=None, aliases=['brick']), start_on_create=dict(required=False, default=True, type='bool'), @@ -350,6 +370,8 @@ def main(): brick_paths = module.params['bricks'] stripes = module.params['stripes'] replicas = module.params['replicas'] + disperses = module.params['disperses'] + redundancies = module.params['redundancies'] transport = module.params['transport'] myhostname = module.params['host'] start_on_create = module.boolean(module.params['start_on_create']) @@ -397,7 +419,7 @@ def main(): # create if it doesn't exist if volume_name not in volumes: - create_volume(volume_name, stripes, replicas, transport, cluster, brick_paths, force) + create_volume(volume_name, stripes, replicas, disperses, redundancies, transport, cluster, brick_paths, force) volumes = get_volumes() changed = True From 86285e882497c2ba21b39b71c9305e53853630bc Mon Sep 17 00:00:00 2001 From: Ethan Devenport Date: Wed, 31 Aug 2016 06:24:55 +0000 Subject: [PATCH 2038/2522] Set variable types, defined choices, and cleaned up whitespace. --- cloud/profitbricks/profitbricks.py | 13 +++++----- cloud/profitbricks/profitbricks_volume.py | 30 ++++++++++++++--------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/cloud/profitbricks/profitbricks.py b/cloud/profitbricks/profitbricks.py index b0791f37e3a..a0bc79c77f8 100644 --- a/cloud/profitbricks/profitbricks.py +++ b/cloud/profitbricks/profitbricks.py @@ -580,15 +580,16 @@ def main(): datacenter=dict(), name=dict(), image=dict(), - cores=dict(default=2), - ram=dict(default=2048), - cpu_family=dict(default='AMD_OPTERON'), - volume_size=dict(default=10), - disk_type=dict(default='HDD'), + cores=dict(type='int', default=2), + ram=dict(type='int', default=2048), + cpu_family=dict(choices=['AMD_OPTERON', 'INTEL_XEON'], + default='AMD_OPTERON'), + volume_size=dict(type='int', default=10), + disk_type=dict(choices=['HDD', 'SSD'], default='HDD'), image_password=dict(default=None), ssh_keys=dict(type='list', default=[]), bus=dict(default='VIRTIO'), - lan=dict(default=1), + lan=dict(type='int', default=1), count=dict(type='int', default=1), auto_increment=dict(type='bool', default=True), instance_ids=dict(type='list', default=[]), diff --git a/cloud/profitbricks/profitbricks_volume.py b/cloud/profitbricks/profitbricks_volume.py index 6b7877f31cc..25b7f771814 100644 --- a/cloud/profitbricks/profitbricks_volume.py +++ b/cloud/profitbricks/profitbricks_volume.py @@ -41,7 +41,7 @@ required: false default: VIRTIO choices: [ "IDE", "VIRTIO"] - image: + image: description: - The system image ID for the volume, e.g. a3eae284-a2fe-11e4-b187-5f1f641608c8. This can also be a snapshot image ID. required: true @@ -63,13 +63,13 @@ choices: [ "HDD", "SSD" ] licence_type: description: - - The licence type for the volume. This is used when the image is non-standard. + - The licence type for the volume. This is used when the image is non-standard. required: false default: UNKNOWN choices: ["LINUX", "WINDOWS", "UNKNOWN" , "OTHER"] count: description: - - The number of volumes you wish to create. + - The number of volumes you wish to create. required: false default: 1 auto_increment: @@ -170,6 +170,7 @@ def _wait_for_completion(profitbricks, promise, wait_timeout, msg): promise['requestId'] ) + '" to complete.') + def _create_volume(module, profitbricks, datacenter, name): size = module.params.get('size') bus = module.params.get('bus') @@ -201,20 +202,22 @@ def _create_volume(module, profitbricks, datacenter, name): except Exception as e: module.fail_json(msg="failed to create the volume: %s" % str(e)) - + return volume_response + def _delete_volume(module, profitbricks, datacenter, volume): try: profitbricks.delete_volume(datacenter, volume) except Exception as e: module.fail_json(msg="failed to remove the volume: %s" % str(e)) + def create_volume(module, profitbricks): """ Creates a volume. - This will create a volume in a datacenter. + This will create a volume in a datacenter. module : AnsibleModule object profitbricks: authenticated profitbricks object. @@ -256,7 +259,7 @@ def create_volume(module, profitbricks): else: module.fail_json(msg=e.message) - number_range = xrange(count_offset,count_offset + count + len(numbers)) + number_range = xrange(count_offset, count_offset + count + len(numbers)) available_numbers = list(set(number_range).difference(numbers)) names = [] numbers_to_use = available_numbers[:count] @@ -265,7 +268,7 @@ def create_volume(module, profitbricks): else: names = [name] * count - for name in names: + for name in names: create_response = _create_volume(module, profitbricks, str(datacenter), name) volumes.append(create_response) _attach_volume(module, profitbricks, datacenter, create_response['id']) @@ -282,11 +285,12 @@ def create_volume(module, profitbricks): return results + def delete_volume(module, profitbricks): """ Removes a volume. - This will create a volume in a datacenter. + This will create a volume in a datacenter. module : AnsibleModule object profitbricks: authenticated profitbricks object. @@ -324,6 +328,7 @@ def delete_volume(module, profitbricks): return changed + def _attach_volume(module, profitbricks, datacenter, volume): """ Attaches a volume. @@ -339,12 +344,12 @@ def _attach_volume(module, profitbricks, datacenter, volume): server = module.params.get('server') # Locate UUID for Server - if server: + if server: if not (uuid_match.match(server)): server_list = profitbricks.list_servers(datacenter) for s in server_list['items']: if server == s['properties']['name']: - server= s['id'] + server = s['id'] break try: @@ -352,18 +357,19 @@ def _attach_volume(module, profitbricks, datacenter, volume): except Exception as e: module.fail_json(msg='failed to attach volume: %s' % str(e)) + def main(): module = AnsibleModule( argument_spec=dict( datacenter=dict(), server=dict(), name=dict(), - size=dict(default=10), + size=dict(type='int', default=10), bus=dict(default='VIRTIO'), image=dict(), image_password=dict(default=None), ssh_keys=dict(type='list', default=[]), - disk_type=dict(default='HDD'), + disk_type=dict(choices=['HDD', 'SSD'], default='HDD'), licence_type=dict(default='UNKNOWN'), count=dict(type='int', default=1), auto_increment=dict(type='bool', default=True), From 1eb5745f4978be031917df3aed04822803c35dd8 Mon Sep 17 00:00:00 2001 From: Ethan Devenport Date: Wed, 31 Aug 2016 07:46:12 +0000 Subject: [PATCH 2039/2522] Added parameter choices for bus. --- cloud/profitbricks/profitbricks.py | 2 +- cloud/profitbricks/profitbricks_volume.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/profitbricks/profitbricks.py b/cloud/profitbricks/profitbricks.py index a0bc79c77f8..7c9f23f6bb0 100644 --- a/cloud/profitbricks/profitbricks.py +++ b/cloud/profitbricks/profitbricks.py @@ -588,7 +588,7 @@ def main(): disk_type=dict(choices=['HDD', 'SSD'], default='HDD'), image_password=dict(default=None), ssh_keys=dict(type='list', default=[]), - bus=dict(default='VIRTIO'), + bus=dict(choices=['VIRTIO', 'IDE'], default='VIRTIO'), lan=dict(type='int', default=1), count=dict(type='int', default=1), auto_increment=dict(type='bool', default=True), diff --git a/cloud/profitbricks/profitbricks_volume.py b/cloud/profitbricks/profitbricks_volume.py index 25b7f771814..1cee9676750 100644 --- a/cloud/profitbricks/profitbricks_volume.py +++ b/cloud/profitbricks/profitbricks_volume.py @@ -365,7 +365,7 @@ def main(): server=dict(), name=dict(), size=dict(type='int', default=10), - bus=dict(default='VIRTIO'), + bus=dict(choices=['VIRTIO', 'IDE'], default='VIRTIO'), image=dict(), image_password=dict(default=None), ssh_keys=dict(type='list', default=[]), From d1799389529d18afcdf82aa4552d382671b101c2 Mon Sep 17 00:00:00 2001 From: Tobias Rueetschi Date: Tue, 30 Aug 2016 18:20:20 +0200 Subject: [PATCH 2040/2522] univention udm_user: pep8 --- univention/udm_user.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/univention/udm_user.py b/univention/udm_user.py index 0654c54aacd..8c689915420 100644 --- a/univention/udm_user.py +++ b/univention/udm_user.py @@ -21,6 +21,8 @@ # +from datetime import date +import crypt from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.univention_umc import ( umc_module_for_add, @@ -28,9 +30,7 @@ ldap_search, base_dn, ) -from datetime import date from dateutil.relativedelta import relativedelta -import crypt DOCUMENTATION = ''' @@ -40,7 +40,8 @@ author: "Tobias Rueetschi (@2-B)" short_description: Manage posix users on a univention corporate server description: - - "This module allows to manage posix users on a univention corporate server (UCS). + - "This module allows to manage posix users on a univention corporate + server (UCS). It uses the python API of the UCS to create a new object or edit it." requirements: - Python >= 2.6 @@ -268,8 +269,8 @@ required: false default: '' description: - - "Define the whole position of users object inside the LDAP tree, e.g. - C(cn=employee,cn=users,ou=school,dc=example,dc=com)." + - "Define the whole position of users object inside the LDAP tree, + e.g. C(cn=employee,cn=users,ou=school,dc=example,dc=com)." ou: required: false default: '' @@ -314,7 +315,7 @@ def main(): - expiry = date.strftime(date.today()+relativedelta(years=1), "%Y-%m-%d") + expiry = date.strftime(date.today() + relativedelta(years=1), "%Y-%m-%d") module = AnsibleModule( argument_spec = dict( birthday = dict(default=None, @@ -449,25 +450,25 @@ def main(): else: obj = umc_module_for_edit('users/user', user_dn) - if module.params['displayName'] == None: + if module.params['displayName'] is None: module.params['displayName'] = '{} {}'.format( module.params['firstname'], module.params['lastname'] ) - if module.params['unixhome'] == None: + if module.params['unixhome'] is None: module.params['unixhome'] = '/home/{}'.format( module.params['username'] ) for k in obj.keys(): if (k != 'password' and - k != 'groups' and - module.params.has_key(k) and - module.params[k] != None): + k != 'groups' and + k in module.params and + module.params[k] is not None): obj[k] = module.params[k] # handle some special values obj['e-mail'] = module.params['email'] password = module.params['password'] - if obj['password'] == None: + if obj['password'] is None: obj['password'] = password else: old_password = obj['password'].split('}', 2)[1] @@ -488,12 +489,17 @@ def main(): obj.modify() except: module.fail_json( - msg="Creating/editing user {} in {} failed".format(username, container) + msg="Creating/editing user {} in {} failed".format( + username, + container + ) ) try: groups = module.params['groups'] if groups: - filter = '(&(objectClass=posixGroup)(|(cn={})))'.format(')(cn='.join(groups)) + filter = '(&(objectClass=posixGroup)(|(cn={})))'.format( + ')(cn='.join(groups) + ) group_dns = list(ldap_search(filter, attr=['dn'])) for dn in group_dns: grp = umc_module_for_edit('groups/group', dn[0]) From caba5d171567a5f11a491ebd5546935041172751 Mon Sep 17 00:00:00 2001 From: Tobias Rueetschi Date: Wed, 31 Aug 2016 11:20:52 +0200 Subject: [PATCH 2041/2522] udm_user: change camelCase to snake_case in documentation --- univention/udm_user.py | 51 ++++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/univention/udm_user.py b/univention/udm_user.py index 8c689915420..d18c386a081 100644 --- a/univention/udm_user.py +++ b/univention/udm_user.py @@ -85,36 +85,40 @@ default: None description: - Country of users business address. - departmentNumber: + department_number: required: false default: None description: - Department number of users business address. + aliases: [ departmentNumber ] description: required: false default: None description: - Description (not gecos) - displayName: + display_name: required: false default: None description: - Display name (not gecos) + aliases: [ displayName ] email: required: false default: [''] description: - A list of e-mail addresses. - employeeNumber: + employee_number: required: false default: None description: - Employee number - employeeType: + aliases: [ employeeNumber ] + employee_type: required: false default: None description: - Employee type + aliases: [ employeeType ] gecos: required: false default: None @@ -127,57 +131,65 @@ - "POSIX groups, the LDAP DNs of the groups will be found with the LDAP filter for each group as $GROUP: C((&(objectClass=posixGroup)(cn=$GROUP)))." - homeShare: + home_share: required: false default: None description: - "Home NFS share. Must be a LDAP DN, e.g. C(cn=home,cn=shares,ou=school,dc=example,dc=com)." - homeSharePath: + aliases: [ homeShare ] + home_share_path: required: false default: None description: - Path to home NFS share, inside the homeShare. - homeTelephoneNumber: + aliases: [ homeSharePath ] + home_telephone_number: required: false default: [] description: - List of private telephone numbers. + aliases: [ homeTelephoneNumber ] homedrive: required: false default: None description: - Windows home drive, e.g. C("H:"). - mailAlternativeAddress: + mail_alternative_address: required: false default: [] description: - List of alternative e-mail addresses. - mailHomeServer: + aliases: [ mailAlternativeAddress ] + mail_home_server: required: false default: None description: - FQDN of mail server - mailPrimaryAddress: + aliases: [ mailHomeServer ] + mail_primary_address: required: false default: None description: - Primary e-mail address - mobileTelephoneNumber: + aliases: [ mailPrimaryAddress ] + mobile_telephone_number: required: false default: [] description: - Mobile phone number + aliases: [ mobileTelephoneNumber ] organisation: required: false default: None description: - Organisation - pagerTelephonenumber: + pager_telephonenumber: required: false default: [] description: - List of pager telephone numbers. + aliases: [ pagerTelephonenumber ] phone: required: false default: [] @@ -188,38 +200,43 @@ default: None description: - Postal code of users business address. - primaryGroup: + primary_group: required: false default: cn=Domain Users,cn=groups,$LDAP_BASE_DN description: - Primary group. This must be the group LDAP DN. + aliases: [ primaryGroup ] profilepath: required: false default: None description: - Windows profile directory - pwdChangeNextLogin: + pwd_change_next_login: required: false default: None choices: [ '0', '1' ] description: - Change password on next login. - roomNumber: + aliases: [ pwdChangeNextLogin ] + room_number: required: false default: None description: - Room number of users business address. - sambaPrivileges: + aliases: [ roomNumber ] + samba_privileges: required: false default: [] description: - "Samba privilege, like allow printer administration, do domain join." - sambaUserWorkstations: + aliases: [ sambaPrivileges ] + samba_user_workstations: required: false default: [] description: - Allow the authentication only on this Microsoft Windows host. + aliases: [ sambaUserWorkstations ] sambahome: required: false default: None From a370a8c8f6f826bd2522291336ebb5868d3c8e5a Mon Sep 17 00:00:00 2001 From: Tobias Rueetschi Date: Wed, 31 Aug 2016 11:30:45 +0200 Subject: [PATCH 2042/2522] udm_user: change code to fit documentation with snake_case instead of camelCase --- univention/udm_user.py | 85 +++++++++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 34 deletions(-) diff --git a/univention/udm_user.py b/univention/udm_user.py index d18c386a081..11213e465a3 100644 --- a/univention/udm_user.py +++ b/univention/udm_user.py @@ -341,46 +341,58 @@ def main(): type='str'), country = dict(default=None, type='str'), - departmentNumber = dict(default=None, - type='str'), + department_number = dict(default=None, + type='str', + aliases=['departmentNumber']), description = dict(default=None, type='str'), - displayName = dict(default=None, - type='str'), + display_name = dict(default=None, + type='str', + aliases=['displayName']), email = dict(default=[''], type='list'), - employeeNumber = dict(default=None, - type='str'), - employeeType = dict(default=None, - type='str'), + employee_number = dict(default=None, + type='str', + aliases=['employeeNumber']), + employee_type = dict(default=None, + type='str', + aliases=['employeeType']), firstname = dict(default=None, type='str'), gecos = dict(default=None, type='str'), groups = dict(default=[], type='list'), - homeShare = dict(default=None, - type='str'), - homeSharePath = dict(default=None, - type='str'), - homeTelephoneNumber = dict(default=[], - type='list'), + home_share = dict(default=None, + type='str', + aliases=['homeShare']), + home_share_path = dict(default=None, + type='str', + aliases=['homeSharePath']), + home_telephone_number = dict(default=[], + type='list', + aliases=['homeTelephoneNumber']), homedrive = dict(default=None, type='str'), lastname = dict(default=None, type='str'), - mailAlternativeAddress = dict(default=[], - type='list'), - mailHomeServer = dict(default=None, - type='str'), - mailPrimaryAddress = dict(default=None, - type='str'), - mobileTelephoneNumber = dict(default=[], - type='list'), + mail_alternative_address= dict(default=[], + type='list', + aliases=['mailAlternativeAddress']), + mail_home_server = dict(default=None, + type='str', + aliases=['mailHomeServer']), + mail_primary_address = dict(default=None, + type='str', + aliases=['mailPrimaryAddress']), + mobile_telephone_number = dict(default=[], + type='list', + aliases=['mobileTelephoneNumber']), organisation = dict(default=None, type='str'), - pagerTelephonenumber = dict(default=[], - type='list'), + pager_telephonenumber = dict(default=[], + type='list', + aliases=['pagerTelephonenumber']), password = dict(default=None, type='str', no_log=True), @@ -388,19 +400,24 @@ def main(): type='list'), postcode = dict(default=None, type='str'), - primaryGroup = dict(default=None, - type='str'), + primary_group = dict(default=None, + type='str', + aliases=['primaryGroup']), profilepath = dict(default=None, type='str'), - pwdChangeNextLogin = dict(default=None, + pwd_change_next_login = dict(default=None, type='str', - choices=['0', '1']), - roomNumber = dict(default=None, - type='str'), - sambaPrivileges = dict(default=[], - type='list'), - sambaUserWorkstations = dict(default=[], - type='list'), + choices=['0', '1'], + aliases=['pwdChangeNextLogin']), + room_number = dict(default=None, + type='str', + aliases=['roomNumber']), + samba_privileges = dict(default=[], + type='list', + aliases=['sambaPrivileges']), + samba_user_workstations = dict(default=[], + type='list', + aliases=['sambaUserWorkstations']), sambahome = dict(default=None, type='str'), scriptpath = dict(default=None, From 81c2fb46f10efaf1ccd4f83f4f34af506498e9cf Mon Sep 17 00:00:00 2001 From: Tobias Rueetschi Date: Tue, 30 Aug 2016 21:47:04 +0200 Subject: [PATCH 2043/2522] univention udm_user: override password history. --- univention/udm_user.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/univention/udm_user.py b/univention/udm_user.py index 11213e465a3..c3406b27299 100644 --- a/univention/udm_user.py +++ b/univention/udm_user.py @@ -184,6 +184,16 @@ default: None description: - Organisation + overridePWHistory: + required: false + default: False + description: + - Override password history + overridePWLength: + required: false + default: False + description: + - Override password check pager_telephonenumber: required: false default: [] @@ -390,6 +400,10 @@ def main(): aliases=['mobileTelephoneNumber']), organisation = dict(default=None, type='str'), + overridePWHistory = dict(default=False, + type='bool'), + overridePWLength = dict(default=False, + type='bool'), pager_telephonenumber = dict(default=[], type='list', aliases=['pagerTelephonenumber']), @@ -496,6 +510,7 @@ def main(): for k in obj.keys(): if (k != 'password' and k != 'groups' and + k != 'overridePWHistory' and k in module.params and module.params[k] is not None): obj[k] = module.params[k] @@ -507,6 +522,8 @@ def main(): else: old_password = obj['password'].split('}', 2)[1] if crypt.crypt(password, old_password) != old_password: + obj['overridePWHistory'] = module.params['overridePWHistory'] + obj['overridePWLength'] = module.params['overridePWLength'] obj['password'] = password diff = obj.diff() From ec2cc904e2edc193fc4a26aea11d8f0648644dae Mon Sep 17 00:00:00 2001 From: Tobias Rueetschi Date: Wed, 31 Aug 2016 12:42:54 +0200 Subject: [PATCH 2044/2522] udm_user: change overridePWHistory and overridePWLength to snake_case --- univention/udm_user.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/univention/udm_user.py b/univention/udm_user.py index c3406b27299..2eed02a2c04 100644 --- a/univention/udm_user.py +++ b/univention/udm_user.py @@ -184,16 +184,18 @@ default: None description: - Organisation - overridePWHistory: + override_pw_history: required: false default: False description: - Override password history - overridePWLength: + aliases: [ overridePWHistory ] + override_pw_length: required: false default: False description: - Override password check + aliases: [ overridePWLength ] pager_telephonenumber: required: false default: [] @@ -401,9 +403,11 @@ def main(): organisation = dict(default=None, type='str'), overridePWHistory = dict(default=False, - type='bool'), + type='bool', + aliases=['override_pw_history']), overridePWLength = dict(default=False, - type='bool'), + type='bool', + aliases=['override_pw_length']), pager_telephonenumber = dict(default=[], type='list', aliases=['pagerTelephonenumber']), From ef6eb80c1233ccdc75cbe938d883f88f6eae46e1 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Wed, 31 Aug 2016 05:49:03 -0700 Subject: [PATCH 2045/2522] Adds allow_service parameter to bigip_selfip (#2808) This parameter can be used to open up access to (among other things) the mgmt address of a BIG-IP. It is necessary for configuring bigips in an HA configuration. --- network/f5/bigip_selfip.py | 198 +++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) diff --git a/network/f5/bigip_selfip.py b/network/f5/bigip_selfip.py index 452e44bc680..abdae4fb997 100644 --- a/network/f5/bigip_selfip.py +++ b/network/f5/bigip_selfip.py @@ -28,6 +28,12 @@ description: - The IP addresses for the new self IP. This value is ignored upon update as addresses themselves cannot be changed after they are created. + allow_service: + description: + - Configure port lockdown for the Self IP. By default, the Self IP has a + "default deny" policy. This can be changed to allow TCP and UDP ports + as well as specific protocols. This list should contain C(protocol):C(port) + values. name: description: - The self IP to create. @@ -90,9 +96,76 @@ user: "admin" validate_certs: "no" delegate_to: localhost + +- name: Allow management web UI to be accessed on this Self IP + bigip_selfip: + name: "self1" + password: "secret" + server: "lb.mydomain.com" + state: "absent" + user: "admin" + validate_certs: "no" + allow_service: + - "tcp:443" + delegate_to: localhost + +- name: Allow HTTPS and SSH access to this Self IP + bigip_selfip: + name: "self1" + password: "secret" + server: "lb.mydomain.com" + state: "absent" + user: "admin" + validate_certs: "no" + allow_service: + - "tcp:443" + - "tpc:22" + delegate_to: localhost + +- name: Allow all services access to this Self IP + bigip_selfip: + name: "self1" + password: "secret" + server: "lb.mydomain.com" + state: "absent" + user: "admin" + validate_certs: "no" + allow_service: + - all + delegate_to: localhost + +- name: Allow only GRE and IGMP protocols access to this Self IP + bigip_selfip: + name: "self1" + password: "secret" + server: "lb.mydomain.com" + state: "absent" + user: "admin" + validate_certs: "no" + allow_service: + - gre:0 + - igmp:0 + delegate_to: localhost + +- name: Allow all TCP, but no other protocols access to this Self IP + bigip_selfip: + name: "self1" + password: "secret" + server: "lb.mydomain.com" + state: "absent" + user: "admin" + validate_certs: "no" + allow_service: + - tcp:0 + delegate_to: localhost ''' RETURN = ''' +allow_service: + description: Services that allowed via this Self IP + returned: changed + type: list + sample: ['igmp:0','tcp:22','udp:53'] address: description: The address for the Self IP returned: created @@ -144,6 +217,8 @@ FLOAT = ['enabled', 'disabled'] DEFAULT_TG = 'traffic-group-local-only' +ALLOWED_PROTOCOLS = ['eigrp', 'egp', 'gre', 'icmp', 'igmp', 'igp', 'ipip', + 'l2tp', 'ospf', 'pim', 'tcp', 'udp'] class BigIpSelfIp(object): @@ -188,6 +263,9 @@ def read(self): Therefore, this method will transform the data from the BIG-IP into a format that is more easily consumable by the rest of the class and the parameters that are supported by the module. + + :return: List of values currently stored in BIG-IP, formatted for use + in this class. """ p = dict() name = self.params['name'] @@ -207,16 +285,97 @@ def read(self): p['traffic_group'] = str(r.trafficGroup) if hasattr(r, 'vlan'): p['vlan'] = str(r.vlan) + if hasattr(r, 'allowService'): + if r.allowService == 'all': + p['allow_service'] = set(['all']) + else: + p['allow_service'] = set([str(x) for x in r.allowService]) + else: + p['allow_service'] = set(['none']) p['name'] = name return p + def verify_services(self): + """Verifies that a supplied service string has correct format + + The string format for port lockdown is PROTOCOL:PORT. This method + will verify that the provided input matches the allowed protocols + and the port ranges before submitting to BIG-IP. + + The only allowed exceptions to this rule are the following values + + * all + * default + * none + + These are special cases that are handled differently in the API. + "all" is set as a string, "default" is set as a one item list, and + "none" removes the key entirely from the REST API. + + :raises F5ModuleError: + """ + result = [] + for svc in self.params['allow_service']: + if svc in ['all', 'none', 'default']: + result = [svc] + break + + tmp = svc.split(':') + if tmp[0] not in ALLOWED_PROTOCOLS: + raise F5ModuleError( + "The provided protocol '%s' is invalid" % (tmp[0]) + ) + try: + port = int(tmp[1]) + except Exception: + raise F5ModuleError( + "The provided port '%s' is not a number" % (tmp[1]) + ) + + if port < 0 or port > 65535: + raise F5ModuleError( + "The provided port '%s' must be between 0 and 65535" + % (port) + ) + else: + result.append(svc) + return set(result) + + def fmt_services(self, services): + """Returns services formatted for consumption by f5-sdk update + + The BIG-IP endpoint for services takes different values depending on + what you want the "allowed services" to be. It can be any of the + following + + - a list containing "protocol:port" values + - the string "all" + - a null value, or None + + This is a convenience function to massage the values the user has + supplied so that they are formatted in such a way that BIG-IP will + accept them and apply the specified policy. + + :param services: The services to format. This is always a Python set + :return: + """ + result = list(services) + if result[0] == 'all': + return 'all' + elif result[0] == 'none': + return None + else: + return list(services) + def update(self): changed = False + svcs = [] params = dict() current = self.read() check_mode = self.params['check_mode'] address = self.params['address'] + allow_service = self.params['allow_service'] name = self.params['name'] netmask = self.params['netmask'] partition = self.params['partition'] @@ -278,6 +437,14 @@ def update(self): 'The specified VLAN was not found' ) + if allow_service is not None: + svcs = self.verify_services() + if 'allow_service' in current: + if svcs != current['allow_service']: + params['allowService'] = self.fmt_services(svcs) + else: + params['allowService'] = self.fmt_services(svcs) + if params: changed = True params['name'] = name @@ -285,6 +452,8 @@ def update(self): if check_mode: return changed self.cparams = camel_dict_to_snake_dict(params) + if svcs: + self.cparams['allow_service'] = list(svcs) else: return changed @@ -298,6 +467,25 @@ def update(self): return True def get_vlans(self): + """Returns formatted list of VLANs + + The VLAN values stored in BIG-IP are done so using their fully + qualified name which includes the partition. Therefore, "correct" + values according to BIG-IP look like this + + /Common/vlan1 + + This is in contrast to the formats that most users think of VLANs + as being stored as + + vlan1 + + To provide for the consistent user experience while not turfing + BIG-IP, we need to massage the values that are provided by the + user so that they include the partition. + + :return: List of vlans formatted with preceeding partition + """ partition = self.params['partition'] vlans = self.api.tm.net.vlans.get_collection() return [str("/" + partition + "/" + x.name) for x in vlans] @@ -305,8 +493,10 @@ def get_vlans(self): def create(self): params = dict() + svcs = [] check_mode = self.params['check_mode'] address = self.params['address'] + allow_service = self.params['allow_service'] name = self.params['name'] netmask = self.params['netmask'] partition = self.params['partition'] @@ -353,10 +543,17 @@ def create(self): 'The specified VLAN was not found' ) + if allow_service is not None: + svcs = self.verify_services() + params['allowService'] = self.fmt_services(svcs) + params['name'] = name params['partition'] = partition self.cparams = camel_dict_to_snake_dict(params) + if svcs: + self.cparams['allow_service'] = list(svcs) + if check_mode: return True @@ -416,6 +613,7 @@ def main(): meta_args = dict( address=dict(required=False, default=None), + allow_service=dict(type='list', default=None), name=dict(required=True), netmask=dict(required=False, default=None), traffic_group=dict(required=False, default=None), From a2d3aac8ccd0599d4f89aaa3779710993d2641e2 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Wed, 31 Aug 2016 16:15:38 +0200 Subject: [PATCH 2046/2522] Use six for iteration, to make it run on python3 (#2800) --- system/timezone.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/system/timezone.py b/system/timezone.py index e5337bfd9c8..ccf7a5829aa 100644 --- a/system/timezone.py +++ b/system/timezone.py @@ -20,6 +20,7 @@ import os import re +from ansible.module_utils.six import iteritems, iterkeys DOCUMENTATION = ''' --- @@ -110,7 +111,7 @@ def __init__(self, module): # Initially there's only info of "planned" phase, but the # `self.check()` function will fill out it. self.value = dict() - for key in module.argument_spec.iterkeys(): + for key in iterkeys(module.argument_spec): value = module.params[key] if value is not None: self.value[key] = dict(planned=value) @@ -162,7 +163,7 @@ def diff(self, phase1='before', phase2='after'): `--diff` option of ansible-playbook. """ diff = {phase1: {}, phase2: {}} - for key, value in self.value.iteritems(): + for key, value in iteritems(self.value): diff[phase1][key] = value[phase1] diff[phase2][key] = value[phase2] return diff @@ -178,12 +179,12 @@ def check(self, phase): """ if phase == 'planned': return - for key, value in self.value.iteritems(): + for key, value in iteritems(self.value): value[phase] = self.get(key, phase) def change(self): """Make the changes effect based on `self.value`.""" - for key, value in self.value.iteritems(): + for key, value in iteritems(self.value): if value['before'] != value['planned']: self.set(key, value['planned']) From b576e116f07c5a5bebb71298ff28efb11052864e Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 31 Aug 2016 07:48:10 -0700 Subject: [PATCH 2047/2522] Style cleanups: * Don't use iterkeys * Don't use wildcard imports --- system/timezone.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/system/timezone.py b/system/timezone.py index ccf7a5829aa..9c616d6123c 100644 --- a/system/timezone.py +++ b/system/timezone.py @@ -20,7 +20,9 @@ import os import re -from ansible.module_utils.six import iteritems, iterkeys +from ansible.module_utils.basic import AnsibleModule, get_platform +from ansible.module_utils.six import iteritems + DOCUMENTATION = ''' --- @@ -111,7 +113,7 @@ def __init__(self, module): # Initially there's only info of "planned" phase, but the # `self.check()` function will fill out it. self.value = dict() - for key in iterkeys(module.argument_spec): + for key in module.argument_spec: value = module.params[key] if value is not None: self.value[key] = dict(planned=value) @@ -456,7 +458,5 @@ def main(): module.exit_json(changed=changed, diff=diff) -from ansible.module_utils.basic import * - if __name__ == '__main__': main() From 3867cc71a68b177660a0d2fd5e0f516ef75b0d7f Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Wed, 31 Aug 2016 10:29:41 -0500 Subject: [PATCH 2048/2522] Make sure we don't catch rc=0 as a timeout (#2823) --- commands/expect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/expect.py b/commands/expect.py index 4b5e5e8d623..a3d1b32d719 100644 --- a/commands/expect.py +++ b/commands/expect.py @@ -224,7 +224,7 @@ def main(): changed=True, ) - if rc: + if rc is not None: module.exit_json(**ret) else: ret['msg'] = 'command exceeded timeout' From ae5852449cee04028d6c6c43b2b77d4e1af880eb Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 31 Aug 2016 14:41:12 -0500 Subject: [PATCH 2049/2522] Error if shade is too old for domain_id on os_project (#2806) * Error if shade is too old for domain_id on os_project os_project's domain_id parameter requires shade >= 1.8.1 to work. Be explicit. Fixes #2805 os_project requires python-shade 1.8.1 or higher * What I really meant was 1.8.0 --- cloud/openstack/os_project.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cloud/openstack/os_project.py b/cloud/openstack/os_project.py index eeaa101e660..4c686724c89 100644 --- a/cloud/openstack/os_project.py +++ b/cloud/openstack/os_project.py @@ -21,6 +21,8 @@ except ImportError: HAS_SHADE = False +from distutils.version import StrictVersion + DOCUMENTATION = ''' --- module: os_project @@ -46,7 +48,8 @@ default: None domain_id: description: - - Domain id to create the project in if the cloud supports domains + - Domain id to create the project in if the cloud supports domains. + The domain_id parameter requires shade >= 1.8.0 required: false default: None aliases: ['domain'] @@ -160,6 +163,9 @@ def main(): enabled = module.params['enabled'] state = module.params['state'] + if domain and StrictVersion(shade.__version__) < StrictVersion('1.8.0'): + module.fail_json(msg="The domain argument requires shade >=1.8.0") + try: if domain: opcloud = shade.operator_cloud(**module.params) From aedaca55ac02d7394c94918170d5c32f013c0938 Mon Sep 17 00:00:00 2001 From: Ryan Brown Date: Wed, 31 Aug 2016 15:42:15 -0400 Subject: [PATCH 2050/2522] New module: execute_lambda (AWS) (#2558) First version of execute_lambda module Supports: - Synchronous or asynchronous invocation - Tailing log of execution (sync execution only) - check mode --- cloud/amazon/execute_lambda.py | 281 +++++++++++++++++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 cloud/amazon/execute_lambda.py diff --git a/cloud/amazon/execute_lambda.py b/cloud/amazon/execute_lambda.py new file mode 100644 index 00000000000..bd1b9288e2d --- /dev/null +++ b/cloud/amazon/execute_lambda.py @@ -0,0 +1,281 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: execute_lambda +short_description: Execute an AWS Lambda function +description: + - This module executes AWS Lambda functions, allowing synchronous and asynchronous + invocation. +version_added: "2.2" +extends_documentation_fragment: + - aws +author: "Ryan Scott Brown (@ryansb) " +requirements: + - python >= 2.6 + - boto3 +notes: + - Async invocation will always return an empty C(output) key. + - Synchronous invocation may result in a function timeout, resulting in an + empty C(output) key. +options: + name: + description: + - The name of the function to be invoked. This can only be used for + invocations within the calling account. To invoke a function in another + account, use I(function_arn) to specify the full ARN. + required: false + default: None + function_arn: + description: + - The name of the function to be invoked + required: false + default: None + tail_log: + description: + - If C(tail_log=true), the result of the task will include the last 4 KB + of the CloudWatch log for the function execution. Log tailing only + works if you use synchronous invocation C(wait=true). This is usually + used for development or testing Lambdas. + required: false + default: false + wait: + description: + - Whether to wait for the function results or not. If I(wait) is false, + the task will not return any results. To wait for the Lambda function + to complete, set C(wait=true) and the result will be available in the + I(output) key. + required: false + default: true + dry_run: + description: + - Do not *actually* invoke the function. A C(DryRun) call will check that + the caller has permissions to call the function, especially for + checking cross-account permissions. + required: false + default: False + version_qualifier: + description: + - Which version/alias of the function to run. This defaults to the + C(LATEST) revision, but can be set to any existing version or alias. + See https;//docs.aws.amazon.com/lambda/latest/dg/versioning-aliases.html + for details. + required: false + default: LATEST + payload: + description: + - A dictionary in any form to be provided as input to the Lambda function. + required: false + default: {} +''' + +EXAMPLES = ''' +- execute_lambda: + name: test-function + # the payload is automatically serialized and sent to the function + payload: + foo: bar + value: 8 + register: response + +# Test that you have sufficient permissions to execute a Lambda function in +# another account +- execute_lambda: + function_arn: arn:aws:lambda:us-east-1:123456789012:function/some-function + dry_run: true + +- execute_lambda: + name: test-function + payload: + foo: bar + value: 8 + wait: true + tail_log: true + register: response + # the response will have a `logs` key that will contain a log (up to 4KB) of the function execution in Lambda. + +- execute_lambda: name=test-function version_qualifier=PRODUCTION +''' + +RETURN = ''' +output: + description: Function output if wait=true and the function returns a value + returned: success + type: dict + sample: "{ 'output': 'something' }" +logs: + description: The last 4KB of the function logs. Only provided if I(tail_log) is true + type: string +status: + description: C(StatusCode) of API call exit (200 for synchronous invokes, 202 for async) + type: int + sample: 200 +''' + +import base64 +import json +import traceback + +try: + import boto3 + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + name = dict(), + function_arn = dict(), + wait = dict(choices=BOOLEANS, default=True, type='bool'), + tail_log = dict(choices=BOOLEANS, default=False, type='bool'), + dry_run = dict(choices=BOOLEANS, default=False, type='bool'), + version_qualifier = dict(), + payload = dict(default={}, type='dict'), + )) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + mutually_exclusive=[ + ['name', 'function_arn'], + ] + ) + + if not HAS_BOTO3: + module.fail_json(msg='boto3 required for this module') + + name = module.params.get('name') + function_arn = module.params.get('function_arn') + await_return = module.params.get('wait') + dry_run = module.params.get('dry_run') + tail_log = module.params.get('tail_log') + version_qualifier = module.params.get('version_qualifier') + payload = module.params.get('payload') + + if not HAS_BOTO3: + module.fail_json(msg='Python module "boto3" is missing, please install it') + + if not (name or function_arn): + module.fail_json(msg="Must provide either a function_arn or a name to invoke.") + + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=HAS_BOTO3) + if not region: + module.fail_json(msg="The AWS region must be specified as an " + "environment variable or in the AWS credentials " + "profile.") + + try: + client = boto3_conn(module, conn_type='client', resource='lambda', + region=region, endpoint=ec2_url, **aws_connect_kwargs) + except (botocore.exceptions.ClientError, botocore.exceptions.ValidationError) as e: + module.fail_json(msg="Failure connecting boto3 to AWS", exception=traceback.format_exc(e)) + + invoke_params = {} + + if await_return: + # await response + invoke_params['InvocationType'] = 'RequestResponse' + else: + # fire and forget + invoke_params['InvocationType'] = 'Event' + if dry_run or module.check_mode: + # dry_run overrides invocation type + invoke_params['InvocationType'] = 'DryRun' + + if tail_log and await_return: + invoke_params['LogType'] = 'Tail' + elif tail_log and not await_return: + module.fail_json(msg="The `tail_log` parameter is only available if " + "the invocation waits for the function to complete. " + "Set `wait` to true or turn off `tail_log`.") + else: + invoke_params['LogType'] = 'None' + + if version_qualifier: + invoke_params['Qualifier'] = version_qualifier + + if payload: + invoke_params['Payload'] = json.dumps(payload) + + if function_arn: + invoke_params['FunctionName'] = function_arn + elif name: + invoke_params['FunctionName'] = name + + try: + response = client.invoke(**invoke_params) + except botocore.exceptions.ClientError as ce: + if ce.response['Error']['Code'] == 'ResourceNotFoundException': + module.fail_json(msg="Could not find Lambda to execute. Make sure " + "the ARN is correct and your profile has " + "permissions to execute this function.", + exception=traceback.format_exc(ce)) + module.fail_json("Client-side error when invoking Lambda, check inputs and specific error", + exception=traceback.format_exc(ce)) + except botocore.exceptions.ParamValidationError as ve: + module.fail_json(msg="Parameters to `invoke` failed to validate", + exception=traceback.format_exc(ve)) + except Exception as e: + module.fail_json(msg="Unexpected failure while invoking Lambda function", + exception=traceback.format_exc(e)) + + results ={ + 'logs': '', + 'status': response['StatusCode'], + 'output': '', + } + + if response.get('LogResult'): + try: + # logs are base64 encoded in the API response + results['logs'] = base64.b64decode(response.get('LogResult', '')) + except Exception as e: + module.fail_json(msg="Failed while decoding logs", exception=traceback.format_exc(e)) + + if invoke_params['InvocationType'] == 'RequestResponse': + try: + results['output'] = json.loads(response['Payload'].read()) + except Exception as e: + module.fail_json(msg="Failed while decoding function return value", exception=traceback.format_exc(e)) + + if isinstance(results.get('output'), dict) and any( + [results['output'].get('stackTrace'), results['output'].get('errorMessage')]): + # AWS sends back stack traces and error messages when a function failed + # in a RequestResponse (synchronous) context. + template = ("Function executed, but there was an error in the Lambda function. " + "Message: {errmsg}, Type: {type}, Stack Trace: {trace}") + error_data = { + # format the stacktrace sent back as an array into a multiline string + 'trace': '\n'.join( + [' '.join([ + str(x) for x in line # cast line numbers to strings + ]) for line in results.get('output', {}).get('stackTrace', [])] + ), + 'errmsg': results['output'].get('errorMessage'), + 'type': results['output'].get('errorType') + } + module.fail_json(msg=template.format(**error_data), result=results) + + module.exit_json(changed=True, result=results) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From e8a5442345b4702816814f86a4264805c1ec7753 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Wed, 31 Aug 2016 17:04:25 -0700 Subject: [PATCH 2051/2522] Add python3 testing for module PRs. (#2825) --- shippable.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shippable.yml b/shippable.yml index c182c149488..c392bfa675f 100644 --- a/shippable.yml +++ b/shippable.yml @@ -17,6 +17,8 @@ matrix: - env: TEST=integration IMAGE=ansible/ansible:ubuntu1404 PRIVILEGED=true - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604 + - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604py3 PYTHON3=1 + - env: TEST=integration PLATFORM=windows VERSION=2008-SP2 - env: TEST=integration PLATFORM=windows VERSION=2008-R2_SP1 - env: TEST=integration PLATFORM=windows VERSION=2012-RTM From f13d376f0cd8eb863d41c6c0ade99d20ea268d11 Mon Sep 17 00:00:00 2001 From: John R Barker Date: Thu, 1 Sep 2016 20:46:08 +0100 Subject: [PATCH 2052/2522] Minor tidyup (#2828) --- cloud/amazon/kinesis_stream.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/cloud/amazon/kinesis_stream.py b/cloud/amazon/kinesis_stream.py index 1ba25e69860..37f20f8c113 100644 --- a/cloud/amazon/kinesis_stream.py +++ b/cloud/amazon/kinesis_stream.py @@ -51,12 +51,12 @@ choices: [ 'present', 'absent' ] wait: description: - - Wait for operation to complete before returning + - Wait for operation to complete before returning. required: false default: true wait_timeout: description: - - How many seconds to wait for an operation to complete before timing out + - How many seconds to wait for an operation to complete before timing out. required: false default: 300 tags: @@ -158,6 +158,7 @@ import time from functools import reduce + def convert_to_lower(data): """Convert all uppercase keys in dict with lowercase_ Args: @@ -195,6 +196,7 @@ def convert_to_lower(data): results[key] = val return results + def make_tags_in_proper_format(tags): """Take a dictionary of tags and convert them into the AWS Tags format. Args: @@ -216,6 +218,7 @@ def make_tags_in_proper_format(tags): return formatted_tags + def make_tags_in_aws_format(tags): """Take a dictionary of tags and convert them into the AWS Tags format. Args: @@ -247,6 +250,7 @@ def make_tags_in_aws_format(tags): return formatted_tags + def get_tags(client, stream_name, check_mode=False): """Retrieve the tags for a Kinesis Stream. Args: @@ -289,6 +293,7 @@ def get_tags(client, stream_name, check_mode=False): return success, err_msg, results + def find_stream(client, stream_name, check_mode=False): """Retrieve a Kinesis Stream. Args: @@ -338,6 +343,7 @@ def find_stream(client, stream_name, check_mode=False): return success, err_msg, results + def wait_for_status(client, stream_name, status, wait_timeout=300, check_mode=False): """Wait for the the status to change for a Kinesis Stream. @@ -398,6 +404,7 @@ def wait_for_status(client, stream_name, status, wait_timeout=300, return status_achieved, err_msg, stream + def tags_action(client, stream_name, tags, action='create', check_mode=False): """Create or delete multiple tags from a Kinesis Stream. Args: @@ -451,6 +458,7 @@ def tags_action(client, stream_name, tags, action='create', check_mode=False): return success, err_msg + def recreate_tags_from_list(list_of_tags): """Recreate tags from a list of tuples into the Amazon Tag format. Args: @@ -483,6 +491,7 @@ def recreate_tags_from_list(list_of_tags): ) return tags + def update_tags(client, stream_name, tags, check_mode=False): """Update tags for an amazon resource. Args: @@ -562,6 +571,7 @@ def update_tags(client, stream_name, tags, check_mode=False): return success, changed, err_msg + def stream_action(client, stream_name, shard_count=1, action='create', timeout=300, check_mode=False): """Create or Delete an Amazon Kinesis Stream. @@ -615,6 +625,7 @@ def stream_action(client, stream_name, shard_count=1, action='create', return success, err_msg + def retention_action(client, stream_name, retention_period=24, action='increase', check_mode=False): """Increase or Decreaste the retention of messages in the Kinesis stream. @@ -678,6 +689,7 @@ def retention_action(client, stream_name, retention_period=24, return success, err_msg + def update(client, current_stream, stream_name, retention_period=None, tags=None, wait=False, wait_timeout=300, check_mode=False): """Update an Amazon Kinesis Stream. @@ -805,6 +817,7 @@ def update(client, current_stream, stream_name, retention_period=None, return success, changed, err_msg + def create_stream(client, stream_name, number_of_shards=1, retention_period=None, tags=None, wait=False, wait_timeout=300, check_mode=False): """Create an Amazon Kinesis Stream. @@ -941,6 +954,7 @@ def create_stream(client, stream_name, number_of_shards=1, retention_period=None return success, changed, err_msg, results + def delete_stream(client, stream_name, wait=False, wait_timeout=300, check_mode=False): """Delete an Amazon Kinesis Stream. @@ -1001,17 +1015,18 @@ def delete_stream(client, stream_name, wait=False, wait_timeout=300, return success, changed, err_msg, results + def main(): argument_spec = ec2_argument_spec() argument_spec.update( dict( - name = dict(default=None, required=True), - shards = dict(default=None, required=False, type='int'), - retention_period = dict(default=None, required=False, type='int'), - tags = dict(default=None, required=False, type='dict', aliases=['resource_tags']), - wait = dict(default=True, required=False, type='bool'), - wait_timeout = dict(default=300, required=False, type='int'), - state = dict(default='present', choices=['present', 'absent']), + name=dict(default=None, required=True), + shards=dict(default=None, required=False, type='int'), + retention_period=dict(default=None, required=False, type='int'), + tags=dict(default=None, required=False, type='dict', aliases=['resource_tags']), + wait=dict(default=True, required=False, type='bool'), + wait_timeout=dict(default=300, required=False, type='int'), + state=dict(default='present', choices=['present', 'absent']), ) ) module = AnsibleModule( From f38fbaefc6b255a61c9f081bf987e6e7f3a7d9bd Mon Sep 17 00:00:00 2001 From: Michael Grandjean Date: Thu, 1 Sep 2016 22:47:38 +0200 Subject: [PATCH 2053/2522] Fix typo in EXAMPLE section (#2833) --- univention/udm_share.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/univention/udm_share.py b/univention/udm_share.py index 8831c3d79f3..fa8639958ea 100644 --- a/univention/udm_share.py +++ b/univention/udm_share.py @@ -375,7 +375,7 @@ EXAMPLES = ''' # Create a share named home on the server ucs.example.com with the path /home. -- udm_sahre: name=home +- udm_share: name=home path=/home host=ucs.example.com sambaName=Home From 64879e5eb5e31f9b32045893170563dd8d83d9e2 Mon Sep 17 00:00:00 2001 From: Robin Roth Date: Fri, 2 Sep 2016 08:08:08 +0300 Subject: [PATCH 2054/2522] Ensure the return value for changed is bool (#2830) fixes #2827 --- packaging/os/zypper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/os/zypper.py b/packaging/os/zypper.py index f958a44da4f..9d90dbe044c 100644 --- a/packaging/os/zypper.py +++ b/packaging/os/zypper.py @@ -427,7 +427,7 @@ def main(): elif state in ['installed', 'present', 'latest']: packages_changed, retvals = package_present(module, name, state == 'latest') - retvals['changed'] = retvals['rc'] == 0 and packages_changed + retvals['changed'] = retvals['rc'] == 0 and bool(packages_changed) if module._diff: set_diff(module, retvals, packages_changed) From ab7391ff14ae2b8eddc4bd35c61eec6456867711 Mon Sep 17 00:00:00 2001 From: Kevin Hulquest Date: Fri, 2 Sep 2016 08:34:04 -0600 Subject: [PATCH 2055/2522] Add facts and storage system modules. (#2748) * Add facts and storage system modules. * Update version metadata. * Add facts and storage system modules. Update version metadata. Add init files. * Fixes for review comments. * Fixes for review comments. * Update document fragments for api_* options to indicate they are required. * Correct bad string concatenation. * Add option description for validate_certs since basic_auth arg_spec is no longer extended. * Add default value for validate_certs in docstring. * Rename directory name so it applies to netapp and not simply eseries platform. * Rename modules to differentiate other netapp modules. --- storage/__init__.py | 0 storage/netapp/__init__.py | 0 storage/netapp/netapp_e_facts.py | 205 ++++++++++++++ storage/netapp/netapp_e_storage_system.py | 309 ++++++++++++++++++++++ 4 files changed, 514 insertions(+) create mode 100644 storage/__init__.py create mode 100644 storage/netapp/__init__.py create mode 100644 storage/netapp/netapp_e_facts.py create mode 100644 storage/netapp/netapp_e_storage_system.py diff --git a/storage/__init__.py b/storage/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/storage/netapp/__init__.py b/storage/netapp/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/storage/netapp/netapp_e_facts.py b/storage/netapp/netapp_e_facts.py new file mode 100644 index 00000000000..514002b9d38 --- /dev/null +++ b/storage/netapp/netapp_e_facts.py @@ -0,0 +1,205 @@ +#!/usr/bin/python + +# (c) 2016, NetApp, Inc +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +DOCUMENTATION = ''' +module: na_eseries_facts +version_added: '2.2' +short_description: Get facts about NetApp E-Series arrays +options: + api_username: + required: true + description: + - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_password: + required: true + description: + - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_url: + required: true + description: + - The url to the SANtricity WebServices Proxy or embedded REST API. + example: + - https://prod-1.wahoo.acme.com/devmgr/v2 + validate_certs: + required: false + default: true + description: + - Should https certificates be validated? + ssid: + required: true + description: + - The ID of the array to manage. This value must be unique for each array. + +description: + - Return various information about NetApp E-Series storage arrays (eg, configuration, disks) + +author: Kevin Hulquest (@hulquest) +''' + +EXAMPLES = """ +--- + - name: Get array facts + na_eseries_facts: + array_id: "{{ netapp_array_id }}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" +""" + +RETURN = """ +msg: Gathered facts for . +""" +import json + +import os + +from ansible.module_utils.api import basic_auth_argument_spec +from ansible.module_utils.basic import AnsibleModule, get_exception +from ansible.module_utils.urls import open_url +from ansible.module_utils.six.moves.urllib.error import HTTPError + + +def request(url, data=None, headers=None, method='GET', use_proxy=True, + force=False, last_mod_time=None, timeout=10, validate_certs=True, + url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False): + try: + r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy, + force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs, + url_username=url_username, url_password=url_password, http_agent=http_agent, + force_basic_auth=force_basic_auth) + except HTTPError: + err = get_exception() + r = err.fp + + try: + raw_data = r.read() + if raw_data: + data = json.loads(raw_data) + else: + data = None + except: + if ignore_errors: + pass + else: + raise + + resp_code = r.getcode() + + if resp_code >= 400 and not ignore_errors: + raise Exception(resp_code, data) + else: + return resp_code, data + + +def main(): + argument_spec = basic_auth_argument_spec() + argument_spec.update( + api_username=dict(type='str', required=True), + api_password=dict(type='str', required=True, no_log=True), + api_url=dict(type='str', required=True), + ssid=dict(required=True)) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True + ) + + p = module.params + + ssid = p['ssid'] + validate_certs = p['validate_certs'] + + api_usr = p['api_username'] + api_pwd = p['api_password'] + api_url = p['api_url'] + + facts = dict(ssid=ssid) + + # fetch the list of storage-pool objects and look for one with a matching name + try: + (rc, resp) = request(api_url + "/storage-systems/%s/graph" % ssid, + headers=dict(Accept="application/json"), + url_username=api_usr, url_password=api_pwd, validate_certs=validate_certs) + except: + error = get_exception() + module.fail_json( + msg="Failed to obtain facts from storage array with id [%s]. Error [%s]" % (ssid, str(error))) + + facts['snapshot_images'] = [ + dict( + id=d['id'], + status=d['status'], + pit_capacity=d['pitCapacity'], + creation_method=d['creationMethod'], + reposity_cap_utilization=d['repositoryCapacityUtilization'], + active_cow=d['activeCOW'], + rollback_source=d['isRollbackSource'] + ) for d in resp['highLevelVolBundle']['pit']] + + facts['netapp_disks'] = [ + dict( + id=d['id'], + available=d['available'], + media_type=d['driveMediaType'], + status=d['status'], + usable_bytes=d['usableCapacity'], + tray_ref=d['physicalLocation']['trayRef'], + product_id=d['productID'], + firmware_version=d['firmwareVersion'], + serial_number=d['serialNumber'].lstrip() + ) for d in resp['drive']] + + facts['netapp_storage_pools'] = [ + dict( + id=sp['id'], + name=sp['name'], + available_capacity=sp['freeSpace'], + total_capacity=sp['totalRaidedSpace'], + used_capacity=sp['usedSpace'] + ) for sp in resp['volumeGroup'] + ] + + all_volumes = list(resp['volume']) + # all_volumes.extend(resp['thinVolume']) + + # TODO: exclude thin-volume repo volumes (how to ID?) + facts['netapp_volumes'] = [ + dict( + id=v['id'], + name=v['name'], + parent_storage_pool_id=v['volumeGroupRef'], + capacity=v['capacity'], + is_thin_provisioned=v['thinProvisioned'] + ) for v in all_volumes + ] + + features = [f for f in resp['sa']['capabilities']] + features.extend([f['capability'] for f in resp['sa']['premiumFeatures'] if f['isEnabled']]) + features = list(set(features)) # ensure unique + features.sort() + facts['netapp_enabled_features'] = features + + # TODO: include other details about the storage pool (size, type, id, etc) + result = dict(ansible_facts=facts, changed=False) + module.exit_json(msg="Gathered facts for %s." % ssid, **result) + + +if __name__ == "__main__": + main() diff --git a/storage/netapp/netapp_e_storage_system.py b/storage/netapp/netapp_e_storage_system.py new file mode 100644 index 00000000000..13ef7c9fbc5 --- /dev/null +++ b/storage/netapp/netapp_e_storage_system.py @@ -0,0 +1,309 @@ +#!/usr/bin/python + +# (c) 2016, NetApp, Inc +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +DOCUMENTATION = ''' +module: na_eseries_storage_system +version_added: "2.2" +short_description: Add/remove arrays from the Web Services Proxy +description: +- Manage the arrays accessible via a NetApp Web Services Proxy for NetApp E-series storage arrays. +options: + api_username: + required: true + description: + - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_password: + required: true + description: + - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_url: + required: true + description: + - The url to the SANtricity WebServices Proxy or embedded REST API. + example: + - https://prod-1.wahoo.acme.com/devmgr/v2 + validate_certs: + required: false + default: true + description: + - Should https certificates be validated? + ssid: + required: true + description: + - The ID of the array to manage. This value must be unique for each array. + state: + required: true + description: + - Whether the specified array should be configured on the Web Services Proxy or not. + choices: ['present', 'absent'] + controller_addresses: + required: true + description: + - The list addresses for the out-of-band management adapter or the agent host. Mutually exclusive of array_wwn parameter. + array_wwn: + required: false + description: + - The WWN of the array to manage. Only necessary if in-band managing multiple arrays on the same agent host. Mutually exclusive of controller_addresses parameter. + array_password: + required: false + description: + - The management password of the array to manage, if set. + enable_trace: + required: false + default: false + description: + - Enable trace logging for SYMbol calls to the storage system. + meta_tags: + required: false + default: None + description: + - Optional meta tags to associate to this storage system +author: Kevin Hulquest (@hulquest) +''' + +EXAMPLES = ''' +--- + - name: Presence of storage system + na_eseries_storage_system: + ssid: "{{ item.key }}" + state: present + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" + controller_addresses: + - "{{ item.value.address1 }}" + - "{{ item.value.address2 }}" + with_dict: "{{ storage_systems }}" + when: check_storage_system +''' + +RETURN = ''' +msg: Storage system removed. +msg: Storage system added. +''' +import json +import os +from datetime import datetime as dt, timedelta, time +from time import sleep + +from ansible.module_utils.api import basic_auth_argument_spec +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import open_url +from ansible.module_utils.six.moves.urllib.error import HTTPError + + +def request(url, data=None, headers=None, method='GET', use_proxy=True, + force=False, last_mod_time=None, timeout=10, validate_certs=True, + url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False): + try: + r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy, + force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs, + url_username=url_username, url_password=url_password, http_agent=http_agent, + force_basic_auth=force_basic_auth) + except HTTPError: + err = get_exception() + r = err.fp + + try: + raw_data = r.read() + if raw_data: + data = json.loads(raw_data) + else: + raw_data = None + except: + if ignore_errors: + pass + else: + raise Exception(raw_data) + + resp_code = r.getcode() + + if resp_code >= 400 and not ignore_errors: + raise Exception(resp_code, data) + else: + return resp_code, data + + +def do_post(ssid, api_url, post_headers, api_usr, api_pwd, validate_certs, request_body, timeout): + (rc, resp) = request(api_url + "/storage-systems", data=request_body, headers=post_headers, + method='POST', url_username=api_usr, url_password=api_pwd, + validate_certs=validate_certs) + status = None + return_resp = resp + if 'status' in resp: + status = resp['status'] + + if rc == 201: + status = 'neverContacted' + fail_after_time = dt.utcnow() + timedelta(seconds=timeout) + + while status == 'neverContacted': + if dt.utcnow() > fail_after_time: + raise Exception("web proxy timed out waiting for array status") + + sleep(1) + (rc, system_resp) = request(api_url + "/storage-systems/%s" % ssid, + headers=dict(Accept="application/json"), url_username=api_usr, + url_password=api_pwd, validate_certs=validate_certs, + ignore_errors=True) + status = system_resp['status'] + return_resp = system_resp + + return status, return_resp + + +def main(): + argument_spec = basic_auth_argument_spec() + argument_spec.update(dict( + state=dict(required=True, choices=['present', 'absent']), + ssid=dict(required=True, type='str'), + controller_addresses=dict(type='list'), + array_wwn=dict(required=False, type='str'), + array_password=dict(required=False, type='str', no_log=True), + array_status_timeout_sec=dict(default=60, type='int'), + enable_trace=dict(default=False, type='bool'), + meta_tags=dict(type='list') + )) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + mutually_exclusive=[['controller_addresses', 'array_wwn']], + required_if=[('state', 'present', ['controller_addresses'])] + ) + + p = module.params + + state = p['state'] + ssid = p['ssid'] + controller_addresses = p['controller_addresses'] + array_wwn = p['array_wwn'] + array_password = p['array_password'] + array_status_timeout_sec = p['array_status_timeout_sec'] + validate_certs = p['validate_certs'] + meta_tags = p['meta_tags'] + enable_trace = p['enable_trace'] + + api_usr = p['api_username'] + api_pwd = p['api_password'] + api_url = p['api_url'] + + changed = False + array_exists = False + + try: + (rc, resp) = request(api_url + "/storage-systems/%s" % ssid, headers=dict(Accept="application/json"), + url_username=api_usr, url_password=api_pwd, validate_certs=validate_certs, + ignore_errors=True) + except: + err = get_exception() + module.fail_json(msg="Error accessing storage-system with id [%s]. Error [%s]" % (ssid, str(err))) + + array_exists = True + array_detail = resp + + if rc == 200: + if state == 'absent': + changed = True + array_exists = False + elif state == 'present': + current_addresses = frozenset(i for i in (array_detail['ip1'], array_detail['ip2']) if i) + if set(controller_addresses) != current_addresses: + changed = True + if array_detail['wwn'] != array_wwn and array_wwn is not None: + module.fail_json( + msg='It seems you may have specified a bad WWN. The storage system ID you specified, %s, currently has the WWN of %s' % (ssid, array_detail['wwn'])) + elif rc == 404: + if state == 'present': + changed = True + array_exists = False + else: + changed = False + module.exit_json(changed=changed, msg="Storage system was not present.") + + if changed and not module.check_mode: + if state == 'present': + if not array_exists: + # add the array + array_add_req = dict( + id=ssid, + controllerAddresses=controller_addresses, + metaTags=meta_tags, + enableTrace=enable_trace + ) + + if array_wwn: + array_add_req['wwn'] = array_wwn + + if array_password: + array_add_req['password'] = array_password + + post_headers = dict(Accept="application/json") + post_headers['Content-Type'] = 'application/json' + request_data = json.dumps(array_add_req) + + try: + (rc, resp) = do_post(ssid, api_url, post_headers, api_usr, api_pwd, validate_certs, request_data, + array_status_timeout_sec) + except: + err = get_exception() + module.fail_json(msg="Failed to add storage system. Id[%s]. Request body [%s]. Error[%s]." % + (ssid, request_data, str(err))) + + + else: # array exists, modify... + post_headers = dict(Accept="application/json") + post_headers['Content-Type'] = 'application/json' + post_body = dict( + controllerAddresses=controller_addresses, + removeAllTags=True, + enableTrace=enable_trace, + metaTags=meta_tags + ) + + try: + (rc, resp) = do_post(ssid, api_url, post_headers, api_usr, api_pwd, validate_certs, post_body, + array_status_timeout_sec) + except: + err = get_exception() + module.fail_json(msg="Failed to update storage system. Id[%s]. Request body [%s]. Error[%s]." % + (ssid, post_body, str(err))) + + + elif state == 'absent': + # delete the array + try: + (rc, resp) = request(api_url + "/storage-systems/%s" % ssid, method='DELETE', + url_username=api_usr, + url_password=api_pwd, validate_certs=validate_certs) + except: + err = get_exception() + module.fail_json(msg="Failed to remove storage array. Id[%s]. Error[%s]." % (ssid, str(err))) + + if rc == 422: + module.exit_json(changed=changed, msg="Storage system was not presnt.") + if rc == 204: + module.exit_json(changed=changed, msg="Storage system removed.") + + module.exit_json(changed=changed, **resp) + + +if __name__ == '__main__': + main() From e81bbf9b8f117dd5b0996adfbf981f68aa9ddf42 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Fri, 2 Sep 2016 23:07:32 -0700 Subject: [PATCH 2056/2522] Bugfix bigip_facts that was trying to check the length of an iterator (#2842) Recently, a user reported that the bigip_facts module was failing with the error received exception: object of type 'itertools.imap' has no len() This reported was occurring at line 1657 of the bigip_facts module bug report is here https://github.com/F5Networks/f5-ansible/issues/25 Upon further investigation, the map function for returning the specified includes was returning an iterator, and calling len() on an iterator does not work. I believe this problem was caused by part of the Python 3.x effort insofar as the inclusion of this line https://github.com/ansible/ansible/blob/devel/lib/ansible/module_utils/basic.py#L143 seems to affect our usage of map(), probably for the better anyway, and we need to change our expectations in our module's code to no longer assume a list, but instead assume an iterator. After trawling through the module_utils/basic code, I think a list comprehension is more appropriate here anyway, so I'm changing it to be that. The affected user reported it works this way, and my own testing on 2.2.0 supports that. --- network/f5/bigip_facts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/f5/bigip_facts.py b/network/f5/bigip_facts.py index ae5c97d351b..2189ded2e29 100644 --- a/network/f5/bigip_facts.py +++ b/network/f5/bigip_facts.py @@ -1641,7 +1641,7 @@ def main(): regex = fnmatch.translate(fact_filter) else: regex = None - include = map(lambda x: x.lower(), module.params['include']) + include = [x.lower() for x in module.params['include']] valid_includes = ('address_class', 'certificate', 'client_ssl_profile', 'device', 'device_group', 'interface', 'key', 'node', 'pool', 'provision', 'rule', 'self_ip', 'software', From 9278cce7d2755786a598cbc655c7df4440a2f68d Mon Sep 17 00:00:00 2001 From: EarlAbides Date: Sat, 3 Sep 2016 03:23:09 -0400 Subject: [PATCH 2057/2522] Fix ec2_asg_facts module when using name parameter (#2840) (#2841) --- cloud/amazon/ec2_asg_facts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_asg_facts.py b/cloud/amazon/ec2_asg_facts.py index 631677f1381..857d0c20a0b 100644 --- a/cloud/amazon/ec2_asg_facts.py +++ b/cloud/amazon/ec2_asg_facts.py @@ -302,7 +302,7 @@ def find_asgs(conn, module, name=None, tags=None): name_prog = re.compile(r'^' + name) for asg in asgs['AutoScalingGroups']: if name: - matched_name = name_prog.search(asg['auto_scaling_group_name']) + matched_name = name_prog.search(asg['AutoScalingGroupName']) else: matched_name = True From 06bd2a5ce2abd0d5fbdce18e7c24a3674b573b46 Mon Sep 17 00:00:00 2001 From: Robin Roth Date: Sun, 4 Sep 2016 08:20:10 +0200 Subject: [PATCH 2058/2522] Zypper repo autoimport keys (#2132) * zypper_repository add auto_import_keys options * also give more output on failure (rc, stdout, stderr) * be more specific in the doc for auto_import_keys * add runrefresh option to zypper_repository * this comes out of ansible/ansible-modules-extras#2411, where AnderEnder adds refresh to the zypper module * adds a way to force zypper to refresh a repository * can be used to refresh independently of auto_import_keys * add option to run name=* runrefresh=yes * name runrefresh to not break existing use to refresh (now alias to autorefresh) * add version_added flag to autorefresh * remove wrong version_added comment --- packaging/os/zypper_repository.py | 117 +++++++++++++++++++++--------- 1 file changed, 83 insertions(+), 34 deletions(-) diff --git a/packaging/os/zypper_repository.py b/packaging/os/zypper_repository.py index aac7d870965..bbed8143f3b 100644 --- a/packaging/os/zypper_repository.py +++ b/packaging/os/zypper_repository.py @@ -59,12 +59,13 @@ required: false default: "no" choices: [ "yes", "no" ] - refresh: + autorefresh: description: - Enable autorefresh of the repository. required: false default: "yes" choices: [ "yes", "no" ] + aliases: [ "refresh" ] priority: description: - Set priority of repository. Packages will always be installed @@ -80,6 +81,26 @@ default: "no" choices: [ "yes", "no" ] version_added: "2.1" + auto_import_keys: + description: + - Automatically import the gpg signing key of the new or changed repository. + - Has an effect only if state is I(present). Has no effect on existing (unchanged) repositories or in combination with I(absent). + - Implies runrefresh. + required: false + default: "no" + choices: ["yes", "no"] + version_added: "2.2" + runrefresh: + description: + - Refresh the package list of the given repository. + - Can be used with repo=* to refresh all repositories. + required: false + default: "no" + choices: ["yes", "no"] + version_added: "2.2" + + + requirements: - "zypper >= 1.0 # included in openSuSE >= 11.1 or SuSE Linux Enterprise Server/Desktop >= 11.0" ''' @@ -93,6 +114,15 @@ # Add python development repository - zypper_repository: repo=http://download.opensuse.org/repositories/devel:/languages:/python/SLE_11_SP3/devel:languages:python.repo + +# Refresh all repos +- zypper_repository: repo=* runrefresh=yes + +# Add a repo and add it's gpg key +- zypper_repository: repo=http://download.opensuse.org/repositories/systemsmanagement/openSUSE_Leap_42.1/ auto_import_keys=yes + +# Force refresh of a repository +- zypper_repository: repo=http://my_internal_ci_repo/repo name=my_ci_repo state=present runrefresh=yes ''' REPO_OPTS = ['alias', 'name', 'priority', 'enabled', 'autorefresh', 'gpgcheck'] @@ -121,14 +151,10 @@ def _parse_repos(module): elif rc == 6: return [] else: - d = { 'zypper_exit_code': rc } - if stderr: - d['stderr'] = stderr - if stdout: - d['stdout'] = stdout - module.fail_json(msg='Failed to execute "%s"' % " ".join(cmd), **d) + module.fail_json(msg='Failed to execute "%s"' % " ".join(cmd), rc=rc, stdout=stdout, stderr=stderr) def _repo_changes(realrepo, repocmp): + "Check whether the 2 given repos have different settings." for k in repocmp: if repocmp[k] and k not in realrepo: return True @@ -144,6 +170,13 @@ def _repo_changes(realrepo, repocmp): return False def repo_exists(module, repodata, overwrite_multiple): + """Check whether the repository already exists. + + returns (exists, mod, old_repos) + exists: whether a matching (name, URL) repo exists + mod: whether there are changes compared to the existing repo + old_repos: list of matching repos + """ existing_repos = _parse_repos(module) # look for repos that have matching alias or url to the one searched @@ -171,7 +204,8 @@ def repo_exists(module, repodata, overwrite_multiple): module.fail_json(msg=errmsg) -def modify_repo(module, repodata, old_repos, zypper_version, warnings): +def addmodify_repo(module, repodata, old_repos, zypper_version, warnings): + "Adds the repo, removes old repos before, that would conflict." repo = repodata['url'] cmd = ['/usr/bin/zypper', 'ar', '--check'] if repodata['name']: @@ -212,24 +246,15 @@ def modify_repo(module, repodata, old_repos, zypper_version, warnings): remove_repo(module, oldrepo['url']) rc, stdout, stderr = module.run_command(cmd, check_rc=False) - changed = rc == 0 - if rc == 0: - changed = True - else: - if stderr: - module.fail_json(msg=stderr) - else: - module.fail_json(msg=stdout) - - return changed + return rc, stdout, stderr def remove_repo(module, repo): + "Removes the repo." cmd = ['/usr/bin/zypper', 'rr', repo] rc, stdout, stderr = module.run_command(cmd, check_rc=True) - changed = rc == 0 - return changed + return rc, stdout, stderr def get_zypper_version(module): @@ -238,14 +263,16 @@ def get_zypper_version(module): return LooseVersion('1.0') return LooseVersion(stdout.split()[1]) +def runrefreshrepo(module, auto_import_keys=False, shortname=None): + "Forces zypper to refresh repo metadata." + cmd = ['/usr/bin/zypper', 'refresh', '--force'] + if auto_import_keys: + cmd.append('--gpg-auto-import-keys') + if shortname is not None: + cmd.extend(['-r', shortname]) -def fail_if_rc_is_null(module, rc, stdout, stderr): - if rc != 0: - #module.fail_json(msg=stderr if stderr else stdout) - if stderr: - module.fail_json(msg=stderr) - else: - module.fail_json(msg=stdout) + rc, stdout, stderr = module.run_command(cmd, check_rc=True) + return rc, stdout, stderr def main(): @@ -254,20 +281,25 @@ def main(): name=dict(required=False), repo=dict(required=False), state=dict(choices=['present', 'absent'], default='present'), + runrefresh=dict(required=False, default='no', type='bool'), description=dict(required=False), disable_gpg_check = dict(required=False, default=False, type='bool'), - refresh = dict(required=False, default=True, type='bool'), + autorefresh = dict(required=False, default=True, type='bool', aliases=['refresh']), priority = dict(required=False, type='int'), enabled = dict(required=False, default=True, type='bool'), overwrite_multiple = dict(required=False, default=False, type='bool'), + auto_import_keys = dict(required=False, default=False, type='bool'), ), supports_check_mode=False, + required_one_of = [['state','runrefresh']], ) repo = module.params['repo'] alias = module.params['name'] state = module.params['state'] overwrite_multiple = module.params['overwrite_multiple'] + auto_import_keys = module.params['auto_import_keys'] + runrefresh = module.params['runrefresh'] zypper_version = get_zypper_version(module) warnings = [] # collect warning messages for final output @@ -287,7 +319,7 @@ def main(): repodata['gpgcheck'] = '0' else: repodata['gpgcheck'] = '1' - if module.params['refresh']: + if module.params['autorefresh']: repodata['autorefresh'] = '1' else: repodata['autorefresh'] = '0' @@ -296,6 +328,13 @@ def exit_unchanged(): module.exit_json(changed=False, repodata=repodata, state=state) # Check run-time module parameters + if repo == '*' or alias == '*': + if runrefresh: + runrefreshrepo(module, auto_import_keys) + module.exit_json(changed=False, runrefresh=True) + else: + module.fail_json(msg='repo=* can only be used with the runrefresh option.') + if state == 'present' and not repo: module.fail_json(msg='Module option state=present requires repo') if state == 'absent' and not repo and not alias: @@ -310,18 +349,28 @@ def exit_unchanged(): exists, mod, old_repos = repo_exists(module, repodata, overwrite_multiple) + if repo: + shortname = repo + else: + shortname = alias + if state == 'present': if exists and not mod: + if runrefresh: + runrefreshrepo(module, auto_import_keys, shortname) exit_unchanged() - changed = modify_repo(module, repodata, old_repos, zypper_version, warnings) + rc, stdout, stderr = addmodify_repo(module, repodata, old_repos, zypper_version, warnings) + if rc == 0 and (runrefresh or auto_import_keys): + runrefreshrepo(module, auto_import_keys, shortname) elif state == 'absent': if not exists: exit_unchanged() - if not repo: - repo=alias - changed = remove_repo(module, repo) + rc, stdout, stderr = remove_repo(module, shortname) - module.exit_json(changed=changed, repodata=repodata, state=state, warnings=warnings) + if rc == 0: + module.exit_json(changed=True, repodata=repodata, state=state, warnings=warnings) + else: + module.fail_json(msg="Zypper failed with rc %s" % rc, rc=rc, stdout=stdout, stderr=stderr, repodata=repodata, state=state, warnings=warnings) # import module snippets from ansible.module_utils.basic import * From e6678a74966010b47ce001cc468f494879bd015c Mon Sep 17 00:00:00 2001 From: Nadir Date: Sun, 4 Sep 2016 08:45:35 +0100 Subject: [PATCH 2059/2522] Added redrive policy options (#2245) --- cloud/amazon/sqs_queue.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/cloud/amazon/sqs_queue.py b/cloud/amazon/sqs_queue.py index a16db036b01..d00a3b638ff 100644 --- a/cloud/amazon/sqs_queue.py +++ b/cloud/amazon/sqs_queue.py @@ -25,6 +25,7 @@ author: - Alan Loi (@loia) - Fernando Jose Pando (@nand0p) + - Nadir Lloret (@nadirollo) requirements: - "boto >= 2.33.0" options: @@ -69,13 +70,19 @@ required: false default: null version_added: "2.1" + redrive_policy: + description: + - json dict with the redrive_policy (see example) + required: false + default: null + version_added: "2.2" extends_documentation_fragment: - aws - ec2 """ EXAMPLES = ''' -# Create SQS queue +# Create SQS queue with redrive policy - sqs_queue: name: my-queue region: ap-southeast-2 @@ -85,6 +92,9 @@ delivery_delay: 30 receive_message_wait_time: 20 policy: "{{ json_dict }}" + redrive_policy: + maxReceiveCount: 5 + deadLetterTargetArn: arn:aws:sqs:eu-west-1:123456789012:my-dead-queue # Delete SQS queue - sqs_queue: @@ -112,6 +122,7 @@ def create_or_update_sqs_queue(connection, module): delivery_delay=module.params.get('delivery_delay'), receive_message_wait_time=module.params.get('receive_message_wait_time'), policy=module.params.get('policy'), + redrive_policy=module.params.get('redrive_policy') ) result = dict( @@ -147,7 +158,8 @@ def update_sqs_queue(queue, maximum_message_size=None, delivery_delay=None, receive_message_wait_time=None, - policy=None): + policy=None, + redrive_policy=None): changed = False changed = set_queue_attribute(queue, 'VisibilityTimeout', default_visibility_timeout, @@ -162,6 +174,8 @@ def update_sqs_queue(queue, check_mode=check_mode) or changed changed = set_queue_attribute(queue, 'Policy', policy, check_mode=check_mode) or changed + changed = set_queue_attribute(queue, 'RedrivePolicy', redrive_policy, + check_mode=check_mode) or changed return changed @@ -175,7 +189,7 @@ def set_queue_attribute(queue, attribute, value, check_mode=False): existing_value = '' # convert dict attributes to JSON strings (sort keys for comparing) - if attribute is 'Policy': + if attribute in ['Policy', 'RedrivePolicy']: value = json.dumps(value, sort_keys=True) if existing_value: existing_value = json.dumps(json.loads(existing_value), sort_keys=True) @@ -224,6 +238,7 @@ def main(): delivery_delay=dict(type='int'), receive_message_wait_time=dict(type='int'), policy=dict(type='dict', required=False), + redrive_policy=dict(type='dict', required=False), )) module = AnsibleModule( From 5c3be5ea286492efff91dcaa6e5cde7571e5f4c5 Mon Sep 17 00:00:00 2001 From: "Thierno IB. BARRY" Date: Sun, 4 Sep 2016 09:47:02 +0200 Subject: [PATCH 2060/2522] elasticsearch_plugin: Fix bug when using proxy (#2603) (#2838) --- packaging/elasticsearch_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/elasticsearch_plugin.py b/packaging/elasticsearch_plugin.py index 7472c033158..d9c05b1ac56 100644 --- a/packaging/elasticsearch_plugin.py +++ b/packaging/elasticsearch_plugin.py @@ -167,7 +167,7 @@ def main(): cmd_args = [plugin_bin, package_state_map[state], name] if proxy_host and proxy_port: - cmd_args.append("-DproxyHost=%s -DproxyPort=%s" % proxy_host, proxy_port) + cmd_args.append("-DproxyHost=%s -DproxyPort=%s" % (proxy_host, proxy_port)) if url: cmd_args.append("--url %s" % url) From 44733e37988578eadb4f03412e0a01d2abad2163 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Sun, 4 Sep 2016 07:46:28 -0400 Subject: [PATCH 2061/2522] roll up of updates to asa_template This updates the asa_template module with updates for Ansible 2.2. * removes get_module() in favor of NetworkModule * fixes up import statements --- network/asa/asa_template.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/network/asa/asa_template.py b/network/asa/asa_template.py index 9644fa71f88..6267da75860 100644 --- a/network/asa/asa_template.py +++ b/network/asa/asa_template.py @@ -19,7 +19,7 @@ --- module: asa_template version_added: "2.2" -author: "Peter Sprygada (@privateip) & Patrick Ogenstad (@ogenstad)" +author: "Peter Sprygada (@privateip), Patrick Ogenstad (@ogenstad)" short_description: Manage Cisco ASA device configurations over SSH description: - Manages Cisco ASA network device configurations over SSH. This module @@ -115,15 +115,15 @@ type: list sample: ['...', '...'] """ - +from ansible.module_utils.netcfg import NetworkConfig, dumps +from ansible.module_utils.asa import NetworkModule, NetworkError def get_config(module): config = module.params['config'] or dict() if not config and not module.params['force']: - config = module.config + config = module.config.get_config() return config - def main(): """ main entry point for module execution """ @@ -138,9 +138,9 @@ def main(): mutually_exclusive = [('config', 'backup'), ('config', 'force')] - module = get_module(argument_spec=argument_spec, - mutually_exclusive=mutually_exclusive, - supports_check_mode=True) + module = NetworkModule(argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) result = dict(changed=False) @@ -149,17 +149,18 @@ def main(): contents = get_config(module) if contents: config = NetworkConfig(contents=contents, indent=1) - result['_backup'] = contents + result['_backup'] = str(contents) if not module.params['force']: commands = candidate.difference(config) + commands = dumps(commands, 'commands').split('\n') + commands = [str(c) for c in commands if c] else: commands = str(candidate).split('\n') if commands: if not module.check_mode: - commands = [str(c).strip() for c in commands] - response = module.configure(commands) + response = module.config(commands) result['responses'] = response result['changed'] = True @@ -167,9 +168,5 @@ def main(): module.exit_json(**result) -from ansible.module_utils.basic import * -from ansible.module_utils.shell import * -from ansible.module_utils.netcfg import * -from ansible.module_utils.asa import * if __name__ == '__main__': main() From db5dd8516c252be682f2e23cf61eb17ea0622a02 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Sun, 4 Sep 2016 08:10:44 -0400 Subject: [PATCH 2062/2522] update asa_config module * removes get_module() factory method for NetworkModule * add src argument to provide path to config file * add new choice to match used to ignore current running config * add update argument with choices merge or check * add backup argument to backup current running config to control host * add defaults argument to control collection of config with or withoutdefaults * add save argument to save current running config to startup config --- network/asa/asa_config.py | 260 ++++++++++++++++++++++++++++---------- 1 file changed, 191 insertions(+), 69 deletions(-) diff --git a/network/asa/asa_config.py b/network/asa/asa_config.py index 7c7d1248b41..36926227e45 100644 --- a/network/asa/asa_config.py +++ b/network/asa/asa_config.py @@ -20,7 +20,7 @@ --- module: asa_config version_added: "2.2" -author: "Peter Sprygada (@privateip) & Patrick Ogenstad (@ogenstad)" +author: "Peter Sprygada (@privateip), Patrick Ogenstad (@ogenstad)" short_description: Manage Cisco ASA configuration sections description: - Cisco ASA configurations use a simple block indent file sytanx @@ -34,9 +34,11 @@ - The ordered set of commands that should be configured in the section. The commands must be the exact same commands as found in the device running-config. Be sure to note the configuration - command syntanx as some commands are automatically modified by the + command syntax as some commands are automatically modified by the device config parser. - required: true + required: false + default: null + aliases: ['commands'] parents: description: - The ordered set of parents that uniquely identify the section @@ -45,6 +47,15 @@ level or global commands. required: false default: null + src: + description: + - Specifies the source path to the file that contains the configuration + or configuration template to load. The path to the source file can + either be the full path on the Ansible control host or a relative + path from the playbook or role root directory. This argument is mutually + exclusive with I(lines). + required: false + default: null before: description: - The ordered set of commands to push on to the command stack if @@ -57,7 +68,7 @@ after: description: - The ordered set of commands to append to the end of the command - stack if a changed needs to be made. Just like with I(before) this + stack if a change needs to be made. Just like with I(before) this allows the playbook designer to append a set of commands to be executed after the command set. required: false @@ -68,11 +79,13 @@ the set of commands against the current device config. If match is set to I(line), commands are matched line by line. If match is set to I(strict), command lines are matched with respect - to position. Finally if match is set to I(exact), command lines - must be an equal match. + to position. If match is set to I(exact), command lines + must be an equal match. Finally, if match is set to I(none), the + module will not attempt to compare the source configuration with + the running configuration on the remote device. required: false default: line - choices: ['line', 'strict', 'exact'] + choices: ['line', 'strict', 'exact', 'none'] replace: description: - Instructs the module on the way to perform the configuration @@ -84,35 +97,84 @@ required: false default: line choices: ['line', 'block'] - force: + update: + description: + - The I(update) argument controls how the configuration statements + are processed on the remote device. Valid choices for the I(update) + argument are I(merge) and I(check). When the argument is set to + I(merge), the configuration changes are merged with the current + device running configuration. When the argument is set to I(check) + the configuration updates are determined but not actually configured + on the remote device. + required: false + default: merge + choices: ['merge', 'check'] + commit: description: - - The force argument instructs the module to not consider the - current devices running-config. When set to true, this will - cause the module to push the contents of I(src) into the device - without first checking if already configured. + - This argument specifies the update method to use when applying the + configuration changes to the remote node. If the value is set to + I(merge) the configuration updates are merged with the running- + config. If the value is set to I(check), no changes are made to + the remote host. required: false - default: false + default: merge + choices: ['merge', 'check'] + backup: + description: + - This argument will cause the module to create a full backup of + the current C(running-config) from the remote device before any + changes are made. The backup file is written to the C(backup) + folder in the playbook root directory. If the directory does not + exist, it is created. + required: false + default: no choices: ['yes', 'no'] config: description: - - The module, by default, will connect to the remote device and - retrieve the current running-config to use as a base for comparing - against the contents of source. There are times when it is not - desirable to have the task get the current running-config for - every task in a playbook. The I(config) argument allows the - implementer to pass in the configuruation to use as the base - config for comparision. + - The C(config) argument allows the playbook desginer to supply + the base configuration to be used to validate configuration + changes necessary. If this argument is provided, the module + will not download the running-config from the remote node. required: false default: null + default: + description: + - This argument specifies whether or not to collect all defaults + when getting the remote device running config. When enabled, + the module will get the current config by issuing the command + C(show running-config all). + required: false + default: no + choices: ['yes', 'no'] + save: + description: + - The C(save) argument instructs the module to save the running- + config to the startup-config at the conclusion of the module + running. If check mode is specified, this argument is ignored. + required: false + default: no + choices: ['yes', 'no'] """ EXAMPLES = """ +# Note: examples below use the following provider dict to handle +# transport and authentication to the node. +vars: + cli: + host: "{{ inventory_hostname }}" + username: cisco + password: cisco + authorize: yes + auth_pass: cisco + transport: cli + - asa_config: lines: - network-object host 10.80.30.18 - network-object host 10.80.30.19 - network-object host 10.80.30.20 parents: ['object-group network OG-MONITORED-SERVERS'] + provider: "{{ cli }}" - asa_config: host: "{{ inventory_hostname }}" @@ -128,12 +190,11 @@ context: ansible - asa_config: - provider: "{{ cli }}" - host: "{{ inventory_hostname }}" show_command: 'more system:running-config' lines: - ikev1 pre-shared-key MyS3cretVPNK3y parents: tunnel-group 1.1.1.1 ipsec-attributes + provider: "{{ cli }}" """ @@ -143,79 +204,140 @@ returned: always type: list sample: ['...', '...'] - +backup_path: + description: The full path to the backup file + returned: when backup is yes + type: path + sample: /playbooks/ansible/backup/config.2016-07-16@22:28:34 responses: description: The set of responses from issuing the commands on the device - retured: when not check_mode + returned: when not check_mode type: list sample: ['...', '...'] """ +import re + +from ansible.module_utils.basic import get_exception +from ansible.module_utils.asa import NetworkModule, NetworkError +from ansible.module_utils.netcfg import NetworkConfig, dumps +from ansible.module_utils.netcli import Command + +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) -def get_config(module): - config = module.params['config'] or dict() - if not config and not module.params['force']: - config = module.config - return config +def check_args(module, warnings): + if module.params['parents']: + if not module.params['lines'] or module.params['src']: + warnings.append('ignoring unnecessary argument parents') + if module.params['match'] == 'none' and module.params['replace']: + warnings.append('ignoring unnecessary argument replace') +def get_config(module, result): + defaults = module.params['default'] + if defaults is True: + key = '__configall__' + else: + key = '__config__' + + contents = module.params['config'] or result.get(key) + + if not contents: + contents = module.config.get_config(include_defaults=defaults) + result[key] = contents + + return NetworkConfig(indent=1, contents=contents) + +def get_candidate(module): + candidate = NetworkConfig(indent=1) + if module.params['src']: + candidate.load(module.params['src']) + elif module.params['lines']: + parents = module.params['parents'] or list() + candidate.add(module.params['lines'], parents=parents) + return candidate + +def load_config(module, commands, result): + if not module.check_mode and module.params['update'] != 'check': + module.config(commands) + result['changed'] = module.params['update'] != 'check' + result['updates'] = commands.split('\n') + +def run(module, result): + match = module.params['match'] + replace = module.params['replace'] + + candidate = get_candidate(module) + + if match != 'none': + config = get_config(module, result) + configobjs = candidate.difference(config, match=match, replace=replace) + else: + config = None + configobjs = candidate.items + + if configobjs: + commands = dumps(configobjs, 'commands') + + if module.params['before']: + commands[:0] = module.params['before'] + + if module.params['after']: + commands.extend(module.params['after']) + + # send the configuration commands to the device and merge + # them with the current running config + load_config(module, commands, result) + + if module.params['save'] and not module.check_mode: + module.config.save_config() def main(): argument_spec = dict( - lines=dict(aliases=['commands'], required=True, type='list'), + lines=dict(aliases=['commands'], type='list'), parents=dict(type='list'), + + src=dict(type='path'), + before=dict(type='list'), after=dict(type='list'), - match=dict(default='line', choices=['line', 'strict', 'exact']), - replace=dict(default='line', choices=['line', 'block']), - force=dict(default=False, type='bool'), - config=dict() - ) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) - - lines = module.params['lines'] - parents = module.params['parents'] or list() + match=dict(default='line', choices=['line', 'strict', 'exact', 'none']), + replace=dict(default='line', choices=['line', 'block']), - before = module.params['before'] - after = module.params['after'] + update=dict(choices=['merge', 'check'], default='merge'), + backup=dict(type='bool', default=False), - match = module.params['match'] - replace = module.params['replace'] + config=dict(), + default=dict(type='bool', default=False), - if not module.params['force']: - contents = get_config(module) - config = NetworkConfig(contents=contents, indent=1) + save=dict(type='bool', default=False), + ) - candidate = NetworkConfig(indent=1) - candidate.add(lines, parents=parents) + mutually_exclusive = [('lines', 'src')] - commands = candidate.difference(config, path=parents, match=match, replace=replace) - else: - commands = parents - commands.extend(lines) + module = NetworkModule(argument_spec=argument_spec, + connect_on_load=False, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) - result = dict(changed=False) + warnings = list() + check_args(module, warnings) - if commands: - if before: - commands[:0] = before + result = dict(changed=False, warnings=warnings) - if after: - commands.extend(after) + if module.params['backup']: + result['__backup__'] = module.config.get_config() - if not module.check_mode: - commands = [str(c).strip() for c in commands] - response = module.configure(commands) - result['responses'] = response - result['changed'] = True + try: + run(module, result) + except NetworkError: + exc = get_exception() + module.fail_json(msg=str(exc)) - result['updates'] = commands module.exit_json(**result) -from ansible.module_utils.basic import * -from ansible.module_utils.shell import * -from ansible.module_utils.netcfg import * -from ansible.module_utils.asa import * if __name__ == '__main__': main() From dc0290a06798b12f089b8f6c1734aa4af9aa45b2 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Sun, 4 Sep 2016 07:42:20 -0400 Subject: [PATCH 2063/2522] added new functionality to asa_command * commands argument now accepts a dict arguments * only show commands are allowd when check mode is specified * config mode is no longer allowed in the command stack * add argument match with valid values any, all --- network/asa/asa_command.py | 148 +++++++++++++++++++++++++------------ 1 file changed, 100 insertions(+), 48 deletions(-) diff --git a/network/asa/asa_command.py b/network/asa/asa_command.py index ceae55a76df..9d013ebd197 100644 --- a/network/asa/asa_command.py +++ b/network/asa/asa_command.py @@ -21,7 +21,7 @@ --- module: asa_command version_added: "2.2" -author: "Peter Sprygada (@privateip) & Patrick Ogenstad (@ogenstad)" +author: "Peter Sprygada (@privateip), Patrick Ogenstad (@ogenstad)" short_description: Run arbitrary commands on Cisco ASA devices. description: - Sends arbitrary commands to an ASA node and returns the results @@ -32,27 +32,39 @@ options: commands: description: - - List of commands to send to the remote ios device over the + - List of commands to send to the remote device over the configured provider. The resulting output from the command - is returned. If the I(waitfor) argument is provided, the + is returned. If the I(wait_for) argument is provided, the module is not returned until the condition is satisfied or the number of retires as expired. required: true - waitfor: + wait_for: description: - List of conditions to evaluate against the output of the - command. The task will wait for a each condition to be true + command. The task will wait for each condition to be true before moving forward. If the conditional is not true within the configured number of retries, the task fails. See examples. required: false default: null + aliases: ['waitfor'] + match: + description: + - The I(match) argument is used in conjunction with the + I(wait_for) argument to specify the match policy. Valid + values are C(all) or C(any). If the value is set to C(all) + then all conditionals in the wait_for must be satisfied. If + the value is set to C(any) then only one of the values must be + satisfied. + required: false + default: all + choices: ['any', 'all'] retries: description: - Specifies the number of retries a command should by tried before it is considered failed. The command is run on the target device every retry and evaluated against the - waitfor conditions. + I(wait_for) conditions. required: false default: 10 interval: @@ -63,25 +75,36 @@ trying the command again. required: false default: 1 - """ EXAMPLES = """ +# Note: examples below use the following provider dict to handle +# transport and authentication to the node. +vars: + cli: + host: "{{ inventory_hostname }}" + username: cisco + password: cisco + authorize: yes + auth_pass: cisco + transport: cli + - asa_command: commands: - show version - register: output + provider: "{{ cli }}" - asa_command: commands: - show asp drop - show memory - register: output + provider: "{{ cli }}" - asa_command: commands: - show version + provider: "{{ cli }}" context: system """ @@ -104,11 +127,12 @@ type: list sample: ['...', '...'] """ +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcli import CommandRunner +from ansible.module_utils.netcli import AddCommandError, FailedConditionsError +from ansible.module_utils.asa import NetworkModule, NetworkError -import time -import shlex -import re - +VALID_KEYS = ['command', 'prompt', 'response'] def to_lines(stdout): for item in stdout: @@ -116,57 +140,85 @@ def to_lines(stdout): item = str(item).split('\n') yield item +def parse_commands(module): + for cmd in module.params['commands']: + if isinstance(cmd, basestring): + cmd = dict(command=cmd, output=None) + elif 'command' not in cmd: + module.fail_json(msg='command keyword argument is required') + elif not set(cmd.keys()).issubset(VALID_KEYS): + module.fail_json(msg='unknown keyword specified') + yield cmd def main(): spec = dict( - commands=dict(type='list'), - waitfor=dict(type='list'), + # { command: , prompt: , response: } + commands=dict(type='list', required=True), + + wait_for=dict(type='list', aliases=['waitfor']), + match=dict(default='all', choices=['all', 'any']), + retries=dict(default=10, type='int'), interval=dict(default=1, type='int') ) - module = get_module(argument_spec=spec, - supports_check_mode=True) + module = NetworkModule(argument_spec=spec, + connect_on_load=False, + supports_check_mode=True) - commands = module.params['commands'] + commands = list(parse_commands(module)) + conditionals = module.params['wait_for'] or list() - retries = module.params['retries'] - interval = module.params['interval'] + warnings = list() - try: - queue = set() - for entry in (module.params['waitfor'] or list()): - queue.add(Conditional(entry)) - except AttributeError: - exc = get_exception() - module.fail_json(msg=exc.message) + runner = CommandRunner(module) + + for cmd in commands: + if module.check_mode and not cmd['command'].startswith('show'): + warnings.append('only show commands are supported when using ' + 'check mode, not executing `%s`' % cmd['command']) + else: + if cmd['command'].startswith('conf'): + module.fail_json(msg='asa_command does not support running ' + 'config mode commands. Please use ' + 'asa_config instead') + try: + runner.add_command(**cmd) + except AddCommandError: + exc = get_exception() + warnings.append('duplicate command detected: %s' % cmd) - result = dict(changed=False) + for item in conditionals: + runner.add_conditional(item) - while retries > 0: - response = module.execute(commands) - result['stdout'] = response + runner.retries = module.params['retries'] + runner.interval = module.params['interval'] + runner.match = module.params['match'] - for item in list(queue): - if item(response): - queue.remove(item) + try: + runner.run() + except FailedConditionsError: + exc = get_exception() + module.fail_json(msg=str(exc), failed_conditions=exc.failed_conditions) + except NetworkError: + exc = get_exception() + module.fail_json(msg=str(exc)) - if not queue: - break + result = dict(changed=False, stdout=list()) - time.sleep(interval) - retries -= 1 - else: - failed_conditions = [item.raw for item in queue] - module.fail_json(msg='timeout waiting for value', failed_conditions=failed_conditions) + for cmd in commands: + try: + output = runner.get_command(cmd['command']) + except ValueError: + output = 'command not executed due to check_mode, see warnings' + result['stdout'].append(output) + result['warnings'] = warnings result['stdout_lines'] = list(to_lines(result['stdout'])) - return module.exit_json(**result) -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * -from ansible.module_utils.shell import * -from ansible.module_utils.netcfg import * -from ansible.module_utils.asa import * + module.exit_json(**result) + + if __name__ == '__main__': - main() + main() + From 10e8cdc93a56c9e20a86e99e3a3e471d6a32a1a2 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Sun, 4 Sep 2016 08:25:17 -0400 Subject: [PATCH 2064/2522] roll up updates to asa_acl module * remove get_module() in favor of NetworkModule * fix up import statements * roll up fixes for NetworkConfig object handling --- network/asa/asa_acl.py | 36 +++++++++++------------------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/network/asa/asa_acl.py b/network/asa/asa_acl.py index 6ef3b7d61db..1ac0461ea36 100644 --- a/network/asa/asa_acl.py +++ b/network/asa/asa_acl.py @@ -128,6 +128,8 @@ type: list sample: ['...', '...'] """ +from ansible.module_utils.netcfg import NetworkConfig +from ansible.module_utils.asa import NetworkModule def get_config(module): @@ -166,8 +168,8 @@ def main(): config=dict() ) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = NetworkModule(argument_spec=argument_spec, + supports_check_mode=True) lines = module.params['lines'] @@ -179,38 +181,22 @@ def main(): module.filter = check_input_acl(lines, module) if not module.params['force']: - contents = get_config(module) - config = NetworkConfig(contents=contents, indent=1) - - candidate = NetworkConfig(indent=1) - candidate.add(lines) - - commands = candidate.difference(config, match=match, replace=replace) + commands = candidate.difference(config) + commands = dumps(commands, 'commands').split('\n') + commands = [str(c) for c in commands if c] else: - commands = [] - commands.extend(lines) - - result = dict(changed=False) + commands = str(candidate).split('\n') if commands: - if before: - commands[:0] = before - - if after: - commands.extend(after) - if not module.check_mode: - commands = [str(c).strip() for c in commands] - response = module.configure(commands) + response = module.config(commands) result['responses'] = response result['changed'] = True result['updates'] = commands + module.exit_json(**result) -from ansible.module_utils.basic import * -from ansible.module_utils.shell import * -from ansible.module_utils.netcfg import * -from ansible.module_utils.asa import * + if __name__ == '__main__': main() From 2214203ce01b548d7946db165bc1d3eb3462eecb Mon Sep 17 00:00:00 2001 From: Werner Dijkerman Date: Mon, 5 Sep 2016 20:31:09 +0200 Subject: [PATCH 2065/2522] Added new module opendj_backendprop for updating backend settings opendj (#2855) Add opendj_backendprop --- identity/__init__.py | 0 identity/opendj/__init__.py | 0 identity/opendj/opendj_backendprop.py | 217 ++++++++++++++++++++++++++ 3 files changed, 217 insertions(+) create mode 100644 identity/__init__.py create mode 100644 identity/opendj/__init__.py create mode 100644 identity/opendj/opendj_backendprop.py diff --git a/identity/__init__.py b/identity/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/identity/opendj/__init__.py b/identity/opendj/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/identity/opendj/opendj_backendprop.py b/identity/opendj/opendj_backendprop.py new file mode 100644 index 00000000000..64571c0ed31 --- /dev/null +++ b/identity/opendj/opendj_backendprop.py @@ -0,0 +1,217 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Werner Dijkerman (ikben@werner-dijkerman.nl) +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +DOCUMENTATION = ''' +--- +module: opendj_backendprop +short_description: Will update the backend configuration of OpenDJ via the dsconfig set-backend-prop command. +description: + - This module will update settings for OpenDJ with the command set-backend-prop. + - It will check first via de get-backend-prop if configuration needs to be applied. +version_added: "2.2" +author: + - Werner Dijkerman +options: + opendj_bindir: + description: + - The path to the bin directory of OpenDJ. + required: false + default: /opt/opendj/bin + hostname: + description: + - The hostname of the OpenDJ server. + required: true + port: + description: + - The Admin port on which the OpenDJ instance is available. + required: true + username: + description: + - The username to connect to. + required: false + default: cn=Directory Manager + password: + description: + - The password for the cn=Directory Manager user. + - Either password or passwordfile is needed. + required: false + passwordfile: + description: + - Location to the password file which holds the password for the cn=Directory Manager user. + - Either password or passwordfile is needed. + required: false + backend: + description: + - The name of the backend on which the property needs to be updated. + required: true + name: + description: + - The configuration setting to update. + required: true + value: + description: + - The value for the configuration item. + required: true + state: + description: + - If configuration needs to be added/updated + required: false + default: "present" +''' + +EXAMPLES = ''' + - name: "Add or update OpenDJ backend properties" + action: opendj_backendprop + hostname=localhost + port=4444 + username="cn=Directory Manager" + password=password + backend=userRoot + name=index-entry-limit + value=5000 +''' + +RETURN = ''' +''' + +import subprocess + + +class BackendProp(object): + def __init__(self, module): + self._module = module + + def get_property(self, opendj_bindir, hostname, port, username, password_method, backend_name): + my_command = [ + opendj_bindir + '/dsconfig', + 'get-backend-prop', + '-h', hostname, + '--port', str(port), + '--bindDN', username, + '--backend-name', backend_name, + '-n', '-X', '-s' + ] + password_method + process = subprocess.Popen(my_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = process.communicate() + if process.returncode == 0: + return stdout + else: + self._module.fail_json(msg="Error message: " + str(stderr)) + + def set_property(self, opendj_bindir, hostname, port, username, password_method, backend_name,name, value): + my_command = [ + opendj_bindir + '/dsconfig', + 'set-backend-prop', + '-h', hostname, + '--port', str(port), + '--bindDN', username, + '--backend-name', backend_name, + '--set', name + ":" + value, + '-n', '-X' + ] + password_method + process = subprocess.Popen(my_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = process.communicate() + if process.returncode == 0: + return True + else: + self._module.fail_json(msg="Error message: " + stderr) + + def validate_data(self, data=None, name=None, value=None): + for config_line in data.split('\n'): + if config_line: + split_line = config_line.split() + if split_line[0] == name: + if split_line[1] == value: + return True + return False + + +def main(): + module = AnsibleModule( + argument_spec=dict( + opendj_bindir=dict(default="/opt/opendj/bin"), + hostname=dict(required=True), + port=dict(required=True), + username=dict(default="cn=Directory Manager", required=False), + password=dict(required=False, no_log=True), + passwordfile=dict(required=False), + backend=dict(required=True), + name=dict(required=True), + value=dict(required=True), + state=dict(default="present"), + ), + supports_check_mode=True + ) + + opendj_bindir = module.params['opendj_bindir'] + hostname = module.params['hostname'] + port = module.params['port'] + username = module.params['username'] + password = module.params['password'] + passwordfile = module.params['passwordfile'] + backend_name = module.params['backend'] + name = module.params['name'] + value = module.params['value'] + state = module.params['state'] + + if module.params["password"] is not None: + password_method = ['-w', password] + elif module.params["passwordfile"] is not None: + password_method = ['-j', passwordfile] + else: + module.fail_json(msg="No credentials are given. Use either 'password' or 'passwordfile'") + + if module.params["passwordfile"] and module.params["password"]: + module.fail_json(msg="only one of 'password' or 'passwordfile' can be set") + + opendj = BackendProp(module) + validate = opendj.get_property(opendj_bindir=opendj_bindir, + hostname=hostname, + port=port, + username=username, + password_method=password_method, + backend_name=backend_name) + + if validate: + if not opendj.validate_data(data=validate, name=name, value=value): + if module.check_mode: + module.exit_json(changed=True) + if opendj.set_property(opendj_bindir=opendj_bindir, + hostname=hostname, + port=port, + username=username, + password_method=password_method, + backend_name=backend_name, + name=name, + value=value): + module.exit_json(changed=True) + else: + module.exit_json(changed=False) + else: + module.exit_json(changed=False) + else: + module.exit_json(changed=False) + + +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From c848957792d6b580c2918afa0ad799a91a866918 Mon Sep 17 00:00:00 2001 From: sermilrod Date: Mon, 5 Sep 2016 21:04:33 +0100 Subject: [PATCH 2066/2522] adding jenkins_job module (#2521) --- web_infrastructure/jenkins_job.py | 326 ++++++++++++++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 web_infrastructure/jenkins_job.py diff --git a/web_infrastructure/jenkins_job.py b/web_infrastructure/jenkins_job.py new file mode 100644 index 00000000000..71d584dd7ac --- /dev/null +++ b/web_infrastructure/jenkins_job.py @@ -0,0 +1,326 @@ +#!/usr/bin/python +# +# This is a free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This Ansible library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this library. If not, see . + +DOCUMENTATION = ''' +--- +module: jenkins_job +short_description: Manage jenkins jobs +description: + - Manage Jenkins jobs by using Jenkins REST API +requirements: + - "python-jenkins >= 0.4.12" + - "lxml >= 3.3.3" +version_added: "2.2" +author: "Sergio Millan Rodriguez" +options: + config: + description: + - config.xml file to use as job config within your Ansible repo. + required: false + enable: + description: + - Action to take with the Jenkins job (enable/disable). + required: false + name: + description: + - Name of the Jenkins job. + required: true + password: + description: + - Password to authenticate with the Jenkins server. + required: false + state: + description: + - Attribute that specifies if the job has to be created or deleted. + required: true + choices: ['present', 'absent'] + token: + description: + - API token used to authenticate alternatively to password. + required: false + url: + description: + - Url where the Jenkins server is accessible. + required: false + default: http://localhost:8080 + user: + description: + - User to authenticate with the Jenkins server. + required: false +''' + +EXAMPLES = ''' +# Create a jenkins job using basic authentication +- jenkins_job: + config: "{{ lookup('file', 'templates/test.xml') }}" + name: test + password: admin + state: present + enable: True + url: "http://localhost:8080" + user: admin + +# Create a jenkins job using the token +- jenkins_job: + config: "{{ lookup('template', 'templates/test.xml.j2') }}" + name: test + token: asdfasfasfasdfasdfadfasfasdfasdfc + state: present + enable: yes + url: "http://localhost:8080" + user: admin + +# Delete a jenkins job using basic authentication +- jenkins_job: + name: test + password: admin + state: absent + url: "http://localhost:8080" + user: admin + +# Delete a jenkins job using the token +- jenkins_job: + name: test + token: asdfasfasfasdfasdfadfasfasdfasdfc + state: absent + url: "http://localhost:8080" + user: admin + +# Disable a jenkins job using basic authentication +- jenkins_job: + name: test + password: admin + state: present + enable: False + url: "http://localhost:8080" + user: admin + +# Disable a jenkins job using the token +- jenkins_job: + name: test + token: asdfasfasfasdfasdfadfasfasdfasdfc + state: present + enable: no + url: "http://localhost:8080" + user: admin +''' + +RETURN = ''' +--- +name: + description: Name of the jenkins job. + returned: success + type: string + sample: test-job +state: + description: State of the jenkins job. + returned: success + type: string + sample: present +url: + description: Url to connect to the Jenkins server. + returned: success + type: string + sample: https://jenkins.mydomain.com +''' + +try: + import jenkins + python_jenkins_installed = True +except ImportError: + python_jenkins_installed = False + +try: + from lxml import etree as ET + python_lxml_installed = True +except ImportError: + python_lxml_installed = False + +class Jenkins: + def __init__(self, config, name, password, state, enable, token, url, user): + self.config = config + self.name = name + self.password = password + self.state = state + self.enable = enable + self.token = token + self.user = user + self.jenkins_url = url + self.server = self.get_jenkins_connection() + + def get_jenkins_connection(self): + try: + if (self.user and self.password): + return jenkins.Jenkins(self.jenkins_url, self.user, self.password) + elif (self.user and self.token): + return jenkins.Jenkins(self.jenkins_url, self.user, self.token) + elif (self.user and not (self.password or self.token)): + return jenkins.Jenkins(self.jenkins_url, self.user) + else: + return jenkins.Jenkins(self.jenkins_url) + except Exception: + e = get_exception() + module.fail_json(msg='Unable to connect to Jenkins server, %s' % str(e)) + + def get_job_status(self, module): + try: + return self.server.get_job_info(self.name)['color'].encode('utf-8') + except Exception: + e = get_exception() + module.fail_json(msg='Unable to fetch job information, %s' % str(e)) + + def job_exists(self, module): + try: + return bool(self.server.job_exists(self.name)) + except Exception: + e = get_exception() + module.fail_json(msg='Unable to validate if job exists, %s for %s' % (str(e), self.jenkins_url)) + + def build(self, module): + if self.state == 'present': + self.update_job(module) + else: + self.delete_job(module) + + def get_config(self): + return job_config_to_string(self.config) + + def configuration_changed(self): + changed = False + config_file = self.get_config() + machine_file = job_config_to_string(self.server.get_job_config(self.name).encode('utf-8')) + if not machine_file == config_file: + changed = True + + return changed + + def update_job(self, module): + if not self.job_exists(module): + self.create_job(module) + else: + self.reconfig_job(module) + + def state_changed(self, status): + changed = False + if ( (self.enable == False and status != "disabled") or (self.enable == True and status == "disabled") ): + changed = True + + return changed + + def change_state(self): + if self.enable == False: + self.server.disable_job(self.name) + else: + self.server.enable_job(self.name) + + def reconfig_job(self, module): + changed = False + try: + status = self.get_job_status(module) + if self.enable == True: + if ( self.configuration_changed() or self.state_changed(status) ): + changed = True + if not module.check_mode: + self.server.reconfig_job(self.name, self.get_config()) + self.change_state() + else: + if self.state_changed(status): + changed = True + if not module.check_mode: + self.change_state() + + except Exception: + e = get_exception() + module.fail_json(msg='Unable to reconfigure job, %s for %s' % (str(e), self.jenkins_url)) + + module.exit_json(changed=changed, name=self.name, state=self.state, url=self.jenkins_url) + + def create_job(self, module): + changed = False + try: + changed = True + if not module.check_mode: + self.server.create_job(self.name, self.get_config()) + self.change_state() + except Exception: + e = get_exception() + module.fail_json(msg='Unable to create job, %s for %s' % (str(e), self.jenkins_url)) + + module.exit_json(changed=changed, name=self.name, state=self.state, url=self.jenkins_url) + + def delete_job(self, module): + changed = False + if self.job_exists(module): + changed = True + if not module.check_mode: + try: + self.server.delete_job(self.name) + except Exception: + e = get_exception() + module.fail_json(msg='Unable to delete job, %s for %s' % (str(e), self.jenkins_url)) + + module.exit_json(changed=changed, name=self.name, state=self.state, url=self.jenkins_url) + +def test_dependencies(module): + if not python_jenkins_installed: + module.fail_json(msg="python-jenkins required for this module. "\ + "see http://python-jenkins.readthedocs.io/en/latest/install.html") + + if not python_lxml_installed: + module.fail_json(msg="lxml required for this module. "\ + "see http://lxml.de/installation.html") + +def job_config_to_string(xml_str): + return ET.tostring(ET.fromstring(xml_str)) + +def jenkins_builder(module): + return Jenkins( + module.params.get('config'), + module.params.get('name'), + module.params.get('password'), + module.params.get('state'), + module.params.get('enable'), + module.params.get('token'), + module.params.get('url'), + module.params.get('user') + ) + +def main(): + module = AnsibleModule( + argument_spec = dict( + config = dict(required=False), + name = dict(required=True), + password = dict(required=False, no_log=True), + state = dict(required=True, choices=['present', 'absent']), + enable = dict(required=False, type='bool'), + token = dict(required=False, no_log=True), + url = dict(required=False, default="http://localhost:8080"), + user = dict(required=False) + ), + required_if = [ + ('state', 'present', ['enable']), + ('enable', True, ['config']) + ], + mutually_exclusive = [['password', 'token']], + supports_check_mode=True, + ) + + test_dependencies(module) + jenkins = jenkins_builder(module) + jenkins.build(module) + +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From 39972fa697806a54084eb2a3b0f15e7212c9d7b9 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Mon, 5 Sep 2016 22:06:18 +0200 Subject: [PATCH 2067/2522] Add diff mode output to debconf module (#2530) Support diff such that the previous and current settings are visible without debug output and just with `--diff` if requested. --- system/debconf.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/system/debconf.py b/system/debconf.py index 74818e908f4..4aa512b4a83 100644 --- a/system/debconf.py +++ b/system/debconf.py @@ -161,8 +161,14 @@ def main(): prev = {question: prev[question]} else: prev[question] = '' + if module._diff: + after = prev.copy() + after.update(curr) + diffdict = {'before': prev, 'after': after} + else: + diff_dict = {} - module.exit_json(changed=changed, msg=msg, current=curr, previous=prev) + module.exit_json(changed=changed, msg=msg, current=curr, previous=prev, diff=diff_dict) module.exit_json(changed=changed, msg=msg, current=prev) From 1474bde8645c1b44ff57fafb8ce9af4352589f08 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Tue, 6 Sep 2016 15:11:36 +0200 Subject: [PATCH 2068/2522] Do not crash if the system do not have required modules (#2852) --- network/nmcli.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/network/nmcli.py b/network/nmcli.py index a7746051099..64ee02b09c5 100644 --- a/network/nmcli.py +++ b/network/nmcli.py @@ -381,9 +381,19 @@ # import ansible.module_utils.basic import os import sys -import dbus -from gi.repository import NetworkManager, NMClient - +HAVE_DBUS=False +try: + import dbus + HAVE_DBUS=True +except ImportError: + pass + +HAVE_NM_CLIENT=False +try: + from gi.repository import NetworkManager, NMClient + HAVE_NM_CLIENT=True +except ImportError: + pass class Nmcli(object): """ @@ -1010,6 +1020,12 @@ def main(): supports_check_mode=True ) + if not HAVE_DBUS: + module.fail_json(msg="This module requires dbus python bindings") + + if not HAVE_NM_CLIENT: + module.fail_json(msg="This module requires NetworkManager glib API") + nmcli=Nmcli(module) rc=None From 1f79c357da345bfab9906ed07e4f3d3e11d7e6f5 Mon Sep 17 00:00:00 2001 From: jmenga Date: Wed, 7 Sep 2016 09:13:51 +1200 Subject: [PATCH 2069/2522] New module cloudformation_facts (#2329) --- cloud/amazon/cloudformation_facts.py | 274 +++++++++++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 cloud/amazon/cloudformation_facts.py diff --git a/cloud/amazon/cloudformation_facts.py b/cloud/amazon/cloudformation_facts.py new file mode 100644 index 00000000000..efccae64e59 --- /dev/null +++ b/cloud/amazon/cloudformation_facts.py @@ -0,0 +1,274 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: cloudformation_facts +short_description: Obtain facts about an AWS CloudFormation stack +description: + - Gets information about an AWS CloudFormation stack +requirements: + - boto3 >= 1.0.0 + - python >= 2.6 +version_added: "2.2" +author: Justin Menga (@jmenga) +options: + stack_name: + description: + - The name or id of the CloudFormation stack + required: true + all_facts: + description: + - Get all stack information for the stack + required: false + default: false + stack_events: + description: + - Get stack events for the stack + required: false + default: false + stack_template: + description: + - Get stack template body for the stack + required: false + default: false + stack_resources: + description: + - Get stack resources for the stack + required: false + default: false + stack_policy: + description: + - Get stack policy for the stack + required: false + default: false +extends_documentation_fragment: + - aws + - ec2 +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Get summary information about a stack +- cloudformation_facts: + stack_name: my-cloudformation-stack + +# Facts are published in ansible_facts['cloudformation'][] +- debug: msg={{ ansible_facts['cloudformation']['my-cloudformation-stack'] }} + +# Get all stack information about a stack +- cloudformation_facts: + stack_name: my-cloudformation-stack + all_facts: true + +# Get stack resource and stack policy information about a stack +- cloudformation_facts: + stack_name: my-cloudformation-stack + stack_resources: true + stack_policy: true + +# Example dictionary outputs for stack_outputs, stack_parameters and stack_resources: +"stack_outputs": { + "ApplicationDatabaseName": "dazvlpr01xj55a.ap-southeast-2.rds.amazonaws.com", + ... +}, +"stack_parameters": { + "DatabaseEngine": "mysql", + "DatabasePassword": "****", + ... +}, +"stack_resources": { + "AutoscalingGroup": "dev-someapp-AutoscalingGroup-1SKEXXBCAN0S7", + "AutoscalingSecurityGroup": "sg-abcd1234", + "ApplicationDatabase": "dazvlpr01xj55a", + "EcsTaskDefinition": "arn:aws:ecs:ap-southeast-2:123456789:task-definition/dev-someapp-EcsTaskDefinition-1F2VM9QB0I7K9:1" + ... +} +''' + +RETURN = ''' +stack_description: + description: Summary facts about the stack + returned: always + type: dict +stack_outputs: + description: Dictionary of stack outputs keyed by the value of each output 'OutputKey' parameter and corresponding value of each output 'OutputValue' parameter + returned: always + type: dict +stack_parameters: + description: Dictionary of stack parameters keyed by the value of each parameter 'ParameterKey' parameter and corresponding value of each parameter 'ParameterValue' parameter + returned: always + type: dict +stack_events: + description: All stack events for the stack + returned: only if all_facts or stack_events is true + type: list of events +stack_policy: + description: Describes the stack policy for the stack + returned: only if all_facts or stack_policy is true + type: dict +stack_template: + description: Describes the stack template for the stack + returned: only if all_facts or stack_template is true + type: dict +stack_resource_list: + description: Describes stack resources for the stack + returned: only if all_facts or stack_resourses is true + type: list of resources +stack_resources: + description: Dictionary of stack resources keyed by the value of each resource 'LogicalResourceId' parameter and corresponding value of each resource 'PhysicalResourceId' parameter + returned: only if all_facts or stack_resourses is true + type: dict +''' + +try: + import boto3 + import botocore + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + +from ansible.module_utils.ec2 import get_aws_connection_info, ec2_argument_spec +from ansible.module_utils.basic import AnsibleModule +from functools import partial +import json +import traceback + +class CloudFormationServiceManager: + """Handles CloudFormation Services""" + + def __init__(self, module): + self.module = module + + try: + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + self.client = boto3.client('cloudformation', region_name=region, **aws_connect_kwargs) + except botocore.exceptions.NoRegionError: + self.module.fail_json(msg="Region must be specified as a parameter, in AWS_DEFAULT_REGION environment variable or in boto configuration file") + except Exception as e: + self.module.fail_json(msg="Can't establish connection - " + str(e), exception=traceback.format_exc(e)) + + def describe_stack(self, stack_name): + try: + func = partial(self.client.describe_stacks,StackName=stack_name) + response = self.paginated_response(func, 'Stacks') + if response: + return response[0] + self.module.fail_json(msg="Error describing stack - an empty response was returned") + except Exception as e: + self.module.fail_json(msg="Error describing stack - " + str(e), exception=traceback.format_exc(e)) + + def list_stack_resources(self, stack_name): + try: + func = partial(self.client.list_stack_resources,StackName=stack_name) + return self.paginated_response(func, 'StackResourceSummaries') + except Exception as e: + self.module.fail_json(msg="Error listing stack resources - " + str(e), exception=traceback.format_exc(e)) + + def describe_stack_events(self, stack_name): + try: + func = partial(self.client.describe_stack_events,StackName=stack_name) + return self.paginated_response(func, 'StackEvents') + except Exception as e: + self.module.fail_json(msg="Error describing stack events - " + str(e), exception=traceback.format_exc(e)) + + def get_stack_policy(self, stack_name): + try: + response = self.client.get_stack_policy(StackName=stack_name) + stack_policy = response.get('StackPolicyBody') + if stack_policy: + return json.loads(stack_policy) + return dict() + except Exception as e: + self.module.fail_json(msg="Error getting stack policy - " + str(e), exception=traceback.format_exc(e)) + + def get_template(self, stack_name): + try: + response = self.client.get_template(StackName=stack_name) + return response.get('TemplateBody') + except Exception as e: + self.module.fail_json(msg="Error getting stack template - " + str(e), exception=traceback.format_exc(e)) + + def paginated_response(self, func, result_key, next_token=None): + ''' + Returns expanded response for paginated operations. + The 'result_key' is used to define the concatenated results that are combined from each paginated response. + ''' + args=dict() + if next_token: + args['NextToken'] = next_token + response = func(**args) + result = response.get(result_key) + next_token = response.get('NextToken') + if not next_token: + return result + return result + self.paginated_response(func, result_key, next_token) + +def to_dict(items, key, value): + ''' Transforms a list of items to a Key/Value dictionary ''' + if items: + return dict(zip([i[key] for i in items], [i[value] for i in items])) + else: + return dict() + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + stack_name=dict(required=True, type='str' ), + all_facts=dict(required=False, default=False, type='bool'), + stack_policy=dict(required=False, default=False, type='bool'), + stack_events=dict(required=False, default=False, type='bool'), + stack_resources=dict(required=False, default=False, type='bool'), + stack_template=dict(required=False, default=False, type='bool'), + )) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False) + + if not HAS_BOTO3: + module.fail_json(msg='boto3 is required.') + + # Describe the stack + service_mgr = CloudFormationServiceManager(module) + stack_name = module.params.get('stack_name') + result = { + 'ansible_facts': { 'cloudformation': { stack_name:{} } } + } + facts = result['ansible_facts']['cloudformation'][stack_name] + facts['stack_description'] = service_mgr.describe_stack(stack_name) + + # Create stack output and stack parameter dictionaries + if facts['stack_description']: + facts['stack_outputs'] = to_dict(facts['stack_description'].get('Outputs'), 'OutputKey', 'OutputValue') + facts['stack_parameters'] = to_dict(facts['stack_description'].get('Parameters'), 'ParameterKey', 'ParameterValue') + + # Create optional stack outputs + all_facts = module.params.get('all_facts') + if all_facts or module.params.get('stack_resources'): + facts['stack_resource_list'] = service_mgr.list_stack_resources(stack_name) + facts['stack_resources'] = to_dict(facts.get('stack_resource_list'), 'LogicalResourceId', 'PhysicalResourceId') + if all_facts or module.params.get('stack_template'): + facts['stack_template'] = service_mgr.get_template(stack_name) + if all_facts or module.params.get('stack_policy'): + facts['stack_policy'] = service_mgr.get_stack_policy(stack_name) + if all_facts or module.params.get('stack_events'): + facts['stack_events'] = service_mgr.describe_stack_events(stack_name) + + result['changed'] = False + module.exit_json(ansible_facts=result) + +if __name__ == '__main__': + main() \ No newline at end of file From b1a25291b5027f9a0708308afd0b3dddc4e17e8f Mon Sep 17 00:00:00 2001 From: "Ryan S. Brown" Date: Tue, 6 Sep 2016 17:14:54 -0400 Subject: [PATCH 2070/2522] cloudformation_facts: Connect boto3 using the module_utils AWS connection instead of calling boto3 directly --- cloud/amazon/cloudformation_facts.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/cloud/amazon/cloudformation_facts.py b/cloud/amazon/cloudformation_facts.py index efccae64e59..028b2edec7b 100644 --- a/cloud/amazon/cloudformation_facts.py +++ b/cloud/amazon/cloudformation_facts.py @@ -156,7 +156,9 @@ def __init__(self, module): try: region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - self.client = boto3.client('cloudformation', region_name=region, **aws_connect_kwargs) + self.client = boto3_conn(module, conn_type='client', + resource='cloudformation', region=region, + endpoint=ec2_url, **aws_connect_kwargs) except botocore.exceptions.NoRegionError: self.module.fail_json(msg="Region must be specified as a parameter, in AWS_DEFAULT_REGION environment variable or in boto configuration file") except Exception as e: @@ -171,7 +173,7 @@ def describe_stack(self, stack_name): self.module.fail_json(msg="Error describing stack - an empty response was returned") except Exception as e: self.module.fail_json(msg="Error describing stack - " + str(e), exception=traceback.format_exc(e)) - + def list_stack_resources(self, stack_name): try: func = partial(self.client.list_stack_resources,StackName=stack_name) @@ -195,7 +197,7 @@ def get_stack_policy(self, stack_name): return dict() except Exception as e: self.module.fail_json(msg="Error getting stack policy - " + str(e), exception=traceback.format_exc(e)) - + def get_template(self, stack_name): try: response = self.client.get_template(StackName=stack_name) @@ -229,7 +231,7 @@ def main(): argument_spec = ec2_argument_spec() argument_spec.update(dict( stack_name=dict(required=True, type='str' ), - all_facts=dict(required=False, default=False, type='bool'), + all_facts=dict(required=False, default=False, type='bool'), stack_policy=dict(required=False, default=False, type='bool'), stack_events=dict(required=False, default=False, type='bool'), stack_resources=dict(required=False, default=False, type='bool'), @@ -240,11 +242,11 @@ def main(): if not HAS_BOTO3: module.fail_json(msg='boto3 is required.') - - # Describe the stack + + # Describe the stack service_mgr = CloudFormationServiceManager(module) stack_name = module.params.get('stack_name') - result = { + result = { 'ansible_facts': { 'cloudformation': { stack_name:{} } } } facts = result['ansible_facts']['cloudformation'][stack_name] @@ -268,7 +270,11 @@ def main(): facts['stack_events'] = service_mgr.describe_stack_events(stack_name) result['changed'] = False - module.exit_json(ansible_facts=result) + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * if __name__ == '__main__': - main() \ No newline at end of file + main() From 8bfdcfcab264386695faf058cfbb221588f2c573 Mon Sep 17 00:00:00 2001 From: "Ryan S. Brown" Date: Tue, 6 Sep 2016 17:15:13 -0400 Subject: [PATCH 2071/2522] Normalize variable naming in cloudformation_facts module using camel2snake --- cloud/amazon/cloudformation_facts.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/cloudformation_facts.py b/cloud/amazon/cloudformation_facts.py index 028b2edec7b..381e479fe6f 100644 --- a/cloud/amazon/cloudformation_facts.py +++ b/cloud/amazon/cloudformation_facts.py @@ -254,8 +254,13 @@ def main(): # Create stack output and stack parameter dictionaries if facts['stack_description']: - facts['stack_outputs'] = to_dict(facts['stack_description'].get('Outputs'), 'OutputKey', 'OutputValue') - facts['stack_parameters'] = to_dict(facts['stack_description'].get('Parameters'), 'ParameterKey', 'ParameterValue') + facts['stack_outputs'] = to_dict(facts['stack_description'].get('Outputs'), 'OutputKey', 'OutputValue') + facts['stack_parameters'] = to_dict(facts['stack_description'].get('Parameters'), 'ParameterKey', 'ParameterValue') + + # normalize stack description API output + facts['stack_description'] = camel_dict_to_snake_dict(facts['stack_description']) + # camel2snake doesn't handle NotificationARNs properly, so let's fix that + facts['stack_description']['notification_arns'] = facts['stack_description'].pop('notification_ar_ns', []) # Create optional stack outputs all_facts = module.params.get('all_facts') From faf8f2192d0b6935bd893c3fef31bf6dc1583e0a Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Wed, 7 Sep 2016 07:34:00 -0700 Subject: [PATCH 2072/2522] Adds bigip_ssl_certificate module (#2831) Adds bigip_ssl_certificate module This module is another in the ongoing "bootstrapping saga" that is being undertaken. With this module you can manage the lifecycle of the SSL certificates on a BIG-IP. This includes those used for SSL offloading. Tests for this module can be found here https://github.com/F5Networks/f5-ansible/blob/master/roles/__bigip_ssl_certificate/tasks/main.yaml Platforms this was tested on are 12.0.0 12.1.0 --- network/f5/bigip_ssl_certificate.py | 516 ++++++++++++++++++++++++++++ 1 file changed, 516 insertions(+) create mode 100644 network/f5/bigip_ssl_certificate.py diff --git a/network/f5/bigip_ssl_certificate.py b/network/f5/bigip_ssl_certificate.py new file mode 100644 index 00000000000..076caba9f54 --- /dev/null +++ b/network/f5/bigip_ssl_certificate.py @@ -0,0 +1,516 @@ +#!/usr/bin/python +# +# (c) 2016, Kevin Coming (@waffie1) +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +module: bigip_ssl_certificate +short_description: Import/Delete certificates from BIG-IP +description: + - This module will import/delete SSL certificates on BIG-IP LTM. + Certificates can be imported from certificate and key files on the local + disk, in PEM format. +version_added: 2.2 +options: + cert_content: + description: + - When used instead of 'cert_src', sets the contents of a certificate directly + to the specified value. This is used with lookup plugins or for anything + with formatting or templating. Either one of C(key_src), + C(key_content), C(cert_src) or C(cert_content) must be provided when + C(state) is C(present). + required: false + key_content: + description: + - When used instead of 'key_src', sets the contents of a certificate key + directly to the specified value. This is used with lookup plugins or for + anything with formatting or templating. Either one of C(key_src), + C(key_content), C(cert_src) or C(cert_content) must be provided when + C(state) is C(present). + required: false + state: + description: + - Certificate and key state. This determines if the provided certificate + and key is to be made C(present) on the device or C(absent). + required: true + default: present + choices: + - present + - absent + partition: + description: + - BIG-IP partition to use when adding/deleting certificate. + required: false + default: Common + name: + description: + - SSL Certificate Name. This is the cert/key pair name used + when importing a certificate/key into the F5. It also + determines the filenames of the objects on the LTM + (:Partition:name.cer_11111_1 and :Partition_name.key_11111_1). + required: true + cert_src: + description: + - This is the local filename of the certificate. Either one of C(key_src), + C(key_content), C(cert_src) or C(cert_content) must be provided when + C(state) is C(present). + required: false + key_src: + description: + - This is the local filename of the private key. Either one of C(key_src), + C(key_content), C(cert_src) or C(cert_content) must be provided when + C(state) is C(present). + required: false + passphrase: + description: + - Passphrase on certificate private key + required: false +notes: + - Requires the f5-sdk Python package on the host. This is as easy as pip + install f5-sdk. + - Requires the netaddr Python package on the host. + - If you use this module, you will not be able to remove the certificates + and keys that are managed, via the web UI. You can only remove them via + tmsh or these modules. +extends_documentation_fragment: f5 +requirements: + - f5-sdk >= 1.3.1 + - BigIP >= v12 +author: + - Kevin Coming (@waffie1) + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = ''' +- name: Import PEM Certificate from local disk + bigip_ssl_certificate: + name: "certificate-name" + server: "lb.mydomain.com" + user: "admin" + password: "secret" + state: "present" + cert_src: "/path/to/cert.crt" + key_src: "/path/to/key.key" + delegate_to: localhost + +- name: Use a file lookup to import PEM Certificate + bigip_ssl_certificate: + name: "certificate-name" + server: "lb.mydomain.com" + user: "admin" + password: "secret" + state: "present" + cert_content: "{{ lookup('file', '/path/to/cert.crt') }}" + key_content: "{{ lookup('file', '/path/to/key.key') }}" + delegate_to: localhost + +- name: "Delete Certificate" + bigip_ssl_certificate: + name: "certificate-name" + server: "lb.mydomain.com" + user: "admin" + password: "secret" + state: "absent" + delegate_to: localhost +''' + +RETURN = ''' +cert_name: + description: > + The name of the SSL certificate. The C(cert_name) and + C(key_name) will be equal to each other. + returned: + - created + - changed + - deleted + type: string + sample: "cert1" +key_name: + description: > + The name of the SSL certificate key. The C(key_name) and + C(cert_name) will be equal to each other. + returned: + - created + - changed + - deleted + type: string + sample: "key1" +partition: + description: Partition in which the cert/key was created + returned: + - changed + - created + - deleted + type: string + sample: "Common" +key_checksum: + description: SHA1 checksum of the key that was provided + return: + - changed + - created + type: string + sample: "cf23df2207d99a74fbe169e3eba035e633b65d94" +cert_checksum: + description: SHA1 checksum of the cert that was provided + return: + - changed + - created + type: string + sample: "f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0" +''' + + +try: + from f5.bigip.contexts import TransactionContextManager + from f5.bigip import ManagementRoot + from icontrol.session import iControlUnexpectedHTTPError + HAS_F5SDK = True +except ImportError: + HAS_F5SDK = False + + +import hashlib +import StringIO + + +class BigIpSslCertificate(object): + def __init__(self, *args, **kwargs): + if not HAS_F5SDK: + raise F5ModuleError("The python f5-sdk module is required") + + required_args = ['key_content', 'key_src', 'cert_content', 'cert_src'] + + ksource = kwargs['key_src'] + if ksource: + with open(ksource) as f: + kwargs['key_content'] = f.read() + + csource = kwargs['cert_src'] + if csource: + with open(csource) as f: + kwargs['cert_content'] = f.read() + + if kwargs['state'] == 'present': + if not any(kwargs[k] is not None for k in required_args): + raise F5ModuleError( + "Either 'key_content', 'key_src', 'cert_content' or " + "'cert_src' must be provided" + ) + + # This is the remote BIG-IP path from where it will look for certs + # to install. + self.dlpath = '/var/config/rest/downloads' + + # The params that change in the module + self.cparams = dict() + + # Stores the params that are sent to the module + self.params = kwargs + self.api = ManagementRoot(kwargs['server'], + kwargs['user'], + kwargs['password'], + port=kwargs['server_port']) + + def exists(self): + cert = self.cert_exists() + key = self.key_exists() + + if cert and key: + return True + else: + return False + + def get_hash(self, content): + k = hashlib.sha1() + s = StringIO.StringIO(content) + while True: + data = s.read(1024) + if not data: + break + k.update(data) + return k.hexdigest() + + def present(self): + current = self.read() + changed = False + do_key = False + do_cert = False + chash = None + khash = None + + check_mode = self.params['check_mode'] + name = self.params['name'] + partition = self.params['partition'] + cert_content = self.params['cert_content'] + key_content = self.params['key_content'] + passphrase = self.params['passphrase'] + + # Technically you dont need to provide us with anything in the form + # of content for your cert, but that's kind of illogical, so we just + # return saying you didn't "do" anything if you left the cert and keys + # empty. + if not cert_content and not key_content: + return False + + if key_content is not None: + if 'key_checksum' in current: + khash = self.get_hash(key_content) + if khash not in current['key_checksum']: + do_key = "update" + else: + do_key = "create" + + if cert_content is not None: + if 'cert_checksum' in current: + chash = self.get_hash(cert_content) + if chash not in current['cert_checksum']: + do_cert = "update" + else: + do_cert = "create" + + if do_cert or do_key: + changed = True + params = dict() + params['cert_name'] = name + params['key_name'] = name + params['partition'] = partition + if khash: + params['key_checksum'] = khash + if chash: + params['cert_checksum'] = chash + self.cparams = params + + if check_mode: + return changed + + if not do_cert and not do_key: + return False + + tx = self.api.tm.transactions.transaction + with TransactionContextManager(tx) as api: + if do_cert: + # Upload the content of a certificate as a StringIO object + cstring = StringIO.StringIO(cert_content) + filename = "%s.crt" % (name) + filepath = os.path.join(self.dlpath, filename) + api.shared.file_transfer.uploads.upload_stringio( + cstring, + filename + ) + + if do_cert == "update": + # Install the certificate + params = { + 'name': name, + 'partition': partition + } + cert = api.tm.sys.file.ssl_certs.ssl_cert.load(**params) + + # This works because, while the source path is the same, + # calling update causes the file to be re-read + cert.update() + changed = True + elif do_cert == "create": + # Install the certificate + params = { + 'sourcePath': "file://" + filepath, + 'name': name, + 'partition': partition + } + api.tm.sys.file.ssl_certs.ssl_cert.create(**params) + changed = True + + if do_key: + # Upload the content of a certificate key as a StringIO object + kstring = StringIO.StringIO(key_content) + filename = "%s.key" % (name) + filepath = os.path.join(self.dlpath, filename) + api.shared.file_transfer.uploads.upload_stringio( + kstring, + filename + ) + + if do_key == "update": + # Install the key + params = { + 'name': name, + 'partition': partition + } + key = api.tm.sys.file.ssl_keys.ssl_key.load(**params) + + params = dict() + + if passphrase: + params['passphrase'] = passphrase + else: + params['passphrase'] = None + + key.update(**params) + changed = True + elif do_key == "create": + # Install the key + params = { + 'sourcePath': "file://" + filepath, + 'name': name, + 'partition': partition + } + if passphrase: + params['passphrase'] = self.params['passphrase'] + else: + params['passphrase'] = None + + api.tm.sys.file.ssl_keys.ssl_key.create(**params) + changed = True + return changed + + def key_exists(self): + return self.api.tm.sys.file.ssl_keys.ssl_key.exists( + name=self.params['name'], + partition=self.params['partition'] + ) + + def cert_exists(self): + return self.api.tm.sys.file.ssl_certs.ssl_cert.exists( + name=self.params['name'], + partition=self.params['partition'] + ) + + def read(self): + p = dict() + name = self.params['name'] + partition = self.params['partition'] + + if self.key_exists(): + key = self.api.tm.sys.file.ssl_keys.ssl_key.load( + name=name, + partition=partition + ) + if hasattr(key, 'checksum'): + p['key_checksum'] = str(key.checksum) + + if self.cert_exists(): + cert = self.api.tm.sys.file.ssl_certs.ssl_cert.load( + name=name, + partition=partition + ) + if hasattr(cert, 'checksum'): + p['cert_checksum'] = str(cert.checksum) + + p['name'] = name + return p + + def flush(self): + result = dict() + state = self.params['state'] + + try: + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + except iControlUnexpectedHTTPError as e: + raise F5ModuleError(str(e)) + + result.update(**self.cparams) + result.update(dict(changed=changed)) + return result + + def absent(self): + changed = False + + if self.exists(): + changed = self.delete() + + return changed + + def delete(self): + changed = False + + check_mode = self.params['check_mode'] + + delete_cert = self.cert_exists() + delete_key = self.key_exists() + + if not delete_cert and not delete_key: + return changed + + if check_mode: + params = dict() + params['cert_name'] = name + params['key_name'] = name + params['partition'] = partition + self.cparams = params + return True + + tx = self.api.tm.transactions.transaction + with TransactionContextManager(tx) as api: + if delete_cert: + # Delete the certificate + c = api.tm.sys.file.ssl_certs.ssl_cert.load( + name=self.params['name'], + partition=self.params['partition'] + ) + c.delete() + changed = True + + if delete_key: + # Delete the certificate key + k = self.api.tm.sys.file.ssl_keys.ssl_key.load( + name=self.params['name'], + partition=self.params['partition'] + ) + k.delete() + changed = True + return changed + + +def main(): + argument_spec = f5_argument_spec() + + meta_args = dict( + name=dict(type='str', required=True), + cert_content=dict(type='str', default=None), + cert_src=dict(type='path', default=None), + key_content=dict(type='str', default=None), + key_src=dict(type='path', default=None), + passphrase=dict(type='str', default=None, no_log=True) + ) + + argument_spec.update(meta_args) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + mutually_exclusive=[ + ['key_content', 'key_src'], + ['cert_content', 'cert_src'] + ] + ) + + try: + obj = BigIpSslCertificate(check_mode=module.check_mode, + **module.params) + result = obj.flush() + module.exit_json(**result) + except F5ModuleError as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.basic import * +from ansible.module_utils.f5 import * + +if __name__ == '__main__': + main() From 6a7358b14e66a92d96d67c2158409549737d5ddd Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Wed, 7 Sep 2016 15:40:17 -0700 Subject: [PATCH 2073/2522] Remove stderr=False from calls to exit_json. (#2879) --- commands/expect.py | 2 -- system/lvol.py | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/commands/expect.py b/commands/expect.py index a3d1b32d719..355f2cff480 100644 --- a/commands/expect.py +++ b/commands/expect.py @@ -167,7 +167,6 @@ def main(): cmd=args, stdout="skipped, since %s exists" % v, changed=False, - stderr=False, rc=0 ) @@ -181,7 +180,6 @@ def main(): cmd=args, stdout="skipped, since %s does not exist" % v, changed=False, - stderr=False, rc=0 ) diff --git a/system/lvol.py b/system/lvol.py index 817a4e66eab..978ce7d1c5f 100644 --- a/system/lvol.py +++ b/system/lvol.py @@ -276,7 +276,7 @@ def main(): if rc != 0: if state == 'absent': - module.exit_json(changed=False, stdout="Volume group %s does not exist." % vg, stderr=False) + module.exit_json(changed=False, stdout="Volume group %s does not exist." % vg) else: module.fail_json(msg="Volume group %s does not exist." % vg, rc=rc, err=err) @@ -290,7 +290,7 @@ def main(): if rc != 0: if state == 'absent': - module.exit_json(changed=False, stdout="Volume group %s does not exist." % vg, stderr=False) + module.exit_json(changed=False, stdout="Volume group %s does not exist." % vg) else: module.fail_json(msg="Volume group %s does not exist." % vg, rc=rc, err=err) From 64ace27be33d81f605e29400b71ff1406e08205f Mon Sep 17 00:00:00 2001 From: Steve Gargan Date: Thu, 8 Sep 2016 07:17:56 +0200 Subject: [PATCH 2074/2522] correctly iterate and return results of any service checks. (#2878) current implementation was breaking making the module unusable, changing to the list comprehension fixes this. Also default to seconds instead of throwing a exception when no duration units are supplied as this causes tests to fail --- clustering/consul.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/clustering/consul.py b/clustering/consul.py index 002aca3e7b6..b9cdfb09d8a 100644 --- a/clustering/consul.py +++ b/clustering/consul.py @@ -315,7 +315,7 @@ def add_service(module, service): service_id=result.id, service_name=result.name, service_port=result.port, - checks=map(lambda x: x.to_dict(), service.checks), + checks=[check.to_dict() for check in service.checks], tags=result.tags) @@ -484,8 +484,7 @@ def validate_duration(self, name, duration): if duration: duration_units = ['ns', 'us', 'ms', 's', 'm', 'h'] if not any((duration.endswith(suffix) for suffix in duration_units)): - raise Exception('Invalid %s %s you must specify units (%s)' % - (name, duration, ', '.join(duration_units))) + duration = "{}s".format(duration) return duration def register(self, consul_api): From 9a338b05eb52b13056366f0cafca759513110e32 Mon Sep 17 00:00:00 2001 From: John R Barker Date: Thu, 8 Sep 2016 10:54:21 +0100 Subject: [PATCH 2075/2522] Document a10_server new options (#2876) * Document write_config and validate_certs --- network/a10/a10_server.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/network/a10/a10_server.py b/network/a10/a10_server.py index 9716b43efbb..d06a2a661af 100644 --- a/network/a10/a10_server.py +++ b/network/a10/a10_server.py @@ -78,6 +78,25 @@ required: false default: present choices: ['present', 'absent'] + write_config: + description: + - If C(yes), any changes will cause a write of the running configuration + to non-volatile memory. This will save I(all) configuration changes, + including those that may have been made manually or through other modules, + so care should be taken when specifying C(yes). + required: false + version_added: 2.2 + default: "no" + choices: ["yes", "no"] + validate_certs: + description: + - If C(no), SSL certificates will not be validated. This should only be used + on personally controlled devices using self-signed certificates. + required: false + version_added: 2.2 + default: 'yes' + choices: ['yes', 'no'] + ''' EXAMPLES = ''' From b88fdde22c45a504619ff0cffa5b6da12fc30a01 Mon Sep 17 00:00:00 2001 From: johanwiren Date: Thu, 8 Sep 2016 18:01:27 +0200 Subject: [PATCH 2076/2522] Fix share aliases logic (#2862) (#2875) Fixes #2862 --- system/zfs.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/system/zfs.py b/system/zfs.py index fb987017d13..0d79569d77a 100644 --- a/system/zfs.py +++ b/system/zfs.py @@ -198,8 +198,9 @@ def get_current_properties(self): if source == 'local': properties[prop] = value # Add alias for enhanced sharing properties - properties['sharenfs'] = properties.get('share.nfs', None) - properties['sharesmb'] = properties.get('share.smb', None) + if self.enhanced_sharing: + properties['sharenfs'] = properties.get('share.nfs', None) + properties['sharesmb'] = properties.get('share.smb', None) return properties From 771e438bc67b0dfc810dd78aa55626a97e98d5f0 Mon Sep 17 00:00:00 2001 From: Greg DeKoenigsberg Date: Thu, 8 Sep 2016 12:32:30 -0400 Subject: [PATCH 2077/2522] Point to GUIDELINES.md --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c0714604f5f..60e850d6ed3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,9 +14,9 @@ Each module in Extras is maintained by the owner of that module; each module's o If you'd like to contribute a new module ======================================== -Ansible welcomes new modules. Please be certain that you've read the [module development guide and standards](http://docs.ansible.com/developing_modules.html) thoroughly before submitting your module. +Ansible welcomes new modules. Please be certain that you've read the [module maintainer guide and standards](./GUIDELINES.md) thoroughly before submitting your module. -Each new module requires two current module owners to approve a new module for inclusion. The Ansible community reviews new modules as often as possible, but please be patient; there are a lot of new module submissions in the pipeline, and it takes time to evaluate a new module for its adherence to module standards. +The Ansible community reviews new modules as often as possible, but please be patient; there are a lot of new module submissions in the pipeline, and it takes time to evaluate a new module for its adherence to module standards. Once your module is accepted, you become responsible for maintenance of that module, which means responding to pull requests and issues in a reasonably timely manner. From 43860de5c688c4cc2bfd7fe0b84e8149eecafbc8 Mon Sep 17 00:00:00 2001 From: Greg DeKoenigsberg Date: Thu, 8 Sep 2016 12:36:30 -0400 Subject: [PATCH 2078/2522] Update MAINTAINERS.md We could have deleted this, but just in case there are links to it from elsewhere, providing a redirect to the now authoritative GUIDELINES.md. --- MAINTAINERS.md | 61 +------------------------------------------------- 1 file changed, 1 insertion(+), 60 deletions(-) diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 9e1e53cfb08..c4370110e93 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -1,60 +1 @@ -# Module Maintainer Guidelines - -Thank you for being a maintainer of one of the modules in ansible-modules-extras! This guide provides module maintainers an overview of their responsibilities, resources for additional information, and links to helpful tools. - -In addition to the information below, module maintainers should be familiar with: -* General Ansible community development practices (http://docs.ansible.com/ansible/community.html) -* Documentation on module development (http://docs.ansible.com/ansible/developing_modules.html) -* Any namespace-specific module guidelines (identified as GUIDELINES.md in the appropriate file tree). - -*** - -# Maintainer Responsibilities - -When you contribute a new module to the ansible-modules-extras repository, you become the maintainer for that module once it has been merged. Maintainership empowers you with the authority to accept, reject, or request revisions to pull requests on your module -- but as they say, "with great power comes great responsibility." - -Maintainers of Ansible modules are expected to provide feedback, responses, or actions on pull requests or issues to the module(s) they maintain in a reasonably timely manner. - -The Ansible community hopes that you will find that maintaining your module is as rewarding for you as having the module is for the wider community. - -*** - -# Pull Requests and Issues - -## Pull Requests - -Module pull requests are located in the [ansible-modules-extras repository](https://github.com/ansible/ansible-modules-extras/pulls). - -Because of the high volume of pull requests, notification of PRs to specific modules are routed by an automated bot to the appropriate maintainer for handling. It is recommended that you set an appropriate notification process to receive notifications which mention your GitHub ID. - -## Issues - -Issues for modules, including bug reports, documentation bug reports, and feature requests, are tracked in the [ansible-modules-extras repository](https://github.com/ansible/ansible-modules-extras/issues). - -At this time, we do not have an automated process by which Issues are handled. If you are a maintainer of a specific module, it is recommended that you periodically search module issues for issues which mention your module's name (or some variation on that name), as well as setting an appropriate notification process for receiving notification of mentions of your GitHub ID. - -*** - -# Extras maintainers list - -The full list of maintainers for modules in ansible-modules-extras is located here: -https://github.com/ansible/ansibullbot/blob/master/MAINTAINERS-EXTRAS.txt - -## Changing Maintainership - -Communities change over time, and no one maintains a module forever. If you'd like to propose an additional maintainer for your module, please submit a PR to the maintainers file with the Github ID of the new maintainer. - -If you'd like to step down as a maintainer, please submit a PR to the maintainers file removing your Github ID from the module in question. If that would leave the module with no maintainers, put "ansible" as the maintainer. This will indicate that the module is temporarily without a maintainer, and the Ansible community team will search for a new maintainer. - -*** - -# Tools and other Resources - -## Useful tools -* https://ansible.sivel.net/pr/byfile.html -- a full list of all open Pull Requests, organized by file. -* https://github.com/sivel/ansible-testing -- these are the tests that run in Travis against all PRs for extras modules, so it's a good idea to run these tests locally first. - -## Other Resources - -* Module maintainer list: https://github.com/ansible/ansibullbot/blob/master/MAINTAINERS-EXTRAS.txt -* Ansibullbot: https://github.com/ansible/ansibullbot +Please refer to [GUIDELINES.md](./GUIDELINES.md) for the updated contributor guidelines. From 780e196c2f0ae4ebb4dc49864afd90f02d9d8c90 Mon Sep 17 00:00:00 2001 From: Robyn Bergeron Date: Thu, 8 Sep 2016 09:39:52 -0700 Subject: [PATCH 2079/2522] Updating GUIDELINES.md Updating info on how issues are routed via bot. --- GUIDELINES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GUIDELINES.md b/GUIDELINES.md index ddd918d4737..373796888ea 100644 --- a/GUIDELINES.md +++ b/GUIDELINES.md @@ -35,7 +35,7 @@ Because of the high volume of pull requests, notification of PRs to specific mod Issues for modules, including bug reports, documentation bug reports, and feature requests, are tracked in the [ansible-modules-extras repository](https://github.com/ansible/ansible-modules-extras/issues). -At this time, we do not have an automated process by which Issues are handled. If you are a maintainer of a specific module, it is recommended that you periodically search module issues for issues which mention your module's name (or some variation on that name), as well as setting an appropriate notification process for receiving notification of mentions of your GitHub ID. + Issues for modules are routed to their maintainers via an automated process. This process is still being refined, and currently depends upon the issue creator to provide adequate details (specifically, providing the proper module name) in order to route it correctly. If you are a maintainer of a specific module, it is recommended that you periodically search module issues for issues which mention your module's name (or some variation on that name), as well as setting an appropriate notification process for receiving notification of mentions of your GitHub ID. ## PR Workflow From f83aa9fff3574f621609e2bded2f604cabda38e1 Mon Sep 17 00:00:00 2001 From: Gilles Gagniard Date: Thu, 8 Sep 2016 21:12:20 +0200 Subject: [PATCH 2080/2522] GCE : Fix image family handling with libcloud > 0.20.1 (#2289) * fix image family handling with libcloud > 0.20.1 * add missing import --- cloud/google/gce_img.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/cloud/google/gce_img.py b/cloud/google/gce_img.py index bf3d9da5356..32c3d238ca3 100644 --- a/cloud/google/gce_img.py +++ b/cloud/google/gce_img.py @@ -38,6 +38,12 @@ - an optional description required: false default: null + family: + description: + - an optional family name + required: false + default: null + version_added: "2.2" source: description: - the source disk or the Google Cloud Storage URI to create the image from @@ -108,6 +114,7 @@ import sys try: + import libcloud from libcloud.compute.types import Provider from libcloud.compute.providers import get_driver from libcloud.common.google import GoogleBaseError @@ -128,6 +135,7 @@ def create_image(gce, name, module): zone = module.params.get('zone') desc = module.params.get('description') timeout = module.params.get('timeout') + family = module.params.get('family') if not source: module.fail_json(msg='Must supply a source', changed=False) @@ -147,10 +155,14 @@ def create_image(gce, name, module): except GoogleBaseError, e: module.fail_json(msg=str(e), changed=False) + gce_extra_args = {} + if family is not None: + gce_extra_args['family'] = family + old_timeout = gce.connection.timeout try: gce.connection.timeout = timeout - gce.ex_create_image(name, volume, desc, False) + gce.ex_create_image(name, volume, desc, use_existing=False, **gce_extra_args) return True except ResourceExistsError: return False @@ -175,6 +187,7 @@ def main(): module = AnsibleModule( argument_spec=dict( name=dict(required=True), + family=dict(), description=dict(), source=dict(), state=dict(default='present', choices=['present', 'absent']), @@ -193,8 +206,13 @@ def main(): name = module.params.get('name') state = module.params.get('state') + family = module.params.get('family') changed = False + if family is not None and hasattr(libcloud, '__version__') and libcloud.__version__ <= '0.20.1': + module.fail_json(msg="Apache Libcloud 1.0.0+ is required to use 'family' option", + changed=False) + # user wants to create an image. if state == 'present': changed = create_image(gce, name, module) From 6c6da8f86ee345ff681d33cbebea772d94ecbbd6 Mon Sep 17 00:00:00 2001 From: Nils Pascal Illenseer Date: Fri, 9 Sep 2016 14:57:36 +0200 Subject: [PATCH 2081/2522] Delete lxd container in state stopped (#2885) If a lxd container is stopped, do not stop it before deleting it. --- cloud/lxd/lxd_container.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index c28a6234e22..a92cdd7ce7e 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -364,7 +364,7 @@ def _restart_container(self): self.actions.append('restart') def _delete_container(self): - return self.client.do('DELETE', '/1.0/containers/{0}'.format(self.name)) + self.client.do('DELETE', '/1.0/containers/{0}'.format(self.name)) self.actions.append('delete') def _freeze_container(self): @@ -446,7 +446,8 @@ def _destroyed(self): if self.old_state != 'absent': if self.old_state == 'frozen': self._unfreeze_container() - self._stop_container() + if self.old_state != 'stopped': + self._stop_container() self._delete_container() def _frozen(self): From ac0e6d2c4d0c614378454ca03d06e54cb7c1afd2 Mon Sep 17 00:00:00 2001 From: sgujic Date: Fri, 9 Sep 2016 20:47:49 +0200 Subject: [PATCH 2082/2522] Create temporary file in a secure manner. (#2887) --- web_infrastructure/jenkins_plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web_infrastructure/jenkins_plugin.py b/web_infrastructure/jenkins_plugin.py index 4dc2c345a6b..266d7f49c77 100644 --- a/web_infrastructure/jenkins_plugin.py +++ b/web_infrastructure/jenkins_plugin.py @@ -564,7 +564,7 @@ def _download_updates(self): msg_exception="Updates download failed.") # Write the updates file - updates_file = tempfile.mktemp() + updates_file = tempfile.mkstemp() try: fd = open(updates_file, 'wb') @@ -641,7 +641,7 @@ def _download_plugin(self, plugin_url): def _write_file(self, f, data): # Store the plugin into a temp file and then move it - tmp_f = tempfile.mktemp() + tmp_f = tempfile.mkstemp() try: fd = open(tmp_f, 'wb') From eb6e0069e96c2308ad7f0c5c31e87ab8b3a7a637 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 10 Sep 2016 08:36:59 +0200 Subject: [PATCH 2083/2522] Start zypper in non-interactive mode (#2854) --- packaging/os/zypper_repository.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packaging/os/zypper_repository.py b/packaging/os/zypper_repository.py index bbed8143f3b..0737d3cc3c0 100644 --- a/packaging/os/zypper_repository.py +++ b/packaging/os/zypper_repository.py @@ -129,9 +129,17 @@ from distutils.version import LooseVersion +def _get_cmd(*args): + """Combines the non-interactive zypper command with arguments/subcommands""" + cmd = ['/usr/bin/zypper', '--quiet', '--non-interactive'] + cmd.extend(args) + + return cmd + + def _parse_repos(module): - """parses the output of zypper -x lr and return a parse repo dictionary""" - cmd = ['/usr/bin/zypper', '-x', 'lr'] + """parses the output of zypper --xmlout repos and return a parse repo dictionary""" + cmd = _get_cmd('--xmlout', 'repos') from xml.dom.minidom import parseString as parseXML rc, stdout, stderr = module.run_command(cmd, check_rc=False) @@ -207,7 +215,7 @@ def repo_exists(module, repodata, overwrite_multiple): def addmodify_repo(module, repodata, old_repos, zypper_version, warnings): "Adds the repo, removes old repos before, that would conflict." repo = repodata['url'] - cmd = ['/usr/bin/zypper', 'ar', '--check'] + cmd = _get_cmd('addrepo', '--check') if repodata['name']: cmd.extend(['--name', repodata['name']]) @@ -251,7 +259,7 @@ def addmodify_repo(module, repodata, old_repos, zypper_version, warnings): def remove_repo(module, repo): "Removes the repo." - cmd = ['/usr/bin/zypper', 'rr', repo] + cmd = _get_cmd('removerepo', repo) rc, stdout, stderr = module.run_command(cmd, check_rc=True) return rc, stdout, stderr @@ -265,7 +273,7 @@ def get_zypper_version(module): def runrefreshrepo(module, auto_import_keys=False, shortname=None): "Forces zypper to refresh repo metadata." - cmd = ['/usr/bin/zypper', 'refresh', '--force'] + cmd = _get_cmd('refresh', '--force') if auto_import_keys: cmd.append('--gpg-auto-import-keys') if shortname is not None: From 472f174693a05764c90586fa031691798e586c70 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 10 Sep 2016 09:07:09 +0200 Subject: [PATCH 2084/2522] jenkins_job: default state to present --- web_infrastructure/jenkins_job.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/web_infrastructure/jenkins_job.py b/web_infrastructure/jenkins_job.py index 71d584dd7ac..9600b6a06f9 100644 --- a/web_infrastructure/jenkins_job.py +++ b/web_infrastructure/jenkins_job.py @@ -23,7 +23,7 @@ - "python-jenkins >= 0.4.12" - "lxml >= 3.3.3" version_added: "2.2" -author: "Sergio Millan Rodriguez" +author: "Sergio Millan Rodriguez (@sermilrod)" options: config: description: @@ -44,7 +44,8 @@ state: description: - Attribute that specifies if the job has to be created or deleted. - required: true + required: false + default: present choices: ['present', 'absent'] token: description: @@ -67,7 +68,6 @@ config: "{{ lookup('file', 'templates/test.xml') }}" name: test password: admin - state: present enable: True url: "http://localhost:8080" user: admin @@ -77,7 +77,6 @@ config: "{{ lookup('template', 'templates/test.xml.j2') }}" name: test token: asdfasfasfasdfasdfadfasfasdfasdfc - state: present enable: yes url: "http://localhost:8080" user: admin @@ -102,7 +101,6 @@ - jenkins_job: name: test password: admin - state: present enable: False url: "http://localhost:8080" user: admin @@ -111,7 +109,6 @@ - jenkins_job: name: test token: asdfasfasfasdfasdfadfasfasdfasdfc - state: present enable: no url: "http://localhost:8080" user: admin @@ -303,7 +300,7 @@ def main(): config = dict(required=False), name = dict(required=True), password = dict(required=False, no_log=True), - state = dict(required=True, choices=['present', 'absent']), + state = dict(required=False, choices=['present', 'absent'], default="present"), enable = dict(required=False, type='bool'), token = dict(required=False, no_log=True), url = dict(required=False, default="http://localhost:8080"), From 407e19fe4f06a11348a04efb1223f38cb535688f Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 10 Sep 2016 16:56:09 +0200 Subject: [PATCH 2085/2522] jenkins_job: rename enable to enabled and mutually exclusive with config Jenkins stores the information about the state (disabled/enabled) in the config, which result in a race condition between `config` and `enabled` and we loose idempotency. It makes sense to define them mutually exclusive. Renamed `enable` to `enabled`. Ansible uses the name `enabled` in many modules, e.g. service as it indicates a state not an action. --- web_infrastructure/jenkins_job.py | 77 +++++++++++++++++++------------ 1 file changed, 47 insertions(+), 30 deletions(-) diff --git a/web_infrastructure/jenkins_job.py b/web_infrastructure/jenkins_job.py index 9600b6a06f9..ea33b729bce 100644 --- a/web_infrastructure/jenkins_job.py +++ b/web_infrastructure/jenkins_job.py @@ -27,11 +27,16 @@ options: config: description: - - config.xml file to use as job config within your Ansible repo. + - config in XML format. + - Required if job does not yet exist. + - Mututally exclusive with C(enabled). + - Considered if C(state=present). required: false - enable: + enabled: description: - - Action to take with the Jenkins job (enable/disable). + - Whether the job should be enabled or disabled. + - Mututally exclusive with C(config). + - Considered if C(state=present). required: false name: description: @@ -68,7 +73,6 @@ config: "{{ lookup('file', 'templates/test.xml') }}" name: test password: admin - enable: True url: "http://localhost:8080" user: admin @@ -77,7 +81,6 @@ config: "{{ lookup('template', 'templates/test.xml.j2') }}" name: test token: asdfasfasfasdfasdfadfasfasdfasdfc - enable: yes url: "http://localhost:8080" user: admin @@ -101,7 +104,7 @@ - jenkins_job: name: test password: admin - enable: False + enabled: false url: "http://localhost:8080" user: admin @@ -109,7 +112,7 @@ - jenkins_job: name: test token: asdfasfasfasdfasdfadfasfasdfasdfc - enable: no + enabled: false url: "http://localhost:8080" user: admin ''' @@ -146,12 +149,12 @@ python_lxml_installed = False class Jenkins: - def __init__(self, config, name, password, state, enable, token, url, user): + def __init__(self, config, name, password, state, enabled, token, url, user): self.config = config self.name = name self.password = password self.state = state - self.enable = enable + self.enabled = enabled self.token = token self.user = user self.jenkins_url = url @@ -195,29 +198,39 @@ def get_config(self): return job_config_to_string(self.config) def configuration_changed(self): + # config is optional, if not provided we keep the current config as is + if self.config is None: + return False + changed = False config_file = self.get_config() machine_file = job_config_to_string(self.server.get_job_config(self.name).encode('utf-8')) - if not machine_file == config_file: + if machine_file != config_file: changed = True - return changed def update_job(self, module): + if self.config is None and self.enabled is None: + module.fail_json(msg='one of the following params is required on state=present: config,enabled') + if not self.job_exists(module): self.create_job(module) else: self.reconfig_job(module) def state_changed(self, status): + # Keep in current state if enabled arg_spec is not given + if self.enabled is None: + return False + changed = False - if ( (self.enable == False and status != "disabled") or (self.enable == True and status == "disabled") ): + if ( (self.enabled == False and status != "disabled") or (self.enabled == True and status == "disabled") ): changed = True return changed def change_state(self): - if self.enable == False: + if self.enabled == False: self.server.disable_job(self.name) else: self.server.enable_job(self.name) @@ -226,17 +239,18 @@ def reconfig_job(self, module): changed = False try: status = self.get_job_status(module) - if self.enable == True: - if ( self.configuration_changed() or self.state_changed(status) ): - changed = True - if not module.check_mode: - self.server.reconfig_job(self.name, self.get_config()) - self.change_state() - else: - if self.state_changed(status): - changed = True - if not module.check_mode: - self.change_state() + + # Handle job config + if self.configuration_changed(): + changed = True + if not module.check_mode: + self.server.reconfig_job(self.name, self.get_config()) + + # Handle job disable/enable + elif self.state_changed(status): + changed = True + if not module.check_mode: + self.change_state() except Exception: e = get_exception() @@ -245,6 +259,10 @@ def reconfig_job(self, module): module.exit_json(changed=changed, name=self.name, state=self.state, url=self.jenkins_url) def create_job(self, module): + + if self.config is None: + module.fail_json(msg='missing required param: config') + changed = False try: changed = True @@ -288,7 +306,7 @@ def jenkins_builder(module): module.params.get('name'), module.params.get('password'), module.params.get('state'), - module.params.get('enable'), + module.params.get('enabled'), module.params.get('token'), module.params.get('url'), module.params.get('user') @@ -301,16 +319,15 @@ def main(): name = dict(required=True), password = dict(required=False, no_log=True), state = dict(required=False, choices=['present', 'absent'], default="present"), - enable = dict(required=False, type='bool'), + enabled = dict(required=False, type='bool'), token = dict(required=False, no_log=True), url = dict(required=False, default="http://localhost:8080"), user = dict(required=False) ), - required_if = [ - ('state', 'present', ['enable']), - ('enable', True, ['config']) + mutually_exclusive = [ + ['password', 'token'], + ['config', 'enabled'], ], - mutually_exclusive = [['password', 'token']], supports_check_mode=True, ) From 66e69b3ad3c67cc9d0e97821463e0aae0b0ee3c5 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 10 Sep 2016 17:21:42 +0200 Subject: [PATCH 2086/2522] jenkins_job: implement diff support --- web_infrastructure/jenkins_job.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/web_infrastructure/jenkins_job.py b/web_infrastructure/jenkins_job.py index ea33b729bce..5ef3e159c1f 100644 --- a/web_infrastructure/jenkins_job.py +++ b/web_infrastructure/jenkins_job.py @@ -159,6 +159,10 @@ def __init__(self, config, name, password, state, enabled, token, url, user): self.user = user self.jenkins_url = url self.server = self.get_jenkins_connection() + self.diff = { + 'before': "", + 'after': "", + } def get_jenkins_connection(self): try: @@ -204,7 +208,9 @@ def configuration_changed(self): changed = False config_file = self.get_config() + self.diff['after'] = config_file machine_file = job_config_to_string(self.server.get_job_config(self.name).encode('utf-8')) + self.diff['before'] = machine_file if machine_file != config_file: changed = True return changed @@ -256,29 +262,31 @@ def reconfig_job(self, module): e = get_exception() module.fail_json(msg='Unable to reconfigure job, %s for %s' % (str(e), self.jenkins_url)) - module.exit_json(changed=changed, name=self.name, state=self.state, url=self.jenkins_url) + module.exit_json(changed=changed, name=self.name, state=self.state, url=self.jenkins_url, diff=self.diff) def create_job(self, module): if self.config is None: module.fail_json(msg='missing required param: config') - changed = False + changed = True try: - changed = True + config_file = self.get_config() + self.diff['after'] = config_file if not module.check_mode: - self.server.create_job(self.name, self.get_config()) + self.server.create_job(self.name, config_file) self.change_state() except Exception: e = get_exception() module.fail_json(msg='Unable to create job, %s for %s' % (str(e), self.jenkins_url)) - module.exit_json(changed=changed, name=self.name, state=self.state, url=self.jenkins_url) + module.exit_json(changed=changed, name=self.name, state=self.state, url=self.jenkins_url, diff=self.diff) def delete_job(self, module): changed = False if self.job_exists(module): changed = True + self.diff['before'] = job_config_to_string(self.server.get_job_config(self.name).encode('utf-8')) if not module.check_mode: try: self.server.delete_job(self.name) @@ -286,7 +294,7 @@ def delete_job(self, module): e = get_exception() module.fail_json(msg='Unable to delete job, %s for %s' % (str(e), self.jenkins_url)) - module.exit_json(changed=changed, name=self.name, state=self.state, url=self.jenkins_url) + module.exit_json(changed=changed, name=self.name, state=self.state, url=self.jenkins_url, diff=self.diff) def test_dependencies(module): if not python_jenkins_installed: From 79efc2c70f396345ff8120bef28f6fe58e990a54 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 10 Sep 2016 18:26:30 +0200 Subject: [PATCH 2087/2522] jenkins_job: refactoring --- web_infrastructure/jenkins_job.py | 184 ++++++++++++++++-------------- 1 file changed, 97 insertions(+), 87 deletions(-) diff --git a/web_infrastructure/jenkins_job.py b/web_infrastructure/jenkins_job.py index 5ef3e159c1f..ee8b1745cb7 100644 --- a/web_infrastructure/jenkins_job.py +++ b/web_infrastructure/jenkins_job.py @@ -18,7 +18,7 @@ module: jenkins_job short_description: Manage jenkins jobs description: - - Manage Jenkins jobs by using Jenkins REST API + - Manage Jenkins jobs by using Jenkins REST API. requirements: - "python-jenkins >= 0.4.12" - "lxml >= 3.3.3" @@ -129,6 +129,16 @@ returned: success type: string sample: present +enabled: + description: Whether the jenkins job is enabled or not. + returned: success + type: bool + sample: true +user: + description: User used for authentication. + returned: success + type: string + sample: admin url: description: Url to connect to the Jenkins server. returned: success @@ -148,20 +158,30 @@ except ImportError: python_lxml_installed = False -class Jenkins: - def __init__(self, config, name, password, state, enabled, token, url, user): - self.config = config - self.name = name - self.password = password - self.state = state - self.enabled = enabled - self.token = token - self.user = user - self.jenkins_url = url +class JenkinsJob: + def __init__(self, module): + self.module = module + + self.config = module.params.get('config') + self.name = module.params.get('name') + self.password = module.params.get('password') + self.state = module.params.get('state') + self.enabled = module.params.get('enabled') + self.token = module.params.get('token') + self.user = module.params.get('user') + self.jenkins_url = module.params.get('url') self.server = self.get_jenkins_connection() - self.diff = { - 'before': "", - 'after': "", + + self.result = { + 'changed': False, + 'url': self.jenkins_url, + 'name': self.name, + 'user': self.user, + 'state': self.state, + 'diff': { + 'before': "", + 'after': "" + } } def get_jenkins_connection(self): @@ -176,125 +196,119 @@ def get_jenkins_connection(self): return jenkins.Jenkins(self.jenkins_url) except Exception: e = get_exception() - module.fail_json(msg='Unable to connect to Jenkins server, %s' % str(e)) + self.module.fail_json(msg='Unable to connect to Jenkins server, %s' % str(e)) - def get_job_status(self, module): + def get_job_status(self): try: return self.server.get_job_info(self.name)['color'].encode('utf-8') except Exception: e = get_exception() - module.fail_json(msg='Unable to fetch job information, %s' % str(e)) + self.module.fail_json(msg='Unable to fetch job information, %s' % str(e)) - def job_exists(self, module): + def job_exists(self): try: return bool(self.server.job_exists(self.name)) except Exception: e = get_exception() - module.fail_json(msg='Unable to validate if job exists, %s for %s' % (str(e), self.jenkins_url)) - - def build(self, module): - if self.state == 'present': - self.update_job(module) - else: - self.delete_job(module) + self.module.fail_json(msg='Unable to validate if job exists, %s for %s' % (str(e), self.jenkins_url)) def get_config(self): return job_config_to_string(self.config) - def configuration_changed(self): + def get_current_config(self): + return job_config_to_string(self.server.get_job_config(self.name).encode('utf-8')) + + def has_config_changed(self): # config is optional, if not provided we keep the current config as is if self.config is None: return False - changed = False config_file = self.get_config() - self.diff['after'] = config_file - machine_file = job_config_to_string(self.server.get_job_config(self.name).encode('utf-8')) - self.diff['before'] = machine_file + machine_file = self.get_current_config() + + self.result['diff']['after'] = config_file + self.result['diff']['before'] = machine_file + if machine_file != config_file: - changed = True - return changed + return True + return False - def update_job(self, module): + def present_job(self): if self.config is None and self.enabled is None: module.fail_json(msg='one of the following params is required on state=present: config,enabled') - if not self.job_exists(module): - self.create_job(module) + if not self.job_exists(): + self.create_job() else: - self.reconfig_job(module) + self.update_job() - def state_changed(self, status): + def has_state_changed(self, status): # Keep in current state if enabled arg_spec is not given if self.enabled is None: return False - changed = False if ( (self.enabled == False and status != "disabled") or (self.enabled == True and status == "disabled") ): - changed = True - - return changed + return True + return False - def change_state(self): + def switch_state(self): if self.enabled == False: self.server.disable_job(self.name) else: self.server.enable_job(self.name) - def reconfig_job(self, module): - changed = False + def update_job(self): try: - status = self.get_job_status(module) + status = self.get_job_status() # Handle job config - if self.configuration_changed(): - changed = True - if not module.check_mode: + if self.has_config_changed(): + self.result['changed'] = True + if not self.module.check_mode: self.server.reconfig_job(self.name, self.get_config()) # Handle job disable/enable - elif self.state_changed(status): - changed = True - if not module.check_mode: - self.change_state() + elif self.has_state_changed(status): + self.result['changed'] = True + if not self.module.check_mode: + self.switch_state() except Exception: e = get_exception() - module.fail_json(msg='Unable to reconfigure job, %s for %s' % (str(e), self.jenkins_url)) - - module.exit_json(changed=changed, name=self.name, state=self.state, url=self.jenkins_url, diff=self.diff) - - def create_job(self, module): + self.module.fail_json(msg='Unable to reconfigure job, %s for %s' % (str(e), self.jenkins_url)) + def create_job(self): if self.config is None: - module.fail_json(msg='missing required param: config') + self.module.fail_json(msg='missing required param: config') - changed = True + self.result['changed'] = True try: config_file = self.get_config() - self.diff['after'] = config_file - if not module.check_mode: + self.result['diff']['after'] = config_file + if not self.module.check_mode: self.server.create_job(self.name, config_file) - self.change_state() except Exception: e = get_exception() - module.fail_json(msg='Unable to create job, %s for %s' % (str(e), self.jenkins_url)) - - module.exit_json(changed=changed, name=self.name, state=self.state, url=self.jenkins_url, diff=self.diff) + self.module.fail_json(msg='Unable to create job, %s for %s' % (str(e), self.jenkins_url)) - def delete_job(self, module): - changed = False - if self.job_exists(module): - changed = True - self.diff['before'] = job_config_to_string(self.server.get_job_config(self.name).encode('utf-8')) - if not module.check_mode: + def absent_job(self): + if self.job_exists(): + self.result['changed'] = True + self.result['diff']['before'] = self.get_current_config() + if not self.module.check_mode: try: self.server.delete_job(self.name) except Exception: e = get_exception() - module.fail_json(msg='Unable to delete job, %s for %s' % (str(e), self.jenkins_url)) + self.module.fail_json(msg='Unable to delete job, %s for %s' % (str(e), self.jenkins_url)) - module.exit_json(changed=changed, name=self.name, state=self.state, url=self.jenkins_url, diff=self.diff) + def get_result(self): + result = self.result + if self.job_exists(): + result['enabled'] = self.get_job_status() != "disabled" + else: + result['enabled'] = None + return result def test_dependencies(module): if not python_jenkins_installed: @@ -308,18 +322,6 @@ def test_dependencies(module): def job_config_to_string(xml_str): return ET.tostring(ET.fromstring(xml_str)) -def jenkins_builder(module): - return Jenkins( - module.params.get('config'), - module.params.get('name'), - module.params.get('password'), - module.params.get('state'), - module.params.get('enabled'), - module.params.get('token'), - module.params.get('url'), - module.params.get('user') - ) - def main(): module = AnsibleModule( argument_spec = dict( @@ -340,8 +342,16 @@ def main(): ) test_dependencies(module) - jenkins = jenkins_builder(module) - jenkins.build(module) + jenkins_job = JenkinsJob(module) + + if module.params.get('state') == "present": + jenkins_job.present_job() + else: + jenkins_job.absent_job() + + result = jenkins_job.get_result() + module.exit_json(**result) + from ansible.module_utils.basic import * if __name__ == '__main__': From 061c6517eb50cc4f33bb8d1817948b171701c0a0 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Mon, 12 Sep 2016 10:25:28 +0200 Subject: [PATCH 2088/2522] Fix one character typo in my previous PR for debconf module (#2899) Small oops in the previous PR #2530 commit. Instead of `diff_dict` it slipped through as `diffdict`. Please merge and sorry. --- system/debconf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/debconf.py b/system/debconf.py index 4aa512b4a83..05e545a7ed6 100644 --- a/system/debconf.py +++ b/system/debconf.py @@ -164,7 +164,7 @@ def main(): if module._diff: after = prev.copy() after.update(curr) - diffdict = {'before': prev, 'after': after} + diff_dict = {'before': prev, 'after': after} else: diff_dict = {} From 67a1bebbd36a2f6daa418b72687ea0f883165f59 Mon Sep 17 00:00:00 2001 From: Robin Schneider Date: Mon, 12 Sep 2016 11:27:45 +0200 Subject: [PATCH 2089/2522] Use addresses and names reserved for documentation (#2894) Trying to preserve the meaning of the examples. Related to: https://github.com/ansible/ansible/issues/17479 --- cloud/amazon/ec2_eni.py | 2 +- cloud/amazon/ec2_vpc_subnet.py | 2 +- cloud/azure/azure_rm_deployment.py | 2 +- cloud/cloudstack/cs_instance.py | 2 +- cloud/google/gcdns_record.py | 22 ++++++++--------- cloud/misc/ovirt.py | 4 +-- cloud/misc/rhevm.py | 6 ++--- cloud/openstack/os_port_facts.py | 2 +- cloud/rackspace/rax_mon_entity.py | 4 +-- cloud/vmware/vca_fw.py | 6 ++--- cloud/vmware/vca_nat.py | 8 +++--- cloud/vmware/vmware_guest.py | 4 +-- database/mysql/mysql_replication.py | 4 +-- files/blockinfile.py | 2 +- network/cloudflare_dns.py | 2 +- network/dnsmadeeasy.py | 2 +- network/f5/bigip_device_dns.py | 6 ++--- network/f5/bigip_device_ntp.py | 4 +-- network/f5/bigip_device_sshd.py | 2 +- network/f5/bigip_gtm_virtual_server.py | 2 +- network/f5/bigip_gtm_wide_ip.py | 2 +- network/f5/bigip_selfip.py | 2 +- network/illumos/dladm_vnic.py | 4 +-- network/nmcli.py | 34 +++++++++++++------------- network/openvswitch_port.py | 4 +-- network/wakeonlan.py | 8 +++--- system/firewalld.py | 2 +- system/gluster_volume.py | 4 +-- univention/udm_dns_record.py | 4 +-- univention/udm_dns_zone.py | 2 +- 30 files changed, 77 insertions(+), 77 deletions(-) diff --git a/cloud/amazon/ec2_eni.py b/cloud/amazon/ec2_eni.py index ac05ba43a39..79d44f9d46c 100644 --- a/cloud/amazon/ec2_eni.py +++ b/cloud/amazon/ec2_eni.py @@ -198,7 +198,7 @@ mac_address: description: interface's physical address type: string - sample: "06:9a:27:6a:6f:99" + sample: "00:00:5E:00:53:23" owner_id: description: aws account id type: string diff --git a/cloud/amazon/ec2_vpc_subnet.py b/cloud/amazon/ec2_vpc_subnet.py index 729357d5020..6e7c3e7d430 100644 --- a/cloud/amazon/ec2_vpc_subnet.py +++ b/cloud/amazon/ec2_vpc_subnet.py @@ -29,7 +29,7 @@ default: null cidr: description: - - "The CIDR block for the subnet. E.g. 10.0.0.0/16. Only required when state=present." + - "The CIDR block for the subnet. E.g. 192.0.2.0/24. Only required when state=present." required: false default: null tags: diff --git a/cloud/azure/azure_rm_deployment.py b/cloud/azure/azure_rm_deployment.py index b908aa48936..b9986207ab6 100644 --- a/cloud/azure/azure_rm_deployment.py +++ b/cloud/azure/azure_rm_deployment.py @@ -230,7 +230,7 @@ imageOffer: "UbuntuServer" OSDiskName: "osdiskforlinuxsimple" nicName: "myVMNic" - addressPrefix: "10.0.0.0/16" + addressPrefix: "192.0.2.0/24" subnetName: "Subnet" subnetPrefix: "10.0.0.0/24" storageAccountType: "Standard_LRS" diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 03c703c4782..49fb21329c3 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -263,7 +263,7 @@ service_offering: Tiny ip_to_networks: - {'network': NetworkA, 'ip': '10.1.1.1'} - - {'network': NetworkB, 'ip': '192.168.1.1'} + - {'network': NetworkB, 'ip': '192.0.2.1'} # Ensure an instance is stopped - local_action: cs_instance name=web-vm-1 state=stopped diff --git a/cloud/google/gcdns_record.py b/cloud/google/gcdns_record.py index ebccfca5dcb..949c5d19dac 100644 --- a/cloud/google/gcdns_record.py +++ b/cloud/google/gcdns_record.py @@ -193,10 +193,10 @@ zone_id: 'example-com' type: A values: - - '10.1.2.3' + - '192.0.2.23' - '10.4.5.6' - - '10.7.8.9' - - '192.168.5.10' + - '198.51.100.5' + - '203.0.113.10' # Change the value of an existing record with multiple values. - gcdns_record: @@ -205,10 +205,10 @@ type: A overwrite: true values: # WARNING: All values in a record will be replaced - - '10.1.2.3' - - '10.5.5.7' # The changed record - - '10.7.8.9' - - '192.168.5.10' + - '192.0.2.23' + - '192.0.2.42' # The changed record + - '198.51.100.5' + - '203.0.113.10' # Safely remove a multi-line record. - gcdns_record: @@ -217,10 +217,10 @@ state: absent type: A values: # NOTE: All of the values must match exactly - - '10.1.2.3' - - '10.5.5.7' - - '10.7.8.9' - - '192.168.5.10' + - '192.0.2.23' + - '192.0.2.42' + - '198.51.100.5' + - '203.0.113.10' # Unconditionally remove a record. - gcdns_record: diff --git a/cloud/misc/ovirt.py b/cloud/misc/ovirt.py index f4f77ca8ce8..8585dfb6b81 100644 --- a/cloud/misc/ovirt.py +++ b/cloud/misc/ovirt.py @@ -256,9 +256,9 @@ url: https://ovirt.example.com hostname: testansible domain: ansible.local - ip: 192.168.1.100 + ip: 192.0.2.100 netmask: 255.255.255.0 - gateway: 192.168.1.1 + gateway: 192.0.2.1 rootpw: bigsecret ''' diff --git a/cloud/misc/rhevm.py b/cloud/misc/rhevm.py index 523f6f6c0b3..9d12a802afb 100644 --- a/cloud/misc/rhevm.py +++ b/cloud/misc/rhevm.py @@ -171,9 +171,9 @@ "size": 40 } ], - "eth0": "00:1b:4a:1f:de:f4", - "eth1": "00:1b:4a:1f:de:f5", - "eth2": "00:1b:4a:1f:de:f6", + "eth0": "00:00:5E:00:53:00", + "eth1": "00:00:5E:00:53:01", + "eth2": "00:00:5E:00:53:02", "exists": true, "failed": false, "ifaces": [ diff --git a/cloud/openstack/os_port_facts.py b/cloud/openstack/os_port_facts.py index c987fed0c3c..e3048211877 100644 --- a/cloud/openstack/os_port_facts.py +++ b/cloud/openstack/os_port_facts.py @@ -162,7 +162,7 @@ description: The MAC address. returned: success type: string - sample: "fa:16:30:5f:10:f1" + sample: "00:00:5E:00:53:42" name: description: The port name. returned: success diff --git a/cloud/rackspace/rax_mon_entity.py b/cloud/rackspace/rax_mon_entity.py index f5f142d2165..7369aaafa3b 100644 --- a/cloud/rackspace/rax_mon_entity.py +++ b/cloud/rackspace/rax_mon_entity.py @@ -68,8 +68,8 @@ state: present label: my_entity named_ip_addresses: - web_box: 192.168.0.10 - db_box: 192.168.0.11 + web_box: 192.0.2.4 + db_box: 192.0.2.5 meta: hurf: durf register: the_entity diff --git a/cloud/vmware/vca_fw.py b/cloud/vmware/vca_fw.py index 617430abf25..17cc093eb56 100644 --- a/cloud/vmware/vca_fw.py +++ b/cloud/vmware/vca_fw.py @@ -49,12 +49,12 @@ fw_rules: - description: "ben testing" source_ip: "Any" - dest_ip: 192.168.2.11 + dest_ip: 192.0.2.23 - description: "ben testing 2" - source_ip: 192.168.2.100 + source_ip: 192.0.2.50 source_port: "Any" dest_port: "22" - dest_ip: 192.168.2.13 + dest_ip: 192.0.2.101 is_enable: "true" enable_logging: "false" protocol: "Tcp" diff --git a/cloud/vmware/vca_nat.py b/cloud/vmware/vca_nat.py index 7dfa0cd3a67..3381b3ced20 100644 --- a/cloud/vmware/vca_nat.py +++ b/cloud/vmware/vca_nat.py @@ -53,8 +53,8 @@ state: 'present' nat_rules: - rule_type: SNAT - original_ip: 192.168.2.10 - translated_ip: 107.189.95.208 + original_ip: 192.0.2.42 + translated_ip: 203.0.113.23 #example for a DNAT - hosts: localhost @@ -66,9 +66,9 @@ state: 'present' nat_rules: - rule_type: DNAT - original_ip: 107.189.95.208 + original_ip: 203.0.113.23 original_port: 22 - translated_ip: 192.168.2.10 + translated_ip: 192.0.2.42 translated_port: 22 ''' diff --git a/cloud/vmware/vmware_guest.py b/cloud/vmware/vmware_guest.py index da6511e1398..43082e83343 100644 --- a/cloud/vmware/vmware_guest.py +++ b/cloud/vmware/vmware_guest.py @@ -97,7 +97,7 @@ - name: create the VM vmware_guest: validate_certs: False - hostname: 192.168.1.209 + hostname: 192.0.2.44 username: administrator@vsphere.local password: vmware name: testvm_2 @@ -117,7 +117,7 @@ osid: centos64guest scsi: paravirtual datacenter: datacenter1 - esxi_hostname: 192.168.1.117 + esxi_hostname: 192.0.2.117 template: template_el7 wait_for_ip_address: yes register: deploy diff --git a/database/mysql/mysql_replication.py b/database/mysql/mysql_replication.py index 8bc964cfdda..a7f366e9db8 100644 --- a/database/mysql/mysql_replication.py +++ b/database/mysql/mysql_replication.py @@ -108,8 +108,8 @@ # Get master binlog file name and binlog position - mysql_replication: mode=getmaster -# Change master to master server 192.168.1.1 and use binary log 'mysql-bin.000009' with position 4578 -- mysql_replication: mode=changemaster master_host=192.168.1.1 master_log_file=mysql-bin.000009 master_log_pos=4578 +# Change master to master server 192.0.2.1 and use binary log 'mysql-bin.000009' with position 4578 +- mysql_replication: mode=changemaster master_host=192.0.2.1 master_log_file=mysql-bin.000009 master_log_pos=4578 # Check slave status using port 3308 - mysql_replication: mode=getslave login_host=ansible.example.com login_port=3308 diff --git a/files/blockinfile.py b/files/blockinfile.py index a58e446bf7f..7b25101242f 100755 --- a/files/blockinfile.py +++ b/files/blockinfile.py @@ -116,7 +116,7 @@ dest: /etc/network/interfaces block: | iface eth0 inet static - address 192.168.0.1 + address 192.0.2.23 netmask 255.255.255.0 - name: insert/update HTML surrounded by custom markers after line diff --git a/network/cloudflare_dns.py b/network/cloudflare_dns.py index 0756d5d8ffc..71cfab22a49 100644 --- a/network/cloudflare_dns.py +++ b/network/cloudflare_dns.py @@ -188,7 +188,7 @@ description: the record content (details depend on record type) returned: success type: string - sample: 192.168.100.20 + sample: 192.0.2.91 created_on: description: the record creation date returned: success diff --git a/network/dnsmadeeasy.py b/network/dnsmadeeasy.py index db819b67662..3f22c3ca0a6 100644 --- a/network/dnsmadeeasy.py +++ b/network/dnsmadeeasy.py @@ -99,7 +99,7 @@ - dnsmadeeasy: account_key=key account_secret=secret domain=my.com state=present record_name="test" record_type="A" record_value="127.0.0.1" # update the previously created record -- dnsmadeeasy: account_key=key account_secret=secret domain=my.com state=present record_name="test" record_value="192.168.0.1" +- dnsmadeeasy: account_key=key account_secret=secret domain=my.com state=present record_name="test" record_value="192.0.2.23" # fetch a specific record - dnsmadeeasy: account_key=key account_secret=secret domain=my.com state=present record_name="test" diff --git a/network/f5/bigip_device_dns.py b/network/f5/bigip_device_dns.py index c0625abd24a..c469fc4bffa 100644 --- a/network/f5/bigip_device_dns.py +++ b/network/f5/bigip_device_dns.py @@ -98,17 +98,17 @@ description: List of name servers that were added or removed returned: changed type: list - sample: "['192.168.1.10', '172.17.12.10']" + sample: "['192.0.2.10', '172.17.12.10']" forwarders: description: List of forwarders that were added or removed returned: changed type: list - sample: "['192.168.1.10', '172.17.12.10']" + sample: "['192.0.2.10', '172.17.12.10']" search: description: List of search domains that were added or removed returned: changed type: list - sample: "['192.168.1.10', '172.17.12.10']" + sample: "['192.0.2.10', '172.17.12.10']" ip_version: description: IP version that was set that DNS will specify IP addresses in returned: changed diff --git a/network/f5/bigip_device_ntp.py b/network/f5/bigip_device_ntp.py index 2c591ebacf6..6dab16a3cb0 100644 --- a/network/f5/bigip_device_ntp.py +++ b/network/f5/bigip_device_ntp.py @@ -60,7 +60,7 @@ - name: Set NTP server bigip_device_ntp: ntp_servers: - - "192.168.10.12" + - "192.0.2.23" password: "secret" server: "lb.mydomain.com" user: "admin" @@ -82,7 +82,7 @@ description: The NTP servers that were set on the device returned: changed type: list - sample: ["192.168.10.10", "172.27.10.10"] + sample: ["192.0.2.23", "192.0.2.42"] timezone: description: The timezone that was set on the device returned: changed diff --git a/network/f5/bigip_device_sshd.py b/network/f5/bigip_device_sshd.py index fd27127f897..e7a87a4e084 100644 --- a/network/f5/bigip_device_sshd.py +++ b/network/f5/bigip_device_sshd.py @@ -122,7 +122,7 @@ system. returned: changed type: list - sample: "192.168.*.*" + sample: "192.0.2.*" banner: description: Whether the banner is enabled or not. returned: changed diff --git a/network/f5/bigip_gtm_virtual_server.py b/network/f5/bigip_gtm_virtual_server.py index 44da600d0af..079709c1b33 100644 --- a/network/f5/bigip_gtm_virtual_server.py +++ b/network/f5/bigip_gtm_virtual_server.py @@ -69,7 +69,7 @@ - name: Enable virtual server local_action: > bigip_gtm_virtual_server - server=192.168.0.1 + server=192.0.2.1 user=admin password=mysecret virtual_server_name=myname diff --git a/network/f5/bigip_gtm_wide_ip.py b/network/f5/bigip_gtm_wide_ip.py index d9e5fdc05c3..19292783bcd 100644 --- a/network/f5/bigip_gtm_wide_ip.py +++ b/network/f5/bigip_gtm_wide_ip.py @@ -57,7 +57,7 @@ - name: Set lb method local_action: > bigip_gtm_wide_ip - server=192.168.0.1 + server=192.0.2.1 user=admin password=mysecret lb_method=round_robin diff --git a/network/f5/bigip_selfip.py b/network/f5/bigip_selfip.py index abdae4fb997..75ed6a09c0a 100644 --- a/network/f5/bigip_selfip.py +++ b/network/f5/bigip_selfip.py @@ -170,7 +170,7 @@ description: The address for the Self IP returned: created type: string - sample: "192.168.10.10" + sample: "192.0.2.10" name: description: The name of the Self IP returned: diff --git a/network/illumos/dladm_vnic.py b/network/illumos/dladm_vnic.py index b57edc00a9c..e47b98b97af 100644 --- a/network/illumos/dladm_vnic.py +++ b/network/illumos/dladm_vnic.py @@ -69,7 +69,7 @@ dladm_vnic: name=vnic0 link=bnx0 state=present # Create VNIC with specified MAC and VLAN tag over 'aggr0' -dladm_vnic: name=vnic1 link=aggr0 mac=2:33:af:12:ab:cd vlan=4 +dladm_vnic: name=vnic1 link=aggr0 mac=00:00:5E:00:53:23 vlan=4 # Remove 'vnic0' VNIC dladm_vnic: name=vnic0 link=bnx0 state=absent @@ -100,7 +100,7 @@ description: MAC address to use for VNIC returned: if mac is specified type: string - sample: "00:aa:bc:fe:11:22" + sample: "00:00:5E:00:53:42" vlan: description: VLAN to use for VNIC returned: success diff --git a/network/nmcli.py b/network/nmcli.py index 64ee02b09c5..5e729af7866 100644 --- a/network/nmcli.py +++ b/network/nmcli.py @@ -73,16 +73,16 @@ required: False default: None description: - - 'The IPv4 address to this interface using this format ie: "192.168.1.24/24"' + - 'The IPv4 address to this interface using this format ie: "192.0.2.24/24"' gw4: required: False description: - - 'The IPv4 gateway for this interface using this format ie: "192.168.100.1"' + - 'The IPv4 gateway for this interface using this format ie: "192.0.2.1"' dns4: required: False default: None description: - - 'A list of upto 3 dns servers, ipv4 format e.g. To add two IPv4 DNS server addresses: ["8.8.8.8 8.8.4.4"]' + - 'A list of upto 3 dns servers, ipv4 format e.g. To add two IPv4 DNS server addresses: ["192.0.2.53", "198.51.100.53"]' ip6: required: False default: None @@ -228,9 +228,9 @@ ```yml --- #devops_os_define_network -storage_gw: "192.168.0.254" -external_gw: "10.10.0.254" -tenant_gw: "172.100.0.254" +storage_gw: "192.0.2.254" +external_gw: "198.51.100.254" +tenant_gw: "203.0.113.254" #Team vars nmcli_team: @@ -265,9 +265,9 @@ ### host_vars ```yml --- -storage_ip: "192.168.160.21/23" -external_ip: "10.10.152.21/21" -tenant_ip: "192.168.200.21/23" +storage_ip: "192.0.2.91/23" +external_ip: "198.51.100.23/21" +tenant_ip: "203.0.113.77/23" ``` @@ -346,16 +346,16 @@ - { conn_name: 'team-p2p2'} ``` # To add an Ethernet connection with static IP configuration, issue a command as follows -- nmcli: conn_name=my-eth1 ifname=eth1 type=ethernet ip4=192.168.100.100/24 gw4=192.168.100.1 state=present +- nmcli: conn_name=my-eth1 ifname=eth1 type=ethernet ip4=192.0.2.100/24 gw4=192.0.2.1 state=present # To add an Team connection with static IP configuration, issue a command as follows -- nmcli: conn_name=my-team1 ifname=my-team1 type=team ip4=192.168.100.100/24 gw4=192.168.100.1 state=present autoconnect=yes +- nmcli: conn_name=my-team1 ifname=my-team1 type=team ip4=192.0.2.100/24 gw4=192.0.2.1 state=present autoconnect=yes # Optionally, at the same time specify IPv6 addresses for the device as follows: -- nmcli: conn_name=my-eth1 ifname=eth1 type=ethernet ip4=192.168.100.100/24 gw4=192.168.100.1 ip6=abbe::cafe gw6=2001:db8::1 state=present +- nmcli: conn_name=my-eth1 ifname=eth1 type=ethernet ip4=192.0.2.100/24 gw4=192.0.2.1 ip6=2001:db8::cafe gw6=2001:db8::1 state=present # To add two IPv4 DNS server addresses: --nmcli: conn_name=my-eth1 dns4=["8.8.8.8", "8.8.4.4"] state=present +-nmcli: conn_name=my-eth1 dns4=["192.0.2.53", "198.51.100.53"] state=present # To make a profile usable for all compatible Ethernet interfaces, issue a command as follows - nmcli: ctype=ethernet name=my-eth1 ifname="*" state=present @@ -806,8 +806,8 @@ def create_connection_ethernet(self): cmd=[self.module.get_bin_path('nmcli', True)] # format for creating ethernet interface # To add an Ethernet connection with static IP configuration, issue a command as follows - # - nmcli: name=add conn_name=my-eth1 ifname=eth1 type=ethernet ip4=192.168.100.100/24 gw4=192.168.100.1 state=present - # nmcli con add con-name my-eth1 ifname eth1 type ethernet ip4 192.168.100.100/24 gw4 192.168.100.1 + # - nmcli: name=add conn_name=my-eth1 ifname=eth1 type=ethernet ip4=192.0.2.100/24 gw4=192.0.2.1 state=present + # nmcli con add con-name my-eth1 ifname eth1 type ethernet ip4 192.0.2.100/24 gw4 192.0.2.1 cmd.append('con') cmd.append('add') cmd.append('type') @@ -843,8 +843,8 @@ def modify_connection_ethernet(self): cmd=[self.module.get_bin_path('nmcli', True)] # format for modifying ethernet interface # To add an Ethernet connection with static IP configuration, issue a command as follows - # - nmcli: name=add conn_name=my-eth1 ifname=eth1 type=ethernet ip4=192.168.100.100/24 gw4=192.168.100.1 state=present - # nmcli con add con-name my-eth1 ifname eth1 type ethernet ip4 192.168.100.100/24 gw4 192.168.100.1 + # - nmcli: name=add conn_name=my-eth1 ifname=eth1 type=ethernet ip4=192.0.2.100/24 gw4=192.0.2.1 state=present + # nmcli con add con-name my-eth1 ifname eth1 type ethernet ip4 192.0.2.100/24 gw4 192.0.2.1 cmd.append('con') cmd.append('mod') cmd.append(self.conn_name) diff --git a/network/openvswitch_port.py b/network/openvswitch_port.py index d23fdc28386..c2224b5240e 100644 --- a/network/openvswitch_port.py +++ b/network/openvswitch_port.py @@ -82,13 +82,13 @@ - openvswitch_port: bridge=br-ex port=vlan10 tag=10 state=present set="Interface vlan10 type=internal" -# Assign interface id server1-vifeth6 and mac address 52:54:00:30:6d:11 +# Assign interface id server1-vifeth6 and mac address 00:00:5E:00:53:23 # to port vifeth6 and setup port to be managed by a controller. - openvswitch_port: bridge=br-int port=vifeth6 state=present args: external_ids: iface-id: "{{inventory_hostname}}-vifeth6" - attached-mac: "52:54:00:30:6d:11" + attached-mac: "00:00:5E:00:53:23" vm-id: "{{inventory_hostname}}" iface-status: "active" ''' diff --git a/network/wakeonlan.py b/network/wakeonlan.py index 11fecdceaad..e7aa6ee7f47 100644 --- a/network/wakeonlan.py +++ b/network/wakeonlan.py @@ -53,10 +53,10 @@ ''' EXAMPLES = ''' -# Send a magic Wake-on-LAN packet to 00:CA:FE:BA:BE:00 -- local_action: wakeonlan mac=00:CA:FE:BA:BE:00 broadcast=192.168.1.255 +# Send a magic Wake-on-LAN packet to 00:00:5E:00:53:66 +- local_action: wakeonlan mac=00:00:5E:00:53:66 broadcast=192.0.2.23 -- wakeonlan: mac=00:CA:FE:BA:BE:00 port=9 +- wakeonlan: mac=00:00:5E:00:53:66 port=9 delegate_to: localhost ''' @@ -123,4 +123,4 @@ def main(): if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/system/firewalld.py b/system/firewalld.py index ff5d32d84c4..b5406f63ec8 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -99,7 +99,7 @@ - firewalld: port=161-162/udp permanent=true state=enabled - firewalld: zone=dmz service=http permanent=true state=enabled - firewalld: rich_rule='rule service name="ftp" audit limit value="1/m" accept' permanent=true state=enabled -- firewalld: source='192.168.1.0/24' zone=internal state=enabled +- firewalld: source='192.0.2.0/24' zone=internal state=enabled - firewalld: zone=trusted interface=eth2 permanent=true state=enabled - firewalld: masquerade=yes state=enabled permanent=true zone=dmz ''' diff --git a/system/gluster_volume.py b/system/gluster_volume.py index 820b7ef2d5c..85271d94ea7 100644 --- a/system/gluster_volume.py +++ b/system/gluster_volume.py @@ -121,7 +121,7 @@ EXAMPLES = """ - name: create gluster volume - gluster_volume: state=present name=test1 bricks=/bricks/brick1/g1 rebalance=yes cluster="192.168.1.10,192.168.1.11" + gluster_volume: state=present name=test1 bricks=/bricks/brick1/g1 rebalance=yes cluster="192.0.2.10,192.0.2.11" run_once: true - name: tune @@ -140,7 +140,7 @@ gluster_volume: state=absent name=test1 - name: create gluster volume with multiple bricks - gluster_volume: state=present name=test2 bricks="/bricks/brick1/g2,/bricks/brick2/g2" cluster="192.168.1.10,192.168.1.11" + gluster_volume: state=present name=test2 bricks="/bricks/brick1/g2,/bricks/brick2/g2" cluster="192.0.2.10,192.0.2.11" run_once: true """ diff --git a/univention/udm_dns_record.py b/univention/udm_dns_record.py index 37c2468da3a..dab1e134388 100644 --- a/univention/udm_dns_record.py +++ b/univention/udm_dns_record.py @@ -74,7 +74,7 @@ required: false default: [] description: - - "Additional data for this record, e.g. ['a': '192.168.1.1']. + - "Additional data for this record, e.g. ['a': '192.0.2.1']. Required if C(state=present)." ''' @@ -84,7 +84,7 @@ - udm_dns_zone: name=www zone=example.com type=host_record - data=['a': '192.168.1.1'] + data=['a': '192.0.2.1'] ''' diff --git a/univention/udm_dns_zone.py b/univention/udm_dns_zone.py index 88fbf878688..baf844b546e 100644 --- a/univention/udm_dns_zone.py +++ b/univention/udm_dns_zone.py @@ -106,7 +106,7 @@ - udm_dns_zone: zone=example.com type=forward_zone nameserver=['ucs.example.com'] - interfaces=['192.168.1.1'] + interfaces=['192.0.2.1'] ''' From 0e5837a2e903c2af847be3ca8c4e9529363b3d86 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Mon, 12 Sep 2016 16:45:56 -0400 Subject: [PATCH 2090/2522] bug fix in asa_acl module for missing candidate config This bug was introduced accidentally when refactoring to 2.2. The instance of the candidate config was deleted. This adds the candidate config instance back fixes #2890 --- network/asa/asa_acl.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/network/asa/asa_acl.py b/network/asa/asa_acl.py index 1ac0461ea36..80df451de6c 100644 --- a/network/asa/asa_acl.py +++ b/network/asa/asa_acl.py @@ -128,8 +128,10 @@ type: list sample: ['...', '...'] """ +import ansible.module_utils.asa + from ansible.module_utils.netcfg import NetworkConfig -from ansible.module_utils.asa import NetworkModule +from ansible.module_utils.network import NetworkModule def get_config(module): @@ -179,16 +181,22 @@ def main(): match = module.params['match'] replace = module.params['replace'] + candidate = NetworkConfig(indent=1) + candidate.add(lines) + module.filter = check_input_acl(lines, module) + if not module.params['force']: + contents = get_config(module) + config = NetworkConfig(indent=1, contents=contents) commands = candidate.difference(config) commands = dumps(commands, 'commands').split('\n') - commands = [str(c) for c in commands if c] else: commands = str(candidate).split('\n') if commands: if not module.check_mode: + commands = [str(c) for c in commands if c] response = module.config(commands) result['responses'] = response result['changed'] = True From 1f6f3b72dbeeb0ef012227f6a4d1d566e0b16015 Mon Sep 17 00:00:00 2001 From: Will Thames Date: Tue, 13 Sep 2016 07:57:10 +1000 Subject: [PATCH 2091/2522] firewalld should fail nicely when service is stopped (#2871) Ensure the HAVE_FIREWALLD checks check only for the presence of the python dependencies, and not the age of the library or the state of the service, which are checked later. --- system/firewalld.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/system/firewalld.py b/system/firewalld.py index b5406f63ec8..eefaa45dd98 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -114,10 +114,7 @@ from firewall.client import Rich_Rule from firewall.client import FirewallClient fw = FirewallClient() - if not fw.connected: - HAS_FIREWALLD = False - else: - HAS_FIREWALLD = True + HAS_FIREWALLD = True except ImportError: HAS_FIREWALLD = False @@ -359,6 +356,13 @@ def main(): ## Pre-run version checking if FW_VERSION < "0.2.11": module.fail_json(msg='unsupported version of firewalld, requires >= 2.0.11') + ## Check for firewalld running + try: + if fw.connected == False: + module.fail_json(msg='firewalld service must be running') + except AttributeError: + module.fail_json(msg="firewalld connection can't be established,\ + installed version (%s) likely too old. Requires firewalld >= 2.0.11" % FW_VERSION) ## Global Vars changed=False @@ -386,14 +390,6 @@ def main(): interface = module.params['interface'] masquerade = module.params['masquerade'] - ## Check for firewalld running - try: - if fw.connected == False: - module.fail_json(msg='firewalld service must be running') - except AttributeError: - module.fail_json(msg="firewalld connection can't be established,\ - version likely too old. Requires firewalld >= 2.0.11") - modification_count = 0 if service != None: modification_count += 1 From 961d461f7b5787c986d5df6e5d3f48c6534366fb Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Tue, 13 Sep 2016 11:03:08 -0400 Subject: [PATCH 2092/2522] fixes a number of nagging issues in asa_acl due to refactoring * fixes issues with import error * removes need for filter attribute in Cli instance * now filters config either from device or provided via config argument ref: #2890 --- network/asa/asa_acl.py | 46 ++++++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/network/asa/asa_acl.py b/network/asa/asa_acl.py index 80df451de6c..b51dfefb4f4 100644 --- a/network/asa/asa_acl.py +++ b/network/asa/asa_acl.py @@ -96,6 +96,16 @@ """ EXAMPLES = """ +# Note: examples below use the following provider dict to handle +# transport and authentication to the node. +vars: + cli: + host: "{{ inventory_hostname }}" + username: cisco + password: cisco + transport: cli + authorize: yes + auth_pass: cisco - asa_acl: lines: @@ -107,12 +117,14 @@ before: clear configure access-list ACL-ANSIBLE match: strict replace: block + provider: "{{ cli }}" - asa_acl: lines: - access-list ACL-OUTSIDE extended permit tcp any any eq www - access-list ACL-OUTSIDE extended permit tcp any any eq https context: customer_a + provider: "{{ cli }}" """ RETURN = """ @@ -130,25 +142,30 @@ """ import ansible.module_utils.asa -from ansible.module_utils.netcfg import NetworkConfig from ansible.module_utils.network import NetworkModule +from ansible.module_utils.netcfg import NetworkConfig, dumps -def get_config(module): - config = module.params['config'] or dict() - if not config and not module.params['force']: - config = module.config - return config +def get_config(module, acl_name): + contents = module.params['config'] + if not contents: + contents = module.config.get_config() + filtered_config = list() + for item in contents.split('\n'): + if item.startswith('access-list %s' % acl_name): + filtered_config.append(item) -def check_input_acl(lines, module): + return NetworkConfig(indent=1, contents='\n'.join(filtered_config)) + +def parse_acl_name(module): first_line = True - for line in lines: + for line in module.params['lines']: ace = line.split() if ace[0] != 'access-list': module.fail_json(msg='All lines/commands must begin with "access-list" %s is not permitted' % ace[0]) if len(ace) <= 1: - module.fail_json(msg='All lines/commainds must contain the name of the access-list') + module.fail_json(msg='All lines/commands must contain the name of the access-list') if first_line: acl_name = ace[1] else: @@ -156,7 +173,7 @@ def check_input_acl(lines, module): module.fail_json(msg='All lines/commands must use the same access-list %s is not %s' % (ace[1], acl_name)) first_line = False - return 'access-list %s' % acl_name + return acl_name def main(): @@ -181,22 +198,25 @@ def main(): match = module.params['match'] replace = module.params['replace'] + result = dict(changed=False) + candidate = NetworkConfig(indent=1) candidate.add(lines) - module.filter = check_input_acl(lines, module) + acl_name = parse_acl_name(module) if not module.params['force']: - contents = get_config(module) + contents = get_config(module, acl_name) config = NetworkConfig(indent=1, contents=contents) + commands = candidate.difference(config) commands = dumps(commands, 'commands').split('\n') + commands = [str(c) for c in commands if c] else: commands = str(candidate).split('\n') if commands: if not module.check_mode: - commands = [str(c) for c in commands if c] response = module.config(commands) result['responses'] = response result['changed'] = True From 6d56ce5120305f42cdd65d5e0629c00277ee86ac Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Tue, 13 Sep 2016 12:01:24 -0400 Subject: [PATCH 2093/2522] updates to asa_config module for Ansible 2.2 * clean up functions and remove unneeded code * config difference now includes keyword argument * module reports changed when save argument is yes with or without check_mode * updated fail_json return with exc kwargs * fixed up import statements --- network/asa/asa_config.py | 92 ++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 54 deletions(-) diff --git a/network/asa/asa_config.py b/network/asa/asa_config.py index 36926227e45..8c196cea1ef 100644 --- a/network/asa/asa_config.py +++ b/network/asa/asa_config.py @@ -208,7 +208,7 @@ description: The full path to the backup file returned: when backup is yes type: path - sample: /playbooks/ansible/backup/config.2016-07-16@22:28:34 + sample: /playbooks/ansible/backup/asa_config.2016-07-16@22:28:34 responses: description: The set of responses from issuing the commands on the device returned: when not check_mode @@ -217,36 +217,17 @@ """ import re +import ansible.module_utils.asa + from ansible.module_utils.basic import get_exception -from ansible.module_utils.asa import NetworkModule, NetworkError +from ansible.module_utils.network import NetworkModule, NetworkError from ansible.module_utils.netcfg import NetworkConfig, dumps -from ansible.module_utils.netcli import Command - -def invoke(name, *args, **kwargs): - func = globals().get(name) - if func: - return func(*args, **kwargs) - -def check_args(module, warnings): - if module.params['parents']: - if not module.params['lines'] or module.params['src']: - warnings.append('ignoring unnecessary argument parents') - if module.params['match'] == 'none' and module.params['replace']: - warnings.append('ignoring unnecessary argument replace') - -def get_config(module, result): - defaults = module.params['default'] - if defaults is True: - key = '__configall__' - else: - key = '__config__' - - contents = module.params['config'] or result.get(key) +def get_config(module): + contents = module.params['config'] if not contents: + defaults = module.params['default'] contents = module.config.get_config(include_defaults=defaults) - result[key] = contents - return NetworkConfig(indent=1, contents=contents) def get_candidate(module): @@ -258,75 +239,78 @@ def get_candidate(module): candidate.add(module.params['lines'], parents=parents) return candidate -def load_config(module, commands, result): - if not module.check_mode and module.params['update'] != 'check': - module.config(commands) - result['changed'] = module.params['update'] != 'check' - result['updates'] = commands.split('\n') - def run(module, result): match = module.params['match'] replace = module.params['replace'] + path = module.params['parents'] candidate = get_candidate(module) if match != 'none': - config = get_config(module, result) - configobjs = candidate.difference(config, match=match, replace=replace) + config = get_config(module) + configobjs = candidate.difference(config, path=path, match=match, + replace=replace) else: - config = None configobjs = candidate.items if configobjs: - commands = dumps(configobjs, 'commands') + commands = dumps(configobjs, 'commands').split('\n') + + if module.params['lines']: + if module.params['before']: + commands[:0] = module.params['before'] - if module.params['before']: - commands[:0] = module.params['before'] + if module.params['after']: + commands.extend(module.params['after']) - if module.params['after']: - commands.extend(module.params['after']) + result['updates'] = commands # send the configuration commands to the device and merge # them with the current running config - load_config(module, commands, result) + if not module.check_mode: + module.config.load_config(commands) + result['changed'] = True - if module.params['save'] and not module.check_mode: - module.config.save_config() + if module.params['save']: + if not module.check_mode: + module.config.save_config() + result['changed'] = True def main(): - + """ main entry point for module execution + """ argument_spec = dict( + src=dict(type='path'), + lines=dict(aliases=['commands'], type='list'), parents=dict(type='list'), - src=dict(type='path'), - before=dict(type='list'), after=dict(type='list'), match=dict(default='line', choices=['line', 'strict', 'exact', 'none']), replace=dict(default='line', choices=['line', 'block']), - update=dict(choices=['merge', 'check'], default='merge'), - backup=dict(type='bool', default=False), - config=dict(), default=dict(type='bool', default=False), + backup=dict(type='bool', default=False), save=dict(type='bool', default=False), ) mutually_exclusive = [('lines', 'src')] + required_if = [('match', 'strict', ['lines']), + ('match', 'exact', ['lines']), + ('replace', 'block', ['lines'])] + module = NetworkModule(argument_spec=argument_spec, connect_on_load=False, mutually_exclusive=mutually_exclusive, + required_if=required_if, supports_check_mode=True) - warnings = list() - check_args(module, warnings) - - result = dict(changed=False, warnings=warnings) + result = dict(changed=False) if module.params['backup']: result['__backup__'] = module.config.get_config() @@ -335,7 +319,7 @@ def main(): run(module, result) except NetworkError: exc = get_exception() - module.fail_json(msg=str(exc)) + module.fail_json(msg=str(exc), **exc.kwargs) module.exit_json(**result) From c0d77be4916f7f77e7f4bf44dcfcc5335b5f46d7 Mon Sep 17 00:00:00 2001 From: Benjamin Doherty Date: Tue, 13 Sep 2016 12:40:07 -0400 Subject: [PATCH 2094/2522] Updates to `archive` module based on code review (#2699) * Use common file arguments on destination file * Rename 'compression' to 'format' h/t @abadger * Add support for plain 'tar' format * Ensure check_mode is respected --- files/archive.py | 170 ++++++++++++++++++++++++++--------------------- 1 file changed, 93 insertions(+), 77 deletions(-) diff --git a/files/archive.py b/files/archive.py index 96658461e9a..2b927e39c19 100644 --- a/files/archive.py +++ b/files/archive.py @@ -1,6 +1,10 @@ #!/usr/bin/python # -*- coding: utf-8 -*- +# import module snippets +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception + """ (c) 2016, Ben Doherty Sponsored by Oomph, Inc. http://www.oomphinc.com @@ -34,7 +38,7 @@ description: - Remote absolute path, glob, or list of paths or globs for the file or files to compress or archive. required: true - compression: + format: description: - The type of compression to use. Can be 'gz', 'bz2', or 'zip'. choices: [ 'gz', 'bz2', 'zip' ] @@ -65,7 +69,7 @@ - archive: path=/path/to/foo remove=True # Create a zip archive of /path/to/foo -- archive: path=/path/to/foo compression=zip +- archive: path=/path/to/foo format=zip # Create a bz2 archive of multiple files, rooted at /path - archive: @@ -73,7 +77,7 @@ - /path/to/foo - /path/wong/foo dest: /path/file.tar.bz2 - compression: bz2 + format: bz2 ''' RETURN = ''' @@ -102,9 +106,8 @@ type: list ''' -import stat import os -import errno +import re import glob import shutil import gzip @@ -117,8 +120,8 @@ def main(): module = AnsibleModule( argument_spec = dict( path = dict(type='list', required=True), - compression = dict(choices=['gz', 'bz2', 'zip'], default='gz', required=False), - dest = dict(required=False), + format = dict(choices=['gz', 'bz2', 'zip', 'tar'], default='gz', required=False), + dest = dict(required=False, type='path'), remove = dict(required=False, default=False, type='bool'), ), add_file_common_args=True, @@ -126,11 +129,13 @@ def main(): ) params = module.params + check_mode = module.check_mode paths = params['path'] dest = params['dest'] remove = params['remove'] + expanded_paths = [] - compression = params['compression'] + format = params['format'] globby = False changed = False state = 'absent' @@ -140,11 +145,16 @@ def main(): successes = [] for i, path in enumerate(paths): - path = os.path.expanduser(path) + path = os.path.expanduser(os.path.expandvars(path)) - # Detect glob-like characters - if any((c in set('*?')) for c in path): + # Expand any glob characters. If found, add the expanded glob to the + # list of expanded_paths, which might be empty. + if ('*' in path or '?' in path): expanded_paths = expanded_paths + glob.glob(path) + globby = True + + # If there are no glob characters the path is added to the expanded paths + # whether the path exists or not else: expanded_paths.append(path) @@ -156,11 +166,9 @@ def main(): archive = globby or os.path.isdir(expanded_paths[0]) or len(expanded_paths) > 1 # Default created file name (for single-file archives) to - # . - if dest: - dest = os.path.expanduser(dest) - elif not archive: - dest = '%s.%s' % (expanded_paths[0], compression) + # . + if not dest and not archive: + dest = '%s.%s' % (expanded_paths[0], format) # Force archives to specify 'dest' if archive and not dest: @@ -168,7 +176,6 @@ def main(): archive_paths = [] missing = [] - exclude = [] arcroot = '' for path in expanded_paths: @@ -177,7 +184,7 @@ def main(): if arcroot == '': arcroot = os.path.dirname(path) + os.sep else: - for i in xrange(len(arcroot)): + for i in range(len(arcroot)): if path[i] != arcroot[i]: break @@ -198,7 +205,7 @@ def main(): # No source files were found but the named archive exists: are we 'compress' or 'archive' now? if len(missing) == len(expanded_paths) and dest and os.path.exists(dest): # Just check the filename to know if it's an archive or simple compressed file - if re.search(r'(\.tar\.gz|\.tgz|.tbz2|\.tar\.bz2|\.zip)$', os.path.basename(dest), re.IGNORECASE): + if re.search(r'(\.tar|\.tar\.gz|\.tgz|.tbz2|\.tar\.bz2|\.zip)$', os.path.basename(dest), re.IGNORECASE): state = 'archive' else: state = 'compress' @@ -221,77 +228,84 @@ def main(): size = os.path.getsize(dest) if state != 'archive': - try: + if check_mode: + changed = True + + else: + try: + # Slightly more difficult (and less efficient!) compression using zipfile module + if format == 'zip': + arcfile = zipfile.ZipFile(dest, 'w', zipfile.ZIP_DEFLATED) - # Slightly more difficult (and less efficient!) compression using zipfile module - if compression == 'zip': - arcfile = zipfile.ZipFile(dest, 'w', zipfile.ZIP_DEFLATED) + # Easier compression using tarfile module + elif format == 'gz' or format == 'bz2': + arcfile = tarfile.open(dest, 'w|' + format) - # Easier compression using tarfile module - elif compression == 'gz' or compression == 'bz2': - arcfile = tarfile.open(dest, 'w|' + compression) + # Or plain tar archiving + elif format == 'tar': + arcfile = tarfile.open(dest, 'w') + + for path in archive_paths: + if os.path.isdir(path): + # Recurse into directories + for dirpath, dirnames, filenames in os.walk(path, topdown=True): + if not dirpath.endswith(os.sep): + dirpath += os.sep + + for dirname in dirnames: + fullpath = dirpath + dirname + arcname = fullpath[len(arcroot):] - for path in archive_paths: - if os.path.isdir(path): - # Recurse into directories - for dirpath, dirnames, filenames in os.walk(path, topdown=True): - if not dirpath.endswith(os.sep): - dirpath += os.sep - - for dirname in dirnames: - fullpath = dirpath + dirname - arcname = fullpath[len(arcroot):] - - try: - if compression == 'zip': - arcfile.write(fullpath, arcname) - else: - arcfile.add(fullpath, arcname, recursive=False) - - except Exception: - e = get_exception() - errors.append('%s: %s' % (fullpath, str(e))) - - for filename in filenames: - fullpath = dirpath + filename - arcname = fullpath[len(arcroot):] - - if not filecmp.cmp(fullpath, dest): try: - if compression == 'zip': + if format == 'zip': arcfile.write(fullpath, arcname) else: arcfile.add(fullpath, arcname, recursive=False) - successes.append(fullpath) except Exception: e = get_exception() - errors.append('Adding %s: %s' % (path, str(e))) - else: - if compression == 'zip': - arcfile.write(path, path[len(arcroot):]) + errors.append('%s: %s' % (fullpath, str(e))) + + for filename in filenames: + fullpath = dirpath + filename + arcname = fullpath[len(arcroot):] + + if not filecmp.cmp(fullpath, dest): + try: + if format == 'zip': + arcfile.write(fullpath, arcname) + else: + arcfile.add(fullpath, arcname, recursive=False) + + successes.append(fullpath) + except Exception: + e = get_exception() + errors.append('Adding %s: %s' % (path, str(e))) else: - arcfile.add(path, path[len(arcroot):], recursive=False) + if format == 'zip': + arcfile.write(path, path[len(arcroot):]) + else: + arcfile.add(path, path[len(arcroot):], recursive=False) - successes.append(path) + successes.append(path) - except Exception: - e = get_exception() - return module.fail_json(msg='Error when writing %s archive at %s: %s' % (compression == 'zip' and 'zip' or ('tar.' + compression), dest, str(e))) + except Exception: + e = get_exception() + return module.fail_json(msg='Error when writing %s archive at %s: %s' % (format == 'zip' and 'zip' or ('tar.' + format), dest, str(e))) - if arcfile: - arcfile.close() - state = 'archive' + if arcfile: + arcfile.close() + state = 'archive' - if len(errors) > 0: - module.fail_json(msg='Errors when writing archive at %s: %s' % (dest, '; '.join(errors))) + if len(errors) > 0: + module.fail_json(msg='Errors when writing archive at %s: %s' % (dest, '; '.join(errors))) if state in ['archive', 'incomplete'] and remove: for path in successes: try: if os.path.isdir(path): shutil.rmtree(path) - else: + elif not check_mode: os.remove(path) except OSError: e = get_exception() @@ -331,7 +345,7 @@ def main(): size = os.path.getsize(dest) try: - if compression == 'zip': + if format == 'zip': arcfile = zipfile.ZipFile(dest, 'w', zipfile.ZIP_DEFLATED) arcfile.write(path, path[len(arcroot):]) arcfile.close() @@ -340,12 +354,12 @@ def main(): else: f_in = open(path, 'rb') - if compression == 'gz': + if format == 'gz': f_out = gzip.open(dest, 'wb') - elif compression == 'bz2': + elif format == 'bz2': f_out = bz2.BZ2File(dest, 'wb') else: - raise OSError("Invalid compression") + raise OSError("Invalid format") shutil.copyfileobj(f_in, f_out) @@ -353,7 +367,6 @@ def main(): except OSError: e = get_exception() - module.fail_json(path=path, dest=dest, msg='Unable to write to compressed file: %s' % str(e)) if arcfile: @@ -369,7 +382,7 @@ def main(): state = 'compress' - if remove: + if remove and not check_mode: try: os.remove(path) @@ -377,9 +390,12 @@ def main(): e = get_exception() module.fail_json(path=path, msg='Unable to remove source file: %s' % str(e)) + params['path'] = dest + file_args = module.load_file_common_arguments(params) + + changed = module.set_fs_attributes_if_different(file_args, changed) + module.exit_json(archived=successes, dest=dest, changed=changed, state=state, arcroot=arcroot, missing=missing, expanded_paths=expanded_paths) -# import module snippets -from ansible.module_utils.basic import * if __name__ == '__main__': main() From fbdb448661e000f654e3365b9bac48e0a8013b8c Mon Sep 17 00:00:00 2001 From: Cougar Date: Tue, 13 Sep 2016 21:03:08 +0300 Subject: [PATCH 2095/2522] vmware_guest Fix: remove 'nic' and 'disk' requirements for facts (#2844) --- cloud/vmware/vmware_guest.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/cloud/vmware/vmware_guest.py b/cloud/vmware/vmware_guest.py index 43082e83343..23e25d0453f 100644 --- a/cloud/vmware/vmware_guest.py +++ b/cloud/vmware/vmware_guest.py @@ -72,7 +72,7 @@ nic: description: - A list of nics to add - required: True + required: False wait_for_ip_address: description: - Wait until vcenter detects an IP address for the guest @@ -94,6 +94,9 @@ EXAMPLES = ''' Example from Ansible playbook +# +# Crate VM from template +# - name: create the VM vmware_guest: validate_certs: False @@ -121,6 +124,19 @@ template: template_el7 wait_for_ip_address: yes register: deploy + +# +# Gather facts only +# + - name: gather the VM facts + vmware_guest: + validate_certs: False + hostname: 192.168.1.209 + username: administrator@vsphere.local + password: vmware + name: testvm_2 + esxi_hostname: 192.168.1.117 + register: facts ''' RETURN = """ @@ -468,6 +484,9 @@ def deploy_template(self, poweron=False, wait_for_ip=False): self.params['folder'] ) + if not 'disk' in self.params: + return ({'changed': False, 'failed': True, 'msg': "'disk' is required for VM deployment"}) + datastore_name = self.params['disk'][0]['datastore'] datastore = get_obj(self.content, [vim.Datastore], datastore_name) @@ -795,8 +814,8 @@ def main(): name_match=dict(required=False, type='str', default='first'), uuid=dict(required=False, type='str'), folder=dict(required=False, type='str', default=None, aliases=['folder']), - disk=dict(required=True, type='list'), - nic=dict(required=True, type='list'), + disk=dict(required=False, type='list'), + nic=dict(required=False, type='list'), hardware=dict(required=False, type='dict', default={}), force=dict(required=False, type='bool', default=False), datacenter=dict(required=False, type='str', default=None), From 7fcc5dcb8aee072f358f8df5afa3136b65aa5722 Mon Sep 17 00:00:00 2001 From: jctanner Date: Wed, 14 Sep 2016 01:46:43 -0400 Subject: [PATCH 2096/2522] vmware_guest implement clonevm for multi-dc environments (#2909) * Fix bug in processing of null return * Fix multi-dc folder location by enhancing the foldermap and using it to search * Remove unused functions * Refactor finding vm by folder Fixes #2900 --- cloud/vmware/vmware_guest.py | 172 +++++++++++++++++------------------ 1 file changed, 86 insertions(+), 86 deletions(-) diff --git a/cloud/vmware/vmware_guest.py b/cloud/vmware/vmware_guest.py index 23e25d0453f..0efe36cd0c2 100644 --- a/cloud/vmware/vmware_guest.py +++ b/cloud/vmware/vmware_guest.py @@ -180,6 +180,8 @@ def __init__(self, module): self.si = None self.smartconnect() self.datacenter = None + self.folders = None + self.foldermap = None def smartconnect(self): kwargs = {'host': self.params['hostname'], @@ -205,6 +207,7 @@ def _build_folder_tree(self, folder, tree={}, treepath=None): tree = {'virtualmachines': [], 'subfolders': {}, + 'vimobj': folder, 'name': folder.name} children = None @@ -216,7 +219,6 @@ def _build_folder_tree(self, folder, tree={}, treepath=None): if child == folder or child in tree: continue if type(child) == vim.Folder: - #ctree = self._build_folder_tree(child, tree={}) ctree = self._build_folder_tree(child) tree['subfolders'][child] = dict.copy(ctree) elif type(child) == vim.VirtualMachine: @@ -246,9 +248,31 @@ def _build_folder_map(self, folder, vmap={}, inpath='/'): else: thispath = os.path.join(inpath, folder['name']) + if thispath not in vmap['paths']: + vmap['paths'][thispath] = [] + + # helpful for isolating folder objects later on + if not 'path_by_fvim' in vmap: + vmap['path_by_fvim'] = {} + if not 'fvim_by_path' in vmap: + vmap['fvim_by_path'] = {} + # store object by path and store path by object + vmap['fvim_by_path'][thispath] = folder['vimobj'] + vmap['path_by_fvim'][folder['vimobj']] = thispath + + # helpful for isolating vm objects later on + if not 'path_by_vvim' in vmap: + vmap['path_by_vvim'] = {} + if not 'vvim_by_path' in vmap: + vmap['vvim_by_path'] = {} + if thispath not in vmap['vvim_by_path']: + vmap['vvim_by_path'][thispath] = [] + + for item in folder.items(): k = item[0] v = item[1] + if k == 'name': pass elif k == 'subfolders': @@ -260,21 +284,25 @@ def _build_folder_map(self, folder, vmap={}, inpath='/'): vmap['names'][x.config.name] = [] vmap['names'][x.config.name].append(x.config.uuid) vmap['uuids'][x.config.uuid] = x.config.name - if not thispath in vmap['paths']: - vmap['paths'][thispath] = [] vmap['paths'][thispath].append(x.config.uuid) + if x not in vmap['vvim_by_path'][thispath]: + vmap['vvim_by_path'][thispath].append(x) + if x not in vmap['path_by_vvim']: + vmap['path_by_vvim'][x] = thispath return vmap def getfolders(self): if not self.datacenter: - self.datacenter = get_obj(self.content, [vim.Datacenter], - self.params['esxi']['datacenter']) + self.get_datacenter() self.folders = self._build_folder_tree(self.datacenter.vmFolder) self.folder_map = self._build_folder_map(self.folders) return (self.folders, self.folder_map) + def get_datacenter(self): + self.datacenter = get_obj(self.content, [vim.Datacenter], + self.params['datacenter']) def getvm(self, name=None, uuid=None, folder=None, name_match=None): @@ -289,35 +317,41 @@ def getvm(self, name=None, uuid=None, folder=None, name_match=None): elif folder: - matches = [] - folder_paths = [] - - datacenter = None - if 'esxi' in self.params: - if 'datacenter' in self.params['esxi']: - datacenter = self.params['esxi']['datacenter'] - - if datacenter: - folder_paths.append('%s/vm/%s' % (datacenter, folder)) + if self.params['folder'].endswith('/'): + self.params['folder'] = self.params['folder'][0:-1] + + # Build the absolute folder path to pass into the search method + searchpath = None + if self.params['folder'].startswith('/vm'): + searchpath = '%s' % self.params['datacenter'] + searchpath += self.params['folder'] + elif self.params['folder'].startswith('/'): + searchpath = '%s' % self.params['datacenter'] + searchpath += '/vm' + self.params['folder'] else: - # get a list of datacenters - datacenters = get_all_objs(self.content, [vim.Datacenter]) - datacenters = [x.name for x in datacenters] - for dc in datacenters: - folder_paths.append('%s/vm/%s' % (dc, folder)) - - for folder_path in folder_paths: - fObj = self.si.content.searchIndex.FindByInventoryPath(folder_path) - for cObj in fObj.childEntity: - if not type(cObj) == vim.VirtualMachine: - continue - if cObj.name == name: - matches.append(cObj) - if len(matches) > 1 and not name_match: - module.fail_json(msg='more than 1 vm exists by the name %s in folder %s. Please specify a uuid, a datacenter or name_match' \ - % (folder, name)) - elif len(matches) > 0: - vm = matches[0] + # need to look for matching absolute path + if not self.folders: + self.getfolders() + paths = self.folder_map['paths'].keys() + paths = [x for x in paths if x.endswith(self.params['folder'])] + if len(paths) > 1: + self.module.fail_json(msg='%s matches more than one folder. Please use the absolute path' % self.params['folder']) + elif paths: + searchpath = paths[0] + + if searchpath: + # get all objects for this path ... + fObj = self.si.content.searchIndex.FindByInventoryPath(searchpath) + if fObj: + if isinstance(fObj, vim.Datacenter): + fObj = fObj.vmFolder + for cObj in fObj.childEntity: + if not type(cObj) == vim.VirtualMachine: + continue + if cObj.name == name: + vm = cObj + break + else: vmList = get_all_objs(self.content, [vim.VirtualMachine]) if name_match: @@ -467,22 +501,30 @@ def deploy_template(self, poweron=False, wait_for_ip=False): # - multiple datacenters # - resource pools # - multiple templates by the same name + # - use disk config from template by default # - static IPs datacenters = get_all_objs(self.content, [vim.Datacenter]) datacenter = get_obj(self.content, [vim.Datacenter], self.params['datacenter']) - # folder is a required clone argument - if len(datacenters) > 1: - # FIXME: need to find the folder in the right DC. - raise "multi-dc with folders is not yet implemented" - else: - destfolder = get_obj( - self.content, - [vim.Folder], - self.params['folder'] - ) + if not self.foldermap: + self.folders, self.foldermap = self.getfolders() + + # find matching folders + if self.params['folder'].startswith('/'): + folders = [x for x in self.foldermap['fvim_by_path'].items() if x[0] == self.params['folder']] + else: + folders = [x for x in self.foldermap['fvim_by_path'].items() if x[0].endswith(self.params['folder'])] + + # throw error if more than one match or no matches + if len(folders) == 0: + self.module.fail_json('no folder matched the path: %s' % self.params['folder']) + elif len(folders) > 1: + self.module.fail_json('too many folders matched "%s", please give the full path' % self.params['folder']) + + # grab the folder vim object + destfolder = folders[0][1] if not 'disk' in self.params: return ({'changed': False, 'failed': True, 'msg': "'disk' is required for VM deployment"}) @@ -737,48 +779,6 @@ def get_all_objs(content, vimtype): return obj -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 main(): vm = None @@ -813,7 +813,7 @@ def main(): name=dict(required=True, type='str'), name_match=dict(required=False, type='str', default='first'), uuid=dict(required=False, type='str'), - folder=dict(required=False, type='str', default=None, aliases=['folder']), + folder=dict(required=False, type='str', default='/vm', aliases=['folder']), disk=dict(required=False, type='list'), nic=dict(required=False, type='list'), hardware=dict(required=False, type='dict', default={}), From 24aceceb3628910e76c50f6d646099ef76ae90e6 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Tue, 13 Sep 2016 23:35:15 -0700 Subject: [PATCH 2097/2522] Fixes domains method not defined (#2907) The domains method was not defined, and therefore when specifying a parent domain during route domain creation, the process would fail. Tests have been added to detect this going forward --- network/f5/bigip_routedomain.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/network/f5/bigip_routedomain.py b/network/f5/bigip_routedomain.py index bb71f850f57..552b20231cc 100644 --- a/network/f5/bigip_routedomain.py +++ b/network/f5/bigip_routedomain.py @@ -260,6 +260,20 @@ def read(self): p['service_policy'] = str(r.servicePolicy) return p + def domains(self): + result = [] + + domains = self.api.tm.net.route_domains.get_collection() + for domain in domains: + # Just checking for the addition of the partition here for + # different versions of BIG-IP + if '/' + self.params['partition'] + '/' in domain.name: + result.append(domain.name) + else: + full_name = '/%s/%s' % (self.params['partition'], domain.name) + result.append(full_name) + return result + def create(self): params = dict() params['id'] = self.params['id'] @@ -284,7 +298,7 @@ def create(self): if parent is not None: parent = '/%s/%s' % (partition, parent) - if parent in self.domains: + if parent in self.domains(): params['parent'] = parent else: raise F5ModuleError( From 6d37aa42603e5a280414ca6f313a9d56bacfda6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Wed, 14 Sep 2016 10:43:27 +0200 Subject: [PATCH 2098/2522] jenkins_job fix: NameError: global name 'module' is not defined (#2910) --- web_infrastructure/jenkins_job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_infrastructure/jenkins_job.py b/web_infrastructure/jenkins_job.py index ee8b1745cb7..af5a28c3e9c 100644 --- a/web_infrastructure/jenkins_job.py +++ b/web_infrastructure/jenkins_job.py @@ -235,7 +235,7 @@ def has_config_changed(self): def present_job(self): if self.config is None and self.enabled is None: - module.fail_json(msg='one of the following params is required on state=present: config,enabled') + self.module.fail_json(msg='one of the following params is required on state=present: config,enabled') if not self.job_exists(): self.create_job() From 2b6f3419b6c3103195d576996c9464b334fb6489 Mon Sep 17 00:00:00 2001 From: Steve Gargan Date: Wed, 14 Sep 2016 11:39:41 +0200 Subject: [PATCH 2099/2522] remove duration from lock delay as seconds are the only granularity supported. (#2877) add utf header to file so that it loads correctly --- clustering/consul_session.py | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/clustering/consul_session.py b/clustering/consul_session.py index d2c24e12a3b..4d733561391 100644 --- a/clustering/consul_session.py +++ b/clustering/consul_session.py @@ -1,4 +1,5 @@ #!/usr/bin/python +# -*- coding: utf-8 -*- # # (c) 2015, Steve Gargan # @@ -30,7 +31,7 @@ - python-consul - requests version_added: "2.0" -author: "Steve Gargan (@sgargan)" +author: "Steve Gargan @sgargan" options: state: description: @@ -54,9 +55,8 @@ description: - the optional lock delay that can be attached to the session when it is created. Locks for invalidated sessions ar blocked from being - acquired until this delay has expired. Valid units for delays - include 'ns', 'us', 'ms', 's', 'm', 'h' - default: 15s + acquired until this delay has expired. Durations are in seconds + default: 15 required: false node: description: @@ -114,7 +114,7 @@ - name: register basic session with consul consul_session: name: session1 - + - name: register a session with an existing check consul_session: name: session_with_check @@ -201,12 +201,11 @@ def update_session(module): consul_client = get_consul_api(module) try: - session = consul_client.session.create( name=name, behavior=behavior, node=node, - lock_delay=validate_duration('delay', delay), + lock_delay=delay, dc=datacenter, checks=checks ) @@ -223,7 +222,6 @@ def update_session(module): def remove_session(module): session_id = module.params.get('id') - if not session_id: module.fail_json(msg="""A session id must be supplied in order to remove a session.""") @@ -239,18 +237,10 @@ def remove_session(module): module.fail_json(msg="Could not remove session with id '%s' %s" % ( session_id, e)) -def validate_duration(name, duration): - if duration: - duration_units = ['ns', 'us', 'ms', 's', 'm', 'h'] - if not any((duration.endswith(suffix) for suffix in duration_units)): - raise Exception('Invalid %s %s you must specify units (%s)' % - (name, duration, ', '.join(duration_units))) - return duration - def get_consul_api(module): return consul.Consul(host=module.params.get('host'), port=module.params.get('port')) - + def test_dependencies(module): if not python_consul_installed: module.fail_json(msg="python-consul required for this module. "\ @@ -259,7 +249,7 @@ def test_dependencies(module): def main(): argument_spec = dict( checks=dict(default=None, required=False, type='list'), - delay=dict(required=False,type='str', default='15s'), + delay=dict(required=False,type='int', default='15'), behavior=dict(required=False,type='str', default='release', choices=['release', 'delete']), host=dict(default='localhost'), @@ -275,9 +265,9 @@ def main(): ) module = AnsibleModule(argument_spec, supports_check_mode=False) - + test_dependencies(module) - + try: execute(module) except ConnectionError, e: From d69a6f20daadf802e987697259ce40b41684f4bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Gr=C3=BCner?= Date: Wed, 14 Sep 2016 14:16:51 +0200 Subject: [PATCH 2100/2522] letsencrypt: Improve error handling (#2868) * letsencrypt: improve error handling Use the new "body" field of the info dict in case of a HTTPError. * letsencrypt: HTTP 202 is a valid status while polling --- web_infrastructure/letsencrypt.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/web_infrastructure/letsencrypt.py b/web_infrastructure/letsencrypt.py index 2b7922852ce..d5afebeaae2 100644 --- a/web_infrastructure/letsencrypt.py +++ b/web_infrastructure/letsencrypt.py @@ -169,14 +169,18 @@ def simple_get(module,url): result = None try: content = resp.read() + except AttributeError: + if info['body']: + content = info['body'] + + if content: if info['content-type'].startswith('application/json'): - result = module.from_json(content.decode('utf8')) + try: + result = module.from_json(content.decode('utf8')) + except ValueError: + module.fail_json(msg="Failed to parse the ACME response: {0} {1}".format(url,content)) else: result = content - except AttributeError: - result = None - except ValueError: - module.fail_json(msg="Failed to parse the ACME response: {0} {1}".format(url,content)) if info['status'] >= 400: module.fail_json(msg="ACME request failed: CODE: {0} RESULT:{1}".format(info['status'],result)) @@ -370,14 +374,18 @@ def send_signed_request(self, url, payload): result = None try: content = resp.read() + except AttributeError: + if info['body']: + content = info['body'] + + if content: if info['content-type'].startswith('application/json'): - result = self.module.from_json(content.decode('utf8')) + try: + result = self.module.from_json(content.decode('utf8')) + except ValueError: + self.module.fail_json(msg="Failed to parse the ACME response: {0} {1}".format(url,content)) else: result = content - except AttributeError: - result = None - except ValueError: - self.module.fail_json(msg="Failed to parse the ACME response: {0} {1}".format(url,content)) return result,info @@ -637,7 +645,7 @@ def _validate_challenges(self,auth): "keyAuthorization": keyauthorization, } result, info = self.account.send_signed_request(uri, challenge_response) - if info['status'] != 200: + if info['status'] not in [200,202]: self.module.fail_json(msg="Error validating challenge: CODE: {0} RESULT: {1}".format(info['status'], result)) status = '' From 333133144e2281f6735278e334cd636f6672f462 Mon Sep 17 00:00:00 2001 From: David Stygstra Date: Wed, 14 Sep 2016 08:32:08 -0400 Subject: [PATCH 2101/2522] Use `modprobe -r` instead of `rmmod` in modprobe module (#2669) If any modules that the module being removed depends on are unused, they will also be removed. Fixes #2140. --- system/modprobe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/modprobe.py b/system/modprobe.py index 94c1a70437b..1bb1d3f70b1 100644 --- a/system/modprobe.py +++ b/system/modprobe.py @@ -114,7 +114,7 @@ def main(): args['changed'] = True elif args['state'] == 'absent': if present: - rc, _, err = module.run_command([module.get_bin_path('rmmod', True), args['name']]) + rc, _, err = module.run_command([module.get_bin_path('modprobe', True), '-r', args['name']]) if rc != 0: module.fail_json(msg=err, **args) args['changed'] = True From 8749c40fee37e752648f7a99b142fcdd7d1590e4 Mon Sep 17 00:00:00 2001 From: Ryan Brown Date: Wed, 14 Sep 2016 11:41:02 -0400 Subject: [PATCH 2102/2522] New module: `lambda_facts` (#2874) --- cloud/amazon/lambda_facts.py | 408 +++++++++++++++++++++++++++++++++++ 1 file changed, 408 insertions(+) create mode 100644 cloud/amazon/lambda_facts.py diff --git a/cloud/amazon/lambda_facts.py b/cloud/amazon/lambda_facts.py new file mode 100644 index 00000000000..9103f69df50 --- /dev/null +++ b/cloud/amazon/lambda_facts.py @@ -0,0 +1,408 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import datetime +import sys + +try: + import boto3 + from botocore.exceptions import ClientError + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + + +DOCUMENTATION = ''' +--- +module: lambda_facts +short_description: Gathers AWS Lambda function details as Ansible facts +description: + - Gathers various details related to Lambda functions, including aliases, versions and event source mappings. + Use module M(lambda) to manage the lambda function itself, M(lambda_alias) to manage function aliases and + M(lambda_event) to manage lambda event source mappings. + +version_added: "2.2" + +options: + query: + description: + - Specifies the resource type for which to gather facts. Leave blank to retrieve all facts. + required: true + choices: [ "aliases", "all", "config", "mappings", "policy", "versions" ] + default: "all" + function_name: + description: + - The name of the lambda function for which facts are requested. + required: false + default: null + aliases: [ "function", "name"] + event_source_arn: + description: + - For query type 'mappings', this is the Amazon Resource Name (ARN) of the Amazon Kinesis or DynamoDB stream. + default: null + required: false +author: Pierre Jodouin (@pjodouin) +requirements: + - boto3 +extends_documentation_fragment: + - aws + +''' + +EXAMPLES = ''' +--- +# Simple example of listing all info for a function +- name: List all for a specific function + lambda_facts: + query: all + function_name: myFunction + register: my_function_details +# List all versions of a function +- name: List function versions + lambda_facts: + query: versions + function_name: myFunction + register: my_function_versions +# List all lambda function versions +- name: List all function + lambda_facts: + query: all + max_items: 20 +- name: show Lambda facts + debug: var=lambda_facts +''' + +RETURN = ''' +--- +lambda_facts: + description: lambda facts + returned: success + type: dict +lambda_facts.function: + description: lambda function list + returned: success + type: dict +lambda_facts.function.TheName: + description: lambda function information, including event, mapping, and version information + returned: success + type: dict +''' + + +def fix_return(node): + """ + fixup returned dictionary + + :param node: + :return: + """ + + if isinstance(node, datetime.datetime): + node_value = str(node) + + elif isinstance(node, list): + node_value = [fix_return(item) for item in node] + + elif isinstance(node, dict): + node_value = dict([(item, fix_return(node[item])) for item in node.keys()]) + + else: + node_value = node + + return node_value + + +def alias_details(client, module): + """ + Returns list of aliases for a specified function. + + :param client: AWS API client reference (boto3) + :param module: Ansible module reference + :return dict: + """ + + lambda_facts = dict() + + function_name = module.params.get('function_name') + if function_name: + params = dict() + if module.params.get('max_items'): + params['MaxItems'] = module.params.get('max_items') + + if module.params.get('next_marker'): + params['Marker'] = module.params.get('next_marker') + try: + lambda_facts.update(aliases=client.list_aliases(FunctionName=function_name, **params)['Aliases']) + except ClientError as e: + if e.response['Error']['Code'] == 'ResourceNotFoundException': + lambda_facts.update(aliases=[]) + else: + module.fail_json(msg='Unable to get {0} aliases, error: {1}'.format(function_name, e)) + else: + module.fail_json(msg='Parameter function_name required for query=aliases.') + + return {function_name: camel_dict_to_snake_dict(lambda_facts)} + + +def all_details(client, module): + """ + Returns all lambda related facts. + + :param client: AWS API client reference (boto3) + :param module: Ansible module reference + :return dict: + """ + + if module.params.get('max_items') or module.params.get('next_marker'): + module.fail_json(msg='Cannot specify max_items nor next_marker for query=all.') + + lambda_facts = dict() + + function_name = module.params.get('function_name') + if function_name: + lambda_facts[function_name] = {} + lambda_facts[function_name].update(config_details(client, module)[function_name]) + lambda_facts[function_name].update(alias_details(client, module)[function_name]) + lambda_facts[function_name].update(policy_details(client, module)[function_name]) + lambda_facts[function_name].update(version_details(client, module)[function_name]) + lambda_facts[function_name].update(mapping_details(client, module)[function_name]) + else: + lambda_facts.update(config_details(client, module)) + + return lambda_facts + + +def config_details(client, module): + """ + Returns configuration details for one or all lambda functions. + + :param client: AWS API client reference (boto3) + :param module: Ansible module reference + :return dict: + """ + + lambda_facts = dict() + + function_name = module.params.get('function_name') + if function_name: + try: + lambda_facts.update(client.get_function_configuration(FunctionName=function_name)) + except ClientError as e: + if e.response['Error']['Code'] == 'ResourceNotFoundException': + lambda_facts.update(function={}) + else: + module.fail_json(msg='Unable to get {0} configuration, error: {1}'.format(function_name, e)) + else: + params = dict() + if module.params.get('max_items'): + params['MaxItems'] = module.params.get('max_items') + + if module.params.get('next_marker'): + params['Marker'] = module.params.get('next_marker') + + try: + lambda_facts.update(function_list=client.list_functions(**params)['Functions']) + except ClientError as e: + if e.response['Error']['Code'] == 'ResourceNotFoundException': + lambda_facts.update(function_list=[]) + else: + module.fail_json(msg='Unable to get function list, error: {0}'.format(e)) + + functions = dict() + for func in lambda_facts.pop('function_list', []): + functions[func['FunctionName']] = camel_dict_to_snake_dict(func) + return functions + + return {function_name: camel_dict_to_snake_dict(lambda_facts)} + + +def mapping_details(client, module): + """ + Returns all lambda event source mappings. + + :param client: AWS API client reference (boto3) + :param module: Ansible module reference + :return dict: + """ + + lambda_facts = dict() + params = dict() + function_name = module.params.get('function_name') + + if function_name: + params['FunctionName'] = module.params.get('function_name') + + if module.params.get('event_source_arn'): + params['EventSourceArn'] = module.params.get('event_source_arn') + + if module.params.get('max_items'): + params['MaxItems'] = module.params.get('max_items') + + if module.params.get('next_marker'): + params['Marker'] = module.params.get('next_marker') + + try: + lambda_facts.update(mappings=client.list_event_source_mappings(**params)['EventSourceMappings']) + except ClientError as e: + if e.response['Error']['Code'] == 'ResourceNotFoundException': + lambda_facts.update(mappings=[]) + else: + module.fail_json(msg='Unable to get source event mappings, error: {0}'.format(e)) + + if function_name: + return {function_name: camel_dict_to_snake_dict(lambda_facts)} + + return camel_dict_to_snake_dict(lambda_facts) + + +def policy_details(client, module): + """ + Returns policy attached to a lambda function. + + :param client: AWS API client reference (boto3) + :param module: Ansible module reference + :return dict: + """ + + if module.params.get('max_items') or module.params.get('next_marker'): + module.fail_json(msg='Cannot specify max_items nor next_marker for query=policy.') + + lambda_facts = dict() + + function_name = module.params.get('function_name') + if function_name: + try: + # get_policy returns a JSON string so must convert to dict before reassigning to its key + lambda_facts.update(policy=json.loads(client.get_policy(FunctionName=function_name)['Policy'])) + except ClientError as e: + if e.response['Error']['Code'] == 'ResourceNotFoundException': + lambda_facts.update(policy={}) + else: + module.fail_json(msg='Unable to get {0} policy, error: {1}'.format(function_name, e)) + else: + module.fail_json(msg='Parameter function_name required for query=policy.') + + return {function_name: camel_dict_to_snake_dict(lambda_facts)} + + +def version_details(client, module): + """ + Returns all lambda function versions. + + :param client: AWS API client reference (boto3) + :param module: Ansible module reference + :return dict: + """ + + lambda_facts = dict() + + function_name = module.params.get('function_name') + if function_name: + params = dict() + if module.params.get('max_items'): + params['MaxItems'] = module.params.get('max_items') + + if module.params.get('next_marker'): + params['Marker'] = module.params.get('next_marker') + + try: + lambda_facts.update(versions=client.list_versions_by_function(FunctionName=function_name, **params)['Versions']) + except ClientError as e: + if e.response['Error']['Code'] == 'ResourceNotFoundException': + lambda_facts.update(versions=[]) + else: + module.fail_json(msg='Unable to get {0} versions, error: {1}'.format(function_name, e)) + else: + module.fail_json(msg='Parameter function_name required for query=versions.') + + return {function_name: camel_dict_to_snake_dict(lambda_facts)} + + +def main(): + """ + Main entry point. + + :return dict: ansible facts + """ + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + function_name=dict(required=False, default=None, aliases=['function', 'name']), + query=dict(required=False, choices=['aliases', 'all', 'config', 'mappings', 'policy', 'versions'], default='all'), + event_source_arn=dict(required=False, default=None) + ) + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + mutually_exclusive=[], + required_together=[] + ) + + # validate dependencies + if not HAS_BOTO3: + module.fail_json(msg='boto3 is required for this module.') + + # validate function_name if present + function_name = module.params['function_name'] + if function_name: + if not re.search("^[\w\-:]+$", function_name): + module.fail_json( + msg='Function name {0} is invalid. Names must contain only alphanumeric characters and hyphens.'.format(function_name) + ) + if len(function_name) > 64: + module.fail_json(msg='Function name "{0}" exceeds 64 character limit'.format(function_name)) + + try: + region, endpoint, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + aws_connect_kwargs.update(dict(region=region, + endpoint=endpoint, + conn_type='client', + resource='lambda' + )) + client = boto3_conn(module, **aws_connect_kwargs) + except ClientError as e: + module.fail_json(msg="Can't authorize connection - {0}".format(e)) + + this_module = sys.modules[__name__] + + invocations = dict( + aliases='alias_details', + all='all_details', + config='config_details', + mappings='mapping_details', + policy='policy_details', + versions='version_details', + ) + + this_module_function = getattr(this_module, invocations[module.params['query']]) + all_facts = fix_return(this_module_function(client, module)) + + results = dict(ansible_facts={'lambda_facts': {'function': all_facts}}, changed=False) + + if module.check_mode: + results['msg'] = 'Check mode set but ignored for fact gathering only.' + + module.exit_json(**results) + + +# ansible import module(s) kept at ~eof as recommended +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From 170adf16bd3ba18d3340c2a59b407dab95e8d459 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Wed, 14 Sep 2016 23:42:29 +0200 Subject: [PATCH 2103/2522] zabbix_host: fix typos in arg spec of login_user Credits to @JasonCormie --- monitoring/zabbix_host.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/zabbix_host.py b/monitoring/zabbix_host.py index e6fec0b0252..20d8b6e21fb 100644 --- a/monitoring/zabbix_host.py +++ b/monitoring/zabbix_host.py @@ -415,7 +415,7 @@ def main(): module = AnsibleModule( argument_spec=dict( server_url=dict(type='str', required=True, aliases=['url']), - login_user=dict(rtype='str', equired=True), + login_user=dict(type='str', required=True), login_password=dict(type='str', required=True, no_log=True), host_name=dict(type='str', required=True), http_login_user=dict(type='str', required=False, default=None), From 2f7be4ceef700a8091f364648ae01fa581d5204e Mon Sep 17 00:00:00 2001 From: jctanner Date: Thu, 15 Sep 2016 09:34:58 -0400 Subject: [PATCH 2104/2522] vmware_guest: use the disk argument to modify disk size and type (#2918) Fixes #2706 --- cloud/vmware/vmware_guest.py | 81 ++++++++++++++++++++++++++++++------ 1 file changed, 68 insertions(+), 13 deletions(-) diff --git a/cloud/vmware/vmware_guest.py b/cloud/vmware/vmware_guest.py index 0efe36cd0c2..8429a30cc92 100644 --- a/cloud/vmware/vmware_guest.py +++ b/cloud/vmware/vmware_guest.py @@ -164,6 +164,7 @@ import atexit import os import ssl +import string import time from ansible.module_utils.urls import fetch_url @@ -495,15 +496,20 @@ def remove_vm(self, vm): def deploy_template(self, poweron=False, wait_for_ip=False): # https://github.com/vmware/pyvmomi-community-samples/blob/master/samples/clone_vm.py + # https://www.vmware.com/support/developer/vc-sdk/visdk25pubs/ReferenceGuide/vim.vm.CloneSpec.html + # https://www.vmware.com/support/developer/vc-sdk/visdk25pubs/ReferenceGuide/vim.vm.ConfigSpec.html # FIXME: # - clusters # - multiple datacenters # - resource pools # - multiple templates by the same name - # - use disk config from template by default + # - multiple disks # - static IPs + # FIXME: need to search for this in the same way as guests to ensure accuracy + template = get_obj(self.content, [vim.VirtualMachine], self.params['template']) + datacenters = get_all_objs(self.content, [vim.Datacenter]) datacenter = get_obj(self.content, [vim.Datacenter], self.params['datacenter']) @@ -526,19 +532,20 @@ def deploy_template(self, poweron=False, wait_for_ip=False): # grab the folder vim object destfolder = folders[0][1] - if not 'disk' in self.params: - return ({'changed': False, 'failed': True, 'msg': "'disk' is required for VM deployment"}) - - datastore_name = self.params['disk'][0]['datastore'] - datastore = get_obj(self.content, [vim.Datastore], datastore_name) - - - # cluster or hostsystem ... ? + # FIXME: cluster or hostsystem ... ? #cluster = get_obj(self.content, [vim.ClusterComputeResource], self.params['esxi']['hostname']) hostsystem = get_obj(self.content, [vim.HostSystem], self.params['esxi_hostname']) - resource_pools = get_all_objs(self.content, [vim.ResourcePool]) + if self.params['disk']: + datastore_name = self.params['disk'][0]['datastore'] + datastore = get_obj(self.content, [vim.Datastore], datastore_name) + else: + # use the template's existing DS + disks = [x for x in template.config.hardware.device if isinstance(x, vim.vm.device.VirtualDisk)] + datastore = disks[0].backing.datastore + datastore_name = datastore.name + relospec = vim.vm.RelocateSpec() relospec.datastore = datastore @@ -546,10 +553,58 @@ def deploy_template(self, poweron=False, wait_for_ip=False): relospec.pool = resource_pools[0] relospec.host = hostsystem - clonespec = vim.vm.CloneSpec() - clonespec.location = relospec + clonespec_kwargs = {} + clonespec_kwargs['location'] = relospec + + # create disk spec if not default + if self.params['disk']: + # grab the template's first disk and modify it for this customization + disks = [x for x in template.config.hardware.device if isinstance(x, vim.vm.device.VirtualDisk)] + diskspec = vim.vm.device.VirtualDeviceSpec() + # set the operation to edit so that it knows to keep other settings + diskspec.operation = vim.vm.device.VirtualDeviceSpec.Operation.edit + diskspec.device = disks[0] + + # get the first disk attributes + pspec = self.params.get('disk')[0] + + # is it thin? + if pspec.get('type', '').lower() == 'thin': + diskspec.device.backing.thinProvisioned = True + + # what size is it? + if [x for x in pspec.keys() if x.startswith('size_') or x == 'size']: + # size_tb, size_gb, size_mb, size_kb, size_b ...? + if 'size' in pspec: + # http://stackoverflow.com/a/1451407 + trans = string.maketrans('', '') + chars = trans.translate(trans, string.digits) + expected = pspec['size'].translate(trans, chars) + expected = expected + unit = pspec['size'].replace(expected, '').lower() + expected = int(expected) + else: + expected = [x for x in pspec.keys() if x.startswith('size_')][0] + unit = expected.split('_')[-1].lower() + + kb = None + if unit == 'tb': + kb = expected * 1024 * 1024 * 1024 + elif unit == 'gb': + kb = expected * 1024 * 1024 + elif unit ==' mb': + kb = expected * 1024 + elif unit == 'kb': + kb = expected + else: + self.module.fail_json(msg='%s is not a supported unit for disk size' % unit) + diskspec.device.capacityInKB = kb + + # tell the configspec that the disk device needs to change + configspec = vim.vm.ConfigSpec(deviceChange=[diskspec]) + clonespec_kwargs['config'] = configspec - template = get_obj(self.content, [vim.VirtualMachine], self.params['template']) + clonespec = vim.vm.CloneSpec(**clonespec_kwargs) task = template.Clone(folder=destfolder, name=self.params['name'], spec=clonespec) self.wait_for_task(task) From aa45bd8a94c0b67fa44ea845ea4eff3980385caa Mon Sep 17 00:00:00 2001 From: Marcin Kawa Date: Thu, 15 Sep 2016 16:35:47 +0100 Subject: [PATCH 2105/2522] Fix undefined info error and accept HTTP 201 response code (#2643) Prevent referenced before assignment error when `notify` argument is not specified and accept HTTP 201 (created) code. --- notification/campfire.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/notification/campfire.py b/notification/campfire.py index 7871747becd..3d003e1363a 100644 --- a/notification/campfire.py +++ b/notification/campfire.py @@ -117,14 +117,14 @@ def main(): # Send some audible notification if requested if notify: response, info = fetch_url(module, target_url, data=NSTR % cgi.escape(notify), headers=headers) - if info['status'] != 200: - module.fail_json(msg="unable to send msg: '%s', campfire api" - " returned error code: '%s'" % - (notify, info['status'])) + if info['status'] not in [200, 201]: + module.fail_json(msg="unable to send msg: '%s', campfire api" + " returned error code: '%s'" % + (notify, info['status'])) # Send the message response, info = fetch_url(module, target_url, data=MSTR %cgi.escape(msg), headers=headers) - if info['status'] != 200: + if info['status'] not in [200, 201]: module.fail_json(msg="unable to send msg: '%s', campfire api" " returned error code: '%s'" % (msg, info['status'])) From 5bbe2adb301bf7c5523883c5f4ff5e3948ca50de Mon Sep 17 00:00:00 2001 From: jctanner Date: Thu, 15 Sep 2016 13:49:38 -0400 Subject: [PATCH 2106/2522] vmware_guest improve and fix some of the errors (#2926) * Add more comments and fix issue with unit conversion --- cloud/vmware/vmware_guest.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/cloud/vmware/vmware_guest.py b/cloud/vmware/vmware_guest.py index 8429a30cc92..05d9525e4cc 100644 --- a/cloud/vmware/vmware_guest.py +++ b/cloud/vmware/vmware_guest.py @@ -336,7 +336,7 @@ def getvm(self, name=None, uuid=None, folder=None, name_match=None): paths = self.folder_map['paths'].keys() paths = [x for x in paths if x.endswith(self.params['folder'])] if len(paths) > 1: - self.module.fail_json(msg='%s matches more than one folder. Please use the absolute path' % self.params['folder']) + self.module.fail_json(msg='%s matches more than one folder. Please use the absolute path starting with /vm/' % self.params['folder']) elif paths: searchpath = paths[0] @@ -498,6 +498,7 @@ def deploy_template(self, poweron=False, wait_for_ip=False): # https://github.com/vmware/pyvmomi-community-samples/blob/master/samples/clone_vm.py # https://www.vmware.com/support/developer/vc-sdk/visdk25pubs/ReferenceGuide/vim.vm.CloneSpec.html # https://www.vmware.com/support/developer/vc-sdk/visdk25pubs/ReferenceGuide/vim.vm.ConfigSpec.html + # https://www.vmware.com/support/developer/vc-sdk/visdk41pubs/ApiReference/vim.vm.RelocateSpec.html # FIXME: # - clusters @@ -505,14 +506,19 @@ def deploy_template(self, poweron=False, wait_for_ip=False): # - resource pools # - multiple templates by the same name # - multiple disks + # - changing the esx host is ignored? # - static IPs # FIXME: need to search for this in the same way as guests to ensure accuracy template = get_obj(self.content, [vim.VirtualMachine], self.params['template']) + if not template: + self.module.fail_json(msg="Could not find a template named %s" % self.params['template']) datacenters = get_all_objs(self.content, [vim.Datacenter]) datacenter = get_obj(self.content, [vim.Datacenter], self.params['datacenter']) + if not datacenter: + self.module.fail_json(msg='No datacenter named %s was found' % self.params['datacenter']) if not self.foldermap: self.folders, self.foldermap = self.getfolders() @@ -525,9 +531,9 @@ def deploy_template(self, poweron=False, wait_for_ip=False): # throw error if more than one match or no matches if len(folders) == 0: - self.module.fail_json('no folder matched the path: %s' % self.params['folder']) + self.module.fail_json(msg='no folder matched the path: %s' % self.params['folder']) elif len(folders) > 1: - self.module.fail_json('too many folders matched "%s", please give the full path' % self.params['folder']) + self.module.fail_json(msg='too many folders matched "%s", please give the full path starting with /vm/' % self.params['folder']) # grab the folder vim object destfolder = folders[0][1] @@ -537,6 +543,7 @@ def deploy_template(self, poweron=False, wait_for_ip=False): hostsystem = get_obj(self.content, [vim.HostSystem], self.params['esxi_hostname']) resource_pools = get_all_objs(self.content, [vim.ResourcePool]) + # set the destination datastore in the relocation spec if self.params['disk']: datastore_name = self.params['disk'][0]['datastore'] datastore = get_obj(self.content, [vim.Datastore], datastore_name) @@ -546,6 +553,7 @@ def deploy_template(self, poweron=False, wait_for_ip=False): datastore = disks[0].backing.datastore datastore_name = datastore.name + # create the relocation spec relospec = vim.vm.RelocateSpec() relospec.datastore = datastore @@ -572,6 +580,13 @@ def deploy_template(self, poweron=False, wait_for_ip=False): if pspec.get('type', '').lower() == 'thin': diskspec.device.backing.thinProvisioned = True + # which datastore? + if pspec.get('datastore'): + # This is already handled by the relocation spec, + # but it needs to eventually be handled for all the + # other disks defined + pass + # what size is it? if [x for x in pspec.keys() if x.startswith('size_') or x == 'size']: # size_tb, size_gb, size_mb, size_kb, size_b ...? @@ -584,8 +599,10 @@ def deploy_template(self, poweron=False, wait_for_ip=False): unit = pspec['size'].replace(expected, '').lower() expected = int(expected) else: - expected = [x for x in pspec.keys() if x.startswith('size_')][0] - unit = expected.split('_')[-1].lower() + param = [x for x in pspec.keys() if x.startswith('size_')][0] + unit = param.split('_')[-1].lower() + expected = [x[1] for x in pspec.items() if x[0].startswith('size_')][0] + expected = int(expected) kb = None if unit == 'tb': @@ -609,6 +626,8 @@ def deploy_template(self, poweron=False, wait_for_ip=False): self.wait_for_task(task) if task.info.state == 'error': + # https://kb.vmware.com/selfservice/microsites/search.do?language=en_US&cmd=displayKC&externalId=2021361 + # https://kb.vmware.com/selfservice/microsites/search.do?language=en_US&cmd=displayKC&externalId=2173 return ({'changed': False, 'failed': True, 'msg': task.info.error.msg}) else: From 2c7563b9bb637b9dbb40eda480569888d7822ea7 Mon Sep 17 00:00:00 2001 From: Adrian Likins Date: Thu, 15 Sep 2016 14:52:55 -0400 Subject: [PATCH 2107/2522] Add modules for NetApp SANtricity storage platform (#2929) The modules prefixed with netapp_e* are built to support the SANtricity storage platform. The modules provide idempotent provisioning for volume groups, disk pools, standard volumes, thin volumes, LUN mapping, hosts, host groups (clusters), volume snapshots, consistency groups, and asynchronous mirroring. They require the SANtricity WebServices Proxy. The WebServices Proxy is free software available at the NetApp Software Download site: http://mysupport.netapp.com/NOW/download/software/eseries_webservices/1.40.X000.0009/ Starting with the E2800 platform (11.30 OS), the modules will work directly with the storage array. Starting with this platform, REST API requests are handled directly on the box. This array can still be managed by proxy for large scale deployments. --- storage/netapp/README.md | 454 +++++++++++ storage/netapp/netapp_e_amg.py | 328 ++++++++ storage/netapp/netapp_e_amg_role.py | 239 ++++++ storage/netapp/netapp_e_amg_sync.py | 269 +++++++ storage/netapp/netapp_e_auth.py | 283 +++++++ storage/netapp/netapp_e_facts.py | 12 +- storage/netapp/netapp_e_flashcache.py | 420 ++++++++++ storage/netapp/netapp_e_host.py | 425 ++++++++++ storage/netapp/netapp_e_hostgroup.py | 413 ++++++++++ storage/netapp/netapp_e_lun_mapping.py | 365 +++++++++ storage/netapp/netapp_e_snapshot_group.py | 382 +++++++++ storage/netapp/netapp_e_snapshot_images.py | 250 ++++++ storage/netapp/netapp_e_snapshot_volume.py | 287 +++++++ storage/netapp/netapp_e_storage_system.py | 9 +- storage/netapp/netapp_e_storagepool.py | 884 +++++++++++++++++++++ storage/netapp/netapp_e_volume.py | 618 ++++++++++++++ storage/netapp/netapp_e_volume_copy.py | 439 ++++++++++ 17 files changed, 6063 insertions(+), 14 deletions(-) create mode 100644 storage/netapp/README.md create mode 100644 storage/netapp/netapp_e_amg.py create mode 100644 storage/netapp/netapp_e_amg_role.py create mode 100644 storage/netapp/netapp_e_amg_sync.py create mode 100644 storage/netapp/netapp_e_auth.py create mode 100644 storage/netapp/netapp_e_flashcache.py create mode 100644 storage/netapp/netapp_e_host.py create mode 100644 storage/netapp/netapp_e_hostgroup.py create mode 100644 storage/netapp/netapp_e_lun_mapping.py create mode 100644 storage/netapp/netapp_e_snapshot_group.py create mode 100644 storage/netapp/netapp_e_snapshot_images.py create mode 100644 storage/netapp/netapp_e_snapshot_volume.py create mode 100644 storage/netapp/netapp_e_storagepool.py create mode 100644 storage/netapp/netapp_e_volume.py create mode 100644 storage/netapp/netapp_e_volume_copy.py diff --git a/storage/netapp/README.md b/storage/netapp/README.md new file mode 100644 index 00000000000..8d5ab2fd4cf --- /dev/null +++ b/storage/netapp/README.md @@ -0,0 +1,454 @@ +#NetApp Storage Modules +This directory contains modules that support the storage platforms in the NetApp portfolio. + +##SANtricity Modules +The modules prefixed with *netapp\_e* are built to support the SANtricity storage platform. They require the SANtricity +WebServices Proxy. The WebServices Proxy is free software available at the [NetApp Software Download site](http://mysupport.netapp.com/NOW/download/software/eseries_webservices/1.40.X000.0009/). +Starting with the E2800 platform (11.30 OS), the modules will work directly with the storage array. Starting with this +platform, REST API requests are handled directly on the box. This array can still be managed by proxy for large scale deployments. +The modules provide idempotent provisioning for volume groups, disk pools, standard volumes, thin volumes, LUN mapping, +hosts, host groups (clusters), volume snapshots, consistency groups, and asynchronous mirroring. +### Prerequisites +| Software | Version | +| -------- |:-------:| +| SANtricity Web Services Proxy*|1.4 or 2.0| +| Ansible | 2.2** | +\* Not required for *E2800 with 11.30 OS*
+\*\*The modules where developed with this version. Ansible forward and backward compatibility applies. + +###Questions and Contribution +Please feel free to submit pull requests with improvements. Issues for these modules should be routed to @hulquest but +we also try to keep an eye on the list for issues specific to these modules. General questions can be made to our [development team](mailto:ng-hsg-engcustomer-esolutions-support@netapp.com) + +### Examples +These examples are not comprehensive but are intended to help you get started when integrating storage provisioning into +your playbooks. +```yml +- name: NetApp Test All Modules + hosts: proxy20 + gather_facts: yes + connection: local + vars: + storage_systems: + ansible1: + address1: "10.251.230.41" + address2: "10.251.230.42" + ansible2: + address1: "10.251.230.43" + address2: "10.251.230.44" + ansible3: + address1: "10.251.230.45" + address2: "10.251.230.46" + ansible4: + address1: "10.251.230.47" + address2: "10.251.230.48" + storage_pools: + Disk_Pool_1: + raid_level: raidDiskPool + criteria_drive_count: 11 + Disk_Pool_2: + raid_level: raidDiskPool + criteria_drive_count: 11 + Disk_Pool_3: + raid_level: raid0 + criteria_drive_count: 2 + volumes: + vol_1: + storage_pool_name: Disk_Pool_1 + size: 10 + thin_provision: false + thin_volume_repo_size: 7 + vol_2: + storage_pool_name: Disk_Pool_2 + size: 10 + thin_provision: false + thin_volume_repo_size: 7 + vol_3: + storage_pool_name: Disk_Pool_3 + size: 10 + thin_provision: false + thin_volume_repo_size: 7 + thin_vol_1: + storage_pool_name: Disk_Pool_1 + size: 10 + thin_provision: true + thin_volume_repo_size: 7 + hosts: + ANSIBLE-1: + host_type: 1 + index: 1 + ports: + - type: 'fc' + label: 'fpPort1' + port: '2100000E1E191B01' + + netapp_api_host: 10.251.230.29 + netapp_api_url: http://{{ netapp_api_host }}/devmgr/v2 + netapp_api_username: rw + netapp_api_password: rw + ssid: ansible1 + auth: no + lun_mapping: no + netapp_api_validate_certs: False + snapshot: no + gather_facts: no + amg_create: no + remove_volume: no + make_volume: no + check_thins: no + remove_storage_pool: yes + check_storage_pool: yes + remove_storage_system: no + check_storage_system: yes + change_role: no + flash_cache: False + configure_hostgroup: no + configure_async_mirror: False + configure_snapshot: no + copy_volume: False + volume_copy_source_volume_id: + volume_destination_source_volume_id: + snapshot_volume_storage_pool_name: Disk_Pool_3 + snapshot_volume_image_id: 3400000060080E5000299B640063074057BC5C5E + snapshot_volume: no + snapshot_volume_name: vol_1_snap_vol + host_type_index: 1 + host_name: ANSIBLE-1 + set_host: no + remove_host: no + amg_member_target_array: + amg_member_primary_pool: + amg_member_secondary_pool: + amg_member_primary_volume: + amg_member_secondary_volume: + set_amg_member: False + amg_array_name: foo + amg_name: amg_made_by_ansible + amg_secondaryArrayId: ansible2 + amg_sync_name: foo + amg_sync: no + + tasks: + + - name: Get array facts + netapp_e_facts: + ssid: "{{ item.key }}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" + with_dict: "{{ storage_systems }}" + when: gather_facts + + - name: Presence of storage system + netapp_e_storage_system: + ssid: "{{ item.key }}" + state: present + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" + controller_addresses: + - "{{ item.value.address1 }}" + - "{{ item.value.address2 }}" + with_dict: "{{ storage_systems }}" + when: check_storage_system + + - name: Create Snapshot + netapp_e_snapshot_images: + ssid: "{{ ssid }}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" + snapshot_group: "ansible_snapshot_group" + state: 'create' + when: snapshot + + - name: Auth Module Example + netapp_e_auth: + ssid: "{{ ssid }}" + current_password: 'Infinit2' + new_password: 'Infinit1' + set_admin: yes + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + when: auth + + - name: No disk groups + netapp_e_storagepool: + ssid: "{{ ssid }}" + name: "{{ item }}" + state: absent + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" + remove_volumes: yes + with_items: + - Disk_Pool_1 + - Disk_Pool_2 + - Disk_Pool_3 + when: remove_storage_pool + + - name: Make disk groups + netapp_e_storagepool: + ssid: "{{ ssid }}" + name: "{{ item.key }}" + state: present + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" + raid_level: "{{ item.value.raid_level }}" + criteria_drive_count: "{{ item.value.criteria_drive_count }}" + with_dict: " {{ storage_pools }}" + when: check_storage_pool + + - name: No thin volume + netapp_e_volume: + ssid: "{{ ssid }}" + name: NewThinVolumeByAnsible + state: absent + thin_provision: yes + log_path: /tmp/volume.log + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" + when: check_thins + + - name: Make a thin volume + netapp_e_volume: + ssid: "{{ ssid }}" + name: NewThinVolumeByAnsible + state: present + thin_provision: yes + thin_volume_repo_size: 7 + size: 10 + log_path: /tmp/volume.log + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" + storage_pool_name: Disk_Pool_1 + when: check_thins + + - name: Remove standard/thick volumes + netapp_e_volume: + ssid: "{{ ssid }}" + name: "{{ item.key }}" + state: absent + log_path: /tmp/volume.log + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" + with_dict: "{{ volumes }}" + when: remove_volume + + - name: Make a volume + netapp_e_volume: + ssid: "{{ ssid }}" + name: "{{ item.key }}" + state: present + storage_pool_name: "{{ item.value.storage_pool_name }}" + size: "{{ item.value.size }}" + thin_provision: "{{ item.value.thin_provision }}" + thin_volume_repo_size: "{{ item.value.thin_volume_repo_size }}" + log_path: /tmp/volume.log + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" + with_dict: "{{ volumes }}" + when: make_volume + + - name: No storage system + netapp_e_storage_system: + ssid: "{{ item.key }}" + state: absent + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" + with_dict: "{{ storage_systems }}" + when: remove_storage_system + + - name: Update the role of a storage array + netapp_e_amg_role: + name: "{{ amg_name }}" + role: primary + force: true + noSync: true + ssid: "{{ ssid }}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" + when: change_role + + - name: Flash Cache + netapp_e_flashcache: + ssid: "{{ ssid }}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" + name: SSDCacheBuiltByAnsible + when: flash_cache + + - name: Configure Hostgroup + netapp_e_hostgroup: + ssid: "{{ ssid }}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" + state: absent + name: "ansible-host-group" + when: configure_hostgroup + + - name: Configure Snapshot group + netapp_e_snapshot_group: + ssid: "{{ ssid }}" + state: present + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" + base_volume_name: vol_3 + name: ansible_snapshot_group + repo_pct: 20 + warning_threshold: 85 + delete_limit: 30 + full_policy: purgepit + storage_pool_name: Disk_Pool_3 + rollback_priority: medium + when: configure_snapshot + + - name: Copy volume + netapp_e_volume_copy: + ssid: "{{ ssid }}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + status: present + source_volume_id: "{{ volume_copy_source_volume_id }}" + destination_volume_id: "{{ volume_destination_source_volume_id }}" + when: copy_volume + + - name: Snapshot volume + netapp_e_snapshot_volume: + ssid: "{{ ssid }}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + state: present + storage_pool_name: "{{ snapshot_volume_storage_pool_name }}" + snapshot_image_id: "{{ snapshot_volume_image_id }}" + name: "{{ snapshot_volume_name }}" + when: snapshot_volume + + - name: Remove hosts + netapp_e_host: + ssid: "{{ ssid }}" + state: absent + name: "{{ item.key }}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + host_type_index: "{{ host_type_index }}" + with_dict: "{{hosts}}" + when: remove_host + + - name: Ensure/add hosts + netapp_e_host: + ssid: "{{ ssid }}" + state: present + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + name: "{{ item.key }}" + host_type_index: "{{ item.value.index }}" + ports: + - type: 'fc' + label: 'fpPort1' + port: '2100000E1E191B01' + with_dict: "{{hosts}}" + when: set_host + + - name: Unmap a volume + netapp_e_lun_mapping: + state: absent + ssid: "{{ ssid }}" + lun: 2 + target: "{{ host_name }}" + volume_name: "thin_vol_1" + target_type: host + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + when: lun_mapping + + - name: Map a volume + netapp_e_lun_mapping: + state: present + ssid: "{{ ssid }}" + lun: 16 + target: "{{ host_name }}" + volume_name: "thin_vol_1" + target_type: host + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + when: lun_mapping + + - name: Update LUN Id + netapp_e_lun_mapping: + state: present + ssid: "{{ ssid }}" + lun: 2 + target: "{{ host_name }}" + volume_name: "thin_vol_1" + target_type: host + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + when: lun_mapping + + - name: AMG removal + netapp_e_amg: + state: absent + ssid: "{{ ssid }}" + secondaryArrayId: "{{amg_secondaryArrayId}}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + new_name: "{{amg_array_name}}" + name: "{{amg_name}}" + when: amg_create + + - name: AMG create + netapp_e_amg: + state: present + ssid: "{{ ssid }}" + secondaryArrayId: "{{amg_secondaryArrayId}}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + new_name: "{{amg_array_name}}" + name: "{{amg_name}}" + when: amg_create + + - name: start AMG async + netapp_e_amg_sync: + name: "{{ amg_name }}" + state: running + ssid: "{{ ssid }}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + when: amg_sync +``` diff --git a/storage/netapp/netapp_e_amg.py b/storage/netapp/netapp_e_amg.py new file mode 100644 index 00000000000..44189988be4 --- /dev/null +++ b/storage/netapp/netapp_e_amg.py @@ -0,0 +1,328 @@ +#!/usr/bin/python +# (c) 2016, NetApp, Inc +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +DOCUMENTATION = """ +--- +module: netapp_e_amg +short_description: Create, Remove, and Update Asynchronous Mirror Groups +description: + - Allows for the creation, removal and updating of Asynchronous Mirror Groups for NetApp E-series storage arrays +version_added: '2.2' +author: Kevin Hulquest (@hulquest) +options: + api_username: + required: true + description: + - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_password: + required: true + description: + - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_url: + required: true + description: + - The url to the SANtricity WebServices Proxy or embedded REST API. + example: + - https://prod-1.wahoo.acme.com/devmgr/v2 + validate_certs: + required: false + default: true + description: + - Should https certificates be validated? + name: + description: + - The name of the async array you wish to target, or create. + - If C(state) is present and the name isn't found, it will attempt to create. + required: yes + secondaryArrayId: + description: + - The ID of the secondary array to be used in mirroing process + required: yes + syncIntervalMinutes: + description: + - The synchronization interval in minutes + required: no + default: 10 + manualSync: + description: + - Setting this to true will cause other synchronization values to be ignored + required: no + default: no + recoveryWarnThresholdMinutes: + description: + - Recovery point warning threshold (minutes). The user will be warned when the age of the last good failures point exceeds this value + required: no + default: 20 + repoUtilizationWarnThreshold: + description: + - Recovery point warning threshold + required: no + default: 80 + interfaceType: + description: + - The intended protocol to use if both Fibre and iSCSI are available. + choices: + - iscsi + - fibre + required: no + default: null + syncWarnThresholdMinutes: + description: + - The threshold (in minutes) for notifying the user that periodic synchronization has taken too long to complete. + required: no + default: 10 + ssid: + description: + - The ID of the primary storage array for the async mirror action + required: yes + state: + description: + - A C(state) of present will either create or update the async mirror group. + - A C(state) of absent will remove the async mirror group. + required: yes +""" + +EXAMPLES = """ + - name: AMG removal + na_eseries_amg: + state: absent + ssid: "{{ ssid }}" + secondaryArrayId: "{{amg_secondaryArrayId}}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + new_name: "{{amg_array_name}}" + name: "{{amg_name}}" + when: amg_create + + - name: AMG create + netapp_e_amg: + state: present + ssid: "{{ ssid }}" + secondaryArrayId: "{{amg_secondaryArrayId}}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + new_name: "{{amg_array_name}}" + name: "{{amg_name}}" + when: amg_create +""" + +RETURN = """ +msg: + description: Successful removal + returned: success + type: string + sample: "Async mirror group removed." + +msg: + description: Successful creation + returned: success + type: string + sample: '{"changed": true, "connectionType": "fc", "groupRef": "3700000060080E5000299C24000006E857AC7EEC", "groupState": "optimal", "id": "3700000060080E5000299C24000006E857AC7EEC", "label": "amg_made_by_ansible", "localRole": "primary", "mirrorChannelRemoteTarget": "9000000060080E5000299C24005B06E557AC7EEC", "orphanGroup": false, "recoveryPointAgeAlertThresholdMinutes": 20, "remoteRole": "secondary", "remoteTarget": {"nodeName": {"ioInterfaceType": "fc", "iscsiNodeName": null, "remoteNodeWWN": "20040080E5299F1C"}, "remoteRef": "9000000060080E5000299C24005B06E557AC7EEC", "scsiinitiatorTargetBaseProperties": {"ioInterfaceType": "fc", "iscsiinitiatorTargetBaseParameters": null}}, "remoteTargetId": "ansible2", "remoteTargetName": "Ansible2", "remoteTargetWwn": "60080E5000299F880000000056A25D56", "repositoryUtilizationWarnThreshold": 80, "roleChangeProgress": "none", "syncActivity": "idle", "syncCompletionTimeAlertThresholdMinutes": 10, "syncIntervalMinutes": 10, "worldWideName": "60080E5000299C24000006E857AC7EEC"}' +""" + +import json + +from ansible.module_utils.api import basic_auth_argument_spec +from ansible.module_utils.basic import AnsibleModule, get_exception +from ansible.module_utils.urls import open_url +from ansible.module_utils.six.moves.urllib.error import HTTPError + +HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json", +} + + +def request(url, data=None, headers=None, method='GET', use_proxy=True, + force=False, last_mod_time=None, timeout=10, validate_certs=True, + url_username=None, url_password=None, http_agent=None, force_basic_auth=False, ignore_errors=False): + try: + r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy, + force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs, + url_username=url_username, url_password=url_password, http_agent=http_agent, + force_basic_auth=force_basic_auth) + except HTTPError: + err = get_exception() + r = err.fp + + try: + raw_data = r.read() + if raw_data: + data = json.loads(raw_data) + else: + data = None + except: + if ignore_errors: + pass + else: + raise Exception(raw_data) + + resp_code = r.getcode() + + if resp_code >= 400 and not ignore_errors: + raise Exception(resp_code, data) + else: + return resp_code, data + + +def has_match(module, ssid, api_url, api_pwd, api_usr, body): + compare_keys = ['syncIntervalMinutes', 'syncWarnThresholdMinutes', + 'recoveryWarnThresholdMinutes', 'repoUtilizationWarnThreshold'] + desired_state = dict((x, (body.get(x))) for x in compare_keys) + label_exists = False + matches_spec = False + current_state = None + async_id = None + api_data = None + desired_name = body.get('name') + endpoint = 'storage-systems/%s/async-mirrors' % ssid + url = api_url + endpoint + try: + rc, data = request(url, url_username=api_usr, url_password=api_pwd, headers=HEADERS) + except Exception: + error = get_exception() + module.exit_json(exception="Error finding a match. Message: %s" % str(error)) + + for async_group in data: + if async_group['label'] == desired_name: + label_exists = True + api_data = async_group + async_id = async_group['groupRef'] + current_state = dict( + syncIntervalMinutes=async_group['syncIntervalMinutes'], + syncWarnThresholdMinutes=async_group['syncCompletionTimeAlertThresholdMinutes'], + recoveryWarnThresholdMinutes=async_group['recoveryPointAgeAlertThresholdMinutes'], + repoUtilizationWarnThreshold=async_group['repositoryUtilizationWarnThreshold'], + ) + + if current_state == desired_state: + matches_spec = True + + return label_exists, matches_spec, api_data, async_id + + +def create_async(module, ssid, api_url, api_pwd, api_usr, body): + endpoint = 'storage-systems/%s/async-mirrors' % ssid + url = api_url + endpoint + post_data = json.dumps(body) + try: + rc, data = request(url, data=post_data, method='POST', url_username=api_usr, url_password=api_pwd, + headers=HEADERS) + except Exception: + error = get_exception() + module.exit_json(exception="Exception while creating aysnc mirror group. Message: %s" % str(error)) + return data + + +def update_async(module, ssid, api_url, pwd, user, body, new_name, async_id): + endpoint = 'storage-systems/%s/async-mirrors/%s' % (ssid, async_id) + url = api_url + endpoint + compare_keys = ['syncIntervalMinutes', 'syncWarnThresholdMinutes', + 'recoveryWarnThresholdMinutes', 'repoUtilizationWarnThreshold'] + desired_state = dict((x, (body.get(x))) for x in compare_keys) + + if new_name: + desired_state['new_name'] = new_name + + post_data = json.dumps(desired_state) + + try: + rc, data = request(url, data=post_data, method='POST', headers=HEADERS, + url_username=user, url_password=pwd) + except Exception: + error = get_exception() + module.exit_json(exception="Exception while updating async mirror group. Message: %s" % str(error)) + + return data + + +def remove_amg(module, ssid, api_url, pwd, user, async_id): + endpoint = 'storage-systems/%s/async-mirrors/%s' % (ssid, async_id) + url = api_url + endpoint + try: + rc, data = request(url, method='DELETE', url_username=user, url_password=pwd, + headers=HEADERS) + except Exception: + error = get_exception() + module.exit_json(exception="Exception while removing async mirror group. Message: %s" % str(error)) + + return + + +def main(): + argument_spec = basic_auth_argument_spec() + argument_spec.update(dict( + api_username=dict(type='str', required=True), + api_password=dict(type='str', required=True, no_log=True), + api_url=dict(type='str', required=True), + name=dict(required=True, type='str'), + new_name=dict(required=False, type='str'), + secondaryArrayId=dict(required=True, type='str'), + syncIntervalMinutes=dict(required=False, default=10, type='int'), + manualSync=dict(required=False, default=False, type='bool'), + recoveryWarnThresholdMinutes=dict(required=False, default=20, type='int'), + repoUtilizationWarnThreshold=dict(required=False, default=80, type='int'), + interfaceType=dict(required=False, choices=['fibre', 'iscsi'], type='str'), + ssid=dict(required=True, type='str'), + state=dict(required=True, choices=['present', 'absent']), + syncWarnThresholdMinutes=dict(required=False, default=10, type='int') + )) + + module = AnsibleModule(argument_spec=argument_spec) + + p = module.params + + ssid = p.pop('ssid') + api_url = p.pop('api_url') + user = p.pop('api_username') + pwd = p.pop('api_password') + new_name = p.pop('new_name') + state = p.pop('state') + + if not api_url.endswith('/'): + api_url += '/' + + name_exists, spec_matches, api_data, async_id = has_match(module, ssid, api_url, pwd, user, p) + + if state == 'present': + if name_exists and spec_matches: + module.exit_json(changed=False, msg="Desired state met", **api_data) + elif name_exists and not spec_matches: + results = update_async(module, ssid, api_url, pwd, user, + p, new_name, async_id) + module.exit_json(changed=True, + msg="Async mirror group updated", async_id=async_id, + **results) + elif not name_exists: + results = create_async(module, ssid, api_url, user, pwd, p) + module.exit_json(changed=True, **results) + + elif state == 'absent': + if name_exists: + remove_amg(module, ssid, api_url, pwd, user, async_id) + module.exit_json(changed=True, msg="Async mirror group removed.", + async_id=async_id) + else: + module.exit_json(changed=False, + msg="Async Mirror group: %s already absent" % p['name']) + + +if __name__ == '__main__': + main() diff --git a/storage/netapp/netapp_e_amg_role.py b/storage/netapp/netapp_e_amg_role.py new file mode 100644 index 00000000000..7a2f1bdf18b --- /dev/null +++ b/storage/netapp/netapp_e_amg_role.py @@ -0,0 +1,239 @@ +#!/usr/bin/python + +# (c) 2016, NetApp, Inc +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +DOCUMENTATION = """ +--- +module: netapp_e_amg_role +short_description: Update the role of a storage array within an Asynchronous Mirror Group (AMG). +description: + - Update a storage array to become the primary or secondary instance in an asynchronous mirror group +version_added: '2.2' +author: Kevin Hulquest (@hulquest) +options: + api_username: + required: true + description: + - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_password: + required: true + description: + - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_url: + required: true + description: + - The url to the SANtricity WebServices Proxy or embedded REST API. + example: + - https://prod-1.wahoo.acme.com/devmgr/v2 + validate_certs: + required: false + default: true + description: + - Should https certificates be validated? + ssid: + description: + - The ID of the primary storage array for the async mirror action + required: yes + role: + description: + - Whether the array should be the primary or secondary array for the AMG + required: yes + choices: ['primary', 'secondary'] + noSync: + description: + - Whether to avoid synchronization prior to role reversal + required: no + default: no + choices: [yes, no] + force: + description: + - Whether to force the role reversal regardless of the online-state of the primary + required: no + default: no +""" + +EXAMPLES = """ + - name: Update the role of a storage array + netapp_e_amg_role: + name: updating amg role + role: primary + ssid: "{{ ssid }}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" +""" + +RETURN = """ +msg: + description: Failure message + returned: failure + type: string + sample: "No Async Mirror Group with the name." +""" +import json + +from ansible.module_utils.api import basic_auth_argument_spec +from ansible.module_utils.basic import AnsibleModule + +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import open_url +from ansible.module_utils.six.moves.urllib.error import HTTPError + +HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json", +} + + +def request(url, data=None, headers=None, method='GET', use_proxy=True, + force=False, last_mod_time=None, timeout=10, validate_certs=True, + url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False): + try: + r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy, + force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs, + url_username=url_username, url_password=url_password, http_agent=http_agent, + force_basic_auth=force_basic_auth) + except HTTPError: + err = get_exception() + r = err.fp + + try: + raw_data = r.read() + if raw_data: + data = json.loads(raw_data) + else: + raw_data = None + except: + if ignore_errors: + pass + else: + raise Exception(raw_data) + + resp_code = r.getcode() + + if resp_code >= 400 and not ignore_errors: + raise Exception(resp_code, data) + else: + return resp_code, data + + +def has_match(module, ssid, api_url, api_pwd, api_usr, body, name): + amg_exists = False + has_desired_role = False + amg_id = None + amg_data = None + get_amgs = 'storage-systems/%s/async-mirrors' % ssid + url = api_url + get_amgs + try: + amg_rc, amgs = request(url, url_username=api_usr, url_password=api_pwd, + headers=HEADERS) + except: + module.fail_json(msg="Failed to find AMGs on storage array. Id [%s]" % (ssid)) + + for amg in amgs: + if amg['label'] == name: + amg_exists = True + amg_id = amg['id'] + amg_data = amg + if amg['localRole'] == body.get('role'): + has_desired_role = True + + return amg_exists, has_desired_role, amg_id, amg_data + + +def update_amg(module, ssid, api_url, api_usr, api_pwd, body, amg_id): + endpoint = 'storage-systems/%s/async-mirrors/%s/role' % (ssid, amg_id) + url = api_url + endpoint + post_data = json.dumps(body) + try: + request(url, data=post_data, method='POST', url_username=api_usr, + url_password=api_pwd, headers=HEADERS) + except: + err = get_exception() + module.fail_json( + msg="Failed to change role of AMG. Id [%s]. AMG Id [%s]. Error [%s]" % (ssid, amg_id, str(err))) + + status_endpoint = 'storage-systems/%s/async-mirrors/%s' % (ssid, amg_id) + status_url = api_url + status_endpoint + try: + rc, status = request(status_url, method='GET', url_username=api_usr, + url_password=api_pwd, headers=HEADERS) + except: + err = get_exception() + module.fail_json( + msg="Failed to check status of AMG after role reversal. " + + "Id [%s]. AMG Id [%s]. Error [%s]" % (ssid, amg_id, str(err))) + + # Here we wait for the role reversal to complete + if 'roleChangeProgress' in status: + while status['roleChangeProgress'] != "none": + try: + rc, status = request(status_url, method='GET', + url_username=api_usr, url_password=api_pwd, headers=HEADERS) + except: + err = get_exception() + module.fail_json( + msg="Failed to check status of AMG after role reversal. " + + "Id [%s]. AMG Id [%s]. Error [%s]" % (ssid, amg_id, str(err))) + return status + + +def main(): + argument_spec = basic_auth_argument_spec() + argument_spec.update(dict( + name=dict(required=True, type='str'), + role=dict(required=True, choices=['primary', 'secondary']), + noSync=dict(required=False, type='bool', default=False), + force=dict(required=False, type='bool', default=False), + ssid=dict(required=True, type='str'), + api_url=dict(required=True), + api_username=dict(required=False), + api_password=dict(required=False, no_log=True), + )) + + module = AnsibleModule(argument_spec=argument_spec) + + p = module.params + + ssid = p.pop('ssid') + api_url = p.pop('api_url') + user = p.pop('api_username') + pwd = p.pop('api_password') + name = p.pop('name') + + if not api_url.endswith('/'): + api_url += '/' + + agm_exists, has_desired_role, async_id, amg_data = has_match(module, ssid, api_url, pwd, user, p, name) + + if not agm_exists: + module.fail_json(msg="No Async Mirror Group with the name: '%s' was found" % name) + elif has_desired_role: + module.exit_json(changed=False, **amg_data) + + else: + amg_data = update_amg(module, ssid, api_url, user, pwd, p, async_id) + if amg_data: + module.exit_json(changed=True, **amg_data) + else: + module.exit_json(changed=True, msg="AMG role changed.") + + +if __name__ == '__main__': + main() diff --git a/storage/netapp/netapp_e_amg_sync.py b/storage/netapp/netapp_e_amg_sync.py new file mode 100644 index 00000000000..a86b594f3b0 --- /dev/null +++ b/storage/netapp/netapp_e_amg_sync.py @@ -0,0 +1,269 @@ +#!/usr/bin/python + +# (c) 2016, NetApp, Inc +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +DOCUMENTATION = """ +--- +module: netapp_e_amg_sync +short_description: Conduct synchronization actions on asynchronous member groups. +description: + - Allows for the initialization, suspension and resumption of an asynchronous mirror group's synchronization for NetApp E-series storage arrays. +version_added: '2.2' +author: Kevin Hulquest (@hulquest) +options: + api_username: + required: true + description: + - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_password: + required: true + description: + - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_url: + required: true + description: + - The url to the SANtricity WebServices Proxy or embedded REST API. + example: + - https://prod-1.wahoo.acme.com/devmgr/v2 + validate_certs: + required: false + default: true + description: + - Should https certificates be validated? + ssid: + description: + - The ID of the storage array containing the AMG you wish to target + name: + description: + - The name of the async mirror group you wish to target + required: yes + state: + description: + - The synchronization action you'd like to take. + - If C(running) then it will begin syncing if there is no active sync or will resume a suspended sync. If there is already a sync in progress, it will return with an OK status. + - If C(suspended) it will suspend any ongoing sync action, but return OK if there is no active sync or if the sync is already suspended + choices: + - running + - suspended + required: yes + delete_recovery_point: + description: + - Indicates whether the failures point can be deleted on the secondary if necessary to achieve the synchronization. + - If true, and if the amount of unsynchronized data exceeds the CoW repository capacity on the secondary for any member volume, the last failures point will be deleted and synchronization will continue. + - If false, the synchronization will be suspended if the amount of unsynchronized data exceeds the CoW Repository capacity on the secondary and the failures point will be preserved. + - "NOTE: This only has impact for newly launched syncs." + choices: + - yes + - no + default: no +""" +EXAMPLES = """ + - name: start AMG async + netapp_e_amg_sync: + name: "{{ amg_sync_name }}" + state: running + ssid: "{{ ssid }}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" +""" +RETURN = """ +json: + description: The object attributes of the AMG. + returned: success + type: string + example: + { + "changed": false, + "connectionType": "fc", + "groupRef": "3700000060080E5000299C24000006EF57ACAC70", + "groupState": "optimal", + "id": "3700000060080E5000299C24000006EF57ACAC70", + "label": "made_with_ansible", + "localRole": "primary", + "mirrorChannelRemoteTarget": "9000000060080E5000299C24005B06E557AC7EEC", + "orphanGroup": false, + "recoveryPointAgeAlertThresholdMinutes": 20, + "remoteRole": "secondary", + "remoteTarget": { + "nodeName": { + "ioInterfaceType": "fc", + "iscsiNodeName": null, + "remoteNodeWWN": "20040080E5299F1C" + }, + "remoteRef": "9000000060080E5000299C24005B06E557AC7EEC", + "scsiinitiatorTargetBaseProperties": { + "ioInterfaceType": "fc", + "iscsiinitiatorTargetBaseParameters": null + } + }, + "remoteTargetId": "ansible2", + "remoteTargetName": "Ansible2", + "remoteTargetWwn": "60080E5000299F880000000056A25D56", + "repositoryUtilizationWarnThreshold": 80, + "roleChangeProgress": "none", + "syncActivity": "idle", + "syncCompletionTimeAlertThresholdMinutes": 10, + "syncIntervalMinutes": 10, + "worldWideName": "60080E5000299C24000006EF57ACAC70" + } +""" +import json + +from ansible.module_utils.api import basic_auth_argument_spec +from ansible.module_utils.basic import AnsibleModule + +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import open_url +from ansible.module_utils.six.moves.urllib.error import HTTPError + + +def request(url, data=None, headers=None, method='GET', use_proxy=True, + force=False, last_mod_time=None, timeout=10, validate_certs=True, + url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False): + try: + r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy, + force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs, + url_username=url_username, url_password=url_password, http_agent=http_agent, + force_basic_auth=force_basic_auth) + except HTTPError: + err = get_exception() + r = err.fp + + try: + raw_data = r.read() + if raw_data: + data = json.loads(raw_data) + else: + raw_data = None + except: + if ignore_errors: + pass + else: + raise Exception(raw_data) + + resp_code = r.getcode() + + if resp_code >= 400 and not ignore_errors: + raise Exception(resp_code, data) + else: + return resp_code, data + + +class AMGsync(object): + def __init__(self): + argument_spec = basic_auth_argument_spec() + argument_spec.update(dict( + api_username=dict(type='str', required=True), + api_password=dict(type='str', required=True, no_log=True), + api_url=dict(type='str', required=True), + name=dict(required=True, type='str'), + ssid=dict(required=True, type='str'), + state=dict(required=True, type='str', choices=['running', 'suspended']), + delete_recovery_point=dict(required=False, type='bool', default=False) + )) + self.module = AnsibleModule(argument_spec=argument_spec) + args = self.module.params + self.name = args['name'] + self.ssid = args['ssid'] + self.state = args['state'] + self.delete_recovery_point = args['delete_recovery_point'] + try: + self.user = args['api_username'] + self.pwd = args['api_password'] + self.url = args['api_url'] + except KeyError: + self.module.fail_json(msg="You must pass in api_username" + "and api_password and api_url to the module.") + self.certs = args['validate_certs'] + + self.post_headers = { + "Accept": "application/json", + "Content-Type": "application/json" + } + self.amg_id, self.amg_obj = self.get_amg() + + def get_amg(self): + endpoint = self.url + '/storage-systems/%s/async-mirrors' % self.ssid + (rc, amg_objs) = request(endpoint, url_username=self.user, url_password=self.pwd, validate_certs=self.certs, + headers=self.post_headers) + try: + amg_id = filter(lambda d: d['label'] == self.name, amg_objs)[0]['id'] + amg_obj = filter(lambda d: d['label'] == self.name, amg_objs)[0] + except IndexError: + self.module.fail_json( + msg="There is no async mirror group %s associated with storage array %s" % (self.name, self.ssid)) + return amg_id, amg_obj + + @property + def current_state(self): + amg_id, amg_obj = self.get_amg() + return amg_obj['syncActivity'] + + def run_sync_action(self): + # If we get to this point we know that the states differ, and there is no 'err' state, + # so no need to revalidate + + post_body = dict() + if self.state == 'running': + if self.current_state == 'idle': + if self.delete_recovery_point: + post_body.update(dict(deleteRecoveryPointIfNecessary=self.delete_recovery_point)) + suffix = 'sync' + else: + # In a suspended state + suffix = 'resume' + else: + suffix = 'suspend' + + endpoint = self.url + "/storage-systems/%s/async-mirrors/%s/%s" % (self.ssid, self.amg_id, suffix) + + (rc, resp) = request(endpoint, method='POST', url_username=self.user, url_password=self.pwd, + validate_certs=self.certs, data=json.dumps(post_body), headers=self.post_headers, + ignore_errors=True) + + if not str(rc).startswith('2'): + self.module.fail_json(msg=str(resp['errorMessage'])) + + return resp + + def apply(self): + state_map = dict( + running=['active'], + suspended=['userSuspended', 'internallySuspended', 'paused'], + err=['unkown', '_UNDEFINED']) + + if self.current_state not in state_map[self.state]: + if self.current_state in state_map['err']: + self.module.fail_json( + msg="The sync is a state of '%s', this requires manual intervention. " + + "Please investigate and try again" % self.current_state) + else: + self.amg_obj = self.run_sync_action() + + (ret, amg) = self.get_amg() + self.module.exit_json(changed=False, **amg) + + +def main(): + sync = AMGsync() + sync.apply() + + +if __name__ == '__main__': + main() diff --git a/storage/netapp/netapp_e_auth.py b/storage/netapp/netapp_e_auth.py new file mode 100644 index 00000000000..a9f54257a3d --- /dev/null +++ b/storage/netapp/netapp_e_auth.py @@ -0,0 +1,283 @@ +#!/usr/bin/python + +# (c) 2016, NetApp, Inc +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +DOCUMENTATION = ''' +--- +module: netapp_e_auth +short_description: Sets or updates the password for a storage array. +description: + - Sets or updates the password for a storage array. When the password is updated on the storage array, it must be updated on the SANtricity Web Services proxy. Note, all storage arrays do not have a Monitor or RO role. +version_added: "2.2" +author: Kevin Hulquest (@hulquest) +options: + api_username: + required: true + description: + - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_password: + required: true + description: + - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_url: + required: true + description: + - The url to the SANtricity WebServices Proxy or embedded REST API. + example: + - https://prod-1.wahoo.acme.com/devmgr/v2 + validate_certs: + required: false + default: true + description: + - Should https certificates be validated? + name: + description: + - The name of the storage array. Note that if more than one storage array with this name is detected, the task will fail and you'll have to use the ID instead. + required: False + ssid: + description: + - the identifier of the storage array in the Web Services Proxy. + required: False + set_admin: + description: + - Boolean value on whether to update the admin password. If set to false then the RO account is updated. + default: False + current_password: + description: + - The current admin password. This is not required if the password hasn't been set before. + required: False + new_password: + description: + - The password you would like to set. Cannot be more than 30 characters. + required: True + api_url: + description: + - The full API url. + - "Example: http://ENDPOINT:8080/devmgr/v2" + - This can optionally be set via an environment variable, API_URL + required: False + api_username: + description: + - The username used to authenticate against the API + - This can optionally be set via an environment variable, API_USERNAME + required: False + api_password: + description: + - The password used to authenticate against the API + - This can optionally be set via an environment variable, API_PASSWORD + required: False +''' + +EXAMPLES = ''' +- name: Test module + netapp_e_auth: + name: trex + current_password: 'B4Dpwd' + new_password: 'W0rs3P4sswd' + set_admin: yes + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" +''' + +RETURN = ''' +msg: + description: Success message + returned: success + type: string + sample: "Password Updated Successfully" +''' +import json + +from ansible.module_utils.api import basic_auth_argument_spec +from ansible.module_utils.basic import AnsibleModule + +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import open_url +from ansible.module_utils.six.moves.urllib.error import HTTPError + + +HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json" +} + + +def request(url, data=None, headers=None, method='GET', use_proxy=True, + force=False, last_mod_time=None, timeout=10, validate_certs=True, + url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False): + try: + r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy, + force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs, + url_username=url_username, url_password=url_password, http_agent=http_agent, + force_basic_auth=force_basic_auth) + except HTTPError: + err = get_exception() + r = err.fp + + try: + raw_data = r.read() + if raw_data: + data = json.loads(raw_data) + else: + raw_data = None + except: + if ignore_errors: + pass + else: + raise Exception(raw_data) + + resp_code = r.getcode() + + if resp_code >= 400 and not ignore_errors: + raise Exception(resp_code, data) + else: + return resp_code, data + + +def get_ssid(module, name, api_url, user, pwd): + count = 0 + all_systems = 'storage-systems' + systems_url = api_url + all_systems + rc, data = request(systems_url, headers=HEADERS, url_username=user, url_password=pwd) + for system in data: + if system['name'] == name: + count += 1 + if count > 1: + module.fail_json( + msg="You supplied a name for the Storage Array but more than 1 array was found with that name. " + + "Use the id instead") + else: + ssid = system['id'] + else: + continue + + if count == 0: + module.fail_json(msg="No storage array with the name %s was found" % name) + + else: + return ssid + + +def get_pwd_status(module, ssid, api_url, user, pwd): + pwd_status = "storage-systems/%s/passwords" % ssid + url = api_url + pwd_status + try: + rc, data = request(url, headers=HEADERS, url_username=user, url_password=pwd) + return data['readOnlyPasswordSet'], data['adminPasswordSet'] + except HTTPError: + error = get_exception() + module.fail_json(msg="There was an issue with connecting, please check that your " + "endpoint is properly defined and your credentials are correct: %s" % str(error)) + + +def update_storage_system_pwd(module, ssid, pwd, api_url, api_usr, api_pwd): + update_pwd = 'storage-systems/%s' % ssid + url = api_url + update_pwd + post_body = json.dumps(dict(storedPassword=pwd)) + try: + rc, data = request(url, data=post_body, method='POST', headers=HEADERS, url_username=api_usr, + url_password=api_pwd) + except: + err = get_exception() + module.fail_json(msg="Failed to update system password. Id [%s]. Error [%s]" % (ssid, str(err))) + return data + + +def set_password(module, ssid, api_url, user, pwd, current_password=None, new_password=None, set_admin=False): + set_pass = "storage-systems/%s/passwords" % ssid + url = api_url + set_pass + + if not current_password: + current_password = "" + + post_body = json.dumps( + dict(currentAdminPassword=current_password, adminPassword=set_admin, newPassword=new_password)) + + try: + rc, data = request(url, method='POST', data=post_body, headers=HEADERS, url_username=user, url_password=pwd, + ignore_errors=True) + except: + err = get_exception() + module.fail_json(msg="Failed to set system password. Id [%s]. Error [%s]" % (ssid, str(err))) + + if rc == 422: + post_body = json.dumps(dict(currentAdminPassword='', adminPassword=set_admin, newPassword=new_password)) + try: + rc, data = request(url, method='POST', data=post_body, headers=HEADERS, url_username=user, url_password=pwd) + except Exception: + module.fail_json(msg="Wrong or no admin password supplied. Please update your playbook and try again") + + update_data = update_storage_system_pwd(module, ssid, new_password, api_url, user, pwd) + + if int(rc) == 204: + return update_data + else: + module.fail_json(msg="%s:%s" % (rc, data)) + + +def main(): + argument_spec = basic_auth_argument_spec() + argument_spec.update(dict( + name=dict(required=False, type='str'), + ssid=dict(required=False, type='str'), + current_password=dict(required=False, no_log=True), + new_password=dict(required=True, no_log=True), + set_admin=dict(required=True, type='bool'), + api_url=dict(required=True), + api_username=dict(required=False), + api_password=dict(required=False, no_log=True) + ) + ) + module = AnsibleModule(argument_spec=argument_spec, mutually_exclusive=[['name', 'ssid']], + required_one_of=[['name', 'ssid']]) + + name = module.params['name'] + ssid = module.params['ssid'] + current_password = module.params['current_password'] + new_password = module.params['new_password'] + set_admin = module.params['set_admin'] + user = module.params['api_username'] + pwd = module.params['api_password'] + api_url = module.params['api_url'] + + if not api_url.endswith('/'): + api_url += '/' + + if name: + ssid = get_ssid(module, name, api_url, user, pwd) + + ro_pwd, admin_pwd = get_pwd_status(module, ssid, api_url, user, pwd) + + if admin_pwd and not current_password: + module.fail_json( + msg="Admin account has a password set. " + + "You must supply current_password in order to update the RO or Admin passwords") + + if len(new_password) > 30: + module.fail_json(msg="Passwords must not be greater than 30 characters in length") + + success = set_password(module, ssid, api_url, user, pwd, current_password=current_password, + new_password=new_password, + set_admin=set_admin) + + module.exit_json(changed=True, msg="Password Updated Successfully", **success) + + +if __name__ == '__main__': + main() diff --git a/storage/netapp/netapp_e_facts.py b/storage/netapp/netapp_e_facts.py index 514002b9d38..37e3f827627 100644 --- a/storage/netapp/netapp_e_facts.py +++ b/storage/netapp/netapp_e_facts.py @@ -18,7 +18,7 @@ # along with Ansible. If not, see . # DOCUMENTATION = ''' -module: na_eseries_facts +module: netapp_e_facts version_added: '2.2' short_description: Get facts about NetApp E-Series arrays options: @@ -55,7 +55,7 @@ EXAMPLES = """ --- - name: Get array facts - na_eseries_facts: + netapp_e_facts: array_id: "{{ netapp_array_id }}" api_url: "{{ netapp_api_url }}" api_username: "{{ netapp_api_username }}" @@ -68,8 +68,6 @@ """ import json -import os - from ansible.module_utils.api import basic_auth_argument_spec from ansible.module_utils.basic import AnsibleModule, get_exception from ansible.module_utils.urls import open_url @@ -173,8 +171,7 @@ def main(): available_capacity=sp['freeSpace'], total_capacity=sp['totalRaidedSpace'], used_capacity=sp['usedSpace'] - ) for sp in resp['volumeGroup'] - ] + ) for sp in resp['volumeGroup']] all_volumes = list(resp['volume']) # all_volumes.extend(resp['thinVolume']) @@ -187,8 +184,7 @@ def main(): parent_storage_pool_id=v['volumeGroupRef'], capacity=v['capacity'], is_thin_provisioned=v['thinProvisioned'] - ) for v in all_volumes - ] + ) for v in all_volumes] features = [f for f in resp['sa']['capabilities']] features.extend([f['capability'] for f in resp['sa']['premiumFeatures'] if f['isEnabled']]) diff --git a/storage/netapp/netapp_e_flashcache.py b/storage/netapp/netapp_e_flashcache.py new file mode 100644 index 00000000000..5fa4a669747 --- /dev/null +++ b/storage/netapp/netapp_e_flashcache.py @@ -0,0 +1,420 @@ +#!/usr/bin/python + +# (c) 2016, NetApp, Inc +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +DOCUMENTATION = ''' +module: netapp_e_flashcache +author: Kevin Hulquest (@hulquest) +version_added: '2.2' +short_description: Manage NetApp SSD caches +description: +- Create or remove SSD caches on a NetApp E-Series storage array. +options: + api_username: + required: true + description: + - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_password: + required: true + description: + - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_url: + required: true + description: + - The url to the SANtricity WebServices Proxy or embedded REST API. + example: + - https://prod-1.wahoo.acme.com/devmgr/v2 + validate_certs: + required: false + default: true + description: + - Should https certificates be validated? + ssid: + required: true + description: + - The ID of the array to manage (as configured on the web services proxy). + state: + required: true + description: + - Whether the specified SSD cache should exist or not. + choices: ['present', 'absent'] + default: present + name: + required: true + description: + - The name of the SSD cache to manage + io_type: + description: + - The type of workload to optimize the cache for. + choices: ['filesystem','database','media'] + default: filesystem + disk_count: + description: + - The minimum number of disks to use for building the cache. The cache will be expanded if this number exceeds the number of disks already in place + size_unit: + description: + - The unit to be applied to size arguments + choices: ['bytes', 'b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb'] + default: gb + cache_size_min: + description: + - The minimum size (in size_units) of the ssd cache. The cache will be expanded if this exceeds the current size of the cache. +''' + +EXAMPLES = """ + - name: Flash Cache + netapp_e_flashcache: + ssid: "{{ ssid }}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" + name: SSDCacheBuiltByAnsible +""" + +RETURN = """ +msg: + description: Success message + returned: success + type: string + sample: json for newly created flash cache +""" +import json +import logging +import sys + +from ansible.module_utils.api import basic_auth_argument_spec +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import open_url + +from ansible.module_utils.six.moves.urllib.error import HTTPError + + +def request(url, data=None, headers=None, method='GET', use_proxy=True, + force=False, last_mod_time=None, timeout=10, validate_certs=True, + url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False): + try: + r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy, + force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs, + url_username=url_username, url_password=url_password, http_agent=http_agent, + force_basic_auth=force_basic_auth) + except HTTPError: + err = get_exception() + r = err.fp + + try: + raw_data = r.read() + if raw_data: + data = json.loads(raw_data) + else: + raw_data = None + except: + if ignore_errors: + pass + else: + raise Exception(raw_data) + + resp_code = r.getcode() + + if resp_code >= 400 and not ignore_errors: + raise Exception(resp_code, data) + else: + return resp_code, data + + +class NetAppESeriesFlashCache(object): + def __init__(self): + self.name = None + self.log_mode = None + self.log_path = None + self.api_url = None + self.api_username = None + self.api_password = None + self.ssid = None + self.validate_certs = None + self.disk_count = None + self.size_unit = None + self.cache_size_min = None + self.io_type = None + self.driveRefs = None + self.state = None + self._size_unit_map = dict( + bytes=1, + b=1, + kb=1024, + mb=1024 ** 2, + gb=1024 ** 3, + tb=1024 ** 4, + pb=1024 ** 5, + eb=1024 ** 6, + zb=1024 ** 7, + yb=1024 ** 8 + ) + + argument_spec = basic_auth_argument_spec() + argument_spec.update(dict( + api_username=dict(type='str', required=True), + api_password=dict(type='str', required=True, no_log=True), + api_url=dict(type='str', required=True), + state=dict(default='present', choices=['present', 'absent'], type='str'), + ssid=dict(required=True, type='str'), + name=dict(required=True, type='str'), + disk_count=dict(type='int'), + disk_refs=dict(type='list'), + cache_size_min=dict(type='int'), + io_type=dict(default='filesystem', choices=['filesystem', 'database', 'media']), + size_unit=dict(default='gb', choices=['bytes', 'b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb'], + type='str'), + criteria_disk_phy_type=dict(choices=['sas', 'sas4k', 'fibre', 'fibre520b', 'scsi', 'sata', 'pata'], + type='str'), + log_mode=dict(type='str'), + log_path=dict(type='str'), + )) + self.module = AnsibleModule( + argument_spec=argument_spec, + required_if=[ + + ], + mutually_exclusive=[ + + ], + # TODO: update validation for various selection criteria + supports_check_mode=True + ) + + self.__dict__.update(self.module.params) + + # logging setup + self._logger = logging.getLogger(self.__class__.__name__) + self.debug = self._logger.debug + + if self.log_mode == 'file' and self.log_path: + logging.basicConfig(level=logging.DEBUG, filename=self.log_path) + elif self.log_mode == 'stderr': + logging.basicConfig(level=logging.DEBUG, stream=sys.stderr) + + self.post_headers = dict(Accept="application/json") + self.post_headers['Content-Type'] = 'application/json' + + def get_candidate_disks(self, disk_count, size_unit='gb', capacity=None): + self.debug("getting candidate disks...") + + drives_req = dict( + driveCount=disk_count, + sizeUnit=size_unit, + driveType='ssd', + ) + + if capacity: + drives_req['targetUsableCapacity'] = capacity + + (rc, drives_resp) = request(self.api_url + "/storage-systems/%s/drives" % (self.ssid), + data=json.dumps(drives_req), headers=self.post_headers, method='POST', + url_username=self.api_username, url_password=self.api_password, + validate_certs=self.validate_certs) + + if rc == 204: + self.module.fail_json(msg='Cannot find disks to match requested criteria for ssd cache') + + disk_ids = [d['id'] for d in drives_resp] + bytes = reduce(lambda s, d: s + int(d['usableCapacity']), drives_resp, 0) + + return (disk_ids, bytes) + + def create_cache(self): + (disk_ids, bytes) = self.get_candidate_disks(disk_count=self.disk_count, size_unit=self.size_unit, + capacity=self.cache_size_min) + + self.debug("creating ssd cache...") + + create_fc_req = dict( + driveRefs=disk_ids, + name=self.name + ) + + (rc, self.resp) = request(self.api_url + "/storage-systems/%s/flash-cache" % (self.ssid), + data=json.dumps(create_fc_req), headers=self.post_headers, method='POST', + url_username=self.api_username, url_password=self.api_password, + validate_certs=self.validate_certs) + + def update_cache(self): + self.debug('updating flash cache config...') + update_fc_req = dict( + name=self.name, + configType=self.io_type + ) + + (rc, self.resp) = request(self.api_url + "/storage-systems/%s/flash-cache/configure" % (self.ssid), + data=json.dumps(update_fc_req), headers=self.post_headers, method='POST', + url_username=self.api_username, url_password=self.api_password, + validate_certs=self.validate_certs) + + def delete_cache(self): + self.debug('deleting flash cache...') + (rc, self.resp) = request(self.api_url + "/storage-systems/%s/flash-cache" % (self.ssid), method='DELETE', + url_username=self.api_username, url_password=self.api_password, + validate_certs=self.validate_certs, ignore_errors=True) + + @property + def needs_more_disks(self): + if len(self.cache_detail['driveRefs']) < self.disk_count: + self.debug("needs resize: current disk count %s < requested requested count %s" % ( + len(self.cache_detail['driveRefs']), self.disk_count)) + return True + + @property + def needs_less_disks(self): + if len(self.cache_detail['driveRefs']) > self.disk_count: + self.debug("needs resize: current disk count %s < requested requested count %s" % ( + len(self.cache_detail['driveRefs']), self.disk_count)) + return True + + @property + def current_size_bytes(self): + return int(self.cache_detail['fcDriveInfo']['fcWithDrives']['usedCapacity']) + + @property + def requested_size_bytes(self): + if self.cache_size_min: + return self.cache_size_min * self._size_unit_map[self.size_unit] + else: + return 0 + + @property + def needs_more_capacity(self): + if self.current_size_bytes < self.requested_size_bytes: + self.debug("needs resize: current capacity %sb is less than requested minimum %sb" % ( + self.current_size_bytes, self.requested_size_bytes)) + return True + + @property + def needs_resize(self): + return self.needs_more_disks or self.needs_more_capacity or self.needs_less_disks + + def resize_cache(self): + # increase up to disk count first, then iteratively add disks until we meet requested capacity + + # TODO: perform this calculation in check mode + current_disk_count = len(self.cache_detail['driveRefs']) + proposed_new_disks = 0 + + proposed_additional_bytes = 0 + proposed_disk_ids = [] + + if self.needs_more_disks: + proposed_disk_count = self.disk_count - current_disk_count + + (disk_ids, bytes) = self.get_candidate_disks(disk_count=proposed_disk_count) + proposed_additional_bytes = bytes + proposed_disk_ids = disk_ids + + while self.current_size_bytes + proposed_additional_bytes < self.requested_size_bytes: + proposed_new_disks += 1 + (disk_ids, bytes) = self.get_candidate_disks(disk_count=proposed_new_disks) + proposed_disk_ids = disk_ids + proposed_additional_bytes = bytes + + add_drives_req = dict( + driveRef=proposed_disk_ids + ) + + self.debug("adding drives to flash-cache...") + (rc, self.resp) = request(self.api_url + "/storage-systems/%s/flash-cache/addDrives" % (self.ssid), + data=json.dumps(add_drives_req), headers=self.post_headers, method='POST', + url_username=self.api_username, url_password=self.api_password, + validate_certs=self.validate_certs) + + elif self.needs_less_disks and self.driveRefs: + rm_drives = dict(driveRef=self.driveRefs) + (rc, self.resp) = request(self.api_url + "/storage-systems/%s/flash-cache/removeDrives" % (self.ssid), + data=json.dumps(rm_drives), headers=self.post_headers, method='POST', + url_username=self.api_username, url_password=self.api_password, + validate_certs=self.validate_certs) + + def apply(self): + result = dict(changed=False) + (rc, cache_resp) = request(self.api_url + "/storage-systems/%s/flash-cache" % (self.ssid), + url_username=self.api_username, url_password=self.api_password, + validate_certs=self.validate_certs, ignore_errors=True) + + if rc == 200: + self.cache_detail = cache_resp + else: + self.cache_detail = None + + if rc not in [200, 404]: + raise Exception( + "Unexpected error code %s fetching flash cache detail. Response data was %s" % (rc, cache_resp)) + + if self.state == 'present': + if self.cache_detail: + # TODO: verify parameters against detail for changes + if self.cache_detail['name'] != self.name: + self.debug("CHANGED: name differs") + result['changed'] = True + if self.cache_detail['flashCacheBase']['configType'] != self.io_type: + self.debug("CHANGED: io_type differs") + result['changed'] = True + if self.needs_resize: + self.debug("CHANGED: resize required") + result['changed'] = True + else: + self.debug("CHANGED: requested state is 'present' but cache does not exist") + result['changed'] = True + else: # requested state is absent + if self.cache_detail: + self.debug("CHANGED: requested state is 'absent' but cache exists") + result['changed'] = True + + if not result['changed']: + self.debug("no changes, exiting...") + self.module.exit_json(**result) + + if self.module.check_mode: + self.debug("changes pending in check mode, exiting early...") + self.module.exit_json(**result) + + if self.state == 'present': + if not self.cache_detail: + self.create_cache() + else: + if self.needs_resize: + self.resize_cache() + + # run update here as well, since io_type can't be set on creation + self.update_cache() + + elif self.state == 'absent': + self.delete_cache() + + # TODO: include other details about the storage pool (size, type, id, etc) + self.module.exit_json(changed=result['changed'], **self.resp) + + +def main(): + sp = NetAppESeriesFlashCache() + try: + sp.apply() + except Exception: + e = get_exception() + sp.debug("Exception in apply(): \n%s" % str(e)) + sp.module.fail_json(msg="Failed to create flash cache. Error[%s]" % str(e)) + +if __name__ == '__main__': + main() diff --git a/storage/netapp/netapp_e_host.py b/storage/netapp/netapp_e_host.py new file mode 100644 index 00000000000..2261d8264de --- /dev/null +++ b/storage/netapp/netapp_e_host.py @@ -0,0 +1,425 @@ +#!/usr/bin/python + +# (c) 2016, NetApp, Inc +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +DOCUMENTATION = """ +--- +module: netapp_e_host +short_description: manage eseries hosts +description: + - Create, update, remove hosts on NetApp E-series storage arrays +version_added: '2.2' +author: Kevin Hulquest (@hulquest) +options: + api_username: + required: true + description: + - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_password: + required: true + description: + - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_url: + required: true + description: + - The url to the SANtricity WebServices Proxy or embedded REST API. + example: + - https://prod-1.wahoo.acme.com/devmgr/v2 + validate_certs: + required: false + default: true + description: + - Should https certificates be validated? + ssid: + description: + - the id of the storage array you wish to act against + required: True + name: + description: + - If the host doesnt yet exist, the label to assign at creation time. + - If the hosts already exists, this is what is used to identify the host to apply any desired changes + required: True + host_type_index: + description: + - The index that maps to host type you wish to create. It is recommended to use the M(netapp_e_facts) module to gather this information. Alternatively you can use the WSP portal to retrieve the information. + required: True + ports: + description: + - a list of of dictionaries of host ports you wish to associate with the newly created host + required: False + group: + description: + - the group you want the host to be a member of + required: False + +""" + +EXAMPLES = """ + - name: Set Host Info + netapp_e_host: + ssid: "{{ ssid }}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + name: "{{ host_name }}" + host_type_index: "{{ host_type_index }}" +""" + +RETURN = """ +msg: + description: Success message + returned: success + type: string + sample: The host has been created. +""" +import json + +from ansible.module_utils.api import basic_auth_argument_spec +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import open_url +from ansible.module_utils.six.moves.urllib.error import HTTPError + +HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json", +} + + +def request(url, data=None, headers=None, method='GET', use_proxy=True, + force=False, last_mod_time=None, timeout=10, validate_certs=True, + url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False): + try: + r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy, + force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs, + url_username=url_username, url_password=url_password, http_agent=http_agent, + force_basic_auth=force_basic_auth) + except HTTPError: + err = get_exception() + r = err.fp + + try: + raw_data = r.read() + if raw_data: + data = json.loads(raw_data) + else: + raw_data is None + except: + if ignore_errors: + pass + else: + raise Exception(raw_data) + + resp_code = r.getcode() + + if resp_code >= 400 and not ignore_errors: + raise Exception(resp_code, data) + else: + return resp_code, data + + +class Host(object): + def __init__(self): + argument_spec = basic_auth_argument_spec() + argument_spec.update(dict( + api_username=dict(type='str', required=True), + api_password=dict(type='str', required=True, no_log=True), + api_url=dict(type='str', required=True), + ssid=dict(type='str', required=True), + state=dict(type='str', required=True, choices=['absent', 'present']), + group=dict(type='str', required=False), + ports=dict(type='list', required=False), + force_port=dict(type='bool', default=False), + name=dict(type='str', required=True), + host_type_index=dict(type='int', required=True) + )) + + self.module = AnsibleModule(argument_spec=argument_spec) + args = self.module.params + self.group = args['group'] + self.ports = args['ports'] + self.force_port = args['force_port'] + self.name = args['name'] + self.host_type_index = args['host_type_index'] + self.state = args['state'] + self.ssid = args['ssid'] + self.url = args['api_url'] + self.user = args['api_username'] + self.pwd = args['api_password'] + self.certs = args['validate_certs'] + self.ports = args['ports'] + self.post_body = dict() + + if not self.url.endswith('/'): + self.url += '/' + + @property + def valid_host_type(self): + try: + (rc, host_types) = request(self.url + 'storage-systems/%s/host-types' % self.ssid, url_password=self.pwd, + url_username=self.user, validate_certs=self.certs, headers=HEADERS) + except Exception: + err = get_exception() + self.module.fail_json( + msg="Failed to get host types. Array Id [%s]. Error [%s]." % (self.ssid, str(err))) + + try: + match = filter(lambda host_type: host_type['index'] == self.host_type_index, host_types)[0] + return True + except IndexError: + self.module.fail_json(msg="There is no host type with index %s" % self.host_type_index) + + @property + def hostports_available(self): + used_ids = list() + try: + (rc, self.available_ports) = request(self.url + 'storage-systems/%s/unassociated-host-ports' % self.ssid, + url_password=self.pwd, url_username=self.user, + validate_certs=self.certs, + headers=HEADERS) + except: + err = get_exception() + self.module.fail_json( + msg="Failed to get unassociated host ports. Array Id [%s]. Error [%s]." % (self.ssid, str(err))) + + if len(self.available_ports) > 0 and len(self.ports) <= len(self.available_ports): + for port in self.ports: + for free_port in self.available_ports: + # Desired Type matches but also make sure we havent already used the ID + if not free_port['id'] in used_ids: + # update the port arg to have an id attribute + used_ids.append(free_port['id']) + break + + if len(used_ids) != len(self.ports) and not self.force_port: + self.module.fail_json( + msg="There are not enough free host ports with the specified port types to proceed") + else: + return True + + else: + self.module.fail_json(msg="There are no host ports available OR there are not enough unassigned host ports") + + @property + def group_id(self): + if self.group: + try: + (rc, all_groups) = request(self.url + 'storage-systems/%s/host-groups' % self.ssid, + url_password=self.pwd, + url_username=self.user, validate_certs=self.certs, headers=HEADERS) + except: + err = get_exception() + self.module.fail_json( + msg="Failed to get host groups. Array Id [%s]. Error [%s]." % (self.ssid, str(err))) + + try: + group_obj = filter(lambda group: group['name'] == self.group, all_groups)[0] + return group_obj['id'] + except IndexError: + self.module.fail_json(msg="No group with the name: %s exists" % self.group) + else: + # Return the value equivalent of no group + return "0000000000000000000000000000000000000000" + + @property + def host_exists(self): + try: + (rc, all_hosts) = request(self.url + 'storage-systems/%s/hosts' % self.ssid, url_password=self.pwd, + url_username=self.user, validate_certs=self.certs, headers=HEADERS) + except: + err = get_exception() + self.module.fail_json( + msg="Failed to determine host existence. Array Id [%s]. Error [%s]." % (self.ssid, str(err))) + + self.all_hosts = all_hosts + try: # Try to grab the host object + self.host_obj = filter(lambda host: host['label'] == self.name, all_hosts)[0] + return True + except IndexError: + # Host with the name passed in does not exist + return False + + @property + def needs_update(self): + needs_update = False + self.force_port_update = False + + if self.host_obj['clusterRef'] != self.group_id or \ + self.host_obj['hostTypeIndex'] != self.host_type_index: + needs_update = True + + if self.ports: + if not self.host_obj['ports']: + needs_update = True + for arg_port in self.ports: + # First a quick check to see if the port is mapped to a different host + if not self.port_on_diff_host(arg_port): + for obj_port in self.host_obj['ports']: + if arg_port['label'] == obj_port['label']: + # Confirmed that port arg passed in exists on the host + # port_id = self.get_port_id(obj_port['label']) + if arg_port['type'] != obj_port['portId']['ioInterfaceType']: + needs_update = True + if 'iscsiChapSecret' in arg_port: + # No way to know the current secret attr, so always return True just in case + needs_update = True + else: + # If the user wants the ports to be reassigned, do it + if self.force_port: + self.force_port_update = True + needs_update = True + else: + self.module.fail_json( + msg="The port you specified:\n%s\n is associated with a different host. Specify force_port as True or try a different port spec" % arg_port) + + return needs_update + + def port_on_diff_host(self, arg_port): + """ Checks to see if a passed in port arg is present on a different host """ + for host in self.all_hosts: + # Only check 'other' hosts + if self.host_obj['name'] != self.name: + for port in host['ports']: + # Check if the port label is found in the port dict list of each host + if arg_port['label'] == port['label']: + self.other_host = host + return True + return False + + def reassign_ports(self, apply=True): + if not self.post_body: + self.post_body = dict( + portsToUpdate=dict() + ) + + for port in self.ports: + if self.port_on_diff_host(port): + self.post_body['portsToUpdate'].update(dict( + portRef=self.other_host['hostPortRef'], + hostRef=self.host_obj['id'], + # Doesnt yet address port identifier or chap secret + )) + + if apply: + try: + (rc, self.host_obj) = request( + self.url + 'storage-systems/%s/hosts/%s' % (self.ssid, self.host_obj['id']), + url_username=self.user, url_password=self.pwd, headers=HEADERS, + validate_certs=self.certs, method='POST', data=json.dumps(self.post_body)) + except: + err = get_exception() + self.module.fail_json( + msg="Failed to reassign host port. Host Id [%s]. Array Id [%s]. Error [%s]." % ( + self.host_obj['id'], self.ssid, str(err))) + + def update_host(self): + if self.ports: + if self.hostports_available: + if self.force_port_update is True: + self.reassign_ports(apply=False) + # Make sure that only ports that arent being reassigned are passed into the ports attr + self.ports = [port for port in self.ports if not self.port_on_diff_host(port)] + + self.post_body['ports'] = self.ports + + if self.group: + self.post_body['groupId'] = self.group_id + + self.post_body['hostType'] = dict(index=self.host_type_index) + + try: + (rc, self.host_obj) = request(self.url + 'storage-systems/%s/hosts/%s' % (self.ssid, self.host_obj['id']), + url_username=self.user, url_password=self.pwd, headers=HEADERS, + validate_certs=self.certs, method='POST', data=json.dumps(self.post_body)) + except: + err = get_exception() + self.module.fail_json(msg="Failed to update host. Array Id [%s]. Error [%s]." % (self.ssid, str(err))) + + self.module.exit_json(changed=True, **self.host_obj) + + def create_host(self): + post_body = dict( + name=self.name, + host_type=dict(index=self.host_type_index), + groupId=self.group_id, + ports=self.ports + ) + if self.ports: + # Check that all supplied port args are valid + if self.hostports_available: + post_body.update(ports=self.ports) + elif not self.force_port: + self.module.fail_json( + msg="You supplied ports that are already in use. Supply force_port to True if you wish to reassign the ports") + + if not self.host_exists: + try: + (rc, create_resp) = request(self.url + "storage-systems/%s/hosts" % self.ssid, method='POST', + url_username=self.user, url_password=self.pwd, validate_certs=self.certs, + data=json.dumps(post_body), headers=HEADERS) + except: + err = get_exception() + self.module.fail_json( + msg="Failed to create host. Array Id [%s]. Error [%s]." % (self.ssid, str(err))) + else: + self.module.exit_json(changed=False, + msg="Host already exists. Id [%s]. Host [%s]." % (self.ssid, self.name)) + + self.host_obj = create_resp + + if self.ports and self.force_port: + self.reassign_ports() + + self.module.exit_json(changed=True, **self.host_obj) + + def remove_host(self): + try: + (rc, resp) = request(self.url + "storage-systems/%s/hosts/%s" % (self.ssid, self.host_obj['id']), + method='DELETE', + url_username=self.user, url_password=self.pwd, validate_certs=self.certs) + except: + err = get_exception() + self.module.fail_json( + msg="Failed to remote host. Host[%s]. Array Id [%s]. Error [%s]." % (self.host_obj['id'], + self.ssid, + str(err))) + + def apply(self): + if self.state == 'present': + if self.host_exists: + if self.needs_update and self.valid_host_type: + self.update_host() + else: + self.module.exit_json(changed=False, msg="Host already present.", id=self.ssid, label=self.name) + elif self.valid_host_type: + self.create_host() + else: + if self.host_exists: + self.remove_host() + self.module.exit_json(changed=True, msg="Host removed.") + else: + self.module.exit_json(changed=False, msg="Host already absent.", id=self.ssid, label=self.name) + + +def main(): + host = Host() + host.apply() + + +if __name__ == '__main__': + main() diff --git a/storage/netapp/netapp_e_hostgroup.py b/storage/netapp/netapp_e_hostgroup.py new file mode 100644 index 00000000000..5248c1d9531 --- /dev/null +++ b/storage/netapp/netapp_e_hostgroup.py @@ -0,0 +1,413 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, NetApp, Inc +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +# +DOCUMENTATION = ''' +--- +module: netapp_e_hostgroup +version_added: "2.2" +short_description: Manage NetApp Storage Array Host Groups +author: Kevin Hulquest (@hulquest) +description: +- Create, update or destroy host groups on a NetApp E-Series storage array. +options: + api_username: + required: true + description: + - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_password: + required: true + description: + - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_url: + required: true + description: + - The url to the SANtricity WebServices Proxy or embedded REST API. + example: + - https://prod-1.wahoo.acme.com/devmgr/v2 + validate_certs: + required: false + default: true + description: + - Should https certificates be validated? + ssid: + required: true + description: + - The ID of the array to manage (as configured on the web services proxy). + state: + required: true + description: + - Whether the specified host group should exist or not. + choices: ['present', 'absent'] + name: + required: false + description: + - The name of the host group to manage. Either this or C(id_num) must be supplied. + new_name: + required: false + description: + - specify this when you need to update the name of a host group + id: + required: false + description: + - The id number of the host group to manage. Either this or C(name) must be supplied. + hosts:: + required: false + description: + - a list of host names/labels to add to the group +''' +EXAMPLES = ''' + - name: Configure Hostgroup + netapp_e_hostgroup: + ssid: "{{ ssid }}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" + state: present +''' +RETURN = ''' +clusterRef: + description: The unique identification value for this object. Other objects may use this reference value to refer to the cluster. + returned: always except when state is absent + type: string + sample: "3233343536373839303132333100000000000000" +confirmLUNMappingCreation: + description: If true, indicates that creation of LUN-to-volume mappings should require careful confirmation from the end-user, since such a mapping will alter the volume access rights of other clusters, in addition to this one. + returned: always + type: boolean + sample: false +hosts: + description: A list of the hosts that are part of the host group after all operations. + returned: always except when state is absent + type: list + sample: ["HostA","HostB"] +id: + description: The id number of the hostgroup + returned: always except when state is absent + type: string + sample: "3233343536373839303132333100000000000000" +isSAControlled: + description: If true, indicates that I/O accesses from this cluster are subject to the storage array's default LUN-to-volume mappings. If false, indicates that I/O accesses from the cluster are subject to cluster-specific LUN-to-volume mappings. + returned: always except when state is absent + type: boolean + sample: false +label: + description: The user-assigned, descriptive label string for the cluster. + returned: always + type: string + sample: "MyHostGroup" +name: + description: same as label + returned: always except when state is absent + type: string + sample: "MyHostGroup" +protectionInformationCapableAccessMethod: + description: This field is true if the host has a PI capable access method. + returned: always except when state is absent + type: boolean + sample: true +''' + +HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json" +} + +import json + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception + +from ansible.module_utils.urls import open_url +from ansible.module_utils.six.moves.urllib.error import HTTPError + + +def request(url, data=None, headers=None, method='GET', use_proxy=True, + force=False, last_mod_time=None, timeout=10, validate_certs=True, + url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False): + try: + r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy, + force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs, + url_username=url_username, url_password=url_password, http_agent=http_agent, + force_basic_auth=force_basic_auth) + except HTTPError: + err = get_exception() + r = err.fp + + try: + raw_data = r.read() + if raw_data: + data = json.loads(raw_data) + else: + raw_data = None + except: + if ignore_errors: + pass + else: + raise Exception(raw_data) + + resp_code = r.getcode() + + if resp_code >= 400 and not ignore_errors: + raise Exception(resp_code, data) + else: + return resp_code, data + + +def group_exists(module, id_type, ident, ssid, api_url, user, pwd): + rc, data = get_hostgroups(module, ssid, api_url, user, pwd) + for group in data: + if group[id_type] == ident: + return True, data + else: + continue + + return False, data + + +def get_hostgroups(module, ssid, api_url, user, pwd): + groups = "storage-systems/%s/host-groups" % ssid + url = api_url + groups + try: + rc, data = request(url, headers=HEADERS, url_username=user, url_password=pwd) + return rc, data + except HTTPError: + err = get_exception() + module.fail_json(msg="Failed to get host groups. Id [%s]. Error [%s]." % (ssid, str(err))) + + +def get_hostref(module, ssid, name, api_url, user, pwd): + all_hosts = 'storage-systems/%s/hosts' % ssid + url = api_url + all_hosts + try: + rc, data = request(url, method='GET', headers=HEADERS, url_username=user, url_password=pwd) + except Exception: + err = get_exception() + module.fail_json(msg="Failed to get hosts. Id [%s]. Error [%s]." % (ssid, str(err))) + + for host in data: + if host['name'] == name: + return host['hostRef'] + else: + continue + + module.fail_json(msg="No host with the name %s could be found" % name) + + +def create_hostgroup(module, ssid, name, api_url, user, pwd, hosts=None): + groups = "storage-systems/%s/host-groups" % ssid + url = api_url + groups + hostrefs = [] + + if hosts: + for host in hosts: + href = get_hostref(module, ssid, host, api_url, user, pwd) + hostrefs.append(href) + + post_data = json.dumps(dict(name=name, hosts=hostrefs)) + try: + rc, data = request(url, method='POST', data=post_data, headers=HEADERS, url_username=user, url_password=pwd) + except Exception: + err = get_exception() + module.fail_json(msg="Failed to create host group. Id [%s]. Error [%s]." % (ssid, str(err))) + + return rc, data + + +def update_hostgroup(module, ssid, name, api_url, user, pwd, hosts=None, new_name=None): + gid = get_hostgroup_id(module, ssid, name, api_url, user, pwd) + groups = "storage-systems/%s/host-groups/%s" % (ssid, gid) + url = api_url + groups + hostrefs = [] + + if hosts: + for host in hosts: + href = get_hostref(module, ssid, host, api_url, user, pwd) + hostrefs.append(href) + + if new_name: + post_data = json.dumps(dict(name=new_name, hosts=hostrefs)) + else: + post_data = json.dumps(dict(hosts=hostrefs)) + + try: + rc, data = request(url, method='POST', data=post_data, headers=HEADERS, url_username=user, url_password=pwd) + except Exception: + err = get_exception() + module.fail_json(msg="Failed to update host group. Group [%s]. Id [%s]. Error [%s]." % (gid, ssid, + str(err))) + + return rc, data + + +def delete_hostgroup(module, ssid, group_id, api_url, user, pwd): + groups = "storage-systems/%s/host-groups/%s" % (ssid, group_id) + url = api_url + groups + # TODO: Loop through hosts, do mapping to href, make new list to pass to data + try: + rc, data = request(url, method='DELETE', headers=HEADERS, url_username=user, url_password=pwd) + except Exception: + err = get_exception() + module.fail_json(msg="Failed to delete host group. Group [%s]. Id [%s]. Error [%s]." % (group_id, ssid, str(err))) + + return rc, data + + +def get_hostgroup_id(module, ssid, name, api_url, user, pwd): + all_groups = 'storage-systems/%s/host-groups' % ssid + url = api_url + all_groups + rc, data = request(url, method='GET', headers=HEADERS, url_username=user, url_password=pwd) + for hg in data: + if hg['name'] == name: + return hg['id'] + else: + continue + + module.fail_json(msg="A hostgroup with the name %s could not be found" % name) + + +def get_hosts_in_group(module, ssid, group_name, api_url, user, pwd): + all_groups = 'storage-systems/%s/host-groups' % ssid + g_url = api_url + all_groups + try: + g_rc, g_data = request(g_url, method='GET', headers=HEADERS, url_username=user, url_password=pwd) + except Exception: + err = get_exception() + module.fail_json( + msg="Failed in first step getting hosts from group. Group: [%s]. Id [%s]. Error [%s]." % (group_name, + ssid, + str(err))) + + all_hosts = 'storage-systems/%s/hosts' % ssid + h_url = api_url + all_hosts + try: + h_rc, h_data = request(h_url, method='GET', headers=HEADERS, url_username=user, url_password=pwd) + except Exception: + err = get_exception() + module.fail_json( + msg="Failed in second step getting hosts from group. Group: [%s]. Id [%s]. Error [%s]." % ( + group_name, + ssid, + str(err))) + + hosts_in_group = [] + + for hg in g_data: + if hg['name'] == group_name: + clusterRef = hg['clusterRef'] + + for host in h_data: + if host['clusterRef'] == clusterRef: + hosts_in_group.append(host['name']) + + return hosts_in_group + + +def main(): + module = AnsibleModule( + argument_spec=dict( + name=dict(required=False), + new_name=dict(required=False), + ssid=dict(required=True), + id=dict(required=False), + state=dict(required=True, choices=['present', 'absent']), + hosts=dict(required=False, type='list'), + api_url=dict(required=True), + api_username=dict(required=True), + validate_certs=dict(required=False, default=True), + api_password=dict(required=True, no_log=True) + ), + supports_check_mode=False, + mutually_exclusive=[['name', 'id']], + required_one_of=[['name', 'id']] + ) + + name = module.params['name'] + new_name = module.params['new_name'] + ssid = module.params['ssid'] + id_num = module.params['id'] + state = module.params['state'] + hosts = module.params['hosts'] + user = module.params['api_username'] + pwd = module.params['api_password'] + api_url = module.params['api_url'] + + if not api_url.endswith('/'): + api_url += '/' + + if name: + id_type = 'name' + id_key = name + elif id_num: + id_type = 'id' + id_key = id_num + + exists, group_data = group_exists(module, id_type, id_key, ssid, api_url, user, pwd) + + if state == 'present': + if not exists: + try: + rc, data = create_hostgroup(module, ssid, name, api_url, user, pwd, hosts) + except Exception: + err = get_exception() + module.fail_json(msg="Failed to create a host group. Id [%s]. Error [%s]." % (ssid, str(err))) + + hosts = get_hosts_in_group(module, ssid, name, api_url, user, pwd) + module.exit_json(changed=True, hosts=hosts, **data) + else: + current_hosts = get_hosts_in_group(module, ssid, name, api_url, user, pwd) + + if not current_hosts: + current_hosts = [] + + if not hosts: + hosts = [] + + if set(current_hosts) != set(hosts): + try: + rc, data = update_hostgroup(module, ssid, name, api_url, user, pwd, hosts, new_name) + except Exception: + err = get_exception() + module.fail_json( + msg="Failed to update host group. Group: [%s]. Id [%s]. Error [%s]." % (name, ssid, str(err))) + module.exit_json(changed=True, hosts=hosts, **data) + else: + for group in group_data: + if group['name'] == name: + module.exit_json(changed=False, hosts=current_hosts, **group) + + elif state == 'absent': + if exists: + hg_id = get_hostgroup_id(module, ssid, name, api_url, user, pwd) + try: + rc, data = delete_hostgroup(module, ssid, hg_id, api_url, user, pwd) + except Exception: + err = get_exception() + module.fail_json( + msg="Failed to delete host group. Group: [%s]. Id [%s]. Error [%s]." % (name, ssid, str(err))) + + module.exit_json(changed=True, msg="Host Group deleted") + else: + module.exit_json(changed=False, msg="Host Group is already absent") + + +if __name__ == '__main__': + main() diff --git a/storage/netapp/netapp_e_lun_mapping.py b/storage/netapp/netapp_e_lun_mapping.py new file mode 100644 index 00000000000..439a7e4f5e5 --- /dev/null +++ b/storage/netapp/netapp_e_lun_mapping.py @@ -0,0 +1,365 @@ +#!/usr/bin/python + +# (c) 2016, NetApp, Inc +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +DOCUMENTATION = ''' +--- +module: netapp_e_lun_mapping +author: Kevin Hulquest (@hulquest) +short_description: + - Create or Remove LUN Mappings +description: + - Allows for the creation and removal of volume to host mappings for NetApp E-series storage arrays. +version_added: "2.2" +options: + api_username: + required: true + description: + - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_password: + required: true + description: + - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_url: + required: true + description: + - The url to the SANtricity WebServices Proxy or embedded REST API. + example: + - https://prod-1.wahoo.acme.com/devmgr/v2 + validate_certs: + required: false + default: true + description: + - Should https certificates be validated? + ssid: + description: + - "The storage system array identifier." + required: False + lun: + description: + - The LUN number you wish to give the mapping + - If the supplied I(volume_name) is associated with a different LUN, it will be updated to what is supplied here. + required: False + default: 0 + target: + description: + - The name of host or hostgroup you wish to assign to the mapping + - If omitted, the default hostgroup is used. + - If the supplied I(volume_name) is associated with a different target, it will be updated to what is supplied here. + required: False + volume_name: + description: + - The name of the volume you wish to include in the mapping. + required: True + target_type: + description: + - Whether the target is a host or group. + - Required if supplying an explicit target. + required: False + choices: ["host", "group"] + state: + description: + - Present will ensure the mapping exists, absent will remove the mapping. + - All parameters I(lun), I(target), I(target_type) and I(volume_name) must still be supplied. + required: True + choices: ["present", "absent"] + api_url: + description: + - "The full API url. Example: http://ENDPOINT:8080/devmgr/v2" + - This can optionally be set via an environment variable, API_URL + required: False + api_username: + description: + - The username used to authenticate against the API. This can optionally be set via an environment variable, API_USERNAME + required: False + api_password: + description: + - The password used to authenticate against the API. This can optionally be set via an environment variable, API_PASSWORD + required: False +''' + +EXAMPLES = ''' +--- + - name: Lun Mapping Example + netapp_e_lun_mapping: + state: present + ssid: 1 + lun: 12 + target: Wilson + volume_name: Colby1 + target_type: group + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" +''' +RETURN = ''' +msg: Mapping exists. +msg: Mapping removed. +''' +import json + +from ansible.module_utils.api import basic_auth_argument_spec +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import open_url + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six.moves.urllib.error import HTTPError + +HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json" +} + + +def request(url, data=None, headers=None, method='GET', use_proxy=True, + force=False, last_mod_time=None, timeout=10, validate_certs=True, + url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False): + try: + r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy, + force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs, + url_username=url_username, url_password=url_password, http_agent=http_agent, + force_basic_auth=force_basic_auth) + except HTTPError: + err = get_exception() + r = err.fp + + try: + raw_data = r.read() + if raw_data: + data = json.loads(raw_data) + else: + raw_data = None + except: + if ignore_errors: + pass + else: + raise Exception(raw_data) + + resp_code = r.getcode() + + if resp_code >= 400 and not ignore_errors: + raise Exception(resp_code, data) + else: + return resp_code, data + + +def get_host_and_group_map(module, ssid, api_url, user, pwd): + mapping = dict(host=dict(), group=dict()) + + hostgroups = 'storage-systems/%s/host-groups' % ssid + groups_url = api_url + hostgroups + try: + hg_rc, hg_data = request(groups_url, headers=HEADERS, url_username=user, url_password=pwd) + except: + err = get_exception() + module.fail_json(msg="Failed to get host groups. Id [%s]. Error [%s]" % (ssid, str(err))) + + for group in hg_data: + mapping['group'][group['name']] = group['id'] + + hosts = 'storage-systems/%s/hosts' % ssid + hosts_url = api_url + hosts + try: + h_rc, h_data = request(hosts_url, headers=HEADERS, url_username=user, url_password=pwd) + except: + err = get_exception() + module.fail_json(msg="Failed to get hosts. Id [%s]. Error [%s]" % (ssid, str(err))) + + for host in h_data: + mapping['host'][host['name']] = host['id'] + + return mapping + + +def get_volume_id(module, data, ssid, name, api_url, user, pwd): + qty = 0 + for volume in data: + if volume['name'] == name: + qty += 1 + + if qty > 1: + module.fail_json(msg="More than one volume with the name: %s was found, " + "please use the volume WWN instead" % name) + else: + wwn = volume['wwn'] + + try: + return wwn + except NameError: + module.fail_json(msg="No volume with the name: %s, was found" % (name)) + + +def get_hostgroups(module, ssid, api_url, user, pwd): + groups = "storage-systems/%s/host-groups" % ssid + url = api_url + groups + try: + rc, data = request(url, headers=HEADERS, url_username=user, url_password=pwd) + return data + except Exception: + module.fail_json(msg="There was an issue with connecting, please check that your" + "endpoint is properly defined and your credentials are correct") + + +def get_volumes(module, ssid, api_url, user, pwd, mappable): + volumes = 'storage-systems/%s/%s' % (ssid, mappable) + url = api_url + volumes + try: + rc, data = request(url, url_username=user, url_password=pwd) + except Exception: + err = get_exception() + module.fail_json( + msg="Failed to mappable objects. Type[%s. Id [%s]. Error [%s]." % (mappable, ssid, str(err))) + return data + + +def get_lun_mappings(ssid, api_url, user, pwd, get_all=None): + mappings = 'storage-systems/%s/volume-mappings' % ssid + url = api_url + mappings + rc, data = request(url, url_username=user, url_password=pwd) + + if not get_all: + remove_keys = ('ssid', 'perms', 'lunMappingRef', 'type', 'id') + + for key in remove_keys: + for mapping in data: + del mapping[key] + + return data + + +def create_mapping(module, ssid, lun_map, vol_name, api_url, user, pwd): + mappings = 'storage-systems/%s/volume-mappings' % ssid + url = api_url + mappings + post_body = json.dumps(dict( + mappableObjectId=lun_map['volumeRef'], + targetId=lun_map['mapRef'], + lun=lun_map['lun'] + )) + + rc, data = request(url, data=post_body, method='POST', url_username=user, url_password=pwd, headers=HEADERS, + ignore_errors=True) + + if rc == 422: + data = move_lun(module, ssid, lun_map, vol_name, api_url, user, pwd) + # module.fail_json(msg="The volume you specified '%s' is already " + # "part of a different LUN mapping. If you " + # "want to move it to a different host or " + # "hostgroup, then please use the " + # "netapp_e_move_lun module" % vol_name) + return data + + +def move_lun(module, ssid, lun_map, vol_name, api_url, user, pwd): + lun_id = get_lun_id(module, ssid, lun_map, api_url, user, pwd) + move_lun = "storage-systems/%s/volume-mappings/%s/move" % (ssid, lun_id) + url = api_url + move_lun + post_body = json.dumps(dict(targetId=lun_map['mapRef'], lun=lun_map['lun'])) + rc, data = request(url, data=post_body, method='POST', url_username=user, url_password=pwd, headers=HEADERS) + return data + + +def get_lun_id(module, ssid, lun_mapping, api_url, user, pwd): + data = get_lun_mappings(ssid, api_url, user, pwd, get_all=True) + + for lun_map in data: + if lun_map['volumeRef'] == lun_mapping['volumeRef']: + return lun_map['id'] + # This shouldn't ever get called + module.fail_json(msg="No LUN map found.") + + +def remove_mapping(module, ssid, lun_mapping, api_url, user, pwd): + lun_id = get_lun_id(module, ssid, lun_mapping, api_url, user, pwd) + lun_del = "storage-systems/%s/volume-mappings/%s" % (ssid, lun_id) + url = api_url + lun_del + rc, data = request(url, method='DELETE', url_username=user, url_password=pwd, headers=HEADERS) + return data + + +def main(): + argument_spec = basic_auth_argument_spec() + argument_spec.update(dict( + api_username=dict(type='str', required=True), + api_password=dict(type='str', required=True, no_log=True), + api_url=dict(type='str', required=True), + state=dict(required=True, choices=['present', 'absent']), + target=dict(required=False, default=None), + target_type=dict(required=False, choices=['host', 'group']), + lun=dict(required=False, type='int', default=0), + ssid=dict(required=False), + volume_name=dict(required=True), + )) + + module = AnsibleModule(argument_spec=argument_spec) + + state = module.params['state'] + target = module.params['target'] + target_type = module.params['target_type'] + lun = module.params['lun'] + ssid = module.params['ssid'] + vol_name = module.params['volume_name'] + user = module.params['api_username'] + pwd = module.params['api_password'] + api_url = module.params['api_url'] + + if not api_url.endswith('/'): + api_url += '/' + + volume_map = get_volumes(module, ssid, api_url, user, pwd, "volumes") + thin_volume_map = get_volumes(module, ssid, api_url, user, pwd, "thin-volumes") + volref = None + + for vol in volume_map: + if vol['label'] == vol_name: + volref = vol['volumeRef'] + + if not volref: + for vol in thin_volume_map: + if vol['label'] == vol_name: + volref = vol['volumeRef'] + + if not volref: + module.fail_json(changed=False, msg="No volume with the name %s was found" % vol_name) + + host_and_group_mapping = get_host_and_group_map(module, ssid, api_url, user, pwd) + + desired_lun_mapping = dict( + mapRef=host_and_group_mapping[target_type][target], + lun=lun, + volumeRef=volref + ) + + lun_mappings = get_lun_mappings(ssid, api_url, user, pwd) + + if state == 'present': + if desired_lun_mapping in lun_mappings: + module.exit_json(changed=False, msg="Mapping exists") + else: + result = create_mapping(module, ssid, desired_lun_mapping, vol_name, api_url, user, pwd) + module.exit_json(changed=True, **result) + + elif state == 'absent': + if desired_lun_mapping in lun_mappings: + result = remove_mapping(module, ssid, desired_lun_mapping, api_url, user, pwd) + module.exit_json(changed=True, msg="Mapping removed") + else: + module.exit_json(changed=False, msg="Mapping absent") + + +if __name__ == '__main__': + main() diff --git a/storage/netapp/netapp_e_snapshot_group.py b/storage/netapp/netapp_e_snapshot_group.py new file mode 100644 index 00000000000..90c6e8471bb --- /dev/null +++ b/storage/netapp/netapp_e_snapshot_group.py @@ -0,0 +1,382 @@ +#!/usr/bin/python + +# (c) 2016, NetApp, Inc +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +DOCUMENTATION = """ +--- +module: netapp_e_snapshot_group +short_description: Manage snapshot groups +description: + - Create, update, delete snapshot groups for NetApp E-series storage arrays +version_added: '2.2' +author: Kevin Hulquest (@hulquest) +options: + api_username: + required: true + description: + - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_password: + required: true + description: + - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_url: + required: true + description: + - The url to the SANtricity WebServices Proxy or embedded REST API. + example: + - https://prod-1.wahoo.acme.com/devmgr/v2 + validate_certs: + required: false + default: true + description: + - Should https certificates be validated? + state: + description: + - Whether to ensure the group is present or absent. + required: True + choices: + - present + - absent + name: + description: + - The name to give the snapshot group + required: True + base_volume_name: + description: + - The name of the base volume or thin volume to use as the base for the new snapshot group. + - If a snapshot group with an identical C(name) already exists but with a different base volume + an error will be returned. + required: True + repo_pct: + description: + - The size of the repository in relation to the size of the base volume + required: False + default: 20 + warning_threshold: + description: + - The repository utilization warning threshold, as a percentage of the repository volume capacity. + required: False + default: 80 + delete_limit: + description: + - The automatic deletion indicator. + - If non-zero, the oldest snapshot image will be automatically deleted when creating a new snapshot image to keep the total number of snapshot images limited to the number specified. + - This value is overridden by the consistency group setting if this snapshot group is associated with a consistency group. + required: False + default: 30 + full_policy: + description: + - The behavior on when the data repository becomes full. + - This value is overridden by consistency group setting if this snapshot group is associated with a consistency group + required: False + default: purgepit + choices: + - purgepit + - unknown + - failbasewrites + - __UNDEFINED + storage_pool_name: + required: True + description: + - The name of the storage pool on which to allocate the repository volume. + rollback_priority: + required: False + description: + - The importance of the rollback operation. + - This value is overridden by consistency group setting if this snapshot group is associated with a consistency group + choices: + - highest + - high + - medium + - low + - lowest + - __UNDEFINED + default: medium +""" + +EXAMPLES = """ + - name: Configure Snapshot group + netapp_e_snapshot_group: + ssid: "{{ ssid }}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" + base_volume_name: SSGroup_test + name=: OOSS_Group + repo_pct: 20 + warning_threshold: 85 + delete_limit: 30 + full_policy: purgepit + storage_pool_name: Disk_Pool_1 + rollback_priority: medium +""" +RETURN = """ +msg: + description: Success message + returned: success + type: string + sample: json facts for newly created snapshot group. +""" +HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json", +} +import json + +from ansible.module_utils.api import basic_auth_argument_spec +from ansible.module_utils.basic import AnsibleModule + +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import open_url +from ansible.module_utils.six.moves.urllib.error import HTTPError + + +def request(url, data=None, headers=None, method='GET', use_proxy=True, + force=False, last_mod_time=None, timeout=10, validate_certs=True, + url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False): + try: + r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy, + force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs, + url_username=url_username, url_password=url_password, http_agent=http_agent, + force_basic_auth=force_basic_auth) + except HTTPError: + err = get_exception() + r = err.fp + + try: + raw_data = r.read() + if raw_data: + data = json.loads(raw_data) + else: + raw_data = None + except: + if ignore_errors: + pass + else: + raise Exception(raw_data) + + resp_code = r.getcode() + + if resp_code >= 400 and not ignore_errors: + raise Exception(resp_code, data) + else: + return resp_code, data + + +class SnapshotGroup(object): + def __init__(self): + + argument_spec = basic_auth_argument_spec() + argument_spec.update( + api_username=dict(type='str', required=True), + api_password=dict(type='str', required=True, no_log=True), + api_url=dict(type='str', required=True), + state=dict(required=True, choices=['present', 'absent']), + base_volume_name=dict(required=True), + name=dict(required=True), + repo_pct=dict(default=20, type='int'), + warning_threshold=dict(default=80, type='int'), + delete_limit=dict(default=30, type='int'), + full_policy=dict(default='purgepit', choices=['unknown', 'failbasewrites', 'purgepit']), + rollback_priority=dict(default='medium', choices=['highest', 'high', 'medium', 'low', 'lowest']), + storage_pool_name=dict(type='str'), + ssid=dict(required=True), + ) + + self.module = AnsibleModule(argument_spec=argument_spec) + + self.post_data = dict() + self.warning_threshold = self.module.params['warning_threshold'] + self.base_volume_name = self.module.params['base_volume_name'] + self.name = self.module.params['name'] + self.repo_pct = self.module.params['repo_pct'] + self.delete_limit = self.module.params['delete_limit'] + self.full_policy = self.module.params['full_policy'] + self.rollback_priority = self.module.params['rollback_priority'] + self.storage_pool_name = self.module.params['storage_pool_name'] + self.state = self.module.params['state'] + + self.url = self.module.params['api_url'] + self.user = self.module.params['api_username'] + self.pwd = self.module.params['api_password'] + self.certs = self.module.params['validate_certs'] + self.ssid = self.module.params['ssid'] + + if not self.url.endswith('/'): + self.url += '/' + + self.changed = False + + @property + def pool_id(self): + pools = 'storage-systems/%s/storage-pools' % self.ssid + url = self.url + pools + try: + (rc, data) = request(url, headers=HEADERS, url_username=self.user, url_password=self.pwd) + except: + err = get_exception() + self.module.fail_json(msg="Snapshot group module - Failed to fetch storage pools. " + + "Id [%s]. Error [%s]." % (self.ssid, str(err))) + + for pool in data: + if pool['name'] == self.storage_pool_name: + self.pool_data = pool + return pool['id'] + + self.module.fail_json(msg="No storage pool with the name: '%s' was found" % self.name) + + @property + def volume_id(self): + volumes = 'storage-systems/%s/volumes' % self.ssid + url = self.url + volumes + try: + rc, data = request(url, headers=HEADERS, url_username=self.user, url_password=self.pwd, + validate_certs=self.certs) + except: + err = get_exception() + self.module.fail_json(msg="Snapshot group module - Failed to fetch volumes. " + + "Id [%s]. Error [%s]." % (self.ssid, str(err))) + qty = 0 + for volume in data: + if volume['name'] == self.base_volume_name: + qty += 1 + + if qty > 1: + self.module.fail_json(msg="More than one volume with the name: %s was found, " + "please ensure your volume has a unique name" % self.base_volume_name) + else: + Id = volume['id'] + self.volume = volume + + try: + return Id + except NameError: + self.module.fail_json(msg="No volume with the name: %s, was found" % self.base_volume_name) + + @property + def snapshot_group_id(self): + url = self.url + 'storage-systems/%s/snapshot-groups' % self.ssid + try: + rc, data = request(url, headers=HEADERS, url_username=self.user, url_password=self.pwd, + validate_certs=self.certs) + except: + err = get_exception() + self.module.fail_json(msg="Failed to fetch snapshot groups. " + + "Id [%s]. Error [%s]." % (self.ssid, str(err))) + for ssg in data: + if ssg['name'] == self.name: + self.ssg_data = ssg + return ssg['id'] + + return None + + @property + def ssg_needs_update(self): + if self.ssg_data['fullWarnThreshold'] != self.warning_threshold or \ + self.ssg_data['autoDeleteLimit'] != self.delete_limit or \ + self.ssg_data['repFullPolicy'] != self.full_policy or \ + self.ssg_data['rollbackPriority'] != self.rollback_priority: + return True + else: + return False + + def create_snapshot_group(self): + self.post_data = dict( + baseMappableObjectId=self.volume_id, + name=self.name, + repositoryPercentage=self.repo_pct, + warningThreshold=self.warning_threshold, + autoDeleteLimit=self.delete_limit, + fullPolicy=self.full_policy, + storagePoolId=self.pool_id, + ) + snapshot = 'storage-systems/%s/snapshot-groups' % self.ssid + url = self.url + snapshot + try: + rc, self.ssg_data = request(url, data=json.dumps(self.post_data), method='POST', headers=HEADERS, + url_username=self.user, url_password=self.pwd, validate_certs=self.certs) + except: + err = get_exception() + self.module.fail_json(msg="Failed to create snapshot group. " + + "Snapshot group [%s]. Id [%s]. Error [%s]." % (self.name, + self.ssid, + str(err))) + + if not self.snapshot_group_id: + self.snapshot_group_id = self.ssg_data['id'] + + if self.ssg_needs_update: + self.update_ssg() + else: + self.module.exit_json(changed=True, **self.ssg_data) + + def update_ssg(self): + self.post_data = dict( + warningThreshold=self.warning_threshold, + autoDeleteLimit=self.delete_limit, + fullPolicy=self.full_policy, + rollbackPriority=self.rollback_priority + ) + + url = self.url + "storage-systems/%s/snapshot-groups/%s" % (self.ssid, self.snapshot_group_id) + try: + rc, self.ssg_data = request(url, data=json.dumps(self.post_data), method='POST', headers=HEADERS, + url_username=self.user, url_password=self.pwd, validate_certs=self.certs) + except: + err = get_exception() + self.module.fail_json(msg="Failed to update snapshot group. " + + "Snapshot group [%s]. Id [%s]. Error [%s]." % (self.name, + self.ssid, + str(err))) + + def apply(self): + if self.state == 'absent': + if self.snapshot_group_id: + try: + rc, resp = request( + self.url + 'storage-systems/%s/snapshot-groups/%s' % (self.ssid, self.snapshot_group_id), + method='DELETE', headers=HEADERS, url_password=self.pwd, url_username=self.user, + validate_certs=self.certs) + except: + err = get_exception() + self.module.fail_json(msg="Failed to delete snapshot group. " + + "Snapshot group [%s]. Id [%s]. Error [%s]." % (self.name, + self.ssid, + str(err))) + self.module.exit_json(changed=True, msg="Snapshot group removed", **self.ssg_data) + else: + self.module.exit_json(changed=False, msg="Snapshot group absent") + + elif self.snapshot_group_id: + if self.ssg_needs_update: + self.update_ssg() + self.module.exit_json(changed=True, **self.ssg_data) + else: + self.module.exit_json(changed=False, **self.ssg_data) + else: + self.create_snapshot_group() + + +def main(): + vg = SnapshotGroup() + vg.apply() + + +if __name__ == '__main__': + main() diff --git a/storage/netapp/netapp_e_snapshot_images.py b/storage/netapp/netapp_e_snapshot_images.py new file mode 100644 index 00000000000..8c81af84535 --- /dev/null +++ b/storage/netapp/netapp_e_snapshot_images.py @@ -0,0 +1,250 @@ +#!/usr/bin/python + +# (c) 2016, NetApp, Inc +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +DOCUMENTATION = """ +--- +module: netapp_e_snapshot_images +short_description: Create and delete snapshot images +description: + - Create and delete snapshots images on snapshot groups for NetApp E-series storage arrays. + - Only the oldest snapshot image can be deleted so consistency is preserved. + - "Related: Snapshot volumes are created from snapshot images." +version_added: '2.2' +author: Kevin Hulquest (@hulquest) +options: + api_username: + required: true + description: + - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_password: + required: true + description: + - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_url: + required: true + description: + - The url to the SANtricity WebServices Proxy or embedded REST API. + example: + - https://prod-1.wahoo.acme.com/devmgr/v2 + validate_certs: + required: false + default: true + description: + - Should https certificates be validated? + snapshot_group: + description: + - The name of the snapshot group in which you want to create a snapshot image. + required: True + state: + description: + - Whether a new snapshot image should be created or oldest be deleted. + required: True + choices: ['create', 'remove'] +""" +EXAMPLES = """ + - name: Create Snapshot + netapp_e_snapshot_images: + ssid: "{{ ssid }}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ validate_certs }}" + snapshot_group: "3300000060080E5000299C24000005B656D9F394" + state: 'create' +""" +RETURN = """ +--- + changed: true + msg: "Created snapshot image" + image_id: "3400000060080E5000299B640063074057BC5C5E " +""" + +HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json", +} +import json + +from ansible.module_utils.api import basic_auth_argument_spec +from ansible.module_utils.basic import AnsibleModule + +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import open_url +from ansible.module_utils.six.moves.urllib.error import HTTPError + + +def request(url, data=None, headers=None, method='GET', use_proxy=True, + force=False, last_mod_time=None, timeout=10, validate_certs=True, + url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False): + try: + r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy, + force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs, + url_username=url_username, url_password=url_password, http_agent=http_agent, + force_basic_auth=force_basic_auth) + except HTTPError: + err = get_exception() + r = err.fp + + try: + raw_data = r.read() + if raw_data: + data = json.loads(raw_data) + else: + raw_data = None + except: + if ignore_errors: + pass + else: + raise Exception(raw_data) + + resp_code = r.getcode() + + if resp_code >= 400 and not ignore_errors: + raise Exception(resp_code, data) + else: + return resp_code, data + + +def snapshot_group_from_name(module, ssid, api_url, api_pwd, api_usr, name): + snap_groups = 'storage-systems/%s/snapshot-groups' % ssid + snap_groups_url = api_url + snap_groups + (ret, snapshot_groups) = request(snap_groups_url, url_username=api_usr, url_password=api_pwd, headers=HEADERS, + validate_certs=module.params['validate_certs']) + + snapshot_group_id = None + for snapshot_group in snapshot_groups: + if name == snapshot_group['label']: + snapshot_group_id = snapshot_group['pitGroupRef'] + break + if snapshot_group_id is None: + module.fail_json(msg="Failed to lookup snapshot group. Group [%s]. Id [%s]." % (name, ssid)) + + return snapshot_group + + +def oldest_image(module, ssid, api_url, api_pwd, api_usr, name): + get_status = 'storage-systems/%s/snapshot-images' % ssid + url = api_url + get_status + + try: + (ret, images) = request(url, url_username=api_usr, url_password=api_pwd, headers=HEADERS, + validate_certs=module.params['validate_certs']) + except: + err = get_exception() + module.fail_json(msg="Failed to get snapshot images for group. Group [%s]. Id [%s]. Error [%s]" % + (name, ssid, str(err))) + if not images: + module.exit_json(msg="There are no snapshot images to remove. Group [%s]. Id [%s]." % (name, ssid)) + + oldest = min(images, key=lambda x: x['pitSequenceNumber']) + if oldest is None or "pitRef" not in oldest: + module.fail_json(msg="Failed to lookup oldest snapshot group. Group [%s]. Id [%s]." % (name, ssid)) + + return oldest + + +def create_image(module, ssid, api_url, pwd, user, p, snapshot_group): + snapshot_group_obj = snapshot_group_from_name(module, ssid, api_url, pwd, user, snapshot_group) + snapshot_group_id = snapshot_group_obj['pitGroupRef'] + endpoint = 'storage-systems/%s/snapshot-images' % ssid + url = api_url + endpoint + post_data = json.dumps({'groupId': snapshot_group_id}) + + image_data = request(url, data=post_data, method='POST', url_username=user, url_password=pwd, headers=HEADERS, + validate_certs=module.params['validate_certs']) + + if image_data[1]['status'] == 'optimal': + status = True + id = image_data[1]['id'] + else: + status = False + id = '' + + return status, id + + +def delete_image(module, ssid, api_url, pwd, user, snapshot_group): + image = oldest_image(module, ssid, api_url, pwd, user, snapshot_group) + image_id = image['pitRef'] + endpoint = 'storage-systems/%s/snapshot-images/%s' % (ssid, image_id) + url = api_url + endpoint + + try: + (ret, image_data) = request(url, method='DELETE', url_username=user, url_password=pwd, headers=HEADERS, + validate_certs=module.params['validate_certs']) + except Exception: + e = get_exception() + image_data = (e[0], e[1]) + + if ret == 204: + deleted_status = True + error_message = '' + else: + deleted_status = False + error_message = image_data[1]['errorMessage'] + + return deleted_status, error_message + + +def main(): + argument_spec = basic_auth_argument_spec() + argument_spec.update(dict( + snapshot_group=dict(required=True, type='str'), + ssid=dict(required=True, type='str'), + api_url=dict(required=True), + api_username=dict(required=False), + api_password=dict(required=False, no_log=True), + validate_certs=dict(required=False, default=True), + state=dict(required=True, choices=['create', 'remove'], type='str'), + )) + module = AnsibleModule(argument_spec) + + p = module.params + + ssid = p.pop('ssid') + api_url = p.pop('api_url') + user = p.pop('api_username') + pwd = p.pop('api_password') + snapshot_group = p.pop('snapshot_group') + desired_state = p.pop('state') + + if not api_url.endswith('/'): + api_url += '/' + + if desired_state == 'create': + created_status, snapshot_id = create_image(module, ssid, api_url, pwd, user, p, snapshot_group) + + if created_status: + module.exit_json(changed=True, msg='Created snapshot image', image_id=snapshot_id) + else: + module.fail_json( + msg="Could not create snapshot image on system %s, in snapshot group %s" % (ssid, snapshot_group)) + else: + deleted, error_msg = delete_image(module, ssid, api_url, pwd, user, snapshot_group) + + if deleted: + module.exit_json(changed=True, msg='Deleted snapshot image for snapshot group [%s]' % (snapshot_group)) + else: + module.fail_json( + msg="Could not create snapshot image on system %s, in snapshot group %s --- %s" % ( + ssid, snapshot_group, error_msg)) + + +if __name__ == '__main__': + main() diff --git a/storage/netapp/netapp_e_snapshot_volume.py b/storage/netapp/netapp_e_snapshot_volume.py new file mode 100644 index 00000000000..9a143bd4125 --- /dev/null +++ b/storage/netapp/netapp_e_snapshot_volume.py @@ -0,0 +1,287 @@ +#!/usr/bin/python + +# (c) 2016, NetApp, Inc +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +DOCUMENTATION = """ +--- +module: netapp_e_snapshot_volume +short_description: Manage E/EF-Series snapshot volumes. +description: + - Create, update, remove snapshot volumes for NetApp E/EF-Series storage arrays. +version_added: '2.2' +author: Kevin Hulquest (@hulquest) +note: Only I(full_threshold) is supported for update operations. If the snapshot volume already exists and the threshold matches, then an C(ok) status will be returned, no other changes can be made to a pre-existing snapshot volume. +options: + api_username: + required: true + description: + - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_password: + required: true + description: + - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_url: + required: true + description: + - The url to the SANtricity WebServices Proxy or embedded REST API. + example: + - https://prod-1.wahoo.acme.com/devmgr/v2 + validate_certs: + required: false + default: true + description: + - Should https certificates be validated? + ssid: + description: + - storage array ID + required: True + snapshot_image_id: + required: True + description: + - The identifier of the snapshot image used to create the new snapshot volume. + - "Note: You'll likely want to use the M(netapp_e_facts) module to find the ID of the image you want." + full_threshold: + description: + - The repository utilization warning threshold percentage + default: 85 + name: + required: True + description: + - The name you wish to give the snapshot volume + view_mode: + required: True + description: + - The snapshot volume access mode + choices: + - modeUnknown + - readWrite + - readOnly + - __UNDEFINED + repo_percentage: + description: + - The size of the view in relation to the size of the base volume + default: 20 + storage_pool_name: + description: + - Name of the storage pool on which to allocate the repository volume. + required: True + state: + description: + - Whether to create or remove the snapshot volume + required: True + choices: + - absent + - present +""" +EXAMPLES = """ + - name: Snapshot volume + netapp_e_snapshot_volume: + ssid: "{{ ssid }}" + api_url: "{{ netapp_api_url }}"/ + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + state: present + storage_pool_name: "{{ snapshot_volume_storage_pool_name }}" + snapshot_image_id: "{{ snapshot_volume_image_id }}" + name: "{{ snapshot_volume_name }}" +""" +RETURN = """ +msg: + description: Success message + returned: success + type: string + sample: Json facts for the volume that was created. +""" +HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json", +} +import json + +from ansible.module_utils.api import basic_auth_argument_spec +from ansible.module_utils.basic import AnsibleModule + +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import open_url +from ansible.module_utils.six.moves.urllib.error import HTTPError + + +def request(url, data=None, headers=None, method='GET', use_proxy=True, + force=False, last_mod_time=None, timeout=10, validate_certs=True, + url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False): + try: + r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy, + force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs, + url_username=url_username, url_password=url_password, http_agent=http_agent, + force_basic_auth=force_basic_auth) + except HTTPError: + err = get_exception() + r = err.fp + + try: + raw_data = r.read() + if raw_data: + data = json.loads(raw_data) + else: + raw_data = None + except: + if ignore_errors: + pass + else: + raise Exception(raw_data) + + resp_code = r.getcode() + + if resp_code >= 400 and not ignore_errors: + raise Exception(resp_code, data) + else: + return resp_code, data + + +class SnapshotVolume(object): + def __init__(self): + argument_spec = basic_auth_argument_spec() + argument_spec.update(dict( + api_username=dict(type='str', required=True), + api_password=dict(type='str', required=True, no_log=True), + api_url=dict(type='str', required=True), + ssid=dict(type='str', required=True), + snapshot_image_id=dict(type='str', required=True), + full_threshold=dict(type='int', default=85), + name=dict(type='str', required=True), + view_mode=dict(type='str', default='readOnly', + choices=['readOnly', 'readWrite', 'modeUnknown', '__Undefined']), + repo_percentage=dict(type='int', default=20), + storage_pool_name=dict(type='str', required=True), + state=dict(type='str', required=True, choices=['absent', 'present']) + )) + + self.module = AnsibleModule(argument_spec=argument_spec) + args = self.module.params + self.state = args['state'] + self.ssid = args['ssid'] + self.snapshot_image_id = args['snapshot_image_id'] + self.full_threshold = args['full_threshold'] + self.name = args['name'] + self.view_mode = args['view_mode'] + self.repo_percentage = args['repo_percentage'] + self.storage_pool_name = args['storage_pool_name'] + self.url = args['api_url'] + self.user = args['api_username'] + self.pwd = args['api_password'] + self.certs = args['validate_certs'] + + if not self.url.endswith('/'): + self.url += '/' + + @property + def pool_id(self): + pools = 'storage-systems/%s/storage-pools' % self.ssid + url = self.url + pools + (rc, data) = request(url, headers=HEADERS, url_username=self.user, url_password=self.pwd, + validate_certs=self.certs) + + for pool in data: + if pool['name'] == self.storage_pool_name: + self.pool_data = pool + return pool['id'] + + self.module.fail_json(msg="No storage pool with the name: '%s' was found" % self.name) + + @property + def ss_vol_exists(self): + rc, ss_vols = request(self.url + 'storage-systems/%s/snapshot-volumes' % self.ssid, headers=HEADERS, + url_username=self.user, url_password=self.pwd, validate_certs=self.certs) + if ss_vols: + for ss_vol in ss_vols: + if ss_vol['name'] == self.name: + self.ss_vol = ss_vol + return True + else: + return False + + return False + + @property + def ss_vol_needs_update(self): + if self.ss_vol['fullWarnThreshold'] != self.full_threshold: + return True + else: + return False + + def create_ss_vol(self): + post_data = dict( + snapshotImageId=self.snapshot_image_id, + fullThreshold=self.full_threshold, + name=self.name, + viewMode=self.view_mode, + repositoryPercentage=self.repo_percentage, + repositoryPoolId=self.pool_id + ) + + rc, create_resp = request(self.url + 'storage-systems/%s/snapshot-volumes' % self.ssid, + data=json.dumps(post_data), headers=HEADERS, url_username=self.user, + url_password=self.pwd, validate_certs=self.certs, method='POST') + + self.ss_vol = create_resp + # Doing a check after creation because the creation call fails to set the specified warning threshold + if self.ss_vol_needs_update: + self.update_ss_vol() + else: + self.module.exit_json(changed=True, **create_resp) + + def update_ss_vol(self): + post_data = dict( + fullThreshold=self.full_threshold, + ) + + rc, resp = request(self.url + 'storage-systems/%s/snapshot-volumes/%s' % (self.ssid, self.ss_vol['id']), + data=json.dumps(post_data), headers=HEADERS, url_username=self.user, url_password=self.pwd, + method='POST', validate_certs=self.certs) + + self.module.exit_json(changed=True, **resp) + + def remove_ss_vol(self): + rc, resp = request(self.url + 'storage-systems/%s/snapshot-volumes/%s' % (self.ssid, self.ss_vol['id']), + headers=HEADERS, url_username=self.user, url_password=self.pwd, validate_certs=self.certs, + method='DELETE') + self.module.exit_json(changed=True, msg="Volume successfully deleted") + + def apply(self): + if self.state == 'present': + if self.ss_vol_exists: + if self.ss_vol_needs_update: + self.update_ss_vol() + else: + self.module.exit_json(changed=False, **self.ss_vol) + else: + self.create_ss_vol() + else: + if self.ss_vol_exists: + self.remove_ss_vol() + else: + self.module.exit_json(changed=False, msg="Volume already absent") + + +def main(): + sv = SnapshotVolume() + sv.apply() + + +if __name__ == '__main__': + main() diff --git a/storage/netapp/netapp_e_storage_system.py b/storage/netapp/netapp_e_storage_system.py index 13ef7c9fbc5..40ef893ad9b 100644 --- a/storage/netapp/netapp_e_storage_system.py +++ b/storage/netapp/netapp_e_storage_system.py @@ -18,7 +18,7 @@ # along with Ansible. If not, see . # DOCUMENTATION = ''' -module: na_eseries_storage_system +module: netapp_e_storage_system version_added: "2.2" short_description: Add/remove arrays from the Web Services Proxy description: @@ -80,7 +80,7 @@ EXAMPLES = ''' --- - name: Presence of storage system - na_eseries_storage_system: + netapp_e_storage_system: ssid: "{{ item.key }}" state: present api_url: "{{ netapp_api_url }}" @@ -99,8 +99,7 @@ msg: Storage system added. ''' import json -import os -from datetime import datetime as dt, timedelta, time +from datetime import datetime as dt, timedelta from time import sleep from ansible.module_utils.api import basic_auth_argument_spec @@ -267,7 +266,6 @@ def main(): module.fail_json(msg="Failed to add storage system. Id[%s]. Request body [%s]. Error[%s]." % (ssid, request_data, str(err))) - else: # array exists, modify... post_headers = dict(Accept="application/json") post_headers['Content-Type'] = 'application/json' @@ -286,7 +284,6 @@ def main(): module.fail_json(msg="Failed to update storage system. Id[%s]. Request body [%s]. Error[%s]." % (ssid, post_body, str(err))) - elif state == 'absent': # delete the array try: diff --git a/storage/netapp/netapp_e_storagepool.py b/storage/netapp/netapp_e_storagepool.py new file mode 100644 index 00000000000..1d86ef46f6b --- /dev/null +++ b/storage/netapp/netapp_e_storagepool.py @@ -0,0 +1,884 @@ +#!/usr/bin/python + +# (c) 2016, NetApp, Inc +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: netapp_e_storagepool +short_description: Manage disk groups and disk pools +version_added: '2.2' +description: + - Create or remove disk groups and disk pools for NetApp E-series storage arrays. +options: + api_username: + required: true + description: + - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_password: + required: true + description: + - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_url: + required: true + description: + - The url to the SANtricity WebServices Proxy or embedded REST API. + example: + - https://prod-1.wahoo.acme.com/devmgr/v2 + validate_certs: + required: false + default: true + description: + - Should https certificates be validated? + ssid: + required: true + description: + - The ID of the array to manage (as configured on the web services proxy). + state: + required: true + description: + - Whether the specified storage pool should exist or not. + - Note that removing a storage pool currently requires the removal of all defined volumes first. + choices: ['present', 'absent'] + name: + required: true + description: + - The name of the storage pool to manage + criteria_drive_count: + description: + - The number of disks to use for building the storage pool. The pool will be expanded if this number exceeds the number of disks already in place + criteria_drive_type: + description: + - The type of disk (hdd or ssd) to use when searching for candidates to use. + choices: ['hdd','ssd'] + criteria_size_unit: + description: + - The unit used to interpret size parameters + choices: ['bytes', 'b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb'] + default: 'gb' + criteria_drive_min_size: + description: + - The minimum individual drive size (in size_unit) to consider when choosing drives for the storage pool. + criteria_min_usable_capacity: + description: + - The minimum size of the storage pool (in size_unit). The pool will be expanded if this value exceeds itscurrent size. + criteria_drive_interface_type: + description: + - The interface type to use when selecting drives for the storage pool (no value means all interface types will be considered) + choices: ['sas', 'sas4k', 'fibre', 'fibre520b', 'scsi', 'sata', 'pata'] + criteria_drive_require_fde: + description: + - Whether full disk encryption ability is required for drives to be added to the storage pool + raid_level: + required: true + choices: ['raidAll', 'raid0', 'raid1', 'raid3', 'raid5', 'raid6', 'raidDiskPool'] + description: + - "Only required when the requested state is 'present'. The RAID level of the storage pool to be created." + erase_secured_drives: + required: false + choices: ['true', 'false'] + description: + - Whether to erase secured disks before adding to storage pool + secure_pool: + required: false + choices: ['true', 'false'] + description: + - Whether to convert to a secure storage pool. Will only work if all drives in the pool are security capable. + reserve_drive_count: + required: false + description: + - Set the number of drives reserved by the storage pool for reconstruction operations. Only valide on raid disk pools. + remove_volumes: + required: false + default: False + description: + - Prior to removing a storage pool, delete all volumes in the pool. +author: Kevin Hulquest (@hulquest) + +''' +EXAMPLES = ''' + - name: No disk groups + netapp_e_storagepool: + ssid: "{{ ssid }}" + name: "{{ item }}" + state: absent + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" +''' +RETURN = ''' +msg: + description: Success message + returned: success + type: string + sample: Json facts for the pool that was created. +''' + +import json +import logging +from traceback import format_exc + +from ansible.module_utils.api import basic_auth_argument_spec +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import open_url +from ansible.module_utils.six.moves.urllib.error import HTTPError + + +def request(url, data=None, headers=None, method='GET', use_proxy=True, + force=False, last_mod_time=None, timeout=10, validate_certs=True, + url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False): + try: + r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy, + force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs, + url_username=url_username, url_password=url_password, http_agent=http_agent, + force_basic_auth=force_basic_auth) + except HTTPError: + err = get_exception() + r = err.fp + + try: + raw_data = r.read() + if raw_data: + data = json.loads(raw_data) + else: + raw_data = None + except: + if ignore_errors: + pass + else: + raise Exception(raw_data) + + resp_code = r.getcode() + + if resp_code >= 400 and not ignore_errors: + raise Exception(resp_code, data) + else: + return resp_code, data + + +def select(predicate, iterable): + # python 2, 3 generic filtering. + if predicate is None: + predicate = bool + for x in iterable: + if predicate(x): + yield x + + +class groupby(object): + # python 2, 3 generic grouping. + def __init__(self, iterable, key=None): + if key is None: + key = lambda x: x + self.keyfunc = key + self.it = iter(iterable) + self.tgtkey = self.currkey = self.currvalue = object() + + def __iter__(self): + return self + + def next(self): + while self.currkey == self.tgtkey: + self.currvalue = next(self.it) # Exit on StopIteration + self.currkey = self.keyfunc(self.currvalue) + self.tgtkey = self.currkey + return (self.currkey, self._grouper(self.tgtkey)) + + def _grouper(self, tgtkey): + while self.currkey == tgtkey: + yield self.currvalue + self.currvalue = next(self.it) # Exit on StopIteration + self.currkey = self.keyfunc(self.currvalue) + + +class NetAppESeriesStoragePool(object): + def __init__(self): + self._sp_drives_cached = None + + self._size_unit_map = dict( + bytes=1, + b=1, + kb=1024, + mb=1024 ** 2, + gb=1024 ** 3, + tb=1024 ** 4, + pb=1024 ** 5, + eb=1024 ** 6, + zb=1024 ** 7, + yb=1024 ** 8 + ) + + argument_spec = basic_auth_argument_spec() + argument_spec.update(dict( + api_username=dict(type='str', required=True), + api_password=dict(type='str', required=True, no_log=True), + api_url=dict(type='str', required=True), + state=dict(required=True, choices=['present', 'absent'], type='str'), + ssid=dict(required=True, type='str'), + name=dict(required=True, type='str'), + criteria_size_unit=dict(default='gb', type='str'), + criteria_drive_count=dict(type='int'), + criteria_drive_interface_type=dict(choices=['sas', 'sas4k', 'fibre', 'fibre520b', 'scsi', 'sata', 'pata'], + type='str'), + criteria_drive_type=dict(choices=['ssd', 'hdd'], type='str'), + criteria_drive_min_size=dict(type='int'), + criteria_drive_require_fde=dict(type='bool'), + criteria_min_usable_capacity=dict(type='int'), + raid_level=dict( + choices=['raidUnsupported', 'raidAll', 'raid0', 'raid1', 'raid3', 'raid5', 'raid6', 'raidDiskPool']), + erase_secured_drives=dict(type='bool'), + log_path=dict(type='str'), + remove_drives=dict(type='list'), + secure_pool=dict(type='bool', default=False), + reserve_drive_count=dict(type='int'), + remove_volumes=dict(type='bool', default=False) + )) + + self.module = AnsibleModule( + argument_spec=argument_spec, + required_if=[ + ('state', 'present', ['raid_level']) + ], + mutually_exclusive=[ + + ], + # TODO: update validation for various selection criteria + supports_check_mode=True + ) + + p = self.module.params + + log_path = p['log_path'] + + # logging setup + self._logger = logging.getLogger(self.__class__.__name__) + self.debug = self._logger.debug + + if log_path: + logging.basicConfig(level=logging.DEBUG, filename=log_path) + + self.state = p['state'] + self.ssid = p['ssid'] + self.name = p['name'] + self.validate_certs = p['validate_certs'] + + self.criteria_drive_count = p['criteria_drive_count'] + self.criteria_drive_type = p['criteria_drive_type'] + self.criteria_size_unit = p['criteria_size_unit'] + self.criteria_drive_min_size = p['criteria_drive_min_size'] + self.criteria_min_usable_capacity = p['criteria_min_usable_capacity'] + self.criteria_drive_interface_type = p['criteria_drive_interface_type'] + self.criteria_drive_require_fde = p['criteria_drive_require_fde'] + + self.raid_level = p['raid_level'] + self.erase_secured_drives = p['erase_secured_drives'] + self.remove_drives = p['remove_drives'] + self.secure_pool = p['secure_pool'] + self.reserve_drive_count = p['reserve_drive_count'] + self.remove_volumes = p['remove_volumes'] + + try: + self.api_usr = p['api_username'] + self.api_pwd = p['api_password'] + self.api_url = p['api_url'] + except KeyError: + self.module.fail_json(msg="You must pass in api_username " + "and api_password and api_url to the module.") + + self.post_headers = dict(Accept="application/json") + self.post_headers['Content-Type'] = 'application/json' + + # Quick and dirty drive selector, since the one provided by web service proxy is broken for min_disk_size as of 2016-03-12. + # Doesn't really need to be a class once this is in module_utils or retired- just groups everything together so we + # can copy/paste to other modules more easily. + # Filters all disks by specified criteria, then groups remaining disks by capacity, interface and disk type, and selects + # the first set that matches the specified count and/or aggregate capacity. + # class DriveSelector(object): + def filter_drives( + self, + drives, # raw drives resp + interface_type=None, # sas, sata, fibre, etc + drive_type=None, # ssd/hdd + spindle_speed=None, # 7200, 10000, 15000, ssd (=0) + min_drive_size=None, + max_drive_size=None, + fde_required=None, + size_unit='gb', + min_total_capacity=None, + min_drive_count=None, + exact_drive_count=None, + raid_level=None + ): + if min_total_capacity is None and exact_drive_count is None: + raise Exception("One of criteria_min_total_capacity or criteria_drive_count must be specified.") + + if min_total_capacity: + min_total_capacity = min_total_capacity * self._size_unit_map[size_unit] + + # filter clearly invalid/unavailable drives first + drives = select(lambda d: self._is_valid_drive(d), drives) + + if interface_type: + drives = select(lambda d: d['phyDriveType'] == interface_type, drives) + + if drive_type: + drives = select(lambda d: d['driveMediaType'] == drive_type, drives) + + if spindle_speed is not None: # 0 is valid for ssds + drives = select(lambda d: d['spindleSpeed'] == spindle_speed, drives) + + if min_drive_size: + min_drive_size_bytes = min_drive_size * self._size_unit_map[size_unit] + drives = select(lambda d: int(d['rawCapacity']) >= min_drive_size_bytes, drives) + + if max_drive_size: + max_drive_size_bytes = max_drive_size * self._size_unit_map[size_unit] + drives = select(lambda d: int(d['rawCapacity']) <= max_drive_size_bytes, drives) + + if fde_required: + drives = select(lambda d: d['fdeCapable'], drives) + + # initial implementation doesn't have a preference for any of these values... + # just return the first set we find that matches the requested disk count and/or minimum total capacity + for (cur_capacity, drives_by_capacity) in groupby(drives, lambda d: int(d['rawCapacity'])): + for (cur_interface_type, drives_by_interface_type) in groupby(drives_by_capacity, + lambda d: d['phyDriveType']): + for (cur_drive_type, drives_by_drive_type) in groupby(drives_by_interface_type, + lambda d: d['driveMediaType']): + # listify so we can consume more than once + drives_by_drive_type = list(drives_by_drive_type) + candidate_set = list() # reset candidate list on each iteration of the innermost loop + + if exact_drive_count: + if len(drives_by_drive_type) < exact_drive_count: + continue # we know this set is too small, move on + + for drive in drives_by_drive_type: + candidate_set.append(drive) + if self._candidate_set_passes(candidate_set, min_capacity_bytes=min_total_capacity, + min_drive_count=min_drive_count, + exact_drive_count=exact_drive_count, raid_level=raid_level): + return candidate_set + + raise Exception("couldn't find an available set of disks to match specified criteria") + + def _is_valid_drive(self, d): + is_valid = d['available'] \ + and d['status'] == 'optimal' \ + and not d['pfa'] \ + and not d['removed'] \ + and not d['uncertified'] \ + and not d['invalidDriveData'] \ + and not d['nonRedundantAccess'] + + return is_valid + + def _candidate_set_passes(self, candidate_set, min_capacity_bytes=None, min_drive_count=None, + exact_drive_count=None, raid_level=None): + if not self._is_drive_count_valid(len(candidate_set), min_drive_count=min_drive_count, + exact_drive_count=exact_drive_count, raid_level=raid_level): + return False + # TODO: this assumes candidate_set is all the same size- if we want to allow wastage, need to update to use min size of set + if min_capacity_bytes is not None and self._calculate_usable_capacity(int(candidate_set[0]['rawCapacity']), + len(candidate_set), + raid_level=raid_level) < min_capacity_bytes: + return False + + return True + + def _calculate_usable_capacity(self, disk_size_bytes, disk_count, raid_level=None): + if raid_level in [None, 'raid0']: + return disk_size_bytes * disk_count + if raid_level == 'raid1': + return (disk_size_bytes * disk_count) / 2 + if raid_level in ['raid3', 'raid5']: + return (disk_size_bytes * disk_count) - disk_size_bytes + if raid_level in ['raid6', 'raidDiskPool']: + return (disk_size_bytes * disk_count) - (disk_size_bytes * 2) + raise Exception("unsupported raid_level: %s" % raid_level) + + def _is_drive_count_valid(self, drive_count, min_drive_count=0, exact_drive_count=None, raid_level=None): + if exact_drive_count and exact_drive_count != drive_count: + return False + if raid_level == 'raidDiskPool': + if drive_count < 11: + return False + if raid_level == 'raid1': + if drive_count % 2 != 0: + return False + if raid_level in ['raid3', 'raid5']: + if drive_count < 3: + return False + if raid_level == 'raid6': + if drive_count < 4: + return False + if min_drive_count and drive_count < min_drive_count: + return False + + return True + + def get_storage_pool(self, storage_pool_name): + # global ifilter + self.debug("fetching storage pools") + # map the storage pool name to its id + try: + (rc, resp) = request(self.api_url + "/storage-systems/%s/storage-pools" % (self.ssid), + headers=dict(Accept="application/json"), url_username=self.api_usr, + url_password=self.api_pwd, validate_certs=self.validate_certs) + except Exception: + err = get_exception() + rc = err.args[0] + if rc == 404 and self.state == 'absent': + self.module.exit_json( + msg="Storage pool [%s] did not exist." % (self.name)) + else: + err = get_exception() + self.module.exit_json( + msg="Failed to get storage pools. Array id [%s]. Error[%s]. State[%s]. RC[%s]." % + (self.ssid, str(err), self.state, rc)) + + self.debug("searching for storage pool '%s'" % storage_pool_name) + + pool_detail = next(select(lambda a: a['name'] == storage_pool_name, resp), None) + + if pool_detail: + found = 'found' + else: + found = 'not found' + self.debug(found) + + return pool_detail + + def get_candidate_disks(self): + self.debug("getting candidate disks...") + + # driveCapacityMin is broken on /drives POST. Per NetApp request we built our own + # switch back to commented code below if it gets fixed + # drives_req = dict( + # driveCount = self.criteria_drive_count, + # sizeUnit = 'mb', + # raidLevel = self.raid_level + # ) + # + # if self.criteria_drive_type: + # drives_req['driveType'] = self.criteria_drive_type + # if self.criteria_disk_min_aggregate_size_mb: + # drives_req['targetUsableCapacity'] = self.criteria_disk_min_aggregate_size_mb + # + # # TODO: this arg appears to be ignored, uncomment if it isn't + # #if self.criteria_disk_min_size_gb: + # # drives_req['driveCapacityMin'] = self.criteria_disk_min_size_gb * 1024 + # (rc,drives_resp) = request(self.api_url + "/storage-systems/%s/drives" % (self.ssid), data=json.dumps(drives_req), headers=self.post_headers, method='POST', url_username=self.api_usr, url_password=self.api_pwd, validate_certs=self.validate_certs) + # + # if rc == 204: + # self.module.fail_json(msg='Cannot find disks to match requested criteria for storage pool') + + # disk_ids = [d['id'] for d in drives_resp] + + try: + (rc, drives_resp) = request(self.api_url + "/storage-systems/%s/drives" % (self.ssid), method='GET', + url_username=self.api_usr, url_password=self.api_pwd, + validate_certs=self.validate_certs) + except: + err = get_exception() + self.module.exit_json( + msg="Failed to fetch disk drives. Array id [%s]. Error[%s]." % (self.ssid, str(err))) + + try: + candidate_set = self.filter_drives(drives_resp, + exact_drive_count=self.criteria_drive_count, + drive_type=self.criteria_drive_type, + min_drive_size=self.criteria_drive_min_size, + raid_level=self.raid_level, + size_unit=self.criteria_size_unit, + min_total_capacity=self.criteria_min_usable_capacity, + interface_type=self.criteria_drive_interface_type, + fde_required=self.criteria_drive_require_fde + ) + except: + err = get_exception() + self.module.fail_json( + msg="Failed to allocate adequate drive count. Id [%s]. Error [%s]." % (self.ssid, str(err))) + + disk_ids = [d['id'] for d in candidate_set] + + return disk_ids + + def create_storage_pool(self): + self.debug("creating storage pool...") + + sp_add_req = dict( + raidLevel=self.raid_level, + diskDriveIds=self.disk_ids, + name=self.name + ) + + if self.erase_secured_drives: + sp_add_req['eraseSecuredDrives'] = self.erase_secured_drives + + try: + (rc, resp) = request(self.api_url + "/storage-systems/%s/storage-pools" % (self.ssid), + data=json.dumps(sp_add_req), headers=self.post_headers, method='POST', + url_username=self.api_usr, url_password=self.api_pwd, + validate_certs=self.validate_certs, + timeout=120) + except: + err = get_exception() + pool_id = self.pool_detail['id'] + self.module.exit_json( + msg="Failed to create storage pool. Pool id [%s]. Array id [%s]. Error[%s]." % (pool_id, + self.ssid, + str(err))) + + self.pool_detail = self.get_storage_pool(self.name) + + if self.secure_pool: + secure_pool_data = dict(securePool=True) + try: + (retc, r) = request( + self.api_url + "/storage-systems/%s/storage-pools/%s" % (self.ssid, self.pool_detail['id']), + data=json.dumps(secure_pool_data), headers=self.post_headers, method='POST', + url_username=self.api_usr, + url_password=self.api_pwd, validate_certs=self.validate_certs, timeout=120, ignore_errors=True) + except: + err = get_exception() + pool_id = self.pool_detail['id'] + self.module.exit_json( + msg="Failed to update storage pool. Pool id [%s]. Array id [%s]. Error[%s]." % (pool_id, + self.ssid, + str(err))) + + @property + def needs_raid_level_migration(self): + current_raid_level = self.pool_detail['raidLevel'] + needs_migration = self.raid_level != current_raid_level + + if needs_migration: # sanity check some things so we can fail early/check-mode + if current_raid_level == 'raidDiskPool': + self.module.fail_json(msg="raid level cannot be changed for disk pools") + + return needs_migration + + def migrate_raid_level(self): + self.debug("migrating storage pool to raid level '%s'..." % self.raid_level) + sp_raid_migrate_req = dict( + raidLevel=self.raid_level + ) + try: + (rc, resp) = request( + self.api_url + "/storage-systems/%s/storage-pools/%s/raid-type-migration" % (self.ssid, + self.name), + data=json.dumps(sp_raid_migrate_req), headers=self.post_headers, method='POST', + url_username=self.api_usr, + url_password=self.api_pwd, validate_certs=self.validate_certs, timeout=120) + except: + err = get_exception() + pool_id = self.pool_detail['id'] + self.module.exit_json( + msg="Failed to change the raid level of storage pool. Pool id [%s]. Array id [%s]. Error[%s]." % ( + pool_id, self.ssid, str(err))) + + @property + def sp_drives(self, exclude_hotspares=True): + if not self._sp_drives_cached: + + self.debug("fetching drive list...") + try: + (rc, resp) = request(self.api_url + "/storage-systems/%s/drives" % (self.ssid), method='GET', + url_username=self.api_usr, url_password=self.api_pwd, + validate_certs=self.validate_certs) + except: + err = get_exception() + pool_id = self.pool_detail['id'] + self.module.exit_json( + msg="Failed to fetch disk drives. Pool id [%s]. Array id [%s]. Error[%s]." % (pool_id, self.ssid, str(err))) + + sp_id = self.pool_detail['id'] + if exclude_hotspares: + self._sp_drives_cached = [d for d in resp if d['currentVolumeGroupRef'] == sp_id and not d['hotSpare']] + else: + self._sp_drives_cached = [d for d in resp if d['currentVolumeGroupRef'] == sp_id] + + return self._sp_drives_cached + + @property + def reserved_drive_count_differs(self): + if int(self.pool_detail['volumeGroupData']['diskPoolData'][ + 'reconstructionReservedDriveCount']) != self.reserve_drive_count: + return True + return False + + @property + def needs_expansion(self): + if self.criteria_drive_count > len(self.sp_drives): + return True + # TODO: is totalRaidedSpace the best attribute for "how big is this SP"? + if self.criteria_min_usable_capacity and \ + (self.criteria_min_usable_capacity * self._size_unit_map[self.criteria_size_unit]) > int(self.pool_detail['totalRaidedSpace']): + return True + + return False + + def get_expansion_candidate_drives(self): + # sanity checks; don't call this if we can't/don't need to expand + if not self.needs_expansion: + self.module.fail_json(msg="can't get expansion candidates when pool doesn't need expansion") + + self.debug("fetching expansion candidate drives...") + try: + (rc, resp) = request( + self.api_url + "/storage-systems/%s/storage-pools/%s/expand" % (self.ssid, + self.pool_detail['id']), + method='GET', url_username=self.api_usr, url_password=self.api_pwd, validate_certs=self.validate_certs, + timeout=120) + except: + err = get_exception() + pool_id = self.pool_detail['id'] + self.module.exit_json( + msg="Failed to fetch candidate drives for storage pool. Pool id [%s]. Array id [%s]. Error[%s]." % ( + pool_id, self.ssid, str(err))) + + current_drive_count = len(self.sp_drives) + current_capacity_bytes = int(self.pool_detail['totalRaidedSpace']) # TODO: is this the right attribute to use? + + if self.criteria_min_usable_capacity: + requested_capacity_bytes = self.criteria_min_usable_capacity * self._size_unit_map[self.criteria_size_unit] + else: + requested_capacity_bytes = current_capacity_bytes + + if self.criteria_drive_count: + minimum_disks_to_add = max((self.criteria_drive_count - current_drive_count), 1) + else: + minimum_disks_to_add = 1 + + minimum_bytes_to_add = max(requested_capacity_bytes - current_capacity_bytes, 0) + + # FUTURE: allow more control over expansion candidate selection? + # loop over candidate disk sets and add until we've met both criteria + + added_drive_count = 0 + added_capacity_bytes = 0 + + drives_to_add = set() + + for s in resp: + # don't trust the API not to give us duplicate drives across candidate sets, especially in multi-drive sets + candidate_drives = s['drives'] + if len(drives_to_add.intersection(candidate_drives)) != 0: + # duplicate, skip + continue + drives_to_add.update(candidate_drives) + added_drive_count += len(candidate_drives) + added_capacity_bytes += int(s['usableCapacity']) + + if added_drive_count >= minimum_disks_to_add and added_capacity_bytes >= minimum_bytes_to_add: + break + + if (added_drive_count < minimum_disks_to_add) or (added_capacity_bytes < minimum_bytes_to_add): + self.module.fail_json( + msg="unable to find at least %s drives to add that would add at least %s bytes of capacity" % ( + minimum_disks_to_add, minimum_bytes_to_add)) + + return list(drives_to_add) + + def expand_storage_pool(self): + drives_to_add = self.get_expansion_candidate_drives() + + self.debug("adding %s drives to storage pool..." % len(drives_to_add)) + sp_expand_req = dict( + drives=drives_to_add + ) + try: + request( + self.api_url + "/storage-systems/%s/storage-pools/%s/expand" % (self.ssid, + self.pool_detail['id']), + data=json.dumps(sp_expand_req), headers=self.post_headers, method='POST', url_username=self.api_usr, + url_password=self.api_pwd, validate_certs=self.validate_certs, timeout=120) + except: + err = get_exception() + pool_id = self.pool_detail['id'] + self.module.exit_json( + msg="Failed to add drives to storage pool. Pool id [%s]. Array id [%s]. Error[%s]." % (pool_id, + self.ssid, + str( + err))) + + # TODO: check response + # TODO: support blocking wait? + + def reduce_drives(self, drive_list): + if all(drive in drive_list for drive in self.sp_drives): + # all the drives passed in are present in the system + pass + else: + self.module.fail_json( + msg="One of the drives you wish to remove does not currently exist in the storage pool you specified") + + try: + (rc, resp) = request( + self.api_url + "/storage-systems/%s/storage-pools/%s/reduction" % (self.ssid, + self.pool_detail['id']), + data=json.dumps(drive_list), headers=self.post_headers, method='POST', url_username=self.api_usr, + url_password=self.api_pwd, validate_certs=self.validate_certs, timeout=120) + except: + err = get_exception() + pool_id = self.pool_detail['id'] + self.module.exit_json( + msg="Failed to remove drives from storage pool. Pool id [%s]. Array id [%s]. Error[%s]." % ( + pool_id, self.ssid, str(err))) + + def update_reserve_drive_count(self, qty): + data = dict(reservedDriveCount=qty) + try: + (rc, resp) = request( + self.api_url + "/storage-systems/%s/storage-pools/%s" % (self.ssid, self.pool_detail['id']), + data=json.dumps(data), headers=self.post_headers, method='POST', url_username=self.api_usr, + url_password=self.api_pwd, validate_certs=self.validate_certs, timeout=120) + except: + err = get_exception() + pool_id = self.pool_detail['id'] + self.module.exit_json( + msg="Failed to update reserve drive count. Pool id [%s]. Array id [%s]. Error[%s]." % (pool_id, + self.ssid, + str( + err))) + + def apply(self): + changed = False + pool_exists = False + + self.pool_detail = self.get_storage_pool(self.name) + + if self.pool_detail: + pool_exists = True + pool_id = self.pool_detail['id'] + + if self.state == 'absent': + self.debug("CHANGED: storage pool exists, but requested state is 'absent'") + changed = True + elif self.state == 'present': + # sanity checks first- we can't change these, so we'll bomb if they're specified + if self.criteria_drive_type and self.criteria_drive_type != self.pool_detail['driveMediaType']: + self.module.fail_json( + msg="drive media type %s cannot be changed to %s" % (self.pool_detail['driveMediaType'], + self.criteria_drive_type)) + + # now the things we can change... + if self.needs_expansion: + self.debug("CHANGED: storage pool needs expansion") + changed = True + + if self.needs_raid_level_migration: + self.debug( + "CHANGED: raid level migration required; storage pool uses '%s', requested is '%s'" % ( + self.pool_detail['raidLevel'], self.raid_level)) + changed = True + + # if self.reserved_drive_count_differs: + # changed = True + + # TODO: validate other state details? (pool priority, alert threshold) + + # per FPoole and others, pool reduce operations will not be supported. Automatic "smart" reduction + # presents a difficult parameter issue, as the disk count can increase due to expansion, so we + # can't just use disk count > criteria_drive_count. + + else: # pool does not exist + if self.state == 'present': + self.debug("CHANGED: storage pool does not exist, but requested state is 'present'") + changed = True + + # ensure we can get back a workable set of disks + # (doing this early so candidate selection runs under check mode) + self.disk_ids = self.get_candidate_disks() + else: + self.module.exit_json(msg="Storage pool [%s] did not exist." % (self.name)) + + if changed and not self.module.check_mode: + # apply changes + if self.state == 'present': + if not pool_exists: + self.create_storage_pool() + else: # pool exists but differs, modify... + if self.needs_expansion: + self.expand_storage_pool() + + if self.remove_drives: + self.reduce_drives(self.remove_drives) + + if self.needs_raid_level_migration: + self.migrate_raid_level() + + # if self.reserved_drive_count_differs: + # self.update_reserve_drive_count(self.reserve_drive_count) + + if self.secure_pool: + secure_pool_data = dict(securePool=True) + try: + (retc, r) = request( + self.api_url + "/storage-systems/%s/storage-pools/%s" % (self.ssid, + self.pool_detail[ + 'id']), + data=json.dumps(secure_pool_data), headers=self.post_headers, method='POST', + url_username=self.api_usr, url_password=self.api_pwd, + validate_certs=self.validate_certs, timeout=120, ignore_errors=True) + except: + err = get_exception() + self.module.exit_json( + msg="Failed to delete storage pool. Pool id [%s]. Array id [%s]. Error[%s]." % ( + pool_id, self.ssid, str(err))) + + if int(retc) == 422: + self.module.fail_json( + msg="Error in enabling secure pool. One of the drives in the specified storage pool is likely not security capable") + + elif self.state == 'absent': + # delete the storage pool + try: + remove_vol_opt = '' + if self.remove_volumes: + remove_vol_opt = '?delete-volumes=true' + (rc, resp) = request( + self.api_url + "/storage-systems/%s/storage-pools/%s%s" % (self.ssid, pool_id, + remove_vol_opt), + method='DELETE', + url_username=self.api_usr, url_password=self.api_pwd, validate_certs=self.validate_certs, + timeout=120) + except: + err = get_exception() + self.module.exit_json( + msg="Failed to delete storage pool. Pool id [%s]. Array id [%s]. Error[%s]." % (pool_id, + self.ssid, + str(err))) + + self.module.exit_json(changed=changed, **self.pool_detail) + + +def main(): + sp = NetAppESeriesStoragePool() + try: + sp.apply() + except Exception: + e = get_exception() + sp.debug("Exception in apply(): \n%s" % format_exc(e)) + raise + + +if __name__ == '__main__': + main() diff --git a/storage/netapp/netapp_e_volume.py b/storage/netapp/netapp_e_volume.py new file mode 100644 index 00000000000..09825c5201e --- /dev/null +++ b/storage/netapp/netapp_e_volume.py @@ -0,0 +1,618 @@ +#!/usr/bin/python + +# (c) 2016, NetApp, Inc +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +from ansible.module_utils.api import basic_auth_argument_spec + +DOCUMENTATION = ''' +--- +module: netapp_e_volume +version_added: "2.2" +short_description: Manage storage volumes (standard and thin) +description: + - Create or remove volumes (standard and thin) for NetApp E/EF-series storage arrays. +options: + api_username: + required: true + description: + - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_password: + required: true + description: + - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_url: + required: true + description: + - The url to the SANtricity WebServices Proxy or embedded REST API. + example: + - https://prod-1.wahoo.acme.com/devmgr/v2 + validate_certs: + required: false + default: true + description: + - Should https certificates be validated? + ssid: + required: true + description: + - The ID of the array to manage (as configured on the web services proxy). + state: + required: true + description: + - Whether the specified volume should exist or not. + choices: ['present', 'absent'] + name: + required: true + description: + - The name of the volume to manage + storage_pool_name: + required: true + description: + - "Required only when requested state is 'present'. The name of the storage pool the volume should exist on." + size_unit: + description: + - The unit used to interpret the size parameter + choices: ['bytes', 'b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb'] + default: 'gb' + size: + required: true + description: + - "Required only when state = 'present'. The size of the volume in (size_unit)." + segment_size_kb: + description: + - The segment size of the new volume + default: 512 + thin_provision: + description: + - Whether the volume should be thin provisioned. Thin volumes can only be created on disk pools (raidDiskPool). + default: False + choices: ['yes','no','true','false'] + thin_volume_repo_size: + description: + - Initial size of the thin volume repository volume (in size_unit) + required: True + thin_volume_max_repo_size: + description: + - Maximum size that the thin volume repository volume will automatically expand to + default: same as size (in size_unit) + ssd_cache_enabled: + description: + - Whether an existing SSD cache should be enabled on the volume (fails if no SSD cache defined) + default: None (ignores existing SSD cache setting) + choices: ['yes','no','true','false'] + data_assurance_enabled: + description: + - If data assurance should be enabled for the volume + default: false + +# TODO: doc thin volume parameters + +author: Kevin Hulquest (@hulquest) + +''' +EXAMPLES = ''' + - name: No thin volume + netapp_e_volume: + ssid: "{{ ssid }}" + name: NewThinVolumeByAnsible + state: absent + log_path: /tmp/volume.log + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" + when: check_volume + + + - name: No fat volume + netapp_e_volume: + ssid: "{{ ssid }}" + name: NewVolumeByAnsible + state: absent + log_path: /tmp/volume.log + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" + when: check_volume +''' +RETURN = ''' +--- +msg: "Standard volume [workload_vol_1] has been created." +msg: "Thin volume [workload_thin_vol] has been created." +msg: "Volume [workload_vol_1] has been expanded." +msg: "Volume [workload_vol_1] has been deleted." +msg: "Volume [workload_vol_1] did not exist." +msg: "Volume [workload_vol_1] already exists." +''' + +import json +import logging +import time +from traceback import format_exc + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import open_url +from ansible.module_utils.six.moves.urllib.error import HTTPError + + +def request(url, data=None, headers=None, method='GET', use_proxy=True, + force=False, last_mod_time=None, timeout=10, validate_certs=True, + url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False): + try: + r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy, + force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs, + url_username=url_username, url_password=url_password, http_agent=http_agent, + force_basic_auth=force_basic_auth) + except HTTPError: + err = get_exception() + r = err.fp + + try: + raw_data = r.read() + if raw_data: + data = json.loads(raw_data) + else: + raw_data is None + except: + if ignore_errors: + pass + else: + raise Exception(raw_data) + + resp_code = r.getcode() + + if resp_code >= 400 and not ignore_errors: + raise Exception(resp_code, data) + else: + return resp_code, data + + +def ifilter(predicate, iterable): + # python 2, 3 generic filtering. + if predicate is None: + predicate = bool + for x in iterable: + if predicate(x): + yield x + + +class NetAppESeriesVolume(object): + def __init__(self): + self._size_unit_map = dict( + bytes=1, + b=1, + kb=1024, + mb=1024 ** 2, + gb=1024 ** 3, + tb=1024 ** 4, + pb=1024 ** 5, + eb=1024 ** 6, + zb=1024 ** 7, + yb=1024 ** 8 + ) + + self._post_headers = dict(Accept="application/json") + self._post_headers['Content-Type'] = 'application/json' + + argument_spec = basic_auth_argument_spec() + argument_spec.update(dict( + state=dict(required=True, choices=['present', 'absent']), + ssid=dict(required=True, type='str'), + name=dict(required=True, type='str'), + storage_pool_name=dict(type='str'), + size_unit=dict(default='gb', choices=['bytes', 'b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb'], + type='str'), + size=dict(type='int'), + segment_size_kb=dict(default=128, choices=[8, 16, 32, 64, 128, 256, 512], type='int'), + ssd_cache_enabled=dict(type='bool'), # no default, leave existing setting alone + data_assurance_enabled=dict(default=False, type='bool'), + thin_provision=dict(default=False, type='bool'), + thin_volume_repo_size=dict(type='int'), + thin_volume_max_repo_size=dict(type='int'), + # TODO: add cache, owning controller support, thin expansion policy, etc + log_path=dict(type='str'), + api_url=dict(type='str'), + api_username=dict(type='str'), + api_password=dict(type='str'), + validate_certs=dict(type='bool'), + )) + + self.module = AnsibleModule(argument_spec=argument_spec, + required_if=[ + ('state', 'present', ['storage_pool_name', 'size']), + ('thin_provision', 'true', ['thin_volume_repo_size']) + ], + supports_check_mode=True) + p = self.module.params + + log_path = p['log_path'] + + # logging setup + self._logger = logging.getLogger(self.__class__.__name__) + self.debug = self._logger.debug + + if log_path: + logging.basicConfig(level=logging.DEBUG, filename=log_path) + + self.state = p['state'] + self.ssid = p['ssid'] + self.name = p['name'] + self.storage_pool_name = p['storage_pool_name'] + self.size_unit = p['size_unit'] + self.size = p['size'] + self.segment_size_kb = p['segment_size_kb'] + self.ssd_cache_enabled = p['ssd_cache_enabled'] + self.data_assurance_enabled = p['data_assurance_enabled'] + self.thin_provision = p['thin_provision'] + self.thin_volume_repo_size = p['thin_volume_repo_size'] + self.thin_volume_max_repo_size = p['thin_volume_max_repo_size'] + + if not self.thin_volume_max_repo_size: + self.thin_volume_max_repo_size = self.size + + self.validate_certs = p['validate_certs'] + + try: + self.api_usr = p['api_username'] + self.api_pwd = p['api_password'] + self.api_url = p['api_url'] + except KeyError: + self.module.fail_json(msg="You must pass in api_username " + "and api_password and api_url to the module.") + + def get_volume(self, volume_name): + self.debug('fetching volumes') + # fetch the list of volume objects and look for one with a matching name (we'll need to merge volumes and thin-volumes) + try: + (rc, volumes) = request(self.api_url + "/storage-systems/%s/volumes" % (self.ssid), + headers=dict(Accept="application/json"), url_username=self.api_usr, + url_password=self.api_pwd, validate_certs=self.validate_certs) + except Exception: + err = get_exception() + self.module.fail_json( + msg="Failed to obtain list of standard/thick volumes. Array Id [%s]. Error[%s]." % (self.ssid, + str(err))) + + try: + self.debug('fetching thin-volumes') + (rc, thinvols) = request(self.api_url + "/storage-systems/%s/thin-volumes" % (self.ssid), + headers=dict(Accept="application/json"), url_username=self.api_usr, + url_password=self.api_pwd, validate_certs=self.validate_certs) + except Exception: + err = get_exception() + self.module.fail_json( + msg="Failed to obtain list of thin volumes. Array Id [%s]. Error[%s]." % (self.ssid, str(err))) + + volumes.extend(thinvols) + + self.debug("searching for volume '%s'" % volume_name) + volume_detail = next(ifilter(lambda a: a['name'] == volume_name, volumes), None) + + if volume_detail: + self.debug('found') + else: + self.debug('not found') + + return volume_detail + + def get_storage_pool(self, storage_pool_name): + self.debug("fetching storage pools") + # map the storage pool name to its id + try: + (rc, resp) = request(self.api_url + "/storage-systems/%s/storage-pools" % (self.ssid), + headers=dict(Accept="application/json"), url_username=self.api_usr, + url_password=self.api_pwd, validate_certs=self.validate_certs) + except Exception: + err = get_exception() + self.module.fail_json( + msg="Failed to obtain list of storage pools. Array Id [%s]. Error[%s]." % (self.ssid, str(err))) + + self.debug("searching for storage pool '%s'" % storage_pool_name) + pool_detail = next(ifilter(lambda a: a['name'] == storage_pool_name, resp), None) + + if pool_detail: + self.debug('found') + else: + self.debug('not found') + + return pool_detail + + def create_volume(self, pool_id, name, size_unit, size, segment_size_kb, data_assurance_enabled): + volume_add_req = dict( + name=name, + poolId=pool_id, + sizeUnit=size_unit, + size=size, + segSize=segment_size_kb, + dataAssuranceEnabled=data_assurance_enabled, + ) + + self.debug("creating volume '%s'" % name) + try: + (rc, resp) = request(self.api_url + "/storage-systems/%s/volumes" % (self.ssid), + data=json.dumps(volume_add_req), headers=self._post_headers, method='POST', + url_username=self.api_usr, url_password=self.api_pwd, + validate_certs=self.validate_certs, + timeout=120) + except Exception: + err = get_exception() + self.module.fail_json( + msg="Failed to create volume. Volume [%s]. Array Id [%s]. Error[%s]." % (self.name, self.ssid, + str(err))) + + def create_thin_volume(self, pool_id, name, size_unit, size, thin_volume_repo_size, + thin_volume_max_repo_size, data_assurance_enabled): + thin_volume_add_req = dict( + name=name, + poolId=pool_id, + sizeUnit=size_unit, + virtualSize=size, + repositorySize=thin_volume_repo_size, + maximumRepositorySize=thin_volume_max_repo_size, + dataAssuranceEnabled=data_assurance_enabled, + ) + + self.debug("creating thin-volume '%s'" % name) + try: + (rc, resp) = request(self.api_url + "/storage-systems/%s/thin-volumes" % (self.ssid), + data=json.dumps(thin_volume_add_req), headers=self._post_headers, method='POST', + url_username=self.api_usr, url_password=self.api_pwd, + validate_certs=self.validate_certs, + timeout=120) + except Exception: + err = get_exception() + self.module.fail_json( + msg="Failed to create thin volume. Volume [%s]. Array Id [%s]. Error[%s]." % (self.name, + self.ssid, + str(err))) + + def delete_volume(self): + # delete the volume + self.debug("deleting volume '%s'" % self.volume_detail['name']) + try: + (rc, resp) = request( + self.api_url + "/storage-systems/%s/%s/%s" % (self.ssid, self.volume_resource_name, + self.volume_detail['id']), + method='DELETE', url_username=self.api_usr, url_password=self.api_pwd, + validate_certs=self.validate_certs, timeout=120) + except Exception: + err = get_exception() + self.module.fail_json( + msg="Failed to delete volume. Volume [%s]. Array Id [%s]. Error[%s]." % (self.name, self.ssid, + str(err))) + + @property + def volume_resource_name(self): + if self.volume_detail['thinProvisioned']: + return 'thin-volumes' + else: + return 'volumes' + + @property + def volume_properties_changed(self): + return self.volume_ssdcache_setting_changed # or with other props here when extended + + # TODO: add support for r/w cache settings, owning controller, scan settings, expansion policy, growth alert threshold + + @property + def volume_ssdcache_setting_changed(self): + # None means ignore existing setting + if self.ssd_cache_enabled is not None and self.ssd_cache_enabled != self.volume_detail['flashCached']: + self.debug("flash cache setting changed") + return True + + def update_volume_properties(self): + update_volume_req = dict() + + # conditionally add values so we ignore unspecified props + if self.volume_ssdcache_setting_changed: + update_volume_req['flashCache'] = self.ssd_cache_enabled + + self.debug("updating volume properties...") + try: + (rc, resp) = request( + self.api_url + "/storage-systems/%s/%s/%s/" % (self.ssid, self.volume_resource_name, + self.volume_detail['id']), + data=json.dumps(update_volume_req), headers=self._post_headers, method='POST', + url_username=self.api_usr, url_password=self.api_pwd, validate_certs=self.validate_certs, + timeout=120) + except Exception: + err = get_exception() + self.module.fail_json( + msg="Failed to update volume properties. Volume [%s]. Array Id [%s]. Error[%s]." % (self.name, + self.ssid, + str(err))) + + @property + def volume_needs_expansion(self): + current_size_bytes = int(self.volume_detail['capacity']) + requested_size_bytes = self.size * self._size_unit_map[self.size_unit] + + # TODO: check requested/current repo volume size for thin-volumes as well + + # TODO: do we need to build any kind of slop factor in here? + return requested_size_bytes > current_size_bytes + + def expand_volume(self): + is_thin = self.volume_detail['thinProvisioned'] + if is_thin: + # TODO: support manual repo expansion as well + self.debug('expanding thin volume') + thin_volume_expand_req = dict( + newVirtualSize=self.size, + sizeUnit=self.size_unit + ) + try: + (rc, resp) = request(self.api_url + "/storage-systems/%s/thin-volumes/%s/expand" % (self.ssid, + self.volume_detail[ + 'id']), + data=json.dumps(thin_volume_expand_req), headers=self._post_headers, method='POST', + url_username=self.api_usr, url_password=self.api_pwd, + validate_certs=self.validate_certs, timeout=120) + except Exception: + err = get_exception() + self.module.fail_json( + msg="Failed to expand thin volume. Volume [%s]. Array Id [%s]. Error[%s]." % (self.name, + self.ssid, + str(err))) + + # TODO: check return code + else: + self.debug('expanding volume') + volume_expand_req = dict( + expansionSize=self.size, + sizeUnit=self.size_unit + ) + try: + (rc, resp) = request( + self.api_url + "/storage-systems/%s/volumes/%s/expand" % (self.ssid, + self.volume_detail['id']), + data=json.dumps(volume_expand_req), headers=self._post_headers, method='POST', + url_username=self.api_usr, url_password=self.api_pwd, validate_certs=self.validate_certs, + timeout=120) + except Exception: + err = get_exception() + self.module.fail_json( + msg="Failed to expand volume. Volume [%s]. Array Id [%s]. Error[%s]." % (self.name, + self.ssid, + str(err))) + + self.debug('polling for completion...') + + while True: + try: + (rc, resp) = request(self.api_url + "/storage-systems/%s/volumes/%s/expand" % (self.ssid, + self.volume_detail[ + 'id']), + method='GET', url_username=self.api_usr, url_password=self.api_pwd, + validate_certs=self.validate_certs) + except Exception: + err = get_exception() + self.module.fail_json( + msg="Failed to get volume expansion progress. Volume [%s]. Array Id [%s]. Error[%s]." % ( + self.name, self.ssid, str(err))) + + action = resp['action'] + percent_complete = resp['percentComplete'] + + self.debug('expand action %s, %s complete...' % (action, percent_complete)) + + if action == 'none': + self.debug('expand complete') + break + else: + time.sleep(5) + + def apply(self): + changed = False + volume_exists = False + msg = None + + self.volume_detail = self.get_volume(self.name) + + if self.volume_detail: + volume_exists = True + + if self.state == 'absent': + self.debug("CHANGED: volume exists, but requested state is 'absent'") + changed = True + elif self.state == 'present': + # check requested volume size, see if expansion is necessary + if self.volume_needs_expansion: + self.debug( + "CHANGED: requested volume size %s%s is larger than current size %sb" % (self.size, + self.size_unit, + self.volume_detail[ + 'capacity'])) + changed = True + + if self.volume_properties_changed: + self.debug("CHANGED: one or more volume properties have changed") + changed = True + + else: + if self.state == 'present': + self.debug("CHANGED: volume does not exist, but requested state is 'present'") + changed = True + + if changed: + if self.module.check_mode: + self.debug('skipping changes due to check mode') + else: + if self.state == 'present': + if not volume_exists: + pool_detail = self.get_storage_pool(self.storage_pool_name) + + if not pool_detail: + self.module.fail_json(msg='Requested storage pool (%s) not found' % self.storage_pool_name) + + if self.thin_provision and not pool_detail['diskPool']: + self.module.fail_json( + msg='Thin provisioned volumes can only be located on disk pools (not volume groups)') + + pool_id = pool_detail['id'] + + if not self.thin_provision: + self.create_volume(pool_id, self.name, self.size_unit, self.size, self.segment_size_kb, + self.data_assurance_enabled) + msg = "Standard volume [%s] has been created." % (self.name) + + else: + self.create_thin_volume(pool_id, self.name, self.size_unit, self.size, + self.thin_volume_repo_size, self.thin_volume_max_repo_size, + self.data_assurance_enabled) + msg = "Thin volume [%s] has been created." % (self.name) + + else: # volume exists but differs, modify... + if self.volume_needs_expansion: + self.expand_volume() + msg = "Volume [%s] has been expanded." % (self.name) + + # this stuff always needs to run on present (since props can't be set on creation) + if self.volume_properties_changed: + self.update_volume_properties() + msg = "Properties of volume [%s] has been updated." % (self.name) + + elif self.state == 'absent': + self.delete_volume() + msg = "Volume [%s] has been deleted." % (self.name) + else: + self.debug("exiting with no changes") + if self.state == 'absent': + msg = "Volume [%s] did not exist." % (self.name) + else: + msg = "Volume [%s] already exists." % (self.name) + + self.module.exit_json(msg=msg, changed=changed) + + +def main(): + v = NetAppESeriesVolume() + + try: + v.apply() + except Exception: + e = get_exception() + v.debug("Exception in apply(): \n%s" % format_exc(e)) + v.module.fail_json(msg="Module failed. Error [%s]." % (str(e))) + + +if __name__ == '__main__': + main() diff --git a/storage/netapp/netapp_e_volume_copy.py b/storage/netapp/netapp_e_volume_copy.py new file mode 100644 index 00000000000..f715c84088f --- /dev/null +++ b/storage/netapp/netapp_e_volume_copy.py @@ -0,0 +1,439 @@ +#!/usr/bin/python + +# (c) 2016, NetApp, Inc +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +DOCUMENTATION = """ +--- +module: netapp_e_volume_copy +short_description: Create volume copy pairs +description: + - Create and delete snapshots images on volume groups for NetApp E-series storage arrays. +version_added: '2.2' +author: Kevin Hulquest (@hulquest) +options: + api_username: + required: true + description: + - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_password: + required: true + description: + - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API. + api_url: + required: true + description: + - The url to the SANtricity WebServices Proxy or embedded REST API. + example: + - https://prod-1.wahoo.acme.com/devmgr/v2 + validate_certs: + required: false + default: true + description: + - Should https certificates be validated? + source_volume_id: + description: + - The the id of the volume copy source. + - If used, must be paired with destination_volume_id + - Mutually exclusive with volume_copy_pair_id, and search_volume_id + destination_volume_id: + description: + - The the id of the volume copy destination. + - If used, must be paired with source_volume_id + - Mutually exclusive with volume_copy_pair_id, and search_volume_id + volume_copy_pair_id: + description: + - The the id of a given volume copy pair + - Mutually exclusive with destination_volume_id, source_volume_id, and search_volume_id + - Can use to delete or check presence of volume pairs + - Must specify this or (destination_volume_id and source_volume_id) + state: + description: + - Whether the specified volume copy pair should exist or not. + required: True + choices: ['present', 'absent'] + create_copy_pair_if_does_not_exist: + description: + - Defines if a copy pair will be created if it does not exist. + - If set to True destination_volume_id and source_volume_id are required. + choices: [True, False] + default: True + start_stop_copy: + description: + - starts a re-copy or stops a copy in progress + - "Note: If you stop the initial file copy before it it done the copy pair will be destroyed" + - Requires volume_copy_pair_id + search_volume_id: + description: + - Searches for all valid potential target and source volumes that could be used in a copy_pair + - Mutually exclusive with volume_copy_pair_id, destination_volume_id and source_volume_id +""" +RESULTS = """ +""" +EXAMPLES = """ +--- +msg: + description: Success message + returned: success + type: string + sample: Json facts for the volume copy that was created. +""" +RETURN = """ +msg: + description: Success message + returned: success + type: string + sample: Created Volume Copy Pair with ID +""" + +import json + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import open_url +from ansible.module_utils.six.moves.urllib.error import HTTPError + +HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json", +} + + +def request(url, data=None, headers=None, method='GET', use_proxy=True, + force=False, last_mod_time=None, timeout=10, validate_certs=True, + url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False): + try: + r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy, + force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs, + url_username=url_username, url_password=url_password, http_agent=http_agent, + force_basic_auth=force_basic_auth) + except HTTPError: + err = get_exception() + r = err.fp + + try: + raw_data = r.read() + if raw_data: + data = json.loads(raw_data) + else: + raw_data = None + except: + if ignore_errors: + pass + else: + raise Exception(raw_data) + + resp_code = r.getcode() + + if resp_code >= 400 and not ignore_errors: + raise Exception(resp_code, data) + else: + return resp_code, data + + +def find_volume_copy_pair_id_from_source_volume_id_and_destination_volume_id(params): + get_status = 'storage-systems/%s/volume-copy-jobs' % params['ssid'] + url = params['api_url'] + get_status + + (rc, resp) = request(url, method='GET', url_username=params['api_username'], + url_password=params['api_password'], headers=HEADERS, + validate_certs=params['validate_certs']) + + volume_copy_pair_id = None + for potential_copy_pair in resp: + if potential_copy_pair['sourceVolume'] == params['source_volume_id']: + if potential_copy_pair['sourceVolume'] == params['source_volume_id']: + volume_copy_pair_id = potential_copy_pair['id'] + + return volume_copy_pair_id + + +def create_copy_pair(params): + get_status = 'storage-systems/%s/volume-copy-jobs' % params['ssid'] + url = params['api_url'] + get_status + + rData = { + "sourceId": params['source_volume_id'], + "targetId": params['destination_volume_id'] + } + + (rc, resp) = request(url, data=json.dumps(rData), ignore_errors=True, method='POST', + url_username=params['api_username'], url_password=params['api_password'], headers=HEADERS, + validate_certs=params['validate_certs']) + if rc != 200: + return False, (rc, resp) + else: + return True, (rc, resp) + + +def delete_copy_pair_by_copy_pair_id(params): + get_status = 'storage-systems/%s/volume-copy-jobs/%s?retainRepositories=false' % ( + params['ssid'], params['volume_copy_pair_id']) + url = params['api_url'] + get_status + + (rc, resp) = request(url, ignore_errors=True, method='DELETE', + url_username=params['api_username'], url_password=params['api_password'], headers=HEADERS, + validate_certs=params['validate_certs']) + if rc != 204: + return False, (rc, resp) + else: + return True, (rc, resp) + + +def find_volume_copy_pair_id_by_volume_copy_pair_id(params): + get_status = 'storage-systems/%s/volume-copy-jobs/%s?retainRepositories=false' % ( + params['ssid'], params['volume_copy_pair_id']) + url = params['api_url'] + get_status + + (rc, resp) = request(url, ignore_errors=True, method='DELETE', + url_username=params['api_username'], url_password=params['api_password'], headers=HEADERS, + validate_certs=params['validate_certs']) + if rc != 200: + return False, (rc, resp) + else: + return True, (rc, resp) + + +def start_stop_copy(params): + get_status = 'storage-systems/%s/volume-copy-jobs-control/%s?control=%s' % ( + params['ssid'], params['volume_copy_pair_id'], params['start_stop_copy']) + url = params['api_url'] + get_status + + (response_code, response_data) = request(url, ignore_errors=True, method='POST', + url_username=params['api_username'], url_password=params['api_password'], + headers=HEADERS, + validate_certs=params['validate_certs']) + + if response_code == 200: + return True, response_data[0]['percentComplete'] + else: + return False, response_data + + +def check_copy_status(params): + get_status = 'storage-systems/%s/volume-copy-jobs-control/%s' % ( + params['ssid'], params['volume_copy_pair_id']) + url = params['api_url'] + get_status + + (response_code, response_data) = request(url, ignore_errors=True, method='GET', + url_username=params['api_username'], url_password=params['api_password'], + headers=HEADERS, + validate_certs=params['validate_certs']) + + if response_code == 200: + if response_data['percentComplete'] != -1: + + return True, response_data['percentComplete'] + else: + return False, response_data['percentComplete'] + else: + return False, response_data + + +def find_valid_copy_pair_targets_and_sources(params): + get_status = 'storage-systems/%s/volumes' % params['ssid'] + url = params['api_url'] + get_status + + (response_code, response_data) = request(url, ignore_errors=True, method='GET', + url_username=params['api_username'], url_password=params['api_password'], + headers=HEADERS, + validate_certs=params['validate_certs']) + + if response_code == 200: + source_capacity = None + candidates = [] + for volume in response_data: + if volume['id'] == params['search_volume_id']: + source_capacity = volume['capacity'] + else: + candidates.append(volume) + + potential_sources = [] + potential_targets = [] + + for volume in candidates: + if volume['capacity'] > source_capacity: + if volume['volumeCopyTarget'] is False: + if volume['volumeCopySource'] is False: + potential_targets.append(volume['id']) + else: + if volume['volumeCopyTarget'] is False: + if volume['volumeCopySource'] is False: + potential_sources.append(volume['id']) + + return potential_targets, potential_sources + + else: + raise Exception("Response [%s]" % response_code) + + +def main(): + module = AnsibleModule(argument_spec=dict( + source_volume_id=dict(type='str'), + destination_volume_id=dict(type='str'), + copy_priority=dict(required=False, default=0, type='int'), + ssid=dict(required=True, type='str'), + api_url=dict(required=True), + api_username=dict(required=False), + api_password=dict(required=False, no_log=True), + validate_certs=dict(required=False, default=True), + targetWriteProtected=dict(required=False, default=True, type='bool'), + onlineCopy=dict(required=False, default=False, type='bool'), + volume_copy_pair_id=dict(type='str'), + status=dict(required=True, choices=['present', 'absent'], type='str'), + create_copy_pair_if_does_not_exist=dict(required=False, default=True, type='bool'), + start_stop_copy=dict(required=False, choices=['start', 'stop'], type='str'), + search_volume_id=dict(type='str'), + ), + mutually_exclusive=[['volume_copy_pair_id', 'destination_volume_id'], + ['volume_copy_pair_id', 'source_volume_id'], + ['volume_copy_pair_id', 'search_volume_id'], + ['search_volume_id', 'destination_volume_id'], + ['search_volume_id', 'source_volume_id'], + ], + required_together=[['source_volume_id', 'destination_volume_id'], + ], + required_if=[["create_copy_pair_if_does_not_exist", True, ['source_volume_id', 'destination_volume_id'], ], + ["start_stop_copy", 'stop', ['volume_copy_pair_id'], ], + ["start_stop_copy", 'start', ['volume_copy_pair_id'], ], + ] + + ) + params = module.params + + if not params['api_url'].endswith('/'): + params['api_url'] += '/' + + # Check if we want to search + if params['search_volume_id'] is not None: + try: + potential_targets, potential_sources = find_valid_copy_pair_targets_and_sources(params) + except: + e = get_exception() + module.fail_json(msg="Failed to find valid copy pair candidates. Error [%s]" % str(e)) + + module.exit_json(changed=False, + msg=' Valid source devices found: %s Valid target devices found: %s' % (len(potential_sources), len(potential_targets)), + search_volume_id=params['search_volume_id'], + valid_targets=potential_targets, + valid_sources=potential_sources) + + # Check if we want to start or stop a copy operation + if params['start_stop_copy'] == 'start' or params['start_stop_copy'] == 'stop': + + # Get the current status info + currenty_running, status_info = check_copy_status(params) + + # If we want to start + if params['start_stop_copy'] == 'start': + + # If we have already started + if currenty_running is True: + module.exit_json(changed=False, msg='Volume Copy Pair copy has started.', + volume_copy_pair_id=params['volume_copy_pair_id'], percent_done=status_info) + # If we need to start + else: + + start_status, info = start_stop_copy(params) + + if start_status is True: + module.exit_json(changed=True, msg='Volume Copy Pair copy has started.', + volume_copy_pair_id=params['volume_copy_pair_id'], percent_done=info) + else: + module.fail_json(msg="Could not start volume copy pair Error: %s" % info) + + # If we want to stop + else: + # If it has already stopped + if currenty_running is False: + module.exit_json(changed=False, msg='Volume Copy Pair copy is stopped.', + volume_copy_pair_id=params['volume_copy_pair_id']) + + # If we need to stop it + else: + start_status, info = start_stop_copy(params) + + if start_status is True: + module.exit_json(changed=True, msg='Volume Copy Pair copy has been stopped.', + volume_copy_pair_id=params['volume_copy_pair_id']) + else: + module.fail_json(msg="Could not stop volume copy pair Error: %s" % info) + + # If we want the copy pair to exist we do this stuff + if params['status'] == 'present': + + # We need to check if it exists first + if params['volume_copy_pair_id'] is None: + params['volume_copy_pair_id'] = find_volume_copy_pair_id_from_source_volume_id_and_destination_volume_id( + params) + + # If no volume copy pair is found we need need to make it. + if params['volume_copy_pair_id'] is None: + + # In order to create we can not do so with just a volume_copy_pair_id + + copy_began_status, (rc, resp) = create_copy_pair(params) + + if copy_began_status is True: + module.exit_json(changed=True, msg='Created Volume Copy Pair with ID: %s' % resp['id']) + else: + module.fail_json(msg="Could not create volume copy pair Code: %s Error: %s" % (rc, resp)) + + # If it does exist we do nothing + else: + # We verify that it exists + exist_status, (exist_status_code, exist_status_data) = find_volume_copy_pair_id_by_volume_copy_pair_id( + params) + + if exist_status: + module.exit_json(changed=False, + msg=' Volume Copy Pair with ID: %s exists' % params['volume_copy_pair_id']) + else: + if exist_status_code == 404: + module.fail_json( + msg=' Volume Copy Pair with ID: %s does not exist. Can not create without source_volume_id and destination_volume_id' % + params['volume_copy_pair_id']) + else: + module.fail_json(msg="Could not find volume copy pair Code: %s Error: %s" % ( + exist_status_code, exist_status_data)) + + module.fail_json(msg="Done") + + # If we want it to not exist we do this + else: + + if params['volume_copy_pair_id'] is None: + params['volume_copy_pair_id'] = find_volume_copy_pair_id_from_source_volume_id_and_destination_volume_id( + params) + + # We delete it by the volume_copy_pair_id + delete_status, (delete_status_code, delete_status_data) = delete_copy_pair_by_copy_pair_id(params) + + if delete_status is True: + module.exit_json(changed=True, + msg=' Volume Copy Pair with ID: %s was deleted' % params['volume_copy_pair_id']) + else: + if delete_status_code == 404: + module.exit_json(changed=False, + msg=' Volume Copy Pair with ID: %s does not exist' % params['volume_copy_pair_id']) + else: + module.fail_json(msg="Could not delete volume copy pair Code: %s Error: %s" % ( + delete_status_code, delete_status_data)) + + +if __name__ == '__main__': + main() From 9b5c64e240da8df378d9c8ef7947a3bda0fb2f23 Mon Sep 17 00:00:00 2001 From: Jens Carl Date: Fri, 16 Sep 2016 04:19:13 -0700 Subject: [PATCH 2108/2522] New ansible module for aws Redshift and Redshift subnet group (#185) --- cloud/amazon/redshift.py | 502 ++++++++++++++++++++++++++ cloud/amazon/redshift_subnet_group.py | 182 ++++++++++ 2 files changed, 684 insertions(+) create mode 100644 cloud/amazon/redshift.py create mode 100644 cloud/amazon/redshift_subnet_group.py diff --git a/cloud/amazon/redshift.py b/cloud/amazon/redshift.py new file mode 100644 index 00000000000..b6c6fdd35d5 --- /dev/null +++ b/cloud/amazon/redshift.py @@ -0,0 +1,502 @@ +#!/usr/bin/python + +# Copyright 2014 Jens Carl, Hothead Games Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +DOCUMENTATION = ''' +--- +author: + - "Jens Carl (@j-carl), Hothead Games Inc." +module: redshift +version_added: "2.1" +short_description: create, delete, or modify an Amazon Redshift instance +description: + - Creates, deletes, or modifies amazon Redshift cluster instances. +options: + command: + description: + - Specifies the action to take. + required: true + choices: [ 'create', 'facts', 'delete', 'modify' ] + identifier: + description: + - Redshift cluster identifier. + required: true + node_type: + description: + - The node type of the cluster. Must be specified when command=create. + required: false + choices: ['dw1.xlarge', 'dw1.8xlarge', 'dw2.large', 'dw2.8xlarge', ] + username: + description: + - Master database username. Used only when command=create. + required: false + password: + description: + - Master database password. Used only when command=create. + required: false + cluster_type: + description: + - The type of cluster. + required: false + choices: ['multi-node', 'single-node' ] + default: 'single-node' + db_name: + description: + - Name of the database. + required: false + default: null + availability_zone: + description: + - availability zone in which to launch cluster + required: false + aliases: ['zone', 'aws_zone'] + number_of_nodes: + description: + - Number of nodes. Only used when cluster_type=multi-node. + required: false + default: null + cluster_subnet_group_name: + description: + - which subnet to place the cluster + required: false + aliases: ['subnet'] + cluster_security_groups: + description: + - in which security group the cluster belongs + required: false + default: null + aliases: ['security_groups'] + vpc_security_group_ids: + description: + - VPC security group + required: false + aliases: ['vpc_security_groups'] + default: null + preferred_maintenance_window: + description: + - maintenance window + required: false + aliases: ['maintance_window', 'maint_window'] + default: null + cluster_parameter_group_name: + description: + - name of the cluster parameter group + required: false + aliases: ['param_group_name'] + default: null + automated_snapshot_retention_period: + description: + - period when the snapshot take place + required: false + aliases: ['retention_period'] + default: null + port: + description: + - which port the cluster is listining + required: false + default: null + cluster_version: + description: + - which version the cluster should have + required: false + aliases: ['version'] + choices: ['1.0'] + default: null + allow_version_upgrade: + description: + - flag to determinate if upgrade of version is possible + required: false + aliases: ['version_upgrade'] + default: null + number_of_nodes: + description: + - number of the nodes the cluster should run + required: false + default: null + publicly_accessible: + description: + - if the cluster is accessible publicly or not + required: false + default: null + encrypted: + description: + - if the cluster is encrypted or not + required: false + default: null + elastic_ip: + description: + - if the cluster has an elastic IP or not + required: false + default: null + new_cluster_identifier: + description: + - Only used when command=modify. + required: false + aliases: ['new_identifier'] + default: null + wait: + description: + - When command=create, modify or restore then wait for the database to enter the 'available' state. When command=delete wait for the database to be terminated. + required: false + default: "no" + choices: [ "yes", "no" ] + wait_timeout: + description: + - how long before wait gives up, in seconds + default: 300 +requirements: [ 'boto' ] +extends_documentation_fragment: aws +''' + +EXAMPLES = ''' +# Basic cluster provisioning example +- redshift: > + command=create + node_type=dw1.xlarge + identifier=new_cluster + username=cluster_admin + password=1nsecure +''' + +RETURN = ''' +cluster: + description: dictionary containing all the cluster information + returned: success + type: dictionary + contains: + identifier: + description: Id of the cluster. + returned: success + type: string + sample: "new_redshift_cluster" + create_time: + description: Time of the cluster creation as timestamp. + returned: success + type: float + sample: 1430158536.308 + status: + description: Stutus of the cluster. + returned: success + type: string + sample: "available" + db_name: + description: Name of the database. + returned: success + type: string + sample: "new_db_name" + availability_zone: + description: Amazon availability zone where the cluster is located. + returned: success + type: string + sample: "us-east-1b" + maintenance_window: + description: Time frame when maintenance/upgrade are done. + returned: success + type: string + sample: "sun:09:30-sun:10:00" + private_ip_address: + description: Private IP address of the main node. + returned: success + type: string + sample: "10.10.10.10" + public_ip_address: + description: Public IP address of the main node. + returned: success + type: string + sample: "0.0.0.0" + port: + description: Port of the cluster. + returned: success + type: int + sample: 5439 + url: + description: FQDN of the main cluster node. + returned: success + type: string + sample: "new-redshift_cluster.jfkdjfdkj.us-east-1.redshift.amazonaws.com" +''' + +import time + +try: + import boto + from boto import redshift + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + + +def _collect_facts(resource): + """Transfrom cluster information to dict.""" + facts = { + 'identifier' : resource['ClusterIdentifier'], + 'create_time' : resource['ClusterCreateTime'], + 'status' : resource['ClusterStatus'], + 'username' : resource['MasterUsername'], + 'db_name' : resource['DBName'], + 'availability_zone' : resource['AvailabilityZone'], + 'maintenance_window': resource['PreferredMaintenanceWindow'], + } + + for node in resource['ClusterNodes']: + if node['NodeRole'] in ('SHARED', 'LEADER'): + facts['private_ip_address'] = node['PrivateIPAddress'] + break + + return facts + + +def create_cluster(module, redshift): + """ + Create a new cluster + + module: AnsibleModule object + redshift: authenticated redshift connection object + + Returns: + """ + + identifier = module.params.get('identifier') + node_type = module.params.get('node_type') + username = module.params.get('username') + password = module.params.get('password') + wait = module.params.get('wait') + wait_timeout = module.params.get('wait_timeout') + + changed = True + # Package up the optional parameters + params = {} + for p in ('db_name', 'cluster_type', 'cluster_security_groups', + 'vpc_security_group_ids', 'cluster_subnet_group_name', + 'availability_zone', 'preferred_maintenance_window', + 'cluster_parameter_group_name', + 'automated_snapshot_retention_period', 'port', + 'cluster_version', 'allow_version_upgrade', + 'number_of_nodes', 'publicly_accessible', + 'encrypted', 'elastic_ip'): + if module.params.get( p ): + params[ p ] = module.params.get( p ) + + try: + redshift.describe_clusters(identifier)['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] + changed = False + except boto.exception.JSONResponseError, e: + try: + redshift.create_cluster(identifier, node_type, username, password, **params) + except boto.exception.JSONResponseError, e: + module.fail_json(msg=str(e)) + + try: + resource = redshift.describe_clusters(identifier)['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] + except boto.exception.JSONResponseError, e: + module.fail_json(msg=str(e)) + + if wait: + try: + wait_timeout = time.time() + wait_timeout + time.sleep(5) + + while wait_timeout > time.time() and resource['ClusterStatus'] != 'available': + time.sleep(5) + if wait_timeout <= time.time(): + module.fail_json(msg = "Timeout waiting for resource %s" % resource.id) + + resource = redshift.describe_clusters(identifier)['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] + + except boto.exception.JSONResponseError, e: + module.fail_json(msg=str(e)) + + return(changed, _collect_facts(resource)) + + +def describe_cluster(module, redshift): + """ + Collect data about the cluster. + + module: Ansible module object + redshift: authenticated redshift connection object + """ + identifier = module.params.get('identifier') + + try: + resource = redshift.describe_clusters(identifier)['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] + except boto.exception.JSONResponseError, e: + module.fail_json(msg=str(e)) + + return(True, _collect_facts(resource)) + + +def delete_cluster(module, redshift): + """ + Delete a cluster. + + module: Ansible module object + redshift: authenticated redshift connection object + """ + + identifier = module.params.get('identifier') + wait = module.params.get('wait') + wait_timeout = module.params.get('wait_timeout') + + try: + redshift.delete_custer( identifier ) + except boto.exception.JSONResponseError, e: + module.fail_json(msg=str(e)) + + if wait: + try: + wait_timeout = time.time() + wait_timeout + resource = redshift.describe_clusters(identifier)['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] + + while wait_timeout > time.time() and resource['ClusterStatus'] != 'deleting': + time.sleep(5) + if wait_timeout <= time.time(): + module.fail_json(msg = "Timeout waiting for resource %s" % resource.id) + + resource = redshift.describe_clusters(identifier)['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] + + except boto.exception.JSONResponseError, e: + module.fail_json(msg=str(e)) + + return(True, {}) + + +def modify_cluster(module, redshift): + """ + Modify an existing cluster. + + module: Ansible module object + redshift: authenticated redshift connection object + """ + + identifier = module.params.get('identifier') + wait = module.params.get('wait') + wait_timeout = module.params.get('wait_timeout') + + # Package up the optional parameters + params = {} + for p in ('cluster_type', 'cluster_security_groups', + 'vpc_security_group_ids', 'cluster_subnet_group_name', + 'availability_zone', 'preferred_maintenance_window', + 'cluster_parameter_group_name', + 'automated_snapshot_retention_period', 'port', 'cluster_version', + 'allow_version_upgrade', 'number_of_nodes', 'new_cluster_identifier'): + if module.params.get(p): + params[p] = module.params.get(p) + + try: + redshift.describe_clusters(identifier)['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] + changed = False + except boto.exception.JSONResponseError, e: + try: + redshift.modify_cluster(identifier, **params) + except boto.exception.JSONResponseError, e: + module.fail_json(msg=str(e)) + + try: + resource = redshift.describe_clusters(identifier)['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] + except boto.exception.JSONResponseError, e: + module.fail_json(msg=str(e)) + + if wait: + try: + wait_timeout = time.time() + wait_timeout + time.sleep(5) + + while wait_timeout > time.time() and resource['ClusterStatus'] != 'available': + time.sleep(5) + if wait_timeout <= time.time(): + module.fail_json(msg = "Timeout waiting for resource %s" % resource.id) + + resource = redshift.describe_clusters(identifier)['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] + + except boto.exception.JSONResponseError, e: + # https://github.com/boto/boto/issues/2776 is fixed. + module.fail_json(msg=str(e)) + + return(True, _collect_facts(resource)) + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + command = dict(choices=['create', 'facts', 'delete', 'modify'], required=True), + identifier = dict(required=True), + node_type = dict(choices=['dw1.xlarge', 'dw1.8xlarge', 'dw2.large', 'dw2.8xlarge', ], required=False), + username = dict(required=False), + password = dict(no_log=True, required=False), + db_name = dict(require=False), + cluster_type = dict(choices=['multi-node', 'single-node', ], default='single-node'), + cluster_security_groups = dict(aliases=['security_groups'], type='list'), + vpc_security_group_ids = dict(aliases=['vpc_security_groups'], type='list'), + cluster_subnet_group_name = dict(aliases=['subnet']), + availability_zone = dict(aliases=['aws_zone', 'zone']), + preferred_maintenance_window = dict(aliases=['maintance_window', 'maint_window']), + cluster_parameter_group_name = dict(aliases=['param_group_name']), + automated_snapshot_retention_period = dict(aliases=['retention_period']), + port = dict(type='int'), + cluster_version = dict(aliases=['version'], choices=['1.0']), + allow_version_upgrade = dict(aliases=['version_upgrade'], type='bool'), + number_of_nodes = dict(type='int'), + publicly_accessible = dict(type='bool'), + encrypted = dict(type='bool'), + elastic_ip = dict(required=False), + new_cluster_identifier = dict(aliases=['new_identifier']), + wait = dict(type='bool', default=False), + wait_timeout = dict(default=300), + ) + ) + + module = AnsibleModule( + argument_spec=argument_spec, + ) + + if not HAS_BOTO: + module.fail_json(msg='boto v2.9.0+ required for this module') + + command = module.params.get('command') + + region, ec2_url, aws_connect_params = get_aws_connection_info(module) + if not region: + module.fail_json(msg=str("region not specified and unable to determine region from EC2_REGION.")) + + # connect to the rds endpoint + try: + conn = connect_to_aws(boto.redshift, region, **aws_connect_params) + except boto.exception.JSONResponseError, e: + module.fail_json(msg=str(e)) + + changed = True + if command == 'create': + (changed, cluster) = create_cluster(module, conn) + + elif command == 'facts': + (changed, cluster) = describe_cluster(module, conn) + + elif command == 'delete': + (changed, cluster) = delete_cluster(module, conn) + + elif command == 'modify': + (changed, cluster) = modify_cluster(module, conn) + + module.exit_json(changed=changed, cluster=cluster) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() diff --git a/cloud/amazon/redshift_subnet_group.py b/cloud/amazon/redshift_subnet_group.py new file mode 100644 index 00000000000..acd3330c1f2 --- /dev/null +++ b/cloud/amazon/redshift_subnet_group.py @@ -0,0 +1,182 @@ +#!/usr/bin/python + +# Copyright 2014 Jens Carl, Hothead Games Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +DOCUMENTATION = ''' +--- +author: + - "Jens Carl (@j-carl), Hothead Games Inc." +module: redshift_subnet_group +version_added: "2.1" +short_description: mange Redshift cluster subnet groups +description: + - Create, modifies, and deletes Redshift cluster subnet groups. +options: + state: + description: + - Specifies whether the subnet should be present or absent. + default: 'present' + choices: ['present', 'absent' ] + group_name: + description: + - Cluster subnet group name. + required: true + aliases: ['name'] + group_description: + description: + - Database subnet group description. + required: false + default: null + aliases: ['description'] + group_subnets: + description: + - List of subnet IDs that make up the cluster subnet group. + required: false + default: null + aliases: ['subnets'] +requirements: [ 'boto' ] +extends_documentation_fragment: aws +''' + +EXAMPLES = ''' +# Create a Redshift subnet group +- local_action: + module: redshift_subnet_group + state: present + group_name: redshift-subnet + group_description: Redshift subnet + group_subnets: + - 'subnet-aaaaa' + - 'subnet-bbbbb' + +# Remove subnet group +redshift_subnet_group: > + state: absent + group_name: redshift-subnet +''' + +RETURN = ''' +group: + description: dictionary containing all Redshift subnet group information + returned: success + type: dictionary + contains: + name: + description: name of the Redshift subnet group + returned: success + type: string + sample: "redshift_subnet_group_name" + vpc_id: + description: Id of the VPC where the subnet is located + returned: success + type: stering + sample: "vpc-aabb1122" +''' + +try: + import boto + import boto.redshift + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + state=dict(required=True, choices=['present', 'absent']), + group_name=dict(required=True, aliases=['name']), + group_description=dict(required=False, aliases=['description']), + group_subnets=dict(required=False, aliases=['subnets'], type='list'), + )) + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO: + module.fail_json(msg='boto v2.9.0+ required for this module') + + state = module.params.get('state') + group_name = module.params.get('group_name') + group_description = module.params.get('group_description') + group_subnets = module.params.get('group_subnets') + + if state == 'present': + for required in ('group_name', 'group_description', 'group_subnets'): + if not module.params.get(required): + module.fail_json(msg=str("parameter %s required for state='present'" % required)) + else: + for not_allowed in ('group_description', 'group_subnets'): + if module.params.get(not_allowed): + module.fail_json(msg=str("parameter %s not allowed for state='absent'" % not_allowed)) + + region, ec2_url, aws_connect_params = get_aws_connection_info(module) + if not region: + module.fail_json(msg=str("region not specified and unable to determine region from EC2_REGION.")) + + # Connect to the Redshift endpoint. + try: + conn = connect_to_aws(boto.redshift, region, **aws_connect_params) + except boto.exception.JSONResponseError, e: + module.fail_json(msg=str(e)) + + try: + changed = False + exists = False + group = None + + try: + matching_groups = conn.describe_cluster_subnet_groups(group_name, max_records=100) + exists = len(matching_groups) > 0 + except boto.exception.JSONResponseError, e: + if e.body['Error']['Code'] != 'ClusterSubnetGroupNotFoundFault': + #if e.code != 'ClusterSubnetGroupNotFoundFault': + module.fail_json(msg=str(e)) + + if state == 'absent': + if exists: + conn.delete_cluster_subnet_group(group_name) + changed = True + + else: + if not exists: + new_group = conn.create_cluster_subnet_group(group_name, group_description, group_subnets) + group = { + 'name': new_group['CreateClusterSubnetGroupResponse']['CreateClusterSubnetGroupResult'] + ['ClusterSubnetGroup']['ClusterSubnetGroupName'], + 'vpc_id': new_group['CreateClusterSubnetGroupResponse']['CreateClusterSubnetGroupResult'] + ['ClusterSubnetGroup']['VpcId'], + } + else: + changed_group = conn.modify_cluster_subnet_group(group_name, group_subnets, description=group_description) + group = { + 'name': changed_group['ModifyClusterSubnetGroupResponse']['ModifyClusterSubnetGroupResult'] + ['ClusterSubnetGroup']['ClusterSubnetGroupName'], + 'vpc_id': changed_group['ModifyClusterSubnetGroupResponse']['ModifyClusterSubnetGroupResult'] + ['ClusterSubnetGroup']['VpcId'], + } + + changed = True + + except boto.exception.JSONResponseError, e: + module.fail_json(msg=str(e)) + + module.exit_json(changed=changed, group=group) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From 48f079f0f2e1570c817381e8bfd5d822db670aa6 Mon Sep 17 00:00:00 2001 From: Jim Dalton Date: Fri, 16 Sep 2016 21:34:37 +0200 Subject: [PATCH 2109/2522] Add module for managing CloudWatch Event rules and targets (#2101) --- cloud/amazon/cloudwatchevent_rule.py | 409 +++++++++++++++++++++++++++ 1 file changed, 409 insertions(+) create mode 100644 cloud/amazon/cloudwatchevent_rule.py diff --git a/cloud/amazon/cloudwatchevent_rule.py b/cloud/amazon/cloudwatchevent_rule.py new file mode 100644 index 00000000000..a21800c1936 --- /dev/null +++ b/cloud/amazon/cloudwatchevent_rule.py @@ -0,0 +1,409 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: cloudwatchevent_rule +short_description: Manage CloudWatch Event rules and targets +description: + - This module creates and manages CloudWatch event rules and targets. +version_added: "2.2" +extends_documentation_fragment: + - aws +author: "Jim Dalton (@jsdalton) " +requirements: + - python >= 2.6 + - boto3 +notes: + - A rule must contain at least an I(event_pattern) or I(schedule_expression). A + rule can have both an I(event_pattern) and a I(schedule_expression), in which + case the rule will trigger on matching events as well as on a schedule. + - When specifying targets, I(input) and I(input_path) are mutually-exclusive + and optional parameters. +options: + name: + description: + - The name of the rule you are creating, updating or deleting. No spaces + or special characters allowed (i.e. must match C([\.\-_A-Za-z0-9]+)) + required: true + schedule_expression: + description: + - A cron or rate expression that defines the schedule the rule will + trigger on. For example, C(cron(0 20 * * ? *)), C(rate(5 minutes)) + required: false + event_pattern: + description: + - A string pattern (in valid JSON format) that is used to match against + incoming events to determine if the rule should be triggered + required: false + state: + description: + - Whether the rule is present (and enabled), disabled, or absent + choices: ["present", "disabled", "absent"] + default: present + required: false + description: + description: + - A description of the rule + required: false + role_arn: + description: + - The Amazon Resource Name (ARN) of the IAM role associated with the rule + required: false + targets: + description: + - "A dictionary array of targets to add to or update for the rule, in the + form C({ id: [string], arn: [string], input: [valid JSON string], input_path: [valid JSONPath string] }). + I(id) [required] is the unique target assignment ID. I(arn) (required) + is the Amazon Resource Name associated with the target. I(input) + (optional) is a JSON object that will override the event data when + passed to the target. I(input_path) (optional) is a JSONPath string + (e.g. C($.detail)) that specifies the part of the event data to be + passed to the target. If neither I(input) nor I(input_path) is + specified, then the entire event is passed to the target in JSON form." + required: false +''' + +EXAMPLES = ''' +- cloudwatchevent_rule: + name: MyCronTask + schedule_expression: "cron(0 20 * * ? *)" + description: Run my scheduled task + targets: + - id: MyTargetId + arn: arn:aws:lambda:us-east-1:123456789012:function:MyFunction + +- cloudwatchevent_rule: + name: MyDisabledCronTask + schedule_expression: "cron(5 minutes)" + description: Run my disabled scheduled task + state: disabled + targets: + - id: MyOtherTargetId + arn: arn:aws:lambda:us-east-1:123456789012:function:MyFunction + input: '{"foo": "bar"}' + +- cloudwatchevent_rule: name=MyCronTask state=absent +''' + +RETURN = ''' +rule: + description: CloudWatch Event rule data + returned: success + type: dict + sample: "{ 'arn': 'arn:aws:events:us-east-1:123456789012:rule/MyCronTask', 'description': 'Run my scheduled task', 'name': 'MyCronTask', 'schedule_expression': 'cron(0 20 * * ? *)', 'state': 'ENABLED' }" +targets: + description: CloudWatch Event target(s) assigned to the rule + returned: success + type: list + sample: "[{ 'arn': 'arn:aws:lambda:us-east-1:123456789012:function:MyFunction', 'id': 'MyTargetId' }]" +''' + + +class CloudWatchEventRule(object): + def __init__(self, module, name, client, schedule_expression=None, + event_pattern=None, description=None, role_arn=None): + self.name = name + self.client = client + self.changed = False + self.schedule_expression = schedule_expression + self.event_pattern = event_pattern + self.description = description + self.role_arn = role_arn + + def describe(self): + """Returns the existing details of the rule in AWS""" + try: + rule_info = self.client.describe_rule(Name=self.name) + except botocore.exceptions.ClientError, e: + error_code = e.response.get('Error', {}).get('Code') + if error_code == 'ResourceNotFoundException': + return {} + raise + return self._snakify(rule_info) + + def put(self, enabled=True): + """Creates or updates the rule in AWS""" + request = { + 'Name': self.name, + 'State': "ENABLED" if enabled else "DISABLED", + } + if self.schedule_expression: + request['ScheduleExpression'] = self.schedule_expression + if self.event_pattern: + request['EventPattern'] = self.event_pattern + if self.description: + request['Description'] = self.description + if self.role_arn: + request['RoleArn'] = self.role_arn + response = self.client.put_rule(**request) + self.changed = True + return response + + def delete(self): + """Deletes the rule in AWS""" + self.remove_all_targets() + response = self.client.delete_rule(Name=self.name) + self.changed = True + return response + + def enable(self): + """Enables the rule in AWS""" + response = self.client.enable_rule(Name=self.name) + self.changed = True + return response + + def disable(self): + """Disables the rule in AWS""" + response = self.client.disable_rule(Name=self.name) + self.changed = True + return response + + def list_targets(self): + """Lists the existing targets for the rule in AWS""" + try: + targets = self.client.list_targets_by_rule(Rule=self.name) + except botocore.exceptions.ClientError, e: + error_code = e.response.get('Error', {}).get('Code') + if error_code == 'ResourceNotFoundException': + return [] + raise + return self._snakify(targets)['targets'] + + def put_targets(self, targets): + """Creates or updates the provided targets on the rule in AWS""" + if not targets: + return + request = { + 'Rule': self.name, + 'Targets': self._targets_request(targets), + } + response = self.client.put_targets(**request) + self.changed = True + return response + + def remove_targets(self, target_ids): + """Removes the provided targets from the rule in AWS""" + if not target_ids: + return + request = { + 'Rule': self.name, + 'Ids': target_ids + } + response = self.client.remove_targets(**request) + self.changed = True + return response + + def remove_all_targets(self): + """Removes all targets on rule""" + targets = self.list_targets() + return self.remove_targets([t['id'] for t in targets]) + + def _targets_request(self, targets): + """Formats each target for the request""" + targets_request = [] + for target in targets: + target_request = { + 'Id': target['id'], + 'Arn': target['arn'] + } + if 'input' in target: + target_request['Input'] = target['input'] + if 'input_path' in target: + target_request['InputPath'] = target['input_path'] + targets_request.append(target_request) + return targets_request + + def _snakify(self, dict): + """Converts cammel case to snake case""" + return camel_dict_to_snake_dict(dict) + + +class CloudWatchEventRuleManager(object): + RULE_FIELDS = ['name', 'event_pattern', 'schedule_expression', 'description', 'role_arn'] + + def __init__(self, rule, targets): + self.rule = rule + self.targets = targets + + def ensure_present(self, enabled=True): + """Ensures the rule and targets are present and synced""" + rule_description = self.rule.describe() + if rule_description: + # Rule exists so update rule, targets and state + self._sync_rule(enabled) + self._sync_targets() + self._sync_state(enabled) + else: + # Rule does not exist, so create new rule and targets + self._create(enabled) + + def ensure_disabled(self): + """Ensures the rule and targets are present, but disabled, and synced""" + self.ensure_present(enabled=False) + + def ensure_absent(self): + """Ensures the rule and targets are absent""" + rule_description = self.rule.describe() + if not rule_description: + # Rule doesn't exist so don't need to delete + return + self.rule.delete() + + def fetch_aws_state(self): + """Retrieves rule and target state from AWS""" + aws_state = { + 'rule': {}, + 'targets': [], + 'changed': self.rule.changed + } + rule_description = self.rule.describe() + if not rule_description: + return aws_state + + # Don't need to include response metadata noise in response + del rule_description['response_metadata'] + + aws_state['rule'] = rule_description + aws_state['targets'].extend(self.rule.list_targets()) + return aws_state + + def _sync_rule(self, enabled=True): + """Syncs local rule state with AWS""" + if not self._rule_matches_aws(): + self.rule.put(enabled) + + def _sync_targets(self): + """Syncs local targets with AWS""" + # Identify and remove extraneous targets on AWS + target_ids_to_remove = self._remote_target_ids_to_remove() + if target_ids_to_remove: + self.rule.remove_targets(target_ids_to_remove) + + # Identify targets that need to be added or updated on AWS + targets_to_put = self._targets_to_put() + if targets_to_put: + self.rule.put_targets(targets_to_put) + + def _sync_state(self, enabled=True): + """Syncs local rule state with AWS""" + remote_state = self._remote_state() + if enabled and remote_state != 'ENABLED': + self.rule.enable() + elif not enabled and remote_state != 'DISABLED': + self.rule.disable() + + def _create(self, enabled=True): + """Creates rule and targets on AWS""" + self.rule.put(enabled) + self.rule.put_targets(self.targets) + + def _rule_matches_aws(self): + """Checks if the local rule data matches AWS""" + aws_rule_data = self.rule.describe() + + # The rule matches AWS only if all rule data fields are equal + # to their corresponding local value defined in the task + return all([ + getattr(self.rule, field) == aws_rule_data.get(field, None) + for field in self.RULE_FIELDS + ]) + + def _targets_to_put(self): + """Returns a list of targets that need to be updated or added remotely""" + remote_targets = self.rule.list_targets() + return [t for t in self.targets if t not in remote_targets] + + def _remote_target_ids_to_remove(self): + """Returns a list of targets that need to be removed remotely""" + target_ids = [t['id'] for t in self.targets] + remote_targets = self.rule.list_targets() + return [ + rt['id'] for rt in remote_targets if rt['id'] not in target_ids + ] + + def _remote_state(self): + """Returns the remote state from AWS""" + description = self.rule.describe() + if not description: + return + return description['state'] + + +def get_cloudwatchevents_client(module): + """Returns a boto3 client for accessing CloudWatch Events""" + try: + region, ec2_url, aws_conn_kwargs = get_aws_connection_info(module, + boto3=True) + if not region: + module.fail_json(msg="Region must be specified as a parameter, in \ + EC2_REGION or AWS_REGION environment variables \ + or in boto configuration file") + return boto3_conn(module, conn_type='client', + resource='events', + region=region, endpoint=ec2_url, + **aws_conn_kwargs) + except boto3.exception.NoAuthHandlerFound, e: + module.fail_json(msg=str(e)) + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + name = dict(required=True), + schedule_expression = dict(), + event_pattern = dict(), + state = dict(choices=['present', 'disabled', 'absent'], + default='present'), + description = dict(), + role_arn = dict(), + targets = dict(type='list', default=[]), + )) + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO3: + module.fail_json(msg='boto3 required for this module') + + rule_data = dict( + [(rf, module.params.get(rf)) for rf in CloudWatchEventRuleManager.RULE_FIELDS] + ) + targets = module.params.get('targets') + state = module.params.get('state') + + cwe_rule = CloudWatchEventRule(module, + client=get_cloudwatchevents_client(module), + **rule_data) + cwe_rule_manager = CloudWatchEventRuleManager(cwe_rule, targets) + + if state == 'present': + cwe_rule_manager.ensure_present() + elif state == 'disabled': + cwe_rule_manager.ensure_disabled() + elif state == 'absent': + cwe_rule_manager.ensure_absent() + else: + module.fail_json(msg="Invalid state '{0}' provided".format(state)) + + module.exit_json(**cwe_rule_manager.fetch_aws_state()) + + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + + +if __name__ == '__main__': + main() From abc1a988265ec59be5a9dac487c1fe0af6f801cf Mon Sep 17 00:00:00 2001 From: Steyn Huizinga Date: Fri, 16 Sep 2016 21:36:50 +0200 Subject: [PATCH 2110/2522] Add AWS Lambda module (#1270) --- cloud/amazon/lambda.py | 437 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 437 insertions(+) create mode 100644 cloud/amazon/lambda.py diff --git a/cloud/amazon/lambda.py b/cloud/amazon/lambda.py new file mode 100644 index 00000000000..7fb5ea8371c --- /dev/null +++ b/cloud/amazon/lambda.py @@ -0,0 +1,437 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + + +DOCUMENTATION = ''' +--- +module: lambda +short_description: Manage AWS Lambda functions +description: + - Allows for the management of Lambda functions. +version_added: '2.2' +requirements: [ boto3 ] +options: + name: + description: + - The name you want to assign to the function you are uploading. Cannot be changed. + required: true + state: + description: + - Create or delete Lambda function + required: false + default: present + choices: [ 'present', 'absent' ] + runtime: + description: + - The runtime environment for the Lambda function you are uploading. Required when creating a function. Use parameters as described in boto3 docs. Current example runtime environments are nodejs, nodejs4.3, java8 or python2.7 + required: true + role_arn: + description: + - The Amazon Resource Name (ARN) of the IAM role that Lambda assumes when it executes your function to access any other Amazon Web Services (AWS) resources + default: null + handler: + description: + - The function within your code that Lambda calls to begin execution + default: null + zip_file: + description: + - A .zip file containing your deployment package + required: false + default: null + aliases: [ 'src' ] + s3_bucket: + description: + - Amazon S3 bucket name where the .zip file containing your deployment package is stored + required: false + default: null + s3_key: + description: + - The Amazon S3 object (the deployment package) key name you want to upload + required: false + default: null + s3_object_version: + description: + - The Amazon S3 object (the deployment package) version you want to upload. + required: false + default: null + description: + description: + - A short, user-defined function description. Lambda does not use this value. Assign a meaningful description as you see fit. + required: false + default: null + timeout: + description: + - The function execution time at which Lambda should terminate the function. + required: false + default: 3 + memory_size: + description: + - The amount of memory, in MB, your Lambda function is given + required: false + default: 128 + vpc_subnet_ids: + description: + - List of subnet IDs to run Lambda function in. Use this option if you need to access resources in your VPC. Leave empty if you don't want to run the function in a VPC. + required: false + default: None + vpc_security_group_ids: + description: + - List of VPC security group IDs to associate with the Lambda function. Required when vpc_subnet_ids is used. + required: false + default: None +notes: + - 'Currently this module only supports uploaded code via S3' +author: + - 'Steyn Huizinga (@steynovich)' +extends_documentation_fragment: + - aws + - ec2 +''' + +EXAMPLES = ''' +# Create Lambda functions +tasks: +- name: looped creation + lambda: + name: '{{ item.name }}' + state: present + zip_file: '{{ item.zip_file }}' + runtime: 'python2.7' + role_arn: 'arn:aws:iam::987654321012:role/lambda_basic_execution' + handler: 'hello_python.my_handler' + vpc_subnet_ids: + - subnet-123abcde + - subnet-edcba321 + vpc_security_group_ids: + - sg-123abcde + - sg-edcba321 + with_items: + - { name: HelloWorld, zip_file: 'hello-code.zip' } + - { name: ByeBye, zip_file: 'bye-code.zip' } + +# Basic Lambda function deletion +tasks: +- name: Delete Lambda functions HelloWorld and ByeBye + lambda: + name: '{{ item }}' + state: absent + with_items: + - HelloWorld + - ByeBye +''' + +RETURN = ''' +output: + description: the data returned by create_function in boto3 + returned: success + type: dict + sample: + { + 'FunctionName': 'string', + 'FunctionArn': 'string', + 'Runtime': 'nodejs', + 'Role': 'string', + 'Handler': 'string', + 'CodeSize': 123, + 'Description': 'string', + 'Timeout': 123, + 'MemorySize': 123, + 'LastModified': 'string', + 'CodeSha256': 'string', + 'Version': 'string', + } +''' + +# Import from Python standard library +import base64 +import hashlib + +try: + import botocore + HAS_BOTOCORE = True +except ImportError: + HAS_BOTOCORE = False + +try: + import boto3 + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + + +def get_current_function(connection, function_name): + try: + return connection.get_function(FunctionName=function_name) + except botocore.exceptions.ClientError as e: + return False + + +def sha256sum(filename): + hasher = hashlib.sha256() + with open(filename, 'rb') as f: + hasher.update(f.read()) + + code_hash = hasher.digest() + code_b64 = base64.b64encode(code_hash) + hex_digest = code_b64.decode('utf-8') + + return hex_digest + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + name=dict(type='str', required=True), + state=dict(type='str', default='present', choices=['present', 'absent']), + runtime=dict(type='str', required=True), + role_arn=dict(type='str', default=None), + handler=dict(type='str', default=None), + zip_file=dict(type='str', default=None, aliases=['src']), + s3_bucket=dict(type='str'), + s3_key=dict(type='str'), + s3_object_version=dict(type='str', default=None), + description=dict(type='str', default=''), + timeout=dict(type='int', default=3), + memory_size=dict(type='int', default=128), + vpc_subnet_ids=dict(type='list', default=None), + vpc_security_group_ids=dict(type='list', default=None), + ) + ) + + mutually_exclusive = [['zip_file', 's3_key'], + ['zip_file', 's3_bucket'], + ['zip_file', 's3_object_version']] + + required_together = [['s3_key', 's3_bucket', 's3_object_version'], + ['vpc_subnet_ids', 'vpc_security_group_ids']] + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True, + mutually_exclusive=mutually_exclusive, + required_together=required_together) + + name = module.params.get('name') + state = module.params.get('state').lower() + runtime = module.params.get('runtime') + role_arn = module.params.get('role_arn') + handler = module.params.get('handler') + s3_bucket = module.params.get('s3_bucket') + s3_key = module.params.get('s3_key') + s3_object_version = module.params.get('s3_object_version') + zip_file = module.params.get('zip_file') + description = module.params.get('description') + timeout = module.params.get('timeout') + memory_size = module.params.get('memory_size') + vpc_subnet_ids = module.params.get('vpc_subnet_ids') + vpc_security_group_ids = module.params.get('vpc_security_group_ids') + + check_mode = module.check_mode + changed = False + + if not HAS_BOTOCORE: + module.fail_json(msg='Python module "botocore" is missing, please install it') + + if not HAS_BOTO3: + module.fail_json(msg='Python module "boto3" is missing, please install it') + + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + if not region: + module.fail_json(msg='region must be specified') + + try: + client = boto3_conn(module, conn_type='client', resource='lambda', + region=region, endpoint=ec2_url, **aws_connect_kwargs) + except (botocore.exceptions.ClientError, botocore.exceptions.ValidationError) as e: + module.fail_json(msg=str(e)) + + # Get function configuration if present, False otherwise + current_function = get_current_function(client, name) + + # Update existing Lambda function + if state == 'present' and current_function: + + # Get current state + current_config = current_function['Configuration'] + + # Update function configuration + func_kwargs = {'FunctionName': name} + + # Update configuration if needed + if role_arn and current_config['Role'] != role_arn: + func_kwargs.update({'Role': role_arn}) + if handler and current_config['Handler'] != handler: + func_kwargs.update({'Handler': handler}) + if description and current_config['Description'] != description: + func_kwargs.update({'Description': description}) + if timeout and current_config['Timeout'] != timeout: + func_kwargs.update({'Timeout': timeout}) + if memory_size and current_config['MemorySize'] != memory_size: + func_kwargs.update({'MemorySize': memory_size}) + + # Check for unsupported mutation + if current_config['Runtime'] != runtime: + module.fail_json(msg='Cannot change runtime. Please recreate the function') + + # If VPC configuration is desired + if vpc_subnet_ids or vpc_security_group_ids: + if len(vpc_subnet_ids) < 1: + module.fail_json(msg='At least 1 subnet is required') + + if len(vpc_security_group_ids) < 1: + module.fail_json(msg='At least 1 security group is required') + + if 'VpcConfig' in current_config: + # Compare VPC config with current config + current_vpc_subnet_ids = current_config['VpcConfig']['SubnetIds'] + current_vpc_security_group_ids = current_config['VpcConfig']['SecurityGroupIds'] + + subnet_net_id_changed = sorted(vpc_subnet_ids) != sorted(current_vpc_subnet_ids) + vpc_security_group_ids_changed = sorted(vpc_security_group_ids) != sorted(current_vpc_security_group_ids) + + if any((subnet_net_id_changed, vpc_security_group_ids_changed)): + func_kwargs.update({'VpcConfig': + {'SubnetIds': vpc_subnet_ids,'SecurityGroupIds': vpc_security_group_ids}}) + else: + # No VPC configuration is desired, assure VPC config is empty when present in current config + if ('VpcConfig' in current_config and + 'VpcId' in current_config['VpcConfig'] and + current_config['VpcConfig']['VpcId'] != ''): + func_kwargs.update({'VpcConfig':{'SubnetIds': [], 'SecurityGroupIds': []}}) + + # Upload new configuration if configuration has changed + if len(func_kwargs) > 1: + try: + if not check_mode: + client.update_function_configuration(**func_kwargs) + changed = True + except (botocore.exceptions.ParamValidationError, botocore.exceptions.ClientError) as e: + module.fail_json(msg=str(e)) + + # Update code configuration + code_kwargs = {'FunctionName': name} + + # Update S3 location + if s3_bucket and s3_key: + # If function is stored on S3 always update + code_kwargs.update({'S3Bucket': s3_bucket, 'S3Key': s3_key}) + + # If S3 Object Version is given + if s3_object_version: + code_kwargs.update({'S3ObjectVersion': s3_object_version}) + + # Compare local checksum, update remote code when different + elif zip_file: + local_checksum = sha256sum(zip_file) + remote_checksum = current_config['CodeSha256'] + + # Only upload new code when local code is different compared to the remote code + if local_checksum != remote_checksum: + try: + with open(zip_file, 'rb') as f: + encoded_zip = f.read() + code_kwargs.update({'ZipFile': encoded_zip}) + except IOError as e: + module.fail_json(msg=str(e)) + + # Upload new code if needed (e.g. code checksum has changed) + if len(code_kwargs) > 1: + try: + if not check_mode: + client.update_function_code(**code_kwargs) + changed = True + except (botocore.exceptions.ParamValidationError, botocore.exceptions.ClientError) as e: + module.fail_json(msg=str(e)) + + # Describe function code and configuration + response = get_current_function(client, name) + if not response: + module.fail_json(msg='Unable to get function information after updating') + + # We're done + module.exit_json(changed=changed, result=camel_dict_to_snake_dict(response)) + + # Function doesn't exists, create new Lambda function + elif state == 'present': + if s3_bucket and s3_key: + # If function is stored on S3 + code = {'S3Bucket': s3_bucket, + 'S3Key': s3_key} + if s3_object_version: + code.update({'S3ObjectVersion': s3_object_version}) + elif zip_file: + # If function is stored in local zipfile + try: + with open(zip_file, 'rb') as f: + zip_content = f.read() + + code = {'ZipFile': zip_content} + except IOError as e: + module.fail_json(msg=str(e)) + + else: + module.fail_json(msg='Either S3 object or path to zipfile required') + + func_kwargs = {'FunctionName': name, + 'Description': description, + 'Runtime': runtime, + 'Role': role_arn, + 'Handler': handler, + 'Code': code, + 'Timeout': timeout, + 'MemorySize': memory_size, + } + + # If VPC configuration is given + if vpc_subnet_ids or vpc_security_group_ids: + if len(vpc_subnet_ids) < 1: + module.fail_json(msg='At least 1 subnet is required') + + if len(vpc_security_group_ids) < 1: + module.fail_json(msg='At least 1 security group is required') + + func_kwargs.update({'VpcConfig': {'SubnetIds': vpc_subnet_ids, + 'SecurityGroupIds': vpc_security_group_ids}}) + + # Finally try to create function + try: + if not check_mode: + response = client.create_function(**func_kwargs) + changed = True + except (botocore.exceptions.ParamValidationError, botocore.exceptions.ClientError) as e: + module.fail_json(msg=str(e)) + + module.exit_json(changed=changed, result=camel_dict_to_snake_dict(response)) + + # Delete existing Lambda function + if state == 'absent' and current_function: + try: + if not check_mode: + client.delete_function(FunctionName=name) + changed = True + except (botocore.exceptions.ParamValidationError, botocore.exceptions.ClientError) as e: + module.fail_json(msg=str(e)) + + module.exit_json(changed=changed) + + # Function already absent, do nothing + elif state == 'absent': + module.exit_json(changed=changed) + + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +main() From 2be52280fc1d3225c0874d4c6963d758078ce79f Mon Sep 17 00:00:00 2001 From: Ryan Brown Date: Fri, 16 Sep 2016 15:46:41 -0400 Subject: [PATCH 2111/2522] Fix exception syntax for Python 3.x (#2940) since boto already precludes python2.4, no need to use a common 2.4/3 syntax --- cloud/amazon/cloudwatchevent_rule.py | 6 +++--- cloud/amazon/redshift_subnet_group.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cloud/amazon/cloudwatchevent_rule.py b/cloud/amazon/cloudwatchevent_rule.py index a21800c1936..8fda1c125ab 100644 --- a/cloud/amazon/cloudwatchevent_rule.py +++ b/cloud/amazon/cloudwatchevent_rule.py @@ -128,7 +128,7 @@ def describe(self): """Returns the existing details of the rule in AWS""" try: rule_info = self.client.describe_rule(Name=self.name) - except botocore.exceptions.ClientError, e: + except botocore.exceptions.ClientError as e: error_code = e.response.get('Error', {}).get('Code') if error_code == 'ResourceNotFoundException': return {} @@ -176,7 +176,7 @@ def list_targets(self): """Lists the existing targets for the rule in AWS""" try: targets = self.client.list_targets_by_rule(Rule=self.name) - except botocore.exceptions.ClientError, e: + except botocore.exceptions.ClientError as e: error_code = e.response.get('Error', {}).get('Code') if error_code == 'ResourceNotFoundException': return [] @@ -356,7 +356,7 @@ def get_cloudwatchevents_client(module): resource='events', region=region, endpoint=ec2_url, **aws_conn_kwargs) - except boto3.exception.NoAuthHandlerFound, e: + except boto3.exception.NoAuthHandlerFound as e: module.fail_json(msg=str(e)) diff --git a/cloud/amazon/redshift_subnet_group.py b/cloud/amazon/redshift_subnet_group.py index acd3330c1f2..d47593c7970 100644 --- a/cloud/amazon/redshift_subnet_group.py +++ b/cloud/amazon/redshift_subnet_group.py @@ -17,7 +17,7 @@ DOCUMENTATION = ''' --- -author: +author: - "Jens Carl (@j-carl), Hothead Games Inc." module: redshift_subnet_group version_added: "2.1" @@ -128,7 +128,7 @@ def main(): # Connect to the Redshift endpoint. try: conn = connect_to_aws(boto.redshift, region, **aws_connect_params) - except boto.exception.JSONResponseError, e: + except boto.exception.JSONResponseError as e: module.fail_json(msg=str(e)) try: @@ -139,7 +139,7 @@ def main(): try: matching_groups = conn.describe_cluster_subnet_groups(group_name, max_records=100) exists = len(matching_groups) > 0 - except boto.exception.JSONResponseError, e: + except boto.exception.JSONResponseError as e: if e.body['Error']['Code'] != 'ClusterSubnetGroupNotFoundFault': #if e.code != 'ClusterSubnetGroupNotFoundFault': module.fail_json(msg=str(e)) @@ -169,7 +169,7 @@ def main(): changed = True - except boto.exception.JSONResponseError, e: + except boto.exception.JSONResponseError as e: module.fail_json(msg=str(e)) module.exit_json(changed=changed, group=group) From 64d60b8704991f225e1bc00efb4c888b5f21126a Mon Sep 17 00:00:00 2001 From: "Ryan S. Brown" Date: Fri, 16 Sep 2016 15:48:44 -0400 Subject: [PATCH 2112/2522] Fix redshift module Python 3.5 syntax --- cloud/amazon/redshift.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/cloud/amazon/redshift.py b/cloud/amazon/redshift.py index b6c6fdd35d5..c5d086a6f12 100644 --- a/cloud/amazon/redshift.py +++ b/cloud/amazon/redshift.py @@ -17,7 +17,7 @@ DOCUMENTATION = ''' --- -author: +author: - "Jens Carl (@j-carl), Hothead Games Inc." module: redshift version_added: "2.1" @@ -293,15 +293,15 @@ def create_cluster(module, redshift): try: redshift.describe_clusters(identifier)['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] changed = False - except boto.exception.JSONResponseError, e: + except boto.exception.JSONResponseError as e: try: redshift.create_cluster(identifier, node_type, username, password, **params) - except boto.exception.JSONResponseError, e: + except boto.exception.JSONResponseError as e: module.fail_json(msg=str(e)) try: resource = redshift.describe_clusters(identifier)['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] - except boto.exception.JSONResponseError, e: + except boto.exception.JSONResponseError as e: module.fail_json(msg=str(e)) if wait: @@ -316,7 +316,7 @@ def create_cluster(module, redshift): resource = redshift.describe_clusters(identifier)['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] - except boto.exception.JSONResponseError, e: + except boto.exception.JSONResponseError as e: module.fail_json(msg=str(e)) return(changed, _collect_facts(resource)) @@ -333,7 +333,7 @@ def describe_cluster(module, redshift): try: resource = redshift.describe_clusters(identifier)['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] - except boto.exception.JSONResponseError, e: + except boto.exception.JSONResponseError as e: module.fail_json(msg=str(e)) return(True, _collect_facts(resource)) @@ -353,7 +353,7 @@ def delete_cluster(module, redshift): try: redshift.delete_custer( identifier ) - except boto.exception.JSONResponseError, e: + except boto.exception.JSONResponseError as e: module.fail_json(msg=str(e)) if wait: @@ -368,7 +368,7 @@ def delete_cluster(module, redshift): resource = redshift.describe_clusters(identifier)['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] - except boto.exception.JSONResponseError, e: + except boto.exception.JSONResponseError as e: module.fail_json(msg=str(e)) return(True, {}) @@ -400,15 +400,15 @@ def modify_cluster(module, redshift): try: redshift.describe_clusters(identifier)['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] changed = False - except boto.exception.JSONResponseError, e: + except boto.exception.JSONResponseError as e: try: redshift.modify_cluster(identifier, **params) - except boto.exception.JSONResponseError, e: + except boto.exception.JSONResponseError as e: module.fail_json(msg=str(e)) try: resource = redshift.describe_clusters(identifier)['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] - except boto.exception.JSONResponseError, e: + except boto.exception.JSONResponseError as e: module.fail_json(msg=str(e)) if wait: @@ -423,7 +423,7 @@ def modify_cluster(module, redshift): resource = redshift.describe_clusters(identifier)['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] - except boto.exception.JSONResponseError, e: + except boto.exception.JSONResponseError as e: # https://github.com/boto/boto/issues/2776 is fixed. module.fail_json(msg=str(e)) @@ -476,7 +476,7 @@ def main(): # connect to the rds endpoint try: conn = connect_to_aws(boto.redshift, region, **aws_connect_params) - except boto.exception.JSONResponseError, e: + except boto.exception.JSONResponseError as e: module.fail_json(msg=str(e)) changed = True From ed249b3466d16da9a389c8461d683159d190f925 Mon Sep 17 00:00:00 2001 From: Ryan Brown Date: Fri, 16 Sep 2016 16:25:33 -0400 Subject: [PATCH 2113/2522] Import Lambda alias module from https://github.com/pjodouin/ansible-lambda (#2829) --- cloud/amazon/lambda_alias.py | 384 +++++++++++++++++++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 cloud/amazon/lambda_alias.py diff --git a/cloud/amazon/lambda_alias.py b/cloud/amazon/lambda_alias.py new file mode 100644 index 00000000000..d744ca7346b --- /dev/null +++ b/cloud/amazon/lambda_alias.py @@ -0,0 +1,384 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +try: + import boto3 + from botocore.exceptions import ClientError, ParamValidationError, MissingParametersError + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + + +DOCUMENTATION = ''' +--- +module: lambda_alias +short_description: Creates, updates or deletes AWS Lambda function aliases. +description: + - This module allows the management of AWS Lambda functions aliases via the Ansible + framework. It is idempotent and supports "Check" mode. Use module M(lambda) to manage the lambda function + itself and M(lambda_event) to manage event source mappings. + +version_added: "2.2" + +author: Pierre Jodouin (@pjodouin), Ryan Scott Brown (@ryansb) +options: + function_name: + description: + - The name of the function alias. + required: true + state: + description: + - Describes the desired state. + required: true + default: "present" + choices: ["present", "absent"] + name: + description: + - Name of the function alias. + required: true + aliases: ['alias_name'] + description: + description: + - A short, user-defined function alias description. + required: false + version: + description: + - Version associated with the Lambda function alias. + A value of 0 (or omitted parameter) sets the alias to the $LATEST version. + required: false + aliases: ['function_version'] +requirements: + - boto3 +extends_documentation_fragment: + - aws + +''' + +EXAMPLES = ''' +--- +# Simple example to create a lambda function and publish a version +- hosts: localhost + gather_facts: no + vars: + state: present + project_folder: /path/to/deployment/package + deployment_package: lambda.zip + account: 123456789012 + production_version: 5 + tasks: + - name: AWS Lambda Function + lambda: + state: "{{ state | default('present') }}" + name: myLambdaFunction + publish: True + description: lambda function description + code_s3_bucket: package-bucket + code_s3_key: "lambda/{{ deployment_package }}" + local_path: "{{ project_folder }}/{{ deployment_package }}" + runtime: python2.7 + timeout: 5 + handler: lambda.handler + memory_size: 128 + role: "arn:aws:iam::{{ account }}:role/API2LambdaExecRole" + + - name: show results + debug: var=lambda_facts + +# The following will set the Dev alias to the latest version ($LATEST) since version is omitted (or = 0) + - name: "alias 'Dev' for function {{ lambda_facts.FunctionName }} " + lambda_alias: + state: "{{ state | default('present') }}" + function_name: "{{ lambda_facts.FunctionName }}" + name: Dev + description: Development is $LATEST version + +# The QA alias will only be created when a new version is published (i.e. not = '$LATEST') + - name: "alias 'QA' for function {{ lambda_facts.FunctionName }} " + lambda_alias: + state: "{{ state | default('present') }}" + function_name: "{{ lambda_facts.FunctionName }}" + name: QA + version: "{{ lambda_facts.Version }}" + description: "QA is version {{ lambda_facts.Version }}" + when: lambda_facts.Version != "$LATEST" + +# The Prod alias will have a fixed version based on a variable + - name: "alias 'Prod' for function {{ lambda_facts.FunctionName }} " + lambda_alias: + state: "{{ state | default('present') }}" + function_name: "{{ lambda_facts.FunctionName }}" + name: Prod + version: "{{ production_version }}" + description: "Production is version {{ production_version }}" +''' + +RETURN = ''' +--- +alias_arn: + description: Full ARN of the function, including the alias + returned: success + type: string + sample: arn:aws:lambda:us-west-2:123456789012:function:myFunction:dev +description: + description: A short description of the alias + returned: success + type: string + sample: The development stage for my hot new app +function_version: + description: The qualifier that the alias refers to + returned: success + type: string + sample: $LATEST +name: + description: The name of the alias assigned + returned: success + type: string + sample: dev +''' + + +class AWSConnection: + """ + Create the connection object and client objects as required. + """ + + def __init__(self, ansible_obj, resources, boto3=True): + + try: + self.region, self.endpoint, aws_connect_kwargs = get_aws_connection_info(ansible_obj, boto3=boto3) + + self.resource_client = dict() + if not resources: + resources = ['lambda'] + + resources.append('iam') + + for resource in resources: + aws_connect_kwargs.update(dict(region=self.region, + endpoint=self.endpoint, + conn_type='client', + resource=resource + )) + self.resource_client[resource] = boto3_conn(ansible_obj, **aws_connect_kwargs) + + # if region is not provided, then get default profile/session region + if not self.region: + self.region = self.resource_client['lambda'].meta.region_name + + except (ClientError, ParamValidationError, MissingParametersError) as e: + ansible_obj.fail_json(msg="Unable to connect, authorize or access resource: {0}".format(e)) + + try: + self.account_id = self.resource_client['iam'].get_user()['User']['Arn'].split(':')[4] + except (ClientError, ValueError, KeyError, IndexError): + self.account_id = '' + + def client(self, resource='lambda'): + return self.resource_client[resource] + + +def pc(key): + """ + Changes python key into Pascale case equivalent. For example, 'this_function_name' becomes 'ThisFunctionName'. + + :param key: + :return: + """ + + return "".join([token.capitalize() for token in key.split('_')]) + + +def set_api_params(module, module_params): + """ + Sets module parameters to those expected by the boto3 API. + + :param module: + :param module_params: + :return: + """ + + api_params = dict() + + for param in module_params: + module_param = module.params.get(param, None) + if module_param: + api_params[pc(param)] = module_param + + return api_params + + +def validate_params(module, aws): + """ + Performs basic parameter validation. + + :param module: Ansible module reference + :param aws: AWS client connection + :return: + """ + + function_name = module.params['function_name'] + + # validate function name + if not re.search('^[\w\-:]+$', function_name): + module.fail_json( + msg='Function name {0} is invalid. Names must contain only alphanumeric characters and hyphens.'.format(function_name) + ) + if len(function_name) > 64: + module.fail_json(msg='Function name "{0}" exceeds 64 character limit'.format(function_name)) + + # if parameter 'function_version' is zero, set it to $LATEST, else convert it to a string + if module.params['function_version'] == 0: + module.params['function_version'] = '$LATEST' + else: + module.params['function_version'] = str(module.params['function_version']) + + return + + +def get_lambda_alias(module, aws): + """ + Returns the lambda function alias if it exists. + + :param module: Ansible module reference + :param aws: AWS client connection + :return: + """ + + client = aws.client('lambda') + + # set API parameters + api_params = set_api_params(module, ('function_name', 'name')) + + # check if alias exists and get facts + try: + results = client.get_alias(**api_params) + + except (ClientError, ParamValidationError, MissingParametersError) as e: + if e.response['Error']['Code'] == 'ResourceNotFoundException': + results = None + else: + module.fail_json(msg='Error retrieving function alias: {0}'.format(e)) + + return results + + +def lambda_alias(module, aws): + """ + Adds, updates or deletes lambda function aliases. + + :param module: Ansible module reference + :param aws: AWS client connection + :return dict: + """ + client = aws.client('lambda') + results = dict() + changed = False + current_state = 'absent' + state = module.params['state'] + + facts = get_lambda_alias(module, aws) + if facts: + current_state = 'present' + + if state == 'present': + if current_state == 'present': + + # check if alias has changed -- only version and description can change + alias_params = ('function_version', 'description') + for param in alias_params: + if module.params.get(param) != facts.get(pc(param)): + changed = True + break + + if changed: + api_params = set_api_params(module, ('function_name', 'name')) + api_params.update(set_api_params(module, alias_params)) + + if not module.check_mode: + try: + results = client.update_alias(**api_params) + except (ClientError, ParamValidationError, MissingParametersError) as e: + module.fail_json(msg='Error updating function alias: {0}'.format(e)) + + else: + # create new function alias + api_params = set_api_params(module, ('function_name', 'name', 'function_version', 'description')) + + try: + if not module.check_mode: + results = client.create_alias(**api_params) + changed = True + except (ClientError, ParamValidationError, MissingParametersError) as e: + module.fail_json(msg='Error creating function alias: {0}'.format(e)) + + else: # state = 'absent' + if current_state == 'present': + # delete the function + api_params = set_api_params(module, ('function_name', 'name')) + + try: + if not module.check_mode: + results = client.delete_alias(**api_params) + changed = True + except (ClientError, ParamValidationError, MissingParametersError) as e: + module.fail_json(msg='Error deleting function alias: {0}'.format(e)) + + return dict(changed=changed, **dict(results or facts)) + + +def main(): + """ + Main entry point. + + :return dict: ansible facts + """ + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + state=dict(required=False, default='present', choices=['present', 'absent']), + function_name=dict(required=True, default=None), + name=dict(required=True, default=None, aliases=['alias_name']), + function_version=dict(type='int', required=False, default=0, aliases=['version']), + description=dict(required=False, default=None), + ) + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + mutually_exclusive=[], + required_together=[] + ) + + # validate dependencies + if not HAS_BOTO3: + module.fail_json(msg='boto3 is required for this module.') + + aws = AWSConnection(module, ['lambda']) + + validate_params(module, aws) + + results = lambda_alias(module, aws) + + module.exit_json(**camel_dict_to_snake_dict(results)) + + +# ansible import module(s) kept at ~eof as recommended +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From 412af426c4a7ed4d3f3d271b87e4f5a09154a62c Mon Sep 17 00:00:00 2001 From: Ryan Sydnor Date: Wed, 25 May 2016 14:00:16 -0400 Subject: [PATCH 2114/2522] Add Amazon Elastic File System (efs) module --- cloud/amazon/efs.py | 677 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 677 insertions(+) create mode 100644 cloud/amazon/efs.py diff --git a/cloud/amazon/efs.py b/cloud/amazon/efs.py new file mode 100644 index 00000000000..26bae9aaf5a --- /dev/null +++ b/cloud/amazon/efs.py @@ -0,0 +1,677 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: efs +short_description: create and maintain EFS file systems +description: + - Module allows create, search and destroy Amazon EFS file systems +version_added: "2.2" +requirements: [ boto3 ] +author: + - "Ryan Sydnor (@ryansydnor)" + - "Artem Kazakov (@akazakov)" +options: + state: + description: + - Allows to create, search and destroy Amazon EFS file system + required: true + choices: ['present', 'absent'] + aliases: [] + name: + description: + - Creation Token of Amazon EFS file system. Required for create. Either name or ID required for delete. It can be omitted for 'list' (unless you want to find EFS with certain name). + required: false + default: None + aliases: [] + id: + description: + - ID of Amazon EFS. Either name or ID required for delete. + required: false + default: None + aliases: [] + tags: + description: + - | + List of tags of Amazon EFS. Should be defined as dictionary + In case of 'present' state with list of tags and existing EFS (matched by 'name'), tags of EFS will be replaced with provided data. + required: false + default: None + aliases: [] + targets: + description: + - | + List of mounted targets. It should be a list of dictionaries, every dictionary should include next attributes: + - SubnetId - Mandatory. The ID of the subnet to add the mount target in. + - IpAddress - Optional. A valid IPv4 address within the address range of the specified subnet. + - SecurityGroups - Optional. List of security group IDs, of the form "sg-xxxxxxxx". These must be for the same VPC as subnet specified + This data may be modified for existing EFS using state 'present' and new list of mount targets. + required: false + default: None + aliases: [] + wait: + description: + - | + In case of 'present' state should wait for EFS 'available' life cycle state (of course, if current state not 'deleting' or 'deleted') + In case of 'absent' state should wait for EFS 'deleted' life cycle state + required: false + default: "no" + choices: ["yes", "no"] + aliases: [] + wait_timeout: + description: + - How long the module should wait (in seconds) for desired state before returning. Zero means wait as long as necessary. + required: false + default: 0 + aliases: [] +extends_documentation_fragment: + - aws +''' + +EXAMPLES = ''' +# EFS provisioning +- efs: + state: present + name: myTestEFS + tags: + Name: myTestNameTag + Purpose: file-storage + targets: + - SubnetId: subnet-748c5d03 + SecurityGroups: [ "sg-1a2b3c4d" ] + +# Modifying EFS data +- efs: + state: present + name: myTestEFS + tags: + Name: myAnotherTestTag + targets: + - SubnetId: subnet-7654fdca + SecurityGroups: [ "sg-4c5d6f7a" ] + +# Deleting EFS +- efs: + state: absent + name: myTestEFS + +# Searching all EFS instances with tag Name = 'myTestNameTag', in subnet 'subnet-1a2b3c4d' and with security group 'sg-4d3c2b1a' +- efs: + state: list + tags: + Name: myTestNameTag + targets: + - subnet-1a2b3c4d + - sg-4d3c2b1a + +''' + +RETURN = ''' +CreationTime: + description: timestamp of creation date + returned: + type: datetime + sample: 2015-11-16 07:30:57-05:00 +CreationToken: + description: EFS creation token + returned: + type: UUID + sample: console-88609e04-9a0e-4a2e-912c-feaa99509961 +FileSystemId: + description: ID of the file system + returned: + type: unique ID + sample: fs-xxxxxxxx +LifeCycleState: + description: state of the EFS file system + returned: + type: str + sample: creating, available, deleting, deleted +MountPoint: + description: url of file system + returned: + type: str + sample: .fs-xxxxxxxx.efs.us-west-2.amazonaws.com:/ +MountTargets: + description: list of mount targets + returned: + type: list of dicts + sample: + [ + { + "FileSystemId": "fs-a7ad440e", + "IpAddress": "172.31.17.173", + "LifeCycleState": "available", + "MountTargetId": "fsmt-d8907871", + "NetworkInterfaceId": "eni-6e387e26", + "OwnerId": "740748460359", + "SecurityGroups": [ + "sg-a30b22c6" + ], + "SubnetId": "subnet-e265c895" + }, + ... + ] +Name: + description: name of the file system + returned: + type: str + sample: my-efs +NumberOfMountTargets: + description: the number of targets mounted + returned: + type: int + sample: 3 +OwnerId: + description: AWS account ID of EFS owner + returned: + type: str + sample: XXXXXXXXXXXX +SizeInBytes: + description: size of the file system in bytes as of a timestamp + returned: + type: dict + sample: + { + "Timestamp": "2015-12-21 13:59:59-05:00", + "Value": 12288 + } +Tags: + description: tags on the efs instance + returned: + type: dict + sample: + { + "Name": "my-efs", + "Key": "Value" + } + +''' + +import sys +from time import sleep +from time import time as timestamp +from collections import defaultdict + +try: + from botocore.exceptions import ClientError + from boto3.session import Session + HAS_BOTO3 = True +except ImportError as e: + HAS_BOTO3 = False + + +class EFSConnection(object): + + DEFAULT_WAIT_TIMEOUT_SECONDS = 0 + + STATE_CREATING = 'creating' + STATE_AVAILABLE = 'available' + STATE_DELETING = 'deleting' + STATE_DELETED = 'deleted' + + def __init__(self, module, region, **aws_connect_params): + try: + session = Session( + aws_access_key_id=aws_connect_params['aws_access_key_id'], + aws_secret_access_key=aws_connect_params['aws_secret_access_key'], + aws_session_token=aws_connect_params['aws_session_token'], + region_name=region + ) + self.connection = session.client('efs') + except Exception as e: + module.fail_json(msg=repr(e)) + + self.region = region + self.wait = module.params.get('wait') + self.wait_timeout = module.params.get('wait_timeout') + + def get_file_systems(self, **kwargs): + """ + Returns generator of file systems including all attributes of FS + """ + items = iterate_all( + 'FileSystems', + self.connection.describe_file_systems, + **kwargs + ) + for item in items: + item['CreationTime'] = str(item['CreationTime']) + """ + Suffix of network path to be used as NFS device for mount. More detail here: + http://docs.aws.amazon.com/efs/latest/ug/gs-step-three-connect-to-ec2-instance.html + """ + item['MountPoint'] = '.%s.efs.%s.amazonaws.com:/' % (item['FileSystemId'], self.region) + if 'Timestamp' in item['SizeInBytes']: + item['SizeInBytes']['Timestamp'] = str(item['SizeInBytes']['Timestamp']) + if item['LifeCycleState'] == self.STATE_AVAILABLE: + item['Tags'] = self.get_tags(FileSystemId=item['FileSystemId']) + item['MountTargets'] = list(self.get_mount_targets(FileSystemId=item['FileSystemId'])) + else: + item['Tags'] = {} + item['MountTargets'] = [] + yield item + + def get_tags(self, **kwargs): + """ + Returns tag list for selected instance of EFS + """ + tags = iterate_all( + 'Tags', + self.connection.describe_tags, + **kwargs + ) + return dict((tag['Key'], tag['Value']) for tag in tags) + + def get_mount_targets(self, **kwargs): + """ + Returns mount targets for selected instance of EFS + """ + targets = iterate_all( + 'MountTargets', + self.connection.describe_mount_targets, + **kwargs + ) + for target in targets: + if target['LifeCycleState'] == self.STATE_AVAILABLE: + target['SecurityGroups'] = list(self.get_security_groups( + MountTargetId=target['MountTargetId'] + )) + else: + target['SecurityGroups'] = [] + yield target + + def get_security_groups(self, **kwargs): + """ + Returns security groups for selected instance of EFS + """ + return iterate_all( + 'SecurityGroups', + self.connection.describe_mount_target_security_groups, + **kwargs + ) + + def get_file_system_id(self, name): + """ + Returns ID of instance by instance name + """ + info = first_or_default(iterate_all( + 'FileSystems', + self.connection.describe_file_systems, + CreationToken=name + )) + return info and info['FileSystemId'] or None + + def get_file_system_state(self, name, file_system_id=None): + """ + Returns state of filesystem by EFS id/name + """ + info = first_or_default(iterate_all( + 'FileSystems', + self.connection.describe_file_systems, + CreationToken=name, + FileSystemId=file_system_id + )) + return info and info['LifeCycleState'] or self.STATE_DELETED + + def get_mount_targets_in_state(self, file_system_id, states=None): + """ + Returns states of mount targets of selected EFS with selected state(s) (optional) + """ + targets = iterate_all( + 'MountTargets', + self.connection.describe_mount_targets, + FileSystemId=file_system_id + ) + + if states: + if not isinstance(states, list): + states = [states] + targets = filter(lambda target: target['LifeCycleState'] in states, targets) + + return list(targets) + + def create_file_system(self, name): + """ + Creates new filesystem with selected name + """ + changed = False + state = self.get_file_system_state(name) + if state in [self.STATE_DELETING, self.STATE_DELETED]: + wait_for( + lambda: self.get_file_system_state(name), + self.STATE_DELETED + ) + self.connection.create_file_system(CreationToken=name) + changed = True + + # we always wait for the state to be available when creating. + # if we try to take any actions on the file system before it's available + # we'll throw errors + wait_for( + lambda: self.get_file_system_state(name), + self.STATE_AVAILABLE, + self.wait_timeout + ) + + return changed + + def converge_file_system(self, name, tags, targets): + """ + Change attributes (mount targets and tags) of filesystem by name + """ + result = False + fs_id = self.get_file_system_id(name) + + if tags is not None: + tags_to_create, _, tags_to_delete = dict_diff(self.get_tags(FileSystemId=fs_id), tags) + + if tags_to_delete: + self.connection.delete_tags( + FileSystemId=fs_id, + TagKeys=[item[0] for item in tags_to_delete] + ) + result = True + + if tags_to_create: + self.connection.create_tags( + FileSystemId=fs_id, + Tags=[{'Key': item[0], 'Value': item[1]} for item in tags_to_create] + ) + result = True + + if targets is not None: + incomplete_states = [self.STATE_CREATING, self.STATE_DELETING] + wait_for( + lambda: len(self.get_mount_targets_in_state(fs_id, incomplete_states)), + 0 + ) + + index_by_subnet_id = lambda items: dict((item['SubnetId'], item) for item in items) + + current_targets = index_by_subnet_id(self.get_mount_targets(FileSystemId=fs_id)) + targets = index_by_subnet_id(targets) + + targets_to_create, intersection, targets_to_delete = dict_diff(current_targets, + targets, True) + + """ To modify mount target it should be deleted and created again """ + changed = filter( + lambda sid: not targets_equal(['SubnetId', 'IpAddress', 'NetworkInterfaceId'], + current_targets[sid], targets[sid]), intersection) + targets_to_delete = list(targets_to_delete) + changed + targets_to_create = list(targets_to_create) + changed + + if targets_to_delete: + for sid in targets_to_delete: + self.connection.delete_mount_target( + MountTargetId=current_targets[sid]['MountTargetId'] + ) + wait_for( + lambda: len(self.get_mount_targets_in_state(fs_id, incomplete_states)), + 0 + ) + result = True + + if targets_to_create: + for sid in targets_to_create: + self.connection.create_mount_target( + FileSystemId=fs_id, + **targets[sid] + ) + wait_for( + lambda: len(self.get_mount_targets_in_state(fs_id, incomplete_states)), + 0, + self.wait_timeout + ) + result = True + + security_groups_to_update = filter( + lambda sid: 'SecurityGroups' in targets[sid] and + current_targets[sid]['SecurityGroups'] != targets[sid]['SecurityGroups'], + intersection + ) + + if security_groups_to_update: + for sid in security_groups_to_update: + self.connection.modify_mount_target_security_groups( + MountTargetId=current_targets[sid]['MountTargetId'], + SecurityGroups=targets[sid]['SecurityGroups'] + ) + result = True + + return result + + def delete_file_system(self, name, file_system_id=None): + """ + Removes EFS instance by id/name + """ + result = False + state = self.get_file_system_state(name, file_system_id) + if state in [self.STATE_CREATING, self.STATE_AVAILABLE]: + wait_for( + lambda: self.get_file_system_state(name), + self.STATE_AVAILABLE + ) + if not file_system_id: + file_system_id = self.get_file_system_id(name) + self.delete_mount_targets(file_system_id) + self.connection.delete_file_system(FileSystemId=file_system_id) + result = True + + if self.wait: + wait_for( + lambda: self.get_file_system_state(name), + self.STATE_DELETED, + self.wait_timeout + ) + + return result + + def delete_mount_targets(self, file_system_id): + """ + Removes mount targets by EFS id + """ + wait_for( + lambda: len(self.get_mount_targets_in_state(file_system_id, self.STATE_CREATING)), + 0 + ) + + targets = self.get_mount_targets_in_state(file_system_id, self.STATE_AVAILABLE) + for target in targets: + self.connection.delete_mount_target(MountTargetId=target['MountTargetId']) + + wait_for( + lambda: len(self.get_mount_targets_in_state(file_system_id, self.STATE_DELETING)), + 0 + ) + + return len(targets) > 0 + + +def iterate_all(attr, map_method, **kwargs): + """ + Method creates iterator from boto result set + """ + args = dict((key, value) for (key, value) in kwargs.iteritems() if value is not None) + wait = 1 + while True: + try: + data = map_method(**args) + for elm in data[attr]: + yield elm + if 'NextMarker' in data: + args['Marker'] = data['Nextmarker'] + continue + break + except ClientError as e: + if e.response['Error']['Code'] == "ThrottlingException" and wait < 600: + sleep(wait) + wait = wait * 2 + continue + +def targets_equal(keys, a, b): + """ + Method compare two mount targets by specified attributes + """ + for key in keys: + if key in b and a[key] != b[key]: + return False + + return True + + +def dict_diff(dict1, dict2, by_key=False): + """ + Helper method to calculate difference of two dictionaries + """ + keys1 = set(dict1.keys() if by_key else dict1.iteritems()) + keys2 = set(dict2.keys() if by_key else dict2.iteritems()) + + intersection = keys1 & keys2 + + return keys2 ^ intersection, intersection, keys1 ^ intersection + + +def group_list_of_dict(array): + """ + Helper method to group list of dict to dict with all possible values + """ + result = defaultdict(list) + for item in array: + for key, value in item.iteritems(): + result[key] += value if isinstance(value, list) else [value] + return result + + +def prefix_to_attr(attr_id): + """ + Helper method to convert ID prefix to mount target attribute + """ + attr_by_prefix = { + 'fsmt-': 'MountTargetId', + 'subnet-': 'SubnetId', + 'eni-': 'NetworkInterfaceId', + 'sg-': 'SecurityGroups' + } + prefix = first_or_default(filter( + lambda pref: str(attr_id).startswith(pref), + attr_by_prefix.keys() + )) + if prefix: + return attr_by_prefix[prefix] + return 'IpAddress' + + +def first_or_default(items, default=None): + """ + Helper method to fetch first element of list (if exists) + """ + for item in items: + return item + return default + + +def has_tags(available, required): + """ + Helper method to determine if tag requested already exists + """ + for key, value in required.iteritems(): + if key not in available or value != available[key]: + return False + return True + + +def has_targets(available, required): + """ + Helper method to determine if mount tager requested already exists + """ + grouped = group_list_of_dict(available) + for (value, field) in required: + if field not in grouped or value not in grouped[field]: + return False + return True + + +def wait_for(callback, value, timeout=EFSConnection.DEFAULT_WAIT_TIMEOUT_SECONDS): + """ + Helper method to wait for desired value returned by callback method + """ + wait_start = timestamp() + while True: + if callback() != value: + if timeout != 0 and (timestamp() - wait_start > timeout): + raise RuntimeError('Wait timeout exceeded (' + str(timeout) + ' sec)') + else: + sleep(5) + continue + break + + +def main(): + """ + Module action handler + """ + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + state=dict(required=True, type='str', choices=["present", "absent"]), + id=dict(required=False, type='str', default=None), + name=dict(required=False, type='str', default=None), + tags=dict(required=False, type="dict", default={}), + targets=dict(required=False, type="list", default=[]), + wait=dict(required=False, type="bool", default=False), + wait_timeout=dict(required=False, type="int", default=0) + )) + + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO3: + module.fail_json(msg='boto3 required for this module') + + region, _, aws_connect_params = get_aws_connection_info(module, boto3=True) + connection = EFSConnection(module, region, **aws_connect_params) + + name = module.params.get('name') + fs_id = module.params.get('id') + tags = module.params.get('tags') + targets = module.params.get('targets') + changed = False + + state = str(module.params.get('state')).lower() + + if state == 'present': + if not name: + module.fail_json(msg='Name parameter is required for create') + + changed = connection.create_file_system(name) + changed = connection.converge_file_system(name=name, tags=tags, targets=targets) or changed + result = first_or_default(connection.get_file_systems(CreationToken=name)) + + elif state == 'absent': + if not name and not fs_id: + module.fail_json(msg='Either name or id parameter is required for delete') + + changed = connection.delete_file_system(name, fs_id) + result = None + + module.exit_json(changed=changed, efs=result) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From c03e26bd20efc494868be4cd3ae90e7102a60f5d Mon Sep 17 00:00:00 2001 From: Ryan Sydnor Date: Fri, 26 Aug 2016 11:23:57 -0400 Subject: [PATCH 2115/2522] Add Amazon Elastic File System Facts (efs_facts) module Also switch EFS module to using boto3_conn from boto3.Session --- cloud/amazon/efs.py | 194 ++++++++------------ cloud/amazon/efs_facts.py | 377 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 450 insertions(+), 121 deletions(-) create mode 100644 cloud/amazon/efs_facts.py diff --git a/cloud/amazon/efs.py b/cloud/amazon/efs.py index 26bae9aaf5a..388a3e8dd89 100644 --- a/cloud/amazon/efs.py +++ b/cloud/amazon/efs.py @@ -29,21 +29,25 @@ state: description: - Allows to create, search and destroy Amazon EFS file system - required: true + required: false + default: 'present' choices: ['present', 'absent'] - aliases: [] name: description: - - Creation Token of Amazon EFS file system. Required for create. Either name or ID required for delete. It can be omitted for 'list' (unless you want to find EFS with certain name). + - Creation Token of Amazon EFS file system. Required for create. Either name or ID required for delete. required: false default: None - aliases: [] id: description: - ID of Amazon EFS. Either name or ID required for delete. required: false default: None - aliases: [] + performance_mode: + description: + - File system's performance mode to use. Only takes effect during creation. + required: false + default: 'general_purpose' + choices: ['general_purpose', 'max_io'] tags: description: - | @@ -51,18 +55,16 @@ In case of 'present' state with list of tags and existing EFS (matched by 'name'), tags of EFS will be replaced with provided data. required: false default: None - aliases: [] targets: description: - | List of mounted targets. It should be a list of dictionaries, every dictionary should include next attributes: - - SubnetId - Mandatory. The ID of the subnet to add the mount target in. - - IpAddress - Optional. A valid IPv4 address within the address range of the specified subnet. - - SecurityGroups - Optional. List of security group IDs, of the form "sg-xxxxxxxx". These must be for the same VPC as subnet specified + - subnet_id - Mandatory. The ID of the subnet to add the mount target in. + - ip_address - Optional. A valid IPv4 address within the address range of the specified subnet. + - security_groups - Optional. List of security group IDs, of the form "sg-xxxxxxxx". These must be for the same VPC as subnet specified This data may be modified for existing EFS using state 'present' and new list of mount targets. required: false default: None - aliases: [] wait: description: - | @@ -71,13 +73,11 @@ required: false default: "no" choices: ["yes", "no"] - aliases: [] wait_timeout: description: - How long the module should wait (in seconds) for desired state before returning. Zero means wait as long as necessary. required: false default: 0 - aliases: [] extends_documentation_fragment: - aws ''' @@ -88,116 +88,111 @@ state: present name: myTestEFS tags: - Name: myTestNameTag - Purpose: file-storage + name: myTestNameTag + purpose: file-storage targets: - - SubnetId: subnet-748c5d03 - SecurityGroups: [ "sg-1a2b3c4d" ] + - subnet_id: subnet-748c5d03 + security_groups: [ "sg-1a2b3c4d" ] # Modifying EFS data - efs: state: present name: myTestEFS tags: - Name: myAnotherTestTag + name: myAnotherTestTag targets: - - SubnetId: subnet-7654fdca - SecurityGroups: [ "sg-4c5d6f7a" ] + - subnet_id: subnet-7654fdca + security_groups: [ "sg-4c5d6f7a" ] # Deleting EFS - efs: state: absent name: myTestEFS - -# Searching all EFS instances with tag Name = 'myTestNameTag', in subnet 'subnet-1a2b3c4d' and with security group 'sg-4d3c2b1a' -- efs: - state: list - tags: - Name: myTestNameTag - targets: - - subnet-1a2b3c4d - - sg-4d3c2b1a - ''' RETURN = ''' -CreationTime: +creation_time: description: timestamp of creation date returned: type: datetime sample: 2015-11-16 07:30:57-05:00 -CreationToken: +creation_token: description: EFS creation token returned: type: UUID sample: console-88609e04-9a0e-4a2e-912c-feaa99509961 -FileSystemId: +file_system_id: description: ID of the file system returned: type: unique ID sample: fs-xxxxxxxx -LifeCycleState: +life_cycle_state: description: state of the EFS file system returned: type: str sample: creating, available, deleting, deleted -MountPoint: +mount_point: description: url of file system returned: type: str sample: .fs-xxxxxxxx.efs.us-west-2.amazonaws.com:/ -MountTargets: +mount_targets: description: list of mount targets returned: type: list of dicts sample: [ { - "FileSystemId": "fs-a7ad440e", - "IpAddress": "172.31.17.173", - "LifeCycleState": "available", - "MountTargetId": "fsmt-d8907871", - "NetworkInterfaceId": "eni-6e387e26", - "OwnerId": "740748460359", - "SecurityGroups": [ + "file_system_id": "fs-a7ad440e", + "ip_address": "172.31.17.173", + "life_cycle_state": "available", + "mount_target_id": "fsmt-d8907871", + "network_interface_id": "eni-6e387e26", + "owner_id": "740748460359", + "security_groups": [ "sg-a30b22c6" ], - "SubnetId": "subnet-e265c895" + "subnet_id": "subnet-e265c895" }, ... ] -Name: +name: description: name of the file system returned: type: str sample: my-efs -NumberOfMountTargets: +number_of_mount_targets: description: the number of targets mounted returned: type: int sample: 3 -OwnerId: +owner_id: description: AWS account ID of EFS owner returned: type: str sample: XXXXXXXXXXXX -SizeInBytes: +size_in_bytes: description: size of the file system in bytes as of a timestamp returned: type: dict sample: { - "Timestamp": "2015-12-21 13:59:59-05:00", - "Value": 12288 + "timestamp": "2015-12-21 13:59:59-05:00", + "value": 12288 } -Tags: +performance_mode: + description: performance mode of the file system + returned: + type: str + sample: "generalPurpose" +tags: description: tags on the efs instance returned: type: dict sample: { - "Name": "my-efs", - "Key": "Value" + "name": "my-efs", + "key": "Value" } ''' @@ -209,7 +204,7 @@ try: from botocore.exceptions import ClientError - from boto3.session import Session + import boto3 HAS_BOTO3 = True except ImportError as e: HAS_BOTO3 = False @@ -226,15 +221,11 @@ class EFSConnection(object): def __init__(self, module, region, **aws_connect_params): try: - session = Session( - aws_access_key_id=aws_connect_params['aws_access_key_id'], - aws_secret_access_key=aws_connect_params['aws_secret_access_key'], - aws_session_token=aws_connect_params['aws_session_token'], - region_name=region - ) - self.connection = session.client('efs') + self.connection = boto3_conn(module, conn_type='client', + resource='efs', region=region, + **aws_connect_params) except Exception as e: - module.fail_json(msg=repr(e)) + module.fail_json(msg="Failed to connect to AWS: %s" % str(e)) self.region = region self.wait = module.params.get('wait') @@ -345,7 +336,7 @@ def get_mount_targets_in_state(self, file_system_id, states=None): return list(targets) - def create_file_system(self, name): + def create_file_system(self, name, performance_mode): """ Creates new filesystem with selected name """ @@ -356,7 +347,7 @@ def create_file_system(self, name): lambda: self.get_file_system_state(name), self.STATE_DELETED ) - self.connection.create_file_system(CreationToken=name) + self.connection.create_file_system(CreationToken=name, PerformanceMode=performance_mode) changed = True # we always wait for the state to be available when creating. @@ -507,7 +498,7 @@ def iterate_all(attr, map_method, **kwargs): """ Method creates iterator from boto result set """ - args = dict((key, value) for (key, value) in kwargs.iteritems() if value is not None) + args = dict((key, value) for (key, value) in kwargs.items() if value is not None) wait = 1 while True: try: @@ -539,44 +530,14 @@ def dict_diff(dict1, dict2, by_key=False): """ Helper method to calculate difference of two dictionaries """ - keys1 = set(dict1.keys() if by_key else dict1.iteritems()) - keys2 = set(dict2.keys() if by_key else dict2.iteritems()) + keys1 = set(dict1.keys() if by_key else dict1.items()) + keys2 = set(dict2.keys() if by_key else dict2.items()) intersection = keys1 & keys2 return keys2 ^ intersection, intersection, keys1 ^ intersection -def group_list_of_dict(array): - """ - Helper method to group list of dict to dict with all possible values - """ - result = defaultdict(list) - for item in array: - for key, value in item.iteritems(): - result[key] += value if isinstance(value, list) else [value] - return result - - -def prefix_to_attr(attr_id): - """ - Helper method to convert ID prefix to mount target attribute - """ - attr_by_prefix = { - 'fsmt-': 'MountTargetId', - 'subnet-': 'SubnetId', - 'eni-': 'NetworkInterfaceId', - 'sg-': 'SecurityGroups' - } - prefix = first_or_default(filter( - lambda pref: str(attr_id).startswith(pref), - attr_by_prefix.keys() - )) - if prefix: - return attr_by_prefix[prefix] - return 'IpAddress' - - def first_or_default(items, default=None): """ Helper method to fetch first element of list (if exists) @@ -586,27 +547,6 @@ def first_or_default(items, default=None): return default -def has_tags(available, required): - """ - Helper method to determine if tag requested already exists - """ - for key, value in required.iteritems(): - if key not in available or value != available[key]: - return False - return True - - -def has_targets(available, required): - """ - Helper method to determine if mount tager requested already exists - """ - grouped = group_list_of_dict(available) - for (value, field) in required: - if field not in grouped or value not in grouped[field]: - return False - return True - - def wait_for(callback, value, timeout=EFSConnection.DEFAULT_WAIT_TIMEOUT_SECONDS): """ Helper method to wait for desired value returned by callback method @@ -628,11 +568,12 @@ def main(): """ argument_spec = ec2_argument_spec() argument_spec.update(dict( - state=dict(required=True, type='str', choices=["present", "absent"]), + state=dict(required=False, type='str', choices=["present", "absent"], default="present"), id=dict(required=False, type='str', default=None), name=dict(required=False, type='str', default=None), tags=dict(required=False, type="dict", default={}), targets=dict(required=False, type="list", default=[]), + performance_mode=dict(required=False, type='str', choices=["general_purpose", "max_io"], default="general_purpose"), wait=dict(required=False, type="bool", default=False), wait_timeout=dict(required=False, type="int", default=0) )) @@ -648,7 +589,17 @@ def main(): name = module.params.get('name') fs_id = module.params.get('id') tags = module.params.get('tags') - targets = module.params.get('targets') + target_translations = { + 'ip_address': 'IpAddress', + 'security_groups': 'SecurityGroups', + 'subnet_id': 'SubnetId' + } + targets = [dict((target_translations[key], value) for (key, value) in x.items()) for x in module.params.get('targets')] + performance_mode_translations = { + 'general_purpose': 'generalPurpose', + 'max_io': 'maxIO' + } + performance_mode = performance_mode_translations[module.params.get('performance_mode')] changed = False state = str(module.params.get('state')).lower() @@ -657,7 +608,7 @@ def main(): if not name: module.fail_json(msg='Name parameter is required for create') - changed = connection.create_file_system(name) + changed = connection.create_file_system(name, performance_mode) changed = connection.converge_file_system(name=name, tags=tags, targets=targets) or changed result = first_or_default(connection.get_file_systems(CreationToken=name)) @@ -667,7 +618,8 @@ def main(): changed = connection.delete_file_system(name, fs_id) result = None - + if result: + result = camel_dict_to_snake_dict(result) module.exit_json(changed=changed, efs=result) from ansible.module_utils.basic import * diff --git a/cloud/amazon/efs_facts.py b/cloud/amazon/efs_facts.py new file mode 100644 index 00000000000..1720ec5d80a --- /dev/null +++ b/cloud/amazon/efs_facts.py @@ -0,0 +1,377 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: efs_facts +short_description: Get information about Amazon EFS file systems +description: + - Module searches Amazon EFS file systems +version_added: "2.2" +requirements: [ boto3 ] +author: + - "Ryan Sydnor (@ryansydnor)" +options: + name: + description: + - Creation Token of Amazon EFS file system. + required: false + default: None + id: + description: + - ID of Amazon EFS. + required: false + default: None + tags: + description: + - | + List of tags of Amazon EFS. Should be defined as dictionary + required: false + default: None + targets: + description: + - | + List of mounted targets. It should be a list of dictionaries, every dictionary should include next attributes: + - SubnetId - Mandatory. The ID of the subnet to add the mount target in. + - IpAddress - Optional. A valid IPv4 address within the address range of the specified subnet. + - SecurityGroups - Optional. List of security group IDs, of the form "sg-xxxxxxxx". These must be for the same VPC as subnet specified. + required: false + default: None +extends_documentation_fragment: + - aws +''' + +EXAMPLES = ''' +# find all existing efs +- efs_facts: + register: result + +- efs_facts: + name: myTestNameTag + +- efs_facts: + id: fs-1234abcd + +# Searching all EFS instances with tag Name = 'myTestNameTag', in subnet 'subnet-1a2b3c4d' and with security group 'sg-4d3c2b1a' +- efs_facts: + tags: + name: myTestNameTag + targets: + - subnet-1a2b3c4d + - sg-4d3c2b1a +''' + +RETURN = ''' +creation_time: + description: timestamp of creation date + returned: + type: datetime + sample: 2015-11-16 07:30:57-05:00 +creation_token: + description: EFS creation token + returned: + type: UUID + sample: console-88609e04-9a0e-4a2e-912c-feaa99509961 +file_system_id: + description: ID of the file system + returned: + type: unique ID + sample: fs-xxxxxxxx +life_cycle_state: + description: state of the EFS file system + returned: + type: str + sample: creating, available, deleting, deleted +mount_point: + description: url of file system + returned: + type: str + sample: .fs-xxxxxxxx.efs.us-west-2.amazonaws.com:/ +mount_targets: + description: list of mount targets + returned: + type: list of dicts + sample: + [ + { + "file_system_id": "fs-a7ad440e", + "ip_address": "172.31.17.173", + "life_cycle_state": "available", + "mount_target_id": "fsmt-d8907871", + "network_interface_id": "eni-6e387e26", + "owner_id": "740748460359", + "security_groups": [ + "sg-a30b22c6" + ], + "subnet_id": "subnet-e265c895" + }, + ... + ] +name: + description: name of the file system + returned: + type: str + sample: my-efs +number_of_mount_targets: + description: the number of targets mounted + returned: + type: int + sample: 3 +owner_id: + description: AWS account ID of EFS owner + returned: + type: str + sample: XXXXXXXXXXXX +size_in_bytes: + description: size of the file system in bytes as of a timestamp + returned: + type: dict + sample: + { + "timestamp": "2015-12-21 13:59:59-05:00", + "value": 12288 + } +performance_mode: + description: performance mode of the file system + returned: + type: str + sample: "generalPurpose" +tags: + description: tags on the efs instance + returned: + type: dict + sample: + { + "name": "my-efs", + "key": "Value" + } + +''' + + +from time import sleep +from collections import defaultdict + +try: + from botocore.exceptions import ClientError + import boto3 + HAS_BOTO3 = True +except ImportError as e: + HAS_BOTO3 = False + +class EFSConnection(object): + STATE_CREATING = 'creating' + STATE_AVAILABLE = 'available' + STATE_DELETING = 'deleting' + STATE_DELETED = 'deleted' + + def __init__(self, module, region, **aws_connect_params): + try: + self.connection = boto3_conn(module, conn_type='client', + resource='efs', region=region, + **aws_connect_params) + except Exception as e: + module.fail_json(msg="Failed to connect to AWS: %s" % str(e)) + + self.region = region + + def get_file_systems(self, **kwargs): + """ + Returns generator of file systems including all attributes of FS + """ + items = iterate_all( + 'FileSystems', + self.connection.describe_file_systems, + **kwargs + ) + for item in items: + item['CreationTime'] = str(item['CreationTime']) + """ + Suffix of network path to be used as NFS device for mount. More detail here: + http://docs.aws.amazon.com/efs/latest/ug/gs-step-three-connect-to-ec2-instance.html + """ + item['MountPoint'] = '.%s.efs.%s.amazonaws.com:/' % (item['FileSystemId'], self.region) + if 'Timestamp' in item['SizeInBytes']: + item['SizeInBytes']['Timestamp'] = str(item['SizeInBytes']['Timestamp']) + if item['LifeCycleState'] == self.STATE_AVAILABLE: + item['Tags'] = self.get_tags(FileSystemId=item['FileSystemId']) + item['MountTargets'] = list(self.get_mount_targets(FileSystemId=item['FileSystemId'])) + else: + item['Tags'] = {} + item['MountTargets'] = [] + yield item + + def get_tags(self, **kwargs): + """ + Returns tag list for selected instance of EFS + """ + tags = iterate_all( + 'Tags', + self.connection.describe_tags, + **kwargs + ) + return dict((tag['Key'], tag['Value']) for tag in tags) + + def get_mount_targets(self, **kwargs): + """ + Returns mount targets for selected instance of EFS + """ + targets = iterate_all( + 'MountTargets', + self.connection.describe_mount_targets, + **kwargs + ) + for target in targets: + if target['LifeCycleState'] == self.STATE_AVAILABLE: + target['SecurityGroups'] = list(self.get_security_groups( + MountTargetId=target['MountTargetId'] + )) + else: + target['SecurityGroups'] = [] + yield target + + def get_security_groups(self, **kwargs): + """ + Returns security groups for selected instance of EFS + """ + return iterate_all( + 'SecurityGroups', + self.connection.describe_mount_target_security_groups, + **kwargs + ) + + +def iterate_all(attr, map_method, **kwargs): + """ + Method creates iterator from boto result set + """ + args = dict((key, value) for (key, value) in kwargs.items() if value is not None) + wait = 1 + while True: + try: + data = map_method(**args) + for elm in data[attr]: + yield elm + if 'NextMarker' in data: + args['Marker'] = data['Nextmarker'] + continue + break + except ClientError as e: + if e.response['Error']['Code'] == "ThrottlingException" and wait < 600: + sleep(wait) + wait = wait * 2 + continue + + +def prefix_to_attr(attr_id): + """ + Helper method to convert ID prefix to mount target attribute + """ + attr_by_prefix = { + 'fsmt-': 'MountTargetId', + 'subnet-': 'SubnetId', + 'eni-': 'NetworkInterfaceId', + 'sg-': 'SecurityGroups' + } + prefix = first_or_default(filter( + lambda pref: str(attr_id).startswith(pref), + attr_by_prefix.keys() + )) + if prefix: + return attr_by_prefix[prefix] + return 'IpAddress' + +def first_or_default(items, default=None): + """ + Helper method to fetch first element of list (if exists) + """ + for item in items: + return item + return default + +def has_tags(available, required): + """ + Helper method to determine if tag requested already exists + """ + for key, value in required.items(): + if key not in available or value != available[key]: + return False + return True + +def has_targets(available, required): + """ + Helper method to determine if mount tager requested already exists + """ + grouped = group_list_of_dict(available) + for (value, field) in required: + if field not in grouped or value not in grouped[field]: + return False + return True + +def group_list_of_dict(array): + """ + Helper method to group list of dict to dict with all possible values + """ + result = defaultdict(list) + for item in array: + for key, value in item.items(): + result[key] += value if isinstance(value, list) else [value] + return result + + +def main(): + """ + Module action handler + """ + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + id=dict(required=False, type='str', default=None), + name=dict(required=False, type='str', default=None), + tags=dict(required=False, type="dict", default={}), + targets=dict(required=False, type="list", default=[]) + )) + + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO3: + module.fail_json(msg='boto3 required for this module') + + region, _, aws_connect_params = get_aws_connection_info(module, boto3=True) + connection = EFSConnection(module, region, **aws_connect_params) + + name = module.params.get('name') + fs_id = module.params.get('id') + tags = module.params.get('tags') + targets = module.params.get('targets') + + file_systems_info = connection.get_file_systems(FileSystemId=fs_id, CreationToken=name) + + if tags: + file_systems_info = filter(lambda item: has_tags(item['Tags'], tags), file_systems_info) + + if targets: + targets = [(item, prefix_to_attr(item)) for item in targets] + file_systems_info = filter(lambda item: + has_targets(item['MountTargets'], targets), file_systems_info) + + file_systems_info = [camel_dict_to_snake_dict(x) for x in file_systems_info] + module.exit_json(changed=False, ansible_facts={'efs': file_systems_info}) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From 24da3602c6c2817485f6055a3ef0b200aef2795f Mon Sep 17 00:00:00 2001 From: "Ryan S. Brown" Date: Fri, 16 Sep 2016 17:15:40 -0400 Subject: [PATCH 2116/2522] Add lambda_event module --- cloud/amazon/lambda_event.py | 422 +++++++++++++++++++++++++++++++++++ 1 file changed, 422 insertions(+) create mode 100644 cloud/amazon/lambda_event.py diff --git a/cloud/amazon/lambda_event.py b/cloud/amazon/lambda_event.py new file mode 100644 index 00000000000..0d642734f05 --- /dev/null +++ b/cloud/amazon/lambda_event.py @@ -0,0 +1,422 @@ +#!/usr/bin/python +# (c) 2016, Pierre Jodouin +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import sys + +try: + import boto3 + from botocore.exceptions import ClientError, ParamValidationError, MissingParametersError + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + + +DOCUMENTATION = ''' +--- +module: lambda_event +short_description: Creates, updates or deletes AWS Lambda function event mappings. +description: + - This module allows the management of AWS Lambda function event source mappings such as DynamoDB and Kinesis stream + events via the Ansible framework. These event source mappings are relevant only in the AWS Lambda pull model, where + AWS Lambda invokes the function. + It is idempotent and supports "Check" mode. Use module M(lambda) to manage the lambda + function itself and M(lambda_alias) to manage function aliases. + +version_added: "2.2" + +author: Pierre Jodouin (@pjodouin), Ryan Brown (@ryansb) +options: + lambda_function_arn: + description: + - The name or ARN of the lambda function. + required: true + aliases: ['function_name', 'function_arn'] + state: + description: + - Describes the desired state. + required: true + default: "present" + choices: ["present", "absent"] + alias: + description: + - Name of the function alias. Mutually exclusive with C(version). + required: true + version: + description: + - Version of the Lambda function. Mutually exclusive with C(alias). + required: false + event_source: + description: + - Source of the event that triggers the lambda function. + required: false + default: stream + choices: ['stream'] + source_params: + description: + - Sub-parameters required for event source. + - I(== stream event source ==) + - C(source_arn) The Amazon Resource Name (ARN) of the Kinesis or DynamoDB stream that is the event source. + - C(enabled) Indicates whether AWS Lambda should begin polling the event source. Default is True. + - C(batch_size) The largest number of records that AWS Lambda will retrieve from your event source at the + time of invoking your function. Default is 100. + - C(starting_position) The position in the stream where AWS Lambda should start reading. + Choices are TRIM_HORIZON or LATEST. + required: true +requirements: + - boto3 +extends_documentation_fragment: + - aws + +''' + +EXAMPLES = ''' +--- +# Example that creates a lambda event notification for a DynamoDB stream +- hosts: localhost + gather_facts: no + vars: + state: present + tasks: + - name: DynamoDB stream event mapping + lambda_event: + state: "{{ state | default('present') }}" + event_source: stream + function_name: "{{ function_name }}" + alias: Dev + source_params: + source_arn: arn:aws:dynamodb:us-east-1:123456789012:table/tableName/stream/2016-03-19T19:51:37.457 + enabled: True + batch_size: 100 + starting_position: TRIM_HORIZON + + - name: show source event + debug: var=lambda_stream_events +''' + +RETURN = ''' +--- +lambda_stream_events: + description: list of dictionaries returned by the API describing stream event mappings + returned: success + type: list +''' + +# --------------------------------------------------------------------------------------------------- +# +# Helper Functions & classes +# +# --------------------------------------------------------------------------------------------------- + + +class AWSConnection: + """ + Create the connection object and client objects as required. + """ + + def __init__(self, ansible_obj, resources, use_boto3=True): + + try: + self.region, self.endpoint, aws_connect_kwargs = get_aws_connection_info(ansible_obj, boto3=use_boto3) + + self.resource_client = dict() + if not resources: + resources = ['lambda'] + + resources.append('iam') + + for resource in resources: + aws_connect_kwargs.update(dict(region=self.region, + endpoint=self.endpoint, + conn_type='client', + resource=resource + )) + self.resource_client[resource] = boto3_conn(ansible_obj, **aws_connect_kwargs) + + # if region is not provided, then get default profile/session region + if not self.region: + self.region = self.resource_client['lambda'].meta.region_name + + except (ClientError, ParamValidationError, MissingParametersError) as e: + ansible_obj.fail_json(msg="Unable to connect, authorize or access resource: {0}".format(e)) + + # set account ID + try: + self.account_id = self.resource_client['iam'].get_user()['User']['Arn'].split(':')[4] + except (ClientError, ValueError, KeyError, IndexError): + self.account_id = '' + + def client(self, resource='lambda'): + return self.resource_client[resource] + + +def pc(key): + """ + Changes python key into Pascale case equivalent. For example, 'this_function_name' becomes 'ThisFunctionName'. + + :param key: + :return: + """ + + return "".join([token.capitalize() for token in key.split('_')]) + + +def ordered_obj(obj): + """ + Order object for comparison purposes + + :param obj: + :return: + """ + + if isinstance(obj, dict): + return sorted((k, ordered_obj(v)) for k, v in obj.items()) + if isinstance(obj, list): + return sorted(ordered_obj(x) for x in obj) + else: + return obj + + +def set_api_sub_params(params): + """ + Sets module sub-parameters to those expected by the boto3 API. + + :param params: + :return: + """ + + api_params = dict() + + for param in params.keys(): + param_value = params.get(param, None) + if param_value: + api_params[pc(param)] = param_value + + return api_params + + +def validate_params(module, aws): + """ + Performs basic parameter validation. + + :param module: + :param aws: + :return: + """ + + function_name = module.params['lambda_function_arn'] + + # validate function name + if not re.search('^[\w\-:]+$', function_name): + module.fail_json( + msg='Function name {0} is invalid. Names must contain only alphanumeric characters and hyphens.'.format(function_name) + ) + if len(function_name) > 64: + module.fail_json(msg='Function name "{0}" exceeds 64 character limit'.format(function_name)) + + # check if 'function_name' needs to be expanded in full ARN format + if not module.params['lambda_function_arn'].startswith('arn:aws:lambda:'): + function_name = module.params['lambda_function_arn'] + module.params['lambda_function_arn'] = 'arn:aws:lambda:{0}:{1}:function:{2}'.format(aws.region, aws.account_id, function_name) + + qualifier = get_qualifier(module) + if qualifier: + function_arn = module.params['lambda_function_arn'] + module.params['lambda_function_arn'] = '{0}:{1}'.format(function_arn, qualifier) + + return + + +def get_qualifier(module): + """ + Returns the function qualifier as a version or alias or None. + + :param module: + :return: + """ + + qualifier = None + if module.params['version'] > 0: + qualifier = str(module.params['version']) + elif module.params['alias']: + qualifier = str(module.params['alias']) + + return qualifier + + +# --------------------------------------------------------------------------------------------------- +# +# Lambda Event Handlers +# +# This section defines a lambda_event_X function where X is an AWS service capable of initiating +# the execution of a Lambda function (pull only). +# +# --------------------------------------------------------------------------------------------------- + +def lambda_event_stream(module, aws): + """ + Adds, updates or deletes lambda stream (DynamoDb, Kinesis) event notifications. + :param module: + :param aws: + :return: + """ + + client = aws.client('lambda') + facts = dict() + changed = False + current_state = 'absent' + state = module.params['state'] + + api_params = dict(FunctionName=module.params['lambda_function_arn']) + + # check if required sub-parameters are present and valid + source_params = module.params['source_params'] + + source_arn = source_params.get('source_arn') + if source_arn: + api_params.update(EventSourceArn=source_arn) + else: + module.fail_json(msg="Source parameter 'source_arn' is required for stream event notification.") + + # check if optional sub-parameters are valid, if present + batch_size = source_params.get('batch_size') + if batch_size: + try: + source_params['batch_size'] = int(batch_size) + except ValueError: + module.fail_json(msg="Source parameter 'batch_size' must be an integer, found: {0}".format(source_params['batch_size'])) + + # optional boolean value needs special treatment as not present does not imply False + source_param_enabled = module.boolean(source_params.get('enabled', 'True')) + + # check if event mapping exist + try: + facts = client.list_event_source_mappings(**api_params)['EventSourceMappings'] + if facts: + current_state = 'present' + except ClientError as e: + module.fail_json(msg='Error retrieving stream event notification configuration: {0}'.format(e)) + + if state == 'present': + if current_state == 'absent': + + starting_position = source_params.get('starting_position') + if starting_position: + api_params.update(StartingPosition=starting_position) + else: + module.fail_json(msg="Source parameter 'starting_position' is required for stream event notification.") + + if source_arn: + api_params.update(Enabled=source_param_enabled) + if source_params.get('batch_size'): + api_params.update(BatchSize=source_params.get('batch_size')) + + try: + if not module.check_mode: + facts = client.create_event_source_mapping(**api_params) + changed = True + except (ClientError, ParamValidationError, MissingParametersError) as e: + module.fail_json(msg='Error creating stream source event mapping: {0}'.format(e)) + + else: + # current_state is 'present' + api_params = dict(FunctionName=module.params['lambda_function_arn']) + current_mapping = facts[0] + api_params.update(UUID=current_mapping['UUID']) + mapping_changed = False + + # check if anything changed + if source_params.get('batch_size') and source_params['batch_size'] != current_mapping['BatchSize']: + api_params.update(BatchSize=source_params['batch_size']) + mapping_changed = True + + if source_param_enabled is not None: + if source_param_enabled: + if current_mapping['State'] not in ('Enabled', 'Enabling'): + api_params.update(Enabled=True) + mapping_changed = True + else: + if current_mapping['State'] not in ('Disabled', 'Disabling'): + api_params.update(Enabled=False) + mapping_changed = True + + if mapping_changed: + try: + if not module.check_mode: + facts = client.update_event_source_mapping(**api_params) + changed = True + except (ClientError, ParamValidationError, MissingParametersError) as e: + module.fail_json(msg='Error updating stream source event mapping: {0}'.format(e)) + + else: + if current_state == 'present': + # remove the stream event mapping + api_params = dict(UUID=facts[0]['UUID']) + + try: + if not module.check_mode: + facts = client.delete_event_source_mapping(**api_params) + changed = True + except (ClientError, ParamValidationError, MissingParametersError) as e: + module.fail_json(msg='Error removing stream source event mapping: {0}'.format(e)) + + return camel_dict_to_snake_dict(dict(changed=changed, events=facts)) + + +def main(): + """Produce a list of function suffixes which handle lambda events.""" + this_module = sys.modules[__name__] + source_choices = ["stream"] + + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + state=dict(required=False, default='present', choices=['present', 'absent']), + lambda_function_arn=dict(required=True, default=None, aliases=['function_name', 'function_arn']), + event_source=dict(required=True, default="stream", choices=source_choices), + source_params=dict(type='dict', required=True, default=None), + alias=dict(required=False, default=None), + version=dict(type='int', required=False, default=0), + ) + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + mutually_exclusive=[['alias', 'version']], + required_together=[] + ) + + # validate dependencies + if not HAS_BOTO3: + module.fail_json(msg='boto3 is required for this module.') + + aws = AWSConnection(module, ['lambda']) + + validate_params(module, aws) + + this_module_function = getattr(this_module, 'lambda_event_{}'.format(module.params['event_source'].lower())) + + results = this_module_function(module, aws) + + module.exit_json(**results) + + +# ansible import module(s) kept at ~eof as recommended +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From e14cb1a06c9ed7b5db5fc12d17e562b5500bf817 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Fri, 16 Sep 2016 22:27:21 -0700 Subject: [PATCH 2117/2522] Run same tests on Shippable as on Travis. (#2948) Run the same tests as used on Travis. --- shippable.yml | 2 + test/utils/shippable/integration.sh | 2 +- test/utils/shippable/sanity-skip-python24.txt | 14 +++ test/utils/shippable/sanity-skip-python3.txt | 95 +++++++++++++++++++ test/utils/shippable/sanity-test-python24.txt | 0 test/utils/shippable/sanity.sh | 22 +++++ 6 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 test/utils/shippable/sanity-skip-python24.txt create mode 100644 test/utils/shippable/sanity-skip-python3.txt create mode 100644 test/utils/shippable/sanity-test-python24.txt create mode 100755 test/utils/shippable/sanity.sh diff --git a/shippable.yml b/shippable.yml index c392bfa675f..a2c4e33585c 100644 --- a/shippable.yml +++ b/shippable.yml @@ -27,6 +27,8 @@ matrix: - env: TEST=integration PLATFORM=freebsd VERSION=10.3-STABLE - env: TEST=integration PLATFORM=osx VERSION=10.11 + + - env: TEST=sanity INSTALL_DEPS=1 build: pre_ci_boot: options: "--privileged=false --net=bridge" diff --git a/test/utils/shippable/integration.sh b/test/utils/shippable/integration.sh index ee16e765c15..cf10e681bfb 100755 --- a/test/utils/shippable/integration.sh +++ b/test/utils/shippable/integration.sh @@ -10,7 +10,7 @@ repo="${REPO_NAME}" if [ "${is_pr}" != "true" ]; then echo "Module integration tests are only supported on pull requests." - exit 1 + exit 0 fi case "${repo}" in diff --git a/test/utils/shippable/sanity-skip-python24.txt b/test/utils/shippable/sanity-skip-python24.txt new file mode 100644 index 00000000000..5e3e5afa8d8 --- /dev/null +++ b/test/utils/shippable/sanity-skip-python24.txt @@ -0,0 +1,14 @@ +/cloud/ +/clustering/consul.*.py +/clustering/znode.py +/database/influxdb/ +/database/mssql/ +/monitoring/zabbix.*.py +/network/f5/ +/notification/pushbullet.py +/packaging/language/maven_artifact.py +/packaging/os/dnf.py +/packaging/os/layman.py +/remote_management/ipmi/ +/univention/ +/web_infrastructure/letsencrypt.py diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt new file mode 100644 index 00000000000..465597ae676 --- /dev/null +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -0,0 +1,95 @@ +/cloud/amazon/cloudtrail.py +/cloud/amazon/dynamodb_table.py +/cloud/amazon/ec2_ami_copy.py +/cloud/amazon/ec2_customer_gateway.py +/cloud/amazon/ec2_elb_facts.py +/cloud/amazon/ec2_eni.py +/cloud/amazon/ec2_eni_facts.py +/cloud/amazon/ec2_remote_facts.py +/cloud/amazon/ec2_snapshot_facts.py +/cloud/amazon/ec2_vol_facts.py +/cloud/amazon/ec2_vpc_dhcp_options_facts.py +/cloud/amazon/ec2_vpc_igw.py +/cloud/amazon/ec2_vpc_nacl.py +/cloud/amazon/ec2_vpc_net_facts.py +/cloud/amazon/ec2_vpc_route_table.py +/cloud/amazon/ec2_vpc_route_table_facts.py +/cloud/amazon/ec2_vpc_subnet.py +/cloud/amazon/ec2_vpc_subnet_facts.py +/cloud/amazon/ecs_cluster.py +/cloud/amazon/ecs_service.py +/cloud/amazon/ecs_service_facts.py +/cloud/amazon/ecs_task.py +/cloud/amazon/ecs_taskdefinition.py +/cloud/amazon/route53_facts.py +/cloud/amazon/route53_health_check.py +/cloud/amazon/route53_zone.py +/cloud/amazon/s3_bucket.py +/cloud/amazon/s3_lifecycle.py +/cloud/amazon/s3_logging.py +/cloud/amazon/sns_topic.py +/cloud/amazon/sqs_queue.py +/cloud/amazon/sts_assume_role.py +/cloud/amazon/sts_session_token.py +/cloud/centurylink/clc_aa_policy.py +/cloud/centurylink/clc_group.py +/cloud/centurylink/clc_publicip.py +/cloud/google/gce_img.py +/cloud/google/gce_tag.py +/cloud/misc/ovirt.py +/cloud/misc/proxmox.py +/cloud/misc/proxmox_template.py +/cloud/misc/virt.py +/cloud/misc/virt_net.py +/cloud/misc/virt_pool.py +/cloud/profitbricks/profitbricks.py +/cloud/profitbricks/profitbricks_volume.py +/cloud/rackspace/rax_clb_ssl.py +/cloud/xenserver_facts.py +/clustering/consul.py +/clustering/consul_acl.py +/clustering/consul_kv.py +/clustering/consul_session.py +/commands/expect.py +/database/misc/mongodb_parameter.py +/database/misc/mongodb_user.py +/database/misc/redis.py +/database/mysql/mysql_replication.py +/database/postgresql/postgresql_ext.py +/database/postgresql/postgresql_lang.py +/database/vertica/vertica_configuration.py +/database/vertica/vertica_facts.py +/database/vertica/vertica_role.py +/database/vertica/vertica_schema.py +/database/vertica/vertica_user.py +/monitoring/bigpanda.py +/monitoring/boundary_meter.py +/monitoring/circonus_annotation.py +/monitoring/datadog_monitor.py +/monitoring/rollbar_deployment.py +/monitoring/sensu_check.py +/monitoring/stackdriver.py +/monitoring/zabbix_group.py +/monitoring/zabbix_host.py +/monitoring/zabbix_hostmacro.py +/monitoring/zabbix_screen.py +/network/citrix/netscaler.py +/network/cloudflare_dns.py +/network/dnsimple.py +/network/dnsmadeeasy.py +/network/f5/bigip_facts.py +/network/f5/bigip_gtm_virtual_server.py +/network/f5/bigip_gtm_wide_ip.py +/network/f5/bigip_monitor_http.py +/network/f5/bigip_monitor_tcp.py +/network/f5/bigip_node.py +/network/f5/bigip_pool.py +/network/f5/bigip_pool_member.py +/network/f5/bigip_virtual_server.py +/network/nmcli.py +/network/openvswitch_bridge.py +/network/openvswitch_port.py +/notification/irc.py +/notification/jabber.py +/notification/mail.py +/notification/mqtt.py diff --git a/test/utils/shippable/sanity-test-python24.txt b/test/utils/shippable/sanity-test-python24.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/utils/shippable/sanity.sh b/test/utils/shippable/sanity.sh new file mode 100755 index 00000000000..5ff826efbae --- /dev/null +++ b/test/utils/shippable/sanity.sh @@ -0,0 +1,22 @@ +#!/bin/bash -eux + +source_root=$(python -c "from os import path; print(path.abspath(path.join(path.dirname('$0'), '../../..')))") + +install_deps="${INSTALL_DEPS:-}" + +cd "${source_root}" + +if [ "${install_deps}" != "" ]; then + add-apt-repository ppa:fkrull/deadsnakes && apt-get update -qq && apt-get install python2.4 -qq + + pip install git+https://github.com/ansible/ansible.git@devel#egg=ansible + pip install git+https://github.com/sivel/ansible-testing.git#egg=ansible_testing +fi + +python2.4 -m compileall -fq -i "test/utils/shippable/sanity-test-python24.txt" +python2.4 -m compileall -fq -x "($(printf %s "$(< "test/utils/shippable/sanity-skip-python24.txt"))" | tr '\n' '|')" . +python2.6 -m compileall -fq . +python2.7 -m compileall -fq . +python3.5 -m compileall -fq . -x "($(printf %s "$(< "test/utils/shippable/sanity-skip-python3.txt"))" | tr '\n' '|')" + +ansible-validate-modules --exclude '/utilities/|/shippable(/|$)' . From 0763489220f9fa5c3761fc695f11893ad08dc74e Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Fri, 16 Sep 2016 22:55:40 -0700 Subject: [PATCH 2118/2522] Cosmetic fix to test PR and merge hooks. (#2949) --- test/utils/shippable/sanity.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/shippable/sanity.sh b/test/utils/shippable/sanity.sh index 5ff826efbae..1c0e2451a43 100755 --- a/test/utils/shippable/sanity.sh +++ b/test/utils/shippable/sanity.sh @@ -13,7 +13,7 @@ if [ "${install_deps}" != "" ]; then pip install git+https://github.com/sivel/ansible-testing.git#egg=ansible_testing fi -python2.4 -m compileall -fq -i "test/utils/shippable/sanity-test-python24.txt" +python2.4 -m compileall -fq -i "test/utils/shippable/sanity-test-python24.txt" python2.4 -m compileall -fq -x "($(printf %s "$(< "test/utils/shippable/sanity-skip-python24.txt"))" | tr '\n' '|')" . python2.6 -m compileall -fq . python2.7 -m compileall -fq . From 560e773b148088a55ec6ce65ba89fb30d602dd04 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Fri, 16 Sep 2016 23:13:20 -0700 Subject: [PATCH 2119/2522] Update CI badge. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a9165059a68..7b860ba7145 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://travis-ci.org/ansible/ansible-modules-extras.svg?branch=devel)](https://travis-ci.org/ansible/ansible-modules-extras) +[![Build Status](https://api.shippable.com/projects/573f79d02a8192902e20e34f/badge?branch=devel)](https://app.shippable.com/projects/573f79d02a8192902e20e34f) ansible-modules-extras ====================== From 91c498557c70882107dba7308fabd176cf62fa3c Mon Sep 17 00:00:00 2001 From: Alexandre Garnier Date: Sat, 17 Sep 2016 08:26:52 +0200 Subject: [PATCH 2120/2522] Fix mixed type comparison resulting in wrong `changed` (#2772) When using `use_max` or `use_min` in `pam_limits`, the new value is an integer compared with the actual_value which is a string, so they are always different and the module reports a changed but none occurred. --- system/pam_limits.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/system/pam_limits.py b/system/pam_limits.py index 30a31b01b9d..8e6bdbe9695 100644 --- a/system/pam_limits.py +++ b/system/pam_limits.py @@ -212,7 +212,7 @@ def main(): if use_max: if value.isdigit() and actual_value.isdigit(): - new_value = max(int(value), int(actual_value)) + new_value = str(max(int(value), int(actual_value))) elif actual_value_unlimited: new_value = actual_value else: @@ -220,7 +220,7 @@ def main(): if use_min: if value.isdigit() and actual_value.isdigit(): - new_value = min(int(value), int(actual_value)) + new_value = str(min(int(value), int(actual_value))) elif value_unlimited: new_value = actual_value else: @@ -229,7 +229,7 @@ def main(): # Change line only if value has changed if new_value != actual_value: changed = True - new_limit = domain + "\t" + limit_type + "\t" + limit_item + "\t" + str(new_value) + new_comment + "\n" + new_limit = domain + "\t" + limit_type + "\t" + limit_item + "\t" + new_value + new_comment + "\n" message = new_limit nf.write(new_limit) else: @@ -240,7 +240,7 @@ def main(): if not found: changed = True - new_limit = domain + "\t" + limit_type + "\t" + limit_item + "\t" + str(new_value) + new_comment + "\n" + new_limit = domain + "\t" + limit_type + "\t" + limit_item + "\t" + new_value + new_comment + "\n" message = new_limit nf.write(new_limit) From 05a656791b3487950338a815894f8b247c463c0f Mon Sep 17 00:00:00 2001 From: Flavio Grossi Date: Sat, 17 Sep 2016 08:38:04 +0200 Subject: [PATCH 2121/2522] fix arg_spec type for rabbitmq_user module (#1657) From 11329c5c4d35259df5f3b6429c92f1648400c3e6 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Sat, 17 Sep 2016 18:25:04 +0200 Subject: [PATCH 2122/2522] Fix mongodb user idempotence with 2.4 version (#2725) (#2920) --- database/misc/mongodb_user.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index c17226a5d12..78d71956c7f 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -184,12 +184,29 @@ def check_compatibility(module, client): elif LooseVersion(PyMongoVersion) <= LooseVersion('2.5'): module.fail_json(msg=' (Note: you must be on mongodb 2.4+ and pymongo 2.5+ to use the roles param)') + def user_find(client, user, db_name): + """Check if the user exists. + + Args: + client (cursor): Mongodb cursor on admin database. + user (str): User to check. + db_name (str): User's database. + + Returns: + dict: when user exists, False otherwise. + """ for mongo_user in client["admin"].system.users.find(): - if mongo_user['user'] == user and mongo_user['db'] == db_name: - return mongo_user + if mongo_user['user'] == user: + # NOTE: there is no 'db' field in mongo 2.4. + if 'db' not in mongo_user: + return mongo_user + + if mongo_user["db"] == db_name: + return mongo_user return False + def user_add(module, client, db_name, user, password, roles): #pymongo's user_add is a _create_or_update_user so we won't know if it was changed or updated #without reproducing a lot of the logic in database.py of pymongo From 5b80ec833f06fbac7bcee21193f8cf2a34366faa Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sat, 17 Sep 2016 18:33:11 +0200 Subject: [PATCH 2123/2522] Fix the mysql_replication argument (#2111) Login_password and master_password are password, should not be logged. And config_file is a path t be expanded, so tagged as such. --- database/mysql/mysql_replication.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/database/mysql/mysql_replication.py b/database/mysql/mysql_replication.py index a7f366e9db8..551875a0d50 100644 --- a/database/mysql/mysql_replication.py +++ b/database/mysql/mysql_replication.py @@ -184,7 +184,7 @@ def main(): module = AnsibleModule( argument_spec = dict( login_user=dict(default=None), - login_password=dict(default=None), + login_password=dict(default=None, no_log=True), login_host=dict(default="localhost"), login_port=dict(default=3306, type='int'), login_unix_socket=dict(default=None), @@ -192,7 +192,7 @@ def main(): master_auto_position=dict(default=False, type='bool'), master_host=dict(default=None), master_user=dict(default=None), - master_password=dict(default=None), + master_password=dict(default=None, no_log=True), master_port=dict(default=None, type='int'), master_connect_retry=dict(default=None, type='int'), master_log_file=dict(default=None), @@ -206,7 +206,7 @@ def main(): master_ssl_key=dict(default=None), master_ssl_cipher=dict(default=None), connect_timeout=dict(default=30, type='int'), - config_file=dict(default="~/.my.cnf"), + config_file=dict(default="~/.my.cnf", type='path'), ssl_cert=dict(default=None), ssl_key=dict(default=None), ssl_ca=dict(default=None), @@ -238,7 +238,6 @@ def main(): ssl_ca = module.params["ssl_ca"] connect_timeout = module.params['connect_timeout'] config_file = module.params['config_file'] - 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 1e290f0f8fcc03eda2d7606d721d67d757804189 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sat, 17 Sep 2016 21:33:46 +0200 Subject: [PATCH 2124/2522] Do not import xenapi without checking, so we can avoid (#1953) backtrace --- cloud/xenserver_facts.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/cloud/xenserver_facts.py b/cloud/xenserver_facts.py index d679afce853..fdefee9f2e0 100644 --- a/cloud/xenserver_facts.py +++ b/cloud/xenserver_facts.py @@ -28,7 +28,13 @@ ''' import platform -import XenAPI + +HAVE_XENAPI = False +try: + import XenAPI + HAVE_XENAPI = True +except ImportError: + pass EXAMPLES = ''' - name: Gather facts from xenserver @@ -158,6 +164,9 @@ def get_srs(session): def main(): module = AnsibleModule({}) + if not HAVE_XENAPI: + module.fail_json(changed=False, msg="python xen api required for this module") + obj = XenServerFacts() try: session = get_xenapi_session() From 88464efdc4194c00db1f6cbe793009b151191d07 Mon Sep 17 00:00:00 2001 From: jctanner Date: Sun, 18 Sep 2016 14:57:35 -0400 Subject: [PATCH 2125/2522] vmware_guest: small refactor (#2955) * use connecion method from module_utils * use resource group related to host system Addresses #2911 --- cloud/vmware/vmware_guest.py | 60 ++++++++++++------------------------ 1 file changed, 19 insertions(+), 41 deletions(-) diff --git a/cloud/vmware/vmware_guest.py b/cloud/vmware/vmware_guest.py index 05d9525e4cc..284d070fa23 100644 --- a/cloud/vmware/vmware_guest.py +++ b/cloud/vmware/vmware_guest.py @@ -156,14 +156,11 @@ try: import pyVmomi from pyVmomi import vim - from pyVim.connect import SmartConnect, Disconnect HAS_PYVMOMI = True except ImportError: pass -import atexit import os -import ssl import string import time @@ -185,24 +182,7 @@ def __init__(self, module): self.foldermap = None def smartconnect(self): - kwargs = {'host': self.params['hostname'], - 'user': self.params['username'], - 'pwd': self.params['password']} - - if hasattr(ssl, 'SSLContext'): - context = ssl.SSLContext(ssl.PROTOCOL_TLSv1) - context.verify_mode = ssl.CERT_NONE - kwargs['sslContext'] = context - - # CONNECT TO THE SERVER - try: - self.si = SmartConnect(**kwargs) - except Exception: - err = get_exception() - self.module.fail_json(msg="Cannot connect to %s: %s" % - (kwargs['host'], err)) - atexit.register(Disconnect, self.si) - self.content = self.si.RetrieveContent() + self.content = connect_to_api(self.module) def _build_folder_tree(self, folder, tree={}, treepath=None): @@ -342,7 +322,7 @@ def getvm(self, name=None, uuid=None, folder=None, name_match=None): if searchpath: # get all objects for this path ... - fObj = self.si.content.searchIndex.FindByInventoryPath(searchpath) + fObj = self.content.searchIndex.FindByInventoryPath(searchpath) if fObj: if isinstance(fObj, vim.Datacenter): fObj = fObj.vmFolder @@ -354,6 +334,7 @@ def getvm(self, name=None, uuid=None, folder=None, name_match=None): break else: + # FIXME - this is unused if folder has a default value vmList = get_all_objs(self.content, [vim.VirtualMachine]) if name_match: if name_match == 'first': @@ -541,7 +522,6 @@ def deploy_template(self, poweron=False, wait_for_ip=False): # FIXME: cluster or hostsystem ... ? #cluster = get_obj(self.content, [vim.ClusterComputeResource], self.params['esxi']['hostname']) hostsystem = get_obj(self.content, [vim.HostSystem], self.params['esxi_hostname']) - resource_pools = get_all_objs(self.content, [vim.ResourcePool]) # set the destination datastore in the relocation spec if self.params['disk']: @@ -555,11 +535,22 @@ def deploy_template(self, poweron=False, wait_for_ip=False): # create the relocation spec relospec = vim.vm.RelocateSpec() + relospec.host = hostsystem relospec.datastore = datastore - # fixme ... use the pool from the cluster if given - relospec.pool = resource_pools[0] - relospec.host = hostsystem + # Find the associated resourcepool for the host system + # * FIXME: find resourcepool for clusters too + resource_pool = None + resource_pools = get_all_objs(self.content, [vim.ResourcePool]) + for rp in resource_pools.items(): + if rp[0].parent == hostsystem.parent: + resource_pool = rp[0] + break + if resource_pool: + relospec.pool = resource_pool + else: + self.module.fail_json(msg="Failed to find a resource group for %s" \ + % hostsystem.name) clonespec_kwargs = {} clonespec_kwargs['location'] = relospec @@ -659,7 +650,6 @@ def wait_for_vm_ip(self, vm, poll=100, sleep=5): time.sleep(sleep) thispoll += 1 - #import epdb; epdb.st() return facts @@ -819,7 +809,6 @@ def run_command_in_guest(self, vm, username, password, program_path, program_arg return result - def get_obj(content, vimtype, name): """ Return an object by name, if name is None the @@ -841,18 +830,6 @@ def get_obj(content, vimtype, name): return obj -def get_all_objs(content, vimtype): - """ - Get all the vsphere objects associated with a given type - """ - obj = [] - container = content.viewManager.CreateContainerView(content.rootFolder, vimtype, True) - for c in container.view: - obj.append(c) - container.Destroy() - return obj - - def main(): vm = None @@ -943,6 +920,7 @@ def main(): poweron=poweron, wait_for_ip=module.params['wait_for_ip_address'] ) + result['changed'] = True elif module.params['state'] == 'absent': result = {'changed': False, 'failed': False} else: @@ -958,7 +936,7 @@ def main(): module.exit_json(**result) -# this is magic, see lib/ansible/module_common.py +from ansible.module_utils.vmware import * from ansible.module_utils.basic import * if __name__ == '__main__': From 73a11be267a371712583e4136f5ceb05c6c3e679 Mon Sep 17 00:00:00 2001 From: Adrian Likins Date: Mon, 19 Sep 2016 11:23:21 -0400 Subject: [PATCH 2126/2522] Remove redundant doc keys for netapp_e_ modules (#2968) Fixes #2967 --- storage/netapp/netapp_e_auth.py | 14 -------------- storage/netapp/netapp_e_lun_mapping.py | 14 -------------- 2 files changed, 28 deletions(-) diff --git a/storage/netapp/netapp_e_auth.py b/storage/netapp/netapp_e_auth.py index a9f54257a3d..36fd7919dcf 100644 --- a/storage/netapp/netapp_e_auth.py +++ b/storage/netapp/netapp_e_auth.py @@ -26,20 +26,6 @@ version_added: "2.2" author: Kevin Hulquest (@hulquest) options: - api_username: - required: true - description: - - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API. - api_password: - required: true - description: - - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API. - api_url: - required: true - description: - - The url to the SANtricity WebServices Proxy or embedded REST API. - example: - - https://prod-1.wahoo.acme.com/devmgr/v2 validate_certs: required: false default: true diff --git a/storage/netapp/netapp_e_lun_mapping.py b/storage/netapp/netapp_e_lun_mapping.py index 439a7e4f5e5..7a4e28fcdaf 100644 --- a/storage/netapp/netapp_e_lun_mapping.py +++ b/storage/netapp/netapp_e_lun_mapping.py @@ -27,20 +27,6 @@ - Allows for the creation and removal of volume to host mappings for NetApp E-series storage arrays. version_added: "2.2" options: - api_username: - required: true - description: - - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API. - api_password: - required: true - description: - - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API. - api_url: - required: true - description: - - The url to the SANtricity WebServices Proxy or embedded REST API. - example: - - https://prod-1.wahoo.acme.com/devmgr/v2 validate_certs: required: false default: true From dbce04307defa5716bbf6810d07ccea9232a7208 Mon Sep 17 00:00:00 2001 From: Adrian Likins Date: Mon, 19 Sep 2016 11:27:07 -0400 Subject: [PATCH 2127/2522] Fix 'netapp_e_lun_mapping ... documentation error' (#2966) The 'short_description' in netapp_e_lun_mapping was a list instead of txt. This fixes errors on 'ansible-doc -l' of form: ERROR! module netapp_e_lun_mapping has a documentation error formatting or is missing documentation Fixes: #17634 (ansible/ansible) --- storage/netapp/netapp_e_lun_mapping.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/storage/netapp/netapp_e_lun_mapping.py b/storage/netapp/netapp_e_lun_mapping.py index 7a4e28fcdaf..be3c27515e5 100644 --- a/storage/netapp/netapp_e_lun_mapping.py +++ b/storage/netapp/netapp_e_lun_mapping.py @@ -21,8 +21,7 @@ --- module: netapp_e_lun_mapping author: Kevin Hulquest (@hulquest) -short_description: - - Create or Remove LUN Mappings +short_description: Create or Remove LUN Mappings description: - Allows for the creation and removal of volume to host mappings for NetApp E-series storage arrays. version_added: "2.2" From b8f782f701e42d6f1b34901130eefbb47f30614c Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Mon, 19 Sep 2016 17:37:23 +0200 Subject: [PATCH 2128/2522] Merge duplicated notes, fix warning returned by ansible-doc (#2969) --- notification/sendgrid.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/notification/sendgrid.py b/notification/sendgrid.py index 34f7ebca5bf..ac2db6b1ce7 100644 --- a/notification/sendgrid.py +++ b/notification/sendgrid.py @@ -34,6 +34,7 @@ account." - "In order to use api_key, cc, bcc, attachments, from_name, html_body, headers you must pip install sendgrid" + - "since 2.2 username and password are not required if you supply an api_key" requirements: - sendgrid python library options: @@ -104,8 +105,6 @@ required: false default: null author: "Matt Makai (@makaimc)" -notes: - - since 2.2 username and password are not required if you supply an api_key. ''' EXAMPLES = ''' From 0b778350eca6194b1881b8393b5a535f3c404f8b Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Mon, 19 Sep 2016 17:38:06 +0200 Subject: [PATCH 2129/2522] Remove duplicate key from the doc (#2970) --- cloud/amazon/redshift.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/cloud/amazon/redshift.py b/cloud/amazon/redshift.py index c5d086a6f12..8b4c942e4ac 100644 --- a/cloud/amazon/redshift.py +++ b/cloud/amazon/redshift.py @@ -121,11 +121,6 @@ required: false aliases: ['version_upgrade'] default: null - number_of_nodes: - description: - - number of the nodes the cluster should run - required: false - default: null publicly_accessible: description: - if the cluster is accessible publicly or not From a12ac1077e56740c8447f7f3d162cbc8f48218c4 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Mon, 19 Sep 2016 12:08:14 -0700 Subject: [PATCH 2130/2522] Enable python 3 sanity tests for more modules. (#2973) --- test/utils/shippable/sanity-skip-python3.txt | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt index 465597ae676..94524fa8729 100644 --- a/test/utils/shippable/sanity-skip-python3.txt +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -2,13 +2,11 @@ /cloud/amazon/dynamodb_table.py /cloud/amazon/ec2_ami_copy.py /cloud/amazon/ec2_customer_gateway.py -/cloud/amazon/ec2_elb_facts.py /cloud/amazon/ec2_eni.py /cloud/amazon/ec2_eni_facts.py /cloud/amazon/ec2_remote_facts.py /cloud/amazon/ec2_snapshot_facts.py /cloud/amazon/ec2_vol_facts.py -/cloud/amazon/ec2_vpc_dhcp_options_facts.py /cloud/amazon/ec2_vpc_igw.py /cloud/amazon/ec2_vpc_nacl.py /cloud/amazon/ec2_vpc_net_facts.py @@ -24,7 +22,6 @@ /cloud/amazon/route53_facts.py /cloud/amazon/route53_health_check.py /cloud/amazon/route53_zone.py -/cloud/amazon/s3_bucket.py /cloud/amazon/s3_lifecycle.py /cloud/amazon/s3_logging.py /cloud/amazon/sns_topic.py @@ -77,15 +74,8 @@ /network/cloudflare_dns.py /network/dnsimple.py /network/dnsmadeeasy.py -/network/f5/bigip_facts.py /network/f5/bigip_gtm_virtual_server.py /network/f5/bigip_gtm_wide_ip.py -/network/f5/bigip_monitor_http.py -/network/f5/bigip_monitor_tcp.py -/network/f5/bigip_node.py -/network/f5/bigip_pool.py -/network/f5/bigip_pool_member.py -/network/f5/bigip_virtual_server.py /network/nmcli.py /network/openvswitch_bridge.py /network/openvswitch_port.py From 99a9933026d22333a2878c6919fc34e4684a32d1 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Mon, 19 Sep 2016 12:30:27 -0700 Subject: [PATCH 2131/2522] Remove Travis config since we only use Shippable. (#2975) --- .travis.yml | 123 -------------------------------------------------- GUIDELINES.md | 2 +- 2 files changed, 1 insertion(+), 124 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 87522120ee3..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,123 +0,0 @@ -sudo: false -language: python -python: - - "2.7" -addons: - apt: - sources: - - deadsnakes - packages: - - python2.4 - - python2.6 - - python3.5 -env: - global: - - PY3_EXCLUDE_LIST="cloud/amazon/cloudtrail.py - cloud/amazon/dynamodb_table.py - cloud/amazon/ec2_ami_copy.py - cloud/amazon/ec2_customer_gateway.py - cloud/amazon/ec2_elb_facts.py - cloud/amazon/ec2_eni_facts.py - cloud/amazon/ec2_eni.py - cloud/amazon/ec2_remote_facts.py - cloud/amazon/ec2_snapshot_facts.py - cloud/amazon/ec2_vol_facts.py - cloud/amazon/ec2_vpc_igw.py - cloud/amazon/ec2_vpc_nacl.py - cloud/amazon/ec2_vpc_net_facts.py - cloud/amazon/ec2_vpc_route_table_facts.py - cloud/amazon/ec2_vpc_route_table.py - cloud/amazon/ec2_vpc_subnet_facts.py - cloud/amazon/ec2_vpc_subnet.py - cloud/amazon/ec2_vpc_dhcp_options_facts.py - cloud/amazon/ecs_cluster.py - cloud/amazon/ecs_service_facts.py - cloud/amazon/ecs_service.py - cloud/amazon/ecs_taskdefinition.py - cloud/amazon/ecs_task.py - cloud/amazon/route53_facts.py - cloud/amazon/route53_health_check.py - cloud/amazon/route53_zone.py - cloud/amazon/s3_bucket.py - cloud/amazon/s3_lifecycle.py - cloud/amazon/s3_logging.py - cloud/amazon/sns_topic.py - cloud/amazon/sqs_queue.py - cloud/amazon/sts_assume_role.py - cloud/amazon/sts_session_token.py - cloud/centurylink/clc_aa_policy.py - cloud/centurylink/clc_group.py - cloud/centurylink/clc_publicip.py - cloud/google/gce_img.py - cloud/google/gce_tag.py - cloud/misc/ovirt.py - cloud/misc/proxmox.py - cloud/misc/proxmox_template.py - cloud/misc/virt_net.py - cloud/misc/virt_pool.py - cloud/misc/virt.py - cloud/profitbricks/profitbricks.py - cloud/profitbricks/profitbricks_volume.py - cloud/rackspace/rax_clb_ssl.py - cloud/xenserver_facts.py - clustering/consul_acl.py - clustering/consul_kv.py - clustering/consul.py - clustering/consul_session.py - commands/expect.py - database/misc/mongodb_parameter.py - database/misc/mongodb_user.py - database/misc/redis.py - database/mysql/mysql_replication.py - database/postgresql/postgresql_ext.py - database/postgresql/postgresql_lang.py - database/vertica/vertica_configuration.py - database/vertica/vertica_facts.py - database/vertica/vertica_role.py - database/vertica/vertica_schema.py - database/vertica/vertica_user.py - monitoring/bigpanda.py - monitoring/boundary_meter.py - monitoring/circonus_annotation.py - monitoring/datadog_monitor.py - monitoring/rollbar_deployment.py - monitoring/sensu_check.py - monitoring/stackdriver.py - monitoring/zabbix_group.py - monitoring/zabbix_hostmacro.py - monitoring/zabbix_host.py - monitoring/zabbix_screen.py - network/citrix/netscaler.py - network/cloudflare_dns.py - network/dnsimple.py - network/dnsmadeeasy.py - network/f5/bigip_facts.py - network/f5/bigip_gtm_virtual_server.py - network/f5/bigip_gtm_wide_ip.py - network/f5/bigip_monitor_http.py - network/f5/bigip_monitor_tcp.py - network/f5/bigip_node.py - network/f5/bigip_pool_member.py - network/f5/bigip_pool.py - network/f5/bigip_virtual_server.py - network/nmcli.py - network/openvswitch_bridge.py - network/openvswitch_port.py - notification/irc.py - notification/jabber.py - notification/mail.py - notification/mqtt.py" -before_install: - - git config user.name "ansible" - - git config user.email "ansible@ansible.com" - - if [[ "$TRAVIS_PULL_REQUEST" != "false" ]]; then git rebase $TRAVIS_BRANCH; fi; -install: - - pip install git+https://github.com/ansible/ansible.git@devel#egg=ansible - - pip install git+https://github.com/sivel/ansible-testing.git#egg=ansible_testing -script: - - python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/dnf\.py|/layman\.py|/maven_artifact\.py|clustering/(consul.*|znode)\.py|notification/pushbullet\.py|database/influxdb/influxdb.*\.py|database/mssql/mssql_db\.py|/letsencrypt\.py|network/f5/bigip.*\.py|remote_management/ipmi/.*\.py|cloud/atomic/atomic_.*\.py|univention/.*\.py' . - - python2.6 -m compileall -fq . - - python2.7 -m compileall -fq . - - python3.5 -m compileall -fq . -x $(echo "$PY3_EXCLUDE_LIST"| tr ' ' '|') - - ansible-validate-modules . - #- ./test-docs.sh extras diff --git a/GUIDELINES.md b/GUIDELINES.md index 373796888ea..2589a730d46 100644 --- a/GUIDELINES.md +++ b/GUIDELINES.md @@ -64,7 +64,7 @@ If you'd like to step down as a maintainer, please submit a PR to the maintainer ## Useful tools * https://ansible.sivel.net/pr/byfile.html -- a full list of all open Pull Requests, organized by file. -* https://github.com/sivel/ansible-testing -- these are the tests that run in Travis against all PRs for extras modules, so it's a good idea to run these tests locally first. +* https://github.com/sivel/ansible-testing -- these are the tests that run on Shippable against all PRs for extras modules, so it's a good idea to run these tests locally first. ## Other Resources From ddbd63c8a696d441d4908e16b5ea166bf962fcb1 Mon Sep 17 00:00:00 2001 From: jctanner Date: Mon, 19 Sep 2016 16:25:44 -0400 Subject: [PATCH 2132/2522] vmware_guest: do not assume disk params contain a datastore (#2974) * vmware_guest: do not assume disk params contain a datastore * Fix missed line during connection refactor --- cloud/vmware/vmware_guest.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/cloud/vmware/vmware_guest.py b/cloud/vmware/vmware_guest.py index 284d070fa23..583d8b1a7d6 100644 --- a/cloud/vmware/vmware_guest.py +++ b/cloud/vmware/vmware_guest.py @@ -294,7 +294,7 @@ def getvm(self, name=None, uuid=None, folder=None, name_match=None): folder_path = None if uuid: - vm = self.si.content.searchIndex.FindByUuid(uuid=uuid, vmSearch=True) + vm = self.content.searchIndex.FindByUuid(uuid=uuid, vmSearch=True) elif folder: @@ -524,14 +524,19 @@ def deploy_template(self, poweron=False, wait_for_ip=False): hostsystem = get_obj(self.content, [vim.HostSystem], self.params['esxi_hostname']) # set the destination datastore in the relocation spec + datastore_name = None + datastore = None if self.params['disk']: - datastore_name = self.params['disk'][0]['datastore'] - datastore = get_obj(self.content, [vim.Datastore], datastore_name) - else: + if 'datastore' in self.params['disk'][0]: + datastore_name = self.params['disk'][0]['datastore'] + datastore = get_obj(self.content, [vim.Datastore], datastore_name) + if not datastore: # use the template's existing DS disks = [x for x in template.config.hardware.device if isinstance(x, vim.vm.device.VirtualDisk)] datastore = disks[0].backing.datastore datastore_name = datastore.name + if not datastore: + self.module.fail_json(msg="Failed to find a matching datastore") # create the relocation spec relospec = vim.vm.RelocateSpec() From 99c44d9640575b711672a70b781101994d689492 Mon Sep 17 00:00:00 2001 From: Rob Date: Tue, 20 Sep 2016 07:44:09 +1000 Subject: [PATCH 2133/2522] =?UTF-8?q?Modification=20of=20describe=5Fgatewa?= =?UTF-8?q?ys=20key=20so=20that=20it=20is=20consistent=20with=20w=E2=80=A6?= =?UTF-8?q?=20(#2936)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Modification of describe_gateways key so that it is consistent with what create_gateway returns. Also added AnsibleModule spec to require bgp_ip on state=present as defined in the doc * Don't remove CustomerGateways key to preserve backward compatibility --- cloud/amazon/ec2_customer_gateway.py | 38 ++++++++++++++++++---------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/cloud/amazon/ec2_customer_gateway.py b/cloud/amazon/ec2_customer_gateway.py index 072fec71178..871513d9418 100644 --- a/cloud/amazon/ec2_customer_gateway.py +++ b/cloud/amazon/ec2_customer_gateway.py @@ -44,6 +44,9 @@ required: false default: present choices: [ 'present', 'absent' ] +notes: + - Return values contain customer_gateway and customer_gateways keys which are identical dicts. You should use + customer_gateway. See U(https://github.com/ansible/ansible-modules-extras/issues/2773) for details. extends_documentation_fragment: - aws - ec2 @@ -182,18 +185,24 @@ def describe_gateways(self, ip_address): ) return response + def main(): argument_spec = ec2_argument_spec() argument_spec.update( dict( - bgp_asn = dict(required=False, type='int'), - ip_address = dict(required=True), - name = dict(required=True), - state = dict(default='present', choices=['present', 'absent']), + bgp_asn=dict(required=False, type='int'), + ip_address=dict(required=True), + name=dict(required=True), + state=dict(default='present', choices=['present', 'absent']), ) ) - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ('state', 'present', ['bgp_arn']) + ] + ) if not HAS_BOTOCORE: module.fail_json(msg='botocore is required.') @@ -208,19 +217,22 @@ def main(): name = module.params.get('name') existing = gw_mgr.describe_gateways(module.params['ip_address']) + # describe_gateways returns a key of CustomerGateways where as create_gateway returns a + # key of CustomerGateway. For consistency, change it here + existing['CustomerGateway'] = existing['CustomerGateways'] results = dict(changed=False) if module.params['state'] == 'present': - if existing['CustomerGateways']: - results['gateway']=existing - if existing['CustomerGateways'][0]['Tags']: - tag_array = existing['CustomerGateways'][0]['Tags'] + if existing['CustomerGateway']: + results['gateway'] = existing + if existing['CustomerGateway'][0]['Tags']: + tag_array = existing['CustomerGateway'][0]['Tags'] for key, value in enumerate(tag_array): if value['Key'] == 'Name': current_name = value['Value'] if current_name != name: results['name'] = gw_mgr.tag_cgw_name( - results['gateway']['CustomerGateways'][0]['CustomerGatewayId'], + results['gateway']['CustomerGateway'][0]['CustomerGatewayId'], module.params['name'], ) results['changed'] = True @@ -237,11 +249,11 @@ def main(): results['changed'] = True elif module.params['state'] == 'absent': - if existing['CustomerGateways']: - results['gateway']=existing + if existing['CustomerGateway']: + results['gateway'] = existing if not module.check_mode: results['gateway'] = gw_mgr.ensure_cgw_absent( - existing['CustomerGateways'][0]['CustomerGatewayId'] + existing['CustomerGateway'][0]['CustomerGatewayId'] ) results['changed'] = True From 7a41a90e42c1d92e274e4ee0573b4e4d90fa9c2d Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Mon, 19 Sep 2016 16:56:45 -0700 Subject: [PATCH 2134/2522] Combined notes to avoid duplicate key warning. (#2980) --- cloud/amazon/ec2_customer_gateway.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cloud/amazon/ec2_customer_gateway.py b/cloud/amazon/ec2_customer_gateway.py index 871513d9418..4e02523a700 100644 --- a/cloud/amazon/ec2_customer_gateway.py +++ b/cloud/amazon/ec2_customer_gateway.py @@ -24,6 +24,8 @@ requirements: [ botocore, boto3 ] notes: - You cannot create more than one customer gateway with the same IP address. If you run an identical request more than one time, the first request creates the customer gateway, and subsequent requests return information about the existing customer gateway. The subsequent requests do not create new customer gateway resources. + - Return values contain customer_gateway and customer_gateways keys which are identical dicts. You should use + customer_gateway. See U(https://github.com/ansible/ansible-modules-extras/issues/2773) for details. options: bgp_asn: description: @@ -44,9 +46,6 @@ required: false default: present choices: [ 'present', 'absent' ] -notes: - - Return values contain customer_gateway and customer_gateways keys which are identical dicts. You should use - customer_gateway. See U(https://github.com/ansible/ansible-modules-extras/issues/2773) for details. extends_documentation_fragment: - aws - ec2 From db7a3f48e10772bf96d422f2836fff5f1b0c751f Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Mon, 19 Sep 2016 16:59:33 -0700 Subject: [PATCH 2135/2522] Test module docs on Shippable. (#2976) --- shippable.yml | 2 + test/utils/shippable/docs-requirements.txt | 2 + test/utils/shippable/docs.sh | 55 ++++++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 test/utils/shippable/docs-requirements.txt create mode 100755 test/utils/shippable/docs.sh diff --git a/shippable.yml b/shippable.yml index a2c4e33585c..a8dd0fc5a9f 100644 --- a/shippable.yml +++ b/shippable.yml @@ -29,6 +29,8 @@ matrix: - env: TEST=integration PLATFORM=osx VERSION=10.11 - env: TEST=sanity INSTALL_DEPS=1 + + - env: TEST=docs build: pre_ci_boot: options: "--privileged=false --net=bridge" diff --git a/test/utils/shippable/docs-requirements.txt b/test/utils/shippable/docs-requirements.txt new file mode 100644 index 00000000000..4e859bb8c71 --- /dev/null +++ b/test/utils/shippable/docs-requirements.txt @@ -0,0 +1,2 @@ +jinja2 +pyyaml diff --git a/test/utils/shippable/docs.sh b/test/utils/shippable/docs.sh new file mode 100755 index 00000000000..9b5a6164f64 --- /dev/null +++ b/test/utils/shippable/docs.sh @@ -0,0 +1,55 @@ +#!/bin/bash -eux + +set -o pipefail + +ansible_repo_url="https://github.com/ansible/ansible.git" + +build_dir="${SHIPPABLE_BUILD_DIR}" +repo="${REPO_NAME}" + +case "${repo}" in + "ansible-modules-core") + this_module_group="core" + other_module_group="extras" + ;; + "ansible-modules-extras") + this_module_group="extras" + other_module_group="core" + ;; + *) + echo "Unsupported repo name: ${repo}" + exit 1 + ;; +esac + +modules_tmp_dir="${build_dir}.tmp" +this_modules_dir="${build_dir}/lib/ansible/modules/${this_module_group}" +other_modules_dir="${build_dir}/lib/ansible/modules/${other_module_group}" + +cd / +mv "${build_dir}" "${modules_tmp_dir}" +git clone "${ansible_repo_url}" "${build_dir}" +cd "${build_dir}" +rmdir "${this_modules_dir}" +mv "${modules_tmp_dir}" "${this_modules_dir}" +mv "${this_modules_dir}/shippable" "${build_dir}" +git submodule init "${other_modules_dir}" +git submodule sync "${other_modules_dir}" +git submodule update "${other_modules_dir}" + +pip install -r lib/ansible/modules/${this_module_group}/test/utils/shippable/docs-requirements.txt --upgrade +pip list + +source hacking/env-setup + +PAGER=/bin/cat \ + ANSIBLE_DEPRECATION_WARNINGS=false \ + bin/ansible-doc -l \ + 2>/tmp/ansible-doc.err + +if [ -s /tmp/ansible-doc.err ]; then + # report warnings as errors + echo "Output from 'ansible-doc -l' on stderr is considered an error:" + cat /tmp/ansible-doc.err + exit 1 +fi From bbe7f73f89d2b5a2cc26fa1c08b5a7c76de75ca8 Mon Sep 17 00:00:00 2001 From: Lujeni Date: Tue, 20 Sep 2016 16:19:53 +0200 Subject: [PATCH 2136/2522] Improve mongodb_user exception (#2962) - Better error message --- database/misc/mongodb_user.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index 78d71956c7f..551b565088e 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -350,7 +350,7 @@ def main(): module.fail_json(msg='The localhost login exception only allows the first admin account to be created') #else: this has to be the first admin user added - except ConnectionFailure, e: + except Exception, e: module.fail_json(msg='unable to connect to database: %s' % str(e)) check_compatibility(module, client) @@ -370,7 +370,7 @@ def main(): module.exit_json(changed=True, user=user) user_add(module, client, db_name, user, password, roles) - except OperationFailure, e: + except Exception, e: module.fail_json(msg='Unable to add or update user: %s' % str(e)) # Here we can check password change if mongo provide a query for that : https://jira.mongodb.org/browse/SERVER-22848 @@ -381,7 +381,7 @@ def main(): elif state == 'absent': try: user_remove(module, client, db_name, user) - except OperationFailure, e: + except Exception, e: module.fail_json(msg='Unable to remove user: %s' % str(e)) module.exit_json(changed=True, user=user) From 1f2319c3f30a12c6193b9a3e663e7ef92f4c91ea Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Tue, 20 Sep 2016 11:43:11 -0700 Subject: [PATCH 2137/2522] Remove script previously used by Travis. --- test-docs.sh | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100755 test-docs.sh diff --git a/test-docs.sh b/test-docs.sh deleted file mode 100755 index 76297fbada6..00000000000 --- a/test-docs.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/sh -set -x - -CHECKOUT_DIR=".ansible-checkout" -MOD_REPO="$1" - -# Hidden file to avoid the module_formatter recursing into the checkout -git clone https://github.com/ansible/ansible "$CHECKOUT_DIR" -cd "$CHECKOUT_DIR" -git submodule update --init -rm -rf "lib/ansible/modules/$MOD_REPO" -ln -s "$TRAVIS_BUILD_DIR/" "lib/ansible/modules/$MOD_REPO" - -pip install -U Jinja2 PyYAML setuptools six pycrypto sphinx - -. ./hacking/env-setup -PAGER=/bin/cat bin/ansible-doc -l -if [ $? -ne 0 ] ; then - exit $? -fi -make -C docsite From dd8c01f8ca01181ee6eda7600b13bd52403ddc29 Mon Sep 17 00:00:00 2001 From: perbly Date: Wed, 21 Sep 2016 18:14:36 +0200 Subject: [PATCH 2138/2522] added enabled vlan functionality for f5 vip (#2988) * added vlan functionality for f5 vip * line 91, : was missing after description --- network/f5/bigip_virtual_server.py | 32 +++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/network/f5/bigip_virtual_server.py b/network/f5/bigip_virtual_server.py index ce4c1745360..89d25103f6e 100644 --- a/network/f5/bigip_virtual_server.py +++ b/network/f5/bigip_virtual_server.py @@ -86,6 +86,12 @@ - List of rules to be applied in priority order required: false default: None + all_enabled_vlans: + version_added: "2.2" + description: + - List of vlans to be enabled + required: false + default: None pool: description: - Default pool for the virtual server @@ -126,6 +132,8 @@ all_profiles: - http - clientssl + all_enabled_vlans: + - /Common/vlan2 delegate_to: localhost - name: Modify Port of the Virtual Server @@ -250,7 +258,7 @@ def set_rules(api, name, rules_list): updated = True return updated except bigsuds.OperationFailed as e: - raise Exception('Error on setting profiles : %s' % e) + raise Exception('Error on setting rules : %s' % e) def get_profiles(api, name): @@ -289,6 +297,24 @@ def set_profiles(api, name, profiles_list): except bigsuds.OperationFailed as e: raise Exception('Error on setting profiles : %s' % e) +def set_enabled_vlans(api, name, vlans_enabled_list): + updated = False + try: + if vlans_enabled_list is None: + return False + + to_add_vlans = [] + for x in vlans_enabled_list: + to_add_vlans.append(x) + + api.LocalLB.VirtualServer.set_vlan( + virtual_servers=[name], + vlans = [{ 'state':'STATE_ENABLED', 'vlans':[to_add_vlans] }] + ) + updated = True + return updated + except bigsuds.OperationFailed as e: + raise Exception('Error on setting enabled vlans : %s' % e) def set_snat(api, name, snat): updated = False @@ -462,6 +488,7 @@ def main(): port=dict(type='int'), all_profiles=dict(type='list'), all_rules=dict(type='list'), + all_enabled_vlans=dict(type='list'), pool=dict(type='str'), description=dict(type='str'), snat=dict(type='str'), @@ -494,6 +521,7 @@ def main(): port = module.params['port'] all_profiles = fq_list_names(partition, module.params['all_profiles']) all_rules = fq_list_names(partition, module.params['all_rules']) + all_enabled_vlans = fq_list_names(partition, module.params['all_enabled_vlans']) pool = fq_name(partition, module.params['pool']) description = module.params['description'] snat = module.params['snat'] @@ -537,6 +565,7 @@ def main(): try: vs_create(api, name, destination, port, pool) set_profiles(api, name, all_profiles) + set_enabled_vlans(api, name, all_enabled_vlans) set_rules(api, name, all_rules) set_snat(api, name, snat) set_description(api, name, description) @@ -562,6 +591,7 @@ def main(): result['changed'] |= set_description(api, name, description) result['changed'] |= set_snat(api, name, snat) result['changed'] |= set_profiles(api, name, all_profiles) + result['changed'] |= set_enabled_vlans(api, name, all_enabled_vlans) result['changed'] |= set_rules(api, name, all_rules) result['changed'] |= set_default_persistence_profiles(api, name, default_persistence_profile) result['changed'] |= set_state(api, name, state) From 87949afbfaaa7bb9ac74b0c19f43e54e0a71cf13 Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Wed, 21 Sep 2016 18:48:05 +0200 Subject: [PATCH 2139/2522] Ovirt auth/vms/disks modules (#2836) * Add oVirt module to manage VMs This patch add oVirt module to manage Virtual Machines * Add oVirt module to manage authentication This patch add oVirt module to manage authentication * Add oVirt module to manage disks * Added VM state management and fixups --- cloud/ovirt/__init__.py | 0 cloud/ovirt/ovirt_auth.py | 233 +++++++++++ cloud/ovirt/ovirt_disks.py | 316 +++++++++++++++ cloud/ovirt/ovirt_vms.py | 804 +++++++++++++++++++++++++++++++++++++ 4 files changed, 1353 insertions(+) create mode 100644 cloud/ovirt/__init__.py create mode 100644 cloud/ovirt/ovirt_auth.py create mode 100644 cloud/ovirt/ovirt_disks.py create mode 100644 cloud/ovirt/ovirt_vms.py diff --git a/cloud/ovirt/__init__.py b/cloud/ovirt/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cloud/ovirt/ovirt_auth.py b/cloud/ovirt/ovirt_auth.py new file mode 100644 index 00000000000..22abf1d8394 --- /dev/null +++ b/cloud/ovirt/ovirt_auth.py @@ -0,0 +1,233 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +try: + import ovirtsdk4 as sdk +except ImportError: + pass + + +DOCUMENTATION = ''' +--- +module: ovirt_auth +short_description: "Module to manage authentication to oVirt." +author: "Ondra Machacek (@machacekondra)" +version_added: "2.2" +description: + - "This module authenticates to oVirt engine and creates SSO token, which should be later used in + all other oVirt modules, so all modules don't need to perform login and logout. + This module returns an Ansible fact called I(ovirt_auth). Every module can use this + fact as C(auth) parameter, to perform authentication." +options: + state: + default: present + choices: ['present', 'absent'] + description: + - "Specifies if a token should be created or revoked." + username: + required: True + description: + - "The name of the user. For example: I(admin@internal)." + password: + required: True + description: + - "The password of the user." + url: + required: True + description: + - "A string containing the base URL of the server. + For example: I(https://server.example.com/ovirt-engine/api)." + insecure: + required: False + description: + - "A boolean flag that indicates if the server TLS certificate and host name should be checked." + ca_file: + required: False + description: + - "A PEM file containing the trusted CA certificates. The + certificate presented by the server will be verified using these CA + certificates. If C(ca_file) parameter is not set, system wide + CA certificate store is used." + timeout: + required: False + description: + - "The maximum total time to wait for the response, in + seconds. A value of zero (the default) means wait forever. If + the timeout expires before the response is received an exception + will be raised." + compress: + required: False + description: + - "A boolean flag indicating if the SDK should ask + the server to send compressed responses. The default is I(True). + Note that this is a hint for the server, and that it may return + uncompressed data even when this parameter is set to I(True)." + kerberos: + required: False + description: + - "A boolean flag indicating if Kerberos authentication + should be used instead of the default basic authentication." +notes: + - "Everytime you use ovirt_auth module to obtain ticket, you need to also revoke the ticket, + when you no longer need it, otherwise the ticket would be revoked by engine when it expires. + For an example of how to achieve that, please take a look at I(examples) section." +''' + +EXAMPLES = ''' +tasks: + - block: + # Create a vault with `ovirt_password` variable which store your + # oVirt user's password, and include that yaml file with variable: + - include_vars: ovirt_password.yml + + # Always be sure to pass 'no_log: true' to ovirt_auth task, + # so the oVirt user's password is not logged: + - name: Obtain SSO token with using username/password credentials: + no_log: true + ovirt_auth: + url: https://ovirt.example.com/ovirt-engine/api + username: admin@internal + ca_file: ca.pem + password: "{{ ovirt_password }}" + + # Previous task generated I(ovirt_auth) fact, which you can later use + # in different modules as follows: + - ovirt_vms: + auth: "{{ ovirt_auth }}" + state: absent + name: myvm + + always: + - name: Always revoke the SSO token + ovirt_auth: + state: absent + ovirt_auth: {{ ovirt_auth }} +''' + +RETURN = ''' +ovirt_auth: + description: Authentication facts, needed to perform authentication to oVirt. + returned: success + type: dictionary + contains: + token: + description: SSO token which is used for connection to oVirt engine. + returned: success + type: string + sample: "kdfVWp9ZgeewBXV-iq3Js1-xQJZPSEQ334FLb3eksoEPRaab07DhZ8ED8ghz9lJd-MQ2GqtRIeqhvhCkrUWQPw" + url: + description: URL of the oVirt engine API endpoint. + returned: success + type: string + sample: "https://ovirt.example.com/ovirt-engine/api" + ca_file: + description: CA file, which is used to verify SSL/TLS connection. + returned: success + type: string + sample: "ca.pem" + insecure: + description: Flag indicating if insecure connection is used. + returned: success + type: bool + sample: False + timeout: + description: Number of seconds to wait for response. + returned: success + type: int + sample: 0 + compress: + description: Flag indicating if compression is used for connection. + returned: success + type: bool + sample: True + kerberos: + description: Flag indicating if kerberos is used for authentication. + returned: success + type: bool + sample: False +''' + + +def main(): + module = AnsibleModule( + argument_spec=dict( + url=dict(default=None), + username=dict(default=None), + password=dict(default=None), + ca_file=dict(default=None), + insecure=dict(required=False, type='bool', default=False), + timeout=dict(required=False, type='int', default=0), + compress=dict(required=False, type='bool', default=True), + kerberos=dict(required=False, type='bool', default=False), + state=dict(default='present', choices=['present', 'absent']), + ovirt_auth=dict(required=None, type='dict'), + ), + required_if=[ + ('state', 'absent', ['ovirt_auth']), + ('state', 'present', ['username', 'password', 'url']), + ], + ) + check_sdk(module) + + state = module.params.get('state') + if state == 'present': + params = module.params + elif state == 'absent': + params = module.params['ovirt_auth'] + + connection = sdk.Connection( + url=params.get('url'), + username=params.get('username'), + password=params.get('password'), + ca_file=params.get('ca_file'), + insecure=params.get('insecure'), + timeout=params.get('timeout'), + compress=params.get('compress'), + kerberos=params.get('kerberos'), + token=params.get('token'), + ) + try: + token = connection.authenticate() + module.exit_json( + changed=False, + ansible_facts=dict( + ovirt_auth=dict( + token=token, + url=params.get('url'), + ca_file=params.get('ca_file'), + insecure=params.get('insecure'), + timeout=params.get('timeout'), + compress=params.get('compress'), + kerberos=params.get('kerberos'), + ) if state == 'present' else dict() + ) + ) + except Exception as e: + module.fail_json(msg="Error: %s" % e) + finally: + # Close the connection, but don't revoke token + connection.close(logout=state == 'absent') + + +from ansible.module_utils.basic import * +from ansible.module_utils.ovirt import * +if __name__ == "__main__": + main() diff --git a/cloud/ovirt/ovirt_disks.py b/cloud/ovirt/ovirt_disks.py new file mode 100644 index 00000000000..a8f84c26e83 --- /dev/null +++ b/cloud/ovirt/ovirt_disks.py @@ -0,0 +1,316 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +try: + import ovirtsdk4 as sdk + import ovirtsdk4.types as otypes +except ImportError: + pass + +from ansible.module_utils.ovirt import * + + +DOCUMENTATION = ''' +--- +module: ovirt_disks +short_description: "Module to manage Virtual Machine and floating disks in oVirt." +version_added: "2.2" +author: "Ondra Machacek (@machacekondra)" +description: + - "Module to manage Virtual Machine and floating disks in oVirt." +options: + id: + description: + - "ID of the disk to manage. Either C(id) or C(name) is required." + name: + description: + - "Name of the disk to manage. Either C(id) or C(name)/C(alias) is required." + aliases: ['alias'] + vm_name: + description: + - "Name of the Virtual Machine to manage. Either C(vm_id) or C(vm_name) is required if C(state) is I(attached) or I(detached)." + vm_id: + description: + - "ID of the Virtual Machine to manage. Either C(vm_id) or C(vm_name) is required if C(state) is I(attached) or I(detached)." + state: + description: + - "Should the Virtual Machine disk be present/absent/attached/detached." + choices: ['present', 'absent', 'attached', 'detached'] + default: 'present' + size: + description: + - "Size of the disk. Size should be specified using IEC standard units. For example 10GiB, 1024MiB, etc." + interface: + description: + - "Driver of the storage interface." + choices: ['virtio', 'ide', 'virtio_scsi'] + default: 'virtio' + format: + description: + - "Format of the disk. Either copy-on-write or raw." + choices: ['raw', 'cow'] + storage_domain: + description: + - "Storage domain name where disk should be created. By default storage is chosen by oVirt engine." + profile: + description: + - "Disk profile name to be attached to disk. By default profile is chosen by oVirt engine." + bootable: + description: + - "I(True) if the disk should be bootable. By default when disk is created it isn't bootable." + shareable: + description: + - "I(True) if the disk should be shareable. By default when disk is created it isn't shareable." + logical_unit: + description: + - "Dictionary which describes LUN to be directly attached to VM:" + - "C(address) - Address of the storage server. Used by iSCSI." + - "C(port) - Port of the storage server. Used by iSCSI." + - "C(target) - iSCSI target." + - "C(lun_id) - LUN id." + - "C(username) - CHAP Username to be used to access storage server. Used by iSCSI." + - "C(password) - CHAP Password of the user to be used to access storage server. Used by iSCSI." + - "C(storage_type) - Storage type either I(fcp) or I(iscsi)." +extends_documentation_fragment: ovirt +''' + + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Create and attach new disk to VM +- ovirt_disks: + name: myvm_disk + vm_name: rhel7 + size: 10GiB + format: cow + interface: virtio + +# Attach logical unit to VM rhel7 +- ovirt_disks: + vm_name: rhel7 + logical_unit: + target: iqn.2016-08-09.brq.str-01:omachace + id: 1IET_000d0001 + address: 10.34.63.204 + interface: virtio + +# Detach disk from VM +- ovirt_disks: + state: detached + name: myvm_disk + vm_name: rhel7 + size: 10GiB + format: cow + interface: virtio +''' + + +RETURN = ''' +id: + description: "ID of the managed disk" + returned: "On success if disk is found." + type: str + sample: 7de90f31-222c-436c-a1ca-7e655bd5b60c +disk: + description: "Dictionary of all the disk attributes. Disk attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/disk." + returned: "On success if disk is found and C(vm_id) or C(vm_name) wasn't passed." + +disk_attachment: + description: "Dictionary of all the disk attachment attributes. Disk attachment attributes can be found + on your oVirt instance at following url: + https://ovirt.example.com/ovirt-engine/api/model#types/disk_attachment." + returned: "On success if disk is found and C(vm_id) or C(vm_name) was passed and VM was found." +''' + + + +def _search_by_lun(disks_service, lun_id): + """ + Find disk by LUN ID. + """ + res = [ + disk for disk in disks_service.list(search='disk_type=lun') if ( + disk.lun_storage.id == lun_id + ) + ] + return res[0] if res else None + + +class DisksModule(BaseModule): + + def build_entity(self): + logical_unit = self._module.params.get('logical_unit') + return otypes.Disk( + id=self._module.params.get('id'), + name=self._module.params.get('name'), + description=self._module.params.get('description'), + format=otypes.DiskFormat( + self._module.params.get('format') + ) if self._module.params.get('format') else None, + provisioned_size=convert_to_bytes( + self._module.params.get('size') + ), + storage_domains=[ + otypes.StorageDomain( + name=self._module.params.get('storage_domain'), + ), + ], + shareable=self._module.params.get('shareable'), + lun_storage=otypes.HostStorage( + type=otypes.StorageType( + logical_unit.get('storage_type', 'iscsi') + ), + logical_units=[ + otypes.LogicalUnit( + address=logical_unit.get('address'), + port=logical_unit.get('port', 3260), + target=logical_unit.get('target'), + id=logical_unit.get('id'), + username=logical_unit.get('username'), + password=logical_unit.get('password'), + ) + ], + ) if logical_unit else None, + ) + + def update_check(self, entity): + return ( + equal(self._module.params.get('description'), entity.description) and + equal(convert_to_bytes(self._module.params.get('size')), entity.provisioned_size) and + equal(self._module.params.get('format'), str(entity.format)) and + equal(self._module.params.get('shareable'), entity.shareable) + ) + + +class DiskAttachmentsModule(DisksModule): + + def build_entity(self): + return otypes.DiskAttachment( + disk=super(DiskAttachmentsModule, self).build_entity(), + interface=otypes.DiskInterface( + self._module.params.get('interface') + ) if self._module.params.get('interface') else None, + bootable=self._module.params.get('bootable'), + active=True, + ) + + def update_check(self, entity): + return ( + equal(self._module.params.get('interface'), str(entity.interface)) and + equal(self._module.params.get('bootable'), entity.bootable) + ) + + +def main(): + argument_spec = ovirt_full_argument_spec( + state=dict( + choices=['present', 'absent', 'attached', 'detached'], + default='present' + ), + id=dict(default=None), + name=dict(default=None, aliases=['alias']), + vm_name=dict(default=None), + vm_id=dict(default=None), + size=dict(default=None), + interface=dict(default=None,), + allocation_policy=dict(default=None), + storage_domain=dict(default=None), + profile=dict(default=None), + format=dict(default=None, choices=['raw', 'cow']), + bootable=dict(default=None, type='bool'), + shareable=dict(default=None, type='bool'), + logical_unit=dict(default=None, type='dict'), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + check_sdk(module) + check_params(module) + + try: + disk = None + state = module.params['state'] + connection = create_connection(module.params.pop('auth')) + disks_service = connection.system_service().disks_service() + disks_module = DisksModule( + connection=connection, + module=module, + service=disks_service, + ) + + lun = module.params.get('logical_unit') + if lun: + disk = _search_by_lun(disks_service, lun.get('id')) + + ret = None + # First take care of creating the VM, if needed: + if state == 'present' or state == 'detached' or state == 'attached': + ret = disks_module.create( + entity=disk, + result_state=otypes.DiskStatus.OK if lun is None else None, + ) + # We need to pass ID to the module, so in case we want detach/attach disk + # we have this ID specified to attach/detach method: + module.params['id'] = ret['id'] if disk is None else disk.id + elif state == 'absent': + ret = disks_module.remove() + + # If VM was passed attach/detach disks to/from the VM: + if 'vm_id' in module.params or 'vm_name' in module.params and state != 'absent': + vms_service = connection.system_service().vms_service() + + # If `vm_id` isn't specified, find VM by name: + vm_id = module.params['vm_id'] + if vm_id is None: + vm_id = getattr(search_by_name(vms_service, module.params['vm_name']), 'id', None) + + if vm_id is None: + module.fail_json( + msg="VM don't exists, please create it first." + ) + + disk_attachments_service = vms_service.vm_service(vm_id).disk_attachments_service() + disk_attachments_module = DiskAttachmentsModule( + connection=connection, + module=module, + service=disk_attachments_service, + changed=ret['changed'] if ret else False, + ) + + if state == 'present' or state == 'attached': + ret = disk_attachments_module.create() + elif state == 'detached': + ret = disk_attachments_module.remove() + + module.exit_json(**ret) + except Exception as e: + module.fail_json(msg=str(e)) + finally: + connection.close(logout=False) + + +from ansible.module_utils.basic import * +if __name__ == "__main__": + main() diff --git a/cloud/ovirt/ovirt_vms.py b/cloud/ovirt/ovirt_vms.py new file mode 100644 index 00000000000..8aa4a98364b --- /dev/null +++ b/cloud/ovirt/ovirt_vms.py @@ -0,0 +1,804 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +try: + import ovirtsdk4 as sdk + import ovirtsdk4.types as otypes +except ImportError: + pass + +from ansible.module_utils.ovirt import * + + +DOCUMENTATION = ''' +--- +module: ovirt_vms +short_description: "Module to manage Virtual Machines in oVirt." +version_added: "2.2" +author: "Ondra Machacek (@machacekondra)" +description: + - "This module manages whole lifecycle of the Virtual Machine(VM) in oVirt. Since VM can hold many states in oVirt, + this see notes to see how the states of the VM are handled." +options: + name: + description: + - "Name of the the Virtual Machine to manage. If VM don't exists C(name) is required. + Otherwise C(id) or C(name) can be used." + id: + description: + - "ID of the the Virtual Machine to manage." + state: + description: + - "Should the Virtual Machine be running/stopped/present/absent/suspended/next_run." + - "I(present) and I(running) are equal states." + - "I(next_run) state updates the VM and if the VM has next run configuration it will be rebooted." + - "Please check I(notes) to more detailed description of states." + choices: ['running', 'stopped', 'present', 'absent', 'suspended', 'next_run'] + default: present + cluster: + description: + - "Name of the cluster, where Virtual Machine should be created. Required if creating VM." + template: + description: + - "Name of the template, which should be used to create Virtual Machine. Required if creating VM." + - "If template is not specified and VM doesn't exist, VM will be created from I(Blank) template." + memory: + description: + - "Amount of memory of the Virtual Machine. Prefix uses IEC 60027-2 standard (for example 1GiB, 1024MiB)." + - "Default value is set by engine." + memory_guaranteed: + description: + - "Amount of minimal guaranteed memory of the Virtual Machine. + Prefix uses IEC 60027-2 standard (for example 1GiB, 1024MiB)." + - "C(memory_guaranteed) parameter can't be lower than C(memory) parameter. Default value is set by engine." + cpu_shares: + description: + - "Set a CPU shares for this Virtual Machine. Default value is set by oVirt engine." + cpu_cores: + description: + - "Number of virtual CPUs cores of the Virtual Machine. Default value is set by oVirt engine." + cpu_sockets: + description: + - "Number of virtual CPUs sockets of the Virtual Machine. Default value is set by oVirt engine." + type: + description: + - "Type of the Virtual Machine. Default value is set by oVirt engine." + choices: [server, desktop] + operating_system: + description: + - "Operating system of the Virtual Machine. Default value is set by oVirt engine." + choices: [ + rhel_6_ppc64, other, freebsd, windows_2003x64, windows_10, rhel_6x64, rhel_4x64, windows_2008x64, + windows_2008R2x64, debian_7, windows_2012x64, ubuntu_14_04, ubuntu_12_04, ubuntu_13_10, windows_8x64, + other_linux_ppc64, windows_2003, other_linux, windows_10x64, windows_2008, rhel_3, rhel_5, rhel_4, + other_ppc64, sles_11, rhel_6, windows_xp, rhel_7x64, freebsdx64, rhel_7_ppc64, windows_7, rhel_5x64, + ubuntu_14_04_ppc64, sles_11_ppc64, windows_8, windows_2012R2x64, windows_2008r2x64, ubuntu_13_04, + ubuntu_12_10, windows_7x64 + ] + boot_devices: + description: + - "List of boot devices which should be used to boot. Choices I(network), I(hd) and I(cdrom)." + - "For example: ['cdrom', 'hd']. Default value is set by oVirt engine." + host: + description: + - "Specify host where Virtual Machine should be running. By default the host is chosen by engine scheduler." + - "This parameter is used only when C(state) is I(running) or I(present)." + high_availability: + description: + - "If I(True) Virtual Machine will be set as highly available." + - "If I(False) Virtual Machine won't be set as highly available." + - "If no value is passed, default value is set by oVirt engine." + delete_protected: + description: + - "If I(True) Virtual Machine will be set as delete protected." + - "If I(False) Virtual Machine won't be set as delete protected." + - "If no value is passed, default value is set by oVirt engine." + stateless: + description: + - "If I(True) Virtual Machine will be set as stateless." + - "If I(False) Virtual Machine will be unset as stateless." + - "If no value is passed, default value is set by oVirt engine." + clone: + description: + - "If I(True) then the disks of the created virtual machine will be cloned and independent of the template." + - "This parameter is used only when C(state) is I(running) or I(present) and VM didn't exist before." + default: False + clone_permissions: + description: + - "If I(True) then the permissions of the template (only the direct ones, not the inherited ones) + will be copied to the created virtual machine." + - "This parameter is used only when C(state) is I(running) or I(present) and VM didn't exist before." + default: False + cd_iso: + description: + - "ISO file from ISO storage domain which should be attached to Virtual Machine." + - "If you pass empty string the CD will be ejected from VM." + - "If used with C(state) I(running) or I(present) and VM is running the CD will be attached to VM." + - "If used with C(state) I(running) or I(present) and VM is down the CD will be attached to VM persistently." + force: + description: + - "Please check to I(Synopsis) to more detailed description of force parameter, it can behave differently + in different situations." + default: False + nics: + description: + - "List of NICs, which should be attached to Virtual Machine. NIC is described by following dictionary:" + - "C(name) - Name of the NIC." + - "C(profile_name) - Profile name where NIC should be attached." + - "C(interface) - Type of the network interface. One of following: I(virtio), I(e1000), I(rtl8139), default is I(virtio)." + - "C(mac_address) - Custom MAC address of the network interface, by default it's obtained from MAC pool." + - "C(Note:)" + - "This parameter is used only when C(state) is I(running) or I(present) and is able to only create NICs. + To manage NICs of the VM in more depth please use M(ovirt_nics) module instead." + disks: + description: + - "List of disks, which should be attached to Virtual Machine. Disk is described by following dictionary:" + - "C(name) - Name of the disk. Either C(name) or C(id) is reuqired." + - "C(id) - ID of the disk. Either C(name) or C(id) is reuqired." + - "C(interface) - Interface of the disk, either I(virtio) or I(IDE), default is I(virtio)." + - "C(bootable) - I(True) if the disk should be bootable, default is non bootable." + - "C(activate) - I(True) if the disk should be activated, default is activated." + - "C(Note:)" + - "This parameter is used only when C(state) is I(running) or I(present) and is able to only attach disks. + To manage disks of the VM in more depth please use M(ovirt_disks) module instead." + sysprep: + description: + - "Dictionary with values for Windows Virtual Machine initialization using sysprep:" + - "C(host_name) - Hostname to be set to Virtual Machine when deployed." + - "C(active_directory_ou) - Active Directory Organizational Unit, to be used for login of user." + - "C(org_name) - Organization name to be set to Windows Virtual Machine." + - "C(domain) - Domain to be set to Windows Virtual Machine." + - "C(timezone) - Timezone to be set to Windows Virtual Machine." + - "C(ui_language) - UI language of the Windows Virtual Machine." + - "C(system_locale) - System localization of the Windows Virtual Machine." + - "C(input_locale) - Input localization of the Windows Virtual Machine." + - "C(windows_license_key) - License key to be set to Windows Virtual Machine." + - "C(user_name) - Username to be used for set password to Windows Virtual Machine." + - "C(root_password) - Password to be set for username to Windows Virtual Machine." + cloud_init: + description: + - "Dictionary with values for Unix-like Virtual Machine initialization using cloud init:" + - "C(host_name) - Hostname to be set to Virtual Machine when deployed." + - "C(timezone) - Timezone to be set to Virtual Machine when deployed." + - "C(user_name) - Username to be used to set password to Virtual Machine when deployed." + - "C(root_password) - Password to be set for user specified by C(user_name) parameter." + - "C(authorized_ssh_keys) - Use this SSH keys to login to Virtual Machine." + - "C(regenerate_ssh_keys) - If I(True) SSH keys will be regenerated on Virtual Machine." + - "C(custom_script) - Cloud-init script which will be executed on Virtual Machine when deployed." + - "C(dns_servers) - DNS servers to be configured on Virtual Machine." + - "C(dns_search) - DNS search domains to be configured on Virtual Machine." + - "C(nic_boot_protocol) - Set boot protocol of the network interface of Virtual Machine. Can be one of None, DHCP or Static." + - "C(nic_ip_address) - If boot protocol is static, set this IP address to network interface of Virtual Machine." + - "C(nic_netmask) - If boot protocol is static, set this netmask to network interface of Virtual Machine." + - "C(nic_gateway) - If boot protocol is static, set this gateway to network interface of Virtual Machine." + - "C(nic_name) - Set name to network interface of Virtual Machine." + - "C(nic_on_boot) - If I(True) network interface will be set to start on boot." +notes: + - "If VM is in I(UNASSIGNED) or I(UNKNOWN) state before any operation, the module will fail. + If VM is in I(IMAGE_LOCKED) state before any operation, we try to wait for VM to be I(DOWN). + If VM is in I(SAVING_STATE) state before any operation, we try to wait for VM to be I(SUSPENDED). + If VM is in I(POWERING_DOWN) state before any operation, we try to wait for VM to be I(UP) or I(DOWN). VM can + get into I(UP) state from I(POWERING_DOWN) state, when there is no ACPI or guest agent running inside VM, or + if the shutdown operation fails. + When user specify I(UP) C(state), we always wait to VM to be in I(UP) state in case VM is I(MIGRATING), + I(REBOOTING), I(POWERING_UP), I(RESTORING_STATE), I(WAIT_FOR_LAUNCH). In other states we run start operation on VM. + When user specify I(stopped) C(state), and If user pass C(force) parameter set to I(true) we forcibly stop the VM in + any state. If user don't pass C(force) parameter, we always wait to VM to be in UP state in case VM is + I(MIGRATING), I(REBOOTING), I(POWERING_UP), I(RESTORING_STATE), I(WAIT_FOR_LAUNCH). If VM is in I(PAUSED) or + I(SUSPENDED) state, we start the VM. Then we gracefully shutdown the VM. + When user specify I(suspended) C(state), we always wait to VM to be in UP state in case VM is I(MIGRATING), + I(REBOOTING), I(POWERING_UP), I(RESTORING_STATE), I(WAIT_FOR_LAUNCH). If VM is in I(PAUSED) or I(DOWN) state, + we start the VM. Then we suspend the VM. + When user specify I(absent) C(state), we forcibly stop the VM in any state and remove it." +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Creates a new Virtual Machine from template named 'rhel7_template' +ovirt_vms: + state: present + name: myvm + template: rhel7_template + +# Creates a new server rhel7 Virtual Machine from Blank template +# on brq01 cluster with 2GiB memory and 2 vcpu cores/sockets +# and attach bootable disk with name rhel7_disk and attach virtio NIC +ovirt_vms: + state: present + cluster: brq01 + name: myvm + memory: 2GiB + cpu_cores: 2 + cpu_sockets: 2 + cpu_shares: 1024 + type: server + operating_system: rhel_7x64 + disks: + - name: rhel7_disk + bootable: True + nics: + - name: nic1 + +# Run VM with cloud init: +ovirt_vms: + name: rhel7 + template: rhel7 + cluster: Default + memory: 1GiB + high_availability: true + cloud_init: + nic_boot_protocol: static + nic_ip_address: 10.34.60.86 + nic_netmask: 255.255.252.0 + nic_gateway: 10.34.63.254 + nic_name: eth1 + nic_on_boot: true + host_name: example.com + custom_script: | + write_files: + - content: | + Hello, world! + path: /tmp/greeting.txt + permissions: '0644' + user_name: root + root_password: super_password + +# Run VM with sysprep: +ovirt_vms: + name: windows2012R2_AD + template: windows2012R2 + cluster: Default + memory: 3GiB + high_availability: true + sysprep: + host_name: windowsad.example.com + user_name: Administrator + root_password: SuperPassword123 + +# Migrate/Run VM to/on host named 'host1' +ovirt_vms: + state: running + name: myvm + host: host1 + +# Change Vm's CD: +ovirt_vms: + name: myvm + cd_iso: drivers.iso + +# Eject Vm's CD: +ovirt_vms: + name: myvm + cd_iso: '' + +# Boot VM from CD: +ovirt_vms: + name: myvm + cd_iso: centos7_x64.iso + boot_devices: + - cdrom + +# Stop vm: +ovirt_vms: + state: stopped + name: myvm + +# Upgrade memory to already created VM: +ovirt_vms: + name: myvm + memory: 4GiB + +# Hot plug memory to already created and running VM: +# (VM won't be restarted) +ovirt_vms: + name: myvm + memory: 4GiB + +# When change on the VM needs restart of the VM, use next_run state, +# The VM will be updated and rebooted if there are any changes. +# If present state would be used, VM won't be restarted. +ovirt_vms: + state: next_run + name: myvm + boot_devices: + - network + +# Remove VM, if VM is running it will be stopped: +ovirt_vms: + state: absent + name: myvm +''' + + +RETURN = ''' +id: + description: ID of the VM which is managed + returned: On success if VM is found. + type: str + sample: 7de90f31-222c-436c-a1ca-7e655bd5b60c +vm: + description: "Dictionary of all the VM attributes. VM attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/vm." + returned: On success if VM is found. +''' + + +class VmsModule(BaseModule): + + def build_entity(self): + return otypes.Vm( + name=self._module.params['name'], + cluster=otypes.Cluster( + name=self._module.params['cluster'] + ) if self._module.params['cluster'] else None, + template=otypes.Template( + name=self._module.params['template'] + ) if self._module.params['template'] else None, + stateless=self._module.params['stateless'], + delete_protected=self._module.params['delete_protected'], + high_availability=otypes.HighAvailability( + enabled=self._module.params['high_availability'] + ) if self._module.params['high_availability'] is not None else None, + cpu=otypes.Cpu( + topology=otypes.CpuTopology( + cores=self._module.params['cpu_cores'], + sockets=self._module.params['cpu_sockets'], + ) + ) if ( + self._module.params['cpu_cores'] or self._module.params['cpu_sockets'] + ) else None, + cpu_shares=self._module.params['cpu_shares'], + os=otypes.OperatingSystem( + type=self._module.params['operating_system'], + boot=otypes.Boot( + devices=[ + otypes.BootDevice(dev) for dev in self._module.params['boot_devices'] + ], + ) if self._module.params['boot_devices'] else None, + ) if ( + self._module.params['operating_system'] or self._module.params['boot_devices'] + ) else None, + type=otypes.VmType( + self._module.params['type'] + ) if self._module.params['type'] else None, + memory=convert_to_bytes( + self._module.params['memory'] + ) if self._module.params['memory'] else None, + memory_policy=otypes.MemoryPolicy( + guaranteed=convert_to_bytes(self._module.params['memory_guaranteed']), + ) if self._module.params['memory_guaranteed'] else None, + ) + + def update_check(self, entity): + return ( + equal(self._module.params.get('cluster'), get_link_name(self._connection, entity.cluster)) and + equal(convert_to_bytes(self._module.params['memory']), entity.memory) and + equal(convert_to_bytes(self._module.params['memory_guaranteed']), entity.memory_policy.guaranteed) and + equal(self._module.params.get('cpu_cores'), entity.cpu.topology.cores) and + equal(self._module.params.get('cpu_sockets'), entity.cpu.topology.sockets) and + equal(self._module.params.get('type'), str(entity.type)) and + equal(self._module.params.get('operating_system'), str(entity.os.type)) and + equal(self._module.params.get('high_availability'), entity.high_availability.enabled) and + equal(self._module.params.get('stateless'), entity.stateless) and + equal(self._module.params.get('cpu_shares'), entity.cpu_shares) and + equal(self._module.params.get('delete_protected'), entity.delete_protected) and + equal(self._module.params.get('boot_devices'), [str(dev) for dev in getattr(entity.os, 'devices', [])]) + ) + + def pre_create(self, entity): + # If VM don't exists, and template is not specified, set it to Blank: + if entity is None: + if self._module.params.get('template') is None: + self._module.params['template'] = 'Blank' + + def post_update(self, entity): + self.post_create(entity) + + def post_create(self, entity): + # After creation of the VM, attach disks and NICs: + self.changed = self.__attach_disks(entity) + self.changed = self.__attach_nics(entity) + + def pre_remove(self, entity): + # Forcibly stop the VM, if it's not in DOWN state: + if entity.status != otypes.VmStatus.DOWN: + if not self._module.check_mode: + self.changed = self.action( + action='stop', + action_condition=lambda vm: vm.status != otypes.VmStatus.DOWN, + wait_condition=lambda vm: vm.status == otypes.VmStatus.DOWN, + )['changed'] + + def __suspend_shutdown_common(self, vm_service): + if vm_service.get().status in [ + otypes.VmStatus.MIGRATING, + otypes.VmStatus.POWERING_UP, + otypes.VmStatus.REBOOT_IN_PROGRESS, + otypes.VmStatus.WAIT_FOR_LAUNCH, + otypes.VmStatus.UP, + otypes.VmStatus.RESTORING_STATE, + ]: + self._wait_for_UP(vm_service) + + def _pre_shutdown_action(self, entity): + vm_service = self._service.vm_service(entity.id) + self.__suspend_shutdown_common(vm_service) + if entity.status in [otypes.VmStatus.SUSPENDED, otypes.VmStatus.PAUSED]: + vm_service.start() + self._wait_for_UP(vm_service) + return vm_service.get() + + def _pre_suspend_action(self, entity): + vm_service = self._service.vm_service(entity.id) + self.__suspend_shutdown_common(vm_service) + if entity.status in [otypes.VmStatus.PAUSED, otypes.VmStatus.DOWN]: + vm_service.start() + self._wait_for_UP(vm_service) + return vm_service.get() + + def _post_start_action(self, entity): + vm_service = self._service.service(entity.id) + self._wait_for_UP(vm_service) + self._attach_cd(vm_service.get()) + self._migrate_vm(vm_service.get()) + + def _attach_cd(self, entity): + cd_iso = self._module.params['cd_iso'] + if cd_iso is not None: + vm_service = self._service.service(entity.id) + current = vm_service.get().status == otypes.VmStatus.UP + cdroms_service = vm_service.cdroms_service() + cdrom_device = cdroms_service.list()[0] + cdrom_service = cdroms_service.cdrom_service(cdrom_device.id) + cdrom = cdrom_service.get(current=current) + if getattr(cdrom.file, 'id', '') != cd_iso: + if not self._module.check_mode: + cdrom_service.update( + cdrom=otypes.Cdrom( + file=otypes.File(id=cd_iso) + ), + current=current, + ) + self.changed = True + + return entity + + def _migrate_vm(self, entity): + vm_host = self._module.params['host'] + vm_service = self._service.vm_service(entity.id) + if vm_host is not None: + # In case VM is preparing to be UP, wait to be up, to migrate it: + if entity.status == otypes.VmStatus.UP: + hosts_service = self._connection.system_service().hosts_service() + current_vm_host = hosts_service.host_service(entity.host.id).get().name + if vm_host != current_vm_host: + if not self._module.check_mode: + vm_service.migrate(host=otypes.Host(name=vm_host)) + self._wait_for_UP(vm_service) + self.changed = True + + return entity + + def _wait_for_UP(self, vm_service): + wait( + service=vm_service, + condition=lambda vm: vm.status == otypes.VmStatus.UP, + wait=self._module.params['wait'], + timeout=self._module.params['timeout'], + ) + + def __attach_disks(self, entity): + disks_service = self._connection.system_service().disks_service() + + for disk in self._module.params['disks']: + # If disk ID is not specified, find disk by name: + disk_id = disk.get('id') + if disk_id is None: + disk_id = getattr( + search_by_name( + service=disks_service, + name=disk.get('name') + ), + 'id', + None + ) + + # Attach disk to VM: + disk_attachments_service = self._service.service(entity.id).disk_attachments_service() + if disk_attachments_service.attachment_service(disk_id).get() is None: + if not self._module.check_mode: + disk_attachments_service.add( + otypes.DiskAttachment( + disk=otypes.Disk( + id=disk_id, + active=disk.get('activate', True), + ), + interface=otypes.DiskInterface( + disk.get('interface', 'virtio') + ), + bootable=disk.get('bootable', False), + ) + ) + self.changed = True + + def __attach_nics(self, entity): + # Attach NICs to VM, if specified: + vnic_profiles_service = self._connection.system_service().vnic_profiles_service() + nics_service = self._service.service(entity.id).nics_service() + for nic in self._module.params['nics']: + if search_by_name(nics_service, nic.get('name')) is None: + if not self._module.check_mode: + nics_service.add( + otypes.Nic( + name=nic.get('name'), + interface=otypes.NicInterface( + nic.get('interface', 'virtio') + ), + vnic_profile=otypes.VnicProfile( + id=search_by_name( + vnic_profiles_service, + nic.get('profile_name'), + ).id + ) if nic.get('profile_name') else None, + mac=otypes.Mac( + address=nic.get('mac_address') + ) if nic.get('mac_address') else None, + ) + ) + self.changed = True + + +def _get_initialization(sysprep, cloud_init): + initialization = None + if cloud_init: + initialization = otypes.Initialization( + nic_configurations=[ + otypes.NicConfiguration( + boot_protocol=otypes.BootProtocol( + cloud_init.pop('nic_boot_protocol').lower() + ) if cloud_init.get('nic_boot_protocol') else None, + name=cloud_init.pop('nic_name'), + on_boot=cloud_init.pop('nic_on_boot'), + ip=otypes.Ip( + address=cloud_init.pop('nic_ip_address'), + netmask=cloud_init.pop('nic_netmask'), + gateway=cloud_init.pop('nic_gateway'), + ) if ( + cloud_init.get('nic_gateway') is not None or + cloud_init.get('nic_netmask') is not None or + cloud_init.get('nic_ip_address') is not None + ) else None, + ) + ] if ( + cloud_init.get('nic_gateway') is not None or + cloud_init.get('nic_netmask') is not None or + cloud_init.get('nic_ip_address') is not None or + cloud_init.get('nic_boot_protocol') is not None or + cloud_init.get('nic_on_boot') is not None + ) else None, + **cloud_init + ) + elif sysprep: + initialization = otypes.Initialization( + **sysprep + ) + return initialization + + +def control_state(vm, vms_service, module): + if vm is None: + return + + force = module.params['force'] + state = module.params['state'] + + vm_service = vms_service.vm_service(vm.id) + if vm.status == otypes.VmStatus.IMAGE_LOCKED: + wait( + service=vm_service, + condition=lambda vm: vm.status == otypes.VmStatus.DOWN, + ) + elif vm.status == otypes.VmStatus.SAVING_STATE: + # Result state is SUSPENDED, we should wait to be suspended: + wait( + service=vm_service, + condition=lambda vm: vm.status == otypes.VmStatus.SUSPENDED, + ) + elif ( + vm.status == otypes.VmStatus.UNASSIGNED or + vm.status == otypes.VmStatus.UNKNOWN + ): + # Invalid states: + module.fail_json("Not possible to control VM, if it's in '{}' status".format(vm.status)) + elif vm.status == otypes.VmStatus.POWERING_DOWN: + if (force and state == 'stopped') or state == 'absent': + vm_service.stop() + wait( + service=vm_service, + condition=lambda vm: vm.status == otypes.VmStatus.DOWN, + ) + else: + # If VM is powering down, wait to be DOWN or UP. + # VM can end in UP state in case there is no GA + # or ACPI on the VM or shutdown operation crashed: + wait( + service=vm_service, + condition=lambda vm: vm.status in [otypes.VmStatus.DOWN, otypes.VmStatus.UP], + ) + + +def main(): + argument_spec = ovirt_full_argument_spec( + state=dict( + choices=['running', 'stopped', 'present', 'absent', 'suspended', 'next_run'], + default='present', + ), + name=dict(default=None), + id=dict(default=None), + cluster=dict(default=None), + template=dict(default=None), + disks=dict(default=[], type='list'), + memory=dict(default=None), + memory_guaranteed=dict(default=None), + cpu_sockets=dict(default=None, type='int'), + cpu_cores=dict(default=None, type='int'), + cpu_shares=dict(default=None, type='int'), + type=dict(choices=['server', 'desktop']), + operating_system=dict( + default=None, + choices=[ + 'rhel_6_ppc64', 'other', 'freebsd', 'windows_2003x64', 'windows_10', + 'rhel_6x64', 'rhel_4x64', 'windows_2008x64', 'windows_2008R2x64', + 'debian_7', 'windows_2012x64', 'ubuntu_14_04', 'ubuntu_12_04', + 'ubuntu_13_10', 'windows_8x64', 'other_linux_ppc64', 'windows_2003', + 'other_linux', 'windows_10x64', 'windows_2008', 'rhel_3', 'rhel_5', + 'rhel_4', 'other_ppc64', 'sles_11', 'rhel_6', 'windows_xp', 'rhel_7x64', + 'freebsdx64', 'rhel_7_ppc64', 'windows_7', 'rhel_5x64', + 'ubuntu_14_04_ppc64', 'sles_11_ppc64', 'windows_8', + 'windows_2012R2x64', 'windows_2008r2x64', 'ubuntu_13_04', + 'ubuntu_12_10', 'windows_7x64', + ], + ), + cd_iso=dict(default=None), + boot_devices=dict(default=None, type='list'), + high_availability=dict(type='bool'), + stateless=dict(type='bool'), + delete_protected=dict(type='bool'), + force=dict(type='bool', default=False), + nics=dict(default=[], type='list'), + cloud_init=dict(type='dict'), + sysprep=dict(type='dict'), + host=dict(default=None), + clone=dict(type='bool', default=False), + clone_permissions=dict(type='bool', default=False), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + check_sdk(module) + check_params(module) + + try: + state = module.params['state'] + connection = create_connection(module.params.pop('auth')) + vms_service = connection.system_service().vms_service() + vms_module = VmsModule( + connection=connection, + module=module, + service=vms_service, + ) + vm = vms_module.search_entity() + + control_state(vm, vms_service, module) + if state == 'present' or state == 'running' or state == 'next_run': + cloud_init = module.params['cloud_init'] + sysprep = module.params['sysprep'] + + # In case VM don't exist, wait for VM DOWN state, + # otherwise don't wait for any state, just update VM: + vms_module.create( + entity=vm, + result_state=otypes.VmStatus.DOWN if vm is None else None, + clone=module.params['clone'], + clone_permissions=module.params['clone_permissions'], + ) + ret = vms_module.action( + action='start', + post_action=vms_module._post_start_action, + action_condition=lambda vm: ( + vm.status not in [ + otypes.VmStatus.MIGRATING, + otypes.VmStatus.POWERING_UP, + otypes.VmStatus.REBOOT_IN_PROGRESS, + otypes.VmStatus.WAIT_FOR_LAUNCH, + otypes.VmStatus.UP, + otypes.VmStatus.RESTORING_STATE, + ] + ), + wait_condition=lambda vm: vm.status == otypes.VmStatus.UP, + # Start action kwargs: + use_cloud_init=cloud_init is not None, + use_sysprep=sysprep is not None, + vm=otypes.Vm( + placement_policy=otypes.VmPlacementPolicy( + hosts=[otypes.Host(name=module.params['host'])] + ) if module.params['host'] else None, + initialization=_get_initialization(sysprep, cloud_init), + ), + ) + + if state == 'next_run': + # Apply next run configuration, if needed: + vm = vms_service.vm_service(ret['id']).get() + if vm.next_run_configuration_exists: + ret = vms_module.action( + action='reboot', + entity=vm, + action_condition=lambda vm: vm.status == otypes.VmStatus.UP, + wait_condition=lambda vm: vm.status == otypes.VmStatus.UP, + ) + elif state == 'stopped': + vms_module.create( + clone=module.params['clone'], + clone_permissions=module.params['clone_permissions'], + ) + if module.params['force']: + ret = vms_module.action( + action='stop', + post_action=vms_module._attach_cd, + action_condition=lambda vm: vm.status != otypes.VmStatus.DOWN, + wait_condition=lambda vm: vm.status == otypes.VmStatus.DOWN, + ) + else: + ret = vms_module.action( + action='shutdown', + pre_action=vms_module._pre_shutdown_action, + post_action=vms_module._attach_cd, + action_condition=lambda vm: vm.status != otypes.VmStatus.DOWN, + wait_condition=lambda vm: vm.status == otypes.VmStatus.DOWN, + ) + elif state == 'suspended': + vms_module.create( + clone=module.params['clone'], + clone_permissions=module.params['clone_permissions'], + ) + ret = vms_module.action( + action='suspend', + pre_action=vms_module._pre_suspend_action, + action_condition=lambda vm: vm.status != otypes.VmStatus.SUSPENDED, + wait_condition=lambda vm: vm.status == otypes.VmStatus.SUSPENDED, + ) + elif state == 'absent': + ret = vms_module.remove() + + module.exit_json(**ret) + except Exception as e: + module.fail_json(msg=str(e)) + finally: + connection.close(logout=False) + +from ansible.module_utils.basic import * +if __name__ == "__main__": + main() From 46d715d7693e727ab4445c466d5b50b8ad3f3d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Wed, 21 Sep 2016 18:57:48 +0200 Subject: [PATCH 2140/2522] composer: update docs about issues on macOS if installed by homebrew (#2987) --- packaging/language/composer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packaging/language/composer.py b/packaging/language/composer.py index d5e6467e9c5..4c5f8518bee 100644 --- a/packaging/language/composer.py +++ b/packaging/language/composer.py @@ -103,6 +103,7 @@ - composer installed in bin path (recommended /usr/local/bin) notes: - Default options that are always appended in each execution are --no-ansi, --no-interaction and --no-progress if available. + - We received reports about issues on macOS if composer was installed by Homebrew. Please use the official install method to avoid it. ''' EXAMPLES = ''' From 1dc9d9512fe75b83f3d4818998b3edbf493a6910 Mon Sep 17 00:00:00 2001 From: Robin Roth Date: Wed, 21 Sep 2016 18:59:12 +0200 Subject: [PATCH 2141/2522] List python-xml in requirements for zypper* (#2937) --- packaging/os/zypper.py | 1 + packaging/os/zypper_repository.py | 1 + 2 files changed, 2 insertions(+) diff --git a/packaging/os/zypper.py b/packaging/os/zypper.py index 9d90dbe044c..c956feac1a1 100644 --- a/packaging/os/zypper.py +++ b/packaging/os/zypper.py @@ -107,6 +107,7 @@ # informational: requirements for nodes requirements: - "zypper >= 1.0 # included in openSuSE >= 11.1 or SuSE Linux Enterprise Server/Desktop >= 11.0" + - python-xml - rpm ''' diff --git a/packaging/os/zypper_repository.py b/packaging/os/zypper_repository.py index 0737d3cc3c0..5a06e6f9ded 100644 --- a/packaging/os/zypper_repository.py +++ b/packaging/os/zypper_repository.py @@ -103,6 +103,7 @@ requirements: - "zypper >= 1.0 # included in openSuSE >= 11.1 or SuSE Linux Enterprise Server/Desktop >= 11.0" + - python-xml ''' EXAMPLES = ''' From d76f4f1795f66ab856c52d4d24af2aa3dc6d0dfc Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Wed, 21 Sep 2016 21:18:37 +0200 Subject: [PATCH 2142/2522] Fix example in ovirt_auth module (#3001) This patch fix missing quotes in ovirt_auth revoke token example --- cloud/ovirt/ovirt_auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/ovirt/ovirt_auth.py b/cloud/ovirt/ovirt_auth.py index 22abf1d8394..19ab2e1641b 100644 --- a/cloud/ovirt/ovirt_auth.py +++ b/cloud/ovirt/ovirt_auth.py @@ -119,7 +119,7 @@ - name: Always revoke the SSO token ovirt_auth: state: absent - ovirt_auth: {{ ovirt_auth }} + ovirt_auth: "{{ ovirt_auth }}" ''' RETURN = ''' From 935a3ab2cb955c565edfe2d581305ad0fe32a85a Mon Sep 17 00:00:00 2001 From: Tom Melendez Date: Wed, 21 Sep 2016 15:15:16 -0700 Subject: [PATCH 2143/2522] Change 'values' to 'record_data' for gcdns module. (#3003) Using values caused problems while creating an integration playbook as it is a reserved word. Seeing as this module is not yet released, it's prudent to make this change now. 'record_data' is more descriptive and uses the _data convention that we've established for instances. No functionality in the module has changed. --- cloud/google/gcdns_record.py | 108 +++++++++++++++++------------------ 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/cloud/google/gcdns_record.py b/cloud/google/gcdns_record.py index 949c5d19dac..19b70a85816 100644 --- a/cloud/google/gcdns_record.py +++ b/cloud/google/gcdns_record.py @@ -71,19 +71,19 @@ - The type of resource record to add. required: true choices: [ 'A', 'AAAA', 'CNAME', 'SRV', 'TXT', 'SOA', 'NS', 'MX', 'SPF', 'PTR' ] - values: + record_data: description: - - The values to use for the resource record. - - I(values) must be specified if I(state) is C(present) or + - The record_data to use for the resource record. + - I(record_data) must be specified if I(state) is C(present) or I(overwrite) is C(True), or the module will fail. - - Valid values vary based on the record's I(type). In addition, + - Valid record_data vary based on the record's I(type). In addition, resource records that contain a DNS domain name in the value field (e.g., CNAME, PTR, SRV, .etc) MUST include a trailing dot in the value. - - Individual string values for TXT records must be enclosed in + - Individual string record_data for TXT records must be enclosed in double quotes. - For resource records that have the same name but different - values (e.g., multiple A records), they must be defined as + record_data (e.g., multiple A records), they must be defined as multiple list entries in a single record. required: false aliases: ['value'] @@ -99,15 +99,15 @@ or fail. The behavior of this option depends on I(state). - If I(state) is C(present) and I(overwrite) is C(True), this module will replace an existing resource record of the same name - with the provided I(values). If I(state) is C(present) and + with the provided I(record_data). If I(state) is C(present) and I(overwrite) is C(False), this module will fail if there is an existing resource record with the same name and type, but different resource data. - If I(state) is C(absent) and I(overwrite) is C(True), this module will remove the given resource record unconditionally. If I(state) is C(absent) and I(overwrite) is C(False), this - module will fail if the provided values do not match exactly - with the existing resource record's values. + module will fail if the provided record_data do not match exactly + with the existing resource record's record_data. required: false choices: [True, False] default: False @@ -192,19 +192,19 @@ record: 'api.example.com' zone_id: 'example-com' type: A - values: + record_data: - '192.0.2.23' - '10.4.5.6' - '198.51.100.5' - '203.0.113.10' -# Change the value of an existing record with multiple values. +# Change the value of an existing record with multiple record_data. - gcdns_record: record: 'api.example.com' zone: 'example.com' type: A overwrite: true - values: # WARNING: All values in a record will be replaced + record_data: # WARNING: All values in a record will be replaced - '192.0.2.23' - '192.0.2.42' # The changed record - '198.51.100.5' @@ -216,7 +216,7 @@ zone_id: 'example-com' state: absent type: A - values: # NOTE: All of the values must match exactly + record_data: # NOTE: All of the values must match exactly - '192.0.2.23' - '192.0.2.42' - '198.51.100.5' @@ -250,7 +250,7 @@ zone: 'example.com' type: NS ttl: 21600 - values: + record_data: - 'ns-cloud-d1.googledomains.com.' # Note the trailing dots on values - 'ns-cloud-d2.googledomains.com.' - 'ns-cloud-d3.googledomains.com.' @@ -261,7 +261,7 @@ record: 'example.com' zone_id: 'example-com' type: TXT - values: + record_data: - '"v=spf1 include:_spf.google.com -all"' # A single-string TXT value - '"hello " "world"' # A multi-string TXT value ''' @@ -292,7 +292,7 @@ returned: success type: string sample: A -values: +record_data: description: The resource record values returned: success type: list @@ -365,8 +365,8 @@ def create_record(module, gcdns, zone, record): record_name = module.params['record'] record_type = module.params['type'] ttl = module.params['ttl'] - values = module.params['values'] - data = dict(ttl=ttl, rrdatas=values) + record_data = module.params['record_data'] + data = dict(ttl=ttl, rrdatas=record_data) # Google Cloud DNS wants the trailing dot on all DNS names. if record_name[-1] != '.': @@ -375,7 +375,7 @@ def create_record(module, gcdns, zone, record): # If we found a record, we need to check if the values match. if record is not None: # If the record matches, we obviously don't have to change anything. - if _records_match(record.data['ttl'], record.data['rrdatas'], ttl, values): + if _records_match(record.data['ttl'], record.data['rrdatas'], ttl, record_data): return False # The record doesn't match, so we need to check if we can overwrite it. @@ -397,7 +397,7 @@ def create_record(module, gcdns, zone, record): # as its value). module.fail_json( msg = 'value is invalid for the given type: ' + - "%s, got value: %s" % (record_type, values), + "%s, got value: %s" % (record_type, record_data), changed = False ) @@ -455,7 +455,7 @@ def remove_record(module, gcdns, record): overwrite = module.boolean(module.params['overwrite']) ttl = module.params['ttl'] - values = module.params['values'] + record_data = module.params['record_data'] # If there is no record, we're obviously done. if record is None: @@ -464,11 +464,11 @@ def remove_record(module, gcdns, record): # If there is an existing record, do our values match the values of the # existing record? if not overwrite: - if not _records_match(record.data['ttl'], record.data['rrdatas'], ttl, values): + if not _records_match(record.data['ttl'], record.data['rrdatas'], ttl, record_data): module.fail_json( - msg = 'cannot delete due to non-matching ttl or values: ' + - "ttl: %d, values: %s " % (ttl, values) + - "original ttl: %d, original values: %s" % (record.data['ttl'], record.data['rrdatas']), + msg = 'cannot delete due to non-matching ttl or record_data: ' + + "ttl: %d, record_data: %s " % (ttl, record_data) + + "original ttl: %d, original record_data: %s" % (record.data['ttl'], record.data['rrdatas']), changed = False ) @@ -516,14 +516,14 @@ def _get_zone(gcdns, zone_name, zone_id): return found_zone -def _records_match(old_ttl, old_values, new_ttl, new_values): +def _records_match(old_ttl, old_record_data, new_ttl, new_record_data): """Checks to see if original and new TTL and values match.""" matches = True if old_ttl != new_ttl: matches = False - if old_values != new_values: + if old_record_data != new_record_data: matches = False return matches @@ -537,7 +537,7 @@ def _sanity_check(module): record_type = module.params['type'] state = module.params['state'] ttl = module.params['ttl'] - values = module.params['values'] + record_data = module.params['record_data'] # Apache libcloud needs to be installed and at least the minimum version. if not HAS_LIBCLOUD: @@ -567,10 +567,10 @@ def _sanity_check(module): module.fail_json(msg='cannot update SOA records', changed=False) # Some sanity checks depend on what value was supplied. - if values is not None and (state == 'present' or not overwrite): + if record_data is not None and (state == 'present' or not overwrite): # A records must contain valid IPv4 addresses. if record_type == 'A': - for value in values: + for value in record_data: try: socket.inet_aton(value) except socket.error: @@ -581,7 +581,7 @@ def _sanity_check(module): # AAAA records must contain valid IPv6 addresses. if record_type == 'AAAA': - for value in values: + for value in record_data: try: socket.inet_pton(socket.AF_INET6, value) except socket.error: @@ -591,10 +591,10 @@ def _sanity_check(module): ) # CNAME and SOA records can't have multiple values. - if record_type in ['CNAME', 'SOA'] and len(values) > 1: + if record_type in ['CNAME', 'SOA'] and len(record_data) > 1: module.fail_json( msg = 'CNAME or SOA records cannot have more than one value, ' + - "got: %s" % values, + "got: %s" % record_data, changed = False ) @@ -607,10 +607,10 @@ def _sanity_check(module): # Values for txt records must begin and end with a double quote. if record_type == 'TXT': - for value in values: + for value in record_data: if value[0] != '"' and value[-1] != '"': module.fail_json( - msg = 'TXT values must be enclosed in double quotes, ' + + msg = 'TXT record_data must be enclosed in double quotes, ' + 'got: %s' % value, changed = False ) @@ -670,7 +670,7 @@ def main(): zone = dict(type='str'), zone_id = dict(type='str'), type = dict(required=True, choices=SUPPORTED_RECORD_TYPES, type='str'), - values = dict(aliases=['value'], type='list'), + record_data = dict(aliases=['value'], type='list'), ttl = dict(default=300, type='int'), overwrite = dict(default=False, type='bool'), service_account_email = dict(type='str'), @@ -679,8 +679,8 @@ def main(): project_id = dict(type='str') ), required_if = [ - ('state', 'present', ['values']), - ('overwrite', False, ['values']) + ('state', 'present', ['record_data']), + ('overwrite', False, ['record_data']) ], required_one_of = [['zone', 'zone_id']], supports_check_mode = True @@ -696,14 +696,14 @@ def main(): zone_id = module.params['zone_id'] json_output = dict( - state = state, - record = record_name, - zone = zone_name, - zone_id = zone_id, - type = record_type, - values = module.params['values'], - ttl = ttl, - overwrite = module.boolean(module.params['overwrite']) + state = state, + record = record_name, + zone = zone_name, + zone_id = zone_id, + type = record_type, + record_data = module.params['record_data'], + ttl = ttl, + overwrite = module.boolean(module.params['overwrite']) ) # Google Cloud DNS wants the trailing dot on all DNS names. @@ -755,20 +755,20 @@ def main(): diff['before_header'] = '' else: diff['before'] = dict( - record = record.data['name'], - type = record.data['type'], - values = record.data['rrdatas'], - ttl = record.data['ttl'] + record = record.data['name'], + type = record.data['type'], + record_data = record.data['rrdatas'], + ttl = record.data['ttl'] ) diff['before_header'] = "%s:%s" % (record_type, record_name) # Create, remove, or modify the record. if state == 'present': diff['after'] = dict( - record = record_name, - type = record_type, - values = module.params['values'], - ttl = ttl + record = record_name, + type = record_type, + record_data = module.params['record_data'], + ttl = ttl ) diff['after_header'] = "%s:%s" % (record_type, record_name) From 26546242e9c65d6f7aaa3f0f6b6facc2a3e01d78 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Thu, 22 Sep 2016 14:43:15 -0700 Subject: [PATCH 2144/2522] Corrects an implied map() usage as list (#3010) In the six package, the map() function returns an iterator instead of a list. This code was continuing to use the map() return value as if it were a list and this broke the address_class facts. This patch changes the code to use the list() method on the return value of map(). --- network/f5/bigip_facts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/f5/bigip_facts.py b/network/f5/bigip_facts.py index 2189ded2e29..dc6c6b7d1dc 100644 --- a/network/f5/bigip_facts.py +++ b/network/f5/bigip_facts.py @@ -1079,7 +1079,7 @@ def get_list(self): def get_address_class(self): key = self.api.LocalLB.Class.get_address_class(self.address_classes) value = self.api.LocalLB.Class.get_address_class_member_data_value(key) - result = map(zip, [x['members'] for x in key], value) + result = list(map(zip, [x['members'] for x in key], value)) return result def get_description(self): From e672b3141f3895652419ab093624033c5b7ec1a6 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Thu, 22 Sep 2016 16:34:33 -0700 Subject: [PATCH 2145/2522] Add shellcheck to sanity checks. (#3013) Also disable deprecation warnings during module validation. --- test/utils/shippable/sanity.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/utils/shippable/sanity.sh b/test/utils/shippable/sanity.sh index 1c0e2451a43..d9234cf0527 100755 --- a/test/utils/shippable/sanity.sh +++ b/test/utils/shippable/sanity.sh @@ -9,6 +9,10 @@ cd "${source_root}" if [ "${install_deps}" != "" ]; then add-apt-repository ppa:fkrull/deadsnakes && apt-get update -qq && apt-get install python2.4 -qq + apt-add-repository 'deb http://archive.ubuntu.com/ubuntu trusty-backports universe' + apt-get update -qq + apt-get install shellcheck + pip install git+https://github.com/ansible/ansible.git@devel#egg=ansible pip install git+https://github.com/sivel/ansible-testing.git#egg=ansible_testing fi @@ -19,4 +23,8 @@ python2.6 -m compileall -fq . python2.7 -m compileall -fq . python3.5 -m compileall -fq . -x "($(printf %s "$(< "test/utils/shippable/sanity-skip-python3.txt"))" | tr '\n' '|')" -ansible-validate-modules --exclude '/utilities/|/shippable(/|$)' . +ANSIBLE_DEPRECATION_WARNINGS=false \ + ansible-validate-modules --exclude '/utilities/|/shippable(/|$)' . + +shellcheck \ + test/utils/shippable/*.sh From 1ade801f65888c1f697ccfab2e4c823e65f1c9a2 Mon Sep 17 00:00:00 2001 From: nitzmahone Date: Thu, 22 Sep 2016 20:06:45 -0700 Subject: [PATCH 2146/2522] Fix win_chocolatey version comparison fixes #2995 --- windows/win_chocolatey.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_chocolatey.ps1 b/windows/win_chocolatey.ps1 index 7a3f14aabc3..3bb6a1f0dc0 100644 --- a/windows/win_chocolatey.ps1 +++ b/windows/win_chocolatey.ps1 @@ -74,7 +74,7 @@ Function Chocolatey-Install-Upgrade { $script:executable = "choco.exe" - if ((choco --version) -lt '0.9.9') + if ([Version](choco --version) -lt [Version]'0.9.9') { Choco-Upgrade chocolatey } From 372828de01bf487eea5b034409f501c7e7f6932e Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Thu, 22 Sep 2016 23:20:19 -0700 Subject: [PATCH 2147/2522] Fixes the bigip_selfip module to respect traffic groups (#3009) The code for traffic groups was not being tested and therefore had errors associated with it. It is now covered in coverage tests and bugs that were found in it have been fixed. See this issue for details https://github.com/F5Networks/f5-ansible/issues/28 --- network/f5/bigip_selfip.py | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/network/f5/bigip_selfip.py b/network/f5/bigip_selfip.py index 75ed6a09c0a..6cbf7badb58 100644 --- a/network/f5/bigip_selfip.py +++ b/network/f5/bigip_selfip.py @@ -367,6 +367,20 @@ def fmt_services(self, services): else: return list(services) + def traffic_groups(self): + result = [] + + groups = self.api.tm.cm.traffic_groups.get_collection() + for group in groups: + # Just checking for the addition of the partition here for + # different versions of BIG-IP + if '/' + self.params['partition'] + '/' in group.name: + result.append(group.name) + else: + full_name = '/%s/%s' % (self.params['partition'], group.name) + result.append(str(full_name)) + return result + def update(self): changed = False svcs = [] @@ -408,8 +422,11 @@ def update(self): ) if traffic_group is not None: - groups = self.api.tm.cm.traffic_groups.get_collection() - params['trafficGroup'] = "/%s/%s" % (partition, traffic_group) + traffic_group = "/%s/%s" % (partition, traffic_group) + if traffic_group not in self.traffic_groups(): + raise F5ModuleError( + 'The specified traffic group was not found' + ) if 'traffic_group' in current: if traffic_group != current['traffic_group']: @@ -417,11 +434,6 @@ def update(self): else: params['trafficGroup'] = traffic_group - if traffic_group not in groups: - raise F5ModuleError( - 'The specified traffic group was not found' - ) - if vlan is not None: vlans = self.get_vlans() vlan = "/%s/%s" % (partition, vlan) @@ -527,9 +539,9 @@ def create(self): if traffic_group is None: params['trafficGroup'] = "/%s/%s" % (partition, DEFAULT_TG) else: - groups = self.api.tm.cm.traffic_groups.get_collection() - if traffic_group in groups: - params['trafficGroup'] = "/%s/%s" % (partition, traffic_group) + traffic_group = "/%s/%s" % (partition, traffic_group) + if traffic_group in self.traffic_groups(): + params['trafficGroup'] = traffic_group else: raise F5ModuleError( 'The specified traffic group was not found' From cf053d0e9c5bb7b9c9dc43faafadb9ee3ffde9ff Mon Sep 17 00:00:00 2001 From: "Ryan S. Brown" Date: Fri, 23 Sep 2016 11:54:53 -0400 Subject: [PATCH 2148/2522] Fix ec2_asg_facts when `name` parameter is None Closes #3021 --- cloud/amazon/ec2_asg_facts.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_asg_facts.py b/cloud/amazon/ec2_asg_facts.py index 857d0c20a0b..d6eb1dc6119 100644 --- a/cloud/amazon/ec2_asg_facts.py +++ b/cloud/amazon/ec2_asg_facts.py @@ -299,7 +299,11 @@ def find_asgs(conn, module, name=None, tags=None): module.fail_json(msg=e.message, **camel_dict_to_snake_dict(e.response)) matched_asgs = [] - name_prog = re.compile(r'^' + name) + + if name is not None: + # if the user didn't specify a name + name_prog = re.compile(r'^' + name) + for asg in asgs['AutoScalingGroups']: if name: matched_name = name_prog.search(asg['AutoScalingGroupName']) From 6444cb3f701e0cc7af9cb65609a9d15bdd1efb36 Mon Sep 17 00:00:00 2001 From: jctanner Date: Fri, 23 Sep 2016 14:09:53 -0400 Subject: [PATCH 2149/2522] vmware_guest: set the cpu and memory settings on clone (#3027) Fixes #3026 --- cloud/vmware/vmware_guest.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cloud/vmware/vmware_guest.py b/cloud/vmware/vmware_guest.py index 583d8b1a7d6..0ae40fb6315 100644 --- a/cloud/vmware/vmware_guest.py +++ b/cloud/vmware/vmware_guest.py @@ -617,6 +617,17 @@ def deploy_template(self, poweron=False, wait_for_ip=False): configspec = vim.vm.ConfigSpec(deviceChange=[diskspec]) clonespec_kwargs['config'] = configspec + # set cpu/memory/etc + if 'hardware' in self.params: + if not 'config' in clonespec_kwargs: + clonespec_kwargs['config'] = vim.vm.ConfigSpec() + if 'num_cpus' in self.params['hardware']: + clonespec_kwargs['config'].numCPUs = \ + int(self.params['hardware']['num_cpus']) + if 'memory_mb' in self.params['hardware']: + clonespec_kwargs['config'].memoryMB = \ + int(self.params['hardware']['memory_mb']) + clonespec = vim.vm.CloneSpec(**clonespec_kwargs) task = template.Clone(folder=destfolder, name=self.params['name'], spec=clonespec) self.wait_for_task(task) From 8aa338ddfa8cd00f96b60391b0d8cd2ebc8c822b Mon Sep 17 00:00:00 2001 From: Lujeni Date: Fri, 23 Sep 2016 21:44:42 +0200 Subject: [PATCH 2150/2522] Fix mongodb_user default role value when update_password is set (#2997) --- database/misc/mongodb_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index 551b565088e..669c68516fd 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -322,7 +322,7 @@ def main(): ssl_cert_reqs = None if ssl: ssl_cert_reqs = getattr(ssl_lib, module.params['ssl_cert_reqs']) - roles = module.params['roles'] + roles = module.params['roles'] or [] state = module.params['state'] update_password = module.params['update_password'] From a482a63ef6f09f2c90382233b1e12b4c4f471471 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Fri, 23 Sep 2016 15:12:24 -0700 Subject: [PATCH 2151/2522] Fix handling of ansible-doc errors. (#3030) --- test/utils/shippable/docs.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/utils/shippable/docs.sh b/test/utils/shippable/docs.sh index 9b5a6164f64..2858f87c997 100755 --- a/test/utils/shippable/docs.sh +++ b/test/utils/shippable/docs.sh @@ -42,10 +42,12 @@ pip list source hacking/env-setup +docs_status=0 + PAGER=/bin/cat \ ANSIBLE_DEPRECATION_WARNINGS=false \ bin/ansible-doc -l \ - 2>/tmp/ansible-doc.err + 2>/tmp/ansible-doc.err || docs_status=$? if [ -s /tmp/ansible-doc.err ]; then # report warnings as errors @@ -53,3 +55,8 @@ if [ -s /tmp/ansible-doc.err ]; then cat /tmp/ansible-doc.err exit 1 fi + +if [ "${docs_status}" -ne 0 ]; then + echo "Running 'ansible-doc -l' failed with no output on stderr and exit code: ${docs_status}" + exit 1 +fi From 7df7b52a5e4ce876910aaa61bc75d5e66d16adb6 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Fri, 23 Sep 2016 16:05:10 -0700 Subject: [PATCH 2152/2522] Set PRIVILEGED=true for Linux integration tests. (#3031) This should allow test_mount tests to run on Shippable. --- shippable.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/shippable.yml b/shippable.yml index a8dd0fc5a9f..d78203de3b9 100644 --- a/shippable.yml +++ b/shippable.yml @@ -8,16 +8,16 @@ matrix: exclude: - env: TEST=none include: - - env: TEST=integration IMAGE=ansible/ansible:centos6 - - env: TEST=integration IMAGE=ansible/ansible:centos7 - - env: TEST=integration IMAGE=ansible/ansible:fedora-rawhide - - env: TEST=integration IMAGE=ansible/ansible:fedora23 - - env: TEST=integration IMAGE=ansible/ansible:opensuseleap + - env: TEST=integration IMAGE=ansible/ansible:centos6 PRIVILEGED=true + - env: TEST=integration IMAGE=ansible/ansible:centos7 PRIVILEGED=true + - env: TEST=integration IMAGE=ansible/ansible:fedora-rawhide PRIVILEGED=true + - env: TEST=integration IMAGE=ansible/ansible:fedora23 PRIVILEGED=true + - env: TEST=integration IMAGE=ansible/ansible:opensuseleap PRIVILEGED=true - env: TEST=integration IMAGE=ansible/ansible:ubuntu1204 PRIVILEGED=true - env: TEST=integration IMAGE=ansible/ansible:ubuntu1404 PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604 + - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604 PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604py3 PYTHON3=1 + - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604py3 PYTHON3=1 PRIVILEGED=true - env: TEST=integration PLATFORM=windows VERSION=2008-SP2 - env: TEST=integration PLATFORM=windows VERSION=2008-R2_SP1 From 5e174c6e50ed57c685412b83814b4d6eaeda81eb Mon Sep 17 00:00:00 2001 From: shane-walker Date: Sat, 24 Sep 2016 01:31:40 -0500 Subject: [PATCH 2153/2522] Fixes #1375, will check for new and outdated packages when running. (#3020) --- packaging/language/npm.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packaging/language/npm.py b/packaging/language/npm.py index 53ce1e77c5e..e15bbea903e 100644 --- a/packaging/language/npm.py +++ b/packaging/language/npm.py @@ -252,7 +252,10 @@ def main(): elif state == 'latest': installed, missing = npm.list() outdated = npm.list_outdated() - if len(missing) or len(outdated): + if len(missing): + changed = True + npm.install() + if len(outdated): changed = True npm.update() else: #absent From 0dbb0b131f02ed6df4f7b0dc2962a326bc17bfab Mon Sep 17 00:00:00 2001 From: steve-dave Date: Sat, 24 Sep 2016 16:37:57 +1000 Subject: [PATCH 2154/2522] ec2_win_password.py - handle missing or unparseable key file more intuitively (#2729) --- cloud/amazon/ec2_win_password.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/cloud/amazon/ec2_win_password.py b/cloud/amazon/ec2_win_password.py index 4ddf4f8f4cc..e0f6205f3b6 100644 --- a/cloud/amazon/ec2_win_password.py +++ b/cloud/amazon/ec2_win_password.py @@ -142,9 +142,15 @@ def main(): try: f = open(key_file, 'r') - key = RSA.importKey(f.read(), key_passphrase) - finally: - f.close() + except IOError as e: + module.fail_json(msg = "I/O error (%d) opening key file: %s" % (e.errno, e.strerror)) + else: + try: + with f: + key = RSA.importKey(f.read(), key_passphrase) + except (ValueError, IndexError, TypeError) as e: + module.fail_json(msg = "unable to parse key file") + cipher = PKCS1_v1_5.new(key) sentinel = 'password decryption failed!!!' From 720f27d63a4e26ae7d9c1a9c4b4a37be2ddaaa0a Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Fri, 23 Sep 2016 23:39:27 -0700 Subject: [PATCH 2155/2522] Revert "Set PRIVILEGED=true for Linux integration tests. (#3031)" This reverts commit 7df7b52a5e4ce876910aaa61bc75d5e66d16adb6. --- shippable.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/shippable.yml b/shippable.yml index d78203de3b9..a8dd0fc5a9f 100644 --- a/shippable.yml +++ b/shippable.yml @@ -8,16 +8,16 @@ matrix: exclude: - env: TEST=none include: - - env: TEST=integration IMAGE=ansible/ansible:centos6 PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:centos7 PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:fedora-rawhide PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:fedora23 PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:opensuseleap PRIVILEGED=true + - env: TEST=integration IMAGE=ansible/ansible:centos6 + - env: TEST=integration IMAGE=ansible/ansible:centos7 + - env: TEST=integration IMAGE=ansible/ansible:fedora-rawhide + - env: TEST=integration IMAGE=ansible/ansible:fedora23 + - env: TEST=integration IMAGE=ansible/ansible:opensuseleap - env: TEST=integration IMAGE=ansible/ansible:ubuntu1204 PRIVILEGED=true - env: TEST=integration IMAGE=ansible/ansible:ubuntu1404 PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604 PRIVILEGED=true + - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604 - - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604py3 PYTHON3=1 PRIVILEGED=true + - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604py3 PYTHON3=1 - env: TEST=integration PLATFORM=windows VERSION=2008-SP2 - env: TEST=integration PLATFORM=windows VERSION=2008-R2_SP1 From 7aab9cd93b6c27a44a9dcbfa5c902b4e37e4b40c Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sun, 25 Sep 2016 22:07:24 +0200 Subject: [PATCH 2156/2522] Fix error in description of acme_directory (#3034) --- web_infrastructure/letsencrypt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_infrastructure/letsencrypt.py b/web_infrastructure/letsencrypt.py index d5afebeaae2..e35b0e37aa1 100644 --- a/web_infrastructure/letsencrypt.py +++ b/web_infrastructure/letsencrypt.py @@ -66,7 +66,7 @@ - "The ACME directory to use. This is the entry point URL to access CA server API." - "For safety reasons the default is set to the Let's Encrypt staging server. - This will create technically correct, but untrusted certifiactes." + This will create technically correct, but untrusted certificates." required: false default: https://acme-staging.api.letsencrypt.org/directory agreement: From c1adb215f045ebe1c951878e475223583afc22b9 Mon Sep 17 00:00:00 2001 From: Lujeni Date: Mon, 26 Sep 2016 13:17:08 +0200 Subject: [PATCH 2157/2522] Fix mongodb_user ssl_cert_reqs param (#2963) (#2965) --- database/misc/mongodb_user.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index 669c68516fd..3f391a7e94e 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -320,20 +320,24 @@ def main(): password = module.params['password'] ssl = module.params['ssl'] ssl_cert_reqs = None - if ssl: - ssl_cert_reqs = getattr(ssl_lib, module.params['ssl_cert_reqs']) roles = module.params['roles'] or [] state = module.params['state'] update_password = module.params['update_password'] try: + connection_params = { + "host": login_host, + "port": int(login_port), + } + if replica_set: - client = MongoClient(login_host, int(login_port), - replicaset=replica_set, ssl=ssl, - ssl_cert_reqs=ssl_cert_reqs) - else: - client = MongoClient(login_host, int(login_port), ssl=ssl, - ssl_cert_reqs=ssl_cert_reqs) + connection_params["replicaset"] = replica_set + + if ssl: + connection_params["ssl"] = ssl + connection_params["ssl_cert_reqs"] = getattr(ssl_lib, module.params['ssl_cert_reqs']) + + client = MongoClient(**connection_params) if login_user is None and login_password is None: mongocnf_creds = load_mongocnf() From cbdc798ad218e41dff22364da11c83d1e6d71f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Mon, 26 Sep 2016 13:48:30 +0200 Subject: [PATCH 2158/2522] twilio: fix false negative failure - sms was sent successfully (#3033) --- notification/twilio.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/notification/twilio.py b/notification/twilio.py index 216520620f9..2c7275a3e9f 100644 --- a/notification/twilio.py +++ b/notification/twilio.py @@ -161,8 +161,12 @@ def main(): for number in to_number: r, info = post_twilio_api(module, account_sid, auth_token, msg, from_number, number, media_url) - if info['status'] != 200: - module.fail_json(msg="unable to send message to %s" % number) + if info['status'] not in [200, 201]: + body_message = "unknown error" + if 'body' in info: + body = json.loads(info['body']) + body_message = body['message'] + module.fail_json(msg="unable to send message to %s: %s" % (number, body_message)) module.exit_json(msg=msg, changed=False) From c38fcb1c309bd394d34104b8da7dd7c6257b7c58 Mon Sep 17 00:00:00 2001 From: Ryan Saunders Date: Mon, 26 Sep 2016 12:11:05 -0400 Subject: [PATCH 2159/2522] Fix documentation typos and broken URL (#3039) --- network/dnsmadeeasy.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/network/dnsmadeeasy.py b/network/dnsmadeeasy.py index 3f22c3ca0a6..4578b5298be 100644 --- a/network/dnsmadeeasy.py +++ b/network/dnsmadeeasy.py @@ -20,17 +20,17 @@ version_added: "1.3" short_description: Interface with dnsmadeeasy.com (a DNS hosting service). description: - - "Manages DNS records via the v2 REST API of the DNS Made Easy service. It handles records only; there is no manipulation of domains or monitor/account support yet. See: U(http://www.dnsmadeeasy.com/services/rest-api/)" + - "Manages DNS records via the v2 REST API of the DNS Made Easy service. It handles records only; there is no manipulation of domains or monitor/account support yet. See: U(https://www.dnsmadeeasy.com/integration/restapi/)" options: account_key: description: - - Accout API Key. + - Account API Key. required: true default: null account_secret: description: - - Accout Secret Key. + - Account Secret Key. required: true default: null From a42383adc1187f0fda744bbfcab377b7585bade0 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sun, 25 Sep 2016 23:06:01 +0200 Subject: [PATCH 2160/2522] Add missing deps, since we use openssl to verify the date --- web_infrastructure/letsencrypt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/web_infrastructure/letsencrypt.py b/web_infrastructure/letsencrypt.py index e35b0e37aa1..6772b524fb8 100644 --- a/web_infrastructure/letsencrypt.py +++ b/web_infrastructure/letsencrypt.py @@ -49,6 +49,7 @@ protocol." requirements: - "python >= 2.6" + - openssl options: account_key: description: From 555439497136c127f5146509972b9adbcf8f4442 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sun, 25 Sep 2016 23:01:51 +0200 Subject: [PATCH 2161/2522] Use 'path' type for file arguments --- web_infrastructure/letsencrypt.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/web_infrastructure/letsencrypt.py b/web_infrastructure/letsencrypt.py index 6772b524fb8..a43014a8ab4 100644 --- a/web_infrastructure/letsencrypt.py +++ b/web_infrastructure/letsencrypt.py @@ -192,12 +192,11 @@ def get_cert_days(module,cert_file): Return the days the certificate in cert_file remains valid and -1 if the file was not found. ''' - _cert_file = os.path.expanduser(cert_file) - if not os.path.exists(_cert_file): + if not os.path.exists(cert_file): return -1 openssl_bin = module.get_bin_path('openssl', True) - openssl_cert_cmd = [openssl_bin, "x509", "-in", _cert_file, "-noout", "-text"] + openssl_cert_cmd = [openssl_bin, "x509", "-in", cert_file, "-noout", "-text"] _, out, _ = module.run_command(openssl_cert_cmd,check_rc=True) try: not_after_str = re.search(r"\s+Not After\s*:\s+(.*)",out.decode('utf8')).group(1) @@ -293,7 +292,7 @@ class ACMEAccount(object): def __init__(self,module): self.module = module self.agreement = module.params['agreement'] - self.key = os.path.expanduser(module.params['account_key']) + self.key = module.params['account_key'] self.email = module.params['account_email'] self.data = module.params['data'] self.directory = ACMEDirectory(module) @@ -498,8 +497,8 @@ class ACMEClient(object): def __init__(self,module): self.module = module self.challenge = module.params['challenge'] - self.csr = os.path.expanduser(module.params['csr']) - self.dest = os.path.expanduser(module.params['dest']) + self.csr = module.params['csr'] + self.dest = module.params['dest'] self.account = ACMEAccount(module) self.directory = self.account.directory self.authorizations = self.account.get_authorizations() @@ -756,14 +755,14 @@ def get_certificate(self): def main(): module = AnsibleModule( argument_spec = dict( - account_key = dict(required=True, type='str'), + account_key = dict(required=True, type='path'), account_email = dict(required=False, default=None, type='str'), acme_directory = dict(required=False, default='https://acme-staging.api.letsencrypt.org/directory', type='str'), agreement = dict(required=False, default='https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf', type='str'), challenge = dict(required=False, default='http-01', choices=['http-01', 'dns-01', 'tls-sni-02'], type='str'), - csr = dict(required=True, aliases=['src'], type='str'), + csr = dict(required=True, aliases=['src'], type='path'), data = dict(required=False, no_log=True, default=None, type='dict'), - dest = dict(required=True, aliases=['cert'], type='str'), + dest = dict(required=True, aliases=['cert'], type='path'), remaining_days = dict(required=False, default=10, type='int'), ), supports_check_mode = True, From 6a0cb85789a31b384fd2bcb61d9c944652e74133 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Sun, 25 Sep 2016 13:30:27 -0700 Subject: [PATCH 2162/2522] Distinguish between untagged and tagged vlans We were incorrectly making VLANS always be untagged when they could be either tagged or untagged. This change corrects the arguments to the vlan module to allow for specifying either untagged or tagged interfaces. The arguments are mutually exclusive --- network/f5/bigip_vlan.py | 117 +++++++++++++++++++++++++++++---------- 1 file changed, 87 insertions(+), 30 deletions(-) diff --git a/network/f5/bigip_vlan.py b/network/f5/bigip_vlan.py index 1fe6947eb43..4e13d2508c3 100644 --- a/network/f5/bigip_vlan.py +++ b/network/f5/bigip_vlan.py @@ -27,11 +27,21 @@ description: description: - The description to give to the VLAN. - interfaces: + tagged_interfaces: description: - - Specifies a list of tagged or untagged interfaces and trunks that you - want to configure for the VLAN. Use tagged interfaces or trunks when + - Specifies a list of tagged interfaces and trunks that you want to + configure for the VLAN. Use tagged interfaces or trunks when you want to assign a single interface or trunk to multiple VLANs. + required: false + aliases: + - tagged_interface + untagged_interfaces: + description: + - Specifies a list of untagged interfaces and trunks that you want to + configure for the VLAN. + required: false + aliases: + - untagged_interface name: description: - The VLAN to manage. If the special VLAN C(ALL) is specified with @@ -85,8 +95,7 @@ - name: Add VLAN 2345 as tagged to interface 1.1 bigip_vlan: - interfaces: - - 1.1 + tagged_interface: 1.1 name: "net1" password: "secret" server: "lb.mydomain.com" @@ -94,6 +103,19 @@ user: "admin" validate_certs: "no" delegate_to: localhost + +- name: Add VLAN 1234 as tagged to interfaces 1.1 and 1.2 + bigip_vlan: + tagged_interfaces: + - 1.1 + - 1.2 + name: "net1" + password: "secret" + server: "lb.mydomain.com" + tag: "1234" + user: "admin" + validate_certs: "no" + delegate_to: localhost ''' RETURN = ''' @@ -148,14 +170,10 @@ def __init__(self, *args, **kwargs): port=kwargs['server_port']) def present(self): - changed = False - if self.exists(): - changed = self.update() + return self.update() else: - changed = self.create() - - return changed + return self.create() def absent(self): changed = False @@ -188,7 +206,17 @@ def read(self): if hasattr(r, 'description'): p['description'] = str(r.description) if len(ifcs) is not 0: - p['interfaces'] = list(set([str(x.name) for x in ifcs])) + untagged = [] + tagged = [] + for x in ifcs: + if hasattr(x, 'tagged'): + tagged.append(str(x.name)) + elif hasattr(x, 'untagged'): + untagged.append(str(x.name)) + if untagged: + p['untagged_interfaces'] = list(set(untagged)) + if tagged: + p['tagged_interfaces'] = list(set(tagged)) p['name'] = name return p @@ -198,14 +226,16 @@ def create(self): check_mode = self.params['check_mode'] description = self.params['description'] name = self.params['name'] - interfaces = self.params['interfaces'] + untagged_interfaces = self.params['untagged_interfaces'] + tagged_interfaces = self.params['tagged_interfaces'] partition = self.params['partition'] tag = self.params['tag'] if tag is not None: params['tag'] = tag - if interfaces is not None: + if untagged_interfaces is not None or tagged_interfaces is not None: + tmp = [] ifcs = self.api.tm.net.interfaces.get_collection() ifcs = [str(x.name) for x in ifcs] @@ -215,12 +245,23 @@ def create(self): ) pinterfaces = [] + if untagged_interfaces: + interfaces = untagged_interfaces + elif tagged_interfaces: + interfaces = tagged_interfaces + for ifc in interfaces: ifc = str(ifc) if ifc in ifcs: pinterfaces.append(ifc) - if pinterfaces: - params['interfaces'] = pinterfaces + + if tagged_interfaces: + tmp = [dict(name=x, tagged=True) for x in pinterfaces] + elif untagged_interfaces: + tmp = [dict(name=x, untagged=True) for x in pinterfaces] + + if tmp: + params['interfaces'] = tmp if description is not None: params['description'] = self.params['description'] @@ -250,9 +291,10 @@ def update(self): name = self.params['name'] tag = self.params['tag'] partition = self.params['partition'] - interfaces = self.params['interfaces'] + tagged_interfaces = self.params['tagged_interfaces'] + untagged_interfaces = self.params['untagged_interfaces'] - if interfaces is not None: + if untagged_interfaces is not None or tagged_interfaces is not None: ifcs = self.api.tm.net.interfaces.get_collection() ifcs = [str(x.name) for x in ifcs] @@ -261,24 +303,35 @@ def update(self): 'No interfaces were found' ) + pinterfaces = [] + if untagged_interfaces: + interfaces = untagged_interfaces + elif tagged_interfaces: + interfaces = tagged_interfaces + for ifc in interfaces: ifc = str(ifc) if ifc in ifcs: - try: - pinterfaces.append(ifc) - except UnboundLocalError: - pinterfaces = [] - pinterfaces.append(ifc) + pinterfaces.append(ifc) else: raise F5ModuleError( 'The specified interface "%s" was not found' % (ifc) ) - if 'interfaces' in current: - if pinterfaces != current['interfaces']: - params['interfaces'] = pinterfaces - else: - params['interfaces'] = pinterfaces + if tagged_interfaces: + tmp = [dict(name=x, tagged=True) for x in pinterfaces] + if 'tagged_interfaces' in current: + if pinterfaces != current['tagged_interfaces']: + params['interfaces'] = tmp + else: + params['interfaces'] = tmp + elif untagged_interfaces: + tmp = [dict(name=x, untagged=True) for x in pinterfaces] + if 'untagged_interfaces' in current: + if pinterfaces != current['untagged_interfaces']: + params['interfaces'] = tmp + else: + params['interfaces'] = tmp if description is not None: if 'description' in current: @@ -361,7 +414,8 @@ def main(): meta_args = dict( description=dict(required=False, default=None), - interfaces=dict(required=False, default=None, type='list'), + tagged_interfaces=dict(required=False, default=None, type='list', aliases=['tagged_interface']), + untagged_interfaces=dict(required=False, default=None, type='list', aliases=['untagged_interface']), name=dict(required=True), tag=dict(required=False, default=None, type='int') ) @@ -369,7 +423,10 @@ def main(): module = AnsibleModule( argument_spec=argument_spec, - supports_check_mode=True + supports_check_mode=True, + mutually_exclusive=[ + ['tagged_interfaces', 'untagged_interfaces'] + ] ) try: From 119bc466be8d11263ecd2dac03054f39266e73b1 Mon Sep 17 00:00:00 2001 From: "Thierno IB. BARRY" Date: Tue, 27 Sep 2016 00:18:42 +0200 Subject: [PATCH 2163/2522] elasticsearch_plugin: rewrite module to not use unsupported parameters (#2839) * elasticsearch_plugin: rewrite module to not use unsupported parameters (#1785) Avoid using parameters when they are not needed (#1785) * elasticsearch_plugin: add version only during plugin installation and parse plugin name for its removal * elasticsearch_plugin: join command args before running it --- packaging/elasticsearch_plugin.py | 100 ++++++++++++++++++------------ 1 file changed, 59 insertions(+), 41 deletions(-) diff --git a/packaging/elasticsearch_plugin.py b/packaging/elasticsearch_plugin.py index d9c05b1ac56..7e01b4a4d5c 100644 --- a/packaging/elasticsearch_plugin.py +++ b/packaging/elasticsearch_plugin.py @@ -39,7 +39,7 @@ description: - Desired state of a plugin. required: False - choices: [present, absent] + choices: ["present", "absent"] default: present url: description: @@ -92,6 +92,10 @@ - elasticsearch_plugin: state=absent name="mobz/elasticsearch-head" ''' +PACKAGE_STATE_MAP = dict( + present="install", + absent="remove" +) def parse_plugin_repo(string): elements = string.split("/") @@ -111,11 +115,9 @@ def parse_plugin_repo(string): return repo - def is_plugin_present(plugin_dir, working_dir): return os.path.isdir(os.path.join(working_dir, plugin_dir)) - def parse_error(string): reason = "reason: " try: @@ -123,18 +125,49 @@ def parse_error(string): except ValueError: return string +def install_plugin(module, plugin_bin, plugin_name, version, url, proxy_host, proxy_port, timeout): + cmd_args = [plugin_bin, PACKAGE_STATE_MAP["present"], plugin_name] -def main(): + if version: + name = name + '/' + version - package_state_map = dict( - present="install", - absent="remove" - ) + if proxy_host and proxy_port: + cmd_args.append("-DproxyHost=%s -DproxyPort=%s" % (proxy_host, proxy_port)) + + if url: + cmd_args.append("--url %s" % url) + + if timeout: + cmd_args.append("--timeout %s" % timeout) + cmd = " ".join(cmd_args) + + rc, out, err = module.run_command(cmd) + + if rc != 0: + reason = parse_error(out) + module.fail_json(msg=reason) + + return True, cmd, out, err + +def remove_plugin(module, plugin_bin, plugin_name): + cmd_args = [plugin_bin, PACKAGE_STATE_MAP["absent"], parse_plugin_repo(plugin_name)] + + cmd = " ".join(cmd_args) + + rc, out, err = module.run_command(cmd) + + if rc != 0: + reason = parse_error(out) + module.fail_json(msg=reason) + + return True, cmd, out, err + +def main(): module = AnsibleModule( argument_spec=dict( name=dict(required=True), - state=dict(default="present", choices=package_state_map.keys()), + state=dict(default="present", choices=PACKAGE_STATE_MAP.keys()), url=dict(default=None), timeout=dict(default="1m"), plugin_bin=dict(default="/usr/share/elasticsearch/bin/plugin", type="path"), @@ -145,46 +178,31 @@ def main(): ) ) - name = module.params["name"] - state = module.params["state"] - url = module.params["url"] - timeout = module.params["timeout"] - plugin_bin = module.params["plugin_bin"] - plugin_dir = module.params["plugin_dir"] - proxy_host = module.params["proxy_host"] - proxy_port = module.params["proxy_port"] - version = module.params["version"] + name = module.params["name"] + state = module.params["state"] + url = module.params["url"] + timeout = module.params["timeout"] + plugin_bin = module.params["plugin_bin"] + plugin_dir = module.params["plugin_dir"] + proxy_host = module.params["proxy_host"] + proxy_port = module.params["proxy_port"] + version = module.params["version"] present = is_plugin_present(parse_plugin_repo(name), plugin_dir) # skip if the state is correct if (present and state == "present") or (state == "absent" and not present): - module.exit_json(changed=False, name=name) + module.exit_json(changed=False, name=name, state=state) - if (version): - name = name + '/' + version + if state == "present": + changed, cmd, out, err = install_plugin(module, plugin_bin, name, version, url, proxy_host, proxy_port, timeout) - cmd_args = [plugin_bin, package_state_map[state], name] - - if proxy_host and proxy_port: - cmd_args.append("-DproxyHost=%s -DproxyPort=%s" % (proxy_host, proxy_port)) - - if url: - cmd_args.append("--url %s" % url) - - if timeout: - cmd_args.append("--timeout %s" % timeout) - - cmd = " ".join(cmd_args) - - rc, out, err = module.run_command(cmd) - - if rc != 0: - reason = parse_error(out) - module.fail_json(msg=reason) + elif state == "absent": + changed, cmd, out, err = remove_plugin(module, plugin_bin, name) - module.exit_json(changed=True, cmd=cmd, name=name, state=state, url=url, timeout=timeout, stdout=out, stderr=err) + module.exit_json(changed=changed, cmd=cmd, name=name, state=state, url=url, timeout=timeout, stdout=out, stderr=err) from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() From aeecd8b09ea95759a0ef16528376f5590badebdf Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Tue, 27 Sep 2016 17:13:36 +0200 Subject: [PATCH 2164/2522] ovirt_vms: wait for VM to be created for states stopped/suspended (#3044) --- cloud/ovirt/ovirt_vms.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cloud/ovirt/ovirt_vms.py b/cloud/ovirt/ovirt_vms.py index 8aa4a98364b..0652b24c10a 100644 --- a/cloud/ovirt/ovirt_vms.py +++ b/cloud/ovirt/ovirt_vms.py @@ -761,6 +761,7 @@ def main(): ) elif state == 'stopped': vms_module.create( + result_state=otypes.VmStatus.DOWN if vm is None else None, clone=module.params['clone'], clone_permissions=module.params['clone_permissions'], ) @@ -781,6 +782,7 @@ def main(): ) elif state == 'suspended': vms_module.create( + result_state=otypes.VmStatus.DOWN if vm is None else None, clone=module.params['clone'], clone_permissions=module.params['clone_permissions'], ) From 48d6bbd8942e9f04bb1658c04233e75f370d3e06 Mon Sep 17 00:00:00 2001 From: John R Barker Date: Wed, 28 Sep 2016 12:09:02 +0100 Subject: [PATCH 2165/2522] Correct typos (#3055) --- network/asa/asa_config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/network/asa/asa_config.py b/network/asa/asa_config.py index 8c196cea1ef..e90f5fbfaae 100644 --- a/network/asa/asa_config.py +++ b/network/asa/asa_config.py @@ -23,8 +23,8 @@ author: "Peter Sprygada (@privateip), Patrick Ogenstad (@ogenstad)" short_description: Manage Cisco ASA configuration sections description: - - Cisco ASA configurations use a simple block indent file sytanx - for segementing configuration into sections. This module provides + - Cisco ASA configurations use a simple block indent file syntax + for segmenting configuration into sections. This module provides an implementation for working with ASA configuration sections in a deterministic way. extends_documentation_fragment: asa @@ -131,7 +131,7 @@ choices: ['yes', 'no'] config: description: - - The C(config) argument allows the playbook desginer to supply + - The C(config) argument allows the playbook designer to supply the base configuration to be used to validate configuration changes necessary. If this argument is provided, the module will not download the running-config from the remote node. From f51cb3cd5413e62f10f86511fc5258ab9ebe3977 Mon Sep 17 00:00:00 2001 From: John R Barker Date: Wed, 28 Sep 2016 14:00:39 +0100 Subject: [PATCH 2166/2522] asa_template is now deprecated (#2992) * asa_template is now deprecated * Delete asa_template Since asa_template was added during 2.2 development, rather than deprecate them --- network/asa/asa_template.py | 172 ------------------------------------ 1 file changed, 172 deletions(-) delete mode 100644 network/asa/asa_template.py diff --git a/network/asa/asa_template.py b/network/asa/asa_template.py deleted file mode 100644 index 6267da75860..00000000000 --- a/network/asa/asa_template.py +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/python -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . -# -DOCUMENTATION = """ ---- -module: asa_template -version_added: "2.2" -author: "Peter Sprygada (@privateip), Patrick Ogenstad (@ogenstad)" -short_description: Manage Cisco ASA device configurations over SSH -description: - - Manages Cisco ASA network device configurations over SSH. This module - allows implementors to work with the device running-config. It - provides a way to push a set of commands onto a network device - by evaluting the current running-config and only pushing configuration - commands that are not already configured. The config source can - be a set of commands or a template. -extends_documentation_fragment: asa -options: - src: - description: - - The path to the config source. The source can be either a - file with config or a template that will be merged during - runtime. By default the task will first search for the source - file in role or playbook root folder in templates unless a full - path to the file is given. - required: true - force: - description: - - The force argument instructs the module not to consider the - current device running-config. When set to true, this will - cause the module to push the contents of I(src) into the device - without first checking if already configured. - required: false - default: false - choices: [ "true", "false" ] - include_defaults: - description: - - The module, by default, will collect the current device - running-config to use as a base for comparision to the commands - in I(src). Setting this value to true will cause the command - issued to add any necessary flags to collect all defaults as - well as the device configuration. If the destination device - does not support such a flag, this argument is silently ignored. - required: false - default: false - choices: [ "true", "false" ] - backup: - description: - - When this argument is configured true, the module will backup - the running-config from the node prior to making any changes. - The backup file will be written to backup_{{ hostname }} in - the root of the playbook directory. - required: false - default: false - choices: [ "true", "false" ] - config: - description: - - The module, by default, will connect to the remote device and - retrieve the current running-config to use as a base for comparing - against the contents of source. There are times when it is not - desirable to have the task get the current running-config for - every task. The I(config) argument allows the implementer to - pass in the configuruation to use as the base config for - comparision. - required: false - default: null -""" - -EXAMPLES = """ -- name: push a configuration onto the device - asa_template: - host: hostname - username: foo - src: config.j2 - -- name: forceable push a configuration onto the device - asa_template: - host: hostname - username: foo - src: config.j2 - force: yes - -- name: provide the base configuration for comparision - asa_template: - host: hostname - username: foo - src: candidate_config.txt - config: current_config.txt -""" - -RETURN = """ -updates: - description: The set of commands that will be pushed to the remote device - returned: always - type: list - sample: ['...', '...'] - -responses: - description: The set of responses from issuing the commands on the device - retured: when not check_mode - type: list - sample: ['...', '...'] -""" -from ansible.module_utils.netcfg import NetworkConfig, dumps -from ansible.module_utils.asa import NetworkModule, NetworkError - -def get_config(module): - config = module.params['config'] or dict() - if not config and not module.params['force']: - config = module.config.get_config() - return config - -def main(): - """ main entry point for module execution - """ - - argument_spec = dict( - src=dict(), - force=dict(default=False, type='bool'), - include_defaults=dict(default=True, type='bool'), - backup=dict(default=False, type='bool'), - config=dict(), - ) - - mutually_exclusive = [('config', 'backup'), ('config', 'force')] - - module = NetworkModule(argument_spec=argument_spec, - mutually_exclusive=mutually_exclusive, - supports_check_mode=True) - - result = dict(changed=False) - - candidate = NetworkConfig(contents=module.params['src'], indent=1) - - contents = get_config(module) - if contents: - config = NetworkConfig(contents=contents, indent=1) - result['_backup'] = str(contents) - - if not module.params['force']: - commands = candidate.difference(config) - commands = dumps(commands, 'commands').split('\n') - commands = [str(c) for c in commands if c] - else: - commands = str(candidate).split('\n') - - if commands: - if not module.check_mode: - response = module.config(commands) - result['responses'] = response - result['changed'] = True - - result['updates'] = commands - module.exit_json(**result) - - -if __name__ == '__main__': - main() From 881d2f95a8e61e385a5a4522fb82121f18de8a07 Mon Sep 17 00:00:00 2001 From: Rob Date: Wed, 28 Sep 2016 23:24:39 +1000 Subject: [PATCH 2167/2522] If parameters for policy, tags or versioning are not supplied, do not change the existing values defined for the bucket (#2938) --- cloud/amazon/s3_bucket.py | 104 ++++++++++++++++++++------------------ 1 file changed, 56 insertions(+), 48 deletions(-) diff --git a/cloud/amazon/s3_bucket.py b/cloud/amazon/s3_bucket.py index 35d4077c39a..704b6e73fe8 100644 --- a/cloud/amazon/s3_bucket.py +++ b/cloud/amazon/s3_bucket.py @@ -68,7 +68,7 @@ description: - Whether versioning is enabled or disabled (note that once versioning is enabled, it can only be suspended) required: false - default: no + default: null choices: [ 'yes', 'no' ] extends_documentation_fragment: - aws @@ -122,6 +122,7 @@ except ImportError: HAS_BOTO = False + def get_request_payment_status(bucket): response = bucket.get_request_payment() @@ -131,6 +132,7 @@ def get_request_payment_status(bucket): return (payer != "BucketOwner") + def create_tags_container(tags): tag_set = TagSet() @@ -141,6 +143,7 @@ def create_tags_container(tags): tags_obj.add_tag_set(tag_set) return tags_obj + def _create_or_update_bucket(connection, module, location): policy = module.params.get("policy") @@ -161,22 +164,22 @@ def _create_or_update_bucket(connection, module, location): # Versioning versioning_status = bucket.get_versioning_status() - if not versioning_status: - if versioning: - try: - bucket.configure_versioning(versioning) - changed = True - versioning_status = bucket.get_versioning_status() - except S3ResponseError as e: - module.fail_json(msg=e.message) - elif versioning_status['Versioning'] == "Enabled" and not versioning: - bucket.configure_versioning(versioning) - changed = True - versioning_status = bucket.get_versioning_status() - elif ( (versioning_status['Versioning'] == "Disabled" and versioning) or (versioning_status['Versioning'] == "Suspended" and versioning) ): - bucket.configure_versioning(versioning) - changed = True - versioning_status = bucket.get_versioning_status() + if versioning_status: + if versioning is not None: + if versioning and versioning_status['Versioning'] != "Enabled": + try: + bucket.configure_versioning(versioning) + changed = True + versioning_status = bucket.get_versioning_status() + except S3ResponseError as e: + module.fail_json(msg=e.message) + elif not versioning and versioning_status['Versioning'] != "Enabled": + try: + bucket.configure_versioning(versioning) + changed = True + versioning_status = bucket.get_versioning_status() + except S3ResponseError as e: + module.fail_json(msg=e.message) # Requester pays requester_pays_status = get_request_payment_status(bucket) @@ -198,27 +201,26 @@ def _create_or_update_bucket(connection, module, location): else: module.fail_json(msg=e.message) - if policy is not None: - compare_policy = json.loads(policy) - - if current_policy is None or json.loads(current_policy) != compare_policy: + if current_policy is not None: + if policy == {}: try: - bucket.set_policy(policy) + bucket.delete_policy() changed = True current_policy = bucket.get_policy() except S3ResponseError as e: - module.fail_json(msg=e.message) - elif current_policy is not None: + if e.error_code == "NoSuchBucketPolicy": + current_policy = None + else: + module.fail_json(msg=e.message) + if policy is not None: + if json.loads(current_policy) != json.loads(policy): + try: + bucket.set_policy(policy) + changed = True + current_policy = bucket.get_policy() + except S3ResponseError as e: + module.fail_json(msg=e.message) - try: - bucket.delete_policy() - changed = True - current_policy = bucket.get_policy() - except S3ResponseError as e: - if e.error_code == "NoSuchBucketPolicy": - current_policy = None - else: - module.fail_json(msg=e.message) # Tags try: current_tags = bucket.get_tags() @@ -228,13 +230,12 @@ def _create_or_update_bucket(connection, module, location): else: module.fail_json(msg=e.message) - if current_tags is not None or tags is not None: - - if current_tags is None: - current_tags_dict = {} - else: - current_tags_dict = dict((t.key, t.value) for t in current_tags[0]) + if current_tags is None: + current_tags_dict = {} + else: + current_tags_dict = dict((t.key, t.value) for t in current_tags[0]) + if tags is not None: if current_tags_dict != tags: try: if tags: @@ -248,6 +249,7 @@ def _create_or_update_bucket(connection, module, location): module.exit_json(changed=changed, name=bucket.name, versioning=versioning_status, requester_pays=requester_pays_status, policy=current_policy, tags=current_tags_dict) + def _destroy_bucket(connection, module): force = module.params.get("force") @@ -280,6 +282,7 @@ def _destroy_bucket(connection, module): module.exit_json(changed=changed) + def _create_or_update_bucket_ceph(connection, module, location): #TODO: add update @@ -301,22 +304,26 @@ def _create_or_update_bucket_ceph(connection, module, location): else: module.fail_json(msg='Unable to create bucket, no error from the API') + def _destroy_bucket_ceph(connection, module): _destroy_bucket(connection, module) + def create_or_update_bucket(connection, module, location, flavour='aws'): if flavour == 'ceph': _create_or_update_bucket_ceph(connection, module, location) else: _create_or_update_bucket(connection, module, location) + def destroy_bucket(connection, module, flavour='aws'): if flavour == 'ceph': _destroy_bucket_ceph(connection, module) else: _destroy_bucket(connection, module) + def is_fakes3(s3_url): """ Return True if s3_url has scheme fakes3:// """ if s3_url is not None: @@ -324,6 +331,7 @@ def is_fakes3(s3_url): else: return False + def is_walrus(s3_url): """ Return True if it's Walrus endpoint, not S3 @@ -339,15 +347,15 @@ def main(): argument_spec = ec2_argument_spec() argument_spec.update( dict( - force = dict(required=False, default='no', type='bool'), - policy = dict(required=False, type='json'), - name = dict(required=True, type='str'), - requester_pays = dict(default='no', type='bool'), - s3_url = dict(aliases=['S3_URL'], type='str'), - state = dict(default='present', type='str', choices=['present', 'absent']), - tags = dict(required=None, default={}, type='dict'), - versioning = dict(default='no', type='bool'), - ceph = dict(default='no', type='bool') + force=dict(required=False, default='no', type='bool'), + policy=dict(required=False, type='json'), + name=dict(required=True, type='str'), + requester_pays=dict(default='no', type='bool'), + s3_url=dict(aliases=['S3_URL'], type='str'), + state=dict(default='present', type='str', choices=['present', 'absent']), + tags=dict(required=False, default=None, type='dict'), + versioning=dict(default=None, type='bool'), + ceph=dict(default='no', type='bool') ) ) From eaf3ace6a6a1d1fd1356c51fbbdf034ac26d5021 Mon Sep 17 00:00:00 2001 From: Ryan Brown Date: Wed, 28 Sep 2016 16:31:40 -0400 Subject: [PATCH 2168/2522] Fix owner on gce_img --- cloud/google/gce_img.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/google/gce_img.py b/cloud/google/gce_img.py index 32c3d238ca3..270ae827ddf 100644 --- a/cloud/google/gce_img.py +++ b/cloud/google/gce_img.py @@ -84,7 +84,7 @@ requirements: - "python >= 2.6" - "apache-libcloud" -author: "Peter Tan (@tanpeter)" +author: "Tom Melendez (supertom)" ''' EXAMPLES = ''' From 48e55c9d7a772b14d436ae0e4fda53da6a164ddb Mon Sep 17 00:00:00 2001 From: Bryan Hundven Date: Wed, 28 Sep 2016 15:18:43 -0700 Subject: [PATCH 2169/2522] Fix the comment in the example. (#3050) Fix the typo: `Crate` -> `Create`. Make a complete sentence. Signed-off-by: Bryan Hundven --- cloud/vmware/vmware_guest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/vmware/vmware_guest.py b/cloud/vmware/vmware_guest.py index 0ae40fb6315..3d7ab028e88 100644 --- a/cloud/vmware/vmware_guest.py +++ b/cloud/vmware/vmware_guest.py @@ -95,7 +95,7 @@ EXAMPLES = ''' Example from Ansible playbook # -# Crate VM from template +# Create a VM from a template # - name: create the VM vmware_guest: From 085321e3a879575340ad83b4169606a6139d75ce Mon Sep 17 00:00:00 2001 From: THEBAULT Julien Date: Tue, 27 Sep 2016 16:05:54 +0200 Subject: [PATCH 2170/2522] Fix mongodb user compatibility check (#2731) - Check the compatibility asap --- database/misc/mongodb_user.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index 3f391a7e94e..33187b35b9d 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -174,13 +174,26 @@ # def check_compatibility(module, client): - srv_info = client.server_info() - if LooseVersion(srv_info['version']) >= LooseVersion('3.2') and LooseVersion(PyMongoVersion) <= LooseVersion('3.2'): + """Check the compatibility between the driver and the database. + + See: https://docs.mongodb.com/ecosystem/drivers/driver-compatibility-reference/#python-driver-compatibility + + Args: + module: Ansible module. + client (cursor): Mongodb cursor on admin database. + """ + loose_srv_version = LooseVersion(client.server_info()['version']) + loose_driver_version = LooseVersion(PyMongoVersion) + + if loose_srv_version >= LooseVersion('3.2') and loose_driver_version <= LooseVersion('3.2'): module.fail_json(msg=' (Note: you must use pymongo 3.2+ with MongoDB >= 3.2)') - elif LooseVersion(srv_info['version']) >= LooseVersion('3.0') and LooseVersion(PyMongoVersion) <= LooseVersion('2.8'): + + elif loose_srv_version >= LooseVersion('3.0') and loose_driver_version <= LooseVersion('2.8'): module.fail_json(msg=' (Note: you must use pymongo 2.8+ with MongoDB 3.0)') - elif LooseVersion(srv_info['version']) >= LooseVersion('2.6') and LooseVersion(PyMongoVersion) <= LooseVersion('2.7'): + + elif loose_srv_version >= LooseVersion('2.6') and loose_srv_version <= LooseVersion('2.7'): module.fail_json(msg=' (Note: you must use pymongo 2.7+ with MongoDB 2.6)') + elif LooseVersion(PyMongoVersion) <= LooseVersion('2.5'): module.fail_json(msg=' (Note: you must be on mongodb 2.4+ and pymongo 2.5+ to use the roles param)') @@ -339,6 +352,10 @@ def main(): client = MongoClient(**connection_params) + # NOTE: this check must be done ASAP. + # We doesn't need to be authenticated. + check_compatibility(module, client) + if login_user is None and login_password is None: mongocnf_creds = load_mongocnf() if mongocnf_creds is not False: @@ -357,8 +374,6 @@ def main(): except Exception, e: module.fail_json(msg='unable to connect to database: %s' % str(e)) - check_compatibility(module, client) - if state == 'present': if password is None and update_password == 'always': module.fail_json(msg='password parameter required when adding a user unless update_password is set to on_create') From a58e1d59c0aac82ef8b1284e55d7cfea31e3317a Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 28 Sep 2016 23:09:20 -0700 Subject: [PATCH 2171/2522] Update shippable config (#3063) --- shippable.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/shippable.yml b/shippable.yml index a8dd0fc5a9f..23d731058e6 100644 --- a/shippable.yml +++ b/shippable.yml @@ -9,13 +9,15 @@ matrix: - env: TEST=none include: - env: TEST=integration IMAGE=ansible/ansible:centos6 - - env: TEST=integration IMAGE=ansible/ansible:centos7 - - env: TEST=integration IMAGE=ansible/ansible:fedora-rawhide - - env: TEST=integration IMAGE=ansible/ansible:fedora23 - - env: TEST=integration IMAGE=ansible/ansible:opensuseleap + - env: TEST=integration IMAGE=ansible/ansible:centos7 PRIVILEGED=true + - env: TEST=integration IMAGE=ansible/ansible:fedora-rawhide PRIVILEGED=true + - env: TEST=integration IMAGE=ansible/ansible:fedora23 PRIVILEGED=true + - env: TEST=integration IMAGE=ansible/ansible:opensuseleap PRIVILEGED=true - env: TEST=integration IMAGE=ansible/ansible:ubuntu1204 PRIVILEGED=true - env: TEST=integration IMAGE=ansible/ansible:ubuntu1404 PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604 + - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604 PRIVILEGED=true + + - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604py3 PYTHON3=1 PRIVILEGED=true - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604py3 PYTHON3=1 @@ -24,7 +26,7 @@ matrix: - env: TEST=integration PLATFORM=windows VERSION=2012-RTM - env: TEST=integration PLATFORM=windows VERSION=2012-R2_RTM - - env: TEST=integration PLATFORM=freebsd VERSION=10.3-STABLE + - env: TEST=integration PLATFORM=freebsd VERSION=10.3-STABLE PRIVILEGED=true - env: TEST=integration PLATFORM=osx VERSION=10.11 From d3e588cd7827ff4b157f22d116d384c4cf718406 Mon Sep 17 00:00:00 2001 From: John R Barker Date: Fri, 30 Sep 2016 07:04:46 +0100 Subject: [PATCH 2172/2522] Typos in doc's strings (#3071) --- network/asa/asa_acl.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/network/asa/asa_acl.py b/network/asa/asa_acl.py index b51dfefb4f4..55d3fd58cdd 100644 --- a/network/asa/asa_acl.py +++ b/network/asa/asa_acl.py @@ -31,7 +31,7 @@ - The ordered set of commands that should be configured in the section. The commands must be the exact same commands as found in the device running-config. Be sure to note the configuration - command syntanx as some commands are automatically modified by the + command syntax as some commands are automatically modified by the device config parser. required: true before: @@ -40,7 +40,7 @@ a change needs to be made. This allows the playbook designer the opportunity to perform configuration commands prior to pushing any changes without affecting how the set of commands are matched - against the system + against the system. required: false default: null after: @@ -69,7 +69,7 @@ the modified lines are pushed to the device in configuration mode. If the replace argument is set to I(block) then the entire command block is pushed to the device in configuration mode if any - line is not correct + line is not correct. required: false default: line choices: ['line', 'block'] From df35d324d62e6034ab86db0fb4a56d3ca122d4b2 Mon Sep 17 00:00:00 2001 From: Jonathan Mainguy Date: Sun, 2 Oct 2016 23:57:19 -0400 Subject: [PATCH 2173/2522] fix timezone for centos6 (#3078) --- system/timezone.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/timezone.py b/system/timezone.py index 9c616d6123c..2f04801790d 100644 --- a/system/timezone.py +++ b/system/timezone.py @@ -52,7 +52,7 @@ to configure, especially on virtual environments such as AWS. required: false aliases: ['rtc'] -author: "Shinichi TAMURA @tmshn" +author: "Shinichi TAMURA (@tmshn)" ''' RETURN = ''' @@ -402,7 +402,7 @@ def get(self, key, phase): def set_timezone(self, value): self._edit_file(filename=self.conf_files['name'], regexp=self.regexps['name'], - value=self.tzline_format.format % value) + value=self.tzline_format % value) self.execute(self.update_timezone) def set_hwclock(self, value): From 5cc72c3f068fe0def9ba2b757f6ab51e80a8eb38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Mon, 3 Oct 2016 20:05:10 +0200 Subject: [PATCH 2174/2522] slack: fix handling of html entities and escaping (#3032) for < > & ' and " --- notification/slack.py | 42 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/notification/slack.py b/notification/slack.py index 2ac609d451f..3639b6d8ac0 100644 --- a/notification/slack.py +++ b/notification/slack.py @@ -1,6 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- +# (c) 2016, René Moser # (c) 2015, Stefan Berggren # (c) 2014, Ramon de la Fuente # @@ -154,7 +155,7 @@ value: "load average: 5,16, 4,64, 2,43" short: "true" -- name: Send notification message via Slack (deprecated API using domian) +- name: Send notification message via Slack (deprecated API using domain) local_action: module: slack domain: future500.slack.com @@ -166,13 +167,27 @@ OLD_SLACK_INCOMING_WEBHOOK = 'https://%s/services/hooks/incoming-webhook?token=%s' SLACK_INCOMING_WEBHOOK = 'https://hooks.slack.com/services/%s' +# See https://api.slack.com/docs/message-formatting#how_to_escape_characters +# Escaping quotes and apostrophe however is related to how Ansible handles them. +html_escape_table = { + '&': "&", + '>': ">", + '<': "<", + '"': "\"", + "'": "\'", +} + +def html_escape(text): + '''Produce entities within text.''' + return "".join(html_escape_table.get(c,c) for c in text) + def build_payload_for_slack(module, text, channel, username, icon_url, icon_emoji, link_names, parse, color, attachments): payload = {} if color == "normal" and text is not None: - payload = dict(text=text) + payload = dict(text=html_escape(text)) elif text is not None: # With a custom color we have to set the message as attachment, and explicitely turn markdown parsing on for it. - payload = dict(attachments=[dict(text=text, color=color, mrkdwn_in=["text"])]) + payload = dict(attachments=[dict(text=html_escape(text), color=color, mrkdwn_in=["text"])]) if channel is not None: if (channel[0] == '#') or (channel[0] == '@'): payload['channel'] = channel @@ -194,12 +209,24 @@ def build_payload_for_slack(module, text, channel, username, icon_url, icon_emoj payload['attachments'] = [] if attachments is not None: + keys_to_escape = [ + 'title', + 'text', + 'author_name', + 'pretext', + 'fallback', + ] for attachment in attachments: + for key in keys_to_escape: + if key in attachment: + attachment[key] = html_escape(attachment[key]) + if 'fallback' not in attachment: attachment['fallback'] = attachment['text'] + payload['attachments'].append(attachment) - payload="payload=" + module.jsonify(payload) + payload=module.jsonify(payload) return payload def do_notify_slack(module, domain, token, payload): @@ -211,7 +238,12 @@ def do_notify_slack(module, domain, token, payload): module.fail_json(msg="Slack has updated its webhook API. You need to specify a token of the form XXXX/YYYY/ZZZZ in your playbook") slack_incoming_webhook = OLD_SLACK_INCOMING_WEBHOOK % (domain, token) - response, info = fetch_url(module, slack_incoming_webhook, data=payload) + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + } + response, info = fetch_url(module=module, url=slack_incoming_webhook, headers=headers, method='POST', data=payload) + if info['status'] != 200: obscured_incoming_webhook = SLACK_INCOMING_WEBHOOK % ('[obscured]') module.fail_json(msg=" failed to send %s to %s: %s" % (payload, obscured_incoming_webhook, info['msg'])) From 4c34b05b9a81b0c87508bca65da96c9d2a331738 Mon Sep 17 00:00:00 2001 From: Robin Roth Date: Wed, 5 Oct 2016 15:08:38 +0200 Subject: [PATCH 2175/2522] Add documention for enabled option in zypper_repository (#3083) --- packaging/os/zypper_repository.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packaging/os/zypper_repository.py b/packaging/os/zypper_repository.py index 5a06e6f9ded..e6e263c3e52 100644 --- a/packaging/os/zypper_repository.py +++ b/packaging/os/zypper_repository.py @@ -98,7 +98,13 @@ default: "no" choices: ["yes", "no"] version_added: "2.2" - + enabled: + description: + - Set repository to enabled (or disabled). + required: false + default: "yes" + choices: ["yes", "no"] + version_added: "2.2" requirements: From 843454a39672f172f1dbfd4f927c3e119485dcee Mon Sep 17 00:00:00 2001 From: Pieter Hollants Date: Wed, 5 Oct 2016 15:14:25 +0200 Subject: [PATCH 2176/2522] zypper_repository: Prepend --gpg-auto-import-keys before refresh command (#3088) Fixes #3086. --- packaging/os/zypper_repository.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packaging/os/zypper_repository.py b/packaging/os/zypper_repository.py index e6e263c3e52..db553970e84 100644 --- a/packaging/os/zypper_repository.py +++ b/packaging/os/zypper_repository.py @@ -280,9 +280,10 @@ def get_zypper_version(module): def runrefreshrepo(module, auto_import_keys=False, shortname=None): "Forces zypper to refresh repo metadata." - cmd = _get_cmd('refresh', '--force') if auto_import_keys: - cmd.append('--gpg-auto-import-keys') + cmd = _get_cmd('--gpg-auto-import-keys', 'refresh', '--force') + else: + cmd = _get_cmd('refresh', '--force') if shortname is not None: cmd.extend(['-r', shortname]) From fdd181afa7426564c01b65ff0820f88bab17609d Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Wed, 5 Oct 2016 09:03:16 -0700 Subject: [PATCH 2177/2522] Corrects the required SDK version (#3095) The SDK version that was mentioned originally was incorrect --- network/f5/bigip_ssl_certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/f5/bigip_ssl_certificate.py b/network/f5/bigip_ssl_certificate.py index 076caba9f54..9c6034d513e 100644 --- a/network/f5/bigip_ssl_certificate.py +++ b/network/f5/bigip_ssl_certificate.py @@ -88,7 +88,7 @@ tmsh or these modules. extends_documentation_fragment: f5 requirements: - - f5-sdk >= 1.3.1 + - f5-sdk >= 1.5.0 - BigIP >= v12 author: - Kevin Coming (@waffie1) From 1678a0ba69d5605f6d9c66a257cc1ca6055f5c14 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Wed, 5 Oct 2016 09:08:31 -0700 Subject: [PATCH 2178/2522] Adds copyright line for F5 contributed modules (#3081) I was asked to do this as part of the contribution effort in house --- network/f5/bigip_device_dns.py | 2 ++ network/f5/bigip_device_ntp.py | 2 ++ network/f5/bigip_device_sshd.py | 2 ++ network/f5/bigip_gtm_datacenter.py | 2 ++ network/f5/bigip_irule.py | 2 ++ network/f5/bigip_routedomain.py | 2 ++ network/f5/bigip_selfip.py | 2 ++ network/f5/bigip_sys_db.py | 2 ++ network/f5/bigip_vlan.py | 2 ++ 9 files changed, 18 insertions(+) diff --git a/network/f5/bigip_device_dns.py b/network/f5/bigip_device_dns.py index c469fc4bffa..a3f855e6be8 100644 --- a/network/f5/bigip_device_dns.py +++ b/network/f5/bigip_device_dns.py @@ -1,6 +1,8 @@ #!/usr/bin/python # -*- coding: utf-8 -*- # +# Copyright 2016 F5 Networks Inc. +# # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify diff --git a/network/f5/bigip_device_ntp.py b/network/f5/bigip_device_ntp.py index 6dab16a3cb0..a58a9f31ce7 100644 --- a/network/f5/bigip_device_ntp.py +++ b/network/f5/bigip_device_ntp.py @@ -1,6 +1,8 @@ #!/usr/bin/python # -*- coding: utf-8 -*- # +# Copyright 2016 F5 Networks Inc. +# # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify diff --git a/network/f5/bigip_device_sshd.py b/network/f5/bigip_device_sshd.py index e7a87a4e084..ac506f656b0 100644 --- a/network/f5/bigip_device_sshd.py +++ b/network/f5/bigip_device_sshd.py @@ -1,6 +1,8 @@ #!/usr/bin/python # -*- coding: utf-8 -*- # +# Copyright 2016 F5 Networks Inc. +# # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify diff --git a/network/f5/bigip_gtm_datacenter.py b/network/f5/bigip_gtm_datacenter.py index 90882b6f644..36308896ce1 100644 --- a/network/f5/bigip_gtm_datacenter.py +++ b/network/f5/bigip_gtm_datacenter.py @@ -1,6 +1,8 @@ #!/usr/bin/python # -*- coding: utf-8 -*- # +# Copyright 2016 F5 Networks Inc. +# # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify diff --git a/network/f5/bigip_irule.py b/network/f5/bigip_irule.py index 5e99ec34faa..4a7ff4d951f 100644 --- a/network/f5/bigip_irule.py +++ b/network/f5/bigip_irule.py @@ -1,6 +1,8 @@ #!/usr/bin/python # -*- coding: utf-8 -*- # +# Copyright 2016 F5 Networks Inc. +# # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify diff --git a/network/f5/bigip_routedomain.py b/network/f5/bigip_routedomain.py index 552b20231cc..0df2446184a 100644 --- a/network/f5/bigip_routedomain.py +++ b/network/f5/bigip_routedomain.py @@ -1,6 +1,8 @@ #!/usr/bin/python # -*- coding: utf-8 -*- # +# Copyright 2016 F5 Networks Inc. +# # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify diff --git a/network/f5/bigip_selfip.py b/network/f5/bigip_selfip.py index 6cbf7badb58..dc4403e60b8 100644 --- a/network/f5/bigip_selfip.py +++ b/network/f5/bigip_selfip.py @@ -1,6 +1,8 @@ #!/usr/bin/python # -*- coding: utf-8 -*- # +# Copyright 2016 F5 Networks Inc. +# # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify diff --git a/network/f5/bigip_sys_db.py b/network/f5/bigip_sys_db.py index 54f5dd74fc9..272fbf266c5 100644 --- a/network/f5/bigip_sys_db.py +++ b/network/f5/bigip_sys_db.py @@ -1,6 +1,8 @@ #!/usr/bin/python # -*- coding: utf-8 -*- # +# Copyright 2016 F5 Networks Inc. +# # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify diff --git a/network/f5/bigip_vlan.py b/network/f5/bigip_vlan.py index 4e13d2508c3..24e3a380295 100644 --- a/network/f5/bigip_vlan.py +++ b/network/f5/bigip_vlan.py @@ -1,6 +1,8 @@ #!/usr/bin/python # -*- coding: utf-8 -*- # +# Copyright 2016 F5 Networks Inc. +# # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify From ab2a74db5ff6e433247d65e8213915c733819c88 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Wed, 5 Oct 2016 09:08:55 -0700 Subject: [PATCH 2179/2522] Fixes the enabled_vlans argument (#3061) This argument had a couple of issues with it. First, as it was being interpreted in the code, it did not check for idempotency. Second, the model of having the parameters be "all_*" is going to hinder the ability to "undo", so-to-speak, what the user did while maintaining legibility. Consider if the user specified "all_enabled_vlans='net1'" and then decided they wanted to backout of this decision. What is the proper argument to fulfill this wish? "all_enabled_vlans='...?' This patch changes the all_enabled_vlans argument to be "enabled_vlans", ensures that idempotency works, and also provides for a way to "undo" a change to the enabled VLANs by allowing the user to specify the special case VLAN named "ALL" (all capitals). This makes the parameter more intuitive because the users will specify which vlans they want to make the virtual available on * enabled_vlans="net1" but also allows them to "undo" what they did by setting it back with the case of all * enabled_vlans="ALL" --- network/f5/bigip_virtual_server.py | 73 +++++++++++++++++++++++------- 1 file changed, 57 insertions(+), 16 deletions(-) diff --git a/network/f5/bigip_virtual_server.py b/network/f5/bigip_virtual_server.py index 89d25103f6e..158d81704d2 100644 --- a/network/f5/bigip_virtual_server.py +++ b/network/f5/bigip_virtual_server.py @@ -86,10 +86,11 @@ - List of rules to be applied in priority order required: false default: None - all_enabled_vlans: + enabled_vlans: version_added: "2.2" description: - - List of vlans to be enabled + - List of vlans to be enabled. When a VLAN named C(ALL) is used, all + VLANs will be allowed. required: false default: None pool: @@ -132,7 +133,7 @@ all_profiles: - http - clientssl - all_enabled_vlans: + enabled_vlans: - /Common/vlan2 delegate_to: localhost @@ -272,7 +273,7 @@ def set_profiles(api, name, profiles_list): try: if profiles_list is None: return False - current_profiles = map(lambda x: x['profile_name'], get_profiles(api, name)) + current_profiles = list(map(lambda x: x['profile_name'], get_profiles(api, name))) to_add_profiles = [] for x in profiles_list: if x not in current_profiles: @@ -297,25 +298,59 @@ def set_profiles(api, name, profiles_list): except bigsuds.OperationFailed as e: raise Exception('Error on setting profiles : %s' % e) + +def get_vlan(api, name): + return api.LocalLB.VirtualServer.get_vlan( + virtual_servers=[name] + )[0] + + def set_enabled_vlans(api, name, vlans_enabled_list): updated = False + to_add_vlans = [] try: if vlans_enabled_list is None: - return False - - to_add_vlans = [] - for x in vlans_enabled_list: - to_add_vlans.append(x) + return updated + current_vlans = get_vlan(api, name) + + # Set allowed list back to default ("all") + # + # This case allows you to undo what you may have previously done. + # The default case is "All VLANs and Tunnels". This case will handle + # that situation. + if 'ALL' in vlans_enabled_list: + # The user is coming from a situation where they previously + # were specifying a list of allowed VLANs + if len(current_vlans['vlans']) > 0 or \ + current_vlans['state'] is "STATE_ENABLED": + api.LocalLB.VirtualServer.set_vlan( + virtual_servers=[name], + vlans=[{'state': 'STATE_DISABLED', 'vlans': []}] + ) + updated = True + else: + if current_vlans['state'] is "STATE_DISABLED": + to_add_vlans = vlans_enabled_list + else: + for vlan in vlans_enabled_list: + if vlan not in current_vlans['vlans']: + updated = True + to_add_vlans = vlans_enabled_list + break + if updated: + api.LocalLB.VirtualServer.set_vlan( + virtual_servers=[name], + vlans=[{ + 'state': 'STATE_ENABLED', + 'vlans': [to_add_vlans] + }] + ) - api.LocalLB.VirtualServer.set_vlan( - virtual_servers=[name], - vlans = [{ 'state':'STATE_ENABLED', 'vlans':[to_add_vlans] }] - ) - updated = True return updated except bigsuds.OperationFailed as e: raise Exception('Error on setting enabled vlans : %s' % e) + def set_snat(api, name, snat): updated = False try: @@ -488,7 +523,7 @@ def main(): port=dict(type='int'), all_profiles=dict(type='list'), all_rules=dict(type='list'), - all_enabled_vlans=dict(type='list'), + enabled_vlans=dict(type='list'), pool=dict(type='str'), description=dict(type='str'), snat=dict(type='str'), @@ -521,7 +556,13 @@ def main(): port = module.params['port'] all_profiles = fq_list_names(partition, module.params['all_profiles']) all_rules = fq_list_names(partition, module.params['all_rules']) - all_enabled_vlans = fq_list_names(partition, module.params['all_enabled_vlans']) + + enabled_vlans = module.params['enabled_vlans'] + if enabled_vlans is None or 'ALL' in enabled_vlans: + all_enabled_vlans = enabled_vlans + else: + all_enabled_vlans = fq_list_names(partition, enabled_vlans) + pool = fq_name(partition, module.params['pool']) description = module.params['description'] snat = module.params['snat'] From 1adaaa4b0aff464b497af6b47dd3d0588d25528a Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Wed, 5 Oct 2016 20:14:32 -0400 Subject: [PATCH 2180/2522] fixes problem where wrong ACL could be selected (#3099) This fixes a bug where the wrong acl name could be matched in the running config if the desired acl name is a starting subset. --- network/asa/asa_acl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/asa/asa_acl.py b/network/asa/asa_acl.py index 55d3fd58cdd..fd547c69f89 100644 --- a/network/asa/asa_acl.py +++ b/network/asa/asa_acl.py @@ -153,7 +153,7 @@ def get_config(module, acl_name): filtered_config = list() for item in contents.split('\n'): - if item.startswith('access-list %s' % acl_name): + if item.startswith('access-list %s ' % acl_name): filtered_config.append(item) return NetworkConfig(indent=1, contents='\n'.join(filtered_config)) From 49dde162f66e86c2b30f7d0affad1abf7b463798 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Wed, 5 Oct 2016 22:09:18 -0400 Subject: [PATCH 2181/2522] fixes asa_config to allow config to include passwords, defaults or none (#3102) The fix allows the asa_config module to request the config to contain all default statements or password information necessary for vpn tunnel endpoints --- network/asa/asa_config.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/network/asa/asa_config.py b/network/asa/asa_config.py index e90f5fbfaae..320255b46f0 100644 --- a/network/asa/asa_config.py +++ b/network/asa/asa_config.py @@ -137,7 +137,7 @@ will not download the running-config from the remote node. required: false default: null - default: + defaults: description: - This argument specifies whether or not to collect all defaults when getting the remote device running config. When enabled, @@ -146,6 +146,15 @@ required: false default: no choices: ['yes', 'no'] + passwords: + description: + - This argument specifies to include passwords in the config + when retrieving the running-config from the remote device. This + includes passwords related to VPN endpoints. This argument is + mutually exclusive with I(defaults). + required: false + default: no + choices: ['yes', 'no'] save: description: - The C(save) argument instructs the module to save the running- @@ -190,10 +199,10 @@ context: ansible - asa_config: - show_command: 'more system:running-config' lines: - ikev1 pre-shared-key MyS3cretVPNK3y parents: tunnel-group 1.1.1.1 ipsec-attributes + passwords: yes provider: "{{ cli }}" """ @@ -226,8 +235,13 @@ def get_config(module): contents = module.params['config'] if not contents: - defaults = module.params['default'] - contents = module.config.get_config(include_defaults=defaults) + if module.params['defaults']: + include = 'defaults' + elif module.params['passwords']: + include = 'passwords' + else: + include = None + contents = module.config.get_config(include=include) return NetworkConfig(indent=1, contents=contents) def get_candidate(module): @@ -292,13 +306,14 @@ def main(): replace=dict(default='line', choices=['line', 'block']), config=dict(), - default=dict(type='bool', default=False), + defaults=dict(type='bool', default=False), + passwords=dict(type='bool', default=False), backup=dict(type='bool', default=False), save=dict(type='bool', default=False), ) - mutually_exclusive = [('lines', 'src')] + mutually_exclusive = [('lines', 'src'), ('defaults', 'passwords')] required_if = [('match', 'strict', ['lines']), ('match', 'exact', ['lines']), From 118fe8283ed2dcff6242e5574110ef2d6e95b50b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Thu, 6 Oct 2016 08:30:31 +0200 Subject: [PATCH 2182/2522] cloudstack: cs_portforward: fix rule not found if domain is not account's domain. (#3093) cs_portforward will not find the rule and tries to create it resulting in an API error. Thanks to @mostkopf for reporting. --- cloud/cloudstack/cs_portforward.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cloud/cloudstack/cs_portforward.py b/cloud/cloudstack/cs_portforward.py index 3c492c54618..945db54a17b 100644 --- a/cloud/cloudstack/cs_portforward.py +++ b/cloud/cloudstack/cs_portforward.py @@ -239,6 +239,8 @@ def get_portforwarding_rule(self): args = {} args['ipaddressid'] = self.get_ip_address(key='id') + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') args['projectid'] = self.get_project(key='id') portforwarding_rules = self.cs.listPortForwardingRules(**args) @@ -271,6 +273,8 @@ def create_portforwarding_rule(self): args['vmguestip'] = self.get_vm_guest_ip() args['ipaddressid'] = self.get_ip_address(key='id') args['virtualmachineid'] = self.get_vm(key='id') + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') portforwarding_rule = None self.result['changed'] = True From 271e300745b7200fc57383916d87efd6640d093e Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Thu, 6 Oct 2016 16:03:24 -0400 Subject: [PATCH 2183/2522] update mask_passwords argument to be more descriptive in asa_config (#3109) This changes the passwords argument to mask_passwords to make the argument more descriptive of its intended function --- network/asa/asa_config.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/network/asa/asa_config.py b/network/asa/asa_config.py index 320255b46f0..4cd8b58635c 100644 --- a/network/asa/asa_config.py +++ b/network/asa/asa_config.py @@ -146,14 +146,14 @@ required: false default: no choices: ['yes', 'no'] - passwords: + mask_passwords: description: - This argument specifies to include passwords in the config when retrieving the running-config from the remote device. This includes passwords related to VPN endpoints. This argument is mutually exclusive with I(defaults). - required: false - default: no + required: true + default: true choices: ['yes', 'no'] save: description: @@ -235,9 +235,9 @@ def get_config(module): contents = module.params['config'] if not contents: - if module.params['defaults']: + if module.params['defaults'] is True: include = 'defaults' - elif module.params['passwords']: + elif module.params['mask_passwords'] is False: include = 'passwords' else: include = None @@ -307,13 +307,13 @@ def main(): config=dict(), defaults=dict(type='bool', default=False), - passwords=dict(type='bool', default=False), + mask_passwords=dict(type='bool', default=True), backup=dict(type='bool', default=False), save=dict(type='bool', default=False), ) - mutually_exclusive = [('lines', 'src'), ('defaults', 'passwords')] + mutually_exclusive = [('lines', 'src'), ('defaults', 'mask_passwords')] required_if = [('match', 'strict', ['lines']), ('match', 'exact', ['lines']), From cc2651422ad6c7971fa48a45d2e16ac297ededd6 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Thu, 6 Oct 2016 16:42:00 -0400 Subject: [PATCH 2184/2522] Revert "update mask_passwords argument to be more descriptive in asa_config (#3109)" (#3110) This reverts commit 271e300745b7200fc57383916d87efd6640d093e. --- network/asa/asa_config.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/network/asa/asa_config.py b/network/asa/asa_config.py index 4cd8b58635c..320255b46f0 100644 --- a/network/asa/asa_config.py +++ b/network/asa/asa_config.py @@ -146,14 +146,14 @@ required: false default: no choices: ['yes', 'no'] - mask_passwords: + passwords: description: - This argument specifies to include passwords in the config when retrieving the running-config from the remote device. This includes passwords related to VPN endpoints. This argument is mutually exclusive with I(defaults). - required: true - default: true + required: false + default: no choices: ['yes', 'no'] save: description: @@ -235,9 +235,9 @@ def get_config(module): contents = module.params['config'] if not contents: - if module.params['defaults'] is True: + if module.params['defaults']: include = 'defaults' - elif module.params['mask_passwords'] is False: + elif module.params['passwords']: include = 'passwords' else: include = None @@ -307,13 +307,13 @@ def main(): config=dict(), defaults=dict(type='bool', default=False), - mask_passwords=dict(type='bool', default=True), + passwords=dict(type='bool', default=False), backup=dict(type='bool', default=False), save=dict(type='bool', default=False), ) - mutually_exclusive = [('lines', 'src'), ('defaults', 'mask_passwords')] + mutually_exclusive = [('lines', 'src'), ('defaults', 'passwords')] required_if = [('match', 'strict', ['lines']), ('match', 'exact', ['lines']), From e19647321e1a7efb6b71dfed51b7e77052dcf340 Mon Sep 17 00:00:00 2001 From: Adam Miller Date: Wed, 5 Oct 2016 17:31:50 -0500 Subject: [PATCH 2185/2522] add offline mode to firewalld Signed-off-by: Adam Miller --- system/firewalld.py | 258 +++++++++++++++++++++++++++++--------------- 1 file changed, 173 insertions(+), 85 deletions(-) diff --git a/system/firewalld.py b/system/firewalld.py index eefaa45dd98..5c91bf09f52 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -61,7 +61,7 @@ choices: [ "work", "drop", "internal", "external", "trusted", "home", "dmz", "public", "block" ] permanent: description: - - "Should this configuration be in the running firewalld configuration or persist across reboots." + - "Should this configuration be in the running firewalld configuration or persist across reboots. As of Ansible version 2.3, permanent operations can operate on firewalld configs when it's not running (requires firewalld >= 3.0.9)" required: false default: null immediate: @@ -88,7 +88,7 @@ version_added: "2.1" notes: - Not tested on any Debian based system. - - Requires the python2 bindings of firewalld, which may not be installed by default if the distribution switched to python 3 + - Requires the python2 bindings of firewalld, which may not be installed by default if the distribution switched to python 3 requirements: [ 'firewalld >= 0.2.11' ] author: "Adam Miller (@maxamillion)" ''' @@ -104,20 +104,32 @@ - firewalld: masquerade=yes state=enabled permanent=true zone=dmz ''' -import os -import re +fw = None +fw_offline = False +Rich_Rule = None +FirewallClientZoneSettings = None -try: - import firewall.config - FW_VERSION = firewall.config.VERSION +##################### +# fw_offline helpers +# + +def get_fw_zone_settings(zone): + if fw_offline: + fw_zone = fw.config.get_zone(zone) + fw_settings = FirewallClientZoneSettings( + list(fw.config.get_zone_config(fw_zone)) + ) + else: + fw_zone = fw.config().getZoneByName(zone) + fw_settings = fw_zone.getSettings() - from firewall.client import Rich_Rule - from firewall.client import FirewallClient - fw = FirewallClient() - HAS_FIREWALLD = True -except ImportError: - HAS_FIREWALLD = False + return (fw_zone, fw_settings) +def update_fw_settings(fw_zone, fw_settings): + if fw_offline: + fw.config.set_zone_config(fw_zone, fw_settings.settings) + else: + fw_zone.update(fw_settings) ##################### # masquerade handling @@ -129,13 +141,13 @@ def get_masquerade_enabled(zone): return False def get_masquerade_enabled_permanent(zone): - fw_zone = fw.config().getZoneByName(zone) - fw_settings = fw_zone.getSettings() + fw_zone, fw_settings = get_fw_zone_settings(zone) if fw_settings.getMasquerade() == True: return True else: return False - + + def set_masquerade_enabled(zone): fw.addMasquerade(zone) @@ -143,16 +155,21 @@ def set_masquerade_disabled(zone): fw.removeMasquerade(zone) def set_masquerade_permanent(zone, masquerade): - fw_zone = fw.config().getZoneByName(zone) - fw_settings = fw_zone.getSettings() + fw_zone, fw_settings = get_fw_zone_settings(zone) fw_settings.setMasquerade(masquerade) - fw_zone.update(fw_settings) + update_fw_settings(fw_zone, fw_settings) ################ # port handling # def get_port_enabled(zone, port_proto): - if port_proto in fw.getPorts(zone): + if fw_offline: + fw_zone, fw_settings = get_fw_zone_settings(zone) + ports_list = fw_settings.getPorts() + else: + ports_list = fw.getPorts(zone) + + if port_proto in ports_list: return True else: return False @@ -164,52 +181,52 @@ def set_port_disabled(zone, port, protocol): fw.removePort(zone, port, protocol) def get_port_enabled_permanent(zone, port_proto): - fw_zone = fw.config().getZoneByName(zone) - fw_settings = fw_zone.getSettings() + fw_zone, fw_settings = get_fw_zone_settings(zone) + if tuple(port_proto) in fw_settings.getPorts(): return True else: return False def set_port_enabled_permanent(zone, port, protocol): - fw_zone = fw.config().getZoneByName(zone) - fw_settings = fw_zone.getSettings() + fw_zone, fw_settings = get_fw_zone_settings(zone) fw_settings.addPort(port, protocol) - fw_zone.update(fw_settings) + update_fw_settings(fw_zone, fw_settings) def set_port_disabled_permanent(zone, port, protocol): - fw_zone = fw.config().getZoneByName(zone) - fw_settings = fw_zone.getSettings() + fw_zone, fw_settings = get_fw_zone_settings(zone) fw_settings.removePort(port, protocol) - fw_zone.update(fw_settings) + update_fw_settings(fw_zone, fw_settings) #################### # source handling # def get_source(zone, source): - fw_zone = fw.config().getZoneByName(zone) - fw_settings = fw_zone.getSettings() + fw_zone, fw_settings = get_fw_zone_settings(zone) if source in fw_settings.getSources(): return True else: return False def add_source(zone, source): - fw_zone = fw.config().getZoneByName(zone) - fw_settings = fw_zone.getSettings() + fw_zone, fw_settings = get_fw_zone_settings(zone) fw_settings.addSource(source) - fw_zone.update(fw_settings) + update_fw_settings(fw_zone, fw_settings) def remove_source(zone, source): - fw_zone = fw.config().getZoneByName(zone) - fw_settings = fw_zone.getSettings() + fw_zone, fw_settings = get_fw_zone_settings(zone) fw_settings.removeSource(source) - fw_zone.update(fw_settings) + update_fw_settings(fw_zone, fw_settings) #################### # interface handling # def get_interface(zone, interface): + if fw_offline: + fw_zone, fw_settings = get_fw_zone_settings(zone) + interface_list = fw_settings.getInterfaces() + else: + interface_list = fw.getInterfaces(zone) if interface in fw.getInterfaces(zone): return True else: @@ -222,31 +239,55 @@ def remove_interface(zone, interface): fw.removeInterface(zone, interface) def get_interface_permanent(zone, interface): - fw_zone = fw.config().getZoneByName(zone) - fw_settings = fw_zone.getSettings() + fw_zone, fw_settings = get_fw_zone_settings(zone) + if interface in fw_settings.getInterfaces(): return True else: return False def change_zone_of_interface_permanent(zone, interface): - fw_zone = fw.config().getZoneByName(zone) - fw_settings = fw_zone.getSettings() - old_zone_name = fw.config().getZoneOfInterface(interface) - if old_zone_name != zone: - if old_zone_name: - old_zone_obj = fw.config().getZoneByName(old_zone_name) - old_zone_settings = old_zone_obj.getSettings() - old_zone_settings.removeInterface(interface) # remove from old - old_zone_obj.update(old_zone_settings) - fw_settings.addInterface(interface) # add to new - fw_zone.update(fw_settings) + fw_zone, fw_settings = get_fw_zone_settings(zone) + if fw_offline: + iface_zone_objs = [ ] + for zone in fw.config.get_zones(): + old_zone_obj = fw.config.get_zone(zone) + if interface in old_zone_obj.interfaces: + iface_zone_objs.append(old_zone_obj) + if len(iface_zone_objs) > 1: + # Even it shouldn't happen, it's actually possible that + # the same interface is in several zone XML files + module.fail_json( + msg = 'ERROR: interface {} is in {} zone XML file, can only be in one'.format( + interface, + len(iface_zone_objs) + ) + ) + old_zone_obj = iface_zone_objs[0] + if old_zone_obj.name != zone: + old_zone_settings = FirewallClientZoneSettings( + fw.config.get_zone_config(old_zone_obj) + ) + old_zone_settings.removeInterface(interface) # remove from old + fw.config.set_zone_config(old_zone_obj, old_zone_settings.settings) + + fw_settings.addInterface(interface) # add to new + fw.config.set_zone_config(fw_zone, fw_settings.settings) + else: + old_zone_name = fw.config().getZoneOfInterface(interface) + if old_zone_name != zone: + if old_zone_name: + old_zone_obj = fw.config().getZoneByName(old_zone_name) + old_zone_settings = old_zone_obj.getSettings() + old_zone_settings.removeInterface(interface) # remove from old + old_zone_obj.update(old_zone_settings) + fw_settings.addInterface(interface) # add to new + fw_zone.update(fw_settings) def remove_interface_permanent(zone, interface): - fw_zone = fw.config().getZoneByName(zone) - fw_settings = fw_zone.getSettings() + fw_zone, fw_settings = get_fw_zone_settings(zone) fw_settings.removeInterface(interface) - fw_zone.update(fw_settings) + update_fw_settings(fw_zone, fw_settings) #################### # service handling @@ -264,25 +305,22 @@ def set_service_disabled(zone, service): fw.removeService(zone, service) def get_service_enabled_permanent(zone, service): - fw_zone = fw.config().getZoneByName(zone) - fw_settings = fw_zone.getSettings() + fw_zone, fw_settings = get_fw_zone_settings(zone) + if service in fw_settings.getServices(): return True else: return False def set_service_enabled_permanent(zone, service): - fw_zone = fw.config().getZoneByName(zone) - fw_settings = fw_zone.getSettings() + fw_zone, fw_settings = get_fw_zone_settings(zone) fw_settings.addService(service) - fw_zone.update(fw_settings) + update_fw_settings(fw_zone, fw_settings) def set_service_disabled_permanent(zone, service): - fw_zone = fw.config().getZoneByName(zone) - fw_settings = fw_zone.getSettings() + fw_zone, fw_settings = get_fw_zone_settings(zone) fw_settings.removeService(service) - fw_zone.update(fw_settings) - + update_fw_settings(fw_zone, fw_settings) #################### # rich rule handling @@ -303,8 +341,7 @@ def set_rich_rule_disabled(zone, rule): fw.removeRichRule(zone, rule) def get_rich_rule_enabled_permanent(zone, rule): - fw_zone = fw.config().getZoneByName(zone) - fw_settings = fw_zone.getSettings() + fw_zone, fw_settings = get_fw_zone_settings(zone) # Convert the rule string to standard format # before checking whether it is present rule = str(Rich_Rule(rule_str=rule)) @@ -314,16 +351,14 @@ def get_rich_rule_enabled_permanent(zone, rule): return False def set_rich_rule_enabled_permanent(zone, rule): - fw_zone = fw.config().getZoneByName(zone) - fw_settings = fw_zone.getSettings() + fw_zone, fw_settings = get_fw_zone_settings(zone) fw_settings.addRichRule(rule) - fw_zone.update(fw_settings) + update_fw_settings(fw_zone, fw_settings) def set_rich_rule_disabled_permanent(zone, rule): - fw_zone = fw.config().getZoneByName(zone) - fw_settings = fw_zone.getSettings() + fw_zone, fw_settings = get_fw_zone_settings(zone) fw_settings.removeRichRule(rule) - fw_zone.update(fw_settings) + update_fw_settings(fw_zone, fw_settings) def main(): @@ -344,25 +379,73 @@ def main(): ), supports_check_mode=True ) + + ## Handle running (online) daemon vs non-running (offline) daemon + global fw + global fw_offline + global Rich_Rule + global FirewallClientZoneSettings + + ## Imports + try: + import firewall.config + FW_VERSION = firewall.config.VERSION + + from firewall.client import Rich_Rule + from firewall.client import FirewallClient + HAS_FIREWALLD = True + fw = None + fw_offline = False + + try: + fw = FirewallClient() + fw.getDefaultZone() + except AttributeError: + ## Firewalld is not currently running, permanent-only operations + + ## Import other required parts of the firewalld API + ## + ## NOTE: + ## online and offline operations do not share a common firewalld API + from firewall.core.fw_test import Firewall_test + from firewall.client import FirewallClientZoneSettings + fw = Firewall_test() + fw.start() + fw_offline = True + + except ImportError: + HAS_FIREWALLD = False + + if not HAS_FIREWALLD: + module.fail_json(msg='firewalld and its python 2 module are required for this module, version 2.0.11 or newer required (3.0.9 or newer for offline operations)') + + if fw_offline: + ## Pre-run version checking + if FW_VERSION < "0.3.9": + module.fail_json(msg='unsupported version of firewalld, offline operations require >= 3.0.9') + else: + ## Pre-run version checking + if FW_VERSION < "0.2.11": + module.fail_json(msg='unsupported version of firewalld, requires >= 2.0.11') + + ## Check for firewalld running + try: + if fw.connected == False: + module.fail_json(msg='firewalld service must be running, or try with offline=true') + except AttributeError: + module.fail_json(msg="firewalld connection can't be established,\ + installed version (%s) likely too old. Requires firewalld >= 2.0.11" % FW_VERSION) + + + ## Verify required params are provided if module.params['source'] == None and module.params['permanent'] == None: module.fail_json(msg='permanent is a required parameter') if module.params['interface'] != None and module.params['zone'] == None: module.fail(msg='zone is a required parameter') - if not HAS_FIREWALLD: - module.fail_json(msg='firewalld and its python 2 module are required for this module') - - ## Pre-run version checking - if FW_VERSION < "0.2.11": - module.fail_json(msg='unsupported version of firewalld, requires >= 2.0.11') - ## Check for firewalld running - try: - if fw.connected == False: - module.fail_json(msg='firewalld service must be running') - except AttributeError: - module.fail_json(msg="firewalld connection can't be established,\ - installed version (%s) likely too old. Requires firewalld >= 2.0.11" % FW_VERSION) + if module.params['immediate'] and fw_offiline: + module.fail(msg='firewall is not currently running, unable to perform immediate actions without a running firewall daemon') ## Global Vars changed=False @@ -381,7 +464,10 @@ def main(): if module.params['zone'] != None: zone = module.params['zone'] else: - zone = fw.getDefaultZone() + if fw_offline: + zone = fw.get_default_zone() + else: + zone = fw.getDefaultZone() permanent = module.params['permanent'] desired_state = module.params['state'] @@ -594,7 +680,7 @@ def main(): if permanent: is_enabled = get_masquerade_enabled_permanent(zone) msgs.append('Permanent operation') - + if desired_state == "enabled": if is_enabled == False: if module.check_mode: @@ -614,7 +700,7 @@ def main(): if immediate or not permanent: is_enabled = get_masquerade_enabled(zone) msgs.append('Non-permanent operation') - + if desired_state == "enabled": if is_enabled == False: if module.check_mode: @@ -632,6 +718,8 @@ def main(): changed=True msgs.append("Removed masquerade from zone %s" % (zone)) + if fw_offline: + msgs.append("(offline operation: only on-disk configs were altered)") module.exit_json(changed=changed, msg=', '.join(msgs)) From d188fb5e2fdf671161f5163d53f9eb76acdfab4b Mon Sep 17 00:00:00 2001 From: "Ryan S. Brown" Date: Fri, 7 Oct 2016 14:49:12 -0400 Subject: [PATCH 2186/2522] `lambda` Support using the role name Instead of needing the full role ARN, allow users to specify a role name as long as the role exists in the same account. --- cloud/amazon/lambda.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/cloud/amazon/lambda.py b/cloud/amazon/lambda.py index 7fb5ea8371c..18977c2ff56 100644 --- a/cloud/amazon/lambda.py +++ b/cloud/amazon/lambda.py @@ -38,9 +38,9 @@ description: - The runtime environment for the Lambda function you are uploading. Required when creating a function. Use parameters as described in boto3 docs. Current example runtime environments are nodejs, nodejs4.3, java8 or python2.7 required: true - role_arn: + role: description: - - The Amazon Resource Name (ARN) of the IAM role that Lambda assumes when it executes your function to access any other Amazon Web Services (AWS) resources + - The Amazon Resource Name (ARN) of the IAM role that Lambda assumes when it executes your function to access any other Amazon Web Services (AWS) resources. You may use the bare ARN if the role belongs to the same AWS account. default: null handler: description: @@ -110,7 +110,7 @@ state: present zip_file: '{{ item.zip_file }}' runtime: 'python2.7' - role_arn: 'arn:aws:iam::987654321012:role/lambda_basic_execution' + role: 'arn:aws:iam::987654321012:role/lambda_basic_execution' handler: 'hello_python.my_handler' vpc_subnet_ids: - subnet-123abcde @@ -197,7 +197,7 @@ def main(): name=dict(type='str', required=True), state=dict(type='str', default='present', choices=['present', 'absent']), runtime=dict(type='str', required=True), - role_arn=dict(type='str', default=None), + role=dict(type='str', default=None), handler=dict(type='str', default=None), zip_file=dict(type='str', default=None, aliases=['src']), s3_bucket=dict(type='str'), @@ -226,7 +226,7 @@ def main(): name = module.params.get('name') state = module.params.get('state').lower() runtime = module.params.get('runtime') - role_arn = module.params.get('role_arn') + role = module.params.get('role') handler = module.params.get('handler') s3_bucket = module.params.get('s3_bucket') s3_key = module.params.get('s3_key') @@ -257,6 +257,18 @@ def main(): except (botocore.exceptions.ClientError, botocore.exceptions.ValidationError) as e: module.fail_json(msg=str(e)) + if role.startswith('arn:aws:iam'): + role_arn = role + else: + # get account ID and assemble ARN + try: + iam_client = boto3_conn(module, conn_type='client', resource='iam', + region=region, endpoint=ec2_url, **aws_connect_kwargs) + account_id = iam_client.get_user()['User']['Arn'].split(':')[4] + role_arn = 'arn:aws:iam::{0}:role/{1}'.format(account_id, role) + except (botocore.exceptions.ClientError, botocore.exceptions.ValidationError) as e: + module.fail_json(msg=str(e)) + # Get function configuration if present, False otherwise current_function = get_current_function(client, name) From 734aa8f8e94db1805bf56a007aa944104d6b3d8e Mon Sep 17 00:00:00 2001 From: "Ryan S. Brown" Date: Fri, 7 Oct 2016 14:50:07 -0400 Subject: [PATCH 2187/2522] `lambda` correct documentation of return output The returns are actually nested under `configuration` keys, so the docs need to reflect that. Also add the automatic return of the function version, so it can be used to feed the `lambda_alias` module. --- cloud/amazon/lambda.py | 74 ++++++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/cloud/amazon/lambda.py b/cloud/amazon/lambda.py index 18977c2ff56..650022e5bcd 100644 --- a/cloud/amazon/lambda.py +++ b/cloud/amazon/lambda.py @@ -139,20 +139,26 @@ returned: success type: dict sample: - { - 'FunctionName': 'string', - 'FunctionArn': 'string', - 'Runtime': 'nodejs', - 'Role': 'string', - 'Handler': 'string', - 'CodeSize': 123, - 'Description': 'string', - 'Timeout': 123, - 'MemorySize': 123, - 'LastModified': 'string', - 'CodeSha256': 'string', - 'Version': 'string', - } + 'code': + { + 'location': 'an S3 URL', + 'repository_type': 'S3', + } + 'configuration': + { + 'function_name': 'string', + 'function_arn': 'string', + 'runtime': 'nodejs', + 'role': 'string', + 'handler': 'string', + 'code_size': 123, + 'description': 'string', + 'timeout': 123, + 'memory_size': 123, + 'last_modified': 'string', + 'code_sha256': 'string', + 'version': 'string', + } ''' # Import from Python standard library @@ -172,11 +178,14 @@ HAS_BOTO3 = False -def get_current_function(connection, function_name): +def get_current_function(connection, function_name, qualifier=None): try: + if qualifier is not None: + return connection.get_function(FunctionName=function_name, + Qualifier=qualifier) return connection.get_function(FunctionName=function_name) - except botocore.exceptions.ClientError as e: - return False + except botocore.exceptions.ClientError: + return None def sha256sum(filename): @@ -277,9 +286,10 @@ def main(): # Get current state current_config = current_function['Configuration'] + current_version = None # Update function configuration - func_kwargs = {'FunctionName': name} + func_kwargs = {'FunctionName': name, 'Publish': True} # Update configuration if needed if role_arn and current_config['Role'] != role_arn: @@ -318,22 +328,23 @@ def main(): {'SubnetIds': vpc_subnet_ids,'SecurityGroupIds': vpc_security_group_ids}}) else: # No VPC configuration is desired, assure VPC config is empty when present in current config - if ('VpcConfig' in current_config and + if ('VpcConfig' in current_config and 'VpcId' in current_config['VpcConfig'] and current_config['VpcConfig']['VpcId'] != ''): func_kwargs.update({'VpcConfig':{'SubnetIds': [], 'SecurityGroupIds': []}}) # Upload new configuration if configuration has changed - if len(func_kwargs) > 1: + if len(func_kwargs) > 2: try: if not check_mode: - client.update_function_configuration(**func_kwargs) + response = client.update_function_configuration(**func_kwargs) + current_version = response['Version'] changed = True except (botocore.exceptions.ParamValidationError, botocore.exceptions.ClientError) as e: module.fail_json(msg=str(e)) # Update code configuration - code_kwargs = {'FunctionName': name} + code_kwargs = {'FunctionName': name, 'Publish': True} # Update S3 location if s3_bucket and s3_key: @@ -359,21 +370,22 @@ def main(): module.fail_json(msg=str(e)) # Upload new code if needed (e.g. code checksum has changed) - if len(code_kwargs) > 1: + if len(code_kwargs) > 2: try: if not check_mode: - client.update_function_code(**code_kwargs) + response = client.update_function_code(**code_kwargs) + current_version = response['Version'] changed = True except (botocore.exceptions.ParamValidationError, botocore.exceptions.ClientError) as e: module.fail_json(msg=str(e)) # Describe function code and configuration - response = get_current_function(client, name) + response = get_current_function(client, name, qualifier=current_version) if not response: module.fail_json(msg='Unable to get function information after updating') # We're done - module.exit_json(changed=changed, result=camel_dict_to_snake_dict(response)) + module.exit_json(changed=changed, **camel_dict_to_snake_dict(response)) # Function doesn't exists, create new Lambda function elif state == 'present': @@ -398,6 +410,7 @@ def main(): func_kwargs = {'FunctionName': name, 'Description': description, + 'Publish': True, 'Runtime': runtime, 'Role': role_arn, 'Handler': handler, @@ -421,11 +434,15 @@ def main(): try: if not check_mode: response = client.create_function(**func_kwargs) + current_version = response['Version'] changed = True except (botocore.exceptions.ParamValidationError, botocore.exceptions.ClientError) as e: module.fail_json(msg=str(e)) - module.exit_json(changed=changed, result=camel_dict_to_snake_dict(response)) + response = get_current_function(client, name, qualifier=current_version) + if not response: + module.fail_json(msg='Unable to get function information after creating') + module.exit_json(changed=changed, **camel_dict_to_snake_dict(response)) # Delete existing Lambda function if state == 'absent' and current_function: @@ -446,4 +463,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * -main() +if __name__ == '__main__': + main() From 669415a7de176d0c551d5e87bfed6fe24b3afce9 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sat, 8 Oct 2016 00:35:03 +0200 Subject: [PATCH 2188/2522] Do not leak the password of gitlab_user in log (#3122) --- source_control/gitlab_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source_control/gitlab_user.py b/source_control/gitlab_user.py index d9b40401b0d..826b9f6f691 100644 --- a/source_control/gitlab_user.py +++ b/source_control/gitlab_user.py @@ -269,7 +269,7 @@ def main(): login_token=dict(required=False, no_log=True), name=dict(required=True), username=dict(required=True), - password=dict(required=True), + password=dict(required=True, no_log=True), email=dict(required=True), sshkey_name=dict(required=False), sshkey_file=dict(required=False), From 4c2ae71d2da89039002299d77dc4e48fa1e7788b Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sat, 8 Oct 2016 00:35:44 +0200 Subject: [PATCH 2189/2522] Do not leak the newrelic token in log (#3118) --- monitoring/newrelic_deployment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/newrelic_deployment.py b/monitoring/newrelic_deployment.py index 3d9bc6c0ec3..87b16e05f39 100644 --- a/monitoring/newrelic_deployment.py +++ b/monitoring/newrelic_deployment.py @@ -92,7 +92,7 @@ def main(): module = AnsibleModule( argument_spec=dict( - token=dict(required=True), + token=dict(required=True, no_log=True), app_name=dict(required=False), application_id=dict(required=False), changelog=dict(required=False), From 6ca0842392c60094d1d158e6fbaa3971f7537559 Mon Sep 17 00:00:00 2001 From: Eric D Helms Date: Fri, 7 Oct 2016 20:10:31 -0400 Subject: [PATCH 2190/2522] New module: Manage Foreman and Katello entities (#2450) --- infrastructure/__init__.py | 0 infrastructure/foreman/__init__.py | 0 infrastructure/foreman/foreman.py | 154 +++++ infrastructure/foreman/katello.py | 529 ++++++++++++++++++ test/utils/shippable/sanity-skip-python24.txt | 1 + 5 files changed, 684 insertions(+) create mode 100644 infrastructure/__init__.py create mode 100644 infrastructure/foreman/__init__.py create mode 100644 infrastructure/foreman/foreman.py create mode 100644 infrastructure/foreman/katello.py diff --git a/infrastructure/__init__.py b/infrastructure/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/infrastructure/foreman/__init__.py b/infrastructure/foreman/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/infrastructure/foreman/foreman.py b/infrastructure/foreman/foreman.py new file mode 100644 index 00000000000..febb7d833c2 --- /dev/null +++ b/infrastructure/foreman/foreman.py @@ -0,0 +1,154 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2016, Eric D Helms +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: foreman +short_description: Manage Foreman Resources +description: + - Allows the management of Foreman resources inside your Foreman server +version_added: "2.3" +author: "Eric D Helms (@ehelms)" +requirements: + - "nailgun >= 0.28.0" + - "python >= 2.6" + - datetime +options: + server_url: + description: + - URL of Foreman server + required: true + username: + description: + - Username on Foreman server + required: true + password: + description: + - Password for user accessing Foreman server + required: true + entity: + description: + - The Foreman resource that the action will be performed on (e.g. organization, host) + required: true + params: + description: + - Parameters associated to the entity resource to set or edit in dictionary format (e.g. name, description) + required: true +''' + +EXAMPLES = ''' +- name: "Create CI Organization" + local_action: + module: foreman + username: "admin" + password: "admin" + server_url: "https://fakeserver.com" + entity: "organization" + params: + name: "My Cool New Organization" +''' + +RETURN = '''# ''' + +import datetime + +try: + from nailgun import entities, entity_fields + from nailgun.config import ServerConfig + HAS_NAILGUN_PACKAGE = True +except: + HAS_NAILGUN_PACKAGE = False + +class NailGun(object): + def __init__(self, server, entities, module): + self._server = server + self._entities = entities + self._module = module + + def find_organization(self, name, **params): + org = self._entities.Organization(self._server, name=name, **params) + response = org.search(set(), {'search': 'name={}'.format(name)}) + + if len(response) == 1: + return response[0] + else: + self._module.fail_json(msg="No Content View found for %s" % name) + + def organization(self, params): + name = params['name'] + del params['name'] + org = self.find_organization(name, **params) + + if org: + org = self._entities.Organization(self._server, name=name, id=org.id, **params) + org.update() + else: + org = self._entities.Organization(self._server, name=name, **params) + org.create() + + return True + +def main(): + module = AnsibleModule( + argument_spec=dict( + server_url=dict(required=True), + username=dict(required=True, no_log=True), + password=dict(required=True, no_log=True), + entity=dict(required=True, no_log=False), + verify_ssl=dict(required=False, type='bool', default=False), + params=dict(required=True, no_log=True, type='dict'), + ), + supports_check_mode=True + ) + + if not HAS_NAILGUN_PACKAGE: + module.fail_json(msg="Missing required nailgun module (check docs or install with: pip install nailgun") + + server_url = module.params['server_url'] + username = module.params['username'] + password = module.params['password'] + entity = module.params['entity'] + params = module.params['params'] + verify_ssl = module.params['verify_ssl'] + + server = ServerConfig( + url=server_url, + auth=(username, password), + verify=verify_ssl + ) + ng = NailGun(server, entities, module) + + # Lets make an connection to the server with username and password + try: + org = entities.Organization(server) + org.search() + except Exception as e: + module.fail_json(msg="Failed to connect to Foreman server: %s " % e) + + if entity == 'organization': + ng.organization(params) + module.exit_json(changed=True, result="%s updated" % entity) + else: + module.fail_json(changed=False, result="Unsupported entity supplied") + +# import module snippets +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() diff --git a/infrastructure/foreman/katello.py b/infrastructure/foreman/katello.py new file mode 100644 index 00000000000..3f7f42942da --- /dev/null +++ b/infrastructure/foreman/katello.py @@ -0,0 +1,529 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2016, Eric D Helms +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: katello +short_description: Manage Katello Resources +description: + - Allows the management of Katello resources inside your Foreman server +version_added: "2.3" +author: "Eric D Helms (@ehelms)" +requirements: + - "nailgun >= 0.28.0" + - "python >= 2.6" + - datetime +options: + server_url: + description: + - URL of Foreman server + required: true + username: + description: + - Username on Foreman server + required: true + password: + description: + - Password for user accessing Foreman server + required: true + entity: + description: + - The Foreman resource that the action will be performed on (e.g. organization, host) + required: true + params: + description: + - Parameters associated to the entity resource to set or edit in dictionary format (e.g. name, description) + required: true +''' + +EXAMPLES = ''' +Simple Example: + +- name: "Create Product" + local_action: + module: katello + username: "admin" + password: "admin" + server_url: "https://fakeserver.com" + entity: "product" + params: + name: "Centos 7" + +Abstraction Example: + +katello.yml +--- +- name: "{{ name }}" + local_action: + module: katello + username: "admin" + password: "admin" + server_url: "https://fakeserver.com" + entity: "{{ entity }}" + params: "{{ params }}" + +tasks.yml +--- +- include: katello.yml + vars: + name: "Create Dev Environment" + entity: "lifecycle_environment" + params: + name: "Dev" + prior: "Library" + organization: "Default Organization" + +- include: katello.yml + vars: + name: "Create Centos Product" + entity: "product" + params: + name: "Centos 7" + organization: "Default Organization" + +- include: katello.yml + vars: + name: "Create 7.2 Repository" + entity: "repository" + params: + name: "Centos 7.2" + product: "Centos 7" + organization: "Default Organization" + content_type: "yum" + url: "http://mirror.centos.org/centos/7/os/x86_64/" + +- include: katello.yml + vars: + name: "Create Centos 7 View" + entity: "content_view" + params: + name: "Centos 7 View" + organization: "Default Organization" + repositories: + - name: "Centos 7.2" + product: "Centos 7" + +- include: katello.yml + vars: + name: "Enable RHEL Product" + entity: "repository_set" + params: + name: "Red Hat Enterprise Linux 7 Server (RPMs)" + product: "Red Hat Enterprise Linux Server" + organization: "Default Organization" + basearch: "x86_64" + releasever: "7" +''' + +RETURN = '''# ''' + +import datetime + +try: + from nailgun import entities, entity_fields, entity_mixins + from nailgun.config import ServerConfig + HAS_NAILGUN_PACKAGE = True +except: + HAS_NAILGUN_PACKAGE = False + + +class NailGun(object): + def __init__(self, server, entities, module): + self._server = server + self._entities = entities + self._module = module + entity_mixins.TASK_TIMEOUT = 1000 + + def find_organization(self, name, **params): + org = self._entities.Organization(self._server, name=name, **params) + response = org.search(set(), {'search': 'name={}'.format(name)}) + + if len(response) == 1: + return response[0] + else: + self._module.fail_json(msg="No organization found for %s" % name) + + def find_lifecycle_environment(self, name, organization): + org = self.find_organization(organization) + + lifecycle_env = self._entities.LifecycleEnvironment(self._server, name=name, organization=org) + response = lifecycle_env.search() + + if len(response) == 1: + return response[0] + else: + self._module.fail_json(msg="No Lifecycle Found found for %s" % name) + + def find_product(self, name, organization): + org = self.find_organization(organization) + + product = self._entities.Product(self._server, name=name, organization=org) + response = product.search() + + if len(response) == 1: + return response[0] + else: + self._module.fail_json(msg="No Product found for %s" % name) + + def find_repository(self, name, product, organization): + product = self.find_product(product, organization) + + repository = self._entities.Repository(self._server, name=name, product=product) + repository._fields['organization'] = entity_fields.OneToOneField(entities.Organization) + repository.organization = product.organization + response = repository.search() + + if len(response) == 1: + return response[0] + else: + self._module.fail_json(msg="No Repository found for %s" % name) + + def find_content_view(self, name, organization): + org = self.find_organization(organization) + + content_view = self._entities.ContentView(self._server, name=name, organization=org) + response = content_view.search() + + if len(response) == 1: + return response[0] + else: + self._module.fail_json(msg="No Content View found for %s" % name) + + def organization(self, params): + name = params['name'] + del params['name'] + org = self.find_organization(name, **params) + + if org: + org = self._entities.Organization(self._server, name=name, id=org.id, **params) + org.update() + else: + org = self._entities.Organization(self._server, name=name, **params) + org.create() + + return True + + def manifest(self, params): + org = self.find_organization(params['organization']) + params['organization'] = org.id + + try: + file = open(os.getcwd() + params['content'], 'r') + content = file.read() + finally: + file.close() + + manifest = self._entities.Subscription(self._server) + + try: + manifest.upload( + data={'organization_id': org.id}, + files={'content': content} + ) + return True + except Exception: + e = get_exception() + + if "Import is the same as existing data" in e.message: + return True + else: + self._module.fail_json(msg="Manifest import failed with %s" % e) + + def product(self, params): + org = self.find_organization(params['organization']) + params['organization'] = org.id + + product = self._entities.Product(self._server, **params) + response = product.search() + + if len(response) == 1: + product.id = response[0].id + product.update() + else: + product.create() + + return True + + def sync_product(self, params): + org = self.find_organization(params['organization']) + product = self.find_product(params['name'], org.name) + + return product.sync() + + def repository(self, params): + product = self.find_product(params['product'], params['organization']) + params['product'] = product.id + del params['organization'] + + repository = self._entities.Repository(self._server, **params) + repository._fields['organization'] = entity_fields.OneToOneField(entities.Organization) + repository.organization = product.organization + response = repository.search() + + if len(response) == 1: + repository.id = response[0].id + repository.update() + else: + repository.create() + + return True + + def sync_repository(self, params): + org = self.find_organization(params['organization']) + repository = self.find_repository(params['name'], params['product'], org.name) + + return repository.sync() + + def repository_set(self, params): + product = self.find_product(params['product'], params['organization']) + del params['product'] + del params['organization'] + + if not product: + return False + else: + reposet = self._entities.RepositorySet(self._server, product=product, name=params['name']) + reposet = reposet.search()[0] + + formatted_name = [params['name'].replace('(', '').replace(')', '')] + formatted_name.append(params['basearch']) + + if params['releasever']: + formatted_name.append(params['releasever']) + + formatted_name = ' '.join(formatted_name) + + repository = self._entities.Repository(self._server, product=product, name=formatted_name) + repository._fields['organization'] = entity_fields.OneToOneField(entities.Organization) + repository.organization = product.organization + repository = repository.search() + + if len(repository) == 0: + reposet.enable(data={'basearch': params['basearch'], 'releasever': params['releasever']}) + + return True + + def sync_plan(self, params): + org = self.find_organization(params['organization']) + params['organization'] = org.id + params['sync_date'] = datetime.datetime.strptime(params['sync_date'], "%H:%M") + + products = params['products'] + del params['products'] + + sync_plan = SyncPlan( + self._server, + name=params['name'], + organization=org + ) + response = sync_plan.search() + + sync_plan.sync_date = params['sync_date'] + sync_plan.interval = params['interval'] + + if len(response) == 1: + sync_plan.id = response[0].id + sync_plan.update() + else: + response = sync_plan.create() + sync_plan.id = response[0].id + + if products: + ids = [] + + for name in products: + product = self.find_product(name, org.name) + ids.append(product.id) + + sync_plan.add_products(data={'product_ids': ids}) + + return True + + def content_view(self, params): + org = self.find_organization(params['organization']) + + content_view = self._entities.ContentView(self._server, name=params['name'], organization=org) + response = content_view.search() + + if len(response) == 1: + content_view.id = response[0].id + content_view.update() + else: + content_view = content_view.create() + + if params['repositories']: + repos = [] + + for repository in params['repositories']: + repository = self.find_repository(repository['name'], repository['product'], org.name) + repos.append(repository) + + content_view.repository = repos + content_view.update(['repository']) + + def find_content_view(self, name, organization): + org = self.find_organization(organization) + + content_view = self._entities.ContentView(self._server, name=name, organization=org) + response = content_view.search() + + if len(response) == 1: + return response[0] + else: + self._module.fail_json(msg="No Content View found for %s" % name) + + def find_content_view_version(self, name, organization, environment): + env = self.find_lifecycle_environment(environment, organization) + content_view = self.find_content_view(name, organization) + + content_view_version = ContentViewVersion(self._server, content_view=content_view) + response = content_view_version.search(set('content_view'), {'environment_id': env.id}) + + if len(response) == 1: + return response[0] + else: + self._module.fail_json(msg="No Content View version found for %s" % response) + + def publish(self, params): + content_view = self.find_content_view(params['name'], params['organization']) + + return content_view.publish() + + def promote(self, params): + to_environment = self.find_lifecycle_environment(params['to_environment'], params['organization']) + version = self.find_content_view_version(params['name'], params['organization'], params['from_environment']) + + data = {'environment_id': to_environment.id} + return version.promote(data=data) + + def lifecycle_environment(self, params): + org = self.find_organization(params['organization']) + prior_env = self.find_lifecycle_environment(params['prior'], params['organization']) + + lifecycle_env = self._entities.LifecycleEnvironment(self._server, name=params['name'], organization=org, prior=prior_env) + response = lifecycle_env.search() + + if len(response) == 1: + lifecycle_env.id = response[0].id + lifecycle_env.update() + else: + lifecycle_env.create() + + return True + + def activation_key(self, params): + org = self.find_organization(params['organization']) + + activation_key = self._entities.ActivationKey(self._server, name=params['name'], organization=org) + response = activation_key.search() + + if len(response) == 1: + activation_key.id = response[0].id + activation_key.update() + else: + activation_key.create() + + if params['content_view']: + content_view = self.find_content_view(params['content_view'], params['organization']) + lifecycle_environment = self.find_lifecycle_environment(params['lifecycle_environment'], params['organization']) + + activation_key.content_view = content_view + activation_key.environment = lifecycle_environment + activation_key.update() + + return True + +def main(): + module = AnsibleModule( + argument_spec=dict( + server_url=dict(required=True), + username=dict(required=True, no_log=True), + password=dict(required=True, no_log=True), + entity=dict(required=True, no_log=False), + action=dict(required=False, no_log=False), + verify_ssl=dict(required=False, type='bool', default=False), + params=dict(required=True, no_log=True, type='dict'), + ), + supports_check_mode=True + ) + + if not HAS_NAILGUN_PACKAGE: + module.fail_json(msg="Missing required nailgun module (check docs or install with: pip install nailgun") + + server_url = module.params['server_url'] + username = module.params['username'] + password = module.params['password'] + entity = module.params['entity'] + action = module.params['action'] + params = module.params['params'] + verify_ssl = module.params['verify_ssl'] + + server = ServerConfig( + url=server_url, + auth=(username, password), + verify=verify_ssl + ) + ng = NailGun(server, entities, module) + + # Lets make an connection to the server with username and password + try: + org = entities.Organization(server) + org.search() + except Exception as e: + module.fail_json(msg="Failed to connect to Foreman server: %s " % e) + + result = False + + if entity == 'product': + if action == 'sync': + result = ng.sync_product(params) + else: + result = ng.product(params) + elif entity == 'repository': + if action == 'sync': + result = ng.sync_repository(params) + else: + result = ng.repository(params) + elif entity == 'manifest': + result = ng.manifest(params) + elif entity == 'repository_set': + result = ng.repository_set(params) + elif entity == 'sync_plan': + result = ng.sync_plan(params) + elif entity == 'content_view': + if action == 'publish': + result = ng.publish(params) + elif action == 'promote': + result = ng.promote(params) + else: + result = ng.content_view(params) + elif entity == 'lifecycle_environment': + result = ng.lifecycle_environment(params) + elif entity == 'activation_key': + result = ng.activation_key(params) + else: + module.fail_json(changed=False, result="Unsupported entity supplied") + + module.exit_json(changed=result, result="%s updated" % entity) + +# import module snippets +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() diff --git a/test/utils/shippable/sanity-skip-python24.txt b/test/utils/shippable/sanity-skip-python24.txt index 5e3e5afa8d8..71c769c8b9b 100644 --- a/test/utils/shippable/sanity-skip-python24.txt +++ b/test/utils/shippable/sanity-skip-python24.txt @@ -12,3 +12,4 @@ /remote_management/ipmi/ /univention/ /web_infrastructure/letsencrypt.py +/infrastructure/foreman/ From 219f415ef2e86ca3d97d3fd8e37b21fdc426b8f4 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sat, 8 Oct 2016 09:45:53 +0200 Subject: [PATCH 2191/2522] Add verification for the arguments of opendj_backendprop (#3116) --- identity/opendj/opendj_backendprop.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/identity/opendj/opendj_backendprop.py b/identity/opendj/opendj_backendprop.py index 64571c0ed31..8af70c6cad4 100644 --- a/identity/opendj/opendj_backendprop.py +++ b/identity/opendj/opendj_backendprop.py @@ -147,12 +147,12 @@ def validate_data(self, data=None, name=None, value=None): def main(): module = AnsibleModule( argument_spec=dict( - opendj_bindir=dict(default="/opt/opendj/bin"), + opendj_bindir=dict(default="/opt/opendj/bin", type="path"), hostname=dict(required=True), port=dict(required=True), username=dict(default="cn=Directory Manager", required=False), password=dict(required=False, no_log=True), - passwordfile=dict(required=False), + passwordfile=dict(required=False, type="path"), backend=dict(required=True), name=dict(required=True), value=dict(required=True), From 100a517af2106d609b17311b54c6a48ffb5fd1af Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sat, 8 Oct 2016 14:27:58 +0200 Subject: [PATCH 2192/2522] Do not leak the channel token in log (#3117) --- notification/grove.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notification/grove.py b/notification/grove.py index 5e6db30d9a2..c7661456897 100644 --- a/notification/grove.py +++ b/notification/grove.py @@ -91,7 +91,7 @@ def do_notify_grove(module, channel_token, service, message, url=None, icon_url= def main(): module = AnsibleModule( argument_spec = dict( - channel_token = dict(type='str', required=True), + channel_token = dict(type='str', required=True, no_log=True), message = dict(type='str', required=True), service = dict(type='str', default='ansible'), url = dict(type='str', default=None), From ee5b968eeb92ff241b7f785ade79130b0431d9d8 Mon Sep 17 00:00:00 2001 From: afunix Date: Sat, 8 Oct 2016 14:15:55 -0500 Subject: [PATCH 2193/2522] gluster_volume adds replica and stripe arguments when adding bricks [#2754] (#2812) --- system/gluster_volume.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/system/gluster_volume.py b/system/gluster_volume.py index 85271d94ea7..96174433de6 100644 --- a/system/gluster_volume.py +++ b/system/gluster_volume.py @@ -317,8 +317,14 @@ def stop_volume(name): def set_volume_option(name, option, parameter): run_gluster([ 'volume', 'set', name, option, parameter ]) -def add_bricks(name, new_bricks, force): +def add_bricks(name, new_bricks, stripe, replica, force): args = [ 'volume', 'add-brick', name ] + if stripe: + args.append('stripe') + args.append(str(stripe)) + if replica: + args.append('replica') + args.append(str(replica)) args.extend(new_bricks) if force: args.append('force') @@ -445,7 +451,7 @@ def main(): removed_bricks.append(brick) if new_bricks: - add_bricks(volume_name, new_bricks, force) + add_bricks(volume_name, new_bricks, stripes, replicas, force) changed = True # handle quotas From 3cc020de19adc77f5991d29cdbc91220a8bf07d8 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Fri, 7 Oct 2016 15:45:53 +0200 Subject: [PATCH 2194/2522] Do not leak the solaris root password in the log --- system/solaris_zone.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/solaris_zone.py b/system/solaris_zone.py index 8c8d22305bc..54726bc82b2 100644 --- a/system/solaris_zone.py +++ b/system/solaris_zone.py @@ -419,7 +419,7 @@ def main(): state = dict(default='present', choices=['running', 'started', 'present', 'installed', 'stopped', 'absent', 'configured', 'detached', 'attached']), path = dict(defalt=None), sparse = dict(default=False, type='bool'), - root_password = dict(default=None), + root_password = dict(default=None, no_log=True), timeout = dict(default=600, type='int'), config = dict(default=''), create_options = dict(default=''), From 16c484fe5c7f294a2427a950469a9de9e8d2674b Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Fri, 7 Oct 2016 15:46:26 +0200 Subject: [PATCH 2195/2522] Fix wrong variable name --- system/solaris_zone.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/solaris_zone.py b/system/solaris_zone.py index 54726bc82b2..c97bb1f2c7b 100644 --- a/system/solaris_zone.py +++ b/system/solaris_zone.py @@ -417,7 +417,7 @@ def main(): argument_spec = dict( name = dict(required=True), state = dict(default='present', choices=['running', 'started', 'present', 'installed', 'stopped', 'absent', 'configured', 'detached', 'attached']), - path = dict(defalt=None), + path = dict(default=None), sparse = dict(default=False, type='bool'), root_password = dict(default=None, no_log=True), timeout = dict(default=600, type='int'), From 6c31d91fa59e3ea9e12f1a0453833a728590b479 Mon Sep 17 00:00:00 2001 From: Aleksey Gavrilov Date: Tue, 11 Oct 2016 22:56:06 +0500 Subject: [PATCH 2196/2522] proxmox add exaples static ip (#3092) --- cloud/misc/proxmox.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cloud/misc/proxmox.py b/cloud/misc/proxmox.py index d0df6b3f42a..786c48c3263 100644 --- a/cloud/misc/proxmox.py +++ b/cloud/misc/proxmox.py @@ -181,6 +181,9 @@ # Create new container with minimal options defining network interface with dhcp - proxmox: vmid=100 node='uk-mc02' api_user='root@pam' api_password='1q2w3e' api_host='node1' password='123456' hostname='example.org' ostemplate='local:vztmpl/ubuntu-14.04-x86_64.tar.gz' netif='{"net0":"name=eth0,ip=dhcp,ip6=dhcp,bridge=vmbr0"}' +# Create new container with minimal options defining network interface with static ip +- proxmox: vmid=100 node='uk-mc02' api_user='root@pam' api_password='1q2w3e' api_host='node1' password='123456' hostname='example.org' ostemplate='local:vztmpl/ubuntu-14.04-x86_64.tar.gz' netif='{"net0":"name=eth0,gw=192.168.0.1,ip=192.168.0.2/24,bridge=vmbr0"}' + # Create new container with minimal options defining a mount - proxmox: vmid=100 node='uk-mc02' api_user='root@pam' api_password='1q2w3e' api_host='node1' password='123456' hostname='example.org' ostemplate='local:vztmpl/ubuntu-14.04-x86_64.tar.gz' mounts='{"mp0":"local:8,mp=/mnt/test/"}' From 19bd99cb79143008d54eedb54be1fd215f440aab Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Wed, 12 Oct 2016 11:49:12 -0700 Subject: [PATCH 2197/2522] Fixes broken documentation in two modules (#3150) The modules listed in this PR were using YAML that resulted in blockquote tages being inserted into the generated RestructedText. This PR fixes that so that the documentation once again looks correct --- network/f5/bigip_device_sshd.py | 8 ++++---- network/f5/bigip_routedomain.py | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/network/f5/bigip_device_sshd.py b/network/f5/bigip_device_sshd.py index ac506f656b0..d69292f6017 100644 --- a/network/f5/bigip_device_sshd.py +++ b/network/f5/bigip_device_sshd.py @@ -118,12 +118,12 @@ RETURN = ''' allow: - description: | + description: > Specifies, if you have enabled SSH access, the IP address or address range for other systems that can use SSH to communicate with this system. returned: changed - type: list + type: string sample: "192.0.2.*" banner: description: Whether the banner is enabled or not. @@ -131,14 +131,14 @@ type: string sample: "true" banner_text: - description: | + description: > Specifies the text included on the pre-login banner that displays when a user attempts to login to the system using SSH. returned: changed and success type: string sample: "This is a corporate device. Connecting to it without..." inactivity_timeout: - description: | + description: > The number of seconds before inactivity causes an SSH. session to log out returned: changed diff --git a/network/f5/bigip_routedomain.py b/network/f5/bigip_routedomain.py index 0df2446184a..f679dd03819 100644 --- a/network/f5/bigip_routedomain.py +++ b/network/f5/bigip_routedomain.py @@ -46,12 +46,13 @@ - The unique identifying integer representing the route domain. required: true parent: - description: | + description: Specifies the route domain the system searches when it cannot find a route in the configured domain. + required: false routing_protocol: description: - - Dynamic routing protocols for the system to use in the route domain. + - Dynamic routing protocols for the system to use in the route domain. choices: - BFD - BGP From 3e1ea76a75c297e2c17a4cd0e7856ca82139c761 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 12 Oct 2016 13:26:54 -0700 Subject: [PATCH 2198/2522] Fix a couple undefined variables One was a typo and one needed to have the variable defined in that scope --- system/firewalld.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/system/firewalld.py b/system/firewalld.py index 5c91bf09f52..c069896863e 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -104,11 +104,15 @@ - firewalld: masquerade=yes state=enabled permanent=true zone=dmz ''' +from ansible.module_utils.basic import AnsibleModule + fw = None fw_offline = False Rich_Rule = None FirewallClientZoneSettings = None +module = None + ##################### # fw_offline helpers # @@ -362,6 +366,7 @@ def set_rich_rule_disabled_permanent(zone, rule): def main(): + global module module = AnsibleModule( argument_spec = dict( @@ -444,7 +449,7 @@ def main(): if module.params['interface'] != None and module.params['zone'] == None: module.fail(msg='zone is a required parameter') - if module.params['immediate'] and fw_offiline: + if module.params['immediate'] and fw_offline: module.fail(msg='firewall is not currently running, unable to perform immediate actions without a running firewall daemon') ## Global Vars @@ -723,7 +728,5 @@ def main(): module.exit_json(changed=changed, msg=', '.join(msgs)) -################################################# -# import module snippets -from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() From 8e477dad8986d56489b34af1385c46d8045c198f Mon Sep 17 00:00:00 2001 From: Ivan Bojer Date: Wed, 12 Oct 2016 18:53:54 -0700 Subject: [PATCH 2199/2522] PanOS module for admin password change using PanOS API as the transport (#2930) * PanOS module that allows admin password change. * fixed a typo * empty __init__.py * added require ansible Python extension .py * added version string * added return docstring * changed version to 2.2 * - changes to the format and module as requested by @privateip * changed version back to 2.2 as 2.3 was failing automated tests * Revert "changed version back to 2.2 as 2.3 was failing automated tests" reverting version info This reverts commit 71d520f3b4b69eb017c2b9f287a74cb77fae9d1c. --- network/panos/__init__.py | 0 network/panos/panos_admin.py | 200 +++++++++++++++++++++++++++++++++++ 2 files changed, 200 insertions(+) create mode 100644 network/panos/__init__.py create mode 100755 network/panos/panos_admin.py diff --git a/network/panos/__init__.py b/network/panos/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/network/panos/panos_admin.py b/network/panos/panos_admin.py new file mode 100755 index 00000000000..b9e24241b04 --- /dev/null +++ b/network/panos/panos_admin.py @@ -0,0 +1,200 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Ansible module to manage PaloAltoNetworks Firewall +# (c) 2016, techbizdev +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: panos_admin +short_description: Add or modify PAN-OS user accounts password. +description: + - PanOS module that allows changes to the user account passwords by doing + API calls to the Firewall using pan-api as the protocol. +author: "Luigi Mori (@jtschichold), Ivan Bojer (@ivanbojer)" +version_added: "2.3" +requirements: + - pan-python +options: + ip_address: + description: + - IP address (or hostname) of PAN-OS device + required: true + password: + description: + - password for authentication + required: true + username: + description: + - username for authentication + required: false + default: "admin" + admin_username: + description: + - username for admin user + required: false + default: "admin" + admin_password: + description: + - password for admin user + required: true + role: + description: + - role for admin user + required: false + default: null + commit: + description: + - commit if changed + required: false + default: true +''' + +EXAMPLES = ''' +# Set the password of user admin to "badpassword" +# Doesn't commit the candidate config + - name: set admin password + panos_admin: + ip_address: "192.168.1.1" + password: "admin" + admin_username: admin + admin_password: "badpassword" + commit: False +''' + +RETURN = ''' +status: + description: success status + returned: success + type: string + sample: "okey dokey" +''' +from ansible.module_utils.basic import AnsibleModule + +try: + import pan.xapi + HAS_LIB = True +except ImportError: + HAS_LIB = False + +_ADMIN_XPATH = "/config/mgt-config/users/entry[@name='%s']" + + +def admin_exists(xapi, admin_username): + xapi.get(_ADMIN_XPATH % admin_username) + e = xapi.element_root.find('.//entry') + return e + + +def admin_set(xapi, module, admin_username, admin_password, role): + if admin_password is not None: + xapi.op(cmd='request password-hash password "%s"' % admin_password, + cmd_xml=True) + r = xapi.element_root + phash = r.find('.//phash').text + if role is not None: + rbval = "yes" + if role != "superuser" and role != 'superreader': + rbval = "" + + ea = admin_exists(xapi, admin_username) + if ea is not None: + # user exists + changed = False + + if role is not None: + rb = ea.find('.//role-based') + if rb is not None: + if rb[0].tag != role: + changed = True + xpath = _ADMIN_XPATH % admin_username + xpath += '/permissions/role-based/%s' % rb[0].tag + xapi.delete(xpath=xpath) + + xpath = _ADMIN_XPATH % admin_username + xpath += '/permissions/role-based' + xapi.set(xpath=xpath, + element='<%s>%s' % (role, rbval, role)) + + if admin_password is not None: + xapi.edit(xpath=_ADMIN_XPATH % admin_username+'/phash', + element='%s' % phash) + changed = True + + return changed + + # setup the non encrypted part of the monitor + exml = [] + + exml.append('%s' % phash) + exml.append('<%s>%s' + '' % (role, rbval, role)) + + exml = ''.join(exml) + # module.fail_json(msg=exml) + + xapi.set(xpath=_ADMIN_XPATH % admin_username, element=exml) + + return True + + +def main(): + argument_spec = dict( + ip_address=dict(), + password=dict(no_log=True), + username=dict(default='admin'), + admin_username=dict(default='admin'), + admin_password=dict(no_log=True), + role=dict(), + commit=dict(type='bool', default=True) + ) + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False) + + if not HAS_LIB: + module.fail_json(msg='pan-python required for this module') + + ip_address = module.params["ip_address"] + if not ip_address: + module.fail_json(msg="ip_address should be specified") + password = module.params["password"] + if not password: + module.fail_json(msg="password is required") + username = module.params['username'] + + xapi = pan.xapi.PanXapi( + hostname=ip_address, + api_username=username, + api_password=password + ) + + admin_username = module.params['admin_username'] + if admin_username is None: + module.fail_json(msg="admin_username is required") + admin_password = module.params['admin_password'] + role = module.params['role'] + commit = module.params['commit'] + + changed = admin_set(xapi, module, admin_username, admin_password, role) + + if changed and commit: + xapi.commit(cmd="", sync=True, interval=1) + + module.exit_json(changed=changed, msg="okey dokey") + +if __name__ == '__main__': + main() From ab036bd61543f513e03c5615084717f65e816ac2 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Thu, 13 Oct 2016 22:41:50 +0200 Subject: [PATCH 2200/2522] Fix crypttab (#3121) * Fix error in crypttab doc * Use path type for file argument This permit to make sure that $HOME and '~' are properly expanded, even if in the case of crypttab, this might not make a lot of sense --- system/crypttab.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/crypttab.py b/system/crypttab.py index ea9698a12c2..ecf207cf03c 100644 --- a/system/crypttab.py +++ b/system/crypttab.py @@ -52,7 +52,7 @@ default: null password: description: - - Encryption password, the path to a file containing the pasword, or + - Encryption password, the path to a file containing the password, or 'none' or '-' if the password should be entered at boot. required: false default: "none" @@ -92,9 +92,9 @@ def main(): name = dict(required=True), state = dict(required=True, choices=['present', 'absent', 'opts_present', 'opts_absent']), backing_device = dict(default=None), - password = dict(default=None), + password = dict(default=None, type='path'), opts = dict(default=None), - path = dict(default='/etc/crypttab') + path = dict(default='/etc/crypttab', type='path') ), supports_check_mode = True ) From 39c5421c43fd350c0aab98cea017a61a01aab889 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Thu, 13 Oct 2016 22:46:24 +0200 Subject: [PATCH 2201/2522] Make sure that the token is not printed in log (#3115) --- source_control/github_key.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source_control/github_key.py b/source_control/github_key.py index 815be9dc94b..9e2e2e9bd9b 100644 --- a/source_control/github_key.py +++ b/source_control/github_key.py @@ -199,7 +199,7 @@ def ensure_key_present(session, name, pubkey, force, check_mode): def main(): argument_spec = { - 'token': {'required': True}, + 'token': {'required': True, 'no_log': True}, 'name': {'required': True}, 'pubkey': {}, 'state': {'choices': ['present', 'absent'], 'default': 'present'}, From 32b1d9c4f8d682e1cec35fbf5d166602ff53ce8f Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Fri, 14 Oct 2016 16:36:58 +0200 Subject: [PATCH 2202/2522] Port virt to python3 (#3167) --- cloud/misc/virt.py | 4 +++- test/utils/shippable/sanity-skip-python3.txt | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cloud/misc/virt.py b/cloud/misc/virt.py index be1a7f9ec62..68ff4a6c6cb 100644 --- a/cloud/misc/virt.py +++ b/cloud/misc/virt.py @@ -510,7 +510,8 @@ def main(): rc = VIRT_SUCCESS try: rc, result = core(module) - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg=str(e)) if rc != 0: # something went wrong emit the msg @@ -521,4 +522,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception main() diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt index 94524fa8729..7cce932ec19 100644 --- a/test/utils/shippable/sanity-skip-python3.txt +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -36,7 +36,6 @@ /cloud/misc/ovirt.py /cloud/misc/proxmox.py /cloud/misc/proxmox_template.py -/cloud/misc/virt.py /cloud/misc/virt_net.py /cloud/misc/virt_pool.py /cloud/profitbricks/profitbricks.py From 4a427fdf7ddf03366c75954609e546cf12768871 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Fri, 14 Oct 2016 00:18:02 +0200 Subject: [PATCH 2203/2522] Make irc.py compile on python3 --- notification/irc.py | 4 +++- test/utils/shippable/sanity-skip-python3.txt | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/notification/irc.py b/notification/irc.py index 92f285df241..19a04862a65 100644 --- a/notification/irc.py +++ b/notification/irc.py @@ -289,7 +289,8 @@ def main(): try: send_msg(msg, server, port, channel, nick_to, key, topic, nick, color, passwd, timeout, use_ssl, part, style) - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg="unable to send to IRC: %s" % e) module.exit_json(changed=False, channel=channel, nick=nick, @@ -297,4 +298,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception main() diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt index 7cce932ec19..1deede9a264 100644 --- a/test/utils/shippable/sanity-skip-python3.txt +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -78,7 +78,6 @@ /network/nmcli.py /network/openvswitch_bridge.py /network/openvswitch_port.py -/notification/irc.py /notification/jabber.py /notification/mail.py /notification/mqtt.py From d4fa1f2037a01c0af34f2729cc2144ed91940b76 Mon Sep 17 00:00:00 2001 From: Ryan Brown Date: Fri, 14 Oct 2016 12:19:28 -0400 Subject: [PATCH 2204/2522] Fix failure to apply bucket policy when creating a bucket from scratch (#3091) --- cloud/amazon/s3_bucket.py | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/cloud/amazon/s3_bucket.py b/cloud/amazon/s3_bucket.py index 704b6e73fe8..664bac29341 100644 --- a/cloud/amazon/s3_bucket.py +++ b/cloud/amazon/s3_bucket.py @@ -194,32 +194,28 @@ def _create_or_update_bucket(connection, module, location): # Policy try: - current_policy = bucket.get_policy() + current_policy = json.loads(bucket.get_policy()) except S3ResponseError as e: if e.error_code == "NoSuchBucketPolicy": - current_policy = None + current_policy = {} else: module.fail_json(msg=e.message) + if policy is not None: + if isinstance(policy, basestring): + policy = json.loads(policy) - if current_policy is not None: - if policy == {}: + if not policy: + bucket.delete_policy() + # only show changed if there was already a policy + changed = bool(current_policy) + + elif current_policy != policy: try: - bucket.delete_policy() + bucket.set_policy(json.dumps(policy)) changed = True - current_policy = bucket.get_policy() + current_policy = json.loads(bucket.get_policy()) except S3ResponseError as e: - if e.error_code == "NoSuchBucketPolicy": - current_policy = None - else: - module.fail_json(msg=e.message) - if policy is not None: - if json.loads(current_policy) != json.loads(policy): - try: - bucket.set_policy(policy) - changed = True - current_policy = bucket.get_policy() - except S3ResponseError as e: - module.fail_json(msg=e.message) + module.fail_json(msg=e.message) # Tags try: @@ -348,7 +344,7 @@ def main(): argument_spec.update( dict( force=dict(required=False, default='no', type='bool'), - policy=dict(required=False, type='json'), + policy=dict(required=False, default=None, type='json'), name=dict(required=True, type='str'), requester_pays=dict(default='no', type='bool'), s3_url=dict(aliases=['S3_URL'], type='str'), From 352088bd94482be3a524845ef0173ce78fa0e790 Mon Sep 17 00:00:00 2001 From: Ryan Brown Date: Fri, 14 Oct 2016 12:19:36 -0400 Subject: [PATCH 2205/2522] Fix `archive` truncating archived file names based on prefix length (#3124) When archiving multiple files, the full length of the calculated `arcroot` would be removed from the beginning of all file names. If there was no arcroot, the first two characters of all files would be removed, so `file.txt` would become `le.txt`. This patch uses the regular expressions substitution anchored to the start of the string, so the arcroot is only removed if it is actually present. --- files/archive.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/files/archive.py b/files/archive.py index 2b927e39c19..3b11d86b339 100644 --- a/files/archive.py +++ b/files/archive.py @@ -245,6 +245,7 @@ def main(): elif format == 'tar': arcfile = tarfile.open(dest, 'w') + match_root = re.compile('^%s' % re.escape(arcroot)) for path in archive_paths: if os.path.isdir(path): # Recurse into directories @@ -254,7 +255,7 @@ def main(): for dirname in dirnames: fullpath = dirpath + dirname - arcname = fullpath[len(arcroot):] + arcname = match_root.sub('', fullpath) try: if format == 'zip': @@ -268,7 +269,7 @@ def main(): for filename in filenames: fullpath = dirpath + filename - arcname = fullpath[len(arcroot):] + arcname = match_root.sub('', fullpath) if not filecmp.cmp(fullpath, dest): try: @@ -283,9 +284,9 @@ def main(): errors.append('Adding %s: %s' % (path, str(e))) else: if format == 'zip': - arcfile.write(path, path[len(arcroot):]) + arcfile.write(path, match_root.sub('', path)) else: - arcfile.add(path, path[len(arcroot):], recursive=False) + arcfile.add(path, match_root.sub('', path), recursive=False) successes.append(path) From db38b45a63f4e3a85fa4efd39fd8e01f1993f981 Mon Sep 17 00:00:00 2001 From: Ryan Currah Date: Sat, 15 Oct 2016 02:46:40 -0400 Subject: [PATCH 2206/2522] Fix mongodb_user.py version detection logic (#3162) Fix mongodb_user.py version detection logic for mongo srv 2.6 and mongo driver 2.7. The wrong variable was used for detecting the mongo driver version. This fix resolves the error "(Note: you must use pymongo 2.7+ with MongoDB 2.6.. 2.6.11)" no matter what version of pymongo you had installed for mongodb 2.6. --- database/misc/mongodb_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index 33187b35b9d..8683900d807 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -191,7 +191,7 @@ def check_compatibility(module, client): elif loose_srv_version >= LooseVersion('3.0') and loose_driver_version <= LooseVersion('2.8'): module.fail_json(msg=' (Note: you must use pymongo 2.8+ with MongoDB 3.0)') - elif loose_srv_version >= LooseVersion('2.6') and loose_srv_version <= LooseVersion('2.7'): + elif loose_srv_version >= LooseVersion('2.6') and loose_driver_version <= LooseVersion('2.7'): module.fail_json(msg=' (Note: you must use pymongo 2.7+ with MongoDB 2.6)') elif LooseVersion(PyMongoVersion) <= LooseVersion('2.5'): From 16b8128c8f159f1923c605d38cf3098fb77b1dbd Mon Sep 17 00:00:00 2001 From: Patrik Lundin Date: Sat, 15 Oct 2016 09:01:48 +0200 Subject: [PATCH 2207/2522] openbsd_pkg: Use correct part of name in match. (#3151) * openbsd_pkg: Use correct part of name in match. Previously this part of the code could assume that the name was a stem with nothing else attached (like "autoconf"). With the introduction of the branch syntax ("autoconf%2.13") this is no longer true. Check if the package name was identified as using a "branch" style name, and base the match on the leading part of the name if that is the case. While here remove unnecessary "pass" and tidy up debug log message. Problem reported by @jasperla. * openbsd_pkg: Add missing "." in comment. --- packaging/os/openbsd_pkg.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packaging/os/openbsd_pkg.py b/packaging/os/openbsd_pkg.py index 59fdd35c26b..67583cdf36e 100644 --- a/packaging/os/openbsd_pkg.py +++ b/packaging/os/openbsd_pkg.py @@ -176,11 +176,14 @@ def package_present(name, installed_state, pkg_spec, module): # "file:/local/package/directory/ is empty" message on stderr # while still installing the package, so we need to look for # for a message like "packagename-1.0: ok" just in case. - match = re.search("\W%s-[^:]+: ok\W" % name, stdout) + if pkg_spec['style'] == 'branch': + match = re.search("\W%s-[^:]+: ok\W" % pkg_spec['pkgname'], stdout) + else: + match = re.search("\W%s-[^:]+: ok\W" % name, stdout) + if match: # It turns out we were able to install the package. - module.debug("package_present(): we were able to install package") - pass + module.debug("package_present(): we were able to install the package") else: # We really did fail, fake the return code. module.debug("package_present(): we really did fail") @@ -353,6 +356,10 @@ def parse_package_name(name, pkg_spec, module): pkg_spec['style'] = 'branch' + # Key names from description in pkg_add(1). + pkg_spec['pkgname'] = pkg_spec['stem'].split('%')[0] + pkg_spec['branch'] = pkg_spec['stem'].split('%')[1] + # Sanity check that there are no trailing dashes in flavor. # Try to stop strange stuff early so we can be strict later. if pkg_spec['flavor']: From baed114a92763661ac023d0c087ebde1a7fa15ce Mon Sep 17 00:00:00 2001 From: Eric D Helms Date: Sat, 15 Oct 2016 03:07:51 -0400 Subject: [PATCH 2208/2522] Fix broken entities reference in Katello module (#3136) --- infrastructure/foreman/katello.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/infrastructure/foreman/katello.py b/infrastructure/foreman/katello.py index 3f7f42942da..3d5219240b9 100644 --- a/infrastructure/foreman/katello.py +++ b/infrastructure/foreman/katello.py @@ -327,7 +327,7 @@ def sync_plan(self, params): products = params['products'] del params['products'] - sync_plan = SyncPlan( + sync_plan = self._entities.SyncPlan( self._server, name=params['name'], organization=org @@ -392,8 +392,8 @@ def find_content_view_version(self, name, organization, environment): env = self.find_lifecycle_environment(environment, organization) content_view = self.find_content_view(name, organization) - content_view_version = ContentViewVersion(self._server, content_view=content_view) - response = content_view_version.search(set('content_view'), {'environment_id': env.id}) + content_view_version = self._entities.ContentViewVersion(self._server, content_view=content_view) + response = content_view_version.search(['content_view'], {'environment_id': env.id}) if len(response) == 1: return response[0] From fb228d682d862ad1572295f74acbf0e10d290106 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sat, 15 Oct 2016 17:21:01 +0200 Subject: [PATCH 2209/2522] Make sensu_check compile on python 3 (#3177) --- monitoring/sensu_check.py | 7 +++++-- test/utils/shippable/sanity-skip-python3.txt | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/monitoring/sensu_check.py b/monitoring/sensu_check.py index 7cf38509669..dff8d19652a 100644 --- a/monitoring/sensu_check.py +++ b/monitoring/sensu_check.py @@ -206,7 +206,8 @@ def sensu_check(module, path, name, state='present', backup=False): try: stream = open(path, 'r') config = json.load(stream) - except IOError, e: + except IOError: + e = get_exception() if e.errno is 2: # File not found, non-fatal if state == 'absent': reasons.append('file did not exist and state is `absent\'') @@ -327,7 +328,8 @@ def sensu_check(module, path, name, state='present', backup=False): try: stream = open(path, 'w') stream.write(json.dumps(config, indent=2) + '\n') - except IOError, e: + except IOError: + e = get_exception() module.fail_json(msg=str(e)) finally: if stream: @@ -381,4 +383,5 @@ def main(): module.exit_json(path=path, changed=changed, msg='OK', name=name, reasons=reasons) from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception main() diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt index 1deede9a264..5e735277405 100644 --- a/test/utils/shippable/sanity-skip-python3.txt +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -63,7 +63,6 @@ /monitoring/circonus_annotation.py /monitoring/datadog_monitor.py /monitoring/rollbar_deployment.py -/monitoring/sensu_check.py /monitoring/stackdriver.py /monitoring/zabbix_group.py /monitoring/zabbix_host.py From e0eb86a5000c1913c8e2925cf2b25a809aab02ef Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sat, 15 Oct 2016 16:13:10 +0200 Subject: [PATCH 2210/2522] Make expect module compile on python 3 --- commands/expect.py | 7 +++++-- test/utils/shippable/sanity-skip-python3.txt | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/commands/expect.py b/commands/expect.py index 355f2cff480..c6f70e750e0 100644 --- a/commands/expect.py +++ b/commands/expect.py @@ -195,7 +195,8 @@ def main(): # Use pexpect.runu in pexpect>=3.3,<4 out, rc = pexpect.runu(args, timeout=timeout, withexitstatus=True, events=events, cwd=chdir, echo=echo) - except (TypeError, AttributeError), e: + except (TypeError, AttributeError): + e = get_exception() # This should catch all insufficient versions of pexpect # We deem them insufficient for their lack of ability to specify # to not echo responses via the run/runu functions, which would @@ -203,7 +204,8 @@ def main(): module.fail_json(msg='Insufficient version of pexpect installed ' '(%s), this module requires pexpect>=3.3. ' 'Error was %s' % (pexpect.__version__, e)) - except pexpect.ExceptionPexpect, e: + except pexpect.ExceptionPexpect: + e = get_exception() module.fail_json(msg='%s' % e) endd = datetime.datetime.now() @@ -230,5 +232,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception main() diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt index 5e735277405..c255551af97 100644 --- a/test/utils/shippable/sanity-skip-python3.txt +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -46,7 +46,6 @@ /clustering/consul_acl.py /clustering/consul_kv.py /clustering/consul_session.py -/commands/expect.py /database/misc/mongodb_parameter.py /database/misc/mongodb_user.py /database/misc/redis.py From 135e10556472df4be5a0b5473b0d574103783b10 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sat, 15 Oct 2016 17:36:44 +0200 Subject: [PATCH 2211/2522] Fix gce module to compile on python 3 (#3179) --- cloud/google/gce_img.py | 3 ++- cloud/google/gce_tag.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cloud/google/gce_img.py b/cloud/google/gce_img.py index 270ae827ddf..031539bf5f4 100644 --- a/cloud/google/gce_img.py +++ b/cloud/google/gce_img.py @@ -166,7 +166,8 @@ def create_image(gce, name, module): return True except ResourceExistsError: return False - except GoogleBaseError, e: + except GoogleBaseError: + e = get_exception() module.fail_json(msg=str(e), changed=False) finally: gce.connection.timeout = old_timeout diff --git a/cloud/google/gce_tag.py b/cloud/google/gce_tag.py index cb1f2a2c3ed..8f280a9ef30 100644 --- a/cloud/google/gce_tag.py +++ b/cloud/google/gce_tag.py @@ -156,7 +156,8 @@ def remove_tags(gce, module, instance_name, tags): node = gce.ex_get_node(instance_name, zone=zone) except ResourceNotFoundError: module.fail_json(msg='Instance %s not found in zone %s' % (instance_name, zone), changed=False) - except GoogleBaseError, e: + except GoogleBaseError: + e = get_exception() module.fail_json(msg=str(e), changed=False) node_tags = node.extra['tags'] From 74acb528ba0cde66879506a2374fc6f6e1e0e966 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sat, 15 Oct 2016 17:01:46 +0200 Subject: [PATCH 2212/2522] Make redis run on python 3 --- database/misc/redis.py | 16 +++++++++++----- test/utils/shippable/sanity-skip-python3.txt | 1 - 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/database/misc/redis.py b/database/misc/redis.py index be012e6d79a..80784619bb2 100644 --- a/database/misc/redis.py +++ b/database/misc/redis.py @@ -210,7 +210,8 @@ def main(): password=login_password) try: r.ping() - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg="unable to connect to database: %s" % e) #Check if we are already in the mode that we want @@ -269,7 +270,8 @@ def main(): db=db) try: r.ping() - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg="unable to connect to database: %s" % e) # Do the stuff @@ -296,13 +298,15 @@ def main(): try: r.ping() - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg="unable to connect to database: %s" % e) try: old_value = r.config_get(name)[name] - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg="unable to read config: %s" % e) changed = old_value != value @@ -311,7 +315,8 @@ def main(): else: try: r.config_set(name, value) - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg="unable to write config: %s" % e) module.exit_json(changed=changed, name=name, value=value) else: @@ -319,4 +324,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception main() diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt index c255551af97..4c0ff360429 100644 --- a/test/utils/shippable/sanity-skip-python3.txt +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -48,7 +48,6 @@ /clustering/consul_session.py /database/misc/mongodb_parameter.py /database/misc/mongodb_user.py -/database/misc/redis.py /database/mysql/mysql_replication.py /database/postgresql/postgresql_ext.py /database/postgresql/postgresql_lang.py From 8593363539b372a31ea7e6f1e8bcdaa3c17f0c0c Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sat, 15 Oct 2016 17:11:01 +0200 Subject: [PATCH 2213/2522] Port ovirt to python 3 Since ovirt sdk is not running on python 2.4, we can use python 2.6 syntax directly for exceptions. --- cloud/misc/ovirt.py | 6 +++--- test/utils/shippable/sanity-skip-python3.txt | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/cloud/misc/ovirt.py b/cloud/misc/ovirt.py index 8585dfb6b81..75572448ebe 100644 --- a/cloud/misc/ovirt.py +++ b/cloud/misc/ovirt.py @@ -461,7 +461,7 @@ def main(): #initialize connection try: c = conn(url+"/api", user, password) - except Exception, e: + except Exception as e: module.fail_json(msg='%s' % e) if state == 'present': @@ -469,14 +469,14 @@ def main(): if resource_type == 'template': try: create_vm_template(c, vmname, image, zone) - except Exception, e: + except Exception as e: module.fail_json(msg='%s' % e) module.exit_json(changed=True, msg="deployed VM %s from template %s" % (vmname,image)) elif resource_type == 'new': # FIXME: refactor, use keyword args. try: create_vm(c, vmtype, vmname, zone, vmdisk_size, vmcpus, vmnic, vmnetwork, vmmem, vmdisk_alloc, sdomain, vmcores, vmos, vmdisk_int) - except Exception, e: + except Exception as e: module.fail_json(msg='%s' % e) module.exit_json(changed=True, msg="deployed VM %s from scratch" % vmname) else: diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt index 4c0ff360429..f5a2c57b676 100644 --- a/test/utils/shippable/sanity-skip-python3.txt +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -33,7 +33,6 @@ /cloud/centurylink/clc_publicip.py /cloud/google/gce_img.py /cloud/google/gce_tag.py -/cloud/misc/ovirt.py /cloud/misc/proxmox.py /cloud/misc/proxmox_template.py /cloud/misc/virt_net.py From 4ca8e5347a5726c4872fa30800298ad606a39cb2 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sat, 15 Oct 2016 17:40:58 +0200 Subject: [PATCH 2214/2522] Make xenserver_facts compile on python 3 Since the xenapi is not needed on python 2.4, we can use the regular exception handling code --- cloud/xenserver_facts.py | 2 +- test/utils/shippable/sanity-skip-python3.txt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/cloud/xenserver_facts.py b/cloud/xenserver_facts.py index fdefee9f2e0..54a2bd61ec3 100644 --- a/cloud/xenserver_facts.py +++ b/cloud/xenserver_facts.py @@ -170,7 +170,7 @@ def main(): obj = XenServerFacts() try: session = get_xenapi_session() - except XenAPI.Failure, e: + except XenAPI.Failure as e: module.fail_json(msg='%s' % e) data = { diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt index f5a2c57b676..debb6c46173 100644 --- a/test/utils/shippable/sanity-skip-python3.txt +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -40,7 +40,6 @@ /cloud/profitbricks/profitbricks.py /cloud/profitbricks/profitbricks_volume.py /cloud/rackspace/rax_clb_ssl.py -/cloud/xenserver_facts.py /clustering/consul.py /clustering/consul_acl.py /clustering/consul_kv.py From cbdaf1044ddebee02a919edb32d47e9bd7c23da4 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Sat, 15 Oct 2016 10:16:22 -0700 Subject: [PATCH 2215/2522] Few more places where gce modules need python3 compat exceptions --- cloud/google/gce_img.py | 18 ++++++++---------- cloud/google/gce_tag.py | 12 +++++------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/cloud/google/gce_img.py b/cloud/google/gce_img.py index 031539bf5f4..010bdfdb8a6 100644 --- a/cloud/google/gce_img.py +++ b/cloud/google/gce_img.py @@ -111,7 +111,6 @@ state: absent ''' -import sys try: import libcloud @@ -125,6 +124,9 @@ except ImportError: has_libcloud = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.gce import gce_connect + GCS_URI = 'https://storage.googleapis.com/' @@ -152,7 +154,7 @@ def create_image(gce, name, module): except ResourceNotFoundError: module.fail_json(msg='Disk %s not found in zone %s' % (source, zone), changed=False) - except GoogleBaseError, e: + except GoogleBaseError as e: module.fail_json(msg=str(e), changed=False) gce_extra_args = {} @@ -166,8 +168,7 @@ def create_image(gce, name, module): return True except ResourceExistsError: return False - except GoogleBaseError: - e = get_exception() + except GoogleBaseError as e: module.fail_json(msg=str(e), changed=False) finally: gce.connection.timeout = old_timeout @@ -180,7 +181,7 @@ def delete_image(gce, name, module): return True except ResourceNotFoundError: return False - except GoogleBaseError, e: + except GoogleBaseError as e: module.fail_json(msg=str(e), changed=False) @@ -224,8 +225,5 @@ def main(): module.exit_json(changed=changed, name=name) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.gce import * - -main() +if __name__ == '__main__': + main() diff --git a/cloud/google/gce_tag.py b/cloud/google/gce_tag.py index 8f280a9ef30..477e86b6756 100644 --- a/cloud/google/gce_tag.py +++ b/cloud/google/gce_tag.py @@ -100,6 +100,9 @@ except ImportError: HAS_LIBCLOUD = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.gce import gce_connect + def add_tags(gce, module, instance_name, tags): """Add tags to instance.""" @@ -117,7 +120,7 @@ def add_tags(gce, module, instance_name, tags): node = gce.ex_get_node(instance_name, zone=zone) except ResourceNotFoundError: module.fail_json(msg='Instance %s not found in zone %s' % (instance_name, zone), changed=False) - except GoogleBaseError, e: + except GoogleBaseError as e: module.fail_json(msg=str(e), changed=False) node_tags = node.extra['tags'] @@ -156,8 +159,7 @@ def remove_tags(gce, module, instance_name, tags): node = gce.ex_get_node(instance_name, zone=zone) except ResourceNotFoundError: module.fail_json(msg='Instance %s not found in zone %s' % (instance_name, zone), changed=False) - except GoogleBaseError: - e = get_exception() + except GoogleBaseError as e: module.fail_json(msg=str(e), changed=False) node_tags = node.extra['tags'] @@ -221,10 +223,6 @@ def main(): module.exit_json(changed=changed, instance_name=instance_name, tags=tags_changed, zone=zone) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.gce import * if __name__ == '__main__': main() - From c0a3e74579defc730b7058930340a6040ccfc39b Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sat, 15 Oct 2016 17:49:00 +0200 Subject: [PATCH 2216/2522] Remove gce from the blacklist for python 3 tests, forgot in #3179 --- test/utils/shippable/sanity-skip-python3.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt index debb6c46173..8332b44c871 100644 --- a/test/utils/shippable/sanity-skip-python3.txt +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -31,8 +31,6 @@ /cloud/centurylink/clc_aa_policy.py /cloud/centurylink/clc_group.py /cloud/centurylink/clc_publicip.py -/cloud/google/gce_img.py -/cloud/google/gce_tag.py /cloud/misc/proxmox.py /cloud/misc/proxmox_template.py /cloud/misc/virt_net.py From 85ceb51e3a12cd0bf7d203efedd67c5002d6e1dc Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sat, 15 Oct 2016 17:30:50 +0200 Subject: [PATCH 2217/2522] Fix proxmox for python 3 Since it doesn't work on python 2.4, we can use the native exception handling way for python 3 --- cloud/misc/proxmox.py | 14 +++++++------- cloud/misc/proxmox_template.py | 8 ++++---- test/utils/shippable/sanity-skip-python3.txt | 2 -- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/cloud/misc/proxmox.py b/cloud/misc/proxmox.py index 786c48c3263..cd4e563313a 100644 --- a/cloud/misc/proxmox.py +++ b/cloud/misc/proxmox.py @@ -350,7 +350,7 @@ def main(): if not api_password: try: api_password = os.environ['PROXMOX_PASSWORD'] - except KeyError, e: + except KeyError as e: module.fail_json(msg='You should set api_password param or use PROXMOX_PASSWORD environment variable') try: @@ -358,7 +358,7 @@ def main(): global VZ_TYPE VZ_TYPE = 'openvz' if float(proxmox.version.get()['version']) < 4.0 else 'lxc' - except Exception, e: + except Exception as e: module.fail_json(msg='authorization on proxmox cluster failed with exception: %s' % e) if state == 'present': @@ -387,7 +387,7 @@ def main(): force = int(module.params['force'])) module.exit_json(changed=True, msg="deployed VM %s from template %s" % (vmid, module.params['ostemplate'])) - except Exception, e: + except Exception as e: module.fail_json(msg="creation of %s VM %s failed with exception: %s" % ( VZ_TYPE, vmid, e )) elif state == 'started': @@ -400,7 +400,7 @@ def main(): if start_instance(module, proxmox, vm, vmid, timeout): module.exit_json(changed=True, msg="VM %s started" % vmid) - except Exception, e: + except Exception as e: module.fail_json(msg="starting of VM %s failed with exception: %s" % ( vmid, e )) elif state == 'stopped': @@ -422,7 +422,7 @@ def main(): if stop_instance(module, proxmox, vm, vmid, timeout, force = module.params['force']): module.exit_json(changed=True, msg="VM %s is shutting down" % vmid) - except Exception, e: + except Exception as e: module.fail_json(msg="stopping of VM %s failed with exception: %s" % ( vmid, e )) elif state == 'restarted': @@ -437,7 +437,7 @@ def main(): if ( stop_instance(module, proxmox, vm, vmid, timeout, force = module.params['force']) and start_instance(module, proxmox, vm, vmid, timeout) ): module.exit_json(changed=True, msg="VM %s is restarted" % vmid) - except Exception, e: + except Exception as e: module.fail_json(msg="restarting of VM %s failed with exception: %s" % ( vmid, e )) elif state == 'absent': @@ -463,7 +463,7 @@ def main(): % proxmox_node.tasks(taskid).log.get()[:1]) time.sleep(1) - except Exception, e: + except Exception as e: module.fail_json(msg="deletion of VM %s failed with exception: %s" % ( vmid, e )) # import module snippets diff --git a/cloud/misc/proxmox_template.py b/cloud/misc/proxmox_template.py index 6434e59be23..6286bb4c720 100644 --- a/cloud/misc/proxmox_template.py +++ b/cloud/misc/proxmox_template.py @@ -184,12 +184,12 @@ def main(): if not api_password: try: api_password = os.environ['PROXMOX_PASSWORD'] - except KeyError, e: + except KeyError as e: module.fail_json(msg='You should set api_password param or use PROXMOX_PASSWORD environment variable') try: proxmox = ProxmoxAPI(api_host, user=api_user, password=api_password, verify_ssl=validate_certs) - except Exception, e: + except Exception as e: module.fail_json(msg='authorization on proxmox cluster failed with exception: %s' % e) if state == 'present': @@ -209,7 +209,7 @@ def main(): if upload_template(module, proxmox, api_host, node, storage, content_type, realpath, timeout): module.exit_json(changed=True, msg='template with volid=%s:%s/%s uploaded' % (storage, content_type, template)) - except Exception, e: + except Exception as e: module.fail_json(msg="uploading of template %s failed with exception: %s" % ( template, e )) elif state == 'absent': @@ -224,7 +224,7 @@ def main(): if delete_template(module, proxmox, node, storage, content_type, template, timeout): module.exit_json(changed=True, msg='template with volid=%s:%s/%s deleted' % (storage, content_type, template)) - except Exception, e: + except Exception as e: module.fail_json(msg="deleting of template %s failed with exception: %s" % ( template, e )) # import module snippets diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt index 8332b44c871..a8bbef1cc34 100644 --- a/test/utils/shippable/sanity-skip-python3.txt +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -31,8 +31,6 @@ /cloud/centurylink/clc_aa_policy.py /cloud/centurylink/clc_group.py /cloud/centurylink/clc_publicip.py -/cloud/misc/proxmox.py -/cloud/misc/proxmox_template.py /cloud/misc/virt_net.py /cloud/misc/virt_pool.py /cloud/profitbricks/profitbricks.py From 3767080dbf3d2dcebf1905c5628a73121194fabe Mon Sep 17 00:00:00 2001 From: Jasper Lievisse Adriaanse Date: Fri, 14 Oct 2016 14:42:45 +0200 Subject: [PATCH 2218/2522] Remove incorrect statement, uri module doesn't require httplib2 anymore --- web_infrastructure/jenkins_plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/web_infrastructure/jenkins_plugin.py b/web_infrastructure/jenkins_plugin.py index 266d7f49c77..af5adb462ff 100644 --- a/web_infrastructure/jenkins_plugin.py +++ b/web_infrastructure/jenkins_plugin.py @@ -251,7 +251,6 @@ state: restarted when: jenkins_restart_required - # Requires python-httplib2 to be installed on the guest - name: Wait for Jenkins to start up uri: url: http://localhost:8080 From a3b83533bdd4cbc0f880e27ad09ad80f4a9fd2cc Mon Sep 17 00:00:00 2001 From: Benjamin Copeland Date: Sun, 16 Oct 2016 22:38:57 +0100 Subject: [PATCH 2219/2522] statusio_maintenance: Fix minor typo (#3137) --- monitoring/statusio_maintenance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/statusio_maintenance.py b/monitoring/statusio_maintenance.py index c2b93db5c92..9e5beb3d68e 100644 --- a/monitoring/statusio_maintenance.py +++ b/monitoring/statusio_maintenance.py @@ -152,7 +152,7 @@ title: "Routine maintenance" desc: "Some security updates" components: - - "server1.example.com + - "server1.example.com" - "server2.example.com" minutes: "60" api_id: "api_id" From 196b87caec14ed66bf1dcc5e62f53fb7b2976c64 Mon Sep 17 00:00:00 2001 From: hyperized Date: Sun, 16 Oct 2016 23:41:36 +0200 Subject: [PATCH 2220/2522] Update irc.py (#3144) Proposal to update the example to YAML syntax. --- notification/irc.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/notification/irc.py b/notification/irc.py index 19a04862a65..8217805ea79 100644 --- a/notification/irc.py +++ b/notification/irc.py @@ -113,22 +113,29 @@ ''' EXAMPLES = ''' -- irc: server=irc.example.net channel="#t1" msg="Hello world" +- irc: + server: irc.example.net + channel: "#t1" + msg: "Hello world" -- local_action: irc port=6669 - server="irc.example.net" - channel="#t1" - msg="All finished at {{ ansible_date_time.iso8601 }}" - color=red - nick=ansibleIRC +- local_action: + module: irc + port: 6669 + server: "irc.example.net" + channel: "#t1" + msg: "All finished at {{ ansible_date_time.iso8601 }}" + color: red + nick: ansibleIRC -- local_action: irc port=6669 - server="irc.example.net" - channel="#t1" - nick_to=["nick1", "nick2"] - msg="All finished at {{ ansible_date_time.iso8601 }}" - color=red - nick=ansibleIRC +- local_action: + module: irc + port: 6669 + server: "irc.example.net" + channel: "#t1" + nick_to: ["nick1", "nick2"] + msg: "All finished at {{ ansible_date_time.iso8601 }}" + color: red + nick: ansibleIRC ''' # =========================================== From 5416e9528d50111b777eb46ad74e3fc3968a3c32 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sun, 16 Oct 2016 09:53:19 +0200 Subject: [PATCH 2221/2522] Make dnssimple compile on python 3 --- network/dnsimple.py | 4 +++- test/utils/shippable/sanity-skip-python3.txt | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/network/dnsimple.py b/network/dnsimple.py index 48b0003cb44..de031c0c8a9 100644 --- a/network/dnsimple.py +++ b/network/dnsimple.py @@ -294,12 +294,14 @@ def main(): else: module.fail_json(msg="'%s' is an unknown value for the state argument" % state) - except DNSimpleException, e: + except DNSimpleException: + e = get_exception() module.fail_json(msg="Unable to contact DNSimple: %s" % e.message) module.fail_json(msg="Unknown what you wanted me to do") # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception main() diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt index a8bbef1cc34..c7ed2636df0 100644 --- a/test/utils/shippable/sanity-skip-python3.txt +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -62,7 +62,6 @@ /monitoring/zabbix_screen.py /network/citrix/netscaler.py /network/cloudflare_dns.py -/network/dnsimple.py /network/dnsmadeeasy.py /network/f5/bigip_gtm_virtual_server.py /network/f5/bigip_gtm_wide_ip.py From 26b76ea1b320fd5dd8786ba9753cff5038796f7a Mon Sep 17 00:00:00 2001 From: Ville Reijonen Date: Mon, 17 Oct 2016 09:44:49 +0300 Subject: [PATCH 2222/2522] win_scheduled_task: fix days_of_week var command typo (#3198) --- windows/win_scheduled_task.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_scheduled_task.ps1 b/windows/win_scheduled_task.ps1 index 6490d5562c3..70ba45e29d3 100644 --- a/windows/win_scheduled_task.ps1 +++ b/windows/win_scheduled_task.ps1 @@ -24,7 +24,7 @@ $ErrorActionPreference = "Stop" $params = Parse-Args $args; -$days_of_week = Get-AnsibleParam $params -anem "days_of_week" +$days_of_week = Get-AnsibleParam $params -name "days_of_week" $enabled = Get-AnsibleParam $params -name "enabled" -default $true $enabled = $enabled | ConvertTo-Bool $description = Get-AnsibleParam $params -name "description" -default " " From 0f8c02b5d39173449ecf24bc3d1f50fcdb60a993 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Mon, 17 Oct 2016 11:38:20 +0200 Subject: [PATCH 2223/2522] ovirt_auth: fix type and password leak (#3119) Do not leak the password in log, and verify the path of ca_file --- cloud/ovirt/ovirt_auth.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/cloud/ovirt/ovirt_auth.py b/cloud/ovirt/ovirt_auth.py index 19ab2e1641b..50ed548eb9a 100644 --- a/cloud/ovirt/ovirt_auth.py +++ b/cloud/ovirt/ovirt_auth.py @@ -98,10 +98,7 @@ # oVirt user's password, and include that yaml file with variable: - include_vars: ovirt_password.yml - # Always be sure to pass 'no_log: true' to ovirt_auth task, - # so the oVirt user's password is not logged: - name: Obtain SSO token with using username/password credentials: - no_log: true ovirt_auth: url: https://ovirt.example.com/ovirt-engine/api username: admin@internal @@ -171,8 +168,8 @@ def main(): argument_spec=dict( url=dict(default=None), username=dict(default=None), - password=dict(default=None), - ca_file=dict(default=None), + password=dict(default=None, no_log=True), + ca_file=dict(default=None, type='path'), insecure=dict(required=False, type='bool', default=False), timeout=dict(required=False, type='int', default=0), compress=dict(required=False, type='bool', default=True), From 38dc03313ec8a741f8b41217e10210809ed66227 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sun, 16 Oct 2016 09:41:02 +0200 Subject: [PATCH 2224/2522] Make zabbix modules compile on python 3 Since the module is not compatible with python 2.4, we do not need to use the get_exception trick --- monitoring/zabbix_group.py | 6 +++--- monitoring/zabbix_host.py | 12 ++++++------ monitoring/zabbix_hostmacro.py | 12 ++++++------ monitoring/zabbix_screen.py | 2 +- test/utils/shippable/sanity-skip-python3.txt | 4 ---- 5 files changed, 16 insertions(+), 20 deletions(-) diff --git a/monitoring/zabbix_group.py b/monitoring/zabbix_group.py index a19c49794f9..ec1f65ed358 100644 --- a/monitoring/zabbix_group.py +++ b/monitoring/zabbix_group.py @@ -136,7 +136,7 @@ def create_host_group(self, group_names): except Already_Exists: return group_add_list return group_add_list - except Exception, e: + except Exception as e: self._module.fail_json(msg="Failed to create host group(s): %s" % e) # delete host group(s) @@ -145,7 +145,7 @@ def delete_host_group(self, group_ids): if self._module.check_mode: self._module.exit_json(changed=True) self._zapi.hostgroup.delete(group_ids) - except Exception, e: + except Exception as e: self._module.fail_json(msg="Failed to delete host group(s), Exception: %s" % e) # get group ids by name @@ -192,7 +192,7 @@ def main(): try: zbx = ZabbixAPI(server_url, timeout=timeout, user=http_login_user, passwd=http_login_password) zbx.login(login_user, login_password) - except Exception, e: + except Exception as e: module.fail_json(msg="Failed to connect to Zabbix server: %s" % e) hostGroup = HostGroup(module, zbx) diff --git a/monitoring/zabbix_host.py b/monitoring/zabbix_host.py index 20d8b6e21fb..6ba256897bd 100644 --- a/monitoring/zabbix_host.py +++ b/monitoring/zabbix_host.py @@ -216,7 +216,7 @@ def add_host(self, host_name, group_ids, status, interfaces, proxy_id): host_list = self._zapi.host.create(parameters) if len(host_list) >= 1: return host_list['hostids'][0] - except Exception, e: + except Exception as e: self._module.fail_json(msg="Failed to create host %s: %s" % (host_name, e)) def update_host(self, host_name, group_ids, status, host_id, interfaces, exist_interface_list, proxy_id): @@ -253,7 +253,7 @@ def update_host(self, host_name, group_ids, status, host_id, interfaces, exist_i remove_interface_ids.append(interface_id) if len(remove_interface_ids) > 0: self._zapi.hostinterface.delete(remove_interface_ids) - except Exception, e: + except Exception as e: self._module.fail_json(msg="Failed to update host %s: %s" % (host_name, e)) def delete_host(self, host_id, host_name): @@ -261,7 +261,7 @@ def delete_host(self, host_id, host_name): if self._module.check_mode: self._module.exit_json(changed=True) self._zapi.host.delete([host_id]) - except Exception, e: + except Exception as e: self._module.fail_json(msg="Failed to delete host %s: %s" % (host_name, e)) # get host by host name @@ -385,7 +385,7 @@ def link_or_clear_template(self, host_id, template_id_list): if self._module.check_mode: self._module.exit_json(changed=True) self._zapi.host.update(request_str) - except Exception, e: + except Exception as e: self._module.fail_json(msg="Failed to link template to host: %s" % e) # Update the host inventory_mode @@ -408,7 +408,7 @@ def update_inventory_mode(self, host_id, inventory_mode): if self._module.check_mode: self._module.exit_json(changed=True) self._zapi.host.update(request_str) - except Exception, e: + except Exception as e: self._module.fail_json(msg="Failed to set inventory_mode to host: %s" % e) def main(): @@ -460,7 +460,7 @@ def main(): try: zbx = ZabbixAPIExtends(server_url, timeout=timeout, user=http_login_user, passwd=http_login_password) zbx.login(login_user, login_password) - except Exception, e: + except Exception as e: module.fail_json(msg="Failed to connect to Zabbix server: %s" % e) host = Host(module, zbx) diff --git a/monitoring/zabbix_hostmacro.py b/monitoring/zabbix_hostmacro.py index c0e3f8c2280..a83a19548f1 100644 --- a/monitoring/zabbix_hostmacro.py +++ b/monitoring/zabbix_hostmacro.py @@ -129,7 +129,7 @@ def get_host_id(self, host_name): else: host_id = host_list[0]['hostid'] return host_id - except Exception, e: + except Exception as e: self._module.fail_json(msg="Failed to get the host %s id: %s." % (host_name, e)) # get host macro @@ -140,7 +140,7 @@ def get_host_macro(self, macro_name, host_id): if len(host_macro_list) > 0: return host_macro_list[0] return None - except Exception, e: + except Exception as e: self._module.fail_json(msg="Failed to get host macro %s: %s" % (macro_name, e)) # create host macro @@ -150,7 +150,7 @@ def create_host_macro(self, macro_name, macro_value, host_id): self._module.exit_json(changed=True) self._zapi.usermacro.create({'hostid': host_id, 'macro': '{$' + macro_name + '}', 'value': macro_value}) self._module.exit_json(changed=True, result="Successfully added host macro %s " % macro_name) - except Exception, e: + except Exception as e: self._module.fail_json(msg="Failed to create host macro %s: %s" % (macro_name, e)) # update host macro @@ -163,7 +163,7 @@ def update_host_macro(self, host_macro_obj, macro_name, macro_value): self._module.exit_json(changed=True) self._zapi.usermacro.update({'hostmacroid': host_macro_id, 'value': macro_value}) self._module.exit_json(changed=True, result="Successfully updated host macro %s " % macro_name) - except Exception, e: + except Exception as e: self._module.fail_json(msg="Failed to updated host macro %s: %s" % (macro_name, e)) # delete host macro @@ -174,7 +174,7 @@ def delete_host_macro(self, host_macro_obj, macro_name): self._module.exit_json(changed=True) self._zapi.usermacro.delete([host_macro_id]) self._module.exit_json(changed=True, result="Successfully deleted host macro %s " % macro_name) - except Exception, e: + except Exception as e: self._module.fail_json(msg="Failed to delete host macro %s: %s" % (macro_name, e)) def main(): @@ -213,7 +213,7 @@ def main(): try: zbx = ZabbixAPIExtends(server_url, timeout=timeout, user=http_login_user, passwd=http_login_password) zbx.login(login_user, login_password) - except Exception, e: + except Exception as e: module.fail_json(msg="Failed to connect to Zabbix server: %s" % e) host_macro_class_obj = HostMacro(module, zbx) diff --git a/monitoring/zabbix_screen.py b/monitoring/zabbix_screen.py index ffdcb21b5f3..85af561c965 100644 --- a/monitoring/zabbix_screen.py +++ b/monitoring/zabbix_screen.py @@ -354,7 +354,7 @@ def main(): try: zbx = ZabbixAPIExtends(server_url, timeout=timeout, user=http_login_user, passwd=http_login_password) zbx.login(login_user, login_password) - except Exception, e: + except Exception as e: module.fail_json(msg="Failed to connect to Zabbix server: %s" % e) screen = Screen(module, zbx) diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt index c7ed2636df0..2674bf0584a 100644 --- a/test/utils/shippable/sanity-skip-python3.txt +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -56,10 +56,6 @@ /monitoring/datadog_monitor.py /monitoring/rollbar_deployment.py /monitoring/stackdriver.py -/monitoring/zabbix_group.py -/monitoring/zabbix_host.py -/monitoring/zabbix_hostmacro.py -/monitoring/zabbix_screen.py /network/citrix/netscaler.py /network/cloudflare_dns.py /network/dnsmadeeasy.py From b90df437e0a4b5c5495646c223f8862b299f5d10 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 17 Oct 2016 13:22:32 +0200 Subject: [PATCH 2225/2522] zypper: fix for checking result is None (#3143) --- packaging/os/zypper.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packaging/os/zypper.py b/packaging/os/zypper.py index c956feac1a1..7b385e792ea 100644 --- a/packaging/os/zypper.py +++ b/packaging/os/zypper.py @@ -267,15 +267,16 @@ def get_cmd(m, subcommand): def set_diff(m, retvals, result): # TODO: if there is only one package, set before/after to version numbers packages = {'installed': [], 'removed': [], 'upgraded': []} - for p in result: - group = result[p]['group'] - if group == 'to-upgrade': - versions = ' (' + result[p]['oldversion'] + ' => ' + result[p]['version'] + ')' - packages['upgraded'].append(p + versions) - elif group == 'to-install': - packages['installed'].append(p) - elif group == 'to-remove': - packages['removed'].append(p) + if result: + for p in result: + group = result[p]['group'] + if group == 'to-upgrade': + versions = ' (' + result[p]['oldversion'] + ' => ' + result[p]['version'] + ')' + packages['upgraded'].append(p + versions) + elif group == 'to-install': + packages['installed'].append(p) + elif group == 'to-remove': + packages['removed'].append(p) output = '' for state in packages: From 8683df885d7a51be25065d77c2dceb3eb49f34e3 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sun, 16 Oct 2016 17:01:09 +0200 Subject: [PATCH 2226/2522] Do not leak login_password when using verbose, with no_log --- database/postgresql/postgresql_ext.py | 2 +- database/postgresql/postgresql_lang.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/database/postgresql/postgresql_ext.py b/database/postgresql/postgresql_ext.py index 684f3b2c32a..f9b0d9570ea 100644 --- a/database/postgresql/postgresql_ext.py +++ b/database/postgresql/postgresql_ext.py @@ -118,7 +118,7 @@ def main(): module = AnsibleModule( argument_spec=dict( login_user=dict(default="postgres"), - login_password=dict(default=""), + login_password=dict(default="", no_log=True), login_host=dict(default=""), port=dict(default="5432"), db=dict(required=True), diff --git a/database/postgresql/postgresql_lang.py b/database/postgresql/postgresql_lang.py index ccee93194ea..73ecb413439 100644 --- a/database/postgresql/postgresql_lang.py +++ b/database/postgresql/postgresql_lang.py @@ -184,7 +184,7 @@ def main(): module = AnsibleModule( argument_spec=dict( login_user=dict(default="postgres"), - login_password=dict(default=""), + login_password=dict(default="", no_log=True), login_host=dict(default=""), db=dict(required=True), port=dict(default='5432'), From 9897c502a6d8d851d96cfa577a54f25a272ac56b Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 17 Oct 2016 08:13:36 -0700 Subject: [PATCH 2227/2522] Add python-2.6 requirement to the proxmox module --- cloud/misc/proxmox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/misc/proxmox.py b/cloud/misc/proxmox.py index cd4e563313a..0ba88186e64 100644 --- a/cloud/misc/proxmox.py +++ b/cloud/misc/proxmox.py @@ -164,7 +164,7 @@ default: present notes: - Requires proxmoxer and requests modules on host. This modules can be installed with pip. -requirements: [ "proxmoxer", "requests" ] +requirements: [ "proxmoxer", "python >= 2.7", "requests" ] author: "Sergei Antipov @UnderGreen" ''' From efe82efb32e1045a73c4776d299d0ce9a6b944e6 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Mon, 17 Oct 2016 16:22:02 +0200 Subject: [PATCH 2228/2522] Make mqtt pass python3 sanity tests --- notification/mqtt.py | 4 +++- test/utils/shippable/sanity-skip-python3.txt | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/notification/mqtt.py b/notification/mqtt.py index 14713c2b1ea..89fa50a7dbd 100644 --- a/notification/mqtt.py +++ b/notification/mqtt.py @@ -156,11 +156,13 @@ def main(): hostname=server, port=port, auth=auth) - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg="unable to publish to MQTT broker %s" % (e)) module.exit_json(changed=False, topic=topic) # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception main() diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt index 2674bf0584a..bfeeff78744 100644 --- a/test/utils/shippable/sanity-skip-python3.txt +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -66,4 +66,3 @@ /network/openvswitch_port.py /notification/jabber.py /notification/mail.py -/notification/mqtt.py From 096206cdb3e5a38fb5e11ad2f67e57936ed9e9e9 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Mon, 17 Oct 2016 15:44:20 +0200 Subject: [PATCH 2229/2522] Fix dnsmadeeasy module to pass py3 sanity check --- network/dnsmadeeasy.py | 5 +++-- test/utils/shippable/sanity-skip-python3.txt | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/network/dnsmadeeasy.py b/network/dnsmadeeasy.py index 4578b5298be..a2bbd70cd9b 100644 --- a/network/dnsmadeeasy.py +++ b/network/dnsmadeeasy.py @@ -121,7 +121,8 @@ from time import strftime, gmtime import hashlib import hmac -except ImportError, e: +except ImportError: + e = get_exception() IMPORT_ERROR = str(e) class DME2: @@ -170,7 +171,7 @@ def query(self, resource, method, data=None): try: return json.load(response) - except Exception, e: + except Exception: return {} def getDomain(self, domain_id): diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt index bfeeff78744..b1e2e756066 100644 --- a/test/utils/shippable/sanity-skip-python3.txt +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -58,7 +58,6 @@ /monitoring/stackdriver.py /network/citrix/netscaler.py /network/cloudflare_dns.py -/network/dnsmadeeasy.py /network/f5/bigip_gtm_virtual_server.py /network/f5/bigip_gtm_wide_ip.py /network/nmcli.py From 9e87e79e28270894bb60032e02d8cba7b5969205 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Mon, 17 Oct 2016 15:26:04 +0200 Subject: [PATCH 2230/2522] Make rax_clb_ssl module pass sanity check for py3 --- cloud/rackspace/rax_clb_ssl.py | 6 +++--- test/utils/shippable/sanity-skip-python3.txt | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/cloud/rackspace/rax_clb_ssl.py b/cloud/rackspace/rax_clb_ssl.py index 2013b8c4d81..90773058165 100644 --- a/cloud/rackspace/rax_clb_ssl.py +++ b/cloud/rackspace/rax_clb_ssl.py @@ -156,7 +156,7 @@ def cloud_load_balancer_ssl(module, loadbalancer, state, enabled, private_key, if needs_change: try: balancer.add_ssl_termination(**ssl_attrs) - except pyrax.exceptions.PyraxException, e: + except pyrax.exceptions.PyraxException as e: module.fail_json(msg='%s' % e.message) changed = True elif state == 'absent': @@ -164,7 +164,7 @@ def cloud_load_balancer_ssl(module, loadbalancer, state, enabled, private_key, if existing_ssl: try: balancer.delete_ssl_termination() - except pyrax.exceptions.PyraxException, e: + except pyrax.exceptions.PyraxException as e: module.fail_json(msg='%s' % e.message) changed = True @@ -176,7 +176,7 @@ def cloud_load_balancer_ssl(module, loadbalancer, state, enabled, private_key, try: balancer.update(httpsRedirect=https_redirect) - except pyrax.exceptions.PyraxException, e: + except pyrax.exceptions.PyraxException as e: module.fail_json(msg='%s' % e.message) changed = True diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt index b1e2e756066..6840ee7baa5 100644 --- a/test/utils/shippable/sanity-skip-python3.txt +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -35,7 +35,6 @@ /cloud/misc/virt_pool.py /cloud/profitbricks/profitbricks.py /cloud/profitbricks/profitbricks_volume.py -/cloud/rackspace/rax_clb_ssl.py /clustering/consul.py /clustering/consul_acl.py /clustering/consul_kv.py From 4c6247c661eb6bb979829e176fabdaf02b23881d Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sun, 16 Oct 2016 09:31:59 +0200 Subject: [PATCH 2231/2522] Make mongodb modules compile on python 3 --- database/misc/mongodb_parameter.py | 10 +++++++--- database/misc/mongodb_user.py | 10 +++++++--- test/utils/shippable/sanity-skip-python3.txt | 2 -- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/database/misc/mongodb_parameter.py b/database/misc/mongodb_parameter.py index 4904be3db32..bd8192fe25c 100644 --- a/database/misc/mongodb_parameter.py +++ b/database/misc/mongodb_parameter.py @@ -184,7 +184,8 @@ def main(): try: if param_type == 'int': value = int(value) - except ValueError, e: + except ValueError: + e = get_exception() module.fail_json(msg="value '%s' is not %s" % (value, param_type)) try: @@ -204,14 +205,16 @@ def main(): if login_user is not None and login_password is not None: client.admin.authenticate(login_user, login_password, source=login_database) - except ConnectionFailure, e: + except ConnectionFailure: + e = get_exception() module.fail_json(msg='unable to connect to database: %s' % str(e)) db = client.admin try: after_value = db.command("setParameter", **{param: int(value)}) - except OperationFailure, e: + except OperationFailure: + e = get_exception() module.fail_json(msg="unable to change parameter: %s" % str(e)) if "was" not in after_value: @@ -223,6 +226,7 @@ def main(): # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception if __name__ == '__main__': main() diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index 8683900d807..fa3154701bd 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -371,7 +371,8 @@ def main(): module.fail_json(msg='The localhost login exception only allows the first admin account to be created') #else: this has to be the first admin user added - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg='unable to connect to database: %s' % str(e)) if state == 'present': @@ -389,7 +390,8 @@ def main(): module.exit_json(changed=True, user=user) user_add(module, client, db_name, user, password, roles) - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg='Unable to add or update user: %s' % str(e)) # Here we can check password change if mongo provide a query for that : https://jira.mongodb.org/browse/SERVER-22848 @@ -400,11 +402,13 @@ def main(): elif state == 'absent': try: user_remove(module, client, db_name, user) - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg='Unable to remove user: %s' % str(e)) module.exit_json(changed=True, user=user) # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception main() diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt index 6840ee7baa5..768b8de61d5 100644 --- a/test/utils/shippable/sanity-skip-python3.txt +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -39,8 +39,6 @@ /clustering/consul_acl.py /clustering/consul_kv.py /clustering/consul_session.py -/database/misc/mongodb_parameter.py -/database/misc/mongodb_user.py /database/mysql/mysql_replication.py /database/postgresql/postgresql_ext.py /database/postgresql/postgresql_lang.py From f2ce143609d8eb2af9b3d8f6f89f7dd4ea68eb22 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sun, 16 Oct 2016 16:57:57 +0200 Subject: [PATCH 2232/2522] Make postgresql module in extras compile on py3 --- database/postgresql/postgresql_ext.py | 10 +++++++--- database/postgresql/postgresql_lang.py | 4 +++- test/utils/shippable/sanity-skip-python3.txt | 2 -- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/database/postgresql/postgresql_ext.py b/database/postgresql/postgresql_ext.py index f9b0d9570ea..39c74afb4c6 100644 --- a/database/postgresql/postgresql_ext.py +++ b/database/postgresql/postgresql_ext.py @@ -159,7 +159,8 @@ def main(): .ISOLATION_LEVEL_AUTOCOMMIT) cursor = db_connection.cursor( cursor_factory=psycopg2.extras.DictCursor) - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg="unable to connect to database: %s" % e) try: @@ -174,14 +175,17 @@ def main(): elif state == "present": changed = ext_create(cursor, ext) - except NotSupportedError, e: + except NotSupportedError: + e = get_exception() module.fail_json(msg=str(e)) - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg="Database query failed: %s" % e) module.exit_json(changed=changed, db=db, ext=ext) # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception main() diff --git a/database/postgresql/postgresql_lang.py b/database/postgresql/postgresql_lang.py index 73ecb413439..a0b24cdd736 100644 --- a/database/postgresql/postgresql_lang.py +++ b/database/postgresql/postgresql_lang.py @@ -222,7 +222,8 @@ def main(): try: db_connection = psycopg2.connect(**kw) cursor = db_connection.cursor() - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg="unable to connect to database: %s" % e) changed = False lang_dropped = False @@ -267,4 +268,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception main() diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt index 768b8de61d5..0738f1e5f88 100644 --- a/test/utils/shippable/sanity-skip-python3.txt +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -40,8 +40,6 @@ /clustering/consul_kv.py /clustering/consul_session.py /database/mysql/mysql_replication.py -/database/postgresql/postgresql_ext.py -/database/postgresql/postgresql_lang.py /database/vertica/vertica_configuration.py /database/vertica/vertica_facts.py /database/vertica/vertica_role.py From 6b87e2f0d8a8baa91b74aa6cc321aecf3c019844 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sun, 16 Oct 2016 09:58:19 +0200 Subject: [PATCH 2233/2522] Make mail compile on python 3 --- notification/mail.py | 10 +++++++--- test/utils/shippable/sanity-skip-python3.txt | 1 - 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/notification/mail.py b/notification/mail.py index c8b2bb30c78..cdd5354b5fa 100644 --- a/notification/mail.py +++ b/notification/mail.py @@ -218,7 +218,8 @@ def main(): smtp = smtplib.SMTP_SSL(host, port=int(port)) except (smtplib.SMTPException, ssl.SSLError): smtp = smtplib.SMTP(host, port=int(port)) - except Exception, e: + except Exception: + e = get_exception() module.fail_json(rc=1, msg='Failed to send mail to server %s on port %s: %s' % (host, port, e)) smtp.ehlo() @@ -283,14 +284,16 @@ def main(): part.add_header('Content-disposition', 'attachment', filename=os.path.basename(file)) msg.attach(part) - except Exception, e: + except Exception: + e = get_exception() module.fail_json(rc=1, msg="Failed to send mail: can't attach file %s: %s" % (file, e)) composed = msg.as_string() try: smtp.sendmail(sender_addr, set(addr_list), composed) - except Exception, e: + except Exception: + e = get_exception() module.fail_json(rc=1, msg='Failed to send mail to %s: %s' % (", ".join(addr_list), e)) smtp.quit() @@ -299,4 +302,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception main() diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt index 0738f1e5f88..24f1bbbc67a 100644 --- a/test/utils/shippable/sanity-skip-python3.txt +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -59,4 +59,3 @@ /network/openvswitch_bridge.py /network/openvswitch_port.py /notification/jabber.py -/notification/mail.py From 10405f94d77233f47b899689219db63933d67d7f Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Mon, 17 Oct 2016 15:54:54 +0200 Subject: [PATCH 2234/2522] Make jabber module compile on python 3 --- notification/jabber.py | 4 +++- test/utils/shippable/sanity-skip-python3.txt | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/notification/jabber.py b/notification/jabber.py index 840954658f8..02751e93503 100644 --- a/notification/jabber.py +++ b/notification/jabber.py @@ -155,11 +155,13 @@ def main(): conn.send(msg) time.sleep(1) conn.disconnect() - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg="unable to send msg: %s" % e) module.exit_json(changed=False, to=to, user=user, msg=msg.getBody()) # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception main() diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt index 24f1bbbc67a..052bf355ca0 100644 --- a/test/utils/shippable/sanity-skip-python3.txt +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -58,4 +58,3 @@ /network/nmcli.py /network/openvswitch_bridge.py /network/openvswitch_port.py -/notification/jabber.py From 8dd32a8134f361185799368fce155393605175a3 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Mon, 17 Oct 2016 15:36:09 +0200 Subject: [PATCH 2235/2522] Make bigpanda module pass python3 sanity check --- monitoring/bigpanda.py | 4 +++- test/utils/shippable/sanity-skip-python3.txt | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/monitoring/bigpanda.py b/monitoring/bigpanda.py index df8e55fd745..1365eb6a49e 100644 --- a/monitoring/bigpanda.py +++ b/monitoring/bigpanda.py @@ -170,11 +170,13 @@ def main(): module.exit_json(changed=True, **deployment) else: module.fail_json(msg=json.dumps(info)) - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg=str(e)) # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.urls import * +from ansible.module_utils.pycompat24 import get_exception if __name__ == '__main__': main() diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt index 052bf355ca0..a15dd20926e 100644 --- a/test/utils/shippable/sanity-skip-python3.txt +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -45,7 +45,6 @@ /database/vertica/vertica_role.py /database/vertica/vertica_schema.py /database/vertica/vertica_user.py -/monitoring/bigpanda.py /monitoring/boundary_meter.py /monitoring/circonus_annotation.py /monitoring/datadog_monitor.py From 3f77bb6857243992f95316e3cd37d01e7f229d64 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Mon, 17 Oct 2016 15:33:02 +0200 Subject: [PATCH 2236/2522] Make consul modules pass sanity test for python 3 --- clustering/consul.py | 6 +++--- clustering/consul_acl.py | 12 ++++++------ clustering/consul_kv.py | 6 +++--- clustering/consul_session.py | 12 ++++++------ test/utils/shippable/sanity-skip-python3.txt | 4 ---- 5 files changed, 18 insertions(+), 22 deletions(-) diff --git a/clustering/consul.py b/clustering/consul.py index b9cdfb09d8a..a034e167fca 100644 --- a/clustering/consul.py +++ b/clustering/consul.py @@ -219,7 +219,7 @@ import consul from requests.exceptions import ConnectionError python_consul_installed = True -except ImportError, e: +except ImportError: python_consul_installed = False def register_with_consul(module): @@ -560,10 +560,10 @@ def main(): try: register_with_consul(module) - except ConnectionError, e: + except ConnectionError as e: module.fail_json(msg='Could not connect to consul agent at %s:%s, error was %s' % ( module.params.get('host'), module.params.get('port'), str(e))) - except Exception, e: + except Exception as e: module.fail_json(msg=str(e)) # import module snippets diff --git a/clustering/consul_acl.py b/clustering/consul_acl.py index a30ba8ab4bd..8e82beeae5e 100644 --- a/clustering/consul_acl.py +++ b/clustering/consul_acl.py @@ -124,7 +124,7 @@ import consul from requests.exceptions import ConnectionError python_consul_installed = True -except ImportError, e: +except ImportError: python_consul_installed = False try: @@ -180,11 +180,11 @@ def update_acl(module): token = consul.acl.create( name=name, type=token_type, rules=rules) changed = True - except Exception, e: + except Exception as e: module.fail_json( msg="No token returned, check your managment key and that \ the host is in the acl datacenter %s" % e) - except Exception, e: + except Exception as e: module.fail_json(msg="Could not create/update acl %s" % e) module.exit_json(changed=changed, @@ -216,7 +216,7 @@ def load_rules_for_token(module, consul_api, token): for pattern, policy in rule_set[rule_type].iteritems(): rules.add_rule(rule_type, Rule(pattern, policy['policy'])) return rules - except Exception, e: + except Exception as e: module.fail_json( msg="Could not load rule list from retrieved rule data %s, %s" % ( token, e)) @@ -351,10 +351,10 @@ def main(): try: execute(module) - except ConnectionError, e: + except ConnectionError as e: module.fail_json(msg='Could not connect to consul agent at %s:%s, error was %s' % ( module.params.get('host'), module.params.get('port'), str(e))) - except Exception, e: + except Exception as e: module.fail_json(msg=str(e)) # import module snippets diff --git a/clustering/consul_kv.py b/clustering/consul_kv.py index 8163cbd986b..9abdfff0930 100644 --- a/clustering/consul_kv.py +++ b/clustering/consul_kv.py @@ -145,7 +145,7 @@ import consul from requests.exceptions import ConnectionError python_consul_installed = True -except ImportError, e: +except ImportError: python_consul_installed = False from requests.exceptions import ConnectionError @@ -276,10 +276,10 @@ def main(): try: execute(module) - except ConnectionError, e: + except ConnectionError as e: module.fail_json(msg='Could not connect to consul agent at %s:%s, error was %s' % ( module.params.get('host'), module.params.get('port'), str(e))) - except Exception, e: + except Exception as e: module.fail_json(msg=str(e)) diff --git a/clustering/consul_session.py b/clustering/consul_session.py index 4d733561391..7b9c4bbbb9b 100644 --- a/clustering/consul_session.py +++ b/clustering/consul_session.py @@ -137,7 +137,7 @@ import consul from requests.exceptions import ConnectionError python_consul_installed = True -except ImportError, e: +except ImportError: python_consul_installed = False def execute(module): @@ -185,7 +185,7 @@ def lookup_sessions(module): session_id=session_id, sessions=session_by_id) - except Exception, e: + except Exception as e: module.fail_json(msg="Could not retrieve session info %s" % e) @@ -216,7 +216,7 @@ def update_session(module): delay=delay, checks=checks, node=node) - except Exception, e: + except Exception as e: module.fail_json(msg="Could not create/update session %s" % e) @@ -233,7 +233,7 @@ def remove_session(module): module.exit_json(changed=True, session_id=session_id) - except Exception, e: + except Exception as e: module.fail_json(msg="Could not remove session with id '%s' %s" % ( session_id, e)) @@ -270,10 +270,10 @@ def main(): try: execute(module) - except ConnectionError, e: + except ConnectionError as e: module.fail_json(msg='Could not connect to consul agent at %s:%s, error was %s' % ( module.params.get('host'), module.params.get('port'), str(e))) - except Exception, e: + except Exception as e: module.fail_json(msg=str(e)) # import module snippets diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt index a15dd20926e..795d92d4b8b 100644 --- a/test/utils/shippable/sanity-skip-python3.txt +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -35,10 +35,6 @@ /cloud/misc/virt_pool.py /cloud/profitbricks/profitbricks.py /cloud/profitbricks/profitbricks_volume.py -/clustering/consul.py -/clustering/consul_acl.py -/clustering/consul_kv.py -/clustering/consul_session.py /database/mysql/mysql_replication.py /database/vertica/vertica_configuration.py /database/vertica/vertica_facts.py From d439271f685c01ace04025f27e7046dd35d988f8 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Tue, 18 Oct 2016 18:14:02 +0200 Subject: [PATCH 2237/2522] Make openvswitch* pass py3 sanity check --- network/openvswitch_bridge.py | 7 +++++-- network/openvswitch_port.py | 7 +++++-- test/utils/shippable/sanity-skip-python3.txt | 2 -- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/network/openvswitch_bridge.py b/network/openvswitch_bridge.py index 68528dd478a..abe89dfdd42 100644 --- a/network/openvswitch_bridge.py +++ b/network/openvswitch_bridge.py @@ -143,7 +143,8 @@ def check(self): changed = True elif self.state == 'present' and not self.exists(): changed = True - except Exception, earg: + except Exception: + earg = get_exception() self.module.fail_json(msg=str(earg)) # pylint: enable=W0703 @@ -189,7 +190,8 @@ def run(self): self.set_external_id(key, None)): changed = True - except Exception, earg: + except Exception: + earg = get_exception() self.module.fail_json(msg=str(earg)) # pylint: enable=W0703 self.module.exit_json(changed=changed) @@ -267,4 +269,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception main() diff --git a/network/openvswitch_port.py b/network/openvswitch_port.py index c2224b5240e..d2bf31a77a0 100644 --- a/network/openvswitch_port.py +++ b/network/openvswitch_port.py @@ -204,7 +204,8 @@ def check(self): changed = True else: changed = False - except Exception, earg: + except Exception: + earg = get_exception() self.module.fail_json(msg=str(earg)) self.module.exit_json(changed=changed) @@ -235,7 +236,8 @@ def run(self): external_id = fmt_opt % (self.port, key, value) changed = self.set(external_id) or changed ## - except Exception, earg: + except Exception: + earg = get_exception() self.module.fail_json(msg=str(earg)) self.module.exit_json(changed=changed) @@ -269,4 +271,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils.pycompat24 import get_exception main() diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt index 795d92d4b8b..4831e17134a 100644 --- a/test/utils/shippable/sanity-skip-python3.txt +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -51,5 +51,3 @@ /network/f5/bigip_gtm_virtual_server.py /network/f5/bigip_gtm_wide_ip.py /network/nmcli.py -/network/openvswitch_bridge.py -/network/openvswitch_port.py From accb04b8679c44d71a6108dcedd2284cdff3dccf Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Wed, 19 Oct 2016 10:02:33 +0200 Subject: [PATCH 2238/2522] Do not import splitter, since we do not use it --- files/blockinfile.py | 1 - 1 file changed, 1 deletion(-) diff --git a/files/blockinfile.py b/files/blockinfile.py index 7b25101242f..8ba76181886 100755 --- a/files/blockinfile.py +++ b/files/blockinfile.py @@ -315,6 +315,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -from ansible.module_utils.splitter import * if __name__ == '__main__': main() From 38a35d7b968318dbdea43ffb3f5d1a4387617040 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Wed, 19 Oct 2016 10:30:49 -0700 Subject: [PATCH 2239/2522] Adds bigip_gtm_facts module (#3232) This patch adds support for querying the GTM(DNS) facts from a BIG-IP. This completes a previous PR that was requested but not finished. Tests for this module can be found here https://github.com/F5Networks/f5-ansible/blob/master/roles/bigip_gtm_facts/tasks/main.yaml Platforms this was tested on are 11.6.0 12.1.0 HF1 --- network/f5/bigip_gtm_facts.py | 491 ++++++++++++++++++++++++++++++++++ 1 file changed, 491 insertions(+) create mode 100644 network/f5/bigip_gtm_facts.py diff --git a/network/f5/bigip_gtm_facts.py b/network/f5/bigip_gtm_facts.py new file mode 100644 index 00000000000..bd54904f95c --- /dev/null +++ b/network/f5/bigip_gtm_facts.py @@ -0,0 +1,491 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2016 F5 Networks Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: bigip_gtm_facts +short_description: Collect facts from F5 BIG-IP GTM devices. +description: + - Collect facts from F5 BIG-IP GTM devices. +version_added: "2.3" +options: + include: + description: + - Fact category to collect + required: true + choices: + - pool + - wide_ip + - virtual_server + filter: + description: + - Perform regex filter of response. Filtering is done on the name of + the resource. Valid filters are anything that can be provided to + Python's C(re) module. + required: false + default: None +notes: + - Requires the f5-sdk Python package on the host. This is as easy as + pip install f5-sdk +extends_documentation_fragment: f5 +requirements: + - f5-sdk +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = ''' +- name: Get pool facts + bigip_gtm_facts: + server: "lb.mydomain.com" + user: "admin" + password: "secret" + include: "pool" + filter: "my_pool" + delegate_to: localhost +''' + +RETURN = ''' +wide_ip: + description: + Contains the lb method for the wide ip and the pools + that are within the wide ip. + returned: changed + type: dict + sample: + wide_ip: + - enabled: "True" + failure_rcode: "noerror" + failure_rcode_response: "disabled" + failure_rcode_ttl: "0" + full_path: "/Common/foo.ok.com" + last_resort_pool: "" + minimal_response: "enabled" + name: "foo.ok.com" + partition: "Common" + persist_cidr_ipv4: "32" + persist_cidr_ipv6: "128" + persistence: "disabled" + pool_lb_mode: "round-robin" + pools: + - name: "d3qw" + order: "0" + partition: "Common" + ratio: "1" + ttl_persistence: "3600" + type: "naptr" +pool: + description: Contains the pool object status and enabled status. + returned: changed + type: dict + sample: + pool: + - alternate_mode: "round-robin" + dynamic_ratio: "disabled" + enabled: "True" + fallback_mode: "return-to-dns" + full_path: "/Common/d3qw" + load_balancing_mode: "round-robin" + manual_resume: "disabled" + max_answers_returned: "1" + members: + - disabled: "True" + flags: "a" + full_path: "ok3.com" + member_order: "0" + name: "ok3.com" + order: "10" + preference: "10" + ratio: "1" + service: "80" + name: "d3qw" + partition: "Common" + qos_hit_ratio: "5" + qos_hops: "0" + qos_kilobytes_second: "3" + qos_lcs: "30" + qos_packet_rate: "1" + qos_rtt: "50" + qos_topology: "0" + qos_vs_capacity: "0" + qos_vs_score: "0" + ttl: "30" + type: "naptr" + verify_member_availability: "disabled" +virtual_server: + description: + Contains the virtual server enabled and availability + status, and address + returned: changed + type: dict + sample: + virtual_server: + - addresses: + - device_name: "/Common/qweqwe" + name: "10.10.10.10" + translation: "none" + datacenter: "/Common/xfxgh" + enabled: "True" + expose_route_domains: "no" + full_path: "/Common/qweqwe" + iq_allow_path: "yes" + iq_allow_service_check: "yes" + iq_allow_snmp: "yes" + limit_cpu_usage: "0" + limit_cpu_usage_status: "disabled" + limit_max_bps: "0" + limit_max_bps_status: "disabled" + limit_max_connections: "0" + limit_max_connections_status: "disabled" + limit_max_pps: "0" + limit_max_pps_status: "disabled" + limit_mem_avail: "0" + limit_mem_avail_status: "disabled" + link_discovery: "disabled" + monitor: "/Common/bigip " + name: "qweqwe" + partition: "Common" + product: "single-bigip" + virtual_server_discovery: "disabled" + virtual_servers: + - destination: "10.10.10.10:0" + enabled: "True" + full_path: "jsdfhsd" + limit_max_bps: "0" + limit_max_bps_status: "disabled" + limit_max_connections: "0" + limit_max_connections_status: "disabled" + limit_max_pps: "0" + limit_max_pps_status: "disabled" + name: "jsdfhsd" + translation_address: "none" + translation_port: "0" +''' + +try: + from distutils.version import LooseVersion + from f5.bigip.contexts import TransactionContextManager + from f5.bigip import ManagementRoot + from icontrol.session import iControlUnexpectedHTTPError + + HAS_F5SDK = True +except ImportError: + HAS_F5SDK = False + +import re + + +class BigIpGtmFactsCommon(object): + def __init__(self): + self.api = None + self.attributes_to_remove = [ + 'kind', 'generation', 'selfLink', '_meta_data', + 'membersReference', 'datacenterReference', + 'virtualServersReference', 'nameReference' + ] + self.gtm_types = dict( + a_s='a', + aaaas='aaaa', + cnames='cname', + mxs='mx', + naptrs='naptr', + srvs='srv' + ) + self.request_params = dict( + params='expandSubcollections=true' + ) + + def is_version_less_than_12(self): + version = self.api.tmos_version + if LooseVersion(version) < LooseVersion('12.0.0'): + return True + else: + return False + + def format_string_facts(self, parameters): + result = dict() + for attribute in self.attributes_to_remove: + parameters.pop(attribute, None) + for key, val in parameters.iteritems(): + result[key] = str(val) + return result + + def filter_matches_name(self, name): + if not self.params['filter']: + return True + matches = re.match(self.params['filter'], str(name)) + if matches: + return True + else: + return False + + def get_facts_from_collection(self, collection, collection_type=None): + results = [] + for item in collection: + if not self.filter_matches_name(item.name): + continue + facts = self.format_facts(item, collection_type) + results.append(facts) + return results + + def connect_to_bigip(self, **kwargs): + return ManagementRoot(kwargs['server'], + kwargs['user'], + kwargs['password'], + port=kwargs['server_port']) + + +class BigIpGtmFactsPools(BigIpGtmFactsCommon): + def __init__(self, *args, **kwargs): + super(BigIpGtmFactsPools, self).__init__() + self.params = kwargs + + def get_facts(self): + self.api = self.connect_to_bigip(**self.params) + return self.get_facts_from_device() + + def get_facts_from_device(self): + try: + if self.is_version_less_than_12(): + return self.get_facts_without_types() + else: + return self.get_facts_with_types() + except iControlUnexpectedHTTPError as e: + raise F5ModuleError(str(e)) + + def get_facts_with_types(self): + result = [] + for key, type in self.gtm_types.iteritems(): + facts = self.get_all_facts_by_type(key, type) + if facts: + result.append(facts) + return result + + def get_facts_without_types(self): + pools = self.api.tm.gtm.pools.get_collection(**self.request_params) + return self.get_facts_from_collection(pools) + + def get_all_facts_by_type(self, key, type): + collection = getattr(self.api.tm.gtm.pools, key) + pools = collection.get_collection(**self.request_params) + return self.get_facts_from_collection(pools, type) + + def format_facts(self, pool, collection_type): + result = dict() + pool_dict = pool.to_dict() + result.update(self.format_string_facts(pool_dict)) + result.update(self.format_member_facts(pool)) + if collection_type: + result['type'] = collection_type + return camel_dict_to_snake_dict(result) + + def format_member_facts(self, pool): + result = [] + if not 'items' in pool.membersReference: + return dict(members=[]) + for member in pool.membersReference['items']: + member_facts = self.format_string_facts(member) + result.append(member_facts) + return dict(members=result) + + +class BigIpGtmFactsWideIps(BigIpGtmFactsCommon): + def __init__(self, *args, **kwargs): + super(BigIpGtmFactsWideIps, self).__init__() + self.params = kwargs + + def get_facts(self): + self.api = self.connect_to_bigip(**self.params) + return self.get_facts_from_device() + + def get_facts_from_device(self): + try: + if self.is_version_less_than_12(): + return self.get_facts_without_types() + else: + return self.get_facts_with_types() + except iControlUnexpectedHTTPError as e: + raise F5ModuleError(str(e)) + + def get_facts_with_types(self): + result = [] + for key, type in self.gtm_types.iteritems(): + facts = self.get_all_facts_by_type(key, type) + if facts: + result.append(facts) + return result + + def get_facts_without_types(self): + wideips = self.api.tm.gtm.wideips.get_collection( + **self.request_params + ) + return self.get_facts_from_collection(wideips) + + def get_all_facts_by_type(self, key, type): + collection = getattr(self.api.tm.gtm.wideips, key) + wideips = collection.get_collection(**self.request_params) + return self.get_facts_from_collection(wideips, type) + + def format_facts(self, wideip, collection_type): + result = dict() + wideip_dict = wideip.to_dict() + result.update(self.format_string_facts(wideip_dict)) + result.update(self.format_pool_facts(wideip)) + if collection_type: + result['type'] = collection_type + return camel_dict_to_snake_dict(result) + + def format_pool_facts(self, wideip): + result = [] + if not hasattr(wideip, 'pools'): + return dict(pools=[]) + for pool in wideip.pools: + pool_facts = self.format_string_facts(pool) + result.append(pool_facts) + return dict(pools=result) + + +class BigIpGtmFactsVirtualServers(BigIpGtmFactsCommon): + def __init__(self, *args, **kwargs): + super(BigIpGtmFactsVirtualServers, self).__init__() + self.params = kwargs + + def get_facts(self): + try: + self.api = self.connect_to_bigip(**self.params) + return self.get_facts_from_device() + except iControlUnexpectedHTTPError as e: + raise F5ModuleError(str(e)) + + def get_facts_from_device(self): + servers = self.api.tm.gtm.servers.get_collection( + **self.request_params + ) + return self.get_facts_from_collection(servers) + + def format_facts(self, server, collection_type=None): + result = dict() + server_dict = server.to_dict() + result.update(self.format_string_facts(server_dict)) + result.update(self.format_address_facts(server)) + result.update(self.format_virtual_server_facts(server)) + return camel_dict_to_snake_dict(result) + + def format_address_facts(self, server): + result = [] + if not hasattr(server, 'addresses'): + return dict(addresses=[]) + for address in server.addresses: + address_facts = self.format_string_facts(address) + result.append(address_facts) + return dict(addresses=result) + + def format_virtual_server_facts(self, server): + result = [] + if not 'items' in server.virtualServersReference: + return dict(virtual_servers=[]) + for server in server.virtualServersReference['items']: + server_facts = self.format_string_facts(server) + result.append(server_facts) + return dict(virtual_servers=result) + +class BigIpGtmFactsManager(object): + def __init__(self, *args, **kwargs): + self.params = kwargs + self.api = None + + def get_facts(self): + result = dict() + facts = dict() + + if 'pool' in self.params['include']: + facts['pool'] = self.get_pool_facts() + if 'wide_ip' in self.params['include']: + facts['wide_ip'] = self.get_wide_ip_facts() + if 'virtual_server' in self.params['include']: + facts['virtual_server'] = self.get_virtual_server_facts() + + result.update(**facts) + result.update(dict(changed=True)) + return result + + def get_pool_facts(self): + pools = BigIpGtmFactsPools(**self.params) + return pools.get_facts() + + def get_wide_ip_facts(self): + wide_ips = BigIpGtmFactsWideIps(**self.params) + return wide_ips.get_facts() + + def get_virtual_server_facts(self): + wide_ips = BigIpGtmFactsVirtualServers(**self.params) + return wide_ips.get_facts() + + +class BigIpGtmFactsModuleConfig(object): + def __init__(self): + self.argument_spec = dict() + self.meta_args = dict() + self.supports_check_mode = False + self.valid_includes = ['pool', 'wide_ip', 'virtual_server'] + self.initialize_meta_args() + self.initialize_argument_spec() + + def initialize_meta_args(self): + args = dict( + include=dict(type='list', required=True), + filter=dict(type='str', required=False) + ) + self.meta_args = args + + def initialize_argument_spec(self): + self.argument_spec = f5_argument_spec() + self.argument_spec.update(self.meta_args) + + def create(self): + return AnsibleModule( + argument_spec=self.argument_spec, + supports_check_mode=self.supports_check_mode + ) + + +def main(): + if not HAS_F5SDK: + raise F5ModuleError("The python f5-sdk module is required") + + config = BigIpGtmFactsModuleConfig() + module = config.create() + + try: + obj = BigIpGtmFactsManager( + check_mode=module.check_mode, **module.params + ) + result = obj.get_facts() + + module.exit_json(**result) + except F5ModuleError as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import camel_dict_to_snake_dict +from ansible.module_utils.f5 import * + +if __name__ == '__main__': + main() From 96d1316119befd28fa66a71dfaa074384c3c42aa Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Wed, 19 Oct 2016 10:32:28 -0700 Subject: [PATCH 2240/2522] Adds the bigip_hostname module (#3216) This module can be used to change the hostname on a bigip. The hostname must be set via the tmsh or API methods or else it will not reflect properly in the config. Tests for this module can be found here https://github.com/F5Networks/f5-ansible/blob/master/roles/bigip_hostname/tasks/main.yaml Platforms this was tested on are 12.0.0 12.1.0 HF1 --- network/f5/bigip_hostname.py | 184 +++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 network/f5/bigip_hostname.py diff --git a/network/f5/bigip_hostname.py b/network/f5/bigip_hostname.py new file mode 100644 index 00000000000..72f423de142 --- /dev/null +++ b/network/f5/bigip_hostname.py @@ -0,0 +1,184 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2016 F5 Networks Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: bigip_hostname +short_description: Manage the hostname of a BIG-IP. +description: + - Manage the hostname of a BIG-IP. +version_added: "2.3" +options: + hostname: + description: + - Hostname of the BIG-IP host. + required: true +notes: + - Requires the f5-sdk Python package on the host. This is as easy as pip + install f5-sdk. +extends_documentation_fragment: f5 +requirements: + - f5-sdk +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = ''' +- name: Set the hostname of the BIG-IP + bigip_hostname: + hostname: "bigip.localhost.localdomain" + password: "admin" + server: "bigip.localhost.localdomain" + user: "admin" + delegate_to: localhost +''' + +RETURN = ''' +hostname: + description: The new hostname of the device + returned: changed + type: string + sample: "big-ip01.internal" +''' + +try: + from f5.bigip.contexts import TransactionContextManager + from f5.bigip import ManagementRoot + from icontrol.session import iControlUnexpectedHTTPError + HAS_F5SDK = True +except ImportError: + HAS_F5SDK = False + + +class BigIpHostnameManager(object): + def __init__(self, *args, **kwargs): + self.changed_params = dict() + self.params = kwargs + self.api = None + + def connect_to_bigip(self, **kwargs): + return ManagementRoot(kwargs['server'], + kwargs['user'], + kwargs['password'], + port=kwargs['server_port']) + + def ensure_hostname_is_present(self): + self.changed_params['hostname'] = self.params['hostname'] + + if self.params['check_mode']: + return True + + tx = self.api.tm.transactions.transaction + with TransactionContextManager(tx) as api: + r = api.tm.sys.global_settings.load() + r.update(hostname=self.params['hostname']) + + if self.hostname_exists(): + return True + else: + raise F5ModuleError("Failed to set the hostname") + + def hostname_exists(self): + if self.params['hostname'] == self.current_hostname(): + return True + else: + return False + + def present(self): + if self.hostname_exists(): + return False + else: + + return self.ensure_hostname_is_present() + + def current_hostname(self): + r = self.api.tm.sys.global_settings.load() + return r.hostname + + def apply_changes(self): + result = dict() + + changed = self.apply_to_running_config() + if changed: + self.save_running_config() + + result.update(**self.changed_params) + result.update(dict(changed=changed)) + return result + + def apply_to_running_config(self): + try: + self.api = self.connect_to_bigip(**self.params) + return self.present() + except iControlUnexpectedHTTPError as e: + raise F5ModuleError(str(e)) + + def save_running_config(self): + self.api.tm.sys.config.exec_cmd('save') + + +class BigIpHostnameModuleConfig(object): + def __init__(self): + self.argument_spec = dict() + self.meta_args = dict() + self.supports_check_mode = True + + self.initialize_meta_args() + self.initialize_argument_spec() + + def initialize_meta_args(self): + args = dict( + hostname=dict(required=True) + ) + self.meta_args = args + + def initialize_argument_spec(self): + self.argument_spec = f5_argument_spec() + self.argument_spec.update(self.meta_args) + + def create(self): + return AnsibleModule( + argument_spec=self.argument_spec, + supports_check_mode=self.supports_check_mode + ) + + +def main(): + if not HAS_F5SDK: + raise F5ModuleError("The python f5-sdk module is required") + + config = BigIpHostnameModuleConfig() + module = config.create() + + try: + obj = BigIpHostnameManager( + check_mode=module.check_mode, **module.params + ) + result = obj.apply_changes() + + module.exit_json(**result) + except F5ModuleError as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.basic import * +from ansible.module_utils.f5 import * + +if __name__ == '__main__': + main() From bd151643668da651b0d81ae7fae608c9fdd88349 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Wed, 19 Oct 2016 10:32:50 -0700 Subject: [PATCH 2241/2522] Adds module to manage SNAT pools on a BIG-IP (#3217) This module adds the ability to manage the lifecycle of SNAT pools on a BIG-IP. Tests for this module can be found here https://github.com/F5Networks/f5-ansible/blob/master/roles/bigip_snat_pool/tasks/main.yaml Platforms this was tested on are 12.1.0 HF1 --- network/f5/bigip_snat_pool.py | 413 ++++++++++++++++++++++++++++++++++ 1 file changed, 413 insertions(+) create mode 100644 network/f5/bigip_snat_pool.py diff --git a/network/f5/bigip_snat_pool.py b/network/f5/bigip_snat_pool.py new file mode 100644 index 00000000000..bacdf218cd6 --- /dev/null +++ b/network/f5/bigip_snat_pool.py @@ -0,0 +1,413 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2016 F5 Networks Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: bigip_snat_pool +short_description: Manage SNAT pools on a BIG-IP. +description: + - Manage SNAT pools on a BIG-IP. +version_added: "2.3" +options: + append: + description: + - When C(yes), will only add members to the SNAT pool. When C(no), will + replace the existing member list with the provided member list. + choices: + - yes + - no + default: no + members: + description: + - List of members to put in the SNAT pool. When a C(state) of present is + provided, this parameter is required. Otherwise, it is optional. + required: false + default: None + aliases: ['member'] + name: + description: The name of the SNAT pool. + required: True + state: + description: + - Whether the SNAT pool should exist or not. + required: false + default: present + choices: + - present + - absent +notes: + - Requires the f5-sdk Python package on the host. This is as easy as + pip install f5-sdk + - Requires the netaddr Python package on the host. This is as easy as + pip install netaddr +extends_documentation_fragment: f5 +requirements: + - f5-sdk +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = ''' +- name: Add the SNAT pool 'my-snat-pool' + bigip_snat_pool: + server: "lb.mydomain.com" + user: "admin" + password: "secret" + name: "my-snat-pool" + state: "present" + members: + - 10.10.10.10 + - 20.20.20.20 + delegate_to: localhost + +- name: Change the SNAT pool's members to a single member + bigip_snat_pool: + server: "lb.mydomain.com" + user: "admin" + password: "secret" + name: "my-snat-pool" + state: "present" + member: "30.30.30.30" + delegate_to: localhost + +- name: Append a new list of members to the existing pool + bigip_snat_pool: + server: "lb.mydomain.com" + user: "admin" + password: "secret" + name: "my-snat-pool" + state: "present" + members: + - 10.10.10.10 + - 20.20.20.20 + delegate_to: localhost + +- name: Remove the SNAT pool 'my-snat-pool' + bigip_snat_pool: + server: "lb.mydomain.com" + user: "admin" + password: "secret" + name: "johnd" + state: "absent" + delegate_to: localhost +''' + +RETURN = ''' +members: + description: + - List of members that are part of the SNAT pool. + returned: changed and success + type: list + sample: "['10.10.10.10']" +''' + +try: + from f5.bigip.contexts import TransactionContextManager + from f5.bigip import ManagementRoot + from icontrol.session import iControlUnexpectedHTTPError + + HAS_F5SDK = True +except ImportError: + HAS_F5SDK = False + +try: + from netaddr import IPAddress, AddrFormatError + HAS_NETADDR = True +except ImportError: + HAS_NETADDR = False + + +class BigIpSnatPoolManager(object): + def __init__(self, *args, **kwargs): + self.changed_params = dict() + self.params = kwargs + self.api = None + + def apply_changes(self): + result = dict() + + changed = self.apply_to_running_config() + if changed: + self.save_running_config() + + result.update(**self.changed_params) + result.update(dict(changed=changed)) + return result + + def apply_to_running_config(self): + try: + self.api = self.connect_to_bigip(**self.params) + if self.params['state'] == "present": + return self.present() + elif self.params['state'] == "absent": + return self.absent() + except iControlUnexpectedHTTPError as e: + raise F5ModuleError(str(e)) + + def save_running_config(self): + self.api.tm.sys.config.exec_cmd('save') + + def present(self): + if self.params['members'] is None: + raise F5ModuleError( + "The members parameter must be specified" + ) + + if self.snat_pool_exists(): + return self.update_snat_pool() + else: + return self.ensure_snat_pool_is_present() + + def absent(self): + changed = False + if self.snat_pool_exists(): + changed = self.ensure_snat_pool_is_absent() + return changed + + def connect_to_bigip(self, **kwargs): + return ManagementRoot(kwargs['server'], + kwargs['user'], + kwargs['password'], + port=kwargs['server_port']) + + def read_snat_pool_information(self): + pool = self.load_snat_pool() + return self.format_snat_pool_information(pool) + + def format_snat_pool_information(self, pool): + """Ensure that the pool information is in a standard format + + The SDK provides information back in a format that may change with + the version of BIG-IP being worked with. Therefore, we need to make + sure that the data is formatted in a way that our module expects it. + + Additionally, this takes care of minor variations between Python 2 + and Python 3. + + :param pool: + :return: + """ + result = dict() + result['name'] = str(pool.name) + if hasattr(pool, 'members'): + result['members'] = self.format_current_members(pool) + return result + + def format_current_members(self, pool): + result = set() + partition_prefix = "/{0}/".format(self.params['partition']) + + for member in pool.members: + member = str(member.replace(partition_prefix, '')) + result.update([member]) + return list(result) + + def load_snat_pool(self): + return self.api.tm.ltm.snatpools.snatpool.load( + name=self.params['name'], + partition=self.params['partition'] + ) + + def snat_pool_exists(self): + return self.api.tm.ltm.snatpools.snatpool.exists( + name=self.params['name'], + partition=self.params['partition'] + ) + + def update_snat_pool(self): + params = self.get_changed_parameters() + if params: + self.changed_params = camel_dict_to_snake_dict(params) + if self.params['check_mode']: + return True + else: + return False + params['name'] = self.params['name'] + params['partition'] = self.params['partition'] + self.update_snat_pool_on_device(params) + return True + + def update_snat_pool_on_device(self, params): + tx = self.api.tm.transactions.transaction + with TransactionContextManager(tx) as api: + r = api.tm.ltm.snatpools.snatpool.load( + name=self.params['name'], + partition=self.params['partition'] + ) + r.modify(**params) + + def get_changed_parameters(self): + result = dict() + current = self.read_snat_pool_information() + if self.are_members_changed(current): + result['members'] = self.get_new_member_list(current['members']) + return result + + def are_members_changed(self, current): + if self.params['members'] is None: + return False + if 'members' not in current: + return True + if set(self.params['members']) == set(current['members']): + return False + if not self.params['append']: + return True + + # Checking to see if the supplied list is a subset of the current + # list is only relevant if the `append` parameter is provided. + new_members = set(self.params['members']) + current_members = set(current['members']) + if new_members.issubset(current_members): + return False + else: + return True + + def get_new_member_list(self, current_members): + result = set() + + if self.params['append']: + result.update(set(current_members)) + result.update(set(self.params['members'])) + else: + result.update(set(self.params['members'])) + return list(result) + + def ensure_snat_pool_is_present(self): + params = self.get_snat_pool_creation_parameters() + self.changed_params = camel_dict_to_snake_dict(params) + if self.params['check_mode']: + return True + self.create_snat_pool_on_device(params) + if self.snat_pool_exists(): + return True + else: + raise F5ModuleError("Failed to create the SNAT pool") + + def get_snat_pool_creation_parameters(self): + members = self.get_formatted_members_list() + return dict( + name=self.params['name'], + partition=self.params['partition'], + members=members + ) + + def get_formatted_members_list(self): + result = set() + try: + for ip in self.params['members']: + address = str(IPAddress(ip)) + result.update([address]) + return list(result) + except AddrFormatError: + raise F5ModuleError( + 'The provided member address is not a valid IP address' + ) + + def create_snat_pool_on_device(self, params): + tx = self.api.tm.transactions.transaction + with TransactionContextManager(tx) as api: + api.tm.ltm.snatpools.snatpool.create(**params) + + def ensure_snat_pool_is_absent(self): + if self.params['check_mode']: + return True + self.delete_snat_pool_from_device() + if self.snat_pool_exists(): + raise F5ModuleError("Failed to delete the SNAT pool") + return True + + def delete_snat_pool_from_device(self): + tx = self.api.tm.transactions.transaction + with TransactionContextManager(tx) as api: + pool = api.tm.ltm.snatpools.snatpool.load( + name=self.params['name'], + partition=self.params['partition'] + ) + pool.delete() + + +class BigIpSnatPoolModuleConfig(object): + def __init__(self): + self.argument_spec = dict() + self.meta_args = dict() + self.supports_check_mode = True + self.states = ['absent', 'present'] + + self.initialize_meta_args() + self.initialize_argument_spec() + + def initialize_meta_args(self): + args = dict( + append=dict( + default=False, + type='bool', + choices=BOOLEANS + ), + name=dict(required=True), + members=dict( + required=False, + default=None, + type='list', + aliases=['member'] + ), + state=dict( + default='present', + choices=self.states + ) + ) + self.meta_args = args + + def initialize_argument_spec(self): + self.argument_spec = f5_argument_spec() + self.argument_spec.update(self.meta_args) + + def create(self): + return AnsibleModule( + argument_spec=self.argument_spec, + supports_check_mode=self.supports_check_mode + ) + + +def main(): + if not HAS_F5SDK: + raise F5ModuleError("The python f5-sdk module is required") + + if not HAS_NETADDR: + raise F5ModuleError("The python netaddr module is required") + + config = BigIpSnatPoolModuleConfig() + module = config.create() + + try: + obj = BigIpSnatPoolManager( + check_mode=module.check_mode, **module.params + ) + result = obj.apply_changes() + + module.exit_json(**result) + except F5ModuleError as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import camel_dict_to_snake_dict +from ansible.module_utils.f5 import * + +if __name__ == '__main__': + main() From 77ac192f3b9b32143c5fd303810c079c9fa31100 Mon Sep 17 00:00:00 2001 From: John R Barker Date: Wed, 19 Oct 2016 19:41:00 +0100 Subject: [PATCH 2242/2522] Run validate-modules from ansible/ansible (#3242) * Run validate-modules from ansible/ansible * Update sanity.sh --- test/utils/shippable/sanity.sh | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/test/utils/shippable/sanity.sh b/test/utils/shippable/sanity.sh index d9234cf0527..248f4d9777c 100755 --- a/test/utils/shippable/sanity.sh +++ b/test/utils/shippable/sanity.sh @@ -6,6 +6,14 @@ install_deps="${INSTALL_DEPS:-}" cd "${source_root}" +# FIXME REPOMERGE: No need to checkout ansible +build_dir=$(mktemp -d) +trap 'rm -rf "${build_dir}"' EXIT + +git clone "https://github.com/ansible/ansible.git" "${build_dir}" --recursive +source "${build_dir}/hacking/env-setup" +# REPOMERGE: END + if [ "${install_deps}" != "" ]; then add-apt-repository ppa:fkrull/deadsnakes && apt-get update -qq && apt-get install python2.4 -qq @@ -13,10 +21,14 @@ if [ "${install_deps}" != "" ]; then apt-get update -qq apt-get install shellcheck - pip install git+https://github.com/ansible/ansible.git@devel#egg=ansible - pip install git+https://github.com/sivel/ansible-testing.git#egg=ansible_testing + # Install dependencies for ansible and validate_modules + pip install -r "${build_dir}/test/utils/shippable/sanity-requirements.txt" --upgrade + pip list + fi +validate_modules="${build_dir}/test/sanity/validate-modules/validate-modules" + python2.4 -m compileall -fq -i "test/utils/shippable/sanity-test-python24.txt" python2.4 -m compileall -fq -x "($(printf %s "$(< "test/utils/shippable/sanity-skip-python24.txt"))" | tr '\n' '|')" . python2.6 -m compileall -fq . @@ -24,7 +36,7 @@ python2.7 -m compileall -fq . python3.5 -m compileall -fq . -x "($(printf %s "$(< "test/utils/shippable/sanity-skip-python3.txt"))" | tr '\n' '|')" ANSIBLE_DEPRECATION_WARNINGS=false \ - ansible-validate-modules --exclude '/utilities/|/shippable(/|$)' . + "${validate_modules}" --exclude '/utilities/|/shippable(/|$)' . shellcheck \ test/utils/shippable/*.sh From a14ba9b505da6fc55a2dc120fb59143f6a2341e2 Mon Sep 17 00:00:00 2001 From: John R Barker Date: Thu, 20 Oct 2016 18:41:13 +0100 Subject: [PATCH 2243/2522] Remove reference to ansible-validate-modules (#3256) ansible-validate-modules is now in ansible/ansible During 2.3 we will be merge the modules into ansible/ansible so this file will go away. The new testing documentation will refer to `ansible-test` which will wrap up the unit, integration, and ansible-validate-modules. So no need to document here. --- GUIDELINES.md | 1 - 1 file changed, 1 deletion(-) diff --git a/GUIDELINES.md b/GUIDELINES.md index 2589a730d46..0096f319295 100644 --- a/GUIDELINES.md +++ b/GUIDELINES.md @@ -64,7 +64,6 @@ If you'd like to step down as a maintainer, please submit a PR to the maintainer ## Useful tools * https://ansible.sivel.net/pr/byfile.html -- a full list of all open Pull Requests, organized by file. -* https://github.com/sivel/ansible-testing -- these are the tests that run on Shippable against all PRs for extras modules, so it's a good idea to run these tests locally first. ## Other Resources From bd53b5496171beb322d754f0dd55dfcfd7dcf251 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Wed, 19 Oct 2016 18:45:33 +0200 Subject: [PATCH 2244/2522] Make nmcli pass py3 sanity check Cleanup include, do not use '*' for future refactoring. Since nmcli is not present on EL5, we can safely use python 2.6 syntax only. --- network/nmcli.py | 8 ++++---- test/utils/shippable/sanity-skip-python24.txt | 1 + test/utils/shippable/sanity-skip-python3.txt | 1 - 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/network/nmcli.py b/network/nmcli.py index 5e729af7866..8078e9da4b1 100644 --- a/network/nmcli.py +++ b/network/nmcli.py @@ -395,6 +395,9 @@ except ImportError: pass +from ansible.module_utils.basic import AnsibleModule + + class Nmcli(object): """ This is the generic nmcli manipulation class that is subclassed based on platform. @@ -490,7 +493,7 @@ def merge_secrets(self, proxy, config, setting_name): for setting in secrets: for key in secrets[setting]: config[setting_name][key]=secrets[setting][key] - except Exception, e: + except Exception as e: pass def dict_to_string(self, d): @@ -1080,7 +1083,4 @@ def main(): module.exit_json(**result) -# import module snippets -from ansible.module_utils.basic import * - main() diff --git a/test/utils/shippable/sanity-skip-python24.txt b/test/utils/shippable/sanity-skip-python24.txt index 71c769c8b9b..cf392501c6f 100644 --- a/test/utils/shippable/sanity-skip-python24.txt +++ b/test/utils/shippable/sanity-skip-python24.txt @@ -13,3 +13,4 @@ /univention/ /web_infrastructure/letsencrypt.py /infrastructure/foreman/ +/network/nmcli.py diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt index 4831e17134a..314b5c1f704 100644 --- a/test/utils/shippable/sanity-skip-python3.txt +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -50,4 +50,3 @@ /network/cloudflare_dns.py /network/f5/bigip_gtm_virtual_server.py /network/f5/bigip_gtm_wide_ip.py -/network/nmcli.py From 89a8c18c6a08d6449ad2e72eb6b6bfc01875c7f3 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Wed, 19 Oct 2016 11:46:25 +0200 Subject: [PATCH 2245/2522] Make netscaler pass python3 sanity check --- network/citrix/netscaler.py | 4 +++- test/utils/shippable/sanity-skip-python3.txt | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/network/citrix/netscaler.py b/network/citrix/netscaler.py index 384a625bdca..7954db89b7a 100644 --- a/network/citrix/netscaler.py +++ b/network/citrix/netscaler.py @@ -173,7 +173,8 @@ def main(): rc = 0 try: rc, result = core(module) - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg=str(e)) if rc != 0: @@ -186,4 +187,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.urls import * +from ansible.module_utils.pycompat24 import get_exception main() diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt index 314b5c1f704..bce8225ce85 100644 --- a/test/utils/shippable/sanity-skip-python3.txt +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -46,7 +46,6 @@ /monitoring/datadog_monitor.py /monitoring/rollbar_deployment.py /monitoring/stackdriver.py -/network/citrix/netscaler.py /network/cloudflare_dns.py /network/f5/bigip_gtm_virtual_server.py /network/f5/bigip_gtm_wide_ip.py From 318993c8c276522cf4461529ae648017150919cf Mon Sep 17 00:00:00 2001 From: Jonathan Sokolowski Date: Sat, 22 Oct 2016 08:50:53 +1100 Subject: [PATCH 2246/2522] lvg: Initialise device list (#3141) --- system/lvg.py | 1 + 1 file changed, 1 insertion(+) diff --git a/system/lvg.py b/system/lvg.py index d22f3750b7a..2d2710e38bc 100644 --- a/system/lvg.py +++ b/system/lvg.py @@ -131,6 +131,7 @@ def main(): pesize = module.params['pesize'] vgoptions = module.params['vg_options'].split() + dev_list = [] if module.params['pvs']: dev_list = module.params['pvs'] elif state == 'present': From 53d9a62d086606248c374216c5f1ce53905ed012 Mon Sep 17 00:00:00 2001 From: Morgan Jones Date: Fri, 21 Oct 2016 23:20:45 +0100 Subject: [PATCH 2247/2522] azure_rm_deployment: fix rg issue with _nic_to_public_ips_instance() (#2950) Fix an issue with _nic_to_public_ips_instance() function. There was an assumption in the code that the Public IP sits in the same resource group, this isn't always the case. --- cloud/azure/azure_rm_deployment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/azure/azure_rm_deployment.py b/cloud/azure/azure_rm_deployment.py index b9986207ab6..35bdc4bd0a3 100644 --- a/cloud/azure/azure_rm_deployment.py +++ b/cloud/azure/azure_rm_deployment.py @@ -644,7 +644,7 @@ def _get_ip_dict(self, ip): return ip_dict def _nic_to_public_ips_instance(self, nics): - return [self.network_client.public_ip_addresses.get(self.resource_group_name, public_ip_id.split('/')[-1]) + return [self.network_client.public_ip_addresses.get(public_ip_id.split('/')[4], public_ip_id.split('/')[-1]) for nic_obj in [self.network_client.network_interfaces.get(self.resource_group_name, nic['dep'].resource_name) for nic in nics] for public_ip_id in [ip_conf_instance.public_ip_address.id From e29584ad70d2f4e0a8ef02781d7bc7e977ff0e69 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Fri, 21 Oct 2016 12:46:27 +0200 Subject: [PATCH 2248/2522] Refactor some code in timezone module --- system/timezone.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/system/timezone.py b/system/timezone.py index 2f04801790d..a6b74c570f5 100644 --- a/system/timezone.py +++ b/system/timezone.py @@ -218,6 +218,12 @@ def set(self, key, value): """ self.abort('set(key, value) is not implemented on target platform') + def _verify_timezone(self): + tz = self.value['name']['planned'] + tzfile = '/usr/share/zoneinfo/%s' % tz + if not os.path.isfile(tzfile): + self.abort('given timezone "%s" is not available' % tz) + class SystemdTimezone(Timezone): """This is a Timezone manipulation class systemd-powered Linux. @@ -241,10 +247,7 @@ def __init__(self, module): self.status = dict() # Validate given timezone if 'name' in self.value: - tz = self.value['name']['planned'] - tzfile = '/usr/share/zoneinfo/%s' % tz - if not os.path.isfile(tzfile): - self.abort('given timezone "%s" is not available' % tz) + self._verify_timezone() def _get_status(self, phase): if phase not in self.status: @@ -298,10 +301,7 @@ def __init__(self, module): super(NosystemdTimezone, self).__init__(module) # Validate given timezone if 'name' in self.value: - tz = self.value['name']['planned'] - tzfile = '/usr/share/zoneinfo/%s' % tz - if not os.path.isfile(tzfile): - self.abort('given timezone "%s" is not available' % tz) + self._verify_timezone() self.update_timezone = self.module.get_bin_path('cp', required=True) self.update_timezone += ' %s /etc/localtime' % tzfile self.update_hwclock = self.module.get_bin_path('hwclock', required=True) From 6d37020b1a41fbbe66da34e722e3f7cf1668e8c7 Mon Sep 17 00:00:00 2001 From: matt colton Date: Sat, 22 Oct 2016 02:37:21 -0500 Subject: [PATCH 2249/2522] sl_vm: update datacenter, cpu and memory options (#3236) --- cloud/softlayer/sl_vm.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/softlayer/sl_vm.py b/cloud/softlayer/sl_vm.py index d82b1da72d9..1866d1a57e3 100644 --- a/cloud/softlayer/sl_vm.py +++ b/cloud/softlayer/sl_vm.py @@ -205,9 +205,9 @@ #TODO: get this info from API STATES = ['present', 'absent'] -DATACENTERS = ['ams01','ams03','dal01','dal05','dal06','dal09','fra02','hkg02','hou02','lon02','mel01','mex01','mil01','mon01','par01','sjc01','sjc03','sao01','sea01','sng01','syd01','tok02','tor01','wdc01','wdc04'] -CPU_SIZES = [1,2,4,8,16] -MEMORY_SIZES = [1024,2048,4096,6144,8192,12288,16384,32768,49152,65536] +DATACENTERS = ['ams01','ams03','che01','dal01','dal05','dal06','dal09','dal10','fra02','hkg02','hou02','lon02','mel01','mex01','mil01','mon01','osl01','par01','sjc01','sjc03','sao01','sea01','sng01','syd01','tok02','tor01','wdc01','wdc04'] +CPU_SIZES = [1,2,4,8,16,32,56] +MEMORY_SIZES = [1024,2048,4096,6144,8192,12288,16384,32768,49152,65536,131072,247808] INITIALDISK_SIZES = [25,100] LOCALDISK_SIZES = [25,100,150,200,300] SANDISK_SIZES = [10,20,25,30,40,50,75,100,125,150,175,200,250,300,350,400,500,750,1000,1500,2000] From 33a41678d4ac061d036831244ce1c5db3566ba71 Mon Sep 17 00:00:00 2001 From: Nick Piper Date: Sat, 22 Oct 2016 11:54:13 +0100 Subject: [PATCH 2250/2522] doc: Correction for spelling of 'azure_rm_deployment' in example (#3212) --- cloud/azure/azure_rm_deployment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/azure/azure_rm_deployment.py b/cloud/azure/azure_rm_deployment.py index 35bdc4bd0a3..2fa8bf239c1 100644 --- a/cloud/azure/azure_rm_deployment.py +++ b/cloud/azure/azure_rm_deployment.py @@ -190,7 +190,7 @@ # Create or update a template deployment based on an inline template and parameters - name: Create Azure Deploy - azure_rm_deploy: + azure_rm_deployment: state: present subscription_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx resource_group_name: dev-ops-cle From 0df53bbbed90d81d0e1f51ec93668ea30721aee9 Mon Sep 17 00:00:00 2001 From: Jeff Date: Sat, 22 Oct 2016 04:20:37 -0700 Subject: [PATCH 2251/2522] logicmonitor: Documentation and logging updates based on feedback (#2756) --- monitoring/logicmonitor.py | 75 +++++++++++++++++--------------- monitoring/logicmonitor_facts.py | 28 ++++++------ 2 files changed, 55 insertions(+), 48 deletions(-) diff --git a/monitoring/logicmonitor.py b/monitoring/logicmonitor.py index 8d35f3bfbb3..3f2267cfd76 100644 --- a/monitoring/logicmonitor.py +++ b/monitoring/logicmonitor.py @@ -73,7 +73,7 @@ - LogicMonitor is a hosted, full-stack, infrastructure monitoring platform. - This module manages hosts, host groups, and collectors within your LogicMonitor account. version_added: "2.2" -author: Ethan Culler-Mayeno, Jeff Wozniak +author: [Ethan Culler-Mayeno (@ethanculler), Jeff Wozniak (@woz5999)] notes: - You must have an existing LogicMonitor account for this module to function. requirements: ["An existing LogicMonitor account", "Linux"] @@ -81,32 +81,32 @@ target: description: - The type of LogicMonitor object you wish to manage. - - "Collector: Perform actions on a LogicMonitor collector" - - NOTE You should use Ansible service modules such as 'service' or 'supervisorctl' for managing the Collector 'logicmonitor-agent' and 'logicmonitor-watchdog' services. Specifically, you'll probably want to start these services after a Collector add and stop these services before a Collector remove. - - "Host: Perform actions on a host device" - - "Hostgroup: Perform actions on a LogicMonitor host group" + - "Collector: Perform actions on a LogicMonitor collector." + - NOTE You should use Ansible service modules such as M(service) or M(supervisorctl) for managing the Collector 'logicmonitor-agent' and 'logicmonitor-watchdog' services. Specifically, you'll probably want to start these services after a Collector add and stop these services before a Collector remove. + - "Host: Perform actions on a host device." + - "Hostgroup: Perform actions on a LogicMonitor host group." - NOTE Host and Hostgroup tasks should always be performed via local_action. There are no benefits to running these tasks on the remote host and doing so will typically cause problems. required: true default: null choices: ['collector', 'host', 'datsource', 'hostgroup'] action: description: - - The action you wish to perform on target - - "Add: Add an object to your LogicMonitor account" - - "Remove: Remove an object from your LogicMonitor account" - - "Update: Update properties, description, or groups (target=host) for an object in your LogicMonitor account" - - "SDT: Schedule downtime for an object in your LogicMonitor account" + - The action you wish to perform on target. + - "Add: Add an object to your LogicMonitor account." + - "Remove: Remove an object from your LogicMonitor account." + - "Update: Update properties, description, or groups (target=host) for an object in your LogicMonitor account." + - "SDT: Schedule downtime for an object in your LogicMonitor account." required: true default: null choices: ['add', 'remove', 'update', 'sdt'] company: description: - - The LogicMonitor account company name. If you would log in to your account at "superheroes.logicmonitor.com" you would use "superheroes" + - The LogicMonitor account company name. If you would log in to your account at "superheroes.logicmonitor.com" you would use "superheroes." required: true default: null user: description: - - A LogicMonitor user name. The module will authenticate and perform actions on behalf of this user + - A LogicMonitor user name. The module will authenticate and perform actions on behalf of this user. required: true default: null password: @@ -117,72 +117,72 @@ collector: description: - The fully qualified domain name of a collector in your LogicMonitor account. - - This is required for the creation of a LogicMonitor host (target=host action=add) - - This is required for updating, removing or scheduling downtime for hosts if 'displayname' isn't specified (target=host action=update action=remove action=sdt) + - This is required for the creation of a LogicMonitor host (target=host action=add). + - This is required for updating, removing or scheduling downtime for hosts if 'displayname' isn't specified (target=host action=update action=remove action=sdt). required: false default: null hostname: description: - The hostname of a host in your LogicMonitor account, or the desired hostname of a device to manage. - - Optional for managing hosts (target=host) + - Optional for managing hosts (target=host). required: false default: 'hostname -f' displayname: description: - The display name of a host in your LogicMonitor account or the desired display name of a device to manage. - - Optional for managing hosts (target=host) + - Optional for managing hosts (target=host). required: false default: 'hostname -f' description: description: - - The long text description of the object in your LogicMonitor account - - Optional for managing hosts and host groups (target=host or target=hostgroup; action=add or action=update) + - The long text description of the object in your LogicMonitor account. + - Optional for managing hosts and host groups (target=host or target=hostgroup; action=add or action=update). required: false default: "" properties: description: - A dictionary of properties to set on the LogicMonitor host or host group. - - Optional for managing hosts and host groups (target=host or target=hostgroup; action=add or action=update) - - This parameter will add or update existing properties in your LogicMonitor account or + - Optional for managing hosts and host groups (target=host or target=hostgroup; action=add or action=update). + - This parameter will add or update existing properties in your LogicMonitor account. required: false default: {} groups: description: - A list of groups that the host should be a member of. - - Optional for managing hosts (target=host; action=add or action=update) + - Optional for managing hosts (target=host; action=add or action=update). required: false default: [] id: description: - - ID of the datasource to target - - Required for management of LogicMonitor datasources (target=datasource) + - ID of the datasource to target. + - Required for management of LogicMonitor datasources (target=datasource). required: false default: null fullpath: description: - - The fullpath of the host group object you would like to manage - - Recommend running on a single Ansible host - - Required for management of LogicMonitor host groups (target=hostgroup) + - The fullpath of the host group object you would like to manage. + - Recommend running on a single Ansible host. + - Required for management of LogicMonitor host groups (target=hostgroup). required: false default: null alertenable: description: - - A boolean flag to turn alerting on or off for an object - - Optional for managing all hosts (action=add or action=update) + - A boolean flag to turn alerting on or off for an object. + - Optional for managing all hosts (action=add or action=update). required: false default: true choices: [true, false] starttime: description: - - The time that the Scheduled Down Time (SDT) should begin - - Optional for managing SDT (action=sdt) + - The time that the Scheduled Down Time (SDT) should begin. + - Optional for managing SDT (action=sdt). - Y-m-d H:M required: false default: Now duration: description: - - The duration (minutes) of the Scheduled Down Time (SDT) - - Optional for putting an object into SDT (action=sdt) + - The duration (minutes) of the Scheduled Down Time (SDT). + - Optional for putting an object into SDT (action=sdt). required: false default: 30 ... @@ -586,7 +586,10 @@ def rpc(self, action, params): else: return raw except IOError: - self.fail(msg="Error: Unknown exception making RPC call") + ioe = get_exception() + self.fail(msg="Error: Exception making RPC call to " + + "https://" + self.company + "." + self.lm_url + + "/rpc/" + action + "\nException" + str(ioe)) def do(self, action, params): """Make a call to the LogicMonitor @@ -612,8 +615,10 @@ def do(self, action, params): "/do/" + action + "?" + param_str) return f.read() except IOError: - # self.module.debug("Error opening URL. " + ioe) - self.fail("Unknown exception opening URL") + ioe = get_exception() + self.fail(msg="Error: Exception making RPC call to " + + "https://" + self.company + "." + self.lm_url + + "/do/" + action + "\nException" + str(ioe)) def get_collectors(self): """Returns a JSON object containing a list of diff --git a/monitoring/logicmonitor_facts.py b/monitoring/logicmonitor_facts.py index cc91ca6122c..b8f2aff7739 100644 --- a/monitoring/logicmonitor_facts.py +++ b/monitoring/logicmonitor_facts.py @@ -19,7 +19,6 @@ import socket -import sys import types import urllib @@ -61,7 +60,7 @@ - LogicMonitor is a hosted, full-stack, infrastructure monitoring platform. - This module collects facts about hosts abd host groups within your LogicMonitor account. version_added: "2.2" -author: Ethan Culler-Mayeno, Jeff Wozniak +author: [Ethan Culler-Mayeno (@ethanculler), Jeff Wozniak (@woz5999)] notes: - You must have an existing LogicMonitor account for this module to function. requirements: ["An existing LogicMonitor account", "Linux"] @@ -74,31 +73,31 @@ choices: ['host', 'hostgroup'] company: description: - - The LogicMonitor account company name. If you would log in to your account at "superheroes.logicmonitor.com" you would use "superheroes" + - The LogicMonitor account company name. If you would log in to your account at "superheroes.logicmonitor.com" you would use "superheroes". required: true default: null user: description: - - A LogicMonitor user name. The module will authenticate and perform actions on behalf of this user + - A LogicMonitor user name. The module will authenticate and perform actions on behalf of this user. required: true default: null password: description: - - The password for the chosen LogicMonitor User - - If an md5 hash is used, the digest flag must be set to true + - The password for the chosen LogicMonitor User. + - If an md5 hash is used, the digest flag must be set to true. required: true default: null collector: description: - The fully qualified domain name of a collector in your LogicMonitor account. - - This is optional for querying a LogicMonitor host when a displayname is specified - - This is required for querying a LogicMonitor host when a displayname is not specified + - This is optional for querying a LogicMonitor host when a displayname is specified. + - This is required for querying a LogicMonitor host when a displayname is not specified. required: false default: null hostname: description: - The hostname of a host in your LogicMonitor account, or the desired hostname of a device to add into monitoring. - - Required for managing hosts (target=host) + - Required for managing hosts (target=host). required: false default: 'hostname -f' displayname: @@ -108,9 +107,9 @@ default: 'hostname -f' fullpath: description: - - The fullpath of the hostgroup object you would like to manage - - Recommend running on a single ansible host - - Required for management of LogicMonitor host groups (target=hostgroup) + - The fullpath of the hostgroup object you would like to manage. + - Recommend running on a single ansible host. + - Required for management of LogicMonitor host groups (target=hostgroup). required: false default: null ... @@ -236,7 +235,10 @@ def rpc(self, action, params): else: return raw except IOError: - self.fail(msg="Error: Unknown exception making RPC call") + ioe = get_exception() + self.fail(msg="Error: Exception making RPC call to " + + "https://" + self.company + "." + self.lm_url + + "/rpc/" + action + "\nException" + str(ioe)) def get_collectors(self): """Returns a JSON object containing a list of From da2d56191313798034b49785f18c0fdc1d573029 Mon Sep 17 00:00:00 2001 From: James Kassemi Date: Sat, 22 Oct 2016 10:46:39 -0600 Subject: [PATCH 2252/2522] Fix issue with multiple pages of results in ec2_lc_find (#3090) --- cloud/amazon/ec2_lc_find.py | 43 +++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/cloud/amazon/ec2_lc_find.py b/cloud/amazon/ec2_lc_find.py index 32e0d0eb3a8..f18bdfb42be 100644 --- a/cloud/amazon/ec2_lc_find.py +++ b/cloud/amazon/ec2_lc_find.py @@ -162,31 +162,32 @@ def find_launch_configs(client, module): } ) + results = [] + for response in response_iterator: response['LaunchConfigurations'] = filter(lambda lc: re.compile(name_regex).match(lc['LaunchConfigurationName']), response['LaunchConfigurations']) - results = [] - for lc in response['LaunchConfigurations']: - data = { - 'name': lc['LaunchConfigurationName'], - 'arn': lc['LaunchConfigurationARN'], - 'created_time': lc['CreatedTime'], - 'user_data': lc['UserData'], - 'instance_type': lc['InstanceType'], - 'image_id': lc['ImageId'], - 'ebs_optimized': lc['EbsOptimized'], - 'instance_monitoring': lc['InstanceMonitoring'], - 'classic_link_vpc_security_groups': lc['ClassicLinkVPCSecurityGroups'], - 'block_device_mappings': lc['BlockDeviceMappings'], - 'keyname': lc['KeyName'], - 'security_groups': lc['SecurityGroups'], - 'kernel_id': lc['KernelId'], - 'ram_disk_id': lc['RamdiskId'], - 'associate_public_address': lc['AssociatePublicIpAddress'], - } - - results.append(data) + for lc in response['LaunchConfigurations']: + data = { + 'name': lc['LaunchConfigurationName'], + 'arn': lc['LaunchConfigurationARN'], + 'created_time': lc['CreatedTime'], + 'user_data': lc['UserData'], + 'instance_type': lc['InstanceType'], + 'image_id': lc['ImageId'], + 'ebs_optimized': lc['EbsOptimized'], + 'instance_monitoring': lc['InstanceMonitoring'], + 'classic_link_vpc_security_groups': lc['ClassicLinkVPCSecurityGroups'], + 'block_device_mappings': lc['BlockDeviceMappings'], + 'keyname': lc['KeyName'], + 'security_groups': lc['SecurityGroups'], + 'kernel_id': lc['KernelId'], + 'ram_disk_id': lc['RamdiskId'], + 'associate_public_address': lc['AssociatePublicIpAddress'], + } + + results.append(data) results.sort(key=lambda e: e['name'], reverse=(sort_order == 'descending')) From 5b7e052d06622dd10fff7129a3d082a162b28887 Mon Sep 17 00:00:00 2001 From: Steyn Huizinga Date: Sat, 22 Oct 2016 18:49:55 +0200 Subject: [PATCH 2253/2522] Fix for ansible/ansible-modules-extras#3173 (#3203) --- cloud/amazon/lambda.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/lambda.py b/cloud/amazon/lambda.py index 650022e5bcd..993effce025 100644 --- a/cloud/amazon/lambda.py +++ b/cloud/amazon/lambda.py @@ -289,7 +289,7 @@ def main(): current_version = None # Update function configuration - func_kwargs = {'FunctionName': name, 'Publish': True} + func_kwargs = {'FunctionName': name} # Update configuration if needed if role_arn and current_config['Role'] != role_arn: From 1e338aa0e68c30ff68b95bfbb27230923a4d0931 Mon Sep 17 00:00:00 2001 From: Alexandre Garnier Date: Tue, 23 Aug 2016 15:03:27 +0200 Subject: [PATCH 2254/2522] Really fix python 2.4 compatibility PR #1544 didn't remove the keyword argument 'delete' not existing in `tempfile.NamedTemporaryFile()` in python 2.4 --- system/pam_limits.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/pam_limits.py b/system/pam_limits.py index 8e6bdbe9695..bd129c5817f 100644 --- a/system/pam_limits.py +++ b/system/pam_limits.py @@ -154,7 +154,7 @@ def main(): message = '' f = open (limits_conf, 'r') # Tempfile - nf = tempfile.NamedTemporaryFile(delete = False) + nf = tempfile.NamedTemporaryFile() found = False new_value = value From 65ba526c9f9a37d28d7842b9912dd6deb7c19c6b Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sun, 23 Oct 2016 18:45:51 +0200 Subject: [PATCH 2255/2522] Fix pkgng on python 3 Fail with: Traceback (most recent call last): File \"/tmp/ansible_2rsl4fg1/ansible_module_pkgng.py\", line 353, in main() File \"/tmp/ansible_2rsl4fg1/ansible_module_pkgng.py\", line 330, in main _changed, _msg = install_packages(module, pkgng_path, pkgs, p[\"cached\"], p[\"pkgsite\"], dir_arg) File \"/tmp/ansible_2rsl4fg1/ansible_module_pkgng.py\", line 161, in install_packages old_pkgng = pkgng_older_than(module, pkgng_path, [1, 1, 4]) File \"/tmp/ansible_2rsl4fg1/ansible_module_pkgng.py\", line 121, in pkgng_older_than while compare_version[i] == version[i]: TypeError: 'map' object is not subscriptable --- packaging/os/pkgng.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/os/pkgng.py b/packaging/os/pkgng.py index 5583bb18ee5..df1ceb37e80 100644 --- a/packaging/os/pkgng.py +++ b/packaging/os/pkgng.py @@ -114,7 +114,7 @@ def query_package(module, pkgng_path, name, dir_arg): def pkgng_older_than(module, pkgng_path, compare_version): rc, out, err = module.run_command("%s -v" % pkgng_path) - version = map(lambda x: int(x), re.split(r'[\._]', out)) + version = [int(x) for x in re.split(r'[\._]', out)] i = 0 new_pkgng = True From 7ce17ab49ef232af2064d2a680dc6ca78c2211f5 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Sun, 23 Oct 2016 12:36:17 -0700 Subject: [PATCH 2256/2522] Fix remaining centurylink modules for py3 Also update the imports to take advantage of the Ansiballz imports --- cloud/centurylink/clc_aa_policy.py | 9 ++++++--- cloud/centurylink/clc_group.py | 9 ++++++--- cloud/centurylink/clc_publicip.py | 9 ++++++--- test/utils/shippable/sanity-skip-python3.txt | 3 --- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/cloud/centurylink/clc_aa_policy.py b/cloud/centurylink/clc_aa_policy.py index 681e71cb3a1..5ba0ac35b8f 100644 --- a/cloud/centurylink/clc_aa_policy.py +++ b/cloud/centurylink/clc_aa_policy.py @@ -131,6 +131,8 @@ __version__ = '${version}' +import os + from distutils.version import LooseVersion try: @@ -153,6 +155,8 @@ else: CLC_FOUND = True +from ansible.module_utils.basic import AnsibleModule + class ClcAntiAffinityPolicy: @@ -267,7 +271,7 @@ def _create_policy(self, p): return self.clc.v2.AntiAffinity.Create( name=p['name'], location=p['location']) - except CLCException, ex: + except CLCException as ex: self.module.fail_json(msg='Failed to create anti affinity policy : {0}. {1}'.format( p['name'], ex.response_text )) @@ -281,7 +285,7 @@ def _delete_policy(self, p): try: policy = self.policy_dict[p['name']] policy.Delete() - except CLCException, ex: + except CLCException as ex: self.module.fail_json(msg='Failed to delete anti affinity policy : {0}. {1}'.format( p['name'], ex.response_text )) @@ -346,6 +350,5 @@ def main(): clc_aa_policy = ClcAntiAffinityPolicy(module) clc_aa_policy.process_request() -from ansible.module_utils.basic import * # pylint: disable=W0614 if __name__ == '__main__': main() diff --git a/cloud/centurylink/clc_group.py b/cloud/centurylink/clc_group.py index f30b37d6ec1..661196b79b3 100644 --- a/cloud/centurylink/clc_group.py +++ b/cloud/centurylink/clc_group.py @@ -212,6 +212,7 @@ __version__ = '${version}' +import os from distutils.version import LooseVersion try: @@ -234,6 +235,8 @@ else: CLC_FOUND = True +from ansible.module_utils.basic import AnsibleModule + class ClcGroup(object): @@ -362,7 +365,7 @@ def _delete_group(self, group_name): group, parent = self.group_dict.get(group_name) try: response = group.Delete() - except CLCException, ex: + except CLCException as ex: self.module.fail_json(msg='Failed to delete group :{0}. {1}'.format( group_name, ex.response_text )) @@ -423,7 +426,7 @@ def _create_group(self, group, parent, description): (parent, grandparent) = self.group_dict[parent] try: response = parent.Create(name=group, description=description) - except CLCException, ex: + except CLCException as ex: self.module.fail_json(msg='Failed to create group :{0}. {1}'.format( group, ex.response_text)) return response @@ -508,6 +511,6 @@ def main(): clc_group = ClcGroup(module) clc_group.process_request() -from ansible.module_utils.basic import * # pylint: disable=W0614 + if __name__ == '__main__': main() diff --git a/cloud/centurylink/clc_publicip.py b/cloud/centurylink/clc_publicip.py index 9c21a9a6156..98e2e15dbe3 100644 --- a/cloud/centurylink/clc_publicip.py +++ b/cloud/centurylink/clc_publicip.py @@ -124,6 +124,7 @@ __version__ = '${version}' +import os from distutils.version import LooseVersion try: @@ -146,6 +147,8 @@ else: CLC_FOUND = True +from ansible.module_utils.basic import AnsibleModule + class ClcPublicIp(object): clc = clc_sdk @@ -241,7 +244,7 @@ def _add_publicip_to_server(self, server, ports_to_expose): result = None try: result = server.PublicIPs().Add(ports_to_expose) - except CLCException, ex: + except CLCException as ex: self.module.fail_json(msg='Failed to add public ip to the server : {0}. {1}'.format( server.id, ex.response_text )) @@ -278,7 +281,7 @@ def _remove_publicip_from_server(self, server): try: for ip_address in server.PublicIPs().public_ips: result = ip_address.Delete() - except CLCException, ex: + except CLCException as ex: self.module.fail_json(msg='Failed to remove public ip from the server : {0}. {1}'.format( server.id, ex.response_text )) @@ -358,6 +361,6 @@ def main(): clc_public_ip = ClcPublicIp(module) clc_public_ip.process_request() -from ansible.module_utils.basic import * # pylint: disable=W0614 + if __name__ == '__main__': main() diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt index bce8225ce85..9fba92376f2 100644 --- a/test/utils/shippable/sanity-skip-python3.txt +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -28,9 +28,6 @@ /cloud/amazon/sqs_queue.py /cloud/amazon/sts_assume_role.py /cloud/amazon/sts_session_token.py -/cloud/centurylink/clc_aa_policy.py -/cloud/centurylink/clc_group.py -/cloud/centurylink/clc_publicip.py /cloud/misc/virt_net.py /cloud/misc/virt_pool.py /cloud/profitbricks/profitbricks.py From 79c7997952ec790f38caf027078bbcc294f928d1 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Sun, 23 Oct 2016 13:41:03 -0700 Subject: [PATCH 2257/2522] Fix amazon extras modules to compile under python3 --- cloud/amazon/cloudtrail.py | 12 ++++---- cloud/amazon/dynamodb_table.py | 16 ++++++----- cloud/amazon/ec2_ami_copy.py | 20 +++++++------ cloud/amazon/ec2_customer_gateway.py | 12 ++++---- cloud/amazon/ec2_eni.py | 10 +++++-- cloud/amazon/ec2_eni_facts.py | 10 +++++-- cloud/amazon/ec2_remote_facts.py | 9 +++--- cloud/amazon/ec2_snapshot_facts.py | 9 ++++-- cloud/amazon/ec2_vol_facts.py | 8 ++++-- cloud/amazon/ec2_vpc_igw.py | 9 +++--- cloud/amazon/ec2_vpc_nacl.py | 14 ++++----- cloud/amazon/ec2_vpc_net_facts.py | 8 ++++-- cloud/amazon/ec2_vpc_route_table.py | 11 ++++--- cloud/amazon/ec2_vpc_route_table_facts.py | 8 ++++-- cloud/amazon/ec2_vpc_subnet.py | 8 +++--- cloud/amazon/ec2_vpc_subnet_facts.py | 8 ++++-- cloud/amazon/ecs_cluster.py | 13 +++++---- cloud/amazon/ecs_service.py | 17 ++++++----- cloud/amazon/ecs_service_facts.py | 12 ++++---- cloud/amazon/ecs_task.py | 11 +++---- cloud/amazon/ecs_taskdefinition.py | 11 +++---- cloud/amazon/route53_facts.py | 10 +++---- cloud/amazon/route53_health_check.py | 14 +++++---- cloud/amazon/route53_zone.py | 13 ++++----- cloud/amazon/s3_lifecycle.py | 23 ++++++++------- cloud/amazon/s3_logging.py | 7 +++-- cloud/amazon/sns_topic.py | 13 ++++----- cloud/amazon/sqs_queue.py | 12 ++++---- cloud/amazon/sts_assume_role.py | 13 ++++----- cloud/amazon/sts_session_token.py | 11 +++---- test/utils/shippable/sanity-skip-python3.txt | 30 -------------------- 31 files changed, 192 insertions(+), 190 deletions(-) diff --git a/cloud/amazon/cloudtrail.py b/cloud/amazon/cloudtrail.py index 557f2ebaae3..f0ca3239117 100644 --- a/cloud/amazon/cloudtrail.py +++ b/cloud/amazon/cloudtrail.py @@ -101,6 +101,10 @@ except ImportError: HAS_BOTO = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import connect_to_aws, ec2_argument_spec, get_ec2_creds + + class CloudTrailManager: """Handles cloudtrail configuration""" @@ -112,7 +116,7 @@ def __init__(self, module, region=None, **aws_connect_params): try: self.conn = connect_to_aws(boto.cloudtrail, self.region, **self.aws_connect_params) - except boto.exception.NoAuthHandlerFound, e: + except boto.exception.NoAuthHandlerFound as e: self.module.fail_json(msg=str(e)) def view_status(self, name): @@ -222,8 +226,6 @@ def main(): module.exit_json(**results) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/amazon/dynamodb_table.py b/cloud/amazon/dynamodb_table.py index ceafbdea9b6..3799c0ffe35 100644 --- a/cloud/amazon/dynamodb_table.py +++ b/cloud/amazon/dynamodb_table.py @@ -133,6 +133,8 @@ sample: ACTIVE ''' +import traceback + try: import boto import boto.dynamodb2 @@ -152,6 +154,10 @@ except ImportError: HAS_BOTO = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import AnsibleAWSError, connect_to_aws, ec2_argument_spec, get_aws_connection_info + + DYNAMO_TYPE_DEFAULT = 'STRING' INDEX_REQUIRED_OPTIONS = ['name', 'type', 'hash_key_name'] INDEX_OPTIONS = INDEX_REQUIRED_OPTIONS + ['hash_key_type', 'range_key_name', 'range_key_type', 'includes', 'read_capacity', 'write_capacity'] @@ -244,7 +250,7 @@ def dynamo_table_exists(table): table.describe() return True - except JSONResponseError, e: + except JSONResponseError as e: if e.message and e.message.startswith('Requested resource not found'): return False else: @@ -281,7 +287,7 @@ def update_dynamo_table(table, throughput=None, check_mode=False, global_indexes # todo: remove try once boto has https://github.com/boto/boto/pull/3447 fixed try: global_indexes_changed = table.update_global_secondary_index(global_indexes=index_throughput_changes) or global_indexes_changed - except ValidationException as e: + except ValidationException: pass else: global_indexes_changed = True @@ -398,7 +404,7 @@ def main(): try: connection = connect_to_aws(boto.dynamodb2, region, **aws_connect_params) - except (NoAuthHandlerFound, AnsibleAWSError), e: + except (NoAuthHandlerFound, AnsibleAWSError) as e: module.fail_json(msg=str(e)) state = module.params.get('state') @@ -408,9 +414,5 @@ def main(): delete_dynamo_table(connection, module) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * - if __name__ == '__main__': main() diff --git a/cloud/amazon/ec2_ami_copy.py b/cloud/amazon/ec2_ami_copy.py index 88eba110899..c053eb31aaf 100644 --- a/cloud/amazon/ec2_ami_copy.py +++ b/cloud/amazon/ec2_ami_copy.py @@ -124,6 +124,8 @@ kms_key_id: arn:aws:kms:us-east-1:XXXXXXXXXXXX:key/746de6ea-50a4-4bcb-8fbc-e3b29f2d367b ''' +import time + try: import boto import boto.ec2 @@ -131,6 +133,9 @@ except ImportError: HAS_BOTO = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import ec2_argument_spec, ec2_connect, get_aws_connection_info + def copy_image(module, ec2): """ @@ -160,7 +165,7 @@ def copy_image(module, ec2): } image_id = ec2.copy_image(**params).image_id - except boto.exception.BotoServerError, e: + except boto.exception.BotoServerError as e: module.fail_json(msg="%s: %s" % (e.error_code, e.error_message)) img = wait_until_image_is_recognized(module, ec2, wait_timeout, image_id, wait) @@ -198,7 +203,7 @@ def wait_until_image_is_recognized(module, ec2, wait_timeout, image_id, wait): for i in range(wait_timeout): try: return ec2.get_image(image_id) - except boto.exception.EC2ResponseError, e: + except boto.exception.EC2ResponseError as e: # This exception we expect initially right after registering the copy with EC2 API if 'InvalidAMIID.NotFound' in e.error_code and wait: time.sleep(1) @@ -231,12 +236,12 @@ def main(): try: ec2 = ec2_connect(module) - except boto.exception.NoAuthHandlerFound, e: + except boto.exception.NoAuthHandlerFound as e: module.fail_json(msg=str(e)) try: region, ec2_url, boto_params = get_aws_connection_info(module) - except boto.exception.NoAuthHandlerFound, e: + except boto.exception.NoAuthHandlerFound as e: module.fail_json(msg=str(e)) if not region: @@ -245,9 +250,6 @@ def main(): copy_image(module, ec2) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * - -main() +if __name__ == '__main__': + main() diff --git a/cloud/amazon/ec2_customer_gateway.py b/cloud/amazon/ec2_customer_gateway.py index 4e02523a700..64a77bb08bf 100644 --- a/cloud/amazon/ec2_customer_gateway.py +++ b/cloud/amazon/ec2_customer_gateway.py @@ -120,6 +120,11 @@ except ImportError: HAS_BOTO3 = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import (boto3_conn, camel_dict_to_snake_dict, + ec2_argument_spec, get_aws_connection_info) + + class Ec2CustomerGatewayManager: def __init__(self, module): @@ -130,7 +135,7 @@ def __init__(self, module): if not region: module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") self.ec2 = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except ClientError, e: + except ClientError as e: module.fail_json(msg=e.message) def ensure_cgw_absent(self, gw_id): @@ -211,8 +216,6 @@ def main(): gw_mgr = Ec2CustomerGatewayManager(module) - bgp_asn = module.params.get('bgp_asn') - ip_address = module.params.get('ip_address') name = module.params.get('name') existing = gw_mgr.describe_gateways(module.params['ip_address']) @@ -259,9 +262,6 @@ def main(): pretty_results = camel_dict_to_snake_dict(results) module.exit_json(**pretty_results) -# import module methods -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * if __name__ == '__main__': main() diff --git a/cloud/amazon/ec2_eni.py b/cloud/amazon/ec2_eni.py index 79d44f9d46c..6946ec6db20 100644 --- a/cloud/amazon/ec2_eni.py +++ b/cloud/amazon/ec2_eni.py @@ -241,6 +241,12 @@ except ImportError: HAS_BOTO = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import (AnsibleAWSError, connect_to_aws, + ec2_argument_spec, get_aws_connection_info, + get_ec2_security_group_ids_from_names) + + def get_eni_info(interface): # Private addresses @@ -539,7 +545,7 @@ def main(): try: connection = connect_to_aws(boto.ec2, region, **aws_connect_params) vpc_connection = connect_to_aws(boto.vpc, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: + except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: module.fail_json(msg=str(e)) else: module.fail_json(msg="region must be specified") @@ -561,8 +567,6 @@ def main(): elif state == 'absent': delete_eni(connection, module) -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * if __name__ == '__main__': main() diff --git a/cloud/amazon/ec2_eni_facts.py b/cloud/amazon/ec2_eni_facts.py index 8b385dabc83..116c8c744ec 100644 --- a/cloud/amazon/ec2_eni_facts.py +++ b/cloud/amazon/ec2_eni_facts.py @@ -60,6 +60,12 @@ except ImportError: HAS_BOTO3 = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import (AnsibleAWSError, + ansible_dict_to_boto3_filter_list, boto3_conn, + boto3_tag_list_to_ansible_dict, camel_dict_to_snake_dict, + connect_to_aws, ec2_argument_spec, get_aws_connection_info) + def list_ec2_snapshots_boto3(connection, module): @@ -163,15 +169,13 @@ def main(): if region: try: connection = connect_to_aws(boto.ec2, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: + except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: module.fail_json(msg=str(e)) else: module.fail_json(msg="region must be specified") list_eni(connection, module) -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * if __name__ == '__main__': main() diff --git a/cloud/amazon/ec2_remote_facts.py b/cloud/amazon/ec2_remote_facts.py index 5b3f9099765..cb80068a7cc 100644 --- a/cloud/amazon/ec2_remote_facts.py +++ b/cloud/amazon/ec2_remote_facts.py @@ -65,6 +65,10 @@ except ImportError: HAS_BOTO = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import AnsibleAWSError, connect_to_aws, ec2_argument_spec, get_aws_connection_info + + def get_instance_info(instance): # Get groups @@ -171,16 +175,13 @@ def main(): if region: try: connection = connect_to_aws(boto.ec2, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: + except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: module.fail_json(msg=str(e)) else: module.fail_json(msg="region must be specified") list_ec2_instances(connection, module) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * if __name__ == '__main__': main() diff --git a/cloud/amazon/ec2_snapshot_facts.py b/cloud/amazon/ec2_snapshot_facts.py index 9904eb8591d..357a1692d89 100644 --- a/cloud/amazon/ec2_snapshot_facts.py +++ b/cloud/amazon/ec2_snapshot_facts.py @@ -163,6 +163,11 @@ except ImportError: HAS_BOTO3 = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import (ansible_dict_to_boto3_filter_list, + boto3_conn, boto3_tag_list_to_ansible_dict, camel_dict_to_snake_dict, + ec2_argument_spec, get_aws_connection_info) + def list_ec2_snapshots(connection, module): @@ -173,7 +178,7 @@ def list_ec2_snapshots(connection, module): try: snapshots = connection.describe_snapshots(SnapshotIds=snapshot_ids, OwnerIds=owner_ids, RestorableByUserIds=restorable_by_user_ids, Filters=filters) - except ClientError, e: + except ClientError as e: module.fail_json(msg=e.message) # Turn the boto3 result in to ansible_friendly_snaked_names @@ -219,8 +224,6 @@ def main(): list_ec2_snapshots(connection, module) -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * if __name__ == '__main__': main() diff --git a/cloud/amazon/ec2_vol_facts.py b/cloud/amazon/ec2_vol_facts.py index e053a772d73..5d099d0fb8d 100644 --- a/cloud/amazon/ec2_vol_facts.py +++ b/cloud/amazon/ec2_vol_facts.py @@ -66,6 +66,10 @@ except ImportError: HAS_BOTO = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import connect_to_aws, ec2_argument_spec, get_aws_connection_info + + def get_volume_info(volume): attachment = volume.attach_data @@ -125,15 +129,13 @@ def main(): if region: try: connection = connect_to_aws(boto.ec2, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, StandardError), e: + except (boto.exception.NoAuthHandlerFound, StandardError) as e: module.fail_json(msg=str(e)) else: module.fail_json(msg="region must be specified") list_ec2_volumes(connection, module) -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * if __name__ == '__main__': main() diff --git a/cloud/amazon/ec2_vpc_igw.py b/cloud/amazon/ec2_vpc_igw.py index a4e58faac8b..fc057bae4b9 100644 --- a/cloud/amazon/ec2_vpc_igw.py +++ b/cloud/amazon/ec2_vpc_igw.py @@ -50,8 +50,6 @@ ''' -import sys # noqa - try: import boto.ec2 import boto.vpc @@ -62,6 +60,9 @@ if __name__ != '__main__': raise +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import AnsibleAWSError, connect_to_aws, ec2_argument_spec, get_aws_connection_info + class AnsibleIGWException(Exception): pass @@ -134,7 +135,7 @@ def main(): if region: try: connection = connect_to_aws(boto.vpc, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: + except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: module.fail_json(msg=str(e)) else: module.fail_json(msg="region must be specified") @@ -152,8 +153,6 @@ def main(): module.exit_json(**result) -from ansible.module_utils.basic import * # noqa -from ansible.module_utils.ec2 import * # noqa if __name__ == '__main__': main() diff --git a/cloud/amazon/ec2_vpc_nacl.py b/cloud/amazon/ec2_vpc_nacl.py index 73eafbc8482..b3d4f197214 100644 --- a/cloud/amazon/ec2_vpc_nacl.py +++ b/cloud/amazon/ec2_vpc_nacl.py @@ -122,13 +122,15 @@ ''' try: - import json import botocore import boto3 HAS_BOTO3 = True except ImportError: HAS_BOTO3 = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info + # Common fields for the default rule that is contained within every VPC NACL. DEFAULT_RULE_FIELDS = { @@ -179,7 +181,6 @@ def subnets_added(nacl_id, subnets, client, module): def subnets_changed(nacl, client, module): changed = False - response = {} vpc_id = module.params.get('vpc_id') nacl_id = nacl['NetworkAcls'][0]['NetworkAclId'] subnets = subnets_to_associate(nacl, client, module) @@ -528,9 +529,9 @@ def main(): try: region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) client = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except botocore.exceptions.NoCredentialsError, e: - module.fail_json(msg="Can't authorize connection - "+str(e)) - + except botocore.exceptions.NoCredentialsError as e: + module.fail_json(msg="Can't authorize connection - %s" % str(e)) + invocations = { "present": setup_network_acl, "absent": remove_network_acl @@ -538,9 +539,6 @@ def main(): (changed, results) = invocations[state](client, module) module.exit_json(changed=changed, nacl_id=results) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * if __name__ == '__main__': main() diff --git a/cloud/amazon/ec2_vpc_net_facts.py b/cloud/amazon/ec2_vpc_net_facts.py index 8de47ed9758..dfa29ba9d20 100644 --- a/cloud/amazon/ec2_vpc_net_facts.py +++ b/cloud/amazon/ec2_vpc_net_facts.py @@ -58,6 +58,10 @@ except ImportError: HAS_BOTO = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import connect_to_aws, ec2_argument_spec, get_aws_connection_info + + def get_vpc_info(vpc): try: @@ -111,15 +115,13 @@ def main(): if region: try: connection = connect_to_aws(boto.vpc, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, StandardError), e: + except (boto.exception.NoAuthHandlerFound, StandardError) as e: module.fail_json(msg=str(e)) else: module.fail_json(msg="region must be specified") list_ec2_vpcs(connection, module) -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * if __name__ == '__main__': main() diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index 416e0b43040..4062f094ec4 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -107,8 +107,6 @@ ''' - -import sys # noqa import re try: @@ -121,6 +119,9 @@ if __name__ != '__main__': raise +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import AnsibleAWSError, connect_to_aws, ec2_argument_spec, get_aws_connection_info + class AnsibleRouteTableException(Exception): pass @@ -179,7 +180,7 @@ def find_subnets(vpc_conn, vpc_id, identified_subnets): for cidr in subnet_cidrs: if not any(s.cidr_block == cidr for s in subnets_by_cidr): raise AnsibleSubnetSearchException( - 'Subnet CIDR "{0}" does not exist'.format(subnet_cidr)) + 'Subnet CIDR "{0}" does not exist'.format(cidr)) subnets_by_name = [] if subnet_names: @@ -605,7 +606,7 @@ def main(): if region: try: connection = connect_to_aws(boto.vpc, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: + except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: module.fail_json(msg=str(e)) else: module.fail_json(msg="region must be specified") @@ -627,8 +628,6 @@ def main(): module.exit_json(**result) -from ansible.module_utils.basic import * # noqa -from ansible.module_utils.ec2 import * # noqa if __name__ == '__main__': main() diff --git a/cloud/amazon/ec2_vpc_route_table_facts.py b/cloud/amazon/ec2_vpc_route_table_facts.py index 8b5e60ab2c9..394c4b28db3 100644 --- a/cloud/amazon/ec2_vpc_route_table_facts.py +++ b/cloud/amazon/ec2_vpc_route_table_facts.py @@ -62,6 +62,10 @@ except ImportError: HAS_BOTO = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import AnsibleAWSError, connect_to_aws, ec2_argument_spec, get_aws_connection_info + + def get_route_table_info(route_table): # Add any routes to array @@ -111,15 +115,13 @@ def main(): if region: try: connection = connect_to_aws(boto.vpc, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: + except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: module.fail_json(msg=str(e)) else: module.fail_json(msg="region must be specified") list_ec2_vpc_route_tables(connection, module) -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * if __name__ == '__main__': main() diff --git a/cloud/amazon/ec2_vpc_subnet.py b/cloud/amazon/ec2_vpc_subnet.py index 6e7c3e7d430..ddc4f5e99cf 100644 --- a/cloud/amazon/ec2_vpc_subnet.py +++ b/cloud/amazon/ec2_vpc_subnet.py @@ -74,7 +74,6 @@ ''' -import sys # noqa import time try: @@ -87,6 +86,9 @@ if __name__ != '__main__': raise +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import AnsibleAWSError, connect_to_aws, ec2_argument_spec, get_aws_connection_info + class AnsibleVPCSubnetException(Exception): pass @@ -242,7 +244,7 @@ def main(): if region: try: connection = connect_to_aws(boto.vpc, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: + except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: module.fail_json(msg=str(e)) else: module.fail_json(msg="region must be specified") @@ -265,8 +267,6 @@ def main(): module.exit_json(**result) -from ansible.module_utils.basic import * # noqa -from ansible.module_utils.ec2 import * # noqa if __name__ == '__main__': main() diff --git a/cloud/amazon/ec2_vpc_subnet_facts.py b/cloud/amazon/ec2_vpc_subnet_facts.py index c3637292248..98f1e4875c5 100644 --- a/cloud/amazon/ec2_vpc_subnet_facts.py +++ b/cloud/amazon/ec2_vpc_subnet_facts.py @@ -77,6 +77,10 @@ except ImportError: HAS_BOTO = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import AnsibleAWSError, connect_to_aws, ec2_argument_spec, get_aws_connection_info + + def get_subnet_info(subnet): subnet_info = { 'id': subnet.id, @@ -126,15 +130,13 @@ def main(): if region: try: connection = connect_to_aws(boto.vpc, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: + except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: module.fail_json(msg=str(e)) else: module.fail_json(msg="region must be specified") list_ec2_vpc_subnets(connection, module) -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * if __name__ == '__main__': main() diff --git a/cloud/amazon/ecs_cluster.py b/cloud/amazon/ecs_cluster.py index 22049a9f3c4..ad029d4e1f3 100644 --- a/cloud/amazon/ecs_cluster.py +++ b/cloud/amazon/ecs_cluster.py @@ -115,6 +115,10 @@ except ImportError: HAS_BOTO3 = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info + + class EcsClusterManager: """Handles ECS Clusters""" @@ -127,8 +131,8 @@ def __init__(self, module): if not region: module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except boto.exception.NoAuthHandlerFound, e: - self.module.fail_json(msg="Can't authorize connection - "+str(e)) + except boto.exception.NoAuthHandlerFound as e: + self.module.fail_json(msg="Can't authorize connection - %s" % str(e)) def find_in_array(self, array_of_clusters, cluster_name, field_name='clusterArn'): for c in array_of_clusters: @@ -180,7 +184,7 @@ def main(): cluster_mgr = EcsClusterManager(module) try: existing = cluster_mgr.describe_cluster(module.params['name']) - except Exception, e: + except Exception as e: module.fail_json(msg="Exception describing cluster '"+module.params['name']+"': "+str(e)) results = dict(changed=False) @@ -230,9 +234,6 @@ def main(): module.exit_json(**results) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * if __name__ == '__main__': main() diff --git a/cloud/amazon/ecs_service.py b/cloud/amazon/ecs_service.py index 94d7078c82d..3ae77e6b519 100644 --- a/cloud/amazon/ecs_service.py +++ b/cloud/amazon/ecs_service.py @@ -175,6 +175,8 @@ returned: when service existed and was deleted type: complex ''' +import time + try: import boto import botocore @@ -188,6 +190,10 @@ except ImportError: HAS_BOTO3 = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info + + class EcsServiceManager: """Handles ECS Services""" @@ -200,8 +206,8 @@ def __init__(self, module): if not region: module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except boto.exception.NoAuthHandlerFound, e: - self.module.fail_json(msg="Can't authorize connection - "+str(e)) + except boto.exception.NoAuthHandlerFound as e: + self.module.fail_json(msg="Can't authorize connection - %s" % str(e)) # def list_clusters(self): # return self.client.list_clusters() @@ -321,7 +327,7 @@ def main(): service_mgr = EcsServiceManager(module) try: existing = service_mgr.describe_service(module.params['cluster'], module.params['name']) - except Exception, e: + except Exception as e: module.fail_json(msg="Exception describing service '"+module.params['name']+"' in cluster '"+module.params['cluster']+"': "+str(e)) results = dict(changed=False ) @@ -392,7 +398,7 @@ def main(): module.params['name'], module.params['cluster'] ) - except botocore.exceptions.ClientError, e: + except botocore.exceptions.ClientError as e: module.fail_json(msg=e.message) results['changed'] = True @@ -418,9 +424,6 @@ def main(): module.exit_json(**results) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * if __name__ == '__main__': main() diff --git a/cloud/amazon/ecs_service_facts.py b/cloud/amazon/ecs_service_facts.py index f363c56a872..aaee2aa01fd 100644 --- a/cloud/amazon/ecs_service_facts.py +++ b/cloud/amazon/ecs_service_facts.py @@ -139,6 +139,10 @@ except ImportError: HAS_BOTO3 = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info + + class EcsServiceManager: """Handles ECS Services""" @@ -151,8 +155,8 @@ def __init__(self, module): if not region: module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except boto.exception.NoAuthHandlerFound, e: - self.module.fail_json(msg="Can't authorize connection - "+str(e)) + except boto.exception.NoAuthHandlerFound as e: + self.module.fail_json(msg="Can't authorize connection - %s" % str(e)) # def list_clusters(self): # return self.client.list_clusters() @@ -227,10 +231,6 @@ def main(): ecs_facts_result = dict(changed=False, ansible_facts=ecs_facts) module.exit_json(**ecs_facts_result) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * -from ansible.module_utils.ec2 import * if __name__ == '__main__': main() diff --git a/cloud/amazon/ecs_task.py b/cloud/amazon/ecs_task.py index f263ef0da92..86c77eceb1c 100644 --- a/cloud/amazon/ecs_task.py +++ b/cloud/amazon/ecs_task.py @@ -165,6 +165,10 @@ except ImportError: HAS_BOTO3 = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info + + class EcsExecManager: """Handles ECS Tasks""" @@ -176,8 +180,8 @@ def __init__(self, module): if not region: module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except boto.exception.NoAuthHandlerFound, e: - module.fail_json(msg="Can't authorize connection - "+str(e)) + except boto.exception.NoAuthHandlerFound as e: + module.fail_json(msg="Can't authorize connection - %s " % str(e)) def list_tasks(self, cluster_name, service_name, status): response = self.ecs.list_tasks( @@ -316,9 +320,6 @@ def main(): module.exit_json(**results) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * if __name__ == '__main__': main() diff --git a/cloud/amazon/ecs_taskdefinition.py b/cloud/amazon/ecs_taskdefinition.py index e924417f9d1..2fefe231144 100644 --- a/cloud/amazon/ecs_taskdefinition.py +++ b/cloud/amazon/ecs_taskdefinition.py @@ -108,6 +108,10 @@ except ImportError: HAS_BOTO3 = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info + + class EcsTaskManager: """Handles ECS Tasks""" @@ -119,8 +123,8 @@ def __init__(self, module): if not region: module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") self.ecs = boto3_conn(module, conn_type='client', resource='ecs', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except boto.exception.NoAuthHandlerFound, e: - module.fail_json(msg="Can't authorize connection - "+str(e)) + except boto.exception.NoAuthHandlerFound as e: + module.fail_json(msg="Can't authorize connection - " % str(e)) def describe_task(self, task_name): try: @@ -213,9 +217,6 @@ def main(): module.exit_json(**results) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * if __name__ == '__main__': main() diff --git a/cloud/amazon/route53_facts.py b/cloud/amazon/route53_facts.py index 95c1491a66f..15379688c3f 100644 --- a/cloud/amazon/route53_facts.py +++ b/cloud/amazon/route53_facts.py @@ -172,6 +172,9 @@ except ImportError: HAS_BOTO3 = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info + def get_hosted_zone(client, module): params = dict() @@ -413,8 +416,8 @@ def main(): try: region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) route53 = boto3_conn(module, conn_type='client', resource='route53', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except boto.exception.NoAuthHandlerFound, e: - module.fail_json(msg="Can't authorize connection - "+str(e)) + except boto.exception.NoAuthHandlerFound as e: + module.fail_json(msg="Can't authorize connection - %s " % str(e)) invocations = { 'change': change_details, @@ -428,9 +431,6 @@ def main(): module.exit_json(**results) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * if __name__ == '__main__': main() diff --git a/cloud/amazon/route53_health_check.py b/cloud/amazon/route53_health_check.py index 9ad7f63d450..9bbb7b3e29c 100644 --- a/cloud/amazon/route53_health_check.py +++ b/cloud/amazon/route53_health_check.py @@ -125,7 +125,6 @@ ''' -import time import uuid try: @@ -138,6 +137,11 @@ except ImportError: HAS_BOTO = False +# import module snippets +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import ec2_argument_spec, get_aws_connection_info + + # Things that can't get changed: # protocol # ip_address or domain @@ -318,7 +322,7 @@ def main(): # connect to the route53 endpoint try: conn = Route53Connection(**aws_connect_kwargs) - except boto.exception.BotoServerError, e: + except boto.exception.BotoServerError as e: module.fail_json(msg = e.error_message) changed = False @@ -351,8 +355,6 @@ def main(): module.exit_json(changed=changed, health_check=dict(id=check_id), action=action) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/amazon/route53_zone.py b/cloud/amazon/route53_zone.py index 328d48dbf67..d08ed88aa15 100644 --- a/cloud/amazon/route53_zone.py +++ b/cloud/amazon/route53_zone.py @@ -108,8 +108,6 @@ sample: "Z6JQG9820BEFMW" ''' -import time - try: import boto import boto.ec2 @@ -120,6 +118,9 @@ except ImportError: HAS_BOTO = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import ec2_argument_spec, get_aws_connection_info + def main(): argument_spec = ec2_argument_spec() @@ -150,7 +151,7 @@ def main(): # connect to the route53 endpoint try: conn = Route53Connection(**aws_connect_kwargs) - except boto.exception.BotoServerError, e: + except boto.exception.BotoServerError as e: module.fail_json(msg=e.error_message) results = conn.get_all_hosted_zones() @@ -218,7 +219,5 @@ def main(): elif state == 'absent': module.exit_json(changed=False) -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * - -main() +if __name__ == '__main__': + main() diff --git a/cloud/amazon/s3_lifecycle.py b/cloud/amazon/s3_lifecycle.py index 25415395361..c34b8ccf248 100644 --- a/cloud/amazon/s3_lifecycle.py +++ b/cloud/amazon/s3_lifecycle.py @@ -159,6 +159,9 @@ except ImportError: HAS_BOTO = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import AnsibleAWSError, ec2_argument_spec, get_aws_connection_info + def create_lifecycle_rule(connection, module): name = module.params.get("name") @@ -174,13 +177,13 @@ def create_lifecycle_rule(connection, module): try: bucket = connection.get_bucket(name) - except S3ResponseError, e: + except S3ResponseError as e: module.fail_json(msg=e.message) # Get the bucket's current lifecycle rules try: current_lifecycle_obj = bucket.get_lifecycle_config() - except S3ResponseError, e: + except S3ResponseError as e: if e.error_code == "NoSuchLifecycleConfiguration": current_lifecycle_obj = Lifecycle() else: @@ -243,7 +246,7 @@ def create_lifecycle_rule(connection, module): # Write lifecycle to bucket try: bucket.configure_lifecycle(lifecycle_obj) - except S3ResponseError, e: + except S3ResponseError as e: module.fail_json(msg=e.message) module.exit_json(changed=changed) @@ -305,13 +308,13 @@ def destroy_lifecycle_rule(connection, module): try: bucket = connection.get_bucket(name) - except S3ResponseError, e: + except S3ResponseError as e: module.fail_json(msg=e.message) # Get the bucket's current lifecycle rules try: current_lifecycle_obj = bucket.get_lifecycle_config() - except S3ResponseError, e: + except S3ResponseError as e: if e.error_code == "NoSuchLifecycleConfiguration": module.exit_json(changed=changed) else: @@ -343,7 +346,7 @@ def destroy_lifecycle_rule(connection, module): bucket.configure_lifecycle(lifecycle_obj) else: bucket.delete_lifecycle_configuration() - except BotoServerError, e: + except BotoServerError as e: module.fail_json(msg=e.message) module.exit_json(changed=changed) @@ -397,7 +400,7 @@ def main(): # use this as fallback because connect_to_region seems to fail in boto + non 'classic' aws accounts in some cases if connection is None: connection = boto.connect_s3(**aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: + except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: module.fail_json(msg=str(e)) expiration_date = module.params.get("expiration_date") @@ -409,13 +412,13 @@ def main(): if expiration_date is not None: try: datetime.datetime.strptime(expiration_date, "%Y-%m-%dT%H:%M:%S.000Z") - except ValueError, e: + except ValueError as e: module.fail_json(msg="expiration_date is not a valid ISO-8601 format. The time must be midnight and a timezone of GMT must be included") if transition_date is not None: try: datetime.datetime.strptime(transition_date, "%Y-%m-%dT%H:%M:%S.000Z") - except ValueError, e: + except ValueError as e: module.fail_json(msg="expiration_date is not a valid ISO-8601 format. The time must be midnight and a timezone of GMT must be included") boto_required_version = (2,40,0) @@ -427,8 +430,6 @@ def main(): elif state == 'absent': destroy_lifecycle_rule(connection, module) -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * if __name__ == '__main__': main() diff --git a/cloud/amazon/s3_logging.py b/cloud/amazon/s3_logging.py index dca2a28aca0..91ca1b34e4b 100644 --- a/cloud/amazon/s3_logging.py +++ b/cloud/amazon/s3_logging.py @@ -72,6 +72,9 @@ except ImportError: HAS_BOTO = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import AnsibleAWSError, ec2_argument_spec, get_aws_connection_info + def compare_bucket_logging(bucket, target_bucket, target_prefix): @@ -162,7 +165,7 @@ def main(): # use this as fallback because connect_to_region seems to fail in boto + non 'classic' aws accounts in some cases if connection is None: connection = boto.connect_s3(**aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: + except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: module.fail_json(msg=str(e)) state = module.params.get("state") @@ -172,8 +175,6 @@ def main(): elif state == 'absent': disable_bucket_logging(connection, module) -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * if __name__ == '__main__': main() diff --git a/cloud/amazon/sns_topic.py b/cloud/amazon/sns_topic.py index f4916693edc..34bae494051 100644 --- a/cloud/amazon/sns_topic.py +++ b/cloud/amazon/sns_topic.py @@ -122,7 +122,6 @@ attributes_set: [] ''' -import sys import time import json import re @@ -134,6 +133,9 @@ except ImportError: HAS_BOTO = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import connect_to_aws, ec2_argument_spec, get_aws_connection_info + class SnsTopicManager(object): """ Handles SNS Topic creation and destruction """ @@ -176,7 +178,7 @@ def _get_boto_connection(self): try: return connect_to_aws(boto.sns, self.region, **self.aws_connect_params) - except BotoServerError, err: + except BotoServerError as err: self.module.fail_json(msg=err.message) def _get_all_topics(self): @@ -185,8 +187,8 @@ def _get_all_topics(self): while True: try: response = self.connection.get_all_topics(next_token) - except BotoServerError, err: - module.fail_json(msg=err.message) + except BotoServerError as err: + self.module.fail_json(msg=err.message) topics.extend(response['ListTopicsResponse']['ListTopicsResult']['Topics']) next_token = response['ListTopicsResponse']['ListTopicsResult']['NextToken'] if not next_token: @@ -400,8 +402,5 @@ def main(): module.exit_json(**sns_facts) -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * - if __name__ == '__main__': main() diff --git a/cloud/amazon/sqs_queue.py b/cloud/amazon/sqs_queue.py index d00a3b638ff..d5e17ad2433 100644 --- a/cloud/amazon/sqs_queue.py +++ b/cloud/amazon/sqs_queue.py @@ -103,6 +103,9 @@ state: absent ''' +import json +import traceback + try: import boto.sqs from boto.exception import BotoServerError, NoAuthHandlerFound @@ -111,6 +114,9 @@ except ImportError: HAS_BOTO = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import AnsibleAWSError, connect_to_aws, ec2_argument_spec, get_aws_connection_info + def create_or_update_sqs_queue(connection, module): queue_name = module.params.get('name') @@ -255,7 +261,7 @@ def main(): try: connection = connect_to_aws(boto.sqs, region, **aws_connect_params) - except (NoAuthHandlerFound, AnsibleAWSError), e: + except (NoAuthHandlerFound, AnsibleAWSError) as e: module.fail_json(msg=str(e)) state = module.params.get('state') @@ -265,9 +271,5 @@ def main(): delete_sqs_queue(connection, module) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * - if __name__ == '__main__': main() diff --git a/cloud/amazon/sts_assume_role.py b/cloud/amazon/sts_assume_role.py index a3fab12137e..da8ba9a7651 100644 --- a/cloud/amazon/sts_assume_role.py +++ b/cloud/amazon/sts_assume_role.py @@ -91,6 +91,9 @@ except ImportError: HAS_BOTO = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import AnsibleAWSError, connect_to_aws, ec2_argument_spec, get_aws_connection_info + def assume_role_policy(connection, module): @@ -106,7 +109,7 @@ def assume_role_policy(connection, module): try: assumed_role = connection.assume_role(role_arn, role_session_name, policy, duration_seconds, external_id, mfa_serial_number, mfa_token) changed = True - except BotoServerError, e: + except BotoServerError as e: module.fail_json(msg=e) module.exit_json(changed=changed, sts_creds=assumed_role.credentials.__dict__, sts_user=assumed_role.user.__dict__) @@ -135,20 +138,16 @@ def main(): if region: try: connection = connect_to_aws(boto.sts, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError), e: + except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: module.fail_json(msg=str(e)) else: module.fail_json(msg="region must be specified") try: assume_role_policy(connection, module) - except BotoServerError, e: + except BotoServerError as e: module.fail_json(msg=e) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * - if __name__ == '__main__': main() diff --git a/cloud/amazon/sts_session_token.py b/cloud/amazon/sts_session_token.py index dc284deaecd..320cc1d271b 100644 --- a/cloud/amazon/sts_session_token.py +++ b/cloud/amazon/sts_session_token.py @@ -46,6 +46,7 @@ requirements: - boto3 - botocore + - python >= 2.6 ''' RETURN = """ @@ -92,6 +93,10 @@ except ImportError: HAS_BOTO3 = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info + + def normalize_credentials(credentials): access_key = credentials.get('AccessKeyId', None) secret_key = credentials.get('SecretAccessKey', None) @@ -121,7 +126,7 @@ def get_session_token(connection, module): try: response = connection.get_session_token(**args) changed = True - except ClientError, e: + except ClientError as e: module.fail_json(msg=e) credentials = normalize_credentials(response.get('Credentials', {})) @@ -151,9 +156,5 @@ def main(): get_session_token(connection, module) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * - if __name__ == '__main__': main() diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt index 9fba92376f2..2abfa3b178d 100644 --- a/test/utils/shippable/sanity-skip-python3.txt +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -1,33 +1,3 @@ -/cloud/amazon/cloudtrail.py -/cloud/amazon/dynamodb_table.py -/cloud/amazon/ec2_ami_copy.py -/cloud/amazon/ec2_customer_gateway.py -/cloud/amazon/ec2_eni.py -/cloud/amazon/ec2_eni_facts.py -/cloud/amazon/ec2_remote_facts.py -/cloud/amazon/ec2_snapshot_facts.py -/cloud/amazon/ec2_vol_facts.py -/cloud/amazon/ec2_vpc_igw.py -/cloud/amazon/ec2_vpc_nacl.py -/cloud/amazon/ec2_vpc_net_facts.py -/cloud/amazon/ec2_vpc_route_table.py -/cloud/amazon/ec2_vpc_route_table_facts.py -/cloud/amazon/ec2_vpc_subnet.py -/cloud/amazon/ec2_vpc_subnet_facts.py -/cloud/amazon/ecs_cluster.py -/cloud/amazon/ecs_service.py -/cloud/amazon/ecs_service_facts.py -/cloud/amazon/ecs_task.py -/cloud/amazon/ecs_taskdefinition.py -/cloud/amazon/route53_facts.py -/cloud/amazon/route53_health_check.py -/cloud/amazon/route53_zone.py -/cloud/amazon/s3_lifecycle.py -/cloud/amazon/s3_logging.py -/cloud/amazon/sns_topic.py -/cloud/amazon/sqs_queue.py -/cloud/amazon/sts_assume_role.py -/cloud/amazon/sts_session_token.py /cloud/misc/virt_net.py /cloud/misc/virt_pool.py /cloud/profitbricks/profitbricks.py From 6b0a204e74144bb2adc42292ea0ca6379b3f739d Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Sun, 23 Oct 2016 16:24:38 -0700 Subject: [PATCH 2258/2522] Fix remaining python3 compile problems --- cloud/misc/virt_net.py | 21 +++++----- cloud/misc/virt_pool.py | 24 ++++++------ cloud/profitbricks/profitbricks.py | 30 ++++++++------ cloud/profitbricks/profitbricks_volume.py | 21 ++++++---- database/mysql/mysql_replication.py | 28 ++++++------- database/vertica/vertica_configuration.py | 22 +++++++---- database/vertica/vertica_facts.py | 36 +++++++++-------- database/vertica/vertica_role.py | 25 ++++++++---- database/vertica/vertica_schema.py | 25 ++++++++---- database/vertica/vertica_user.py | 25 ++++++++---- monitoring/boundary_meter.py | 41 +++++++++++--------- monitoring/circonus_annotation.py | 24 +++++++++--- monitoring/datadog_monitor.py | 37 +++++++++++------- monitoring/rollbar_deployment.py | 13 +++++-- monitoring/stackdriver.py | 16 +++++--- network/cloudflare_dns.py | 34 +++++++++------- network/f5/bigip_gtm_virtual_server.py | 16 +++++--- network/f5/bigip_gtm_wide_ip.py | 19 +++++---- test/utils/shippable/sanity-skip-python3.txt | 18 --------- 19 files changed, 279 insertions(+), 196 deletions(-) diff --git a/cloud/misc/virt_net.py b/cloud/misc/virt_net.py index e2dd88f4d4a..29cb43c3b97 100755 --- a/cloud/misc/virt_net.py +++ b/cloud/misc/virt_net.py @@ -117,7 +117,6 @@ VIRT_SUCCESS = 0 VIRT_UNAVAILABLE=2 -import sys try: import libvirt @@ -133,6 +132,9 @@ else: HAS_XML = True +from ansible.module_utils.basic import AnsibleModule + + ALL_COMMANDS = [] ENTRY_COMMANDS = ['create', 'status', 'start', 'stop', 'undefine', 'destroy', 'get_xml', 'define', @@ -345,7 +347,7 @@ def define_from_xml(self, entryid, xml): return self.conn.networkDefineXML(xml) else: try: - state = self.find_entry(entryid) + self.find_entry(entryid) except: return self.module.exit_json(changed=True) @@ -428,17 +430,17 @@ def facts(self, facts_mode='facts'): try: results[entry]["forward_mode"] = self.conn.get_forward(entry) - except ValueError as e: + except ValueError: pass try: results[entry]["domain"] = self.conn.get_domain(entry) - except ValueError as e: + except ValueError: pass try: results[entry]["macaddress"] = self.conn.get_macaddress(entry) - except ValueError as e: + except ValueError: pass facts = dict() @@ -532,7 +534,7 @@ def core(module): return VIRT_SUCCESS, res else: - module.fail_json(msg="Command %s not recognized" % basecmd) + module.fail_json(msg="Command %s not recognized" % command) if autostart is not None: if not name: @@ -580,7 +582,7 @@ def main(): rc = VIRT_SUCCESS try: rc, result = core(module) - except Exception, e: + except Exception as e: module.fail_json(msg=str(e)) if rc != 0: # something went wrong emit the msg @@ -589,6 +591,5 @@ def main(): module.exit_json(**result) -# import module snippets -from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/misc/virt_pool.py b/cloud/misc/virt_pool.py index b104ef548dc..a5664479db5 100755 --- a/cloud/misc/virt_pool.py +++ b/cloud/misc/virt_pool.py @@ -128,8 +128,6 @@ VIRT_SUCCESS = 0 VIRT_UNAVAILABLE=2 -import sys - try: import libvirt except ImportError: @@ -144,6 +142,9 @@ else: HAS_XML = True +from ansible.module_utils.basic import AnsibleModule + + ALL_COMMANDS = [] ENTRY_COMMANDS = ['create', 'status', 'start', 'stop', 'build', 'delete', 'undefine', 'destroy', 'get_xml', 'define', 'refresh'] @@ -389,7 +390,7 @@ def define_from_xml(self, entryid, xml): return self.conn.storagePoolDefineXML(xml) else: try: - state = self.find_entry(entryid) + self.find_entry(entryid) except: return self.module.exit_json(changed=True) @@ -499,23 +500,23 @@ def facts(self, facts_mode='facts'): try: results[entry]["host"] = self.conn.get_host(entry) - except ValueError as e: + except ValueError: pass try: results[entry]["source_path"] = self.conn.get_source_path(entry) - except ValueError as e: + except ValueError: pass try: results[entry]["format"] = self.conn.get_format(entry) - except ValueError as e: + except ValueError: pass try: devices = self.conn.get_devices(entry) results[entry]["devices"] = devices - except ValueError as e: + except ValueError: pass else: @@ -627,7 +628,7 @@ def core(module): return VIRT_SUCCESS, res else: - module.fail_json(msg="Command %s not recognized" % basecmd) + module.fail_json(msg="Command %s not recognized" % command) if autostart is not None: if not name: @@ -676,7 +677,7 @@ def main(): rc = VIRT_SUCCESS try: rc, result = core(module) - except Exception, e: + except Exception as e: module.fail_json(msg=str(e)) if rc != 0: # something went wrong emit the msg @@ -685,6 +686,5 @@ def main(): module.exit_json(**result) -# import module snippets -from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/profitbricks/profitbricks.py b/cloud/profitbricks/profitbricks.py index 7c9f23f6bb0..caa1e0cc1ef 100644 --- a/cloud/profitbricks/profitbricks.py +++ b/cloud/profitbricks/profitbricks.py @@ -206,6 +206,10 @@ except ImportError: HAS_PB_SDK = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception + + LOCATIONS = ['us/las', 'de/fra', 'de/fkb'] @@ -325,7 +329,7 @@ def _startstop_machine(module, profitbricks, datacenter_id, server_id): return True except Exception as e: - module.fail_json(msg="failed to start or stop the virtual machine %s: %s" % (name, str(e))) + module.fail_json(msg="failed to start or stop the virtual machine %s at %s: %s" % (server_id, datacenter_id, str(e))) def _create_datacenter(module, profitbricks): @@ -390,7 +394,8 @@ def create_virtual_machine(module, profitbricks): try: name % 0 - except TypeError, e: + except TypeError: + e = get_exception() if e.message.startswith('not all'): name = '%s%%d' % name else: @@ -475,7 +480,8 @@ def remove_virtual_machine(module, profitbricks): # Remove the server try: server_response = profitbricks.delete_server(datacenter_id, server_id) - except Exception as e: + except Exception: + e = get_exception() module.fail_json(msg="failed to terminate the virtual server: %s" % str(e)) else: changed = True @@ -491,7 +497,8 @@ def _remove_boot_volume(module, profitbricks, datacenter_id, server_id): server = profitbricks.get_server(datacenter_id, server_id) volume_id = server['properties']['bootVolume']['id'] volume_response = profitbricks.delete_volume(datacenter_id, volume_id) - except Exception as e: + except Exception: + e = get_exception() module.fail_json(msg="failed to remove the server's boot volume: %s" % str(e)) @@ -609,8 +616,6 @@ def main(): subscription_user = module.params.get('subscription_user') subscription_password = module.params.get('subscription_password') - wait = module.params.get('wait') - wait_timeout = module.params.get('wait_timeout') profitbricks = ProfitBricksService( username=subscription_user, @@ -626,7 +631,8 @@ def main(): try: (changed) = remove_virtual_machine(module, profitbricks) module.exit_json(changed=changed) - except Exception as e: + except Exception: + e = get_exception() module.fail_json(msg='failed to set instance state: %s' % str(e)) elif state in ('running', 'stopped'): @@ -636,7 +642,8 @@ def main(): try: (changed) = startstop_machine(module, profitbricks, state) module.exit_json(changed=changed) - except Exception as e: + except Exception: + e = get_exception() module.fail_json(msg='failed to set instance state: %s' % str(e)) elif state == 'present': @@ -654,9 +661,10 @@ def main(): try: (machine_dict_array) = create_virtual_machine(module, profitbricks) module.exit_json(**machine_dict_array) - except Exception as e: + except Exception: + e = get_exception() module.fail_json(msg='failed to set instance state: %s' % str(e)) -from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/profitbricks/profitbricks_volume.py b/cloud/profitbricks/profitbricks_volume.py index 1cee9676750..a6c3d06958f 100644 --- a/cloud/profitbricks/profitbricks_volume.py +++ b/cloud/profitbricks/profitbricks_volume.py @@ -135,7 +135,6 @@ ''' import re -import uuid import time HAS_PB_SDK = True @@ -145,6 +144,10 @@ except ImportError: HAS_PB_SDK = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception + + uuid_match = re.compile( '[\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12}', re.I) @@ -253,7 +256,8 @@ def create_volume(module, profitbricks): try: name % 0 - except TypeError, e: + except TypeError: + e = get_exception() if e.message.startswith('not all'): name = '%s%%d' % name else: @@ -354,7 +358,8 @@ def _attach_volume(module, profitbricks, datacenter, volume): try: return profitbricks.attach_volume(datacenter, server, volume) - except Exception as e: + except Exception: + e = get_exception() module.fail_json(msg='failed to attach volume: %s' % str(e)) @@ -403,7 +408,8 @@ def main(): try: (changed) = delete_volume(module, profitbricks) module.exit_json(changed=changed) - except Exception as e: + except Exception: + e = get_exception() module.fail_json(msg='failed to set volume state: %s' % str(e)) elif state == 'present': @@ -415,9 +421,10 @@ def main(): try: (volume_dict_array) = create_volume(module, profitbricks) module.exit_json(**volume_dict_array) - except Exception as e: + except Exception: + e = get_exception() module.fail_json(msg='failed to set volume state: %s' % str(e)) -from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/database/mysql/mysql_replication.py b/database/mysql/mysql_replication.py index 551875a0d50..68b6bc8eeb7 100644 --- a/database/mysql/mysql_replication.py +++ b/database/mysql/mysql_replication.py @@ -125,6 +125,10 @@ else: mysqldb_found = True +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.mysql import mysql_connect +from ansible.module_utils.pycompat24 import get_exception + def get_master_status(cursor): cursor.execute("SHOW MASTER STATUS") @@ -212,10 +216,6 @@ def main(): ssl_ca=dict(default=None), ) ) - user = module.params["login_user"] - password = module.params["login_password"] - host = module.params["login_host"] - port = module.params["login_port"] mode = module.params["mode"] master_host = module.params["master_host"] master_user = module.params["master_user"] @@ -250,7 +250,8 @@ def main(): try: cursor = mysql_connect(module, login_user, login_password, config_file, ssl_cert, ssl_key, ssl_ca, None, 'MySQLdb.cursors.DictCursor', connect_timeout=connect_timeout) - except Exception, e: + except Exception: + e = get_exception() 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: @@ -324,9 +325,11 @@ def main(): chm.append("MASTER_AUTO_POSITION = 1") try: changemaster(cursor, chm, chm_params) - except MySQLdb.Warning, e: - result['warning'] = str(e) - except Exception, e: + except MySQLdb.Warning: + e = get_exception() + result['warning'] = str(e) + except Exception: + e = get_exception() module.fail_json(msg='%s. Query == CHANGE MASTER TO %s' % (e, chm)) result['changed']=True module.exit_json(**result) @@ -355,8 +358,7 @@ def main(): else: module.exit_json(msg="Slave already reset", changed=False) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.mysql import * -main() -warnings.simplefilter("ignore") + +if __name__ == '__main__': + main() + warnings.simplefilter("ignore") diff --git a/database/vertica/vertica_configuration.py b/database/vertica/vertica_configuration.py index ed75667b139..e4861ca1225 100644 --- a/database/vertica/vertica_configuration.py +++ b/database/vertica/vertica_configuration.py @@ -82,6 +82,10 @@ else: pyodbc_found = True +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception + + class NotSupportedError(Exception): pass @@ -164,7 +168,8 @@ def main(): module.params['login_user'], module.params['login_password'], 'true') db_conn = pyodbc.connect(dsn, autocommit=True) cursor = db_conn.cursor() - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg="Unable to connect to database: {0}.".format(e)) try: @@ -174,21 +179,24 @@ def main(): else: try: changed = present(configuration_facts, cursor, parameter_name, current_value) - except pyodbc.Error, e: + except pyodbc.Error: + e = get_exception() module.fail_json(msg=str(e)) - except NotSupportedError, e: + except NotSupportedError: + e = get_exception() module.fail_json(msg=str(e), ansible_facts={'vertica_configuration': configuration_facts}) - except CannotDropError, e: + except CannotDropError: + e = get_exception() module.fail_json(msg=str(e), ansible_facts={'vertica_configuration': configuration_facts}) except SystemExit: # avoid catching this on python 2.4 raise - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg=e) module.exit_json(changed=changed, parameter=parameter_name, ansible_facts={'vertica_configuration': configuration_facts}) -# import ansible utilities -from ansible.module_utils.basic import * + if __name__ == '__main__': main() diff --git a/database/vertica/vertica_facts.py b/database/vertica/vertica_facts.py index 705b74a04f5..ee8335cf997 100644 --- a/database/vertica/vertica_facts.py +++ b/database/vertica/vertica_facts.py @@ -74,6 +74,10 @@ else: pyodbc_found = True +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception + + class NotSupportedError(Exception): pass @@ -232,24 +236,23 @@ def main(): if module.params['db']: db = module.params['db'] - changed = False - try: dsn = ( "Driver=Vertica;" - "Server={0};" - "Port={1};" - "Database={2};" - "User={3};" - "Password={4};" - "ConnectionLoadBalance={5}" - ).format(module.params['cluster'], module.params['port'], db, + "Server=%s;" + "Port=%s;" + "Database=%s;" + "User=%s;" + "Password=%s;" + "ConnectionLoadBalance=%s" + ) % (module.params['cluster'], module.params['port'], db, module.params['login_user'], module.params['login_password'], 'true') db_conn = pyodbc.connect(dsn, autocommit=True) cursor = db_conn.cursor() - except Exception, e: - module.fail_json(msg="Unable to connect to database: {0}.".format(e)) - + except Exception: + e = get_exception() + module.fail_json(msg="Unable to connect to database: %s." % str(e)) + try: schema_facts = get_schema_facts(cursor) user_facts = get_user_facts(cursor) @@ -262,15 +265,16 @@ def main(): 'vertica_roles': role_facts, 'vertica_configuration': configuration_facts, 'vertica_nodes': node_facts}) - except NotSupportedError, e: + except NotSupportedError: + e = get_exception() module.fail_json(msg=str(e)) except SystemExit: # avoid catching this on python 2.4 raise - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg=e) -# import ansible utilities -from ansible.module_utils.basic import * + if __name__ == '__main__': main() diff --git a/database/vertica/vertica_role.py b/database/vertica/vertica_role.py index b7a0a5d66ef..ba0eab4daf7 100644 --- a/database/vertica/vertica_role.py +++ b/database/vertica/vertica_role.py @@ -93,6 +93,10 @@ else: pyodbc_found = True +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception + + class NotSupportedError(Exception): pass @@ -208,7 +212,8 @@ def main(): module.params['login_user'], module.params['login_password'], 'true') db_conn = pyodbc.connect(dsn, autocommit=True) cursor = db_conn.cursor() - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg="Unable to connect to database: {0}.".format(e)) try: @@ -218,26 +223,30 @@ def main(): elif state == 'absent': try: changed = absent(role_facts, cursor, role, assigned_roles) - except pyodbc.Error, e: + except pyodbc.Error: + e = get_exception() module.fail_json(msg=str(e)) elif state == 'present': try: changed = present(role_facts, cursor, role, assigned_roles) - except pyodbc.Error, e: + except pyodbc.Error: + e = get_exception() module.fail_json(msg=str(e)) - except NotSupportedError, e: + except NotSupportedError: + e = get_exception() module.fail_json(msg=str(e), ansible_facts={'vertica_roles': role_facts}) - except CannotDropError, e: + except CannotDropError: + e = get_exception() module.fail_json(msg=str(e), ansible_facts={'vertica_roles': role_facts}) except SystemExit: # avoid catching this on python 2.4 raise - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg=e) module.exit_json(changed=changed, role=role, ansible_facts={'vertica_roles': role_facts}) -# import ansible utilities -from ansible.module_utils.basic import * + if __name__ == '__main__': main() diff --git a/database/vertica/vertica_schema.py b/database/vertica/vertica_schema.py index 39ccb0b60e8..0e849c5df8e 100644 --- a/database/vertica/vertica_schema.py +++ b/database/vertica/vertica_schema.py @@ -117,6 +117,10 @@ else: pyodbc_found = True +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception + + class NotSupportedError(Exception): pass @@ -282,7 +286,8 @@ def main(): module.params['login_user'], module.params['login_password'], 'true') db_conn = pyodbc.connect(dsn, autocommit=True) cursor = db_conn.cursor() - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg="Unable to connect to database: {0}.".format(e)) try: @@ -292,26 +297,30 @@ def main(): elif state == 'absent': try: changed = absent(schema_facts, cursor, schema, usage_roles, create_roles) - except pyodbc.Error, e: + except pyodbc.Error: + e = get_exception() module.fail_json(msg=str(e)) elif state == 'present': try: changed = present(schema_facts, cursor, schema, usage_roles, create_roles, owner) - except pyodbc.Error, e: + except pyodbc.Error: + e = get_exception() module.fail_json(msg=str(e)) - except NotSupportedError, e: + except NotSupportedError: + e = get_exception() module.fail_json(msg=str(e), ansible_facts={'vertica_schemas': schema_facts}) - except CannotDropError, e: + except CannotDropError: + e = get_exception() module.fail_json(msg=str(e), ansible_facts={'vertica_schemas': schema_facts}) except SystemExit: # avoid catching this on python 2.4 raise - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg=e) module.exit_json(changed=changed, schema=schema, ansible_facts={'vertica_schemas': schema_facts}) -# import ansible utilities -from ansible.module_utils.basic import * + if __name__ == '__main__': main() diff --git a/database/vertica/vertica_user.py b/database/vertica/vertica_user.py index 7c52df3163a..29a90429abb 100644 --- a/database/vertica/vertica_user.py +++ b/database/vertica/vertica_user.py @@ -130,6 +130,10 @@ else: pyodbc_found = True +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception + + class NotSupportedError(Exception): pass @@ -351,7 +355,8 @@ def main(): module.params['login_user'], module.params['login_password'], 'true') db_conn = pyodbc.connect(dsn, autocommit=True) cursor = db_conn.cursor() - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg="Unable to connect to database: {0}.".format(e)) try: @@ -362,27 +367,31 @@ def main(): elif state == 'absent': try: changed = absent(user_facts, cursor, user, roles) - except pyodbc.Error, e: + except pyodbc.Error: + e = get_exception() module.fail_json(msg=str(e)) elif state in ['present', 'locked']: try: changed = present(user_facts, cursor, user, profile, resource_pool, locked, password, expired, ldap, roles) - except pyodbc.Error, e: + except pyodbc.Error: + e = get_exception() module.fail_json(msg=str(e)) - except NotSupportedError, e: + except NotSupportedError: + e = get_exception() module.fail_json(msg=str(e), ansible_facts={'vertica_users': user_facts}) - except CannotDropError, e: + except CannotDropError: + e = get_exception() module.fail_json(msg=str(e), ansible_facts={'vertica_users': user_facts}) except SystemExit: # avoid catching this on python 2.4 raise - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg=e) module.exit_json(changed=changed, user=user, ansible_facts={'vertica_users': user_facts}) -# import ansible utilities -from ansible.module_utils.basic import * + if __name__ == '__main__': main() diff --git a/monitoring/boundary_meter.py b/monitoring/boundary_meter.py index 3729b606a1c..d41c2659abd 100644 --- a/monitoring/boundary_meter.py +++ b/monitoring/boundary_meter.py @@ -22,19 +22,6 @@ along with Ansible. If not, see . """ -try: - import json -except ImportError: - try: - import simplejson as json - except ImportError: - # Let snippet from module_utils/basic.py return a proper error in this case - pass - -import datetime -import base64 -import os - DOCUMENTATION = ''' module: boundary_meter @@ -88,15 +75,33 @@ ''' +import base64 +import os + +try: + import json +except ImportError: + try: + import simplejson as json + except ImportError: + # Let snippet from module_utils/basic.py return a proper error in this case + pass + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.urls import fetch_url + + api_host = "api.boundary.com" config_directory = "/etc/bprobe" + # "resource" like thing or apikey? def auth_encode(apikey): auth = base64.standard_b64encode(apikey) auth.replace("\n", "") return auth - + + def build_url(name, apiid, action, meter_id=None, cert_type=None): if action == "create": return 'https://%s/%s/meters' % (api_host, apiid) @@ -198,7 +203,7 @@ def delete_meter(module, name, apiid, apikey): try: cert_file = '%s/%s.pem' % (config_directory,cert_type) os.remove(cert_file) - except OSError, e: + except OSError: module.fail_json("Failed to remove " + cert_type + ".pem file") return 0, "Meter " + name + " deleted" @@ -221,7 +226,7 @@ def download_request(module, name, apiid, apikey, cert_type): cert_file.write(body) cert_file.close() os.chmod(cert_file_path, int('0600', 8)) - except: + except: module.fail_json("Could not write to certificate file") return True @@ -256,9 +261,7 @@ def main(): module.exit_json(status=result,changed=True) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * + if __name__ == '__main__': main() diff --git a/monitoring/circonus_annotation.py b/monitoring/circonus_annotation.py index 9c5fbbb0fd6..1452547a781 100644 --- a/monitoring/circonus_annotation.py +++ b/monitoring/circonus_annotation.py @@ -17,9 +17,6 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -import requests -import time -import json DOCUMENTATION = ''' --- @@ -86,6 +83,15 @@ start_time: 1395940006 end_time: 1395954407 ''' +import json +import time + +import requests + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception + + def post_annotation(annotation, api_key): ''' Takes annotation dict and api_key string''' base_url = 'https://api.circonus.com/v2' @@ -95,6 +101,7 @@ def post_annotation(annotation, api_key): resp.raise_for_status() return resp + def create_annotation(module): ''' Takes ansible module object ''' annotation = {} @@ -116,6 +123,8 @@ def create_annotation(module): annotation['description'] = module.params['description'] annotation['title'] = module.params['title'] return annotation + + def build_headers(api_token): '''Takes api token, returns headers with it included.''' headers = {'X-Circonus-App-Name': 'ansible', @@ -123,6 +132,7 @@ def build_headers(api_token): 'Accept': 'application/json'} return headers + def main(): '''Main function, dispatches logic''' module = AnsibleModule( @@ -139,9 +149,11 @@ def main(): annotation = create_annotation(module) try: resp = post_annotation(annotation, module.params['api_key']) - except requests.exceptions.RequestException, err_str: + except requests.exceptions.RequestException: + err_str = get_exception() module.fail_json(msg='Request Failed', reason=err_str) module.exit_json(changed=True, annotation=resp.json()) -from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py index 208dc73305e..7ed1805c668 100644 --- a/monitoring/datadog_monitor.py +++ b/monitoring/datadog_monitor.py @@ -19,13 +19,6 @@ # along with Ansible. If not, see . # import module snippets -# Import Datadog -try: - from datadog import initialize, api - HAS_DATADOG = True -except: - HAS_DATADOG = False - DOCUMENTATION = ''' --- module: datadog_monitor @@ -144,6 +137,16 @@ app_key: "87ce4a24b5553d2e482ea8a8500e71b8ad4554ff" ''' +# Import Datadog +try: + from datadog import initialize, api + HAS_DATADOG = True +except: + HAS_DATADOG = False + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception + def main(): module = AnsibleModule( @@ -211,7 +214,8 @@ def _post_monitor(module, options): module.fail_json(msg=str(msg['errors'])) else: module.exit_json(changed=True, msg=msg) - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg=str(e)) def _equal_dicts(a, b, ignore_keys): @@ -234,7 +238,8 @@ def _update_monitor(module, monitor, options): module.exit_json(changed=False, msg=msg) else: module.exit_json(changed=True, msg=msg) - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg=str(e)) @@ -269,7 +274,8 @@ def delete_monitor(module): try: msg = api.Monitor.delete(monitor['id']) module.exit_json(changed=True, msg=msg) - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg=str(e)) @@ -288,7 +294,8 @@ def mute_monitor(module): else: msg = api.Monitor.mute(id=monitor['id'], silenced=module.params['silenced']) module.exit_json(changed=True, msg=msg) - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg=str(e)) @@ -301,10 +308,10 @@ def unmute_monitor(module): try: msg = api.Monitor.unmute(monitor['id']) module.exit_json(changed=True, msg=msg) - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg=str(e)) -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * -main() +if __name__ == '__main__': + main() diff --git a/monitoring/rollbar_deployment.py b/monitoring/rollbar_deployment.py index 060193b78a5..5db8626f23b 100644 --- a/monitoring/rollbar_deployment.py +++ b/monitoring/rollbar_deployment.py @@ -78,6 +78,11 @@ import urllib +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import fetch_url + + def main(): module = AnsibleModule( @@ -120,7 +125,8 @@ def main(): try: data = urllib.urlencode(params) response, info = fetch_url(module, url, data=data) - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg='Unable to notify Rollbar: %s' % e) else: if info['status'] == 200: @@ -128,7 +134,6 @@ def main(): else: module.fail_json(msg='HTTP result code: %d connecting to %s' % (info['status'], url)) -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * -main() +if __name__ == '__main__': + main() diff --git a/monitoring/stackdriver.py b/monitoring/stackdriver.py index 25af77ec26e..6b39b0cdf96 100644 --- a/monitoring/stackdriver.py +++ b/monitoring/stackdriver.py @@ -102,6 +102,10 @@ # Let snippet from module_utils/basic.py return a proper error in this case pass +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import fetch_url + def send_deploy_event(module, key, revision_id, deployed_by='Ansible', deployed_to=None, repository=None): """Send a deploy event to Stackdriver""" @@ -195,7 +199,8 @@ def main(): module.fail_json(msg="revision_id required for deploy events") try: send_deploy_event(module, key, revision_id, deployed_by, deployed_to, repository) - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg="unable to sent deploy event: %s" % e) if event == 'annotation': @@ -203,14 +208,13 @@ def main(): module.fail_json(msg="msg required for annotation events") try: send_annotation_event(module, key, msg, annotated_by, level, instance_id, event_epoch) - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg="unable to sent annotation event: %s" % e) changed = True module.exit_json(changed=changed, deployed_by=deployed_by) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * -main() +if __name__ == '__main__': + main() diff --git a/network/cloudflare_dns.py b/network/cloudflare_dns.py index 71cfab22a49..32b953e52de 100644 --- a/network/cloudflare_dns.py +++ b/network/cloudflare_dns.py @@ -18,16 +18,6 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -try: - import json -except ImportError: - try: - import simplejson as json - except ImportError: - # Let snippet from module_utils/basic.py return a proper error in this case - pass -import urllib - DOCUMENTATION = ''' --- module: cloudflare_dns @@ -269,6 +259,22 @@ sample: sample.com ''' +try: + import json +except ImportError: + try: + import simplejson as json + except ImportError: + # Let snippet from module_utils/basic.py return a proper error in this case + pass + +import urllib + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import fetch_url + + class CloudflareAPI(object): cf_api_endpoint = 'https://api.cloudflare.com/client/v4' @@ -315,8 +321,9 @@ def _cf_simple_api_call(self,api_call,method='GET',payload=None): if payload: try: data = json.dumps(payload) - except Exception, e: - self.module.fail_json(msg="Failed to encode payload as JSON: {0}".format(e)) + except Exception: + e = get_exception() + self.module.fail_json(msg="Failed to encode payload as JSON: %s " % str(e)) resp, info = fetch_url(self.module, self.cf_api_endpoint + api_call, @@ -636,9 +643,6 @@ def main(): changed = cf_api.delete_dns_records(solo=False) module.exit_json(changed=changed) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * if __name__ == '__main__': main() diff --git a/network/f5/bigip_gtm_virtual_server.py b/network/f5/bigip_gtm_virtual_server.py index 079709c1b33..13fc8508f9e 100644 --- a/network/f5/bigip_gtm_virtual_server.py +++ b/network/f5/bigip_gtm_virtual_server.py @@ -86,6 +86,10 @@ else: bigsuds_found = True +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.f5 import bigip_api, f5_argument_spec + def server_exists(api, server): # hack to determine if virtual server exists @@ -93,7 +97,8 @@ def server_exists(api, server): try: api.GlobalLB.Server.get_object_status([server]) result = True - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed: + e = get_exception() if "was not found" in str(e): result = False else: @@ -109,7 +114,8 @@ def virtual_server_exists(api, name, server): virtual_server_id = {'name': name, 'server': server} api.GlobalLB.VirtualServerV2.get_object_status([virtual_server_id]) result = True - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed: + e = get_exception() if "was not found" in str(e): result = False else: @@ -222,14 +228,12 @@ def main(): else: result = {'changed': True} - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg="received exception: %s" % e) module.exit_json(**result) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.f5 import * if __name__ == '__main__': main() diff --git a/network/f5/bigip_gtm_wide_ip.py b/network/f5/bigip_gtm_wide_ip.py index 19292783bcd..4067311b4e6 100644 --- a/network/f5/bigip_gtm_wide_ip.py +++ b/network/f5/bigip_gtm_wide_ip.py @@ -71,6 +71,11 @@ else: bigsuds_found = True +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.f5 import bigip_api, f5_argument_spec + + def get_wide_ip_lb_method(api, wide_ip): lb_method = api.GlobalLB.WideIP.get_lb_method(wide_ips=[wide_ip])[0] lb_method = lb_method.strip().replace('LB_METHOD_', '').lower() @@ -79,8 +84,9 @@ def get_wide_ip_lb_method(api, wide_ip): def get_wide_ip_pools(api, wide_ip): try: return api.GlobalLB.WideIP.get_wideip_pool([wide_ip]) - except Exception, e: - print e + except Exception: + e = get_exception() + print(e) def wide_ip_exists(api, wide_ip): # hack to determine if wide_ip exists @@ -88,7 +94,8 @@ def wide_ip_exists(api, wide_ip): try: api.GlobalLB.WideIP.get_object_status(wide_ips=[wide_ip]) result = True - except bigsuds.OperationFailed, e: + except bigsuds.OperationFailed: + e = get_exception() if "was not found" in str(e): result = False else: @@ -145,14 +152,12 @@ def main(): else: result = {'changed': True} - except Exception, e: + except Exception: + e = get_exception() module.fail_json(msg="received exception: %s" % e) module.exit_json(**result) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.f5 import * if __name__ == '__main__': main() diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt index 2abfa3b178d..e69de29bb2d 100644 --- a/test/utils/shippable/sanity-skip-python3.txt +++ b/test/utils/shippable/sanity-skip-python3.txt @@ -1,18 +0,0 @@ -/cloud/misc/virt_net.py -/cloud/misc/virt_pool.py -/cloud/profitbricks/profitbricks.py -/cloud/profitbricks/profitbricks_volume.py -/database/mysql/mysql_replication.py -/database/vertica/vertica_configuration.py -/database/vertica/vertica_facts.py -/database/vertica/vertica_role.py -/database/vertica/vertica_schema.py -/database/vertica/vertica_user.py -/monitoring/boundary_meter.py -/monitoring/circonus_annotation.py -/monitoring/datadog_monitor.py -/monitoring/rollbar_deployment.py -/monitoring/stackdriver.py -/network/cloudflare_dns.py -/network/f5/bigip_gtm_virtual_server.py -/network/f5/bigip_gtm_wide_ip.py From 00628af561ed2e6372a2dfb4d824e0bd9be2a805 Mon Sep 17 00:00:00 2001 From: Kevin Maris Date: Fri, 7 Oct 2016 10:52:01 -0600 Subject: [PATCH 2259/2522] Respect include.* directives by default. --- source_control/git_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source_control/git_config.py b/source_control/git_config.py index 1e229b7bc1b..adf18e99390 100644 --- a/source_control/git_config.py +++ b/source_control/git_config.py @@ -158,7 +158,7 @@ def main(): else: new_value = None - args = [git_path, "config"] + args = [git_path, "config", "--includes"] if params['list_all']: args.append('-l') if scope: From 5ae16821be38d1eb8e57a0adf4dc13029e3c35ae Mon Sep 17 00:00:00 2001 From: jhawkesworth Date: Mon, 24 Oct 2016 14:49:06 +0100 Subject: [PATCH 2260/2522] Win say (#2866) * Add a text-to-speech module for windows. * Fix documentation --- windows/win_say.ps1 | 106 ++++++++++++++++++++++++++++++++++++++++++ windows/win_say.py | 110 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 windows/win_say.ps1 create mode 100644 windows/win_say.py diff --git a/windows/win_say.ps1 b/windows/win_say.ps1 new file mode 100644 index 00000000000..2a1a0c18aa5 --- /dev/null +++ b/windows/win_say.ps1 @@ -0,0 +1,106 @@ +#!powershell +# This file is part of Ansible +# +# Copyright 2016, Jon Hawkesworth (@jhawkesworth) +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# WANT_JSON +# POWERSHELL_COMMON + +$params = Parse-Args $args; +$result = New-Object PSObject; +$msg = Get-AnsibleParam -obj $params -name "msg" +$msg_file = Get-AnsibleParam -obj $params -name "msg_file" +$start_sound_path = Get-AnsibleParam -obj $params -name "start_sound_path" +$end_sound_path = Get-AnsibleParam -obj $params -name "end_sound_path" +$voice = Get-AnsibleParam -obj $params -name "voice" +$speech_speed = Get-AnsibleParam -obj $params -name "speech_speed" +$speed = 0 +$words = $null + +if ($speech_speed -ne $null) { + try { + $speed = [convert]::ToInt32($speech_speed, 10) + } catch { + Fail-Json $result "speech_speed needs to a integer in the range -10 to 10. The value $speech_speed could not be converted to an integer." + + } + if ($speed -lt -10 -or $speed -gt 10) { + Fail-Json $result "speech_speed needs to a integer in the range -10 to 10. The value $speech_speed is outside this range." + } +} + + +if ($msg_file -ne $null -and $msg -ne $null ) { + Fail-Json $result "Please specify either msg_file or msg parameters, not both" +} + +if ($msg_file -eq $null -and $msg -eq $null -and $start_sound_path -eq $null -and $end_sound_path -eq $null) { + Fail-Json $result "No msg_file, msg, start_sound_path, or end_sound_path parameters have been specified. Please specify at least one so the module has something to do" + +} + + +if ($msg_file -ne $null) { + if (Test-Path $msg_file) { + $words = Get-Content $msg_file | Out-String + } else { + Fail-Json $result "Message file $msg_file could not be found or opened. Ensure you have specified the full path to the file, and the ansible windows user has permission to read the file." + } +} + +if ($start_sound_path -ne $null) { + if (Test-Path $start_sound_path) { + (new-object Media.SoundPlayer $start_sound_path).playSync(); + } else { + Fail-Json $result "Start sound file $start_sound_path could not be found or opened. Ensure you have specified the full path to the file, and the ansible windows user has permission to read the file." + } +} + +if ($msg -ne $null) { + $words = $msg +} + +if ($words -ne $null) { + Add-Type -AssemblyName System.speech + $tts = New-Object System.Speech.Synthesis.SpeechSynthesizer + if ($voice -ne $null) { + try { + $tts.SelectVoice($voice) + } catch [System.Management.Automation.MethodInvocationException] { + Set-Attr $result "voice_info" "Could not load voice $voice, using system default voice." + } + } + + Set-Attr $result "voice" $tts.Voice.Name + if ($speed -ne 0) { + $tts.Rate = $speed + } + $tts.Speak($words) + $tts.Dispose() +} + +if ($end_sound_path -ne $null) { + if (Test-Path $end_sound_path) { + (new-object Media.SoundPlayer $end_sound_path).playSync(); + } else { + Fail-Json $result "End sound file $start_sound_path could not be found or opened. Ensure you have specified the full path to the file, and the ansible windows user has permission to read the file." + } +} + +Set-Attr $result "changed" $false; +Set-Attr $result "message_text" $words; + +Exit-Json $result; diff --git a/windows/win_say.py b/windows/win_say.py new file mode 100644 index 00000000000..e82cf0f9780 --- /dev/null +++ b/windows/win_say.py @@ -0,0 +1,110 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Jon Hawkesworth (@jhawkesworth) +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# this is a windows documentation stub. actual code lives in the .ps1 +# file of the same name + +DOCUMENTATION = ''' +--- +module: win_say +version_added: "2.2" +short_description: Text to speech module for Windows to speak messages and optionally play sounds +description: + - Uses .NET libraries to convert text to speech and optionally play .wav sounds. Audio Service needs to be running and some kind of speakers or headphones need to be attached to the windows target(s) for the speech to be audible. +options: + msg: + description: + - The text to be spoken. Use either msg or msg_file. Optional so that you can use this module just to play sounds. + required: false + default: none + msg_file: + description: + - Full path to a windows format text file containing the text to be spokend. Use either msg or msg_file. Optional so that you can use this module just to play sounds. + required: false + default: none + voice: + description: + - Which voice to use. See notes for how to discover installed voices. If the requested voice is not available the default voice will be used. Example voice names from Windows 10 are 'Microsoft Zira Desktop' and 'Microsoft Hazel Desktop'. + required: false + default: system default voice + speech_speed: + description: + - How fast or slow to speak the text. Must be an integer value in the range -10 to 10. -10 is slowest, 10 is fastest. + required: false + default: 0 + start_sound_path: + description: + - Full path to a C(.wav) file containing a sound to play before the text is spoken. Useful on conference calls to alert other speakers that ansible has something to say. + required: false + default: null + end_sound_path: + description: + - Full path to a C(.wav) file containing a sound to play after the text has been spoken. Useful on conference calls to alert other speakers that ansible has finished speaking. + required: false + default: null +author: "Jon Hawkesworth (@jhawkesworth)" +notes: + - Needs speakers or headphones to do anything useful. + - To find which voices are installed, run the following powershell + Add-Type -AssemblyName System.Speech + $speech = New-Object -TypeName System.Speech.Synthesis.SpeechSynthesizer + $speech.GetInstalledVoices() | ForEach-Object { $_.VoiceInfo } + $speech.Dispose() + - Speech can be surprisingly slow, so its best to keep message text short. +''' + +EXAMPLES = ''' + # Warn of impending deployment +- win_say: + msg: Warning, deployment commencing in 5 minutes, please log out. + # Using a different voice and a start sound +- win_say: + start_sound_path: 'C:\Windows\Media\ding.wav' + msg: Warning, deployment commencing in 5 minutes, please log out. + voice: Microsoft Hazel Desktop + # example with start and end sound +- win_say: + start_sound_path: 'C:\Windows\Media\Windows Balloon.wav' + msg: "New software installed" + end_sound_path: 'C:\Windows\Media\chimes.wav' + # text from file example +- win_say: + start_sound_path: 'C:\Windows\Media\Windows Balloon.wav' + msg_text: AppData\Local\Temp\morning_report.txt + end_sound_path: 'C:\Windows\Media\chimes.wav' +''' +RETURN = ''' +message_text: + description: the text that the module attempted to speak + returned: success + type: string + sample: "Warning, deployment commencing in 5 minutes." +voice: + description: the voice used to speak the text. + returned: success + type: string + sample: Microsoft Hazel Desktop +voice_info: + description: the voice used to speak the text. + returned: when requested voice could not be loaded + type: string + sample: Could not load voice TestVoice, using system default voice +''' + From 8ffe314ea55798c3af8b8a2e74782c190a19ef55 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 24 Oct 2016 09:49:31 -0400 Subject: [PATCH 2261/2522] corrected version --- windows/win_say.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_say.py b/windows/win_say.py index e82cf0f9780..8ab159912b7 100644 --- a/windows/win_say.py +++ b/windows/win_say.py @@ -24,7 +24,7 @@ DOCUMENTATION = ''' --- module: win_say -version_added: "2.2" +version_added: "2.3" short_description: Text to speech module for Windows to speak messages and optionally play sounds description: - Uses .NET libraries to convert text to speech and optionally play .wav sounds. Audio Service needs to be running and some kind of speakers or headphones need to be attached to the windows target(s) for the speech to be audible. From 8985a1fbd9a22ca89e07f7c4dd75512d3a326d1d Mon Sep 17 00:00:00 2001 From: YAEGASHI Takeshi Date: Mon, 24 Oct 2016 23:30:46 +0900 Subject: [PATCH 2262/2522] blockinfile: Add a newline at EOF when the file is newly created (#3174) Ref: #2687 --- files/blockinfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/blockinfile.py b/files/blockinfile.py index 8ba76181886..30fe77d9244 100755 --- a/files/blockinfile.py +++ b/files/blockinfile.py @@ -285,7 +285,7 @@ def main(): if lines: result = '\n'.join(lines) - if original and original.endswith('\n'): + if original is None or original.endswith('\n'): result += '\n' else: result = '' From 8f47c0d71bdf407bd15b10ff03a8161e5e7f72de Mon Sep 17 00:00:00 2001 From: Steve Kuznetsov Date: Mon, 24 Oct 2016 15:34:29 -0400 Subject: [PATCH 2263/2522] make: Expose std{out,err} of successful make commands (#3074) When using the `make:` extras module, often the action taken by the Make target is large. It is useful, therefore, to see the output that `make` had to std{out,err} during execution even when the target did not fail. Signed-off-by: Steve Kuznetsov --- system/make.py | 102 +++++++++++++++++++++++++++++++------------------ 1 file changed, 64 insertions(+), 38 deletions(-) diff --git a/system/make.py b/system/make.py index ee8d07be744..41aa4faf28a 100644 --- a/system/make.py +++ b/system/make.py @@ -66,27 +66,33 @@ RETURN = '''# ''' -def format_params(params): - return [k + '=' + str(v) for k, v in params.iteritems()] - - -def push_arguments(cmd, args): - if args['target'] != None: - cmd.append(args['target']) - if args['params'] != None: - cmd.extend(format_params(args['params'])) - return cmd - - -def check_changed(make_path, module, args): - cmd = push_arguments([make_path, '--question'], args) - rc, _, __ = module.run_command(cmd, check_rc=False, cwd=args['chdir']) - return (rc != 0) - - -def run_make(make_path, module, args): - cmd = push_arguments([make_path], args) - module.run_command(cmd, check_rc=True, cwd=args['chdir']) +def run_command(command, module, check_rc=True): + """ + Run a command using the module, return + the result code and std{err,out} content. + + :param command: list of command arguments + :param module: Ansible make module instance + :return: return code, stdout content, stderr content + """ + rc, out, err = module.run_command(command, check_rc=check_rc, cwd=module.params['chdir']) + return rc, sanitize_output(out), sanitize_output(err) + + +def sanitize_output(output): + """ + Sanitize the output string before we + pass it to module.fail_json. Defaults + the string to empty if it is None, else + strips trailing newlines. + + :param output: output to sanitize + :return: sanitized output + """ + if output is None: + return b('') + else: + return output.rstrip(b("\r\n")) def main(): @@ -98,28 +104,48 @@ def main(): chdir=dict(required=True, default=None, type='str'), ), ) - args = dict( - changed=False, - failed=False, - target=module.params['target'], - params=module.params['params'], - chdir=module.params['chdir'], - ) + # Build up the invocation of `make` we are going to use make_path = module.get_bin_path('make', True) + make_target = module.params['target'] + if module.params['params'] is not None: + make_parameters = [k + '=' + str(v) for k, v in module.params['params'].iteritems()] + else: + make_parameters = [] - # Check if target is up to date - args['changed'] = check_changed(make_path, module, args) + base_command = [make_path, make_target] + base_command.extend(make_parameters) - # Check only; don't modify + # Check if the target is already up to date + rc, out, err = run_command(base_command + ['--question'], module, check_rc=False) if module.check_mode: - module.exit_json(changed=args['changed']) - - # Target is already up to date - if args['changed'] == False: - module.exit_json(**args) + # If we've been asked to do a dry run, we only need + # to report whether or not the target is up to date + changed = (rc != 0) + else: + if rc == 0: + # The target is up to date, so we don't have to + # do anything + changed = False + else: + # The target isn't upd to date, so we need to run it + rc, out, err = run_command(base_command, module) + changed = True + + # We don't report the return code, as if this module failed + # we would be calling fail_json from run_command, so even if + # we had a non-zero return code, we did not fail. However, if + # we report a non-zero return code here, we will be marked as + # failed regardless of what we signal using the failed= kwarg. + module.exit_json( + changed=changed, + failed=False, + stdout=out, + stderr=err, + target=module.params['target'], + params=module.params['params'], + chdir=module.params['chdir'] + ) - run_make(make_path, module, args) - module.exit_json(**args) from ansible.module_utils.basic import * From fa82c27b2e3181c96b53bc925b7a4d36b34ad78b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc?= Date: Mon, 24 Oct 2016 21:45:24 +0200 Subject: [PATCH 2264/2522] New module ec2_lc_facts (#2325) New module to gather facts about AWS Autoscaling Launch Configurations --- cloud/amazon/ec2_lc_facts.py | 225 +++++++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 cloud/amazon/ec2_lc_facts.py diff --git a/cloud/amazon/ec2_lc_facts.py b/cloud/amazon/ec2_lc_facts.py new file mode 100644 index 00000000000..0e2aa034ebb --- /dev/null +++ b/cloud/amazon/ec2_lc_facts.py @@ -0,0 +1,225 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# This is a free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This Ansible library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this library. If not, see . + +DOCUMENTATION = ''' +--- +module: ec2_lc_facts +short_description: Gather facts about AWS Autoscaling Launch Configurations +description: + - Gather facts about AWS Autoscaling Launch Configurations +version_added: "2.2" +author: "Loïc Latreille (@psykotox)" +requirements: [ boto3 ] +options: + name: + description: + - A name or a list of name to match. + required: false + default: [] + sort: + description: + - Optional attribute which with to sort the results. + choices: ['launch_configuration_name', 'image_id', 'created_time', 'instance_type', 'kernel_id', 'ramdisk_id', 'key_name'] + default: null + required: false + sort_order: + description: + - Order in which to sort results. + - Only used when the 'sort' parameter is specified. + choices: ['ascending', 'descending'] + default: 'ascending' + required: false + sort_start: + description: + - Which result to start with (when sorting). + - Corresponds to Python slice notation. + default: null + required: false + sort_end: + description: + - Which result to end with (when sorting). + - Corresponds to Python slice notation. + default: null + required: false +extends_documentation_fragment: + - aws + - ec2 +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Gather facts about all launch configurations +- ec2_lc_facts: + +# Gather facts about launch configuration with name "example" +- ec2_lc_facts: + name: example + +# Gather facts sorted by created_time from most recent to least recent +- ec2_lc_facts: + sort: created_time + sort_order: descending +''' + +RETURN = ''' +block_device_mapping: + description: Block device mapping for the instances of launch configuration + type: list of block devices + sample: "[{ + 'device_name': '/dev/xvda':, + 'ebs': { + 'delete_on_termination': true, + 'volume_size': 8, + 'volume_type': 'gp2' + }]" +classic_link_vpc_security_groups: + description: IDs of one or more security groups for the VPC specified in classic_link_vpc_id + type: string + sample: +created_time: + description: The creation date and time for the launch configuration + type: string + sample: "2016-05-27T13:47:44.216000+00:00" +ebs_optimized: + description: EBS I/O optimized (true ) or not (false ) + type: bool + sample: true, +image_id: + description: ID of the Amazon Machine Image (AMI) + type: string + sample: "ami-12345678" +instance_monitoring: + description: Launched with detailed monitoring or not + type: dict + sample: "{ + 'enabled': true + }" +instance_type: + description: Instance type + type: string + sample: "t2.micro" +kernel_id: + description: ID of the kernel associated with the AMI + type: string + sample: +key_name: + description: Name of the key pair + type: string + sample: "user_app" +launch_configuration_arn: + description: Amazon Resource Name (ARN) of the launch configuration + type: string + sample: "arn:aws:autoscaling:us-east-1:666612345678:launchConfiguration:ba785e3a-dd42-6f02-4585-ea1a2b458b3d:launchConfigurationName/lc-app" +launch_configuration_name: + description: Name of the launch configuration + type: string + sample: "lc-app" +ramdisk_id: + description: ID of the RAM disk associated with the AMI + type: string + sample: +security_groups: + description: Security groups to associated + type: list + sample: "[ + 'web' + ]" +user_data: + description: User data available + type: string + sample: +''' + +try: + import boto3 + from botocore.exceptions import ClientError, NoCredentialsError + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + + +def list_launch_configs(connection, module): + + launch_config_name = module.params.get("name") + sort = module.params.get('sort') + sort_order = module.params.get('sort_order') + sort_start = module.params.get('sort_start') + sort_end = module.params.get('sort_end') + + try: + launch_configs = connection.describe_launch_configurations(LaunchConfigurationNames=launch_config_name) + except ClientError as e: + module.fail_json(msg=e.message) + + snaked_launch_configs = [] + for launch_config in launch_configs['LaunchConfigurations']: + snaked_launch_configs.append(camel_dict_to_snake_dict(launch_config)) + + for launch_config in snaked_launch_configs: + if 'CreatedTime' in launch_config: + launch_config['CreatedTime'] = str(launch_config['CreatedTime']) + + if sort: + snaked_launch_configs.sort(key=lambda e: e[sort], reverse=(sort_order=='descending')) + + try: + if sort and sort_start and sort_end: + snaked_launch_configs = snaked_launch_configs[int(sort_start):int(sort_end)] + elif sort and sort_start: + snaked_launch_configs = snaked_launch_configs[int(sort_start):] + elif sort and sort_end: + snaked_launch_configs = snaked_launch_configs[:int(sort_end)] + except TypeError: + module.fail_json(msg="Please supply numeric values for sort_start and/or sort_end") + + module.exit_json(launch_configurations=snaked_launch_configs) + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + name = dict(required=False, default=[], type='list'), + sort = dict(required=False, default=None, + choices=['launch_configuration_name', 'image_id', 'created_time', 'instance_type', 'kernel_id', 'ramdisk_id', 'key_name']), + sort_order = dict(required=False, default='ascending', + choices=['ascending', 'descending']), + sort_start = dict(required=False), + sort_end = dict(required=False), + ) + ) + + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO3: + module.fail_json(msg='boto3 required for this module') + + region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) + + if region: + connection = boto3_conn(module, conn_type='client', resource='autoscaling', region=region, endpoint=ec2_url, **aws_connect_params) + else: + module.fail_json(msg="region must be specified") + + list_launch_configs(connection, module) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From 1ead85f4743cd549e284550ef656660eb7cc49fe Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Mon, 24 Oct 2016 21:48:47 +0200 Subject: [PATCH 2265/2522] docs: ec2_lc_facts: adjust version_added --- cloud/amazon/ec2_lc_facts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/ec2_lc_facts.py b/cloud/amazon/ec2_lc_facts.py index 0e2aa034ebb..ea94b749f4a 100644 --- a/cloud/amazon/ec2_lc_facts.py +++ b/cloud/amazon/ec2_lc_facts.py @@ -20,7 +20,7 @@ short_description: Gather facts about AWS Autoscaling Launch Configurations description: - Gather facts about AWS Autoscaling Launch Configurations -version_added: "2.2" +version_added: "2.3" author: "Loïc Latreille (@psykotox)" requirements: [ boto3 ] options: @@ -136,7 +136,7 @@ description: Security groups to associated type: list sample: "[ - 'web' + 'web' ]" user_data: description: User data available From 1dc0936f8ffcaf5765e04ea7c9584ef2fce9686d Mon Sep 17 00:00:00 2001 From: Georg Date: Fri, 21 Oct 2016 16:57:45 +0300 Subject: [PATCH 2266/2522] Update to firewalld doc Missinformation about where available firewalld services are listed --- system/firewalld.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/firewalld.py b/system/firewalld.py index c069896863e..f3ba6eb065a 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -28,7 +28,7 @@ options: service: description: - - "Name of a service to add/remove to/from firewalld - service must be listed in /etc/services." + - "Name of a service to add/remove to/from firewalld - service must be listed in output of firewall-cmd --get-services." required: false default: null port: From 3aeb25069db1d224d57135ec50101afe68472036 Mon Sep 17 00:00:00 2001 From: John Barker Date: Mon, 24 Oct 2016 14:29:18 +0100 Subject: [PATCH 2267/2522] Conditional main() This is needed to allow the debugger work --- network/a10/a10_server.py | 3 ++- network/a10/a10_service_group.py | 3 ++- network/a10/a10_virtual_server.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/network/a10/a10_server.py b/network/a10/a10_server.py index d06a2a661af..c89389e2c0c 100644 --- a/network/a10/a10_server.py +++ b/network/a10/a10_server.py @@ -291,4 +291,5 @@ def status_needs_update(current_status, new_status): from ansible.module_utils.urls import * from ansible.module_utils.a10 import * -main() +if __name__ == '__main__': + main() diff --git a/network/a10/a10_service_group.py b/network/a10/a10_service_group.py index af664084b6a..4b06ad20118 100644 --- a/network/a10/a10_service_group.py +++ b/network/a10/a10_service_group.py @@ -338,4 +338,5 @@ def main(): from ansible.module_utils.urls import * from ansible.module_utils.a10 import * -main() +if __name__ == '__main__': + main() diff --git a/network/a10/a10_virtual_server.py b/network/a10/a10_virtual_server.py index 1a04f1a1754..cc0f537a981 100644 --- a/network/a10/a10_virtual_server.py +++ b/network/a10/a10_virtual_server.py @@ -292,6 +292,6 @@ def needs_update(src_ports, dst_ports): from ansible.module_utils.basic import * from ansible.module_utils.urls import * from ansible.module_utils.a10 import * + if __name__ == '__main__': main() - From 3db76b820718450e49ccb1880b9f9323ead3bab0 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Mon, 24 Oct 2016 09:05:01 +0200 Subject: [PATCH 2268/2522] Make blockinfile work with python3 Traceback (most recent call last): File \"/tmp/ansible_ueg52c0b/ansible_module_blockinfile.py\", line 319, in main() File \"/tmp/ansible_ueg52c0b/ansible_module_blockinfile.py\", line 259, in main if line.startswith(marker0): TypeError: startswith first arg must be bytes or a tuple of bytes, not str Also clean imports while on it. --- files/blockinfile.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/files/blockinfile.py b/files/blockinfile.py index 30fe77d9244..96f430cf14a 100755 --- a/files/blockinfile.py +++ b/files/blockinfile.py @@ -149,7 +149,9 @@ import re import os import tempfile - +from ansible.module_utils.six import b +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_bytes def write_changes(module, contents, dest): @@ -227,8 +229,8 @@ def main(): insertbefore = params['insertbefore'] insertafter = params['insertafter'] - block = params['block'] - marker = params['marker'] + block = to_bytes(params['block']) + marker = to_bytes(params['marker']) present = params['state'] == 'present' if not present and not path_exists: @@ -244,8 +246,8 @@ def main(): else: insertre = None - marker0 = re.sub(r'{mark}', 'BEGIN', marker) - marker1 = re.sub(r'{mark}', 'END', marker) + marker0 = re.sub(b(r'{mark}'), b('BEGIN'), marker) + marker1 = re.sub(b(r'{mark}'), b('END'), marker) if present and block: # Escape seqeuences like '\n' need to be handled in Ansible 1.x if module.ansible_version.startswith('1.'): @@ -284,9 +286,9 @@ def main(): lines[n0:n0] = blocklines if lines: - result = '\n'.join(lines) - if original is None or original.endswith('\n'): - result += '\n' + result = b('\n').join(lines) + if original is None or original.endswith(b('\n')): + result += b('\n') else: result = '' if original == result: @@ -313,7 +315,6 @@ def main(): msg, changed = check_file_attrs(module, changed, msg) module.exit_json(changed=changed, msg=msg) -# import module snippets -from ansible.module_utils.basic import * + if __name__ == '__main__': main() From 9a9e92d1531c984573836fc34582759df039e313 Mon Sep 17 00:00:00 2001 From: jctanner Date: Mon, 24 Oct 2016 23:55:56 -0400 Subject: [PATCH 2269/2522] Implement a workaround for broken FindByInventoryPath method in pyvmomi (#3243) * Add initial support for using a cluster instead of an esxi hostname * FindByInventoryPath doesn't always work, so implement a fallback method to match the path --- cloud/vmware/vmware_guest.py | 55 ++++++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/cloud/vmware/vmware_guest.py b/cloud/vmware/vmware_guest.py index 3d7ab028e88..751cac93856 100644 --- a/cloud/vmware/vmware_guest.py +++ b/cloud/vmware/vmware_guest.py @@ -281,6 +281,22 @@ def getfolders(self): self.folder_map = self._build_folder_map(self.folders) return (self.folders, self.folder_map) + def compile_folder_path_for_object(self, vobj): + ''' make a /vm/foo/bar/baz like folder path for an object ''' + paths = [] + if type(vobj) == vim.Folder: + paths.append(vobj.name) + + thisobj = vobj + while hasattr(thisobj, 'parent'): + thisobj = thisobj.parent + if type(thisobj) == vim.Folder: + paths.append(thisobj.name) + paths.reverse() + if paths[0] == 'Datacenters': + paths.remove('Datacenters') + return '/' + '/'.join(paths) + def get_datacenter(self): self.datacenter = get_obj(self.content, [vim.Datacenter], self.params['datacenter']) @@ -292,6 +308,7 @@ def getvm(self, name=None, uuid=None, folder=None, name_match=None): vm = None folder_path = None + searchpath = None if uuid: vm = self.content.searchIndex.FindByUuid(uuid=uuid, vmSearch=True) @@ -302,7 +319,6 @@ def getvm(self, name=None, uuid=None, folder=None, name_match=None): self.params['folder'] = self.params['folder'][0:-1] # Build the absolute folder path to pass into the search method - searchpath = None if self.params['folder'].startswith('/vm'): searchpath = '%s' % self.params['datacenter'] searchpath += self.params['folder'] @@ -333,9 +349,24 @@ def getvm(self, name=None, uuid=None, folder=None, name_match=None): vm = cObj break - else: + if not vm: + # FIXME - this is unused if folder has a default value vmList = get_all_objs(self.content, [vim.VirtualMachine]) + + # narrow down by folder + if folder: + if not self.folders: + self.getfolders() + + # compare the folder path of each VM against the search path + for item in vmList.items(): + vobj = item[0] + if not type(vobj.parent) == vim.Folder: + continue + if self.compile_folder_path_for_object(vobj) == searchpath: + return vobj + if name_match: if name_match == 'first': vm = get_obj(self.content, [vim.VirtualMachine], name) @@ -519,9 +550,18 @@ def deploy_template(self, poweron=False, wait_for_ip=False): # grab the folder vim object destfolder = folders[0][1] - # FIXME: cluster or hostsystem ... ? - #cluster = get_obj(self.content, [vim.ClusterComputeResource], self.params['esxi']['hostname']) - hostsystem = get_obj(self.content, [vim.HostSystem], self.params['esxi_hostname']) + # if the user wants a cluster, get the list of hosts for the cluster and use the first one + if self.params['cluster']: + cluster = get_obj(self.content, [vim.ClusterComputeResource], self.params['cluster']) + if not cluster: + self.module.fail_json(msg="Failed to find a cluster named %s" % self.params['cluster']) + #resource_pool = cluster.resourcePool + hostsystems = [x for x in cluster.host] + hostsystem = hostsystems[0] + else: + hostsystem = get_obj(self.content, [vim.HostSystem], self.params['esxi_hostname']) + if not hostsystem: + self.module.fail_json(msg="Failed to find a host named %s" % self.params['esxi_hostname']) # set the destination datastore in the relocation spec datastore_name = None @@ -548,6 +588,10 @@ def deploy_template(self, poweron=False, wait_for_ip=False): resource_pool = None resource_pools = get_all_objs(self.content, [vim.ResourcePool]) for rp in resource_pools.items(): + if not rp[0]: + continue + if not hasattr(rp[0], 'parent'): + continue if rp[0].parent == hostsystem.parent: resource_pool = rp[0] break @@ -887,6 +931,7 @@ def main(): force=dict(required=False, type='bool', default=False), datacenter=dict(required=False, type='str', default=None), esxi_hostname=dict(required=False, type='str', default=None), + cluster=dict(required=False, type='str', default=None), wait_for_ip_address=dict(required=False, type='bool', default=True) ), supports_check_mode=True, From a18578e95d124e383545c551599f17d93b761a19 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 25 Oct 2016 00:17:38 -0400 Subject: [PATCH 2270/2522] Add TLS encyrption support to MQTT (#2700) This commit adds module settings for configuring TLS encyption on the mqtt notification module. Previously there was no way to configure sending the messages encrpyted to mqtt. --- notification/mqtt.py | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/notification/mqtt.py b/notification/mqtt.py index 89fa50a7dbd..4a6e1084815 100644 --- a/notification/mqtt.py +++ b/notification/mqtt.py @@ -75,6 +75,36 @@ retained message immediately. required: false default: False + ca_certs: + description: + - The path to the Certificate Authority certificate files that are to be + treated as trusted by this client. If this is the only option given + then the client will operate in a similar manner to a web browser. That + is to say it will require the broker to have a certificate signed by the + Certificate Authorities in ca_certs and will communicate using TLS v1, + but will not attempt any form of authentication. This provides basic + network encryption but may not be sufficient depending on how the broker + is configured. + required: False + default: None + version_added: 2.3 + certfile: + description: + - The path pointing to the PEM encoded client certificate. If this is not + None it will be used as client information for TLS based + authentication. Support for this feature is broker dependent. + required: False + default: None + version_added: 2.3 + keyfile: + description: + - The path pointing to the PEM encoded client private key. If this is not + None it will be used as client information for TLS based + authentication. Support for this feature is broker dependent. + required: False + default: None + version_added: 2.3 + # informational: requirements for nodes requirements: [ mosquitto ] @@ -121,6 +151,9 @@ def main(): retain = dict(default=False, type='bool'), username = dict(default = None), password = dict(default = None, no_log=True), + ca_certs = dict(default = None, type='path'), + certfile = dict(default = None, type='path'), + keyfile = dict(default = None, type='path'), ), supports_check_mode=True ) @@ -137,6 +170,9 @@ def main(): retain = module.params.get("retain") username = module.params.get("username", None) password = module.params.get("password", None) + ca_certs = module.params.get("ca_certs", None) + certfile = module.params.get("certfile", None) + keyfile = module.params.get("keyfile", None) if client_id is None: client_id = "%s_%s" % (socket.getfqdn(), os.getpid()) @@ -148,6 +184,11 @@ def main(): if username is not None: auth = { 'username' : username, 'password' : password } + tls=None + if ca_certs is not None: + tls = {'ca_certs': ca_certs, 'certfile': certfile, + 'keyfile': keyfile} + try: rc = mqtt.single(topic, payload, qos=qos, @@ -155,7 +196,8 @@ def main(): client_id=client_id, hostname=server, port=port, - auth=auth) + auth=auth, + tls=tls) except Exception: e = get_exception() module.fail_json(msg="unable to publish to MQTT broker %s" % (e)) From 577e86bd2be31c17fe80bb69887b183110834251 Mon Sep 17 00:00:00 2001 From: Pitsanu Swangpheaw Date: Tue, 25 Oct 2016 11:30:47 +0700 Subject: [PATCH 2271/2522] support proxmox resource pool (#2859) --- cloud/misc/proxmox.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cloud/misc/proxmox.py b/cloud/misc/proxmox.py index 0ba88186e64..709f3e0dc77 100644 --- a/cloud/misc/proxmox.py +++ b/cloud/misc/proxmox.py @@ -55,6 +55,12 @@ - for another states will be autodiscovered default: null required: false + pool: + description: + - Proxmox VE resource pool + default: null + required: false + version_added: "2.3" password: description: - the instance root password @@ -306,6 +312,7 @@ def main(): vmid = dict(required=True), validate_certs = dict(type='bool', default='no'), node = dict(), + pool = dict(), password = dict(no_log=True), hostname = dict(), ostemplate = dict(), @@ -374,6 +381,7 @@ def main(): % (module.params['ostemplate'], node, template_store)) create_instance(module, proxmox, vmid, node, disk, storage, cpus, memory, swap, timeout, + pool = module.params['pool'], password = module.params['password'], hostname = module.params['hostname'], ostemplate = module.params['ostemplate'], From 4b5cee5bada0949f0a8822ee24dc627a18c18ae9 Mon Sep 17 00:00:00 2001 From: Rowan Date: Tue, 25 Oct 2016 05:36:34 +0100 Subject: [PATCH 2272/2522] Added proxied option to cloudflare_dns (#2961) --- network/cloudflare_dns.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/network/cloudflare_dns.py b/network/cloudflare_dns.py index 32b953e52de..f0d933b1818 100644 --- a/network/cloudflare_dns.py +++ b/network/cloudflare_dns.py @@ -50,6 +50,11 @@ required: false choices: [ 'tcp', 'udp' ] default: null + proxied: + description: Proxy through cloudflare network or just use DNS + required: false + default: no + version_added: "2.2" record: description: - Record to add. Required if C(state=present). Default is C(@) (e.g. the zone name) @@ -144,6 +149,16 @@ account_email: test@example.com account_api_token: dummyapitoken +# create a my.com CNAME record to example.com and proxy through cloudflare's network +- cloudflare_dns: + zone: my.com + type: CNAME + value: example.com + state: present + proxied: yes + account_email: test@example.com + account_api_token: dummyapitoken + # create TXT record "test.my.com" with value "unique value" # delete all other TXT records named "test.my.com" - cloudflare_dns: @@ -287,6 +302,7 @@ def __init__(self, module): self.port = module.params['port'] self.priority = module.params['priority'] self.proto = module.params['proto'] + self.proxied = module.params['proxied'] self.record = module.params['record'] self.service = module.params['service'] self.is_solo = module.params['solo'] @@ -493,7 +509,7 @@ def delete_dns_records(self,**kwargs): def ensure_dns_record(self,**kwargs): params = {} - for param in ['port','priority','proto','service','ttl','type','record','value','weight','zone']: + for param in ['port','priority','proto','proxied','service','ttl','type','record','value','weight','zone']: if param in kwargs: params[param] = kwargs[param] else: @@ -523,6 +539,9 @@ def ensure_dns_record(self,**kwargs): "ttl": params['ttl'] } + if (params['type'] in [ 'A', 'AAAA', 'CNAME' ]): + new_record["proxied"] = params["proxied"] + if params['type'] == 'MX': for attr in [params['priority'],params['value']]: if (attr is None) or (attr == ''): @@ -591,6 +610,7 @@ def main(): port = dict(required=False, default=None, type='int'), priority = dict(required=False, default=1, type='int'), proto = dict(required=False, default=None, choices=[ 'tcp', 'udp' ], type='str'), + proxied = dict(required=False, default=False, type='bool'), record = dict(required=False, default='@', aliases=['name'], type='str'), service = dict(required=False, default=None, type='str'), solo = dict(required=False, default=None, type='bool'), From 0aa433925b6fd0dd5aeb9f8a5fdf0b20e4fdfe02 Mon Sep 17 00:00:00 2001 From: Andrea Scarpino Date: Tue, 25 Oct 2016 06:38:14 +0200 Subject: [PATCH 2273/2522] maven_artifact: Allow to specify a custom timeout (#2526) --- packaging/language/maven_artifact.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packaging/language/maven_artifact.py b/packaging/language/maven_artifact.py index 1136f7aaaff..f79f3175cba 100644 --- a/packaging/language/maven_artifact.py +++ b/packaging/language/maven_artifact.py @@ -102,6 +102,12 @@ required: true default: present choices: [present,absent] + timeout: + description: + - Specifies a timeout in seconds for the connection attempt + required: false + default: 10 + version_added: '2.2' validate_certs: description: - If C(no), SSL certificates will not be validated. This should only be set to C(no) when no other option exists. @@ -240,12 +246,14 @@ def _request(self, url, failmsg, f): client = boto3.client('s3',aws_access_key_id=self.module.params.get('username', ''), aws_secret_access_key=self.module.params.get('password', '')) url_to_use = client.generate_presigned_url('get_object',Params={'Bucket':bucket_name,'Key':key_name},ExpiresIn=10) + req_timeout = self.module.params.get('timeout') + # Hack to add parameters in the way that fetch_url expects self.module.params['url_username'] = self.module.params.get('username', '') self.module.params['url_password'] = self.module.params.get('password', '') self.module.params['http_agent'] = self.module.params.get('user_agent', None) - response, info = fetch_url(self.module, url_to_use) + response, info = fetch_url(self.module, url_to_use, timeout=req_timeout) if info['status'] != 200: raise ValueError(failmsg + " because of " + info['msg'] + "for URL " + url_to_use) else: @@ -328,6 +336,7 @@ def main(): username = dict(default=None,aliases=['aws_secret_key']), password = dict(default=None, no_log=True,aliases=['aws_secret_access_key']), state = dict(default="present", choices=["present","absent"]), # TODO - Implement a "latest" state + timeout = dict(default=10, type='int'), dest = dict(type="path", default=None), validate_certs = dict(required=False, default=True, type='bool'), ) From 1ca889bc738c9419f7409171531144f5adf258f9 Mon Sep 17 00:00:00 2001 From: "Thierno IB. BARRY" Date: Tue, 25 Oct 2016 06:46:21 +0200 Subject: [PATCH 2274/2522] openvswitch_bridge: add fake bridge support (#3054) * openvswitch_bridge: add fake bridge support * openvswitch_bridge: check if vlan is between 0 and 4095 --- network/openvswitch_bridge.py | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/network/openvswitch_bridge.py b/network/openvswitch_bridge.py index abe89dfdd42..5d44ff070f8 100644 --- a/network/openvswitch_bridge.py +++ b/network/openvswitch_bridge.py @@ -35,7 +35,19 @@ bridge: required: true description: - - Name of bridge to manage + - Name of bridge or fake bridge to manage + parent: + version_added: 2.2 + required: false + default: None + description: + - Bridge parent of the fake bridge to manage + vlan: + version_added: 2.2 + required: false + default: None + description: + - The VLAN id of the fake bridge to manage (must be between 0 and 4095) state: required: false default: "present" @@ -67,6 +79,9 @@ # Create a bridge named br-int - openvswitch_bridge: bridge=br-int state=present +# Create a fake bridge named br-int within br-parent on the VLAN 405 +- openvswitch_bridge: bridge=br-int parent=br-parent vlan=405 state=present + # Create an integration bridge - openvswitch_bridge: bridge=br-int state=present fail_mode=secure args: @@ -80,10 +95,18 @@ class OVSBridge(object): def __init__(self, module): self.module = module self.bridge = module.params['bridge'] + self.parent = module.params['parent'] + self.vlan = module.params['vlan'] self.state = module.params['state'] self.timeout = module.params['timeout'] self.fail_mode = module.params['fail_mode'] + if self.parent and self.vlan is None: + self.module.fail_json(msg='VLAN id must be set when parent is defined') + + if self.vlan < 0 or self.vlan > 4095: + self.module.fail_json(msg='Invalid VLAN ID (must be between 0 and 4095)') + def _vsctl(self, command): '''Run ovs-vsctl command''' return self.module.run_command(['ovs-vsctl', '-t', @@ -100,7 +123,11 @@ def exists(self): def add(self): '''Create the bridge''' - rtc, _, err = self._vsctl(['add-br', self.bridge]) + if self.parent and self.vlan: # Add fake bridge + rtc, _, err = self._vsctl(['add-br', self.bridge, self.parent, self.vlan]) + else: + rtc, _, err = self._vsctl(['add-br', self.bridge]) + if rtc != 0: self.module.fail_json(msg=err) if self.fail_mode: @@ -249,6 +276,8 @@ def main(): module = AnsibleModule( argument_spec={ 'bridge': {'required': True}, + 'parent': {'default': None}, + 'vlan': {'default': None, 'type': 'int'}, 'state': {'default': 'present', 'choices': ['present', 'absent']}, 'timeout': {'default': 5, 'type': 'int'}, 'external_ids': {'default': None, 'type': 'dict'}, From 8f77a0e72a1a9385035db289299feeb3966f9d59 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Tue, 25 Oct 2016 07:43:50 +0200 Subject: [PATCH 2275/2522] docs: adjust version_added --- network/cloudflare_dns.py | 2 +- network/openvswitch_bridge.py | 4 ++-- packaging/language/maven_artifact.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/network/cloudflare_dns.py b/network/cloudflare_dns.py index f0d933b1818..92052c0d014 100644 --- a/network/cloudflare_dns.py +++ b/network/cloudflare_dns.py @@ -54,7 +54,7 @@ description: Proxy through cloudflare network or just use DNS required: false default: no - version_added: "2.2" + version_added: "2.3" record: description: - Record to add. Required if C(state=present). Default is C(@) (e.g. the zone name) diff --git a/network/openvswitch_bridge.py b/network/openvswitch_bridge.py index 5d44ff070f8..fe48ca99a2f 100644 --- a/network/openvswitch_bridge.py +++ b/network/openvswitch_bridge.py @@ -37,13 +37,13 @@ description: - Name of bridge or fake bridge to manage parent: - version_added: 2.2 + version_added: "2.3" required: false default: None description: - Bridge parent of the fake bridge to manage vlan: - version_added: 2.2 + version_added: "2.3" required: false default: None description: diff --git a/packaging/language/maven_artifact.py b/packaging/language/maven_artifact.py index f79f3175cba..f116ab810f7 100644 --- a/packaging/language/maven_artifact.py +++ b/packaging/language/maven_artifact.py @@ -107,7 +107,7 @@ - Specifies a timeout in seconds for the connection attempt required: false default: 10 - version_added: '2.2' + version_added: "2.3" validate_certs: description: - If C(no), SSL certificates will not be validated. This should only be set to C(no) when no other option exists. From 8a625fe6ed74e90ee6be21e801f36567ce8e0f1e Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Tue, 25 Oct 2016 04:50:42 -0700 Subject: [PATCH 2276/2522] Make irules module idempotent (#3175) The irules module was failing to strip whitespace that is, for some reason, automatically inserted by BIG-IP. This patch adds necessary strips --- network/f5/bigip_irule.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/network/f5/bigip_irule.py b/network/f5/bigip_irule.py index 4a7ff4d951f..f2be61dfd89 100644 --- a/network/f5/bigip_irule.py +++ b/network/f5/bigip_irule.py @@ -202,7 +202,7 @@ def read(self): ) if hasattr(r, 'apiAnonymous'): - p['content'] = str(r.apiAnonymous) + p['content'] = str(r.apiAnonymous.strip()) p['name'] = name return p @@ -246,14 +246,10 @@ def exists(self): ) def present(self): - changed = False - if self.exists(): - changed = self.update() + return self.update() else: - changed = self.create() - - return changed + return self.create() def update(self): params = dict() @@ -267,6 +263,7 @@ def update(self): module = self.params['module'] if content is not None: + content = content.strip() if 'content' in current: if content != current['content']: params['apiAnonymous'] = content @@ -318,7 +315,7 @@ def create(self): return True if content is not None: - params['apiAnonymous'] = content + params['apiAnonymous'] = content.strip() params['name'] = name params['partition'] = partition From f1e931e27365cc073f65754391fda7e67588fe22 Mon Sep 17 00:00:00 2001 From: John R Barker Date: Tue, 25 Oct 2016 16:27:35 +0100 Subject: [PATCH 2277/2522] a10 - Use docs_fragments (#3281) Remove duplicated documentation (common options defined in module_utils/a10.py). Also tidy up formatting. --- network/a10/a10_server.py | 43 ++++----------------------- network/a10/a10_service_group.py | 48 +++--------------------------- network/a10/a10_virtual_server.py | 49 +++---------------------------- 3 files changed, 13 insertions(+), 127 deletions(-) diff --git a/network/a10/a10_server.py b/network/a10/a10_server.py index c89389e2c0c..1bd483bca11 100644 --- a/network/a10/a10_server.py +++ b/network/a10/a10_server.py @@ -29,37 +29,22 @@ description: - Manage slb server objects on A10 Networks devices via aXAPI author: "Mischa Peters (@mischapeters)" -notes: - - Requires A10 Networks aXAPI 2.1 +extends_documentation_fragment: a10 options: - host: - description: - - hostname or ip of your A10 Networks device - required: true - username: - description: - - admin account of your A10 Networks device - required: true - aliases: ['user', 'admin'] - password: - description: - - admin password of your A10 Networks device - required: true - aliases: ['pass', 'pwd'] server_name: description: - - slb server name + - SLB server name. required: true aliases: ['server'] server_ip: description: - - slb server IP address + - SLB server IP address. required: false default: null aliases: ['ip', 'address'] server_status: description: - - slb virtual server status + - SLB virtual server status. required: false default: enabled aliases: ['status'] @@ -74,28 +59,10 @@ default: null state: description: - - create, update or remove slb server + - Create, update or remove slb server. required: false default: present choices: ['present', 'absent'] - write_config: - description: - - If C(yes), any changes will cause a write of the running configuration - to non-volatile memory. This will save I(all) configuration changes, - including those that may have been made manually or through other modules, - so care should be taken when specifying C(yes). - required: false - version_added: 2.2 - default: "no" - choices: ["yes", "no"] - validate_certs: - description: - - If C(no), SSL certificates will not be validated. This should only be used - on personally controlled devices using self-signed certificates. - required: false - version_added: 2.2 - default: 'yes' - choices: ['yes', 'no'] ''' diff --git a/network/a10/a10_service_group.py b/network/a10/a10_service_group.py index 4b06ad20118..4a6cb0a67fe 100644 --- a/network/a10/a10_service_group.py +++ b/network/a10/a10_service_group.py @@ -30,47 +30,25 @@ - Manage slb service-group objects on A10 Networks devices via aXAPI author: "Mischa Peters (@mischapeters)" notes: - - Requires A10 Networks aXAPI 2.1 - When a server doesn't exist and is added to the service-group the server will be created +extends_documentation_fragment: a10 options: - host: - description: - - hostname or ip of your A10 Networks device - required: true - default: null - aliases: [] - choices: [] - username: - description: - - admin account of your A10 Networks device - required: true - default: null - aliases: ['user', 'admin'] - choices: [] - password: - description: - - admin password of your A10 Networks device - required: true - default: null - aliases: ['pass', 'pwd'] - choices: [] service_group: description: - - slb service-group name + - SLB service-group name. required: true default: null aliases: ['service', 'pool', 'group'] - choices: [] service_group_protocol: description: - - slb service-group protocol + - SLB service-group protocol. required: false default: tcp aliases: ['proto', 'protocol'] choices: ['tcp', 'udp'] service_group_method: description: - - slb service-group loadbalancing method + - SLB service-group loadbalancing method. required: false default: round-robin aliases: ['method'] @@ -82,24 +60,6 @@ specify the C(status:). See the examples below for details. required: false default: null - aliases: [] - choices: [] - write_config: - description: - - If C(yes), any changes will cause a write of the running configuration - to non-volatile memory. This will save I(all) configuration changes, - including those that may have been made manually or through other modules, - so care should be taken when specifying C(yes). - required: false - default: "no" - choices: ["yes", "no"] - validate_certs: - description: - - If C(no), SSL certificates will not be validated. This should only be used - on personally controlled devices using self-signed certificates. - required: false - default: 'yes' - choices: ['yes', 'no'] ''' diff --git a/network/a10/a10_virtual_server.py b/network/a10/a10_virtual_server.py index cc0f537a981..065cc98a2a2 100644 --- a/network/a10/a10_virtual_server.py +++ b/network/a10/a10_virtual_server.py @@ -29,48 +29,23 @@ description: - Manage slb virtual server objects on A10 Networks devices via aXAPI author: "Mischa Peters (@mischapeters)" -notes: - - Requires A10 Networks aXAPI 2.1 -requirements: [] +extends_documentation_fragment: a10 options: - host: - description: - - hostname or ip of your A10 Networks device - required: true - default: null - aliases: [] - choices: [] - username: - description: - - admin account of your A10 Networks device - required: true - default: null - aliases: ['user', 'admin'] - choices: [] - password: - description: - - admin password of your A10 Networks device - required: true - default: null - aliases: ['pass', 'pwd'] - choices: [] virtual_server: description: - - slb virtual server name + - SLB virtual server name. required: true default: null aliases: ['vip', 'virtual'] - choices: [] virtual_server_ip: description: - - slb virtual server ip address + - SLB virtual server IP address. required: false default: null aliases: ['ip', 'address'] - choices: [] virtual_server_status: description: - - slb virtual server status + - SLB virtual server status. required: false default: enable aliases: ['status'] @@ -82,22 +57,6 @@ specify the C(service_group:) as well as the C(status:). See the examples below for details. This parameter is required when C(state) is C(present). required: false - write_config: - description: - - If C(yes), any changes will cause a write of the running configuration - to non-volatile memory. This will save I(all) configuration changes, - including those that may have been made manually or through other modules, - so care should be taken when specifying C(yes). - required: false - default: "no" - choices: ["yes", "no"] - validate_certs: - description: - - If C(no), SSL certificates will not be validated. This should only be used - on personally controlled devices using self-signed certificates. - required: false - default: 'yes' - choices: ['yes', 'no'] ''' From 94e71e5d4273f1bf5be94bf644303de3d3ee8483 Mon Sep 17 00:00:00 2001 From: Jens Carl Date: Tue, 25 Oct 2016 15:05:40 -0700 Subject: [PATCH 2278/2522] Fix typo (#3289) --- cloud/amazon/redshift_subnet_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/redshift_subnet_group.py b/cloud/amazon/redshift_subnet_group.py index d47593c7970..750c8dd94c7 100644 --- a/cloud/amazon/redshift_subnet_group.py +++ b/cloud/amazon/redshift_subnet_group.py @@ -82,7 +82,7 @@ vpc_id: description: Id of the VPC where the subnet is located returned: success - type: stering + type: string sample: "vpc-aabb1122" ''' From 239468aa1f32d0d19eb4a6cfa8d4b36c2955efcc Mon Sep 17 00:00:00 2001 From: Ryan Conway Date: Wed, 26 Oct 2016 10:57:55 +0200 Subject: [PATCH 2279/2522] Adds support for setting a virtual server's "source address translation" policy to a specific SNAT pool, in addition to the 'None' or 'Automap' options. (#3158) --- network/f5/bigip_virtual_server.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/network/f5/bigip_virtual_server.py b/network/f5/bigip_virtual_server.py index 158d81704d2..b1fb318ca59 100644 --- a/network/f5/bigip_virtual_server.py +++ b/network/f5/bigip_virtual_server.py @@ -102,6 +102,10 @@ description: - Source network address policy required: false + choices: + - None + - Automap + - Name of a SNAT pool (eg "/Common/snat_pool_name") to enable SNAT with the specific pool default: None default_persistence_profile: description: @@ -355,6 +359,7 @@ def set_snat(api, name, snat): updated = False try: current_state = get_snat_type(api, name) + current_snat_pool = get_snat_pool(api, name) if snat is None: return updated elif snat == 'None' and current_state != 'SRC_TRANS_NONE': @@ -367,6 +372,11 @@ def set_snat(api, name, snat): virtual_servers=[name] ) updated = True + elif snat_settings_need_updating(snat, current_state, current_snat_pool): + api.LocalLB.VirtualServer.set_source_address_translation_snat_pool( + virtual_servers=[name], + pools=[snat] + ) return updated except bigsuds.OperationFailed as e: raise Exception('Error on setting snat : %s' % e) @@ -378,6 +388,23 @@ def get_snat_type(api, name): )[0] +def get_snat_pool(api, name): + return api.LocalLB.VirtualServer.get_source_address_translation_snat_pool( + virtual_servers=[name] + )[0] + + +def snat_settings_need_updating(snat, current_state, current_snat_pool): + if snat == 'None' or snat == 'Automap': + return False + elif snat and current_state != 'SRC_TRANS_SNATPOOL': + return True + elif snat and current_state == 'SRC_TRANS_SNATPOOL' and current_snat_pool != snat: + return True + else: + return False + + def get_pool(api, name): return api.LocalLB.VirtualServer.get_default_pool_name( virtual_servers=[name] From 4c58c430a4ca62e3dab47aa50008db616e10d70f Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Wed, 26 Oct 2016 09:06:34 -0700 Subject: [PATCH 2280/2522] Adds route advertisement state to the bigip_virtual_server module (#3273) --- network/f5/bigip_virtual_server.py | 31 ++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/network/f5/bigip_virtual_server.py b/network/f5/bigip_virtual_server.py index b1fb318ca59..f1c6905ca1d 100644 --- a/network/f5/bigip_virtual_server.py +++ b/network/f5/bigip_virtual_server.py @@ -112,6 +112,12 @@ - Default Profile which manages the session persistence required: false default: None + route_advertisement_state: + description: + - Enable route advertisement for destination + required: false + default: disabled + version_added: "2.3" description: description: - Virtual server description @@ -540,6 +546,27 @@ def set_default_persistence_profiles(api, name, persistence_profile): raise Exception('Error on setting default persistence profile : %s' % e) +def get_route_advertisement_status(api, address): + result = api.LocalLB.VirtualAddressV2.get_route_advertisement_state(virtual_addresses=[address]).pop(0) + result = result.split("STATE_")[-1].lower() + return result + + +def set_route_advertisement_state(api, destination, partition, route_advertisement_state): + updated = False + + try: + state = "STATE_%s" % route_advertisement_state.strip().upper() + address = fq_name(partition, destination,) + current_route_advertisement_state=get_route_advertisement_status(api,address) + if current_route_advertisement_state != route_advertisement_state: + api.LocalLB.VirtualAddressV2.set_route_advertisement_state(virtual_addresses=[address], states=[state]) + updated = True + return updated + except bigsuds.OperationFailed as e: + raise Exception('Error on setting profiles : %s' % e) + + def main(): argument_spec = f5_argument_spec() argument_spec.update(dict( @@ -554,6 +581,7 @@ def main(): pool=dict(type='str'), description=dict(type='str'), snat=dict(type='str'), + route_advertisement_state=dict(type='str', default='disabled', choices=['enabled', 'disabled']), default_persistence_profile=dict(type='str') )) @@ -593,6 +621,7 @@ def main(): pool = fq_name(partition, module.params['pool']) description = module.params['description'] snat = module.params['snat'] + route_advertisement_state = module.params['route_advertisement_state'] default_persistence_profile = fq_name(partition, module.params['default_persistence_profile']) if 1 > port > 65535: @@ -639,6 +668,7 @@ def main(): set_description(api, name, description) set_default_persistence_profiles(api, name, default_persistence_profile) set_state(api, name, state) + set_route_advertisement_state(api, destination, partition, route_advertisement_state) result = {'changed': True} except bigsuds.OperationFailed as e: raise Exception('Error on creating Virtual Server : %s' % e) @@ -663,6 +693,7 @@ def main(): result['changed'] |= set_rules(api, name, all_rules) result['changed'] |= set_default_persistence_profiles(api, name, default_persistence_profile) result['changed'] |= set_state(api, name, state) + result['changed'] |= set_route_advertisement_state(api, destination, partition, route_advertisement_state) api.System.Session.submit_transaction() except Exception as e: raise Exception("Error on updating Virtual Server : %s" % e) From 6235f9caa3e20f0fa078def20afbd223db496f9d Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Wed, 26 Oct 2016 09:07:12 -0700 Subject: [PATCH 2281/2522] Adds route_domain parameter to the selfip module. (#3272) This patch allows one to specify a route_domain to create the selfip in. --- network/f5/bigip_selfip.py | 49 ++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/network/f5/bigip_selfip.py b/network/f5/bigip_selfip.py index dc4403e60b8..fbc31f80b93 100644 --- a/network/f5/bigip_selfip.py +++ b/network/f5/bigip_selfip.py @@ -64,6 +64,13 @@ description: - The VLAN that the new self IPs will be on. required: true + route_domain: + description: + - The route domain id of the system. + If none, id of the route domain will be "0" (default route domain) + required: false + default: none + version_added: 2.3 notes: - Requires the f5-sdk Python package on the host. This is as easy as pip install f5-sdk. @@ -89,6 +96,20 @@ vlan: "vlan1" delegate_to: localhost +- name: Create Self IP with a Route Domain + bigip_selfip: + server: "lb.mydomain.com" + user: "admin" + password: "secret" + validate_certs: "no" + name: "self1" + address: "10.10.10.10" + netmask: "255.255.255.0" + vlan: "vlan1" + route_domain: "10" + allow_service: "default" + delegate_to: localhost + - name: Delete Self IP bigip_selfip: name: "self1" @@ -278,10 +299,15 @@ def read(self): ) if hasattr(r, 'address'): + p['route_domain'] = str(None) + if '%' in r.address: + ipaddr = [] + ipaddr = r.address.split('%', 1) + rdmask = ipaddr[1].split('/', 1) + r.address = "%s/%s" % (ipaddr[0], rdmask[1]) + p['route_domain'] = str(rdmask[0]) ipnet = IPNetwork(r.address) p['address'] = str(ipnet.ip) - if hasattr(r, 'address'): - ipnet = IPNetwork(r.address) p['netmask'] = str(ipnet.netmask) if hasattr(r, 'trafficGroup'): p['traffic_group'] = str(r.trafficGroup) @@ -397,6 +423,7 @@ def update(self): partition = self.params['partition'] traffic_group = self.params['traffic_group'] vlan = self.params['vlan'] + route_domain = self.params['route_domain'] if address is not None and address != current['address']: raise F5ModuleError( @@ -411,12 +438,19 @@ def update(self): new_addr = "%s/%s" % (address.ip, netmask) nipnet = IPNetwork(new_addr) + if route_domain is not None: + nipnet = "%s%s%s" % (address.ip, route_domain, netmask) cur_addr = "%s/%s" % (current['address'], current['netmask']) cipnet = IPNetwork(cur_addr) + if route_domain is not None: + cipnet = "%s%s%s" % (current['address'], current['route_domain'], current['netmask']) if nipnet != cipnet: - address = "%s/%s" % (nipnet.ip, nipnet.prefixlen) + if route_domain is not None: + address = "%s%s%s/%s" % (address.ip, '%', route_domain, netmask) + else: + address = "%s/%s" % (nipnet.ip, nipnet.prefixlen) params['address'] = address except AddrFormatError: raise F5ModuleError( @@ -516,6 +550,7 @@ def create(self): partition = self.params['partition'] traffic_group = self.params['traffic_group'] vlan = self.params['vlan'] + route_domain = self.params['route_domain'] if address is None or netmask is None: raise F5ModuleError( @@ -532,7 +567,10 @@ def create(self): try: ipin = "%s/%s" % (address, netmask) ipnet = IPNetwork(ipin) - params['address'] = "%s/%s" % (ipnet.ip, ipnet.prefixlen) + if route_domain is not None: + params['address'] = "%s%s%s/%s" % (ipnet.ip, '%', route_domain, ipnet.prefixlen) + else: + params['address'] = "%s/%s" % (ipnet.ip, ipnet.prefixlen) except AddrFormatError: raise F5ModuleError( 'The provided address/netmask value was invalid' @@ -631,7 +669,8 @@ def main(): name=dict(required=True), netmask=dict(required=False, default=None), traffic_group=dict(required=False, default=None), - vlan=dict(required=False, default=None) + vlan=dict(required=False, default=None), + route_domain=dict(required=False, default=None) ) argument_spec.update(meta_args) From 99eb1bdae84ff3ce6e13c462f94d5f687d86d195 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Wed, 26 Oct 2016 09:12:46 -0700 Subject: [PATCH 2282/2522] Adds bigip_sys_global module (#3271) This module can be used to change a variety of system parameters typically used for bootstrapping. Tests for this module can be found here https://github.com/F5Networks/f5-ansible/blob/master/roles/bigip_sys_global/tasks/main.yaml Platforms this was tested on are 12.1.0-hf1 --- network/f5/bigip_sys_global.py | 426 +++++++++++++++++++++++++++++++++ 1 file changed, 426 insertions(+) create mode 100644 network/f5/bigip_sys_global.py diff --git a/network/f5/bigip_sys_global.py b/network/f5/bigip_sys_global.py new file mode 100644 index 00000000000..8a86ac50bba --- /dev/null +++ b/network/f5/bigip_sys_global.py @@ -0,0 +1,426 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2016 F5 Networks Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: bigip_sys_global +short_description: Manage BIG-IP global settings. +description: + - Manage BIG-IP global settings. +version_added: "2.3" +options: + banner_text: + description: + - Specifies the text to present in the advisory banner. + console_timeout: + description: + - Specifies the number of seconds of inactivity before the system logs + off a user that is logged on. + gui_setup: + description: + - C(enable) or C(disabled) the Setup utility in the browser-based + Configuration utility + choices: + - enabled + - disabled + lcd_display: + description: + - Specifies, when C(enabled), that the system menu displays on the + LCD screen on the front of the unit. This setting has no effect + when used on the VE platform. + choices: + - enabled + - disabled + mgmt_dhcp: + description: + - Specifies whether or not to enable DHCP client on the management + interface + choices: + - enabled + - disabled + net_reboot: + description: + - Specifies, when C(enabled), that the next time you reboot the system, + the system boots to an ISO image on the network, rather than an + internal media drive. + choices: + - enabled + - disabled + quiet_boot: + description: + - Specifies, when C(enabled), that the system suppresses informational + text on the console during the boot cycle. When C(disabled), the + system presents messages and informational text on the console during + the boot cycle. + security_banner: + description: + - Specifies whether the system displays an advisory message on the + login screen. + choices: + - enabled + - disabled + state: + description: + - The state of the variable on the system. When C(present), guarantees + that an existing variable is set to C(value). + required: false + default: present + choices: + - present +notes: + - Requires the f5-sdk Python package on the host. This is as easy as pip + install f5-sdk. +extends_documentation_fragment: f5 +requirements: + - f5-sdk +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = ''' +- name: Disable the setup utility + bigip_sys_global: + gui_setup: "disabled" + password: "secret" + server: "lb.mydomain.com" + user: "admin" + state: "present" + delegate_to: localhost +''' + +RETURN = ''' +banner_text: + description: The new text to present in the advisory banner. + returned: changed + type: string + sample: "This is a corporate device. Do not touch." +console_timeout: + description: > + The new number of seconds of inactivity before the system + logs off a user that is logged on. + returned: changed + type: integer + sample: 600 +gui_setup: + description: The new setting for the Setup utility. + returned: changed + type: string + sample: enabled +lcd_display: + description: The new setting for displaying the system menu on the LCD. + returned: changed + type: string + sample: enabled +mgmt_dhcp: + description: > + The new setting for whether the mgmt interface should DHCP + or not + returned: changed + type: string + sample: enabled +net_reboot: + description: > + The new setting for whether the system should boot to an ISO on the + network or not + returned: changed + type: string + sample: enabled +quiet_boot: + description: > + The new setting for whether the system should suppress information to + the console during boot or not. + returned: changed + type: string + sample: enabled +security_banner: + description: > + The new setting for whether the system should display an advisory message + on the login screen or not + returned: changed + type: string + sample: enabled +''' + +try: + from f5.bigip.contexts import TransactionContextManager + from f5.bigip import ManagementRoot + from icontrol.session import iControlUnexpectedHTTPError + HAS_F5SDK = True +except ImportError: + HAS_F5SDK = False + + +class BigIpSysGlobalManager(object): + def __init__(self, *args, **kwargs): + self.changed_params = dict() + self.params = kwargs + self.api = None + + def apply_changes(self): + result = dict() + + changed = self.apply_to_running_config() + + result.update(**self.changed_params) + result.update(dict(changed=changed)) + return result + + def apply_to_running_config(self): + try: + self.api = self.connect_to_bigip(**self.params) + return self.update_sys_global_settings() + except iControlUnexpectedHTTPError as e: + raise F5ModuleError(str(e)) + + def connect_to_bigip(self, **kwargs): + return ManagementRoot(kwargs['server'], + kwargs['user'], + kwargs['password'], + port=kwargs['server_port']) + + def read_sys_global_information(self): + settings = self.load_sys_global() + return self.format_sys_global_information(settings) + + def load_sys_global(self): + return self.api.tm.sys.global_settings.load() + + def get_changed_parameters(self): + result = dict() + current = self.read_sys_global_information() + if self.security_banner_is_changed(current): + result['guiSecurityBanner'] = self.params['security_banner'] + if self.banner_text_is_changed(current): + result['guiSecurityBannerText'] = self.params['banner_text'] + if self.gui_setup_is_changed(current): + result['guiSetup'] = self.params['gui_setup'] + if self.lcd_display_is_changed(current): + result['lcdDisplay'] = self.params['lcd_display'] + if self.mgmt_dhcp_is_changed(current): + result['mgmtDhcp'] = self.params['mgmt_dhcp'] + if self.net_reboot_is_changed(current): + result['netReboot'] = self.params['net_reboot'] + if self.quiet_boot_is_changed(current): + result['quietBoot'] = self.params['quiet_boot'] + if self.console_timeout_is_changed(current): + result['consoleInactivityTimeout'] = self.params['console_timeout'] + return result + + def security_banner_is_changed(self, current): + if self.params['security_banner'] is None: + return False + if 'security_banner' not in current: + return True + if self.params['security_banner'] == current['security_banner']: + return False + else: + return True + + def banner_text_is_changed(self, current): + if self.params['banner_text'] is None: + return False + if 'banner_text' not in current: + return True + if self.params['banner_text'] == current['banner_text']: + return False + else: + return True + + def gui_setup_is_changed(self, current): + if self.params['gui_setup'] is None: + return False + if 'gui_setup' not in current: + return True + if self.params['gui_setup'] == current['gui_setup']: + return False + else: + return True + + def lcd_display_is_changed(self, current): + if self.params['lcd_display'] is None: + return False + if 'lcd_display' not in current: + return True + if self.params['lcd_display'] == current['lcd_display']: + return False + else: + return True + + def mgmt_dhcp_is_changed(self, current): + if self.params['mgmt_dhcp'] is None: + return False + if 'mgmt_dhcp' not in current: + return True + if self.params['mgmt_dhcp'] == current['mgmt_dhcp']: + return False + else: + return True + + def net_reboot_is_changed(self, current): + if self.params['net_reboot'] is None: + return False + if 'net_reboot' not in current: + return True + if self.params['net_reboot'] == current['net_reboot']: + return False + else: + return True + + def quiet_boot_is_changed(self, current): + if self.params['quiet_boot'] is None: + return False + if 'quiet_boot' not in current: + return True + if self.params['quiet_boot'] == current['quiet_boot']: + return False + else: + return True + + def console_timeout_is_changed(self, current): + if self.params['console_timeout'] is None: + return False + if 'console_timeout' not in current: + return True + if self.params['console_timeout'] == current['console_timeout']: + return False + else: + return True + + def format_sys_global_information(self, settings): + result = dict() + if hasattr(settings, 'guiSecurityBanner'): + result['security_banner'] = str(settings.guiSecurityBanner) + if hasattr(settings, 'guiSecurityBannerText'): + result['banner_text'] = str(settings.guiSecurityBannerText) + if hasattr(settings, 'guiSetup'): + result['gui_setup'] = str(settings.guiSetup) + if hasattr(settings, 'lcdDisplay'): + result['lcd_display'] = str(settings.lcdDisplay) + if hasattr(settings, 'mgmtDhcp'): + result['mgmt_dhcp'] = str(settings.mgmtDhcp) + if hasattr(settings, 'netReboot'): + result['net_reboot'] = str(settings.netReboot) + if hasattr(settings, 'quietBoot'): + result['quiet_boot'] = str(settings.quietBoot) + if hasattr(settings, 'consoleInactivityTimeout'): + result['console_timeout'] = int(settings.consoleInactivityTimeout) + return result + + def update_sys_global_settings(self): + params = self.get_changed_parameters() + if params: + self.changed_params = camel_dict_to_snake_dict(params) + if self.params['check_mode']: + return True + else: + return False + self.update_sys_global_settings_on_device(params) + return True + + def update_sys_global_settings_on_device(self, params): + tx = self.api.tm.transactions.transaction + with TransactionContextManager(tx) as api: + r = api.tm.sys.global_settings.load() + r.update(**params) + + +class BigIpSysGlobalModuleConfig(object): + def __init__(self): + self.argument_spec = dict() + self.meta_args = dict() + self.supports_check_mode = True + self.states = ['present'] + self.on_off_choices = ['enabled', 'disabled'] + + self.initialize_meta_args() + self.initialize_argument_spec() + + def initialize_meta_args(self): + args = dict( + security_banner=dict( + required=False, + choices=self.on_off_choices, + default=None + ), + banner_text=dict(required=False, default=None), + gui_setup=dict( + required=False, + choices=self.on_off_choices, + default=None + ), + lcd_display=dict( + required=False, + choices=self.on_off_choices, + default=None + ), + mgmt_dhcp=dict( + required=False, + choices=self.on_off_choices, + default=None + ), + net_reboot=dict( + required=False, + choices=self.on_off_choices, + default=None + ), + quiet_boot=dict( + required=False, + choices=self.on_off_choices, + default=None + ), + console_timeout=dict(required=False, type='int', default=None), + state=dict(default='present', choices=['present']) + ) + self.meta_args = args + + def initialize_argument_spec(self): + self.argument_spec = f5_argument_spec() + self.argument_spec.update(self.meta_args) + + def create(self): + return AnsibleModule( + argument_spec=self.argument_spec, + supports_check_mode=self.supports_check_mode + ) + + +def main(): + if not HAS_F5SDK: + raise F5ModuleError("The python f5-sdk module is required") + + config = BigIpSysGlobalModuleConfig() + module = config.create() + + try: + obj = BigIpSysGlobalManager( + check_mode=module.check_mode, **module.params + ) + result = obj.apply_changes() + + module.exit_json(**result) + except F5ModuleError as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import camel_dict_to_snake_dict +from ansible.module_utils.f5 import * + +if __name__ == '__main__': + main() From 1db36bea62d032f841153f2abd04d84fceb48fa7 Mon Sep 17 00:00:00 2001 From: Michal Klempa Date: Wed, 26 Oct 2016 21:39:14 +0200 Subject: [PATCH 2283/2522] maven_artifact: fix download of SBT published snapshot artifact (#3085) Fixes #1717 --- packaging/language/maven_artifact.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packaging/language/maven_artifact.py b/packaging/language/maven_artifact.py index f116ab810f7..5c0e88ac725 100644 --- a/packaging/language/maven_artifact.py +++ b/packaging/language/maven_artifact.py @@ -222,6 +222,9 @@ def find_uri_for_artifact(self, artifact): xml = self._request(self.base + path, "Failed to download maven-metadata.xml", lambda r: etree.parse(r)) timestamp = xml.xpath("/metadata/versioning/snapshot/timestamp/text()")[0] buildNumber = xml.xpath("/metadata/versioning/snapshot/buildNumber/text()")[0] + for snapshotArtifact in xml.xpath("/metadata/versioning/snapshotVersions/snapshotVersion"): + if len(snapshotArtifact.xpath("classifier/text()")) > 0 and snapshotArtifact.xpath("classifier/text()")[0] == artifact.classifier and len(snapshotArtifact.xpath("extension/text()")) > 0 and snapshotArtifact.xpath("extension/text()")[0] == artifact.extension: + return self._uri_for_artifact(artifact, snapshotArtifact.xpath("value/text()")[0]) return self._uri_for_artifact(artifact, artifact.version.replace("SNAPSHOT", timestamp + "-" + buildNumber)) return self._uri_for_artifact(artifact, artifact.version) From 1e9615730fbc2fcbaa23e7ae5f905aacf919a9f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory?= Date: Wed, 26 Oct 2016 21:49:38 +0200 Subject: [PATCH 2284/2522] Change azure default deployment mode to incremental (#3023) * Changed default deployment mode to match with azure -cli behaviour. "Complete" mode by default is too dangerous. * Set incremental as default behaviour for deployment mode. --- cloud/azure/azure_rm_deployment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/azure/azure_rm_deployment.py b/cloud/azure/azure_rm_deployment.py index 2fa8bf239c1..a95ef54a2b4 100644 --- a/cloud/azure/azure_rm_deployment.py +++ b/cloud/azure/azure_rm_deployment.py @@ -42,7 +42,7 @@ - In incremental mode, resources are deployed without deleting existing resources that are not included in the template. In complete mode resources are deployed and existing resources in the resource group not included in the template are deleted. required: false - default: complete + default: incremental choices: - complete - incremental @@ -405,7 +405,7 @@ def __init__(self): template_link=dict(type='str', default=None), parameters_link=dict(type='str', default=None), location=dict(type='str', default="westus"), - deployment_mode=dict(type='str', default='complete', choices=['complete', 'incremental']), + deployment_mode=dict(type='str', default='incremental', choices=['complete', 'incremental']), deployment_name=dict(type='str', default="ansible-arm"), wait_for_deployment_completion=dict(type='bool', default=True), wait_for_deployment_polling_period=dict(type='int', default=10) From eab1f1bad3265087b88632d98414729fa864ee46 Mon Sep 17 00:00:00 2001 From: Scott Butler Date: Wed, 26 Oct 2016 12:51:10 -0700 Subject: [PATCH 2285/2522] Clarifies description of path parameter. --- windows/win_scheduled_task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_scheduled_task.py b/windows/win_scheduled_task.py index 3c6ef9d28a9..dc827026a57 100644 --- a/windows/win_scheduled_task.py +++ b/windows/win_scheduled_task.py @@ -79,7 +79,7 @@ required: false path: description: - - Folder path of scheduled task + - Task folder in which this task will be stored default: '\' ''' From 0700f52f1df4dbbad657e78eb60540359105a496 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Kliemeck Date: Thu, 27 Oct 2016 11:56:08 +0200 Subject: [PATCH 2286/2522] win_nssm: set application directory on change of application binary (#3267) --- windows/win_nssm.ps1 | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/windows/win_nssm.ps1 b/windows/win_nssm.ps1 index 2801307f602..0ef05d892ff 100644 --- a/windows/win_nssm.ps1 +++ b/windows/win_nssm.ps1 @@ -95,6 +95,9 @@ Function Nssm-Install { Throw "Error installing service ""$name"". No application was supplied." } + If (-Not (Test-Path -Path $application -PathType Leaf)) { + Throw "$application does not exist on the host" + } if (!(Service-Exists -name $name)) { @@ -138,6 +141,21 @@ Function Nssm-Install $result.changed = $true } } + + if ($result.changed) + { + $applicationPath = (Get-Item $application).DirectoryName + $cmd = "nssm set ""$name"" AppDirectory $applicationPath" + + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "nssm_error_cmd" $cmd + Set-Attr $result "nssm_error_log" "$results" + Throw "Error installing service ""$name""" + } + } } Function ParseAppParameters() From 687601e4fca1ef69acf97df3724fb866a26f4520 Mon Sep 17 00:00:00 2001 From: Yanis Guenane Date: Fri, 27 May 2016 12:16:14 +0200 Subject: [PATCH 2287/2522] network: Add new module openssl_privatekey This module aims to allow a user to manage the lifecycle of OpenSSL private keys. Internally it relies on the pyOpenSSL python library to interact with openssl. A user is able to specify : * key size (via `size` parameter) * key algorithm (via `type` parameter) * key location (via `path` parameter) The most simple use case is: ``` - name: Generate ansible.com.pem SSL private key openssl_privatekey: name=ansible.com.pem path=/etc/ssl/private ``` A user can speficy more settings: ``` - name: Generate ansible.com.pem SSL private key openssl_privatekey: name=ansible.com.pem path=/etc/ssl/private size=2048 type=DSA ``` A user can also force the regeneration of an SSL key: ``` - name: Generate ansible.com.pem SSL private key openssl_privatekey: name=ansible.com.pem path=/etc/ssl/private force=true ``` --- crypto/__init__.py | 0 crypto/openssl_privatekey.py | 247 +++++++++++++++++++++++++++++++++++ crypto/openssl_publickey.py | 224 +++++++++++++++++++++++++++++++ 3 files changed, 471 insertions(+) create mode 100644 crypto/__init__.py create mode 100644 crypto/openssl_privatekey.py create mode 100644 crypto/openssl_publickey.py diff --git a/crypto/__init__.py b/crypto/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/crypto/openssl_privatekey.py b/crypto/openssl_privatekey.py new file mode 100644 index 00000000000..5055a2826e6 --- /dev/null +++ b/crypto/openssl_privatekey.py @@ -0,0 +1,247 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Yanis Guenane +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +from ansible.module_utils.basic import * + +try: + from OpenSSL import crypto +except ImportError: + pyopenssl_found = False +else: + pyopenssl_found = True + + +import os + +DOCUMENTATION = ''' +--- +module: openssl_privatekey +author: "Yanis Guenane (@Spredzy)" +version_added: "2.3" +short_description: Generate OpenSSL private keys. +description: + - "This module allows one to (re)generate OpenSSL private keys. It uses + the pyOpenSSL python library to interact with openssl. One can generate + either RSA or DSA private keys. Keys are generated in PEM format." +requirements: + - "python-pyOpenSSL" +options: + state: + required: false + default: "present" + choices: [ present, absent ] + description: + - Whether the private key should exist or not, taking action if the state is different from what is stated. + size: + required: false + default: 4096 + description: + - Size (in bits) of the TLS/SSL key to generate + type: + required: false + default: "RSA" + choices: [ RSA, DSA ] + description: + - The algorithm used to generate the TLS/SSL private key + force: + required: false + default: False + choices: [ True, False ] + description: + - Should the key be regenerated even it it already exists + path: + required: true + description: + - Name of the file in which the generated TLS/SSL private key will be written. It will have 0600 mode. +''' + +EXAMPLES = ''' +# Generate an OpenSSL private key with the default values (4096 bits, RSA) +# and no public key +- openssl_privatekey: path=/etc/ssl/private/ansible.com.pem + +# Generate an OpenSSL private key with a different size (2048 bits) +- openssl_privatekey: path=/etc/ssl/private/ansible.com.pem size=2048 + +# Force regenerate an OpenSSL private key if it already exists +- openssl_privatekey: path=/etc/ssl/private/ansible.com.pem force=True + +# Generate an OpenSSL private key with a different algorithm (DSA) +- openssl_privatekey: path=/etc/ssl/private/ansible.com.pem type=DSA +''' + +RETURN = ''' +size: + description: Size (in bits) of the TLS/SSL private key + returned: + - changed + - success + type: integer + sample: 4096 +type: + description: Algorithm used to generate the TLS/SSL private key + returned: + - changed + - success + type: string + sample: RSA +filename: + description: Path to the generated TLS/SSL private key file + returned: + - changed + - success + type: string + sample: /etc/ssl/private/ansible.com.pem +''' + +class PrivateKeyError(Exception): + pass + +class PrivateKey(object): + + def __init__(self, module): + self.size = module.params['size'] + self.state = module.params['state'] + self.name = os.path.basename(module.params['path']) + self.type = module.params['type'] + self.force = module.params['force'] + self.path = module.params['path'] + self.mode = module.params['mode'] + self.changed = True + self.check_mode = module.check_mode + + + def generate(self, module): + """Generate a keypair.""" + + if not os.path.exists(self.path) or self.force: + self.privatekey = crypto.PKey() + + if self.type == 'RSA': + crypto_type = crypto.TYPE_RSA + else: + crypto_type = crypto.TYPE_DSA + + try: + self.privatekey.generate_key(crypto_type, self.size) + except (TypeError, ValueError): + raise PrivateKeyError(get_exception()) + + try: + privatekey_file = os.open(self.path, + os.O_WRONLY | os.O_CREAT | os.O_TRUNC, + self.mode) + + os.write(privatekey_file, crypto.dump_privatekey(crypto.FILETYPE_PEM, self.privatekey)) + os.close(privatekey_file) + except IOError: + self.remove() + raise PrivateKeyError(get_exception()) + else: + self.changed = False + + file_args = module.load_file_common_arguments(module.params) + if module.set_fs_attributes_if_different(file_args, False): + self.changed = True + + + def remove(self): + """Remove the private key from the filesystem.""" + + try: + os.remove(self.path) + except OSError: + e = get_exception() + if e.errno != errno.ENOENT: + raise PrivateKeyError(e) + else: + self.changed = False + + + def dump(self): + """Serialize the object into a dictionnary.""" + + result = { + 'size': self.size, + 'type': self.type, + 'filename': self.path, + 'changed': self.changed, + } + + return result + + +def main(): + + module = AnsibleModule( + argument_spec = dict( + state=dict(default='present', choices=['present', 'absent'], type='str'), + size=dict(default=4096, type='int'), + type=dict(default='RSA', choices=['RSA', 'DSA'], type='str'), + force=dict(default=False, type='bool'), + path=dict(required=True, type='path'), + ), + supports_check_mode = True, + add_file_common_args = True, + ) + + if not pyopenssl_found: + module.fail_json(msg='the python pyOpenSSL module is required') + + path = module.params['path'] + base_dir = os.path.dirname(module.params['path']) + + if not os.path.isdir(base_dir): + module.fail_json(name=base_dir, msg='The directory %s does not exist or the file is not a directory' % base_dir) + + if not module.params['mode']: + module.params['mode'] = 0600 + + private_key = PrivateKey(module) + if private_key.state == 'present': + + if module.check_mode: + result = private_key.dump() + result['changed'] = module.params['force'] or not os.path.exists(path) + module.exit_json(**result) + + try: + private_key.generate(module) + except PrivateKeyError: + e = get_exception() + module.fail_json(msg=str(e)) + else: + + if module.check_mode: + result = private_key.dump() + result['changed'] = os.path.exists(path) + module.exit_json(**result) + + try: + private_key.remove() + except PrivateKeyError: + e = get_exception() + module.fail_json(msg=str(e)) + + result = private_key.dump() + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/crypto/openssl_publickey.py b/crypto/openssl_publickey.py new file mode 100644 index 00000000000..3fb2de85ae7 --- /dev/null +++ b/crypto/openssl_publickey.py @@ -0,0 +1,224 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Yanis Guenane +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +from ansible.module_utils.basic import * + +try: + from OpenSSL import crypto +except ImportError: + pyopenssl_found = False +else: + pyopenssl_found = True + + +import os + +DOCUMENTATION = ''' +--- +module: openssl_publickey +author: "Yanis Guenane (@Spredzy)" +version_added: "2.3" +short_description: Generate an OpenSSL public key from its private key. +description: + - "This module allows one to (re)generate OpenSSL public keys from their private keys. + It uses the pyOpenSSL python library to interact with openssl. Keys are generated + in PEM format. This module works only if the version of PyOpenSSL is recent enough (> 16.0.0)" +requirements: + - "python-pyOpenSSL" +options: + state: + required: false + default: "present" + choices: [ present, absent ] + description: + - Whether the public key should exist or not, taking action if the state is different from what is stated. + force: + required: false + default: False + choices: [ True, False ] + description: + - Should the key be regenerated even it it already exists + path: + required: true + description: + - Name of the file in which the generated TLS/SSL public key will be written. + privatekey_path: + required: true + description: + - Path to the TLS/SSL private key from which to genereate the public key. +''' + +EXAMPLES = ''' +# Generate an OpenSSL public key. +- openssl_publickey: path=/etc/ssl/public/ansible.com.pem + privatekey_path=/etc/ssl/private/ansible.com.pem + +# Force regenerate an OpenSSL public key if it already exists +- openssl_publickey: path=/etc/ssl/public/ansible.com.pem + privatekey_path=/etc/ssl/private/ansible.com.pem + force=True + +# Remove an OpenSSL public key +- openssl_publickey: path=/etc/ssl/public/ansible.com.pem + privatekey_path=/etc/ssl/private/ansible.com.pem + state=absent +''' + +RETURN = ''' +privatekey: + description: Path to the TLS/SSL private key the public key was generated from + returned: + - changed + - success + type: string + sample: /etc/ssl/private/ansible.com.pem +filename: + description: Path to the generated TLS/SSL public key file + returned: + - changed + - success + type: string + sample: /etc/ssl/public/ansible.com.pem +''' + +class PublicKeyError(Exception): + pass + +class PublicKey(object): + + def __init__(self, module): + self.state = module.params['state'] + self.force = module.params['force'] + self.name = os.path.basename(module.params['path']) + self.path = module.params['path'] + self.privatekey_path = module.params['privatekey_path'] + self.changed = True + self.check_mode = module.check_mode + + + def generate(self, module): + """Generate the public key..""" + + if not os.path.exists(self.path) or self.force: + try: + privatekey_content = open(self.privatekey_path, 'r').read() + privatekey = crypto.load_privatekey(crypto.FILETYPE_PEM, privatekey_content) + publickey_file = open(self.path, 'w') + publickey_file.write(crypto.dump_publickey(crypto.FILETYPE_PEM, privatekey)) + publickey_file.close() + except (IOError, OSError): + e = get_exception() + raise PublicKeyError(e) + except AttributeError: + self.remove() + raise PublicKeyError('You need to have PyOpenSSL>=16.0.0 to generate public keys') + else: + self.changed = False + + file_args = module.load_file_common_arguments(module.params) + if module.set_fs_attributes_if_different(file_args, False): + self.changed = True + + def remove(self): + """Remove the public key from the filesystem.""" + + try: + os.remove(self.path) + except OSError: + e = get_exception() + if e.errno != errno.ENOENT: + raise PublicKeyError(e) + else: + self.changed = False + + def dump(self): + """Serialize the object into a dictionnary.""" + + result = { + 'privatekey': self.privatekey_path, + 'filename': self.path, + 'changed': self.changed, + } + + return result + + +def main(): + + module = AnsibleModule( + argument_spec = dict( + state=dict(default='present', choices=['present', 'absent'], type='str'), + force=dict(default=False, type='bool'), + path=dict(required=True, type='path'), + privatekey_path=dict(type='path'), + ), + supports_check_mode = True, + add_file_common_args = True, + ) + + if not pyopenssl_found: + module.fail_json(msg='the python pyOpenSSL module is required') + + path = module.params['path'] + privatekey_path = module.params['privatekey_path'] + base_dir = os.path.dirname(module.params['path']) + + if not os.path.isdir(base_dir): + module.fail_json(name=base_dir, msg='The directory %s does not exist or the file is not a directory' % base_dir) + + public_key = PublicKey(module) + if public_key.state == 'present': + + # This is only applicable when generating a new public key. + # When removing one the privatekey_path should not be required. + if not privatekey_path: + module.fail_json(msg='When generating a new public key you must specify a private key') + + if not os.path.exists(privatekey_path): + module.fail_json(name=privatekey_path, msg='The private key %s does not exist' % privatekey_path) + + if module.check_mode: + result = public_key.dump() + result['changed'] = module.params['force'] or not os.path.exists(path) + module.exit_json(**result) + + try: + public_key.generate(module) + except PublicKeyError: + e = get_exception() + module.fail_json(msg=str(e)) + else: + + if module.check_mode: + result = public_key.dump() + result['changed'] = os.path.exists(path) + module.exit_json(**result) + + try: + public_key.remove() + except PublicKeyError: + e = get_exception() + module.fail_json(msg=str(e)) + + result = public_key.dump() + + module.exit_json(**result) + + +if __name__ == '__main__': + main() From 21bdd8810b7093d6c3e5bd82878facb114725602 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Thu, 27 Oct 2016 09:28:22 -0700 Subject: [PATCH 2288/2522] install dnf python bindings if possible Fixes #14427 --- packaging/os/dnf.py | 55 ++++++++++++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index 8df9401fa16..18484ec4736 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -119,17 +119,43 @@ try: import dnf - from dnf import cli, const, exceptions, subject, util + import dnf + import dnf.cli + import dnf.const + import dnf.exceptions + import dnf.subject + import dnf.util HAS_DNF = True except ImportError: HAS_DNF = False +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import PY2 + -def _fail_if_no_dnf(module): - """Fail if unable to import dnf.""" +def _ensure_dnf(module): if not HAS_DNF: - module.fail_json( - msg="`python2-dnf` is not installed, but it is required for the Ansible dnf module.") + if PY2: + package = 'python2-dnf' + else: + package = 'python3-dnf' + + if module.check_mode: + module.fail_json(msg="`{0}` is not installed, but it is required" + " for the Ansible dnf module.".format(package)) + + module.run_command(['dnf', 'install', '-y', package], check_rc=True) + global dnf + try: + import dnf + import dnf.cli + import dnf.const + import dnf.exceptions + import dnf.subject + import dnf.util + except ImportError: + module.fail_json(msg="Could not import the dnf python module." + " Please install `{0}` package.".format(package)) def _configure_base(module, base, conf_file, disable_gpg_check): @@ -218,7 +244,7 @@ def list_items(module, base, command): for repo in base.repos.iter_enabled()] # Return any matching packages else: - packages = subject.Subject(command).get_best_query(base.sack) + packages = dnf.subject.Subject(command).get_best_query(base.sack) results = [_package_dict(package) for package in packages] module.exit_json(results=results) @@ -228,7 +254,7 @@ def _mark_package_install(module, base, pkg_spec): """Mark the package for install.""" try: base.install(pkg_spec) - except exceptions.MarkingError: + except dnf.exceptions.MarkingError: module.fail_json(msg="No package {} available.".format(pkg_spec)) @@ -237,7 +263,7 @@ def ensure(module, base, state, names): if names == ['*'] and state == 'latest': base.upgrade_all() else: - pkg_specs, group_specs, filenames = cli.commands.parse_spec_group_file( + pkg_specs, group_specs, filenames = dnf.cli.commands.parse_spec_group_file( names) if group_specs: base.read_comps() @@ -257,7 +283,7 @@ def ensure(module, base, state, names): base.package_install(base.add_remote_rpm(filename)) # Install groups. for group in groups: - base.group_install(group, const.GROUP_PACKAGE_TYPES) + base.group_install(group, dnf.const.GROUP_PACKAGE_TYPES) # Install packages. for pkg_spec in pkg_specs: _mark_package_install(module, base, pkg_spec) @@ -269,9 +295,9 @@ def ensure(module, base, state, names): for group in groups: try: base.group_upgrade(group) - except exceptions.CompsError: + except dnf.exceptions.CompsError: # If not already installed, try to install. - base.group_install(group, const.GROUP_PACKAGE_TYPES) + base.group_install(group, dnf.const.GROUP_PACKAGE_TYPES) for pkg_spec in pkg_specs: # best effort causes to install the latest package # even if not previously installed @@ -331,7 +357,8 @@ def main(): supports_check_mode=True) params = module.params - _fail_if_no_dnf(module) + _ensure_dnf(module) + if params['list']: base = _base( module, params['conf_file'], params['disable_gpg_check'], @@ -340,7 +367,7 @@ def main(): else: # Note: base takes a long time to run so we want to check for failure # before running it. - if not util.am_i_root(): + if not dnf.util.am_i_root(): module.fail_json(msg="This command has to be run under the root user.") base = _base( module, params['conf_file'], params['disable_gpg_check'], @@ -349,7 +376,5 @@ def main(): ensure(module, base, params['state'], params['name']) -# import module snippets -from ansible.module_utils.basic import * if __name__ == '__main__': main() From 86d71bd97186e95ac00057a1bd976716ffdb1987 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Thu, 27 Oct 2016 12:36:46 -0700 Subject: [PATCH 2289/2522] * Fix for spaces in the package spec. * Fix for python-2.6 compat --- packaging/os/dnf.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index 18484ec4736..65f3a082a19 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -279,13 +279,13 @@ def ensure(module, base, state, names): if state in ['installed', 'present']: # Install files. - for filename in filenames: + for filename in (f.strip() for f in filenames): base.package_install(base.add_remote_rpm(filename)) # Install groups. - for group in groups: + for group in (g.strip() for g in groups): base.group_install(group, dnf.const.GROUP_PACKAGE_TYPES) # Install packages. - for pkg_spec in pkg_specs: + for pkg_spec in (p.strip() for p in pkg_specs): _mark_package_install(module, base, pkg_spec) elif state == 'latest': @@ -330,9 +330,9 @@ def ensure(module, base, state, names): base.do_transaction() response = {'changed': True, 'results': []} for package in base.transaction.install_set: - response['results'].append("Installed: {}".format(package)) + response['results'].append("Installed: {0}".format(package)) for package in base.transaction.remove_set: - response['results'].append("Removed: {}".format(package)) + response['results'].append("Removed: {0}".format(package)) module.exit_json(**response) From 9a768e7e0a957260f095a3f7f2c742197fe31fb6 Mon Sep 17 00:00:00 2001 From: Andrew Gaffney Date: Fri, 28 Oct 2016 09:29:31 -0600 Subject: [PATCH 2290/2522] Add openwrt_init module for managing services on OpenWrt (#3312) --- system/openwrt_init.py | 189 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 system/openwrt_init.py diff --git a/system/openwrt_init.py b/system/openwrt_init.py new file mode 100644 index 00000000000..0a92b18e93c --- /dev/null +++ b/system/openwrt_init.py @@ -0,0 +1,189 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2016, Andrew Gaffney +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +module: openwrt_init +author: + - "Andrew Gaffney (@agaffney)" +version_added: "2.3" +short_description: Manage services on OpenWrt. +description: + - Controls OpenWrt services on remote hosts. +options: + name: + required: true + description: + - Name of the service. + aliases: ['service'] + state: + required: false + default: null + choices: [ 'started', 'stopped', 'restarted', 'reloaded' ] + description: + - C(started)/C(stopped) are idempotent actions that will not run commands unless necessary. + C(restarted) will always bounce the service. C(reloaded) will always reload. + enabled: + required: false + choices: [ "yes", "no" ] + default: null + description: + - Whether the service should start on boot. B(At least one of state and enabled are required.) + pattern: + required: false + description: + - If the service does not respond to the 'running' command, name a + substring to look for as would be found in the output of the I(ps) + command as a stand-in for a 'running' result. If the string is found, + the service will be assumed to be running. +notes: + - One option other than name is required. +requirements: + - An OpenWrt system +''' + +EXAMPLES = ''' +# Example action to start service httpd, if not running +- openwrt_init: state=started name=httpd +# Example action to stop service cron, if running +- openwrt_init: name=cron state=stopped +# Example action to reload service httpd, in all cases +- openwrt_init: name=httpd state=reloaded +# Example action to enable service httpd +- openwrt_init: + name: httpd + enabled: yes +''' + +RETURN = ''' +''' + +import os +import glob +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_bytes, to_native + +# =========================================== +# Main control flow + +def main(): + # init + module = AnsibleModule( + argument_spec = dict( + name = dict(required=True, type='str', aliases=['service']), + state = dict(choices=['started', 'stopped', 'restarted', 'reloaded'], type='str'), + enabled = dict(type='bool'), + pattern = dict(required=False, default=None), + ), + supports_check_mode=True, + required_one_of=[['state', 'enabled']], + ) + + # initialize + service = module.params['name'] + init_script = '/etc/init.d/' + service + rc = 0 + out = err = '' + result = { + 'name': service, + 'changed': False, + } + + # check if service exists + if not os.path.exists(init_script): + module.fail_json(msg='service %s does not exist' % service) + + # Enable/disable service startup at boot if requested + if module.params['enabled'] is not None: + # do we need to enable the service? + enabled = False + (rc, out, err) = module.run_command("%s enabled" % init_script) + + if rc == 0: + enabled = True + + # default to current state + result['enabled'] = enabled + + # Change enable/disable if needed + if enabled != module.params['enabled']: + result['changed'] = True + if module.params['enabled']: + action = 'enable' + else: + action = 'disable' + + if not module.check_mode: + (rc, out, err) = module.run_command("%s %s" % (init_script, action)) + if rc != 0: + module.fail_json(msg="Unable to %s service %s: %s" % (action, service, err)) + + result['enabled'] = not enabled + + if module.params['state'] is not None: + running = False + + # check if service is currently running + if module.params['pattern']: + # Find ps binary + psbin = module.get_bin_path('ps', True) + + (rc, psout, pserr) = module.run_command('%s auxwww' % psbin) + # If rc is 0, set running as appropriate + if rc == 0: + lines = psout.split("\n") + for line in lines: + if module.params['pattern'] in line and not "pattern=" in line: + # so as to not confuse ./hacking/test-module + running = True + break + else: + (rc, out, err) = module.run_command("%s running" % init_script) + + if rc == 0: + running = True + + # default to desired state + result['state'] = module.params['state'] + + # determine action, if any + action = None + if module.params['state'] == 'started': + if not running: + action = 'start' + result['changed'] = True + elif module.params['state'] == 'stopped': + if running: + action = 'stop' + result['changed'] = True + else: + action = module.params['state'][:-2] # remove 'ed' from restarted/reloaded + result['state'] = 'started' + result['changed'] = True + + if action: + if not module.check_mode: + (rc, out, err) = module.run_command("%s %s" % (init_script, action)) + if rc != 0: + module.fail_json(msg="Unable to %s service %s: %s" % (action, service, err)) + + + module.exit_json(**result) + +if __name__ == '__main__': + main() From 8bd40fb6226fc5b4a1610badd61d5cb3a82e1ebb Mon Sep 17 00:00:00 2001 From: "Thierno IB. BARRY" Date: Fri, 28 Oct 2016 17:50:46 +0200 Subject: [PATCH 2291/2522] elasticsearch_plugin: add check mode support (#3043) --- packaging/elasticsearch_plugin.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packaging/elasticsearch_plugin.py b/packaging/elasticsearch_plugin.py index 7e01b4a4d5c..3bffd99b089 100644 --- a/packaging/elasticsearch_plugin.py +++ b/packaging/elasticsearch_plugin.py @@ -142,7 +142,10 @@ def install_plugin(module, plugin_bin, plugin_name, version, url, proxy_host, pr cmd = " ".join(cmd_args) - rc, out, err = module.run_command(cmd) + if module.check_mode: + rc, out, err = 0, "check mode", "" + else: + rc, out, err = module.run_command(cmd) if rc != 0: reason = parse_error(out) @@ -155,7 +158,10 @@ def remove_plugin(module, plugin_bin, plugin_name): cmd = " ".join(cmd_args) - rc, out, err = module.run_command(cmd) + if module.check_mode: + rc, out, err = 0, "check mode", "" + else: + rc, out, err = module.run_command(cmd) if rc != 0: reason = parse_error(out) @@ -175,7 +181,8 @@ def main(): proxy_host=dict(default=None), proxy_port=dict(default=None), version=dict(default=None) - ) + ), + supports_check_mode=True ) name = module.params["name"] From b14a548267c6e93962b4790ae95acaa5bd47ddcb Mon Sep 17 00:00:00 2001 From: Mike Rostermund Date: Sat, 29 Oct 2016 00:03:45 +0200 Subject: [PATCH 2292/2522] lxd_container: doc: Correct name and state for example of deleting (#3299) --- cloud/lxd/lxd_container.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index a92cdd7ce7e..3b00d3b4f7a 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -179,10 +179,10 @@ - hosts: localhost connection: local tasks: - - name: Restart a container + - name: Delete a container lxd_container: name: mycontainer - state: restarted + state: absent # An example for restarting a container - hosts: localhost From 5701a2e0500f4c7cd17a35e1f386cafd8883d080 Mon Sep 17 00:00:00 2001 From: Yevgeniy Valeyev Date: Sat, 29 Oct 2016 04:13:25 +0600 Subject: [PATCH 2293/2522] cloudtrail: Fix error on existing trail without S3 prefix (#2939) --- cloud/amazon/cloudtrail.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/cloud/amazon/cloudtrail.py b/cloud/amazon/cloudtrail.py index f0ca3239117..7e76f4848bc 100644 --- a/cloud/amazon/cloudtrail.py +++ b/cloud/amazon/cloudtrail.py @@ -156,13 +156,13 @@ def main(): argument_spec = ec2_argument_spec() argument_spec.update(dict( - state={'required': True, 'choices': ['enabled', 'disabled'] }, - name={'required': True, 'type': 'str' }, - s3_bucket_name={'required': False, 'type': 'str' }, - s3_key_prefix={'default':'', 'required': False, 'type': 'str' }, - include_global_events={'default':True, 'required': False, 'type': 'bool' }, + state={'required': True, 'choices': ['enabled', 'disabled']}, + name={'required': True, 'type': 'str'}, + s3_bucket_name={'required': False, 'type': 'str'}, + s3_key_prefix={'default': '', 'required': False, 'type': 'str'}, + include_global_events={'default': True, 'required': False, 'type': 'bool'}, )) - required_together = ( ['state', 's3_bucket_name'] ) + required_together = (['state', 's3_bucket_name']) module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True, required_together=required_together) @@ -180,6 +180,7 @@ def main(): s3_bucket_name = module.params['s3_bucket_name'] # remove trailing slash from the key prefix, really messes up the key structure. s3_key_prefix = module.params['s3_key_prefix'].rstrip('/') + include_global_events = module.params['include_global_events'] #if module.params['state'] == 'present' and 'ec2_elbs' not in module.params: @@ -194,7 +195,7 @@ def main(): results['view'] = cf_man.view(ct_name) # only update if the values have changed. if results['view']['S3BucketName'] != s3_bucket_name or \ - results['view']['S3KeyPrefix'] != s3_key_prefix or \ + results['view'].get('S3KeyPrefix', '') != s3_key_prefix or \ results['view']['IncludeGlobalServiceEvents'] != include_global_events: if not module.check_mode: results['update'] = cf_man.update(name=ct_name, s3_bucket_name=s3_bucket_name, s3_key_prefix=s3_key_prefix, include_global_service_events=include_global_events) From 6f6385dead33238aad34a1dccf4de7f231a389cb Mon Sep 17 00:00:00 2001 From: Andrew Gaffney Date: Fri, 28 Oct 2016 22:13:24 -0600 Subject: [PATCH 2294/2522] Minor fixes for openwrt_init for busybox ps and worthless exit codes --- system/openwrt_init.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/system/openwrt_init.py b/system/openwrt_init.py index 0a92b18e93c..c54cd3295b3 100644 --- a/system/openwrt_init.py +++ b/system/openwrt_init.py @@ -78,10 +78,22 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils._text import to_bytes, to_native +module = None +init_script = None + +# =============================== +# Check if service is enabled +def is_enabled(): + (rc, out, err) = module.run_command("%s enabled" % init_script) + if rc == 0: + return True + return False + # =========================================== # Main control flow def main(): + global module, init_script # init module = AnsibleModule( argument_spec = dict( @@ -111,11 +123,7 @@ def main(): # Enable/disable service startup at boot if requested if module.params['enabled'] is not None: # do we need to enable the service? - enabled = False - (rc, out, err) = module.run_command("%s enabled" % init_script) - - if rc == 0: - enabled = True + enabled = is_enabled() # default to current state result['enabled'] = enabled @@ -130,7 +138,10 @@ def main(): if not module.check_mode: (rc, out, err) = module.run_command("%s %s" % (init_script, action)) - if rc != 0: + # openwrt init scripts can return a non-zero exit code on a successful 'enable' + # command if the init script doesn't contain a STOP value, so we ignore the exit + # code and explicitly check if the service is now in the desired state + if is_enabled() != module.params['enabled']: module.fail_json(msg="Unable to %s service %s: %s" % (action, service, err)) result['enabled'] = not enabled @@ -143,7 +154,8 @@ def main(): # Find ps binary psbin = module.get_bin_path('ps', True) - (rc, psout, pserr) = module.run_command('%s auxwww' % psbin) + # this should be busybox ps, so we only want/need to the 'w' option + (rc, psout, pserr) = module.run_command('%s w' % psbin) # If rc is 0, set running as appropriate if rc == 0: lines = psout.split("\n") @@ -154,7 +166,6 @@ def main(): break else: (rc, out, err) = module.run_command("%s running" % init_script) - if rc == 0: running = True From 149baf4253554c33880bb952a91dc1ee54ea231a Mon Sep 17 00:00:00 2001 From: Scott Butler Date: Fri, 28 Oct 2016 15:04:50 -0700 Subject: [PATCH 2295/2522] Typo fix requested by marketing. --- monitoring/logicmonitor_facts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/logicmonitor_facts.py b/monitoring/logicmonitor_facts.py index b8f2aff7739..71705052b1d 100644 --- a/monitoring/logicmonitor_facts.py +++ b/monitoring/logicmonitor_facts.py @@ -58,7 +58,7 @@ short_description: Collect facts about LogicMonitor objects description: - LogicMonitor is a hosted, full-stack, infrastructure monitoring platform. - - This module collects facts about hosts abd host groups within your LogicMonitor account. + - This module collects facts about hosts and host groups within your LogicMonitor account. version_added: "2.2" author: [Ethan Culler-Mayeno (@ethanculler), Jeff Wozniak (@woz5999)] notes: From 33be2d4a7e6d8c045e6492d05e7cb6af795fb43d Mon Sep 17 00:00:00 2001 From: Igor Gnatenko Date: Mon, 31 Oct 2016 17:24:26 +0100 Subject: [PATCH 2296/2522] dnf: fix compatibility with DNF 2.0 (#3325) * dnf: fix compatibility with DNF 2.0 * Reimplement (copy) old dnf.cli.commands.parse_spec_group_file(), upstream uses argparse since 2.0. * add_remote_rpm() has been changed to the add_remote_rpms() Closes: https://github.com/ansible/ansible-modules-extras/issues/3310 Signed-off-by: Igor Gnatenko * fixup! dnf: fix compatibility with DNF 2.0 --- packaging/os/dnf.py | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index 65f3a082a19..1f85b92a7e2 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -258,13 +258,35 @@ def _mark_package_install(module, base, pkg_spec): module.fail_json(msg="No package {} available.".format(pkg_spec)) +def _parse_spec_group_file(names): + pkg_specs, grp_specs, filenames = [], [], [] + for name in names: + if name.endswith(".rpm"): + filenames.append(name) + elif name.startswith("@"): + grp_specs.append(name[1:]) + else: + pkg_specs.append(name) + return pkg_specs, grp_specs, filenames + + +def _install_remote_rpms(base, filenames): + if int(dnf.__version__.split(".")[0]) >= 2: + pkgs = list(sorted(base.add_remote_rpms(list(filenames)), reverse=True)) + else: + pkgs = [] + for filename in filenames: + pkgs.append(base.add_remote_rpm(filename)) + for pkg in pkgs: + base.package_install(pkg) + + def ensure(module, base, state, names): allow_erasing = False if names == ['*'] and state == 'latest': base.upgrade_all() else: - pkg_specs, group_specs, filenames = dnf.cli.commands.parse_spec_group_file( - names) + pkg_specs, group_specs, filenames = _parse_spec_group_file(names) if group_specs: base.read_comps() @@ -279,8 +301,7 @@ def ensure(module, base, state, names): if state in ['installed', 'present']: # Install files. - for filename in (f.strip() for f in filenames): - base.package_install(base.add_remote_rpm(filename)) + _install_remote_rpms(base, (f.strip() for f in filenames)) # Install groups. for group in (g.strip() for g in groups): base.group_install(group, dnf.const.GROUP_PACKAGE_TYPES) @@ -290,8 +311,7 @@ def ensure(module, base, state, names): elif state == 'latest': # "latest" is same as "installed" for filenames. - for filename in filenames: - base.package_install(base.add_remote_rpm(filename)) + _install_remote_rpms(base, filenames) for group in groups: try: base.group_upgrade(group) From 55e8996842e30a6a16f52bdf7d455bf86f458649 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Mon, 31 Oct 2016 17:15:28 -0700 Subject: [PATCH 2297/2522] Skip shard count test in check mode. (#3329) The shard count is not available in check mode. --- cloud/amazon/kinesis_stream.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/kinesis_stream.py b/cloud/amazon/kinesis_stream.py index 37f20f8c113..6a9994daca6 100644 --- a/cloud/amazon/kinesis_stream.py +++ b/cloud/amazon/kinesis_stream.py @@ -857,7 +857,7 @@ def create_stream(client, stream_name, number_of_shards=1, retention_period=None stream_found, stream_msg, current_stream = ( find_stream(client, stream_name, check_mode=check_mode) ) - if stream_found: + if stream_found and not check_mode: if current_stream['ShardsCount'] != number_of_shards: err_msg = 'Can not change the number of shards in a Kinesis Stream' return success, changed, err_msg, results From cf524673e1de7cb227fefd9716b0d73dc971dafc Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Mon, 31 Oct 2016 21:24:07 -0700 Subject: [PATCH 2298/2522] Removed tests migrated to ansible/ansible repo. (#3330) --- test/integrations/group_vars/all.yml | 1 - .../roles/ec2_vpc_nat_gateway/tasks/main.yml | 76 --- test/integrations/site.yml | 3 - .../cloud/amazon/test_ec2_vpc_nat_gateway.py | 486 ------------------ test/unit/cloud/amazon/test_kinesis_stream.py | 285 ---------- 5 files changed, 851 deletions(-) delete mode 100644 test/integrations/group_vars/all.yml delete mode 100644 test/integrations/roles/ec2_vpc_nat_gateway/tasks/main.yml delete mode 100644 test/integrations/site.yml delete mode 100644 test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py delete mode 100644 test/unit/cloud/amazon/test_kinesis_stream.py diff --git a/test/integrations/group_vars/all.yml b/test/integrations/group_vars/all.yml deleted file mode 100644 index 8a3ccba7168..00000000000 --- a/test/integrations/group_vars/all.yml +++ /dev/null @@ -1 +0,0 @@ -test_subnet_id: 'subnet-123456789' diff --git a/test/integrations/roles/ec2_vpc_nat_gateway/tasks/main.yml b/test/integrations/roles/ec2_vpc_nat_gateway/tasks/main.yml deleted file mode 100644 index f5ad5f50fc8..00000000000 --- a/test/integrations/roles/ec2_vpc_nat_gateway/tasks/main.yml +++ /dev/null @@ -1,76 +0,0 @@ -- name: Launching NAT Gateway and allocate a new eip. - ec2_vpc_nat_gateway: - region: us-west-2 - state: present - subnet_id: "{{ test_subnet_id }}" - wait: yes - wait_timeout: 600 - register: nat - -- debug: - var: nat -- fail: - msg: "Failed to create" - when: '"{{ nat["changed"] }}" != "True"' - -- name: Launch a new gateway only if one does not exist already in this subnet. - ec2_vpc_nat_gateway: - if_exist_do_not_create: yes - region: us-west-2 - state: present - subnet_id: "{{ test_subnet_id }}" - wait: yes - wait_timeout: 600 - register: nat_idempotent - -- debug: - var: nat_idempotent -- fail: - msg: "Failed to be idempotent" - when: '"{{ nat_idempotent["changed"] }}" == "True"' - -- name: Launching NAT Gateway and allocate a new eip even if one already exists in the subnet. - ec2_vpc_nat_gateway: - region: us-west-2 - state: present - subnet_id: "{{ test_subnet_id }}" - wait: yes - wait_timeout: 600 - register: new_nat - -- debug: - var: new_nat -- fail: - msg: "Failed to create" - when: '"{{ new_nat["changed"] }}" != "True"' - -- name: Launching NAT Gateway with allocation id, this call is idempotent and will not create anything. - ec2_vpc_nat_gateway: - allocation_id: eipalloc-1234567 - region: us-west-2 - state: present - subnet_id: "{{ test_subnet_id }}" - wait: yes - wait_timeout: 600 - register: nat_with_eipalloc - -- debug: - var: nat_with_eipalloc -- fail: - msg: 'Failed to be idempotent.' - when: '"{{ nat_with_eipalloc["changed"] }}" == "True"' - -- name: Delete the 1st nat gateway and do not wait for it to finish - ec2_vpc_nat_gateway: - region: us-west-2 - nat_gateway_id: "{{ nat.nat_gateway_id }}" - state: absent - -- name: Delete the nat_with_eipalloc and release the eip - ec2_vpc_nat_gateway: - region: us-west-2 - nat_gateway_id: "{{ new_nat.nat_gateway_id }}" - release_eip: yes - state: absent - wait: yes - wait_timeout: 600 diff --git a/test/integrations/site.yml b/test/integrations/site.yml deleted file mode 100644 index 62416726ebc..00000000000 --- a/test/integrations/site.yml +++ /dev/null @@ -1,3 +0,0 @@ -- hosts: 127.0.0.1 - roles: - - { role: ec2_vpc_nat_gateway } diff --git a/test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py b/test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py deleted file mode 100644 index 1b75c88a143..00000000000 --- a/test/unit/cloud/amazon/test_ec2_vpc_nat_gateway.py +++ /dev/null @@ -1,486 +0,0 @@ -#!/usr/bin/python - -import boto3 -import unittest - -from collections import namedtuple -from ansible.parsing.dataloader import DataLoader -from ansible.vars import VariableManager -from ansible.inventory import Inventory -from ansible.playbook.play import Play -from ansible.executor.task_queue_manager import TaskQueueManager - -import cloud.amazon.ec2_vpc_nat_gateway as ng - -Options = ( - namedtuple( - 'Options', [ - 'connection', 'module_path', 'forks', 'become', 'become_method', - 'become_user', 'remote_user', 'private_key_file', 'ssh_common_args', - 'sftp_extra_args', 'scp_extra_args', 'ssh_extra_args', 'verbosity', - 'check' - ] - ) -) -# initialize needed objects -variable_manager = VariableManager() -loader = DataLoader() -options = ( - Options( - connection='local', - module_path='cloud/amazon', - forks=1, become=None, become_method=None, become_user=None, check=True, - remote_user=None, private_key_file=None, ssh_common_args=None, - sftp_extra_args=None, scp_extra_args=None, ssh_extra_args=None, - verbosity=3 - ) -) -passwords = dict(vault_pass='') - -aws_region = 'us-west-2' - -# create inventory and pass to var manager -inventory = Inventory(loader=loader, variable_manager=variable_manager, host_list='localhost') -variable_manager.set_inventory(inventory) - -def run(play): - tqm = None - results = None - try: - tqm = TaskQueueManager( - inventory=inventory, - variable_manager=variable_manager, - loader=loader, - options=options, - passwords=passwords, - stdout_callback='default', - ) - results = tqm.run(play) - finally: - if tqm is not None: - tqm.cleanup() - return tqm, results - -class AnsibleVpcNatGatewayTasks(unittest.TestCase): - - def test_create_gateway_using_allocation_id(self): - play_source = dict( - name = "Create new nat gateway with eip allocation-id", - hosts = 'localhost', - gather_facts = 'no', - tasks = [ - dict( - action=dict( - module='ec2_vpc_nat_gateway', - args=dict( - subnet_id='subnet-12345678', - allocation_id='eipalloc-12345678', - wait='yes', - region=aws_region, - ) - ), - register='nat_gateway', - ), - dict( - action=dict( - module='debug', - args=dict( - msg='{{nat_gateway}}' - ) - ) - ) - ] - ) - play = Play().load(play_source, variable_manager=variable_manager, loader=loader) - tqm, results = run(play) - self.failUnless(tqm._stats.ok['localhost'] == 2) - self.failUnless(tqm._stats.changed['localhost'] == 1) - - def test_create_gateway_using_allocation_id_idempotent(self): - play_source = dict( - name = "Create new nat gateway with eip allocation-id", - hosts = 'localhost', - gather_facts = 'no', - tasks = [ - dict( - action=dict( - module='ec2_vpc_nat_gateway', - args=dict( - subnet_id='subnet-123456789', - allocation_id='eipalloc-1234567', - wait='yes', - region=aws_region, - ) - ), - register='nat_gateway', - ), - dict( - action=dict( - module='debug', - args=dict( - msg='{{nat_gateway}}' - ) - ) - ) - ] - ) - play = Play().load(play_source, variable_manager=variable_manager, loader=loader) - tqm, results = run(play) - self.failUnless(tqm._stats.ok['localhost'] == 2) - self.assertFalse(tqm._stats.changed.has_key('localhost')) - - def test_create_gateway_using_eip_address(self): - play_source = dict( - name = "Create new nat gateway with eip address", - hosts = 'localhost', - gather_facts = 'no', - tasks = [ - dict( - action=dict( - module='ec2_vpc_nat_gateway', - args=dict( - subnet_id='subnet-12345678', - eip_address='55.55.55.55', - wait='yes', - region=aws_region, - ) - ), - register='nat_gateway', - ), - dict( - action=dict( - module='debug', - args=dict( - msg='{{nat_gateway}}' - ) - ) - ) - ] - ) - play = Play().load(play_source, variable_manager=variable_manager, loader=loader) - tqm, results = run(play) - self.failUnless(tqm._stats.ok['localhost'] == 2) - self.failUnless(tqm._stats.changed['localhost'] == 1) - - def test_create_gateway_using_eip_address_idempotent(self): - play_source = dict( - name = "Create new nat gateway with eip address", - hosts = 'localhost', - gather_facts = 'no', - tasks = [ - dict( - action=dict( - module='ec2_vpc_nat_gateway', - args=dict( - subnet_id='subnet-123456789', - eip_address='55.55.55.55', - wait='yes', - region=aws_region, - ) - ), - register='nat_gateway', - ), - dict( - action=dict( - module='debug', - args=dict( - msg='{{nat_gateway}}' - ) - ) - ) - ] - ) - play = Play().load(play_source, variable_manager=variable_manager, loader=loader) - tqm, results = run(play) - self.failUnless(tqm._stats.ok['localhost'] == 2) - self.assertFalse(tqm._stats.changed.has_key('localhost')) - - def test_create_gateway_in_subnet_only_if_one_does_not_exist_already(self): - play_source = dict( - name = "Create new nat gateway only if one does not exist already", - hosts = 'localhost', - gather_facts = 'no', - tasks = [ - dict( - action=dict( - module='ec2_vpc_nat_gateway', - args=dict( - if_exist_do_not_create='yes', - subnet_id='subnet-123456789', - wait='yes', - region=aws_region, - ) - ), - register='nat_gateway', - ), - dict( - action=dict( - module='debug', - args=dict( - msg='{{nat_gateway}}' - ) - ) - ) - ] - ) - play = Play().load(play_source, variable_manager=variable_manager, loader=loader) - tqm, results = run(play) - self.failUnless(tqm._stats.ok['localhost'] == 2) - self.assertFalse(tqm._stats.changed.has_key('localhost')) - - def test_delete_gateway(self): - play_source = dict( - name = "Delete Nat Gateway", - hosts = 'localhost', - gather_facts = 'no', - tasks = [ - dict( - action=dict( - module='ec2_vpc_nat_gateway', - args=dict( - nat_gateway_id='nat-123456789', - state='absent', - wait='yes', - region=aws_region, - ) - ), - register='nat_gateway', - ), - dict( - action=dict( - module='debug', - args=dict( - msg='{{nat_gateway}}' - ) - ) - ) - ] - ) - play = Play().load(play_source, variable_manager=variable_manager, loader=loader) - tqm, results = run(play) - self.failUnless(tqm._stats.ok['localhost'] == 2) - self.assertTrue(tqm._stats.changed.has_key('localhost')) - -class AnsibleEc2VpcNatGatewayFunctions(unittest.TestCase): - - def test_convert_to_lower(self): - example = ng.DRY_RUN_GATEWAY_UNCONVERTED - converted_example = ng.convert_to_lower(example[0]) - keys = converted_example.keys() - keys.sort() - for i in range(len(keys)): - if i == 0: - self.assertEqual(keys[i], 'create_time') - if i == 1: - self.assertEqual(keys[i], 'nat_gateway_addresses') - gw_addresses_keys = converted_example[keys[i]][0].keys() - gw_addresses_keys.sort() - for j in range(len(gw_addresses_keys)): - if j == 0: - self.assertEqual(gw_addresses_keys[j], 'allocation_id') - if j == 1: - self.assertEqual(gw_addresses_keys[j], 'network_interface_id') - if j == 2: - self.assertEqual(gw_addresses_keys[j], 'private_ip') - if j == 3: - self.assertEqual(gw_addresses_keys[j], 'public_ip') - if i == 2: - self.assertEqual(keys[i], 'nat_gateway_id') - if i == 3: - self.assertEqual(keys[i], 'state') - if i == 4: - self.assertEqual(keys[i], 'subnet_id') - if i == 5: - self.assertEqual(keys[i], 'vpc_id') - - def test_get_nat_gateways(self): - client = boto3.client('ec2', region_name=aws_region) - success, err_msg, stream = ( - ng.get_nat_gateways(client, 'subnet-123456789', check_mode=True) - ) - should_return = ng.DRY_RUN_GATEWAYS - self.assertTrue(success) - self.assertEqual(stream, should_return) - - def test_get_nat_gateways_no_gateways_found(self): - client = boto3.client('ec2', region_name=aws_region) - success, err_msg, stream = ( - ng.get_nat_gateways(client, 'subnet-1234567', check_mode=True) - ) - self.assertTrue(success) - self.assertEqual(stream, []) - - def test_wait_for_status(self): - client = boto3.client('ec2', region_name=aws_region) - success, err_msg, gws = ( - ng.wait_for_status( - client, 5, 'nat-123456789', 'available', check_mode=True - ) - ) - should_return = ng.DRY_RUN_GATEWAYS[0] - self.assertTrue(success) - self.assertEqual(gws, should_return) - - def test_wait_for_status_to_timeout(self): - client = boto3.client('ec2', region_name=aws_region) - success, err_msg, gws = ( - ng.wait_for_status( - client, 2, 'nat-12345678', 'available', check_mode=True - ) - ) - self.assertFalse(success) - self.assertEqual(gws, {}) - - def test_gateway_in_subnet_exists_with_allocation_id(self): - client = boto3.client('ec2', region_name=aws_region) - gws, err_msg = ( - ng.gateway_in_subnet_exists( - client, 'subnet-123456789', 'eipalloc-1234567', check_mode=True - ) - ) - should_return = ng.DRY_RUN_GATEWAYS - self.assertEqual(gws, should_return) - - def test_gateway_in_subnet_exists_with_allocation_id_does_not_exist(self): - client = boto3.client('ec2', region_name=aws_region) - gws, err_msg = ( - ng.gateway_in_subnet_exists( - client, 'subnet-123456789', 'eipalloc-123', check_mode=True - ) - ) - should_return = list() - self.assertEqual(gws, should_return) - - def test_gateway_in_subnet_exists_without_allocation_id(self): - client = boto3.client('ec2', region_name=aws_region) - gws, err_msg = ( - ng.gateway_in_subnet_exists( - client, 'subnet-123456789', check_mode=True - ) - ) - should_return = ng.DRY_RUN_GATEWAYS - self.assertEqual(gws, should_return) - - def test_get_eip_allocation_id_by_address(self): - client = boto3.client('ec2', region_name=aws_region) - allocation_id, _ = ( - ng.get_eip_allocation_id_by_address( - client, '55.55.55.55', check_mode=True - ) - ) - should_return = 'eipalloc-1234567' - self.assertEqual(allocation_id, should_return) - - def test_get_eip_allocation_id_by_address_does_not_exist(self): - client = boto3.client('ec2', region_name=aws_region) - allocation_id, err_msg = ( - ng.get_eip_allocation_id_by_address( - client, '52.52.52.52', check_mode=True - ) - ) - self.assertEqual(err_msg, 'EIP 52.52.52.52 does not exist') - self.assertIsNone(allocation_id) - - def test_allocate_eip_address(self): - client = boto3.client('ec2', region_name=aws_region) - success, err_msg, eip_id = ( - ng.allocate_eip_address( - client, check_mode=True - ) - ) - self.assertTrue(success) - - def test_release_address(self): - client = boto3.client('ec2', region_name=aws_region) - success, _ = ( - ng.release_address( - client, 'eipalloc-1234567', check_mode=True - ) - ) - self.assertTrue(success) - - def test_create(self): - client = boto3.client('ec2', region_name=aws_region) - success, changed, err_msg, results = ( - ng.create( - client, 'subnet-123456', 'eipalloc-1234567', check_mode=True - ) - ) - self.assertTrue(success) - self.assertTrue(changed) - - def test_pre_create(self): - client = boto3.client('ec2', region_name=aws_region) - success, changed, err_msg, results = ( - ng.pre_create( - client, 'subnet-123456', check_mode=True - ) - ) - self.assertTrue(success) - self.assertTrue(changed) - - def test_pre_create_idemptotent_with_allocation_id(self): - client = boto3.client('ec2', region_name=aws_region) - success, changed, err_msg, results = ( - ng.pre_create( - client, 'subnet-123456789', allocation_id='eipalloc-1234567', check_mode=True - ) - ) - self.assertTrue(success) - self.assertFalse(changed) - - def test_pre_create_idemptotent_with_eip_address(self): - client = boto3.client('ec2', region_name=aws_region) - success, changed, err_msg, results = ( - ng.pre_create( - client, 'subnet-123456789', eip_address='55.55.55.55', check_mode=True - ) - ) - self.assertTrue(success) - self.assertFalse(changed) - - def test_pre_create_idemptotent_if_exist_do_not_create(self): - client = boto3.client('ec2', region_name=aws_region) - success, changed, err_msg, results = ( - ng.pre_create( - client, 'subnet-123456789', if_exist_do_not_create=True, check_mode=True - ) - ) - self.assertTrue(success) - self.assertFalse(changed) - - def test_delete(self): - client = boto3.client('ec2', region_name=aws_region) - success, changed, err_msg, _ = ( - ng.remove( - client, 'nat-123456789', check_mode=True - ) - ) - self.assertTrue(success) - self.assertTrue(changed) - - def test_delete_and_release_ip(self): - client = boto3.client('ec2', region_name=aws_region) - success, changed, err_msg, _ = ( - ng.remove( - client, 'nat-123456789', release_eip=True, check_mode=True - ) - ) - self.assertTrue(success) - self.assertTrue(changed) - - def test_delete_if_does_not_exist(self): - client = boto3.client('ec2', region_name=aws_region) - success, changed, err_msg, _ = ( - ng.remove( - client, 'nat-12345', check_mode=True - ) - ) - self.assertFalse(success) - self.assertFalse(changed) - -def main(): - unittest.main() - -if __name__ == '__main__': - main() diff --git a/test/unit/cloud/amazon/test_kinesis_stream.py b/test/unit/cloud/amazon/test_kinesis_stream.py deleted file mode 100644 index 280ec5e2de6..00000000000 --- a/test/unit/cloud/amazon/test_kinesis_stream.py +++ /dev/null @@ -1,285 +0,0 @@ -#!/usr/bin/python - -import boto3 -import unittest - -import cloud.amazon.kinesis_stream as kinesis_stream - -aws_region = 'us-west-2' - - -class AnsibleKinesisStreamFunctions(unittest.TestCase): - - def test_convert_to_lower(self): - example = { - 'HasMoreShards': True, - 'RetentionPeriodHours': 24, - 'StreamName': 'test', - 'StreamARN': 'arn:aws:kinesis:east-side:123456789:stream/test', - 'StreamStatus': 'ACTIVE' - } - converted_example = kinesis_stream.convert_to_lower(example) - keys = converted_example.keys() - keys.sort() - for i in range(len(keys)): - if i == 0: - self.assertEqual(keys[i], 'has_more_shards') - if i == 1: - self.assertEqual(keys[i], 'retention_period_hours') - if i == 2: - self.assertEqual(keys[i], 'stream_arn') - if i == 3: - self.assertEqual(keys[i], 'stream_name') - if i == 4: - self.assertEqual(keys[i], 'stream_status') - - def test_make_tags_in_aws_format(self): - example = { - 'env': 'development' - } - should_return = [ - { - 'Key': 'env', - 'Value': 'development' - } - ] - aws_tags = kinesis_stream.make_tags_in_aws_format(example) - self.assertEqual(aws_tags, should_return) - - def test_make_tags_in_proper_format(self): - example = [ - { - 'Key': 'env', - 'Value': 'development' - }, - { - 'Key': 'service', - 'Value': 'web' - } - ] - should_return = { - 'env': 'development', - 'service': 'web' - } - proper_tags = kinesis_stream.make_tags_in_proper_format(example) - self.assertEqual(proper_tags, should_return) - - def test_recreate_tags_from_list(self): - example = [('environment', 'development'), ('service', 'web')] - should_return = [ - { - 'Key': 'environment', - 'Value': 'development' - }, - { - 'Key': 'service', - 'Value': 'web' - } - ] - aws_tags = kinesis_stream.recreate_tags_from_list(example) - self.assertEqual(aws_tags, should_return) - - def test_get_tags(self): - client = boto3.client('kinesis', region_name=aws_region) - success, err_msg, tags = kinesis_stream.get_tags(client, 'test', True) - self.assertTrue(success) - should_return = [ - { - 'Key': 'DryRunMode', - 'Value': 'true' - } - ] - self.assertEqual(tags, should_return) - - def test_find_stream(self): - client = boto3.client('kinesis', region_name=aws_region) - success, err_msg, stream = ( - kinesis_stream.find_stream(client, 'test', check_mode=True) - ) - should_return = { - 'HasMoreShards': True, - 'RetentionPeriodHours': 24, - 'StreamName': 'test', - 'StreamARN': 'arn:aws:kinesis:east-side:123456789:stream/test', - 'StreamStatus': 'ACTIVE' - } - self.assertTrue(success) - self.assertEqual(stream, should_return) - - def test_wait_for_status(self): - client = boto3.client('kinesis', region_name=aws_region) - success, err_msg, stream = ( - kinesis_stream.wait_for_status( - client, 'test', 'ACTIVE', check_mode=True - ) - ) - should_return = { - 'HasMoreShards': True, - 'RetentionPeriodHours': 24, - 'StreamName': 'test', - 'StreamARN': 'arn:aws:kinesis:east-side:123456789:stream/test', - 'StreamStatus': 'ACTIVE' - } - self.assertTrue(success) - self.assertEqual(stream, should_return) - - def test_tags_action_create(self): - client = boto3.client('kinesis', region_name=aws_region) - tags = { - 'env': 'development', - 'service': 'web' - } - success, err_msg = ( - kinesis_stream.tags_action( - client, 'test', tags, 'create', check_mode=True - ) - ) - self.assertTrue(success) - - def test_tags_action_delete(self): - client = boto3.client('kinesis', region_name=aws_region) - tags = { - 'env': 'development', - 'service': 'web' - } - success, err_msg = ( - kinesis_stream.tags_action( - client, 'test', tags, 'delete', check_mode=True - ) - ) - self.assertTrue(success) - - def test_tags_action_invalid(self): - client = boto3.client('kinesis', region_name=aws_region) - tags = { - 'env': 'development', - 'service': 'web' - } - success, err_msg = ( - kinesis_stream.tags_action( - client, 'test', tags, 'append', check_mode=True - ) - ) - self.assertFalse(success) - - def test_update_tags(self): - client = boto3.client('kinesis', region_name=aws_region) - tags = { - 'env': 'development', - 'service': 'web' - } - success, err_msg = ( - kinesis_stream.update_tags( - client, 'test', tags, check_mode=True - ) - ) - self.assertTrue(success) - - def test_stream_action_create(self): - client = boto3.client('kinesis', region_name=aws_region) - success, err_msg = ( - kinesis_stream.stream_action( - client, 'test', 10, 'create', check_mode=True - ) - ) - self.assertTrue(success) - - def test_stream_action_delete(self): - client = boto3.client('kinesis', region_name=aws_region) - success, err_msg = ( - kinesis_stream.stream_action( - client, 'test', 10, 'delete', check_mode=True - ) - ) - self.assertTrue(success) - - def test_stream_action_invalid(self): - client = boto3.client('kinesis', region_name=aws_region) - success, err_msg = ( - kinesis_stream.stream_action( - client, 'test', 10, 'append', check_mode=True - ) - ) - self.assertFalse(success) - - def test_retention_action_increase(self): - client = boto3.client('kinesis', region_name=aws_region) - success, err_msg = ( - kinesis_stream.retention_action( - client, 'test', 48, 'increase', check_mode=True - ) - ) - self.assertTrue(success) - - def test_retention_action_decrease(self): - client = boto3.client('kinesis', region_name=aws_region) - success, err_msg = ( - kinesis_stream.retention_action( - client, 'test', 24, 'decrease', check_mode=True - ) - ) - self.assertTrue(success) - - def test_retention_action_invalid(self): - client = boto3.client('kinesis', region_name=aws_region) - success, err_msg = ( - kinesis_stream.retention_action( - client, 'test', 24, 'create', check_mode=True - ) - ) - self.assertFalse(success) - - def test_update(self): - client = boto3.client('kinesis', region_name=aws_region) - current_stream = { - 'HasMoreShards': True, - 'RetentionPeriodHours': 24, - 'StreamName': 'test', - 'StreamARN': 'arn:aws:kinesis:east-side:123456789:stream/test', - 'StreamStatus': 'ACTIVE' - } - tags = { - 'env': 'development', - 'service': 'web' - } - success, changed, err_msg = ( - kinesis_stream.update( - client, current_stream, 'test', retention_period=48, - tags=tags, check_mode=True - ) - ) - self.assertTrue(success) - self.assertTrue(changed) - self.assertEqual(err_msg, 'Kinesis Stream test updated successfully.') - - def test_create_stream(self): - client = boto3.client('kinesis', region_name=aws_region) - tags = { - 'env': 'development', - 'service': 'web' - } - success, changed, err_msg, results = ( - kinesis_stream.create_stream( - client, 'test', number_of_shards=10, retention_period=48, - tags=tags, check_mode=True - ) - ) - should_return = { - 'has_more_shards': True, - 'retention_period_hours': 24, - 'stream_name': 'test', - 'stream_arn': 'arn:aws:kinesis:east-side:123456789:stream/test', - 'stream_status': 'ACTIVE', - 'tags': tags, - } - self.assertTrue(success) - self.assertTrue(changed) - self.assertEqual(results, should_return) - self.assertEqual(err_msg, 'Kinesis Stream test updated successfully.') - - -def main(): - unittest.main() - -if __name__ == '__main__': - main() From 9a01d01f78f855d737abee4136955fa48a579ebb Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Mon, 31 Oct 2016 21:56:46 -0700 Subject: [PATCH 2299/2522] Code cleanup. --- cloud/centurylink/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cloud/centurylink/__init__.py b/cloud/centurylink/__init__.py index 8b137891791..e69de29bb2d 100644 --- a/cloud/centurylink/__init__.py +++ b/cloud/centurylink/__init__.py @@ -1 +0,0 @@ - From 34c8073a1ae0e2dc72300bf2cff54571ca6386b2 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Sat, 29 Oct 2016 19:08:37 -0700 Subject: [PATCH 2300/2522] Fix installation of environment groups In dnf, environment groups are separate from groups. Need to handle them separately when calling the API. Fixes #2178 After upstream review, hande dnf-2.0 mandatory packages in groups If mandatory packages in a group are not installed, a group will report failure. Fix this by catching the error and reporting after trying to install the other packages and groups in the transaction. --- packaging/os/dnf.py | 75 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 12 deletions(-) diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index 1f85b92a7e2..504a9dac5c6 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -205,7 +205,7 @@ def _base(module, conf_file, disable_gpg_check, disablerepo, enablerepo): base = dnf.Base() _configure_base(module, base, conf_file, disable_gpg_check) _specify_repositories(base, disablerepo, enablerepo) - base.fill_sack() + base.fill_sack(load_system_repo='auto') return base @@ -282,6 +282,9 @@ def _install_remote_rpms(base, filenames): def ensure(module, base, state, names): + # Accumulate failures. Package management modules install what they can + # and fail with a message about what they can't. + failures = [] allow_erasing = False if names == ['*'] and state == 'latest': base.upgrade_all() @@ -290,34 +293,70 @@ def ensure(module, base, state, names): if group_specs: base.read_comps() + pkg_specs = [p.strip() for p in pkg_specs] + filenames = [f.strip() for f in filenames] groups = [] + environments = [] for group_spec in group_specs: group = base.comps.group_by_pattern(group_spec) if group: - groups.append(group) + groups.append(group.strip()) else: - module.fail_json( - msg="No group {} available.".format(group_spec)) + environment = base.comps.environments_by_pattern(group_spec) + if environment: + environments.extend((e.id.strip() for e in environment)) + else: + module.fail_json( + msg="No group {} available.".format(group_spec)) if state in ['installed', 'present']: # Install files. - _install_remote_rpms(base, (f.strip() for f in filenames)) + _install_remote_rpms(base, filenames) + # Install groups. - for group in (g.strip() for g in groups): - base.group_install(group, dnf.const.GROUP_PACKAGE_TYPES) + for group in groups: + try: + base.group_install(group, dnf.const.GROUP_PACKAGE_TYPES) + except dnf.exceptions.Error as e: + # In dnf 2.0 if all the mandatory packages in a group do + # not install, an error is raised. We want to capture + # this but still install as much as possible. + failures.append((group, e)) + + for environment in environments: + try: + base.environment_install(environment, dnf.const.GROUP_PACKAGE_TYPES) + except dnf.exceptions.Error as e: + failures.append((group, e)) + # Install packages. - for pkg_spec in (p.strip() for p in pkg_specs): + for pkg_spec in pkg_specs: _mark_package_install(module, base, pkg_spec) elif state == 'latest': # "latest" is same as "installed" for filenames. _install_remote_rpms(base, filenames) + for group in groups: try: - base.group_upgrade(group) - except dnf.exceptions.CompsError: - # If not already installed, try to install. - base.group_install(group, dnf.const.GROUP_PACKAGE_TYPES) + try: + base.group_upgrade(group) + except dnf.exceptions.CompsError: + # If not already installed, try to install. + base.group_install(group, dnf.const.GROUP_PACKAGE_TYPES) + except dnf.exceptions.Error as e: + failures.append((group, e)) + + for environment in environments: + try: + try: + base.environment_upgrade(environment) + except dnf.exceptions.CompsError: + # If not already installed, try to install. + base.environment_install(group, dnf.const.GROUP_PACKAGE_TYPES) + except dnf.exceptions.Error as e: + failures.append((group, e)) + for pkg_spec in pkg_specs: # best effort causes to install the latest package # even if not previously installed @@ -334,18 +373,27 @@ def ensure(module, base, state, names): for group in groups: if installed.filter(name=group.name): base.group_remove(group) + for pkg_spec in pkg_specs: if installed.filter(name=pkg_spec): base.remove(pkg_spec) + # Like the dnf CLI we want to allow recursive removal of dependent # packages allow_erasing = True if not base.resolve(allow_erasing=allow_erasing): + if failures: + module.fail_json(msg='Failed to install some of the specified packages', + failures=failures) module.exit_json(msg="Nothing to do") else: if module.check_mode: + if failures: + module.fail_json(msg='Failed to install some of the specified packages', + failures=failures) module.exit_json(changed=True) + base.download_packages(base.transaction.install_set) base.do_transaction() response = {'changed': True, 'results': []} @@ -354,6 +402,9 @@ def ensure(module, base, state, names): for package in base.transaction.remove_set: response['results'].append("Removed: {0}".format(package)) + if failures: + module.fail_json(msg='Failed to install some of the specified packages', + failures=failures) module.exit_json(**response) From 9d51f823956e47b02abd7754522b5bd4202bd82c Mon Sep 17 00:00:00 2001 From: bmildren Date: Wed, 2 Nov 2016 19:03:10 +0000 Subject: [PATCH 2301/2522] add support for proxysql (#2917) * Adding support for proxysql * Moved and restricted imports, updated exception handling * Updated version_added, and mysqldb_found constant name * Removed unnecessary parentheses --- database/proxysql/__init__.py | 0 database/proxysql/proxysql_backend_servers.py | 547 +++++++++++++++ .../proxysql/proxysql_global_variables.py | 310 ++++++++ database/proxysql/proxysql_manage_config.py | 251 +++++++ database/proxysql/proxysql_mysql_users.py | 516 ++++++++++++++ database/proxysql/proxysql_query_rules.py | 659 ++++++++++++++++++ .../proxysql_replication_hostgroups.py | 424 +++++++++++ database/proxysql/proxysql_scheduler.py | 458 ++++++++++++ 8 files changed, 3165 insertions(+) create mode 100644 database/proxysql/__init__.py create mode 100644 database/proxysql/proxysql_backend_servers.py create mode 100644 database/proxysql/proxysql_global_variables.py create mode 100644 database/proxysql/proxysql_manage_config.py create mode 100644 database/proxysql/proxysql_mysql_users.py create mode 100644 database/proxysql/proxysql_query_rules.py create mode 100644 database/proxysql/proxysql_replication_hostgroups.py create mode 100644 database/proxysql/proxysql_scheduler.py diff --git a/database/proxysql/__init__.py b/database/proxysql/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/database/proxysql/proxysql_backend_servers.py b/database/proxysql/proxysql_backend_servers.py new file mode 100644 index 00000000000..0d743701752 --- /dev/null +++ b/database/proxysql/proxysql_backend_servers.py @@ -0,0 +1,547 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: proxysql_backend_servers +version_added: "2.3" +author: "Ben Mildren (@bmildren)" +short_description: Adds or removes mysql hosts from proxysql admin interface. +description: + - The M(proxysql_backend_servers) module adds or removes mysql hosts using + the proxysql admin interface. +options: + hostgroup_id: + description: + - The hostgroup in which this mysqld instance is included. An instance + can be part of one or more hostgroups. + default: 0 + hostname: + description: + - The ip address at which the mysqld instance can be contacted. + required: True + port: + description: + - The port at which the mysqld instance can be contacted. + default: 3306 + status: + description: + - ONLINE - Backend server is fully operational. + OFFLINE_SOFT - When a server is put into C(OFFLINE_SOFT) mode, + connections are kept in use until the current + transaction is completed. This allows to gracefully + detach a backend. + OFFLINE_HARD - When a server is put into C(OFFLINE_HARD) mode, the + existing connections are dropped, while new incoming + connections aren't accepted either. + + If ommitted the proxysql default for I(status) is C(ONLINE). + choices: [ "ONLINE", "OFFLINE_SOFT", "OFFLINE_HARD"] + weight: + description: + - The bigger the weight of a server relative to other weights, the higher + the probability of the server being chosen from the hostgroup. + If ommitted the proxysql default for I(weight) is 1. + compression: + description: + - If the value of I(compression) is greater than 0, new connections to + that server will use compression. + If ommitted the proxysql default for I(compression) is 0. + max_connections: + description: + - The maximum number of connections ProxySQL will open to this backend + server. + If ommitted the proxysql default for I(max_connections) is 1000. + max_replication_lag: + description: + - If greater than 0, ProxySQL will reguarly monitor replication lag. If + replication lag goes above I(max_replication_lag), proxysql will + temporarily shun the server until replication catches up. + If ommitted the proxysql default for I(max_replication_lag) is 0. + use_ssl: + description: + - If I(use_ssl) is set to C(True), connections to this server will be + made using SSL connections. + If ommitted the proxysql default for I(use_ssl) is C(False). + max_latency_ms: + description: + - Ping time is monitored regularly. If a host has a ping time greater + than I(max_latency_ms) it is excluded from the connection pool + (although the server stays ONLINE). + If ommitted the proxysql default for I(max_latency_ms) is 0. + comment: + description: + - Text field that can be used for any purposed defined by the user. Could + be a description of what the host stores, a reminder of when the host + was added or disabled, or a JSON processed by some checker script. + default: '' + state: + description: + - When C(present) - adds the host, when C(absent) - removes the host. + choices: [ "present", "absent" ] + default: present + save_to_disk: + description: + - Save mysql host config to sqlite db on disk to persist the + configuration. + default: True + load_to_runtime: + description: + - Dynamically load mysql host config to runtime memory. + default: True + login_user: + description: + - The username used to authenticate to ProxySQL admin interface + default: None + login_password: + description: + - The password used to authenticate to ProxySQL admin interface + default: None + login_host: + description: + - The host used to connect to ProxySQL admin interface + default: '127.0.0.1' + login_port: + description: + - The port used to connect to ProxySQL admin interface + default: 6032 + config_file: + description: + - Specify a config file from which login_user and login_password are to + be read + default: '' +''' + +EXAMPLES = ''' +--- +# This example adds a server, it saves the mysql server config to disk, but +# avoids loading the mysql server config to runtime (this might be because +# several servers are being added and the user wants to push the config to +# runtime in a single batch using the M(proxysql_manage_config) module). It +# uses supplied credentials to connect to the proxysql admin interface. + +- proxysql_backend_servers: + login_user: 'admin' + login_password: 'admin' + hostname: 'mysql01' + state: present + load_to_runtime: False + +# This example removes a server, saves the mysql server config to disk, and +# dynamically loads the mysql server config to runtime. It uses credentials +# in a supplied config file to connect to the proxysql admin interface. + +- proxysql_backend_servers: + config_file: '~/proxysql.cnf' + hostname: 'mysql02' + state: absent +''' + +RETURN = ''' +stdout: + description: The mysql host modified or removed from proxysql + returned: On create/update will return the newly modified host, on delete + it will return the deleted record. + type: dict + "sample": { + "changed": true, + "hostname": "192.168.52.1", + "msg": "Added server to mysql_hosts", + "server": { + "comment": "", + "compression": "0", + "hostgroup_id": "1", + "hostname": "192.168.52.1", + "max_connections": "1000", + "max_latency_ms": "0", + "max_replication_lag": "0", + "port": "3306", + "status": "ONLINE", + "use_ssl": "0", + "weight": "1" + }, + "state": "present" + } +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.mysql import mysql_connect +from ansible.module_utils.pycompat24 import get_exception + +try: + import MySQLdb + import MySQLdb.cursors +except ImportError: + MYSQLDB_FOUND = False +else: + MYSQLDB_FOUND = True + +# =========================================== +# proxysql module specific support methods. +# + + +def perform_checks(module): + if module.params["login_port"] < 0 \ + or module.params["login_port"] > 65535: + module.fail_json( + msg="login_port must be a valid unix port number (0-65535)" + ) + + if module.params["port"] < 0 \ + or module.params["port"] > 65535: + module.fail_json( + msg="port must be a valid unix port number (0-65535)" + ) + + if module.params["compression"]: + if module.params["compression"] < 0 \ + or module.params["compression"] > 102400: + module.fail_json( + msg="compression must be set between 0 and 102400" + ) + + if module.params["max_replication_lag"]: + if module.params["max_replication_lag"] < 0 \ + or module.params["max_replication_lag"] > 126144000: + module.fail_json( + msg="max_replication_lag must be set between 0 and 102400" + ) + + if not MYSQLDB_FOUND: + module.fail_json( + msg="the python mysqldb module is required" + ) + + +def save_config_to_disk(cursor): + cursor.execute("SAVE MYSQL SERVERS TO DISK") + return True + + +def load_config_to_runtime(cursor): + cursor.execute("LOAD MYSQL SERVERS TO RUNTIME") + return True + + +class ProxySQLServer(object): + + def __init__(self, module): + self.state = module.params["state"] + self.save_to_disk = module.params["save_to_disk"] + self.load_to_runtime = module.params["load_to_runtime"] + + self.hostgroup_id = module.params["hostgroup_id"] + self.hostname = module.params["hostname"] + self.port = module.params["port"] + + config_data_keys = ["status", + "weight", + "compression", + "max_connections", + "max_replication_lag", + "use_ssl", + "max_latency_ms", + "comment"] + + self.config_data = dict((k, module.params[k]) + for k in config_data_keys) + + def check_server_config_exists(self, cursor): + query_string = \ + """SELECT count(*) AS `host_count` + FROM mysql_servers + WHERE hostgroup_id = %s + AND hostname = %s + AND port = %s""" + + query_data = \ + [self.hostgroup_id, + self.hostname, + self.port] + + cursor.execute(query_string, query_data) + check_count = cursor.fetchone() + return (int(check_count['host_count']) > 0) + + def check_server_config(self, cursor): + query_string = \ + """SELECT count(*) AS `host_count` + FROM mysql_servers + WHERE hostgroup_id = %s + AND hostname = %s + AND port = %s""" + + query_data = \ + [self.hostgroup_id, + self.hostname, + self.port] + + for col, val in self.config_data.iteritems(): + if val is not None: + query_data.append(val) + query_string += "\n AND " + col + " = %s" + + cursor.execute(query_string, query_data) + check_count = cursor.fetchone() + return (int(check_count['host_count']) > 0) + + def get_server_config(self, cursor): + query_string = \ + """SELECT * + FROM mysql_servers + WHERE hostgroup_id = %s + AND hostname = %s + AND port = %s""" + + query_data = \ + [self.hostgroup_id, + self.hostname, + self.port] + + cursor.execute(query_string, query_data) + server = cursor.fetchone() + return server + + def create_server_config(self, cursor): + query_string = \ + """INSERT INTO mysql_servers ( + hostgroup_id, + hostname, + port""" + + cols = 3 + query_data = \ + [self.hostgroup_id, + self.hostname, + self.port] + + for col, val in self.config_data.iteritems(): + if val is not None: + cols += 1 + query_data.append(val) + query_string += ",\n" + col + + query_string += \ + (")\n" + + "VALUES (" + + "%s ," * cols) + + query_string = query_string[:-2] + query_string += ")" + + cursor.execute(query_string, query_data) + return True + + def update_server_config(self, cursor): + query_string = """UPDATE mysql_servers""" + + cols = 0 + query_data = [] + + for col, val in self.config_data.iteritems(): + if val is not None: + cols += 1 + query_data.append(val) + if cols == 1: + query_string += "\nSET " + col + "= %s," + else: + query_string += "\n " + col + " = %s," + + query_string = query_string[:-1] + query_string += ("\nWHERE hostgroup_id = %s\n AND hostname = %s" + + "\n AND port = %s") + + query_data.append(self.hostgroup_id) + query_data.append(self.hostname) + query_data.append(self.port) + + cursor.execute(query_string, query_data) + return True + + def delete_server_config(self, cursor): + query_string = \ + """DELETE FROM mysql_servers + WHERE hostgroup_id = %s + AND hostname = %s + AND port = %s""" + + query_data = \ + [self.hostgroup_id, + self.hostname, + self.port] + + cursor.execute(query_string, query_data) + return True + + def manage_config(self, cursor, state): + if state: + if self.save_to_disk: + save_config_to_disk(cursor) + if self.load_to_runtime: + load_config_to_runtime(cursor) + + def create_server(self, check_mode, result, cursor): + if not check_mode: + result['changed'] = \ + self.create_server_config(cursor) + result['msg'] = "Added server to mysql_hosts" + result['server'] = \ + self.get_server_config(cursor) + self.manage_config(cursor, + result['changed']) + else: + result['changed'] = True + result['msg'] = ("Server would have been added to" + + " mysql_hosts, however check_mode" + + " is enabled.") + + def update_server(self, check_mode, result, cursor): + if not check_mode: + result['changed'] = \ + self.update_server_config(cursor) + result['msg'] = "Updated server in mysql_hosts" + result['server'] = \ + self.get_server_config(cursor) + self.manage_config(cursor, + result['changed']) + else: + result['changed'] = True + result['msg'] = ("Server would have been updated in" + + " mysql_hosts, however check_mode" + + " is enabled.") + + def delete_server(self, check_mode, result, cursor): + if not check_mode: + result['server'] = \ + self.get_server_config(cursor) + result['changed'] = \ + self.delete_server_config(cursor) + result['msg'] = "Deleted server from mysql_hosts" + self.manage_config(cursor, + result['changed']) + else: + result['changed'] = True + result['msg'] = ("Server would have been deleted from" + + " mysql_hosts, however check_mode is" + + " enabled.") + +# =========================================== +# Module execution. +# + + +def main(): + module = AnsibleModule( + argument_spec=dict( + login_user=dict(default=None, type='str'), + login_password=dict(default=None, no_log=True, type='str'), + login_host=dict(default='127.0.0.1'), + login_unix_socket=dict(default=None), + login_port=dict(default=6032, type='int'), + config_file=dict(default='', type='path'), + hostgroup_id=dict(default=0, type='int'), + hostname=dict(required=True, type='str'), + port=dict(default=3306, type='int'), + status=dict(choices=['ONLINE', + 'OFFLINE_SOFT', + 'OFFLINE_HARD']), + weight=dict(type='int'), + compression=dict(type='int'), + max_connections=dict(type='int'), + max_replication_lag=dict(type='int'), + use_ssl=dict(type='bool'), + max_latency_ms=dict(type='int'), + comment=dict(default='', type='str'), + state=dict(default='present', choices=['present', + 'absent']), + save_to_disk=dict(default=True, type='bool'), + load_to_runtime=dict(default=True, type='bool') + ), + supports_check_mode=True + ) + + perform_checks(module) + + login_user = module.params["login_user"] + login_password = module.params["login_password"] + config_file = module.params["config_file"] + + cursor = None + try: + cursor = mysql_connect(module, + login_user, + login_password, + config_file, + cursor_class=MySQLdb.cursors.DictCursor) + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to connect to ProxySQL Admin Module.. %s" % e + ) + + proxysql_server = ProxySQLServer(module) + result = {} + + result['state'] = proxysql_server.state + if proxysql_server.hostname: + result['hostname'] = proxysql_server.hostname + + if proxysql_server.state == "present": + try: + if not proxysql_server.check_server_config_exists(cursor): + if not proxysql_server.check_server_config(cursor): + proxysql_server.create_server(module.check_mode, + result, + cursor) + else: + proxysql_server.update_server(module.check_mode, + result, + cursor) + else: + result['changed'] = False + result['msg'] = ("The server already exists in mysql_hosts" + + " and doesn't need to be updated.") + result['server'] = \ + proxysql_server.get_server_config(cursor) + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to modify server.. %s" % e + ) + + elif proxysql_server.state == "absent": + try: + if proxysql_server.check_server_config_exists(cursor): + proxysql_server.delete_server(module.check_mode, + result, + cursor) + else: + result['changed'] = False + result['msg'] = ("The server is already absent from the" + + " mysql_hosts memory configuration") + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to remove server.. %s" % e + ) + + module.exit_json(**result) + +if __name__ == '__main__': + main() diff --git a/database/proxysql/proxysql_global_variables.py b/database/proxysql/proxysql_global_variables.py new file mode 100644 index 00000000000..36f6019fc7b --- /dev/null +++ b/database/proxysql/proxysql_global_variables.py @@ -0,0 +1,310 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: proxysql_global_variables +version_added: "2.3" +author: "Ben Mildren (@bmildren)" +short_description: Gets or sets the proxysql global variables. +description: + - The M(proxysql_global_variables) module gets or sets the proxysql global + variables. +options: + variable: + description: + - Defines which variable should be returned, or if I(value) is specified + which variable should be updated. + required: True + value: + description: + - Defines a value the variable specified using I(variable) should be set + to. + save_to_disk: + description: + - Save mysql host config to sqlite db on disk to persist the + configuration. + default: True + load_to_runtime: + description: + - Dynamically load mysql host config to runtime memory. + default: True + login_user: + description: + - The username used to authenticate to ProxySQL admin interface + default: None + login_password: + description: + - The password used to authenticate to ProxySQL admin interface + default: None + login_host: + description: + - The host used to connect to ProxySQL admin interface + default: '127.0.0.1' + login_port: + description: + - The port used to connect to ProxySQL admin interface + default: 6032 + config_file: + description: + - Specify a config file from which login_user and login_password are to + be read + default: '' +''' + +EXAMPLES = ''' +--- +# This example sets the value of a variable, saves the mysql admin variables +# config to disk, and dynamically loads the mysql admin variables config to +# runtime. It uses supplied credentials to connect to the proxysql admin +# interface. + +- proxysql_global_variables: + login_user: 'admin' + login_password: 'admin' + variable: 'mysql-max_connections' + value: 4096 + +# This example gets the value of a variable. It uses credentials in a +# supplied config file to connect to the proxysql admin interface. + +- proxysql_global_variables: + config_file: '~/proxysql.cnf' + variable: 'mysql-default_query_delay' +''' + +RETURN = ''' +stdout: + description: Returns the mysql variable supplied with it's associted value. + returned: Returns the current variable and value, or the newly set value + for the variable supplied.. + type: dict + "sample": { + "changed": false, + "msg": "The variable is already been set to the supplied value", + "var": { + "variable_name": "mysql-poll_timeout", + "variable_value": "3000" + } + } +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.mysql import mysql_connect +from ansible.module_utils.pycompat24 import get_exception + +try: + import MySQLdb + import MySQLdb.cursors +except ImportError: + MYSQLDB_FOUND = False +else: + MYSQLDB_FOUND = True + +# =========================================== +# proxysql module specific support methods. +# + + +def perform_checks(module): + if module.params["login_port"] < 0 \ + or module.params["login_port"] > 65535: + module.fail_json( + msg="login_port must be a valid unix port number (0-65535)" + ) + + if not MYSQLDB_FOUND: + module.fail_json( + msg="the python mysqldb module is required" + ) + + +def save_config_to_disk(variable, cursor): + if variable.startswith("admin"): + cursor.execute("SAVE ADMIN VARIABLES TO DISK") + else: + cursor.execute("SAVE MYSQL VARIABLES TO DISK") + return True + + +def load_config_to_runtime(variable, cursor): + if variable.startswith("admin"): + cursor.execute("LOAD ADMIN VARIABLES TO RUNTIME") + else: + cursor.execute("LOAD MYSQL VARIABLES TO RUNTIME") + return True + + +def check_config(variable, value, cursor): + query_string = \ + """SELECT count(*) AS `variable_count` + FROM global_variables + WHERE variable_name = %s and variable_value = %s""" + + query_data = \ + [variable, value] + + cursor.execute(query_string, query_data) + check_count = cursor.fetchone() + return (int(check_count['variable_count']) > 0) + + +def get_config(variable, cursor): + + query_string = \ + """SELECT * + FROM global_variables + WHERE variable_name = %s""" + + query_data = \ + [variable, ] + + cursor.execute(query_string, query_data) + row_count = cursor.rowcount + resultset = cursor.fetchone() + + if row_count > 0: + return resultset + else: + return False + + +def set_config(variable, value, cursor): + + query_string = \ + """UPDATE global_variables + SET variable_value = %s + WHERE variable_name = %s""" + + query_data = \ + [value, variable] + + cursor.execute(query_string, query_data) + return True + + +def manage_config(variable, save_to_disk, load_to_runtime, cursor, state): + if state: + if save_to_disk: + save_config_to_disk(variable, cursor) + if load_to_runtime: + load_config_to_runtime(variable, cursor) + +# =========================================== +# Module execution. +# + + +def main(): + module = AnsibleModule( + argument_spec=dict( + login_user=dict(default=None, type='str'), + login_password=dict(default=None, no_log=True, type='str'), + login_host=dict(default="127.0.0.1"), + login_unix_socket=dict(default=None), + login_port=dict(default=6032, type='int'), + config_file=dict(default="", type='path'), + variable=dict(required=True, type='str'), + value=dict(), + save_to_disk=dict(default=True, type='bool'), + load_to_runtime=dict(default=True, type='bool') + ), + supports_check_mode=True + ) + + perform_checks(module) + + login_user = module.params["login_user"] + login_password = module.params["login_password"] + config_file = module.params["config_file"] + variable = module.params["variable"] + value = module.params["value"] + save_to_disk = module.params["save_to_disk"] + load_to_runtime = module.params["load_to_runtime"] + + cursor = None + try: + cursor = mysql_connect(module, + login_user, + login_password, + config_file, + cursor_class=MySQLdb.cursors.DictCursor) + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to connect to ProxySQL Admin Module.. %s" % e + ) + + result = {} + + if not value: + try: + if get_config(variable, cursor): + result['changed'] = False + result['msg'] = \ + "Returned the variable and it's current value" + result['var'] = get_config(variable, cursor) + else: + module.fail_json( + msg="The variable \"%s\" was not found" % variable + ) + + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to get config.. %s" % e + ) + else: + try: + if get_config(variable, cursor): + if not check_config(variable, value, cursor): + if not module.check_mode: + result['changed'] = set_config(variable, value, cursor) + result['msg'] = \ + "Set the variable to the supplied value" + result['var'] = get_config(variable, cursor) + manage_config(variable, + save_to_disk, + load_to_runtime, + cursor, + result['changed']) + else: + result['changed'] = True + result['msg'] = ("Variable would have been set to" + + " the supplied value, however" + + " check_mode is enabled.") + else: + result['changed'] = False + result['msg'] = ("The variable is already been set to" + + " the supplied value") + result['var'] = get_config(variable, cursor) + else: + module.fail_json( + msg="The variable \"%s\" was not found" % variable + ) + + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to set config.. %s" % e + ) + + module.exit_json(**result) + +if __name__ == '__main__': + main() diff --git a/database/proxysql/proxysql_manage_config.py b/database/proxysql/proxysql_manage_config.py new file mode 100644 index 00000000000..57a6ecf6967 --- /dev/null +++ b/database/proxysql/proxysql_manage_config.py @@ -0,0 +1,251 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: proxysql_manage_config +version_added: "2.3" + +author: "Ben Mildren (@bmildren)" +short_description: Writes the proxysql configuration settings between layers. +description: + - The M(proxysql_global_variables) module writes the proxysql configuration + settings between layers. Currently this module will always report a + changed state, so should typically be used with WHEN however this will + change in a future version when the CHECKSUM table commands are available + for all tables in proxysql. +options: + action: + description: + - The supplied I(action) combines with the supplied I(direction) to + provide the semantics of how we want to move the I(config_settings) + between the I(config_layers). + choices: [ "LOAD", "SAVE" ] + required: True + config_settings: + description: + - The I(config_settings) specifies which configuration we're writing. + choices: [ "MYSQL USERS", "MYSQL SERVERS", "MYSQL QUERY RULES", + "MYSQL VARIABLES", "ADMIN VARIABLES", "SCHEDULER" ] + required: True + direction: + description: + - FROM - denotes we're reading values FROM the supplied I(config_layer) + and writing to the next layer + TO - denotes we're reading from the previous layer and writing TO the + supplied I(config_layer). + choices: [ "FROM", "TO" ] + required: True + config_layer: + description: + - RUNTIME - represents the in-memory data structures of ProxySQL used by + the threads that are handling the requests. + MEMORY - (sometime also referred as main) represents the in-memory + SQLite3 database. + DISK - represents the on-disk SQLite3 database. + CONFIG - is the classical config file. You can only LOAD FROM the + config file. + choices: [ "MEMORY", "DISK", "RUNTIME", "CONFIG" ] + required: True + login_user: + description: + - The username used to authenticate to ProxySQL admin interface + default: None + login_password: + description: + - The password used to authenticate to ProxySQL admin interface + default: None + login_host: + description: + - The host used to connect to ProxySQL admin interface + default: '127.0.0.1' + login_port: + description: + - The port used to connect to ProxySQL admin interface + default: 6032 + config_file: + description: + - Specify a config file from which login_user and login_password are to + be read + default: '' +''' + +EXAMPLES = ''' +--- +# This example saves the mysql users config from memory to disk. It uses +# supplied credentials to connect to the proxysql admin interface. + +- proxysql_global_variables: + login_user: 'admin' + login_password: 'admin' + action: "SAVE" + config_settings: "MYSQL USERS" + direction: "FROM" + config_layer: "MEMORY" + +# This example loads the mysql query rules config from memory to to runtime. It +# uses supplied credentials to connect to the proxysql admin interface. + +- proxysql_global_variables: + config_file: '~/proxysql.cnf' + action: "LOAD" + config_settings: "MYSQL QUERY RULES" + direction: "TO" + config_layer: "RUNTIME" +''' + +RETURN = ''' +stdout: + description: Simply reports whether the action reported a change. + returned: Currently the returned value with always be changed=True. + type: dict + "sample": { + "changed": true + } +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.mysql import mysql_connect +from ansible.module_utils.pycompat24 import get_exception + +try: + import MySQLdb +except ImportError: + MYSQLDB_FOUND = False +else: + MYSQLDB_FOUND = True + +# =========================================== +# proxysql module specific support methods. +# + + +def perform_checks(module): + if module.params["login_port"] < 0 \ + or module.params["login_port"] > 65535: + module.fail_json( + msg="login_port must be a valid unix port number (0-65535)" + ) + + if module.params["config_layer"] == 'CONFIG' and \ + (module.params["action"] != 'LOAD' or + module.params["direction"] != 'FROM'): + + if (module.params["action"] != 'LOAD' and + module.params["direction"] != 'FROM'): + msg_string = ("Neither the action \"%s\" nor the direction" + + " \"%s\" are valid combination with the CONFIG" + + " config_layer") + module.fail_json(msg=msg_string % (module.params["action"], + module.params["direction"])) + + elif module.params["action"] != 'LOAD': + msg_string = ("The action \"%s\" is not a valid combination" + + " with the CONFIG config_layer") + module.fail_json(msg=msg_string % module.params["action"]) + + else: + msg_string = ("The direction \"%s\" is not a valid combination" + + " with the CONFIG config_layer") + module.fail_json(msg=msg_string % module.params["direction"]) + + if not MYSQLDB_FOUND: + module.fail_json( + msg="the python mysqldb module is required" + ) + + +def manage_config(manage_config_settings, cursor): + + query_string = "%s" % ' '.join(manage_config_settings) + + cursor.execute(query_string) + return True + +# =========================================== +# Module execution. +# + + +def main(): + module = AnsibleModule( + argument_spec=dict( + login_user=dict(default=None, type='str'), + login_password=dict(default=None, no_log=True, type='str'), + login_host=dict(default="127.0.0.1"), + login_unix_socket=dict(default=None), + login_port=dict(default=6032, type='int'), + config_file=dict(default="", type='path'), + action=dict(required=True, choices=['LOAD', + 'SAVE']), + config_settings=dict(required=True, choices=['MYSQL USERS', + 'MYSQL SERVERS', + 'MYSQL QUERY RULES', + 'MYSQL VARIABLES', + 'ADMIN VARIABLES', + 'SCHEDULER']), + direction=dict(required=True, choices=['FROM', + 'TO']), + config_layer=dict(required=True, choices=['MEMORY', + 'DISK', + 'RUNTIME', + 'CONFIG']) + ), + supports_check_mode=True + ) + + perform_checks(module) + + login_user = module.params["login_user"] + login_password = module.params["login_password"] + config_file = module.params["config_file"] + action = module.params["action"] + config_settings = module.params["config_settings"] + direction = module.params["direction"] + config_layer = module.params["config_layer"] + + cursor = None + try: + cursor = mysql_connect(module, + login_user, + login_password, + config_file) + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to connect to ProxySQL Admin Module.. %s" % e + ) + + result = {} + + manage_config_settings = \ + [action, config_settings, direction, config_layer] + + try: + result['changed'] = manage_config(manage_config_settings, + cursor) + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to manage config.. %s" % e + ) + + module.exit_json(**result) + +if __name__ == '__main__': + main() diff --git a/database/proxysql/proxysql_mysql_users.py b/database/proxysql/proxysql_mysql_users.py new file mode 100644 index 00000000000..4eec7d9b60d --- /dev/null +++ b/database/proxysql/proxysql_mysql_users.py @@ -0,0 +1,516 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: proxysql_mysql_users +version_added: "2.3" +author: "Ben Mildren (@bmildren)" +short_description: Adds or removes mysql users from proxysql admin interface. +description: + - The M(proxysql_mysql_users) module adds or removes mysql users using the + proxysql admin interface. +options: + username: + description: + - Name of the user connecting to the mysqld or ProxySQL instance. + required: True + password: + description: + - Password of the user connecting to the mysqld or ProxySQL instance. + active: + description: + - A user with I(active) set to C(False) will be tracked in the database, + but will be never loaded in the in-memory data structures. + If ommitted the proxysql default for I(active) is C(True). + use_ssl: + description: + - If I(use_ssl) is set to C(True), connections by this user will be + made using SSL connections. + If ommitted the proxysql default for I(use_ssl) is C(False). + default_hostgroup: + description: + - If there is no matching rule for the queries sent by this user, the + traffic it generates is sent to the specified hostgroup. + If ommitted the proxysql default for I(use_ssl) is 0. + default_schema: + description: + - The schema to which the connection should change to by default. + transaction_persistent: + description: + - If this is set for the user with which the MySQL client is connecting + to ProxySQL (thus a "frontend" user), transactions started within a + hostgroup will remain within that hostgroup regardless of any other + rules. + If ommitted the proxysql default for I(transaction_persistent) is + C(False). + fast_forward: + description: + - If I(fast_forward) is set to C(True), I(fast_forward) will bypass the + query processing layer (rewriting, caching) and pass through the query + directly as is to the backend server. + If ommitted the proxysql default for I(fast_forward) is C(False). + backend: + description: + - If I(backend) is set to C(True), this (username, password) pair is + used for authenticating to the ProxySQL instance. + default: True + frontend: + description: + - If I(frontend) is set to C(True), this (username, password) pair is + used for authenticating to the mysqld servers against any hostgroup. + default: True + max_connections: + description: + - The maximum number of connections ProxySQL will open to the backend + for this user. + If ommitted the proxysql default for I(max_connections) is 10000. + state: + description: + - When C(present) - adds the user, when C(absent) - removes the user. + choices: [ "present", "absent" ] + default: present + save_to_disk: + description: + - Save mysql host config to sqlite db on disk to persist the + configuration. + default: True + load_to_runtime: + description: + - Dynamically load mysql host config to runtime memory. + default: True + login_user: + description: + - The username used to authenticate to ProxySQL admin interface + default: None + login_password: + description: + - The password used to authenticate to ProxySQL admin interface + default: None + login_host: + description: + - The host used to connect to ProxySQL admin interface + default: '127.0.0.1' + login_port: + description: + - The port used to connect to ProxySQL admin interface + default: 6032 + config_file: + description: + - Specify a config file from which login_user and login_password are to + be read + default: '' +''' + +EXAMPLES = ''' +--- +# This example adds a user, it saves the mysql user config to disk, but +# avoids loading the mysql user config to runtime (this might be because +# several users are being added and the user wants to push the config to +# runtime in a single batch using the M(proxysql_manage_config) module). It +# uses supplied credentials to connect to the proxysql admin interface. + +- proxysql_mysql_users: + login_user: 'admin' + login_password: 'admin' + username: 'productiondba' + state: present + load_to_runtime: False + +# This example removes a user, saves the mysql user config to disk, and +# dynamically loads the mysql user config to runtime. It uses credentials +# in a supplied config file to connect to the proxysql admin interface. + +- proxysql_mysql_users: + config_file: '~/proxysql.cnf' + username: 'mysqlboy' + state: absent +''' + +RETURN = ''' +stdout: + description: The mysql user modified or removed from proxysql + returned: On create/update will return the newly modified user, on delete + it will return the deleted record. + type: dict + sample": { + "changed": true, + "msg": "Added user to mysql_users", + "state": "present", + "user": { + "active": "1", + "backend": "1", + "default_hostgroup": "1", + "default_schema": null, + "fast_forward": "0", + "frontend": "1", + "max_connections": "10000", + "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", + "schema_locked": "0", + "transaction_persistent": "0", + "use_ssl": "0", + "username": "guest_ro" + }, + "username": "guest_ro" + } +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.mysql import mysql_connect +from ansible.module_utils.pycompat24 import get_exception + +try: + import MySQLdb + import MySQLdb.cursors +except ImportError: + MYSQLDB_FOUND = False +else: + MYSQLDB_FOUND = True + +# =========================================== +# proxysql module specific support methods. +# + + +def perform_checks(module): + if module.params["login_port"] < 0 \ + or module.params["login_port"] > 65535: + module.fail_json( + msg="login_port must be a valid unix port number (0-65535)" + ) + + if not MYSQLDB_FOUND: + module.fail_json( + msg="the python mysqldb module is required" + ) + + +def save_config_to_disk(cursor): + cursor.execute("SAVE MYSQL USERS TO DISK") + return True + + +def load_config_to_runtime(cursor): + cursor.execute("LOAD MYSQL USERS TO RUNTIME") + return True + + +class ProxySQLUser(object): + + def __init__(self, module): + self.state = module.params["state"] + self.save_to_disk = module.params["save_to_disk"] + self.load_to_runtime = module.params["load_to_runtime"] + + self.username = module.params["username"] + self.backend = module.params["backend"] + self.frontend = module.params["frontend"] + + config_data_keys = ["password", + "active", + "use_ssl", + "default_hostgroup", + "default_schema", + "transaction_persistent", + "fast_forward", + "max_connections"] + + self.config_data = dict((k, module.params[k]) + for k in config_data_keys) + + def check_user_config_exists(self, cursor): + query_string = \ + """SELECT count(*) AS `user_count` + FROM mysql_users + WHERE username = %s + AND backend = %s + AND frontend = %s""" + + query_data = \ + [self.username, + self.backend, + self.frontend] + + cursor.execute(query_string, query_data) + check_count = cursor.fetchone() + return (int(check_count['user_count']) > 0) + + def check_user_privs(self, cursor): + query_string = \ + """SELECT count(*) AS `user_count` + FROM mysql_users + WHERE username = %s + AND backend = %s + AND frontend = %s""" + + query_data = \ + [self.username, + self.backend, + self.frontend] + + for col, val in self.config_data.iteritems(): + if val is not None: + query_data.append(val) + query_string += "\n AND " + col + " = %s" + + cursor.execute(query_string, query_data) + check_count = cursor.fetchone() + return (int(check_count['user_count']) > 0) + + def get_user_config(self, cursor): + query_string = \ + """SELECT * + FROM mysql_users + WHERE username = %s + AND backend = %s + AND frontend = %s""" + + query_data = \ + [self.username, + self.backend, + self.frontend] + + cursor.execute(query_string, query_data) + user = cursor.fetchone() + return user + + def create_user_config(self, cursor): + query_string = \ + """INSERT INTO mysql_users ( + username, + backend, + frontend""" + + cols = 3 + query_data = \ + [self.username, + self.backend, + self.frontend] + + for col, val in self.config_data.iteritems(): + if val is not None: + cols += 1 + query_data.append(val) + query_string += ",\n" + col + + query_string += \ + (")\n" + + "VALUES (" + + "%s ," * cols) + + query_string = query_string[:-2] + query_string += ")" + + cursor.execute(query_string, query_data) + return True + + def update_user_config(self, cursor): + query_string = """UPDATE mysql_users""" + + cols = 0 + query_data = [] + + for col, val in self.config_data.iteritems(): + if val is not None: + cols += 1 + query_data.append(val) + if cols == 1: + query_string += "\nSET " + col + "= %s," + else: + query_string += "\n " + col + " = %s," + + query_string = query_string[:-1] + query_string += ("\nWHERE username = %s\n AND backend = %s" + + "\n AND frontend = %s") + + query_data.append(self.username) + query_data.append(self.backend) + query_data.append(self.frontend) + + cursor.execute(query_string, query_data) + return True + + def delete_user_config(self, cursor): + query_string = \ + """DELETE FROM mysql_users + WHERE username = %s + AND backend = %s + AND frontend = %s""" + + query_data = \ + [self.username, + self.backend, + self.frontend] + + cursor.execute(query_string, query_data) + return True + + def manage_config(self, cursor, state): + if state: + if self.save_to_disk: + save_config_to_disk(cursor) + if self.load_to_runtime: + load_config_to_runtime(cursor) + + def create_user(self, check_mode, result, cursor): + if not check_mode: + result['changed'] = \ + self.create_user_config(cursor) + result['msg'] = "Added user to mysql_users" + result['user'] = \ + self.get_user_config(cursor) + self.manage_config(cursor, + result['changed']) + else: + result['changed'] = True + result['msg'] = ("User would have been added to" + + " mysql_users, however check_mode" + + " is enabled.") + + def update_user(self, check_mode, result, cursor): + if not check_mode: + result['changed'] = \ + self.update_user_config(cursor) + result['msg'] = "Updated user in mysql_users" + result['user'] = \ + self.get_user_config(cursor) + self.manage_config(cursor, + result['changed']) + else: + result['changed'] = True + result['msg'] = ("User would have been updated in" + + " mysql_users, however check_mode" + + " is enabled.") + + def delete_user(self, check_mode, result, cursor): + if not check_mode: + result['user'] = \ + self.get_user_config(cursor) + result['changed'] = \ + self.delete_user_config(cursor) + result['msg'] = "Deleted user from mysql_users" + self.manage_config(cursor, + result['changed']) + else: + result['changed'] = True + result['msg'] = ("User would have been deleted from" + + " mysql_users, however check_mode is" + + " enabled.") + +# =========================================== +# Module execution. +# + + +def main(): + module = AnsibleModule( + argument_spec=dict( + login_user=dict(default=None, type='str'), + login_password=dict(default=None, no_log=True, type='str'), + login_host=dict(default="127.0.0.1"), + login_unix_socket=dict(default=None), + login_port=dict(default=6032, type='int'), + config_file=dict(default='', type='path'), + username=dict(required=True, type='str'), + password=dict(no_log=True, type='str'), + active=dict(type='bool'), + use_ssl=dict(type='bool'), + default_hostgroup=dict(type='int'), + default_schema=dict(type='str'), + transaction_persistent=dict(type='bool'), + fast_forward=dict(type='bool'), + backend=dict(default=True, type='bool'), + frontend=dict(default=True, type='bool'), + max_connections=dict(type='int'), + state=dict(default='present', choices=['present', + 'absent']), + save_to_disk=dict(default=True, type='bool'), + load_to_runtime=dict(default=True, type='bool') + ), + supports_check_mode=True + ) + + perform_checks(module) + + login_user = module.params["login_user"] + login_password = module.params["login_password"] + config_file = module.params["config_file"] + + cursor = None + try: + cursor = mysql_connect(module, + login_user, + login_password, + config_file, + cursor_class=MySQLdb.cursors.DictCursor) + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to connect to ProxySQL Admin Module.. %s" % e + ) + + proxysql_user = ProxySQLUser(module) + result = {} + + result['state'] = proxysql_user.state + if proxysql_user.username: + result['username'] = proxysql_user.username + + if proxysql_user.state == "present": + try: + if not proxysql_user.check_user_privs(cursor): + if not proxysql_user.check_user_config_exists(cursor): + proxysql_user.create_user(module.check_mode, + result, + cursor) + else: + proxysql_user.update_user(module.check_mode, + result, + cursor) + else: + result['changed'] = False + result['msg'] = ("The user already exists in mysql_users" + + " and doesn't need to be updated.") + result['user'] = \ + proxysql_user.get_user_config(cursor) + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to modify user.. %s" % e + ) + + elif proxysql_user.state == "absent": + try: + if proxysql_user.check_user_config_exists(cursor): + proxysql_user.delete_user(module.check_mode, + result, + cursor) + else: + result['changed'] = False + result['msg'] = ("The user is already absent from the" + + " mysql_users memory configuration") + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to remove user.. %s" % e + ) + + module.exit_json(**result) + +if __name__ == '__main__': + main() diff --git a/database/proxysql/proxysql_query_rules.py b/database/proxysql/proxysql_query_rules.py new file mode 100644 index 00000000000..8af46c6ab9c --- /dev/null +++ b/database/proxysql/proxysql_query_rules.py @@ -0,0 +1,659 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: proxysql_query_rules +version_added: "2.3" +author: "Ben Mildren (@bmildren)" +short_description: Modifies query rules using the proxysql admin interface. +description: + - The M(proxysql_query_rules) module modifies query rules using the proxysql + admin interface. +options: + rule_id: + description: + - The unique id of the rule. Rules are processed in rule_id order. + [integer] + active: + description: + - A rule with I(active) set to C(False) will be tracked in the database, + but will be never loaded in the in-memory data structures [boolean] + username: + description: + - Filtering criteria matching username. If I(username) is non-NULL, a + query will match only if the connection is made with the correct + username. [string] + schemaname: + description: + - Filtering criteria matching schemaname. If I(schemaname) is + non-NULL, a query will match only if the connection uses schemaname as + its default schema. [string] + flagIN: + description: + - Used in combination with I(flagOUT) and I(apply) to create chains of + rules. [integer] + client_addr: + description: + - Match traffic from a specific source. [string] + proxy_addr: + description: + - Match incoming traffic on a specific local IP. [string] + proxy_port: + description: + - Match incoming traffic on a specific local port. [integer] + digest: + description: + - Match queries with a specific digest, as returned by + stats_mysql_query_digest.digest. [string] + match_digest: + description: + - Regular expression that matches the query digest. The dialect of + regular expressions used is that of re2 - + https://github.com/google/re2. [string] + match_pattern: + description: + - Regular expression that matches the query text. The dialect of regular + expressions used is that of re2 - https://github.com/google/re2. + [string] + negate_match_pattern: + description: + - If I(negate_match_pattern) is set to C(True), only queries not matching + the query text will be considered as a match. This acts as a NOT + operator in front of the regular expression matching against + match_pattern. [boolean] + flagOUT: + description: + - Used in combination with I(flagIN) and apply to create chains of rules. + When set, I(flagOUT) signifies the I(flagIN) to be used in the next + chain of rules. [integer] + replace_pattern: + description: + - This is the pattern with which to replace the matched pattern. Note + that this is optional, and when ommitted, the query processor will only + cache, route, or set other parameters without rewriting. [string] + destination_hostgroup: + description: + - Route matched queries to this hostgroup. This happens unless there is + a started transaction and the logged in user has + I(transaction_persistent) set to C(True) (see M(proxysql_mysql_users)). + [integer] + cache_ttl: + description: + - The number of milliseconds for which to cache the result of the query. + Note in ProxySQL 1.1 I(cache_ttl) was in seconds. [integer] + timeout: + description: + - The maximum timeout in milliseconds with which the matched or rewritten + query should be executed. If a query run for longer than the specific + threshold, the query is automatically killed. If timeout is not + specified, the global variable mysql-default_query_timeout applies. + [integer] + retries: + description: + - The maximum number of times a query needs to be re-executed in case of + detected failure during the execution of the query. If retries is not + specified, the global variable mysql-query_retries_on_failure applies. + [integer] + delay: + description: + - Number of milliseconds to delay the execution of the query. This is + essentially a throttling mechanism and QoS, and allows a way to give + priority to queries over others. This value is added to the + mysql-default_query_delay global variable that applies to all queries. + [integer] + mirror_flagOUT: + description: + - Enables query mirroring. If set I(mirror_flagOUT) can be used to + evaluates the mirrored query against the specified chain of rules. + [integer] + mirror_hostgroup: + description: + - Enables query mirroring. If set I(mirror_hostgroup) can be used to + mirror queries to the same or different hostgroup. [integer] + error_msg: + description: + - Query will be blocked, and the specified error_msg will be returned to + the client. [string] + log: + description: + - Query will be logged. [boolean] + apply: + description: + - Used in combination with I(flagIN) and I(flagOUT) to create chains of + rules. Setting apply to True signifies the last rule to be applied. + [boolean] + comment: + description: + - Free form text field, usable for a descriptive comment of the query + rule. [string] + state: + description: + - When C(present) - adds the rule, when C(absent) - removes the rule. + choices: [ "present", "absent" ] + default: present + force_delete: + description: + - By default we avoid deleting more than one schedule in a single batch, + however if you need this behaviour and you're not concerned about the + schedules deleted, you can set I(force_delete) to C(True). + default: False + save_to_disk: + description: + - Save mysql host config to sqlite db on disk to persist the + configuration. + default: True + load_to_runtime: + description: + - Dynamically load mysql host config to runtime memory. + default: True + login_user: + description: + - The username used to authenticate to ProxySQL admin interface + default: None + login_password: + description: + - The password used to authenticate to ProxySQL admin interface + default: None + login_host: + description: + - The host used to connect to ProxySQL admin interface + default: '127.0.0.1' + login_port: + description: + - The port used to connect to ProxySQL admin interface + default: 6032 + config_file: + description: + - Specify a config file from which login_user and login_password are to + be read + default: '' +''' + +EXAMPLES = ''' +--- +# This example adds a rule to redirect queries from a specific user to another +# hostgroup, it saves the mysql query rule config to disk, but avoids loading +# the mysql query config config to runtime (this might be because several +# rules are being added and the user wants to push the config to runtime in a +# single batch using the M(proxysql_manage_config) module). It uses supplied +# credentials to connect to the proxysql admin interface. + +- proxysql_backend_servers: + login_user: admin + login_password: admin + username: 'guest_ro' + destination_hostgroup: 1 + active: 1 + retries: 3 + state: present + load_to_runtime: False + +# This example removes all rules that use the username 'guest_ro', saves the +# mysql query rule config to disk, and dynamically loads the mysql query rule +# config to runtime. It uses credentials in a supplied config file to connect +# to the proxysql admin interface. + +- proxysql_backend_servers: + config_file: '~/proxysql.cnf' + username: 'guest_ro' + state: absent + force_delete: true +''' + +RETURN = ''' +stdout: + description: The mysql user modified or removed from proxysql + returned: On create/update will return the newly modified rule, in all + other cases will return a list of rules that match the supplied + criteria. + type: dict + "sample": { + "changed": true, + "msg": "Added rule to mysql_query_rules", + "rules": [ + { + "active": "0", + "apply": "0", + "cache_ttl": null, + "client_addr": null, + "comment": null, + "delay": null, + "destination_hostgroup": 1, + "digest": null, + "error_msg": null, + "flagIN": "0", + "flagOUT": null, + "log": null, + "match_digest": null, + "match_pattern": null, + "mirror_flagOUT": null, + "mirror_hostgroup": null, + "negate_match_pattern": "0", + "proxy_addr": null, + "proxy_port": null, + "reconnect": null, + "replace_pattern": null, + "retries": null, + "rule_id": "1", + "schemaname": null, + "timeout": null, + "username": "guest_ro" + } + ], + "state": "present" + } +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.mysql import mysql_connect +from ansible.module_utils.pycompat24 import get_exception + +try: + import MySQLdb + import MySQLdb.cursors +except ImportError: + MYSQLDB_FOUND = False +else: + MYSQLDB_FOUND = True + +# =========================================== +# proxysql module specific support methods. +# + + +def perform_checks(module): + if module.params["login_port"] < 0 \ + or module.params["login_port"] > 65535: + module.fail_json( + msg="login_port must be a valid unix port number (0-65535)" + ) + + if not MYSQLDB_FOUND: + module.fail_json( + msg="the python mysqldb module is required" + ) + + +def save_config_to_disk(cursor): + cursor.execute("SAVE MYSQL QUERY RULES TO DISK") + return True + + +def load_config_to_runtime(cursor): + cursor.execute("LOAD MYSQL QUERY RULES TO RUNTIME") + return True + + +class ProxyQueryRule(object): + + def __init__(self, module): + self.state = module.params["state"] + self.force_delete = module.params["force_delete"] + self.save_to_disk = module.params["save_to_disk"] + self.load_to_runtime = module.params["load_to_runtime"] + + config_data_keys = ["rule_id", + "active", + "username", + "schemaname", + "flagIN", + "client_addr", + "proxy_addr", + "proxy_port", + "digest", + "match_digest", + "match_pattern", + "negate_match_pattern", + "flagOUT", + "replace_pattern", + "destination_hostgroup", + "cache_ttl", + "timeout", + "retries", + "delay", + "mirror_flagOUT", + "mirror_hostgroup", + "error_msg", + "log", + "apply", + "comment"] + + self.config_data = dict((k, module.params[k]) + for k in config_data_keys) + + def check_rule_pk_exists(self, cursor): + query_string = \ + """SELECT count(*) AS `rule_count` + FROM mysql_query_rules + WHERE rule_id = %s""" + + query_data = \ + [self.config_data["rule_id"]] + + cursor.execute(query_string, query_data) + check_count = cursor.fetchone() + return (int(check_count['rule_count']) > 0) + + def check_rule_cfg_exists(self, cursor): + query_string = \ + """SELECT count(*) AS `rule_count` + FROM mysql_query_rules""" + + cols = 0 + query_data = [] + + for col, val in self.config_data.iteritems(): + if val is not None: + cols += 1 + query_data.append(val) + if cols == 1: + query_string += "\n WHERE " + col + " = %s" + else: + query_string += "\n AND " + col + " = %s" + + if cols > 0: + cursor.execute(query_string, query_data) + else: + cursor.execute(query_string) + check_count = cursor.fetchone() + return int(check_count['rule_count']) + + def get_rule_config(self, cursor, created_rule_id=None): + query_string = \ + """SELECT * + FROM mysql_query_rules""" + + if created_rule_id: + query_data = [created_rule_id, ] + query_string += "\nWHERE rule_id = %s" + + cursor.execute(query_string, query_data) + rule = cursor.fetchone() + else: + cols = 0 + query_data = [] + + for col, val in self.config_data.iteritems(): + if val is not None: + cols += 1 + query_data.append(val) + if cols == 1: + query_string += "\n WHERE " + col + " = %s" + else: + query_string += "\n AND " + col + " = %s" + + if cols > 0: + cursor.execute(query_string, query_data) + else: + cursor.execute(query_string) + rule = cursor.fetchall() + + return rule + + def create_rule_config(self, cursor): + query_string = \ + """INSERT INTO mysql_query_rules (""" + + cols = 0 + query_data = [] + + for col, val in self.config_data.iteritems(): + if val is not None: + cols += 1 + query_data.append(val) + query_string += "\n" + col + "," + + query_string = query_string[:-1] + + query_string += \ + (")\n" + + "VALUES (" + + "%s ," * cols) + + query_string = query_string[:-2] + query_string += ")" + + cursor.execute(query_string, query_data) + new_rule_id = cursor.lastrowid + return True, new_rule_id + + def update_rule_config(self, cursor): + query_string = """UPDATE mysql_query_rules""" + + cols = 0 + query_data = [] + + for col, val in self.config_data.iteritems(): + if val is not None and col != "rule_id": + cols += 1 + query_data.append(val) + if cols == 1: + query_string += "\nSET " + col + "= %s," + else: + query_string += "\n " + col + " = %s," + + query_string = query_string[:-1] + query_string += "\nWHERE rule_id = %s" + + query_data.append(self.config_data["rule_id"]) + + cursor.execute(query_string, query_data) + return True + + def delete_rule_config(self, cursor): + query_string = \ + """DELETE FROM mysql_query_rules""" + + cols = 0 + query_data = [] + + for col, val in self.config_data.iteritems(): + if val is not None: + cols += 1 + query_data.append(val) + if cols == 1: + query_string += "\n WHERE " + col + " = %s" + else: + query_string += "\n AND " + col + " = %s" + + if cols > 0: + cursor.execute(query_string, query_data) + else: + cursor.execute(query_string) + check_count = cursor.rowcount + return True, int(check_count) + + def manage_config(self, cursor, state): + if state: + if self.save_to_disk: + save_config_to_disk(cursor) + if self.load_to_runtime: + load_config_to_runtime(cursor) + + def create_rule(self, check_mode, result, cursor): + if not check_mode: + result['changed'], new_rule_id = \ + self.create_rule_config(cursor) + result['msg'] = "Added rule to mysql_query_rules" + self.manage_config(cursor, + result['changed']) + result['rules'] = \ + self.get_rule_config(cursor, new_rule_id) + else: + result['changed'] = True + result['msg'] = ("Rule would have been added to" + + " mysql_query_rules, however" + + " check_mode is enabled.") + + def update_rule(self, check_mode, result, cursor): + if not check_mode: + result['changed'] = \ + self.update_rule_config(cursor) + result['msg'] = "Updated rule in mysql_query_rules" + self.manage_config(cursor, + result['changed']) + result['rules'] = \ + self.get_rule_config(cursor) + else: + result['changed'] = True + result['msg'] = ("Rule would have been updated in" + + " mysql_query_rules, however" + + " check_mode is enabled.") + + def delete_rule(self, check_mode, result, cursor): + if not check_mode: + result['rules'] = \ + self.get_rule_config(cursor) + result['changed'], result['rows_affected'] = \ + self.delete_rule_config(cursor) + result['msg'] = "Deleted rule from mysql_query_rules" + self.manage_config(cursor, + result['changed']) + else: + result['changed'] = True + result['msg'] = ("Rule would have been deleted from" + + " mysql_query_rules, however" + + " check_mode is enabled.") + +# =========================================== +# Module execution. +# + + +def main(): + module = AnsibleModule( + argument_spec=dict( + login_user=dict(default=None, type='str'), + login_password=dict(default=None, no_log=True, type='str'), + login_host=dict(default="127.0.0.1"), + login_unix_socket=dict(default=None), + login_port=dict(default=6032, type='int'), + config_file=dict(default="", type='path'), + rule_id=dict(type='int'), + active=dict(type='bool'), + username=dict(type='str'), + schemaname=dict(type='str'), + flagIN=dict(type='int'), + client_addr=dict(type='str'), + proxy_addr=dict(type='str'), + proxy_port=dict(type='int'), + digest=dict(type='str'), + match_digest=dict(type='str'), + match_pattern=dict(type='str'), + negate_match_pattern=dict(type='bool'), + flagOUT=dict(type='int'), + replace_pattern=dict(type='str'), + destination_hostgroup=dict(type='int'), + cache_ttl=dict(type='int'), + timeout=dict(type='int'), + retries=dict(type='int'), + delay=dict(type='int'), + mirror_flagOUT=dict(type='int'), + mirror_hostgroup=dict(type='int'), + error_msg=dict(type='str'), + log=dict(type='bool'), + apply=dict(type='bool'), + comment=dict(type='str'), + state=dict(default='present', choices=['present', + 'absent']), + force_delete=dict(default=False, type='bool'), + save_to_disk=dict(default=True, type='bool'), + load_to_runtime=dict(default=True, type='bool') + ), + supports_check_mode=True + ) + + perform_checks(module) + + login_user = module.params["login_user"] + login_password = module.params["login_password"] + config_file = module.params["config_file"] + + cursor = None + try: + cursor = mysql_connect(module, + login_user, + login_password, + config_file, + cursor_class=MySQLdb.cursors.DictCursor) + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to connect to ProxySQL Admin Module.. %s" % e + ) + + proxysql_query_rule = ProxyQueryRule(module) + result = {} + + result['state'] = proxysql_query_rule.state + + if proxysql_query_rule.state == "present": + try: + if not proxysql_query_rule.check_rule_cfg_exists(cursor): + if proxysql_query_rule.config_data["rule_id"] and \ + proxysql_query_rule.check_rule_pk_exists(cursor): + proxysql_query_rule.update_rule(module.check_mode, + result, + cursor) + else: + proxysql_query_rule.create_rule(module.check_mode, + result, + cursor) + else: + result['changed'] = False + result['msg'] = ("The rule already exists in" + + " mysql_query_rules and doesn't need to be" + + " updated.") + result['rules'] = \ + proxysql_query_rule.get_rule_config(cursor) + + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to modify rule.. %s" % e + ) + + elif proxysql_query_rule.state == "absent": + try: + existing_rules = proxysql_query_rule.check_rule_cfg_exists(cursor) + if existing_rules > 0: + if existing_rules == 1 or \ + proxysql_query_rule.force_delete: + proxysql_query_rule.delete_rule(module.check_mode, + result, + cursor) + else: + module.fail_json( + msg=("Operation would delete multiple rules" + + " use force_delete to override this") + ) + else: + result['changed'] = False + result['msg'] = ("The rule is already absent from the" + + " mysql_query_rules memory configuration") + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to remove rule.. %s" % e + ) + + module.exit_json(**result) + +if __name__ == '__main__': + main() diff --git a/database/proxysql/proxysql_replication_hostgroups.py b/database/proxysql/proxysql_replication_hostgroups.py new file mode 100644 index 00000000000..41ade82734c --- /dev/null +++ b/database/proxysql/proxysql_replication_hostgroups.py @@ -0,0 +1,424 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: proxysql_replication_hostgroups +version_added: "2.3" +author: "Ben Mildren (@bmildren)" +short_description: Manages replication hostgroups using the proxysql admin + interface. +description: + - Each row in mysql_replication_hostgroups represent a pair of + writer_hostgroup and reader_hostgroup . + ProxySQL will monitor the value of read_only for all the servers in + specified hostgroups, and based on the value of read_only will assign the + server to the writer or reader hostgroups. +options: + writer_hostgroup: + description: + - Id of the writer hostgroup. + required: True + reader_hostgroup: + description: + - Id of the reader hostgroup. + required: True + comment: + description: + - Text field that can be used for any purposed defined by the user. + state: + description: + - When C(present) - adds the replication hostgroup, when C(absent) - + removes the replication hostgroup. + choices: [ "present", "absent" ] + default: present + save_to_disk: + description: + - Save mysql host config to sqlite db on disk to persist the + configuration. + default: True + load_to_runtime: + description: + - Dynamically load mysql host config to runtime memory. + default: True + login_user: + description: + - The username used to authenticate to ProxySQL admin interface + default: None + login_password: + description: + - The password used to authenticate to ProxySQL admin interface + default: None + login_host: + description: + - The host used to connect to ProxySQL admin interface + default: '127.0.0.1' + login_port: + description: + - The port used to connect to ProxySQL admin interface + default: 6032 + config_file: + description: + - Specify a config file from which login_user and login_password are to + be read + default: '' +''' + +EXAMPLES = ''' +--- +# This example adds a replication hostgroup, it saves the mysql server config +# to disk, but avoids loading the mysql server config to runtime (this might be +# because several replication hostgroup are being added and the user wants to +# push the config to runtime in a single batch using the +# M(proxysql_manage_config) module). It uses supplied credentials to connect +# to the proxysql admin interface. + +- proxysql_replication_hostgroups: + login_user: 'admin' + login_password: 'admin' + writer_hostgroup: 1 + reader_hostgroup: 2 + state: present + load_to_runtime: False + +# This example removes a replication hostgroup, saves the mysql server config +# to disk, and dynamically loads the mysql server config to runtime. It uses +# credentials in a supplied config file to connect to the proxysql admin +# interface. + +- proxysql_replication_hostgroups: + config_file: '~/proxysql.cnf' + writer_hostgroup: 3 + reader_hostgroup: 4 + state: absent +''' + +RETURN = ''' +stdout: + description: The replication hostgroup modified or removed from proxysql + returned: On create/update will return the newly modified group, on delete + it will return the deleted record. + type: dict + "sample": { + "changed": true, + "msg": "Added server to mysql_hosts", + "repl_group": { + "comment": "", + "reader_hostgroup": "1", + "writer_hostgroup": "2" + }, + "state": "present" + } +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.mysql import mysql_connect +from ansible.module_utils.pycompat24 import get_exception + +try: + import MySQLdb + import MySQLdb.cursors +except ImportError: + MYSQLDB_FOUND = False +else: + MYSQLDB_FOUND = True + +# =========================================== +# proxysql module specific support methods. +# + + +def perform_checks(module): + if module.params["login_port"] < 0 \ + or module.params["login_port"] > 65535: + module.fail_json( + msg="login_port must be a valid unix port number (0-65535)" + ) + + if not module.params["writer_hostgroup"] >= 0: + module.fail_json( + msg="writer_hostgroup must be a integer greater than or equal to 0" + ) + + if not module.params["reader_hostgroup"] == \ + module.params["writer_hostgroup"]: + if not module.params["reader_hostgroup"] > 0: + module.fail_json( + msg=("writer_hostgroup must be a integer greater than" + + " or equal to 0") + ) + else: + module.fail_json( + msg="reader_hostgroup cannot equal writer_hostgroup" + ) + + if not MYSQLDB_FOUND: + module.fail_json( + msg="the python mysqldb module is required" + ) + + +def save_config_to_disk(cursor): + cursor.execute("SAVE MYSQL SERVERS TO DISK") + return True + + +def load_config_to_runtime(cursor): + cursor.execute("LOAD MYSQL SERVERS TO RUNTIME") + return True + + +class ProxySQLReplicationHostgroup(object): + + def __init__(self, module): + self.state = module.params["state"] + self.save_to_disk = module.params["save_to_disk"] + self.load_to_runtime = module.params["load_to_runtime"] + self.writer_hostgroup = module.params["writer_hostgroup"] + self.reader_hostgroup = module.params["reader_hostgroup"] + self.comment = module.params["comment"] + + def check_repl_group_config(self, cursor, keys): + query_string = \ + """SELECT count(*) AS `repl_groups` + FROM mysql_replication_hostgroups + WHERE writer_hostgroup = %s + AND reader_hostgroup = %s""" + + query_data = \ + [self.writer_hostgroup, + self.reader_hostgroup] + + if self.comment and not keys: + query_string += "\n AND comment = %s" + query_data.append(self.comment) + + cursor.execute(query_string, query_data) + check_count = cursor.fetchone() + return (int(check_count['repl_groups']) > 0) + + def get_repl_group_config(self, cursor): + query_string = \ + """SELECT * + FROM mysql_replication_hostgroups + WHERE writer_hostgroup = %s + AND reader_hostgroup = %s""" + + query_data = \ + [self.writer_hostgroup, + self.reader_hostgroup] + + cursor.execute(query_string, query_data) + repl_group = cursor.fetchone() + return repl_group + + def create_repl_group_config(self, cursor): + query_string = \ + """INSERT INTO mysql_replication_hostgroups ( + writer_hostgroup, + reader_hostgroup, + comment) + VALUES (%s, %s, %s)""" + + query_data = \ + [self.writer_hostgroup, + self.reader_hostgroup, + self.comment or ''] + + cursor.execute(query_string, query_data) + return True + + def update_repl_group_config(self, cursor): + query_string = \ + """UPDATE mysql_replication_hostgroups + SET comment = %s + WHERE writer_hostgroup = %s + AND reader_hostgroup = %s""" + + query_data = \ + [self.comment, + self.writer_hostgroup, + self.reader_hostgroup] + + cursor.execute(query_string, query_data) + return True + + def delete_repl_group_config(self, cursor): + query_string = \ + """DELETE FROM mysql_replication_hostgroups + WHERE writer_hostgroup = %s + AND reader_hostgroup = %s""" + + query_data = \ + [self.writer_hostgroup, + self.reader_hostgroup] + + cursor.execute(query_string, query_data) + return True + + def manage_config(self, cursor, state): + if state: + if self.save_to_disk: + save_config_to_disk(cursor) + if self.load_to_runtime: + load_config_to_runtime(cursor) + + def create_repl_group(self, check_mode, result, cursor): + if not check_mode: + result['changed'] = \ + self.create_repl_group_config(cursor) + result['msg'] = "Added server to mysql_hosts" + result['repl_group'] = \ + self.get_repl_group_config(cursor) + self.manage_config(cursor, + result['changed']) + else: + result['changed'] = True + result['msg'] = ("Repl group would have been added to" + + " mysql_replication_hostgroups, however" + + " check_mode is enabled.") + + def update_repl_group(self, check_mode, result, cursor): + if not check_mode: + result['changed'] = \ + self.update_repl_group_config(cursor) + result['msg'] = "Updated server in mysql_hosts" + result['repl_group'] = \ + self.get_repl_group_config(cursor) + self.manage_config(cursor, + result['changed']) + else: + result['changed'] = True + result['msg'] = ("Repl group would have been updated in" + + " mysql_replication_hostgroups, however" + + " check_mode is enabled.") + + def delete_repl_group(self, check_mode, result, cursor): + if not check_mode: + result['repl_group'] = \ + self.get_repl_group_config(cursor) + result['changed'] = \ + self.delete_repl_group_config(cursor) + result['msg'] = "Deleted server from mysql_hosts" + self.manage_config(cursor, + result['changed']) + else: + result['changed'] = True + result['msg'] = ("Repl group would have been deleted from" + + " mysql_replication_hostgroups, however" + + " check_mode is enabled.") + +# =========================================== +# Module execution. +# + + +def main(): + module = AnsibleModule( + argument_spec=dict( + login_user=dict(default=None, type='str'), + login_password=dict(default=None, no_log=True, type='str'), + login_host=dict(default="127.0.0.1"), + login_unix_socket=dict(default=None), + login_port=dict(default=6032, type='int'), + config_file=dict(default="", type='path'), + writer_hostgroup=dict(required=True, type='int'), + reader_hostgroup=dict(required=True, type='int'), + comment=dict(type='str'), + state=dict(default='present', choices=['present', + 'absent']), + save_to_disk=dict(default=True, type='bool'), + load_to_runtime=dict(default=True, type='bool') + ), + supports_check_mode=True + ) + + perform_checks(module) + + login_user = module.params["login_user"] + login_password = module.params["login_password"] + config_file = module.params["config_file"] + + cursor = None + try: + cursor = mysql_connect(module, + login_user, + login_password, + config_file, + cursor_class=MySQLdb.cursors.DictCursor) + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to connect to ProxySQL Admin Module.. %s" % e + ) + + proxysql_repl_group = ProxySQLReplicationHostgroup(module) + result = {} + + result['state'] = proxysql_repl_group.state + + if proxysql_repl_group.state == "present": + try: + if not proxysql_repl_group.check_repl_group_config(cursor, + keys=True): + proxysql_repl_group.create_repl_group(module.check_mode, + result, + cursor) + else: + if not proxysql_repl_group.check_repl_group_config(cursor, + keys=False): + proxysql_repl_group.update_repl_group(module.check_mode, + result, + cursor) + else: + result['changed'] = False + result['msg'] = ("The repl group already exists in" + + " mysql_replication_hostgroups and" + + " doesn't need to be updated.") + result['repl_group'] = \ + proxysql_repl_group.get_repl_group_config(cursor) + + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to modify replication hostgroup.. %s" % e + ) + + elif proxysql_repl_group.state == "absent": + try: + if proxysql_repl_group.check_repl_group_config(cursor, + keys=True): + proxysql_repl_group.delete_repl_group(module.check_mode, + result, + cursor) + else: + result['changed'] = False + result['msg'] = ("The repl group is already absent from the" + + " mysql_replication_hostgroups memory" + + " configuration") + + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to delete replication hostgroup.. %s" % e + ) + + module.exit_json(**result) + +if __name__ == '__main__': + main() diff --git a/database/proxysql/proxysql_scheduler.py b/database/proxysql/proxysql_scheduler.py new file mode 100644 index 00000000000..790dfcaee6e --- /dev/null +++ b/database/proxysql/proxysql_scheduler.py @@ -0,0 +1,458 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: proxysql_scheduler +version_added: "2.3" +author: "Ben Mildren (@bmildren)" +short_description: Adds or removes schedules from proxysql admin interface. +description: + - The M(proxysql_scheduler) module adds or removes schedules using the + proxysql admin interface. +options: + active: + description: + - A schedule with I(active) set to C(False) will be tracked in the + database, but will be never loaded in the in-memory data structures + default: True + interval_ms: + description: + - How often (in millisecond) the job will be started. The minimum value + for I(interval_ms) is 100 milliseconds + default: 10000 + filename: + description: + - Full path of the executable to be executed. + required: True + arg1: + description: + - Argument that can be passed to the job. + arg2: + description: + - Argument that can be passed to the job. + arg3: + description: + - Argument that can be passed to the job. + arg4: + description: + - Argument that can be passed to the job. + arg5: + description: + - Argument that can be passed to the job. + comment: + description: + - Text field that can be used for any purposed defined by the user. + state: + description: + - When C(present) - adds the schedule, when C(absent) - removes the + schedule. + choices: [ "present", "absent" ] + default: present + force_delete: + description: + - By default we avoid deleting more than one schedule in a single batch, + however if you need this behaviour and you're not concerned about the + schedules deleted, you can set I(force_delete) to C(True). + default: False + save_to_disk: + description: + - Save mysql host config to sqlite db on disk to persist the + configuration. + default: True + load_to_runtime: + description: + - Dynamically load mysql host config to runtime memory. + default: True + login_user: + description: + - The username used to authenticate to ProxySQL admin interface + default: None + login_password: + description: + - The password used to authenticate to ProxySQL admin interface + default: None + login_host: + description: + - The host used to connect to ProxySQL admin interface + default: '127.0.0.1' + login_port: + description: + - The port used to connect to ProxySQL admin interface + default: 6032 + config_file: + description: + - Specify a config file from which login_user and login_password are to + be read + default: '' +''' + +EXAMPLES = ''' +--- +# This example adds a schedule, it saves the scheduler config to disk, but +# avoids loading the scheduler config to runtime (this might be because +# several servers are being added and the user wants to push the config to +# runtime in a single batch using the M(proxysql_manage_config) module). It +# uses supplied credentials to connect to the proxysql admin interface. + +- proxysql_scheduler: + login_user: 'admin' + login_password: 'admin' + interval_ms: 1000 + filename: "/opt/maintenance.py" + state: present + load_to_runtime: False + +# This example removes a schedule, saves the scheduler config to disk, and +# dynamically loads the scheduler config to runtime. It uses credentials +# in a supplied config file to connect to the proxysql admin interface. + +- proxysql_scheduler: + config_file: '~/proxysql.cnf' + filename: "/opt/old_script.py" + state: absent +''' + +RETURN = ''' +stdout: + description: The schedule modified or removed from proxysql + returned: On create/update will return the newly modified schedule, on + delete it will return the deleted record. + type: dict + "sample": { + "changed": true, + "filename": "/opt/test.py", + "msg": "Added schedule to scheduler", + "schedules": [ + { + "active": "1", + "arg1": null, + "arg2": null, + "arg3": null, + "arg4": null, + "arg5": null, + "comment": "", + "filename": "/opt/test.py", + "id": "1", + "interval_ms": "10000" + } + ], + "state": "present" + } +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.mysql import mysql_connect +from ansible.module_utils.pycompat24 import get_exception + +try: + import MySQLdb + import MySQLdb.cursors +except ImportError: + MYSQLDB_FOUND = False +else: + MYSQLDB_FOUND = True + +# =========================================== +# proxysql module specific support methods. +# + + +def perform_checks(module): + if module.params["login_port"] < 0 \ + or module.params["login_port"] > 65535: + module.fail_json( + msg="login_port must be a valid unix port number (0-65535)" + ) + + if module.params["interval_ms"] < 100 \ + or module.params["interval_ms"] > 100000000: + module.fail_json( + msg="interval_ms must between 100ms & 100000000ms" + ) + + if not MYSQLDB_FOUND: + module.fail_json( + msg="the python mysqldb module is required" + ) + + +def save_config_to_disk(cursor): + cursor.execute("SAVE SCHEDULER TO DISK") + return True + + +def load_config_to_runtime(cursor): + cursor.execute("LOAD SCHEDULER TO RUNTIME") + return True + + +class ProxySQLSchedule(object): + + def __init__(self, module): + self.state = module.params["state"] + self.force_delete = module.params["force_delete"] + self.save_to_disk = module.params["save_to_disk"] + self.load_to_runtime = module.params["load_to_runtime"] + self.active = module.params["active"] + self.interval_ms = module.params["interval_ms"] + self.filename = module.params["filename"] + + config_data_keys = ["arg1", + "arg2", + "arg3", + "arg4", + "arg5", + "comment"] + + self.config_data = dict((k, module.params[k]) + for k in config_data_keys) + + def check_schedule_config(self, cursor): + query_string = \ + """SELECT count(*) AS `schedule_count` + FROM scheduler + WHERE active = %s + AND interval_ms = %s + AND filename = %s""" + + query_data = \ + [self.active, + self.interval_ms, + self.filename] + + for col, val in self.config_data.iteritems(): + if val is not None: + query_data.append(val) + query_string += "\n AND " + col + " = %s" + + cursor.execute(query_string, query_data) + check_count = cursor.fetchone() + return int(check_count['schedule_count']) + + def get_schedule_config(self, cursor): + query_string = \ + """SELECT * + FROM scheduler + WHERE active = %s + AND interval_ms = %s + AND filename = %s""" + + query_data = \ + [self.active, + self.interval_ms, + self.filename] + + for col, val in self.config_data.iteritems(): + if val is not None: + query_data.append(val) + query_string += "\n AND " + col + " = %s" + + cursor.execute(query_string, query_data) + schedule = cursor.fetchall() + return schedule + + def create_schedule_config(self, cursor): + query_string = \ + """INSERT INTO scheduler ( + active, + interval_ms, + filename""" + + cols = 0 + query_data = \ + [self.active, + self.interval_ms, + self.filename] + + for col, val in self.config_data.iteritems(): + if val is not None: + cols += 1 + query_data.append(val) + query_string += ",\n" + col + + query_string += \ + (")\n" + + "VALUES (%s, %s, %s" + + ", %s" * cols + + ")") + + cursor.execute(query_string, query_data) + return True + + def delete_schedule_config(self, cursor): + query_string = \ + """DELETE FROM scheduler + WHERE active = %s + AND interval_ms = %s + AND filename = %s""" + + query_data = \ + [self.active, + self.interval_ms, + self.filename] + + for col, val in self.config_data.iteritems(): + if val is not None: + query_data.append(val) + query_string += "\n AND " + col + " = %s" + + cursor.execute(query_string, query_data) + check_count = cursor.rowcount + return True, int(check_count) + + def manage_config(self, cursor, state): + if state: + if self.save_to_disk: + save_config_to_disk(cursor) + if self.load_to_runtime: + load_config_to_runtime(cursor) + + def create_schedule(self, check_mode, result, cursor): + if not check_mode: + result['changed'] = \ + self.create_schedule_config(cursor) + result['msg'] = "Added schedule to scheduler" + result['schedules'] = \ + self.get_schedule_config(cursor) + self.manage_config(cursor, + result['changed']) + else: + result['changed'] = True + result['msg'] = ("Schedule would have been added to" + + " scheduler, however check_mode" + + " is enabled.") + + def delete_schedule(self, check_mode, result, cursor): + if not check_mode: + result['schedules'] = \ + self.get_schedule_config(cursor) + result['changed'] = \ + self.delete_schedule_config(cursor) + result['msg'] = "Deleted schedule from scheduler" + self.manage_config(cursor, + result['changed']) + else: + result['changed'] = True + result['msg'] = ("Schedule would have been deleted from" + + " scheduler, however check_mode is" + + " enabled.") + +# =========================================== +# Module execution. +# + + +def main(): + module = AnsibleModule( + argument_spec=dict( + login_user=dict(default=None, type='str'), + login_password=dict(default=None, no_log=True, type='str'), + login_host=dict(default="127.0.0.1"), + login_unix_socket=dict(default=None), + login_port=dict(default=6032, type='int'), + config_file=dict(default="", type='path'), + active=dict(default=True, type='bool'), + interval_ms=dict(default=10000, type='int'), + filename=dict(required=True, type='str'), + arg1=dict(type='str'), + arg2=dict(type='str'), + arg3=dict(type='str'), + arg4=dict(type='str'), + arg5=dict(type='str'), + comment=dict(type='str'), + state=dict(default='present', choices=['present', + 'absent']), + force_delete=dict(default=False, type='bool'), + save_to_disk=dict(default=True, type='bool'), + load_to_runtime=dict(default=True, type='bool') + ), + supports_check_mode=True + ) + + perform_checks(module) + + login_user = module.params["login_user"] + login_password = module.params["login_password"] + config_file = module.params["config_file"] + + cursor = None + try: + cursor = mysql_connect(module, + login_user, + login_password, + config_file, + cursor_class=MySQLdb.cursors.DictCursor) + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to connect to ProxySQL Admin Module.. %s" % e + ) + + proxysql_schedule = ProxySQLSchedule(module) + result = {} + + result['state'] = proxysql_schedule.state + result['filename'] = proxysql_schedule.filename + + if proxysql_schedule.state == "present": + try: + if not proxysql_schedule.check_schedule_config(cursor) > 0: + proxysql_schedule.create_schedule(module.check_mode, + result, + cursor) + else: + result['changed'] = False + result['msg'] = ("The schedule already exists and doesn't" + + " need to be updated.") + result['schedules'] = \ + proxysql_schedule.get_schedule_config(cursor) + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to modify schedule.. %s" % e + ) + + elif proxysql_schedule.state == "absent": + try: + existing_schedules = \ + proxysql_schedule.check_schedule_config(cursor) + if existing_schedules > 0: + if existing_schedules == 1 or proxysql_schedule.force_delete: + proxysql_schedule.delete_schedule(module.check_mode, + result, + cursor) + else: + module.fail_json( + msg=("Operation would delete multiple records" + + " use force_delete to override this") + ) + else: + result['changed'] = False + result['msg'] = ("The schedule is already absent from the" + + " memory configuration") + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to remove schedule.. %s" % e + ) + + module.exit_json(**result) + +if __name__ == '__main__': + main() From fcea2821851b535d39056b480882f0510423975b Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 2 Nov 2016 12:53:46 -0700 Subject: [PATCH 2302/2522] Archive is being added in 2.3 rather than 2.2 --- files/archive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/archive.py b/files/archive.py index 3b11d86b339..36d2c4231b2 100644 --- a/files/archive.py +++ b/files/archive.py @@ -28,7 +28,7 @@ DOCUMENTATION = ''' --- module: archive -version_added: 2.2 +version_added: 2.3 short_description: Creates a compressed archive of one or more files or trees. extends_documentation_fragment: files description: From 4e97ff49875dd4b87ec17e6e5b0db788403d18c7 Mon Sep 17 00:00:00 2001 From: cameronurnes Date: Wed, 2 Nov 2016 16:37:56 -0700 Subject: [PATCH 2303/2522] Fix elf and elf_facts documentation formatting (#3348) * This method breaks the output on the documentation site * Conflicting quotes * Conflicting quotes --- cloud/amazon/efs.py | 21 +++++++++------------ cloud/amazon/efs_facts.py | 8 +++----- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/cloud/amazon/efs.py b/cloud/amazon/efs.py index 388a3e8dd89..565d6ba5129 100644 --- a/cloud/amazon/efs.py +++ b/cloud/amazon/efs.py @@ -50,26 +50,23 @@ choices: ['general_purpose', 'max_io'] tags: description: - - | - List of tags of Amazon EFS. Should be defined as dictionary - In case of 'present' state with list of tags and existing EFS (matched by 'name'), tags of EFS will be replaced with provided data. + - "List of tags of Amazon EFS. Should be defined as dictionary + In case of 'present' state with list of tags and existing EFS (matched by 'name'), tags of EFS will be replaced with provided data." required: false default: None targets: description: - - | - List of mounted targets. It should be a list of dictionaries, every dictionary should include next attributes: - - subnet_id - Mandatory. The ID of the subnet to add the mount target in. - - ip_address - Optional. A valid IPv4 address within the address range of the specified subnet. - - security_groups - Optional. List of security group IDs, of the form "sg-xxxxxxxx". These must be for the same VPC as subnet specified - This data may be modified for existing EFS using state 'present' and new list of mount targets. + - "List of mounted targets. It should be a list of dictionaries, every dictionary should include next attributes: + - subnet_id - Mandatory. The ID of the subnet to add the mount target in. + - ip_address - Optional. A valid IPv4 address within the address range of the specified subnet. + - security_groups - Optional. List of security group IDs, of the form 'sg-xxxxxxxx'. These must be for the same VPC as subnet specified + This data may be modified for existing EFS using state 'present' and new list of mount targets." required: false default: None wait: description: - - | - In case of 'present' state should wait for EFS 'available' life cycle state (of course, if current state not 'deleting' or 'deleted') - In case of 'absent' state should wait for EFS 'deleted' life cycle state + - "In case of 'present' state should wait for EFS 'available' life cycle state (of course, if current state not 'deleting' or 'deleted') + In case of 'absent' state should wait for EFS 'deleted' life cycle state" required: false default: "no" choices: ["yes", "no"] diff --git a/cloud/amazon/efs_facts.py b/cloud/amazon/efs_facts.py index 1720ec5d80a..3b45e068ee2 100644 --- a/cloud/amazon/efs_facts.py +++ b/cloud/amazon/efs_facts.py @@ -37,17 +37,15 @@ default: None tags: description: - - | - List of tags of Amazon EFS. Should be defined as dictionary + - List of tags of Amazon EFS. Should be defined as dictionary required: false default: None targets: description: - - | - List of mounted targets. It should be a list of dictionaries, every dictionary should include next attributes: + - "List of mounted targets. It should be a list of dictionaries, every dictionary should include next attributes: - SubnetId - Mandatory. The ID of the subnet to add the mount target in. - IpAddress - Optional. A valid IPv4 address within the address range of the specified subnet. - - SecurityGroups - Optional. List of security group IDs, of the form "sg-xxxxxxxx". These must be for the same VPC as subnet specified. + - SecurityGroups - Optional. List of security group IDs, of the form 'sg-xxxxxxxx'. These must be for the same VPC as subnet specified." required: false default: None extends_documentation_fragment: From 6e4a182684a54e60be5318525c658cfe315d06ad Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Wed, 2 Nov 2016 20:14:45 -0400 Subject: [PATCH 2304/2522] several fixes to doc to avoid breaking doc build --- database/proxysql/proxysql_query_rules.py | 85 ++++++++--------------- 1 file changed, 29 insertions(+), 56 deletions(-) diff --git a/database/proxysql/proxysql_query_rules.py b/database/proxysql/proxysql_query_rules.py index 8af46c6ab9c..993f1485113 100644 --- a/database/proxysql/proxysql_query_rules.py +++ b/database/proxysql/proxysql_query_rules.py @@ -23,128 +23,105 @@ author: "Ben Mildren (@bmildren)" short_description: Modifies query rules using the proxysql admin interface. description: - - The M(proxysql_query_rules) module modifies query rules using the proxysql - admin interface. + - The M(proxysql_query_rules) module modifies query rules using the proxysql admin interface. options: rule_id: description: - The unique id of the rule. Rules are processed in rule_id order. - [integer] active: description: - - A rule with I(active) set to C(False) will be tracked in the database, - but will be never loaded in the in-memory data structures [boolean] + - A rule with I(active) set to C(False) will be tracked in the database, but will be never loaded in the in-memory data structures username: description: - - Filtering criteria matching username. If I(username) is non-NULL, a - query will match only if the connection is made with the correct - username. [string] + - Filtering criteria matching username. If I(username) is non-NULL, a query will match only if the connection is made with the correct username. schemaname: description: - - Filtering criteria matching schemaname. If I(schemaname) is - non-NULL, a query will match only if the connection uses schemaname as - its default schema. [string] + - Filtering criteria matching schemaname. If I(schemaname) is non-NULL, + a query will match only if the connection uses schemaname as its default schema. flagIN: description: - - Used in combination with I(flagOUT) and I(apply) to create chains of - rules. [integer] + - Used in combination with I(flagOUT) and I(apply) to create chains of rules. client_addr: description: - - Match traffic from a specific source. [string] + - Match traffic from a specific source. proxy_addr: description: - - Match incoming traffic on a specific local IP. [string] + - Match incoming traffic on a specific local IP. proxy_port: description: - - Match incoming traffic on a specific local port. [integer] + - Match incoming traffic on a specific local port. digest: description: - - Match queries with a specific digest, as returned by - stats_mysql_query_digest.digest. [string] + - Match queries with a specific digest, as returned by stats_mysql_query_digest.digest. match_digest: description: - - Regular expression that matches the query digest. The dialect of - regular expressions used is that of re2 - - https://github.com/google/re2. [string] + - "Regular expression that matches the query digest. + The dialect of regular expressions used is that of re2 - https://github.com/google/re2." match_pattern: description: - - Regular expression that matches the query text. The dialect of regular - expressions used is that of re2 - https://github.com/google/re2. - [string] + - "Regular expression that matches the query text. The dialect of regular expressions used is that of re2 - https://github.com/google/re2." negate_match_pattern: description: - If I(negate_match_pattern) is set to C(True), only queries not matching the query text will be considered as a match. This acts as a NOT operator in front of the regular expression matching against - match_pattern. [boolean] + match_pattern. flagOUT: description: - Used in combination with I(flagIN) and apply to create chains of rules. When set, I(flagOUT) signifies the I(flagIN) to be used in the next - chain of rules. [integer] + chain of rules. replace_pattern: description: - This is the pattern with which to replace the matched pattern. Note that this is optional, and when ommitted, the query processor will only - cache, route, or set other parameters without rewriting. [string] + cache, route, or set other parameters without rewriting. destination_hostgroup: description: - - Route matched queries to this hostgroup. This happens unless there is - a started transaction and the logged in user has + - Route matched queries to this hostgroup. This happens unless there is a started transaction and the logged in user has I(transaction_persistent) set to C(True) (see M(proxysql_mysql_users)). - [integer] cache_ttl: description: - The number of milliseconds for which to cache the result of the query. - Note in ProxySQL 1.1 I(cache_ttl) was in seconds. [integer] + Note in ProxySQL 1.1 I(cache_ttl) was in seconds. timeout: description: - The maximum timeout in milliseconds with which the matched or rewritten query should be executed. If a query run for longer than the specific threshold, the query is automatically killed. If timeout is not specified, the global variable mysql-default_query_timeout applies. - [integer] retries: description: - - The maximum number of times a query needs to be re-executed in case of - detected failure during the execution of the query. If retries is not - specified, the global variable mysql-query_retries_on_failure applies. - [integer] + - The maximum number of times a query needs to be re-executed in case of detected failure during the execution of the query. + If retries is not specified, the global variable mysql-query_retries_on_failure applies. delay: description: - Number of milliseconds to delay the execution of the query. This is essentially a throttling mechanism and QoS, and allows a way to give priority to queries over others. This value is added to the mysql-default_query_delay global variable that applies to all queries. - [integer] mirror_flagOUT: description: - Enables query mirroring. If set I(mirror_flagOUT) can be used to evaluates the mirrored query against the specified chain of rules. - [integer] mirror_hostgroup: description: - - Enables query mirroring. If set I(mirror_hostgroup) can be used to - mirror queries to the same or different hostgroup. [integer] + - Enables query mirroring. If set I(mirror_hostgroup) can be used to mirror queries to the same or different hostgroup. error_msg: description: - - Query will be blocked, and the specified error_msg will be returned to - the client. [string] + - Query will be blocked, and the specified error_msg will be returned to the client. log: description: - - Query will be logged. [boolean] + - Query will be logged. apply: description: - - Used in combination with I(flagIN) and I(flagOUT) to create chains of - rules. Setting apply to True signifies the last rule to be applied. - [boolean] + - Used in combination with I(flagIN) and I(flagOUT) to create chains of rules. Setting apply to True signifies the last rule to be applied. comment: description: - - Free form text field, usable for a descriptive comment of the query - rule. [string] + - Free form text field, usable for a descriptive comment of the query rule. state: description: - - When C(present) - adds the rule, when C(absent) - removes the rule. + - "When C(present) - adds the rule, when C(absent) - removes the rule." choices: [ "present", "absent" ] default: present force_delete: @@ -155,8 +132,7 @@ default: False save_to_disk: description: - - Save mysql host config to sqlite db on disk to persist the - configuration. + - Save mysql host config to sqlite db on disk to persist the configuration. default: True load_to_runtime: description: @@ -180,20 +156,17 @@ default: 6032 config_file: description: - - Specify a config file from which login_user and login_password are to - be read + - Specify a config file from which login_user and login_password are to be read default: '' ''' EXAMPLES = ''' ---- # This example adds a rule to redirect queries from a specific user to another # hostgroup, it saves the mysql query rule config to disk, but avoids loading -# the mysql query config config to runtime (this might be because several +# the mysql query config config to runtime (this might be because several # rules are being added and the user wants to push the config to runtime in a # single batch using the M(proxysql_manage_config) module). It uses supplied # credentials to connect to the proxysql admin interface. - - proxysql_backend_servers: login_user: admin login_password: admin From cb6168a251b0caa1855772fb3a4571f812c9e6e3 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 2 Nov 2016 17:39:03 -0700 Subject: [PATCH 2305/2522] Revert "several fixes to doc to avoid breaking doc build" This reverts commit 6e4a182684a54e60be5318525c658cfe315d06ad. reverting proxysql modules for owner to fix docs build so reverting this fix as well. --- database/proxysql/proxysql_query_rules.py | 85 +++++++++++++++-------- 1 file changed, 56 insertions(+), 29 deletions(-) diff --git a/database/proxysql/proxysql_query_rules.py b/database/proxysql/proxysql_query_rules.py index 993f1485113..8af46c6ab9c 100644 --- a/database/proxysql/proxysql_query_rules.py +++ b/database/proxysql/proxysql_query_rules.py @@ -23,105 +23,128 @@ author: "Ben Mildren (@bmildren)" short_description: Modifies query rules using the proxysql admin interface. description: - - The M(proxysql_query_rules) module modifies query rules using the proxysql admin interface. + - The M(proxysql_query_rules) module modifies query rules using the proxysql + admin interface. options: rule_id: description: - The unique id of the rule. Rules are processed in rule_id order. + [integer] active: description: - - A rule with I(active) set to C(False) will be tracked in the database, but will be never loaded in the in-memory data structures + - A rule with I(active) set to C(False) will be tracked in the database, + but will be never loaded in the in-memory data structures [boolean] username: description: - - Filtering criteria matching username. If I(username) is non-NULL, a query will match only if the connection is made with the correct username. + - Filtering criteria matching username. If I(username) is non-NULL, a + query will match only if the connection is made with the correct + username. [string] schemaname: description: - - Filtering criteria matching schemaname. If I(schemaname) is non-NULL, - a query will match only if the connection uses schemaname as its default schema. + - Filtering criteria matching schemaname. If I(schemaname) is + non-NULL, a query will match only if the connection uses schemaname as + its default schema. [string] flagIN: description: - - Used in combination with I(flagOUT) and I(apply) to create chains of rules. + - Used in combination with I(flagOUT) and I(apply) to create chains of + rules. [integer] client_addr: description: - - Match traffic from a specific source. + - Match traffic from a specific source. [string] proxy_addr: description: - - Match incoming traffic on a specific local IP. + - Match incoming traffic on a specific local IP. [string] proxy_port: description: - - Match incoming traffic on a specific local port. + - Match incoming traffic on a specific local port. [integer] digest: description: - - Match queries with a specific digest, as returned by stats_mysql_query_digest.digest. + - Match queries with a specific digest, as returned by + stats_mysql_query_digest.digest. [string] match_digest: description: - - "Regular expression that matches the query digest. - The dialect of regular expressions used is that of re2 - https://github.com/google/re2." + - Regular expression that matches the query digest. The dialect of + regular expressions used is that of re2 - + https://github.com/google/re2. [string] match_pattern: description: - - "Regular expression that matches the query text. The dialect of regular expressions used is that of re2 - https://github.com/google/re2." + - Regular expression that matches the query text. The dialect of regular + expressions used is that of re2 - https://github.com/google/re2. + [string] negate_match_pattern: description: - If I(negate_match_pattern) is set to C(True), only queries not matching the query text will be considered as a match. This acts as a NOT operator in front of the regular expression matching against - match_pattern. + match_pattern. [boolean] flagOUT: description: - Used in combination with I(flagIN) and apply to create chains of rules. When set, I(flagOUT) signifies the I(flagIN) to be used in the next - chain of rules. + chain of rules. [integer] replace_pattern: description: - This is the pattern with which to replace the matched pattern. Note that this is optional, and when ommitted, the query processor will only - cache, route, or set other parameters without rewriting. + cache, route, or set other parameters without rewriting. [string] destination_hostgroup: description: - - Route matched queries to this hostgroup. This happens unless there is a started transaction and the logged in user has + - Route matched queries to this hostgroup. This happens unless there is + a started transaction and the logged in user has I(transaction_persistent) set to C(True) (see M(proxysql_mysql_users)). + [integer] cache_ttl: description: - The number of milliseconds for which to cache the result of the query. - Note in ProxySQL 1.1 I(cache_ttl) was in seconds. + Note in ProxySQL 1.1 I(cache_ttl) was in seconds. [integer] timeout: description: - The maximum timeout in milliseconds with which the matched or rewritten query should be executed. If a query run for longer than the specific threshold, the query is automatically killed. If timeout is not specified, the global variable mysql-default_query_timeout applies. + [integer] retries: description: - - The maximum number of times a query needs to be re-executed in case of detected failure during the execution of the query. - If retries is not specified, the global variable mysql-query_retries_on_failure applies. + - The maximum number of times a query needs to be re-executed in case of + detected failure during the execution of the query. If retries is not + specified, the global variable mysql-query_retries_on_failure applies. + [integer] delay: description: - Number of milliseconds to delay the execution of the query. This is essentially a throttling mechanism and QoS, and allows a way to give priority to queries over others. This value is added to the mysql-default_query_delay global variable that applies to all queries. + [integer] mirror_flagOUT: description: - Enables query mirroring. If set I(mirror_flagOUT) can be used to evaluates the mirrored query against the specified chain of rules. + [integer] mirror_hostgroup: description: - - Enables query mirroring. If set I(mirror_hostgroup) can be used to mirror queries to the same or different hostgroup. + - Enables query mirroring. If set I(mirror_hostgroup) can be used to + mirror queries to the same or different hostgroup. [integer] error_msg: description: - - Query will be blocked, and the specified error_msg will be returned to the client. + - Query will be blocked, and the specified error_msg will be returned to + the client. [string] log: description: - - Query will be logged. + - Query will be logged. [boolean] apply: description: - - Used in combination with I(flagIN) and I(flagOUT) to create chains of rules. Setting apply to True signifies the last rule to be applied. + - Used in combination with I(flagIN) and I(flagOUT) to create chains of + rules. Setting apply to True signifies the last rule to be applied. + [boolean] comment: description: - - Free form text field, usable for a descriptive comment of the query rule. + - Free form text field, usable for a descriptive comment of the query + rule. [string] state: description: - - "When C(present) - adds the rule, when C(absent) - removes the rule." + - When C(present) - adds the rule, when C(absent) - removes the rule. choices: [ "present", "absent" ] default: present force_delete: @@ -132,7 +155,8 @@ default: False save_to_disk: description: - - Save mysql host config to sqlite db on disk to persist the configuration. + - Save mysql host config to sqlite db on disk to persist the + configuration. default: True load_to_runtime: description: @@ -156,17 +180,20 @@ default: 6032 config_file: description: - - Specify a config file from which login_user and login_password are to be read + - Specify a config file from which login_user and login_password are to + be read default: '' ''' EXAMPLES = ''' +--- # This example adds a rule to redirect queries from a specific user to another # hostgroup, it saves the mysql query rule config to disk, but avoids loading -# the mysql query config config to runtime (this might be because several +# the mysql query config config to runtime (this might be because several # rules are being added and the user wants to push the config to runtime in a # single batch using the M(proxysql_manage_config) module). It uses supplied # credentials to connect to the proxysql admin interface. + - proxysql_backend_servers: login_user: admin login_password: admin From cee034dd897d42b59ac87fdfdf8b2dda5c443cfe Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 2 Nov 2016 17:39:33 -0700 Subject: [PATCH 2306/2522] Revert "add support for proxysql (#2917)" This reverts commit 9d51f823956e47b02abd7754522b5bd4202bd82c. proxysql is breaking docs build. Reverting until those are fixed --- database/proxysql/__init__.py | 0 database/proxysql/proxysql_backend_servers.py | 547 --------------- .../proxysql/proxysql_global_variables.py | 310 -------- database/proxysql/proxysql_manage_config.py | 251 ------- database/proxysql/proxysql_mysql_users.py | 516 -------------- database/proxysql/proxysql_query_rules.py | 659 ------------------ .../proxysql_replication_hostgroups.py | 424 ----------- database/proxysql/proxysql_scheduler.py | 458 ------------ 8 files changed, 3165 deletions(-) delete mode 100644 database/proxysql/__init__.py delete mode 100644 database/proxysql/proxysql_backend_servers.py delete mode 100644 database/proxysql/proxysql_global_variables.py delete mode 100644 database/proxysql/proxysql_manage_config.py delete mode 100644 database/proxysql/proxysql_mysql_users.py delete mode 100644 database/proxysql/proxysql_query_rules.py delete mode 100644 database/proxysql/proxysql_replication_hostgroups.py delete mode 100644 database/proxysql/proxysql_scheduler.py diff --git a/database/proxysql/__init__.py b/database/proxysql/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/database/proxysql/proxysql_backend_servers.py b/database/proxysql/proxysql_backend_servers.py deleted file mode 100644 index 0d743701752..00000000000 --- a/database/proxysql/proxysql_backend_servers.py +++ /dev/null @@ -1,547 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -DOCUMENTATION = ''' ---- -module: proxysql_backend_servers -version_added: "2.3" -author: "Ben Mildren (@bmildren)" -short_description: Adds or removes mysql hosts from proxysql admin interface. -description: - - The M(proxysql_backend_servers) module adds or removes mysql hosts using - the proxysql admin interface. -options: - hostgroup_id: - description: - - The hostgroup in which this mysqld instance is included. An instance - can be part of one or more hostgroups. - default: 0 - hostname: - description: - - The ip address at which the mysqld instance can be contacted. - required: True - port: - description: - - The port at which the mysqld instance can be contacted. - default: 3306 - status: - description: - - ONLINE - Backend server is fully operational. - OFFLINE_SOFT - When a server is put into C(OFFLINE_SOFT) mode, - connections are kept in use until the current - transaction is completed. This allows to gracefully - detach a backend. - OFFLINE_HARD - When a server is put into C(OFFLINE_HARD) mode, the - existing connections are dropped, while new incoming - connections aren't accepted either. - - If ommitted the proxysql default for I(status) is C(ONLINE). - choices: [ "ONLINE", "OFFLINE_SOFT", "OFFLINE_HARD"] - weight: - description: - - The bigger the weight of a server relative to other weights, the higher - the probability of the server being chosen from the hostgroup. - If ommitted the proxysql default for I(weight) is 1. - compression: - description: - - If the value of I(compression) is greater than 0, new connections to - that server will use compression. - If ommitted the proxysql default for I(compression) is 0. - max_connections: - description: - - The maximum number of connections ProxySQL will open to this backend - server. - If ommitted the proxysql default for I(max_connections) is 1000. - max_replication_lag: - description: - - If greater than 0, ProxySQL will reguarly monitor replication lag. If - replication lag goes above I(max_replication_lag), proxysql will - temporarily shun the server until replication catches up. - If ommitted the proxysql default for I(max_replication_lag) is 0. - use_ssl: - description: - - If I(use_ssl) is set to C(True), connections to this server will be - made using SSL connections. - If ommitted the proxysql default for I(use_ssl) is C(False). - max_latency_ms: - description: - - Ping time is monitored regularly. If a host has a ping time greater - than I(max_latency_ms) it is excluded from the connection pool - (although the server stays ONLINE). - If ommitted the proxysql default for I(max_latency_ms) is 0. - comment: - description: - - Text field that can be used for any purposed defined by the user. Could - be a description of what the host stores, a reminder of when the host - was added or disabled, or a JSON processed by some checker script. - default: '' - state: - description: - - When C(present) - adds the host, when C(absent) - removes the host. - choices: [ "present", "absent" ] - default: present - save_to_disk: - description: - - Save mysql host config to sqlite db on disk to persist the - configuration. - default: True - load_to_runtime: - description: - - Dynamically load mysql host config to runtime memory. - default: True - login_user: - description: - - The username used to authenticate to ProxySQL admin interface - default: None - login_password: - description: - - The password used to authenticate to ProxySQL admin interface - default: None - login_host: - description: - - The host used to connect to ProxySQL admin interface - default: '127.0.0.1' - login_port: - description: - - The port used to connect to ProxySQL admin interface - default: 6032 - config_file: - description: - - Specify a config file from which login_user and login_password are to - be read - default: '' -''' - -EXAMPLES = ''' ---- -# This example adds a server, it saves the mysql server config to disk, but -# avoids loading the mysql server config to runtime (this might be because -# several servers are being added and the user wants to push the config to -# runtime in a single batch using the M(proxysql_manage_config) module). It -# uses supplied credentials to connect to the proxysql admin interface. - -- proxysql_backend_servers: - login_user: 'admin' - login_password: 'admin' - hostname: 'mysql01' - state: present - load_to_runtime: False - -# This example removes a server, saves the mysql server config to disk, and -# dynamically loads the mysql server config to runtime. It uses credentials -# in a supplied config file to connect to the proxysql admin interface. - -- proxysql_backend_servers: - config_file: '~/proxysql.cnf' - hostname: 'mysql02' - state: absent -''' - -RETURN = ''' -stdout: - description: The mysql host modified or removed from proxysql - returned: On create/update will return the newly modified host, on delete - it will return the deleted record. - type: dict - "sample": { - "changed": true, - "hostname": "192.168.52.1", - "msg": "Added server to mysql_hosts", - "server": { - "comment": "", - "compression": "0", - "hostgroup_id": "1", - "hostname": "192.168.52.1", - "max_connections": "1000", - "max_latency_ms": "0", - "max_replication_lag": "0", - "port": "3306", - "status": "ONLINE", - "use_ssl": "0", - "weight": "1" - }, - "state": "present" - } -''' - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.mysql import mysql_connect -from ansible.module_utils.pycompat24 import get_exception - -try: - import MySQLdb - import MySQLdb.cursors -except ImportError: - MYSQLDB_FOUND = False -else: - MYSQLDB_FOUND = True - -# =========================================== -# proxysql module specific support methods. -# - - -def perform_checks(module): - if module.params["login_port"] < 0 \ - or module.params["login_port"] > 65535: - module.fail_json( - msg="login_port must be a valid unix port number (0-65535)" - ) - - if module.params["port"] < 0 \ - or module.params["port"] > 65535: - module.fail_json( - msg="port must be a valid unix port number (0-65535)" - ) - - if module.params["compression"]: - if module.params["compression"] < 0 \ - or module.params["compression"] > 102400: - module.fail_json( - msg="compression must be set between 0 and 102400" - ) - - if module.params["max_replication_lag"]: - if module.params["max_replication_lag"] < 0 \ - or module.params["max_replication_lag"] > 126144000: - module.fail_json( - msg="max_replication_lag must be set between 0 and 102400" - ) - - if not MYSQLDB_FOUND: - module.fail_json( - msg="the python mysqldb module is required" - ) - - -def save_config_to_disk(cursor): - cursor.execute("SAVE MYSQL SERVERS TO DISK") - return True - - -def load_config_to_runtime(cursor): - cursor.execute("LOAD MYSQL SERVERS TO RUNTIME") - return True - - -class ProxySQLServer(object): - - def __init__(self, module): - self.state = module.params["state"] - self.save_to_disk = module.params["save_to_disk"] - self.load_to_runtime = module.params["load_to_runtime"] - - self.hostgroup_id = module.params["hostgroup_id"] - self.hostname = module.params["hostname"] - self.port = module.params["port"] - - config_data_keys = ["status", - "weight", - "compression", - "max_connections", - "max_replication_lag", - "use_ssl", - "max_latency_ms", - "comment"] - - self.config_data = dict((k, module.params[k]) - for k in config_data_keys) - - def check_server_config_exists(self, cursor): - query_string = \ - """SELECT count(*) AS `host_count` - FROM mysql_servers - WHERE hostgroup_id = %s - AND hostname = %s - AND port = %s""" - - query_data = \ - [self.hostgroup_id, - self.hostname, - self.port] - - cursor.execute(query_string, query_data) - check_count = cursor.fetchone() - return (int(check_count['host_count']) > 0) - - def check_server_config(self, cursor): - query_string = \ - """SELECT count(*) AS `host_count` - FROM mysql_servers - WHERE hostgroup_id = %s - AND hostname = %s - AND port = %s""" - - query_data = \ - [self.hostgroup_id, - self.hostname, - self.port] - - for col, val in self.config_data.iteritems(): - if val is not None: - query_data.append(val) - query_string += "\n AND " + col + " = %s" - - cursor.execute(query_string, query_data) - check_count = cursor.fetchone() - return (int(check_count['host_count']) > 0) - - def get_server_config(self, cursor): - query_string = \ - """SELECT * - FROM mysql_servers - WHERE hostgroup_id = %s - AND hostname = %s - AND port = %s""" - - query_data = \ - [self.hostgroup_id, - self.hostname, - self.port] - - cursor.execute(query_string, query_data) - server = cursor.fetchone() - return server - - def create_server_config(self, cursor): - query_string = \ - """INSERT INTO mysql_servers ( - hostgroup_id, - hostname, - port""" - - cols = 3 - query_data = \ - [self.hostgroup_id, - self.hostname, - self.port] - - for col, val in self.config_data.iteritems(): - if val is not None: - cols += 1 - query_data.append(val) - query_string += ",\n" + col - - query_string += \ - (")\n" + - "VALUES (" + - "%s ," * cols) - - query_string = query_string[:-2] - query_string += ")" - - cursor.execute(query_string, query_data) - return True - - def update_server_config(self, cursor): - query_string = """UPDATE mysql_servers""" - - cols = 0 - query_data = [] - - for col, val in self.config_data.iteritems(): - if val is not None: - cols += 1 - query_data.append(val) - if cols == 1: - query_string += "\nSET " + col + "= %s," - else: - query_string += "\n " + col + " = %s," - - query_string = query_string[:-1] - query_string += ("\nWHERE hostgroup_id = %s\n AND hostname = %s" + - "\n AND port = %s") - - query_data.append(self.hostgroup_id) - query_data.append(self.hostname) - query_data.append(self.port) - - cursor.execute(query_string, query_data) - return True - - def delete_server_config(self, cursor): - query_string = \ - """DELETE FROM mysql_servers - WHERE hostgroup_id = %s - AND hostname = %s - AND port = %s""" - - query_data = \ - [self.hostgroup_id, - self.hostname, - self.port] - - cursor.execute(query_string, query_data) - return True - - def manage_config(self, cursor, state): - if state: - if self.save_to_disk: - save_config_to_disk(cursor) - if self.load_to_runtime: - load_config_to_runtime(cursor) - - def create_server(self, check_mode, result, cursor): - if not check_mode: - result['changed'] = \ - self.create_server_config(cursor) - result['msg'] = "Added server to mysql_hosts" - result['server'] = \ - self.get_server_config(cursor) - self.manage_config(cursor, - result['changed']) - else: - result['changed'] = True - result['msg'] = ("Server would have been added to" + - " mysql_hosts, however check_mode" + - " is enabled.") - - def update_server(self, check_mode, result, cursor): - if not check_mode: - result['changed'] = \ - self.update_server_config(cursor) - result['msg'] = "Updated server in mysql_hosts" - result['server'] = \ - self.get_server_config(cursor) - self.manage_config(cursor, - result['changed']) - else: - result['changed'] = True - result['msg'] = ("Server would have been updated in" + - " mysql_hosts, however check_mode" + - " is enabled.") - - def delete_server(self, check_mode, result, cursor): - if not check_mode: - result['server'] = \ - self.get_server_config(cursor) - result['changed'] = \ - self.delete_server_config(cursor) - result['msg'] = "Deleted server from mysql_hosts" - self.manage_config(cursor, - result['changed']) - else: - result['changed'] = True - result['msg'] = ("Server would have been deleted from" + - " mysql_hosts, however check_mode is" + - " enabled.") - -# =========================================== -# Module execution. -# - - -def main(): - module = AnsibleModule( - argument_spec=dict( - login_user=dict(default=None, type='str'), - login_password=dict(default=None, no_log=True, type='str'), - login_host=dict(default='127.0.0.1'), - login_unix_socket=dict(default=None), - login_port=dict(default=6032, type='int'), - config_file=dict(default='', type='path'), - hostgroup_id=dict(default=0, type='int'), - hostname=dict(required=True, type='str'), - port=dict(default=3306, type='int'), - status=dict(choices=['ONLINE', - 'OFFLINE_SOFT', - 'OFFLINE_HARD']), - weight=dict(type='int'), - compression=dict(type='int'), - max_connections=dict(type='int'), - max_replication_lag=dict(type='int'), - use_ssl=dict(type='bool'), - max_latency_ms=dict(type='int'), - comment=dict(default='', type='str'), - state=dict(default='present', choices=['present', - 'absent']), - save_to_disk=dict(default=True, type='bool'), - load_to_runtime=dict(default=True, type='bool') - ), - supports_check_mode=True - ) - - perform_checks(module) - - login_user = module.params["login_user"] - login_password = module.params["login_password"] - config_file = module.params["config_file"] - - cursor = None - try: - cursor = mysql_connect(module, - login_user, - login_password, - config_file, - cursor_class=MySQLdb.cursors.DictCursor) - except MySQLdb.Error: - e = get_exception() - module.fail_json( - msg="unable to connect to ProxySQL Admin Module.. %s" % e - ) - - proxysql_server = ProxySQLServer(module) - result = {} - - result['state'] = proxysql_server.state - if proxysql_server.hostname: - result['hostname'] = proxysql_server.hostname - - if proxysql_server.state == "present": - try: - if not proxysql_server.check_server_config_exists(cursor): - if not proxysql_server.check_server_config(cursor): - proxysql_server.create_server(module.check_mode, - result, - cursor) - else: - proxysql_server.update_server(module.check_mode, - result, - cursor) - else: - result['changed'] = False - result['msg'] = ("The server already exists in mysql_hosts" + - " and doesn't need to be updated.") - result['server'] = \ - proxysql_server.get_server_config(cursor) - except MySQLdb.Error: - e = get_exception() - module.fail_json( - msg="unable to modify server.. %s" % e - ) - - elif proxysql_server.state == "absent": - try: - if proxysql_server.check_server_config_exists(cursor): - proxysql_server.delete_server(module.check_mode, - result, - cursor) - else: - result['changed'] = False - result['msg'] = ("The server is already absent from the" + - " mysql_hosts memory configuration") - except MySQLdb.Error: - e = get_exception() - module.fail_json( - msg="unable to remove server.. %s" % e - ) - - module.exit_json(**result) - -if __name__ == '__main__': - main() diff --git a/database/proxysql/proxysql_global_variables.py b/database/proxysql/proxysql_global_variables.py deleted file mode 100644 index 36f6019fc7b..00000000000 --- a/database/proxysql/proxysql_global_variables.py +++ /dev/null @@ -1,310 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -DOCUMENTATION = ''' ---- -module: proxysql_global_variables -version_added: "2.3" -author: "Ben Mildren (@bmildren)" -short_description: Gets or sets the proxysql global variables. -description: - - The M(proxysql_global_variables) module gets or sets the proxysql global - variables. -options: - variable: - description: - - Defines which variable should be returned, or if I(value) is specified - which variable should be updated. - required: True - value: - description: - - Defines a value the variable specified using I(variable) should be set - to. - save_to_disk: - description: - - Save mysql host config to sqlite db on disk to persist the - configuration. - default: True - load_to_runtime: - description: - - Dynamically load mysql host config to runtime memory. - default: True - login_user: - description: - - The username used to authenticate to ProxySQL admin interface - default: None - login_password: - description: - - The password used to authenticate to ProxySQL admin interface - default: None - login_host: - description: - - The host used to connect to ProxySQL admin interface - default: '127.0.0.1' - login_port: - description: - - The port used to connect to ProxySQL admin interface - default: 6032 - config_file: - description: - - Specify a config file from which login_user and login_password are to - be read - default: '' -''' - -EXAMPLES = ''' ---- -# This example sets the value of a variable, saves the mysql admin variables -# config to disk, and dynamically loads the mysql admin variables config to -# runtime. It uses supplied credentials to connect to the proxysql admin -# interface. - -- proxysql_global_variables: - login_user: 'admin' - login_password: 'admin' - variable: 'mysql-max_connections' - value: 4096 - -# This example gets the value of a variable. It uses credentials in a -# supplied config file to connect to the proxysql admin interface. - -- proxysql_global_variables: - config_file: '~/proxysql.cnf' - variable: 'mysql-default_query_delay' -''' - -RETURN = ''' -stdout: - description: Returns the mysql variable supplied with it's associted value. - returned: Returns the current variable and value, or the newly set value - for the variable supplied.. - type: dict - "sample": { - "changed": false, - "msg": "The variable is already been set to the supplied value", - "var": { - "variable_name": "mysql-poll_timeout", - "variable_value": "3000" - } - } -''' - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.mysql import mysql_connect -from ansible.module_utils.pycompat24 import get_exception - -try: - import MySQLdb - import MySQLdb.cursors -except ImportError: - MYSQLDB_FOUND = False -else: - MYSQLDB_FOUND = True - -# =========================================== -# proxysql module specific support methods. -# - - -def perform_checks(module): - if module.params["login_port"] < 0 \ - or module.params["login_port"] > 65535: - module.fail_json( - msg="login_port must be a valid unix port number (0-65535)" - ) - - if not MYSQLDB_FOUND: - module.fail_json( - msg="the python mysqldb module is required" - ) - - -def save_config_to_disk(variable, cursor): - if variable.startswith("admin"): - cursor.execute("SAVE ADMIN VARIABLES TO DISK") - else: - cursor.execute("SAVE MYSQL VARIABLES TO DISK") - return True - - -def load_config_to_runtime(variable, cursor): - if variable.startswith("admin"): - cursor.execute("LOAD ADMIN VARIABLES TO RUNTIME") - else: - cursor.execute("LOAD MYSQL VARIABLES TO RUNTIME") - return True - - -def check_config(variable, value, cursor): - query_string = \ - """SELECT count(*) AS `variable_count` - FROM global_variables - WHERE variable_name = %s and variable_value = %s""" - - query_data = \ - [variable, value] - - cursor.execute(query_string, query_data) - check_count = cursor.fetchone() - return (int(check_count['variable_count']) > 0) - - -def get_config(variable, cursor): - - query_string = \ - """SELECT * - FROM global_variables - WHERE variable_name = %s""" - - query_data = \ - [variable, ] - - cursor.execute(query_string, query_data) - row_count = cursor.rowcount - resultset = cursor.fetchone() - - if row_count > 0: - return resultset - else: - return False - - -def set_config(variable, value, cursor): - - query_string = \ - """UPDATE global_variables - SET variable_value = %s - WHERE variable_name = %s""" - - query_data = \ - [value, variable] - - cursor.execute(query_string, query_data) - return True - - -def manage_config(variable, save_to_disk, load_to_runtime, cursor, state): - if state: - if save_to_disk: - save_config_to_disk(variable, cursor) - if load_to_runtime: - load_config_to_runtime(variable, cursor) - -# =========================================== -# Module execution. -# - - -def main(): - module = AnsibleModule( - argument_spec=dict( - login_user=dict(default=None, type='str'), - login_password=dict(default=None, no_log=True, type='str'), - login_host=dict(default="127.0.0.1"), - login_unix_socket=dict(default=None), - login_port=dict(default=6032, type='int'), - config_file=dict(default="", type='path'), - variable=dict(required=True, type='str'), - value=dict(), - save_to_disk=dict(default=True, type='bool'), - load_to_runtime=dict(default=True, type='bool') - ), - supports_check_mode=True - ) - - perform_checks(module) - - login_user = module.params["login_user"] - login_password = module.params["login_password"] - config_file = module.params["config_file"] - variable = module.params["variable"] - value = module.params["value"] - save_to_disk = module.params["save_to_disk"] - load_to_runtime = module.params["load_to_runtime"] - - cursor = None - try: - cursor = mysql_connect(module, - login_user, - login_password, - config_file, - cursor_class=MySQLdb.cursors.DictCursor) - except MySQLdb.Error: - e = get_exception() - module.fail_json( - msg="unable to connect to ProxySQL Admin Module.. %s" % e - ) - - result = {} - - if not value: - try: - if get_config(variable, cursor): - result['changed'] = False - result['msg'] = \ - "Returned the variable and it's current value" - result['var'] = get_config(variable, cursor) - else: - module.fail_json( - msg="The variable \"%s\" was not found" % variable - ) - - except MySQLdb.Error: - e = get_exception() - module.fail_json( - msg="unable to get config.. %s" % e - ) - else: - try: - if get_config(variable, cursor): - if not check_config(variable, value, cursor): - if not module.check_mode: - result['changed'] = set_config(variable, value, cursor) - result['msg'] = \ - "Set the variable to the supplied value" - result['var'] = get_config(variable, cursor) - manage_config(variable, - save_to_disk, - load_to_runtime, - cursor, - result['changed']) - else: - result['changed'] = True - result['msg'] = ("Variable would have been set to" + - " the supplied value, however" + - " check_mode is enabled.") - else: - result['changed'] = False - result['msg'] = ("The variable is already been set to" + - " the supplied value") - result['var'] = get_config(variable, cursor) - else: - module.fail_json( - msg="The variable \"%s\" was not found" % variable - ) - - except MySQLdb.Error: - e = get_exception() - module.fail_json( - msg="unable to set config.. %s" % e - ) - - module.exit_json(**result) - -if __name__ == '__main__': - main() diff --git a/database/proxysql/proxysql_manage_config.py b/database/proxysql/proxysql_manage_config.py deleted file mode 100644 index 57a6ecf6967..00000000000 --- a/database/proxysql/proxysql_manage_config.py +++ /dev/null @@ -1,251 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -DOCUMENTATION = ''' ---- -module: proxysql_manage_config -version_added: "2.3" - -author: "Ben Mildren (@bmildren)" -short_description: Writes the proxysql configuration settings between layers. -description: - - The M(proxysql_global_variables) module writes the proxysql configuration - settings between layers. Currently this module will always report a - changed state, so should typically be used with WHEN however this will - change in a future version when the CHECKSUM table commands are available - for all tables in proxysql. -options: - action: - description: - - The supplied I(action) combines with the supplied I(direction) to - provide the semantics of how we want to move the I(config_settings) - between the I(config_layers). - choices: [ "LOAD", "SAVE" ] - required: True - config_settings: - description: - - The I(config_settings) specifies which configuration we're writing. - choices: [ "MYSQL USERS", "MYSQL SERVERS", "MYSQL QUERY RULES", - "MYSQL VARIABLES", "ADMIN VARIABLES", "SCHEDULER" ] - required: True - direction: - description: - - FROM - denotes we're reading values FROM the supplied I(config_layer) - and writing to the next layer - TO - denotes we're reading from the previous layer and writing TO the - supplied I(config_layer). - choices: [ "FROM", "TO" ] - required: True - config_layer: - description: - - RUNTIME - represents the in-memory data structures of ProxySQL used by - the threads that are handling the requests. - MEMORY - (sometime also referred as main) represents the in-memory - SQLite3 database. - DISK - represents the on-disk SQLite3 database. - CONFIG - is the classical config file. You can only LOAD FROM the - config file. - choices: [ "MEMORY", "DISK", "RUNTIME", "CONFIG" ] - required: True - login_user: - description: - - The username used to authenticate to ProxySQL admin interface - default: None - login_password: - description: - - The password used to authenticate to ProxySQL admin interface - default: None - login_host: - description: - - The host used to connect to ProxySQL admin interface - default: '127.0.0.1' - login_port: - description: - - The port used to connect to ProxySQL admin interface - default: 6032 - config_file: - description: - - Specify a config file from which login_user and login_password are to - be read - default: '' -''' - -EXAMPLES = ''' ---- -# This example saves the mysql users config from memory to disk. It uses -# supplied credentials to connect to the proxysql admin interface. - -- proxysql_global_variables: - login_user: 'admin' - login_password: 'admin' - action: "SAVE" - config_settings: "MYSQL USERS" - direction: "FROM" - config_layer: "MEMORY" - -# This example loads the mysql query rules config from memory to to runtime. It -# uses supplied credentials to connect to the proxysql admin interface. - -- proxysql_global_variables: - config_file: '~/proxysql.cnf' - action: "LOAD" - config_settings: "MYSQL QUERY RULES" - direction: "TO" - config_layer: "RUNTIME" -''' - -RETURN = ''' -stdout: - description: Simply reports whether the action reported a change. - returned: Currently the returned value with always be changed=True. - type: dict - "sample": { - "changed": true - } -''' - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.mysql import mysql_connect -from ansible.module_utils.pycompat24 import get_exception - -try: - import MySQLdb -except ImportError: - MYSQLDB_FOUND = False -else: - MYSQLDB_FOUND = True - -# =========================================== -# proxysql module specific support methods. -# - - -def perform_checks(module): - if module.params["login_port"] < 0 \ - or module.params["login_port"] > 65535: - module.fail_json( - msg="login_port must be a valid unix port number (0-65535)" - ) - - if module.params["config_layer"] == 'CONFIG' and \ - (module.params["action"] != 'LOAD' or - module.params["direction"] != 'FROM'): - - if (module.params["action"] != 'LOAD' and - module.params["direction"] != 'FROM'): - msg_string = ("Neither the action \"%s\" nor the direction" + - " \"%s\" are valid combination with the CONFIG" + - " config_layer") - module.fail_json(msg=msg_string % (module.params["action"], - module.params["direction"])) - - elif module.params["action"] != 'LOAD': - msg_string = ("The action \"%s\" is not a valid combination" + - " with the CONFIG config_layer") - module.fail_json(msg=msg_string % module.params["action"]) - - else: - msg_string = ("The direction \"%s\" is not a valid combination" + - " with the CONFIG config_layer") - module.fail_json(msg=msg_string % module.params["direction"]) - - if not MYSQLDB_FOUND: - module.fail_json( - msg="the python mysqldb module is required" - ) - - -def manage_config(manage_config_settings, cursor): - - query_string = "%s" % ' '.join(manage_config_settings) - - cursor.execute(query_string) - return True - -# =========================================== -# Module execution. -# - - -def main(): - module = AnsibleModule( - argument_spec=dict( - login_user=dict(default=None, type='str'), - login_password=dict(default=None, no_log=True, type='str'), - login_host=dict(default="127.0.0.1"), - login_unix_socket=dict(default=None), - login_port=dict(default=6032, type='int'), - config_file=dict(default="", type='path'), - action=dict(required=True, choices=['LOAD', - 'SAVE']), - config_settings=dict(required=True, choices=['MYSQL USERS', - 'MYSQL SERVERS', - 'MYSQL QUERY RULES', - 'MYSQL VARIABLES', - 'ADMIN VARIABLES', - 'SCHEDULER']), - direction=dict(required=True, choices=['FROM', - 'TO']), - config_layer=dict(required=True, choices=['MEMORY', - 'DISK', - 'RUNTIME', - 'CONFIG']) - ), - supports_check_mode=True - ) - - perform_checks(module) - - login_user = module.params["login_user"] - login_password = module.params["login_password"] - config_file = module.params["config_file"] - action = module.params["action"] - config_settings = module.params["config_settings"] - direction = module.params["direction"] - config_layer = module.params["config_layer"] - - cursor = None - try: - cursor = mysql_connect(module, - login_user, - login_password, - config_file) - except MySQLdb.Error: - e = get_exception() - module.fail_json( - msg="unable to connect to ProxySQL Admin Module.. %s" % e - ) - - result = {} - - manage_config_settings = \ - [action, config_settings, direction, config_layer] - - try: - result['changed'] = manage_config(manage_config_settings, - cursor) - except MySQLdb.Error: - e = get_exception() - module.fail_json( - msg="unable to manage config.. %s" % e - ) - - module.exit_json(**result) - -if __name__ == '__main__': - main() diff --git a/database/proxysql/proxysql_mysql_users.py b/database/proxysql/proxysql_mysql_users.py deleted file mode 100644 index 4eec7d9b60d..00000000000 --- a/database/proxysql/proxysql_mysql_users.py +++ /dev/null @@ -1,516 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -DOCUMENTATION = ''' ---- -module: proxysql_mysql_users -version_added: "2.3" -author: "Ben Mildren (@bmildren)" -short_description: Adds or removes mysql users from proxysql admin interface. -description: - - The M(proxysql_mysql_users) module adds or removes mysql users using the - proxysql admin interface. -options: - username: - description: - - Name of the user connecting to the mysqld or ProxySQL instance. - required: True - password: - description: - - Password of the user connecting to the mysqld or ProxySQL instance. - active: - description: - - A user with I(active) set to C(False) will be tracked in the database, - but will be never loaded in the in-memory data structures. - If ommitted the proxysql default for I(active) is C(True). - use_ssl: - description: - - If I(use_ssl) is set to C(True), connections by this user will be - made using SSL connections. - If ommitted the proxysql default for I(use_ssl) is C(False). - default_hostgroup: - description: - - If there is no matching rule for the queries sent by this user, the - traffic it generates is sent to the specified hostgroup. - If ommitted the proxysql default for I(use_ssl) is 0. - default_schema: - description: - - The schema to which the connection should change to by default. - transaction_persistent: - description: - - If this is set for the user with which the MySQL client is connecting - to ProxySQL (thus a "frontend" user), transactions started within a - hostgroup will remain within that hostgroup regardless of any other - rules. - If ommitted the proxysql default for I(transaction_persistent) is - C(False). - fast_forward: - description: - - If I(fast_forward) is set to C(True), I(fast_forward) will bypass the - query processing layer (rewriting, caching) and pass through the query - directly as is to the backend server. - If ommitted the proxysql default for I(fast_forward) is C(False). - backend: - description: - - If I(backend) is set to C(True), this (username, password) pair is - used for authenticating to the ProxySQL instance. - default: True - frontend: - description: - - If I(frontend) is set to C(True), this (username, password) pair is - used for authenticating to the mysqld servers against any hostgroup. - default: True - max_connections: - description: - - The maximum number of connections ProxySQL will open to the backend - for this user. - If ommitted the proxysql default for I(max_connections) is 10000. - state: - description: - - When C(present) - adds the user, when C(absent) - removes the user. - choices: [ "present", "absent" ] - default: present - save_to_disk: - description: - - Save mysql host config to sqlite db on disk to persist the - configuration. - default: True - load_to_runtime: - description: - - Dynamically load mysql host config to runtime memory. - default: True - login_user: - description: - - The username used to authenticate to ProxySQL admin interface - default: None - login_password: - description: - - The password used to authenticate to ProxySQL admin interface - default: None - login_host: - description: - - The host used to connect to ProxySQL admin interface - default: '127.0.0.1' - login_port: - description: - - The port used to connect to ProxySQL admin interface - default: 6032 - config_file: - description: - - Specify a config file from which login_user and login_password are to - be read - default: '' -''' - -EXAMPLES = ''' ---- -# This example adds a user, it saves the mysql user config to disk, but -# avoids loading the mysql user config to runtime (this might be because -# several users are being added and the user wants to push the config to -# runtime in a single batch using the M(proxysql_manage_config) module). It -# uses supplied credentials to connect to the proxysql admin interface. - -- proxysql_mysql_users: - login_user: 'admin' - login_password: 'admin' - username: 'productiondba' - state: present - load_to_runtime: False - -# This example removes a user, saves the mysql user config to disk, and -# dynamically loads the mysql user config to runtime. It uses credentials -# in a supplied config file to connect to the proxysql admin interface. - -- proxysql_mysql_users: - config_file: '~/proxysql.cnf' - username: 'mysqlboy' - state: absent -''' - -RETURN = ''' -stdout: - description: The mysql user modified or removed from proxysql - returned: On create/update will return the newly modified user, on delete - it will return the deleted record. - type: dict - sample": { - "changed": true, - "msg": "Added user to mysql_users", - "state": "present", - "user": { - "active": "1", - "backend": "1", - "default_hostgroup": "1", - "default_schema": null, - "fast_forward": "0", - "frontend": "1", - "max_connections": "10000", - "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", - "schema_locked": "0", - "transaction_persistent": "0", - "use_ssl": "0", - "username": "guest_ro" - }, - "username": "guest_ro" - } -''' - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.mysql import mysql_connect -from ansible.module_utils.pycompat24 import get_exception - -try: - import MySQLdb - import MySQLdb.cursors -except ImportError: - MYSQLDB_FOUND = False -else: - MYSQLDB_FOUND = True - -# =========================================== -# proxysql module specific support methods. -# - - -def perform_checks(module): - if module.params["login_port"] < 0 \ - or module.params["login_port"] > 65535: - module.fail_json( - msg="login_port must be a valid unix port number (0-65535)" - ) - - if not MYSQLDB_FOUND: - module.fail_json( - msg="the python mysqldb module is required" - ) - - -def save_config_to_disk(cursor): - cursor.execute("SAVE MYSQL USERS TO DISK") - return True - - -def load_config_to_runtime(cursor): - cursor.execute("LOAD MYSQL USERS TO RUNTIME") - return True - - -class ProxySQLUser(object): - - def __init__(self, module): - self.state = module.params["state"] - self.save_to_disk = module.params["save_to_disk"] - self.load_to_runtime = module.params["load_to_runtime"] - - self.username = module.params["username"] - self.backend = module.params["backend"] - self.frontend = module.params["frontend"] - - config_data_keys = ["password", - "active", - "use_ssl", - "default_hostgroup", - "default_schema", - "transaction_persistent", - "fast_forward", - "max_connections"] - - self.config_data = dict((k, module.params[k]) - for k in config_data_keys) - - def check_user_config_exists(self, cursor): - query_string = \ - """SELECT count(*) AS `user_count` - FROM mysql_users - WHERE username = %s - AND backend = %s - AND frontend = %s""" - - query_data = \ - [self.username, - self.backend, - self.frontend] - - cursor.execute(query_string, query_data) - check_count = cursor.fetchone() - return (int(check_count['user_count']) > 0) - - def check_user_privs(self, cursor): - query_string = \ - """SELECT count(*) AS `user_count` - FROM mysql_users - WHERE username = %s - AND backend = %s - AND frontend = %s""" - - query_data = \ - [self.username, - self.backend, - self.frontend] - - for col, val in self.config_data.iteritems(): - if val is not None: - query_data.append(val) - query_string += "\n AND " + col + " = %s" - - cursor.execute(query_string, query_data) - check_count = cursor.fetchone() - return (int(check_count['user_count']) > 0) - - def get_user_config(self, cursor): - query_string = \ - """SELECT * - FROM mysql_users - WHERE username = %s - AND backend = %s - AND frontend = %s""" - - query_data = \ - [self.username, - self.backend, - self.frontend] - - cursor.execute(query_string, query_data) - user = cursor.fetchone() - return user - - def create_user_config(self, cursor): - query_string = \ - """INSERT INTO mysql_users ( - username, - backend, - frontend""" - - cols = 3 - query_data = \ - [self.username, - self.backend, - self.frontend] - - for col, val in self.config_data.iteritems(): - if val is not None: - cols += 1 - query_data.append(val) - query_string += ",\n" + col - - query_string += \ - (")\n" + - "VALUES (" + - "%s ," * cols) - - query_string = query_string[:-2] - query_string += ")" - - cursor.execute(query_string, query_data) - return True - - def update_user_config(self, cursor): - query_string = """UPDATE mysql_users""" - - cols = 0 - query_data = [] - - for col, val in self.config_data.iteritems(): - if val is not None: - cols += 1 - query_data.append(val) - if cols == 1: - query_string += "\nSET " + col + "= %s," - else: - query_string += "\n " + col + " = %s," - - query_string = query_string[:-1] - query_string += ("\nWHERE username = %s\n AND backend = %s" + - "\n AND frontend = %s") - - query_data.append(self.username) - query_data.append(self.backend) - query_data.append(self.frontend) - - cursor.execute(query_string, query_data) - return True - - def delete_user_config(self, cursor): - query_string = \ - """DELETE FROM mysql_users - WHERE username = %s - AND backend = %s - AND frontend = %s""" - - query_data = \ - [self.username, - self.backend, - self.frontend] - - cursor.execute(query_string, query_data) - return True - - def manage_config(self, cursor, state): - if state: - if self.save_to_disk: - save_config_to_disk(cursor) - if self.load_to_runtime: - load_config_to_runtime(cursor) - - def create_user(self, check_mode, result, cursor): - if not check_mode: - result['changed'] = \ - self.create_user_config(cursor) - result['msg'] = "Added user to mysql_users" - result['user'] = \ - self.get_user_config(cursor) - self.manage_config(cursor, - result['changed']) - else: - result['changed'] = True - result['msg'] = ("User would have been added to" + - " mysql_users, however check_mode" + - " is enabled.") - - def update_user(self, check_mode, result, cursor): - if not check_mode: - result['changed'] = \ - self.update_user_config(cursor) - result['msg'] = "Updated user in mysql_users" - result['user'] = \ - self.get_user_config(cursor) - self.manage_config(cursor, - result['changed']) - else: - result['changed'] = True - result['msg'] = ("User would have been updated in" + - " mysql_users, however check_mode" + - " is enabled.") - - def delete_user(self, check_mode, result, cursor): - if not check_mode: - result['user'] = \ - self.get_user_config(cursor) - result['changed'] = \ - self.delete_user_config(cursor) - result['msg'] = "Deleted user from mysql_users" - self.manage_config(cursor, - result['changed']) - else: - result['changed'] = True - result['msg'] = ("User would have been deleted from" + - " mysql_users, however check_mode is" + - " enabled.") - -# =========================================== -# Module execution. -# - - -def main(): - module = AnsibleModule( - argument_spec=dict( - login_user=dict(default=None, type='str'), - login_password=dict(default=None, no_log=True, type='str'), - login_host=dict(default="127.0.0.1"), - login_unix_socket=dict(default=None), - login_port=dict(default=6032, type='int'), - config_file=dict(default='', type='path'), - username=dict(required=True, type='str'), - password=dict(no_log=True, type='str'), - active=dict(type='bool'), - use_ssl=dict(type='bool'), - default_hostgroup=dict(type='int'), - default_schema=dict(type='str'), - transaction_persistent=dict(type='bool'), - fast_forward=dict(type='bool'), - backend=dict(default=True, type='bool'), - frontend=dict(default=True, type='bool'), - max_connections=dict(type='int'), - state=dict(default='present', choices=['present', - 'absent']), - save_to_disk=dict(default=True, type='bool'), - load_to_runtime=dict(default=True, type='bool') - ), - supports_check_mode=True - ) - - perform_checks(module) - - login_user = module.params["login_user"] - login_password = module.params["login_password"] - config_file = module.params["config_file"] - - cursor = None - try: - cursor = mysql_connect(module, - login_user, - login_password, - config_file, - cursor_class=MySQLdb.cursors.DictCursor) - except MySQLdb.Error: - e = get_exception() - module.fail_json( - msg="unable to connect to ProxySQL Admin Module.. %s" % e - ) - - proxysql_user = ProxySQLUser(module) - result = {} - - result['state'] = proxysql_user.state - if proxysql_user.username: - result['username'] = proxysql_user.username - - if proxysql_user.state == "present": - try: - if not proxysql_user.check_user_privs(cursor): - if not proxysql_user.check_user_config_exists(cursor): - proxysql_user.create_user(module.check_mode, - result, - cursor) - else: - proxysql_user.update_user(module.check_mode, - result, - cursor) - else: - result['changed'] = False - result['msg'] = ("The user already exists in mysql_users" + - " and doesn't need to be updated.") - result['user'] = \ - proxysql_user.get_user_config(cursor) - except MySQLdb.Error: - e = get_exception() - module.fail_json( - msg="unable to modify user.. %s" % e - ) - - elif proxysql_user.state == "absent": - try: - if proxysql_user.check_user_config_exists(cursor): - proxysql_user.delete_user(module.check_mode, - result, - cursor) - else: - result['changed'] = False - result['msg'] = ("The user is already absent from the" + - " mysql_users memory configuration") - except MySQLdb.Error: - e = get_exception() - module.fail_json( - msg="unable to remove user.. %s" % e - ) - - module.exit_json(**result) - -if __name__ == '__main__': - main() diff --git a/database/proxysql/proxysql_query_rules.py b/database/proxysql/proxysql_query_rules.py deleted file mode 100644 index 8af46c6ab9c..00000000000 --- a/database/proxysql/proxysql_query_rules.py +++ /dev/null @@ -1,659 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -DOCUMENTATION = ''' ---- -module: proxysql_query_rules -version_added: "2.3" -author: "Ben Mildren (@bmildren)" -short_description: Modifies query rules using the proxysql admin interface. -description: - - The M(proxysql_query_rules) module modifies query rules using the proxysql - admin interface. -options: - rule_id: - description: - - The unique id of the rule. Rules are processed in rule_id order. - [integer] - active: - description: - - A rule with I(active) set to C(False) will be tracked in the database, - but will be never loaded in the in-memory data structures [boolean] - username: - description: - - Filtering criteria matching username. If I(username) is non-NULL, a - query will match only if the connection is made with the correct - username. [string] - schemaname: - description: - - Filtering criteria matching schemaname. If I(schemaname) is - non-NULL, a query will match only if the connection uses schemaname as - its default schema. [string] - flagIN: - description: - - Used in combination with I(flagOUT) and I(apply) to create chains of - rules. [integer] - client_addr: - description: - - Match traffic from a specific source. [string] - proxy_addr: - description: - - Match incoming traffic on a specific local IP. [string] - proxy_port: - description: - - Match incoming traffic on a specific local port. [integer] - digest: - description: - - Match queries with a specific digest, as returned by - stats_mysql_query_digest.digest. [string] - match_digest: - description: - - Regular expression that matches the query digest. The dialect of - regular expressions used is that of re2 - - https://github.com/google/re2. [string] - match_pattern: - description: - - Regular expression that matches the query text. The dialect of regular - expressions used is that of re2 - https://github.com/google/re2. - [string] - negate_match_pattern: - description: - - If I(negate_match_pattern) is set to C(True), only queries not matching - the query text will be considered as a match. This acts as a NOT - operator in front of the regular expression matching against - match_pattern. [boolean] - flagOUT: - description: - - Used in combination with I(flagIN) and apply to create chains of rules. - When set, I(flagOUT) signifies the I(flagIN) to be used in the next - chain of rules. [integer] - replace_pattern: - description: - - This is the pattern with which to replace the matched pattern. Note - that this is optional, and when ommitted, the query processor will only - cache, route, or set other parameters without rewriting. [string] - destination_hostgroup: - description: - - Route matched queries to this hostgroup. This happens unless there is - a started transaction and the logged in user has - I(transaction_persistent) set to C(True) (see M(proxysql_mysql_users)). - [integer] - cache_ttl: - description: - - The number of milliseconds for which to cache the result of the query. - Note in ProxySQL 1.1 I(cache_ttl) was in seconds. [integer] - timeout: - description: - - The maximum timeout in milliseconds with which the matched or rewritten - query should be executed. If a query run for longer than the specific - threshold, the query is automatically killed. If timeout is not - specified, the global variable mysql-default_query_timeout applies. - [integer] - retries: - description: - - The maximum number of times a query needs to be re-executed in case of - detected failure during the execution of the query. If retries is not - specified, the global variable mysql-query_retries_on_failure applies. - [integer] - delay: - description: - - Number of milliseconds to delay the execution of the query. This is - essentially a throttling mechanism and QoS, and allows a way to give - priority to queries over others. This value is added to the - mysql-default_query_delay global variable that applies to all queries. - [integer] - mirror_flagOUT: - description: - - Enables query mirroring. If set I(mirror_flagOUT) can be used to - evaluates the mirrored query against the specified chain of rules. - [integer] - mirror_hostgroup: - description: - - Enables query mirroring. If set I(mirror_hostgroup) can be used to - mirror queries to the same or different hostgroup. [integer] - error_msg: - description: - - Query will be blocked, and the specified error_msg will be returned to - the client. [string] - log: - description: - - Query will be logged. [boolean] - apply: - description: - - Used in combination with I(flagIN) and I(flagOUT) to create chains of - rules. Setting apply to True signifies the last rule to be applied. - [boolean] - comment: - description: - - Free form text field, usable for a descriptive comment of the query - rule. [string] - state: - description: - - When C(present) - adds the rule, when C(absent) - removes the rule. - choices: [ "present", "absent" ] - default: present - force_delete: - description: - - By default we avoid deleting more than one schedule in a single batch, - however if you need this behaviour and you're not concerned about the - schedules deleted, you can set I(force_delete) to C(True). - default: False - save_to_disk: - description: - - Save mysql host config to sqlite db on disk to persist the - configuration. - default: True - load_to_runtime: - description: - - Dynamically load mysql host config to runtime memory. - default: True - login_user: - description: - - The username used to authenticate to ProxySQL admin interface - default: None - login_password: - description: - - The password used to authenticate to ProxySQL admin interface - default: None - login_host: - description: - - The host used to connect to ProxySQL admin interface - default: '127.0.0.1' - login_port: - description: - - The port used to connect to ProxySQL admin interface - default: 6032 - config_file: - description: - - Specify a config file from which login_user and login_password are to - be read - default: '' -''' - -EXAMPLES = ''' ---- -# This example adds a rule to redirect queries from a specific user to another -# hostgroup, it saves the mysql query rule config to disk, but avoids loading -# the mysql query config config to runtime (this might be because several -# rules are being added and the user wants to push the config to runtime in a -# single batch using the M(proxysql_manage_config) module). It uses supplied -# credentials to connect to the proxysql admin interface. - -- proxysql_backend_servers: - login_user: admin - login_password: admin - username: 'guest_ro' - destination_hostgroup: 1 - active: 1 - retries: 3 - state: present - load_to_runtime: False - -# This example removes all rules that use the username 'guest_ro', saves the -# mysql query rule config to disk, and dynamically loads the mysql query rule -# config to runtime. It uses credentials in a supplied config file to connect -# to the proxysql admin interface. - -- proxysql_backend_servers: - config_file: '~/proxysql.cnf' - username: 'guest_ro' - state: absent - force_delete: true -''' - -RETURN = ''' -stdout: - description: The mysql user modified or removed from proxysql - returned: On create/update will return the newly modified rule, in all - other cases will return a list of rules that match the supplied - criteria. - type: dict - "sample": { - "changed": true, - "msg": "Added rule to mysql_query_rules", - "rules": [ - { - "active": "0", - "apply": "0", - "cache_ttl": null, - "client_addr": null, - "comment": null, - "delay": null, - "destination_hostgroup": 1, - "digest": null, - "error_msg": null, - "flagIN": "0", - "flagOUT": null, - "log": null, - "match_digest": null, - "match_pattern": null, - "mirror_flagOUT": null, - "mirror_hostgroup": null, - "negate_match_pattern": "0", - "proxy_addr": null, - "proxy_port": null, - "reconnect": null, - "replace_pattern": null, - "retries": null, - "rule_id": "1", - "schemaname": null, - "timeout": null, - "username": "guest_ro" - } - ], - "state": "present" - } -''' - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.mysql import mysql_connect -from ansible.module_utils.pycompat24 import get_exception - -try: - import MySQLdb - import MySQLdb.cursors -except ImportError: - MYSQLDB_FOUND = False -else: - MYSQLDB_FOUND = True - -# =========================================== -# proxysql module specific support methods. -# - - -def perform_checks(module): - if module.params["login_port"] < 0 \ - or module.params["login_port"] > 65535: - module.fail_json( - msg="login_port must be a valid unix port number (0-65535)" - ) - - if not MYSQLDB_FOUND: - module.fail_json( - msg="the python mysqldb module is required" - ) - - -def save_config_to_disk(cursor): - cursor.execute("SAVE MYSQL QUERY RULES TO DISK") - return True - - -def load_config_to_runtime(cursor): - cursor.execute("LOAD MYSQL QUERY RULES TO RUNTIME") - return True - - -class ProxyQueryRule(object): - - def __init__(self, module): - self.state = module.params["state"] - self.force_delete = module.params["force_delete"] - self.save_to_disk = module.params["save_to_disk"] - self.load_to_runtime = module.params["load_to_runtime"] - - config_data_keys = ["rule_id", - "active", - "username", - "schemaname", - "flagIN", - "client_addr", - "proxy_addr", - "proxy_port", - "digest", - "match_digest", - "match_pattern", - "negate_match_pattern", - "flagOUT", - "replace_pattern", - "destination_hostgroup", - "cache_ttl", - "timeout", - "retries", - "delay", - "mirror_flagOUT", - "mirror_hostgroup", - "error_msg", - "log", - "apply", - "comment"] - - self.config_data = dict((k, module.params[k]) - for k in config_data_keys) - - def check_rule_pk_exists(self, cursor): - query_string = \ - """SELECT count(*) AS `rule_count` - FROM mysql_query_rules - WHERE rule_id = %s""" - - query_data = \ - [self.config_data["rule_id"]] - - cursor.execute(query_string, query_data) - check_count = cursor.fetchone() - return (int(check_count['rule_count']) > 0) - - def check_rule_cfg_exists(self, cursor): - query_string = \ - """SELECT count(*) AS `rule_count` - FROM mysql_query_rules""" - - cols = 0 - query_data = [] - - for col, val in self.config_data.iteritems(): - if val is not None: - cols += 1 - query_data.append(val) - if cols == 1: - query_string += "\n WHERE " + col + " = %s" - else: - query_string += "\n AND " + col + " = %s" - - if cols > 0: - cursor.execute(query_string, query_data) - else: - cursor.execute(query_string) - check_count = cursor.fetchone() - return int(check_count['rule_count']) - - def get_rule_config(self, cursor, created_rule_id=None): - query_string = \ - """SELECT * - FROM mysql_query_rules""" - - if created_rule_id: - query_data = [created_rule_id, ] - query_string += "\nWHERE rule_id = %s" - - cursor.execute(query_string, query_data) - rule = cursor.fetchone() - else: - cols = 0 - query_data = [] - - for col, val in self.config_data.iteritems(): - if val is not None: - cols += 1 - query_data.append(val) - if cols == 1: - query_string += "\n WHERE " + col + " = %s" - else: - query_string += "\n AND " + col + " = %s" - - if cols > 0: - cursor.execute(query_string, query_data) - else: - cursor.execute(query_string) - rule = cursor.fetchall() - - return rule - - def create_rule_config(self, cursor): - query_string = \ - """INSERT INTO mysql_query_rules (""" - - cols = 0 - query_data = [] - - for col, val in self.config_data.iteritems(): - if val is not None: - cols += 1 - query_data.append(val) - query_string += "\n" + col + "," - - query_string = query_string[:-1] - - query_string += \ - (")\n" + - "VALUES (" + - "%s ," * cols) - - query_string = query_string[:-2] - query_string += ")" - - cursor.execute(query_string, query_data) - new_rule_id = cursor.lastrowid - return True, new_rule_id - - def update_rule_config(self, cursor): - query_string = """UPDATE mysql_query_rules""" - - cols = 0 - query_data = [] - - for col, val in self.config_data.iteritems(): - if val is not None and col != "rule_id": - cols += 1 - query_data.append(val) - if cols == 1: - query_string += "\nSET " + col + "= %s," - else: - query_string += "\n " + col + " = %s," - - query_string = query_string[:-1] - query_string += "\nWHERE rule_id = %s" - - query_data.append(self.config_data["rule_id"]) - - cursor.execute(query_string, query_data) - return True - - def delete_rule_config(self, cursor): - query_string = \ - """DELETE FROM mysql_query_rules""" - - cols = 0 - query_data = [] - - for col, val in self.config_data.iteritems(): - if val is not None: - cols += 1 - query_data.append(val) - if cols == 1: - query_string += "\n WHERE " + col + " = %s" - else: - query_string += "\n AND " + col + " = %s" - - if cols > 0: - cursor.execute(query_string, query_data) - else: - cursor.execute(query_string) - check_count = cursor.rowcount - return True, int(check_count) - - def manage_config(self, cursor, state): - if state: - if self.save_to_disk: - save_config_to_disk(cursor) - if self.load_to_runtime: - load_config_to_runtime(cursor) - - def create_rule(self, check_mode, result, cursor): - if not check_mode: - result['changed'], new_rule_id = \ - self.create_rule_config(cursor) - result['msg'] = "Added rule to mysql_query_rules" - self.manage_config(cursor, - result['changed']) - result['rules'] = \ - self.get_rule_config(cursor, new_rule_id) - else: - result['changed'] = True - result['msg'] = ("Rule would have been added to" + - " mysql_query_rules, however" + - " check_mode is enabled.") - - def update_rule(self, check_mode, result, cursor): - if not check_mode: - result['changed'] = \ - self.update_rule_config(cursor) - result['msg'] = "Updated rule in mysql_query_rules" - self.manage_config(cursor, - result['changed']) - result['rules'] = \ - self.get_rule_config(cursor) - else: - result['changed'] = True - result['msg'] = ("Rule would have been updated in" + - " mysql_query_rules, however" + - " check_mode is enabled.") - - def delete_rule(self, check_mode, result, cursor): - if not check_mode: - result['rules'] = \ - self.get_rule_config(cursor) - result['changed'], result['rows_affected'] = \ - self.delete_rule_config(cursor) - result['msg'] = "Deleted rule from mysql_query_rules" - self.manage_config(cursor, - result['changed']) - else: - result['changed'] = True - result['msg'] = ("Rule would have been deleted from" + - " mysql_query_rules, however" + - " check_mode is enabled.") - -# =========================================== -# Module execution. -# - - -def main(): - module = AnsibleModule( - argument_spec=dict( - login_user=dict(default=None, type='str'), - login_password=dict(default=None, no_log=True, type='str'), - login_host=dict(default="127.0.0.1"), - login_unix_socket=dict(default=None), - login_port=dict(default=6032, type='int'), - config_file=dict(default="", type='path'), - rule_id=dict(type='int'), - active=dict(type='bool'), - username=dict(type='str'), - schemaname=dict(type='str'), - flagIN=dict(type='int'), - client_addr=dict(type='str'), - proxy_addr=dict(type='str'), - proxy_port=dict(type='int'), - digest=dict(type='str'), - match_digest=dict(type='str'), - match_pattern=dict(type='str'), - negate_match_pattern=dict(type='bool'), - flagOUT=dict(type='int'), - replace_pattern=dict(type='str'), - destination_hostgroup=dict(type='int'), - cache_ttl=dict(type='int'), - timeout=dict(type='int'), - retries=dict(type='int'), - delay=dict(type='int'), - mirror_flagOUT=dict(type='int'), - mirror_hostgroup=dict(type='int'), - error_msg=dict(type='str'), - log=dict(type='bool'), - apply=dict(type='bool'), - comment=dict(type='str'), - state=dict(default='present', choices=['present', - 'absent']), - force_delete=dict(default=False, type='bool'), - save_to_disk=dict(default=True, type='bool'), - load_to_runtime=dict(default=True, type='bool') - ), - supports_check_mode=True - ) - - perform_checks(module) - - login_user = module.params["login_user"] - login_password = module.params["login_password"] - config_file = module.params["config_file"] - - cursor = None - try: - cursor = mysql_connect(module, - login_user, - login_password, - config_file, - cursor_class=MySQLdb.cursors.DictCursor) - except MySQLdb.Error: - e = get_exception() - module.fail_json( - msg="unable to connect to ProxySQL Admin Module.. %s" % e - ) - - proxysql_query_rule = ProxyQueryRule(module) - result = {} - - result['state'] = proxysql_query_rule.state - - if proxysql_query_rule.state == "present": - try: - if not proxysql_query_rule.check_rule_cfg_exists(cursor): - if proxysql_query_rule.config_data["rule_id"] and \ - proxysql_query_rule.check_rule_pk_exists(cursor): - proxysql_query_rule.update_rule(module.check_mode, - result, - cursor) - else: - proxysql_query_rule.create_rule(module.check_mode, - result, - cursor) - else: - result['changed'] = False - result['msg'] = ("The rule already exists in" + - " mysql_query_rules and doesn't need to be" + - " updated.") - result['rules'] = \ - proxysql_query_rule.get_rule_config(cursor) - - except MySQLdb.Error: - e = get_exception() - module.fail_json( - msg="unable to modify rule.. %s" % e - ) - - elif proxysql_query_rule.state == "absent": - try: - existing_rules = proxysql_query_rule.check_rule_cfg_exists(cursor) - if existing_rules > 0: - if existing_rules == 1 or \ - proxysql_query_rule.force_delete: - proxysql_query_rule.delete_rule(module.check_mode, - result, - cursor) - else: - module.fail_json( - msg=("Operation would delete multiple rules" + - " use force_delete to override this") - ) - else: - result['changed'] = False - result['msg'] = ("The rule is already absent from the" + - " mysql_query_rules memory configuration") - except MySQLdb.Error: - e = get_exception() - module.fail_json( - msg="unable to remove rule.. %s" % e - ) - - module.exit_json(**result) - -if __name__ == '__main__': - main() diff --git a/database/proxysql/proxysql_replication_hostgroups.py b/database/proxysql/proxysql_replication_hostgroups.py deleted file mode 100644 index 41ade82734c..00000000000 --- a/database/proxysql/proxysql_replication_hostgroups.py +++ /dev/null @@ -1,424 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -DOCUMENTATION = ''' ---- -module: proxysql_replication_hostgroups -version_added: "2.3" -author: "Ben Mildren (@bmildren)" -short_description: Manages replication hostgroups using the proxysql admin - interface. -description: - - Each row in mysql_replication_hostgroups represent a pair of - writer_hostgroup and reader_hostgroup . - ProxySQL will monitor the value of read_only for all the servers in - specified hostgroups, and based on the value of read_only will assign the - server to the writer or reader hostgroups. -options: - writer_hostgroup: - description: - - Id of the writer hostgroup. - required: True - reader_hostgroup: - description: - - Id of the reader hostgroup. - required: True - comment: - description: - - Text field that can be used for any purposed defined by the user. - state: - description: - - When C(present) - adds the replication hostgroup, when C(absent) - - removes the replication hostgroup. - choices: [ "present", "absent" ] - default: present - save_to_disk: - description: - - Save mysql host config to sqlite db on disk to persist the - configuration. - default: True - load_to_runtime: - description: - - Dynamically load mysql host config to runtime memory. - default: True - login_user: - description: - - The username used to authenticate to ProxySQL admin interface - default: None - login_password: - description: - - The password used to authenticate to ProxySQL admin interface - default: None - login_host: - description: - - The host used to connect to ProxySQL admin interface - default: '127.0.0.1' - login_port: - description: - - The port used to connect to ProxySQL admin interface - default: 6032 - config_file: - description: - - Specify a config file from which login_user and login_password are to - be read - default: '' -''' - -EXAMPLES = ''' ---- -# This example adds a replication hostgroup, it saves the mysql server config -# to disk, but avoids loading the mysql server config to runtime (this might be -# because several replication hostgroup are being added and the user wants to -# push the config to runtime in a single batch using the -# M(proxysql_manage_config) module). It uses supplied credentials to connect -# to the proxysql admin interface. - -- proxysql_replication_hostgroups: - login_user: 'admin' - login_password: 'admin' - writer_hostgroup: 1 - reader_hostgroup: 2 - state: present - load_to_runtime: False - -# This example removes a replication hostgroup, saves the mysql server config -# to disk, and dynamically loads the mysql server config to runtime. It uses -# credentials in a supplied config file to connect to the proxysql admin -# interface. - -- proxysql_replication_hostgroups: - config_file: '~/proxysql.cnf' - writer_hostgroup: 3 - reader_hostgroup: 4 - state: absent -''' - -RETURN = ''' -stdout: - description: The replication hostgroup modified or removed from proxysql - returned: On create/update will return the newly modified group, on delete - it will return the deleted record. - type: dict - "sample": { - "changed": true, - "msg": "Added server to mysql_hosts", - "repl_group": { - "comment": "", - "reader_hostgroup": "1", - "writer_hostgroup": "2" - }, - "state": "present" - } -''' - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.mysql import mysql_connect -from ansible.module_utils.pycompat24 import get_exception - -try: - import MySQLdb - import MySQLdb.cursors -except ImportError: - MYSQLDB_FOUND = False -else: - MYSQLDB_FOUND = True - -# =========================================== -# proxysql module specific support methods. -# - - -def perform_checks(module): - if module.params["login_port"] < 0 \ - or module.params["login_port"] > 65535: - module.fail_json( - msg="login_port must be a valid unix port number (0-65535)" - ) - - if not module.params["writer_hostgroup"] >= 0: - module.fail_json( - msg="writer_hostgroup must be a integer greater than or equal to 0" - ) - - if not module.params["reader_hostgroup"] == \ - module.params["writer_hostgroup"]: - if not module.params["reader_hostgroup"] > 0: - module.fail_json( - msg=("writer_hostgroup must be a integer greater than" + - " or equal to 0") - ) - else: - module.fail_json( - msg="reader_hostgroup cannot equal writer_hostgroup" - ) - - if not MYSQLDB_FOUND: - module.fail_json( - msg="the python mysqldb module is required" - ) - - -def save_config_to_disk(cursor): - cursor.execute("SAVE MYSQL SERVERS TO DISK") - return True - - -def load_config_to_runtime(cursor): - cursor.execute("LOAD MYSQL SERVERS TO RUNTIME") - return True - - -class ProxySQLReplicationHostgroup(object): - - def __init__(self, module): - self.state = module.params["state"] - self.save_to_disk = module.params["save_to_disk"] - self.load_to_runtime = module.params["load_to_runtime"] - self.writer_hostgroup = module.params["writer_hostgroup"] - self.reader_hostgroup = module.params["reader_hostgroup"] - self.comment = module.params["comment"] - - def check_repl_group_config(self, cursor, keys): - query_string = \ - """SELECT count(*) AS `repl_groups` - FROM mysql_replication_hostgroups - WHERE writer_hostgroup = %s - AND reader_hostgroup = %s""" - - query_data = \ - [self.writer_hostgroup, - self.reader_hostgroup] - - if self.comment and not keys: - query_string += "\n AND comment = %s" - query_data.append(self.comment) - - cursor.execute(query_string, query_data) - check_count = cursor.fetchone() - return (int(check_count['repl_groups']) > 0) - - def get_repl_group_config(self, cursor): - query_string = \ - """SELECT * - FROM mysql_replication_hostgroups - WHERE writer_hostgroup = %s - AND reader_hostgroup = %s""" - - query_data = \ - [self.writer_hostgroup, - self.reader_hostgroup] - - cursor.execute(query_string, query_data) - repl_group = cursor.fetchone() - return repl_group - - def create_repl_group_config(self, cursor): - query_string = \ - """INSERT INTO mysql_replication_hostgroups ( - writer_hostgroup, - reader_hostgroup, - comment) - VALUES (%s, %s, %s)""" - - query_data = \ - [self.writer_hostgroup, - self.reader_hostgroup, - self.comment or ''] - - cursor.execute(query_string, query_data) - return True - - def update_repl_group_config(self, cursor): - query_string = \ - """UPDATE mysql_replication_hostgroups - SET comment = %s - WHERE writer_hostgroup = %s - AND reader_hostgroup = %s""" - - query_data = \ - [self.comment, - self.writer_hostgroup, - self.reader_hostgroup] - - cursor.execute(query_string, query_data) - return True - - def delete_repl_group_config(self, cursor): - query_string = \ - """DELETE FROM mysql_replication_hostgroups - WHERE writer_hostgroup = %s - AND reader_hostgroup = %s""" - - query_data = \ - [self.writer_hostgroup, - self.reader_hostgroup] - - cursor.execute(query_string, query_data) - return True - - def manage_config(self, cursor, state): - if state: - if self.save_to_disk: - save_config_to_disk(cursor) - if self.load_to_runtime: - load_config_to_runtime(cursor) - - def create_repl_group(self, check_mode, result, cursor): - if not check_mode: - result['changed'] = \ - self.create_repl_group_config(cursor) - result['msg'] = "Added server to mysql_hosts" - result['repl_group'] = \ - self.get_repl_group_config(cursor) - self.manage_config(cursor, - result['changed']) - else: - result['changed'] = True - result['msg'] = ("Repl group would have been added to" + - " mysql_replication_hostgroups, however" + - " check_mode is enabled.") - - def update_repl_group(self, check_mode, result, cursor): - if not check_mode: - result['changed'] = \ - self.update_repl_group_config(cursor) - result['msg'] = "Updated server in mysql_hosts" - result['repl_group'] = \ - self.get_repl_group_config(cursor) - self.manage_config(cursor, - result['changed']) - else: - result['changed'] = True - result['msg'] = ("Repl group would have been updated in" + - " mysql_replication_hostgroups, however" + - " check_mode is enabled.") - - def delete_repl_group(self, check_mode, result, cursor): - if not check_mode: - result['repl_group'] = \ - self.get_repl_group_config(cursor) - result['changed'] = \ - self.delete_repl_group_config(cursor) - result['msg'] = "Deleted server from mysql_hosts" - self.manage_config(cursor, - result['changed']) - else: - result['changed'] = True - result['msg'] = ("Repl group would have been deleted from" + - " mysql_replication_hostgroups, however" + - " check_mode is enabled.") - -# =========================================== -# Module execution. -# - - -def main(): - module = AnsibleModule( - argument_spec=dict( - login_user=dict(default=None, type='str'), - login_password=dict(default=None, no_log=True, type='str'), - login_host=dict(default="127.0.0.1"), - login_unix_socket=dict(default=None), - login_port=dict(default=6032, type='int'), - config_file=dict(default="", type='path'), - writer_hostgroup=dict(required=True, type='int'), - reader_hostgroup=dict(required=True, type='int'), - comment=dict(type='str'), - state=dict(default='present', choices=['present', - 'absent']), - save_to_disk=dict(default=True, type='bool'), - load_to_runtime=dict(default=True, type='bool') - ), - supports_check_mode=True - ) - - perform_checks(module) - - login_user = module.params["login_user"] - login_password = module.params["login_password"] - config_file = module.params["config_file"] - - cursor = None - try: - cursor = mysql_connect(module, - login_user, - login_password, - config_file, - cursor_class=MySQLdb.cursors.DictCursor) - except MySQLdb.Error: - e = get_exception() - module.fail_json( - msg="unable to connect to ProxySQL Admin Module.. %s" % e - ) - - proxysql_repl_group = ProxySQLReplicationHostgroup(module) - result = {} - - result['state'] = proxysql_repl_group.state - - if proxysql_repl_group.state == "present": - try: - if not proxysql_repl_group.check_repl_group_config(cursor, - keys=True): - proxysql_repl_group.create_repl_group(module.check_mode, - result, - cursor) - else: - if not proxysql_repl_group.check_repl_group_config(cursor, - keys=False): - proxysql_repl_group.update_repl_group(module.check_mode, - result, - cursor) - else: - result['changed'] = False - result['msg'] = ("The repl group already exists in" + - " mysql_replication_hostgroups and" + - " doesn't need to be updated.") - result['repl_group'] = \ - proxysql_repl_group.get_repl_group_config(cursor) - - except MySQLdb.Error: - e = get_exception() - module.fail_json( - msg="unable to modify replication hostgroup.. %s" % e - ) - - elif proxysql_repl_group.state == "absent": - try: - if proxysql_repl_group.check_repl_group_config(cursor, - keys=True): - proxysql_repl_group.delete_repl_group(module.check_mode, - result, - cursor) - else: - result['changed'] = False - result['msg'] = ("The repl group is already absent from the" + - " mysql_replication_hostgroups memory" + - " configuration") - - except MySQLdb.Error: - e = get_exception() - module.fail_json( - msg="unable to delete replication hostgroup.. %s" % e - ) - - module.exit_json(**result) - -if __name__ == '__main__': - main() diff --git a/database/proxysql/proxysql_scheduler.py b/database/proxysql/proxysql_scheduler.py deleted file mode 100644 index 790dfcaee6e..00000000000 --- a/database/proxysql/proxysql_scheduler.py +++ /dev/null @@ -1,458 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -DOCUMENTATION = ''' ---- -module: proxysql_scheduler -version_added: "2.3" -author: "Ben Mildren (@bmildren)" -short_description: Adds or removes schedules from proxysql admin interface. -description: - - The M(proxysql_scheduler) module adds or removes schedules using the - proxysql admin interface. -options: - active: - description: - - A schedule with I(active) set to C(False) will be tracked in the - database, but will be never loaded in the in-memory data structures - default: True - interval_ms: - description: - - How often (in millisecond) the job will be started. The minimum value - for I(interval_ms) is 100 milliseconds - default: 10000 - filename: - description: - - Full path of the executable to be executed. - required: True - arg1: - description: - - Argument that can be passed to the job. - arg2: - description: - - Argument that can be passed to the job. - arg3: - description: - - Argument that can be passed to the job. - arg4: - description: - - Argument that can be passed to the job. - arg5: - description: - - Argument that can be passed to the job. - comment: - description: - - Text field that can be used for any purposed defined by the user. - state: - description: - - When C(present) - adds the schedule, when C(absent) - removes the - schedule. - choices: [ "present", "absent" ] - default: present - force_delete: - description: - - By default we avoid deleting more than one schedule in a single batch, - however if you need this behaviour and you're not concerned about the - schedules deleted, you can set I(force_delete) to C(True). - default: False - save_to_disk: - description: - - Save mysql host config to sqlite db on disk to persist the - configuration. - default: True - load_to_runtime: - description: - - Dynamically load mysql host config to runtime memory. - default: True - login_user: - description: - - The username used to authenticate to ProxySQL admin interface - default: None - login_password: - description: - - The password used to authenticate to ProxySQL admin interface - default: None - login_host: - description: - - The host used to connect to ProxySQL admin interface - default: '127.0.0.1' - login_port: - description: - - The port used to connect to ProxySQL admin interface - default: 6032 - config_file: - description: - - Specify a config file from which login_user and login_password are to - be read - default: '' -''' - -EXAMPLES = ''' ---- -# This example adds a schedule, it saves the scheduler config to disk, but -# avoids loading the scheduler config to runtime (this might be because -# several servers are being added and the user wants to push the config to -# runtime in a single batch using the M(proxysql_manage_config) module). It -# uses supplied credentials to connect to the proxysql admin interface. - -- proxysql_scheduler: - login_user: 'admin' - login_password: 'admin' - interval_ms: 1000 - filename: "/opt/maintenance.py" - state: present - load_to_runtime: False - -# This example removes a schedule, saves the scheduler config to disk, and -# dynamically loads the scheduler config to runtime. It uses credentials -# in a supplied config file to connect to the proxysql admin interface. - -- proxysql_scheduler: - config_file: '~/proxysql.cnf' - filename: "/opt/old_script.py" - state: absent -''' - -RETURN = ''' -stdout: - description: The schedule modified or removed from proxysql - returned: On create/update will return the newly modified schedule, on - delete it will return the deleted record. - type: dict - "sample": { - "changed": true, - "filename": "/opt/test.py", - "msg": "Added schedule to scheduler", - "schedules": [ - { - "active": "1", - "arg1": null, - "arg2": null, - "arg3": null, - "arg4": null, - "arg5": null, - "comment": "", - "filename": "/opt/test.py", - "id": "1", - "interval_ms": "10000" - } - ], - "state": "present" - } -''' - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.mysql import mysql_connect -from ansible.module_utils.pycompat24 import get_exception - -try: - import MySQLdb - import MySQLdb.cursors -except ImportError: - MYSQLDB_FOUND = False -else: - MYSQLDB_FOUND = True - -# =========================================== -# proxysql module specific support methods. -# - - -def perform_checks(module): - if module.params["login_port"] < 0 \ - or module.params["login_port"] > 65535: - module.fail_json( - msg="login_port must be a valid unix port number (0-65535)" - ) - - if module.params["interval_ms"] < 100 \ - or module.params["interval_ms"] > 100000000: - module.fail_json( - msg="interval_ms must between 100ms & 100000000ms" - ) - - if not MYSQLDB_FOUND: - module.fail_json( - msg="the python mysqldb module is required" - ) - - -def save_config_to_disk(cursor): - cursor.execute("SAVE SCHEDULER TO DISK") - return True - - -def load_config_to_runtime(cursor): - cursor.execute("LOAD SCHEDULER TO RUNTIME") - return True - - -class ProxySQLSchedule(object): - - def __init__(self, module): - self.state = module.params["state"] - self.force_delete = module.params["force_delete"] - self.save_to_disk = module.params["save_to_disk"] - self.load_to_runtime = module.params["load_to_runtime"] - self.active = module.params["active"] - self.interval_ms = module.params["interval_ms"] - self.filename = module.params["filename"] - - config_data_keys = ["arg1", - "arg2", - "arg3", - "arg4", - "arg5", - "comment"] - - self.config_data = dict((k, module.params[k]) - for k in config_data_keys) - - def check_schedule_config(self, cursor): - query_string = \ - """SELECT count(*) AS `schedule_count` - FROM scheduler - WHERE active = %s - AND interval_ms = %s - AND filename = %s""" - - query_data = \ - [self.active, - self.interval_ms, - self.filename] - - for col, val in self.config_data.iteritems(): - if val is not None: - query_data.append(val) - query_string += "\n AND " + col + " = %s" - - cursor.execute(query_string, query_data) - check_count = cursor.fetchone() - return int(check_count['schedule_count']) - - def get_schedule_config(self, cursor): - query_string = \ - """SELECT * - FROM scheduler - WHERE active = %s - AND interval_ms = %s - AND filename = %s""" - - query_data = \ - [self.active, - self.interval_ms, - self.filename] - - for col, val in self.config_data.iteritems(): - if val is not None: - query_data.append(val) - query_string += "\n AND " + col + " = %s" - - cursor.execute(query_string, query_data) - schedule = cursor.fetchall() - return schedule - - def create_schedule_config(self, cursor): - query_string = \ - """INSERT INTO scheduler ( - active, - interval_ms, - filename""" - - cols = 0 - query_data = \ - [self.active, - self.interval_ms, - self.filename] - - for col, val in self.config_data.iteritems(): - if val is not None: - cols += 1 - query_data.append(val) - query_string += ",\n" + col - - query_string += \ - (")\n" + - "VALUES (%s, %s, %s" + - ", %s" * cols + - ")") - - cursor.execute(query_string, query_data) - return True - - def delete_schedule_config(self, cursor): - query_string = \ - """DELETE FROM scheduler - WHERE active = %s - AND interval_ms = %s - AND filename = %s""" - - query_data = \ - [self.active, - self.interval_ms, - self.filename] - - for col, val in self.config_data.iteritems(): - if val is not None: - query_data.append(val) - query_string += "\n AND " + col + " = %s" - - cursor.execute(query_string, query_data) - check_count = cursor.rowcount - return True, int(check_count) - - def manage_config(self, cursor, state): - if state: - if self.save_to_disk: - save_config_to_disk(cursor) - if self.load_to_runtime: - load_config_to_runtime(cursor) - - def create_schedule(self, check_mode, result, cursor): - if not check_mode: - result['changed'] = \ - self.create_schedule_config(cursor) - result['msg'] = "Added schedule to scheduler" - result['schedules'] = \ - self.get_schedule_config(cursor) - self.manage_config(cursor, - result['changed']) - else: - result['changed'] = True - result['msg'] = ("Schedule would have been added to" + - " scheduler, however check_mode" + - " is enabled.") - - def delete_schedule(self, check_mode, result, cursor): - if not check_mode: - result['schedules'] = \ - self.get_schedule_config(cursor) - result['changed'] = \ - self.delete_schedule_config(cursor) - result['msg'] = "Deleted schedule from scheduler" - self.manage_config(cursor, - result['changed']) - else: - result['changed'] = True - result['msg'] = ("Schedule would have been deleted from" + - " scheduler, however check_mode is" + - " enabled.") - -# =========================================== -# Module execution. -# - - -def main(): - module = AnsibleModule( - argument_spec=dict( - login_user=dict(default=None, type='str'), - login_password=dict(default=None, no_log=True, type='str'), - login_host=dict(default="127.0.0.1"), - login_unix_socket=dict(default=None), - login_port=dict(default=6032, type='int'), - config_file=dict(default="", type='path'), - active=dict(default=True, type='bool'), - interval_ms=dict(default=10000, type='int'), - filename=dict(required=True, type='str'), - arg1=dict(type='str'), - arg2=dict(type='str'), - arg3=dict(type='str'), - arg4=dict(type='str'), - arg5=dict(type='str'), - comment=dict(type='str'), - state=dict(default='present', choices=['present', - 'absent']), - force_delete=dict(default=False, type='bool'), - save_to_disk=dict(default=True, type='bool'), - load_to_runtime=dict(default=True, type='bool') - ), - supports_check_mode=True - ) - - perform_checks(module) - - login_user = module.params["login_user"] - login_password = module.params["login_password"] - config_file = module.params["config_file"] - - cursor = None - try: - cursor = mysql_connect(module, - login_user, - login_password, - config_file, - cursor_class=MySQLdb.cursors.DictCursor) - except MySQLdb.Error: - e = get_exception() - module.fail_json( - msg="unable to connect to ProxySQL Admin Module.. %s" % e - ) - - proxysql_schedule = ProxySQLSchedule(module) - result = {} - - result['state'] = proxysql_schedule.state - result['filename'] = proxysql_schedule.filename - - if proxysql_schedule.state == "present": - try: - if not proxysql_schedule.check_schedule_config(cursor) > 0: - proxysql_schedule.create_schedule(module.check_mode, - result, - cursor) - else: - result['changed'] = False - result['msg'] = ("The schedule already exists and doesn't" + - " need to be updated.") - result['schedules'] = \ - proxysql_schedule.get_schedule_config(cursor) - except MySQLdb.Error: - e = get_exception() - module.fail_json( - msg="unable to modify schedule.. %s" % e - ) - - elif proxysql_schedule.state == "absent": - try: - existing_schedules = \ - proxysql_schedule.check_schedule_config(cursor) - if existing_schedules > 0: - if existing_schedules == 1 or proxysql_schedule.force_delete: - proxysql_schedule.delete_schedule(module.check_mode, - result, - cursor) - else: - module.fail_json( - msg=("Operation would delete multiple records" + - " use force_delete to override this") - ) - else: - result['changed'] = False - result['msg'] = ("The schedule is already absent from the" + - " memory configuration") - except MySQLdb.Error: - e = get_exception() - module.fail_json( - msg="unable to remove schedule.. %s" % e - ) - - module.exit_json(**result) - -if __name__ == '__main__': - main() From 16490af367110ae1cffc730b8fb0ccd23a172ecb Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 2 Nov 2016 19:32:02 -0700 Subject: [PATCH 2307/2522] Fix python3 syntax for octal numbers --- crypto/openssl_privatekey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto/openssl_privatekey.py b/crypto/openssl_privatekey.py index 5055a2826e6..f7da9d11053 100644 --- a/crypto/openssl_privatekey.py +++ b/crypto/openssl_privatekey.py @@ -210,7 +210,7 @@ def main(): module.fail_json(name=base_dir, msg='The directory %s does not exist or the file is not a directory' % base_dir) if not module.params['mode']: - module.params['mode'] = 0600 + module.params['mode'] = int('0600', 8) private_key = PrivateKey(module) if private_key.state == 'present': From e4bc6189563a52e64a0ff60c05fc4221a393324b Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Wed, 2 Nov 2016 21:44:08 -0700 Subject: [PATCH 2308/2522] Simplify compileall checks. Remove py3 skip list. Also applied updates to sanity.sh from ansible-modules-core. --- test/utils/shippable/sanity-skip-python3.txt | 0 test/utils/shippable/sanity-test-python24.txt | 0 test/utils/shippable/sanity.sh | 9 ++++----- 3 files changed, 4 insertions(+), 5 deletions(-) delete mode 100644 test/utils/shippable/sanity-skip-python3.txt delete mode 100644 test/utils/shippable/sanity-test-python24.txt diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/utils/shippable/sanity-test-python24.txt b/test/utils/shippable/sanity-test-python24.txt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/utils/shippable/sanity.sh b/test/utils/shippable/sanity.sh index 248f4d9777c..8c1453022e7 100755 --- a/test/utils/shippable/sanity.sh +++ b/test/utils/shippable/sanity.sh @@ -15,11 +15,11 @@ source "${build_dir}/hacking/env-setup" # REPOMERGE: END if [ "${install_deps}" != "" ]; then - add-apt-repository ppa:fkrull/deadsnakes && apt-get update -qq && apt-get install python2.4 -qq - + add-apt-repository ppa:fkrull/deadsnakes apt-add-repository 'deb http://archive.ubuntu.com/ubuntu trusty-backports universe' apt-get update -qq - apt-get install shellcheck + + apt-get install -qq shellcheck python2.4 # Install dependencies for ansible and validate_modules pip install -r "${build_dir}/test/utils/shippable/sanity-requirements.txt" --upgrade @@ -29,11 +29,10 @@ fi validate_modules="${build_dir}/test/sanity/validate-modules/validate-modules" -python2.4 -m compileall -fq -i "test/utils/shippable/sanity-test-python24.txt" python2.4 -m compileall -fq -x "($(printf %s "$(< "test/utils/shippable/sanity-skip-python24.txt"))" | tr '\n' '|')" . python2.6 -m compileall -fq . python2.7 -m compileall -fq . -python3.5 -m compileall -fq . -x "($(printf %s "$(< "test/utils/shippable/sanity-skip-python3.txt"))" | tr '\n' '|')" +python3.5 -m compileall -fq . ANSIBLE_DEPRECATION_WARNINGS=false \ "${validate_modules}" --exclude '/utilities/|/shippable(/|$)' . From a1dcbf9ce50c19c21b0aa47891b1512c9abec3fc Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Thu, 3 Nov 2016 14:21:53 -0700 Subject: [PATCH 2309/2522] Fix line endings. --- cloud/misc/rhevm.py | 3060 +++++++++++++++++++++---------------------- 1 file changed, 1530 insertions(+), 1530 deletions(-) diff --git a/cloud/misc/rhevm.py b/cloud/misc/rhevm.py index 9d12a802afb..68927a9cac6 100644 --- a/cloud/misc/rhevm.py +++ b/cloud/misc/rhevm.py @@ -1,1530 +1,1530 @@ -#!/usr/bin/python - -# (c) 2016, Timothy Vandenbrande -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -DOCUMENTATION = ''' ---- -module: rhevm -author: Timothy Vandenbrande -short_description: RHEV/oVirt automation -description: - - Allows you to create/remove/update or powermanage virtual machines on a RHEV/oVirt platform. -version_added: "2.2" -requirements: - - ovirtsdk -options: - user: - description: - - The user to authenticate with. - default: "admin@internal" - required: false - server: - description: - - The name/ip of your RHEV-m/oVirt instance. - default: "127.0.0.1" - required: false - port: - description: - - The port on which the API is reacheable. - default: "443" - required: false - insecure_api: - description: - - A boolean switch to make a secure or insecure connection to the server. - default: false - required: false - name: - description: - - The name of the VM. - cluster: - description: - - The rhev/ovirt cluster in which you want you VM to start. - required: false - datacenter: - description: - - The rhev/ovirt datacenter in which you want you VM to start. - required: false - default: "Default" - state: - description: - - This serves to create/remove/update or powermanage your VM. - default: "present" - required: false - choices: ['ping', 'present', 'absent', 'up', 'down', 'restarted', 'cd', 'info'] - image: - description: - - The template to use for the VM. - default: null - required: false - type: - description: - - To define if the VM is a server or desktop. - default: server - required: false - choices: [ 'server', 'desktop', 'host' ] - vmhost: - description: - - The host you wish your VM to run on. - required: false - vmcpu: - description: - - The number of CPUs you want in your VM. - default: "2" - required: false - cpu_share: - description: - - This parameter is used to configure the cpu share. - default: "0" - required: false - vmmem: - description: - - The amount of memory you want your VM to use (in GB). - default: "1" - required: false - osver: - description: - - The operationsystem option in RHEV/oVirt. - default: "rhel_6x64" - required: false - mempol: - description: - - The minimum amount of memory you wish to reserve for this system. - default: "1" - required: false - vm_ha: - description: - - To make your VM High Available. - default: true - required: false - disks: - description: - - This option uses complex arguments and is a list of disks with the options name, size and domain. - required: false - ifaces: - description: - - This option uses complex arguments and is a list of interfaces with the options name and vlan. - aliases: ['nics', 'interfaces'] - required: false - boot_order: - description: - - This option uses complex arguments and is a list of items that specify the bootorder. - default: ["network","hd"] - required: false - del_prot: - description: - - This option sets the delete protection checkbox. - default: true - required: false - cd_drive: - description: - - The CD you wish to have mounted on the VM when I(state = 'CD'). - default: null - required: false - timeout: - description: - - The timeout you wish to define for power actions. - - When I(state = 'up') - - When I(state = 'down') - - When I(state = 'restarted') - default: null - required: false -''' - -RETURN = ''' -vm: - description: Returns all of the VMs variables and execution. - returned: always - type: dict - sample: '{ - "boot_order": [ - "hd", - "network" - ], - "changed": true, - "changes": [ - "Delete Protection" - ], - "cluster": "C1", - "cpu_share": "0", - "created": false, - "datacenter": "Default", - "del_prot": true, - "disks": [ - { - "domain": "ssd-san", - "name": "OS", - "size": 40 - } - ], - "eth0": "00:00:5E:00:53:00", - "eth1": "00:00:5E:00:53:01", - "eth2": "00:00:5E:00:53:02", - "exists": true, - "failed": false, - "ifaces": [ - { - "name": "eth0", - "vlan": "Management" - }, - { - "name": "eth1", - "vlan": "Internal" - }, - { - "name": "eth2", - "vlan": "External" - } - ], - "image": false, - "mempol": "0", - "msg": [ - "VM exists", - "cpu_share was already set to 0", - "VM high availability was already set to True", - "The boot order has already been set", - "VM delete protection has been set to True", - "Disk web2_Disk0_OS already exists", - "The VM starting host was already set to host416" - ], - "name": "web2", - "type": "server", - "uuid": "4ba5a1be-e60b-4368-9533-920f156c817b", - "vm_ha": true, - "vmcpu": "4", - "vmhost": "host416", - "vmmem": "16" - }' -''' - -EXAMPLES = ''' -# basic get info from VM - action: rhevm - args: - name: "demo" - user: "{{ rhev.admin.name }}" - password: "{{ rhev.admin.pass }}" - server: "rhevm01" - state: "info" - -# basic create example from image - action: rhevm - args: - name: "demo" - user: "{{ rhev.admin.name }}" - password: "{{ rhev.admin.pass }}" - server: "rhevm01" - state: "present" - image: "centos7_x64" - cluster: "centos" - -# power management - action: rhevm - args: - name: "uptime_server" - user: "{{ rhev.admin.name }}" - password: "{{ rhev.admin.pass }}" - server: "rhevm01" - cluster: "RH" - state: "down" - image: "centos7_x64" - cluster: "centos - -# multi disk, multi nic create example - action: rhevm - args: - name: "server007" - user: "{{ rhev.admin.name }}" - password: "{{ rhev.admin.pass }}" - server: "rhevm01" - cluster: "RH" - state: "present" - type: "server" - vmcpu: 4 - vmmem: 2 - ifaces: - - name: "eth0" - vlan: "vlan2202" - - name: "eth1" - vlan: "vlan36" - - name: "eth2" - vlan: "vlan38" - - name: "eth3" - vlan: "vlan2202" - disks: - - name: "root" - size: 10 - domain: "ssd-san" - - name: "swap" - size: 10 - domain: "15kiscsi-san" - - name: "opt" - size: 10 - domain: "15kiscsi-san" - - name: "var" - size: 10 - domain: "10kiscsi-san" - - name: "home" - size: 10 - domain: "sata-san" - boot_order: - - "network" - - "hd" - -# add a CD to the disk cd_drive - action: rhevm - args: - name: 'server007' - user: "{{ rhev.admin.name }}" - password: "{{ rhev.admin.pass }}" - state: 'cd' - cd_drive: 'rhev-tools-setup.iso' - -# new host deployment + host network configuration - action: rhevm - args: - name: "ovirt_node007" - password: "{{ rhevm.admin.pass }}" - type: "host" - state: present - cluster: "rhevm01" - ifaces: - - name: em1 - - name: em2 - - name: p3p1 - ip: '172.31.224.200' - netmask: '255.255.254.0' - - name: p3p2 - ip: '172.31.225.200' - netmask: '255.255.254.0' - - name: bond0 - bond: - - em1 - - em2 - network: 'rhevm' - ip: '172.31.222.200' - netmask: '255.255.255.0' - management: True - - name: bond0.36 - network: 'vlan36' - ip: '10.2.36.200' - netmask: '255.255.254.0' - gateway: '10.2.36.254' - - name: bond0.2202 - network: 'vlan2202' - - name: bond0.38 - network: 'vlan38' -''' - -import time -import sys -import traceback -import json - -try: - from ovirtsdk.api import API - from ovirtsdk.xml import params - HAS_SDK = True -except ImportError: - HAS_SDK = False - -RHEV_FAILED = 1 -RHEV_SUCCESS = 0 -RHEV_UNAVAILABLE = 2 - -RHEV_TYPE_OPTS = ['server', 'desktop', 'host'] -STATE_OPTS = ['ping', 'present', 'absent', 'up', 'down', 'restart', 'cd', 'info'] - -global msg, changed, failed -msg = [] -changed = False -failed = False - - -class RHEVConn(object): - 'Connection to RHEV-M' - def __init__(self, module): - self.module = module - - user = module.params.get('user') - password = module.params.get('password') - server = module.params.get('server') - port = module.params.get('port') - insecure_api = module.params.get('insecure_api') - - url = "https://%s:%s" % (server, port) - - try: - api = API(url=url, username=user, password=password, insecure=str(insecure_api)) - api.test() - self.conn = api - except: - raise Exception("Failed to connect to RHEV-M.") - - def __del__(self): - self.conn.disconnect() - - def createVMimage(self, name, cluster, template): - try: - vmparams = params.VM( - name=name, - cluster=self.conn.clusters.get(name=cluster), - template=self.conn.templates.get(name=template), - disks=params.Disks(clone=True) - ) - self.conn.vms.add(vmparams) - setMsg("VM is created") - setChanged() - return True - except Exception as e: - setMsg("Failed to create VM") - setMsg(str(e)) - setFailed() - return False - - def createVM(self, name, cluster, os, actiontype): - try: - vmparams = params.VM( - name=name, - cluster=self.conn.clusters.get(name=cluster), - os=params.OperatingSystem(type_=os), - template=self.conn.templates.get(name="Blank"), - type_=actiontype - ) - self.conn.vms.add(vmparams) - setMsg("VM is created") - setChanged() - return True - except Exception as e: - setMsg("Failed to create VM") - setMsg(str(e)) - setFailed() - return False - - def createDisk(self, vmname, diskname, disksize, diskdomain, diskinterface, diskformat, diskallocationtype, diskboot): - VM = self.get_VM(vmname) - - newdisk = params.Disk( - name=diskname, - size=1024 * 1024 * 1024 * int(disksize), - wipe_after_delete=True, - sparse=diskallocationtype, - interface=diskinterface, - format=diskformat, - bootable=diskboot, - storage_domains=params.StorageDomains( - storage_domain=[self.get_domain(diskdomain)] - ) - ) - - try: - VM.disks.add(newdisk) - VM.update() - setMsg("Successfully added disk " + diskname) - setChanged() - except Exception as e: - setFailed() - setMsg("Error attaching " + diskname + "disk, please recheck and remove any leftover configuration.") - setMsg(str(e)) - return False - - try: - currentdisk = VM.disks.get(name=diskname) - attempt = 1 - while currentdisk.status.state != 'ok': - currentdisk = VM.disks.get(name=diskname) - if attempt == 100: - setMsg("Error, disk %s, state %s" % (diskname, str(currentdisk.status.state))) - raise - else: - attempt += 1 - time.sleep(2) - setMsg("The disk " + diskname + " is ready.") - except Exception as e: - setFailed() - setMsg("Error getting the state of " + diskname + ".") - setMsg(str(e)) - return False - return True - - def createNIC(self, vmname, nicname, vlan, interface): - VM = self.get_VM(vmname) - CLUSTER = self.get_cluster_byid(VM.cluster.id) - DC = self.get_DC_byid(CLUSTER.data_center.id) - newnic = params.NIC( - name=nicname, - network=DC.networks.get(name=vlan), - interface=interface - ) - - try: - VM.nics.add(newnic) - VM.update() - setMsg("Successfully added iface " + nicname) - setChanged() - except Exception as e: - setFailed() - setMsg("Error attaching " + nicname + " iface, please recheck and remove any leftover configuration.") - setMsg(str(e)) - return False - - try: - currentnic = VM.nics.get(name=nicname) - attempt = 1 - while currentnic.active is not True: - currentnic = VM.nics.get(name=nicname) - if attempt == 100: - setMsg("Error, iface %s, state %s" % (nicname, str(currentnic.active))) - raise - else: - attempt += 1 - time.sleep(2) - setMsg("The iface " + nicname + " is ready.") - except Exception as e: - setFailed() - setMsg("Error getting the state of " + nicname + ".") - setMsg(str(e)) - return False - return True - - def get_DC(self, dc_name): - return self.conn.datacenters.get(name=dc_name) - - def get_DC_byid(self, dc_id): - return self.conn.datacenters.get(id=dc_id) - - def get_VM(self, vm_name): - return self.conn.vms.get(name=vm_name) - - def get_cluster_byid(self, cluster_id): - return self.conn.clusters.get(id=cluster_id) - - def get_cluster(self, cluster_name): - return self.conn.clusters.get(name=cluster_name) - - def get_domain_byid(self, dom_id): - return self.conn.storagedomains.get(id=dom_id) - - def get_domain(self, domain_name): - return self.conn.storagedomains.get(name=domain_name) - - def get_disk(self, disk): - return self.conn.disks.get(disk) - - def get_network(self, dc_name, network_name): - return self.get_DC(dc_name).networks.get(network_name) - - def get_network_byid(self, network_id): - return self.conn.networks.get(id=network_id) - - def get_NIC(self, vm_name, nic_name): - return self.get_VM(vm_name).nics.get(nic_name) - - def get_Host(self, host_name): - return self.conn.hosts.get(name=host_name) - - def get_Host_byid(self, host_id): - return self.conn.hosts.get(id=host_id) - - def set_Memory(self, name, memory): - VM = self.get_VM(name) - VM.memory = int(int(memory) * 1024 * 1024 * 1024) - try: - VM.update() - setMsg("The Memory has been updated.") - setChanged() - return True - except Exception as e: - setMsg("Failed to update memory.") - setMsg(str(e)) - setFailed() - return False - - def set_Memory_Policy(self, name, memory_policy): - VM = self.get_VM(name) - VM.memory_policy.guaranteed = int(int(memory_policy) * 1024 * 1024 * 1024) - try: - VM.update() - setMsg("The memory policy has been updated.") - setChanged() - return True - except Exception as e: - setMsg("Failed to update memory policy.") - setMsg(str(e)) - setFailed() - return False - - def set_CPU(self, name, cpu): - VM = self.get_VM(name) - VM.cpu.topology.cores = int(cpu) - try: - VM.update() - setMsg("The number of CPUs has been updated.") - setChanged() - return True - except Exception as e: - setMsg("Failed to update the number of CPUs.") - setMsg(str(e)) - setFailed() - return False - - def set_CPU_share(self, name, cpu_share): - VM = self.get_VM(name) - VM.cpu_shares = int(cpu_share) - try: - VM.update() - setMsg("The CPU share has been updated.") - setChanged() - return True - except Exception as e: - setMsg("Failed to update the CPU share.") - setMsg(str(e)) - setFailed() - return False - - def set_Disk(self, diskname, disksize, diskinterface, diskboot): - DISK = self.get_disk(diskname) - setMsg("Checking disk " + diskname) - if DISK.get_bootable() != diskboot: - try: - DISK.set_bootable(diskboot) - setMsg("Updated the boot option on the disk.") - setChanged() - except Exception as e: - setMsg("Failed to set the boot option on the disk.") - setMsg(str(e)) - setFailed() - return False - else: - setMsg("The boot option of the disk is correct") - if int(DISK.size) < (1024 * 1024 * 1024 * int(disksize)): - try: - DISK.size = (1024 * 1024 * 1024 * int(disksize)) - setMsg("Updated the size of the disk.") - setChanged() - except Exception as e: - setMsg("Failed to update the size of the disk.") - setMsg(str(e)) - setFailed() - return False - elif int(DISK.size) < (1024 * 1024 * 1024 * int(disksize)): - setMsg("Shrinking disks is not supported") - setMsg(str(e)) - setFailed() - return False - else: - setMsg("The size of the disk is correct") - if str(DISK.interface) != str(diskinterface): - try: - DISK.interface = diskinterface - setMsg("Updated the interface of the disk.") - setChanged() - except Exception as e: - setMsg("Failed to update the interface of the disk.") - setMsg(str(e)) - setFailed() - return False - else: - setMsg("The interface of the disk is correct") - return True - - def set_NIC(self, vmname, nicname, newname, vlan, interface): - NIC = self.get_NIC(vmname, nicname) - VM = self.get_VM(vmname) - CLUSTER = self.get_cluster_byid(VM.cluster.id) - DC = self.get_DC_byid(CLUSTER.data_center.id) - NETWORK = self.get_network(str(DC.name), vlan) - checkFail() - if NIC.name != newname: - NIC.name = newname - setMsg('Updating iface name to ' + newname) - setChanged() - if str(NIC.network.id) != str(NETWORK.id): - NIC.set_network(NETWORK) - setMsg('Updating iface network to ' + vlan) - setChanged() - if NIC.interface != interface: - NIC.interface = interface - setMsg('Updating iface interface to ' + interface) - setChanged() - try: - NIC.update() - setMsg('iface has succesfully been updated.') - except Exception as e: - setMsg("Failed to update the iface.") - setMsg(str(e)) - setFailed() - return False - return True - - def set_DeleteProtection(self, vmname, del_prot): - VM = self.get_VM(vmname) - VM.delete_protected = del_prot - try: - VM.update() - setChanged() - except Exception as e: - setMsg("Failed to update delete protection.") - setMsg(str(e)) - setFailed() - return False - return True - - def set_BootOrder(self, vmname, boot_order): - VM = self.get_VM(vmname) - bootorder = [] - for device in boot_order: - bootorder.append(params.Boot(dev=device)) - VM.os.boot = bootorder - - try: - VM.update() - setChanged() - except Exception as e: - setMsg("Failed to update the boot order.") - setMsg(str(e)) - setFailed() - return False - return True - - def set_Host(self, host_name, cluster, ifaces): - HOST = self.get_Host(host_name) - CLUSTER = self.get_cluster(cluster) - - if HOST is None: - setMsg("Host does not exist.") - ifacelist = dict() - networklist = [] - manageip = '' - - try: - for iface in ifaces: - try: - setMsg('creating host interface ' + iface['name']) - if 'management' in iface.keys(): - manageip = iface['ip'] - if 'boot_protocol' not in iface.keys(): - if 'ip' in iface.keys(): - iface['boot_protocol'] = 'static' - else: - iface['boot_protocol'] = 'none' - if 'ip' not in iface.keys(): - iface['ip'] = '' - if 'netmask' not in iface.keys(): - iface['netmask'] = '' - if 'gateway' not in iface.keys(): - iface['gateway'] = '' - - if 'network' in iface.keys(): - if 'bond' in iface.keys(): - bond = [] - for slave in iface['bond']: - bond.append(ifacelist[slave]) - try: - tmpiface = params.Bonding( - slaves = params.Slaves(host_nic = bond), - options = params.Options( - option = [ - params.Option(name = 'miimon', value = '100'), - params.Option(name = 'mode', value = '4') - ] - ) - ) - except Exception as e: - setMsg('Failed to create the bond for ' + iface['name']) - setFailed() - setMsg(str(e)) - return False - try: - tmpnetwork = params.HostNIC( - network = params.Network(name = iface['network']), - name = iface['name'], - boot_protocol = iface['boot_protocol'], - ip = params.IP( - address = iface['ip'], - netmask = iface['netmask'], - gateway = iface['gateway'] - ), - override_configuration = True, - bonding = tmpiface) - networklist.append(tmpnetwork) - setMsg('Applying network ' + iface['name']) - except Exception as e: - setMsg('Failed to set' + iface['name'] + ' as network interface') - setFailed() - setMsg(str(e)) - return False - else: - tmpnetwork = params.HostNIC( - network = params.Network(name = iface['network']), - name = iface['name'], - boot_protocol = iface['boot_protocol'], - ip = params.IP( - address = iface['ip'], - netmask = iface['netmask'], - gateway = iface['gateway'] - )) - networklist.append(tmpnetwork) - setMsg('Applying network ' + iface['name']) - else: - tmpiface = params.HostNIC( - name=iface['name'], - network=params.Network(), - boot_protocol=iface['boot_protocol'], - ip=params.IP( - address=iface['ip'], - netmask=iface['netmask'], - gateway=iface['gateway'] - )) - ifacelist[iface['name']] = tmpiface - except Exception as e: - setMsg('Failed to set ' + iface['name']) - setFailed() - setMsg(str(e)) - return False - except Exception as e: - setMsg('Failed to set networks') - setMsg(str(e)) - setFailed() - return False - - if manageip == '': - setMsg('No management network is defined') - setFailed() - return False - - try: - HOST = params.Host(name=host_name, address=manageip, cluster=CLUSTER, ssh=params.SSH(authentication_method='publickey')) - if self.conn.hosts.add(HOST): - setChanged() - HOST = self.get_Host(host_name) - state = HOST.status.state - while (state != 'non_operational' and state != 'up'): - HOST = self.get_Host(host_name) - state = HOST.status.state - time.sleep(1) - if state == 'non_responsive': - setMsg('Failed to add host to RHEVM') - setFailed() - return False - - setMsg('status host: up') - time.sleep(5) - - HOST = self.get_Host(host_name) - state = HOST.status.state - setMsg('State before setting to maintenance: ' + str(state)) - HOST.deactivate() - while state != 'maintenance': - HOST = self.get_Host(host_name) - state = HOST.status.state - time.sleep(1) - setMsg('status host: maintenance') - - try: - HOST.nics.setupnetworks(params.Action( - force=True, - check_connectivity = False, - host_nics = params.HostNics(host_nic = networklist) - )) - setMsg('nics are set') - except Exception as e: - setMsg('Failed to apply networkconfig') - setFailed() - setMsg(str(e)) - return False - - try: - HOST.commitnetconfig() - setMsg('Network config is saved') - except Exception as e: - setMsg('Failed to save networkconfig') - setFailed() - setMsg(str(e)) - return False - except Exception as e: - if 'The Host name is already in use' in str(e): - setMsg("Host already exists") - else: - setMsg("Failed to add host") - setFailed() - setMsg(str(e)) - return False - - HOST.activate() - while state != 'up': - HOST = self.get_Host(host_name) - state = HOST.status.state - time.sleep(1) - if state == 'non_responsive': - setMsg('Failed to apply networkconfig.') - setFailed() - return False - setMsg('status host: up') - else: - setMsg("Host exists.") - - return True - - def del_NIC(self, vmname, nicname): - return self.get_NIC(vmname, nicname).delete() - - def remove_VM(self, vmname): - VM = self.get_VM(vmname) - try: - VM.delete() - except Exception as e: - setMsg("Failed to remove VM.") - setMsg(str(e)) - setFailed() - return False - return True - - def start_VM(self, vmname, timeout): - VM = self.get_VM(vmname) - try: - VM.start() - except Exception as e: - setMsg("Failed to start VM.") - setMsg(str(e)) - setFailed() - return False - return self.wait_VM(vmname, "up", timeout) - - def wait_VM(self, vmname, state, timeout): - VM = self.get_VM(vmname) - while VM.status.state != state: - VM = self.get_VM(vmname) - time.sleep(10) - if timeout is not False: - timeout -= 10 - if timeout <= 0: - setMsg("Timeout expired") - setFailed() - return False - return True - - def stop_VM(self, vmname, timeout): - VM = self.get_VM(vmname) - try: - VM.stop() - except Exception as e: - setMsg("Failed to stop VM.") - setMsg(str(e)) - setFailed() - return False - return self.wait_VM(vmname, "down", timeout) - - def set_CD(self, vmname, cd_drive): - VM = self.get_VM(vmname) - try: - if str(VM.status.state) == 'down': - cdrom = params.CdRom(file=cd_iso) - VM.cdroms.add(cdrom) - setMsg("Attached the image.") - setChanged() - else: - cdrom = VM.cdroms.get(id="00000000-0000-0000-0000-000000000000") - cdrom.set_file(cd_iso) - cdrom.update(current=True) - setMsg("Attached the image.") - setChanged() - except Exception as e: - setMsg("Failed to attach image.") - setMsg(str(e)) - setFailed() - return False - return True - - def set_VM_Host(self, vmname, vmhost): - VM = self.get_VM(vmname) - HOST = self.get_Host(vmhost) - try: - VM.placement_policy.host = HOST - VM.update() - setMsg("Set startup host to " + vmhost) - setChanged() - except Exception as e: - setMsg("Failed to set startup host.") - setMsg(str(e)) - setFailed() - return False - return True - - def migrate_VM(self, vmname, vmhost): - VM = self.get_VM(vmname) - - HOST = self.get_Host_byid(VM.host.id) - if str(HOST.name) != vmhost: - try: - vm.migrate( - action=params.Action( - host=params.Host( - name=vmhost, - ) - ), - ) - setChanged() - setMsg("VM migrated to " + vmhost) - except Exception as e: - setMsg("Failed to set startup host.") - setMsg(str(e)) - setFailed() - return False - return True - - def remove_CD(self, vmname): - VM = self.get_VM(vmname) - try: - VM.cdroms.get(id="00000000-0000-0000-0000-000000000000").delete() - setMsg("Removed the image.") - setChanged() - except Exception as e: - setMsg("Failed to remove the image.") - setMsg(str(e)) - setFailed() - return False - return True - - -class RHEV(object): - def __init__(self, module): - self.module = module - - def __get_conn(self): - self.conn = RHEVConn(self.module) - return self.conn - - def test(self): - self.__get_conn() - return "OK" - - def getVM(self, name): - self.__get_conn() - VM = self.conn.get_VM(name) - if VM: - vminfo = dict() - vminfo['uuid'] = VM.id - vminfo['name'] = VM.name - vminfo['status'] = VM.status.state - vminfo['cpu_cores'] = VM.cpu.topology.cores - vminfo['cpu_sockets'] = VM.cpu.topology.sockets - vminfo['cpu_shares'] = VM.cpu_shares - vminfo['memory'] = (int(VM.memory) / 1024 / 1024 / 1024) - vminfo['mem_pol'] = (int(VM.memory_policy.guaranteed) / 1024 / 1024 / 1024) - vminfo['os'] = VM.get_os().type_ - vminfo['del_prot'] = VM.delete_protected - try: - vminfo['host'] = str(self.conn.get_Host_byid(str(VM.host.id)).name) - except Exception as e: - vminfo['host'] = None - vminfo['boot_order'] = [] - for boot_dev in VM.os.get_boot(): - vminfo['boot_order'].append(str(boot_dev.dev)) - vminfo['disks'] = [] - for DISK in VM.disks.list(): - disk = dict() - disk['name'] = DISK.name - disk['size'] = (int(DISK.size) / 1024 / 1024 / 1024) - disk['domain'] = str((self.conn.get_domain_byid(DISK.get_storage_domains().get_storage_domain()[0].id)).name) - disk['interface'] = DISK.interface - vminfo['disks'].append(disk) - vminfo['ifaces'] = [] - for NIC in VM.nics.list(): - iface = dict() - iface['name'] = str(NIC.name) - iface['vlan'] = str(self.conn.get_network_byid(NIC.get_network().id).name) - iface['interface'] = NIC.interface - iface['mac'] = NIC.mac.address - vminfo['ifaces'].append(iface) - vminfo[str(NIC.name)] = NIC.mac.address - CLUSTER = self.conn.get_cluster_byid(VM.cluster.id) - if CLUSTER: - vminfo['cluster'] = CLUSTER.name - else: - vminfo = False - return vminfo - - def createVMimage(self, name, cluster, template, disks): - self.__get_conn() - return self.conn.createVMimage(name, cluster, template, disks) - - def createVM(self, name, cluster, os, actiontype): - self.__get_conn() - return self.conn.createVM(name, cluster, os, actiontype) - - def setMemory(self, name, memory): - self.__get_conn() - return self.conn.set_Memory(name, memory) - - def setMemoryPolicy(self, name, memory_policy): - self.__get_conn() - return self.conn.set_Memory_Policy(name, memory_policy) - - def setCPU(self, name, cpu): - self.__get_conn() - return self.conn.set_CPU(name, cpu) - - def setCPUShare(self, name, cpu_share): - self.__get_conn() - return self.conn.set_CPU_share(name, cpu_share) - - def setDisks(self, name, disks): - self.__get_conn() - counter = 0 - bootselect = False - for disk in disks: - if 'bootable' in disk: - if disk['bootable'] is True: - bootselect = True - - for disk in disks: - diskname = name + "_Disk" + str(counter) + "_" + disk.get('name', '').replace('/', '_') - disksize = disk.get('size', 1) - diskdomain = disk.get('domain', None) - if diskdomain is None: - setMsg("`domain` is a required disk key.") - setFailed() - return False - diskinterface = disk.get('interface', 'virtio') - diskformat = disk.get('format', 'raw') - diskallocationtype = disk.get('thin', False) - diskboot = disk.get('bootable', False) - - if bootselect is False and counter == 0: - diskboot = True - - DISK = self.conn.get_disk(diskname) - - if DISK is None: - self.conn.createDisk(name, diskname, disksize, diskdomain, diskinterface, diskformat, diskallocationtype, diskboot) - else: - self.conn.set_Disk(diskname, disksize, diskinterface, diskboot) - checkFail() - counter += 1 - - return True - - def setNetworks(self, vmname, ifaces): - self.__get_conn() - VM = self.conn.get_VM(vmname) - - counter = 0 - length = len(ifaces) - - for NIC in VM.nics.list(): - if counter < length: - iface = ifaces[counter] - name = iface.get('name', None) - if name is None: - setMsg("`name` is a required iface key.") - setFailed() - elif str(name) != str(NIC.name): - setMsg("ifaces are in the wrong order, rebuilding everything.") - for NIC in VM.nics.list(): - self.conn.del_NIC(vmname, NIC.name) - self.setNetworks(vmname, ifaces) - checkFail() - return True - vlan = iface.get('vlan', None) - if vlan is None: - setMsg("`vlan` is a required iface key.") - setFailed() - checkFail() - interface = iface.get('interface', 'virtio') - self.conn.set_NIC(vmname, str(NIC.name), name, vlan, interface) - else: - self.conn.del_NIC(vmname, NIC.name) - counter += 1 - checkFail() - - while counter < length: - iface = ifaces[counter] - name = iface.get('name', None) - if name is None: - setMsg("`name` is a required iface key.") - setFailed() - vlan = iface.get('vlan', None) - if vlan is None: - setMsg("`vlan` is a required iface key.") - setFailed() - if failed is True: - return False - interface = iface.get('interface', 'virtio') - self.conn.createNIC(vmname, name, vlan, interface) - - counter += 1 - checkFail() - return True - - def setDeleteProtection(self, vmname, del_prot): - self.__get_conn() - VM = self.conn.get_VM(vmname) - if bool(VM.delete_protected) != bool(del_prot): - self.conn.set_DeleteProtection(vmname, del_prot) - checkFail() - setMsg("`delete protection` has been updated.") - else: - setMsg("`delete protection` already has the right value.") - return True - - def setBootOrder(self, vmname, boot_order): - self.__get_conn() - VM = self.conn.get_VM(vmname) - bootorder = [] - for boot_dev in VM.os.get_boot(): - bootorder.append(str(boot_dev.dev)) - - if boot_order != bootorder: - self.conn.set_BootOrder(vmname, boot_order) - setMsg('The boot order has been set') - else: - setMsg('The boot order has already been set') - return True - - def removeVM(self, vmname): - self.__get_conn() - self.setPower(vmname, "down", 300) - return self.conn.remove_VM(vmname) - - def setPower(self, vmname, state, timeout): - self.__get_conn() - VM = self.conn.get_VM(vmname) - if VM is None: - setMsg("VM does not exist.") - setFailed() - return False - - if state == VM.status.state: - setMsg("VM state was already " + state) - else: - if state == "up": - setMsg("VM is going to start") - self.conn.start_VM(vmname, timeout) - setChanged() - elif state == "down": - setMsg("VM is going to stop") - self.conn.stop_VM(vmname, timeout) - setChanged() - elif state == "restarted": - self.setPower(vmname, "down", timeout) - checkFail() - self.setPower(vmname, "up", timeout) - checkFail() - setMsg("the vm state is set to " + state) - return True - - def setCD(self, vmname, cd_drive): - self.__get_conn() - if cd_drive: - return self.conn.set_CD(vmname, cd_drive) - else: - return self.conn.remove_CD(vmname) - - def setVMHost(self, vmname, vmhost): - self.__get_conn() - return self.conn.set_VM_Host(vmname, vmhost) - - VM = self.conn.get_VM(vmname) - HOST = self.conn.get_Host(vmhost) - - if VM.placement_policy.host is None: - self.conn.set_VM_Host(vmname, vmhost) - elif str(VM.placement_policy.host.id) != str(HOST.id): - self.conn.set_VM_Host(vmname, vmhost) - else: - setMsg("VM's startup host was already set to " + vmhost) - checkFail() - - if str(VM.status.state) == "up": - self.conn.migrate_VM(vmname, vmhost) - checkFail() - - return True - - def setHost(self, hostname, cluster, ifaces): - self.__get_conn() - return self.conn.set_Host(hostname, cluster, ifaces) - - -def checkFail(): - if failed: - module.fail_json(msg=msg) - else: - return True - - -def setFailed(): - global failed - failed = True - - -def setChanged(): - global changed - changed = True - - -def setMsg(message): - global failed - msg.append(message) - - -def core(module): - - r = RHEV(module) - - state = module.params.get('state', 'present') - - if state == 'ping': - r.test() - return RHEV_SUCCESS, {"ping": "pong"} - elif state == 'info': - name = module.params.get('name') - if not name: - setMsg("`name` is a required argument.") - return RHEV_FAILED, msg - vminfo = r.getVM(name) - return RHEV_SUCCESS, {'changed': changed, 'msg': msg, 'vm': vminfo} - elif state == 'present': - created = False - name = module.params.get('name') - if not name: - setMsg("`name` is a required argument.") - return RHEV_FAILED, msg - actiontype = module.params.get('type') - if actiontype == 'server' or actiontype == 'desktop': - vminfo = r.getVM(name) - if vminfo: - setMsg('VM exists') - else: - # Create VM - cluster = module.params.get('cluster') - if cluster is None: - setMsg("cluster is a required argument.") - setFailed() - template = module.params.get('image') - if template: - disks = module.params.get('disks') - if disks is None: - setMsg("disks is a required argument.") - setFailed() - checkFail() - if r.createVMimage(name, cluster, template, disks) is False: - return RHEV_FAILED, vminfo - else: - os = module.params.get('osver') - if os is None: - setMsg("osver is a required argument.") - setFailed() - checkFail() - if r.createVM(name, cluster, os, actiontype) is False: - return RHEV_FAILED, vminfo - created = True - - # Set MEMORY and MEMORY POLICY - vminfo = r.getVM(name) - memory = module.params.get('vmmem') - if memory is not None: - memory_policy = module.params.get('mempol') - if int(memory_policy) == 0: - memory_policy = memory - mem_pol_nok = True - if int(vminfo['mem_pol']) == int(memory_policy): - setMsg("Memory is correct") - mem_pol_nok = False - - mem_nok = True - if int(vminfo['memory']) == int(memory): - setMsg("Memory is correct") - mem_nok = False - - if memory_policy > memory: - setMsg('memory_policy cannot have a higher value than memory.') - return RHEV_FAILED, msg - - if mem_nok and mem_pol_nok: - if int(memory_policy) > int(vminfo['memory']): - r.setMemory(vminfo['name'], memory) - r.setMemoryPolicy(vminfo['name'], memory_policy) - else: - r.setMemoryPolicy(vminfo['name'], memory_policy) - r.setMemory(vminfo['name'], memory) - elif mem_nok: - r.setMemory(vminfo['name'], memory) - elif mem_pol_nok: - r.setMemoryPolicy(vminfo['name'], memory_policy) - checkFail() - - # Set CPU - cpu = module.params.get('vmcpu') - if int(vminfo['cpu_cores']) == int(cpu): - setMsg("Number of CPUs is correct") - else: - if r.setCPU(vminfo['name'], cpu) is False: - return RHEV_FAILED, msg - - # Set CPU SHARE - cpu_share = module.params.get('cpu_share') - if cpu_share is not None: - if int(vminfo['cpu_shares']) == int(cpu_share): - setMsg("CPU share is correct.") - else: - if r.setCPUShare(vminfo['name'], cpu_share) is False: - return RHEV_FAILED, msg - - # Set DISKS - disks = module.params.get('disks') - if disks is not None: - if r.setDisks(vminfo['name'], disks) is False: - return RHEV_FAILED, msg - - # Set NETWORKS - ifaces = module.params.get('ifaces', None) - if ifaces is not None: - if r.setNetworks(vminfo['name'], ifaces) is False: - return RHEV_FAILED, msg - - # Set Delete Protection - del_prot = module.params.get('del_prot') - if r.setDeleteProtection(vminfo['name'], del_prot) is False: - return RHEV_FAILED, msg - - # Set Boot Order - boot_order = module.params.get('boot_order') - if r.setBootOrder(vminfo['name'], boot_order) is False: - return RHEV_FAILED, msg - - # Set VM Host - vmhost = module.params.get('vmhost') - if vmhost is not False and vmhost is not "False": - if r.setVMHost(vminfo['name'], vmhost) is False: - return RHEV_FAILED, msg - - vminfo = r.getVM(name) - vminfo['created'] = created - return RHEV_SUCCESS, {'changed': changed, 'msg': msg, 'vm': vminfo} - - if actiontype == 'host': - cluster = module.params.get('cluster') - if cluster is None: - setMsg("cluster is a required argument.") - setFailed() - ifaces = module.params.get('ifaces') - if ifaces is None: - setMsg("ifaces is a required argument.") - setFailed() - if r.setHost(name, cluster, ifaces) is False: - return RHEV_FAILED, msg - return RHEV_SUCCESS, {'changed': changed, 'msg': msg} - - elif state == 'absent': - name = module.params.get('name') - if not name: - setMsg("`name` is a required argument.") - return RHEV_FAILED, msg - actiontype = module.params.get('type') - if actiontype == 'server' or actiontype == 'desktop': - vminfo = r.getVM(name) - if vminfo: - setMsg('VM exists') - - # Set Delete Protection - del_prot = module.params.get('del_prot') - if r.setDeleteProtection(vminfo['name'], del_prot) is False: - return RHEV_FAILED, msg - - # Remove VM - if r.removeVM(vminfo['name']) is False: - return RHEV_FAILED, msg - setMsg('VM has been removed.') - vminfo['state'] = 'DELETED' - else: - setMsg('VM was already removed.') - return RHEV_SUCCESS, {'changed': changed, 'msg': msg, 'vm': vminfo} - - elif state == 'up' or state == 'down' or state == 'restarted': - name = module.params.get('name') - if not name: - setMsg("`name` is a required argument.") - return RHEV_FAILED, msg - timeout = module.params.get('timeout') - if r.setPower(name, state, timeout) is False: - return RHEV_FAILED, msg - vminfo = r.getVM(name) - return RHEV_SUCCESS, {'changed': changed, 'msg': msg, 'vm': vminfo} - - elif state == 'cd': - name = module.params.get('name') - cd_drive = module.params.get('cd_drive') - if r.setCD(name, cd_drive) is False: - return RHEV_FAILED, msg - return RHEV_SUCCESS, {'changed': changed, 'msg': msg} - - -def main(): - global module - module = AnsibleModule( - argument_spec = dict( - state = dict(default='present', choices=['ping', 'present', 'absent', 'up', 'down', 'restarted', 'cd', 'info']), - user = dict(default="admin@internal"), - password = dict(required=True), - server = dict(default="127.0.0.1"), - port = dict(default="443"), - insecure_api = dict(default=False, type='bool'), - name = dict(), - image = dict(default=False), - datacenter = dict(default="Default"), - type = dict(default="server", choices=['server', 'desktop', 'host']), - cluster = dict(default=''), - vmhost = dict(default=False), - vmcpu = dict(default="2"), - vmmem = dict(default="1"), - disks = dict(), - osver = dict(default="rhel_6x64"), - ifaces = dict(aliases=['nics', 'interfaces']), - timeout = dict(default=False), - mempol = dict(default="1"), - vm_ha = dict(default=True), - cpu_share = dict(default="0"), - boot_order = dict(default=["network", "hd"]), - del_prot = dict(default=True, type="bool"), - cd_drive = dict(default=False) - ), - ) - - if not HAS_SDK: - module.fail_json( - msg='The `ovirtsdk` module is not importable. Check the requirements.' - ) - - rc = RHEV_SUCCESS - try: - rc, result = core(module) - except Exception as e: - module.fail_json(msg=str(e)) - - if rc != 0: # something went wrong emit the msg - module.fail_json(rc=rc, msg=result) - else: - module.exit_json(**result) - - -# import module snippets -from ansible.module_utils.basic import * - -if __name__ == '__main__': - main() +#!/usr/bin/python + +# (c) 2016, Timothy Vandenbrande +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: rhevm +author: Timothy Vandenbrande +short_description: RHEV/oVirt automation +description: + - Allows you to create/remove/update or powermanage virtual machines on a RHEV/oVirt platform. +version_added: "2.2" +requirements: + - ovirtsdk +options: + user: + description: + - The user to authenticate with. + default: "admin@internal" + required: false + server: + description: + - The name/ip of your RHEV-m/oVirt instance. + default: "127.0.0.1" + required: false + port: + description: + - The port on which the API is reacheable. + default: "443" + required: false + insecure_api: + description: + - A boolean switch to make a secure or insecure connection to the server. + default: false + required: false + name: + description: + - The name of the VM. + cluster: + description: + - The rhev/ovirt cluster in which you want you VM to start. + required: false + datacenter: + description: + - The rhev/ovirt datacenter in which you want you VM to start. + required: false + default: "Default" + state: + description: + - This serves to create/remove/update or powermanage your VM. + default: "present" + required: false + choices: ['ping', 'present', 'absent', 'up', 'down', 'restarted', 'cd', 'info'] + image: + description: + - The template to use for the VM. + default: null + required: false + type: + description: + - To define if the VM is a server or desktop. + default: server + required: false + choices: [ 'server', 'desktop', 'host' ] + vmhost: + description: + - The host you wish your VM to run on. + required: false + vmcpu: + description: + - The number of CPUs you want in your VM. + default: "2" + required: false + cpu_share: + description: + - This parameter is used to configure the cpu share. + default: "0" + required: false + vmmem: + description: + - The amount of memory you want your VM to use (in GB). + default: "1" + required: false + osver: + description: + - The operationsystem option in RHEV/oVirt. + default: "rhel_6x64" + required: false + mempol: + description: + - The minimum amount of memory you wish to reserve for this system. + default: "1" + required: false + vm_ha: + description: + - To make your VM High Available. + default: true + required: false + disks: + description: + - This option uses complex arguments and is a list of disks with the options name, size and domain. + required: false + ifaces: + description: + - This option uses complex arguments and is a list of interfaces with the options name and vlan. + aliases: ['nics', 'interfaces'] + required: false + boot_order: + description: + - This option uses complex arguments and is a list of items that specify the bootorder. + default: ["network","hd"] + required: false + del_prot: + description: + - This option sets the delete protection checkbox. + default: true + required: false + cd_drive: + description: + - The CD you wish to have mounted on the VM when I(state = 'CD'). + default: null + required: false + timeout: + description: + - The timeout you wish to define for power actions. + - When I(state = 'up') + - When I(state = 'down') + - When I(state = 'restarted') + default: null + required: false +''' + +RETURN = ''' +vm: + description: Returns all of the VMs variables and execution. + returned: always + type: dict + sample: '{ + "boot_order": [ + "hd", + "network" + ], + "changed": true, + "changes": [ + "Delete Protection" + ], + "cluster": "C1", + "cpu_share": "0", + "created": false, + "datacenter": "Default", + "del_prot": true, + "disks": [ + { + "domain": "ssd-san", + "name": "OS", + "size": 40 + } + ], + "eth0": "00:00:5E:00:53:00", + "eth1": "00:00:5E:00:53:01", + "eth2": "00:00:5E:00:53:02", + "exists": true, + "failed": false, + "ifaces": [ + { + "name": "eth0", + "vlan": "Management" + }, + { + "name": "eth1", + "vlan": "Internal" + }, + { + "name": "eth2", + "vlan": "External" + } + ], + "image": false, + "mempol": "0", + "msg": [ + "VM exists", + "cpu_share was already set to 0", + "VM high availability was already set to True", + "The boot order has already been set", + "VM delete protection has been set to True", + "Disk web2_Disk0_OS already exists", + "The VM starting host was already set to host416" + ], + "name": "web2", + "type": "server", + "uuid": "4ba5a1be-e60b-4368-9533-920f156c817b", + "vm_ha": true, + "vmcpu": "4", + "vmhost": "host416", + "vmmem": "16" + }' +''' + +EXAMPLES = ''' +# basic get info from VM + action: rhevm + args: + name: "demo" + user: "{{ rhev.admin.name }}" + password: "{{ rhev.admin.pass }}" + server: "rhevm01" + state: "info" + +# basic create example from image + action: rhevm + args: + name: "demo" + user: "{{ rhev.admin.name }}" + password: "{{ rhev.admin.pass }}" + server: "rhevm01" + state: "present" + image: "centos7_x64" + cluster: "centos" + +# power management + action: rhevm + args: + name: "uptime_server" + user: "{{ rhev.admin.name }}" + password: "{{ rhev.admin.pass }}" + server: "rhevm01" + cluster: "RH" + state: "down" + image: "centos7_x64" + cluster: "centos + +# multi disk, multi nic create example + action: rhevm + args: + name: "server007" + user: "{{ rhev.admin.name }}" + password: "{{ rhev.admin.pass }}" + server: "rhevm01" + cluster: "RH" + state: "present" + type: "server" + vmcpu: 4 + vmmem: 2 + ifaces: + - name: "eth0" + vlan: "vlan2202" + - name: "eth1" + vlan: "vlan36" + - name: "eth2" + vlan: "vlan38" + - name: "eth3" + vlan: "vlan2202" + disks: + - name: "root" + size: 10 + domain: "ssd-san" + - name: "swap" + size: 10 + domain: "15kiscsi-san" + - name: "opt" + size: 10 + domain: "15kiscsi-san" + - name: "var" + size: 10 + domain: "10kiscsi-san" + - name: "home" + size: 10 + domain: "sata-san" + boot_order: + - "network" + - "hd" + +# add a CD to the disk cd_drive + action: rhevm + args: + name: 'server007' + user: "{{ rhev.admin.name }}" + password: "{{ rhev.admin.pass }}" + state: 'cd' + cd_drive: 'rhev-tools-setup.iso' + +# new host deployment + host network configuration + action: rhevm + args: + name: "ovirt_node007" + password: "{{ rhevm.admin.pass }}" + type: "host" + state: present + cluster: "rhevm01" + ifaces: + - name: em1 + - name: em2 + - name: p3p1 + ip: '172.31.224.200' + netmask: '255.255.254.0' + - name: p3p2 + ip: '172.31.225.200' + netmask: '255.255.254.0' + - name: bond0 + bond: + - em1 + - em2 + network: 'rhevm' + ip: '172.31.222.200' + netmask: '255.255.255.0' + management: True + - name: bond0.36 + network: 'vlan36' + ip: '10.2.36.200' + netmask: '255.255.254.0' + gateway: '10.2.36.254' + - name: bond0.2202 + network: 'vlan2202' + - name: bond0.38 + network: 'vlan38' +''' + +import time +import sys +import traceback +import json + +try: + from ovirtsdk.api import API + from ovirtsdk.xml import params + HAS_SDK = True +except ImportError: + HAS_SDK = False + +RHEV_FAILED = 1 +RHEV_SUCCESS = 0 +RHEV_UNAVAILABLE = 2 + +RHEV_TYPE_OPTS = ['server', 'desktop', 'host'] +STATE_OPTS = ['ping', 'present', 'absent', 'up', 'down', 'restart', 'cd', 'info'] + +global msg, changed, failed +msg = [] +changed = False +failed = False + + +class RHEVConn(object): + 'Connection to RHEV-M' + def __init__(self, module): + self.module = module + + user = module.params.get('user') + password = module.params.get('password') + server = module.params.get('server') + port = module.params.get('port') + insecure_api = module.params.get('insecure_api') + + url = "https://%s:%s" % (server, port) + + try: + api = API(url=url, username=user, password=password, insecure=str(insecure_api)) + api.test() + self.conn = api + except: + raise Exception("Failed to connect to RHEV-M.") + + def __del__(self): + self.conn.disconnect() + + def createVMimage(self, name, cluster, template): + try: + vmparams = params.VM( + name=name, + cluster=self.conn.clusters.get(name=cluster), + template=self.conn.templates.get(name=template), + disks=params.Disks(clone=True) + ) + self.conn.vms.add(vmparams) + setMsg("VM is created") + setChanged() + return True + except Exception as e: + setMsg("Failed to create VM") + setMsg(str(e)) + setFailed() + return False + + def createVM(self, name, cluster, os, actiontype): + try: + vmparams = params.VM( + name=name, + cluster=self.conn.clusters.get(name=cluster), + os=params.OperatingSystem(type_=os), + template=self.conn.templates.get(name="Blank"), + type_=actiontype + ) + self.conn.vms.add(vmparams) + setMsg("VM is created") + setChanged() + return True + except Exception as e: + setMsg("Failed to create VM") + setMsg(str(e)) + setFailed() + return False + + def createDisk(self, vmname, diskname, disksize, diskdomain, diskinterface, diskformat, diskallocationtype, diskboot): + VM = self.get_VM(vmname) + + newdisk = params.Disk( + name=diskname, + size=1024 * 1024 * 1024 * int(disksize), + wipe_after_delete=True, + sparse=diskallocationtype, + interface=diskinterface, + format=diskformat, + bootable=diskboot, + storage_domains=params.StorageDomains( + storage_domain=[self.get_domain(diskdomain)] + ) + ) + + try: + VM.disks.add(newdisk) + VM.update() + setMsg("Successfully added disk " + diskname) + setChanged() + except Exception as e: + setFailed() + setMsg("Error attaching " + diskname + "disk, please recheck and remove any leftover configuration.") + setMsg(str(e)) + return False + + try: + currentdisk = VM.disks.get(name=diskname) + attempt = 1 + while currentdisk.status.state != 'ok': + currentdisk = VM.disks.get(name=diskname) + if attempt == 100: + setMsg("Error, disk %s, state %s" % (diskname, str(currentdisk.status.state))) + raise + else: + attempt += 1 + time.sleep(2) + setMsg("The disk " + diskname + " is ready.") + except Exception as e: + setFailed() + setMsg("Error getting the state of " + diskname + ".") + setMsg(str(e)) + return False + return True + + def createNIC(self, vmname, nicname, vlan, interface): + VM = self.get_VM(vmname) + CLUSTER = self.get_cluster_byid(VM.cluster.id) + DC = self.get_DC_byid(CLUSTER.data_center.id) + newnic = params.NIC( + name=nicname, + network=DC.networks.get(name=vlan), + interface=interface + ) + + try: + VM.nics.add(newnic) + VM.update() + setMsg("Successfully added iface " + nicname) + setChanged() + except Exception as e: + setFailed() + setMsg("Error attaching " + nicname + " iface, please recheck and remove any leftover configuration.") + setMsg(str(e)) + return False + + try: + currentnic = VM.nics.get(name=nicname) + attempt = 1 + while currentnic.active is not True: + currentnic = VM.nics.get(name=nicname) + if attempt == 100: + setMsg("Error, iface %s, state %s" % (nicname, str(currentnic.active))) + raise + else: + attempt += 1 + time.sleep(2) + setMsg("The iface " + nicname + " is ready.") + except Exception as e: + setFailed() + setMsg("Error getting the state of " + nicname + ".") + setMsg(str(e)) + return False + return True + + def get_DC(self, dc_name): + return self.conn.datacenters.get(name=dc_name) + + def get_DC_byid(self, dc_id): + return self.conn.datacenters.get(id=dc_id) + + def get_VM(self, vm_name): + return self.conn.vms.get(name=vm_name) + + def get_cluster_byid(self, cluster_id): + return self.conn.clusters.get(id=cluster_id) + + def get_cluster(self, cluster_name): + return self.conn.clusters.get(name=cluster_name) + + def get_domain_byid(self, dom_id): + return self.conn.storagedomains.get(id=dom_id) + + def get_domain(self, domain_name): + return self.conn.storagedomains.get(name=domain_name) + + def get_disk(self, disk): + return self.conn.disks.get(disk) + + def get_network(self, dc_name, network_name): + return self.get_DC(dc_name).networks.get(network_name) + + def get_network_byid(self, network_id): + return self.conn.networks.get(id=network_id) + + def get_NIC(self, vm_name, nic_name): + return self.get_VM(vm_name).nics.get(nic_name) + + def get_Host(self, host_name): + return self.conn.hosts.get(name=host_name) + + def get_Host_byid(self, host_id): + return self.conn.hosts.get(id=host_id) + + def set_Memory(self, name, memory): + VM = self.get_VM(name) + VM.memory = int(int(memory) * 1024 * 1024 * 1024) + try: + VM.update() + setMsg("The Memory has been updated.") + setChanged() + return True + except Exception as e: + setMsg("Failed to update memory.") + setMsg(str(e)) + setFailed() + return False + + def set_Memory_Policy(self, name, memory_policy): + VM = self.get_VM(name) + VM.memory_policy.guaranteed = int(int(memory_policy) * 1024 * 1024 * 1024) + try: + VM.update() + setMsg("The memory policy has been updated.") + setChanged() + return True + except Exception as e: + setMsg("Failed to update memory policy.") + setMsg(str(e)) + setFailed() + return False + + def set_CPU(self, name, cpu): + VM = self.get_VM(name) + VM.cpu.topology.cores = int(cpu) + try: + VM.update() + setMsg("The number of CPUs has been updated.") + setChanged() + return True + except Exception as e: + setMsg("Failed to update the number of CPUs.") + setMsg(str(e)) + setFailed() + return False + + def set_CPU_share(self, name, cpu_share): + VM = self.get_VM(name) + VM.cpu_shares = int(cpu_share) + try: + VM.update() + setMsg("The CPU share has been updated.") + setChanged() + return True + except Exception as e: + setMsg("Failed to update the CPU share.") + setMsg(str(e)) + setFailed() + return False + + def set_Disk(self, diskname, disksize, diskinterface, diskboot): + DISK = self.get_disk(diskname) + setMsg("Checking disk " + diskname) + if DISK.get_bootable() != diskboot: + try: + DISK.set_bootable(diskboot) + setMsg("Updated the boot option on the disk.") + setChanged() + except Exception as e: + setMsg("Failed to set the boot option on the disk.") + setMsg(str(e)) + setFailed() + return False + else: + setMsg("The boot option of the disk is correct") + if int(DISK.size) < (1024 * 1024 * 1024 * int(disksize)): + try: + DISK.size = (1024 * 1024 * 1024 * int(disksize)) + setMsg("Updated the size of the disk.") + setChanged() + except Exception as e: + setMsg("Failed to update the size of the disk.") + setMsg(str(e)) + setFailed() + return False + elif int(DISK.size) < (1024 * 1024 * 1024 * int(disksize)): + setMsg("Shrinking disks is not supported") + setMsg(str(e)) + setFailed() + return False + else: + setMsg("The size of the disk is correct") + if str(DISK.interface) != str(diskinterface): + try: + DISK.interface = diskinterface + setMsg("Updated the interface of the disk.") + setChanged() + except Exception as e: + setMsg("Failed to update the interface of the disk.") + setMsg(str(e)) + setFailed() + return False + else: + setMsg("The interface of the disk is correct") + return True + + def set_NIC(self, vmname, nicname, newname, vlan, interface): + NIC = self.get_NIC(vmname, nicname) + VM = self.get_VM(vmname) + CLUSTER = self.get_cluster_byid(VM.cluster.id) + DC = self.get_DC_byid(CLUSTER.data_center.id) + NETWORK = self.get_network(str(DC.name), vlan) + checkFail() + if NIC.name != newname: + NIC.name = newname + setMsg('Updating iface name to ' + newname) + setChanged() + if str(NIC.network.id) != str(NETWORK.id): + NIC.set_network(NETWORK) + setMsg('Updating iface network to ' + vlan) + setChanged() + if NIC.interface != interface: + NIC.interface = interface + setMsg('Updating iface interface to ' + interface) + setChanged() + try: + NIC.update() + setMsg('iface has succesfully been updated.') + except Exception as e: + setMsg("Failed to update the iface.") + setMsg(str(e)) + setFailed() + return False + return True + + def set_DeleteProtection(self, vmname, del_prot): + VM = self.get_VM(vmname) + VM.delete_protected = del_prot + try: + VM.update() + setChanged() + except Exception as e: + setMsg("Failed to update delete protection.") + setMsg(str(e)) + setFailed() + return False + return True + + def set_BootOrder(self, vmname, boot_order): + VM = self.get_VM(vmname) + bootorder = [] + for device in boot_order: + bootorder.append(params.Boot(dev=device)) + VM.os.boot = bootorder + + try: + VM.update() + setChanged() + except Exception as e: + setMsg("Failed to update the boot order.") + setMsg(str(e)) + setFailed() + return False + return True + + def set_Host(self, host_name, cluster, ifaces): + HOST = self.get_Host(host_name) + CLUSTER = self.get_cluster(cluster) + + if HOST is None: + setMsg("Host does not exist.") + ifacelist = dict() + networklist = [] + manageip = '' + + try: + for iface in ifaces: + try: + setMsg('creating host interface ' + iface['name']) + if 'management' in iface.keys(): + manageip = iface['ip'] + if 'boot_protocol' not in iface.keys(): + if 'ip' in iface.keys(): + iface['boot_protocol'] = 'static' + else: + iface['boot_protocol'] = 'none' + if 'ip' not in iface.keys(): + iface['ip'] = '' + if 'netmask' not in iface.keys(): + iface['netmask'] = '' + if 'gateway' not in iface.keys(): + iface['gateway'] = '' + + if 'network' in iface.keys(): + if 'bond' in iface.keys(): + bond = [] + for slave in iface['bond']: + bond.append(ifacelist[slave]) + try: + tmpiface = params.Bonding( + slaves = params.Slaves(host_nic = bond), + options = params.Options( + option = [ + params.Option(name = 'miimon', value = '100'), + params.Option(name = 'mode', value = '4') + ] + ) + ) + except Exception as e: + setMsg('Failed to create the bond for ' + iface['name']) + setFailed() + setMsg(str(e)) + return False + try: + tmpnetwork = params.HostNIC( + network = params.Network(name = iface['network']), + name = iface['name'], + boot_protocol = iface['boot_protocol'], + ip = params.IP( + address = iface['ip'], + netmask = iface['netmask'], + gateway = iface['gateway'] + ), + override_configuration = True, + bonding = tmpiface) + networklist.append(tmpnetwork) + setMsg('Applying network ' + iface['name']) + except Exception as e: + setMsg('Failed to set' + iface['name'] + ' as network interface') + setFailed() + setMsg(str(e)) + return False + else: + tmpnetwork = params.HostNIC( + network = params.Network(name = iface['network']), + name = iface['name'], + boot_protocol = iface['boot_protocol'], + ip = params.IP( + address = iface['ip'], + netmask = iface['netmask'], + gateway = iface['gateway'] + )) + networklist.append(tmpnetwork) + setMsg('Applying network ' + iface['name']) + else: + tmpiface = params.HostNIC( + name=iface['name'], + network=params.Network(), + boot_protocol=iface['boot_protocol'], + ip=params.IP( + address=iface['ip'], + netmask=iface['netmask'], + gateway=iface['gateway'] + )) + ifacelist[iface['name']] = tmpiface + except Exception as e: + setMsg('Failed to set ' + iface['name']) + setFailed() + setMsg(str(e)) + return False + except Exception as e: + setMsg('Failed to set networks') + setMsg(str(e)) + setFailed() + return False + + if manageip == '': + setMsg('No management network is defined') + setFailed() + return False + + try: + HOST = params.Host(name=host_name, address=manageip, cluster=CLUSTER, ssh=params.SSH(authentication_method='publickey')) + if self.conn.hosts.add(HOST): + setChanged() + HOST = self.get_Host(host_name) + state = HOST.status.state + while (state != 'non_operational' and state != 'up'): + HOST = self.get_Host(host_name) + state = HOST.status.state + time.sleep(1) + if state == 'non_responsive': + setMsg('Failed to add host to RHEVM') + setFailed() + return False + + setMsg('status host: up') + time.sleep(5) + + HOST = self.get_Host(host_name) + state = HOST.status.state + setMsg('State before setting to maintenance: ' + str(state)) + HOST.deactivate() + while state != 'maintenance': + HOST = self.get_Host(host_name) + state = HOST.status.state + time.sleep(1) + setMsg('status host: maintenance') + + try: + HOST.nics.setupnetworks(params.Action( + force=True, + check_connectivity = False, + host_nics = params.HostNics(host_nic = networklist) + )) + setMsg('nics are set') + except Exception as e: + setMsg('Failed to apply networkconfig') + setFailed() + setMsg(str(e)) + return False + + try: + HOST.commitnetconfig() + setMsg('Network config is saved') + except Exception as e: + setMsg('Failed to save networkconfig') + setFailed() + setMsg(str(e)) + return False + except Exception as e: + if 'The Host name is already in use' in str(e): + setMsg("Host already exists") + else: + setMsg("Failed to add host") + setFailed() + setMsg(str(e)) + return False + + HOST.activate() + while state != 'up': + HOST = self.get_Host(host_name) + state = HOST.status.state + time.sleep(1) + if state == 'non_responsive': + setMsg('Failed to apply networkconfig.') + setFailed() + return False + setMsg('status host: up') + else: + setMsg("Host exists.") + + return True + + def del_NIC(self, vmname, nicname): + return self.get_NIC(vmname, nicname).delete() + + def remove_VM(self, vmname): + VM = self.get_VM(vmname) + try: + VM.delete() + except Exception as e: + setMsg("Failed to remove VM.") + setMsg(str(e)) + setFailed() + return False + return True + + def start_VM(self, vmname, timeout): + VM = self.get_VM(vmname) + try: + VM.start() + except Exception as e: + setMsg("Failed to start VM.") + setMsg(str(e)) + setFailed() + return False + return self.wait_VM(vmname, "up", timeout) + + def wait_VM(self, vmname, state, timeout): + VM = self.get_VM(vmname) + while VM.status.state != state: + VM = self.get_VM(vmname) + time.sleep(10) + if timeout is not False: + timeout -= 10 + if timeout <= 0: + setMsg("Timeout expired") + setFailed() + return False + return True + + def stop_VM(self, vmname, timeout): + VM = self.get_VM(vmname) + try: + VM.stop() + except Exception as e: + setMsg("Failed to stop VM.") + setMsg(str(e)) + setFailed() + return False + return self.wait_VM(vmname, "down", timeout) + + def set_CD(self, vmname, cd_drive): + VM = self.get_VM(vmname) + try: + if str(VM.status.state) == 'down': + cdrom = params.CdRom(file=cd_iso) + VM.cdroms.add(cdrom) + setMsg("Attached the image.") + setChanged() + else: + cdrom = VM.cdroms.get(id="00000000-0000-0000-0000-000000000000") + cdrom.set_file(cd_iso) + cdrom.update(current=True) + setMsg("Attached the image.") + setChanged() + except Exception as e: + setMsg("Failed to attach image.") + setMsg(str(e)) + setFailed() + return False + return True + + def set_VM_Host(self, vmname, vmhost): + VM = self.get_VM(vmname) + HOST = self.get_Host(vmhost) + try: + VM.placement_policy.host = HOST + VM.update() + setMsg("Set startup host to " + vmhost) + setChanged() + except Exception as e: + setMsg("Failed to set startup host.") + setMsg(str(e)) + setFailed() + return False + return True + + def migrate_VM(self, vmname, vmhost): + VM = self.get_VM(vmname) + + HOST = self.get_Host_byid(VM.host.id) + if str(HOST.name) != vmhost: + try: + vm.migrate( + action=params.Action( + host=params.Host( + name=vmhost, + ) + ), + ) + setChanged() + setMsg("VM migrated to " + vmhost) + except Exception as e: + setMsg("Failed to set startup host.") + setMsg(str(e)) + setFailed() + return False + return True + + def remove_CD(self, vmname): + VM = self.get_VM(vmname) + try: + VM.cdroms.get(id="00000000-0000-0000-0000-000000000000").delete() + setMsg("Removed the image.") + setChanged() + except Exception as e: + setMsg("Failed to remove the image.") + setMsg(str(e)) + setFailed() + return False + return True + + +class RHEV(object): + def __init__(self, module): + self.module = module + + def __get_conn(self): + self.conn = RHEVConn(self.module) + return self.conn + + def test(self): + self.__get_conn() + return "OK" + + def getVM(self, name): + self.__get_conn() + VM = self.conn.get_VM(name) + if VM: + vminfo = dict() + vminfo['uuid'] = VM.id + vminfo['name'] = VM.name + vminfo['status'] = VM.status.state + vminfo['cpu_cores'] = VM.cpu.topology.cores + vminfo['cpu_sockets'] = VM.cpu.topology.sockets + vminfo['cpu_shares'] = VM.cpu_shares + vminfo['memory'] = (int(VM.memory) / 1024 / 1024 / 1024) + vminfo['mem_pol'] = (int(VM.memory_policy.guaranteed) / 1024 / 1024 / 1024) + vminfo['os'] = VM.get_os().type_ + vminfo['del_prot'] = VM.delete_protected + try: + vminfo['host'] = str(self.conn.get_Host_byid(str(VM.host.id)).name) + except Exception as e: + vminfo['host'] = None + vminfo['boot_order'] = [] + for boot_dev in VM.os.get_boot(): + vminfo['boot_order'].append(str(boot_dev.dev)) + vminfo['disks'] = [] + for DISK in VM.disks.list(): + disk = dict() + disk['name'] = DISK.name + disk['size'] = (int(DISK.size) / 1024 / 1024 / 1024) + disk['domain'] = str((self.conn.get_domain_byid(DISK.get_storage_domains().get_storage_domain()[0].id)).name) + disk['interface'] = DISK.interface + vminfo['disks'].append(disk) + vminfo['ifaces'] = [] + for NIC in VM.nics.list(): + iface = dict() + iface['name'] = str(NIC.name) + iface['vlan'] = str(self.conn.get_network_byid(NIC.get_network().id).name) + iface['interface'] = NIC.interface + iface['mac'] = NIC.mac.address + vminfo['ifaces'].append(iface) + vminfo[str(NIC.name)] = NIC.mac.address + CLUSTER = self.conn.get_cluster_byid(VM.cluster.id) + if CLUSTER: + vminfo['cluster'] = CLUSTER.name + else: + vminfo = False + return vminfo + + def createVMimage(self, name, cluster, template, disks): + self.__get_conn() + return self.conn.createVMimage(name, cluster, template, disks) + + def createVM(self, name, cluster, os, actiontype): + self.__get_conn() + return self.conn.createVM(name, cluster, os, actiontype) + + def setMemory(self, name, memory): + self.__get_conn() + return self.conn.set_Memory(name, memory) + + def setMemoryPolicy(self, name, memory_policy): + self.__get_conn() + return self.conn.set_Memory_Policy(name, memory_policy) + + def setCPU(self, name, cpu): + self.__get_conn() + return self.conn.set_CPU(name, cpu) + + def setCPUShare(self, name, cpu_share): + self.__get_conn() + return self.conn.set_CPU_share(name, cpu_share) + + def setDisks(self, name, disks): + self.__get_conn() + counter = 0 + bootselect = False + for disk in disks: + if 'bootable' in disk: + if disk['bootable'] is True: + bootselect = True + + for disk in disks: + diskname = name + "_Disk" + str(counter) + "_" + disk.get('name', '').replace('/', '_') + disksize = disk.get('size', 1) + diskdomain = disk.get('domain', None) + if diskdomain is None: + setMsg("`domain` is a required disk key.") + setFailed() + return False + diskinterface = disk.get('interface', 'virtio') + diskformat = disk.get('format', 'raw') + diskallocationtype = disk.get('thin', False) + diskboot = disk.get('bootable', False) + + if bootselect is False and counter == 0: + diskboot = True + + DISK = self.conn.get_disk(diskname) + + if DISK is None: + self.conn.createDisk(name, diskname, disksize, diskdomain, diskinterface, diskformat, diskallocationtype, diskboot) + else: + self.conn.set_Disk(diskname, disksize, diskinterface, diskboot) + checkFail() + counter += 1 + + return True + + def setNetworks(self, vmname, ifaces): + self.__get_conn() + VM = self.conn.get_VM(vmname) + + counter = 0 + length = len(ifaces) + + for NIC in VM.nics.list(): + if counter < length: + iface = ifaces[counter] + name = iface.get('name', None) + if name is None: + setMsg("`name` is a required iface key.") + setFailed() + elif str(name) != str(NIC.name): + setMsg("ifaces are in the wrong order, rebuilding everything.") + for NIC in VM.nics.list(): + self.conn.del_NIC(vmname, NIC.name) + self.setNetworks(vmname, ifaces) + checkFail() + return True + vlan = iface.get('vlan', None) + if vlan is None: + setMsg("`vlan` is a required iface key.") + setFailed() + checkFail() + interface = iface.get('interface', 'virtio') + self.conn.set_NIC(vmname, str(NIC.name), name, vlan, interface) + else: + self.conn.del_NIC(vmname, NIC.name) + counter += 1 + checkFail() + + while counter < length: + iface = ifaces[counter] + name = iface.get('name', None) + if name is None: + setMsg("`name` is a required iface key.") + setFailed() + vlan = iface.get('vlan', None) + if vlan is None: + setMsg("`vlan` is a required iface key.") + setFailed() + if failed is True: + return False + interface = iface.get('interface', 'virtio') + self.conn.createNIC(vmname, name, vlan, interface) + + counter += 1 + checkFail() + return True + + def setDeleteProtection(self, vmname, del_prot): + self.__get_conn() + VM = self.conn.get_VM(vmname) + if bool(VM.delete_protected) != bool(del_prot): + self.conn.set_DeleteProtection(vmname, del_prot) + checkFail() + setMsg("`delete protection` has been updated.") + else: + setMsg("`delete protection` already has the right value.") + return True + + def setBootOrder(self, vmname, boot_order): + self.__get_conn() + VM = self.conn.get_VM(vmname) + bootorder = [] + for boot_dev in VM.os.get_boot(): + bootorder.append(str(boot_dev.dev)) + + if boot_order != bootorder: + self.conn.set_BootOrder(vmname, boot_order) + setMsg('The boot order has been set') + else: + setMsg('The boot order has already been set') + return True + + def removeVM(self, vmname): + self.__get_conn() + self.setPower(vmname, "down", 300) + return self.conn.remove_VM(vmname) + + def setPower(self, vmname, state, timeout): + self.__get_conn() + VM = self.conn.get_VM(vmname) + if VM is None: + setMsg("VM does not exist.") + setFailed() + return False + + if state == VM.status.state: + setMsg("VM state was already " + state) + else: + if state == "up": + setMsg("VM is going to start") + self.conn.start_VM(vmname, timeout) + setChanged() + elif state == "down": + setMsg("VM is going to stop") + self.conn.stop_VM(vmname, timeout) + setChanged() + elif state == "restarted": + self.setPower(vmname, "down", timeout) + checkFail() + self.setPower(vmname, "up", timeout) + checkFail() + setMsg("the vm state is set to " + state) + return True + + def setCD(self, vmname, cd_drive): + self.__get_conn() + if cd_drive: + return self.conn.set_CD(vmname, cd_drive) + else: + return self.conn.remove_CD(vmname) + + def setVMHost(self, vmname, vmhost): + self.__get_conn() + return self.conn.set_VM_Host(vmname, vmhost) + + VM = self.conn.get_VM(vmname) + HOST = self.conn.get_Host(vmhost) + + if VM.placement_policy.host is None: + self.conn.set_VM_Host(vmname, vmhost) + elif str(VM.placement_policy.host.id) != str(HOST.id): + self.conn.set_VM_Host(vmname, vmhost) + else: + setMsg("VM's startup host was already set to " + vmhost) + checkFail() + + if str(VM.status.state) == "up": + self.conn.migrate_VM(vmname, vmhost) + checkFail() + + return True + + def setHost(self, hostname, cluster, ifaces): + self.__get_conn() + return self.conn.set_Host(hostname, cluster, ifaces) + + +def checkFail(): + if failed: + module.fail_json(msg=msg) + else: + return True + + +def setFailed(): + global failed + failed = True + + +def setChanged(): + global changed + changed = True + + +def setMsg(message): + global failed + msg.append(message) + + +def core(module): + + r = RHEV(module) + + state = module.params.get('state', 'present') + + if state == 'ping': + r.test() + return RHEV_SUCCESS, {"ping": "pong"} + elif state == 'info': + name = module.params.get('name') + if not name: + setMsg("`name` is a required argument.") + return RHEV_FAILED, msg + vminfo = r.getVM(name) + return RHEV_SUCCESS, {'changed': changed, 'msg': msg, 'vm': vminfo} + elif state == 'present': + created = False + name = module.params.get('name') + if not name: + setMsg("`name` is a required argument.") + return RHEV_FAILED, msg + actiontype = module.params.get('type') + if actiontype == 'server' or actiontype == 'desktop': + vminfo = r.getVM(name) + if vminfo: + setMsg('VM exists') + else: + # Create VM + cluster = module.params.get('cluster') + if cluster is None: + setMsg("cluster is a required argument.") + setFailed() + template = module.params.get('image') + if template: + disks = module.params.get('disks') + if disks is None: + setMsg("disks is a required argument.") + setFailed() + checkFail() + if r.createVMimage(name, cluster, template, disks) is False: + return RHEV_FAILED, vminfo + else: + os = module.params.get('osver') + if os is None: + setMsg("osver is a required argument.") + setFailed() + checkFail() + if r.createVM(name, cluster, os, actiontype) is False: + return RHEV_FAILED, vminfo + created = True + + # Set MEMORY and MEMORY POLICY + vminfo = r.getVM(name) + memory = module.params.get('vmmem') + if memory is not None: + memory_policy = module.params.get('mempol') + if int(memory_policy) == 0: + memory_policy = memory + mem_pol_nok = True + if int(vminfo['mem_pol']) == int(memory_policy): + setMsg("Memory is correct") + mem_pol_nok = False + + mem_nok = True + if int(vminfo['memory']) == int(memory): + setMsg("Memory is correct") + mem_nok = False + + if memory_policy > memory: + setMsg('memory_policy cannot have a higher value than memory.') + return RHEV_FAILED, msg + + if mem_nok and mem_pol_nok: + if int(memory_policy) > int(vminfo['memory']): + r.setMemory(vminfo['name'], memory) + r.setMemoryPolicy(vminfo['name'], memory_policy) + else: + r.setMemoryPolicy(vminfo['name'], memory_policy) + r.setMemory(vminfo['name'], memory) + elif mem_nok: + r.setMemory(vminfo['name'], memory) + elif mem_pol_nok: + r.setMemoryPolicy(vminfo['name'], memory_policy) + checkFail() + + # Set CPU + cpu = module.params.get('vmcpu') + if int(vminfo['cpu_cores']) == int(cpu): + setMsg("Number of CPUs is correct") + else: + if r.setCPU(vminfo['name'], cpu) is False: + return RHEV_FAILED, msg + + # Set CPU SHARE + cpu_share = module.params.get('cpu_share') + if cpu_share is not None: + if int(vminfo['cpu_shares']) == int(cpu_share): + setMsg("CPU share is correct.") + else: + if r.setCPUShare(vminfo['name'], cpu_share) is False: + return RHEV_FAILED, msg + + # Set DISKS + disks = module.params.get('disks') + if disks is not None: + if r.setDisks(vminfo['name'], disks) is False: + return RHEV_FAILED, msg + + # Set NETWORKS + ifaces = module.params.get('ifaces', None) + if ifaces is not None: + if r.setNetworks(vminfo['name'], ifaces) is False: + return RHEV_FAILED, msg + + # Set Delete Protection + del_prot = module.params.get('del_prot') + if r.setDeleteProtection(vminfo['name'], del_prot) is False: + return RHEV_FAILED, msg + + # Set Boot Order + boot_order = module.params.get('boot_order') + if r.setBootOrder(vminfo['name'], boot_order) is False: + return RHEV_FAILED, msg + + # Set VM Host + vmhost = module.params.get('vmhost') + if vmhost is not False and vmhost is not "False": + if r.setVMHost(vminfo['name'], vmhost) is False: + return RHEV_FAILED, msg + + vminfo = r.getVM(name) + vminfo['created'] = created + return RHEV_SUCCESS, {'changed': changed, 'msg': msg, 'vm': vminfo} + + if actiontype == 'host': + cluster = module.params.get('cluster') + if cluster is None: + setMsg("cluster is a required argument.") + setFailed() + ifaces = module.params.get('ifaces') + if ifaces is None: + setMsg("ifaces is a required argument.") + setFailed() + if r.setHost(name, cluster, ifaces) is False: + return RHEV_FAILED, msg + return RHEV_SUCCESS, {'changed': changed, 'msg': msg} + + elif state == 'absent': + name = module.params.get('name') + if not name: + setMsg("`name` is a required argument.") + return RHEV_FAILED, msg + actiontype = module.params.get('type') + if actiontype == 'server' or actiontype == 'desktop': + vminfo = r.getVM(name) + if vminfo: + setMsg('VM exists') + + # Set Delete Protection + del_prot = module.params.get('del_prot') + if r.setDeleteProtection(vminfo['name'], del_prot) is False: + return RHEV_FAILED, msg + + # Remove VM + if r.removeVM(vminfo['name']) is False: + return RHEV_FAILED, msg + setMsg('VM has been removed.') + vminfo['state'] = 'DELETED' + else: + setMsg('VM was already removed.') + return RHEV_SUCCESS, {'changed': changed, 'msg': msg, 'vm': vminfo} + + elif state == 'up' or state == 'down' or state == 'restarted': + name = module.params.get('name') + if not name: + setMsg("`name` is a required argument.") + return RHEV_FAILED, msg + timeout = module.params.get('timeout') + if r.setPower(name, state, timeout) is False: + return RHEV_FAILED, msg + vminfo = r.getVM(name) + return RHEV_SUCCESS, {'changed': changed, 'msg': msg, 'vm': vminfo} + + elif state == 'cd': + name = module.params.get('name') + cd_drive = module.params.get('cd_drive') + if r.setCD(name, cd_drive) is False: + return RHEV_FAILED, msg + return RHEV_SUCCESS, {'changed': changed, 'msg': msg} + + +def main(): + global module + module = AnsibleModule( + argument_spec = dict( + state = dict(default='present', choices=['ping', 'present', 'absent', 'up', 'down', 'restarted', 'cd', 'info']), + user = dict(default="admin@internal"), + password = dict(required=True), + server = dict(default="127.0.0.1"), + port = dict(default="443"), + insecure_api = dict(default=False, type='bool'), + name = dict(), + image = dict(default=False), + datacenter = dict(default="Default"), + type = dict(default="server", choices=['server', 'desktop', 'host']), + cluster = dict(default=''), + vmhost = dict(default=False), + vmcpu = dict(default="2"), + vmmem = dict(default="1"), + disks = dict(), + osver = dict(default="rhel_6x64"), + ifaces = dict(aliases=['nics', 'interfaces']), + timeout = dict(default=False), + mempol = dict(default="1"), + vm_ha = dict(default=True), + cpu_share = dict(default="0"), + boot_order = dict(default=["network", "hd"]), + del_prot = dict(default=True, type="bool"), + cd_drive = dict(default=False) + ), + ) + + if not HAS_SDK: + module.fail_json( + msg='The `ovirtsdk` module is not importable. Check the requirements.' + ) + + rc = RHEV_SUCCESS + try: + rc, result = core(module) + except Exception as e: + module.fail_json(msg=str(e)) + + if rc != 0: # something went wrong emit the msg + module.fail_json(rc=rc, msg=result) + else: + module.exit_json(**result) + + +# import module snippets +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From f9b97726da5d7fb6bdfbc4f056301495bc948588 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Fri, 4 Nov 2016 08:37:21 -0700 Subject: [PATCH 2310/2522] Fix for dnf groupinstall Previous fix to group install introduced a different bug trying to strip() group names at the wrong level. This patch fixes that. Fixes #3358 --- packaging/os/dnf.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index 504a9dac5c6..98616da8478 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -297,14 +297,14 @@ def ensure(module, base, state, names): filenames = [f.strip() for f in filenames] groups = [] environments = [] - for group_spec in group_specs: + for group_spec in (g.strip() for g in group_specs): group = base.comps.group_by_pattern(group_spec) if group: - groups.append(group.strip()) + groups.append(group) else: - environment = base.comps.environments_by_pattern(group_spec) + environment = base.comps.environment_by_pattern(group_spec) if environment: - environments.extend((e.id.strip() for e in environment)) + environments.append(environment.id) else: module.fail_json( msg="No group {} available.".format(group_spec)) @@ -369,11 +369,21 @@ def ensure(module, base, state, names): module.fail_json( msg="Cannot remove paths -- please specify package name.") - installed = base.sack.query().installed() for group in groups: - if installed.filter(name=group.name): + try: base.group_remove(group) + except dnf.exceptions.CompsError: + # Group is already uninstalled. + pass + for envioronment in environments: + try: + base.environment_remove(environment) + except dnf.exceptions.CompsError: + # Environment is already uninstalled. + pass + + installed = base.sack.query().installed() for pkg_spec in pkg_specs: if installed.filter(name=pkg_spec): base.remove(pkg_spec) From cc0d3cb15fafc1b570b5e4854ec8ee4eaa170dd8 Mon Sep 17 00:00:00 2001 From: Michael Ansel Date: Sat, 5 Nov 2016 03:17:49 -0700 Subject: [PATCH 2311/2522] jira: Specify the correct argument type (#3368) By default, all arguments are considered strings, but the module code expects the `fields` parameter to be a proper Python dictionary. Fixes #2600 --- web_infrastructure/jira.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_infrastructure/jira.py b/web_infrastructure/jira.py index 0053e0a32cd..85b988d24bc 100755 --- a/web_infrastructure/jira.py +++ b/web_infrastructure/jira.py @@ -311,7 +311,7 @@ def main(): comment=dict(), status=dict(), assignee=dict(), - fields=dict(default={}) + fields=dict(default={}, type='dict') ), supports_check_mode=False ) From 63e6d3a4601c191ea40ec70ce1587aa53492683d Mon Sep 17 00:00:00 2001 From: Tom Paine Date: Wed, 2 Nov 2016 11:29:12 +0000 Subject: [PATCH 2312/2522] Update letsencrypt.py Minor spelling and TLA case. --- web_infrastructure/letsencrypt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web_infrastructure/letsencrypt.py b/web_infrastructure/letsencrypt.py index a43014a8ab4..10e9f8dc417 100644 --- a/web_infrastructure/letsencrypt.py +++ b/web_infrastructure/letsencrypt.py @@ -204,7 +204,7 @@ def get_cert_days(module,cert_file): except AttributeError: module.fail_json(msg="No 'Not after' date found in {0}".format(cert_file)) except ValueError: - module.fail_json(msg="Faild to parse 'Not after' date of {0}".format(cert_file)) + module.fail_json(msg="Failed to parse 'Not after' date of {0}".format(cert_file)) now = datetime.datetime.utcnow() return (not_after - now).days @@ -261,8 +261,8 @@ def write_file(module, dest, content): class ACMEDirectory(object): ''' The ACME server directory. Gives access to the available resources - and the Replay-Nonce for a given uri. This only works for - uris that permit GET requests (so normally not the ones that + and the Replay-Nonce for a given URI. This only works for + URIs that permit GET requests (so normally not the ones that require authentication). https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.2 ''' From c80f12c4c9e751350e966de9d89ea7a0beb1bfc7 Mon Sep 17 00:00:00 2001 From: Tom Paine Date: Wed, 2 Nov 2016 12:44:09 +0000 Subject: [PATCH 2313/2522] Update letsencrypt.py Extend `remaining_days` description. --- web_infrastructure/letsencrypt.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web_infrastructure/letsencrypt.py b/web_infrastructure/letsencrypt.py index 10e9f8dc417..74f6a64d6d7 100644 --- a/web_infrastructure/letsencrypt.py +++ b/web_infrastructure/letsencrypt.py @@ -103,8 +103,10 @@ alias: ['cert'] remaining_days: description: - - "The number of days the certificate must have left being valid before it - will be renewed." + - "The number of days the certificate must have left being valid. + If C(remaining_days < cert_days), then it will be renewed. + If the certificate is not renewed, module return values will not + include C(challenge_data)." required: false default: 10 ''' From d6b4023a15490585305fc00df3c3226f8af7c036 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 7 Nov 2016 09:53:26 -0800 Subject: [PATCH 2314/2522] Remove need for translate/maketrans due to py3 differences * translate() has a different api for text vs byte strings * maketrans must be imported from a different location on py2 vs py3 Since this is such a small string outside of a loop we don't have to worry too much about speed so it's better to have a single piece of code that works on both py2 and py3 --- cloud/vmware/vmware_guest.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/cloud/vmware/vmware_guest.py b/cloud/vmware/vmware_guest.py index 751cac93856..99a13b6f525 100644 --- a/cloud/vmware/vmware_guest.py +++ b/cloud/vmware/vmware_guest.py @@ -161,7 +161,6 @@ pass import os -import string import time from ansible.module_utils.urls import fetch_url @@ -631,11 +630,7 @@ def deploy_template(self, poweron=False, wait_for_ip=False): if [x for x in pspec.keys() if x.startswith('size_') or x == 'size']: # size_tb, size_gb, size_mb, size_kb, size_b ...? if 'size' in pspec: - # http://stackoverflow.com/a/1451407 - trans = string.maketrans('', '') - chars = trans.translate(trans, string.digits) - expected = pspec['size'].translate(trans, chars) - expected = expected + expected = ''.join(c for c in pspec['size'] if c.isdigit()) unit = pspec['size'].replace(expected, '').lower() expected = int(expected) else: From 3d5709983326ceff14cc72bca72dafcd7b8bd6a8 Mon Sep 17 00:00:00 2001 From: Adam Miller Date: Thu, 6 Oct 2016 16:38:39 -0500 Subject: [PATCH 2315/2522] provide useful error when invalid service name provided add offline mode to firewalld permanent operations Signed-off-by: Adam Miller --- system/firewalld.py | 396 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 349 insertions(+), 47 deletions(-) diff --git a/system/firewalld.py b/system/firewalld.py index f3ba6eb065a..5aae5fc0a79 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -106,7 +106,13 @@ from ansible.module_utils.basic import AnsibleModule +import sys + +##################### +# Globals +# fw = None +module = None fw_offline = False Rich_Rule = None FirewallClientZoneSettings = None @@ -114,9 +120,37 @@ module = None ##################### -# fw_offline helpers +# exception handling # +def action_handler(action_func, action_func_args): + """ + Function to wrap calls to make actions on firewalld in try/except + logic and emit (hopefully) useful error messages + """ + + msgs = [] + + try: + return action_func(*action_func_args) + except Exception: + # Make python 2.4 shippable ci tests happy + e = sys.exc_info()[1] + + # If there are any commonly known errors that we should provide more + # context for to help the users diagnose what's wrong. Handle that here + if "INVALID_SERVICE" in "%s" % e: + msgs.append("Services are defined by port/tcp relationship and named as they are in /etc/services (on most systems)") + + if len(msgs) > 0: + module.fail_json( + msg='ERROR: Exception caught: %s %s' % (e, ', '.join(msgs)) + ) + else: + module.fail_json(msg='ERROR: Exception caught: %s' % e) +##################### +# fw_offline helpers +# def get_fw_zone_settings(zone): if fw_offline: fw_zone = fw.config.get_zone(zone) @@ -151,7 +185,6 @@ def get_masquerade_enabled_permanent(zone): else: return False - def set_masquerade_enabled(zone): fw.addMasquerade(zone) @@ -364,10 +397,12 @@ def set_rich_rule_disabled_permanent(zone, rule): fw_settings.removeRichRule(rule) update_fw_settings(fw_zone, fw_settings) - def main(): global module + ## make module global so we don't have to pass it to action_handler every + ## function call + global module module = AnsibleModule( argument_spec = dict( service=dict(required=False,default=None), @@ -381,6 +416,7 @@ def main(): timeout=dict(type='int',required=False,default=0), interface=dict(required=False,default=None), masquerade=dict(required=False,default=None), + offline=dict(type='bool',required=False,default=None), ), supports_check_mode=True ) @@ -398,7 +434,6 @@ def main(): from firewall.client import Rich_Rule from firewall.client import FirewallClient - HAS_FIREWALLD = True fw = None fw_offline = False @@ -419,10 +454,9 @@ def main(): fw_offline = True except ImportError: - HAS_FIREWALLD = False - - if not HAS_FIREWALLD: - module.fail_json(msg='firewalld and its python 2 module are required for this module, version 2.0.11 or newer required (3.0.9 or newer for offline operations)') + ## Make python 2.4 shippable ci tests happy + e = sys.exc_info()[1] + module.fail_json(msg='firewalld and its python 2 module are required for this module, version 2.0.11 or newer required (3.0.9 or newer for offline operations) \n %s' % e) if fw_offline: ## Pre-run version checking @@ -497,8 +531,57 @@ def main(): module.fail_json(msg='can only operate on port, service, rich_rule or interface at once') if service != None: - if permanent: - is_enabled = get_service_enabled_permanent(zone, service) + if immediate and permanent: + is_enabled_permanent = action_handler( + get_service_enabled_permanent, + (zone, service) + ) + is_enabled_immediate = action_handler( + get_service_enabled, + (zone, service) + ) + msgs.append('Permanent and Non-Permanent(immediate) operation') + + if desired_state == "enabled": + if not is_enabled_permanent or not is_enabled_immediate: + if module.check_mode: + module.exit_json(changed=True) + if not is_enabled_permanent: + action_handler( + set_service_enabled_permanent, + (zone, service) + ) + changed=True + if not is_enabled_immediate: + action_handler( + set_service_enabled, + (zone, service, timeout) + ) + changed=True + + + elif desired_state == "disabled": + if is_enabled_permanent or is_enabled_immediate: + if module.check_mode: + module.exit_json(changed=True) + if is_enabled_permanent: + action_handler( + set_service_disabled_permanent, + (zone, service) + ) + changed=True + if is_enabled_immediate: + action_handler( + set_service_disabled, + (zone, service) + ) + changed=True + + elif permanent and not immediate: + is_enabled = action_handler( + get_service_enabled_permanent, + (zone, service) + ) msgs.append('Permanent operation') if desired_state == "enabled": @@ -506,17 +589,26 @@ def main(): if module.check_mode: module.exit_json(changed=True) - set_service_enabled_permanent(zone, service) + action_handler( + set_service_enabled_permanent, + (zone, service) + ) changed=True elif desired_state == "disabled": if is_enabled == True: if module.check_mode: module.exit_json(changed=True) - set_service_disabled_permanent(zone, service) + action_handler( + set_service_disabled_permanent, + (zone, service) + ) changed=True - if immediate or not permanent: - is_enabled = get_service_enabled(zone, service) + elif immediate and not permanent: + is_enabled = action_handler( + get_service_enabled, + (zone, service) + ) msgs.append('Non-permanent operation') @@ -525,27 +617,35 @@ def main(): if module.check_mode: module.exit_json(changed=True) - set_service_enabled(zone, service, timeout) + action_handler( + set_service_enabled, + (zone, service, timeout) + ) changed=True elif desired_state == "disabled": if is_enabled == True: if module.check_mode: module.exit_json(changed=True) - set_service_disabled(zone, service) + action_handler( + set_service_disabled, + (zone, service) + ) changed=True if changed == True: msgs.append("Changed service %s to %s" % (service, desired_state)) + # FIXME - source type does not handle non-permanent mode, this was an + # oversight in the past. if source != None: - is_enabled = get_source(zone, source) + is_enabled = action_handler(get_source, (zone, source)) if desired_state == "enabled": if is_enabled == False: if module.check_mode: module.exit_json(changed=True) - add_source(zone, source) + action_handler(add_source, (zone, source)) changed=True msgs.append("Added %s to zone %s" % (source, zone)) elif desired_state == "disabled": @@ -553,13 +653,61 @@ def main(): if module.check_mode: module.exit_json(changed=True) - remove_source(zone, source) + action_handler(remove_source, (zone, source)) changed=True msgs.append("Removed %s from zone %s" % (source, zone)) if port != None: - if permanent: - is_enabled = get_port_enabled_permanent(zone, [port, protocol]) + if immediate and permanent: + is_enabled_permanent = action_handler( + get_port_enabled_permanent, + (zone,[port, protocol]) + ) + is_enabled_immediate = action_handler( + get_port_enabled, + (zone, [port, protocol]) + ) + msgs.append('Permanent and Non-Permanent(immediate) operation') + + if desired_state == "enabled": + if not is_enabled_permanent or not is_enabled_immediate: + if module.check_mode: + module.exit_json(changed=True) + if not is_enabled_permanent: + action_handler( + set_port_enabled_permanent, + (zone, port, protocol) + ) + changed=True + if not is_enabled_immediate: + action_handler( + set_port_enabled, + (zone, port, protocol, timeout) + ) + changed=True + + elif desired_state == "disabled": + if is_enabled_permanent or is_enabled_immediate: + if module.check_mode: + module.exit_json(changed=True) + if is_enabled_permanent: + action_handler( + set_port_disabled_permanent, + (zone, port, protocol) + ) + changed=True + if is_enabled_immediate: + action_handler( + set_port_disabled, + (zone, port, protocol) + ) + changed=True + + elif permanent and not immediate: + is_enabled = action_handler( + get_port_enabled_permanent, + (zone, [port, protocol]) + ) msgs.append('Permanent operation') if desired_state == "enabled": @@ -567,17 +715,26 @@ def main(): if module.check_mode: module.exit_json(changed=True) - set_port_enabled_permanent(zone, port, protocol) + action_handler( + set_port_enabled_permanent, + (zone, port, protocol) + ) changed=True elif desired_state == "disabled": if is_enabled == True: if module.check_mode: module.exit_json(changed=True) - set_port_disabled_permanent(zone, port, protocol) + action_handler( + set_port_disabled_permanent, + (zone, port, protocol) + ) changed=True - if immediate or not permanent: - is_enabled = get_port_enabled(zone, [port,protocol]) + if immediate and not permanent: + is_enabled = action_handler( + get_port_enabled, + (zone, [port,protocol]) + ) msgs.append('Non-permanent operation') if desired_state == "enabled": @@ -585,14 +742,20 @@ def main(): if module.check_mode: module.exit_json(changed=True) - set_port_enabled(zone, port, protocol, timeout) + action_handler( + set_port_enabled, + (zone, port, protocol, timeout) + ) changed=True elif desired_state == "disabled": if is_enabled == True: if module.check_mode: module.exit_json(changed=True) - set_port_disabled(zone, port, protocol) + action_handler( + set_port_disabled, + (zone, port, protocol) + ) changed=True if changed == True: @@ -600,8 +763,55 @@ def main(): desired_state)) if rich_rule != None: - if permanent: - is_enabled = get_rich_rule_enabled_permanent(zone, rich_rule) + if immediate and permanent: + is_enabled_permanent = action_handler( + get_rich_rule_enabled_permanent, + (zone, rich_rule) + ) + is_enabled_immediate = action_handler( + get_rich_rule_enabled, + (zone, rich_rule) + ) + msgs.append('Permanent and Non-Permanent(immediate) operation') + + if desired_state == "enabled": + if not is_enabled_permanent or not is_enabled_immediate: + if module.check_mode: + module.exit_json(changed=True) + if not is_enabled_permanent: + action_handler( + set_rich_rule_enabled_permanent, + (zone, rich_rule) + ) + changed=True + if not is_enabled_immediate: + action_handler( + set_rich_rule_enabled, + (zone, rich_rule, timeout) + ) + changed=True + + elif desired_state == "disabled": + if is_enabled_permanent or is_enabled_immediate: + if module.check_mode: + module.exit_json(changed=True) + if is_enabled_permanent: + action_handler( + set_rich_rule_disabled_permanent, + (zone, rich_rule) + ) + changed=True + if is_enabled_immediate: + action_handler( + set_rich_rule_disabled, + (zone, rich_rule) + ) + changed=True + if permanent and not immediate: + is_enabled = action_handler( + get_rich_rule_enabled_permanent, + (zone, rich_rule) + ) msgs.append('Permanent operation') if desired_state == "enabled": @@ -609,17 +819,26 @@ def main(): if module.check_mode: module.exit_json(changed=True) - set_rich_rule_enabled_permanent(zone, rich_rule) + action_handler( + set_rich_rule_enabled_permanent, + (zone, rich_rule) + ) changed=True elif desired_state == "disabled": if is_enabled == True: if module.check_mode: module.exit_json(changed=True) - set_rich_rule_disabled_permanent(zone, rich_rule) + action_handler( + set_rich_rule_disabled_permanent, + (zone, rich_rule) + ) changed=True - if immediate or not permanent: - is_enabled = get_rich_rule_enabled(zone, rich_rule) + if immediate and not permanent: + is_enabled = action_handler( + get_rich_rule_enabled, + (zone, rich_rule) + ) msgs.append('Non-permanent operation') if desired_state == "enabled": @@ -627,22 +846,68 @@ def main(): if module.check_mode: module.exit_json(changed=True) - set_rich_rule_enabled(zone, rich_rule, timeout) + action_handler( + set_rich_rule_enabled, + (zone, rich_rule, timeout) + ) changed=True elif desired_state == "disabled": if is_enabled == True: if module.check_mode: module.exit_json(changed=True) - set_rich_rule_disabled(zone, rich_rule) + action_handler( + set_rich_rule_disabled, + (zone, rich_rule) + ) changed=True if changed == True: msgs.append("Changed rich_rule %s to %s" % (rich_rule, desired_state)) if interface != None: - if permanent: - is_enabled = get_interface_permanent(zone, interface) + if immediate and permanent: + is_enabled_permanent = action_handler( + get_interface_permanent, + (zone, interface) + ) + is_enabled_immediate = action_handler( + get_interface, + (zone, interface) + ) + msgs.append('Permanent and Non-Permanent(immediate) operation') + + if desired_state == "enabled": + if not is_enabled_permanent or not is_enabled_immediate: + if module.check_mode: + module.exit_json(changed=True) + if not is_enabled_permanent: + change_zone_of_interface_permanent(zone, interface) + changed=True + if not is_enabled_immediate: + change_zone_of_interface(zone, interface) + changed=True + if changed: + msgs.append("Changed %s to zone %s" % (interface, zone)) + + elif desired_state == "disabled": + if is_enabled_permanent or is_enabled_immediate: + if module.check_mode: + module.exit_json(changed=True) + if is_enabled_permanent: + remove_interface_permanent(zone, interface) + changed=True + if is_enabled_immediate: + remove_interface(zone, interface) + changed=True + if changed: + msgs.append("Removed %s from zone %s" % (interface, zone)) + + elif permanent and not immediate: + is_enabled = action_handler( + get_interface_permanent, + (zone, interface) + ) msgs.append('Permanent operation') if desired_state == "enabled": if is_enabled == False: @@ -660,8 +925,11 @@ def main(): remove_interface_permanent(zone, interface) changed=True msgs.append("Removed %s from zone %s" % (interface, zone)) - if immediate or not permanent: - is_enabled = get_interface(zone, interface) + elif immediate and not permanent: + is_enabled = action_handler( + get_interface, + (zone, interface) + ) msgs.append('Non-permanent operation') if desired_state == "enabled": if is_enabled == False: @@ -682,8 +950,42 @@ def main(): if masquerade != None: - if permanent: - is_enabled = get_masquerade_enabled_permanent(zone) + if immediate and permanent: + is_enabled_permanent = action_handler( + get_masquerade_enabled_permanent, + (zone) + ) + is_enabled_immediate = action_handler(get_masquerade_enabled, (zone)) + msgs.append('Permanent and Non-Permanent(immediate) operation') + + if desired_state == "enabled": + if not is_enabled_permanent or not is_enabled_immediate: + if module.check_mode: + module.exit_json(changed=True) + if not is_enabled_permanent: + action_handler(set_masquerade_permanent, (zone, True)) + changed=True + if not is_enabled_immediate: + action_handler(set_masquerade_enabled, (zone)) + changed=True + if changed: + msgs.append("Added masquerade to zone %s" % (zone)) + + elif desired_state == "disabled": + if is_enabled_permanent or is_enabled_immediate: + if module.check_mode: + module.exit_json(changed=True) + if is_enabled_permanent: + action_handler(set_masquerade_permanent, (zone, False)) + changed=True + if is_enabled_immediate: + action_handler(set_masquerade_disabled, (zone)) + changed=True + if changed: + msgs.append("Removed masquerade from zone %s" % (zone)) + + elif permanent and not immediate: + is_enabled = action_handler(get_masquerade_enabled_permanent, (zone)) msgs.append('Permanent operation') if desired_state == "enabled": @@ -691,7 +993,7 @@ def main(): if module.check_mode: module.exit_json(changed=True) - set_masquerade_permanent(zone, True) + action_handler(set_masquerade_permanent, (zone, True)) changed=True msgs.append("Added masquerade to zone %s" % (zone)) elif desired_state == "disabled": @@ -699,11 +1001,11 @@ def main(): if module.check_mode: module.exit_json(changed=True) - set_masquerade_permanent(zone, False) + action_handler(set_masquerade_permanent, (zone, False)) changed=True msgs.append("Removed masquerade from zone %s" % (zone)) - if immediate or not permanent: - is_enabled = get_masquerade_enabled(zone) + elif immediate and not permanent: + is_enabled = action_handler(get_masquerade_enabled, (zone)) msgs.append('Non-permanent operation') if desired_state == "enabled": @@ -711,7 +1013,7 @@ def main(): if module.check_mode: module.exit_json(changed=True) - set_masquerade_enabled(zone) + action_handler(set_masquerade_enabled, (zone)) changed=True msgs.append("Added masquerade to zone %s" % (zone)) elif desired_state == "disabled": @@ -719,7 +1021,7 @@ def main(): if module.check_mode: module.exit_json(changed=True) - set_masquerade_disabled(zone) + action_handler(set_masquerade_disabled, (zone)) changed=True msgs.append("Removed masquerade from zone %s" % (zone)) From 562cf4f60480bbfdeb88f1712941070485f71a53 Mon Sep 17 00:00:00 2001 From: Iago Garrido Date: Mon, 7 Nov 2016 22:04:09 +0100 Subject: [PATCH 2316/2522] Fixes win_uri module ignoring body argument (#2504) * Fixes win_uri module ignoring body argument * Added body field of the response to the documentation --- windows/win_uri.ps1 | 5 +++++ windows/win_uri.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/windows/win_uri.ps1 b/windows/win_uri.ps1 index b02418e8912..d701ef56b92 100644 --- a/windows/win_uri.ps1 +++ b/windows/win_uri.ps1 @@ -65,6 +65,11 @@ if ($headers -ne $null) { $webrequest_opts.Headers = $req_headers } +if ($body -ne $null) { + $webrequest_opts.Body = $body + Set-Attr $result.win_uri "body" $body +} + try { $response = Invoke-WebRequest @webrequest_opts } catch { diff --git a/windows/win_uri.py b/windows/win_uri.py index 7045f70bd42..c65f3ab536a 100644 --- a/windows/win_uri.py +++ b/windows/win_uri.py @@ -120,6 +120,11 @@ returned: always type: bool sample: True +body: + description: The content of the body used (added in version 2.2) + returned: when body is specified + type: string + sample: '{"id":1}' status_code: description: The HTTP Status Code of the response. returned: success From 08d9cdf883fe3df4535bd914661ee3a9de58c9ae Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 7 Nov 2016 16:06:09 -0500 Subject: [PATCH 2317/2522] corrected version added --- windows/win_uri.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/windows/win_uri.py b/windows/win_uri.py index c65f3ab536a..b5ab0f69cb7 100644 --- a/windows/win_uri.py +++ b/windows/win_uri.py @@ -121,10 +121,11 @@ type: bool sample: True body: - description: The content of the body used (added in version 2.2) + description: The content of the body used returned: when body is specified type: string sample: '{"id":1}' + version_added: "2.3" status_code: description: The HTTP Status Code of the response. returned: success From 2c1e88a180e88bb4bd39c63b57677758b871a590 Mon Sep 17 00:00:00 2001 From: Trond Hindenes Date: Mon, 7 Nov 2016 22:07:53 +0100 Subject: [PATCH 2318/2522] Added support for IIS AppPool identities (#2675) --- windows/win_acl.ps1 | 29 ++++++++++++++++++++++++++--- windows/win_acl.py | 2 +- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/windows/win_acl.ps1 b/windows/win_acl.ps1 index 2e20793e1fe..068130a203f 100644 --- a/windows/win_acl.ps1 +++ b/windows/win_acl.ps1 @@ -32,9 +32,19 @@ Function UserSearch $searchDomain = $false $searchDomainUPN = $false + $SearchAppPools = $false if ($accountName.Split("\").count -gt 1) { - if ($accountName.Split("\")[0] -ne $env:COMPUTERNAME) + if ($accountName.Split("\")[0] -eq $env:COMPUTERNAME) + { + + } + elseif ($accountName.Split("\")[0] -eq "IIS APPPOOL") + { + $SearchAppPools = $true + $accountName = $accountName.split("\")[1] + } + else { $searchDomain = $true $accountName = $accountName.split("\")[1] @@ -51,7 +61,7 @@ Function UserSearch $accountName = $env:COMPUTERNAME + "\" + $accountName } - if ($searchDomain -eq $false) + if (($searchDomain -eq $false) -and ($SearchAppPools -eq $false)) { # do not use Win32_UserAccount, because e.g. SYSTEM (BUILTIN\SYSTEM or COMPUUTERNAME\SYSTEM) will not be listed. on Win32_Account groups will be listed too $localaccount = get-wmiobject -class "Win32_Account" -namespace "root\CIMV2" -filter "(LocalAccount = True)" | where {$_.Caption -eq $accountName} @@ -60,7 +70,20 @@ Function UserSearch return $localaccount.SID } } - Else + Elseif ($SearchAppPools -eq $true) + { + Import-Module WebAdministration + $testiispath = Test-path "IIS:" + if ($testiispath -eq $false) + { + return $null + } + else + { + $apppoolobj = Get-ItemProperty IIS:\AppPools\$accountName + return $apppoolobj.applicationPoolSid + } + } { #Search by samaccountname $Searcher = [adsisearcher]"" diff --git a/windows/win_acl.py b/windows/win_acl.py index 89ec45c7e08..42cb91ce12a 100644 --- a/windows/win_acl.py +++ b/windows/win_acl.py @@ -29,7 +29,7 @@ version_added: "2.0" short_description: Set file/directory permissions for a system user or group. description: - - Add or remove rights/permissions for a given user or group for the specified src file or folder. + - Add or remove rights/permissions for a given user or group for the specified src file or folder. If adding ACL's for AppPool identities, the Windows "Feature Web-Scripting-Tools" must be enabled options: path: description: From 4f9058c1c705fbbd1646e64566481901b5adc573 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 7 Nov 2016 16:09:00 -0500 Subject: [PATCH 2319/2522] added version avialable to docs --- windows/win_acl.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/windows/win_acl.py b/windows/win_acl.py index 42cb91ce12a..cd25af0a34e 100644 --- a/windows/win_acl.py +++ b/windows/win_acl.py @@ -29,7 +29,8 @@ version_added: "2.0" short_description: Set file/directory permissions for a system user or group. description: - - Add or remove rights/permissions for a given user or group for the specified src file or folder. If adding ACL's for AppPool identities, the Windows "Feature Web-Scripting-Tools" must be enabled + - Add or remove rights/permissions for a given user or group for the specified src file or folder. + - If adding ACL's for AppPool identities (available since 2.3), the Windows "Feature Web-Scripting-Tools" must be enabled options: path: description: From 5c9703107d5048aa8c653dda70c9aefd534262ee Mon Sep 17 00:00:00 2001 From: Jason Cormie Date: Mon, 7 Nov 2016 21:11:22 +0000 Subject: [PATCH 2320/2522] Allow setting the visible name of a host in zabbix (#2919) In Zabbix, the visible name defaults to the hostname. This is not very useful if you try to manage vmware VMs as the so called host_name within zabbix must be set to the vcenter UUID. This patch allows you to provide an alias which will be shown with zabbix. If its not supplied it will default to host_name. --- monitoring/zabbix_host.py | 41 ++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/monitoring/zabbix_host.py b/monitoring/zabbix_host.py index 6ba256897bd..3917187520f 100644 --- a/monitoring/zabbix_host.py +++ b/monitoring/zabbix_host.py @@ -64,6 +64,11 @@ - Name of the host in Zabbix. - host_name is the unique identifier used and cannot be updated using this module. required: true + visible_name: + description: + - Visible name of the host in Zabbix. + required: false + version_added: '2.2' host_groups: description: - List of host groups the host is part of. @@ -127,6 +132,7 @@ login_user: username login_password: password host_name: ExampleHost + visible_name: ExampleName host_groups: - Example group1 - Example group2 @@ -206,26 +212,30 @@ def get_template_ids(self, template_list): template_ids.append(template_id) return template_ids - def add_host(self, host_name, group_ids, status, interfaces, proxy_id): + def add_host(self, host_name, group_ids, status, interfaces, proxy_id, visible_name): try: if self._module.check_mode: self._module.exit_json(changed=True) parameters = {'host': host_name, 'interfaces': interfaces, 'groups': group_ids, 'status': status} if proxy_id: parameters['proxy_hostid'] = proxy_id + if visible_name: + parameters['name'] = visible_name host_list = self._zapi.host.create(parameters) if len(host_list) >= 1: return host_list['hostids'][0] except Exception as e: self._module.fail_json(msg="Failed to create host %s: %s" % (host_name, e)) - def update_host(self, host_name, group_ids, status, host_id, interfaces, exist_interface_list, proxy_id): + def update_host(self, host_name, group_ids, status, host_id, interfaces, exist_interface_list, proxy_id, visible_name): try: if self._module.check_mode: self._module.exit_json(changed=True) parameters = {'hostid': host_id, 'groups': group_ids, 'status': status} if proxy_id: parameters['proxy_hostid'] = proxy_id + if visible_name: + parameters['name'] = visible_name self._zapi.host.update(parameters) interface_list_copy = exist_interface_list if interfaces: @@ -342,7 +352,7 @@ def get_host_status_by_host(self, host): # check all the properties before link or clear template def check_all_properties(self, host_id, host_groups, status, interfaces, template_ids, - exist_interfaces, host, proxy_id): + exist_interfaces, host, proxy_id, visible_name): # get the existing host's groups exist_host_groups = self.get_host_groups_by_host_id(host_id) if set(host_groups) != set(exist_host_groups): @@ -362,10 +372,12 @@ def check_all_properties(self, host_id, host_groups, status, interfaces, templat if set(list(template_ids)) != set(exist_template_ids): return True - if proxy_id is not None: - if host['proxy_hostid'] != proxy_id: - return True - + if host['proxy_hostid'] != proxy_id: + return True + + if host['name'] != visible_name: + return True + return False # link or clear template of the host @@ -428,7 +440,9 @@ def main(): timeout=dict(type='int', default=10), interfaces=dict(type='list', required=False), force=dict(type='bool', default=True), - proxy=dict(type='str', required=False) + proxy=dict(type='str', required=False), + visible_name=dict(type='str', required=False) + ), supports_check_mode=True ) @@ -442,6 +456,7 @@ def main(): http_login_user = module.params['http_login_user'] http_login_password = module.params['http_login_password'] host_name = module.params['host_name'] + visible_name = module.params['visible_name'] host_groups = module.params['host_groups'] link_templates = module.params['link_templates'] inventory_mode = module.params['inventory_mode'] @@ -514,10 +529,10 @@ def main(): if len(exist_interfaces) > interfaces_len: if host.check_all_properties(host_id, host_groups, status, interfaces, template_ids, - exist_interfaces, zabbix_host_obj, proxy_id): + exist_interfaces, zabbix_host_obj, proxy_id, visible_name): host.link_or_clear_template(host_id, template_ids) host.update_host(host_name, group_ids, status, host_id, - interfaces, exist_interfaces, proxy_id) + interfaces, exist_interfaces, proxy_id, visible_name) module.exit_json(changed=True, result="Successfully update host %s (%s) and linked with template '%s'" % (host_name, ip, link_templates)) @@ -525,8 +540,8 @@ def main(): module.exit_json(changed=False) else: if host.check_all_properties(host_id, host_groups, status, interfaces, template_ids, - exist_interfaces_copy, zabbix_host_obj, proxy_id): - host.update_host(host_name, group_ids, status, host_id, interfaces, exist_interfaces, proxy_id) + exist_interfaces_copy, zabbix_host_obj, proxy_id, visible_name): + host.update_host(host_name, group_ids, status, host_id, interfaces, exist_interfaces, proxy_id, visible_name) host.link_or_clear_template(host_id, template_ids) host.update_inventory_mode(host_id, inventory_mode) module.exit_json(changed=True, @@ -552,7 +567,7 @@ def main(): module.fail_json(msg="Specify at least one interface for creating host '%s'." % host_name) # create host - host_id = host.add_host(host_name, group_ids, status, interfaces, proxy_id) + host_id = host.add_host(host_name, group_ids, status, interfaces, proxy_id, visible_name) host.link_or_clear_template(host_id, template_ids) host.update_inventory_mode(host_id, inventory_mode) module.exit_json(changed=True, result="Successfully added host %s (%s) and linked with template '%s'" % ( From ea345d09418e7951b6dd123bebe17e213ebb89d2 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 7 Nov 2016 16:11:44 -0500 Subject: [PATCH 2321/2522] corrected versionadded --- monitoring/zabbix_host.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/zabbix_host.py b/monitoring/zabbix_host.py index 3917187520f..34b68c78b9f 100644 --- a/monitoring/zabbix_host.py +++ b/monitoring/zabbix_host.py @@ -68,7 +68,7 @@ description: - Visible name of the host in Zabbix. required: false - version_added: '2.2' + version_added: '2.3' host_groups: description: - List of host groups the host is part of. From eefd9b07fa34844bfa6c8a5804a8f5d40d083ed1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc?= Date: Mon, 7 Nov 2016 22:18:06 +0100 Subject: [PATCH 2322/2522] Add parameter hash_host (#3204) * Add parameter hash_host * Fix version_added * Remove spurious whitespace --- system/known_hosts.py | 55 +++++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/system/known_hosts.py b/system/known_hosts.py index 0c9f24f4c2c..810759989f4 100644 --- a/system/known_hosts.py +++ b/system/known_hosts.py @@ -45,6 +45,12 @@ - The known_hosts file to edit required: no default: "(homedir)+/.ssh/known_hosts" + hash_host: + description: + - Hash the hostname in the known_hosts file + required: no + default: no + version_added: "2.3" state: description: - I(present) to add the host key, I(absent) to remove it. @@ -71,6 +77,7 @@ # name = hostname whose key should be added (alias: host) # key = line(s) to add to known_hosts file # path = the known_hosts file to edit (default: ~/.ssh/known_hosts) +# hash_host = yes|no (default: no) hash the hostname in the known_hosts file # state = absent|present (default: present) import os @@ -90,6 +97,7 @@ def enforce_state(module, params): key = params.get("key",None) port = params.get("port",None) path = params.get("path") + hash_host = params.get("hash_host") state = params.get("state") #Find the ssh-keygen binary sshkeygen = module.get_bin_path("ssh-keygen",True) @@ -103,7 +111,7 @@ def enforce_state(module, params): sanity_check(module,host,key,sshkeygen) - found,replace_or_add,found_line=search_for_host_key(module,host,key,path,sshkeygen) + found,replace_or_add,found_line,key=search_for_host_key(module,host,key,hash_host,path,sshkeygen) #We will change state if found==True & state!="present" #or found==False & state=="present" @@ -192,7 +200,7 @@ def sanity_check(module,host,key,sshkeygen): if stdout=='': #host not found module.fail_json(msg="Host parameter does not match hashed host field in supplied key") -def search_for_host_key(module,host,key,path,sshkeygen): +def search_for_host_key(module,host,key,hash_host,path,sshkeygen): '''search_for_host_key(module,host,key,path,sshkeygen) -> (found,replace_or_add,found_line) Looks up host and keytype in the known_hosts file path; if it's there, looks to see @@ -204,24 +212,33 @@ def search_for_host_key(module,host,key,path,sshkeygen): sshkeygen is the path to ssh-keygen, found earlier with get_bin_path ''' if os.path.exists(path)==False: - return False, False, None + return False, False, None, key + + sshkeygen_command=[sshkeygen,'-F',host,'-f',path] + #openssh >=6.4 has changed ssh-keygen behaviour such that it returns #1 if no host is found, whereas previously it returned 0 - rc,stdout,stderr=module.run_command([sshkeygen,'-F',host,'-f',path], + rc,stdout,stderr=module.run_command(sshkeygen_command, check_rc=False) if stdout=='' and stderr=='' and (rc==0 or rc==1): - return False, False, None #host not found, no other errors + return False, False, None, key #host not found, no other errors if rc!=0: #something went wrong module.fail_json(msg="ssh-keygen failed (rc=%d,stdout='%s',stderr='%s')" % (rc,stdout,stderr)) #If user supplied no key, we don't want to try and replace anything with it if key is None: - return True, False, None + return True, False, None, key lines=stdout.split('\n') - new_key = normalize_known_hosts_key(key, host) + new_key = normalize_known_hosts_key(key) + + sshkeygen_command.insert(1,'-H') + rc,stdout,stderr=module.run_command(sshkeygen_command,check_rc=False) + if rc!=0: #something went wrong + module.fail_json(msg="ssh-keygen failed to hash host (rc=%d,stdout='%s',stderr='%s')" % (rc,stdout,stderr)) + hashed_lines=stdout.split('\n') - for l in lines: + for lnum,l in enumerate(lines): if l=='': continue elif l[0]=='#': # info output from ssh-keygen; contains the line number where key was found @@ -233,15 +250,22 @@ def search_for_host_key(module,host,key,path,sshkeygen): e = get_exception() module.fail_json(msg="failed to parse output of ssh-keygen for line number: '%s'" % l) else: - found_key = normalize_known_hosts_key(l,host) + found_key = normalize_known_hosts_key(l) + if hash_host==True: + if found_key['host'][:3]=='|1|': + new_key['host']=found_key['host'] + else: + hashed_host=normalize_known_hosts_key(hashed_lines[lnum]) + found_key['host']=hashed_host['host'] + key=key.replace(host,found_key['host']) if new_key==found_key: #found a match - return True, False, found_line #found exactly the same key, don't replace + return True, False, found_line, key #found exactly the same key, don't replace elif new_key['type'] == found_key['type']: # found a different key for the same key type - return True, True, found_line + return True, True, found_line, key #No match found, return found and replace, but no line - return True, True, None + return True, True, None, key -def normalize_known_hosts_key(key, host): +def normalize_known_hosts_key(key): ''' Transform a key, either taken from a known_host file or provided by the user, into a normalized form. @@ -256,11 +280,11 @@ def normalize_known_hosts_key(key, host): #The optional "marker" field, used for @cert-authority or @revoked if k[0][0] == '@': d['options'] = k[0] - d['host']=host + d['host']=k[1] d['type']=k[2] d['key']=k[3] else: - d['host']=host + d['host']=k[0] d['type']=k[1] d['key']=k[2] return d @@ -272,6 +296,7 @@ def main(): name = dict(required=True, type='str', aliases=['host']), key = dict(required=False, type='str'), path = dict(default="~/.ssh/known_hosts", type='path'), + hash_host = dict(required=False, type='bool' ,default=False), state = dict(default='present', choices=['absent','present']), ), supports_check_mode = True From 00332638313f9a2aebddb256dc67fc844bbbfd83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Kr=C3=A4mer?= Date: Tue, 8 Nov 2016 08:21:38 +1100 Subject: [PATCH 2323/2522] Add require_full_window option for Datadog monitors (#2653) --- monitoring/datadog_monitor.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py index 7ed1805c668..1446218a352 100644 --- a/monitoring/datadog_monitor.py +++ b/monitoring/datadog_monitor.py @@ -101,6 +101,11 @@ required: false default: False version_added: 2.2 + require_full_window: + description: ["A boolean indicating whether this monitor needs a full window of data before it's evaluated. We highly recommend you set this to False for sparse metrics, otherwise some evaluations will be skipped."] + required: false + default: null + version_added: 2.2 ''' EXAMPLES = ''' @@ -167,7 +172,8 @@ def main(): notify_audit=dict(required=False, default=False, type='bool'), thresholds=dict(required=False, type='dict', default=None), tags=dict(required=False, type='list', default=None), - locked=dict(required=False, default=False, type='bool') + locked=dict(required=False, default=False, type='bool'), + require_full_window=dict(required=False, default=None, type='bool') ) ) @@ -253,6 +259,7 @@ def install_monitor(module): "escalation_message": module.params['escalation_message'], "notify_audit": module.boolean(module.params['notify_audit']), "locked": module.boolean(module.params['locked']), + "require_full_window" : module.params['require_full_window'] } if module.params['type'] == "service check": From 63e18673101e0fb2a84ff0d8953740f41e2191a1 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 7 Nov 2016 16:22:13 -0500 Subject: [PATCH 2324/2522] corrected version added --- monitoring/datadog_monitor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py index 1446218a352..4688a0e5944 100644 --- a/monitoring/datadog_monitor.py +++ b/monitoring/datadog_monitor.py @@ -28,7 +28,6 @@ - "Options like described on http://docs.datadoghq.com/api/" version_added: "2.0" author: "Sebastian Kornehl (@skornehl)" -notes: [] requirements: [datadog] options: api_key: @@ -45,7 +44,7 @@ description: ["A list of tags to associate with your monitor when creating or updating. This can help you categorize and filter monitors."] required: false default: None - version_added: 2.2 + version_added: "2.2" type: description: - "The type of the monitor." @@ -100,12 +99,12 @@ description: ["A boolean indicating whether changes to this monitor should be restricted to the creator or admins."] required: false default: False - version_added: 2.2 + version_added: "2.2" require_full_window: description: ["A boolean indicating whether this monitor needs a full window of data before it's evaluated. We highly recommend you set this to False for sparse metrics, otherwise some evaluations will be skipped."] required: false default: null - version_added: 2.2 + version_added: "2.3" ''' EXAMPLES = ''' From 8cb27b22f82febc7b27a426e6a72e57c8b807039 Mon Sep 17 00:00:00 2001 From: Travis Truman Date: Wed, 19 Oct 2016 14:11:58 -0400 Subject: [PATCH 2325/2522] Add support for filtering flavors on ephemeral storage --- cloud/openstack/os_flavor_facts.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/cloud/openstack/os_flavor_facts.py b/cloud/openstack/os_flavor_facts.py index 05a3782be7e..6afecb61f07 100644 --- a/cloud/openstack/os_flavor_facts.py +++ b/cloud/openstack/os_flavor_facts.py @@ -49,7 +49,7 @@ options: name: description: - - A flavor name. Cannot be used with I(ram) or I(vcpus). + - A flavor name. Cannot be used with I(ram) or I(vcpus) or I(ephemeral). required: false default: None ram: @@ -80,6 +80,13 @@ returned by default. required: false default: None + ephemeral: + description: + - A string used for filtering flavors based on the amount of ephemeral + storage. Format is the same as the I(ram) parameter + required: false + default: false + version_added: "2.3" extends_documentation_fragment: openstack ''' @@ -115,6 +122,14 @@ cloud: mycloud ram: ">=1024" vcpus: "2" + +# Get all flavors with 1024 MB of RAM or more, exactly 2 virtual CPUs, and +# less than 30gb of ephemeral storage. +- os_flavor_facts: + cloud: mycloud + ram: ">=1024" + vcpus: "2" + ephemeral: "<30" ''' @@ -173,11 +188,13 @@ def main(): ram=dict(required=False, default=None), vcpus=dict(required=False, default=None), limit=dict(required=False, default=None, type='int'), + ephemeral=dict(required=False, default=None), ) module_kwargs = openstack_module_kwargs( mutually_exclusive=[ ['name', 'ram'], ['name', 'vcpus'], + ['name', 'ephemeral'] ] ) module = AnsibleModule(argument_spec, **module_kwargs) @@ -188,6 +205,7 @@ def main(): name = module.params['name'] vcpus = module.params['vcpus'] ram = module.params['ram'] + ephemeral = module.params['ephemeral'] limit = module.params['limit'] try: @@ -202,6 +220,8 @@ def main(): filters['vcpus'] = vcpus if ram: filters['ram'] = ram + if ephemeral: + filters['ephemeral'] = ephemeral if filters: # Range search added in 1.5.0 if StrictVersion(shade.__version__) < StrictVersion('1.5.0'): From fb334c042a56a726cd256fa50c4dca86fc9a945f Mon Sep 17 00:00:00 2001 From: Nijin Ashok Date: Sun, 30 Oct 2016 17:56:45 +0530 Subject: [PATCH 2326/2522] Fix issue in activating the VM disk while attaching disk Currently tag is passed within the disk element which is incorrect. As a result, disk will remain inactive even though the default option is true. --- cloud/ovirt/ovirt_vms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/ovirt/ovirt_vms.py b/cloud/ovirt/ovirt_vms.py index 0652b24c10a..0cda1f62964 100644 --- a/cloud/ovirt/ovirt_vms.py +++ b/cloud/ovirt/ovirt_vms.py @@ -532,8 +532,8 @@ def __attach_disks(self, entity): otypes.DiskAttachment( disk=otypes.Disk( id=disk_id, - active=disk.get('activate', True), ), + active=disk.get('activate', True), interface=otypes.DiskInterface( disk.get('interface', 'virtio') ), From 83d132cf9942eae7d3774d6997419585cdf297b1 Mon Sep 17 00:00:00 2001 From: Ryan Morlok Date: Wed, 1 Jun 2016 15:41:15 -0500 Subject: [PATCH 2327/2522] Enhanced ecs_taskdefinition module. Added support to explicitly manage task definitions be revision. If the revision expectations of the ansible task cannot be met, an error is thrown. If revision is not explicitly specified, enhanced module to be idempotent with respect to task definitions. It will search for an active revision of the task definition that matches the containers and volumes specified. If none can be found, a new revision will be created. --- cloud/amazon/ecs_taskdefinition.py | 194 +++++++++++++++++++++++------ 1 file changed, 156 insertions(+), 38 deletions(-) diff --git a/cloud/amazon/ecs_taskdefinition.py b/cloud/amazon/ecs_taskdefinition.py index 2fefe231144..8f9f894e11f 100644 --- a/cloud/amazon/ecs_taskdefinition.py +++ b/cloud/amazon/ecs_taskdefinition.py @@ -138,6 +138,33 @@ def register_task(self, family, container_definitions, volumes): containerDefinitions=container_definitions, volumes=volumes) return response['taskDefinition'] + def describe_task_definitions(self, family): + data = { + "taskDefinitionArns": [], + "nextToken": None + } + + def fetch(): + # Boto3 is weird about params passed, so only pass nextToken if we have a value + params = { + 'familyPrefix': family + } + + if data['nextToken']: + params['nextToken'] = data['nextToken'] + + result = self.ecs.list_task_definitions(**params) + data['taskDefinitionArns'] += result['taskDefinitionArns'] + data['nextToken'] = result.get('nextToken', None) + return data['nextToken'] is not None + + # Fetch all the arns, possibly across multiple pages + while fetch(): + pass + + # Return the full descriptions of the task definitions, sorted ascending by revision + return list(sorted([self.ecs.describe_task_definition(taskDefinition=arn)['taskDefinition'] for arn in data['taskDefinitionArns']], key=lambda td: td['revision'])) + def deregister_task(self, taskArn): response = self.ecs.deregister_task_definition(taskDefinition=taskArn) return response['taskDefinition'] @@ -146,12 +173,12 @@ def main(): argument_spec = ec2_argument_spec() argument_spec.update(dict( - state=dict(required=True, choices=['present', 'absent'] ), - arn=dict(required=False, type='str' ), - family=dict(required=False, type='str' ), - revision=dict(required=False, type='int' ), - containers=dict(required=False, type='list' ), - volumes=dict(required=False, type='list' ) + state=dict(required=True, choices=['present', 'absent']), + arn=dict(required=False, type='str'), + family=dict(required=False, type='str'), + revision=dict(required=False, type='int'), + containers=dict(required=False, type='list'), + volumes=dict(required=False, type='list') )) module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) @@ -163,52 +190,143 @@ def main(): module.fail_json(msg='boto3 is required.') task_to_describe = None - # When deregistering a task, we can specify the ARN OR - # the family and revision. - if module.params['state'] == 'absent': - if 'arn' in module.params and module.params['arn'] is not None: - task_to_describe = module.params['arn'] - elif 'family' in module.params and module.params['family'] is not None and 'revision' in module.params and module.params['revision'] is not None: - task_to_describe = module.params['family']+":"+str(module.params['revision']) - else: - module.fail_json(msg="To use task definitions, an arn or family and revision must be specified") - # When registering a task, we can specify the ARN OR - # the family and revision. + task_mgr = EcsTaskManager(module) + results = dict(changed=False) + if module.params['state'] == 'present': - if not 'family' in module.params: - module.fail_json(msg="To use task definitions, a family must be specified") - if not 'containers' in module.params: + if 'containers' not in module.params or not module.params['containers']: module.fail_json(msg="To use task definitions, a list of containers must be specified") - task_to_describe = module.params['family'] - task_mgr = EcsTaskManager(module) - existing = task_mgr.describe_task(task_to_describe) + if 'family' not in module.params or not module.params['family']: + module.fail_json(msg="To use task definitions, a family must be specified") - results = dict(changed=False) - if module.params['state'] == 'present': - if existing and 'status' in existing and existing['status']=="ACTIVE": - results['taskdefinition']=existing + family = module.params['family'] + existing_definitions_in_family = task_mgr.describe_task_definitions(module.params['family']) + + if 'revision' in module.params and module.params['revision']: + # The definition specifies revision. We must gurantee that an active revision of that number will result from this. + revision = int(module.params['revision']) + + # A revision has been explicitly specified. Attempt to locate a matching revision + tasks_defs_for_revision = [td for td in existing_definitions_in_family if td['revision'] == revision] + existing = tasks_defs_for_revision[0] if len(tasks_defs_for_revision) > 0 else None + + if existing and existing['status'] != "ACTIVE": + # We cannot reactivate an inactive revision + module.fail_json(msg="A task in family '%s' already exists for revsion %d, but it is inactive" % (family, revision)) + elif not existing: + if len(existing_definitions_in_family) == 0 and revision != 1: + module.fail_json(msg="You have specified a revision of %d but a created revision would be 1" % revision) + elif existing_definitions_in_family[-1]['revision'] + 1 != revision: + module.fail_json(msg="You have specified a revision of %d but a created revision would be %d" % (revision, existing_definitions_in_family[-1]['revision'] + 1)) + else: + existing = None + + def _right_has_values_of_left(left, right): + # Make sure the values are equivalent for everything left has + for k, v in left.iteritems(): + if not ((not v and (k not in right or not right[k])) or (k in right and v == right[k])): + # We don't care about list ordering because ECS can change things + if isinstance(v, list) and k in right: + left_list = v + right_list = right[k] or [] + + if len(left_list) != len(right_list): + return False + + for list_val in left_list: + if list_val not in right_list: + return False + else: + return False + + # Make sure right doesn't have anything that left doesn't + for k, v in right.iteritems(): + if v and k not in left: + return False + + return True + + def _task_definition_matches(requested_volumes, requested_containers, existing_task_definition): + if td['status'] != "ACTIVE": + return None + + existing_volumes = td.get('volumes', []) or [] + + if len(requested_volumes) != len(existing_volumes): + # Nope. + return None + + if len(requested_volumes) > 0: + for requested_vol in requested_volumes: + found = False + + for actual_vol in existing_volumes: + if _right_has_values_of_left(requested_vol, actual_vol): + found = True + break + + if not found: + return None + + existing_containers = td.get('containerDefinitions', []) or [] + + if len(requested_containers) != len(existing_containers): + # Nope. + return None + + for requested_container in requested_containers: + found = False + + for actual_container in existing_containers: + if _right_has_values_of_left(requested_container, actual_container): + found = True + break + + if not found: + return None + + return existing_task_definition + + # No revision explicitly specified. Attempt to find an active, matching revision that has all the properties requested + for td in existing_definitions_in_family: + requested_volumes = module.params.get('volumes', []) or [] + requested_containers = module.params.get('containers', []) or [] + existing = _task_definition_matches(requested_volumes, requested_containers, td) + + if existing: + break + + if existing: + # Awesome. Have an existing one. Nothing to do. + results['taskdefinition'] = existing else: if not module.check_mode: - # doesn't exist. create it. - volumes = [] - if 'volumes' in module.params: - volumes = module.params['volumes'] - if volumes is None: - volumes = [] + # Doesn't exist. create it. + volumes = module.params.get('volumes', []) or [] results['taskdefinition'] = task_mgr.register_task(module.params['family'], - module.params['containers'], volumes) + module.params['containers'], volumes) results['changed'] = True - # delete the cloudtrai elif module.params['state'] == 'absent': + # When de-registering a task definition, we can specify the ARN OR the family and revision. + if module.params['state'] == 'absent': + if 'arn' in module.params and module.params['arn'] is not None: + task_to_describe = module.params['arn'] + elif 'family' in module.params and module.params['family'] is not None and 'revision' in module.params and \ + module.params['revision'] is not None: + task_to_describe = module.params['family'] + ":" + str(module.params['revision']) + else: + module.fail_json(msg="To use task definitions, an arn or family and revision must be specified") + + existing = task_mgr.describe_task(task_to_describe) + if not existing: pass else: - # it exists, so we should delete it and mark changed. - # return info about the cluster deleted + # It exists, so we should delete it and mark changed. Return info about the task definition deleted results['taskdefinition'] = existing - if 'status' in existing and existing['status']=="INACTIVE": + if 'status' in existing and existing['status'] == "INACTIVE": results['changed'] = False else: if not module.check_mode: From 10dfb2f81b794859867fb4a1efc96927d3fe6c1b Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 7 Nov 2016 20:04:30 -0500 Subject: [PATCH 2328/2522] Return actual queue attributes with result from sqs_queue creation/update (#1362) * Return actual queue attributes with result Previously this was only returning the desired queue attributes, and not even returning the QueueARN for use elsewhere. Now it will return "results.attributes" that is retrieved with boto's get_queue_attributes(). * update return structure to reflect current SQS config; add documentation of return values * Remove redundancy from if/else statement --- cloud/amazon/sqs_queue.py | 48 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/cloud/amazon/sqs_queue.py b/cloud/amazon/sqs_queue.py index d5e17ad2433..70e6d92ffc6 100644 --- a/cloud/amazon/sqs_queue.py +++ b/cloud/amazon/sqs_queue.py @@ -81,6 +81,41 @@ - ec2 """ +RETURN = ''' +default_visibility_timeout: + description: The default visibility timeout in seconds. + returned: always + sample: 30 +delivery_delay: + description: The delivery delay in seconds. + returned: always + sample: 0 +maximum_message_size: + description: The maximum message size in bytes. + returned: always + sample: 262144 +message_retention_period: + description: The message retention period in seconds. + returned: always + sample: 345600 +name: + description: Name of the SQS Queue + returned: always + sample: "queuename-987d2de0" +queue_arn: + description: The queue's Amazon resource name (ARN). + returned: on successful creation or update of the queue + sample: 'arn:aws:sqs:us-east-1:199999999999:queuename-987d2de0' +receive_message_wait_time: + description: The receive message wait time in seconds. + returned: always + sample: 0 +region: + description: Region that the queue was created within + returned: always + sample: 'us-east-1' +''' + EXAMPLES = ''' # Create SQS queue with redrive policy - sqs_queue: @@ -140,16 +175,23 @@ def create_or_update_sqs_queue(connection, module): try: queue = connection.get_queue(queue_name) if queue: - # Update existing + # Update existing result['changed'] = update_sqs_queue(queue, check_mode=module.check_mode, **queue_attributes) - else: # Create new if not module.check_mode: queue = connection.create_queue(queue_name) update_sqs_queue(queue, **queue_attributes) result['changed'] = True - + + if not module.check_mode: + result['queue_arn'] = queue.get_attributes('QueueArn')['QueueArn'] + result['default_visibility_timeout'] = queue.get_attributes('VisibilityTimeout')['VisibilityTimeout'] + result['message_retention_period'] = queue.get_attributes('MessageRetentionPeriod')['MessageRetentionPeriod'] + result['maximum_message_size'] = queue.get_attributes('MaximumMessageSize')['MaximumMessageSize'] + result['delivery_delay'] = queue.get_attributes('DelaySeconds')['DelaySeconds'] + result['receive_message_wait_time'] = queue.get_attributes('ReceiveMessageWaitTimeSeconds')['ReceiveMessageWaitTimeSeconds'] + except BotoServerError: result['msg'] = 'Failed to create/update sqs queue due to error: ' + traceback.format_exc() module.fail_json(**result) From 328efedfe63ec30f0a8307d4532ac0ef06367c72 Mon Sep 17 00:00:00 2001 From: Steve Gargan Date: Tue, 8 Nov 2016 09:40:15 +0100 Subject: [PATCH 2329/2522] allow services to be removed by name as well as id (#3372) --- clustering/consul.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/clustering/consul.py b/clustering/consul.py index a034e167fca..f32e7a6a3f8 100644 --- a/clustering/consul.py +++ b/clustering/consul.py @@ -298,7 +298,7 @@ def add_service(module, service): changed = False consul_api = get_consul_api(module) - existing = get_service_by_id(consul_api, service.id) + existing = get_service_by_id_or_name(consul_api, service.id) # there is no way to retrieve the details of checks so if a check is present # in the service it must be re-registered @@ -306,7 +306,7 @@ def add_service(module, service): service.register(consul_api) # check that it registered correctly - registered = get_service_by_id(consul_api, service.id) + registered = get_service_by_id_or_name(consul_api, service.id) if registered: result = registered changed = True @@ -322,7 +322,7 @@ def add_service(module, service): def remove_service(module, service_id): ''' deregister a service from the given agent using its service id ''' consul_api = get_consul_api(module) - service = get_service_by_id(consul_api, service_id) + service = get_service_by_id_or_name(consul_api, service_id) if service: consul_api.agent.service.deregister(service_id) module.exit_json(changed=True, id=service_id) @@ -338,10 +338,10 @@ def get_consul_api(module, token=None): token=module.params.get('token')) -def get_service_by_id(consul_api, service_id): +def get_service_by_id_or_name(consul_api, service_id_or_name): ''' iterate the registered services and find one with the given id ''' for name, service in consul_api.agent.services().iteritems(): - if service['ID'] == service_id: + if service['ID'] == service_id_or_name or service['Service'] == service_id_or_name: return ConsulService(loaded=service) From 2434d52e1240d6dc04feca12e8d3dff83c345b93 Mon Sep 17 00:00:00 2001 From: Steve Gargan Date: Tue, 8 Nov 2016 09:53:32 +0100 Subject: [PATCH 2330/2522] consul_kv: remove default token (#3373) changes default token from "anonymous" to None. Fixes #792 --- clustering/consul_kv.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/clustering/consul_kv.py b/clustering/consul_kv.py index 9abdfff0930..7304606d959 100644 --- a/clustering/consul_kv.py +++ b/clustering/consul_kv.py @@ -251,7 +251,7 @@ def test_dependencies(module): if not python_consul_installed: module.fail_json(msg="python-consul required for this module. "\ "see http://python-consul.readthedocs.org/en/latest/#installation") - + def main(): argument_spec = dict( @@ -265,7 +265,7 @@ def main(): recurse=dict(required=False, type='bool'), retrieve=dict(required=False, default=True), state=dict(default='present', choices=['present', 'absent', 'acquire', 'release']), - token=dict(required=False, default='anonymous', no_log=True), + token=dict(required=False, no_log=True), value=dict(required=False), session=dict(required=False) ) @@ -273,7 +273,7 @@ def main(): module = AnsibleModule(argument_spec, supports_check_mode=False) test_dependencies(module) - + try: execute(module) except ConnectionError as e: From 47aeaed1e7aa49459ffcddb4afcfa797240b35bc Mon Sep 17 00:00:00 2001 From: Jens Carl Date: Tue, 8 Nov 2016 00:59:41 -0800 Subject: [PATCH 2331/2522] Add support for current Redshift node types (#3328) --- cloud/amazon/redshift.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/amazon/redshift.py b/cloud/amazon/redshift.py index 8b4c942e4ac..b67020b196c 100644 --- a/cloud/amazon/redshift.py +++ b/cloud/amazon/redshift.py @@ -38,7 +38,7 @@ description: - The node type of the cluster. Must be specified when command=create. required: false - choices: ['dw1.xlarge', 'dw1.8xlarge', 'dw2.large', 'dw2.8xlarge', ] + choices: ['ds1.xlarge', 'ds1.8xlarge', 'ds2.xlarge', 'ds2.8xlarge', 'dc1.large', 'dc1.8xlarge', 'dw1.xlarge', 'dw1.8xlarge', 'dw2.large', 'dw2.8xlarge'] username: description: - Master database username. Used only when command=create. @@ -160,7 +160,7 @@ # Basic cluster provisioning example - redshift: > command=create - node_type=dw1.xlarge + node_type=ds1.xlarge identifier=new_cluster username=cluster_admin password=1nsecure @@ -430,7 +430,7 @@ def main(): argument_spec.update(dict( command = dict(choices=['create', 'facts', 'delete', 'modify'], required=True), identifier = dict(required=True), - node_type = dict(choices=['dw1.xlarge', 'dw1.8xlarge', 'dw2.large', 'dw2.8xlarge', ], required=False), + node_type = dict(choices=['ds1.xlarge', 'ds1.8xlarge', 'ds2.xlarge', 'ds2.8xlarge', 'dc1.large', 'dc1.8xlarge', 'dw1.xlarge', 'dw1.8xlarge', 'dw2.large', 'dw2.8xlarge'], required=False), username = dict(required=False), password = dict(no_log=True, required=False), db_name = dict(require=False), From 99e26510b7772cdabdeace4766b00986413602c5 Mon Sep 17 00:00:00 2001 From: Davis Phillips Date: Tue, 8 Nov 2016 11:44:04 -0600 Subject: [PATCH 2332/2522] closes 3305 adds customize support --- cloud/vmware/vmware_guest.py | 151 ++++++++++++++++++++++++++++++++++- 1 file changed, 150 insertions(+), 1 deletion(-) diff --git a/cloud/vmware/vmware_guest.py b/cloud/vmware/vmware_guest.py index 751cac93856..3cc467b3031 100644 --- a/cloud/vmware/vmware_guest.py +++ b/cloud/vmware/vmware_guest.py @@ -89,6 +89,26 @@ description: - The esxi hostname where the VM will run. required: True + customize: + description: + - Should customization spec be run + required: False + ips: + description: + - IP Addresses to set + required: False + networks: + description: + - Network to use should include VM network name and gateway + required: False + dns_servers: + description: + - DNS servers to use: 4.4.4.4, 8.8.8.8 + required: False + domain: + description: + - Domain to use while customizing + required: False extends_documentation_fragment: vmware.documentation ''' @@ -126,6 +146,28 @@ register: deploy # +# Clone Template and customize +# + - name: Clone template and customize + vmware_guest: + hostname: "192.168.1.209" + username: "administrator@vsphere.local" + password: "vmware" + validate_certs: False + name: testvm-2 + datacenter: datacenter1 + cluster: cluster + validate_certs: False + template: template_el7 + customize: True + domain: "example.com" + dns_servers: ['192.168.1.1','192.168.1.2'] + ips: "192.168.1.100" + networks: + '192.168.1.0/24': + network: 'VM Network' + gateway: '192.168.1.1' +# # Gather facts only # - name: gather the VM facts @@ -163,6 +205,7 @@ import os import string import time +from netaddr import IPNetwork, IPAddress from ansible.module_utils.urls import fetch_url @@ -672,6 +715,107 @@ def deploy_template(self, poweron=False, wait_for_ip=False): clonespec_kwargs['config'].memoryMB = \ int(self.params['hardware']['memory_mb']) + # lets try and assign a static ip addresss + if 'customize' in self.params: + ip_settings = list() + for ip_string in self.params['ips']: + ip = IPAddress(self.params['ips']) + + for network in self.params['networks']: + + if ip in IPNetwork(network): + self.params['networks'][network]['ip'] = str(ip) + ipnet = IPNetwork(network) + self.params['networks'][network]['subnet_mask'] = str( + ipnet.netmask + ) + ip_settings.append(self.params['networks'][network]) + + + network = get_obj(self.content, [vim.Network], ip_settings[0]['network']) + datacenter = get_obj(self.content, [vim.Datacenter], self.params['datacenter']) + # get the folder where VMs are kept for this datacenter + destfolder = datacenter.vmFolder + + cluster = get_obj(self.content, [vim.ClusterComputeResource],self.params['cluster']) + + + devices = [] + adaptermaps = [] + + try: + for device in template.config.hardware.device: + + if hasattr(device, 'addressType'): + nic = vim.vm.device.VirtualDeviceSpec() + nic.operation = \ + vim.vm.device.VirtualDeviceSpec.Operation.remove + nic.device = device + devices.append(nic) + except: + pass + + # for key, ip in enumerate(ip_settings): + # VM device + # single device support + key = 0 + nic = vim.vm.device.VirtualDeviceSpec() + nic.operation = vim.vm.device.VirtualDeviceSpec.Operation.add + nic.device = vim.vm.device.VirtualVmxnet3() + nic.device.wakeOnLanEnabled = True + nic.device.addressType = 'assigned' + nic.device.deviceInfo = vim.Description() + nic.device.deviceInfo.label = 'Network Adapter %s' % (key + 1) + + nic.device.deviceInfo.summary = ip_settings[key]['network'] + nic.device.backing = vim.vm.device.VirtualEthernetCard.NetworkBackingInfo() + nic.device.backing.network = get_obj(self.content, [vim.Network], ip_settings[key]['network']) + + nic.device.backing.deviceName = ip_settings[key]['network'] + nic.device.connectable = vim.vm.device.VirtualDevice.ConnectInfo() + nic.device.connectable.startConnected = True + nic.device.connectable.allowGuestControl = True + devices.append(nic) + + clonespec_kwargs['config'].deviceChange = devices + + guest_map = vim.vm.customization.AdapterMapping() + guest_map.adapter = vim.vm.customization.IPSettings() + guest_map.adapter.ip = vim.vm.customization.FixedIp() + guest_map.adapter.ip.ipAddress = str(ip_settings[key]['ip']) + guest_map.adapter.subnetMask = str(ip_settings[key]['subnet_mask']) + + try: + guest_map.adapter.gateway = ip_settings[key]['gateway'] + except: + pass + + try: + guest_map.adapter.dnsDomain = self.params['domain'] + except: + pass + + adaptermaps.append(guest_map) + + # DNS settings + globalip = vim.vm.customization.GlobalIPSettings() + globalip.dnsServerList = self.params['dns_servers'] + globalip.dnsSuffixList = str(self.params['domain']) + + # Hostname settings + ident = vim.vm.customization.LinuxPrep() + ident.domain = str(self.params['domain']) + ident.hostName = vim.vm.customization.FixedName() + ident.hostName.name = self.params['name'] + + customspec = vim.vm.customization.Specification() + customspec.nicSettingMap = adaptermaps + customspec.globalIPSettings = globalip + customspec.identity = ident + + clonespec = vim.vm.CloneSpec(**clonespec_kwargs) + clonespec.customization = customspec + clonespec = vim.vm.CloneSpec(**clonespec_kwargs) task = template.Clone(folder=destfolder, name=self.params['name'], spec=clonespec) self.wait_for_task(task) @@ -932,7 +1076,12 @@ def main(): datacenter=dict(required=False, type='str', default=None), esxi_hostname=dict(required=False, type='str', default=None), cluster=dict(required=False, type='str', default=None), - wait_for_ip_address=dict(required=False, type='bool', default=True) + wait_for_ip_address=dict(required=False, type='bool', default=True), + customize=dict(required=False, type='bool', default=False), + ips=dict(required=False, type='str', default=None), + dns_servers=dict(required=False, type='list', default=None), + domain=dict(required=False, type='str', default=None), + networks=dict(required=False, type='dict', default={}) ), supports_check_mode=True, mutually_exclusive=[], From cad77230e7fb524eecc2ba0c0579c8ff5413c657 Mon Sep 17 00:00:00 2001 From: Davis Phillips Date: Tue, 8 Nov 2016 12:26:41 -0600 Subject: [PATCH 2333/2522] mend --- cloud/vmware/vmware_guest.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/cloud/vmware/vmware_guest.py b/cloud/vmware/vmware_guest.py index 3cc467b3031..3e74c149930 100644 --- a/cloud/vmware/vmware_guest.py +++ b/cloud/vmware/vmware_guest.py @@ -716,23 +716,25 @@ def deploy_template(self, poweron=False, wait_for_ip=False): int(self.params['hardware']['memory_mb']) # lets try and assign a static ip addresss - if 'customize' in self.params: + if self.params['customize'] is True: ip_settings = list() - for ip_string in self.params['ips']: - ip = IPAddress(self.params['ips']) + if self.params['ips'] and self.params['network']: + for ip_string in self.params['ips']: + ip = IPAddress(self.params['ips']) - for network in self.params['networks']: + for network in self.params['networks']: - if ip in IPNetwork(network): - self.params['networks'][network]['ip'] = str(ip) - ipnet = IPNetwork(network) - self.params['networks'][network]['subnet_mask'] = str( - ipnet.netmask - ) - ip_settings.append(self.params['networks'][network]) + if ip in IPNetwork(network): + self.params['networks'][network]['ip'] = str(ip) + ipnet = IPNetwork(network) + self.params['networks'][network]['subnet_mask'] = str( + ipnet.netmask + ) + ip_settings.append(self.params['networks'][network]) - - network = get_obj(self.content, [vim.Network], ip_settings[0]['network']) + + key = 0 + network = get_obj(self.content, [vim.Network], ip_settings[key]['network']) datacenter = get_obj(self.content, [vim.Datacenter], self.params['datacenter']) # get the folder where VMs are kept for this datacenter destfolder = datacenter.vmFolder @@ -758,7 +760,7 @@ def deploy_template(self, poweron=False, wait_for_ip=False): # for key, ip in enumerate(ip_settings): # VM device # single device support - key = 0 + nic = vim.vm.device.VirtualDeviceSpec() nic.operation = vim.vm.device.VirtualDeviceSpec.Operation.add nic.device = vim.vm.device.VirtualVmxnet3() From 5cc238fe9c6fd0d4f2eb7bf617707e40b89ad4c9 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Tue, 8 Nov 2016 18:41:23 +0100 Subject: [PATCH 2334/2522] cloudstack: fix VPC doc --- cloud/cloudstack/cs_network.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/cloud/cloudstack/cs_network.py b/cloud/cloudstack/cs_network.py index 69206d8105f..a1f05fa8a42 100644 --- a/cloud/cloudstack/cs_network.py +++ b/cloud/cloudstack/cs_network.py @@ -59,14 +59,14 @@ gateway: description: - The gateway of the network. - - Required for shared networks and isolated networks when it belongs to VPC. + - Required for shared networks and isolated networks when it belongs to a VPC. - Only considered on create. required: false default: null netmask: description: - The netmask of the network. - - Required for shared networks and isolated networks when it belongs to VPC. + - Required for shared networks and isolated networks when it belongs to a VPC. - Only considered on create. required: false default: null @@ -91,7 +91,7 @@ default: null gateway_ipv6: description: - - The gateway of the IPv6 network. + - The gateway of the IPv6 network. - Required for shared networks. - Only considered on create. required: false @@ -103,12 +103,12 @@ default: null vpc: description: - - The ID or VID of the network. + - Name of the VPC of the network. required: false default: null isolated_pvlan: description: - - The isolated private vlan for this network. + - The isolated private VLAN for this network. required: false default: null clean_up: @@ -342,7 +342,6 @@ def __init__(self, module): 'dns1': 'dns1', 'dns2': 'dns2', } - self.network = None From c177cc608a2f3a93e290755fc3665fd227a591af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Wed, 9 Nov 2016 08:16:39 +0100 Subject: [PATCH 2335/2522] cloudstack: cs_network: cleanup dublicate VPC code (#3393) Code has been moved to module utils, also see https://github.com/ansible/ansible/commit/fe05c5e35a0a0a5da0b87ff86c7753eab477d7c5 --- cloud/cloudstack/cs_network.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/cloud/cloudstack/cs_network.py b/cloud/cloudstack/cs_network.py index a1f05fa8a42..41f5fa01f96 100644 --- a/cloud/cloudstack/cs_network.py +++ b/cloud/cloudstack/cs_network.py @@ -345,25 +345,6 @@ def __init__(self, module): self.network = None - def get_vpc(self, key=None): - vpc = self.module.params.get('vpc') - if not vpc: - return None - - args = {} - args['account'] = self.get_account(key='name') - args['domainid'] = self.get_domain(key='id') - args['projectid'] = self.get_project(key='id') - args['zoneid'] = self.get_zone(key='id') - - vpcs = self.cs.listVPCs(**args) - if vpcs: - for v in vpcs['vpc']: - if vpc in [ v['name'], v['displaytext'], v['id'] ]: - return self._get_by_key(key, v) - self.module.fail_json(msg="VPC '%s' not found" % vpc) - - def get_network_offering(self, key=None): network_offering = self.module.params.get('network_offering') if not network_offering: From a8db09c14eb08595c0974e8c7789743d472c4982 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Wed, 9 Nov 2016 14:29:00 +0100 Subject: [PATCH 2336/2522] Fix chdir argument to be 'path' --- system/make.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/make.py b/system/make.py index 41aa4faf28a..46b0c453902 100644 --- a/system/make.py +++ b/system/make.py @@ -101,7 +101,7 @@ def main(): argument_spec=dict( target=dict(required=False, default=None, type='str'), params=dict(required=False, default=None, type='dict'), - chdir=dict(required=True, default=None, type='str'), + chdir=dict(required=True, default=None, type='path'), ), ) # Build up the invocation of `make` we are going to use From 405bb22fd46fcaca52647b21f0782c40010cb51e Mon Sep 17 00:00:00 2001 From: Davis Phillips Date: Wed, 9 Nov 2016 11:06:03 -0600 Subject: [PATCH 2337/2522] remove commented code and fixed formatting --- cloud/vmware/vmware_guest.py | 192 +++++++++++++++++------------------ 1 file changed, 92 insertions(+), 100 deletions(-) diff --git a/cloud/vmware/vmware_guest.py b/cloud/vmware/vmware_guest.py index 3e74c149930..a2e613401d1 100644 --- a/cloud/vmware/vmware_guest.py +++ b/cloud/vmware/vmware_guest.py @@ -717,106 +717,98 @@ def deploy_template(self, poweron=False, wait_for_ip=False): # lets try and assign a static ip addresss if self.params['customize'] is True: - ip_settings = list() - if self.params['ips'] and self.params['network']: - for ip_string in self.params['ips']: - ip = IPAddress(self.params['ips']) - - for network in self.params['networks']: - - if ip in IPNetwork(network): - self.params['networks'][network]['ip'] = str(ip) - ipnet = IPNetwork(network) - self.params['networks'][network]['subnet_mask'] = str( - ipnet.netmask - ) - ip_settings.append(self.params['networks'][network]) - - - key = 0 - network = get_obj(self.content, [vim.Network], ip_settings[key]['network']) - datacenter = get_obj(self.content, [vim.Datacenter], self.params['datacenter']) - # get the folder where VMs are kept for this datacenter - destfolder = datacenter.vmFolder - - cluster = get_obj(self.content, [vim.ClusterComputeResource],self.params['cluster']) - - - devices = [] - adaptermaps = [] - - try: - for device in template.config.hardware.device: - - if hasattr(device, 'addressType'): - nic = vim.vm.device.VirtualDeviceSpec() - nic.operation = \ - vim.vm.device.VirtualDeviceSpec.Operation.remove - nic.device = device - devices.append(nic) - except: - pass - - # for key, ip in enumerate(ip_settings): - # VM device - # single device support - - nic = vim.vm.device.VirtualDeviceSpec() - nic.operation = vim.vm.device.VirtualDeviceSpec.Operation.add - nic.device = vim.vm.device.VirtualVmxnet3() - nic.device.wakeOnLanEnabled = True - nic.device.addressType = 'assigned' - nic.device.deviceInfo = vim.Description() - nic.device.deviceInfo.label = 'Network Adapter %s' % (key + 1) - - nic.device.deviceInfo.summary = ip_settings[key]['network'] - nic.device.backing = vim.vm.device.VirtualEthernetCard.NetworkBackingInfo() - nic.device.backing.network = get_obj(self.content, [vim.Network], ip_settings[key]['network']) - - nic.device.backing.deviceName = ip_settings[key]['network'] - nic.device.connectable = vim.vm.device.VirtualDevice.ConnectInfo() - nic.device.connectable.startConnected = True - nic.device.connectable.allowGuestControl = True - devices.append(nic) - - clonespec_kwargs['config'].deviceChange = devices - - guest_map = vim.vm.customization.AdapterMapping() - guest_map.adapter = vim.vm.customization.IPSettings() - guest_map.adapter.ip = vim.vm.customization.FixedIp() - guest_map.adapter.ip.ipAddress = str(ip_settings[key]['ip']) - guest_map.adapter.subnetMask = str(ip_settings[key]['subnet_mask']) - - try: - guest_map.adapter.gateway = ip_settings[key]['gateway'] - except: - pass - - try: - guest_map.adapter.dnsDomain = self.params['domain'] - except: - pass - - adaptermaps.append(guest_map) - - # DNS settings - globalip = vim.vm.customization.GlobalIPSettings() - globalip.dnsServerList = self.params['dns_servers'] - globalip.dnsSuffixList = str(self.params['domain']) - - # Hostname settings - ident = vim.vm.customization.LinuxPrep() - ident.domain = str(self.params['domain']) - ident.hostName = vim.vm.customization.FixedName() - ident.hostName.name = self.params['name'] - - customspec = vim.vm.customization.Specification() - customspec.nicSettingMap = adaptermaps - customspec.globalIPSettings = globalip - customspec.identity = ident - - clonespec = vim.vm.CloneSpec(**clonespec_kwargs) - clonespec.customization = customspec + ip_settings = list() + if self.params['ips'] and self.params['network']: + for ip_string in self.params['ips']: + ip = IPAddress(self.params['ips']) + for network in self.params['networks']: + if ip in IPNetwork(network): + self.params['networks'][network]['ip'] = str(ip) + ipnet = IPNetwork(network) + self.params['networks'][network]['subnet_mask'] = str( + ipnet.netmask + ) + ip_settings.append(self.params['networks'][network]) + + key = 0 + network = get_obj(self.content, [vim.Network], ip_settings[key]['network']) + datacenter = get_obj(self.content, [vim.Datacenter], self.params['datacenter']) + # get the folder where VMs are kept for this datacenter + destfolder = datacenter.vmFolder + + cluster = get_obj(self.content, [vim.ClusterComputeResource],self.params['cluster']) + + devices = [] + adaptermaps = [] + + try: + for device in template.config.hardware.device: + if hasattr(device, 'addressType'): + nic = vim.vm.device.VirtualDeviceSpec() + nic.operation = vim.vm.device.VirtualDeviceSpec.Operation.remove + nic.device = device + devices.append(nic) + except: + pass + + # single device support + nic = vim.vm.device.VirtualDeviceSpec() + nic.operation = vim.vm.device.VirtualDeviceSpec.Operation.add + nic.device = vim.vm.device.VirtualVmxnet3() + nic.device.wakeOnLanEnabled = True + nic.device.addressType = 'assigned' + nic.device.deviceInfo = vim.Description() + nic.device.deviceInfo.label = 'Network Adapter %s' % (key + 1) + + nic.device.deviceInfo.summary = ip_settings[key]['network'] + nic.device.backing = vim.vm.device.VirtualEthernetCard.NetworkBackingInfo() + nic.device.backing.network = get_obj(self.content, [vim.Network], ip_settings[key]['network']) + + nic.device.backing.deviceName = ip_settings[key]['network'] + nic.device.connectable = vim.vm.device.VirtualDevice.ConnectInfo() + nic.device.connectable.startConnected = True + nic.device.connectable.allowGuestControl = True + devices.append(nic) + + # Update the spec with the added NIC + clonespec_kwargs['config'].deviceChange = devices + + guest_map = vim.vm.customization.AdapterMapping() + guest_map.adapter = vim.vm.customization.IPSettings() + guest_map.adapter.ip = vim.vm.customization.FixedIp() + guest_map.adapter.ip.ipAddress = str(ip_settings[key]['ip']) + guest_map.adapter.subnetMask = str(ip_settings[key]['subnet_mask']) + + try: + guest_map.adapter.gateway = ip_settings[key]['gateway'] + except: + pass + + try: + guest_map.adapter.dnsDomain = self.params['domain'] + except: + pass + + adaptermaps.append(guest_map) + + # DNS settings + globalip = vim.vm.customization.GlobalIPSettings() + globalip.dnsServerList = self.params['dns_servers'] + globalip.dnsSuffixList = str(self.params['domain']) + + # Hostname settings + ident = vim.vm.customization.LinuxPrep() + ident.domain = str(self.params['domain']) + ident.hostName = vim.vm.customization.FixedName() + ident.hostName.name = self.params['name'] + + customspec = vim.vm.customization.Specification() + customspec.nicSettingMap = adaptermaps + customspec.globalIPSettings = globalip + customspec.identity = ident + + clonespec = vim.vm.CloneSpec(**clonespec_kwargs) + clonespec.customization = customspec clonespec = vim.vm.CloneSpec(**clonespec_kwargs) task = template.Clone(folder=destfolder, name=self.params['name'], spec=clonespec) From f747d682a7ceb056c5bfc2fe230c4d2eaf460df5 Mon Sep 17 00:00:00 2001 From: Davis Phillips Date: Wed, 9 Nov 2016 11:11:02 -0600 Subject: [PATCH 2338/2522] Added version_added to all the new params --- cloud/vmware/vmware_guest.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cloud/vmware/vmware_guest.py b/cloud/vmware/vmware_guest.py index a2e613401d1..e74b530acf6 100644 --- a/cloud/vmware/vmware_guest.py +++ b/cloud/vmware/vmware_guest.py @@ -93,22 +93,27 @@ description: - Should customization spec be run required: False + version_added: "2.2" ips: description: - IP Addresses to set required: False + version_added: "2.2" networks: description: - Network to use should include VM network name and gateway required: False + version_added: "2.2" dns_servers: description: - DNS servers to use: 4.4.4.4, 8.8.8.8 required: False + version_added: "2.2" domain: description: - Domain to use while customizing required: False + version_added: "2.2" extends_documentation_fragment: vmware.documentation ''' From 9760ec2538f8b44cb7f27924617a8e024a694724 Mon Sep 17 00:00:00 2001 From: jctanner Date: Wed, 9 Nov 2016 13:46:50 -0500 Subject: [PATCH 2339/2522] replace type() with isinstance() (#3404) Replace use of type() with isinstance() Addresses https://github.com/ansible/ansible/issues/18310 --- cloud/misc/virt.py | 6 +++--- cloud/misc/virt_net.py | 6 +++--- cloud/misc/virt_pool.py | 10 +++++----- cloud/vmware/vmware_guest.py | 16 ++++++++-------- database/misc/redis.py | 2 +- system/osx_defaults.py | 2 +- system/zfs.py | 2 +- 7 files changed, 22 insertions(+), 22 deletions(-) mode change 100755 => 100644 cloud/misc/virt_net.py mode change 100755 => 100644 cloud/misc/virt_pool.py diff --git a/cloud/misc/virt.py b/cloud/misc/virt.py index 68ff4a6c6cb..912eb3b9add 100644 --- a/cloud/misc/virt.py +++ b/cloud/misc/virt.py @@ -430,7 +430,7 @@ def core(module): if state and command=='list_vms': res = v.list_vms(state=state) - if type(res) != dict: + if not isinstance(res, dict): res = { command: res } return VIRT_SUCCESS, res @@ -477,13 +477,13 @@ def core(module): res = {'changed': True, 'created': guest} return VIRT_SUCCESS, res res = getattr(v, command)(guest) - if type(res) != dict: + if not isinstance(res, dict): res = { command: res } return VIRT_SUCCESS, res elif hasattr(v, command): res = getattr(v, command)() - if type(res) != dict: + if not isinstance(res, dict): res = { command: res } return VIRT_SUCCESS, res diff --git a/cloud/misc/virt_net.py b/cloud/misc/virt_net.py old mode 100755 new mode 100644 index 29cb43c3b97..469603d1aeb --- a/cloud/misc/virt_net.py +++ b/cloud/misc/virt_net.py @@ -466,7 +466,7 @@ def core(module): if state and command == 'list_nets': res = v.list_nets(state=state) - if type(res) != dict: + if not isinstance(res, dict): res = { command: res } return VIRT_SUCCESS, res @@ -523,13 +523,13 @@ def core(module): res = {'changed': mod, 'modified': name} return VIRT_SUCCESS, res res = getattr(v, command)(name) - if type(res) != dict: + if not isinstance(res, dict): res = { command: res } return VIRT_SUCCESS, res elif hasattr(v, command): res = getattr(v, command)() - if type(res) != dict: + if not isinstance(res, dict): res = { command: res } return VIRT_SUCCESS, res diff --git a/cloud/misc/virt_pool.py b/cloud/misc/virt_pool.py old mode 100755 new mode 100644 index a5664479db5..69c52e13c7e --- a/cloud/misc/virt_pool.py +++ b/cloud/misc/virt_pool.py @@ -546,7 +546,7 @@ def core(module): if state and command == 'list_pools': res = v.list_pools(state=state) - if type(res) != dict: + if not isinstance(res, dict): res = { command: res } return VIRT_SUCCESS, res @@ -608,22 +608,22 @@ def core(module): return VIRT_SUCCESS, res elif command == 'build': res = v.build(name, mode) - if type(res) != dict: + if not isinstance(res, dict): res = { 'changed': True, command: res } return VIRT_SUCCESS, res elif command == 'delete': res = v.delete(name, mode) - if type(res) != dict: + if not isinstance(res, dict): res = { 'changed': True, command: res } return VIRT_SUCCESS, res res = getattr(v, command)(name) - if type(res) != dict: + if not isinstance(res, dict): res = { command: res } return VIRT_SUCCESS, res elif hasattr(v, command): res = getattr(v, command)() - if type(res) != dict: + if not isinstance(res, dict): res = { command: res } return VIRT_SUCCESS, res diff --git a/cloud/vmware/vmware_guest.py b/cloud/vmware/vmware_guest.py index 751cac93856..cb73bd8062a 100644 --- a/cloud/vmware/vmware_guest.py +++ b/cloud/vmware/vmware_guest.py @@ -199,13 +199,13 @@ def _build_folder_tree(self, folder, tree={}, treepath=None): for child in children: if child == folder or child in tree: continue - if type(child) == vim.Folder: + if isinstance(child, vim.Folder): ctree = self._build_folder_tree(child) tree['subfolders'][child] = dict.copy(ctree) - elif type(child) == vim.VirtualMachine: + elif isinstance(child, vim.VirtualMachine): tree['virtualmachines'].append(child) else: - if type(folder) == vim.VirtualMachine: + if isinstance(folder, vim.VirtualMachine): return folder return tree @@ -214,7 +214,7 @@ def _build_folder_map(self, folder, vmap={}, inpath='/'): ''' Build a searchable index for vms+uuids+folders ''' - if type(folder) == tuple: + if isinstance(folder, tuple): folder = folder[1] if not 'names' in vmap: @@ -284,13 +284,13 @@ def getfolders(self): def compile_folder_path_for_object(self, vobj): ''' make a /vm/foo/bar/baz like folder path for an object ''' paths = [] - if type(vobj) == vim.Folder: + if isinstance(vobj, vim.Folder): paths.append(vobj.name) thisobj = vobj while hasattr(thisobj, 'parent'): thisobj = thisobj.parent - if type(thisobj) == vim.Folder: + if isinstance(thisobj, vim.Folder): paths.append(thisobj.name) paths.reverse() if paths[0] == 'Datacenters': @@ -343,7 +343,7 @@ def getvm(self, name=None, uuid=None, folder=None, name_match=None): if isinstance(fObj, vim.Datacenter): fObj = fObj.vmFolder for cObj in fObj.childEntity: - if not type(cObj) == vim.VirtualMachine: + if not isinstance(cObj, vim.VirtualMachine): continue if cObj.name == name: vm = cObj @@ -362,7 +362,7 @@ def getvm(self, name=None, uuid=None, folder=None, name_match=None): # compare the folder path of each VM against the search path for item in vmList.items(): vobj = item[0] - if not type(vobj.parent) == vim.Folder: + if not isinstance(vobj.parent, vim.Folder): continue if self.compile_folder_path_for_object(vobj) == searchpath: return vobj diff --git a/database/misc/redis.py b/database/misc/redis.py index 80784619bb2..20d16de4530 100644 --- a/database/misc/redis.py +++ b/database/misc/redis.py @@ -149,7 +149,7 @@ def set_master_mode(client): def flush(client, db=None): try: - if type(db) != int: + if not isinstance(db, int): return client.flushall() else: # The passed client has been connected to the database already diff --git a/system/osx_defaults.py b/system/osx_defaults.py index 93d81305860..a71cecbd7fe 100644 --- a/system/osx_defaults.py +++ b/system/osx_defaults.py @@ -233,7 +233,7 @@ def read(self): def write(self): # We need to convert some values so the defaults commandline understands it - if type(self.value) is bool: + if isinstance(self.value, bool): if self.value: value = "TRUE" else: diff --git a/system/zfs.py b/system/zfs.py index 0d79569d77a..1a1bad4a0f9 100644 --- a/system/zfs.py +++ b/system/zfs.py @@ -226,7 +226,7 @@ def main(): # All freestyle params are zfs properties if prop not in module.argument_spec: # Reverse the boolification of freestyle zfs properties - if type(value) == bool: + if isinstance(value, bool): if value is True: properties[prop] = 'on' else: From 1dede78d609740b69502c8ee62b819033dc5154b Mon Sep 17 00:00:00 2001 From: Kei Nohguchi Date: Wed, 9 Nov 2016 12:10:15 -0800 Subject: [PATCH 2340/2522] openvswitch_bridge: Check VLAN ID only under fake bridge (#3374) --- network/openvswitch_bridge.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/network/openvswitch_bridge.py b/network/openvswitch_bridge.py index fe48ca99a2f..b52df601c25 100644 --- a/network/openvswitch_bridge.py +++ b/network/openvswitch_bridge.py @@ -101,11 +101,11 @@ def __init__(self, module): self.timeout = module.params['timeout'] self.fail_mode = module.params['fail_mode'] - if self.parent and self.vlan is None: - self.module.fail_json(msg='VLAN id must be set when parent is defined') - - if self.vlan < 0 or self.vlan > 4095: - self.module.fail_json(msg='Invalid VLAN ID (must be between 0 and 4095)') + if self.parent: + if self.vlan is None: + self.module.fail_json(msg='VLAN id must be set when parent is defined') + elif self.vlan < 0 or self.vlan > 4095: + self.module.fail_json(msg='Invalid VLAN ID (must be between 0 and 4095)') def _vsctl(self, command): '''Run ovs-vsctl command''' From 7ca4e0fbebebc194a3842afe44fca5481fcb1ae8 Mon Sep 17 00:00:00 2001 From: Thomas Krahn Date: Wed, 9 Nov 2016 21:16:00 +0100 Subject: [PATCH 2341/2522] Add FreeIPA modules (#3247) * Add FreeIPA modules * Update version_added from 2.2 to 2.3 * ipa_*: Use Python 2.4 syntax to concatenate strings * ipa_*: Replace 'except Exception as e' with 'e = get_exception()' * ipa_*: import simplejson if json can't be imported * ipa_hbacrule: Fix: 'SyntaxError' on Python 2.4 * ipa_sudorule: Fix: 'SyntaxError' on Python 2.4 * ipa_*: Fix 'SyntaxError' on Python 2.4 * ipa_*: Import get_exception from ansible.module_utils.pycompat24 * Add FreeIPA modules * Update version_added from 2.2 to 2.3 * ipa_*: Fix 'SyntaxError' on Python 2.4 * ipa_*: Replace Python requests by ansible.module_utils.url * ipa_*: Replace Python requests by ansible.module_utils.url * ipa_*: Add option validate_certs * ipa_*: Remove requests from Ansible module documentation requirements * ipa_sudorule: Remove unnecessary empty line * ipa_sudorule: Remove markdown code from example * ipa_group: Add choices of state option * ipa_host: Rename options nshostlocation to ns_host_location, nshardwareplatform to ns_hardware_platform, nsosversion to ns_os_version, macaddress to mac_address and usercertificate to user_certificate and add aliases to be backward compatible --- identity/ipa/__init__.py | 0 identity/ipa/ipa_group.py | 384 ++++++++++++++++++++++++ identity/ipa/ipa_hbacrule.py | 479 ++++++++++++++++++++++++++++++ identity/ipa/ipa_host.py | 378 ++++++++++++++++++++++++ identity/ipa/ipa_hostgroup.py | 343 +++++++++++++++++++++ identity/ipa/ipa_role.py | 411 ++++++++++++++++++++++++++ identity/ipa/ipa_sudocmd.py | 275 +++++++++++++++++ identity/ipa/ipa_sudocmdgroup.py | 317 ++++++++++++++++++++ identity/ipa/ipa_sudorule.py | 491 +++++++++++++++++++++++++++++++ identity/ipa/ipa_user.py | 401 +++++++++++++++++++++++++ 10 files changed, 3479 insertions(+) create mode 100644 identity/ipa/__init__.py create mode 100644 identity/ipa/ipa_group.py create mode 100644 identity/ipa/ipa_hbacrule.py create mode 100644 identity/ipa/ipa_host.py create mode 100644 identity/ipa/ipa_hostgroup.py create mode 100644 identity/ipa/ipa_role.py create mode 100644 identity/ipa/ipa_sudocmd.py create mode 100644 identity/ipa/ipa_sudocmdgroup.py create mode 100644 identity/ipa/ipa_sudorule.py create mode 100644 identity/ipa/ipa_user.py diff --git a/identity/ipa/__init__.py b/identity/ipa/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/identity/ipa/ipa_group.py b/identity/ipa/ipa_group.py new file mode 100644 index 00000000000..39ce80639d1 --- /dev/null +++ b/identity/ipa/ipa_group.py @@ -0,0 +1,384 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ipa_group +author: Thomas Krahn (@Nosmoht) +short_description: Manage FreeIPA group +description: +- Add, modify and delete group within IPA server +options: + cn: + description: + - Canonical name. + - Can not be changed as it is the unique identifier. + required: true + aliases: ['name'] + external: + description: + - Allow adding external non-IPA members from trusted domains. + required: false + gidnumber: + description: + - GID (use this option to set it manually). + required: false + group: + description: + - List of group names assigned to this group. + - If an empty list is passed all groups will be removed from this group. + - If option is omitted assigned groups will not be checked or changed. + - Groups that are already assigned but not passed will be removed. + nonposix: + description: + - Create as a non-POSIX group. + required: false + user: + description: + - List of user names assigned to this group. + - If an empty list is passed all users will be removed from this group. + - If option is omitted assigned users will not be checked or changed. + - Users that are already assigned but not passed will be removed. + state: + description: + - State to ensure + required: false + default: "present" + choices: ["present", "absent"] + ipa_port: + description: Port of IPA server + required: false + default: 443 + ipa_host: + description: IP or hostname of IPA server + required: false + default: "ipa.example.com" + ipa_user: + description: Administrative account used on IPA server + required: false + default: "admin" + ipa_pass: + description: Password of administrative user + required: true + ipa_prot: + description: Protocol used by IPA server + required: false + default: "https" + choices: ["http", "https"] + validate_certs: + description: + - This only applies if C(ipa_prot) is I(https). + - If set to C(no), the SSL certificates will not be validated. + - This should only set to C(no) used on personally controlled sites using self-signed certificates. + required: false + default: true +version_added: "2.3" +requirements: +- json +''' + +EXAMPLES = ''' +# Ensure group is present +- ipa_group: + name: oinstall + gidnumber: 54321 + state: present + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret + +# Ensure that groups sysops and appops are assigned to ops but no other group +- ipa_group: + name: ops + group: + - sysops + - appops + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret + +# Ensure that users linus and larry are assign to the group, but no other user +- ipa_group: + name: sysops + user: + - linus + - larry + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret + +# Ensure group is absent +- ipa_group: + name: sysops + state: absent + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret +''' + +RETURN = ''' +group: + description: Group as returned by IPA API + returned: always + type: dict +''' + +try: + import json +except ImportError: + import simplejson as json + + +class IPAClient: + def __init__(self, module, host, port, protocol): + self.host = host + self.port = port + self.protocol = protocol + self.module = module + self.headers = None + + def get_base_url(self): + return '%s://%s/ipa' % (self.protocol, self.host) + + def get_json_url(self): + return '%s/session/json' % self.get_base_url() + + def login(self, username, password): + url = '%s/session/login_password' % self.get_base_url() + data = 'user=%s&password=%s' % (username, password) + headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'text/plain'} + try: + resp, info = fetch_url(module=self.module, url=url, data=data, headers=headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail('login', info['body']) + + self.headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': resp.info().getheader('Set-Cookie')} + except Exception: + e = get_exception() + self._fail('login', str(e)) + + def _fail(self, msg, e): + if 'message' in e: + err_string = e.get('message') + else: + err_string = e + self.module.fail_json(msg='%s: %s' % (msg, err_string)) + + def _post_json(self, method, name, item=None): + if item is None: + item = {} + url = '%s/session/json' % self.get_base_url() + data = {'method': method, 'params': [[name], item]} + try: + resp, info = fetch_url(module=self.module, url=url, data=json.dumps(data), headers=self.headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail(method, info['body']) + except Exception: + e = get_exception() + self._fail('post %s' % method, str(e)) + + resp = json.loads(resp.read()) + err = resp.get('error') + if err is not None: + self._fail('repsonse %s' % method, err) + + if 'result' in resp: + result = resp.get('result') + if 'result' in result: + result = result.get('result') + if isinstance(result, list): + if len(result) > 0: + return result[0] + return result + return None + + def group_find(self, name): + return self._post_json(method='group_find', name=None, item={'all': True, 'cn': name}) + + def group_add(self, name, item): + return self._post_json(method='group_add', name=name, item=item) + + def group_mod(self, name, item): + return self._post_json(method='group_mod', name=name, item=item) + + def group_del(self, name): + return self._post_json(method='group_del', name=name) + + def group_add_member(self, name, item): + return self._post_json(method='group_add_member', name=name, item=item) + + def group_add_member_group(self, name, item): + return self.group_add_member(name=name, item={'group': item}) + + def group_add_member_user(self, name, item): + return self.group_add_member(name=name, item={'user': item}) + + def group_remove_member(self, name, item): + return self._post_json(method='group_remove_member', name=name, item=item) + + def group_remove_member_group(self, name, item): + return self.group_remove_member(name=name, item={'group': item}) + + def group_remove_member_user(self, name, item): + return self.group_remove_member(name=name, item={'user': item}) + + +def get_group_dict(description=None, external=None, gid=None, nonposix=None): + group = {} + if description is not None: + group['description'] = description + if external is not None: + group['external'] = external + if gid is not None: + group['gidnumber'] = gid + if nonposix is not None: + group['nonposix'] = nonposix + return group + + +def get_group_diff(ipa_group, module_group): + data = [] + # With group_add attribute nonposix is passed, whereas with group_mod only posix can be passed. + if 'nonposix' in module_group: + # Only non-posix groups can be changed to posix + if not module_group['nonposix'] and ipa_group.get('nonposix'): + module_group['posix'] = True + del module_group['nonposix'] + + for key in module_group.keys(): + module_value = module_group.get(key, None) + ipa_value = ipa_group.get(key, None) + if isinstance(ipa_value, list) and not isinstance(module_value, list): + module_value = [module_value] + if isinstance(ipa_value, list) and isinstance(module_value, list): + ipa_value = sorted(ipa_value) + module_value = sorted(module_value) + if ipa_value != module_value: + data.append(key) + return data + + +def modify_if_diff(module, name, ipa_list, module_list, add_method, remove_method): + changed = False + diff = list(set(ipa_list) - set(module_list)) + if len(diff) > 0: + changed = True + if not module.check_mode: + remove_method(name=name, item=diff) + + diff = list(set(module_list) - set(ipa_list)) + if len(diff) > 0: + changed = True + if not module.check_mode: + add_method(name=name, item=diff) + + return changed + + +def ensure(module, client): + state = module.params['state'] + name = module.params['name'] + group = module.params['group'] + user = module.params['user'] + + module_group = get_group_dict(description=module.params['description'], external=module.params['external'], + gid=module.params['gidnumber'], nonposix=module.params['nonposix']) + ipa_group = client.group_find(name=name) + + changed = False + if state == 'present': + if not ipa_group: + changed = True + if not module.check_mode: + ipa_group = client.group_add(name, item=module_group) + else: + diff = get_group_diff(ipa_group, module_group) + if len(diff) > 0: + changed = True + if not module.check_mode: + data = {} + for key in diff: + data[key] = module_group.get(key) + client.group_mod(name=name, item=data) + + if group is not None: + changed = modify_if_diff(module, name, ipa_group.get('member_group', []), group, + client.group_add_member_group, + client.group_remove_member_group) or changed + + if user is not None: + changed = modify_if_diff(module, name, ipa_group.get('member_user', []), user, + client.group_add_member_user, + client.group_remove_member_user) or changed + + else: + if ipa_group: + changed = True + if not module.check_mode: + client.group_del(name) + + return changed, client.group_find(name=name) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + cn=dict(type='str', required=True, aliases=['name']), + description=dict(type='str', required=False), + external=dict(type='bool', required=False), + gidnumber=dict(type='str', required=False, aliases=['gid']), + group=dict(type='list', required=False), + nonposix=dict(type='bool', required=False), + state=dict(type='str', required=False, default='present', choices=['present', 'absent']), + user=dict(type='list', required=False), + ipa_prot=dict(type='str', required=False, default='https', choices=['http', 'https']), + ipa_host=dict(type='str', required=False, default='ipa.example.com'), + ipa_port=dict(type='int', required=False, default=443), + ipa_user=dict(type='str', required=False, default='admin'), + ipa_pass=dict(type='str', required=True, no_log=True), + validate_certs=dict(type='bool', required=False, default=True), + ), + supports_check_mode=True, + ) + + client = IPAClient(module=module, + host=module.params['ipa_host'], + port=module.params['ipa_port'], + protocol=module.params['ipa_prot']) + try: + client.login(username=module.params['ipa_user'], + password=module.params['ipa_pass']) + changed, group = ensure(module, client) + module.exit_json(changed=changed, group=group) + except Exception: + e = get_exception() + module.fail_json(msg=str(e)) + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import fetch_url + +if __name__ == '__main__': + main() diff --git a/identity/ipa/ipa_hbacrule.py b/identity/ipa/ipa_hbacrule.py new file mode 100644 index 00000000000..5657ffb8efe --- /dev/null +++ b/identity/ipa/ipa_hbacrule.py @@ -0,0 +1,479 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ipa_hbacrule +author: Thomas Krahn (@Nosmoht) +short_description: Manage FreeIPA HBAC rule +description: +- Add, modify or delete an IPA HBAC rule using IPA API. +options: + cn: + description: + - Canonical name. + - Can not be changed as it is the unique identifier. + required: true + aliases: ["name"] + description: + description: Description + required: false + host: + description: + - List of host names to assign. + - If an empty list is passed all hosts will be removed from the rule. + - If option is omitted hosts will not be checked or changed. + required: false + hostcategory: + description: Host category + required: false + choices: ['all'] + hostgroup: + description: + - List of hostgroup names to assign. + - If an empty list is passed all hostgroups will be removed. from the rule + - If option is omitted hostgroups will not be checked or changed. + service: + description: + - List of service names to assign. + - If an empty list is passed all services will be removed from the rule. + - If option is omitted services will not be checked or changed. + servicecategory: + description: Service category + required: false + choices: ['all'] + servicegroup: + description: + - List of service group names to assign. + - If an empty list is passed all assigned service groups will be removed from the rule. + - If option is omitted service groups will not be checked or changed. + sourcehost: + description: + - List of source host names to assign. + - If an empty list if passed all assigned source hosts will be removed from the rule. + - If option is omitted source hosts will not be checked or changed. + sourcehostcategory: + description: Source host category + required: false + choices: ['all'] + sourcehostgroup: + description: + - List of source host group names to assign. + - If an empty list if passed all assigned source host groups will be removed from the rule. + - If option is omitted source host groups will not be checked or changed. + state: + description: State to ensure + required: false + default: "present" + choices: ["present", "absent", "enabled", "disabled"] + user: + description: + - List of user names to assign. + - If an empty list if passed all assigned users will be removed from the rule. + - If option is omitted users will not be checked or changed. + usercategory: + description: User category + required: false + choices: ['all'] + usergroup: + description: + - List of user group names to assign. + - If an empty list if passed all assigned user groups will be removed from the rule. + - If option is omitted user groups will not be checked or changed. + ipa_port: + description: Port of IPA server + required: false + default: 443 + ipa_host: + description: IP or hostname of IPA server + required: false + default: "ipa.example.com" + ipa_user: + description: Administrative account used on IPA server + required: false + default: "admin" + ipa_pass: + description: Password of administrative user + required: true + ipa_prot: + description: Protocol used by IPA server + required: false + default: "https" + choices: ["http", "https"] + validate_certs: + description: + - This only applies if C(ipa_prot) is I(https). + - If set to C(no), the SSL certificates will not be validated. + - This should only set to C(no) used on personally controlled sites using self-signed certificates. + required: false + default: true +version_added: "2.3" +requirements: +- json +''' + +EXAMPLES = ''' +# Ensure rule to allow all users to access any host from any host +- ipa_hbacrule: + name: allow_all + description: Allow all users to access any host from any host + hostcategory: all + servicecategory: all + usercategory: all + state: present + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret + +# Ensure rule with certain limitations +- ipa_hbacrule: + name: allow_all_developers_access_to_db + description: Allow all developers to access any database from any host + hostgroup: + - db-server + usergroup: + - developers + state: present + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret + +# Ensure rule is absent +- ipa_hbacrule: + name: rule_to_be_deleted + state: absent + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret +''' + +RETURN = ''' +hbacrule: + description: HBAC rule as returned by IPA API. + returned: always + type: dict +''' + +try: + import json +except ImportError: + import simplejson as json + + +class IPAClient: + def __init__(self, module, host, port, protocol): + self.host = host + self.port = port + self.protocol = protocol + self.module = module + self.headers = None + + def get_base_url(self): + return '%s://%s/ipa' % (self.protocol, self.host) + + def get_json_url(self): + return '%s/session/json' % self.get_base_url() + + def login(self, username, password): + url = '%s/session/login_password' % self.get_base_url() + data = 'user=%s&password=%s' % (username, password) + headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'text/plain'} + try: + resp, info = fetch_url(module=self.module, url=url, data=data, headers=headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail('login', info['body']) + + self.headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': resp.info().getheader('Set-Cookie')} + except Exception: + e = get_exception() + self._fail('login', str(e)) + + def _fail(self, msg, e): + if 'message' in e: + err_string = e.get('message') + else: + err_string = e + self.module.fail_json(msg='%s: %s' % (msg, err_string)) + + def _post_json(self, method, name, item=None): + if item is None: + item = {} + url = '%s/session/json' % self.get_base_url() + data = {'method': method, 'params': [[name], item]} + try: + resp, info = fetch_url(module=self.module, url=url, data=json.dumps(data), headers=self.headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail(method, info['body']) + except Exception: + e = get_exception() + self._fail('post %s' % method, str(e)) + + resp = json.loads(resp.read()) + err = resp.get('error') + if err is not None: + self._fail('repsonse %s' % method, err) + + if 'result' in resp: + result = resp.get('result') + if 'result' in result: + result = result.get('result') + if isinstance(result, list): + if len(result) > 0: + return result[0] + return result + return None + + def hbacrule_find(self, name): + return self._post_json(method='hbacrule_find', name=None, item={'all': True, 'cn': name}) + + def hbacrule_add(self, name, item): + return self._post_json(method='hbacrule_add', name=name, item=item) + + def hbacrule_mod(self, name, item): + return self._post_json(method='hbacrule_mod', name=name, item=item) + + def hbacrule_del(self, name): + return self._post_json(method='hbacrule_del', name=name) + + def hbacrule_add_host(self, name, item): + return self._post_json(method='hbacrule_add_host', name=name, item=item) + + def hbacrule_remove_host(self, name, item): + return self._post_json(method='hbacrule_remove_host', name=name, item=item) + + def hbacrule_add_service(self, name, item): + return self._post_json(method='hbacrule_add_service', name=name, item=item) + + def hbacrule_remove_service(self, name, item): + return self._post_json(method='hbacrule_remove_service', name=name, item=item) + + def hbacrule_add_user(self, name, item): + return self._post_json(method='hbacrule_add_user', name=name, item=item) + + def hbacrule_remove_user(self, name, item): + return self._post_json(method='hbacrule_remove_user', name=name, item=item) + + def hbacrule_add_sourcehost(self, name, item): + return self._post_json(method='hbacrule_add_sourcehost', name=name, item=item) + + def hbacrule_remove_sourcehost(self, name, item): + return self._post_json(method='hbacrule_remove_sourcehost', name=name, item=item) + + +def get_hbacrule_dict(description=None, hostcategory=None, ipaenabledflag=None, servicecategory=None, + sourcehostcategory=None, + usercategory=None): + data = {} + if description is not None: + data['description'] = description + if hostcategory is not None: + data['hostcategory'] = hostcategory + if ipaenabledflag is not None: + data['ipaenabledflag'] = ipaenabledflag + if servicecategory is not None: + data['servicecategory'] = servicecategory + if sourcehostcategory is not None: + data['sourcehostcategory'] = sourcehostcategory + if usercategory is not None: + data['usercategory'] = usercategory + return data + + +def get_hbcarule_diff(ipa_hbcarule, module_hbcarule): + data = [] + for key in module_hbcarule.keys(): + module_value = module_hbcarule.get(key, None) + ipa_value = ipa_hbcarule.get(key, None) + if isinstance(ipa_value, list) and not isinstance(module_value, list): + module_value = [module_value] + if isinstance(ipa_value, list) and isinstance(module_value, list): + ipa_value = sorted(ipa_value) + module_value = sorted(module_value) + if ipa_value != module_value: + data.append(key) + return data + + +def modify_if_diff(module, name, ipa_list, module_list, add_method, remove_method, item): + changed = False + diff = list(set(ipa_list) - set(module_list)) + if len(diff) > 0: + changed = True + if not module.check_mode: + remove_method(name=name, item={item: diff}) + + diff = list(set(module_list) - set(ipa_list)) + if len(diff) > 0: + changed = True + if not module.check_mode: + add_method(name=name, item={item: diff}) + + return changed + + +def ensure(module, client): + name = module.params['name'] + state = module.params['state'] + + if state in ['present', 'enabled']: + ipaenabledflag = 'TRUE' + else: + ipaenabledflag = 'NO' + + host = module.params['host'] + hostcategory = module.params['hostcategory'] + hostgroup = module.params['hostgroup'] + service = module.params['service'] + servicecategory = module.params['servicecategory'] + servicegroup = module.params['servicegroup'] + sourcehost = module.params['sourcehost'] + sourcehostcategory = module.params['sourcehostcategory'] + sourcehostgroup = module.params['sourcehostgroup'] + user = module.params['user'] + usercategory = module.params['usercategory'] + usergroup = module.params['usergroup'] + + module_hbacrule = get_hbacrule_dict(description=module.params['description'], + hostcategory=hostcategory, + ipaenabledflag=ipaenabledflag, + servicecategory=servicecategory, + sourcehostcategory=sourcehostcategory, + usercategory=usercategory) + ipa_hbacrule = client.hbacrule_find(name=name) + + changed = False + if state in ['present', 'enabled', 'disabled']: + if not ipa_hbacrule: + changed = True + if not module.check_mode: + ipa_hbacrule = client.hbacrule_add(name=name, item=module_hbacrule) + else: + diff = get_hbcarule_diff(ipa_hbacrule, module_hbacrule) + if len(diff) > 0: + changed = True + if not module.check_mode: + data = {} + for key in diff: + data[key] = module_hbacrule.get(key) + client.hbacrule_mod(name=name, item=data) + + if host is not None: + changed = modify_if_diff(module, name, ipa_hbacrule.get('memberhost_host', []), host, + client.hbacrule_add_host, + client.hbacrule_remove_host, 'host') or changed + + if hostgroup is not None: + changed = modify_if_diff(module, name, ipa_hbacrule.get('memberhost_hostgroup', []), hostgroup, + client.hbacrule_add_host, + client.hbacrule_remove_host, 'hostgroup') or changed + + if service is not None: + changed = modify_if_diff(module, name, ipa_hbacrule.get('memberservice_hbacsvc', []), service, + client.hbacrule_add_service, + client.hbacrule_remove_service, 'hbacsvc') or changed + + if servicegroup is not None: + changed = modify_if_diff(module, name, ipa_hbacrule.get('memberservice_hbacsvcgroup', []), + servicegroup, + client.hbacrule_add_service, + client.hbacrule_remove_service, 'hbacsvcgroup') or changed + + if sourcehost is not None: + changed = modify_if_diff(module, name, ipa_hbacrule.get('sourcehost_host', []), sourcehost, + client.hbacrule_add_sourcehost, + client.hbacrule_remove_sourcehost, 'host') or changed + + if sourcehostgroup is not None: + changed = modify_if_diff(module, name, ipa_hbacrule.get('sourcehost_group', []), sourcehostgroup, + client.hbacrule_add_sourcehost, + client.hbacrule_remove_sourcehost, 'hostgroup') or changed + + if user is not None: + changed = modify_if_diff(module, name, ipa_hbacrule.get('memberuser_user', []), user, + client.hbacrule_add_user, + client.hbacrule_remove_user, 'user') or changed + + if usergroup is not None: + changed = modify_if_diff(module, name, ipa_hbacrule.get('memberuser_group', []), usergroup, + client.hbacrule_add_user, + client.hbacrule_remove_user, 'group') or changed + else: + if ipa_hbacrule: + changed = True + if not module.check_mode: + client.hbacrule_del(name=name) + + return changed, client.hbacrule_find(name=name) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + cn=dict(type='str', required=True, aliases=['name']), + description=dict(type='str', required=False), + host=dict(type='list', required=False), + hostcategory=dict(type='str', required=False, choices=['all']), + hostgroup=dict(type='list', required=False), + service=dict(type='list', required=False), + servicecategory=dict(type='str', required=False, choices=['all']), + servicegroup=dict(type='list', required=False), + sourcehost=dict(type='list', required=False), + sourcehostcategory=dict(type='str', required=False, choices=['all']), + sourcehostgroup=dict(type='list', required=False), + state=dict(type='str', required=False, default='present', + choices=['present', 'absent', 'enabled', 'disabled']), + user=dict(type='list', required=False), + usercategory=dict(type='str', required=False, choices=['all']), + usergroup=dict(type='list', required=False), + ipa_prot=dict(type='str', required=False, default='https', choices=['http', 'https']), + ipa_host=dict(type='str', required=False, default='ipa.example.com'), + ipa_port=dict(type='int', required=False, default=443), + ipa_user=dict(type='str', required=False, default='admin'), + ipa_pass=dict(type='str', required=True, no_log=True), + validate_certs=dict(type='bool', required=False, default=True), + ), + supports_check_mode=True, + ) + + client = IPAClient(module=module, + host=module.params['ipa_host'], + port=module.params['ipa_port'], + protocol=module.params['ipa_prot']) + + try: + client.login(username=module.params['ipa_user'], + password=module.params['ipa_pass']) + changed, hbacrule = ensure(module, client) + module.exit_json(changed=changed, hbacrule=hbacrule) + except Exception: + e = get_exception() + module.fail_json(msg=str(e)) + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import fetch_url + +if __name__ == '__main__': + main() diff --git a/identity/ipa/ipa_host.py b/identity/ipa/ipa_host.py new file mode 100644 index 00000000000..1ca4113b9d6 --- /dev/null +++ b/identity/ipa/ipa_host.py @@ -0,0 +1,378 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ipa_host +short_description: Manage FreeIPA host +description: +- Add, modify and delete an IPA host using IPA API +options: + fqdn: + description: + - Full qualified domain name. + - Can not be changed as it is the unique identifier. + required: true + aliases: ["name"] + description: + description: + - A description of this host. + required: false + force: + description: + - Force host name even if not in DNS. + required: false + ip_address: + description: + - Add the host to DNS with this IP address. + required: false + mac_address: + description: + - List of Hardware MAC address(es) off this host. + - If option is omitted MAC addresses will not be checked or changed. + - If an empty list is passed all assigned MAC addresses will be removed. + - MAC addresses that are already assigned but not passed will be removed. + required: false + aliases: ["macaddress"] + ns_host_location: + description: + - Host location (e.g. "Lab 2") + required: false + aliases: ["nshostlocation"] + ns_hardware_platform: + description: + - Host hardware platform (e.g. "Lenovo T61") + required: false + aliases: ["nshardwareplatform"] + ns_os_version: + description: + - Host operating system and version (e.g. "Fedora 9") + required: false + aliases: ["nsosversion"] + user_certificate: + description: + - List of Base-64 encoded server certificates. + - If option is ommitted certificates will not be checked or changed. + - If an emtpy list is passed all assigned certificates will be removed. + - Certificates already assigned but not passed will be removed. + required: false + aliases: ["usercertificate"] + state: + description: State to ensure + required: false + default: present + choices: ["present", "absent", "disabled"] + ipa_port: + description: Port of IPA server + required: false + default: 443 + ipa_host: + description: IP or hostname of IPA server + required: false + default: ipa.example.com + ipa_user: + description: Administrative account used on IPA server + required: false + default: admin + ipa_pass: + description: Password of administrative user + required: true + ipa_prot: + description: Protocol used by IPA server + required: false + default: https + choices: ["http", "https"] + validate_certs: + description: + - This only applies if C(ipa_prot) is I(https). + - If set to C(no), the SSL certificates will not be validated. + - This should only set to C(no) used on personally controlled sites using self-signed certificates. + required: false + default: true +version_added: "2.3" +requirements: +- json +''' + +EXAMPLES = ''' +# Ensure host is present +- ipa_host: + name: host01.example.com + description: Example host + ip_address: 192.168.0.123 + ns_host_location: Lab + ns_os_version: CentOS 7 + ns_hardware_platform: Lenovo T61 + mac_address: + - "08:00:27:E3:B1:2D" + - "52:54:00:BD:97:1E" + state: present + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret + +# Ensure host is disabled +- ipa_host: + name: host01.example.com + state: disabled + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret + +# Ensure that all user certificates are removed +- ipa_host: + name: host01.example.com + user_certificate: [] + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret + +# Ensure host is absent +- ipa_host: + name: host01.example.com + state: absent + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret +''' + +RETURN = ''' +host: + description: Host as returned by IPA API. + returned: always + type: dict +host_diff: + description: List of options that differ and would be changed + returned: if check mode and a difference is found + type: list +''' + +try: + import json +except ImportError: + import simplejson as json + + +class IPAClient: + def __init__(self, module, host, port, protocol): + self.host = host + self.port = port + self.protocol = protocol + self.module = module + self.headers = None + + def get_base_url(self): + return '%s://%s/ipa' % (self.protocol, self.host) + + def get_json_url(self): + return '%s/session/json' % self.get_base_url() + + def login(self, username, password): + url = '%s/session/login_password' % self.get_base_url() + data = 'user=%s&password=%s' % (username, password) + headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'text/plain'} + try: + resp, info = fetch_url(module=self.module, url=url, data=data, headers=headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail('login', info['body']) + + self.headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': resp.info().getheader('Set-Cookie')} + except Exception: + e = get_exception() + self._fail('login', str(e)) + + def _fail(self, msg, e): + if 'message' in e: + err_string = e.get('message') + else: + err_string = e + self.module.fail_json(msg='%s: %s' % (msg, err_string)) + + def _post_json(self, method, name, item=None): + if item is None: + item = {} + url = '%s/session/json' % self.get_base_url() + data = {'method': method, 'params': [[name], item]} + try: + resp, info = fetch_url(module=self.module, url=url, data=json.dumps(data), headers=self.headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail(method, info['body']) + except Exception: + e = get_exception() + self._fail('post %s' % method, str(e)) + + resp = json.loads(resp.read()) + err = resp.get('error') + if err is not None: + self._fail('repsonse %s' % method, err) + + if 'result' in resp: + result = resp.get('result') + if 'result' in result: + result = result.get('result') + if isinstance(result, list): + if len(result) > 0: + return result[0] + return result + return None + + def host_find(self, name): + return self._post_json(method='host_find', name=None, item={'all': True, 'fqdn': name}) + + def host_add(self, name, host): + return self._post_json(method='host_add', name=name, item=host) + + def host_mod(self, name, host): + return self._post_json(method='host_mod', name=name, item=host) + + def host_del(self, name): + return self._post_json(method='host_del', name=name) + + def host_disable(self, name): + return self._post_json(method='host_disable', name=name) + + +def get_host_dict(description=None, force=None, ip_address=None, ns_host_location=None, ns_hardware_platform=None, + ns_os_version=None, user_certificate=None, mac_address=None): + data = {} + if description is not None: + data['description'] = description + if force is not None: + data['force'] = force + if ip_address is not None: + data['ip_address'] = ip_address + if ns_host_location is not None: + data['nshostlocation'] = ns_host_location + if ns_hardware_platform is not None: + data['nshardwareplatform'] = ns_hardware_platform + if ns_os_version is not None: + data['nsosversion'] = ns_os_version + if user_certificate is not None: + data['usercertificate'] = [{"__base64__": item} for item in user_certificate] + if mac_address is not None: + data['macaddress'] = mac_address + return data + + +def get_host_diff(ipa_host, module_host): + non_updateable_keys = ['force', 'ip_address'] + data = [] + for key in non_updateable_keys: + if key in module_host: + del module_host[key] + for key in module_host.keys(): + ipa_value = ipa_host.get(key, None) + module_value = module_host.get(key, None) + if isinstance(ipa_value, list) and not isinstance(module_value, list): + module_value = [module_value] + if isinstance(ipa_value, list) and isinstance(module_value, list): + ipa_value = sorted(ipa_value) + module_value = sorted(module_value) + if ipa_value != module_value: + data.append(key) + return data + + +def ensure(module, client): + name = module.params['name'] + state = module.params['state'] + + ipa_host = client.host_find(name=name) + module_host = get_host_dict(description=module.params['description'], + force=module.params['force'], ip_address=module.params['ip_address'], + ns_host_location=module.params['ns_host_location'], + ns_hardware_platform=module.params['ns_hardware_platform'], + ns_os_version=module.params['ns_os_version'], + user_certificate=module.params['user_certificate'], + mac_address=module.params['mac_address']) + changed = False + if state in ['present', 'enabled', 'disabled']: + if not ipa_host: + changed = True + if not module.check_mode: + client.host_add(name=name, host=module_host) + else: + diff = get_host_diff(ipa_host, module_host) + if len(diff) > 0: + changed = True + if not module.check_mode: + data = {} + for key in diff: + data[key] = module_host.get(key) + client.host_mod(name=name, host=data) + + else: + if ipa_host: + changed = True + if not module.check_mode: + client.host_del(name=name) + + return changed, client.host_find(name=name) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + description=dict(type='str', required=False), + fqdn=dict(type='str', required=True, aliases=['name']), + force=dict(type='bool', required=False), + ip_address=dict(type='str', required=False), + ns_host_location=dict(type='str', required=False, aliases=['nshostlocation']), + ns_hardware_platform=dict(type='str', required=False, aliases=['nshardwareplatform']), + ns_os_version=dict(type='str', required=False, aliases=['nsosversion']), + user_certificate=dict(type='list', required=False, aliases=['usercertificate']), + mac_address=dict(type='list', required=False, aliases=['macaddress']), + state=dict(type='str', required=False, default='present', + choices=['present', 'absent', 'enabled', 'disabled']), + ipa_prot=dict(type='str', required=False, default='https', choices=['http', 'https']), + ipa_host=dict(type='str', required=False, default='ipa.example.com'), + ipa_port=dict(type='int', required=False, default=443), + ipa_user=dict(type='str', required=False, default='admin'), + ipa_pass=dict(type='str', required=True, no_log=True), + validate_certs=dict(type='bool', required=False, default=True), + ), + supports_check_mode=True, + ) + + client = IPAClient(module=module, + host=module.params['ipa_host'], + port=module.params['ipa_port'], + protocol=module.params['ipa_prot']) + + try: + client.login(username=module.params['ipa_user'], + password=module.params['ipa_pass']) + changed, host = ensure(module, client) + module.exit_json(changed=changed, host=host) + except Exception: + e = get_exception() + module.fail_json(msg=str(e)) + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import fetch_url + +if __name__ == '__main__': + main() diff --git a/identity/ipa/ipa_hostgroup.py b/identity/ipa/ipa_hostgroup.py new file mode 100644 index 00000000000..50e66428805 --- /dev/null +++ b/identity/ipa/ipa_hostgroup.py @@ -0,0 +1,343 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ipa_hostgroup +short_description: Manage FreeIPA host-group +description: +- Add, modify and delete an IPA host-group using IPA API +options: + cn: + description: + - Name of host-group. + - Can not be changed as it is the unique identifier. + required: true + aliases: ["name"] + description: + description: + - Description + required: false + host: + description: + - List of hosts that belong to the host-group. + - If an empty list is passed all hosts will be removed from the group. + - If option is omitted hosts will not be checked or changed. + - If option is passed all assigned hosts that are not passed will be unassigned from the group. + required: false + hostgroup: + description: + - List of host-groups than belong to that host-group. + - If an empty list is passed all host-groups will be removed from the group. + - If option is omitted host-groups will not be checked or changed. + - If option is passed all assigned hostgroups that are not passed will be unassigned from the group. + required: false + state: + description: + - State to ensure. + required: false + default: "present" + choices: ["present", "absent"] + ipa_port: + description: Port of IPA server + required: false + default: 443 + ipa_host: + description: IP or hostname of IPA server + required: false + default: "ipa.example.com" + ipa_user: + description: Administrative account used on IPA server + required: false + default: "admin" + ipa_pass: + description: Password of administrative user + required: true + ipa_prot: + description: Protocol used by IPA server + required: false + default: "https" + choices: ["http", "https"] + validate_certs: + description: + - This only applies if C(ipa_prot) is I(https). + - If set to C(no), the SSL certificates will not be validated. + - This should only set to C(no) used on personally controlled sites using self-signed certificates. + required: false + default: true +version_added: "2.3" +requirements: +- json +''' + +EXAMPLES = ''' +# Ensure host-group databases is present +- ipa_hostgroup: + name: databases + state: present + host: + - db.example.com + hostgroup: + - mysql-server + - oracle-server + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret + +# Ensure host-group databases is absent +- ipa_hostgroup: + name: databases + state: absent + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret +''' + +RETURN = ''' +hostgroup: + description: Hostgroup as returned by IPA API. + returned: always + type: dict +''' + +try: + import json +except ImportError: + import simplejson as json + + +class IPAClient: + def __init__(self, module, host, port, protocol): + self.host = host + self.port = port + self.protocol = protocol + self.module = module + self.headers = None + + def get_base_url(self): + return '%s://%s/ipa' % (self.protocol, self.host) + + def get_json_url(self): + return '%s/session/json' % self.get_base_url() + + def login(self, username, password): + url = '%s/session/login_password' % self.get_base_url() + data = 'user=%s&password=%s' % (username, password) + headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'text/plain'} + try: + resp, info = fetch_url(module=self.module, url=url, data=data, headers=headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail('login', info['body']) + + self.headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': resp.info().getheader('Set-Cookie')} + except Exception: + e = get_exception() + self._fail('login', str(e)) + + def _fail(self, msg, e): + if 'message' in e: + err_string = e.get('message') + else: + err_string = e + self.module.fail_json(msg='%s: %s' % (msg, err_string)) + + def _post_json(self, method, name, item=None): + if item is None: + item = {} + url = '%s/session/json' % self.get_base_url() + data = {'method': method, 'params': [[name], item]} + try: + resp, info = fetch_url(module=self.module, url=url, data=json.dumps(data), headers=self.headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail(method, info['body']) + except Exception: + e = get_exception() + self._fail('post %s' % method, str(e)) + + resp = json.loads(resp.read()) + err = resp.get('error') + if err is not None: + self._fail('repsonse %s' % method, err) + + if 'result' in resp: + result = resp.get('result') + if 'result' in result: + result = result.get('result') + if isinstance(result, list): + if len(result) > 0: + return result[0] + return result + return None + + def hostgroup_find(self, name): + return self._post_json(method='hostgroup_find', name=None, item={'all': True, 'cn': name}) + + def hostgroup_add(self, name, item): + return self._post_json(method='hostgroup_add', name=name, item=item) + + def hostgroup_mod(self, name, item): + return self._post_json(method='hostgroup_mod', name=name, item=item) + + def hostgroup_del(self, name): + return self._post_json(method='hostgroup_del', name=name) + + def hostgroup_add_member(self, name, item): + return self._post_json(method='hostgroup_add_member', name=name, item=item) + + def hostgroup_add_host(self, name, item): + return self.hostgroup_add_member(name=name, item={'host': item}) + + def hostgroup_add_hostgroup(self, name, item): + return self.hostgroup_add_member(name=name, item={'hostgroup': item}) + + def hostgroup_remove_member(self, name, item): + return self._post_json(method='hostgroup_remove_member', name=name, item=item) + + def hostgroup_remove_host(self, name, item): + return self.hostgroup_remove_member(name=name, item={'host': item}) + + def hostgroup_remove_hostgroup(self, name, item): + return self.hostgroup_remove_member(name=name, item={'hostgroup': item}) + + +def get_hostgroup_dict(description=None): + data = {} + if description is not None: + data['description'] = description + return data + + +def get_hostgroup_diff(ipa_hostgroup, module_hostgroup): + data = [] + for key in module_hostgroup.keys(): + ipa_value = ipa_hostgroup.get(key, None) + module_value = module_hostgroup.get(key, None) + if isinstance(ipa_value, list) and not isinstance(module_value, list): + module_value = [module_value] + if isinstance(ipa_value, list) and isinstance(module_value, list): + ipa_value = sorted(ipa_value) + module_value = sorted(module_value) + if ipa_value != module_value: + data.append(key) + return data + + +def modify_if_diff(module, name, ipa_list, module_list, add_method, remove_method): + changed = False + diff = list(set(ipa_list) - set(module_list)) + if len(diff) > 0: + changed = True + if not module.check_mode: + remove_method(name=name, item=diff) + + diff = list(set(module_list) - set(ipa_list)) + if len(diff) > 0: + changed = True + if not module.check_mode: + add_method(name=name, item=diff) + return changed + + +def ensure(module, client): + name = module.params['name'] + state = module.params['state'] + host = module.params['host'] + hostgroup = module.params['hostgroup'] + + ipa_hostgroup = client.hostgroup_find(name=name) + module_hostgroup = get_hostgroup_dict(description=module.params['description']) + + changed = False + if state == 'present': + if not ipa_hostgroup: + changed = True + if not module.check_mode: + ipa_hostgroup = client.hostgroup_add(name=name, item=module_hostgroup) + else: + diff = get_hostgroup_diff(ipa_hostgroup, module_hostgroup) + if len(diff) > 0: + changed = True + if not module.check_mode: + data = {} + for key in diff: + data[key] = module_hostgroup.get(key) + client.hostgroup_mod(name=name, item=data) + + if host is not None: + changed = modify_if_diff(module, name, ipa_hostgroup.get('member_host', []), host, + client.hostgroup_add_host, client.hostgroup_remove_host) or changed + + if hostgroup is not None: + changed = modify_if_diff(module, name, ipa_hostgroup.get('member_hostgroup', []), hostgroup, + client.hostgroup_add_hostgroup, client.hostgroup_remove_hostgroup) or changed + + else: + if ipa_hostgroup: + changed = True + if not module.check_mode: + client.hostgroup_del(name=name) + + return changed, client.hostgroup_find(name=name) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + cn=dict(type='str', required=True, aliases=['name']), + description=dict(type='str', required=False), + host=dict(type='list', required=False), + hostgroup=dict(type='list', required=False), + state=dict(type='str', required=False, default='present', + choices=['present', 'absent', 'enabled', 'disabled']), + ipa_prot=dict(type='str', required=False, default='https', choices=['http', 'https']), + ipa_host=dict(type='str', required=False, default='ipa.example.com'), + ipa_port=dict(type='int', required=False, default=443), + ipa_user=dict(type='str', required=False, default='admin'), + ipa_pass=dict(type='str', required=True, no_log=True), + validate_certs=dict(type='bool', required=False, default=True), + ), + supports_check_mode=True, + ) + + client = IPAClient(module=module, + host=module.params['ipa_host'], + port=module.params['ipa_port'], + protocol=module.params['ipa_prot']) + + try: + client.login(username=module.params['ipa_user'], + password=module.params['ipa_pass']) + changed, hostgroup = ensure(module, client) + module.exit_json(changed=changed, hostgroup=hostgroup) + except Exception: + e = get_exception() + module.fail_json(msg=str(e)) + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import fetch_url + +if __name__ == '__main__': + main() diff --git a/identity/ipa/ipa_role.py b/identity/ipa/ipa_role.py new file mode 100644 index 00000000000..48508a256ae --- /dev/null +++ b/identity/ipa/ipa_role.py @@ -0,0 +1,411 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ipa_role +short_description: Manage FreeIPA role +description: +- Add, modify and delete a role within FreeIPA server using FreeIPA API +options: + cn: + description: + - Role name. + - Can not be changed as it is the unique identifier. + required: true + aliases: ['name'] + description: + description: + - A description of this role-group. + required: false + group: + description: + - List of group names assign to this role. + - If an empty list is passed all assigned groups will be unassigned from the role. + - If option is omitted groups will not be checked or changed. + - If option is passed all assigned groups that are not passed will be unassigned from the role. + host: + description: + - List of host names to assign. + - If an empty list is passed all assigned hosts will be unassigned from the role. + - If option is omitted hosts will not be checked or changed. + - If option is passed all assigned hosts that are not passed will be unassigned from the role. + required: false + hostgroup: + description: + - List of host group names to assign. + - If an empty list is passed all assigned host groups will be removed from the role. + - If option is omitted host groups will not be checked or changed. + - If option is passed all assigned hostgroups that are not passed will be unassigned from the role. + required: false + service: + description: + - List of service names to assign. + - If an empty list is passed all assigned services will be removed from the role. + - If option is omitted services will not be checked or changed. + - If option is passed all assigned services that are not passed will be removed from the role. + required: false + state: + description: State to ensure + required: false + default: "present" + choices: ["present", "absent"] + user: + description: + - List of user names to assign. + - If an empty list is passed all assigned users will be removed from the role. + - If option is omitted users will not be checked or changed. + required: false + ipa_port: + description: Port of IPA server + required: false + default: 443 + ipa_host: + description: IP or hostname of IPA server + required: false + default: "ipa.example.com" + ipa_user: + description: Administrative account used on IPA server + required: false + default: "admin" + ipa_pass: + description: Password of administrative user + required: true + ipa_prot: + description: Protocol used by IPA server + required: false + default: "https" + choices: ["http", "https"] + validate_certs: + description: + - This only applies if C(ipa_prot) is I(https). + - If set to C(no), the SSL certificates will not be validated. + - This should only set to C(no) used on personally controlled sites using self-signed certificates. + required: false + default: true +version_added: "2.3" +requirements: +- json +''' + +EXAMPLES = ''' +# Ensure role is present +- ipa_role: + name: dba + description: Database Administrators + state: present + user: + - pinky + - brain + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret + +# Ensure role with certain details +- ipa_role: + name: another-role + description: Just another role + group: + - editors + host: + - host01.example.com + hostgroup: + - hostgroup01 + service: + - service01 + +# Ensure role is absent +- ipa_role: + name: dba + state: absent + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret +''' + +RETURN = ''' +role: + description: Role as returned by IPA API. + returned: always + type: dict +''' + +try: + import json +except ImportError: + import simplejson as json + + +class IPAClient: + def __init__(self, module, host, port, protocol): + self.host = host + self.port = port + self.protocol = protocol + self.module = module + self.headers = None + + def get_base_url(self): + return '%s://%s/ipa' % (self.protocol, self.host) + + def get_json_url(self): + return '%s/session/json' % self.get_base_url() + + def login(self, username, password): + url = '%s/session/login_password' % self.get_base_url() + data = 'user=%s&password=%s' % (username, password) + headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'text/plain'} + try: + resp, info = fetch_url(module=self.module, url=url, data=data, headers=headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail('login', info['body']) + + self.headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': resp.info().getheader('Set-Cookie')} + except Exception: + e = get_exception() + self._fail('login', str(e)) + + def _fail(self, msg, e): + if 'message' in e: + err_string = e.get('message') + else: + err_string = e + self.module.fail_json(msg='%s: %s' % (msg, err_string)) + + def _post_json(self, method, name, item=None): + if item is None: + item = {} + url = '%s/session/json' % self.get_base_url() + data = {'method': method, 'params': [[name], item]} + try: + resp, info = fetch_url(module=self.module, url=url, data=json.dumps(data), headers=self.headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail(method, info['body']) + except Exception: + e = get_exception() + self._fail('post %s' % method, str(e)) + + resp = json.loads(resp.read()) + err = resp.get('error') + if err is not None: + self._fail('repsonse %s' % method, err) + + if 'result' in resp: + result = resp.get('result') + if 'result' in result: + result = result.get('result') + if isinstance(result, list): + if len(result) > 0: + return result[0] + return result + return None + + def role_find(self, name): + return self._post_json(method='role_find', name=None, item={'all': True, 'cn': name}) + + def role_add(self, name, item): + return self._post_json(method='role_add', name=name, item=item) + + def role_mod(self, name, item): + return self._post_json(method='role_mod', name=name, item=item) + + def role_del(self, name): + return self._post_json(method='role_del', name=name) + + def role_add_member(self, name, item): + return self._post_json(method='role_add_member', name=name, item=item) + + def role_add_group(self, name, item): + return self.role_add_member(name=name, item={'group': item}) + + def role_add_host(self, name, item): + return self.role_add_member(name=name, item={'host': item}) + + def role_add_hostgroup(self, name, item): + return self.role_add_member(name=name, item={'hostgroup': item}) + + def role_add_service(self, name, item): + return self.role_add_member(name=name, item={'service': item}) + + def role_add_user(self, name, item): + return self.role_add_member(name=name, item={'user': item}) + + def role_remove_member(self, name, item): + return self._post_json(method='role_remove_member', name=name, item=item) + + def role_remove_group(self, name, item): + return self.role_remove_member(name=name, item={'group': item}) + + def role_remove_host(self, name, item): + return self.role_remove_member(name=name, item={'host': item}) + + def role_remove_hostgroup(self, name, item): + return self.role_remove_member(name=name, item={'hostgroup': item}) + + def role_remove_service(self, name, item): + return self.role_remove_member(name=name, item={'service': item}) + + def role_remove_user(self, name, item): + return self.role_remove_member(name=name, item={'user': item}) + + +def get_role_dict(description=None): + data = {} + if description is not None: + data['description'] = description + return data + + +def get_role_diff(ipa_role, module_role): + data = [] + for key in module_role.keys(): + module_value = module_role.get(key, None) + ipa_value = ipa_role.get(key, None) + if isinstance(ipa_value, list) and not isinstance(module_value, list): + module_value = [module_value] + if isinstance(ipa_value, list) and isinstance(module_value, list): + ipa_value = sorted(ipa_value) + module_value = sorted(module_value) + if ipa_value != module_value: + data.append(key) + return data + + +def modify_if_diff(module, name, ipa_list, module_list, add_method, remove_method): + changed = False + diff = list(set(ipa_list) - set(module_list)) + if len(diff) > 0: + changed = True + if not module.check_mode: + remove_method(name=name, item=diff) + + diff = list(set(module_list) - set(ipa_list)) + if len(diff) > 0: + changed = True + if not module.check_mode: + add_method(name=name, item=diff) + return changed + + +def ensure(module, client): + state = module.params['state'] + name = module.params['name'] + group = module.params['group'] + host = module.params['host'] + hostgroup = module.params['hostgroup'] + service = module.params['service'] + user = module.params['user'] + + module_role = get_role_dict(description=module.params['description']) + ipa_role = client.role_find(name=name) + + changed = False + if state == 'present': + if not ipa_role: + changed = True + if not module.check_mode: + ipa_role = client.role_add(name=name, item=module_role) + else: + diff = get_role_diff(ipa_role=ipa_role, module_role=module_role) + if len(diff) > 0: + changed = True + if not module.check_mode: + data = {} + for key in diff: + data[key] = module_role.get(key) + client.role_mod(name=name, item=data) + + if group is not None: + changed = modify_if_diff(module, name, ipa_role.get('member_group', []), group, + client.role_add_group, + client.role_remove_group) or changed + + if host is not None: + changed = modify_if_diff(module, name, ipa_role.get('member_host', []), host, + client.role_add_host, + client.role_remove_host) or changed + + if hostgroup is not None: + changed = modify_if_diff(module, name, ipa_role.get('member_hostgroup', []), hostgroup, + client.role_add_hostgroup, + client.role_remove_hostgroup) or changed + + if service is not None: + changed = modify_if_diff(module, name, ipa_role.get('member_service', []), service, + client.role_add_service, + client.role_remove_service) or changed + if user is not None: + changed = modify_if_diff(module, name, ipa_role.get('member_user', []), user, + client.role_add_user, + client.role_remove_user) or changed + else: + if ipa_role: + changed = True + if not module.check_mode: + client.role_del(name) + + return changed, client.role_find(name=name) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + cn=dict(type='str', required=True, aliases=['name']), + description=dict(type='str', required=False), + group=dict(type='list', required=False), + host=dict(type='list', required=False), + hostgroup=dict(type='list', required=False), + service=dict(type='list', required=False), + state=dict(type='str', required=False, default='present', choices=['present', 'absent']), + user=dict(type='list', required=False), + ipa_prot=dict(type='str', required=False, default='https', choices=['http', 'https']), + ipa_host=dict(type='str', required=False, default='ipa.example.com'), + ipa_port=dict(type='int', required=False, default=443), + ipa_user=dict(type='str', required=False, default='admin'), + ipa_pass=dict(type='str', required=True, no_log=True), + validate_certs=dict(type='bool', required=False, default=True), + ), + supports_check_mode=True, + ) + + client = IPAClient(module=module, + host=module.params['ipa_host'], + port=module.params['ipa_port'], + protocol=module.params['ipa_prot']) + + try: + client.login(username=module.params['ipa_user'], + password=module.params['ipa_pass']) + changed, role = ensure(module, client) + module.exit_json(changed=changed, role=role) + except Exception: + e = get_exception() + module.fail_json(msg=str(e)) + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import fetch_url + +if __name__ == '__main__': + main() diff --git a/identity/ipa/ipa_sudocmd.py b/identity/ipa/ipa_sudocmd.py new file mode 100644 index 00000000000..5b9dbec5fde --- /dev/null +++ b/identity/ipa/ipa_sudocmd.py @@ -0,0 +1,275 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ipa_sudocmd +author: Thomas Krahn (@Nosmoht) +short_description: Manage FreeIPA sudo command +description: +- Add, modify or delete sudo command within FreeIPA server using FreeIPA API. +options: + sudocmd: + description: + - Sudo Command. + aliases: ['name'] + required: true + description: + description: + - A description of this command. + required: false + state: + description: State to ensure + required: false + default: present + choices: ['present', 'absent'] + ipa_port: + description: Port of IPA server + required: false + default: 443 + ipa_host: + description: IP or hostname of IPA server + required: false + default: "ipa.example.com" + ipa_user: + description: Administrative account used on IPA server + required: false + default: "admin" + ipa_pass: + description: Password of administrative user + required: true + ipa_prot: + description: Protocol used by IPA server + required: false + default: "https" + choices: ["http", "https"] + validate_certs: + description: + - This only applies if C(ipa_prot) is I(https). + - If set to C(no), the SSL certificates will not be validated. + - This should only set to C(no) used on personally controlled sites using self-signed certificates. + required: false + default: true +version_added: "2.3" +requirements: +- json +''' + +EXAMPLES = ''' +# Ensure sudo command exists +- ipa_sudocmd: + name: su + description: Allow to run su via sudo + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret + +# Ensure sudo command does not exist +- ipa_sudocmd: + name: su + state: absent + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret +''' + +RETURN = ''' +sudocmd: + description: Sudo command as return from IPA API + returned: always + type: dict +''' + +try: + import json +except ImportError: + import simplejson as json + + +class IPAClient: + def __init__(self, module, host, port, protocol): + self.host = host + self.port = port + self.protocol = protocol + self.module = module + self.headers = None + + def get_base_url(self): + return '%s://%s/ipa' % (self.protocol, self.host) + + def get_json_url(self): + return '%s/session/json' % self.get_base_url() + + def login(self, username, password): + url = '%s/session/login_password' % self.get_base_url() + data = 'user=%s&password=%s' % (username, password) + headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'text/plain'} + try: + resp, info = fetch_url(module=self.module, url=url, data=data, headers=headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail('login', info['body']) + + self.headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': resp.info().getheader('Set-Cookie')} + except Exception: + e = get_exception() + self._fail('login', str(e)) + + def _fail(self, msg, e): + if 'message' in e: + err_string = e.get('message') + else: + err_string = e + self.module.fail_json(msg='%s: %s' % (msg, err_string)) + + def _post_json(self, method, name, item=None): + if item is None: + item = {} + url = '%s/session/json' % self.get_base_url() + data = {'method': method, 'params': [[name], item]} + try: + resp, info = fetch_url(module=self.module, url=url, data=json.dumps(data), headers=self.headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail(method, info['body']) + except Exception: + e = get_exception() + self._fail('post %s' % method, str(e)) + + resp = json.loads(resp.read()) + err = resp.get('error') + if err is not None: + self._fail('repsonse %s' % method, err) + + if 'result' in resp: + result = resp.get('result') + if 'result' in result: + result = result.get('result') + if isinstance(result, list): + if len(result) > 0: + return result[0] + return result + return None + + def sudocmd_find(self, name): + return self._post_json(method='sudocmd_find', name=None, item={'all': True, 'sudocmd': name}) + + def sudocmd_add(self, name, item): + return self._post_json(method='sudocmd_add', name=name, item=item) + + def sudocmd_mod(self, name, item): + return self._post_json(method='sudocmd_mod', name=name, item=item) + + def sudocmd_del(self, name): + return self._post_json(method='sudocmd_del', name=name) + + +def get_sudocmd_dict(description=None): + data = {} + if description is not None: + data['description'] = description + return data + + +def get_sudocmd_diff(ipa_sudocmd, module_sudocmd): + data = [] + for key in module_sudocmd.keys(): + module_value = module_sudocmd.get(key, None) + ipa_value = ipa_sudocmd.get(key, None) + if isinstance(ipa_value, list) and not isinstance(module_value, list): + module_value = [module_value] + if isinstance(ipa_value, list) and isinstance(module_value, list): + ipa_value = sorted(ipa_value) + module_value = sorted(module_value) + if ipa_value != module_value: + data.append(key) + return data + + +def ensure(module, client): + name = module.params['sudocmd'] + state = module.params['state'] + + module_sudocmd = get_sudocmd_dict(description=module.params['description']) + ipa_sudocmd = client.sudocmd_find(name=name) + + changed = False + if state == 'present': + if not ipa_sudocmd: + changed = True + if not module.check_mode: + client.sudocmd_add(name=name, item=module_sudocmd) + else: + diff = get_sudocmd_diff(ipa_sudocmd, module_sudocmd) + if len(diff) > 0: + changed = True + if not module.check_mode: + data = {} + for key in diff: + data[key] = module_sudocmd.get(key) + client.sudocmd_mod(name=name, item=data) + else: + if ipa_sudocmd: + changed = True + if not module.check_mode: + client.sudocmd_del(name=name) + + return changed, client.sudocmd_find(name=name) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + description=dict(type='str', required=False), + state=dict(type='str', required=False, default='present', + choices=['present', 'absent', 'enabled', 'disabled']), + sudocmd=dict(type='str', required=True, aliases=['name']), + ipa_prot=dict(type='str', required=False, default='https', choices=['http', 'https']), + ipa_host=dict(type='str', required=False, default='ipa.example.com'), + ipa_port=dict(type='int', required=False, default=443), + ipa_user=dict(type='str', required=False, default='admin'), + ipa_pass=dict(type='str', required=True, no_log=True), + validate_certs=dict(type='bool', required=False, default=True), + ), + supports_check_mode=True, + ) + + client = IPAClient(module=module, + host=module.params['ipa_host'], + port=module.params['ipa_port'], + protocol=module.params['ipa_prot']) + try: + client.login(username=module.params['ipa_user'], + password=module.params['ipa_pass']) + changed, sudocmd = ensure(module, client) + module.exit_json(changed=changed, sudocmd=sudocmd) + except Exception: + e = get_exception() + module.fail_json(msg=str(e)) + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import fetch_url + +if __name__ == '__main__': + main() diff --git a/identity/ipa/ipa_sudocmdgroup.py b/identity/ipa/ipa_sudocmdgroup.py new file mode 100644 index 00000000000..182bf82806a --- /dev/null +++ b/identity/ipa/ipa_sudocmdgroup.py @@ -0,0 +1,317 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ipa_sudocmdgroup +author: Thomas Krahn (@Nosmoht) +short_description: Manage FreeIPA sudo command group +description: +- Add, modify or delete sudo command group within IPA server using IPA API. +options: + cn: + description: + - Sudo Command Group. + aliases: ['name'] + required: true + description: + description: + - Group description. + state: + description: State to ensure + required: false + default: present + choices: ['present', 'absent'] + sudocmd: + description: + - List of sudo commands to assign to the group. + - If an empty list is passed all assigned commands will be removed from the group. + - If option is omitted sudo commands will not be checked or changed. + required: false + ipa_port: + description: Port of IPA server + required: false + default: 443 + ipa_host: + description: IP or hostname of IPA server + required: false + default: "ipa.example.com" + ipa_user: + description: Administrative account used on IPA server + required: false + default: "admin" + ipa_pass: + description: Password of administrative user + required: true + ipa_prot: + description: Protocol used by IPA server + required: false + default: "https" + choices: ["http", "https"] + validate_certs: + description: + - This only applies if C(ipa_prot) is I(https). + - If set to C(no), the SSL certificates will not be validated. + - This should only set to C(no) used on personally controlled sites using self-signed certificates. + required: false + default: true +version_added: "2.3" +requirements: +- json +''' + +EXAMPLES = ''' +- name: Ensure sudo command group exists + ipa_sudocmdgroup: + name: group01 + description: Group of important commands + sudocmd: + - su + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret + +- name: Ensure sudo command group does not exists + ipa_sudocmdgroup: + name: group01 + state: absent + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret +''' + +RETURN = ''' +sudocmdgroup: + description: Sudo command group as returned by IPA API + returned: always + type: dict +''' + +try: + import json +except ImportError: + import simplejson as json + + +class IPAClient: + def __init__(self, module, host, port, protocol): + self.host = host + self.port = port + self.protocol = protocol + self.module = module + self.headers = None + + def get_base_url(self): + return '%s://%s/ipa' % (self.protocol, self.host) + + def get_json_url(self): + return '%s/session/json' % self.get_base_url() + + def login(self, username, password): + url = '%s/session/login_password' % self.get_base_url() + data = 'user=%s&password=%s' % (username, password) + headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'text/plain'} + try: + resp, info = fetch_url(module=self.module, url=url, data=data, headers=headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail('login', info['body']) + + self.headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': resp.info().getheader('Set-Cookie')} + except Exception: + e = get_exception() + self._fail('login', str(e)) + + def _fail(self, msg, e): + if 'message' in e: + err_string = e.get('message') + else: + err_string = e + self.module.fail_json(msg='%s: %s' % (msg, err_string)) + + def _post_json(self, method, name, item=None): + if item is None: + item = {} + url = '%s/session/json' % self.get_base_url() + data = {'method': method, 'params': [[name], item]} + try: + resp, info = fetch_url(module=self.module, url=url, data=json.dumps(data), headers=self.headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail(method, info['body']) + except Exception: + e = get_exception() + self._fail('post %s' % method, str(e)) + + resp = json.loads(resp.read()) + err = resp.get('error') + if err is not None: + self._fail('repsonse %s' % method, err) + + if 'result' in resp: + result = resp.get('result') + if 'result' in result: + result = result.get('result') + if isinstance(result, list): + if len(result) > 0: + return result[0] + return result + return None + + def sudocmdgroup_find(self, name): + return self._post_json(method='sudocmdgroup_find', name=None, item={'all': True, 'cn': name}) + + def sudocmdgroup_add(self, name, item): + return self._post_json(method='sudocmdgroup_add', name=name, item=item) + + def sudocmdgroup_mod(self, name, item): + return self._post_json(method='sudocmdgroup_mod', name=name, item=item) + + def sudocmdgroup_del(self, name): + return self._post_json(method='sudocmdgroup_del', name=name) + + def sudocmdgroup_add_member(self, name, item): + return self._post_json(method='sudocmdgroup_add_member', name=name, item=item) + + def sudocmdgroup_add_member_sudocmd(self, name, item): + return self.sudocmdgroup_add_member(name=name, item={'sudocmd': item}) + + def sudocmdgroup_remove_member(self, name, item): + return self._post_json(method='sudocmdgroup_remove_member', name=name, item=item) + + def sudocmdgroup_remove_member_sudocmd(self, name, item): + return self.sudocmdgroup_remove_member(name=name, item={'sudocmd': item}) + + +def get_sudocmdgroup_dict(description=None): + data = {} + if description is not None: + data['description'] = description + return data + + +def modify_if_diff(module, name, ipa_list, module_list, add_method, remove_method): + changed = False + diff = list(set(ipa_list) - set(module_list)) + if len(diff) > 0: + changed = True + if not module.check_mode: + remove_method(name=name, item=diff) + + diff = list(set(module_list) - set(ipa_list)) + if len(diff) > 0: + changed = True + if not module.check_mode: + add_method(name=name, item=diff) + return changed + + +def get_sudocmdgroup_diff(ipa_sudocmdgroup, module_sudocmdgroup): + data = [] + for key in module_sudocmdgroup.keys(): + module_value = module_sudocmdgroup.get(key, None) + ipa_value = ipa_sudocmdgroup.get(key, None) + if isinstance(ipa_value, list) and not isinstance(module_value, list): + module_value = [module_value] + if isinstance(ipa_value, list) and isinstance(module_value, list): + ipa_value = sorted(ipa_value) + module_value = sorted(module_value) + if ipa_value != module_value: + data.append(key) + return data + + +def ensure(module, client): + name = module.params['name'] + state = module.params['state'] + sudocmd = module.params['sudocmd'] + + module_sudocmdgroup = get_sudocmdgroup_dict(description=module.params['description']) + ipa_sudocmdgroup = client.sudocmdgroup_find(name=name) + + changed = False + if state == 'present': + if not ipa_sudocmdgroup: + changed = True + if not module.check_mode: + ipa_sudocmdgroup = client.sudocmdgroup_add(name=name, item=module_sudocmdgroup) + else: + diff = get_sudocmdgroup_diff(ipa_sudocmdgroup, module_sudocmdgroup) + if len(diff) > 0: + changed = True + if not module.check_mode: + data = {} + for key in diff: + data[key] = module_sudocmdgroup.get(key) + client.sudocmdgroup_mod(name=name, item=data) + + if sudocmd is not None: + changed = modify_if_diff(module, name, ipa_sudocmdgroup.get('member_sudocmd', []), sudocmd, + client.sudocmdgroup_add_member_sudocmd, + client.sudocmdgroup_remove_member_sudocmd) + else: + if ipa_sudocmdgroup: + changed = True + if not module.check_mode: + client.sudocmdgroup_del(name=name) + + return changed, client.sudocmdgroup_find(name=name) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + cn=dict(type='str', required=True, aliases=['name']), + description=dict(type='str', required=False), + state=dict(type='str', required=False, default='present', + choices=['present', 'absent', 'enabled', 'disabled']), + sudocmd=dict(type='list', required=False), + ipa_prot=dict(type='str', required=False, default='https', choices=['http', 'https']), + ipa_host=dict(type='str', required=False, default='ipa.example.com'), + ipa_port=dict(type='int', required=False, default=443), + ipa_user=dict(type='str', required=False, default='admin'), + ipa_pass=dict(type='str', required=True, no_log=True), + validate_certs=dict(type='bool', required=False, default=True), + ), + supports_check_mode=True, + ) + + client = IPAClient(module=module, + host=module.params['ipa_host'], + port=module.params['ipa_port'], + protocol=module.params['ipa_prot']) + try: + client.login(username=module.params['ipa_user'], + password=module.params['ipa_pass']) + changed, sudocmdgroup = ensure(module, client) + module.exit_json(changed=changed, sudorule=sudocmdgroup) + except Exception: + e = get_exception() + module.fail_json(msg=str(e)) + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import fetch_url + +if __name__ == '__main__': + main() diff --git a/identity/ipa/ipa_sudorule.py b/identity/ipa/ipa_sudorule.py new file mode 100644 index 00000000000..162baad5d8c --- /dev/null +++ b/identity/ipa/ipa_sudorule.py @@ -0,0 +1,491 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ipa_sudorule +author: Thomas Krahn (@Nosmoht) +short_description: Manage FreeIPA sudo rule +description: +- Add, modify or delete sudo rule within IPA server using IPA API. +options: + cn: + description: + - Canonical name. + - Can not be changed as it is the unique identifier. + required: true + aliases: ['name'] + cmdcategory: + description: + - Command category the rule applies to. + choices: ['all'] + required: false + cmd: + description: + - List of commands assigned to the rule. + - If an empty list is passed all commands will be removed from the rule. + - If option is omitted commands will not be checked or changed. + required: false + host: + description: + - List of hosts assigned to the rule. + - If an empty list is passed all hosts will be removed from the rule. + - If option is omitted hosts will not be checked or changed. + - Option C(hostcategory) must be omitted to assign hosts. + required: false + hostcategory: + description: + - Host category the rule applies to. + - If 'all' is passed one must omit C(host) and C(hostgroup). + - Option C(host) and C(hostgroup) must be omitted to assign 'all'. + choices: ['all'] + required: false + hostgroup: + description: + - List of host groups assigned to the rule. + - If an empty list is passed all host groups will be removed from the rule. + - If option is omitted host groups will not be checked or changed. + - Option C(hostcategory) must be omitted to assign host groups. + required: false + user: + description: + - List of users assigned to the rule. + - If an empty list is passed all users will be removed from the rule. + - If option is omitted users will not be checked or changed. + required: false + usercategory: + description: + - User category the rule applies to. + choices: ['all'] + required: false + usergroup: + description: + - List of user groups assigned to the rule. + - If an empty list is passed all user groups will be removed from the rule. + - If option is omitted user groups will not be checked or changed. + required: false + state: + description: State to ensure + required: false + default: present + choices: ['present', 'absent', 'enabled', 'disabled'] + ipa_port: + description: Port of IPA server + required: false + default: 443 + ipa_host: + description: IP or hostname of IPA server + required: false + default: "ipa.example.com" + ipa_user: + description: Administrative account used on IPA server + required: false + default: "admin" + ipa_pass: + description: Password of administrative user + required: true + ipa_prot: + description: Protocol used by IPA server + required: false + default: "https" + choices: ["http", "https"] + validate_certs: + description: + - This only applies if C(ipa_prot) is I(https). + - If set to C(no), the SSL certificates will not be validated. + - This should only set to C(no) used on personally controlled sites using self-signed certificates. + required: false + default: true +version_added: "2.3" +requirements: +- json +''' + +EXAMPLES = ''' +# Ensure sudo rule is present thats allows all every body to execute any command on any host without beeing asked for a password. +- ipa_sudorule: + name: sudo_all_nopasswd + cmdcategory: all + description: Allow to run every command with sudo without password + hostcategory: all + sudoopt: + - '!authenticate' + usercategory: all + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret +# Ensure user group developers can run every command on host group db-server as well as on host db01.example.com. +- ipa_sudorule: + name: sudo_dev_dbserver + description: Allow developers to run every command with sudo on all database server + cmdcategory: all + host: + - db01.example.com + hostgroup: + - db-server + sudoopt: + - '!authenticate' + usergroup: + - developers + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret +''' + +RETURN = ''' +sudorule: + description: Sudorule as returned by IPA + returned: always + type: dict +''' + +try: + import json +except ImportError: + import simplejson as json + + +class IPAClient: + def __init__(self, module, host, port, protocol): + self.host = host + self.port = port + self.protocol = protocol + self.module = module + self.headers = None + + def get_base_url(self): + return '%s://%s/ipa' % (self.protocol, self.host) + + def get_json_url(self): + return '%s/session/json' % self.get_base_url() + + def login(self, username, password): + url = '%s/session/login_password' % self.get_base_url() + data = 'user=%s&password=%s' % (username, password) + headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'text/plain'} + try: + resp, info = fetch_url(module=self.module, url=url, data=data, headers=headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail('login', info['body']) + + self.headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': resp.info().getheader('Set-Cookie')} + except Exception: + e = get_exception() + self._fail('login', str(e)) + + def _fail(self, msg, e): + if 'message' in e: + err_string = e.get('message') + else: + err_string = e + self.module.fail_json(msg='%s: %s' % (msg, err_string)) + + def _post_json(self, method, name, item=None): + if item is None: + item = {} + url = '%s/session/json' % self.get_base_url() + data = {'method': method, 'params': [[name], item]} + try: + resp, info = fetch_url(module=self.module, url=url, data=json.dumps(data), headers=self.headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail(method, info['body']) + except Exception: + e = get_exception() + self._fail('post %s' % method, str(e)) + + resp = json.loads(resp.read()) + err = resp.get('error') + if err is not None: + self._fail('repsonse %s' % method, err) + + if 'result' in resp: + result = resp.get('result') + if 'result' in result: + result = result.get('result') + if isinstance(result, list): + if len(result) > 0: + return result[0] + return result + return None + + def sudorule_find(self, name): + return self._post_json(method='sudorule_find', name=None, item={'all': True, 'cn': name}) + + def sudorule_add(self, name, item): + return self._post_json(method='sudorule_add', name=name, item=item) + + def sudorule_mod(self, name, item): + return self._post_json(method='sudorule_mod', name=name, item=item) + + def sudorule_del(self, name): + return self._post_json(method='sudorule_del', name=name) + + def sudorule_add_option(self, name, item): + return self._post_json(method='sudorule_add_option', name=name, item=item) + + def sudorule_add_option_ipasudoopt(self, name, item): + return self.sudorule_add_option(name=name, item={'ipasudoopt': item}) + + def sudorule_remove_option(self, name, item): + return self._post_json(method='sudorule_remove_option', name=name, item=item) + + def sudorule_remove_option_ipasudoopt(self, name, item): + return self.sudorule_remove_option(name=name, item={'ipasudoopt': item}) + + def sudorule_add_host(self, name, item): + return self._post_json(method='sudorule_add_host', name=name, item=item) + + def sudorule_add_host_host(self, name, item): + return self.sudorule_add_host(name=name, item={'host': item}) + + def sudorule_add_host_hostgroup(self, name, item): + return self.sudorule_add_host(name=name, item={'hostgroup': item}) + + def sudorule_remove_host(self, name, item): + return self._post_json(method='sudorule_remove_host', name=name, item=item) + + def sudorule_remove_host_host(self, name, item): + return self.sudorule_remove_host(name=name, item={'host': item}) + + def sudorule_remove_host_hostgroup(self, name, item): + return self.sudorule_remove_host(name=name, item={'hostgroup': item}) + + def sudorule_add_allow_command(self, name, item): + return self._post_json(method='sudorule_add_allow_command', name=name, item=item) + + def sudorule_remove_allow_command(self, name, item): + return self._post_json(method='sudorule_remove_allow_command', name=name, item=item) + + def sudorule_add_user(self, name, item): + return self._post_json(method='sudorule_add_user', name=name, item=item) + + def sudorule_add_user_user(self, name, item): + return self.sudorule_add_user(name=name, item={'user': item}) + + def sudorule_add_user_group(self, name, item): + return self.sudorule_add_user(name=name, item={'group': item}) + + def sudorule_remove_user(self, name, item): + return self._post_json(method='sudorule_remove_user', name=name, item=item) + + def sudorule_remove_user_user(self, name, item): + return self.sudorule_remove_user(name=name, item={'user': item}) + + def sudorule_remove_user_group(self, name, item): + return self.sudorule_remove_user(name=name, item={'group': item}) + + +def get_sudorule_dict(cmdcategory=None, description=None, hostcategory=None, ipaenabledflag=None, usercategory=None): + data = {} + if cmdcategory is not None: + data['cmdcategory'] = cmdcategory + if description is not None: + data['description'] = description + if hostcategory is not None: + data['hostcategory'] = hostcategory + if ipaenabledflag is not None: + data['ipaenabledflag'] = ipaenabledflag + if usercategory is not None: + data['usercategory'] = usercategory + return data + + +def get_sudorule_diff(ipa_sudorule, module_sudorule): + data = [] + for key in module_sudorule.keys(): + module_value = module_sudorule.get(key, None) + ipa_value = ipa_sudorule.get(key, None) + if isinstance(ipa_value, list) and not isinstance(module_value, list): + module_value = [module_value] + if isinstance(ipa_value, list) and isinstance(module_value, list): + ipa_value = sorted(ipa_value) + module_value = sorted(module_value) + if ipa_value != module_value: + data.append(key) + return data + + +def modify_if_diff(module, name, ipa_list, module_list, add_method, remove_method): + changed = False + diff = list(set(ipa_list) - set(module_list)) + if len(diff) > 0: + changed = True + if not module.check_mode: + for item in diff: + remove_method(name=name, item=item) + + diff = list(set(module_list) - set(ipa_list)) + if len(diff) > 0: + changed = True + if not module.check_mode: + for item in diff: + add_method(name=name, item=item) + + return changed + + +def category_changed(module, client, category_name, ipa_sudorule): + if ipa_sudorule.get(category_name, None) == ['all']: + if not module.check_mode: + client.sudorule_mod(name=ipa_sudorule.get('cn'), item={category_name: None}) + return True + return False + + +def ensure(module, client): + state = module.params['state'] + name = module.params['name'] + cmd = module.params['cmd'] + cmdcategory = module.params['cmdcategory'] + host = module.params['host'] + hostcategory = module.params['hostcategory'] + hostgroup = module.params['hostgroup'] + + if state in ['present', 'enabled']: + ipaenabledflag = 'TRUE' + else: + ipaenabledflag = 'NO' + + sudoopt = module.params['sudoopt'] + user = module.params['user'] + usercategory = module.params['usercategory'] + usergroup = module.params['usergroup'] + + module_sudorule = get_sudorule_dict(cmdcategory=cmdcategory, + description=module.params['description'], + hostcategory=hostcategory, + ipaenabledflag=ipaenabledflag, + usercategory=usercategory) + ipa_sudorule = client.sudorule_find(name=name) + + changed = False + if state in ['present', 'disabled', 'enabled']: + if not ipa_sudorule: + changed = True + if not module.check_mode: + ipa_sudorule = client.sudorule_add(name=name, item=module_sudorule) + else: + diff = get_sudorule_diff(ipa_sudorule, module_sudorule) + if len(diff) > 0: + changed = True + if not module.check_mode: + if 'hostcategory' in diff: + if ipa_sudorule.get('memberhost_host', None) is not None: + client.sudorule_remove_host_host(name=name, item=ipa_sudorule.get('memberhost_host')) + if ipa_sudorule.get('memberhost_hostgroup', None) is not None: + client.sudorule_remove_host_hostgroup(name=name, + item=ipa_sudorule.get('memberhost_hostgroup')) + + client.sudorule_mod(name=name, item=module_sudorule) + + if cmd is not None: + changed = category_changed(module, client, 'cmdcategory', ipa_sudorule) or changed + if not module.check_mode: + client.sudorule_add_allow_command(name=name, item=cmd) + + if host is not None: + changed = category_changed(module, client, 'hostcategory', ipa_sudorule) or changed + changed = modify_if_diff(module, name, ipa_sudorule.get('memberhost_host', []), host, + client.sudorule_add_host_host, + client.sudorule_remove_host_host) or changed + + if hostgroup is not None: + changed = category_changed(module, client, 'hostcategory', ipa_sudorule) or changed + changed = modify_if_diff(module, name, ipa_sudorule.get('memberhost_hostgroup', []), hostgroup, + client.sudorule_add_host_hostgroup, + client.sudorule_remove_host_hostgroup) or changed + if sudoopt is not None: + changed = modify_if_diff(module, name, ipa_sudorule.get('ipasudoopt', []), sudoopt, + client.sudorule_add_option_ipasudoopt, + client.sudorule_remove_option_ipasudoopt) or changed + if user is not None: + changed = category_changed(module, client, 'usercategory', ipa_sudorule) or changed + changed = modify_if_diff(module, name, ipa_sudorule.get('memberuser_user', []), user, + client.sudorule_add_user_user, + client.sudorule_remove_user_user) or changed + if usergroup is not None: + changed = category_changed(module, client, 'usercategory', ipa_sudorule) or changed + changed = modify_if_diff(module, name, ipa_sudorule.get('memberuser_group', []), usergroup, + client.sudorule_add_user_group, + client.sudorule_remove_user_group) or changed + else: + if ipa_sudorule: + changed = True + if not module.check_mode: + client.sudorule_del(name) + + return changed, client.sudorule_find(name) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + cmd=dict(type='list', required=False), + cmdcategory=dict(type='str', required=False, choices=['all']), + cn=dict(type='str', required=True, aliases=['name']), + description=dict(type='str', required=False), + host=dict(type='list', required=False), + hostcategory=dict(type='str', required=False, choices=['all']), + hostgroup=dict(type='list', required=False), + sudoopt=dict(type='list', required=False), + state=dict(type='str', required=False, default='present', + choices=['present', 'absent', 'enabled', 'disabled']), + user=dict(type='list', required=False), + usercategory=dict(type='str', required=False, choices=['all']), + usergroup=dict(type='list', required=False), + ipa_prot=dict(type='str', required=False, default='https', choices=['http', 'https']), + ipa_host=dict(type='str', required=False, default='ipa.example.com'), + ipa_port=dict(type='int', required=False, default=443), + ipa_user=dict(type='str', required=False, default='admin'), + ipa_pass=dict(type='str', required=True, no_log=True), + validate_certs=dict(type='bool', required=False, default=True), + ), + mutually_exclusive=[['cmdcategory', 'cmd'], + ['hostcategory', 'host'], + ['hostcategory', 'hostgroup'], + ['usercategory', 'user'], + ['usercategory', 'usergroup']], + supports_check_mode=True, + ) + + client = IPAClient(module=module, + host=module.params['ipa_host'], + port=module.params['ipa_port'], + protocol=module.params['ipa_prot']) + try: + client.login(username=module.params['ipa_user'], + password=module.params['ipa_pass']) + changed, sudorule = ensure(module, client) + module.exit_json(changed=changed, sudorule=sudorule) + except Exception: + e = get_exception() + module.fail_json(msg=str(e)) + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import fetch_url + +if __name__ == '__main__': + main() diff --git a/identity/ipa/ipa_user.py b/identity/ipa/ipa_user.py new file mode 100644 index 00000000000..8a5c4b09168 --- /dev/null +++ b/identity/ipa/ipa_user.py @@ -0,0 +1,401 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ipa_user +short_description: Manage FreeIPA users +description: +- Add, modify and delete user within IPA server +options: + displayname: + description: Display name + required: false + givenname: + description: First name + required: false + loginshell: + description: Login shell + required: false + mail: + description: + - List of mail addresses assigned to the user. + - If an empty list is passed all assigned email addresses will be deleted. + - If None is passed email addresses will not be checked or changed. + required: false + password: + description: + - Password + required: false + sn: + description: Surname + required: false + sshpubkey: + description: + - List of public SSH key. + - If an empty list is passed all assigned public keys will be deleted. + - If None is passed SSH public keys will not be checked or changed. + required: false + state: + description: State to ensure + required: false + default: "present" + choices: ["present", "absent", "enabled", "disabled"] + telephonenumber: + description: + - List of telephone numbers assigned to the user. + - If an empty list is passed all assigned telephone numbers will be deleted. + - If None is passed telephone numbers will not be checked or changed. + required: false + title: + description: Title + required: false + uid: + description: uid of the user + required: true + aliases: ["name"] + ipa_port: + description: Port of IPA server + required: false + default: 443 + ipa_host: + description: IP or hostname of IPA server + required: false + default: "ipa.example.com" + ipa_user: + description: Administrative account used on IPA server + required: false + default: "admin" + ipa_pass: + description: Password of administrative user + required: true + ipa_prot: + description: Protocol used by IPA server + required: false + default: "https" + choices: ["http", "https"] + validate_certs: + description: + - This only applies if C(ipa_prot) is I(https). + - If set to C(no), the SSL certificates will not be validated. + - This should only set to C(no) used on personally controlled sites using self-signed certificates. + required: false + default: true +version_added: "2.3" +requirements: +- base64 +- hashlib +- json +''' + +EXAMPLES = ''' +# Ensure pinky is present +- ipa_user: + name: pinky + state: present + givenname: Pinky + sn: Acme + mail: + - pinky@acme.com + telephonenumber: + - '+555123456' + sshpubkeyfp: + - ssh-rsa .... + - ssh-dsa .... + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret + +# Ensure brain is absent +- ipa_user: + name: brain + state: absent + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret +''' + +RETURN = ''' +user: + description: User as returned by IPA API + returned: always + type: dict +''' + +import base64 +import hashlib + +try: + import json +except ImportError: + import simplejson as json + + +class IPAClient: + def __init__(self, module, host, port, protocol): + self.host = host + self.port = port + self.protocol = protocol + self.module = module + self.headers = None + + def get_base_url(self): + return '%s://%s/ipa' % (self.protocol, self.host) + + def get_json_url(self): + return '%s/session/json' % self.get_base_url() + + def login(self, username, password): + url = '%s/session/login_password' % self.get_base_url() + data = 'user=%s&password=%s' % (username, password) + headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'text/plain'} + try: + resp, info = fetch_url(module=self.module, url=url, data=data, headers=headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail('login', info['body']) + + self.headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': resp.info().getheader('Set-Cookie')} + except Exception: + e = get_exception() + self._fail('login', str(e)) + + def _fail(self, msg, e): + if 'message' in e: + err_string = e.get('message') + else: + err_string = e + self.module.fail_json(msg='%s: %s' % (msg, err_string)) + + def _post_json(self, method, name, item=None): + if item is None: + item = {} + url = '%s/session/json' % self.get_base_url() + data = {'method': method, 'params': [[name], item]} + try: + resp, info = fetch_url(module=self.module, url=url, data=json.dumps(data), headers=self.headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail(method, info['body']) + except Exception: + e = get_exception() + self._fail('post %s' % method, str(e)) + + resp = json.loads(resp.read()) + err = resp.get('error') + if err is not None: + self._fail('repsonse %s' % method, err) + + if 'result' in resp: + result = resp.get('result') + if 'result' in result: + result = result.get('result') + if isinstance(result, list): + if len(result) > 0: + return result[0] + return result + return None + + def user_find(self, name): + return self._post_json(method='user_find', name=None, item={'all': True, 'uid': name}) + + def user_add(self, name, item): + return self._post_json(method='user_add', name=name, item=item) + + def user_mod(self, name, item): + return self._post_json(method='user_mod', name=name, item=item) + + def user_del(self, name): + return self._post_json(method='user_del', name=name) + + def user_disable(self, name): + return self._post_json(method='user_disable', name=name) + + def user_enable(self, name): + return self._post_json(method='user_enable', name=name) + + +def get_user_dict(givenname=None, loginshell=None, mail=None, nsaccountlock=False, sn=None, sshpubkey=None, + telephonenumber=None, + title=None): + user = {} + if givenname is not None: + user['givenname'] = givenname + if loginshell is not None: + user['loginshell'] = loginshell + if mail is not None: + user['mail'] = mail + user['nsaccountlock'] = nsaccountlock + if sn is not None: + user['sn'] = sn + if sshpubkey is not None: + user['ipasshpubkey'] = sshpubkey + if telephonenumber is not None: + user['telephonenumber'] = telephonenumber + if title is not None: + user['title'] = title + + return user + + +def get_user_diff(ipa_user, module_user): + """ + Return the keys of each dict whereas values are different. Unfortunately the IPA + API returns everything as a list even if only a single value is possible. + Therefore some more complexity is needed. + The method will check if the value type of module_user.attr is not a list and + create a list with that element if the same attribute in ipa_user is list. In this way i hope that the method + must not be changed if the returned API dict is changed. + :param ipa_user: + :param module_user: + :return: + """ + # return [item for item in module_user.keys() if module_user.get(item, None) != ipa_user.get(item, None)] + result = [] + # sshpubkeyfp is the list of ssh key fingerprints. IPA doesn't return the keys itself but instead the fingerprints. + # These are used for comparison. + sshpubkey = None + if 'ipasshpubkey' in module_user: + module_user['sshpubkeyfp'] = [get_ssh_key_fingerprint(pubkey) for pubkey in module_user['ipasshpubkey']] + # Remove the ipasshpubkey element as it is not returned from IPA but save it's value to be used later on + sshpubkey = module_user['ipasshpubkey'] + del module_user['ipasshpubkey'] + for key in module_user.keys(): + mod_value = module_user.get(key, None) + ipa_value = ipa_user.get(key, None) + if isinstance(ipa_value, list) and not isinstance(mod_value, list): + mod_value = [mod_value] + if isinstance(ipa_value, list) and isinstance(mod_value, list): + mod_value = sorted(mod_value) + ipa_value = sorted(ipa_value) + if mod_value != ipa_value: + result.append(key) + # If there are public keys, remove the fingerprints and add them back to the dict + if sshpubkey is not None: + del module_user['sshpubkeyfp'] + module_user['ipasshpubkey'] = sshpubkey + return result + + +def get_ssh_key_fingerprint(ssh_key): + """ + Return the public key fingerprint of a given public SSH key + in format "FB:0C:AC:0A:07:94:5B:CE:75:6E:63:32:13:AD:AD:D7 (ssh-rsa)" + :param ssh_key: + :return: + """ + parts = ssh_key.strip().split() + if len(parts) == 0: + return None + key_type = parts[0] + key = base64.b64decode(parts[1].encode('ascii')) + + fp_plain = hashlib.md5(key).hexdigest() + return ':'.join(a + b for a, b in zip(fp_plain[::2], fp_plain[1::2])).upper() + ' (%s)' % key_type + + +def ensure(module, client): + state = module.params['state'] + name = module.params['name'] + nsaccountlock = state == 'disabled' + + module_user = get_user_dict(givenname=module.params.get('givenname'), loginshell=module.params['loginshell'], + mail=module.params['mail'], sn=module.params['sn'], + sshpubkey=module.params['sshpubkey'], nsaccountlock=nsaccountlock, + telephonenumber=module.params['telephonenumber'], title=module.params['title']) + + ipa_user = client.user_find(name=name) + + changed = False + if state in ['present', 'enabled', 'disabled']: + if not ipa_user: + changed = True + if not module.check_mode: + ipa_user = client.user_add(name=name, item=module_user) + else: + diff = get_user_diff(ipa_user, module_user) + if len(diff) > 0: + changed = True + if not module.check_mode: + ipa_user = client.user_mod(name=name, item=module_user) + else: + if ipa_user: + changed = True + if not module.check_mode: + client.user_del(name) + + return changed, ipa_user + + +def main(): + module = AnsibleModule( + argument_spec=dict( + displayname=dict(type='str', required=False), + givenname=dict(type='str', required=False), + loginshell=dict(type='str', required=False), + mail=dict(type='list', required=False), + sn=dict(type='str', required=False), + uid=dict(type='str', required=True, aliases=['name']), + password=dict(type='str', required=False, no_log=True), + sshpubkey=dict(type='list', required=False), + state=dict(type='str', required=False, default='present', + choices=['present', 'absent', 'enabled', 'disabled']), + telephonenumber=dict(type='list', required=False), + title=dict(type='str', required=False), + ipa_prot=dict(type='str', required=False, default='https', choices=['http', 'https']), + ipa_host=dict(type='str', required=False, default='ipa.example.com'), + ipa_port=dict(type='int', required=False, default=443), + ipa_user=dict(type='str', required=False, default='admin'), + ipa_pass=dict(type='str', required=True, no_log=True), + validate_certs=dict(type='bool', required=False, default=True), + ), + supports_check_mode=True, + ) + + client = IPAClient(module=module, + host=module.params['ipa_host'], + port=module.params['ipa_port'], + protocol=module.params['ipa_prot']) + + # If sshpubkey is defined as None than module.params['sshpubkey'] is [None]. IPA itself returns None (not a list). + # Therefore a small check here to replace list(None) by None. Otherwise get_user_diff() would return sshpubkey + # as different which should be avoided. + if module.params['sshpubkey'] is not None: + if len(module.params['sshpubkey']) == 1 and module.params['sshpubkey'][0] is "": + module.params['sshpubkey'] = None + + try: + client.login(username=module.params['ipa_user'], + password=module.params['ipa_pass']) + changed, user = ensure(module, client) + module.exit_json(changed=changed, user=user) + except Exception: + e = get_exception() + module.fail_json(msg=str(e)) + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import fetch_url + +if __name__ == '__main__': + main() From f941dbd143745f87d320a61b984ce99db44581b3 Mon Sep 17 00:00:00 2001 From: jctanner Date: Wed, 9 Nov 2016 15:25:17 -0500 Subject: [PATCH 2342/2522] More isinstance fixes (#3405) * More isinstance fixes * Use double types --- clustering/znode.py | 2 +- network/nmcli.py | 8 ++++---- system/osx_defaults.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/clustering/znode.py b/clustering/znode.py index aff1cd1d224..6d01b869c58 100644 --- a/clustering/znode.py +++ b/clustering/znode.py @@ -194,7 +194,7 @@ def _get(self, path): for i in dir(zstat): if not i.startswith('_'): attr = getattr(zstat, i) - if type(attr) in (int, str): + if isinstance(attr, (int, str)): stat_dict[i] = attr result = True, {'msg': 'The node was retrieved.', 'znode': path, 'value': value, 'stat': stat_dict} diff --git a/network/nmcli.py b/network/nmcli.py index 8078e9da4b1..00bab41760a 100644 --- a/network/nmcli.py +++ b/network/nmcli.py @@ -504,13 +504,13 @@ def dict_to_string(self, d): val=d[key] str_val="" add_string=True - if type(val)==type(dbus.Array([])): + if isinstance(val, dbus.Array): for elt in val: - if type(elt)==type(dbus.Byte(1)): + if isinstance(elt, dbus.Byte): str_val+="%s " % int(elt) - elif type(elt)==type(dbus.String("")): + elif isinstance(elt, dbus.String): str_val+="%s" % elt - elif type(val)==type(dbus.Dictionary({})): + elif isinstance(val, dbus.Dictionary): dstr+=self.dict_to_string(val) add_string=False else: diff --git a/system/osx_defaults.py b/system/osx_defaults.py index a71cecbd7fe..7bec2b99640 100644 --- a/system/osx_defaults.py +++ b/system/osx_defaults.py @@ -238,7 +238,7 @@ def write(self): value = "TRUE" else: value = "FALSE" - elif type(self.value) is int or type(self.value) is float: + elif isinstance(self.value, (int, float)): value = str(self.value) elif self.array_add and self.current_value is not None: value = list(set(self.value) - set(self.current_value)) @@ -285,7 +285,7 @@ def run(self): return True # There is a type mismatch! Given type does not match the type in defaults - if self.current_value is not None and type(self.current_value) is not type(self.value): + if self.current_value is not None and not isinstance(self.current_value, type(self.value)): raise OSXDefaultsException("Type mismatch. Type in defaults: " + type(self.current_value).__name__) # Current value matches the given value. Nothing need to be done. Arrays need extra care From 711647178bdee60bdd27fb2667bae171b49ad9d5 Mon Sep 17 00:00:00 2001 From: Davis Phillips Date: Wed, 9 Nov 2016 14:27:58 -0600 Subject: [PATCH 2343/2522] updated version_added to 2.3 --- cloud/vmware/vmware_guest.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cloud/vmware/vmware_guest.py b/cloud/vmware/vmware_guest.py index e74b530acf6..48fb50f52ea 100644 --- a/cloud/vmware/vmware_guest.py +++ b/cloud/vmware/vmware_guest.py @@ -93,27 +93,27 @@ description: - Should customization spec be run required: False - version_added: "2.2" + version_added: "2.3" ips: description: - IP Addresses to set required: False - version_added: "2.2" + version_added: "2.3" networks: description: - Network to use should include VM network name and gateway required: False - version_added: "2.2" + version_added: "2.3" dns_servers: description: - - DNS servers to use: 4.4.4.4, 8.8.8.8 + - DNS servers to use: ['4.4.4.4', '8.8.8.8'] required: False - version_added: "2.2" + version_added: "2.3" domain: description: - Domain to use while customizing required: False - version_added: "2.2" + version_added: "2.3" extends_documentation_fragment: vmware.documentation ''' From 3f55dc9db7f9996c9697aeff32bb25278a87e743 Mon Sep 17 00:00:00 2001 From: Davis Phillips Date: Wed, 9 Nov 2016 14:36:47 -0600 Subject: [PATCH 2344/2522] Removed list value from description in dns_servers --- cloud/vmware/vmware_guest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/vmware/vmware_guest.py b/cloud/vmware/vmware_guest.py index 48fb50f52ea..540e9e61c9b 100644 --- a/cloud/vmware/vmware_guest.py +++ b/cloud/vmware/vmware_guest.py @@ -106,7 +106,7 @@ version_added: "2.3" dns_servers: description: - - DNS servers to use: ['4.4.4.4', '8.8.8.8'] + - DNS servers to use required: False version_added: "2.3" domain: From b23740e40e38816447e80aa1683a0fd0fa022c5e Mon Sep 17 00:00:00 2001 From: Christophe Biocca Date: Wed, 9 Nov 2016 16:42:27 -0500 Subject: [PATCH 2345/2522] haproxy: Fix compatibility when map is actually imap. (#3350) While I still have no idea why or how the `map` call is being swapped out while still running in python 2.7, this change will fix the following error, as well as improve py3 compatibility. --- network/haproxy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/network/haproxy.py b/network/haproxy.py index 2fc11987d50..0d1db0397e7 100644 --- a/network/haproxy.py +++ b/network/haproxy.py @@ -211,7 +211,7 @@ def discover_all_backends(self): """ data = self.execute('show stat', 200, False).lstrip('# ') r = csv.DictReader(data.splitlines()) - return map(lambda d: d['pxname'], filter(lambda d: d['svname'] == 'BACKEND', r)) + return tuple(map(lambda d: d['pxname'], filter(lambda d: d['svname'] == 'BACKEND', r))) def execute_for_backends(self, cmd, pxname, svname, wait_for_status = None): @@ -244,7 +244,7 @@ def get_state_for(self, pxname, svname): """ data = self.execute('show stat', 200, False).lstrip('# ') r = csv.DictReader(data.splitlines()) - state = map(lambda d: { 'status': d['status'], 'weight': d['weight'] }, filter(lambda d: (pxname is None or d['pxname'] == pxname) and d['svname'] == svname, r)) + state = tuple(map(lambda d: { 'status': d['status'], 'weight': d['weight'] }, filter(lambda d: (pxname is None or d['pxname'] == pxname) and d['svname'] == svname, r))) return state or None From 475d11948decca5b7b03606e3297f411c16d6bb4 Mon Sep 17 00:00:00 2001 From: Jeremy Olexa Date: Wed, 9 Nov 2016 16:00:09 -0600 Subject: [PATCH 2346/2522] consul_kv, consul_acl: fix missing types bool (#3327) * Specify bool in consul_kv: validate_certs param * Specify bool in consul_acl: validate_certs param * Specify bool in consul_kv: retrieve param --- clustering/consul_acl.py | 2 +- clustering/consul_kv.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/clustering/consul_acl.py b/clustering/consul_acl.py index 8e82beeae5e..858f6aea50e 100644 --- a/clustering/consul_acl.py +++ b/clustering/consul_acl.py @@ -336,7 +336,7 @@ def main(): mgmt_token=dict(required=True, no_log=True), host=dict(default='localhost'), scheme=dict(required=False, default='http'), - validate_certs=dict(required=False, default=True), + validate_certs=dict(required=False, type='bool', default=True), name=dict(required=False), port=dict(default=8500, type='int'), rules=dict(default=None, required=False, type='list'), diff --git a/clustering/consul_kv.py b/clustering/consul_kv.py index 7304606d959..e409f2a3a58 100644 --- a/clustering/consul_kv.py +++ b/clustering/consul_kv.py @@ -260,10 +260,10 @@ def main(): key=dict(required=True), host=dict(default='localhost'), scheme=dict(required=False, default='http'), - validate_certs=dict(required=False, default=True), + validate_certs=dict(required=False, type='bool', default=True), port=dict(default=8500, type='int'), recurse=dict(required=False, type='bool'), - retrieve=dict(required=False, default=True), + retrieve=dict(required=False, type='bool', default=True), state=dict(default='present', choices=['present', 'absent', 'acquire', 'release']), token=dict(required=False, no_log=True), value=dict(required=False), From b97494fe1682e19379d342dbbc390e13153bf5fa Mon Sep 17 00:00:00 2001 From: jctanner Date: Wed, 9 Nov 2016 18:59:21 -0500 Subject: [PATCH 2347/2522] Refactor usage of type() (#3412) Addresses https://github.com/ansible/ansible/issues/18440 --- system/osx_defaults.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system/osx_defaults.py b/system/osx_defaults.py index 7bec2b99640..64bf79ab102 100644 --- a/system/osx_defaults.py +++ b/system/osx_defaults.py @@ -285,7 +285,8 @@ def run(self): return True # There is a type mismatch! Given type does not match the type in defaults - if self.current_value is not None and not isinstance(self.current_value, type(self.value)): + value_type = type(self.value) + if self.current_value is not None and not isinstance(self.current_value, value_type): raise OSXDefaultsException("Type mismatch. Type in defaults: " + type(self.current_value).__name__) # Current value matches the given value. Nothing need to be done. Arrays need extra care From 8ed8f7e999f556d050e1c1f6cc84e7fd77f49e9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Thu, 10 Nov 2016 11:13:04 +0100 Subject: [PATCH 2348/2522] new module nginx_status_facts (#3286) * new module nginx_status_facts * nginx_status_facts: remove requirement * nginx_status_facts: implement timeout param * nginx_status_facts: update example docs --- web_infrastructure/nginx_status_facts.py | 160 +++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 web_infrastructure/nginx_status_facts.py diff --git a/web_infrastructure/nginx_status_facts.py b/web_infrastructure/nginx_status_facts.py new file mode 100644 index 00000000000..970cd3fb5ee --- /dev/null +++ b/web_infrastructure/nginx_status_facts.py @@ -0,0 +1,160 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2016, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: nginx_status_facts +short_description: Retrieve nginx status facts. +description: + - Gathers facts from nginx from an URL having C(stub_status) enabled. +version_added: "2.3" +author: "René Moser (@resmo)" +options: + url: + description: + - URL of the nginx status. + required: true + timeout: + description: + - HTTP connection timeout in seconds. + required: false + default: 10 + +notes: + - See http://nginx.org/en/docs/http/ngx_http_stub_status_module.html for more information. +''' + +EXAMPLES = ''' +# Gather status facts from nginx on localhost +- name: get current http stats + nginx_status_facts: + url: http://localhost/nginx_status + +# Gather status facts from nginx on localhost with a custom timeout of 20 seconds +- name: get current http stats + nginx_status_facts: + url: http://localhost/nginx_status + timeout: 20 +''' + +RETURN = ''' +--- +nginx_status_facts.active_connections: + description: Active connections. + returned: success + type: int + sample: 2340 +nginx_status_facts.accepts: + description: The total number of accepted client connections. + returned: success + type: int + sample: 81769947 +nginx_status_facts.handled: + description: The total number of handled connections. Generally, the parameter value is the same as accepts unless some resource limits have been reached. + returned: success + type: int + sample: 81769947 +nginx_status_facts.requests: + description: The total number of client requests. + returned: success + type: int + sample: 144332345 +nginx_status_facts.reading: + description: The current number of connections where nginx is reading the request header. + returned: success + type: int + sample: 0 +nginx_status_facts.writing: + description: The current number of connections where nginx is writing the response back to the client. + returned: success + type: int + sample: 241 +nginx_status_facts.waiting: + description: The current number of idle client connections waiting for a request. + returned: success + type: int + sample: 2092 +nginx_status_facts.data: + description: HTTP response as is. + returned: success + type: string + sample: "Active connections: 2340 \nserver accepts handled requests\n 81769947 81769947 144332345 \nReading: 0 Writing: 241 Waiting: 2092 \n" +''' + +import re +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.urls import fetch_url + + +class NginxStatusFacts(object): + + def __init__(self): + self.url = module.params.get('url') + self.timeout = module.params.get('timeout') + + def run(self): + result = { + 'nginx_status_facts': { + 'active_connections': None, + 'accepts': None, + 'handled': None, + 'requests': None, + 'reading': None, + 'writing': None, + 'waiting': None, + 'data': None, + } + } + (response, info) = fetch_url(module=module, url=self.url, force=True, timeout=self.timeout) + if not response: + module.fail_json(msg="No valid or no response from url %s within %s seconds (timeout)" % (self.url, self.timeout)) + + data = response.read() + if not data: + return result + + result['nginx_status_facts']['data'] = data + match = re.match(r'Active connections: ([0-9]+) \nserver accepts handled requests\n ([0-9]+) ([0-9]+) ([0-9]+) \nReading: ([0-9]+) Writing: ([0-9]+) Waiting: ([0-9]+)', data, re.S) + if match: + result['nginx_status_facts']['active_connections'] = int(match.group(1)) + result['nginx_status_facts']['accepts'] = int(match.group(2)) + result['nginx_status_facts']['handled'] = int(match.group(3)) + result['nginx_status_facts']['requests'] = int(match.group(4)) + result['nginx_status_facts']['reading'] = int(match.group(5)) + result['nginx_status_facts']['writing'] = int(match.group(6)) + result['nginx_status_facts']['waiting'] = int(match.group(7)) + return result + +def main(): + global module + module = AnsibleModule( + argument_spec=dict( + url=dict(required=True), + timeout=dict(type='int', default=10), + ), + supports_check_mode=True, + ) + + nginx_status_facts = NginxStatusFacts().run() + result = dict(changed=False, ansible_facts=nginx_status_facts) + module.exit_json(**result) + +if __name__ == '__main__': + main() From 86db95671b7c12d232c99b8115e6b8047579a5c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Thu, 10 Nov 2016 16:04:11 +0100 Subject: [PATCH 2349/2522] cloudstack: cs_ip_address: fix returns on state=present (#3406) In case poll_job=false the return must be None --- cloud/cloudstack/cs_ip_address.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cloud/cloudstack/cs_ip_address.py b/cloud/cloudstack/cs_ip_address.py index 4d4eae2f787..55eb1e32f6c 100644 --- a/cloud/cloudstack/cs_ip_address.py +++ b/cloud/cloudstack/cs_ip_address.py @@ -202,8 +202,7 @@ def associate_ip_address(self): poll_async = self.module.params.get('poll_async') if poll_async: - res = self.poll_job(res, 'ipaddress') - ip_address = res + ip_address = self.poll_job(res, 'ipaddress') return ip_address From 00d350de4e03b787be04dbae0b844835d10f2035 Mon Sep 17 00:00:00 2001 From: Krzysztof Magosa Date: Thu, 10 Nov 2016 18:33:09 +0100 Subject: [PATCH 2350/2522] tempfile: simple module creating temporary files/directories in OS-independent manner (#2991) --- files/tempfile.py | 106 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 files/tempfile.py diff --git a/files/tempfile.py b/files/tempfile.py new file mode 100644 index 00000000000..88bdf358de7 --- /dev/null +++ b/files/tempfile.py @@ -0,0 +1,106 @@ +#!/usr/bin/python +#coding: utf-8 -*- + +# (c) 2016 Krzysztof Magosa +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: tempfile +version_added: "2.3" +author: + - Krzysztof Magosa +short_description: Creates temporary files and directories. +description: + - The M(tempfile) module creates temporary files and directories. C(mktemp) command takes different parameters on various systems, this module helps to avoid troubles related to that. Files/directories created by module are accessible only by creator. In case you need to make them world-accessible you need to use M(file) module. +options: + state: + description: + - Whether to create file or directory. + required: false + choices: [ "file", "directory" ] + default: file + path: + description: + - Location where temporary file or directory should be created. If path is not specified default system temporary directory will be used. + required: false + default: null + prefix: + description: + - Prefix of file/directory name created by module. + required: false + default: ansible. + suffix: + description: + - Suffix of file/directory name created by module. + required: false + default: "" +''' + +EXAMPLES = """ + - name: create temporary build directory + tempfile: state=directory suffix=build + + - name: create temporary file + tempfile: state=file suffix=temp +""" + +RETURN = ''' +path: + description: Path to created file or directory + returned: success + type: string + sample: "/tmp/ansible.bMlvdk" +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from tempfile import mkstemp, mkdtemp +from os import close + +def main(): + module = AnsibleModule( + argument_spec = dict( + state = dict(default='file', choices=['file', 'directory']), + path = dict(default=None), + prefix = dict(default='ansible.'), + suffix = dict(default='') + ) + ) + + try: + if module.params['state'] == 'file': + handle, path = mkstemp( + prefix=module.params['prefix'], + suffix=module.params['suffix'], + dir=module.params['path'] + ) + close(handle) + elif module.params['state'] == 'directory': + path = mkdtemp( + prefix=module.params['prefix'], + suffix=module.params['suffix'], + dir=module.params['path'] + ) + + module.exit_json(changed=True, path=path) + except Exception: + e = get_exception() + module.fail_json(msg=str(e)) + +if __name__ == '__main__': + main() From 459e585d0cecc1a23b4fe40a4286d91116f89c98 Mon Sep 17 00:00:00 2001 From: Andrew Gaffney Date: Thu, 10 Nov 2016 10:12:03 -0700 Subject: [PATCH 2351/2522] Fix bare variable references in docs --- cloud/amazon/ec2_elb_facts.py | 4 ++-- cloud/azure/azure_rm_deployment.py | 2 +- cloud/misc/virt_net.py | 2 +- cloud/misc/virt_pool.py | 2 +- cloud/smartos/smartos_image_facts.py | 2 +- cloud/vmware/vmware_vsan_cluster.py | 2 +- cloud/xenserver_facts.py | 2 +- network/lldp.py | 2 +- notification/hall.py | 2 +- system/crypttab.py | 2 +- web_infrastructure/jenkins_plugin.py | 12 ++++++------ 11 files changed, 17 insertions(+), 17 deletions(-) diff --git a/cloud/amazon/ec2_elb_facts.py b/cloud/amazon/ec2_elb_facts.py index e386439d1d5..52da210f9b4 100644 --- a/cloud/amazon/ec2_elb_facts.py +++ b/cloud/amazon/ec2_elb_facts.py @@ -47,7 +47,7 @@ - action: module: debug msg: "{{ item.dns_name }}" - with_items: elb_facts.elbs + with_items: "{{ elb_facts.elbs }}" # Gather facts about a particular ELB - action: @@ -70,7 +70,7 @@ - action: module: debug msg: "{{ item.dns_name }}" - with_items: elb_facts.elbs + with_items: "{{ elb_facts.elbs }}" ''' diff --git a/cloud/azure/azure_rm_deployment.py b/cloud/azure/azure_rm_deployment.py index a95ef54a2b4..226fb4e22fb 100644 --- a/cloud/azure/azure_rm_deployment.py +++ b/cloud/azure/azure_rm_deployment.py @@ -159,7 +159,7 @@ - name: Add new instance to host group add_host: hostname={{ item['ips'][0].public_ip }} groupname=azure_vms - with_items: azure.deployment.instances + with_items: "{{ azure.deployment.instances }}" - hosts: azure_vms user: devopscle diff --git a/cloud/misc/virt_net.py b/cloud/misc/virt_net.py index 469603d1aeb..8b277e439fb 100644 --- a/cloud/misc/virt_net.py +++ b/cloud/misc/virt_net.py @@ -97,7 +97,7 @@ # Gather information about network managed by 'libvirt' remotely using uri - virt_net: command=info uri='{{ item }}' - with_items: libvirt_uris + with_items: "{{ libvirt_uris }}" register: networks # Ensure that a network is active (needs to be defined and built first) diff --git a/cloud/misc/virt_pool.py b/cloud/misc/virt_pool.py index 69c52e13c7e..7684125bf8a 100644 --- a/cloud/misc/virt_pool.py +++ b/cloud/misc/virt_pool.py @@ -108,7 +108,7 @@ # Gather information about pools managed by 'libvirt' remotely using uri - virt_pool: command=info uri='{{ item }}' - with_items: libvirt_uris + with_items: "{{ libvirt_uris }}" register: storage_pools # Ensure that a pool is active (needs to be defined and built first) diff --git a/cloud/smartos/smartos_image_facts.py b/cloud/smartos/smartos_image_facts.py index 189389de720..eb3ecd08a3d 100644 --- a/cloud/smartos/smartos_image_facts.py +++ b/cloud/smartos/smartos_image_facts.py @@ -51,7 +51,7 @@ debug: msg="{{ smartos_images[item]['name'] }}-{{smartos_images[item]['version'] }} has {{ smartos_images[item]['clones'] }} VM(s)" -with_items: smartos_images.keys() +with_items: "{{ smartos_images.keys() }}" ''' RETURN = ''' diff --git a/cloud/vmware/vmware_vsan_cluster.py b/cloud/vmware/vmware_vsan_cluster.py index 015386d9064..3f2bad1fa80 100644 --- a/cloud/vmware/vmware_vsan_cluster.py +++ b/cloud/vmware/vmware_vsan_cluster.py @@ -61,7 +61,7 @@ username: "{{ esxi_username }}" password: "{{ site_password }}" cluster_uuid: "{{ vsan_cluster.cluster_uuid }}" - with_items: groups['esxi'][1:] + with_items: "{{ groups['esxi'][1:] }}" ''' diff --git a/cloud/xenserver_facts.py b/cloud/xenserver_facts.py index 54a2bd61ec3..04c88d34312 100644 --- a/cloud/xenserver_facts.py +++ b/cloud/xenserver_facts.py @@ -42,7 +42,7 @@ - name: Print running VMs debug: msg="{{ item }}" - with_items: xs_vms.keys() + with_items: "{{ xs_vms.keys() }}" when: xs_vms[item]['power_state'] == "Running" TASK: [Print running VMs] *********************************************************** diff --git a/network/lldp.py b/network/lldp.py index fd1b1092d5e..f44afa1cbd5 100644 --- a/network/lldp.py +++ b/network/lldp.py @@ -37,7 +37,7 @@ - name: Print each switch/port debug: msg="{{ lldp[item]['chassis']['name'] }} / {{ lldp[item]['port']['ifalias'] }} - with_items: lldp.keys() + with_items: "{{ lldp.keys() }}" # TASK: [Print each switch/port] *********************************************************** # ok: [10.13.0.22] => (item=eth2) => {"item": "eth2", "msg": "switch1.example.com / Gi0/24"} diff --git a/notification/hall.py b/notification/hall.py index 05c1a981b73..eb55f5c9454 100755 --- a/notification/hall.py +++ b/notification/hall.py @@ -60,7 +60,7 @@ room_token: title: Server Creation msg: "Created EC2 instance {{ item.id }} of type {{ item.instance_type }}.\\nInstance can be reached at {{ item.public_ip }} in the {{ item.region }} region." - with_items: ec2.instances + with_items: "{{ ec2.instances }}" """ HALL_API_ENDPOINT = 'https://hall.com/api/1/services/generic/%s' diff --git a/system/crypttab.py b/system/crypttab.py index ecf207cf03c..d004f8fa4f6 100644 --- a/system/crypttab.py +++ b/system/crypttab.py @@ -78,7 +78,7 @@ - name: Add the 'discard' option to any existing options for all devices crypttab: name={{ item.device }} state=opts_present opts=discard - with_items: ansible_mounts + with_items: "{{ ansible_mounts }}" when: '/dev/mapper/luks-' in {{ item.device }} ''' diff --git a/web_infrastructure/jenkins_plugin.py b/web_infrastructure/jenkins_plugin.py index af5adb462ff..b08f7541a98 100644 --- a/web_infrastructure/jenkins_plugin.py +++ b/web_infrastructure/jenkins_plugin.py @@ -218,7 +218,7 @@ register: my_jenkins_plugin_unversioned when: > 'version' not in item.value - with_dict: my_jenkins_plugins + with_dict: "{{ my_jenkins_plugins }}" - name: Install plugins with a specific version jenkins_plugin: @@ -227,7 +227,7 @@ register: my_jenkins_plugin_versioned when: > 'version' in item.value - with_dict: my_jenkins_plugins + with_dict: "{{ my_jenkins_plugins }}" - name: Initiate the fact set_fact: @@ -237,13 +237,13 @@ set_fact: jenkins_restart_required: yes when: item.changed - with_items: my_jenkins_plugin_versioned.results + with_items: "{{ my_jenkins_plugin_versioned.results }}" - name: Check if restart is required by any of the unversioned plugins set_fact: jenkins_restart_required: yes when: item.changed - with_items: my_jenkins_plugin_unversioned.results + with_items: "{{ my_jenkins_plugin_unversioned.results }}" - name: Restart Jenkins if required service: @@ -276,7 +276,7 @@ state: "{{ 'pinned' if item.value['pinned'] else 'unpinned'}}" when: > 'pinned' in item.value - with_dict: my_jenkins_plugins + with_dict: "{{ my_jenkins_plugins }}" - name: Plugin enabling jenkins_plugin: @@ -284,7 +284,7 @@ state: "{{ 'enabled' if item.value['enabled'] else 'disabled'}}" when: > 'enabled' in item.value - with_dict: my_jenkins_plugins + with_dict: "{{ my_jenkins_plugins }}" ''' RETURN = ''' From 09f6af873247c5c7fe12379ec4216453755aee3a Mon Sep 17 00:00:00 2001 From: Henrique Rodrigues Date: Fri, 11 Nov 2016 14:17:47 +0000 Subject: [PATCH 2352/2522] New ec2_group_facts module to be able to get facts from EC2 security groups (#2591) Add `ec2_group_facts` module to gather facts from EC2 security groups --- cloud/amazon/ec2_group_facts.py | 163 ++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 cloud/amazon/ec2_group_facts.py diff --git a/cloud/amazon/ec2_group_facts.py b/cloud/amazon/ec2_group_facts.py new file mode 100644 index 00000000000..6e4039f8b55 --- /dev/null +++ b/cloud/amazon/ec2_group_facts.py @@ -0,0 +1,163 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ec2_group_facts +short_description: Gather facts about ec2 security groups in AWS. +description: + - Gather facts about ec2 security groups in AWS. +version_added: "2.3" +author: "Henrique Rodrigues (github.com/Sodki)" +options: + filters: + description: + - A dict of filters to apply. Each dict item consists of a filter key and a filter value. See \ + U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeSecurityGroups.html) for \ + possible filters. Filter names and values are case sensitive. You can also use underscores (_) \ + instead of dashes (-) in the filter keys, which will take precedence in case of conflict. + required: false + default: {} +notes: + - By default, the module will return all security groups. To limit results use the appropriate filters. + +extends_documentation_fragment: + - aws + - ec2 +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Gather facts about all security groups +- ec2_group_facts: + +# Gather facts about all security groups in a specific VPC +- ec2_group_facts: + filters: + vpc-id: vpc-12345678 + +# Gather facts about all security groups in a specific VPC +- ec2_group_facts: + filters: + vpc-id: vpc-12345678 + +# Gather facts about a security group +- ec2_group_facts: + filters: + group-name: example-1 + +# Gather facts about a security group by id +- ec2_group_facts: + filters: + group-id: sg-12345678 + +# Gather facts about a security group with multiple filters, also mixing the use of underscores as filter keys +- ec2_group_facts: + filters: + group_id: sg-12345678 + vpc-id: vpc-12345678 + +# Gather facts about various security groups +- ec2_group_facts: + filters: + group-name: + - example-1 + - example-2 + - example-3 + +# Gather facts about any security group with a tag key Name and value Example. The quotes around 'tag:name' are important because of the colon in the value +- ec2_group_facts: + filters: + "tag:Name": Example +''' + +RETURN = ''' +security_groups: + description: Security groups that match the provided filters. Each element consists of a dict with all the information related to that security group. + type: list + sample: +''' + + +try: + import boto3 + from botocore.exceptions import ClientError + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = Falsentry + +import traceback + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + filters=dict(default={}, type='dict') + ) + ) + + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO3: + module.fail_json(msg='boto3 required for this module') + + region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) + + if region: + connection = boto3_conn( + module, + conn_type='client', + resource='ec2', + region=region, + endpoint=ec2_url, + **aws_connect_params + ) + else: + module.fail_json(msg="region must be specified") + + # Replace filter key underscores with dashes, for compatibility, except if we're dealing with tags + sanitized_filters = module.params.get("filters") + for key in sanitized_filters: + if not key.startswith("tag:"): + sanitized_filters[key.replace("_", "-")] = sanitized_filters.pop(key) + + try: + security_groups = connection.describe_security_groups( + Filters=ansible_dict_to_boto3_filter_list(sanitized_filters) + ) + except ClientError as e: + module.fail_json(msg=e.message, exception=traceback.format_exc(e)) + + # Turn the boto3 result in to ansible_friendly_snaked_names + snaked_security_groups = [] + for security_group in security_groups['SecurityGroups']: + snaked_security_groups.append(camel_dict_to_snake_dict(security_group)) + + # Turn the boto3 result in to ansible friendly tag dictionary + for security_group in snaked_security_groups: + if 'tags' in security_group: + security_group['tags'] = boto3_tag_list_to_ansible_dict(security_group['tags']) + + module.exit_json(security_groups=snaked_security_groups) + + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() From 88e58df86b38d48ffd2f702c32ec8c2df37a824e Mon Sep 17 00:00:00 2001 From: Jason Ritchie Date: Fri, 11 Nov 2016 13:40:37 -0500 Subject: [PATCH 2353/2522] Detect and correct PowerShell mishandling nssm Unicode output as UTF8 (#2867) * extra detail on which step triggered 'change', detect and handle powershell mishandling nssm's unicode as utf8 * Simpler handling of nssm output encoding Thanks @nitzmahone for a cleaner way to control PowerShell's behavior --- windows/win_nssm.ps1 | 167 ++++++++++++++++++++++++++----------------- 1 file changed, 102 insertions(+), 65 deletions(-) diff --git a/windows/win_nssm.ps1 b/windows/win_nssm.ps1 index 0ef05d892ff..da3d01a7161 100644 --- a/windows/win_nssm.ps1 +++ b/windows/win_nssm.ps1 @@ -42,6 +42,33 @@ $dependencies = Get-Attr $params "dependencies" -default $null $user = Get-Attr $params "user" -default $null $password = Get-Attr $params "password" -default $null + +#abstract the calling of nssm because some PowerShell environments +#mishandle its stdout(which is Unicode) as UTF8 +Function Nssm-Invoke +{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string]$cmd + ) + Try { + $encodingWas = [System.Console]::OutputEncoding + [System.Console]::OutputEncoding = [System.Text.Encoding]::Unicode + + $nssmOutput = invoke-expression "nssm $cmd" + return $nssmOutput + } + Catch { + $ErrorMessage = $_.Exception.Message + Fail-Json $result "an exception occurred when invoking NSSM: $ErrorMessage" + } + Finally { + # Set the console encoding back to what it was + [System.Console]::OutputEncoding = $encodingWas + } +} + Function Service-Exists { [CmdletBinding()] @@ -63,11 +90,11 @@ Function Nssm-Remove if (Service-Exists -name $name) { - $cmd = "nssm stop ""$name""" - $results = invoke-expression $cmd + $cmd = "stop ""$name""" + $results = Nssm-Invoke $cmd - $cmd = "nssm remove ""$name"" confirm" - $results = invoke-expression $cmd + $cmd = "remove ""$name"" confirm" + $results = Nssm-Invoke $cmd if ($LastExitCode -ne 0) { @@ -76,6 +103,7 @@ Function Nssm-Remove Throw "Error removing service ""$name""" } + Set-Attr $result "changed_by" "remove_service" $result.changed = $true } } @@ -101,9 +129,7 @@ Function Nssm-Install if (!(Service-Exists -name $name)) { - $cmd = "nssm install ""$name"" $application" - - $results = invoke-expression $cmd + $results = Nssm-Invoke "install ""$name"" $application" if ($LastExitCode -ne 0) { @@ -112,11 +138,11 @@ Function Nssm-Install Throw "Error installing service ""$name""" } + Set-Attr $result "changed_by" "install_service" $result.changed = $true } else { - $cmd = "nssm get ""$name"" Application" - $results = invoke-expression $cmd + $results = Nssm-Invoke "get ""$name"" Application" if ($LastExitCode -ne 0) { @@ -125,11 +151,11 @@ Function Nssm-Install Throw "Error installing service ""$name""" } - if ($results -ne $application) + if ($results -cnotlike $application) { - $cmd = "nssm set ""$name"" Application $application" + $cmd = "set ""$name"" Application $application" - $results = invoke-expression $cmd + $results = Nssm-Invoke $cmd if ($LastExitCode -ne 0) { @@ -137,7 +163,9 @@ Function Nssm-Install Set-Attr $result "nssm_error_log" "$results" Throw "Error installing service ""$name""" } + Set-Attr $result "application" "$application" + Set-Attr $result "changed_by" "reinstall_service" $result.changed = $true } } @@ -184,8 +212,8 @@ Function Nssm-Update-AppParameters [string]$appParameters ) - $cmd = "nssm get ""$name"" AppParameters" - $results = invoke-expression $cmd + $cmd = "get ""$name"" AppParameters" + $results = Nssm-Invoke $cmd if ($LastExitCode -ne 0) { @@ -228,11 +256,11 @@ Function Nssm-Update-AppParameters { if ($appParameters) { - $cmd = "nssm set ""$name"" AppParameters $singleLineParams" + $cmd = "set ""$name"" AppParameters $singleLineParams" } else { - $cmd = "nssm set ""$name"" AppParameters '""""'" + $cmd = "set ""$name"" AppParameters '""""'" } - $results = invoke-expression $cmd + $results = Nssm-Invoke $cmd if ($LastExitCode -ne 0) { @@ -241,11 +269,12 @@ Function Nssm-Update-AppParameters Throw "Error updating AppParameters for service ""$name""" } + Set-Attr $result "changed_by" "update_app_parameters" $result.changed = $true } } -Function Nssm-Set-Ouput-Files +Function Nssm-Set-Output-Files { [CmdletBinding()] param( @@ -255,8 +284,8 @@ Function Nssm-Set-Ouput-Files [string]$stderr ) - $cmd = "nssm get ""$name"" AppStdout" - $results = invoke-expression $cmd + $cmd = "get ""$name"" AppStdout" + $results = Nssm-Invoke $cmd if ($LastExitCode -ne 0) { @@ -265,16 +294,16 @@ Function Nssm-Set-Ouput-Files Throw "Error retrieving existing stdout file for service ""$name""" } - if ($results -ne $stdout) + if ($results -cnotlike $stdout) { if (!$stdout) { - $cmd = "nssm reset ""$name"" AppStdout" + $cmd = "reset ""$name"" AppStdout" } else { - $cmd = "nssm set ""$name"" AppStdout $stdout" + $cmd = "set ""$name"" AppStdout $stdout" } - $results = invoke-expression $cmd + $results = Nssm-Invoke $cmd if ($LastExitCode -ne 0) { @@ -283,11 +312,12 @@ Function Nssm-Set-Ouput-Files Throw "Error setting stdout file for service ""$name""" } + Set-Attr $result "changed_by" "set_stdout" $result.changed = $true } - $cmd = "nssm get ""$name"" AppStderr" - $results = invoke-expression $cmd + $cmd = "get ""$name"" AppStderr" + $results = Nssm-Invoke $cmd if ($LastExitCode -ne 0) { @@ -296,12 +326,12 @@ Function Nssm-Set-Ouput-Files Throw "Error retrieving existing stderr file for service ""$name""" } - if ($results -ne $stderr) + if ($results -cnotlike $stderr) { if (!$stderr) { - $cmd = "nssm reset ""$name"" AppStderr" - $results = invoke-expression $cmd + $cmd = "reset ""$name"" AppStderr" + $results = Nssm-Invoke $cmd if ($LastExitCode -ne 0) { @@ -310,8 +340,8 @@ Function Nssm-Set-Ouput-Files Throw "Error clearing stderr file setting for service ""$name""" } } else { - $cmd = "nssm set ""$name"" AppStderr $stderr" - $results = invoke-expression $cmd + $cmd = "set ""$name"" AppStderr $stderr" + $results = Nssm-Invoke $cmd if ($LastExitCode -ne 0) { @@ -321,6 +351,7 @@ Function Nssm-Set-Ouput-Files } } + Set-Attr $result "changed_by" "set_stderr" $result.changed = $true } @@ -329,28 +360,28 @@ Function Nssm-Set-Ouput-Files ### #set files to overwrite - $cmd = "nssm set ""$name"" AppStdoutCreationDisposition 2" - $results = invoke-expression $cmd + $cmd = "set ""$name"" AppStdoutCreationDisposition 2" + $results = Nssm-Invoke $cmd - $cmd = "nssm set ""$name"" AppStderrCreationDisposition 2" - $results = invoke-expression $cmd + $cmd = "set ""$name"" AppStderrCreationDisposition 2" + $results = Nssm-Invoke $cmd #enable file rotation - $cmd = "nssm set ""$name"" AppRotateFiles 1" - $results = invoke-expression $cmd + $cmd = "set ""$name"" AppRotateFiles 1" + $results = Nssm-Invoke $cmd #don't rotate until the service restarts - $cmd = "nssm set ""$name"" AppRotateOnline 0" - $results = invoke-expression $cmd + $cmd = "set ""$name"" AppRotateOnline 0" + $results = Nssm-Invoke $cmd #both of the below conditions must be met before rotation will happen #minimum age before rotating - $cmd = "nssm set ""$name"" AppRotateSeconds 86400" - $results = invoke-expression $cmd + $cmd = "set ""$name"" AppRotateSeconds 86400" + $results = Nssm-Invoke $cmd #minimum size before rotating - $cmd = "nssm set ""$name"" AppRotateBytes 104858" - $results = invoke-expression $cmd + $cmd = "set ""$name"" AppRotateBytes 104858" + $results = Nssm-Invoke $cmd } Function Nssm-Update-Credentials @@ -365,8 +396,8 @@ Function Nssm-Update-Credentials [string]$password ) - $cmd = "nssm get ""$name"" ObjectName" - $results = invoke-expression $cmd + $cmd = "get ""$name"" ObjectName" + $results = Nssm-Invoke $cmd if ($LastExitCode -ne 0) { @@ -386,8 +417,8 @@ Function Nssm-Update-Credentials } If ($results -ne $fullUser) { - $cmd = "nssm set ""$name"" ObjectName $fullUser $password" - $results = invoke-expression $cmd + $cmd = "set ""$name"" ObjectName $fullUser $password" + $results = Nssm-Invoke $cmd if ($LastExitCode -ne 0) { @@ -396,6 +427,7 @@ Function Nssm-Update-Credentials Throw "Error updating credentials for service ""$name""" } + Set-Attr $result "changed_by" "update_credentials" $result.changed = $true } } @@ -412,8 +444,8 @@ Function Nssm-Update-Dependencies [string]$dependencies ) - $cmd = "nssm get ""$name"" DependOnService" - $results = invoke-expression $cmd + $cmd = "get ""$name"" DependOnService" + $results = Nssm-Invoke $cmd if ($LastExitCode -ne 0) { @@ -423,8 +455,8 @@ Function Nssm-Update-Dependencies } If (($dependencies) -and ($results.Tolower() -ne $dependencies.Tolower())) { - $cmd = "nssm set ""$name"" DependOnService $dependencies" - $results = invoke-expression $cmd + $cmd = "set ""$name"" DependOnService $dependencies" + $results = Nssm-Invoke $cmd if ($LastExitCode -ne 0) { @@ -433,6 +465,7 @@ Function Nssm-Update-Dependencies Throw "Error updating dependencies for service ""$name""" } + Set-Attr $result "changed_by" "update-dependencies" $result.changed = $true } } @@ -447,8 +480,8 @@ Function Nssm-Update-StartMode [string]$mode ) - $cmd = "nssm get ""$name"" Start" - $results = invoke-expression $cmd + $cmd = "get ""$name"" Start" + $results = Nssm-Invoke $cmd if ($LastExitCode -ne 0) { @@ -459,9 +492,9 @@ Function Nssm-Update-StartMode $modes=@{"auto" = "SERVICE_AUTO_START"; "manual" = "SERVICE_DEMAND_START"; "disabled" = "SERVICE_DISABLED"} $mappedMode = $modes.$mode - if ($mappedMode -ne $results) { - $cmd = "nssm set ""$name"" Start $mappedMode" - $results = invoke-expression $cmd + if ($results -cnotlike $mappedMode) { + $cmd = "set ""$name"" Start $mappedMode" + $results = Nssm-Invoke $cmd if ($LastExitCode -ne 0) { @@ -470,6 +503,7 @@ Function Nssm-Update-StartMode Throw "Error updating start mode for service ""$name""" } + Set-Attr $result "changed_by" "start_mode" $result.changed = $true } } @@ -482,8 +516,8 @@ Function Nssm-Get-Status [string]$name ) - $cmd = "nssm status ""$name""" - $results = invoke-expression $cmd + $cmd = "status ""$name""" + $results = Nssm-Invoke $cmd return ,$results } @@ -526,9 +560,9 @@ Function Nssm-Start-Service-Command [string]$name ) - $cmd = "nssm start ""$name""" + $cmd = "start ""$name""" - $results = invoke-expression $cmd + $results = Nssm-Invoke $cmd if ($LastExitCode -ne 0) { @@ -537,6 +571,7 @@ Function Nssm-Start-Service-Command Throw "Error starting service ""$name""" } + Set-Attr $result "changed_by" "start_service" $result.changed = $true } @@ -548,9 +583,9 @@ Function Nssm-Stop-Service-Command [string]$name ) - $cmd = "nssm stop ""$name""" + $cmd = "stop ""$name""" - $results = invoke-expression $cmd + $results = Nssm-Invoke $cmd if ($LastExitCode -ne 0) { @@ -559,6 +594,7 @@ Function Nssm-Stop-Service-Command Throw "Error stopping service ""$name""" } + Set-Attr $result "changed_by" "stop_service_command" $result.changed = $true } @@ -581,9 +617,9 @@ Function Nssm-Stop if ($currentStatus -ne "SERVICE_STOPPED") { - $cmd = "nssm stop ""$name""" + $cmd = "stop ""$name""" - $results = invoke-expression $cmd + $results = Nssm-Invoke $cmd if ($LastExitCode -ne 0) { @@ -592,6 +628,7 @@ Function Nssm-Stop Throw "Error stopping service ""$name""" } + Set-Attr $result "changed_by" "stop_service" $result.changed = $true } } @@ -612,7 +649,7 @@ Function NssmProcedure { Nssm-Install -name $name -application $application Nssm-Update-AppParameters -name $name -appParameters $appParameters - Nssm-Set-Ouput-Files -name $name -stdout $stdoutFile -stderr $stderrFile + Nssm-Set-Output-Files -name $name -stdout $stdoutFile -stderr $stderrFile Nssm-Update-Dependencies -name $name -dependencies $dependencies Nssm-Update-Credentials -name $name -user $user -password $password Nssm-Update-StartMode -name $name -mode $startMode From 20dfbdd932cf2f2eaa8c2b7d512a55cbfa43a121 Mon Sep 17 00:00:00 2001 From: Thomas Krahn Date: Fri, 11 Nov 2016 20:23:14 +0100 Subject: [PATCH 2354/2522] Ipa bugfixes (#3421) * ipa_group: Fix: 'list' object has no attribute 'get' * ipa_hbacrule: Fix: 'list' object has no attribute 'get' * ipa_host: Fix: 'list' object has no attribute 'get' * ipa_hostgroup: Fix: 'list' object has no attribute 'get' * ipa_role: Fix: 'list' object has no attribute 'get' * ipa_sudocmd: Fix: 'list' object has no attribute 'get' * ipa_sudocmdgroup: Fix: 'list' object has no attribute 'get' * ipa_sudorule: Fix: 'list' object has no attribute 'get' * ipa_user: Fix: 'list' object has no attribute 'get' * ipa_sudorule: Fix: invalid 'cn': Only one value is allowed * ipa_hostgroup: module returns changed if assigned hosts or hostgroups are not in lowercase --- identity/ipa/ipa_group.py | 2 ++ identity/ipa/ipa_hbacrule.py | 2 ++ identity/ipa/ipa_host.py | 2 ++ identity/ipa/ipa_hostgroup.py | 8 ++++++-- identity/ipa/ipa_role.py | 2 ++ identity/ipa/ipa_sudocmd.py | 2 ++ identity/ipa/ipa_sudocmdgroup.py | 2 ++ identity/ipa/ipa_sudorule.py | 5 ++++- identity/ipa/ipa_user.py | 2 ++ 9 files changed, 24 insertions(+), 3 deletions(-) diff --git a/identity/ipa/ipa_group.py b/identity/ipa/ipa_group.py index 39ce80639d1..0c45776a23f 100644 --- a/identity/ipa/ipa_group.py +++ b/identity/ipa/ipa_group.py @@ -210,6 +210,8 @@ def _post_json(self, method, name, item=None): if isinstance(result, list): if len(result) > 0: return result[0] + else: + return {} return result return None diff --git a/identity/ipa/ipa_hbacrule.py b/identity/ipa/ipa_hbacrule.py index 5657ffb8efe..54619c5ec33 100644 --- a/identity/ipa/ipa_hbacrule.py +++ b/identity/ipa/ipa_hbacrule.py @@ -241,6 +241,8 @@ def _post_json(self, method, name, item=None): if isinstance(result, list): if len(result) > 0: return result[0] + else: + return {} return result return None diff --git a/identity/ipa/ipa_host.py b/identity/ipa/ipa_host.py index 1ca4113b9d6..a7cc4d361e2 100644 --- a/identity/ipa/ipa_host.py +++ b/identity/ipa/ipa_host.py @@ -234,6 +234,8 @@ def _post_json(self, method, name, item=None): if isinstance(result, list): if len(result) > 0: return result[0] + else: + return {} return result return None diff --git a/identity/ipa/ipa_hostgroup.py b/identity/ipa/ipa_hostgroup.py index 50e66428805..f0350277313 100644 --- a/identity/ipa/ipa_hostgroup.py +++ b/identity/ipa/ipa_hostgroup.py @@ -187,6 +187,8 @@ def _post_json(self, method, name, item=None): if isinstance(result, list): if len(result) > 0: return result[0] + else: + return {} return result return None @@ -285,11 +287,13 @@ def ensure(module, client): client.hostgroup_mod(name=name, item=data) if host is not None: - changed = modify_if_diff(module, name, ipa_hostgroup.get('member_host', []), host, + changed = modify_if_diff(module, name, ipa_hostgroup.get('member_host', []), + [item.lower() for item in host], client.hostgroup_add_host, client.hostgroup_remove_host) or changed if hostgroup is not None: - changed = modify_if_diff(module, name, ipa_hostgroup.get('member_hostgroup', []), hostgroup, + changed = modify_if_diff(module, name, ipa_hostgroup.get('member_hostgroup', []), + [item.lower() for item in hostgroup], client.hostgroup_add_hostgroup, client.hostgroup_remove_hostgroup) or changed else: diff --git a/identity/ipa/ipa_role.py b/identity/ipa/ipa_role.py index 48508a256ae..9d3ac043f3a 100644 --- a/identity/ipa/ipa_role.py +++ b/identity/ipa/ipa_role.py @@ -217,6 +217,8 @@ def _post_json(self, method, name, item=None): if isinstance(result, list): if len(result) > 0: return result[0] + else: + return {} return result return None diff --git a/identity/ipa/ipa_sudocmd.py b/identity/ipa/ipa_sudocmd.py index 5b9dbec5fde..f759f2d726f 100644 --- a/identity/ipa/ipa_sudocmd.py +++ b/identity/ipa/ipa_sudocmd.py @@ -167,6 +167,8 @@ def _post_json(self, method, name, item=None): if isinstance(result, list): if len(result) > 0: return result[0] + else: + return {} return result return None diff --git a/identity/ipa/ipa_sudocmdgroup.py b/identity/ipa/ipa_sudocmdgroup.py index 182bf82806a..b29216f70cd 100644 --- a/identity/ipa/ipa_sudocmdgroup.py +++ b/identity/ipa/ipa_sudocmdgroup.py @@ -174,6 +174,8 @@ def _post_json(self, method, name, item=None): if isinstance(result, list): if len(result) > 0: return result[0] + else: + return {} return result return None diff --git a/identity/ipa/ipa_sudorule.py b/identity/ipa/ipa_sudorule.py index 162baad5d8c..426a4444ecc 100644 --- a/identity/ipa/ipa_sudorule.py +++ b/identity/ipa/ipa_sudorule.py @@ -226,6 +226,8 @@ def _post_json(self, method, name, item=None): if isinstance(result, list): if len(result) > 0: return result[0] + else: + return {} return result return None @@ -348,7 +350,8 @@ def modify_if_diff(module, name, ipa_list, module_list, add_method, remove_metho def category_changed(module, client, category_name, ipa_sudorule): if ipa_sudorule.get(category_name, None) == ['all']: if not module.check_mode: - client.sudorule_mod(name=ipa_sudorule.get('cn'), item={category_name: None}) + # cn is returned as list even with only a single value. + client.sudorule_mod(name=ipa_sudorule.get('cn')[0], item={category_name: None}) return True return False diff --git a/identity/ipa/ipa_user.py b/identity/ipa/ipa_user.py index 8a5c4b09168..464348ee9bc 100644 --- a/identity/ipa/ipa_user.py +++ b/identity/ipa/ipa_user.py @@ -212,6 +212,8 @@ def _post_json(self, method, name, item=None): if isinstance(result, list): if len(result) > 0: return result[0] + else: + return {} return result return None From 96a7c3782bf99cfa79aa1a2f8bd7bc467b9163a1 Mon Sep 17 00:00:00 2001 From: Jose Armesto Date: Fri, 11 Nov 2016 21:57:55 +0100 Subject: [PATCH 2355/2522] ec2_lc_find: Set default value for AssociatePublicIpAddress, since is missing when is false (#3417) --- cloud/amazon/ec2_lc_find.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_lc_find.py b/cloud/amazon/ec2_lc_find.py index f18bdfb42be..e5463e4ce80 100644 --- a/cloud/amazon/ec2_lc_find.py +++ b/cloud/amazon/ec2_lc_find.py @@ -184,7 +184,7 @@ def find_launch_configs(client, module): 'security_groups': lc['SecurityGroups'], 'kernel_id': lc['KernelId'], 'ram_disk_id': lc['RamdiskId'], - 'associate_public_address': lc['AssociatePublicIpAddress'], + 'associate_public_address': lc.get('AssociatePublicIpAddress', False), } results.append(data) From 2fb062604f9b809ab6d12888ed84cfc39267a112 Mon Sep 17 00:00:00 2001 From: mattwwarren Date: Fri, 11 Nov 2016 16:02:07 -0500 Subject: [PATCH 2356/2522] ec2_elb_facts: fix errors with no names input (#3381) * None being passed around results in a Bad Time (tm) * need to return the full set of elbs for an empty list * logic is hard --- cloud/amazon/ec2_elb_facts.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/cloud/amazon/ec2_elb_facts.py b/cloud/amazon/ec2_elb_facts.py index 52da210f9b4..122e0f70a2c 100644 --- a/cloud/amazon/ec2_elb_facts.py +++ b/cloud/amazon/ec2_elb_facts.py @@ -205,16 +205,19 @@ def list_elbs(self): self.module.fail_json(msg = "%s: %s" % (err.error_code, err.error_message)) if all_elbs: - for existing_lb in all_elbs: - if existing_lb.name in self.names: - elb_array.append(self._get_elb_info(existing_lb)) - - return elb_array + if self.names: + for existing_lb in all_elbs: + if existing_lb.name in self.names: + elb_array.append(existing_lb) + else: + elb_array = all_elbs + + return list(map(self._get_elb_info, elb_array)) def main(): argument_spec = ec2_argument_spec() argument_spec.update(dict( - names={'default': None, 'type': 'list'} + names={'default': [], 'type': 'list'} ) ) module = AnsibleModule(argument_spec=argument_spec) From c5c6bb5e518a8d30b9f4d6132f156012c5786494 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Fri, 11 Nov 2016 22:26:09 +0000 Subject: [PATCH 2357/2522] Use native YAML (#3433) --- cloud/amazon/lambda_event.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/lambda_event.py b/cloud/amazon/lambda_event.py index 0d642734f05..306762a676d 100644 --- a/cloud/amazon/lambda_event.py +++ b/cloud/amazon/lambda_event.py @@ -102,8 +102,9 @@ batch_size: 100 starting_position: TRIM_HORIZON - - name: show source event - debug: var=lambda_stream_events + - name: Show source event + debug: + var: lambda_stream_events ''' RETURN = ''' From ba0460cf1f92642358c8a0b298997025e52aa519 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Fri, 11 Nov 2016 22:27:33 +0000 Subject: [PATCH 2358/2522] Use native YAML (#3434) --- cloud/amazon/cloudtrail.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/cloud/amazon/cloudtrail.py b/cloud/amazon/cloudtrail.py index 7e76f4848bc..8a8fad33415 100644 --- a/cloud/amazon/cloudtrail.py +++ b/cloud/amazon/cloudtrail.py @@ -80,16 +80,25 @@ EXAMPLES = """ - name: enable cloudtrail local_action: cloudtrail - state=enabled name=main s3_bucket_name=ourbucket - s3_key_prefix=cloudtrail region=us-east-1 + state: enabled + name: main + s3_bucket_name: ourbucket + s3_key_prefix: cloudtrail + region: us-east-1 - name: enable cloudtrail with different configuration local_action: cloudtrail - state=enabled name=main s3_bucket_name=ourbucket2 - s3_key_prefix='' region=us-east-1 + state: enabled + name: main + s3_bucket_name: ourbucket2 + s3_key_prefix: '' + region: us-east-1 - name: remove cloudtrail - local_action: cloudtrail state=disabled name=main region=us-east-1 + local_action: cloudtrail + state: disabled + name: main + region: us-east-1 """ HAS_BOTO = False From fd9a4bce7bf2f696133504fc69f754a96da48324 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Fri, 11 Nov 2016 22:29:39 +0000 Subject: [PATCH 2359/2522] Normalize yaml example (#3425) --- system/at.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/system/at.py b/system/at.py index 0ce9ff2c7d4..52006e54710 100644 --- a/system/at.py +++ b/system/at.py @@ -64,13 +64,22 @@ EXAMPLES = ''' # Schedule a command to execute in 20 minutes as root. -- at: command="ls -d / > /dev/null" count=20 units="minutes" +- at: + command: "ls -d / > /dev/null" + count: 20 + units: minutes # Match a command to an existing job and delete the job. -- at: command="ls -d / > /dev/null" state="absent" +- at: + command: "ls -d / > /dev/null" + state: absent # Schedule a command to execute in 20 minutes making sure it is unique in the queue. -- at: command="ls -d / > /dev/null" unique=true count=20 units="minutes" +- at: + command: "ls -d / > /dev/null" + unique: true + count: 20 + units: minutes ''' import os From cfeef6d6c8f3457aba2e6de7af176686cdb7875b Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Fri, 11 Nov 2016 22:30:34 +0000 Subject: [PATCH 2360/2522] Normalize yaml example (#3426) --- system/capabilities.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/system/capabilities.py b/system/capabilities.py index aa0785f6f62..009507669fb 100644 --- a/system/capabilities.py +++ b/system/capabilities.py @@ -55,10 +55,16 @@ EXAMPLES = ''' # Set cap_sys_chroot+ep on /foo -- capabilities: path=/foo capability=cap_sys_chroot+ep state=present +- capabilities: + path: /foo + capability: cap_sys_chroot+ep + state: present # Remove cap_net_bind_service from /bar -- capabilities: path=/bar capability=cap_net_bind_service state=absent +- capabilities: + path: /bar + capability: cap_net_bind_service + state: absent ''' From 911aaba4380fd05d9b659fd773ee77b981309308 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Fri, 11 Nov 2016 22:31:52 +0000 Subject: [PATCH 2361/2522] Use native YAML (#3439) --- system/cronvar.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/system/cronvar.py b/system/cronvar.py index 21f92be964e..4c198d65d81 100644 --- a/system/cronvar.py +++ b/system/cronvar.py @@ -89,15 +89,22 @@ EXAMPLES = ''' # Ensure a variable exists. # Creates an entry like "EMAIL=doug@ansibmod.con.com" -- cronvar: name="EMAIL" value="doug@ansibmod.con.com" +- cronvar: + name: EMAIL + value: doug@ansibmod.con.com # Make sure a variable is gone. This will remove any variable named # "LEGACY" -- cronvar: name="LEGACY" state=absent +- cronvar: + name: LEGACY + state: absent # Adds a variable to a file under /etc/cron.d -- cronvar: name="LOGFILE" value="/var/log/yum-autoupdate.log" - user="root" cron_file=ansible_yum-autoupdate +- cronvar: + name: LOGFILE + value: /var/log/yum-autoupdate.log + user: root + cron_file: ansible_yum-autoupdate ''' import os From 7e42dfd50529c6cb7471919a7b59470853ca05f9 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Fri, 11 Nov 2016 22:32:23 +0000 Subject: [PATCH 2362/2522] Use native YAML (#3438) --- cloud/atomic/atomic_host.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cloud/atomic/atomic_host.py b/cloud/atomic/atomic_host.py index dc098e6721b..a802b93f916 100644 --- a/cloud/atomic/atomic_host.py +++ b/cloud/atomic/atomic_host.py @@ -42,11 +42,12 @@ EXAMPLES = ''' # Upgrade the atomic host platform to the latest version (atomic host upgrade) -- atomic_host: revision=latest +- atomic_host: + revision: latest # Deploy a specific revision as the atomic host (atomic host deploy 23.130) -- atomic_host: revision=23.130 - +- atomic_host: + revision: 23.130 ''' RETURN = ''' From 38ab1fe369c165fe9079c6f097ca3a8e2137f792 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Fri, 11 Nov 2016 22:32:53 +0000 Subject: [PATCH 2363/2522] Use native YAML (#3437) --- cloud/atomic/atomic_image.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cloud/atomic/atomic_image.py b/cloud/atomic/atomic_image.py index cebd97a7d48..40517140db6 100644 --- a/cloud/atomic/atomic_image.py +++ b/cloud/atomic/atomic_image.py @@ -54,7 +54,9 @@ EXAMPLES = ''' # Execute the run command on rsyslog container image (atomic run rhel7/rsyslog) -- atomic_image: name=rhel7/rsyslog state=latest +- atomic_image: + name: rhel7/rsyslog + state: latest ''' From b89cfe42ef5cf2c70b0f92c5cc7b9c310979a8b1 Mon Sep 17 00:00:00 2001 From: Matthieu Dolci Date: Fri, 11 Nov 2016 14:33:18 -0800 Subject: [PATCH 2364/2522] ec2_customer_gateway: state present expect bgp_arn instead of bgp_asn (#3366) --- cloud/amazon/ec2_customer_gateway.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_customer_gateway.py b/cloud/amazon/ec2_customer_gateway.py index 64a77bb08bf..0138e8399e6 100644 --- a/cloud/amazon/ec2_customer_gateway.py +++ b/cloud/amazon/ec2_customer_gateway.py @@ -204,7 +204,7 @@ def main(): module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True, required_if=[ - ('state', 'present', ['bgp_arn']) + ('state', 'present', ['bgp_asn']) ] ) From be272567b53f67b1b950d6c59dfd6d06bb525ae3 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Fri, 11 Nov 2016 22:33:32 +0000 Subject: [PATCH 2365/2522] Use native YAML in the examples (#3441) --- crypto/openssl_privatekey.py | 15 +++++++++++---- crypto/openssl_publickey.py | 19 +++++++++++-------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/crypto/openssl_privatekey.py b/crypto/openssl_privatekey.py index f7da9d11053..e9be5e35635 100644 --- a/crypto/openssl_privatekey.py +++ b/crypto/openssl_privatekey.py @@ -73,16 +73,23 @@ EXAMPLES = ''' # Generate an OpenSSL private key with the default values (4096 bits, RSA) # and no public key -- openssl_privatekey: path=/etc/ssl/private/ansible.com.pem +- openssl_privatekey: + path: /etc/ssl/private/ansible.com.pem # Generate an OpenSSL private key with a different size (2048 bits) -- openssl_privatekey: path=/etc/ssl/private/ansible.com.pem size=2048 +- openssl_privatekey: + path: /etc/ssl/private/ansible.com.pem + size: 2048 # Force regenerate an OpenSSL private key if it already exists -- openssl_privatekey: path=/etc/ssl/private/ansible.com.pem force=True +- openssl_privatekey: + path: /etc/ssl/private/ansible.com.pem + force: True # Generate an OpenSSL private key with a different algorithm (DSA) -- openssl_privatekey: path=/etc/ssl/private/ansible.com.pem type=DSA +- openssl_privatekey: + path: /etc/ssl/private/ansible.com.pem + type: DSA ''' RETURN = ''' diff --git a/crypto/openssl_publickey.py b/crypto/openssl_publickey.py index 3fb2de85ae7..c01364c8ce8 100644 --- a/crypto/openssl_publickey.py +++ b/crypto/openssl_publickey.py @@ -65,18 +65,21 @@ EXAMPLES = ''' # Generate an OpenSSL public key. -- openssl_publickey: path=/etc/ssl/public/ansible.com.pem - privatekey_path=/etc/ssl/private/ansible.com.pem +- openssl_publickey: + path: /etc/ssl/public/ansible.com.pem + privatekey_path: /etc/ssl/private/ansible.com.pem # Force regenerate an OpenSSL public key if it already exists -- openssl_publickey: path=/etc/ssl/public/ansible.com.pem - privatekey_path=/etc/ssl/private/ansible.com.pem - force=True +- openssl_publickey: + path: /etc/ssl/public/ansible.com.pem + privatekey_path: /etc/ssl/private/ansible.com.pem + force: True # Remove an OpenSSL public key -- openssl_publickey: path=/etc/ssl/public/ansible.com.pem - privatekey_path=/etc/ssl/private/ansible.com.pem - state=absent +- openssl_publickey: + path: /etc/ssl/public/ansible.com.pem + privatekey_path: /etc/ssl/private/ansible.com.pem + state: absent ''' RETURN = ''' From 4dc9005c2b12a728a556d3453735b1456fff8aca Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Fri, 11 Nov 2016 22:36:55 +0000 Subject: [PATCH 2366/2522] Use native YAML (#3436) --- cloud/amazon/cloudwatchevent_rule.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cloud/amazon/cloudwatchevent_rule.py b/cloud/amazon/cloudwatchevent_rule.py index 8fda1c125ab..45b69cabf1e 100644 --- a/cloud/amazon/cloudwatchevent_rule.py +++ b/cloud/amazon/cloudwatchevent_rule.py @@ -96,7 +96,9 @@ arn: arn:aws:lambda:us-east-1:123456789012:function:MyFunction input: '{"foo": "bar"}' -- cloudwatchevent_rule: name=MyCronTask state=absent +- cloudwatchevent_rule: + name: MyCronTask + state: absent ''' RETURN = ''' From 5d46150e9a07f3bb6ce59b6f97e892423424da02 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Fri, 11 Nov 2016 22:40:07 +0000 Subject: [PATCH 2367/2522] Improve yaml code and add an if to make the import more standard (#3424) --- system/alternatives.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/system/alternatives.py b/system/alternatives.py index 09c8d8ad3e6..f4ae56ebe93 100644 --- a/system/alternatives.py +++ b/system/alternatives.py @@ -58,13 +58,21 @@ EXAMPLES = ''' - name: correct java version selected - alternatives: name=java path=/usr/lib/jvm/java-7-openjdk-amd64/jre/bin/java + alternatives: + name: java + path: /usr/lib/jvm/java-7-openjdk-amd64/jre/bin/java - name: alternatives link created - alternatives: name=hadoop-conf link=/etc/hadoop/conf path=/etc/hadoop/conf.ansible + alternatives: + name: hadoop-conf + link: /etc/hadoop/conf + path: /etc/hadoop/conf.ansible - name: make java 32 bit an alternative with low priority - alternatives: name=java path=/usr/lib/jvm/java-7-openjdk-i386/jre/bin/java priority=-10 + alternatives: + name: java + path: /usr/lib/jvm/java-7-openjdk-i386/jre/bin/java + priority: -10 ''' import re @@ -154,5 +162,5 @@ def main(): else: module.exit_json(changed=False) - -main() +if __name__ == '__main__': + main() From cb87ac2a6b3f1383fc9be9a812aacfa924339dbc Mon Sep 17 00:00:00 2001 From: James Hart Date: Fri, 11 Nov 2016 22:44:02 +0000 Subject: [PATCH 2368/2522] consul: Pass through service_id if specified on a check (#3295) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #3249 The python-consul library already supports this, so it is just a simple case of enablement. This does not break the current logic in `add` of parsing as a check, then parsing as a service if that fails… because service_name is mandatory on a service registration and is invalid on a check registration. --- clustering/consul.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/clustering/consul.py b/clustering/consul.py index f32e7a6a3f8..7f2b7d7e5fa 100644 --- a/clustering/consul.py +++ b/clustering/consul.py @@ -185,7 +185,7 @@ service_name: nginx service_port: 80 interval: 60s - http: /status + http: "http://localhost:80/status" - name: register external service nginx available at 10.1.5.23 consul: @@ -213,6 +213,14 @@ script: "/opt/disk_usage.py" interval: 5m + - name: register an http check against a service that's already registered + consul: + check_name: nginx-check2 + check_id: nginx-check2 + service_id: nginx + interval: 60s + http: "http://localhost:80/morestatus" + ''' try: @@ -265,7 +273,7 @@ def add_check(module, check): retrieve the full metadata of an existing check through the consul api. Without this we can't compare to the supplied check and so we must assume a change. ''' - if not check.name: + if not check.name and not service_id: module.fail_json(msg='a check name is required for a node level check, one not attached to a service') consul_api = get_consul_api(module) @@ -278,7 +286,8 @@ def add_check(module, check): interval=check.interval, ttl=check.ttl, http=check.http, - timeout=check.timeout) + timeout=check.timeout, + service_id=check.service_id) def remove_check(module, check_id): @@ -363,7 +372,8 @@ def parse_check(module): module.params.get('ttl'), module.params.get('notes'), module.params.get('http'), - module.params.get('timeout') + module.params.get('timeout'), + module.params.get('service_id'), ) @@ -451,10 +461,11 @@ def to_dict(self): class ConsulCheck(): def __init__(self, check_id, name, node=None, host='localhost', - script=None, interval=None, ttl=None, notes=None, http=None, timeout=None): + script=None, interval=None, ttl=None, notes=None, http=None, timeout=None, service_id=None): self.check_id = self.name = name if check_id: self.check_id = check_id + self.service_id = service_id self.notes = notes self.node = node self.host = host @@ -488,13 +499,14 @@ def validate_duration(self, name, duration): return duration def register(self, consul_api): - consul_api.agent.check.register(self.name, check_id=self.check_id, + consul_api.agent.check.register(self.name, check_id=self.check_id, service_id=self.service_id, notes=self.notes, check=self.check) def __eq__(self, other): return (isinstance(other, self.__class__) and self.check_id == other.check_id + and self.service_id == other.service_id and self.name == other.name and self.script == script and self.interval == interval) @@ -514,6 +526,7 @@ def to_dict(self): self._add(data, 'ttl') self._add(data, 'http') self._add(data, 'timeout') + self._add(data, 'service_id') return data def _add(self, data, key, attr=None): From 81009c5f6f603b984461a14c724ed9cccf2de68a Mon Sep 17 00:00:00 2001 From: Tristan Bessoussa Date: Sun, 13 Nov 2016 01:23:11 +0100 Subject: [PATCH 2369/2522] Fixed wrong variable name in the let's encrypt doc (#3398) It could lead to unwanted error when dummy-paste to try this module. --- web_infrastructure/letsencrypt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web_infrastructure/letsencrypt.py b/web_infrastructure/letsencrypt.py index 74f6a64d6d7..939c51c629f 100644 --- a/web_infrastructure/letsencrypt.py +++ b/web_infrastructure/letsencrypt.py @@ -122,8 +122,8 @@ # for example: # # - copy: -# dest: /var/www/html/{{ sample_com_http_challenge['challenge_data']['sample.com']['http-01']['resource'] }} -# content: "{{ sample_com_http_challenge['challenge_data']['sample.com']['http-01']['resource_value'] }}" +# dest: /var/www/html/{{ sample_com_challenge['challenge_data']['sample.com']['http-01']['resource'] }} +# content: "{{ sample_com_challenge['challenge_data']['sample.com']['http-01']['resource_value'] }}" # when: sample_com_challenge|changed - letsencrypt: From 63dd0a023084ce16959b3f5d851f8f66dfae23ae Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Sun, 13 Nov 2016 09:13:08 +0000 Subject: [PATCH 2370/2522] Use native YAML (#3435) --- cloud/amazon/route53_zone.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/cloud/amazon/route53_zone.py b/cloud/amazon/route53_zone.py index d08ed88aa15..2fc532955a3 100644 --- a/cloud/amazon/route53_zone.py +++ b/cloud/amazon/route53_zone.py @@ -54,25 +54,34 @@ EXAMPLES = ''' # create a public zone -- route53_zone: zone=example.com state=present comment="this is an example" +- route53_zone: + zone: example.com + state: present + comment: this is an example # delete a public zone -- route53_zone: zone=example.com state=absent +- route53_zone: + zone: example.com + state: absent - name: private zone for devel - route53_zone: zone=devel.example.com state=present vpc_id={{myvpc_id}} comment='developer domain' + route53_zone: + zone: devel.example.com + state: present + vpc_id: '{{ myvpc_id }}' + comment: developer domain # more complex example - name: register output after creating zone in parameterized region route53_zone: - vpc_id: "{{ vpc.vpc_id }}" - vpc_region: "{{ ec2_region }}" - zone: "{{ vpc_dns_zone }}" + vpc_id: '{{ vpc.vpc_id }}' + vpc_region: '{{ ec2_region }}' + zone: '{{ vpc_dns_zone }}' state: present register: zone_out -- debug: var=zone_out - +- debug: + var: zone_out ''' RETURN=''' From 1deae5514a5018562922e2e0102c021f5ff7efd5 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Fri, 11 Nov 2016 08:28:05 +0100 Subject: [PATCH 2371/2522] cloudstack: new module cs_vpc --- cloud/cloudstack/cs_vpc.py | 387 +++++++++++++++++++++++++++++++++++++ 1 file changed, 387 insertions(+) create mode 100644 cloud/cloudstack/cs_vpc.py diff --git a/cloud/cloudstack/cs_vpc.py b/cloud/cloudstack/cs_vpc.py new file mode 100644 index 00000000000..8171cb76090 --- /dev/null +++ b/cloud/cloudstack/cs_vpc.py @@ -0,0 +1,387 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2016, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it an/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: cs_vpc +short_description: "Manages VPCs on Apache CloudStack based clouds." +description: + - "Create, update and delete VPCs." +version_added: "2.3" +author: "René Moser (@resmo)" +options: + name: + description: + - "Name of the VPC." + required: true + display_text: + description: + - "Display text of the VPC." + - "If not set, C(name) will be used for creating." + required: false + default: null + cidr: + description: + - "CIDR of the VPC, e.g. 10.1.0.0/16" + - "All VPC guest networks' CIDRs must be within this CIDR." + - "Required on C(state=present)." + required: false + default: null + network_domain: + description: + - "Network domain for the VPC." + - "All networks inside the VPC will belong to this domain." + required: false + default: null + vpc_offering: + description: + - "Name of the VPC offering." + - "If not set, default VPC offering is used." + required: false + default: null + state: + description: + - "State of the VPC." + required: false + default: present + choices: + - present + - absent + - restarted + domain: + description: + - "Domain the VPC is related to." + required: false + default: null + account: + description: + - "Account the VPC is related to." + required: false + default: null + project: + description: + - "Name of the project the VPC is related to." + required: false + default: null + zone: + description: + - "Name of the zone." + - "If not set, default zone is used." + required: false + default: null + tags: + description: + - "List of tags. Tags are a list of dictionaries having keys C(key) and C(value)." + - "For deleting all tags, set an empty list e.g. C(tags: [])." + required: false + default: null + aliases: + - tag + poll_async: + description: + - "Poll async jobs until job has finished." + required: false + default: true +extends_documentation_fragment: cloudstack +''' + +EXAMPLES = ''' +# Ensure a VPC is present +- local_action: + module: cs_vpc + name: my_vpc + display_text: My example VPC + cidr: 10.10.0.0/16 + +# Ensure a VPC is absent +- local_action: + module: cs_vpc + name: my_vpc + state: absent + +# Ensure a VPC is restarted +- local_action: + module: cs_vpc + name: my_vpc + state: restarted +''' + +RETURN = ''' +--- +id: + description: "UUID of the VPC." + returned: success + type: string + sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6 +name: + description: "Name of the VPC." + returned: success + type: string + sample: my_vpc +display_text: + description: "Display text of the VPC." + returned: success + type: string + sample: My example VPC +cidr: + description: "CIDR of the VPC." + returned: success + type: string + sample: 10.10.0.0/16 +network_domain: + description: "Network domain of the VPC." + returned: success + type: string + sample: example.com +region_level_vpc: + description: "Whether the VPC is region level or not." + returned: success + type: boolean + sample: true +restart_required: + description: "Wheter the VPC router needs a restart or not." + returned: success + type: boolean + sample: true +distributed_vpc_router: + description: "Whether the VPC uses distributed router or not." + returned: success + type: boolean + sample: true +redundant_vpc_router: + description: "Whether the VPC has redundant routers or not." + returned: success + type: boolean + sample: true +domain: + description: "Domain the VPC is related to." + returned: success + type: string + sample: example domain +account: + description: "Account the VPC is related to." + returned: success + type: string + sample: example account +project: + description: "Name of project the VPC is related to." + returned: success + type: string + sample: Production +zone: + description: "Name of zone the VPC is in." + returned: success + type: string + sample: ch-gva-2 +state: + description: "State of the VPC." + returned: success + type: string + sample: Enabled +tags: + description: "List of resource tags associated with the VPC." + returned: success + type: dict + sample: '[ { "key": "foo", "value": "bar" } ]' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.cloudstack import * + + +class AnsibleCloudStackVpc(AnsibleCloudStack): + + def __init__(self, module): + super(AnsibleCloudStackVpc, self).__init__(module) + self.returns = { + 'cidr': 'cidr', + 'networkdomain': 'network_domain', + 'redundantvpcrouter': 'redundant_vpc_router', + 'distributedvpcrouter': 'distributed_vpc_router', + 'regionlevelvpc': 'region_level_vpc', + 'restartrequired': 'restart_required', + } + self.vpc = None + self.vpc_offering = None + + def get_vpc_offering(self, key=None): + if self.vpc_offering: + return self._get_by_key(key, self.vpc_offering) + + vpc_offering = self.module.params.get('vpc_offering') + args = {} + if vpc_offering: + args['name'] = vpc_offering + else: + args['isdefault'] = True + + vpc_offerings = self.cs.listVPCOfferings(**args) + if vpc_offerings: + self.vpc_offering = vpc_offerings['vpcoffering'][0] + return self._get_by_key(key, self.vpc_offering) + self.module.fail_json(msg="VPC offering '%s' not found" % vpc_offering) + + def get_vpc(self): + if self.vpc: + return self.vpc + args = { + 'account': self.get_account(key='name'), + 'domainid': self.get_domain(key='id'), + 'projectid': self.get_project(key='id'), + 'zoneid': self.get_zone(key='id'), + } + vpcs = self.cs.listVPCs() + if vpcs: + vpc_name = self.module.params.get('name') + for v in vpcs['vpc']: + if vpc_name.lower() in [ v['name'].lower(), v['id']]: + self.vpc = v + break + return self.vpc + + def restart_vpc(self): + self.result['changed'] = True + vpc = self.get_vpc() + if vpc and not self.module.check_mode: + args = { + 'id': vpc['id'], + } + res = self.cs.restartVPC(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + + poll_async = self.module.params.get('poll_async') + if poll_async: + self.poll_job(res, 'vpc') + return vpc + + def present_vpc(self): + vpc = self.get_vpc() + if not vpc: + vpc = self._create_vpc(vpc) + else: + vpc = self._update_vpc(vpc) + + if vpc: + vpc = self.ensure_tags(resource=vpc, resource_type='Vpc') + return vpc + + def _create_vpc(self, vpc): + self.result['changed'] = True + args = { + 'name': self.module.params.get('name'), + 'displaytext': self.get_or_fallback('display_text', 'name'), + 'vpcofferingid': self.get_vpc_offering(key='id'), + 'cidr': self.module.params.get('cidr'), + 'account': self.get_account(key='name'), + 'domainid': self.get_domain(key='id'), + 'projectid': self.get_project(key='id'), + 'zoneid': self.get_zone(key='id'), + } + self.result['diff']['after'] = args + if not self.module.check_mode: + res = self.cs.createVPC(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + + poll_async = self.module.params.get('poll_async') + if poll_async: + vpc = self.poll_job(res, 'vpc') + return vpc + + def _update_vpc(self, vpc): + args = { + 'id': vpc['id'], + 'displaytext': self.module.params.get('display_text'), + } + if self.has_changed(args, vpc): + self.result['changed'] = True + if not self.module.check_mode: + res = self.cs.updateVPC(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + + poll_async = self.module.params.get('poll_async') + if poll_async: + vpc = self.poll_job(res, 'vpc') + return vpc + + def absent_vpc(self): + vpc = self.get_vpc() + if vpc: + self.result['changed'] = True + self.result['diff']['before'] = vpc + if not self.module.check_mode: + res = self.cs.deleteVPC(id=vpc['id']) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + + poll_async = self.module.params.get('poll_async') + if poll_async: + self.poll_job(res, 'vpc') + return vpc + + +def main(): + argument_spec=cs_argument_spec() + argument_spec.update(dict( + name=dict(required=True), + cidr=dict(default=None), + display_text=dict(default=None), + vpc_offering=dict(default=None), + network_domain=dict(default=None), + state=dict(choices=['present', 'absent', 'restarted'], default='present'), + domain=dict(default=None), + account=dict(default=None), + project=dict(default=None), + zone=dict(default=None), + tags=dict(type='list', aliases=['tag'], default=None), + poll_async=dict(type='bool', default=True), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=cs_required_together(), + required_if=[ + ('state', 'present', ['cidr']), + ], + supports_check_mode=True, + ) + + try: + acs_vpc = AnsibleCloudStackVpc(module) + + state = module.params.get('state') + if state == 'absent': + vpc = acs_vpc.absent_vpc() + elif state == 'restarted': + vpc = acs_vpc.restart_vpc() + else: + vpc = acs_vpc.present_vpc() + + result = acs_vpc.get_result(vpc) + + except CloudStackException as e: + module.fail_json(msg='CloudStackException: %s' % str(e)) + + module.exit_json(**result) + +if __name__ == '__main__': + main() From 896c46d6174669fa3a14d865d02b81ff29e8ec5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Mon, 14 Nov 2016 07:19:45 +0100 Subject: [PATCH 2372/2522] cloudstack: add new module cs_nic (#3392) --- cloud/cloudstack/cs_nic.py | 293 +++++++++++++++++++++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 cloud/cloudstack/cs_nic.py diff --git a/cloud/cloudstack/cs_nic.py b/cloud/cloudstack/cs_nic.py new file mode 100644 index 00000000000..4017604e38a --- /dev/null +++ b/cloud/cloudstack/cs_nic.py @@ -0,0 +1,293 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2016, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: cs_nic +short_description: Manages NICs and secondary IPs of an instance on Apache CloudStack based clouds. +description: + - Add and remove secondary IPs to and from a NIC. +version_added: "2.3" +author: "René Moser (@resmo)" +options: + vm: + description: + - Name of instance. + required: true + aliases: ['name'] + network: + description: + - Name of the network. + - Required to find the NIC if instance has multiple networks assigned. + required: false + default: null + vm_guest_ip: + description: + - Secondary IP address to be added to the instance nic. + - If not set, the API always returns a new IP address and idempotency is not given. + required: false + default: null + aliases: ['secondary_ip'] + vpc: + description: + - Name of the VPC the C(vm) is related to. + required: false + default: null + domain: + description: + - Domain the instance is related to. + required: false + default: null + account: + description: + - Account the instance is related to. + required: false + default: null + project: + description: + - Name of the project the instance is deployed in. + required: false + default: null + zone: + description: + - Name of the zone in which the instance is deployed in. + - If not set, default zone is used. + required: false + default: null + state: + description: + - State of the ipaddress. + required: false + default: "present" + choices: [ 'present', 'absent' ] + poll_async: + description: + - Poll async jobs until job has finished. + required: false + default: true +extends_documentation_fragment: cloudstack +''' + +EXAMPLES = ''' +# Assign a specific IP to the default NIC of the VM +- local_action: + module: cs_nic + vm: customer_xy + vm_guest_ip: 10.10.10.10 + +# Assign an IP to the default NIC of the VM +# Note: If vm_guest_ip is not set, you will get a new IP address on every run. +- local_action: + module: cs_nic + vm: customer_xy + +# Remove a specific IP from the default NIC +- local_action: + module: cs_nic + vm: customer_xy + vm_guest_ip: 10.10.10.10 + state: absent +''' + +RETURN = ''' +--- +id: + description: UUID of the nic. + returned: success + type: string + sample: 87b1e0ce-4e01-11e4-bb66-0050569e64b8 +vm: + description: Name of the VM. + returned: success + type: string + sample: web-01 +ip_address: + description: Primary IP of the NIC. + returned: success + type: string + sample: 10.10.10.10 +netmask: + description: Netmask of the NIC. + returned: success + type: string + sample: 255.255.255.0 +mac_address: + description: MAC address of the NIC. + returned: success + type: string + sample: 02:00:33:31:00:e4 +vm_guest_ip: + description: Secondary IP of the NIC. + returned: success + type: string + sample: 10.10.10.10 +network: + description: Name of the network if not default. + returned: success + type: string + sample: sync network +domain: + description: Domain the VM is related to. + returned: success + type: string + sample: example domain +account: + description: Account the VM is related to. + returned: success + type: string + sample: example account +project: + description: Name of project the VM is related to. + returned: success + type: string + sample: Production +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.cloudstack import * + + +class AnsibleCloudStackNic(AnsibleCloudStack): + + def __init__(self, module): + super(AnsibleCloudStackNic, self).__init__(module) + self.vm_guest_ip = self.module.params.get('vm_guest_ip') + self.nic = None + self.returns = { + 'ipaddress': 'ip_address', + 'macaddress': 'mac_address', + 'netmask': 'netmask', + } + + def get_nic(self): + if self.nic: + return self.nic + args = { + 'virtualmachineid': self.get_vm(key='id'), + 'networkdid': self.get_network(key='id'), + } + nics = self.cs.listNics(**args) + if nics: + self.nic = nics['nic'][0] + return self.nic + self.module.fail_json("NIC for VM %s in network %s not found" (self.get_vm(key='name'), self.get_network(key='name'))) + + def get_secondary_ip(self): + nic = self.get_nic() + if self.vm_guest_ip: + secondary_ips = nic.get('secondaryip') or [] + for secondary_ip in secondary_ips: + if secondary_ip['ipaddress'] == self.vm_guest_ip: + return secondary_ip + return None + + def present_nic(self): + nic = self.get_nic() + if not self.get_secondary_ip(): + self.result['changed'] = True + args = { + 'nicid': nic['id'], + 'ipaddress': self.vm_guest_ip, + } + + if not self.module.check_mode: + res = self.cs.addIpToNic(**args) + + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + + poll_async = self.module.params.get('poll_async') + if poll_async: + nic = self.poll_job(res, 'nicsecondaryip') + # Save result for RETURNS + self.vm_guest_ip = nic['ipaddress'] + return nic + + def absent_nic(self): + nic = self.get_nic() + secondary_ip = self.get_secondary_ip() + if secondary_ip: + self.result['changed'] = True + if not self.module.check_mode: + res = self.cs.removeIpFromNic(id=secondary_ip['id']) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % nic['errortext']) + + poll_async = self.module.params.get('poll_async') + if poll_async: + self.poll_job(res, 'nicsecondaryip') + return nic + + def get_result(self, nic): + super(AnsibleCloudStackNic, self).get_result(nic) + if nic and not self.module.params.get('network'): + self.module.params['network'] = nic.get('networkid') + self.result['network'] = self.get_network(key='name') + self.result['vm'] = self.get_vm(key='name') + self.result['vm_guest_ip'] = self.vm_guest_ip + self.result['domain'] = self.get_domain(key='path') + self.result['account'] = self.get_account(key='name') + self.result['project'] = self.get_project(key='name') + return self.result + + +def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + vm=dict(required=True, aliases=['name']), + vm_guest_ip=dict(default=None, aliases=['secondary_ip']), + network=dict(default=None), + vpc=dict(default=None), + state=dict(choices=['present', 'absent'], default='present'), + domain=dict(default=None), + account=dict(default=None), + project=dict(default=None), + zone=dict(default=None), + poll_async=dict(type='bool', default=True), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=cs_required_together(), + supports_check_mode=True, + required_if=([ + ('state', 'absent', ['vm_guest_ip']) + ]) + ) + + try: + acs_nic = AnsibleCloudStackNic(module) + + state = module.params.get('state') + + if state == 'absent': + nic = acs_nic.absent_nic() + else: + nic = acs_nic.present_nic() + + result = acs_nic.get_result(nic) + + except CloudStackException as e: + module.fail_json(msg='CloudStackException: %s' % str(e)) + + module.exit_json(**result) + +if __name__ == '__main__': + main() From ba1833cf3d3fb9ea0bc9ea1a15ef22167f06615f Mon Sep 17 00:00:00 2001 From: Abdul-Anshad-A Date: Sun, 13 Nov 2016 14:35:18 -0500 Subject: [PATCH 2373/2522] Initial effort for pyVmomi snapshot ops Make description optional during snapshot create --- cloud/vmware/vmware_guest.py | 174 +++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) diff --git a/cloud/vmware/vmware_guest.py b/cloud/vmware/vmware_guest.py index 6ed98f2c2d9..a0c108b7e0a 100644 --- a/cloud/vmware/vmware_guest.py +++ b/cloud/vmware/vmware_guest.py @@ -114,6 +114,11 @@ - Domain to use while customizing required: False version_added: "2.3" + snapshot_op: + description: + - A key, value pair of snapshot operation types and their additional required parameters. + required: False + version_added: "2.3" extends_documentation_fragment: vmware.documentation ''' @@ -184,6 +189,71 @@ name: testvm_2 esxi_hostname: 192.168.1.117 register: facts + +### Snapshot Operations +# Create snapshot + - vmware_guest: + hostname: 192.168.1.209 + username: administrator@vsphere.local + password: vmware + validate_certs: False + name: dummy_vm + snapshot_op: + op_type: create + name: snap1 + description: snap1_description + +# Remove a snapshot + - vmware_guest: + hostname: 192.168.1.209 + username: administrator@vsphere.local + password: vmware + validate_certs: False + name: dummy_vm + snapshot_op: + op_type: remove + name: snap1 + +# Revert to a snapshot + - vmware_guest: + hostname: 192.168.1.209 + username: administrator@vsphere.local + password: vmware + validate_certs: False + name: dummy_vm + snapshot_op: + op_type: revert + name: snap1 + +# List all snapshots of a VM + - vmware_guest: + hostname: 192.168.1.209 + username: administrator@vsphere.local + password: vmware + validate_certs: False + name: dummy_vm + snapshot_op: + op_type: list_all + +# List current snapshot of a VM + - vmware_guest: + hostname: 192.168.1.209 + username: administrator@vsphere.local + password: vmware + validate_certs: False + name: dummy_vm + snapshot_op: + op_type: list_current + +# Remove all snapshots of a VM + - vmware_guest: + hostname: 192.168.1.209 + username: administrator@vsphere.local + password: vmware + validate_certs: False + name: dummy_vm + snapshot_op: + op_type: remove_all ''' RETURN = """ @@ -1012,6 +1082,107 @@ def run_command_in_guest(self, vm, username, password, program_path, program_arg return result + def list_snapshots_recursively(self, snapshots): + snapshot_data = [] + snap_text = '' + for snapshot in snapshots: + snap_text = 'Id: %s; Name: %s; Description: %s; CreateTime: %s; State: %s'%(snapshot.id, snapshot.name, + snapshot.description, snapshot.createTime, snapshot.state) + snapshot_data.append(snap_text) + snapshot_data = snapshot_data + self.list_snapshots_recursively(snapshot.childSnapshotList) + return snapshot_data + + + def get_snapshots_by_name_recursively(self, snapshots, snapname): + snap_obj = [] + for snapshot in snapshots: + if snapshot.name == snapname: + snap_obj.append(snapshot) + else: + snap_obj = snap_obj + self.get_snapshots_by_name_recursively(snapshot.childSnapshotList, snapname) + return snap_obj + + def get_current_snap_obj(self, snapshots, snapob): + snap_obj = [] + for snapshot in snapshots: + if snapshot.snapshot == snapob: + snap_obj.append(snapshot) + snap_obj = snap_obj + self.get_current_snap_obj(snapshot.childSnapshotList, snapob) + return snap_obj + + def snapshot_vm(self, vm, guest, snapshot_op): + ''' To perform snapshot operations create/remove/revert/list_all/list_current/remove_all ''' + + try: + snapshot_op_name = snapshot_op['op_type'] + except KeyError: + self.module.fail_json(msg="Specify op_type - create/remove/revert/list_all/list_current/remove_all") + + task = None + result = {} + + if snapshot_op_name not in ['create', 'remove', 'revert', 'list_all', 'list_current', 'remove_all']: + self.module.fail_json(msg="Specify op_type - create/remove/revert/list_all/list_current/remove_all") + + if snapshot_op_name != 'create' and vm.snapshot is None: + self.module.exit_json(msg="VM - %s doesn't have any snapshots"%guest) + + if snapshot_op_name == 'create': + try: + snapname = snapshot_op['name'] + except KeyError: + self.module.fail_json(msg="specify name & description(optional) to create a snapshot") + + if 'description' in snapshot_op: + snapdesc = snapshot_op['description'] + else: + snapdesc = '' + + dumpMemory = False + quiesce = False + task = vm.CreateSnapshot(snapname, snapdesc, dumpMemory, quiesce) + + elif snapshot_op_name in ['remove', 'revert']: + try: + snapname = snapshot_op['name'] + except KeyError: + self.module.fail_json(msg="specify snapshot name") + + snap_obj = self.get_snapshots_by_name_recursively(vm.snapshot.rootSnapshotList, snapname) + + #if len(snap_obj) is 0; then no snapshots with specified name + if len(snap_obj) == 1: + snap_obj = snap_obj[0].snapshot + if snapshot_op_name == 'remove': + task = snap_obj.RemoveSnapshot_Task(True) + else: + task = snap_obj.RevertToSnapshot_Task() + else: + self.module.exit_json(msg="Couldn't find any snapshots with specified name: %s on VM: %s"%(snapname, guest)) + + elif snapshot_op_name == 'list_all': + snapshot_data = self.list_snapshots_recursively(vm.snapshot.rootSnapshotList) + result['snapshot_data'] = snapshot_data + + elif snapshot_op_name == 'list_current': + current_snapref = vm.snapshot.currentSnapshot + current_snap_obj = self.get_current_snap_obj(vm.snapshot.rootSnapshotList, current_snapref) + result['current_snapshot'] = 'Id: %s; Name: %s; Description: %s; CreateTime: %s; State: %s'%(current_snap_obj[0].id, + current_snap_obj[0].name, current_snap_obj[0].description, current_snap_obj[0].createTime, + current_snap_obj[0].state) + + elif snapshot_op_name == 'remove_all': + task = vm.RemoveAllSnapshots() + + if task: + self.wait_for_task(task) + if task.info.state == 'error': + result = {'changed': False, 'failed': True, 'msg': task.info.error.msg} + else: + result = {'changed': True, 'failed': False} + + return result + def get_obj(content, vimtype, name): """ Return an object by name, if name is None the @@ -1066,6 +1237,7 @@ def main(): template_src=dict(required=False, type='str', aliases=['template']), name=dict(required=True, type='str'), name_match=dict(required=False, type='str', default='first'), + snapshot_op=dict(required=False, type='dict', default={}), uuid=dict(required=False, type='str'), folder=dict(required=False, type='str', default='/vm', aliases=['folder']), disk=dict(required=False, type='list'), @@ -1110,6 +1282,8 @@ def main(): elif module.params['state'] in ['poweredon', 'poweredoff', 'restarted']: # set powerstate result = pyv.set_powerstate(vm, module.params['state'], module.params['force']) + elif module.params['snapshot_op']: + result = pyv.snapshot_vm(vm, module.params['name'], module.params['snapshot_op']) else: # Run for facts only try: From 6fbab2b43201d986f07b55c1384767699c07871a Mon Sep 17 00:00:00 2001 From: Davis Phillips Date: Tue, 15 Nov 2016 14:39:20 -0600 Subject: [PATCH 2374/2522] added vDS support for config spec and note/annotation support --- cloud/vmware/vmware_guest.py | 61 +++++++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/cloud/vmware/vmware_guest.py b/cloud/vmware/vmware_guest.py index 6ed98f2c2d9..10d6b46c15a 100644 --- a/cloud/vmware/vmware_guest.py +++ b/cloud/vmware/vmware_guest.py @@ -89,6 +89,11 @@ description: - The esxi hostname where the VM will run. required: True + annotation: + description: + - A note or annotation to include in the VM + required: False + version_added: "2.3" customize: description: - Should customization spec be run @@ -723,17 +728,18 @@ def deploy_template(self, poweron=False, wait_for_ip=False): # lets try and assign a static ip addresss if self.params['customize'] is True: ip_settings = list() - if self.params['ips'] and self.params['network']: + if self.params['ips']: for ip_string in self.params['ips']: ip = IPAddress(self.params['ips']) for network in self.params['networks']: - if ip in IPNetwork(network): - self.params['networks'][network]['ip'] = str(ip) - ipnet = IPNetwork(network) - self.params['networks'][network]['subnet_mask'] = str( - ipnet.netmask - ) - ip_settings.append(self.params['networks'][network]) + if network: + if ip in IPNetwork(network): + self.params['networks'][network]['ip'] = str(ip) + ipnet = IPNetwork(network) + self.params['networks'][network]['subnet_mask'] = str( + ipnet.netmask + ) + ip_settings.append(self.params['networks'][network]) key = 0 network = get_obj(self.content, [vim.Network], ip_settings[key]['network']) @@ -764,15 +770,28 @@ def deploy_template(self, poweron=False, wait_for_ip=False): nic.device.addressType = 'assigned' nic.device.deviceInfo = vim.Description() nic.device.deviceInfo.label = 'Network Adapter %s' % (key + 1) - nic.device.deviceInfo.summary = ip_settings[key]['network'] - nic.device.backing = vim.vm.device.VirtualEthernetCard.NetworkBackingInfo() - nic.device.backing.network = get_obj(self.content, [vim.Network], ip_settings[key]['network']) + + if hasattr(get_obj(self.content, [vim.Network], ip_settings[key]['network']), 'portKeys'): + # VDS switch + pg_obj = get_obj(self.content, [vim.dvs.DistributedVirtualPortgroup], ip_settings[key]['network']) + dvs_port_connection = vim.dvs.PortConnection() + dvs_port_connection.portgroupKey= pg_obj.key + dvs_port_connection.switchUuid= pg_obj.config.distributedVirtualSwitch.uuid + nic.device.backing = vim.vm.device.VirtualEthernetCard.DistributedVirtualPortBackingInfo() + nic.device.backing.port = dvs_port_connection + + else: + # vSwitch + nic.device.backing = vim.vm.device.VirtualEthernetCard.NetworkBackingInfo() + nic.device.backing.network = get_obj(self.content, [vim.Network], ip_settings[key]['network']) + nic.device.backing.deviceName = ip_settings[key]['network'] - nic.device.backing.deviceName = ip_settings[key]['network'] nic.device.connectable = vim.vm.device.VirtualDevice.ConnectInfo() nic.device.connectable.startConnected = True nic.device.connectable.allowGuestControl = True + nic.device.connectable.connected = True + nic.device.connectable.allowGuestControl = True devices.append(nic) # Update the spec with the added NIC @@ -808,13 +827,12 @@ def deploy_template(self, poweron=False, wait_for_ip=False): ident.hostName.name = self.params['name'] customspec = vim.vm.customization.Specification() - customspec.nicSettingMap = adaptermaps - customspec.globalIPSettings = globalip - customspec.identity = ident - - clonespec = vim.vm.CloneSpec(**clonespec_kwargs) - clonespec.customization = customspec + clonespec_kwargs['customization'] = customspec + clonespec_kwargs['customization'].nicSettingMap = adaptermaps + clonespec_kwargs['customization'].globalIPSettings = globalip + clonespec_kwargs['customization'].identity = ident + clonespec = vim.vm.CloneSpec(**clonespec_kwargs) task = template.Clone(folder=destfolder, name=self.params['name'], spec=clonespec) self.wait_for_task(task) @@ -825,7 +843,13 @@ def deploy_template(self, poweron=False, wait_for_ip=False): return ({'changed': False, 'failed': True, 'msg': task.info.error.msg}) else: + # set annotation vm = task.info.result + if self.params['annotation']: + annotation_spec = vim.vm.ConfigSpec() + annotation_spec.annotation = str(self.params['annotation']) + task = vm.ReconfigVM_Task(annotation_spec) + self.wait_for_task(task) if wait_for_ip: self.set_powerstate(vm, 'poweredon', force=False) self.wait_for_vm_ip(vm) @@ -1064,6 +1088,7 @@ def main(): default='present'), validate_certs=dict(required=False, type='bool', default=True), template_src=dict(required=False, type='str', aliases=['template']), + annotation=dict(required=False, type='str', aliases=['notes']), name=dict(required=True, type='str'), name_match=dict(required=False, type='str', default='first'), uuid=dict(required=False, type='str'), From bcaf2a7d8333348ba02de947534410a94c7faed2 Mon Sep 17 00:00:00 2001 From: Davis Phillips Date: Tue, 15 Nov 2016 15:02:36 -0600 Subject: [PATCH 2375/2522] mend --- cloud/vmware/vmware_guest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/vmware/vmware_guest.py b/cloud/vmware/vmware_guest.py index 10d6b46c15a..2d7cd94f437 100644 --- a/cloud/vmware/vmware_guest.py +++ b/cloud/vmware/vmware_guest.py @@ -732,7 +732,7 @@ def deploy_template(self, poweron=False, wait_for_ip=False): for ip_string in self.params['ips']: ip = IPAddress(self.params['ips']) for network in self.params['networks']: - if network: + if network: if ip in IPNetwork(network): self.params['networks'][network]['ip'] = str(ip) ipnet = IPNetwork(network) @@ -849,7 +849,7 @@ def deploy_template(self, poweron=False, wait_for_ip=False): annotation_spec = vim.vm.ConfigSpec() annotation_spec.annotation = str(self.params['annotation']) task = vm.ReconfigVM_Task(annotation_spec) - self.wait_for_task(task) + self.wait_for_task(task) if wait_for_ip: self.set_powerstate(vm, 'poweredon', force=False) self.wait_for_vm_ip(vm) From 9c2adc4b99abe4f5c419c5afa767db823593ba84 Mon Sep 17 00:00:00 2001 From: Davis Phillips Date: Tue, 15 Nov 2016 15:08:56 -0600 Subject: [PATCH 2376/2522] removed tabs and fixed formatting --- cloud/vmware/vmware_guest.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cloud/vmware/vmware_guest.py b/cloud/vmware/vmware_guest.py index 2d7cd94f437..c43bbcccb55 100644 --- a/cloud/vmware/vmware_guest.py +++ b/cloud/vmware/vmware_guest.py @@ -832,7 +832,7 @@ def deploy_template(self, poweron=False, wait_for_ip=False): clonespec_kwargs['customization'].nicSettingMap = adaptermaps clonespec_kwargs['customization'].globalIPSettings = globalip clonespec_kwargs['customization'].identity = ident - + clonespec = vim.vm.CloneSpec(**clonespec_kwargs) task = template.Clone(folder=destfolder, name=self.params['name'], spec=clonespec) self.wait_for_task(task) @@ -855,7 +855,6 @@ def deploy_template(self, poweron=False, wait_for_ip=False): self.wait_for_vm_ip(vm) vm_facts = self.gather_facts(vm) return ({'changed': True, 'failed': False, 'instance': vm_facts}) - def wait_for_task(self, task): # https://www.vmware.com/support/developer/vc-sdk/visdk25pubs/ReferenceGuide/vim.Task.html From c6249cbbbde490cce6a91add530af7773cc530d8 Mon Sep 17 00:00:00 2001 From: jctanner Date: Tue, 15 Nov 2016 21:12:05 -0500 Subject: [PATCH 2377/2522] vmware_guest: Fix the esxi_hostname docstring to match the arg dict's required= (#3479) Fixes #3476 --- cloud/vmware/vmware_guest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/vmware/vmware_guest.py b/cloud/vmware/vmware_guest.py index cc520432de4..2a749bda674 100644 --- a/cloud/vmware/vmware_guest.py +++ b/cloud/vmware/vmware_guest.py @@ -88,7 +88,7 @@ esxi_hostname: description: - The esxi hostname where the VM will run. - required: True + required: False annotation: description: - A note or annotation to include in the VM From 680af9eb386a35a906b982011302d1367b1e8f15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Wed, 16 Nov 2016 08:09:14 +0100 Subject: [PATCH 2378/2522] ipify_facts: implement timeout (#3369) --- network/ipify_facts.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/network/ipify_facts.py b/network/ipify_facts.py index 95bf549be92..7a07f577c2e 100644 --- a/network/ipify_facts.py +++ b/network/ipify_facts.py @@ -33,6 +33,12 @@ - C(?format=json) will be appended per default. required: false default: 'https://api.ipify.org' + timeout: + description: + - HTTP connection timeout in seconds. + required: false + default: 10 + version_added: "2.3" notes: - "Visit https://www.ipify.org to get more information." ''' @@ -42,9 +48,11 @@ - name: get my public IP ipify_facts: -# Gather IP facts from your own ipify service endpoint +# Gather IP facts from your own ipify service endpoint with a custom timeout - name: get my public IP - ipify_facts: api_url=http://api.example.com/ipify + ipify_facts: + api_url: http://api.example.com/ipify + timeout: 20 ''' RETURN = ''' @@ -65,27 +73,35 @@ # Let snippet from module_utils/basic.py return a proper error in this case pass +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.urls import fetch_url + class IpifyFacts(object): def __init__(self): self.api_url = module.params.get('api_url') + self.timeout = module.params.get('timeout') def run(self): result = { 'ipify_public_ip': None } - (response, info) = fetch_url(module, self.api_url + "?format=json" , force=True) - if response: - data = json.loads(response.read()) - result['ipify_public_ip'] = data.get('ip') + (response, info) = fetch_url(module=module, url=self.api_url + "?format=json" , force=True, timeout=self.timeout) + + if not response: + module.fail_json(msg="No valid or no response from url %s within %s seconds (timeout)" % (self.api_url, self.timeout)) + + data = json.loads(response.read()) + result['ipify_public_ip'] = data.get('ip') return result def main(): global module module = AnsibleModule( argument_spec = dict( - api_url = dict(default='https://api.ipify.org'), + api_url=dict(default='https://api.ipify.org'), + timeout=dict(type='int', default=10), ), supports_check_mode=True, ) @@ -94,7 +110,5 @@ def main(): ipify_facts_result = dict(changed=False, ansible_facts=ipify_facts) module.exit_json(**ipify_facts_result) -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * if __name__ == '__main__': main() From c68fc88390fa21e49780816f27857a3bc1fab442 Mon Sep 17 00:00:00 2001 From: Brian Haggard Date: Wed, 16 Nov 2016 01:11:58 -0600 Subject: [PATCH 2379/2522] Required and default are mutually exclusive (#3475) --- cloud/amazon/lambda_event.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/lambda_event.py b/cloud/amazon/lambda_event.py index 306762a676d..ddc5a5aa951 100644 --- a/cloud/amazon/lambda_event.py +++ b/cloud/amazon/lambda_event.py @@ -386,7 +386,7 @@ def main(): dict( state=dict(required=False, default='present', choices=['present', 'absent']), lambda_function_arn=dict(required=True, default=None, aliases=['function_name', 'function_arn']), - event_source=dict(required=True, default="stream", choices=source_choices), + event_source=dict(required=False, default="stream", choices=source_choices), source_params=dict(type='dict', required=True, default=None), alias=dict(required=False, default=None), version=dict(type='int', required=False, default=0), From 74af996f59ff0c617f051afc1628e3133204a917 Mon Sep 17 00:00:00 2001 From: Abdoul Bah Date: Wed, 16 Nov 2016 10:58:22 +0100 Subject: [PATCH 2380/2522] New module proxmox_kvm (#3292) * New module proxmox_kvm * fixed qxl value vor vga param > | Name | Type | Format | Description | > |------|------|--------|-------------| > | vga | enum | std \| cirrus \| vmware \| qxl \| serial0 \| serial1 \| serial2 \| serial3 \| qxl2 \| qxl3 \| qxl4 | Select the VGA type. If you want to use high resolution modes (>= 1280x1024x16) then you should use the options 'std' or 'vmware'. Default is 'std' for win8/win7/w2k8, and 'cirrus' for other OS types. The 'qxl' option enables the SPICE display sever. For win* OS you can select how many independent displays you want, Linux guests can add displays them self. You can also run without any graphic card, using a serial device as terminal. | * Fix create_vm() fail on PV 4.3 * Set default for force as null in doc * proxmox_kvm: revision fixes * proxmox_kvm: more revision fixes * Fix indentation * revision fixes * Ensure PEP-3110: Catching Exceptions * KeyError, to KeyError as -- PEP-3110: Catching Exceptions * Fix Yaml document syntax; Notes: => Notes - * Refix documentation issue * Fix Documentation * Remove Notes: in description * Add current state and it return value * Update documentation * fixed local variable 'results' referenced before assignment * Fix fixed local variable 'results' referenced before assignment * minor fixes in error messages * merge upstream/devel int devel * minor fixes in error messages * Fix indentation and documentation * Update validate_certs description --- cloud/misc/proxmox_kvm.py | 1054 +++++++++++++++++++++++++++++++++++++ 1 file changed, 1054 insertions(+) create mode 100644 cloud/misc/proxmox_kvm.py diff --git a/cloud/misc/proxmox_kvm.py b/cloud/misc/proxmox_kvm.py new file mode 100644 index 00000000000..96c06707612 --- /dev/null +++ b/cloud/misc/proxmox_kvm.py @@ -0,0 +1,1054 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Abdoul Bah (@helldorado) + +""" +Ansible module to manage Qemu(KVM) instance in Proxmox VE cluster. +This module is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. +This software is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +You should have received a copy of the GNU General Public License +along with this software. If not, see . +""" + +DOCUMENTATION = ''' +--- +module: proxmox_kvm +short_description: Management of Qemu(KVM) Virtual Machines in Proxmox VE cluster. +description: + - Allows you to create/delete/stop Qemu(KVM) Virtual Machines in Proxmox VE cluster. +version_added: "2.3" +author: "Abdoul Bah (@helldorado) " +options: + acpi: + description: + - Specify if ACPI should be enables/disabled. + required: false + default: "yes" + choices: [ "yes", "no" ] + type: boolean + agent: + description: + - Specify if the QEMU GuestAgent should be enabled/disabled. + required: false + default: null + choices: [ "yes", "no" ] + type: boolean + args: + description: + - Pass arbitrary arguments to kvm. + - This option is for experts only! + default: "-serial unix:/var/run/qemu-server/VMID.serial,server,nowait" + required: false + type: string + api_host: + description: + - Specify the target host of the Proxmox VE cluster. + required: true + api_user: + description: + - Specify the user to authenticate with. + required: true + api_password: + description: + - Specify the password to authenticate with. + - You can use C(PROXMOX_PASSWORD) environment variable. + default: null + required: false + autostart: + description: + - Specify, if the VM should be automatically restarted after crash (currently ignored in PVE API). + required: false + default: "no" + choices: [ "yes", "no" ] + type: boolean + balloon: + description: + - Specify the amount of RAM for the VM in MB. + - Using zero disables the balloon driver. + required: false + default: 0 + type: integer + bios: + description: + - Specify the BIOS implementation. + choices: ['seabios', 'ovmf'] + required: false + default: null + type: string + boot: + description: + - Specify the boot order -> boot on floppy C(a), hard disk C(c), CD-ROM C(d), or network C(n). + - You can combine to set order. + required: false + default: cnd + type: string + bootdisk: + description: + - Enable booting from specified disk. C((ide|sata|scsi|virtio)\d+) + required: false + default: null + type: string + cores: + description: + - Specify number of cores per socket. + required: false + default: 1 + type: integer + cpu: + description: + - Specify emulated CPU type. + required: false + default: kvm64 + type: string + cpulimit: + description: + - Specify if CPU usage will be limited. Value 0 indicates no CPU limit. + - If the computer has 2 CPUs, it has total of '2' CPU time + required: false + default: null + type: integer + cpuunits: + description: + - Specify CPU weight for a VM. + - You can disable fair-scheduler configuration by setting this to 0 + default: 1000 + required: false + type: integer + delete: + description: + - Specify a list of settings you want to delete. + required: false + default: null + type: string + description: + description: + - Specify the description for the VM. Only used on the configuration web interface. + - This is saved as comment inside the configuration file. + required: false + default: null + type: string + digest: + description: + - Specify if to prevent changes if current configuration file has different SHA1 digest. + - This can be used to prevent concurrent modifications. + required: false + default: null + type: string + force: + description: + - Allow to force stop VM. + - Can be used only with states C(stopped), C(restarted). + default: null + choices: [ "yes", "no" ] + required: false + type: boolean + freeze: + description: + - Specify if PVE should freeze CPU at startup (use 'c' monitor command to start execution). + required: false + default: null + choices: [ "yes", "no" ] + type: boolean + hostpci: + description: + - Specify a hash/dictionary of map host pci devices into guest. C(hostpci='{"key":"value", "key":"value"}'). + - Keys allowed are - C(hostpci[n]) where 0 ≤ n ≤ N. + - Values allowed are - C("host="HOSTPCIID[;HOSTPCIID2...]",pcie="1|0",rombar="1|0",x-vga="1|0""). + - The C(host) parameter is Host PCI device pass through. HOSTPCIID syntax is C(bus:dev.func) (hexadecimal numbers). + - C(pcie=boolean) I(default=0) Choose the PCI-express bus (needs the q35 machine model). + - C(rombar=boolean) I(default=1) Specify whether or not the device’s ROM will be visible in the guest’s memory map. + - C(x-vga=boolean) I(default=0) Enable vfio-vga device support. + - /!\ This option allows direct access to host hardware. So it is no longer possible to migrate such machines - use with special care. + required: false + default: null + type: A hash/dictionary defining host pci devices + hotplug: + description: + - Selectively enable hotplug features. + - This is a comma separated list of hotplug features C('network', 'disk', 'cpu', 'memory' and 'usb'). + - Value 0 disables hotplug completely and value 1 is an alias for the default C('network,disk,usb'). + required: false + default: null + type: string + hugepages: + description: + - Enable/disable hugepages memory. + choices: ['any', '2', '1024'] + required: false + default: null + type: string + ide: + description: + - A hash/dictionary of volume used as IDE hard disk or CD-ROM. C(ide='{"key":"value", "key":"value"}'). + - Keys allowed are - C(ide[n]) where 0 ≤ n ≤ 3. + - Values allowed are - C("storage:size,format=value"). + - C(storage) is the storage identifier where to create the disk. + - C(size) is the size of the disk in GB. + - C(format) is the drive’s backing file’s data format. C(qcow2|raw|subvol). + required: false + default: null + type: A hash/dictionary defining ide + keyboard: + description: + - Sets the keyboard layout for VNC server. + required: false + default: null + type: string + kvm: + description: + - Enable/disable KVM hardware virtualization. + required: false + default: "yes" + choices: [ "yes", "no" ] + type: boolean + localtime: + description: + - Sets the real time clock to local time. + - This is enabled by default if ostype indicates a Microsoft OS. + required: false + default: null + choices: [ "yes", "no" ] + type: boolean + lock: + description: + - Lock/unlock the VM. + choices: ['migrate', 'backup', 'snapshot', 'rollback'] + required: false + default: null + type: string + machine: + description: + - Specifies the Qemu machine type. + - type => C((pc|pc(-i440fx)?-\d+\.\d+(\.pxe)?|q35|pc-q35-\d+\.\d+(\.pxe)?)) + required: false + default: null + type: string + memory: + description: + - Memory size in MB for instance. + required: false + default: 512 + type: integer + migrate_downtime: + description: + - Sets maximum tolerated downtime (in seconds) for migrations. + required: false + default: null + type: integer + migrate_speed: + description: + - Sets maximum speed (in MB/s) for migrations. + - A value of 0 is no limit. + required: false + default: null + type: integer + name: + description: + - Specifies the VM name. Only used on the configuration web interface. + - Required only for C(state=present). + default: null + required: false + net: + description: + - A hash/dictionary of network interfaces for the VM. C(net='{"key":"value", "key":"value"}'). + - Keys allowed are - C(net[n]) where 0 ≤ n ≤ N. + - Values allowed are - C("model="XX:XX:XX:XX:XX:XX",brigde="value",rate="value",tag="value",firewall="1|0",trunks="vlanid""). + - Model is one of C(e1000 e1000-82540em e1000-82544gc e1000-82545em i82551 i82557b i82559er ne2k_isa ne2k_pci pcnet rtl8139 virtio vmxnet3). + - C(XX:XX:XX:XX:XX:XX) should be an unique MAC address. This is automatically generated if not specified. + - The C(bridge) parameter can be used to automatically add the interface to a bridge device. The Proxmox VE standard bridge is called 'vmbr0'. + - Option C(rate) is used to limit traffic bandwidth from and to this interface. It is specified as floating point number, unit is 'Megabytes per second'. + - If you specify no bridge, we create a kvm 'user' (NATed) network device, which provides DHCP and DNS services. + default: null + required: false + type: A hash/dictionary defining interfaces + node: + description: + - Proxmox VE node, where the new VM will be created. + - Only required for C(state=present). + - For other states, it will be autodiscovered. + default: null + required: false + numa: + description: + - A hash/dictionaries of NUMA topology. C(numa='{"key":"value", "key":"value"}'). + - Keys allowed are - C(numa[n]) where 0 ≤ n ≤ N. + - Values allowed are - C("cpu="",hostnodes="",memory="number",policy="(bind|interleave|preferred)""). + - C(cpus) CPUs accessing this NUMA node. + - C(hostnodes) Host NUMA nodes to use. + - C(memory) Amount of memory this NUMA node provides. + - C(policy) NUMA allocation policy. + default: null + required: false + type: A hash/dictionary defining NUMA topology + onboot: + description: + - Specifies whether a VM will be started during system bootup. + default: "yes" + choices: [ "yes", "no" ] + required: false + type: boolean + ostype: + description: + - Specifies guest operating system. This is used to enable special optimization/features for specific operating systems. + - The l26 is Linux 2.6/3.X Kernel. + choices: ['other', 'wxp', 'w2k', 'w2k3', 'w2k8', 'wvista', 'win7', 'win8', 'l24', 'l26', 'solaris'] + default: l26 + required: false + type: string + parallel: + description: + - A hash/dictionary of map host parallel devices. C(parallel='{"key":"value", "key":"value"}'). + - Keys allowed are - (parallel[n]) where 0 ≤ n ≤ 2. + - Values allowed are - C("/dev/parport\d+|/dev/usb/lp\d+"). + default: null + required: false + type: A hash/dictionary defining host parallel devices + protection: + description: + - Enable/disable the protection flag of the VM. This will enable/disable the remove VM and remove disk operations. + default: null + choices: [ "yes", "no" ] + required: false + type: boolean + reboot: + description: + - Allow reboot. If set to yes, the VM exit on reboot. + default: null + choices: [ "yes", "no" ] + required: false + type: boolean + revert: + description: + - Revert a pending change. + default: null + required: false + type: string + sata: + description: + - A hash/dictionary of volume used as sata hard disk or CD-ROM. C(sata='{"key":"value", "key":"value"}'). + - Keys allowed are - C(sata[n]) where 0 ≤ n ≤ 5. + - Values allowed are - C("storage:size,format=value"). + - C(storage) is the storage identifier where to create the disk. + - C(size) is the size of the disk in GB. + - C(format) is the drive’s backing file’s data format. C(qcow2|raw|subvol). + default: null + required: false + type: A hash/dictionary defining sata + scsi: + description: + - A hash/dictionary of volume used as SCSI hard disk or CD-ROM. C(scsi='{"key":"value", "key":"value"}'). + - Keys allowed are - C(sata[n]) where 0 ≤ n ≤ 13. + - Values allowed are - C("storage:size,format=value"). + - C(storage) is the storage identifier where to create the disk. + - C(size) is the size of the disk in GB. + - C(format) is the drive’s backing file’s data format. C(qcow2|raw|subvol). + default: null + required: false + type: A hash/dictionary defining scsi + scsihw: + description: + - Specifies the SCSI controller model. + choices: ['lsi', 'lsi53c810', 'virtio-scsi-pci', 'virtio-scsi-single', 'megasas', 'pvscsi'] + required: false + default: null + type: string + serial: + description: + - A hash/dictionary of serial device to create inside the VM. C('{"key":"value", "key":"value"}'). + - Keys allowed are - serial[n](str; required) where 0 ≤ n ≤ 3. + - Values allowed are - C((/dev/.+|socket)). + - /!\ If you pass through a host serial device, it is no longer possible to migrate such machines - use with special care. + default: null + required: false + type: A hash/dictionary defining serial + shares: + description: + - Rets amount of memory shares for auto-ballooning. (0 - 50000). + - The larger the number is, the more memory this VM gets. + - The number is relative to weights of all other running VMs. + - Using 0 disables auto-ballooning, this means no limit. + required: false + default: null + type: integer + skiplock: + description: + - Ignore locks + - Only root is allowed to use this option. + required: false + default: null + choices: [ "yes", "no" ] + type: boolean + smbios: + description: + - Specifies SMBIOS type 1 fields. + required: false + default: null + type: string + sockets: + description: + - Sets the number of CPU sockets. (1 - N). + required: false + default: 1 + type: integer + startdate: + description: + - Sets the initial date of the real time clock. + - Valid format for date are C('now') or C('2016-09-25T16:01:21') or C('2016-09-25'). + required: false + default: null + type: string + startup: + description: + - Startup and shutdown behavior. C([[order=]\d+] [,up=\d+] [,down=\d+]). + - Order is a non-negative number defining the general startup order. + - Shutdown in done with reverse ordering. + required: false + default: null + type: string + state: + description: + - Indicates desired state of the instance. + - If C(current), the current state of the VM will be fecthed. You can acces it with C(results.status) + choices: ['present', 'started', 'absent', 'stopped', 'restarted','current'] + required: false + default: present + tablet: + description: + - Enables/disables the USB tablet device. + required: false + choices: [ "yes", "no" ] + default: "no" + type: boolean + tdf: + description: + - Enables/disables time drift fix. + required: false + default: null + choices: [ "yes", "no" ] + type: boolean + template: + description: + - Enables/disables the template. + required: false + default: "no" + choices: [ "yes", "no" ] + type: boolean + timeout: + description: + - Timeout for operations. + default: 30 + required: false + type: integer + validate_certs: + description: + - If C(no), SSL certificates will not be validated. This should only be used on personally controlled sites using self-signed certificates. + default: "no" + choices: [ "yes", "no" ] + required: false + type: boolean + vcpus: + description: + - Sets number of hotplugged vcpus. + required: false + default: null + type: integer + vga: + description: + - Select VGA type. If you want to use high resolution modes (>= 1280x1024x16) then you should use option 'std' or 'vmware'. + choices: ['std', 'cirrus', 'vmware', 'qxl', 'serial0', 'serial1', 'serial2', 'serial3', 'qxl2', 'qxl3', 'qxl4'] + required: false + default: std + virtio: + description: + - A hash/dictionary of volume used as VIRTIO hard disk. C(virtio='{"key":"value", "key":"value"}'). + - Keys allowed are - C(virto[n]) where 0 ≤ n ≤ 15. + - Values allowed are - C("storage:size,format=value"). + - C(storage) is the storage identifier where to create the disk. + - C(size) is the size of the disk in GB. + - C(format) is the drive’s backing file’s data format. C(qcow2|raw|subvol). + required: false + default: null + type: A hash/dictionary defining virtio + vmid: + description: + - Specifies the VM ID. Instead use I(name) parameter. + - If vmid is not set, the next available VM ID will be fetched from ProxmoxAPI. + default: null + required: false + watchdog: + description: + - Creates a virtual hardware watchdog device. + required: false + default: null + type: string +Notes: + - Requires proxmoxer and requests modules on host. This modules can be installed with pip. +requirements: [ "proxmoxer", "requests" ] +''' + +EXAMPLES = ''' +# Create new VM with minimal options +- proxmox_kvm: + api_user : root@pam + api_password: secret + api_host : helldorado + name : spynal + node : sabrewulf + +# Create new VM with minimal options and given vmid +- proxmox_kvm: + api_user : root@pam + api_password: secret + api_host : helldorado + name : spynal + node : sabrewulf + vmid : 100 + +# Create new VM with two network interface options. +- proxmox_kvm: + api_user : root@pam + api_password: secret + api_host : helldorado + name : spynal + node : sabrewulf + net : '{"net0":"virtio,bridge=vmbr1,rate=200", "net1":"e1000,bridge=vmbr2,"}' + +# Create new VM with one network interface, three virto hard disk, 4 cores, and 2 vcpus. +- proxmox_kvm: + api_user : root@pam + api_password: secret + api_host : helldorado + name : spynal + node : sabrewulf + net : '{"net0":"virtio,bridge=vmbr1,rate=200"}' + virtio : '{"virtio0":"VMs_LVM:10", "virtio1":"VMs:2,format=qcow2", "virtio2":"VMs:5,format=raw"}' + cores : 4 + vcpus : 2 + +# Create new VM and lock it for snapashot. +- proxmox_kvm: + api_user : root@pam + api_password: secret + api_host : helldorado + name : spynal + node : sabrewulf + lock : snapshot + +# Create new VM and set protection to disable the remove VM and remove disk operations +- proxmox_kvm: + api_user : root@pam + api_password: secret + api_host : helldorado + name : spynal + node : sabrewulf + protection : yes + +# Start VM +- proxmox_kvm: + api_user : root@pam + api_password: secret + api_host : helldorado + name : spynal + node : sabrewulf + state : started + +# Stop VM +- proxmox_kvm: + api_user : root@pam + api_password: secret + api_host : helldorado + name : spynal + node : sabrewulf + state : stopped + +# Stop VM with force +- proxmox_kvm: + api_user : root@pam + api_password: secret + api_host : helldorado + name : spynal + node : sabrewulf + state : stopped + force : yes + +# Restart VM +- proxmox_kvm: + api_user : root@pam + api_password: secret + api_host : helldorado + name : spynal + node : sabrewulf + state : restarted + +# Remove VM +- proxmox_kvm: + api_user : root@pam + api_password: secret + api_host : helldorado + name : spynal + node : sabrewulf + state : absent + +# Get VM current state +- proxmox_kvm: + api_user : root@pam + api_password: secret + api_host : helldorado + name : spynal + node : sabrewulf + state : current +''' + +RETURN = ''' +devices: + description: The list of devices created or used. + returned: success + type: dict + sample: ' + { + "ide0": "VMS_LVM:vm-115-disk-1", + "ide1": "VMs:115/vm-115-disk-3.raw", + "virtio0": "VMS_LVM:vm-115-disk-2", + "virtio1": "VMs:115/vm-115-disk-1.qcow2", + "virtio2": "VMs:115/vm-115-disk-2.raw" + }' +mac: + description: List of mac address created and net[n] attached. Useful when you want to use provision systems like Foreman via PXE. + returned: success + type: dict + sample: ' + { + "net0": "3E:6E:97:D2:31:9F", + "net1": "B6:A1:FC:EF:78:A4" + }' +vmid: + description: The VM vmid. + returned: success + type: int + sample: 115 +status: + description: + - The current virtual machine status. + - Returned only when C(state=current) + returned: success + type: dict + sample: '{ + "changed": false, + "msg": "VM kropta with vmid = 110 is running", + "status": "running" + }' +''' + +import os +import time + + +try: + from proxmoxer import ProxmoxAPI + HAS_PROXMOXER = True +except ImportError: + HAS_PROXMOXER = False + +VZ_TYPE='qemu' + +def get_nextvmid(proxmox): + try: + vmid = proxmox.cluster.nextid.get() + return vmid + except Exception as e: + module.fail_json(msg="Unable to get next vmid. Failed with exception: %s") + +def get_vmid(proxmox, name): + return [ vm['vmid'] for vm in proxmox.cluster.resources.get(type='vm') if vm['name'] == name ] + +def get_vm(proxmox, vmid): + return [ vm for vm in proxmox.cluster.resources.get(type='vm') if vm['vmid'] == int(vmid) ] + +def node_check(proxmox, node): + return [ True for nd in proxmox.nodes.get() if nd['node'] == node ] + +def get_vminfo(module, proxmox, node, vmid, **kwargs): + global results + results = {} + mac = {} + devices = {} + try: + vm = proxmox.nodes(node).qemu(vmid).config.get() + except Exception as e: + module.fail_json(msg='Getting information for VM with vmid = %s failed with exception: %s' % (vmid, e)) + + # Sanitize kwargs. Remove not defined args and ensure True and False converted to int. + kwargs = dict((k,v) for k, v in kwargs.iteritems() if v is not None) + + # Convert all dict in kwargs to elements. For hostpci[n], ide[n], net[n], numa[n], parallel[n], sata[n], scsi[n], serial[n], virtio[n] + for k in kwargs.keys(): + if isinstance(kwargs[k], dict): + kwargs.update(kwargs[k]) + del kwargs[k] + + # Split information by type + for k, v in kwargs.iteritems(): + if re.match(r'net[0-9]', k) is not None: + interface = k + k = vm[k] + k = re.search('=(.*?),', k).group(1) + mac[interface] = k + if re.match(r'virtio[0-9]', k) is not None or re.match(r'ide[0-9]', k) is not None or re.match(r'scsi[0-9]', k) is not None or re.match(r'sata[0-9]', k) is not None: + device = k + k = vm[k] + k = re.search('(.*?),', k).group(1) + devices[device] = k + + results['mac'] = mac + results['devices'] = devices + results['vmid'] = int(vmid) + +def create_vm(module, proxmox, vmid, node, name, memory, cpu, cores, sockets, timeout, **kwargs): + # Available only in PVE 4 + only_v4 = ['force','protection','skiplock'] + # Default args for vm. Note: -args option is for experts only. It allows you to pass arbitrary arguments to kvm. + vm_args = "-serial unix:/var/run/qemu-server/{}.serial,server,nowait".format(vmid) + + proxmox_node = proxmox.nodes(node) + + # Sanitize kwargs. Remove not defined args and ensure True and False converted to int. + kwargs = dict((k,v) for k, v in kwargs.iteritems() if v is not None) + kwargs.update(dict([k, int(v)] for k, v in kwargs.iteritems() if isinstance(v, bool))) + + # The features work only on PVE 4 + if PVE_MAJOR_VERSION < 4: + for p in only_v4: + if p in kwargs: + del kwargs[p] + + # Convert all dict in kwargs to elements. For hostpci[n], ide[n], net[n], numa[n], parallel[n], sata[n], scsi[n], serial[n], virtio[n] + for k in kwargs.keys(): + if isinstance(kwargs[k], dict): + kwargs.update(kwargs[k]) + del kwargs[k] + + # -args and skiplock require root@pam user + if module.params['api_user'] == "root@pam" and module.params['args'] is None: + kwargs['args'] = vm_args + elif module.params['api_user'] == "root@pam" and module.params['args'] is not None: + kwargs['args'] = module.params['args'] + elif module.params['api_user'] != "root@pam" and module.params['args'] is not None: + module.fail_json(msg='args parameter require root@pam user. ') + + if module.params['api_user'] != "root@pam" and module.params['skiplock'] is not None: + module.fail_json(msg='skiplock parameter require root@pam user. ') + + taskid = getattr(proxmox_node, VZ_TYPE).create(vmid=vmid, name=name, memory=memory, cpu=cpu, cores=cores, sockets=sockets, **kwargs) + + while timeout: + if ( proxmox_node.tasks(taskid).status.get()['status'] == 'stopped' + and proxmox_node.tasks(taskid).status.get()['exitstatus'] == 'OK' ): + return True + timeout = timeout - 1 + if timeout == 0: + module.fail_json(msg='Reached timeout while waiting for creating VM. Last line in task before timeout: %s' + % proxmox_node.tasks(taskid).log.get()[:1]) + time.sleep(1) + return False + +def start_vm(module, proxmox, vm, vmid, timeout): + taskid = getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.start.post() + while timeout: + if ( proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['status'] == 'stopped' + and proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['exitstatus'] == 'OK' ): + return True + timeout = timeout - 1 + if timeout == 0: + module.fail_json(msg='Reached timeout while waiting for starting VM. Last line in task before timeout: %s' + % proxmox.nodes(vm[0]['node']).tasks(taskid).log.get()[:1]) + + time.sleep(1) + return False + +def stop_vm(module, proxmox, vm, vmid, timeout, force): + if force: + taskid = getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.shutdown.post(forceStop=1) + else: + taskid = getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.shutdown.post() + while timeout: + if ( proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['status'] == 'stopped' + and proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['exitstatus'] == 'OK' ): + return True + timeout = timeout - 1 + if timeout == 0: + module.fail_json(msg='Reached timeout while waiting for stopping VM. Last line in task before timeout: %s' + % proxmox.nodes(vm[0]['node']).tasks(taskid).log.get()[:1]) + + time.sleep(1) + return False + +def main(): + module = AnsibleModule( + argument_spec = dict( + acpi = dict(type='bool', default='yes'), + agent = dict(type='bool'), + args = dict(type='str', default=None), + api_host = dict(required=True), + api_user = dict(required=True), + api_password = dict(no_log=True), + autostart = dict(type='bool', default='no'), + balloon = dict(type='int',default=0), + bios = dict(choices=['seabios', 'ovmf']), + boot = dict(type='str', default='cnd'), + bootdisk = dict(type='str'), + cores = dict(type='int', default=1), + cpu = dict(type='str', default='kvm64'), + cpulimit = dict(type='int'), + cpuunits = dict(type='int', default=1000), + delete = dict(type='str'), + description = dict(type='str'), + digest = dict(type='str'), + force = dict(type='bool', default=None), + freeze = dict(type='bool'), + hostpci = dict(type='dict'), + hotplug = dict(type='str'), + hugepages = dict(choices=['any', '2', '1024']), + ide = dict(type='dict', default=None), + keyboard = dict(type='str'), + kvm = dict(type='bool', default='yes'), + localtime = dict(type='bool'), + lock = dict(choices=['migrate', 'backup', 'snapshot', 'rollback']), + machine = dict(type='str'), + memory = dict(type='int', default=512), + migrate_downtime = dict(type='int'), + migrate_speed = dict(type='int'), + name = dict(type='str'), + net = dict(type='dict'), + node = dict(), + numa = dict(type='dict'), + onboot = dict(type='bool', default='yes'), + ostype = dict(default='l26', choices=['other', 'wxp', 'w2k', 'w2k3', 'w2k8', 'wvista', 'win7', 'win8', 'l24', 'l26', 'solaris']), + parallel = dict(type='dict'), + protection = dict(type='bool'), + reboot = dict(type='bool'), + revert = dict(), + sata = dict(type='dict'), + scsi = dict(type='dict'), + scsihw = dict(choices=['lsi', 'lsi53c810', 'virtio-scsi-pci', 'virtio-scsi-single', 'megasas', 'pvscsi']), + serial = dict(type='dict'), + shares = dict(type='int'), + skiplock = dict(type='bool'), + smbios = dict(type='str'), + sockets = dict(type='int', default=1), + startdate = dict(type='str'), + startup = dict(), + state = dict(default='present', choices=['present', 'absent', 'stopped', 'started', 'restarted', 'current']), + tablet = dict(type='bool', default='no'), + tdf = dict(type='bool'), + template = dict(type='bool', default='no'), + timeout = dict(type='int', default=30), + validate_certs = dict(type='bool', default='no'), + vcpus = dict(type='int', default=None), + vga = dict(default='std', choices=['std', 'cirrus', 'vmware', 'qxl', 'serial0', 'serial1', 'serial2', 'serial3', 'qxl2', 'qxl3', 'qxl4']), + virtio = dict(type='dict', default=None), + vmid = dict(type='int', default=None), + watchdog = dict(), + ) + ) + + if not HAS_PROXMOXER: + module.fail_json(msg='proxmoxer required for this module') + + api_user = module.params['api_user'] + api_host = module.params['api_host'] + api_password = module.params['api_password'] + cpu = module.params['cpu'] + cores = module.params['cores'] + memory = module.params['memory'] + name = module.params['name'] + node = module.params['node'] + sockets = module.params['sockets'], + state = module.params['state'] + timeout = module.params['timeout'] + validate_certs = module.params['validate_certs'] + + # If password not set get it from PROXMOX_PASSWORD env + if not api_password: + try: + api_password = os.environ['PROXMOX_PASSWORD'] + except KeyError as e: + module.fail_json(msg='You should set api_password param or use PROXMOX_PASSWORD environment variable') + + try: + proxmox = ProxmoxAPI(api_host, user=api_user, password=api_password, verify_ssl=validate_certs) + global VZ_TYPE + global PVE_MAJOR_VERSION + PVE_MAJOR_VERSION = 3 if float(proxmox.version.get()['version']) < 4.0 else 4 + except Exception as e: + module.fail_json(msg='authorization on proxmox cluster failed with exception: %s' % e) + + + # If vmid not set get the Next VM id from ProxmoxAPI + # If vm name is set get the VM id from ProxmoxAPI + if module.params['vmid'] is not None: + vmid = module.params['vmid'] + elif state == 'present': + vmid = get_nextvmid(proxmox) + elif module.params['name'] is not None: + vmid = get_vmid(proxmox, name)[0] + + if state == 'present': + try: + if get_vm(proxmox, vmid) and not module.params['force']: + module.exit_json(changed=False, msg="VM with vmid <%s> already exists" % vmid) + elif get_vmid(proxmox, name) and not module.params['force']: + module.exit_json(changed=False, msg="VM with name <%s> already exists" % name) + elif not (node, module.params['name']): + module.fail_json(msg='node, name is mandatory for creating vm') + elif not node_check(proxmox, node): + module.fail_json(msg="node '%s' does not exist in cluster" % node) + + create_vm(module, proxmox, vmid, node, name, memory, cpu, cores, sockets, timeout, + acpi = module.params['acpi'], + agent = module.params['agent'], + autostart = module.params['autostart'], + balloon = module.params['balloon'], + bios = module.params['bios'], + boot = module.params['boot'], + bootdisk = module.params['bootdisk'], + cpulimit = module.params['cpulimit'], + cpuunits = module.params['cpuunits'], + delete = module.params['delete'], + description = module.params['description'], + digest = module.params['digest'], + force = module.params['force'], + freeze = module.params['freeze'], + hostpci = module.params['hostpci'], + hotplug = module.params['hotplug'], + hugepages = module.params['hugepages'], + ide = module.params['ide'], + keyboard = module.params['keyboard'], + kvm = module.params['kvm'], + localtime = module.params['localtime'], + lock = module.params['lock'], + machine = module.params['machine'], + migrate_downtime = module.params['migrate_downtime'], + migrate_speed = module.params['migrate_speed'], + net = module.params['net'], + numa = module.params['numa'], + onboot = module.params['onboot'], + ostype = module.params['ostype'], + parallel = module.params['parallel'], + protection = module.params['protection'], + reboot = module.params['reboot'], + revert = module.params['revert'], + sata = module.params['sata'], + scsi = module.params['scsi'], + scsihw = module.params['scsihw'], + serial = module.params['serial'], + shares = module.params['shares'], + skiplock = module.params['skiplock'], + smbios1 = module.params['smbios'], + startdate = module.params['startdate'], + startup = module.params['startup'], + tablet = module.params['tablet'], + tdf = module.params['tdf'], + template = module.params['template'], + vcpus = module.params['vcpus'], + vga = module.params['vga'], + virtio = module.params['virtio'], + watchdog = module.params['watchdog']) + + get_vminfo(module, proxmox, node, vmid, + ide = module.params['ide'], + net = module.params['net'], + sata = module.params['sata'], + scsi = module.params['scsi'], + virtio = module.params['virtio']) + module.exit_json(changed=True, msg="VM %s with vmid %s deployed" % (name, vmid), **results) + except Exception as e: + module.fail_json(msg="creation of %s VM %s with vmid %s failed with exception: %s" % ( VZ_TYPE, name, vmid, e )) + + elif state == 'started': + try: + vm = get_vm(proxmox, vmid) + if not vm: + module.fail_json(msg='VM with vmid <%s> does not exist in cluster' % vmid) + if getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'running': + module.exit_json(changed=False, msg="VM %s is already running" % vmid) + + if start_vm(module, proxmox, vm, vmid, timeout): + module.exit_json(changed=True, msg="VM %s started" % vmid) + except Exception as e: + module.fail_json(msg="starting of VM %s failed with exception: %s" % ( vmid, e )) + + elif state == 'stopped': + try: + vm = get_vm(proxmox, vmid) + if not vm: + module.fail_json(msg='VM with vmid = %s does not exist in cluster' % vmid) + + if getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'stopped': + module.exit_json(changed=False, msg="VM %s is already stopped" % vmid) + + if stop_vm(module, proxmox, vm, vmid, timeout, force = module.params['force']): + module.exit_json(changed=True, msg="VM %s is shutting down" % vmid) + except Exception as e: + module.fail_json(msg="stopping of VM %s failed with exception: %s" % ( vmid, e )) + + elif state == 'restarted': + try: + vm = get_vm(proxmox, vmid) + if not vm: + module.fail_json(msg='VM with vmid = %s does not exist in cluster' % vmid) + if getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'stopped': + module.exit_json(changed=False, msg="VM %s is not running" % vmid) + + if ( stop_vm(module, proxmox, vm, vmid, timeout, force = module.params['force']) and + start_vm(module, proxmox, vm, vmid, timeout) ): + module.exit_json(changed=True, msg="VM %s is restarted" % vmid) + except Exception as e: + module.fail_json(msg="restarting of VM %s failed with exception: %s" % ( vmid, e )) + + elif state == 'absent': + try: + vm = get_vm(proxmox, vmid) + if not vm: + module.exit_json(changed=False, msg="VM %s does not exist" % vmid) + + if getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'running': + module.exit_json(changed=False, msg="VM %s is running. Stop it before deletion." % vmid) + + taskid = getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE).delete(vmid) + while timeout: + if ( proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['status'] == 'stopped' + and proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['exitstatus'] == 'OK' ): + module.exit_json(changed=True, msg="VM %s removed" % vmid) + timeout = timeout - 1 + if timeout == 0: + module.fail_json(msg='Reached timeout while waiting for removing VM. Last line in task before timeout: %s' + % proxmox_node.tasks(taskid).log.get()[:1]) + + time.sleep(1) + except Exception as e: + module.fail_json(msg="deletion of VM %s failed with exception: %s" % ( vmid, e )) + + elif state == 'current': + status = {} + try: + vm = get_vm(proxmox, vmid) + if not vm: + module.fail_json(msg='VM with vmid = %s does not exist in cluster' % vmid) + current = getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.current.get()['status'] + status['status'] = current + if status: + module.exit_json(changed=False, msg="VM %s with vmid = %s is %s" % (name, vmid, current), **status) + except Exception as e: + module.fail_json(msg="Unable to get vm {} with vmid = {} status: ".format(name, vmid) + str(e)) + +# import module snippets +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From 37c9741fc141eaa2b25fcdb7ead39cc2000c6501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Wed, 16 Nov 2016 22:18:06 +0100 Subject: [PATCH 2381/2522] cloudstack: fix state=absent, ip_address not None but falsy (#3483) --- cloud/cloudstack/cs_ip_address.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/cloudstack/cs_ip_address.py b/cloud/cloudstack/cs_ip_address.py index 55eb1e32f6c..62d6ac211a2 100644 --- a/cloud/cloudstack/cs_ip_address.py +++ b/cloud/cloudstack/cs_ip_address.py @@ -208,8 +208,8 @@ def associate_ip_address(self): def disassociate_ip_address(self): ip_address = self.get_ip_address() - if ip_address is None: - return ip_address + if not ip_address: + return None if ip_address['isstaticnat']: self.module.fail_json(msg="IP address is allocated via static nat") From 40a958761cc6134f9b9b3d3477563c4a629fcc63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Wed, 16 Nov 2016 22:20:37 +0100 Subject: [PATCH 2382/2522] cloudtack: cs_instance: doc fix for missing tag alias (#3484) --- cloud/cloudstack/cs_instance.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 49fb21329c3..9903279f9e3 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -204,6 +204,7 @@ - "If you want to delete all tags, set a empty list e.g. C(tags: [])." required: false default: null + aliases: [ 'tag' ] poll_async: description: - Poll async jobs until job has finished. From 84d6426e2850614ac47b5e9460641386da93fd44 Mon Sep 17 00:00:00 2001 From: Maarten Bezemer Date: Wed, 16 Nov 2016 22:27:09 +0100 Subject: [PATCH 2383/2522] mongodb_user: Allow pymongo version 3.2 in combination with mongodb 3.2 (#3474) --- database/misc/mongodb_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index fa3154701bd..1d1157b15a7 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -185,7 +185,7 @@ def check_compatibility(module, client): loose_srv_version = LooseVersion(client.server_info()['version']) loose_driver_version = LooseVersion(PyMongoVersion) - if loose_srv_version >= LooseVersion('3.2') and loose_driver_version <= LooseVersion('3.2'): + if loose_srv_version >= LooseVersion('3.2') and loose_driver_version < LooseVersion('3.2'): module.fail_json(msg=' (Note: you must use pymongo 3.2+ with MongoDB >= 3.2)') elif loose_srv_version >= LooseVersion('3.0') and loose_driver_version <= LooseVersion('2.8'): From 3031764ce0c2ce8f79323ca7043d26854c94355a Mon Sep 17 00:00:00 2001 From: Nijin Ashok Date: Thu, 17 Nov 2016 19:54:50 +0530 Subject: [PATCH 2384/2522] Fix issue in adding RAW disk in block storage domain (#3432) By default, sparse option is true in ovirt. So the raw disk creation in a block storage domain will fail with error "Disk configuration (RAW Sparse) is incompatible with the storage domain type". The commit adds sparse option where it is send as False when format is raw and True when format is qcow2 --- cloud/ovirt/ovirt_disks.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cloud/ovirt/ovirt_disks.py b/cloud/ovirt/ovirt_disks.py index a8f84c26e83..092fb051cf1 100644 --- a/cloud/ovirt/ovirt_disks.py +++ b/cloud/ovirt/ovirt_disks.py @@ -65,7 +65,10 @@ default: 'virtio' format: description: - - "Format of the disk. Either copy-on-write or raw." + - Specify format of the disk. + - If (cow) format is used, disk will by created as sparse, so space will be allocated for the volume as needed, also known as I(thin provision). + - If (raw) format is used, disk storage will be allocated right away, also known as I(preallocated). + - Note that this option isn't idempotent as it's not currently possible to change format of the disk via API. choices: ['raw', 'cow'] storage_domain: description: @@ -168,6 +171,7 @@ def build_entity(self): format=otypes.DiskFormat( self._module.params.get('format') ) if self._module.params.get('format') else None, + sparse=False if self._module.params.get('format') == 'raw' else True, provisioned_size=convert_to_bytes( self._module.params.get('size') ), @@ -198,7 +202,6 @@ def update_check(self, entity): return ( equal(self._module.params.get('description'), entity.description) and equal(convert_to_bytes(self._module.params.get('size')), entity.provisioned_size) and - equal(self._module.params.get('format'), str(entity.format)) and equal(self._module.params.get('shareable'), entity.shareable) ) @@ -234,7 +237,6 @@ def main(): vm_id=dict(default=None), size=dict(default=None), interface=dict(default=None,), - allocation_policy=dict(default=None), storage_domain=dict(default=None), profile=dict(default=None), format=dict(default=None, choices=['raw', 'cow']), From 8c1c499d9d1c87f288122aebf8dfa4320e55bc7b Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Thu, 17 Nov 2016 15:32:05 +0100 Subject: [PATCH 2385/2522] Performance improvement using in-operator on dicts Just a small cleanup for the existing occurrences. Using the in-operator for hash lookups is faster than using .keys() http://stackoverflow.com/questions/29314269/why-do-key-in-dict-and-key-in-dict-keys-have-the-same-output --- cloud/misc/rhevm.py | 16 ++++++++-------- network/haproxy.py | 4 ++-- network/snmp_facts.py | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/cloud/misc/rhevm.py b/cloud/misc/rhevm.py index 68927a9cac6..6606101eb7a 100644 --- a/cloud/misc/rhevm.py +++ b/cloud/misc/rhevm.py @@ -715,22 +715,22 @@ def set_Host(self, host_name, cluster, ifaces): for iface in ifaces: try: setMsg('creating host interface ' + iface['name']) - if 'management' in iface.keys(): + if 'management' in iface: manageip = iface['ip'] - if 'boot_protocol' not in iface.keys(): - if 'ip' in iface.keys(): + if 'boot_protocol' not in iface: + if 'ip' in iface: iface['boot_protocol'] = 'static' else: iface['boot_protocol'] = 'none' - if 'ip' not in iface.keys(): + if 'ip' not in iface: iface['ip'] = '' - if 'netmask' not in iface.keys(): + if 'netmask' not in iface: iface['netmask'] = '' - if 'gateway' not in iface.keys(): + if 'gateway' not in iface: iface['gateway'] = '' - if 'network' in iface.keys(): - if 'bond' in iface.keys(): + if 'network' in iface: + if 'bond' in iface: bond = [] for slave in iface['bond']: bond.append(ifacelist[slave]) diff --git a/network/haproxy.py b/network/haproxy.py index 0d1db0397e7..0ab17549466 100644 --- a/network/haproxy.py +++ b/network/haproxy.py @@ -196,10 +196,10 @@ def capture_command_output(self, cmd, output): """ Capture the output for a command """ - if not 'command' in self.command_results.keys(): + if 'command' not in self.command_results: self.command_results['command'] = [] self.command_results['command'].append(cmd) - if not 'output' in self.command_results.keys(): + if 'output' not in self.command_results: self.command_results['output'] = [] self.command_results['output'].append(output) diff --git a/network/snmp_facts.py b/network/snmp_facts.py index 28546dfc71d..1411b8026a2 100644 --- a/network/snmp_facts.py +++ b/network/snmp_facts.py @@ -153,7 +153,7 @@ def lookup_adminstatus(int_adminstatus): 2: 'down', 3: 'testing' } - if int_adminstatus in adminstatus_options.keys(): + if int_adminstatus in adminstatus_options: return adminstatus_options[int_adminstatus] else: return "" @@ -168,7 +168,7 @@ def lookup_operstatus(int_operstatus): 6: 'notPresent', 7: 'lowerLayerDown' } - if int_operstatus in operstatus_options.keys(): + if int_operstatus in operstatus_options: return operstatus_options[int_operstatus] else: return "" From 4404ab27d007cb45d54d5d4daaf49489d02de7c8 Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Thu, 17 Nov 2016 16:02:09 +0100 Subject: [PATCH 2386/2522] Performance improvement using in-operator for hash lookups Just a small cleanup for the existing occurrences. Using the in-operator for hash lookups is faster than using .has_key() http://stackoverflow.com/questions/1323410/has-key-or-in --- system/crypttab.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/crypttab.py b/system/crypttab.py index d004f8fa4f6..b8834a23c8a 100644 --- a/system/crypttab.py +++ b/system/crypttab.py @@ -312,7 +312,7 @@ def __init__(self, opts_string): def add(self, opts_string): changed = False for k, v in Options(opts_string).items(): - if self.has_key(k): + if k in self: if self[k] != v: changed = True else: @@ -323,7 +323,7 @@ def add(self, opts_string): def remove(self, opts_string): changed = False for k in Options(opts_string): - if self.has_key(k): + if k in self: del self[k] changed = True return changed, 'removed options' @@ -341,7 +341,7 @@ def __iter__(self): return iter(self.itemlist) def __setitem__(self, key, value): - if not self.has_key(key): + if key not in self: self.itemlist.append(key) super(Options, self).__setitem__(key, value) From 431a229069146b92945a04f0170c54077342964e Mon Sep 17 00:00:00 2001 From: Matthew Krupcale Date: Thu, 17 Nov 2016 13:15:11 -0500 Subject: [PATCH 2387/2522] gluster_volume: Fixes gluster peer probe / volume creation issue. (#3486) * gluster_volume: Fixes issue when creating a new volume failing due to peers not being present. The peers which are not 'localhost' should invoke wait_for_peer, but the find method returns -1 (not 0) on non-localhost peers. --- system/gluster_volume.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/gluster_volume.py b/system/gluster_volume.py index 96174433de6..44ff2780c86 100644 --- a/system/gluster_volume.py +++ b/system/gluster_volume.py @@ -274,7 +274,7 @@ def wait_for_peer(host): def probe(host, myhostname): global module out = run_gluster([ 'peer', 'probe', host ]) - if not out.find('localhost') and not wait_for_peer(host): + if out.find('localhost') == -1 and not wait_for_peer(host): module.fail_json(msg='failed to probe peer %s on %s' % (host, myhostname)) changed = True From 432d1ca519d0ccfd8053bf19e2e447ae6504d094 Mon Sep 17 00:00:00 2001 From: Jens Carl Date: Thu, 17 Nov 2016 10:18:23 -0800 Subject: [PATCH 2388/2522] redshift: Fix error with boolean parameters (#3467) --- cloud/amazon/redshift.py | 38 ++++++++------------------------------ 1 file changed, 8 insertions(+), 30 deletions(-) diff --git a/cloud/amazon/redshift.py b/cloud/amazon/redshift.py index b67020b196c..a4190ff9e94 100644 --- a/cloud/amazon/redshift.py +++ b/cloud/amazon/redshift.py @@ -37,115 +37,94 @@ node_type: description: - The node type of the cluster. Must be specified when command=create. - required: false choices: ['ds1.xlarge', 'ds1.8xlarge', 'ds2.xlarge', 'ds2.8xlarge', 'dc1.large', 'dc1.8xlarge', 'dw1.xlarge', 'dw1.8xlarge', 'dw2.large', 'dw2.8xlarge'] username: description: - Master database username. Used only when command=create. - required: false password: description: - Master database password. Used only when command=create. - required: false cluster_type: description: - The type of cluster. - required: false choices: ['multi-node', 'single-node' ] default: 'single-node' db_name: description: - Name of the database. - required: false default: null availability_zone: description: - availability zone in which to launch cluster - required: false aliases: ['zone', 'aws_zone'] number_of_nodes: description: - Number of nodes. Only used when cluster_type=multi-node. - required: false default: null cluster_subnet_group_name: description: - which subnet to place the cluster - required: false aliases: ['subnet'] cluster_security_groups: description: - in which security group the cluster belongs - required: false default: null aliases: ['security_groups'] vpc_security_group_ids: description: - VPC security group - required: false aliases: ['vpc_security_groups'] default: null preferred_maintenance_window: description: - maintenance window - required: false aliases: ['maintance_window', 'maint_window'] default: null cluster_parameter_group_name: description: - name of the cluster parameter group - required: false aliases: ['param_group_name'] default: null automated_snapshot_retention_period: description: - period when the snapshot take place - required: false aliases: ['retention_period'] default: null port: description: - which port the cluster is listining - required: false default: null cluster_version: description: - which version the cluster should have - required: false aliases: ['version'] choices: ['1.0'] default: null allow_version_upgrade: description: - flag to determinate if upgrade of version is possible - required: false aliases: ['version_upgrade'] - default: null + default: true publicly_accessible: description: - if the cluster is accessible publicly or not - required: false - default: null + default: false encrypted: description: - if the cluster is encrypted or not - required: false - default: null + default: false elastic_ip: description: - if the cluster has an elastic IP or not - required: false default: null new_cluster_identifier: description: - Only used when command=modify. - required: false aliases: ['new_identifier'] default: null wait: description: - When command=create, modify or restore then wait for the database to enter the 'available' state. When command=delete wait for the database to be terminated. - required: false default: "no" choices: [ "yes", "no" ] wait_timeout: @@ -282,7 +261,7 @@ def create_cluster(module, redshift): 'cluster_version', 'allow_version_upgrade', 'number_of_nodes', 'publicly_accessible', 'encrypted', 'elastic_ip'): - if module.params.get( p ): + if p in module.params: params[ p ] = module.params.get( p ) try: @@ -389,12 +368,11 @@ def modify_cluster(module, redshift): 'cluster_parameter_group_name', 'automated_snapshot_retention_period', 'port', 'cluster_version', 'allow_version_upgrade', 'number_of_nodes', 'new_cluster_identifier'): - if module.params.get(p): + if p in module.params: params[p] = module.params.get(p) try: redshift.describe_clusters(identifier)['DescribeClustersResponse']['DescribeClustersResult']['Clusters'][0] - changed = False except boto.exception.JSONResponseError as e: try: redshift.modify_cluster(identifier, **params) @@ -444,10 +422,10 @@ def main(): automated_snapshot_retention_period = dict(aliases=['retention_period']), port = dict(type='int'), cluster_version = dict(aliases=['version'], choices=['1.0']), - allow_version_upgrade = dict(aliases=['version_upgrade'], type='bool'), + allow_version_upgrade = dict(aliases=['version_upgrade'], type='bool', default=True), number_of_nodes = dict(type='int'), - publicly_accessible = dict(type='bool'), - encrypted = dict(type='bool'), + publicly_accessible = dict(type='bool', default=False), + encrypted = dict(type='bool', default=False), elastic_ip = dict(required=False), new_cluster_identifier = dict(aliases=['new_identifier']), wait = dict(type='bool', default=False), From 7c77b3334dda23d8d81a3bc60dfd42725ef750eb Mon Sep 17 00:00:00 2001 From: Jiri Tyr Date: Thu, 17 Nov 2016 18:20:31 +0000 Subject: [PATCH 2389/2522] yum_repository: Add diff support (#3460) --- packaging/os/yum_repository.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packaging/os/yum_repository.py b/packaging/os/yum_repository.py index dfdd665ed2f..2f333ad1a7c 100644 --- a/packaging/os/yum_repository.py +++ b/packaging/os/yum_repository.py @@ -21,6 +21,7 @@ import ConfigParser import os +from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.pycompat24 import get_exception @@ -273,8 +274,8 @@ required: false default: null description: - - URL to the proxy server that yum should use. Set to C(_none_) to disable - the global proxy setting. + - URL to the proxy server that yum should use. Set to C(_none_) to + disable the global proxy setting. proxy_password: required: false default: null @@ -719,7 +720,12 @@ def main(): yumrepo = YumRepo(module) # Get repo status before change - yumrepo_before = yumrepo.dump() + diff = { + 'before_header': yumrepo.params['dest'], + 'before': yumrepo.dump(), + 'after_header': yumrepo.params['dest'], + 'after': '' + } # Perform action depending on the state if state == 'present': @@ -728,10 +734,10 @@ def main(): yumrepo.remove() # Get repo status after change - yumrepo_after = yumrepo.dump() + diff['after'] = yumrepo.dump() # Compare repo states - changed = yumrepo_before != yumrepo_after + changed = diff['before'] != diff['after'] # Save the file only if not in check mode and if there was a change if not module.check_mode and changed: @@ -743,11 +749,7 @@ def main(): changed = module.set_fs_attributes_if_different(file_args, changed) # Print status of the change - module.exit_json(changed=changed, repo=name, state=state) - - -# Import module snippets -from ansible.module_utils.basic import * + module.exit_json(changed=changed, repo=name, state=state, diff=diff) if __name__ == '__main__': From 22a6449150ef451f14028e75e1ffc09d2a989a51 Mon Sep 17 00:00:00 2001 From: Marius Gedminas Date: Thu, 17 Nov 2016 20:24:37 +0200 Subject: [PATCH 2390/2522] letsencrypt: Locale-independent date parsing (#3314) Should fix #3155. --- web_infrastructure/letsencrypt.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web_infrastructure/letsencrypt.py b/web_infrastructure/letsencrypt.py index 939c51c629f..751997af2b8 100644 --- a/web_infrastructure/letsencrypt.py +++ b/web_infrastructure/letsencrypt.py @@ -20,6 +20,7 @@ import binascii import copy +import locale import textwrap from datetime import datetime @@ -769,6 +770,9 @@ def main(): ), supports_check_mode = True, ) + + # AnsibleModule() changes the locale, so change it back to C because we rely on time.strptime() when parsing certificate dates. + locale.setlocale(locale.LC_ALL, "C") cert_days = get_cert_days(module,module.params['dest']) if cert_days < module.params['remaining_days']: From 94ef04befedae18514c401740dac3981f2e098ec Mon Sep 17 00:00:00 2001 From: Koki Nomura Date: Fri, 18 Nov 2016 03:28:09 +0900 Subject: [PATCH 2391/2522] blockinfile: Fixes #1926 by comparing a marker to a whole line instead of a line prefix (#3339) --- files/blockinfile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/files/blockinfile.py b/files/blockinfile.py index 96f430cf14a..ecee4800117 100755 --- a/files/blockinfile.py +++ b/files/blockinfile.py @@ -258,9 +258,9 @@ def main(): n0 = n1 = None for i, line in enumerate(lines): - if line.startswith(marker0): + if line == marker0: n0 = i - if line.startswith(marker1): + if line == marker1: n1 = i if None in (n0, n1): From 39afa37a78a39304d6050b42e79eea4c3661a8d8 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Thu, 17 Nov 2016 20:19:21 +0100 Subject: [PATCH 2392/2522] redshift: fix version added --- cloud/amazon/redshift.py | 2 +- cloud/amazon/redshift_subnet_group.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/redshift.py b/cloud/amazon/redshift.py index a4190ff9e94..bd6911ecc50 100644 --- a/cloud/amazon/redshift.py +++ b/cloud/amazon/redshift.py @@ -20,7 +20,7 @@ author: - "Jens Carl (@j-carl), Hothead Games Inc." module: redshift -version_added: "2.1" +version_added: "2.2" short_description: create, delete, or modify an Amazon Redshift instance description: - Creates, deletes, or modifies amazon Redshift cluster instances. diff --git a/cloud/amazon/redshift_subnet_group.py b/cloud/amazon/redshift_subnet_group.py index 750c8dd94c7..113d57988c6 100644 --- a/cloud/amazon/redshift_subnet_group.py +++ b/cloud/amazon/redshift_subnet_group.py @@ -20,7 +20,7 @@ author: - "Jens Carl (@j-carl), Hothead Games Inc." module: redshift_subnet_group -version_added: "2.1" +version_added: "2.2" short_description: mange Redshift cluster subnet groups description: - Create, modifies, and deletes Redshift cluster subnet groups. From 815b144c1df6229ca3eb36eafaa1a8041a50d3b2 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Wed, 9 Nov 2016 14:43:31 +0100 Subject: [PATCH 2393/2522] Make the 'make' module run on python 3 Traceback: Traceback (most recent call last): File \"/tmp/ansible_d28_6uwl/ansible_module_make.py\", line 153, in main() File \"/tmp/ansible_d28_6uwl/ansible_module_make.py\", line 119, in main rc, out, err = run_command(base_command + ['--question'], module, check_rc=False) File \"/tmp/ansible_d28_6uwl/ansible_module_make.py\", line 79, in run_command return rc, sanitize_output(out), sanitize_output(err) File \"/tmp/ansible_d28_6uwl/ansible_module_make.py\", line 95, in sanitize_output return output.rstrip(b(\"\\r\\n\")) TypeError: rstrip arg must be None or str There is also a six.iteritems issue, fixed using six. --- system/make.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/system/make.py b/system/make.py index 46b0c453902..497b21328ba 100644 --- a/system/make.py +++ b/system/make.py @@ -65,6 +65,9 @@ # fix this RETURN = '''# ''' +from ansible.module_utils.six import iteritems +from ansible.module_utils.basic import AnsibleModule + def run_command(command, module, check_rc=True): """ @@ -90,9 +93,9 @@ def sanitize_output(output): :return: sanitized output """ if output is None: - return b('') + return '' else: - return output.rstrip(b("\r\n")) + return output.rstrip("\r\n") def main(): @@ -108,7 +111,7 @@ def main(): make_path = module.get_bin_path('make', True) make_target = module.params['target'] if module.params['params'] is not None: - make_parameters = [k + '=' + str(v) for k, v in module.params['params'].iteritems()] + make_parameters = [k + '=' + str(v) for k, v in iteritems(module.params['params'])] else: make_parameters = [] @@ -147,7 +150,5 @@ def main(): ) -from ansible.module_utils.basic import * - if __name__ == '__main__': main() From d3c804c758d3f6ac07bfdde99be1097eca93ee50 Mon Sep 17 00:00:00 2001 From: Andy Dirnberger Date: Sat, 12 Nov 2016 18:01:05 -0500 Subject: [PATCH 2394/2522] Make Homebrew-related modules run on Python 3 Both the `homebrew` and `homebrew_cask` modules iterate over dictionaries using `iteritems`. This is a Python 2-specific method whose behavior is similar to `items` in Python 3+. The `iteritems` function in the six library was designed to make it possible to use the correct method. --- packaging/os/homebrew.py | 4 +++- packaging/os/homebrew_cask.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packaging/os/homebrew.py b/packaging/os/homebrew.py index fa61984e0ff..482e1b92cdb 100755 --- a/packaging/os/homebrew.py +++ b/packaging/os/homebrew.py @@ -101,6 +101,8 @@ import os.path import re +from ansible.module_utils.six import iteritems + # exceptions -------------------------------------------------------------- {{{ class HomebrewException(Exception): @@ -348,7 +350,7 @@ def _setup_status_vars(self): self.message = '' def _setup_instance_vars(self, **kwargs): - for key, val in kwargs.iteritems(): + for key, val in iteritems(kwargs): setattr(self, key, val) def _prep(self): diff --git a/packaging/os/homebrew_cask.py b/packaging/os/homebrew_cask.py index debcb788ea4..2d1722398eb 100755 --- a/packaging/os/homebrew_cask.py +++ b/packaging/os/homebrew_cask.py @@ -75,6 +75,8 @@ import os.path import re +from ansible.module_utils.six import iteritems + # exceptions -------------------------------------------------------------- {{{ class HomebrewCaskException(Exception): @@ -309,7 +311,7 @@ def _setup_status_vars(self): self.message = '' def _setup_instance_vars(self, **kwargs): - for key, val in kwargs.iteritems(): + for key, val in iteritems(kwargs): setattr(self, key, val) def _prep(self): From 8d879e7727bf797a855332bebaff9bb3098b22a2 Mon Sep 17 00:00:00 2001 From: Berislav Lopac Date: Mon, 14 Nov 2016 10:24:07 +0000 Subject: [PATCH 2395/2522] replace iteritems with items to ensure python3 compatibility --- system/locale_gen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/locale_gen.py b/system/locale_gen.py index 9aa732f57c3..c8c5128d21e 100644 --- a/system/locale_gen.py +++ b/system/locale_gen.py @@ -98,7 +98,7 @@ def is_present(name): def fix_case(name): """locale -a might return the encoding in either lower or upper case. Passing through this function makes them uniform for comparisons.""" - for s, r in LOCALE_NORMALIZATION.iteritems(): + for s, r in LOCALE_NORMALIZATION.items(): name = name.replace(s, r) return name From c884bf060618f77cc96c3a902bc6a20e171a0a3e Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Fri, 18 Nov 2016 15:15:32 +0100 Subject: [PATCH 2396/2522] Add oVirt ovirt_host_pm module (#3253) --- cloud/ovirt/ovirt_host_pm.py | 232 +++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 cloud/ovirt/ovirt_host_pm.py diff --git a/cloud/ovirt/ovirt_host_pm.py b/cloud/ovirt/ovirt_host_pm.py new file mode 100644 index 00000000000..17bf5ee9371 --- /dev/null +++ b/cloud/ovirt/ovirt_host_pm.py @@ -0,0 +1,232 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +try: + import ovirtsdk4 as sdk + import ovirtsdk4.types as otypes +except ImportError: + pass + +from ansible.module_utils.ovirt import * + + +DOCUMENTATION = ''' +--- +module: ovirt_host_pm +short_description: Module to manage power management of hosts in oVirt +version_added: "2.3" +author: "Ondra Machacek (@machacekondra)" +description: + - "Module to manage power management of hosts in oVirt." +options: + name: + description: + - "Name of the the host to manage." + required: true + aliases: ['host'] + state: + description: + - "Should the host be present/absent." + choices: ['present', 'absent'] + default: present + address: + description: + - "Address of the power management interface." + username: + description: + - "Username to be used to connect to power management interface." + password: + description: + - "Password of the user specified in C(username) parameter." + type: + description: + - "Type of the power management. oVirt predefined values are I(drac5), I(ipmilan), I(rsa), + I(bladecenter), I(alom), I(apc), I(apc_snmp), I(eps), I(wti), I(rsb), I(cisco_ucs), + I(drac7), I(hpblade), I(ilo), I(ilo2), I(ilo3), I(ilo4), I(ilo_ssh), + but user can have defined custom type." + port: + description: + - "Power management interface port." + slot: + description: + - "Power management slot." + options: + description: + - "Dictionary of additional fence agent options." + - "Additional information about options can be found at U(https://fedorahosted.org/cluster/wiki/FenceArguments)." + encrypt_options: + description: + - "If (true) options will be encrypted when send to agent." + aliases: ['encrypt'] + order: + description: + - "Integer value specifying, by default it's added at the end." +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Add fence agent to host 'myhost' +- ovirt_host_pm: + name: myhost + address: 1.2.3.4 + options: + myoption1: x + myoption2: y + username: admin + password: admin + port: 3333 + type: ipmilan + +# Remove ipmilan fence agent with address 1.2.3.4 on host 'myhost' +- ovirt_host_pm: + state: absent + name: myhost + address: 1.2.3.4 + type: ipmilan +''' + +RETURN = ''' +id: + description: ID of the agent which is managed + returned: On success if agent is found. + type: str + sample: 7de90f31-222c-436c-a1ca-7e655bd5b60c +agent: + description: "Dictionary of all the agent attributes. Agent attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/agent." + returned: On success if agent is found. +''' + + +class HostModule(BaseModule): + def build_entity(self): + return otypes.Host( + power_management=otypes.PowerManagement( + enabled=True, + ), + ) + + def update_check(self, entity): + return equal(True, entity.power_management.enabled) + + +class HostPmModule(BaseModule): + + def build_entity(self): + return otypes.Agent( + address=self._module.params['address'], + encrypt_options=self._module.params['encrypt_options'], + options=[ + otypes.Option( + name=name, + value=value, + ) for name, value in self._module.params['options'].items() + ] if self._module.params['options'] else None, + password=self._module.params['password'], + port=self._module.params['port'], + type=self._module.params['type'], + username=self._module.params['username'], + order=self._module.params.get('order', 100), + ) + + def update_check(self, entity): + return ( + equal(self._module.params.get('address'), entity.address) and + equal(self._module.params.get('encrypt_options'), entity.encrypt_options) and + equal(self._module.params.get('password'), entity.password) and + equal(self._module.params.get('username'), entity.username) and + equal(self._module.params.get('port'), entity.port) and + equal(self._module.params.get('type'), entity.type) + ) + + +def main(): + argument_spec = ovirt_full_argument_spec( + state=dict( + choices=['present', 'absent'], + default='present', + ), + name=dict(default=None, required=True, aliases=['host']), + address=dict(default=None), + username=dict(default=None), + password=dict(default=None), + type=dict(default=None), + port=dict(default=None, type='int'), + slot=dict(default=None), + options=dict(default=None, type='dict'), + encrypt_options=dict(default=None, type='bool', aliases=['encrypt']), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + check_sdk(module) + + try: + connection = create_connection(module.params.pop('auth')) + hosts_service = connection.system_service().hosts_service() + host = search_by_name(hosts_service, module.params['name']) + fence_agents_service = hosts_service.host_service(host.id).fence_agents_service() + + host_pm_module = HostPmModule( + connection=connection, + module=module, + service=fence_agents_service, + ) + host_module = HostModule( + connection=connection, + module=module, + service=hosts_service, + ) + + state = module.params['state'] + if state == 'present': + agent = host_pm_module.search_entity( + search_params={ + 'address': module.params['address'], + 'type': module.params['type'], + } + ) + ret = host_pm_module.create(entity=agent) + + # Enable Power Management, if it's not enabled: + host_module.create(entity=host) + elif state == 'absent': + agent = host_pm_module.search_entity( + search_params={ + 'address': module.params['address'], + 'type': module.params['type'], + } + ) + ret = host_pm_module.remove(entity=agent) + + module.exit_json(**ret) + except Exception as e: + module.fail_json(msg=str(e)) + finally: + connection.close(logout=False) + +from ansible.module_utils.basic import * +if __name__ == "__main__": + main() From 89248c082ec0b70b732674b64f3e81a6edb687b0 Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Fri, 18 Nov 2016 15:16:47 +0100 Subject: [PATCH 2397/2522] Add oVirt ovirt_host_networks modules (#3227) --- cloud/ovirt/ovirt_host_networks.py | 364 +++++++++++++++++++++++++++++ 1 file changed, 364 insertions(+) create mode 100644 cloud/ovirt/ovirt_host_networks.py diff --git a/cloud/ovirt/ovirt_host_networks.py b/cloud/ovirt/ovirt_host_networks.py new file mode 100644 index 00000000000..47404dcebe8 --- /dev/null +++ b/cloud/ovirt/ovirt_host_networks.py @@ -0,0 +1,364 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +try: + import ovirtsdk4 as sdk + import ovirtsdk4.types as otypes +except ImportError: + pass + +from ansible.module_utils.ovirt import * + + +DOCUMENTATION = ''' +--- +module: ovirt_host_networks +short_description: Module to manage host networks in oVirt +version_added: "2.3" +author: "Ondra Machacek (@machacekondra)" +description: + - "Module to manage host networks in oVirt." +options: + name: + description: + - "Name of the the host to manage networks for." + required: true + state: + description: + - "Should the host be present/absent." + choices: ['present', 'absent'] + default: present + bond: + description: + - "Dictionary describing network bond:" + - "C(name) - Bond name." + - "C(mode) - Bonding mode." + - "C(interfaces) - List of interfaces to create a bond." + interface: + description: + - "Name of the network interface where logical network should be attached." + networks: + description: + - "List of dictionary describing networks to be attached to interface or bond:" + - "C(name) - Name of the logical network to be assigned to bond or interface." + - "C(boot_protocol) - Boot protocol one of the I(none), I(static) or I(dhcp)." + - "C(address) - IP address in case of I(static) boot protocol is used." + - "C(prefix) - Routing prefix in case of I(static) boot protocol is used." + - "C(gateway) - Gateway in case of I(static) boot protocol is used." + - "C(version) - IP version. Either v4 or v6." + labels: + description: + - "List of names of the network label to be assigned to bond or interface." + check: + description: + - "If I(true) verify connectivity between host and engine." + - "Network configuration changes will be rolled back if connectivity between + engine and the host is lost after changing network configuration." + save: + description: + - "If I(true) network configuration will be persistent, by default they are temporary." +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Create bond on eth0 and eth1 interface, and put 'myvlan' network on top of it: +- name: Bonds + ovirt_host_networks: + name: myhost + bond: + name: bond0 + mode: 2 + interfaces: + - eth1 + - eth2 + networks: + - name: myvlan + boot_protocol: static + address: 1.2.3.4 + prefix: 24 + gateway: 1.2.3.4 + version: v4 + +# Remove bond0 bond from host interfaces: +- ovirt_host_networks: + state: absent + name: myhost + bond: + name: bond0 + +# Assign myvlan1 and myvlan2 vlans to host eth0 interface: +- ovirt_host_networks: + name: myhost + interface: eth0 + networks: + - name: myvlan1 + - name: myvlan2 + +# Remove myvlan2 vlan from host eth0 interface: +- ovirt_host_networks: + state: absent + name: myhost + interface: eth0 + networks: + - name: myvlan2 + +# Remove all networks/vlans from host eth0 interface: +- ovirt_host_networks: + state: absent + name: myhost + interface: eth0 +''' + +RETURN = ''' +id: + description: ID of the host NIC which is managed + returned: On success if host NIC is found. + type: str + sample: 7de90f31-222c-436c-a1ca-7e655bd5b60c +host_nic: + description: "Dictionary of all the host NIC attributes. Host NIC attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/host_nic." + returned: On success if host NIC is found. +''' + + +class HostNetworksModule(BaseModule): + + def build_entity(self): + return otypes.Host() + + def update_address(self, attachment, network): + # Check if there is any change in address assignenmts and + # update it if needed: + for ip in attachment.ip_address_assignments: + if str(ip.ip.version) == network.get('version'): + changed = False + if not equal(network.get('boot_protocol'), str(ip.assignment_method)): + ip.assignment_method = otypes.BootProtocol(network.get('boot_protocol')) + changed = True + if not equal(network.get('address'), ip.ip.address): + ip.ip.address = network.get('address') + changed = True + if not equal(network.get('gateway'), ip.ip.gateway): + ip.ip.gateway = network.get('gateway') + changed = True + if not equal(network.get('prefix'), int(ip.ip.netmask)): + ip.ip.netmask = str(network.get('prefix')) + changed = True + + if changed: + attachments_service.service(attachment.id).update(attachment) + self.changed = True + break + + def has_update(self, nic_service): + update = False + bond = self._module.params['bond'] + networks = self._module.params['networks'] + nic = nic_service.get() + + if nic is None: + return update + + # Check if bond configuration should be updated: + if bond: + update = not ( + equal(str(bond.get('mode')), nic.bonding.options[0].value) and + equal( + sorted(bond.get('interfaces')) if bond.get('interfaces') else None, + sorted(get_link_name(self._connection, s) for s in nic.bonding.slaves) + ) + ) + + if not networks: + return update + + # Check if networks attachments configuration should be updated: + attachments_service = nic_service.network_attachments_service() + network_names = [network.get('name') for network in networks] + + attachments = {} + for attachment in attachments_service.list(): + name = get_link_name(self._connection, attachment.network) + if name in network_names: + attachments[name] = attachment + + for network in networks: + attachment = attachments.get(network.get('name')) + # If attachment don't exsits, we need to create it: + if attachment is None: + return True + + self.update_address(attachment, network) + + return update + + def _action_save_configuration(self, entity): + if self._module.params['save']: + if not self._module.check_mode: + self._service.service(entity.id).commit_net_config() + self.changed = True + + +def main(): + argument_spec = ovirt_full_argument_spec( + state=dict( + choices=['present', 'absent'], + default='present', + ), + name=dict(default=None, aliases=['host'], required=True), + bond=dict(default=None, type='dict'), + interface=dict(default=None), + networks=dict(default=None, type='list'), + labels=dict(default=None, type='list'), + check=dict(default=None, type='bool'), + save=dict(default=None, type='bool'), + ) + module = AnsibleModule(argument_spec=argument_spec) + check_sdk(module) + + try: + connection = create_connection(module.params.pop('auth')) + hosts_service = connection.system_service().hosts_service() + host_networks_module = HostNetworksModule( + connection=connection, + module=module, + service=hosts_service, + ) + + host = host_networks_module.search_entity() + if host is None: + raise Exception("Host '%s' was not found." % module.params['name']) + + bond = module.params['bond'] + interface = module.params['interface'] + networks = module.params['networks'] + labels = module.params['labels'] + nic_name = bond.get('name') if bond else module.params['interface'] + + nics_service = hosts_service.host_service(host.id).nics_service() + nic = search_by_name(nics_service, nic_name) + + state = module.params['state'] + if ( + state == 'present' and + (nic is None or host_networks_module.has_update(nics_service.service(nic.id))) + ): + host_networks_module.action( + entity=host, + action='setup_networks', + post_action=host_networks_module._action_save_configuration, + check_connectivity=module.params['check'], + modified_bonds=[ + otypes.HostNic( + name=bond.get('name'), + bonding=otypes.Bonding( + options=[ + otypes.Option( + name="mode", + value=str(bond.get('mode')), + ) + ], + slaves=[ + otypes.HostNic(name=i) for i in bond.get('interfaces', []) + ], + ), + ), + ] if bond else None, + modified_labels=[ + otypes.NetworkLabel( + name=str(name), + host_nic=otypes.HostNic( + name=bond.get('name') if bond else interface + ), + ) for name in labels + ] if labels else None, + modified_network_attachments=[ + otypes.NetworkAttachment( + network=otypes.Network( + name=network['name'] + ) if network['name'] else None, + host_nic=otypes.HostNic( + name=bond.get('name') if bond else interface + ), + ip_address_assignments=[ + otypes.IpAddressAssignment( + assignment_method=otypes.BootProtocol( + network.get('boot_protocol', 'none') + ), + ip=otypes.Ip( + address=network.get('address'), + gateway=network.get('gateway'), + netmask=network.get('netmask'), + version=otypes.IpVersion( + network.get('version') + ) if network.get('version') else None, + ), + ), + ], + ) for network in networks + ] if networks else None, + ) + elif state == 'absent' and nic: + attachments_service = nics_service.nic_service(nic.id).network_attachments_service() + attachments = attachments_service.list() + if networks: + network_names = [network['name'] for network in networks] + attachments = [ + attachment for attachment in attachments + if get_link_name(connection, attachment.network) in network_names + ] + if labels or bond or attachments: + host_networks_module.action( + entity=host, + action='setup_networks', + post_action=host_networks_module._action_save_configuration, + check_connectivity=module.params['check'], + removed_bonds=[ + otypes.HostNic( + name=bond.get('name'), + ), + ] if bond else None, + removed_labels=[ + otypes.NetworkLabel( + name=str(name), + ) for name in labels + ] if labels else None, + removed_network_attachments=list(attachments), + ) + + nic = search_by_name(nics_service, nic_name) + module.exit_json(**{ + 'changed': host_networks_module.changed, + 'id': nic.id if nic else None, + 'host_nic': get_dict_of_struct(nic), + }) + except Exception as e: + module.fail_json(msg=str(e)) + finally: + connection.close(logout=False) + +from ansible.module_utils.basic import * +if __name__ == "__main__": + main() From 9c275b8826f77d4f06b5b4be2324227f0c03c5ed Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Fri, 18 Nov 2016 15:23:11 +0100 Subject: [PATCH 2398/2522] Add oVirt ovirt_hosts and ovirt_storage_hosts_facts modules (#3225) --- cloud/ovirt/ovirt_hosts.py | 322 +++++++++++++++++++++++++++++++ cloud/ovirt/ovirt_hosts_facts.py | 95 +++++++++ 2 files changed, 417 insertions(+) create mode 100644 cloud/ovirt/ovirt_hosts.py create mode 100644 cloud/ovirt/ovirt_hosts_facts.py diff --git a/cloud/ovirt/ovirt_hosts.py b/cloud/ovirt/ovirt_hosts.py new file mode 100644 index 00000000000..1a11aefdb96 --- /dev/null +++ b/cloud/ovirt/ovirt_hosts.py @@ -0,0 +1,322 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +try: + import ovirtsdk4 as sdk + import ovirtsdk4.types as otypes + + from ovirtsdk4.types import HostStatus as hoststate +except ImportError: + pass + +from ansible.module_utils.ovirt import * + + +DOCUMENTATION = ''' +--- +module: ovirt_hosts +short_description: Module to manage hosts in oVirt +version_added: "2.3" +author: "Ondra Machacek (@machacekondra)" +description: + - "Module to manage hosts in oVirt" +options: + name: + description: + - "Name of the the host to manage." + required: true + state: + description: + - "State which should a host to be in after successful completion." + choices: ['present', 'absent', 'maintenance', 'upgraded', 'started', 'restarted', 'stopped'] + default: present + comment: + description: + - "Description of the host." + cluster: + description: + - "Name of the cluster, where host should be created." + address: + description: + - "Host address. It can be either FQDN (preferred) or IP address." + password: + description: + - "Password of the root. It's required in case C(public_key) is set to I(False)." + public_key: + description: + - "I(True) if the public key should be used to authenticate to host." + - "It's required in case C(password) is not set." + default: False + aliases: ['ssh_public_key'] + kdump_integration: + description: + - "Specify if host will have enabled Kdump integration." + choices: ['enabled', 'disabled'] + default: enabled + spm_priority: + description: + - "SPM priority of the host. Integer value from 1 to 10, where higher number means higher priority." + override_iptables: + description: + - "If True host iptables will be overridden by host deploy script." + force: + description: + - "If True host will be forcibly moved to desired state." + default: False +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Add host with username/password +- ovirt_hosts: + cluster: Default + name: myhost + address: 10.34.61.145 + password: secret + +# Add host using public key +- ovirt_hosts: + public_key: true + cluster: Default + name: myhost2 + address: 10.34.61.145 + +# Maintenance +- ovirt_hosts: + state: maintenance + name: myhost + +# Restart host using power management: +- ovirt_hosts: + state: restarted + name: myhost + +# Upgrade host +- ovirt_hosts: + state: upgraded + name: myhost + +# Remove host +- ovirt_hosts: + state: absent + name: myhost + force: True +''' + +RETURN = ''' +id: + description: ID of the host which is managed + returned: On success if host is found. + type: str + sample: 7de90f31-222c-436c-a1ca-7e655bd5b60c +host: + description: "Dictionary of all the host attributes. Host attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/host." + returned: On success if host is found. +''' + + +class HostsModule(BaseModule): + + def build_entity(self): + return otypes.Host( + name=self._module.params['name'], + cluster=otypes.Cluster( + name=self._module.params['cluster'] + ) if self._module.params['cluster'] else None, + comment=self._module.params['comment'], + address=self._module.params['address'], + root_password=self._module.params['password'], + ssh=otypes.Ssh( + authentication_method='publickey', + ) if self._module.params['public_key'] else None, + kdump_status=otypes.KdumpStatus( + self._module.params['kdump_integration'] + ) if self._module.params['kdump_integration'] else None, + spm=otypes.Spm( + priority=self._module.params['spm_priority'], + ) if self._module.params['spm_priority'] else None, + override_iptables=self._module.params['override_iptables'], + ) + + def update_check(self, entity): + return ( + equal(self._module.params.get('comment'), entity.comment) and + equal(self._module.params.get('kdump_integration'), entity.kdump_status) and + equal(self._module.params.get('spm_priority'), entity.spm.priority) + ) + + def pre_remove(self, entity): + self.action( + entity=entity, + action='deactivate', + action_condition=lambda h: h.status != hoststate.MAINTENANCE, + wait_condition=lambda h: h.status == hoststate.MAINTENANCE, + ) + + def post_update(self, entity): + if entity.status != hoststate.UP: + if not self._module.check_mode: + self._service.host_service(entity.id).activate() + self.changed = True + + +def failed_state(host): + return host.status in [ + hoststate.ERROR, + hoststate.INSTALL_FAILED, + hoststate.NON_RESPONSIVE, + hoststate.NON_OPERATIONAL, + ] + + +def control_state(host_module): + host = host_module.search_entity() + if host is None: + return + + state = host_module._module.params['state'] + host_service = host_module._service.service(host.id) + if failed_state(host): + raise Exception("Not possible to manage host '%s'." % host.name) + elif host.status in [ + hoststate.REBOOT, + hoststate.CONNECTING, + hoststate.INITIALIZING, + hoststate.INSTALLING, + hoststate.INSTALLING_OS, + ]: + wait( + service=host_service, + condition=lambda host: host.status == hoststate.UP, + fail_condition=failed_state, + ) + elif host.status == hoststate.PREPARING_FOR_MAINTENANCE: + wait( + service=host_service, + condition=lambda host: host.status == hoststate.MAINTENANCE, + fail_condition=failed_state, + ) + + +def main(): + argument_spec = ovirt_full_argument_spec( + state=dict( + choices=['present', 'absent', 'maintenance', 'upgraded', 'started', 'restarted', 'stopped'], + default='present', + ), + name=dict(required=True), + comment=dict(default=None), + cluster=dict(default=None), + address=dict(default=None), + password=dict(default=None), + public_key=dict(default=False, type='bool', aliases=['ssh_public_key']), + kdump_integration=dict(default=None, choices=['enabled', 'disabled']), + spm_priority=dict(default=None, type='int'), + override_iptables=dict(default=None, type='bool'), + force=dict(default=False, type='bool'), + timeout=dict(default=600, type='int'), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + check_sdk(module) + + try: + connection = create_connection(module.params.pop('auth')) + hosts_service = connection.system_service().hosts_service() + hosts_module = HostsModule( + connection=connection, + module=module, + service=hosts_service, + ) + + state = module.params['state'] + control_state(hosts_module) + if state == 'present': + ret = hosts_module.create() + hosts_module.action( + action='activate', + action_condition=lambda h: h.status == hoststate.MAINTENANCE, + wait_condition=lambda h: h.status == hoststate.UP, + fail_condition=failed_state, + ) + elif state == 'absent': + ret = hosts_module.remove() + elif state == 'maintenance': + ret = hosts_module.action( + action='deactivate', + action_condition=lambda h: h.status != hoststate.MAINTENANCE, + wait_condition=lambda h: h.status == hoststate.MAINTENANCE, + fail_condition=failed_state, + ) + elif state == 'upgraded': + ret = hosts_module.action( + action='upgrade', + action_condition=lambda h: h.update_available, + wait_condition=lambda h: h.status == hoststate.UP, + fail_condition=failed_state, + ) + elif state == 'started': + ret = hosts_module.action( + action='fence', + action_condition=lambda h: h.status == hoststate.DOWN, + wait_condition=lambda h: h.status in [hoststate.UP, hoststate.MAINTENANCE], + fail_condition=failed_state, + fence_type='start', + ) + elif state == 'stopped': + hosts_module.action( + action='deactivate', + action_condition=lambda h: h.status not in [hoststate.MAINTENANCE, hoststate.DOWN], + wait_condition=lambda h: h.status in [hoststate.MAINTENANCE, hoststate.DOWN], + fail_condition=failed_state, + ) + ret = hosts_module.action( + action='fence', + action_condition=lambda h: h.status != hoststate.DOWN, + wait_condition=lambda h: h.status == hoststate.DOWN, + fail_condition=failed_state, + fence_type='stop', + ) + elif state == 'restarted': + ret = hosts_module.action( + action='fence', + wait_condition=lambda h: h.status == hoststate.UP, + fail_condition=failed_state, + fence_type='restart', + ) + + + module.exit_json(**ret) + except Exception as e: + module.fail_json(msg=str(e)) + finally: + connection.close(logout=False) + + +from ansible.module_utils.basic import * +if __name__ == "__main__": + main() diff --git a/cloud/ovirt/ovirt_hosts_facts.py b/cloud/ovirt/ovirt_hosts_facts.py new file mode 100644 index 00000000000..53932868172 --- /dev/null +++ b/cloud/ovirt/ovirt_hosts_facts.py @@ -0,0 +1,95 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +try: + import ovirtsdk4 as sdk +except ImportError: + pass + +from ansible.module_utils.ovirt import * + + +DOCUMENTATION = ''' +--- +module: ovirt_hosts_facts +short_description: Retrieve facts about one or more oVirt hosts +author: "Ondra Machacek (@machacekondra)" +version_added: "2.3" +description: + - "Retrieve facts about one or more oVirt hosts." +notes: + - "This module creates a new top-level C(ovirt_hosts) fact, which + contains a list of hosts." +options: + pattern: + description: + - "Search term which is accepted by oVirt search backend." + - "For example to search host X from datacenter Y use following pattern: + name=X and datacenter=Y" +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Gather facts about all hosts which names start with C(host) and +# belong to data center C(west): +- ovirt_hosts_facts: + pattern: name=host* and datacenter=west +- debug: + var: ovirt_hosts +''' + +RETURN = ''' +ovirt_hosts: + description: "List of dictionaries describing the hosts. Host attribues are mapped to dictionary keys, + all hosts attributes can be found at following url: https://ovirt.example.com/ovirt-engine/api/model#types/host." + returned: On success. + type: list +''' + + +def main(): + argument_spec = ovirt_full_argument_spec( + pattern=dict(default='', required=False), + ) + module = AnsibleModule(argument_spec) + check_sdk(module) + + try: + connection = create_connection(module.params.pop('auth')) + hosts_service = connection.system_service().hosts_service() + hosts = hosts_service.list(search=module.params['pattern']) + module.exit_json( + changed=False, + ansible_facts=dict( + ovirt_hosts=[ + get_dict_of_struct(c) for c in hosts + ], + ), + ) + except Exception as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From 6a53c04cd426f9155fb775bf5c3ae2d657b92832 Mon Sep 17 00:00:00 2001 From: Jiri Tyr Date: Fri, 18 Nov 2016 14:38:04 +0000 Subject: [PATCH 2399/2522] Making yum_repository module compatible with Python 3 (#3487) --- packaging/os/yum_repository.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packaging/os/yum_repository.py b/packaging/os/yum_repository.py index 2f333ad1a7c..d73c81fb22a 100644 --- a/packaging/os/yum_repository.py +++ b/packaging/os/yum_repository.py @@ -19,10 +19,10 @@ # along with Ansible. If not, see . -import ConfigParser import os from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.six.moves import configparser DOCUMENTATION = ''' @@ -469,7 +469,7 @@ class YumRepo(object): module = None params = None section = None - repofile = ConfigParser.RawConfigParser() + repofile = configparser.RawConfigParser() # List of parameters which will be allowed in the repo file output allowed_params = [ @@ -576,7 +576,7 @@ def save(self): if len(self.repofile.sections()): # Write data into the file try: - fd = open(self.params['dest'], 'wb') + fd = open(self.params['dest'], 'w') except IOError: e = get_exception() self.module.fail_json( From 291c0a294e8c18207a5cea14afc8d264267ace08 Mon Sep 17 00:00:00 2001 From: Ryan Brown Date: Fri, 18 Nov 2016 10:04:16 -0500 Subject: [PATCH 2400/2522] Create `serverless` module for handling Serverless Framework deploys (#3352) * Create `serverless` module for handling deploys on the Serverless Framework * fix interpreter line * Successfully exit when a stage is already absent --- cloud/serverless.py | 185 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 cloud/serverless.py diff --git a/cloud/serverless.py b/cloud/serverless.py new file mode 100644 index 00000000000..473624200e6 --- /dev/null +++ b/cloud/serverless.py @@ -0,0 +1,185 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Ryan Scott Brown +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +DOCUMENTATION = ''' +--- +module: serverless +short_description: Manages a Serverless Framework project +description: + - Provides support for managing Serverless Framework (https://serverless.com/) project deployments and stacks. +version_added: "2.3" +options: + state: + choices: ['present', 'absent'] + description: + - Goal state of given stage/project + required: false + default: present + service_path: + description: + - The path to the root of the Serverless Service to be operated on. + required: true + functions: + description: + - A list of specific functions to deploy. If this is not provided, all functions in the service will be deployed. + required: false + default: [] + region: + description: + - AWS region to deploy the service to + required: false + default: us-east-1 + deploy: + description: + - Whether or not to deploy artifacts after building them. When this option is `false` all the functions will be built, but no stack update will be run to send them out. This is mostly useful for generating artifacts to be stored/deployed elsewhere. + required: false + default: true +notes: + - Currently, the `serverless` command must be in the path of the node executing the task. In the future this may be a flag. +requirements: [ "serverless" ] +author: "Ryan Scott Brown @ryansb" +''' + +EXAMPLES = """ +# Basic deploy of a service +- serverless: service_path={{ project_dir }} state=present + +# Deploy specific functions +- serverless: + service_path: "{{ project_dir }}" + functions: + - my_func_one + - my_func_two + +# deploy a project, then pull its resource list back into Ansible +- serverless: + stage: dev + region: us-east-1 + service_path: "{{ project_dir }}" + register: sls +# The cloudformation stack is always named the same as the full service, so the +# cloudformation_facts module can get a full list of the stack resources, as +# well as stack events and outputs +- cloudformation_facts: + region: us-east-1 + stack_name: "{{ sls.service_name }}" + stack_resources: true +""" + +RETURN = """ +service_name: + type: string + description: Most + returned: always + sample: my-fancy-service-dev +state: + type: string + description: Whether the stack for the serverless project is present/absent. + returned: always +command: + type: string + description: Full `serverless` command run by this module, in case you want to re-run the command outside the module. + returned: always + sample: serverless deploy --stage production +""" + + +import os +import traceback +import yaml + + +def read_serverless_config(module): + path = os.path.expanduser(module.params.get('service_path')) + + try: + with open(os.path.join(path, 'serverless.yml')) as sls_config: + config = yaml.safe_load(sls_config.read()) + return config + except IOError as e: + module.fail_json(msg="Could not open serverless.yml in {}. err: {}".format(path, str(e)), exception=traceback.format_exc()) + + module.fail_json(msg="Failed to open serverless config at {}".format( + os.path.join(path, 'serverless.yml'))) + + +def get_service_name(module, stage): + config = read_serverless_config(module) + if config.get('service') is None: + module.fail_json(msg="Could not read `service` key from serverless.yml file") + + if stage: + return "{}-{}".format(config['service'], stage) + + return "{}-{}".format(config['service'], config.get('stage', 'dev')) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + service_path = dict(required=True), + state = dict(default='present', choices=['present', 'absent'], required=False), + functions = dict(type='list', required=False), + region = dict(default='', required=False), + stage = dict(default='', required=False), + deploy = dict(default=True, type='bool', required=False), + ), + ) + + service_path = os.path.expanduser(module.params.get('service_path')) + state = module.params.get('state') + functions = module.params.get('functions') + region = module.params.get('region') + stage = module.params.get('stage') + deploy = module.params.get('deploy', True) + + command = "serverless " + if state == 'present': + command += 'deploy ' + elif state == 'absent': + command += 'remove ' + else: + module.fail_json(msg="State must either be 'present' or 'absent'. Received: {}".format(state)) + + if not deploy and state == 'present': + command += '--noDeploy ' + if region: + command += '--region {} '.format(region) + if stage: + command += '--stage {} '.format(stage) + + rc, out, err = module.run_command(command, cwd=service_path) + if rc != 0: + if state == 'absent' and "-{}' does not exist".format(stage) in out: + module.exit_json(changed=False, state='absent', command=command, + out=out, service_name=get_service_name(module, stage)) + + module.fail_json(msg="Failure when executing Serverless command. Exited {}.\nstdout: {}\nstderr: {}".format(rc, out, err)) + + # gather some facts about the deployment + module.exit_json(changed=True, state='present', out=out, command=command, + service_name=get_service_name(module, stage)) + +# import module snippets +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From 24972d3d64e538ca067c685f1c26ba06463c683b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Fri, 18 Nov 2016 18:12:57 +0100 Subject: [PATCH 2401/2522] cloudstack: cs_snapshot_policy: ignore intervaltype in has_changed (#3499) Fixes ValueError: invalid literal for int() with base 10: 'daily' --- cloud/cloudstack/cs_snapshot_policy.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cloud/cloudstack/cs_snapshot_policy.py b/cloud/cloudstack/cs_snapshot_policy.py index ce8b2344f36..a3a76ba4c10 100644 --- a/cloud/cloudstack/cs_snapshot_policy.py +++ b/cloud/cloudstack/cs_snapshot_policy.py @@ -230,13 +230,14 @@ def present_snapshot_policy(self): policy = self.get_snapshot_policy() args = { + 'id': policy.get('id') if policy else None, 'intervaltype': self.module.params.get('interval_type'), - 'schedule': self.module.params.get('schedule'), - 'maxsnaps': self.module.params.get('max_snaps'), - 'timezone': self.module.params.get('time_zone'), - 'volumeid': self.get_volume(key='id') + 'schedule': self.module.params.get('schedule'), + 'maxsnaps': self.module.params.get('max_snaps'), + 'timezone': self.module.params.get('time_zone'), + 'volumeid': self.get_volume(key='id') } - if not policy or (policy and self.has_changed(policy, args)): + if not policy or (policy and self.has_changed(policy, args, only_keys=['schedule', 'maxsnaps', 'timezone'])): self.result['changed'] = True if not self.module.check_mode: res = self.cs.createSnapshotPolicy(**args) From 160f2261decad0576a819d23c999b22b6fe357de Mon Sep 17 00:00:00 2001 From: Saravanan K R Date: Fri, 18 Nov 2016 22:45:28 +0530 Subject: [PATCH 2402/2522] Update the code fragment contents to have correct format (#3342) --- cloud/atomic/atomic_host.py | 2 +- cloud/atomic/atomic_image.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/atomic/atomic_host.py b/cloud/atomic/atomic_host.py index a802b93f916..a697a3ea53d 100644 --- a/cloud/atomic/atomic_host.py +++ b/cloud/atomic/atomic_host.py @@ -33,7 +33,7 @@ options: revision: description: - - The version number of the atomic host to be deployed. Providing ```latest``` will upgrade to the latest available version. + - The version number of the atomic host to be deployed. Providing C(latest) will upgrade to the latest available version. required: false default: latest aliases: ["version"] diff --git a/cloud/atomic/atomic_image.py b/cloud/atomic/atomic_image.py index 40517140db6..1011465c2c0 100644 --- a/cloud/atomic/atomic_image.py +++ b/cloud/atomic/atomic_image.py @@ -26,7 +26,7 @@ version_added: "2.2" author: "Saravanan KR @krsacme" notes: - - Host should be support ```atomic``` command + - Host should be support C(atomic) command requirements: - atomic - "python >= 2.6" @@ -39,7 +39,7 @@ state: description: - The state of the container image. - - The state ```latest``` will ensure container image is upgraded to the latest version and forcefully restart container, if running. + - The state C(latest) will ensure container image is upgraded to the latest version and forcefully restart container, if running. required: False choices: ["present", "absent", "latest"] default: latest From 9511de1e3d48270ac30d6d99bb96fde58f227ae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Fri, 18 Nov 2016 18:55:27 +0100 Subject: [PATCH 2403/2522] cs_snapshot_policy: extend volume determination (#3500) --- cloud/cloudstack/cs_snapshot_policy.py | 73 +++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 6 deletions(-) diff --git a/cloud/cloudstack/cs_snapshot_policy.py b/cloud/cloudstack/cs_snapshot_policy.py index a3a76ba4c10..d7963820c73 100644 --- a/cloud/cloudstack/cs_snapshot_policy.py +++ b/cloud/cloudstack/cs_snapshot_policy.py @@ -30,7 +30,40 @@ volume: description: - Name of the volume. - required: true + - Either C(volume) or C(vm) is required. + required: false + default: null + volume_type: + description: + - Type of the volume. + required: false + default: null + choices: + - DATADISK + - ROOT + version_added: "2.3" + vm: + description: + - Name of the instance to select the volume from. + - Use C(volume_type) if VM has a DATADISK and ROOT volume. + - In case of C(volume_type=DATADISK), additionally use C(device_id) if VM has more than one DATADISK volume. + - Either C(volume) or C(vm) is required. + required: false + default: null + version_added: "2.3" + device_id: + description: + - ID of the device on a VM the volume is attached to. + - This will only be considered if VM has multiple DATADISK volumes. + required: false + default: null + version_added: "2.3" + vpc: + description: + - Name of the vpc the instance is deployed in. + required: false + default: null + version_added: "2.3" interval_type: description: - Interval of the snapshot. @@ -91,6 +124,15 @@ schedule: '00:1' max_snaps: 3 +# Ensure a snapshot policy daily at 1h00 UTC on the second DATADISK of VM web-01 +- local_action: + module: cs_snapshot_policy + vm: web-01 + volume_type: DATADISK + device_id: 2 + schedule: '00:1' + max_snaps: 3 + # Ensure a snapshot policy hourly at minute 5 UTC - local_action: module: cs_snapshot_policy @@ -200,13 +242,25 @@ def get_volume(self, key=None): return self._get_by_key(key, self.volume) args = { - 'name': self.module.params.get('volume'), - 'account': self.get_account(key='name'), - 'domainid': self.get_domain(key='id'), - 'projectid': self.get_project(key='id'), + 'name': self.module.params.get('volume'), + 'account': self.get_account(key='name'), + 'domainid': self.get_domain(key='id'), + 'projectid': self.get_project(key='id'), + 'virtualmachineid': self.get_vm(key='id'), + 'type': self.module.params.get('volume_type'), } volumes = self.cs.listVolumes(**args) if volumes: + if volumes['count'] > 1: + device_id = self.module.params.get('device_id') + if not device_id: + self.module.fail_json(msg="Found more then 1 volume: combine params 'vm', 'volume_type', 'device_id' and/or 'volume' to select the volume") + else: + for v in volumes['volume']: + if v.get('deviceid') == device_id: + self.volume = v + return self._get_by_key(key, self.volume) + self.module.fail_json(msg="No volume found with device id %s" % device_id) self.volume = volumes['volume'][0] return self._get_by_key(key, self.volume) return None @@ -282,7 +336,11 @@ def get_result(self, policy): def main(): argument_spec = cs_argument_spec() argument_spec.update(dict( - volume=dict(required=True), + volume=dict(default=None), + volume_type=dict(choices=['DATADISK', 'ROOT'], default=None), + vm=dict(default=None), + device_id=dict(type='int', default=None), + vpc=dict(default=None), interval_type=dict(default='daily', choices=['hourly', 'daily', 'weekly', 'monthly'], aliases=['interval']), schedule=dict(default=None), time_zone=dict(default='UTC', aliases=['timezone']), @@ -296,6 +354,9 @@ def main(): module = AnsibleModule( argument_spec=argument_spec, required_together=cs_required_together(), + required_one_of = ( + ['vm', 'volume'], + ), supports_check_mode=True ) From 296cee682af4ec169e586c4c3b4a293a191d1029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Sun, 20 Nov 2016 23:59:16 +0100 Subject: [PATCH 2404/2522] cloudstack: cs_instance: implement vpc support (#3402) * cloudstack: cs_instance: implement vpc support * cloudstack: cs_instance: distinguish VPC and non VPC VMs --- cloud/cloudstack/cs_instance.py | 36 +++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 9903279f9e3..42ff6b42fa2 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -193,6 +193,12 @@ - Consider switching to HTTP_POST by using C(CLOUDSTACK_METHOD=post) to increase the HTTP_GET size limit of 2KB to 32 KB. required: false default: null + vpc: + description: + - Name of the VPC. + required: false + default: null + version_added: "2.3" force: description: - Force stop/start the instance if required to apply changes, otherwise a running instance will not be changed. @@ -494,15 +500,21 @@ def get_instance(self): instance = self.instance if not instance: instance_name = self.get_or_fallback('name', 'display_name') - - args = {} - args['account'] = self.get_account(key='name') - args['domainid'] = self.get_domain(key='id') - args['projectid'] = self.get_project(key='id') + vpc_id = self.get_vpc(key='id') + args = { + 'account': self.get_account(key='name'), + 'domainid': self.get_domain(key='id'), + 'projectid': self.get_project(key='id'), + 'vpcid': vpc_id, + } # Do not pass zoneid, as the instance name must be unique across zones. instances = self.cs.listVirtualMachines(**args) if instances: for v in instances['virtualmachine']: + # Due the limitation of the API, there is no easy way (yet) to get only those VMs + # not belonging to a VPC. + if not vpc_id and self.is_vm_in_vpc(vm=v): + continue if instance_name.lower() in [ v['name'].lower(), v['displayname'].lower(), v['id'] ]: self.instance = v break @@ -553,12 +565,13 @@ def get_network_ids(self, network_names=None): if not network_names: return None - args = {} - args['account'] = self.get_account(key='name') - args['domainid'] = self.get_domain(key='id') - args['projectid'] = self.get_project(key='id') - args['zoneid'] = self.get_zone(key='id') - + args = { + 'account': self.get_account(key='name'), + 'domainid': self.get_domain(key='id'), + 'projectid': self.get_project(key='id'), + 'zoneid': self.get_zone(key='id'), + 'vpcid': self.get_vpc(key='id'), + } networks = self.cs.listNetworks(**args) if not networks: self.module.fail_json(msg="No networks available") @@ -937,6 +950,7 @@ def main(): ssh_key = dict(default=None), force = dict(type='bool', default=False), tags = dict(type='list', aliases=[ 'tag' ], default=None), + vpc = dict(default=None), poll_async = dict(type='bool', default=True), )) From 25292b3ebd45a32597a9661763b8a9fbb0e9791f Mon Sep 17 00:00:00 2001 From: Andrea Tartaglia Date: Tue, 22 Nov 2016 19:26:32 +0000 Subject: [PATCH 2405/2522] py3 - ported ec2_vpc_route_table iterkeys to dict.keys() (#3503) * ported ec2_vpc_route_table iterkeys to dict.keys() for py3 Addresses ansible/ansible#18507 * Removed '.keys()' --- cloud/amazon/ec2_vpc_route_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index 4062f094ec4..a70d60a3ce5 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -299,7 +299,7 @@ def route_spec_matches_route(route_spec, route): if all((not route.gateway_id, not route.instance_id, not route.interface_id, not route.vpc_peering_connection_id)): return True - for k in key_attr_map.iterkeys(): + for k in key_attr_map: if k in route_spec: if route_spec[k] != getattr(route, k): return False From 43bb97bc3763b0335e245606eb2985314902cc91 Mon Sep 17 00:00:00 2001 From: Alexey Kostyuk Date: Wed, 23 Nov 2016 11:10:11 +0300 Subject: [PATCH 2406/2522] Add ipinfoio_facts module (#3497) * Add ipinfoio_facts module * Updated ipinfoio_facts module docs --- network/ipinfoio_facts.py | 137 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 network/ipinfoio_facts.py diff --git a/network/ipinfoio_facts.py b/network/ipinfoio_facts.py new file mode 100644 index 00000000000..fadcaa5c8f0 --- /dev/null +++ b/network/ipinfoio_facts.py @@ -0,0 +1,137 @@ +#!/usr/bin/python +# +# (c) 2016, Aleksei Kostiuk +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ipinfoio_facts +short_description: "Retrieve IP geolocation facts of a host's IP address" +description: + - "Gather IP geolocation facts of a host's IP address using ipinfo.io API" +version_added: "2.3" +author: "Aleksei Kostiuk (@akostyuk)" +options: + timeout: + description: + - HTTP connection timeout in seconds + required: false + default: 10 + http_agent: + description: + - Set http user agent + required: false + default: "ansible-ipinfoio-module/0.0.1" +notes: + - "Check http://ipinfo.io/ for more information" +''' + +EXAMPLES = ''' +# Retrieve geolocation data of a host's IP address +- name: get IP geolocation data + ipinfoio_facts: +''' + +RETURN = ''' +ansible_facts: + description: "Dictionary of ip geolocation facts for a host's IP address" + returned: changed + type: dictionary + contains: + ip: + description: "Public IP address of a host" + type: string + sample: "8.8.8.8" + hostname: + description: Domain name + type: string + sample: "google-public-dns-a.google.com" + country: + description: ISO 3166-1 alpha-2 country code + type: string + sample: "US" + region: + description: State or province name + type: string + sample: "California" + city: + description: City name + type: string + sample: "Mountain View" + loc: + description: Latitude and Longitude of the location + type: string + sample: "37.3860,-122.0838" + org: + description: "organization's name" + type: string + sample: "AS3356 Level 3 Communications, Inc." + postal: + description: Postal code + type: string + sample: "94035" +''' + +USER_AGENT = 'ansible-ipinfoio-module/0.0.1' + + +class IpinfoioFacts(object): + + def __init__(self, module): + self.url = 'https://ipinfo.io/json' + self.timeout = module.params.get('timeout') + self.module = module + + def get_geo_data(self): + response, info = fetch_url(self.module, self.url, force=True, # NOQA + timeout=self.timeout) + try: + info['status'] == 200 + except AssertionError: + self.module.fail_json(msg='Could not get {} page, ' + 'check for connectivity!'.format(self.url)) + else: + try: + content = response.read() + result = self.module.from_json(content.decode('utf8')) + except ValueError: + self.module.fail_json( + msg='Failed to parse the ipinfo.io response: ' + '{0} {1}'.format(self.url, content)) + else: + return result + + +def main(): + module = AnsibleModule( # NOQA + argument_spec=dict( + http_agent=dict(default=USER_AGENT), + timeout=dict(type='int', default=10), + ), + supports_check_mode=True, + ) + + ipinfoio = IpinfoioFacts(module) + ipinfoio_result = dict( + changed=False, ansible_facts=ipinfoio.get_geo_data()) + module.exit_json(**ipinfoio_result) + +from ansible.module_utils.basic import * # NOQA +from ansible.module_utils.urls import * # NOQA + +if __name__ == '__main__': + main() From 8ce6d9f78f048f4b8f0092c721f7c556a2a24574 Mon Sep 17 00:00:00 2001 From: Eric Chou Date: Thu, 24 Nov 2016 10:04:08 -0800 Subject: [PATCH 2407/2522] add a10_server_axapi3 module (#3239) * add a10_server_axapi3 module * added return documentation * modified a10_server_axapi3.py per feedback * fixed line 60 s/action/operation/ * modified a10_server_axapi3.py per feedback * modified a10_server_axapi3.py per feedback * corrected YAML format error in documentation * removed slp_server_ip and slp_server check in code since the arguments are labeled as required, per feedback * modified: a10_server.py modified: a10_service_group.py modified: a10_virtual_server.py Changed main() block, restricted import to only functions used. * removed space for main() to be last line * removed invalid lines * Modified Documentations for a10_server.py, a10_service_group.py, a10_virtual_server.py * Take out alias:[] and choices:[] in Documentation from a10_service_group.py and a10_virtual_server.py since they are now the default * deleted a10_server.py, a10_service_group.py, a10_virtual_server.py * deleted 'version_last_modified' line in Documentation across a10_server.py, a10_service_group.py and a10_virtual_server.py as they were added in error, change validate_certs version_added in a10_server.py * added newline after main() * added newline after main() for a10_server_axapi3.py --- network/a10/a10_server.py | 41 +++-- network/a10/a10_server_axapi3.py | 252 ++++++++++++++++++++++++++++++ network/a10/a10_service_group.py | 37 +++-- network/a10/a10_virtual_server.py | 36 +++-- 4 files changed, 333 insertions(+), 33 deletions(-) create mode 100644 network/a10/a10_server_axapi3.py diff --git a/network/a10/a10_server.py b/network/a10/a10_server.py index 1bd483bca11..6df65743d0b 100644 --- a/network/a10/a10_server.py +++ b/network/a10/a10_server.py @@ -3,7 +3,8 @@ """ Ansible module to manage A10 Networks slb server objects -(c) 2014, Mischa Peters +(c) 2014, Mischa Peters , +2016, Eric Chou This file is part of Ansible @@ -25,26 +26,28 @@ --- module: a10_server version_added: 1.8 -short_description: Manage A10 Networks AX/SoftAX/Thunder/vThunder devices +short_description: Manage A10 Networks AX/SoftAX/Thunder/vThunder devices' server object. description: - - Manage slb server objects on A10 Networks devices via aXAPI -author: "Mischa Peters (@mischapeters)" + - Manage SLB (Server Load Balancer) server objects on A10 Networks devices via aXAPIv2. +author: "Eric Chou (@ericchou) 2016, Mischa Peters (@mischapeters) 2014" +notes: + - Requires A10 Networks aXAPI 2.1. extends_documentation_fragment: a10 options: server_name: description: - - SLB server name. + - The SLB (Server Load Balancer) server name. required: true aliases: ['server'] server_ip: description: - - SLB server IP address. + - The SLB server IPv4 address. required: false default: null aliases: ['ip', 'address'] server_status: description: - - SLB virtual server status. + - The SLB virtual server status. required: false default: enabled aliases: ['status'] @@ -59,13 +62,25 @@ default: null state: description: - - Create, update or remove slb server. + - This is to specify the operation to create, update or remove SLB server. required: false default: present choices: ['present', 'absent'] + validate_certs: + description: + - If C(no), SSL certificates will not be validated. This should only be used + on personally controlled devices using self-signed certificates. + required: false + version_added: 2.3 + default: 'yes' + choices: ['yes', 'no'] ''' +RETURN = ''' +# +''' + EXAMPLES = ''' # Create a new server - a10_server: @@ -253,10 +268,12 @@ def status_needs_update(current_status, new_status): axapi_call(module, session_url + '&method=session.close') module.exit_json(changed=changed, content=result) -# standard ansible module imports -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * -from ansible.module_utils.a10 import * +# ansible module imports +import json +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.urls import url_argument_spec +from ansible.module_utils.a10 import axapi_call, a10_argument_spec, axapi_authenticate, axapi_failure, axapi_get_port_protocol, axapi_enabled_disabled + if __name__ == '__main__': main() diff --git a/network/a10/a10_server_axapi3.py b/network/a10/a10_server_axapi3.py new file mode 100644 index 00000000000..083ef824324 --- /dev/null +++ b/network/a10/a10_server_axapi3.py @@ -0,0 +1,252 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +Ansible module to manage A10 Networks slb server objects +(c) 2014, Mischa Peters , 2016, Eric Chou + +This file is part of Ansible + +Ansible is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Ansible is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Ansible. If not, see . +""" + +DOCUMENTATION = ''' +--- +module: a10_server_axapi3 +version_added: 2.3 +short_description: Manage A10 Networks AX/SoftAX/Thunder/vThunder devices +description: + - Manage SLB (Server Load Balancer) server objects on A10 Networks devices via aXAPIv3. +author: "Eric Chou (@ericchou) based on previous work by Mischa Peters (@mischapeters)" +extends_documentation_fragment: a10 +options: + server_name: + description: + - The SLB (Server Load Balancer) server name. + required: true + aliases: ['server'] + server_ip: + description: + - The SLB (Server Load Balancer) server IPv4 address. + required: true + aliases: ['ip', 'address'] + server_status: + description: + - The SLB (Server Load Balancer) virtual server status. + required: false + default: enable + aliases: ['action'] + choices: ['enable', 'disable'] + server_ports: + description: + - A list of ports to create for the server. Each list item should be a dictionary which specifies the C(port:) + and C(protocol:). + required: false + default: null + operation: + description: + - Create, Update or Remove SLB server. For create and update operation, we use the IP address and server + name specified in the POST message. For delete operation, we use the server name in the request URI. + required: false + default: create + choices: ['create', 'update', 'remove'] + validate_certs: + description: + - If C(no), SSL certificates will not be validated. This should only be used + on personally controlled devices using self-signed certificates. + required: false + default: 'yes' + choices: ['yes', 'no'] + +''' + +RETURN = ''' +# +''' + +EXAMPLES = ''' +# Create a new server +- a10_server: + host: a10.mydomain.com + username: myadmin + password: mypassword + server: test + server_ip: 1.1.1.100 + validate_certs: false + server_status: enable + write_config: yes + operation: create + server_ports: + - port-number: 8080 + protocol: tcp + action: enable + - port-number: 8443 + protocol: TCP + +''' + +VALID_PORT_FIELDS = ['port-number', 'protocol', 'action'] + +def validate_ports(module, ports): + for item in ports: + for key in item: + if key not in VALID_PORT_FIELDS: + module.fail_json(msg="invalid port field (%s), must be one of: %s" % (key, ','.join(VALID_PORT_FIELDS))) + + # validate the port number is present and an integer + if 'port-number' in item: + try: + item['port-number'] = int(item['port-number']) + except: + module.fail_json(msg="port-number entries in the port definitions must be integers") + else: + module.fail_json(msg="port definitions must define the port-number field") + + # validate the port protocol is present, no need to convert to the internal API integer value in v3 + if 'protocol' in item: + protocol = item['protocol'] + if not protocol: + module.fail_json(msg="invalid port protocol, must be one of: %s" % ','.join(AXAPI_PORT_PROTOCOLS)) + else: + item['protocol'] = protocol + else: + module.fail_json(msg="port definitions must define the port protocol (%s)" % ','.join(AXAPI_PORT_PROTOCOLS)) + + # 'status' is 'action' in AXAPIv3 + # no need to convert the status, a.k.a action, to the internal API integer value in v3 + # action is either enabled or disabled + if 'action' in item: + action = item['action'] + if action not in ['enable', 'disable']: + module.fail_json(msg="server action must be enable or disable") + else: + item['action'] = 'enable' + + +def main(): + argument_spec = a10_argument_spec() + argument_spec.update(url_argument_spec()) + argument_spec.update( + dict( + operation=dict(type='str', default='create', choices=['create', 'update', 'delete']), + server_name=dict(type='str', aliases=['server'], required=True), + server_ip=dict(type='str', aliases=['ip', 'address'], required=True), + server_status=dict(type='str', default='enable', aliases=['action'], choices=['enable', 'disable']), + server_ports=dict(type='list', aliases=['port'], default=[]), + ) + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=False + ) + + host = module.params['host'] + username = module.params['username'] + password = module.params['password'] + operation = module.params['operation'] + write_config = module.params['write_config'] + slb_server = module.params['server_name'] + slb_server_ip = module.params['server_ip'] + slb_server_status = module.params['server_status'] + slb_server_ports = module.params['server_ports'] + + axapi_base_url = 'https://{}/axapi/v3/'.format(host) + axapi_auth_url = axapi_base_url + 'auth/' + signature = axapi_authenticate_v3(module, axapi_auth_url, username, password) + + # validate the ports data structure + validate_ports(module, slb_server_ports) + + + json_post = { + "server-list": [ + { + "name": slb_server, + "host": slb_server_ip + } + ] + } + + # add optional module parameters + if slb_server_ports: + json_post['server-list'][0]['port-list'] = slb_server_ports + + if slb_server_status: + json_post['server-list'][0]['action'] = slb_server_status + + slb_server_data = axapi_call_v3(module, axapi_base_url+'slb/server/', method='GET', body='', signature=signature) + + # for empty slb server list + if axapi_failure(slb_server_data): + slb_server_exists = False + else: + slb_server_list = [server['name'] for server in slb_server_data['server-list']] + if slb_server in slb_server_list: + slb_server_exists = True + else: + slb_server_exists = False + + changed = False + if operation == 'create': + if slb_server_exists == False: + result = axapi_call_v3(module, axapi_base_url+'slb/server/', method='POST', body=json.dumps(json_post), signature=signature) + if axapi_failure(result): + module.fail_json(msg="failed to create the server: %s" % result['response']['err']['msg']) + changed = True + else: + module.fail_json(msg="server already exists, use state='update' instead") + changed = False + # if we changed things, get the full info regarding result + if changed: + result = axapi_call_v3(module, axapi_base_url + 'slb/server/' + slb_server, method='GET', body='', signature=signature) + else: + result = slb_server_data + elif operation == 'delete': + if slb_server_exists: + result = axapi_call_v3(module, axapi_base_url + 'slb/server/' + slb_server, method='DELETE', body='', signature=signature) + if axapi_failure(result): + module.fail_json(msg="failed to delete server: %s" % result['response']['err']['msg']) + changed = True + else: + result = dict(msg="the server was not present") + elif operation == 'update': + if slb_server_exists: + result = axapi_call_v3(module, axapi_base_url + 'slb/server/', method='PUT', body=json.dumps(json_post), signature=signature) + if axapi_failure(result): + module.fail_json(msg="failed to update server: %s" % result['response']['err']['msg']) + changed = True + else: + result = dict(msg="the server was not present") + + # if the config has changed, save the config unless otherwise requested + if changed and write_config: + write_result = axapi_call_v3(module, axapi_base_url+'write/memory/', method='POST', body='', signature=signature) + if axapi_failure(write_result): + module.fail_json(msg="failed to save the configuration: %s" % write_result['response']['err']['msg']) + + # log out gracefully and exit + axapi_call_v3(module, axapi_base_url + 'logoff/', method='POST', body='', signature=signature) + module.exit_json(changed=changed, content=result) + + +import json +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.urls import url_argument_spec +from ansible.module_utils.a10 import axapi_call_v3, a10_argument_spec, axapi_authenticate_v3, axapi_failure + + +if __name__ == '__main__': + main() + \ No newline at end of file diff --git a/network/a10/a10_service_group.py b/network/a10/a10_service_group.py index 4a6cb0a67fe..c99676b832e 100644 --- a/network/a10/a10_service_group.py +++ b/network/a10/a10_service_group.py @@ -3,7 +3,8 @@ """ Ansible module to manage A10 Networks slb service-group objects -(c) 2014, Mischa Peters +(c) 2014, Mischa Peters , +Eric Chou This file is part of Ansible @@ -25,30 +26,31 @@ --- module: a10_service_group version_added: 1.8 -short_description: Manage A10 Networks devices' service groups +short_description: Manage A10 Networks AX/SoftAX/Thunder/vThunder devices' service groups. description: - - Manage slb service-group objects on A10 Networks devices via aXAPI -author: "Mischa Peters (@mischapeters)" + - Manage SLB (Server Load Balancing) service-group objects on A10 Networks devices via aXAPIv2. +author: "Eric Chou (@ericchou) 2016, Mischa Peters (@mischapeters) 2014" notes: - - When a server doesn't exist and is added to the service-group the server will be created + - Requires A10 Networks aXAPI 2.1. + - When a server doesn't exist and is added to the service-group the server will be created. extends_documentation_fragment: a10 options: service_group: description: - - SLB service-group name. + - The SLB (Server Load Balancing) service-group name required: true default: null aliases: ['service', 'pool', 'group'] service_group_protocol: description: - - SLB service-group protocol. + - The SLB service-group protocol of TCP or UDP. required: false default: tcp aliases: ['proto', 'protocol'] choices: ['tcp', 'udp'] service_group_method: description: - - SLB service-group loadbalancing method. + - The SLB service-group load balancing method, such as round-robin or weighted-rr. required: false default: round-robin aliases: ['method'] @@ -60,9 +62,20 @@ specify the C(status:). See the examples below for details. required: false default: null + validate_certs: + description: + - If C(no), SSL certificates will not be validated. This should only be used + on personally controlled devices using self-signed certificates. + required: false + default: 'yes' + choices: ['yes', 'no'] ''' +RETURN = ''' +# +''' + EXAMPLES = ''' # Create a new service-group - a10_service_group: @@ -294,9 +307,11 @@ def main(): module.exit_json(changed=changed, content=result) # standard ansible module imports -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * -from ansible.module_utils.a10 import * +import json +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.urls import url_argument_spec +from ansible.module_utils.a10 import axapi_call, a10_argument_spec, axapi_authenticate, axapi_failure, axapi_enabled_disabled + if __name__ == '__main__': main() diff --git a/network/a10/a10_virtual_server.py b/network/a10/a10_virtual_server.py index 065cc98a2a2..d631a0564f4 100644 --- a/network/a10/a10_virtual_server.py +++ b/network/a10/a10_virtual_server.py @@ -3,7 +3,8 @@ """ Ansible module to manage A10 Networks slb virtual server objects -(c) 2014, Mischa Peters +(c) 2014, Mischa Peters , +Eric Chou This file is part of Ansible @@ -25,27 +26,29 @@ --- module: a10_virtual_server version_added: 1.8 -short_description: Manage A10 Networks devices' virtual servers +short_description: Manage A10 Networks AX/SoftAX/Thunder/vThunder devices' virtual servers. description: - - Manage slb virtual server objects on A10 Networks devices via aXAPI -author: "Mischa Peters (@mischapeters)" + - Manage SLB (Server Load Balancing) virtual server objects on A10 Networks devices via aXAPIv2. +author: "Eric Chou (@ericchou) 2016, Mischa Peters (@mischapeters) 2014" +notes: + - Requires A10 Networks aXAPI 2.1. extends_documentation_fragment: a10 options: virtual_server: description: - - SLB virtual server name. + - The SLB (Server Load Balancing) virtual server name. required: true default: null aliases: ['vip', 'virtual'] virtual_server_ip: description: - - SLB virtual server IP address. + - The SLB virtual server IPv4 address. required: false default: null aliases: ['ip', 'address'] virtual_server_status: description: - - SLB virtual server status. + - The SLB virtual server status, such as enabled or disabled. required: false default: enable aliases: ['status'] @@ -57,9 +60,20 @@ specify the C(service_group:) as well as the C(status:). See the examples below for details. This parameter is required when C(state) is C(present). required: false + validate_certs: + description: + - If C(no), SSL certificates will not be validated. This should only be used + on personally controlled devices using self-signed certificates. + required: false + default: 'yes' + choices: ['yes', 'no'] ''' +RETURN = ''' +# +''' + EXAMPLES = ''' # Create a new virtual server - a10_virtual_server: @@ -248,9 +262,11 @@ def needs_update(src_ports, dst_ports): module.exit_json(changed=changed, content=result) # standard ansible module imports -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * -from ansible.module_utils.a10 import * +import json +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.urls import url_argument_spec +from ansible.module_utils.a10 import axapi_call, a10_argument_spec, axapi_authenticate, axapi_failure, axapi_enabled_disabled, axapi_get_vport_protocol + if __name__ == '__main__': main() From 47146851a1e382052bd6d8cbea67c7cdfdecbeb0 Mon Sep 17 00:00:00 2001 From: Dorn- Date: Thu, 24 Nov 2016 19:07:51 +0100 Subject: [PATCH 2408/2522] Add a new module to handle schema with postgres (#1883) --- database/postgresql/postgresql_schema.py | 266 +++++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 database/postgresql/postgresql_schema.py diff --git a/database/postgresql/postgresql_schema.py b/database/postgresql/postgresql_schema.py new file mode 100644 index 00000000000..9aafe75776b --- /dev/null +++ b/database/postgresql/postgresql_schema.py @@ -0,0 +1,266 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: postgresql_schema +short_description: Add or remove PostgreSQL schema from a remote host. +description: + - Add or remove PostgreSQL schema from a remote host. +version_added: "2.2" +options: + name: + description: + - name of the schema to add or remove + required: true + default: null + database: + description: + - name of the database to connect to + required: false + default: postgres + 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_unix_socket: + description: + - Path to a Unix domain socket for local connections + required: false + default: null + owner: + description: + - Name of the role to set as owner of the schema + required: false + default: null + port: + description: + - Database port to connect to. + required: false + default: 5432 + state: + description: + - The schema state + required: false + default: present + choices: [ "present", "absent" ] +notes: + - This module uses I(psycopg2), a Python PostgreSQL database adapter. You must ensure that psycopg2 is installed on + the host before using this module. If the remote host is the PostgreSQL server (which is the default case), then PostgreSQL must also be installed on the remote host. For Ubuntu-based systems, install the C(postgresql), C(libpq-dev), and C(python-psycopg2) packages on the remote host before using this module. +requirements: [ psycopg2 ] +author: "Flavien Chantelot " +''' + +EXAMPLES = ''' +# Create a new schema with name "acme" +- postgresql_schema: name=acme +# Create a new schema "acme" with a user "bob" who will own it +- postgresql_schema: name=acme owner=bob + +''' + +RETURN = ''' +schema: + description: Name of the schema + returned: success, changed + type: string + sample: "acme" +''' + + +try: + import psycopg2 + import psycopg2.extras +except ImportError: + postgresqldb_found = False +else: + postgresqldb_found = True + +class NotSupportedError(Exception): + pass + + +# =========================================== +# PostgreSQL module specific support methods. +# + +def set_owner(cursor, schema, owner): + query = "ALTER SCHEMA %s OWNER TO %s" % ( + pg_quote_identifier(schema, 'schema'), + pg_quote_identifier(owner, 'role')) + cursor.execute(query) + return True + +def get_schema_info(cursor, schema): + query = """ + SELECT schema_owner AS owner + FROM information_schema.schemata + WHERE schema_name = %(schema)s + """ + cursor.execute(query, {'schema': schema}) + return cursor.fetchone() + +def schema_exists(cursor, schema): + query = "SELECT schema_name FROM information_schema.schemata WHERE schema_name = %(schema)s" + cursor.execute(query, {'schema': schema}) + return cursor.rowcount == 1 + +def schema_delete(cursor, schema): + if schema_exists(cursor, schema): + query = "DROP SCHEMA %s" % pg_quote_identifier(schema, 'schema') + cursor.execute(query) + return True + else: + return False + +def schema_create(cursor, schema, owner): + if not schema_exists(cursor, schema): + query_fragments = ['CREATE SCHEMA %s' % pg_quote_identifier(schema, 'schema')] + if owner: + query_fragments.append('AUTHORIZATION %s' % pg_quote_identifier(owner, 'role')) + query = ' '.join(query_fragments) + cursor.execute(query) + return True + else: + schema_info = get_schema_info(cursor, schema) + if owner and owner != schema_info['owner']: + return set_owner(cursor, schema, owner) + else: + return False + +def schema_matches(cursor, schema, owner): + if not schema_exists(cursor, schema): + return False + else: + schema_info = get_schema_info(cursor, schema) + if owner and owner != schema_info['owner']: + return False + else: + return True + +# =========================================== +# Module execution. +# + +def main(): + module = AnsibleModule( + argument_spec=dict( + login_user=dict(default="postgres"), + login_password=dict(default=""), + login_host=dict(default=""), + login_unix_socket=dict(default=""), + port=dict(default="5432"), + schema=dict(required=True, aliases=['name']), + owner=dict(default=""), + database=dict(default="postgres"), + state=dict(default="present", choices=["absent", "present"]), + ), + supports_check_mode = True + ) + + if not postgresqldb_found: + module.fail_json(msg="the python psycopg2 module is required") + + schema = module.params["schema"] + owner = module.params["owner"] + state = module.params["state"] + database = module.params["database"] + changed = False + + # To use defaults values, keyword arguments must be absent, so + # check which values are empty and don't include in the **kw + # dictionary + params_map = { + "login_host":"host", + "login_user":"user", + "login_password":"password", + "port":"port" + } + kw = dict( (params_map[k], v) for (k, v) in module.params.iteritems() + if k in params_map and v != '' ) + + # If a login_unix_socket is specified, incorporate it here. + is_localhost = "host" not in kw or kw["host"] == "" or kw["host"] == "localhost" + if is_localhost and module.params["login_unix_socket"] != "": + kw["host"] = module.params["login_unix_socket"] + + try: + db_connection = psycopg2.connect(database=database, **kw) + # Enable autocommit so we can create databases + if psycopg2.__version__ >= '2.4.2': + db_connection.autocommit = True + else: + db_connection.set_isolation_level(psycopg2 + .extensions + .ISOLATION_LEVEL_AUTOCOMMIT) + cursor = db_connection.cursor( + cursor_factory=psycopg2.extras.DictCursor) + except Exception: + e = get_exception() + module.fail_json(msg="unable to connect to database: %s" %(text, str(e))) + + try: + if module.check_mode: + if state == "absent": + changed = not schema_exists(cursor, schema) + elif state == "present": + changed = not schema_matches(cursor, schema, owner) + module.exit_json(changed=changed, schema=schema) + + if state == "absent": + try: + changed = schema_delete(cursor, schema) + except SQLParseError: + e = get_exception() + module.fail_json(msg=str(e)) + + elif state == "present": + try: + changed = schema_create(cursor, schema, owner) + except SQLParseError: + e = get_exception() + module.fail_json(msg=str(e)) + except NotSupportedError: + e = get_exception() + module.fail_json(msg=str(e)) + except SystemExit: + # Avoid catching this on Python 2.4 + raise + except Exception: + e = get_exception() + module.fail_json(msg="Database query failed: %s" %(text, str(e))) + + module.exit_json(changed=changed, schema=schema) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.database import * +if __name__ == '__main__': + main() + From 82191f724f327db4b29f9049808067f89a592eef Mon Sep 17 00:00:00 2001 From: John R Barker Date: Thu, 24 Nov 2016 18:35:53 +0000 Subject: [PATCH 2409/2522] Update version_added, update docs (#3542) k: v in docs now Minor tweaks after https://github.com/ansible/ansible-modules-extras/pull/1883 --- database/postgresql/postgresql_schema.py | 30 ++++++++++++++---------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/database/postgresql/postgresql_schema.py b/database/postgresql/postgresql_schema.py index 9aafe75776b..06cf048d43e 100644 --- a/database/postgresql/postgresql_schema.py +++ b/database/postgresql/postgresql_schema.py @@ -19,44 +19,44 @@ DOCUMENTATION = ''' --- module: postgresql_schema -short_description: Add or remove PostgreSQL schema from a remote host. +short_description: Add or remove PostgreSQL schema from a remote host description: - Add or remove PostgreSQL schema from a remote host. -version_added: "2.2" +version_added: "2.3" options: name: description: - - name of the schema to add or remove + - Name of the schema to add or remove. required: true default: null database: description: - - name of the database to connect to + - Name of the database to connect to. required: false default: postgres login_user: description: - - The username used to authenticate with + - The username used to authenticate with. required: false default: null login_password: description: - - The password used to authenticate with + - The password used to authenticate with. required: false default: null login_host: description: - - Host running the database + - Host running the database. required: false default: localhost login_unix_socket: description: - - Path to a Unix domain socket for local connections + - Path to a Unix domain socket for local connections. required: false default: null owner: description: - - Name of the role to set as owner of the schema + - Name of the role to set as owner of the schema. required: false default: null port: @@ -66,7 +66,7 @@ default: 5432 state: description: - - The schema state + - The schema state. required: false default: present choices: [ "present", "absent" ] @@ -79,9 +79,13 @@ EXAMPLES = ''' # Create a new schema with name "acme" -- postgresql_schema: name=acme +- postgresql_schema: + name: acme + # Create a new schema "acme" with a user "bob" who will own it -- postgresql_schema: name=acme owner=bob +- postgresql_schema: + name: acme + owner: bob ''' @@ -261,6 +265,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.database import * + if __name__ == '__main__': main() - From b31d3786736255921777c50b544854fc8ac2fe06 Mon Sep 17 00:00:00 2001 From: Jesse Keating Date: Sat, 26 Nov 2016 02:28:45 -0800 Subject: [PATCH 2410/2522] Handle empty datadog_monitor message (#3468) Since message is not required, it may be of type None, which cannot have a replace() called on it. --- monitoring/datadog_monitor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py index 4688a0e5944..8fe9ded9be3 100644 --- a/monitoring/datadog_monitor.py +++ b/monitoring/datadog_monitor.py @@ -197,7 +197,9 @@ def main(): unmute_monitor(module) def _fix_template_vars(message): - return message.replace('[[', '{{').replace(']]', '}}') + if message: + return message.replace('[[', '{{').replace(']]', '}}') + return message def _get_monitor(module): From ae518ba6700b7b00c34c4762cd5765b5bd9137b8 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Mon, 28 Nov 2016 13:12:43 -0800 Subject: [PATCH 2411/2522] Fix ast parse issue for python 2.6. --- network/a10/a10_server_axapi3.py | 1 - 1 file changed, 1 deletion(-) diff --git a/network/a10/a10_server_axapi3.py b/network/a10/a10_server_axapi3.py index 083ef824324..8363e548d10 100644 --- a/network/a10/a10_server_axapi3.py +++ b/network/a10/a10_server_axapi3.py @@ -249,4 +249,3 @@ def main(): if __name__ == '__main__': main() - \ No newline at end of file From 01cee31950a7dfc442d3bd0a1bf87e3e713fddf1 Mon Sep 17 00:00:00 2001 From: "William L. Thomson Jr" Date: Mon, 28 Nov 2016 17:05:47 -0500 Subject: [PATCH 2412/2522] Portage module improvements (#3520) * packaging/os/portage.py: Added portage parameter --keep-going * packaging/os/portage.py: Added portage parameter --load-avg [FLOAT] * packaging/os/portage.py: Added portage parameter --jobs[=INT] * packaging/os/portage.py: Added myself to Authors --- packaging/os/portage.py | 43 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/packaging/os/portage.py b/packaging/os/portage.py index 4e8507fedf5..f88b67fa9ae 100644 --- a/packaging/os/portage.py +++ b/packaging/os/portage.py @@ -1,8 +1,10 @@ #!/usr/bin/python # -*- coding: utf-8 -*- +# (c) 2016, William L Thomson Jr # (c) 2013, Yap Sok Ann # Written by Yap Sok Ann +# Modified by William L. Thomson Jr. # Based on apt module written by Matthew Williams # # This module is free software: you can redistribute it and/or modify @@ -146,8 +148,34 @@ default: False choices: [ "yes", "no" ] + keepgoing: + description: + - Continue as much as possible after an error. + required: false + default: False + choices: [ "yes", "no" ] + version_added: 2.3 + + jobs: + description: + - Specifies the number of packages to build simultaneously. + required: false + default: None + type: int + version_added: 2.3 + + loadavg: + description: + - Specifies that no new builds should be started if there are + - other builds running and the load average is at least LOAD + required: false + default: None + type: float + version_added: 2.3 + requirements: [ gentoolkit ] -author: +author: + - "William L Thomson Jr (@wltjr)" - "Yap Sok Ann (@sayap)" - "Andrew Udvare" notes: [] @@ -272,6 +300,7 @@ def emerge_packages(module, packages): 'getbinpkg': '--getbinpkg', 'usepkgonly': '--usepkgonly', 'usepkg': '--usepkg', + 'keepgoing': '--keep-going', } for flag, arg in emerge_flags.iteritems(): if p[flag]: @@ -280,6 +309,15 @@ def emerge_packages(module, packages): if p['usepkg'] and p['usepkgonly']: module.fail_json(msg='Use only one of usepkg, usepkgonly') + emerge_flags = { + 'jobs': '--jobs=', + 'loadavg': '--load-average ', + } + + for flag, arg in emerge_flags.iteritems(): + if p[flag] is not None: + args.append(arg + str(p[flag])) + cmd, (rc, out, err) = run_emerge(module, packages, *args) if rc != 0: module.fail_json( @@ -416,6 +454,9 @@ def main(): getbinpkg=dict(default=False, type='bool'), usepkgonly=dict(default=False, type='bool'), usepkg=dict(default=False, type='bool'), + keepgoing=dict(default=False, type='bool'), + jobs=dict(default=None, type='int'), + loadavg=dict(default=None, type='float'), ), required_one_of=[['package', 'sync', 'depclean']], mutually_exclusive=[['nodeps', 'onlydeps'], ['quiet', 'verbose']], From 670ae951dceefdc7078f9221c5c815b6beb508ca Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Tue, 29 Nov 2016 01:26:30 -0800 Subject: [PATCH 2413/2522] Remove iteritems from portage module for python3 --- packaging/os/portage.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packaging/os/portage.py b/packaging/os/portage.py index f88b67fa9ae..a7f43182cdc 100644 --- a/packaging/os/portage.py +++ b/packaging/os/portage.py @@ -302,7 +302,7 @@ def emerge_packages(module, packages): 'usepkg': '--usepkg', 'keepgoing': '--keep-going', } - for flag, arg in emerge_flags.iteritems(): + for flag, arg in emerge_flags.items(): if p[flag]: args.append(arg) @@ -314,7 +314,7 @@ def emerge_packages(module, packages): 'loadavg': '--load-average ', } - for flag, arg in emerge_flags.iteritems(): + for flag, arg in emerge_flags.items(): if p[flag] is not None: args.append(arg + str(p[flag])) From 20ea46642b928d44cbb14ba2f73ed4b91c776fb0 Mon Sep 17 00:00:00 2001 From: Aaron Chen Date: Wed, 30 Nov 2016 05:12:43 +0800 Subject: [PATCH 2414/2522] elasticsearch-plugin: fix local variable 'name' referenced before assignment (#3431) --- packaging/elasticsearch_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/elasticsearch_plugin.py b/packaging/elasticsearch_plugin.py index 3bffd99b089..9bb904a88c5 100644 --- a/packaging/elasticsearch_plugin.py +++ b/packaging/elasticsearch_plugin.py @@ -129,7 +129,7 @@ def install_plugin(module, plugin_bin, plugin_name, version, url, proxy_host, pr cmd_args = [plugin_bin, PACKAGE_STATE_MAP["present"], plugin_name] if version: - name = name + '/' + version + plugin_name = plugin_name + '/' + version if proxy_host and proxy_port: cmd_args.append("-DproxyHost=%s -DproxyPort=%s" % (proxy_host, proxy_port)) From 6d23a4364da1134212161de792ee248175e0df33 Mon Sep 17 00:00:00 2001 From: Dorian Dietzel Date: Wed, 30 Nov 2016 16:08:53 +0100 Subject: [PATCH 2415/2522] Added new option to select the active a10 partition (#2628) * Added new option to select the active a10 partition * added version_added to the description of the new option * added RETURN documentation * fixed indents * Removed empty cases, removed unneeded aliases * removed artifacts from merging * updated version_added to 2.3 * removed host, username and password option * removed write_config and validate_certs documentation --- network/a10/a10_server.py | 20 ++++++++++++++++++++ network/a10/a10_service_group.py | 20 +++++++++++++++++++- network/a10/a10_virtual_server.py | 18 ++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/network/a10/a10_server.py b/network/a10/a10_server.py index 6df65743d0b..38a3b1a3aa1 100644 --- a/network/a10/a10_server.py +++ b/network/a10/a10_server.py @@ -34,6 +34,12 @@ - Requires A10 Networks aXAPI 2.1. extends_documentation_fragment: a10 options: + partition: + version_added: "2.3" + description: + - set active-partition + required: false + default: null server_name: description: - The SLB (Server Load Balancer) server name. @@ -87,6 +93,7 @@ host: a10.mydomain.com username: myadmin password: mypassword + partition: mypartition server: test server_ip: 1.1.1.100 server_ports: @@ -97,6 +104,15 @@ ''' +RETURN = ''' +content: + description: the full info regarding the slb_server + returned: success + type: string + sample: "mynewserver" +''' + + VALID_PORT_FIELDS = ['port_num', 'protocol', 'status'] def validate_ports(module, ports): @@ -142,6 +158,7 @@ def main(): server_ip=dict(type='str', aliases=['ip', 'address']), server_status=dict(type='str', default='enabled', aliases=['status'], choices=['enabled', 'disabled']), server_ports=dict(type='list', aliases=['port'], default=[]), + partition=dict(type='str', default=[]), ) ) @@ -151,6 +168,7 @@ def main(): ) host = module.params['host'] + partition = module.params['partition'] username = module.params['username'] password = module.params['password'] state = module.params['state'] @@ -185,6 +203,8 @@ def main(): if slb_server_status: json_post['server']['status'] = axapi_enabled_disabled(slb_server_status) + slb_server_partition = axapi_call(module, session_url + '&method=system.partition.active', json.dumps({'name': partition})) + slb_server_data = axapi_call(module, session_url + '&method=slb.server.search', json.dumps({'name': slb_server})) slb_server_exists = not axapi_failure(slb_server_data) diff --git a/network/a10/a10_service_group.py b/network/a10/a10_service_group.py index c99676b832e..39716fda49b 100644 --- a/network/a10/a10_service_group.py +++ b/network/a10/a10_service_group.py @@ -35,6 +35,12 @@ - When a server doesn't exist and is added to the service-group the server will be created. extends_documentation_fragment: a10 options: + partition: + version_added: "2.3" + description: + - set active-partition + required: false + default: null service_group: description: - The SLB (Server Load Balancing) service-group name @@ -82,6 +88,7 @@ host: a10.mydomain.com username: myadmin password: mypassword + partition: mypartition service_group: sg-80-tcp servers: - server: foo1.mydomain.com @@ -96,6 +103,14 @@ ''' +RETURN = ''' +content: + description: the full info regarding the slb_service_group + returned: success + type: string + sample: "mynewservicegroup" +''' + VALID_SERVICE_GROUP_FIELDS = ['name', 'protocol', 'lb_method'] VALID_SERVER_FIELDS = ['server', 'port', 'status'] @@ -147,6 +162,7 @@ def main(): 'src-ip-only-hash', 'src-ip-hash']), servers=dict(type='list', aliases=['server', 'member'], default=[]), + partition=dict(type='str', default=[]), ) ) @@ -158,6 +174,7 @@ def main(): host = module.params['host'] username = module.params['username'] password = module.params['password'] + partition = module.params['partition'] state = module.params['state'] write_config = module.params['write_config'] slb_service_group = module.params['service_group'] @@ -199,7 +216,8 @@ def main(): # first we authenticate to get a session id session_url = axapi_authenticate(module, axapi_base_url, username, password) - + # then we select the active-partition + slb_server_partition = axapi_call(module, session_url + '&method=system.partition.active', json.dumps({'name': partition})) # then we check to see if the specified group exists slb_result = axapi_call(module, session_url + '&method=slb.service_group.search', json.dumps({'name': slb_service_group})) slb_service_group_exist = not axapi_failure(slb_result) diff --git a/network/a10/a10_virtual_server.py b/network/a10/a10_virtual_server.py index d631a0564f4..783cfa4baa4 100644 --- a/network/a10/a10_virtual_server.py +++ b/network/a10/a10_virtual_server.py @@ -34,6 +34,12 @@ - Requires A10 Networks aXAPI 2.1. extends_documentation_fragment: a10 options: + partition: + version_added: "2.3" + description: + - set active-partition + required: false + default: null virtual_server: description: - The SLB (Server Load Balancing) virtual server name. @@ -80,6 +86,7 @@ host: a10.mydomain.com username: myadmin password: mypassword + partition: mypartition virtual_server: vserver1 virtual_server_ip: 1.1.1.1 virtual_server_ports: @@ -95,6 +102,14 @@ ''' +RETURN = ''' +content: + description: the full info regarding the slb_virtual + returned: success + type: string + sample: "mynewvirtualserver" +''' + VALID_PORT_FIELDS = ['port', 'protocol', 'service_group', 'status'] def validate_ports(module, ports): @@ -143,6 +158,7 @@ def main(): virtual_server_ip=dict(type='str', aliases=['ip', 'address'], required=True), virtual_server_status=dict(type='str', default='enabled', aliases=['status'], choices=['enabled', 'disabled']), virtual_server_ports=dict(type='list', required=True), + partition=dict(type='str', default=[]), ) ) @@ -154,6 +170,7 @@ def main(): host = module.params['host'] username = module.params['username'] password = module.params['password'] + partition = module.params['partition'] state = module.params['state'] write_config = module.params['write_config'] slb_virtual = module.params['virtual_server'] @@ -169,6 +186,7 @@ def main(): axapi_base_url = 'https://%s/services/rest/V2.1/?format=json' % host session_url = axapi_authenticate(module, axapi_base_url, username, password) + slb_server_partition = axapi_call(module, session_url + '&method=system.partition.active', json.dumps({'name': partition})) slb_virtual_data = axapi_call(module, session_url + '&method=slb.virtual_server.search', json.dumps({'name': slb_virtual})) slb_virtual_exists = not axapi_failure(slb_virtual_data) From 27a93430b91bfce8e3344358aeae19e20baab69b Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Wed, 30 Nov 2016 17:11:21 +0000 Subject: [PATCH 2416/2522] Use native YAML (#3440) * Use native YAML * Add comment on quotes and column --- system/crypttab.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/system/crypttab.py b/system/crypttab.py index b8834a23c8a..0565f57f9eb 100644 --- a/system/crypttab.py +++ b/system/crypttab.py @@ -73,12 +73,20 @@ ''' EXAMPLES = ''' + +# Since column is a special character in YAML, if your string contains a column, it's better to use quotes around the string - name: Set the options explicitly a device which must already exist - crypttab: name=luks-home state=present opts=discard,cipher=aes-cbc-essiv:sha256 + crypttab: + name: luks-home + state: present + opts: 'discard,cipher=aes-cbc-essiv:sha256' - name: Add the 'discard' option to any existing options for all devices - crypttab: name={{ item.device }} state=opts_present opts=discard - with_items: "{{ ansible_mounts }}" + crypttab: + name: '{{ item.device }}' + state: opts_present + opts: discard + with_items: '{{ ansible_mounts }}' when: '/dev/mapper/luks-' in {{ item.device }} ''' From f9374eb3432bc7e1785bfa7a13467a57e9f484ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Wed, 30 Nov 2016 20:47:36 +0100 Subject: [PATCH 2417/2522] cloudstack: new module cs_region (#3568) --- cloud/cloudstack/cs_region.py | 204 ++++++++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 cloud/cloudstack/cs_region.py diff --git a/cloud/cloudstack/cs_region.py b/cloud/cloudstack/cs_region.py new file mode 100644 index 00000000000..ae863f025c2 --- /dev/null +++ b/cloud/cloudstack/cs_region.py @@ -0,0 +1,204 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2016, René Moser +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: cs_region +short_description: Manages regions on Apache CloudStack based clouds. +description: + - Add, update and remove regions. +version_added: "2.3" +author: "René Moser (@resmo)" +options: + id: + description: + - ID of the region. + - Must be an number (int). + required: true + name: + description: + - Name of the region. + - Required if C(state=present) + required: false + default: null + endpoint: + description: + - Endpoint URL of the region. + - Required if C(state=present) + required: false + default: null + state: + description: + - State of the region. + required: false + default: 'present' + choices: [ 'present', 'absent' ] +extends_documentation_fragment: cloudstack +''' + +EXAMPLES = ''' +# create a region +local_action: + module: cs_region + id: 2 + name: geneva + endpoint: https://cloud.gva.example.com + +# remove a region with ID 2 +local_action: + module: cs_region + id: 2 + state: absent +''' + +RETURN = ''' +--- +id: + description: ID of the region. + returned: success + type: int + sample: 1 +name: + description: Name of the region. + returned: success + type: string + sample: local +endpoint: + description: Endpoint of the region. + returned: success + type: string + sample: http://cloud.example.com +gslb_service_enabled: + description: Whether the GSLB service is enabled or not + returned: success + type: bool + sample: true +portable_ip_service_enabled: + description: Whether the portable IP service is enabled or not + returned: success + type: bool + sample: true +''' + + +from ansible.module_utils.cloudstack import * +from ansible.module_utils.basic import AnsibleModule + +class AnsibleCloudStackRegion(AnsibleCloudStack): + + def __init__(self, module): + super(AnsibleCloudStackRegion, self).__init__(module) + self.returns = { + 'endpoint': 'endpoint', + 'gslbserviceenabled': 'gslb_service_enabled', + 'portableipserviceenabled': 'portable_ip_service_enabled', + } + + def get_region(self): + id = self.module.params.get('id') + regions = self.cs.listRegions(id=id) + if regions: + return regions['region'][0] + return None + + def present_region(self): + region = self.get_region() + if not region: + region = self._create_region(region=region) + else: + region = self._update_region(region=region) + return region + + def _create_region(self, region): + self.result['changed'] = True + args = { + 'id': self.module.params.get('id'), + 'name': self.module.params.get('name'), + 'endpoint': self.module.params.get('endpoint') + } + if not self.module.check_mode: + res = self.cs.addRegion(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + region = res['region'] + return region + + def _update_region(self, region): + args = { + 'id': self.module.params.get('id'), + 'name': self.module.params.get('name'), + 'endpoint': self.module.params.get('endpoint') + } + if self.has_changed(args, region): + self.result['changed'] = True + if not self.module.check_mode: + res = self.cs.updateRegion(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + region = res['region'] + return region + + def absent_region(self): + region = self.get_region() + if region: + self.result['changed'] = True + if not self.module.check_mode: + res = self.cs.removeRegion(id=region['id']) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + return region + + +def main(): + argument_spec = cs_argument_spec() + argument_spec.update(dict( + id=dict(required=True, type='int'), + name=dict(default=None), + endpoint=dict(default=None), + state=dict(choices=['present', 'absent'], default='present'), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=cs_required_together(), + required_if=[ + ('state', 'present', ['name', 'endpoint']), + ], + supports_check_mode=True + ) + + try: + acs_region = AnsibleCloudStackRegion(module) + + state = module.params.get('state') + if state == 'absent': + region = acs_region.absent_region() + else: + region = acs_region.present_region() + + result = acs_region.get_result(region) + + except CloudStackException as e: + module.fail_json(msg='CloudStackException: %s' % str(e)) + + module.exit_json(**result) + +if __name__ == '__main__': + main() From ae2a0edb0a423fb06c5363ad6620a98719639d3a Mon Sep 17 00:00:00 2001 From: dimid Date: Thu, 1 Dec 2016 10:19:32 +0200 Subject: [PATCH 2418/2522] Fix name/package parameter convert when passed list of values using with_items. (#3556) --- packaging/os/portage.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packaging/os/portage.py b/packaging/os/portage.py index a7f43182cdc..2880902fb9a 100644 --- a/packaging/os/portage.py +++ b/packaging/os/portage.py @@ -434,7 +434,7 @@ def run_emerge(module, packages, *args): def main(): module = AnsibleModule( argument_spec=dict( - package=dict(default=None, aliases=['name']), + package=dict(default=None, aliases=['name'], type='list'), state=dict( default=portage_present_states[0], choices=portage_present_states + portage_absent_states, @@ -475,7 +475,7 @@ def main(): packages = [] if p['package']: - packages.extend(p['package'].split(',')) + packages.extend(p['package']) if p['depclean']: if packages and p['state'] not in portage_absent_states: From ee35620c75e59fcbbe91671794c5a64992ca3416 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 09:54:56 +0000 Subject: [PATCH 2419/2522] Native YAML (#3585) * Native YAML * Fix YAML lists --- monitoring/zabbix_maintenance.py | 60 ++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/monitoring/zabbix_maintenance.py b/monitoring/zabbix_maintenance.py index 89f792ce5d0..247b0a8e717 100644 --- a/monitoring/zabbix_maintenance.py +++ b/monitoring/zabbix_maintenance.py @@ -122,40 +122,48 @@ EXAMPLES = ''' # Create maintenance window named "Update of www1" # for host www1.example.com for 90 minutes -- zabbix_maintenance: name="Update of www1" - host_name=www1.example.com - state=present - minutes=90 - server_url=https://monitoring.example.com - login_user=ansible - login_password=pAsSwOrD +- zabbix_maintenance: + name: Update of www1 + host_name: www1.example.com + state: present + minutes: 90 + server_url: 'https://monitoring.example.com' + login_user: ansible + login_password: pAsSwOrD # Create maintenance window named "Mass update" # for host www1.example.com and host groups Office and Dev -- zabbix_maintenance: name="Update of www1" - host_name=www1.example.com - host_groups=Office,Dev - state=present - server_url=https://monitoring.example.com - login_user=ansible - login_password=pAsSwOrD +- zabbix_maintenance: + name: Update of www1 + host_name: www1.example.com + host_groups: + - Office + - Dev + state: present + server_url: 'https://monitoring.example.com' + login_user: ansible + login_password: pAsSwOrD # Create maintenance window named "update" # for hosts www1.example.com and db1.example.com and without data collection. -- zabbix_maintenance: name=update - host_names=www1.example.com,db1.example.com - state=present - collect_data=false - server_url=https://monitoring.example.com - login_user=ansible - login_password=pAsSwOrD +- zabbix_maintenance: + name: update + host_names: + - www1.example.com + - db1.example.com + state: present + collect_data: false + server_url: 'https://monitoring.example.com' + login_user: ansible + login_password: pAsSwOrD # Remove maintenance window named "Test1" -- zabbix_maintenance: name=Test1 - state=absent - server_url=https://monitoring.example.com - login_user=ansible - login_password=pAsSwOrD +- zabbix_maintenance: + name: Test1 + state: absent + server_url: 'https://monitoring.example.com' + login_user: ansible + login_password: pAsSwOrD ''' import datetime From 37418a347417bdb978c560434f1c91a56de28648 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 10:50:56 +0000 Subject: [PATCH 2420/2522] Native YAML (#3593) --- univention/udm_dns_record.py | 10 ++++++---- univention/udm_dns_zone.py | 11 +++++++---- univention/udm_group.py | 15 +++++++++------ univention/udm_share.py | 9 +++++---- univention/udm_user.py | 33 ++++++++++++++++++--------------- 5 files changed, 45 insertions(+), 33 deletions(-) diff --git a/univention/udm_dns_record.py b/univention/udm_dns_record.py index dab1e134388..5d29b2c3820 100644 --- a/univention/udm_dns_record.py +++ b/univention/udm_dns_record.py @@ -81,10 +81,12 @@ EXAMPLES = ''' # Create a DNS record on a UCS -- udm_dns_zone: name=www - zone=example.com - type=host_record - data=['a': '192.0.2.1'] +- udm_dns_zone: + name: www + zone: example.com + type: host_record + data: + - a: 192.0.2.1 ''' diff --git a/univention/udm_dns_zone.py b/univention/udm_dns_zone.py index baf844b546e..dd576f4e574 100644 --- a/univention/udm_dns_zone.py +++ b/univention/udm_dns_zone.py @@ -103,10 +103,13 @@ EXAMPLES = ''' # Create a DNS zone on a UCS -- udm_dns_zone: zone=example.com - type=forward_zone - nameserver=['ucs.example.com'] - interfaces=['192.0.2.1'] +- udm_dns_zone: + zone: example.com + type: forward_zone + nameserver: + - ucs.example.com + interfaces: + - 192.0.2.1 ''' diff --git a/univention/udm_group.py b/univention/udm_group.py index 588c7655241..1db030efa98 100644 --- a/univention/udm_group.py +++ b/univention/udm_group.py @@ -74,16 +74,19 @@ EXAMPLES = ''' # Create a POSIX group -- udm_group: name=g123m-1A +- udm_group: + name: g123m-1A # Create a POSIX group with the exact DN # C(cn=g123m-1A,cn=classes,cn=students,cn=groups,ou=school,dc=school,dc=example,dc=com) -- udm_group: name=g123m-1A - subpath='cn=classes,cn=students,cn=groups' - ou=school +- udm_group: + name: g123m-1A + subpath: 'cn=classes,cn=students,cn=groups' + ou: school # or -- udm_group: name=g123m-1A - position='cn=classes,cn=students,cn=groups,ou=school,dc=school,dc=example,dc=com' +- udm_group: + name: g123m-1A + position: 'cn=classes,cn=students,cn=groups,ou=school,dc=school,dc=example,dc=com' ''' diff --git a/univention/udm_share.py b/univention/udm_share.py index fa8639958ea..2f7febfdd40 100644 --- a/univention/udm_share.py +++ b/univention/udm_share.py @@ -375,10 +375,11 @@ EXAMPLES = ''' # Create a share named home on the server ucs.example.com with the path /home. -- udm_share: name=home - path=/home - host=ucs.example.com - sambaName=Home +- udm_share: + name: home + path: /home + host: ucs.example.com + sambaName: Home ''' diff --git a/univention/udm_user.py b/univention/udm_user.py index 2eed02a2c04..ecd6e86546b 100644 --- a/univention/udm_user.py +++ b/univention/udm_user.py @@ -318,25 +318,28 @@ EXAMPLES = ''' # Create a user on a UCS -- udm_user: name=FooBar - password=secure_password - firstname=Foo - lastname=Bar +- udm_user: + name: FooBar + password: secure_password + firstname: Foo + lastname: Bar # Create a user with the DN # C(uid=foo,cn=teachers,cn=users,ou=school,dc=school,dc=example,dc=com) -- udm_user: name=foo - password=secure_password - firstname=Foo - lastname=Bar - ou=school - subpath='cn=teachers,cn=users' +- udm_user: + name: foo + password: secure_password + firstname: Foo + lastname: Bar + ou: school + subpath: 'cn=teachers,cn=users' # or define the position -- udm_user: name=foo - password=secure_password - firstname=Foo - lastname=Bar - position='cn=teachers,cn=users,ou=school,dc=school,dc=example,dc=com' +- udm_user: + name: foo + password: secure_password + firstname: Foo + lastname: Bar + position: 'cn=teachers,cn=users,ou=school,dc=school,dc=example,dc=com' ''' From 211c49dece7388c448028e9547bde779b7fd6fc7 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 10:54:07 +0000 Subject: [PATCH 2421/2522] Native YAML (#3573) --- clustering/znode.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/clustering/znode.py b/clustering/znode.py index 6d01b869c58..0e36c0d994b 100644 --- a/clustering/znode.py +++ b/clustering/znode.py @@ -64,19 +64,36 @@ EXAMPLES = """ # Creating or updating a znode with a given value -- action: znode hosts=localhost:2181 name=/mypath value=myvalue state=present +- znode: + hosts: 'localhost:2181' + name: /mypath + value: myvalue + state: present # Getting the value and stat structure for a znode -- action: znode hosts=localhost:2181 name=/mypath op=get +- znode: + hosts: 'localhost:2181' + name: /mypath + op: get # Listing a particular znode's children -- action: znode hosts=localhost:2181 name=/zookeeper op=list +- znode: + hosts: 'localhost:2181' + name: /zookeeper + op: list # Waiting 20 seconds for a znode to appear at path /mypath -- action: znode hosts=localhost:2181 name=/mypath op=wait timeout=20 +- znode: + hosts: 'localhost:2181' + name: /mypath + op: wait + timeout: 20 # Deleting a znode at path /mypath -- action: znode hosts=localhost:2181 name=/mypath state=absent +- znode: + hosts: 'localhost:2181' + name: /mypath + state: absent """ try: From e0c7a6e7a614327d9bd5fa1120b1a9f7b717cb90 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 10:54:59 +0000 Subject: [PATCH 2422/2522] Native YAML (#3574) --- monitoring/pingdom.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/monitoring/pingdom.py b/monitoring/pingdom.py index 4346e8ca6fe..6709c6bc6cd 100644 --- a/monitoring/pingdom.py +++ b/monitoring/pingdom.py @@ -69,18 +69,20 @@ EXAMPLES = ''' # Pause the check with the ID of 12345. -- pingdom: uid=example@example.com - passwd=password123 - key=apipassword123 - checkid=12345 - state=paused +- pingdom: + uid: example@example.com + passwd: password123 + key: apipassword123 + checkid: 12345 + state: paused # Unpause the check with the ID of 12345. -- pingdom: uid=example@example.com - passwd=password123 - key=apipassword123 - checkid=12345 - state=running +- pingdom: + uid: example@example.com + passwd: password123 + key: apipassword123 + checkid: 12345 + state: running ''' try: From 50e3add77a3e348e0723edcc61a6241ef7760f19 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 10:55:28 +0000 Subject: [PATCH 2423/2522] Native YAML (#3575) --- monitoring/uptimerobot.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/monitoring/uptimerobot.py b/monitoring/uptimerobot.py index 65d963cda6d..ec57587416e 100644 --- a/monitoring/uptimerobot.py +++ b/monitoring/uptimerobot.py @@ -53,15 +53,16 @@ EXAMPLES = ''' # Pause the monitor with an ID of 12345. -- uptimerobot: monitorid=12345 - apikey=12345-1234512345 - state=paused +- uptimerobot: + monitorid: 12345 + apikey: 12345-1234512345 + state: paused # Start the monitor with an ID of 12345. -- uptimerobot: monitorid=12345 - apikey=12345-1234512345 - state=started - +- uptimerobot: + monitorid: 12345 + apikey: 12345-1234512345 + state: started ''' try: From 16e7f2e95987fedc6de3824192ee7985b36557ae Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 10:55:49 +0000 Subject: [PATCH 2424/2522] Native YAML (#3576) --- monitoring/honeybadger_deployment.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/monitoring/honeybadger_deployment.py b/monitoring/honeybadger_deployment.py index 3a6d2df7c8c..c9b62f0d439 100644 --- a/monitoring/honeybadger_deployment.py +++ b/monitoring/honeybadger_deployment.py @@ -67,11 +67,12 @@ ''' EXAMPLES = ''' -- honeybadger_deployment: token=AAAAAA - environment='staging' - user='ansible' - revision=b6826b8 - repo=git@github.com:user/repo.git +- honeybadger_deployment: + token: AAAAAA + environment: staging + user: ansible + revision: b6826b8 + repo: 'git@github.com:user/repo.git' ''' RETURN = '''# ''' From eb69519e7cc1e7ec59875505295812faa41fb28c Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 10:57:37 +0000 Subject: [PATCH 2425/2522] Native YAML (#3577) --- monitoring/nagios.py | 87 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 69 insertions(+), 18 deletions(-) diff --git a/monitoring/nagios.py b/monitoring/nagios.py index 689e9f0903c..18db0be0e73 100644 --- a/monitoring/nagios.py +++ b/monitoring/nagios.py @@ -89,56 +89,107 @@ EXAMPLES = ''' # set 30 minutes of apache downtime -- nagios: action=downtime minutes=30 service=httpd host={{ inventory_hostname }} +- nagios: + action: downtime + minutes: 30 + service: httpd + host: '{{ inventory_hostname }}' # schedule an hour of HOST downtime -- nagios: action=downtime minutes=60 service=host host={{ inventory_hostname }} +- nagios: + action: downtime + minutes: 60 + service: host + host: '{{ inventory_hostname }}' # schedule an hour of HOST downtime, with a comment describing the reason -- nagios: action=downtime minutes=60 service=host host={{ inventory_hostname }} - comment='This host needs disciplined' +- nagios: + action: downtime + minutes: 60 + service: host + host: '{{ inventory_hostname }}' + comment: This host needs disciplined # schedule downtime for ALL services on HOST -- nagios: action=downtime minutes=45 service=all host={{ inventory_hostname }} +- nagios: + action: downtime + minutes: 45 + service: all + host: '{{ inventory_hostname }}' # schedule downtime for a few services -- nagios: action=downtime services=frob,foobar,qeuz host={{ inventory_hostname }} +- nagios: + action: downtime + services: frob,foobar,qeuz + host: '{{ inventory_hostname }}' # set 30 minutes downtime for all services in servicegroup foo -- nagios: action=servicegroup_service_downtime minutes=30 servicegroup=foo host={{ inventory_hostname }} +- nagios: + action: servicegroup_service_downtime + minutes: 30 + servicegroup: foo + host: '{{ inventory_hostname }}' # set 30 minutes downtime for all host in servicegroup foo -- nagios: action=servicegroup_host_downtime minutes=30 servicegroup=foo host={{ inventory_hostname }} +- nagios: + action: servicegroup_host_downtime + minutes: 30 + servicegroup: foo + host: '{{ inventory_hostname }}' # delete all downtime for a given host -- nagios: action=delete_downtime host={{ inventory_hostname }} service=all +- nagios: + action: delete_downtime + host: '{{ inventory_hostname }}' + service: all # delete all downtime for HOST with a particular comment -- nagios: action=delete_downtime host={{ inventory_hostname }} service=host comment="Planned maintenance" +- nagios: + action: delete_downtime + host: '{{ inventory_hostname }}' + service: host + comment: Planned maintenance # enable SMART disk alerts -- nagios: action=enable_alerts service=smart host={{ inventory_hostname }} +- nagios: + action: enable_alerts + service: smart + host: '{{ inventory_hostname }}' # "two services at once: disable httpd and nfs alerts" -- nagios: action=disable_alerts service=httpd,nfs host={{ inventory_hostname }} +- nagios: + action: disable_alerts + service: httpd,nfs + host: '{{ inventory_hostname }}' # disable HOST alerts -- nagios: action=disable_alerts service=host host={{ inventory_hostname }} +- nagios: + action: disable_alerts + service: host + host: '{{ inventory_hostname }}' # silence ALL alerts -- nagios: action=silence host={{ inventory_hostname }} +- nagios: + action: silence + host: '{{ inventory_hostname }}' # unsilence all alerts -- nagios: action=unsilence host={{ inventory_hostname }} +- nagios: + action: unsilence + host: '{{ inventory_hostname }}' # SHUT UP NAGIOS -- nagios: action=silence_nagios +- nagios: + action: silence_nagios # ANNOY ME NAGIOS -- nagios: action=unsilence_nagios +- nagios: + action: unsilence_nagios # command something -- nagios: action=command command='DISABLE_FAILURE_PREDICTION' +- nagios: + action: command + command: DISABLE_FAILURE_PREDICTION ''' import ConfigParser From 2f9679460115b07259eb0b3f87516b2024e44613 Mon Sep 17 00:00:00 2001 From: John R Barker Date: Thu, 1 Dec 2016 11:01:54 +0000 Subject: [PATCH 2426/2522] nagios - Better `comment:` example (#3595) --- monitoring/nagios.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/nagios.py b/monitoring/nagios.py index 18db0be0e73..cb7b1ae5699 100644 --- a/monitoring/nagios.py +++ b/monitoring/nagios.py @@ -108,7 +108,7 @@ minutes: 60 service: host host: '{{ inventory_hostname }}' - comment: This host needs disciplined + comment: Rebuilding machine # schedule downtime for ALL services on HOST - nagios: From 9962caffd70ec033cb4f61101037d22e41f6fe99 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 11:02:39 +0000 Subject: [PATCH 2427/2522] Native YAML (#3578) --- monitoring/airbrake_deployment.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/monitoring/airbrake_deployment.py b/monitoring/airbrake_deployment.py index 262c3d2b445..7b36c16a0a3 100644 --- a/monitoring/airbrake_deployment.py +++ b/monitoring/airbrake_deployment.py @@ -65,10 +65,11 @@ ''' EXAMPLES = ''' -- airbrake_deployment: token=AAAAAA - environment='staging' - user='ansible' - revision=4.2 +- airbrake_deployment: + token: AAAAAA + environment: staging + user: ansible + revision: 4.2 ''' import urllib From 77243a6eb9ea8a373ada3057ff8bcb6142d3597a Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 11:03:35 +0000 Subject: [PATCH 2428/2522] Native YAML (#3579) --- monitoring/rollbar_deployment.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/monitoring/rollbar_deployment.py b/monitoring/rollbar_deployment.py index 5db8626f23b..ac7b2c650a2 100644 --- a/monitoring/rollbar_deployment.py +++ b/monitoring/rollbar_deployment.py @@ -68,12 +68,13 @@ ''' EXAMPLES = ''' -- rollbar_deployment: token=AAAAAA - environment='staging' - user='ansible' - revision=4.2, - rollbar_user='admin', - comment='Test Deploy' +- rollbar_deployment: + token: AAAAAA + environment: staging + user: ansible + revision: 4.2 + rollbar_user: admin + comment: Test Deploy ''' import urllib From 04d00f57aba2687b513f986450d5f5f67c0bcf51 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 11:04:16 +0000 Subject: [PATCH 2429/2522] Native YAML (#3580) --- monitoring/newrelic_deployment.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/monitoring/newrelic_deployment.py b/monitoring/newrelic_deployment.py index 87b16e05f39..8b6cc7a83a1 100644 --- a/monitoring/newrelic_deployment.py +++ b/monitoring/newrelic_deployment.py @@ -76,10 +76,11 @@ ''' EXAMPLES = ''' -- newrelic_deployment: token=AAAAAA - app_name=myapp - user='ansible deployment' - revision=1.0 +- newrelic_deployment: + token: AAAAAA + app_name: myapp + user: ansible deployment + revision: 1.0 ''' import urllib From c7bf921c4f27e2cddfe912c81b13ae96c7349d4a Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 11:04:56 +0000 Subject: [PATCH 2430/2522] Native YAML (#3581) --- monitoring/pagerduty.py | 61 ++++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/monitoring/pagerduty.py b/monitoring/pagerduty.py index 99a9be8a044..43b2af94a12 100644 --- a/monitoring/pagerduty.py +++ b/monitoring/pagerduty.py @@ -117,43 +117,54 @@ EXAMPLES=''' # List ongoing maintenance windows using a user/passwd -- pagerduty: name=companyabc user=example@example.com passwd=password123 state=ongoing +- pagerduty: + name: companyabc + user: example@example.com + passwd: password123 + state: ongoing # List ongoing maintenance windows using a token -- pagerduty: name=companyabc token=xxxxxxxxxxxxxx state=ongoing +- pagerduty: + name: companyabc + token: xxxxxxxxxxxxxx + state: ongoing # Create a 1 hour maintenance window for service FOO123, using a user/passwd -- pagerduty: name=companyabc - user=example@example.com - passwd=password123 - state=running - service=FOO123 +- pagerduty: + name: companyabc + user: example@example.com + passwd: password123 + state: running + service: FOO123 # Create a 5 minute maintenance window for service FOO123, using a token -- pagerduty: name=companyabc - token=xxxxxxxxxxxxxx - hours=0 - minutes=5 - state=running - service=FOO123 +- pagerduty: + name: companyabc + token: xxxxxxxxxxxxxx + hours: 0 + minutes: 5 + state: running + service: FOO123 # Create a 4 hour maintenance window for service FOO123 with the description "deployment". -- pagerduty: name=companyabc - user=example@example.com - passwd=password123 - state=running - service=FOO123 - hours=4 - desc=deployment +- pagerduty: + name: companyabc + user: example@example.com + passwd: password123 + state: running + service: FOO123 + hours: 4 + desc: deployment register: pd_window # Delete the previous maintenance window -- pagerduty: name=companyabc - user=example@example.com - passwd=password123 - state=absent - service={{ pd_window.result.maintenance_window.id }} +- pagerduty: + name: companyabc + user: example@example.com + passwd: password123 + state: absent + service: '{{ pd_window.result.maintenance_window.id }}' ''' import datetime From 7f67f7d48244caadcefdc205f8fa249cdbefab54 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 11:05:33 +0000 Subject: [PATCH 2431/2522] Native YAML + add comments (#3582) --- monitoring/logentries.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/monitoring/logentries.py b/monitoring/logentries.py index a347afd84c2..f4e3fef3294 100644 --- a/monitoring/logentries.py +++ b/monitoring/logentries.py @@ -48,8 +48,16 @@ - Requires the LogEntries agent which can be installed following the instructions at logentries.com ''' EXAMPLES = ''' -- logentries: path=/var/log/nginx/access.log state=present name=nginx-access-log -- logentries: path=/var/log/nginx/error.log state=absent +# Track nginx logs +- logentries: + path: /var/log/nginx/access.log + state: present + name: nginx-access-log + +# Stop tracking nginx logs +- logentries: + path: /var/log/nginx/error.log + state: absent ''' def query_log_status(module, le_path, path, state="present"): From 92cc9f6dfc754261ea6f14fa88b859ab382dda00 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 11:06:26 +0000 Subject: [PATCH 2432/2522] Native YAML (#3584) --- monitoring/stackdriver.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/monitoring/stackdriver.py b/monitoring/stackdriver.py index 6b39b0cdf96..251588695d2 100644 --- a/monitoring/stackdriver.py +++ b/monitoring/stackdriver.py @@ -84,9 +84,21 @@ ''' EXAMPLES = ''' -- stackdriver: key=AAAAAA event=deploy deployed_to=production deployed_by=leeroyjenkins repository=MyWebApp revision_id=abcd123 - -- stackdriver: key=AAAAAA event=annotation msg="Greetings from Ansible" annotated_by=leeroyjenkins level=WARN instance_id=i-abcd1234 +- stackdriver: + key: AAAAAA + event: deploy + deployed_to: production + deployed_by: leeroyjenkins + repository: MyWebApp + revision_id: abcd123 + +- stackdriver: + key: AAAAAA + event: annotation + msg: Greetings from Ansible + annotated_by: leeroyjenkins + level: WARN + instance_id: i-abcd1234 ''' # =========================================== From a7e16cd27ad1ae22a4d10011c4be3b1d2e2f45ea Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 11:17:32 +0000 Subject: [PATCH 2433/2522] Native YAML - Network (#3587) * Fix citrix native yaml * Fix snmp native yaml and connectio * Fix more native syntax * More native syntax * Fix openvswitch native syntax * More YAML code ! * More fixes * Improve nmcli --- network/citrix/netscaler.py | 19 ++- network/dnsimple.py | 59 ++++++-- network/dnsmadeeasy.py | 37 ++++- network/haproxy.py | 63 ++++++-- network/illumos/dladm_etherstub.py | 8 +- network/illumos/dladm_vnic.py | 16 ++- network/illumos/flowadm.py | 20 ++- network/illumos/ipadm_if.py | 8 +- network/lldp.py | 3 +- network/nmcli.py | 223 +++++++++++++++++++++-------- network/openvswitch_bridge.py | 17 ++- network/openvswitch_db.py | 16 ++- network/openvswitch_port.py | 37 +++-- network/snmp_facts.py | 23 +-- network/wakeonlan.py | 11 +- 15 files changed, 424 insertions(+), 136 deletions(-) diff --git a/network/citrix/netscaler.py b/network/citrix/netscaler.py index 7954db89b7a..b7465267424 100644 --- a/network/citrix/netscaler.py +++ b/network/citrix/netscaler.py @@ -87,13 +87,26 @@ EXAMPLES = ''' # Disable the server -ansible host -m netscaler -a "nsc_host=nsc.example.com user=apiuser password=apipass" +- netscaler: + nsc_host: nsc.example.com + user: apiuser + password: apipass # Enable the server -ansible host -m netscaler -a "nsc_host=nsc.example.com user=apiuser password=apipass action=enable" +- netscaler: + nsc_host: nsc.example.com + user: apiuser + password: apipass + action: enable # Disable the service local:8080 -ansible host -m netscaler -a "nsc_host=nsc.example.com user=apiuser password=apipass name=local:8080 type=service action=disable" +- netscaler: + nsc_host: nsc.example.com + user: apiuser + password: apipass + name: 'local:8080' + type: service + action: disable ''' diff --git a/network/dnsimple.py b/network/dnsimple.py index de031c0c8a9..17941f496fe 100644 --- a/network/dnsimple.py +++ b/network/dnsimple.py @@ -97,36 +97,67 @@ ''' EXAMPLES = ''' -# authenticate using email and API token -- local_action: dnsimple account_email=test@example.com account_api_token=dummyapitoken - -# fetch all domains -- local_action dnsimple - register: domains +# authenticate using email and API token and fetch all domains +- dnsimple: + account_email: test@example.com + account_api_token: dummyapitoken + delegate_to: localhost # fetch my.com domain records -- local_action: dnsimple domain=my.com state=present +- dnsimple: + domain: my.com + state: present + delegate_to: localhost register: records # delete a domain -- local_action: dnsimple domain=my.com state=absent +- dnsimple: + domain: my.com + state: absent + delegate_to: localhost # create a test.my.com A record to point to 127.0.0.01 -- local_action: dnsimple domain=my.com record=test type=A value=127.0.0.1 +- dnsimple: + domain: my.com + record: test + type: A + value: 127.0.0.1 + delegate_to: localhost register: record # and then delete it -- local_action: dnsimple domain=my.com record_ids={{ record['id'] }} +- dnsimple: + domain: my.com + record_ids: '{{ record["id"] }}' + delegate_to: localhost # create a my.com CNAME record to example.com -- local_action: dnsimple domain=my.com record= type=CNAME value=example.com state=present +- dnsimple + domain: my.com + record: '' + type: CNAME + value: example.com + state: present + delegate_to: localhost # change it's ttl -- local_action: dnsimple domain=my.com record= type=CNAME value=example.com ttl=600 state=present +- dnsimple: + domain: my.com + record: '' + type: CNAME + value: example.com + ttl: 600 + state: present + delegate_to: localhost # and delete the record -- local_action: dnsimpledomain=my.com record= type=CNAME value=example.com state=absent - +- dnsimple: + domain: my.com + record: '' + type: CNAME + value: example.com + state: absent + delegate_to: localhost ''' import os diff --git a/network/dnsmadeeasy.py b/network/dnsmadeeasy.py index a2bbd70cd9b..eac0859c1f5 100644 --- a/network/dnsmadeeasy.py +++ b/network/dnsmadeeasy.py @@ -92,21 +92,48 @@ EXAMPLES = ''' # fetch my.com domain records -- dnsmadeeasy: account_key=key account_secret=secret domain=my.com state=present +- dnsmadeeasy: + account_key: key + account_secret: secret + domain: my.com + state: present register: response # create / ensure the presence of a record -- dnsmadeeasy: account_key=key account_secret=secret domain=my.com state=present record_name="test" record_type="A" record_value="127.0.0.1" +- dnsmadeeasy: + account_key: key + account_secret: secret + domain: my.com + state: present + record_name: test + record_type: A + record_value: 127.0.0.1 # update the previously created record -- dnsmadeeasy: account_key=key account_secret=secret domain=my.com state=present record_name="test" record_value="192.0.2.23" +- dnsmadeeasy: + account_key: key + account_secret: secret + domain: my.com + state: present + record_name: test + record_value: 192.0.2.23 # fetch a specific record -- dnsmadeeasy: account_key=key account_secret=secret domain=my.com state=present record_name="test" +- dnsmadeeasy: + account_key: key + account_secret: secret + domain: my.com + state: present + record_name: test register: response # delete a record / ensure it is absent -- dnsmadeeasy: account_key=key account_secret=secret domain=my.com state=absent record_name="test" +- dnsmadeeasy: + account_key: key + account_secret: secret + domain: my.com + state: absent + record_name: test ''' # ============================================ diff --git a/network/haproxy.py b/network/haproxy.py index 0ab17549466..d1502de6f3b 100644 --- a/network/haproxy.py +++ b/network/haproxy.py @@ -23,6 +23,7 @@ module: haproxy version_added: "1.9" short_description: Enable, disable, and set weights for HAProxy backend servers using socket commands. +author: "Ravi Bhure (@ravibhure)" description: - Enable, disable, and set weights for HAProxy backend servers using socket commands. @@ -97,36 +98,74 @@ EXAMPLES = ''' # disable server in 'www' backend pool -- haproxy: state=disabled host={{ inventory_hostname }} backend=www +- haproxy: + state: disabled + host: '{{ inventory_hostname }}' + backend: www # disable server without backend pool name (apply to all available backend pool) -- haproxy: state=disabled host={{ inventory_hostname }} +- haproxy: + state: disabled + host: '{{ inventory_hostname }}' # disable server, provide socket file -- haproxy: state=disabled host={{ inventory_hostname }} socket=/var/run/haproxy.sock backend=www +- haproxy: + state: disabled + host: '{{ inventory_hostname }}' + socket: /var/run/haproxy.sock + backend: www # disable server, provide socket file, wait until status reports in maintenance -- haproxy: state=disabled host={{ inventory_hostname }} socket=/var/run/haproxy.sock backend=www wait=yes +- haproxy: + state: disabled + host: '{{ inventory_hostname }}' + socket: /var/run/haproxy.sock + backend: www + wait: yes # disable backend server in 'www' backend pool and drop open sessions to it -- haproxy: state=disabled host={{ inventory_hostname }} backend=www socket=/var/run/haproxy.sock shutdown_sessions=true +- haproxy: + state: disabled + host: '{{ inventory_hostname }}' + backend: www + socket: /var/run/haproxy.sock + shutdown_sessions: true # disable server without backend pool name (apply to all available backend pool) but fail when the backend host is not found -- haproxy: state=disabled host={{ inventory_hostname }} fail_on_not_found=yes +- haproxy: + state: disabled + host: '{{ inventory_hostname }}' + fail_on_not_found: yes # enable server in 'www' backend pool -- haproxy: state=enabled host={{ inventory_hostname }} backend=www +- haproxy: + state: enabled + host: '{{ inventory_hostname }}' + backend: www # enable server in 'www' backend pool wait until healthy -- haproxy: state=enabled host={{ inventory_hostname }} backend=www wait=yes +- haproxy: + state: enabled + host: '{{ inventory_hostname }}' + backend: www + wait: yes # enable server in 'www' backend pool wait until healthy. Retry 10 times with intervals of 5 seconds to retrieve the health -- haproxy: state=enabled host={{ inventory_hostname }} backend=www wait=yes wait_retries=10 wait_interval=5 +- haproxy: + state: enabled + host: '{{ inventory_hostname }}' + backend: www + wait: yes + wait_retries: 10 + wait_interval: 5 # enable server in 'www' backend pool with change server(s) weight -- haproxy: state=enabled host={{ inventory_hostname }} socket=/var/run/haproxy.sock weight=10 backend=www - -author: "Ravi Bhure (@ravibhure)" +- haproxy: + state: enabled + host: '{{ inventory_hostname }}' + socket: /var/run/haproxy.sock + weight: 10 + backend: www ''' import socket diff --git a/network/illumos/dladm_etherstub.py b/network/illumos/dladm_etherstub.py index 72b2e6759ff..9e2bf2b1734 100644 --- a/network/illumos/dladm_etherstub.py +++ b/network/illumos/dladm_etherstub.py @@ -49,10 +49,14 @@ EXAMPLES = ''' # Create 'stub0' etherstub -dladm_etherstub: name=stub0 state=present +- dladm_etherstub: + name: stub0 + state: present # Remove 'stub0 etherstub -dladm_etherstub: name=stub0 state=absent +- dladm_etherstub: + name: stub0 + state: absent ''' RETURN = ''' diff --git a/network/illumos/dladm_vnic.py b/network/illumos/dladm_vnic.py index e47b98b97af..4c85cd89afd 100644 --- a/network/illumos/dladm_vnic.py +++ b/network/illumos/dladm_vnic.py @@ -66,13 +66,23 @@ EXAMPLES = ''' # Create 'vnic0' VNIC over 'bnx0' link -dladm_vnic: name=vnic0 link=bnx0 state=present +- dladm_vnic: + name: vnic0 + link: bnx0 + state: present # Create VNIC with specified MAC and VLAN tag over 'aggr0' -dladm_vnic: name=vnic1 link=aggr0 mac=00:00:5E:00:53:23 vlan=4 +- dladm_vnic: + name: vnic1 + link: aggr0 + mac: '00:00:5E:00:53:23' + vlan: 4 # Remove 'vnic0' VNIC -dladm_vnic: name=vnic0 link=bnx0 state=absent +- dladm_vnic: + name: vnic0 + link: bnx0 + state: absent ''' RETURN = ''' diff --git a/network/illumos/flowadm.py b/network/illumos/flowadm.py index 73cc91af442..ec93d6a0f64 100644 --- a/network/illumos/flowadm.py +++ b/network/illumos/flowadm.py @@ -92,13 +92,27 @@ EXAMPLES = ''' # Limit SSH traffic to 100M via vnic0 interface -flowadm: link=vnic0 flow=ssh_out transport=tcp local_port=22 maxbw=100M state=present +- flowadm: + link: vnic0 + flow: ssh_out + transport: tcp + local_port: 22 + maxbw: 100M + state: present # Reset flow properties -flowadm: name=dns state=resetted +- flowadm: + name: dns + state: resetted # Configure policy for EF PHB (DSCP value of 101110 from RFC 2598) with a bandwidth of 500 Mbps and a high priority. -flowadm: link=bge0 dsfield=0x2e:0xfc maxbw=500M priority=high flow=efphb-flow state=present +- flowadm: + link: bge0 + dsfield: '0x2e:0xfc' + maxbw: 500M + priority: high + flow: efphb-flow + state: present ''' RETURN = ''' diff --git a/network/illumos/ipadm_if.py b/network/illumos/ipadm_if.py index c7419848fc0..d2bff90e3bc 100644 --- a/network/illumos/ipadm_if.py +++ b/network/illumos/ipadm_if.py @@ -50,10 +50,14 @@ EXAMPLES = ''' # Create vnic0 interface -ipadm_if: name=vnic0 state=enabled +- ipadm_if: + name: vnic0 + state: enabled # Disable vnic0 interface -ipadm_if: name=vnic0 state=disabled +- ipadm_if: + name: vnic0 + state: disabled ''' RETURN = ''' diff --git a/network/lldp.py b/network/lldp.py index f44afa1cbd5..981aca0f0ed 100644 --- a/network/lldp.py +++ b/network/lldp.py @@ -36,7 +36,8 @@ lldp: - name: Print each switch/port - debug: msg="{{ lldp[item]['chassis']['name'] }} / {{ lldp[item]['port']['ifalias'] }} + debug: + msg: "{{ lldp[item]['chassis']['name'] }} / {{ lldp[item]['port']['ifalias'] }}" with_items: "{{ lldp.keys() }}" # TASK: [Print each switch/port] *********************************************************** diff --git a/network/nmcli.py b/network/nmcli.py index 00bab41760a..a35d90f85a0 100644 --- a/network/nmcli.py +++ b/network/nmcli.py @@ -234,32 +234,75 @@ #Team vars nmcli_team: - - {conn_name: 'tenant', ip4: "{{tenant_ip}}", gw4: "{{tenant_gw}}"} - - {conn_name: 'external', ip4: "{{external_ip}}", gw4: "{{external_gw}}"} - - {conn_name: 'storage', ip4: "{{storage_ip}}", gw4: "{{storage_gw}}"} + - conn_name: tenant + ip4: '{{ tenant_ip }}' + gw4: '{{ tenant_gw }}' + - conn_name: external + ip4: '{{ external_ip }}' + gw4: '{{ external_gw }}' + - conn_name: storage + ip4: '{{ storage_ip }}' + gw4: '{{ storage_gw }}' nmcli_team_slave: - - {conn_name: 'em1', ifname: 'em1', master: 'tenant'} - - {conn_name: 'em2', ifname: 'em2', master: 'tenant'} - - {conn_name: 'p2p1', ifname: 'p2p1', master: 'storage'} - - {conn_name: 'p2p2', ifname: 'p2p2', master: 'external'} + - conn_name: em1 + ifname: em1 + master: tenant + - conn_name: em2 + ifname: em2 + master: tenant + - conn_name: p2p1 + ifname: p2p1 + master: storage + - conn_name: p2p2 + ifname: p2p2 + master: external #bond vars nmcli_bond: - - {conn_name: 'tenant', ip4: "{{tenant_ip}}", gw4: '', mode: 'balance-rr'} - - {conn_name: 'external', ip4: "{{external_ip}}", gw4: '', mode: 'balance-rr'} - - {conn_name: 'storage', ip4: "{{storage_ip}}", gw4: "{{storage_gw}}", mode: 'balance-rr'} + - conn_name: tenant + ip4: '{{ tenant_ip }}' + gw4: '' + mode: balance-rr + - conn_name: external + ip4: '{{ external_ip }}' + gw4: '' + mode: balance-rr + - conn_name: storage + ip4: '{{ storage_ip }}' + gw4: '{{ storage_gw }}' + mode: balance-rr nmcli_bond_slave: - - {conn_name: 'em1', ifname: 'em1', master: 'tenant'} - - {conn_name: 'em2', ifname: 'em2', master: 'tenant'} - - {conn_name: 'p2p1', ifname: 'p2p1', master: 'storage'} - - {conn_name: 'p2p2', ifname: 'p2p2', master: 'external'} + - conn_name: em1 + ifname: em1 + master: tenant + - conn_name: em2 + ifname: em2 + master: tenant + - conn_name: p2p1 + ifname: p2p1 + master: storage + - conn_name: p2p2 + ifname: p2p2 + master: external #ethernet vars nmcli_ethernet: - - {conn_name: 'em1', ifname: 'em1', ip4: "{{tenant_ip}}", gw4: "{{tenant_gw}}"} - - {conn_name: 'em2', ifname: 'em2', ip4: "{{tenant_ip1}}", gw4: "{{tenant_gw}}"} - - {conn_name: 'p2p1', ifname: 'p2p1', ip4: "{{storage_ip}}", gw4: "{{storage_gw}}"} - - {conn_name: 'p2p2', ifname: 'p2p2', ip4: "{{external_ip}}", gw4: "{{external_gw}}"} + - conn_name: em1 + ifname: em1 + ip4: '{{ tenant_ip }}' + gw4: '{{ tenant_gw }}' + - conn_name: em2 + ifname: em2 + ip4: '{{ tenant_ip1 }}' + gw4: '{{ tenant_gw }}' + - conn_name: p2p1 + ifname: p2p1 + ip4: '{{ storage_ip }}' + gw4: '{{ storage_gw }}' + - conn_name: p2p2 + ifname: p2p2 + ip4: '{{ external_ip }}' + gw4: '{{ external_gw }}' ``` ### host_vars @@ -280,42 +323,70 @@ remote_user: root tasks: -- name: install needed network manager libs - yum: name={{ item }} state=installed - with_items: - - NetworkManager-glib - - libnm-qt-devel.x86_64 - - nm-connection-editor.x86_64 - - libsemanage-python - - policycoreutils-python + - name: install needed network manager libs + yum: + name: '{{ item }}' + state: installed + with_items: + - NetworkManager-glib + - libnm-qt-devel.x86_64 + - nm-connection-editor.x86_64 + - libsemanage-python + - policycoreutils-python ##### Working with all cloud nodes - Teaming - name: try nmcli add team - conn_name only & ip4 gw4 - nmcli: type=team conn_name={{item.conn_name}} ip4={{item.ip4}} gw4={{item.gw4}} state=present + nmcli: + type: team + conn_name: '{{ item.conn_name }}' + ip4: '{{ item.ip4 }}' + gw4: '{{ item.gw4 }}' + state: present with_items: - - "{{nmcli_team}}" + - '{{ nmcli_team }}' - name: try nmcli add teams-slave - nmcli: type=team-slave conn_name={{item.conn_name}} ifname={{item.ifname}} master={{item.master}} state=present + nmcli: + type: team-slave + conn_name: '{{ item.conn_name }}' + ifname: '{{ item.ifname }}' + master: '{{ item.master }}' + state: present with_items: - - "{{nmcli_team_slave}}" + - '{{ nmcli_team_slave }}' ###### Working with all cloud nodes - Bonding # - name: try nmcli add bond - conn_name only & ip4 gw4 mode -# nmcli: type=bond conn_name={{item.conn_name}} ip4={{item.ip4}} gw4={{item.gw4}} mode={{item.mode}} state=present +# nmcli: +# type: bond +# conn_name: '{{ item.conn_name }}' +# ip4: '{{ item.ip4 }}' +# gw4: '{{ item.gw4 }}' +# mode: '{{ item.mode }}' +# state: present # with_items: -# - "{{nmcli_bond}}" +# - '{{ nmcli_bond }}' # # - name: try nmcli add bond-slave -# nmcli: type=bond-slave conn_name={{item.conn_name}} ifname={{item.ifname}} master={{item.master}} state=present +# nmcli: +# type: bond-slave +# conn_name: '{{ item.conn_name }}' +# ifname: '{{ item.ifname }}' +# master: '{{ item.master }}' +# state: present # with_items: -# - "{{nmcli_bond_slave}}" +# - '{{ nmcli_bond_slave }}' ##### Working with all cloud nodes - Ethernet # - name: nmcli add Ethernet - conn_name only & ip4 gw4 -# nmcli: type=ethernet conn_name={{item.conn_name}} ip4={{item.ip4}} gw4={{item.gw4}} state=present +# nmcli: +# type: ethernet +# conn_name: '{{ item.conn_name }}' +# ip4: '{{ item.ip4 }}' +# gw4: '{{ item.gw4 }}' +# state: present # with_items: -# - "{{nmcli_ethernet}}" +# - '{{ nmcli_ethernet }}' ``` ## playbook-del.yml example @@ -327,41 +398,77 @@ tasks: - name: try nmcli del team - multiple - nmcli: conn_name={{item.conn_name}} state=absent + nmcli: + conn_name: '{{ item.conn_name }}' + state: absent with_items: - - { conn_name: 'em1'} - - { conn_name: 'em2'} - - { conn_name: 'p1p1'} - - { conn_name: 'p1p2'} - - { conn_name: 'p2p1'} - - { conn_name: 'p2p2'} - - { conn_name: 'tenant'} - - { conn_name: 'storage'} - - { conn_name: 'external'} - - { conn_name: 'team-em1'} - - { conn_name: 'team-em2'} - - { conn_name: 'team-p1p1'} - - { conn_name: 'team-p1p2'} - - { conn_name: 'team-p2p1'} - - { conn_name: 'team-p2p2'} + - conn_name: em1 + - conn_name: em2 + - conn_name: p1p1 + - conn_name: p1p2 + - conn_name: p2p1 + - conn_name: p2p2 + - conn_name: tenant + - conn_name: storage + - conn_name: external + - conn_name: team-em1 + - conn_name: team-em2 + - conn_name: team-p1p1 + - conn_name: team-p1p2 + - conn_name: team-p2p1 + - conn_name: team-p2p2 ``` # To add an Ethernet connection with static IP configuration, issue a command as follows -- nmcli: conn_name=my-eth1 ifname=eth1 type=ethernet ip4=192.0.2.100/24 gw4=192.0.2.1 state=present +- nmcli: + conn_name: my-eth1 + ifname: eth1 + type: ethernet + ip4: 192.0.2.100/24 + gw4: 192.0.2.1 + state: present # To add an Team connection with static IP configuration, issue a command as follows -- nmcli: conn_name=my-team1 ifname=my-team1 type=team ip4=192.0.2.100/24 gw4=192.0.2.1 state=present autoconnect=yes +- nmcli: + conn_name: my-team1 + ifname: my-team1 + type: team + ip4: 192.0.2.100/24 + gw4: 192.0.2.1 + state: present + autoconnect: yes # Optionally, at the same time specify IPv6 addresses for the device as follows: -- nmcli: conn_name=my-eth1 ifname=eth1 type=ethernet ip4=192.0.2.100/24 gw4=192.0.2.1 ip6=2001:db8::cafe gw6=2001:db8::1 state=present +- nmcli: + conn_name: my-eth1 + ifname: eth1 + type: ethernet + ip4: 192.0.2.100/24 + gw4: 192.0.2.1 + ip6: '2001:db8::cafe' + gw6: '2001:db8::1' + state: present # To add two IPv4 DNS server addresses: --nmcli: conn_name=my-eth1 dns4=["192.0.2.53", "198.51.100.53"] state=present +- nmcli: + conn_name: my-eth1 + dns4: + - 192.0.2.53 + - 198.51.100.53 + state: present # To make a profile usable for all compatible Ethernet interfaces, issue a command as follows -- nmcli: ctype=ethernet name=my-eth1 ifname="*" state=present +- nmcli: + ctype: ethernet + name: my-eth1 + ifname: * + state: present # To change the property of a setting e.g. MTU, issue a command as follows: -- nmcli: conn_name=my-eth1 mtu=9000 type=ethernet state=present +- nmcli: + conn_name: my-eth1 + mtu: 9000 + type: ethernet + state: present Exit Status's: - nmcli exits with status 0 if it succeeds, a value greater than 0 is diff --git a/network/openvswitch_bridge.py b/network/openvswitch_bridge.py index b52df601c25..0cbf3096b20 100644 --- a/network/openvswitch_bridge.py +++ b/network/openvswitch_bridge.py @@ -77,16 +77,25 @@ EXAMPLES = ''' # Create a bridge named br-int -- openvswitch_bridge: bridge=br-int state=present +- openvswitch_bridge: + bridge: br-int + state: present # Create a fake bridge named br-int within br-parent on the VLAN 405 -- openvswitch_bridge: bridge=br-int parent=br-parent vlan=405 state=present +- openvswitch_bridge: + bridge: br-int + parent: br-parent + vlan: 405 + state: present # Create an integration bridge -- openvswitch_bridge: bridge=br-int state=present fail_mode=secure +- openvswitch_bridge: + bridge: br-int + state: present + fail_mode: secure args: external_ids: - bridge-id: "br-int" + bridge-id: br-int ''' diff --git a/network/openvswitch_db.py b/network/openvswitch_db.py index e6ec2658e0b..8c2a290a913 100644 --- a/network/openvswitch_db.py +++ b/network/openvswitch_db.py @@ -63,12 +63,20 @@ EXAMPLES = ''' # Increase the maximum idle time to 50 seconds before pruning unused kernel # rules. -- openvswitch_db: table=open_vswitch record=. col=other_config key=max-idle - value=50000 +- openvswitch_db: + table: open_vswitch + record: . + col: other_config + key: max-idle + value: 50000 # Disable in band copy -- openvswitch_db: table=Bridge record=br-int col=other_config - key=disable-in-band value=true +- openvswitch_db: + table: Bridge + record: br-int + col: other_config + key: disable-in-band + value: true ''' diff --git a/network/openvswitch_port.py b/network/openvswitch_port.py index d2bf31a77a0..fc84fe44a3a 100644 --- a/network/openvswitch_port.py +++ b/network/openvswitch_port.py @@ -72,25 +72,38 @@ EXAMPLES = ''' # Creates port eth2 on bridge br-ex -- openvswitch_port: bridge=br-ex port=eth2 state=present - -# Creates port eth6 and set ofport equal to 6. -- openvswitch_port: bridge=bridge-loop port=eth6 state=present - set="Interface eth6 ofport_request=6" +- openvswitch_port: + bridge: br-ex + port: eth2 + state: present + +# Creates port eth6 +- openvswitch_port: + bridge: bridge-loop + port: eth6 + state: present + set: Interface eth6 # Creates port vlan10 with tag 10 on bridge br-ex -- openvswitch_port: bridge=br-ex port=vlan10 tag=10 state=present - set="Interface vlan10 type=internal" +- openvswitch_port: + bridge: br-ex + port: vlan10 + tag: 10 + state: present + set: Interface vlan10 # Assign interface id server1-vifeth6 and mac address 00:00:5E:00:53:23 # to port vifeth6 and setup port to be managed by a controller. -- openvswitch_port: bridge=br-int port=vifeth6 state=present +- openvswitch_port: + bridge: br-int + port: vifeth6 + state: present args: external_ids: - iface-id: "{{inventory_hostname}}-vifeth6" - attached-mac: "00:00:5E:00:53:23" - vm-id: "{{inventory_hostname}}" - iface-status: "active" + iface-id: '{{ inventory_hostname }}-vifeth6' + attached-mac: '00:00:5E:00:53:23' + vm-id: '{{ inventory_hostname }}' + iface-status: active ''' # pylint: disable=W0703 diff --git a/network/snmp_facts.py b/network/snmp_facts.py index 1411b8026a2..b8ae753770d 100644 --- a/network/snmp_facts.py +++ b/network/snmp_facts.py @@ -72,19 +72,22 @@ EXAMPLES = ''' # Gather facts with SNMP version 2 -- snmp_facts: host={{ inventory_hostname }} version=2c community=public - connection: local +- snmp_facts: + host: '{{ inventory_hostname }}' + version: 2c + community: public + delegate_to: local # Gather facts using SNMP version 3 - snmp_facts: - host={{ inventory_hostname }} - version=v3 - level=authPriv - integrity=sha - privacy=aes - username=snmp-user - authkey=abc12345 - privkey=def6789 + host: '{{ inventory_hostname }}' + version: v3 + level: authPriv + integrity: sha + privacy: aes + username: snmp-user + authkey: abc12345 + privkey: def6789 delegate_to: localhost ''' diff --git a/network/wakeonlan.py b/network/wakeonlan.py index e7aa6ee7f47..4e7e7176fec 100644 --- a/network/wakeonlan.py +++ b/network/wakeonlan.py @@ -54,9 +54,14 @@ EXAMPLES = ''' # Send a magic Wake-on-LAN packet to 00:00:5E:00:53:66 -- local_action: wakeonlan mac=00:00:5E:00:53:66 broadcast=192.0.2.23 - -- wakeonlan: mac=00:00:5E:00:53:66 port=9 +- wakeonlan: + mac: '00:00:5E:00:53:66' + broadcast: 192.0.2.23 + delegate_to: loclahost + +- wakeonlan: + mac: 00:00:5E:00:53:66 + port: 9 delegate_to: localhost ''' From 23a4f752fd110078975f5451811dcc9bce3b7c5d Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 11:17:56 +0000 Subject: [PATCH 2434/2522] Native YAML (#3583) --- monitoring/monit.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monitoring/monit.py b/monitoring/monit.py index 2983d5e49af..eafa3e02b77 100644 --- a/monitoring/monit.py +++ b/monitoring/monit.py @@ -53,7 +53,9 @@ EXAMPLES = ''' # Manage the state of program "httpd" to be in "started" state. -- monit: name=httpd state=started +- monit: + name: httpd + state: started ''' def main(): From 3e86fc7077364606b1fd435725953827615b22cc Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 12:26:22 +0000 Subject: [PATCH 2435/2522] bigpanda: Use delegate_to (over local_action) (#3586) * Fix YAML, naed variables * Fix spaces --- monitoring/bigpanda.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/monitoring/bigpanda.py b/monitoring/bigpanda.py index 1365eb6a49e..1efce9580a1 100644 --- a/monitoring/bigpanda.py +++ b/monitoring/bigpanda.py @@ -79,15 +79,34 @@ ''' EXAMPLES = ''' -- bigpanda: component=myapp version=1.3 token={{ bigpanda_token }} state=started +- bigpanda: + component: myapp + version: 1.3 + token: '{{ bigpanda_token }}' + state: started ... -- bigpanda: component=myapp version=1.3 token={{ bigpanda_token }} state=finished - -If outside servers aren't reachable from your machine, use local_action and override hosts: -- local_action: bigpanda component=myapp version=1.3 token={{ bigpanda_token }} hosts={{ansible_hostname}} state=started +- bigpanda: + component: myapp + version: 1.3 + token: '{{ bigpanda_token }}' + state: finished + +# If outside servers aren't reachable from your machine, use delegate_to and override hosts: +- bigpanda: + component: myapp + version: 1.3 + token: '{{ bigpanda_token }}' + hosts: '{{ ansible_hostname }}' + state: started + delegate_to: localhost register: deployment ... -- local_action: bigpanda component=deployment.component version=deployment.version token=deployment.token state=finished +- bigpanda: + component: '{{ deployment.component }}' + version: '{{ deployment.version }}' + token: '{{ deployment.token }}' + state: finished + delegate_to: localhost ''' # =========================================== From eaee713e6406bc60121906035fc02f6afdb9685a Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 12:30:04 +0000 Subject: [PATCH 2436/2522] Native YAML (#3596) --- source_control/bzr.py | 5 ++- source_control/git_config.py | 61 ++++++++++++++++++++++++++-------- source_control/github_hooks.py | 14 ++++++-- 3 files changed, 64 insertions(+), 16 deletions(-) diff --git a/source_control/bzr.py b/source_control/bzr.py index e6cfe9f1ea8..8a75789a11e 100644 --- a/source_control/bzr.py +++ b/source_control/bzr.py @@ -62,7 +62,10 @@ EXAMPLES = ''' # Example bzr checkout from Ansible Playbooks -- bzr: name=bzr+ssh://foosball.example.org/path/to/branch dest=/srv/checkout version=22 +- bzr: + name: 'bzr+ssh://foosball.example.org/path/to/branch' + dest: /srv/checkout + version: 22 ''' import re diff --git a/source_control/git_config.py b/source_control/git_config.py index adf18e99390..7ce01cd9c4e 100644 --- a/source_control/git_config.py +++ b/source_control/git_config.py @@ -72,30 +72,65 @@ EXAMPLES = ''' # Set some settings in ~/.gitconfig -- git_config: name=alias.ci scope=global value=commit -- git_config: name=alias.st scope=global value=status +- git_config: + name: alias.ci + scope: global + value: commit + +- git_config: + name: alias.st + scope: global + value: status # Or system-wide: -- git_config: name=alias.remotev scope=system value="remote -v" -- git_config: name=core.editor scope=global value=vim +- git_config: + name: alias.remotev + scope: system + value: remote -v + +- git_config: + name: core.editor + scope: global + value: vim + # scope=system is the default -- git_config: name=alias.diffc value="diff --cached" -- git_config: name=color.ui value=auto +- git_config: + name: alias.diffc + value: diff --cached + +- git_config: + name: color.ui + value: auto # Make etckeeper not complain when invoked by cron -- git_config: name=user.email repo=/etc scope=local value="root@{{ ansible_fqdn }}" +- git_config: + name: user.email + repo: /etc + scope: local + value: 'root@{{ ansible_fqdn }}' # Read individual values from git config -- git_config: name=alias.ci scope=global -# scope=system is also assumed when reading values, unless list_all=yes -- git_config: name=alias.diffc +- git_config: + name: alias.ci + scope: global + +# scope: system is also assumed when reading values, unless list_all=yes +- git_config: + name: alias.diffc # Read all values from git config -- git_config: list_all=yes scope=global +- git_config: + list_all: yes + scope: global + # When list_all=yes and no scope is specified, you get configuration from all scopes -- git_config: list_all=yes +- git_config: + list_all: yes + # Specify a repository to include local settings -- git_config: list_all=yes repo=/path/to/repo.git +- git_config: + list_all: yes + repo: /path/to/repo.git ''' RETURN = ''' diff --git a/source_control/github_hooks.py b/source_control/github_hooks.py index 8d3c120a787..eec7a6f990d 100644 --- a/source_control/github_hooks.py +++ b/source_control/github_hooks.py @@ -77,10 +77,20 @@ EXAMPLES = ''' # Example creating a new service hook. It ignores duplicates. -- github_hooks: action=create hookurl=http://11.111.111.111:2222 user={{ gituser }} oauthkey={{ oauthkey }} repo=https://api.github.com/repos/pcgentry/Github-Auto-Deploy +- github_hooks: + action: create + hookurl: 'http://11.111.111.111:2222' + user: '{{ gituser }}' + oauthkey: '{{ oauthkey }}' + repo: 'https://api.github.com/repos/pcgentry/Github-Auto-Deploy' # Cleaning all hooks for this repo that had an error on the last update. Since this works for all hooks in a repo it is probably best that this would be called from a handler. -- local_action: github_hooks action=cleanall user={{ gituser }} oauthkey={{ oauthkey }} repo={{ repo }} +- github_hooks: + action: cleanall + user: '{{ gituser }}' + oauthkey: '{{ oauthkey }}' + repo: '{{ repo }}' + delegate_to: localhost ''' def _list(module, hookurl, oauthkey, repo, user): From 3ee11bb691b7b2d618fb158322aff876a5e68b05 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 12:31:20 +0000 Subject: [PATCH 2437/2522] Native YAML - notifications (#3598) --- notification/campfire.py | 14 +++++++++++--- notification/flowdock.py | 26 ++++++++++++++------------ notification/hipchat.py | 9 +++++---- notification/jabber.py | 13 +++++++------ notification/mail.py | 10 +++++----- notification/osx_say.py | 5 ++++- notification/pushover.py | 7 +++++-- notification/telegram.py | 10 ++++------ notification/typetalk.py | 6 +++++- 9 files changed, 60 insertions(+), 40 deletions(-) diff --git a/notification/campfire.py b/notification/campfire.py index 3d003e1363a..9f9c34c5da4 100644 --- a/notification/campfire.py +++ b/notification/campfire.py @@ -60,10 +60,18 @@ ''' EXAMPLES = ''' -- campfire: subscription=foo token=12345 room=123 msg="Task completed." +- campfire: + subscription: foo + token: 12345 + room: 123 + msg: Task completed. -- campfire: subscription=foo token=12345 room=123 notify=loggins - msg="Task completed ... with feeling." +- campfire: + subscription: foo + token: 12345 + room: 123 + notify: loggins + msg: Task completed ... with feeling. ''' import cgi diff --git a/notification/flowdock.py b/notification/flowdock.py index 24fee07af13..1e1e5e9fde0 100644 --- a/notification/flowdock.py +++ b/notification/flowdock.py @@ -89,18 +89,20 @@ ''' EXAMPLES = ''' -- flowdock: type=inbox - token=AAAAAA - from_address=user@example.com - source='my cool app' - msg='test from ansible' - subject='test subject' - -- flowdock: type=chat - token=AAAAAA - external_user_name=testuser - msg='test from ansible' - tags=tag1,tag2,tag3 +- flowdock: + type: inbox + token: AAAAAA + from_address: user@example.com + source: my cool app + msg: test from ansible + subject: test subject + +- flowdock: + type: chat + token: AAAAAA + external_user_name: testuser + msg: test from ansible + tags: tag1,tag2,tag3 ''' import urllib diff --git a/notification/hipchat.py b/notification/hipchat.py index a07042bc3f8..9455c09500c 100644 --- a/notification/hipchat.py +++ b/notification/hipchat.py @@ -81,15 +81,16 @@ ''' EXAMPLES = ''' -- hipchat: room=notify msg="Ansible task finished" +- hipchat: + room: notif + msg: Ansible task finished # Use Hipchat API version 2 - - hipchat: - api: "https://api.hipchat.com/v2/" + api: 'https://api.hipchat.com/v2/' token: OAUTH2_TOKEN room: notify - msg: "Ansible task finished" + msg: Ansible task finished ''' # =========================================== diff --git a/notification/jabber.py b/notification/jabber.py index 02751e93503..d881d28132c 100644 --- a/notification/jabber.py +++ b/notification/jabber.py @@ -78,12 +78,13 @@ msg="Ansible task finished" # send a message, specifying the host and port -- jabber user=mybot@example.net - host=talk.example.net - port=5223 - password=secret - to=mychaps@example.net - msg="Ansible task finished" +- jabber + user: mybot@example.net + host: talk.example.net + port: 5223 + password: secret + to: mychaps@example.net + msg: Ansible task finished ''' import os diff --git a/notification/mail.py b/notification/mail.py index cdd5354b5fa..5cab0d3584a 100644 --- a/notification/mail.py +++ b/notification/mail.py @@ -146,11 +146,11 @@ charset=utf8 # Sending an e-mail using the remote machine, not the Ansible controller node - mail: - host='localhost' - port=25 - to="John Smith " - subject='Ansible-report' - body='System {{ ansible_hostname }} has been successfully provisioned.' + host: localhost + port: 25 + to: 'John Smith ' + subject: Ansible-report + body: 'System {{ ansible_hostname }} has been successfully provisioned.' ''' import os diff --git a/notification/osx_say.py b/notification/osx_say.py index 7c0ba844583..e803bed12f3 100644 --- a/notification/osx_say.py +++ b/notification/osx_say.py @@ -43,7 +43,10 @@ ''' EXAMPLES = ''' -- local_action: osx_say msg="{{inventory_hostname}} is all done" voice=Zarvox +- osx_say: + msg: '{{ inventory_hostname }} is all done' + voice: Zarvox + delegate_to: localhost ''' DEFAULT_VOICE='Trinoids' diff --git a/notification/pushover.py b/notification/pushover.py index 2cd973b1bcc..ac924193fec 100644 --- a/notification/pushover.py +++ b/notification/pushover.py @@ -53,8 +53,11 @@ ''' EXAMPLES = ''' -- local_action: pushover msg="{{inventory_hostname}} has exploded in flames, - It is now time to panic" app_token=wxfdksl user_key=baa5fe97f2c5ab3ca8f0bb59 +- pushover: + msg: '{{ inventory_hostname }} has exploded in flames, It is now time to panic' + app_token: wxfdksl + user_key: baa5fe97f2c5ab3ca8f0bb59 + delegate_to: localhost ''' import urllib diff --git a/notification/telegram.py b/notification/telegram.py index 254a1bf12f2..a12fd46929d 100644 --- a/notification/telegram.py +++ b/notification/telegram.py @@ -51,10 +51,10 @@ EXAMPLES = """ send a message to chat in playbook -- telegram: token=bot9999999:XXXXXXXXXXXXXXXXXXXXXXX - chat_id=000000 - msg="Ansible task finished" - +- telegram: + token: 'bot9999999:XXXXXXXXXXXXXXXXXXXXXXX' + chat_id: 000000 + msg: Ansible task finished """ RETURN = """ @@ -64,8 +64,6 @@ returned: success type: string sample: "Ansible task finished" - - """ import urllib diff --git a/notification/typetalk.py b/notification/typetalk.py index 2f91022936f..4d4cf8a2f8a 100644 --- a/notification/typetalk.py +++ b/notification/typetalk.py @@ -44,7 +44,11 @@ ''' EXAMPLES = ''' -- typetalk: client_id=12345 client_secret=12345 topic=1 msg="install completed" +- typetalk: + client_id: 12345 + client_secret: 12345 + topic: 1 + msg: install completed ''' import urllib From b3dd2928dd5fa94ba52b8f18e5b9b99dd1304721 Mon Sep 17 00:00:00 2001 From: Shinichi TAMURA Date: Thu, 1 Dec 2016 21:47:55 +0900 Subject: [PATCH 2438/2522] timezone: Fix TypeError closes #3337 --- system/timezone.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/timezone.py b/system/timezone.py index a6b74c570f5..3c920c4bff5 100644 --- a/system/timezone.py +++ b/system/timezone.py @@ -310,8 +310,8 @@ def __init__(self, module): # Debian/Ubuntu self.update_timezone = self.module.get_bin_path('dpkg-reconfigure', required=True) self.update_timezone += ' --frontend noninteractive tzdata' - self.conf_files['name'] = '/etc/timezone', - self.conf_files['hwclock'] = '/etc/default/rcS', + self.conf_files['name'] = '/etc/timezone' + self.conf_files['hwclock'] = '/etc/default/rcS' self.regexps['name'] = re.compile(r'^([^\s]+)', re.MULTILINE) self.tzline_format = '%s\n' else: From 1edda31110686907e49e7d1bdee03a70f4cd1e8e Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Thu, 1 Dec 2016 14:16:18 +0100 Subject: [PATCH 2439/2522] Bugfix for newer policycoreutils-python (eg. RHEL7) (#3569) The policycoreutils python API for RHEL6 and RHEL7 are sufficiently different, requiring some additional definitions and specific conversion that works on old and new implementations. It also implements a fix for non-ascii error messages (like when using a French locale configuration). This fixes #3551. --- system/sefcontext.py | 44 ++++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/system/sefcontext.py b/system/sefcontext.py index 6977ec622e9..96f576c064f 100644 --- a/system/sefcontext.py +++ b/system/sefcontext.py @@ -81,6 +81,7 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils._text import to_native try: import selinux @@ -94,21 +95,35 @@ except ImportError: HAVE_SEOBJECT=False +### Add missing entries (backward compatible) +seobject.file_types.update(dict( + a = seobject.SEMANAGE_FCONTEXT_ALL, + b = seobject.SEMANAGE_FCONTEXT_BLOCK, + c = seobject.SEMANAGE_FCONTEXT_CHAR, + d = seobject.SEMANAGE_FCONTEXT_DIR, + f = seobject.SEMANAGE_FCONTEXT_REG, + l = seobject.SEMANAGE_FCONTEXT_LINK, + p = seobject.SEMANAGE_FCONTEXT_PIPE, + s = seobject.SEMANAGE_FCONTEXT_SOCK, +)) + ### Make backward compatible -option_to_file_type_str = { - 'a': 'all files', - 'b': 'block device', - 'c': 'character device', - 'd': 'directory', - 'f': 'regular file', - 'l': 'symbolic link', - 's': 'socket file', - 'p': 'named pipe', -} +option_to_file_type_str = dict( + a = 'all files', + b = 'block device', + c = 'character device', + d = 'directory', + f = 'regular file', + l = 'symbolic link', + p = 'named pipe', + s = 'socket file', +) def semanage_fcontext_exists(sefcontext, target, ftype): ''' Get the SELinux file context mapping definition from policy. Return None if it does not exist. ''' - record = (target, ftype) + + # Beware that records comprise of a string representation of the file_type + record = (target, option_to_file_type_str[ftype]) records = sefcontext.get_all() try: return records[record] @@ -160,7 +175,7 @@ def semanage_fcontext_modify(module, result, target, ftype, setype, do_reload, s except Exception: e = get_exception() - module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) + module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, to_native(e))) if module._diff and prepared_diff: result['diff'] = dict(prepared=prepared_diff) @@ -191,7 +206,7 @@ def semanage_fcontext_delete(module, result, target, ftype, do_reload, sestore=' except Exception: e = get_exception() - module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, str(e))) + module.fail_json(msg="%s: %s\n" % (e.__class__.__name__, to_native(e))) if module._diff and prepared_diff: result['diff'] = dict(prepared=prepared_diff) @@ -231,9 +246,6 @@ def main(): result = dict(target=target, ftype=ftype, setype=setype, state=state) - # Convert file types to (internally used) strings - ftype = option_to_file_type_str[ftype] - if state == 'present': semanage_fcontext_modify(module, result, target, ftype, setype, do_reload, serange, seuser) elif state == 'absent': From e7064125095503bffb8b11a2473ed285d257197d Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 13:58:22 +0000 Subject: [PATCH 2440/2522] Native YAML - Web infrastructure (#3594) * Native YAML * YAML for jira as well * Native YAML for Jboss * Fix missing spaces --- web_infrastructure/apache2_mod_proxy.py | 36 +++++-- web_infrastructure/deploy_helper.py | 120 +++++++++++++++++------- web_infrastructure/ejabberd_user.py | 19 ++-- web_infrastructure/jboss.py | 16 +++- web_infrastructure/jira.py | 98 ++++++++++++------- 5 files changed, 203 insertions(+), 86 deletions(-) diff --git a/web_infrastructure/apache2_mod_proxy.py b/web_infrastructure/apache2_mod_proxy.py index 0117c118bbd..34bc0c80e45 100644 --- a/web_infrastructure/apache2_mod_proxy.py +++ b/web_infrastructure/apache2_mod_proxy.py @@ -71,21 +71,41 @@ EXAMPLES = ''' # Get all current balancer pool members' attributes: -- apache2_mod_proxy: balancer_vhost=10.0.0.2 +- apache2_mod_proxy: + balancer_vhost: 10.0.0.2 # Get a specific member's attributes: -- apache2_mod_proxy: balancer_vhost=myws.mydomain.org balancer_suffix="/lb/" member_host=node1.myws.mydomain.org +- apache2_mod_proxy: + balancer_vhost: myws.mydomain.org + balancer_suffix: /lb/ + member_host: node1.myws.mydomain.org # Enable all balancer pool members: -- apache2_mod_proxy: balancer_vhost="{{ myloadbalancer_host }}" +- apache2_mod_proxy: + balancer_vhost: '{{ myloadbalancer_host }}' register: result -- apache2_mod_proxy: balancer_vhost="{{ myloadbalancer_host }}" member_host="{{ item.host }}" state=present - with_items: "{{ result.members }}" +- apache2_mod_proxy: + balancer_vhost: '{{ myloadbalancer_host }}' + member_host: '{{ item.host }}' + state: present + with_items: '{{ result.members }}' # Gracefully disable a member from a loadbalancer node: -- apache2_mod_proxy: balancer_vhost="{{ vhost_host }}" member_host="{{ member.host }}" state=drained delegate_to=myloadbalancernode -- wait_for: host="{{ member.host }}" port={{ member.port }} state=drained delegate_to=myloadbalancernode -- apache2_mod_proxy: balancer_vhost="{{ vhost_host }}" member_host="{{ member.host }}" state=absent delegate_to=myloadbalancernode +- apache2_mod_proxy: + balancer_vhost: '{{ vhost_host }}' + member_host: '{{ member.host }}' + state: drained + delegate_to: myloadbalancernode +- wait_for: + host: '{{ member.host }}' + port: '{{ member.port }}' + state: drained + delegate_to: myloadbalancernode +- apache2_mod_proxy: + balancer_vhost: '{{ vhost_host }}' + member_host: '{{ member.host }}' + state: absent + delegate_to: myloadbalancernode ''' RETURN = ''' diff --git a/web_infrastructure/deploy_helper.py b/web_infrastructure/deploy_helper.py index b956e38d260..6fbdd14151d 100644 --- a/web_infrastructure/deploy_helper.py +++ b/web_infrastructure/deploy_helper.py @@ -163,69 +163,121 @@ # Typical usage: - name: Initialize the deploy root and gather facts - deploy_helper: path=/path/to/root + deploy_helper: + path: /path/to/root - name: Clone the project to the new release folder - git: repo=git://foosball.example.org/path/to/repo.git dest={{ deploy_helper.new_release_path }} version=v1.1.1 + git: + repo: 'git://foosball.example.org/path/to/repo.git' + dest: '{{ deploy_helper.new_release_path }}' + version: 'v1.1.1' - name: Add an unfinished file, to allow cleanup on successful finalize - file: path={{ deploy_helper.new_release_path }}/{{ deploy_helper.unfinished_filename }} state=touch + file: + path: '{{ deploy_helper.new_release_path }}/{{ deploy_helper.unfinished_filename }}' + state: touch - name: Perform some build steps, like running your dependency manager for example - composer: command=install working_dir={{ deploy_helper.new_release_path }} + composer: + command: install + working_dir: '{{ deploy_helper.new_release_path }}' - name: Create some folders in the shared folder - file: path='{{ deploy_helper.shared_path }}/{{ item }}' state=directory - with_items: ['sessions', 'uploads'] + file: + path: '{{ deploy_helper.shared_path }}/{{ item }}' + state: directory + with_items: + - sessions + - uploads - name: Add symlinks from the new release to the shared folder - file: path='{{ deploy_helper.new_release_path }}/{{ item.path }}' - src='{{ deploy_helper.shared_path }}/{{ item.src }}' - state=link + file: + path: '{{ deploy_helper.new_release_path }}/{{ item.path }}' + src: '{{ deploy_helper.shared_path }}/{{ item.src }}' + state: link with_items: - - { path: "app/sessions", src: "sessions" } - - { path: "web/uploads", src: "uploads" } + - path: app/sessions + src: sessions + - path: web/uploads + src: uploads - name: Finalize the deploy, removing the unfinished file and switching the symlink - deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=finalize + deploy_helper: + path: /path/to/root + release: '{{ deploy_helper.new_release }}' + state: finalize # Retrieving facts before running a deploy - name: Run 'state=query' to gather facts without changing anything - deploy_helper: path=/path/to/root state=query + deploy_helper: + path: /path/to/root + state: query # Remember to set the 'release' parameter when you actually call 'state=present' later - name: Initialize the deploy root - deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=present + deploy_helper: + path: /path/to/root + release: '{{ deploy_helper.new_release }}' + state: present # all paths can be absolute or relative (to the 'path' parameter) -- deploy_helper: path=/path/to/root - releases_path=/var/www/project/releases - shared_path=/var/www/shared - current_path=/var/www/active +- deploy_helper: + path: /path/to/root + releases_path: /var/www/project/releases + shared_path: /var/www/shared + current_path: /var/www/active # Using your own naming strategy for releases (a version tag in this case): -- deploy_helper: path=/path/to/root release=v1.1.1 state=present -- deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=finalize +- deploy_helper: + path: /path/to/root + release: 'v1.1.1' + state: present +- deploy_helper: + path: /path/to/root + release: '{{ deploy_helper.new_release }}' + state: finalize # Using a different unfinished_filename: -- deploy_helper: path=/path/to/root - unfinished_filename=README.md - release={{ deploy_helper.new_release }} - state=finalize +- deploy_helper: + path: /path/to/root + unfinished_filename: README.md + release: '{{ deploy_helper.new_release }}' + state: finalize # Postponing the cleanup of older builds: -- deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=finalize clean=False -- deploy_helper: path=/path/to/root state=clean +- deploy_helper: + path: /path/to/root + release: '{{ deploy_helper.new_release }}' + state: finalize + clean: False +- deploy_helper: + path: /path/to/root + state: clean # Or running the cleanup ahead of the new deploy -- deploy_helper: path=/path/to/root state=clean -- deploy_helper: path=/path/to/root state=present +- deploy_helper: + path: /path/to/root + state: clean +- deploy_helper: + path: /path/to/root + state: present # Keeping more old releases: -- deploy_helper: path=/path/to/root release={{ deploy_helper.new_release }} state=finalize keep_releases=10 +- deploy_helper: + path: /path/to/root + release: '{{ deploy_helper.new_release }}' + state: finalize + keep_releases: 10 # Or, if you use 'clean=false' on finalize: -- deploy_helper: path=/path/to/root state=clean keep_releases=10 +- deploy_helper: + path: /path/to/root + state: clean + keep_releases: 10 # Removing the entire project root folder -- deploy_helper: path=/path/to/root state=absent +- deploy_helper: + path: /path/to/root + state: absent # Debugging the facts returned by the module -- deploy_helper: path=/path/to/root -- debug: var=deploy_helper - +- deploy_helper: + path: /path/to/root +- debug: + var: deploy_helper ''' + # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.pycompat24 import get_exception diff --git a/web_infrastructure/ejabberd_user.py b/web_infrastructure/ejabberd_user.py index e89918a2486..0f47167f23d 100644 --- a/web_infrastructure/ejabberd_user.py +++ b/web_infrastructure/ejabberd_user.py @@ -59,14 +59,19 @@ EXAMPLES = ''' Example playbook entries using the ejabberd_user module to manage users state. - tasks: - - - name: create a user if it does not exists - action: ejabberd_user username=test host=server password=password - - - name: delete a user if it exists - action: ejabberd_user username=test host=server state=absent +- name: create a user if it does not exists + ejabberd_user: + username: test + host: server + password: password + +- name: delete a user if it exists + ejabberd_user: + username: test + host: server + state: absent ''' + import syslog from ansible.module_utils.pycompat24 import get_exception from ansible.module_utils.basic import * diff --git a/web_infrastructure/jboss.py b/web_infrastructure/jboss.py index 9ec67b7c7b1..53ffcf1f840 100644 --- a/web_infrastructure/jboss.py +++ b/web_infrastructure/jboss.py @@ -52,11 +52,21 @@ EXAMPLES = """ # Deploy a hello world application -- jboss: src=/tmp/hello-1.0-SNAPSHOT.war deployment=hello.war state=present +- jboss: + src: /tmp/hello-1.0-SNAPSHOT.war + deployment: hello.war + state: present + # Update the hello world application -- jboss: src=/tmp/hello-1.1-SNAPSHOT.war deployment=hello.war state=present +- jboss: + src: /tmp/hello-1.1-SNAPSHOT.war + deployment: hello.war + state: present + # Undeploy the hello world application -- jboss: deployment=hello.war state=absent +- jboss: + deployment: hello.war + state: absent """ import os diff --git a/web_infrastructure/jira.py b/web_infrastructure/jira.py index 85b988d24bc..479e623b3c1 100755 --- a/web_infrastructure/jira.py +++ b/web_infrastructure/jira.py @@ -105,59 +105,89 @@ EXAMPLES = """ # Create a new issue and add a comment to it: - name: Create an issue - jira: uri={{server}} username={{user}} password={{pass}} - project=ANS operation=create - summary="Example Issue" description="Created using Ansible" issuetype=Task + jira: + uri: '{{ server }}' + username: '{{ user }}' + password: '{{ pass }}' + project: ANS + operation: create + summary: Example Issue + description: Created using Ansible + issuetype: Task register: issue - name: Comment on issue - jira: uri={{server}} username={{user}} password={{pass}} - issue={{issue.meta.key}} operation=comment - comment="A comment added by Ansible" + jira: + uri: '{{ server }}' + username: '{{ user }}' + password: '{{ pass }}' + issue: '{{ issue.meta.key }}' + operation: comment + comment: A comment added by Ansible # Assign an existing issue using edit - name: Assign an issue using free-form fields - jira: uri={{server}} username={{user}} password={{pass}} - issue={{issue.meta.key}} operation=edit - assignee=ssmith + jira: + uri: '{{ server }}' + username: '{{ user }}' + password: '{{ pass }}' + issue: '{{ issue.meta.key}}' + operation: edit + assignee: ssmith # Create an issue with an existing assignee - name: Create an assigned issue - jira: uri={{server}} username={{user}} password={{pass}} - project=ANS operation=create - summary="Assigned issue" description="Created and assigned using Ansible" - issuetype=Task assignee=ssmith - -# Edit an issue using free-form fields + jira: + uri: '{{ server }}' + username: '{{ user }}' + password: '{{ pass }}' + project: ANS + operation: create + summary: Assigned issue + description: Created and assigned using Ansible + issuetype: Task + assignee: ssmith + +# Edit an issue - name: Set the labels on an issue using free-form fields - jira: uri={{server}} username={{user}} password={{pass}} - issue={{issue.meta.key}} operation=edit - args: { fields: {labels: ["autocreated", "ansible"]}} - -- name: Set the labels on an issue, YAML version - jira: uri={{server}} username={{user}} password={{pass}} - issue={{issue.meta.key}} operation=edit - args: - fields: - labels: - - "autocreated" - - "ansible" - - "yaml" + jira: + uri: '{{ server }}' + username: '{{ user }}' + password: '{{ pass }}' + issue: '{{ issue.meta.key }}' + operation: edit + args: + fields: + labels: + - autocreated + - ansible # Retrieve metadata for an issue and use it to create an account - name: Get an issue - jira: uri={{server}} username={{user}} password={{pass}} - project=ANS operation=fetch issue="ANS-63" + jira: + uri: '{{ server }}' + username: '{{ user }}' + password: '{{ pass }}' + project: ANS + operation: fetch + issue: ANS-63 register: issue - name: Create a unix account for the reporter - sudo: true - user: name="{{issue.meta.fields.creator.name}}" comment="{{issue.meta.fields.creator.displayName}}" + become: true + user: + name: '{{ issue.meta.fields.creator.name }}' + comment: '{{issue.meta.fields.creator.displayName }}' # Transition an issue by target status - name: Close the issue - jira: uri={{server}} username={{user}} password={{pass}} - issue={{issue.meta.key}} operation=transition status="Done" + jira: + uri: '{{ server }}' + username: '{{ user }}' + password: '{{ pass }}' + issue: '{{ issue.meta.key }}' + operation: transition + status: Done """ try: From 061589187056fe786e5ed010a2e771f5b98f669e Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 13:59:53 +0000 Subject: [PATCH 2441/2522] Native YAML - messaging (#3599) --- messaging/rabbitmq_binding.py | 12 ++++++++++-- messaging/rabbitmq_exchange.py | 8 ++++++-- messaging/rabbitmq_parameter.py | 9 +++++---- messaging/rabbitmq_plugin.py | 4 +++- messaging/rabbitmq_policy.py | 12 +++++++++--- messaging/rabbitmq_queue.py | 9 +++++++-- messaging/rabbitmq_user.py | 28 +++++++++++++++++----------- messaging/rabbitmq_vhost.py | 4 +++- 8 files changed, 60 insertions(+), 26 deletions(-) diff --git a/messaging/rabbitmq_binding.py b/messaging/rabbitmq_binding.py index c1ca32a1ce4..bc466388acc 100644 --- a/messaging/rabbitmq_binding.py +++ b/messaging/rabbitmq_binding.py @@ -94,10 +94,18 @@ EXAMPLES = ''' # Bind myQueue to directExchange with routing key info -- rabbitmq_binding: name=directExchange destination=myQueue type=queue routing_key=info +- rabbitmq_binding: + name: directExchange + destination: myQueue + type: queue + routing_key: info # Bind directExchange to topicExchange with routing key *.info -- rabbitmq_binding: name=topicExchange destination=topicExchange type=exchange routing_key="*.info" +- rabbitmq_binding: + name: topicExchange + destination: topicExchange + type: exchange + routing_key: *.info ''' import requests diff --git a/messaging/rabbitmq_exchange.py b/messaging/rabbitmq_exchange.py index 7b55b5c6836..836db467bae 100644 --- a/messaging/rabbitmq_exchange.py +++ b/messaging/rabbitmq_exchange.py @@ -100,10 +100,14 @@ EXAMPLES = ''' # Create direct exchange -- rabbitmq_exchange: name=directExchange +- rabbitmq_exchange: + name: directExchange # Create topic exchange on vhost -- rabbitmq_exchange: name=topicExchange type=topic vhost=myVhost +- rabbitmq_exchange: + name: topicExchange + type: topic + vhost: myVhost ''' import requests diff --git a/messaging/rabbitmq_parameter.py b/messaging/rabbitmq_parameter.py index 9022910928b..602f92fc4c4 100644 --- a/messaging/rabbitmq_parameter.py +++ b/messaging/rabbitmq_parameter.py @@ -63,10 +63,11 @@ EXAMPLES = """ # Set the federation parameter 'local_username' to a value of 'guest' (in quotes) -- rabbitmq_parameter: component=federation - name=local-username - value='"guest"' - state=present +- rabbitmq_parameter: + component: federation + name: local-username + value: '"guest"' + state: present """ class RabbitMqParameter(object): diff --git a/messaging/rabbitmq_plugin.py b/messaging/rabbitmq_plugin.py index b52de337e2e..6aa4fac3053 100644 --- a/messaging/rabbitmq_plugin.py +++ b/messaging/rabbitmq_plugin.py @@ -56,7 +56,9 @@ EXAMPLES = ''' # Enables the rabbitmq_management plugin -- rabbitmq_plugin: names=rabbitmq_management state=enabled +- rabbitmq_plugin: + names: rabbitmq_management + state: enabled ''' import os diff --git a/messaging/rabbitmq_policy.py b/messaging/rabbitmq_policy.py index a9207b3cbcd..8293f6f8972 100644 --- a/messaging/rabbitmq_policy.py +++ b/messaging/rabbitmq_policy.py @@ -74,13 +74,19 @@ EXAMPLES = ''' - name: ensure the default vhost contains the HA policy via a dict - rabbitmq_policy: name=HA pattern='.*' + rabbitmq_policy: + name: HA + pattern: .* args: tags: - "ha-mode": all + ha-mode: all - name: ensure the default vhost contains the HA policy - rabbitmq_policy: name=HA pattern='.*' tags="ha-mode=all" + rabbitmq_policy: + name: HA + pattern: .* + tags: + - ha-mode: all ''' class RabbitMqPolicy(object): def __init__(self, module, name): diff --git a/messaging/rabbitmq_queue.py b/messaging/rabbitmq_queue.py index afdd410349b..7b0b69affe4 100644 --- a/messaging/rabbitmq_queue.py +++ b/messaging/rabbitmq_queue.py @@ -114,10 +114,15 @@ EXAMPLES = ''' # Create a queue -- rabbitmq_queue: name=myQueue +- rabbitmq_queue: + name: myQueue # Create a queue on remote host -- rabbitmq_queue: name=myRemoteQueue login_user=user login_password=secret login_host=remote.example.org +- rabbitmq_queue: + name: myRemoteQueue + login_user: user + login_password: secret + login_host: remote.example.org ''' import requests diff --git a/messaging/rabbitmq_user.py b/messaging/rabbitmq_user.py index 103650e2c94..470ae7c1431 100644 --- a/messaging/rabbitmq_user.py +++ b/messaging/rabbitmq_user.py @@ -107,20 +107,26 @@ EXAMPLES = ''' # Add user to server and assign full access control on / vhost. # The user might have permission rules for other vhost but you don't care. -- rabbitmq_user: user=joe - password=changeme - vhost=/ - configure_priv=.* - read_priv=.* - write_priv=.* - state=present +- rabbitmq_user: + user: joe + password: changeme + vhost: / + configure_priv: .* + read_priv: .* + write_priv: .* + state: present # Add user to server and assign full access control on / vhost. # The user doesn't have permission rules for other vhosts -- rabbitmq_user: user=joe - password=changeme - permissions=[{vhost='/', configure_priv='.*', read_priv='.*', write_priv='.*'}] - state=present +- rabbitmq_user: + user: joe + password: changeme + permissions: + - vhost: / + configure_priv: .* + read_priv: .* + write_priv: .* + state: present ''' class RabbitMqUser(object): diff --git a/messaging/rabbitmq_vhost.py b/messaging/rabbitmq_vhost.py index dbde32393cb..1ffb3d2674f 100644 --- a/messaging/rabbitmq_vhost.py +++ b/messaging/rabbitmq_vhost.py @@ -55,7 +55,9 @@ EXAMPLES = ''' # Ensure that the vhost /test exists. -- rabbitmq_vhost: name=/test state=present +- rabbitmq_vhost: + name: /test + state: present ''' class RabbitMqVhost(object): From ac3411dad295d89e2b454e1170865e978a98485c Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 14:00:17 +0000 Subject: [PATCH 2442/2522] Native YAML - remote_management (#3601) --- remote_management/ipmi/ipmi_boot.py | 14 ++++++++++++-- remote_management/ipmi/ipmi_power.py | 6 +++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/remote_management/ipmi/ipmi_boot.py b/remote_management/ipmi/ipmi_boot.py index e8f13d8bd7c..6c0c9cdc12a 100644 --- a/remote_management/ipmi/ipmi_boot.py +++ b/remote_management/ipmi/ipmi_boot.py @@ -110,9 +110,19 @@ EXAMPLES = ''' # Ensure bootdevice is HD. -- ipmi_boot: name="test.testdomain.com" user="admin" password="password" bootdev="hd" +- ipmi_boot: + name: test.testdomain.com + user: admin + password: password + bootdev: hd + # Ensure bootdevice is not Network -- ipmi_boot: name="test.testdomain.com" user="admin" password="password" bootdev="network" state=absent +- ipmi_boot: + name: test.testdomain.com + user: admin + password: password + bootdev: network + state: absent ''' # ================================================== diff --git a/remote_management/ipmi/ipmi_power.py b/remote_management/ipmi/ipmi_power.py index c6cc8df0301..fc702fa1def 100644 --- a/remote_management/ipmi/ipmi_power.py +++ b/remote_management/ipmi/ipmi_power.py @@ -83,7 +83,11 @@ EXAMPLES = ''' # Ensure machine is powered on. -- ipmi_power: name="test.testdomain.com" user="admin" password="password" state="on" +- ipmi_power: + name: test.testdomain.com + user: admin + password: password + state: on ''' # ================================================== From 8f84a8f027a5e40bbe510e93eca9c404adf4c2a0 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 14:01:59 +0000 Subject: [PATCH 2443/2522] Native YAML - Database/musc (#3603) --- database/misc/mongodb_parameter.py | 5 +++- database/misc/mongodb_user.py | 47 +++++++++++++++++++++++++----- database/misc/redis.py | 28 ++++++++++++++---- database/misc/riak.py | 10 +++++-- 4 files changed, 73 insertions(+), 17 deletions(-) diff --git a/database/misc/mongodb_parameter.py b/database/misc/mongodb_parameter.py index bd8192fe25c..8dafeea179e 100644 --- a/database/misc/mongodb_parameter.py +++ b/database/misc/mongodb_parameter.py @@ -92,7 +92,10 @@ EXAMPLES = ''' # Set MongoDB syncdelay to 60 (this is an int) -- mongodb_parameter: param="syncdelay" value=60 param_type="int" +- mongodb_parameter: + param: syncdelay + value: 60 + param_type: int ''' RETURN = ''' diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index 1d1157b15a7..a022d6aa9a8 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -118,21 +118,54 @@ EXAMPLES = ''' # Create 'burgers' database user with name 'bob' and password '12345'. -- mongodb_user: database=burgers name=bob password=12345 state=present +- mongodb_user: + database: burgers + name: bob + password: 12345 + state: present # Create a database user via SSL (MongoDB must be compiled with the SSL option and configured properly) -- mongodb_user: database=burgers name=bob password=12345 state=present ssl=True +- mongodb_user: + database: burgers + name: bob + password: 12345 + state: present + ssl: True # Delete 'burgers' database user with name 'bob'. -- mongodb_user: database=burgers name=bob state=absent +- mongodb_user: + database: burgers + name: bob + state: absent # Define more users with various specific roles (if not defined, no roles is assigned, and the user will be added via pre mongo 2.2 style) -- mongodb_user: database=burgers name=ben password=12345 roles='read' state=present -- mongodb_user: database=burgers name=jim password=12345 roles='readWrite,dbAdmin,userAdmin' state=present -- mongodb_user: database=burgers name=joe password=12345 roles='readWriteAnyDatabase' state=present +- mongodb_user: + database: burgers + name: ben + password: 12345 + roles: read + state: present +- mongodb_user: + database: burgers + name: jim + password: 12345 + roles: readWrite,dbAdmin,userAdmin + state: present +- mongodb_user: + database: burgers + name: joe + password: 12345 + roles: readWriteAnyDatabase + state: present # add a user to database in a replica set, the primary server is automatically discovered and written to -- mongodb_user: database=burgers name=bob replica_set=belcher password=12345 roles='readWriteAnyDatabase' state=present +- mongodb_user: + database: burgers + name: bob + replica_set: belcher + password: 12345 + roles: readWriteAnyDatabase + state: present # add a user 'oplog_reader' with read only access to the 'local' database on the replica_set 'belcher'. This is usefull for oplog access (MONGO_OPLOG_URL). # please notice the credentials must be added to the 'admin' database because the 'local' database is not syncronized and can't receive user credentials diff --git a/database/misc/redis.py b/database/misc/redis.py index 20d16de4530..49d9cc23a7d 100644 --- a/database/misc/redis.py +++ b/database/misc/redis.py @@ -103,22 +103,38 @@ EXAMPLES = ''' # Set local redis instance to be slave of melee.island on port 6377 -- redis: command=slave master_host=melee.island master_port=6377 +- redis: + command: slave + master_host: melee.island + master_port: 6377 # Deactivate slave mode -- redis: command=slave slave_mode=master +- redis: + command: slave + slave_mode: master # Flush all the redis db -- redis: command=flush flush_mode=all +- redis: + command: flush + flush_mode: all # Flush only one db in a redis instance -- redis: command=flush db=1 flush_mode=db +- redis: + command: flush + db: 1 + flush_mode: db # Configure local redis to have 10000 max clients -- redis: command=config name=maxclients value=10000 +- redis: + command: config + name: maxclients + value: 10000 # Configure local redis to have lua time limit of 100 ms -- redis: command=config name=lua-time-limit value=100 +- redis: + command: config + name: lua-time-limit + value: 100 ''' try: diff --git a/database/misc/riak.py b/database/misc/riak.py index ccdec82f0af..bec1ce8928b 100644 --- a/database/misc/riak.py +++ b/database/misc/riak.py @@ -88,13 +88,17 @@ EXAMPLES = ''' # Join's a Riak node to another node -- riak: command=join target_node=riak@10.1.1.1 +- riak: + command: join + target_node: riak@10.1.1.1 # Wait for handoffs to finish. Use with async and poll. -- riak: wait_for_handoffs=yes +- riak: + wait_for_handoffs: yes # Wait for riak_kv service to startup -- riak: wait_for_service=kv +- riak: + wait_for_service: kv ''' import time From 186e86411313ec4fa83031c38a77d148a72b0fcd Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 14:02:24 +0000 Subject: [PATCH 2444/2522] Native YAML - Databases/MsSQL (#3604) --- database/mssql/mssql_db.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/database/mssql/mssql_db.py b/database/mssql/mssql_db.py index 45642c579f7..eb6866151b8 100644 --- a/database/mssql/mssql_db.py +++ b/database/mssql/mssql_db.py @@ -81,10 +81,19 @@ EXAMPLES = ''' # Create a new database with name 'jackdata' -- mssql_db: name=jackdata state=present +- mssql_db: + name: jackdata + state: present + # Copy database dump file to remote host and restore it to database 'my_db' -- copy: src=dump.sql dest=/tmp -- mssql_db: name=my_db state=import target=/tmp/dump.sql +- copy: + src: dump.sql + dest: /tmp + +- mssql_db: + name: my_db + state: import + target: /tmp/dump.sql ''' RETURN = ''' From 990a9462e56d45f7595671594e0958a6295815e4 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 14:03:07 +0000 Subject: [PATCH 2445/2522] Native YAML - Databases/mysql (#3605) --- database/mysql/mysql_replication.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/database/mysql/mysql_replication.py b/database/mysql/mysql_replication.py index 68b6bc8eeb7..5f58c4a2e11 100644 --- a/database/mysql/mysql_replication.py +++ b/database/mysql/mysql_replication.py @@ -103,16 +103,25 @@ EXAMPLES = ''' # Stop mysql slave thread -- mysql_replication: mode=stopslave +- mysql_replication: + mode: stopslave # Get master binlog file name and binlog position -- mysql_replication: mode=getmaster +- mysql_replication: + mode: getmaster # Change master to master server 192.0.2.1 and use binary log 'mysql-bin.000009' with position 4578 -- mysql_replication: mode=changemaster master_host=192.0.2.1 master_log_file=mysql-bin.000009 master_log_pos=4578 +- mysql_replication: + mode: changemaster + master_host: 192.0.2.1 + master_log_file: mysql-bin.000009 + master_log_pos: 4578 # Check slave status using port 3308 -- mysql_replication: mode=getslave login_host=ansible.example.com login_port=3308 +- mysql_replication: + mode: getslave + login_host: ansible.example.com + login_port: 3308 ''' import os From f9131363ba958e558ea8023298e7ac1dd2411e37 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 14:03:39 +0000 Subject: [PATCH 2446/2522] Native YAML - databases/pgsql (#3606) --- database/postgresql/postgresql_lang.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/database/postgresql/postgresql_lang.py b/database/postgresql/postgresql_lang.py index a0b24cdd736..8e4fad3731c 100644 --- a/database/postgresql/postgresql_lang.py +++ b/database/postgresql/postgresql_lang.py @@ -119,16 +119,32 @@ # Add language pltclu to database testdb if it doesn't exist and mark it as trusted: # Marks the language as trusted if it exists but isn't trusted yet # force_trust makes sure that the language will be marked as trusted -- postgresql_lang db=testdb lang=pltclu state=present trust=yes force_trust=yes +- postgresql_lang: + db: testdb + lang: pltclu + state: present + trust: yes + force_trust: yes # Remove language pltclu from database testdb: -- postgresql_lang: db=testdb lang=pltclu state=absent +- postgresql_lang: + db: testdb + lang: pltclu + state: absent # Remove language pltclu from database testdb and remove all dependencies: -- postgresql_lang: db=testdb lang=pltclu state=absent cascade=yes +- postgresql_lang: + db: testdb + lang: pltclu + state: absent + cascade: yes # Remove language c from database testdb but ignore errors if something prevents the removal: -- postgresql_lang: db=testdb lang=pltclu state=absent fail_on_drop=no +- postgresql_lang: + db: testdb + lang: pltclu + state: absent + fail_on_drop: no ''' try: From 199b78a364c1177dcdad242432a11d896e524c74 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 14:19:36 +0000 Subject: [PATCH 2447/2522] Native YAML - files (#3608) --- files/archive.py | 12 +++++++++--- files/tempfile.py | 12 ++++++++---- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/files/archive.py b/files/archive.py index 36d2c4231b2..7d27fb3e858 100644 --- a/files/archive.py +++ b/files/archive.py @@ -63,13 +63,19 @@ EXAMPLES = ''' # Compress directory /path/to/foo/ into /path/to/foo.tgz -- archive: path=/path/to/foo dest=/path/to/foo.tgz +- archive: + path: /path/to/foo + dest: /path/to/foo.tgz # Compress regular file /path/to/foo into /path/to/foo.gz and remove it -- archive: path=/path/to/foo remove=True +- archive: + path: /path/to/foo + remove: True # Create a zip archive of /path/to/foo -- archive: path=/path/to/foo format=zip +- archive: + path: /path/to/foo + format: zip # Create a bz2 archive of multiple files, rooted at /path - archive: diff --git a/files/tempfile.py b/files/tempfile.py index 88bdf358de7..35969141fe7 100644 --- a/files/tempfile.py +++ b/files/tempfile.py @@ -52,11 +52,15 @@ ''' EXAMPLES = """ - - name: create temporary build directory - tempfile: state=directory suffix=build +- name: create temporary build directory + tempfile: + state: directory + suffix: build - - name: create temporary file - tempfile: state=file suffix=temp +- name: create temporary file + tempfile: + state: file + suffix: temp """ RETURN = ''' From 4c39eb666715afdb72b833ace8a2ffcff77da4c2 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 16:07:13 +0000 Subject: [PATCH 2448/2522] Use native YAML - Packaging (#3588) * Fix kibana * More native YAML * More native YAML * More native YAML * More native YAML. Now only languages/ is missing * Use native yaml sintax for packaging/languages as well * Some more and quote fixes * Fix wrong grouping --- packaging/dpkg_selections.py | 4 ++- packaging/elasticsearch_plugin.py | 13 ++++++-- packaging/kibana_plugin.py | 13 ++++++-- packaging/language/bower.py | 49 ++++++++++++++++++---------- packaging/language/bundler.py | 20 +++++++++--- packaging/language/composer.py | 18 +++++----- packaging/language/cpanm.py | 30 ++++++++++++----- packaging/language/maven_artifact.py | 26 ++++++++++++--- packaging/language/npm.py | 34 ++++++++++++++----- packaging/language/pear.py | 16 ++++++--- packaging/os/apk.py | 40 +++++++++++++++++------ packaging/os/dnf.py | 30 ++++++++++++----- packaging/os/homebrew.py | 48 +++++++++++++++++++++------ packaging/os/homebrew_cask.py | 27 ++++++++++++--- packaging/os/homebrew_tap.py | 18 +++++++--- packaging/os/layman.py | 20 +++++++++--- packaging/os/macports.py | 25 +++++++++++--- packaging/os/openbsd_pkg.py | 33 ++++++++++++++----- packaging/os/opkg.py | 26 ++++++++++++--- packaging/os/pacman.py | 33 ++++++++++++++----- packaging/os/pkg5.py | 7 ++-- packaging/os/pkg5_publisher.py | 9 +++-- packaging/os/pkgin.py | 32 +++++++++++++----- packaging/os/pkgng.py | 12 +++++-- packaging/os/pkgutil.py | 9 +++-- packaging/os/portage.py | 34 ++++++++++++++----- packaging/os/portinstall.py | 12 +++++-- packaging/os/slackpkg.py | 13 +++++--- packaging/os/svr4pkg.py | 26 ++++++++++++--- packaging/os/swdepot.py | 16 +++++++-- packaging/os/urpmi.py | 20 +++++++++--- packaging/os/yum_repository.py | 1 + packaging/os/zypper.py | 44 +++++++++++++++++++------ packaging/os/zypper_repository.py | 27 +++++++++++---- 34 files changed, 594 insertions(+), 191 deletions(-) diff --git a/packaging/dpkg_selections.py b/packaging/dpkg_selections.py index fa0f73a713b..81dd849e599 100644 --- a/packaging/dpkg_selections.py +++ b/packaging/dpkg_selections.py @@ -39,7 +39,9 @@ ''' EXAMPLES = ''' # Prevent python from being upgraded. -- dpkg_selections: name=python selection=hold +- dpkg_selections: + name: python + selection: hold ''' def main(): diff --git a/packaging/elasticsearch_plugin.py b/packaging/elasticsearch_plugin.py index 9bb904a88c5..e89361edd89 100644 --- a/packaging/elasticsearch_plugin.py +++ b/packaging/elasticsearch_plugin.py @@ -83,13 +83,20 @@ EXAMPLES = ''' # Install Elasticsearch head plugin -- elasticsearch_plugin: state=present name="mobz/elasticsearch-head" +- elasticsearch_plugin: + state: present + name: mobz/elasticsearch-head # Install specific version of a plugin -- elasticsearch_plugin: state=present name="com.github.kzwang/elasticsearch-image" version="1.2.0" +- elasticsearch_plugin: + state: present + name: com.github.kzwang/elasticsearch-image + version: '1.2.0' # Uninstall Elasticsearch head plugin -- elasticsearch_plugin: state=absent name="mobz/elasticsearch-head" +- elasticsearch_plugin: + state: absent + name: mobz/elasticsearch-head ''' PACKAGE_STATE_MAP = dict( diff --git a/packaging/kibana_plugin.py b/packaging/kibana_plugin.py index f0ffcd9ddf7..e877d756b08 100644 --- a/packaging/kibana_plugin.py +++ b/packaging/kibana_plugin.py @@ -79,13 +79,20 @@ EXAMPLES = ''' # Install Elasticsearch head plugin -- kibana_plugin: state=present name="elasticsearch/marvel" +- kibana_plugin: + state: present + name=: elasticsearch/marvel # Install specific version of a plugin -- kibana_plugin: state=present name="elasticsearch/marvel" version="2.3.3" +- kibana_plugin: + state: present + name: elasticsearch/marvel + version: '2.3.3' # Uninstall Elasticsearch head plugin -- kibana_plugin: state=absent name="elasticsearch/marvel" +- kibana_plugin: + state: absent + name: elasticsearch/marvel ''' RETURN = ''' diff --git a/packaging/language/bower.py b/packaging/language/bower.py index 2b58b1ce1f5..1627741d5a6 100644 --- a/packaging/language/bower.py +++ b/packaging/language/bower.py @@ -67,24 +67,37 @@ ''' EXAMPLES = ''' -description: Install "bootstrap" bower package. -- bower: name=bootstrap - -description: Install "bootstrap" bower package on version 3.1.1. -- bower: name=bootstrap version=3.1.1 - -description: Remove the "bootstrap" bower package. -- bower: name=bootstrap state=absent - -description: Install packages based on bower.json. -- bower: path=/app/location - -description: Update packages based on bower.json to their latest version. -- bower: path=/app/location state=latest - -description: install bower locally and run from there -- npm: path=/app/location name=bower global=no -- bower: path=/app/location relative_execpath=node_modules/.bin +- name: Install "bootstrap" bower package. + bower: + name: bootstrap + +- name: Install "bootstrap" bower package on version 3.1.1. + bower: + name: bootstrap + version: '3.1.1' + +- name: Remove the "bootstrap" bower package. + bower: + name: bootstrap + state: absent + +- name: Install packages based on bower.json. + bower: + path: /app/location + +- name: Update packages based on bower.json to their latest version. + bower: + path: /app/location + state: latest + +# install bower locally and run from there +- npm: + path: /app/location + name: bower + global: no +- bower: + path: /app/location + relative_execpath: node_modules/.bin ''' diff --git a/packaging/language/bundler.py b/packaging/language/bundler.py index 152b51810a0..fc647862fd8 100644 --- a/packaging/language/bundler.py +++ b/packaging/language/bundler.py @@ -112,19 +112,29 @@ EXAMPLES=''' # Installs gems from a Gemfile in the current directory -- bundler: state=present executable=~/.rvm/gems/2.1.5/bin/bundle +- bundler: + state: present + executable: ~/.rvm/gems/2.1.5/bin/bundle # Excludes the production group from installing -- bundler: state=present exclude_groups=production +- bundler: + state: present + exclude_groups: production # Only install gems from the default and production groups -- bundler: state=present deployment_mode=yes +- bundler: + state: present + deployment_mode: yes # Installs gems using a Gemfile in another directory -- bundler: state=present gemfile=../rails_project/Gemfile +- bundler: + state: present + gemfile: ../rails_project/Gemfile # Updates Gemfile in another directory -- bundler: state=latest chdir=~/rails_project +- bundler: + state: latest + chdir: ~/rails_project ''' diff --git a/packaging/language/composer.py b/packaging/language/composer.py index 4c5f8518bee..9ff393b25b7 100644 --- a/packaging/language/composer.py +++ b/packaging/language/composer.py @@ -108,19 +108,21 @@ EXAMPLES = ''' # Downloads and installs all the libs and dependencies outlined in the /path/to/project/composer.lock -- composer: command=install working_dir=/path/to/project +- composer: + command: install + working_dir: /path/to/project - composer: - command: "require" - arguments: "my/package" - working_dir: "/path/to/project" + command: require + arguments: my/package + working_dir: /path/to/project # Clone project and install with all dependencies - composer: - command: "create-project" - arguments: "package/package /path/to/project ~1.0" - working_dir: "/path/to/project" - prefer_dist: "yes" + command: create-project + arguments: package/package /path/to/project ~1.0 + working_dir: /path/to/project + prefer_dist: yes ''' import os diff --git a/packaging/language/cpanm.py b/packaging/language/cpanm.py index 790a4939156..26566c6dbf8 100644 --- a/packaging/language/cpanm.py +++ b/packaging/language/cpanm.py @@ -91,29 +91,43 @@ EXAMPLES = ''' # install Dancer perl package -- cpanm: name=Dancer +- cpanm: + name: Dancer # install version 0.99_05 of the Plack perl package -- cpanm: name=MIYAGAWA/Plack-0.99_05.tar.gz +- cpanm: + name: MIYAGAWA/Plack-0.99_05.tar.gz # install Dancer into the specified locallib -- cpanm: name=Dancer locallib=/srv/webapps/my_app/extlib +- cpanm: + name: Dancer + locallib: /srv/webapps/my_app/extlib # install perl dependencies from local directory -- cpanm: from_path=/srv/webapps/my_app/src/ +- cpanm: + from_path: /srv/webapps/my_app/src/ # install Dancer perl package without running the unit tests in indicated locallib -- cpanm: name=Dancer notest=True locallib=/srv/webapps/my_app/extlib +- cpanm: + name: Dancer + notest: True + locallib: /srv/webapps/my_app/extlib # install Dancer perl package from a specific mirror -- cpanm: name=Dancer mirror=http://cpan.cpantesters.org/ +- cpanm: + name: Dancer + mirror: 'http://cpan.cpantesters.org/' # install Dancer perl package into the system root path -- cpanm: name=Dancer system_lib=yes +- cpanm: + name: Dancer + system_lib: yes # install Dancer if it's not already installed # OR the installed version is older than version 1.0 -- cpanm: name=Dancer version=1.0 +- cpanm: + name: Dancer + version: '1.0' ''' def _is_package_installed(module, name, locallib, cpanm, version): diff --git a/packaging/language/maven_artifact.py b/packaging/language/maven_artifact.py index 5c0e88ac725..3e7fddbd0af 100644 --- a/packaging/language/maven_artifact.py +++ b/packaging/language/maven_artifact.py @@ -119,16 +119,34 @@ EXAMPLES = ''' # Download the latest version of the JUnit framework artifact from Maven Central -- maven_artifact: group_id=junit artifact_id=junit dest=/tmp/junit-latest.jar +- maven_artifact: + group_id: junit + artifact_id: junit + dest: /tmp/junit-latest.jar # Download JUnit 4.11 from Maven Central -- maven_artifact: group_id=junit artifact_id=junit version=4.11 dest=/tmp/junit-4.11.jar +- maven_artifact: + group_id: junit + artifact_id: junit + version: 4.11 + dest: /tmp/junit-4.11.jar # Download an artifact from a private repository requiring authentication -- maven_artifact: group_id=com.company artifact_id=library-name repository_url=https://repo.company.com/maven username=user password=pass dest=/tmp/library-name-latest.jar +- maven_artifact: + group_id: com.company + artifact_id: library-name + repository_url: 'https://repo.company.com/maven' + username: user + password: pass + dest: /tmp/library-name-latest.jar # Download a WAR File to the Tomcat webapps directory to be deployed -- maven_artifact: group_id=com.company artifact_id=web-app extension=war repository_url=https://repo.company.com/maven dest=/var/lib/tomcat7/webapps/web-app.war +- maven_artifact: + group_id: com.company + artifact_id: web-app + extension: war + repository_url: 'https://repo.company.com/maven' + dest: /var/lib/tomcat7/webapps/web-app.war ''' class Artifact(object): diff --git a/packaging/language/npm.py b/packaging/language/npm.py index e15bbea903e..58e29dc86e2 100644 --- a/packaging/language/npm.py +++ b/packaging/language/npm.py @@ -78,28 +78,46 @@ EXAMPLES = ''' description: Install "coffee-script" node.js package. -- npm: name=coffee-script path=/app/location +- npm: + name: coffee-script + path: /app/location description: Install "coffee-script" node.js package on version 1.6.1. -- npm: name=coffee-script version=1.6.1 path=/app/location +- npm: + name: coffee-script + version: '1.6.1' + path: /app/location description: Install "coffee-script" node.js package globally. -- npm: name=coffee-script global=yes +- npm: + name: coffee-script + global: yes description: Remove the globally package "coffee-script". -- npm: name=coffee-script global=yes state=absent +- npm: + name: coffee-script + global: yes + state: absent description: Install "coffee-script" node.js package from custom registry. -- npm: name=coffee-script registry=http://registry.mysite.com +- npm: + name: coffee-script + registry: 'http://registry.mysite.com' description: Install packages based on package.json. -- npm: path=/app/location +- npm: + path: /app/location description: Update packages based on package.json to their latest version. -- npm: path=/app/location state=latest +- npm: + path: /app/location + state: latest description: Install packages based on package.json using the npm installed with nvm v0.10.1. -- npm: path=/app/location executable=/opt/nvm/v0.10.1/bin/npm state=present +- npm: + path: /app/location + executable: /opt/nvm/v0.10.1/bin/npm + state: present ''' import os diff --git a/packaging/language/pear.py b/packaging/language/pear.py index 5762f9c815c..ae513baf142 100644 --- a/packaging/language/pear.py +++ b/packaging/language/pear.py @@ -45,16 +45,24 @@ EXAMPLES = ''' # Install pear package -- pear: name=Net_URL2 state=present +- pear: + name: Net_URL2 + state: present # Install pecl package -- pear: name=pecl/json_post state=present +- pear: + name: pecl/json_post + state: present # Upgrade package -- pear: name=Net_URL2 state=latest +- pear: + name: Net_URL2 + state: latest # Remove packages -- pear: name=Net_URL2,pecl/json_post state=absent +- pear: + name: Net_URL2,pecl/json_post + state: absent ''' import os diff --git a/packaging/os/apk.py b/packaging/os/apk.py index 911e50e0942..898b69a3043 100644 --- a/packaging/os/apk.py +++ b/packaging/os/apk.py @@ -58,34 +58,54 @@ EXAMPLES = ''' # Update repositories and install "foo" package -- apk: name=foo update_cache=yes +- apk: + name: foo + update_cache: yes # Update repositories and install "foo" and "bar" packages -- apk: name=foo,bar update_cache=yes +- apk: + name: foo,bar + update_cache: yes # Remove "foo" package -- apk: name=foo state=absent +- apk: + name: foo + state: absent # Remove "foo" and "bar" packages -- apk: name=foo,bar state=absent +- apk: + name: foo,bar + state: absent # Install the package "foo" -- apk: name=foo state=present +- apk: + name: foo + state: present # Install the packages "foo" and "bar" -- apk: name=foo,bar state=present +- apk: + name: foo,bar + state: present # Update repositories and update package "foo" to latest version -- apk: name=foo state=latest update_cache=yes +- apk: + name: foo + state: latest + update_cache: yes # Update repositories and update packages "foo" and "bar" to latest versions -- apk: name=foo,bar state=latest update_cache=yes +- apk: + name: foo,bar + state: latest + update_cache: yes # Update all installed packages to the latest versions -- apk: upgrade=yes +- apk: + upgrade: yes # Update repositories as a separate step -- apk: update_cache=yes +- apk: + update_cache: yes ''' import os diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index 98616da8478..715e5d0bf65 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -94,26 +94,40 @@ EXAMPLES = ''' - name: install the latest version of Apache - dnf: name=httpd state=latest + dnf: + name: httpd + state: latest - name: remove the Apache package - dnf: name=httpd state=absent + dnf: + name: httpd + state: absent - name: install the latest version of Apache from the testing repo - dnf: name=httpd enablerepo=testing state=present + dnf: + name: httpd + enablerepo: testing + state: present - name: upgrade all packages - dnf: name=* state=latest + dnf: + name: * + state: latest - name: install the nginx rpm from a remote repo - dnf: name=http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm state=present + dnf: + name: 'http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm' + state: present - name: install nginx rpm from a local file - dnf: name=/usr/local/src/nginx-release-centos-6-0.el6.ngx.noarch.rpm state=present + dnf: + name: /usr/local/src/nginx-release-centos-6-0.el6.ngx.noarch.rpm + state: present - name: install the 'Development tools' package group - dnf: name="@Development tools" state=present - + dnf: + name: '@Development tools' + state: present ''' import os diff --git a/packaging/os/homebrew.py b/packaging/os/homebrew.py index 482e1b92cdb..ca12cb8c608 100755 --- a/packaging/os/homebrew.py +++ b/packaging/os/homebrew.py @@ -76,26 +76,54 @@ ''' EXAMPLES = ''' # Install formula foo with 'brew' in default path (C(/usr/local/bin)) -- homebrew: name=foo state=present +- homebrew: + name: foo + state: present # Install formula foo with 'brew' in alternate path C(/my/other/location/bin) -- homebrew: name=foo path=/my/other/location/bin state=present +- homebrew: + name: foo + path: /my/other/location/bin + state: present # Update homebrew first and install formula foo with 'brew' in default path -- homebrew: name=foo state=present update_homebrew=yes +- homebrew: + name: foo + state: present + update_homebrew: yes # Update homebrew first and upgrade formula foo to latest available with 'brew' in default path -- homebrew: name=foo state=latest update_homebrew=yes +- homebrew: + name: foo + state: latest + update_homebrew: yes # Update homebrew and upgrade all packages -- homebrew: update_homebrew=yes upgrade_all=yes +- homebrew: + update_homebrew: yes + upgrade_all: yes # Miscellaneous other examples -- homebrew: name=foo state=head -- homebrew: name=foo state=linked -- homebrew: name=foo state=absent -- homebrew: name=foo,bar state=absent -- homebrew: name=foo state=present install_options=with-baz,enable-debug +- homebrew: + name: foo + state: head + +- homebrew: + name: foo + state: linked + +- homebrew: + name: foo + state: absent + +- homebrew: + name: foo,bar + state: absent + +- homebrew: + name: foo + state: present + install_options: with-baz,enable-debug ''' import os.path diff --git a/packaging/os/homebrew_cask.py b/packaging/os/homebrew_cask.py index 2d1722398eb..fdfa3f0cdbf 100755 --- a/packaging/os/homebrew_cask.py +++ b/packaging/os/homebrew_cask.py @@ -65,11 +65,28 @@ version_added: "2.2" ''' EXAMPLES = ''' -- homebrew_cask: name=alfred state=present -- homebrew_cask: name=alfred state=absent -- homebrew_cask: name=alfred state=present install_options="appdir=/Applications" -- homebrew_cask: name=alfred state=present install_options="debug,appdir=/Applications" -- homebrew_cask: name=alfred state=absent install_options="force" +- homebrew_cask: + name: alfred + state: present + +- homebrew_cask: + name: alfred + state: absent + +- homebrew_cask: + name: alfred + state: present + install_options: 'appdir=/Applications' + +- homebrew_cask: + name: alfred + state: present + install_options: 'debug,appdir=/Applications' + +- homebrew_cask: + name: alfred + state: absent + install_options: force ''' import os.path diff --git a/packaging/os/homebrew_tap.py b/packaging/os/homebrew_tap.py index 9264db8775e..2a981deaeb8 100644 --- a/packaging/os/homebrew_tap.py +++ b/packaging/os/homebrew_tap.py @@ -59,10 +59,20 @@ ''' EXAMPLES = ''' -homebrew_tap: name=homebrew/dupes -homebrew_tap: name=homebrew/dupes state=absent -homebrew_tap: name=homebrew/dupes,homebrew/science state=present -homebrew_tap: name=telemachus/brew url=https://bitbucket.org/telemachus/brew +- homebrew_tap: + name: homebrew/dupes + +- homebrew_tap: + name: homebrew/dupes + state: absent + +- homebrew_tap: + name: homebrew/dupes,homebrew/science + state: present + +- homebrew_tap: + name: telemachus/brew + url: 'https://bitbucket.org/telemachus/brew' ''' diff --git a/packaging/os/layman.py b/packaging/os/layman.py index ac6acd12d4b..f18d0eaa0a7 100644 --- a/packaging/os/layman.py +++ b/packaging/os/layman.py @@ -64,19 +64,29 @@ EXAMPLES = ''' # Install the overlay 'mozilla' which is on the central overlays list. -- layman: name=mozilla +- layman: + name: mozilla # Install the overlay 'cvut' from the specified alternative list. -- layman: name=cvut list_url=http://raw.github.com/cvut/gentoo-overlay/master/overlay.xml +- layman: + name: cvut + list_url: 'http://raw.github.com/cvut/gentoo-overlay/master/overlay.xml' # Update (sync) the overlay 'cvut', or install if not installed yet. -- layman: name=cvut list_url=http://raw.github.com/cvut/gentoo-overlay/master/overlay.xml state=updated +- layman: + name: cvut + list_url: 'http://raw.github.com/cvut/gentoo-overlay/master/overlay.xml' + state: updated # Update (sync) all of the installed overlays. -- layman: name=ALL state=updated +- layman: + name: ALL + state: updated # Uninstall the overlay 'cvut'. -- layman: name=cvut state=absent +- layman: + name: cvut + state: absent ''' USERAGENT = 'ansible-httpget' diff --git a/packaging/os/macports.py b/packaging/os/macports.py index ca3a0f97426..07926b9520b 100644 --- a/packaging/os/macports.py +++ b/packaging/os/macports.py @@ -46,11 +46,26 @@ notes: [] ''' EXAMPLES = ''' -- macports: name=foo state=present -- macports: name=foo state=present update_cache=yes -- macports: name=foo state=absent -- macports: name=foo state=active -- macports: name=foo state=inactive +- macports: + name: foo + state: present + +- macports: + name: foo + state: present + update_cache: yes + +- macports: + name: foo + state: absent + +- macports: + name: foo + state: active + +- macports: + name: foo + state: inactive ''' import pipes diff --git a/packaging/os/openbsd_pkg.py b/packaging/os/openbsd_pkg.py index 67583cdf36e..e68ef18989e 100644 --- a/packaging/os/openbsd_pkg.py +++ b/packaging/os/openbsd_pkg.py @@ -68,28 +68,45 @@ EXAMPLES = ''' # Make sure nmap is installed -- openbsd_pkg: name=nmap state=present +- openbsd_pkg: + name: nmap + state: present # Make sure nmap is the latest version -- openbsd_pkg: name=nmap state=latest +- openbsd_pkg: + name: nmap + state: latest # Make sure nmap is not installed -- openbsd_pkg: name=nmap state=absent +- openbsd_pkg: + name: nmap + state: absent # Make sure nmap is installed, build it from source if it is not -- openbsd_pkg: name=nmap state=present build=yes +- openbsd_pkg: + name: nmap + state: present + build: yes # Specify a pkg flavour with '--' -- openbsd_pkg: name=vim--no_x11 state=present +- openbsd_pkg: + name: vim--no_x11 + state: present # Specify the default flavour to avoid ambiguity errors -- openbsd_pkg: name=vim-- state=present +- openbsd_pkg: + name: vim-- + state: present # Specify a package branch (requires at least OpenBSD 6.0) -- openbsd_pkg: name=python%3.5 state=present +- openbsd_pkg: + name: python%3.5 + state: present # Update all packages on the system -- openbsd_pkg: name=* state=latest +- openbsd_pkg: + name: * + state: latest ''' # Function used for executing commands. diff --git a/packaging/os/opkg.py b/packaging/os/opkg.py index 9ac8f99b8c8..1d740202408 100644 --- a/packaging/os/opkg.py +++ b/packaging/os/opkg.py @@ -52,11 +52,27 @@ notes: [] ''' EXAMPLES = ''' -- opkg: name=foo state=present -- opkg: name=foo state=present update_cache=yes -- opkg: name=foo state=absent -- opkg: name=foo,bar state=absent -- opkg: name=foo state=present force=overwrite +- opkg: + name: foo + state: present + +- opkg: + name: foo + state: present + update_cache: yes + +- opkg: + name: foo + state: absent + +- opkg: + name: foo,bar + state: absent + +- opkg: + name: foo + state: present + force: overwrite ''' import pipes diff --git a/packaging/os/pacman.py b/packaging/os/pacman.py index 74c474ad922..c27a67c9e7b 100644 --- a/packaging/os/pacman.py +++ b/packaging/os/pacman.py @@ -89,28 +89,45 @@ EXAMPLES = ''' # Install package foo -- pacman: name=foo state=present +- pacman: + name: foo + state: present # Upgrade package foo -- pacman: name=foo state=latest update_cache=yes +- pacman: + name: foo + state: latest + update_cache: yes # Remove packages foo and bar -- pacman: name=foo,bar state=absent +- pacman: + name: foo,bar + state: absent # Recursively remove package baz -- pacman: name=baz state=absent recurse=yes +- pacman: + name: baz + state: absent + recurse: yes # Run the equivalent of "pacman -Sy" as a separate step -- pacman: update_cache=yes +- pacman: + update_cache: yes # Run the equivalent of "pacman -Su" as a separate step -- pacman: upgrade=yes +- pacman: + upgrade: yes # Run the equivalent of "pacman -Syu" as a separate step -- pacman: update_cache=yes upgrade=yes +- pacman: + update_cache: yes + upgrade: yes # Run the equivalent of "pacman -Rdd", force remove package baz -- pacman: name=baz state=absent force=yes +- pacman: + name: baz + state: absent + force: yes ''' import shlex diff --git a/packaging/os/pkg5.py b/packaging/os/pkg5.py index 4fb34d7a51c..b1ade8e4dfd 100644 --- a/packaging/os/pkg5.py +++ b/packaging/os/pkg5.py @@ -49,10 +49,13 @@ ''' EXAMPLES = ''' # Install Vim: -- pkg5: name=editor/vim +- pkg5: + name: editor/vim # Remove finger daemon: -- pkg5: name=service/network/finger state=absent +- pkg5: + name: service/network/finger + state: absent # Install several packages at once: - pkg5: diff --git a/packaging/os/pkg5_publisher.py b/packaging/os/pkg5_publisher.py index 79eccd2ec08..cc9c3c8ca8b 100644 --- a/packaging/os/pkg5_publisher.py +++ b/packaging/os/pkg5_publisher.py @@ -66,10 +66,15 @@ ''' EXAMPLES = ''' # Fetch packages for the solaris publisher direct from Oracle: -- pkg5_publisher: name=solaris sticky=true origin=https://pkg.oracle.com/solaris/support/ +- pkg5_publisher: + name: solaris + sticky: true + origin: https://pkg.oracle.com/solaris/support/ # Configure a publisher for locally-produced packages: -- pkg5_publisher: name=site origin=https://pkg.example.com/site/ +- pkg5_publisher: + name: site + origin: 'https://pkg.example.com/site/' ''' def main(): diff --git a/packaging/os/pkgin.py b/packaging/os/pkgin.py index 055891ebe08..19f9a157b37 100755 --- a/packaging/os/pkgin.py +++ b/packaging/os/pkgin.py @@ -91,31 +91,45 @@ EXAMPLES = ''' # install package foo -- pkgin: name=foo state=present +- pkgin: + name: foo + state: present # Update database and install "foo" package -- pkgin: name=foo update_cache=yes +- pkgin: + name: foo + update_cache: yes # remove package foo -- pkgin: name=foo state=absent +- pkgin: + name: foo + state: absent # remove packages foo and bar -- pkgin: name=foo,bar state=absent +- pkgin: + name: foo,bar + state: absent # Update repositories as a separate step -- pkgin: update_cache=yes +- pkgin: + update_cache: yes # Upgrade main packages (equivalent to C(pkgin upgrade)) -- pkgin: upgrade=yes +- pkgin: + upgrade: yes # Upgrade all packages (equivalent to C(pkgin full-upgrade)) -- pkgin: full_upgrade=yes +- pkgin: + full_upgrade: yes # Force-upgrade all packages (equivalent to C(pkgin -F full-upgrade)) -- pkgin: full_upgrade=yes force=yes +- pkgin: + full_upgrade: yes + force: yes # clean packages cache (equivalent to C(pkgin clean)) -- pkgin: clean=yes +- pkgin: + clean: yes ''' diff --git a/packaging/os/pkgng.py b/packaging/os/pkgng.py index df1ceb37e80..4863a55d23e 100644 --- a/packaging/os/pkgng.py +++ b/packaging/os/pkgng.py @@ -89,13 +89,19 @@ EXAMPLES = ''' # Install package foo -- pkgng: name=foo state=present +- pkgng: + name: foo + state: present # Annotate package foo and bar -- pkgng: name=foo,bar annotation=+test1=baz,-test2,:test3=foobar +- pkgng: + name: foo,bar + annotation: '+test1=baz,-test2,:test3=foobar' # Remove packages foo and bar -- pkgng: name=foo,bar state=absent +- pkgng: + name: foo,bar + state: absent ''' diff --git a/packaging/os/pkgutil.py b/packaging/os/pkgutil.py index 35ccb4e1906..8495a19b24c 100644 --- a/packaging/os/pkgutil.py +++ b/packaging/os/pkgutil.py @@ -60,10 +60,15 @@ EXAMPLES = ''' # Install a package -pkgutil: name=CSWcommon state=present +- pkgutil: + name: CSWcommon + state: present # Install a package from a specific repository -pkgutil: name=CSWnrpe site='ftp://myinternal.repo/opencsw/kiel state=latest' +- pkgutil: + name: CSWnrpe + site: 'ftp://myinternal.repo/opencsw/kiel' + state: latest ''' import os diff --git a/packaging/os/portage.py b/packaging/os/portage.py index 2880902fb9a..0f3731968bc 100644 --- a/packaging/os/portage.py +++ b/packaging/os/portage.py @@ -183,28 +183,46 @@ EXAMPLES = ''' # Make sure package foo is installed -- portage: package=foo state=present +- portage: + package: foo + state: present # Make sure package foo is not installed -- portage: package=foo state=absent +- portage: + package: foo + state: absent # Update package foo to the "best" version -- portage: package=foo update=yes +- portage: + package: foo + update: yes # Install package foo using PORTAGE_BINHOST setup -- portage: package=foo getbinpkg=yes +- portage: + package: foo + getbinpkg: yes # Re-install world from binary packages only and do not allow any compiling -- portage: package=@world usepkgonly=yes +- portage: + package: @world + usepkgonly: yes # Sync repositories and update world -- portage: package=@world update=yes deep=yes sync=yes +- portage: + package: @world + update: yes + deep: yes + sync: yes # Remove unneeded packages -- portage: depclean=yes +- portage: + depclean: yes # Remove package foo if it is not explicitly needed -- portage: package=foo state=absent depclean=yes +- portage: + package: foo + state: absent + depclean: yes ''' diff --git a/packaging/os/portinstall.py b/packaging/os/portinstall.py index a5d0e510978..44f21efb7c7 100644 --- a/packaging/os/portinstall.py +++ b/packaging/os/portinstall.py @@ -48,13 +48,19 @@ EXAMPLES = ''' # Install package foo -- portinstall: name=foo state=present +- portinstall: + name: foo + state: present # Install package security/cyrus-sasl2-saslauthd -- portinstall: name=security/cyrus-sasl2-saslauthd state=present +- portinstall: + name: security/cyrus-sasl2-saslauthd + state: present # Remove packages foo and bar -- portinstall: name=foo,bar state=absent +- portinstall: + name: foo,bar + state: absent ''' diff --git a/packaging/os/slackpkg.py b/packaging/os/slackpkg.py index 674de538efe..9b65afddc86 100644 --- a/packaging/os/slackpkg.py +++ b/packaging/os/slackpkg.py @@ -56,14 +56,19 @@ EXAMPLES = ''' # Install package foo -- slackpkg: name=foo state=present +- slackpkg: + name: foo + state: present # Remove packages foo and bar -- slackpkg: name=foo,bar state=absent +- slackpkg: + name: foo,bar + state: absent # Make sure that it is the most updated package -- slackpkg: name=foo state=latest - +- slackpkg: + name: foo + state: latest ''' diff --git a/packaging/os/svr4pkg.py b/packaging/os/svr4pkg.py index 807e00f543b..1d8a9b26d5b 100644 --- a/packaging/os/svr4pkg.py +++ b/packaging/os/svr4pkg.py @@ -75,19 +75,35 @@ EXAMPLES = ''' # Install a package from an already copied file -- svr4pkg: name=CSWcommon src=/tmp/cswpkgs.pkg state=present +- svr4pkg: + name: CSWcommon + src: /tmp/cswpkgs.pkg + state: present # Install a package directly from an http site -- svr4pkg: name=CSWpkgutil src=http://get.opencsw.org/now state=present zone=current +- svr4pkg: + name: CSWpkgutil + src: 'http://get.opencsw.org/now' + state: present + zone: current # Install a package with a response file -- svr4pkg: name=CSWggrep src=/tmp/third-party.pkg response_file=/tmp/ggrep.response state=present +- svr4pkg: + name: CSWggrep + src: /tmp/third-party.pkg + response_file: /tmp/ggrep.response + state: present # Ensure that a package is not installed. -- svr4pkg: name=SUNWgnome-sound-recorder state=absent +- svr4pkg: + name: SUNWgnome-sound-recorder + state: absent # Ensure that a category is not installed. -- svr4pkg: name=FIREFOX state=absent category=true +- svr4pkg: + name: FIREFOX + state: absent + category: true ''' diff --git a/packaging/os/swdepot.py b/packaging/os/swdepot.py index b14af742057..8c7652af966 100644 --- a/packaging/os/swdepot.py +++ b/packaging/os/swdepot.py @@ -58,9 +58,19 @@ ''' EXAMPLES = ''' -- swdepot: name=unzip-6.0 state=installed depot=repository:/path -- swdepot: name=unzip state=latest depot=repository:/path -- swdepot: name=unzip state=absent +- swdepot: + name: unzip-6.0 + state: installed + depot: 'repository:/path' + +- swdepot: + name: unzip + state: latest + depot: 'repository:/path' + +- swdepot: + name: unzip + state: absent ''' def compare_package(version1, version2): diff --git a/packaging/os/urpmi.py b/packaging/os/urpmi.py index 0b9ec929316..47d7c1f6846 100644 --- a/packaging/os/urpmi.py +++ b/packaging/os/urpmi.py @@ -63,13 +63,25 @@ EXAMPLES = ''' # install package foo -- urpmi: pkg=foo state=present +- urpmi: + pkg: foo + state: present + # remove package foo -- urpmi: pkg=foo state=absent +- urpmi: + pkg: foo + state: absent + # description: remove packages foo and bar -- urpmi: pkg=foo,bar state=absent +- urpmi: + pkg: foo,bar + state: absent + # description: update the package database (urpmi.update -a -q) and install bar (bar will be the updated if a newer version exists) -- urpmi: name=bar, state=present, update_cache=yes +- urpmi: + name: bar + state: present + update_cache: yes ''' diff --git a/packaging/os/yum_repository.py b/packaging/os/yum_repository.py index d73c81fb22a..a796a0dd6ae 100644 --- a/packaging/os/yum_repository.py +++ b/packaging/os/yum_repository.py @@ -410,6 +410,7 @@ file: external_repos baseurl: http://download.fedoraproject.org/pub/epel/$releasever/$basearch/ gpgcheck: no + - name: Add multiple repositories into the same file (2/2) yum_repository: name: rpmforge diff --git a/packaging/os/zypper.py b/packaging/os/zypper.py index 7b385e792ea..c91528b2199 100644 --- a/packaging/os/zypper.py +++ b/packaging/os/zypper.py @@ -113,34 +113,58 @@ EXAMPLES = ''' # Install "nmap" -- zypper: name=nmap state=present +- zypper: + name: nmap + state: present # Install apache2 with recommended packages -- zypper: name=apache2 state=present disable_recommends=no +- zypper: + name: apache2 + state: present + disable_recommends: no # Apply a given patch -- zypper: name=openSUSE-2016-128 state=present type=patch +- zypper: + name: openSUSE-2016-128 + state: present + type: patch # Remove the "nmap" package -- zypper: name=nmap state=absent +- zypper: + name: nmap + state: absent # Install the nginx rpm from a remote repo -- zypper: name=http://nginx.org/packages/sles/12/x86_64/RPMS/nginx-1.8.0-1.sles12.ngx.x86_64.rpm state=present +- zypper: + name: 'http://nginx.org/packages/sles/12/x86_64/RPMS/nginx-1.8.0-1.sles12.ngx.x86_64.rpm' + state: present # Install local rpm file -- zypper: name=/tmp/fancy-software.rpm state=present +- zypper: + name: /tmp/fancy-software.rpm + state: present # Update all packages -- zypper: name=* state=latest +- zypper: + name: * + state: latest # Apply all available patches -- zypper: name=* state=latest type=patch +- zypper: + name: * + state: latest + type: patch # Refresh repositories and update package "openssl" -- zypper: name=openssl state=present update_cache=yes +- zypper: + name: openssl + state: present + update_cache: yes # Install specific version (possible comparisons: <, >, <=, >=, =) -- zypper: name=docker>=1.10 state=installed +- zypper: + name: 'docker>=1.10' + state: installed ''' diff --git a/packaging/os/zypper_repository.py b/packaging/os/zypper_repository.py index db553970e84..40510db9a98 100644 --- a/packaging/os/zypper_repository.py +++ b/packaging/os/zypper_repository.py @@ -114,22 +114,37 @@ EXAMPLES = ''' # Add NVIDIA repository for graphics drivers -- zypper_repository: name=nvidia-repo repo='ftp://download.nvidia.com/opensuse/12.2' state=present +- zypper_repository: + name: nvidia-repo + repo: 'ftp://download.nvidia.com/opensuse/12.2' + state: present # Remove NVIDIA repository -- zypper_repository: name=nvidia-repo repo='ftp://download.nvidia.com/opensuse/12.2' state=absent +- zypper_repository: + name: nvidia-repo + repo: 'ftp://download.nvidia.com/opensuse/12.2' + state: absent # Add python development repository -- zypper_repository: repo=http://download.opensuse.org/repositories/devel:/languages:/python/SLE_11_SP3/devel:languages:python.repo +- zypper_repository: + repo: 'http://download.opensuse.org/repositories/devel:/languages:/python/SLE_11_SP3/devel:languages:python.repo' # Refresh all repos -- zypper_repository: repo=* runrefresh=yes +- zypper_repository: + repo: * + runrefresh: yes # Add a repo and add it's gpg key -- zypper_repository: repo=http://download.opensuse.org/repositories/systemsmanagement/openSUSE_Leap_42.1/ auto_import_keys=yes +- zypper_repository: + repo: 'http://download.opensuse.org/repositories/systemsmanagement/openSUSE_Leap_42.1/' + auto_import_keys: yes # Force refresh of a repository -- zypper_repository: repo=http://my_internal_ci_repo/repo name=my_ci_repo state=present runrefresh=yes +- zypper_repository: + repo: 'http://my_internal_ci_repo/repo + name: my_ci_repo + state: present + runrefresh: yes ''' REPO_OPTS = ['alias', 'name', 'priority', 'enabled', 'autorefresh', 'gpgcheck'] From 270a7d905266213eb029d7e9878bb3384d856025 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 16:39:21 +0000 Subject: [PATCH 2449/2522] Cloud amazon and azure (#3610) * Native YAML - cloud/amazon * Native YAML - cloud/azure --- cloud/amazon/cloudformation_facts.py | 3 ++- cloud/amazon/execute_lambda.py | 4 +++- cloud/amazon/lambda_alias.py | 3 ++- cloud/amazon/lambda_facts.py | 3 ++- cloud/azure/azure_rm_deployment.py | 24 ++++++++++++------------ 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/cloud/amazon/cloudformation_facts.py b/cloud/amazon/cloudformation_facts.py index 381e479fe6f..0e502ce5a33 100644 --- a/cloud/amazon/cloudformation_facts.py +++ b/cloud/amazon/cloudformation_facts.py @@ -68,7 +68,8 @@ stack_name: my-cloudformation-stack # Facts are published in ansible_facts['cloudformation'][] -- debug: msg={{ ansible_facts['cloudformation']['my-cloudformation-stack'] }} +- debug: + msg: '{{ ansible_facts['cloudformation']['my-cloudformation-stack'] }}' # Get all stack information about a stack - cloudformation_facts: diff --git a/cloud/amazon/execute_lambda.py b/cloud/amazon/execute_lambda.py index bd1b9288e2d..03ab4264072 100644 --- a/cloud/amazon/execute_lambda.py +++ b/cloud/amazon/execute_lambda.py @@ -108,7 +108,9 @@ register: response # the response will have a `logs` key that will contain a log (up to 4KB) of the function execution in Lambda. -- execute_lambda: name=test-function version_qualifier=PRODUCTION +- execute_lambda: + name: test-function + version_qualifier: PRODUCTION ''' RETURN = ''' diff --git a/cloud/amazon/lambda_alias.py b/cloud/amazon/lambda_alias.py index d744ca7346b..c85ecd2ee37 100644 --- a/cloud/amazon/lambda_alias.py +++ b/cloud/amazon/lambda_alias.py @@ -95,7 +95,8 @@ role: "arn:aws:iam::{{ account }}:role/API2LambdaExecRole" - name: show results - debug: var=lambda_facts + debug: + var: lambda_facts # The following will set the Dev alias to the latest version ($LATEST) since version is omitted (or = 0) - name: "alias 'Dev' for function {{ lambda_facts.FunctionName }} " diff --git a/cloud/amazon/lambda_facts.py b/cloud/amazon/lambda_facts.py index 9103f69df50..9c17df1dd37 100644 --- a/cloud/amazon/lambda_facts.py +++ b/cloud/amazon/lambda_facts.py @@ -82,7 +82,8 @@ query: all max_items: 20 - name: show Lambda facts - debug: var=lambda_facts + debug: + var: lambda_facts ''' RETURN = ''' diff --git a/cloud/azure/azure_rm_deployment.py b/cloud/azure/azure_rm_deployment.py index 226fb4e22fb..a1f924adc56 100644 --- a/cloud/azure/azure_rm_deployment.py +++ b/cloud/azure/azure_rm_deployment.py @@ -158,14 +158,19 @@ register: azure - name: Add new instance to host group - add_host: hostname={{ item['ips'][0].public_ip }} groupname=azure_vms + add_host: + hostname: '{{ item['ips'][0].public_ip }}' + groupname: azure_vms with_items: "{{ azure.deployment.instances }}" - hosts: azure_vms user: devopscle tasks: - name: Wait for SSH to come up - wait_for: port=22 timeout=2000 state=started + wait_for: + port: 22 + timeout: 2000 + state: started - name: echo the hostname of the vm shell: hostname @@ -243,15 +248,13 @@ vnetID: "[resourceId('Microsoft.Network/virtualNetworks',variables('virtualNetworkName'))]" subnetRef: "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]" resources: - - - type: "Microsoft.Storage/storageAccounts" + - type: "Microsoft.Storage/storageAccounts" name: "[parameters('newStorageAccountName')]" apiVersion: "2015-05-01-preview" location: "[variables('location')]" properties: accountType: "[variables('storageAccountType')]" - - - apiVersion: "2015-05-01-preview" + - apiVersion: "2015-05-01-preview" type: "Microsoft.Network/publicIPAddresses" name: "[variables('publicIPAddressName')]" location: "[variables('location')]" @@ -259,8 +262,7 @@ publicIPAllocationMethod: "[variables('publicIPAddressType')]" dnsSettings: domainNameLabel: "[parameters('dnsNameForPublicIP')]" - - - type: "Microsoft.Network/virtualNetworks" + - type: "Microsoft.Network/virtualNetworks" apiVersion: "2015-05-01-preview" name: "[variables('virtualNetworkName')]" location: "[variables('location')]" @@ -273,8 +275,7 @@ name: "[variables('subnetName')]" properties: addressPrefix: "[variables('subnetPrefix')]" - - - type: "Microsoft.Network/networkInterfaces" + - type: "Microsoft.Network/networkInterfaces" apiVersion: "2015-05-01-preview" name: "[variables('nicName')]" location: "[variables('location')]" @@ -291,8 +292,7 @@ id: "[resourceId('Microsoft.Network/publicIPAddresses',variables('publicIPAddressName'))]" subnet: id: "[variables('subnetRef')]" - - - type: "Microsoft.Compute/virtualMachines" + - type: "Microsoft.Compute/virtualMachines" apiVersion: "2015-06-15" name: "[variables('vmName')]" location: "[variables('location')]" From 0a6081a89f800f19400432c3f1f6dd7a2cb86ece Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 16:39:40 +0000 Subject: [PATCH 2450/2522] Cloud lxc (#3611) * Native YAML - cloud/lxc * debug var uses naked vars --- cloud/lxc/lxc_container.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index 22c72f43447..d83be951536 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -268,7 +268,8 @@ register: lvm_container_info - name: Debug info on container "test-container-lvm" - debug: var=lvm_container_info + debug: + var: lvm_container_info - name: Run a command in a container and ensure its in a "stopped" state. lxc_container: @@ -334,7 +335,8 @@ register: clone_container_info - name: debug info on container "test-container" - debug: var=clone_container_info + debug: + var: clone_container_info - name: Clone a container using snapshot lxc_container: @@ -364,7 +366,7 @@ - name: Destroy a container lxc_container: - name: "{{ item }}" + name: '{{ item }}' state: absent with_items: - test-container-stopped From 89a567f7fa4760799704cd421f31d9dcf69fff05 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 16:40:49 +0000 Subject: [PATCH 2451/2522] Native YAML - cloud/cloudstack (#3613) --- cloud/cloudstack/cs_instance.py | 43 +++++++++++++++++---------- cloud/cloudstack/cs_instance_facts.py | 7 +++-- cloud/cloudstack/cs_sshkeypair.py | 17 ++++++++--- cloud/cloudstack/cs_zone_facts.py | 7 +++-- 4 files changed, 49 insertions(+), 25 deletions(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 42ff6b42fa2..928746c2c97 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -222,8 +222,7 @@ EXAMPLES = ''' # Create a instance from an ISO # NOTE: Names of offerings and ISOs depending on the CloudStack configuration. -- local_action: - module: cs_instance +- cs_instance: name: web-vm-1 iso: Linux Debian 7 64-bit hypervisor: VMware @@ -236,50 +235,64 @@ - Server Integration - Sync Integration - Storage Integration + delegate_to: localhost # For changing a running instance, use the 'force' parameter -- local_action: - module: cs_instance +- cs_instance: name: web-vm-1 display_name: web-vm-01.example.com iso: Linux Debian 7 64-bit service_offering: 2cpu_2gb force: yes + delegate_to: localhost # Create or update a instance on Exoscale's public cloud using display_name. # Note: user_data can be used to kickstart the instance using cloud-init yaml config. -- local_action: - module: cs_instance +- cs_instance: display_name: web-vm-1 template: Linux Debian 7 64-bit service_offering: Tiny ssh_key: john@example.com tags: - - { key: admin, value: john } - - { key: foo, value: bar } + - key: admin + value: john + - key: foo + value: bar user_data: | #cloud-config packages: - nginx + delegate_to: localhost # Create an instance with multiple interfaces specifying the IP addresses -- local_action: - module: cs_instance +- cs_instance: name: web-vm-1 template: Linux Debian 7 64-bit service_offering: Tiny ip_to_networks: - - {'network': NetworkA, 'ip': '10.1.1.1'} - - {'network': NetworkB, 'ip': '192.0.2.1'} + - network: NetworkA + ip: 10.1.1.1 + - network: NetworkB + ip: 192.0.2.1 + delegate_to: localhost # Ensure an instance is stopped -- local_action: cs_instance name=web-vm-1 state=stopped +- cs_instance: + name: web-vm-1 + state: stopped + delegate_to: localhost # Ensure an instance is running -- local_action: cs_instance name=web-vm-1 state=started +- cs_instance: + name: web-vm-1 + state: started + delegate_to: localhost # Remove an instance -- local_action: cs_instance name=web-vm-1 state=absent +- cs_instance: + name: web-vm-1 + state: absent + delegate_to: localhost ''' RETURN = ''' diff --git a/cloud/cloudstack/cs_instance_facts.py b/cloud/cloudstack/cs_instance_facts.py index f405debca3f..4efaf645291 100644 --- a/cloud/cloudstack/cs_instance_facts.py +++ b/cloud/cloudstack/cs_instance_facts.py @@ -50,11 +50,12 @@ ''' EXAMPLES = ''' -- local_action: - module: cs_instance_facts +- cs_instance_facts: name: web-vm-1 + delegate_to: localhost -- debug: var=cloudstack_instance +- debug: + var: cloudstack_instance ''' RETURN = ''' diff --git a/cloud/cloudstack/cs_sshkeypair.py b/cloud/cloudstack/cs_sshkeypair.py index c0c73d9f3bc..d756059f7ef 100644 --- a/cloud/cloudstack/cs_sshkeypair.py +++ b/cloud/cloudstack/cs_sshkeypair.py @@ -64,15 +64,24 @@ EXAMPLES = ''' # create a new private / public key pair: -- local_action: cs_sshkeypair name=linus@example.com +- cs_sshkeypair: + name: linus@example.com + delegate_to: localhost register: key -- debug: msg='private key is {{ key.private_key }}' +- debug: + msg: 'Private key is {{ key.private_key }}' # remove a public key by its name: -- local_action: cs_sshkeypair name=linus@example.com state=absent +- cs_sshkeypair: + name: linus@example.com + state: absent + delegate_to: localhost # register your existing local public key: -- local_action: cs_sshkeypair name=linus@example.com public_key='{{ lookup('file', '~/.ssh/id_rsa.pub') }}' +- cs_sshkeypair: + name: linus@example.com + public_key: '{{ lookup('file', '~/.ssh/id_rsa.pub') }}' + delegate_to: localhost ''' RETURN = ''' diff --git a/cloud/cloudstack/cs_zone_facts.py b/cloud/cloudstack/cs_zone_facts.py index 7b5076659fd..2ce82423ec6 100644 --- a/cloud/cloudstack/cs_zone_facts.py +++ b/cloud/cloudstack/cs_zone_facts.py @@ -35,11 +35,12 @@ ''' EXAMPLES = ''' -- local_action: - module: cs_zone_facts +- cs_zone_facts: name: ch-gva-1 + delegate_to: localhost -- debug: var=cloudstack_zone +- debug: + var: cloudstack_zone ''' RETURN = ''' From 58b3a438531c6becf6b48224fdabe9ad4e9e916e Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 16:41:07 +0000 Subject: [PATCH 2452/2522] Normalize YAML - Cloud/VMWare (#3612) --- cloud/vmware/vsphere_copy.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/cloud/vmware/vsphere_copy.py b/cloud/vmware/vsphere_copy.py index 41971fa977d..8582c39c358 100644 --- a/cloud/vmware/vsphere_copy.py +++ b/cloud/vmware/vsphere_copy.py @@ -70,9 +70,23 @@ ''' EXAMPLES = ''' -- vsphere_copy: host=vhost login=vuser password=vpass src=/some/local/file datacenter='DC1 Someplace' datastore=datastore1 path=some/remote/file +- vsphere_copy: + host: vhost + login: vuser + password: vpass + src: /some/local/file + datacenter: DC1 Someplace + datastore: datastore1 + path: some/remote/file transport: local -- vsphere_copy: host=vhost login=vuser password=vpass src=/other/local/file datacenter='DC2 Someplace' datastore=datastore2 path=other/remote/file +- vsphere_copy: + host: vhost + login: vuser + password: vpass + src: /other/local/file + datacenter: DC2 Someplace + datastore: datastore2 + path: other/remote/file delegate_to: other_system ''' From d1d5a5ea85cf742fa5ee5aab8d8f81fc1a25af42 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 17:24:49 +0000 Subject: [PATCH 2453/2522] Fix spacing (#3616) --- monitoring/zabbix_hostmacro.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monitoring/zabbix_hostmacro.py b/monitoring/zabbix_hostmacro.py index a83a19548f1..c02f9b6eb98 100644 --- a/monitoring/zabbix_hostmacro.py +++ b/monitoring/zabbix_hostmacro.py @@ -92,8 +92,8 @@ login_user: username login_password: password host_name: ExampleHost - macro_name:Example macro - macro_value:Example value + macro_name: Example macro + macro_value: Example value state: present ''' From 5a521eec854c9ad1d08cc9e730024704cce884a6 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 17:39:49 +0000 Subject: [PATCH 2454/2522] Native YAML - windows (#3602) * Native YAML - windows * Fix baskslash * Sorry --- windows/win_file_version.py | 3 ++- windows/win_scheduled_task.py | 13 +++++++++++-- windows/win_updates.py | 21 ++++++++++++++------- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/windows/win_file_version.py b/windows/win_file_version.py index 4f2ecc0d615..71aae57135a 100644 --- a/windows/win_file_version.py +++ b/windows/win_file_version.py @@ -42,7 +42,8 @@ path: 'C:\Windows\System32\cmd.exe' register: exe_file_version -- debug: msg="{{exe_file_version}}" +- debug: + msg: '{{ exe_file_version }}' ''' diff --git a/windows/win_scheduled_task.py b/windows/win_scheduled_task.py index dc827026a57..5428a0b836e 100644 --- a/windows/win_scheduled_task.py +++ b/windows/win_scheduled_task.py @@ -84,6 +84,15 @@ ''' EXAMPLES = ''' - # Create a scheduled task to open a command prompt - win_scheduled_task: name="TaskName" execute="cmd" frequency="daily" time="9am" description="open command prompt" path="example" enable=yes state=present user=SYSTEM +# Create a scheduled task to open a command prompt +- win_scheduled_task: + name: TaskName + execute: cmd + frequency: daily + time: 9am + description: open command prompt + path: example + enable: yes + state: present + user: SYSTEM ''' diff --git a/windows/win_updates.py b/windows/win_updates.py index efdd1146add..8700126c180 100644 --- a/windows/win_updates.py +++ b/windows/win_updates.py @@ -69,15 +69,22 @@ ''' EXAMPLES = ''' - # Install all security, critical, and rollup updates - win_updates: - category_names: ['SecurityUpdates','CriticalUpdates','UpdateRollups'] +# Install all security, critical, and rollup updates +- win_updates: + category_names: + - SecurityUpdates + - CriticalUpdates + - UpdateRollups - # Install only security updates - win_updates: category_names=SecurityUpdates +# Install only security updates +- win_updates: + category_names: SecurityUpdates - # Search-only, return list of found updates (if any), log to c:\ansible_wu.txt - win_updates: category_names=SecurityUpdates state=searched log_path=c:/ansible_wu.txt +# Search-only, return list of found updates (if any), log to c:\ansible_wu.txt +- win_updates: + category_names: SecurityUpdates + state: searched + log_path: c:\ansible_wu.txt ''' RETURN = ''' From 58d1c77a361506f054dfe828dd7c0b3b3b35bb72 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 19:04:34 +0000 Subject: [PATCH 2455/2522] Native YAML - cloud/serverless (#3618) --- cloud/serverless.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cloud/serverless.py b/cloud/serverless.py index 473624200e6..e065139c71b 100644 --- a/cloud/serverless.py +++ b/cloud/serverless.py @@ -60,11 +60,13 @@ EXAMPLES = """ # Basic deploy of a service -- serverless: service_path={{ project_dir }} state=present +- serverless: + service_path: '{{ project_dir }}' + state: present # Deploy specific functions - serverless: - service_path: "{{ project_dir }}" + service_path: '{{ project_dir }}' functions: - my_func_one - my_func_two @@ -73,14 +75,14 @@ - serverless: stage: dev region: us-east-1 - service_path: "{{ project_dir }}" + service_path: '{{ project_dir }}' register: sls # The cloudformation stack is always named the same as the full service, so the # cloudformation_facts module can get a full list of the stack resources, as # well as stack events and outputs - cloudformation_facts: region: us-east-1 - stack_name: "{{ sls.service_name }}" + stack_name: '{{ sls.service_name }}' stack_resources: true """ From f75fdfd8c24bf258659651ff54be3534adc2e1b8 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 19:08:14 +0000 Subject: [PATCH 2456/2522] Native YAML - database/postgresql/postgresql_ext (#3617) --- database/postgresql/postgresql_ext.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/database/postgresql/postgresql_ext.py b/database/postgresql/postgresql_ext.py index 39c74afb4c6..b9fe87c4e69 100644 --- a/database/postgresql/postgresql_ext.py +++ b/database/postgresql/postgresql_ext.py @@ -70,7 +70,9 @@ EXAMPLES = ''' # Adds postgis to the database "acme" -- postgresql_ext: name=postgis db=acme +- postgresql_ext: + name: postgis + db: acme ''' try: From f41e4f4a297cbe8aa70a0b0345296d53cc85dc67 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 19:33:16 +0000 Subject: [PATCH 2457/2522] Native YAML - cloud/openstack (#3614) --- cloud/openstack/os_keystone_domain_facts.py | 9 ++++++--- cloud/openstack/os_project_facts.py | 12 ++++++++---- cloud/openstack/os_user_facts.py | 12 ++++++++---- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/cloud/openstack/os_keystone_domain_facts.py b/cloud/openstack/os_keystone_domain_facts.py index 5df2f2b7977..f62dc16f819 100644 --- a/cloud/openstack/os_keystone_domain_facts.py +++ b/cloud/openstack/os_keystone_domain_facts.py @@ -50,13 +50,15 @@ # Gather facts about previously created domain - os_keystone_domain_facts: cloud: awesomecloud -- debug: var=openstack_domains +- debug: + var: openstack_domains # Gather facts about a previously created domain by name - os_keystone_domain_facts: cloud: awesomecloud name: demodomain -- debug: var=openstack_domains +- debug: + var: openstack_domains # Gather facts about a previously created domain with filter - os_keystone_domain_facts @@ -64,7 +66,8 @@ name: demodomain filters: enabled: False -- debug: var=openstack_domains +- debug: + var: openstack_domains ''' diff --git a/cloud/openstack/os_project_facts.py b/cloud/openstack/os_project_facts.py index 87d3a1e9d76..a53acf532ea 100644 --- a/cloud/openstack/os_project_facts.py +++ b/cloud/openstack/os_project_facts.py @@ -55,20 +55,23 @@ # Gather facts about previously created projects - os_project_facts: cloud: awesomecloud -- debug: var=openstack_projects +- debug: + var: openstack_projects # Gather facts about a previously created project by name - os_project_facts: cloud: awesomecloud name: demoproject -- debug: var=openstack_projects +- debug: + var: openstack_projects # Gather facts about a previously created project in a specific domain - os_project_facts cloud: awesomecloud name: demoproject domain: admindomain -- debug: var=openstack_projects +- debug: + var: openstack_projects # Gather facts about a previously created project in a specific domain with filter @@ -78,7 +81,8 @@ domain: admindomain filters: enabled: False -- debug: var=openstack_projects +- debug: + var: openstack_projects ''' diff --git a/cloud/openstack/os_user_facts.py b/cloud/openstack/os_user_facts.py index db8cebe4757..4330eb430c8 100644 --- a/cloud/openstack/os_user_facts.py +++ b/cloud/openstack/os_user_facts.py @@ -55,20 +55,23 @@ # Gather facts about previously created users - os_user_facts: cloud: awesomecloud -- debug: var=openstack_users +- debug: + var: openstack_users # Gather facts about a previously created user by name - os_user_facts: cloud: awesomecloud name: demouser -- debug: var=openstack_users +- debug: + var: openstack_users # Gather facts about a previously created user in a specific domain - os_user_facts cloud: awesomecloud name: demouser domain: admindomain -- debug: var=openstack_users +- debug: + var: openstack_users # Gather facts about a previously created user in a specific domain with filter @@ -78,7 +81,8 @@ domain: admindomain filters: enabled: False -- debug: var=openstack_users +- debug: + var: openstack_users ''' From f77390962662e6c89ee28266179ab45fdcb8eab8 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 19:53:51 +0000 Subject: [PATCH 2458/2522] Add quotes for non-floats decimals (#3609) --- monitoring/airbrake_deployment.py | 2 +- monitoring/bigpanda.py | 6 +++--- monitoring/newrelic_deployment.py | 2 +- monitoring/rollbar_deployment.py | 2 +- windows/win_chocolatey.py | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/monitoring/airbrake_deployment.py b/monitoring/airbrake_deployment.py index 7b36c16a0a3..282d4c60d30 100644 --- a/monitoring/airbrake_deployment.py +++ b/monitoring/airbrake_deployment.py @@ -69,7 +69,7 @@ token: AAAAAA environment: staging user: ansible - revision: 4.2 + revision: '4.2' ''' import urllib diff --git a/monitoring/bigpanda.py b/monitoring/bigpanda.py index 1efce9580a1..7e818092f72 100644 --- a/monitoring/bigpanda.py +++ b/monitoring/bigpanda.py @@ -81,20 +81,20 @@ EXAMPLES = ''' - bigpanda: component: myapp - version: 1.3 + version: '1.3' token: '{{ bigpanda_token }}' state: started ... - bigpanda: component: myapp - version: 1.3 + version: '1.3' token: '{{ bigpanda_token }}' state: finished # If outside servers aren't reachable from your machine, use delegate_to and override hosts: - bigpanda: component: myapp - version: 1.3 + version: '1.3' token: '{{ bigpanda_token }}' hosts: '{{ ansible_hostname }}' state: started diff --git a/monitoring/newrelic_deployment.py b/monitoring/newrelic_deployment.py index 8b6cc7a83a1..6224fbf48d7 100644 --- a/monitoring/newrelic_deployment.py +++ b/monitoring/newrelic_deployment.py @@ -80,7 +80,7 @@ token: AAAAAA app_name: myapp user: ansible deployment - revision: 1.0 + revision: '1.0' ''' import urllib diff --git a/monitoring/rollbar_deployment.py b/monitoring/rollbar_deployment.py index ac7b2c650a2..c13186474c2 100644 --- a/monitoring/rollbar_deployment.py +++ b/monitoring/rollbar_deployment.py @@ -72,7 +72,7 @@ token: AAAAAA environment: staging user: ansible - revision: 4.2 + revision: '4.2' rollbar_user: admin comment: Test Deploy ''' diff --git a/windows/win_chocolatey.py b/windows/win_chocolatey.py index ac80ad9e18a..d6993502bc1 100644 --- a/windows/win_chocolatey.py +++ b/windows/win_chocolatey.py @@ -102,7 +102,7 @@ # Install notepadplusplus version 6.6 win_chocolatey: name: notepadplusplus.install - version: 6.6 + version: '6.6' # Uninstall git win_chocolatey: From ce902b69aaa15ea38909b4b94cecce0fe67c7702 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Fri, 2 Dec 2016 14:49:23 +0000 Subject: [PATCH 2459/2522] Native YAML - cloud/misc (#3615) * Native YAML - cloud/misc * Fix mistake --- cloud/misc/proxmox.py | 99 ++++++++++++++++++++++++++++++---- cloud/misc/proxmox_template.py | 31 +++++++++-- cloud/misc/virt.py | 18 ++++--- cloud/misc/virt_net.py | 49 ++++++++++++----- cloud/misc/virt_pool.py | 57 ++++++++++++++------ 5 files changed, 205 insertions(+), 49 deletions(-) diff --git a/cloud/misc/proxmox.py b/cloud/misc/proxmox.py index 709f3e0dc77..694d79e9267 100644 --- a/cloud/misc/proxmox.py +++ b/cloud/misc/proxmox.py @@ -176,37 +176,114 @@ EXAMPLES = ''' # Create new container with minimal options -- proxmox: vmid=100 node='uk-mc02' api_user='root@pam' api_password='1q2w3e' api_host='node1' password='123456' hostname='example.org' ostemplate='local:vztmpl/ubuntu-14.04-x86_64.tar.gz' +- proxmox: + vmid: 100 + node: uk-mc02 + api_user: root@pam + api_password: 1q2w3e + api_host: node1 + password: 123456 + hostname: example.org + ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz' # Create new container with minimal options with force(it will rewrite existing container) -- proxmox: vmid=100 node='uk-mc02' api_user='root@pam' api_password='1q2w3e' api_host='node1' password='123456' hostname='example.org' ostemplate='local:vztmpl/ubuntu-14.04-x86_64.tar.gz' force=yes +- proxmox: + vmid: 100 + node: uk-mc02 + api_user: root@pam + api_password: 1q2w3e + api_host: node1 + password: 123456 + hostname: example.org + ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz' + force: yes # Create new container with minimal options use environment PROXMOX_PASSWORD variable(you should export it before) -- proxmox: vmid=100 node='uk-mc02' api_user='root@pam' api_host='node1' password='123456' hostname='example.org' ostemplate='local:vztmpl/ubuntu-14.04-x86_64.tar.gz' +- proxmox: + vmid: 100 + node: uk-mc02 + api_user: root@pam + api_host: node1 + password: 123456 + hostname: example.org + ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz' # Create new container with minimal options defining network interface with dhcp -- proxmox: vmid=100 node='uk-mc02' api_user='root@pam' api_password='1q2w3e' api_host='node1' password='123456' hostname='example.org' ostemplate='local:vztmpl/ubuntu-14.04-x86_64.tar.gz' netif='{"net0":"name=eth0,ip=dhcp,ip6=dhcp,bridge=vmbr0"}' +- proxmox: + vmid: 100 + node: uk-mc02 + api_user: root@pam + api_password: 1q2w3e + api_host: node1 + password: 123456 + hostname: example.org + ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz' + netif: '{"net0":"name=eth0,ip=dhcp,ip6=dhcp,bridge=vmbr0"}' # Create new container with minimal options defining network interface with static ip -- proxmox: vmid=100 node='uk-mc02' api_user='root@pam' api_password='1q2w3e' api_host='node1' password='123456' hostname='example.org' ostemplate='local:vztmpl/ubuntu-14.04-x86_64.tar.gz' netif='{"net0":"name=eth0,gw=192.168.0.1,ip=192.168.0.2/24,bridge=vmbr0"}' +- proxmox: + vmid: 100 + node: uk-mc02 + api_user: root@pam + api_password: 1q2w3e + api_host: node1 + password: 123456 + hostname: example.org + ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz' + netif: '{"net0":"name=eth0,gw=192.168.0.1,ip=192.168.0.2/24,bridge=vmbr0"}' # Create new container with minimal options defining a mount -- proxmox: vmid=100 node='uk-mc02' api_user='root@pam' api_password='1q2w3e' api_host='node1' password='123456' hostname='example.org' ostemplate='local:vztmpl/ubuntu-14.04-x86_64.tar.gz' mounts='{"mp0":"local:8,mp=/mnt/test/"}' +- proxmox: + vmid: 100 + node: uk-mc02 + api_user: root@pam + api_password: 1q2w3e + api_host: node1 + password: 123456 + hostname: example.org + ostemplate: local:vztmpl/ubuntu-14.04-x86_64.tar.gz' + mounts: '{"mp0":"local:8,mp=/mnt/test/"}' # Start container -- proxmox: vmid=100 api_user='root@pam' api_password='1q2w3e' api_host='node1' state=started +- proxmox: + vmid: 100 + api_user: root@pam + api_password: 1q2w3e + api_host: node1 + state: started # Stop container -- proxmox: vmid=100 api_user='root@pam' api_password='1q2w3e' api_host='node1' state=stopped +- proxmox: + vmid: 100 + api_user: root@pam + api_password: 1q2w3e + api_host: node1 + state: stopped # Stop container with force -- proxmox: vmid=100 api_user='root@pam' api_password='1q2w3e' api_host='node1' force=yes state=stopped +- proxmox: + vmid: 100 + api_user: root@pam + api_passwordL 1q2w3e + api_host: node1 + force: yes + state: stopped # Restart container(stopped or mounted container you can't restart) -- proxmox: vmid=100 api_user='root@pam' api_password='1q2w3e' api_host='node1' state=stopped +- proxmox: + vmid: 100 + api_user: root@pam + api_password: 1q2w3e + api_host: node1 + state: stopped # Remove container -- proxmox: vmid=100 api_user='root@pam' api_password='1q2w3e' api_host='node1' state=absent +- proxmox: + vmid: 100 + api_user: root@pam + api_password: 1q2w3e + api_host: node1 + state: absent ''' import os diff --git a/cloud/misc/proxmox_template.py b/cloud/misc/proxmox_template.py index 6286bb4c720..69a2272408f 100644 --- a/cloud/misc/proxmox_template.py +++ b/cloud/misc/proxmox_template.py @@ -98,16 +98,39 @@ EXAMPLES = ''' # Upload new openvz template with minimal options -- proxmox_template: node='uk-mc02' api_user='root@pam' api_password='1q2w3e' api_host='node1' src='~/ubuntu-14.04-x86_64.tar.gz' +- proxmox_template: + node: uk-mc02 + api_user: root@pam + api_password: 1q2w3e + api_host: node1 + src: ~/ubuntu-14.04-x86_64.tar.gz # Upload new openvz template with minimal options use environment PROXMOX_PASSWORD variable(you should export it before) -- proxmox_template: node='uk-mc02' api_user='root@pam' api_host='node1' src='~/ubuntu-14.04-x86_64.tar.gz' +- proxmox_template: + node: uk-mc02 + api_user: root@pam + api_host: node1 + src: ~/ubuntu-14.04-x86_64.tar.gz # Upload new openvz template with all options and force overwrite -- proxmox_template: node='uk-mc02' api_user='root@pam' api_password='1q2w3e' api_host='node1' storage='local' content_type='vztmpl' src='~/ubuntu-14.04-x86_64.tar.gz' force=yes +- proxmox_template: + node: uk-mc02 + api_user: root@pam + api_password: 1q2w3e + api_host: node1 + storage: local + content_type: vztmpl + src: ~/ubuntu-14.04-x86_64.tar.gz + force: yes # Delete template with minimal options -- proxmox_template: node='uk-mc02' api_user='root@pam' api_password='1q2w3e' api_host='node1' template='ubuntu-14.04-x86_64.tar.gz' state=absent +- proxmox_template: + node: uk-mc02 + api_user: root@pam + api_password: 1q2w3e + api_host: node1 + template: ubuntu-14.04-x86_64.tar.gz + state: absent ''' import os diff --git a/cloud/misc/virt.py b/cloud/misc/virt.py index 912eb3b9add..8c1e2969ac2 100644 --- a/cloud/misc/virt.py +++ b/cloud/misc/virt.py @@ -66,7 +66,9 @@ EXAMPLES = ''' # a playbook task line: -- virt: name=alpha state=running +- virt: + name: alpha + state: running # /usr/bin/ansible invocations ansible host -m virt -a "name=alpha command=status" @@ -76,12 +78,16 @@ # a playbook example of defining and launching an LXC guest tasks: - name: define vm - virt: name=foo - command=define - xml="{{ lookup('template', 'container-template.xml.j2') }}" - uri=lxc:/// + virt: + name: foo + command: define + xml: '{{ lookup('template', 'container-template.xml.j2') }}' + uri: 'lxc:///' - name: start vm - virt: name=foo state=running uri=lxc:/// + virt: + name: foo + state: running + uri: 'lxc:///' ''' RETURN = ''' diff --git a/cloud/misc/virt_net.py b/cloud/misc/virt_net.py index 8b277e439fb..c389520e424 100644 --- a/cloud/misc/virt_net.py +++ b/cloud/misc/virt_net.py @@ -74,43 +74,66 @@ EXAMPLES = ''' # Define a new network -- virt_net: command=define name=br_nat xml='{{ lookup("template", "network/bridge.xml.j2") }}' +- virt_net: + command: define + name: br_nat + xml: '{{ lookup("template", "network/bridge.xml.j2") }}' # Start a network -- virt_net: command=create name=br_nat +- virt_net: + command: create + name: br_nat # List available networks -- virt_net: command=list_nets +- virt_net: + command: list_nets # Get XML data of a specified network -- virt_net: command=get_xml name=br_nat +- virt_net: + command: get_xml + name: br_nat # Stop a network -- virt_net: command=destroy name=br_nat +- virt_net: + command: destroy + name: br_nat # Undefine a network -- virt_net: command=undefine name=br_nat +- virt_net: + command: undefine + name: br_nat # Gather facts about networks # Facts will be available as 'ansible_libvirt_networks' -- virt_net: command=facts +- virt_net: + command: facts # Gather information about network managed by 'libvirt' remotely using uri -- virt_net: command=info uri='{{ item }}' - with_items: "{{ libvirt_uris }}" +- virt_net: + command: info + uri: '{{ item }}' + with_items: '{{ libvirt_uris }}' register: networks # Ensure that a network is active (needs to be defined and built first) -- virt_net: state=active name=br_nat +- virt_net: + state: active + name: br_nat # Ensure that a network is inactive -- virt_net: state=inactive name=br_nat +- virt_net: + state: inactive + name: br_nat # Ensure that a given network will be started at boot -- virt_net: autostart=yes name=br_nat +- virt_net: + autostart: yes + name: br_nat # Disable autostart for a given network -- virt_net: autostart=no name=br_nat +- virt_net: + autostart: no + name: br_nat ''' VIRT_FAILED = 1 diff --git a/cloud/misc/virt_pool.py b/cloud/misc/virt_pool.py index 7684125bf8a..ad5bcc66112 100644 --- a/cloud/misc/virt_pool.py +++ b/cloud/misc/virt_pool.py @@ -79,49 +79,76 @@ EXAMPLES = ''' # Define a new storage pool -- virt_pool: command=define name=vms xml='{{ lookup("template", "pool/dir.xml.j2") }}' +- virt_pool: + command: define + name: vms + xml: '{{ lookup("template", "pool/dir.xml.j2") }}' # Build a storage pool if it does not exist -- virt_pool: command=build name=vms +- virt_pool: + command: build + name: vms # Start a storage pool -- virt_pool: command=create name=vms +- virt_pool: + command: create + name: vms # List available pools -- virt_pool: command=list_pools +- virt_pool: + command: list_pools # Get XML data of a specified pool -- virt_pool: command=get_xml name=vms +- virt_pool: + command: get_xml + name: vms # Stop a storage pool -- virt_pool: command=destroy name=vms +- virt_pool: + command: destroy + name: vms # Delete a storage pool (destroys contents) -- virt_pool: command=delete name=vms +- virt_pool: + command: delete + name: vms # Undefine a storage pool -- virt_pool: command=undefine name=vms +- virt_pool: + command: undefine + name: vms # Gather facts about storage pools # Facts will be available as 'ansible_libvirt_pools' -- virt_pool: command=facts +- virt_pool: + command: facts # Gather information about pools managed by 'libvirt' remotely using uri -- virt_pool: command=info uri='{{ item }}' - with_items: "{{ libvirt_uris }}" +- virt_pool: + command: info + uri: '{{ item }}' + with_items: '{{ libvirt_uris }}' register: storage_pools # Ensure that a pool is active (needs to be defined and built first) -- virt_pool: state=active name=vms +- virt_pool: + state: active + name: vms # Ensure that a pool is inactive -- virt_pool: state=inactive name=vms +- virt_pool: + state: inactive + name: vms # Ensure that a given pool will be started at boot -- virt_pool: autostart=yes name=vms +- virt_pool: + autostart: yes + name: vms # Disable autostart for a given pool -- virt_pool: autostart=no name=vms +- virt_pool: + autostart: no + name: vms ''' VIRT_FAILED = 1 From 414f62b2c50de975d57868183bb1cb3a25d9f151 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Fri, 2 Dec 2016 15:09:20 +0000 Subject: [PATCH 2460/2522] Native YAML - notification leftovers (#3626) --- notification/jabber.py | 18 +++++++++------- notification/mail.py | 47 +++++++++++++++++++++++------------------- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/notification/jabber.py b/notification/jabber.py index d881d28132c..d9fa0015157 100644 --- a/notification/jabber.py +++ b/notification/jabber.py @@ -66,16 +66,18 @@ EXAMPLES = ''' # send a message to a user -- jabber: user=mybot@example.net - password=secret - to=friend@example.net - msg="Ansible task finished" +- jabber: + user: mybot@example.net + password: secret + to: friend@example.net + msg: Ansible task finished # send a message to a room -- jabber: user=mybot@example.net - password=secret - to=mychaps@conference.example.net/ansiblebot - msg="Ansible task finished" +- jabber: + user: mybot@example.net + password: secret + to: mychaps@conference.example.net/ansiblebot + msg: Ansible task finished # send a message, specifying the host and port - jabber diff --git a/notification/mail.py b/notification/mail.py index 5cab0d3584a..fbbdcff2674 100644 --- a/notification/mail.py +++ b/notification/mail.py @@ -120,35 +120,40 @@ EXAMPLES = ''' # Example playbook sending mail to root -- local_action: mail subject='System {{ ansible_hostname }} has been successfully provisioned.' +- mail: + subject: 'System {{ ansible_hostname }} has been successfully provisioned.' + delegate_to: localhost # Sending an e-mail using Gmail SMTP servers -- local_action: mail - host='smtp.gmail.com' - port=587 - username=username@gmail.com - password='mysecret' - to="John Smith " - subject='Ansible-report' - body='System {{ ansible_hostname }} has been successfully provisioned.' +- mail: + host: smtp.gmail.com + port: 587 + username: username@gmail.com + password: mysecret + to: John Smith + subject: Ansible-report + body: 'System {{ ansible_hostname }} has been successfully provisioned.' + delegate_to: localhost # Send e-mail to a bunch of users, attaching files -- local_action: mail - host='127.0.0.1' - port=2025 - subject="Ansible-report" - body="Hello, this is an e-mail. I hope you like it ;-)" - from="jane@example.net (Jane Jolie)" - to="John Doe , Suzie Something " - cc="Charlie Root " - attach="/etc/group /tmp/pavatar2.png" - headers=Reply-To=john@example.com|X-Special="Something or other" - charset=utf8 +- mail: + host: 127.0.0.1 + port: 2025 + subject: Ansible-report + body: Hello, this is an e-mail. I hope you like it ;-) + from: jane@example.net (Jane Jolie) + to: John Doe , Suzie Something + cc: Charlie Root + attach: /etc/group /tmp/pavatar2.png + headers: 'Reply-To=john@example.com|X-Special="Something or other"' + charset: utf8 + delegate_to: localhost + # Sending an e-mail using the remote machine, not the Ansible controller node - mail: host: localhost port: 25 - to: 'John Smith ' + to: John Smith subject: Ansible-report body: 'System {{ ansible_hostname }} has been successfully provisioned.' ''' From a2f1545316f14749b678b2c3a367c82e90053d00 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Fri, 2 Dec 2016 15:10:44 +0000 Subject: [PATCH 2461/2522] Native YAML - cloud/amazon/lambda.py (#3628) --- cloud/amazon/lambda.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/lambda.py b/cloud/amazon/lambda.py index 993effce025..bbabe39949a 100644 --- a/cloud/amazon/lambda.py +++ b/cloud/amazon/lambda.py @@ -119,8 +119,10 @@ - sg-123abcde - sg-edcba321 with_items: - - { name: HelloWorld, zip_file: 'hello-code.zip' } - - { name: ByeBye, zip_file: 'bye-code.zip' } + - name: HelloWorld + zip_file: hello-code.zip + - name: ByeBye + zip_file: bye-code.zip # Basic Lambda function deletion tasks: From 2fdc5e09308897776f097d0b29b43770f522c665 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Fri, 2 Dec 2016 15:14:19 +0000 Subject: [PATCH 2462/2522] Native YAML - database/misc/mongodb_user.py (#3631) --- database/misc/mongodb_user.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index a022d6aa9a8..e4280827c3b 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -180,7 +180,8 @@ state: present replica_set: belcher roles: - - { db: "local" , role: "read" } + - db: local + role: read ''' From 58a0ffa553023ff212d69e815ba68ef50c69a8f1 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Fri, 2 Dec 2016 15:48:22 +0000 Subject: [PATCH 2463/2522] Native YAML - system (#3625) * Native YAML - system * Remove comment that is not applicable to the code --- system/debconf.py | 21 +++++++-- system/filesystem.py | 9 +++- system/firewalld.py | 50 ++++++++++++++++---- system/getent.py | 36 +++++++++++---- system/gluster_volume.py | 41 ++++++++++++++--- system/iptables.py | 36 ++++++++++++--- system/kernel_blacklist.py | 4 +- system/known_hosts.py | 8 ++-- system/locale_gen.py | 4 +- system/lvg.py | 13 ++++-- system/lvol.py | 89 +++++++++++++++++++++++++++++------- system/make.py | 7 ++- system/modprobe.py | 10 +++- system/open_iscsi.py | 18 ++++++-- system/openwrt_init.py | 15 ++++-- system/osx_defaults.py | 40 +++++++++++++--- system/pam_limits.py | 20 ++++++-- system/puppet.py | 16 +++++-- system/sefcontext.py | 7 ++- system/selinux_permissive.py | 4 +- system/seport.py | 20 ++++++-- system/solaris_zone.py | 46 ++++++++++++++----- system/svc.py | 25 +++++++--- system/timezone.py | 3 +- system/ufw.py | 79 ++++++++++++++++++++++++-------- system/zfs.py | 28 +++++++++--- 26 files changed, 514 insertions(+), 135 deletions(-) diff --git a/system/debconf.py b/system/debconf.py index 05e545a7ed6..468f0b5725e 100644 --- a/system/debconf.py +++ b/system/debconf.py @@ -73,16 +73,29 @@ EXAMPLES = ''' # Set default locale to fr_FR.UTF-8 -debconf: name=locales question='locales/default_environment_locale' value=fr_FR.UTF-8 vtype='select' +- debconf: + name: locales + question: locales/default_environment_locale + value: fr_FR.UTF-8 + vtype: select # set to generate locales: -debconf: name=locales question='locales/locales_to_be_generated' value='en_US.UTF-8 UTF-8, fr_FR.UTF-8 UTF-8' vtype='multiselect' +- debconf: + name: locales + question: locales/locales_to_be_generated + value: en_US.UTF-8 UTF-8, fr_FR.UTF-8 UTF-8 + vtype: multiselect # Accept oracle license -debconf: name='oracle-java7-installer' question='shared/accepted-oracle-license-v1-1' value='true' vtype='select' +- debconf: + name: oracle-java7-installer + question: shared/accepted-oracle-license-v1-1 + value: true + vtype: select # Specifying package you can register/return the list of questions and current values -debconf: name='tzdata' +- debconf: + name: tzdata ''' def get_selections(module, pkg): diff --git a/system/filesystem.py b/system/filesystem.py index 10fa5afbb1b..70c7c320b31 100644 --- a/system/filesystem.py +++ b/system/filesystem.py @@ -58,10 +58,15 @@ EXAMPLES = ''' # Create a ext2 filesystem on /dev/sdb1. -- filesystem: fstype=ext2 dev=/dev/sdb1 +- filesystem: + fstype: ext2 + dev: /dev/sdb1 # Create a ext4 filesystem on /dev/sdb1 and check disk blocks. -- filesystem: fstype=ext4 dev=/dev/sdb1 opts="-cc" +- filesystem: + fstype: ext4 + dev: /dev/sdb1 + opts: -cc ''' def _get_dev_size(dev, module): diff --git a/system/firewalld.py b/system/firewalld.py index 5aae5fc0a79..83f78b049e1 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -94,14 +94,48 @@ ''' EXAMPLES = ''' -- firewalld: service=https permanent=true state=enabled -- firewalld: port=8081/tcp permanent=true state=disabled -- firewalld: port=161-162/udp permanent=true state=enabled -- firewalld: zone=dmz service=http permanent=true state=enabled -- firewalld: rich_rule='rule service name="ftp" audit limit value="1/m" accept' permanent=true state=enabled -- firewalld: source='192.0.2.0/24' zone=internal state=enabled -- firewalld: zone=trusted interface=eth2 permanent=true state=enabled -- firewalld: masquerade=yes state=enabled permanent=true zone=dmz +- firewalld: + service: https + permanent: true + state: enabled + +- firewalld: + port: 8081/tcp + permanent: true + state: disabled + +- firewalld: + port: 161-162/udp + permanent: true + state: enabled + +- firewalld: + zone: dmz + service: http + permanent: true + state: enabled + +- firewalld: + rich_rule: 'rule service name="ftp" audit limit value="1/m" accept' + permanent: true + state: enabled + +- firewalld: + source: 192.0.2.0/24 + zone: internal + state: enabled + +- firewalld: + zone: trusted + interface: eth2 + permanent: true + state: enabled + +- firewalld: + masquerade: yes + state: enabled + permanent: true + zone: dmz ''' from ansible.module_utils.basic import AnsibleModule diff --git a/system/getent.py b/system/getent.py index 37bfc244dea..2b70a2856c5 100644 --- a/system/getent.py +++ b/system/getent.py @@ -59,24 +59,40 @@ EXAMPLES = ''' # get root user info -- getent: database=passwd key=root -- debug: var=getent_passwd +- getent: + database: passwd + key: root +- debug: + var: getent_passwd # get all groups -- getent: database=group split=':' -- debug: var=getent_group +- getent: + database: group + split: ':' +- debug: + var: getent_group # get all hosts, split by tab -- getent: database=hosts -- debug: var=getent_hosts +- getent: + database: hosts +- debug: + var: getent_hosts # get http service info, no error if missing -- getent: database=services key=http fail_key=False -- debug: var=getent_services +- getent: + database: services + key: http + fail_key: False +- debug: + var: getent_services # get user password hash (requires sudo/root) -- getent: database=shadow key=www-data split=: -- debug: var=getent_shadow +- getent: + database: shadow + key: www-data + split: ':' +- debug: + var: getent_shadow ''' diff --git a/system/gluster_volume.py b/system/gluster_volume.py index 44ff2780c86..f5bca5f9e83 100644 --- a/system/gluster_volume.py +++ b/system/gluster_volume.py @@ -121,26 +121,53 @@ EXAMPLES = """ - name: create gluster volume - gluster_volume: state=present name=test1 bricks=/bricks/brick1/g1 rebalance=yes cluster="192.0.2.10,192.0.2.11" + gluster_volume: + state: present + name: test1 + bricks: /bricks/brick1/g1 + rebalance: yes + cluster: + - 192.0.2.10 + - 192.0.2.11 run_once: true - name: tune - gluster_volume: state=present name=test1 options='{performance.cache-size: 256MB}' + gluster_volume: + state: present + name: test1 + options: + performance.cache-size: 256MB - name: start gluster volume - gluster_volume: state=started name=test1 + gluster_volume: + state: started + name: test1 - name: limit usage - gluster_volume: state=present name=test1 directory=/foo quota=20.0MB + gluster_volume: + state: present + name: test1 + directory: /foo + quota: 20.0MB - name: stop gluster volume - gluster_volume: state=stopped name=test1 + gluster_volume: + state: stopped + name: test1 - name: remove gluster volume - gluster_volume: state=absent name=test1 + gluster_volume: + state: absent + name: test1 - name: create gluster volume with multiple bricks - gluster_volume: state=present name=test2 bricks="/bricks/brick1/g2,/bricks/brick2/g2" cluster="192.0.2.10,192.0.2.11" + gluster_volume: + state: present + name: test2 + bricks: /bricks/brick1/g2,/bricks/brick2/g2 + cluster: + - 192.0.2.10 + - 192.0.2.11 run_once: true """ diff --git a/system/iptables.py b/system/iptables.py index 5d055182367..8a08e38d785 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -295,25 +295,49 @@ EXAMPLES = ''' # Block specific IP -- iptables: chain=INPUT source=8.8.8.8 jump=DROP +- iptables: + chain: INPUT + source: 8.8.8.8 + jump: DROP become: yes # Forward port 80 to 8600 -- iptables: table=nat chain=PREROUTING in_interface=eth0 protocol=tcp match=tcp destination_port=80 jump=REDIRECT to_ports=8600 comment="Redirect web traffic to port 8600" +- iptables: + table: nat + chain: PREROUTING + in_interface: eth0 + protocol: tcp + match: tcp + destination_port: 80 + jump: REDIRECT + to_ports: 8600 + comment: Redirect web traffic to port 8600 become: yes # Allow related and established connections -- iptables: chain=INPUT ctstate=ESTABLISHED,RELATED jump=ACCEPT +- iptables: + chain: INPUT + ctstate: ESTABLISHED,RELATED + jump: ACCEPT become: yes # Tag all outbound tcp packets with DSCP mark 8 -- iptables: chain=OUTPUT jump=DSCP table=mangle set_dscp_mark=8 protocol=tcp +- iptables: + chain: OUTPUT + jump: DSCP + table: mangle + set_dscp_mark: 8 + protocol: tcp # Tag all outbound tcp packets with DSCP DiffServ class CS1 -- iptables: chain=OUTPUT jump=DSCP table=mangle set_dscp_mark_class=CS1 protocol=tcp +- iptables: + chain: OUTPUT + jump: DSCP + table: mangle + set_dscp_mark_class: CS1 + protocol: tcp ''' - def append_param(rule, param, flag, is_list): if is_list: for item in param: diff --git a/system/kernel_blacklist.py b/system/kernel_blacklist.py index 296a082a2ea..2100b158fda 100644 --- a/system/kernel_blacklist.py +++ b/system/kernel_blacklist.py @@ -52,7 +52,9 @@ EXAMPLES = ''' # Blacklist the nouveau driver module -- kernel_blacklist: name=nouveau state=present +- kernel_blacklist: + name: nouveau + state: present ''' diff --git a/system/known_hosts.py b/system/known_hosts.py index 810759989f4..40c13002ddc 100644 --- a/system/known_hosts.py +++ b/system/known_hosts.py @@ -62,11 +62,11 @@ ''' EXAMPLES = ''' -# Example using with_file to set the system known_hosts file - name: tell the host about our servers it might want to ssh to - known_hosts: path='/etc/ssh/ssh_known_hosts' - name='foo.com.invalid' - key="{{ lookup('file', 'pubkeys/foo.com.invalid') }}" + known_hosts: + path: /etc/ssh/ssh_known_hosts + name: foo.com.invalid + key: "{{ lookup('file', 'pubkeys/foo.com.invalid') }}" ''' # Makes sure public host keys are present or absent in the given known_hosts diff --git a/system/locale_gen.py b/system/locale_gen.py index c8c5128d21e..57e79a25b73 100644 --- a/system/locale_gen.py +++ b/system/locale_gen.py @@ -41,7 +41,9 @@ EXAMPLES = ''' # Ensure a locale exists. -- locale_gen: name=de_CH.UTF-8 state=present +- locale_gen: + name: de_CH.UTF-8 + state: present ''' import os diff --git a/system/lvg.py b/system/lvg.py index 2d2710e38bc..d0b0409a634 100644 --- a/system/lvg.py +++ b/system/lvg.py @@ -66,17 +66,24 @@ EXAMPLES = ''' # Create a volume group on top of /dev/sda1 with physical extent size = 32MB. -- lvg: vg=vg.services pvs=/dev/sda1 pesize=32 +- lvg: + vg: vg.services + pvs: /dev/sda1 + pesize: 32 # Create or resize a volume group on top of /dev/sdb1 and /dev/sdc5. # If, for example, we already have VG vg.services on top of /dev/sdb1, # this VG will be extended by /dev/sdc5. Or if vg.services was created on # top of /dev/sda5, we first extend it with /dev/sdb1 and /dev/sdc5, # and then reduce by /dev/sda5. -- lvg: vg=vg.services pvs=/dev/sdb1,/dev/sdc5 +- lvg: + vg: vg.services + pvs: /dev/sdb1,/dev/sdc5 # Remove a volume group with name vg.services. -- lvg: vg=vg.services state=absent +- lvg: + vg: vg.services + state: absent ''' def parse_vgs(data): diff --git a/system/lvol.py b/system/lvol.py index 978ce7d1c5f..c3213bdd241 100644 --- a/system/lvol.py +++ b/system/lvol.py @@ -92,52 +92,109 @@ EXAMPLES = ''' # Create a logical volume of 512m. -- lvol: vg=firefly lv=test size=512 +- lvol: + vg: firefly + lv: test + size: 512 # Create a logical volume of 512m with disks /dev/sda and /dev/sdb -- lvol: vg=firefly lv=test size=512 pvs=/dev/sda,/dev/sdb +- lvol: + vg: firefly + lv: test + size: 512 + pvs: /dev/sda,/dev/sdb # Create cache pool logical volume -- lvol: vg=firefly lv=lvcache size=512m opts='--type cache-pool' +- lvol: + vg: firefly + lv: lvcache + size: 512m + opts: --type cache-pool # Create a logical volume of 512g. -- lvol: vg=firefly lv=test size=512g +- lvol: + vg: firefly + lv: test + size: 512g # Create a logical volume the size of all remaining space in the volume group -- lvol: vg=firefly lv=test size=100%FREE +- lvol: + vg: firefly + lv: test + size: 100%FREE # Create a logical volume with special options -- lvol: vg=firefly lv=test size=512g opts="-r 16" +- lvol: + vg: firefly + lv: test + size: 512g + opts: -r 16 # Extend the logical volume to 1024m. -- lvol: vg=firefly lv=test size=1024 +- lvol: + vg: firefly + lv: test + size: 1024 # Extend the logical volume to consume all remaining space in the volume group -- lvol: vg=firefly lv=test size=+100%FREE +- lvol: + vg: firefly + lv: test + size: +100%FREE # Extend the logical volume to take all remaining space of the PVs -- lvol: vg=firefly lv=test size=100%PVS +- lvol: + vg: firefly + lv: test + size: 100%PVS # Resize the logical volume to % of VG -- lvol: vg-firefly lv=test size=80%VG force=yes +- lvol: + vg: firefly + lv: test + size: 80%VG + force: yes # Reduce the logical volume to 512m -- lvol: vg=firefly lv=test size=512 force=yes +- lvol: + vg: firefly + lv: test + size: 512 + force: yes # Set the logical volume to 512m and do not try to shrink if size is lower than current one -- lvol: vg=firefly lv=test size=512 shrink=no +- lvol: + vg: firefly + lv: test + size: 512 + shrink: no # Remove the logical volume. -- lvol: vg=firefly lv=test state=absent force=yes +- lvol: + vg: firefly + lv: test + state: absent + force: yes # Create a snapshot volume of the test logical volume. -- lvol: vg=firefly lv=test snapshot=snap1 size=100m +- lvol: + vg: firefly + lv: test + snapshot: snap1 + size: 100m # Deactivate a logical volume -- lvol: vg=firefly lv=test active=false +- lvol: + vg: firefly + lv: test + active: false # Create a deactivated logical volume -- lvol: vg=firefly lv=test size=512g active=false +- lvol: + vg: firefly + lv: test + size: 512g + active: false ''' import re diff --git a/system/make.py b/system/make.py index 497b21328ba..5207470bb0d 100644 --- a/system/make.py +++ b/system/make.py @@ -46,10 +46,13 @@ EXAMPLES = ''' # Build the default target -- make: chdir=/home/ubuntu/cool-project +- make: + chdir: /home/ubuntu/cool-project # Run `install` target as root -- make: chdir=/home/ubuntu/cool-project target=install +- make: + chdir: /home/ubuntu/cool-project + target: install become: yes # Pass in extra arguments to build diff --git a/system/modprobe.py b/system/modprobe.py index 1bb1d3f70b1..1acd2ef3ed3 100644 --- a/system/modprobe.py +++ b/system/modprobe.py @@ -52,9 +52,15 @@ EXAMPLES = ''' # Add the 802.1q module -- modprobe: name=8021q state=present +- modprobe: + name: 8021q + state: present + # Add the dummy module -- modprobe: name=dummy state=present params="numdummies=2" +- modprobe: + name: dummy + state: present + params: 'numdummies=2' ''' from ansible.module_utils.basic import * diff --git a/system/open_iscsi.py b/system/open_iscsi.py index 74349ce8680..77586289e77 100644 --- a/system/open_iscsi.py +++ b/system/open_iscsi.py @@ -88,18 +88,28 @@ EXAMPLES = ''' # perform a discovery on 10.1.2.3 and show available target nodes -- open_iscsi: show_nodes=yes discover=yes portal=10.1.2.3 +- open_iscsi: + show_nodes: yes + discover: yes + portal: 10.1.2.3 # discover targets on portal and login to the one available # (only works if exactly one target is exported to the initiator) -- open_iscsi: portal={{iscsi_target}} login=yes discover=yes +- open_iscsi: + portal: '{{ iscsi_target }}' + login: yes + discover: yes # description: connect to the named target, after updating the local # persistent database (cache) -- open_iscsi: login=yes target=iqn.1986-03.com.sun:02:f8c1f9e0-c3ec-ec84-c9c9-8bfb0cd5de3d +- open_iscsi: + login: yes + target: 'iqn.1986-03.com.sun:02:f8c1f9e0-c3ec-ec84-c9c9-8bfb0cd5de3d' # description: discconnect from the cached named target -- open_iscsi: login=no target=iqn.1986-03.com.sun:02:f8c1f9e0-c3ec-ec84-c9c9-8bfb0cd5de3d" +- open_iscsi: + login: no + target: 'iqn.1986-03.com.sun:02:f8c1f9e0-c3ec-ec84-c9c9-8bfb0cd5de3d' ''' import glob diff --git a/system/openwrt_init.py b/system/openwrt_init.py index c54cd3295b3..297826076c1 100644 --- a/system/openwrt_init.py +++ b/system/openwrt_init.py @@ -59,11 +59,20 @@ EXAMPLES = ''' # Example action to start service httpd, if not running -- openwrt_init: state=started name=httpd +- openwrt_init: + state: started + name: httpd + # Example action to stop service cron, if running -- openwrt_init: name=cron state=stopped +- openwrt_init: + name: cron + state: stopped + # Example action to reload service httpd, in all cases -- openwrt_init: name=httpd state=reloaded +- openwrt_init: + name: httpd + state: reloaded + # Example action to enable service httpd - openwrt_init: name: httpd diff --git a/system/osx_defaults.py b/system/osx_defaults.py index 64bf79ab102..986a263ede6 100644 --- a/system/osx_defaults.py +++ b/system/osx_defaults.py @@ -72,15 +72,43 @@ ''' EXAMPLES = ''' -- osx_defaults: domain=com.apple.Safari key=IncludeInternalDebugMenu type=bool value=true state=present -- osx_defaults: domain=NSGlobalDomain key=AppleMeasurementUnits type=string value=Centimeters state=present -- osx_defaults: domain=com.apple.screensaver host=currentHost key=showClock type=int value=1 -- osx_defaults: key=AppleMeasurementUnits type=string value=Centimeters +- osx_defaults: + domain: com.apple.Safari + key: IncludeInternalDebugMenu + type: bool + value: true + state: present + +- osx_defaults: + domain: NSGlobalDomain + key: AppleMeasurementUnits + type: string + value: Centimeters + state: present + +- osx_defaults: + domain: com.apple.screensaver + host: currentHost + key: showClock + type: int + value: 1 + +- osx_defaults: + key: AppleMeasurementUnits + type: string + value: Centimeters + - osx_defaults: key: AppleLanguages type: array - value: ["en", "nl"] -- osx_defaults: domain=com.geekchimp.macable key=ExampleKeyToRemove state=absent + value: + - en + - nl + +- osx_defaults: + domain: com.geekchimp.macable + key: ExampleKeyToRemove + state: absent ''' import datetime diff --git a/system/pam_limits.py b/system/pam_limits.py index bd129c5817f..23fa35725e1 100644 --- a/system/pam_limits.py +++ b/system/pam_limits.py @@ -89,13 +89,27 @@ EXAMPLES = ''' # Add or modify nofile soft limit for the user joe -- pam_limits: domain=joe limit_type=soft limit_item=nofile value=64000 +- pam_limits: + domain: joe + limit_type: soft + limit_item: nofile + value: 64000 # Add or modify fsize hard limit for the user smith. Keep or set the maximal value. -- pam_limits: domain=smith limit_type=hard limit_item=fsize value=1000000 use_max=yes +- pam_limits: + domain: smith + limit_type: hard + limit_item: fsize + value: 1000000 + use_max: yes # Add or modify memlock, both soft and hard, limit for the user james with a comment. -- pam_limits: domain=james limit_type=- limit_item=memlock value=unlimited comment="unlimited memory lock for james" +- pam_limits: + domain: james + limit_type: - + limit_item: memlock + value: unlimited + comment: unlimited memory lock for james ''' def main(): diff --git a/system/puppet.py b/system/puppet.py index 97c1a3eb38c..411552d86b8 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -102,19 +102,25 @@ - puppet # Run puppet and timeout in 5 minutes -- puppet: timeout=5m +- puppet: + timeout: 5m # Run puppet using a different environment -- puppet: environment=testing +- puppet: + environment: testing # Run puppet using a specific certname -- puppet: certname=agent01.example.com +- puppet: + certname: agent01.example.com + # Run puppet using a specific piece of Puppet code. Has no effect with a # puppetmaster. -- puppet: execute='include ::mymodule' +- puppet: + execute: 'include ::mymodule' # Run puppet using a specific tags -- puppet: tags=update,nginx +- puppet: + tags: update,nginx ''' diff --git a/system/sefcontext.py b/system/sefcontext.py index 96f576c064f..120481cad3b 100644 --- a/system/sefcontext.py +++ b/system/sefcontext.py @@ -72,7 +72,10 @@ EXAMPLES = ''' # Allow apache to modify files in /srv/git_repos -- sefcontext: target='/srv/git_repos(/.*)?' setype=httpd_git_rw_content_t state=present +- sefcontext: + target: '/srv/git_repos(/.*)?' + setype: httpd_git_rw_content_t + state: present ''' RETURN = ''' @@ -255,4 +258,4 @@ def main(): if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/system/selinux_permissive.py b/system/selinux_permissive.py index ced9716cc01..6374f94bbdf 100644 --- a/system/selinux_permissive.py +++ b/system/selinux_permissive.py @@ -56,7 +56,9 @@ ''' EXAMPLES = ''' -- selinux_permissive: name=httpd_t permissive=true +- selinux_permissive: + name: httpd_t + permissive: true ''' HAVE_SEOBJECT = False diff --git a/system/seport.py b/system/seport.py index 242661a1430..c661db43084 100644 --- a/system/seport.py +++ b/system/seport.py @@ -61,11 +61,25 @@ EXAMPLES = ''' # Allow Apache to listen on tcp port 8888 -- seport: ports=8888 proto=tcp setype=http_port_t state=present +- seport: + ports: 8888 + proto: tcp + setype: http_port_t + state: present + # Allow sshd to listen on tcp port 8991 -- seport: ports=8991 proto=tcp setype=ssh_port_t state=present +- seport: + ports: 8991 + proto: tcp + setype: ssh_port_t + state: present + # Allow memcached to listen on tcp ports 10000-10100 and 10112 -- seport: ports=10000-10100,10112 proto=tcp setype=memcache_port_t state=present +- seport: + ports: 10000-10100,10112 + proto: tcp + setype: memcache_port_t + state: present ''' try: diff --git a/system/solaris_zone.py b/system/solaris_zone.py index c97bb1f2c7b..f57649130fd 100644 --- a/system/solaris_zone.py +++ b/system/solaris_zone.py @@ -104,31 +104,55 @@ EXAMPLES = ''' # Create and install a zone, but don't boot it -solaris_zone: name=zone1 state=present path=/zones/zone1 sparse=true root_password="Be9oX7OSwWoU." - config='set autoboot=true; add net; set physical=bge0; set address=10.1.1.1; end' +- solaris_zone: + name: zone1 + state: present + path: /zones/zone1 + sparse: true + root_password: Be9oX7OSwWoU. + config: 'set autoboot=true; add net; set physical=bge0; set address=10.1.1.1; end' # Create and install a zone and boot it -solaris_zone: name=zone1 state=running path=/zones/zone1 root_password="Be9oX7OSwWoU." - config='set autoboot=true; add net; set physical=bge0; set address=10.1.1.1; end' +- solaris_zone: + name: zone1 + state: running + path: /zones/zone1 + root_password: Be9oX7OSwWoU. + config: 'set autoboot=true; add net; set physical=bge0; set address=10.1.1.1; end' # Boot an already installed zone -solaris_zone: name=zone1 state=running +- solaris_zone: + name: zone1 + state: running # Stop a zone -solaris_zone: name=zone1 state=stopped +- solaris_zone: + name: zone1 + state: stopped # Destroy a zone -solaris_zone: name=zone1 state=absent +- solaris_zone: + name: zone1 + state: absent # Detach a zone -solaris_zone: name=zone1 state=detached +- solaris_zone: + name: zone1 + state: detached # Configure a zone, ready to be attached -solaris_zone: name=zone1 state=configured path=/zones/zone1 root_password="Be9oX7OSwWoU." - config='set autoboot=true; add net; set physical=bge0; set address=10.1.1.1; end' +- solaris_zone: + name: zone1 + state: configured + path: /zones/zone1 + root_password: Be9oX7OSwWoU. + config: 'set autoboot=true; add net; set physical=bge0; set address=10.1.1.1; end' # Attach a zone -solaris_zone: name=zone1 state=attached attach_options='-u' +- solaris_zone: + name: zone1 + state: attached + attach_options=: -u ''' class Zone(object): diff --git a/system/svc.py b/system/svc.py index e82b0591d59..376062b4be5 100755 --- a/system/svc.py +++ b/system/svc.py @@ -67,22 +67,35 @@ EXAMPLES = ''' # Example action to start svc dnscache, if not running - - svc: name=dnscache state=started + - svc: + name: dnscache + state: started # Example action to stop svc dnscache, if running - - svc: name=dnscache state=stopped + - svc: + name: dnscache + state: stopped # Example action to kill svc dnscache, in all cases - - svc : name=dnscache state=killed + - svc: + name: dnscache + state: killed # Example action to restart svc dnscache, in all cases - - svc : name=dnscache state=restarted + - svc: + name: dnscache + state: restarted # Example action to reload svc dnscache, in all cases - - svc: name=dnscache state=reloaded + - svc: + name: dnscache + state: reloaded # Example using alt svc directory location - - svc: name=dnscache state=reloaded service_dir=/var/service + - svc: + name: dnscache + state: reloaded + service_dir: /var/service ''' import platform diff --git a/system/timezone.py b/system/timezone.py index 3c920c4bff5..c750611c9be 100644 --- a/system/timezone.py +++ b/system/timezone.py @@ -71,7 +71,8 @@ EXAMPLES = ''' - name: set timezone to Asia/Tokyo - timezone: name=Asia/Tokyo + timezone: + name: Asia/Tokyo ''' diff --git a/system/ufw.py b/system/ufw.py index c692211d124..67eaba131c7 100644 --- a/system/ufw.py +++ b/system/ufw.py @@ -125,60 +125,103 @@ EXAMPLES = ''' # Allow everything and enable UFW -ufw: state=enabled policy=allow +- ufw: + state: enabled + policy: allow # Set logging -ufw: logging=on +- ufw: + logging: on # Sometimes it is desirable to let the sender know when traffic is # being denied, rather than simply ignoring it. In these cases, use # reject instead of deny. In addition, log rejected connections: -ufw: rule=reject port=auth log=yes +- ufw: + rule: reject + port: auth + log: yes # ufw supports connection rate limiting, which is useful for protecting # against brute-force login attacks. ufw will deny connections if an IP # address has attempted to initiate 6 or more connections in the last # 30 seconds. See http://www.debian-administration.org/articles/187 # for details. Typical usage is: -ufw: rule=limit port=ssh proto=tcp +- ufw: + rule: limit + port: ssh + proto: tcp # Allow OpenSSH. (Note that as ufw manages its own state, simply removing # a rule=allow task can leave those ports exposed. Either use delete=yes # or a separate state=reset task) -ufw: rule=allow name=OpenSSH +- ufw: + rule: allow + name: OpenSSH # Delete OpenSSH rule -ufw: rule=allow name=OpenSSH delete=yes +- ufw: + rule: allow + name: OpenSSH + delete: yes # Deny all access to port 53: -ufw: rule=deny port=53 +- ufw: + rule: deny + port: 53 # Allow port range 60000-61000 -ufw: rule=allow port=60000:61000 +- ufw: + rule: allow + port: '60000:61000' # Allow all access to tcp port 80: -ufw: rule=allow port=80 proto=tcp +- ufw: + rule: allow + port: 80 + proto: tcp # Allow all access from RFC1918 networks to this host: -ufw: rule=allow src={{ item }} -with_items: -- 10.0.0.0/8 -- 172.16.0.0/12 -- 192.168.0.0/16 +- ufw: + rule: allow + src: '{{ item }}' + with_items: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 # Deny access to udp port 514 from host 1.2.3.4: -ufw: rule=deny proto=udp src=1.2.3.4 port=514 +- ufw: + rule: deny + proto: udp + src: 1.2.3.4 + port: 514 # Allow incoming access to eth0 from 1.2.3.5 port 5469 to 1.2.3.4 port 5469 -ufw: rule=allow interface=eth0 direction=in proto=udp src=1.2.3.5 from_port=5469 dest=1.2.3.4 to_port=5469 +- ufw: + rule: allow + interface: eth0 + direction: in + proto: udp + src: 1.2.3.5 + from_port: 5469 + dest: 1.2.3.4 + to_port: 5469 # Deny all traffic from the IPv6 2001:db8::/32 to tcp port 25 on this host. # Note that IPv6 must be enabled in /etc/default/ufw for IPv6 firewalling to work. -ufw: rule=deny proto=tcp src=2001:db8::/32 port=25 +- ufw: + rule: deny + proto: tcp + src: '2001:db8::/32' + port: 25 # Deny forwarded/routed traffic from subnet 1.2.3.0/24 to subnet 4.5.6.0/24. # Can be used to further restrict a global FORWARD policy set to allow -ufw: rule=deny route=yes src=1.2.3.0/24 dest=4.5.6.0/24 +- ufw: + rule: deny + route: yes + src: 1.2.3.0/24 + dest: 4.5.6.0/24 ''' from operator import itemgetter diff --git a/system/zfs.py b/system/zfs.py index 1a1bad4a0f9..47ce13edce8 100644 --- a/system/zfs.py +++ b/system/zfs.py @@ -54,22 +54,38 @@ EXAMPLES = ''' # Create a new file system called myfs in pool rpool with the setuid property turned off -- zfs: name=rpool/myfs state=present setuid=off +- zfs: + name: rpool/myfs + state: present + setuid: off # Create a new volume called myvol in pool rpool. -- zfs: name=rpool/myvol state=present volsize=10M +- zfs: + name: rpool/myvol + state: present + volsize: 10M # Create a snapshot of rpool/myfs file system. -- zfs: name=rpool/myfs@mysnapshot state=present +- zfs: + name: rpool/myfs@mysnapshot + state: present # Create a new file system called myfs2 with snapdir enabled -- zfs: name=rpool/myfs2 state=present snapdir=enabled +- zfs: + name: rpool/myfs2 + state: present + snapdir: enabled # Create a new file system by cloning a snapshot -- zfs: name=rpool/cloned_fs state=present origin=rpool/myfs@mysnapshot +- zfs: + name: rpool/cloned_fs + state: present + origin: rpool/myfs@mysnapshot # Destroy a filesystem -- zfs: name=rpool/myfs state=absent +- zfs: + name: rpool/myfs + state: absent ''' From 12e806d76bfb84775af7b8293a6d2de5e9a872c1 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Fri, 2 Dec 2016 15:49:38 +0000 Subject: [PATCH 2464/2522] Native YAML - cloud/centurylink/clc_loadbalancer.py (#3632) --- cloud/centurylink/clc_loadbalancer.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/cloud/centurylink/clc_loadbalancer.py b/cloud/centurylink/clc_loadbalancer.py index abb421c755e..dcbbfd0bc88 100644 --- a/cloud/centurylink/clc_loadbalancer.py +++ b/cloud/centurylink/clc_loadbalancer.py @@ -109,7 +109,8 @@ location: WA1 port: 443 nodes: - - { 'ipAddress': '10.11.22.123', 'privatePort': 80 } + - ipAddress: 10.11.22.123 + privatePort: 80 state: present - name: Add node to an existing loadbalancer pool @@ -124,7 +125,8 @@ location: WA1 port: 443 nodes: - - { 'ipAddress': '10.11.22.234', 'privatePort': 80 } + - ipAddress: 10.11.22.234 + privatePort: 80 state: nodes_present - name: Remove node from an existing loadbalancer pool @@ -139,7 +141,8 @@ location: WA1 port: 443 nodes: - - { 'ipAddress': '10.11.22.234', 'privatePort': 80 } + - ipAddress: 10.11.22.234 + privatePort: 80 state: nodes_absent - name: Delete LoadbalancerPool @@ -154,7 +157,8 @@ location: WA1 port: 443 nodes: - - { 'ipAddress': '10.11.22.123', 'privatePort': 80 } + - ipAddress: 10.11.22.123 + privatePort: 80 state: port_absent - name: Delete Loadbalancer @@ -169,7 +173,8 @@ location: WA1 port: 443 nodes: - - { 'ipAddress': '10.11.22.123', 'privatePort': 80 } + - ipAddress: 10.11.22.123 + privatePort: 80 state: absent ''' From 519cc173451a0fb67799a20a04730a9d6f252b4e Mon Sep 17 00:00:00 2001 From: Jon Hawkesworth Date: Fri, 18 Nov 2016 08:18:13 +0000 Subject: [PATCH 2465/2522] Fix documentation error on read message from file example. --- windows/win_say.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_say.py b/windows/win_say.py index 8ab159912b7..9fadddeeda8 100644 --- a/windows/win_say.py +++ b/windows/win_say.py @@ -87,7 +87,7 @@ # text from file example - win_say: start_sound_path: 'C:\Windows\Media\Windows Balloon.wav' - msg_text: AppData\Local\Temp\morning_report.txt + msg_file: AppData\Local\Temp\morning_report.txt end_sound_path: 'C:\Windows\Media\chimes.wav' ''' RETURN = ''' From 60d466a68f02d947f65dceecc48fe84a9d804592 Mon Sep 17 00:00:00 2001 From: Fabian von Feilitzsch Date: Sat, 3 Dec 2016 08:47:52 -0500 Subject: [PATCH 2466/2522] Check values for vm_id and vm_names instead of keys (#3621) --- cloud/ovirt/ovirt_disks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/ovirt/ovirt_disks.py b/cloud/ovirt/ovirt_disks.py index 092fb051cf1..f354c7a6d25 100644 --- a/cloud/ovirt/ovirt_disks.py +++ b/cloud/ovirt/ovirt_disks.py @@ -280,7 +280,7 @@ def main(): ret = disks_module.remove() # If VM was passed attach/detach disks to/from the VM: - if 'vm_id' in module.params or 'vm_name' in module.params and state != 'absent': + if module.params.get('vm_id') is not None or module.params.get('vm_name') is not None and state != 'absent': vms_service = connection.system_service().vms_service() # If `vm_id` isn't specified, find VM by name: From ec4faa6f9b14a362f5f503a1f1b483f6b0503102 Mon Sep 17 00:00:00 2001 From: Matthew Krupcale Date: Sun, 4 Dec 2016 05:44:43 -0500 Subject: [PATCH 2467/2522] FreeIPA module polymorphic restructuring and small fixes. (#3485) * Moved JSON-RPC client IPAClient class to ansible.module_utils.ipa, which is extended by all ipa modules * ipa_user: incorporate displayname and userpassword attributes in module_user * ipa_user: capitalized "I" in comment * ipa_user: updated get_ssh_key_fingerprint to include possibility of the uploaded SSH key including user@hostname comment, which also appears in the queried fingerprint. This fixes a mismatch in the calculated and queried SSH key fingerprint in the user_diff calculation when the user already exists. * ipa_hbacrule: ipaenabledflag must be 'TRUE' or 'FALSE', not 'NO' * ipa_sudorule: ipaenabledflag must be 'TRUE' or 'FALSE', not 'NO' * Add author to files missing it --- identity/ipa/ipa_group.py | 88 ++--------------------- identity/ipa/ipa_hbacrule.py | 90 +++--------------------- identity/ipa/ipa_host.py | 89 +++--------------------- identity/ipa/ipa_hostgroup.py | 89 +++--------------------- identity/ipa/ipa_role.py | 89 +++--------------------- identity/ipa/ipa_sudocmd.py | 88 ++--------------------- identity/ipa/ipa_sudocmdgroup.py | 88 ++--------------------- identity/ipa/ipa_sudorule.py | 90 +++--------------------- identity/ipa/ipa_user.py | 115 ++++++++----------------------- 9 files changed, 88 insertions(+), 738 deletions(-) diff --git a/identity/ipa/ipa_group.py b/identity/ipa/ipa_group.py index 0c45776a23f..246b769dd0d 100644 --- a/identity/ipa/ipa_group.py +++ b/identity/ipa/ipa_group.py @@ -87,8 +87,6 @@ required: false default: true version_added: "2.3" -requirements: -- json ''' EXAMPLES = ''' @@ -137,83 +135,12 @@ type: dict ''' -try: - import json -except ImportError: - import simplejson as json +from ansible.module_utils.ipa import IPAClient +class GroupIPAClient(IPAClient): -class IPAClient: def __init__(self, module, host, port, protocol): - self.host = host - self.port = port - self.protocol = protocol - self.module = module - self.headers = None - - def get_base_url(self): - return '%s://%s/ipa' % (self.protocol, self.host) - - def get_json_url(self): - return '%s/session/json' % self.get_base_url() - - def login(self, username, password): - url = '%s/session/login_password' % self.get_base_url() - data = 'user=%s&password=%s' % (username, password) - headers = {'referer': self.get_base_url(), - 'Content-Type': 'application/x-www-form-urlencoded', - 'Accept': 'text/plain'} - try: - resp, info = fetch_url(module=self.module, url=url, data=data, headers=headers) - status_code = info['status'] - if status_code not in [200, 201, 204]: - self._fail('login', info['body']) - - self.headers = {'referer': self.get_base_url(), - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Cookie': resp.info().getheader('Set-Cookie')} - except Exception: - e = get_exception() - self._fail('login', str(e)) - - def _fail(self, msg, e): - if 'message' in e: - err_string = e.get('message') - else: - err_string = e - self.module.fail_json(msg='%s: %s' % (msg, err_string)) - - def _post_json(self, method, name, item=None): - if item is None: - item = {} - url = '%s/session/json' % self.get_base_url() - data = {'method': method, 'params': [[name], item]} - try: - resp, info = fetch_url(module=self.module, url=url, data=json.dumps(data), headers=self.headers) - status_code = info['status'] - if status_code not in [200, 201, 204]: - self._fail(method, info['body']) - except Exception: - e = get_exception() - self._fail('post %s' % method, str(e)) - - resp = json.loads(resp.read()) - err = resp.get('error') - if err is not None: - self._fail('repsonse %s' % method, err) - - if 'result' in resp: - result = resp.get('result') - if 'result' in result: - result = result.get('result') - if isinstance(result, list): - if len(result) > 0: - return result[0] - else: - return {} - return result - return None + super(GroupIPAClient, self).__init__(module, host, port, protocol) def group_find(self, name): return self._post_json(method='group_find', name=None, item={'all': True, 'cn': name}) @@ -364,10 +291,10 @@ def main(): supports_check_mode=True, ) - client = IPAClient(module=module, - host=module.params['ipa_host'], - port=module.params['ipa_port'], - protocol=module.params['ipa_prot']) + client = GroupIPAClient(module=module, + host=module.params['ipa_host'], + port=module.params['ipa_port'], + protocol=module.params['ipa_prot']) try: client.login(username=module.params['ipa_user'], password=module.params['ipa_pass']) @@ -380,7 +307,6 @@ def main(): from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.pycompat24 import get_exception -from ansible.module_utils.urls import fetch_url if __name__ == '__main__': main() diff --git a/identity/ipa/ipa_hbacrule.py b/identity/ipa/ipa_hbacrule.py index 54619c5ec33..29661fe77bf 100644 --- a/identity/ipa/ipa_hbacrule.py +++ b/identity/ipa/ipa_hbacrule.py @@ -122,8 +122,6 @@ required: false default: true version_added: "2.3" -requirements: -- json ''' EXAMPLES = ''' @@ -168,83 +166,12 @@ type: dict ''' -try: - import json -except ImportError: - import simplejson as json +from ansible.module_utils.ipa import IPAClient +class HBACRuleIPAClient(IPAClient): -class IPAClient: def __init__(self, module, host, port, protocol): - self.host = host - self.port = port - self.protocol = protocol - self.module = module - self.headers = None - - def get_base_url(self): - return '%s://%s/ipa' % (self.protocol, self.host) - - def get_json_url(self): - return '%s/session/json' % self.get_base_url() - - def login(self, username, password): - url = '%s/session/login_password' % self.get_base_url() - data = 'user=%s&password=%s' % (username, password) - headers = {'referer': self.get_base_url(), - 'Content-Type': 'application/x-www-form-urlencoded', - 'Accept': 'text/plain'} - try: - resp, info = fetch_url(module=self.module, url=url, data=data, headers=headers) - status_code = info['status'] - if status_code not in [200, 201, 204]: - self._fail('login', info['body']) - - self.headers = {'referer': self.get_base_url(), - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Cookie': resp.info().getheader('Set-Cookie')} - except Exception: - e = get_exception() - self._fail('login', str(e)) - - def _fail(self, msg, e): - if 'message' in e: - err_string = e.get('message') - else: - err_string = e - self.module.fail_json(msg='%s: %s' % (msg, err_string)) - - def _post_json(self, method, name, item=None): - if item is None: - item = {} - url = '%s/session/json' % self.get_base_url() - data = {'method': method, 'params': [[name], item]} - try: - resp, info = fetch_url(module=self.module, url=url, data=json.dumps(data), headers=self.headers) - status_code = info['status'] - if status_code not in [200, 201, 204]: - self._fail(method, info['body']) - except Exception: - e = get_exception() - self._fail('post %s' % method, str(e)) - - resp = json.loads(resp.read()) - err = resp.get('error') - if err is not None: - self._fail('repsonse %s' % method, err) - - if 'result' in resp: - result = resp.get('result') - if 'result' in result: - result = result.get('result') - if isinstance(result, list): - if len(result) > 0: - return result[0] - else: - return {} - return result - return None + super(HBACRuleIPAClient, self).__init__(module, host, port, protocol) def hbacrule_find(self, name): return self._post_json(method='hbacrule_find', name=None, item={'all': True, 'cn': name}) @@ -341,7 +268,7 @@ def ensure(module, client): if state in ['present', 'enabled']: ipaenabledflag = 'TRUE' else: - ipaenabledflag = 'NO' + ipaenabledflag = 'FALSE' host = module.params['host'] hostcategory = module.params['hostcategory'] @@ -458,10 +385,10 @@ def main(): supports_check_mode=True, ) - client = IPAClient(module=module, - host=module.params['ipa_host'], - port=module.params['ipa_port'], - protocol=module.params['ipa_prot']) + client = HBACRuleIPAClient(module=module, + host=module.params['ipa_host'], + port=module.params['ipa_port'], + protocol=module.params['ipa_prot']) try: client.login(username=module.params['ipa_user'], @@ -475,7 +402,6 @@ def main(): from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.pycompat24 import get_exception -from ansible.module_utils.urls import fetch_url if __name__ == '__main__': main() diff --git a/identity/ipa/ipa_host.py b/identity/ipa/ipa_host.py index a7cc4d361e2..59c6772673d 100644 --- a/identity/ipa/ipa_host.py +++ b/identity/ipa/ipa_host.py @@ -18,6 +18,7 @@ DOCUMENTATION = ''' --- module: ipa_host +author: Thomas Krahn (@Nosmoht) short_description: Manage FreeIPA host description: - Add, modify and delete an IPA host using IPA API @@ -104,8 +105,6 @@ required: false default: true version_added: "2.3" -requirements: -- json ''' EXAMPLES = ''' @@ -161,83 +160,12 @@ type: list ''' -try: - import json -except ImportError: - import simplejson as json +from ansible.module_utils.ipa import IPAClient +class HostIPAClient(IPAClient): -class IPAClient: def __init__(self, module, host, port, protocol): - self.host = host - self.port = port - self.protocol = protocol - self.module = module - self.headers = None - - def get_base_url(self): - return '%s://%s/ipa' % (self.protocol, self.host) - - def get_json_url(self): - return '%s/session/json' % self.get_base_url() - - def login(self, username, password): - url = '%s/session/login_password' % self.get_base_url() - data = 'user=%s&password=%s' % (username, password) - headers = {'referer': self.get_base_url(), - 'Content-Type': 'application/x-www-form-urlencoded', - 'Accept': 'text/plain'} - try: - resp, info = fetch_url(module=self.module, url=url, data=data, headers=headers) - status_code = info['status'] - if status_code not in [200, 201, 204]: - self._fail('login', info['body']) - - self.headers = {'referer': self.get_base_url(), - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Cookie': resp.info().getheader('Set-Cookie')} - except Exception: - e = get_exception() - self._fail('login', str(e)) - - def _fail(self, msg, e): - if 'message' in e: - err_string = e.get('message') - else: - err_string = e - self.module.fail_json(msg='%s: %s' % (msg, err_string)) - - def _post_json(self, method, name, item=None): - if item is None: - item = {} - url = '%s/session/json' % self.get_base_url() - data = {'method': method, 'params': [[name], item]} - try: - resp, info = fetch_url(module=self.module, url=url, data=json.dumps(data), headers=self.headers) - status_code = info['status'] - if status_code not in [200, 201, 204]: - self._fail(method, info['body']) - except Exception: - e = get_exception() - self._fail('post %s' % method, str(e)) - - resp = json.loads(resp.read()) - err = resp.get('error') - if err is not None: - self._fail('repsonse %s' % method, err) - - if 'result' in resp: - result = resp.get('result') - if 'result' in result: - result = result.get('result') - if isinstance(result, list): - if len(result) > 0: - return result[0] - else: - return {} - return result - return None + super(HostIPAClient, self).__init__(module, host, port, protocol) def host_find(self, name): return self._post_json(method='host_find', name=None, item={'all': True, 'fqdn': name}) @@ -357,10 +285,10 @@ def main(): supports_check_mode=True, ) - client = IPAClient(module=module, - host=module.params['ipa_host'], - port=module.params['ipa_port'], - protocol=module.params['ipa_prot']) + client = HostIPAClient(module=module, + host=module.params['ipa_host'], + port=module.params['ipa_port'], + protocol=module.params['ipa_prot']) try: client.login(username=module.params['ipa_user'], @@ -374,7 +302,6 @@ def main(): from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.pycompat24 import get_exception -from ansible.module_utils.urls import fetch_url if __name__ == '__main__': main() diff --git a/identity/ipa/ipa_hostgroup.py b/identity/ipa/ipa_hostgroup.py index f0350277313..ba1ee33b8ac 100644 --- a/identity/ipa/ipa_hostgroup.py +++ b/identity/ipa/ipa_hostgroup.py @@ -18,6 +18,7 @@ DOCUMENTATION = ''' --- module: ipa_hostgroup +author: Thomas Krahn (@Nosmoht) short_description: Manage FreeIPA host-group description: - Add, modify and delete an IPA host-group using IPA API @@ -80,8 +81,6 @@ required: false default: true version_added: "2.3" -requirements: -- json ''' EXAMPLES = ''' @@ -114,83 +113,12 @@ type: dict ''' -try: - import json -except ImportError: - import simplejson as json +from ansible.module_utils.ipa import IPAClient +class HostGroupIPAClient(IPAClient): -class IPAClient: def __init__(self, module, host, port, protocol): - self.host = host - self.port = port - self.protocol = protocol - self.module = module - self.headers = None - - def get_base_url(self): - return '%s://%s/ipa' % (self.protocol, self.host) - - def get_json_url(self): - return '%s/session/json' % self.get_base_url() - - def login(self, username, password): - url = '%s/session/login_password' % self.get_base_url() - data = 'user=%s&password=%s' % (username, password) - headers = {'referer': self.get_base_url(), - 'Content-Type': 'application/x-www-form-urlencoded', - 'Accept': 'text/plain'} - try: - resp, info = fetch_url(module=self.module, url=url, data=data, headers=headers) - status_code = info['status'] - if status_code not in [200, 201, 204]: - self._fail('login', info['body']) - - self.headers = {'referer': self.get_base_url(), - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Cookie': resp.info().getheader('Set-Cookie')} - except Exception: - e = get_exception() - self._fail('login', str(e)) - - def _fail(self, msg, e): - if 'message' in e: - err_string = e.get('message') - else: - err_string = e - self.module.fail_json(msg='%s: %s' % (msg, err_string)) - - def _post_json(self, method, name, item=None): - if item is None: - item = {} - url = '%s/session/json' % self.get_base_url() - data = {'method': method, 'params': [[name], item]} - try: - resp, info = fetch_url(module=self.module, url=url, data=json.dumps(data), headers=self.headers) - status_code = info['status'] - if status_code not in [200, 201, 204]: - self._fail(method, info['body']) - except Exception: - e = get_exception() - self._fail('post %s' % method, str(e)) - - resp = json.loads(resp.read()) - err = resp.get('error') - if err is not None: - self._fail('repsonse %s' % method, err) - - if 'result' in resp: - result = resp.get('result') - if 'result' in result: - result = result.get('result') - if isinstance(result, list): - if len(result) > 0: - return result[0] - else: - return {} - return result - return None + super(HostGroupIPAClient, self).__init__(module, host, port, protocol) def hostgroup_find(self, name): return self._post_json(method='hostgroup_find', name=None, item={'all': True, 'cn': name}) @@ -324,10 +252,10 @@ def main(): supports_check_mode=True, ) - client = IPAClient(module=module, - host=module.params['ipa_host'], - port=module.params['ipa_port'], - protocol=module.params['ipa_prot']) + client = HostGroupIPAClient(module=module, + host=module.params['ipa_host'], + port=module.params['ipa_port'], + protocol=module.params['ipa_prot']) try: client.login(username=module.params['ipa_user'], @@ -341,7 +269,6 @@ def main(): from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.pycompat24 import get_exception -from ansible.module_utils.urls import fetch_url if __name__ == '__main__': main() diff --git a/identity/ipa/ipa_role.py b/identity/ipa/ipa_role.py index 9d3ac043f3a..9740a4d4a4d 100644 --- a/identity/ipa/ipa_role.py +++ b/identity/ipa/ipa_role.py @@ -18,6 +18,7 @@ DOCUMENTATION = ''' --- module: ipa_role +author: Thomas Krahn (@Nosmoht) short_description: Manage FreeIPA role description: - Add, modify and delete a role within FreeIPA server using FreeIPA API @@ -98,8 +99,6 @@ required: false default: true version_added: "2.3" -requirements: -- json ''' EXAMPLES = ''' @@ -144,83 +143,12 @@ type: dict ''' -try: - import json -except ImportError: - import simplejson as json +from ansible.module_utils.ipa import IPAClient +class RoleIPAClient(IPAClient): -class IPAClient: def __init__(self, module, host, port, protocol): - self.host = host - self.port = port - self.protocol = protocol - self.module = module - self.headers = None - - def get_base_url(self): - return '%s://%s/ipa' % (self.protocol, self.host) - - def get_json_url(self): - return '%s/session/json' % self.get_base_url() - - def login(self, username, password): - url = '%s/session/login_password' % self.get_base_url() - data = 'user=%s&password=%s' % (username, password) - headers = {'referer': self.get_base_url(), - 'Content-Type': 'application/x-www-form-urlencoded', - 'Accept': 'text/plain'} - try: - resp, info = fetch_url(module=self.module, url=url, data=data, headers=headers) - status_code = info['status'] - if status_code not in [200, 201, 204]: - self._fail('login', info['body']) - - self.headers = {'referer': self.get_base_url(), - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Cookie': resp.info().getheader('Set-Cookie')} - except Exception: - e = get_exception() - self._fail('login', str(e)) - - def _fail(self, msg, e): - if 'message' in e: - err_string = e.get('message') - else: - err_string = e - self.module.fail_json(msg='%s: %s' % (msg, err_string)) - - def _post_json(self, method, name, item=None): - if item is None: - item = {} - url = '%s/session/json' % self.get_base_url() - data = {'method': method, 'params': [[name], item]} - try: - resp, info = fetch_url(module=self.module, url=url, data=json.dumps(data), headers=self.headers) - status_code = info['status'] - if status_code not in [200, 201, 204]: - self._fail(method, info['body']) - except Exception: - e = get_exception() - self._fail('post %s' % method, str(e)) - - resp = json.loads(resp.read()) - err = resp.get('error') - if err is not None: - self._fail('repsonse %s' % method, err) - - if 'result' in resp: - result = resp.get('result') - if 'result' in result: - result = result.get('result') - if isinstance(result, list): - if len(result) > 0: - return result[0] - else: - return {} - return result - return None + super(RoleIPAClient, self).__init__(module, host, port, protocol) def role_find(self, name): return self._post_json(method='role_find', name=None, item={'all': True, 'cn': name}) @@ -390,10 +318,10 @@ def main(): supports_check_mode=True, ) - client = IPAClient(module=module, - host=module.params['ipa_host'], - port=module.params['ipa_port'], - protocol=module.params['ipa_prot']) + client = RoleIPAClient(module=module, + host=module.params['ipa_host'], + port=module.params['ipa_port'], + protocol=module.params['ipa_prot']) try: client.login(username=module.params['ipa_user'], @@ -407,7 +335,6 @@ def main(): from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.pycompat24 import get_exception -from ansible.module_utils.urls import fetch_url if __name__ == '__main__': main() diff --git a/identity/ipa/ipa_sudocmd.py b/identity/ipa/ipa_sudocmd.py index f759f2d726f..0a86bf68821 100644 --- a/identity/ipa/ipa_sudocmd.py +++ b/identity/ipa/ipa_sudocmd.py @@ -65,8 +65,6 @@ required: false default: true version_added: "2.3" -requirements: -- json ''' EXAMPLES = ''' @@ -94,83 +92,12 @@ type: dict ''' -try: - import json -except ImportError: - import simplejson as json +from ansible.module_utils.ipa import IPAClient +class SudoCmdIPAClient(IPAClient): -class IPAClient: def __init__(self, module, host, port, protocol): - self.host = host - self.port = port - self.protocol = protocol - self.module = module - self.headers = None - - def get_base_url(self): - return '%s://%s/ipa' % (self.protocol, self.host) - - def get_json_url(self): - return '%s/session/json' % self.get_base_url() - - def login(self, username, password): - url = '%s/session/login_password' % self.get_base_url() - data = 'user=%s&password=%s' % (username, password) - headers = {'referer': self.get_base_url(), - 'Content-Type': 'application/x-www-form-urlencoded', - 'Accept': 'text/plain'} - try: - resp, info = fetch_url(module=self.module, url=url, data=data, headers=headers) - status_code = info['status'] - if status_code not in [200, 201, 204]: - self._fail('login', info['body']) - - self.headers = {'referer': self.get_base_url(), - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Cookie': resp.info().getheader('Set-Cookie')} - except Exception: - e = get_exception() - self._fail('login', str(e)) - - def _fail(self, msg, e): - if 'message' in e: - err_string = e.get('message') - else: - err_string = e - self.module.fail_json(msg='%s: %s' % (msg, err_string)) - - def _post_json(self, method, name, item=None): - if item is None: - item = {} - url = '%s/session/json' % self.get_base_url() - data = {'method': method, 'params': [[name], item]} - try: - resp, info = fetch_url(module=self.module, url=url, data=json.dumps(data), headers=self.headers) - status_code = info['status'] - if status_code not in [200, 201, 204]: - self._fail(method, info['body']) - except Exception: - e = get_exception() - self._fail('post %s' % method, str(e)) - - resp = json.loads(resp.read()) - err = resp.get('error') - if err is not None: - self._fail('repsonse %s' % method, err) - - if 'result' in resp: - result = resp.get('result') - if 'result' in result: - result = result.get('result') - if isinstance(result, list): - if len(result) > 0: - return result[0] - else: - return {} - return result - return None + super(SudoCmdIPAClient, self).__init__(module, host, port, protocol) def sudocmd_find(self, name): return self._post_json(method='sudocmd_find', name=None, item={'all': True, 'sudocmd': name}) @@ -255,10 +182,10 @@ def main(): supports_check_mode=True, ) - client = IPAClient(module=module, - host=module.params['ipa_host'], - port=module.params['ipa_port'], - protocol=module.params['ipa_prot']) + client = SudoCmdIPAClient(module=module, + host=module.params['ipa_host'], + port=module.params['ipa_port'], + protocol=module.params['ipa_prot']) try: client.login(username=module.params['ipa_user'], password=module.params['ipa_pass']) @@ -271,7 +198,6 @@ def main(): from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.pycompat24 import get_exception -from ansible.module_utils.urls import fetch_url if __name__ == '__main__': main() diff --git a/identity/ipa/ipa_sudocmdgroup.py b/identity/ipa/ipa_sudocmdgroup.py index b29216f70cd..b6730bccefe 100644 --- a/identity/ipa/ipa_sudocmdgroup.py +++ b/identity/ipa/ipa_sudocmdgroup.py @@ -70,8 +70,6 @@ required: false default: true version_added: "2.3" -requirements: -- json ''' EXAMPLES = ''' @@ -101,83 +99,12 @@ type: dict ''' -try: - import json -except ImportError: - import simplejson as json +from ansible.module_utils.ipa import IPAClient +class SudoCmdGroupIPAClient(IPAClient): -class IPAClient: def __init__(self, module, host, port, protocol): - self.host = host - self.port = port - self.protocol = protocol - self.module = module - self.headers = None - - def get_base_url(self): - return '%s://%s/ipa' % (self.protocol, self.host) - - def get_json_url(self): - return '%s/session/json' % self.get_base_url() - - def login(self, username, password): - url = '%s/session/login_password' % self.get_base_url() - data = 'user=%s&password=%s' % (username, password) - headers = {'referer': self.get_base_url(), - 'Content-Type': 'application/x-www-form-urlencoded', - 'Accept': 'text/plain'} - try: - resp, info = fetch_url(module=self.module, url=url, data=data, headers=headers) - status_code = info['status'] - if status_code not in [200, 201, 204]: - self._fail('login', info['body']) - - self.headers = {'referer': self.get_base_url(), - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Cookie': resp.info().getheader('Set-Cookie')} - except Exception: - e = get_exception() - self._fail('login', str(e)) - - def _fail(self, msg, e): - if 'message' in e: - err_string = e.get('message') - else: - err_string = e - self.module.fail_json(msg='%s: %s' % (msg, err_string)) - - def _post_json(self, method, name, item=None): - if item is None: - item = {} - url = '%s/session/json' % self.get_base_url() - data = {'method': method, 'params': [[name], item]} - try: - resp, info = fetch_url(module=self.module, url=url, data=json.dumps(data), headers=self.headers) - status_code = info['status'] - if status_code not in [200, 201, 204]: - self._fail(method, info['body']) - except Exception: - e = get_exception() - self._fail('post %s' % method, str(e)) - - resp = json.loads(resp.read()) - err = resp.get('error') - if err is not None: - self._fail('repsonse %s' % method, err) - - if 'result' in resp: - result = resp.get('result') - if 'result' in result: - result = result.get('result') - if isinstance(result, list): - if len(result) > 0: - return result[0] - else: - return {} - return result - return None + super(SudoCmdGroupIPAClient, self).__init__(module, host, port, protocol) def sudocmdgroup_find(self, name): return self._post_json(method='sudocmdgroup_find', name=None, item={'all': True, 'cn': name}) @@ -297,10 +224,10 @@ def main(): supports_check_mode=True, ) - client = IPAClient(module=module, - host=module.params['ipa_host'], - port=module.params['ipa_port'], - protocol=module.params['ipa_prot']) + client = SudoCmdGroupIPAClient(module=module, + host=module.params['ipa_host'], + port=module.params['ipa_port'], + protocol=module.params['ipa_prot']) try: client.login(username=module.params['ipa_user'], password=module.params['ipa_pass']) @@ -313,7 +240,6 @@ def main(): from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.pycompat24 import get_exception -from ansible.module_utils.urls import fetch_url if __name__ == '__main__': main() diff --git a/identity/ipa/ipa_sudorule.py b/identity/ipa/ipa_sudorule.py index 426a4444ecc..b8a4c062777 100644 --- a/identity/ipa/ipa_sudorule.py +++ b/identity/ipa/ipa_sudorule.py @@ -111,8 +111,6 @@ required: false default: true version_added: "2.3" -requirements: -- json ''' EXAMPLES = ''' @@ -153,83 +151,12 @@ type: dict ''' -try: - import json -except ImportError: - import simplejson as json +from ansible.module_utils.ipa import IPAClient +class SudoRuleIPAClient(IPAClient): -class IPAClient: def __init__(self, module, host, port, protocol): - self.host = host - self.port = port - self.protocol = protocol - self.module = module - self.headers = None - - def get_base_url(self): - return '%s://%s/ipa' % (self.protocol, self.host) - - def get_json_url(self): - return '%s/session/json' % self.get_base_url() - - def login(self, username, password): - url = '%s/session/login_password' % self.get_base_url() - data = 'user=%s&password=%s' % (username, password) - headers = {'referer': self.get_base_url(), - 'Content-Type': 'application/x-www-form-urlencoded', - 'Accept': 'text/plain'} - try: - resp, info = fetch_url(module=self.module, url=url, data=data, headers=headers) - status_code = info['status'] - if status_code not in [200, 201, 204]: - self._fail('login', info['body']) - - self.headers = {'referer': self.get_base_url(), - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Cookie': resp.info().getheader('Set-Cookie')} - except Exception: - e = get_exception() - self._fail('login', str(e)) - - def _fail(self, msg, e): - if 'message' in e: - err_string = e.get('message') - else: - err_string = e - self.module.fail_json(msg='%s: %s' % (msg, err_string)) - - def _post_json(self, method, name, item=None): - if item is None: - item = {} - url = '%s/session/json' % self.get_base_url() - data = {'method': method, 'params': [[name], item]} - try: - resp, info = fetch_url(module=self.module, url=url, data=json.dumps(data), headers=self.headers) - status_code = info['status'] - if status_code not in [200, 201, 204]: - self._fail(method, info['body']) - except Exception: - e = get_exception() - self._fail('post %s' % method, str(e)) - - resp = json.loads(resp.read()) - err = resp.get('error') - if err is not None: - self._fail('repsonse %s' % method, err) - - if 'result' in resp: - result = resp.get('result') - if 'result' in result: - result = result.get('result') - if isinstance(result, list): - if len(result) > 0: - return result[0] - else: - return {} - return result - return None + super(SudoRuleIPAClient, self).__init__(module, host, port, protocol) def sudorule_find(self, name): return self._post_json(method='sudorule_find', name=None, item={'all': True, 'cn': name}) @@ -368,7 +295,7 @@ def ensure(module, client): if state in ['present', 'enabled']: ipaenabledflag = 'TRUE' else: - ipaenabledflag = 'NO' + ipaenabledflag = 'FALSE' sudoopt = module.params['sudoopt'] user = module.params['user'] @@ -472,10 +399,10 @@ def main(): supports_check_mode=True, ) - client = IPAClient(module=module, - host=module.params['ipa_host'], - port=module.params['ipa_port'], - protocol=module.params['ipa_prot']) + client = SudoRuleIPAClient(module=module, + host=module.params['ipa_host'], + port=module.params['ipa_port'], + protocol=module.params['ipa_prot']) try: client.login(username=module.params['ipa_user'], password=module.params['ipa_pass']) @@ -488,7 +415,6 @@ def main(): from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.pycompat24 import get_exception -from ansible.module_utils.urls import fetch_url if __name__ == '__main__': main() diff --git a/identity/ipa/ipa_user.py b/identity/ipa/ipa_user.py index 464348ee9bc..9ee677b6653 100644 --- a/identity/ipa/ipa_user.py +++ b/identity/ipa/ipa_user.py @@ -18,6 +18,7 @@ DOCUMENTATION = ''' --- module: ipa_user +author: Thomas Krahn (@Nosmoht) short_description: Manage FreeIPA users description: - Add, modify and delete user within IPA server @@ -99,7 +100,6 @@ requirements: - base64 - hashlib -- json ''' EXAMPLES = ''' @@ -139,83 +139,12 @@ import base64 import hashlib -try: - import json -except ImportError: - import simplejson as json +from ansible.module_utils.ipa import IPAClient +class UserIPAClient(IPAClient): -class IPAClient: def __init__(self, module, host, port, protocol): - self.host = host - self.port = port - self.protocol = protocol - self.module = module - self.headers = None - - def get_base_url(self): - return '%s://%s/ipa' % (self.protocol, self.host) - - def get_json_url(self): - return '%s/session/json' % self.get_base_url() - - def login(self, username, password): - url = '%s/session/login_password' % self.get_base_url() - data = 'user=%s&password=%s' % (username, password) - headers = {'referer': self.get_base_url(), - 'Content-Type': 'application/x-www-form-urlencoded', - 'Accept': 'text/plain'} - try: - resp, info = fetch_url(module=self.module, url=url, data=data, headers=headers) - status_code = info['status'] - if status_code not in [200, 201, 204]: - self._fail('login', info['body']) - - self.headers = {'referer': self.get_base_url(), - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Cookie': resp.info().getheader('Set-Cookie')} - except Exception: - e = get_exception() - self._fail('login', str(e)) - - def _fail(self, msg, e): - if 'message' in e: - err_string = e.get('message') - else: - err_string = e - self.module.fail_json(msg='%s: %s' % (msg, err_string)) - - def _post_json(self, method, name, item=None): - if item is None: - item = {} - url = '%s/session/json' % self.get_base_url() - data = {'method': method, 'params': [[name], item]} - try: - resp, info = fetch_url(module=self.module, url=url, data=json.dumps(data), headers=self.headers) - status_code = info['status'] - if status_code not in [200, 201, 204]: - self._fail(method, info['body']) - except Exception: - e = get_exception() - self._fail('post %s' % method, str(e)) - - resp = json.loads(resp.read()) - err = resp.get('error') - if err is not None: - self._fail('repsonse %s' % method, err) - - if 'result' in resp: - result = resp.get('result') - if 'result' in result: - result = result.get('result') - if isinstance(result, list): - if len(result) > 0: - return result[0] - else: - return {} - return result - return None + super(UserIPAClient, self).__init__(module, host, port, protocol) def user_find(self, name): return self._post_json(method='user_find', name=None, item={'all': True, 'uid': name}) @@ -236,10 +165,11 @@ def user_enable(self, name): return self._post_json(method='user_enable', name=name) -def get_user_dict(givenname=None, loginshell=None, mail=None, nsaccountlock=False, sn=None, sshpubkey=None, - telephonenumber=None, - title=None): +def get_user_dict(displayname=None, givenname=None, loginshell=None, mail=None, nsaccountlock=False, sn=None, + sshpubkey=None, telephonenumber=None, title=None, userpassword=None): user = {} + if displayname is not None: + user['displayname'] = displayname if givenname is not None: user['givenname'] = givenname if loginshell is not None: @@ -255,6 +185,8 @@ def get_user_dict(givenname=None, loginshell=None, mail=None, nsaccountlock=Fals user['telephonenumber'] = telephonenumber if title is not None: user['title'] = title + if userpassword is not None: + user['userpassword'] = userpassword return user @@ -265,7 +197,7 @@ def get_user_diff(ipa_user, module_user): API returns everything as a list even if only a single value is possible. Therefore some more complexity is needed. The method will check if the value type of module_user.attr is not a list and - create a list with that element if the same attribute in ipa_user is list. In this way i hope that the method + create a list with that element if the same attribute in ipa_user is list. In this way I hope that the method must not be changed if the returned API dict is changed. :param ipa_user: :param module_user: @@ -301,7 +233,7 @@ def get_user_diff(ipa_user, module_user): def get_ssh_key_fingerprint(ssh_key): """ Return the public key fingerprint of a given public SSH key - in format "FB:0C:AC:0A:07:94:5B:CE:75:6E:63:32:13:AD:AD:D7 (ssh-rsa)" + in format "FB:0C:AC:0A:07:94:5B:CE:75:6E:63:32:13:AD:AD:D7 [user@host] (ssh-rsa)" :param ssh_key: :return: """ @@ -312,7 +244,12 @@ def get_ssh_key_fingerprint(ssh_key): key = base64.b64decode(parts[1].encode('ascii')) fp_plain = hashlib.md5(key).hexdigest() - return ':'.join(a + b for a, b in zip(fp_plain[::2], fp_plain[1::2])).upper() + ' (%s)' % key_type + key_fp = ':'.join(a + b for a, b in zip(fp_plain[::2], fp_plain[1::2])).upper() + if len(parts) < 3: + return "%s (%s)" % (key_fp, key_type) + else: + user_host = parts[2] + return "%s %s (%s)" % (key_fp, user_host, key_type) def ensure(module, client): @@ -320,10 +257,13 @@ def ensure(module, client): name = module.params['name'] nsaccountlock = state == 'disabled' - module_user = get_user_dict(givenname=module.params.get('givenname'), loginshell=module.params['loginshell'], + module_user = get_user_dict(displayname=module.params.get('displayname'), + givenname=module.params.get('givenname'), + loginshell=module.params['loginshell'], mail=module.params['mail'], sn=module.params['sn'], sshpubkey=module.params['sshpubkey'], nsaccountlock=nsaccountlock, - telephonenumber=module.params['telephonenumber'], title=module.params['title']) + telephonenumber=module.params['telephonenumber'], title=module.params['title'], + userpassword=module.params['password']) ipa_user = client.user_find(name=name) @@ -373,10 +313,10 @@ def main(): supports_check_mode=True, ) - client = IPAClient(module=module, - host=module.params['ipa_host'], - port=module.params['ipa_port'], - protocol=module.params['ipa_prot']) + client = UserIPAClient(module=module, + host=module.params['ipa_host'], + port=module.params['ipa_port'], + protocol=module.params['ipa_prot']) # If sshpubkey is defined as None than module.params['sshpubkey'] is [None]. IPA itself returns None (not a list). # Therefore a small check here to replace list(None) by None. Otherwise get_user_diff() would return sshpubkey @@ -397,7 +337,6 @@ def main(): from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.pycompat24 import get_exception -from ansible.module_utils.urls import fetch_url if __name__ == '__main__': main() From f42c34088a7af929a1b884a684dd075bf4a6753e Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Sun, 4 Dec 2016 10:53:19 +0000 Subject: [PATCH 2468/2522] Correct indentation in examples - files/blockinfile.py (#3630) * indentation in with_items section is out, correct that. --- files/blockinfile.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/files/blockinfile.py b/files/blockinfile.py index ecee4800117..72bd33e1d6c 100755 --- a/files/blockinfile.py +++ b/files/blockinfile.py @@ -141,9 +141,9 @@ {{item.ip}} {{item.name}} marker: "# {mark} ANSIBLE MANAGED BLOCK {{item.name}}" with_items: - - { name: host1, ip: 10.10.1.10 } - - { name: host2, ip: 10.10.1.11 } - - { name: host3, ip: 10.10.1.12 } + - { name: host1, ip: 10.10.1.10 } + - { name: host2, ip: 10.10.1.11 } + - { name: host3, ip: 10.10.1.12 } """ import re From 1f61381faeedeb9e705b3de94b4606364a06ab21 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Sun, 4 Dec 2016 10:54:53 +0000 Subject: [PATCH 2469/2522] Native YAML - monitoring/datadog_event (#3637) --- monitoring/datadog_event.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/monitoring/datadog_event.py b/monitoring/datadog_event.py index 88d921bf912..30fb2cafd96 100644 --- a/monitoring/datadog_event.py +++ b/monitoring/datadog_event.py @@ -95,14 +95,19 @@ EXAMPLES = ''' # Post an event with low priority -datadog_event: title="Testing from ansible" text="Test!" priority="low" - api_key: "9775a026f1ca7d1c6c5af9d94d9595a4" - app_key: "j4JyCYfefWHhgFgiZUqRm63AXHNZQyPGBfJtAzmN" +- datadog_event: + title: Testing from ansible + text: Test + priority: low + api_key: 9775a026f1ca7d1c6c5af9d94d9595a4 + app_key: j4JyCYfefWHhgFgiZUqRm63AXHNZQyPGBfJtAzmN # Post an event with several tags -datadog_event: title="Testing from ansible" text="Test!" - api_key: "9775a026f1ca7d1c6c5af9d94d9595a4" - app_key: "j4JyCYfefWHhgFgiZUqRm63AXHNZQyPGBfJtAzmN" - tags=aa,bb,#host:{{ inventory_hostname }} +- datadog_event: + title: Testing from ansible + text: Test + api_key: 9775a026f1ca7d1c6c5af9d94d9595a4 + app_key: j4JyCYfefWHhgFgiZUqRm63AXHNZQyPGBfJtAzmN + tags: 'aa,bb,#host:{{ inventory_hostname }}' ''' # Import Datadog From 51f09503a7a624de6e28ab2e678192c19f808887 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Sun, 4 Dec 2016 10:58:06 +0000 Subject: [PATCH 2470/2522] Native YAML - monitoring/sensu_check (#3635) --- monitoring/sensu_check.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/monitoring/sensu_check.py b/monitoring/sensu_check.py index dff8d19652a..6ded2aae8ca 100644 --- a/monitoring/sensu_check.py +++ b/monitoring/sensu_check.py @@ -170,21 +170,30 @@ # Fetch metrics about the CPU load every 60 seconds, # the sensu server has a handler called 'relay' which forwards stats to graphite - name: get cpu metrics - sensu_check: name=cpu_load - command=/etc/sensu/plugins/system/cpu-mpstat-metrics.rb - metric=yes handlers=relay subscribers=common interval=60 + sensu_check: + name: cpu_load + command: /etc/sensu/plugins/system/cpu-mpstat-metrics.rb + metric: yes + handlers: relay + subscribers: common + interval: 60 # Check whether nginx is running - name: check nginx process - sensu_check: name=nginx_running - command='/etc/sensu/plugins/processes/check-procs.rb -f /var/run/nginx.pid' - handlers=default subscribers=nginx interval=60 + sensu_check: + name: nginx_running + command: /etc/sensu/plugins/processes/check-procs.rb -f /var/run/nginx.pid + handlers: default + subscribers: nginx + interval: 60 # Stop monitoring the disk capacity. # Note that the check will still show up in the sensu dashboard, # to remove it completely you need to issue a DELETE request to the sensu api. - name: check disk - sensu_check: name=check_disk_capacity state=absent + sensu_check: + name: check_disk_capacity + state: absent ''' try: From 83b86ec970ed9541f6c6a31649aeeacb3d835e58 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Mon, 5 Dec 2016 04:15:23 +0000 Subject: [PATCH 2471/2522] Native YAML, improve quotation (#3643) --- cloud/amazon/route53_facts.py | 6 +++--- cloud/centurylink/clc_aa_policy.py | 14 ++++++++------ cloud/centurylink/clc_firewall_policy.py | 2 +- cloud/centurylink/clc_group.py | 15 ++++++++------- cloud/centurylink/clc_publicip.py | 18 ++++++++++-------- cloud/centurylink/clc_server.py | 15 +++++++++------ cloud/lxd/lxd_profile.py | 4 ++-- monitoring/circonus_annotation.py | 18 +++++++++--------- monitoring/librato_annotation.py | 21 +++++++++++---------- source_control/github_key.py | 6 +++--- storage/netapp/netapp_e_auth.py | 10 +++++----- 11 files changed, 69 insertions(+), 60 deletions(-) diff --git a/cloud/amazon/route53_facts.py b/cloud/amazon/route53_facts.py index 15379688c3f..2cb84b039bc 100644 --- a/cloud/amazon/route53_facts.py +++ b/cloud/amazon/route53_facts.py @@ -134,7 +134,7 @@ route53_facts: profile: account_name query: record_sets - hosted_zone_id: 'ZZZ1111112222' + hosted_zone_id: ZZZ1111112222 max_items: 20 register: record_sets @@ -149,13 +149,13 @@ route53_facts: query: health_check health_check_method: failure_reason - health_check_id: '00000000-1111-2222-3333-12345678abcd' + health_check_id: 00000000-1111-2222-3333-12345678abcd register: health_check_failure_reason - name: Retrieve reusable delegation set details route53_facts: query: reusable_delegation_set - delegation_set_id: 'delegation id' + delegation_set_id: delegation id register: delegation_sets ''' diff --git a/cloud/centurylink/clc_aa_policy.py b/cloud/centurylink/clc_aa_policy.py index 5ba0ac35b8f..bf5f800feaf 100644 --- a/cloud/centurylink/clc_aa_policy.py +++ b/cloud/centurylink/clc_aa_policy.py @@ -74,13 +74,14 @@ tasks: - name: Create an Anti Affinity Policy clc_aa_policy: - name: 'Hammer Time' - location: 'UK3' + name: Hammer Time + location: UK3 state: present register: policy - name: debug - debug: var=policy + debug: + var: policy --- - name: Delete AA Policy @@ -90,13 +91,14 @@ tasks: - name: Delete an Anti Affinity Policy clc_aa_policy: - name: 'Hammer Time' - location: 'UK3' + name: Hammer Time + location: UK3 state: absent register: policy - name: debug - debug: var=policy + debug: + var: policy ''' RETURN = ''' diff --git a/cloud/centurylink/clc_firewall_policy.py b/cloud/centurylink/clc_firewall_policy.py index c26128a40ba..78a334a6430 100644 --- a/cloud/centurylink/clc_firewall_policy.py +++ b/cloud/centurylink/clc_firewall_policy.py @@ -126,7 +126,7 @@ source_account_alias: WFAD location: VA1 state: absent - firewall_policy_id: 'c62105233d7a4231bd2e91b9c791e43e1' + firewall_policy_id: c62105233d7a4231bd2e91b9c791e43e1 ''' RETURN = ''' diff --git a/cloud/centurylink/clc_group.py b/cloud/centurylink/clc_group.py index 661196b79b3..76364a02c0f 100644 --- a/cloud/centurylink/clc_group.py +++ b/cloud/centurylink/clc_group.py @@ -83,13 +83,14 @@ tasks: - name: Create / Verify a Server Group at CenturyLink Cloud clc_group: - name: 'My Cool Server Group' - parent: 'Default Group' + name: My Cool Server Group + parent: Default Group state: present register: clc - name: debug - debug: var=clc + debug: + var: clc # Delete a Server Group @@ -101,14 +102,14 @@ tasks: - name: Delete / Verify Absent a Server Group at CenturyLink Cloud clc_group: - name: 'My Cool Server Group' - parent: 'Default Group' + name: My Cool Server Group + parent: Default Group state: absent register: clc - name: debug - debug: var=clc - + debug: + var: clc ''' RETURN = ''' diff --git a/cloud/centurylink/clc_publicip.py b/cloud/centurylink/clc_publicip.py index 98e2e15dbe3..531f19ca07c 100644 --- a/cloud/centurylink/clc_publicip.py +++ b/cloud/centurylink/clc_publicip.py @@ -81,17 +81,18 @@ tasks: - name: Create Public IP For Servers clc_publicip: - protocol: 'TCP' + protocol: TCP ports: - - 80 + - 80 server_ids: - - UC1TEST-SVR01 - - UC1TEST-SVR02 + - UC1TEST-SVR01 + - UC1TEST-SVR02 state: present register: clc - name: debug - debug: var=clc + debug: + var: clc - name: Delete Public IP from Server hosts: localhost @@ -101,13 +102,14 @@ - name: Create Public IP For Servers clc_publicip: server_ids: - - UC1TEST-SVR01 - - UC1TEST-SVR02 + - UC1TEST-SVR01 + - UC1TEST-SVR02 state: absent register: clc - name: debug - debug: var=clc + debug: + var: clc ''' RETURN = ''' diff --git a/cloud/centurylink/clc_server.py b/cloud/centurylink/clc_server.py index 3cb44040126..f98cd54e049 100644 --- a/cloud/centurylink/clc_server.py +++ b/cloud/centurylink/clc_server.py @@ -249,7 +249,7 @@ name: test template: ubuntu-14-64 count: 1 - group: 'Default Group' + group: Default Group state: present - name: Ensure 'Default Group' has exactly 5 servers @@ -257,22 +257,25 @@ name: test template: ubuntu-14-64 exact_count: 5 - count_group: 'Default Group' - group: 'Default Group' + count_group: Default Group + group: Default Group - name: Stop a Server clc_server: - server_ids: ['UC1ACCT-TEST01'] + server_ids: + - UC1ACCT-TEST01 state: stopped - name: Start a Server clc_server: - server_ids: ['UC1ACCT-TEST01'] + server_ids: + - UC1ACCT-TEST01 state: started - name: Delete a Server clc_server: - server_ids: ['UC1ACCT-TEST01'] + server_ids: + - UC1ACCT-TEST01 state: absent ''' diff --git a/cloud/lxd/lxd_profile.py b/cloud/lxd/lxd_profile.py index 272a88b1748..831765bfe5f 100644 --- a/cloud/lxd/lxd_profile.py +++ b/cloud/lxd/lxd_profile.py @@ -105,7 +105,7 @@ name: macvlan state: present config: {} - description: 'my macvlan profile' + description: my macvlan profile devices: eth0: nictype: macvlan @@ -126,7 +126,7 @@ name: macvlan state: present config: {} - description: 'my macvlan profile' + description: my macvlan profile devices: eth0: nictype: macvlan diff --git a/monitoring/circonus_annotation.py b/monitoring/circonus_annotation.py index 1452547a781..2435c3caa70 100644 --- a/monitoring/circonus_annotation.py +++ b/monitoring/circonus_annotation.py @@ -64,22 +64,22 @@ # Create a simple annotation event with a source, defaults to start and end time of now - circonus_annotation: api_key: XXXXXXXXXXXXXXXXX - title: 'App Config Change' - description: 'This is a detailed description of the config change' - category: 'This category groups like annotations' + title: App Config Change + description: This is a detailed description of the config change + category: This category groups like annotations # Create an annotation with a duration of 5 minutes and a default start time of now - circonus_annotation: api_key: XXXXXXXXXXXXXXXXX - title: 'App Config Change' - description: 'This is a detailed description of the config change' - category: 'This category groups like annotations' + title: App Config Change + description: This is a detailed description of the config change + category: This category groups like annotations duration: 300 # Create an annotation with a start_time and end_time - circonus_annotation: api_key: XXXXXXXXXXXXXXXXX - title: 'App Config Change' - description: 'This is a detailed description of the config change' - category: 'This category groups like annotations' + title: App Config Change + description: This is a detailed description of the config change + category: This category groups like annotations start_time: 1395940006 end_time: 1395954407 ''' diff --git a/monitoring/librato_annotation.py b/monitoring/librato_annotation.py index f174bda0ea4..d73de459834 100644 --- a/monitoring/librato_annotation.py +++ b/monitoring/librato_annotation.py @@ -77,27 +77,28 @@ - librato_annotation: user: user@example.com api_key: XXXXXXXXXXXXXXXXX - title: 'App Config Change' - source: 'foo.bar' - description: 'This is a detailed description of the config change' + title: App Config Change + source: foo.bar + description: This is a detailed description of the config change # Create an annotation that includes a link - librato_annotation: user: user@example.com api_key: XXXXXXXXXXXXXXXXXX - name: 'code.deploy' - title: 'app code deploy' - description: 'this is a detailed description of a deployment' + name: code.deploy + title: app code deploy + description: this is a detailed description of a deployment links: - - { rel: 'example', href: 'http://www.example.com/deploy' } + - rel: example + href: http://www.example.com/deploy # Create an annotation with a start_time and end_time - librato_annotation: user: user@example.com api_key: XXXXXXXXXXXXXXXXXX - name: 'maintenance' - title: 'Maintenance window' - description: 'This is a detailed description of maintenance' + name: maintenance + title: Maintenance window + description: This is a detailed description of maintenance start_time: 1395940006 end_time: 1395954406 ''' diff --git a/source_control/github_key.py b/source_control/github_key.py index 9e2e2e9bd9b..c27285625a4 100644 --- a/source_control/github_key.py +++ b/source_control/github_key.py @@ -78,9 +78,9 @@ - name: Authorize key with GitHub local_action: module: github_key - name: 'Access Key for Some Machine' - token: '{{github_access_token}}' - pubkey: '{{ssh_pub_key.stdout}}' + name: Access Key for Some Machine + token: '{{ github_access_token }}' + pubkey: '{{ ssh_pub_key.stdout }}' ''' diff --git a/storage/netapp/netapp_e_auth.py b/storage/netapp/netapp_e_auth.py index 36fd7919dcf..c22de91cd12 100644 --- a/storage/netapp/netapp_e_auth.py +++ b/storage/netapp/netapp_e_auth.py @@ -73,12 +73,12 @@ - name: Test module netapp_e_auth: name: trex - current_password: 'B4Dpwd' - new_password: 'W0rs3P4sswd' + current_password: OldPasswd + new_password: NewPasswd set_admin: yes - api_url: "{{ netapp_api_url }}" - api_username: "{{ netapp_api_username }}" - api_password: "{{ netapp_api_password }}" + api_url: '{{ netapp_api_url }}' + api_username: '{{ netapp_api_username }}' + api_password: '{{ netapp_api_password }}' ''' RETURN = ''' From 5aa1143b172e670693bc9aee26b691a35d936567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Mon, 5 Dec 2016 16:15:15 +0100 Subject: [PATCH 2472/2522] cloudstack: cs_staticnat: implement VPC support (#3409) --- cloud/cloudstack/cs_staticnat.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cloud/cloudstack/cs_staticnat.py b/cloud/cloudstack/cs_staticnat.py index 1d721612b2d..c023b947fff 100644 --- a/cloud/cloudstack/cs_staticnat.py +++ b/cloud/cloudstack/cs_staticnat.py @@ -48,6 +48,12 @@ required: false default: null version_added: "2.2" + vpc: + description: + - Name of the VPC. + required: false + default: null + version_added: "2.3" state: description: - State of the static NAT. @@ -235,6 +241,7 @@ def main(): vm = dict(default=None), vm_guest_ip = dict(default=None), network = dict(default=None), + vpc = dict(default=None), state = dict(choices=['present', 'absent'], default='present'), zone = dict(default=None), domain = dict(default=None), From 424e853dd48e3d0976484dd51b2740bc82a75803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Mon, 5 Dec 2016 16:18:28 +0100 Subject: [PATCH 2473/2522] cloudstack: cs_ip_address: implement VPC support (#3403) --- cloud/cloudstack/cs_ip_address.py | 62 +++++++++---------------------- 1 file changed, 18 insertions(+), 44 deletions(-) diff --git a/cloud/cloudstack/cs_ip_address.py b/cloud/cloudstack/cs_ip_address.py index 62d6ac211a2..2eb21411987 100644 --- a/cloud/cloudstack/cs_ip_address.py +++ b/cloud/cloudstack/cs_ip_address.py @@ -139,62 +139,34 @@ def __init__(self, module): 'ipaddress': 'ip_address', } - - #TODO: Add to parent class, duplicated in cs_network - def get_network(self, key=None, network=None): - if not network: - network = self.module.params.get('network') - - if not network: - return None - - args = {} - args['account'] = self.get_account('name') - args['domainid'] = self.get_domain('id') - args['projectid'] = self.get_project('id') - args['zoneid'] = self.get_zone('id') - - networks = self.cs.listNetworks(**args) - if not networks: - self.module.fail_json(msg="No networks available") - - for n in networks['network']: - if network in [ n['displaytext'], n['name'], n['id'] ]: - return self._get_by_key(key, n) - self.module.fail_json(msg="Network '%s' not found" % network) - - - #TODO: Merge changes here with parent class def get_ip_address(self, key=None): if self.ip_address: return self._get_by_key(key, self.ip_address) ip_address = self.module.params.get('ip_address') - if not ip_address: - self.module.fail_json(msg="IP address param 'ip_address' is required") - - args = {} - args['ipaddress'] = ip_address - args['account'] = self.get_account(key='name') - args['domainid'] = self.get_domain(key='id') - args['projectid'] = self.get_project(key='id') - args['vpcid'] = self.get_vpc(key='id') + args = { + 'ipaddress': self.module.params.get('ip_address'), + 'account': self.get_account(key='name'), + 'domainid': self.get_domain(key='id'), + 'projectid': self.get_project(key='id'), + 'vpcid': self.get_vpc(key='id'), + } ip_addresses = self.cs.listPublicIpAddresses(**args) if ip_addresses: self.ip_address = ip_addresses['publicipaddress'][0] return self._get_by_key(key, self.ip_address) - def associate_ip_address(self): self.result['changed'] = True - args = {} - args['account'] = self.get_account(key='name') - args['domainid'] = self.get_domain(key='id') - args['projectid'] = self.get_project(key='id') - args['networkid'] = self.get_network(key='id') - args['zoneid'] = self.get_zone(key='id') - ip_address = {} + args = { + 'account': self.get_account(key='name'), + 'domainid': self.get_domain(key='id'), + 'projectid': self.get_project(key='id'), + 'networkid': self.get_network(key='id'), + 'zoneid': self.get_zone(key='id'), + } + ip_address = None if not self.module.check_mode: res = self.cs.associateIpAddress(**args) if 'errortext' in res: @@ -205,7 +177,6 @@ def associate_ip_address(self): ip_address = self.poll_job(res, 'ipaddress') return ip_address - def disassociate_ip_address(self): ip_address = self.get_ip_address() if not ip_address: @@ -241,6 +212,9 @@ def main(): module = AnsibleModule( argument_spec=argument_spec, required_together=cs_required_together(), + required_if=[ + ('state', 'absent', ['ip_address']), + ], supports_check_mode=True ) From 572daab063f32cb3b95e4ae161dcadf7c43085bf Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Mon, 5 Dec 2016 16:51:12 +0100 Subject: [PATCH 2474/2522] ovirt_vms: Add support to specify template version (#3567) --- cloud/ovirt/ovirt_vms.py | 48 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/cloud/ovirt/ovirt_vms.py b/cloud/ovirt/ovirt_vms.py index 0cda1f62964..29cb2695cb9 100644 --- a/cloud/ovirt/ovirt_vms.py +++ b/cloud/ovirt/ovirt_vms.py @@ -60,6 +60,16 @@ description: - "Name of the template, which should be used to create Virtual Machine. Required if creating VM." - "If template is not specified and VM doesn't exist, VM will be created from I(Blank) template." + template_version: + description: + - "Version number of the template to be used for VM." + - "By default the latest available version of the template is used." + version_added: "2.3" + use_latest_template_version: + description: + - "Specify if latest template version should be used, when running a stateless VM." + - "If this parameter is set to I(true) stateless VM is created." + version_added: "2.3" memory: description: - "Amount of memory of the Virtual Machine. Prefix uses IEC 60027-2 standard (for example 1GiB, 1024MiB)." @@ -221,6 +231,13 @@ name: myvm template: rhel7_template +# Creates a stateless VM which will always use latest template version: +ovirt_vms: + name: myvm + template: rhel7 + cluster: mycluster + use_latest_template_version: true + # Creates a new server rhel7 Virtual Machine from Blank template # on brq01 cluster with 2GiB memory and 2 vcpu cores/sockets # and attach bootable disk with name rhel7_disk and attach virtio NIC @@ -346,16 +363,38 @@ class VmsModule(BaseModule): + def __get_template_with_version(self): + """ + oVirt in version 4.1 doesn't support search by template+version_number, + so we need to list all templates with specific name and then iterate + throught it's version until we find the version we look for. + """ + template = None + if self._module.params['template']: + templates_service = self._connection.system_service().templates_service() + templates = templates_service.list(search='name=%s' % self._module.params['template']) + if self._module.params['template_version']: + templates = [ + t for t in templates + if t.version.version_number == self._module.params['template_version'] + ] + if templates: + template = templates[0] + + return template + def build_entity(self): + template = self.__get_template_with_version() return otypes.Vm( name=self._module.params['name'], cluster=otypes.Cluster( name=self._module.params['cluster'] ) if self._module.params['cluster'] else None, template=otypes.Template( - name=self._module.params['template'] - ) if self._module.params['template'] else None, - stateless=self._module.params['stateless'], + id=template.id, + ) if template else None, + use_latest_template_version=self._module.params['use_latest_template_version'], + stateless=self._module.params['stateless'] or self._module.params['use_latest_template_version'], delete_protected=self._module.params['delete_protected'], high_availability=otypes.HighAvailability( enabled=self._module.params['high_availability'] @@ -403,6 +442,7 @@ def update_check(self, entity): equal(self._module.params.get('stateless'), entity.stateless) and equal(self._module.params.get('cpu_shares'), entity.cpu_shares) and equal(self._module.params.get('delete_protected'), entity.delete_protected) and + equal(self._module.params.get('use_latest_template_version'), entity.use_latest_template_version) and equal(self._module.params.get('boot_devices'), [str(dev) for dev in getattr(entity.os, 'devices', [])]) ) @@ -658,6 +698,8 @@ def main(): id=dict(default=None), cluster=dict(default=None), template=dict(default=None), + template_version=dict(default=None, type='int'), + use_latest_template_version=dict(default=None, type='bool'), disks=dict(default=[], type='list'), memory=dict(default=None), memory_guaranteed=dict(default=None), From 8cadf88e868eacd838f8e3786100ee500615f773 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Mon, 5 Dec 2016 16:19:27 +0000 Subject: [PATCH 2475/2522] Make `main()` calls conditional - database (#3659) --- database/misc/mongodb_user.py | 4 +++- database/misc/redis.py | 4 +++- database/postgresql/postgresql_ext.py | 3 ++- database/postgresql/postgresql_lang.py | 4 +++- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index e4280827c3b..5e9453f96c2 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -445,4 +445,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.pycompat24 import get_exception -main() + +if __name__ == '__main__': + main() diff --git a/database/misc/redis.py b/database/misc/redis.py index 49d9cc23a7d..6e906b24b3e 100644 --- a/database/misc/redis.py +++ b/database/misc/redis.py @@ -341,4 +341,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.pycompat24 import get_exception -main() + +if __name__ == '__main__': + main() diff --git a/database/postgresql/postgresql_ext.py b/database/postgresql/postgresql_ext.py index b9fe87c4e69..9f14a182858 100644 --- a/database/postgresql/postgresql_ext.py +++ b/database/postgresql/postgresql_ext.py @@ -189,5 +189,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.pycompat24 import get_exception -main() +if __name__ == '__main__': + main() diff --git a/database/postgresql/postgresql_lang.py b/database/postgresql/postgresql_lang.py index 8e4fad3731c..a25432954c7 100644 --- a/database/postgresql/postgresql_lang.py +++ b/database/postgresql/postgresql_lang.py @@ -285,4 +285,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.pycompat24 import get_exception -main() + +if __name__ == '__main__': + main() From a861bb8c2fb9f4f9e47d7b15c83435149008e97b Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Mon, 5 Dec 2016 16:20:10 +0000 Subject: [PATCH 2476/2522] Make `main()` calls conditional - files (#3658) --- files/patch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/files/patch.py b/files/patch.py index 123d667fdbf..8b62c519073 100644 --- a/files/patch.py +++ b/files/patch.py @@ -193,4 +193,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() From 9684b456be7e7de1d3446273b184ebf9c61d66ba Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Mon, 5 Dec 2016 16:20:51 +0000 Subject: [PATCH 2477/2522] Make `main()` calls conditional - cloud (#3657) --- cloud/amazon/ec2_win_password.py | 3 ++- cloud/misc/ovirt.py | 4 +++- cloud/misc/proxmox.py | 4 +++- cloud/misc/proxmox_template.py | 4 +++- cloud/misc/virt.py | 4 +++- cloud/profitbricks/profitbricks_datacenter.py | 3 ++- cloud/profitbricks/profitbricks_nic.py | 3 ++- cloud/profitbricks/profitbricks_volume_attachments.py | 3 ++- cloud/rackspace/rax_clb_ssl.py | 3 ++- cloud/rackspace/rax_mon_alarm.py | 3 ++- cloud/rackspace/rax_mon_check.py | 3 ++- cloud/rackspace/rax_mon_entity.py | 3 ++- cloud/rackspace/rax_mon_notification.py | 3 ++- cloud/rackspace/rax_mon_notification_plan.py | 3 ++- cloud/smartos/smartos_image_facts.py | 4 +++- cloud/webfaction/webfaction_app.py | 3 ++- cloud/webfaction/webfaction_db.py | 3 ++- cloud/webfaction/webfaction_domain.py | 3 ++- cloud/webfaction/webfaction_mailbox.py | 3 ++- cloud/webfaction/webfaction_site.py | 3 ++- cloud/xenserver_facts.py | 3 ++- 21 files changed, 47 insertions(+), 21 deletions(-) diff --git a/cloud/amazon/ec2_win_password.py b/cloud/amazon/ec2_win_password.py index e0f6205f3b6..b320a24c5de 100644 --- a/cloud/amazon/ec2_win_password.py +++ b/cloud/amazon/ec2_win_password.py @@ -172,4 +172,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/misc/ovirt.py b/cloud/misc/ovirt.py index 75572448ebe..7f0b421339f 100644 --- a/cloud/misc/ovirt.py +++ b/cloud/misc/ovirt.py @@ -518,4 +518,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/cloud/misc/proxmox.py b/cloud/misc/proxmox.py index 694d79e9267..db0233c22f5 100644 --- a/cloud/misc/proxmox.py +++ b/cloud/misc/proxmox.py @@ -553,4 +553,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/cloud/misc/proxmox_template.py b/cloud/misc/proxmox_template.py index 69a2272408f..70cd445a185 100644 --- a/cloud/misc/proxmox_template.py +++ b/cloud/misc/proxmox_template.py @@ -252,4 +252,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/cloud/misc/virt.py b/cloud/misc/virt.py index 8c1e2969ac2..468cd2c5fb6 100644 --- a/cloud/misc/virt.py +++ b/cloud/misc/virt.py @@ -529,4 +529,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.pycompat24 import get_exception -main() + +if __name__ == '__main__': + main() diff --git a/cloud/profitbricks/profitbricks_datacenter.py b/cloud/profitbricks/profitbricks_datacenter.py index 0b21d3e4cd6..de64f1c210e 100644 --- a/cloud/profitbricks/profitbricks_datacenter.py +++ b/cloud/profitbricks/profitbricks_datacenter.py @@ -255,4 +255,5 @@ def main(): from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/profitbricks/profitbricks_nic.py b/cloud/profitbricks/profitbricks_nic.py index 902d5266843..8835e1175ad 100644 --- a/cloud/profitbricks/profitbricks_nic.py +++ b/cloud/profitbricks/profitbricks_nic.py @@ -287,4 +287,5 @@ def main(): from ansible.module_utils.basic import * -main() \ No newline at end of file +if __name__ == '__main__': + main() diff --git a/cloud/profitbricks/profitbricks_volume_attachments.py b/cloud/profitbricks/profitbricks_volume_attachments.py index fe87594fddc..ac4db01364f 100644 --- a/cloud/profitbricks/profitbricks_volume_attachments.py +++ b/cloud/profitbricks/profitbricks_volume_attachments.py @@ -259,4 +259,5 @@ def main(): from ansible.module_utils.basic import * -main() \ No newline at end of file +if __name__ == '__main__': + main() diff --git a/cloud/rackspace/rax_clb_ssl.py b/cloud/rackspace/rax_clb_ssl.py index 90773058165..57b21354110 100644 --- a/cloud/rackspace/rax_clb_ssl.py +++ b/cloud/rackspace/rax_clb_ssl.py @@ -266,4 +266,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.rax import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/rackspace/rax_mon_alarm.py b/cloud/rackspace/rax_mon_alarm.py index a3f29e22f50..2a7a3d8db6b 100644 --- a/cloud/rackspace/rax_mon_alarm.py +++ b/cloud/rackspace/rax_mon_alarm.py @@ -224,4 +224,5 @@ def main(): from ansible.module_utils.rax import * # Invoke the module. -main() +if __name__ == '__main__': + main() diff --git a/cloud/rackspace/rax_mon_check.py b/cloud/rackspace/rax_mon_check.py index 14b86864e2f..6cc5eade348 100644 --- a/cloud/rackspace/rax_mon_check.py +++ b/cloud/rackspace/rax_mon_check.py @@ -310,4 +310,5 @@ def main(): from ansible.module_utils.rax import * # Invoke the module. -main() +if __name__ == '__main__': + main() diff --git a/cloud/rackspace/rax_mon_entity.py b/cloud/rackspace/rax_mon_entity.py index 7369aaafa3b..cbceb495d7b 100644 --- a/cloud/rackspace/rax_mon_entity.py +++ b/cloud/rackspace/rax_mon_entity.py @@ -189,4 +189,5 @@ def main(): from ansible.module_utils.rax import * # Invoke the module. -main() +if __name__ == '__main__': + main() diff --git a/cloud/rackspace/rax_mon_notification.py b/cloud/rackspace/rax_mon_notification.py index d7b6692dc2c..092b1826b8d 100644 --- a/cloud/rackspace/rax_mon_notification.py +++ b/cloud/rackspace/rax_mon_notification.py @@ -173,4 +173,5 @@ def main(): from ansible.module_utils.rax import * # Invoke the module. -main() +if __name__ == '__main__': + main() diff --git a/cloud/rackspace/rax_mon_notification_plan.py b/cloud/rackspace/rax_mon_notification_plan.py index 5bb3fa1652a..41a15bca239 100644 --- a/cloud/rackspace/rax_mon_notification_plan.py +++ b/cloud/rackspace/rax_mon_notification_plan.py @@ -178,4 +178,5 @@ def main(): from ansible.module_utils.rax import * # Invoke the module. -main() +if __name__ == '__main__': + main() diff --git a/cloud/smartos/smartos_image_facts.py b/cloud/smartos/smartos_image_facts.py index eb3ecd08a3d..6a16e2e9653 100644 --- a/cloud/smartos/smartos_image_facts.py +++ b/cloud/smartos/smartos_image_facts.py @@ -114,4 +114,6 @@ def main(): module.exit_json(ansible_facts=data) from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/cloud/webfaction/webfaction_app.py b/cloud/webfaction/webfaction_app.py index 8f40a9ab85f..77cb6f17554 100644 --- a/cloud/webfaction/webfaction_app.py +++ b/cloud/webfaction/webfaction_app.py @@ -195,5 +195,6 @@ def main(): ) from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/webfaction/webfaction_db.py b/cloud/webfaction/webfaction_db.py index 6c45e700e9b..8f3d77c3dfb 100644 --- a/cloud/webfaction/webfaction_db.py +++ b/cloud/webfaction/webfaction_db.py @@ -196,5 +196,6 @@ def main(): ) from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/webfaction/webfaction_domain.py b/cloud/webfaction/webfaction_domain.py index c809dd6beb3..d89add88cb1 100644 --- a/cloud/webfaction/webfaction_domain.py +++ b/cloud/webfaction/webfaction_domain.py @@ -167,5 +167,6 @@ def main(): ) from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/webfaction/webfaction_mailbox.py b/cloud/webfaction/webfaction_mailbox.py index bcb355c9632..08008d7d5cf 100644 --- a/cloud/webfaction/webfaction_mailbox.py +++ b/cloud/webfaction/webfaction_mailbox.py @@ -135,5 +135,6 @@ def main(): from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/webfaction/webfaction_site.py b/cloud/webfaction/webfaction_site.py index bd5504b6b46..46f440b6a02 100644 --- a/cloud/webfaction/webfaction_site.py +++ b/cloud/webfaction/webfaction_site.py @@ -206,5 +206,6 @@ def main(): from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/xenserver_facts.py b/cloud/xenserver_facts.py index 04c88d34312..1ca8e9e6c81 100644 --- a/cloud/xenserver_facts.py +++ b/cloud/xenserver_facts.py @@ -201,4 +201,5 @@ def main(): from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() From 2fcd4af567b6f06108368f03547552d4501cadc5 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Mon, 5 Dec 2016 16:21:26 +0000 Subject: [PATCH 2478/2522] Make `main()` calls conditional - notifications (#3656) --- notification/flowdock.py | 4 ++-- notification/grove.py | 4 +++- notification/hall.py | 4 +++- notification/irc.py | 4 +++- notification/jabber.py | 4 +++- notification/mail.py | 4 +++- notification/mqtt.py | 4 +++- notification/nexmo.py | 3 ++- notification/osx_say.py | 4 +++- notification/pushbullet.py | 4 +++- 10 files changed, 28 insertions(+), 11 deletions(-) diff --git a/notification/flowdock.py b/notification/flowdock.py index 1e1e5e9fde0..7297ef1f63a 100644 --- a/notification/flowdock.py +++ b/notification/flowdock.py @@ -191,5 +191,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.urls import * -main() - +if __name__ == '__main__': + main() diff --git a/notification/grove.py b/notification/grove.py index c7661456897..9db937c0cf7 100644 --- a/notification/grove.py +++ b/notification/grove.py @@ -114,4 +114,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.urls import * -main() + +if __name__ == '__main__': + main() diff --git a/notification/hall.py b/notification/hall.py index eb55f5c9454..162bb5153b4 100755 --- a/notification/hall.py +++ b/notification/hall.py @@ -94,4 +94,6 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.urls import * -main() + +if __name__ == '__main__': + main() diff --git a/notification/irc.py b/notification/irc.py index 8217805ea79..765e155551d 100644 --- a/notification/irc.py +++ b/notification/irc.py @@ -306,4 +306,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.pycompat24 import get_exception -main() + +if __name__ == '__main__': + main() diff --git a/notification/jabber.py b/notification/jabber.py index d9fa0015157..4da7f8296fc 100644 --- a/notification/jabber.py +++ b/notification/jabber.py @@ -167,4 +167,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.pycompat24 import get_exception -main() + +if __name__ == '__main__': + main() diff --git a/notification/mail.py b/notification/mail.py index fbbdcff2674..f51982f37c1 100644 --- a/notification/mail.py +++ b/notification/mail.py @@ -308,4 +308,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.pycompat24 import get_exception -main() + +if __name__ == '__main__': + main() diff --git a/notification/mqtt.py b/notification/mqtt.py index 4a6e1084815..b28e57dc4a8 100644 --- a/notification/mqtt.py +++ b/notification/mqtt.py @@ -207,4 +207,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.pycompat24 import get_exception -main() + +if __name__ == '__main__': + main() diff --git a/notification/nexmo.py b/notification/nexmo.py index 89a246c0d90..fa6b0b2225a 100644 --- a/notification/nexmo.py +++ b/notification/nexmo.py @@ -138,4 +138,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.urls import * -main() +if __name__ == '__main__': + main() diff --git a/notification/osx_say.py b/notification/osx_say.py index e803bed12f3..70946228489 100644 --- a/notification/osx_say.py +++ b/notification/osx_say.py @@ -76,4 +76,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/notification/pushbullet.py b/notification/pushbullet.py index 0d5ab7c4d48..434eb1fda0d 100644 --- a/notification/pushbullet.py +++ b/notification/pushbullet.py @@ -184,4 +184,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() From 255d894bb08950c2ed212d31d30101f629ba2829 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Mon, 5 Dec 2016 16:21:52 +0000 Subject: [PATCH 2479/2522] Make `main()` calls conditional - clustering (#3647) --- clustering/znode.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/clustering/znode.py b/clustering/znode.py index 0e36c0d994b..90c0420ad5d 100644 --- a/clustering/znode.py +++ b/clustering/znode.py @@ -248,4 +248,5 @@ def _wait(self, path, timeout, interval=5): from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() From eee420c9fc74e60759dc97adf96c4209240d49b4 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Mon, 5 Dec 2016 16:22:17 +0000 Subject: [PATCH 2480/2522] Conditional main - monitoring (#3648) --- monitoring/airbrake_deployment.py | 4 ++-- monitoring/logentries.py | 3 ++- monitoring/monit.py | 3 ++- monitoring/nagios.py | 4 +++- monitoring/newrelic_deployment.py | 4 ++-- monitoring/pagerduty.py | 3 ++- monitoring/pingdom.py | 4 +++- monitoring/sensu_check.py | 4 +++- monitoring/zabbix_group.py | 4 +++- monitoring/zabbix_host.py | 4 +++- monitoring/zabbix_hostmacro.py | 3 ++- monitoring/zabbix_maintenance.py | 4 +++- 12 files changed, 30 insertions(+), 14 deletions(-) diff --git a/monitoring/airbrake_deployment.py b/monitoring/airbrake_deployment.py index 282d4c60d30..d6daec2ceb4 100644 --- a/monitoring/airbrake_deployment.py +++ b/monitoring/airbrake_deployment.py @@ -128,5 +128,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.urls import * -main() - +if __name__ == '__main__': + main() diff --git a/monitoring/logentries.py b/monitoring/logentries.py index f4e3fef3294..c772b6a1cd0 100644 --- a/monitoring/logentries.py +++ b/monitoring/logentries.py @@ -152,4 +152,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/monitoring/monit.py b/monitoring/monit.py index eafa3e02b77..f7c46b86bec 100644 --- a/monitoring/monit.py +++ b/monitoring/monit.py @@ -184,4 +184,5 @@ def wait_for_monit_to_stop_pending(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/monitoring/nagios.py b/monitoring/nagios.py index cb7b1ae5699..b14d03e98f7 100644 --- a/monitoring/nagios.py +++ b/monitoring/nagios.py @@ -1078,4 +1078,6 @@ def act(self): ###################################################################### # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/monitoring/newrelic_deployment.py b/monitoring/newrelic_deployment.py index 6224fbf48d7..4d47169e89c 100644 --- a/monitoring/newrelic_deployment.py +++ b/monitoring/newrelic_deployment.py @@ -144,5 +144,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.urls import * -main() - +if __name__ == '__main__': + main() diff --git a/monitoring/pagerduty.py b/monitoring/pagerduty.py index 43b2af94a12..a6d6da7d72b 100644 --- a/monitoring/pagerduty.py +++ b/monitoring/pagerduty.py @@ -307,4 +307,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.urls import * -main() +if __name__ == '__main__': + main() diff --git a/monitoring/pingdom.py b/monitoring/pingdom.py index 6709c6bc6cd..8b719e48082 100644 --- a/monitoring/pingdom.py +++ b/monitoring/pingdom.py @@ -151,4 +151,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/monitoring/sensu_check.py b/monitoring/sensu_check.py index 6ded2aae8ca..9acbfefa837 100644 --- a/monitoring/sensu_check.py +++ b/monitoring/sensu_check.py @@ -393,4 +393,6 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.pycompat24 import get_exception -main() + +if __name__ == '__main__': + main() diff --git a/monitoring/zabbix_group.py b/monitoring/zabbix_group.py index ec1f65ed358..2dd8d98331e 100644 --- a/monitoring/zabbix_group.py +++ b/monitoring/zabbix_group.py @@ -222,4 +222,6 @@ def main(): module.exit_json(changed=False) from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/monitoring/zabbix_host.py b/monitoring/zabbix_host.py index 34b68c78b9f..611a5f9faa1 100644 --- a/monitoring/zabbix_host.py +++ b/monitoring/zabbix_host.py @@ -574,4 +574,6 @@ def main(): host_name, ip, link_templates)) from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/monitoring/zabbix_hostmacro.py b/monitoring/zabbix_hostmacro.py index c02f9b6eb98..446f658b269 100644 --- a/monitoring/zabbix_hostmacro.py +++ b/monitoring/zabbix_hostmacro.py @@ -239,5 +239,6 @@ def main(): host_macro_class_obj.update_host_macro(host_macro_obj, macro_name, macro_value) from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/monitoring/zabbix_maintenance.py b/monitoring/zabbix_maintenance.py index 247b0a8e717..7fe0a494609 100644 --- a/monitoring/zabbix_maintenance.py +++ b/monitoring/zabbix_maintenance.py @@ -382,4 +382,6 @@ def main(): module.exit_json(changed=changed) from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() From 5cdc64435d98214615d21815efe0a6dc39c165a9 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Mon, 5 Dec 2016 16:22:51 +0000 Subject: [PATCH 2481/2522] Make `main()` calls conditional - network (#3649) --- network/citrix/netscaler.py | 4 +++- network/dnsimple.py | 3 ++- network/dnsmadeeasy.py | 3 ++- network/haproxy.py | 4 ++-- network/illumos/dladm_etherstub.py | 4 +++- network/illumos/dladm_vnic.py | 4 +++- network/illumos/flowadm.py | 4 +++- network/illumos/ipadm_if.py | 4 +++- network/illumos/ipadm_prop.py | 4 +++- network/lldp.py | 3 ++- network/nmcli.py | 3 ++- network/openvswitch_bridge.py | 4 +++- network/openvswitch_db.py | 4 +++- network/openvswitch_port.py | 4 +++- network/snmp_facts.py | 3 ++- 15 files changed, 39 insertions(+), 16 deletions(-) diff --git a/network/citrix/netscaler.py b/network/citrix/netscaler.py index b7465267424..c84cf740bd2 100644 --- a/network/citrix/netscaler.py +++ b/network/citrix/netscaler.py @@ -201,4 +201,6 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.urls import * from ansible.module_utils.pycompat24 import get_exception -main() + +if __name__ == '__main__': + main() diff --git a/network/dnsimple.py b/network/dnsimple.py index 17941f496fe..0042229c976 100644 --- a/network/dnsimple.py +++ b/network/dnsimple.py @@ -335,4 +335,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.pycompat24 import get_exception -main() +if __name__ == '__main__': + main() diff --git a/network/dnsmadeeasy.py b/network/dnsmadeeasy.py index eac0859c1f5..c4007542a66 100644 --- a/network/dnsmadeeasy.py +++ b/network/dnsmadeeasy.py @@ -393,4 +393,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.urls import * -main() +if __name__ == '__main__': + main() diff --git a/network/haproxy.py b/network/haproxy.py index d1502de6f3b..4cfc8128674 100644 --- a/network/haproxy.py +++ b/network/haproxy.py @@ -386,5 +386,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() - +if __name__ == '__main__': + main() diff --git a/network/illumos/dladm_etherstub.py b/network/illumos/dladm_etherstub.py index 9e2bf2b1734..3107f8e843e 100644 --- a/network/illumos/dladm_etherstub.py +++ b/network/illumos/dladm_etherstub.py @@ -172,4 +172,6 @@ def main(): module.exit_json(**result) from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/network/illumos/dladm_vnic.py b/network/illumos/dladm_vnic.py index 4c85cd89afd..81be07313a5 100644 --- a/network/illumos/dladm_vnic.py +++ b/network/illumos/dladm_vnic.py @@ -265,4 +265,6 @@ def main(): module.exit_json(**result) from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/network/illumos/flowadm.py b/network/illumos/flowadm.py index ec93d6a0f64..82e0250b701 100644 --- a/network/illumos/flowadm.py +++ b/network/illumos/flowadm.py @@ -514,4 +514,6 @@ def main(): from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/network/illumos/ipadm_if.py b/network/illumos/ipadm_if.py index d2bff90e3bc..06db3b6a656 100644 --- a/network/illumos/ipadm_if.py +++ b/network/illumos/ipadm_if.py @@ -223,4 +223,6 @@ def main(): module.exit_json(**result) from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/network/illumos/ipadm_prop.py b/network/illumos/ipadm_prop.py index 5399189ad35..15410eb5e09 100644 --- a/network/illumos/ipadm_prop.py +++ b/network/illumos/ipadm_prop.py @@ -261,4 +261,6 @@ def main(): from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/network/lldp.py b/network/lldp.py index 981aca0f0ed..acbc914112a 100644 --- a/network/lldp.py +++ b/network/lldp.py @@ -83,5 +83,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/network/nmcli.py b/network/nmcli.py index a35d90f85a0..35718cc46c0 100644 --- a/network/nmcli.py +++ b/network/nmcli.py @@ -1190,4 +1190,5 @@ def main(): module.exit_json(**result) -main() +if __name__ == '__main__': + main() diff --git a/network/openvswitch_bridge.py b/network/openvswitch_bridge.py index 0cbf3096b20..1b9a7f7abf9 100644 --- a/network/openvswitch_bridge.py +++ b/network/openvswitch_bridge.py @@ -308,4 +308,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.pycompat24 import get_exception -main() + +if __name__ == '__main__': + main() diff --git a/network/openvswitch_db.py b/network/openvswitch_db.py index 8c2a290a913..39f27649cfb 100644 --- a/network/openvswitch_db.py +++ b/network/openvswitch_db.py @@ -137,4 +137,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/network/openvswitch_port.py b/network/openvswitch_port.py index fc84fe44a3a..52c2d94b4c4 100644 --- a/network/openvswitch_port.py +++ b/network/openvswitch_port.py @@ -285,4 +285,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.pycompat24 import get_exception -main() + +if __name__ == '__main__': + main() diff --git a/network/snmp_facts.py b/network/snmp_facts.py index b8ae753770d..eee7a4690ba 100644 --- a/network/snmp_facts.py +++ b/network/snmp_facts.py @@ -367,4 +367,5 @@ def main(): module.exit_json(ansible_facts=results) -main() +if __name__ == '__main__': + main() From 29698c277773609abb31a1697d20faef51694771 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Mon, 5 Dec 2016 16:23:08 +0000 Subject: [PATCH 2482/2522] Make `main()` calls conditional - packaging (#3650) --- packaging/dpkg_selections.py | 4 +++- packaging/language/cpanm.py | 3 ++- packaging/language/npm.py | 4 +++- packaging/language/pear.py | 3 ++- packaging/os/macports.py | 3 ++- packaging/os/openbsd_pkg.py | 4 +++- packaging/os/opkg.py | 3 ++- packaging/os/pkg5.py | 4 +++- packaging/os/pkg5_publisher.py | 4 +++- packaging/os/pkgutil.py | 4 +++- packaging/os/portage.py | 3 ++- packaging/os/portinstall.py | 3 ++- packaging/os/slackpkg.py | 3 ++- packaging/os/svr4pkg.py | 4 +++- packaging/os/swdepot.py | 4 ++-- packaging/os/urpmi.py | 3 ++- packaging/os/zypper_repository.py | 3 ++- 17 files changed, 41 insertions(+), 18 deletions(-) diff --git a/packaging/dpkg_selections.py b/packaging/dpkg_selections.py index 81dd849e599..bf13dc13fd7 100644 --- a/packaging/dpkg_selections.py +++ b/packaging/dpkg_selections.py @@ -75,4 +75,6 @@ def main(): from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/packaging/language/cpanm.py b/packaging/language/cpanm.py index 26566c6dbf8..addf0c198d7 100644 --- a/packaging/language/cpanm.py +++ b/packaging/language/cpanm.py @@ -231,4 +231,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/packaging/language/npm.py b/packaging/language/npm.py index 58e29dc86e2..4b147e37035 100644 --- a/packaging/language/npm.py +++ b/packaging/language/npm.py @@ -286,4 +286,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/packaging/language/pear.py b/packaging/language/pear.py index ae513baf142..880c275f25b 100644 --- a/packaging/language/pear.py +++ b/packaging/language/pear.py @@ -232,4 +232,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/packaging/os/macports.py b/packaging/os/macports.py index 07926b9520b..1e56fc84f0b 100644 --- a/packaging/os/macports.py +++ b/packaging/os/macports.py @@ -229,4 +229,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/packaging/os/openbsd_pkg.py b/packaging/os/openbsd_pkg.py index e68ef18989e..f86a4d081ae 100644 --- a/packaging/os/openbsd_pkg.py +++ b/packaging/os/openbsd_pkg.py @@ -543,4 +543,6 @@ def main(): # Import module snippets. from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/packaging/os/opkg.py b/packaging/os/opkg.py index 1d740202408..50b37d3cdb5 100644 --- a/packaging/os/opkg.py +++ b/packaging/os/opkg.py @@ -182,4 +182,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/packaging/os/pkg5.py b/packaging/os/pkg5.py index b1ade8e4dfd..86a555a37fa 100644 --- a/packaging/os/pkg5.py +++ b/packaging/os/pkg5.py @@ -168,4 +168,6 @@ def is_latest(module, package): from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/packaging/os/pkg5_publisher.py b/packaging/os/pkg5_publisher.py index cc9c3c8ca8b..4eee4d4f31c 100644 --- a/packaging/os/pkg5_publisher.py +++ b/packaging/os/pkg5_publisher.py @@ -203,4 +203,6 @@ def unstringify(val): from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/packaging/os/pkgutil.py b/packaging/os/pkgutil.py index 8495a19b24c..5323581ba57 100644 --- a/packaging/os/pkgutil.py +++ b/packaging/os/pkgutil.py @@ -225,4 +225,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/packaging/os/portage.py b/packaging/os/portage.py index 0f3731968bc..dc4c22890f6 100644 --- a/packaging/os/portage.py +++ b/packaging/os/portage.py @@ -513,4 +513,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/packaging/os/portinstall.py b/packaging/os/portinstall.py index 44f21efb7c7..78555a02194 100644 --- a/packaging/os/portinstall.py +++ b/packaging/os/portinstall.py @@ -209,4 +209,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/packaging/os/slackpkg.py b/packaging/os/slackpkg.py index 9b65afddc86..ac0c230bfa5 100644 --- a/packaging/os/slackpkg.py +++ b/packaging/os/slackpkg.py @@ -201,4 +201,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/packaging/os/svr4pkg.py b/packaging/os/svr4pkg.py index 1d8a9b26d5b..e5931941562 100644 --- a/packaging/os/svr4pkg.py +++ b/packaging/os/svr4pkg.py @@ -258,4 +258,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/packaging/os/swdepot.py b/packaging/os/swdepot.py index 8c7652af966..017e91b0b58 100644 --- a/packaging/os/swdepot.py +++ b/packaging/os/swdepot.py @@ -202,5 +202,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() - +if __name__ == '__main__': + main() diff --git a/packaging/os/urpmi.py b/packaging/os/urpmi.py index 47d7c1f6846..934ef11ee22 100644 --- a/packaging/os/urpmi.py +++ b/packaging/os/urpmi.py @@ -209,4 +209,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/packaging/os/zypper_repository.py b/packaging/os/zypper_repository.py index 40510db9a98..7fae6065bcb 100644 --- a/packaging/os/zypper_repository.py +++ b/packaging/os/zypper_repository.py @@ -406,4 +406,5 @@ def exit_unchanged(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() From 89827618e2fba70c6796839b6a31f64d39a0efed Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Mon, 5 Dec 2016 16:23:23 +0000 Subject: [PATCH 2483/2522] Make `main()` calls conditional - source_control (#3651) --- source_control/bzr.py | 4 +++- source_control/github_hooks.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/source_control/bzr.py b/source_control/bzr.py index 8a75789a11e..961c715d905 100644 --- a/source_control/bzr.py +++ b/source_control/bzr.py @@ -199,4 +199,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/source_control/github_hooks.py b/source_control/github_hooks.py index eec7a6f990d..0430b44007e 100644 --- a/source_control/github_hooks.py +++ b/source_control/github_hooks.py @@ -201,4 +201,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.urls import * -main() +if __name__ == '__main__': + main() From 1825252033b01870cd528628b9f017c40c45a1ff Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Mon, 5 Dec 2016 16:24:19 +0000 Subject: [PATCH 2484/2522] Make `main()` calls conditional - system (#3652) --- system/at.py | 4 +++- system/capabilities.py | 4 +++- system/cronvar.py | 3 ++- system/crypttab.py | 3 ++- system/debconf.py | 3 ++- system/facter.py | 4 ++-- system/getent.py | 4 ++-- system/gluster_volume.py | 3 ++- system/kernel_blacklist.py | 4 +++- system/known_hosts.py | 3 ++- system/locale_gen.py | 3 ++- system/lvg.py | 4 +++- system/modprobe.py | 3 ++- system/ohai.py | 3 ++- system/open_iscsi.py | 4 ++-- system/osx_defaults.py | 3 ++- system/pam_limits.py | 4 +++- system/puppet.py | 3 ++- system/selinux_permissive.py | 4 ++-- system/seport.py | 3 ++- system/solaris_zone.py | 4 +++- system/svc.py | 3 ++- system/ufw.py | 3 ++- system/zfs.py | 4 +++- 24 files changed, 55 insertions(+), 28 deletions(-) diff --git a/system/at.py b/system/at.py index 52006e54710..9c5f10b5947 100644 --- a/system/at.py +++ b/system/at.py @@ -206,4 +206,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/system/capabilities.py b/system/capabilities.py index 009507669fb..67cd66b2b0c 100644 --- a/system/capabilities.py +++ b/system/capabilities.py @@ -189,4 +189,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/system/cronvar.py b/system/cronvar.py index 4c198d65d81..1a26182be88 100644 --- a/system/cronvar.py +++ b/system/cronvar.py @@ -437,4 +437,5 @@ def main(): module.exit_json(msg="Unable to execute cronvar task.") -main() +if __name__ == '__main__': + main() diff --git a/system/crypttab.py b/system/crypttab.py index 0565f57f9eb..27523415dfb 100644 --- a/system/crypttab.py +++ b/system/crypttab.py @@ -370,4 +370,5 @@ def __str__(self): ret.append('%s=%s' % (k, v)) return ','.join(ret) -main() +if __name__ == '__main__': + main() diff --git a/system/debconf.py b/system/debconf.py index 468f0b5725e..3c9218b408e 100644 --- a/system/debconf.py +++ b/system/debconf.py @@ -188,4 +188,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/system/facter.py b/system/facter.py index b594836df9c..d9a7d65cca9 100644 --- a/system/facter.py +++ b/system/facter.py @@ -57,5 +57,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() - +if __name__ == '__main__': + main() diff --git a/system/getent.py b/system/getent.py index 2b70a2856c5..d200d420fd4 100644 --- a/system/getent.py +++ b/system/getent.py @@ -157,5 +157,5 @@ def main(): module.fail_json(msg=msg) -main() - +if __name__ == '__main__': + main() diff --git a/system/gluster_volume.py b/system/gluster_volume.py index f5bca5f9e83..f34511a3eaf 100644 --- a/system/gluster_volume.py +++ b/system/gluster_volume.py @@ -522,4 +522,5 @@ def main(): module.exit_json(changed=changed, ansible_facts=facts) -main() +if __name__ == '__main__': + main() diff --git a/system/kernel_blacklist.py b/system/kernel_blacklist.py index 2100b158fda..701ba883517 100644 --- a/system/kernel_blacklist.py +++ b/system/kernel_blacklist.py @@ -140,4 +140,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/system/known_hosts.py b/system/known_hosts.py index 40c13002ddc..656fb38d4a1 100644 --- a/system/known_hosts.py +++ b/system/known_hosts.py @@ -305,4 +305,5 @@ def main(): results = enforce_state(module,module.params) module.exit_json(**results) -main() +if __name__ == '__main__': + main() diff --git a/system/locale_gen.py b/system/locale_gen.py index 57e79a25b73..bd4b149dcce 100644 --- a/system/locale_gen.py +++ b/system/locale_gen.py @@ -238,4 +238,5 @@ def main(): module.exit_json(name=name, changed=changed, msg="OK") -main() +if __name__ == '__main__': + main() diff --git a/system/lvg.py b/system/lvg.py index d0b0409a634..427cb1b1c1d 100644 --- a/system/lvg.py +++ b/system/lvg.py @@ -260,4 +260,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/system/modprobe.py b/system/modprobe.py index 1acd2ef3ed3..ef5a9dd4ed8 100644 --- a/system/modprobe.py +++ b/system/modprobe.py @@ -127,4 +127,5 @@ def main(): module.exit_json(**args) -main() +if __name__ == '__main__': + main() diff --git a/system/ohai.py b/system/ohai.py index d71d581b628..222a81e6d93 100644 --- a/system/ohai.py +++ b/system/ohai.py @@ -53,4 +53,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/system/open_iscsi.py b/system/open_iscsi.py index 77586289e77..71eeda1aa8c 100644 --- a/system/open_iscsi.py +++ b/system/open_iscsi.py @@ -381,5 +381,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() - +if __name__ == '__main__': + main() diff --git a/system/osx_defaults.py b/system/osx_defaults.py index 986a263ede6..12a3ec8f219 100644 --- a/system/osx_defaults.py +++ b/system/osx_defaults.py @@ -412,4 +412,5 @@ def main(): # /main ------------------------------------------------------------------- }}} -main() +if __name__ == '__main__': + main() diff --git a/system/pam_limits.py b/system/pam_limits.py index 23fa35725e1..5b5cc3583e0 100644 --- a/system/pam_limits.py +++ b/system/pam_limits.py @@ -281,4 +281,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/system/puppet.py b/system/puppet.py index 411552d86b8..6686682cae1 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -284,4 +284,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/system/selinux_permissive.py b/system/selinux_permissive.py index 6374f94bbdf..e18eb2fbc00 100644 --- a/system/selinux_permissive.py +++ b/system/selinux_permissive.py @@ -131,5 +131,5 @@ def main(): permissive=permissive, domain=domain) - -main() +if __name__ == '__main__': + main() diff --git a/system/seport.py b/system/seport.py index c661db43084..0e8c76197b2 100644 --- a/system/seport.py +++ b/system/seport.py @@ -316,4 +316,5 @@ def main(): module.exit_json(**result) -main() +if __name__ == '__main__': + main() diff --git a/system/solaris_zone.py b/system/solaris_zone.py index f57649130fd..aa569d7e0d2 100644 --- a/system/solaris_zone.py +++ b/system/solaris_zone.py @@ -477,4 +477,6 @@ def main(): module.exit_json(changed=zone.changed, msg=', '.join(zone.msg)) from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/system/svc.py b/system/svc.py index 376062b4be5..43eea5b0f7b 100755 --- a/system/svc.py +++ b/system/svc.py @@ -312,4 +312,5 @@ def main(): -main() +if __name__ == '__main__': + main() diff --git a/system/ufw.py b/system/ufw.py index 67eaba131c7..c0f48fca667 100644 --- a/system/ufw.py +++ b/system/ufw.py @@ -329,4 +329,5 @@ def execute(cmd): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/system/zfs.py b/system/zfs.py index 47ce13edce8..7dda883b893 100644 --- a/system/zfs.py +++ b/system/zfs.py @@ -272,4 +272,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() From 3620f419fff63de4a8de2c24f8e830e929c3ff32 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Mon, 5 Dec 2016 16:24:34 +0000 Subject: [PATCH 2485/2522] Make `main()` calls conditional - web_infrastructure (#3653) --- web_infrastructure/ejabberd_user.py | 3 ++- web_infrastructure/jboss.py | 4 +++- web_infrastructure/jira.py | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/web_infrastructure/ejabberd_user.py b/web_infrastructure/ejabberd_user.py index 0f47167f23d..989145a36f8 100644 --- a/web_infrastructure/ejabberd_user.py +++ b/web_infrastructure/ejabberd_user.py @@ -221,4 +221,5 @@ def main(): module.exit_json(**result) -main() +if __name__ == '__main__': + main() diff --git a/web_infrastructure/jboss.py b/web_infrastructure/jboss.py index 53ffcf1f840..8957f1b31de 100644 --- a/web_infrastructure/jboss.py +++ b/web_infrastructure/jboss.py @@ -147,4 +147,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/web_infrastructure/jira.py b/web_infrastructure/jira.py index 479e623b3c1..e7d1e1a9017 100755 --- a/web_infrastructure/jira.py +++ b/web_infrastructure/jira.py @@ -385,5 +385,5 @@ def main(): module.exit_json(changed=True, meta=ret) - -main() +if __name__ == '__main__': + main() From acf1f63325f00b512377af7df3c92308266a1777 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Mon, 5 Dec 2016 16:24:50 +0000 Subject: [PATCH 2486/2522] Make `main()` calls conditional - messaging (#3654) --- messaging/rabbitmq_binding.py | 4 +++- messaging/rabbitmq_exchange.py | 4 +++- messaging/rabbitmq_parameter.py | 4 +++- messaging/rabbitmq_plugin.py | 4 +++- messaging/rabbitmq_policy.py | 4 +++- messaging/rabbitmq_queue.py | 4 +++- messaging/rabbitmq_user.py | 4 +++- messaging/rabbitmq_vhost.py | 4 +++- 8 files changed, 24 insertions(+), 8 deletions(-) diff --git a/messaging/rabbitmq_binding.py b/messaging/rabbitmq_binding.py index bc466388acc..2d0f9e4f089 100644 --- a/messaging/rabbitmq_binding.py +++ b/messaging/rabbitmq_binding.py @@ -224,4 +224,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/messaging/rabbitmq_exchange.py b/messaging/rabbitmq_exchange.py index 836db467bae..0c955820820 100644 --- a/messaging/rabbitmq_exchange.py +++ b/messaging/rabbitmq_exchange.py @@ -219,4 +219,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/messaging/rabbitmq_parameter.py b/messaging/rabbitmq_parameter.py index 602f92fc4c4..f3d188fd25a 100644 --- a/messaging/rabbitmq_parameter.py +++ b/messaging/rabbitmq_parameter.py @@ -157,4 +157,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/messaging/rabbitmq_plugin.py b/messaging/rabbitmq_plugin.py index 6aa4fac3053..832af02faf8 100644 --- a/messaging/rabbitmq_plugin.py +++ b/messaging/rabbitmq_plugin.py @@ -149,4 +149,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/messaging/rabbitmq_policy.py b/messaging/rabbitmq_policy.py index 8293f6f8972..ef7ffc60686 100644 --- a/messaging/rabbitmq_policy.py +++ b/messaging/rabbitmq_policy.py @@ -171,4 +171,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/messaging/rabbitmq_queue.py b/messaging/rabbitmq_queue.py index 7b0b69affe4..09860175606 100644 --- a/messaging/rabbitmq_queue.py +++ b/messaging/rabbitmq_queue.py @@ -265,4 +265,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/messaging/rabbitmq_user.py b/messaging/rabbitmq_user.py index 470ae7c1431..c51c6b0b592 100644 --- a/messaging/rabbitmq_user.py +++ b/messaging/rabbitmq_user.py @@ -305,4 +305,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/messaging/rabbitmq_vhost.py b/messaging/rabbitmq_vhost.py index 1ffb3d2674f..ad8f9cba207 100644 --- a/messaging/rabbitmq_vhost.py +++ b/messaging/rabbitmq_vhost.py @@ -146,4 +146,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() From 936086fe1bd961af7ca56e5685bc4cda611a91ea Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Mon, 5 Dec 2016 16:25:01 +0000 Subject: [PATCH 2487/2522] Make `main()` calls conditional - commands (#3655) --- commands/expect.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/commands/expect.py b/commands/expect.py index c6f70e750e0..a8eff373e06 100644 --- a/commands/expect.py +++ b/commands/expect.py @@ -234,4 +234,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.pycompat24 import get_exception -main() +if __name__ == '__main__': + main() From 659782dbc609bd8b1cc2fd2d50c58b324c3ed454 Mon Sep 17 00:00:00 2001 From: Tristan de Cacqueray Date: Mon, 5 Dec 2016 16:27:29 +0000 Subject: [PATCH 2488/2522] Use parameters in os_stack update (#3560) This change makes os_stack module idempotent. Otherwise, re-use of the module fails with: Error updating stack: ERROR: The Parameter (...) was not provided. Fixes #3165. --- cloud/openstack/os_stack.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cloud/openstack/os_stack.py b/cloud/openstack/os_stack.py index 503ae635dbb..3d3cb9be07e 100644 --- a/cloud/openstack/os_stack.py +++ b/cloud/openstack/os_stack.py @@ -180,7 +180,8 @@ def _update_stack(module, stack, cloud): environment_files=module.params['environment'], timeout=module.params['timeout'], rollback=module.params['rollback'], - wait=module.params['wait']) + wait=module.params['wait'], + **module.params['parameters']) if stack['stack_status'] == 'UPDATE_COMPLETE': return stack From 405e6501b9e567ee1cbcac075672b3f284b49804 Mon Sep 17 00:00:00 2001 From: Ben Tomasik Date: Mon, 5 Dec 2016 10:33:54 -0600 Subject: [PATCH 2489/2522] Add check mode support (#3522) --- cloud/amazon/ec2_elb_facts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_elb_facts.py b/cloud/amazon/ec2_elb_facts.py index 122e0f70a2c..1c6fb747638 100644 --- a/cloud/amazon/ec2_elb_facts.py +++ b/cloud/amazon/ec2_elb_facts.py @@ -220,7 +220,8 @@ def main(): names={'default': [], 'type': 'list'} ) ) - module = AnsibleModule(argument_spec=argument_spec) + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) if not HAS_BOTO: module.fail_json(msg='boto required for this module') From ba3f8058e2c1e43621bf31dfd047043025956e15 Mon Sep 17 00:00:00 2001 From: Constantin Date: Mon, 5 Dec 2016 16:50:48 +0000 Subject: [PATCH 2490/2522] Fix: convert owner_ids to a list of strings (#3488) --- cloud/amazon/ec2_snapshot_facts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_snapshot_facts.py b/cloud/amazon/ec2_snapshot_facts.py index 357a1692d89..2ce0619441f 100644 --- a/cloud/amazon/ec2_snapshot_facts.py +++ b/cloud/amazon/ec2_snapshot_facts.py @@ -172,7 +172,7 @@ def list_ec2_snapshots(connection, module): snapshot_ids = module.params.get("snapshot_ids") - owner_ids = module.params.get("owner_ids") + owner_ids = map(str, module.params.get("owner_ids")) restorable_by_user_ids = module.params.get("restorable_by_user_ids") filters = ansible_dict_to_boto3_filter_list(module.params.get("filters")) From 8d0052a6ba68db2059bae70696938a05022857e5 Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Mon, 5 Dec 2016 18:27:38 +0100 Subject: [PATCH 2491/2522] Add new ovirt_mac_pools module (#3646) This patch adds new module to manage oVirt MAC pools. --- cloud/ovirt/ovirt_mac_pools.py | 176 +++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 cloud/ovirt/ovirt_mac_pools.py diff --git a/cloud/ovirt/ovirt_mac_pools.py b/cloud/ovirt/ovirt_mac_pools.py new file mode 100644 index 00000000000..6f14f6f78ff --- /dev/null +++ b/cloud/ovirt/ovirt_mac_pools.py @@ -0,0 +1,176 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +import traceback + +try: + import ovirtsdk4.types as otypes +except ImportError: + pass + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + BaseModule, + check_sdk, + equal, + create_connection, + ovirt_full_argument_spec, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_mac_pools +short_description: Module to manage MAC pools in oVirt +version_added: "2.3" +author: "Ondra Machacek (@machacekondra)" +description: + - "This module manage MAC pools in oVirt." +options: + name: + description: + - "Name of the the MAC pool to manage." + required: true + description: + description: + - "Description of the MAC pool." + state: + description: + - "Should the mac pool be present or absent." + choices: ['present', 'absent'] + default: present + allow_duplicates: + description: + - "If (true) allow a MAC address to be used multiple times in a pool." + - "Default value is set by oVirt engine to I(false)." + ranges: + description: + - "List of MAC ranges. The from and to should be splitted by comma." + - "For example: 00:1a:4a:16:01:51,00:1a:4a:16:01:61" +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Create MAC pool: +- ovirt_mac_pools: + name: mymacpool + allow_duplicates: false + ranges: + - 00:1a:4a:16:01:51,00:1a:4a:16:01:61 + - 00:1a:4a:16:02:51,00:1a:4a:16:02:61 + +# Remove MAC pool: +- ovirt_mac_pools: + state: absent + name: mymacpool +''' + +RETURN = ''' +id: + description: ID of the MAC pool which is managed + returned: On success if MAC pool is found. + type: str + sample: 7de90f31-222c-436c-a1ca-7e655bd5b60c +template: + description: "Dictionary of all the MAC pool attributes. MAC pool attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/mac_pool." + returned: On success if MAC pool is found. +''' + + +class MACPoolModule(BaseModule): + + def build_entity(self): + return otypes.MacPool( + name=self._module.params['name'], + allow_duplicates=self._module.params['allow_duplicates'], + description=self._module.params['description'], + ranges=[ + otypes.Range( + from_=mac_range.split(',')[0], + to=mac_range.split(',')[1], + ) + for mac_range in self._module.params['ranges'] + ], + ) + + def _compare_ranges(self, entity): + if self._module.params['ranges'] is not None: + ranges = sorted([ + '%s,%s' % (mac_range.from_, mac_range.to) + for mac_range in entity.ranges + ]) + return equal(sorted(self._module.params['ranges']), ranges) + + return True + + def update_check(self, entity): + return ( + self._compare_ranges(entity) and + equal(self._module.params['allow_duplicates'], entity.allow_duplicates) and + equal(self._module.params['description'], entity.description) + ) + + +def main(): + argument_spec = ovirt_full_argument_spec( + state=dict( + choices=['present', 'absent'], + default='present', + ), + name=dict(default=None, required=True), + allow_duplicates=dict(default=None, type='bool'), + description=dict(default=None), + ranges=dict(default=None, type='list'), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + check_sdk(module) + + try: + connection = create_connection(module.params.pop('auth')) + mac_pools_service = connection.system_service().mac_pools_service() + mac_pools_module = MACPoolModule( + connection=connection, + module=module, + service=mac_pools_service, + ) + + state = module.params['state'] + if state == 'present': + ret = mac_pools_module.create() + elif state == 'absent': + ret = mac_pools_module.remove() + + module.exit_json(**ret) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + + +if __name__ == "__main__": + main() From 0baef3c5662bcd18fec06aa0940083107e89acf2 Mon Sep 17 00:00:00 2001 From: Krzysztof Magosa Date: Mon, 5 Dec 2016 18:28:13 +0100 Subject: [PATCH 2492/2522] kubernetes: handle situation when target host does not have yaml library (fixes #3301) (#3449) --- clustering/kubernetes.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/clustering/kubernetes.py b/clustering/kubernetes.py index 18372cb62d9..12c62b30924 100644 --- a/clustering/kubernetes.py +++ b/clustering/kubernetes.py @@ -150,9 +150,14 @@ phase: "Active" ''' -import yaml import base64 +try: + import yaml + has_lib_yaml = True +except ImportError: + has_lib_yaml = False + ############################################################################ ############################################################################ # For API coverage, this Anislbe module provides capability to operate on @@ -325,6 +330,9 @@ def main(): required_one_of = (('file_reference', 'inline_data'),), ) + if not has_lib_yaml: + module.fail_json(msg="missing python library: yaml") + decode_cert_data(module) api_endpoint = module.params.get('api_endpoint') From c57b1674f231f417c181a978420851c53c5d9bca Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Mon, 5 Dec 2016 18:31:44 +0100 Subject: [PATCH 2493/2522] Add ovirt_clusters and ovirt_clusters_facts modules (#3138) * Add ovirt_clusters and ovirt_clusters_facts modules * Add return values examples * Improve documentation * Added all cluster parameters --- cloud/ovirt/ovirt_clusters.py | 560 ++++++++++++++++++++++++++++ cloud/ovirt/ovirt_clusters_facts.py | 99 +++++ 2 files changed, 659 insertions(+) create mode 100644 cloud/ovirt/ovirt_clusters.py create mode 100644 cloud/ovirt/ovirt_clusters_facts.py diff --git a/cloud/ovirt/ovirt_clusters.py b/cloud/ovirt/ovirt_clusters.py new file mode 100644 index 00000000000..fda01e7f464 --- /dev/null +++ b/cloud/ovirt/ovirt_clusters.py @@ -0,0 +1,560 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +import traceback + +try: + import ovirtsdk4.types as otypes +except ImportError: + pass + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + BaseModule, + check_sdk, + create_connection, + equal, + ovirt_full_argument_spec, + search_by_name, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_clusters +short_description: Module to manage clusters in oVirt +version_added: "2.3" +author: "Ondra Machacek (@machacekondra)" +description: + - "Module to manage clusters in oVirt" +options: + name: + description: + - "Name of the the cluster to manage." + required: true + state: + description: + - "Should the cluster be present or absent" + choices: ['present', 'absent'] + default: present + datacenter: + description: + - "Datacenter name where cluster reside." + description: + description: + - "Description of the cluster." + comment: + description: + - "Comment of the cluster." + network: + description: + - "Management network of cluster to access cluster hosts." + ballooning: + description: + - "If (True) enable memory balloon optimization. Memory balloon is used to + re-distribute / reclaim the host memory based on VM needs + in a dynamic way." + virt: + description: + - "If (True), hosts in this cluster will be used to run virtual machines." + gluster: + description: + - "If (True), hosts in this cluster will be used as Gluster Storage + server nodes, and not for running virtual machines." + - "By default the cluster is created for virtual machine hosts." + threads_as_cores: + description: + - "If (True) the exposed host threads would be treated as cores + which can be utilized by virtual machines." + ksm: + description: + - "I (True) MoM enables to run Kernel Same-page Merging (KSM) when + necessary and when it can yield a memory saving benefit that + outweighs its CPU cost." + ksm_numa: + description: + - "If (True) enables KSM C(ksm) for best berformance inside NUMA nodes." + ha_reservation: + description: + - "If (True) enable the oVirt to monitor cluster capacity for highly + available virtual machines." + trusted_service: + description: + - "If (True) enable integration with an OpenAttestation server." + vm_reason: + description: + - "If (True) enable an optional reason field when a virtual machine + is shut down from the Manager, allowing the administrator to + provide an explanation for the maintenance." + host_reason: + description: + - "If (True) enable an optional reason field when a host is placed + into maintenance mode from the Manager, allowing the administrator + to provide an explanation for the maintenance." + memory_policy: + description: + - "I(disabled) - Disables memory page sharing." + - "I(server) - Sets the memory page sharing threshold to 150% of the system memory on each host." + - "I(desktop) - Sets the memory page sharing threshold to 200% of the system memory on each host." + choices: ['disabled', 'server', 'desktop'] + rng_sources: + description: + - "List that specify the random number generator devices that all hosts in the cluster will use." + - "Supported generators are: I(hwrng) and I(random)." + spice_proxy: + description: + - "The proxy by which the SPICE client will connect to virtual machines." + - "The address must be in the following format: I(protocol://[host]:[port])" + fence_enabled: + description: + - "If (True) enables fencing on the cluster." + - "Fencing is enabled by default." + fence_skip_if_sd_active: + description: + - "If (True) any hosts in the cluster that are Non Responsive + and still connected to storage will not be fenced." + fence_skip_if_connectivity_broken: + description: + - "If (True) fencing will be temporarily disabled if the percentage + of hosts in the cluster that are experiencing connectivity issues + is greater than or equal to the defined threshold." + - "The threshold can be specified by C(fence_connectivity_threshold)." + fence_connectivity_threshold: + description: + - "The threshold used by C(fence_skip_if_connectivity_broken)." + resilience_policy: + description: + - "The resilience policy defines how the virtual machines are prioritized in the migration." + - "Following values are supported:" + - "C(do_not_migrate) - Prevents virtual machines from being migrated. " + - "C(migrate) - Migrates all virtual machines in order of their defined priority." + - "C(migrate_highly_available) - Migrates only highly available virtual machines to prevent overloading other hosts." + choices: ['do_not_migrate', 'migrate', 'migrate_highly_available'] + migration_bandwidth: + description: + - "The bandwidth settings define the maximum bandwidth of both outgoing and incoming migrations per host." + - "Following bandwith options are supported:" + - "C(auto) - Bandwidth is copied from the I(rate limit) [Mbps] setting in the data center host network QoS." + - "C(hypervisor_default) - Bandwidth is controlled by local VDSM setting on sending host." + - "C(custom) - Defined by user (in Mbps)." + choices: ['auto', 'hypervisor_default', 'custom'] + migration_bandwidth_limit: + description: + - "Set the I(custom) migration bandwidth limit." + - "This parameter is used only when C(migration_bandwidth) is I(custom)." + migration_auto_converge: + description: + - "If (True) auto-convergence is used during live migration of virtual machines." + - "Used only when C(migration_policy) is set to I(legacy)." + - "Following options are supported:" + - "C(true) - Override the global setting to I(true)." + - "C(false) - Override the global setting to I(false)." + - "C(inherit) - Use value which is set globally." + choices: ['true', 'false', 'inherit'] + migration_compressed: + description: + - "If (True) compression is used during live migration of the virtual machine." + - "Used only when C(migration_policy) is set to I(legacy)." + - "Following options are supported:" + - "C(true) - Override the global setting to I(true)." + - "C(false) - Override the global setting to I(false)." + - "C(inherit) - Use value which is set globally." + choices: ['true', 'false', 'inherit'] + migration_policy: + description: + - "A migration policy defines the conditions for live migrating + virtual machines in the event of host failure." + - "Following policies are supported:" + - "C(legacy) - Legacy behavior of 3.6 version." + - "C(minimal_downtime) - Virtual machines should not experience any significant downtime." + - "C(suspend_workload) - Virtual machines may experience a more significant downtime." + choices: ['legacy', 'minimal_downtime', 'suspend_workload'] + serial_policy: + description: + - "Specify a serial number policy for the virtual machines in the cluster." + - "Following options are supported:" + - "C(vm) - Sets the virtual machine's UUID as its serial number." + - "C(host) - Sets the host's UUID as the virtual machine's serial number." + - "C(custom) - Allows you to specify a custom serial number in C(serial_policy_value)." + serial_policy_value: + description: + - "Allows you to specify a custom serial number." + - "This parameter is used only when C(serial_policy) is I(custom)." + scheduling_policy: + description: + - "Name of the scheduling policy to be used for cluster." + cpu_arch: + description: + - "CPU architecture of cluster." + choices: ['x86_64', 'ppc64', 'undefined'] + cpu_type: + description: + - "CPU codename. For example I(Intel SandyBridge Family)." + switch_type: + description: + - "Type of switch to be used by all networks in given cluster. + Either I(legacy) which is using linux brigde or I(ovs) using + Open vSwitch." + choices: ['legacy', 'ovs'] + compatibility_version: + description: + - "The compatibility version of the cluster. All hosts in this + cluster must support at least this compatibility version." +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Create cluster +- ovirt_clusters: + datacenter: mydatacenter + name: mycluster + cpu_type: Intel SandyBridge Family + description: mycluster + compatibility_version: 4.0 + +# Create virt service cluster: +- ovirt_clusters: + datacenter: mydatacenter + name: mycluster + cpu_type: Intel Nehalem Family + description: mycluster + switch_type: legacy + compatibility_version: 4.0 + ballooning: true + gluster: false + threads_as_cores: true + ha_reservation: true + trusted_service: false + host_reason: false + vm_reason: true + ksm_numa: true + memory_policy: server + rng_sources: + - hwrng + - random + +# Remove cluster +- ovirt_clusters: + state: absent + name: mycluster +''' + +RETURN = ''' +id: + description: ID of the cluster which is managed + returned: On success if cluster is found. + type: str + sample: 7de90f31-222c-436c-a1ca-7e655bd5b60c +cluster: + description: "Dictionary of all the cluster attributes. Cluster attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/cluster." + returned: On success if cluster is found. +''' + + +class ClustersModule(BaseModule): + + def __get_major(self, full_version): + if full_version is None: + return None + if isinstance(full_version, otypes.Version): + return full_version.major + return int(full_version.split('.')[0]) + + def __get_minor(self, full_version): + if full_version is None: + return None + if isinstance(full_version, otypes.Version): + return full_version.minor + return int(full_version.split('.')[1]) + + def param(self, name, default=None): + return self._module.params.get(name, default) + + def _get_memory_policy(self): + memory_policy = self.param('memory_policy') + if memory_policy == 'desktop': + return 200 + elif memory_policy == 'server': + return 150 + elif memory_policy == 'disabled': + return 100 + + def _get_policy_id(self): + # These are hardcoded IDs, once there is API, please fix this. + # legacy - 00000000-0000-0000-0000-000000000000 + # minimal downtime - 80554327-0569-496b-bdeb-fcbbf52b827b + # suspend workload if needed - 80554327-0569-496b-bdeb-fcbbf52b827c + migration_policy = self.param('migration_policy') + if migration_policy == 'legacy': + return '00000000-0000-0000-0000-000000000000' + elif migration_policy == 'minimal_downtime': + return '80554327-0569-496b-bdeb-fcbbf52b827b' + elif migration_policy == 'suspend_workload': + return '80554327-0569-496b-bdeb-fcbbf52b827c' + + def _get_sched_policy(self): + sched_policy = None + if self.param('serial_policy'): + sched_policies_service = self._connection.system_service().scheduling_policies_service() + sched_policy = search_by_name(sched_policies_service, self.param('scheduling_policy')) + if not sched_policy: + raise Exception("Scheduling policy '%s' was not found" % self.param('scheduling_policy')) + + return sched_policy + + def build_entity(self): + sched_policy = self._get_sched_policy() + return otypes.Cluster( + name=self.param('name'), + comment=self.param('comment'), + description=self.param('description'), + ballooning_enabled=self.param('ballooning'), + gluster_service=self.param('gluster'), + virt_service=self.param('virt'), + threads_as_cores=self.param('threads_as_cores'), + ha_reservation=self.param('ha_reservation'), + trusted_service=self.param('trusted_service'), + optional_reason=self.param('vm_reason'), + maintenance_reason_required=self.param('host_reason'), + scheduling_policy=otypes.SchedulingPolicy( + id=sched_policy.id, + ) if sched_policy else None, + serial_number=otypes.SerialNumber( + policy=otypes.SerialNumberPolicy(self.param('serial_policy')), + value=self.param('serial_policy_value'), + ) if ( + self.param('serial_policy') is not None or + self.param('serial_policy_value') is not None + ) else None, + migration=otypes.MigrationOptions( + auto_converge=otypes.InheritableBoolean( + self.param('migration_auto_converge'), + ) if self.param('migration_auto_converge') else None, + bandwidth=otypes.MigrationBandwidth( + assignment_method=otypes.MigrationBandwidthAssignmentMethod( + self.param('migration_bandwidth'), + ) if self.param('migration_bandwidth') else None, + custom_value=self.param('migration_bandwidth_limit'), + ) if ( + self.param('migration_bandwidth') or + self.param('migration_bandwidth_limit') + ) else None, + compressed=otypes.InheritableBoolean( + self.param('migration_compressed'), + ) if self.param('migration_compressed') else None, + policy=otypes.MigrationPolicy( + id=self._get_policy_id() + ) if self.param('migration_policy') else None, + ) if ( + self.param('migration_bandwidth') is not None or + self.param('migration_bandwidth_limit') is not None or + self.param('migration_auto_converge') is not None or + self.param('migration_compressed') is not None or + self.param('migration_policy') is not None + ) else None, + error_handling=otypes.ErrorHandling( + on_error=otypes.MigrateOnError( + self.param('resilience_policy') + ), + ) if self.param('resilience_policy') else None, + fencing_policy=otypes.FencingPolicy( + enabled=( + self.param('fence_enabled') or + self.param('fence_skip_if_connectivity_broken') or + self.param('fence_skip_if_sd_active') + ), + skip_if_connectivity_broken=otypes.SkipIfConnectivityBroken( + enabled=self.param('fence_skip_if_connectivity_broken'), + threshold=self.param('fence_connectivity_threshold'), + ) if ( + self.param('fence_skip_if_connectivity_broken') is not None or + self.param('fence_connectivity_threshold') is not None + ) else None, + skip_if_sd_active=otypes.SkipIfSdActive( + enabled=self.param('fence_skip_if_sd_active'), + ) if self.param('fence_skip_if_sd_active') else None, + ) if ( + self.param('fence_enabled') is not None or + self.param('fence_skip_if_sd_active') is not None or + self.param('fence_skip_if_connectivity_broken') is not None or + self.param('fence_connectivity_threshold') is not None + ) else None, + display=otypes.Display( + proxy=self.param('spice_proxy'), + ) if self.param('spice_proxy') else None, + required_rng_sources=[ + otypes.RngSource(rng) for rng in self.param('rng_sources') + ] if self.param('rng_sources') else None, + memory_policy=otypes.MemoryPolicy( + over_commit=otypes.MemoryOverCommit( + percent=self._get_memory_policy(), + ), + ) if self.param('memory_policy') else None, + ksm=otypes.Ksm( + enabled=self.param('ksm') or self.param('ksm_numa'), + merge_across_nodes=not self.param('ksm_numa'), + ) if ( + self.param('ksm_numa') is not None or + self.param('ksm') is not None + ) else None, + data_center=otypes.DataCenter( + name=self.param('datacenter'), + ) if self.param('datacenter') else None, + management_network=otypes.Network( + name=self.param('network'), + ) if self.param('network') else None, + cpu=otypes.Cpu( + architecture=self.param('cpu_arch'), + type=self.param('cpu_type'), + ) if ( + self.param('cpu_arch') or self.param('cpu_type') + ) else None, + version=otypes.Version( + major=self.__get_major(self.param('compatibility_version')), + minor=self.__get_minor(self.param('compatibility_version')), + ) if self.param('compatibility_version') else None, + switch_type=otypes.SwitchType( + self.param('switch_type') + ) if self.param('switch_type') else None, + ) + + def update_check(self, entity): + return ( + equal(self.param('comment'), entity.comment) and + equal(self.param('description'), entity.description) and + equal(self.param('switch_type'), str(entity.switch_type)) and + equal(self.param('cpu_arch'), str(entity.cpu.architecture)) and + equal(self.param('cpu_type'), entity.cpu.type) and + equal(self.param('ballooning'), entity.ballooning_enabled) and + equal(self.param('gluster'), entity.gluster_service) and + equal(self.param('virt'), entity.virt_service) and + equal(self.param('threads_as_cores'), entity.threads_as_cores) and + equal(self.param('ksm_numa'), not entity.ksm.merge_across_nodes and entity.ksm.enabled) and + equal(self.param('ksm'), entity.ksm.merge_across_nodes and entity.ksm.enabled) and + equal(self.param('ha_reservation'), entity.ha_reservation) and + equal(self.param('trusted_service'), entity.trusted_service) and + equal(self.param('host_reason'), entity.maintenance_reason_required) and + equal(self.param('vm_reason'), entity.optional_reason) and + equal(self.param('spice_proxy'), getattr(entity.display, 'proxy', None)) and + equal(self.param('fence_enabled'), entity.fencing_policy.enabled) and + equal(self.param('fence_skip_if_sd_active'), entity.fencing_policy.skip_if_sd_active.enabled) and + equal(self.param('fence_skip_if_connectivity_broken'), entity.fencing_policy.skip_if_connectivity_broken.enabled) and + equal(self.param('fence_connectivity_threshold'), entity.fencing_policy.skip_if_connectivity_broken.threshold) and + equal(self.param('resilience_policy'), str(entity.error_handling.on_error)) and + equal(self.param('migration_bandwidth'), str(entity.migration.bandwidth.assignment_method)) and + equal(self.param('migration_auto_converge'), str(entity.migration.auto_converge)) and + equal(self.param('migration_compressed'), str(entity.migration.compressed)) and + equal(self.param('serial_policy'), str(entity.serial_number.policy)) and + equal(self.param('serial_policy_value'), entity.serial_number.value) and + equal(self.param('scheduling_policy'), self._get_sched_policy().name) and + equal(self._get_policy_id(), entity.migration.policy.id) and + equal(self._get_memory_policy(), entity.memory_policy.over_commit.percent) and + equal(self.__get_minor(self.param('compatibility_version')), self.__get_minor(entity.version)) and + equal(self.__get_major(self.param('compatibility_version')), self.__get_major(entity.version)) and + equal( + self.param('migration_bandwidth_limit') if self.param('migration_bandwidth') == 'custom' else None, + entity.migration.bandwidth.custom_value + ) and + equal( + sorted(self.param('rng_sources')) if self.param('rng_sources') else None, + sorted([ + str(source) for source in entity.required_rng_sources + ]) + ) + ) + + +def main(): + argument_spec = ovirt_full_argument_spec( + state=dict( + choices=['present', 'absent'], + default='present', + ), + name=dict(default=None, required=True), + ballooning=dict(default=None, type='bool', aliases=['balloon']), + gluster=dict(default=None, type='bool'), + virt=dict(default=None, type='bool'), + threads_as_cores=dict(default=None, type='bool'), + ksm_numa=dict(default=None, type='bool'), + ksm=dict(default=None, type='bool'), + ha_reservation=dict(default=None, type='bool'), + trusted_service=dict(default=None, type='bool'), + vm_reason=dict(default=None, type='bool'), + host_reason=dict(default=None, type='bool'), + memory_policy=dict(default=None, choices=['disabled', 'server', 'desktop']), + rng_sources=dict(default=None, type='list'), + spice_proxy=dict(default=None), + fence_enabled=dict(default=None, type='bool'), + fence_skip_if_sd_active=dict(default=None, type='bool'), + fence_skip_if_connectivity_broken=dict(default=None, type='bool'), + fence_connectivity_threshold=dict(default=None, type='int'), + resilience_policy=dict(default=None, choices=['migrate_highly_available', 'migrate', 'do_not_migrate']), + migration_bandwidth=dict(default=None, choices=['auto', 'hypervisor_default', 'custom']), + migration_bandwidth_limit=dict(default=None, type='int'), + migration_auto_converge=dict(default=None, choices=['true', 'false', 'inherit']), + migration_compressed=dict(default=None, choices=['true', 'false', 'inherit']), + migration_policy=dict(default=None, choices=['legacy', 'minimal_downtime', 'suspend_workload']), + serial_policy=dict(default=None, choices=['vm', 'host', 'custom']), + serial_policy_value=dict(default=None), + scheduling_policy=dict(default=None), + datacenter=dict(default=None), + description=dict(default=None), + comment=dict(default=None), + network=dict(default=None), + cpu_arch=dict(default=None, choices=['ppc64', 'undefined', 'x86_64']), + cpu_type=dict(default=None), + switch_type=dict(default=None, choices=['legacy', 'ovs']), + compatibility_version=dict(default=None), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + check_sdk(module) + + try: + connection = create_connection(module.params.pop('auth')) + clusters_service = connection.system_service().clusters_service() + clusters_module = ClustersModule( + connection=connection, + module=module, + service=clusters_service, + ) + + state = module.params['state'] + if state == 'present': + ret = clusters_module.create() + elif state == 'absent': + ret = clusters_module.remove() + + module.exit_json(**ret) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + + +if __name__ == "__main__": + main() diff --git a/cloud/ovirt/ovirt_clusters_facts.py b/cloud/ovirt/ovirt_clusters_facts.py new file mode 100644 index 00000000000..42a09c52c42 --- /dev/null +++ b/cloud/ovirt/ovirt_clusters_facts.py @@ -0,0 +1,99 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + check_sdk, + create_connection, + get_dict_of_struct, + ovirt_full_argument_spec, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_clusters_facts +short_description: Retrieve facts about one or more oVirt clusters +author: "Ondra Machacek (@machacekondra)" +version_added: "2.3" +description: + - "Retrieve facts about one or more oVirt clusters." +notes: + - "This module creates a new top-level C(ovirt_clusters) fact, which + contains a list of clusters." +options: + pattern: + description: + - "Search term which is accepted by oVirt search backend." + - "For example to search cluster X from datacenter Y use following pattern: + name=X and datacenter=Y" +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Gather facts about all clusters which names start with C: +- ovirt_clusters_facts: + pattern: name=production* +- debug: + var: ovirt_clusters +''' + +RETURN = ''' +ovirt_clusters: + description: "List of dictionaries describing the clusters. Cluster attribues are mapped to dictionary keys, + all clusters attributes can be found at following url: https://ovirt.example.com/ovirt-engine/api/model#types/cluster." + returned: On success. + type: list +''' + + +def main(): + argument_spec = ovirt_full_argument_spec( + pattern=dict(default='', required=False), + ) + module = AnsibleModule(argument_spec) + check_sdk(module) + + try: + connection = create_connection(module.params.pop('auth')) + clusters_service = connection.system_service().clusters_service() + clusters = clusters_service.list(search=module.params['pattern']) + module.exit_json( + changed=False, + ansible_facts=dict( + ovirt_clusters=[ + get_dict_of_struct(c) for c in clusters + ], + ), + ) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + + +if __name__ == '__main__': + main() From 4354a5daed7598d19ebca891de8deb262c9a9c32 Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Mon, 5 Dec 2016 18:31:54 +0100 Subject: [PATCH 2494/2522] Add ovirt_datacenters and ovirt_datacenters_facts modules (#3146) --- cloud/ovirt/ovirt_datacenters.py | 217 +++++++++++++++++++++++++ cloud/ovirt/ovirt_datacenters_facts.py | 98 +++++++++++ 2 files changed, 315 insertions(+) create mode 100644 cloud/ovirt/ovirt_datacenters.py create mode 100644 cloud/ovirt/ovirt_datacenters_facts.py diff --git a/cloud/ovirt/ovirt_datacenters.py b/cloud/ovirt/ovirt_datacenters.py new file mode 100644 index 00000000000..1e6fbf374eb --- /dev/null +++ b/cloud/ovirt/ovirt_datacenters.py @@ -0,0 +1,217 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +import traceback + +try: + import ovirtsdk4.types as otypes +except ImportError: + pass + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + BaseModule, + check_sdk, + check_params, + create_connection, + equal, + ovirt_full_argument_spec, + search_by_name, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_datacenters +short_description: Module to manage data centers in oVirt +version_added: "2.3" +author: "Ondra Machacek (@machacekondra)" +description: + - "Module to manage data centers in oVirt" +options: + name: + description: + - "Name of the the data center to manage." + required: true + state: + description: + - "Should the data center be present or absent" + choices: ['present', 'absent'] + default: present + description: + description: + - "Description of the data center." + comment: + description: + - "Comment of the data center." + local: + description: + - "I(True) if the data center should be local, I(False) if should be shared." + - "Default value is set by engine." + compatibility_version: + description: + - "Compatibility version of the data center." + quota_mode: + description: + - "Quota mode of the data center. One of I(disabled), I(audit) or I(enabled)" + choices: ['disabled', 'audit', 'enabled'] + mac_pool: + description: + - "MAC pool to be used by this datacenter." + - "IMPORTANT: This option is deprecated in oVirt 4.1. You should + use C(mac_pool) in C(ovirt_clusters) module, as MAC pools are + set per cluster since 4.1." +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Create datacenter +- ovirt_datacenters: + name: mydatacenter + local: True + compatibility_version: 4.0 + quota_mode: enabled + +# Remove datacenter +- ovirt_datacenters: + state: absent + name: mydatacenter +''' + +RETURN = ''' +id: + description: "ID of the managed datacenter" + returned: "On success if datacenter is found." + type: str + sample: 7de90f31-222c-436c-a1ca-7e655bd5b60c +data_center: + description: "Dictionary of all the datacenter attributes. Datacenter attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/datacenter." + returned: "On success if datacenter is found." +''' + + +class DatacentersModule(BaseModule): + + def __get_major(self, full_version): + if full_version is None: + return None + if isinstance(full_version, otypes.Version): + return full_version.major + return int(full_version.split('.')[0]) + + def __get_minor(self, full_version): + if full_version is None: + return None + if isinstance(full_version, otypes.Version): + return full_version.minor + return int(full_version.split('.')[1]) + + def _get_mac_pool(self): + mac_pool = None + if self._module.params.get('mac_pool'): + mac_pool = search_by_name( + self._connection.system_service().mac_pools_service(), + self._module.params.get('mac_pool'), + ) + + return mac_pool + + def build_entity(self): + return otypes.DataCenter( + name=self._module.params['name'], + comment=self._module.params['comment'], + description=self._module.params['description'], + mac_pool=otypes.MacPool( + id=getattr(self._get_mac_pool(), 'id', None), + ) if self._module.params.get('mac_pool') else None, + quota_mode=otypes.QuotaModeType( + self._module.params['quota_mode'] + ) if self._module.params['quota_mode'] else None, + local=self._module.params['local'], + version=otypes.Version( + major=self.__get_major(self._module.params['compatibility_version']), + minor=self.__get_minor(self._module.params['compatibility_version']), + ) if self._module.params['compatibility_version'] else None, + ) + + def update_check(self, entity): + minor = self.__get_minor(self._module.params.get('compatibility_version')) + major = self.__get_major(self._module.params.get('compatibility_version')) + return ( + equal(getattr(self._get_mac_pool(), 'id', None), getattr(entity.mac_pool, 'id', None)) and + equal(self._module.params.get('comment'), entity.comment) and + equal(self._module.params.get('description'), entity.description) and + equal(self._module.params.get('quota_mode'), str(entity.quota_mode)) and + equal(self._module.params.get('local'), entity.local) and + equal(minor, self.__get_minor(entity.version)) and + equal(major, self.__get_major(entity.version)) + ) + + +def main(): + argument_spec = ovirt_full_argument_spec( + state=dict( + choices=['present', 'absent'], + default='present', + ), + name=dict(default=None, required=True), + description=dict(default=None), + local=dict(type='bool'), + compatibility_version=dict(default=None), + quota_mode=dict(choices=['disabled', 'audit', 'enabled']), + comment=dict(default=None), + mac_pool=dict(default=None), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + check_sdk(module) + check_params(module) + + try: + connection = create_connection(module.params.pop('auth')) + data_centers_service = connection.system_service().data_centers_service() + clusters_module = DatacentersModule( + connection=connection, + module=module, + service=data_centers_service, + ) + + state = module.params['state'] + if state == 'present': + ret = clusters_module.create() + elif state == 'absent': + ret = clusters_module.remove() + + module.exit_json(**ret) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + + +if __name__ == "__main__": + main() diff --git a/cloud/ovirt/ovirt_datacenters_facts.py b/cloud/ovirt/ovirt_datacenters_facts.py new file mode 100644 index 00000000000..f153cec10bf --- /dev/null +++ b/cloud/ovirt/ovirt_datacenters_facts.py @@ -0,0 +1,98 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + check_sdk, + create_connection, + get_dict_of_struct, + ovirt_full_argument_spec, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_datacenters_facts +short_description: Retrieve facts about one or more oVirt datacenters +author: "Ondra Machacek (@machacekondra)" +version_added: "2.3" +description: + - "Retrieve facts about one or more oVirt datacenters." +notes: + - "This module creates a new top-level C(ovirt_datacenters) fact, which + contains a list of datacenters." +options: + pattern: + description: + - "Search term which is accepted by oVirt search backend." + - "For example to search datacenter I(X) use following pattern: I(name=X)" +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Gather facts about all data centers which names start with C(production): +- ovirt_datacenters_facts: + pattern: name=production* +- debug: + var: ovirt_datacenters +''' + +RETURN = ''' +ovirt_datacenters: + description: "List of dictionaries describing the datacenters. Datacenter attribues are mapped to dictionary keys, + all datacenters attributes can be found at following url: https://ovirt.example.com/ovirt-engine/api/model#types/data_center." + returned: On success. + type: list +''' + + +def main(): + argument_spec = ovirt_full_argument_spec( + pattern=dict(default='', required=False), + ) + module = AnsibleModule(argument_spec) + check_sdk(module) + + try: + connection = create_connection(module.params.pop('auth')) + datacenters_service = connection.system_service().data_centers_service() + datacenters = datacenters_service.list(search=module.params['pattern']) + module.exit_json( + changed=False, + ansible_facts=dict( + ovirt_datacenters=[ + get_dict_of_struct(c) for c in datacenters + ], + ), + ) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + + +if __name__ == '__main__': + main() From 4a97aba2170bad190906aa4b93e2cf026faf7e36 Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Mon, 5 Dec 2016 18:32:04 +0100 Subject: [PATCH 2495/2522] Add ovirt_networks and ovirt_networks_facts modules (#3148) --- cloud/ovirt/ovirt_networks.py | 264 ++++++++++++++++++++++++++++ cloud/ovirt/ovirt_networks_facts.py | 100 +++++++++++ 2 files changed, 364 insertions(+) create mode 100644 cloud/ovirt/ovirt_networks.py create mode 100644 cloud/ovirt/ovirt_networks_facts.py diff --git a/cloud/ovirt/ovirt_networks.py b/cloud/ovirt/ovirt_networks.py new file mode 100644 index 00000000000..44b7fefe7af --- /dev/null +++ b/cloud/ovirt/ovirt_networks.py @@ -0,0 +1,264 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +import traceback + +try: + import ovirtsdk4.types as otypes +except ImportError: + pass + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + BaseModule, + check_sdk, + check_params, + create_connection, + equal, + ovirt_full_argument_spec, + search_by_name, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_networks +short_description: Module to manage logical networks in oVirt +version_added: "2.3" +author: "Ondra Machacek (@machacekondra)" +description: + - "Module to manage logical networks in oVirt" +options: + name: + description: + - "Name of the the network to manage." + required: true + state: + description: + - "Should the network be present or absent" + choices: ['present', 'absent'] + default: present + datacenter: + description: + - "Datacenter name where network reside." + description: + description: + - "Description of the network." + comment: + description: + - "Comment of the network." + vlan_tag: + description: + - "Specify VLAN tag." + vm_network: + description: + - "If I(True) network will be marked as network for VM." + - "VM network carries traffic relevant to the virtual machine." + mtu: + description: + - "Maximum transmission unit (MTU) of the network." + clusters: + description: + - "List of dictionaries describing how the network is managed in specific cluster." + - "C(name) - Cluster name." + - "C(assigned) - I(true) if the network should be assigned to cluster. Default is I(true)." + - "C(required) - I(true) if the network must remain operational for all hosts associated with this network." + - "C(display) - I(true) if the network should marked as display network." + - "C(migration) - I(true) if the network should marked as migration network." + - "C(gluster) - I(true) if the network should marked as gluster network." + +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Create network +- ovirt_networks: + datacenter: mydatacenter + name: mynetwork + vlan_tag: 1 + vm_network: true + +# Remove network +- ovirt_networks: + state: absent + name: mynetwork +''' + +RETURN = ''' +id: + description: "ID of the managed network" + returned: "On success if network is found." + type: str + sample: 7de90f31-222c-436c-a1ca-7e655bd5b60c +network: + description: "Dictionary of all the network attributes. Network attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/network." + returned: "On success if network is found." +''' + + +class NetworksModule(BaseModule): + + def build_entity(self): + return otypes.Network( + name=self._module.params['name'], + comment=self._module.params['comment'], + description=self._module.params['description'], + data_center=otypes.DataCenter( + name=self._module.params['datacenter'], + ) if self._module.params['datacenter'] else None, + vlan=otypes.Vlan( + self._module.params['vlan_tag'], + ) if self._module.params['vlan_tag'] else None, + usages=[ + otypes.NetworkUsage.VM if self._module.params['vm_network'] else None + ] if self._module.params['vm_network'] is not None else None, + mtu=self._module.params['mtu'], + ) + + def update_check(self, entity): + return ( + equal(self._module.params.get('comment'), entity.comment) and + equal(self._module.params.get('description'), entity.description) and + equal(self._module.params.get('vlan_tag'), getattr(entity.vlan, 'id', None)) and + equal(self._module.params.get('vm_network'), True if entity.usages else False) and + equal(self._module.params.get('mtu'), entity.mtu) + ) + + +class ClusterNetworksModule(BaseModule): + + def __init__(self, network_id, cluster_network, *args, **kwargs): + super(ClusterNetworksModule, self).__init__(*args, **kwargs) + self._network_id = network_id + self._cluster_network = cluster_network + + def build_entity(self): + return otypes.Network( + id=self._network_id, + name=self._module.params['name'], + required=self._cluster_network.get('required'), + display=self._cluster_network.get('display'), + usages=[ + otypes.NetworkUsage(usage) + for usage in ['display', 'gluster', 'migration'] + if self._cluster_network.get(usage, False) + ] if ( + self._cluster_network.get('display') is not None or + self._cluster_network.get('gluster') is not None or + self._cluster_network.get('migration') is not None + ) else None, + ) + + def update_check(self, entity): + return ( + equal(self._cluster_network.get('required'), entity.required) and + equal(self._cluster_network.get('display'), entity.display) and + equal( + sorted([ + usage + for usage in ['display', 'gluster', 'migration'] + if self._cluster_network.get(usage, False) + ]), + sorted([ + str(usage) + for usage in getattr(entity, 'usages', []) + # VM + MANAGEMENT is part of root network + if usage != otypes.NetworkUsage.VM and usage != otypes.NetworkUsage.MANAGEMENT + ]), + ) + ) + + +def main(): + argument_spec = ovirt_full_argument_spec( + state=dict( + choices=['present', 'absent'], + default='present', + ), + datacenter=dict(default=None, required=True), + name=dict(default=None, required=True), + description=dict(default=None), + comment=dict(default=None), + vlan_tag=dict(default=None, type='int'), + vm_network=dict(default=None, type='bool'), + mtu=dict(default=None, type='int'), + clusters=dict(default=None, type='list'), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + check_sdk(module) + check_params(module) + + try: + connection = create_connection(module.params.pop('auth')) + clusters_service = connection.system_service().clusters_service() + networks_service = connection.system_service().networks_service() + networks_module = NetworksModule( + connection=connection, + module=module, + service=networks_service, + ) + state = module.params['state'] + network = networks_module.search_entity( + search_params={ + 'name': module.params['name'], + 'datacenter': module.params['datacenter'], + }, + ) + if state == 'present': + ret = networks_module.create(entity=network) + + # Update clusters networks: + for param_cluster in module.params.get('clusters', []): + cluster = search_by_name(clusters_service, param_cluster.get('name', None)) + if cluster is None: + raise Exception("Cluster '%s' was not found." % cluster_name) + cluster_networks_service = clusters_service.service(cluster.id).networks_service() + cluster_networks_module = ClusterNetworksModule( + network_id=ret['id'], + cluster_network=param_cluster, + connection=connection, + module=module, + service=cluster_networks_service, + ) + if param_cluster.get('assigned', True): + ret = cluster_networks_module.create() + else: + ret = cluster_networks_module.remove() + + elif state == 'absent': + ret = networks_module.remove(entity=network) + + module.exit_json(**ret) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + + +if __name__ == "__main__": + main() diff --git a/cloud/ovirt/ovirt_networks_facts.py b/cloud/ovirt/ovirt_networks_facts.py new file mode 100644 index 00000000000..9c42244ce4c --- /dev/null +++ b/cloud/ovirt/ovirt_networks_facts.py @@ -0,0 +1,100 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + check_sdk, + create_connection, + get_dict_of_struct, + ovirt_full_argument_spec, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_networks_facts +short_description: Retrieve facts about one or more oVirt networks +author: "Ondra Machacek (@machacekondra)" +version_added: "2.3" +description: + - "Retrieve facts about one or more oVirt networks." +notes: + - "This module creates a new top-level C(ovirt_networks) fact, which + contains a list of networks." +options: + pattern: + description: + - "Search term which is accepted by oVirt search backend." + - "For example to search network starting with string vlan1 use: name=vlan1*" +extends_documentation_fragment: ovirt +''' + + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Gather facts about all networks which names start with C(vlan1): +- ovirt_networks_facts: + pattern: name=vlan1* +- debug: + var: ovirt_networks +''' + + +RETURN = ''' +ovirt_networks: + description: "List of dictionaries describing the networks. Network attribues are mapped to dictionary keys, + all networks attributes can be found at following url: https://ovirt.example.com/ovirt-engine/api/model#types/network." + returned: On success. + type: list +''' + + +def main(): + argument_spec = ovirt_full_argument_spec( + pattern=dict(default='', required=False), + ) + module = AnsibleModule(argument_spec) + check_sdk(module) + + try: + connection = create_connection(module.params.pop('auth')) + networks_service = connection.system_service().networks_service() + networks = networks_service.list(search=module.params['pattern']) + module.exit_json( + changed=False, + ansible_facts=dict( + ovirt_networks=[ + get_dict_of_struct(c) for c in networks + ], + ), + ) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + + +if __name__ == '__main__': + main() From 9b4f14bc9158a01e8414705b12ce5a274cc524f0 Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Mon, 5 Dec 2016 18:32:15 +0100 Subject: [PATCH 2496/2522] Add oVirt users/groups and users_facts/group/facts modules (#3153) --- cloud/ovirt/ovirt_groups.py | 178 ++++++++++++++++++++++++++++++ cloud/ovirt/ovirt_groups_facts.py | 98 ++++++++++++++++ cloud/ovirt/ovirt_users.py | 165 +++++++++++++++++++++++++++ cloud/ovirt/ovirt_users_facts.py | 98 ++++++++++++++++ 4 files changed, 539 insertions(+) create mode 100644 cloud/ovirt/ovirt_groups.py create mode 100644 cloud/ovirt/ovirt_groups_facts.py create mode 100644 cloud/ovirt/ovirt_users.py create mode 100644 cloud/ovirt/ovirt_users_facts.py diff --git a/cloud/ovirt/ovirt_groups.py b/cloud/ovirt/ovirt_groups.py new file mode 100644 index 00000000000..7170bfab3d9 --- /dev/null +++ b/cloud/ovirt/ovirt_groups.py @@ -0,0 +1,178 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +import traceback + +try: + import ovirtsdk4.types as otypes +except ImportError: + pass + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + BaseModule, + check_sdk, + check_params, + create_connection, + equal, + ovirt_full_argument_spec, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_groups +short_description: Module to manage groups in oVirt +version_added: "2.3" +author: "Ondra Machacek (@machacekondra)" +description: + - "Module to manage groups in oVirt" +options: + name: + description: + - "Name of the the group to manage." + required: true + state: + description: + - "Should the group be present/absent." + choices: ['present', 'absent'] + default: present + authz_name: + description: + - "Authorization provider of the group. In previous versions of oVirt known as domain." + required: true + aliases: ['domain'] + namespace: + description: + - "Namespace of the authorization provider, where group resides." + required: false +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Add group group1 from authorization provider example.com-authz +ovirt_groups: + name: group1 + domain: example.com-authz + +# Add group group1 from authorization provider example.com-authz +# In case of multi-domain Active Directory setup, you should pass +# also namespace, so it adds correct group: +ovirt_groups: + name: group1 + namespace: dc=ad2,dc=example,dc=com + domain: example.com-authz + +# Remove group group1 with authorization provider example.com-authz +ovirt_groups: + state: absent + name: group1 + domain: example.com-authz +''' + +RETURN = ''' +id: + description: ID of the group which is managed + returned: On success if group is found. + type: str + sample: 7de90f31-222c-436c-a1ca-7e655bd5b60c +group: + description: "Dictionary of all the group attributes. Group attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/group." + returned: On success if group is found. +''' + + +def _group(connection, module): + groups = connection.system_service().groups_service().list( + search="name={name}".format( + name=module.params['name'], + ) + ) + + # If found more groups, filter them by namespace and authz name: + # (filtering here, as oVirt backend doesn't support it) + if len(groups) > 1: + groups = [ + g for g in groups if ( + equal(module.params['namespace'], g.namespace) and + equal(module.params['authz_name'], g.domain.name) + ) + ] + return groups[0] if groups else None + + +class GroupsModule(BaseModule): + + def build_entity(self): + return otypes.Group( + domain=otypes.Domain( + name=self._module.params['authz_name'] + ), + name=self._module.params['name'], + namespace=self._module.params['namespace'], + ) + + +def main(): + argument_spec = ovirt_full_argument_spec( + state=dict( + choices=['present', 'absent'], + default='present', + ), + name=dict(required=True), + authz_name=dict(required=True, aliases=['domain']), + namespace=dict(default=None), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + check_sdk(module) + check_params(module) + + try: + connection = create_connection(module.params.pop('auth')) + groups_service = connection.system_service().groups_service() + groups_module = GroupsModule( + connection=connection, + module=module, + service=groups_service, + ) + group = _group(connection, module) + state = module.params['state'] + if state == 'present': + ret = groups_module.create(entity=group) + elif state == 'absent': + ret = groups_module.remove(entity=group) + + module.exit_json(**ret) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + + +if __name__ == "__main__": + main() diff --git a/cloud/ovirt/ovirt_groups_facts.py b/cloud/ovirt/ovirt_groups_facts.py new file mode 100644 index 00000000000..1f6ac177124 --- /dev/null +++ b/cloud/ovirt/ovirt_groups_facts.py @@ -0,0 +1,98 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + check_sdk, + create_connection, + get_dict_of_struct, + ovirt_full_argument_spec, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_groups_facts +short_description: Retrieve facts about one or more oVirt groups +author: "Ondra Machacek (@machacekondra)" +version_added: "2.3" +description: + - "Retrieve facts about one or more oVirt groups." +notes: + - "This module creates a new top-level C(ovirt_groups) fact, which + contains a list of groups." +options: + pattern: + description: + - "Search term which is accepted by oVirt search backend." + - "For example to search group X use following pattern: name=X" +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Gather facts about all groups which names start with C(admin): +- ovirt_groups_facts: + pattern: name=admin* +- debug: + var: ovirt_groups +''' + +RETURN = ''' +ovirt_groups: + description: "List of dictionaries describing the groups. Group attribues are mapped to dictionary keys, + all groups attributes can be found at following url: https://ovirt.example.com/ovirt-engine/api/model#types/group." + returned: On success. + type: list +''' + + +def main(): + argument_spec = ovirt_full_argument_spec( + pattern=dict(default='', required=False), + ) + module = AnsibleModule(argument_spec) + check_sdk(module) + + try: + connection = create_connection(module.params.pop('auth')) + groups_service = connection.system_service().groups_service() + groups = groups_service.list(search=module.params['pattern']) + module.exit_json( + changed=False, + ansible_facts=dict( + ovirt_groups=[ + get_dict_of_struct(c) for c in groups + ], + ), + ) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + + +if __name__ == '__main__': + main() diff --git a/cloud/ovirt/ovirt_users.py b/cloud/ovirt/ovirt_users.py new file mode 100644 index 00000000000..3abcefbe96b --- /dev/null +++ b/cloud/ovirt/ovirt_users.py @@ -0,0 +1,165 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +import traceback + +try: + import ovirtsdk4.types as otypes +except ImportError: + pass + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + BaseModule, + check_sdk, + check_params, + create_connection, + ovirt_full_argument_spec, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_users +short_description: Module to manage users in oVirt +version_added: "2.3" +author: "Ondra Machacek (@machacekondra)" +description: + - "Module to manage users in oVirt." +options: + name: + description: + - "Name of the the user to manage. In most LDAPs it's I(uid) of the user, but in Active Directory you must specify I(UPN) of the user." + required: true + state: + description: + - "Should the user be present/absent." + choices: ['present', 'absent'] + default: present + authz_name: + description: + - "Authorization provider of the user. In previous versions of oVirt known as domain." + required: true + aliases: ['domain'] +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Add user user1 from authorization provider example.com-authz +ovirt_users: + name: user1 + domain: example.com-authz + +# Add user user1 from authorization provider example.com-authz +# In case of Active Directory specify UPN: +ovirt_users: + name: user1@ad2.example.com + domain: example.com-authz + +# Remove user user1 with authorization provider example.com-authz +ovirt_users: + state: absent + name: user1 + authz_name: example.com-authz +''' + +RETURN = ''' +id: + description: ID of the user which is managed + returned: On success if user is found. + type: str + sample: 7de90f31-222c-436c-a1ca-7e655bd5b60c +user: + description: "Dictionary of all the user attributes. User attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/user." + returned: On success if user is found. +''' + + +def username(module): + return '{}@{}'.format(module.params['name'], module.params['authz_name']) + + +class UsersModule(BaseModule): + + def build_entity(self): + return otypes.User( + domain=otypes.Domain( + name=self._module.params['authz_name'] + ), + user_name=username(self._module), + principal=self._module.params['name'], + namespace=self._module.params['namespace'], + ) + + +def main(): + argument_spec = ovirt_full_argument_spec( + state=dict( + choices=['present', 'absent'], + default='present', + ), + name=dict(required=True), + authz_name=dict(required=True, aliases=['domain']), + namespace=dict(default=None), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + check_sdk(module) + check_params(module) + + try: + connection = create_connection(module.params.pop('auth')) + users_service = connection.system_service().users_service() + users_module = UsersModule( + connection=connection, + module=module, + service=users_service, + ) + + state = module.params['state'] + if state == 'present': + ret = users_module.create( + search_params={ + 'usrname': username(module), + } + ) + elif state == 'absent': + ret = users_module.remove( + search_params={ + 'usrname': username(module), + } + ) + + module.exit_json(**ret) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + + +if __name__ == "__main__": + main() diff --git a/cloud/ovirt/ovirt_users_facts.py b/cloud/ovirt/ovirt_users_facts.py new file mode 100644 index 00000000000..11f69f811e1 --- /dev/null +++ b/cloud/ovirt/ovirt_users_facts.py @@ -0,0 +1,98 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + check_sdk, + create_connection, + get_dict_of_struct, + ovirt_full_argument_spec, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_users_facts +short_description: Retrieve facts about one or more oVirt users +author: "Ondra Machacek (@machacekondra)" +version_added: "2.3" +description: + - "Retrieve facts about one or more oVirt users." +notes: + - "This module creates a new top-level C(ovirt_users) fact, which + contains a list of users." +options: + pattern: + description: + - "Search term which is accepted by oVirt search backend." + - "For example to search user X use following pattern: name=X" +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Gather facts about all users which first names start with C(john): +- ovirt_users_facts: + pattern: name=john* +- debug: + var: ovirt_users +''' + +RETURN = ''' +ovirt_users: + description: "List of dictionaries describing the users. User attribues are mapped to dictionary keys, + all users attributes can be found at following url: https://ovirt.example.com/ovirt-engine/api/model#types/user." + returned: On success. + type: list +''' + + +def main(): + argument_spec = ovirt_full_argument_spec( + pattern=dict(default='', required=False), + ) + module = AnsibleModule(argument_spec) + check_sdk(module) + + try: + connection = create_connection(module.params.pop('auth')) + users_service = connection.system_service().users_service() + users = users_service.list(search=module.params['pattern']) + module.exit_json( + changed=False, + ansible_facts=dict( + ovirt_users=[ + get_dict_of_struct(c) for c in users + ], + ), + ) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + + +if __name__ == '__main__': + main() From 59e5678b02e713c0b504311320c2dec33a4c4531 Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Mon, 5 Dec 2016 18:33:51 +0100 Subject: [PATCH 2497/2522] Add oVirt ovirt_affinity_labels and ovirt_affinity_labels_facts modules (#3570) --- cloud/ovirt/ovirt_affinity_labels.py | 203 +++++++++++++++++++++ cloud/ovirt/ovirt_affinity_labels_facts.py | 154 ++++++++++++++++ 2 files changed, 357 insertions(+) create mode 100644 cloud/ovirt/ovirt_affinity_labels.py create mode 100644 cloud/ovirt/ovirt_affinity_labels_facts.py diff --git a/cloud/ovirt/ovirt_affinity_labels.py b/cloud/ovirt/ovirt_affinity_labels.py new file mode 100644 index 00000000000..3e79b1a3e69 --- /dev/null +++ b/cloud/ovirt/ovirt_affinity_labels.py @@ -0,0 +1,203 @@ +#!/usr/bin/pythonapi/ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +import traceback + +try: + import ovirtsdk4.types as otypes +except ImportError: + pass + +from collections import defaultdict +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + BaseModule, + check_sdk, + create_connection, + ovirt_full_argument_spec, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_affinity_labels +short_description: Module to affinity labels in oVirt +version_added: "2.3" +author: "Ondra Machacek (@machacekondra)" +description: + - "This module manage affinity labels in oVirt. It can also manage assignments + of those labels to hosts and VMs." +options: + name: + description: + - "Name of the the affinity label to manage." + required: true + state: + description: + - "Should the affinity label be present or absent." + choices: ['present', 'absent'] + default: present + cluster: + description: + - "Name of the cluster where vms and hosts resides." + vms: + description: + - "List of the VMs names, which should have assigned this affinity label." + hosts: + description: + - "List of the hosts names, which should have assigned this affinity label." +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Create(if not exists) and assign affinity label to vms vm1 and vm2 and host host1 +- ovirt_affinity_labels: + name: mylabel + cluster: mycluster + vms: + - vm1 + - vm2 + hosts: + - host1 + +# To detach all VMs from label +- ovirt_affinity_labels: + name: mylabel + cluster: mycluster + vms: [] + +# Remove affinity label +- ovirt_affinity_labels: + state: absent + name: mylabel +''' + +RETURN = ''' +id: + description: ID of the affinity label which is managed + returned: On success if affinity label is found. + type: str + sample: 7de90f31-222c-436c-a1ca-7e655bd5b60c +template: + description: "Dictionary of all the affinity label attributes. Affinity label attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/affinity_label." + returned: On success if affinity label is found. +''' + + +class AffinityLabelsModule(BaseModule): + + def build_entity(self): + return otypes.AffinityLabel(name=self._module.params['name']) + + def post_create(self, entity): + self.update_check(entity) + + def pre_remove(self, entity): + self._module.params['vms'] = [] + self._module.params['hosts'] = [] + self.update_check(entity) + + def _update_label_assignments(self, entity, name, label_obj_type): + objs_service = getattr(self._connection.system_service(), '%s_service' % name)() + if self._module.params[name] is not None: + objs = self._connection.follow_link(getattr(entity, name)) + objs_names = defaultdict(list) + for obj in objs: + labeled_entity = objs_service.service(obj.id).get() + if self._module.params['cluster'] is None: + objs_names[labeled_entity.name].append(obj.id) + elif self._connection.follow_link(labeled_entity.cluster).name == self._module.params['cluster']: + objs_names[labeled_entity.name].append(obj.id) + + for obj in self._module.params[name]: + if obj not in objs_names: + for obj_id in objs_service.list( + search='name=%s and cluster=%s' % (obj, self._module.params['cluster']) + ): + label_service = getattr(self._service.service(entity.id), '%s_service' % name)() + if not self._module.check_mode: + label_service.add(**{ + name[:-1]: label_obj_type(id=obj_id.id) + }) + self.changed = True + + for obj in objs_names: + if obj not in self._module.params[name]: + label_service = getattr(self._service.service(entity.id), '%s_service' % name)() + if not self._module.check_mode: + for obj_id in objs_names[obj]: + label_service.service(obj_id).remove() + self.changed = True + + def update_check(self, entity): + self._update_label_assignments(entity, 'vms', otypes.Vm) + self._update_label_assignments(entity, 'hosts', otypes.Host) + return True + + +def main(): + argument_spec = ovirt_full_argument_spec( + state=dict( + choices=['present', 'absent'], + default='present', + ), + cluster=dict(default=None), + name=dict(default=None, required=True), + vms=dict(default=None, type='list'), + hosts=dict(default=None, type='list'), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ('state', 'present', ['cluster']), + ], + ) + check_sdk(module) + + try: + connection = create_connection(module.params.pop('auth')) + affinity_labels_service = connection.system_service().affinity_labels_service() + affinity_labels_module = AffinityLabelsModule( + connection=connection, + module=module, + service=affinity_labels_service, + ) + + state = module.params['state'] + if state == 'present': + ret = affinity_labels_module.create() + elif state == 'absent': + ret = affinity_labels_module.remove() + + module.exit_json(**ret) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + + +if __name__ == "__main__": + main() diff --git a/cloud/ovirt/ovirt_affinity_labels_facts.py b/cloud/ovirt/ovirt_affinity_labels_facts.py new file mode 100644 index 00000000000..9d13c4cb3d6 --- /dev/null +++ b/cloud/ovirt/ovirt_affinity_labels_facts.py @@ -0,0 +1,154 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +import fnmatch +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + check_sdk, + create_connection, + get_dict_of_struct, + ovirt_full_argument_spec, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_affinity_labels_facts +short_description: Retrieve facts about one or more oVirt affinity labels +author: "Ondra Machacek (@machacekondra)" +version_added: "2.3" +description: + - "Retrieve facts about one or more oVirt affinity labels." +notes: + - "This module creates a new top-level C(affinity_labels) fact, which + contains a list of affinity labels." +options: + name: + description: + - "Name of the affinity labels which should be listed." + vm: + description: + - "Name of the VM, which affinity labels should be listed." + host: + description: + - "Name of the host, which affinity labels should be listed." +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Gather facts about all affinity labels, which names start with C(label): +- ovirt_affinity_labels_facts: + name: label* +- debug: + var: affinity_labels + +# Gather facts about all affinity labels, which are assigned to VMs +# which names start with C(postgres): +- ovirt_affinity_labels_facts: + vm: postgres* +- debug: + var: affinity_labels + +# Gather facts about all affinity labels, which are assigned to hosts +# which names start with C(west): +- ovirt_affinity_labels_facts: + host: west* +- debug: + var: affinity_labels + +# Gather facts about all affinity labels, which are assigned to hosts +# which names start with C(west) or VMs which names start with C(postgres): +- ovirt_affinity_labels_facts: + host: west* + vm: postgres* +- debug: + var: affinity_labels +''' + +RETURN = ''' +ovirt_vms: + description: "List of dictionaries describing the affinity labels. Affinity labels attribues are mapped to dictionary keys, + all affinity labels attributes can be found at following url: https://ovirt.example.com/ovirt-engine/api/model#types/affinity_label." + returned: On success. + type: list +''' + + +def main(): + argument_spec = ovirt_full_argument_spec( + name=dict(default=None), + host=dict(default=None), + vm=dict(default=None), + ) + module = AnsibleModule(argument_spec) + check_sdk(module) + + try: + connection = create_connection(module.params.pop('auth')) + affinity_labels_service = connection.system_service().affinity_labels_service() + labels = [] + all_labels = affinity_labels_service.list() + if module.params['name']: + labels.extend([ + l for l in all_labels + if fnmatch.fnmatch(l.name, module.params['name']) + ]) + if module.params['host']: + hosts_service = connection.system_service().hosts_service() + labels.extend([ + label + for label in all_labels + for host in connection.follow_link(label.hosts) + if fnmatch.fnmatch(hosts_service.service(host.id).get().name, module.params['host']) + ]) + if module.params['vm']: + vms_service = connection.system_service().vms_service() + labels.extend([ + label + for label in all_labels + for vm in connection.follow_link(label.vms) + if fnmatch.fnmatch(vms_service.service(vm.id).get().name, module.params['vm']) + ]) + + if not (module.params['vm'] or module.params['host'] or module.params['name']): + labels = all_labels + + module.exit_json( + changed=False, + ansible_facts=dict( + affinity_labels=[ + get_dict_of_struct(l) for l in labels + ], + ), + ) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + + +if __name__ == '__main__': + main() From a45ae4e1011e08583cc2b80b2f17cf608d059d20 Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Mon, 5 Dec 2016 18:34:12 +0100 Subject: [PATCH 2498/2522] ovirt_vms: Add new cloud_init_nics parameter (#3557) --- cloud/ovirt/ovirt_vms.py | 81 ++++++++++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 23 deletions(-) diff --git a/cloud/ovirt/ovirt_vms.py b/cloud/ovirt/ovirt_vms.py index 29cb2695cb9..0c1ea05388f 100644 --- a/cloud/ovirt/ovirt_vms.py +++ b/cloud/ovirt/ovirt_vms.py @@ -195,12 +195,26 @@ - "C(custom_script) - Cloud-init script which will be executed on Virtual Machine when deployed." - "C(dns_servers) - DNS servers to be configured on Virtual Machine." - "C(dns_search) - DNS search domains to be configured on Virtual Machine." - - "C(nic_boot_protocol) - Set boot protocol of the network interface of Virtual Machine. Can be one of None, DHCP or Static." + - "C(nic_boot_protocol) - Set boot protocol of the network interface of Virtual Machine. Can be one of none, dhcp or static." - "C(nic_ip_address) - If boot protocol is static, set this IP address to network interface of Virtual Machine." - "C(nic_netmask) - If boot protocol is static, set this netmask to network interface of Virtual Machine." - "C(nic_gateway) - If boot protocol is static, set this gateway to network interface of Virtual Machine." - "C(nic_name) - Set name to network interface of Virtual Machine." - "C(nic_on_boot) - If I(True) network interface will be set to start on boot." + cloud_init_nics: + description: + - "List of dictionaries representing network interafaces to be setup by cloud init." + - "This option is used, when user needs to setup more network interfaces via cloud init." + - "If one network interface is enough, user should use C(cloud_init) I(nic_*) parameters. C(cloud_init) I(nic_*) parameters + are merged with C(cloud_init_nics) parameters." + - "Dictionary can contain following values:" + - "C(nic_boot_protocol) - Set boot protocol of the network interface of Virtual Machine. Can be one of none, dhcp or static." + - "C(nic_ip_address) - If boot protocol is static, set this IP address to network interface of Virtual Machine." + - "C(nic_netmask) - If boot protocol is static, set this netmask to network interface of Virtual Machine." + - "C(nic_gateway) - If boot protocol is static, set this gateway to network interface of Virtual Machine." + - "C(nic_name) - Set name to network interface of Virtual Machine." + - "C(nic_on_boot) - If I(True) network interface will be set to start on boot." + version_added: "2.3" notes: - "If VM is in I(UNASSIGNED) or I(UNKNOWN) state before any operation, the module will fail. If VM is in I(IMAGE_LOCKED) state before any operation, we try to wait for VM to be I(DOWN). @@ -281,6 +295,22 @@ user_name: root root_password: super_password +# Run VM with cloud init, with multiple network interfaces: +ovirt_vms: + name: rhel7_4 + template: rhel7 + cluster: mycluster + cloud_init_nics: + - nic_name: eth0 + nic_boot_protocol: dhcp + nic_on_boot: true + - nic_name: eth1 + nic_boot_protocol: static + nic_ip_address: 10.34.60.86 + nic_netmask: 255.255.252.0 + nic_gateway: 10.34.63.254 + nic_on_boot: true + # Run VM with sysprep: ovirt_vms: name: windows2012R2_AD @@ -609,34 +639,36 @@ def __attach_nics(self, entity): self.changed = True -def _get_initialization(sysprep, cloud_init): +def _get_initialization(sysprep, cloud_init, cloud_init_nics): initialization = None - if cloud_init: + if cloud_init or cloud_init_nics: initialization = otypes.Initialization( nic_configurations=[ otypes.NicConfiguration( boot_protocol=otypes.BootProtocol( - cloud_init.pop('nic_boot_protocol').lower() - ) if cloud_init.get('nic_boot_protocol') else None, - name=cloud_init.pop('nic_name'), - on_boot=cloud_init.pop('nic_on_boot'), + nic.pop('nic_boot_protocol').lower() + ) if nic.get('nic_boot_protocol') else None, + name=nic.pop('nic_name', None), + on_boot=nic.pop('nic_on_boot', None), ip=otypes.Ip( - address=cloud_init.pop('nic_ip_address'), - netmask=cloud_init.pop('nic_netmask'), - gateway=cloud_init.pop('nic_gateway'), + address=nic.pop('nic_ip_address', None), + netmask=nic.pop('nic_netmask', None), + gateway=nic.pop('nic_gateway', None), ) if ( - cloud_init.get('nic_gateway') is not None or - cloud_init.get('nic_netmask') is not None or - cloud_init.get('nic_ip_address') is not None + nic.get('nic_gateway') is not None or + nic.get('nic_netmask') is not None or + nic.get('nic_ip_address') is not None ) else None, ) - ] if ( - cloud_init.get('nic_gateway') is not None or - cloud_init.get('nic_netmask') is not None or - cloud_init.get('nic_ip_address') is not None or - cloud_init.get('nic_boot_protocol') is not None or - cloud_init.get('nic_on_boot') is not None - ) else None, + for nic in cloud_init_nics + if ( + nic.get('nic_gateway') is not None or + nic.get('nic_netmask') is not None or + nic.get('nic_ip_address') is not None or + nic.get('nic_boot_protocol') is not None or + nic.get('nic_on_boot') is not None + ) + ] if cloud_init_nics else None, **cloud_init ) elif sysprep: @@ -730,6 +762,7 @@ def main(): force=dict(type='bool', default=False), nics=dict(default=[], type='list'), cloud_init=dict(type='dict'), + cloud_init_nics=dict(defaul=[], type='list'), sysprep=dict(type='dict'), host=dict(default=None), clone=dict(type='bool', default=False), @@ -755,8 +788,10 @@ def main(): control_state(vm, vms_service, module) if state == 'present' or state == 'running' or state == 'next_run': - cloud_init = module.params['cloud_init'] sysprep = module.params['sysprep'] + cloud_init = module.params['cloud_init'] + cloud_init_nics = module.params['cloud_init_nics'] + cloud_init_nics.append(cloud_init) # In case VM don't exist, wait for VM DOWN state, # otherwise don't wait for any state, just update VM: @@ -781,13 +816,13 @@ def main(): ), wait_condition=lambda vm: vm.status == otypes.VmStatus.UP, # Start action kwargs: - use_cloud_init=cloud_init is not None, + use_cloud_init=cloud_init is not None or len(cloud_init_nics) > 0, use_sysprep=sysprep is not None, vm=otypes.Vm( placement_policy=otypes.VmPlacementPolicy( hosts=[otypes.Host(name=module.params['host'])] ) if module.params['host'] else None, - initialization=_get_initialization(sysprep, cloud_init), + initialization=_get_initialization(sysprep, cloud_init, cloud_init_nics), ), ) From 90e0347e9d3684692797de86323808f899f95353 Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Mon, 5 Dec 2016 18:34:22 +0100 Subject: [PATCH 2499/2522] Add oVirt ovirt_vms_facts module (#3226) --- cloud/ovirt/ovirt_vms_facts.py | 100 +++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 cloud/ovirt/ovirt_vms_facts.py diff --git a/cloud/ovirt/ovirt_vms_facts.py b/cloud/ovirt/ovirt_vms_facts.py new file mode 100644 index 00000000000..ae287c82068 --- /dev/null +++ b/cloud/ovirt/ovirt_vms_facts.py @@ -0,0 +1,100 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + check_sdk, + create_connection, + get_dict_of_struct, + ovirt_full_argument_spec, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_vms_facts +short_description: Retrieve facts about one or more oVirt virtual machines +author: "Ondra Machacek (@machacekondra)" +version_added: "2.3" +description: + - "Retrieve facts about one or more oVirt virtual machines." +notes: + - "This module creates a new top-level C(ovirt_vms) fact, which + contains a list of virtual machines." +options: + pattern: + description: + - "Search term which is accepted by oVirt search backend." + - "For example to search VM X from cluster Y use following pattern: + name=X and cluster=Y" +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Gather facts about all VMs which names start with C(centos) and +# belong to cluster C(west): +- ovirt_vms_facts: + pattern: name=centos* and cluster=west +- debug: + var: ovirt_vms +''' + +RETURN = ''' +ovirt_vms: + description: "List of dictionaries describing the VMs. VM attribues are mapped to dictionary keys, + all VMs attributes can be found at following url: https://ovirt.example.com/ovirt-engine/api/model#types/vm." + returned: On success. + type: list +''' + + +def main(): + argument_spec = ovirt_full_argument_spec( + pattern=dict(default='', required=False), + ) + module = AnsibleModule(argument_spec) + check_sdk(module) + + try: + connection = create_connection(module.params.pop('auth')) + vms_service = connection.system_service().vms_service() + vms = vms_service.list(search=module.params['pattern']) + module.exit_json( + changed=False, + ansible_facts=dict( + ovirt_vms=[ + get_dict_of_struct(c) for c in vms + ], + ), + ) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + + +if __name__ == '__main__': + main() From c32270d503e8c7cd5adf5f75598c99f7ad2dd8ed Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Mon, 5 Dec 2016 18:35:13 +0100 Subject: [PATCH 2500/2522] Add oVirt ovirt_storage_domains and ovirt_storage_domains_facts modules (#3222) --- cloud/ovirt/ovirt_storage_domains.py | 440 +++++++++++++++++++++ cloud/ovirt/ovirt_storage_domains_facts.py | 100 +++++ 2 files changed, 540 insertions(+) create mode 100644 cloud/ovirt/ovirt_storage_domains.py create mode 100644 cloud/ovirt/ovirt_storage_domains_facts.py diff --git a/cloud/ovirt/ovirt_storage_domains.py b/cloud/ovirt/ovirt_storage_domains.py new file mode 100644 index 00000000000..77e53bb710f --- /dev/null +++ b/cloud/ovirt/ovirt_storage_domains.py @@ -0,0 +1,440 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +try: + import ovirtsdk4.types as otypes + + from ovirtsdk4.types import StorageDomainStatus as sdstate +except ImportError: + pass + +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + BaseModule, + check_sdk, + create_connection, + ovirt_full_argument_spec, + search_by_name, + wait, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_storage_domains +short_description: Module to manage storage domains in oVirt +version_added: "2.3" +author: "Ondra Machacek (@machacekondra)" +description: + - "Module to manage storage domains in oVirt" +options: + name: + description: + - "Name of the the storage domain to manage." + state: + description: + - "Should the storage domain be present/absent/maintenance/unattached" + choices: ['present', 'absent', 'maintenance', 'unattached'] + default: present + description: + description: + - "Description of the storage domain." + comment: + description: + - "Comment of the storage domain." + data_center: + description: + - "Data center name where storage domain should be attached." + domain_function: + description: + - "Function of the storage domain." + choices: ['data', 'iso', 'export'] + default: 'data' + aliases: ['type'] + host: + description: + - "Host to be used to mount storage." + nfs: + description: + - "Dictionary with values for NFS storage type:" + - "C(address) - Address of the NFS server. E.g.: myserver.mydomain.com" + - "C(path) - Path of the mount point. E.g.: /path/to/my/data" + iscsi: + description: + - "Dictionary with values for iSCSI storage type:" + - "C(address) - Address of the iSCSI storage server." + - "C(port) - Port of the iSCSI storage server." + - "C(target) - iSCSI target." + - "C(lun_id) - LUN id." + - "C(username) - Username to be used to access storage server." + - "C(password) - Password of the user to be used to access storage server." + posixfs: + description: + - "Dictionary with values for PosixFS storage type:" + - "C(path) - Path of the mount point. E.g.: /path/to/my/data" + - "C(vfs_type) - Virtual File System type." + - "C(mount_options) - Option which will be passed when mounting storage." + glusterfs: + description: + - "Dictionary with values for GlusterFS storage type:" + - "C(address) - Address of the NFS server. E.g.: myserver.mydomain.com" + - "C(path) - Path of the mount point. E.g.: /path/to/my/data" + - "C(mount_options) - Option which will be passed when mounting storage." + fcp: + description: + - "Dictionary with values for fibre channel storage type:" + - "C(address) - Address of the fibre channel storage server." + - "C(port) - Port of the fibre channel storage server." + - "C(lun_id) - LUN id." + destroy: + description: + - "If I(True) storage domain metadata won't be cleaned, and user have to clean them manually." + - "This parameter is relevant only when C(state) is I(absent)." + format: + description: + - "If I(True) storage domain will be removed after removing it from oVirt." + - "This parameter is relevant only when C(state) is I(absent)." +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Add data NFS storage domain +- ovirt_storage_domains: + name: data_nfs + host: myhost + data_center: mydatacenter + nfs: + address: 10.34.63.199 + path: /path/data + +# Add data iSCSI storage domain: +- ovirt_storage_domains: + name: data_iscsi + host: myhost + data_center: mydatacenter + iscsi: + target: iqn.2016-08-09.domain-01:nickname + lun_id: 1IET_000d0002 + address: 10.34.63.204 + +# Import export NFS storage domain: +- ovirt_storage_domains: + domain_function: export + host: myhost + data_center: mydatacenter + nfs: + address: 10.34.63.199 + path: /path/export + +# Create ISO NFS storage domain +- ovirt_storage_domains: + name: myiso + domain_function: iso + host: myhost + data_center: mydatacenter + nfs: + address: 10.34.63.199 + path: /path/iso + +# Remove storage domain +- ovirt_storage_domains: + state: absent + name: mystorage_domain + format: true +''' + +RETURN = ''' +id: + description: ID of the storage domain which is managed + returned: On success if storage domain is found. + type: str + sample: 7de90f31-222c-436c-a1ca-7e655bd5b60c +storage domain: + description: "Dictionary of all the storage domain attributes. Storage domain attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/storage_domain." + returned: On success if storage domain is found. +''' + + +class StorageDomainModule(BaseModule): + + def _get_storage_type(self): + for sd_type in ['nfs', 'iscsi', 'posixfs', 'glusterfs', 'fcp']: + if self._module.params.get(sd_type) is not None: + return sd_type + + def _get_storage(self): + for sd_type in ['nfs', 'iscsi', 'posixfs', 'glusterfs', 'fcp']: + if self._module.params.get(sd_type) is not None: + return self._module.params.get(sd_type) + + def _login(self, storage_type, storage): + if storage_type == 'iscsi': + hosts_service = self._connection.system_service().hosts_service() + host = search_by_name(hosts_service, self._module.params['host']) + hosts_service.host_service(host.id).iscsi_login( + iscsi=otypes.IscsiDetails( + username=storage.get('username'), + password=storage.get('password'), + address=storage.get('address'), + target=storage.get('target'), + ), + ) + + def build_entity(self): + storage_type = self._get_storage_type() + storage = self._get_storage() + self._login(storage_type, storage) + + return otypes.StorageDomain( + name=self._module.params['name'], + description=self._module.params['description'], + comment=self._module.params['comment'], + type=otypes.StorageDomainType( + self._module.params['domain_function'] + ), + host=otypes.Host( + name=self._module.params['host'], + ), + storage=otypes.HostStorage( + type=otypes.StorageType(storage_type), + logical_units=[ + otypes.LogicalUnit( + id=storage.get('lun_id'), + address=storage.get('address'), + port=storage.get('port', 3260), + target=storage.get('target'), + username=storage.get('username'), + password=storage.get('password'), + ), + ] if storage_type in ['iscsi', 'fcp'] else None, + mount_options=storage.get('mount_options'), + vfs_type=storage.get('vfs_type'), + address=storage.get('address'), + path=storage.get('path'), + ) + ) + + def _attached_sds_service(self): + # Get data center object of the storage domain: + dcs_service = self._connection.system_service().data_centers_service() + dc = search_by_name(dcs_service, self._module.params['data_center']) + if dc is None: + return + + dc_service = dcs_service.data_center_service(dc.id) + return dc_service.storage_domains_service() + + def _maintenance(self, storage_domain): + attached_sds_service = self._attached_sds_service() + if attached_sds_service is None: + return + + attached_sd_service = attached_sds_service.storage_domain_service(storage_domain.id) + attached_sd = attached_sd_service.get() + + if attached_sd and attached_sd.status != sdstate.MAINTENANCE: + if not self._module.check_mode: + attached_sd_service.deactivate() + self.changed = True + + wait( + service=attached_sd_service, + condition=lambda sd: sd.status == sdstate.MAINTENANCE, + wait=self._module.params['wait'], + timeout=self._module.params['timeout'], + ) + + def _unattach(self, storage_domain): + attached_sds_service = self._attached_sds_service() + if attached_sds_service is None: + return + + attached_sd_service = attached_sds_service.storage_domain_service(storage_domain.id) + attached_sd = attached_sd_service.get() + + if attached_sd and attached_sd.status == sdstate.MAINTENANCE: + if not self._module.check_mode: + # Detach the storage domain: + attached_sd_service.remove() + self.changed = True + # Wait until storage domain is detached: + wait( + service=attached_sd_service, + condition=lambda sd: sd is None, + wait=self._module.params['wait'], + timeout=self._module.params['timeout'], + ) + + def pre_remove(self, storage_domain): + # Before removing storage domain we need to put it into maintenance state: + self._maintenance(storage_domain) + + # Before removing storage domain we need to detach it from data center: + self._unattach(storage_domain) + + def post_create_check(self, sd_id): + storage_domain = self._service.service(sd_id).get() + self._service = self._attached_sds_service() + + # If storage domain isn't attached, attach it: + attached_sd_service = self._service.service(storage_domain.id) + if attached_sd_service.get() is None: + self._service.add( + otypes.StorageDomain( + id=storage_domain.id, + ), + ) + self.changed = True + # Wait until storage domain is in maintenance: + wait( + service=attached_sd_service, + condition=lambda sd: sd.status == sdstate.ACTIVE, + wait=self._module.params['wait'], + timeout=self._module.params['timeout'], + ) + + def unattached_pre_action(self, storage_domain): + self._service = self._attached_sds_service(storage_domain) + self._maintenance(self._service, storage_domain) + + +def failed_state(sd): + return sd.status in [sdstate.UNKNOWN, sdstate.INACTIVE] + + +def control_state(sd_module): + sd = sd_module.search_entity() + if sd is None: + return + + sd_service = sd_module._service.service(sd.id) + if sd.status == sdstate.LOCKED: + wait( + service=sd_service, + condition=lambda sd: sd.status != sdstate.LOCKED, + fail_condition=failed_state, + ) + + if failed_state(sd): + raise Exception("Not possible to manage storage domain '%s'." % sd.name) + elif sd.status == sdstate.ACTIVATING: + wait( + service=sd_service, + condition=lambda sd: sd.status == sdstate.ACTIVE, + fail_condition=failed_state, + ) + elif sd.status == sdstate.DETACHING: + wait( + service=sd_service, + condition=lambda sd: sd.status == sdstate.UNATTACHED, + fail_condition=failed_state, + ) + elif sd.status == sdstate.PREPARING_FOR_MAINTENANCE: + wait( + service=sd_service, + condition=lambda sd: sd.status == sdstate.MAINTENANCE, + fail_condition=failed_state, + ) + + +def main(): + argument_spec = ovirt_full_argument_spec( + state=dict( + choices=['present', 'absent', 'maintenance', 'unattached'], + default='present', + ), + name=dict(required=True), + description=dict(default=None), + comment=dict(default=None), + data_center=dict(required=True), + domain_function=dict(choices=['data', 'iso', 'export'], default='data', aliases=['type']), + host=dict(default=None), + nfs=dict(default=None, type='dict'), + iscsi=dict(default=None, type='dict'), + posixfs=dict(default=None, type='dict'), + glusterfs=dict(default=None, type='dict'), + fcp=dict(default=None, type='dict'), + destroy=dict(type='bool', default=False), + format=dict(type='bool', default=False), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + check_sdk(module) + + try: + connection = create_connection(module.params.pop('auth')) + storage_domains_service = connection.system_service().storage_domains_service() + storage_domains_module = StorageDomainModule( + connection=connection, + module=module, + service=storage_domains_service, + ) + + state = module.params['state'] + control_state(storage_domains_module) + if state == 'absent': + ret = storage_domains_module.remove( + destroy=module.params['destroy'], + format=module.params['format'], + host=module.params['host'], + ) + elif state == 'present': + sd_id = storage_domains_module.create()['id'] + storage_domains_module.post_create_check(sd_id) + ret = storage_domains_module.action( + action='activate', + action_condition=lambda s: s.status == sdstate.MAINTENANCE, + wait_condition=lambda s: s.status == sdstate.ACTIVE, + fail_condition=failed_state, + ) + elif state == 'maintenance': + sd_id = storage_domains_module.create()['id'] + storage_domains_module.post_create_check(sd_id) + ret = storage_domains_module.action( + action='deactivate', + action_condition=lambda s: s.status == sdstate.ACTIVE, + wait_condition=lambda s: s.status == sdstate.MAINTENANCE, + fail_condition=failed_state, + ) + elif state == 'unattached': + ret = storage_domains_module.create() + storage_domains_module.pre_remove( + storage_domain=storage_domains_service.service(ret['id']).get() + ) + ret['changed'] = storage_domains_module.changed + + module.exit_json(**ret) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + + +if __name__ == "__main__": + main() diff --git a/cloud/ovirt/ovirt_storage_domains_facts.py b/cloud/ovirt/ovirt_storage_domains_facts.py new file mode 100644 index 00000000000..121ca1ae99a --- /dev/null +++ b/cloud/ovirt/ovirt_storage_domains_facts.py @@ -0,0 +1,100 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + check_sdk, + create_connection, + get_dict_of_struct, + ovirt_full_argument_spec, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_storage_domains_facts +short_description: Retrieve facts about one or more oVirt storage domains +author: "Ondra Machacek (@machacekondra)" +version_added: "2.3" +description: + - "Retrieve facts about one or more oVirt storage domains." +notes: + - "This module creates a new top-level C(ovirt_storage_domains) fact, which + contains a list of storage domains." +options: + pattern: + description: + - "Search term which is accepted by oVirt search backend." + - "For example to search storage domain X from datacenter Y use following pattern: + name=X and datacenter=Y" +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Gather facts about all storage domains which names start with C(data) and +# belong to data center C(west): +- ovirt_storage_domains_facts: + pattern: name=data* and datacenter=west +- debug: + var: ovirt_storage_domains +''' + +RETURN = ''' +ovirt_storage_domains: + description: "List of dictionaries describing the storage domains. Storage_domain attribues are mapped to dictionary keys, + all storage domains attributes can be found at following url: https://ovirt.example.com/ovirt-engine/api/model#types/storage_domain." + returned: On success. + type: list +''' + + +def main(): + argument_spec = ovirt_full_argument_spec( + pattern=dict(default='', required=False), + ) + module = AnsibleModule(argument_spec) + check_sdk(module) + + try: + connection = create_connection(module.params.pop('auth')) + storage_domains_service = connection.system_service().storage_domains_service() + storage_domains = storage_domains_service.list(search=module.params['pattern']) + module.exit_json( + changed=False, + ansible_facts=dict( + ovirt_storage_domains=[ + get_dict_of_struct(c) for c in storage_domains + ], + ), + ) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + + +if __name__ == '__main__': + main() From 63bf4f504bf0063510fa549870e67a5a86d604e5 Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Mon, 5 Dec 2016 18:37:14 +0100 Subject: [PATCH 2501/2522] Add oVirt ovirt_templates and ovirt_templates_facts modules (#3221) --- cloud/ovirt/ovirt_templates.py | 310 +++++++++++++++++++++++++++ cloud/ovirt/ovirt_templates_facts.py | 100 +++++++++ 2 files changed, 410 insertions(+) create mode 100644 cloud/ovirt/ovirt_templates.py create mode 100644 cloud/ovirt/ovirt_templates_facts.py diff --git a/cloud/ovirt/ovirt_templates.py b/cloud/ovirt/ovirt_templates.py new file mode 100644 index 00000000000..ca083af6b90 --- /dev/null +++ b/cloud/ovirt/ovirt_templates.py @@ -0,0 +1,310 @@ +#!/usr/bin/pythonapi/ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +import time +import traceback + +try: + import ovirtsdk4.types as otypes +except ImportError: + pass + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + BaseModule, + check_sdk, + create_connection, + equal, + get_dict_of_struct, + get_link_name, + ovirt_full_argument_spec, + search_by_attributes, + search_by_name, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_templates +short_description: Module to manage virtual machine templates in oVirt +version_added: "2.3" +author: "Ondra Machacek (@machacekondra)" +description: + - "Module to manage virtual machine templates in oVirt." +options: + name: + description: + - "Name of the the template to manage." + required: true + state: + description: + - "Should the template be present/absent/exported/imported" + choices: ['present', 'absent', 'exported', 'imported'] + default: present + vm: + description: + - "Name of the VM, which will be used to create template." + description: + description: + - "Description of the template." + cpu_profile: + description: + - "CPU profile to be set to template." + cluster: + description: + - "Name of the cluster, where template should be created/imported." + exclusive: + description: + - "When C(state) is I(exported) this parameter indicates if the existing templates with the + same name should be overwritten." + export_domain: + description: + - "When C(state) is I(exported) or I(imported) this parameter specifies the name of the + export storage domain." + image_provider: + description: + - "When C(state) is I(imported) this parameter specifies the name of the image provider to be used." + image_disk: + description: + - "When C(state) is I(imported) and C(image_provider) is used this parameter specifies the name of disk + to be imported as template." + storage_domain: + description: + - "When C(state) is I(imported) this parameter specifies the name of the destination data storage domain." + clone_permissions: + description: + - "If I(True) then the permissions of the VM (only the direct ones, not the inherited ones) + will be copied to the created template." + - "This parameter is used only when C(state) I(present)." + default: False +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Create template from vm +- ovirt_templates: + cluster: Default + name: mytemplate + vm: rhel7 + cpu_profile: Default + description: Test + +# Import template +- ovirt_templates: + state: imported + name: mytemplate + export_domain: myexport + storage_domain: mystorage + cluster: mycluster + +# Remove template +- ovirt_templates: + state: absent + name: mytemplate +''' + +RETURN = ''' +id: + description: ID of the template which is managed + returned: On success if template is found. + type: str + sample: 7de90f31-222c-436c-a1ca-7e655bd5b60c +template: + description: "Dictionary of all the template attributes. Template attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/template." + returned: On success if template is found. +''' + + +class TemplatesModule(BaseModule): + + def build_entity(self): + return otypes.Template( + name=self._module.params['name'], + cluster=otypes.Cluster( + name=self._module.params['cluster'] + ) if self._module.params['cluster'] else None, + vm=otypes.Vm( + name=self._module.params['vm'] + ) if self._module.params['vm'] else None, + description=self._module.params['description'], + cpu_profile=otypes.CpuProfile( + id=search_by_name( + self._connection.system_service().cpu_profiles_service(), + self._module.params['cpu_profile'], + ).id + ) if self._module.params['cpu_profile'] else None, + ) + + def update_check(self, entity): + return ( + equal(self._module.params.get('cluster'), get_link_name(self._connection, entity.cluster)) and + equal(self._module.params.get('description'), entity.description) and + equal(self._module.params.get('cpu_profile'), get_link_name(self._connection, entity.cpu_profile)) + ) + + def _get_export_domain_service(self): + provider_name = self._module.params['export_domain'] or self._module.params['image_provider'] + export_sds_service = self._connection.system_service().storage_domains_service() + export_sd = search_by_name(export_sds_service, provider_name) + if export_sd is None: + raise ValueError( + "Export storage domain/Image Provider '%s' wasn't found." % provider_name + ) + + return export_sds_service.service(export_sd.id) + + def post_export_action(self, entity): + self._service = self._get_export_domain_service().templates_service() + + def post_import_action(self, entity): + self._service = self._connection.system_service().templates_service() + + +def wait_for_import(module, templates_service): + if module.params['wait']: + start = time.time() + timeout = module.params['timeout'] + poll_interval = module.params['poll_interval'] + while time.time() < start + timeout: + template = search_by_name(templates_service, module.params['name']) + if template: + return template + time.sleep(poll_interval) + + +def main(): + argument_spec = ovirt_full_argument_spec( + state=dict( + choices=['present', 'absent', 'exported', 'imported'], + default='present', + ), + name=dict(default=None, required=True), + vm=dict(default=None), + description=dict(default=None), + cluster=dict(default=None), + cpu_profile=dict(default=None), + disks=dict(default=[], type='list'), + clone_permissions=dict(type='bool'), + export_domain=dict(default=None), + storage_domain=dict(default=None), + exclusive=dict(type='bool'), + image_provider=dict(default=None), + image_disk=dict(default=None), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + check_sdk(module) + + try: + connection = create_connection(module.params.pop('auth')) + templates_service = connection.system_service().templates_service() + templates_module = TemplatesModule( + connection=connection, + module=module, + service=templates_service, + ) + + state = module.params['state'] + if state == 'present': + ret = templates_module.create( + result_state=otypes.TemplateStatus.OK, + clone_permissions=module.params['clone_permissions'], + ) + elif state == 'absent': + ret = templates_module.remove() + elif state == 'exported': + template = templates_module.search_entity() + export_service = templates_module._get_export_domain_service() + export_template = search_by_attributes(export_service.templates_service(), id=template.id) + + ret = templates_module.action( + entity=template, + action='export', + action_condition=lambda t: export_template is None, + wait_condition=lambda t: t is not None, + post_action=templates_module.post_export_action, + storage_domain=otypes.StorageDomain(id=export_service.get().id), + exclusive=module.params['exclusive'], + ) + elif state == 'imported': + template = templates_module.search_entity() + if template: + ret = templates_module.create( + result_state=otypes.TemplateStatus.OK, + ) + else: + kwargs = {} + if module.params['image_provider']: + kwargs.update( + disk=otypes.Disk( + name=module.params['image_disk'] + ), + template=otypes.Template( + name=module.params['name'], + ), + import_as_template=True, + ) + + if module.params['image_disk']: + # We need to refresh storage domain to get list of images: + templates_module._get_export_domain_service().images_service().list() + + glance_service = connection.system_service().openstack_image_providers_service() + image_provider = search_by_name(glance_service, module.params['image_provider']) + images_service = glance_service.service(image_provider.id).images_service() + else: + images_service = templates_module._get_export_domain_service().templates_service() + template_name = module.params['image_disk'] or module.params['name'] + entity = search_by_name(images_service, template_name) + if entity is None: + raise Exception("Image/template '%s' was not found." % template_name) + + images_service.service(entity.id).import_( + storage_domain=otypes.StorageDomain( + name=module.params['storage_domain'] + ) if module.params['storage_domain'] else None, + cluster=otypes.Cluster( + name=module.params['cluster'] + ) if module.params['cluster'] else None, + **kwargs + ) + template = wait_for_import(module, templates_service) + ret = { + 'changed': True, + 'id': template.id, + 'template': get_dict_of_struct(template), + } + + module.exit_json(**ret) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + + +if __name__ == "__main__": + main() diff --git a/cloud/ovirt/ovirt_templates_facts.py b/cloud/ovirt/ovirt_templates_facts.py new file mode 100644 index 00000000000..189fccb9ed8 --- /dev/null +++ b/cloud/ovirt/ovirt_templates_facts.py @@ -0,0 +1,100 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + check_sdk, + create_connection, + get_dict_of_struct, + ovirt_full_argument_spec, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_templates_facts +short_description: Retrieve facts about one or more oVirt templates +author: "Ondra Machacek (@machacekondra)" +version_added: "2.3" +description: + - "Retrieve facts about one or more oVirt templates." +notes: + - "This module creates a new top-level C(ovirt_templates) fact, which + contains a list of templates." +options: + pattern: + description: + - "Search term which is accepted by oVirt search backend." + - "For example to search template X from datacenter Y use following pattern: + name=X and datacenter=Y" +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Gather facts about all templates which names start with C(centos) and +# belongs to data center C(west): +- ovirt_templates_facts: + pattern: name=centos* and datacenter=west +- debug: + var: ovirt_templates +''' + +RETURN = ''' +ovirt_templates: + description: "List of dictionaries describing the templates. Template attribues are mapped to dictionary keys, + all templates attributes can be found at following url: https://ovirt.example.com/ovirt-engine/api/model#types/template." + returned: On success. + type: list +''' + + +def main(): + argument_spec = ovirt_full_argument_spec( + pattern=dict(default='', required=False), + ) + module = AnsibleModule(argument_spec) + check_sdk(module) + + try: + connection = create_connection(module.params.pop('auth')) + templates_service = connection.system_service().templates_service() + templates = templates_service.list(search=module.params['pattern']) + module.exit_json( + changed=False, + ansible_facts=dict( + ovirt_templates=[ + get_dict_of_struct(c) for c in templates + ], + ), + ) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + + +if __name__ == '__main__': + main() From c9a37dd0f860caa247f8492be80e323c31637bbd Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Mon, 5 Dec 2016 18:42:41 +0100 Subject: [PATCH 2502/2522] Add oVirt ovirt_nics and ovirt_nics_facts modules (#3205) --- cloud/ovirt/ovirt_nics.py | 243 ++++++++++++++++++++++++++++++++ cloud/ovirt/ovirt_nics_facts.py | 118 ++++++++++++++++ 2 files changed, 361 insertions(+) create mode 100644 cloud/ovirt/ovirt_nics.py create mode 100644 cloud/ovirt/ovirt_nics_facts.py diff --git a/cloud/ovirt/ovirt_nics.py b/cloud/ovirt/ovirt_nics.py new file mode 100644 index 00000000000..912e03c9881 --- /dev/null +++ b/cloud/ovirt/ovirt_nics.py @@ -0,0 +1,243 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +try: + import ovirtsdk4.types as otypes +except ImportError: + pass + +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + BaseModule, + check_sdk, + create_connection, + equal, + get_link_name, + ovirt_full_argument_spec, + search_by_name, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_nics +short_description: Module to manage network interfaces of Virtual Machines in oVirt +version_added: "2.3" +author: "Ondra Machacek (@machacekondra)" +description: + - "Module to manage network interfaces of Virtual Machines in oVirt." +options: + name: + description: + - "Name of the network interface to manage." + required: true + vm: + description: + - "Name of the Virtual Machine to manage." + required: true + state: + description: + - "Should the Virtual Machine NIC be present/absent/plugged/unplugged." + choices: ['present', 'absent', 'plugged', 'unplugged'] + default: present + network: + description: + - "Logical network to which the VM network interface should use, + by default Empty network is used if network is not specified." + profile: + description: + - "Virtual network interface profile to be attached to VM network interface." + interface: + description: + - "Type of the network interface." + choices: ['virtio', 'e1000', 'rtl8139', 'pci_passthrough', 'rtl8139_virtio', 'spapr_vlan'] + default: 'virtio' + mac_address: + description: + - "Custom MAC address of the network interface, by default it's obtained from MAC pool." +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Add NIC to VM +- ovirt_nics: + state: present + vm: myvm + name: mynic + interface: e1000 + mac_address: 00:1a:4a:16:01:56 + profile: ovirtmgmt + network: ovirtmgmt + +# Plug NIC to VM +- ovirt_nics: + state: plugged + vm: myvm + name: mynic + +# Unplug NIC from VM +- ovirt_nics: + state: unplugged + vm: myvm + name: mynic + +# Remove NIC from VM +- ovirt_nics: + state: absent + vm: myvm + name: mynic +''' + +RETURN = ''' +id: + description: ID of the network interface which is managed + returned: On success if network interface is found. + type: str + sample: 7de90f31-222c-436c-a1ca-7e655bd5b60c +nic: + description: "Dictionary of all the network interface attributes. Network interface attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/nic." + returned: On success if network interface is found. +''' + + +class VmNicsModule(BaseModule): + + def __init__(self, *args, **kwargs): + super(VmNicsModule, self).__init__(*args, **kwargs) + self.vnic_id = None + + @property + def vnic_id(self): + return self._vnic_id + + @vnic_id.setter + def vnic_id(self, vnic_id): + self._vnic_id = vnic_id + + def build_entity(self): + return otypes.Nic( + name=self._module.params.get('name'), + interface=otypes.NicInterface( + self._module.params.get('interface') + ) if self._module.params.get('interface') else None, + vnic_profile=otypes.VnicProfile( + id=self.vnic_id, + ) if self.vnic_id else None, + mac=otypes.Mac( + address=self._module.params.get('mac_address') + ) if self._module.params.get('mac_address') else None, + ) + + def update_check(self, entity): + return ( + equal(self._module.params.get('interface'), str(entity.interface)) and + equal(self._module.params.get('profile'), get_link_name(self._connection, entity.vnic_profile)) and + equal(self._module.params.get('mac_address'), entity.mac.address) + ) + + +def main(): + argument_spec = ovirt_full_argument_spec( + state=dict( + choices=['present', 'absent', 'plugged', 'unplugged'], + default='present' + ), + vm=dict(required=True), + name=dict(required=True), + interface=dict(default=None), + profile=dict(default=None), + network=dict(default=None), + mac_address=dict(default=None), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + check_sdk(module) + + try: + # Locate the service that manages the virtual machines and use it to + # search for the NIC: + connection = create_connection(module.params.pop('auth')) + vms_service = connection.system_service().vms_service() + + # Locate the VM, where we will manage NICs: + vm_name = module.params.get('vm') + vm = search_by_name(vms_service, vm_name) + if vm is None: + raise Exception("VM '%s' was not found." % vm_name) + + # Locate the service that manages the virtual machines NICs: + vm_service = vms_service.vm_service(vm.id) + nics_service = vm_service.nics_service() + vmnics_module = VmNicsModule( + connection=connection, + module=module, + service=nics_service, + ) + + # Find vNIC id of the network interface (if any): + profile = module.params.get('profile') + if profile and module.params['network']: + cluster_name = get_link_name(connection, vm.cluster) + dcs_service = connection.system_service().data_centers_service() + dc = dcs_service.list(search='Clusters.name=%s' % cluster_name)[0] + networks_service = dcs_service.service(dc.id).networks_service() + network = search_by_name(networks_service, module.params['network']) + for vnic in connection.system_service().vnic_profiles_service().list(): + if vnic.name == profile and vnic.network.id == network.id: + vmnics_module.vnic_id = vnic.id + + # Handle appropriate action: + state = module.params['state'] + if state == 'present': + ret = vmnics_module.create() + elif state == 'absent': + ret = vmnics_module.remove() + elif state == 'plugged': + vmnics_module.create() + ret = vmnics_module.action( + action='activate', + action_condition=lambda nic: not nic.plugged, + wait_condition=lambda nic: nic.plugged, + ) + elif state == 'unplugged': + vmnics_module.create() + ret = vmnics_module.action( + action='deactivate', + action_condition=lambda nic: nic.plugged, + wait_condition=lambda nic: not nic.plugged, + ) + + module.exit_json(**ret) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + +if __name__ == "__main__": + main() diff --git a/cloud/ovirt/ovirt_nics_facts.py b/cloud/ovirt/ovirt_nics_facts.py new file mode 100644 index 00000000000..0e913912647 --- /dev/null +++ b/cloud/ovirt/ovirt_nics_facts.py @@ -0,0 +1,118 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +import fnmatch +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + check_sdk, + create_connection, + get_dict_of_struct, + ovirt_full_argument_spec, + search_by_name, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_nics_facts +short_description: Retrieve facts about one or more oVirt virtual machine network interfaces +author: "Ondra Machacek (@machacekondra)" +version_added: "2.3" +description: + - "Retrieve facts about one or more oVirt virtual machine network interfaces." +notes: + - "This module creates a new top-level C(ovirt_nics) fact, which + contains a list of NICs." +options: + vm: + description: + - "Name of the VM where NIC is attached." + required: true + name: + description: + - "Name of the NIC, can be used as glob expression." +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Gather facts about all NICs which names start with C(eth) for VM named C(centos7): +- ovirt_nics_facts: + vm: centos7 + name: eth* +- debug: + var: ovirt_nics +''' + +RETURN = ''' +ovirt_nics: + description: "List of dictionaries describing the network interfaces. NIC attribues are mapped to dictionary keys, + all NICs attributes can be found at following url: https://ovirt.example.com/ovirt-engine/api/model#types/nic." + returned: On success. + type: list +''' + + +def main(): + argument_spec = ovirt_full_argument_spec( + vm=dict(required=True), + name=dict(default=None), + ) + module = AnsibleModule(argument_spec) + check_sdk(module) + + try: + connection = create_connection(module.params.pop('auth')) + vms_service = connection.system_service().vms_service() + vm_name = module.params['vm'] + vm = search_by_name(vms_service, vm_name) + if vm is None: + raise Exception("VM '%s' was not found." % vm_name) + + nics_service = vms_service.service(vm.id).nics_service() + if module.params['name']: + nics = [ + e for e in nics_service.list() + if fnmatch.fnmatch(e.name, module.params['name']) + ] + else: + nics = nics_service.list() + + module.exit_json( + changed=False, + ansible_facts=dict( + ovirt_nics=[ + get_dict_of_struct(c) for c in nics + ], + ), + ) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + + +if __name__ == '__main__': + main() From e555bbdb71624b0cf5f94cc5d8e9fc3be55ffcf4 Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Mon, 5 Dec 2016 18:42:50 +0100 Subject: [PATCH 2503/2522] Add oVirt ovirt_quotas and ovirt_quotas_facts modules (#3172) --- cloud/ovirt/ovirt_quotas.py | 294 ++++++++++++++++++++++++++++++ cloud/ovirt/ovirt_quotas_facts.py | 117 ++++++++++++ 2 files changed, 411 insertions(+) create mode 100644 cloud/ovirt/ovirt_quotas.py create mode 100644 cloud/ovirt/ovirt_quotas_facts.py diff --git a/cloud/ovirt/ovirt_quotas.py b/cloud/ovirt/ovirt_quotas.py new file mode 100644 index 00000000000..4b64e53c154 --- /dev/null +++ b/cloud/ovirt/ovirt_quotas.py @@ -0,0 +1,294 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +try: + import ovirtsdk4.types as otypes +except ImportError: + pass + +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + BaseModule, + check_sdk, + create_connection, + equal, + get_link_name, + ovirt_full_argument_spec, + search_by_name, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_quotas +short_description: Module to manage datacenter quotas in oVirt +version_added: "2.3" +author: "Ondra Machacek (@machacekondra)" +description: + - "Module to manage datacenter quotas in oVirt" +options: + name: + description: + - "Name of the the quota to manage." + required: true + state: + description: + - "Should the quota be present/absent." + choices: ['present', 'absent'] + default: present + datacenter: + description: + - "Name of the datacenter where quota should be managed." + required: true + description: + description: + - "Description of the the quota to manage." + cluster_threshold: + description: + - "Cluster threshold(soft limit) defined in percentage (0-100)." + cluster_grace: + description: + - "Cluster grace(hard limit) defined in percentage (1-100)." + storage_threshold: + description: + - "Storage threshold(soft limit) defined in percentage (0-100)." + storage_grace: + description: + - "Storage grace(hard limit) defined in percentage (1-100)." + clusters: + description: + - "List of dictionary of cluster limits, which is valid to specific cluster." + - "If cluster isn't spefied it's valid to all clusters in system:" + - "C(cluster) - Name of the cluster." + - "C(memory) - Memory limit (in GiB)." + - "C(cpu) - CPU limit." + storages: + description: + - "List of dictionary of storage limits, which is valid to specific storage." + - "If storage isn't spefied it's valid to all storages in system:" + - "C(storage) - Name of the storage." + - "C(size) - Size limit (in GiB)." +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Add cluster quota to cluster cluster1 with memory limit 20GiB and CPU limit to 10: +ovirt_quotas: + name: quota1 + datacenter: dcX + clusters: + - name: cluster1 + memory: 20 + cpu: 10 + +# Add cluster quota to all clusters with memory limit 30GiB and CPU limit to 15: +ovirt_quotas: + name: quota2 + datacenter: dcX + clusters: + - memory: 30 + cpu: 15 + +# Add storage quota to storage data1 with size limit to 100GiB +ovirt_quotas: + name: quota3 + datacenter: dcX + storage_grace: 40 + storage_threshold: 60 + storages: + - name: data1 + size: 100 + +# Remove quota quota1 (Note the quota must not be assigned to any VM/disk): +ovirt_quotas: + state: absent + datacenter: dcX + name: quota1 +''' + +RETURN = ''' +id: + description: ID of the quota which is managed + returned: On success if quota is found. + type: str + sample: 7de90f31-222c-436c-a1ca-7e655bd5b60c +quota: + description: "Dictionary of all the quota attributes. Quota attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/quota." + returned: On success if quota is found. +''' + + +class QuotasModule(BaseModule): + + def build_entity(self): + return otypes.Quota( + description=self._module.params['description'], + name=self._module.params['name'], + storage_hard_limit_pct=self._module.params.get('storage_grace'), + storage_soft_limit_pct=self._module.params.get('storage_threshold'), + cluster_hard_limit_pct=self._module.params.get('cluster_grace'), + cluster_soft_limit_pct=self._module.params.get('cluster_threshold'), + ) + + def update_storage_limits(self, entity): + new_limits = {} + for storage in self._module.params.get('storages'): + new_limits[storage.get('name', '')] = { + 'size': storage.get('size'), + } + + old_limits = {} + sd_limit_service = self._service.service(entity.id).quota_storage_limits_service() + for limit in sd_limit_service.list(): + storage = get_link_name(self._connection, limit.storage_domain) if limit.storage_domain else '' + old_limits[storage] = { + 'size': limit.limit, + } + sd_limit_service.service(limit.id).remove() + + return new_limits == old_limits + + def update_cluster_limits(self, entity): + new_limits = {} + for cluster in self._module.params.get('clusters'): + new_limits[cluster.get('name', '')] = { + 'cpu': cluster.get('cpu'), + 'memory': float(cluster.get('memory')), + } + + old_limits = {} + cl_limit_service = self._service.service(entity.id).quota_cluster_limits_service() + for limit in cl_limit_service.list(): + cluster = get_link_name(self._connection, limit.cluster) if limit.cluster else '' + old_limits[cluster] = { + 'cpu': limit.vcpu_limit, + 'memory': limit.memory_limit, + } + cl_limit_service.service(limit.id).remove() + + return new_limits == old_limits + + def update_check(self, entity): + # -- FIXME -- + # Note that we here always remove all cluster/storage limits, because + # it's not currently possible to update them and then re-create the limits + # appropriatelly, this shouldn't have any side-effects, but it's not considered + # as a correct approach. + # This feature is tracked here: https://bugzilla.redhat.com/show_bug.cgi?id=1398576 + # + + return ( + self.update_storage_limits(entity) and + self.update_cluster_limits(entity) and + equal(self._module.params.get('description'), entity.description) and + equal(self._module.params.get('storage_grace'), entity.storage_hard_limit_pct) and + equal(self._module.params.get('storage_threshold'), entity.storage_soft_limit_pct) and + equal(self._module.params.get('cluster_grace'), entity.cluster_hard_limit_pct) and + equal(self._module.params.get('cluster_threshold'), entity.cluster_soft_limit_pct) + ) + + +def main(): + argument_spec = ovirt_full_argument_spec( + state=dict( + choices=['present', 'absent'], + default='present', + ), + name=dict(required=True), + datacenter=dict(required=True), + description=dict(default=None), + cluster_threshold=dict(default=None, type='int', aliases=['cluster_soft_limit']), + cluster_grace=dict(default=None, type='int', aliases=['cluster_hard_limit']), + storage_threshold=dict(default=None, type='int', aliases=['storage_soft_limit']), + storage_grace=dict(default=None, type='int', aliases=['storage_hard_limit']), + clusters=dict(default=[], type='list'), + storages=dict(default=[], type='list'), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + check_sdk(module) + + try: + connection = create_connection(module.params.pop('auth')) + datacenters_service = connection.system_service().data_centers_service() + dc_name = module.params['datacenter'] + dc_id = getattr(search_by_name(datacenters_service, dc_name), 'id', None) + if dc_id is None: + raise Exception("Datacenter '%s' was not found." % dc_name) + + quotas_service = datacenters_service.service(dc_id).quotas_service() + quotas_module = QuotasModule( + connection=connection, + module=module, + service=quotas_service, + ) + + state = module.params['state'] + if state == 'present': + ret = quotas_module.create() + + # Manage cluster limits: + cl_limit_service = quotas_service.service(ret['id']).quota_cluster_limits_service() + for cluster in module.params.get('clusters'): + cl_limit_service.add( + limit=otypes.QuotaClusterLimit( + memory_limit=float(cluster.get('memory')), + vcpu_limit=cluster.get('cpu'), + cluster=search_by_name( + connection.system_service().clusters_service(), + cluster.get('name') + ), + ), + ) + + # Manage storage limits: + sd_limit_service = quotas_service.service(ret['id']).quota_storage_limits_service() + for storage in module.params.get('storages'): + sd_limit_service.add( + limit=otypes.QuotaStorageLimit( + limit=storage.get('size'), + storage_domain=search_by_name( + connection.system_service().storage_domains_service(), + storage.get('name') + ), + ) + ) + + elif state == 'absent': + ret = quotas_module.remove() + + module.exit_json(**ret) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + + +if __name__ == "__main__": + main() diff --git a/cloud/ovirt/ovirt_quotas_facts.py b/cloud/ovirt/ovirt_quotas_facts.py new file mode 100644 index 00000000000..3d354ac084d --- /dev/null +++ b/cloud/ovirt/ovirt_quotas_facts.py @@ -0,0 +1,117 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +import fnmatch +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + check_sdk, + create_connection, + get_dict_of_struct, + ovirt_full_argument_spec, + search_by_name, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_quotas_facts +short_description: Retrieve facts about one or more oVirt quotas +version_added: "2.3" +description: + - "Retrieve facts about one or more oVirt quotas." +notes: + - "This module creates a new top-level C(ovirt_quotas) fact, which + contains a list of quotas." +options: + datacenter: + description: + - "Name of the datacenter where quota resides." + required: true + name: + description: + - "Name of the quota, can be used as glob expression." +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Gather facts about quota named C in Default datacenter: +- ovirt_quotas_facts: + datacenter: Default + name: myquota +- debug: + var: ovirt_quotas +''' + +RETURN = ''' +ovirt_quotas: + description: "List of dictionaries describing the quotas. Quota attribues are mapped to dictionary keys, + all quotas attributes can be found at following url: https://ovirt.example.com/ovirt-engine/api/model#types/quota." + returned: On success. + type: list +''' + + +def main(): + argument_spec = ovirt_full_argument_spec( + datacenter=dict(required=True), + name=dict(default=None), + ) + module = AnsibleModule(argument_spec) + check_sdk(module) + + try: + connection = create_connection(module.params.pop('auth')) + datacenters_service = connection.system_service().data_centers_service() + dc_name = module.params['datacenter'] + dc = search_by_name(datacenters_service, dc_name) + if dc is None: + raise Exception("Datacenter '%s' was not found." % dc_name) + + quotas_service = datacenters_service.service(dc.id).quotas_service() + if module.params['name']: + quotas = [ + e for e in quotas_service.list() + if fnmatch.fnmatch(e.name, module.params['name']) + ] + else: + quotas = quotas_service.list() + + module.exit_json( + changed=False, + ansible_facts=dict( + ovirt_quotas=[ + get_dict_of_struct(c) for c in quotas + ], + ), + ) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + + +if __name__ == '__main__': + main() From 3e21d4ef878bf1841d802e114ed8d8e18a671b29 Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Mon, 5 Dec 2016 18:43:07 +0100 Subject: [PATCH 2504/2522] Add oVirt ovirt_vmpools and ovirt_vmpools_facts modules (#3171) --- cloud/ovirt/ovirt_vmpools.py | 216 +++++++++++++++++++++++++++++ cloud/ovirt/ovirt_vmpools_facts.py | 97 +++++++++++++ 2 files changed, 313 insertions(+) create mode 100644 cloud/ovirt/ovirt_vmpools.py create mode 100644 cloud/ovirt/ovirt_vmpools_facts.py diff --git a/cloud/ovirt/ovirt_vmpools.py b/cloud/ovirt/ovirt_vmpools.py new file mode 100644 index 00000000000..4d0f839bf14 --- /dev/null +++ b/cloud/ovirt/ovirt_vmpools.py @@ -0,0 +1,216 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +try: + import ovirtsdk4.types as otypes +except ImportError: + pass + +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + BaseModule, + check_params, + check_sdk, + create_connection, + equal, + get_link_name, + ovirt_full_argument_spec, + wait, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_vmpools +short_description: Module to manage VM pools in oVirt +version_added: "2.3" +author: "Ondra Machacek (@machacekondra)" +description: + - "Module to manage VM pools in oVirt." +options: + name: + description: + - "Name of the the VM pool to manage." + required: true + state: + description: + - "Should the VM pool be present/absent." + - "Note that when C(state) is I(absent) all VMs in VM pool are stopped and removed." + choices: ['present', 'absent'] + default: present + template: + description: + - "Name of the template, which will be used to create VM pool." + description: + description: + - "Description of the VM pool." + cluster: + description: + - "Name of the cluster, where VM pool should be created." + type: + description: + - "Type of the VM pool. Either manual or automatic." + - "C(manual) - The administrator is responsible for explicitly returning the virtual machine to the pool. + The virtual machine reverts to the original base image after the administrator returns it to the pool." + - "C(Automatic) - When the virtual machine is shut down, it automatically reverts to its base image and + is returned to the virtual machine pool." + - "Default value is set by engine." + choices: ['manual', 'automatic'] + vm_per_user: + description: + - "Maximum number of VMs a single user can attach to from this pool." + - "Default value is set by engine." + prestarted: + description: + - "Number of pre-started VMs defines the number of VMs in run state, that are waiting + to be attached to Users." + - "Default value is set by engine." + vm_count: + description: + - "Number of VMs in the pool." + - "Default value is set by engine." +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Create VM pool from template +- ovirt_vmpools: + cluster: mycluster + name: myvmpool + template: rhel7 + vm_count: 2 + prestarted: 2 + vm_per_user: 1 + +# Remove vmpool, note that all VMs in pool will be stopped and removed: +- ovirt_vmpools: + state: absent + name: myvmpool +''' + +RETURN = ''' +id: + description: ID of the VM pool which is managed + returned: On success if VM pool is found. + type: str + sample: 7de90f31-222c-436c-a1ca-7e655bd5b60c +vm_pool: + description: "Dictionary of all the VM pool attributes. VM pool attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/vm_pool." + returned: On success if VM pool is found. +''' + + +class VmPoolsModule(BaseModule): + + def build_entity(self): + return otypes.VmPool( + name=self._module.params['name'], + description=self._module.params['description'], + comment=self._module.params['comment'], + cluster=otypes.Cluster( + name=self._module.params['cluster'] + ) if self._module.params['cluster'] else None, + template=otypes.Template( + name=self._module.params['template'] + ) if self._module.params['template'] else None, + max_user_vms=self._module.params['vm_per_user'], + prestarted_vms=self._module.params['prestarted'], + size=self._module.params['vm_count'], + type=otypes.VmPoolType( + self._module.params['type'] + ) if self._module.params['type'] else None, + ) + + def update_check(self, entity): + return ( + equal(self._module.params.get('cluster'), get_link_name(self._connection, entity.cluster)) and + equal(self._module.params.get('description'), entity.description) and + equal(self._module.params.get('comment'), entity.comment) and + equal(self._module.params.get('vm_per_user'), entity.max_user_vms) and + equal(self._module.params.get('prestarted'), entity.prestarted_vms) and + equal(self._module.params.get('vm_count'), entity.size) + ) + + +def main(): + argument_spec = ovirt_full_argument_spec( + state=dict( + choices=['present', 'absent'], + default='present', + ), + name=dict(default=None, required=True), + template=dict(default=None), + cluster=dict(default=None), + description=dict(default=None), + comment=dict(default=None), + vm_per_user=dict(default=None, type='int'), + prestarted=dict(default=None, type='int'), + vm_count=dict(default=None, type='int'), + type=dict(default=None, choices=['automatic', 'manual']), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + check_sdk(module) + check_params(module) + + try: + connection = create_connection(module.params.pop('auth')) + vm_pools_service = connection.system_service().vm_pools_service() + vm_pools_module = VmPoolsModule( + connection=connection, + module=module, + service=vm_pools_service, + ) + + state = module.params['state'] + if state == 'present': + ret = vm_pools_module.create() + + # Wait for all VM pool VMs to be created: + if module.params['wait']: + vms_service = connection.system_service().vms_service() + for vm in vms_service.list(search='pool=%s' % module.params['name']): + wait( + service=vms_service.service(vm.id), + condition=lambda vm: vm.status in [otypes.VmStatus.DOWN, otypes.VmStatus.UP], + timeout=module.params['timeout'], + ) + + elif state == 'absent': + ret = vm_pools_module.remove() + + module.exit_json(**ret) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + + +if __name__ == "__main__": + main() diff --git a/cloud/ovirt/ovirt_vmpools_facts.py b/cloud/ovirt/ovirt_vmpools_facts.py new file mode 100644 index 00000000000..eb86ae8162b --- /dev/null +++ b/cloud/ovirt/ovirt_vmpools_facts.py @@ -0,0 +1,97 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + check_sdk, + create_connection, + get_dict_of_struct, + ovirt_full_argument_spec, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_vmpools_facts +short_description: Retrieve facts about one or more oVirt vmpools +author: "Ondra Machacek (@machacekondra)" +version_added: "2.3" +description: + - "Retrieve facts about one or more oVirt vmpools." +notes: + - "This module creates a new top-level C(ovirt_vmpools) fact, which + contains a list of vmpools." +options: + pattern: + description: + - "Search term which is accepted by oVirt search backend." + - "For example to search vmpool X: name=X" +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Gather facts about all vm pools which names start with C(centos): +- ovirt_vmpools_facts: + pattern: name=centos* +- debug: + var: ovirt_vmpools +''' + +RETURN = ''' +ovirt_vm_pools: + description: "List of dictionaries describing the vmpools. Vm pool attribues are mapped to dictionary keys, + all vmpools attributes can be found at following url: https://ovirt.example.com/ovirt-engine/api/model#types/vm_pool." + returned: On success. + type: list +''' + + +def main(): + argument_spec = ovirt_full_argument_spec( + pattern=dict(default='', required=False), + ) + module = AnsibleModule(argument_spec) + check_sdk(module) + + try: + connection = create_connection(module.params.pop('auth')) + vmpools_service = connection.system_service().vm_pools_service() + vmpools = vmpools_service.list(search=module.params['pattern']) + module.exit_json( + changed=False, + ansible_facts=dict( + ovirt_vm_pools=[ + get_dict_of_struct(c) for c in vmpools + ], + ), + ) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + +if __name__ == '__main__': + main() From a10503abdd897fd72ee4aa1d416cd22f98f0a492 Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Mon, 5 Dec 2016 18:43:16 +0100 Subject: [PATCH 2505/2522] Add oVirt ovirt_external_providers and ovirt_external_providers_facts modules (#3168) --- cloud/ovirt/ovirt_external_providers.py | 244 ++++++++++++++++++ cloud/ovirt/ovirt_external_providers_facts.py | 147 +++++++++++ 2 files changed, 391 insertions(+) create mode 100644 cloud/ovirt/ovirt_external_providers.py create mode 100644 cloud/ovirt/ovirt_external_providers_facts.py diff --git a/cloud/ovirt/ovirt_external_providers.py b/cloud/ovirt/ovirt_external_providers.py new file mode 100644 index 00000000000..cfcd3c1123f --- /dev/null +++ b/cloud/ovirt/ovirt_external_providers.py @@ -0,0 +1,244 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +import traceback + +try: + import ovirtsdk4.types as otypes +except ImportError: + pass + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + BaseModule, + check_params, + check_sdk, + create_connection, + equal, + ovirt_full_argument_spec, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_external_providers +short_description: Module to manage external providers in oVirt +version_added: "2.3" +author: "Ondra Machacek (@machacekondra)" +description: + - "Module to manage external providers in oVirt" +options: + name: + description: + - "Name of the the external provider to manage." + state: + description: + - "Should the external be present or absent" + choices: ['present', 'absent'] + default: present + description: + description: + - "Description of the external provider." + type: + description: + - "Type of the external provider." + choices: ['os_image', 'os_network', 'os_volume', 'foreman'] + url: + description: + - "URL where external provider is hosted." + - "Applicable for those types: I(os_image), I(os_volume), I(os_network) and I(foreman)." + username: + description: + - "Username to be used for login to external provider." + - "Applicable for all types." + password: + description: + - "Password of the user specified in C(username) parameter." + - "Applicable for all types." + tenant_name: + description: + - "Name of the tenant." + - "Applicable for those types: I(os_image), I(os_volume) and I(os_network)." + aliases: ['tenant'] + authentication_url: + description: + - "Keystone authentication URL of the openstack provider." + - "Applicable for those types: I(os_image), I(os_volume) and I(os_network)." + aliases: ['auth_url'] + data_center: + description: + - "Name of the data center where provider should be attached." + - "Applicable for those type: I(os_volume)." +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Add image external provider: +- ovirt_external_providers: + name: image_provider + type: os_image + url: http://10.34.63.71:9292 + username: admin + password: 123456 + tenant: admin + auth_url: http://10.34.63.71:35357/v2.0/ + +# Add foreman provider: +- ovirt_external_providers: + name: foreman_provider + type: foreman + url: https://foreman.example.com + username: admin + password: 123456 + +# Remove image external provider: +- ovirt_external_providers: + state: absent + name: image_provider + type: os_image +''' + +RETURN = ''' +id: + description: ID of the external provider which is managed + returned: On success if external provider is found. + type: str + sample: 7de90f31-222c-436c-a1ca-7e655bd5b60c +external_host_provider: + description: "Dictionary of all the external_host_provider attributes. External provider attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/external_host_provider." + returned: "On success and if parameter 'type: foreman' is used." + type: dictionary +openstack_image_provider: + description: "Dictionary of all the openstack_image_provider attributes. External provider attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/openstack_image_provider." + returned: "On success and if parameter 'type: os_image' is used." + type: dictionary +openstack_volume_provider: + description: "Dictionary of all the openstack_volume_provider attributes. External provider attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/openstack_volume_provider." + returned: "On success and if parameter 'type: os_volume' is used." + type: dictionary +openstack_network_provider: + description: "Dictionary of all the openstack_network_provider attributes. External provider attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/openstack_network_provider." + returned: "On success and if parameter 'type: os_network' is used." + type: dictionary +''' + + +class ExternalProviderModule(BaseModule): + + def provider_type(self, provider_type): + self._provider_type = provider_type + + def build_entity(self): + provider_type = self._provider_type( + requires_authentication='username' in self._module.params, + ) + for key, value in self._module.params.items(): + if hasattr(provider_type, key): + setattr(provider_type, key, value) + + return provider_type + + def update_check(self, entity): + return ( + equal(self._module.params.get('description'), entity.description) and + equal(self._module.params.get('url'), entity.url) and + equal(self._module.params.get('authentication_url'), entity.authentication_url) and + equal(self._module.params.get('tenant_name'), getattr(entity, 'tenant_name', None)) and + equal(self._module.params.get('username'), entity.username) + ) + + +def _external_provider_service(provider_type, system_service): + if provider_type == 'os_image': + return otypes.OpenStackImageProvider, system_service.openstack_image_providers_service() + elif provider_type == 'os_network': + return otypes.OpenStackNetworkProvider, system_service.openstack_network_providers_service() + elif provider_type == 'os_volume': + return otypes.OpenStackVolumeProvider, system_service.openstack_volume_providers_service() + elif provider_type == 'foreman': + return otypes.ExternalHostProvider, system_service.external_host_providers_service() + + +def main(): + argument_spec = ovirt_full_argument_spec( + state=dict( + choices=['present', 'absent'], + default='present', + ), + name=dict(default=None), + description=dict(default=None), + type=dict( + default=None, + required=True, + choices=[ + 'os_image', 'os_network', 'os_volume', 'foreman', + ], + aliases=['provider'], + ), + url=dict(default=None), + username=dict(default=None), + password=dict(default=None, no_log=True), + tenant_name=dict(default=None, aliases=['tenant']), + authentication_url=dict(default=None, aliases=['auth_url']), + data_center=dict(default=None, aliases=['data_center']), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + check_sdk(module) + check_params(module) + + try: + connection = create_connection(module.params.pop('auth')) + provider_type, external_providers_service = _external_provider_service( + provider_type=module.params.pop('type'), + system_service=connection.system_service(), + ) + external_providers_module = ExternalProviderModule( + connection=connection, + module=module, + service=external_providers_service, + ) + external_providers_module.provider_type(provider_type) + + state = module.params.pop('state') + if state == 'absent': + ret = external_providers_module.remove() + elif state == 'present': + ret = external_providers_module.create() + + module.exit_json(**ret) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + + +if __name__ == "__main__": + main() diff --git a/cloud/ovirt/ovirt_external_providers_facts.py b/cloud/ovirt/ovirt_external_providers_facts.py new file mode 100644 index 00000000000..9f3e601866c --- /dev/null +++ b/cloud/ovirt/ovirt_external_providers_facts.py @@ -0,0 +1,147 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +import fnmatch +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + check_sdk, + create_connection, + get_dict_of_struct, + ovirt_full_argument_spec, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_external_providers_facts +short_description: Retrieve facts about one or more oVirt external_providers +version_added: "2.3" +description: + - "Retrieve facts about one or more oVirt external_providers." +notes: + - "This module creates a new top-level C(ovirt_external_providers) fact, which + contains a list of external_providers." +options: + type: + description: + - "Type of the external provider." + choices: ['os_image', 'os_network', 'os_volume', 'foreman'] + required: true + name: + description: + - "Name of the external provider, can be used as glob expression." +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Gather facts about all image external providers named C: +- ovirt_external_providers_facts: + type: os_image + name: glance +- debug: + var: ovirt_external_providers +''' + +RETURN = ''' +external_host_providers: + description: "List of dictionaries of all the external_host_provider attributes. External provider attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/external_host_provider." + returned: "On success and if parameter 'type: foreman' is used." + type: list +openstack_image_providers: + description: "List of dictionaries of all the openstack_image_provider attributes. External provider attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/openstack_image_provider." + returned: "On success and if parameter 'type: os_image' is used." + type: list +openstack_volume_providers: + description: "List of dictionaries of all the openstack_volume_provider attributes. External provider attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/openstack_volume_provider." + returned: "On success and if parameter 'type: os_volume' is used." + type: list +openstack_network_providers: + description: "List of dictionaries of all the openstack_network_provider attributes. External provider attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/openstack_network_provider." + returned: "On success and if parameter 'type: os_network' is used." + type: list +''' + + +def _external_provider_service(provider_type, system_service): + if provider_type == 'os_image': + return system_service.openstack_image_providers_service() + elif provider_type == 'os_network': + return system_service.openstack_network_providers_service() + elif provider_type == 'os_volume': + return system_service.openstack_volume_providers_service() + elif provider_type == 'foreman': + return system_service.external_host_providers_service() + + +def main(): + argument_spec = ovirt_full_argument_spec( + name=dict(default=None, required=False), + type=dict( + default=None, + required=True, + choices=[ + 'os_image', 'os_network', 'os_volume', 'foreman', + ], + aliases=['provider'], + ), + ) + module = AnsibleModule(argument_spec) + check_sdk(module) + + try: + connection = create_connection(module.params.pop('auth')) + external_providers_service = _external_provider_service( + provider_type=module.params.pop('type'), + system_service=connection.system_service(), + ) + if module.params['name']: + external_providers = [ + e for e in external_providers_service.list() + if fnmatch.fnmatch(e.name, module.params['name']) + ] + else: + external_providers = external_providers_service.list() + + module.exit_json( + changed=False, + ansible_facts=dict( + ovirt_external_providers=[ + get_dict_of_struct(c) for c in external_providers + ], + ), + ) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + + +if __name__ == '__main__': + main() From 91d5822d68e43760f967b45751279d12314b7a10 Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Mon, 5 Dec 2016 18:43:26 +0100 Subject: [PATCH 2506/2522] Add oVirt ovirt_permissions and ovirt_permissions_facts modules (#3160) --- cloud/ovirt/ovirt_permissions.py | 287 +++++++++++++++++++++++++ cloud/ovirt/ovirt_permissions_facts.py | 136 ++++++++++++ 2 files changed, 423 insertions(+) create mode 100644 cloud/ovirt/ovirt_permissions.py create mode 100644 cloud/ovirt/ovirt_permissions_facts.py diff --git a/cloud/ovirt/ovirt_permissions.py b/cloud/ovirt/ovirt_permissions.py new file mode 100644 index 00000000000..11a3182ad23 --- /dev/null +++ b/cloud/ovirt/ovirt_permissions.py @@ -0,0 +1,287 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +try: + import ovirtsdk4.types as otypes +except ImportError: + pass + +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + BaseModule, + check_sdk, + create_connection, + equal, + follow_link, + get_link_name, + ovirt_full_argument_spec, + search_by_attributes, + search_by_name, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_permissions +short_description: "Module to manage permissions of users/groups in oVirt" +version_added: "2.3" +author: "Ondra Machacek (@machacekondra)" +description: + - "Module to manage permissions of users/groups in oVirt" +options: + role: + description: + - "Name of the the role to be assigned to user/group on specific object." + default: UserRole + state: + description: + - "Should the permission be present/absent." + choices: ['present', 'absent'] + default: present + object_id: + description: + - "ID of the object where the permissions should be managed." + object_name: + description: + - "Name of the object where the permissions should be managed." + object_type: + description: + - "The object where the permissions should be managed." + default: 'virtual_machine' + choices: [ + 'data_center', + 'cluster', + 'host', + 'storage_domain', + 'network', + 'disk', + 'vm', + 'vm_pool', + 'template', + ] + user_name: + description: + - "Username of the the user to manage. In most LDAPs it's I(uid) of the user, but in Active Directory you must specify I(UPN) of the user." + group_name: + description: + - "Name of the the group to manage." + authz_name: + description: + - "Authorization provider of the user/group. In previous versions of oVirt known as domain." + required: true + aliases: ['domain'] + namespace: + description: + - "Namespace of the authorization provider, where user/group resides." + required: false +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Add user user1 from authorization provider example.com-authz +- ovirt_permissions: + user_name: user1 + authz_name: example.com-authz + object_type: vm + object_name: myvm + role: UserVmManager + +# Remove permission from user +- ovirt_permissions: + state: absent + user_name: user1 + authz_name: example.com-authz + object_type: cluster + object_name: mycluster + role: ClusterAdmin +''' + +RETURN = ''' +id: + description: ID of the permission which is managed + returned: On success if permission is found. + type: str + sample: 7de90f31-222c-436c-a1ca-7e655bd5b60c +permission: + description: "Dictionary of all the permission attributes. Permission attributes can be found on your oVirt instance + at following url: https://ovirt.example.com/ovirt-engine/api/model#types/permission." + returned: On success if permission is found. +''' + + +def _objects_service(connection, object_type): + return getattr( + connection.system_service(), + '%ss_service' % object_type, + None, + )() + + +def _object_service(connection, module): + object_type = module.params['object_type'] + objects_service = _objects_service(connection, object_type) + + object_id = module.params['object_id'] + if object_id is None: + sdk_object = search_by_name(objects_service, module.params['object_name']) + if sdk_object is None: + raise Exception( + "'%s' object '%s' was not found." % ( + module.params['object_type'], + module.params['object_name'] + ) + ) + object_id = sdk_object.id + + return objects_service.service(object_id) + + +def _permission(module, permissions_service, connection): + for permission in permissions_service.list(): + user = follow_link(connection, permission.user) + if ( + equal(module.params['user_name'], user.principal if user else None) and + equal(module.params['group_name'], get_link_name(connection, permission.group)) and + equal(module.params['role'], get_link_name(connection, permission.role)) + ): + return permission + + +class PermissionsModule(BaseModule): + + def _user(self): + user = search_by_attributes( + self._connection.system_service().users_service(), + usrname="{name}@{authz_name}".format( + name=self._module.params['user_name'], + authz_name=self._module.params['authz_name'], + ), + ) + if user is None: + raise Exception("User '%s' was not found." % self._module.params['user_name']) + return user + + def _group(self): + groups = self._connection.system_service().groups_service().list( + search="name={name}".format( + name=self._module.params['group_name'], + ) + ) + + # If found more groups, filter them by namespace and authz name: + # (filtering here, as oVirt backend doesn't support it) + if len(groups) > 1: + groups = [ + g for g in groups if ( + equal(self._module.params['namespace'], g.namespace) and + equal(self._module.params['authz_name'], g.domain.name) + ) + ] + if not groups: + raise Exception("Group '%s' was not found." % self._module.params['group_name']) + return groups[0] + + def build_entity(self): + entity = self._group() if self._module.params['group_name'] else self._user() + + return otypes.Permission( + user=otypes.User( + id=entity.id + ) if self._module.params['user_name'] else None, + group=otypes.Group( + id=entity.id + ) if self._module.params['group_name'] else None, + role=otypes.Role( + name=self._module.params['role'] + ), + ) + + +def main(): + argument_spec = ovirt_full_argument_spec( + state=dict( + choices=['present', 'absent'], + default='present', + ), + role=dict(default='UserRole'), + object_type=dict( + default='virtual_machine', + choices=[ + 'data_center', + 'cluster', + 'host', + 'storage_domain', + 'network', + 'disk', + 'vm', + 'vm_pool', + 'template', + ] + ), + authz_name=dict(required=True, aliases=['domain']), + object_id=dict(default=None), + object_name=dict(default=None), + user_name=dict(rdefault=None), + group_name=dict(default=None), + namespace=dict(default=None), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + check_sdk(module) + + if module.params['object_name'] is None and module.params['object_id'] is None: + module.fail_json(msg='"object_name" or "object_id" is required') + + if module.params['user_name'] is None and module.params['group_name'] is None: + module.fail_json(msg='"user_name" or "group_name" is required') + + try: + connection = create_connection(module.params.pop('auth')) + permissions_service = _object_service(connection, module).permissions_service() + permissions_module = PermissionsModule( + connection=connection, + module=module, + service=permissions_service, + ) + + permission = _permission(module, permissions_service, connection) + state = module.params['state'] + if state == 'present': + ret = permissions_module.create(entity=permission) + elif state == 'absent': + ret = permissions_module.remove(entity=permission) + + module.exit_json(**ret) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + + +if __name__ == "__main__": + main() diff --git a/cloud/ovirt/ovirt_permissions_facts.py b/cloud/ovirt/ovirt_permissions_facts.py new file mode 100644 index 00000000000..2c1b4fb5c01 --- /dev/null +++ b/cloud/ovirt/ovirt_permissions_facts.py @@ -0,0 +1,136 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +import traceback + +try: + import ovirtsdk4 as sdk +except ImportError: + pass + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ovirt import ( + check_sdk, + create_connection, + get_link_name, + ovirt_full_argument_spec, + search_by_name, +) + + +DOCUMENTATION = ''' +--- +module: ovirt_permissions_facts +short_description: Retrieve facts about one or more oVirt permissions +author: "Ondra Machacek (@machacekondra)" +version_added: "2.3" +description: + - "Retrieve facts about one or more oVirt permissions." +notes: + - "This module creates a new top-level C(ovirt_permissions) fact, which + contains a list of permissions." +options: + user_name: + description: + - "Username of the the user to manage. In most LDAPs it's I(uid) of the user, but in Active Directory you must specify I(UPN) of the user." + group_name: + description: + - "Name of the the group to manage." + authz_name: + description: + - "Authorization provider of the user/group. In previous versions of oVirt known as domain." + required: true + aliases: ['domain'] + namespace: + description: + - "Namespace of the authorization provider, where user/group resides." + required: false +extends_documentation_fragment: ovirt +''' + +EXAMPLES = ''' +# Examples don't contain auth parameter for simplicity, +# look at ovirt_auth module to see how to reuse authentication: + +# Gather facts about all permissions of user with username C(john): +- ovirt_permissions_facts: + user_name: john + authz_name: example.com-authz +- debug: + var: ovirt_permissions +''' + +RETURN = ''' +ovirt_permissions: + description: "List of dictionaries describing the permissions. Permission attribues are mapped to dictionary keys, + all permissions attributes can be found at following url: https://ovirt.example.com/ovirt-engine/api/model#types/permission." + returned: On success. + type: list +''' + + +def _permissions_service(connection, module): + if module.params['user_name']: + service = connection.system_service().users_service() + entity = search_by_name(service, module.params['user_name']) + else: + service = connection.system_service().groups_service() + entity = search_by_name(service, module.params['group_name']) + + if entity is None: + raise Exception("User/Group wasn't found.") + + return service.service(entity.id).permissions_service() + + +def main(): + argument_spec = ovirt_full_argument_spec( + authz_name=dict(required=True, aliases=['domain']), + user_name=dict(rdefault=None), + group_name=dict(default=None), + namespace=dict(default=None), + ) + module = AnsibleModule(argument_spec) + check_sdk(module) + + try: + connection = create_connection(module.params.pop('auth')) + permissions_service = _permissions_service(connection, module) + permissions = [] + for p in permissions_service.list(): + newperm = dict() + for key, value in p.__dict__.items(): + if value and isinstance(value, sdk.Struct): + newperm[key[1:]] = get_link_name(connection, value) + permissions.append(newperm) + + module.exit_json( + changed=False, + ansible_facts=dict(ovirt_permissions=permissions), + ) + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + finally: + connection.close(logout=False) + + +if __name__ == '__main__': + main() From 3c95e7567bcc2488dbf01518c1ac5b990e9c7f17 Mon Sep 17 00:00:00 2001 From: Bill Wang Date: Tue, 6 Dec 2016 04:47:46 +1100 Subject: [PATCH 2507/2522] improve example for module ec2_vpc_subnet_facts (#3511) --- cloud/amazon/ec2_vpc_subnet_facts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud/amazon/ec2_vpc_subnet_facts.py b/cloud/amazon/ec2_vpc_subnet_facts.py index 98f1e4875c5..c8adce2c2c5 100644 --- a/cloud/amazon/ec2_vpc_subnet_facts.py +++ b/cloud/amazon/ec2_vpc_subnet_facts.py @@ -65,6 +65,7 @@ - publicA - publicB - publicC + register: subnet_facts - set_fact: subnet_ids: "{{ subnet_facts.results|map(attribute='subnets.0.id')|list }}" From 6d2b36df2ed985ad8f50d5a5b84f8011f2f67168 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 5 Dec 2016 18:49:15 +0100 Subject: [PATCH 2508/2522] Update route53_zone.py fix indentation typo in examples (#3255) --- cloud/amazon/route53_zone.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/route53_zone.py b/cloud/amazon/route53_zone.py index 2fc532955a3..002e89023f3 100644 --- a/cloud/amazon/route53_zone.py +++ b/cloud/amazon/route53_zone.py @@ -78,7 +78,7 @@ vpc_region: '{{ ec2_region }}' zone: '{{ vpc_dns_zone }}' state: present - register: zone_out + register: zone_out - debug: var: zone_out From 15197d797539fc6a01dcc8c136be7eab61863fd7 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Mon, 5 Dec 2016 12:50:14 -0500 Subject: [PATCH 2509/2522] yum_repository: use https:// for EPEL examples (#3464) This whole module is really lacking in security guidelines, but downloading RPMs via plain `http://` without gpg is quite bad. Let's use `https://` for the EPEL examples for a start. --- packaging/os/yum_repository.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packaging/os/yum_repository.py b/packaging/os/yum_repository.py index a796a0dd6ae..3f14ff50f99 100644 --- a/packaging/os/yum_repository.py +++ b/packaging/os/yum_repository.py @@ -401,14 +401,14 @@ yum_repository: name: epel description: EPEL YUM repo - baseurl: http://download.fedoraproject.org/pub/epel/$releasever/$basearch/ + baseurl: https://download.fedoraproject.org/pub/epel/$releasever/$basearch/ - name: Add multiple repositories into the same file (1/2) yum_repository: name: epel description: EPEL YUM repo file: external_repos - baseurl: http://download.fedoraproject.org/pub/epel/$releasever/$basearch/ + baseurl: https://download.fedoraproject.org/pub/epel/$releasever/$basearch/ gpgcheck: no - name: Add multiple repositories into the same file (2/2) From 1853e00f2c6e2a96ef2917a094e634f8d545eb75 Mon Sep 17 00:00:00 2001 From: MDCollins Date: Mon, 5 Dec 2016 13:02:15 -0800 Subject: [PATCH 2510/2522] Update the status codes to look for (#2120) Creation of a maintenance window returns a 201 (PagerDuty Developer documentation is unfortunately incorrect). Deleting a maintenance window returns a 204. --- monitoring/pagerduty.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monitoring/pagerduty.py b/monitoring/pagerduty.py index a6d6da7d72b..e2ba95fef7d 100644 --- a/monitoring/pagerduty.py +++ b/monitoring/pagerduty.py @@ -214,7 +214,7 @@ def create(module, name, user, passwd, token, requester_id, service, hours, minu data = json.dumps(request_data) response, info = fetch_url(module, url, data=data, headers=headers, method='POST') - if info['status'] != 200: + if info['status'] != 201: module.fail_json(msg="failed to create the window: %s" % info['msg']) try: @@ -240,7 +240,7 @@ def absent(module, name, user, passwd, token, requester_id, service): data = json.dumps(request_data) response, info = fetch_url(module, url, data=data, headers=headers, method='DELETE') - if info['status'] != 200: + if info['status'] != 204: module.fail_json(msg="failed to delete the window: %s" % info['msg']) try: From 409bce2fa19705578615df0c7559ee20f1412cc3 Mon Sep 17 00:00:00 2001 From: David Stygstra Date: Mon, 5 Dec 2016 17:29:46 -0500 Subject: [PATCH 2511/2522] Fix #3410 (#3411) A port with the same name as the bridge is implicitly created for every bridge, but it doesn't show in in `ovs-vsctl list-ports BRIDGE`. --- network/openvswitch_port.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/openvswitch_port.py b/network/openvswitch_port.py index 52c2d94b4c4..ad7ec0865bb 100644 --- a/network/openvswitch_port.py +++ b/network/openvswitch_port.py @@ -159,7 +159,7 @@ def exists(self): if rtc != 0: self.module.fail_json(msg=err) - return any(port.rstrip() == self.port for port in out.split('\n')) + return any(port.rstrip() == self.port for port in out.split('\n')) or self.port == self.bridge def set(self, set_opt): """ Set attributes on a port. """ From 461cabd176fccb78402b77567f3e885d48c596e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Kr=C3=A4mer?= Date: Tue, 6 Dec 2016 09:31:52 +1100 Subject: [PATCH 2512/2522] Allow Datadog monitors to be retrieved by id instead of name. (#3456) --- monitoring/datadog_monitor.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py index 8fe9ded9be3..d10dd70cff5 100644 --- a/monitoring/datadog_monitor.py +++ b/monitoring/datadog_monitor.py @@ -105,6 +105,11 @@ required: false default: null version_added: "2.3" + id: + description: ["The id of the alert. If set, will be used instead of the name to locate the alert."] + required: false + default: null + version_added: "2.3" ''' EXAMPLES = ''' @@ -172,7 +177,8 @@ def main(): thresholds=dict(required=False, type='dict', default=None), tags=dict(required=False, type='list', default=None), locked=dict(required=False, default=False, type='bool'), - require_full_window=dict(required=False, default=None, type='bool') + require_full_window=dict(required=False, default=None, type='bool'), + id=dict(required=False) ) ) @@ -203,9 +209,16 @@ def _fix_template_vars(message): def _get_monitor(module): - for monitor in api.Monitor.get_all(): - if monitor['name'] == module.params['name']: - return monitor + if module.params['id'] is not None: + monitor = api.Monitor.get(module.params['id']) + if 'errors' in monitor: + module.fail_json(msg="Failed to retrieve monitor with id %s, errors are %s" % (module.params['id'], str(monitor['errors']))) + return monitor + else: + monitors = api.Monitor.get_all() + for monitor in monitors: + if monitor['name'] == module.params['name']: + return monitor return {} From e6405e2d49eb866038af16534c7b3fcad77c71fc Mon Sep 17 00:00:00 2001 From: 0livd Date: Mon, 5 Dec 2016 23:47:55 +0100 Subject: [PATCH 2513/2522] Fetch vmid from the ProxmoxAPI when not set (#3591) The vmid is no longer a required parameter For the 'present' state: If not set, the next available one will be fetched from the API For the 'started', 'stopped', 'restarted' and 'absent' states: If not set, the module will try to fetch it from the API based on the hostname Inspired from the behavior of the proxmox_kvm module --- cloud/misc/proxmox.py | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/cloud/misc/proxmox.py b/cloud/misc/proxmox.py index db0233c22f5..6b065bb7bca 100644 --- a/cloud/misc/proxmox.py +++ b/cloud/misc/proxmox.py @@ -40,8 +40,10 @@ vmid: description: - the instance id + - if not set, the next available VM ID will be fetched from ProxmoxAPI. + - if not set, will be fetched from PromoxAPI based on the hostname default: null - required: true + required: false validate_certs: description: - enable / disable https certificate verification @@ -71,6 +73,7 @@ description: - the instance hostname - required only for C(state=present) + - must be unique if vmid is not passed default: null required: false ostemplate: @@ -186,6 +189,9 @@ hostname: example.org ostemplate: 'local:vztmpl/ubuntu-14.04-x86_64.tar.gz' +# Create new container automatically selecting the next available vmid. +- proxmox: node='uk-mc02' api_user='root@pam' api_password='1q2w3e' api_host='node1' password='123456' hostname='example.org' ostemplate='local:vztmpl/ubuntu-14.04-x86_64.tar.gz' + # Create new container with minimal options with force(it will rewrite existing container) - proxmox: vmid: 100 @@ -297,6 +303,16 @@ VZ_TYPE=None +def get_nextvmid(proxmox): + try: + vmid = proxmox.cluster.nextid.get() + return vmid + except Exception as e: + module.fail_json(msg="Unable to get next vmid. Failed with exception: %s") + +def get_vmid(proxmox, hostname): + return [ vm['vmid'] for vm in proxmox.cluster.resources.get(type='vm') if vm['name'] == hostname ] + def get_instance(proxmox, vmid): return [ vm for vm in proxmox.cluster.resources.get(type='vm') if vm['vmid'] == int(vmid) ] @@ -386,7 +402,7 @@ def main(): api_host = dict(required=True), api_user = dict(required=True), api_password = dict(no_log=True), - vmid = dict(required=True), + vmid = dict(required=False), validate_certs = dict(type='bool', default='no'), node = dict(), pool = dict(), @@ -426,6 +442,7 @@ def main(): memory = module.params['memory'] swap = module.params['swap'] storage = module.params['storage'] + hostname = module.params['hostname'] if module.params['ostemplate'] is not None: template_store = module.params['ostemplate'].split(":")[0] timeout = module.params['timeout'] @@ -445,10 +462,22 @@ def main(): except Exception as e: module.fail_json(msg='authorization on proxmox cluster failed with exception: %s' % e) + # If vmid not set get the Next VM id from ProxmoxAPI + # If hostname is set get the VM id from ProxmoxAPI + if not vmid and state == 'present': + vmid = get_nextvmid(proxmox) + elif not vmid and hostname: + vmid = get_vmid(proxmox, hostname)[0] + elif not vmid: + module.exit_json(changed=False, msg="Vmid could not be fetched for the following action: %s" % state) + if state == 'present': try: if get_instance(proxmox, vmid) and not module.params['force']: module.exit_json(changed=False, msg="VM with vmid = %s is already exists" % vmid) + # If no vmid was passed, there cannot be another VM named 'hostname' + if not module.params['vmid'] and get_vmid(proxmox, hostname) and not module.params['force']: + module.exit_json(changed=False, msg="VM with hostname %s already exists and has ID number %s" % (hostname, get_vmid(proxmox, hostname)[0])) elif not (node, module.params['hostname'] and module.params['password'] and module.params['ostemplate']): module.fail_json(msg='node, hostname, password and ostemplate are mandatory for creating vm') elif not node_check(proxmox, node): From ee18fdede0adeec89a023b83f9365f74b0d7563b Mon Sep 17 00:00:00 2001 From: Ben Tomasik Date: Mon, 5 Dec 2016 17:23:11 -0600 Subject: [PATCH 2514/2522] Add check mode support (#3523) --- cloud/amazon/ec2_remote_facts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_remote_facts.py b/cloud/amazon/ec2_remote_facts.py index cb80068a7cc..2e29b798003 100644 --- a/cloud/amazon/ec2_remote_facts.py +++ b/cloud/amazon/ec2_remote_facts.py @@ -165,7 +165,8 @@ def main(): ) ) - module = AnsibleModule(argument_spec=argument_spec) + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) if not HAS_BOTO: module.fail_json(msg='boto required for this module') From cc9e9cde6d558ab30e783b60757434cfca7b29c0 Mon Sep 17 00:00:00 2001 From: Slezhuk Evgeniy Date: Thu, 28 Apr 2016 18:00:11 +0300 Subject: [PATCH 2515/2522] Add 'link' action to jira module --- web_infrastructure/jira.py | 44 +++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/web_infrastructure/jira.py b/web_infrastructure/jira.py index e7d1e1a9017..3947f15f32b 100755 --- a/web_infrastructure/jira.py +++ b/web_infrastructure/jira.py @@ -91,6 +91,24 @@ description: - Sets the assignee on create or transition operations. Note not all transitions will allow this. + linktype: + required: false + version_added: 2.3 + description: + - Set type of link, when action 'link' selected + + inwardissue: + required: false + version_added: 2.3 + description: + - set issue from which link will be created + + outwardissue: + required: false + version_added: 2.3 + description: + - set issue to which link will be created + fields: required: false description: @@ -179,6 +197,10 @@ name: '{{ issue.meta.fields.creator.name }}' comment: '{{issue.meta.fields.creator.displayName }}' +- name: Create link from HSP-1 to MKY-1 + jira: uri={{server}} username={{user}} password={{pass}} operation=link + linktype=Relate inwardissue=HSP-1 outwardissue=MKY-1 + # Transition an issue by target status - name: Close the issue jira: @@ -315,13 +337,26 @@ def transition(restbase, user, passwd, params): return ret +def link(restbase, user, passwd, params): + data = { + 'type': { 'name': params['linktype'] }, + 'inwardIssue': { 'key': params['inwardissue'] }, + 'outwardIssue': { 'key': params['outwardissue'] }, + } + + url = restbase + '/issueLink/' + + ret = post(url, user, passwd, data) + + return ret # Some parameters are required depending on the operation: OP_REQUIRED = dict(create=['project', 'issuetype', 'summary', 'description'], comment=['issue', 'comment'], edit=[], fetch=['issue'], - transition=['status']) + transition=['status'], + link=['linktype', 'inwardissue', 'outwardissue']) def main(): @@ -329,7 +364,7 @@ def main(): module = AnsibleModule( argument_spec=dict( uri=dict(required=True), - operation=dict(choices=['create', 'comment', 'edit', 'fetch', 'transition'], + operation=dict(choices=['create', 'comment', 'edit', 'fetch', 'transition', 'link'], aliases=['command'], required=True), username=dict(required=True), password=dict(required=True), @@ -341,7 +376,10 @@ def main(): comment=dict(), status=dict(), assignee=dict(), - fields=dict(default={}, type='dict') + fields=dict(default={}, type='dict'), + linktype=dict(), + inwardissue=dict(), + outwardissue=dict(), ), supports_check_mode=False ) From a54daa611b6a99ea37ce599d676e52b7277a49fd Mon Sep 17 00:00:00 2001 From: Dino Occhialini Date: Tue, 6 Dec 2016 07:40:31 -0600 Subject: [PATCH 2516/2522] Add XBPS module (#1749) Adds xbps module for managing Void Linux packages. Currently supports: * Installation * Removal * Updating Specific Packages * Updating All Packages * Updating package cache --- packaging/os/xbps.py | 300 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 packaging/os/xbps.py diff --git a/packaging/os/xbps.py b/packaging/os/xbps.py new file mode 100644 index 00000000000..c7b7e6ee472 --- /dev/null +++ b/packaging/os/xbps.py @@ -0,0 +1,300 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2016 Dino Occhialini +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +DOCUMENTATION = ''' +--- +module: xbps +short_description: Manage packages with XBPS +description: + - Manage packages with the XBPS package manager. +author: + - "Dino Occhialini (@dinoocch)" + - "Michael Aldridge (@the-maldridge)" +version_added: "2.3" +options: + name: + description: + - Name of the package to install, upgrade, or remove. + required: false + default: null + state: + description: + - Desired state of the package. + required: false + default: "present" + choices: ["present", "absent", "latest"] + recurse: + description: + - When removing a package, also remove its dependencies, provided + that they are not required by other packages and were not + explicitly installed by a user. + required: false + default: no + choices: ["yes", "no"] + update_cache: + description: + - Whether or not to refresh the master package lists. This can be + run as part of a package installation or as a separate step. + required: false + default: yes + choices: ["yes", "no"] + upgrade: + description: + - Whether or not to upgrade whole system + required: false + default: no + choices: ["yes", "no"] +''' + +EXAMPLES = ''' +# Install package foo +- xbps: name=foo state=present +# Upgrade package foo +- xbps: name=foo state=latest update_cache=yes +# Remove packages foo and bar +- xbps: name=foo,bar state=absent +# Recursively remove package foo +- xbps: name=foo state=absent recurse=yes +# Update package cache +- xbps: update_cache=yes +# Upgrade packages +- xbps: upgrade=yes +''' + +RETURN = ''' +msg: + description: Message about results + returned: success + type: string + sample: "System Upgraded" +packages: + description: Packages that are affected/would be affected + type: list + sample: ["ansible"] +''' + + +import os + + +def is_installed(xbps_output): + """Returns package install state""" + return bool(len(xbps_output)) + + +def query_package(module, xbps_path, name, state="present"): + """Returns Package info""" + if state == "present": + lcmd = "%s %s" % (xbps_path['query'], name) + lrc, lstdout, lstderr = module.run_command(lcmd, check_rc=False) + if not is_installed(lstdout): + # package is not installed locally + return False, False + + rcmd = "%s -Sun" % (xbps_path['install']) + rrc, rstdout, rstderr = module.run_command(rcmd, check_rc=False) + if rrc == 0 or rrc == 17: + """Return True to indicate that the package is installed locally, + and the result of the version number comparison to determine if the + package is up-to-date""" + return True, name not in rstdout + + return False, False + + +def update_package_db(module, xbps_path): + """Returns True if update_package_db changed""" + cmd = "%s -S" % (xbps_path['install']) + rc, stdout, stderr = module.run_command(cmd, check_rc=False) + + if rc != 0: + module.fail_json(msg="Could not update package db") + if "avg rate" in stdout: + return True + else: + return False + + +def upgrade(module, xbps_path): + """Returns true is full upgrade succeeds""" + cmdupgrade = "%s -uy" % (xbps_path['install']) + cmdneedupgrade = "%s -un" % (xbps_path['install']) + + rc, stdout, stderr = module.run_command(cmdneedupgrade, check_rc=False) + if rc == 0: + if(len(stdout.splitlines()) == 0): + module.exit_json(changed=False, msg='Nothing to upgrade') + else: + rc, stdout, stderr = module.run_command(cmdupgrade, check_rc=False) + if rc == 0: + module.exit_json(changed=True, msg='System upgraded') + else: + module.fail_json(msg="Could not upgrade") + else: + module.fail_json(msg="Could not upgrade") + + +def remove_packages(module, xbps_path, packages): + """Returns true if package removal succeeds""" + changed_packages = [] + # Using a for loop incase of error, we can report the package that failed + for package in packages: + # Query the package first, to see if we even need to remove + installed, updated = query_package(module, xbps_path, package) + if not installed: + continue + + cmd = "%s -y %s" % (xbps_path['remove'], package) + rc, stdout, stderr = module.run_command(cmd, check_rc=False) + + if rc != 0: + module.fail_json(msg="failed to remove %s" % (package)) + + changed_packages.append(package) + + if len(changed_packages) > 0: + + module.exit_json(changed=True, msg="removed %s package(s)" % + len(changed_packages), packages=changed_packages) + + module.exit_json(changed=False, msg="package(s) already absent") + + +def install_packages(module, xbps_path, state, packages): + """Returns true if package install succeeds.""" + toInstall = [] + for i, package in enumerate(packages): + """If the package is installed and state == present or state == latest + and is up-to-date then skip""" + installed, updated = query_package(module, xbps_path, package) + if installed and (state == 'present' or + (state == 'latest' and updated)): + continue + + toInstall.append(package) + + if len(toInstall) == 0: + module.exit_json(changed=False, msg="Nothing to Install") + + cmd = "%s -y %s" % (xbps_path['install'], " ".join(toInstall)) + rc, stdout, stderr = module.run_command(cmd, check_rc=False) + + if rc != 0 and not (state == 'latest' and rc == 17): + module.fail_json(msg="failed to install %s" % (package)) + + module.exit_json(changed=True, msg="installed %s package(s)" + % (len(toInstall)), + packages=toInstall) + + module.exit_json(changed=False, msg="package(s) already installed", + packages=[]) + + +def check_packages(module, xbps_path, packages, state): + """Returns change status of command""" + would_be_changed = [] + for package in packages: + installed, updated = query_package(module, xbps_path, package) + if ((state in ["present", "latest"] and not installed) or + (state == "absent" and installed) or + (state == "latest" and not updated)): + would_be_changed.append(package) + if would_be_changed: + if state == "absent": + state = "removed" + module.exit_json(changed=True, msg="%s package(s) would be %s" % ( + len(would_be_changed), state), + packages=would_be_changed) + else: + module.exit_json(changed=False, msg="package(s) already %s" % state, + packages=[]) + + +def main(): + """Returns, calling appropriate command""" + + module = AnsibleModule( + argument_spec=dict( + name=dict(default=None, aliases=['pkg', 'package'], type='list'), + state=dict(default='present', choices=['present', 'installed', + "latest", 'absent', + 'removed']), + recurse=dict(default=False, type='bool'), + force=dict(default=False, type='bool'), + upgrade=dict(default=False, type='bool'), + update_cache=dict(default=True, aliases=['update-cache'], + type='bool') + ), + required_one_of=[['name', 'update_cache', 'upgrade']], + supports_check_mode=True) + + xbps_path = dict() + xbps_path['install'] = module.get_bin_path('xbps-install', True) + xbps_path['query'] = module.get_bin_path('xbps-query', True) + xbps_path['remove'] = module.get_bin_path('xbps-remove', True) + + if not os.path.exists(xbps_path['install']): + module.fail_json(msg="cannot find xbps, in path %s" + % (xbps_path['install'])) + + p = module.params + + # normalize the state parameter + if p['state'] in ['present', 'installed']: + p['state'] = 'present' + elif p['state'] in ['absent', 'removed']: + p['state'] = 'absent' + + if p["update_cache"] and not module.check_mode: + changed = update_package_db(module, xbps_path) + if p['name'] is None and not p['upgrade']: + if changed: + module.exit_json(changed=True, + msg='Updated the package master lists') + else: + module.exit_json(changed=False, + msg='Package list already up to date') + + if (p['update_cache'] and module.check_mode and not + (p['name'] or p['upgrade'])): + module.exit_json(changed=True, + msg='Would have updated the package cache') + + if p['upgrade']: + upgrade(module, xbps_path) + + if p['name']: + pkgs = p['name'] + + if module.check_mode: + check_packages(module, xbps_path, pkgs, p['state']) + + if p['state'] in ['present', 'latest']: + install_packages(module, xbps_path, p['state'], pkgs) + elif p['state'] == 'absent': + remove_packages(module, xbps_path, pkgs) + + +# import module snippets +from ansible.module_utils.basic import * + +if __name__ == "__main__": + main() From ca3eb7c89082682ff366546577c3db36945bf5ed Mon Sep 17 00:00:00 2001 From: "Ryan S. Brown" Date: Tue, 6 Dec 2016 08:41:16 -0500 Subject: [PATCH 2517/2522] Style fix for xbps module --- packaging/os/xbps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/os/xbps.py b/packaging/os/xbps.py index c7b7e6ee472..ad3a90b9395 100644 --- a/packaging/os/xbps.py +++ b/packaging/os/xbps.py @@ -235,7 +235,7 @@ def main(): argument_spec=dict( name=dict(default=None, aliases=['pkg', 'package'], type='list'), state=dict(default='present', choices=['present', 'installed', - "latest", 'absent', + 'latest', 'absent', 'removed']), recurse=dict(default=False, type='bool'), force=dict(default=False, type='bool'), From 6c7d63b15c77126b4d6a8a7668545555578469c5 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Tue, 6 Dec 2016 02:35:25 -0800 Subject: [PATCH 2518/2522] Refreshed metadata for extras modules --- cloud/amazon/cloudformation_facts.py | 4 ++++ cloud/amazon/cloudtrail.py | 4 ++++ cloud/amazon/cloudwatchevent_rule.py | 4 ++++ cloud/amazon/dynamodb_table.py | 4 ++++ cloud/amazon/ec2_ami_copy.py | 4 ++++ cloud/amazon/ec2_asg_facts.py | 4 ++++ cloud/amazon/ec2_customer_gateway.py | 4 ++++ cloud/amazon/ec2_elb_facts.py | 4 ++++ cloud/amazon/ec2_eni.py | 4 ++++ cloud/amazon/ec2_eni_facts.py | 4 ++++ cloud/amazon/ec2_group_facts.py | 4 ++++ cloud/amazon/ec2_lc_facts.py | 4 ++++ cloud/amazon/ec2_lc_find.py | 4 ++++ cloud/amazon/ec2_remote_facts.py | 4 ++++ cloud/amazon/ec2_snapshot_facts.py | 4 ++++ cloud/amazon/ec2_vol_facts.py | 4 ++++ cloud/amazon/ec2_vpc_dhcp_options.py | 4 ++++ cloud/amazon/ec2_vpc_dhcp_options_facts.py | 4 ++++ cloud/amazon/ec2_vpc_igw.py | 4 ++++ cloud/amazon/ec2_vpc_nacl.py | 4 ++++ cloud/amazon/ec2_vpc_nacl_facts.py | 4 ++++ cloud/amazon/ec2_vpc_nat_gateway.py | 4 ++++ cloud/amazon/ec2_vpc_net_facts.py | 4 ++++ cloud/amazon/ec2_vpc_peer.py | 4 ++++ cloud/amazon/ec2_vpc_route_table.py | 4 ++++ cloud/amazon/ec2_vpc_route_table_facts.py | 4 ++++ cloud/amazon/ec2_vpc_subnet.py | 4 ++++ cloud/amazon/ec2_vpc_subnet_facts.py | 4 ++++ cloud/amazon/ec2_vpc_vgw.py | 4 ++++ cloud/amazon/ec2_win_password.py | 4 ++++ cloud/amazon/ecs_cluster.py | 4 ++++ cloud/amazon/ecs_service.py | 4 ++++ cloud/amazon/ecs_service_facts.py | 4 ++++ cloud/amazon/ecs_task.py | 4 ++++ cloud/amazon/ecs_taskdefinition.py | 4 ++++ cloud/amazon/efs.py | 4 ++++ cloud/amazon/efs_facts.py | 4 ++++ cloud/amazon/execute_lambda.py | 4 ++++ cloud/amazon/iam_mfa_device_facts.py | 4 ++++ cloud/amazon/iam_server_certificate_facts.py | 4 ++++ cloud/amazon/kinesis_stream.py | 4 ++++ cloud/amazon/lambda.py | 4 ++++ cloud/amazon/lambda_alias.py | 4 ++++ cloud/amazon/lambda_event.py | 4 ++++ cloud/amazon/lambda_facts.py | 4 ++++ cloud/amazon/redshift.py | 4 ++++ cloud/amazon/redshift_subnet_group.py | 4 ++++ cloud/amazon/route53_facts.py | 4 ++++ cloud/amazon/route53_health_check.py | 4 ++++ cloud/amazon/route53_zone.py | 4 ++++ cloud/amazon/s3_bucket.py | 4 ++++ cloud/amazon/s3_lifecycle.py | 4 ++++ cloud/amazon/s3_logging.py | 4 ++++ cloud/amazon/s3_website.py | 4 ++++ cloud/amazon/sns_topic.py | 4 ++++ cloud/amazon/sqs_queue.py | 4 ++++ cloud/amazon/sts_assume_role.py | 4 ++++ cloud/amazon/sts_session_token.py | 4 ++++ cloud/atomic/atomic_host.py | 4 ++++ cloud/atomic/atomic_image.py | 4 ++++ cloud/azure/azure_rm_deployment.py | 4 ++++ cloud/centurylink/clc_aa_policy.py | 4 ++++ cloud/centurylink/clc_alert_policy.py | 4 ++++ cloud/centurylink/clc_blueprint_package.py | 4 ++++ cloud/centurylink/clc_firewall_policy.py | 4 ++++ cloud/centurylink/clc_group.py | 4 ++++ cloud/centurylink/clc_loadbalancer.py | 4 ++++ cloud/centurylink/clc_modify_server.py | 4 ++++ cloud/centurylink/clc_publicip.py | 4 ++++ cloud/centurylink/clc_server.py | 4 ++++ cloud/centurylink/clc_server_snapshot.py | 4 ++++ cloud/cloudstack/cs_account.py | 4 ++++ cloud/cloudstack/cs_affinitygroup.py | 4 ++++ cloud/cloudstack/cs_cluster.py | 4 ++++ cloud/cloudstack/cs_configuration.py | 4 ++++ cloud/cloudstack/cs_domain.py | 4 ++++ cloud/cloudstack/cs_facts.py | 4 ++++ cloud/cloudstack/cs_firewall.py | 4 ++++ cloud/cloudstack/cs_instance.py | 4 ++++ cloud/cloudstack/cs_instance_facts.py | 4 ++++ cloud/cloudstack/cs_instancegroup.py | 4 ++++ cloud/cloudstack/cs_ip_address.py | 4 ++++ cloud/cloudstack/cs_iso.py | 4 ++++ cloud/cloudstack/cs_loadbalancer_rule.py | 4 ++++ cloud/cloudstack/cs_loadbalancer_rule_member.py | 4 ++++ cloud/cloudstack/cs_network.py | 4 ++++ cloud/cloudstack/cs_nic.py | 4 ++++ cloud/cloudstack/cs_pod.py | 4 ++++ cloud/cloudstack/cs_portforward.py | 4 ++++ cloud/cloudstack/cs_project.py | 4 ++++ cloud/cloudstack/cs_region.py | 4 ++++ cloud/cloudstack/cs_resourcelimit.py | 4 ++++ cloud/cloudstack/cs_router.py | 4 ++++ cloud/cloudstack/cs_securitygroup.py | 4 ++++ cloud/cloudstack/cs_securitygroup_rule.py | 4 ++++ cloud/cloudstack/cs_snapshot_policy.py | 4 ++++ cloud/cloudstack/cs_sshkeypair.py | 4 ++++ cloud/cloudstack/cs_staticnat.py | 4 ++++ cloud/cloudstack/cs_template.py | 4 ++++ cloud/cloudstack/cs_user.py | 4 ++++ cloud/cloudstack/cs_vmsnapshot.py | 4 ++++ cloud/cloudstack/cs_volume.py | 4 ++++ cloud/cloudstack/cs_vpc.py | 4 ++++ cloud/cloudstack/cs_zone.py | 4 ++++ cloud/cloudstack/cs_zone_facts.py | 4 ++++ cloud/google/gcdns_record.py | 4 ++++ cloud/google/gcdns_zone.py | 4 ++++ cloud/google/gce_img.py | 4 ++++ cloud/google/gce_tag.py | 4 ++++ cloud/lxc/lxc_container.py | 4 ++++ cloud/lxd/lxd_container.py | 4 ++++ cloud/lxd/lxd_profile.py | 4 ++++ cloud/misc/ovirt.py | 4 ++++ cloud/misc/proxmox.py | 4 ++++ cloud/misc/proxmox_kvm.py | 4 ++++ cloud/misc/proxmox_template.py | 4 ++++ cloud/misc/rhevm.py | 4 ++++ cloud/misc/virt.py | 4 ++++ cloud/misc/virt_net.py | 4 ++++ cloud/misc/virt_pool.py | 4 ++++ cloud/openstack/os_flavor_facts.py | 4 ++++ cloud/openstack/os_group.py | 4 ++++ cloud/openstack/os_ironic_inspect.py | 4 ++++ cloud/openstack/os_keystone_domain.py | 4 ++++ cloud/openstack/os_keystone_domain_facts.py | 4 ++++ cloud/openstack/os_keystone_role.py | 4 ++++ cloud/openstack/os_keystone_service.py | 4 ++++ cloud/openstack/os_port_facts.py | 4 ++++ cloud/openstack/os_project.py | 4 ++++ cloud/openstack/os_project_facts.py | 4 ++++ cloud/openstack/os_recordset.py | 4 ++++ cloud/openstack/os_server_group.py | 4 ++++ cloud/openstack/os_stack.py | 4 ++++ cloud/openstack/os_user_facts.py | 4 ++++ cloud/openstack/os_user_role.py | 4 ++++ cloud/openstack/os_zone.py | 4 ++++ cloud/ovh/ovh_ip_loadbalancing_backend.py | 4 ++++ cloud/ovirt/ovirt_affinity_labels.py | 4 ++++ cloud/ovirt/ovirt_affinity_labels_facts.py | 4 ++++ cloud/ovirt/ovirt_auth.py | 4 ++++ cloud/ovirt/ovirt_clusters.py | 4 ++++ cloud/ovirt/ovirt_clusters_facts.py | 4 ++++ cloud/ovirt/ovirt_datacenters.py | 4 ++++ cloud/ovirt/ovirt_datacenters_facts.py | 4 ++++ cloud/ovirt/ovirt_disks.py | 4 ++++ cloud/ovirt/ovirt_external_providers.py | 4 ++++ cloud/ovirt/ovirt_external_providers_facts.py | 4 ++++ cloud/ovirt/ovirt_groups.py | 4 ++++ cloud/ovirt/ovirt_groups_facts.py | 4 ++++ cloud/ovirt/ovirt_host_networks.py | 4 ++++ cloud/ovirt/ovirt_host_pm.py | 4 ++++ cloud/ovirt/ovirt_hosts.py | 4 ++++ cloud/ovirt/ovirt_hosts_facts.py | 4 ++++ cloud/ovirt/ovirt_mac_pools.py | 4 ++++ cloud/ovirt/ovirt_networks.py | 4 ++++ cloud/ovirt/ovirt_networks_facts.py | 4 ++++ cloud/ovirt/ovirt_nics.py | 4 ++++ cloud/ovirt/ovirt_nics_facts.py | 4 ++++ cloud/ovirt/ovirt_permissions.py | 4 ++++ cloud/ovirt/ovirt_permissions_facts.py | 4 ++++ cloud/ovirt/ovirt_quotas.py | 4 ++++ cloud/ovirt/ovirt_quotas_facts.py | 4 ++++ cloud/ovirt/ovirt_storage_domains.py | 4 ++++ cloud/ovirt/ovirt_storage_domains_facts.py | 4 ++++ cloud/ovirt/ovirt_templates.py | 4 ++++ cloud/ovirt/ovirt_templates_facts.py | 4 ++++ cloud/ovirt/ovirt_users.py | 4 ++++ cloud/ovirt/ovirt_users_facts.py | 4 ++++ cloud/ovirt/ovirt_vmpools.py | 4 ++++ cloud/ovirt/ovirt_vmpools_facts.py | 4 ++++ cloud/ovirt/ovirt_vms.py | 4 ++++ cloud/ovirt/ovirt_vms_facts.py | 4 ++++ cloud/profitbricks/profitbricks.py | 4 ++++ cloud/profitbricks/profitbricks_datacenter.py | 4 ++++ cloud/profitbricks/profitbricks_nic.py | 4 ++++ cloud/profitbricks/profitbricks_volume.py | 4 ++++ cloud/profitbricks/profitbricks_volume_attachments.py | 4 ++++ cloud/rackspace/rax_clb_ssl.py | 4 ++++ cloud/rackspace/rax_mon_alarm.py | 4 ++++ cloud/rackspace/rax_mon_check.py | 4 ++++ cloud/rackspace/rax_mon_entity.py | 4 ++++ cloud/rackspace/rax_mon_notification.py | 4 ++++ cloud/rackspace/rax_mon_notification_plan.py | 4 ++++ cloud/serverless.py | 4 ++++ cloud/smartos/smartos_image_facts.py | 4 ++++ cloud/softlayer/sl_vm.py | 4 ++++ cloud/vmware/vca_fw.py | 4 ++++ cloud/vmware/vca_nat.py | 4 ++++ cloud/vmware/vca_vapp.py | 4 ++++ cloud/vmware/vmware_cluster.py | 4 ++++ cloud/vmware/vmware_datacenter.py | 4 ++++ cloud/vmware/vmware_dns_config.py | 4 ++++ cloud/vmware/vmware_dvs_host.py | 4 ++++ cloud/vmware/vmware_dvs_portgroup.py | 4 ++++ cloud/vmware/vmware_dvswitch.py | 4 ++++ cloud/vmware/vmware_guest.py | 4 ++++ cloud/vmware/vmware_host.py | 4 ++++ cloud/vmware/vmware_local_user_manager.py | 4 ++++ cloud/vmware/vmware_maintenancemode.py | 4 ++++ cloud/vmware/vmware_migrate_vmk.py | 4 ++++ cloud/vmware/vmware_portgroup.py | 4 ++++ cloud/vmware/vmware_target_canonical_facts.py | 4 ++++ cloud/vmware/vmware_vm_facts.py | 4 ++++ cloud/vmware/vmware_vm_shell.py | 4 ++++ cloud/vmware/vmware_vm_vss_dvs_migrate.py | 4 ++++ cloud/vmware/vmware_vmkernel.py | 4 ++++ cloud/vmware/vmware_vmkernel_ip_config.py | 4 ++++ cloud/vmware/vmware_vmotion.py | 4 ++++ cloud/vmware/vmware_vsan_cluster.py | 4 ++++ cloud/vmware/vmware_vswitch.py | 4 ++++ cloud/vmware/vsphere_copy.py | 4 ++++ cloud/webfaction/webfaction_app.py | 4 ++++ cloud/webfaction/webfaction_db.py | 4 ++++ cloud/webfaction/webfaction_domain.py | 4 ++++ cloud/webfaction/webfaction_mailbox.py | 4 ++++ cloud/webfaction/webfaction_site.py | 4 ++++ cloud/xenserver_facts.py | 4 ++++ clustering/consul.py | 4 ++++ clustering/consul_acl.py | 4 ++++ clustering/consul_kv.py | 4 ++++ clustering/consul_session.py | 4 ++++ clustering/kubernetes.py | 4 ++++ clustering/znode.py | 4 ++++ commands/expect.py | 4 ++++ crypto/openssl_privatekey.py | 4 ++++ crypto/openssl_publickey.py | 4 ++++ database/influxdb/influxdb_database.py | 4 ++++ database/influxdb/influxdb_retention_policy.py | 4 ++++ database/misc/mongodb_parameter.py | 4 ++++ database/misc/mongodb_user.py | 4 ++++ database/misc/redis.py | 4 ++++ database/misc/riak.py | 4 ++++ database/mssql/mssql_db.py | 4 ++++ database/mysql/mysql_replication.py | 4 ++++ database/postgresql/postgresql_ext.py | 4 ++++ database/postgresql/postgresql_lang.py | 4 ++++ database/postgresql/postgresql_schema.py | 4 ++++ database/vertica/vertica_configuration.py | 4 ++++ database/vertica/vertica_facts.py | 4 ++++ database/vertica/vertica_role.py | 4 ++++ database/vertica/vertica_schema.py | 4 ++++ database/vertica/vertica_user.py | 4 ++++ files/archive.py | 4 ++++ files/blockinfile.py | 4 ++++ files/patch.py | 4 ++++ files/tempfile.py | 4 ++++ identity/ipa/ipa_group.py | 4 ++++ identity/ipa/ipa_hbacrule.py | 4 ++++ identity/ipa/ipa_host.py | 4 ++++ identity/ipa/ipa_hostgroup.py | 4 ++++ identity/ipa/ipa_role.py | 4 ++++ identity/ipa/ipa_sudocmd.py | 4 ++++ identity/ipa/ipa_sudocmdgroup.py | 4 ++++ identity/ipa/ipa_sudorule.py | 4 ++++ identity/ipa/ipa_user.py | 4 ++++ identity/opendj/opendj_backendprop.py | 4 ++++ infrastructure/foreman/foreman.py | 4 ++++ infrastructure/foreman/katello.py | 4 ++++ messaging/rabbitmq_binding.py | 4 ++++ messaging/rabbitmq_exchange.py | 4 ++++ messaging/rabbitmq_parameter.py | 4 ++++ messaging/rabbitmq_plugin.py | 4 ++++ messaging/rabbitmq_policy.py | 4 ++++ messaging/rabbitmq_queue.py | 4 ++++ messaging/rabbitmq_user.py | 4 ++++ messaging/rabbitmq_vhost.py | 4 ++++ monitoring/airbrake_deployment.py | 4 ++++ monitoring/bigpanda.py | 4 ++++ monitoring/boundary_meter.py | 4 ++++ monitoring/circonus_annotation.py | 4 ++++ monitoring/datadog_event.py | 4 ++++ monitoring/datadog_monitor.py | 4 ++++ monitoring/honeybadger_deployment.py | 4 ++++ monitoring/librato_annotation.py | 4 ++++ monitoring/logentries.py | 4 ++++ monitoring/logicmonitor.py | 4 ++++ monitoring/logicmonitor_facts.py | 4 ++++ monitoring/monit.py | 4 ++++ monitoring/nagios.py | 4 ++++ monitoring/newrelic_deployment.py | 4 ++++ monitoring/pagerduty.py | 4 ++++ monitoring/pagerduty_alert.py | 4 ++++ monitoring/pingdom.py | 4 ++++ monitoring/rollbar_deployment.py | 4 ++++ monitoring/sensu_check.py | 4 ++++ monitoring/sensu_subscription.py | 4 ++++ monitoring/stackdriver.py | 4 ++++ monitoring/statusio_maintenance.py | 4 ++++ monitoring/uptimerobot.py | 4 ++++ monitoring/zabbix_group.py | 4 ++++ monitoring/zabbix_host.py | 4 ++++ monitoring/zabbix_hostmacro.py | 4 ++++ monitoring/zabbix_maintenance.py | 4 ++++ monitoring/zabbix_screen.py | 4 ++++ network/a10/a10_server.py | 4 ++++ network/a10/a10_server_axapi3.py | 4 ++++ network/a10/a10_service_group.py | 4 ++++ network/a10/a10_virtual_server.py | 4 ++++ network/asa/asa_acl.py | 4 ++++ network/asa/asa_command.py | 4 ++++ network/asa/asa_config.py | 4 ++++ network/citrix/netscaler.py | 4 ++++ network/cloudflare_dns.py | 4 ++++ network/dnsimple.py | 4 ++++ network/dnsmadeeasy.py | 4 ++++ network/exoscale/exo_dns_domain.py | 4 ++++ network/exoscale/exo_dns_record.py | 4 ++++ network/f5/bigip_device_dns.py | 4 ++++ network/f5/bigip_device_ntp.py | 4 ++++ network/f5/bigip_device_sshd.py | 4 ++++ network/f5/bigip_facts.py | 4 ++++ network/f5/bigip_gtm_datacenter.py | 4 ++++ network/f5/bigip_gtm_facts.py | 4 ++++ network/f5/bigip_gtm_virtual_server.py | 4 ++++ network/f5/bigip_gtm_wide_ip.py | 4 ++++ network/f5/bigip_hostname.py | 4 ++++ network/f5/bigip_irule.py | 4 ++++ network/f5/bigip_monitor_http.py | 4 ++++ network/f5/bigip_monitor_tcp.py | 4 ++++ network/f5/bigip_node.py | 4 ++++ network/f5/bigip_pool.py | 4 ++++ network/f5/bigip_pool_member.py | 4 ++++ network/f5/bigip_routedomain.py | 4 ++++ network/f5/bigip_selfip.py | 4 ++++ network/f5/bigip_snat_pool.py | 4 ++++ network/f5/bigip_ssl_certificate.py | 4 ++++ network/f5/bigip_sys_db.py | 4 ++++ network/f5/bigip_sys_global.py | 4 ++++ network/f5/bigip_virtual_server.py | 4 ++++ network/f5/bigip_vlan.py | 4 ++++ network/haproxy.py | 4 ++++ network/illumos/dladm_etherstub.py | 4 ++++ network/illumos/dladm_vnic.py | 4 ++++ network/illumos/flowadm.py | 4 ++++ network/illumos/ipadm_if.py | 4 ++++ network/illumos/ipadm_prop.py | 4 ++++ network/ipify_facts.py | 4 ++++ network/ipinfoio_facts.py | 4 ++++ network/lldp.py | 4 ++++ network/netconf/netconf_config.py | 4 ++++ network/nmcli.py | 4 ++++ network/openvswitch_bridge.py | 4 ++++ network/openvswitch_db.py | 4 ++++ network/openvswitch_port.py | 4 ++++ network/panos/panos_admin.py | 4 ++++ network/snmp_facts.py | 4 ++++ network/wakeonlan.py | 4 ++++ notification/campfire.py | 4 ++++ notification/flowdock.py | 4 ++++ notification/grove.py | 4 ++++ notification/hall.py | 4 ++++ notification/hipchat.py | 4 ++++ notification/irc.py | 4 ++++ notification/jabber.py | 4 ++++ notification/mail.py | 4 ++++ notification/mqtt.py | 4 ++++ notification/nexmo.py | 4 ++++ notification/osx_say.py | 4 ++++ notification/pushbullet.py | 4 ++++ notification/pushover.py | 4 ++++ notification/rocketchat.py | 4 ++++ notification/sendgrid.py | 4 ++++ notification/slack.py | 4 ++++ notification/sns.py | 4 ++++ notification/telegram.py | 4 ++++ notification/twilio.py | 4 ++++ notification/typetalk.py | 4 ++++ packaging/dpkg_selections.py | 4 ++++ packaging/elasticsearch_plugin.py | 4 ++++ packaging/kibana_plugin.py | 4 ++++ packaging/language/bower.py | 4 ++++ packaging/language/bundler.py | 4 ++++ packaging/language/composer.py | 4 ++++ packaging/language/cpanm.py | 4 ++++ packaging/language/maven_artifact.py | 4 ++++ packaging/language/npm.py | 4 ++++ packaging/language/pear.py | 4 ++++ packaging/os/apk.py | 4 ++++ packaging/os/dnf.py | 4 ++++ packaging/os/homebrew.py | 4 ++++ packaging/os/homebrew_cask.py | 4 ++++ packaging/os/homebrew_tap.py | 4 ++++ packaging/os/layman.py | 4 ++++ packaging/os/macports.py | 4 ++++ packaging/os/openbsd_pkg.py | 4 ++++ packaging/os/opkg.py | 4 ++++ packaging/os/pacman.py | 4 ++++ packaging/os/pkg5.py | 4 ++++ packaging/os/pkg5_publisher.py | 4 ++++ packaging/os/pkgin.py | 4 ++++ packaging/os/pkgng.py | 4 ++++ packaging/os/pkgutil.py | 4 ++++ packaging/os/portage.py | 4 ++++ packaging/os/portinstall.py | 4 ++++ packaging/os/slackpkg.py | 4 ++++ packaging/os/svr4pkg.py | 4 ++++ packaging/os/swdepot.py | 4 ++++ packaging/os/urpmi.py | 4 ++++ packaging/os/yum_repository.py | 4 ++++ packaging/os/zypper.py | 4 ++++ packaging/os/zypper_repository.py | 4 ++++ remote_management/ipmi/ipmi_boot.py | 4 ++++ remote_management/ipmi/ipmi_power.py | 4 ++++ source_control/bzr.py | 4 ++++ source_control/git_config.py | 4 ++++ source_control/github_hooks.py | 4 ++++ source_control/github_key.py | 4 ++++ source_control/github_release.py | 4 ++++ source_control/gitlab_group.py | 4 ++++ source_control/gitlab_project.py | 4 ++++ source_control/gitlab_user.py | 4 ++++ storage/netapp/netapp_e_amg.py | 4 ++++ storage/netapp/netapp_e_amg_role.py | 4 ++++ storage/netapp/netapp_e_amg_sync.py | 4 ++++ storage/netapp/netapp_e_auth.py | 4 ++++ storage/netapp/netapp_e_facts.py | 4 ++++ storage/netapp/netapp_e_flashcache.py | 4 ++++ storage/netapp/netapp_e_host.py | 4 ++++ storage/netapp/netapp_e_hostgroup.py | 4 ++++ storage/netapp/netapp_e_lun_mapping.py | 4 ++++ storage/netapp/netapp_e_snapshot_group.py | 4 ++++ storage/netapp/netapp_e_snapshot_images.py | 4 ++++ storage/netapp/netapp_e_snapshot_volume.py | 4 ++++ storage/netapp/netapp_e_storage_system.py | 4 ++++ storage/netapp/netapp_e_storagepool.py | 4 ++++ storage/netapp/netapp_e_volume.py | 4 ++++ storage/netapp/netapp_e_volume_copy.py | 4 ++++ system/alternatives.py | 4 ++++ system/at.py | 4 ++++ system/capabilities.py | 4 ++++ system/cronvar.py | 4 ++++ system/crypttab.py | 4 ++++ system/debconf.py | 4 ++++ system/facter.py | 4 ++++ system/filesystem.py | 4 ++++ system/firewalld.py | 4 ++++ system/getent.py | 4 ++++ system/gluster_volume.py | 4 ++++ system/iptables.py | 4 ++++ system/kernel_blacklist.py | 4 ++++ system/known_hosts.py | 4 ++++ system/locale_gen.py | 4 ++++ system/lvg.py | 4 ++++ system/lvol.py | 4 ++++ system/make.py | 4 ++++ system/modprobe.py | 4 ++++ system/ohai.py | 4 ++++ system/open_iscsi.py | 4 ++++ system/openwrt_init.py | 4 ++++ system/osx_defaults.py | 4 ++++ system/pam_limits.py | 4 ++++ system/puppet.py | 4 ++++ system/sefcontext.py | 4 ++++ system/selinux_permissive.py | 4 ++++ system/seport.py | 4 ++++ system/solaris_zone.py | 4 ++++ system/svc.py | 4 ++++ system/timezone.py | 4 ++++ system/ufw.py | 4 ++++ system/zfs.py | 4 ++++ univention/udm_dns_record.py | 4 ++++ univention/udm_dns_zone.py | 4 ++++ univention/udm_group.py | 4 ++++ univention/udm_share.py | 4 ++++ univention/udm_user.py | 4 ++++ web_infrastructure/apache2_mod_proxy.py | 4 ++++ web_infrastructure/deploy_helper.py | 4 ++++ web_infrastructure/ejabberd_user.py | 4 ++++ web_infrastructure/jboss.py | 4 ++++ web_infrastructure/jenkins_job.py | 4 ++++ web_infrastructure/jenkins_plugin.py | 4 ++++ web_infrastructure/jira.py | 4 ++++ web_infrastructure/letsencrypt.py | 4 ++++ web_infrastructure/nginx_status_facts.py | 4 ++++ web_infrastructure/taiga_issue.py | 4 ++++ windows/win_acl.py | 4 ++++ windows/win_acl_inheritance.py | 4 ++++ windows/win_chocolatey.py | 4 ++++ windows/win_dotnet_ngen.py | 4 ++++ windows/win_environment.py | 4 ++++ windows/win_file_version.py | 4 ++++ windows/win_firewall_rule.py | 4 ++++ windows/win_iis_virtualdirectory.py | 4 ++++ windows/win_iis_webapplication.py | 4 ++++ windows/win_iis_webapppool.py | 4 ++++ windows/win_iis_webbinding.py | 4 ++++ windows/win_iis_website.py | 4 ++++ windows/win_nssm.py | 4 ++++ windows/win_owner.py | 4 ++++ windows/win_package.py | 4 ++++ windows/win_regedit.py | 4 ++++ windows/win_regmerge.py | 4 ++++ windows/win_robocopy.py | 4 ++++ windows/win_say.py | 4 ++++ windows/win_scheduled_task.py | 4 ++++ windows/win_share.py | 4 ++++ windows/win_timezone.py | 4 ++++ windows/win_unzip.py | 4 ++++ windows/win_updates.py | 4 ++++ windows/win_uri.py | 4 ++++ windows/win_webpicmd.py | 4 ++++ 501 files changed, 2004 insertions(+) diff --git a/cloud/amazon/cloudformation_facts.py b/cloud/amazon/cloudformation_facts.py index 0e502ce5a33..ae40ed0242d 100644 --- a/cloud/amazon/cloudformation_facts.py +++ b/cloud/amazon/cloudformation_facts.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cloudformation_facts diff --git a/cloud/amazon/cloudtrail.py b/cloud/amazon/cloudtrail.py index 8a8fad33415..ab4652fccd4 100644 --- a/cloud/amazon/cloudtrail.py +++ b/cloud/amazon/cloudtrail.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: cloudtrail diff --git a/cloud/amazon/cloudwatchevent_rule.py b/cloud/amazon/cloudwatchevent_rule.py index 45b69cabf1e..643343d82fb 100644 --- a/cloud/amazon/cloudwatchevent_rule.py +++ b/cloud/amazon/cloudwatchevent_rule.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cloudwatchevent_rule diff --git a/cloud/amazon/dynamodb_table.py b/cloud/amazon/dynamodb_table.py index 3799c0ffe35..75e410d4b71 100644 --- a/cloud/amazon/dynamodb_table.py +++ b/cloud/amazon/dynamodb_table.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: dynamodb_table diff --git a/cloud/amazon/ec2_ami_copy.py b/cloud/amazon/ec2_ami_copy.py index c053eb31aaf..71b3c611a8f 100644 --- a/cloud/amazon/ec2_ami_copy.py +++ b/cloud/amazon/ec2_ami_copy.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_ami_copy diff --git a/cloud/amazon/ec2_asg_facts.py b/cloud/amazon/ec2_asg_facts.py index d6eb1dc6119..3cd6e678605 100644 --- a/cloud/amazon/ec2_asg_facts.py +++ b/cloud/amazon/ec2_asg_facts.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_asg_facts diff --git a/cloud/amazon/ec2_customer_gateway.py b/cloud/amazon/ec2_customer_gateway.py index 0138e8399e6..a8a74926cdd 100644 --- a/cloud/amazon/ec2_customer_gateway.py +++ b/cloud/amazon/ec2_customer_gateway.py @@ -13,6 +13,10 @@ # You should have received a copy of the GNU General Public License # along with this library. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_customer_gateway diff --git a/cloud/amazon/ec2_elb_facts.py b/cloud/amazon/ec2_elb_facts.py index 1c6fb747638..c4857f6a3cd 100644 --- a/cloud/amazon/ec2_elb_facts.py +++ b/cloud/amazon/ec2_elb_facts.py @@ -13,6 +13,10 @@ # You should have received a copy of the GNU General Public License # along with this library. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_elb_facts diff --git a/cloud/amazon/ec2_eni.py b/cloud/amazon/ec2_eni.py index 6946ec6db20..aca78a459da 100644 --- a/cloud/amazon/ec2_eni.py +++ b/cloud/amazon/ec2_eni.py @@ -13,6 +13,10 @@ # You should have received a copy of the GNU General Public License # along with this library. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_eni diff --git a/cloud/amazon/ec2_eni_facts.py b/cloud/amazon/ec2_eni_facts.py index 116c8c744ec..4c6882e127c 100644 --- a/cloud/amazon/ec2_eni_facts.py +++ b/cloud/amazon/ec2_eni_facts.py @@ -13,6 +13,10 @@ # You should have received a copy of the GNU General Public License # along with this library. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_eni_facts diff --git a/cloud/amazon/ec2_group_facts.py b/cloud/amazon/ec2_group_facts.py index 6e4039f8b55..ccb4aa64e30 100644 --- a/cloud/amazon/ec2_group_facts.py +++ b/cloud/amazon/ec2_group_facts.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_group_facts diff --git a/cloud/amazon/ec2_lc_facts.py b/cloud/amazon/ec2_lc_facts.py index ea94b749f4a..b81ce8975b6 100644 --- a/cloud/amazon/ec2_lc_facts.py +++ b/cloud/amazon/ec2_lc_facts.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with this library. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_lc_facts diff --git a/cloud/amazon/ec2_lc_find.py b/cloud/amazon/ec2_lc_find.py index e5463e4ce80..d6c515d6ffe 100644 --- a/cloud/amazon/ec2_lc_find.py +++ b/cloud/amazon/ec2_lc_find.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with this software. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: ec2_lc_find diff --git a/cloud/amazon/ec2_remote_facts.py b/cloud/amazon/ec2_remote_facts.py index 2e29b798003..98ea16628fa 100644 --- a/cloud/amazon/ec2_remote_facts.py +++ b/cloud/amazon/ec2_remote_facts.py @@ -13,6 +13,10 @@ # You should have received a copy of the GNU General Public License # along with this library. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_remote_facts diff --git a/cloud/amazon/ec2_snapshot_facts.py b/cloud/amazon/ec2_snapshot_facts.py index 2ce0619441f..1fd91960983 100644 --- a/cloud/amazon/ec2_snapshot_facts.py +++ b/cloud/amazon/ec2_snapshot_facts.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_snapshot_facts diff --git a/cloud/amazon/ec2_vol_facts.py b/cloud/amazon/ec2_vol_facts.py index 5d099d0fb8d..14f5282eca7 100644 --- a/cloud/amazon/ec2_vol_facts.py +++ b/cloud/amazon/ec2_vol_facts.py @@ -13,6 +13,10 @@ # You should have received a copy of the GNU General Public License # along with this library. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_vol_facts diff --git a/cloud/amazon/ec2_vpc_dhcp_options.py b/cloud/amazon/ec2_vpc_dhcp_options.py index 198d0637177..4caee644519 100644 --- a/cloud/amazon/ec2_vpc_dhcp_options.py +++ b/cloud/amazon/ec2_vpc_dhcp_options.py @@ -13,6 +13,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = """ --- module: ec2_vpc_dhcp_options diff --git a/cloud/amazon/ec2_vpc_dhcp_options_facts.py b/cloud/amazon/ec2_vpc_dhcp_options_facts.py index a60a2104892..063f525ea0f 100644 --- a/cloud/amazon/ec2_vpc_dhcp_options_facts.py +++ b/cloud/amazon/ec2_vpc_dhcp_options_facts.py @@ -13,6 +13,10 @@ # You should have received a copy of the GNU General Public License # along with this library. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_vpc_dhcp_options_facts diff --git a/cloud/amazon/ec2_vpc_igw.py b/cloud/amazon/ec2_vpc_igw.py index fc057bae4b9..91366f35ac6 100644 --- a/cloud/amazon/ec2_vpc_igw.py +++ b/cloud/amazon/ec2_vpc_igw.py @@ -13,6 +13,10 @@ # You should have received a copy of the GNU General Public License # along with this library. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_vpc_igw diff --git a/cloud/amazon/ec2_vpc_nacl.py b/cloud/amazon/ec2_vpc_nacl.py index b3d4f197214..1758e288c61 100644 --- a/cloud/amazon/ec2_vpc_nacl.py +++ b/cloud/amazon/ec2_vpc_nacl.py @@ -13,6 +13,10 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' module: ec2_vpc_nacl short_description: create and delete Network ACLs. diff --git a/cloud/amazon/ec2_vpc_nacl_facts.py b/cloud/amazon/ec2_vpc_nacl_facts.py index b809642c714..e7f6a5b2380 100644 --- a/cloud/amazon/ec2_vpc_nacl_facts.py +++ b/cloud/amazon/ec2_vpc_nacl_facts.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_vpc_nacl_facts diff --git a/cloud/amazon/ec2_vpc_nat_gateway.py b/cloud/amazon/ec2_vpc_nat_gateway.py index ee53d7bb138..f3f95c107e6 100644 --- a/cloud/amazon/ec2_vpc_nat_gateway.py +++ b/cloud/amazon/ec2_vpc_nat_gateway.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_vpc_nat_gateway diff --git a/cloud/amazon/ec2_vpc_net_facts.py b/cloud/amazon/ec2_vpc_net_facts.py index dfa29ba9d20..14e1c4920f5 100644 --- a/cloud/amazon/ec2_vpc_net_facts.py +++ b/cloud/amazon/ec2_vpc_net_facts.py @@ -13,6 +13,10 @@ # You should have received a copy of the GNU General Public License # along with this library. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_vpc_net_facts diff --git a/cloud/amazon/ec2_vpc_peer.py b/cloud/amazon/ec2_vpc_peer.py index 3eb6582d0f7..6615ba38a27 100644 --- a/cloud/amazon/ec2_vpc_peer.py +++ b/cloud/amazon/ec2_vpc_peer.py @@ -13,6 +13,10 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' module: ec2_vpc_peer short_description: create, delete, accept, and reject VPC peering connections between two VPCs. diff --git a/cloud/amazon/ec2_vpc_route_table.py b/cloud/amazon/ec2_vpc_route_table.py index a70d60a3ce5..1529d923536 100644 --- a/cloud/amazon/ec2_vpc_route_table.py +++ b/cloud/amazon/ec2_vpc_route_table.py @@ -13,6 +13,10 @@ # You should have received a copy of the GNU General Public License # along with this library. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_vpc_route_table diff --git a/cloud/amazon/ec2_vpc_route_table_facts.py b/cloud/amazon/ec2_vpc_route_table_facts.py index 394c4b28db3..f270f2cbb2b 100644 --- a/cloud/amazon/ec2_vpc_route_table_facts.py +++ b/cloud/amazon/ec2_vpc_route_table_facts.py @@ -13,6 +13,10 @@ # You should have received a copy of the GNU General Public License # along with this library. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_vpc_route_table_facts diff --git a/cloud/amazon/ec2_vpc_subnet.py b/cloud/amazon/ec2_vpc_subnet.py index ddc4f5e99cf..dc66d445864 100644 --- a/cloud/amazon/ec2_vpc_subnet.py +++ b/cloud/amazon/ec2_vpc_subnet.py @@ -13,6 +13,10 @@ # You should have received a copy of the GNU General Public License # along with this library. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_vpc_subnet diff --git a/cloud/amazon/ec2_vpc_subnet_facts.py b/cloud/amazon/ec2_vpc_subnet_facts.py index c8adce2c2c5..83b4c1cfc51 100644 --- a/cloud/amazon/ec2_vpc_subnet_facts.py +++ b/cloud/amazon/ec2_vpc_subnet_facts.py @@ -13,6 +13,10 @@ # You should have received a copy of the GNU General Public License # along with this library. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_vpc_subnet_facts diff --git a/cloud/amazon/ec2_vpc_vgw.py b/cloud/amazon/ec2_vpc_vgw.py index c3e4d1f1ce4..40eb386156b 100644 --- a/cloud/amazon/ec2_vpc_vgw.py +++ b/cloud/amazon/ec2_vpc_vgw.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' module: ec2_vpc_vgw short_description: Create and delete AWS VPN Virtual Gateways. diff --git a/cloud/amazon/ec2_win_password.py b/cloud/amazon/ec2_win_password.py index b320a24c5de..4d246d43676 100644 --- a/cloud/amazon/ec2_win_password.py +++ b/cloud/amazon/ec2_win_password.py @@ -13,6 +13,10 @@ # You should have received a copy of the GNU General Public License # along with this library. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_win_password diff --git a/cloud/amazon/ecs_cluster.py b/cloud/amazon/ecs_cluster.py index ad029d4e1f3..b1409005a8c 100644 --- a/cloud/amazon/ecs_cluster.py +++ b/cloud/amazon/ecs_cluster.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ecs_cluster diff --git a/cloud/amazon/ecs_service.py b/cloud/amazon/ecs_service.py index 3ae77e6b519..004a11b267d 100644 --- a/cloud/amazon/ecs_service.py +++ b/cloud/amazon/ecs_service.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ecs_service diff --git a/cloud/amazon/ecs_service_facts.py b/cloud/amazon/ecs_service_facts.py index aaee2aa01fd..e62b492c4b9 100644 --- a/cloud/amazon/ecs_service_facts.py +++ b/cloud/amazon/ecs_service_facts.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ecs_service_facts diff --git a/cloud/amazon/ecs_task.py b/cloud/amazon/ecs_task.py index 86c77eceb1c..a8ecc4dde48 100644 --- a/cloud/amazon/ecs_task.py +++ b/cloud/amazon/ecs_task.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ecs_task diff --git a/cloud/amazon/ecs_taskdefinition.py b/cloud/amazon/ecs_taskdefinition.py index 8f9f894e11f..4ee9003aab1 100644 --- a/cloud/amazon/ecs_taskdefinition.py +++ b/cloud/amazon/ecs_taskdefinition.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ecs_taskdefinition diff --git a/cloud/amazon/efs.py b/cloud/amazon/efs.py index 565d6ba5129..1def68daedc 100644 --- a/cloud/amazon/efs.py +++ b/cloud/amazon/efs.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: efs diff --git a/cloud/amazon/efs_facts.py b/cloud/amazon/efs_facts.py index 3b45e068ee2..aa7adf8bee1 100644 --- a/cloud/amazon/efs_facts.py +++ b/cloud/amazon/efs_facts.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: efs_facts diff --git a/cloud/amazon/execute_lambda.py b/cloud/amazon/execute_lambda.py index 03ab4264072..676d3c5e30b 100644 --- a/cloud/amazon/execute_lambda.py +++ b/cloud/amazon/execute_lambda.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: execute_lambda diff --git a/cloud/amazon/iam_mfa_device_facts.py b/cloud/amazon/iam_mfa_device_facts.py index 2b97d0bee46..539867663c3 100644 --- a/cloud/amazon/iam_mfa_device_facts.py +++ b/cloud/amazon/iam_mfa_device_facts.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: iam_mfa_device_facts diff --git a/cloud/amazon/iam_server_certificate_facts.py b/cloud/amazon/iam_server_certificate_facts.py index 259b5153204..1c8637362f3 100644 --- a/cloud/amazon/iam_server_certificate_facts.py +++ b/cloud/amazon/iam_server_certificate_facts.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: iam_server_certificate_facts diff --git a/cloud/amazon/kinesis_stream.py b/cloud/amazon/kinesis_stream.py index 6a9994daca6..b4e0f7205bf 100644 --- a/cloud/amazon/kinesis_stream.py +++ b/cloud/amazon/kinesis_stream.py @@ -13,6 +13,10 @@ # You should have received a copy of the GNU General Public License # along with this library. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: kinesis_stream diff --git a/cloud/amazon/lambda.py b/cloud/amazon/lambda.py index bbabe39949a..cef3b38e30f 100644 --- a/cloud/amazon/lambda.py +++ b/cloud/amazon/lambda.py @@ -15,6 +15,10 @@ # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: lambda diff --git a/cloud/amazon/lambda_alias.py b/cloud/amazon/lambda_alias.py index c85ecd2ee37..a06880e4101 100644 --- a/cloud/amazon/lambda_alias.py +++ b/cloud/amazon/lambda_alias.py @@ -22,6 +22,10 @@ HAS_BOTO3 = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: lambda_alias diff --git a/cloud/amazon/lambda_event.py b/cloud/amazon/lambda_event.py index ddc5a5aa951..acb057a8dee 100644 --- a/cloud/amazon/lambda_event.py +++ b/cloud/amazon/lambda_event.py @@ -24,6 +24,10 @@ HAS_BOTO3 = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: lambda_event diff --git a/cloud/amazon/lambda_facts.py b/cloud/amazon/lambda_facts.py index 9c17df1dd37..ac3db667948 100644 --- a/cloud/amazon/lambda_facts.py +++ b/cloud/amazon/lambda_facts.py @@ -25,6 +25,10 @@ HAS_BOTO3 = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: lambda_facts diff --git a/cloud/amazon/redshift.py b/cloud/amazon/redshift.py index bd6911ecc50..a1ae146a427 100644 --- a/cloud/amazon/redshift.py +++ b/cloud/amazon/redshift.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- author: diff --git a/cloud/amazon/redshift_subnet_group.py b/cloud/amazon/redshift_subnet_group.py index 113d57988c6..cecf68209ab 100644 --- a/cloud/amazon/redshift_subnet_group.py +++ b/cloud/amazon/redshift_subnet_group.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- author: diff --git a/cloud/amazon/route53_facts.py b/cloud/amazon/route53_facts.py index 2cb84b039bc..6dad5e21646 100644 --- a/cloud/amazon/route53_facts.py +++ b/cloud/amazon/route53_facts.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: route53_facts short_description: Retrieves route53 details using AWS methods diff --git a/cloud/amazon/route53_health_check.py b/cloud/amazon/route53_health_check.py index 9bbb7b3e29c..0070b3e288c 100644 --- a/cloud/amazon/route53_health_check.py +++ b/cloud/amazon/route53_health_check.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: route53_health_check diff --git a/cloud/amazon/route53_zone.py b/cloud/amazon/route53_zone.py index 002e89023f3..758860f6853 100644 --- a/cloud/amazon/route53_zone.py +++ b/cloud/amazon/route53_zone.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: route53_zone short_description: add or delete Route53 zones diff --git a/cloud/amazon/s3_bucket.py b/cloud/amazon/s3_bucket.py index 664bac29341..970967e30b0 100644 --- a/cloud/amazon/s3_bucket.py +++ b/cloud/amazon/s3_bucket.py @@ -13,6 +13,10 @@ # You should have received a copy of the GNU General Public License # along with this library. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: s3_bucket diff --git a/cloud/amazon/s3_lifecycle.py b/cloud/amazon/s3_lifecycle.py index c34b8ccf248..f981dfadb8f 100644 --- a/cloud/amazon/s3_lifecycle.py +++ b/cloud/amazon/s3_lifecycle.py @@ -13,6 +13,10 @@ # You should have received a copy of the GNU General Public License # along with this library. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: s3_lifecycle diff --git a/cloud/amazon/s3_logging.py b/cloud/amazon/s3_logging.py index 91ca1b34e4b..653e315848f 100644 --- a/cloud/amazon/s3_logging.py +++ b/cloud/amazon/s3_logging.py @@ -13,6 +13,10 @@ # You should have received a copy of the GNU General Public License # along with this library. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: s3_logging diff --git a/cloud/amazon/s3_website.py b/cloud/amazon/s3_website.py index 93de7210953..b8e1503b2d2 100644 --- a/cloud/amazon/s3_website.py +++ b/cloud/amazon/s3_website.py @@ -13,6 +13,10 @@ # You should have received a copy of the GNU General Public License # along with this library. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: s3_website diff --git a/cloud/amazon/sns_topic.py b/cloud/amazon/sns_topic.py index 34bae494051..e2b31484a1f 100644 --- a/cloud/amazon/sns_topic.py +++ b/cloud/amazon/sns_topic.py @@ -15,6 +15,10 @@ # along with this library. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = """ module: sns_topic short_description: Manages AWS SNS topics and subscriptions diff --git a/cloud/amazon/sqs_queue.py b/cloud/amazon/sqs_queue.py index 70e6d92ffc6..bad72f96bb1 100644 --- a/cloud/amazon/sqs_queue.py +++ b/cloud/amazon/sqs_queue.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = """ --- module: sqs_queue diff --git a/cloud/amazon/sts_assume_role.py b/cloud/amazon/sts_assume_role.py index da8ba9a7651..d856947a7d0 100644 --- a/cloud/amazon/sts_assume_role.py +++ b/cloud/amazon/sts_assume_role.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: sts_assume_role diff --git a/cloud/amazon/sts_session_token.py b/cloud/amazon/sts_session_token.py index 320cc1d271b..4886b625fd2 100644 --- a/cloud/amazon/sts_session_token.py +++ b/cloud/amazon/sts_session_token.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: sts_session_token diff --git a/cloud/atomic/atomic_host.py b/cloud/atomic/atomic_host.py index a697a3ea53d..ae4cb06e28c 100644 --- a/cloud/atomic/atomic_host.py +++ b/cloud/atomic/atomic_host.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public licenses # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION=''' --- module: atomic_host diff --git a/cloud/atomic/atomic_image.py b/cloud/atomic/atomic_image.py index 1011465c2c0..8210a1d3b86 100644 --- a/cloud/atomic/atomic_image.py +++ b/cloud/atomic/atomic_image.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION=''' --- module: atomic_image diff --git a/cloud/azure/azure_rm_deployment.py b/cloud/azure/azure_rm_deployment.py index a1f924adc56..88ecf0cea02 100644 --- a/cloud/azure/azure_rm_deployment.py +++ b/cloud/azure/azure_rm_deployment.py @@ -14,6 +14,10 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: azure_rm_deployment diff --git a/cloud/centurylink/clc_aa_policy.py b/cloud/centurylink/clc_aa_policy.py index bf5f800feaf..8693f4c774b 100644 --- a/cloud/centurylink/clc_aa_policy.py +++ b/cloud/centurylink/clc_aa_policy.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: clc_aa_policy short_description: Create or Delete Anti Affinity Policies at CenturyLink Cloud. diff --git a/cloud/centurylink/clc_alert_policy.py b/cloud/centurylink/clc_alert_policy.py index b8817b6618a..6e8c4618543 100644 --- a/cloud/centurylink/clc_alert_policy.py +++ b/cloud/centurylink/clc_alert_policy.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: clc_alert_policy short_description: Create or Delete Alert Policies at CenturyLink Cloud. diff --git a/cloud/centurylink/clc_blueprint_package.py b/cloud/centurylink/clc_blueprint_package.py index 4e8a392495a..8d4d28d20f8 100644 --- a/cloud/centurylink/clc_blueprint_package.py +++ b/cloud/centurylink/clc_blueprint_package.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: clc_blueprint_package short_description: deploys a blue print package on a set of servers in CenturyLink Cloud. diff --git a/cloud/centurylink/clc_firewall_policy.py b/cloud/centurylink/clc_firewall_policy.py index 78a334a6430..4ccfe171f21 100644 --- a/cloud/centurylink/clc_firewall_policy.py +++ b/cloud/centurylink/clc_firewall_policy.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: clc_firewall_policy short_description: Create/delete/update firewall policies diff --git a/cloud/centurylink/clc_group.py b/cloud/centurylink/clc_group.py index 76364a02c0f..4c522b7b0ba 100644 --- a/cloud/centurylink/clc_group.py +++ b/cloud/centurylink/clc_group.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: clc_group short_description: Create/delete Server Groups at Centurylink Cloud diff --git a/cloud/centurylink/clc_loadbalancer.py b/cloud/centurylink/clc_loadbalancer.py index dcbbfd0bc88..e159953ba3e 100644 --- a/cloud/centurylink/clc_loadbalancer.py +++ b/cloud/centurylink/clc_loadbalancer.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: clc_loadbalancer short_description: Create, Delete shared loadbalancers in CenturyLink Cloud. diff --git a/cloud/centurylink/clc_modify_server.py b/cloud/centurylink/clc_modify_server.py index a676248ffd0..d65073daccb 100644 --- a/cloud/centurylink/clc_modify_server.py +++ b/cloud/centurylink/clc_modify_server.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: clc_modify_server short_description: modify servers in CenturyLink Cloud. diff --git a/cloud/centurylink/clc_publicip.py b/cloud/centurylink/clc_publicip.py index 531f19ca07c..a53aeb79531 100644 --- a/cloud/centurylink/clc_publicip.py +++ b/cloud/centurylink/clc_publicip.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: clc_publicip short_description: Add and Delete public ips on servers in CenturyLink Cloud. diff --git a/cloud/centurylink/clc_server.py b/cloud/centurylink/clc_server.py index f98cd54e049..721582cc33c 100644 --- a/cloud/centurylink/clc_server.py +++ b/cloud/centurylink/clc_server.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: clc_server short_description: Create, Delete, Start and Stop servers in CenturyLink Cloud. diff --git a/cloud/centurylink/clc_server_snapshot.py b/cloud/centurylink/clc_server_snapshot.py index 6c7e8920e46..e176f2d779f 100644 --- a/cloud/centurylink/clc_server_snapshot.py +++ b/cloud/centurylink/clc_server_snapshot.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: clc_server_snapshot short_description: Create, Delete and Restore server snapshots in CenturyLink Cloud. diff --git a/cloud/cloudstack/cs_account.py b/cloud/cloudstack/cs_account.py index d4b27dea798..0074ad29ca3 100644 --- a/cloud/cloudstack/cs_account.py +++ b/cloud/cloudstack/cs_account.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_account diff --git a/cloud/cloudstack/cs_affinitygroup.py b/cloud/cloudstack/cs_affinitygroup.py index 2ffe2bace14..a9c71c42b0c 100644 --- a/cloud/cloudstack/cs_affinitygroup.py +++ b/cloud/cloudstack/cs_affinitygroup.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_affinitygroup diff --git a/cloud/cloudstack/cs_cluster.py b/cloud/cloudstack/cs_cluster.py index 4834c07b65d..7c9d39e6149 100644 --- a/cloud/cloudstack/cs_cluster.py +++ b/cloud/cloudstack/cs_cluster.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_cluster diff --git a/cloud/cloudstack/cs_configuration.py b/cloud/cloudstack/cs_configuration.py index 9c62daeba7d..696593550a5 100644 --- a/cloud/cloudstack/cs_configuration.py +++ b/cloud/cloudstack/cs_configuration.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_configuration diff --git a/cloud/cloudstack/cs_domain.py b/cloud/cloudstack/cs_domain.py index 17c93a84614..35e32aa0661 100644 --- a/cloud/cloudstack/cs_domain.py +++ b/cloud/cloudstack/cs_domain.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_domain diff --git a/cloud/cloudstack/cs_facts.py b/cloud/cloudstack/cs_facts.py index 4a774479537..6f51127df65 100644 --- a/cloud/cloudstack/cs_facts.py +++ b/cloud/cloudstack/cs_facts.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_facts diff --git a/cloud/cloudstack/cs_firewall.py b/cloud/cloudstack/cs_firewall.py index 1a677da4dfd..160e58d4723 100644 --- a/cloud/cloudstack/cs_firewall.py +++ b/cloud/cloudstack/cs_firewall.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_firewall diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 928746c2c97..58c98724853 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_instance diff --git a/cloud/cloudstack/cs_instance_facts.py b/cloud/cloudstack/cs_instance_facts.py index 4efaf645291..2aee631395d 100644 --- a/cloud/cloudstack/cs_instance_facts.py +++ b/cloud/cloudstack/cs_instance_facts.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_instance_facts diff --git a/cloud/cloudstack/cs_instancegroup.py b/cloud/cloudstack/cs_instancegroup.py index 323e0391213..12b2bc7baeb 100644 --- a/cloud/cloudstack/cs_instancegroup.py +++ b/cloud/cloudstack/cs_instancegroup.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_instancegroup diff --git a/cloud/cloudstack/cs_ip_address.py b/cloud/cloudstack/cs_ip_address.py index 2eb21411987..233720827f1 100644 --- a/cloud/cloudstack/cs_ip_address.py +++ b/cloud/cloudstack/cs_ip_address.py @@ -19,6 +19,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_ip_address diff --git a/cloud/cloudstack/cs_iso.py b/cloud/cloudstack/cs_iso.py index a61fb180781..ee84bd22f2c 100644 --- a/cloud/cloudstack/cs_iso.py +++ b/cloud/cloudstack/cs_iso.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_iso diff --git a/cloud/cloudstack/cs_loadbalancer_rule.py b/cloud/cloudstack/cs_loadbalancer_rule.py index 83eb8883602..2e5f11e415b 100644 --- a/cloud/cloudstack/cs_loadbalancer_rule.py +++ b/cloud/cloudstack/cs_loadbalancer_rule.py @@ -19,6 +19,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_loadbalancer_rule diff --git a/cloud/cloudstack/cs_loadbalancer_rule_member.py b/cloud/cloudstack/cs_loadbalancer_rule_member.py index c5410491a16..0695ed9be5b 100644 --- a/cloud/cloudstack/cs_loadbalancer_rule_member.py +++ b/cloud/cloudstack/cs_loadbalancer_rule_member.py @@ -19,6 +19,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_loadbalancer_rule_member diff --git a/cloud/cloudstack/cs_network.py b/cloud/cloudstack/cs_network.py index 41f5fa01f96..092fbf7326e 100644 --- a/cloud/cloudstack/cs_network.py +++ b/cloud/cloudstack/cs_network.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_network diff --git a/cloud/cloudstack/cs_nic.py b/cloud/cloudstack/cs_nic.py index 4017604e38a..a9947c266e5 100644 --- a/cloud/cloudstack/cs_nic.py +++ b/cloud/cloudstack/cs_nic.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_nic diff --git a/cloud/cloudstack/cs_pod.py b/cloud/cloudstack/cs_pod.py index e78eb2844cf..afccea1404a 100644 --- a/cloud/cloudstack/cs_pod.py +++ b/cloud/cloudstack/cs_pod.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_pod diff --git a/cloud/cloudstack/cs_portforward.py b/cloud/cloudstack/cs_portforward.py index 945db54a17b..139fa7773d3 100644 --- a/cloud/cloudstack/cs_portforward.py +++ b/cloud/cloudstack/cs_portforward.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_portforward diff --git a/cloud/cloudstack/cs_project.py b/cloud/cloudstack/cs_project.py index 6f3d41b3914..472762b4324 100644 --- a/cloud/cloudstack/cs_project.py +++ b/cloud/cloudstack/cs_project.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_project diff --git a/cloud/cloudstack/cs_region.py b/cloud/cloudstack/cs_region.py index ae863f025c2..74e4c079fa6 100644 --- a/cloud/cloudstack/cs_region.py +++ b/cloud/cloudstack/cs_region.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_region diff --git a/cloud/cloudstack/cs_resourcelimit.py b/cloud/cloudstack/cs_resourcelimit.py index 40567165c5b..e5bfb7096e2 100644 --- a/cloud/cloudstack/cs_resourcelimit.py +++ b/cloud/cloudstack/cs_resourcelimit.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_resourcelimit diff --git a/cloud/cloudstack/cs_router.py b/cloud/cloudstack/cs_router.py index 73575c80010..49a2dbe7b6b 100644 --- a/cloud/cloudstack/cs_router.py +++ b/cloud/cloudstack/cs_router.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_router diff --git a/cloud/cloudstack/cs_securitygroup.py b/cloud/cloudstack/cs_securitygroup.py index edf4d533f42..c65d63c8f4d 100644 --- a/cloud/cloudstack/cs_securitygroup.py +++ b/cloud/cloudstack/cs_securitygroup.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_securitygroup diff --git a/cloud/cloudstack/cs_securitygroup_rule.py b/cloud/cloudstack/cs_securitygroup_rule.py index 5ac22960b57..85617b5baac 100644 --- a/cloud/cloudstack/cs_securitygroup_rule.py +++ b/cloud/cloudstack/cs_securitygroup_rule.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_securitygroup_rule diff --git a/cloud/cloudstack/cs_snapshot_policy.py b/cloud/cloudstack/cs_snapshot_policy.py index d7963820c73..157d05e803c 100644 --- a/cloud/cloudstack/cs_snapshot_policy.py +++ b/cloud/cloudstack/cs_snapshot_policy.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_snapshot_policy diff --git a/cloud/cloudstack/cs_sshkeypair.py b/cloud/cloudstack/cs_sshkeypair.py index d756059f7ef..2724c58c71d 100644 --- a/cloud/cloudstack/cs_sshkeypair.py +++ b/cloud/cloudstack/cs_sshkeypair.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_sshkeypair diff --git a/cloud/cloudstack/cs_staticnat.py b/cloud/cloudstack/cs_staticnat.py index c023b947fff..a805a1c8bb5 100644 --- a/cloud/cloudstack/cs_staticnat.py +++ b/cloud/cloudstack/cs_staticnat.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_staticnat diff --git a/cloud/cloudstack/cs_template.py b/cloud/cloudstack/cs_template.py index 3db11755184..7e6d74e9c65 100644 --- a/cloud/cloudstack/cs_template.py +++ b/cloud/cloudstack/cs_template.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_template diff --git a/cloud/cloudstack/cs_user.py b/cloud/cloudstack/cs_user.py index bee4653d163..f9f43322e47 100644 --- a/cloud/cloudstack/cs_user.py +++ b/cloud/cloudstack/cs_user.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_user diff --git a/cloud/cloudstack/cs_vmsnapshot.py b/cloud/cloudstack/cs_vmsnapshot.py index 29d19149935..e3b43820a56 100644 --- a/cloud/cloudstack/cs_vmsnapshot.py +++ b/cloud/cloudstack/cs_vmsnapshot.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_vmsnapshot diff --git a/cloud/cloudstack/cs_volume.py b/cloud/cloudstack/cs_volume.py index c2a542741d6..36071e0d78a 100644 --- a/cloud/cloudstack/cs_volume.py +++ b/cloud/cloudstack/cs_volume.py @@ -19,6 +19,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_volume diff --git a/cloud/cloudstack/cs_vpc.py b/cloud/cloudstack/cs_vpc.py index 8171cb76090..1495b865500 100644 --- a/cloud/cloudstack/cs_vpc.py +++ b/cloud/cloudstack/cs_vpc.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_vpc diff --git a/cloud/cloudstack/cs_zone.py b/cloud/cloudstack/cs_zone.py index 2a343e0b970..1dd5dd64221 100644 --- a/cloud/cloudstack/cs_zone.py +++ b/cloud/cloudstack/cs_zone.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_zone diff --git a/cloud/cloudstack/cs_zone_facts.py b/cloud/cloudstack/cs_zone_facts.py index 2ce82423ec6..74894b7494e 100644 --- a/cloud/cloudstack/cs_zone_facts.py +++ b/cloud/cloudstack/cs_zone_facts.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cs_zone_facts diff --git a/cloud/google/gcdns_record.py b/cloud/google/gcdns_record.py index 19b70a85816..7c209c5cbad 100644 --- a/cloud/google/gcdns_record.py +++ b/cloud/google/gcdns_record.py @@ -23,6 +23,10 @@ # Documentation ################################################################################ +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: gcdns_record diff --git a/cloud/google/gcdns_zone.py b/cloud/google/gcdns_zone.py index 4b7bd16985b..683cb881899 100644 --- a/cloud/google/gcdns_zone.py +++ b/cloud/google/gcdns_zone.py @@ -23,6 +23,10 @@ # Documentation ################################################################################ +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: gcdns_zone diff --git a/cloud/google/gce_img.py b/cloud/google/gce_img.py index 010bdfdb8a6..e340808539a 100644 --- a/cloud/google/gce_img.py +++ b/cloud/google/gce_img.py @@ -18,6 +18,10 @@ """An Ansible module to utilize GCE image resources.""" +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: gce_img diff --git a/cloud/google/gce_tag.py b/cloud/google/gce_tag.py index 477e86b6756..7122a2398a0 100644 --- a/cloud/google/gce_tag.py +++ b/cloud/google/gce_tag.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: gce_tag diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index d83be951536..d3b6804ce50 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: lxc_container diff --git a/cloud/lxd/lxd_container.py b/cloud/lxd/lxd_container.py index 3b00d3b4f7a..b4eaa5739a7 100644 --- a/cloud/lxd/lxd_container.py +++ b/cloud/lxd/lxd_container.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: lxd_container diff --git a/cloud/lxd/lxd_profile.py b/cloud/lxd/lxd_profile.py index 831765bfe5f..546d0c09ea4 100644 --- a/cloud/lxd/lxd_profile.py +++ b/cloud/lxd/lxd_profile.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: lxd_profile diff --git a/cloud/misc/ovirt.py b/cloud/misc/ovirt.py index 7f0b421339f..af89998258c 100644 --- a/cloud/misc/ovirt.py +++ b/cloud/misc/ovirt.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt diff --git a/cloud/misc/proxmox.py b/cloud/misc/proxmox.py index 6b065bb7bca..c404519d499 100644 --- a/cloud/misc/proxmox.py +++ b/cloud/misc/proxmox.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: proxmox diff --git a/cloud/misc/proxmox_kvm.py b/cloud/misc/proxmox_kvm.py index 96c06707612..e77f266b42a 100644 --- a/cloud/misc/proxmox_kvm.py +++ b/cloud/misc/proxmox_kvm.py @@ -17,6 +17,10 @@ along with this software. If not, see . """ +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: proxmox_kvm diff --git a/cloud/misc/proxmox_template.py b/cloud/misc/proxmox_template.py index 70cd445a185..64c9b96cb62 100644 --- a/cloud/misc/proxmox_template.py +++ b/cloud/misc/proxmox_template.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: proxmox_template diff --git a/cloud/misc/rhevm.py b/cloud/misc/rhevm.py index 6606101eb7a..8789e880281 100644 --- a/cloud/misc/rhevm.py +++ b/cloud/misc/rhevm.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rhevm diff --git a/cloud/misc/virt.py b/cloud/misc/virt.py index 468cd2c5fb6..3e9c098f3d3 100644 --- a/cloud/misc/virt.py +++ b/cloud/misc/virt.py @@ -15,6 +15,10 @@ along with this program. If not, see . """ +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: virt diff --git a/cloud/misc/virt_net.py b/cloud/misc/virt_net.py index c389520e424..a37c7ca9e38 100644 --- a/cloud/misc/virt_net.py +++ b/cloud/misc/virt_net.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: virt_net diff --git a/cloud/misc/virt_pool.py b/cloud/misc/virt_pool.py index ad5bcc66112..4a24dffee08 100644 --- a/cloud/misc/virt_pool.py +++ b/cloud/misc/virt_pool.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: virt_pool diff --git a/cloud/openstack/os_flavor_facts.py b/cloud/openstack/os_flavor_facts.py index 6afecb61f07..c6e938b63b1 100644 --- a/cloud/openstack/os_flavor_facts.py +++ b/cloud/openstack/os_flavor_facts.py @@ -26,6 +26,10 @@ from distutils.version import StrictVersion +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_flavor_facts diff --git a/cloud/openstack/os_group.py b/cloud/openstack/os_group.py index 4f317abccd3..2347efb483f 100644 --- a/cloud/openstack/os_group.py +++ b/cloud/openstack/os_group.py @@ -21,6 +21,10 @@ except ImportError: HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_group diff --git a/cloud/openstack/os_ironic_inspect.py b/cloud/openstack/os_ironic_inspect.py index 5299da09335..b436f7f0429 100644 --- a/cloud/openstack/os_ironic_inspect.py +++ b/cloud/openstack/os_ironic_inspect.py @@ -24,6 +24,10 @@ from distutils.version import StrictVersion +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_ironic_inspect diff --git a/cloud/openstack/os_keystone_domain.py b/cloud/openstack/os_keystone_domain.py index bed2f0410ce..b355971e8b5 100644 --- a/cloud/openstack/os_keystone_domain.py +++ b/cloud/openstack/os_keystone_domain.py @@ -21,6 +21,10 @@ except ImportError: HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_keystone_domain diff --git a/cloud/openstack/os_keystone_domain_facts.py b/cloud/openstack/os_keystone_domain_facts.py index f62dc16f819..9e363415210 100644 --- a/cloud/openstack/os_keystone_domain_facts.py +++ b/cloud/openstack/os_keystone_domain_facts.py @@ -21,6 +21,10 @@ except ImportError: HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_keystone_domain_facts diff --git a/cloud/openstack/os_keystone_role.py b/cloud/openstack/os_keystone_role.py index def91a8b326..db5b0027c05 100644 --- a/cloud/openstack/os_keystone_role.py +++ b/cloud/openstack/os_keystone_role.py @@ -21,6 +21,10 @@ except ImportError: HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_keystone_role diff --git a/cloud/openstack/os_keystone_service.py b/cloud/openstack/os_keystone_service.py index 4e3e46cc5cf..d23f2881621 100644 --- a/cloud/openstack/os_keystone_service.py +++ b/cloud/openstack/os_keystone_service.py @@ -22,6 +22,10 @@ from distutils.version import StrictVersion +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_keystone_service diff --git a/cloud/openstack/os_port_facts.py b/cloud/openstack/os_port_facts.py index e3048211877..0da37d88ef4 100644 --- a/cloud/openstack/os_port_facts.py +++ b/cloud/openstack/os_port_facts.py @@ -21,6 +21,10 @@ except ImportError: HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: os_port_facts short_description: Retrieve facts about ports within OpenStack. diff --git a/cloud/openstack/os_project.py b/cloud/openstack/os_project.py index 4c686724c89..22f50107558 100644 --- a/cloud/openstack/os_project.py +++ b/cloud/openstack/os_project.py @@ -23,6 +23,10 @@ from distutils.version import StrictVersion +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_project diff --git a/cloud/openstack/os_project_facts.py b/cloud/openstack/os_project_facts.py index a53acf532ea..856b6304ce7 100644 --- a/cloud/openstack/os_project_facts.py +++ b/cloud/openstack/os_project_facts.py @@ -21,6 +21,10 @@ except ImportError: HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_project_facts diff --git a/cloud/openstack/os_recordset.py b/cloud/openstack/os_recordset.py index 0e860207166..62fa8564102 100644 --- a/cloud/openstack/os_recordset.py +++ b/cloud/openstack/os_recordset.py @@ -23,6 +23,10 @@ from distutils.version import StrictVersion +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_recordset diff --git a/cloud/openstack/os_server_group.py b/cloud/openstack/os_server_group.py index 155c4497cc2..0103fef8670 100644 --- a/cloud/openstack/os_server_group.py +++ b/cloud/openstack/os_server_group.py @@ -22,6 +22,10 @@ except ImportError: HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_server_group diff --git a/cloud/openstack/os_stack.py b/cloud/openstack/os_stack.py index 3d3cb9be07e..fc42b62112e 100644 --- a/cloud/openstack/os_stack.py +++ b/cloud/openstack/os_stack.py @@ -25,6 +25,10 @@ except ImportError: HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_stack diff --git a/cloud/openstack/os_user_facts.py b/cloud/openstack/os_user_facts.py index 4330eb430c8..52af5b8e621 100644 --- a/cloud/openstack/os_user_facts.py +++ b/cloud/openstack/os_user_facts.py @@ -21,6 +21,10 @@ except ImportError: HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_user_facts diff --git a/cloud/openstack/os_user_role.py b/cloud/openstack/os_user_role.py index 22f41830c61..41b0b73e075 100644 --- a/cloud/openstack/os_user_role.py +++ b/cloud/openstack/os_user_role.py @@ -24,6 +24,10 @@ from distutils.version import StrictVersion +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_user_role diff --git a/cloud/openstack/os_zone.py b/cloud/openstack/os_zone.py index 0a0e7ed3dae..a733d80ab22 100644 --- a/cloud/openstack/os_zone.py +++ b/cloud/openstack/os_zone.py @@ -23,6 +23,10 @@ from distutils.version import StrictVersion +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_zone diff --git a/cloud/ovh/ovh_ip_loadbalancing_backend.py b/cloud/ovh/ovh_ip_loadbalancing_backend.py index 7f2c5d5963f..3499e73a92f 100644 --- a/cloud/ovh/ovh_ip_loadbalancing_backend.py +++ b/cloud/ovh/ovh_ip_loadbalancing_backend.py @@ -13,6 +13,10 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovh_ip_loadbalancing_backend diff --git a/cloud/ovirt/ovirt_affinity_labels.py b/cloud/ovirt/ovirt_affinity_labels.py index 3e79b1a3e69..5a680f92976 100644 --- a/cloud/ovirt/ovirt_affinity_labels.py +++ b/cloud/ovirt/ovirt_affinity_labels.py @@ -36,6 +36,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_affinity_labels diff --git a/cloud/ovirt/ovirt_affinity_labels_facts.py b/cloud/ovirt/ovirt_affinity_labels_facts.py index 9d13c4cb3d6..0708b7d880b 100644 --- a/cloud/ovirt/ovirt_affinity_labels_facts.py +++ b/cloud/ovirt/ovirt_affinity_labels_facts.py @@ -31,6 +31,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_affinity_labels_facts diff --git a/cloud/ovirt/ovirt_auth.py b/cloud/ovirt/ovirt_auth.py index 50ed548eb9a..6f43fe8d029 100644 --- a/cloud/ovirt/ovirt_auth.py +++ b/cloud/ovirt/ovirt_auth.py @@ -25,6 +25,10 @@ pass +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_auth diff --git a/cloud/ovirt/ovirt_clusters.py b/cloud/ovirt/ovirt_clusters.py index fda01e7f464..c40ffcddd8f 100644 --- a/cloud/ovirt/ovirt_clusters.py +++ b/cloud/ovirt/ovirt_clusters.py @@ -37,6 +37,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_clusters diff --git a/cloud/ovirt/ovirt_clusters_facts.py b/cloud/ovirt/ovirt_clusters_facts.py index 42a09c52c42..edcf680bee6 100644 --- a/cloud/ovirt/ovirt_clusters_facts.py +++ b/cloud/ovirt/ovirt_clusters_facts.py @@ -30,6 +30,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_clusters_facts diff --git a/cloud/ovirt/ovirt_datacenters.py b/cloud/ovirt/ovirt_datacenters.py index 1e6fbf374eb..ef63709a5c9 100644 --- a/cloud/ovirt/ovirt_datacenters.py +++ b/cloud/ovirt/ovirt_datacenters.py @@ -38,6 +38,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_datacenters diff --git a/cloud/ovirt/ovirt_datacenters_facts.py b/cloud/ovirt/ovirt_datacenters_facts.py index f153cec10bf..6f812951584 100644 --- a/cloud/ovirt/ovirt_datacenters_facts.py +++ b/cloud/ovirt/ovirt_datacenters_facts.py @@ -30,6 +30,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_datacenters_facts diff --git a/cloud/ovirt/ovirt_disks.py b/cloud/ovirt/ovirt_disks.py index f354c7a6d25..7730242afbf 100644 --- a/cloud/ovirt/ovirt_disks.py +++ b/cloud/ovirt/ovirt_disks.py @@ -28,6 +28,10 @@ from ansible.module_utils.ovirt import * +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_disks diff --git a/cloud/ovirt/ovirt_external_providers.py b/cloud/ovirt/ovirt_external_providers.py index cfcd3c1123f..9bcb38a78f3 100644 --- a/cloud/ovirt/ovirt_external_providers.py +++ b/cloud/ovirt/ovirt_external_providers.py @@ -37,6 +37,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_external_providers diff --git a/cloud/ovirt/ovirt_external_providers_facts.py b/cloud/ovirt/ovirt_external_providers_facts.py index 9f3e601866c..b67ec4d89d8 100644 --- a/cloud/ovirt/ovirt_external_providers_facts.py +++ b/cloud/ovirt/ovirt_external_providers_facts.py @@ -31,6 +31,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_external_providers_facts diff --git a/cloud/ovirt/ovirt_groups.py b/cloud/ovirt/ovirt_groups.py index 7170bfab3d9..34f326e64ba 100644 --- a/cloud/ovirt/ovirt_groups.py +++ b/cloud/ovirt/ovirt_groups.py @@ -37,6 +37,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_groups diff --git a/cloud/ovirt/ovirt_groups_facts.py b/cloud/ovirt/ovirt_groups_facts.py index 1f6ac177124..ab4252ffc93 100644 --- a/cloud/ovirt/ovirt_groups_facts.py +++ b/cloud/ovirt/ovirt_groups_facts.py @@ -30,6 +30,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_groups_facts diff --git a/cloud/ovirt/ovirt_host_networks.py b/cloud/ovirt/ovirt_host_networks.py index 47404dcebe8..edf6d3c3789 100644 --- a/cloud/ovirt/ovirt_host_networks.py +++ b/cloud/ovirt/ovirt_host_networks.py @@ -28,6 +28,10 @@ from ansible.module_utils.ovirt import * +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_host_networks diff --git a/cloud/ovirt/ovirt_host_pm.py b/cloud/ovirt/ovirt_host_pm.py index 17bf5ee9371..41475ad7bb7 100644 --- a/cloud/ovirt/ovirt_host_pm.py +++ b/cloud/ovirt/ovirt_host_pm.py @@ -28,6 +28,10 @@ from ansible.module_utils.ovirt import * +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_host_pm diff --git a/cloud/ovirt/ovirt_hosts.py b/cloud/ovirt/ovirt_hosts.py index 1a11aefdb96..1394692f8c9 100644 --- a/cloud/ovirt/ovirt_hosts.py +++ b/cloud/ovirt/ovirt_hosts.py @@ -30,6 +30,10 @@ from ansible.module_utils.ovirt import * +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_hosts diff --git a/cloud/ovirt/ovirt_hosts_facts.py b/cloud/ovirt/ovirt_hosts_facts.py index 53932868172..ad1945e538c 100644 --- a/cloud/ovirt/ovirt_hosts_facts.py +++ b/cloud/ovirt/ovirt_hosts_facts.py @@ -27,6 +27,10 @@ from ansible.module_utils.ovirt import * +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_hosts_facts diff --git a/cloud/ovirt/ovirt_mac_pools.py b/cloud/ovirt/ovirt_mac_pools.py index 6f14f6f78ff..622f57d89d7 100644 --- a/cloud/ovirt/ovirt_mac_pools.py +++ b/cloud/ovirt/ovirt_mac_pools.py @@ -36,6 +36,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_mac_pools diff --git a/cloud/ovirt/ovirt_networks.py b/cloud/ovirt/ovirt_networks.py index 44b7fefe7af..047a24d3880 100644 --- a/cloud/ovirt/ovirt_networks.py +++ b/cloud/ovirt/ovirt_networks.py @@ -38,6 +38,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_networks diff --git a/cloud/ovirt/ovirt_networks_facts.py b/cloud/ovirt/ovirt_networks_facts.py index 9c42244ce4c..974acbf95d8 100644 --- a/cloud/ovirt/ovirt_networks_facts.py +++ b/cloud/ovirt/ovirt_networks_facts.py @@ -30,6 +30,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_networks_facts diff --git a/cloud/ovirt/ovirt_nics.py b/cloud/ovirt/ovirt_nics.py index 912e03c9881..f0513503a9b 100644 --- a/cloud/ovirt/ovirt_nics.py +++ b/cloud/ovirt/ovirt_nics.py @@ -38,6 +38,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_nics diff --git a/cloud/ovirt/ovirt_nics_facts.py b/cloud/ovirt/ovirt_nics_facts.py index 0e913912647..ab5fcdad721 100644 --- a/cloud/ovirt/ovirt_nics_facts.py +++ b/cloud/ovirt/ovirt_nics_facts.py @@ -32,6 +32,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_nics_facts diff --git a/cloud/ovirt/ovirt_permissions.py b/cloud/ovirt/ovirt_permissions.py index 11a3182ad23..6ea833599a0 100644 --- a/cloud/ovirt/ovirt_permissions.py +++ b/cloud/ovirt/ovirt_permissions.py @@ -40,6 +40,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_permissions diff --git a/cloud/ovirt/ovirt_permissions_facts.py b/cloud/ovirt/ovirt_permissions_facts.py index 2c1b4fb5c01..6c855f6296d 100644 --- a/cloud/ovirt/ovirt_permissions_facts.py +++ b/cloud/ovirt/ovirt_permissions_facts.py @@ -36,6 +36,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_permissions_facts diff --git a/cloud/ovirt/ovirt_quotas.py b/cloud/ovirt/ovirt_quotas.py index 4b64e53c154..d9b94afa202 100644 --- a/cloud/ovirt/ovirt_quotas.py +++ b/cloud/ovirt/ovirt_quotas.py @@ -38,6 +38,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_quotas diff --git a/cloud/ovirt/ovirt_quotas_facts.py b/cloud/ovirt/ovirt_quotas_facts.py index 3d354ac084d..4553f64d394 100644 --- a/cloud/ovirt/ovirt_quotas_facts.py +++ b/cloud/ovirt/ovirt_quotas_facts.py @@ -32,6 +32,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_quotas_facts diff --git a/cloud/ovirt/ovirt_storage_domains.py b/cloud/ovirt/ovirt_storage_domains.py index 77e53bb710f..cfdd1230386 100644 --- a/cloud/ovirt/ovirt_storage_domains.py +++ b/cloud/ovirt/ovirt_storage_domains.py @@ -39,6 +39,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_storage_domains diff --git a/cloud/ovirt/ovirt_storage_domains_facts.py b/cloud/ovirt/ovirt_storage_domains_facts.py index 121ca1ae99a..23431ead50a 100644 --- a/cloud/ovirt/ovirt_storage_domains_facts.py +++ b/cloud/ovirt/ovirt_storage_domains_facts.py @@ -30,6 +30,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_storage_domains_facts diff --git a/cloud/ovirt/ovirt_templates.py b/cloud/ovirt/ovirt_templates.py index ca083af6b90..831ab906c08 100644 --- a/cloud/ovirt/ovirt_templates.py +++ b/cloud/ovirt/ovirt_templates.py @@ -41,6 +41,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_templates diff --git a/cloud/ovirt/ovirt_templates_facts.py b/cloud/ovirt/ovirt_templates_facts.py index 189fccb9ed8..4a2c7c0d00f 100644 --- a/cloud/ovirt/ovirt_templates_facts.py +++ b/cloud/ovirt/ovirt_templates_facts.py @@ -30,6 +30,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_templates_facts diff --git a/cloud/ovirt/ovirt_users.py b/cloud/ovirt/ovirt_users.py index 3abcefbe96b..4fb47122256 100644 --- a/cloud/ovirt/ovirt_users.py +++ b/cloud/ovirt/ovirt_users.py @@ -36,6 +36,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_users diff --git a/cloud/ovirt/ovirt_users_facts.py b/cloud/ovirt/ovirt_users_facts.py index 11f69f811e1..7d2b04f1fb1 100644 --- a/cloud/ovirt/ovirt_users_facts.py +++ b/cloud/ovirt/ovirt_users_facts.py @@ -30,6 +30,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_users_facts diff --git a/cloud/ovirt/ovirt_vmpools.py b/cloud/ovirt/ovirt_vmpools.py index 4d0f839bf14..82e76d91dc1 100644 --- a/cloud/ovirt/ovirt_vmpools.py +++ b/cloud/ovirt/ovirt_vmpools.py @@ -39,6 +39,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_vmpools diff --git a/cloud/ovirt/ovirt_vmpools_facts.py b/cloud/ovirt/ovirt_vmpools_facts.py index eb86ae8162b..fb20a12f833 100644 --- a/cloud/ovirt/ovirt_vmpools_facts.py +++ b/cloud/ovirt/ovirt_vmpools_facts.py @@ -30,6 +30,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_vmpools_facts diff --git a/cloud/ovirt/ovirt_vms.py b/cloud/ovirt/ovirt_vms.py index 0c1ea05388f..4edfe0aa596 100644 --- a/cloud/ovirt/ovirt_vms.py +++ b/cloud/ovirt/ovirt_vms.py @@ -28,6 +28,10 @@ from ansible.module_utils.ovirt import * +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_vms diff --git a/cloud/ovirt/ovirt_vms_facts.py b/cloud/ovirt/ovirt_vms_facts.py index ae287c82068..2a11ad75280 100644 --- a/cloud/ovirt/ovirt_vms_facts.py +++ b/cloud/ovirt/ovirt_vms_facts.py @@ -30,6 +30,10 @@ ) +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ovirt_vms_facts diff --git a/cloud/profitbricks/profitbricks.py b/cloud/profitbricks/profitbricks.py index caa1e0cc1ef..cfafc8e0a46 100644 --- a/cloud/profitbricks/profitbricks.py +++ b/cloud/profitbricks/profitbricks.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: profitbricks diff --git a/cloud/profitbricks/profitbricks_datacenter.py b/cloud/profitbricks/profitbricks_datacenter.py index de64f1c210e..b6ce2371653 100644 --- a/cloud/profitbricks/profitbricks_datacenter.py +++ b/cloud/profitbricks/profitbricks_datacenter.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: profitbricks_datacenter diff --git a/cloud/profitbricks/profitbricks_nic.py b/cloud/profitbricks/profitbricks_nic.py index 8835e1175ad..01377a338b3 100644 --- a/cloud/profitbricks/profitbricks_nic.py +++ b/cloud/profitbricks/profitbricks_nic.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: profitbricks_nic diff --git a/cloud/profitbricks/profitbricks_volume.py b/cloud/profitbricks/profitbricks_volume.py index a6c3d06958f..caed8579aa7 100644 --- a/cloud/profitbricks/profitbricks_volume.py +++ b/cloud/profitbricks/profitbricks_volume.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: profitbricks_volume diff --git a/cloud/profitbricks/profitbricks_volume_attachments.py b/cloud/profitbricks/profitbricks_volume_attachments.py index ac4db01364f..1904c470a55 100644 --- a/cloud/profitbricks/profitbricks_volume_attachments.py +++ b/cloud/profitbricks/profitbricks_volume_attachments.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: profitbricks_volume_attachments diff --git a/cloud/rackspace/rax_clb_ssl.py b/cloud/rackspace/rax_clb_ssl.py index 57b21354110..37c35b32de6 100644 --- a/cloud/rackspace/rax_clb_ssl.py +++ b/cloud/rackspace/rax_clb_ssl.py @@ -16,6 +16,10 @@ # This is a DOCUMENTATION stub specific to this module, it extends # a documentation fragment located in ansible.utils.module_docs_fragments +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION=''' module: rax_clb_ssl short_description: Manage SSL termination for a Rackspace Cloud Load Balancer. diff --git a/cloud/rackspace/rax_mon_alarm.py b/cloud/rackspace/rax_mon_alarm.py index 2a7a3d8db6b..0df4fad3401 100644 --- a/cloud/rackspace/rax_mon_alarm.py +++ b/cloud/rackspace/rax_mon_alarm.py @@ -16,6 +16,10 @@ # This is a DOCUMENTATION stub specific to this module, it extends # a documentation fragment located in ansible.utils.module_docs_fragments +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rax_mon_alarm diff --git a/cloud/rackspace/rax_mon_check.py b/cloud/rackspace/rax_mon_check.py index 6cc5eade348..c8bcfcd569a 100644 --- a/cloud/rackspace/rax_mon_check.py +++ b/cloud/rackspace/rax_mon_check.py @@ -16,6 +16,10 @@ # This is a DOCUMENTATION stub specific to this module, it extends # a documentation fragment located in ansible.utils.module_docs_fragments +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rax_mon_check diff --git a/cloud/rackspace/rax_mon_entity.py b/cloud/rackspace/rax_mon_entity.py index cbceb495d7b..fae58309652 100644 --- a/cloud/rackspace/rax_mon_entity.py +++ b/cloud/rackspace/rax_mon_entity.py @@ -16,6 +16,10 @@ # This is a DOCUMENTATION stub specific to this module, it extends # a documentation fragment located in ansible.utils.module_docs_fragments +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rax_mon_entity diff --git a/cloud/rackspace/rax_mon_notification.py b/cloud/rackspace/rax_mon_notification.py index 092b1826b8d..21396e7cb06 100644 --- a/cloud/rackspace/rax_mon_notification.py +++ b/cloud/rackspace/rax_mon_notification.py @@ -16,6 +16,10 @@ # This is a DOCUMENTATION stub specific to this module, it extends # a documentation fragment located in ansible.utils.module_docs_fragments +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rax_mon_notification diff --git a/cloud/rackspace/rax_mon_notification_plan.py b/cloud/rackspace/rax_mon_notification_plan.py index 41a15bca239..a0b283884ff 100644 --- a/cloud/rackspace/rax_mon_notification_plan.py +++ b/cloud/rackspace/rax_mon_notification_plan.py @@ -16,6 +16,10 @@ # This is a DOCUMENTATION stub specific to this module, it extends # a documentation fragment located in ansible.utils.module_docs_fragments +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rax_mon_notification_plan diff --git a/cloud/serverless.py b/cloud/serverless.py index e065139c71b..a075a2b49b0 100644 --- a/cloud/serverless.py +++ b/cloud/serverless.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: serverless diff --git a/cloud/smartos/smartos_image_facts.py b/cloud/smartos/smartos_image_facts.py index 6a16e2e9653..487aa3f648c 100644 --- a/cloud/smartos/smartos_image_facts.py +++ b/cloud/smartos/smartos_image_facts.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: smartos_image_facts diff --git a/cloud/softlayer/sl_vm.py b/cloud/softlayer/sl_vm.py index 1866d1a57e3..b24c0f06fac 100644 --- a/cloud/softlayer/sl_vm.py +++ b/cloud/softlayer/sl_vm.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: sl_vm diff --git a/cloud/vmware/vca_fw.py b/cloud/vmware/vca_fw.py index 17cc093eb56..78cebbb012e 100644 --- a/cloud/vmware/vca_fw.py +++ b/cloud/vmware/vca_fw.py @@ -18,6 +18,10 @@ # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: vca_fw diff --git a/cloud/vmware/vca_nat.py b/cloud/vmware/vca_nat.py index 3381b3ced20..64771da6928 100644 --- a/cloud/vmware/vca_nat.py +++ b/cloud/vmware/vca_nat.py @@ -18,6 +18,10 @@ # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: vca_nat diff --git a/cloud/vmware/vca_vapp.py b/cloud/vmware/vca_vapp.py index 68ed5f255db..4ebdda24d6c 100644 --- a/cloud/vmware/vca_vapp.py +++ b/cloud/vmware/vca_vapp.py @@ -18,6 +18,10 @@ # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: vca_vapp diff --git a/cloud/vmware/vmware_cluster.py b/cloud/vmware/vmware_cluster.py index 8067d36de2c..5fd986d52b0 100644 --- a/cloud/vmware/vmware_cluster.py +++ b/cloud/vmware/vmware_cluster.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: vmware_cluster diff --git a/cloud/vmware/vmware_datacenter.py b/cloud/vmware/vmware_datacenter.py index ef2fd2f1f73..fb60f2c9f5c 100644 --- a/cloud/vmware/vmware_datacenter.py +++ b/cloud/vmware/vmware_datacenter.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: vmware_datacenter diff --git a/cloud/vmware/vmware_dns_config.py b/cloud/vmware/vmware_dns_config.py index 57eda23b7d4..4faa8b6e295 100644 --- a/cloud/vmware/vmware_dns_config.py +++ b/cloud/vmware/vmware_dns_config.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: vmware_dns_config diff --git a/cloud/vmware/vmware_dvs_host.py b/cloud/vmware/vmware_dvs_host.py index dcfb4ba7f58..031b90ec66f 100644 --- a/cloud/vmware/vmware_dvs_host.py +++ b/cloud/vmware/vmware_dvs_host.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: vmware_dvs_host diff --git a/cloud/vmware/vmware_dvs_portgroup.py b/cloud/vmware/vmware_dvs_portgroup.py index 06b39672ed1..58b4cff67c7 100644 --- a/cloud/vmware/vmware_dvs_portgroup.py +++ b/cloud/vmware/vmware_dvs_portgroup.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: vmware_dvs_portgroup diff --git a/cloud/vmware/vmware_dvswitch.py b/cloud/vmware/vmware_dvswitch.py index fb9d530605f..b3108f6a9d3 100644 --- a/cloud/vmware/vmware_dvswitch.py +++ b/cloud/vmware/vmware_dvswitch.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: vmware_dvswitch diff --git a/cloud/vmware/vmware_guest.py b/cloud/vmware/vmware_guest.py index 2a749bda674..cf3e83b3833 100644 --- a/cloud/vmware/vmware_guest.py +++ b/cloud/vmware/vmware_guest.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: vmware_guest diff --git a/cloud/vmware/vmware_host.py b/cloud/vmware/vmware_host.py index dd8e2f9eed4..22cb82d55db 100644 --- a/cloud/vmware/vmware_host.py +++ b/cloud/vmware/vmware_host.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: vmware_host diff --git a/cloud/vmware/vmware_local_user_manager.py b/cloud/vmware/vmware_local_user_manager.py index ff7736fe883..ac52b57465a 100644 --- a/cloud/vmware/vmware_local_user_manager.py +++ b/cloud/vmware/vmware_local_user_manager.py @@ -19,6 +19,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: vmware_maintenancemode diff --git a/cloud/vmware/vmware_migrate_vmk.py b/cloud/vmware/vmware_migrate_vmk.py index a18dcc4a883..730102c2049 100644 --- a/cloud/vmware/vmware_migrate_vmk.py +++ b/cloud/vmware/vmware_migrate_vmk.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: vmware_migrate_vmk diff --git a/cloud/vmware/vmware_portgroup.py b/cloud/vmware/vmware_portgroup.py index c367a976f23..089d584d039 100644 --- a/cloud/vmware/vmware_portgroup.py +++ b/cloud/vmware/vmware_portgroup.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: vmware_portgroup diff --git a/cloud/vmware/vmware_target_canonical_facts.py b/cloud/vmware/vmware_target_canonical_facts.py index cbf9d3edaa9..817d736d3ae 100644 --- a/cloud/vmware/vmware_target_canonical_facts.py +++ b/cloud/vmware/vmware_target_canonical_facts.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: vmware_target_canonical_facts diff --git a/cloud/vmware/vmware_vm_facts.py b/cloud/vmware/vmware_vm_facts.py index 62381849144..46de7a39157 100644 --- a/cloud/vmware/vmware_vm_facts.py +++ b/cloud/vmware/vmware_vm_facts.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: vmware_vm_facts diff --git a/cloud/vmware/vmware_vm_shell.py b/cloud/vmware/vmware_vm_shell.py index 80b4df192bd..34eb6b0f446 100644 --- a/cloud/vmware/vmware_vm_shell.py +++ b/cloud/vmware/vmware_vm_shell.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: vmware_vm_shell diff --git a/cloud/vmware/vmware_vm_vss_dvs_migrate.py b/cloud/vmware/vmware_vm_vss_dvs_migrate.py index 00d98a3200d..594a9e17830 100644 --- a/cloud/vmware/vmware_vm_vss_dvs_migrate.py +++ b/cloud/vmware/vmware_vm_vss_dvs_migrate.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: vmware_vm_vss_dvs_migrate diff --git a/cloud/vmware/vmware_vmkernel.py b/cloud/vmware/vmware_vmkernel.py index 863a41226af..238b85ea345 100644 --- a/cloud/vmware/vmware_vmkernel.py +++ b/cloud/vmware/vmware_vmkernel.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: vmware_vmkernel diff --git a/cloud/vmware/vmware_vmkernel_ip_config.py b/cloud/vmware/vmware_vmkernel_ip_config.py index 31c50e6c68c..fe545e356d8 100644 --- a/cloud/vmware/vmware_vmkernel_ip_config.py +++ b/cloud/vmware/vmware_vmkernel_ip_config.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: vmware_vmkernel_ip_config diff --git a/cloud/vmware/vmware_vmotion.py b/cloud/vmware/vmware_vmotion.py index 43e8a5d5d0b..0ceaf597879 100644 --- a/cloud/vmware/vmware_vmotion.py +++ b/cloud/vmware/vmware_vmotion.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: vmware_vmotion diff --git a/cloud/vmware/vmware_vsan_cluster.py b/cloud/vmware/vmware_vsan_cluster.py index 3f2bad1fa80..714f6f22ff8 100644 --- a/cloud/vmware/vmware_vsan_cluster.py +++ b/cloud/vmware/vmware_vsan_cluster.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: vmware_vsan_cluster diff --git a/cloud/vmware/vmware_vswitch.py b/cloud/vmware/vmware_vswitch.py index 7b115056ef5..ef14f2d6bfc 100644 --- a/cloud/vmware/vmware_vswitch.py +++ b/cloud/vmware/vmware_vswitch.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: vmware_vswitch diff --git a/cloud/vmware/vsphere_copy.py b/cloud/vmware/vsphere_copy.py index 8582c39c358..7e2ef125c86 100644 --- a/cloud/vmware/vsphere_copy.py +++ b/cloud/vmware/vsphere_copy.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: vsphere_copy diff --git a/cloud/webfaction/webfaction_app.py b/cloud/webfaction/webfaction_app.py index 77cb6f17554..63c00bd1778 100644 --- a/cloud/webfaction/webfaction_app.py +++ b/cloud/webfaction/webfaction_app.py @@ -27,6 +27,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: webfaction_app diff --git a/cloud/webfaction/webfaction_db.py b/cloud/webfaction/webfaction_db.py index 8f3d77c3dfb..6fe785f76a9 100644 --- a/cloud/webfaction/webfaction_db.py +++ b/cloud/webfaction/webfaction_db.py @@ -24,6 +24,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: webfaction_db diff --git a/cloud/webfaction/webfaction_domain.py b/cloud/webfaction/webfaction_domain.py index d89add88cb1..859209c9ce7 100644 --- a/cloud/webfaction/webfaction_domain.py +++ b/cloud/webfaction/webfaction_domain.py @@ -22,6 +22,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: webfaction_domain diff --git a/cloud/webfaction/webfaction_mailbox.py b/cloud/webfaction/webfaction_mailbox.py index 08008d7d5cf..2132eeaffbb 100644 --- a/cloud/webfaction/webfaction_mailbox.py +++ b/cloud/webfaction/webfaction_mailbox.py @@ -21,6 +21,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: webfaction_mailbox diff --git a/cloud/webfaction/webfaction_site.py b/cloud/webfaction/webfaction_site.py index 46f440b6a02..08a9b4d76d4 100644 --- a/cloud/webfaction/webfaction_site.py +++ b/cloud/webfaction/webfaction_site.py @@ -22,6 +22,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: webfaction_site diff --git a/cloud/xenserver_facts.py b/cloud/xenserver_facts.py index 1ca8e9e6c81..d908e5a3fdd 100644 --- a/cloud/xenserver_facts.py +++ b/cloud/xenserver_facts.py @@ -15,6 +15,10 @@ # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: xenserver_facts diff --git a/clustering/consul.py b/clustering/consul.py index 7f2b7d7e5fa..fd69726eef9 100644 --- a/clustering/consul.py +++ b/clustering/consul.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ module: consul short_description: "Add, modify & delete services within a consul cluster." diff --git a/clustering/consul_acl.py b/clustering/consul_acl.py index 858f6aea50e..845c26f98fe 100644 --- a/clustering/consul_acl.py +++ b/clustering/consul_acl.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ module: consul_acl short_description: "manipulate consul acl keys and rules" diff --git a/clustering/consul_kv.py b/clustering/consul_kv.py index e409f2a3a58..1f3db18359c 100644 --- a/clustering/consul_kv.py +++ b/clustering/consul_kv.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ module: consul_kv short_description: Manipulate entries in the key/value store of a consul cluster. diff --git a/clustering/consul_session.py b/clustering/consul_session.py index 7b9c4bbbb9b..e2c23c45dc5 100644 --- a/clustering/consul_session.py +++ b/clustering/consul_session.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ module: consul_session short_description: "manipulate consul sessions" diff --git a/clustering/kubernetes.py b/clustering/kubernetes.py index 12c62b30924..20514b0fe0a 100644 --- a/clustering/kubernetes.py +++ b/clustering/kubernetes.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: kubernetes diff --git a/clustering/znode.py b/clustering/znode.py index 90c0420ad5d..44cdc2bc83b 100644 --- a/clustering/znode.py +++ b/clustering/znode.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: znode diff --git a/commands/expect.py b/commands/expect.py index a8eff373e06..77dcdfdfa0a 100644 --- a/commands/expect.py +++ b/commands/expect.py @@ -27,6 +27,10 @@ HAS_PEXPECT = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: expect diff --git a/crypto/openssl_privatekey.py b/crypto/openssl_privatekey.py index e9be5e35635..d643142c653 100644 --- a/crypto/openssl_privatekey.py +++ b/crypto/openssl_privatekey.py @@ -28,6 +28,10 @@ import os +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: openssl_privatekey diff --git a/crypto/openssl_publickey.py b/crypto/openssl_publickey.py index c01364c8ce8..6ac73dc975e 100644 --- a/crypto/openssl_publickey.py +++ b/crypto/openssl_publickey.py @@ -28,6 +28,10 @@ import os +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: openssl_publickey diff --git a/database/influxdb/influxdb_database.py b/database/influxdb/influxdb_database.py index 7cedc44d4da..2e1245850da 100644 --- a/database/influxdb/influxdb_database.py +++ b/database/influxdb/influxdb_database.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: influxdb_database diff --git a/database/influxdb/influxdb_retention_policy.py b/database/influxdb/influxdb_retention_policy.py index ec4c32da215..7541b3dfd0d 100644 --- a/database/influxdb/influxdb_retention_policy.py +++ b/database/influxdb/influxdb_retention_policy.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: influxdb_retention_policy diff --git a/database/misc/mongodb_parameter.py b/database/misc/mongodb_parameter.py index 8dafeea179e..d284d2cc3f8 100644 --- a/database/misc/mongodb_parameter.py +++ b/database/misc/mongodb_parameter.py @@ -22,6 +22,10 @@ along with Ansible. If not, see . """ +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: mongodb_parameter diff --git a/database/misc/mongodb_user.py b/database/misc/mongodb_user.py index 5e9453f96c2..7fbcf332268 100644 --- a/database/misc/mongodb_user.py +++ b/database/misc/mongodb_user.py @@ -19,6 +19,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: mongodb_user diff --git a/database/misc/redis.py b/database/misc/redis.py index 6e906b24b3e..f99d025742b 100644 --- a/database/misc/redis.py +++ b/database/misc/redis.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: redis diff --git a/database/misc/riak.py b/database/misc/riak.py index bec1ce8928b..af4ec9489f3 100644 --- a/database/misc/riak.py +++ b/database/misc/riak.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: riak diff --git a/database/mssql/mssql_db.py b/database/mssql/mssql_db.py index eb6866151b8..2daf74d011e 100644 --- a/database/mssql/mssql_db.py +++ b/database/mssql/mssql_db.py @@ -20,6 +20,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: mssql_db diff --git a/database/mysql/mysql_replication.py b/database/mysql/mysql_replication.py index 5f58c4a2e11..76bcdc16c47 100644 --- a/database/mysql/mysql_replication.py +++ b/database/mysql/mysql_replication.py @@ -22,6 +22,10 @@ along with Ansible. If not, see . """ +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: mysql_replication diff --git a/database/postgresql/postgresql_ext.py b/database/postgresql/postgresql_ext.py index 9f14a182858..09b2903dab1 100644 --- a/database/postgresql/postgresql_ext.py +++ b/database/postgresql/postgresql_ext.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: postgresql_ext diff --git a/database/postgresql/postgresql_lang.py b/database/postgresql/postgresql_lang.py index a25432954c7..1a868bf67a9 100644 --- a/database/postgresql/postgresql_lang.py +++ b/database/postgresql/postgresql_lang.py @@ -17,6 +17,10 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: postgresql_lang diff --git a/database/postgresql/postgresql_schema.py b/database/postgresql/postgresql_schema.py index 06cf048d43e..52c1e5843ee 100644 --- a/database/postgresql/postgresql_schema.py +++ b/database/postgresql/postgresql_schema.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: postgresql_schema diff --git a/database/vertica/vertica_configuration.py b/database/vertica/vertica_configuration.py index e4861ca1225..c99627a021d 100644 --- a/database/vertica/vertica_configuration.py +++ b/database/vertica/vertica_configuration.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: vertica_configuration diff --git a/database/vertica/vertica_facts.py b/database/vertica/vertica_facts.py index ee8335cf997..4796a53612c 100644 --- a/database/vertica/vertica_facts.py +++ b/database/vertica/vertica_facts.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: vertica_facts diff --git a/database/vertica/vertica_role.py b/database/vertica/vertica_role.py index ba0eab4daf7..aff14581a38 100644 --- a/database/vertica/vertica_role.py +++ b/database/vertica/vertica_role.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: vertica_role diff --git a/database/vertica/vertica_schema.py b/database/vertica/vertica_schema.py index 0e849c5df8e..0bc1918d318 100644 --- a/database/vertica/vertica_schema.py +++ b/database/vertica/vertica_schema.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: vertica_schema diff --git a/database/vertica/vertica_user.py b/database/vertica/vertica_user.py index 29a90429abb..48d20c0f6d2 100644 --- a/database/vertica/vertica_user.py +++ b/database/vertica/vertica_user.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: vertica_user diff --git a/files/archive.py b/files/archive.py index 7d27fb3e858..93ddbe76cde 100644 --- a/files/archive.py +++ b/files/archive.py @@ -25,6 +25,10 @@ along with Ansible. If not, see . """ +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: archive diff --git a/files/blockinfile.py b/files/blockinfile.py index 72bd33e1d6c..ec85c078822 100755 --- a/files/blockinfile.py +++ b/files/blockinfile.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = """ --- module: blockinfile diff --git a/files/patch.py b/files/patch.py index 8b62c519073..c5aecf4e0d4 100644 --- a/files/patch.py +++ b/files/patch.py @@ -19,6 +19,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: patch diff --git a/files/tempfile.py b/files/tempfile.py index 35969141fe7..021c88dbbb1 100644 --- a/files/tempfile.py +++ b/files/tempfile.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: tempfile diff --git a/identity/ipa/ipa_group.py b/identity/ipa/ipa_group.py index 246b769dd0d..e34efc48daf 100644 --- a/identity/ipa/ipa_group.py +++ b/identity/ipa/ipa_group.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ipa_group diff --git a/identity/ipa/ipa_hbacrule.py b/identity/ipa/ipa_hbacrule.py index 29661fe77bf..d93bc32fd45 100644 --- a/identity/ipa/ipa_hbacrule.py +++ b/identity/ipa/ipa_hbacrule.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ipa_hbacrule diff --git a/identity/ipa/ipa_host.py b/identity/ipa/ipa_host.py index 59c6772673d..17b78500bc5 100644 --- a/identity/ipa/ipa_host.py +++ b/identity/ipa/ipa_host.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ipa_host diff --git a/identity/ipa/ipa_hostgroup.py b/identity/ipa/ipa_hostgroup.py index ba1ee33b8ac..57fbc5b4531 100644 --- a/identity/ipa/ipa_hostgroup.py +++ b/identity/ipa/ipa_hostgroup.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ipa_hostgroup diff --git a/identity/ipa/ipa_role.py b/identity/ipa/ipa_role.py index 9740a4d4a4d..95cd2bc45ed 100644 --- a/identity/ipa/ipa_role.py +++ b/identity/ipa/ipa_role.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ipa_role diff --git a/identity/ipa/ipa_sudocmd.py b/identity/ipa/ipa_sudocmd.py index 0a86bf68821..6ec3c84bb1d 100644 --- a/identity/ipa/ipa_sudocmd.py +++ b/identity/ipa/ipa_sudocmd.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ipa_sudocmd diff --git a/identity/ipa/ipa_sudocmdgroup.py b/identity/ipa/ipa_sudocmdgroup.py index b6730bccefe..e1d0e9b6021 100644 --- a/identity/ipa/ipa_sudocmdgroup.py +++ b/identity/ipa/ipa_sudocmdgroup.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ipa_sudocmdgroup diff --git a/identity/ipa/ipa_sudorule.py b/identity/ipa/ipa_sudorule.py index b8a4c062777..f5da15a7046 100644 --- a/identity/ipa/ipa_sudorule.py +++ b/identity/ipa/ipa_sudorule.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ipa_sudorule diff --git a/identity/ipa/ipa_user.py b/identity/ipa/ipa_user.py index 9ee677b6653..5e020d73440 100644 --- a/identity/ipa/ipa_user.py +++ b/identity/ipa/ipa_user.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ipa_user diff --git a/identity/opendj/opendj_backendprop.py b/identity/opendj/opendj_backendprop.py index 8af70c6cad4..893bbfdd47d 100644 --- a/identity/opendj/opendj_backendprop.py +++ b/identity/opendj/opendj_backendprop.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: opendj_backendprop diff --git a/infrastructure/foreman/foreman.py b/infrastructure/foreman/foreman.py index febb7d833c2..d7dcb5f2959 100644 --- a/infrastructure/foreman/foreman.py +++ b/infrastructure/foreman/foreman.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: foreman diff --git a/infrastructure/foreman/katello.py b/infrastructure/foreman/katello.py index 3d5219240b9..86b7be0622c 100644 --- a/infrastructure/foreman/katello.py +++ b/infrastructure/foreman/katello.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: katello diff --git a/messaging/rabbitmq_binding.py b/messaging/rabbitmq_binding.py index 2d0f9e4f089..428bec096f3 100644 --- a/messaging/rabbitmq_binding.py +++ b/messaging/rabbitmq_binding.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rabbitmq_binding diff --git a/messaging/rabbitmq_exchange.py b/messaging/rabbitmq_exchange.py index 0c955820820..a5e1e353dac 100644 --- a/messaging/rabbitmq_exchange.py +++ b/messaging/rabbitmq_exchange.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rabbitmq_exchange diff --git a/messaging/rabbitmq_parameter.py b/messaging/rabbitmq_parameter.py index f3d188fd25a..32959f2e562 100644 --- a/messaging/rabbitmq_parameter.py +++ b/messaging/rabbitmq_parameter.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rabbitmq_parameter diff --git a/messaging/rabbitmq_plugin.py b/messaging/rabbitmq_plugin.py index 832af02faf8..cc16966dcf4 100644 --- a/messaging/rabbitmq_plugin.py +++ b/messaging/rabbitmq_plugin.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rabbitmq_plugin diff --git a/messaging/rabbitmq_policy.py b/messaging/rabbitmq_policy.py index ef7ffc60686..6d5a053f3d6 100644 --- a/messaging/rabbitmq_policy.py +++ b/messaging/rabbitmq_policy.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rabbitmq_policy diff --git a/messaging/rabbitmq_queue.py b/messaging/rabbitmq_queue.py index 09860175606..6b49fea9f06 100644 --- a/messaging/rabbitmq_queue.py +++ b/messaging/rabbitmq_queue.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rabbitmq_queue diff --git a/messaging/rabbitmq_user.py b/messaging/rabbitmq_user.py index c51c6b0b592..02afe298cb2 100644 --- a/messaging/rabbitmq_user.py +++ b/messaging/rabbitmq_user.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rabbitmq_user diff --git a/messaging/rabbitmq_vhost.py b/messaging/rabbitmq_vhost.py index ad8f9cba207..635d8b77bbe 100644 --- a/messaging/rabbitmq_vhost.py +++ b/messaging/rabbitmq_vhost.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rabbitmq_vhost diff --git a/monitoring/airbrake_deployment.py b/monitoring/airbrake_deployment.py index d6daec2ceb4..124a801ea94 100644 --- a/monitoring/airbrake_deployment.py +++ b/monitoring/airbrake_deployment.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: airbrake_deployment diff --git a/monitoring/bigpanda.py b/monitoring/bigpanda.py index 7e818092f72..90b37841526 100644 --- a/monitoring/bigpanda.py +++ b/monitoring/bigpanda.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: bigpanda diff --git a/monitoring/boundary_meter.py b/monitoring/boundary_meter.py index d41c2659abd..ccbf014026f 100644 --- a/monitoring/boundary_meter.py +++ b/monitoring/boundary_meter.py @@ -22,6 +22,10 @@ along with Ansible. If not, see . """ +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: boundary_meter diff --git a/monitoring/circonus_annotation.py b/monitoring/circonus_annotation.py index 2435c3caa70..5e9029e9fb0 100644 --- a/monitoring/circonus_annotation.py +++ b/monitoring/circonus_annotation.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: circonus_annotation diff --git a/monitoring/datadog_event.py b/monitoring/datadog_event.py index 30fb2cafd96..4e3bf03b159 100644 --- a/monitoring/datadog_event.py +++ b/monitoring/datadog_event.py @@ -29,6 +29,10 @@ except: HAS_DATADOG = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: datadog_event diff --git a/monitoring/datadog_monitor.py b/monitoring/datadog_monitor.py index d10dd70cff5..50a067d8a2a 100644 --- a/monitoring/datadog_monitor.py +++ b/monitoring/datadog_monitor.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # import module snippets +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: datadog_monitor diff --git a/monitoring/honeybadger_deployment.py b/monitoring/honeybadger_deployment.py index c9b62f0d439..362af67963a 100644 --- a/monitoring/honeybadger_deployment.py +++ b/monitoring/honeybadger_deployment.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: honeybadger_deployment diff --git a/monitoring/librato_annotation.py b/monitoring/librato_annotation.py index d73de459834..838abf14e60 100644 --- a/monitoring/librato_annotation.py +++ b/monitoring/librato_annotation.py @@ -20,6 +20,10 @@ # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: librato_annotation diff --git a/monitoring/logentries.py b/monitoring/logentries.py index c772b6a1cd0..a85679ef2eb 100644 --- a/monitoring/logentries.py +++ b/monitoring/logentries.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with this software. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: logentries diff --git a/monitoring/logicmonitor.py b/monitoring/logicmonitor.py index 3f2267cfd76..f2267207a71 100644 --- a/monitoring/logicmonitor.py +++ b/monitoring/logicmonitor.py @@ -65,6 +65,10 @@ ''' +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: logicmonitor diff --git a/monitoring/logicmonitor_facts.py b/monitoring/logicmonitor_facts.py index 71705052b1d..5ade901a76a 100644 --- a/monitoring/logicmonitor_facts.py +++ b/monitoring/logicmonitor_facts.py @@ -52,6 +52,10 @@ HAS_LIB_JSON = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: logicmonitor_facts diff --git a/monitoring/monit.py b/monitoring/monit.py index f7c46b86bec..5e88c7b54d8 100644 --- a/monitoring/monit.py +++ b/monitoring/monit.py @@ -20,6 +20,10 @@ # import time +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: monit diff --git a/monitoring/nagios.py b/monitoring/nagios.py index b14d03e98f7..78bd897ed1d 100644 --- a/monitoring/nagios.py +++ b/monitoring/nagios.py @@ -15,6 +15,10 @@ # along with this program. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nagios diff --git a/monitoring/newrelic_deployment.py b/monitoring/newrelic_deployment.py index 4d47169e89c..c8f8703230d 100644 --- a/monitoring/newrelic_deployment.py +++ b/monitoring/newrelic_deployment.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: newrelic_deployment diff --git a/monitoring/pagerduty.py b/monitoring/pagerduty.py index e2ba95fef7d..43d93501c16 100644 --- a/monitoring/pagerduty.py +++ b/monitoring/pagerduty.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: pagerduty diff --git a/monitoring/pagerduty_alert.py b/monitoring/pagerduty_alert.py index e2d127f0155..f011b902703 100644 --- a/monitoring/pagerduty_alert.py +++ b/monitoring/pagerduty_alert.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: pagerduty_alert diff --git a/monitoring/pingdom.py b/monitoring/pingdom.py index 8b719e48082..d37ae44ab19 100644 --- a/monitoring/pingdom.py +++ b/monitoring/pingdom.py @@ -15,6 +15,10 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: pingdom diff --git a/monitoring/rollbar_deployment.py b/monitoring/rollbar_deployment.py index c13186474c2..5ee332fcf2c 100644 --- a/monitoring/rollbar_deployment.py +++ b/monitoring/rollbar_deployment.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rollbar_deployment diff --git a/monitoring/sensu_check.py b/monitoring/sensu_check.py index 9acbfefa837..77a39647cf6 100644 --- a/monitoring/sensu_check.py +++ b/monitoring/sensu_check.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: sensu_check diff --git a/monitoring/sensu_subscription.py b/monitoring/sensu_subscription.py index 192b474ee48..90535ad2d0b 100644 --- a/monitoring/sensu_subscription.py +++ b/monitoring/sensu_subscription.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: sensu_subscription diff --git a/monitoring/stackdriver.py b/monitoring/stackdriver.py index 251588695d2..b20b1911588 100644 --- a/monitoring/stackdriver.py +++ b/monitoring/stackdriver.py @@ -15,6 +15,10 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: stackdriver diff --git a/monitoring/statusio_maintenance.py b/monitoring/statusio_maintenance.py index 9e5beb3d68e..5533e454713 100644 --- a/monitoring/statusio_maintenance.py +++ b/monitoring/statusio_maintenance.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: statusio_maintenance diff --git a/monitoring/uptimerobot.py b/monitoring/uptimerobot.py index ec57587416e..3a87c3838a6 100644 --- a/monitoring/uptimerobot.py +++ b/monitoring/uptimerobot.py @@ -15,6 +15,10 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: uptimerobot diff --git a/monitoring/zabbix_group.py b/monitoring/zabbix_group.py index 2dd8d98331e..ff90db01bea 100644 --- a/monitoring/zabbix_group.py +++ b/monitoring/zabbix_group.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: zabbix_group diff --git a/monitoring/zabbix_host.py b/monitoring/zabbix_host.py index 611a5f9faa1..aa113efe508 100644 --- a/monitoring/zabbix_host.py +++ b/monitoring/zabbix_host.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: zabbix_host diff --git a/monitoring/zabbix_hostmacro.py b/monitoring/zabbix_hostmacro.py index 446f658b269..75c552cf229 100644 --- a/monitoring/zabbix_hostmacro.py +++ b/monitoring/zabbix_hostmacro.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: zabbix_hostmacro diff --git a/monitoring/zabbix_maintenance.py b/monitoring/zabbix_maintenance.py index 7fe0a494609..4d4c1d972a2 100644 --- a/monitoring/zabbix_maintenance.py +++ b/monitoring/zabbix_maintenance.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: zabbix_maintenance diff --git a/monitoring/zabbix_screen.py b/monitoring/zabbix_screen.py index 85af561c965..7e0ade2abe7 100644 --- a/monitoring/zabbix_screen.py +++ b/monitoring/zabbix_screen.py @@ -20,6 +20,10 @@ # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: zabbix_screen diff --git a/network/a10/a10_server.py b/network/a10/a10_server.py index 38a3b1a3aa1..3a298cb25f4 100644 --- a/network/a10/a10_server.py +++ b/network/a10/a10_server.py @@ -22,6 +22,10 @@ along with Ansible. If not, see . """ +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: a10_server diff --git a/network/a10/a10_server_axapi3.py b/network/a10/a10_server_axapi3.py index 8363e548d10..46f7bf05746 100644 --- a/network/a10/a10_server_axapi3.py +++ b/network/a10/a10_server_axapi3.py @@ -21,6 +21,10 @@ along with Ansible. If not, see . """ +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: a10_server_axapi3 diff --git a/network/a10/a10_service_group.py b/network/a10/a10_service_group.py index 39716fda49b..486fcb0b3e1 100644 --- a/network/a10/a10_service_group.py +++ b/network/a10/a10_service_group.py @@ -22,6 +22,10 @@ along with Ansible. If not, see . """ +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: a10_service_group diff --git a/network/a10/a10_virtual_server.py b/network/a10/a10_virtual_server.py index 783cfa4baa4..212e65203ac 100644 --- a/network/a10/a10_virtual_server.py +++ b/network/a10/a10_virtual_server.py @@ -22,6 +22,10 @@ along with Ansible. If not, see . """ +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: a10_virtual_server diff --git a/network/asa/asa_acl.py b/network/asa/asa_acl.py index fd547c69f89..366284155f2 100644 --- a/network/asa/asa_acl.py +++ b/network/asa/asa_acl.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: asa_acl diff --git a/network/asa/asa_command.py b/network/asa/asa_command.py index 9d013ebd197..3bffcca0425 100644 --- a/network/asa/asa_command.py +++ b/network/asa/asa_command.py @@ -17,6 +17,10 @@ # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: asa_command diff --git a/network/asa/asa_config.py b/network/asa/asa_config.py index 320255b46f0..ffd082684ec 100644 --- a/network/asa/asa_config.py +++ b/network/asa/asa_config.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: asa_config diff --git a/network/citrix/netscaler.py b/network/citrix/netscaler.py index c84cf740bd2..30442ade78c 100644 --- a/network/citrix/netscaler.py +++ b/network/citrix/netscaler.py @@ -21,6 +21,10 @@ along with Ansible. If not, see . """ +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: netscaler diff --git a/network/cloudflare_dns.py b/network/cloudflare_dns.py index 92052c0d014..621e92ac1f0 100644 --- a/network/cloudflare_dns.py +++ b/network/cloudflare_dns.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cloudflare_dns diff --git a/network/dnsimple.py b/network/dnsimple.py index 0042229c976..3f6c2188b04 100644 --- a/network/dnsimple.py +++ b/network/dnsimple.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: dnsimple diff --git a/network/dnsmadeeasy.py b/network/dnsmadeeasy.py index c4007542a66..7650960e434 100644 --- a/network/dnsmadeeasy.py +++ b/network/dnsmadeeasy.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: dnsmadeeasy diff --git a/network/exoscale/exo_dns_domain.py b/network/exoscale/exo_dns_domain.py index d886728bea5..b0046c803dc 100644 --- a/network/exoscale/exo_dns_domain.py +++ b/network/exoscale/exo_dns_domain.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: exo_dns_domain diff --git a/network/exoscale/exo_dns_record.py b/network/exoscale/exo_dns_record.py index 6395990639e..495508d3d47 100644 --- a/network/exoscale/exo_dns_record.py +++ b/network/exoscale/exo_dns_record.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: exo_dns_record diff --git a/network/f5/bigip_device_dns.py b/network/f5/bigip_device_dns.py index a3f855e6be8..a6c1e8e30d7 100644 --- a/network/f5/bigip_device_dns.py +++ b/network/f5/bigip_device_dns.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: bigip_device_dns diff --git a/network/f5/bigip_device_ntp.py b/network/f5/bigip_device_ntp.py index a58a9f31ce7..23ed81b7819 100644 --- a/network/f5/bigip_device_ntp.py +++ b/network/f5/bigip_device_ntp.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: bigip_device_ntp diff --git a/network/f5/bigip_device_sshd.py b/network/f5/bigip_device_sshd.py index d69292f6017..87ffeb6bee0 100644 --- a/network/f5/bigip_device_sshd.py +++ b/network/f5/bigip_device_sshd.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: bigip_device_sshd diff --git a/network/f5/bigip_facts.py b/network/f5/bigip_facts.py index dc6c6b7d1dc..33d5e1937e6 100644 --- a/network/f5/bigip_facts.py +++ b/network/f5/bigip_facts.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: bigip_facts diff --git a/network/f5/bigip_gtm_datacenter.py b/network/f5/bigip_gtm_datacenter.py index 36308896ce1..fff876007cf 100644 --- a/network/f5/bigip_gtm_datacenter.py +++ b/network/f5/bigip_gtm_datacenter.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: bigip_gtm_datacenter diff --git a/network/f5/bigip_gtm_facts.py b/network/f5/bigip_gtm_facts.py index bd54904f95c..9e3fc8b492f 100644 --- a/network/f5/bigip_gtm_facts.py +++ b/network/f5/bigip_gtm_facts.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: bigip_gtm_facts diff --git a/network/f5/bigip_gtm_virtual_server.py b/network/f5/bigip_gtm_virtual_server.py index 13fc8508f9e..03be3a9df64 100644 --- a/network/f5/bigip_gtm_virtual_server.py +++ b/network/f5/bigip_gtm_virtual_server.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: bigip_gtm_virtual_server diff --git a/network/f5/bigip_gtm_wide_ip.py b/network/f5/bigip_gtm_wide_ip.py index 4067311b4e6..c1712902f40 100644 --- a/network/f5/bigip_gtm_wide_ip.py +++ b/network/f5/bigip_gtm_wide_ip.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: bigip_gtm_wide_ip diff --git a/network/f5/bigip_hostname.py b/network/f5/bigip_hostname.py index 72f423de142..9dc9d085c5a 100644 --- a/network/f5/bigip_hostname.py +++ b/network/f5/bigip_hostname.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: bigip_hostname diff --git a/network/f5/bigip_irule.py b/network/f5/bigip_irule.py index f2be61dfd89..52b8f30fb58 100644 --- a/network/f5/bigip_irule.py +++ b/network/f5/bigip_irule.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: bigip_irule diff --git a/network/f5/bigip_monitor_http.py b/network/f5/bigip_monitor_http.py index 3c303c3ce51..02017569c8c 100644 --- a/network/f5/bigip_monitor_http.py +++ b/network/f5/bigip_monitor_http.py @@ -20,6 +20,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: bigip_monitor_http diff --git a/network/f5/bigip_monitor_tcp.py b/network/f5/bigip_monitor_tcp.py index 45756b1ba28..aedc71f642b 100644 --- a/network/f5/bigip_monitor_tcp.py +++ b/network/f5/bigip_monitor_tcp.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: bigip_monitor_tcp diff --git a/network/f5/bigip_node.py b/network/f5/bigip_node.py index 53a0b1973f2..08107f6e2ce 100644 --- a/network/f5/bigip_node.py +++ b/network/f5/bigip_node.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: bigip_node diff --git a/network/f5/bigip_pool.py b/network/f5/bigip_pool.py index 69ee1c07508..eb6b8f3adaa 100644 --- a/network/f5/bigip_pool.py +++ b/network/f5/bigip_pool.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: bigip_pool diff --git a/network/f5/bigip_pool_member.py b/network/f5/bigip_pool_member.py index f93ac271ec0..42d4538f9f6 100644 --- a/network/f5/bigip_pool_member.py +++ b/network/f5/bigip_pool_member.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: bigip_pool_member diff --git a/network/f5/bigip_routedomain.py b/network/f5/bigip_routedomain.py index f679dd03819..7abe77abac2 100644 --- a/network/f5/bigip_routedomain.py +++ b/network/f5/bigip_routedomain.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: bigip_routedomain diff --git a/network/f5/bigip_selfip.py b/network/f5/bigip_selfip.py index fbc31f80b93..d60dafbf7ce 100644 --- a/network/f5/bigip_selfip.py +++ b/network/f5/bigip_selfip.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: bigip_selfip diff --git a/network/f5/bigip_snat_pool.py b/network/f5/bigip_snat_pool.py index bacdf218cd6..52341e4dfe8 100644 --- a/network/f5/bigip_snat_pool.py +++ b/network/f5/bigip_snat_pool.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: bigip_snat_pool diff --git a/network/f5/bigip_ssl_certificate.py b/network/f5/bigip_ssl_certificate.py index 9c6034d513e..fe0a753e834 100644 --- a/network/f5/bigip_ssl_certificate.py +++ b/network/f5/bigip_ssl_certificate.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: bigip_ssl_certificate short_description: Import/Delete certificates from BIG-IP diff --git a/network/f5/bigip_sys_db.py b/network/f5/bigip_sys_db.py index 272fbf266c5..b451461b9c2 100644 --- a/network/f5/bigip_sys_db.py +++ b/network/f5/bigip_sys_db.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: bigip_sys_db diff --git a/network/f5/bigip_sys_global.py b/network/f5/bigip_sys_global.py index 8a86ac50bba..7e6cfd78064 100644 --- a/network/f5/bigip_sys_global.py +++ b/network/f5/bigip_sys_global.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: bigip_sys_global diff --git a/network/f5/bigip_virtual_server.py b/network/f5/bigip_virtual_server.py index f1c6905ca1d..ddcf2cd0e6a 100644 --- a/network/f5/bigip_virtual_server.py +++ b/network/f5/bigip_virtual_server.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: bigip_virtual_server diff --git a/network/f5/bigip_vlan.py b/network/f5/bigip_vlan.py index 24e3a380295..40df948f6c6 100644 --- a/network/f5/bigip_vlan.py +++ b/network/f5/bigip_vlan.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: bigip_vlan diff --git a/network/haproxy.py b/network/haproxy.py index 4cfc8128674..5ee3006629e 100644 --- a/network/haproxy.py +++ b/network/haproxy.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: haproxy diff --git a/network/illumos/dladm_etherstub.py b/network/illumos/dladm_etherstub.py index 3107f8e843e..861e0a70131 100644 --- a/network/illumos/dladm_etherstub.py +++ b/network/illumos/dladm_etherstub.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: dladm_etherstub diff --git a/network/illumos/dladm_vnic.py b/network/illumos/dladm_vnic.py index 81be07313a5..0718517d475 100644 --- a/network/illumos/dladm_vnic.py +++ b/network/illumos/dladm_vnic.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: dladm_vnic diff --git a/network/illumos/flowadm.py b/network/illumos/flowadm.py index 82e0250b701..8b5807f7090 100644 --- a/network/illumos/flowadm.py +++ b/network/illumos/flowadm.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: flowadm diff --git a/network/illumos/ipadm_if.py b/network/illumos/ipadm_if.py index 06db3b6a656..d3d0c0af0bd 100644 --- a/network/illumos/ipadm_if.py +++ b/network/illumos/ipadm_if.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ipadm_if diff --git a/network/illumos/ipadm_prop.py b/network/illumos/ipadm_prop.py index 15410eb5e09..509ff82b1f7 100644 --- a/network/illumos/ipadm_prop.py +++ b/network/illumos/ipadm_prop.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ipadm_prop diff --git a/network/ipify_facts.py b/network/ipify_facts.py index 7a07f577c2e..4ffe19d3f5c 100644 --- a/network/ipify_facts.py +++ b/network/ipify_facts.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ipify_facts diff --git a/network/ipinfoio_facts.py b/network/ipinfoio_facts.py index fadcaa5c8f0..748c49dcc9a 100644 --- a/network/ipinfoio_facts.py +++ b/network/ipinfoio_facts.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': 'preview', + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ipinfoio_facts diff --git a/network/lldp.py b/network/lldp.py index acbc914112a..f222d765fe9 100644 --- a/network/lldp.py +++ b/network/lldp.py @@ -16,6 +16,10 @@ import subprocess +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: lldp diff --git a/network/netconf/netconf_config.py b/network/netconf/netconf_config.py index 43baa63a5da..7ed79a908b5 100755 --- a/network/netconf/netconf_config.py +++ b/network/netconf/netconf_config.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: netconf_config diff --git a/network/nmcli.py b/network/nmcli.py index 35718cc46c0..86a844c7ee0 100644 --- a/network/nmcli.py +++ b/network/nmcli.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION=''' --- module: nmcli diff --git a/network/openvswitch_bridge.py b/network/openvswitch_bridge.py index 1b9a7f7abf9..9816e2bff3a 100644 --- a/network/openvswitch_bridge.py +++ b/network/openvswitch_bridge.py @@ -22,6 +22,10 @@ # pylint: disable=C0111 +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: openvswitch_bridge diff --git a/network/openvswitch_db.py b/network/openvswitch_db.py index 39f27649cfb..6d769e43672 100644 --- a/network/openvswitch_db.py +++ b/network/openvswitch_db.py @@ -23,6 +23,10 @@ # You should have received a copy of the GNU General Public License # along with this software. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: openvswitch_db diff --git a/network/openvswitch_port.py b/network/openvswitch_port.py index ad7ec0865bb..759a2489c16 100644 --- a/network/openvswitch_port.py +++ b/network/openvswitch_port.py @@ -22,6 +22,10 @@ # You should have received a copy of the GNU General Public License # along with this software. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: openvswitch_port diff --git a/network/panos/panos_admin.py b/network/panos/panos_admin.py index b9e24241b04..dd36ac08977 100755 --- a/network/panos/panos_admin.py +++ b/network/panos/panos_admin.py @@ -19,6 +19,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: panos_admin diff --git a/network/snmp_facts.py b/network/snmp_facts.py index eee7a4690ba..7801d0f2955 100644 --- a/network/snmp_facts.py +++ b/network/snmp_facts.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: snmp_facts diff --git a/network/wakeonlan.py b/network/wakeonlan.py index 4e7e7176fec..d49118d60ba 100644 --- a/network/wakeonlan.py +++ b/network/wakeonlan.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: wakeonlan diff --git a/notification/campfire.py b/notification/campfire.py index 9f9c34c5da4..8a7b44355f4 100644 --- a/notification/campfire.py +++ b/notification/campfire.py @@ -15,6 +15,10 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: campfire diff --git a/notification/flowdock.py b/notification/flowdock.py index 7297ef1f63a..e0584295afa 100644 --- a/notification/flowdock.py +++ b/notification/flowdock.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: flowdock diff --git a/notification/grove.py b/notification/grove.py index 9db937c0cf7..fe16289a220 100644 --- a/notification/grove.py +++ b/notification/grove.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: grove diff --git a/notification/hall.py b/notification/hall.py index 162bb5153b4..d8766412d01 100755 --- a/notification/hall.py +++ b/notification/hall.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ module: hall short_description: Send notification to Hall diff --git a/notification/hipchat.py b/notification/hipchat.py index 9455c09500c..f321a6b9141 100644 --- a/notification/hipchat.py +++ b/notification/hipchat.py @@ -15,6 +15,10 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: hipchat diff --git a/notification/irc.py b/notification/irc.py index 765e155551d..d2fa22a4f52 100644 --- a/notification/irc.py +++ b/notification/irc.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: irc diff --git a/notification/jabber.py b/notification/jabber.py index 4da7f8296fc..f68790fb296 100644 --- a/notification/jabber.py +++ b/notification/jabber.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- version_added: "1.2" diff --git a/notification/mail.py b/notification/mail.py index f51982f37c1..51902f3f87f 100644 --- a/notification/mail.py +++ b/notification/mail.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = """ --- author: "Dag Wieers (@dagwieers)" diff --git a/notification/mqtt.py b/notification/mqtt.py index b28e57dc4a8..b13124b4f01 100644 --- a/notification/mqtt.py +++ b/notification/mqtt.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: mqtt diff --git a/notification/nexmo.py b/notification/nexmo.py index fa6b0b2225a..9fafcc03769 100644 --- a/notification/nexmo.py +++ b/notification/nexmo.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ module: nexmo short_description: Send a SMS via nexmo diff --git a/notification/osx_say.py b/notification/osx_say.py index 70946228489..ff6d3ae0147 100644 --- a/notification/osx_say.py +++ b/notification/osx_say.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: osx_say diff --git a/notification/pushbullet.py b/notification/pushbullet.py index 434eb1fda0d..ed09be8f516 100644 --- a/notification/pushbullet.py +++ b/notification/pushbullet.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- author: "Willy Barro (@willybarro)" diff --git a/notification/pushover.py b/notification/pushover.py index ac924193fec..294da075cec 100644 --- a/notification/pushover.py +++ b/notification/pushover.py @@ -20,6 +20,10 @@ ### +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: pushover diff --git a/notification/rocketchat.py b/notification/rocketchat.py index ffce79712b1..f7089f7984f 100644 --- a/notification/rocketchat.py +++ b/notification/rocketchat.py @@ -20,6 +20,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ module: rocketchat short_description: Send notifications to Rocket Chat diff --git a/notification/sendgrid.py b/notification/sendgrid.py index ac2db6b1ce7..b0821983dc7 100644 --- a/notification/sendgrid.py +++ b/notification/sendgrid.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- version_added: "2.0" diff --git a/notification/slack.py b/notification/slack.py index 3639b6d8ac0..3d50e89df95 100644 --- a/notification/slack.py +++ b/notification/slack.py @@ -20,6 +20,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ module: slack short_description: Send Slack notifications diff --git a/notification/sns.py b/notification/sns.py index 4eb79e13ade..8e5a07dad63 100644 --- a/notification/sns.py +++ b/notification/sns.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ module: sns short_description: Send Amazon Simple Notification Service (SNS) messages diff --git a/notification/telegram.py b/notification/telegram.py index a12fd46929d..57746cf06ae 100644 --- a/notification/telegram.py +++ b/notification/telegram.py @@ -20,6 +20,10 @@ # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ module: telegram diff --git a/notification/twilio.py b/notification/twilio.py index 2c7275a3e9f..1d7e059e5c8 100644 --- a/notification/twilio.py +++ b/notification/twilio.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- version_added: "1.6" diff --git a/notification/typetalk.py b/notification/typetalk.py index 4d4cf8a2f8a..f638be09ab2 100644 --- a/notification/typetalk.py +++ b/notification/typetalk.py @@ -15,6 +15,10 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: typetalk diff --git a/packaging/dpkg_selections.py b/packaging/dpkg_selections.py index bf13dc13fd7..f26ad68f02d 100644 --- a/packaging/dpkg_selections.py +++ b/packaging/dpkg_selections.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: dpkg_selections diff --git a/packaging/elasticsearch_plugin.py b/packaging/elasticsearch_plugin.py index e89361edd89..8a165189625 100644 --- a/packaging/elasticsearch_plugin.py +++ b/packaging/elasticsearch_plugin.py @@ -22,6 +22,10 @@ along with Ansible. If not, see . """ +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: elasticsearch_plugin diff --git a/packaging/kibana_plugin.py b/packaging/kibana_plugin.py index e877d756b08..91e2f23cf57 100644 --- a/packaging/kibana_plugin.py +++ b/packaging/kibana_plugin.py @@ -23,6 +23,10 @@ import os +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: kibana_plugin diff --git a/packaging/language/bower.py b/packaging/language/bower.py index 1627741d5a6..489ab3cb804 100644 --- a/packaging/language/bower.py +++ b/packaging/language/bower.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: bower diff --git a/packaging/language/bundler.py b/packaging/language/bundler.py index fc647862fd8..e7950b08548 100644 --- a/packaging/language/bundler.py +++ b/packaging/language/bundler.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION=''' --- module: bundler diff --git a/packaging/language/composer.py b/packaging/language/composer.py index 9ff393b25b7..172acb4ad1c 100644 --- a/packaging/language/composer.py +++ b/packaging/language/composer.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: composer diff --git a/packaging/language/cpanm.py b/packaging/language/cpanm.py index addf0c198d7..59677698069 100644 --- a/packaging/language/cpanm.py +++ b/packaging/language/cpanm.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cpanm diff --git a/packaging/language/maven_artifact.py b/packaging/language/maven_artifact.py index 3e7fddbd0af..d4a241d0e9d 100644 --- a/packaging/language/maven_artifact.py +++ b/packaging/language/maven_artifact.py @@ -35,6 +35,10 @@ except ImportError: HAS_BOTO = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: maven_artifact diff --git a/packaging/language/npm.py b/packaging/language/npm.py index 4b147e37035..b1df88e60a2 100644 --- a/packaging/language/npm.py +++ b/packaging/language/npm.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: npm diff --git a/packaging/language/pear.py b/packaging/language/pear.py index 880c275f25b..0379538874d 100644 --- a/packaging/language/pear.py +++ b/packaging/language/pear.py @@ -20,6 +20,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: pear diff --git a/packaging/os/apk.py b/packaging/os/apk.py index 898b69a3043..8d8c5a6f808 100644 --- a/packaging/os/apk.py +++ b/packaging/os/apk.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with this software. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: apk diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index 715e5d0bf65..016fdf60453 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -20,6 +20,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: dnf diff --git a/packaging/os/homebrew.py b/packaging/os/homebrew.py index ca12cb8c608..c44ccabbe6f 100755 --- a/packaging/os/homebrew.py +++ b/packaging/os/homebrew.py @@ -20,6 +20,10 @@ # You should have received a copy of the GNU General Public License # along with this software. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: homebrew diff --git a/packaging/os/homebrew_cask.py b/packaging/os/homebrew_cask.py index fdfa3f0cdbf..86d7f35e0ca 100755 --- a/packaging/os/homebrew_cask.py +++ b/packaging/os/homebrew_cask.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with this software. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: homebrew_cask diff --git a/packaging/os/homebrew_tap.py b/packaging/os/homebrew_tap.py index 2a981deaeb8..649a32f1b89 100644 --- a/packaging/os/homebrew_tap.py +++ b/packaging/os/homebrew_tap.py @@ -23,6 +23,10 @@ import re +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: homebrew_tap diff --git a/packaging/os/layman.py b/packaging/os/layman.py index f18d0eaa0a7..440001b48a0 100644 --- a/packaging/os/layman.py +++ b/packaging/os/layman.py @@ -21,6 +21,10 @@ import shutil from os import path +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: layman diff --git a/packaging/os/macports.py b/packaging/os/macports.py index 1e56fc84f0b..ac49f1568e5 100644 --- a/packaging/os/macports.py +++ b/packaging/os/macports.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with this software. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: macports diff --git a/packaging/os/openbsd_pkg.py b/packaging/os/openbsd_pkg.py index f86a4d081ae..7d0e9ac9459 100644 --- a/packaging/os/openbsd_pkg.py +++ b/packaging/os/openbsd_pkg.py @@ -26,6 +26,10 @@ from distutils.version import StrictVersion +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: openbsd_pkg diff --git a/packaging/os/opkg.py b/packaging/os/opkg.py index 50b37d3cdb5..6360f45af33 100644 --- a/packaging/os/opkg.py +++ b/packaging/os/opkg.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with this software. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: opkg diff --git a/packaging/os/pacman.py b/packaging/os/pacman.py index c27a67c9e7b..89766a49745 100644 --- a/packaging/os/pacman.py +++ b/packaging/os/pacman.py @@ -20,6 +20,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: pacman diff --git a/packaging/os/pkg5.py b/packaging/os/pkg5.py index 86a555a37fa..4c02d63821a 100644 --- a/packaging/os/pkg5.py +++ b/packaging/os/pkg5.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: pkg5 diff --git a/packaging/os/pkg5_publisher.py b/packaging/os/pkg5_publisher.py index 4eee4d4f31c..279b40f0090 100644 --- a/packaging/os/pkg5_publisher.py +++ b/packaging/os/pkg5_publisher.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: pkg5_publisher diff --git a/packaging/os/pkgin.py b/packaging/os/pkgin.py index 19f9a157b37..8e75f2d18ce 100755 --- a/packaging/os/pkgin.py +++ b/packaging/os/pkgin.py @@ -23,6 +23,10 @@ # along with this software. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: pkgin diff --git a/packaging/os/pkgng.py b/packaging/os/pkgng.py index 4863a55d23e..5727b190031 100644 --- a/packaging/os/pkgng.py +++ b/packaging/os/pkgng.py @@ -21,6 +21,10 @@ # along with this software. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: pkgng diff --git a/packaging/os/pkgutil.py b/packaging/os/pkgutil.py index 5323581ba57..a54e96eeb08 100644 --- a/packaging/os/pkgutil.py +++ b/packaging/os/pkgutil.py @@ -21,6 +21,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: pkgutil diff --git a/packaging/os/portage.py b/packaging/os/portage.py index dc4c22890f6..5debeda058c 100644 --- a/packaging/os/portage.py +++ b/packaging/os/portage.py @@ -21,6 +21,10 @@ # along with this software. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: portage diff --git a/packaging/os/portinstall.py b/packaging/os/portinstall.py index 78555a02194..ccd301e526a 100644 --- a/packaging/os/portinstall.py +++ b/packaging/os/portinstall.py @@ -19,6 +19,10 @@ # along with this software. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: portinstall diff --git a/packaging/os/slackpkg.py b/packaging/os/slackpkg.py index ac0c230bfa5..3c4ee4f62e2 100644 --- a/packaging/os/slackpkg.py +++ b/packaging/os/slackpkg.py @@ -22,6 +22,10 @@ # along with this software. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: slackpkg diff --git a/packaging/os/svr4pkg.py b/packaging/os/svr4pkg.py index e5931941562..81409e3b2dd 100644 --- a/packaging/os/svr4pkg.py +++ b/packaging/os/svr4pkg.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: svr4pkg diff --git a/packaging/os/swdepot.py b/packaging/os/swdepot.py index 017e91b0b58..6ea7d1059be 100644 --- a/packaging/os/swdepot.py +++ b/packaging/os/swdepot.py @@ -21,6 +21,10 @@ import re import pipes +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: swdepot diff --git a/packaging/os/urpmi.py b/packaging/os/urpmi.py index 934ef11ee22..e995f1d4894 100644 --- a/packaging/os/urpmi.py +++ b/packaging/os/urpmi.py @@ -19,6 +19,10 @@ # along with this software. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: urpmi diff --git a/packaging/os/yum_repository.py b/packaging/os/yum_repository.py index 3f14ff50f99..1d00d26f682 100644 --- a/packaging/os/yum_repository.py +++ b/packaging/os/yum_repository.py @@ -25,6 +25,10 @@ from ansible.module_utils.six.moves import configparser +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: yum_repository diff --git a/packaging/os/zypper.py b/packaging/os/zypper.py index c91528b2199..837a7ef4774 100644 --- a/packaging/os/zypper.py +++ b/packaging/os/zypper.py @@ -29,6 +29,10 @@ from xml.dom.minidom import parseString as parseXML import re +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: zypper diff --git a/packaging/os/zypper_repository.py b/packaging/os/zypper_repository.py index 7fae6065bcb..187e5803674 100644 --- a/packaging/os/zypper_repository.py +++ b/packaging/os/zypper_repository.py @@ -20,6 +20,10 @@ # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: zypper_repository diff --git a/remote_management/ipmi/ipmi_boot.py b/remote_management/ipmi/ipmi_boot.py index 6c0c9cdc12a..06281d4d46f 100644 --- a/remote_management/ipmi/ipmi_boot.py +++ b/remote_management/ipmi/ipmi_boot.py @@ -24,6 +24,10 @@ from ansible.module_utils.basic import * +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ipmi_boot diff --git a/remote_management/ipmi/ipmi_power.py b/remote_management/ipmi/ipmi_power.py index fc702fa1def..b661be4c535 100644 --- a/remote_management/ipmi/ipmi_power.py +++ b/remote_management/ipmi/ipmi_power.py @@ -24,6 +24,10 @@ from ansible.module_utils.basic import * +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ipmi_power diff --git a/source_control/bzr.py b/source_control/bzr.py index 961c715d905..f66c00abf82 100644 --- a/source_control/bzr.py +++ b/source_control/bzr.py @@ -19,6 +19,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = u''' --- module: bzr diff --git a/source_control/git_config.py b/source_control/git_config.py index 7ce01cd9c4e..16f2457dd98 100644 --- a/source_control/git_config.py +++ b/source_control/git_config.py @@ -19,6 +19,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: git_config diff --git a/source_control/github_hooks.py b/source_control/github_hooks.py index 0430b44007e..ce76b503c23 100644 --- a/source_control/github_hooks.py +++ b/source_control/github_hooks.py @@ -29,6 +29,10 @@ import base64 +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: github_hooks diff --git a/source_control/github_key.py b/source_control/github_key.py index c27285625a4..cc54734e004 100644 --- a/source_control/github_key.py +++ b/source_control/github_key.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: github_key short_description: Manage GitHub access keys. diff --git a/source_control/github_release.py b/source_control/github_release.py index daeb005e87c..ac59e6b69ae 100644 --- a/source_control/github_release.py +++ b/source_control/github_release.py @@ -17,6 +17,10 @@ # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: github_release diff --git a/source_control/gitlab_group.py b/source_control/gitlab_group.py index a5fa98d13f1..4c133028474 100644 --- a/source_control/gitlab_group.py +++ b/source_control/gitlab_group.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: gitlab_group diff --git a/source_control/gitlab_project.py b/source_control/gitlab_project.py index da21589186c..94852afac86 100644 --- a/source_control/gitlab_project.py +++ b/source_control/gitlab_project.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: gitlab_project diff --git a/source_control/gitlab_user.py b/source_control/gitlab_user.py index 826b9f6f691..e289d70e2c0 100644 --- a/source_control/gitlab_user.py +++ b/source_control/gitlab_user.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: gitlab_user diff --git a/storage/netapp/netapp_e_amg.py b/storage/netapp/netapp_e_amg.py index 44189988be4..e5f60b29454 100644 --- a/storage/netapp/netapp_e_amg.py +++ b/storage/netapp/netapp_e_amg.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: netapp_e_amg diff --git a/storage/netapp/netapp_e_amg_role.py b/storage/netapp/netapp_e_amg_role.py index 7a2f1bdf18b..bfe3c4b8334 100644 --- a/storage/netapp/netapp_e_amg_role.py +++ b/storage/netapp/netapp_e_amg_role.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: netapp_e_amg_role diff --git a/storage/netapp/netapp_e_amg_sync.py b/storage/netapp/netapp_e_amg_sync.py index a86b594f3b0..548b115ff0a 100644 --- a/storage/netapp/netapp_e_amg_sync.py +++ b/storage/netapp/netapp_e_amg_sync.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: netapp_e_amg_sync diff --git a/storage/netapp/netapp_e_auth.py b/storage/netapp/netapp_e_auth.py index c22de91cd12..19bdb0bfea5 100644 --- a/storage/netapp/netapp_e_auth.py +++ b/storage/netapp/netapp_e_auth.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: netapp_e_auth diff --git a/storage/netapp/netapp_e_facts.py b/storage/netapp/netapp_e_facts.py index 37e3f827627..5a877afab61 100644 --- a/storage/netapp/netapp_e_facts.py +++ b/storage/netapp/netapp_e_facts.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: netapp_e_facts version_added: '2.2' diff --git a/storage/netapp/netapp_e_flashcache.py b/storage/netapp/netapp_e_flashcache.py index 5fa4a669747..da7d520542b 100644 --- a/storage/netapp/netapp_e_flashcache.py +++ b/storage/netapp/netapp_e_flashcache.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: netapp_e_flashcache author: Kevin Hulquest (@hulquest) diff --git a/storage/netapp/netapp_e_host.py b/storage/netapp/netapp_e_host.py index 2261d8264de..458bb6fb8b6 100644 --- a/storage/netapp/netapp_e_host.py +++ b/storage/netapp/netapp_e_host.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: netapp_e_host diff --git a/storage/netapp/netapp_e_hostgroup.py b/storage/netapp/netapp_e_hostgroup.py index 5248c1d9531..f89397af59d 100644 --- a/storage/netapp/netapp_e_hostgroup.py +++ b/storage/netapp/netapp_e_hostgroup.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: netapp_e_hostgroup diff --git a/storage/netapp/netapp_e_lun_mapping.py b/storage/netapp/netapp_e_lun_mapping.py index be3c27515e5..5c9d71973b4 100644 --- a/storage/netapp/netapp_e_lun_mapping.py +++ b/storage/netapp/netapp_e_lun_mapping.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: netapp_e_lun_mapping diff --git a/storage/netapp/netapp_e_snapshot_group.py b/storage/netapp/netapp_e_snapshot_group.py index 90c6e8471bb..f0464bbf7c8 100644 --- a/storage/netapp/netapp_e_snapshot_group.py +++ b/storage/netapp/netapp_e_snapshot_group.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: netapp_e_snapshot_group diff --git a/storage/netapp/netapp_e_snapshot_images.py b/storage/netapp/netapp_e_snapshot_images.py index 8c81af84535..460d1a2a0c1 100644 --- a/storage/netapp/netapp_e_snapshot_images.py +++ b/storage/netapp/netapp_e_snapshot_images.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: netapp_e_snapshot_images diff --git a/storage/netapp/netapp_e_snapshot_volume.py b/storage/netapp/netapp_e_snapshot_volume.py index 9a143bd4125..afc6e340aaf 100644 --- a/storage/netapp/netapp_e_snapshot_volume.py +++ b/storage/netapp/netapp_e_snapshot_volume.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: netapp_e_snapshot_volume diff --git a/storage/netapp/netapp_e_storage_system.py b/storage/netapp/netapp_e_storage_system.py index 40ef893ad9b..64414af6f1e 100644 --- a/storage/netapp/netapp_e_storage_system.py +++ b/storage/netapp/netapp_e_storage_system.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: netapp_e_storage_system version_added: "2.2" diff --git a/storage/netapp/netapp_e_storagepool.py b/storage/netapp/netapp_e_storagepool.py index 1d86ef46f6b..89309708efd 100644 --- a/storage/netapp/netapp_e_storagepool.py +++ b/storage/netapp/netapp_e_storagepool.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: netapp_e_storagepool diff --git a/storage/netapp/netapp_e_volume.py b/storage/netapp/netapp_e_volume.py index 09825c5201e..26107965855 100644 --- a/storage/netapp/netapp_e_volume.py +++ b/storage/netapp/netapp_e_volume.py @@ -20,6 +20,10 @@ from ansible.module_utils.api import basic_auth_argument_spec +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: netapp_e_volume diff --git a/storage/netapp/netapp_e_volume_copy.py b/storage/netapp/netapp_e_volume_copy.py index f715c84088f..179ee8ff5ad 100644 --- a/storage/netapp/netapp_e_volume_copy.py +++ b/storage/netapp/netapp_e_volume_copy.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: netapp_e_volume_copy diff --git a/system/alternatives.py b/system/alternatives.py index f4ae56ebe93..833ef27aaa5 100644 --- a/system/alternatives.py +++ b/system/alternatives.py @@ -22,6 +22,10 @@ along with Ansible. If not, see . """ +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: alternatives diff --git a/system/at.py b/system/at.py index 9c5f10b5947..2c01c5d3195 100644 --- a/system/at.py +++ b/system/at.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: at diff --git a/system/capabilities.py b/system/capabilities.py index 67cd66b2b0c..27f3c7519cc 100644 --- a/system/capabilities.py +++ b/system/capabilities.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: capabilities diff --git a/system/cronvar.py b/system/cronvar.py index 1a26182be88..a65610811b7 100644 --- a/system/cronvar.py +++ b/system/cronvar.py @@ -26,6 +26,10 @@ # This module is based on the crontab module. # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: cronvar diff --git a/system/crypttab.py b/system/crypttab.py index 27523415dfb..f957a51293a 100644 --- a/system/crypttab.py +++ b/system/crypttab.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: crypttab diff --git a/system/debconf.py b/system/debconf.py index 3c9218b408e..224f2fbcb9b 100644 --- a/system/debconf.py +++ b/system/debconf.py @@ -21,6 +21,10 @@ along with Ansible. If not, see . """ +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: debconf diff --git a/system/facter.py b/system/facter.py index d9a7d65cca9..5ae13ab7371 100644 --- a/system/facter.py +++ b/system/facter.py @@ -20,6 +20,10 @@ # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: facter diff --git a/system/filesystem.py b/system/filesystem.py index 70c7c320b31..d49360f09bc 100644 --- a/system/filesystem.py +++ b/system/filesystem.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- author: "Alexander Bulimov (@abulimov)" diff --git a/system/firewalld.py b/system/firewalld.py index 83f78b049e1..8324069b1b3 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: firewalld diff --git a/system/getent.py b/system/getent.py index d200d420fd4..960a1221f70 100644 --- a/system/getent.py +++ b/system/getent.py @@ -20,6 +20,10 @@ # +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: getent diff --git a/system/gluster_volume.py b/system/gluster_volume.py index f34511a3eaf..7fcca45886d 100644 --- a/system/gluster_volume.py +++ b/system/gluster_volume.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ module: gluster_volume short_description: Manage GlusterFS volumes diff --git a/system/iptables.py b/system/iptables.py index 8a08e38d785..521ad6b043a 100644 --- a/system/iptables.py +++ b/system/iptables.py @@ -23,6 +23,10 @@ ipv6='ip6tables', ) +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: iptables diff --git a/system/kernel_blacklist.py b/system/kernel_blacklist.py index 701ba883517..5498f10b3a1 100644 --- a/system/kernel_blacklist.py +++ b/system/kernel_blacklist.py @@ -22,6 +22,10 @@ import re +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: kernel_blacklist diff --git a/system/known_hosts.py b/system/known_hosts.py index 656fb38d4a1..69210d9fdf2 100644 --- a/system/known_hosts.py +++ b/system/known_hosts.py @@ -18,6 +18,10 @@ along with this module. If not, see . """ +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: known_hosts diff --git a/system/locale_gen.py b/system/locale_gen.py index bd4b149dcce..b56a5e498e2 100644 --- a/system/locale_gen.py +++ b/system/locale_gen.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: locale_gen diff --git a/system/lvg.py b/system/lvg.py index 427cb1b1c1d..9c638f4d317 100644 --- a/system/lvg.py +++ b/system/lvg.py @@ -19,6 +19,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- author: "Alexander Bulimov (@abulimov)" diff --git a/system/lvol.py b/system/lvol.py index c3213bdd241..3ab60cb40ac 100644 --- a/system/lvol.py +++ b/system/lvol.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- author: diff --git a/system/make.py b/system/make.py index 5207470bb0d..2b618db9fac 100644 --- a/system/make.py +++ b/system/make.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with this software. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: make diff --git a/system/modprobe.py b/system/modprobe.py index ef5a9dd4ed8..d84f0d3377d 100644 --- a/system/modprobe.py +++ b/system/modprobe.py @@ -19,6 +19,10 @@ # along with this software. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: modprobe diff --git a/system/ohai.py b/system/ohai.py index 222a81e6d93..47926a34d12 100644 --- a/system/ohai.py +++ b/system/ohai.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ohai diff --git a/system/open_iscsi.py b/system/open_iscsi.py index 71eeda1aa8c..2e3c0e838f8 100644 --- a/system/open_iscsi.py +++ b/system/open_iscsi.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: open_iscsi diff --git a/system/openwrt_init.py b/system/openwrt_init.py index 297826076c1..7b4f7f79d37 100644 --- a/system/openwrt_init.py +++ b/system/openwrt_init.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' module: openwrt_init author: diff --git a/system/osx_defaults.py b/system/osx_defaults.py index 12a3ec8f219..757cc811d92 100644 --- a/system/osx_defaults.py +++ b/system/osx_defaults.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: osx_defaults diff --git a/system/pam_limits.py b/system/pam_limits.py index 5b5cc3583e0..f47fbf06bbf 100644 --- a/system/pam_limits.py +++ b/system/pam_limits.py @@ -23,6 +23,10 @@ import shutil import re +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: pam_limits diff --git a/system/puppet.py b/system/puppet.py index 6686682cae1..15acb97d262 100644 --- a/system/puppet.py +++ b/system/puppet.py @@ -29,6 +29,10 @@ pass +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: puppet diff --git a/system/sefcontext.py b/system/sefcontext.py index 120481cad3b..f1000b34cc1 100644 --- a/system/sefcontext.py +++ b/system/sefcontext.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: sefcontext diff --git a/system/selinux_permissive.py b/system/selinux_permissive.py index e18eb2fbc00..fed5db2bcf2 100644 --- a/system/selinux_permissive.py +++ b/system/selinux_permissive.py @@ -19,6 +19,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: selinux_permissive diff --git a/system/seport.py b/system/seport.py index 0e8c76197b2..bbd049c030c 100644 --- a/system/seport.py +++ b/system/seport.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: seport diff --git a/system/solaris_zone.py b/system/solaris_zone.py index aa569d7e0d2..85e0f41a1ca 100644 --- a/system/solaris_zone.py +++ b/system/solaris_zone.py @@ -22,6 +22,10 @@ import platform import tempfile +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: solaris_zone diff --git a/system/svc.py b/system/svc.py index 43eea5b0f7b..378d647bee9 100755 --- a/system/svc.py +++ b/system/svc.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: svc diff --git a/system/timezone.py b/system/timezone.py index c750611c9be..7d8d9aef76c 100644 --- a/system/timezone.py +++ b/system/timezone.py @@ -24,6 +24,10 @@ from ansible.module_utils.six import iteritems +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: timezone diff --git a/system/ufw.py b/system/ufw.py index c0f48fca667..6d381785bc5 100644 --- a/system/ufw.py +++ b/system/ufw.py @@ -21,6 +21,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ufw diff --git a/system/zfs.py b/system/zfs.py index 7dda883b893..d95971455ed 100644 --- a/system/zfs.py +++ b/system/zfs.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: zfs diff --git a/univention/udm_dns_record.py b/univention/udm_dns_record.py index 5d29b2c3820..92cea504948 100644 --- a/univention/udm_dns_record.py +++ b/univention/udm_dns_record.py @@ -36,6 +36,10 @@ ) +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: udm_dns_record diff --git a/univention/udm_dns_zone.py b/univention/udm_dns_zone.py index dd576f4e574..2d7bbd09070 100644 --- a/univention/udm_dns_zone.py +++ b/univention/udm_dns_zone.py @@ -30,6 +30,10 @@ ) +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: udm_dns_zone diff --git a/univention/udm_group.py b/univention/udm_group.py index 1db030efa98..82ef43faef5 100644 --- a/univention/udm_group.py +++ b/univention/udm_group.py @@ -30,6 +30,10 @@ ) +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: udm_group diff --git a/univention/udm_share.py b/univention/udm_share.py index 2f7febfdd40..7cb472c3141 100644 --- a/univention/udm_share.py +++ b/univention/udm_share.py @@ -30,6 +30,10 @@ ) +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: udm_share diff --git a/univention/udm_user.py b/univention/udm_user.py index ecd6e86546b..ac2d8acb11e 100644 --- a/univention/udm_user.py +++ b/univention/udm_user.py @@ -33,6 +33,10 @@ from dateutil.relativedelta import relativedelta +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: udm_user diff --git a/web_infrastructure/apache2_mod_proxy.py b/web_infrastructure/apache2_mod_proxy.py index 34bc0c80e45..4d2f2c39a8f 100644 --- a/web_infrastructure/apache2_mod_proxy.py +++ b/web_infrastructure/apache2_mod_proxy.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: apache2_mod_proxy diff --git a/web_infrastructure/deploy_helper.py b/web_infrastructure/deploy_helper.py index 6fbdd14151d..a40abda2427 100644 --- a/web_infrastructure/deploy_helper.py +++ b/web_infrastructure/deploy_helper.py @@ -19,6 +19,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: deploy_helper diff --git a/web_infrastructure/ejabberd_user.py b/web_infrastructure/ejabberd_user.py index 989145a36f8..84a8dadbf63 100644 --- a/web_infrastructure/ejabberd_user.py +++ b/web_infrastructure/ejabberd_user.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ejabberd_user diff --git a/web_infrastructure/jboss.py b/web_infrastructure/jboss.py index 8957f1b31de..738b536782d 100644 --- a/web_infrastructure/jboss.py +++ b/web_infrastructure/jboss.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ module: jboss version_added: "1.4" diff --git a/web_infrastructure/jenkins_job.py b/web_infrastructure/jenkins_job.py index af5a28c3e9c..0c91c8b876e 100644 --- a/web_infrastructure/jenkins_job.py +++ b/web_infrastructure/jenkins_job.py @@ -13,6 +13,10 @@ # You should have received a copy of the GNU General Public License # along with this library. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: jenkins_job diff --git a/web_infrastructure/jenkins_plugin.py b/web_infrastructure/jenkins_plugin.py index b08f7541a98..56067c38a60 100644 --- a/web_infrastructure/jenkins_plugin.py +++ b/web_infrastructure/jenkins_plugin.py @@ -32,6 +32,10 @@ import urllib +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: jenkins_plugin diff --git a/web_infrastructure/jira.py b/web_infrastructure/jira.py index 3947f15f32b..aca751801c4 100755 --- a/web_infrastructure/jira.py +++ b/web_infrastructure/jira.py @@ -20,6 +20,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ module: jira version_added: "1.6" diff --git a/web_infrastructure/letsencrypt.py b/web_infrastructure/letsencrypt.py index 751997af2b8..a8541a6d77a 100644 --- a/web_infrastructure/letsencrypt.py +++ b/web_infrastructure/letsencrypt.py @@ -24,6 +24,10 @@ import textwrap from datetime import datetime +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: letsencrypt diff --git a/web_infrastructure/nginx_status_facts.py b/web_infrastructure/nginx_status_facts.py index 970cd3fb5ee..dd2fbd5ee17 100644 --- a/web_infrastructure/nginx_status_facts.py +++ b/web_infrastructure/nginx_status_facts.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nginx_status_facts diff --git a/web_infrastructure/taiga_issue.py b/web_infrastructure/taiga_issue.py index e58c6c0270b..03be0952862 100644 --- a/web_infrastructure/taiga_issue.py +++ b/web_infrastructure/taiga_issue.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: taiga_issue diff --git a/windows/win_acl.py b/windows/win_acl.py index cd25af0a34e..4e6e9cb7ad6 100644 --- a/windows/win_acl.py +++ b/windows/win_acl.py @@ -23,6 +23,10 @@ # this is a windows documentation stub. actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_acl diff --git a/windows/win_acl_inheritance.py b/windows/win_acl_inheritance.py index a4bb90a47b3..549ce629335 100644 --- a/windows/win_acl_inheritance.py +++ b/windows/win_acl_inheritance.py @@ -21,6 +21,10 @@ # this is a windows documentation stub. actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_acl_inheritance diff --git a/windows/win_chocolatey.py b/windows/win_chocolatey.py index d6993502bc1..89e6d73af0e 100644 --- a/windows/win_chocolatey.py +++ b/windows/win_chocolatey.py @@ -21,6 +21,10 @@ # this is a windows documentation stub. actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_chocolatey diff --git a/windows/win_dotnet_ngen.py b/windows/win_dotnet_ngen.py index 75ce9cc138b..9fb7e44e016 100644 --- a/windows/win_dotnet_ngen.py +++ b/windows/win_dotnet_ngen.py @@ -21,6 +21,10 @@ # this is a windows documentation stub. actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_dotnet_ngen diff --git a/windows/win_environment.py b/windows/win_environment.py index 522eff6a8d3..f66771a758d 100644 --- a/windows/win_environment.py +++ b/windows/win_environment.py @@ -21,6 +21,10 @@ # this is a windows documentation stub. actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_environment diff --git a/windows/win_file_version.py b/windows/win_file_version.py index 71aae57135a..f882a4439de 100644 --- a/windows/win_file_version.py +++ b/windows/win_file_version.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_file_version diff --git a/windows/win_firewall_rule.py b/windows/win_firewall_rule.py index 3ed0f7e3e7b..1a5c699f795 100644 --- a/windows/win_firewall_rule.py +++ b/windows/win_firewall_rule.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_firewall_rule diff --git a/windows/win_iis_virtualdirectory.py b/windows/win_iis_virtualdirectory.py index 66810b84071..9388cb9d6be 100644 --- a/windows/win_iis_virtualdirectory.py +++ b/windows/win_iis_virtualdirectory.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_iis_virtualdirectory diff --git a/windows/win_iis_webapplication.py b/windows/win_iis_webapplication.py index b8ebd085162..26177eb90b2 100644 --- a/windows/win_iis_webapplication.py +++ b/windows/win_iis_webapplication.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_iis_webapplication diff --git a/windows/win_iis_webapppool.py b/windows/win_iis_webapppool.py index c77c3b04cb7..e2cb8778b5f 100644 --- a/windows/win_iis_webapppool.py +++ b/windows/win_iis_webapppool.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_iis_webapppool diff --git a/windows/win_iis_webbinding.py b/windows/win_iis_webbinding.py index 0aa1ee12594..c7a08628f48 100644 --- a/windows/win_iis_webbinding.py +++ b/windows/win_iis_webbinding.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_iis_webbinding diff --git a/windows/win_iis_website.py b/windows/win_iis_website.py index b158fb8d8ac..9c65c067c95 100644 --- a/windows/win_iis_website.py +++ b/windows/win_iis_website.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_iis_website diff --git a/windows/win_nssm.py b/windows/win_nssm.py index c0a4332cc3b..57d9dfa3cb5 100644 --- a/windows/win_nssm.py +++ b/windows/win_nssm.py @@ -21,6 +21,10 @@ # this is a windows documentation stub. actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_nssm diff --git a/windows/win_owner.py b/windows/win_owner.py index 1b16c1b727f..b3ad35b40a6 100644 --- a/windows/win_owner.py +++ b/windows/win_owner.py @@ -21,6 +21,10 @@ # this is a windows documentation stub. actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_owner diff --git a/windows/win_package.py b/windows/win_package.py index e8a91176c3e..9c358fcd845 100644 --- a/windows/win_package.py +++ b/windows/win_package.py @@ -21,6 +21,10 @@ # this is a windows documentation stub. actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_package diff --git a/windows/win_regedit.py b/windows/win_regedit.py index d9de288e687..693b4c2f370 100644 --- a/windows/win_regedit.py +++ b/windows/win_regedit.py @@ -21,6 +21,10 @@ # this is a windows documentation stub. actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_regedit diff --git a/windows/win_regmerge.py b/windows/win_regmerge.py index 6507b84b9c2..cefc98029a4 100644 --- a/windows/win_regmerge.py +++ b/windows/win_regmerge.py @@ -21,6 +21,10 @@ # this is a windows documentation stub. actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_regmerge diff --git a/windows/win_robocopy.py b/windows/win_robocopy.py index d627918e521..c29c07604bb 100644 --- a/windows/win_robocopy.py +++ b/windows/win_robocopy.py @@ -21,6 +21,10 @@ # this is a windows documentation stub. actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: win_robocopy diff --git a/windows/win_say.py b/windows/win_say.py index 9fadddeeda8..61fa74b9c87 100644 --- a/windows/win_say.py +++ b/windows/win_say.py @@ -21,6 +21,10 @@ # this is a windows documentation stub. actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_say diff --git a/windows/win_scheduled_task.py b/windows/win_scheduled_task.py index 5428a0b836e..96a9b48f951 100644 --- a/windows/win_scheduled_task.py +++ b/windows/win_scheduled_task.py @@ -18,6 +18,10 @@ # this is a windows documentation stub. actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_scheduled_task diff --git a/windows/win_share.py b/windows/win_share.py index 14608e6e17f..bca7646cf3f 100644 --- a/windows/win_share.py +++ b/windows/win_share.py @@ -21,6 +21,10 @@ # this is a windows documentation stub. actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_share diff --git a/windows/win_timezone.py b/windows/win_timezone.py index 2f7cf1fdc4b..02b9bb9c457 100644 --- a/windows/win_timezone.py +++ b/windows/win_timezone.py @@ -21,6 +21,10 @@ # this is a windows documentation stub. actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_timezone diff --git a/windows/win_unzip.py b/windows/win_unzip.py index b24e6c6b29d..708a909820b 100644 --- a/windows/win_unzip.py +++ b/windows/win_unzip.py @@ -21,6 +21,10 @@ # this is a windows documentation stub. actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_unzip diff --git a/windows/win_updates.py b/windows/win_updates.py index 8700126c180..3fa5d0e3278 100644 --- a/windows/win_updates.py +++ b/windows/win_updates.py @@ -21,6 +21,10 @@ # this is a windows documentation stub. actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_updates diff --git a/windows/win_uri.py b/windows/win_uri.py index b5ab0f69cb7..f9e923dfafe 100644 --- a/windows/win_uri.py +++ b/windows/win_uri.py @@ -21,6 +21,10 @@ # this is a windows documentation stub. actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: win_uri diff --git a/windows/win_webpicmd.py b/windows/win_webpicmd.py index 215123cef8c..3fc9d7d4335 100644 --- a/windows/win_webpicmd.py +++ b/windows/win_webpicmd.py @@ -21,6 +21,10 @@ # this is a windows documentation stub. actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_webpicmd From 2bd42f2d35b74431d61aa4d50d4d84195fe6a904 Mon Sep 17 00:00:00 2001 From: James Cammarata Date: Tue, 6 Dec 2016 10:13:13 -0500 Subject: [PATCH 2519/2522] Removing unnecessary files before repo merge --- CONTRIBUTING.md | 37 --- COPYING | 675 ------------------------------------------------ GUIDELINES.md | 72 ------ MAINTAINERS.md | 1 - README.md | 28 -- REVIEWERS.md | 58 ----- VERSION | 1 - __init__.py | 0 shippable.yml | 65 ----- 9 files changed, 937 deletions(-) delete mode 100644 CONTRIBUTING.md delete mode 100644 COPYING delete mode 100644 GUIDELINES.md delete mode 100644 MAINTAINERS.md delete mode 100644 README.md delete mode 100644 REVIEWERS.md delete mode 100644 VERSION delete mode 100644 __init__.py delete mode 100644 shippable.yml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 60e850d6ed3..00000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,37 +0,0 @@ -Contributing to ansible-modules-extras -====================================== - -The Ansible Extras Modules are written and maintained by the Ansible community, according to the following contribution guidelines. - -If you'd like to contribute code -================================ - -Please see [this web page](http://docs.ansible.com/community.html) for information about the contribution process. Important license agreement information is also included on that page. - -If you'd like to contribute code to an existing module -====================================================== -Each module in Extras is maintained by the owner of that module; each module's owner is indicated in the documentation section of the module itself. Any pull request for a module that is given a "shipit" by the owner in the comments will be merged by the Ansible team. - -If you'd like to contribute a new module -======================================== -Ansible welcomes new modules. Please be certain that you've read the [module maintainer guide and standards](./GUIDELINES.md) thoroughly before submitting your module. - -The Ansible community reviews new modules as often as possible, but please be patient; there are a lot of new module submissions in the pipeline, and it takes time to evaluate a new module for its adherence to module standards. - -Once your module is accepted, you become responsible for maintenance of that module, which means responding to pull requests and issues in a reasonably timely manner. - -If you'd like to ask a question -=============================== - -Please see [this web page ](http://docs.ansible.com/community.html) for community information, which includes pointers on how to ask questions on the [mailing lists](http://docs.ansible.com/community.html#mailing-list-information) and IRC. - -The Github issue tracker is not the best place for questions for various reasons, but both IRC and the mailing list are very helpful places for those things, and that page has the pointers to those. - -If you'd like to file a bug -=========================== - -Read the community page above, but in particular, make sure you copy [this issue template](https://github.com/ansible/ansible-modules-extras/blob/devel/.github/ISSUE_TEMPLATE.md) into your ticket description. We have a friendly neighborhood bot that will remind you if you forget :) This template helps us organize tickets faster and prevents asking some repeated questions, so it's very helpful to us and we appreciate your help with it. - -Also please make sure you are testing on the latest released version of Ansible or the development branch. - -Thanks! diff --git a/COPYING b/COPYING deleted file mode 100644 index 10926e87f11..00000000000 --- a/COPYING +++ /dev/null @@ -1,675 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. - diff --git a/GUIDELINES.md b/GUIDELINES.md deleted file mode 100644 index 0096f319295..00000000000 --- a/GUIDELINES.md +++ /dev/null @@ -1,72 +0,0 @@ -# Module Maintainer Guidelines - -Thank you for being a maintainer of one of the modules in ansible-modules-extras! This guide provides module maintainers an overview of their responsibilities, resources for additional information, and links to helpful tools. - -In addition to the information below, module maintainers should be familiar with: -* General Ansible community development practices (http://docs.ansible.com/ansible/community.html) -* Documentation on module development (http://docs.ansible.com/ansible/developing_modules.html) -* Any namespace-specific module guidelines (identified as GUIDELINES.md in the appropriate file tree). - -*** - -# Maintainer Responsibilities - -When you contribute a new module to the ansible-modules-extras repository, you become the maintainer for that module once it has been merged. Maintainership empowers you with the authority to accept, reject, or request revisions to pull requests on your module -- but as they say, "with great power comes great responsibility." - -Maintainers of Ansible modules are expected to provide feedback, responses, or actions on pull requests or issues to the module(s) they maintain in a reasonably timely manner. - -It is also recommended that you occasionally revisit the [contribution guidelines](https://github.com/alikins/ansible-modules-extras/commit/c87795da5b0c95c67fea1608a5a2a4ec54cb3905), as they are continually refined. Occasionally, you may be requested to update your module to move it closer to the general accepted standard requirements; we hope for this to be infrequent, and will always be a request with a fair amount of lead time (ie: not by tomorrow!). - -Finally, following the ansible-devel mailing list can be a great way to participate in the broader Ansible community, and a place where you can influence the overall direction, quality, and goals of the Extras modules repository. If you're not on this relatively low-volume list, please join us here: https://groups.google.com/forum/#!forum/ansible-devel - -The Ansible community hopes that you will find that maintaining your module is as rewarding for you as having the module is for the wider community. - -*** - -# Pull Requests, Issues, and Workflow - -## Pull Requests - -Module pull requests are located in the [ansible-modules-extras repository](https://github.com/ansible/ansible-modules-extras/pulls). - -Because of the high volume of pull requests, notification of PRs to specific modules are routed by an automated bot to the appropriate maintainer for handling. It is recommended that you set an appropriate notification process to receive notifications which mention your GitHub ID. - -## Issues - -Issues for modules, including bug reports, documentation bug reports, and feature requests, are tracked in the [ansible-modules-extras repository](https://github.com/ansible/ansible-modules-extras/issues). - - Issues for modules are routed to their maintainers via an automated process. This process is still being refined, and currently depends upon the issue creator to provide adequate details (specifically, providing the proper module name) in order to route it correctly. If you are a maintainer of a specific module, it is recommended that you periodically search module issues for issues which mention your module's name (or some variation on that name), as well as setting an appropriate notification process for receiving notification of mentions of your GitHub ID. - -## PR Workflow - -Automated routing of pull requests is handled by a tool called [Ansibullbot](https://github.com/ansible/ansibullbot). (You could say that he moooo-ves things around.) - -Being moderately familiar with how the workflow behind the bot operates can be helpful to you, and -- should things go awry -- your feedback can be helpful to the folks that continually help Ansibullbot to evolve. - -A detailed explanation of the PR workflow can be seen here: https://github.com/ansible/community/blob/master/PR-FLOW.md - -*** - -# Extras maintainers list - -The full list of maintainers for modules in ansible-modules-extras is located here: -https://github.com/ansible/ansibullbot/blob/master/MAINTAINERS-EXTRAS.txt - -## Changing Maintainership - -Communities change over time, and no one maintains a module forever. If you'd like to propose an additional maintainer for your module, please submit a PR to the maintainers file with the Github ID of the new maintainer. - -If you'd like to step down as a maintainer, please submit a PR to the maintainers file removing your Github ID from the module in question. If that would leave the module with no maintainers, put "ansible" as the maintainer. This will indicate that the module is temporarily without a maintainer, and the Ansible community team will search for a new maintainer. - -*** - -# Tools and other Resources - -## Useful tools -* https://ansible.sivel.net/pr/byfile.html -- a full list of all open Pull Requests, organized by file. - -## Other Resources - -* Module maintainer list: https://github.com/ansible/ansibullbot/blob/master/MAINTAINERS-EXTRAS.txt -* Ansibullbot: https://github.com/ansible/ansibullbot -* Triage / pull request workflow and information, including definitions for Labels in GitHub: https://github.com/ansible/community/blob/master/PR-FLOW.md diff --git a/MAINTAINERS.md b/MAINTAINERS.md deleted file mode 100644 index c4370110e93..00000000000 --- a/MAINTAINERS.md +++ /dev/null @@ -1 +0,0 @@ -Please refer to [GUIDELINES.md](./GUIDELINES.md) for the updated contributor guidelines. diff --git a/README.md b/README.md deleted file mode 100644 index 7b860ba7145..00000000000 --- a/README.md +++ /dev/null @@ -1,28 +0,0 @@ -[![Build Status](https://api.shippable.com/projects/573f79d02a8192902e20e34f/badge?branch=devel)](https://app.shippable.com/projects/573f79d02a8192902e20e34f) - -ansible-modules-extras -====================== - -This repo contains a subset of ansible-modules with slightly lower use or priority than "core" modules. - -All new modules should be submitted here, and have a chance to be promoted to core over time. - -Reporting bugs -============== - -Take care to submit tickets to the appropriate repo where modules are contained. The repo is mentioned at the bottom of module documentation page at [docs.ansible.com](http://docs.ansible.com/). - -Testing modules -=============== - -Ansible [module development guide](http://docs.ansible.com/developing_modules.html#testing-modules) contains the latest info about that. - -License -======= - -As with Ansible, modules distributed with Ansible are GPLv3 licensed. User generated modules not part of this project can be of any license. - -Installation -============ - -There should be no need to install this repo separately as it should be included in any Ansible install using the official documented methods. diff --git a/REVIEWERS.md b/REVIEWERS.md deleted file mode 100644 index fe7392d7f04..00000000000 --- a/REVIEWERS.md +++ /dev/null @@ -1,58 +0,0 @@ -Ansible Extras Reviewers -==================== -The Ansible Extras Modules are written and maintained by the Ansible community, and are included in Extras through a community-driven approval process. - -Expectations -======= - -1. New modules will be tested in good faith by users who care about them. -2. New modules will adhere to the module guidelines, located here: http://docs.ansible.com/ansible/developing_modules.html#module-checklist -3. The submitter of the module is willing and able to maintain the module over time. - -New Modules -======= - -New modules are subject to review by anyone in the Ansible community. For inclusion of a new module into Ansible Extras, a pull request must receive at least one approval from a fellow community member on each of the following criteria: - -* One "worksforme" approval from someone who has thoroughly tested the module, including all parameters and switches. -* One "passes_guidelines" approval from someone who has vetted the code according to the module guidelines. - -Either of these approvals can be given, in a comment, by anybody (except the submitter). - -Any module that has both of these, and no "needs_revision" votes (which can also be given by anybody) will be approved for inclusion. - -The core team will continue to be the point of escalation for any issues that may arise (duplicate modules, disagreements over guidelines, etc.) - -Existing Modules -======= - -PRs made against existing modules in Extras are subject to review by the module author or current maintainer. - -Unmaintained Modules -======= - -If modules in Extras go unmaintained, we will seek new maintainers, and if we don't find new -maintainers, we will ultimately deprecate them. - -Subject Matter Experts -======= - -Subject matter experts are groups of acknowledged community members who have expertise and experience in particular modules. Pull requests for existing or new modules are sometimes referred to these wider groups during triage, for expedience or escalation. - -Openstack: @emonty @shrews @dguerri @juliakreger @j2sol @rcarrillocruz - -Windows: @trondhindenes @petemounce @elventear @smadam813 @jhawkesworth @angstwad @sivel @chrishoffman @cchurch - -AWS: @jsmartin @scicoin-project @tombamford @garethr @jarv @jsdalton @silviud @adq @zbal @zeekin @willthames @lwade @carsongee @defionscode @tastychutney @bpennypacker @loia - -Docker: @cove @joshuaconner @softzilla @smashwilson - -Red Hat Network: @barnabycourt @vritant @flossware - -Zabbix: @cove @harrisongu @abulimov - -PR Process -======= - -A full view of the pull request process for Extras can be seen here: -![here](http://gregdek.org/extras_PR_process_2015_09.png) diff --git a/VERSION b/VERSION deleted file mode 100644 index 47c909bbc53..00000000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -2.0.0-0.5.beta3 diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/shippable.yml b/shippable.yml deleted file mode 100644 index 23d731058e6..00000000000 --- a/shippable.yml +++ /dev/null @@ -1,65 +0,0 @@ -language: python - -env: - matrix: - - TEST=none - -matrix: - exclude: - - env: TEST=none - include: - - env: TEST=integration IMAGE=ansible/ansible:centos6 - - env: TEST=integration IMAGE=ansible/ansible:centos7 PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:fedora-rawhide PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:fedora23 PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:opensuseleap PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:ubuntu1204 PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:ubuntu1404 PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604 PRIVILEGED=true - - - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604py3 PYTHON3=1 PRIVILEGED=true - - - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604py3 PYTHON3=1 - - - env: TEST=integration PLATFORM=windows VERSION=2008-SP2 - - env: TEST=integration PLATFORM=windows VERSION=2008-R2_SP1 - - env: TEST=integration PLATFORM=windows VERSION=2012-RTM - - env: TEST=integration PLATFORM=windows VERSION=2012-R2_RTM - - - env: TEST=integration PLATFORM=freebsd VERSION=10.3-STABLE PRIVILEGED=true - - - env: TEST=integration PLATFORM=osx VERSION=10.11 - - - env: TEST=sanity INSTALL_DEPS=1 - - - env: TEST=docs -build: - pre_ci_boot: - options: "--privileged=false --net=bridge" - ci: - - test/utils/shippable/ci.sh - -integrations: - notifications: - - integrationName: email - type: email - on_success: never - on_failure: never - on_start: never - on_pull_request: never - - integrationName: irc - type: irc - recipients: - - "chat.freenode.net#ansible-notices" - on_success: change - on_failure: always - on_start: never - on_pull_request: always - - integrationName: slack - type: slack - recipients: - - "#shippable" - on_success: change - on_failure: always - on_start: never - on_pull_request: never From 240c0ae65040df01192f9168e79fa67cb361fab5 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Tue, 6 Dec 2016 08:37:08 -0800 Subject: [PATCH 2520/2522] Add metadata for xbps Pass pyflakes --- packaging/os/xbps.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packaging/os/xbps.py b/packaging/os/xbps.py index ad3a90b9395..0bfe678ab89 100644 --- a/packaging/os/xbps.py +++ b/packaging/os/xbps.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: xbps @@ -94,6 +98,8 @@ import os +from ansible.module_utils.basic import AnsibleModule + def is_installed(xbps_output): """Returns package install state""" @@ -293,8 +299,5 @@ def main(): remove_packages(module, xbps_path, pkgs) -# import module snippets -from ansible.module_utils.basic import * - if __name__ == "__main__": main() From 6a4944e8ad1b1683cd3c9dd1cef22119e09d2f20 Mon Sep 17 00:00:00 2001 From: jctanner Date: Fri, 9 Dec 2016 10:52:33 -0500 Subject: [PATCH 2521/2522] Update templates for repomerge (#3689) --- .github/ISSUE_TEMPLATE.md | 56 ++------------------------------ .github/PULL_REQUEST_TEMPLATE.md | 29 ++--------------- 2 files changed, 6 insertions(+), 79 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 7cc5b860273..300886a6973 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,55 +1,5 @@ - +# This repository is locked -##### ISSUE TYPE - - - Bug Report - - Feature Idea - - Documentation Report +Please open all new issues and pull requests in https://github.com/ansible/ansible -##### COMPONENT NAME - - -##### ANSIBLE VERSION - -``` - -``` - -##### CONFIGURATION - - -##### OS / ENVIRONMENT - - -##### SUMMARY - - -##### STEPS TO REPRODUCE - - - -``` - -``` - - - -##### EXPECTED RESULTS - - -##### ACTUAL RESULTS - - - -``` - -``` +For more information please see http://docs.ansible.com/ansible/dev_guide/repomerge.html diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 5cfd027103a..300886a6973 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,28 +1,5 @@ -##### ISSUE TYPE - - - Feature Pull Request - - New Module Pull Request - - Bugfix Pull Request - - Docs Pull Request +# This repository is locked -##### COMPONENT NAME - +Please open all new issues and pull requests in https://github.com/ansible/ansible -##### ANSIBLE VERSION - -``` - -``` - -##### SUMMARY - - - - - -``` - -``` +For more information please see http://docs.ansible.com/ansible/dev_guide/repomerge.html From f216ba8e0616bc8ad8794c22d4b48e1ab18886cf Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Tue, 13 Dec 2016 17:14:03 -0800 Subject: [PATCH 2522/2522] Add README.md explaining repo merge. --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000000..3bb1f395c56 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +**NOTE:** As of Ansible 2.3, modules are now in the +[main Ansible repository](https://github.com/ansible/ansible/tree/devel/lib/ansible/modules). + +See the [repo merge guide](https://docs.ansible.com/ansible/dev_guide/repomerge.html) for more information. + +This repo still exists to allow bug fixes for `stable-2.2` and older releases.